From 36d4c10202809ce581bceaff16b062be0683046b Mon Sep 17 00:00:00 2001 From: mochou Date: Thu, 13 Nov 2025 13:21:09 +0800 Subject: [PATCH] feat: Add OSD background opacity setting - Add `OSD` background transparency - Add `OSD` background transparency translation --- Assets/Translations/de.json | 4 + Assets/Translations/en.json | 4 + Assets/Translations/es.json | 4 + Assets/Translations/fr.json | 4 + Assets/Translations/nl.json | 4 + Assets/Translations/pt.json | 4 + Assets/Translations/ru.json | 4 + Assets/Translations/tr.json | 4 + Assets/Translations/uk-UA.json | 4 + Assets/Translations/zh-CN.json | 5 + Assets/settings-default.json | 3 +- Commons/Settings.qml | 1 + Modules/OSD/OSD.qml | 229 ++++++++++++------------ Modules/Panels/Settings/Tabs/OsdTab.qml | 76 +++++--- 14 files changed, 209 insertions(+), 141 deletions(-) diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index d7aea21c..a40b6585 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -1440,6 +1440,10 @@ "description": "On-Screen Display über Vollbildfenstern und anderen Ebenen anzeigen.", "label": "Immer im Vordergrund" }, + "background-opacity": { + "description": "Controls the transparency of the OSD background.", + "label": "Background opacity" + }, "description": "Bildschirm-Overlays wie Lautstärke- und Helligkeitsanzeigen konfigurieren.", "duration": { "auto-hide": { diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index bb26852e..105cd572 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -1440,6 +1440,10 @@ "description": "Display OSD above fullscreen windows and other layers.", "label": "Always on top" }, + "background-opacity": { + "description": "Controls the transparency of the OSD background.", + "label": "Background opacity" + }, "description": "Configure on-screen indicators such as volume and brightness overlays.", "duration": { "auto-hide": { diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index b556ddeb..e92bc358 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -1440,6 +1440,10 @@ "description": "Mostrar OSD por encima de ventanas de pantalla completa y otras capas.", "label": "Siempre encima" }, + "background-opacity": { + "description": "Controls the transparency of the OSD background.", + "label": "Background opacity" + }, "description": "Configura indicadores superpuestos como volumen y brillo.", "duration": { "auto-hide": { diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index ebeba4ca..90d6e5f5 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -1440,6 +1440,10 @@ "description": "Afficher l'OSD au-dessus des fenêtres plein écran et autres couches.", "label": "Toujours au premier plan" }, + "background-opacity": { + "description": "Contrôle la transparence de l'arrière-plan de l'OSD.", + "label": "Opacité de l'arrière-plan" + }, "description": "Configurer les indicateurs à l'écran tels que le volume et la luminosité.", "duration": { "auto-hide": { diff --git a/Assets/Translations/nl.json b/Assets/Translations/nl.json index 2c87c2f6..acd1897a 100644 --- a/Assets/Translations/nl.json +++ b/Assets/Translations/nl.json @@ -1440,6 +1440,10 @@ "description": "Toon OSD boven fullscreen-vensters en andere lagen.", "label": "Altijd bovenop" }, + "background-opacity": { + "description": "Regelt de transparantie van de OSD-achtergrond.", + "label": "Achtergrond dekkendheid" + }, "description": "Configureer on-screenindicatoren zoals overlays voor volume en helderheid.", "duration": { "auto-hide": { diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index b24bc15e..642ae508 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -1440,6 +1440,10 @@ "description": "Exibir OSD acima de janelas em tela cheia e outras camadas.", "label": "Sempre no topo" }, + "background-opacity": { + "description": "Controla a transparência do fundo do OSD.", + "label": "Opacidade do fundo" + }, "description": "Configure indicadores na tela, como volume e brilho.", "duration": { "auto-hide": { diff --git a/Assets/Translations/ru.json b/Assets/Translations/ru.json index 3d0b9830..f76968f5 100644 --- a/Assets/Translations/ru.json +++ b/Assets/Translations/ru.json @@ -1440,6 +1440,10 @@ "description": "Отображать OSD поверх полноэкранных окон и других слоев.", "label": "Всегда сверху" }, + "background-opacity": { + "description": "Управляет прозрачностью фона OSD.", + "label": "Прозрачность фона" + }, "description": "Настройка экранных индикаторов, таких как оверлеи громкости и яркости.", "duration": { "auto-hide": { diff --git a/Assets/Translations/tr.json b/Assets/Translations/tr.json index 560d859a..5347d72a 100644 --- a/Assets/Translations/tr.json +++ b/Assets/Translations/tr.json @@ -1440,6 +1440,10 @@ "description": "OSD'yi tam ekran pencerelerinin ve diğer katmanların üzerinde görüntüle.", "label": "Her zaman üstte" }, + "background-opacity": { + "description": "OSD arka planının şeffaflığını kontrol eder.", + "label": "Arka plan opaklığı" + }, "description": "Ses ve parlaklık kaplamaları gibi ekran göstergelerini yapılandırın.", "duration": { "auto-hide": { diff --git a/Assets/Translations/uk-UA.json b/Assets/Translations/uk-UA.json index 9223e582..cbbb30b7 100644 --- a/Assets/Translations/uk-UA.json +++ b/Assets/Translations/uk-UA.json @@ -1440,6 +1440,10 @@ "description": "Відображати екранну індикацію поверх повноекранних вікон та інших шарів.", "label": "Завжди зверху" }, + "background-opacity": { + "description": "Керує прозорістю фону OSD.", + "label": "Прозорість фону" + }, "description": "Налаштуйте екранні індикатори, такі як накладки гучності та яскравості.", "duration": { "auto-hide": { diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index 197d2533..1bc217de 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -1440,6 +1440,10 @@ "description": "在全屏窗口和其他图层之上显示OSD。", "label": "始终置顶" }, + "background-opacity": { + "description": "调整OSD背景的透明度。", + "label": "背景透明度" + }, "description": "配置屏幕叠加指示器,例如音量和亮度。", "duration": { "auto-hide": { @@ -1470,6 +1474,7 @@ "description": "配置屏显菜单(OSD)的可见性与行为。", "label": "常规" } + }, "title": "屏显菜单" }, diff --git a/Assets/settings-default.json b/Assets/settings-default.json index f648641b..1c1806f3 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -250,7 +250,8 @@ "location": "top_right", "monitors": [], "autoHideMs": 2000, - "overlayLayer": true + "overlayLayer": true, + "backgroundOpacity": 1 }, "audio": { "volumeStep": 5, diff --git a/Commons/Settings.qml b/Commons/Settings.qml index d1a8ae17..28045d96 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -385,6 +385,7 @@ Singleton { property list monitors: [] property int autoHideMs: 2000 property bool overlayLayer: true + property real backgroundOpacity: 1.0 } // audio diff --git a/Modules/OSD/OSD.qml b/Modules/OSD/OSD.qml index 786b5200..1409d545 100644 --- a/Modules/OSD/OSD.qml +++ b/Modules/OSD/OSD.qml @@ -49,83 +49,83 @@ Variants { function getIcon() { if (currentOSDType === "volume") { if (AudioService.muted) { - return "volume-mute" + return "volume-mute"; } - return (AudioService.volume <= Number.EPSILON) ? "volume-zero" : (AudioService.volume <= 0.5) ? "volume-low" : "volume-high" + return (AudioService.volume <= Number.EPSILON) ? "volume-zero" : (AudioService.volume <= 0.5) ? "volume-low" : "volume-high"; } else if (currentOSDType === "inputVolume") { if (AudioService.inputMuted) { - return "microphone-off" + return "microphone-off"; } - return "microphone" + return "microphone"; } else if (currentOSDType === "brightness") { - return currentBrightness <= 0.5 ? "brightness-low" : "brightness-high" + return currentBrightness <= 0.5 ? "brightness-low" : "brightness-high"; } - return "" + return ""; } // Get current value (0-1 range) function getCurrentValue() { if (currentOSDType === "volume") { - return isMuted ? 0 : currentVolume + return isMuted ? 0 : currentVolume; } else if (currentOSDType === "inputVolume") { - return isInputMuted ? 0 : currentInputVolume + return isInputMuted ? 0 : currentInputVolume; } else if (currentOSDType === "brightness") { - return currentBrightness + return currentBrightness; } - return 0 + return 0; } // Get maximum value for current OSD type function getMaxValue() { if (currentOSDType === "volume" || currentOSDType === "inputVolume") { - return Settings.data.audio.volumeOverdrive ? 1.5 : 1.0 + return Settings.data.audio.volumeOverdrive ? 1.5 : 1.0; } else if (currentOSDType === "brightness") { - return 1.0 + return 1.0; } - return 1.0 + return 1.0; } // Get display percentage function getDisplayPercentage() { if (currentOSDType === "volume") { if (isMuted) - return "0%" - const max = getMaxValue() - const pct = Math.round(Math.min(max, currentVolume) * 100) - return pct + "%" + return "0%"; + const max = getMaxValue(); + const pct = Math.round(Math.min(max, currentVolume) * 100); + return pct + "%"; } else if (currentOSDType === "inputVolume") { if (isInputMuted) - return "0%" - const max = getMaxValue() - const pct = Math.round(Math.min(max, currentInputVolume) * 100) - return pct + "%" + return "0%"; + const max = getMaxValue(); + const pct = Math.round(Math.min(max, currentInputVolume) * 100); + return pct + "%"; } else if (currentOSDType === "brightness") { - const pct = Math.round(Math.min(1.0, currentBrightness) * 100) - return pct + "%" + const pct = Math.round(Math.min(1.0, currentBrightness) * 100); + return pct + "%"; } - return "" + return ""; } // Get progress bar color function getProgressColor() { if (currentOSDType === "volume") { if (isMuted) - return Color.mError - return Color.mPrimary + return Color.mError; + return Color.mPrimary; } else if (currentOSDType === "inputVolume") { if (isInputMuted) - return Color.mError - return Color.mPrimary + return Color.mError; + return Color.mPrimary; } - return Color.mPrimary + return Color.mPrimary; } // Get icon color function getIconColor() { if ((currentOSDType === "volume" && isMuted) || (currentOSDType === "inputVolume" && isInputMuted)) { - return Color.mError + return Color.mError; } - return Color.mOnSurface + return Color.mOnSurface; } sourceComponent: PanelWindow { @@ -146,8 +146,8 @@ Variants { // Ensure an even width to keep the vertical bar perfectly centered readonly property int barThickness: { - const base = Math.max(8, Math.round(8 * Style.uiScaleRatio)) - return (base % 2 === 0) ? base : base + 1 + const base = Math.max(8, Math.round(8 * Style.uiScaleRatio)); + return (base % 2 === 0) ? base : base + 1; } // Anchor selection based on location (window edges) @@ -159,46 +159,46 @@ Variants { // Margins depending on bar position and chosen location margins.top: { if (!(anchors.top)) - return 0 - var base = Style.marginM + return 0; + var base = Style.marginM; if (Settings.data.bar.position === "top") { - var floatExtraV = Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL : 0 - return Style.barHeight + base + floatExtraV + var floatExtraV = Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL : 0; + return Style.barHeight + base + floatExtraV; } - return base + return base; } margins.bottom: { if (!(anchors.bottom)) - return 0 - var base = Style.marginM + return 0; + var base = Style.marginM; if (Settings.data.bar.position === "bottom") { - var floatExtraV = Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL : 0 - return Style.barHeight + base + floatExtraV + var floatExtraV = Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL : 0; + return Style.barHeight + base + floatExtraV; } - return base + return base; } margins.left: { if (!(anchors.left)) - return 0 - var base = Style.marginM + return 0; + var base = Style.marginM; if (Settings.data.bar.position === "left") { - var floatExtraH = Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0 - return Style.barHeight + base + floatExtraH + var floatExtraH = Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0; + return Style.barHeight + base + floatExtraH; } - return base + return base; } margins.right: { if (!(anchors.right)) - return 0 - var base = Style.marginM + return 0; + var base = Style.marginM; if (Settings.data.bar.position === "right") { - var floatExtraH = Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0 - return Style.barHeight + base + floatExtraH + var floatExtraH = Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0; + return Style.barHeight + base + floatExtraH; } - return base + return base; } implicitWidth: verticalMode ? vWidth : hWidth @@ -206,6 +206,7 @@ Variants { color: Color.transparent + WlrLayershell.namespace: "noctalia-osd-" + (screen?.name || "unknown") WlrLayershell.keyboardFocus: WlrKeyboardFocus.None WlrLayershell.layer: (Settings.data.osd && Settings.data.osd.overlayLayer) ? WlrLayer.Overlay : WlrLayer.Top exclusionMode: PanelWindow.ExclusionMode.Ignore @@ -248,10 +249,10 @@ Variants { id: visibilityTimer interval: Style.animationNormal + 50 // Add small buffer onTriggered: { - osdItem.visible = false - root.currentOSDType = "" + osdItem.visible = false; + root.currentOSDType = ""; // Deactivate the loader when done - root.active = false + root.active = false; } } @@ -261,11 +262,11 @@ Variants { anchors.fill: parent anchors.margins: Style.marginM * 1.5 radius: Style.radiusL - color: Color.mSurface - border.color: Color.mOutline + color: Qt.alpha(Color.mSurface, Settings.data.osd.backgroundOpacity || 1.0) + border.color: Qt.alpha(Color.mOutline, Settings.data.osd.backgroundOpacity || 1.0) border.width: { - const bw = Math.max(2, Style.borderM) - return (bw % 2 === 0) ? bw : bw + 1 + const bw = Math.max(2, Style.borderM); + return (bw % 2 === 0) ? bw : bw + 1; } } @@ -364,9 +365,9 @@ Variants { ColumnLayout { // Ensure inner padding respects the rounded corners; avoid clipping the icon/text property int vMargin: { - const styleMargin = Style.marginL - const cornerGuard = Math.round(osdItem.radius) - return Math.max(styleMargin, cornerGuard) + const styleMargin = Style.marginL; + const cornerGuard = Math.round(osdItem.radius); + return Math.max(styleMargin, cornerGuard); } property int vMarginTop: Math.round(Math.max(osdItem.radius, Style.marginS)) property int balanceDelta: Style.marginS @@ -448,46 +449,46 @@ Variants { function show() { // Cancel any pending hide operations - hideTimer.stop() - visibilityTimer.stop() + hideTimer.stop(); + visibilityTimer.stop(); // Make visible and animate in - osdItem.visible = true + osdItem.visible = true; // Use Qt.callLater to ensure the visible change is processed before animation Qt.callLater(() => { - osdItem.opacity = 1 - osdItem.scale = 1.0 - }) + osdItem.opacity = 1; + osdItem.scale = 1.0; + }); // Start the auto-hide timer - hideTimer.start() + hideTimer.start(); } function hide() { - hideTimer.stop() - visibilityTimer.stop() + hideTimer.stop(); + visibilityTimer.stop(); // Start fade out animation - osdItem.opacity = 0 - osdItem.scale = 0.85 // Less dramatic scale change for smoother effect + osdItem.opacity = 0; + osdItem.scale = 0.85; // Less dramatic scale change for smoother effect // Delay hiding the element until after animation completes - visibilityTimer.start() + visibilityTimer.start(); } function hideImmediately() { - hideTimer.stop() - visibilityTimer.stop() - osdItem.opacity = 0 - osdItem.scale = 0.85 - osdItem.visible = false - root.currentOSDType = "" - root.active = false + hideTimer.stop(); + visibilityTimer.stop(); + osdItem.opacity = 0; + osdItem.scale = 0.85; + osdItem.visible = false; + root.currentOSDType = ""; + root.active = false; } } function showOSD() { - osdItem.show() + osdItem.show(); } } @@ -497,34 +498,34 @@ Variants { function onVolumeChanged() { if (volumeInitialized) { - showOSD("volume") + showOSD("volume"); } } function onMutedChanged() { if (muteInitialized) { - showOSD("volume") + showOSD("volume"); } } function onInputVolumeChanged() { if (!inputAudioInitialized) { - return + return; } if (!AudioService.hasInput) { - return + return; } - showOSD("inputVolume") + showOSD("inputVolume"); } function onInputMutedChanged() { if (!inputAudioInitialized) { - return + return; } if (!AudioService.hasInput) { - return + return; } - showOSD("inputVolume") + showOSD("inputVolume"); } } @@ -534,11 +535,11 @@ Variants { interval: 500 running: true onTriggered: { - volumeInitialized = true - muteInitialized = true - inputAudioInitialized = true + volumeInitialized = true; + muteInitialized = true; + inputAudioInitialized = true; // Brightness initializes on first change to avoid showing OSD on startup - connectBrightnessMonitors() + connectBrightnessMonitors(); } } @@ -547,65 +548,65 @@ Variants { target: BrightnessService function onMonitorsChanged() { - connectBrightnessMonitors() + connectBrightnessMonitors(); } } function disconnectBrightnessMonitors() { for (var i = 0; i < BrightnessService.monitors.length; i++) { - let monitor = BrightnessService.monitors[i] - monitor.brightnessUpdated.disconnect(onBrightnessChanged) + let monitor = BrightnessService.monitors[i]; + monitor.brightnessUpdated.disconnect(onBrightnessChanged); } } function connectBrightnessMonitors() { for (var i = 0; i < BrightnessService.monitors.length; i++) { - let monitor = BrightnessService.monitors[i] + let monitor = BrightnessService.monitors[i]; // Disconnect first to avoid duplicate connections - monitor.brightnessUpdated.disconnect(onBrightnessChanged) - monitor.brightnessUpdated.connect(onBrightnessChanged) + monitor.brightnessUpdated.disconnect(onBrightnessChanged); + monitor.brightnessUpdated.connect(onBrightnessChanged); } } function onBrightnessChanged(newBrightness) { - root.lastUpdatedBrightness = newBrightness + root.lastUpdatedBrightness = newBrightness; if (!brightnessInitialized) { - brightnessInitialized = true - return + brightnessInitialized = true; + return; } - showOSD("brightness") + showOSD("brightness"); } function showOSD(type) { // Update the current OSD type - currentOSDType = type + currentOSDType = type; // Activate the loader if not already active if (!root.active) { - root.active = true + root.active = true; } // Show the OSD (may need to wait for loader to create the item) if (root.item) { - root.item.showOSD() + root.item.showOSD(); } else { // If item not ready yet, wait for it Qt.callLater(() => { - if (root.item) { - root.item.showOSD() - } - }) + if (root.item) { + root.item.showOSD(); + } + }); } } function hideOSD() { if (root.item && root.item.osdItem) { - root.item.osdItem.hideImmediately() + root.item.osdItem.hideImmediately(); } else if (root.active) { // If loader is active but item isn't ready, just deactivate - root.active = false + root.active = false; } } } diff --git a/Modules/Panels/Settings/Tabs/OsdTab.qml b/Modules/Panels/Settings/Tabs/OsdTab.qml index 3474ec87..10f54551 100644 --- a/Modules/Panels/Settings/Tabs/OsdTab.qml +++ b/Modules/Panels/Settings/Tabs/OsdTab.qml @@ -11,15 +11,15 @@ ColumnLayout { // Helper functions to update arrays immutably function addMonitor(list, name) { - const arr = (list || []).slice() + const arr = (list || []).slice(); if (!arr.includes(name)) - arr.push(name) - return arr + arr.push(name); + return arr; } function removeMonitor(list, name) { return (list || []).filter(function (n) { - return n !== name - }) + return n !== name; + }); } // Display @@ -30,31 +30,40 @@ ColumnLayout { NComboBox { label: I18n.tr("settings.osd.location.label") description: I18n.tr("settings.osd.location.description") - model: [{ + model: [ + { "key": "top", "name": I18n.tr("options.osd.position.top_center") - }, { + }, + { "key": "top_left", "name": I18n.tr("options.osd.position.top_left") - }, { + }, + { "key": "top_right", "name": I18n.tr("options.osd.position.top_right") - }, { + }, + { "key": "bottom", "name": I18n.tr("options.osd.position.bottom_center") - }, { + }, + { "key": "bottom_left", "name": I18n.tr("options.osd.position.bottom_left") - }, { + }, + { "key": "bottom_right", "name": I18n.tr("options.osd.position.bottom_right") - }, { + }, + { "key": "left", "name": I18n.tr("options.osd.position.center_left") - }, { + }, + { "key": "right", "name": I18n.tr("options.osd.position.center_right") - }] + } + ] currentKey: Settings.data.osd.location || "top_right" onSelected: key => Settings.data.osd.location = key } @@ -90,6 +99,21 @@ ColumnLayout { onToggled: checked => Settings.data.osd.overlayLayer = checked } + NLabel { + label: I18n.tr("settings.osd.background-opacity.label", "Background opacity") + description: I18n.tr("settings.osd.background-opacity.description", "Controls the transparency of the OSD background.") + } + + NValueSlider { + Layout.fillWidth: true + from: 0 + to: 100 + stepSize: 1 + value: Settings.data.osd.backgroundOpacity * 100 + onMoved: value => Settings.data.osd.backgroundOpacity = value / 100 + text: Math.round(Settings.data.osd.backgroundOpacity * 100) + "%" + } + NLabel { label: I18n.tr("settings.osd.duration.auto-hide.label") description: I18n.tr("settings.osd.duration.auto-hide.description") @@ -128,22 +152,22 @@ ColumnLayout { Layout.fillWidth: true label: modelData.name || I18n.tr("system.unknown") description: { - const compositorScale = CompositorService.getDisplayScale(modelData.name) + const compositorScale = CompositorService.getDisplayScale(modelData.name); I18n.tr("system.monitor-description", { - "model": modelData.model, - "width": modelData.width * compositorScale, - "height": modelData.height * compositorScale, - "scale": compositorScale - }) + "model": modelData.model, + "width": modelData.width * compositorScale, + "height": modelData.height * compositorScale, + "scale": compositorScale + }); } checked: (Settings.data.osd.monitors || []).indexOf(modelData.name) !== -1 onToggled: checked => { - if (checked) { - Settings.data.osd.monitors = addMonitor(Settings.data.osd.monitors, modelData.name) - } else { - Settings.data.osd.monitors = removeMonitor(Settings.data.osd.monitors, modelData.name) - } - } + if (checked) { + Settings.data.osd.monitors = addMonitor(Settings.data.osd.monitors, modelData.name); + } else { + Settings.data.osd.monitors = removeMonitor(Settings.data.osd.monitors, modelData.name); + } + } } } }