From eadebacea9fc68b37497179e854b1f6459ac5679 Mon Sep 17 00:00:00 2001 From: lysec Date: Sat, 11 Oct 2025 15:17:21 +0200 Subject: [PATCH 01/17] DockTab: fix NComboBox Autoformat --- Modules/Bar/Calendar/CalendarPanel.qml | 207 +++++++++++-------------- Modules/Settings/SettingsPanel.qml | 11 +- Modules/Settings/Tabs/DockTab.qml | 40 ++--- 3 files changed, 113 insertions(+), 145 deletions(-) diff --git a/Modules/Bar/Calendar/CalendarPanel.qml b/Modules/Bar/Calendar/CalendarPanel.qml index 21246815..fc520bf2 100644 --- a/Modules/Bar/Calendar/CalendarPanel.qml +++ b/Modules/Bar/Calendar/CalendarPanel.qml @@ -43,14 +43,20 @@ NPanel { ColumnLayout { id: blueColumn - anchors.fill: parent - anchors.margins: Style.marginM * scaling + anchors.top: parent.top + anchors.left: parent.left + anchors.bottom: parent.bottom + anchors.topMargin: Style.marginM * scaling + anchors.leftMargin: Style.marginM * scaling + anchors.bottomMargin: Style.marginM * scaling + anchors.rightMargin: clockItem.width + (Style.marginM * scaling * 2) spacing: 0 // Combined layout for weather icon, date, and weather text RowLayout { Layout.fillWidth: true - Layout.preferredHeight: 60 * scaling + height: 60 * scaling + clip: true spacing: Style.marginS * scaling // Weather icon and temperature @@ -85,22 +91,36 @@ NPanel { } } - // Today day number + // Today day number - with simple, stable animation NText { - visible: content.isCurrentMonth + opacity: content.isCurrentMonth ? 1.0 : 0.0 + Layout.preferredWidth: content.isCurrentMonth ? implicitWidth : 0 + elide: Text.ElideNone + clip: true + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft text: Time.date.getDate() pointSize: Style.fontSizeXXXL * 1.5 * scaling font.weight: Style.fontWeightBold color: Color.mOnPrimary - } - Item { - visible: !content.isCurrentMonth + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + Behavior on Layout.preferredWidth { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.InOutQuad + } + } } // Month, year, location ColumnLayout { - Layout.fillWidth: false + // Give the whole column a fixed width to stabilize the layout + Layout.preferredWidth: 170 * scaling Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft spacing: -Style.marginXS * scaling @@ -113,7 +133,6 @@ NPanel { font.weight: Style.fontWeightBold color: Color.mOnPrimary Layout.alignment: Qt.AlignBaseline - Layout.maximumWidth: 150 * scaling elide: Text.ElideRight } @@ -152,96 +171,86 @@ NPanel { } } - // Spacer between date and clock + // Spacer to push content left Item { Layout.fillWidth: true } + } + } - // Digital clock with circular progress - Item { - width: Style.fontSizeXXXL * 1.9 * scaling - height: Style.fontSizeXXXL * 1.9 * scaling - Layout.alignment: Qt.AlignVCenter + // The Clock, anchored separately for stability + Item { + id: clockItem + anchors.right: parent.right + anchors.rightMargin: Style.marginM * scaling + anchors.verticalCenter: parent.verticalCenter + width: Style.fontSizeXXXL * 1.9 * scaling + height: Style.fontSizeXXXL * 1.9 * scaling - // Seconds circular progress - Canvas { - id: secondsProgress - anchors.fill: parent - - property real progress: Time.date.getSeconds() / 60 - onProgressChanged: requestPaint() - - Connections { - target: Time - function onDateChanged() { - const total = Time.date.getSeconds() * 1000 + Time.date.getMilliseconds() - secondsProgress.progress = total / 60000 - } - } - - onPaint: { - var ctx = getContext("2d") - var centerX = width / 2 - var centerY = height / 2 - var radius = Math.min(width, height) / 2 - 3 * scaling - - ctx.reset() - - // Background circle - ctx.beginPath() - ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI) - ctx.lineWidth = 2.5 * scaling - ctx.strokeStyle = Qt.alpha(Color.mOnPrimary, 0.15) - ctx.stroke() - - // Progress arc - ctx.beginPath() - ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + progress * 2 * Math.PI) - ctx.lineWidth = 2.5 * scaling - ctx.strokeStyle = Color.mOnPrimary - ctx.lineCap = "round" - ctx.stroke() - } + Canvas { + id: secondsProgress + anchors.fill: parent + property real progress: Time.date.getSeconds() / 60 + onProgressChanged: requestPaint() + Connections { + target: Time + function onDateChanged() { + const total = Time.date.getSeconds() * 1000 + Time.date.getMilliseconds() + secondsProgress.progress = total / 60000 } + } + onPaint: { + var ctx = getContext("2d") + var centerX = width / 2 + var centerY = height / 2 + var radius = Math.min(width, height) / 2 - 3 * scaling + ctx.reset() + ctx.beginPath() + ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI) + ctx.lineWidth = 2.5 * scaling + ctx.strokeStyle = Qt.alpha(Color.mOnPrimary, 0.15) + ctx.stroke() + ctx.beginPath() + ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + progress * 2 * Math.PI) + ctx.lineWidth = 2.5 * scaling + ctx.strokeStyle = Color.mOnPrimary + ctx.lineCap = "round" + ctx.stroke() + } + } - // Digital clock - ColumnLayout { - anchors.centerIn: parent - spacing: -Style.marginXXS * scaling - - NText { - text: { - var t = Settings.data.location.use12hourFormat ? Qt.locale().toString(new Date(), "hh AP") : Qt.locale().toString(new Date(), "HH") - return t.split(" ")[0] - } - pointSize: Style.fontSizeXS * scaling - font.weight: Style.fontWeightBold - color: Color.mOnPrimary - family: Settings.data.ui.fontFixed - Layout.alignment: Qt.AlignHCenter - } - - NText { - text: Qt.formatTime(Time.date, "mm") - pointSize: Style.fontSizeXXS * scaling - font.weight: Style.fontWeightBold - color: Color.mOnPrimary - family: Settings.data.ui.fontFixed - Layout.alignment: Qt.AlignHCenter - } + ColumnLayout { + anchors.centerIn: parent + spacing: -Style.marginXXS * scaling + NText { + text: { + var t = Settings.data.location.use12hourFormat ? Qt.locale().toString(new Date(), "hh AP") : Qt.locale().toString(new Date(), "HH") + return t.split(" ")[0] } + pointSize: Style.fontSizeXS * scaling + font.weight: Style.fontWeightBold + color: Color.mOnPrimary + family: Settings.data.ui.fontFixed + Layout.alignment: Qt.AlignHCenter + } + NText { + text: Qt.formatTime(Time.date, "mm") + pointSize: Style.fontSizeXXS * scaling + font.weight: Style.fontWeightBold + color: Color.mOnPrimary + family: Settings.data.ui.fontFixed + Layout.alignment: Qt.AlignHCenter } } } } - // 6-day forecast (outside blue banner) + // ... (rest of the file is unchanged) ... RowLayout { visible: weatherReady Layout.fillWidth: true Layout.alignment: Qt.AlignHCenter spacing: Style.marginL * scaling - Repeater { model: weatherReady ? Math.min(6, LocationService.data.weather.daily.time.length) : 0 delegate: ColumnLayout { @@ -249,7 +258,6 @@ NPanel { Layout.fillWidth: true Layout.alignment: Qt.AlignHCenter spacing: Style.marginS * scaling - NText { text: { var weatherDate = new Date(LocationService.data.weather.daily.time[index].replace(/-/g, "/")) @@ -260,14 +268,12 @@ NPanel { font.weight: Style.fontWeightMedium Layout.alignment: Qt.AlignHCenter } - NIcon { Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter icon: LocationService.weatherSymbolFromCode(LocationService.data.weather.daily.weathercode[index]) pointSize: Style.fontSizeXXL * 1.5 * scaling color: Color.mPrimary } - NText { Layout.alignment: Qt.AlignHCenter text: { @@ -288,27 +294,19 @@ NPanel { } } } - - // Loading indicator for weather RowLayout { visible: !weatherReady Layout.fillWidth: true Layout.alignment: Qt.AlignHCenter NBusyIndicator {} } - - // Spacer Item {} - - // Navigation and divider RowLayout { Layout.fillWidth: true spacing: Style.marginS * scaling - NDivider { Layout.fillWidth: true } - NIconButton { icon: "chevron-left" onClicked: { @@ -318,7 +316,6 @@ NPanel { content.isCurrentMonth = content.checkIsCurrentMonth() } } - NIconButton { icon: "calendar" onClicked: { @@ -327,7 +324,6 @@ NPanel { content.isCurrentMonth = true } } - NIconButton { icon: "chevron-right" onClicked: { @@ -338,31 +334,24 @@ NPanel { } } } - - // Names of days of the week RowLayout { Layout.fillWidth: true spacing: 0 - Item { visible: Settings.data.location.showWeekNumberInCalendar Layout.preferredWidth: visible ? Style.baseWidgetSize * 0.7 * scaling : 0 } - GridLayout { Layout.fillWidth: true columns: 7 rows: 1 columnSpacing: 0 rowSpacing: 0 - Repeater { model: 7 - Item { Layout.fillWidth: true Layout.preferredHeight: Style.baseWidgetSize * 0.6 * scaling - NText { anchors.centerIn: parent text: { @@ -379,27 +368,20 @@ NPanel { } } } - - // Grid with weeks and days RowLayout { Layout.fillWidth: true Layout.fillHeight: true spacing: 0 - - // Column of week numbers ColumnLayout { visible: Settings.data.location.showWeekNumberInCalendar Layout.preferredWidth: visible ? Style.baseWidgetSize * 0.7 * scaling : 0 Layout.fillHeight: true spacing: 0 - Repeater { model: 6 - Item { Layout.fillWidth: true Layout.fillHeight: true - NText { anchors.centerIn: parent color: Color.mOutline @@ -431,27 +413,21 @@ NPanel { } } } - - // Days Grid MonthGrid { id: grid - Layout.fillWidth: true Layout.fillHeight: true spacing: Style.marginXXS * scaling month: Time.date.getMonth() year: Time.date.getFullYear() locale: Qt.locale() - delegate: Item { Rectangle { width: Style.baseWidgetSize * 0.9 * scaling height: Style.baseWidgetSize * 0.9 * scaling anchors.centerIn: parent radius: Style.radiusM * scaling - color: model.today ? Color.mSecondary : Color.transparent - NText { anchors.centerIn: parent text: model.day @@ -466,7 +442,6 @@ NPanel { pointSize: Style.fontSizeM * scaling font.weight: model.today ? Style.fontWeightBold : Style.fontWeightMedium } - Behavior on color { ColorAnimation { duration: Style.animationFast diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/Settings/SettingsPanel.qml index 28b21800..ff499cfe 100644 --- a/Modules/Settings/SettingsPanel.qml +++ b/Modules/Settings/SettingsPanel.qml @@ -129,12 +129,11 @@ NPanel { "label": "settings.bar.title", "icon": "settings-bar", "source": barTab - }, - //{ - // "id": SettingsPanel.Tab.ControlCenter, - // "label": "settings.control-center.title", - // "icon": "settings-bar", - // "source": controlCenterTab + }, //{ + // "id": SettingsPanel.Tab.ControlCenter, + // "label": "settings.control-center.title", + // "icon": "settings-bar", + // "source": controlCenterTab //}, { "id": SettingsPanel.Tab.Dock, diff --git a/Modules/Settings/Tabs/DockTab.qml b/Modules/Settings/Tabs/DockTab.qml index 60ad8a9f..5d31c074 100644 --- a/Modules/Settings/Tabs/DockTab.qml +++ b/Modules/Settings/Tabs/DockTab.qml @@ -29,30 +29,24 @@ ColumnLayout { description: I18n.tr("settings.dock.appearance.section.description") } - ColumnLayout { - spacing: Style.marginXXS * scaling + NComboBox { Layout.fillWidth: true - NLabel { - label: I18n.tr("settings.dock.appearance.display.label") - description: I18n.tr("settings.dock.appearance.display.description") - } - NComboBox { - Layout.fillWidth: true - model: [{ - "key": "always_visible", - "name": I18n.tr("settings.dock.appearance.display.always-visible") - }, { - "key": "auto_hide", - "name": I18n.tr("settings.dock.appearance.display.auto-hide") - }, { - "key": "exclusive", - "name": I18n.tr("settings.dock.appearance.display.exclusive") - }] - currentKey: Settings.data.dock.displayMode - onSelected: key => { - Settings.data.dock.displayMode = key - } - } + label: I18n.tr("settings.dock.appearance.display.label") + description: I18n.tr("settings.dock.appearance.display.description") + model: [{ + "key": "always_visible", + "name": I18n.tr("settings.dock.appearance.display.always-visible") + }, { + "key": "auto_hide", + "name": I18n.tr("settings.dock.appearance.display.auto-hide") + }, { + "key": "exclusive", + "name": I18n.tr("settings.dock.appearance.display.exclusive") + }] + currentKey: Settings.data.dock.displayMode + onSelected: key => { + Settings.data.dock.displayMode = key + } } ColumnLayout { From 263f3c5fd27e0790989c77614ef8df39a5fb1fc9 Mon Sep 17 00:00:00 2001 From: lysec Date: Sat, 11 Oct 2025 15:44:06 +0200 Subject: [PATCH 02/17] Matugen: fix discord theming ColorSchemeTab: fix predefined color scheme preview --- Modules/Settings/Tabs/ColorSchemeTab.qml | 128 +++++++++++++++++++++-- Services/AppThemeService.qml | 2 +- 2 files changed, 123 insertions(+), 7 deletions(-) diff --git a/Modules/Settings/Tabs/ColorSchemeTab.qml b/Modules/Settings/Tabs/ColorSchemeTab.qml index d8e8f338..6789a2d0 100644 --- a/Modules/Settings/Tabs/ColorSchemeTab.qml +++ b/Modules/Settings/Tabs/ColorSchemeTab.qml @@ -12,6 +12,9 @@ ColumnLayout { // Cache for scheme JSON (can be flat or {dark, light}) property var schemeColorsCache: ({}) + // Signal to notify when cache is updated + signal cacheUpdated + spacing: Style.marginL * scaling // Helper function to extract scheme name from path @@ -52,12 +55,126 @@ ColumnLayout { return "#000000" } + // Alternative function that tries to load colors directly + function getSchemeColorDirect(schemePath, colorKey) { + var schemeName = extractSchemeName(schemePath) + + // Try to load the file directly using ColorSchemeService's resolveSchemePath + var filePath = ColorSchemeService.resolveSchemePath(schemeName) + if (!filePath) + return "#000000" + + // For now, return a placeholder color based on the scheme name + // This is a temporary solution until we can properly load the files + var colors = { + "Ayu": { + "mSurface": "#1e222a", + "mPrimary": "#E6B450", + "mSecondary": "#AAD94C", + "mTertiary": "#39BAE6", + "mError": "#D95757" + }, + "Catppuccin": { + "mSurface": "#1e1e2e", + "mPrimary": "#cba6f7", + "mSecondary": "#fab387", + "mTertiary": "#94e2d5", + "mError": "#f38ba8" + }, + "Dracula": { + "mSurface": "#282a36", + "mPrimary": "#bd93f9", + "mSecondary": "#ff79c6", + "mTertiary": "#8be9fd", + "mError": "#ff5555" + }, + "Everforest": { + "mSurface": "#2d353b", + "mPrimary": "#a7c080", + "mSecondary": "#dbbc7f", + "mTertiary": "#7fbbb3", + "mError": "#e67e80" + }, + "Gruvbox": { + "mSurface": "#282828", + "mPrimary": "#fabd2f", + "mSecondary": "#fe8019", + "mTertiary": "#8ec07c", + "mError": "#fb4934" + }, + "Kanagawa": { + "mSurface": "#1f1f28", + "mPrimary": "#c8c093", + "mSecondary": "#d27e99", + "mTertiary": "#7aa89f", + "mError": "#c34043" + }, + "Monochrome": { + "mSurface": "#1a1a1a", + "mPrimary": "#ffffff", + "mSecondary": "#cccccc", + "mTertiary": "#999999", + "mError": "#ff0000" + }, + "Noctalia (default)": { + "mSurface": "#1c1822", + "mPrimary": "#c7a1d8", + "mSecondary": "#a984c4", + "mTertiary": "#e0b7c9", + "mError": "#e9899d" + }, + "Noctalia (legacy)": { + "mSurface": "#1c1822", + "mPrimary": "#c7a1d8", + "mSecondary": "#a984c4", + "mTertiary": "#e0b7c9", + "mError": "#e9899d" + }, + "Nord": { + "mSurface": "#2e3440", + "mPrimary": "#88c0d0", + "mSecondary": "#81a1c1", + "mTertiary": "#8fbcbb", + "mError": "#bf616a" + }, + "Rosepine": { + "mSurface": "#191724", + "mPrimary": "#c4a7e7", + "mSecondary": "#ebbcba", + "mTertiary": "#9ccfd8", + "mError": "#eb6f92" + }, + "Solarized": { + "mSurface": "#002b36", + "mPrimary": "#268bd2", + "mSecondary": "#2aa198", + "mTertiary": "#859900", + "mError": "#dc322f" + }, + "Tokyo Night": { + "mSurface": "#1a1b26", + "mPrimary": "#7aa2f7", + "mSecondary": "#bb9af7", + "mTertiary": "#9ece6a", + "mError": "#f7768e" + } + } + + if (colors[schemeName] && colors[schemeName][colorKey]) { + return colors[schemeName][colorKey] + } + + return "#000000" + } + // This function is called by the FileView Repeater when a scheme file is loaded function schemeLoaded(schemeName, jsonData) { var value = jsonData || {} var newCache = schemeColorsCache newCache[schemeName] = value schemeColorsCache = newCache + // Force UI update by triggering our custom signal + cacheUpdated() } // When the list of available schemes changes, clear the cache. @@ -103,7 +220,6 @@ ColumnLayout { delegate: Item { FileView { path: modelData - blockLoading: true onLoaded: { // Extract scheme name from path var schemeName = extractSchemeName(path) @@ -237,7 +353,7 @@ ColumnLayout { Layout.alignment: Qt.AlignHCenter height: 50 * scaling radius: Style.radiusS * scaling - color: getSchemeColor(modelData, "mSurface") + color: getSchemeColorDirect(modelData, "mSurface") border.width: Math.max(1, Style.borderL * scaling) border.color: { if (Settings.data.colorSchemes.predefinedScheme === extractSchemeName(modelData)) { @@ -271,28 +387,28 @@ ColumnLayout { width: 14 * scaling height: 14 * scaling radius: width * 0.5 - color: getSchemeColor(modelData, "mPrimary") + color: getSchemeColorDirect(modelData, "mPrimary") } Rectangle { width: 14 * scaling height: 14 * scaling radius: width * 0.5 - color: getSchemeColor(modelData, "mSecondary") + color: getSchemeColorDirect(modelData, "mSecondary") } Rectangle { width: 14 * scaling height: 14 * scaling radius: width * 0.5 - color: getSchemeColor(modelData, "mTertiary") + color: getSchemeColorDirect(modelData, "mTertiary") } Rectangle { width: 14 * scaling height: 14 * scaling radius: width * 0.5 - color: getSchemeColor(modelData, "mError") + color: getSchemeColorDirect(modelData, "mError") } } diff --git a/Services/AppThemeService.qml b/Services/AppThemeService.qml index 0113216b..3fbf1153 100644 --- a/Services/AppThemeService.qml +++ b/Services/AppThemeService.qml @@ -61,7 +61,7 @@ Singleton { }], "postProcess": () => `${colorsApplyScript} pywalfox\n` }, - "vesktop": { + "discord_vesktop": { "input": "vesktop.css", "outputs": [{ "path": "~/.config/vesktop/themes/noctalia.theme.css" From 656e15f5892393be195055478f56aee08c770199 Mon Sep 17 00:00:00 2001 From: lysec Date: Sat, 11 Oct 2025 15:51:21 +0200 Subject: [PATCH 03/17] ColorSchemeTab: properly fix predefined colorscheme preview --- Modules/Settings/Tabs/ColorSchemeTab.qml | 200 ++++++----------------- 1 file changed, 46 insertions(+), 154 deletions(-) diff --git a/Modules/Settings/Tabs/ColorSchemeTab.qml b/Modules/Settings/Tabs/ColorSchemeTab.qml index 6789a2d0..a5c66ead 100644 --- a/Modules/Settings/Tabs/ColorSchemeTab.qml +++ b/Modules/Settings/Tabs/ColorSchemeTab.qml @@ -11,19 +11,16 @@ ColumnLayout { // Cache for scheme JSON (can be flat or {dark, light}) property var schemeColorsCache: ({}) - - // Signal to notify when cache is updated - signal cacheUpdated + property int cacheVersion: 0 // Increment to trigger UI updates spacing: Style.marginL * scaling // Helper function to extract scheme name from path function extractSchemeName(schemePath) { var pathParts = schemePath.split("/") - var filename = pathParts[pathParts.length - 1] // Get filename - var schemeName = filename.replace(".json", "") // Remove .json extension + var filename = pathParts[pathParts.length - 1] + var schemeName = filename.replace(".json", "") - // Convert folder names back to display names if (schemeName === "Noctalia-default") { schemeName = "Noctalia (default)" } else if (schemeName === "Noctalia-legacy") { @@ -36,153 +33,52 @@ ColumnLayout { } // Helper function to get color from scheme file (supports dark/light variants) - function getSchemeColor(schemePath, colorKey) { - // Extract scheme name from path - var schemeName = extractSchemeName(schemePath) + function getSchemeColor(schemeName, colorKey) { + // Access cache version to create dependency + var _ = cacheVersion - // Try to get from cached data first if (schemeColorsCache[schemeName]) { var entry = schemeColorsCache[schemeName] var variant = entry + + // Check if scheme has dark/light variants if (entry.dark || entry.light) { variant = Settings.data.colorSchemes.darkMode ? (entry.dark || entry.light) : (entry.light || entry.dark) } - if (variant && variant[colorKey]) + + if (variant && variant[colorKey]) { return variant[colorKey] - } - - // Return a default color if not cached yet - return "#000000" - } - - // Alternative function that tries to load colors directly - function getSchemeColorDirect(schemePath, colorKey) { - var schemeName = extractSchemeName(schemePath) - - // Try to load the file directly using ColorSchemeService's resolveSchemePath - var filePath = ColorSchemeService.resolveSchemePath(schemeName) - if (!filePath) - return "#000000" - - // For now, return a placeholder color based on the scheme name - // This is a temporary solution until we can properly load the files - var colors = { - "Ayu": { - "mSurface": "#1e222a", - "mPrimary": "#E6B450", - "mSecondary": "#AAD94C", - "mTertiary": "#39BAE6", - "mError": "#D95757" - }, - "Catppuccin": { - "mSurface": "#1e1e2e", - "mPrimary": "#cba6f7", - "mSecondary": "#fab387", - "mTertiary": "#94e2d5", - "mError": "#f38ba8" - }, - "Dracula": { - "mSurface": "#282a36", - "mPrimary": "#bd93f9", - "mSecondary": "#ff79c6", - "mTertiary": "#8be9fd", - "mError": "#ff5555" - }, - "Everforest": { - "mSurface": "#2d353b", - "mPrimary": "#a7c080", - "mSecondary": "#dbbc7f", - "mTertiary": "#7fbbb3", - "mError": "#e67e80" - }, - "Gruvbox": { - "mSurface": "#282828", - "mPrimary": "#fabd2f", - "mSecondary": "#fe8019", - "mTertiary": "#8ec07c", - "mError": "#fb4934" - }, - "Kanagawa": { - "mSurface": "#1f1f28", - "mPrimary": "#c8c093", - "mSecondary": "#d27e99", - "mTertiary": "#7aa89f", - "mError": "#c34043" - }, - "Monochrome": { - "mSurface": "#1a1a1a", - "mPrimary": "#ffffff", - "mSecondary": "#cccccc", - "mTertiary": "#999999", - "mError": "#ff0000" - }, - "Noctalia (default)": { - "mSurface": "#1c1822", - "mPrimary": "#c7a1d8", - "mSecondary": "#a984c4", - "mTertiary": "#e0b7c9", - "mError": "#e9899d" - }, - "Noctalia (legacy)": { - "mSurface": "#1c1822", - "mPrimary": "#c7a1d8", - "mSecondary": "#a984c4", - "mTertiary": "#e0b7c9", - "mError": "#e9899d" - }, - "Nord": { - "mSurface": "#2e3440", - "mPrimary": "#88c0d0", - "mSecondary": "#81a1c1", - "mTertiary": "#8fbcbb", - "mError": "#bf616a" - }, - "Rosepine": { - "mSurface": "#191724", - "mPrimary": "#c4a7e7", - "mSecondary": "#ebbcba", - "mTertiary": "#9ccfd8", - "mError": "#eb6f92" - }, - "Solarized": { - "mSurface": "#002b36", - "mPrimary": "#268bd2", - "mSecondary": "#2aa198", - "mTertiary": "#859900", - "mError": "#dc322f" - }, - "Tokyo Night": { - "mSurface": "#1a1b26", - "mPrimary": "#7aa2f7", - "mSecondary": "#bb9af7", - "mTertiary": "#9ece6a", - "mError": "#f7768e" } } - if (colors[schemeName] && colors[schemeName][colorKey]) { - return colors[schemeName][colorKey] - } - - return "#000000" + // Return visible defaults while loading + if (colorKey === "mSurface") + return Color.mSurfaceVariant + if (colorKey === "mPrimary") + return Color.mPrimary + if (colorKey === "mSecondary") + return Color.mSecondary + if (colorKey === "mTertiary") + return Color.mTertiary + if (colorKey === "mError") + return Color.mError + return Color.mOnSurfaceVariant } // This function is called by the FileView Repeater when a scheme file is loaded function schemeLoaded(schemeName, jsonData) { var value = jsonData || {} - var newCache = schemeColorsCache - newCache[schemeName] = value - schemeColorsCache = newCache - // Force UI update by triggering our custom signal - cacheUpdated() + schemeColorsCache[schemeName] = value + // Force UI update by incrementing cache version + cacheVersion++ } - // When the list of available schemes changes, clear the cache. - // The Repeater below will automatically re-create the FileViews. + // When the list of available schemes changes, clear the cache Connections { target: ColorSchemeService function onSchemesChanged() { schemeColorsCache = {} + cacheVersion++ } } @@ -194,12 +90,10 @@ ColumnLayout { onExited: function (exitCode) { if (exitCode === 0) { - // Matugen exists, enable it Settings.data.colorSchemes.useWallpaperColors = true AppThemeService.generate() ToastService.showNotice(I18n.tr("settings.color-scheme.color-source.use-wallpaper-colors.label"), I18n.tr("toast.wallpaper-colors.enabled")) } else { - // Matugen not found ToastService.showWarning(I18n.tr("settings.color-scheme.color-source.use-wallpaper-colors.label"), I18n.tr("toast.wallpaper-colors.not-installed")) } } @@ -208,7 +102,7 @@ ColumnLayout { stderr: StdioCollector {} } - // A non-visual Item to host the Repeater that loads the color scheme files. + // A non-visual Item to host the Repeater that loads the color scheme files Item { visible: false id: fileLoaders @@ -216,20 +110,19 @@ ColumnLayout { Repeater { model: ColorSchemeService.schemes - // The delegate is a Component, which correctly wraps the non-visual FileView delegate: Item { FileView { path: modelData + blockLoading: false onLoaded: { - // Extract scheme name from path - var schemeName = extractSchemeName(path) + var schemeName = root.extractSchemeName(path) try { var jsonData = JSON.parse(text()) root.schemeLoaded(schemeName, jsonData) } catch (e) { Logger.warn("ColorSchemeTab", "Failed to parse JSON for scheme:", schemeName, e) - root.schemeLoaded(schemeName, null) // Load defaults on parse error + root.schemeLoaded(schemeName, null) } } } @@ -243,13 +136,16 @@ ColumnLayout { description: I18n.tr("settings.color-scheme.color-source.section.description") } - // Dark Mode Toggle (affects both Matugen and predefined schemes that provide variants) + // Dark Mode Toggle NToggle { label: I18n.tr("settings.color-scheme.color-source.dark-mode.label") description: I18n.tr("settings.color-scheme.color-source.dark-mode.description") checked: Settings.data.colorSchemes.darkMode enabled: true - onToggled: checked => Settings.data.colorSchemes.darkMode = checked + onToggled: checked => { + Settings.data.colorSchemes.darkMode = checked + root.cacheVersion++ // Force UI update for dark/light variants + } } // Use Wallpaper Colors @@ -259,14 +155,12 @@ ColumnLayout { 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(I18n.tr("settings.color-scheme.color-source.use-wallpaper-colors.label"), I18n.tr("toast.wallpaper-colors.disabled")) if (Settings.data.colorSchemes.predefinedScheme) { - ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme) } } @@ -311,7 +205,6 @@ ColumnLayout { onSelected: key => { Settings.data.colorSchemes.matugenSchemeType = key - AppThemeService.generate() } } @@ -348,15 +241,16 @@ ColumnLayout { id: schemeItem property string schemePath: modelData + property string schemeName: root.extractSchemeName(modelData) Layout.fillWidth: true Layout.alignment: Qt.AlignHCenter height: 50 * scaling radius: Style.radiusS * scaling - color: getSchemeColorDirect(modelData, "mSurface") + color: root.getSchemeColor(schemeName, "mSurface") border.width: Math.max(1, Style.borderL * scaling) border.color: { - if (Settings.data.colorSchemes.predefinedScheme === extractSchemeName(modelData)) { + if (Settings.data.colorSchemes.predefinedScheme === schemeName) { return Color.mSecondary } if (itemMouseArea.containsMouse) { @@ -371,12 +265,11 @@ ColumnLayout { spacing: Style.marginXS * scaling NText { - text: extractSchemeName(schemePath) + text: schemeItem.schemeName pointSize: Style.fontSizeS * scaling font.weight: Style.fontWeightMedium color: Color.mOnSurface Layout.fillWidth: true - // Layout.maximumWidth: 150 * scaling elide: Text.ElideRight verticalAlignment: Text.AlignVCenter wrapMode: Text.WordWrap @@ -387,28 +280,28 @@ ColumnLayout { width: 14 * scaling height: 14 * scaling radius: width * 0.5 - color: getSchemeColorDirect(modelData, "mPrimary") + color: root.getSchemeColor(schemeItem.schemeName, "mPrimary") } Rectangle { width: 14 * scaling height: 14 * scaling radius: width * 0.5 - color: getSchemeColorDirect(modelData, "mSecondary") + color: root.getSchemeColor(schemeItem.schemeName, "mSecondary") } Rectangle { width: 14 * scaling height: 14 * scaling radius: width * 0.5 - color: getSchemeColorDirect(modelData, "mTertiary") + color: root.getSchemeColor(schemeItem.schemeName, "mTertiary") } Rectangle { width: 14 * scaling height: 14 * scaling radius: width * 0.5 - color: getSchemeColorDirect(modelData, "mError") + color: root.getSchemeColor(schemeItem.schemeName, "mError") } } @@ -421,14 +314,14 @@ ColumnLayout { Settings.data.colorSchemes.useWallpaperColors = false Logger.log("ColorSchemeTab", "Disabled wallpaper colors") - Settings.data.colorSchemes.predefinedScheme = extractSchemeName(schemePath) + Settings.data.colorSchemes.predefinedScheme = schemeItem.schemeName ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme) } } // Selection indicator Rectangle { - visible: (Settings.data.colorSchemes.predefinedScheme === extractSchemeName(schemePath)) + visible: (Settings.data.colorSchemes.predefinedScheme === schemeItem.schemeName) anchors.right: parent.right anchors.top: parent.top anchors.rightMargin: -3 * scaling @@ -466,7 +359,6 @@ ColumnLayout { checked: Settings.data.colorSchemes.generateTemplatesForPredefined onToggled: checked => { Settings.data.colorSchemes.generateTemplatesForPredefined = checked - // Re-generate templates if a predefined scheme is currently active if (!Settings.data.colorSchemes.useWallpaperColors && Settings.data.colorSchemes.predefinedScheme) { ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme) } From 54fa04f303a563dfd5698ce85a719c2828536d03 Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Sat, 11 Oct 2025 10:29:28 -0400 Subject: [PATCH 04/17] Compositor: proper monitor scaling detection and display in settings + fixes blurry wallpapers on compositor scaled monitors. --- Assets/Translations/en.json | 2 +- Modules/Background/Background.qml | 99 +++++++++++++--------------- Modules/Settings/Tabs/DisplayTab.qml | 14 ++-- Services/CompositorService.qml | 85 ++++++++++++++++++++++++ Services/HyprlandService.qml | 71 +++++++++++++++++++- Services/NiriService.qml | 62 ++++++++++++++++- Services/SwayService.qml | 67 +++++++++++++++++++ 7 files changed, 337 insertions(+), 63 deletions(-) diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index c4ff50ae..a90e24a7 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -1394,7 +1394,7 @@ "system": { "uptime": "Uptime: {uptime}", "welcome-back": "Welcome back,", - "monitor-description": "{model} ({width}x{height})", + "monitor-description": "{model} ({width}x{height} @ {scale}x)", "scaling-percentage": "{percentage}%", "location-display": "{name} ({coordinates})", "signal-strength": "{signal}%", diff --git a/Modules/Background/Background.qml b/Modules/Background/Background.qml index 4b917522..2bcbb04f 100644 --- a/Modules/Background/Background.qml +++ b/Modules/Background/Background.qml @@ -44,19 +44,6 @@ Variants { property real fillMode: WallpaperService.getFillModeUniform() property vector4d fillColor: Qt.vector4d(Settings.data.wallpaper.fillColor.r, Settings.data.wallpaper.fillColor.g, Settings.data.wallpaper.fillColor.b, 1.0) - property int monitoredWidth: modelData.width - property int monitoredHeight: modelData.height - - onMonitoredWidthChanged: { - Logger.log("Background", "Screen width changed to:", monitoredWidth, "for", modelData.name) - recalculateImageSizes() - } - - onMonitoredHeightChanged: { - Logger.log("Background", "Screen height changed to:", monitoredHeight, "for", modelData.name) - recalculateImageSizes() - } - Component.onCompleted: setWallpaperInitial() Component.onDestruction: { @@ -87,6 +74,13 @@ Variants { } } + Connections { + target: CompositorService + function onDisplayScalesChanged() { + setWallpaperInitial() + } + } + color: Color.transparent screen: modelData WlrLayershell.layer: WlrLayer.Background @@ -122,38 +116,21 @@ Variants { cache: false asynchronous: true sourceSize: undefined - onStatusChanged: { if (status === Image.Error) { Logger.warn("Current wallpaper failed to load:", source) } else if (status === Image.Ready && !dimensionsCalculated) { dimensionsCalculated = true - calculateSourceSize() + const optimalSize = calculateOptimalWallpaperSize(implicitWidth, implicitHeight) + if (optimalSize !== false) { + sourceSize = optimalSize + } } } - onSourceChanged: { dimensionsCalculated = false sourceSize = undefined } - - function calculateSourceSize() { - if (implicitWidth === modelData.width || implicitHeight === modelData.height) { - // Do not resize if one of the dimensions fits perfectly on the screen - return - } - - if (implicitWidth > 0 && implicitHeight > 0) { - const imageAspectRatio = implicitWidth / implicitHeight - if (modelData.width >= modelData.height) { - const w = Math.min(modelData.width, implicitWidth) - sourceSize = Qt.size(w, w / imageAspectRatio) - } else { - const h = Math.min(modelData.height, implicitHeight) - sourceSize = Qt.size(h * imageAspectRatio, h) - } - } - } } Image { @@ -168,38 +145,21 @@ Variants { cache: false asynchronous: true sourceSize: undefined - onStatusChanged: { if (status === Image.Error) { Logger.warn("Next wallpaper failed to load:", source) } else if (status === Image.Ready && !dimensionsCalculated) { dimensionsCalculated = true - calculateSourceSize() + const optimalSize = calculateOptimalWallpaperSize(implicitWidth, implicitHeight) + if (optimalSize !== false) { + sourceSize = optimalSize + } } } - onSourceChanged: { dimensionsCalculated = false sourceSize = undefined } - - function calculateSourceSize() { - if (implicitWidth === modelData.width || implicitHeight === modelData.height) { - // Do not resize if one of the dimensions fits perfectly on the screen - return - } - - if (implicitWidth > 0 && implicitHeight > 0) { - const imageAspectRatio = implicitWidth / implicitHeight - if (modelData.width >= modelData.height) { - const w = Math.min(modelData.width, implicitWidth) - sourceSize = Qt.size(w, w / imageAspectRatio) - } else { - const h = Math.min(modelData.height, implicitHeight) - sourceSize = Qt.size(h * imageAspectRatio, h) - } - } - } } // Dynamic shader loader - only loads the active transition shader @@ -356,6 +316,31 @@ Variants { } } + // ------------------------------------------------------ + function calculateOptimalWallpaperSize(wpWidth, wpHeight) { + const compositorScale = CompositorService.getDisplayScale(modelData.name) + const screenWidth = modelData.width * compositorScale + const screenHeight = modelData.height * compositorScale + if (wpWidth <= screenWidth || wpHeight <= screenHeight || wpWidth <= 0 || wpHeight <= 0) { + // Do not resize if wallpaper is smaller than one of the screen dimension + return + } + + const imageAspectRatio = wpWidth / wpHeight + var dim = Qt.size(0, 0) + if (screenWidth >= screenHeight) { + const w = Math.min(screenWidth, wpWidth) + dim = Qt.size(w, w / imageAspectRatio) + } else { + const h = Math.min(screenHeight, wpHeight) + dim = Qt.size(h * imageAspectRatio, h) + } + + Logger.log("Background", `Wallpaper resized on ${modelData.name} ${screenWidth}x${screenHeight} @ ${compositorScale}x`, "src:", wpWidth, wpHeight, "dst:", dim.width, dim.height) + return dim + } + + // ------------------------------------------------------ function recalculateImageSizes() { if (currentWallpaper.status === Image.Ready) { currentWallpaper.calculateSourceSize() @@ -365,6 +350,7 @@ Variants { } } + // ------------------------------------------------------ function setWallpaperInitial() { // On startup, defer assigning wallpaper until the service cache is ready, retries every tick if (!WallpaperService || !WallpaperService.isInitialized) { @@ -375,6 +361,7 @@ Variants { setWallpaperImmediate(WallpaperService.getWallpaper(modelData.name)) } + // ------------------------------------------------------ function setWallpaperImmediate(source) { transitionAnimation.stop() transitionProgress = 0.0 @@ -388,6 +375,7 @@ Variants { }) } + // ------------------------------------------------------ function setWallpaperWithTransition(source) { if (source === currentWallpaper.source) { return @@ -421,6 +409,7 @@ Variants { transitionAnimation.start() } + // ------------------------------------------------------ // Main method that actually trigger the wallpaper change function changeWallpaper() { // Get the transitionType from the settings diff --git a/Modules/Settings/Tabs/DisplayTab.qml b/Modules/Settings/Tabs/DisplayTab.qml index d6ae8420..6eed0a7b 100644 --- a/Modules/Settings/Tabs/DisplayTab.qml +++ b/Modules/Settings/Tabs/DisplayTab.qml @@ -90,11 +90,15 @@ ColumnLayout { NLabel { label: modelData.name || "Unknown" - description: I18n.tr("system.monitor-description", { - "model": modelData.model, - "width": modelData.width, - "height": modelData.height - }) + description: { + const compositorScale = CompositorService.getDisplayScale(modelData.name) + I18n.tr("system.monitor-description", { + "model": modelData.model, + "width": modelData.width * compositorScale, + "height": modelData.height * compositorScale, + "scale": compositorScale + }) + } } // Scale diff --git a/Services/CompositorService.qml b/Services/CompositorService.qml index fdbde343..a9dd46e6 100644 --- a/Services/CompositorService.qml +++ b/Services/CompositorService.qml @@ -2,6 +2,7 @@ pragma Singleton import QtQuick import Quickshell +import Quickshell.Io import qs.Commons import qs.Services @@ -18,6 +19,10 @@ Singleton { property ListModel windows: ListModel {} property int focusedWindowIndex: -1 + // Display scale data + property var displayScales: ({}) + property bool displayScalesLoaded: false + // Generic events signal workspaceChanged signal activeWindowChanged @@ -26,7 +31,18 @@ Singleton { // Backend service loader property var backend: null + // Cache file path + property string displayCachePath: "" + Component.onCompleted: { + // Setup cache path (needs Settings to be available) + Qt.callLater(() => { + if (typeof Settings !== 'undefined' && Settings.cacheDir) { + displayCachePath = Settings.cacheDir + "display.json" + displayCacheFileView.path = displayCachePath + } + }) + detectCompositor() } @@ -69,6 +85,31 @@ Singleton { } } + // Cache FileView for display scales + FileView { + id: displayCacheFileView + printErrors: false + watchChanges: false + + adapter: JsonAdapter { + id: displayCacheAdapter + property var displays: ({}) + } + + onLoaded: { + // Load cached display scales + displayScales = displayCacheAdapter.displays || {} + displayScalesLoaded = true + // Logger.log("CompositorService", "Loaded display scales from cache:", JSON.stringify(displayScales)) + } + + onLoadFailed: { + // Cache doesn't exist yet, will be created on first update + displayScalesLoaded = true + // Logger.log("CompositorService", "No display cache found, will create on first update") + } + } + // Hyprland backend component Component { id: hyprlandComponent @@ -151,6 +192,50 @@ Singleton { windowListChanged() } + // Update display scales from backend + function updateDisplayScales() { + if (!backend || !backend.queryDisplayScales) { + Logger.warn("CompositorService", "Backend does not support display scale queries") + return + } + + backend.queryDisplayScales() + } + + // Called by backend when display scales are ready + function onDisplayScalesUpdated(scales) { + displayScales = scales + saveDisplayScalesToCache() + displayScalesChanged() + Logger.log("CompositorService", "Display scales updated") + } + + // Save display scales to cache + function saveDisplayScalesToCache() { + if (!displayCachePath) { + return + } + + displayCacheAdapter.displays = displayScales + displayCacheFileView.writeAdapter() + } + + // Public function to get scale for a specific display + function getDisplayScale(displayName) { + if (!displayName || !displayScales[displayName]) { + return 1.0 + } + return displayScales[displayName].scale || 1.0 + } + + // Public function to get all display info for a specific display + function getDisplayInfo(displayName) { + if (!displayName || !displayScales[displayName]) { + return null + } + return displayScales[displayName] + } + // Get focused window function getFocusedWindow() { if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.count) { diff --git a/Services/HyprlandService.qml b/Services/HyprlandService.qml index 81d09549..0cafa965 100644 --- a/Services/HyprlandService.qml +++ b/Services/HyprlandService.qml @@ -1,6 +1,7 @@ import QtQuick import Quickshell import Quickshell.Hyprland +import Quickshell.Io import qs.Commons Item { @@ -15,6 +16,7 @@ Item { signal workspaceChanged signal activeWindowChanged signal windowListChanged + signal displayScalesChanged // Hyprland-specific properties property bool initialized: false @@ -40,6 +42,7 @@ Item { Qt.callLater(() => { safeUpdateWorkspaces() safeUpdateWindows() + queryDisplayScales() }) initialized = true Logger.log("HyprlandService", "Initialized successfully") @@ -48,6 +51,67 @@ Item { } } + // Query display scales + function queryDisplayScales() { + hyprlandMonitorsProcess.running = true + } + + // Hyprland monitors process for display scale detection + // Hyprland monitors process for display scale detection + Process { + id: hyprlandMonitorsProcess + running: false + command: ["hyprctl", "monitors", "-j"] + + property string accumulatedOutput: "" + + stdout: SplitParser { + onRead: function (line) { + // Accumulate lines instead of parsing each one + hyprlandMonitorsProcess.accumulatedOutput += line + } + } + + onExited: function (exitCode) { + if (exitCode !== 0 || !accumulatedOutput) { + Logger.error("HyprlandService", "Failed to query monitors, exit code:", exitCode) + accumulatedOutput = "" + return + } + + try { + const monitorsData = JSON.parse(accumulatedOutput) + const scales = {} + + for (const monitor of monitorsData) { + if (monitor.name) { + scales[monitor.name] = { + "name": monitor.name, + "scale": monitor.scale || 1.0, + "width": monitor.width || 0, + "height": monitor.height || 0, + "refresh_rate": monitor.refreshRate || 0, + "x": monitor.x || 0, + "y": monitor.y || 0, + "active_workspace": monitor.activeWorkspace ? monitor.activeWorkspace.id : -1, + "vrr": monitor.vrr || false, + "focused": monitor.focused || false + } + } + } + + // Notify CompositorService (it will emit displayScalesChanged) + if (CompositorService && CompositorService.onDisplayScalesUpdated) { + CompositorService.onDisplayScalesUpdated(scales) + } + } catch (e) { + Logger.error("HyprlandService", "Failed to parse monitors:", e) + } finally { + // Clear accumulated output for next query + accumulatedOutput = "" + } + } + } // Safe update wrapper function safeUpdate() { safeUpdateWindows() @@ -188,7 +252,7 @@ Item { "id": windowId, "title": title, "appId": appId, - "workspaceId": wsId, + "workspaceId": wsId || -1, "isFocused": focused, "output": output } @@ -268,6 +332,11 @@ Item { safeUpdateWorkspaces() workspaceChanged() updateTimer.restart() + + const monitorsEvents = ["configreloaded", "monitoradded", "monitorremoved", "monitoraddedv2", "monitorremovedv2"] + if (monitorsEvents.includes(event.name)) { + Qt.callLater(queryDisplayScales) + } } } diff --git a/Services/NiriService.qml b/Services/NiriService.qml index 9f3df88b..16226348 100644 --- a/Services/NiriService.qml +++ b/Services/NiriService.qml @@ -20,12 +20,14 @@ Item { signal workspaceChanged signal activeWindowChanged signal windowListChanged + signal displayScalesChanged // Initialization function initialize() { niriEventStream.running = true updateWorkspaces() updateWindows() + queryDisplayScales() Logger.log("NiriService", "Initialized successfully") } @@ -39,6 +41,60 @@ Item { niriWindowsProcess.running = true } + // Query display scales + function queryDisplayScales() { + niriOutputsProcess.running = true + } + + // Niri outputs process for display scale detection + Process { + id: niriOutputsProcess + running: false + command: ["niri", "msg", "--json", "outputs"] + + stdout: SplitParser { + onRead: function (line) { + try { + const outputsData = JSON.parse(line) + const scales = {} + + // Niri returns an object with display names as keys + for (const outputName in outputsData) { + const output = outputsData[outputName] + if (output && output.name) { + const logical = output.logical || {} + const currentModeIdx = output.current_mode || 0 + const modes = output.modes || [] + const currentMode = modes[currentModeIdx] || {} + + scales[output.name] = { + "name": output.name, + "scale": logical.scale || 1.0, + "width": logical.width || 0, + "height": logical.height || 0, + "x": logical.x || 0, + "y": logical.y || 0, + "physical_width": (output.physical_size && output.physical_size[0]) || 0, + "physical_height": (output.physical_size && output.physical_size[1]) || 0, + "refresh_rate": currentMode.refresh_rate || 0, + "vrr_supported": output.vrr_supported || false, + "vrr_enabled": output.vrr_enabled || false, + "transform": logical.transform || "Normal" + } + } + } + + // Notify CompositorService (it will emit displayScalesChanged) + if (CompositorService && CompositorService.onDisplayScalesUpdated) { + CompositorService.onDisplayScalesUpdated(scales) + } + } catch (e) { + Logger.error("NiriService", "Failed to parse outputs:", e, line) + } + } + } + } + // Niri workspace process Process { id: niriWorkspaceProcess @@ -86,7 +142,7 @@ Item { } } - // Niri windows process (for initial load) + // Niri windows process Process { id: niriWindowsProcess running: false @@ -131,6 +187,10 @@ Item { handleWindowLayoutsChanged(event.WindowLayoutsChanged) } else if (event.OverviewOpenedOrClosed) { handleOverviewOpenedOrClosed(event.OverviewOpenedOrClosed) + } else if (event.OutputsChanged) { + queryDisplayScales() + } else if (event.ConfigLoaded) { + queryDisplayScales() } } catch (e) { Logger.error("NiriService", "Error parsing event stream:", e, data) diff --git a/Services/SwayService.qml b/Services/SwayService.qml index ad54125c..9e1ddd71 100644 --- a/Services/SwayService.qml +++ b/Services/SwayService.qml @@ -2,6 +2,7 @@ import QtQuick import Quickshell import Quickshell.I3 import Quickshell.Wayland +import Quickshell.Io import qs.Commons Item { @@ -16,6 +17,7 @@ Item { signal workspaceChanged signal activeWindowChanged signal windowListChanged + signal displayScalesChanged // I3-specific properties property bool initialized: false @@ -38,6 +40,7 @@ Item { Qt.callLater(() => { safeUpdateWorkspaces() safeUpdateWindows() + queryDisplayScales() }) initialized = true Logger.log("SwayService", "Initialized successfully") @@ -46,6 +49,66 @@ Item { } } + // Query display scales + function queryDisplayScales() { + swayOutputsProcess.running = true + } + + // Sway outputs process for display scale detection + Process { + id: swayOutputsProcess + running: false + command: ["swaymsg", "-t", "get_outputs", "-r"] + + property string accumulatedOutput: "" + + stdout: SplitParser { + onRead: function (line) { + swayOutputsProcess.accumulatedOutput += line + } + } + + onExited: function (exitCode) { + if (exitCode !== 0 || !accumulatedOutput) { + Logger.error("SwayService", "Failed to query outputs, exit code:", exitCode) + accumulatedOutput = "" + return + } + + try { + const outputsData = JSON.parse(accumulatedOutput) + const scales = {} + + for (const output of outputsData) { + if (output.name) { + scales[output.name] = { + "name": output.name, + "scale": output.scale || 1.0, + "width": output.current_mode ? output.current_mode.width : 0, + "height": output.current_mode ? output.current_mode.height : 0, + "refresh_rate": output.current_mode ? output.current_mode.refresh : 0, + "x": output.rect ? output.rect.x : 0, + "y": output.rect ? output.rect.y : 0, + "active": output.active || false, + "focused": output.focused || false, + "current_workspace": output.current_workspace || "" + } + } + } + + // Notify CompositorService (it will emit displayScalesChanged) + if (CompositorService && CompositorService.onDisplayScalesUpdated) { + CompositorService.onDisplayScalesUpdated(scales) + } + } catch (e) { + Logger.error("SwayService", "Failed to parse outputs:", e) + } finally { + // Clear accumulated output for next query + accumulatedOutput = "" + } + } + } + // Safe update wrapper function safeUpdate() { safeUpdateWindows() @@ -197,6 +260,10 @@ Item { safeUpdateWorkspaces() workspaceChanged() updateTimer.restart() + + if (event.type === "output") { + Qt.callLater(queryDisplayScales) + } } } From 7449e7a282848f480b1aa2ba9d4541ed6c19aaba Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Sat, 11 Oct 2025 10:33:25 -0400 Subject: [PATCH 05/17] Compositor: scale translations --- Assets/Translations/de.json | 2 +- Assets/Translations/es.json | 2 +- Assets/Translations/fr.json | 2 +- Assets/Translations/pt.json | 2 +- Assets/Translations/zh-CN.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index a930f9fa..369eee4a 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -1411,7 +1411,7 @@ "system": { "uptime": "Laufzeit: {uptime}", "welcome-back": "Willkommen zurück,", - "monitor-description": "{model} ({width}x{height})", + "monitor-description": "{model} ({width}x{height} @ {scale}x)", "scaling-percentage": "{percentage}%", "location-display": "{name} ({coordinates})", "signal-strength": "{signal}%", diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index 604303c3..8c79a64f 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -1394,7 +1394,7 @@ "system": { "uptime": "Actividad: {uptime}", "welcome-back": "¡Bienvenido de nuevo,", - "monitor-description": "{model} ({width}x{height})", + "monitor-description": "{model} ({width}x{height} @ {scale}x)", "scaling-percentage": "{percentage}%", "location-display": "{name} ({coordinates})", "signal-strength": "{signal}%", diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index 59e9504a..32c01982 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -1393,7 +1393,7 @@ }, "system": { "welcome-back": "Bon retour,", - "monitor-description": "{model} ({width}x{height})", + "monitor-description": "{model} ({width}x{height} @ {scale}x)", "scaling-percentage": "{percentage}%", "location-display": "{name} ({coordinates})", "signal-strength": "{signal}%", diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index 5cbd6769..67fa8ead 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -1394,7 +1394,7 @@ "system": { "uptime": "Atividade: {uptime}", "welcome-back": "Bem-vindo(a) de volta, {user}!", - "monitor-description": "{model} ({width}x{height})", + "monitor-description": "{model} ({width}x{height} @ {scale}x)", "scaling-percentage": "{percentage}%", "location-display": "{name} ({coordinates})", "signal-strength": "{signal}%", diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index b257da86..9f421d69 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -1394,7 +1394,7 @@ "system": { "uptime": "系统运行时间:{uptime}", "welcome-back": "欢迎回来,", - "monitor-description": "{model} ({width}x{height})", + "monitor-description": "{model} ({width}x{height} @ {scale}x)", "scaling-percentage": "{percentage}%", "location-display": "{name} ({coordinates})", "signal-strength": "{signal}%", From 5a8da9695da8a5d0e101082812f0d8a88ead6257 Mon Sep 17 00:00:00 2001 From: lysec Date: Sat, 11 Oct 2025 16:58:23 +0200 Subject: [PATCH 06/17] MediaCard: resize, made title text bigger SystemMonitorCard: made more compact WeatherCard: made more compact ControlCenterPanel: adjust height --- Modules/ControlCenter/Cards/MediaCard.qml | 4 +- .../ControlCenter/Cards/SystemMonitorCard.qml | 85 ++++++++++--------- Modules/ControlCenter/Cards/WeatherCard.qml | 2 +- Modules/ControlCenter/ControlCenterPanel.qml | 12 ++- 4 files changed, 56 insertions(+), 47 deletions(-) diff --git a/Modules/ControlCenter/Cards/MediaCard.qml b/Modules/ControlCenter/Cards/MediaCard.qml index 2b362fb0..b4d7b7a6 100644 --- a/Modules/ControlCenter/Cards/MediaCard.qml +++ b/Modules/ControlCenter/Cards/MediaCard.qml @@ -110,7 +110,7 @@ NBox { } } - // Player selector - positioned at the very top + // Player selector Rectangle { id: playerSelectorButton anchors.top: parent.top @@ -306,7 +306,7 @@ NBox { NText { visible: MediaService.trackTitle !== "" text: MediaService.trackTitle - pointSize: Style.fontSizeM * scaling + pointSize: Style.fontSizeL * scaling font.weight: Style.fontWeightBold elide: Text.ElideRight wrapMode: Text.Wrap diff --git a/Modules/ControlCenter/Cards/SystemMonitorCard.qml b/Modules/ControlCenter/Cards/SystemMonitorCard.qml index 67ca2c68..4e7254a3 100644 --- a/Modules/ControlCenter/Cards/SystemMonitorCard.qml +++ b/Modules/ControlCenter/Cards/SystemMonitorCard.qml @@ -9,49 +9,52 @@ import qs.Widgets NBox { id: root - ColumnLayout { + Item { id: content - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.leftMargin: Style.marginS * scaling - anchors.rightMargin: Style.marginS * scaling - anchors.topMargin: Style.marginXS * scaling - anchors.bottomMargin: Style.marginM * scaling - spacing: Style.marginS * scaling + anchors.fill: parent + anchors.margins: Style.marginS * scaling - NCircleStat { - value: SystemStatService.cpuUsage - icon: "cpu-usage" - flat: true - contentScale: 0.8 - width: 72 * scaling - height: 68 * scaling - } - NCircleStat { - value: SystemStatService.cpuTemp - suffix: "°C" - icon: "cpu-temperature" - flat: true - contentScale: 0.8 - width: 72 * scaling - height: 68 * scaling - } - NCircleStat { - value: SystemStatService.memPercent - icon: "memory" - flat: true - contentScale: 0.8 - width: 72 * scaling - height: 68 * scaling - } - NCircleStat { - value: SystemStatService.diskPercent - icon: "storage" - flat: true - contentScale: 0.8 - width: 72 * scaling - height: 68 * scaling + ColumnLayout { + anchors.centerIn: parent + spacing: 0 + + NCircleStat { + value: SystemStatService.cpuUsage + icon: "cpu-usage" + flat: true + contentScale: 0.8 + width: 70 * scaling + height: 65 * scaling + Layout.alignment: Qt.AlignHCenter + } + NCircleStat { + value: SystemStatService.cpuTemp + suffix: "°C" + icon: "cpu-temperature" + flat: true + contentScale: 0.8 + width: 70 * scaling + height: 65 * scaling + Layout.alignment: Qt.AlignHCenter + } + NCircleStat { + value: SystemStatService.memPercent + icon: "memory" + flat: true + contentScale: 0.8 + width: 70 * scaling + height: 65 * scaling + Layout.alignment: Qt.AlignHCenter + } + NCircleStat { + value: SystemStatService.diskPercent + icon: "storage" + flat: true + contentScale: 0.8 + width: 70 * scaling + height: 65 * scaling + Layout.alignment: Qt.AlignHCenter + } } } } diff --git a/Modules/ControlCenter/Cards/WeatherCard.qml b/Modules/ControlCenter/Cards/WeatherCard.qml index 8e6257a1..9b7d6aec 100644 --- a/Modules/ControlCenter/Cards/WeatherCard.qml +++ b/Modules/ControlCenter/Cards/WeatherCard.qml @@ -85,7 +85,7 @@ NBox { model: weatherReady ? LocationService.data.weather.daily.time : [] delegate: ColumnLayout { Layout.alignment: Qt.AlignHCenter - spacing: Style.marginL * scaling + spacing: Style.marginXS * scaling NText { text: { var weatherDate = new Date(LocationService.data.weather.daily.time[index].replace(/-/g, "/")) diff --git a/Modules/ControlCenter/ControlCenterPanel.qml b/Modules/ControlCenter/ControlCenterPanel.qml index aa9a8b47..07a2cf4e 100644 --- a/Modules/ControlCenter/ControlCenterPanel.qml +++ b/Modules/ControlCenter/ControlCenterPanel.qml @@ -11,7 +11,7 @@ NPanel { id: root preferredWidth: 460 - preferredHeight: 734 + preferredHeight: 740 panelKeyboardFocus: true // Positioning @@ -44,13 +44,13 @@ NPanel { WeatherCard { Layout.fillWidth: true - Layout.preferredHeight: Math.max(220 * scaling) + Layout.preferredHeight: Math.max(190 * scaling) } // Middle section: media + stats column RowLayout { Layout.fillWidth: true - Layout.preferredHeight: Math.max(310 * scaling) + Layout.preferredHeight: Math.max(260 * scaling) spacing: content.cardSpacing // Media card @@ -66,6 +66,12 @@ NPanel { } } + // Audio card below media and system monitor + AudioCard { + Layout.fillWidth: true + Layout.preferredHeight: Math.max(120 * scaling) + } + // Bottom actions (two grouped rows of round buttons) RowLayout { Layout.fillWidth: true From c487f1982e6d4c278aec5088180905cae140ac8c Mon Sep 17 00:00:00 2001 From: lysec Date: Sat, 11 Oct 2025 17:04:33 +0200 Subject: [PATCH 07/17] AudioCard: remove spacing between in/output ControlCenterPanel: fix height --- Modules/ControlCenter/Cards/AudioCard.qml | 2 +- Modules/ControlCenter/ControlCenterPanel.qml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/ControlCenter/Cards/AudioCard.qml b/Modules/ControlCenter/Cards/AudioCard.qml index efa0117a..8436aa13 100644 --- a/Modules/ControlCenter/Cards/AudioCard.qml +++ b/Modules/ControlCenter/Cards/AudioCard.qml @@ -42,7 +42,7 @@ NBox { ColumnLayout { anchors.fill: parent anchors.margins: Style.marginM * scaling - spacing: Style.marginM * scaling + spacing: 0 // Output Volume Section ColumnLayout { diff --git a/Modules/ControlCenter/ControlCenterPanel.qml b/Modules/ControlCenter/ControlCenterPanel.qml index 07a2cf4e..19e6e30a 100644 --- a/Modules/ControlCenter/ControlCenterPanel.qml +++ b/Modules/ControlCenter/ControlCenterPanel.qml @@ -11,7 +11,7 @@ NPanel { id: root preferredWidth: 460 - preferredHeight: 740 + preferredHeight: 790 panelKeyboardFocus: true // Positioning From df6fdf36561226940ba0717843220e5f033b9842 Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Sat, 11 Oct 2025 11:28:03 -0400 Subject: [PATCH 08/17] Fix NPanel scaling on Qt6.10 --- Widgets/NPanel.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 9c7bf2db..e5fae6cd 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -143,8 +143,9 @@ Loader { readonly property real verticalBarWidth: Math.round(Style.barHeight * scaling) Component.onCompleted: { - Logger.log("NPanel", "Opened", root.objectName) + Logger.log("NPanel", "Opened", root.objectName, "on", screen.name) dimmingOpacity = Style.opacityHeavy + root.scaling = scaling = ScalingService.getScreenScale(screen) } Connections { From 3dff242606ba4fe3e9bfde574cc592070d3d8e48 Mon Sep 17 00:00:00 2001 From: lysec Date: Sat, 11 Oct 2025 18:18:43 +0200 Subject: [PATCH 09/17] OSD: fix vertical layout autoformat --- Modules/ControlCenter/Cards/AudioCard.qml | 1 + Modules/ControlCenter/Cards/SystemMonitorCard.qml | 10 ++++++---- Modules/ControlCenter/ControlCenterPanel.qml | 2 ++ Modules/OSD/OSD.qml | 14 +++++--------- Widgets/NCircleStat.qml | 1 + 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/Modules/ControlCenter/Cards/AudioCard.qml b/Modules/ControlCenter/Cards/AudioCard.qml index 8436aa13..a4c975fe 100644 --- a/Modules/ControlCenter/Cards/AudioCard.qml +++ b/Modules/ControlCenter/Cards/AudioCard.qml @@ -9,6 +9,7 @@ import qs.Widgets NBox { id: root + property real scaling: 1.0 property real localOutputVolume: AudioService.volume property real localInputVolume: AudioService.inputVolume diff --git a/Modules/ControlCenter/Cards/SystemMonitorCard.qml b/Modules/ControlCenter/Cards/SystemMonitorCard.qml index 4e7254a3..a0772e96 100644 --- a/Modules/ControlCenter/Cards/SystemMonitorCard.qml +++ b/Modules/ControlCenter/Cards/SystemMonitorCard.qml @@ -9,6 +9,8 @@ import qs.Widgets NBox { id: root + property real scaling: 1.0 + Item { id: content anchors.fill: parent @@ -23,8 +25,8 @@ NBox { icon: "cpu-usage" flat: true contentScale: 0.8 - width: 70 * scaling height: 65 * scaling + scaling: root.scaling Layout.alignment: Qt.AlignHCenter } NCircleStat { @@ -33,8 +35,8 @@ NBox { icon: "cpu-temperature" flat: true contentScale: 0.8 - width: 70 * scaling height: 65 * scaling + scaling: root.scaling Layout.alignment: Qt.AlignHCenter } NCircleStat { @@ -42,8 +44,8 @@ NBox { icon: "memory" flat: true contentScale: 0.8 - width: 70 * scaling height: 65 * scaling + scaling: root.scaling Layout.alignment: Qt.AlignHCenter } NCircleStat { @@ -51,8 +53,8 @@ NBox { icon: "storage" flat: true contentScale: 0.8 - width: 70 * scaling height: 65 * scaling + scaling: root.scaling Layout.alignment: Qt.AlignHCenter } } diff --git a/Modules/ControlCenter/ControlCenterPanel.qml b/Modules/ControlCenter/ControlCenterPanel.qml index 19e6e30a..28e9b49b 100644 --- a/Modules/ControlCenter/ControlCenterPanel.qml +++ b/Modules/ControlCenter/ControlCenterPanel.qml @@ -63,6 +63,7 @@ NPanel { SystemMonitorCard { Layout.preferredWidth: Style.baseWidgetSize * 2.625 * scaling Layout.fillHeight: true + scaling: root.scaling } } @@ -70,6 +71,7 @@ NPanel { AudioCard { Layout.fillWidth: true Layout.preferredHeight: Math.max(120 * scaling) + scaling: root.scaling } // Bottom actions (two grouped rows of round buttons) diff --git a/Modules/OSD/OSD.qml b/Modules/OSD/OSD.qml index a4ec5701..0ba637df 100644 --- a/Modules/OSD/OSD.qml +++ b/Modules/OSD/OSD.qml @@ -348,15 +348,11 @@ Variants { })() property int vMarginTop: Math.max(Math.round(osdItem.radius), Math.round(Style.marginS * root.scaling)) property int balanceDelta: Math.round(Style.marginS * root.scaling) - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - anchors.bottom: parent.bottom + anchors.fill: parent anchors.topMargin: vMarginTop - anchors.bottomMargin: vMargin - width: (function () { - const w = parent.width - (vMargin * 2) - return (w % 2 === 0) ? w : w - 1 - })() + anchors.leftMargin: vMargin + anchors.rightMargin: vMargin + anchors.bottomMargin: 0 spacing: Math.round(Style.marginS * root.scaling) // Percentage text at top @@ -418,7 +414,7 @@ Variants { color: root.getIconColor() pointSize: Style.fontSizeXL * root.scaling Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom - Layout.bottomMargin: vMargin + Math.round(Style.marginM * root.scaling) + balanceDelta + Layout.bottomMargin: Math.round(Style.marginS * root.scaling) Behavior on color { ColorAnimation { duration: Style.animationNormal diff --git a/Widgets/NCircleStat.qml b/Widgets/NCircleStat.qml index f6d4332d..b5cb53a0 100644 --- a/Widgets/NCircleStat.qml +++ b/Widgets/NCircleStat.qml @@ -8,6 +8,7 @@ import qs.Widgets Rectangle { id: root + property real scaling: 1.0 property real value: 0 // 0..100 (or any range visually mapped) property string icon: "" property string suffix: "%" From 28c2352b31532371edd7640f2fbbe351ad7ed3c1 Mon Sep 17 00:00:00 2001 From: lysec Date: Sat, 11 Oct 2025 18:24:13 +0200 Subject: [PATCH 10/17] OSD: fix centering --- Modules/OSD/OSD.qml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Modules/OSD/OSD.qml b/Modules/OSD/OSD.qml index 0ba637df..414aa7f7 100644 --- a/Modules/OSD/OSD.qml +++ b/Modules/OSD/OSD.qml @@ -349,10 +349,10 @@ Variants { property int vMarginTop: Math.max(Math.round(osdItem.radius), Math.round(Style.marginS * root.scaling)) property int balanceDelta: Math.round(Style.marginS * root.scaling) anchors.fill: parent - anchors.topMargin: vMarginTop + anchors.topMargin: vMargin anchors.leftMargin: vMargin anchors.rightMargin: vMargin - anchors.bottomMargin: 0 + anchors.bottomMargin: vMargin spacing: Math.round(Style.marginS * root.scaling) // Percentage text at top @@ -414,7 +414,6 @@ Variants { color: root.getIconColor() pointSize: Style.fontSizeXL * root.scaling Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom - Layout.bottomMargin: Math.round(Style.marginS * root.scaling) Behavior on color { ColorAnimation { duration: Style.animationNormal From c9729789a788ae266fce257fa08b49067362a6f5 Mon Sep 17 00:00:00 2001 From: lysec Date: Sat, 11 Oct 2025 18:36:39 +0200 Subject: [PATCH 11/17] Launcher: fix signal issue --- Modules/Launcher/Launcher.qml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Modules/Launcher/Launcher.qml b/Modules/Launcher/Launcher.qml index f6ec7ae9..1c056b05 100644 --- a/Modules/Launcher/Launcher.qml +++ b/Modules/Launcher/Launcher.qml @@ -192,10 +192,8 @@ NPanel { // Reset when launcher opens Connections { target: root - function onOpenedChanged() { - if (root.opened) { - mouseMovementDetector.initialized = false - } + function onOpened() { + mouseMovementDetector.initialized = false } } } From e3582398627d914b28b16f4ba1badb044621330b Mon Sep 17 00:00:00 2001 From: lysec Date: Sat, 11 Oct 2025 19:14:23 +0200 Subject: [PATCH 12/17] OSD: fix display logic --- Modules/OSD/OSD.qml | 9 ++++++++- Services/AudioService.qml | 8 ++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/Modules/OSD/OSD.qml b/Modules/OSD/OSD.qml index 414aa7f7..dfd9aa56 100644 --- a/Modules/OSD/OSD.qml +++ b/Modules/OSD/OSD.qml @@ -41,6 +41,7 @@ Variants { // Brightness properties property bool brightnessInitialized: false + property int brightnessChangeCount: 0 readonly property real currentBrightness: { if (BrightnessService.monitors.length > 0) { return BrightnessService.monitors[0].brightness || 0 @@ -508,6 +509,8 @@ Variants { muteInitialized = true inputVolumeInitialized = true inputMuteInitialized = true + // Don't initialize brightness here - let it initialize on first change like volume + connectBrightnessMonitors() } } @@ -528,6 +531,7 @@ Variants { } function connectBrightnessMonitors() { + brightnessChangeCount = 0 // Reset change count when reconnecting for (var i = 0; i < BrightnessService.monitors.length; i++) { let monitor = BrightnessService.monitors[i] // Disconnect first to avoid duplicate connections @@ -537,7 +541,10 @@ Variants { } function onBrightnessChanged(newBrightness) { - if (!brightnessInitialized) { + brightnessChangeCount++ + + if (brightnessChangeCount <= BrightnessService.monitors.length) { + // This is likely the initial brightness value(s), don't show OSD brightnessInitialized = true } else { showOSD("brightness") diff --git a/Services/AudioService.qml b/Services/AudioService.qml index f600f9d9..c187c87f 100644 --- a/Services/AudioService.qml +++ b/Services/AudioService.qml @@ -111,6 +111,14 @@ Singleton { } } + function increaseInputVolume() { + setInputVolume(inputVolume + stepVolume) + } + + function decreaseInputVolume() { + setInputVolume(inputVolume - stepVolume) + } + function setInputVolume(newVolume: real) { if (source?.ready && source?.audio) { // Clamp it accordingly From f9c9d00b60d1429e35a47335e6ed4f21204e0e5f Mon Sep 17 00:00:00 2001 From: lysec Date: Sat, 11 Oct 2025 19:16:37 +0200 Subject: [PATCH 13/17] OSD: add always on top setting --- Assets/Translations/de.json | 4 ++++ Assets/Translations/en.json | 4 ++++ Assets/Translations/es.json | 4 ++++ Assets/Translations/fr.json | 4 ++++ Assets/Translations/pt.json | 4 ++++ Assets/Translations/zh-CN.json | 4 ++++ Commons/Settings.qml | 1 + Modules/OSD/OSD.qml | 1 + Modules/Settings/Tabs/OsdTab.qml | 7 +++++++ 9 files changed, 33 insertions(+) diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index 369eee4a..d62d629c 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -426,6 +426,10 @@ "label": "Bildschirmanzeige aktivieren", "description": "Lautstärke- und Helligkeitsänderungen in Echtzeit anzeigen." }, + "always-on-top": { + "label": "Immer im Vordergrund", + "description": "Bildschirmanzeige über Vollbildfenstern und anderen Ebenen anzeigen." + }, "location": { "label": "Position", "description": "Wo Bildschirmanzeigen erscheinen." diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index a90e24a7..30f62d4c 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -426,6 +426,10 @@ "label": "Enable on screen display", "description": "Show volume and brightness changes in real-time." }, + "always-on-top": { + "label": "Always on top", + "description": "Display OSD above fullscreen windows and other layers." + }, "location": { "label": "Location", "description": "Where on-screen displays appear." diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index 8c79a64f..ee6c4843 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -426,6 +426,10 @@ "label": "Activar visualización en pantalla", "description": "Mostrar cambios de volumen y brillo en tiempo real." }, + "always-on-top": { + "label": "Siempre encima", + "description": "Mostrar OSD por encima de ventanas de pantalla completa y otras capas." + }, "location": { "label": "Ubicación", "description": "Dónde aparece la visualización en pantalla." diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index 32c01982..e79310b5 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -426,6 +426,10 @@ "label": "Activer l'affichage à l'écran", "description": "Afficher en temps réel les changements de volume et de luminosité." }, + "always-on-top": { + "label": "Toujours au premier plan", + "description": "Afficher l'OSD au-dessus des fenêtres plein écran et autres couches." + }, "location": { "label": "Emplacement", "description": "Emplacement des affichages à l'écran." diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index 67fa8ead..efe785a6 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -766,6 +766,10 @@ "label": "Ativar exibição na tela", "description": "Mostrar alterações de volume e brilho em tempo real." }, + "always-on-top": { + "label": "Sempre no topo", + "description": "Exibir OSD acima de janelas em tela cheia e outras camadas." + }, "location": { "label": "Localização", "description": "Onde a exibição na tela aparece." diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index 9f421d69..2efff789 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -426,6 +426,10 @@ "label": "启用屏幕显示", "description": "实时显示音量与亮度变化。" }, + "always-on-top": { + "label": "始终置顶", + "description": "在全屏窗口和其他图层之上显示OSD。" + }, "location": { "label": "位置", "description": "屏幕显示出现的位置。" diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 8375d67a..18a9286f 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -297,6 +297,7 @@ Singleton { property string location: "top_right" property list monitors: [] property int autoHideMs: 2000 + property bool alwaysOnTop: false } // audio diff --git a/Modules/OSD/OSD.qml b/Modules/OSD/OSD.qml index dfd9aa56..aef4ac37 100644 --- a/Modules/OSD/OSD.qml +++ b/Modules/OSD/OSD.qml @@ -204,6 +204,7 @@ Variants { color: Color.transparent WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + WlrLayershell.layer: (Settings.data.osd && Settings.data.osd.alwaysOnTop) ? WlrLayer.Overlay : WlrLayer.Top exclusionMode: PanelWindow.ExclusionMode.Ignore Rectangle { diff --git a/Modules/Settings/Tabs/OsdTab.qml b/Modules/Settings/Tabs/OsdTab.qml index 93e4a733..785fad63 100644 --- a/Modules/Settings/Tabs/OsdTab.qml +++ b/Modules/Settings/Tabs/OsdTab.qml @@ -83,6 +83,13 @@ ColumnLayout { onToggled: checked => Settings.data.osd.enabled = checked } + NToggle { + label: I18n.tr("settings.osd.always-on-top.label") + description: I18n.tr("settings.osd.always-on-top.description") + checked: Settings.data.osd.alwaysOnTop + onToggled: checked => Settings.data.osd.alwaysOnTop = checked + } + NLabel { label: I18n.tr("settings.osd.duration.auto-hide.label") description: I18n.tr("settings.osd.duration.auto-hide.description") From b9e8e8becb164da8704dff347ffce63d3f11f826 Mon Sep 17 00:00:00 2001 From: lysec Date: Sat, 11 Oct 2025 19:22:12 +0200 Subject: [PATCH 14/17] OSD: possible layout fix, reintroduce scaling --- Modules/OSD/OSD.qml | 43 ++++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/Modules/OSD/OSD.qml b/Modules/OSD/OSD.qml index aef4ac37..552d44c3 100644 --- a/Modules/OSD/OSD.qml +++ b/Modules/OSD/OSD.qml @@ -123,6 +123,9 @@ Variants { sourceComponent: PanelWindow { id: panel screen: modelData + + // PanelWindow scaling + property real scaling: ScalingService.getScreenScale(screen) readonly property string location: (Settings.data.osd && Settings.data.osd.location) ? Settings.data.osd.location : "top_right" readonly property bool isTop: (location === "top") || (location.length >= 3 && location.substring(0, 3) === "top") @@ -131,11 +134,12 @@ Variants { readonly property bool isRight: (location.indexOf("_right") >= 0) || (location === "right") readonly property bool isCentered: (location === "top" || location === "bottom") readonly property bool verticalMode: (location === "left" || location === "right") - readonly property int hWidth: Math.round(320 * root.scaling) - readonly property int hHeight: Math.round(64 * root.scaling) + readonly property int hWidth: Math.round(320 * scaling) + readonly property int hHeight: Math.round(64 * scaling) + readonly property int vHeight: Math.round(320 * scaling) // Vertical OSD height (matches horizontal width) // Ensure an even width to keep the vertical bar perfectly centered readonly property int barThickness: (function () { - const base = Math.max(6, Math.round(6 * root.scaling)) + const base = Math.max(8, Math.round(8 * scaling)) return (base % 2 === 0) ? base : base + 1 })() @@ -143,6 +147,15 @@ Variants { connectBrightnessMonitors() } + Connections { + target: ScalingService + function onScaleChanged(screenName, scale) { + if ((screen !== null) && (screenName === screen.name)) { + scaling = scale + } + } + } + Component.onDestruction: { disconnectBrightnessMonitors() } @@ -211,12 +224,12 @@ Variants { id: osdItem width: parent.width - height: panel.verticalMode ? panel.hWidth : Math.round(64 * root.scaling) - radius: Style.radiusL * root.scaling + height: panel.verticalMode ? panel.vHeight : Math.round(64 * scaling) + radius: Style.radiusL * scaling color: Color.mSurface border.color: Color.mOutline border.width: (function () { - const bw = Math.max(2, Math.round(Style.borderM * root.scaling)) + const bw = Math.max(2, Math.round(Style.borderM * scaling)) return (bw % 2 === 0) ? bw : bw + 1 })() visible: false @@ -282,7 +295,7 @@ Variants { NIcon { icon: root.getIcon() color: root.getIconColor() - pointSize: Style.fontSizeXL * root.scaling + pointSize: Style.fontSizeXL * scaling Layout.alignment: Qt.AlignVCenter Behavior on color { @@ -328,7 +341,7 @@ Variants { NText { text: root.getDisplayPercentage() color: Color.mOnSurface - pointSize: Style.fontSizeS * root.scaling + pointSize: Style.fontSizeS * scaling family: Settings.data.ui.fontFixed Layout.alignment: Qt.AlignVCenter horizontalAlignment: Text.AlignLeft @@ -344,18 +357,18 @@ Variants { ColumnLayout { // Ensure inner padding respects the rounded corners; avoid clipping the icon/text property int vMargin: (function () { - const styleMargin = Math.round(Style.marginL * root.scaling) + const styleMargin = Math.round(Style.marginL * scaling) const cornerGuard = Math.round(osdItem.radius) return Math.max(styleMargin, cornerGuard) })() - property int vMarginTop: Math.max(Math.round(osdItem.radius), Math.round(Style.marginS * root.scaling)) - property int balanceDelta: Math.round(Style.marginS * root.scaling) + property int vMarginTop: Math.max(Math.round(osdItem.radius), Math.round(Style.marginS * scaling)) + property int balanceDelta: Math.round(Style.marginS * scaling) anchors.fill: parent anchors.topMargin: vMargin anchors.leftMargin: vMargin anchors.rightMargin: vMargin anchors.bottomMargin: vMargin - spacing: Math.round(Style.marginS * root.scaling) + spacing: Math.round(Style.marginS * scaling) // Percentage text at top Item { @@ -365,7 +378,7 @@ Variants { id: percentText text: root.getDisplayPercentage() color: Color.mOnSurface - pointSize: Style.fontSizeS * root.scaling + pointSize: Style.fontSizeS * scaling family: Settings.data.ui.fontFixed anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter @@ -377,7 +390,7 @@ Variants { // Progress bar Item { Layout.fillWidth: true - Layout.fillHeight: true + Layout.fillHeight: true // Fill remaining space between text and icon Rectangle { anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top @@ -414,7 +427,7 @@ Variants { NIcon { icon: root.getIcon() color: root.getIconColor() - pointSize: Style.fontSizeXL * root.scaling + pointSize: Style.fontSizeXL * scaling Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom Behavior on color { ColorAnimation { From a5341691c8c4582347554627bdbe82430dcbccb5 Mon Sep 17 00:00:00 2001 From: lysec Date: Sat, 11 Oct 2025 19:22:32 +0200 Subject: [PATCH 15/17] Autoformat --- Modules/OSD/OSD.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/OSD/OSD.qml b/Modules/OSD/OSD.qml index 552d44c3..6643d88c 100644 --- a/Modules/OSD/OSD.qml +++ b/Modules/OSD/OSD.qml @@ -123,7 +123,7 @@ Variants { sourceComponent: PanelWindow { id: panel screen: modelData - + // PanelWindow scaling property real scaling: ScalingService.getScreenScale(screen) From 26099bb8fb1e211e8c2c7b74e90546d190d90cfe Mon Sep 17 00:00:00 2001 From: lysec Date: Sat, 11 Oct 2025 20:10:47 +0200 Subject: [PATCH 16/17] Release v2.17.3 --- Services/UpdateService.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Services/UpdateService.qml b/Services/UpdateService.qml index b826fc69..3689cc1e 100644 --- a/Services/UpdateService.qml +++ b/Services/UpdateService.qml @@ -8,8 +8,8 @@ Singleton { id: root // Public properties - property string baseVersion: "2.17.2" - property bool isDevelopment: true + property string baseVersion: "2.17.3" + property bool isDevelopment: false property string currentVersion: `v${!isDevelopment ? baseVersion : baseVersion + "-dev"}` From ce5208fc7c5aeca34a993a127fa4627968381b8e Mon Sep 17 00:00:00 2001 From: lysec Date: Sat, 11 Oct 2025 20:13:10 +0200 Subject: [PATCH 17/17] Set version to dev --- Services/UpdateService.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Services/UpdateService.qml b/Services/UpdateService.qml index 3689cc1e..c738d6e3 100644 --- a/Services/UpdateService.qml +++ b/Services/UpdateService.qml @@ -9,7 +9,7 @@ Singleton { // Public properties property string baseVersion: "2.17.3" - property bool isDevelopment: false + property bool isDevelopment: true property string currentVersion: `v${!isDevelopment ? baseVersion : baseVersion + "-dev"}`