diff --git a/Assets/ColorScheme/Niri.json b/Assets/ColorScheme/Niri.json new file mode 100644 index 0000000..b752de8 --- /dev/null +++ b/Assets/ColorScheme/Niri.json @@ -0,0 +1,35 @@ +{ + "dark": { + "mPrimary": "#7fccff", + "mOnPrimary": "#000000", + "mSecondary": "#4c4c4c", + "mOnSecondary": "#ffffff", + "mTertiary": "#2c2c2c", + "mOnTertiary": "#ffffff", + "mError": "#ff4c4c", + "mOnError": "#ffffff", + "mSurface": "#191919", + "mOnSurface": "#ffffff", + "mSurfaceVariant": "#191919", + "mOnSurfaceVariant": "#ffffff", + "mOutline": "#4c4c4c", + "mShadow": "#000000" + }, + "light": { + "mPrimary": "#7fccff", + "mOnPrimary": "#000000", + "mSecondary": "#4c4c4c", + "mOnSecondary": "#ffffff", + "mTertiary": "#2c2c2c", + "mOnTertiary": "#ffffff", + "mError": "#ff4c4c", + "mOnError": "#ffffff", + "mSurface": "#191919", + "mOnSurface": "#ffffff", + "mSurfaceVariant": "#191919", + "mOnSurfaceVariant": "#ffffff", + "mOutline": "#4c4c4c", + "mShadow": "#000000" + } +} + diff --git a/Assets/Wallpaper/noctalia.png b/Assets/Wallpaper/noctalia.png new file mode 100644 index 0000000..a9b41a2 Binary files /dev/null and b/Assets/Wallpaper/noctalia.png differ 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/Commons/Color.qml b/Commons/Color.qml index d7636a0..7abc21f 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 097f5e9..3072636 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -26,11 +26,13 @@ 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 property bool isLoaded: false + property bool directoriesCreated: false // Signal emitted when settings are loaded after startupcale changes signal settingsLoaded @@ -71,34 +73,93 @@ 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 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") { + 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 "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 + } + } // ----------------------------------------------------- // Kickoff essential services function kickOffServices() { @@ -117,14 +178,15 @@ Singleton { } // ----------------------------------------------------- - Item { - Component.onCompleted: { + // 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]) - // 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 @@ -138,12 +200,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) { @@ -174,15 +240,16 @@ 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 real backgroundOpacity: 1.0 - property bool useDistroLogo: false - property string showWorkspaceLabel: "none" property list monitors: [] + property bool showActiveWindowIcon: true // TODO: delete + property bool alwaysShowBatteryPercentage: false // TODO: delete + property bool showNetworkStats: false // TODO: delete + property bool useDistroLogo: false // TODO: delete + property string showWorkspaceLabel: "none" // TODO: delete + // Widget configuration for modular bar system property JsonObject widgets widgets: JsonObject { @@ -236,9 +303,10 @@ 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 @@ -267,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: [] } @@ -299,25 +368,27 @@ Singleton { property JsonObject notifications: JsonObject { property bool doNotDisturb: false property list monitors: [] + // Last time the user opened the notification history (ms since epoch) + property real lastSeenTs: 0 } // audio property JsonObject audio: JsonObject { - property bool showMiniplayerAlbumArt: false - property bool showMiniplayerCava: false - 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/Commons/Style.qml b/Commons/Style.qml index 902a225..6517b8e 100644 --- a/Commons/Style.qml +++ b/Commons/Style.qml @@ -36,8 +36,8 @@ Singleton { property int radiusL: 20 * Settings.data.general.radiusRatio // Border - property int borderS: 1 - property int borderM: 2 + property int borderS: 3 + property int borderM: 3 property int borderL: 3 // Margins (for margins and spacing) @@ -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/Commons/Time.qml b/Commons/Time.qml index d7ec78e..c086f5b 100644 --- a/Commons/Time.qml +++ b/Commons/Time.qml @@ -9,52 +9,38 @@ 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 + // Returns a Unix Timestamp (in seconds) + readonly property int timestamp: { + return Math.floor(date / 1000) } - readonly property string dateString: { + + 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/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/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 65f900e..8fb5961 100644 --- a/Modules/Bar/Widgets/ActiveWindow.qml +++ b/Modules/Bar/Widgets/ActiveWindow.qml @@ -12,6 +12,27 @@ RowLayout { id: root 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) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex] + } + } + return {} + } + + 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 @@ -74,7 +95,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() !== "" && showIcon IconImage { id: windowIcon diff --git a/Modules/Bar/Widgets/Battery.qml b/Modules/Bar/Widgets/Battery.qml index 22d8602..9b8aef5 100644 --- a/Modules/Bar/Widgets/Battery.qml +++ b/Modules/Bar/Widgets/Battery.qml @@ -11,11 +11,42 @@ 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 - // Track if we've already notified to avoid spam + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] + 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 : widgetMetadata.alwaysShowPercentage + readonly property real warningThreshold: widgetSettings.warningThreshold + !== undefined ? widgetSettings.warningThreshold : widgetMetadata.warningThreshold + + // Test mode + readonly property bool testMode: false + 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 +54,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 +70,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,76 +84,44 @@ Item { NPill { id: pill - // Test mode - property bool testMode: false - property int testPercent: 20 - property bool testCharging: false - 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 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 = [] 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/Bar/Widgets/Brightness.qml b/Modules/Bar/Widgets/Brightness.qml index 4b6d91a..30948c3 100644 --- a/Modules/Bar/Widgets/Brightness.qml +++ b/Modules/Bar/Widgets/Brightness.qml @@ -10,10 +10,28 @@ 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) { + 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 : widgetMetadata.alwaysShowPercentage + // Used to avoid opening the pill on Quickshell startup property bool firstBrightnessReceived: false @@ -37,28 +55,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 @@ -71,6 +87,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..3b472d9 100644 --- a/Modules/Bar/Widgets/Clock.qml +++ b/Modules/Bar/Widgets/Clock.qml @@ -10,24 +10,70 @@ Rectangle { 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) { + 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 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) radius: Math.round(Style.radiusM * scaling) color: Color.mSurfaceVariant // Clock Icon with attached calendar - NClock { + NText { id: clock - anchors.verticalCenter: parent.verticalCenter - anchors.horizontalCenter: parent.horizontalCenter + 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/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 2483dbc..141698a 100644 --- a/Modules/Bar/Widgets/MediaMini.qml +++ b/Modules/Bar/Widgets/MediaMini.qml @@ -12,18 +12,44 @@ 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) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex] + } + } + return {} + } + + 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 - function getTitle() { - return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "") - } - // A hidden text element to safely measure the full title width NText { id: fullTitleMetrics @@ -58,8 +84,7 @@ RowLayout { Loader { anchors.verticalCenter: parent.verticalCenter anchors.horizontalCenter: parent.horizontalCenter - active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "linear" - && MediaService.isPlaying + active: showVisualizer && visualizerType == "linear" && MediaService.isPlaying z: 0 sourceComponent: LinearSpectrum { @@ -74,8 +99,7 @@ RowLayout { Loader { anchors.verticalCenter: parent.verticalCenter anchors.horizontalCenter: parent.horizontalCenter - active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "mirrored" - && MediaService.isPlaying + active: showVisualizer && visualizerType == "mirrored" && MediaService.isPlaying z: 0 sourceComponent: MirroredSpectrum { @@ -90,8 +114,7 @@ RowLayout { Loader { anchors.verticalCenter: parent.verticalCenter anchors.horizontalCenter: parent.horizontalCenter - active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "wave" - && MediaService.isPlaying + active: showVisualizer && visualizerType == "wave" && MediaService.isPlaying z: 0 sourceComponent: WaveSpectrum { @@ -115,12 +138,12 @@ RowLayout { font.pointSize: Style.fontSizeL * scaling verticalAlignment: Text.AlignVCenter Layout.alignment: Qt.AlignVCenter - visible: !Settings.data.audio.showMiniplayerAlbumArt && getTitle() !== "" && !trackArt.visible + visible: !showAlbumArt && getTitle() !== "" && !trackArt.visible } ColumnLayout { Layout.alignment: Qt.AlignVCenter - visible: Settings.data.audio.showMiniplayerAlbumArt + visible: showAlbumArt spacing: 0 Item { diff --git a/Modules/Bar/Widgets/Microphone.qml b/Modules/Bar/Widgets/Microphone.qml index f4e1c1a..15f4437 100644 --- a/Modules/Bar/Widgets/Microphone.qml +++ b/Modules/Bar/Widgets/Microphone.qml @@ -12,10 +12,28 @@ 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) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex] + } + } + return {} + } + + readonly property bool alwaysShowPercentage: (widgetSettings.alwaysShowPercentage + !== undefined) ? widgetSettings.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage + // Used to avoid opening the pill on Quickshell startup property bool firstInputVolumeReceived: false property int wheelAccumulator: 0 @@ -78,6 +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: 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..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 @@ -14,7 +15,6 @@ NIconButton { property real scaling: 1.0 sizeRatio: 0.8 - colorBg: Color.mSurfaceVariant colorFg: Color.mOnSurface colorBorder: Color.transparent @@ -26,7 +26,7 @@ NIconButton { onRightClicked: { var settingsPanel = PanelService.getPanel("settingsPanel") - settingsPanel.requestedTab = SettingsPanel.Tab.Display + settingsPanel.requestedTab = SettingsPanel.Tab.Brightness settingsPanel.open(screen) } } diff --git a/Modules/Bar/Widgets/NotificationHistory.qml b/Modules/Bar/Widgets/NotificationHistory.qml index 48a62fd..31657f1 100644 --- a/Modules/Bar/Widgets/NotificationHistory.qml +++ b/Modules/Bar/Widgets/NotificationHistory.qml @@ -13,6 +13,45 @@ 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) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex] + } + } + return {} + } + 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 + } + + 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'." @@ -21,7 +60,40 @@ NIconButton { colorBorder: Color.transparent colorBorderHover: Color.transparent - onClicked: PanelService.getPanel("notificationHistoryPanel")?.toggle(screen, this) + onClicked: { + var panel = PanelService.getPanel("notificationHistoryPanel") + panel?.toggle(screen, this) + Settings.data.notifications.lastSeenTs = Time.timestamp * 1000 + } 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: showUnreadBadge && (!hideWhenZero || computeUnreadCount() > 0) + sourceComponent: Rectangle { + 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 + width: Math.max(height, textNode.implicitWidth + pad) + radius: height / 2 + color: Color.mError + border.color: Color.mSurface + border.width: 1 + visible: count > 0 || !hideWhenZero + NText { + id: textNode + anchors.centerIn: parent + text: badge.label + font.pointSize: Style.fontSizeXXS * scaling + color: Color.mOnError + } + } + } } diff --git a/Modules/Bar/Widgets/SidePanelToggle.qml b/Modules/Bar/Widgets/SidePanelToggle.qml index b9572fb..14a8c6f 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 @@ -11,7 +12,28 @@ NIconButton { property ShellScreen screen property real scaling: 1.0 - icon: Settings.data.bar.useDistroLogo ? "" : "widgets" + // 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) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex] + } + } + return {} + } + + readonly property bool useDistroLogo: (widgetSettings.useDistroLogo + !== undefined) ? widgetSettings.useDistroLogo : widgetMetadata.useDistroLogo + + icon: useDistroLogo ? "" : "widgets" tooltipText: "Open side panel." sizeRatio: 0.8 @@ -24,14 +46,13 @@ 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 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: 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 6c2346c..91f3fd8 100644 --- a/Modules/Bar/Widgets/SystemMonitor.qml +++ b/Modules/Bar/Widgets/SystemMonitor.qml @@ -11,6 +11,34 @@ 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) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex] + } + } + return {} + } + + 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 showMemoryAsPercent: (widgetSettings.showMemoryAsPercent + !== undefined) ? widgetSettings.showMemoryAsPercent : widgetMetadata.showMemoryAsPercent + readonly property bool showNetworkStats: (widgetSettings.showNetworkStats + !== undefined) ? widgetSettings.showNetworkStats : widgetMetadata.showNetworkStats + Layout.alignment: Qt.AlignVCenter spacing: Style.marginS * scaling @@ -34,6 +62,7 @@ RowLayout { id: cpuUsageLayout spacing: Style.marginXS * scaling Layout.alignment: Qt.AlignVCenter + visible: showCpuUsage NIcon { id: cpuUsageIcon @@ -59,6 +88,7 @@ RowLayout { // spacing is thin here to compensate for the vertical thermometer icon spacing: Style.marginXXS * scaling Layout.alignment: Qt.AlignVCenter + visible: showCpuTemp NIcon { text: "thermometer" @@ -81,6 +111,7 @@ RowLayout { id: memoryUsageLayout spacing: Style.marginXS * scaling Layout.alignment: Qt.AlignVCenter + visible: showMemoryUsage NIcon { text: "memory" @@ -88,7 +119,7 @@ RowLayout { } NText { - text: `${SystemStatService.memoryUsageGb}G` + text: showMemoryAsPercent ? `${SystemStatService.memPercent}%` : `${SystemStatService.memGb}G` font.family: Settings.data.ui.fontFixed font.pointSize: Style.fontSizeS * scaling font.weight: Style.fontWeightMedium @@ -103,7 +134,7 @@ RowLayout { id: networkDownloadLayout spacing: Style.marginXS * scaling Layout.alignment: Qt.AlignVCenter - visible: Settings.data.bar.showNetworkStats + visible: showNetworkStats NIcon { text: "download" @@ -126,7 +157,7 @@ RowLayout { id: networkUploadLayout spacing: Style.marginXS * scaling Layout.alignment: Qt.AlignVCenter - visible: Settings.data.bar.showNetworkStats + 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 84f8b22..80e79db 100644 --- a/Modules/Bar/Widgets/Volume.qml +++ b/Modules/Bar/Widgets/Volume.qml @@ -12,10 +12,28 @@ 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) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex] + } + } + return {} + } + + readonly property bool alwaysShowPercentage: (widgetSettings.alwaysShowPercentage + !== undefined) ? widgetSettings.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage + // Used to avoid opening the pill on Quickshell startup property bool firstVolumeReceived: false property int wheelAccumulator: 0 @@ -63,6 +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: 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/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/Modules/Bar/Widgets/Workspace.qml b/Modules/Bar/Widgets/Workspace.qml index 051bdea..d8bb543 100644 --- a/Modules/Bar/Widgets/Workspace.qml +++ b/Modules/Bar/Widgets/Workspace.qml @@ -14,6 +14,26 @@ 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) { + var widgets = Settings.data.bar.widgets[section] + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex] + } + } + return {} + } + + readonly property string labelMode: (widgetSettings.labelMode !== undefined) ? widgetSettings.labelMode : widgetMetadata.labelMode + property bool isDestroying: false property bool hovered: false @@ -22,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) @@ -124,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) @@ -145,7 +165,7 @@ Item { model: localWorkspaces Item { id: workspacePillContainer - height: (Settings.data.bar.showWorkspaceLabel !== "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 { @@ -153,15 +173,13 @@ Item { anchors.fill: parent Loader { - active: (Settings.data.bar.showWorkspaceLabel !== "none") + active: (labelMode !== "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 (labelMode === "name" && model.name && model.name.length > 0) { return model.name.substring(0, 2) } else { return model.idx.toString() diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml index 9a71bba..04d64f7 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,15 +191,9 @@ Variants { Item { id: dock width: dockLayout.implicitWidth - height: parent.height - (20 * scaling) + 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 "" @@ -203,39 +209,48 @@ 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 + // Individual tooltip for this app + NTooltip { + id: appTooltip + target: appButton + positionAbove: true + visible: false + } + + // 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 +261,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 } } } @@ -270,7 +285,6 @@ Variants { anyAppHovered = true const appName = appButton.appTitle || appButton.appId || "Unknown" appTooltip.text = appName.length > 40 ? appName.substring(0, 37) + "..." : appName - appTooltip.target = appButton appTooltip.isVisible = true if (autoHide) { showTimer.stop() @@ -300,15 +314,32 @@ 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 + } + } } } } 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/Modules/SettingsPanel/Extras/BarSectionEditor.qml b/Modules/SettingsPanel/Bar/BarSectionEditor.qml similarity index 90% rename from Modules/SettingsPanel/Extras/BarSectionEditor.qml rename to Modules/SettingsPanel/Bar/BarSectionEditor.qml index fb17c34..7a1684a 100644 --- a/Modules/SettingsPanel/Extras/BarSectionEditor.qml +++ b/Modules/SettingsPanel/Bar/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) } } @@ -75,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 } @@ -89,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 } @@ -188,13 +178,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()) + } + }) + } } } } @@ -221,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 diff --git a/Modules/SettingsPanel/Bar/BarWidgetSettingsDialog.qml b/Modules/SettingsPanel/Bar/BarWidgetSettingsDialog.qml new file mode 100644 index 0000000..9ba0045 --- /dev/null +++ b/Modules/SettingsPanel/Bar/BarWidgetSettingsDialog.qml @@ -0,0 +1,134 @@ +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 + } + + // 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 + 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 + // Will be triggered via settingsLoader.setSource() + Loader { + id: settingsLoader + Layout.fillWidth: true + } + + // 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..eabf587 --- /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 + } + + NToggle { + 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..54b589a --- /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 + } + + 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 new file mode 100644 index 0000000..6054e9c --- /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 + } + + 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 new file mode 100644 index 0000000..cef94a8 --- /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 + } + + NToggle { + label: "Show date" + checked: valueShowDate + onToggled: checked => valueShowDate = checked + } + + NToggle { + label: "Use 12-hour clock" + checked: valueUse12h + onToggled: checked => valueUse12h = checked + } + + NToggle { + label: "Show seconds" + checked: valueShowSeconds + onToggled: checked => valueShowSeconds = checked + } + + NToggle { + 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..fb70f9d --- /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 + } + + NToggle { + label: "Show album art" + checked: valueShowAlbumArt + onToggled: checked => valueShowAlbumArt = checked + } + + NToggle { + 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..6054e9c --- /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 + } + + 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 new file mode 100644 index 0000000..751a832 --- /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 + } + + NToggle { + label: "Show unread badge" + checked: valueShowUnreadBadge + onToggled: checked => valueShowUnreadBadge = checked + } + + 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 new file mode 100644 index 0000000..abb2b7e --- /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 + } + + NToggle { + 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..4f2459b --- /dev/null +++ b/Modules/SettingsPanel/Bar/WidgetSettings/SystemMonitorSettings.qml @@ -0,0 +1,74 @@ +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 valueShowMemoryAsPercent: widgetData.showMemoryAsPercent + !== undefined ? widgetData.showMemoryAsPercent : widgetMetadata.showMemoryAsPercent + 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.showMemoryAsPercent = valueShowMemoryAsPercent + settings.showNetworkStats = valueShowNetworkStats + return settings + } + + NToggle { + id: showCpuUsage + Layout.fillWidth: true + label: "CPU usage" + checked: valueShowCpuUsage + onToggled: checked => valueShowCpuUsage = checked + } + + NToggle { + id: showCpuTemp + Layout.fillWidth: true + label: "CPU temperature" + checked: valueShowCpuTemp + onToggled: checked => valueShowCpuTemp = checked + } + + NToggle { + id: showMemoryUsage + Layout.fillWidth: true + label: "Memory usage" + checked: valueShowMemoryUsage + 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 + 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..6054e9c --- /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 + } + + NToggle { + 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 f9aa98d..0000000 --- a/Modules/SettingsPanel/Extras/BarWidgetSettingsDialog.qml +++ /dev/null @@ -1,186 +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 - } - // 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: "Save" - onClicked: { - if (settingsLoader.item && settingsLoader.item.saveSettings) { - var newSettings = settingsLoader.item.saveSettings() - root.updateWidgetSettings(sectionId, settingsPopup.widgetIndex, newSettings) - settingsPopup.close() - } - } - } - } - } - - // 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" - } - } - } -} diff --git a/Modules/SettingsPanel/SettingsPanel.qml b/Modules/SettingsPanel/SettingsPanel.qml index 1e6d6cc..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", @@ -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 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 e1bf63a..c543018 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,57 +70,6 @@ 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 - } - - 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 { @@ -138,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/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/ColorSchemeTab.qml b/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml index 5196387..de5ba2c 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,225 +104,218 @@ ColumnLayout { } } + // Main Toggles - Dark Mode / Matugen ColumnLayout { - spacing: 0 + spacing: Style.marginL * scaling + Layout.fillWidth: true - Item { - Layout.fillWidth: true - Layout.preferredHeight: 0 + // 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 } - ColumnLayout { - spacing: Style.marginL * scaling - Layout.fillWidth: true + // 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") - // 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 - } + if (Settings.data.colorSchemes.predefinedScheme) { - // 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) { - - 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 - } + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginXL * scaling + Layout.bottomMargin: Style.marginXL * scaling + } - ColumnLayout { - spacing: Style.marginS * scaling - Layout.fillWidth: true + // 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: "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 + 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 } } } diff --git a/Modules/SettingsPanel/Tabs/GeneralTab.qml b/Modules/SettingsPanel/Tabs/GeneralTab.qml index 7407112..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 @@ -206,6 +223,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 +236,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 +249,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 } diff --git a/Modules/SettingsPanel/Tabs/LauncherTab.qml b/Modules/SettingsPanel/Tabs/LauncherTab.qml index 6ca4ece..28bb9f0 100644 --- a/Modules/SettingsPanel/Tabs/LauncherTab.qml +++ b/Modules/SettingsPanel/Tabs/LauncherTab.qml @@ -52,20 +52,6 @@ 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 { spacing: Style.marginXXS * scaling Layout.fillWidth: true @@ -105,6 +91,20 @@ 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 { diff --git a/Modules/SettingsPanel/Tabs/TimeWeatherTab.qml b/Modules/SettingsPanel/Tabs/WeatherTab.qml similarity index 61% rename from Modules/SettingsPanel/Tabs/TimeWeatherTab.qml rename to Modules/SettingsPanel/Tabs/WeatherTab.qml index 2fa89dd..667aca5 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 @@ -111,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 - } } 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/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 } } } 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/BarWidgetRegistry.qml b/Services/BarWidgetRegistry.qml index 39afcd3..b82a3e1 100644 --- a/Services/BarWidgetRegistry.qml +++ b/Services/BarWidgetRegistry.qml @@ -38,6 +38,26 @@ Singleton { }) property var widgetMetadata: ({ + "ActiveWindow": { + "allowUserSettings": true, + "showIcon": true + }, + "Battery": { + "allowUserSettings": true, + "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", @@ -45,10 +65,44 @@ Singleton { "rightClickExec": "", "middleClickExec": "" }, + "Microphone": { + "allowUserSettings": true, + "alwaysShowPercentage": false + }, + "NotificationHistory": { + "allowUserSettings": true, + "showUnreadBadge": true, + "hideWhenZero": true + }, "Spacer": { "allowUserSettings": true, - "icon": "space_bar", "width": 20 + }, + "SystemMonitor": { + "allowUserSettings": true, + "showCpuUsage": true, + "showCpuTemp": true, + "showMemoryUsage": true, + "showMemoryAsPercent": false, + "showNetworkStats": false + }, + "Workspace": { + "allowUserSettings": true, + "labelMode": "index" + }, + "MediaMini": { + "allowUserSettings": true, + "showAlbumArt": false, + "showVisualizer": false, + "visualizerType": "linear" + }, + "SidePanelToggle": { + "allowUserSettings": true, + "useDistroLogo": false + }, + "Volume": { + "allowUserSettings": true, + "alwaysShowPercentage": false } }) 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/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 } 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 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/NetworkService.qml b/Services/NetworkService.qml index c4b5820..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 @@ -54,7 +57,7 @@ Singleton { Component.onCompleted: { Logger.log("Network", "Service initialized") syncWifiState() - refresh() + scan() } // Save cache with debounce @@ -75,6 +78,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 @@ -82,26 +95,26 @@ Singleton { function setWifiEnabled(enabled) { Settings.data.network.wifiEnabled = enabled - - wifiToggleProcess.action = enabled ? "on" : "off" - wifiToggleProcess.running = true - } - - function refresh() { - ethernetStateProcess.running = true - - if (Settings.data.network.wifiEnabled) { - scan() - } } 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 = "" - scanProcess.running = true + ignoreScanResults = false + + // Get existing profiles first, then scan + profileCheckProcess.running = true Logger.log("Network", "Wi-Fi scan in progress...") } @@ -174,8 +187,7 @@ Singleton { "ssid": ssid, "security": "--", "signal": 100, - "connected"// Default to good signal until real scan - : true, + "connected": true, "existing": true, "cached": true } @@ -206,7 +218,7 @@ Singleton { // Processes Process { id: ethernetStateProcess - running: false + running: true command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"] stdout: StdioCollector { @@ -239,30 +251,34 @@ 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) + 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 + } + scanProcess.existingProfiles = profiles + scanProcess.running = true } } } @@ -270,74 +286,103 @@ 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 - `] + property var existingProfiles: ({}) stdout: StdioCollector { onStreamFinished: { - const nets = {} - const lines = text.split("\n").filter(l => l.trim()) + if (root.ignoreScanResults) { + Logger.log("Network", "Ignoring scan results (new scan requested)") + root.scanning = false - for (const line of lines) { - const parts = line.split("|") - if (parts.length < 5) + // 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 = {} + + 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(", ")) @@ -345,12 +390,19 @@ 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") + 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() + } } } @@ -359,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" @@ -390,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] = { @@ -408,7 +469,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() } } @@ -464,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() } } @@ -522,8 +583,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 +596,7 @@ Singleton { Logger.warn("Network", "Forget error: " + text) } // Still Trigger a scan even on error - delayedScanTimer.interval = 500 + delayedScanTimer.interval = 5000 delayedScanTimer.restart() } } diff --git a/Services/NotificationService.qml b/Services/NotificationService.qml index 4ce9747..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 } } @@ -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, { @@ -166,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) { @@ -190,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 diff --git a/Services/SystemStatService.qml b/Services/SystemStatService.qml index 4f09c1d..7328f71 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: 0 + property real prevTxBytes: 0 + 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 of all cores/sensors + 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) { + 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() + } } diff --git a/Services/UpdateService.qml b/Services/UpdateService.qml index c4ec7d2..f2d3207 100644 --- a/Services/UpdateService.qml +++ b/Services/UpdateService.qml @@ -8,7 +8,7 @@ Singleton { id: root // Public properties - property string baseVersion: "2.6.0" + property string baseVersion: "2.7.0" property bool isDevelopment: true property string currentVersion: `v${!isDevelopment ? baseVersion : baseVersion + "-dev"}` 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 } // ------------------------------------------------------------------- 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/NCheckbox.qml b/Widgets/NCheckbox.qml index 48d76e5..4b5962d 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 @@ -39,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 } } diff --git a/Widgets/NClock.qml b/Widgets/NClock.qml deleted file mode 100644 index aa8ce33..0000000 --- a/Widgets/NClock.qml +++ /dev/null @@ -1,34 +0,0 @@ -import QtQuick -import qs.Commons -import qs.Services -import qs.Widgets - -Rectangle { - id: root - - signal entered - signal exited - signal clicked - - width: textItem.paintedWidth - height: textItem.paintedHeight - color: Color.transparent - - NText { - id: textItem - text: Time.time - 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() - } -} 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/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/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 { diff --git a/Widgets/NPill.qml b/Widgets/NPill.qml index 2432544..2011c9d 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 @@ -37,13 +39,13 @@ 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) - width: iconSize + (effectiveShown ? maxPillWidth - pillOverlap : 0) + width: iconSize + Math.max(0, pill.width - pillOverlap) height: pillHeight Rectangle { @@ -65,7 +67,13 @@ Item { NText { id: textItem - anchors.centerIn: parent + anchors.verticalCenter: parent.verticalCenter + 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 @@ -97,9 +105,10 @@ 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 + x: rightOpen ? 0 : (parent.width - width) Behavior on color { ColorAnimation { @@ -110,6 +119,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) diff --git a/Widgets/NSpinBox.qml b/Widgets/NSpinBox.qml index 8049cf6..828aa56 100644 --- a/Widgets/NSpinBox.qml +++ b/Widgets/NSpinBox.qml @@ -44,7 +44,7 @@ RowLayout { radius: height * 0.5 // Fully rounded like toggle color: Color.mSurfaceVariant border.color: root.hovering ? Color.mPrimary : Color.mOutline - border.width: 1 + border.width: Math.max(1, Style.borderS * scaling) Behavior on border.color { ColorAnimation { 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 }