diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index a930f9fa..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." @@ -1411,7 +1415,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/en.json b/Assets/Translations/en.json index c4ff50ae..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." @@ -1394,7 +1398,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/Assets/Translations/es.json b/Assets/Translations/es.json index 604303c3..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." @@ -1394,7 +1398,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..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." @@ -1393,7 +1397,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..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." @@ -1394,7 +1398,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..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": "屏幕显示出现的位置。" @@ -1394,7 +1398,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}%", 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/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/Bar/Calendar/CalendarPanel.qml b/Modules/Bar/Calendar/CalendarPanel.qml index 64c68738..d1a00070 100644 --- a/Modules/Bar/Calendar/CalendarPanel.qml +++ b/Modules/Bar/Calendar/CalendarPanel.qml @@ -91,7 +91,7 @@ NPanel { } } - // Today day number + // Today day number - with simple, stable animation NText { opacity: content.isCurrentMonth ? 1.0 : 0.0 Layout.preferredWidth: content.isCurrentMonth ? implicitWidth : 0 @@ -104,8 +104,17 @@ NPanel { font.weight: Style.fontWeightBold color: Color.mOnPrimary - Behavior on opacity { NumberAnimation { duration: Style.animationFast } } - Behavior on Layout.preferredWidth { NumberAnimation { duration: Style.animationFast; easing.type: Easing.InOutQuad } } + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + Behavior on Layout.preferredWidth { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.InOutQuad + } + } } // Month, year, location @@ -163,7 +172,7 @@ NPanel { } } - // Spacer between date and clock + // Spacer to push content left Item { Layout.fillWidth: true } @@ -227,6 +236,26 @@ NPanel { var t = Settings.data.location.use12hourFormat ? Qt.locale().toString(new Date(), "hh AP") : Qt.locale().toString(new Date(), "HH") return t.split(" ")[0] } + } + 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() + } + } pointSize: Style.fontSizeXS * scaling font.weight: Style.fontWeightBold @@ -247,13 +276,12 @@ NPanel { } } - // 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 { @@ -261,7 +289,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, "/")) @@ -272,14 +299,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: { @@ -300,27 +325,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: { @@ -330,7 +347,6 @@ NPanel { content.isCurrentMonth = content.checkIsCurrentMonth() } } - NIconButton { icon: "calendar" onClicked: { @@ -339,7 +355,6 @@ NPanel { content.isCurrentMonth = true } } - NIconButton { icon: "chevron-right" onClicked: { @@ -350,31 +365,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: { @@ -391,27 +399,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 @@ -443,27 +444,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 @@ -478,7 +473,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/ControlCenter/Cards/AudioCard.qml b/Modules/ControlCenter/Cards/AudioCard.qml index efa0117a..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 @@ -42,7 +43,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/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..a0772e96 100644 --- a/Modules/ControlCenter/Cards/SystemMonitorCard.qml +++ b/Modules/ControlCenter/Cards/SystemMonitorCard.qml @@ -9,49 +9,54 @@ import qs.Widgets NBox { id: root - ColumnLayout { - 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 + property real scaling: 1.0 - 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 + Item { + id: content + anchors.fill: parent + anchors.margins: Style.marginS * scaling + + ColumnLayout { + anchors.centerIn: parent + spacing: 0 + + NCircleStat { + value: SystemStatService.cpuUsage + icon: "cpu-usage" + flat: true + contentScale: 0.8 + height: 65 * scaling + scaling: root.scaling + Layout.alignment: Qt.AlignHCenter + } + NCircleStat { + value: SystemStatService.cpuTemp + suffix: "°C" + icon: "cpu-temperature" + flat: true + contentScale: 0.8 + height: 65 * scaling + scaling: root.scaling + Layout.alignment: Qt.AlignHCenter + } + NCircleStat { + value: SystemStatService.memPercent + icon: "memory" + flat: true + contentScale: 0.8 + height: 65 * scaling + scaling: root.scaling + Layout.alignment: Qt.AlignHCenter + } + NCircleStat { + value: SystemStatService.diskPercent + icon: "storage" + flat: true + contentScale: 0.8 + height: 65 * scaling + scaling: root.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..28e9b49b 100644 --- a/Modules/ControlCenter/ControlCenterPanel.qml +++ b/Modules/ControlCenter/ControlCenterPanel.qml @@ -11,7 +11,7 @@ NPanel { id: root preferredWidth: 460 - preferredHeight: 734 + preferredHeight: 790 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 @@ -63,9 +63,17 @@ NPanel { SystemMonitorCard { Layout.preferredWidth: Style.baseWidgetSize * 2.625 * scaling Layout.fillHeight: true + scaling: root.scaling } } + // Audio card below media and system monitor + AudioCard { + Layout.fillWidth: true + Layout.preferredHeight: Math.max(120 * scaling) + scaling: root.scaling + } + // Bottom actions (two grouped rows of round buttons) RowLayout { Layout.fillWidth: true 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 } } } diff --git a/Modules/OSD/OSD.qml b/Modules/OSD/OSD.qml index a4ec5701..6643d88c 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 @@ -123,6 +124,9 @@ Variants { 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") readonly property bool isBottom: (location === "bottom") || (location.length >= 6 && location.substring(0, 6) === "bottom") @@ -130,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 })() @@ -142,6 +147,15 @@ Variants { connectBrightnessMonitors() } + Connections { + target: ScalingService + function onScaleChanged(screenName, scale) { + if ((screen !== null) && (screenName === screen.name)) { + scaling = scale + } + } + } + Component.onDestruction: { disconnectBrightnessMonitors() } @@ -203,18 +217,19 @@ 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 { 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 @@ -280,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 { @@ -326,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 @@ -342,22 +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) - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.topMargin: vMarginTop + 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 - width: (function () { - const w = parent.width - (vMargin * 2) - return (w % 2 === 0) ? w : w - 1 - })() - spacing: Math.round(Style.marginS * root.scaling) + spacing: Math.round(Style.marginS * scaling) // Percentage text at top Item { @@ -367,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 @@ -379,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 @@ -416,9 +427,8 @@ Variants { NIcon { icon: root.getIcon() color: root.getIconColor() - pointSize: Style.fontSizeXL * root.scaling + pointSize: Style.fontSizeXL * scaling Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom - Layout.bottomMargin: vMargin + Math.round(Style.marginM * root.scaling) + balanceDelta Behavior on color { ColorAnimation { duration: Style.animationNormal @@ -513,6 +523,8 @@ Variants { muteInitialized = true inputVolumeInitialized = true inputMuteInitialized = true + // Don't initialize brightness here - let it initialize on first change like volume + connectBrightnessMonitors() } } @@ -533,6 +545,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 @@ -542,7 +555,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/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/ColorSchemeTab.qml b/Modules/Settings/Tabs/ColorSchemeTab.qml index d8e8f338..a5c66ead 100644 --- a/Modules/Settings/Tabs/ColorSchemeTab.qml +++ b/Modules/Settings/Tabs/ColorSchemeTab.qml @@ -11,16 +11,16 @@ ColumnLayout { // Cache for scheme JSON (can be flat or {dark, light}) property var schemeColorsCache: ({}) + 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") { @@ -33,39 +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" + // 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 + 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++ } } @@ -77,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")) } } @@ -91,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 @@ -99,21 +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: true + 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) } } } @@ -127,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 @@ -143,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) } } @@ -195,7 +205,6 @@ ColumnLayout { onSelected: key => { Settings.data.colorSchemes.matugenSchemeType = key - AppThemeService.generate() } } @@ -232,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: getSchemeColor(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) { @@ -255,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 @@ -271,28 +280,28 @@ ColumnLayout { width: 14 * scaling height: 14 * scaling radius: width * 0.5 - color: getSchemeColor(modelData, "mPrimary") + color: root.getSchemeColor(schemeItem.schemeName, "mPrimary") } Rectangle { width: 14 * scaling height: 14 * scaling radius: width * 0.5 - color: getSchemeColor(modelData, "mSecondary") + color: root.getSchemeColor(schemeItem.schemeName, "mSecondary") } Rectangle { width: 14 * scaling height: 14 * scaling radius: width * 0.5 - color: getSchemeColor(modelData, "mTertiary") + color: root.getSchemeColor(schemeItem.schemeName, "mTertiary") } Rectangle { width: 14 * scaling height: 14 * scaling radius: width * 0.5 - color: getSchemeColor(modelData, "mError") + color: root.getSchemeColor(schemeItem.schemeName, "mError") } } @@ -305,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 @@ -350,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) } 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/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 { 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") 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" 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 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) + } } } diff --git a/Services/UpdateService.qml b/Services/UpdateService.qml index b826fc69..c738d6e3 100644 --- a/Services/UpdateService.qml +++ b/Services/UpdateService.qml @@ -8,7 +8,7 @@ Singleton { id: root // Public properties - property string baseVersion: "2.17.2" + property string baseVersion: "2.17.3" property bool isDevelopment: true property string currentVersion: `v${!isDevelopment ? baseVersion : baseVersion + "-dev"}` 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: "%" 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 {