diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index de424fa02..15f9f537d 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -1006,6 +1006,9 @@ "weather-show-background-description": "Zeige den Hintergrundcontainer für das Wetter-Widget an." }, "display": { + "monitors-backlight-device-auto-option": "Standard", + "monitors-backlight-device-description": "Wähle ein Hintergrundbeleuchtungsgerät für diesen Ausgang.", + "monitors-backlight-device-label": "Hintergrundbeleuchtungsgerät", "monitors-brightness-step-description": "Schrittgröße für Helligkeitsänderungen anpassen (Mausrad und Tastenkürzel).", "monitors-brightness-step-label": "Helligkeits-Schrittgröße", "monitors-brightness-unavailable-ddc-disabled": "Helligkeitssteuerung nicht verfügbar. Aktivieren Sie \"Externe Helligkeitsunterstützung\", um die Helligkeit dieses Displays zu steuern.", diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index f9bcf1942..64563f858 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -1006,6 +1006,9 @@ "weather-show-background-description": "Show the background container for the weather widget." }, "display": { + "monitors-backlight-device-auto-option": "Default", + "monitors-backlight-device-description": "Select a backlight device for this output.", + "monitors-backlight-device-label": "Backlight device", "monitors-brightness-step-description": "Adjust the step size for brightness changes (scroll wheel and keyboard shortcuts).", "monitors-brightness-step-label": "Brightness step size", "monitors-brightness-unavailable-ddc-disabled": "Brightness control unavailable. Enable \"External brightness support\" to control this display's brightness.", diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index 7262cc152..b1d545831 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -1006,6 +1006,9 @@ "weather-show-background-description": "Mostrar el contenedor de fondo para el widget del clima." }, "display": { + "monitors-backlight-device-auto-option": "Predeterminado", + "monitors-backlight-device-description": "Selecciona un dispositivo de retroiluminación para esta salida.", + "monitors-backlight-device-label": "Dispositivo de Retroiluminación", "monitors-brightness-step-description": "Ajusta el tamaño del paso para los cambios de brillo (rueda de desplazamiento y atajos de teclado).", "monitors-brightness-step-label": "Tamaño del paso de brillo", "monitors-brightness-unavailable-ddc-disabled": "Control de brillo no disponible. Habilita \"Soporte de brillo externo\" para controlar el brillo de esta pantalla.", diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index 0628e9c94..ea42665eb 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -1006,6 +1006,9 @@ "weather-show-background-description": "Afficher le conteneur d'arrière-plan pour le widget météo." }, "display": { + "monitors-backlight-device-auto-option": "Par défaut", + "monitors-backlight-device-description": "Sélectionnez un appareil de rétroéclairage pour cette sortie.", + "monitors-backlight-device-label": "Appareil de Rétroéclairage", "monitors-brightness-step-description": "Ajustez l'incrément pour les changements de luminosité (molette de la souris et raccourcis clavier).", "monitors-brightness-step-label": "Incrément de luminosité", "monitors-brightness-unavailable-ddc-disabled": "Contrôle de la luminosité indisponible. Activez \"Prise en charge de la luminosité externe\" pour contrôler la luminosité de cet écran.", diff --git a/Assets/Translations/hu.json b/Assets/Translations/hu.json index 3857309ba..8115f23c6 100644 --- a/Assets/Translations/hu.json +++ b/Assets/Translations/hu.json @@ -1006,6 +1006,9 @@ "weather-show-background-description": "Jelenítse meg az időjárás widget háttérkonténerét." }, "display": { + "monitors-backlight-device-auto-option": "Alapértelmezett", + "monitors-backlight-device-description": "Válasszon háttérvilágítási eszközt ehhez a kimenethez.", + "monitors-backlight-device-label": "Háttérvilágítási Eszköz", "monitors-brightness-step-description": "Állítsa be a lépésméretet a fényerő változásokhoz (görgetőkerék és billentyűparancsok).", "monitors-brightness-step-label": "Fényerő lépésméret", "monitors-brightness-unavailable-ddc-disabled": "Fényerő vezérlés nem érhető el. Engedélyezze a \"Külső fényerő támogatás\" opciót a kijelző fényerejének szabályozásához.", diff --git a/Assets/Translations/ja.json b/Assets/Translations/ja.json index 3f3dd7a29..70f1240f2 100644 --- a/Assets/Translations/ja.json +++ b/Assets/Translations/ja.json @@ -1006,6 +1006,9 @@ "weather-show-background-description": "天気ウィジェットの背景コンテナを表示します。" }, "display": { + "monitors-backlight-device-auto-option": "デフォルト", + "monitors-backlight-device-description": "この出力のバックライトデバイスを選択してください。", + "monitors-backlight-device-label": "バックライトデバイス", "monitors-brightness-step-description": "明るさの変化量(スクロールホイールやショートカットキー)を調整します。", "monitors-brightness-step-label": "明るさの調整ステップ", "monitors-brightness-unavailable-ddc-disabled": "明るさ調整を利用できません。このディスプレイを操作するには「外部ディスプレイの明るさ制御」を有効にしてください。", diff --git a/Assets/Translations/ko-KR.json b/Assets/Translations/ko-KR.json index 83ce0db13..8021125f0 100644 --- a/Assets/Translations/ko-KR.json +++ b/Assets/Translations/ko-KR.json @@ -1006,6 +1006,9 @@ "weather-show-background-description": "날씨 위젯의 배경 컨테이너를 표시합니다." }, "display": { + "monitors-backlight-device-auto-option": "기본값", + "monitors-backlight-device-description": "이 출력에 대한 백라이트 장치를 선택하세요.", + "monitors-backlight-device-label": "백라이트 장치", "monitors-brightness-step-description": "밝기 변경(스크롤 휠 및 키보드 단축키)의 단계 크기를 조정합니다.", "monitors-brightness-step-label": "밝기 단계 크기", "monitors-brightness-unavailable-ddc-disabled": "밝기 제어를 사용할 수 없습니다. 이 디스플레이의 밝기를 제어하려면 \"외부 밝기 지원\"을 활성화하세요.", diff --git a/Assets/Translations/nl.json b/Assets/Translations/nl.json index 87dbab495..26ce7874a 100644 --- a/Assets/Translations/nl.json +++ b/Assets/Translations/nl.json @@ -1006,6 +1006,9 @@ "weather-show-background-description": "Toon de achtergrondcontainer voor de weerwidget." }, "display": { + "monitors-backlight-device-auto-option": "Standaard", + "monitors-backlight-device-description": "Selecteer een achtergrondverlichtingsapparaat voor deze uitvoer.", + "monitors-backlight-device-label": "Achtergrondverlichting Apparaat", "monitors-brightness-step-description": "Pas de stapgrootte voor helderheidswijzigingen aan (scrollwiel en sneltoetsen).", "monitors-brightness-step-label": "Stapgrootte helderheid", "monitors-brightness-unavailable-ddc-disabled": "Helderheidsregeling niet beschikbaar. Schakel \"Ondersteuning voor externe helderheid\" in om de helderheid van dit scherm te regelen.", diff --git a/Assets/Translations/pl.json b/Assets/Translations/pl.json index 765f0849b..728535f1c 100644 --- a/Assets/Translations/pl.json +++ b/Assets/Translations/pl.json @@ -1006,6 +1006,9 @@ "weather-show-background-description": "Pokaż tło dla widżetu pogody." }, "display": { + "monitors-backlight-device-auto-option": "Domyślny", + "monitors-backlight-device-description": "Wybierz urządzenie podświetlenia dla tego wyjścia.", + "monitors-backlight-device-label": "Urządzenie Podświetlenia", "monitors-brightness-step-description": "Dostosuj skok zmiany jasności (kółko myszy i skróty klawiszowe).", "monitors-brightness-step-label": "Skok jasności", "monitors-brightness-unavailable-ddc-disabled": "Sterowanie jasnością niedostępne. Włącz \"Obsługa zewnętrznej jasności\", aby sterować jasnością tego ekranu.", diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index ed7a69c80..7d35e4cd3 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -1006,6 +1006,9 @@ "weather-show-background-description": "Mostrar o contêiner de fundo para o widget de clima." }, "display": { + "monitors-backlight-device-auto-option": "Padrão", + "monitors-backlight-device-description": "Selecione um dispositivo de retroiluminação para esta saída.", + "monitors-backlight-device-label": "Dispositivo de Retroiluminação", "monitors-brightness-step-description": "Ajuste o tamanho do passo para alterações de brilho (roda do mouse e atalhos de teclado).", "monitors-brightness-step-label": "Tamanho do passo do brilho", "monitors-brightness-unavailable-ddc-disabled": "Controle de brilho indisponível. Ative \"Suporte de brilho externo\" para controlar o brilho desta tela.", diff --git a/Assets/Translations/ru.json b/Assets/Translations/ru.json index 50b35934c..c68f9a035 100644 --- a/Assets/Translations/ru.json +++ b/Assets/Translations/ru.json @@ -1006,6 +1006,9 @@ "weather-show-background-description": "Показать фоновый контейнер для погодного виджета." }, "display": { + "monitors-backlight-device-auto-option": "По умолчанию", + "monitors-backlight-device-description": "Выберите устройство подсветки для этого вывода.", + "monitors-backlight-device-label": "Устройство Подсветки", "monitors-brightness-step-description": "Настройка шага изменения яркости (колесо прокрутки и сочетания клавиш).", "monitors-brightness-step-label": "Шаг изменения яркости", "monitors-brightness-unavailable-ddc-disabled": "Управление яркостью недоступно. Включите \"Поддержка внешней яркости\", чтобы управлять яркостью этого дисплея.", diff --git a/Assets/Translations/sv.json b/Assets/Translations/sv.json index a532e8038..d184eb3d7 100644 --- a/Assets/Translations/sv.json +++ b/Assets/Translations/sv.json @@ -1006,6 +1006,9 @@ "weather-show-background-description": "Visa bakgrundsbehållaren för väderwidgeten." }, "display": { + "monitors-backlight-device-auto-option": "Standard", + "monitors-backlight-device-description": "Välj en bakgrundsbelysningsenhet för denna utgång.", + "monitors-backlight-device-label": "Bakgrundsbelysningsenhet", "monitors-brightness-step-description": "Justera stegstorleken för ljusstyrkeändringar (rullhjul och kortkommandon).", "monitors-brightness-step-label": "Ljusstyrkestegstorlek", "monitors-brightness-unavailable-ddc-disabled": "Ljusstyrkekontroll otillgänglig. Aktivera \"Extern ljusstyrkestöd\"för att kontrollera ljusstyrkan på denna skärm.", diff --git a/Assets/Translations/tr.json b/Assets/Translations/tr.json index 70fc25589..330632a06 100644 --- a/Assets/Translations/tr.json +++ b/Assets/Translations/tr.json @@ -1006,6 +1006,9 @@ "weather-show-background-description": "Hava durumu widget'ı için arka plan konteynerini göster." }, "display": { + "monitors-backlight-device-auto-option": "Varsayılan", + "monitors-backlight-device-description": "Bu çıkış için bir arka ışık cihazı seçin.", + "monitors-backlight-device-label": "Arka Işık Cihazı", "monitors-brightness-step-description": "Parlaklık değişimleri için adım boyutunu ayarlayın (tekerlek ve klavye kısayolları).", "monitors-brightness-step-label": "Parlaklık adım boyutu", "monitors-brightness-unavailable-ddc-disabled": "Parlaklık kontrolü kullanılamıyor. Bu ekranın parlaklığını kontrol etmek için \"Harici parlaklık desteği\"ni etkinleştirin.", diff --git a/Assets/Translations/uk-UA.json b/Assets/Translations/uk-UA.json index 7850abd5a..29f3f9553 100644 --- a/Assets/Translations/uk-UA.json +++ b/Assets/Translations/uk-UA.json @@ -1006,6 +1006,9 @@ "weather-show-background-description": "Показати фоновий контейнер для погодного віджета." }, "display": { + "monitors-backlight-device-auto-option": "За замовчуванням", + "monitors-backlight-device-description": "Виберіть пристрій підсвічування для цього виходу.", + "monitors-backlight-device-label": "Пристрій Підсвічування", "monitors-brightness-step-description": "Налаштуйте крок зміни яскравості (колесо миші та гарячі клавіші).", "monitors-brightness-step-label": "Крок зміни яскравості", "monitors-brightness-unavailable-ddc-disabled": "Регулювання яскравості недоступне. Увімкніть \"Підтримку зовнішньої яскравості\", щоб керувати яскравістю цього дисплея.", diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index eaa43117b..50ba25205 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -1006,6 +1006,9 @@ "weather-show-background-description": "显示天气小部件的背景容器。" }, "display": { + "monitors-backlight-device-auto-option": "默认", + "monitors-backlight-device-description": "为此输出选择一个背光设备。", + "monitors-backlight-device-label": "背光设备", "monitors-brightness-step-description": "调整亮度变化的步长(滚轮和键盘快捷键)。", "monitors-brightness-step-label": "亮度步长", "monitors-brightness-unavailable-ddc-disabled": "亮度控制不可用。启用“外部亮度支持”以控制此显示器的亮度。", diff --git a/Assets/Translations/zh-TW.json b/Assets/Translations/zh-TW.json index a3266f032..5aab1f8b1 100644 --- a/Assets/Translations/zh-TW.json +++ b/Assets/Translations/zh-TW.json @@ -1006,6 +1006,9 @@ "weather-show-background-description": "顯示天氣小工具的填充背景" }, "display": { + "monitors-backlight-device-auto-option": "預設", + "monitors-backlight-device-description": "為此輸出選擇一個背光裝置。", + "monitors-backlight-device-label": "背光裝置", "monitors-brightness-step-description": "微調亮度調整一格的大小 (滾輪及鍵盤快捷鍵)", "monitors-brightness-step-label": "亮度步進大小", "monitors-brightness-unavailable-ddc-disabled": "無法使用亮度控制, 啟用 \"外部亮度調整支援\" 來控制這個顯示器的亮度", diff --git a/Assets/settings-default.json b/Assets/settings-default.json index e47126bb2..b9013ed98 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -460,7 +460,8 @@ "brightness": { "brightnessStep": 5, "enforceMinimum": true, - "enableDdcSupport": false + "enableDdcSupport": false, + "backlightDeviceMappings": [] }, "colorSchemes": { "useWallpaperColors": false, @@ -512,4 +513,4 @@ "gridSnap": false, "monitorWidgets": [] } -} \ No newline at end of file +} diff --git a/Assets/settings-search-index.json b/Assets/settings-search-index.json index ba02c665d..e9e0a8bae 100644 --- a/Assets/settings-search-index.json +++ b/Assets/settings-search-index.json @@ -519,6 +519,15 @@ "tabLabel": "panels.desktop-widgets.title", "subTab": null }, + { + "labelKey": "panels.display.monitors-backlight-device-label", + "descriptionKey": "panels.display.monitors-backlight-device-description", + "widget": "NComboBox", + "tab": 14, + "tabLabel": "panels.display.title", + "subTab": 0, + "subTabLabel": "common.brightness" + }, { "labelKey": "panels.display.monitors-brightness-step-label", "descriptionKey": "panels.display.monitors-brightness-step-description", diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 2caa1c19a..3d9d4984a 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -670,6 +670,8 @@ Singleton { property int brightnessStep: 5 property bool enforceMinimum: true property bool enableDdcSupport: false + property list backlightDeviceMappings: [] + // Format: [{ "output": "eDP-1", "device": "/sys/class/backlight/intel_backlight" }] } property JsonObject colorSchemes: JsonObject { diff --git a/Modules/Bar/Widgets/CustomButton.qml b/Modules/Bar/Widgets/CustomButton.qml index aaa794267..af026f6ac 100644 --- a/Modules/Bar/Widgets/CustomButton.qml +++ b/Modules/Bar/Widgets/CustomButton.qml @@ -205,6 +205,7 @@ Item { rotateText: isVerticalBar && currentMaxTextLength > 0 autoHide: false forceOpen: _pillForceOpen + forceClose: !_pillForceOpen customTextIconColor: iconColor // Helper function to build tooltip content diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml index 4e69f3f50..f265c72b3 100644 --- a/Modules/Dock/Dock.qml +++ b/Modules/Dock/Dock.qml @@ -102,7 +102,7 @@ Loader { readonly property int barHeight: Style.getBarHeightForScreen(modelData?.name) readonly property int peekEdgeLength: { const edgeSize = isVertical ? Math.round(modelData?.height || maxHeight) : Math.round(modelData?.width || maxWidth); - const minLength = Math.max(1, Math.round(edgeSize * 0.1)); + const minLength = Math.max(1, Math.round(edgeSize * ((isStaticMode && Settings.data.dock.showFrameIndicator && Settings.data.bar.barType === "framed" && hasBar) ? 0.1 : 0.25))); return Math.max(minLength, frameIndicatorLength); } readonly property int peekCenterOffsetX: { @@ -791,8 +791,9 @@ Loader { readonly property int extraLeft: (!isVertical && !exclusive && barOnLeft) ? barHeight : 0 readonly property int extraRight: (!isVertical && !exclusive && barOnRight) ? barHeight : 0 - width: dockContent.dockContainer.width + extraLeft + extraRight - height: dockContent.dockContainer.height + extraTop + extraBottom + // Add +2 buffer for fractional scaling issues + width: dockContent.dockContainer.width + extraLeft + extraRight + (root.isVertical ? 2 : Style.margin2XL * 6) + height: dockContent.dockContainer.height + extraTop + extraBottom + 2 anchors.horizontalCenter: isVertical ? undefined : parent.horizontalCenter anchors.verticalCenter: isVertical ? parent.verticalCenter : undefined @@ -802,6 +803,9 @@ Loader { anchors.left: dockPosition === "left" ? parent.left : undefined anchors.right: dockPosition === "right" ? parent.right : undefined + // Enable layer caching to reduce GPU usage from continuous animations + layer.enabled: true + opacity: hidden ? 0 : 1 scale: hidden ? 0.85 : 1 diff --git a/Modules/Dock/DockContent.qml b/Modules/Dock/DockContent.qml index 9368f826e..4006204b7 100644 --- a/Modules/Dock/DockContent.qml +++ b/Modules/Dock/DockContent.qml @@ -41,9 +41,6 @@ Item { border.width: Style.borderS border.color: Qt.alpha(Color.mOutline, (isStaticMode ? 0 : Settings.data.dock.backgroundOpacity)) - // Enable layer caching to reduce GPU usage from continuous animations - layer.enabled: true - MouseArea { id: dockMouseArea anchors.fill: parent diff --git a/Modules/Dock/DockMenu.qml b/Modules/Dock/DockMenu.qml index 86dd1d084..7f18d6309 100644 --- a/Modules/Dock/DockMenu.qml +++ b/Modules/Dock/DockMenu.qml @@ -37,7 +37,7 @@ PopupWindow { property real menuMinWidth: 120 property real menuMaxWidth: 360 property real menuMaxHeight: Math.max(180, Math.min(420, Math.round((targetScreen ? targetScreen.height : 600) * 0.3))) - property int separatorCompactHeight: 8 + property int separatorCompactHeight: Style.borderS + Style.margin2S property string forcedGroupMenuMode: "" readonly property int separatorIndex: { for (let i = 0; i < root.items.length; i++) { @@ -49,7 +49,7 @@ PopupWindow { readonly property bool splitExtendedLayout: separatorIndex >= 0 readonly property var scrollItems: splitExtendedLayout ? root.items.slice(0, separatorIndex) : root.items readonly property var fixedItems: splitExtendedLayout ? root.items.slice(separatorIndex + 1) : [] - readonly property real menuInnerHeight: Math.max(0, implicitHeight - Style.marginXL) + readonly property real menuInnerHeight: Math.max(0, implicitHeight - Style.margin2M) readonly property real fixedActionsHeight: listHeight(fixedItems) readonly property real separatorBlockHeight: splitExtendedLayout ? separatorCompactHeight : 0 readonly property real scrollAreaHeight: splitExtendedLayout ? Math.max(0, menuInnerHeight - fixedActionsHeight - separatorBlockHeight) : menuInnerHeight @@ -709,12 +709,14 @@ PopupWindow { } Rectangle { + id: separator visible: root.splitExtendedLayout anchors.left: parent.left anchors.right: parent.right anchors.top: menuFlick.bottom anchors.leftMargin: Style.marginS anchors.rightMargin: Style.marginS + anchors.topMargin: Style.marginS height: Style.borderS color: Qt.alpha(Color.mOutline, 0.7) radius: Style.radiusXS @@ -728,15 +730,16 @@ PopupWindow { anchors.bottom: parent.bottom anchors.leftMargin: Style.marginM anchors.rightMargin: Style.marginM + anchors.topMargin: Style.marginS anchors.bottomMargin: Style.marginM - anchors.top: menuFlick.bottom - anchors.topMargin: root.separatorBlockHeight + anchors.top: separator.bottom spacing: 0 Repeater { model: root.fixedItems Rectangle { + id: fixedItemRect readonly property int globalIndex: root.fixedItemGlobalIndex(index) width: fixedColumn.width height: root.rowHeightForItem(modelData) @@ -755,7 +758,7 @@ PopupWindow { NIcon { icon: modelData.icon pointSize: Style.fontSizeL - color: root.hoveredItem === parent.globalIndex ? Color.mOnHover : Color.mOnSurfaceVariant + color: root.hoveredItem === fixedItemRect.globalIndex ? Color.mOnHover : Color.mOnSurfaceVariant visible: icon !== "" anchors.verticalCenter: parent.verticalCenter } @@ -763,7 +766,7 @@ PopupWindow { NText { text: modelData.text pointSize: Style.fontSizeS - color: root.hoveredItem === parent.globalIndex ? Color.mOnHover : Color.mOnSurfaceVariant + color: root.hoveredItem === fixedItemRect.globalIndex ? Color.mOnHover : Color.mOnSurfaceVariant anchors.verticalCenter: parent.verticalCenter width: fixedRowLayout.width - ((modelData.icon && modelData.icon !== "") ? (Style.fontSizeL + Style.marginS) : 0) elide: Text.ElideRight diff --git a/Modules/Panels/Network/WiFiNetworksList.qml b/Modules/Panels/Network/WiFiNetworksList.qml index c348bba42..ba617b398 100644 --- a/Modules/Panels/Network/WiFiNetworksList.qml +++ b/Modules/Panels/Network/WiFiNetworksList.qml @@ -201,7 +201,7 @@ NBox { color: Color.mError radius: height * 0.5 width: Math.round(forgettingText.implicitWidth + Style.margin2S) - height: math.round(forgettingText.implicitHeight + Style.margin2XXS) + height: Math.round(forgettingText.implicitHeight + Style.margin2XXS) NText { id: forgettingText diff --git a/Modules/Panels/Settings/SettingsContent.qml b/Modules/Panels/Settings/SettingsContent.qml index a324a0788..a67453402 100644 --- a/Modules/Panels/Settings/SettingsContent.qml +++ b/Modules/Panels/Settings/SettingsContent.qml @@ -326,13 +326,29 @@ Item { highlightOverlay.opacity = 0; } - // Find and highlight a widget by its label key + function isEffectivelyVisible(item) { + var current = item; + while (current) { + if (current.visible === false) + return false; + if (current.opacity !== undefined && current.opacity <= 0) + return false; + current = current.parent; + } + return true; + } + + // Find and highlight a widget by its label key. function findAndHighlightWidget(item, labelKey) { if (!item) return null; - // Check if this item has a matching label - if (item.hasOwnProperty("label") && item.label === I18n.tr(labelKey)) { + // Skip hidden branches to avoid highlighting controls that are not on screen. + if (!isEffectivelyVisible(item)) + return null; + + // Check if this item has a matching label. + if (item.hasOwnProperty("label") && item.label === I18n.tr(labelKey) && item.width > 0 && item.height > 0) { return item; } diff --git a/Modules/Panels/Settings/Tabs/Connections/BluetoothSubTab.qml b/Modules/Panels/Settings/Tabs/Connections/BluetoothSubTab.qml index f74ae5b63..51987587a 100644 --- a/Modules/Panels/Settings/Tabs/Connections/BluetoothSubTab.qml +++ b/Modules/Panels/Settings/Tabs/Connections/BluetoothSubTab.qml @@ -196,7 +196,7 @@ Item { id: connectedDevicesBox visible: root.connectedDevices.length > 0 && BluetoothService.enabled Layout.fillWidth: true - Layout.preferredHeight: connectedDevicesCol.implicitHeight + Style.marginXL + Layout.preferredHeight: connectedDevicesCol.implicitHeight + Style.margin2M border.color: showOnlyLists ? Style.boxBorderColor : "transparent" ColumnLayout { @@ -226,7 +226,7 @@ Item { id: pairedDevicesBox visible: root.pairedDevices.length > 0 && BluetoothService.enabled Layout.fillWidth: true - Layout.preferredHeight: pairedDevicesCol.implicitHeight + Style.marginXL + Layout.preferredHeight: pairedDevicesCol.implicitHeight + Style.margin2M border.color: showOnlyLists ? Style.boxBorderColor : "transparent" ColumnLayout { @@ -256,7 +256,7 @@ Item { id: availableDevicesBox visible: !root.showOnlyLists && root.unnamedAvailableDevices.length > 0 && BluetoothService.enabled Layout.fillWidth: true - Layout.preferredHeight: availableDevicesCol.implicitHeight + Style.marginXL + Layout.preferredHeight: availableDevicesCol.implicitHeight + Style.margin2M border.color: "transparent" ColumnLayout { diff --git a/Modules/Panels/Settings/Tabs/Display/BrightnessSubTab.qml b/Modules/Panels/Settings/Tabs/Display/BrightnessSubTab.qml index e74affea3..88fbb3015 100644 --- a/Modules/Panels/Settings/Tabs/Display/BrightnessSubTab.qml +++ b/Modules/Panels/Settings/Tabs/Display/BrightnessSubTab.qml @@ -25,6 +25,32 @@ ColumnLayout { property var brightnessMonitor: BrightnessService.getMonitorForScreen(modelData) property real localBrightness: 0.5 property bool localBrightnessChanging: false + readonly property string automaticOptionLabel: { + var baseLabel = I18n.tr("panels.display.monitors-backlight-device-auto-option"); + var autoDevicePath = (BrightnessService.availableBacklightDevices && BrightnessService.availableBacklightDevices.length > 0) ? BrightnessService.availableBacklightDevices[0] : ""; + if (autoDevicePath === "") + return baseLabel; + + var autoDeviceName = BrightnessService.getBacklightDeviceName(autoDevicePath) || autoDevicePath; + return baseLabel + "(" + autoDeviceName + ")"; + } + readonly property var backlightDeviceOptions: { + var options = [{ + "key": "", + "name": automaticOptionLabel + }]; + + var devices = BrightnessService.availableBacklightDevices || []; + for (var i = 0; i < devices.length; i++) { + var devicePath = devices[i]; + var deviceName = BrightnessService.getBacklightDeviceName(devicePath) || devicePath; + options.push({ + "key": devicePath, + "name": deviceName + }); + } + return options; + } onBrightnessMonitorChanged: { if (brightnessMonitor && !localBrightnessChanging) @@ -160,13 +186,23 @@ ColumnLayout { } NText { - visible: brightnessMonitor && !brightnessMonitor.brightnessControlAvailable + visible: brightnessMonitor && !brightnessMonitor.brightnessControlAvailable && !(brightnessMonitor.method === "internal" && brightnessMonitor.initInProgress) text: !Settings.data.brightness.enableDdcSupport ? I18n.tr("panels.display.monitors-brightness-unavailable-ddc-disabled") : I18n.tr("panels.display.monitors-brightness-unavailable-generic") pointSize: Style.fontSizeXS color: Color.mOnSurfaceVariant Layout.fillWidth: true wrapMode: Text.WordWrap } + + NComboBox { + Layout.fillWidth: true + visible: brightnessMonitor && brightnessMonitor.method === "internal" + label: I18n.tr("panels.display.monitors-backlight-device-label") + description: I18n.tr("panels.display.monitors-backlight-device-description") + model: backlightDeviceOptions + currentKey: BrightnessService.getMappedBacklightDevice(modelData.name) || "" + onSelected: key => BrightnessService.setMappedBacklightDevice(modelData.name, key) + } } } } diff --git a/Modules/Tooltip/Tooltip.qml b/Modules/Tooltip/Tooltip.qml index 2ffe94016..59a42fe65 100644 --- a/Modules/Tooltip/Tooltip.qml +++ b/Modules/Tooltip/Tooltip.qml @@ -260,19 +260,17 @@ PopupWindow { const tipWidth = Math.ceil(Math.min(contentWidth + (padding * 2), maxWidth)); root.implicitWidth = tipWidth; - // Add +2 buffer for fractional scaling issues (especially with "top" direction) - const tipHeight = Math.ceil(contentHeight + (padding * 2)) + 2; + const tipHeight = Math.ceil(contentHeight + (padding * 2)); root.implicitHeight = tipHeight; // Get target's global position and convert to screen-relative - // Round all values to avoid sub-pixel positioning issues with fractional scaling var targetGlobalAbs = targetItem.mapToGlobal(0, 0); var targetGlobal = { - "x": Math.round(targetGlobalAbs.x - screenX), - "y": Math.round(targetGlobalAbs.y - screenY) + "x": targetGlobalAbs.x - screenX, + "y": targetGlobalAbs.y - screenY }; - const targetWidth = Math.round(targetItem.width); - const targetHeight = Math.round(targetItem.height); + const targetWidth = targetItem.width; + const targetHeight = targetItem.height; var newAnchorX = 0; var newAnchorY = 0; @@ -427,7 +425,6 @@ PopupWindow { } // Apply position first (before making visible) - // Round to avoid sub-pixel positioning issues with fractional scaling // Use floor for negative values to push tooltip away from target anchorX = newAnchorX < 0 ? Math.floor(newAnchorX) : Math.round(newAnchorX); anchorY = newAnchorY < 0 ? Math.floor(newAnchorY) : Math.round(newAnchorY); @@ -519,19 +516,18 @@ PopupWindow { const tipWidth = Math.ceil(Math.min(contentWidth + (padding * 2), maxWidth)); root.implicitWidth = tipWidth; - // Add +2 buffer for fractional scaling issues (especially with "top" direction) - const tipHeight = Math.ceil(contentHeight + (padding * 2)) + 2; + const tipHeight = Math.ceil(contentHeight + (padding * 2)); root.implicitHeight = tipHeight; // Reposition based on current direction (screen-relative) // Round all values to avoid sub-pixel positioning issues with fractional scaling var targetGlobalAbs = targetItem.mapToGlobal(0, 0); var targetGlobal = { - "x": Math.round(targetGlobalAbs.x - screenX), - "y": Math.round(targetGlobalAbs.y - screenY) + "x": targetGlobalAbs.x - screenX, + "y": targetGlobalAbs.y - screenY }; - const targetWidth = Math.round(targetItem.width); - const targetHeight = Math.round(targetItem.height); + const targetWidth = targetItem.width; + const targetHeight = targetItem.height; // Recalculate base anchor position (center on target for top/bottom, etc.) var newAnchorX = anchorX; @@ -608,7 +604,6 @@ PopupWindow { } // Apply the new anchor positions - // Round to avoid sub-pixel positioning issues with fractional scaling // Use floor for negative values to push tooltip away from target anchorX = newAnchorX < 0 ? Math.floor(newAnchorX) : Math.round(newAnchorX); anchorY = newAnchorY < 0 ? Math.floor(newAnchorY) : Math.round(newAnchorY); @@ -663,6 +658,7 @@ PopupWindow { Rectangle { anchors.fill: parent + anchors.margins: border.width / 2 color: Color.mSurface border.color: Color.mOutline border.width: Style.borderS diff --git a/Services/Control/IPCService.qml b/Services/Control/IPCService.qml index 636f62f60..c4898efe6 100644 --- a/Services/Control/IPCService.qml +++ b/Services/Control/IPCService.qml @@ -499,6 +499,10 @@ Singleton { }); } + function lock() { + CompositorService.lock(); + } + function lockAndSuspend() { CompositorService.lockAndSuspend(); } diff --git a/Services/Hardware/BrightnessService.qml b/Services/Hardware/BrightnessService.qml index fb360ab91..e4447873f 100644 --- a/Services/Hardware/BrightnessService.qml +++ b/Services/Hardware/BrightnessService.qml @@ -11,6 +11,7 @@ Singleton { property list ddcMonitors: [] readonly property list monitors: variants.instances property bool appleDisplayPresent: false + property list availableBacklightDevices: [] function getMonitorForScreen(screen: ShellScreen): var { return monitors.find(m => m.modelData === screen); @@ -47,10 +48,109 @@ Singleton { return detectedDisplays; } + function normalizeBacklightDevicePath(devicePath): string { + if (devicePath === undefined || devicePath === null) + return ""; + + var normalized = String(devicePath).trim(); + if (normalized === "") + return ""; + + if (normalized.startsWith("/sys/class/backlight/")) + return normalized; + + if (normalized.indexOf("/") === -1) + return "/sys/class/backlight/" + normalized; + + return normalized; + } + + function getBacklightDeviceName(devicePath): string { + var normalized = normalizeBacklightDevicePath(devicePath); + if (normalized === "") + return ""; + + var parts = normalized.split("/"); + while (parts.length > 0 && parts[parts.length - 1] === "") { + parts.pop(); + } + return parts.length > 0 ? parts[parts.length - 1] : ""; + } + + function getMappedBacklightDevice(outputName): string { + var normalizedOutput = String(outputName || "").trim(); + if (normalizedOutput === "") + return ""; + + var mappings = Settings.data.brightness.backlightDeviceMappings || []; + for (var i = 0; i < mappings.length; i++) { + var mapping = mappings[i]; + if (!mapping || typeof mapping !== "object") + continue; + + if (String(mapping.output || "").trim() === normalizedOutput) + return normalizeBacklightDevicePath(mapping.device || ""); + } + + return ""; + } + + function setMappedBacklightDevice(outputName, devicePath): void { + var normalizedOutput = String(outputName || "").trim(); + if (normalizedOutput === "") + return; + + var normalizedDevicePath = normalizeBacklightDevicePath(devicePath); + var mappings = Settings.data.brightness.backlightDeviceMappings || []; + var nextMappings = []; + var replaced = false; + + for (var i = 0; i < mappings.length; i++) { + var mapping = mappings[i]; + if (!mapping || typeof mapping !== "object") + continue; + + var mappingOutput = String(mapping.output || "").trim(); + var mappingDevice = normalizeBacklightDevicePath(mapping.device || ""); + if (mappingOutput === "" || mappingDevice === "") + continue; + + if (mappingOutput === normalizedOutput) { + if (!replaced && normalizedDevicePath !== "") { + nextMappings.push({ + "output": normalizedOutput, + "device": normalizedDevicePath + }); + } + replaced = true; + } else { + nextMappings.push({ + "output": mappingOutput, + "device": mappingDevice + }); + } + } + + if (!replaced && normalizedDevicePath !== "") { + nextMappings.push({ + "output": normalizedOutput, + "device": normalizedDevicePath + }); + } + + Settings.data.brightness.backlightDeviceMappings = nextMappings; + } + + function scanBacklightDevices(): void { + if (!scanBacklightProc.running) + scanBacklightProc.running = true; + } + reloadableId: "brightness" Component.onCompleted: { Logger.i("Brightness", "Service started"); + scanBacklightDevices(); if (Settings.data.brightness.enableDdcSupport) { ddcProc.running = true; } @@ -58,6 +158,7 @@ Singleton { onMonitorsChanged: { ddcMonitors = []; + scanBacklightDevices(); if (Settings.data.brightness.enableDdcSupport) { ddcProc.running = true; } @@ -75,6 +176,14 @@ Singleton { ddcMonitors = []; } } + function onBacklightDeviceMappingsChanged() { + scanBacklightDevices(); + for (var i = 0; i < monitors.length; i++) { + var m = monitors[i]; + if (m && !m.isDdc && !m.isAppleDisplay) + m.initBrightness(); + } + } } Variants { @@ -92,6 +201,34 @@ Singleton { } } + // Detect available internal backlight devices + Process { + id: scanBacklightProc + command: ["sh", "-c", "for dev in /sys/class/backlight/*; do if [ -f \"$dev/brightness\" ] && [ -f \"$dev/max_brightness\" ]; then echo \"$dev\"; fi; done"] + stdout: StdioCollector { + onStreamFinished: { + var data = text.trim(); + if (data === "") { + root.availableBacklightDevices = []; + return; + } + + var lines = data.split("\n"); + var found = []; + var seen = ({}); + for (var i = 0; i < lines.length; i++) { + var path = root.normalizeBacklightDevicePath(lines[i]); + if (path === "" || seen[path]) + continue; + seen[path] = true; + found.push(path); + } + + root.availableBacklightDevices = found; + } + } + } + // Detect DDC monitors Process { id: ddcProc @@ -152,6 +289,7 @@ Singleton { property string maxBrightnessPath: "" property int maxBrightness: 100 property bool ignoreNextChange: false + property bool initInProgress: false // Signal for brightness changes signal brightnessUpdated(real newBrightness) @@ -296,14 +434,22 @@ Singleton { Logger.d("Brightness", "Internal brightness:", current + "/" + max + " =", monitor.brightness); Logger.d("Brightness", "Using backlight device:", monitor.backlightDevice); } + } else { + monitor.backlightDevice = ""; + monitor.brightnessPath = ""; + monitor.maxBrightnessPath = ""; } } // Always update monitor.brightnessUpdated(monitor.brightness); root.monitorBrightnessChanged(monitor, monitor.brightness); + monitor.initInProgress = false; } } + onExited: (exitCode, exitStatus) => { + monitor.initInProgress = false; + } } readonly property real stepSize: Settings.data.brightness.brightnessStep / 100.0 @@ -379,12 +525,18 @@ Singleton { } else if (!isDdc) { monitor.commandRunning = true; monitor.ignoreNextChange = true; - setBrightnessProc.command = ["brightnessctl", "s", rounded + "%"]; + var backlightDeviceName = root.getBacklightDeviceName(monitor.backlightDevice); + if (backlightDeviceName !== "") { + setBrightnessProc.command = ["brightnessctl", "-d", backlightDeviceName, "s", rounded + "%"]; + } else { + setBrightnessProc.command = ["brightnessctl", "s", rounded + "%"]; + } setBrightnessProc.running = true; } } function initBrightness(): void { + monitor.initInProgress = true; if (isAppleDisplay) { initProc.command = ["asdbctl", "get"]; initProc.running = true; @@ -392,16 +544,24 @@ Singleton { initProc.command = ["ddcutil", "-b", busNum, "--sleep-multiplier=0.05", "getvcp", "10", "--brief"]; initProc.running = true; } else if (!isDdc) { - // Internal backlight - find the first available backlight device and get its info - // This now returns: device_path, current_brightness, max_brightness (on separate lines) - initProc.command = ["sh", "-c", "for dev in /sys/class/backlight/*; do " + " if [ -f \"$dev/brightness\" ] && [ -f \"$dev/max_brightness\" ]; then " + " echo \"$dev\"; " + " cat \"$dev/brightness\"; " + " cat \"$dev/max_brightness\"; " + " break; " + " fi; " + "done"]; + // Internal backlight: first try explicit output mapping, then fall back to first available. + var preferredDevicePath = root.getMappedBacklightDevice(modelData.name); + var probeScript = [ + "preferred=\"$1\"", + "if [ -n \"$preferred\" ] && [ ! -d \"$preferred\" ]; then preferred=\"/sys/class/backlight/$preferred\"; fi", + "selected=\"\"", + "if [ -n \"$preferred\" ] && [ -f \"$preferred/brightness\" ] && [ -f \"$preferred/max_brightness\" ]; then selected=\"$preferred\"; else for dev in /sys/class/backlight/*; do if [ -f \"$dev/brightness\" ] && [ -f \"$dev/max_brightness\" ]; then selected=\"$dev\"; break; fi; done; fi", + "if [ -n \"$selected\" ]; then echo \"$selected\"; cat \"$selected/brightness\"; cat \"$selected/max_brightness\"; fi" + ].join("; "); + initProc.command = ["sh", "-c", probeScript, "sh", preferredDevicePath]; initProc.running = true; + } else { + monitor.initInProgress = false; } } onBusNumChanged: initBrightness() - onIsDdcChanged: if (isDdc) - initBrightness() + onIsDdcChanged: initBrightness() Component.onCompleted: initBrightness() } } diff --git a/Services/System/HostService.qml b/Services/System/HostService.qml index 41f360260..83ca32c31 100644 --- a/Services/System/HostService.qml +++ b/Services/System/HostService.qml @@ -192,16 +192,23 @@ Singleton { stderr: StdioCollector {} } - // Read /etc/hostname - FileView { - id: hostName - path: "/etc/hostname" - onLoaded: { - const name = text().trim(); - if (name) { - root.hostName = name; - Logger.i("HostService", "resolved hostname", name); + // Resolve hostname from distro-specific locations. + // Prefer /etc/hostname, fallback to Gentoo's /etc/conf.d/hostname. + Process { + id: hostNameProcess + command: ["sh", "-c", + "if [ -r /etc/hostname ]; then sed -n '1p' /etc/hostname; exit 0; fi; if [ -r /etc/conf.d/hostname ]; then v=$(sed -n -E 's/^[[:space:]]*[Hh][Oo][Ss][Tt][Nn][Aa][Mm][Ee][[:space:]]*=[[:space:]]*//p' /etc/conf.d/hostname | sed -n '1p'); v=$(printf '%s' \"$v\" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//; s/^\"//; s/\"$//; s/^\x27//; s/\x27$//'); printf '%s\n' \"$v\"; exit 0; fi; exit 0"] + running: true + + stdout: StdioCollector { + onStreamFinished: { + const name = String(text || "").trim(); + if (name.length > 0) { + root.hostName = name; + Logger.i("HostService", "resolved hostname", name); + } } } + stderr: StdioCollector {} } } diff --git a/Services/System/NotificationService.qml b/Services/System/NotificationService.qml index b469dead5..b256140f3 100644 --- a/Services/System/NotificationService.qml +++ b/Services/System/NotificationService.qml @@ -547,7 +547,15 @@ Singleton { // Image handling function queueImage(path, appName, summary, notificationId) { - if (!path || !path.startsWith("image://") || !notificationId) + if (!path || !notificationId) + return; + + // Cache image:// URIs and temporary file paths (e.g. /tmp/ from Chromium) + const filePath = path.startsWith("file://") ? path.substring(7) : path; + const isImageUri = path.startsWith("image://"); + const isTempFile = (path.startsWith("/") || path.startsWith("file://")) && filePath.startsWith("/tmp/"); + + if (!isImageUri && !isTempFile) return; ImageCacheService.getNotificationIcon(path, appName, summary, function (cachedPath, success) { diff --git a/Services/Theming/AppThemeService.qml b/Services/Theming/AppThemeService.qml index 12e0acb37..137e57317 100644 --- a/Services/Theming/AppThemeService.qml +++ b/Services/Theming/AppThemeService.qml @@ -13,16 +13,19 @@ Singleton { // When the wallpaper changes, regenerate theme if necessary function onWallpaperChanged(screenName, path) { - if (!Settings.data.colorSchemes.useWallpaperColors) - return; - var effectiveMonitor = Settings.data.colorSchemes.monitorForColors; if (effectiveMonitor === "" || effectiveMonitor === undefined) { effectiveMonitor = Screen.name; } - if (screenName === effectiveMonitor) { + if (screenName !== effectiveMonitor) + return; + + if (Settings.data.colorSchemes.useWallpaperColors) { generateFromWallpaper(); + } else { + // Re-run predefined scheme templates so {{image}} reflects the new wallpaper path + ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme); } } } @@ -77,6 +80,11 @@ Singleton { function generateFromPredefinedScheme(schemeData) { Logger.i("AppThemeService", "Generating templates from predefined color scheme"); const mode = Settings.data.colorSchemes.darkMode ? "dark" : "light"; - TemplateProcessor.processPredefinedScheme(schemeData, mode); + var effectiveMonitor = Settings.data.colorSchemes.monitorForColors; + if (effectiveMonitor === "" || effectiveMonitor === undefined) { + effectiveMonitor = Screen.name; + } + const wallpaperPath = WallpaperService.getWallpaper(effectiveMonitor) || ""; + TemplateProcessor.processPredefinedScheme(schemeData, mode, wallpaperPath); } } diff --git a/Services/Theming/TemplateProcessor.qml b/Services/Theming/TemplateProcessor.qml index 4aa3125a8..4af72d633 100644 --- a/Services/Theming/TemplateProcessor.qml +++ b/Services/Theming/TemplateProcessor.qml @@ -118,16 +118,17 @@ Singleton { * Uses --scheme flag to expand 14-color scheme to full 48-color palette * Uses debouncing to prevent spawning multiple processes when spamming scheme changes */ - function processPredefinedScheme(schemeData, mode) { + function processPredefinedScheme(schemeData, mode, wallpaperPath) { pendingPredefinedRequest = { schemeData: schemeData, - mode: mode + mode: mode, + wallpaperPath: wallpaperPath || "" }; pendingWallpaperRequest = null; debounceTimer.restart(); } - function executePredefinedScheme(schemeData, mode) { + function executePredefinedScheme(schemeData, mode, wallpaperPath) { // 1. Handle terminal themes (runtime generation or pre-rendered file copy) handleTerminalThemes(schemeData, mode); @@ -161,10 +162,12 @@ Singleton { // Run Python template processor with --scheme flag // Don't pass --mode so templates get both dark and light colors (e.g., zed.json needs both) // Pass --default-mode so "default" in templates resolves to the current theme mode - script += `python3 "${templateProcessorScript}" --scheme '${schemeJsonPathEsc}' --config '${configPathEsc}' --default-mode ${mode}\n`; + // Pass wallpaper as positional arg so image_path is available in templates (no extraction occurs when --scheme is used) + const wpArg = wallpaperPath ? `'${wallpaperPath.replace(/'/g, "'\\''")}'` : ""; + script += `python3 "${templateProcessorScript}" ${wpArg} --scheme '${schemeJsonPathEsc}' --config '${configPathEsc}' --default-mode ${mode}\n`; // Add user templates if enabled - script += buildUserTemplateCommandForPredefined(schemeData, mode); + script += buildUserTemplateCommandForPredefined(schemeData, mode, wallpaperPath); generateProcess.command = ["sh", "-c", script]; generateProcess.running = true; @@ -515,7 +518,7 @@ Singleton { return script; } - function buildUserTemplateCommandForPredefined(schemeData, mode) { + function buildUserTemplateCommandForPredefined(schemeData, mode, wallpaperPath) { if (!Settings.data.templates.enableUserTheming) return ""; @@ -523,13 +526,15 @@ Singleton { // Reuse the scheme JSON already written by processPredefinedScheme() const schemeJsonPathEsc = schemeJsonPath.replace(/'/g, "'\\''"); + const wpArg = wallpaperPath ? `'${wallpaperPath.replace(/'/g, "'\\''")}'` : ""; let script = "\n# Execute user templates with predefined scheme colors\n"; script += `if [ -f '${userConfigPath}' ]; then\n`; // Use --scheme flag with the already-written scheme JSON // Don't pass --mode so user templates get both dark and light colors // Pass --default-mode so "default" in templates resolves to the current theme mode - script += ` python3 "${templateProcessorScript}" --scheme '${schemeJsonPathEsc}' --config '${userConfigPath}' --default-mode ${mode}\n`; + // Pass wallpaper as positional arg so image_path is available in templates + script += ` python3 "${templateProcessorScript}" ${wpArg} --scheme '${schemeJsonPathEsc}' --config '${userConfigPath}' --default-mode ${mode}\n`; script += "fi"; return script; @@ -551,7 +556,7 @@ Singleton { } else if (pendingPredefinedRequest) { const req = pendingPredefinedRequest; pendingPredefinedRequest = null; - executePredefinedScheme(req.schemeData, req.mode); + executePredefinedScheme(req.schemeData, req.mode, req.wallpaperPath); } else { Logger.d("TemplateProcessor", "executePendingRequest: no pending request"); } diff --git a/Services/UI/ImageCacheService.qml b/Services/UI/ImageCacheService.qml index 09d939977..d2ed489eb 100644 --- a/Services/UI/ImageCacheService.qml +++ b/Services/UI/ImageCacheService.qml @@ -162,8 +162,11 @@ Singleton { return; } - // File paths are used directly, not cached - if (imageUri.startsWith("/") || imageUri.startsWith("file://")) { + // Resolve bare file path for temp check + const filePath = imageUri.startsWith("file://") ? imageUri.substring(7) : imageUri; + + // File paths in persistent locations are used directly, not cached + if ((imageUri.startsWith("/") || imageUri.startsWith("file://")) && !isTemporaryPath(filePath)) { callback(imageUri, false); return; } @@ -171,12 +174,59 @@ Singleton { const cacheKey = generateNotificationKey(imageUri, appName, summary); const cachedPath = notificationsDir + cacheKey + ".png"; + // Temporary file paths are copied to cache before the source is cleaned up + if (imageUri.startsWith("/") || imageUri.startsWith("file://")) { + processRequest(cacheKey, cachedPath, imageUri, callback, function () { + copyTempFileToCache(filePath, cachedPath, cacheKey); + }); + return; + } + processRequest(cacheKey, cachedPath, imageUri, callback, function () { // Notifications always use Qt fallback (image:// URIs can't be read by ImageMagick) queueFallbackProcessing(imageUri, cachedPath, cacheKey, 64); }); } + // Check if a path is in a temporary directory that may be cleaned up + function isTemporaryPath(path) { + return path.startsWith("/tmp/"); + } + + // Copy a temporary file to the cache directory + function copyTempFileToCache(sourcePath, destPath, cacheKey) { + const srcEsc = sourcePath.replace(/'/g, "'\\''"); + const dstEsc = destPath.replace(/'/g, "'\\''"); + + const processString = ` + import QtQuick + import Quickshell.Io + Process { + command: ["cp", "--", "${srcEsc}", "${dstEsc}"] + stdout: StdioCollector {} + stderr: StdioCollector {} + } + `; + + queueUtilityProcess({ + name: "CopyTempFile_" + cacheKey, + processString: processString, + onComplete: function (exitCode) { + if (exitCode === 0) { + Logger.d("ImageCache", "Temp file cached:", destPath); + notifyCallbacks(cacheKey, destPath, true); + } else { + Logger.w("ImageCache", "Failed to cache temp file:", sourcePath); + notifyCallbacks(cacheKey, "", false); + } + }, + onError: function () { + Logger.e("ImageCache", "Error caching temp file:", sourcePath); + notifyCallbacks(cacheKey, "", false); + } + }); + } + // ------------------------------------------------- // Public API: Get Circular Avatar (256x256) // ------------------------------------------------- diff --git a/Widgets/NImageRounded.qml b/Widgets/NImageRounded.qml index 8ede5bf91..848b0cc92 100644 --- a/Widgets/NImageRounded.qml +++ b/Widgets/NImageRounded.qml @@ -15,7 +15,7 @@ Item { property color borderColor: "transparent" property int imageFillMode: Image.PreserveAspectCrop - readonly property bool showFallback: (fallbackIcon !== undefined && fallbackIcon !== "") && (imagePath === undefined || imagePath === "") + readonly property bool showFallback: (fallbackIcon !== undefined && fallbackIcon !== "") && (imagePath === undefined || imagePath === "" || imageSource.status === Image.Error) readonly property int status: imageSource.status Rectangle {