From 809f16c27edd15566091e583331cb34b4206a018 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sat, 6 Sep 2025 15:18:53 -0400 Subject: [PATCH 01/54] Dock: improvements, new animations, always float, better look. --- Commons/Style.qml | 1 + Modules/Dock/Dock.qml | 130 ++++++++++++++++++++++++------------------ 2 files changed, 77 insertions(+), 54 deletions(-) diff --git a/Commons/Style.qml b/Commons/Style.qml index 902a225..a3a7d0f 100644 --- a/Commons/Style.qml +++ b/Commons/Style.qml @@ -60,6 +60,7 @@ Singleton { property int animationFast: Math.round(150 * Settings.data.general.animationSpeed) property int animationNormal: Math.round(300 * Settings.data.general.animationSpeed) property int animationSlow: Math.round(450 * Settings.data.general.animationSpeed) + property int animationSlowest: Math.round(750 * Settings.data.general.animationSpeed) // Dimensions property int barHeight: 36 diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml index 9a71bba..2e34ca0 100644 --- a/Modules/Dock/Dock.qml +++ b/Modules/Dock/Dock.qml @@ -34,24 +34,28 @@ Variants { WlrLayershell.namespace: "noctalia-dock" - property bool autoHide: Settings.data.dock.autoHide - property bool hidden: autoHide - property int hideDelay: 500 - property int showDelay: 100 - property int hideAnimationDuration: Style.animationFast - property int showAnimationDuration: Style.animationFast - property int peekHeight: 7 * scaling - property int fullHeight: dockContainer.height - property int iconSize: 36 + readonly property bool autoHide: Settings.data.dock.autoHide + readonly property int hideDelay: 500 + readonly property int showDelay: 100 + readonly property int hideAnimationDuration: Style.animationFast + readonly property int showAnimationDuration: Style.animationFast + readonly property int peekHeight: 7 * scaling + readonly property int fullHeight: dockContainer.height + readonly property int iconSize: 36 * scaling + readonly property int floatingMargin: 12 * scaling // Margin to make dock float - // Bar positioning properties - property bool barAtBottom: Settings.data.bar.position === "bottom" - property int barHeight: barAtBottom ? (Settings.data.bar.height || 30) * scaling : 0 - property int dockSpacing: 4 * scaling // Space between dock and bar + // Bar detection and positioning properties + readonly property bool hasBar: modelData.name ? (Settings.data.bar.monitors.includes(modelData.name) + || (Settings.data.bar.monitors.length === 0)) : false + readonly property bool barAtBottom: hasBar && Settings.data.bar.position === "bottom" + readonly property bool barAtTop: hasBar && Settings.data.bar.position === "top" + readonly property int barHeight: (barAtBottom || barAtTop) ? (Settings.data.bar.height || 30) * scaling : 0 + readonly property int dockSpacing: 8 * scaling // Space between dock and bar/edge // Track hover state property bool dockHovered: false property bool anyAppHovered: false + property bool hidden: autoHide // Dock is positioned at the bottom anchors.bottom: true @@ -63,11 +67,11 @@ Variants { // Make the window transparent color: Color.transparent - // Set the window size - always include space for peek area when auto-hide is enabled - implicitWidth: dockContainer.width - implicitHeight: fullHeight + (barAtBottom ? barHeight + dockSpacing : 0) + // Set the window size - include extra height only if bar is at bottom + implicitWidth: dockContainer.width + (floatingMargin * 2) + implicitHeight: fullHeight + floatingMargin + (barAtBottom ? barHeight + dockSpacing : 0) - // Position the entire window above the bar when bar is at bottom + // Position the entire window above the bar only when bar is at bottom margins.bottom: barAtBottom ? barHeight : 0 // Watch for autoHide setting changes @@ -111,7 +115,7 @@ Variants { anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right - height: peekHeight + dockSpacing + height: peekHeight + floatingMargin + (barAtBottom ? dockSpacing : 0) hoverEnabled: autoHide visible: autoHide @@ -130,24 +134,32 @@ Variants { Rectangle { id: dockContainer - width: dockLayout.implicitWidth + 48 * scaling - height: iconSize * 1.4 * scaling + width: dockLayout.implicitWidth + Style.marginL * scaling * 2 + height: Math.round(iconSize * 1.6) color: Qt.alpha(Color.mSurface, Settings.data.dock.backgroundOpacity) anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom - anchors.bottomMargin: dockSpacing - topLeftRadius: Style.radiusL * scaling - topRightRadius: Style.radiusL * scaling + anchors.bottomMargin: floatingMargin + (barAtBottom ? dockSpacing : 0) + radius: Style.radiusL * scaling + border.width: Math.max(1, Style.borderS * scaling) + border.color: Color.mOutline - // Animate the dock sliding up and down - transform: Translate { - y: hidden ? (fullHeight - peekHeight) : 0 + // Fade and zoom animation properties + opacity: hidden ? 0 : 1 + scale: hidden ? 0.85 : 1 - Behavior on y { - NumberAnimation { - duration: hidden ? hideAnimationDuration : showAnimationDuration - easing.type: Easing.InOutQuad - } + Behavior on opacity { + NumberAnimation { + duration: hidden ? hideAnimationDuration : showAnimationDuration + easing.type: Easing.InOutQuad + } + } + + Behavior on scale { + NumberAnimation { + duration: hidden ? hideAnimationDuration : showAnimationDuration + easing.type: hidden ? Easing.InQuad : Easing.OutBack + easing.overshoot: hidden ? 0 : 1.05 } } @@ -179,7 +191,7 @@ Variants { Item { id: dock width: dockLayout.implicitWidth - height: parent.height - (20 * scaling) + height: parent.height - (Style.marginM * 2 * scaling) anchors.centerIn: parent NTooltip { @@ -203,39 +215,40 @@ Variants { Repeater { model: ToplevelManager ? ToplevelManager.toplevels : null - delegate: Rectangle { + delegate: Item { id: appButton - Layout.preferredWidth: iconSize * scaling - Layout.preferredHeight: iconSize * scaling + Layout.preferredWidth: iconSize + Layout.preferredHeight: iconSize Layout.alignment: Qt.AlignCenter - color: Color.transparent - radius: Style.radiusM * scaling - property bool isActive: ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData property bool hovered: appMouseArea.containsMouse property string appId: modelData ? modelData.appId : "" property string appTitle: modelData ? modelData.title : "" - // The icon + // The icon with better quality settings Image { id: appIcon - width: iconSize * scaling - height: iconSize * scaling + width: iconSize + height: iconSize anchors.centerIn: parent source: dock.getAppIcon(modelData) visible: source.toString() !== "" + sourceSize.width: iconSize * 2 + sourceSize.height: iconSize * 2 smooth: true - mipmap: false - antialiasing: false + mipmap: true + antialiasing: true fillMode: Image.PreserveAspectFit + cache: true - scale: appButton.hovered ? 1.1 : 1.0 + scale: appButton.hovered ? 1.15 : 1.0 Behavior on scale { NumberAnimation { - duration: Style.animationFast + duration: Style.animationNormal easing.type: Easing.OutBack + easing.overshoot: 1.2 } } } @@ -246,15 +259,15 @@ Variants { visible: !appIcon.visible text: "question_mark" font.family: "Material Symbols Rounded" - font.pointSize: iconSize * 0.7 * scaling + font.pointSize: iconSize * 0.7 color: appButton.isActive ? Color.mPrimary : Color.mOnSurfaceVariant - - scale: appButton.hovered ? 1.1 : 1.0 + scale: appButton.hovered ? 1.15 : 1.0 Behavior on scale { NumberAnimation { duration: Style.animationFast easing.type: Easing.OutBack + easing.overshoot: 1.2 } } } @@ -269,8 +282,8 @@ Variants { onEntered: { anyAppHovered = true const appName = appButton.appTitle || appButton.appId || "Unknown" - appTooltip.text = appName.length > 40 ? appName.substring(0, 37) + "..." : appName appTooltip.target = appButton + appTooltip.text = appName.length > 40 ? appName.substring(0, 37) + "..." : appName appTooltip.isVisible = true if (autoHide) { showTimer.stop() @@ -300,15 +313,24 @@ Variants { } } + // Active indicator Rectangle { visible: isActive - width: iconSize * 0.25 - height: 4 * scaling + width: iconSize * 0.2 + height: iconSize * 0.1 color: Color.mPrimary - radius: Style.radiusXS + radius: Style.radiusXS * scaling anchors.top: parent.bottom anchors.horizontalCenter: parent.horizontalCenter - anchors.topMargin: Style.marginXXS * scaling + anchors.topMargin: Style.marginXXS * 1.5 * scaling + + // Pulse animation for active indicator + SequentialAnimation on opacity { + running: isActive + loops: Animation.Infinite + NumberAnimation { to: 0.6; duration: Style.animationSlowest; easing.type: Easing.InOutQuad } + NumberAnimation { to: 1.0; duration: Style.animationSlowest; easing.type: Easing.InOutQuad } + } } } } @@ -317,4 +339,4 @@ Variants { } } } -} +} \ No newline at end of file From ac43b6d78aaec8385ad35ee84664a9d5ff2d5f8c Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sat, 6 Sep 2025 15:19:06 -0400 Subject: [PATCH 02/54] Dock: autoformatting --- Modules/Dock/Dock.qml | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml index 2e34ca0..0795dfa 100644 --- a/Modules/Dock/Dock.qml +++ b/Modules/Dock/Dock.qml @@ -45,8 +45,8 @@ Variants { readonly property int floatingMargin: 12 * scaling // Margin to make dock float // Bar detection and positioning properties - readonly property bool hasBar: modelData.name ? (Settings.data.bar.monitors.includes(modelData.name) - || (Settings.data.bar.monitors.length === 0)) : false + readonly property bool hasBar: modelData.name ? (Settings.data.bar.monitors.includes(modelData.name) + || (Settings.data.bar.monitors.length === 0)) : false readonly property bool barAtBottom: hasBar && Settings.data.bar.position === "bottom" readonly property bool barAtTop: hasBar && Settings.data.bar.position === "top" readonly property int barHeight: (barAtBottom || barAtTop) ? (Settings.data.bar.height || 30) * scaling : 0 @@ -191,7 +191,7 @@ Variants { Item { id: dock width: dockLayout.implicitWidth - height: parent.height - (Style.marginM * 2 * scaling) + height: parent.height - (Style.marginM * 2 * scaling) anchors.centerIn: parent NTooltip { @@ -235,7 +235,7 @@ Variants { source: dock.getAppIcon(modelData) visible: source.toString() !== "" sourceSize.width: iconSize * 2 - sourceSize.height: iconSize * 2 + sourceSize.height: iconSize * 2 smooth: true mipmap: true antialiasing: true @@ -328,8 +328,16 @@ Variants { SequentialAnimation on opacity { running: isActive loops: Animation.Infinite - NumberAnimation { to: 0.6; duration: Style.animationSlowest; easing.type: Easing.InOutQuad } - NumberAnimation { to: 1.0; duration: Style.animationSlowest; easing.type: Easing.InOutQuad } + NumberAnimation { + to: 0.6 + duration: Style.animationSlowest + easing.type: Easing.InOutQuad + } + NumberAnimation { + to: 1.0 + duration: Style.animationSlowest + easing.type: Easing.InOutQuad + } } } } @@ -339,4 +347,4 @@ Variants { } } } -} \ No newline at end of file +} From 1bb1015fdf7aa7e0963e927d3b084f4a97293abf Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sat, 6 Sep 2025 15:25:57 -0400 Subject: [PATCH 03/54] Dock: one tooltip per app instead of a shared tooltip. avoid a few glitches when hovering. --- Modules/Dock/Dock.qml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml index 0795dfa..04d64f7 100644 --- a/Modules/Dock/Dock.qml +++ b/Modules/Dock/Dock.qml @@ -194,12 +194,6 @@ Variants { height: parent.height - (Style.marginM * 2 * scaling) anchors.centerIn: parent - NTooltip { - id: appTooltip - visible: false - positionAbove: true - } - function getAppIcon(toplevel: Toplevel): string { if (!toplevel) return "" @@ -226,6 +220,14 @@ Variants { property string appId: modelData ? modelData.appId : "" property string appTitle: modelData ? modelData.title : "" + // Individual tooltip for this app + NTooltip { + id: appTooltip + target: appButton + positionAbove: true + visible: false + } + // The icon with better quality settings Image { id: appIcon @@ -282,7 +284,6 @@ Variants { onEntered: { anyAppHovered = true const appName = appButton.appTitle || appButton.appId || "Unknown" - appTooltip.target = appButton appTooltip.text = appName.length > 40 ? appName.substring(0, 37) + "..." : appName appTooltip.isVisible = true if (autoHide) { From 86c6135def476ec87210654641c0b89b5848515c Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sat, 6 Sep 2025 16:11:16 -0400 Subject: [PATCH 04/54] Network/Wi-Fi: improvements - Always check for ethernet status every 30s. Should not affect battery life. - Less aggressive scan intervals to give more times for slow adapters. --- Modules/Bar/Widgets/WiFi.qml | 2 +- Services/NetworkService.qml | 30 ++++++++++++++++-------------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/Modules/Bar/Widgets/WiFi.qml b/Modules/Bar/Widgets/WiFi.qml index 77f8664..fe8ff75 100644 --- a/Modules/Bar/Widgets/WiFi.qml +++ b/Modules/Bar/Widgets/WiFi.qml @@ -40,6 +40,6 @@ NIconButton { return "signal_wifi_bad" } } - tooltipText: "Network / Wi-Fi." + tooltipText: "Manage Wi-Fi." onClicked: PanelService.getPanel("wifiPanel")?.toggle(screen, this) } diff --git a/Services/NetworkService.qml b/Services/NetworkService.qml index c4b5820..ee22010 100644 --- a/Services/NetworkService.qml +++ b/Services/NetworkService.qml @@ -54,7 +54,7 @@ Singleton { Component.onCompleted: { Logger.log("Network", "Service initialized") syncWifiState() - refresh() + scan() } // Save cache with debounce @@ -75,6 +75,16 @@ Singleton { onTriggered: scan() } + // Ethernet check timer + // Always running every 30s + Timer { + id: ethernetCheckTimer + interval: 30000 + running: true + repeat: true + onTriggered: ethernetStateProcess.running = true + } + // Core functions function syncWifiState() { wifiStateProcess.running = true @@ -87,14 +97,6 @@ Singleton { wifiToggleProcess.running = true } - function refresh() { - ethernetStateProcess.running = true - - if (Settings.data.network.wifiEnabled) { - scan() - } - } - function scan() { if (scanning) return @@ -206,7 +208,7 @@ Singleton { // Processes Process { id: ethernetStateProcess - running: false + running: true command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"] stdout: StdioCollector { @@ -408,7 +410,7 @@ Singleton { Logger.log("Network", `Connected to network: "${connectProcess.ssid}"`) // Still do a scan to get accurate signal and security info - delayedScanTimer.interval = 1000 + delayedScanTimer.interval = 5000 delayedScanTimer.restart() } } @@ -522,8 +524,8 @@ Singleton { root.forgettingNetwork = "" - // Quick scan to verify the profile is gone - delayedScanTimer.interval = 500 + // Scan to verify the profile is gone + delayedScanTimer.interval = 5000 delayedScanTimer.restart() } } @@ -535,7 +537,7 @@ Singleton { Logger.warn("Network", "Forget error: " + text) } // Still Trigger a scan even on error - delayedScanTimer.interval = 500 + delayedScanTimer.interval = 1000 delayedScanTimer.restart() } } From 56993d3c00152ab387e399d80d42a9cb7177114e Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sat, 6 Sep 2025 18:16:59 -0400 Subject: [PATCH 05/54] Battery: Minimal BatteryService which only serve an appropriate icon. Trying different icons rotated 90 degrees to the left. --- Modules/Bar/Widgets/Battery.qml | 49 +++++++------------------ Modules/LockScreen/LockScreen.qml | 61 ++++++++++--------------------- Services/BatteryService.qml | 49 +++++++++++++++++++++++++ Widgets/NPill.qml | 3 ++ 4 files changed, 85 insertions(+), 77 deletions(-) create mode 100644 Services/BatteryService.qml diff --git a/Modules/Bar/Widgets/Battery.qml b/Modules/Bar/Widgets/Battery.qml index 22d8602..b4654fe 100644 --- a/Modules/Bar/Widgets/Battery.qml +++ b/Modules/Bar/Widgets/Battery.qml @@ -65,40 +65,17 @@ Item { // Test mode property bool testMode: false - property int testPercent: 20 - property bool testCharging: false + property int testPercent: 50 + property bool testCharging: true property var battery: UPower.displayDevice property bool isReady: testMode ? true : (battery && battery.ready && battery.isLaptopBattery && battery.isPresent) property real percent: testMode ? testPercent : (isReady ? (battery.percentage * 100) : 0) property bool charging: testMode ? testCharging : (isReady ? battery.state === UPowerDeviceState.Charging : false) - // Choose icon based on charge and charging state - function batteryIcon() { - if (!isReady || !battery.isLaptopBattery) - return "battery_android_alert" - if (charging) - return "battery_android_bolt" - if (percent >= 95) - return "battery_android_full" - // Hardcoded battery symbols - if (percent >= 85) - return "battery_android_6" - if (percent >= 70) - return "battery_android_5" - if (percent >= 55) - return "battery_android_4" - if (percent >= 40) - return "battery_android_3" - if (percent >= 25) - return "battery_android_2" - if (percent >= 10) - return "battery_android_1" - if (percent >= 0) - return "battery_android_0" - } - rightOpen: BarWidgetRegistry.getNPillDirection(root) - icon: batteryIcon() + icon: testMode ? BatteryService.getIcon(testPercent, testCharging, true) : BatteryService.getIcon(percent, + charging, isReady) + iconRotation: -90 text: ((isReady && battery.isLaptopBattery) || testMode) ? Math.round(percent) + "%" : "-" textColor: charging ? Color.mPrimary : Color.mOnSurface iconCircleColor: Color.mPrimary @@ -109,30 +86,30 @@ Item { tooltipText: { let lines = [] if (testMode) { - lines.push("Time left: " + Time.formatVagueHumanReadableDuration(12345)) + lines.push(`Time left: ${Time.formatVagueHumanReadableDuration(12345)}.`) return lines.join("\n") } if (!isReady || !battery.isLaptopBattery) { - return "No battery detected" + return "No battery detected." } if (battery.timeToEmpty > 0) { - lines.push("Time left: " + Time.formatVagueHumanReadableDuration(battery.timeToEmpty)) + lines.push(`Time left: ${Time.formatVagueHumanReadableDuration(battery.timeToEmpty)}.`) } if (battery.timeToFull > 0) { - lines.push("Time until full: " + Time.formatVagueHumanReadableDuration(battery.timeToFull)) + lines.push(`Time until full: ${Time.formatVagueHumanReadableDuration(battery.timeToFull)}.`) } if (battery.changeRate !== undefined) { const rate = battery.changeRate if (rate > 0) { - lines.push(charging ? "Charging rate: " + rate.toFixed(2) + " W" : "Discharging rate: " + rate.toFixed( - 2) + " W") + lines.push(charging ? "Charging rate: " + rate.toFixed(2) + " W." : "Discharging rate: " + rate.toFixed( + 2) + " W.") } else if (rate < 0) { - lines.push("Discharging rate: " + Math.abs(rate).toFixed(2) + " W") + lines.push("Discharging rate: " + Math.abs(rate).toFixed(2) + " W.") } else { lines.push("Estimating...") } } else { - lines.push(charging ? "Charging" : "Discharging") + lines.push(charging ? "Charging." : "Discharging.") } if (battery.healthPercentage !== undefined && battery.healthPercentage > 0) { lines.push("Health: " + Math.round(battery.healthPercentage) + "%") diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index 3382b69..25b69b1 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -58,29 +58,6 @@ Loader { property real percent: isReady ? (battery.percentage * 100) : 0 property bool charging: isReady ? battery.state === UPowerDeviceState.Charging : false property bool batteryVisible: isReady && percent > 0 - - function getIcon() { - if (!batteryVisible) - return "" - if (charging) - return "battery_android_bolt" - if (percent >= 95) - return "battery_android_full" - if (percent >= 85) - return "battery_android_6" - if (percent >= 70) - return "battery_android_5" - if (percent >= 55) - return "battery_android_4" - if (percent >= 40) - return "battery_android_3" - if (percent >= 25) - return "battery_android_2" - if (percent >= 10) - return "battery_android_1" - if (percent >= 0) - return "battery_android_0" - } } Item { @@ -420,7 +397,7 @@ Loader { anchors.bottomMargin: Style.marginM * scaling anchors.leftMargin: Style.marginL * scaling anchors.rightMargin: Style.marginL * scaling - spacing: Style.marginM * scaling + spacing: Style.marginL * scaling NText { text: "SECURE TERMINAL" @@ -431,23 +408,6 @@ Loader { Layout.fillWidth: true } - RowLayout { - spacing: Style.marginS * scaling - visible: batteryIndicator.batteryVisible - NIcon { - text: batteryIndicator.getIcon() - font.pointSize: Style.fontSizeM * scaling - color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface - } - NText { - text: Math.round(batteryIndicator.percent) + "%" - color: Color.mOnSurface - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeM * scaling - font.weight: Style.fontWeightBold - } - } - RowLayout { spacing: Style.marginS * scaling NText { @@ -463,6 +423,25 @@ Loader { color: Color.mOnSurface } } + + RowLayout { + spacing: Style.marginS * scaling + visible: batteryIndicator.batteryVisible + NIcon { + text: BatteryService.getIcon(batteryIndicator.percent, batteryIndicator.charging, + batteryIndicator.isReady) + font.pointSize: Style.fontSizeM * scaling + color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface + rotation: -90 + } + NText { + text: Math.round(batteryIndicator.percent) + "%" + color: Color.mOnSurface + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeM * scaling + font.weight: Style.fontWeightBold + } + } } } diff --git a/Services/BatteryService.qml b/Services/BatteryService.qml new file mode 100644 index 0000000..6ceb872 --- /dev/null +++ b/Services/BatteryService.qml @@ -0,0 +1,49 @@ +pragma Singleton + +import Quickshell +import Quickshell.Services.UPower + +Singleton { + id: root + + // Choose icon based on charge and charging state + function getIcon(percent, charging, isReady) { + if (!isReady) { + return "battery_error" + } + + if (charging) { + if (percent >= 95) + return "battery_full" + if (percent >= 85) + return "battery_charging_90" + if (percent >= 65) + return "battery_charging_80" + if (percent >= 55) + return "battery_charging_60" + if (percent >= 45) + return "battery_charging_50" + if (percent >= 25) + return "battery_charging_30" + if (percent >= 0) + return "battery_charging_20" + } else { + if (percent >= 95) + return "battery_full" + if (percent >= 85) + return "battery_6_bar" + if (percent >= 70) + return "battery_5_bar" + if (percent >= 55) + return "battery_4_bar" + if (percent >= 40) + return "battery_3_bar" + if (percent >= 25) + return "battery_2_bar" + if (percent >= 10) + return "battery_1_bar" + if (percent >= 0) + return "battery_0_bar" + } + } +} diff --git a/Widgets/NPill.qml b/Widgets/NPill.qml index 2432544..94719b3 100644 --- a/Widgets/NPill.qml +++ b/Widgets/NPill.qml @@ -14,6 +14,8 @@ Item { property color iconCircleColor: Color.mPrimary property color iconTextColor: Color.mSurface property color collapsedIconColor: Color.mOnSurface + + property real iconRotation: 0 property real sizeRatio: 0.8 property bool autoHide: false property bool forceOpen: false @@ -110,6 +112,7 @@ Item { NIcon { text: root.icon + rotation: root.iconRotation font.pointSize: Style.fontSizeM * scaling // When forced shown, use pill text color; otherwise accent color when hovered color: forceOpen ? textColor : (showPill ? iconTextColor : Color.mOnSurface) From 9bc6479c9222e44faa146d7e972ca46d3e5235ad Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sat, 6 Sep 2025 18:34:44 -0400 Subject: [PATCH 06/54] NPill: for battery use a very light outline around the icon --- Widgets/NPill.qml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Widgets/NPill.qml b/Widgets/NPill.qml index 94719b3..794caa9 100644 --- a/Widgets/NPill.qml +++ b/Widgets/NPill.qml @@ -99,6 +99,8 @@ Item { // When forced shown, match pill background; otherwise use accent when hovered color: forceOpen ? pillColor : (showPill ? iconCircleColor : Color.mSurfaceVariant) anchors.verticalCenter: parent.verticalCenter + border.width: Math.max(1, Style.borderS * scaling) + border.color: forceOpen ? Qt.alpha(Color.mOutline, 0.5) : Color.transparent anchors.left: rightOpen ? parent.left : undefined anchors.right: rightOpen ? undefined : parent.right From 36d3a50f217db5be164b7c8e1424c32e27ab8a3a Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sat, 6 Sep 2025 19:27:32 -0400 Subject: [PATCH 07/54] Brightness: brings back realtime brightness monitoring for internal(laptop) display. The pill will open and show the change in real time --- Modules/Bar/Widgets/Brightness.qml | 26 +++++------ Services/BrightnessService.qml | 72 ++++++++++++++++++++++++------ 2 files changed, 70 insertions(+), 28 deletions(-) diff --git a/Modules/Bar/Widgets/Brightness.qml b/Modules/Bar/Widgets/Brightness.qml index 4b6d91a..c16866c 100644 --- a/Modules/Bar/Widgets/Brightness.qml +++ b/Modules/Bar/Widgets/Brightness.qml @@ -37,28 +37,26 @@ Item { target: getMonitor() ignoreUnknownSignals: true function onBrightnessUpdated() { - Logger.log("Bar-Brightness", "OnBrightnessUpdated") - var monitor = getMonitor() - if (!monitor) - return - var currentBrightness = monitor.brightness - - // Ignore if this is the first time or if brightness hasn't actually changed + // Ignore if this is the first time we receive an update. + // Most likely service just kicked off. if (!firstBrightnessReceived) { firstBrightnessReceived = true - monitor.lastBrightness = currentBrightness return } - // Only show pill if brightness actually changed (not just loaded from settings) - if (Math.abs(currentBrightness - monitor.lastBrightness) > 0.1) { - pill.show() - } - - monitor.lastBrightness = currentBrightness + pill.show() + hideTimerAfterChange.restart() } } + Timer { + id: hideTimerAfterChange + interval: 2500 + running: false + repeat: false + onTriggered: pill.hide() + } + NPill { id: pill diff --git a/Services/BrightnessService.qml b/Services/BrightnessService.qml index 6cf1b5d..d14b166 100644 --- a/Services/BrightnessService.qml +++ b/Services/BrightnessService.qml @@ -110,9 +110,43 @@ Singleton { property real lastBrightness: 0 property real queuedBrightness: NaN + // For internal displays - store the backlight device path + property string backlightDevice: "" + property string brightnessPath: "" + property string maxBrightnessPath: "" + property int maxBrightness: 100 + property bool ignoreNextChange: false + // Signal for brightness changes signal brightnessUpdated(real newBrightness) + // FileView to watch for external brightness changes (internal displays only) + readonly property FileView brightnessWatcher: FileView { + id: brightnessWatcher + // Only set path for internal displays with a valid brightness path + path: (!monitor.isDdc && !monitor.isAppleDisplay && monitor.brightnessPath !== "") ? monitor.brightnessPath : "" + watchChanges: path !== "" + onFileChanged: { + reload() + if (monitor.ignoreNextChange) { + monitor.ignoreNextChange = false + return + } + if (text() === "") + return + var current = parseInt(text().trim()) + if (!isNaN(current) && monitor.maxBrightness > 0) { + var newBrightness = current / monitor.maxBrightness + // Only update if it's actually different (avoid feedback loops) + if (Math.abs(newBrightness - monitor.brightness) > 0.01) { + monitor.brightness = newBrightness + monitor.brightnessUpdated(monitor.brightness) + //Logger.log("Brightness", "External change detected:", monitor.modelData.name, monitor.brightness) + } + } + } + } + // Initialize brightness readonly property Process initProc: Process { stdout: StdioCollector { @@ -121,8 +155,8 @@ Singleton { if (dataText === "") { return } - Logger.log("Brightness", "Raw brightness data for", monitor.modelData.name + ":", dataText) + //Logger.log("Brightness", "Raw brightness data for", monitor.modelData.name + ":", dataText) if (monitor.isAppleDisplay) { var val = parseInt(dataText) if (!isNaN(val)) { @@ -140,14 +174,20 @@ Singleton { } } } else { - // Internal backlight - var parts = dataText.split(" ") - if (parts.length >= 2) { - var current = parseInt(parts[0]) - var max = parseInt(parts[1]) + // Internal backlight - parse the response which includes device path + var lines = dataText.split("\n") + if (lines.length >= 3) { + monitor.backlightDevice = lines[0] + monitor.brightnessPath = monitor.backlightDevice + "/brightness" + monitor.maxBrightnessPath = monitor.backlightDevice + "/max_brightness" + + var current = parseInt(lines[1]) + var max = parseInt(lines[2]) if (!isNaN(current) && !isNaN(max) && max > 0) { + monitor.maxBrightness = max monitor.brightness = current / max Logger.log("Brightness", "Internal brightness:", current + "/" + max + " =", monitor.brightness) + Logger.log("Brightness", "Using backlight device:", monitor.backlightDevice) } } } @@ -171,7 +211,7 @@ Singleton { function increaseBrightness(): void { var stepSize = Settings.data.brightness.brightnessStep / 100.0 - setBrightnessDebounced(brightness + stepSize) + setBrightnessDebounced(monitor.brightness + stepSize) } function decreaseBrightness(): void { @@ -183,22 +223,23 @@ Singleton { value = Math.max(0, Math.min(1, value)) var rounded = Math.round(value * 100) - if (Math.round(brightness * 100) === rounded) + if (Math.round(monitor.brightness * 100) === rounded) return if (isDdc && timer.running) { - queuedBrightness = value + monitor.queuedBrightness = value return } - brightness = value - brightnessUpdated(brightness) + monitor.brightness = value + brightnessUpdated(monitor.brightness) if (isAppleDisplay) { Quickshell.execDetached(["asdbctl", "set", rounded]) } else if (isDdc) { Quickshell.execDetached(["ddcutil", "-b", busNum, "setvcp", "10", rounded]) } else { + monitor.ignoreNextChange = true Quickshell.execDetached(["brightnessctl", "s", rounded + "%"]) } @@ -208,7 +249,7 @@ Singleton { } function setBrightnessDebounced(value: real): void { - queuedBrightness = value + monitor.queuedBrightness = value timer.restart() } @@ -218,8 +259,11 @@ Singleton { } else if (isDdc) { initProc.command = ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"] } else { - // Internal backlight - try to find the first available backlight device - initProc.command = ["sh", "-c", "for dev in /sys/class/backlight/*; do if [ -f \"$dev/brightness\" ] && [ -f \"$dev/max_brightness\" ]; then echo \"$(cat $dev/brightness) $(cat $dev/max_brightness)\"; break; fi; done"] + // Internal backlight - find the first available backlight device and get its info + // This now returns: device_path, current_brightness, max_brightness (on separate lines) + initProc.command = ["sh", "-c", "for dev in /sys/class/backlight/*; do " + + " if [ -f \"$dev/brightness\" ] && [ -f \"$dev/max_brightness\" ]; then " + " echo \"$dev\"; " + + " cat \"$dev/brightness\"; " + " cat \"$dev/max_brightness\"; " + " break; " + " fi; " + "done"] } initProc.running = true } From 2bc1d53b18bdd3068ed5d13c2be14465bc72d952 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sat, 6 Sep 2025 20:50:49 -0400 Subject: [PATCH 08/54] SysStat Service: Porting code to JS/QML instead of an external bash --- Modules/Bar/Widgets/SystemMonitor.qml | 2 +- Modules/SidePanel/Cards/SystemMonitorCard.qml | 4 +- Services/SystemStatService.qml | 367 ++++++++++++++++-- 3 files changed, 346 insertions(+), 27 deletions(-) diff --git a/Modules/Bar/Widgets/SystemMonitor.qml b/Modules/Bar/Widgets/SystemMonitor.qml index 6c2346c..e57d599 100644 --- a/Modules/Bar/Widgets/SystemMonitor.qml +++ b/Modules/Bar/Widgets/SystemMonitor.qml @@ -88,7 +88,7 @@ RowLayout { } NText { - text: `${SystemStatService.memoryUsageGb}G` + text: `${SystemStatService.memGb}G` font.family: Settings.data.ui.fontFixed font.pointSize: Style.fontSizeS * scaling font.weight: Style.fontWeightMedium diff --git a/Modules/SidePanel/Cards/SystemMonitorCard.qml b/Modules/SidePanel/Cards/SystemMonitorCard.qml index 2fc18de..9d3154d 100644 --- a/Modules/SidePanel/Cards/SystemMonitorCard.qml +++ b/Modules/SidePanel/Cards/SystemMonitorCard.qml @@ -40,7 +40,7 @@ NBox { height: 68 * scaling } NCircleStat { - value: SystemStatService.memoryUsagePer + value: SystemStatService.memPercent icon: "memory" flat: true contentScale: 0.8 @@ -48,7 +48,7 @@ NBox { height: 68 * scaling } NCircleStat { - value: SystemStatService.diskUsage + value: SystemStatService.diskPercent icon: "hard_drive" flat: true contentScale: 0.8 diff --git a/Services/SystemStatService.qml b/Services/SystemStatService.qml index 4f09c1d..5de99f0 100644 --- a/Services/SystemStatService.qml +++ b/Services/SystemStatService.qml @@ -4,6 +4,7 @@ import QtQuick import Qt.labs.folderlistmodel import Quickshell import Quickshell.Io +import qs.Commons Singleton { id: root @@ -11,12 +12,313 @@ Singleton { // Public values property real cpuUsage: 0 property real cpuTemp: 0 - property real memoryUsageGb: 0 - property real memoryUsagePer: 0 - property real diskUsage: 0 + property real memGb: 0 + property real memPercent: 0 + property real diskPercent: 0 property real rxSpeed: 0 property real txSpeed: 0 + // Configuration + property int sleepDuration: 3000 + + // Internal state for CPU calculation + property var prevCpuStats: null + + // Internal state for network speed calculation + // Previous Bytes need to be stored as 'real' as they represent the total of bytes transfered + // since the computer started, so their value will easily overlfow a 32bit int. + property real prevRxBytes: undefined + property real prevTxBytes: undefined + property real prevTime: 0 + + // Cpu temperature is the most complex + readonly property var supportedTempCpuSensorNames: ["coretemp", "k10temp", "zenpower"] + property string cpuTempSensorName: "" + property string cpuTempHwmonPath: "" + // For Intel coretemp averaging + property var intelTempValues: [] + property int intelTempFilesChecked: 0 + property int intelTempMaxFiles: 20 // Will test up to temp20_input + + // -------------------------------------------- + Component.onCompleted: { + Logger.log("SystemStat", "Service started with interval:", root.sleepDuration, "ms") + + // Kickoff the cpu name detection for temperature + cpuTempNameReader.checkNext() + } + + // -------------------------------------------- + // Timer for periodic updates + Timer { + id: updateTimer + interval: root.sleepDuration + repeat: true + running: true + triggeredOnStart: true + onTriggered: { + // Trigger all direct system files reads + memInfoFile.reload() + cpuStatFile.reload() + netDevFile.reload() + + // Run df (disk free) one time + dfProcess.running = true + + updateCpuTemperature() + } + } + + // -------------------------------------------- + // FileView components for reading system files + FileView { + id: memInfoFile + path: "/proc/meminfo" + onLoaded: parseMemoryInfo(text()) + } + + FileView { + id: cpuStatFile + path: "/proc/stat" + onLoaded: calculateCpuUsage(text()) + } + + FileView { + id: netDevFile + path: "/proc/net/dev" + onLoaded: calculateNetworkSpeed(text()) + } + + // -------------------------------------------- + // Process to fetch disk usage in percent + // Uses 'df' aka 'disk free' + Process { + id: dfProcess + command: ["df", "--output=pcent", "/"] + running: false + stdout: StdioCollector { + onStreamFinished: { + const lines = text.trim().split('\n') + if (lines.length >= 2) { + const percent = lines[1].replace(/[^0-9]/g, '') + root.diskPercent = parseInt(percent) || 0 + } + } + } + } + + // -------------------------------------------- + // CPU Temperature + // It's more complex. + // ---- + // #1 - Find a common cpu sensor name ie: "coretemp", "k10temp", "zenpower" + FileView { + id: cpuTempNameReader + property int currentIndex: 0 + + function checkNext() { + if (currentIndex >= 10) { + // Check up to hwmon10 + Logger.warn("No supported temperature sensor found") + return + } + + //Logger.log("SystemStat", "---- Probing: hwmon", currentIndex) + cpuTempNameReader.path = `/sys/class/hwmon/hwmon${currentIndex}/name` + cpuTempNameReader.reload() + } + + onLoaded: { + const name = text().trim() + if (root.supportedTempCpuSensorNames.includes(name)) { + root.cpuTempSensorName = name + root.cpuTempHwmonPath = `/sys/class/hwmon/hwmon${currentIndex}` + Logger.log("SystemStat", `Found ${root.cpuTempSensorName} CPU thermal sensor at ${root.cpuTempHwmonPath}`) + } else { + currentIndex++ + Qt.callLater(() => { + // Qt.callLater is mandatory + checkNext() + }) + } + } + + onLoadFailed: function (error) { + currentIndex++ + Qt.callLater(() => { + // Qt.callLater is mandatory + checkNext() + }) + } + } + + // ---- + // #2 - Read sensor value + FileView { + id: cpuTempReader + printErrors: false + + onLoaded: { + const data = text().trim() + if (root.cpuTempSensorName === "coretemp") { + // For Intel, collect all temperature values + const temp = parseInt(data) / 1000.0 + //console.log(temp, cpuTempReader.path) + root.intelTempValues.push(temp) + Qt.callLater(() => { + // Qt.callLater is mandatory + checkNextIntelTemp() + }) + } else { + // For AMD sensors (k10temp and zenpower), directly set the temperature + root.cpuTemp = Math.round(parseInt(data) / 1000.0) + } + } + onLoadFailed: function (error) { + Qt.callLater(() => { + // Qt.callLater is mandatory + checkNextIntelTemp() + }) + } + } + + // ------------------------------------------------------- + // Parse memory info from /proc/meminfo + function parseMemoryInfo(text) { + if (!text) + return + + const lines = text.split('\n') + let memTotal = 0 + let memAvailable = 0 + + for (const line of lines) { + if (line.startsWith('MemTotal:')) { + memTotal = parseInt(line.split(/\s+/)[1]) || 0 + } else if (line.startsWith('MemAvailable:')) { + memAvailable = parseInt(line.split(/\s+/)[1]) || 0 + } + } + + if (memTotal > 0) { + const usageKb = memTotal - memAvailable + root.memGb = (usageKb / 1000000).toFixed(1) + root.memPercent = Math.round((usageKb / memTotal) * 100) + } + } + + // ------------------------------------------------------- + // Calculate CPU usage from /proc/stat + function calculateCpuUsage(text) { + if (!text) + return + + const lines = text.split('\n') + const cpuLine = lines[0] + + // First line is total CPU + if (!cpuLine.startsWith('cpu ')) + return + + const parts = cpuLine.split(/\s+/) + const stats = { + "user": parseInt(parts[1]) || 0, + "nice": parseInt(parts[2]) || 0, + "system": parseInt(parts[3]) || 0, + "idle": parseInt(parts[4]) || 0, + "iowait": parseInt(parts[5]) || 0, + "irq": parseInt(parts[6]) || 0, + "softirq": parseInt(parts[7]) || 0, + "steal": parseInt(parts[8]) || 0, + "guest": parseInt(parts[9]) || 0, + "guestNice": parseInt(parts[10]) || 0 + } + const totalIdle = stats.idle + stats.iowait + const total = Object.values(stats).reduce((sum, val) => sum + val, 0) + + if (root.prevCpuStats) { + const prevTotalIdle = root.prevCpuStats.idle + root.prevCpuStats.iowait + const prevTotal = Object.values(root.prevCpuStats).reduce((sum, val) => sum + val, 0) + + const diffTotal = total - prevTotal + const diffIdle = totalIdle - prevTotalIdle + + if (diffTotal > 0) { + root.cpuUsage = (((diffTotal - diffIdle) / diffTotal) * 100).toFixed(1) + } + } + + root.prevCpuStats = stats + } + + // ------------------------------------------------------- + // Calculate RX and TX speed from /proc/net/dev + // Average speed of all interfaces excepted 'lo' + function calculateNetworkSpeed(text) { + if (!text) { + return + } + + const currentTime = Date.now() / 1000 + const lines = text.split('\n') + + let totalRx = 0 + let totalTx = 0 + + for (var i = 2; i < lines.length; i++) { + const line = lines[i].trim() + if (!line) { + continue + } + + const colonIndex = line.indexOf(':') + if (colonIndex === -1) { + continue + } + + const iface = line.substring(0, colonIndex).trim() + if (iface === 'lo') { + continue + } + + const statsLine = line.substring(colonIndex + 1).trim() + const stats = statsLine.split(/\s+/) + + const rxBytes = parseInt(stats[0], 10) || 0 + const txBytes = parseInt(stats[8], 10) || 0 + + totalRx += rxBytes + totalTx += txBytes + } + + // Compute only if we have a previous run to compare to. + if (root.prevTime > 0 && root.prevRxBytes !== undefined) { + const timeDiff = currentTime - root.prevTime + + // Avoid division by zero if time hasn't passed. + if (timeDiff > 0) { + let rxDiff = totalRx - root.prevRxBytes + let txDiff = totalTx - root.prevTxBytes + + // Handle counter resets (e.g., WiFi reconnect), which would cause a negative value. + if (rxDiff < 0) { + rxDiff = 0 + } + if (txDiff < 0) { + txDiff = 0 + } + + root.rxSpeed = Math.round(rxDiff / timeDiff) // Speed in Bytes/s + root.txSpeed = Math.round(txDiff / timeDiff) + } + } + + root.prevRxBytes = totalRx + root.prevTxBytes = totalTx + root.prevTime = currentTime + } + + // ------------------------------------------------------- // Helper function to format network speeds function formatSpeed(bytesPerSecond) { if (bytesPerSecond < 1024) { @@ -30,27 +332,44 @@ Singleton { } } - // Background process emitting one JSON line per sample - Process { - id: reader - running: true - command: ["sh", "-c", Quickshell.shellDir + "/Bin/system-stats.sh"] - stdout: SplitParser { - onRead: function (line) { - try { - const data = JSON.parse(line) - root.cpuUsage = data.cpu - root.cpuTemp = data.cputemp - root.memoryUsageGb = data.memgb - root.memoryUsagePer = data.memper - root.diskUsage = data.diskper - root.rxSpeed = parseFloat(data.rx_speed) || 0 - root.txSpeed = parseFloat(data.tx_speed) || 0 - } catch (e) { - - // ignore malformed lines - } - } + // ------------------------------------------------------- + // Function to start fetching and computing the cpu temperature + function updateCpuTemperature() { + // For AMD sensors (k10temp and zenpower), only use Tctl sensor + // temp1_input corresponds to Tctl (Temperature Control) on these sensors + if (root.cpuTempSensorName === "k10temp" || root.cpuTempSensorName === "zenpower") { + cpuTempReader.path = `${root.cpuTempHwmonPath}/temp1_input` + cpuTempReader.reload() + } // For Intel coretemp, start averaging all available sensors/cores + else if (root.cpuTempSensorName === "coretemp") { + root.intelTempValues = [] + root.intelTempFilesChecked = 0 + checkNextIntelTemp() } } + + // ------------------------------------------------------- + // Function to check next Intel temperature sensor + function checkNextIntelTemp() { + if (root.intelTempFilesChecked >= root.intelTempMaxFiles) { + // Calculate average of all found temperatures + if (root.intelTempValues.length > 0) { + let sum = 0 + for (var i = 0; i < root.intelTempValues.length; i++) { + sum += root.intelTempValues[i] + } + root.cpuTemp = Math.round(sum / root.intelTempValues.length) + Logger.log("SystemStat", `Averaged ${root.intelTempValues.length} CPU thermal sensors: ${root.cpuTemp}°C`) + } else { + Logger.warn("SystemStat", "No temperature sensors found for coretemp") + root.cpuTemp = 0 + } + return + } + + // Check next temperature file + root.intelTempFilesChecked++ + cpuTempReader.path = `${root.cpuTempHwmonPath}/temp${root.intelTempFilesChecked}_input` + cpuTempReader.reload() + } } From fb2d42da57adf38ee469f334159b3f2a6f3dd62c Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sat, 6 Sep 2025 23:47:17 -0400 Subject: [PATCH 09/54] SysStat Service: less log on intel CPU --- Services/SystemStatService.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Services/SystemStatService.qml b/Services/SystemStatService.qml index 5de99f0..71aa760 100644 --- a/Services/SystemStatService.qml +++ b/Services/SystemStatService.qml @@ -35,7 +35,7 @@ Singleton { readonly property var supportedTempCpuSensorNames: ["coretemp", "k10temp", "zenpower"] property string cpuTempSensorName: "" property string cpuTempHwmonPath: "" - // For Intel coretemp averaging + // For Intel coretemp averaging of all cores/sensors property var intelTempValues: [] property int intelTempFilesChecked: 0 property int intelTempMaxFiles: 20 // Will test up to temp20_input @@ -359,7 +359,7 @@ Singleton { sum += root.intelTempValues[i] } root.cpuTemp = Math.round(sum / root.intelTempValues.length) - Logger.log("SystemStat", `Averaged ${root.intelTempValues.length} CPU thermal sensors: ${root.cpuTemp}°C`) + //Logger.log("SystemStat", `Averaged ${root.intelTempValues.length} CPU thermal sensors: ${root.cpuTemp}°C`) } else { Logger.warn("SystemStat", "No temperature sensors found for coretemp") root.cpuTemp = 0 From f27608947c31c9bd64af429ecc5a358d8cde7837 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sun, 7 Sep 2025 00:06:58 -0400 Subject: [PATCH 10/54] Settings: slightly more compact tabs --- Modules/SettingsPanel/SettingsPanel.qml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Modules/SettingsPanel/SettingsPanel.qml b/Modules/SettingsPanel/SettingsPanel.qml index 1e6d6cc..8bfbe77 100644 --- a/Modules/SettingsPanel/SettingsPanel.qml +++ b/Modules/SettingsPanel/SettingsPanel.qml @@ -368,7 +368,7 @@ NPanel { ColumnLayout { anchors.fill: parent anchors.margins: Style.marginS * scaling - spacing: Style.marginXS * 1.5 * scaling + spacing: Style.marginXS * scaling Repeater { id: sections @@ -398,7 +398,8 @@ NPanel { RowLayout { id: tabEntryRow anchors.fill: parent - anchors.margins: Style.marginS * scaling + anchors.leftMargin: Style.marginS * scaling + anchors.rightMargin: Style.marginS * scaling spacing: Style.marginS * scaling // Tab icon From 9010a1668b50a7820f9175be5c42d0fb534c0067 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sun, 7 Sep 2025 00:43:57 -0400 Subject: [PATCH 11/54] SysStat: fixed warning. cant assign undefined to real --- Services/SystemStatService.qml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Services/SystemStatService.qml b/Services/SystemStatService.qml index 71aa760..7328f71 100644 --- a/Services/SystemStatService.qml +++ b/Services/SystemStatService.qml @@ -27,8 +27,8 @@ Singleton { // Internal state for network speed calculation // Previous Bytes need to be stored as 'real' as they represent the total of bytes transfered // since the computer started, so their value will easily overlfow a 32bit int. - property real prevRxBytes: undefined - property real prevTxBytes: undefined + property real prevRxBytes: 0 + property real prevTxBytes: 0 property real prevTime: 0 // Cpu temperature is the most complex @@ -292,7 +292,7 @@ Singleton { } // Compute only if we have a previous run to compare to. - if (root.prevTime > 0 && root.prevRxBytes !== undefined) { + if (root.prevTime > 0) { const timeDiff = currentTime - root.prevTime // Avoid division by zero if time hasn't passed. From adac96ee84ddc799e31a0841384bf53582fc13e4 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sun, 7 Sep 2025 00:49:59 -0400 Subject: [PATCH 12/54] SidePanel: proper height computation --- Modules/SidePanel/SidePanel.qml | 61 ++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml index 6225494..4dba15a 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/SidePanel/SidePanel.qml @@ -11,71 +11,92 @@ NPanel { id: root panelWidth: 460 * scaling - panelHeight: 708 * scaling + panelHeight: contentHeight + + // Default height, will be modified via binding when the content is fully loaded + property real contentHeight: 720 * scaling panelContent: Item { id: content property real cardSpacing: Style.marginL * scaling - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: content.cardSpacing - implicitHeight: layout.implicitHeight + width: root.panelWidth + implicitHeight: layout.implicitHeight + (2 * cardSpacing) + height: implicitHeight - // Layout content (not vertically anchored so implicitHeight is valid) + // Update parent's contentHeight whenever our height changes + onHeightChanged: { + root.contentHeight = height + } + + onImplicitHeightChanged: { + if (implicitHeight > 0) { + root.contentHeight = implicitHeight + } + } + + // Layout content ColumnLayout { id: layout - // Use the same spacing value horizontally and vertically - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top + x: content.cardSpacing + y: content.cardSpacing + width: parent.width - (2 * content.cardSpacing) spacing: content.cardSpacing // Cards (consistent inter-card spacing via ColumnLayout spacing) - ProfileCard {// Layout.topMargin: 0 - // Layout.bottomMargin: 0 + ProfileCard { + id: profileCard + Layout.fillWidth: true } - WeatherCard {// Layout.topMargin: 0 - // Layout.bottomMargin: 0 + + WeatherCard { + id: weatherCard + Layout.fillWidth: true } // Middle section: media + stats column RowLayout { + id: middleRow Layout.fillWidth: true - Layout.topMargin: 0 - Layout.bottomMargin: 0 + Layout.minimumHeight: 280 * scaling + Layout.preferredHeight: Math.max(280 * scaling, statsCard.implicitHeight) spacing: content.cardSpacing // Media card MediaCard { id: mediaCard Layout.fillWidth: true - implicitHeight: statsCard.implicitHeight + Layout.fillHeight: true } // System monitors combined in one card SystemMonitorCard { id: statsCard + Layout.alignment: Qt.AlignTop } } // Bottom actions (two grouped rows of round buttons) RowLayout { + id: bottomRow Layout.fillWidth: true - Layout.topMargin: 0 - Layout.bottomMargin: 0 + Layout.minimumHeight: 60 * scaling + Layout.preferredHeight: Math.max(60 * scaling, powerProfilesCard.implicitHeight, utilitiesCard.implicitHeight) spacing: content.cardSpacing // Power Profiles switcher PowerProfilesCard { + id: powerProfilesCard spacing: content.cardSpacing + Layout.fillWidth: true } // Utilities buttons UtilitiesCard { + id: utilitiesCard spacing: content.cardSpacing + Layout.fillWidth: true } } } From 291d919b9f1b0d91bdc23b0ec987a2e205e29577 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 7 Sep 2025 12:51:13 +0200 Subject: [PATCH 13/54] Notification: add -i support --- Bin/test-notifications.sh | 21 ++++++++++++++++++ Services/NotificationService.qml | 37 +++++++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/Bin/test-notifications.sh b/Bin/test-notifications.sh index 56e58a5..5e677c4 100755 --- a/Bin/test-notifications.sh +++ b/Bin/test-notifications.sh @@ -9,3 +9,24 @@ for i in {1..8}; do done echo "All notifications sent!" + +# Additional tests for icon/image handling +if command -v notify-send >/dev/null 2>&1; then + echo "Sending icon/image tests..." + + # 1) Themed icon name + notify-send -i dialog-information "Icon name test" "Should resolve from theme (dialog-information)" + + # 2) Absolute path if a sample image exists + SAMPLE_IMG="/usr/share/pixmaps/debian-logo.png" + if [ -f "$SAMPLE_IMG" ]; then + notify-send -i "$SAMPLE_IMG" "Absolute path test" "Should show the provided image path" + fi + + # 3) file:// URL form + if [ -f "$SAMPLE_IMG" ]; then + notify-send -i "file://$SAMPLE_IMG" "file:// URL test" "Should display after stripping scheme" + fi + + echo "Icon/image tests sent!" +fi diff --git a/Services/NotificationService.qml b/Services/NotificationService.qml index 4ce9747..da6ab32 100644 --- a/Services/NotificationService.qml +++ b/Services/NotificationService.qml @@ -118,12 +118,13 @@ Singleton { // Function to add notification to model function addNotification(notification) { + const resolvedImage = resolveNotificationImage(notification) notificationModel.insert(0, { "rawNotification": notification, "summary": notification.summary, "body": notification.body, "appName": notification.appName, - "image": notification.image, + "image": resolvedImage, "appIcon": notification.appIcon, "urgency": notification.urgency, "timestamp": new Date() @@ -139,6 +140,40 @@ Singleton { } } + // Resolve an image path for a notification, supporting icon names and absolute paths + function resolveNotificationImage(notification) { + try { + // If an explicit image is already provided, prefer it + if (notification && notification.image && notification.image !== "") { + return notification.image + } + + // Fallback to appIcon which may be a name or a path (notify-send -i) + const icon = notification ? (notification.appIcon || "") : "" + if (!icon) + return "" + + // Accept absolute file paths or file URLs directly + if (icon.startsWith("/")) { + return icon + } + if (icon.startsWith("file://")) { + // Strip the scheme for QML image source compatibility + return icon.substring("file://".length) + } + + // Resolve themed icon names to absolute paths + try { + const p = Icons.iconFromName(icon, "") + return p || "" + } catch (e2) { + return "" + } + } catch (e) { + return "" + } + } + // Add a simplified copy into persistent history function addToHistory(notification) { historyModel.insert(0, { From 57448f100c1beb1965b6e39104bc8ed136190c95 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 7 Sep 2025 14:48:20 +0200 Subject: [PATCH 14/54] bartab-overhaul: initial commit --- Modules/Bar/Widgets/ActiveWindow.qml | 36 +- Modules/Bar/Widgets/Brightness.qml | 32 ++ Modules/Bar/Widgets/Clock.qml | 27 ++ Modules/Bar/Widgets/MediaMini.qml | 59 ++- Modules/Bar/Widgets/Microphone.qml | 32 ++ Modules/Bar/Widgets/NotificationHistory.qml | 87 +++- Modules/Bar/Widgets/SidePanelToggle.qml | 41 +- Modules/Bar/Widgets/SystemMonitor.qml | 45 +- Modules/Bar/Widgets/Volume.qml | 31 ++ Modules/Bar/Widgets/Workspace.qml | 39 +- .../SettingsPanel/Extras/BarSectionEditor.qml | 34 +- .../Extras/BarWidgetSettingsDialog.qml | 392 ++++++++++++++++++ Modules/SettingsPanel/Tabs/BarTab.qml | 45 +- Services/BarWidgetRegistry.qml | 55 +++ Widgets/NCheckbox.qml | 5 + Widgets/NClock.qml | 21 +- 16 files changed, 905 insertions(+), 76 deletions(-) diff --git a/Modules/Bar/Widgets/ActiveWindow.qml b/Modules/Bar/Widgets/ActiveWindow.qml index 65f900e..c84d2b0 100644 --- a/Modules/Bar/Widgets/ActiveWindow.qml +++ b/Modules/Bar/Widgets/ActiveWindow.qml @@ -18,10 +18,44 @@ RowLayout { spacing: Style.marginS * scaling visible: getTitle() !== "" + property string barSection: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var widgetSettings: { + var section = barSection.replace("Section", "").toLowerCase() + if (section && sectionWidgetIndex >= 0) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex] + } + } + return {} + } + + readonly property bool userShowIcon: (widgetSettings.showIcon !== undefined) ? widgetSettings.showIcon : ((Settings.data.bar.showActiveWindowIcon !== undefined) ? Settings.data.bar.showActiveWindowIcon : BarWidgetRegistry.widgetMetadata["ActiveWindow"].showIcon) + function getTitle() { return CompositorService.focusedWindowTitle !== "(No active window)" ? CompositorService.focusedWindowTitle : "" } + Component.onCompleted: { + try { + var section = barSection.replace("Section", "").toLowerCase() + if (section && sectionWidgetIndex >= 0) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + if (widgets[sectionWidgetIndex].showIcon === undefined + && Settings.data.bar.showActiveWindowIcon !== undefined) { + widgets[sectionWidgetIndex].showIcon = Settings.data.bar.showActiveWindowIcon + } + } + } + } catch (e) { + + } + } + function getAppIcon() { // Try CompositorService first const focusedWindow = CompositorService.getFocusedWindow() @@ -74,7 +108,7 @@ RowLayout { Layout.preferredWidth: Style.fontSizeL * scaling * 1.2 Layout.preferredHeight: Style.fontSizeL * scaling * 1.2 Layout.alignment: Qt.AlignVCenter - visible: getTitle() !== "" && Settings.data.bar.showActiveWindowIcon + visible: getTitle() !== "" && userShowIcon IconImage { id: windowIcon diff --git a/Modules/Bar/Widgets/Brightness.qml b/Modules/Bar/Widgets/Brightness.qml index c16866c..2c299f3 100644 --- a/Modules/Bar/Widgets/Brightness.qml +++ b/Modules/Bar/Widgets/Brightness.qml @@ -14,6 +14,20 @@ Item { property int sectionWidgetIndex: 0 property int sectionWidgetsCount: 0 + property var widgetSettings: { + var section = barSection.replace("Section", "").toLowerCase() + if (section && sectionWidgetIndex >= 0) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex] + } + } + return {} + } + + readonly property bool userAlwaysShowPercentage: (widgetSettings.alwaysShowPercentage + !== undefined) ? widgetSettings.alwaysShowPercentage : BarWidgetRegistry.widgetMetadata["Brightness"].alwaysShowPercentage + // Used to avoid opening the pill on Quickshell startup property bool firstBrightnessReceived: false @@ -57,6 +71,23 @@ Item { onTriggered: pill.hide() } + Component.onCompleted: { + try { + var section = barSection.replace("Section", "").toLowerCase() + if (section && sectionWidgetIndex >= 0) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + if (widgets[sectionWidgetIndex].alwaysShowPercentage === undefined + && Settings.data.bar.alwaysShowBatteryPercentage !== undefined) { + widgets[sectionWidgetIndex].alwaysShowPercentage = Settings.data.bar.alwaysShowBatteryPercentage + } + } + } + } catch (e) { + + } + } + NPill { id: pill @@ -69,6 +100,7 @@ Item { var monitor = getMonitor() return monitor ? (Math.round(monitor.brightness * 100) + "%") : "" } + forceOpen: userAlwaysShowPercentage tooltipText: { var monitor = getMonitor() if (!monitor) diff --git a/Modules/Bar/Widgets/Clock.qml b/Modules/Bar/Widgets/Clock.qml index ee57b57..38b964b 100644 --- a/Modules/Bar/Widgets/Clock.qml +++ b/Modules/Bar/Widgets/Clock.qml @@ -10,6 +10,29 @@ Rectangle { property ShellScreen screen property real scaling: 1.0 + // Widget properties passed from Bar.qml for per-instance settings + property string barSection: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + // Resolve per-instance widget settings from Settings.data + property var widgetSettings: { + var section = barSection.replace("Section", "").toLowerCase() + if (section && sectionWidgetIndex >= 0) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex] + } + } + return {} + } + + // Use settings or defaults from BarWidgetRegistry metadata + readonly property bool userShowDate: (widgetSettings.showDate + !== undefined) ? widgetSettings.showDate : BarWidgetRegistry.widgetMetadata["Clock"].showDate + readonly property bool userUse12h: (widgetSettings.use12HourClock !== undefined) ? widgetSettings.use12HourClock : BarWidgetRegistry.widgetMetadata["Clock"].use12HourClock + readonly property bool userShowSeconds: (widgetSettings.showSeconds !== undefined) ? widgetSettings.showSeconds : BarWidgetRegistry.widgetMetadata["Clock"].showSeconds + implicitWidth: clock.width + Style.marginM * 2 * scaling implicitHeight: Math.round(Style.capsuleHeight * scaling) radius: Math.round(Style.radiusM * scaling) @@ -20,6 +43,10 @@ Rectangle { id: clock anchors.verticalCenter: parent.verticalCenter anchors.horizontalCenter: parent.horizontalCenter + // Per-instance overrides to Time formatting + showDate: userShowDate + use12h: userUse12h + showSeconds: userShowSeconds NTooltip { id: tooltip diff --git a/Modules/Bar/Widgets/MediaMini.qml b/Modules/Bar/Widgets/MediaMini.qml index 2483dbc..76a5655 100644 --- a/Modules/Bar/Widgets/MediaMini.qml +++ b/Modules/Bar/Widgets/MediaMini.qml @@ -20,6 +20,28 @@ RowLayout { visible: MediaService.currentPlayer !== null && MediaService.canPlay Layout.preferredWidth: MediaService.currentPlayer !== null && MediaService.canPlay ? implicitWidth : 0 + property string barSection: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var widgetSettings: { + var section = barSection.replace("Section", "").toLowerCase() + if (section && sectionWidgetIndex >= 0) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex] + } + } + return {} + } + + readonly property bool userShowAlbumArt: (widgetSettings.showAlbumArt !== undefined) ? widgetSettings.showAlbumArt : ((Settings.data.audio.showMiniplayerAlbumArt !== undefined) ? Settings.data.audio.showMiniplayerAlbumArt : BarWidgetRegistry.widgetMetadata["MediaMini"].showAlbumArt) + readonly property bool userShowVisualizer: (widgetSettings.showVisualizer !== undefined) ? widgetSettings.showVisualizer : ((Settings.data.audio.showMiniplayerCava !== undefined) ? Settings.data.audio.showMiniplayerCava : BarWidgetRegistry.widgetMetadata["MediaMini"].showVisualizer) + readonly property string userVisualizerType: (widgetSettings.visualizerType !== undefined + && widgetSettings.visualizerType + !== "") ? widgetSettings.visualizerType : ((Settings.data.audio.visualizerType !== undefined + && Settings.data.audio.visualizerType !== "") ? Settings.data.audio.visualizerType : BarWidgetRegistry.widgetMetadata["MediaMini"].visualizerType) + function getTitle() { return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "") } @@ -58,8 +80,7 @@ RowLayout { Loader { anchors.verticalCenter: parent.verticalCenter anchors.horizontalCenter: parent.horizontalCenter - active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "linear" - && MediaService.isPlaying + active: userShowVisualizer && userVisualizerType == "linear" && MediaService.isPlaying z: 0 sourceComponent: LinearSpectrum { @@ -74,8 +95,7 @@ RowLayout { Loader { anchors.verticalCenter: parent.verticalCenter anchors.horizontalCenter: parent.horizontalCenter - active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "mirrored" - && MediaService.isPlaying + active: userShowVisualizer && userVisualizerType == "mirrored" && MediaService.isPlaying z: 0 sourceComponent: MirroredSpectrum { @@ -90,8 +110,7 @@ RowLayout { Loader { anchors.verticalCenter: parent.verticalCenter anchors.horizontalCenter: parent.horizontalCenter - active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "wave" - && MediaService.isPlaying + active: userShowVisualizer && userVisualizerType == "wave" && MediaService.isPlaying z: 0 sourceComponent: WaveSpectrum { @@ -115,12 +134,12 @@ RowLayout { font.pointSize: Style.fontSizeL * scaling verticalAlignment: Text.AlignVCenter Layout.alignment: Qt.AlignVCenter - visible: !Settings.data.audio.showMiniplayerAlbumArt && getTitle() !== "" && !trackArt.visible + visible: !userShowAlbumArt && getTitle() !== "" && !trackArt.visible } ColumnLayout { Layout.alignment: Qt.AlignVCenter - visible: Settings.data.audio.showMiniplayerAlbumArt + visible: userShowAlbumArt spacing: 0 Item { @@ -199,6 +218,30 @@ RowLayout { } } + Component.onCompleted: { + try { + var section = barSection.replace("Section", "").toLowerCase() + if (section && sectionWidgetIndex >= 0) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + var w = widgets[sectionWidgetIndex] + if (w.showAlbumArt === undefined && Settings.data.audio.showMiniplayerAlbumArt !== undefined) { + w.showAlbumArt = Settings.data.audio.showMiniplayerAlbumArt + } + if (w.showVisualizer === undefined && Settings.data.audio.showMiniplayerCava !== undefined) { + w.showVisualizer = Settings.data.audio.showMiniplayerCava + } + if ((w.visualizerType === undefined || w.visualizerType === "") + && (Settings.data.audio.visualizerType !== undefined && Settings.data.audio.visualizerType !== "")) { + w.visualizerType = Settings.data.audio.visualizerType + } + } + } + } catch (e) { + + } + } + NTooltip { id: tooltip text: { diff --git a/Modules/Bar/Widgets/Microphone.qml b/Modules/Bar/Widgets/Microphone.qml index f4e1c1a..75a5a20 100644 --- a/Modules/Bar/Widgets/Microphone.qml +++ b/Modules/Bar/Widgets/Microphone.qml @@ -16,6 +16,20 @@ Item { property int sectionWidgetIndex: 0 property int sectionWidgetsCount: 0 + property var widgetSettings: { + var section = barSection.replace("Section", "").toLowerCase() + if (section && sectionWidgetIndex >= 0) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex] + } + } + return {} + } + + readonly property bool userAlwaysShowPercentage: (widgetSettings.alwaysShowPercentage + !== undefined) ? widgetSettings.alwaysShowPercentage : BarWidgetRegistry.widgetMetadata["Microphone"].alwaysShowPercentage + // Used to avoid opening the pill on Quickshell startup property bool firstInputVolumeReceived: false property int wheelAccumulator: 0 @@ -69,6 +83,23 @@ Item { } } + Component.onCompleted: { + try { + var section = barSection.replace("Section", "").toLowerCase() + if (section && sectionWidgetIndex >= 0) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + if (widgets[sectionWidgetIndex].alwaysShowPercentage === undefined + && Settings.data.bar.alwaysShowBatteryPercentage !== undefined) { + widgets[sectionWidgetIndex].alwaysShowPercentage = Settings.data.bar.alwaysShowBatteryPercentage + } + } + } + } catch (e) { + + } + } + NPill { id: pill @@ -78,6 +109,7 @@ Item { collapsedIconColor: Color.mOnSurface autoHide: false // Important to be false so we can hover as long as we want text: Math.floor(AudioService.inputVolume * 100) + "%" + forceOpen: userAlwaysShowPercentage tooltipText: "Microphone: " + Math.round(AudioService.inputVolume * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume.\nRight click to toggle mute." diff --git a/Modules/Bar/Widgets/NotificationHistory.qml b/Modules/Bar/Widgets/NotificationHistory.qml index 48a62fd..9897d05 100644 --- a/Modules/Bar/Widgets/NotificationHistory.qml +++ b/Modules/Bar/Widgets/NotificationHistory.qml @@ -13,15 +13,94 @@ NIconButton { property ShellScreen screen property real scaling: 1.0 + property string barSection: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var widgetSettings: { + var section = barSection.replace("Section", "").toLowerCase() + if (section && sectionWidgetIndex >= 0) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex] + } + } + return {} + } + + readonly property bool userShowUnreadBadge: (widgetSettings.showUnreadBadge !== undefined) ? widgetSettings.showUnreadBadge : BarWidgetRegistry.widgetMetadata["NotificationHistory"].showUnreadBadge + readonly property bool userHideWhenZero: (widgetSettings.hideWhenZero !== undefined) ? widgetSettings.hideWhenZero : BarWidgetRegistry.widgetMetadata["NotificationHistory"].hideWhenZero + readonly property bool userDoNotDisturb: (widgetSettings.doNotDisturb !== undefined) ? widgetSettings.doNotDisturb : BarWidgetRegistry.widgetMetadata["NotificationHistory"].doNotDisturb + + function lastSeenTs() { + return widgetSettings.lastSeenTs || 0 + } + + function computeUnreadCount() { + var since = lastSeenTs() + var count = 0 + var model = NotificationService.historyModel + for (var i = 0; i < model.count; i++) { + var item = model.get(i) + var ts = item.timestamp instanceof Date ? item.timestamp.getTime() : item.timestamp + if (ts > since) + count++ + } + return count + } + sizeRatio: 0.8 - icon: Settings.data.notifications.doNotDisturb ? "notifications_off" : "notifications" - tooltipText: Settings.data.notifications.doNotDisturb ? "Notification history.\nRight-click to disable 'Do Not Disturb'." : "Notification history.\nRight-click to enable 'Do Not Disturb'." + icon: (Settings.data.notifications.doNotDisturb || userDoNotDisturb) ? "notifications_off" : "notifications" + tooltipText: (Settings.data.notifications.doNotDisturb + || userDoNotDisturb) ? "Notification history.\nRight-click to disable 'Do Not Disturb'." : "Notification history.\nRight-click to enable 'Do Not Disturb'." colorBg: Color.mSurfaceVariant - colorFg: Settings.data.notifications.doNotDisturb ? Color.mError : Color.mOnSurface + colorFg: (Settings.data.notifications.doNotDisturb || userDoNotDisturb) ? Color.mError : Color.mOnSurface colorBorder: Color.transparent colorBorderHover: Color.transparent - onClicked: PanelService.getPanel("notificationHistoryPanel")?.toggle(screen, this) + onClicked: { + // Open first using current geometry as anchor + var panel = PanelService.getPanel("notificationHistoryPanel") + panel?.toggle(screen, this) + // Update last seen right after to avoid affecting anchor calculation + Qt.callLater(function () { + try { + var section = barSection.replace("Section", "").toLowerCase() + if (section && sectionWidgetIndex >= 0) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + widgets[sectionWidgetIndex].lastSeenTs = Time.timestamp * 1000 + } + } + } catch (e) { + + } + }) + } onRightClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb + + Loader { + anchors.right: parent.right + anchors.top: parent.top + anchors.rightMargin: -4 * scaling + anchors.topMargin: -4 * scaling + z: 2 + active: userShowUnreadBadge && (!userHideWhenZero || computeUnreadCount() > 0) + sourceComponent: Rectangle { + width: 16 * scaling + height: 16 * scaling + radius: width / 2 + color: Color.mError + border.color: Color.mSurface + border.width: 1 + visible: computeUnreadCount() > 0 || !userHideWhenZero + NText { + anchors.centerIn: parent + text: Math.min(computeUnreadCount(), 9) + font.pointSize: Style.fontSizeXXS * scaling + color: Color.mOnError + } + } + } } diff --git a/Modules/Bar/Widgets/SidePanelToggle.qml b/Modules/Bar/Widgets/SidePanelToggle.qml index b9572fb..6c0b489 100644 --- a/Modules/Bar/Widgets/SidePanelToggle.qml +++ b/Modules/Bar/Widgets/SidePanelToggle.qml @@ -1,3 +1,4 @@ +import QtQuick import Quickshell import Quickshell.Widgets import QtQuick.Effects @@ -10,8 +11,24 @@ NIconButton { property ShellScreen screen property real scaling: 1.0 + property string barSection: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 - icon: Settings.data.bar.useDistroLogo ? "" : "widgets" + property var widgetSettings: { + var section = barSection.replace("Section", "").toLowerCase() + if (section && sectionWidgetIndex >= 0) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex] + } + } + return {} + } + + readonly property bool userUseDistroLogo: (widgetSettings.useDistroLogo !== undefined) ? widgetSettings.useDistroLogo : ((Settings.data.bar.useDistroLogo !== undefined) ? Settings.data.bar.useDistroLogo : BarWidgetRegistry.widgetMetadata["SidePanelToggle"].useDistroLogo) + + icon: userUseDistroLogo ? "" : "widgets" tooltipText: "Open side panel." sizeRatio: 0.8 @@ -24,14 +41,30 @@ NIconButton { onClicked: PanelService.getPanel("sidePanel")?.toggle(screen, this) onRightClicked: PanelService.getPanel("settingsPanel")?.toggle(screen) - // When enabled, draw the distro logo instead of the icon glyph + Component.onCompleted: { + try { + var section = barSection.replace("Section", "").toLowerCase() + if (section && sectionWidgetIndex >= 0) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + if (widgets[sectionWidgetIndex].useDistroLogo === undefined + && Settings.data.bar.useDistroLogo !== undefined) { + widgets[sectionWidgetIndex].useDistroLogo = Settings.data.bar.useDistroLogo + } + } + } + } catch (e) { + + } + } + IconImage { id: logo anchors.centerIn: parent width: root.width * 0.6 height: width - source: Settings.data.bar.useDistroLogo ? DistroLogoService.osLogo : "" - visible: false //Settings.data.bar.useDistroLogo && source !== "" + source: userUseDistroLogo ? DistroLogoService.osLogo : "" + visible: userUseDistroLogo && source !== "" smooth: true } diff --git a/Modules/Bar/Widgets/SystemMonitor.qml b/Modules/Bar/Widgets/SystemMonitor.qml index e57d599..654f161 100644 --- a/Modules/Bar/Widgets/SystemMonitor.qml +++ b/Modules/Bar/Widgets/SystemMonitor.qml @@ -11,6 +11,44 @@ RowLayout { property ShellScreen screen property real scaling: 1.0 + property string barSection: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var widgetSettings: { + var section = barSection.replace("Section", "").toLowerCase() + if (section && sectionWidgetIndex >= 0) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex] + } + } + return {} + } + + readonly property bool userShowCpuUsage: (widgetSettings.showCpuUsage !== undefined) ? widgetSettings.showCpuUsage : BarWidgetRegistry.widgetMetadata["SystemMonitor"].showCpuUsage + readonly property bool userShowCpuTemp: (widgetSettings.showCpuTemp !== undefined) ? widgetSettings.showCpuTemp : BarWidgetRegistry.widgetMetadata["SystemMonitor"].showCpuTemp + readonly property bool userShowMemoryUsage: (widgetSettings.showMemoryUsage !== undefined) ? widgetSettings.showMemoryUsage : BarWidgetRegistry.widgetMetadata["SystemMonitor"].showMemoryUsage + readonly property bool userShowNetworkStats: (widgetSettings.showNetworkStats + !== undefined) ? widgetSettings.showNetworkStats : ((Settings.data.bar.showNetworkStats !== undefined) ? Settings.data.bar.showNetworkStats : BarWidgetRegistry.widgetMetadata["SystemMonitor"].showNetworkStats) + + Component.onCompleted: { + try { + var section = barSection.replace("Section", "").toLowerCase() + if (section && sectionWidgetIndex >= 0) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + if (widgets[sectionWidgetIndex].showNetworkStats === undefined + && Settings.data.bar.showNetworkStats !== undefined) { + widgets[sectionWidgetIndex].showNetworkStats = Settings.data.bar.showNetworkStats + } + } + } + } catch (e) { + + } + } + Layout.alignment: Qt.AlignVCenter spacing: Style.marginS * scaling @@ -34,6 +72,7 @@ RowLayout { id: cpuUsageLayout spacing: Style.marginXS * scaling Layout.alignment: Qt.AlignVCenter + visible: userShowCpuUsage NIcon { id: cpuUsageIcon @@ -59,6 +98,7 @@ RowLayout { // spacing is thin here to compensate for the vertical thermometer icon spacing: Style.marginXXS * scaling Layout.alignment: Qt.AlignVCenter + visible: userShowCpuTemp NIcon { text: "thermometer" @@ -81,6 +121,7 @@ RowLayout { id: memoryUsageLayout spacing: Style.marginXS * scaling Layout.alignment: Qt.AlignVCenter + visible: userShowMemoryUsage NIcon { text: "memory" @@ -103,7 +144,7 @@ RowLayout { id: networkDownloadLayout spacing: Style.marginXS * scaling Layout.alignment: Qt.AlignVCenter - visible: Settings.data.bar.showNetworkStats + visible: userShowNetworkStats NIcon { text: "download" @@ -126,7 +167,7 @@ RowLayout { id: networkUploadLayout spacing: Style.marginXS * scaling Layout.alignment: Qt.AlignVCenter - visible: Settings.data.bar.showNetworkStats + visible: userShowNetworkStats NIcon { text: "upload" diff --git a/Modules/Bar/Widgets/Volume.qml b/Modules/Bar/Widgets/Volume.qml index 84f8b22..116718e 100644 --- a/Modules/Bar/Widgets/Volume.qml +++ b/Modules/Bar/Widgets/Volume.qml @@ -16,6 +16,19 @@ Item { property int sectionWidgetIndex: 0 property int sectionWidgetsCount: 0 + property var widgetSettings: { + var section = barSection.replace("Section", "").toLowerCase() + if (section && sectionWidgetIndex >= 0) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex] + } + } + return {} + } + readonly property bool userAlwaysShowPercentage: (widgetSettings.alwaysShowPercentage + !== undefined) ? widgetSettings.alwaysShowPercentage : ((Settings.data.bar.alwaysShowBatteryPercentage !== undefined) ? Settings.data.bar.alwaysShowBatteryPercentage : BarWidgetRegistry.widgetMetadata["Volume"].alwaysShowPercentage) + // Used to avoid opening the pill on Quickshell startup property bool firstVolumeReceived: false property int wheelAccumulator: 0 @@ -54,6 +67,23 @@ Item { } } + Component.onCompleted: { + try { + var section = barSection.replace("Section", "").toLowerCase() + if (section && sectionWidgetIndex >= 0) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + if (widgets[sectionWidgetIndex].alwaysShowPercentage === undefined + && Settings.data.bar.alwaysShowBatteryPercentage !== undefined) { + widgets[sectionWidgetIndex].alwaysShowPercentage = Settings.data.bar.alwaysShowBatteryPercentage + } + } + } + } catch (e) { + + } + } + NPill { id: pill @@ -63,6 +93,7 @@ Item { collapsedIconColor: Color.mOnSurface autoHide: false // Important to be false so we can hover as long as we want text: Math.floor(AudioService.volume * 100) + "%" + forceOpen: userAlwaysShowPercentage tooltipText: "Volume: " + Math.round(AudioService.volume * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume.\nRight click to toggle mute." diff --git a/Modules/Bar/Widgets/Workspace.qml b/Modules/Bar/Widgets/Workspace.qml index 051bdea..3fe6641 100644 --- a/Modules/Bar/Widgets/Workspace.qml +++ b/Modules/Bar/Widgets/Workspace.qml @@ -14,6 +14,23 @@ Item { property ShellScreen screen property real scaling: 1.0 + property string barSection: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var widgetSettings: { + var section = barSection.replace("Section", "").toLowerCase() + if (section && sectionWidgetIndex >= 0) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex] + } + } + return {} + } + + readonly property string userLabelMode: (widgetSettings.labelMode !== undefined) ? widgetSettings.labelMode : ((Settings.data.bar.showWorkspaceLabel !== undefined) ? Settings.data.bar.showWorkspaceLabel : BarWidgetRegistry.widgetMetadata["Workspace"].labelMode) + property bool isDestroying: false property bool hovered: false @@ -50,6 +67,20 @@ Item { Component.onCompleted: { refreshWorkspaces() + try { + var section = barSection.replace("Section", "").toLowerCase() + if (section && sectionWidgetIndex >= 0) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + if (widgets[sectionWidgetIndex].labelMode === undefined + && Settings.data.bar.showWorkspaceLabel !== undefined) { + widgets[sectionWidgetIndex].labelMode = Settings.data.bar.showWorkspaceLabel + } + } + } + } catch (e) { + + } } Component.onDestruction: { @@ -145,7 +176,7 @@ Item { model: localWorkspaces Item { id: workspacePillContainer - height: (Settings.data.bar.showWorkspaceLabel !== "none") ? Math.round(18 * scaling) : Math.round(14 * scaling) + height: (userLabelMode !== "none") ? Math.round(18 * scaling) : Math.round(14 * scaling) width: root.calculatedWsWidth(model) Rectangle { @@ -153,15 +184,13 @@ Item { anchors.fill: parent Loader { - active: (Settings.data.bar.showWorkspaceLabel !== "none") + active: (userLabelMode !== "none") sourceComponent: Component { Text { - // Center horizontally x: (pill.width - width) / 2 - // Center vertically accounting for font metrics y: (pill.height - height) / 2 + (height - contentHeight) / 2 text: { - if (Settings.data.bar.showWorkspaceLabel === "name" && model.name && model.name.length > 0) { + if (userLabelMode === "name" && model.name && model.name.length > 0) { return model.name.substring(0, 2) } else { return model.idx.toString() diff --git a/Modules/SettingsPanel/Extras/BarSectionEditor.qml b/Modules/SettingsPanel/Extras/BarSectionEditor.qml index fb17c34..4ea82e2 100644 --- a/Modules/SettingsPanel/Extras/BarSectionEditor.qml +++ b/Modules/SettingsPanel/Extras/BarSectionEditor.qml @@ -188,13 +188,33 @@ NBox { colorBgHover: Qt.alpha(Color.mOnPrimary, Style.opacityLight) colorFgHover: Color.mOnPrimary onClicked: { - var dialog = Qt.createComponent("BarWidgetSettingsDialog.qml").createObject(root, { - "widgetIndex": index, - "widgetData": modelData, - "widgetId": modelData.id, - "parent": Overlay.overlay - }) - dialog.open() + var component = Qt.createComponent(Qt.resolvedUrl("BarWidgetSettingsDialog.qml")) + function instantiateAndOpen() { + var dialog = component.createObject(root, { + "widgetIndex": index, + "widgetData": modelData, + "widgetId": modelData.id, + "parent": Overlay.overlay + }) + if (dialog) { + dialog.open() + } else { + Logger.error("BarSectionEditor", "Failed to create settings dialog instance") + } + } + if (component.status === Component.Ready) { + instantiateAndOpen() + } else if (component.status === Component.Error) { + Logger.error("BarSectionEditor", component.errorString()) + } else { + component.statusChanged.connect(function () { + if (component.status === Component.Ready) { + instantiateAndOpen() + } else if (component.status === Component.Error) { + Logger.error("BarSectionEditor", component.errorString()) + } + }) + } } } } diff --git a/Modules/SettingsPanel/Extras/BarWidgetSettingsDialog.qml b/Modules/SettingsPanel/Extras/BarWidgetSettingsDialog.qml index f9aa98d..a7c0311 100644 --- a/Modules/SettingsPanel/Extras/BarWidgetSettingsDialog.qml +++ b/Modules/SettingsPanel/Extras/BarWidgetSettingsDialog.qml @@ -70,6 +70,26 @@ Popup { return customButtonSettings } else if (settingsPopup.widgetId === "Spacer") { return spacerSettings + } else if (settingsPopup.widgetId === "Workspace") { + return workspaceSettings + } else if (settingsPopup.widgetId === "SystemMonitor") { + return systemMonitorSettings + } else if (settingsPopup.widgetId === "ActiveWindow") { + return activeWindowSettings + } else if (settingsPopup.widgetId === "MediaMini") { + return mediaMiniSettings + } else if (settingsPopup.widgetId === "Clock") { + return clockSettings + } else if (settingsPopup.widgetId === "Volume") { + return volumeSettings + } else if (settingsPopup.widgetId === "Microphone") { + return microphoneSettings + } else if (settingsPopup.widgetId === "NotificationHistory") { + return notificationHistorySettings + } else if (settingsPopup.widgetId === "Brightness") { + return brightnessSettings + } else if (settingsPopup.widgetId === "SidePanelToggle") { + return sidePanelToggleSettings } // Add more widget settings components here as needed return null @@ -104,6 +124,279 @@ Popup { } } + // SidePanelToggle settings component + Component { + id: sidePanelToggleSettings + + ColumnLayout { + spacing: Style.marginM * scaling + + // Local state + property bool valueUseDistroLogo: settingsPopup.widgetData.useDistroLogo + !== undefined ? settingsPopup.widgetData.useDistroLogo : BarWidgetRegistry.widgetMetadata["SidePanelToggle"].useDistroLogo + + function saveSettings() { + var settings = Object.assign({}, settingsPopup.widgetData) + settings.useDistroLogo = valueUseDistroLogo + return settings + } + + NCheckbox { + label: "Use distro logo instead of icon" + checked: valueUseDistroLogo + onToggled: checked => valueUseDistroLogo = checked + } + } + } + + // Brightness settings component + Component { + id: brightnessSettings + + ColumnLayout { + spacing: Style.marginM * scaling + + // Local state + property bool valueAlwaysShowPercentage: settingsPopup.widgetData.alwaysShowPercentage + !== undefined ? settingsPopup.widgetData.alwaysShowPercentage : BarWidgetRegistry.widgetMetadata["Brightness"].alwaysShowPercentage + + function saveSettings() { + var settings = Object.assign({}, settingsPopup.widgetData) + settings.alwaysShowPercentage = valueAlwaysShowPercentage + return settings + } + + NCheckbox { + label: "Always show percentage" + checked: valueAlwaysShowPercentage + onToggled: checked => valueAlwaysShowPercentage = checked + } + } + } + + // NotificationHistory settings component + Component { + id: notificationHistorySettings + + ColumnLayout { + spacing: Style.marginM * scaling + + // Local state + property bool valueShowUnreadBadge: settingsPopup.widgetData.showUnreadBadge + !== undefined ? settingsPopup.widgetData.showUnreadBadge : BarWidgetRegistry.widgetMetadata["NotificationHistory"].showUnreadBadge + property bool valueHideWhenZero: settingsPopup.widgetData.hideWhenZero + !== undefined ? settingsPopup.widgetData.hideWhenZero : BarWidgetRegistry.widgetMetadata["NotificationHistory"].hideWhenZero + // Stage DND locally; commit on Save + property bool valueDoNotDisturbGlobal: Settings.data.notifications.doNotDisturb + + function saveSettings() { + var settings = Object.assign({}, settingsPopup.widgetData) + settings.showUnreadBadge = valueShowUnreadBadge + settings.hideWhenZero = valueHideWhenZero + Settings.data.notifications.doNotDisturb = valueDoNotDisturbGlobal + return settings + } + + NCheckbox { + label: "Show unread badge" + checked: valueShowUnreadBadge + onToggled: checked => valueShowUnreadBadge = checked + } + + NCheckbox { + label: "Hide badge when zero" + checked: valueHideWhenZero + onToggled: checked => valueHideWhenZero = checked + } + + NCheckbox { + label: "Do Not Disturb (notifications)" + description: "Toggle notifications 'Do Not Disturb'" + checked: valueDoNotDisturbGlobal + onToggled: checked => valueDoNotDisturbGlobal = checked + } + } + } + + // Microphone settings component + Component { + id: microphoneSettings + + ColumnLayout { + spacing: Style.marginM * scaling + + // Local state + property bool valueAlwaysShowPercentage: settingsPopup.widgetData.alwaysShowPercentage + !== undefined ? settingsPopup.widgetData.alwaysShowPercentage : BarWidgetRegistry.widgetMetadata["Microphone"].alwaysShowPercentage + + function saveSettings() { + var settings = Object.assign({}, settingsPopup.widgetData) + settings.alwaysShowPercentage = valueAlwaysShowPercentage + return settings + } + + NCheckbox { + label: "Always show percentage" + checked: valueAlwaysShowPercentage + onToggled: checked => valueAlwaysShowPercentage = checked + } + } + } + + // Volume settings component + Component { + id: volumeSettings + + ColumnLayout { + spacing: Style.marginM * scaling + + // Local state + property bool valueAlwaysShowPercentage: settingsPopup.widgetData.alwaysShowPercentage + !== undefined ? settingsPopup.widgetData.alwaysShowPercentage : BarWidgetRegistry.widgetMetadata["Volume"].alwaysShowPercentage + + function saveSettings() { + var settings = Object.assign({}, settingsPopup.widgetData) + settings.alwaysShowPercentage = valueAlwaysShowPercentage + return settings + } + + NCheckbox { + label: "Always show percentage" + checked: valueAlwaysShowPercentage + onToggled: checked => valueAlwaysShowPercentage = checked + } + } + } + + // Clock settings component + Component { + id: clockSettings + + ColumnLayout { + spacing: Style.marginM * scaling + + // Local state + property bool valueShowDate: settingsPopup.widgetData.showDate + !== undefined ? settingsPopup.widgetData.showDate : BarWidgetRegistry.widgetMetadata["Clock"].showDate + property bool valueUse12h: settingsPopup.widgetData.use12HourClock + !== undefined ? settingsPopup.widgetData.use12HourClock : BarWidgetRegistry.widgetMetadata["Clock"].use12HourClock + property bool valueShowSeconds: settingsPopup.widgetData.showSeconds + !== undefined ? settingsPopup.widgetData.showSeconds : BarWidgetRegistry.widgetMetadata["Clock"].showSeconds + + function saveSettings() { + var settings = Object.assign({}, settingsPopup.widgetData) + settings.showDate = valueShowDate + settings.use12HourClock = valueUse12h + settings.showSeconds = valueShowSeconds + return settings + } + + NCheckbox { + label: "Show date next to time" + checked: valueShowDate + onToggled: checked => valueShowDate = checked + } + + NCheckbox { + label: "Use 12-hour clock" + checked: valueUse12h + onToggled: checked => valueUse12h = checked + } + + NCheckbox { + label: "Show seconds" + checked: valueShowSeconds + onToggled: checked => valueShowSeconds = checked + } + } + } + + // MediaMini settings component + Component { + id: mediaMiniSettings + + ColumnLayout { + spacing: Style.marginM * scaling + + // Local state + property bool valueShowAlbumArt: settingsPopup.widgetData.showAlbumArt + !== undefined ? settingsPopup.widgetData.showAlbumArt : BarWidgetRegistry.widgetMetadata["MediaMini"].showAlbumArt + property bool valueShowVisualizer: settingsPopup.widgetData.showVisualizer + !== undefined ? settingsPopup.widgetData.showVisualizer : BarWidgetRegistry.widgetMetadata["MediaMini"].showVisualizer + property string valueVisualizerType: settingsPopup.widgetData.visualizerType + || BarWidgetRegistry.widgetMetadata["MediaMini"].visualizerType + + function saveSettings() { + var settings = Object.assign({}, settingsPopup.widgetData) + settings.showAlbumArt = valueShowAlbumArt + settings.showVisualizer = valueShowVisualizer + settings.visualizerType = valueVisualizerType + return settings + } + + NCheckbox { + label: "Show album art" + checked: valueShowAlbumArt + onToggled: checked => valueShowAlbumArt = checked + } + + NCheckbox { + label: "Show visualizer" + checked: valueShowVisualizer + onToggled: checked => valueShowVisualizer = checked + } + + NComboBox { + label: "Visualizer type" + description: "Select the visualizer style" + preferredWidth: 180 * scaling + model: ListModel { + ListElement { + key: "linear" + name: "Linear" + } + ListElement { + key: "mirrored" + name: "Mirrored" + } + ListElement { + key: "wave" + name: "Wave" + } + } + currentKey: valueVisualizerType + onSelected: key => valueVisualizerType = key + } + } + } + + // ActiveWindow settings component + Component { + id: activeWindowSettings + + ColumnLayout { + spacing: Style.marginM * scaling + + // Local, editable state + property bool valueShowIcon: settingsPopup.widgetData.showIcon + !== undefined ? settingsPopup.widgetData.showIcon : BarWidgetRegistry.widgetMetadata["ActiveWindow"].showIcon + + function saveSettings() { + var settings = Object.assign({}, settingsPopup.widgetData) + settings.showIcon = valueShowIcon + return settings + } + + NCheckbox { + id: showIcon + Layout.fillWidth: true + label: "Show app icon" + checked: valueShowIcon + onToggled: checked => valueShowIcon = checked + } + } + } + // CustomButton settings component Component { id: customButtonSettings @@ -183,4 +476,103 @@ Popup { } } } + + // Workspace settings component + Component { + id: workspaceSettings + + ColumnLayout { + spacing: Style.marginM * scaling + + function saveSettings() { + var settings = Object.assign({}, settingsPopup.widgetData) + settings.labelMode = labelModeCombo.currentKey + return settings + } + + NComboBox { + id: labelModeCombo + Layout.fillWidth: true + preferredWidth: 180 * scaling + label: "Label Mode" + description: "Choose how to label workspace pills." + model: ListModel { + ListElement { + key: "none" + name: "None" + } + ListElement { + key: "index" + name: "Index" + } + ListElement { + key: "name" + name: "Name" + } + } + currentKey: settingsPopup.widgetData.labelMode || BarWidgetRegistry.widgetMetadata["Workspace"].labelMode + onSelected: key => labelModeCombo.currentKey = key + } + } + } + + // SystemMonitor settings component + Component { + id: systemMonitorSettings + + ColumnLayout { + spacing: Style.marginM * scaling + + // Local, editable state for checkboxes + property bool valueShowCpuUsage: settingsPopup.widgetData.showCpuUsage + !== undefined ? settingsPopup.widgetData.showCpuUsage : BarWidgetRegistry.widgetMetadata["SystemMonitor"].showCpuUsage + property bool valueShowCpuTemp: settingsPopup.widgetData.showCpuTemp + !== undefined ? settingsPopup.widgetData.showCpuTemp : BarWidgetRegistry.widgetMetadata["SystemMonitor"].showCpuTemp + property bool valueShowMemoryUsage: settingsPopup.widgetData.showMemoryUsage + !== undefined ? settingsPopup.widgetData.showMemoryUsage : BarWidgetRegistry.widgetMetadata["SystemMonitor"].showMemoryUsage + property bool valueShowNetworkStats: settingsPopup.widgetData.showNetworkStats + !== undefined ? settingsPopup.widgetData.showNetworkStats : BarWidgetRegistry.widgetMetadata["SystemMonitor"].showNetworkStats + + function saveSettings() { + var settings = Object.assign({}, settingsPopup.widgetData) + settings.showCpuUsage = valueShowCpuUsage + settings.showCpuTemp = valueShowCpuTemp + settings.showMemoryUsage = valueShowMemoryUsage + settings.showNetworkStats = valueShowNetworkStats + return settings + } + + NCheckbox { + id: showCpuUsage + Layout.fillWidth: true + label: "CPU usage" + checked: valueShowCpuUsage + onToggled: checked => valueShowCpuUsage = checked + } + + NCheckbox { + id: showCpuTemp + Layout.fillWidth: true + label: "CPU temperature" + checked: valueShowCpuTemp + onToggled: checked => valueShowCpuTemp = checked + } + + NCheckbox { + id: showMemoryUsage + Layout.fillWidth: true + label: "Memory usage" + checked: valueShowMemoryUsage + onToggled: checked => valueShowMemoryUsage = checked + } + + NCheckbox { + id: showNetworkStats + Layout.fillWidth: true + label: "Network traffic" + checked: valueShowNetworkStats + onToggled: checked => valueShowNetworkStats = checked + } + } + } } diff --git a/Modules/SettingsPanel/Tabs/BarTab.qml b/Modules/SettingsPanel/Tabs/BarTab.qml index e1bf63a..2e6368f 100644 --- a/Modules/SettingsPanel/Tabs/BarTab.qml +++ b/Modules/SettingsPanel/Tabs/BarTab.qml @@ -71,56 +71,13 @@ ColumnLayout { } } - NToggle { - label: "Show Active Window's Icon" - description: "Display the app icon next to the title of the currently focused window." - checked: Settings.data.bar.showActiveWindowIcon - onToggled: checked => Settings.data.bar.showActiveWindowIcon = checked - } - + // Keep Battery toggle here for now (cannot test per-widget yet) NToggle { label: "Show Battery Percentage" description: "Display battery percentage at all times." checked: Settings.data.bar.alwaysShowBatteryPercentage onToggled: checked => Settings.data.bar.alwaysShowBatteryPercentage = checked } - - NToggle { - label: "Show Network Statistics" - description: "Display network upload and download speeds in the system monitor." - checked: Settings.data.bar.showNetworkStats - onToggled: checked => Settings.data.bar.showNetworkStats = checked - } - - NToggle { - label: "Replace SidePanel toggle with distro logo" - description: "Show distro logo instead of the SidePanel toggle button in the bar." - checked: Settings.data.bar.useDistroLogo - onToggled: checked => { - Settings.data.bar.useDistroLogo = checked - } - } - - NComboBox { - label: "Show Workspaces Labels" - description: "Show the workspace name or index within the workspace indicator." - model: ListModel { - ListElement { - key: "none" - name: "None" - } - ListElement { - key: "index" - name: "Index" - } - ListElement { - key: "name" - name: "Name" - } - } - currentKey: Settings.data.bar.showWorkspaceLabel - onSelected: key => Settings.data.bar.showWorkspaceLabel = key - } } NDivider { diff --git a/Services/BarWidgetRegistry.qml b/Services/BarWidgetRegistry.qml index 39afcd3..33d26f9 100644 --- a/Services/BarWidgetRegistry.qml +++ b/Services/BarWidgetRegistry.qml @@ -49,6 +49,61 @@ Singleton { "allowUserSettings": true, "icon": "space_bar", "width": 20 + }, + "ActiveWindow"// Per-instance settings for common widgets shown in BarTab + : { + "allowUserSettings": true, + "showIcon": true + }, + "Battery": { + "allowUserSettings": true, + "alwaysShowPercentage": false + }, + "SystemMonitor": { + "allowUserSettings": true, + "showCpuUsage": true, + "showCpuTemp": true, + "showMemoryUsage": true, + "showNetworkStats": false + }, + "Workspace": { + "allowUserSettings": true, + "labelMode"// none | index | name + : "index" + }, + "MediaMini": { + "allowUserSettings": true, + "showAlbumArt": false, + "showVisualizer": false, + "visualizerType"// linear | mirrored | wave + : "linear" + }, + "Clock": { + "allowUserSettings": true, + "showDate": false, + "use12HourClock": false, + "showSeconds": false + }, + "Volume": { + "allowUserSettings": true, + "alwaysShowPercentage": false + }, + "Microphone": { + "allowUserSettings": true, + "alwaysShowPercentage": false + }, + "Brightness": { + "allowUserSettings": true, + "alwaysShowPercentage": false + }, + "SidePanelToggle": { + "allowUserSettings": true, + "useDistroLogo": false + }, + "NotificationHistory": { + "allowUserSettings": true, + "showUnreadBadge": true, + "hideWhenZero": false } }) diff --git a/Widgets/NCheckbox.qml b/Widgets/NCheckbox.qml index 48d76e5..f903e8a 100644 --- a/Widgets/NCheckbox.qml +++ b/Widgets/NCheckbox.qml @@ -27,6 +27,11 @@ RowLayout { visible: root.label !== "" || root.description !== "" } + // Spacer to push the checkbox to the far right + Item { + Layout.fillWidth: true + } + Rectangle { id: box diff --git a/Widgets/NClock.qml b/Widgets/NClock.qml index aa8ce33..552388c 100644 --- a/Widgets/NClock.qml +++ b/Widgets/NClock.qml @@ -10,13 +10,32 @@ Rectangle { signal exited signal clicked + // Per-instance overrides (default to global settings if not provided by parent) + // Parent widgets like Bar `Clock.qml` can bind these + property bool showDate: Settings.data.location.showDateWithClock + property bool use12h: Settings.data.location.use12HourClock + property bool showSeconds: false + width: textItem.paintedWidth height: textItem.paintedHeight color: Color.transparent NText { id: textItem - text: Time.time + text: { + const now = Time.date + const timeFormat = use12h ? (showSeconds ? "h:mm:ss AP" : "h:mm AP") : (showSeconds ? "HH:mm:ss" : "HH:mm") + const timeString = Qt.formatDateTime(now, timeFormat) + + if (showDate) { + let dayName = now.toLocaleDateString(Qt.locale(), "ddd") + dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1) + let day = now.getDate() + let month = now.toLocaleDateString(Qt.locale(), "MMM") + return timeString + " - " + (Settings.data.location.reverseDayMonth ? `${dayName}, ${month} ${day}` : `${dayName}, ${day} ${month}`) + } + return timeString + } anchors.centerIn: parent font.pointSize: Style.fontSizeS * scaling font.weight: Style.fontWeightBold From 4578aad0bcb76b138c1860062c770c9d4a63ad6e Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 7 Sep 2025 14:57:09 +0200 Subject: [PATCH 15/54] NotificationHistory: properly hook up the unread counter --- Modules/Bar/Widgets/NotificationHistory.qml | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/Modules/Bar/Widgets/NotificationHistory.qml b/Modules/Bar/Widgets/NotificationHistory.qml index 9897d05..81ac749 100644 --- a/Modules/Bar/Widgets/NotificationHistory.qml +++ b/Modules/Bar/Widgets/NotificationHistory.qml @@ -33,7 +33,7 @@ NIconButton { readonly property bool userDoNotDisturb: (widgetSettings.doNotDisturb !== undefined) ? widgetSettings.doNotDisturb : BarWidgetRegistry.widgetMetadata["NotificationHistory"].doNotDisturb function lastSeenTs() { - return widgetSettings.lastSeenTs || 0 + return Settings.data.notifications?.lastSeenTs || 0 } function computeUnreadCount() { @@ -59,23 +59,9 @@ NIconButton { colorBorderHover: Color.transparent onClicked: { - // Open first using current geometry as anchor var panel = PanelService.getPanel("notificationHistoryPanel") panel?.toggle(screen, this) - // Update last seen right after to avoid affecting anchor calculation - Qt.callLater(function () { - try { - var section = barSection.replace("Section", "").toLowerCase() - if (section && sectionWidgetIndex >= 0) { - var widgets = Settings.data.bar.widgets[section] - if (widgets && sectionWidgetIndex < widgets.length) { - widgets[sectionWidgetIndex].lastSeenTs = Time.timestamp * 1000 - } - } - } catch (e) { - - } - }) + Settings.data.notifications.lastSeenTs = Time.timestamp * 1000 } onRightClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb From a2ea3c116db8a61e7465a70202b60538bfba9696 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 7 Sep 2025 15:09:30 +0200 Subject: [PATCH 16/54] NotificationHistory: better display for unread notifications --- Commons/Settings.qml | 2 ++ Modules/Bar/Widgets/NotificationHistory.qml | 13 +++++++++---- Services/NotificationService.qml | 12 ++++++++++-- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 097f5e9..b595323 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -299,6 +299,8 @@ Singleton { property JsonObject notifications: JsonObject { property bool doNotDisturb: false property list monitors: [] + // Last time the user opened the notification history (ms since epoch) + property double lastSeenTs: 0 } // audio diff --git a/Modules/Bar/Widgets/NotificationHistory.qml b/Modules/Bar/Widgets/NotificationHistory.qml index 81ac749..cf39b52 100644 --- a/Modules/Bar/Widgets/NotificationHistory.qml +++ b/Modules/Bar/Widgets/NotificationHistory.qml @@ -74,16 +74,21 @@ NIconButton { z: 2 active: userShowUnreadBadge && (!userHideWhenZero || computeUnreadCount() > 0) sourceComponent: Rectangle { - width: 16 * scaling + id: badge + readonly property int count: computeUnreadCount() + readonly property string label: count <= 99 ? String(count) : "99+" + readonly property real pad: 8 * scaling height: 16 * scaling - radius: width / 2 + width: Math.max(height, textNode.implicitWidth + pad) + radius: height / 2 color: Color.mError border.color: Color.mSurface border.width: 1 - visible: computeUnreadCount() > 0 || !userHideWhenZero + visible: count > 0 || !userHideWhenZero NText { + id: textNode anchors.centerIn: parent - text: Math.min(computeUnreadCount(), 9) + text: badge.label font.pointSize: Style.fontSizeXXS * scaling color: Color.mOnError } diff --git a/Services/NotificationService.qml b/Services/NotificationService.qml index da6ab32..608048e 100644 --- a/Services/NotificationService.qml +++ b/Services/NotificationService.qml @@ -201,12 +201,17 @@ Singleton { const items = historyAdapter.history || [] for (var i = 0; i < items.length; i++) { const it = items[i] + // Coerce legacy second-based timestamps to milliseconds + var ts = it.timestamp + if (typeof ts === "number" && ts < 1e12) { + ts = ts * 1000 + } historyModel.append({ "summary": it.summary || "", "body": it.body || "", "appName": it.appName || "", "urgency": it.urgency, - "timestamp": it.timestamp ? new Date(it.timestamp) : new Date() + "timestamp": ts ? new Date(ts) : new Date() }) } } catch (e) { @@ -225,7 +230,10 @@ Singleton { "body": n.body, "appName": n.appName, "urgency": n.urgency, - "timestamp": (n.timestamp instanceof Date) ? n.timestamp.getTime() : n.timestamp + "timestamp"// Always persist in milliseconds + : (n.timestamp instanceof Date) ? n.timestamp.getTime( + ) : (typeof n.timestamp === "number" + && n.timestamp < 1e12 ? n.timestamp * 1000 : n.timestamp) }) } historyAdapter.history = arr From dc0ef93680aa7e24257bf6f4f51194acb7d1fd3c Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 7 Sep 2025 15:18:12 +0200 Subject: [PATCH 17/54] Notification: DND just uses Settings.data.notifications.doNotDisturb now --- Modules/Bar/Widgets/NotificationHistory.qml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Modules/Bar/Widgets/NotificationHistory.qml b/Modules/Bar/Widgets/NotificationHistory.qml index cf39b52..b79e63c 100644 --- a/Modules/Bar/Widgets/NotificationHistory.qml +++ b/Modules/Bar/Widgets/NotificationHistory.qml @@ -30,7 +30,6 @@ NIconButton { readonly property bool userShowUnreadBadge: (widgetSettings.showUnreadBadge !== undefined) ? widgetSettings.showUnreadBadge : BarWidgetRegistry.widgetMetadata["NotificationHistory"].showUnreadBadge readonly property bool userHideWhenZero: (widgetSettings.hideWhenZero !== undefined) ? widgetSettings.hideWhenZero : BarWidgetRegistry.widgetMetadata["NotificationHistory"].hideWhenZero - readonly property bool userDoNotDisturb: (widgetSettings.doNotDisturb !== undefined) ? widgetSettings.doNotDisturb : BarWidgetRegistry.widgetMetadata["NotificationHistory"].doNotDisturb function lastSeenTs() { return Settings.data.notifications?.lastSeenTs || 0 @@ -50,11 +49,10 @@ NIconButton { } sizeRatio: 0.8 - icon: (Settings.data.notifications.doNotDisturb || userDoNotDisturb) ? "notifications_off" : "notifications" - tooltipText: (Settings.data.notifications.doNotDisturb - || userDoNotDisturb) ? "Notification history.\nRight-click to disable 'Do Not Disturb'." : "Notification history.\nRight-click to enable 'Do Not Disturb'." + icon: Settings.data.notifications.doNotDisturb ? "notifications_off" : "notifications" + tooltipText: Settings.data.notifications.doNotDisturb ? "Notification history.\nRight-click to disable 'Do Not Disturb'." : "Notification history.\nRight-click to enable 'Do Not Disturb'." colorBg: Color.mSurfaceVariant - colorFg: (Settings.data.notifications.doNotDisturb || userDoNotDisturb) ? Color.mError : Color.mOnSurface + colorFg: Settings.data.notifications.doNotDisturb ? Color.mError : Color.mOnSurface colorBorder: Color.transparent colorBorderHover: Color.transparent From c14eb95dba298439031e6693ff244750b75cf837 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 7 Sep 2025 15:20:24 +0200 Subject: [PATCH 18/54] BarWidgetSettingsDialog: remove DND, rename Save to Apply --- .../SettingsPanel/Extras/BarWidgetSettingsDialog.qml | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/Modules/SettingsPanel/Extras/BarWidgetSettingsDialog.qml b/Modules/SettingsPanel/Extras/BarWidgetSettingsDialog.qml index a7c0311..d4b661e 100644 --- a/Modules/SettingsPanel/Extras/BarWidgetSettingsDialog.qml +++ b/Modules/SettingsPanel/Extras/BarWidgetSettingsDialog.qml @@ -112,7 +112,7 @@ Popup { } NButton { - text: "Save" + text: "Apply" onClicked: { if (settingsLoader.item && settingsLoader.item.saveSettings) { var newSettings = settingsLoader.item.saveSettings() @@ -186,14 +186,11 @@ Popup { !== undefined ? settingsPopup.widgetData.showUnreadBadge : BarWidgetRegistry.widgetMetadata["NotificationHistory"].showUnreadBadge property bool valueHideWhenZero: settingsPopup.widgetData.hideWhenZero !== undefined ? settingsPopup.widgetData.hideWhenZero : BarWidgetRegistry.widgetMetadata["NotificationHistory"].hideWhenZero - // Stage DND locally; commit on Save - property bool valueDoNotDisturbGlobal: Settings.data.notifications.doNotDisturb function saveSettings() { var settings = Object.assign({}, settingsPopup.widgetData) settings.showUnreadBadge = valueShowUnreadBadge settings.hideWhenZero = valueHideWhenZero - Settings.data.notifications.doNotDisturb = valueDoNotDisturbGlobal return settings } @@ -208,13 +205,6 @@ Popup { checked: valueHideWhenZero onToggled: checked => valueHideWhenZero = checked } - - NCheckbox { - label: "Do Not Disturb (notifications)" - description: "Toggle notifications 'Do Not Disturb'" - checked: valueDoNotDisturbGlobal - onToggled: checked => valueDoNotDisturbGlobal = checked - } } } From 888ba108e097b847ad3c87decbf9140c0de9b5a7 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 7 Sep 2025 15:33:47 +0200 Subject: [PATCH 19/54] Edit NButton alignment --- Modules/SettingsPanel/Extras/BarWidgetSettingsDialog.qml | 1 + Widgets/NButton.qml | 5 ++++- Widgets/NIcon.qml | 4 ++++ Widgets/NText.qml | 1 + 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Modules/SettingsPanel/Extras/BarWidgetSettingsDialog.qml b/Modules/SettingsPanel/Extras/BarWidgetSettingsDialog.qml index d4b661e..2bf2d83 100644 --- a/Modules/SettingsPanel/Extras/BarWidgetSettingsDialog.qml +++ b/Modules/SettingsPanel/Extras/BarWidgetSettingsDialog.qml @@ -113,6 +113,7 @@ Popup { NButton { text: "Apply" + icon: "check" onClicked: { if (settingsLoader.item && settingsLoader.item.saveSettings) { var newSettings = settingsLoader.item.saveSettings() diff --git a/Widgets/NButton.qml b/Widgets/NButton.qml index 5648c46..75c9bc5 100644 --- a/Widgets/NButton.qml +++ b/Widgets/NButton.qml @@ -77,10 +77,12 @@ Rectangle { RowLayout { id: contentRow anchors.centerIn: parent - spacing: Style.marginS * scaling + spacing: Style.marginXS * scaling // Icon (optional) NIcon { + Layout.alignment: Qt.AlignVCenter + layoutTopMargin: 1 * scaling visible: root.icon !== "" text: root.icon font.pointSize: root.iconSize @@ -105,6 +107,7 @@ Rectangle { // Text NText { + Layout.alignment: Qt.AlignVCenter visible: root.text !== "" text: root.text font.pointSize: root.fontSize diff --git a/Widgets/NIcon.qml b/Widgets/NIcon.qml index 4a244aa..ac5a0ec 100644 --- a/Widgets/NIcon.qml +++ b/Widgets/NIcon.qml @@ -1,8 +1,11 @@ import QtQuick import qs.Commons import qs.Widgets +import QtQuick.Layouts Text { + // Optional layout nudge for optical alignment when used inside Layouts + property real layoutTopMargin: 0 text: "question_mark" font.family: "Material Symbols Rounded" font.pointSize: Style.fontSizeL * scaling @@ -12,4 +15,5 @@ Text { } color: Color.mOnSurface verticalAlignment: Text.AlignVCenter + Layout.topMargin: layoutTopMargin } diff --git a/Widgets/NText.qml b/Widgets/NText.qml index 00f5561..c15198d 100644 --- a/Widgets/NText.qml +++ b/Widgets/NText.qml @@ -13,4 +13,5 @@ Text { font.kerning: true color: Color.mOnSurface renderType: Text.QtRendering + verticalAlignment: Text.AlignVCenter } From e4e2ed41b486203df648996d480e1cb2861dee41 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 7 Sep 2025 15:48:16 +0200 Subject: [PATCH 20/54] Rename TimeWeatherTab to WeatherTab, remove Time settings from said tab WeatherTab: renamed from TimeWeatherTab, remove Time settings Time: Time/Date is now widget driven --- Commons/Time.qml | 14 ------- Modules/SettingsPanel/SettingsPanel.qml | 14 +++---- .../{TimeWeatherTab.qml => WeatherTab.qml} | 40 ------------------- 3 files changed, 7 insertions(+), 61 deletions(-) rename Modules/SettingsPanel/Tabs/{TimeWeatherTab.qml => WeatherTab.qml} (65%) diff --git a/Commons/Time.qml b/Commons/Time.qml index d7ec78e..0d70d26 100644 --- a/Commons/Time.qml +++ b/Commons/Time.qml @@ -9,21 +9,7 @@ Singleton { id: root property var date: new Date() - property string time: { - let timeFormat = Settings.data.location.use12HourClock ? "h:mm AP" : "HH:mm" - let timeString = Qt.formatDateTime(date, timeFormat) - if (Settings.data.location.showDateWithClock) { - let dayName = date.toLocaleDateString(Qt.locale(), "ddd") - dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1) - let day = date.getDate() - let month = date.toLocaleDateString(Qt.locale(), "MMM") - - return timeString + " - " + (Settings.data.location.reverseDayMonth ? `${dayName}, ${month} ${day}` : `${dayName}, ${day} ${month}`) - } - - return timeString - } readonly property string dateString: { let now = date let dayName = now.toLocaleDateString(Qt.locale(), "ddd") diff --git a/Modules/SettingsPanel/SettingsPanel.qml b/Modules/SettingsPanel/SettingsPanel.qml index 8bfbe77..ea8c701 100644 --- a/Modules/SettingsPanel/SettingsPanel.qml +++ b/Modules/SettingsPanel/SettingsPanel.qml @@ -39,7 +39,7 @@ NPanel { General, Network, ScreenRecorder, - TimeWeather, + Weather, Wallpaper, WallpaperSelector } @@ -90,8 +90,8 @@ NPanel { Tabs.NetworkTab {} } Component { - id: timeWeatherTab - Tabs.TimeWeatherTab {} + id: weatherTab + Tabs.WeatherTab {} } Component { id: colorSchemeTab @@ -156,10 +156,10 @@ NPanel { "icon": "brightness_6", "source": brightnessTab }, { - "id": SettingsPanel.Tab.TimeWeather, - "label": "Time & Weather", - "icon": "schedule", - "source": timeWeatherTab + "id": SettingsPanel.Tab.Weather, + "label": "Weather", + "icon": "partly_cloudy_day", + "source": weatherTab }, { "id": SettingsPanel.Tab.ColorScheme, "label": "Color Scheme", diff --git a/Modules/SettingsPanel/Tabs/TimeWeatherTab.qml b/Modules/SettingsPanel/Tabs/WeatherTab.qml similarity index 65% rename from Modules/SettingsPanel/Tabs/TimeWeatherTab.qml rename to Modules/SettingsPanel/Tabs/WeatherTab.qml index 2fa89dd..7553158 100644 --- a/Modules/SettingsPanel/Tabs/TimeWeatherTab.qml +++ b/Modules/SettingsPanel/Tabs/WeatherTab.qml @@ -52,46 +52,6 @@ ColumnLayout { Layout.bottomMargin: Style.marginXL * scaling } - // Time section - ColumnLayout { - spacing: Style.marginL * scaling - Layout.fillWidth: true - - NText { - text: "Time Format" - font.pointSize: Style.fontSizeXXL * scaling - font.weight: Style.fontWeightBold - color: Color.mSecondary - } - - NToggle { - label: "Use 12-Hour Clock" - description: "Display time in 12-hour format (AM/PM) instead of 24-hour." - checked: Settings.data.location.use12HourClock - onToggled: checked => Settings.data.location.use12HourClock = checked - } - - NToggle { - label: "Reverse Day/Month" - description: "Display date as dd/mm instead of mm/dd." - checked: Settings.data.location.reverseDayMonth - onToggled: checked => Settings.data.location.reverseDayMonth = checked - } - - NToggle { - label: "Show Date with Clock" - description: "Display date alongside time (e.g., 18:12 - Sat, 23 Aug)." - checked: Settings.data.location.showDateWithClock - onToggled: checked => Settings.data.location.showDateWithClock = checked - } - } - - NDivider { - Layout.fillWidth: true - Layout.topMargin: Style.marginXL * scaling - Layout.bottomMargin: Style.marginXL * scaling - } - // Weather section ColumnLayout { spacing: Style.marginM * scaling From 4ba0f8d958a43ca6782cc6eb4e0f16a268d3e17a Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sun, 7 Sep 2025 10:06:53 -0400 Subject: [PATCH 21/54] Network: Scanning use a more reliable backward parsing + added logs to figure potential bug. --- Services/NetworkService.qml | 166 +++++++++++++++++++----------------- 1 file changed, 87 insertions(+), 79 deletions(-) diff --git a/Services/NetworkService.qml b/Services/NetworkService.qml index ee22010..0808a1d 100644 --- a/Services/NetworkService.qml +++ b/Services/NetworkService.qml @@ -92,9 +92,6 @@ Singleton { function setWifiEnabled(enabled) { Settings.data.network.wifiEnabled = enabled - - wifiToggleProcess.action = enabled ? "on" : "off" - wifiToggleProcess.running = true } function scan() { @@ -103,7 +100,9 @@ Singleton { scanning = true lastError = "" - scanProcess.running = true + + // Get existing profiles first, then scan + profileCheckProcess.running = true Logger.log("Network", "Wi-Fi scan in progress...") } @@ -176,8 +175,7 @@ Singleton { "ssid": ssid, "security": "--", "signal": 100, - "connected"// Default to good signal until real scan - : true, + "connected": true, "existing": true, "cached": true } @@ -241,30 +239,23 @@ Singleton { } } + // Helper process to get existing profiles Process { - id: wifiToggleProcess - property string action: "on" + id: profileCheckProcess running: false - command: ["nmcli", "radio", "wifi", action] + command: ["nmcli", "-t", "-f", "NAME", "connection", "show"] - onRunningChanged: { - if (!running) { - if (action === "on") { - // Clear networks immediately and start delayed scan - root.networks = ({}) - delayedScanTimer.interval = 8000 - delayedScanTimer.restart() - } else { - root.networks = ({}) - } - } - } - - stderr: StdioCollector { + stdout: StdioCollector { onStreamFinished: { - if (text.trim()) { - Logger.warn("Network", "WiFi toggle error: " + text) + const profiles = {} + const lines = text.split("\n").filter(l => l.trim()) + for (const line of lines) { + profiles[line.trim()] = true } + + Logger.log("Network", "Got profiles", JSON.stringify(profiles)) + scanProcess.existingProfiles = profiles + scanProcess.running = true } } } @@ -272,74 +263,90 @@ Singleton { Process { id: scanProcess running: false - command: ["sh", "-c", ` - # Get list of saved connection profiles (just the names) - profiles=$(nmcli -t -f NAME connection show | tr '\n' '|') + command: ["nmcli", "-t", "-f", "SSID,SECURITY,SIGNAL,IN-USE", "device", "wifi", "list", "--rescan", "yes"] - # Get WiFi networks - nmcli -t -f SSID,SECURITY,SIGNAL,IN-USE device wifi list --rescan yes | while read line; do - ssid=$(echo "$line" | cut -d: -f1) - security=$(echo "$line" | cut -d: -f2) - signal=$(echo "$line" | cut -d: -f3) - in_use=$(echo "$line" | cut -d: -f4) - - # Skip empty SSIDs - if [ -z "$ssid" ]; then - continue - fi - - # Check if SSID matches any profile name (simple check) - # This covers most cases where profile name equals or contains the SSID - existing=false - if echo "$profiles" | grep -qF "$ssid|"; then - existing=true - fi - - echo "$ssid|$security|$signal|$in_use|$existing" - done - `] + // Store existing profiles + property var existingProfiles: ({}) stdout: StdioCollector { onStreamFinished: { - const nets = {} - const lines = text.split("\n").filter(l => l.trim()) + const lines = text.split("\n") + const networksMap = {} - for (const line of lines) { - const parts = line.split("|") - if (parts.length < 5) + for (var i = 0; i < lines.length; ++i) { + const line = lines[i].trim() + if (!line) continue - const ssid = parts[0] - if (!ssid || ssid.trim() === "") - continue - - const network = { - "ssid": ssid, - "security": parts[1] || "--", - "signal": parseInt(parts[2]) || 0, - "connected": parts[3] === "*", - "existing": parts[4] === "true", - "cached": ssid in cacheAdapter.knownNetworks + // Parse from the end to handle SSIDs with colons + // Format is SSID:SECURITY:SIGNAL:IN-USE + // We know the last 3 fields, so everything else is SSID + const lastColonIdx = line.lastIndexOf(":") + if (lastColonIdx === -1) { + Logger.warn("Network", "Malformed nmcli output line:", line) + continue } - // Track connected network - if (network.connected && cacheAdapter.lastConnected !== ssid) { - cacheAdapter.lastConnected = ssid - saveCache() + const inUse = line.substring(lastColonIdx + 1) + const remainingLine = line.substring(0, lastColonIdx) + + const secondLastColonIdx = remainingLine.lastIndexOf(":") + if (secondLastColonIdx === -1) { + Logger.warn("Network", "Malformed nmcli output line:", line) + continue } - // Keep best signal for duplicate SSIDs - if (!nets[ssid] || network.signal > nets[ssid].signal) { - nets[ssid] = network + const signal = remainingLine.substring(secondLastColonIdx + 1) + const remainingLine2 = remainingLine.substring(0, secondLastColonIdx) + + const thirdLastColonIdx = remainingLine2.lastIndexOf(":") + if (thirdLastColonIdx === -1) { + Logger.warn("Network", "Malformed nmcli output line:", line) + continue + } + + const security = remainingLine2.substring(thirdLastColonIdx + 1) + const ssid = remainingLine2.substring(0, thirdLastColonIdx) + + if (ssid) { + const signalInt = parseInt(signal) || 0 + const connected = inUse === "*" + + // Track connected network in cache + if (connected && cacheAdapter.lastConnected !== ssid) { + cacheAdapter.lastConnected = ssid + saveCache() + } + + if (!networksMap[ssid]) { + networksMap[ssid] = { + "ssid": ssid, + "security": security || "--", + "signal": signalInt, + "connected": connected, + "existing": ssid in scanProcess.existingProfiles, + "cached": ssid in cacheAdapter.knownNetworks + } + } else { + // Keep the best signal for duplicate SSIDs + const existingNet = networksMap[ssid] + if (connected) { + existingNet.connected = true + } + if (signalInt > existingNet.signal) { + existingNet.signal = signalInt + existingNet.security = security || "--" + } + } } } - // For logging purpose only - Logger.log("Network", "Wi-Fi scan completed") + // Logging const oldSSIDs = Object.keys(root.networks) - const newSSIDs = Object.keys(nets) + const newSSIDs = Object.keys(networksMap) const newNetworks = newSSIDs.filter(ssid => !oldSSIDs.includes(ssid)) const lostNetworks = oldSSIDs.filter(ssid => !newSSIDs.includes(ssid)) + if (newNetworks.length > 0 || lostNetworks.length > 0) { if (newNetworks.length > 0) { Logger.log("Network", "New Wi-Fi SSID discovered:", newNetworks.join(", ")) @@ -347,11 +354,12 @@ Singleton { if (lostNetworks.length > 0) { Logger.log("Network", "Wi-Fi SSID disappeared:", lostNetworks.join(", ")) } - Logger.log("Network", "Total Wi-Fi SSIDs:", Object.keys(nets).length) + Logger.log("Network", "Total Wi-Fi SSIDs:", Object.keys(networksMap).length) } - // Assign the results - root.networks = nets + Logger.log("Network", "Wi-Fi scan completed") + Logger.log("Network", JSON.stringify(networksMap)) + root.networks = networksMap root.scanning = false } } From c32a8a863adf3dceede4a98ff8af327b64d5b9fe Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 7 Sep 2025 16:22:07 +0200 Subject: [PATCH 22/54] WeatherTab: remove useless divider --- Modules/SettingsPanel/Tabs/WeatherTab.qml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Modules/SettingsPanel/Tabs/WeatherTab.qml b/Modules/SettingsPanel/Tabs/WeatherTab.qml index 7553158..667aca5 100644 --- a/Modules/SettingsPanel/Tabs/WeatherTab.qml +++ b/Modules/SettingsPanel/Tabs/WeatherTab.qml @@ -71,10 +71,4 @@ ColumnLayout { onToggled: checked => Settings.data.location.useFahrenheit = checked } } - - NDivider { - Layout.fillWidth: true - Layout.topMargin: Style.marginXL * scaling - Layout.bottomMargin: Style.marginXL * scaling - } } From d6e253fe7f616ef037cc9f12268800485abce349 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 7 Sep 2025 16:25:11 +0200 Subject: [PATCH 23/54] Replace some double with real --- Commons/Settings.qml | 2 +- Services/GitHubService.qml | 2 +- Services/NotificationService.qml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Commons/Settings.qml b/Commons/Settings.qml index b595323..38356da 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -300,7 +300,7 @@ Singleton { property bool doNotDisturb: false property list monitors: [] // Last time the user opened the notification history (ms since epoch) - property double lastSeenTs: 0 + property real lastSeenTs: 0 } // audio diff --git a/Services/GitHubService.qml b/Services/GitHubService.qml index 2523a50..99dc283 100644 --- a/Services/GitHubService.qml +++ b/Services/GitHubService.qml @@ -45,7 +45,7 @@ Singleton { property string version: "Unknown" property var contributors: [] - property double timestamp: 0 + property real timestamp: 0 } } diff --git a/Services/NotificationService.qml b/Services/NotificationService.qml index 608048e..f30269f 100644 --- a/Services/NotificationService.qml +++ b/Services/NotificationService.qml @@ -80,7 +80,7 @@ Singleton { JsonAdapter { id: historyAdapter property var history: [] - property double timestamp: 0 + property real timestamp: 0 } } From ba3345195789fdf212a5b6d7ba1cc823b0cb0fb5 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sun, 7 Sep 2025 13:02:13 -0400 Subject: [PATCH 24/54] Network/Wi-Fi: many fixes and robustness improvements - proper detection when password is wrong - prevent a new connection while already connecting to a network - new mechanism to skip scan results if a new scan is incoming (avoid UI discrepancies) --- Modules/WiFiPanel/WiFiPanel.qml | 5 ++- Services/NetworkService.qml | 77 +++++++++++++++++++++++++++------ 2 files changed, 67 insertions(+), 15 deletions(-) diff --git a/Modules/WiFiPanel/WiFiPanel.qml b/Modules/WiFiPanel/WiFiPanel.qml index 1fa5f18..627be65 100644 --- a/Modules/WiFiPanel/WiFiPanel.qml +++ b/Modules/WiFiPanel/WiFiPanel.qml @@ -397,6 +397,7 @@ NPanel { } outlined: !hovered fontSize: Style.fontSizeXS * scaling + enabled: !NetworkService.connecting onClicked: { if (modelData.existing || modelData.cached || !NetworkService.isSecured(modelData.security)) { NetworkService.connect(modelData.ssid) @@ -461,7 +462,7 @@ NPanel { onVisibleChanged: if (visible) forceActiveFocus() onAccepted: { - if (text) { + if (text && !NetworkService.connecting) { NetworkService.connect(passwordSsid, text) passwordSsid = "" passwordInput = "" @@ -481,7 +482,7 @@ NPanel { NButton { text: "Connect" fontSize: Style.fontSizeXXS * scaling - enabled: passwordInput.length > 0 + enabled: passwordInput.length > 0 && !NetworkService.connecting outlined: true onClicked: { NetworkService.connect(passwordSsid, passwordInput) diff --git a/Services/NetworkService.qml b/Services/NetworkService.qml index 0808a1d..4e61618 100644 --- a/Services/NetworkService.qml +++ b/Services/NetworkService.qml @@ -18,6 +18,9 @@ Singleton { property string disconnectingFrom: "" property string forgettingNetwork: "" + property bool ignoreScanResults: false + property bool scanPending: false + // Persistent cache property string cacheFile: Settings.cacheDir + "network.json" readonly property string cachedLastConnected: cacheAdapter.lastConnected @@ -95,11 +98,20 @@ Singleton { } function scan() { - if (scanning) + if (!Settings.data.network.wifiEnabled) return + if (scanning) { + // Mark current scan results to be ignored and schedule a new scan + Logger.log("Network", "Scan already in progress, will ignore results and rescan") + ignoreScanResults = true + scanPending = true + return + } + scanning = true lastError = "" + ignoreScanResults = false // Get existing profiles first, then scan profileCheckProcess.running = true @@ -247,13 +259,24 @@ Singleton { stdout: StdioCollector { onStreamFinished: { + if (root.ignoreScanResults) { + Logger.log("Network", "Ignoring profile check results (new scan requested)") + root.scanning = false + + // Check if we need to start a new scan + if (root.scanPending) { + root.scanPending = false + delayedScanTimer.interval = 100 + delayedScanTimer.restart() + } + return + } + const profiles = {} const lines = text.split("\n").filter(l => l.trim()) for (const line of lines) { profiles[line.trim()] = true } - - Logger.log("Network", "Got profiles", JSON.stringify(profiles)) scanProcess.existingProfiles = profiles scanProcess.running = true } @@ -265,11 +288,24 @@ Singleton { running: false command: ["nmcli", "-t", "-f", "SSID,SECURITY,SIGNAL,IN-USE", "device", "wifi", "list", "--rescan", "yes"] - // Store existing profiles property var existingProfiles: ({}) stdout: StdioCollector { onStreamFinished: { + if (root.ignoreScanResults) { + Logger.log("Network", "Ignoring scan results (new scan requested)") + root.scanning = false + + // Check if we need to start a new scan + if (root.scanPending) { + root.scanPending = false + delayedScanTimer.interval = 100 + delayedScanTimer.restart() + } + return + } + + // Process the scan results as before... const lines = text.split("\n") const networksMap = {} @@ -358,9 +394,15 @@ Singleton { } Logger.log("Network", "Wi-Fi scan completed") - Logger.log("Network", JSON.stringify(networksMap)) root.networks = networksMap root.scanning = false + + // Check if we need to start a new scan + if (root.scanPending) { + root.scanPending = false + delayedScanTimer.interval = 100 + delayedScanTimer.restart() + } } } @@ -369,16 +411,14 @@ Singleton { root.scanning = false if (text.trim()) { Logger.warn("Network", "Scan error: " + text) - // If scan fails, set a short retry - if (Settings.data.network.wifiEnabled) { - delayedScanTimer.interval = 5000 - delayedScanTimer.restart() - } + + // If scan fails, retry + delayedScanTimer.interval = 5000 + delayedScanTimer.restart() } } } } - Process { id: connectProcess property string mode: "new" @@ -400,6 +440,17 @@ Singleton { stdout: StdioCollector { onStreamFinished: { + // Check if the output actually indicates success + // nmcli outputs "Device '...' successfully activated" or "Connection successfully activated" + // on success. Empty output or other messages indicate failure. + const output = text.trim() + + if (!output || (!output.includes("successfully activated") && !output.includes("Connection successfully"))) { + // No success message - likely an error occurred + // Don't update anything, let stderr handler deal with it + return + } + // Success - update cache let known = cacheAdapter.knownNetworks known[connectProcess.ssid] = { @@ -474,7 +525,7 @@ Singleton { Logger.warn("Network", "Disconnect error: " + text) } // Still trigger a scan even on error - delayedScanTimer.interval = 1000 + delayedScanTimer.interval = 5000 delayedScanTimer.restart() } } @@ -545,7 +596,7 @@ Singleton { Logger.warn("Network", "Forget error: " + text) } // Still Trigger a scan even on error - delayedScanTimer.interval = 1000 + delayedScanTimer.interval = 5000 delayedScanTimer.restart() } } From 498ee478e70424d5273c7436f16d79ceca89eff5 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sun, 7 Sep 2025 14:28:50 -0400 Subject: [PATCH 25/54] Settings: centralized migration to user settings. wip --- Commons/Settings.qml | 88 +++++++++++++++++++++++----------- Commons/Time.qml | 34 ++++++------- Modules/Bar/Widgets/Clock.qml | 54 ++++++++++++++------- Services/BarWidgetRegistry.qml | 3 +- Widgets/NClock.qml | 53 -------------------- 5 files changed, 115 insertions(+), 117 deletions(-) delete mode 100644 Widgets/NClock.qml diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 38356da..82277d8 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -71,34 +71,64 @@ Singleton { // ----------------------------------------------------- // If the settings structure has changed, ensure - // backward compatibility + // backward compatibility by upgrading the settings function upgradeSettingsData() { - for (var i = 0; i < adapter.bar.widgets.left.length; i++) { - var obj = adapter.bar.widgets.left[i] - if (typeof obj === "string") { - adapter.bar.widgets.left[i] = { - "id": obj + + const sections = ["left", "center", "right"] + + // ----------------- + // 1st. check our settings are not super old, when we only had the widget type as a string + for (var s = 0; s < sections.length; s++) { + const sectionName = sections[s] + for (var i = 0; i < adapter.bar.widgets[sectionName].length; i++) { + var widget = adapter.bar.widgets[sectionName][i] + if (typeof widget === "string") { + console.log("founf old stuff") + adapter.bar.widgets[sectionName][i] = { + "id": widget + } } } } - for (var i = 0; i < adapter.bar.widgets.center.length; i++) { - var obj = adapter.bar.widgets.center[i] - if (typeof obj === "string") { - adapter.bar.widgets.center[i] = { - "id": obj + + // ----------------- + // 2nd. migrate global settings to user settings + for (var s = 0; s < sections.length; s++) { + const sectionName = sections[s] + for (var i = 0; i < adapter.bar.widgets[sectionName].length; i++) { + var widget = adapter.bar.widgets[sectionName][i] + + // Check if widget registry supports user settings, if it does not, then there is nothing to do + const reg = BarWidgetRegistry.widgetMetadata[widget.id] + if ((reg === undefined) || (reg.allowUserSettings === undefined) || !reg.allowUserSettings) { + continue } - } - } - for (var i = 0; i < adapter.bar.widgets.right.length; i++) { - var obj = adapter.bar.widgets.right[i] - if (typeof obj === "string") { - adapter.bar.widgets.right[i] = { - "id": obj + + // Check that the widget was not previously migrated and skip if necessary + const keys = Object.keys(widget) + if (keys.length > 1) { + continue } + + _migrateWidget(widget) + Logger.log("Settings", JSON.stringify(widget)) } } } + // ----------------------------------------------------- + function _migrateWidget(widget) { + Logger.log("Settings", `Migrating '${widget.id}' widget`) + + switch (widget.id) { + case "Clock": + widget.showDate = adapter.location.showDateWithClock + widget.use12HourClock = adapter.location.use12HourClock + widget.reverseDayMonth = adapter.location.reverseDayMonth + widget.showSeconds = BarWidgetRegistry.widgetMetadata[widget.id].reverseDayMonth + break + } + } // ----------------------------------------------------- // Kickoff essential services function kickOffServices() { @@ -174,13 +204,13 @@ Singleton { // bar property JsonObject bar: JsonObject { - property string position: "top" // Possible values: "top", "bottom" - property bool showActiveWindowIcon: true - property bool alwaysShowBatteryPercentage: false - property bool showNetworkStats: false + property string position: "top" // "top" or "bottom" + property bool showActiveWindowIcon: true // TODO: delete + property bool alwaysShowBatteryPercentage: false // TODO: delete + property bool showNetworkStats: false // TODO: delete property real backgroundOpacity: 1.0 - property bool useDistroLogo: false - property string showWorkspaceLabel: "none" + property bool useDistroLogo: false // TODO: delete + property string showWorkspaceLabel: "none" // TODO: delete property list monitors: [] // Widget configuration for modular bar system @@ -236,9 +266,9 @@ Singleton { property JsonObject location: JsonObject { property string name: defaultLocation property bool useFahrenheit: false - property bool reverseDayMonth: false - property bool use12HourClock: false - property bool showDateWithClock: false + property bool reverseDayMonth: false // TODO: delete + property bool use12HourClock: false // TODO: delete + property bool showDateWithClock: false // TODO: delete } // screen recorder @@ -305,8 +335,8 @@ Singleton { // audio property JsonObject audio: JsonObject { - property bool showMiniplayerAlbumArt: false - property bool showMiniplayerCava: false + property bool showMiniplayerAlbumArt: false // TODO: delete + property bool showMiniplayerCava: false // TODO: delete property string visualizerType: "linear" property int volumeStep: 5 property int cavaFrameRate: 60 diff --git a/Commons/Time.qml b/Commons/Time.qml index 0d70d26..c086f5b 100644 --- a/Commons/Time.qml +++ b/Commons/Time.qml @@ -10,37 +10,37 @@ Singleton { property var date: new Date() - readonly property string dateString: { + // Returns a Unix Timestamp (in seconds) + readonly property int timestamp: { + return Math.floor(date / 1000) + } + + function formatDate(reverseDayMonth = true) { let now = date let dayName = now.toLocaleDateString(Qt.locale(), "ddd") dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1) let day = now.getDate() let suffix if (day > 3 && day < 21) - suffix = 'th' + suffix = 'th' else - switch (day % 10) { + switch (day % 10) { case 1: - suffix = "st" - break + suffix = "st" + break case 2: - suffix = "nd" - break + suffix = "nd" + break case 3: - suffix = "rd" - break + suffix = "rd" + break default: - suffix = "th" - } + suffix = "th" + } let month = now.toLocaleDateString(Qt.locale(), "MMMM") let year = now.toLocaleDateString(Qt.locale(), "yyyy") - return `${dayName}, ` - + (Settings.data.location.reverseDayMonth ? `${month} ${day}${suffix} ${year}` : `${day}${suffix} ${month} ${year}`) - } - // Returns a Unix Timestamp (in seconds) - readonly property int timestamp: { - return Math.floor(date / 1000) + return `${dayName}, ` + (reverseDayMonth ? `${month} ${day}${suffix} ${year}` : `${day}${suffix} ${month} ${year}`) } diff --git a/Modules/Bar/Widgets/Clock.qml b/Modules/Bar/Widgets/Clock.qml index 38b964b..a9bb3de 100644 --- a/Modules/Bar/Widgets/Clock.qml +++ b/Modules/Bar/Widgets/Clock.qml @@ -27,11 +27,14 @@ Rectangle { return {} } - // Use settings or defaults from BarWidgetRegistry metadata - readonly property bool userShowDate: (widgetSettings.showDate - !== undefined) ? widgetSettings.showDate : BarWidgetRegistry.widgetMetadata["Clock"].showDate - readonly property bool userUse12h: (widgetSettings.use12HourClock !== undefined) ? widgetSettings.use12HourClock : BarWidgetRegistry.widgetMetadata["Clock"].use12HourClock - readonly property bool userShowSeconds: (widgetSettings.showSeconds !== undefined) ? widgetSettings.showSeconds : BarWidgetRegistry.widgetMetadata["Clock"].showSeconds + // Resolve settings: try user settings or defaults from BarWidgetRegistry + readonly property bool showDate: widgetSettings.showDate || BarWidgetRegistry.widgetMetadata["Clock"].showDate + readonly property bool use12h: widgetSettings.use12HourClock + || BarWidgetRegistry.widgetMetadata["Clock"].use12HourClock + readonly property bool showSeconds: widgetSettings.showSeconds + || BarWidgetRegistry.widgetMetadata["Clock"].showSeconds + readonly property bool reverseDayMonth: widgetSettings.reverseDayMonth + || BarWidgetRegistry.widgetMetadata["Clock"].reverseDayMonth implicitWidth: clock.width + Style.marginM * 2 * scaling implicitHeight: Math.round(Style.capsuleHeight * scaling) @@ -39,22 +42,39 @@ Rectangle { color: Color.mSurfaceVariant // Clock Icon with attached calendar - NClock { + NText { id: clock - anchors.verticalCenter: parent.verticalCenter - anchors.horizontalCenter: parent.horizontalCenter - // Per-instance overrides to Time formatting - showDate: userShowDate - use12h: userUse12h - showSeconds: userShowSeconds + text: { + const now = Time.date + const timeFormat = use12h ? (showSeconds ? "h:mm:ss AP" : "h:mm AP") : (showSeconds ? "HH:mm:ss" : "HH:mm") + const timeString = Qt.formatDateTime(now, timeFormat) - NTooltip { - id: tooltip - text: `${Time.dateString}.` - target: clock - positionAbove: Settings.data.bar.position === "bottom" + if (showDate) { + let dayName = now.toLocaleDateString(Qt.locale(), "ddd") + dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1) + let day = now.getDate() + let month = now.toLocaleDateString(Qt.locale(), "MMM") + return timeString + " - " + (reverseDayMonth ? `${dayName}, ${month} ${day}` : `${dayName}, ${day} ${month}`) + } + return timeString } + anchors.centerIn: parent + font.pointSize: Style.fontSizeS * scaling + font.weight: Style.fontWeightBold + } + NTooltip { + id: tooltip + text: `${Time.formatDate(reverseDayMonth)}.` + target: clock + positionAbove: Settings.data.bar.position === "bottom" + } + + MouseArea { + id: clockMouseArea + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true onEntered: { if (!PanelService.getPanel("calendarPanel")?.active) { tooltip.show() diff --git a/Services/BarWidgetRegistry.qml b/Services/BarWidgetRegistry.qml index 33d26f9..48e710e 100644 --- a/Services/BarWidgetRegistry.qml +++ b/Services/BarWidgetRegistry.qml @@ -82,7 +82,8 @@ Singleton { "allowUserSettings": true, "showDate": false, "use12HourClock": false, - "showSeconds": false + "showSeconds": false, + "reverseDayMonth": true }, "Volume": { "allowUserSettings": true, diff --git a/Widgets/NClock.qml b/Widgets/NClock.qml deleted file mode 100644 index 552388c..0000000 --- a/Widgets/NClock.qml +++ /dev/null @@ -1,53 +0,0 @@ -import QtQuick -import qs.Commons -import qs.Services -import qs.Widgets - -Rectangle { - id: root - - signal entered - signal exited - signal clicked - - // Per-instance overrides (default to global settings if not provided by parent) - // Parent widgets like Bar `Clock.qml` can bind these - property bool showDate: Settings.data.location.showDateWithClock - property bool use12h: Settings.data.location.use12HourClock - property bool showSeconds: false - - width: textItem.paintedWidth - height: textItem.paintedHeight - color: Color.transparent - - NText { - id: textItem - text: { - const now = Time.date - const timeFormat = use12h ? (showSeconds ? "h:mm:ss AP" : "h:mm AP") : (showSeconds ? "HH:mm:ss" : "HH:mm") - const timeString = Qt.formatDateTime(now, timeFormat) - - if (showDate) { - let dayName = now.toLocaleDateString(Qt.locale(), "ddd") - dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1) - let day = now.getDate() - let month = now.toLocaleDateString(Qt.locale(), "MMM") - return timeString + " - " + (Settings.data.location.reverseDayMonth ? `${dayName}, ${month} ${day}` : `${dayName}, ${day} ${month}`) - } - return timeString - } - anchors.centerIn: parent - font.pointSize: Style.fontSizeS * scaling - font.weight: Style.fontWeightBold - } - - MouseArea { - id: clockMouseArea - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - hoverEnabled: true - onEntered: root.entered() - onExited: root.exited() - onClicked: root.clicked() - } -} From fb01392bc3ea63e99e40ced26beb367e8dec8bf7 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sun, 7 Sep 2025 14:29:14 -0400 Subject: [PATCH 26/54] Settings: cleanup --- Commons/Settings.qml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 82277d8..d0e94dd 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -77,13 +77,12 @@ Singleton { const sections = ["left", "center", "right"] // ----------------- - // 1st. check our settings are not super old, when we only had the widget type as a string + // 1st. check our settings are not super old, when we only had the widget type as a plain string for (var s = 0; s < sections.length; s++) { const sectionName = sections[s] for (var i = 0; i < adapter.bar.widgets[sectionName].length; i++) { var widget = adapter.bar.widgets[sectionName][i] if (typeof widget === "string") { - console.log("founf old stuff") adapter.bar.widgets[sectionName][i] = { "id": widget } From f95c9b76d4932ad2e389504128e43fd8cdb64ebb Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sun, 7 Sep 2025 14:40:33 -0400 Subject: [PATCH 27/54] Clock fully migrated to new user settings --- Commons/Settings.qml | 2 +- Modules/Bar/Widgets/Clock.qml | 10 +++++----- .../SettingsPanel/Extras/BarWidgetSettingsDialog.qml | 9 +++++++++ 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/Commons/Settings.qml b/Commons/Settings.qml index d0e94dd..24e83d4 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -124,7 +124,7 @@ Singleton { widget.showDate = adapter.location.showDateWithClock widget.use12HourClock = adapter.location.use12HourClock widget.reverseDayMonth = adapter.location.reverseDayMonth - widget.showSeconds = BarWidgetRegistry.widgetMetadata[widget.id].reverseDayMonth + widget.showSeconds = BarWidgetRegistry.widgetMetadata[widget.id].showSeconds break } } diff --git a/Modules/Bar/Widgets/Clock.qml b/Modules/Bar/Widgets/Clock.qml index a9bb3de..3067e7c 100644 --- a/Modules/Bar/Widgets/Clock.qml +++ b/Modules/Bar/Widgets/Clock.qml @@ -28,13 +28,13 @@ Rectangle { } // Resolve settings: try user settings or defaults from BarWidgetRegistry - readonly property bool showDate: widgetSettings.showDate || BarWidgetRegistry.widgetMetadata["Clock"].showDate + readonly property bool showDate: widgetSettings.showDate + !== undefined ? widgetSettings.showDate : BarWidgetRegistry.widgetMetadata["Clock"].showDate readonly property bool use12h: widgetSettings.use12HourClock - || BarWidgetRegistry.widgetMetadata["Clock"].use12HourClock + !== undefined ? widgetSettings.use12HourClock : BarWidgetRegistry.widgetMetadata["Clock"].use12HourClock readonly property bool showSeconds: widgetSettings.showSeconds - || BarWidgetRegistry.widgetMetadata["Clock"].showSeconds - readonly property bool reverseDayMonth: widgetSettings.reverseDayMonth - || BarWidgetRegistry.widgetMetadata["Clock"].reverseDayMonth + !== undefined ? widgetSettings.showSeconds : BarWidgetRegistry.widgetMetadata["Clock"].showSeconds + readonly property bool reverseDayMonth: widgetSettings.reverseDayMonth !== undefined ? widgetSettings.reverseDayMonth : BarWidgetRegistry.widgetMetadata["Clock"].reverseDayMonth implicitWidth: clock.width + Style.marginM * 2 * scaling implicitHeight: Math.round(Style.capsuleHeight * scaling) diff --git a/Modules/SettingsPanel/Extras/BarWidgetSettingsDialog.qml b/Modules/SettingsPanel/Extras/BarWidgetSettingsDialog.qml index 2bf2d83..b35d3ce 100644 --- a/Modules/SettingsPanel/Extras/BarWidgetSettingsDialog.qml +++ b/Modules/SettingsPanel/Extras/BarWidgetSettingsDialog.qml @@ -273,12 +273,15 @@ Popup { !== undefined ? settingsPopup.widgetData.use12HourClock : BarWidgetRegistry.widgetMetadata["Clock"].use12HourClock property bool valueShowSeconds: settingsPopup.widgetData.showSeconds !== undefined ? settingsPopup.widgetData.showSeconds : BarWidgetRegistry.widgetMetadata["Clock"].showSeconds + property bool valueReverseDayMonth: settingsPopup.widgetData.reverseDayMonth + !== undefined ? settingsPopup.widgetData.reverseDayMonth : BarWidgetRegistry.widgetMetadata["Clock"].reverseDayMonth function saveSettings() { var settings = Object.assign({}, settingsPopup.widgetData) settings.showDate = valueShowDate settings.use12HourClock = valueUse12h settings.showSeconds = valueShowSeconds + settings.reverseDayMonth = valueReverseDayMonth return settings } @@ -299,6 +302,12 @@ Popup { checked: valueShowSeconds onToggled: checked => valueShowSeconds = checked } + + NCheckbox { + label: "Reverse day and month" + checked: valueReverseDayMonth + onToggled: checked => valueReverseDayMonth = checked + } } } From c4846cd97718acba8c5bf3804f617374a32a7285 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sun, 7 Sep 2025 18:42:39 -0400 Subject: [PATCH 28/54] NPill: improved text centering --- Widgets/NPill.qml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Widgets/NPill.qml b/Widgets/NPill.qml index 794caa9..88d7000 100644 --- a/Widgets/NPill.qml +++ b/Widgets/NPill.qml @@ -67,7 +67,11 @@ Item { NText { id: textItem - anchors.centerIn: parent + anchors.verticalCenter: parent.verticalCenter + anchors.left: rightOpen ? parent.left : undefined + anchors.right: rightOpen ? undefined : parent.right + anchors.leftMargin: rightOpen ? iconSize * 0.8 : 0 + anchors.rightMargin: rightOpen ? 0 : iconSize * 0.8 text: root.text font.pointSize: Style.fontSizeXS * scaling font.weight: Style.fontWeightBold From dae1d12b6fcf7cc20318fb3fab93b00ab2b7fc5f Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sun, 7 Sep 2025 18:50:21 -0400 Subject: [PATCH 29/54] NPill: smoother animation when opening and closing (no instant width jump) --- Widgets/NPill.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Widgets/NPill.qml b/Widgets/NPill.qml index 88d7000..f177864 100644 --- a/Widgets/NPill.qml +++ b/Widgets/NPill.qml @@ -45,7 +45,7 @@ Item { readonly property int pillOverlap: iconSize * 0.5 readonly property int maxPillWidth: Math.max(1, textItem.implicitWidth + pillPaddingHorizontal * 2 + pillOverlap) - width: iconSize + (effectiveShown ? maxPillWidth - pillOverlap : 0) + width: iconSize + Math.max(0, pill.width - pillOverlap) height: pillHeight Rectangle { From 3065bec6c90fc7d29935ae0fbefb7de3d5b3f073 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sun, 7 Sep 2025 20:03:14 -0400 Subject: [PATCH 30/54] BarSectionEditor: Buttons are now easier to click + reverted back to 5 basic colors --- Modules/SettingsPanel/Extras/BarSectionEditor.qml | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/Modules/SettingsPanel/Extras/BarSectionEditor.qml b/Modules/SettingsPanel/Extras/BarSectionEditor.qml index 4ea82e2..14cf45a 100644 --- a/Modules/SettingsPanel/Extras/BarSectionEditor.qml +++ b/Modules/SettingsPanel/Extras/BarSectionEditor.qml @@ -39,7 +39,7 @@ NBox { const totalSum = JSON.stringify(widget).split('').reduce((acc, character) => { return acc + character.charCodeAt(0) }, 0) - switch (totalSum % 10) { + switch (totalSum % 5) { case 0: return Color.mPrimary case 1: @@ -50,16 +50,6 @@ NBox { return Color.mError case 4: return Color.mOnSurface - case 5: - return Qt.darker(Color.mPrimary, 1.3) - case 6: - return Qt.darker(Color.mSecondary, 1.3) - case 7: - return Qt.darker(Color.mTertiary, 1.3) - case 8: - return Qt.darker(Color.mError, 1.3) - case 9: - return Qt.darker(Color.mOnSurface, 1.3) } } @@ -241,7 +231,7 @@ NBox { MouseArea { id: flowDragArea anchors.fill: parent - z: 999 // Above all widgets to ensure it gets events first + z: -1 // Ensure this mouse area is below the Settings and Close buttons // Critical properties for proper event handling acceptedButtons: Qt.LeftButton From e03042c411ff960bd60bd12d23896c1484865f04 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sun, 7 Sep 2025 21:13:31 -0400 Subject: [PATCH 31/54] NCheckBox: fast animation speed like the others --- Widgets/NCheckbox.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Widgets/NCheckbox.qml b/Widgets/NCheckbox.qml index f903e8a..4b5962d 100644 --- a/Widgets/NCheckbox.qml +++ b/Widgets/NCheckbox.qml @@ -44,13 +44,13 @@ RowLayout { Behavior on color { ColorAnimation { - duration: Style.animationNormal + duration: Style.animationFast } } Behavior on border.color { ColorAnimation { - duration: Style.animationNormal + duration: Style.animationFast } } From a68b3f49b0e2997b0316d7ba46e748d41412ad85 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sun, 7 Sep 2025 21:13:45 -0400 Subject: [PATCH 32/54] NComboBox: better sizing --- Widgets/NComboBox.qml | 15 ++++++++++----- Widgets/NPill.qml | 2 +- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Widgets/NComboBox.qml b/Widgets/NComboBox.qml index 813eee5..57bc0bb 100644 --- a/Widgets/NComboBox.qml +++ b/Widgets/NComboBox.qml @@ -8,8 +8,7 @@ import qs.Widgets RowLayout { id: root - readonly property real preferredHeight: Style.baseWidgetSize * 1.1 * scaling - property real preferredWidth: 320 * scaling + property real minimumWidth: 280 * scaling property real popupHeight: 180 * scaling property string label: "" @@ -20,9 +19,11 @@ RowLayout { property string currentKey: "" property string placeholder: "" + readonly property real preferredHeight: Style.baseWidgetSize * 1.1 * scaling + signal selected(string key) - spacing: Style.marginS * scaling + spacing: Style.marginL * scaling Layout.fillWidth: true function findIndexByKey(key) { @@ -39,11 +40,15 @@ RowLayout { description: root.description } + Item { + Layout.fillWidth: true + } + ComboBox { id: combo - Layout.preferredWidth: root.preferredWidth - Layout.preferredHeight: height + Layout.minimumWidth: root.minimumWidth + Layout.preferredHeight: root.preferredHeight model: model currentIndex: findIndexByKey(currentKey) onActivated: { diff --git a/Widgets/NPill.qml b/Widgets/NPill.qml index f177864..8a1dd63 100644 --- a/Widgets/NPill.qml +++ b/Widgets/NPill.qml @@ -71,7 +71,7 @@ Item { anchors.left: rightOpen ? parent.left : undefined anchors.right: rightOpen ? undefined : parent.right anchors.leftMargin: rightOpen ? iconSize * 0.8 : 0 - anchors.rightMargin: rightOpen ? 0 : iconSize * 0.8 + anchors.rightMargin: rightOpen ? 0 : iconSize * 0.8 text: root.text font.pointSize: Style.fontSizeXS * scaling font.weight: Style.fontWeightBold From c01167c9da0296391e6d9dac3c363c43be629364 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sun, 7 Sep 2025 21:24:53 -0400 Subject: [PATCH 33/54] Settings tabs: adapt to new sizing of NComboBox --- Modules/SettingsPanel/Tabs/BrightnessTab.qml | 6 ++---- Modules/SettingsPanel/Tabs/GeneralTab.qml | 3 +++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Modules/SettingsPanel/Tabs/BrightnessTab.qml b/Modules/SettingsPanel/Tabs/BrightnessTab.qml index 4bbcace..0b02d4a 100644 --- a/Modules/SettingsPanel/Tabs/BrightnessTab.qml +++ b/Modules/SettingsPanel/Tabs/BrightnessTab.qml @@ -299,8 +299,7 @@ ColumnLayout { currentKey: Settings.data.nightLight.manualSunrise placeholder: "Select start time" onSelected: key => Settings.data.nightLight.manualSunrise = key - - preferredWidth: 120 * scaling + minimumWidth: 120 * scaling } Item {// add a little more spacing @@ -316,8 +315,7 @@ ColumnLayout { currentKey: Settings.data.nightLight.manualSunset placeholder: "Select stop time" onSelected: key => Settings.data.nightLight.manualSunset = key - - preferredWidth: 120 * scaling + minimumWidth: 120 * scaling } } } diff --git a/Modules/SettingsPanel/Tabs/GeneralTab.qml b/Modules/SettingsPanel/Tabs/GeneralTab.qml index 7407112..f619eda 100644 --- a/Modules/SettingsPanel/Tabs/GeneralTab.qml +++ b/Modules/SettingsPanel/Tabs/GeneralTab.qml @@ -206,6 +206,7 @@ ColumnLayout { currentKey: Settings.data.ui.fontDefault placeholder: "Select default font..." popupHeight: 420 * scaling + minimumWidth: 300 * scaling onSelected: function (key) { Settings.data.ui.fontDefault = key } @@ -218,6 +219,7 @@ ColumnLayout { currentKey: Settings.data.ui.fontFixed placeholder: "Select monospace font..." popupHeight: 320 * scaling + minimumWidth: 300 * scaling onSelected: function (key) { Settings.data.ui.fontFixed = key } @@ -230,6 +232,7 @@ ColumnLayout { currentKey: Settings.data.ui.fontBillboard placeholder: "Select display font..." popupHeight: 320 * scaling + minimumWidth: 300 * scaling onSelected: function (key) { Settings.data.ui.fontBillboard = key } From 45af873a6fcd47b8edc32e121b6f2d6be02b3ad0 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sun, 7 Sep 2025 21:45:28 -0400 Subject: [PATCH 34/54] Bar Widget Settings: One file per Widget settings, refactor - wip --- Modules/Bar/Widgets/ActiveWindow.qml | 17 - Modules/Bar/Widgets/Battery.qml | 67 +- Modules/Bar/Widgets/Brightness.qml | 17 - Modules/Bar/Widgets/MediaMini.qml | 24 - Modules/Bar/Widgets/Microphone.qml | 20 +- Modules/Bar/Widgets/SidePanelToggle.qml | 17 - Modules/Bar/Widgets/SystemMonitor.qml | 17 - Modules/Bar/Widgets/Volume.qml | 17 - Modules/Bar/Widgets/Workspace.qml | 14 - .../{Extras => Bar}/BarSectionEditor.qml | 0 .../Bar/BarWidgetSettingsDialog.qml | 126 ++++ .../WidgetSettings/ActiveWindowSettings.qml | 32 + .../Bar/WidgetSettings/BatterySettings.qml | 31 + .../Bar/WidgetSettings/BrightnessSettings.qml | 31 + .../Bar/WidgetSettings/ClockSettings.qml | 54 ++ .../WidgetSettings/CustomButtonSettings.qml | 58 ++ .../Bar/WidgetSettings/MediaMiniSettings.qml | 62 ++ .../Bar/WidgetSettings/MicrophoneSettings.qml | 31 + .../NotificationHistorySettings.qml | 38 ++ .../SidePanelToggleSettings.qml | 30 + .../Bar/WidgetSettings/SpacerSettings.qml | 30 + .../WidgetSettings/SystemMonitorSettings.qml | 63 ++ .../Bar/WidgetSettings/VolumeSettings.qml | 31 + .../Bar/WidgetSettings/WorkspaceSettings.qml | 44 ++ .../Extras/BarWidgetSettingsDialog.qml | 578 ------------------ Modules/SettingsPanel/Tabs/BarTab.qml | 10 +- Services/BarWidgetRegistry.qml | 13 +- 27 files changed, 707 insertions(+), 765 deletions(-) rename Modules/SettingsPanel/{Extras => Bar}/BarSectionEditor.qml (100%) create mode 100644 Modules/SettingsPanel/Bar/BarWidgetSettingsDialog.qml create mode 100644 Modules/SettingsPanel/Bar/WidgetSettings/ActiveWindowSettings.qml create mode 100644 Modules/SettingsPanel/Bar/WidgetSettings/BatterySettings.qml create mode 100644 Modules/SettingsPanel/Bar/WidgetSettings/BrightnessSettings.qml create mode 100644 Modules/SettingsPanel/Bar/WidgetSettings/ClockSettings.qml create mode 100644 Modules/SettingsPanel/Bar/WidgetSettings/CustomButtonSettings.qml create mode 100644 Modules/SettingsPanel/Bar/WidgetSettings/MediaMiniSettings.qml create mode 100644 Modules/SettingsPanel/Bar/WidgetSettings/MicrophoneSettings.qml create mode 100644 Modules/SettingsPanel/Bar/WidgetSettings/NotificationHistorySettings.qml create mode 100644 Modules/SettingsPanel/Bar/WidgetSettings/SidePanelToggleSettings.qml create mode 100644 Modules/SettingsPanel/Bar/WidgetSettings/SpacerSettings.qml create mode 100644 Modules/SettingsPanel/Bar/WidgetSettings/SystemMonitorSettings.qml create mode 100644 Modules/SettingsPanel/Bar/WidgetSettings/VolumeSettings.qml create mode 100644 Modules/SettingsPanel/Bar/WidgetSettings/WorkspaceSettings.qml delete mode 100644 Modules/SettingsPanel/Extras/BarWidgetSettingsDialog.qml diff --git a/Modules/Bar/Widgets/ActiveWindow.qml b/Modules/Bar/Widgets/ActiveWindow.qml index c84d2b0..e817bb1 100644 --- a/Modules/Bar/Widgets/ActiveWindow.qml +++ b/Modules/Bar/Widgets/ActiveWindow.qml @@ -39,23 +39,6 @@ RowLayout { return CompositorService.focusedWindowTitle !== "(No active window)" ? CompositorService.focusedWindowTitle : "" } - Component.onCompleted: { - try { - var section = barSection.replace("Section", "").toLowerCase() - if (section && sectionWidgetIndex >= 0) { - var widgets = Settings.data.bar.widgets[section] - if (widgets && sectionWidgetIndex < widgets.length) { - if (widgets[sectionWidgetIndex].showIcon === undefined - && Settings.data.bar.showActiveWindowIcon !== undefined) { - widgets[sectionWidgetIndex].showIcon = Settings.data.bar.showActiveWindowIcon - } - } - } - } catch (e) { - - } - } - function getAppIcon() { // Try CompositorService first const focusedWindow = CompositorService.getFocusedWindow() diff --git a/Modules/Bar/Widgets/Battery.qml b/Modules/Bar/Widgets/Battery.qml index b4654fe..d27c2cb 100644 --- a/Modules/Bar/Widgets/Battery.qml +++ b/Modules/Bar/Widgets/Battery.qml @@ -11,11 +11,41 @@ Item { property ShellScreen screen property real scaling: 1.0 + + // Widget properties passed from Bar.qml for per-instance settings property string barSection: "" - property int sectionWidgetIndex: 0 + property int sectionWidgetIndex: -1 property int sectionWidgetsCount: 0 - // Track if we've already notified to avoid spam + // Resolve per-instance widget settings from Settings.data + property var widgetSettings: { + var section = barSection.replace("Section", "").toLowerCase() + if (section && sectionWidgetIndex >= 0) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex] + } + } + return {} + } + + // Resolve settings: try user settings or defaults from BarWidgetRegistry + readonly property bool alwaysShowPercentage: widgetSettings.alwaysShowPercentage + !== undefined ? widgetSettings.alwaysShowPercentage : BarWidgetRegistry.widgetMetadata["Battery"].alwaysShowPercentage + readonly property real warningThreshold: widgetSettings.warningThreshold + !== undefined ? widgetSettings.warningThreshold : BarWidgetRegistry.widgetMetadata["Battery"].warningThreshold + + // Test mode + readonly property bool testMode: true + readonly property int testPercent: 50 + readonly property bool testCharging: true + + // Main properties + readonly property var battery: UPower.displayDevice + readonly property bool isReady: testMode ? true : (battery && battery.ready && battery.isLaptopBattery + && battery.isPresent) + readonly property real percent: testMode ? testPercent : (isReady ? (battery.percentage * 100) : 0) + readonly property bool charging: testMode ? testCharging : (isReady ? battery.state === UPowerDeviceState.Charging : false) property bool hasNotifiedLowBattery: false implicitWidth: pill.width @@ -23,15 +53,14 @@ Item { // Helper to evaluate and possibly notify function maybeNotify(percent, charging) { - const p = Math.round(percent) - // Only notify exactly at 15%, not at 0% or any other percentage - if (!charging && p === 15 && !root.hasNotifiedLowBattery) { + // Only notify once we are a below threshold + if (!charging && !root.hasNotifiedLowBattery && percent <= warningThreshold) { + root.hasNotifiedLowBattery = true + // Maybe go with toast ? Quickshell.execDetached( ["notify-send", "-u", "critical", "-i", "battery-caution", "Low Battery", `Battery is at ${p}%. Please connect charger.`]) - root.hasNotifiedLowBattery = true - } - // Reset when charging starts or when battery recovers above 20% - if (charging || p > 20) { + } else if (root.hasNotifiedLowBattery && (charging || percent > warningThreshold + 5)) { + // Reset when charging starts or when battery recovers 5% above threshold root.hasNotifiedLowBattery = false } } @@ -40,19 +69,10 @@ Item { Connections { target: UPower.displayDevice function onPercentageChanged() { - let battery = UPower.displayDevice - let isReady = battery && battery.ready && battery.isLaptopBattery && battery.isPresent - let percent = isReady ? (battery.percentage * 100) : 0 - let charging = isReady ? battery.state === UPowerDeviceState.Charging : false - root.maybeNotify(percent, charging) } function onStateChanged() { - let battery = UPower.displayDevice - let isReady = battery && battery.ready && battery.isLaptopBattery && battery.isPresent - let charging = isReady ? battery.state === UPowerDeviceState.Charging : false - // Reset notification flag when charging starts if (charging) { root.hasNotifiedLowBattery = false @@ -63,15 +83,6 @@ Item { NPill { id: pill - // Test mode - property bool testMode: false - property int testPercent: 50 - property bool testCharging: true - property var battery: UPower.displayDevice - property bool isReady: testMode ? true : (battery && battery.ready && battery.isLaptopBattery && battery.isPresent) - property real percent: testMode ? testPercent : (isReady ? (battery.percentage * 100) : 0) - property bool charging: testMode ? testCharging : (isReady ? battery.state === UPowerDeviceState.Charging : false) - rightOpen: BarWidgetRegistry.getNPillDirection(root) icon: testMode ? BatteryService.getIcon(testPercent, testCharging, true) : BatteryService.getIcon(percent, charging, isReady) @@ -81,7 +92,7 @@ Item { iconCircleColor: Color.mPrimary collapsedIconColor: Color.mOnSurface autoHide: false - forceOpen: isReady && (testMode || battery.isLaptopBattery) && Settings.data.bar.alwaysShowBatteryPercentage + forceOpen: isReady && (testMode || battery.isLaptopBattery) && alwaysShowPercentage disableOpen: (!isReady || (!testMode && !battery.isLaptopBattery)) tooltipText: { let lines = [] diff --git a/Modules/Bar/Widgets/Brightness.qml b/Modules/Bar/Widgets/Brightness.qml index 2c299f3..aa434a2 100644 --- a/Modules/Bar/Widgets/Brightness.qml +++ b/Modules/Bar/Widgets/Brightness.qml @@ -71,23 +71,6 @@ Item { onTriggered: pill.hide() } - Component.onCompleted: { - try { - var section = barSection.replace("Section", "").toLowerCase() - if (section && sectionWidgetIndex >= 0) { - var widgets = Settings.data.bar.widgets[section] - if (widgets && sectionWidgetIndex < widgets.length) { - if (widgets[sectionWidgetIndex].alwaysShowPercentage === undefined - && Settings.data.bar.alwaysShowBatteryPercentage !== undefined) { - widgets[sectionWidgetIndex].alwaysShowPercentage = Settings.data.bar.alwaysShowBatteryPercentage - } - } - } - } catch (e) { - - } - } - NPill { id: pill diff --git a/Modules/Bar/Widgets/MediaMini.qml b/Modules/Bar/Widgets/MediaMini.qml index 76a5655..ba5fd5c 100644 --- a/Modules/Bar/Widgets/MediaMini.qml +++ b/Modules/Bar/Widgets/MediaMini.qml @@ -218,30 +218,6 @@ RowLayout { } } - Component.onCompleted: { - try { - var section = barSection.replace("Section", "").toLowerCase() - if (section && sectionWidgetIndex >= 0) { - var widgets = Settings.data.bar.widgets[section] - if (widgets && sectionWidgetIndex < widgets.length) { - var w = widgets[sectionWidgetIndex] - if (w.showAlbumArt === undefined && Settings.data.audio.showMiniplayerAlbumArt !== undefined) { - w.showAlbumArt = Settings.data.audio.showMiniplayerAlbumArt - } - if (w.showVisualizer === undefined && Settings.data.audio.showMiniplayerCava !== undefined) { - w.showVisualizer = Settings.data.audio.showMiniplayerCava - } - if ((w.visualizerType === undefined || w.visualizerType === "") - && (Settings.data.audio.visualizerType !== undefined && Settings.data.audio.visualizerType !== "")) { - w.visualizerType = Settings.data.audio.visualizerType - } - } - } - } catch (e) { - - } - } - NTooltip { id: tooltip text: { diff --git a/Modules/Bar/Widgets/Microphone.qml b/Modules/Bar/Widgets/Microphone.qml index 75a5a20..2b983a4 100644 --- a/Modules/Bar/Widgets/Microphone.qml +++ b/Modules/Bar/Widgets/Microphone.qml @@ -27,8 +27,7 @@ Item { return {} } - readonly property bool userAlwaysShowPercentage: (widgetSettings.alwaysShowPercentage - !== undefined) ? widgetSettings.alwaysShowPercentage : BarWidgetRegistry.widgetMetadata["Microphone"].alwaysShowPercentage + readonly property bool userAlwaysShowPercentage: widgetSettings?.alwaysShowPercentage // Used to avoid opening the pill on Quickshell startup property bool firstInputVolumeReceived: false @@ -83,23 +82,6 @@ Item { } } - Component.onCompleted: { - try { - var section = barSection.replace("Section", "").toLowerCase() - if (section && sectionWidgetIndex >= 0) { - var widgets = Settings.data.bar.widgets[section] - if (widgets && sectionWidgetIndex < widgets.length) { - if (widgets[sectionWidgetIndex].alwaysShowPercentage === undefined - && Settings.data.bar.alwaysShowBatteryPercentage !== undefined) { - widgets[sectionWidgetIndex].alwaysShowPercentage = Settings.data.bar.alwaysShowBatteryPercentage - } - } - } - } catch (e) { - - } - } - NPill { id: pill diff --git a/Modules/Bar/Widgets/SidePanelToggle.qml b/Modules/Bar/Widgets/SidePanelToggle.qml index 6c0b489..7c6b4f3 100644 --- a/Modules/Bar/Widgets/SidePanelToggle.qml +++ b/Modules/Bar/Widgets/SidePanelToggle.qml @@ -41,23 +41,6 @@ NIconButton { onClicked: PanelService.getPanel("sidePanel")?.toggle(screen, this) onRightClicked: PanelService.getPanel("settingsPanel")?.toggle(screen) - Component.onCompleted: { - try { - var section = barSection.replace("Section", "").toLowerCase() - if (section && sectionWidgetIndex >= 0) { - var widgets = Settings.data.bar.widgets[section] - if (widgets && sectionWidgetIndex < widgets.length) { - if (widgets[sectionWidgetIndex].useDistroLogo === undefined - && Settings.data.bar.useDistroLogo !== undefined) { - widgets[sectionWidgetIndex].useDistroLogo = Settings.data.bar.useDistroLogo - } - } - } - } catch (e) { - - } - } - IconImage { id: logo anchors.centerIn: parent diff --git a/Modules/Bar/Widgets/SystemMonitor.qml b/Modules/Bar/Widgets/SystemMonitor.qml index 654f161..49dceef 100644 --- a/Modules/Bar/Widgets/SystemMonitor.qml +++ b/Modules/Bar/Widgets/SystemMonitor.qml @@ -32,23 +32,6 @@ RowLayout { readonly property bool userShowNetworkStats: (widgetSettings.showNetworkStats !== undefined) ? widgetSettings.showNetworkStats : ((Settings.data.bar.showNetworkStats !== undefined) ? Settings.data.bar.showNetworkStats : BarWidgetRegistry.widgetMetadata["SystemMonitor"].showNetworkStats) - Component.onCompleted: { - try { - var section = barSection.replace("Section", "").toLowerCase() - if (section && sectionWidgetIndex >= 0) { - var widgets = Settings.data.bar.widgets[section] - if (widgets && sectionWidgetIndex < widgets.length) { - if (widgets[sectionWidgetIndex].showNetworkStats === undefined - && Settings.data.bar.showNetworkStats !== undefined) { - widgets[sectionWidgetIndex].showNetworkStats = Settings.data.bar.showNetworkStats - } - } - } - } catch (e) { - - } - } - Layout.alignment: Qt.AlignVCenter spacing: Style.marginS * scaling diff --git a/Modules/Bar/Widgets/Volume.qml b/Modules/Bar/Widgets/Volume.qml index 116718e..21035f8 100644 --- a/Modules/Bar/Widgets/Volume.qml +++ b/Modules/Bar/Widgets/Volume.qml @@ -67,23 +67,6 @@ Item { } } - Component.onCompleted: { - try { - var section = barSection.replace("Section", "").toLowerCase() - if (section && sectionWidgetIndex >= 0) { - var widgets = Settings.data.bar.widgets[section] - if (widgets && sectionWidgetIndex < widgets.length) { - if (widgets[sectionWidgetIndex].alwaysShowPercentage === undefined - && Settings.data.bar.alwaysShowBatteryPercentage !== undefined) { - widgets[sectionWidgetIndex].alwaysShowPercentage = Settings.data.bar.alwaysShowBatteryPercentage - } - } - } - } catch (e) { - - } - } - NPill { id: pill diff --git a/Modules/Bar/Widgets/Workspace.qml b/Modules/Bar/Widgets/Workspace.qml index 3fe6641..a9e8c1d 100644 --- a/Modules/Bar/Widgets/Workspace.qml +++ b/Modules/Bar/Widgets/Workspace.qml @@ -67,20 +67,6 @@ Item { Component.onCompleted: { refreshWorkspaces() - try { - var section = barSection.replace("Section", "").toLowerCase() - if (section && sectionWidgetIndex >= 0) { - var widgets = Settings.data.bar.widgets[section] - if (widgets && sectionWidgetIndex < widgets.length) { - if (widgets[sectionWidgetIndex].labelMode === undefined - && Settings.data.bar.showWorkspaceLabel !== undefined) { - widgets[sectionWidgetIndex].labelMode = Settings.data.bar.showWorkspaceLabel - } - } - } - } catch (e) { - - } } Component.onDestruction: { diff --git a/Modules/SettingsPanel/Extras/BarSectionEditor.qml b/Modules/SettingsPanel/Bar/BarSectionEditor.qml similarity index 100% rename from Modules/SettingsPanel/Extras/BarSectionEditor.qml rename to Modules/SettingsPanel/Bar/BarSectionEditor.qml diff --git a/Modules/SettingsPanel/Bar/BarWidgetSettingsDialog.qml b/Modules/SettingsPanel/Bar/BarWidgetSettingsDialog.qml new file mode 100644 index 0000000..ef38ae6 --- /dev/null +++ b/Modules/SettingsPanel/Bar/BarWidgetSettingsDialog.qml @@ -0,0 +1,126 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import QtQuick.Layouts +import qs.Commons +import qs.Widgets +import qs.Services +import "./WidgetSettings" as WidgetSettings + +// Widget Settings Dialog Component +Popup { + id: settingsPopup + + property int widgetIndex: -1 + property var widgetData: null + property string widgetId: "" + + // Center popup in parent + x: (parent.width - width) * 0.5 + y: (parent.height - height) * 0.5 + + width: 420 * scaling + height: content.implicitHeight + padding * 2 + padding: Style.marginXL * scaling + modal: true + + background: Rectangle { + id: bgRect + color: Color.mSurface + radius: Style.radiusL * scaling + border.color: Color.mPrimary + border.width: Style.borderM * scaling + } + + ColumnLayout { + id: content + width: parent.width + spacing: Style.marginM * scaling + + // Title + RowLayout { + Layout.fillWidth: true + + NText { + text: `${settingsPopup.widgetId} Settings` + font.pointSize: Style.fontSizeL * scaling + font.weight: Style.fontWeightBold + color: Color.mPrimary + Layout.fillWidth: true + } + + NIconButton { + icon: "close" + onClicked: settingsPopup.close() + } + } + + // Separator + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Color.mOutline + } + + // Settings based on widget type + Loader { + id: settingsLoader + Layout.fillWidth: true + + source: { + const widgetSettingsMap = { + "ActiveWindow": "WidgetSettings/ActiveWindowSettings.qml", + "Battery": "WidgetSettings/BatterySettings.qml", + "Brightness": "WidgetSettings/BrightnessSettings.qml", + "Clock": "WidgetSettings/ClockSettings.qml", + "CustomButton": "WidgetSettings/CustomButtonSettings.qml", + "MediaMini": "WidgetSettings/MediaMiniSettings.qml", + "Microphone": "WidgetSettings/MicrophoneSettings.qml", + "NotificationHistory": "WidgetSettings/NotificationHistorySettings.qml", + "Workspace": "WidgetSettings/WorkspaceSettings.qml", + "SidePanelToggle": "WidgetSettings/SidePanelToggleSettings.qml", + "Spacer": "WidgetSettings/SpacerSettings.qml", + "SystemMonitor": "WidgetSettings/SystemMonitorSettings.qml", + "Volume": "WidgetSettings/VolumeSettings.qml" + } + return widgetSettingsMap[settingsPopup.widgetId] || "" + } + + onLoaded: { + if (item) { + // Pass data to the loaded component + item.widgetData = settingsPopup.widgetData + item.widgetMetadata = BarWidgetRegistry.widgetMetadata[settingsPopup.widgetId] + } + } + } + + // Action buttons + RowLayout { + Layout.fillWidth: true + Layout.topMargin: Style.marginM * scaling + + Item { + Layout.fillWidth: true + } + + NButton { + text: "Cancel" + outlined: true + onClicked: settingsPopup.close() + } + + NButton { + text: "Apply" + icon: "check" + onClicked: { + if (settingsLoader.item && settingsLoader.item.saveSettings) { + var newSettings = settingsLoader.item.saveSettings() + root.updateWidgetSettings(sectionId, settingsPopup.widgetIndex, newSettings) + settingsPopup.close() + } + } + } + } + } +} diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/ActiveWindowSettings.qml b/Modules/SettingsPanel/Bar/WidgetSettings/ActiveWindowSettings.qml new file mode 100644 index 0000000..9f5d8a6 --- /dev/null +++ b/Modules/SettingsPanel/Bar/WidgetSettings/ActiveWindowSettings.qml @@ -0,0 +1,32 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets +import qs.Services + +ColumnLayout { + id: root + spacing: Style.marginM * scaling + + // Properties to receive data from parent + property var widgetData: null + property var widgetMetadata: null + + // Local state + property bool valueShowIcon: widgetData.showIcon !== undefined ? widgetData.showIcon : widgetMetadata.showIcon + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}) + settings.showIcon = valueShowIcon + return settings + } + + NCheckbox { + id: showIcon + Layout.fillWidth: true + label: "Show app icon" + checked: root.valueShowIcon + onToggled: checked => root.valueShowIcon = checked + } +} diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/BatterySettings.qml b/Modules/SettingsPanel/Bar/WidgetSettings/BatterySettings.qml new file mode 100644 index 0000000..f907204 --- /dev/null +++ b/Modules/SettingsPanel/Bar/WidgetSettings/BatterySettings.qml @@ -0,0 +1,31 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets +import qs.Services + +ColumnLayout { + id: root + spacing: Style.marginM * scaling + + // Properties to receive data from parent + property var widgetData: null + property var widgetMetadata: null + + // Local state + property bool valueAlwaysShowPercentage: widgetData.alwaysShowPercentage + !== undefined ? widgetData.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}) + settings.alwaysShowPercentage = valueAlwaysShowPercentage + return settings + } + + NCheckbox { + label: "Always show percentage" + checked: root.valueAlwaysShowPercentage + onToggled: checked => root.valueAlwaysShowPercentage = checked + } +} diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/BrightnessSettings.qml b/Modules/SettingsPanel/Bar/WidgetSettings/BrightnessSettings.qml new file mode 100644 index 0000000..840c33c --- /dev/null +++ b/Modules/SettingsPanel/Bar/WidgetSettings/BrightnessSettings.qml @@ -0,0 +1,31 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets +import qs.Services + +ColumnLayout { + id: root + spacing: Style.marginM * scaling + + // Properties to receive data from parent + property var widgetData: null + property var widgetMetadata: null + + // Local state + property bool valueAlwaysShowPercentage: widgetData.alwaysShowPercentage + !== undefined ? widgetData.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}) + settings.alwaysShowPercentage = valueAlwaysShowPercentage + return settings + } + + NCheckbox { + label: "Always show percentage" + checked: valueAlwaysShowPercentage + onToggled: checked => valueAlwaysShowPercentage = checked + } +} diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/ClockSettings.qml b/Modules/SettingsPanel/Bar/WidgetSettings/ClockSettings.qml new file mode 100644 index 0000000..d285681 --- /dev/null +++ b/Modules/SettingsPanel/Bar/WidgetSettings/ClockSettings.qml @@ -0,0 +1,54 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets +import qs.Services + +ColumnLayout { + id: root + spacing: Style.marginM * scaling + + // Properties to receive data from parent + property var widgetData: null + property var widgetMetadata: null + + // Local state + property bool valueShowDate: widgetData.showDate !== undefined ? widgetData.showDate : widgetMetadata.showDate + property bool valueUse12h: widgetData.use12HourClock !== undefined ? widgetData.use12HourClock : widgetMetadata.use12HourClock + property bool valueShowSeconds: widgetData.showSeconds !== undefined ? widgetData.showSeconds : widgetMetadata.showSeconds + property bool valueReverseDayMonth: widgetData.reverseDayMonth !== undefined ? widgetData.reverseDayMonth : widgetMetadata.reverseDayMonth + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}) + settings.showDate = valueShowDate + settings.use12HourClock = valueUse12h + settings.showSeconds = valueShowSeconds + settings.reverseDayMonth = valueReverseDayMonth + return settings + } + + NCheckbox { + label: "Show date next to time" + checked: valueShowDate + onToggled: checked => valueShowDate = checked + } + + NCheckbox { + label: "Use 12-hour clock" + checked: valueUse12h + onToggled: checked => valueUse12h = checked + } + + NCheckbox { + label: "Show seconds" + checked: valueShowSeconds + onToggled: checked => valueShowSeconds = checked + } + + NCheckbox { + label: "Reverse day and month" + checked: valueReverseDayMonth + onToggled: checked => valueReverseDayMonth = checked + } +} diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/CustomButtonSettings.qml b/Modules/SettingsPanel/Bar/WidgetSettings/CustomButtonSettings.qml new file mode 100644 index 0000000..b7c896f --- /dev/null +++ b/Modules/SettingsPanel/Bar/WidgetSettings/CustomButtonSettings.qml @@ -0,0 +1,58 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets +import qs.Services + +ColumnLayout { + id: root + spacing: Style.marginM * scaling + + // Properties to receive data from parent + property var widgetData: null + property var widgetMetadata: null + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}) + settings.icon = iconInput.text + settings.leftClickExec = leftClickExecInput.text + settings.rightClickExec = rightClickExecInput.text + settings.middleClickExec = middleClickExecInput.text + return settings + } + + // Icon setting + NTextInput { + id: iconInput + Layout.fillWidth: true + label: "Icon Name" + description: "Choose a name from the Material Icon set." + placeholderText: "Enter icon name (e.g., favorite, home, settings)" + text: widgetData?.icon || widgetMetadata.icon + } + + NTextInput { + id: leftClickExecInput + Layout.fillWidth: true + label: "Left Click Command" + placeholderText: "Enter command to execute (app or custom script)" + text: widgetData?.leftClickExec || widgetMetadata.leftClickExec + } + + NTextInput { + id: rightClickExecInput + Layout.fillWidth: true + label: "Right Click Command" + placeholderText: "Enter command to execute (app or custom script)" + text: widgetData?.rightClickExec || widgetMetadata.rightClickExec + } + + NTextInput { + id: middleClickExecInput + Layout.fillWidth: true + label: "Middle Click Command" + placeholderText: "Enter command to execute (app or custom script)" + text: widgetData.middleClickExec || widgetMetadata.middleClickExec + } +} diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/MediaMiniSettings.qml b/Modules/SettingsPanel/Bar/WidgetSettings/MediaMiniSettings.qml new file mode 100644 index 0000000..d1af307 --- /dev/null +++ b/Modules/SettingsPanel/Bar/WidgetSettings/MediaMiniSettings.qml @@ -0,0 +1,62 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets +import qs.Services + +ColumnLayout { + id: root + spacing: Style.marginM * scaling + + // Properties to receive data from parent + property var widgetData: null + property var widgetMetadata: null + + // Local state + property bool valueShowAlbumArt: widgetData.showAlbumArt !== undefined ? widgetData.showAlbumArt : widgetMetadata.showAlbumArt + property bool valueShowVisualizer: widgetData.showVisualizer !== undefined ? widgetData.showVisualizer : widgetMetadata.showVisualizer + property string valueVisualizerType: widgetData.visualizerType || widgetMetadata.visualizerType + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}) + settings.showAlbumArt = valueShowAlbumArt + settings.showVisualizer = valueShowVisualizer + settings.visualizerType = valueVisualizerType + return settings + } + + NCheckbox { + label: "Show album art" + checked: valueShowAlbumArt + onToggled: checked => valueShowAlbumArt = checked + } + + NCheckbox { + label: "Show visualizer" + checked: valueShowVisualizer + onToggled: checked => valueShowVisualizer = checked + } + + NComboBox { + visible: valueShowVisualizer + label: "Visualizer type" + model: ListModel { + ListElement { + key: "linear" + name: "Linear" + } + ListElement { + key: "mirrored" + name: "Mirrored" + } + ListElement { + key: "wave" + name: "Wave" + } + } + currentKey: valueVisualizerType + onSelected: key => valueVisualizerType = key + minimumWidth: 200 * scaling + } +} diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/MicrophoneSettings.qml b/Modules/SettingsPanel/Bar/WidgetSettings/MicrophoneSettings.qml new file mode 100644 index 0000000..840c33c --- /dev/null +++ b/Modules/SettingsPanel/Bar/WidgetSettings/MicrophoneSettings.qml @@ -0,0 +1,31 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets +import qs.Services + +ColumnLayout { + id: root + spacing: Style.marginM * scaling + + // Properties to receive data from parent + property var widgetData: null + property var widgetMetadata: null + + // Local state + property bool valueAlwaysShowPercentage: widgetData.alwaysShowPercentage + !== undefined ? widgetData.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}) + settings.alwaysShowPercentage = valueAlwaysShowPercentage + return settings + } + + NCheckbox { + label: "Always show percentage" + checked: valueAlwaysShowPercentage + onToggled: checked => valueAlwaysShowPercentage = checked + } +} diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/NotificationHistorySettings.qml b/Modules/SettingsPanel/Bar/WidgetSettings/NotificationHistorySettings.qml new file mode 100644 index 0000000..64b1c56 --- /dev/null +++ b/Modules/SettingsPanel/Bar/WidgetSettings/NotificationHistorySettings.qml @@ -0,0 +1,38 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets +import qs.Services + +ColumnLayout { + id: root + spacing: Style.marginM * scaling + + // Properties to receive data from parent + property var widgetData: null + property var widgetMetadata: null + + // Local state + property bool valueShowUnreadBadge: widgetData.showUnreadBadge !== undefined ? widgetData.showUnreadBadge : widgetMetadata.showUnreadBadge + property bool valueHideWhenZero: widgetData.hideWhenZero !== undefined ? widgetData.hideWhenZero : widgetMetadata.hideWhenZero + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}) + settings.showUnreadBadge = valueShowUnreadBadge + settings.hideWhenZero = valueHideWhenZero + return settings + } + + NCheckbox { + label: "Show unread badge" + checked: valueShowUnreadBadge + onToggled: checked => valueShowUnreadBadge = checked + } + + NCheckbox { + label: "Hide badge when zero" + checked: valueHideWhenZero + onToggled: checked => valueHideWhenZero = checked + } +} diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/SidePanelToggleSettings.qml b/Modules/SettingsPanel/Bar/WidgetSettings/SidePanelToggleSettings.qml new file mode 100644 index 0000000..7161f3d --- /dev/null +++ b/Modules/SettingsPanel/Bar/WidgetSettings/SidePanelToggleSettings.qml @@ -0,0 +1,30 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets +import qs.Services + +ColumnLayout { + id: root + spacing: Style.marginM * scaling + + // Properties to receive data from parent + property var widgetData: null + property var widgetMetadata: null + + // Local state + property bool valueUseDistroLogo: widgetData.useDistroLogo !== undefined ? widgetData.useDistroLogo : widgetMetadata.useDistroLogo + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}) + settings.useDistroLogo = valueUseDistroLogo + return settings + } + + NCheckbox { + label: "Use distro logo instead of icon" + checked: valueUseDistroLogo + onToggled: checked => valueUseDistroLogo = checked + } +} diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/SpacerSettings.qml b/Modules/SettingsPanel/Bar/WidgetSettings/SpacerSettings.qml new file mode 100644 index 0000000..8de5f6e --- /dev/null +++ b/Modules/SettingsPanel/Bar/WidgetSettings/SpacerSettings.qml @@ -0,0 +1,30 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets +import qs.Services + +ColumnLayout { + id: root + spacing: Style.marginM * scaling + + // Properties to receive data from parent + property var widgetData: null + property var widgetMetadata: null + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}) + settings.width = parseInt(widthInput.text) || widgetMetadata.width + return settings + } + + NTextInput { + id: widthInput + Layout.fillWidth: true + label: "Width" + description: "Spacing width in pixels" + text: widgetData.width || widgetMetadata.width + placeholderText: "Enter width in pixels" + } +} diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/SystemMonitorSettings.qml b/Modules/SettingsPanel/Bar/WidgetSettings/SystemMonitorSettings.qml new file mode 100644 index 0000000..3c3bbf9 --- /dev/null +++ b/Modules/SettingsPanel/Bar/WidgetSettings/SystemMonitorSettings.qml @@ -0,0 +1,63 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets +import qs.Services + +ColumnLayout { + id: root + spacing: Style.marginM * scaling + + // Properties to receive data from parent + property var widgetData: null + property var widgetMetadata: null + + // Local, editable state for checkboxes + property bool valueShowCpuUsage: widgetData.showCpuUsage !== undefined ? widgetData.showCpuUsage : widgetMetadata.showCpuUsage + property bool valueShowCpuTemp: widgetData.showCpuTemp !== undefined ? widgetData.showCpuTemp : widgetMetadata.showCpuTemp + property bool valueShowMemoryUsage: widgetData.showMemoryUsage !== undefined ? widgetData.showMemoryUsage : widgetMetadata.showMemoryUsage + property bool valueShowNetworkStats: widgetData.showNetworkStats + !== undefined ? widgetData.showNetworkStats : widgetMetadata.showNetworkStats + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}) + settings.showCpuUsage = valueShowCpuUsage + settings.showCpuTemp = valueShowCpuTemp + settings.showMemoryUsage = valueShowMemoryUsage + settings.showNetworkStats = valueShowNetworkStats + return settings + } + + NCheckbox { + id: showCpuUsage + Layout.fillWidth: true + label: "CPU usage" + checked: valueShowCpuUsage + onToggled: checked => valueShowCpuUsage = checked + } + + NCheckbox { + id: showCpuTemp + Layout.fillWidth: true + label: "CPU temperature" + checked: valueShowCpuTemp + onToggled: checked => valueShowCpuTemp = checked + } + + NCheckbox { + id: showMemoryUsage + Layout.fillWidth: true + label: "Memory usage" + checked: valueShowMemoryUsage + onToggled: checked => valueShowMemoryUsage = checked + } + + NCheckbox { + id: showNetworkStats + Layout.fillWidth: true + label: "Network traffic" + checked: valueShowNetworkStats + onToggled: checked => valueShowNetworkStats = checked + } +} diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/VolumeSettings.qml b/Modules/SettingsPanel/Bar/WidgetSettings/VolumeSettings.qml new file mode 100644 index 0000000..840c33c --- /dev/null +++ b/Modules/SettingsPanel/Bar/WidgetSettings/VolumeSettings.qml @@ -0,0 +1,31 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets +import qs.Services + +ColumnLayout { + id: root + spacing: Style.marginM * scaling + + // Properties to receive data from parent + property var widgetData: null + property var widgetMetadata: null + + // Local state + property bool valueAlwaysShowPercentage: widgetData.alwaysShowPercentage + !== undefined ? widgetData.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}) + settings.alwaysShowPercentage = valueAlwaysShowPercentage + return settings + } + + NCheckbox { + label: "Always show percentage" + checked: valueAlwaysShowPercentage + onToggled: checked => valueAlwaysShowPercentage = checked + } +} diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/WorkspaceSettings.qml b/Modules/SettingsPanel/Bar/WidgetSettings/WorkspaceSettings.qml new file mode 100644 index 0000000..1e44dae --- /dev/null +++ b/Modules/SettingsPanel/Bar/WidgetSettings/WorkspaceSettings.qml @@ -0,0 +1,44 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets +import qs.Services + +ColumnLayout { + id: root + spacing: Style.marginM * scaling + + // Properties to receive data from parent + property var widgetData: null + property var widgetMetadata: null + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}) + settings.labelMode = labelModeCombo.currentKey + return settings + } + + NComboBox { + id: labelModeCombo + + label: "Label Mode" + model: ListModel { + ListElement { + key: "none" + name: "None" + } + ListElement { + key: "index" + name: "Index" + } + ListElement { + key: "name" + name: "Name" + } + } + currentKey: widgetData.labelMode || widgetMetadata.labelMode + onSelected: key => labelModeCombo.currentKey = key + minimumWidth: 200 * scaling + } +} diff --git a/Modules/SettingsPanel/Extras/BarWidgetSettingsDialog.qml b/Modules/SettingsPanel/Extras/BarWidgetSettingsDialog.qml deleted file mode 100644 index b35d3ce..0000000 --- a/Modules/SettingsPanel/Extras/BarWidgetSettingsDialog.qml +++ /dev/null @@ -1,578 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Effects -import QtQuick.Layouts -import qs.Commons -import qs.Widgets -import qs.Services - -// Widget Settings Dialog Component -Popup { - id: settingsPopup - - property int widgetIndex: -1 - property var widgetData: null - property string widgetId: "" - - // Center popup in parent - x: (parent.width - width) * 0.5 - y: (parent.height - height) * 0.5 - - width: 420 * scaling - height: content.implicitHeight + padding * 2 - padding: Style.marginXL * scaling - modal: true - - background: Rectangle { - id: bgRect - color: Color.mSurface - radius: Style.radiusL * scaling - border.color: Color.mPrimary - border.width: Style.borderM * scaling - } - - ColumnLayout { - id: content - width: parent.width - spacing: Style.marginM * scaling - - // Title - RowLayout { - Layout.fillWidth: true - - NText { - text: "Widget Settings: " + settingsPopup.widgetId - font.pointSize: Style.fontSizeL * scaling - font.weight: Style.fontWeightBold - color: Color.mPrimary - Layout.fillWidth: true - } - - NIconButton { - icon: "close" - onClicked: settingsPopup.close() - } - } - - // Separator - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 1 - color: Color.mOutline - } - - // Settings based on widget type - Loader { - id: settingsLoader - Layout.fillWidth: true - sourceComponent: { - if (settingsPopup.widgetId === "CustomButton") { - return customButtonSettings - } else if (settingsPopup.widgetId === "Spacer") { - return spacerSettings - } else if (settingsPopup.widgetId === "Workspace") { - return workspaceSettings - } else if (settingsPopup.widgetId === "SystemMonitor") { - return systemMonitorSettings - } else if (settingsPopup.widgetId === "ActiveWindow") { - return activeWindowSettings - } else if (settingsPopup.widgetId === "MediaMini") { - return mediaMiniSettings - } else if (settingsPopup.widgetId === "Clock") { - return clockSettings - } else if (settingsPopup.widgetId === "Volume") { - return volumeSettings - } else if (settingsPopup.widgetId === "Microphone") { - return microphoneSettings - } else if (settingsPopup.widgetId === "NotificationHistory") { - return notificationHistorySettings - } else if (settingsPopup.widgetId === "Brightness") { - return brightnessSettings - } else if (settingsPopup.widgetId === "SidePanelToggle") { - return sidePanelToggleSettings - } - // Add more widget settings components here as needed - return null - } - } - - // Action buttons - RowLayout { - Layout.fillWidth: true - Layout.topMargin: Style.marginM * scaling - - Item { - Layout.fillWidth: true - } - - NButton { - text: "Cancel" - outlined: true - onClicked: settingsPopup.close() - } - - NButton { - text: "Apply" - icon: "check" - onClicked: { - if (settingsLoader.item && settingsLoader.item.saveSettings) { - var newSettings = settingsLoader.item.saveSettings() - root.updateWidgetSettings(sectionId, settingsPopup.widgetIndex, newSettings) - settingsPopup.close() - } - } - } - } - } - - // SidePanelToggle settings component - Component { - id: sidePanelToggleSettings - - ColumnLayout { - spacing: Style.marginM * scaling - - // Local state - property bool valueUseDistroLogo: settingsPopup.widgetData.useDistroLogo - !== undefined ? settingsPopup.widgetData.useDistroLogo : BarWidgetRegistry.widgetMetadata["SidePanelToggle"].useDistroLogo - - function saveSettings() { - var settings = Object.assign({}, settingsPopup.widgetData) - settings.useDistroLogo = valueUseDistroLogo - return settings - } - - NCheckbox { - label: "Use distro logo instead of icon" - checked: valueUseDistroLogo - onToggled: checked => valueUseDistroLogo = checked - } - } - } - - // Brightness settings component - Component { - id: brightnessSettings - - ColumnLayout { - spacing: Style.marginM * scaling - - // Local state - property bool valueAlwaysShowPercentage: settingsPopup.widgetData.alwaysShowPercentage - !== undefined ? settingsPopup.widgetData.alwaysShowPercentage : BarWidgetRegistry.widgetMetadata["Brightness"].alwaysShowPercentage - - function saveSettings() { - var settings = Object.assign({}, settingsPopup.widgetData) - settings.alwaysShowPercentage = valueAlwaysShowPercentage - return settings - } - - NCheckbox { - label: "Always show percentage" - checked: valueAlwaysShowPercentage - onToggled: checked => valueAlwaysShowPercentage = checked - } - } - } - - // NotificationHistory settings component - Component { - id: notificationHistorySettings - - ColumnLayout { - spacing: Style.marginM * scaling - - // Local state - property bool valueShowUnreadBadge: settingsPopup.widgetData.showUnreadBadge - !== undefined ? settingsPopup.widgetData.showUnreadBadge : BarWidgetRegistry.widgetMetadata["NotificationHistory"].showUnreadBadge - property bool valueHideWhenZero: settingsPopup.widgetData.hideWhenZero - !== undefined ? settingsPopup.widgetData.hideWhenZero : BarWidgetRegistry.widgetMetadata["NotificationHistory"].hideWhenZero - - function saveSettings() { - var settings = Object.assign({}, settingsPopup.widgetData) - settings.showUnreadBadge = valueShowUnreadBadge - settings.hideWhenZero = valueHideWhenZero - return settings - } - - NCheckbox { - label: "Show unread badge" - checked: valueShowUnreadBadge - onToggled: checked => valueShowUnreadBadge = checked - } - - NCheckbox { - label: "Hide badge when zero" - checked: valueHideWhenZero - onToggled: checked => valueHideWhenZero = checked - } - } - } - - // Microphone settings component - Component { - id: microphoneSettings - - ColumnLayout { - spacing: Style.marginM * scaling - - // Local state - property bool valueAlwaysShowPercentage: settingsPopup.widgetData.alwaysShowPercentage - !== undefined ? settingsPopup.widgetData.alwaysShowPercentage : BarWidgetRegistry.widgetMetadata["Microphone"].alwaysShowPercentage - - function saveSettings() { - var settings = Object.assign({}, settingsPopup.widgetData) - settings.alwaysShowPercentage = valueAlwaysShowPercentage - return settings - } - - NCheckbox { - label: "Always show percentage" - checked: valueAlwaysShowPercentage - onToggled: checked => valueAlwaysShowPercentage = checked - } - } - } - - // Volume settings component - Component { - id: volumeSettings - - ColumnLayout { - spacing: Style.marginM * scaling - - // Local state - property bool valueAlwaysShowPercentage: settingsPopup.widgetData.alwaysShowPercentage - !== undefined ? settingsPopup.widgetData.alwaysShowPercentage : BarWidgetRegistry.widgetMetadata["Volume"].alwaysShowPercentage - - function saveSettings() { - var settings = Object.assign({}, settingsPopup.widgetData) - settings.alwaysShowPercentage = valueAlwaysShowPercentage - return settings - } - - NCheckbox { - label: "Always show percentage" - checked: valueAlwaysShowPercentage - onToggled: checked => valueAlwaysShowPercentage = checked - } - } - } - - // Clock settings component - Component { - id: clockSettings - - ColumnLayout { - spacing: Style.marginM * scaling - - // Local state - property bool valueShowDate: settingsPopup.widgetData.showDate - !== undefined ? settingsPopup.widgetData.showDate : BarWidgetRegistry.widgetMetadata["Clock"].showDate - property bool valueUse12h: settingsPopup.widgetData.use12HourClock - !== undefined ? settingsPopup.widgetData.use12HourClock : BarWidgetRegistry.widgetMetadata["Clock"].use12HourClock - property bool valueShowSeconds: settingsPopup.widgetData.showSeconds - !== undefined ? settingsPopup.widgetData.showSeconds : BarWidgetRegistry.widgetMetadata["Clock"].showSeconds - property bool valueReverseDayMonth: settingsPopup.widgetData.reverseDayMonth - !== undefined ? settingsPopup.widgetData.reverseDayMonth : BarWidgetRegistry.widgetMetadata["Clock"].reverseDayMonth - - function saveSettings() { - var settings = Object.assign({}, settingsPopup.widgetData) - settings.showDate = valueShowDate - settings.use12HourClock = valueUse12h - settings.showSeconds = valueShowSeconds - settings.reverseDayMonth = valueReverseDayMonth - return settings - } - - NCheckbox { - label: "Show date next to time" - checked: valueShowDate - onToggled: checked => valueShowDate = checked - } - - NCheckbox { - label: "Use 12-hour clock" - checked: valueUse12h - onToggled: checked => valueUse12h = checked - } - - NCheckbox { - label: "Show seconds" - checked: valueShowSeconds - onToggled: checked => valueShowSeconds = checked - } - - NCheckbox { - label: "Reverse day and month" - checked: valueReverseDayMonth - onToggled: checked => valueReverseDayMonth = checked - } - } - } - - // MediaMini settings component - Component { - id: mediaMiniSettings - - ColumnLayout { - spacing: Style.marginM * scaling - - // Local state - property bool valueShowAlbumArt: settingsPopup.widgetData.showAlbumArt - !== undefined ? settingsPopup.widgetData.showAlbumArt : BarWidgetRegistry.widgetMetadata["MediaMini"].showAlbumArt - property bool valueShowVisualizer: settingsPopup.widgetData.showVisualizer - !== undefined ? settingsPopup.widgetData.showVisualizer : BarWidgetRegistry.widgetMetadata["MediaMini"].showVisualizer - property string valueVisualizerType: settingsPopup.widgetData.visualizerType - || BarWidgetRegistry.widgetMetadata["MediaMini"].visualizerType - - function saveSettings() { - var settings = Object.assign({}, settingsPopup.widgetData) - settings.showAlbumArt = valueShowAlbumArt - settings.showVisualizer = valueShowVisualizer - settings.visualizerType = valueVisualizerType - return settings - } - - NCheckbox { - label: "Show album art" - checked: valueShowAlbumArt - onToggled: checked => valueShowAlbumArt = checked - } - - NCheckbox { - label: "Show visualizer" - checked: valueShowVisualizer - onToggled: checked => valueShowVisualizer = checked - } - - NComboBox { - label: "Visualizer type" - description: "Select the visualizer style" - preferredWidth: 180 * scaling - model: ListModel { - ListElement { - key: "linear" - name: "Linear" - } - ListElement { - key: "mirrored" - name: "Mirrored" - } - ListElement { - key: "wave" - name: "Wave" - } - } - currentKey: valueVisualizerType - onSelected: key => valueVisualizerType = key - } - } - } - - // ActiveWindow settings component - Component { - id: activeWindowSettings - - ColumnLayout { - spacing: Style.marginM * scaling - - // Local, editable state - property bool valueShowIcon: settingsPopup.widgetData.showIcon - !== undefined ? settingsPopup.widgetData.showIcon : BarWidgetRegistry.widgetMetadata["ActiveWindow"].showIcon - - function saveSettings() { - var settings = Object.assign({}, settingsPopup.widgetData) - settings.showIcon = valueShowIcon - return settings - } - - NCheckbox { - id: showIcon - Layout.fillWidth: true - label: "Show app icon" - checked: valueShowIcon - onToggled: checked => valueShowIcon = checked - } - } - } - - // CustomButton settings component - Component { - id: customButtonSettings - - ColumnLayout { - spacing: Style.marginM * scaling - - function saveSettings() { - var settings = Object.assign({}, settingsPopup.widgetData) - settings.icon = iconInput.text - settings.leftClickExec = leftClickExecInput.text - settings.rightClickExec = rightClickExecInput.text - settings.middleClickExec = middleClickExecInput.text - return settings - } - - // Icon setting - NTextInput { - id: iconInput - Layout.fillWidth: true - Layout.bottomMargin: Style.marginXL * scaling - label: "Icon Name" - description: "Use Material Icon names from the icon set." - text: settingsPopup.widgetData.icon || "" - placeholderText: "Enter icon name (e.g., favorite, home, settings)" - } - - NTextInput { - id: leftClickExecInput - Layout.fillWidth: true - label: "Left Click Command" - description: "Command or application to run when left clicked." - text: settingsPopup.widgetData.leftClickExec || "" - placeholderText: "Enter command to execute (app or custom script)" - } - - NTextInput { - id: rightClickExecInput - Layout.fillWidth: true - label: "Right Click Command" - description: "Command or application to run when right clicked." - text: settingsPopup.widgetData.rightClickExec || "" - placeholderText: "Enter command to execute (app or custom script)" - } - - NTextInput { - id: middleClickExecInput - Layout.fillWidth: true - label: "Middle Click Command" - description: "Command or application to run when middle clicked." - text: settingsPopup.widgetData.middleClickExec || "" - placeholderText: "Enter command to execute (app or custom script)" - } - } - } - - // Spacer settings component - Component { - id: spacerSettings - - ColumnLayout { - spacing: Style.marginM * scaling - - function saveSettings() { - var settings = Object.assign({}, settingsPopup.widgetData) - settings.width = parseInt(widthInput.text) || 20 - return settings - } - - NTextInput { - id: widthInput - Layout.fillWidth: true - label: "Width (pixels)" - description: "Width of the spacer in pixels." - text: settingsPopup.widgetData.width || "20" - placeholderText: "Enter width in pixels" - } - } - } - - // Workspace settings component - Component { - id: workspaceSettings - - ColumnLayout { - spacing: Style.marginM * scaling - - function saveSettings() { - var settings = Object.assign({}, settingsPopup.widgetData) - settings.labelMode = labelModeCombo.currentKey - return settings - } - - NComboBox { - id: labelModeCombo - Layout.fillWidth: true - preferredWidth: 180 * scaling - label: "Label Mode" - description: "Choose how to label workspace pills." - model: ListModel { - ListElement { - key: "none" - name: "None" - } - ListElement { - key: "index" - name: "Index" - } - ListElement { - key: "name" - name: "Name" - } - } - currentKey: settingsPopup.widgetData.labelMode || BarWidgetRegistry.widgetMetadata["Workspace"].labelMode - onSelected: key => labelModeCombo.currentKey = key - } - } - } - - // SystemMonitor settings component - Component { - id: systemMonitorSettings - - ColumnLayout { - spacing: Style.marginM * scaling - - // Local, editable state for checkboxes - property bool valueShowCpuUsage: settingsPopup.widgetData.showCpuUsage - !== undefined ? settingsPopup.widgetData.showCpuUsage : BarWidgetRegistry.widgetMetadata["SystemMonitor"].showCpuUsage - property bool valueShowCpuTemp: settingsPopup.widgetData.showCpuTemp - !== undefined ? settingsPopup.widgetData.showCpuTemp : BarWidgetRegistry.widgetMetadata["SystemMonitor"].showCpuTemp - property bool valueShowMemoryUsage: settingsPopup.widgetData.showMemoryUsage - !== undefined ? settingsPopup.widgetData.showMemoryUsage : BarWidgetRegistry.widgetMetadata["SystemMonitor"].showMemoryUsage - property bool valueShowNetworkStats: settingsPopup.widgetData.showNetworkStats - !== undefined ? settingsPopup.widgetData.showNetworkStats : BarWidgetRegistry.widgetMetadata["SystemMonitor"].showNetworkStats - - function saveSettings() { - var settings = Object.assign({}, settingsPopup.widgetData) - settings.showCpuUsage = valueShowCpuUsage - settings.showCpuTemp = valueShowCpuTemp - settings.showMemoryUsage = valueShowMemoryUsage - settings.showNetworkStats = valueShowNetworkStats - return settings - } - - NCheckbox { - id: showCpuUsage - Layout.fillWidth: true - label: "CPU usage" - checked: valueShowCpuUsage - onToggled: checked => valueShowCpuUsage = checked - } - - NCheckbox { - id: showCpuTemp - Layout.fillWidth: true - label: "CPU temperature" - checked: valueShowCpuTemp - onToggled: checked => valueShowCpuTemp = checked - } - - NCheckbox { - id: showMemoryUsage - Layout.fillWidth: true - label: "Memory usage" - checked: valueShowMemoryUsage - onToggled: checked => valueShowMemoryUsage = checked - } - - NCheckbox { - id: showNetworkStats - Layout.fillWidth: true - label: "Network traffic" - checked: valueShowNetworkStats - onToggled: checked => valueShowNetworkStats = checked - } - } - } -} diff --git a/Modules/SettingsPanel/Tabs/BarTab.qml b/Modules/SettingsPanel/Tabs/BarTab.qml index 2e6368f..7180642 100644 --- a/Modules/SettingsPanel/Tabs/BarTab.qml +++ b/Modules/SettingsPanel/Tabs/BarTab.qml @@ -4,7 +4,7 @@ import QtQuick.Layouts import qs.Commons import qs.Services import qs.Widgets -import qs.Modules.SettingsPanel.Extras +import qs.Modules.SettingsPanel.Bar ColumnLayout { id: root @@ -70,14 +70,6 @@ ColumnLayout { } } } - - // Keep Battery toggle here for now (cannot test per-widget yet) - NToggle { - label: "Show Battery Percentage" - description: "Display battery percentage at all times." - checked: Settings.data.bar.alwaysShowBatteryPercentage - onToggled: checked => Settings.data.bar.alwaysShowBatteryPercentage = checked - } } NDivider { diff --git a/Services/BarWidgetRegistry.qml b/Services/BarWidgetRegistry.qml index 48e710e..80a8ed9 100644 --- a/Services/BarWidgetRegistry.qml +++ b/Services/BarWidgetRegistry.qml @@ -47,17 +47,16 @@ Singleton { }, "Spacer": { "allowUserSettings": true, - "icon": "space_bar", "width": 20 }, - "ActiveWindow"// Per-instance settings for common widgets shown in BarTab - : { + "ActiveWindow": { "allowUserSettings": true, "showIcon": true }, "Battery": { "allowUserSettings": true, - "alwaysShowPercentage": false + "alwaysShowPercentage": false, + "warningThreshold": 30 }, "SystemMonitor": { "allowUserSettings": true, @@ -68,15 +67,13 @@ Singleton { }, "Workspace": { "allowUserSettings": true, - "labelMode"// none | index | name - : "index" + "labelMode": "index" }, "MediaMini": { "allowUserSettings": true, "showAlbumArt": false, "showVisualizer": false, - "visualizerType"// linear | mirrored | wave - : "linear" + "visualizerType": "linear" }, "Clock": { "allowUserSettings": true, From 517c7c97d4787e78c742490a07254e5da33ca099 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sun, 7 Sep 2025 22:23:45 -0400 Subject: [PATCH 35/54] Bar Widgets FrontEnd: Simplified access to editable widget settings --- Modules/Bar/Bar.qml | 3 ++ Modules/Bar/Widgets/ActiveWindow.qml | 7 ++- Modules/Bar/Widgets/Battery.qml | 7 +-- Modules/Bar/Widgets/Brightness.qml | 8 +++- Modules/Bar/Widgets/Clock.qml | 15 +++---- Modules/Bar/Widgets/CustomButton.qml | 49 ++++++++++----------- Modules/Bar/Widgets/KeyboardLayout.qml | 3 -- Modules/Bar/Widgets/MediaMini.qml | 40 +++++++++-------- Modules/Bar/Widgets/Microphone.qml | 11 +++-- Modules/Bar/Widgets/NightLight.qml | 1 - Modules/Bar/Widgets/NotificationHistory.qml | 14 +++--- Modules/Bar/Widgets/SidePanelToggle.qml | 13 ++++-- Modules/Bar/Widgets/Spacer.qml | 19 +++----- Modules/Bar/Widgets/SystemMonitor.qml | 25 ++++++----- Modules/Bar/Widgets/Tray.qml | 1 + Modules/Bar/Widgets/Volume.qml | 13 ++++-- Modules/Bar/Widgets/Workspace.qml | 11 +++-- 17 files changed, 135 insertions(+), 105 deletions(-) diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 30308e0..dfcb721 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -76,6 +76,7 @@ Variants { widgetProps: { "screen": root.modelData || null, "scaling": ScalingService.getScreenScale(screen), + "widgetId": modelData.id, "barSection": parent.objectName, "sectionWidgetIndex": index, "sectionWidgetsCount": Settings.data.bar.widgets.left.length @@ -103,6 +104,7 @@ Variants { widgetProps: { "screen": root.modelData || null, "scaling": ScalingService.getScreenScale(screen), + "widgetId": modelData.id, "barSection": parent.objectName, "sectionWidgetIndex": index, "sectionWidgetsCount": Settings.data.bar.widgets.center.length @@ -131,6 +133,7 @@ Variants { widgetProps: { "screen": root.modelData || null, "scaling": ScalingService.getScreenScale(screen), + "widgetId": modelData.id, "barSection": parent.objectName, "sectionWidgetIndex": index, "sectionWidgetsCount": Settings.data.bar.widgets.right.length diff --git a/Modules/Bar/Widgets/ActiveWindow.qml b/Modules/Bar/Widgets/ActiveWindow.qml index e817bb1..d2d4578 100644 --- a/Modules/Bar/Widgets/ActiveWindow.qml +++ b/Modules/Bar/Widgets/ActiveWindow.qml @@ -18,10 +18,13 @@ RowLayout { spacing: Style.marginS * scaling visible: getTitle() !== "" + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" property string barSection: "" property int sectionWidgetIndex: -1 property int sectionWidgetsCount: 0 + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] property var widgetSettings: { var section = barSection.replace("Section", "").toLowerCase() if (section && sectionWidgetIndex >= 0) { @@ -33,7 +36,7 @@ RowLayout { return {} } - readonly property bool userShowIcon: (widgetSettings.showIcon !== undefined) ? widgetSettings.showIcon : ((Settings.data.bar.showActiveWindowIcon !== undefined) ? Settings.data.bar.showActiveWindowIcon : BarWidgetRegistry.widgetMetadata["ActiveWindow"].showIcon) + readonly property bool showIcon: (widgetSettings.showIcon !== undefined) ? widgetSettings.showIcon : widgetMetadata.showIcon function getTitle() { return CompositorService.focusedWindowTitle !== "(No active window)" ? CompositorService.focusedWindowTitle : "" @@ -91,7 +94,7 @@ RowLayout { Layout.preferredWidth: Style.fontSizeL * scaling * 1.2 Layout.preferredHeight: Style.fontSizeL * scaling * 1.2 Layout.alignment: Qt.AlignVCenter - visible: getTitle() !== "" && userShowIcon + visible: getTitle() !== "" && showIcon IconImage { id: windowIcon diff --git a/Modules/Bar/Widgets/Battery.qml b/Modules/Bar/Widgets/Battery.qml index d27c2cb..060d3e1 100644 --- a/Modules/Bar/Widgets/Battery.qml +++ b/Modules/Bar/Widgets/Battery.qml @@ -13,11 +13,12 @@ Item { property real scaling: 1.0 // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" property string barSection: "" property int sectionWidgetIndex: -1 property int sectionWidgetsCount: 0 - // Resolve per-instance widget settings from Settings.data + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] property var widgetSettings: { var section = barSection.replace("Section", "").toLowerCase() if (section && sectionWidgetIndex >= 0) { @@ -31,9 +32,9 @@ Item { // Resolve settings: try user settings or defaults from BarWidgetRegistry readonly property bool alwaysShowPercentage: widgetSettings.alwaysShowPercentage - !== undefined ? widgetSettings.alwaysShowPercentage : BarWidgetRegistry.widgetMetadata["Battery"].alwaysShowPercentage + !== undefined ? widgetSettings.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage readonly property real warningThreshold: widgetSettings.warningThreshold - !== undefined ? widgetSettings.warningThreshold : BarWidgetRegistry.widgetMetadata["Battery"].warningThreshold + !== undefined ? widgetSettings.warningThreshold : widgetMetadata.warningThreshold // Test mode readonly property bool testMode: true diff --git a/Modules/Bar/Widgets/Brightness.qml b/Modules/Bar/Widgets/Brightness.qml index aa434a2..30948c3 100644 --- a/Modules/Bar/Widgets/Brightness.qml +++ b/Modules/Bar/Widgets/Brightness.qml @@ -10,10 +10,14 @@ Item { property ShellScreen screen property real scaling: 1.0 + + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" property string barSection: "" - property int sectionWidgetIndex: 0 + property int sectionWidgetIndex: -1 property int sectionWidgetsCount: 0 + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] property var widgetSettings: { var section = barSection.replace("Section", "").toLowerCase() if (section && sectionWidgetIndex >= 0) { @@ -26,7 +30,7 @@ Item { } readonly property bool userAlwaysShowPercentage: (widgetSettings.alwaysShowPercentage - !== undefined) ? widgetSettings.alwaysShowPercentage : BarWidgetRegistry.widgetMetadata["Brightness"].alwaysShowPercentage + !== undefined) ? widgetSettings.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage // Used to avoid opening the pill on Quickshell startup property bool firstBrightnessReceived: false diff --git a/Modules/Bar/Widgets/Clock.qml b/Modules/Bar/Widgets/Clock.qml index 3067e7c..3b472d9 100644 --- a/Modules/Bar/Widgets/Clock.qml +++ b/Modules/Bar/Widgets/Clock.qml @@ -11,11 +11,12 @@ Rectangle { property real scaling: 1.0 // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" property string barSection: "" property int sectionWidgetIndex: -1 property int sectionWidgetsCount: 0 - // Resolve per-instance widget settings from Settings.data + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] property var widgetSettings: { var section = barSection.replace("Section", "").toLowerCase() if (section && sectionWidgetIndex >= 0) { @@ -28,13 +29,11 @@ Rectangle { } // Resolve settings: try user settings or defaults from BarWidgetRegistry - readonly property bool showDate: widgetSettings.showDate - !== undefined ? widgetSettings.showDate : BarWidgetRegistry.widgetMetadata["Clock"].showDate - readonly property bool use12h: widgetSettings.use12HourClock - !== undefined ? widgetSettings.use12HourClock : BarWidgetRegistry.widgetMetadata["Clock"].use12HourClock - readonly property bool showSeconds: widgetSettings.showSeconds - !== undefined ? widgetSettings.showSeconds : BarWidgetRegistry.widgetMetadata["Clock"].showSeconds - readonly property bool reverseDayMonth: widgetSettings.reverseDayMonth !== undefined ? widgetSettings.reverseDayMonth : BarWidgetRegistry.widgetMetadata["Clock"].reverseDayMonth + readonly property bool showDate: widgetSettings.showDate !== undefined ? widgetSettings.showDate : widgetMetadata.showDate + readonly property bool use12h: widgetSettings.use12HourClock !== undefined ? widgetSettings.use12HourClock : widgetMetadata.use12HourClock + readonly property bool showSeconds: widgetSettings.showSeconds !== undefined ? widgetSettings.showSeconds : widgetMetadata.showSeconds + readonly property bool reverseDayMonth: widgetSettings.reverseDayMonth + !== undefined ? widgetSettings.reverseDayMonth : widgetMetadata.reverseDayMonth implicitWidth: clock.width + Style.marginM * 2 * scaling implicitHeight: Math.round(Style.capsuleHeight * scaling) diff --git a/Modules/Bar/Widgets/CustomButton.qml b/Modules/Bar/Widgets/CustomButton.qml index de8f96d..08d3dc8 100644 --- a/Modules/Bar/Widgets/CustomButton.qml +++ b/Modules/Bar/Widgets/CustomButton.qml @@ -13,11 +13,13 @@ NIconButton { property var screen property real scaling: 1.0 + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" property string barSection: "" property int sectionWidgetIndex: -1 property int sectionWidgetsCount: 0 - // Get user settings from Settings data + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] property var widgetSettings: { var section = barSection.replace("Section", "").toLowerCase() if (section && sectionWidgetIndex >= 0) { @@ -30,30 +32,27 @@ NIconButton { } // Use settings or defaults from BarWidgetRegistry - readonly property string userIcon: widgetSettings.icon || BarWidgetRegistry.widgetMetadata["CustomButton"].icon - readonly property string userLeftClickExec: widgetSettings.leftClickExec - || BarWidgetRegistry.widgetMetadata["CustomButton"].leftClickExec - readonly property string userRightClickExec: widgetSettings.rightClickExec - || BarWidgetRegistry.widgetMetadata["CustomButton"].rightClickExec - readonly property string userMiddleClickExec: widgetSettings.middleClickExec - || BarWidgetRegistry.widgetMetadata["CustomButton"].middleClickExec - readonly property bool hasExec: (userLeftClickExec || userRightClickExec || userMiddleClickExec) + readonly property string customIcon: widgetSettings.icon || widgetMetadata.icon + readonly property string leftClickExec: widgetSettings.leftClickExec || widgetMetadata.leftClickExec + readonly property string rightClickExec: widgetSettings.rightClickExec || widgetMetadata.rightClickExec + readonly property string middleClickExec: widgetSettings.middleClickExec || widgetMetadata.middleClickExec + readonly property bool hasExec: (leftClickExec || rightClickExec || middleClickExec) sizeRatio: 0.8 - icon: userIcon + icon: customIcon tooltipText: { if (!hasExec) { return "Custom Button - Configure in settings" } else { var lines = [] - if (userLeftClickExec !== "") { - lines.push(`Left click: ${userLeftClickExec}.`) + if (leftClickExec !== "") { + lines.push(`Left click: ${leftClickExec}.`) } - if (userRightClickExec !== "") { - lines.push(`Right click: ${userRightClickExec}.`) + if (rightClickExec !== "") { + lines.push(`Right click: ${rightClickExec}.`) } - if (userMiddleClickExec !== "") { - lines.push(`Middle click: ${userMiddleClickExec}.`) + if (middleClickExec !== "") { + lines.push(`Middle click: ${middleClickExec}.`) } return lines.join("
") } @@ -61,9 +60,9 @@ NIconButton { opacity: hasExec ? Style.opacityFull : Style.opacityMedium onClicked: { - if (userLeftClickExec) { - Quickshell.execDetached(["sh", "-c", userLeftClickExec]) - Logger.log("CustomButton", `Executing command: ${userLeftClickExec}`) + if (leftClickExec) { + Quickshell.execDetached(["sh", "-c", leftClickExec]) + Logger.log("CustomButton", `Executing command: ${leftClickExec}`) } else if (!hasExec) { // No script was defined, open settings var settingsPanel = PanelService.getPanel("settingsPanel") @@ -73,16 +72,16 @@ NIconButton { } onRightClicked: { - if (userRightClickExec) { - Quickshell.execDetached(["sh", "-c", userRightClickExec]) - Logger.log("CustomButton", `Executing command: ${userRightClickExec}`) + if (rightClickExec) { + Quickshell.execDetached(["sh", "-c", rightClickExec]) + Logger.log("CustomButton", `Executing command: ${rightClickExec}`) } } onMiddleClicked: { - if (userMiddleClickExec) { - Quickshell.execDetached(["sh", "-c", userMiddleClickExec]) - Logger.log("CustomButton", `Executing command: ${userMiddleClickExec}`) + if (middleClickExec) { + Quickshell.execDetached(["sh", "-c", middleClickExec]) + Logger.log("CustomButton", `Executing command: ${middleClickExec}`) } } } diff --git a/Modules/Bar/Widgets/KeyboardLayout.qml b/Modules/Bar/Widgets/KeyboardLayout.qml index b9b44a5..7de3a6d 100644 --- a/Modules/Bar/Widgets/KeyboardLayout.qml +++ b/Modules/Bar/Widgets/KeyboardLayout.qml @@ -12,9 +12,6 @@ Item { property ShellScreen screen property real scaling: 1.0 - property string barSection: "" - property int sectionWidgetIndex: 0 - property int sectionWidgetsCount: 0 // Use the shared service for keyboard layout property string currentLayout: KeyboardLayoutService.currentLayout diff --git a/Modules/Bar/Widgets/MediaMini.qml b/Modules/Bar/Widgets/MediaMini.qml index ba5fd5c..141698a 100644 --- a/Modules/Bar/Widgets/MediaMini.qml +++ b/Modules/Bar/Widgets/MediaMini.qml @@ -12,18 +12,14 @@ RowLayout { property ShellScreen screen property real scaling: 1.0 - readonly property real minWidth: 160 - readonly property real maxWidth: 400 - - Layout.alignment: Qt.AlignVCenter - spacing: Style.marginS * scaling - visible: MediaService.currentPlayer !== null && MediaService.canPlay - Layout.preferredWidth: MediaService.currentPlayer !== null && MediaService.canPlay ? implicitWidth : 0 + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" property string barSection: "" property int sectionWidgetIndex: -1 property int sectionWidgetsCount: 0 + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] property var widgetSettings: { var section = barSection.replace("Section", "").toLowerCase() if (section && sectionWidgetIndex >= 0) { @@ -35,17 +31,25 @@ RowLayout { return {} } - readonly property bool userShowAlbumArt: (widgetSettings.showAlbumArt !== undefined) ? widgetSettings.showAlbumArt : ((Settings.data.audio.showMiniplayerAlbumArt !== undefined) ? Settings.data.audio.showMiniplayerAlbumArt : BarWidgetRegistry.widgetMetadata["MediaMini"].showAlbumArt) - readonly property bool userShowVisualizer: (widgetSettings.showVisualizer !== undefined) ? widgetSettings.showVisualizer : ((Settings.data.audio.showMiniplayerCava !== undefined) ? Settings.data.audio.showMiniplayerCava : BarWidgetRegistry.widgetMetadata["MediaMini"].showVisualizer) - readonly property string userVisualizerType: (widgetSettings.visualizerType !== undefined - && widgetSettings.visualizerType - !== "") ? widgetSettings.visualizerType : ((Settings.data.audio.visualizerType !== undefined - && Settings.data.audio.visualizerType !== "") ? Settings.data.audio.visualizerType : BarWidgetRegistry.widgetMetadata["MediaMini"].visualizerType) + readonly property bool showAlbumArt: (widgetSettings.showAlbumArt + !== undefined) ? widgetSettings.showAlbumArt : widgetMetadata.showAlbumArt + readonly property bool showVisualizer: (widgetSettings.showVisualizer + !== undefined) ? widgetSettings.showVisualizer : widgetMetadata.showVisualizer + readonly property string visualizerType: (widgetSettings.visualizerType !== undefined && widgetSettings.visualizerType + !== "") ? widgetSettings.visualizerType : widgetMetadata.visualizerType + + readonly property real minWidth: 160 + readonly property real maxWidth: 400 function getTitle() { return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "") } + Layout.alignment: Qt.AlignVCenter + spacing: Style.marginS * scaling + visible: MediaService.currentPlayer !== null && MediaService.canPlay + Layout.preferredWidth: MediaService.currentPlayer !== null && MediaService.canPlay ? implicitWidth : 0 + // A hidden text element to safely measure the full title width NText { id: fullTitleMetrics @@ -80,7 +84,7 @@ RowLayout { Loader { anchors.verticalCenter: parent.verticalCenter anchors.horizontalCenter: parent.horizontalCenter - active: userShowVisualizer && userVisualizerType == "linear" && MediaService.isPlaying + active: showVisualizer && visualizerType == "linear" && MediaService.isPlaying z: 0 sourceComponent: LinearSpectrum { @@ -95,7 +99,7 @@ RowLayout { Loader { anchors.verticalCenter: parent.verticalCenter anchors.horizontalCenter: parent.horizontalCenter - active: userShowVisualizer && userVisualizerType == "mirrored" && MediaService.isPlaying + active: showVisualizer && visualizerType == "mirrored" && MediaService.isPlaying z: 0 sourceComponent: MirroredSpectrum { @@ -110,7 +114,7 @@ RowLayout { Loader { anchors.verticalCenter: parent.verticalCenter anchors.horizontalCenter: parent.horizontalCenter - active: userShowVisualizer && userVisualizerType == "wave" && MediaService.isPlaying + active: showVisualizer && visualizerType == "wave" && MediaService.isPlaying z: 0 sourceComponent: WaveSpectrum { @@ -134,12 +138,12 @@ RowLayout { font.pointSize: Style.fontSizeL * scaling verticalAlignment: Text.AlignVCenter Layout.alignment: Qt.AlignVCenter - visible: !userShowAlbumArt && getTitle() !== "" && !trackArt.visible + visible: !showAlbumArt && getTitle() !== "" && !trackArt.visible } ColumnLayout { Layout.alignment: Qt.AlignVCenter - visible: userShowAlbumArt + visible: showAlbumArt spacing: 0 Item { diff --git a/Modules/Bar/Widgets/Microphone.qml b/Modules/Bar/Widgets/Microphone.qml index 2b983a4..15f4437 100644 --- a/Modules/Bar/Widgets/Microphone.qml +++ b/Modules/Bar/Widgets/Microphone.qml @@ -12,10 +12,14 @@ Item { property ShellScreen screen property real scaling: 1.0 + + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" property string barSection: "" - property int sectionWidgetIndex: 0 + property int sectionWidgetIndex: -1 property int sectionWidgetsCount: 0 + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] property var widgetSettings: { var section = barSection.replace("Section", "").toLowerCase() if (section && sectionWidgetIndex >= 0) { @@ -27,7 +31,8 @@ Item { return {} } - readonly property bool userAlwaysShowPercentage: widgetSettings?.alwaysShowPercentage + readonly property bool alwaysShowPercentage: (widgetSettings.alwaysShowPercentage + !== undefined) ? widgetSettings.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage // Used to avoid opening the pill on Quickshell startup property bool firstInputVolumeReceived: false @@ -91,7 +96,7 @@ Item { collapsedIconColor: Color.mOnSurface autoHide: false // Important to be false so we can hover as long as we want text: Math.floor(AudioService.inputVolume * 100) + "%" - forceOpen: userAlwaysShowPercentage + forceOpen: alwaysShowPercentage tooltipText: "Microphone: " + Math.round(AudioService.inputVolume * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume.\nRight click to toggle mute." diff --git a/Modules/Bar/Widgets/NightLight.qml b/Modules/Bar/Widgets/NightLight.qml index 6ea2e20..90c8540 100644 --- a/Modules/Bar/Widgets/NightLight.qml +++ b/Modules/Bar/Widgets/NightLight.qml @@ -14,7 +14,6 @@ NIconButton { property real scaling: 1.0 sizeRatio: 0.8 - colorBg: Color.mSurfaceVariant colorFg: Color.mOnSurface colorBorder: Color.transparent diff --git a/Modules/Bar/Widgets/NotificationHistory.qml b/Modules/Bar/Widgets/NotificationHistory.qml index b79e63c..31657f1 100644 --- a/Modules/Bar/Widgets/NotificationHistory.qml +++ b/Modules/Bar/Widgets/NotificationHistory.qml @@ -13,10 +13,13 @@ NIconButton { property ShellScreen screen property real scaling: 1.0 + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" property string barSection: "" property int sectionWidgetIndex: -1 property int sectionWidgetsCount: 0 + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] property var widgetSettings: { var section = barSection.replace("Section", "").toLowerCase() if (section && sectionWidgetIndex >= 0) { @@ -27,9 +30,10 @@ NIconButton { } return {} } - - readonly property bool userShowUnreadBadge: (widgetSettings.showUnreadBadge !== undefined) ? widgetSettings.showUnreadBadge : BarWidgetRegistry.widgetMetadata["NotificationHistory"].showUnreadBadge - readonly property bool userHideWhenZero: (widgetSettings.hideWhenZero !== undefined) ? widgetSettings.hideWhenZero : BarWidgetRegistry.widgetMetadata["NotificationHistory"].hideWhenZero + readonly property bool showUnreadBadge: (widgetSettings.showUnreadBadge + !== undefined) ? widgetSettings.showUnreadBadge : widgetMetadata.showUnreadBadge + readonly property bool hideWhenZero: (widgetSettings.hideWhenZero + !== undefined) ? widgetSettings.hideWhenZero : widgetMetadata.hideWhenZero function lastSeenTs() { return Settings.data.notifications?.lastSeenTs || 0 @@ -70,7 +74,7 @@ NIconButton { anchors.rightMargin: -4 * scaling anchors.topMargin: -4 * scaling z: 2 - active: userShowUnreadBadge && (!userHideWhenZero || computeUnreadCount() > 0) + active: showUnreadBadge && (!hideWhenZero || computeUnreadCount() > 0) sourceComponent: Rectangle { id: badge readonly property int count: computeUnreadCount() @@ -82,7 +86,7 @@ NIconButton { color: Color.mError border.color: Color.mSurface border.width: 1 - visible: count > 0 || !userHideWhenZero + visible: count > 0 || !hideWhenZero NText { id: textNode anchors.centerIn: parent diff --git a/Modules/Bar/Widgets/SidePanelToggle.qml b/Modules/Bar/Widgets/SidePanelToggle.qml index 7c6b4f3..14a8c6f 100644 --- a/Modules/Bar/Widgets/SidePanelToggle.qml +++ b/Modules/Bar/Widgets/SidePanelToggle.qml @@ -11,10 +11,14 @@ NIconButton { property ShellScreen screen property real scaling: 1.0 + + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" property string barSection: "" property int sectionWidgetIndex: -1 property int sectionWidgetsCount: 0 + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] property var widgetSettings: { var section = barSection.replace("Section", "").toLowerCase() if (section && sectionWidgetIndex >= 0) { @@ -26,9 +30,10 @@ NIconButton { return {} } - readonly property bool userUseDistroLogo: (widgetSettings.useDistroLogo !== undefined) ? widgetSettings.useDistroLogo : ((Settings.data.bar.useDistroLogo !== undefined) ? Settings.data.bar.useDistroLogo : BarWidgetRegistry.widgetMetadata["SidePanelToggle"].useDistroLogo) + readonly property bool useDistroLogo: (widgetSettings.useDistroLogo + !== undefined) ? widgetSettings.useDistroLogo : widgetMetadata.useDistroLogo - icon: userUseDistroLogo ? "" : "widgets" + icon: useDistroLogo ? "" : "widgets" tooltipText: "Open side panel." sizeRatio: 0.8 @@ -46,8 +51,8 @@ NIconButton { anchors.centerIn: parent width: root.width * 0.6 height: width - source: userUseDistroLogo ? DistroLogoService.osLogo : "" - visible: userUseDistroLogo && source !== "" + source: useDistroLogo ? DistroLogoService.osLogo : "" + visible: useDistroLogo && source !== "" smooth: true } diff --git a/Modules/Bar/Widgets/Spacer.qml b/Modules/Bar/Widgets/Spacer.qml index 5a62372..dc2651c 100644 --- a/Modules/Bar/Widgets/Spacer.qml +++ b/Modules/Bar/Widgets/Spacer.qml @@ -12,11 +12,13 @@ Item { property var screen property real scaling: 1.0 + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" property string barSection: "" property int sectionWidgetIndex: -1 property int sectionWidgetsCount: 0 - // Get user settings from Settings data - make it reactive + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] property var widgetSettings: { var section = barSection.replace("Section", "").toLowerCase() if (section && sectionWidgetIndex >= 0) { @@ -29,19 +31,10 @@ Item { } // Use settings or defaults from BarWidgetRegistry - readonly property int userWidth: { - var section = barSection.replace("Section", "").toLowerCase() - if (section && sectionWidgetIndex >= 0) { - var widgets = Settings.data.bar.widgets[section] - if (widgets && sectionWidgetIndex < widgets.length) { - return widgets[sectionWidgetIndex].width || BarWidgetRegistry.widgetMetadata["Spacer"].width - } - } - return BarWidgetRegistry.widgetMetadata["Spacer"].width - } + readonly property int spacerWidth: widgetSettings.width !== undefined ? widgetSettings.width : widgetMetadata.width // Set the width based on user settings - implicitWidth: userWidth * scaling + implicitWidth: spacerWidth * scaling implicitHeight: Style.barHeight * scaling width: implicitWidth height: implicitHeight @@ -51,6 +44,6 @@ Item { anchors.fill: parent color: Qt.rgba(1, 0, 0, 0.1) // Very subtle red tint visible: Settings.data.general.debugMode || false - radius: 2 * scaling + radius: Style.radiusXXS * scaling } } diff --git a/Modules/Bar/Widgets/SystemMonitor.qml b/Modules/Bar/Widgets/SystemMonitor.qml index 49dceef..e9c4ad2 100644 --- a/Modules/Bar/Widgets/SystemMonitor.qml +++ b/Modules/Bar/Widgets/SystemMonitor.qml @@ -11,10 +11,13 @@ RowLayout { property ShellScreen screen property real scaling: 1.0 + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" property string barSection: "" property int sectionWidgetIndex: -1 property int sectionWidgetsCount: 0 + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] property var widgetSettings: { var section = barSection.replace("Section", "").toLowerCase() if (section && sectionWidgetIndex >= 0) { @@ -26,11 +29,13 @@ RowLayout { return {} } - readonly property bool userShowCpuUsage: (widgetSettings.showCpuUsage !== undefined) ? widgetSettings.showCpuUsage : BarWidgetRegistry.widgetMetadata["SystemMonitor"].showCpuUsage - readonly property bool userShowCpuTemp: (widgetSettings.showCpuTemp !== undefined) ? widgetSettings.showCpuTemp : BarWidgetRegistry.widgetMetadata["SystemMonitor"].showCpuTemp - readonly property bool userShowMemoryUsage: (widgetSettings.showMemoryUsage !== undefined) ? widgetSettings.showMemoryUsage : BarWidgetRegistry.widgetMetadata["SystemMonitor"].showMemoryUsage - readonly property bool userShowNetworkStats: (widgetSettings.showNetworkStats - !== undefined) ? widgetSettings.showNetworkStats : ((Settings.data.bar.showNetworkStats !== undefined) ? Settings.data.bar.showNetworkStats : BarWidgetRegistry.widgetMetadata["SystemMonitor"].showNetworkStats) + readonly property bool showCpuUsage: (widgetSettings.showCpuUsage + !== undefined) ? widgetSettings.showCpuUsage : widgetMetadata.showCpuUsage + readonly property bool showCpuTemp: (widgetSettings.showCpuTemp !== undefined) ? widgetSettings.showCpuTemp : widgetMetadata.showCpuTemp + readonly property bool showMemoryUsage: (widgetSettings.showMemoryUsage + !== undefined) ? widgetSettings.showMemoryUsage : widgetMetadata.showMemoryUsage + readonly property bool showNetworkStats: (widgetSettings.showNetworkStats + !== undefined) ? widgetSettings.showNetworkStats : widgetMetadata.showNetworkStats Layout.alignment: Qt.AlignVCenter spacing: Style.marginS * scaling @@ -55,7 +60,7 @@ RowLayout { id: cpuUsageLayout spacing: Style.marginXS * scaling Layout.alignment: Qt.AlignVCenter - visible: userShowCpuUsage + visible: showCpuUsage NIcon { id: cpuUsageIcon @@ -81,7 +86,7 @@ RowLayout { // spacing is thin here to compensate for the vertical thermometer icon spacing: Style.marginXXS * scaling Layout.alignment: Qt.AlignVCenter - visible: userShowCpuTemp + visible: showCpuTemp NIcon { text: "thermometer" @@ -104,7 +109,7 @@ RowLayout { id: memoryUsageLayout spacing: Style.marginXS * scaling Layout.alignment: Qt.AlignVCenter - visible: userShowMemoryUsage + visible: showMemoryUsage NIcon { text: "memory" @@ -127,7 +132,7 @@ RowLayout { id: networkDownloadLayout spacing: Style.marginXS * scaling Layout.alignment: Qt.AlignVCenter - visible: userShowNetworkStats + visible: showNetworkStats NIcon { text: "download" @@ -150,7 +155,7 @@ RowLayout { id: networkUploadLayout spacing: Style.marginXS * scaling Layout.alignment: Qt.AlignVCenter - visible: userShowNetworkStats + visible: showNetworkStats NIcon { text: "upload" diff --git a/Modules/Bar/Widgets/Tray.qml b/Modules/Bar/Widgets/Tray.qml index 06de40f..5c1b090 100644 --- a/Modules/Bar/Widgets/Tray.qml +++ b/Modules/Bar/Widgets/Tray.qml @@ -15,6 +15,7 @@ Rectangle { property ShellScreen screen property real scaling: 1.0 + readonly property real itemSize: 24 * scaling function onLoaded() { diff --git a/Modules/Bar/Widgets/Volume.qml b/Modules/Bar/Widgets/Volume.qml index 21035f8..80e79db 100644 --- a/Modules/Bar/Widgets/Volume.qml +++ b/Modules/Bar/Widgets/Volume.qml @@ -12,10 +12,14 @@ Item { property ShellScreen screen property real scaling: 1.0 + + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" property string barSection: "" - property int sectionWidgetIndex: 0 + property int sectionWidgetIndex: -1 property int sectionWidgetsCount: 0 + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] property var widgetSettings: { var section = barSection.replace("Section", "").toLowerCase() if (section && sectionWidgetIndex >= 0) { @@ -26,8 +30,9 @@ Item { } return {} } - readonly property bool userAlwaysShowPercentage: (widgetSettings.alwaysShowPercentage - !== undefined) ? widgetSettings.alwaysShowPercentage : ((Settings.data.bar.alwaysShowBatteryPercentage !== undefined) ? Settings.data.bar.alwaysShowBatteryPercentage : BarWidgetRegistry.widgetMetadata["Volume"].alwaysShowPercentage) + + readonly property bool alwaysShowPercentage: (widgetSettings.alwaysShowPercentage + !== undefined) ? widgetSettings.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage // Used to avoid opening the pill on Quickshell startup property bool firstVolumeReceived: false @@ -76,7 +81,7 @@ Item { collapsedIconColor: Color.mOnSurface autoHide: false // Important to be false so we can hover as long as we want text: Math.floor(AudioService.volume * 100) + "%" - forceOpen: userAlwaysShowPercentage + forceOpen: alwaysShowPercentage tooltipText: "Volume: " + Math.round(AudioService.volume * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume.\nRight click to toggle mute." diff --git a/Modules/Bar/Widgets/Workspace.qml b/Modules/Bar/Widgets/Workspace.qml index a9e8c1d..1a629dc 100644 --- a/Modules/Bar/Widgets/Workspace.qml +++ b/Modules/Bar/Widgets/Workspace.qml @@ -14,10 +14,13 @@ Item { property ShellScreen screen property real scaling: 1.0 + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" property string barSection: "" property int sectionWidgetIndex: -1 property int sectionWidgetsCount: 0 + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] property var widgetSettings: { var section = barSection.replace("Section", "").toLowerCase() if (section && sectionWidgetIndex >= 0) { @@ -29,7 +32,7 @@ Item { return {} } - readonly property string userLabelMode: (widgetSettings.labelMode !== undefined) ? widgetSettings.labelMode : ((Settings.data.bar.showWorkspaceLabel !== undefined) ? Settings.data.bar.showWorkspaceLabel : BarWidgetRegistry.widgetMetadata["Workspace"].labelMode) + readonly property string labelMode: (widgetSettings.labelMode !== undefined) ? widgetSettings.labelMode : widgetMetadata.labelMode property bool isDestroying: false property bool hovered: false @@ -162,7 +165,7 @@ Item { model: localWorkspaces Item { id: workspacePillContainer - height: (userLabelMode !== "none") ? Math.round(18 * scaling) : Math.round(14 * scaling) + height: (labelMode !== "none") ? Math.round(18 * scaling) : Math.round(14 * scaling) width: root.calculatedWsWidth(model) Rectangle { @@ -170,13 +173,13 @@ Item { anchors.fill: parent Loader { - active: (userLabelMode !== "none") + active: (labelMode !== "none") sourceComponent: Component { Text { x: (pill.width - width) / 2 y: (pill.height - height) / 2 + (height - contentHeight) / 2 text: { - if (userLabelMode === "name" && model.name && model.name.length > 0) { + if (labelMode === "name" && model.name && model.name.length > 0) { return model.name.substring(0, 2) } else { return model.idx.toString() From 5a1231a17e73a26208e8d12b3b5652a36ab080ec Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sun, 7 Sep 2025 22:55:28 -0400 Subject: [PATCH 36/54] Settings: completed migration of old settings on startup --- Commons/Settings.qml | 54 +++++++++++---- Modules/Bar/Widgets/ActiveWindow.qml | 11 ++-- .../Bar/WidgetSettings/ClockSettings.qml | 2 +- Services/BarWidgetRegistry.qml | 66 +++++++++---------- 4 files changed, 83 insertions(+), 50 deletions(-) diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 24e83d4..9c2268f 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -109,23 +109,53 @@ Singleton { continue } - _migrateWidget(widget) + migrateWidget(widget) Logger.log("Settings", JSON.stringify(widget)) } } } // ----------------------------------------------------- - function _migrateWidget(widget) { + function migrateWidget(widget) { Logger.log("Settings", `Migrating '${widget.id}' widget`) switch (widget.id) { + case "ActiveWindow": + widget.showIcon = adapter.bar.showActiveWindowIcon + break + case "Battery": + widget.alwaysShowPercentage = adapter.bar.alwaysShowBatteryPercentage + break + case "Brightness": + widget.alwaysShowPercentage = BarWidgetRegistry.widgetMetadata[widget.id].alwaysShowPercentage + break case "Clock": widget.showDate = adapter.location.showDateWithClock widget.use12HourClock = adapter.location.use12HourClock widget.reverseDayMonth = adapter.location.reverseDayMonth widget.showSeconds = BarWidgetRegistry.widgetMetadata[widget.id].showSeconds break + case "MediaMini": + widget.showAlbumArt = adapter.audio.showMiniplayerAlbumArt + widget.showVisualizer = adapter.audio.showMiniplayerCava + widget.visualizerType = BarWidgetRegistry.widgetMetadata[widget.id].visualizerType + break + case "NotificationHistory": + widget.showUnreadBadge = BarWidgetRegistry.widgetMetadata[widget.id].showUnreadBadge + widget.hideWhenZero = BarWidgetRegistry.widgetMetadata[widget.id].hideWhenZero + break + case "SidePanelToggle": + widget.useDistroLogo = adapter.bar.useDistroLogo + break + case "SystemMonitor": + widget.showNetworkStats = adapter.bar.showNetworkStats + break + case "Volume": + widget.alwaysShowPercentage = BarWidgetRegistry.widgetMetadata[widget.id].alwaysShowPercentage + break + case "Workspace": + widget.labelMode = adapter.bar.showWorkspaceLabel + break } } // ----------------------------------------------------- @@ -204,13 +234,14 @@ Singleton { // bar property JsonObject bar: JsonObject { property string position: "top" // "top" or "bottom" + property real backgroundOpacity: 1.0 + property list monitors: [] + property bool showActiveWindowIcon: true // TODO: delete property bool alwaysShowBatteryPercentage: false // TODO: delete property bool showNetworkStats: false // TODO: delete - property real backgroundOpacity: 1.0 property bool useDistroLogo: false // TODO: delete property string showWorkspaceLabel: "none" // TODO: delete - property list monitors: [] // Widget configuration for modular bar system property JsonObject widgets @@ -265,6 +296,7 @@ Singleton { property JsonObject location: JsonObject { property string name: defaultLocation property bool useFahrenheit: false + property bool reverseDayMonth: false // TODO: delete property bool use12HourClock: false // TODO: delete property bool showDateWithClock: false // TODO: delete @@ -334,21 +366,21 @@ Singleton { // audio property JsonObject audio: JsonObject { - property bool showMiniplayerAlbumArt: false // TODO: delete - property bool showMiniplayerCava: false // TODO: delete - property string visualizerType: "linear" property int volumeStep: 5 property int cavaFrameRate: 60 - // MPRIS controls + property string visualizerType: "linear" property list mprisBlacklist: [] property string preferredPlayer: "" + + property bool showMiniplayerAlbumArt: false // TODO: delete + property bool showMiniplayerCava: false // TODO: delete } // ui property JsonObject ui: JsonObject { - property string fontDefault: "Roboto" // Default font for all text - property string fontFixed: "DejaVu Sans Mono" // Fixed width font for terminal - property string fontBillboard: "Inter" // Large bold font for clocks and prominent displays + property string fontDefault: "Roboto" + property string fontFixed: "DejaVu Sans Mono" + property string fontBillboard: "Inter" property list monitorsScaling: [] property bool idleInhibitorEnabled: false } diff --git a/Modules/Bar/Widgets/ActiveWindow.qml b/Modules/Bar/Widgets/ActiveWindow.qml index d2d4578..8fb5961 100644 --- a/Modules/Bar/Widgets/ActiveWindow.qml +++ b/Modules/Bar/Widgets/ActiveWindow.qml @@ -12,11 +12,6 @@ RowLayout { id: root property ShellScreen screen property real scaling: 1.0 - readonly property real minWidth: 160 - readonly property real maxWidth: 400 - Layout.alignment: Qt.AlignVCenter - spacing: Style.marginS * scaling - visible: getTitle() !== "" // Widget properties passed from Bar.qml for per-instance settings property string widgetId: "" @@ -38,6 +33,12 @@ RowLayout { readonly property bool showIcon: (widgetSettings.showIcon !== undefined) ? widgetSettings.showIcon : widgetMetadata.showIcon + readonly property real minWidth: 160 + readonly property real maxWidth: 400 + Layout.alignment: Qt.AlignVCenter + spacing: Style.marginS * scaling + visible: getTitle() !== "" + function getTitle() { return CompositorService.focusedWindowTitle !== "(No active window)" ? CompositorService.focusedWindowTitle : "" } diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/ClockSettings.qml b/Modules/SettingsPanel/Bar/WidgetSettings/ClockSettings.qml index d285681..e701ed0 100644 --- a/Modules/SettingsPanel/Bar/WidgetSettings/ClockSettings.qml +++ b/Modules/SettingsPanel/Bar/WidgetSettings/ClockSettings.qml @@ -29,7 +29,7 @@ ColumnLayout { } NCheckbox { - label: "Show date next to time" + label: "Show date" checked: valueShowDate onToggled: checked => valueShowDate = checked } diff --git a/Services/BarWidgetRegistry.qml b/Services/BarWidgetRegistry.qml index 80a8ed9..294db65 100644 --- a/Services/BarWidgetRegistry.qml +++ b/Services/BarWidgetRegistry.qml @@ -38,17 +38,6 @@ Singleton { }) property var widgetMetadata: ({ - "CustomButton": { - "allowUserSettings": true, - "icon": "favorite", - "leftClickExec": "", - "rightClickExec": "", - "middleClickExec": "" - }, - "Spacer": { - "allowUserSettings": true, - "width": 20 - }, "ActiveWindow": { "allowUserSettings": true, "showIcon": true @@ -58,6 +47,37 @@ Singleton { "alwaysShowPercentage": false, "warningThreshold": 30 }, + "Brightness": { + "allowUserSettings": true, + "alwaysShowPercentage": false + }, + "Clock": { + "allowUserSettings": true, + "showDate": false, + "use12HourClock": false, + "showSeconds": false, + "reverseDayMonth": true + }, + "CustomButton": { + "allowUserSettings": true, + "icon": "favorite", + "leftClickExec": "", + "rightClickExec": "", + "middleClickExec": "" + }, + "Microphone": { + "allowUserSettings": true, + "alwaysShowPercentage": false + }, + "NotificationHistory": { + "allowUserSettings": true, + "showUnreadBadge": true, + "hideWhenZero": false + }, + "Spacer": { + "allowUserSettings": true, + "width": 20 + }, "SystemMonitor": { "allowUserSettings": true, "showCpuUsage": true, @@ -75,33 +95,13 @@ Singleton { "showVisualizer": false, "visualizerType": "linear" }, - "Clock": { - "allowUserSettings": true, - "showDate": false, - "use12HourClock": false, - "showSeconds": false, - "reverseDayMonth": true - }, - "Volume": { - "allowUserSettings": true, - "alwaysShowPercentage": false - }, - "Microphone": { - "allowUserSettings": true, - "alwaysShowPercentage": false - }, - "Brightness": { - "allowUserSettings": true, - "alwaysShowPercentage": false - }, "SidePanelToggle": { "allowUserSettings": true, "useDistroLogo": false }, - "NotificationHistory": { + "Volume": { "allowUserSettings": true, - "showUnreadBadge": true, - "hideWhenZero": false + "alwaysShowPercentage": false } }) From 91747c71f27e3da10a655c4c3d49dca19a7bd2c4 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sun, 7 Sep 2025 23:18:10 -0400 Subject: [PATCH 37/54] Main Settings: cleaned tabs since we removed many settings --- .../SettingsPanel/Bar/BarSectionEditor.qml | 2 +- Modules/SettingsPanel/Tabs/AudioTab.qml | 16 +- Modules/SettingsPanel/Tabs/BarTab.qml | 2 +- Modules/SettingsPanel/Tabs/ColorSchemeTab.qml | 315 +++++++++--------- Modules/SettingsPanel/Tabs/GeneralTab.qml | 109 +++--- Modules/SettingsPanel/Tabs/LauncherTab.qml | 32 +- 6 files changed, 240 insertions(+), 236 deletions(-) diff --git a/Modules/SettingsPanel/Bar/BarSectionEditor.qml b/Modules/SettingsPanel/Bar/BarSectionEditor.qml index 14cf45a..df1ed3e 100644 --- a/Modules/SettingsPanel/Bar/BarSectionEditor.qml +++ b/Modules/SettingsPanel/Bar/BarSectionEditor.qml @@ -65,7 +65,7 @@ NBox { text: sectionName + " Section" font.pointSize: Style.fontSizeL * scaling font.weight: Style.fontWeightBold - color: Color.mSecondary + color: Color.mOnSurface Layout.alignment: Qt.AlignVCenter } diff --git a/Modules/SettingsPanel/Tabs/AudioTab.qml b/Modules/SettingsPanel/Tabs/AudioTab.qml index a5dd9a1..d60ae38 100644 --- a/Modules/SettingsPanel/Tabs/AudioTab.qml +++ b/Modules/SettingsPanel/Tabs/AudioTab.qml @@ -242,21 +242,7 @@ ColumnLayout { Layout.bottomMargin: Style.marginS * scaling } - // Miniplayer section - NToggle { - label: "Show Album Art In Bar Media Player" - description: "Show the album art of the currently playing song next to the title." - checked: Settings.data.audio.showMiniplayerAlbumArt - onToggled: checked => Settings.data.audio.showMiniplayerAlbumArt = checked - } - - NToggle { - label: "Show Audio Visualizer In Bar Media Player" - description: "Shows an audio visualizer in the background of the miniplayer." - checked: Settings.data.audio.showMiniplayerCava - onToggled: checked => Settings.data.audio.showMiniplayerCava = checked - } - // Preferred player (persistent) + // Preferred player NTextInput { label: "Preferred Player" description: "Substring to match MPRIS player (identity/bus/desktop)." diff --git a/Modules/SettingsPanel/Tabs/BarTab.qml b/Modules/SettingsPanel/Tabs/BarTab.qml index 7180642..c543018 100644 --- a/Modules/SettingsPanel/Tabs/BarTab.qml +++ b/Modules/SettingsPanel/Tabs/BarTab.qml @@ -87,7 +87,7 @@ ColumnLayout { text: "Widgets Positioning" font.pointSize: Style.fontSizeXXL * scaling font.weight: Style.fontWeightBold - color: Color.mOnSurface + color: Color.mSecondary Layout.bottomMargin: Style.marginS * scaling } diff --git a/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml b/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml index 5196387..b564371 100644 --- a/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml +++ b/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml @@ -8,6 +8,7 @@ import qs.Widgets ColumnLayout { id: root + spacing: 0 // Cache for scheme JSON (can be flat or {dark, light}) property var schemeColorsCache: ({}) @@ -103,14 +104,8 @@ ColumnLayout { } } - ColumnLayout { - spacing: 0 - - Item { - Layout.fillWidth: true - Layout.preferredHeight: 0 - } + // Main Toggles - Dark Mode / Matugen ColumnLayout { spacing: Style.marginL * scaling Layout.fillWidth: true @@ -144,190 +139,194 @@ ColumnLayout { } } } + } - NDivider { - Layout.fillWidth: true - Layout.topMargin: Style.marginXL * scaling - Layout.bottomMargin: Style.marginXL * scaling + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginXL * scaling + Layout.bottomMargin: Style.marginXL * scaling + } + + // Predefined Color Schemes + ColumnLayout { + spacing: Style.marginM * scaling + Layout.fillWidth: true + + NText { + text: "Predefined Color Schemes" + font.pointSize: Style.fontSizeXXL * scaling + font.weight: Style.fontWeightBold + color: Color.mSecondary } - ColumnLayout { - spacing: Style.marginS * scaling + NText { + text: "To use these color schemes, you must turn off Matugen. With Matugen enabled, colors are automatically generated from your wallpaper." + font.pointSize: Style.fontSizeM * scaling + color: Color.mOnSurfaceVariant Layout.fillWidth: true + wrapMode: Text.WordWrap + } - NText { - text: "Predefined Color Schemes" - font.pointSize: Style.fontSizeXXL * scaling - font.weight: Style.fontWeightBold - color: Color.mSecondary - } + // Color Schemes Grid + GridLayout { + columns: 3 + rowSpacing: Style.marginM * scaling + columnSpacing: Style.marginM * scaling + Layout.fillWidth: true + + Repeater { + model: ColorSchemeService.schemes + + Rectangle { + id: schemeCard + + property string schemePath: modelData - NText { - text: "These color schemes are only active when 'Use Matugen' is turned off. With Matugen enabled, colors will be automatically generated from your wallpaper. You can still switch between light and dark themes while using Matugen." - font.pointSize: Style.fontSizeM * scaling - color: Color.mOnSurfaceVariant Layout.fillWidth: true - wrapMode: Text.WordWrap - } - } + Layout.preferredHeight: 120 * scaling + radius: Style.radiusM * scaling + color: getSchemeColor(modelData, "mSurface") + border.width: Math.max(1, Style.borderL * scaling) + border.color: (!Settings.data.colorSchemes.useWallpaperColors + && (Settings.data.colorSchemes.predefinedScheme === modelData)) ? Color.mPrimary : Color.mOutline + scale: root.cardScaleLow - // Color Schemes Grid - GridLayout { - columns: 3 - rowSpacing: Style.marginM * scaling - columnSpacing: Style.marginM * scaling - Layout.fillWidth: true + // Mouse area for selection + MouseArea { + anchors.fill: parent + onClicked: { + // Disable useWallpaperColors when picking a predefined color scheme + Settings.data.colorSchemes.useWallpaperColors = false + Logger.log("ColorSchemeTab", "Disabled matugen setting") - Repeater { - model: ColorSchemeService.schemes + Settings.data.colorSchemes.predefinedScheme = schemePath + ColorSchemeService.applyScheme(schemePath) + } + hoverEnabled: true + cursorShape: Qt.PointingHandCursor - Rectangle { - id: schemeCard - - property string schemePath: modelData - - Layout.fillWidth: true - Layout.preferredHeight: 120 * scaling - radius: Style.radiusM * scaling - color: getSchemeColor(modelData, "mSurface") - border.width: Math.max(1, Style.borderL * scaling) - border.color: (!Settings.data.colorSchemes.useWallpaperColors - && (Settings.data.colorSchemes.predefinedScheme === modelData)) ? Color.mPrimary : Color.mOutline - scale: root.cardScaleLow - - // Mouse area for selection - MouseArea { - anchors.fill: parent - onClicked: { - // Disable useWallpaperColors when picking a predefined color scheme - Settings.data.colorSchemes.useWallpaperColors = false - Logger.log("ColorSchemeTab", "Disabled matugen setting") - - Settings.data.colorSchemes.predefinedScheme = schemePath - ColorSchemeService.applyScheme(schemePath) - } - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - - onEntered: { - schemeCard.scale = root.cardScaleHigh - } - - onExited: { - schemeCard.scale = root.cardScaleLow - } + onEntered: { + schemeCard.scale = root.cardScaleHigh } - // Card content - ColumnLayout { - anchors.fill: parent - anchors.margins: Style.marginXL * scaling + onExited: { + schemeCard.scale = root.cardScaleLow + } + } + + // Card content + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginXL * scaling + spacing: Style.marginS * scaling + + // Scheme name + NText { + text: { + // Remove json and the full path + var chunks = schemePath.replace(".json", "").split("/") + return chunks[chunks.length - 1] + } + font.pointSize: Style.fontSizeM * scaling + font.weight: Style.fontWeightBold + color: getSchemeColor(modelData, "mOnSurface") + Layout.fillWidth: true + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter + } + + // Color swatches + RowLayout { + id: swatches + spacing: Style.marginS * scaling + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter - // Scheme name - NText { - text: { - // Remove json and the full path - var chunks = schemePath.replace(".json", "").split("/") - return chunks[chunks.length - 1] - } - font.pointSize: Style.fontSizeM * scaling - font.weight: Style.fontWeightBold - color: getSchemeColor(modelData, "mOnSurface") - Layout.fillWidth: true - elide: Text.ElideRight - horizontalAlignment: Text.AlignHCenter + readonly property int swatchSize: 20 * scaling + + // Primary color swatch + Rectangle { + width: swatches.swatchSize + height: swatches.swatchSize + radius: width * 0.5 + color: getSchemeColor(modelData, "mPrimary") } - // Color swatches - RowLayout { - id: swatches + // Secondary color swatch + Rectangle { + width: swatches.swatchSize + height: swatches.swatchSize + radius: width * 0.5 + color: getSchemeColor(modelData, "mSecondary") + } - spacing: Style.marginS * scaling - Layout.fillWidth: true - Layout.alignment: Qt.AlignHCenter + // Tertiary color swatch + Rectangle { + width: swatches.swatchSize + height: swatches.swatchSize + radius: width * 0.5 + color: getSchemeColor(modelData, "mTertiary") + } - readonly property int swatchSize: 20 * scaling - - // Primary color swatch - Rectangle { - width: swatches.swatchSize - height: swatches.swatchSize - radius: width * 0.5 - color: getSchemeColor(modelData, "mPrimary") - } - - // Secondary color swatch - Rectangle { - width: swatches.swatchSize - height: swatches.swatchSize - radius: width * 0.5 - color: getSchemeColor(modelData, "mSecondary") - } - - // Tertiary color swatch - Rectangle { - width: swatches.swatchSize - height: swatches.swatchSize - radius: width * 0.5 - color: getSchemeColor(modelData, "mTertiary") - } - - // Error color swatch - Rectangle { - width: swatches.swatchSize - height: swatches.swatchSize - radius: width * 0.5 - color: getSchemeColor(modelData, "mError") - } + // Error color swatch + Rectangle { + width: swatches.swatchSize + height: swatches.swatchSize + radius: width * 0.5 + color: getSchemeColor(modelData, "mError") } } + } - // Selection indicator (Checkmark) - Rectangle { - visible: !Settings.data.colorSchemes.useWallpaperColors - && (Settings.data.colorSchemes.predefinedScheme === schemePath) - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: Style.marginS * scaling - width: 24 * scaling - height: 24 * scaling - radius: width * 0.5 - color: Color.mPrimary + // Selection indicator (Checkmark) + Rectangle { + visible: !Settings.data.colorSchemes.useWallpaperColors + && (Settings.data.colorSchemes.predefinedScheme === schemePath) + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Style.marginS * scaling + width: 24 * scaling + height: 24 * scaling + radius: width * 0.5 + color: Color.mPrimary - NText { - anchors.centerIn: parent - text: "✓" - font.pointSize: Style.fontSizeXS * scaling - font.weight: Style.fontWeightBold - color: Color.mOnPrimary - } + NText { + anchors.centerIn: parent + text: "✓" + font.pointSize: Style.fontSizeXS * scaling + font.weight: Style.fontWeightBold + color: Color.mOnPrimary } + } - // Smooth animations - Behavior on scale { - NumberAnimation { - duration: Style.animationNormal - easing.type: Easing.OutCubic - } + // Smooth animations + Behavior on scale { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic } + } - Behavior on border.color { - ColorAnimation { - duration: Style.animationNormal - } + Behavior on border.color { + ColorAnimation { + duration: Style.animationNormal } + } - Behavior on border.width { - NumberAnimation { - duration: Style.animationFast - } + Behavior on border.width { + NumberAnimation { + duration: Style.animationFast } } } } } - } + } + + + + NDivider { Layout.fillWidth: true diff --git a/Modules/SettingsPanel/Tabs/GeneralTab.qml b/Modules/SettingsPanel/Tabs/GeneralTab.qml index f619eda..fe4dbcd 100644 --- a/Modules/SettingsPanel/Tabs/GeneralTab.qml +++ b/Modules/SettingsPanel/Tabs/GeneralTab.qml @@ -70,52 +70,6 @@ ColumnLayout { onToggled: checked => Settings.data.general.dimDesktop = checked } - NToggle { - label: "Auto-hide Dock" - description: "Automatically hide the dock when not in use." - checked: Settings.data.dock.autoHide - onToggled: checked => Settings.data.dock.autoHide = checked - } - - ColumnLayout { - spacing: Style.marginXXS * scaling - Layout.fillWidth: true - - NText { - text: "Dock Background Opacity" - font.pointSize: Style.fontSizeL * scaling - font.weight: Style.fontWeightBold - color: Color.mOnSurface - } - - NText { - text: "Adjust the background opacity of the dock." - font.pointSize: Style.fontSizeXS * scaling - color: Color.mOnSurfaceVariant - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - - RowLayout { - NSlider { - Layout.fillWidth: true - from: 0 - to: 1 - stepSize: 0.01 - value: Settings.data.dock.backgroundOpacity - onMoved: Settings.data.dock.backgroundOpacity = value - cutoutColor: Color.mSurface - } - - NText { - text: Math.floor(Settings.data.dock.backgroundOpacity * 100) + "%" - Layout.alignment: Qt.AlignVCenter - Layout.leftMargin: Style.marginS * scaling - color: Color.mOnSurface - } - } - } - ColumnLayout { spacing: Style.marginXXS * scaling Layout.fillWidth: true @@ -175,7 +129,70 @@ ColumnLayout { } } } + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginXL * scaling + Layout.bottomMargin: Style.marginXL * scaling + } + // Dock + ColumnLayout { + spacing: Style.marginL * scaling + Layout.fillWidth: true + NText { + text: "Dock" + font.pointSize: Style.fontSizeXXL * scaling + font.weight: Style.fontWeightBold + color: Color.mSecondary + Layout.bottomMargin: Style.marginS * scaling + } + + NToggle { + label: "Auto-hide Dock" + description: "Automatically hide the dock when not in use." + checked: Settings.data.dock.autoHide + onToggled: checked => Settings.data.dock.autoHide = checked + } + + ColumnLayout { + spacing: Style.marginXXS * scaling + Layout.fillWidth: true + + NText { + text: "Dock Background Opacity" + font.pointSize: Style.fontSizeL * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + NText { + text: "Adjust the background opacity of the dock." + font.pointSize: Style.fontSizeXS * scaling + color: Color.mOnSurfaceVariant + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + RowLayout { + NSlider { + Layout.fillWidth: true + from: 0 + to: 1 + stepSize: 0.01 + value: Settings.data.dock.backgroundOpacity + onMoved: Settings.data.dock.backgroundOpacity = value + cutoutColor: Color.mSurface + } + + NText { + text: Math.floor(Settings.data.dock.backgroundOpacity * 100) + "%" + Layout.alignment: Qt.AlignVCenter + Layout.leftMargin: Style.marginS * scaling + color: Color.mOnSurface + } + } + } + } NDivider { Layout.fillWidth: true Layout.topMargin: Style.marginXL * scaling diff --git a/Modules/SettingsPanel/Tabs/LauncherTab.qml b/Modules/SettingsPanel/Tabs/LauncherTab.qml index 6ca4ece..0b3a992 100644 --- a/Modules/SettingsPanel/Tabs/LauncherTab.qml +++ b/Modules/SettingsPanel/Tabs/LauncherTab.qml @@ -52,21 +52,7 @@ ColumnLayout { } } - NToggle { - label: "Enable Clipboard History" - description: "Show clipboard history in the launcher." - checked: Settings.data.appLauncher.enableClipboardHistory - onToggled: checked => Settings.data.appLauncher.enableClipboardHistory = checked - } - - NToggle { - label: "Use App2Unit for Launching" - description: "Use app2unit -- 'desktop-entry' when launching applications for better systemd integration." - checked: Settings.data.appLauncher.useApp2Unit - onToggled: checked => Settings.data.appLauncher.useApp2Unit = checked - } - - ColumnLayout { + ColumnLayout { spacing: Style.marginXXS * scaling Layout.fillWidth: true @@ -105,6 +91,22 @@ ColumnLayout { } } } + + NToggle { + label: "Enable Clipboard History" + description: "Show clipboard history in the launcher." + checked: Settings.data.appLauncher.enableClipboardHistory + onToggled: checked => Settings.data.appLauncher.enableClipboardHistory = checked + } + + NToggle { + label: "Use App2Unit for Launching" + description: "Use app2unit -- 'desktop-entry' when launching applications for better systemd integration." + checked: Settings.data.appLauncher.useApp2Unit + onToggled: checked => Settings.data.appLauncher.useApp2Unit = checked + } + + } NDivider { From d9c36a81c46d0add4bdd840f684de7268cc154a3 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sun, 7 Sep 2025 23:18:27 -0400 Subject: [PATCH 38/54] NightLight: fixed rightclick to open settings --- Modules/Bar/Widgets/NightLight.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/Bar/Widgets/NightLight.qml b/Modules/Bar/Widgets/NightLight.qml index 90c8540..c9f302e 100644 --- a/Modules/Bar/Widgets/NightLight.qml +++ b/Modules/Bar/Widgets/NightLight.qml @@ -4,6 +4,7 @@ import QtQuick.Controls import Quickshell import Quickshell.Wayland import qs.Commons +import qs.Modules.SettingsPanel import qs.Services import qs.Widgets @@ -25,7 +26,7 @@ NIconButton { onRightClicked: { var settingsPanel = PanelService.getPanel("settingsPanel") - settingsPanel.requestedTab = SettingsPanel.Tab.Display + settingsPanel.requestedTab = SettingsPanel.Tab.Brightness settingsPanel.open(screen) } } From 89c7f05782cf83f05391914eb33daa459b07eb65 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sun, 7 Sep 2025 23:45:13 -0400 Subject: [PATCH 39/54] NLabel: always full width even when there is no description --- Widgets/NLabel.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/Widgets/NLabel.qml b/Widgets/NLabel.qml index b9dc96e..853181a 100644 --- a/Widgets/NLabel.qml +++ b/Widgets/NLabel.qml @@ -20,6 +20,7 @@ ColumnLayout { font.capitalization: Font.Capitalize color: labelColor visible: label !== "" + Layout.fillWidth: true } NText { From 99d9dbe2186a33007e81e3104628195bc00ce1ed Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sun, 7 Sep 2025 23:51:09 -0400 Subject: [PATCH 40/54] WidgetSettings: replaced all checkboxes by the usual toggles. --- .../Bar/WidgetSettings/ActiveWindowSettings.qml | 2 +- .../SettingsPanel/Bar/WidgetSettings/BatterySettings.qml | 2 +- .../Bar/WidgetSettings/BrightnessSettings.qml | 2 +- .../SettingsPanel/Bar/WidgetSettings/ClockSettings.qml | 8 ++++---- .../Bar/WidgetSettings/MediaMiniSettings.qml | 4 ++-- .../Bar/WidgetSettings/MicrophoneSettings.qml | 2 +- .../Bar/WidgetSettings/NotificationHistorySettings.qml | 4 ++-- .../Bar/WidgetSettings/SidePanelToggleSettings.qml | 2 +- .../Bar/WidgetSettings/SystemMonitorSettings.qml | 8 ++++---- .../SettingsPanel/Bar/WidgetSettings/VolumeSettings.qml | 2 +- 10 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/ActiveWindowSettings.qml b/Modules/SettingsPanel/Bar/WidgetSettings/ActiveWindowSettings.qml index 9f5d8a6..eabf587 100644 --- a/Modules/SettingsPanel/Bar/WidgetSettings/ActiveWindowSettings.qml +++ b/Modules/SettingsPanel/Bar/WidgetSettings/ActiveWindowSettings.qml @@ -22,7 +22,7 @@ ColumnLayout { return settings } - NCheckbox { + NToggle { id: showIcon Layout.fillWidth: true label: "Show app icon" diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/BatterySettings.qml b/Modules/SettingsPanel/Bar/WidgetSettings/BatterySettings.qml index f907204..54b589a 100644 --- a/Modules/SettingsPanel/Bar/WidgetSettings/BatterySettings.qml +++ b/Modules/SettingsPanel/Bar/WidgetSettings/BatterySettings.qml @@ -23,7 +23,7 @@ ColumnLayout { return settings } - NCheckbox { + NToggle { label: "Always show percentage" checked: root.valueAlwaysShowPercentage onToggled: checked => root.valueAlwaysShowPercentage = checked diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/BrightnessSettings.qml b/Modules/SettingsPanel/Bar/WidgetSettings/BrightnessSettings.qml index 840c33c..6054e9c 100644 --- a/Modules/SettingsPanel/Bar/WidgetSettings/BrightnessSettings.qml +++ b/Modules/SettingsPanel/Bar/WidgetSettings/BrightnessSettings.qml @@ -23,7 +23,7 @@ ColumnLayout { return settings } - NCheckbox { + NToggle { label: "Always show percentage" checked: valueAlwaysShowPercentage onToggled: checked => valueAlwaysShowPercentage = checked diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/ClockSettings.qml b/Modules/SettingsPanel/Bar/WidgetSettings/ClockSettings.qml index e701ed0..cef94a8 100644 --- a/Modules/SettingsPanel/Bar/WidgetSettings/ClockSettings.qml +++ b/Modules/SettingsPanel/Bar/WidgetSettings/ClockSettings.qml @@ -28,25 +28,25 @@ ColumnLayout { return settings } - NCheckbox { + NToggle { label: "Show date" checked: valueShowDate onToggled: checked => valueShowDate = checked } - NCheckbox { + NToggle { label: "Use 12-hour clock" checked: valueUse12h onToggled: checked => valueUse12h = checked } - NCheckbox { + NToggle { label: "Show seconds" checked: valueShowSeconds onToggled: checked => valueShowSeconds = checked } - NCheckbox { + NToggle { label: "Reverse day and month" checked: valueReverseDayMonth onToggled: checked => valueReverseDayMonth = checked diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/MediaMiniSettings.qml b/Modules/SettingsPanel/Bar/WidgetSettings/MediaMiniSettings.qml index d1af307..fb70f9d 100644 --- a/Modules/SettingsPanel/Bar/WidgetSettings/MediaMiniSettings.qml +++ b/Modules/SettingsPanel/Bar/WidgetSettings/MediaMiniSettings.qml @@ -26,13 +26,13 @@ ColumnLayout { return settings } - NCheckbox { + NToggle { label: "Show album art" checked: valueShowAlbumArt onToggled: checked => valueShowAlbumArt = checked } - NCheckbox { + NToggle { label: "Show visualizer" checked: valueShowVisualizer onToggled: checked => valueShowVisualizer = checked diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/MicrophoneSettings.qml b/Modules/SettingsPanel/Bar/WidgetSettings/MicrophoneSettings.qml index 840c33c..6054e9c 100644 --- a/Modules/SettingsPanel/Bar/WidgetSettings/MicrophoneSettings.qml +++ b/Modules/SettingsPanel/Bar/WidgetSettings/MicrophoneSettings.qml @@ -23,7 +23,7 @@ ColumnLayout { return settings } - NCheckbox { + NToggle { label: "Always show percentage" checked: valueAlwaysShowPercentage onToggled: checked => valueAlwaysShowPercentage = checked diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/NotificationHistorySettings.qml b/Modules/SettingsPanel/Bar/WidgetSettings/NotificationHistorySettings.qml index 64b1c56..751a832 100644 --- a/Modules/SettingsPanel/Bar/WidgetSettings/NotificationHistorySettings.qml +++ b/Modules/SettingsPanel/Bar/WidgetSettings/NotificationHistorySettings.qml @@ -24,13 +24,13 @@ ColumnLayout { return settings } - NCheckbox { + NToggle { label: "Show unread badge" checked: valueShowUnreadBadge onToggled: checked => valueShowUnreadBadge = checked } - NCheckbox { + NToggle { label: "Hide badge when zero" checked: valueHideWhenZero onToggled: checked => valueHideWhenZero = checked diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/SidePanelToggleSettings.qml b/Modules/SettingsPanel/Bar/WidgetSettings/SidePanelToggleSettings.qml index 7161f3d..abb2b7e 100644 --- a/Modules/SettingsPanel/Bar/WidgetSettings/SidePanelToggleSettings.qml +++ b/Modules/SettingsPanel/Bar/WidgetSettings/SidePanelToggleSettings.qml @@ -22,7 +22,7 @@ ColumnLayout { return settings } - NCheckbox { + NToggle { label: "Use distro logo instead of icon" checked: valueUseDistroLogo onToggled: checked => valueUseDistroLogo = checked diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/SystemMonitorSettings.qml b/Modules/SettingsPanel/Bar/WidgetSettings/SystemMonitorSettings.qml index 3c3bbf9..171e0ce 100644 --- a/Modules/SettingsPanel/Bar/WidgetSettings/SystemMonitorSettings.qml +++ b/Modules/SettingsPanel/Bar/WidgetSettings/SystemMonitorSettings.qml @@ -29,7 +29,7 @@ ColumnLayout { return settings } - NCheckbox { + NToggle { id: showCpuUsage Layout.fillWidth: true label: "CPU usage" @@ -37,7 +37,7 @@ ColumnLayout { onToggled: checked => valueShowCpuUsage = checked } - NCheckbox { + NToggle { id: showCpuTemp Layout.fillWidth: true label: "CPU temperature" @@ -45,7 +45,7 @@ ColumnLayout { onToggled: checked => valueShowCpuTemp = checked } - NCheckbox { + NToggle { id: showMemoryUsage Layout.fillWidth: true label: "Memory usage" @@ -53,7 +53,7 @@ ColumnLayout { onToggled: checked => valueShowMemoryUsage = checked } - NCheckbox { + NToggle { id: showNetworkStats Layout.fillWidth: true label: "Network traffic" diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/VolumeSettings.qml b/Modules/SettingsPanel/Bar/WidgetSettings/VolumeSettings.qml index 840c33c..6054e9c 100644 --- a/Modules/SettingsPanel/Bar/WidgetSettings/VolumeSettings.qml +++ b/Modules/SettingsPanel/Bar/WidgetSettings/VolumeSettings.qml @@ -23,7 +23,7 @@ ColumnLayout { return settings } - NCheckbox { + NToggle { label: "Always show percentage" checked: valueAlwaysShowPercentage onToggled: checked => valueAlwaysShowPercentage = checked From dda0266798f892908d8b87dbb3953c6ab61e1a30 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sun, 7 Sep 2025 23:51:31 -0400 Subject: [PATCH 41/54] Autoformatting --- Modules/SettingsPanel/Tabs/ColorSchemeTab.qml | 115 +++++++++--------- Modules/SettingsPanel/Tabs/LauncherTab.qml | 4 +- 2 files changed, 56 insertions(+), 63 deletions(-) diff --git a/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml b/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml index b564371..de5ba2c 100644 --- a/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml +++ b/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml @@ -104,70 +104,69 @@ ColumnLayout { } } + // Main Toggles - Dark Mode / Matugen + ColumnLayout { + spacing: Style.marginL * scaling + Layout.fillWidth: true - // Main Toggles - Dark Mode / Matugen - ColumnLayout { - spacing: Style.marginL * scaling - Layout.fillWidth: true + // Dark Mode Toggle (affects both Matugen and predefined schemes that provide variants) + NToggle { + label: "Dark Mode" + description: Settings.data.colorSchemes.useWallpaperColors ? "Generate dark theme colors when using Matugen." : "Use a dark variant if available." + checked: Settings.data.colorSchemes.darkMode + enabled: true + onToggled: checked => Settings.data.colorSchemes.darkMode = checked + } - // Dark Mode Toggle (affects both Matugen and predefined schemes that provide variants) - NToggle { - label: "Dark Mode" - description: Settings.data.colorSchemes.useWallpaperColors ? "Generate dark theme colors when using Matugen." : "Use a dark variant if available." - checked: Settings.data.colorSchemes.darkMode - enabled: true - onToggled: checked => Settings.data.colorSchemes.darkMode = checked - } + // Use Matugen + NToggle { + label: "Enable Matugen" + description: "Automatically generate colors based on your active wallpaper." + checked: Settings.data.colorSchemes.useWallpaperColors + onToggled: checked => { + if (checked) { + // Check if matugen is installed + matugenCheck.running = true + } else { + Settings.data.colorSchemes.useWallpaperColors = false + ToastService.showNotice("Matugen", "Disabled") - // Use Matugen - NToggle { - label: "Enable Matugen" - description: "Automatically generate colors based on your active wallpaper." - checked: Settings.data.colorSchemes.useWallpaperColors - onToggled: checked => { - if (checked) { - // Check if matugen is installed - matugenCheck.running = true - } else { - Settings.data.colorSchemes.useWallpaperColors = false - ToastService.showNotice("Matugen", "Disabled") + if (Settings.data.colorSchemes.predefinedScheme) { - if (Settings.data.colorSchemes.predefinedScheme) { - - ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme) - } + ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme) } } - } + } + } + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginXL * scaling + Layout.bottomMargin: Style.marginXL * scaling + } + + // Predefined Color Schemes + ColumnLayout { + spacing: Style.marginM * scaling + Layout.fillWidth: true + + NText { + text: "Predefined Color Schemes" + font.pointSize: Style.fontSizeXXL * scaling + font.weight: Style.fontWeightBold + color: Color.mSecondary } - NDivider { + NText { + text: "To use these color schemes, you must turn off Matugen. With Matugen enabled, colors are automatically generated from your wallpaper." + font.pointSize: Style.fontSizeM * scaling + color: Color.mOnSurfaceVariant Layout.fillWidth: true - Layout.topMargin: Style.marginXL * scaling - Layout.bottomMargin: Style.marginXL * scaling + wrapMode: Text.WordWrap } - // Predefined Color Schemes - ColumnLayout { - spacing: Style.marginM * scaling - Layout.fillWidth: true - - NText { - text: "Predefined Color Schemes" - font.pointSize: Style.fontSizeXXL * scaling - font.weight: Style.fontWeightBold - color: Color.mSecondary - } - - NText { - text: "To use these color schemes, you must turn off Matugen. With Matugen enabled, colors are automatically generated from your wallpaper." - font.pointSize: Style.fontSizeM * scaling - color: Color.mOnSurfaceVariant - Layout.fillWidth: true - wrapMode: Text.WordWrap - } - - // Color Schemes Grid + // Color Schemes Grid GridLayout { columns: 3 rowSpacing: Style.marginM * scaling @@ -188,7 +187,7 @@ ColumnLayout { color: getSchemeColor(modelData, "mSurface") border.width: Math.max(1, Style.borderL * scaling) border.color: (!Settings.data.colorSchemes.useWallpaperColors - && (Settings.data.colorSchemes.predefinedScheme === modelData)) ? Color.mPrimary : Color.mOutline + && (Settings.data.colorSchemes.predefinedScheme === modelData)) ? Color.mPrimary : Color.mOutline scale: root.cardScaleLow // Mouse area for selection @@ -282,7 +281,7 @@ ColumnLayout { // Selection indicator (Checkmark) Rectangle { visible: !Settings.data.colorSchemes.useWallpaperColors - && (Settings.data.colorSchemes.predefinedScheme === schemePath) + && (Settings.data.colorSchemes.predefinedScheme === schemePath) anchors.right: parent.right anchors.top: parent.top anchors.margins: Style.marginS * scaling @@ -322,11 +321,7 @@ ColumnLayout { } } } - } - - - - + } NDivider { Layout.fillWidth: true diff --git a/Modules/SettingsPanel/Tabs/LauncherTab.qml b/Modules/SettingsPanel/Tabs/LauncherTab.qml index 0b3a992..28bb9f0 100644 --- a/Modules/SettingsPanel/Tabs/LauncherTab.qml +++ b/Modules/SettingsPanel/Tabs/LauncherTab.qml @@ -52,7 +52,7 @@ ColumnLayout { } } - ColumnLayout { + ColumnLayout { spacing: Style.marginXXS * scaling Layout.fillWidth: true @@ -105,8 +105,6 @@ ColumnLayout { checked: Settings.data.appLauncher.useApp2Unit onToggled: checked => Settings.data.appLauncher.useApp2Unit = checked } - - } NDivider { From 74ec5ea6065a29b3cf39ffd0fb280816a3ebe46b Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Sun, 7 Sep 2025 23:59:22 -0400 Subject: [PATCH 42/54] Cava: running at all time as its getting to know if a widget needs it. --- Services/CavaService.qml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Services/CavaService.qml b/Services/CavaService.qml index 6cfb735..12f39e7 100644 --- a/Services/CavaService.qml +++ b/Services/CavaService.qml @@ -37,9 +37,7 @@ Singleton { Process { id: process stdinEnabled: true - running: (Settings.data.audio.visualizerType !== "none") - && (PanelService.getPanel("sidePanel").active || Settings.data.audio.showMiniplayerCava - || (PanelService.lockScreen && PanelService.lockScreen.active)) + running: true command: ["cava", "-p", "/dev/stdin"] onExited: { stdinEnabled = true From 4d7bc811c445d95986550dfce63a6d1124f64761 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Mon, 8 Sep 2025 00:02:15 -0400 Subject: [PATCH 43/54] Widget Settings: load settings before triggering the loader to avoid async loading. --- .../Bar/BarWidgetSettingsDialog.qml | 62 +++++++++++-------- 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/Modules/SettingsPanel/Bar/BarWidgetSettingsDialog.qml b/Modules/SettingsPanel/Bar/BarWidgetSettingsDialog.qml index ef38ae6..9ba0045 100644 --- a/Modules/SettingsPanel/Bar/BarWidgetSettingsDialog.qml +++ b/Modules/SettingsPanel/Bar/BarWidgetSettingsDialog.qml @@ -32,6 +32,40 @@ Popup { border.width: Style.borderM * scaling } + // Load settings when popup opens with data + onOpened: { + if (widgetData && widgetId) { + loadWidgetSettings() + } + } + + function loadWidgetSettings() { + const widgetSettingsMap = { + "ActiveWindow": "WidgetSettings/ActiveWindowSettings.qml", + "Battery": "WidgetSettings/BatterySettings.qml", + "Brightness": "WidgetSettings/BrightnessSettings.qml", + "Clock": "WidgetSettings/ClockSettings.qml", + "CustomButton": "WidgetSettings/CustomButtonSettings.qml", + "MediaMini": "WidgetSettings/MediaMiniSettings.qml", + "Microphone": "WidgetSettings/MicrophoneSettings.qml", + "NotificationHistory": "WidgetSettings/NotificationHistorySettings.qml", + "Workspace": "WidgetSettings/WorkspaceSettings.qml", + "SidePanelToggle": "WidgetSettings/SidePanelToggleSettings.qml", + "Spacer": "WidgetSettings/SpacerSettings.qml", + "SystemMonitor": "WidgetSettings/SystemMonitorSettings.qml", + "Volume": "WidgetSettings/VolumeSettings.qml" + } + + const source = widgetSettingsMap[widgetId] + if (source) { + // Use setSource to pass properties at creation time + settingsLoader.setSource(source, { + "widgetData": widgetData, + "widgetMetadata": BarWidgetRegistry.widgetMetadata[widgetId] + }) + } + } + ColumnLayout { id: content width: parent.width @@ -63,36 +97,10 @@ Popup { } // Settings based on widget type + // Will be triggered via settingsLoader.setSource() Loader { id: settingsLoader Layout.fillWidth: true - - source: { - const widgetSettingsMap = { - "ActiveWindow": "WidgetSettings/ActiveWindowSettings.qml", - "Battery": "WidgetSettings/BatterySettings.qml", - "Brightness": "WidgetSettings/BrightnessSettings.qml", - "Clock": "WidgetSettings/ClockSettings.qml", - "CustomButton": "WidgetSettings/CustomButtonSettings.qml", - "MediaMini": "WidgetSettings/MediaMiniSettings.qml", - "Microphone": "WidgetSettings/MicrophoneSettings.qml", - "NotificationHistory": "WidgetSettings/NotificationHistorySettings.qml", - "Workspace": "WidgetSettings/WorkspaceSettings.qml", - "SidePanelToggle": "WidgetSettings/SidePanelToggleSettings.qml", - "Spacer": "WidgetSettings/SpacerSettings.qml", - "SystemMonitor": "WidgetSettings/SystemMonitorSettings.qml", - "Volume": "WidgetSettings/VolumeSettings.qml" - } - return widgetSettingsMap[settingsPopup.widgetId] || "" - } - - onLoaded: { - if (item) { - // Pass data to the loaded component - item.widgetData = settingsPopup.widgetData - item.widgetMetadata = BarWidgetRegistry.widgetMetadata[settingsPopup.widgetId] - } - } } // Action buttons From b3eea2215d391e4452c98a44bb6824a5e8ad34f8 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Mon, 8 Sep 2025 00:05:58 -0400 Subject: [PATCH 44/54] Bar Add Widget: taller NComboBox --- Modules/SettingsPanel/Bar/BarSectionEditor.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/SettingsPanel/Bar/BarSectionEditor.qml b/Modules/SettingsPanel/Bar/BarSectionEditor.qml index df1ed3e..7a1684a 100644 --- a/Modules/SettingsPanel/Bar/BarSectionEditor.qml +++ b/Modules/SettingsPanel/Bar/BarSectionEditor.qml @@ -79,7 +79,7 @@ NBox { description: "" placeholder: "Select a widget to add..." onSelected: key => comboBox.currentKey = key - popupHeight: 240 * scaling + popupHeight: 340 * scaling Layout.alignment: Qt.AlignVCenter } From 8bfde2f6d8dfe5206d52763085429cfd209cceb3 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Mon, 8 Sep 2025 00:39:07 -0400 Subject: [PATCH 45/54] NPill: fixed, finally! --- Widgets/NPill.qml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Widgets/NPill.qml b/Widgets/NPill.qml index 8a1dd63..0ac11b2 100644 --- a/Widgets/NPill.qml +++ b/Widgets/NPill.qml @@ -68,10 +68,12 @@ Item { NText { id: textItem anchors.verticalCenter: parent.verticalCenter - anchors.left: rightOpen ? parent.left : undefined - anchors.right: rightOpen ? undefined : parent.right - anchors.leftMargin: rightOpen ? iconSize * 0.8 : 0 - anchors.rightMargin: rightOpen ? 0 : iconSize * 0.8 + x: { + // Little tweak to have a better text horizontal centering + var centerX = (parent.width - width) / 2 + var offset = rightOpen ? Style.marginXS * scaling : -Style.marginXS * scaling + return centerX + offset + } text: root.text font.pointSize: Style.fontSizeXS * scaling font.weight: Style.fontWeightBold From d4f6462e8a179629cfba35d91945b1d6c671b275 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Mon, 8 Sep 2025 00:40:12 -0400 Subject: [PATCH 46/54] Battery: deactivated test mode --- Modules/Bar/Widgets/Battery.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Bar/Widgets/Battery.qml b/Modules/Bar/Widgets/Battery.qml index 060d3e1..9b8aef5 100644 --- a/Modules/Bar/Widgets/Battery.qml +++ b/Modules/Bar/Widgets/Battery.qml @@ -37,7 +37,7 @@ Item { !== undefined ? widgetSettings.warningThreshold : widgetMetadata.warningThreshold // Test mode - readonly property bool testMode: true + readonly property bool testMode: false readonly property int testPercent: 50 readonly property bool testCharging: true From 993a7965fdd630694d6ae92aa26e21787e51c0d5 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Mon, 8 Sep 2025 01:00:38 -0400 Subject: [PATCH 47/54] NPill: fixed look at high scaling --- Widgets/NPill.qml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Widgets/NPill.qml b/Widgets/NPill.qml index 0ac11b2..2011c9d 100644 --- a/Widgets/NPill.qml +++ b/Widgets/NPill.qml @@ -39,9 +39,9 @@ Item { property bool shouldAnimateHide: false // Exposed width logic - readonly property int pillHeight: Style.baseWidgetSize * sizeRatio * scaling - readonly property int iconSize: Style.baseWidgetSize * sizeRatio * scaling - readonly property int pillPaddingHorizontal: Style.marginM * scaling + readonly property int iconSize: Math.round(Style.baseWidgetSize * sizeRatio * scaling) + readonly property int pillHeight: iconSize + readonly property int pillPaddingHorizontal: Style.marginS * scaling readonly property int pillOverlap: iconSize * 0.5 readonly property int maxPillWidth: Math.max(1, textItem.implicitWidth + pillPaddingHorizontal * 2 + pillOverlap) @@ -108,8 +108,7 @@ Item { border.width: Math.max(1, Style.borderS * scaling) border.color: forceOpen ? Qt.alpha(Color.mOutline, 0.5) : Color.transparent - anchors.left: rightOpen ? parent.left : undefined - anchors.right: rightOpen ? undefined : parent.right + x: rightOpen ? 0 : (parent.width - width) Behavior on color { ColorAnimation { From f9e55c8f8d525d604cffc76867e3bf9220583b13 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Mon, 8 Sep 2025 01:03:58 -0400 Subject: [PATCH 48/54] Workspace: removed extra transparent padding around. --- Modules/Bar/Widgets/Workspace.qml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/Bar/Widgets/Workspace.qml b/Modules/Bar/Widgets/Workspace.qml index 1a629dc..c51c469 100644 --- a/Modules/Bar/Widgets/Workspace.qml +++ b/Modules/Bar/Widgets/Workspace.qml @@ -42,8 +42,8 @@ Item { property bool effectsActive: false property color effectColor: Color.mPrimary - property int horizontalPadding: Math.round(16 * scaling) - property int spacingBetweenPills: Math.round(8 * scaling) + property int horizontalPadding: Math.round(Style.marginS * scaling) + property int spacingBetweenPills: Math.round(Style.marginXS * scaling) signal workspaceChanged(int workspaceId, color accentColor) @@ -144,7 +144,7 @@ Item { Rectangle { id: workspaceBackground - width: parent.width - Style.marginS * scaling * 2 + width: parent.width height: Math.round(Style.capsuleHeight * scaling) radius: Math.round(Style.radiusM * scaling) From 849f3c52d7680807ba05fe55da3baf4152510e54 Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Mon, 8 Sep 2025 01:10:48 -0400 Subject: [PATCH 49/54] Notifications badge: hidden by default --- Services/BarWidgetRegistry.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Services/BarWidgetRegistry.qml b/Services/BarWidgetRegistry.qml index 294db65..6bd4395 100644 --- a/Services/BarWidgetRegistry.qml +++ b/Services/BarWidgetRegistry.qml @@ -72,7 +72,7 @@ Singleton { "NotificationHistory": { "allowUserSettings": true, "showUnreadBadge": true, - "hideWhenZero": false + "hideWhenZero": true }, "Spacer": { "allowUserSettings": true, From 38928abab79cb2cc76c1a01ee2e3758f40df0221 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Mon, 8 Sep 2025 07:51:49 +0200 Subject: [PATCH 50/54] Fix first start noctalia settings & color creation --- Commons/Color.qml | 9 ++++++++- Commons/Settings.qml | 28 +++++++++++++++++----------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/Commons/Color.qml b/Commons/Color.qml index d7636a0..fa2912e 100644 --- a/Commons/Color.qml +++ b/Commons/Color.qml @@ -102,7 +102,7 @@ Singleton { // FileView to load custom colors data from colors.json FileView { id: customColorsFile - path: Settings.configDir + "colors.json" + path: Settings.directoriesCreated ? (Settings.configDir + "colors.json") : "" watchChanges: true onFileChanged: { Logger.log("Color", "Reloading colors from disk") @@ -112,6 +112,13 @@ Singleton { Logger.log("Color", "Writing colors to disk") writeAdapter() } + + // Trigger initial load when path changes from empty to actual path + onPathChanged: { + if (path === Settings.configDir + "colors.json") { + reload() + } + } onLoadFailed: function (error) { if (error.toString().includes("No such file") || error === 2) { // File doesn't exist, create it with default values diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 9c2268f..ef38b47 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -31,6 +31,7 @@ Singleton { readonly property alias data: adapter property bool isLoaded: false + property bool directoriesCreated: false // Signal emitted when settings are loaded after startupcale changes signal settingsLoaded @@ -176,14 +177,15 @@ Singleton { } // ----------------------------------------------------- - Item { - Component.onCompleted: { - - // ensure settings dir exists - Quickshell.execDetached(["mkdir", "-p", configDir]) - Quickshell.execDetached(["mkdir", "-p", cacheDir]) - Quickshell.execDetached(["mkdir", "-p", cacheDirImages]) - } + // Ensure directories exist before FileView tries to read files + Component.onCompleted: { + // ensure settings dir exists + Quickshell.execDetached(["mkdir", "-p", configDir]) + Quickshell.execDetached(["mkdir", "-p", cacheDir]) + Quickshell.execDetached(["mkdir", "-p", cacheDirImages]) + + // Mark directories as created and trigger file loading + directoriesCreated = true } // Don't write settings to disk immediately @@ -197,12 +199,16 @@ Singleton { FileView { id: settingsFileView - path: settingsFile + path: directoriesCreated ? settingsFile : "" watchChanges: true onFileChanged: reload() onAdapterUpdated: saveTimer.start() - Component.onCompleted: function () { - reload() + + // Trigger initial load when path changes from empty to actual path + onPathChanged: { + if (path === settingsFile) { + reload() + } } onLoaded: function () { if (!isLoaded) { From b6166a2a7cfbfe1cda317e51f38837e114705123 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Mon, 8 Sep 2025 08:04:18 +0200 Subject: [PATCH 51/54] SystemMonitor: add % support for RAM usage --- Commons/Color.qml | 2 +- Commons/Settings.qml | 4 ++-- Modules/Bar/Widgets/SystemMonitor.qml | 4 +++- Modules/Bar/Widgets/Workspace.qml | 2 +- .../Bar/WidgetSettings/SystemMonitorSettings.qml | 11 +++++++++++ Services/BarWidgetRegistry.qml | 1 + 6 files changed, 19 insertions(+), 5 deletions(-) diff --git a/Commons/Color.qml b/Commons/Color.qml index fa2912e..7abc21f 100644 --- a/Commons/Color.qml +++ b/Commons/Color.qml @@ -112,7 +112,7 @@ Singleton { Logger.log("Color", "Writing colors to disk") writeAdapter() } - + // Trigger initial load when path changes from empty to actual path onPathChanged: { if (path === Settings.configDir + "colors.json") { diff --git a/Commons/Settings.qml b/Commons/Settings.qml index ef38b47..da0da40 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -183,7 +183,7 @@ Singleton { Quickshell.execDetached(["mkdir", "-p", configDir]) Quickshell.execDetached(["mkdir", "-p", cacheDir]) Quickshell.execDetached(["mkdir", "-p", cacheDirImages]) - + // Mark directories as created and trigger file loading directoriesCreated = true } @@ -203,7 +203,7 @@ Singleton { watchChanges: true onFileChanged: reload() onAdapterUpdated: saveTimer.start() - + // Trigger initial load when path changes from empty to actual path onPathChanged: { if (path === settingsFile) { diff --git a/Modules/Bar/Widgets/SystemMonitor.qml b/Modules/Bar/Widgets/SystemMonitor.qml index e9c4ad2..91f3fd8 100644 --- a/Modules/Bar/Widgets/SystemMonitor.qml +++ b/Modules/Bar/Widgets/SystemMonitor.qml @@ -34,6 +34,8 @@ RowLayout { readonly property bool showCpuTemp: (widgetSettings.showCpuTemp !== undefined) ? widgetSettings.showCpuTemp : widgetMetadata.showCpuTemp readonly property bool showMemoryUsage: (widgetSettings.showMemoryUsage !== undefined) ? widgetSettings.showMemoryUsage : widgetMetadata.showMemoryUsage + readonly property bool showMemoryAsPercent: (widgetSettings.showMemoryAsPercent + !== undefined) ? widgetSettings.showMemoryAsPercent : widgetMetadata.showMemoryAsPercent readonly property bool showNetworkStats: (widgetSettings.showNetworkStats !== undefined) ? widgetSettings.showNetworkStats : widgetMetadata.showNetworkStats @@ -117,7 +119,7 @@ RowLayout { } NText { - text: `${SystemStatService.memGb}G` + text: showMemoryAsPercent ? `${SystemStatService.memPercent}%` : `${SystemStatService.memGb}G` font.family: Settings.data.ui.fontFixed font.pointSize: Style.fontSizeS * scaling font.weight: Style.fontWeightMedium diff --git a/Modules/Bar/Widgets/Workspace.qml b/Modules/Bar/Widgets/Workspace.qml index c51c469..d8bb543 100644 --- a/Modules/Bar/Widgets/Workspace.qml +++ b/Modules/Bar/Widgets/Workspace.qml @@ -43,7 +43,7 @@ Item { property color effectColor: Color.mPrimary property int horizontalPadding: Math.round(Style.marginS * scaling) - property int spacingBetweenPills: Math.round(Style.marginXS * scaling) + property int spacingBetweenPills: Math.round(Style.marginXS * scaling) signal workspaceChanged(int workspaceId, color accentColor) diff --git a/Modules/SettingsPanel/Bar/WidgetSettings/SystemMonitorSettings.qml b/Modules/SettingsPanel/Bar/WidgetSettings/SystemMonitorSettings.qml index 171e0ce..4f2459b 100644 --- a/Modules/SettingsPanel/Bar/WidgetSettings/SystemMonitorSettings.qml +++ b/Modules/SettingsPanel/Bar/WidgetSettings/SystemMonitorSettings.qml @@ -17,6 +17,8 @@ ColumnLayout { property bool valueShowCpuUsage: widgetData.showCpuUsage !== undefined ? widgetData.showCpuUsage : widgetMetadata.showCpuUsage property bool valueShowCpuTemp: widgetData.showCpuTemp !== undefined ? widgetData.showCpuTemp : widgetMetadata.showCpuTemp property bool valueShowMemoryUsage: widgetData.showMemoryUsage !== undefined ? widgetData.showMemoryUsage : widgetMetadata.showMemoryUsage + property bool valueShowMemoryAsPercent: widgetData.showMemoryAsPercent + !== undefined ? widgetData.showMemoryAsPercent : widgetMetadata.showMemoryAsPercent property bool valueShowNetworkStats: widgetData.showNetworkStats !== undefined ? widgetData.showNetworkStats : widgetMetadata.showNetworkStats @@ -25,6 +27,7 @@ ColumnLayout { settings.showCpuUsage = valueShowCpuUsage settings.showCpuTemp = valueShowCpuTemp settings.showMemoryUsage = valueShowMemoryUsage + settings.showMemoryAsPercent = valueShowMemoryAsPercent settings.showNetworkStats = valueShowNetworkStats return settings } @@ -53,6 +56,14 @@ ColumnLayout { onToggled: checked => valueShowMemoryUsage = checked } + NToggle { + id: showMemoryAsPercent + Layout.fillWidth: true + label: "Show memory as percentage" + checked: valueShowMemoryAsPercent + onToggled: checked => valueShowMemoryAsPercent = checked + } + NToggle { id: showNetworkStats Layout.fillWidth: true diff --git a/Services/BarWidgetRegistry.qml b/Services/BarWidgetRegistry.qml index 6bd4395..b82a3e1 100644 --- a/Services/BarWidgetRegistry.qml +++ b/Services/BarWidgetRegistry.qml @@ -83,6 +83,7 @@ Singleton { "showCpuUsage": true, "showCpuTemp": true, "showMemoryUsage": true, + "showMemoryAsPercent": false, "showNetworkStats": false }, "Workspace": { From c0900b105b6b97305a5020c2b11a5dabab885453 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Mon, 8 Sep 2025 08:46:10 +0200 Subject: [PATCH 52/54] Background: add default wallpaper --- Assets/Wallpaper/noctalia.png | Bin 0 -> 89091 bytes Commons/Settings.qml | 2 ++ Modules/Background/Background.qml | 13 +++++++++++++ Services/WallpaperService.qml | 6 +++++- 4 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 Assets/Wallpaper/noctalia.png diff --git a/Assets/Wallpaper/noctalia.png b/Assets/Wallpaper/noctalia.png new file mode 100644 index 0000000000000000000000000000000000000000..a9b41a23c24f87ef12636fd966b085454f36d409 GIT binary patch literal 89091 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

~h3alcU1TmLpk-ApO?E-x77hV+>7| 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 a&$h}HD;1SUsE_hJz{Jqfp!k&YjsF3T>P)c! literal 0 HcmV?d00001 diff --git a/Commons/Settings.qml b/Commons/Settings.qml index da0da40..3072636 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -26,6 +26,7 @@ Singleton { property string defaultWallpapersDirectory: Quickshell.env("HOME") + "/Pictures/Wallpapers" property string defaultVideosDirectory: Quickshell.env("HOME") + "/Videos" property string defaultLocation: "Tokyo" + property string defaultWallpaper: Quickshell.shellDir + "/Assets/Wallpaper/noctalia.png" // Used to access via Settings.data.xxx.yyy readonly property alias data: adapter @@ -334,6 +335,7 @@ Singleton { property int transitionDuration: 1500 // 1500 ms property string transitionType: "random" property real transitionEdgeSmoothness: 0.05 + property string defaultWallpaper: root.defaultWallpaper property list monitors: [] } diff --git a/Modules/Background/Background.qml b/Modules/Background/Background.qml index a1eca8b..3730838 100644 --- a/Modules/Background/Background.qml +++ b/Modules/Background/Background.qml @@ -3,6 +3,8 @@ import Quickshell import Quickshell.Wayland import qs.Commons import qs.Services +import qs.Modules.SettingsPanel +import qs.Widgets Variants { id: backgroundVariants @@ -20,6 +22,8 @@ Variants { // Internal state management property string transitionType: "fade" property real transitionProgress: 0 + // Scaling support for widgets that rely on it + property real scaling: ScalingService.getScreenScale(screen) readonly property real edgeSmoothness: Settings.data.wallpaper.transitionEdgeSmoothness readonly property var allTransitions: WallpaperService.allTransitions @@ -87,6 +91,15 @@ Variants { left: true } + Connections { + target: ScalingService + function onScaleChanged(screenName, scale) { + if ((screen !== null) && (screenName === screen.name)) { + scaling = scale + } + } + } + Timer { id: debounceTimer interval: 333 diff --git a/Services/WallpaperService.qml b/Services/WallpaperService.qml index 7f99a0d..311bdae 100644 --- a/Services/WallpaperService.qml +++ b/Services/WallpaperService.qml @@ -216,7 +216,11 @@ Singleton { // ------------------------------------------------------------------- // Get specific monitor wallpaper - now from cache function getWallpaper(screenName) { - return currentWallpapers[screenName] || "" + var path = currentWallpapers[screenName] || "" + if (path === "") { + return Settings.data.wallpaper.defaultWallpaper || "" + } + return path } // ------------------------------------------------------------------- From 983e3c5cbebbbf2c4c1a03eaadede65d0918111c Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Mon, 8 Sep 2025 12:28:35 +0200 Subject: [PATCH 53/54] Release v2.7.0 Network: Even more improvements SysStat: Remove bash script Notification: Pore image support NotificationHistory: Proper unread count Settings: Migrate Bar widgets to new settings BarWidgets: Easier to access, edit Background: add default wallpaper (if none is set) SystemMonitor: add % support for RAM BarTab: - remove global settings for widgets - add settings button per bar widget, this makes it possible to have separate settings of the same kind with different settings. This also makes it way easier to configure. A decent amount of QoL changes & fixes --- Services/UpdateService.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Services/UpdateService.qml b/Services/UpdateService.qml index c4ec7d2..1b8f3f1 100644 --- a/Services/UpdateService.qml +++ b/Services/UpdateService.qml @@ -8,8 +8,8 @@ Singleton { id: root // Public properties - property string baseVersion: "2.6.0" - property bool isDevelopment: true + property string baseVersion: "2.7.0" + property bool isDevelopment: false property string currentVersion: `v${!isDevelopment ? baseVersion : baseVersion + "-dev"}` From 66a4618d091f27797511feb68804bd4bc9f65c8b Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Mon, 8 Sep 2025 12:33:17 +0200 Subject: [PATCH 54/54] switch to dev version --- Services/UpdateService.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Services/UpdateService.qml b/Services/UpdateService.qml index 1b8f3f1..f2d3207 100644 --- a/Services/UpdateService.qml +++ b/Services/UpdateService.qml @@ -9,7 +9,7 @@ Singleton { // Public properties property string baseVersion: "2.7.0" - property bool isDevelopment: false + property bool isDevelopment: true property string currentVersion: `v${!isDevelopment ? baseVersion : baseVersion + "-dev"}`