From 863c08978c860ea7698248a27f41867a1e134120 Mon Sep 17 00:00:00 2001 From: Corey Woodworth Date: Sun, 2 Nov 2025 23:00:29 -0500 Subject: [PATCH 01/30] committing files that won't stack --- Modules/Bar/Widgets/LockKeys.qml | 42 ++--- .../Bar/WidgetSettings/LockKeysSettings.qml | 146 ++++++++++++------ Services/BarWidgetRegistry.qml | 6 +- 3 files changed, 112 insertions(+), 82 deletions(-) diff --git a/Modules/Bar/Widgets/LockKeys.qml b/Modules/Bar/Widgets/LockKeys.qml index 3b356e85..cc1d7443 100644 --- a/Modules/Bar/Widgets/LockKeys.qml +++ b/Modules/Bar/Widgets/LockKeys.qml @@ -30,11 +30,14 @@ Rectangle { readonly property string barPosition: Settings.data.bar.position readonly property bool isVertical: barPosition === "left" || barPosition === "right" - readonly property string iconStyle: (widgetSettings.indicatorStyle !== undefined) ? widgetSettings.indicatorStyle : widgetMetadata.indicatorStyle readonly property bool showCaps: (widgetSettings.showCapsLock !== undefined) ? widgetSettings.showCapsLock : widgetMetadata.showCapsLock readonly property bool showNum: (widgetSettings.showNumLock !== undefined) ? widgetSettings.showNumLock : widgetMetadata.showNumLock readonly property bool showScroll: (widgetSettings.showScrollLock !== undefined) ? widgetSettings.showScrollLock : widgetMetadata.showScrollLock + readonly property string capsIcon: widgetSettings.capsLockIcon !== undefined ? widgetSettings.capsLockIcon : widgetMetadata.capsLockIcon + readonly property string numIcon: widgetSettings.numLockIcon !== undefined ? widgetSettings.numLockIcon : widgetMetadata.numLockIcon + readonly property string scrollIcon: widgetSettings.scrollLockIcon !== undefined ? widgetSettings.scrollLockIcon : widgetMetadata.scrollLockIcon + implicitWidth: isVertical ? Style.capsuleHeight : Math.round(layout.implicitWidth + Style.marginM * 2) implicitHeight: isVertical ? Math.round(layout.implicitHeight + Style.marginM * 2) : Style.capsuleHeight @@ -51,8 +54,6 @@ Rectangle { implicitWidth: rowLayout.visible ? rowLayout.implicitWidth : colLayout.implicitWidth implicitHeight: rowLayout.visible ? rowLayout.implicitHeight : colLayout.implicitHeight - readonly property var indicatorStyle: root.getIndicatorStyle(root.iconStyle) - RowLayout { id: rowLayout visible: !root.isVertical @@ -60,17 +61,17 @@ Rectangle { NIcon { visible: root.showCaps - icon: layout.indicatorStyle[0] + icon: root.capsIcon color: LockKeysService.capsLockOn ? Color.mTertiary : Qt.alpha(Color.mOnSurfaceVariant, 0.3) } NIcon { visible: root.showNum - icon: layout.indicatorStyle[1] + icon: root.numIcon color: LockKeysService.numLockOn ? Color.mTertiary : Qt.alpha(Color.mOnSurfaceVariant, 0.3) } NIcon { visible: root.showScroll - icon: layout.indicatorStyle[2] + icon: root.scrollIcon color: LockKeysService.scrollLockOn ? Color.mTertiary : Qt.alpha(Color.mOnSurfaceVariant, 0.3) } } @@ -82,42 +83,19 @@ Rectangle { NIcon { visible: root.showCaps - icon: layout.indicatorStyle[0] + icon: root.capsIcon color: LockKeysService.capsLockOn ? Color.mTertiary : Qt.alpha(Color.mOnSurfaceVariant, 0.3) } NIcon { visible: root.showNum - icon: layout.indicatorStyle[1] + icon: root.numIcon color: LockKeysService.numLockOn ? Color.mTertiary : Qt.alpha(Color.mOnSurfaceVariant, 0.3) } NIcon { visible: root.showScroll - icon: layout.indicatorStyle[2] + icon: root.scrollIcon color: LockKeysService.scrollLockOn ? Color.mTertiary : Qt.alpha(Color.mOnSurfaceVariant, 0.3) } } } - - function getIndicatorStyle(styleName) { - switch (styleName) { - case "large": - return ["letter-c", "letter-n", "letter-s"] - case "small": - return ["letter-c-small", "letter-n-small", "letter-s-small"] - case "square": - return ["square-letter-c", "square-letter-n", "square-letter-s"] - case "square-round": - return ["square-rounded-letter-c", "square-rounded-letter-n", "square-rounded-letter-s"] - case "circle": - return ["circle-letter-c", "circle-letter-n", "circle-letter-s"] - case "circle-dash": - return ["circle-dashed-letter-c", "circle-dashed-letter-n", "circle-dashed-letter-s"] - case "circle-dot": - return ["circle-dotted-letter-c", "circle-dotted-letter-n", "circle-dotted-letter-s"] - case "hex": - return ["hexagon-letter-c", "hexagon-letter-n", "hexagon-letter-s"] - default: - return ["letter-c", "letter-n", "letter-s"] - } - } } diff --git a/Modules/Settings/Bar/WidgetSettings/LockKeysSettings.qml b/Modules/Settings/Bar/WidgetSettings/LockKeysSettings.qml index 541ce9d8..192ce1a3 100644 --- a/Modules/Settings/Bar/WidgetSettings/LockKeysSettings.qml +++ b/Modules/Settings/Bar/WidgetSettings/LockKeysSettings.qml @@ -14,71 +14,121 @@ ColumnLayout { property var widgetMetadata: null // Local state - property string valueIndicatorStyle: widgetData.indicatorStyle !== undefined ? widgetData.indicatorStyle : widgetMetadata.indicatorStyle property bool valueShowCapsLock: widgetData.showCapsLock !== undefined ? widgetData.showCapsLock : widgetMetadata.showCapsLock property bool valueShowNumLock: widgetData.showNumLock !== undefined ? widgetData.showNumLock : widgetMetadata.showNumLock property bool valueShowScrollLock: widgetData.showScrollLock !== undefined ? widgetData.showScrollLock : widgetMetadata.showScrollLock + property string capsIcon: widgetData.capsLockIcon !== undefined ? widgetData.capsLockIcon : widgetMetadata.capsLockIcon + property string numIcon: widgetData.numLockIcon !== undefined ? widgetData.numLockIcon : widgetMetadata.numLockIcon + property string scrollIcon: widgetData.scrollLockIcon !== undefined ? widgetData.scrollLockIcon : widgetMetadata.scrollLockIcon + function saveSettings() { var settings = Object.assign({}, widgetData || {}) - settings.indicatorStyle = valueIndicatorStyle settings.showCapsLock = valueShowCapsLock settings.showNumLock = valueShowNumLock settings.showScrollLock = valueShowScrollLock + settings.capsLockIcon = capsIcon + settings.numLockIcon = numIcon + settings.scrollLockIcon = scrollIcon return settings } - NComboBox { - Layout.fillWidth: true - label: I18n.tr("bar.widget-settings.lock-keys.indicator-style.label") - description: I18n.tr("bar.widget-settings.lock-keys.indicator-style.description") - model: [{ - "key": "large", - "name": I18n.tr("bar.widget-settings.lock-keys.indicator-style.large") - }, { - "key": "small", - "name": I18n.tr("bar.widget-settings.lock-keys.indicator-style.small") - }, { - "key": "square", - "name": I18n.tr("bar.widget-settings.lock-keys.indicator-style.square") - }, { - "key": "square-round", - "name": I18n.tr("bar.widget-settings.lock-keys.indicator-style.square-round") - }, { - "key": "circle", - "name": I18n.tr("bar.widget-settings.lock-keys.indicator-style.circle") - }, { - "key": "circle-dash", - "name": I18n.tr("bar.widget-settings.lock-keys.indicator-style.circle-dash") - }, { - "key": "circle-dot", - "name": I18n.tr("bar.widget-settings.lock-keys.indicator-style.circle-dot") - }, { - "key": "hex", - "name": I18n.tr("bar.widget-settings.lock-keys.indicator-style.hex") - }] - currentKey: valueIndicatorStyle - onSelected: key => valueIndicatorStyle = key + RowLayout { + spacing: Style.marginM + + NToggle { + label: I18n.tr("bar.widget-settings.lock-keys.show-caps-lock.label") + description: I18n.tr("bar.widget-settings.lock-keys.show-caps-lock.description") + checked: valueShowCapsLock + onToggled: checked => valueShowCapsLock = checked + } + + NIcon { + Layout.alignment: Qt.AlignVCenter + icon: capsIcon + pointSize: Style.fontSizeXL + visible: capsIcon !== "" + } + + NButton { + text: I18n.tr("bar.widget-settings.custom-button.browse") + onClicked: capsPicker.open() + enabled: valueShowCapsLock + } } - NToggle { - label: I18n.tr("bar.widget-settings.lock-keys.show-caps-lock.label") - description: I18n.tr("bar.widget-settings.lock-keys.show-caps-lock.description") - checked: valueShowCapsLock - onToggled: checked => valueShowCapsLock = checked + NIconPicker { + id: capsPicker + initialIcon: capsIcon + query: "letter-c" + onIconSelected: function (iconName) { + capsIcon = iconName + } } - NToggle { - label: I18n.tr("bar.widget-settings.lock-keys.show-num-lock.label") - description: I18n.tr("bar.widget-settings.lock-keys.show-num-lock.description") - checked: valueShowNumLock - onToggled: checked => valueShowNumLock = checked + RowLayout { + spacing: Style.marginM + + NToggle { + label: I18n.tr("bar.widget-settings.lock-keys.show-num-lock.label") + description: I18n.tr("bar.widget-settings.lock-keys.show-num-lock.description") + checked: valueShowNumLock + onToggled: checked => valueShowNumLock = checked + } + + NIcon { + Layout.alignment: Qt.AlignVCenter + icon: numIcon + pointSize: Style.fontSizeXL + visible: numIcon !== "" + } + + NButton { + text: I18n.tr("bar.widget-settings.custom-button.browse") + onClicked: numPicker.open() + enabled: valueShowNumLock + } } - NToggle { - label: I18n.tr("bar.widget-settings.lock-keys.show-scroll-lock.label") - description: I18n.tr("bar.widget-settings.lock-keys.show-scroll-lock.description") - checked: valueShowScrollLock - onToggled: checked => valueShowScrollLock = checked + NIconPicker { + id: numPicker + initialIcon: numIcon + query: "letter-n" + onIconSelected: function (iconName) { + numIcon = iconName + } + } + + RowLayout { + spacing: Style.marginM + + NToggle { + label: I18n.tr("bar.widget-settings.lock-keys.show-scroll-lock.label") + description: I18n.tr("bar.widget-settings.lock-keys.show-scroll-lock.description") + checked: valueShowScrollLock + onToggled: checked => valueShowScrollLock = checked + } + + NIcon { + Layout.alignment: Qt.AlignVCenter + icon: scrollIcon + pointSize: Style.fontSizeXL + visible: scrollIcon !== "" + } + + NButton { + text: I18n.tr("bar.widget-settings.custom-button.browse") + onClicked: scrollPicker.open() + enabled: valueShowScrollLock + } + } + + NIconPicker { + id: scrollPicker + initialIcon: scrollIcon + query: "letter-s" + onIconSelected: function (iconName) { + scrollIcon = iconName + } } } diff --git a/Services/BarWidgetRegistry.qml b/Services/BarWidgetRegistry.qml index b4425195..678e9ad8 100644 --- a/Services/BarWidgetRegistry.qml +++ b/Services/BarWidgetRegistry.qml @@ -101,10 +101,12 @@ Singleton { }, "LockKeys": { "allowUserSettings": true, - "indicatorStyle": "large", "showCapsLock": true, "showNumLock": true, - "showScrollLock": true + "showScrollLock": true, + "capsLockIcon": "letter-c", + "numLockIcon": "letter-n", + "scrollLockIcon": "letter-s", }, "MediaMini": { "allowUserSettings": true, From 8730f0bb1623e74c78786cf31166e9660067451e Mon Sep 17 00:00:00 2001 From: Corey Woodworth Date: Sun, 2 Nov 2025 23:15:47 -0500 Subject: [PATCH 02/30] Stacking changes and translations done --- Assets/Translations/de.json | 13 +----------- Assets/Translations/en.json | 21 +++++-------------- Assets/Translations/es.json | 13 +----------- Assets/Translations/fr.json | 13 +----------- Assets/Translations/pt.json | 13 +----------- Assets/Translations/zh-CN.json | 13 +----------- .../Bar/WidgetSettings/LockKeysSettings.qml | 6 +++--- 7 files changed, 13 insertions(+), 79 deletions(-) diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index f9224eef..aae6719d 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -1291,18 +1291,7 @@ "description": "Scroll-Lock-Status anzeigen.", "label": "Rollenfeststelltaste" }, - "indicator-style": { - "circle": "Kreisbuchstaben", - "circle-dash": "Gestrichelte Kreisbuchstaben", - "circle-dot": "Punktierte Kreisbuchstaben", - "description": "Symbolstil für die Feststelltastenanzeigen", - "hex": "Sechseck-Buchstaben", - "label": "Indikatorstil", - "large": "Große Buchstaben", - "small": "Kleinbuchstaben", - "square": "Quadratische Buchstaben", - "square-round": "Abgerundete quadratische Buchstaben" - } + "browse": "Durchsuchen" } } }, diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index 76b87f64..fac2278f 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -1262,18 +1262,6 @@ } }, "lock-keys": { - "indicator-style": { - "label": "Indicator Style", - "description": "Icon style for the lock key indicators", - "large": "Large Letters", - "small": "Small Letters", - "square": "Square Letters", - "square-round": "Rounded Squre Letters", - "circle": "Circle Letters", - "circle-dash": "Dashed Circle Letters", - "circle-dot": "Dotted Circle Letters", - "hex": "Hexagon Letters" - }, "show-caps-lock": { "label": "Caps Lock", "description": "Display caps lock status." @@ -1285,7 +1273,8 @@ "show-scroll-lock": { "label": "Scroll Lock", "description": "Display scroll lock status." - } + }, + "browse": "Browse" } } }, @@ -1456,9 +1445,9 @@ "colors": { "primary": "Primary", "secondary": "Secondary", - "tertiary": "Tertiary", - "error": "Error", - "onSurface": "On Surface" + "tertiary": "Tertiary", + "error": "Error", + "onSurface": "On Surface" }, "bar": { "position": { diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index 7252dbf9..f12e357f 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -1274,18 +1274,7 @@ "description": "Mostrar el estado de Bloq Despl.", "label": "Bloq Despl" }, - "indicator-style": { - "circle": "Letras circulares", - "circle-dash": "Letras de círculo discontinuo", - "circle-dot": "Letras de círculo punteado", - "description": "Estilo de icono para los indicadores de las teclas de bloqueo", - "hex": "Letras hexagonales", - "label": "Estilo del indicador", - "large": "Letras grandes", - "small": "Letras minúsculas", - "square": "Letras cuadradas", - "square-round": "Letras cuadradas redondeadas" - } + "browse": "Navegar" } } }, diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index 1e0b879d..3eb65361 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -1274,18 +1274,7 @@ "description": "Afficher l'état du verrouillage du défilement.", "label": "Verr Maj" }, - "indicator-style": { - "circle": "Lettres circulaires", - "circle-dash": "Lettres en cercle pointillé", - "circle-dot": "Lettres en pointillés dans un cercle", - "description": "Style d'icône pour les indicateurs de la touche de verrouillage", - "hex": "Lettres hexagonales", - "label": "Style d'indicateur", - "large": "Grandes lettres", - "small": "Petites lettres", - "square": "Lettres carrées", - "square-round": "Lettres carrées arrondies" - } + "browse": "Parcourir" } } }, diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index 7b3722cd..f10087de 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -1274,18 +1274,7 @@ "description": "Exibir o status do Scroll Lock.", "label": "Scroll Lock" }, - "indicator-style": { - "circle": "Letras Circulares", - "circle-dash": "Letras em Círculo Tracejadas", - "circle-dot": "Letras de Círculo Pontilhado", - "description": "Estilo de ícone para os indicadores da tecla de bloqueio.", - "hex": "Letras Hexagonais", - "label": "Estilo do Indicador", - "large": "Letras grandes", - "small": "Letras minúsculas", - "square": "Letras Quadradas", - "square-round": "Letras Quadradas Arredondadas" - } + "browse": "Navegar" } } }, diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index d107d181..78a2f4fa 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -1274,18 +1274,7 @@ "description": "显示滚动锁定状态。", "label": "滚动锁定" }, - "indicator-style": { - "hex": "六边形字母", - "large": "大写字母", - "small": "小写字母", - "square": "方块字", - "square-round": "圆角方形字母", - "circle": "圆圈字母", - "circle-dash": "虚线圆圈字母", - "circle-dot": "虚线圆圈字母", - "description": "锁键指示器的图标样式", - "label": "指标样式" - } + "browse": "浏览" } } }, diff --git a/Modules/Settings/Bar/WidgetSettings/LockKeysSettings.qml b/Modules/Settings/Bar/WidgetSettings/LockKeysSettings.qml index 192ce1a3..5ffbf55a 100644 --- a/Modules/Settings/Bar/WidgetSettings/LockKeysSettings.qml +++ b/Modules/Settings/Bar/WidgetSettings/LockKeysSettings.qml @@ -51,7 +51,7 @@ ColumnLayout { } NButton { - text: I18n.tr("bar.widget-settings.custom-button.browse") + text: I18n.tr("bar.widget-settings.lock-keys.browsedd") onClicked: capsPicker.open() enabled: valueShowCapsLock } @@ -84,7 +84,7 @@ ColumnLayout { } NButton { - text: I18n.tr("bar.widget-settings.custom-button.browse") + text: I18n.tr("bar.widget-settings.lock-keys.browse") onClicked: numPicker.open() enabled: valueShowNumLock } @@ -117,7 +117,7 @@ ColumnLayout { } NButton { - text: I18n.tr("bar.widget-settings.custom-button.browse") + text: I18n.tr("bar.widget-settings.lock-keys.browse") onClicked: scrollPicker.open() enabled: valueShowScrollLock } From b044c6ddd1a82e9241505fc609465f97d6548001 Mon Sep 17 00:00:00 2001 From: MrDowntempo Date: Sun, 2 Nov 2025 23:26:29 -0500 Subject: [PATCH 03/30] Removed superfluous comma --- Services/BarWidgetRegistry.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Services/BarWidgetRegistry.qml b/Services/BarWidgetRegistry.qml index 678e9ad8..55214a5a 100644 --- a/Services/BarWidgetRegistry.qml +++ b/Services/BarWidgetRegistry.qml @@ -106,7 +106,7 @@ Singleton { "showScrollLock": true, "capsLockIcon": "letter-c", "numLockIcon": "letter-n", - "scrollLockIcon": "letter-s", + "scrollLockIcon": "letter-s" }, "MediaMini": { "allowUserSettings": true, From 101b27fcc78062448db410848c2599f2fc53cf45 Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Mon, 3 Nov 2025 00:53:02 -0500 Subject: [PATCH 04/30] New windowing system Large commit that totally refactor of the way we handle the bar and panels. Testing should focus on Panels, Bar, Keyboard Focus, IPC calls. Changes brief: - One NFullScreenWindow per screen which handle it's bar and dedicated panels. - Added shadows - Reintroduced dimming - New panels animations - Proper Z ordering - Panels on overlay laywer is not reimplemented, if we do it then the bar will be on the Overlay too - Panel dragging was not reimplemented, to be discussed before reimplementing - Still a WIP, need to work more on shadows and polishing + debugging. --- Assets/Translations/de.json | 14 +- Assets/Translations/en.json | 14 +- Assets/Translations/es.json | 14 +- Assets/Translations/fr.json | 14 +- Assets/Translations/pt.json | 14 +- Assets/Translations/zh-CN.json | 14 +- CLAUDE.md | 366 +++++++ Commons/Logger.qml | 18 +- Commons/Settings.qml | 7 + Modules/Background/ScreenCorners.qml | 146 --- Modules/Bar/Audio/AudioPanel.qml | 1 - Modules/Bar/Bar.qml | 439 ++++---- Modules/Bar/Battery/BatteryPanel.qml | 3 +- Modules/Bar/Bluetooth/BluetoothPanel.qml | 1 - Modules/Bar/Calendar/CalendarPanel.qml | 22 +- Modules/Bar/Extras/BarWidgetLoader.qml | 70 +- Modules/Bar/WiFi/WiFiPanel.qml | 1 - Modules/Bar/Widgets/Battery.qml | 2 +- Modules/Bar/Widgets/Bluetooth.qml | 4 +- Modules/Bar/Widgets/Brightness.qml | 4 +- Modules/Bar/Widgets/Clock.qml | 4 +- Modules/Bar/Widgets/ControlCenter.qml | 4 +- Modules/Bar/Widgets/CustomButton.qml | 2 +- Modules/Bar/Widgets/Microphone.qml | 2 +- Modules/Bar/Widgets/NightLight.qml | 2 +- Modules/Bar/Widgets/NotificationHistory.qml | 2 +- Modules/Bar/Widgets/SessionMenu.qml | 2 +- Modules/Bar/Widgets/Tray.qml | 1 - Modules/Bar/Widgets/Volume.qml | 2 +- Modules/Bar/Widgets/WallpaperSelector.qml | 2 +- Modules/Bar/Widgets/WiFi.qml | 4 +- Modules/ControlCenter/Cards/ProfileCard.qml | 11 +- Modules/ControlCenter/Cards/ShortcutsCard.qml | 10 +- Modules/ControlCenter/ControlCenterPanel.qml | 1 - .../ControlCenterWidgetLoader.qml | 40 +- Modules/ControlCenter/Widgets/Bluetooth.qml | 4 +- Modules/ControlCenter/Widgets/NightLight.qml | 2 +- .../ControlCenter/Widgets/Notifications.qml | 2 +- .../ControlCenter/Widgets/PowerProfile.qml | 2 +- .../ControlCenter/Widgets/ScreenRecorder.qml | 2 +- .../Widgets/WallpaperSelector.qml | 2 +- Modules/ControlCenter/Widgets/WiFi.qml | 2 +- Modules/Launcher/Launcher.qml | 236 ++--- .../Notification/NotificationHistoryPanel.qml | 5 +- Modules/SessionMenu/SessionMenu.qml | 123 +-- Modules/Settings/SettingsPanel.qml | 92 +- Modules/Settings/Tabs/BarTab.qml | 12 +- Modules/Settings/Tabs/ControlCenterTab.qml | 4 +- Modules/Settings/Tabs/LauncherTab.qml | 6 +- Modules/Settings/Tabs/UserInterfaceTab.qml | 7 + Modules/SetupWizard/SetupWizard.qml | 21 +- Modules/Wallpaper/WallpaperPanel.qml | 112 +- Services/BatteryService.qml | 4 +- Services/CavaService.qml | 10 +- Services/IPCService.qml | 126 ++- Services/MediaService.qml | 2 +- Services/PanelService.qml | 56 +- Widgets/BarExclusionZone.qml | 74 ++ Widgets/NFullScreenWindow.qml | 608 +++++++++++ Widgets/NPanel.qml | 959 ++++++++---------- Widgets/NShapedRectangle.qml | 264 +++-- shell.qml | 229 ++++- 62 files changed, 2727 insertions(+), 1496 deletions(-) create mode 100644 CLAUDE.md delete mode 100644 Modules/Background/ScreenCorners.qml create mode 100644 Widgets/BarExclusionZone.qml create mode 100644 Widgets/NFullScreenWindow.qml diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index f9224eef..4949b25f 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -226,13 +226,17 @@ }, "floating": { "label": "Schwebende Statusleiste", - "description": "Statusleiste als schwebende 'Pille' anzeigen. Hinweis: Dies verschiebt die Bildschirmecken an die Ränder." + "description": "Statusleiste als schwebende 'Pille' anzeigen." }, "margins": { "label": "Ränder", "description": "Ränder um die schwebende Statusleiste anpassen.", "vertical": "Vertikal", "horizontal": "Horizontal" + }, + "outer-corners": { + "description": "Zeigt nach außen gewölbte Ecken auf der Leiste an.", + "label": "Äußere Ecken" } }, "widgets": { @@ -584,6 +588,10 @@ "foot": { "description": "Schreibt {filepath} und lädt neu", "description-missing": "Erfordert {app} Terminal" + }, + "alacritty": { + "description": "Schreibe {Dateipfad} und lade neu", + "description-missing": "Benötigt die Installation von {app}" } }, "programs": { @@ -872,6 +880,10 @@ "panels-attached-to-bar": { "description": "Wenn aktiviert, werden die Panels mit einem schönen, umgekehrten Eckdesign an der Leiste befestigt.", "label": "Paneele an Stange befestigen" + }, + "dim-desktop": { + "description": "Den Desktop abdunkeln, wenn Fenster oder Menüs geöffnet sind.", + "label": "Dimmer Schreibtisch" } }, "lock-screen": { diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index 76b87f64..8bee2a80 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -226,7 +226,11 @@ }, "floating": { "label": "Floating bar", - "description": "Displays the bar as a floating 'pill'. Note: This will move the screen corners to the edges." + "description": "Displays the bar as a floating 'pill'." + }, + "outer-corners": { + "label": "Outer corners", + "description": "Displays outwardly curved corners on the bar." }, "margins": { "label": "Margins", @@ -577,6 +581,10 @@ "terminal": { "label": "Terminal", "description": "Terminal emulator theming.", + "alacritty": { + "description": "Write {filepath} and reload", + "description-missing": "Requires {app} to be installed" + }, "kitty": { "description": "Write {filepath} and reload", "description-missing": "Requires {app} to be installed" @@ -851,6 +859,10 @@ "description": "Changes the size of the general user interface, excluding the bar.", "reset-scaling": "Reset interface scaling" }, + "dim-desktop": { + "label": "Dim desktop", + "description": "Dim the desktop when panels or menus are open." + }, "border-radius": { "label": "Border radius", "description": "Controls the corner roundness of windows, buttons, and other elements.", diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index 7252dbf9..e4eb12b6 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -226,13 +226,17 @@ }, "floating": { "label": "Barra flotante", - "description": "Muestra la barra como una 'píldora' flotante. Nota: Esto moverá las esquinas de la pantalla a los bordes." + "description": "Muestra la barra como una 'píldora' flotante." }, "margins": { "label": "Márgenes", "description": "Ajusta los márgenes alrededor de la barra flotante.", "vertical": "Vertical", "horizontal": "Horizontal" + }, + "outer-corners": { + "description": "Muestra esquinas curvadas hacia afuera en la barra.", + "label": "Esquinas exteriores" } }, "widgets": { @@ -584,6 +588,10 @@ "foot": { "description": "Escribir {filepath} y recargar", "description-missing": "Requiere que {app} esté instalado" + }, + "alacritty": { + "description": "Escribe {filepath} y recarga", + "description-missing": "Requiere que {app} esté instalado/a." } }, "programs": { @@ -872,6 +880,10 @@ "panels-attached-to-bar": { "description": "Cuando está habilitado, los paneles se adjuntarán a la barra con un hermoso diseño de esquina invertida.", "label": "Adjuntar paneles a la barra" + }, + "dim-desktop": { + "description": "Atenuar el escritorio cuando los paneles o menús estén abiertos.", + "label": "Dim escritorio" } }, "lock-screen": { diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index 1e0b879d..cadc7836 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -226,13 +226,17 @@ }, "floating": { "label": "Barre flottante", - "description": "Affiche la barre sous forme de 'pilule' flottante. Note : Ceci déplacera les coins de l'écran vers les bords." + "description": "Affiche la barre sous forme de 'pilule' flottante." }, "margins": { "label": "Marges", "description": "Ajustez les marges autour de la barre flottante.", "vertical": "Verticale", "horizontal": "Horizontale" + }, + "outer-corners": { + "description": "Affiche des coins incurvés vers l'extérieur sur la barre.", + "label": "Coins extérieurs" } }, "widgets": { @@ -584,6 +588,10 @@ "foot": { "description": "Écrire ~/.config/foot/themes/noctalia et recharger", "description-missing": "Nécessite que le terminal foot soit installé" + }, + "alacritty": { + "description": "Écrire {filepath} et recharger.", + "description-missing": "Nécessite l'installation de {app}" } }, "programs": { @@ -872,6 +880,10 @@ "panels-attached-to-bar": { "description": "Lorsque cette option est activée, les panneaux seront attachés à la barre avec un design élégant de coin inversé.", "label": "Fixer les panneaux à la barre." + }, + "dim-desktop": { + "description": "Atténuer le bureau lorsque des panneaux ou des menus sont ouverts.", + "label": "Dim bureau" } }, "lock-screen": { diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index 7b3722cd..41ca38e0 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -226,13 +226,17 @@ }, "floating": { "label": "Barra flutuante", - "description": "Exibe a barra como uma 'pílula' flutuante. Nota: Isso moverá os cantos da tela para as bordas." + "description": "Exibe a barra como uma 'pílula' flutuante." }, "margins": { "label": "Margens", "description": "Ajuste as margens ao redor da barra flutuante.", "vertical": "Vertical", "horizontal": "Horizontal" + }, + "outer-corners": { + "description": "Exibe cantos curvados para fora na barra.", + "label": "Cantos externos" } }, "widgets": { @@ -546,6 +550,10 @@ "foot": { "description": "Escrever {filepath} e recarregar", "description-missing": "Requer que o {app} esteja instalado" + }, + "alacritty": { + "description": "Escreva {filepath} e recarregue.", + "description-missing": "Requer que o {app} esteja instalado." } }, "programs": { @@ -872,6 +880,10 @@ "panels-attached-to-bar": { "description": "Quando ativado, os painéis serão anexados à barra com um belo design de canto invertido.", "label": "Anexar painéis à barra" + }, + "dim-desktop": { + "description": "Escurecer a área de trabalho quando painéis ou menus estiverem abertos.", + "label": "Dim área de trabalho" } }, "lock-screen": { diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index d107d181..11a1b176 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -226,13 +226,17 @@ }, "floating": { "label": "浮动状态栏", - "description": "将状态栏显示为浮动样式(类似于一个药丸)。注意:这会将屏幕边角移动到边缘。" + "description": "将工具栏显示为浮动的“药丸”形状。" }, "margins": { "label": "边距", "description": "调整浮动状态栏周围的边距。", "vertical": "垂直", "horizontal": "水平" + }, + "outer-corners": { + "description": "在栏上显示向外弯曲的角。", + "label": "外角" } }, "widgets": { @@ -584,6 +588,10 @@ "foot": { "description": "写入 {filepath} 并重新加载", "description-missing": "需要安装 {app}" + }, + "alacritty": { + "description": "写入 {filepath} 并重新加载", + "description-missing": "需要安装 {app}" } }, "programs": { @@ -872,6 +880,10 @@ "panels-attached-to-bar": { "description": "启用后,面板将以美观的倒角设计附加到栏上。", "label": "将面板连接到杆上" + }, + "dim-desktop": { + "description": "当面板或菜单打开时,桌面变暗。", + "label": "昏暗的桌面" } }, "lock-screen": { diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..1fb05a83 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,366 @@ +# Noctalia Shell + +**A beautiful, minimal desktop shell for Wayland that actually gets out of your way.** + +Noctalia is a desktop shell built on Quickshell (Qt/QML framework) with a warm lavender aesthetic. It provides a complete desktop environment experience with panels, dock, notifications, lock screen, and extensive customization options. + +## AI Guidance + +* After receiving tool results, carefully reflect on their quality and determine optimal next steps before proceeding. Use your thinking to plan and iterate based on this new information, and then take the best next action. +* For maximum efficiency, whenever you need to perform multiple independent operations, invoke all relevant tools simultaneously rather than sequentially. +* Before you finish, please verify your solution +* Do what has been asked; nothing more, nothing less. +* NEVER create files unless they're absolutely necessary for achieving your goal. +* ALWAYS prefer editing an existing file to creating a new one. +* NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User. + +## Project Overview + +- **Primary Language**: QML (Qt Quick) +- **Framework**: Quickshell (Wayland-native shell framework) +- **Supported Compositors**: Niri, Hyprland, Sway (with support for other Wayland compositors) +- **License**: MIT +- **Design Philosophy**: "quiet by design" - minimal, non-intrusive UI + +## Architecture + +### Core Entry Point +- [shell.qml](shell.qml) - Main shell root that orchestrates all components + - Initializes services in a specific order + - Manages screen-specific instances of bars and panels + - Uses lazy loading with QML Loaders for memory optimization + - Implements NFullScreenWindow for each screen to manage bar + panels + +### Directory Structure + +#### `/Modules/` - UI Components +Core visual modules and panels: +- **Bar/** - Top/bottom bar with multiple widgets + - Audio, Bluetooth, Battery, Calendar, WiFi submodules + - Extras for additional bar functionality +- **ControlCenter/** - Quick settings panel + - Cards/ - MediaCard, ShortcutsCard, etc. + - Widgets/ - WiFi, Bluetooth, NightLight, PowerProfile, KeepAwake, ScreenRecorder, Notifications, WallpaperSelector +- **Dock/** - Application dock/launcher +- **Launcher/** - Application launcher/search +- **LockScreen/** - Screen locking functionality +- **Notification/** - Notification system and history +- **OSD/** - On-screen display for volume, brightness, etc. +- **Settings/** - Shell configuration UI +- **SetupWizard/** - First-run setup experience +- **SessionMenu/** - Power menu (logout, shutdown, etc.) +- **Toast/** - Toast notifications +- **Tooltip/** - Tooltip system +- **Wallpaper/** - Wallpaper management +- **Background/** - Background/wallpaper rendering +- **Audio/** - Audio visualizations (MirroredSpectrum, WaveSpectrum, LinearSpectrum) + +#### `/Services/` - Business Logic +Core services that power the shell (40+ services): + +**System Integration:** +- `CompositorService.qml` - Compositor-agnostic API +- `HyprlandService.qml` - Hyprland-specific integration +- `NiriService.qml` - Niri-specific integration +- `SwayService.qml` - Sway-specific integration +- `IPCService.qml` - Inter-process communication + +**Hardware & System:** +- `AudioService.qml` - Audio control and monitoring +- `BatteryService.qml` - Battery status and management +- `BluetoothService.qml` - Bluetooth device management +- `BrightnessService.qml` - Screen brightness control +- `NetworkService.qml` - Network connection management +- `PowerProfileService.qml` - Power profile management +- `SystemStatService.qml` - System resource monitoring + +**UI & Theming:** +- `AppThemeService.qml` - Application theming engine +- `ColorSchemeService.qml` - Color scheme management +- `DarkModeService.qml` - Dark/light mode switching +- `FontService.qml` - Font management +- `WallpaperService.qml` - Wallpaper handling (with Matugen integration) +- `MatugenTemplates.qml` - Material You color generation templates +- `NightLightService.qml` - Blue light filter + +**Features:** +- `NotificationService.qml` - Notification daemon +- `MediaService.qml` - Media player control (MPRIS) +- `CalendarService.qml` - Calendar integration +- `ClipboardService.qml` - Clipboard management +- `LocationService.qml` - Geolocation for weather, etc. +- `ScreenRecorderService.qml` - Screen recording functionality +- `IdleInhibitorService.qml` - Prevent screen idle/sleep +- `KeyboardLayoutService.qml` - Keyboard layout switching +- `LockKeysService.qml` - Caps/Num lock status + +**Infrastructure:** +- `BarService.qml` - Bar visibility and state management +- `BarWidgetRegistry.qml` - Registry for bar widgets +- `ControlCenterWidgetRegistry.qml` - Registry for control center widgets +- `PanelService.qml` - Panel state management +- `ToastService.qml` - Toast notification service +- `TooltipService.qml` - Tooltip service +- `HooksService.qml` - Custom hook execution +- `ProgramCheckerService.qml` - Check for installed programs +- `DistroService.qml` - Linux distribution detection +- `GitHubService.qml` - GitHub API integration +- `UpdateService.qml` - Update checking +- `CavaService.qml` - Audio visualization (Cava integration) + +#### `/Commons/` - Shared Utilities +Common components used throughout the shell: +- `Settings.qml` - Centralized settings management +- `I18n.qml` - Internationalization/translations +- `Color.qml` - Color utilities and helpers +- `Icons.qml` - Icon management +- `TablerIcons.qml` - Tabler icon set (207KB icon definitions) +- `ThemeIcons.qml` - Theme-specific icons +- `Logger.qml` - Logging utility +- `Style.qml` - Shared styling definitions +- `Time.qml` - Time utilities +- `KeyboardLayout.qml` - Keyboard layout definitions + +#### `/Widgets/` - Reusable UI Components +40+ custom QML widgets with the "N" prefix (Noctalia): +- **Layout**: NBox, NPanel, NScrollView, NListView, NDivider +- **Input**: NButton, NIconButton, NIconButtonHot, NTextInput, NSlider, NSpinBox, NToggle, NCheckbox, NRadioButton, NComboBox, NSearchableComboBox +- **Display**: NLabel, NText, NIcon, NHeader, NImageCached, NImageCircled, NImageRounded +- **Dialogs**: NColorPickerDialog, NFilePicker +- **Special**: NContextMenu, NColorPicker, NIconPicker, NCircleStat, NCollapsible, NSectionEditor, NReorderCheckboxes, NDateTimeTokens, NBusyIndicator, NShapedRectangle +- **System**: NFullScreenWindow, BarExclusionZone + +#### `/Helpers/` - JavaScript Utilities +Helper JavaScript modules: +- `AdvancedMath.js` - Advanced mathematical functions +- `ColorsConvert.js` - Color conversion utilities +- `FuzzySort.js` - Fuzzy search implementation +- `QtObj2JS.js` - Qt object to JavaScript conversion +- `sha256.js` - SHA-256 hashing +- `Debug.js` - Debug utilities + +#### `/Assets/` - Resources +- Screenshots, icons, logos, themes +- Default wallpapers +- Theme resources + +#### `/Shaders/` - Graphics Shaders +Custom shader effects for visual polish + +#### `/Bin/` - Executable Scripts +Helper scripts and utilities + +## Key Features + +### 1. Multi-Monitor Support +- Per-monitor bar configuration (TODO) +- Screen-specific panel instances +- Exclusion zones for proper compositor integration + +### 2. Theming System +- Material You color generation (Matugen integration) +- Dark/light mode support +- Customizable color schemes +- Font customization +- Per-app theming capabilities + +### 3. Compositor Integration +- Native support for Niri, Hyprland, Sway +- Compositor-agnostic service layer +- Workspace management +- Window control + +### 4. Panel System +Advanced panel management via NFullScreenWindow: +- Launcher panel +- Control Center panel +- Calendar panel +- Settings panel +- Widget settings panel +- Notification history panel +- Session menu panel +- WiFi panel +- Bluetooth panel +- Audio panel +- Wallpaper panel +- Battery panel + +All panels use z-index layering and component-based loading. + +### 5. Customization +- Setup wizard for first-time users +- Extensive settings interface +- Widget registry system for adding custom widgets +- Hook system for custom scripts +- Reorderable UI elements + +### 6. Audio Features +- Multiple visualization types (Mirrored, Wave, Linear spectrum) +- MPRIS media player integration +- Audio device switching +- Volume OSD + +### 7. Notifications +- Custom notification daemon +- Notification history +- Do Not Disturb mode +- Per-app notification settings + +## Development Setup + +```bash +# Run the shell (requires Quickshell to be installed) +quickshell -p shell.qml + +# Or use the shorthand +qs -p . + +# Run with verbose output for debugging +qs -v -p shell.qml + +# Code formatting and linting +qmlfmt -e -b 360 -t 2 -i 2 -w /path/to/file.qml # Format a QML file (requires qmlfmt, do not use qmlformat) +qmllint **/*.qml # Lint all QML files for syntax errors +``` + +### Nix/NixOS (Recommended) +```bash +# Enter development shell +nix develop + +# Or use the legacy shell +nix-shell +``` + +The dev shell includes: +- Quickshell with required features +- Development utilities +- Required environment variables + +### Package Structure +- Nix flake with NixOS and Home Manager modules +- Quickshell dependency (with X11 disabled, i3 enabled, hyprland enabled) +- App2unit integration for .desktop file management + +## Configuration + +Settings are managed through `Commons/Settings.qml`: +- Persistent configuration storage +- Settings versioning +- Migration handling +- Type-safe settings access + +## Service Initialization Order + +From [shell.qml:150-164](shell.qml#L150-L164): +1. WallpaperService +2. AppThemeService +3. ColorSchemeService +4. BarWidgetRegistry +5. LocationService +6. NightLightService +7. DarkModeService +8. FontService +9. HooksService +10. BluetoothService +11. BatteryService +12. IdleInhibitorService +13. PowerProfileService +14. DistroService + +This order is critical - services depend on previously initialized services. + +## Component Lifecycle + +1. **Shell Root Initialization** + - Wait for I18n to load translations + - Wait for Settings to load configuration + +2. **Service Initialization** + - Services initialize in dependency order + - Each service may depend on Settings, I18n, or other services + +3. **Screen Components** + - NFullScreenWindow created per screen + - Bar and panel components loaded lazily + - Exclusion zones created after window loads + +4. **Background Components** + - Background/wallpaper + - Overview (workspace overview) + - Screen corners + - Dock + - Notifications + - Lock screen + - Toast overlay + - OSD + +## Special Patterns + +### Lazy Loading +Components use QML Loaders extensively: +- `active` property controls when components load +- `asynchronous` for non-blocking loads +- Memory optimization for unused screens/panels + +### Panel Management +NFullScreenWindow pattern: +- Single fullscreen window per screen +- Manages bar + all overlay panels +- Z-index based layering (panels at z-index 50) +- Component-based architecture for panels + +### Registry Pattern +BarWidgetRegistry and ControlCenterWidgetRegistry: +- Centralized widget registration +- Dynamic widget loading +- Easy extension point for custom widgets + +## Git Hooks +Uses `lefthook` for git hooks (see lefthook.yml) + +## Community Resources +- Documentation: https://docs.noctalia.dev +- Discord: https://discord.noctalia.dev +- GitHub: https://github.com/noctalia-dev/noctalia-shell + +## Contributing +See [development guidelines](https://docs.noctalia.dev/development/guideline) + +## Current Work (Git Status) +- Modified: ControlCenter widgets (ShortcutsCard, WiFi) +- Recent commits focus on shadow effects and panel animations +- Working on bar shadow behavior when panels open + +## Notes for AI Assistants + +### Code Style +- QML component names use PascalCase +- Service names end with "Service.qml" +- Widget names start with "N" prefix (e.g., NButton, NPanel) +- JavaScript helpers in Helpers/ directory + +### Common Tasks +1. **Adding a new bar widget**: Register in BarWidgetRegistry +2. **Adding a control center widget**: Register in ControlCenterWidgetRegistry +3. **Creating a service**: Follow the Service pattern, add to init order if needed +4. **Modifying theming**: Check AppThemeService and ColorSchemeService +5. **Panel work**: Edit in Modules/, ensure proper z-index in shell.qml + +### Important Files to Check +- Settings schema: `Commons/Settings.qml` +- Service initialization: `shell.qml` (Component.onCompleted) +- Panel registration: `shell.qml` (panelComponents array) +- Theme system: `Services/AppThemeService.qml` +- Color generation: `Services/MatugenTemplates.qml` + +### Testing +- Test on target compositors: Niri, Hyprland, Sway +- Check multi-monitor scenarios +- Verify lazy loading doesn't break functionality +- Test settings persistence across restarts + +### Debugging +- Use `Logger.qml` for logging (Logger.i, Logger.d, Logger.w, Logger.e) +- Check console output for service initialization messages +- Verify service initialization order if adding dependencies diff --git a/Commons/Logger.qml b/Commons/Logger.qml index 5613d890..5c18b90b 100644 --- a/Commons/Logger.qml +++ b/Commons/Logger.qml @@ -25,27 +25,27 @@ Singleton { } } - // Info log (always visible) - function i(...args) { - var msg = _formatMessage(...args) - console.log(msg) - } - // Debug log (only when Settings.isDebug is true) function d(...args) { if (Settings && Settings.isDebug) { var msg = _formatMessage(...args) - console.log(msg) + console.debug(msg) } } - // Warning log + // Info log (always visible) + function i(...args) { + var msg = _formatMessage(...args) + console.info(msg) + } + + // Warning log (always visible) function w(...args) { var msg = _formatMessage(...args) console.warn(msg) } - // Error log + // Error log (always visible) function e(...args) { var msg = _formatMessage(...args) console.error(msg) diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 1483a519..105b6df4 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -146,6 +146,12 @@ Singleton { property real marginVertical: 0.25 property real marginHorizontal: 0.25 + // Bar outer corners (inverted/concave corners at bar edges when not floating) + property bool outerCorners: true + + // Reserves space with compositor + property bool exclusive: true + // Widget configuration for modular bar system property JsonObject widgets widgets: JsonObject { @@ -182,6 +188,7 @@ Singleton { // general property JsonObject general: JsonObject { property string avatarImage: "" + property bool dimDesktop: true property bool showScreenCorners: false property bool forceBlackScreenCorners: false property real scaleRatio: 1.0 diff --git a/Modules/Background/ScreenCorners.qml b/Modules/Background/ScreenCorners.qml deleted file mode 100644 index 6c35b008..00000000 --- a/Modules/Background/ScreenCorners.qml +++ /dev/null @@ -1,146 +0,0 @@ -import QtQuick -import QtQuick.Effects -import Quickshell -import Quickshell.Wayland -import qs.Commons -import qs.Services -import qs.Widgets - -Loader { - active: Settings.data.general.showScreenCorners && (!Settings.data.ui.panelsAttachedToBar || Settings.data.bar.backgroundOpacity >= 1 || Settings.data.bar.floating) - - sourceComponent: Variants { - model: Quickshell.screens - - PanelWindow { - id: root - - required property ShellScreen modelData - screen: modelData - - property color cornerColor: Settings.data.general.forceBlackScreenCorners ? Qt.rgba(0, 0, 0, 1) : Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity) - property real cornerRadius: Style.screenRadius - property real cornerSize: Style.screenRadius - - // Helper properties for margin calculations - readonly property bool barOnThisMonitor: BarService.isVisible && ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.backgroundOpacity > 0 - readonly property real barMargin: !Settings.data.bar.floating && barOnThisMonitor ? Style.barHeight : 0 - - color: Color.transparent - - WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.namespace: "quickshell-corner" - WlrLayershell.keyboardFocus: WlrKeyboardFocus.None - - anchors { - top: true - bottom: true - left: true - right: true - } - - margins { - // When bar is floating, corners should be at screen edges (no margins) - // When bar is not floating, respect bar margins as before - top: Settings.data.bar.position === "top" ? barMargin : 0 - bottom: Settings.data.bar.position === "bottom" ? barMargin : 0 - left: Settings.data.bar.position === "left" ? barMargin : 0 - right: Settings.data.bar.position === "right" ? barMargin : 0 - } - - mask: Region {} - - // Reusable corner canvas component - component CornerCanvas: Canvas { - id: corner - - required property real arcCenterX - required property real arcCenterY - - width: root.cornerSize - height: root.cornerSize - antialiasing: true - renderTarget: Canvas.FramebufferObject - smooth: true - - onPaint: { - const ctx = getContext("2d") - if (!ctx) - return - - ctx.reset() - ctx.clearRect(0, 0, width, height) - - // Fill the entire area with the corner color - ctx.fillStyle = root.cornerColor - ctx.fillRect(0, 0, width, height) - - // Cut out the rounded corner using destination-out - ctx.globalCompositeOperation = "destination-out" - ctx.fillStyle = "#ffffff" - ctx.beginPath() - ctx.arc(arcCenterX, arcCenterY, root.cornerRadius, 0, 2 * Math.PI) - ctx.fill() - } - - onWidthChanged: if (available) - requestPaint() - onHeightChanged: if (available) - requestPaint() - } - - // Consolidated repaint handler for all corners - property var corners: [topLeftCorner, topRightCorner, bottomLeftCorner, bottomRightCorner] - - onCornerColorChanged: { - corners.forEach(corner => { - if (corner.available) - corner.requestPaint() - }) - } - - onCornerRadiusChanged: { - corners.forEach(corner => { - if (corner.available) - corner.requestPaint() - }) - } - - // Top-left concave corner - CornerCanvas { - id: topLeftCorner - anchors.top: parent.top - anchors.left: parent.left - arcCenterX: width - arcCenterY: height - } - - // Top-right concave corner - CornerCanvas { - id: topRightCorner - anchors.top: parent.top - anchors.right: parent.right - arcCenterX: 0 - arcCenterY: height - } - - // Bottom-left concave corner - CornerCanvas { - id: bottomLeftCorner - anchors.bottom: parent.bottom - anchors.left: parent.left - arcCenterX: width - arcCenterY: 0 - } - - // Bottom-right concave corner - CornerCanvas { - id: bottomRightCorner - anchors.bottom: parent.bottom - anchors.right: parent.right - arcCenterX: 0 - arcCenterY: 0 - } - } - } -} diff --git a/Modules/Bar/Audio/AudioPanel.qml b/Modules/Bar/Audio/AudioPanel.qml index dc4e1779..7393f846 100644 --- a/Modules/Bar/Audio/AudioPanel.qml +++ b/Modules/Bar/Audio/AudioPanel.qml @@ -18,7 +18,6 @@ NPanel { preferredWidth: 380 * Style.uiScaleRatio preferredHeight: 500 * Style.uiScaleRatio - panelKeyboardFocus: true // Connections to update local volumes when AudioService changes Connections { diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 31504433..0de76baa 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -10,60 +10,92 @@ import qs.Widgets import qs.Modules.Notification import qs.Modules.Bar.Extras -Variants { - model: Quickshell.screens +// Bar Component +Item { + id: root - delegate: Loader { - id: root + // This property will be set by NFullScreenWindow + property ShellScreen screen: null - required property ShellScreen modelData + // Expose bar region for click-through mask + readonly property var barRegion: barContentLoader.item?.children[0] || null - active: BarService.isVisible && modelData && modelData.name ? (Settings.data.bar.monitors.includes(modelData.name) || (Settings.data.bar.monitors.length === 0)) : false + // Bar positioning properties + readonly property string barPosition: Settings.data.bar.position || "top" + readonly property bool barIsVertical: barPosition === "left" || barPosition === "right" + readonly property bool barFloating: Settings.data.bar.floating || false + readonly property real barMarginH: barFloating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0 + readonly property real barMarginV: barFloating ? Settings.data.bar.marginVertical * Style.marginXL : 0 - sourceComponent: PanelWindow { - screen: modelData || null + // Fill the parent (the Loader) + anchors.fill: parent - WlrLayershell.namespace: "noctalia-bar" + // Register bar when screen becomes available + onScreenChanged: { + if (screen && screen.name) { + Logger.d("Bar", "Bar screen set to:", screen.name) + Logger.d("Bar", " Position:", barPosition, "Floating:", barFloating) + Logger.d("Bar", " Margins - H:", barMarginH, "V:", barMarginV) + BarService.registerBar(screen.name) + } + } - implicitHeight: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? screen.height : Style.barHeight - implicitWidth: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? Style.barHeight : screen.width - color: Color.transparent + // Wait for screen to be set before loading bar content + Loader { + id: barContentLoader + anchors.fill: parent + active: root.screen !== null && root.screen !== undefined - anchors { - top: Settings.data.bar.position === "top" || Settings.data.bar.position === "left" || Settings.data.bar.position === "right" - bottom: Settings.data.bar.position === "bottom" || Settings.data.bar.position === "left" || Settings.data.bar.position === "right" - left: Settings.data.bar.position === "left" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom" - right: Settings.data.bar.position === "right" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom" - } + sourceComponent: Item { + anchors.fill: parent - // Floating bar margins - only apply when floating is enabled - // Also don't apply margin on the opposite side ot the bar orientation, ex: if bar is floating on top, margin is only applied on top, not bottom. - margins { - top: Settings.data.bar.floating && Settings.data.bar.position !== "bottom" ? Settings.data.bar.marginVertical * Style.marginXL : 0 - bottom: Settings.data.bar.floating && Settings.data.bar.position !== "top" ? Settings.data.bar.marginVertical * Style.marginXL : 0 - left: Settings.data.bar.floating && Settings.data.bar.position !== "right" ? Settings.data.bar.marginHorizontal * Style.marginXL : 0 - right: Settings.data.bar.floating && Settings.data.bar.position !== "left" ? Settings.data.bar.marginHorizontal * Style.marginXL : 0 - } + // Background fill with shadow + NShapedRectangle { + id: bar - Component.onCompleted: { - if (modelData && modelData.name) { - BarService.registerBar(modelData.name) - } - } + // Position and size the bar based on orientation and floating margins + x: (root.barPosition === "right") ? (parent.width - Style.barHeight - root.barMarginH) : root.barMarginH + y: (root.barPosition === "bottom") ? (parent.height - Style.barHeight - root.barMarginV) : root.barMarginV + width: root.barIsVertical ? Style.barHeight : (parent.width - root.barMarginH * 2) + height: root.barIsVertical ? (parent.height - root.barMarginV * 2) : Style.barHeight - Item { - anchors.fill: parent - clip: true + backgroundColor: Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity) - // Background fill with shadow - Rectangle { - id: bar + // Floating bar rounded corners + topLeftRadius: Settings.data.bar.floating || topLeftInverted ? Style.radiusL : 0 + topRightRadius: Settings.data.bar.floating || topRightInverted ? Style.radiusL : 0 + bottomLeftRadius: Settings.data.bar.floating || bottomLeftInverted ? Style.radiusL : 0 + bottomRightRadius: Settings.data.bar.floating || bottomRightInverted ? Style.radiusL : 0 - anchors.fill: parent - color: Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity) + topLeftInverted: Settings.data.bar.outerCorners && (barPosition === "bottom" || barPosition === "right") + topLeftInvertedDirection: barIsVertical ? "horizontal" : "vertical" + topRightInverted: Settings.data.bar.outerCorners && (barPosition === "bottom" || barPosition === "left") + topRightInvertedDirection: barIsVertical ? "horizontal" : "vertical" - // Floating bar rounded corners - radius: Settings.data.bar.floating ? Style.radiusL : 0 + bottomLeftInverted: Settings.data.bar.outerCorners && (barPosition === "top" || barPosition === "right") + bottomLeftInvertedDirection: barIsVertical ? "horizontal" : "vertical" + bottomRightInverted: Settings.data.bar.outerCorners && (barPosition === "top" || barPosition === "left") + bottomRightInvertedDirection: barIsVertical ? "horizontal" : "vertical" + + // No border on the bar + borderWidth: 0 + + // Shadow configuration + shadowEnabled: true + shadowBlur: 0.5 + // Fade shadow progressively when a panel is attached to the bar to avoid visual disconnection + // shadowOpacity: { + // if (PanelService.openedPanel && PanelService.openedPanel.attachedToBar) { + // // Fade shadow out as panel opens (animationProgress goes from 0 to 1) + // return 1.0 - PanelService.openedPanel.animationProgress + // } + // return 1.0 + // } + Behavior on shadowOpacity { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } } MouseArea { @@ -73,8 +105,11 @@ Variants { preventStealing: true onClicked: function (mouse) { if (mouse.button === Qt.RightButton) { - // Important to pass the screen here so we get the right widget for the actual bar that was clicked. - controlCenterPanel.toggle(BarService.lookupWidget("ControlCenter", screen.name)) + // Look up for any ControlCenter button on this bar + var widget = BarService.lookupWidget("ControlCenter", root.screen.name) + + // Open the panel near the button if any + PanelService.getPanel("controlCenterPanel", root.screen)?.toggle(widget) mouse.accepted = true } } @@ -84,168 +119,188 @@ Variants { anchors.fill: parent sourceComponent: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? verticalBarComponent : horizontalBarComponent } + } + } + } - // For vertical bars - Component { - id: verticalBarComponent - Item { - anchors.fill: parent + // For vertical bars + Component { + id: verticalBarComponent + Item { + anchors.fill: parent + clip: true - // Top section (left widgets) - ColumnLayout { - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - anchors.topMargin: Style.marginM - spacing: Style.marginS + // Top section (left widgets) + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: Style.marginM + spacing: Style.marginS - Repeater { - model: Settings.data.bar.widgets.left - delegate: BarWidgetLoader { - widgetId: (modelData.id !== undefined ? modelData.id : "") - barDensity: Settings.data.bar.density - widgetProps: { - "screen": root.modelData || null, - "widgetId": modelData.id, - "section": "left", - "sectionWidgetIndex": index, - "sectionWidgetsCount": Settings.data.bar.widgets.left.length - } - Layout.alignment: Qt.AlignHCenter - } - } - } + Repeater { + model: Settings.data.bar.widgets.left + delegate: BarWidgetLoader { + required property var modelData + required property int index - // Center section (center widgets) - ColumnLayout { - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - spacing: Style.marginS - - Repeater { - model: Settings.data.bar.widgets.center - delegate: BarWidgetLoader { - widgetId: (modelData.id !== undefined ? modelData.id : "") - barDensity: Settings.data.bar.density - widgetProps: { - "screen": root.modelData || null, - "widgetId": modelData.id, - "section": "center", - "sectionWidgetIndex": index, - "sectionWidgetsCount": Settings.data.bar.widgets.center.length - } - Layout.alignment: Qt.AlignHCenter - } - } - } - - // Bottom section (right widgets) - ColumnLayout { - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: parent.bottom - anchors.bottomMargin: Style.marginM - spacing: Style.marginS - - Repeater { - model: Settings.data.bar.widgets.right - delegate: BarWidgetLoader { - widgetId: (modelData.id !== undefined ? modelData.id : "") - barDensity: Settings.data.bar.density - widgetProps: { - "screen": root.modelData || null, - "widgetId": modelData.id, - "section": "right", - "sectionWidgetIndex": index, - "sectionWidgetsCount": Settings.data.bar.widgets.right.length - } - Layout.alignment: Qt.AlignHCenter - } - } - } + widgetId: modelData.id || "" + barDensity: Settings.data.bar.density + widgetScreen: root.screen + widgetProps: ({ + "widgetId": modelData.id, + "section": "left", + "sectionWidgetIndex": index, + "sectionWidgetsCount": Settings.data.bar.widgets.left.length + }) + Layout.alignment: Qt.AlignHCenter } } + } - // For horizontal bars - Component { - id: horizontalBarComponent - Item { - anchors.fill: parent + // Center section (center widgets) + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginS - // Left Section - RowLayout { - id: leftSection - objectName: "leftSection" - anchors.left: parent.left - anchors.leftMargin: Style.marginS - anchors.verticalCenter: parent.verticalCenter - spacing: Style.marginS + Repeater { + model: Settings.data.bar.widgets.center + delegate: BarWidgetLoader { + required property var modelData + required property int index - Repeater { - model: Settings.data.bar.widgets.left - delegate: BarWidgetLoader { - widgetId: (modelData.id !== undefined ? modelData.id : "") - barDensity: Settings.data.bar.density - widgetProps: { - "screen": root.modelData || null, - "widgetId": modelData.id, - "section": "left", - "sectionWidgetIndex": index, - "sectionWidgetsCount": Settings.data.bar.widgets.left.length - } - Layout.alignment: Qt.AlignVCenter - } - } - } + widgetId: modelData.id || "" + barDensity: Settings.data.bar.density + widgetScreen: root.screen + widgetProps: ({ + "widgetId": modelData.id, + "section": "center", + "sectionWidgetIndex": index, + "sectionWidgetsCount": Settings.data.bar.widgets.center.length + }) + Layout.alignment: Qt.AlignHCenter + } + } + } - // Center Section - RowLayout { - id: centerSection - objectName: "centerSection" - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - spacing: Style.marginS + // Bottom section (right widgets) + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: Style.marginM + spacing: Style.marginS - Repeater { - model: Settings.data.bar.widgets.center - delegate: BarWidgetLoader { - widgetId: (modelData.id !== undefined ? modelData.id : "") - barDensity: Settings.data.bar.density - widgetProps: { - "screen": root.modelData || null, - "widgetId": modelData.id, - "section": "center", - "sectionWidgetIndex": index, - "sectionWidgetsCount": Settings.data.bar.widgets.center.length - } - Layout.alignment: Qt.AlignVCenter - } - } - } + Repeater { + model: Settings.data.bar.widgets.right + delegate: BarWidgetLoader { + required property var modelData + required property int index - // Right Section - RowLayout { - id: rightSection - objectName: "rightSection" - anchors.right: parent.right - anchors.rightMargin: Style.marginS - anchors.verticalCenter: parent.verticalCenter - spacing: Style.marginS + widgetId: modelData.id || "" + barDensity: Settings.data.bar.density + widgetScreen: root.screen + widgetProps: ({ + "widgetId": modelData.id, + "section": "right", + "sectionWidgetIndex": index, + "sectionWidgetsCount": Settings.data.bar.widgets.right.length + }) + Layout.alignment: Qt.AlignHCenter + } + } + } + } + } - Repeater { - model: Settings.data.bar.widgets.right - delegate: BarWidgetLoader { - widgetId: (modelData.id !== undefined ? modelData.id : "") - barDensity: Settings.data.bar.density - widgetProps: { - "screen": root.modelData || null, - "widgetId": modelData.id, - "section": "right", - "sectionWidgetIndex": index, - "sectionWidgetsCount": Settings.data.bar.widgets.right.length - } - Layout.alignment: Qt.AlignVCenter - } - } - } + // For horizontal bars + Component { + id: horizontalBarComponent + Item { + anchors.fill: parent + clip: true + + // Left Section + RowLayout { + id: leftSection + objectName: "leftSection" + anchors.left: parent.left + anchors.leftMargin: Style.marginS + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginS + + Repeater { + model: Settings.data.bar.widgets.left + delegate: BarWidgetLoader { + required property var modelData + required property int index + + widgetId: modelData.id || "" + barDensity: Settings.data.bar.density + widgetScreen: root.screen + widgetProps: ({ + "widgetId": modelData.id, + "section": "left", + "sectionWidgetIndex": index, + "sectionWidgetsCount": Settings.data.bar.widgets.left.length + }) + Layout.alignment: Qt.AlignVCenter + } + } + } + + // Center Section + RowLayout { + id: centerSection + objectName: "centerSection" + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginS + + Repeater { + model: Settings.data.bar.widgets.center + delegate: BarWidgetLoader { + required property var modelData + required property int index + + widgetId: modelData.id || "" + barDensity: Settings.data.bar.density + widgetScreen: root.screen + widgetProps: ({ + "widgetId": modelData.id, + "section": "center", + "sectionWidgetIndex": index, + "sectionWidgetsCount": Settings.data.bar.widgets.center.length + }) + Layout.alignment: Qt.AlignVCenter + } + } + } + + // Right Section + RowLayout { + id: rightSection + objectName: "rightSection" + anchors.right: parent.right + anchors.rightMargin: Style.marginS + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginS + + Repeater { + model: Settings.data.bar.widgets.right + delegate: BarWidgetLoader { + required property var modelData + required property int index + + widgetId: modelData.id || "" + barDensity: Settings.data.bar.density + widgetScreen: root.screen + widgetProps: ({ + "widgetId": modelData.id, + "section": "right", + "sectionWidgetIndex": index, + "sectionWidgetsCount": Settings.data.bar.widgets.right.length + }) + Layout.alignment: Qt.AlignVCenter } } } diff --git a/Modules/Bar/Battery/BatteryPanel.qml b/Modules/Bar/Battery/BatteryPanel.qml index 8f4d2161..4d359dce 100644 --- a/Modules/Bar/Battery/BatteryPanel.qml +++ b/Modules/Bar/Battery/BatteryPanel.qml @@ -11,8 +11,7 @@ NPanel { id: root preferredWidth: 350 * Style.uiScaleRatio - preferredHeight: 250 * Style.uiScaleRatio - panelKeyboardFocus: true + preferredHeight: 210 * Style.uiScaleRatio property var optionsModel: [] diff --git a/Modules/Bar/Bluetooth/BluetoothPanel.qml b/Modules/Bar/Bluetooth/BluetoothPanel.qml index 18563c0d..6255fe93 100644 --- a/Modules/Bar/Bluetooth/BluetoothPanel.qml +++ b/Modules/Bar/Bluetooth/BluetoothPanel.qml @@ -13,7 +13,6 @@ NPanel { preferredWidth: 420 * Style.uiScaleRatio preferredHeight: 500 * Style.uiScaleRatio - panelKeyboardFocus: true panelContent: Rectangle { color: Color.transparent diff --git a/Modules/Bar/Calendar/CalendarPanel.qml b/Modules/Bar/Calendar/CalendarPanel.qml index 7df32214..a2317d61 100644 --- a/Modules/Bar/Calendar/CalendarPanel.qml +++ b/Modules/Bar/Calendar/CalendarPanel.qml @@ -14,7 +14,8 @@ NPanel { property ShellScreen screen readonly property var now: Time.date - panelKeyboardFocus: true + preferredWidth: 500 + preferredHeight: 700 // Helper function to calculate ISO week number function getISOWeekNumber(date) { @@ -44,7 +45,8 @@ NPanel { return numWeeks * rowHeight } - property real contentPreferredHeight: banner.height + calendar.height + weatherLoader.height + Style.marginM * 4 + (Settings.data.location.weatherEnabled && Settings.data.location.showCalendarWeather) * Style.marginM + // Use implicitHeight from content + margins to avoid binding loops + property real contentPreferredHeight: content.implicitHeight + Style.marginL * 2 ColumnLayout { id: content @@ -65,20 +67,6 @@ NPanel { isCurrentMonth = checkIsCurrentMonth() } - Shortcut { - sequence: "Escape" - onActivated: { - if (timerActive) { - cancelTimer() - } else { - cancelTimer() - root.close() - } - } - context: Qt.WidgetShortcut - enabled: root.opened - } - Connections { target: Time function onDateChanged() { @@ -623,7 +611,7 @@ NPanel { onClicked: { const dateWithSlashes = `${(modelData.month + 1).toString().padStart(2, '0')}/${modelData.day.toString().padStart(2, '0')}/${modelData.year.toString().substring(2)}` Quickshell.execDetached(["gnome-calendar", "--date", dateWithSlashes]) - PanelService.getPanel("calendarPanel").toggle(null) + root.close() } onExited: { diff --git a/Modules/Bar/Extras/BarWidgetLoader.qml b/Modules/Bar/Extras/BarWidgetLoader.qml index 37ea0391..fb90c645 100644 --- a/Modules/Bar/Extras/BarWidgetLoader.qml +++ b/Modules/Bar/Extras/BarWidgetLoader.qml @@ -6,15 +6,17 @@ import qs.Commons Item { id: root - property string widgetId: "" - property var widgetProps: ({}) - property string screenName: widgetProps && widgetProps.screen ? widgetProps.screen.name : "" - property string section: widgetProps && widgetProps.section || "" - property int sectionIndex: widgetProps && widgetProps.sectionWidgetIndex || 0 + required property string widgetId + required property var widgetScreen + required property var widgetProps property string barDensity: "default" readonly property real scaling: barDensity === "mini" ? 0.8 : (barDensity === "compact" ? 0.9 : 1.0) + // Extract section info from widgetProps + readonly property string section: widgetProps.section || "" + readonly property int sectionIndex: widgetProps.sectionWidgetIndex || 0 + // Don't reserve space unless the loaded widget is really visible implicitWidth: getImplicitSize(loader.item, "implicitWidth") implicitHeight: getImplicitSize(loader.item, "implicitHeight") @@ -26,56 +28,54 @@ Item { Loader { id: loader anchors.fill: parent - active: widgetId !== "" asynchronous: false - sourceComponent: { - if (!active) { - return null - } - return BarWidgetRegistry.getWidget(widgetId) - } + sourceComponent: BarWidgetRegistry.getWidget(widgetId) onLoaded: { - if (item && widgetProps) { - // Apply properties to loaded widget - for (var prop in widgetProps) { - if (item.hasOwnProperty(prop)) { - item[prop] = widgetProps[prop] - } - } - // Explicitly set scaling property - if (item.hasOwnProperty("scaling")) { - item.scaling = Qt.binding(function () { - return root.scaling - }) + if (!item) + return + + Logger.d("BarWidgetLoader", "Loading widget", widgetId, "on screen:", widgetScreen.name) + + // Apply properties to loaded widget + for (var prop in widgetProps) { + if (item.hasOwnProperty(prop)) { + item[prop] = widgetProps[prop] } } + // Set screen property + if (item.hasOwnProperty("screen")) { + item.screen = widgetScreen + } + + // Set scaling property + if (item.hasOwnProperty("scaling")) { + item.scaling = Qt.binding(function () { + return root.scaling + }) + } + // Register this widget instance with BarService - if (screenName && section) { - BarService.registerWidget(screenName, section, widgetId, sectionIndex, item) - } + BarService.registerWidget(widgetScreen.name, section, widgetId, sectionIndex, item) + // Call custom onLoaded if it exists if (item.hasOwnProperty("onLoaded")) { item.onLoaded() } - - //Logger.i("BarWidgetLoader", "Loaded", widgetId, "on screen", item.screen.name) } Component.onDestruction: { // Unregister when destroyed - if (screenName && section) { - BarService.unregisterWidget(screenName, section, widgetId, sectionIndex) + if (widgetScreen && section) { + BarService.unregisterWidget(widgetScreen.name, section, widgetId, sectionIndex) } - // Explicitly clear references - widgetProps = null } } // Error handling - onWidgetIdChanged: { - if (widgetId && !BarWidgetRegistry.hasWidget(widgetId)) { + Component.onCompleted: { + if (!BarWidgetRegistry.hasWidget(widgetId)) { Logger.w("BarWidgetLoader", "Widget not found in registry:", widgetId) } } diff --git a/Modules/Bar/WiFi/WiFiPanel.qml b/Modules/Bar/WiFi/WiFiPanel.qml index 281ace59..29b09685 100644 --- a/Modules/Bar/WiFi/WiFiPanel.qml +++ b/Modules/Bar/WiFi/WiFiPanel.qml @@ -12,7 +12,6 @@ NPanel { preferredWidth: 420 * Style.uiScaleRatio preferredHeight: 500 * Style.uiScaleRatio - panelKeyboardFocus: true property string passwordSsid: "" property string passwordInput: "" diff --git a/Modules/Bar/Widgets/Battery.qml b/Modules/Bar/Widgets/Battery.qml index e19e564f..7eb2a714 100644 --- a/Modules/Bar/Widgets/Battery.qml +++ b/Modules/Bar/Widgets/Battery.qml @@ -94,7 +94,7 @@ Item { autoHide: false forceOpen: isReady && (testMode || battery.isLaptopBattery) && displayMode === "alwaysShow" forceClose: displayMode === "alwaysHide" || !isReady || (!testMode && !battery.isLaptopBattery) - onClicked: PanelService.getPanel("batteryPanel")?.toggle(this) + onClicked: PanelService.getPanel("batteryPanel", screen)?.toggle(this) tooltipText: { let lines = [] if (testMode) { diff --git a/Modules/Bar/Widgets/Bluetooth.qml b/Modules/Bar/Widgets/Bluetooth.qml index 1c3fd2b0..84ca7f41 100644 --- a/Modules/Bar/Widgets/Bluetooth.qml +++ b/Modules/Bar/Widgets/Bluetooth.qml @@ -54,8 +54,8 @@ Item { autoHide: false forceOpen: !isBarVertical && root.displayMode === "alwaysShow" forceClose: isBarVertical || root.displayMode === "alwaysHide" || BluetoothService.connectedDevices.length === 0 - onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this) - onRightClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this) + onClicked: PanelService.getPanel("bluetoothPanel", screen)?.toggle(this) + onRightClicked: PanelService.getPanel("bluetoothPanel", screen)?.toggle(this) tooltipText: { if (pill.text !== "") { return pill.text diff --git a/Modules/Bar/Widgets/Brightness.qml b/Modules/Bar/Widgets/Brightness.qml index 5246a182..ca3c7879 100644 --- a/Modules/Bar/Widgets/Brightness.qml +++ b/Modules/Bar/Widgets/Brightness.qml @@ -108,13 +108,13 @@ Item { } onClicked: { - var settingsPanel = PanelService.getPanel("settingsPanel") + var settingsPanel = PanelService.getPanel("settingsPanel", screen) settingsPanel.requestedTab = SettingsPanel.Tab.Display settingsPanel.open() } onRightClicked: { - var settingsPanel = PanelService.getPanel("settingsPanel") + var settingsPanel = PanelService.getPanel("settingsPanel", screen) settingsPanel.requestedTab = SettingsPanel.Tab.Display settingsPanel.open() } diff --git a/Modules/Bar/Widgets/Clock.qml b/Modules/Bar/Widgets/Clock.qml index 02e8b6d6..3b7a2208 100644 --- a/Modules/Bar/Widgets/Clock.qml +++ b/Modules/Bar/Widgets/Clock.qml @@ -121,7 +121,7 @@ Rectangle { cursorShape: Qt.PointingHandCursor hoverEnabled: true onEntered: { - if (!PanelService.getPanel("calendarPanel")?.active) { + if (!PanelService.getPanel("calendarPanel", screen)?.active) { TooltipService.show(Screen, root, I18n.tr("clock.tooltip"), BarService.getTooltipDirection()) } } @@ -130,7 +130,7 @@ Rectangle { } onClicked: { TooltipService.hide() - PanelService.getPanel("calendarPanel")?.toggle(this) + PanelService.getPanel("calendarPanel", screen)?.toggle(this) } } } diff --git a/Modules/Bar/Widgets/ControlCenter.qml b/Modules/Bar/Widgets/ControlCenter.qml index 9e08fa84..d6950a35 100644 --- a/Modules/Bar/Widgets/ControlCenter.qml +++ b/Modules/Bar/Widgets/ControlCenter.qml @@ -44,8 +44,8 @@ NIconButton { colorBgHover: useDistroLogo ? Color.mSurfaceVariant : Color.mHover colorBorder: Color.transparent colorBorderHover: useDistroLogo ? Color.mHover : Color.transparent - onClicked: PanelService.getPanel("controlCenterPanel")?.toggle(this) - onRightClicked: PanelService.getPanel("settingsPanel")?.toggle() + onClicked: PanelService.getPanel("controlCenterPanel", screen)?.toggle(this) + onRightClicked: PanelService.getPanel("settingsPanel", screen)?.toggle() IconImage { id: customOrDistroLogo diff --git a/Modules/Bar/Widgets/CustomButton.qml b/Modules/Bar/Widgets/CustomButton.qml index 9c501aef..222b0824 100644 --- a/Modules/Bar/Widgets/CustomButton.qml +++ b/Modules/Bar/Widgets/CustomButton.qml @@ -184,7 +184,7 @@ Item { Logger.i("CustomButton", `Executing command: ${leftClickExec}`) } else if (!hasExec) { // No script was defined, open settings - var settingsPanel = PanelService.getPanel("settingsPanel") + var settingsPanel = PanelService.getPanel("settingsPanel", screen) settingsPanel.requestedTab = SettingsPanel.Tab.Bar settingsPanel.open() } diff --git a/Modules/Bar/Widgets/Microphone.qml b/Modules/Bar/Widgets/Microphone.qml index 4592653d..f0a8cb3b 100644 --- a/Modules/Bar/Widgets/Microphone.qml +++ b/Modules/Bar/Widgets/Microphone.qml @@ -105,7 +105,7 @@ Item { } } onClicked: { - PanelService.getPanel("audioPanel")?.toggle(this) + PanelService.getPanel("audioPanel", screen)?.toggle(this) } onRightClicked: { AudioService.setInputMuted(!AudioService.inputMuted) diff --git a/Modules/Bar/Widgets/NightLight.qml b/Modules/Bar/Widgets/NightLight.qml index 80d2c01d..06580d4e 100644 --- a/Modules/Bar/Widgets/NightLight.qml +++ b/Modules/Bar/Widgets/NightLight.qml @@ -43,7 +43,7 @@ NIconButton { } onRightClicked: { - var settingsPanel = PanelService.getPanel("settingsPanel") + var settingsPanel = PanelService.getPanel("settingsPanel", screen) settingsPanel.requestedTab = SettingsPanel.Tab.Display settingsPanel.open() } diff --git a/Modules/Bar/Widgets/NotificationHistory.qml b/Modules/Bar/Widgets/NotificationHistory.qml index ccceeaa3..55f41d9b 100644 --- a/Modules/Bar/Widgets/NotificationHistory.qml +++ b/Modules/Bar/Widgets/NotificationHistory.qml @@ -56,7 +56,7 @@ NIconButton { colorBorderHover: Color.transparent onClicked: { - var panel = PanelService.getPanel("notificationHistoryPanel") + var panel = PanelService.getPanel("notificationHistoryPanel", screen) panel?.toggle(this) } diff --git a/Modules/Bar/Widgets/SessionMenu.qml b/Modules/Bar/Widgets/SessionMenu.qml index bb24f446..d1b79998 100644 --- a/Modules/Bar/Widgets/SessionMenu.qml +++ b/Modules/Bar/Widgets/SessionMenu.qml @@ -20,5 +20,5 @@ NIconButton { colorFg: Color.mError colorBorder: Color.transparent colorBorderHover: Color.transparent - onClicked: PanelService.getPanel("sessionMenuPanel")?.toggle() + onClicked: PanelService.getPanel("sessionMenuPanel", screen)?.toggle() } diff --git a/Modules/Bar/Widgets/Tray.qml b/Modules/Bar/Widgets/Tray.qml index 07e7b755..f8f61375 100644 --- a/Modules/Bar/Widgets/Tray.qml +++ b/Modules/Bar/Widgets/Tray.qml @@ -279,7 +279,6 @@ Rectangle { function open() { visible = true - PanelService.willOpenPanel(trayPanel) } function close() { diff --git a/Modules/Bar/Widgets/Volume.qml b/Modules/Bar/Widgets/Volume.qml index d494d3cb..0c603c6f 100644 --- a/Modules/Bar/Widgets/Volume.qml +++ b/Modules/Bar/Widgets/Volume.qml @@ -90,7 +90,7 @@ Item { } } onClicked: { - PanelService.getPanel("audioPanel")?.toggle(this) + PanelService.getPanel("audioPanel", screen)?.toggle(this) } onRightClicked: { AudioService.setOutputMuted(!AudioService.muted) diff --git a/Modules/Bar/Widgets/WallpaperSelector.qml b/Modules/Bar/Widgets/WallpaperSelector.qml index e48f368a..322b54c0 100644 --- a/Modules/Bar/Widgets/WallpaperSelector.qml +++ b/Modules/Bar/Widgets/WallpaperSelector.qml @@ -20,5 +20,5 @@ NIconButton { colorFg: Color.mOnSurface colorBorder: Color.transparent colorBorderHover: Color.transparent - onClicked: PanelService.getPanel("wallpaperPanel")?.toggle(this) + onClicked: PanelService.getPanel("wallpaperPanel", screen)?.toggle(this) } diff --git a/Modules/Bar/Widgets/WiFi.qml b/Modules/Bar/Widgets/WiFi.qml index 25de98ae..19f37ed9 100644 --- a/Modules/Bar/Widgets/WiFi.qml +++ b/Modules/Bar/Widgets/WiFi.qml @@ -76,8 +76,8 @@ Item { autoHide: false forceOpen: !isBarVertical && root.displayMode === "alwaysShow" forceClose: isBarVertical || root.displayMode === "alwaysHide" || !pill.text - onClicked: PanelService.getPanel("wifiPanel")?.toggle(this) - onRightClicked: PanelService.getPanel("wifiPanel")?.toggle(this) + onClicked: PanelService.getPanel("wifiPanel", screen)?.toggle(this) + onRightClicked: PanelService.getPanel("wifiPanel", screen)?.toggle(this) tooltipText: { if (pill.text !== "") { return pill.text diff --git a/Modules/ControlCenter/Cards/ProfileCard.qml b/Modules/ControlCenter/Cards/ProfileCard.qml index 1eaffb3a..408c3f19 100644 --- a/Modules/ControlCenter/Cards/ProfileCard.qml +++ b/Modules/ControlCenter/Cards/ProfileCard.qml @@ -60,8 +60,9 @@ NBox { icon: "settings" tooltipText: I18n.tr("tooltips.open-settings") onClicked: { - settingsPanel.requestedTab = SettingsPanel.Tab.General - settingsPanel.open() + var panel = PanelService.getPanel("settingsPanel", screen) + panel.requestedTab = SettingsPanel.Tab.General + panel.open() } } @@ -69,8 +70,8 @@ NBox { icon: "power" tooltipText: I18n.tr("tooltips.session-menu") onClicked: { - sessionMenuPanel.open() - controlCenterPanel.close() + PanelService.getPanel("sessionMenuPanel", screen)?.open() + PanelService.getPanel("controlCenterPanel", screen)?.close() } } @@ -78,7 +79,7 @@ NBox { icon: "close" tooltipText: I18n.tr("tooltips.close") onClicked: { - controlCenterPanel.close() + PanelService.getPanel("controlCenterPanel", screen)?.close() } } } diff --git a/Modules/ControlCenter/Cards/ShortcutsCard.qml b/Modules/ControlCenter/Cards/ShortcutsCard.qml index 2100a76b..18e593d8 100644 --- a/Modules/ControlCenter/Cards/ShortcutsCard.qml +++ b/Modules/ControlCenter/Cards/ShortcutsCard.qml @@ -28,10 +28,13 @@ RowLayout { Repeater { model: Settings.data.controlCenter.shortcuts.left delegate: ControlCenterWidgetLoader { + required property var modelData + required property int index + Layout.fillWidth: false widgetId: (modelData.id !== undefined ? modelData.id : "") + widgetScreen: root.screen widgetProps: { - "screen": root.modelData || null, "widgetId": modelData.id, "section": "quickSettings", "sectionWidgetIndex": index, @@ -63,10 +66,13 @@ RowLayout { Repeater { model: Settings.data.controlCenter.shortcuts.right delegate: ControlCenterWidgetLoader { + required property var modelData + required property int index + Layout.fillWidth: false widgetId: (modelData.id !== undefined ? modelData.id : "") + widgetScreen: root.screen widgetProps: { - "screen": root.modelData || null, "widgetId": modelData.id, "section": "quickSettings", "sectionWidgetIndex": index, diff --git a/Modules/ControlCenter/ControlCenterPanel.qml b/Modules/ControlCenter/ControlCenterPanel.qml index 0e899019..10cccf70 100644 --- a/Modules/ControlCenter/ControlCenterPanel.qml +++ b/Modules/ControlCenter/ControlCenterPanel.qml @@ -10,7 +10,6 @@ import qs.Widgets NPanel { id: root - panelKeyboardFocus: true preferredWidth: Math.round(460 * Style.uiScaleRatio) preferredHeight: { var height = 0 diff --git a/Modules/ControlCenter/ControlCenterWidgetLoader.qml b/Modules/ControlCenter/ControlCenterWidgetLoader.qml index e0360d4c..ebfb05f7 100644 --- a/Modules/ControlCenter/ControlCenterWidgetLoader.qml +++ b/Modules/ControlCenter/ControlCenterWidgetLoader.qml @@ -6,9 +6,10 @@ import qs.Commons Item { id: root - property string widgetId: "" - property var widgetProps: ({}) - property string screenName: widgetProps && widgetProps.screen ? widgetProps.screen.name : "" + required property string widgetId + required property var widgetScreen + required property var widgetProps + property string section: widgetProps && widgetProps.section || "" property int sectionIndex: widgetProps && widgetProps.sectionWidgetIndex || 0 @@ -23,30 +24,29 @@ Item { Loader { id: loader anchors.fill: parent - active: widgetId !== "" asynchronous: false - sourceComponent: { - if (!active) { - return null - } - return ControlCenterWidgetRegistry.getWidget(widgetId) - } + sourceComponent: ControlCenterWidgetRegistry.getWidget(widgetId) onLoaded: { - if (item && widgetProps) { - // Apply properties to loaded widget - for (var prop in widgetProps) { - if (item.hasOwnProperty(prop)) { - item[prop] = widgetProps[prop] - } + if (!item) + return + + // Apply properties to loaded widget + for (var prop in widgetProps) { + if (item.hasOwnProperty(prop)) { + item[prop] = widgetProps[prop] } } + // Set screen property + if (item.hasOwnProperty("screen")) { + item.screen = widgetScreen + } + + // Call custom onLoaded if it exists if (item.hasOwnProperty("onLoaded")) { item.onLoaded() } - - //Logger.i("ControlCenterWidgetLoader", "Loaded", widgetId, "on screen", item.screen.name) } Component.onDestruction: { @@ -56,8 +56,8 @@ Item { } // Error handling - onWidgetIdChanged: { - if (widgetId && !ControlCenterWidgetRegistry.hasWidget(widgetId)) { + Component.onCompleted: { + if (!ControlCenterWidgetRegistry.hasWidget(widgetId)) { Logger.w("ControlCenterWidgetLoader", "Widget not found in registry:", widgetId) } } diff --git a/Modules/ControlCenter/Widgets/Bluetooth.qml b/Modules/ControlCenter/Widgets/Bluetooth.qml index e4549a31..5378156d 100644 --- a/Modules/ControlCenter/Widgets/Bluetooth.qml +++ b/Modules/ControlCenter/Widgets/Bluetooth.qml @@ -9,5 +9,7 @@ NIconButtonHot { icon: BluetoothService.enabled ? "bluetooth" : "bluetooth-off" tooltipText: I18n.tr("quickSettings.bluetooth.tooltip.action") - onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this) + onClicked: { + PanelService.getPanel("bluetoothPanel", screen)?.toggle(this) + } } diff --git a/Modules/ControlCenter/Widgets/NightLight.qml b/Modules/ControlCenter/Widgets/NightLight.qml index eb338882..930a8f96 100644 --- a/Modules/ControlCenter/Widgets/NightLight.qml +++ b/Modules/ControlCenter/Widgets/NightLight.qml @@ -25,7 +25,7 @@ NIconButtonHot { } onRightClicked: { - var settingsPanel = PanelService.getPanel("settingsPanel") + var settingsPanel = PanelService.getPanel("settingsPanel", screen) settingsPanel.requestedTab = SettingsPanel.Tab.Display settingsPanel.open() } diff --git a/Modules/ControlCenter/Widgets/Notifications.qml b/Modules/ControlCenter/Widgets/Notifications.qml index ae291517..35f5cdf7 100644 --- a/Modules/ControlCenter/Widgets/Notifications.qml +++ b/Modules/ControlCenter/Widgets/Notifications.qml @@ -10,6 +10,6 @@ NIconButtonHot { icon: Settings.data.notifications.doNotDisturb ? "bell-off" : "bell" hot: Settings.data.notifications.doNotDisturb tooltipText: I18n.tr("quickSettings.notifications.tooltip.action") - onClicked: PanelService.getPanel("notificationHistoryPanel")?.toggle(this) + onClicked: PanelService.getPanel("notificationHistoryPanel", screen)?.toggle(this) onRightClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb } diff --git a/Modules/ControlCenter/Widgets/PowerProfile.qml b/Modules/ControlCenter/Widgets/PowerProfile.qml index d26922ed..ca5f497f 100644 --- a/Modules/ControlCenter/Widgets/PowerProfile.qml +++ b/Modules/ControlCenter/Widgets/PowerProfile.qml @@ -14,7 +14,7 @@ NIconButtonHot { enabled: hasPP icon: PowerProfileService.getIcon() hot: !PowerProfileService.isDefault() - tooltipText: hasPP ? I18n.tr("quickSettings.powerProfile.tooltip.action") : I18n.tr("quickSettings.powerProfile.tooltip.disabled") + tooltipText: I18n.tr("quickSettings.powerProfile.tooltip.action") onClicked: { PowerProfileService.cycleProfile() } diff --git a/Modules/ControlCenter/Widgets/ScreenRecorder.qml b/Modules/ControlCenter/Widgets/ScreenRecorder.qml index 7a58c8f3..85fbacd9 100644 --- a/Modules/ControlCenter/Widgets/ScreenRecorder.qml +++ b/Modules/ControlCenter/Widgets/ScreenRecorder.qml @@ -14,7 +14,7 @@ NIconButtonHot { onClicked: { ScreenRecorderService.toggleRecording() if (!ScreenRecorderService.isRecording) { - var panel = PanelService.getPanel("controlCenterPanel") + var panel = PanelService.getPanel("controlCenterPanel", screen) panel?.close() } } diff --git a/Modules/ControlCenter/Widgets/WallpaperSelector.qml b/Modules/ControlCenter/Widgets/WallpaperSelector.qml index a725974c..daa941ee 100644 --- a/Modules/ControlCenter/Widgets/WallpaperSelector.qml +++ b/Modules/ControlCenter/Widgets/WallpaperSelector.qml @@ -10,6 +10,6 @@ NIconButtonHot { enabled: Settings.data.wallpaper.enabled icon: "wallpaper-selector" tooltipText: I18n.tr("quickSettings.wallpaperSelector.tooltip.action") - onClicked: PanelService.getPanel("wallpaperPanel")?.toggle() + onClicked: PanelService.getPanel("wallpaperPanel", screen)?.toggle() onRightClicked: WallpaperService.setRandomWallpaper() } diff --git a/Modules/ControlCenter/Widgets/WiFi.qml b/Modules/ControlCenter/Widgets/WiFi.qml index eea64700..8970dc45 100644 --- a/Modules/ControlCenter/Widgets/WiFi.qml +++ b/Modules/ControlCenter/Widgets/WiFi.qml @@ -29,5 +29,5 @@ NIconButtonHot { } tooltipText: I18n.tr("quickSettings.wifi.tooltip.action") - onClicked: PanelService.getPanel("wifiPanel")?.toggle(this) + onClicked: PanelService.getPanel("wifiPanel", screen)?.toggle(this) } diff --git a/Modules/Launcher/Launcher.qml b/Modules/Launcher/Launcher.qml index 03c6f0af..5f1ed23b 100644 --- a/Modules/Launcher/Launcher.qml +++ b/Modules/Launcher/Launcher.qml @@ -16,8 +16,8 @@ NPanel { preferredWidthRatio: 0.3 preferredHeightRatio: 0.5 - panelKeyboardFocus: true panelBackgroundColor: Qt.alpha(Color.mSurface, Settings.data.appLauncher.backgroundOpacity) + panelKeyboardFocus: true // Needs Exclusive focus for text input // Positioning readonly property string launcherPosition: Settings.data.appLauncher.position @@ -40,6 +40,51 @@ NPanel { readonly property int badgeSize: Math.round(Style.baseWidgetSize * 1.6) readonly property int entryHeight: Math.round(badgeSize + Style.marginM * 2) + // Override keyboard handlers from NPanel for navigation + function onTabPressed() { + selectNextWrapped() + } + + function onShiftTabPressed() { + selectPreviousWrapped() + } + + function onUpPressed() { + selectPreviousWrapped() + } + + function onDownPressed() { + selectNextWrapped() + } + + function onReturnPressed() { + activate() + } + + function onHomePressed() { + selectFirst() + } + + function onEndPressed() { + selectLast() + } + + function onPageUpPressed() { + selectPreviousPage() + } + + function onPageDownPressed() { + selectNextPage() + } + + function onCtrlJPressed() { + selectNextWrapped() + } + + function onCtrlKPressed() { + selectPreviousWrapped() + } + // Public API for plugins function setSearchText(text) { searchText = text @@ -151,6 +196,54 @@ NPanel { } } + // Navigation functions + function selectNextWrapped() { + if (results.length > 0) { + selectedIndex = (selectedIndex + 1) % results.length + } + } + + function selectPreviousWrapped() { + if (results.length > 0) { + selectedIndex = (((selectedIndex - 1) % results.length) + results.length) % results.length + } + } + + function selectFirst() { + selectedIndex = 0 + } + + function selectLast() { + if (results.length > 0) { + selectedIndex = results.length - 1 + } else { + selectedIndex = 0 + } + } + + function selectNextPage() { + if (results.length > 0) { + const page = Math.max(1, Math.floor(600 / entryHeight)) // Use approximate height + selectedIndex = Math.min(selectedIndex + page, results.length - 1) + } + } + + function selectPreviousPage() { + if (results.length > 0) { + const page = Math.max(1, Math.floor(600 / entryHeight)) // Use approximate height + selectedIndex = Math.max(selectedIndex - page, 0) + } + } + + function activate() { + if (results.length > 0 && results[selectedIndex]) { + const item = results[selectedIndex] + if (item.onActivate) { + item.onActivate() + } + } + } + // UI panelContent: Rectangle { id: ui @@ -198,6 +291,19 @@ NPanel { } } + // Focus management + Connections { + target: root + function onOpened() { + // Delay focus to ensure window has keyboard focus + Qt.callLater(() => { + if (searchInput.inputItem) { + searchInput.inputItem.forceActiveFocus() + } + }) + } + } + Behavior on opacity { NumberAnimation { duration: Style.animationFast @@ -205,102 +311,6 @@ NPanel { } } - // --------------------- - // Navigation - function selectNextWrapped() { - if (results.length > 0) { - selectedIndex = (selectedIndex + 1) % results.length - } - } - - function selectPreviousWrapped() { - if (results.length > 0) { - selectedIndex = (((selectedIndex - 1) % results.length) + results.length) % results.length - } - } - - function selectFirst() { - selectedIndex = 0 - } - - function selectLast() { - if (results.length > 0) { - selectedIndex = results.length - 1 - } else { - selectedIndex = 0 - } - } - - function selectNextPage() { - if (results.length > 0) { - const page = Math.max(1, Math.floor(resultsList.height / entryHeight)) - selectedIndex = Math.min(selectedIndex + page, results.length - 1) - } - } - function selectPreviousPage() { - if (results.length > 0) { - const page = Math.max(1, Math.floor(resultsList.height / entryHeight)) - selectedIndex = Math.max(selectedIndex - page, 0) - } - } - - function activate() { - if (results.length > 0 && results[selectedIndex]) { - const item = results[selectedIndex] - if (item.onActivate) { - item.onActivate() - } - } - } - - Shortcut { - sequence: "Ctrl+K" - onActivated: ui.selectPreviousWrapped() - enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus - } - - Shortcut { - sequence: "Ctrl+J" - onActivated: ui.selectNextWrapped() - enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus - } - - Shortcut { - sequence: "Tab" - onActivated: ui.selectNextWrapped() - enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus - } - - Shortcut { - sequence: "Shift+Tab" - onActivated: ui.selectPreviousWrapped() - enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus - } - - Shortcut { - sequence: "PgDown" // or "PageDown" - onActivated: ui.selectNextPage() - enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus - } - - Shortcut { - sequence: "PgUp" // or "PageUp" - onActivated: ui.selectPreviousPage() - enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus - } - - Shortcut { - sequence: "Home" - onActivated: ui.selectFirst() - enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus - } - - Shortcut { - sequence: "End" - onActivated: ui.selectLast() - enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus - } - ColumnLayout { anchors.fill: parent anchors.margins: Style.marginL @@ -319,36 +329,8 @@ NPanel { onTextChanged: searchText = text Component.onCompleted: { - if (searchInput.inputItem && searchInput.inputItem.visible) { + if (searchInput.inputItem) { searchInput.inputItem.forceActiveFocus() - - // Override the TextField's default Home/End behavior - searchInput.inputItem.Keys.priority = Keys.BeforeItem - searchInput.inputItem.Keys.onPressed.connect(function (event) { - // Intercept Home, End, and Numpad Enter BEFORE the TextField handles them - if (event.key === Qt.Key_Home) { - ui.selectFirst() - event.accepted = true - return - } else if (event.key === Qt.Key_End) { - ui.selectLast() - event.accepted = true - return - } else if (event.key === Qt.Key_Enter) { - ui.activate() - event.accepted = true - return - } - }) - searchInput.inputItem.Keys.onDownPressed.connect(function (event) { - ui.selectNextWrapped() - }) - searchInput.inputItem.Keys.onUpPressed.connect(function (event) { - ui.selectPreviousWrapped() - }) - searchInput.inputItem.Keys.onReturnPressed.connect(function (event) { - ui.activate() - }) } } } @@ -588,7 +570,7 @@ NPanel { onClicked: mouse => { if (mouse.button === Qt.LeftButton) { selectedIndex = index - ui.activate() + root.activate() mouse.accepted = true } } diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml index f2336910..3d7c61b1 100644 --- a/Modules/Notification/NotificationHistoryPanel.qml +++ b/Modules/Notification/NotificationHistoryPanel.qml @@ -12,9 +12,8 @@ import qs.Widgets NPanel { id: root - preferredWidth: 380 - preferredHeight: 480 - panelKeyboardFocus: true + preferredWidth: 380 * Style.uiScaleRatio + preferredHeight: 480 * Style.uiScaleRatio onOpened: function () { NotificationService.updateLastSeenTs() diff --git a/Modules/SessionMenu/SessionMenu.qml b/Modules/SessionMenu/SessionMenu.qml index 623a93c5..6d789d6e 100644 --- a/Modules/SessionMenu/SessionMenu.qml +++ b/Modules/SessionMenu/SessionMenu.qml @@ -15,9 +15,9 @@ NPanel { preferredWidth: 400 * Style.uiScaleRatio preferredHeight: 340 * Style.uiScaleRatio + panelAnchorHorizontalCenter: true panelAnchorVerticalCenter: true - panelKeyboardFocus: true // Timer properties property int timerDuration: 9000 // 9 seconds @@ -148,6 +148,52 @@ NPanel { } } + // Override keyboard handlers from NPanel + function onEscapePressed() { + if (timerActive) { + cancelTimer() + } else { + cancelTimer() + close() + } + } + + function onTabPressed() { + selectNextWrapped() + } + + function onShiftTabPressed() { + selectPreviousWrapped() + } + + function onUpPressed() { + selectPreviousWrapped() + } + + function onDownPressed() { + selectNextWrapped() + } + + function onReturnPressed() { + activate() + } + + function onHomePressed() { + selectFirst() + } + + function onEndPressed() { + selectLast() + } + + function onCtrlJPressed() { + selectNextWrapped() + } + + function onCtrlKPressed() { + selectPreviousWrapped() + } + // Countdown timer Timer { id: countdownTimer @@ -165,81 +211,6 @@ NPanel { id: ui color: Color.transparent - // Keyboard shortcuts - Shortcut { - sequence: "Ctrl+K" - onActivated: ui.selectPreviousWrapped() - enabled: root.opened - } - - Shortcut { - sequence: "Ctrl+J" - onActivated: ui.selectNextWrapped() - enabled: root.opened - } - - Shortcut { - sequence: "Up" - onActivated: ui.selectPreviousWrapped() - enabled: root.opened - } - - Shortcut { - sequence: "Down" - onActivated: ui.selectNextWrapped() - enabled: root.opened - } - - Shortcut { - sequence: "Shift+Tab" - onActivated: ui.selectPreviousWrapped() - enabled: root.opened - } - - Shortcut { - sequence: "Tab" - onActivated: ui.selectNextWrapped() - enabled: root.opened - } - - Shortcut { - sequence: "Home" - onActivated: ui.selectFirst() - enabled: root.opened - } - - Shortcut { - sequence: "End" - onActivated: ui.selectLast() - enabled: root.opened - } - - Shortcut { - sequence: "Return" - onActivated: ui.activate() - enabled: root.opened - } - - Shortcut { - sequence: "Enter" - onActivated: ui.activate() - enabled: root.opened - } - - Shortcut { - sequence: "Escape" - onActivated: { - if (timerActive) { - cancelTimer() - } else { - cancelTimer() - root.close() - } - } - context: Qt.WidgetShortcut - enabled: root.opened - } - // Navigation functions function selectFirst() { root.selectFirst() diff --git a/Modules/Settings/SettingsPanel.qml b/Modules/Settings/SettingsPanel.qml index a8fc5bce..08b4c484 100644 --- a/Modules/Settings/SettingsPanel.qml +++ b/Modules/Settings/SettingsPanel.qml @@ -16,9 +16,6 @@ NPanel { panelAnchorHorizontalCenter: true panelAnchorVerticalCenter: true - panelKeyboardFocus: true - - draggable: !PanelService.hasOpenedPopup // Tabs enumeration, order is NOT relevant enum Tab { @@ -287,6 +284,39 @@ NPanel { } } + // Override keyboard handlers from NPanel + function onTabPressed() { + selectNextTab() + } + + function onShiftTabPressed() { + selectPreviousTab() + } + + function onUpPressed() { + scrollUp() + } + + function onDownPressed() { + scrollDown() + } + + function onPageUpPressed() { + scrollPageUp() + } + + function onPageDownPressed() { + scrollPageDown() + } + + function onCtrlJPressed() { + scrollDown() + } + + function onCtrlKPressed() { + scrollUp() + } + panelContent: Rectangle { color: Color.transparent @@ -296,62 +326,6 @@ NPanel { anchors.margins: Style.marginL spacing: 0 - // Keyboard shortcuts container - Item { - Layout.preferredWidth: 0 - Layout.preferredHeight: 0 - - // Scrolling via keyboard - Shortcut { - sequence: "Down" - onActivated: root.scrollDown() - enabled: root.opened - } - - Shortcut { - sequence: "Up" - onActivated: root.scrollUp() - enabled: root.opened - } - - Shortcut { - sequence: "Ctrl+J" - onActivated: root.scrollDown() - enabled: root.opened - } - - Shortcut { - sequence: "Ctrl+K" - onActivated: root.scrollUp() - enabled: root.opened - } - - Shortcut { - sequence: "PgDown" - onActivated: root.scrollPageDown() - enabled: root.opened - } - - Shortcut { - sequence: "PgUp" - onActivated: root.scrollPageUp() - enabled: root.opened - } - - // Changing tab via keyboard - Shortcut { - sequence: "Tab" - onActivated: root.selectNextTab() - enabled: root.opened - } - - Shortcut { - sequence: "Shift+Tab" - onActivated: root.selectPreviousTab() - enabled: root.opened - } - } - // Main content area RowLayout { Layout.fillWidth: true diff --git a/Modules/Settings/Tabs/BarTab.qml b/Modules/Settings/Tabs/BarTab.qml index a9552659..639010e5 100644 --- a/Modules/Settings/Tabs/BarTab.qml +++ b/Modules/Settings/Tabs/BarTab.qml @@ -25,7 +25,7 @@ ColumnLayout { // Handler for drag start - disables panel background clicks function handleDragStart() { - var panel = PanelService.getPanel("settingsPanel") + var panel = PanelService.getPanel("settingsPanel", screen) if (panel && panel.disableBackgroundClick) { panel.disableBackgroundClick() } @@ -33,7 +33,7 @@ ColumnLayout { // Handler for drag end - re-enables panel background clicks function handleDragEnd() { - var panel = PanelService.getPanel("settingsPanel") + var panel = PanelService.getPanel("settingsPanel", screen) if (panel && panel.enableBackgroundClick) { panel.enableBackgroundClick() } @@ -102,6 +102,14 @@ ColumnLayout { onToggled: checked => Settings.data.bar.floating = checked } + NToggle { + Layout.fillWidth: true + label: I18n.tr("settings.bar.appearance.outer-corners.label") + description: I18n.tr("settings.bar.appearance.outer-corners.description") + checked: Settings.data.bar.outerCorners + onToggled: checked => Settings.data.bar.outerCorners = checked + } + // Floating bar options - only show when floating is enabled ColumnLayout { visible: Settings.data.bar.floating diff --git a/Modules/Settings/Tabs/ControlCenterTab.qml b/Modules/Settings/Tabs/ControlCenterTab.qml index 372add57..7b1a91a7 100644 --- a/Modules/Settings/Tabs/ControlCenterTab.qml +++ b/Modules/Settings/Tabs/ControlCenterTab.qml @@ -40,7 +40,7 @@ ColumnLayout { // Handler for drag start - disables panel background clicks function handleDragStart() { - var panel = PanelService.getPanel("settingsPanel") + var panel = PanelService.getPanel("settingsPanel", screen) if (panel && panel.disableBackgroundClick) { panel.disableBackgroundClick() } @@ -48,7 +48,7 @@ ColumnLayout { // Handler for drag end - re-enables panel background clicks function handleDragEnd() { - var panel = PanelService.getPanel("settingsPanel") + var panel = PanelService.getPanel("settingsPanel", screen) if (panel && panel.enableBackgroundClick) { panel.enableBackgroundClick() } diff --git a/Modules/Settings/Tabs/LauncherTab.qml b/Modules/Settings/Tabs/LauncherTab.qml index 86cf97ee..dd6cee27 100644 --- a/Modules/Settings/Tabs/LauncherTab.qml +++ b/Modules/Settings/Tabs/LauncherTab.qml @@ -22,6 +22,9 @@ ColumnLayout { model: [{ "key": "center", "name": I18n.tr("options.launcher.position.center") + }, { + "key": "top_center", + "name": I18n.tr("options.launcher.position.top_center") }, { "key": "top_left", "name": I18n.tr("options.launcher.position.top_left") @@ -37,9 +40,6 @@ ColumnLayout { }, { "key": "bottom_center", "name": I18n.tr("options.launcher.position.bottom_center") - }, { - "key": "top_center", - "name": I18n.tr("options.launcher.position.top_center") }] currentKey: Settings.data.appLauncher.position onSelected: function (key) { diff --git a/Modules/Settings/Tabs/UserInterfaceTab.qml b/Modules/Settings/Tabs/UserInterfaceTab.qml index 063714e5..a466c7ec 100644 --- a/Modules/Settings/Tabs/UserInterfaceTab.qml +++ b/Modules/Settings/Tabs/UserInterfaceTab.qml @@ -26,6 +26,13 @@ ColumnLayout { onToggled: checked => Settings.data.ui.tooltipsEnabled = checked } + NToggle { + label: I18n.tr("settings.user-interface.dim-desktop.label") + description: I18n.tr("settings.user-interface.dim-desktop.description") + checked: Settings.data.general.dimDesktop + onToggled: checked => Settings.data.general.dimDesktop = checked + } + NToggle { label: I18n.tr("settings.user-interface.panels-attached-to-bar.label") description: I18n.tr("settings.user-interface.panels-attached-to-bar.description") diff --git a/Modules/SetupWizard/SetupWizard.qml b/Modules/SetupWizard/SetupWizard.qml index 5be6df91..996d7d57 100644 --- a/Modules/SetupWizard/SetupWizard.qml +++ b/Modules/SetupWizard/SetupWizard.qml @@ -14,17 +14,17 @@ NPanel { preferredHeight: 600 * Style.uiScaleRatio preferredWidthRatio: 0.4 preferredHeightRatio: 0.6 + panelAnchorHorizontalCenter: true panelAnchorVerticalCenter: true - panelKeyboardFocus: true - - // Prevent closing during setup - backgroundClickEnabled: false - draggable: false property int currentStep: 0 property int totalSteps: 5 + // Override Escape handler to prevent closing the setup wizard + function onEscapePressed() {// Do nothing - prevent ESC from closing the setup wizard + } + // Setup wizard data property string selectedWallpaperDirectory: Settings.defaultWallpapersDirectory property string selectedWallpaper: "" @@ -42,17 +42,6 @@ NPanel { anchors.margins: Style.marginXL spacing: Style.marginL - // Override ESC key to prevent closing during setup - Shortcut { - sequences: ["Escape"] - enabled: root.active - onActivated: { - - // Do nothing - prevent ESC from closing the setup wizard - } - context: Qt.WindowShortcut - } - // Step content - takes most of the space Item { Layout.fillWidth: true diff --git a/Modules/Wallpaper/WallpaperPanel.qml b/Modules/Wallpaper/WallpaperPanel.qml index 2a1f6d57..472cdfcc 100644 --- a/Modules/Wallpaper/WallpaperPanel.qml +++ b/Modules/Wallpaper/WallpaperPanel.qml @@ -12,15 +12,87 @@ import "../../Helpers/FuzzySort.js" as FuzzySort NPanel { id: root - preferredWidth: 640 * Style.uiScaleRatio - preferredHeight: 480 * Style.uiScaleRatio - preferredWidthRatio: 0.4 + preferredWidth: 800 * Style.uiScaleRatio + preferredHeight: 600 * Style.uiScaleRatio + preferredWidthRatio: 0.5 preferredHeightRatio: 0.52 - panelAnchorHorizontalCenter: true - panelAnchorVerticalCenter: true - panelKeyboardFocus: true - draggable: !PanelService.hasOpenedPopup + // Positioning - Use launcher position. This saves a setting... + readonly property string launcherPosition: Settings.data.appLauncher.position + panelAnchorHorizontalCenter: launcherPosition === "center" || launcherPosition.endsWith("_center") + panelAnchorVerticalCenter: launcherPosition === "center" + panelAnchorLeft: launcherPosition !== "center" && launcherPosition.endsWith("_left") + panelAnchorRight: launcherPosition !== "center" && launcherPosition.endsWith("_right") + panelAnchorBottom: launcherPosition.startsWith("bottom_") + panelAnchorTop: launcherPosition.startsWith("top_") + + // panelAnchorHorizontalCenter: true + // panelAnchorVerticalCenter: true + panelKeyboardFocus: true // Needs Exclusive focus for text input (search) + + // Store direct reference to content for instant access + property var contentItem: null + + // Override keyboard handlers to enable grid navigation + function onDownPressed() { + if (!contentItem) + return + let view = contentItem.screenRepeater.itemAt(contentItem.currentScreenIndex) + if (view?.gridView) { + if (!view.gridView.activeFocus) { + view.gridView.forceActiveFocus() + if (view.gridView.currentIndex < 0) { + view.gridView.currentIndex = 0 + } + } else { + view.gridView.moveCurrentIndexDown() + } + } + } + + function onUpPressed() { + if (!contentItem) + return + let view = contentItem.screenRepeater.itemAt(contentItem.currentScreenIndex) + if (view?.gridView?.activeFocus) { + view.gridView.moveCurrentIndexUp() + } + } + + function onLeftPressed() { + if (!contentItem) + return + let view = contentItem.screenRepeater.itemAt(contentItem.currentScreenIndex) + if (view?.gridView?.activeFocus) { + view.gridView.moveCurrentIndexLeft() + } + } + + function onRightPressed() { + if (!contentItem) + return + let view = contentItem.screenRepeater.itemAt(contentItem.currentScreenIndex) + if (view?.gridView?.activeFocus) { + view.gridView.moveCurrentIndexRight() + } + } + + function onReturnPressed() { + if (!contentItem) + return + let view = contentItem.screenRepeater.itemAt(contentItem.currentScreenIndex) + if (view?.gridView?.activeFocus) { + let gridView = view.gridView + if (gridView.currentIndex >= 0 && gridView.currentIndex < gridView.model.length) { + let path = gridView.model[gridView.currentIndex] + if (Settings.data.wallpaper.setWallpaperOnAllMonitors) { + WallpaperService.changeWallpaper(path, undefined) + } else { + WallpaperService.changeWallpaper(path, view.targetScreen.name) + } + } + } + } panelContent: Rectangle { id: wallpaperPanel @@ -37,9 +109,31 @@ NPanel { } property var currentScreen: Quickshell.screens[currentScreenIndex] property string filterText: "" + property alias screenRepeater: screenRepeater + + Component.onCompleted: { + root.contentItem = wallpaperPanel + } color: Color.transparent + // Focus management + Connections { + target: root + function onOpened() { + // Ensure contentItem is set + if (!root.contentItem) { + root.contentItem = wallpaperPanel + } + // Give initial focus to search input + Qt.callLater(() => { + if (searchInput.inputItem) { + searchInput.inputItem.forceActiveFocus() + } + }) + } + } + // Debounce timer for search Timer { id: searchDebounceTimer @@ -85,7 +179,7 @@ NPanel { tooltipText: I18n.tr("settings.wallpaper.settings.section.label") baseSize: Style.baseWidgetSize * 0.8 onClicked: { - var settingsPanel = PanelService.getPanel("settingsPanel") + var settingsPanel = PanelService.getPanel("settingsPanel", screen) settingsPanel.requestedTab = SettingsPanel.Tab.Wallpaper settingsPanel.open() } @@ -324,7 +418,7 @@ NPanel { model: filteredWallpapers - property int columns: 4 + property int columns: 5 property int itemSize: cellWidth cellWidth: Math.floor((width - leftMargin - rightMargin) / columns) diff --git a/Services/BatteryService.qml b/Services/BatteryService.qml index 3f942403..14b831c9 100644 --- a/Services/BatteryService.qml +++ b/Services/BatteryService.qml @@ -62,7 +62,7 @@ Singleton { } else { BatteryService.initialSetter = true ToastService.showNotice(I18n.tr("toast.battery-manager.title"), I18n.tr("toast.battery-manager.uninstall-setup")) - PanelService.getPanel("batteryPanel")?.toggle(this) + PanelService.getPanel("batteryPanel", screen)?.toggle(this) uninstallerProcess.running = true } } @@ -123,7 +123,7 @@ Singleton { Settings.data.battery.chargingMode = BatteryService.chargingMode } else if (exitCode === 2) { ToastService.showWarning(I18n.tr("toast.battery-manager.title"), I18n.tr("toast.battery-manager.initial-setup")) - PanelService.getPanel("batteryPanel")?.toggle(this) + PanelService.getPanel("batteryPanel", screen)?.toggle(this) BatteryService.runInstaller() } else { ToastService.showError(I18n.tr("toast.battery-manager.title"), I18n.tr("toast.battery-manager.set-failed")) diff --git a/Services/CavaService.qml b/Services/CavaService.qml index 30fccea1..cf5c174c 100644 --- a/Services/CavaService.qml +++ b/Services/CavaService.qml @@ -8,7 +8,15 @@ import qs.Commons Singleton { id: root - property bool shouldRun: BarService.hasAudioVisualizer || (PanelService.getPanel("controlCenterPanel") === PanelService.openedPanel) || PanelService.lockScreen.active + + /** + * Cava runs if: + * - Bar has an audio visualizer + * - LockScreen is opened + * - A control center is open + */ + property bool shouldRun: BarService.hasAudioVisualizer || PanelService.lockScreen.active || (PanelService.openedPanel && PanelService.openedPanel.objectName.startsWith("controlCenterPanel")) + property var values: Array(barsCount).fill(0) property int barsCount: 48 property var config: ({ diff --git a/Services/IPCService.qml b/Services/IPCService.qml index 7a569485..a9323518 100644 --- a/Services/IPCService.qml +++ b/Services/IPCService.qml @@ -2,6 +2,7 @@ import QtQuick import Quickshell import Quickshell.Io import Quickshell.Wayland +import Quickshell.Widgets import qs.Commons import qs.Services @@ -27,7 +28,10 @@ Item { IpcHandler { target: "settings" function toggle() { - settingsPanel.toggle() + root.withTargetScreen(screen => { + var settingsPanel = PanelService.getPanel("settingsPanel", screen) + settingsPanel.toggle() + }) } } @@ -35,7 +39,10 @@ Item { target: "notifications" function toggleHistory() { // Will attempt to open the panel next to the bar button if any. - notificationHistoryPanel.toggle(null, "NotificationHistory") + root.withTargetScreen(screen => { + var notificationHistoryPanel = PanelService.getPanel("notificationHistoryPanel", screen) + notificationHistoryPanel.toggle(null, "NotificationHistory") + }) } function toggleDND() { Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb @@ -63,15 +70,24 @@ Item { IpcHandler { target: "launcher" function toggle() { - launcherPanel.toggle() + root.withTargetScreen(screen => { + var launcherPanel = PanelService.getPanel("launcherPanel", screen) + launcherPanel.toggle() + }) } function clipboard() { - launcherPanel.setSearchText(">clip ") - launcherPanel.toggle() + root.withTargetScreen(screen => { + var launcherPanel = PanelService.getPanel("launcherPanel", screen) + launcherPanel.setSearchText(">clip ") + launcherPanel.toggle() + }) } function calculator() { - launcherPanel.setSearchText(">calc ") - launcherPanel.toggle() + root.withTargetScreen(screen => { + var launcherPanel = PanelService.getPanel("launcherPanel", screen) + launcherPanel.setSearchText(">calc ") + launcherPanel.toggle() + }) } } @@ -158,7 +174,10 @@ Item { IpcHandler { target: "sessionMenu" function toggle() { - sessionMenuPanel.toggle() + root.withTargetScreen(screen => { + var sessionMenuPanel = PanelService.getPanel("sessionMenuPanel", screen) + sessionMenuPanel.toggle() + }) } function lockAndSuspend() { @@ -170,7 +189,10 @@ Item { target: "controlCenter" function toggle() { // Will attempt to open the panel next to the bar button if any. - controlCenterPanel.toggle(null, "ControlCenter") + root.withTargetScreen(screen => { + var controlCenterPanel = PanelService.getPanel("controlCenterPanel", screen) + controlCenterPanel.toggle(null, "ControlCenter") + }) } } @@ -179,7 +201,10 @@ Item { target: "wallpaper" function toggle() { if (Settings.data.wallpaper.enabled) { - wallpaperPanel.toggle() + root.withTargetScreen(screen => { + var wallpaperPanel = PanelService.getPanel("wallpaperPanel", screen) + wallpaperPanel.toggle() + }) } } @@ -228,6 +253,7 @@ Item { } } } + IpcHandler { target: "powerProfile" function cycle() { @@ -248,6 +274,7 @@ Item { } } } + IpcHandler { target: "media" function playPause() { @@ -292,4 +319,83 @@ Item { MediaService.seekByRatio(positionVal) } } + + // Queue an IPC panel operation - will execute when screen is detected + function withTargetScreen(callback) { + if (pendingCallback) { + Logger.w("IPC", "Another IPC call is pending, ignoring new call") + return + } + + // Single monitor setup can execute immediately + if (Quickshell.screens.length === 1) { + pendingCallback(Quickshell.screens[0]) + } else { + // Multi-monitors setup needs to start async detection + detectedScreen = null + pendingCallback = callback + screenDetectorLoader.active = true + } + } + + + /** + * For IPC calls on multi-monitors setup that will open panels on screen, + * we need to open a QS PanelWindow and wait for it's "screen" property to stabilize. + */ + property ShellScreen detectedScreen: null + property var pendingCallback: null + + Timer { + id: screenDetectorDebounce + running: false + interval: 20 + onTriggered: { + Logger.d("IPC", "Screen debounced to:", detectedScreen?.name || "null") + + // Execute pending callback if any + if (pendingCallback) { + // Verify we have a NFullScreenWindow for this screen + var monitors = Settings.data.bar.monitors || [] + if (!(monitors.length === 0 || monitors.includes(detectedScreen.name))) { + // Fall back to first enabled screen as we can NOT show a panel on a screen without a Bar/NFullScreenWindow + if (monitors.length === 0 && Quickshell.screens.length > 0) { + detectedScreen = Quickshell.screens[0] + } else { + for (var i = 0; i < Quickshell.screens.length; i++) { + if (monitors.includes(Quickshell.screens[i].name)) { + detectedScreen = Quickshell.screens[i] + break + } + } + } + } + Logger.d("IPC", "Executing pending IPC callback on screen:", detectedScreen.name) + pendingCallback(detectedScreen) + pendingCallback = null + } + + // Clean up + screenDetectorLoader.active = false + } + } + + // Invisible dummy PanelWindow to detect which screen should receive IPC calls + Loader { + id: screenDetectorLoader + active: false + + sourceComponent: PanelWindow { + implicitWidth: 0 + implicitHeight: 0 + color: Color.transparent + WlrLayershell.exclusionMode: ExclusionMode.Ignore + mask: Region {} + + onScreenChanged: { + detectedScreen = screen + screenDetectorDebounce.restart() + } + } + } } diff --git a/Services/MediaService.qml b/Services/MediaService.qml index 505a945b..b96cafbd 100644 --- a/Services/MediaService.qml +++ b/Services/MediaService.qml @@ -304,7 +304,7 @@ Singleton { repeat: true running: true onTriggered: { - Logger.d("MediaService", "playerStateMonitor triggered. autoSwitchingPaused: " + root.autoSwitchingPaused) + //Logger.d("MediaService", "playerStateMonitor triggered. autoSwitchingPaused: " + root.autoSwitchingPaused) if (autoSwitchingPaused) return // Only update if we don't have a playing player or if current player is paused diff --git a/Services/PanelService.qml b/Services/PanelService.qml index 1ee055c1..6c2b071b 100644 --- a/Services/PanelService.qml +++ b/Services/PanelService.qml @@ -14,6 +14,7 @@ Singleton { property var registeredPanels: ({}) property var openedPanel: null signal willOpen + signal didClose // Currently opened popups, can have more than one. // ex: when opening an NIconPicker from a widget setting. @@ -21,15 +22,53 @@ Singleton { property bool hasOpenedPopup: false signal popupChanged - // Register this panel - function registerPanel(panel) { - registeredPanels[panel.objectName] = panel - Logger.d("PanelService", "Registered:", panel.objectName) + // Registered panel loaders (before they're loaded) + property var registeredPanelLoaders: ({}) + + // Register a panel loader (called before panel is loaded) + function registerPanelLoader(panelLoader, objectName) { + registeredPanelLoaders[objectName] = panelLoader + Logger.d("PanelService", "Registered panel loader:", objectName) } - // Returns a panel - function getPanel(name) { - return registeredPanels[name] || null + // Register this panel (called after panel is loaded) + function registerPanel(panel) { + registeredPanels[panel.objectName] = panel + Logger.i("PanelService", "Registered panel:", panel.objectName) + } + + // Returns a panel (loads it on-demand if not yet loaded) + function getPanel(name, screen) { + if (!screen) { + Logger.w("PanelService", "missing screen for getPanel:", name) + Logger.callStack() + // If no screen specified, return the first matching panel + for (var key in registeredPanels) { + if (key.startsWith(name + "-")) { + return registeredPanels[key] + } + } + return null + } + + var panelKey = `${name}-${screen.name}` + + // Check if panel is already loaded + if (registeredPanels[panelKey]) { + return registeredPanels[panelKey] + } + + // Panel not loaded yet - try to load it via the loader + if (registeredPanelLoaders[panelKey]) { + Logger.d("PanelService", "Loading panel on-demand:", panelKey) + registeredPanelLoaders[panelKey].ensureLoaded() + // After ensureLoaded(), the panel should register itself via registerPanel() + // Return it if it registered synchronously + return registeredPanels[panelKey] || null + } + + Logger.w("PanelService", "Panel not found:", panelKey) + return null } // Check if a panel exists @@ -52,6 +91,9 @@ Singleton { if (openedPanel && openedPanel === panel) { openedPanel = null } + + // emit signal + didClose() } // Popups diff --git a/Widgets/BarExclusionZone.qml b/Widgets/BarExclusionZone.qml new file mode 100644 index 00000000..a2448a92 --- /dev/null +++ b/Widgets/BarExclusionZone.qml @@ -0,0 +1,74 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland +import qs.Commons + + +/** + * BarExclusionZone - Invisible PanelWindow that reserves exclusive space for the bar + * + * This is a minimal window that works with the compositor to reserve space, + * while the actual bar UI is rendered in NFullScreenWindow. + */ +PanelWindow { + id: root + + property bool exclusive: Settings.data.bar.exclusive !== undefined ? Settings.data.bar.exclusive : false + + readonly property string barPosition: Settings.data.bar.position || "top" + readonly property bool barIsVertical: barPosition === "left" || barPosition === "right" + readonly property bool barFloating: Settings.data.bar.floating || false + readonly property real barMarginH: barFloating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0 + readonly property real barMarginV: barFloating ? Settings.data.bar.marginVertical * Style.marginXL : 0 + + // Invisible - just reserves space + color: "transparent" + + mask: Region {} + + // Wayland layer shell configuration + WlrLayershell.layer: WlrLayer.Top + WlrLayershell.namespace: "noctalia-bar-exclusion-" + (screen?.name || "unknown") + WlrLayershell.exclusionMode: exclusive ? ExclusionMode.Auto : ExclusionMode.Ignore + + // Anchor based on bar position + anchors { + top: barPosition === "top" + bottom: barPosition === "bottom" + left: barPosition === "left" || barPosition === "top" || barPosition === "bottom" + right: barPosition === "right" || barPosition === "top" || barPosition === "bottom" + } + + // Size based on bar orientation + // When floating, only reserve space for the bar + margin on the anchored edge + implicitWidth: { + if (barIsVertical) { + // Vertical bar: reserve bar height + margin on the anchored edge only + if (barFloating) { + // For left bar, reserve left margin; for right bar, reserve right margin + return Style.barHeight + barMarginH + } + return Style.barHeight + } + return 0 // Auto-width when left/right anchors are true + } + + implicitHeight: { + if (!barIsVertical) { + // Horizontal bar: reserve bar height + margin on the anchored edge only + if (barFloating) { + // For top bar, reserve top margin; for bottom bar, reserve bottom margin + return Style.barHeight + barMarginV + } + return Style.barHeight + } + return 0 // Auto-height when top/bottom anchors are true + } + + Component.onCompleted: { + Logger.d("BarExclusionZone", "Created for screen:", screen?.name) + Logger.d("BarExclusionZone", " Position:", barPosition, "Exclusive:", exclusive, "Floating:", barFloating) + Logger.d("BarExclusionZone", " Anchors - top:", anchors.top, "bottom:", anchors.bottom, "left:", anchors.left, "right:", anchors.right) + Logger.d("BarExclusionZone", " Size:", width, "x", height, "implicitWidth:", implicitWidth, "implicitHeight:", implicitHeight) + } +} diff --git a/Widgets/NFullScreenWindow.qml b/Widgets/NFullScreenWindow.qml new file mode 100644 index 00000000..e5302156 --- /dev/null +++ b/Widgets/NFullScreenWindow.qml @@ -0,0 +1,608 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Services + + +/** + * NFullScreenWindow - Single PanelWindow per screen that manages all panels and the bar + */ +PanelWindow { + id: root + + required property var barComponent + required property var panelComponents + + Component.onCompleted: { + Logger.d("NFullScreenWindow", "Initialized for screen:", screen?.name, "- Dimensions:", screen?.width, "x", screen?.height, "- Position:", screen?.x, ",", screen?.y) + } + + // Debug: Log mask region changes + onMaskChanged: { + Logger.d("NFullScreenWindow", "Mask changed!") + Logger.d("NFullScreenWindow", " Bar region:", barLoader.item?.barRegion) + Logger.d("NFullScreenWindow", " Panel count:", panelsRepeater.count) + for (var i = 0; i < panelsRepeater.count; i++) { + var panelItem = panelsRepeater.itemAt(i)?.item + Logger.d("NFullScreenWindow", " Panel", i, "- open:", panelItem?.isPanelOpen, "- region:", panelItem?.panelRegion) + } + } + + // Wayland + // Always use Exclusive keyboard focus when a panel is open + // This ensures all keyboard shortcuts work reliably (Escape, etc.) + // The centralized shortcuts in this window handle delegation to panels + WlrLayershell.keyboardFocus: root.isPanelOpen ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None + WlrLayershell.layer: WlrLayer.Top + WlrLayershell.namespace: "noctalia-screen-" + (screen?.name || "unknown") + + anchors { + top: true + bottom: true + left: true + right: true + } + + // Desktop dimming when panels are open + property bool dimDesktop: Settings.data.general.dimDesktop + property bool isPanelOpen: PanelService.openedPanel !== null + color: { + if (dimDesktop && isPanelOpen) { + return Qt.alpha(Color.mSurfaceVariant, Style.opacityHeavy) + } + return Color.transparent + } + + Behavior on color { + ColorAnimation { + duration: Style.animationNormal + easing.type: Easing.OutQuad + } + } + + function updateMask() { + // Build the regions list + var regionsList = [barMaskRegion] + + // Add background region if a panel is open + // This makes the background clickable (not click-through) so we can detect clicks to close panels + if (root.isPanelOpen) { + regionsList.push(backgroundMaskRegion) + } + + // Add regions for each open panel + for (var i = 0; i < panelMaskRepeater.count; i++) { + var wrapperItem = panelMaskRepeater.itemAt(i) + if (wrapperItem && wrapperItem.maskRegion) { + var panelItem = wrapperItem.panelItem + if (panelItem && panelItem.isPanelOpen) { + var panelRegion = panelItem.panelRegion + // Update the mask region's coordinates from the panel's actual region + if (panelRegion) { + wrapperItem.maskRegion.x = panelRegion.x + wrapperItem.maskRegion.y = panelRegion.y + wrapperItem.maskRegion.width = panelRegion.width + wrapperItem.maskRegion.height = panelRegion.height + regionsList.push(wrapperItem.maskRegion) + } + } + } + } + + // Update the mask's regions + clickableMask.regions = regionsList + } + + // Listen to PanelService to update mask when panels open/close + Connections { + target: PanelService + function onWillOpen() { + root.updateMask() + } + function onDidClose() { + root.updateMask() + } + } + + // Also update mask when isPanelOpen changes (defensive) + onIsPanelOpenChanged: { + Logger.d("NFullScreenWindow", "isPanelOpen changed to:", isPanelOpen) + Qt.callLater(() => root.updateMask()) + } + + // Background region - for closing panels when clicking outside (separate from mask) + Region { + id: backgroundMaskRegion + x: 0 + y: 0 + width: root.width + height: root.height + intersection: Intersection.Subtract + } + + // Smart mask: Make everything click-through except bar and open panels + mask: Region { + id: clickableMask + + // Cover entire window (everything is masked/click-through) + x: 0 + y: 0 + width: root.width + height: root.height + intersection: Intersection.Xor + + // Regions list is set programmatically in updateMask() + // Initially just the bar + regions: [barMaskRegion] + + // Bar region - subtract bar area from mask + Region { + id: barMaskRegion + property var barRegion: barLoader.item && barLoader.item.barRegion ? barLoader.item.barRegion : null + + x: barRegion ? barRegion.x : 0 + y: barRegion ? barRegion.y : 0 + width: barRegion ? barRegion.width : 0 + height: barRegion ? barRegion.height : 0 + intersection: Intersection.Subtract + } + } + + // Container for panel mask regions (created dynamically) + Item { + id: panelMaskRegions + + // Create a Region for each panel + Repeater { + id: panelMaskRepeater + model: panelsRepeater.count + + delegate: Item { + required property int index + property var panelItem: panelsRepeater.itemAt(index)?.item + property var region: panelItem && panelItem.panelRegion ? panelItem.panelRegion : null + + // The actual mask region as a child + property alias maskRegion: panelMask + + Region { + id: panelMask + // Coordinates are set programmatically in updateMask() + intersection: Intersection.Subtract + } + } + } + } + + // Container for all UI elements + Item { + id: container + width: root.width + height: root.height + + // Screen corners (integrated to avoid separate PanelWindow) + // Always positioned at actual screen edges + Loader { + id: screenCornersLoader + active: Settings.data.general.showScreenCorners && (!Settings.data.ui.panelsAttachedToBar || Settings.data.bar.backgroundOpacity >= 1 || Settings.data.bar.floating) + + anchors.fill: parent + z: 1000 // Very high z-index to be on top of everything + + sourceComponent: Item { + id: cornersRoot + anchors.fill: parent + + property color cornerColor: Settings.data.general.forceBlackScreenCorners ? Qt.rgba(0, 0, 0, 1) : Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity) + property real cornerRadius: Style.screenRadius + property real cornerSize: Style.screenRadius + + // Top-left concave corner + Canvas { + id: topLeftCorner + anchors.top: parent.top + anchors.left: parent.left + width: cornersRoot.cornerSize + height: cornersRoot.cornerSize + antialiasing: true + renderTarget: Canvas.FramebufferObject + smooth: true + + onPaint: { + const ctx = getContext("2d") + if (!ctx) + return + + ctx.reset() + ctx.clearRect(0, 0, width, height) + ctx.fillStyle = Qt.rgba(cornersRoot.cornerColor.r, cornersRoot.cornerColor.g, cornersRoot.cornerColor.b, cornersRoot.cornerColor.a) + ctx.fillRect(0, 0, width, height) + ctx.globalCompositeOperation = "destination-out" + ctx.fillStyle = "#ffffff" + ctx.beginPath() + ctx.arc(width, height, cornersRoot.cornerRadius, 0, 2 * Math.PI) + ctx.fill() + } + + onWidthChanged: if (available) + requestPaint() + onHeightChanged: if (available) + requestPaint() + } + + // Top-right concave corner + Canvas { + id: topRightCorner + anchors.top: parent.top + anchors.right: parent.right + width: cornersRoot.cornerSize + height: cornersRoot.cornerSize + antialiasing: true + renderTarget: Canvas.FramebufferObject + smooth: true + + onPaint: { + const ctx = getContext("2d") + if (!ctx) + return + + ctx.reset() + ctx.clearRect(0, 0, width, height) + ctx.fillStyle = Qt.rgba(cornersRoot.cornerColor.r, cornersRoot.cornerColor.g, cornersRoot.cornerColor.b, cornersRoot.cornerColor.a) + ctx.fillRect(0, 0, width, height) + ctx.globalCompositeOperation = "destination-out" + ctx.fillStyle = "#ffffff" + ctx.beginPath() + ctx.arc(0, height, cornersRoot.cornerRadius, 0, 2 * Math.PI) + ctx.fill() + } + + onWidthChanged: if (available) + requestPaint() + onHeightChanged: if (available) + requestPaint() + } + + // Bottom-left concave corner + Canvas { + id: bottomLeftCorner + anchors.bottom: parent.bottom + anchors.left: parent.left + width: cornersRoot.cornerSize + height: cornersRoot.cornerSize + antialiasing: true + renderTarget: Canvas.FramebufferObject + smooth: true + + onPaint: { + const ctx = getContext("2d") + if (!ctx) + return + + ctx.reset() + ctx.clearRect(0, 0, width, height) + ctx.fillStyle = Qt.rgba(cornersRoot.cornerColor.r, cornersRoot.cornerColor.g, cornersRoot.cornerColor.b, cornersRoot.cornerColor.a) + ctx.fillRect(0, 0, width, height) + ctx.globalCompositeOperation = "destination-out" + ctx.fillStyle = "#ffffff" + ctx.beginPath() + ctx.arc(width, 0, cornersRoot.cornerRadius, 0, 2 * Math.PI) + ctx.fill() + } + + onWidthChanged: if (available) + requestPaint() + onHeightChanged: if (available) + requestPaint() + } + + // Bottom-right concave corner + Canvas { + id: bottomRightCorner + anchors.bottom: parent.bottom + anchors.right: parent.right + width: cornersRoot.cornerSize + height: cornersRoot.cornerSize + antialiasing: true + renderTarget: Canvas.FramebufferObject + smooth: true + + onPaint: { + const ctx = getContext("2d") + if (!ctx) + return + + ctx.reset() + ctx.clearRect(0, 0, width, height) + ctx.fillStyle = Qt.rgba(cornersRoot.cornerColor.r, cornersRoot.cornerColor.g, cornersRoot.cornerColor.b, cornersRoot.cornerColor.a) + ctx.fillRect(0, 0, width, height) + ctx.globalCompositeOperation = "destination-out" + ctx.fillStyle = "#ffffff" + ctx.beginPath() + ctx.arc(0, 0, cornersRoot.cornerRadius, 0, 2 * Math.PI) + ctx.fill() + } + + onWidthChanged: if (available) + requestPaint() + onHeightChanged: if (available) + requestPaint() + } + + // Repaint all corners when color or radius changes + onCornerColorChanged: { + if (topLeftCorner.available) + topLeftCorner.requestPaint() + if (topRightCorner.available) + topRightCorner.requestPaint() + if (bottomLeftCorner.available) + bottomLeftCorner.requestPaint() + if (bottomRightCorner.available) + bottomRightCorner.requestPaint() + } + + onCornerRadiusChanged: { + if (topLeftCorner.available) + topLeftCorner.requestPaint() + if (topRightCorner.available) + topRightCorner.requestPaint() + if (bottomLeftCorner.available) + bottomLeftCorner.requestPaint() + if (bottomRightCorner.available) + bottomRightCorner.requestPaint() + } + } + } + + // Background MouseArea for closing panels when clicking outside + // Active whenever a panel is open - the mask ensures it only receives clicks when panel is open + MouseArea { + anchors.fill: parent + enabled: root.isPanelOpen + onClicked: { + if (PanelService.openedPanel) { + PanelService.openedPanel.close() + } + } + z: 0 // Behind panels and bar + } + + // All panels (as Items, not PanelWindows) + Repeater { + id: panelsRepeater + model: root.panelComponents + + delegate: Loader { + id: panelLoader + + // Lazy load panels - only create when first requested + // Panel stays loaded once created for faster subsequent opens + active: false + asynchronous: false + sourceComponent: modelData.component + + // Fill the container so panels have proper parent dimensions + anchors.fill: parent + + // Panel properties binding + property var panelScreen: root.screen + property string panelId: modelData.id + property int panelZIndex: modelData.zIndex || 50 + property bool hasBeenRequested: false + + Component.onCompleted: { + // Register the loader immediately so PanelService can load it on-demand + var objectName = panelId + "-" + (panelScreen?.name || "unknown") + PanelService.registerPanelLoader(panelLoader, objectName) + } + + // Activate loader when panel is first requested + function ensureLoaded() { + if (!hasBeenRequested) { + Logger.d("NFullScreenWindow", "Loading panel on-demand:", panelId) + hasBeenRequested = true + active = true + } + } + + onLoaded: { + if (item) { + // Set unique objectName per screen BEFORE registration: "calendarPanel-DP-1" + item.objectName = panelId + "-" + (panelScreen?.name || "unknown") + + // Set z-order for panels + item.z = panelZIndex + item.screen = panelScreen + + // Now register with PanelService (after objectName is set) + PanelService.registerPanel(item) + + Logger.d("NFullScreenWindow", "Panel loaded with objectName:", item.objectName, "on screen:", panelScreen?.name) + } + } + } + } + + // Bar (always on top) + Loader { + id: barLoader + asynchronous: false + sourceComponent: root.barComponent + + // Fill parent to provide dimensions for Bar to reference + anchors.fill: parent + + property ShellScreen screen: root.screen + + onLoaded: { + Logger.d("NFullScreenWindow", "Bar loaded:", item !== null) + if (item) { + Logger.d("NFullScreenWindow", "Bar size:", item.width, "x", item.height) + // Bar always has highest z-index + item.z = 100 + // Bind screen to bar component (use binding for reactivity) + item.screen = Qt.binding(function () { + return barLoader.screen + }) + Logger.d("NFullScreenWindow", "Bar screen set to:", item.screen?.name) + } + } + } + } + + // Centralized keyboard shortcuts - delegate to opened panel + // This ensures shortcuts work regardless of panel focus state + Shortcut { + sequence: "Escape" + enabled: root.isPanelOpen + onActivated: { + if (PanelService.openedPanel && PanelService.openedPanel.onEscapePressed) { + PanelService.openedPanel.onEscapePressed() + } else if (PanelService.openedPanel) { + PanelService.openedPanel.close() + } + } + } + + Shortcut { + sequence: "Tab" + enabled: root.isPanelOpen + onActivated: { + if (PanelService.openedPanel && PanelService.openedPanel.onTabPressed) { + PanelService.openedPanel.onTabPressed() + } + } + } + + Shortcut { + sequence: "Shift+Tab" + enabled: root.isPanelOpen + onActivated: { + if (PanelService.openedPanel && PanelService.openedPanel.onShiftTabPressed) { + PanelService.openedPanel.onShiftTabPressed() + } + } + } + + Shortcut { + sequence: "Up" + enabled: root.isPanelOpen + onActivated: { + if (PanelService.openedPanel && PanelService.openedPanel.onUpPressed) { + PanelService.openedPanel.onUpPressed() + } + } + } + + Shortcut { + sequence: "Down" + enabled: root.isPanelOpen + onActivated: { + if (PanelService.openedPanel && PanelService.openedPanel.onDownPressed) { + PanelService.openedPanel.onDownPressed() + } + } + } + + Shortcut { + sequence: "Return" + enabled: root.isPanelOpen + onActivated: { + if (PanelService.openedPanel && PanelService.openedPanel.onReturnPressed) { + PanelService.openedPanel.onReturnPressed() + } + } + } + + Shortcut { + sequence: "Enter" + enabled: root.isPanelOpen + onActivated: { + if (PanelService.openedPanel && PanelService.openedPanel.onReturnPressed) { + PanelService.openedPanel.onReturnPressed() + } + } + } + + Shortcut { + sequence: "Home" + enabled: root.isPanelOpen + onActivated: { + if (PanelService.openedPanel && PanelService.openedPanel.onHomePressed) { + PanelService.openedPanel.onHomePressed() + } + } + } + + Shortcut { + sequence: "End" + enabled: root.isPanelOpen + onActivated: { + if (PanelService.openedPanel && PanelService.openedPanel.onEndPressed) { + PanelService.openedPanel.onEndPressed() + } + } + } + + Shortcut { + sequence: "PgUp" + enabled: root.isPanelOpen + onActivated: { + if (PanelService.openedPanel && PanelService.openedPanel.onPageUpPressed) { + PanelService.openedPanel.onPageUpPressed() + } + } + } + + Shortcut { + sequence: "PgDown" + enabled: root.isPanelOpen + onActivated: { + if (PanelService.openedPanel && PanelService.openedPanel.onPageDownPressed) { + PanelService.openedPanel.onPageDownPressed() + } + } + } + + Shortcut { + sequence: "Ctrl+J" + enabled: root.isPanelOpen + onActivated: { + if (PanelService.openedPanel && PanelService.openedPanel.onCtrlJPressed) { + PanelService.openedPanel.onCtrlJPressed() + } + } + } + + Shortcut { + sequence: "Ctrl+K" + enabled: root.isPanelOpen + onActivated: { + if (PanelService.openedPanel && PanelService.openedPanel.onCtrlKPressed) { + PanelService.openedPanel.onCtrlKPressed() + } + } + } + + Shortcut { + sequence: "Left" + enabled: root.isPanelOpen + onActivated: { + if (PanelService.openedPanel && PanelService.openedPanel.onLeftPressed) { + PanelService.openedPanel.onLeftPressed() + } + } + } + + Shortcut { + sequence: "Right" + enabled: root.isPanelOpen + onActivated: { + if (PanelService.openedPanel && PanelService.openedPanel.onRightPressed) { + PanelService.openedPanel.onRightPressed() + } + } + } +} diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 98dfb453..2f301f53 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -1,35 +1,39 @@ import QtQuick -import QtQuick.Effects import Quickshell -import Quickshell.Wayland import qs.Commons import qs.Services -Loader { + +/** + * NPanel for use within NFullScreenWindow + */ +Item { id: root - property ShellScreen screen + // Screen property provided by NFullScreenWindow + property ShellScreen screen: null readonly property real opacityThreshold: 0.33 - property bool attachedToBar: (Settings.data.ui.panelsAttachedToBar && Settings.data.bar.backgroundOpacity > opacityThreshold) - property bool useOverlay: Settings.data.ui.panelsOverlayLayer + property bool forceDetached: false // Force panel to be detached regardless of settings + property bool attachedToBar: (Settings.data.ui.panelsAttachedToBar && Settings.data.bar.backgroundOpacity > opacityThreshold && !forceDetached) + + // Keyboard focus documentation (not currently used for focus mode) + // Just for documentation: true for panels with text input + // NFullScreenWindow always uses Exclusive focus when any panel is open + property bool panelKeyboardFocus: false property Component panelContent: null - // Panel size properties. Can be set directly on NPanel, or dynamically by the content. - // For dynamic sizing, the content should expose contentPreferredWidth, contentPreferredHeight, - // contentPreferredWidthRatio, or contentPreferredHeightRatio properties. - // Changes to these properties will be animated smoothly (except during panel dragging). + // Panel size properties property real preferredWidth: 700 property real preferredHeight: 900 property real preferredWidthRatio property real preferredHeightRatio property color panelBackgroundColor: Color.mSurface property color panelBorderColor: Color.mOutline - property bool draggable: false property var buttonItem: null - property string buttonName: "" + // Anchoring properties property bool panelAnchorHorizontalCenter: false property bool panelAnchorVerticalCenter: false property bool panelAnchorTop: false @@ -37,606 +41,469 @@ Loader { property bool panelAnchorLeft: false property bool panelAnchorRight: false - // Properties to support positioning relative to the opener (button) + // Button position properties property bool useButtonPosition: false property point buttonPosition: Qt.point(0, 0) property int buttonWidth: 0 property int buttonHeight: 0 - property bool panelKeyboardFocus: false - property bool backgroundClickEnabled: true + // Track whether panel is open + property bool isPanelOpen: false // Animation properties - property real panelBackgroundOpacity: 0 - property real panelContentOpacity: 0 - property real dimmingOpacity: 0 + property real animationProgress: 0 + + // Keyboard event handlers - override these in specific panels to handle shortcuts + // These are called from NFullScreenWindow's centralized shortcuts + function onEscapePressed() { + close() + } + function onTabPressed() {} + function onShiftTabPressed() {} + function onUpPressed() {} + function onDownPressed() {} + function onLeftPressed() {} + function onRightPressed() {} + function onReturnPressed() {} + function onHomePressed() {} + function onEndPressed() {} + function onPageUpPressed() {} + function onPageDownPressed() {} + function onCtrlJPressed() {} + function onCtrlKPressed() {} + + Behavior on animationProgress { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + + // Expose panel region for click-through mask (only when open) + readonly property var panelRegion: panelContentContainer.item?.maskRegion || null readonly property string barPosition: Settings.data.bar.position readonly property bool barIsVertical: barPosition === "left" || barPosition === "right" - readonly property real verticalBarWidth: Style.barHeight + readonly property bool barFloating: Settings.data.bar.floating || false + readonly property real barMarginH: barFloating ? Settings.data.bar.marginHorizontal * Style.marginXL : 0 + readonly property real barMarginV: barFloating ? Settings.data.bar.marginVertical * Style.marginXL : 0 - // Effective anchor properties - combines explicit anchors with implicit anchoring from useButtonPosition - readonly property bool effectivePanelAnchorTop: panelAnchorTop || (useButtonPosition && barPosition === "top") - readonly property bool effectivePanelAnchorBottom: panelAnchorBottom || (useButtonPosition && barPosition === "bottom") - readonly property bool effectivePanelAnchorLeft: panelAnchorLeft || (useButtonPosition && barPosition === "left") - readonly property bool effectivePanelAnchorRight: panelAnchorRight || (useButtonPosition && barPosition === "right") + // Helper to detect if any anchor is explicitly set + readonly property bool hasExplicitHorizontalAnchor: panelAnchorHorizontalCenter || panelAnchorLeft || panelAnchorRight + readonly property bool hasExplicitVerticalAnchor: panelAnchorVerticalCenter || panelAnchorTop || panelAnchorBottom + + // Effective anchor properties + // These are true when: + // 1. Explicitly anchored, OR + // 2. Using button position and bar is on that edge, OR + // 3. Attached to bar with no explicit anchors (default centering behavior) + readonly property bool effectivePanelAnchorTop: panelAnchorTop || (useButtonPosition && barPosition === "top") || (attachedToBar && !hasExplicitVerticalAnchor && barPosition === "top" && !barIsVertical) + readonly property bool effectivePanelAnchorBottom: panelAnchorBottom || (useButtonPosition && barPosition === "bottom") || (attachedToBar && !hasExplicitVerticalAnchor && barPosition === "bottom" && !barIsVertical) + readonly property bool effectivePanelAnchorLeft: panelAnchorLeft || (useButtonPosition && barPosition === "left") || (attachedToBar && !hasExplicitHorizontalAnchor && barPosition === "left" && barIsVertical) + readonly property bool effectivePanelAnchorRight: panelAnchorRight || (useButtonPosition && barPosition === "right") || (attachedToBar && !hasExplicitHorizontalAnchor && barPosition === "right" && barIsVertical) signal opened signal closed - active: false - asynchronous: true + // Panel visibility and sizing + visible: isPanelOpen + width: parent ? parent.width : 0 + height: parent ? parent.height : 0 - Component.onCompleted: { - PanelService.registerPanel(root) - } - - // ----------------------------------------- - // Functions to control background click behavior - function disableBackgroundClick() { - backgroundClickEnabled = false - } - - function enableBackgroundClick() { - // Add a small delay to prevent immediate close after drag release - enableBackgroundClickTimer.restart() - } - - Timer { - id: enableBackgroundClickTimer - interval: 100 - repeat: false - onTriggered: backgroundClickEnabled = true - } - - // ----------------------------------------- + // Panel control functions function toggle(buttonItem, buttonName) { - if (!active) { + if (!isPanelOpen) { open(buttonItem, buttonName) } else { close() } } - // ----------------------------------------- function open(buttonItem, buttonName) { - root.buttonItem = buttonItem - root.buttonName = buttonName || "" + if (!buttonItem && buttonName) { + buttonItem = BarService.lookupWidget(buttonName, screen.name) + } + + if (buttonItem) { + root.buttonItem = buttonItem + // Map button position to screen coordinates + var buttonPos = buttonItem.mapToItem(null, 0, 0) + root.buttonPosition = Qt.point(buttonPos.x, buttonPos.y) + root.buttonWidth = buttonItem.width + root.buttonHeight = buttonItem.height + root.useButtonPosition = true + } else { + // No button provided: reset button position mode + root.buttonItem = null + root.useButtonPosition = false + } setPosition() + isPanelOpen = true + animationProgress = 1 + // Notify PanelService PanelService.willOpenPanel(root) - backgroundClickEnabled = true - active = true - root.opened() + // Delay the opened signal to ensure content is fully loaded + // This ensures Component.onCompleted of the loaded content runs first + Qt.callLater(() => { + opened() + }) + + Logger.d("NPanel", "Opened panel", objectName) + Logger.d("NPanel", " Root size:", width, "x", height) } - // ----------------------------------------- function close() { - dimmingOpacity = 0 - panelBackgroundOpacity = 0 - panelContentOpacity = 0 - root.closed() - active = false - useButtonPosition = false - backgroundClickEnabled = true + isPanelOpen = false + animationProgress = 0 + + // Notify PanelService PanelService.closedPanel(root) + + closed() + + Logger.d("NPanel", "Closed panel", objectName) } - // ----------------------------------------- - function setPosition() { - // If we have a button name, we are landing here from an IPC call. - // IPC calls have no idead on which screen they panel will spawn. - // Resolve the button name to a proper button item now that we have a screen. - if (buttonName !== "" && root.screen !== null) { - buttonItem = BarService.lookupWidget(buttonName, root.screen.name) - } - - // Get the button position if provided - if (buttonItem !== undefined && buttonItem !== null) { - useButtonPosition = true - var itemPos = buttonItem.mapToItem(null, 0, 0) - buttonPosition = Qt.point(itemPos.x, itemPos.y) - buttonWidth = buttonItem.width - buttonHeight = buttonItem.height - } else { - useButtonPosition = false - } + function setPosition() {// Position calculation will be handled here + // For now, panels will be positioned based on anchors } - // ----------------------------------------- - sourceComponent: Component { - // PanelWindow has its own screen property inherited of QsWindow - PanelWindow { - id: panelWindow + // Loader for panel content + Loader { + id: panelContentContainer + anchors.fill: parent + active: root.isPanelOpen + asynchronous: false - readonly property bool barIsVisible: (screen !== null) && (Settings.data.bar.monitors.includes(screen.name) || (Settings.data.bar.monitors.length === 0)) + sourceComponent: Item { + anchors.fill: parent - Component.onCompleted: { - Logger.d("NPanel", "Opened", root.objectName, "on", screen.name) - dimmingOpacity = Style.opacityHeavy - } + // Expose panelBackground for mask region + property alias maskRegion: panelBackground - Connections { - target: panelWindow - function onScreenChanged() { - root.screen = screen - - // If called from IPC always reposition if screen is updated - if (buttonName) { - setPosition() - } - Logger.d("NPanel", "OnScreenChanged", root.screen.name) - } - } - - color: Color.transparent - - WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.namespace: "noctalia-panel" - WlrLayershell.layer: useOverlay ? WlrLayer.Overlay : WlrLayer.Top - WlrLayershell.keyboardFocus: root.panelKeyboardFocus ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None - - Region { - id: maskRegion - } - - Behavior on color { - ColorAnimation { - duration: Style.animationNormal - } - } - - anchors.top: true - anchors.left: true - anchors.right: true - anchors.bottom: true - - // Close any panel with Esc without requiring focus - Shortcut { - sequences: ["Escape"] - enabled: root.active - onActivated: root.close() - context: Qt.WindowShortcut - } - - // Clicking outside of the rectangle to close - MouseArea { + // The actual panel background and content + Item { anchors.fill: parent - enabled: root.backgroundClickEnabled - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - onClicked: root.close() - } - // The actual panel's content - NShapedRectangle { - id: panelBackground + NShapedRectangle { + id: panelBackground - backgroundColor: (attachedToBar && (topLeftInverted || topRightInverted || bottomLeftInverted || bottomRightInverted)) ? Qt.alpha(panelBackgroundColor, Settings.data.bar.backgroundOpacity) : panelBackgroundColor + backgroundColor: root.attachedToBar ? Qt.alpha(root.panelBackgroundColor, Settings.data.bar.backgroundOpacity) : root.panelBackgroundColor - topLeftRadius: Style.radiusL - topRightRadius: Style.radiusL - bottomLeftRadius: Style.radiusL - bottomRightRadius: Style.radiusL + // Animation properties + opacity: root.animationProgress + scale: root.attachedToBar ? 1 : (0.95 + root.animationProgress * 0.05) + // Transform origin for scale animation + transformOrigin: { + // For detached panels, scale from center + if (!root.attachedToBar) { + return Item.Center + } - /*// Drop shadow effect - layer.enabled: true - layer.effect: MultiEffect { - shadowEnabled: true - shadowBlur: 0.85 - shadowOpacity: 0.45 - shadowColor: Color.mShadow - shadowHorizontalOffset: (barPosition === "left" || barPosition === "top") ? 6 : - 6 - shadowVerticalOffset: (barPosition === "left" || barPosition === "top") ? 6 : - 6 - }*/ - - // Set inverted corners based on panel anchors and bar position - - // Top-left corner - topLeftInverted: { - if (!attachedToBar) - return false - - // Inverted if panel is anchored to top edge (bar is at top) - if (effectivePanelAnchorTop) - return true - // Or if panel is anchored to left edge (bar is at left) - if (effectivePanelAnchorLeft) - return true - return false - } - topLeftInvertedDirection: effectivePanelAnchorTop ? "horizontal" : "vertical" - - // Top-right corner - topRightInverted: { - if (!attachedToBar) - return false - - // Inverted if panel is anchored to top edge (bar is at top) - if (effectivePanelAnchorTop) - return true - // Or if panel is anchored to right edge (bar is at right) - if (effectivePanelAnchorRight) - return true - return false - } - topRightInvertedDirection: effectivePanelAnchorTop ? "horizontal" : "vertical" - - // Bottom-left corner - bottomLeftInverted: { - if (!attachedToBar) - return false - - // Inverted if panel is anchored to bottom edge (bar is at bottom) - if (effectivePanelAnchorBottom) - return true - // Or if panel is anchored to left edge (bar is at left) - if (effectivePanelAnchorLeft) - return true - return false - } - bottomLeftInvertedDirection: effectivePanelAnchorBottom ? "horizontal" : "vertical" - - // Bottom-right corner - bottomRightInverted: { - if (!attachedToBar) - return false - - // Inverted if panel is anchored to bottom edge (bar is at bottom) - if (effectivePanelAnchorBottom) - return true - // Or if panel is anchored to right edge (bar is at right) - if (effectivePanelAnchorRight) - return true - return false - } - bottomRightInvertedDirection: effectivePanelAnchorBottom ? "horizontal" : "vertical" - - // Dragging support - property bool draggable: root.draggable - property bool isDragged: false - property real manualX: 0 - property real manualY: 0 - width: { - var w - if (root.preferredWidthRatio !== undefined) { - w = Math.round(Math.max(screen?.width * root.preferredWidthRatio, root.preferredWidth)) - } else { - w = root.preferredWidth - } - // Clamp width so it is never bigger than the screen - return Math.min(w, screen?.width - Style.marginL * 2) - } - height: { - var h - if (root.preferredHeightRatio !== undefined) { - h = Math.round(Math.max(screen?.height * root.preferredHeightRatio, root.preferredHeight)) - } else { - h = root.preferredHeight + // For bar-attached panels, scale from the edge touching the bar + if (root.barPosition === "top") + return Item.Top + if (root.barPosition === "bottom") + return Item.Bottom + if (root.barPosition === "left") + return Item.Left + if (root.barPosition === "right") + return Item.Right + return Item.Center } - // Clamp height so it is never bigger than the screen - return Math.min(h, screen?.height - Style.barHeight - Style.marginL * 2) - } + topLeftRadius: Style.radiusL + topRightRadius: Style.radiusL + bottomLeftRadius: Style.radiusL + bottomRightRadius: Style.radiusL - opacity: root.panelBackgroundOpacity - x: isDragged ? manualX : calculatedX - y: isDragged ? manualY : calculatedY + // Inverted corners based on bar attachment + // When attached to bar AND effectively anchored to it, the corner(s) touching the bar should be inverted + topLeftInverted: root.attachedToBar && ((root.barPosition === "top" && !root.barIsVertical && root.effectivePanelAnchorTop) || (root.barPosition === "left" && root.barIsVertical && root.effectivePanelAnchorLeft)) + topRightInverted: root.attachedToBar && ((root.barPosition === "top" && !root.barIsVertical && root.effectivePanelAnchorTop) || (root.barPosition === "right" && root.barIsVertical && root.effectivePanelAnchorRight)) + bottomLeftInverted: root.attachedToBar && ((root.barPosition === "bottom" && !root.barIsVertical && root.effectivePanelAnchorBottom) || (root.barPosition === "left" && root.barIsVertical && root.effectivePanelAnchorLeft)) + bottomRightInverted: root.attachedToBar && ((root.barPosition === "bottom" && !root.barIsVertical && root.effectivePanelAnchorBottom) || (root.barPosition === "right" && root.barIsVertical && root.effectivePanelAnchorRight)) - // Animate width and height changes smoothly - Behavior on width { - enabled: !panelBackground.isDragged - NumberAnimation { - duration: Style.animationFast - easing.type: Easing.InOutQuad - } - } - - Behavior on height { - enabled: !panelBackground.isDragged - NumberAnimation { - duration: Style.animationFast - easing.type: Easing.InOutQuad - } - } - - // --------------------------------------------- - // Does not account for corners are they are negligible and helps keep the code clean. - // --------------------------------------------- - property real marginTop: { - if (!barIsVisible) { - return 0 + // Set inverted corner direction based on which edge touches the bar + // For horizontal bars (top/bottom): left/right edges touch bar → horizontal curves + // For vertical bars (left/right): top/bottom edges touch bar → vertical curves + topLeftInvertedDirection: root.barIsVertical ? "vertical" : "horizontal" + topRightInvertedDirection: root.barIsVertical ? "vertical" : "horizontal" + bottomLeftInvertedDirection: root.barIsVertical ? "vertical" : "horizontal" + bottomRightInvertedDirection: root.barIsVertical ? "vertical" : "horizontal" + width: { + var w + // Priority 1: Content-driven size (dynamic) + if (contentLoader.item && contentLoader.item.contentPreferredWidth !== undefined) { + w = contentLoader.item.contentPreferredWidth + } // Priority 2: Ratio-based size + else if (root.preferredWidthRatio !== undefined) { + w = Math.round(Math.max((parent.width || 1920) * root.preferredWidthRatio, root.preferredWidth)) + } // Priority 3: Static preferred width + else { + w = root.preferredWidth + } + return Math.min(w, (parent.width || 1920) - Style.marginL * 2) } - switch (barPosition) { - case "top": - return (Style.barHeight + (attachedToBar ? 0 : Style.marginS)) + (Settings.data.bar.floating ? Math.round(Settings.data.bar.marginVertical * Style.marginXL) : 0) - default: - return attachedToBar ? 0 : Style.marginS - } - } - - property real marginBottom: { - if (!barIsVisible) { - return 0 - } - switch (barPosition) { - case "bottom": - return (Style.barHeight + (attachedToBar ? 0 : Style.marginS)) + (Settings.data.bar.floating ? Math.round(Settings.data.bar.marginVertical * Style.marginXL) : 0) - default: - return attachedToBar ? 0 : Style.marginS - } - } - - property real marginLeft: { - if (!barIsVisible) { - return 0 - } - switch (barPosition) { - case "left": - return (Style.barHeight + (attachedToBar ? 0 : Style.marginS)) + (Settings.data.bar.floating ? Math.round(Settings.data.bar.marginHorizontal * Style.marginXL) : 0) - default: - return attachedToBar ? 0 : Style.marginS - } - } - - property real marginRight: { - if (!barIsVisible) { - return 0 - } - switch (barPosition) { - case "right": - return (Style.barHeight + (attachedToBar ? 0 : Style.marginS)) + (Settings.data.bar.floating ? Math.round(Settings.data.bar.marginHorizontal * Style.marginXL) : 0) - default: - return attachedToBar ? 0 : Style.marginS - } - } - - // --------------------------------------------- - property int calculatedX: { - // Priority to fixed anchoring - if (panelAnchorHorizontalCenter) { - // Center horizontally but respect bar margins - var centerX = Math.round((panelWindow.width - panelBackground.width) / 2) - var minX = marginLeft - var maxX = panelWindow.width - panelBackground.width - marginRight - return Math.round(Math.max(minX, Math.min(centerX, maxX))) - } else if (panelAnchorLeft) { - return marginLeft - } else if (panelAnchorRight) { - return Math.round(panelWindow.width - panelBackground.width - marginRight) + height: { + var h + // Priority 1: Content-driven size (dynamic) + if (contentLoader.item && contentLoader.item.contentPreferredHeight !== undefined) { + h = contentLoader.item.contentPreferredHeight + } // Priority 2: Ratio-based size + else if (root.preferredHeightRatio !== undefined) { + h = Math.round(Math.max((parent.height || 1080) * root.preferredHeightRatio, root.preferredHeight)) + } // Priority 3: Static preferred height + else { + h = root.preferredHeight + } + return Math.min(h, (parent.height || 1080) - Style.barHeight - Style.marginL * 2) } - // No fixed anchoring - if (barIsVertical) { - // Vertical bar - if (barPosition === "right") { - // To the left of the right bar - return Math.round(panelWindow.width - panelBackground.width - marginRight) + // Animation offset for slide effect on bar-attached panels + readonly property real slideOffset: root.attachedToBar ? (1 - root.animationProgress) * 20 : 0 + + // Position the panel using explicit x/y coordinates (no anchors) + // This makes coordinates clearer for the click-through mask system + x: { + // If useButtonPosition is enabled, align panel X with button + // Note: We check useButtonPosition, not buttonItem, because buttonItem may become invalid + // after the source panel (e.g., ControlCenter) closes, but we still have valid position data + if (root.useButtonPosition && parent.width > 0 && width > 0) { + if (root.barIsVertical) { + // For vertical bars + if (root.attachedToBar) { + // Attached panels: align with bar edge (left or right side) + if (root.barPosition === "left") { + // Panel to the right of left bar + var leftBarEdge = root.barMarginH + Style.barHeight + // Panel sits right at bar edge (inverted corners curve up/down) + // Slide from the bar when opening + // Shift left by 1px to eliminate any gap between bar and panel + return leftBarEdge - slideOffset - 1 + } else { + // right + // Panel to the left of right bar + var rightBarEdge = parent.width - root.barMarginH - Style.barHeight + // Panel sits right at bar edge (inverted corners curve up/down) + // Slide from the bar when opening + // Shift right by 1px to eliminate any gap between bar and panel + return rightBarEdge - width + slideOffset + 1 + } + } else { + // Detached panels: center on button X position + var panelX = root.buttonPosition.x + root.buttonWidth / 2 - width / 2 + // Clamp to screen bounds with margins + panelX = Math.max(Style.marginL, Math.min(panelX, parent.width - width - Style.marginL)) + return panelX + } + } else { + // For horizontal bars, center panel on button X position + var panelX = root.buttonPosition.x + root.buttonWidth / 2 - width / 2 + // Clamp to bar bounds (account for floating bar margins) + // When attached, panel should not extend beyond bar edges + if (root.attachedToBar) { + // Inverted corners with horizontal direction extend left/right by radiusL + // When bar is floating, it also has rounded corners, so we need extra inset + var cornerInset = Style.radiusL + (root.barFloating ? Style.radiusL : 0) + var barLeftEdge = root.barMarginH + cornerInset + var barRightEdge = parent.width - root.barMarginH - cornerInset + panelX = Math.max(barLeftEdge, Math.min(panelX, barRightEdge - width)) + } else { + panelX = Math.max(Style.marginL, Math.min(panelX, parent.width - width - Style.marginL)) + } + return panelX + } + } + + // Standard anchor positioning + Logger.d("NPanel", "Fallback to standard anchor positioning") + + if (root.panelAnchorHorizontalCenter) { + Logger.d("NPanel", " -> Horizontal center") + return (parent.width - width) / 2 + } else if (root.effectivePanelAnchorRight) { + Logger.d("NPanel", " -> Right anchor") + return parent.width - width - Style.marginL + } else if (root.effectivePanelAnchorLeft) { + Logger.d("NPanel", " -> Left anchor") + return Style.marginL } else { - // To the right of the left bar - return marginLeft - } - } else { - // Horizontal bar - if (root.useButtonPosition) { - // Position panel relative to button - var targetX = buttonPosition.x + (buttonWidth / 2) - (panelBackground.width / 2) - // Keep panel within screen bounds - var maxX = panelWindow.width - panelBackground.width - marginRight - var minX = marginLeft + // No explicit anchor: default to centering on bar + Logger.d("NPanel", " -> Default to center (no explicit anchor)") - if (Settings.data.bar.floating) { - maxX -= Settings.data.bar.marginHorizontal * Style.marginXL * 10 - minX += Settings.data.bar.marginHorizontal * Style.marginXL * 10 + // For horizontal bars: center horizontally + // For vertical bars: center horizontally in available space + if (root.barIsVertical) { + // Center in the space not occupied by the bar + if (root.barPosition === "left") { + var availableStart = root.barMarginH + Style.barHeight + var availableWidth = parent.width - availableStart - Style.marginL + return availableStart + (availableWidth - width) / 2 + } else { + // right + var availableWidth = parent.width - root.barMarginH - Style.barHeight - Style.marginL + return Style.marginL + (availableWidth - width) / 2 + } + } else { + // For horizontal bars: center horizontally, respect bar margins if attached + if (root.attachedToBar) { + // When attached, respect bar bounds (like button position does) + var cornerInset = Style.radiusL + (root.barFloating ? Style.radiusL : 0) + var barLeftEdge = root.barMarginH + cornerInset + var barRightEdge = parent.width - root.barMarginH - cornerInset + var centeredX = (parent.width - width) / 2 + return Math.max(barLeftEdge, Math.min(centeredX, barRightEdge - width)) + } else { + return (parent.width - width) / 2 + } + } + } + } + + y: { + // If useButtonPosition is enabled, position panel relative to bar + // Note: We check useButtonPosition, not buttonItem, because buttonItem may become invalid + // after the source panel (e.g., ControlCenter) closes, but we still have valid position data + if (root.useButtonPosition && parent.height > 0 && height > 0) { + if (root.barPosition === "top") { + // Panel below top bar + var topBarEdge = root.barMarginV + Style.barHeight + if (root.attachedToBar) { + // Panel sits right at bar edge (inverted corners curve to the sides) + // Slide from the bar when opening + // Shift up by 1px to eliminate any gap between bar and panel + return topBarEdge - slideOffset - 1 + } else { + return topBarEdge + Style.marginM + } + } else if (root.barPosition === "bottom") { + // Panel above bottom bar + var bottomBarEdge = parent.height - root.barMarginV - Style.barHeight + if (root.attachedToBar) { + // Panel sits right at bar edge (inverted corners curve to the sides) + // Slide from the bar when opening + // Shift down by 1px to eliminate any gap between bar and panel + return bottomBarEdge - height + slideOffset + 1 + } else { + return bottomBarEdge - height - Style.marginM + } + } else if (root.barIsVertical) { + // For vertical bars, center panel on button Y position + var panelY = root.buttonPosition.y + root.buttonHeight / 2 - height / 2 + // Clamp to bar bounds (account for floating bar margins and inverted corners) + var extraPadding = root.attachedToBar ? Style.radiusL : 0 + if (root.attachedToBar) { + // When attached, panel should not extend beyond bar edges (accounting for floating margins) + // Inverted corners with vertical direction extend up/down by radiusL + // When bar is floating, it also has rounded corners, so we need extra inset + var cornerInset = extraPadding + (root.barFloating ? Style.radiusL : 0) + var barTopEdge = root.barMarginV + cornerInset + var barBottomEdge = parent.height - root.barMarginV - cornerInset + panelY = Math.max(barTopEdge, Math.min(panelY, barBottomEdge - height)) + } else { + panelY = Math.max(Style.marginL + extraPadding, Math.min(panelY, parent.height - height - Style.marginL - extraPadding)) + } + return panelY + } + } + + // Standard anchor positioning + // Calculate bar offset for detached panels - they should never overlap the bar + var barOffset = 0 + if (!root.attachedToBar) { + // For detached panels, always account for bar position + if (root.barPosition === "top") { + barOffset = root.barMarginV + Style.barHeight + Style.marginM + } else if (root.barPosition === "bottom") { + barOffset = root.barMarginV + Style.barHeight + Style.marginM } - return Math.round(Math.max(minX, Math.min(targetX, maxX))) } else { - // Fallback to center horizontally - return Math.round((panelWindow.width - panelBackground.width) / 2) - } - } - } - - // --------------------------------------------- - property int calculatedY: { - // Priority to fixed anchoring - if (panelAnchorVerticalCenter) { - // Center vertically but respect bar margins - var centerY = Math.round((panelWindow.height - panelBackground.height) / 2) - var minY = marginTop - var maxY = panelWindow.height - panelBackground.height - marginBottom - return Math.round(Math.max(minY, Math.min(centerY, maxY))) - } else if (panelAnchorTop) { - return marginTop - } else if (panelAnchorBottom) { - return Math.round(panelWindow.height - panelBackground.height - marginBottom) - } - - // No fixed anchoring - if (barIsVertical) { - // Vertical bar - if (useButtonPosition) { - // Position panel relative to button - var targetY = buttonPosition.y + (buttonHeight / 2) - (panelBackground.height / 2) - // Keep panel within screen bounds - var maxY = panelWindow.height - panelBackground.height - marginBottom - var minY = marginTop - - if (Settings.data.bar.floating) { - maxY -= Settings.data.bar.marginHorizontal * Style.marginXL * 10 - minY += Settings.data.bar.marginHorizontal * Style.marginXL * 10 + // For attached panels with explicit anchors + if (root.effectivePanelAnchorTop && root.barPosition === "top") { + // When attached to top bar: position right at bar edge (like useButtonPosition does) + // Shift up by 1px to eliminate gap between bar and panel + return root.barMarginV + Style.barHeight - slideOffset - 1 + } else if (root.effectivePanelAnchorBottom && root.barPosition === "bottom") { + // When attached to bottom bar: position right at bar edge + // Shift down by 1px to eliminate gap between bar and panel + return parent.height - root.barMarginV - Style.barHeight - height + slideOffset + 1 + } else if (!root.hasExplicitVerticalAnchor) { + // No explicit vertical anchor AND attached: default to attaching to bar edge + if (root.barPosition === "top") { + // Attach to top bar + return root.barMarginV + Style.barHeight - slideOffset - 1 + } else if (root.barPosition === "bottom") { + // Attach to bottom bar + return parent.height - root.barMarginV - Style.barHeight - height + slideOffset + 1 + } + // For vertical bars with no explicit anchor: center vertically on bar + // This is handled in the else block below } + } - return Math.round(Math.max(minY, Math.min(targetY, maxY))) + if (root.panelAnchorVerticalCenter) { + return (parent.height - height) / 2 + } else if (root.effectivePanelAnchorTop) { + return barOffset + Style.marginL + } else if (root.effectivePanelAnchorBottom) { + return parent.height - height - barOffset - Style.marginL } else { - // Fallback to center vertically - return Math.round((panelWindow.height - panelBackground.height) / 2) - } - } else { - // Horizontal bar - if (barPosition === "bottom") { - // Above the bottom bar - return Math.round(panelWindow.height - panelBackground.height - marginBottom) - } else { - // Below the top bar - return marginTop - } - } - } - - // Animate in when component is completed - Component.onCompleted: { - // Start invisible - // Use a timer to delay the animation start, allowing QML to properly set up initial state - fadeInTimer.start() - } - - Timer { - id: fadeInTimer - interval: 1 - repeat: false - onTriggered: { - // Fade in background - root.panelBackgroundOpacity = 1.0 - } - } - - // Timer to fade in content after slide animation completes - Timer { - id: contentFadeInTimer - interval: Style.animationFast - repeat: false - running: true - onTriggered: root.panelContentOpacity = 1.0 - } - - // Reset drag position when panel closes - Connections { - target: root - function onClosed() { - panelBackground.isDragged = false - } - } - - // Prevent closing when clicking in the panel bg - MouseArea { - anchors.fill: parent - } - - // Animation behavior - Behavior on opacity { - NumberAnimation { - duration: Style.animationFast - easing.type: Easing.OutQuad - } - } - - Loader { - id: panelContentLoader - anchors.fill: parent - sourceComponent: root.panelContent - opacity: root.panelContentOpacity - - Behavior on opacity { - NumberAnimation { - duration: Style.animationFast - easing.type: Easing.OutQuad - } - } - - // Allow content to dynamically resize the panel - onItemChanged: { - if (item) { - // Bind to content's preferredWidth/Height if they exist - if (item.hasOwnProperty('contentPreferredWidth')) { - root.preferredWidth = Qt.binding(() => item.contentPreferredWidth) - } - if (item.hasOwnProperty('contentPreferredHeight')) { - root.preferredHeight = Qt.binding(() => item.contentPreferredHeight) - } - if (item.hasOwnProperty('contentPreferredWidthRatio')) { - root.preferredWidthRatio = Qt.binding(() => item.contentPreferredWidthRatio) - } - if (item.hasOwnProperty('contentPreferredHeightRatio')) { - root.preferredHeightRatio = Qt.binding(() => item.contentPreferredHeightRatio) + // No explicit vertical anchor + if (root.barIsVertical) { + // For vertical bars: center vertically on bar + if (root.attachedToBar) { + // When attached, respect bar bounds + var cornerInset = Style.radiusL + (root.barFloating ? Style.radiusL : 0) + var barTopEdge = root.barMarginV + cornerInset + var barBottomEdge = parent.height - root.barMarginV - cornerInset + var centeredY = (parent.height - height) / 2 + return Math.max(barTopEdge, Math.min(centeredY, barBottomEdge - height)) + } else { + return (parent.height - height) / 2 + } + } else { + // For horizontal bars: attach to bar edge by default + if (root.attachedToBar) { + if (root.barPosition === "top") { + return root.barMarginV + Style.barHeight - slideOffset - 1 + } else if (root.barPosition === "bottom") { + return parent.height - root.barMarginV - Style.barHeight - height + slideOffset + 1 + } + } + // Detached or no bar position: use default positioning + if (root.barPosition === "top") { + return barOffset + Style.marginL + } else if (root.barPosition === "bottom") { + return Style.marginL + } else { + return Style.marginL + } } } } - } - // Handle drag move on the whole panel area - DragHandler { - id: dragHandler - target: null - enabled: panelBackground.draggable - property real dragStartX: 0 - property real dragStartY: 0 - onActiveChanged: { - if (active) { - // Capture current position into manual coordinates BEFORE toggling isDragged - panelBackground.manualX = panelBackground.x - panelBackground.manualY = panelBackground.y - dragStartX = panelBackground.x - dragStartY = panelBackground.y - panelBackground.isDragged = true - if (root.enableBackgroundClick) - root.disableBackgroundClick() - } else { - // Keep isDragged true so we continue using the manual x/y after release - if (root.enableBackgroundClick) - root.enableBackgroundClick() - } - } - onTranslationChanged: { - // Proposed new coordinates from fixed drag origin - var nx = dragStartX + translation.x - var ny = dragStartY + translation.y - - // Calculate gaps so we never overlap the bar on any side - var baseGap = Style.marginS - var floatExtraH = Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * 2 * Style.marginXL : 0 - var floatExtraV = Settings.data.bar.floating ? Settings.data.bar.marginVertical * 2 * Style.marginXL : 0 - - var insetLeft = baseGap + ((barIsVisible && barPosition === "left") ? (Style.barHeight + floatExtraH) : 0) - var insetRight = baseGap + ((barIsVisible && barPosition === "right") ? (Style.barHeight + floatExtraH) : 0) - var insetTop = baseGap + ((barIsVisible && barPosition === "top") ? (Style.barHeight + floatExtraV) : 0) - var insetBottom = baseGap + ((barIsVisible && barPosition === "bottom") ? (Style.barHeight + floatExtraV) : 0) - - // Clamp within screen bounds accounting for insets - var maxX = panelWindow.width - panelBackground.width - insetRight - var minX = insetLeft - var maxY = panelWindow.height - panelBackground.height - insetBottom - var minY = insetTop - - panelBackground.manualX = Math.round(Math.max(minX, Math.min(nx, maxX))) - panelBackground.manualY = Math.round(Math.max(minY, Math.min(ny, maxY))) - } - } - - // Drag indicator border - Rectangle { - anchors.fill: parent - anchors.margins: 0 - color: Color.transparent - border.color: Color.mPrimary - border.width: Style.borderM - radius: Style.radiusL - visible: panelBackground.isDragged && dragHandler.active - opacity: 0.8 - z: 3000 - - // Subtle glow effect - Rectangle { + // MouseArea to catch clicks on the panel and prevent them from reaching the background + // This prevents closing the panel when clicking inside it + MouseArea { anchors.fill: parent - anchors.margins: 0 - color: Color.transparent - border.color: Color.mPrimary - border.width: Style.borderS - radius: Style.radiusL - opacity: 0.3 + z: -1 // Behind content, but on the panel background + onClicked: { + + // Accept and ignore - prevents propagation to background + } + } + + // Panel content loader + Loader { + id: contentLoader + anchors.fill: parent + sourceComponent: root.panelContent } } } diff --git a/Widgets/NShapedRectangle.qml b/Widgets/NShapedRectangle.qml index 36f76192..9da7ebb4 100644 --- a/Widgets/NShapedRectangle.qml +++ b/Widgets/NShapedRectangle.qml @@ -1,4 +1,5 @@ import QtQuick +import QtQuick.Effects import qs.Commons Item { @@ -24,8 +25,17 @@ Item { property string bottomLeftInvertedDirection: "horizontal" // default: curves left property string bottomRightInvertedDirection: "horizontal" // default: curves right - // Background color - property color backgroundColor: "white" + // Background color and borders + property color backgroundColor: Color.mPrimary + property color borderColor: Color.mOutline + property int borderWidth: Style.borderS + + // Shadow properties + property bool shadowEnabled: true + property real shadowOpacity: 1.0 // 0.0 to 1.0 + property real shadowBlur: 0.9 + property real shadowHorizontalOffset: 3 + property real shadowVerticalOffset: 3 // Check if any corner is inverted readonly property bool hasInvertedCorners: topLeftInverted || topRightInverted || bottomLeftInverted || bottomRightInverted @@ -36,144 +46,174 @@ Item { readonly property real leftPadding: Math.max((topLeftInverted && topLeftInvertedDirection === "horizontal") ? topLeftRadius : 0, (bottomLeftInverted && bottomLeftInvertedDirection === "horizontal") ? bottomLeftRadius : 0) readonly property real rightPadding: Math.max((topRightInverted && topRightInvertedDirection === "horizontal") ? topRightRadius : 0, (bottomRightInverted && bottomRightInvertedDirection === "horizontal") ? bottomRightRadius : 0) - // Simple rectangle for non-inverted corners (better performance) - Rectangle { - id: simpleBackground + // Background layer: shape with shadow effects (layer.enabled) + Item { + id: shadowLayer anchors.fill: parent - color: root.backgroundColor - radius: topLeftRadius // Use topLeftRadius as default - border.width: Style.borderS - border.color: Color.mOutline - visible: !root.hasInvertedCorners + z: 0 - topLeftRadius: root.topLeftRadius - topRightRadius: root.topRightRadius - bottomLeftRadius: root.bottomLeftRadius - bottomRightRadius: root.bottomRightRadius - } + // Apply shadow effect to this layer only + layer.enabled: root.shadowEnabled + layer.smooth: true + // layer.textureSize: Qt.size(width * Screen.devicePixelRatio, height * Screen.devicePixelRatio) + layer.effect: MultiEffect { + shadowEnabled: root.shadowEnabled + shadowOpacity: root.shadowOpacity + shadowColor: "#000000" + shadowHorizontalOffset: root.shadowHorizontalOffset + shadowVerticalOffset: root.shadowVerticalOffset + blur: root.shadowBlur + blurMax: 32 + } - // Background with custom corners (for inverted corners) - Canvas { - id: background - anchors.fill: parent - anchors.topMargin: -root.topPadding - anchors.bottomMargin: -root.bottomPadding - anchors.leftMargin: -root.leftPadding - anchors.rightMargin: -root.rightPadding - visible: root.hasInvertedCorners + // Simple rectangle for non-inverted corners (better performance) + Rectangle { + id: simpleBackground + anchors.fill: parent + color: root.backgroundColor + radius: topLeftRadius // Use topLeftRadius as default + border.width: borderWidth + border.color: borderColor + visible: !root.hasInvertedCorners - antialiasing: true - renderTarget: Canvas.FramebufferObject - smooth: true + topLeftRadius: root.topLeftRadius + topRightRadius: root.topRightRadius + bottomLeftRadius: root.bottomLeftRadius + bottomRightRadius: root.bottomRightRadius + } - onPaint: { - var ctx = getContext("2d") - ctx.reset() + // Background with custom corners (for inverted corners) + Canvas { + id: background + anchors.fill: parent + anchors.topMargin: -root.topPadding + anchors.bottomMargin: -root.bottomPadding + anchors.leftMargin: -root.leftPadding + anchors.rightMargin: -root.rightPadding + visible: root.hasInvertedCorners - // Adjust coordinates to account for inverted corner padding - var x = root.leftPadding - var y = root.topPadding - var w = width - root.leftPadding - root.rightPadding - var h = height - root.topPadding - root.bottomPadding + antialiasing: true + renderTarget: Canvas.FramebufferObject + smooth: true - ctx.fillStyle = root.backgroundColor - ctx.beginPath() + onPaint: { + var ctx = getContext("2d") + ctx.reset() - // Start from top left - if (topLeftInverted) { - if (topLeftInvertedDirection === "vertical") { - ctx.moveTo(x, y) + // Adjust coordinates to account for inverted corner padding + var x = root.leftPadding + var y = root.topPadding + var w = width - root.leftPadding - root.rightPadding + var h = height - root.topPadding - root.bottomPadding + + ctx.fillStyle = root.backgroundColor + ctx.beginPath() + + // Start from top left + if (topLeftInverted) { + if (topLeftInvertedDirection === "vertical") { + ctx.moveTo(x, y) + } else { + ctx.moveTo(x + topLeftRadius, y) + } } else { ctx.moveTo(x + topLeftRadius, y) } - } else { - ctx.moveTo(x + topLeftRadius, y) - } - // Top edge and top right corner - if (topRightInverted) { - if (topRightInvertedDirection === "horizontal") { - // Curves to the right - ctx.lineTo(x + w, y) - ctx.lineTo(x + w + topRightRadius, y) - ctx.quadraticCurveTo(x + w, y, x + w, y + topRightRadius) + // Top edge and top right corner + if (topRightInverted) { + if (topRightInvertedDirection === "horizontal") { + // Curves to the right + ctx.lineTo(x + w, y) + ctx.lineTo(x + w + topRightRadius, y) + ctx.quadraticCurveTo(x + w, y, x + w, y + topRightRadius) + } else { + // Curves upward + ctx.lineTo(x + w, y) + ctx.lineTo(x + w, y - topRightRadius) + ctx.quadraticCurveTo(x + w, y, x + w - topRightRadius, y) + ctx.lineTo(x + w, y) + ctx.lineTo(x + w, y + topRightRadius) + } } else { - // Curves upward - ctx.lineTo(x + w, y) - ctx.lineTo(x + w, y - topRightRadius) - ctx.quadraticCurveTo(x + w, y, x + w - topRightRadius, y) - ctx.lineTo(x + w, y) - ctx.lineTo(x + w, y + topRightRadius) + ctx.lineTo(x + w - topRightRadius, y) + ctx.arcTo(x + w, y, x + w, y + topRightRadius, topRightRadius) } - } else { - ctx.lineTo(x + w - topRightRadius, y) - ctx.arcTo(x + w, y, x + w, y + topRightRadius, topRightRadius) - } - // Right edge and bottom right corner - if (bottomRightInverted) { - if (bottomRightInvertedDirection === "horizontal") { - // Curves to the right + // Right edge and bottom right corner + if (bottomRightInverted) { + if (bottomRightInvertedDirection === "horizontal") { + // Curves to the right + ctx.lineTo(x + w, y + h - bottomRightRadius) + ctx.quadraticCurveTo(x + w, y + h, x + w + bottomRightRadius, y + h) + ctx.lineTo(x + w, y + h) + ctx.lineTo(x + w - bottomRightRadius, y + h) + } else { + // Curves downward + ctx.lineTo(x + w, y + h) + ctx.lineTo(x + w, y + h + bottomRightRadius) + ctx.quadraticCurveTo(x + w, y + h, x + w - bottomRightRadius, y + h) + } + } else { ctx.lineTo(x + w, y + h - bottomRightRadius) - ctx.quadraticCurveTo(x + w, y + h, x + w + bottomRightRadius, y + h) - ctx.lineTo(x + w, y + h) - ctx.lineTo(x + w - bottomRightRadius, y + h) - } else { - // Curves downward - ctx.lineTo(x + w, y + h) - ctx.lineTo(x + w, y + h + bottomRightRadius) - ctx.quadraticCurveTo(x + w, y + h, x + w - bottomRightRadius, y + h) + ctx.arcTo(x + w, y + h, x + w - bottomRightRadius, y + h, bottomRightRadius) } - } else { - ctx.lineTo(x + w, y + h - bottomRightRadius) - ctx.arcTo(x + w, y + h, x + w - bottomRightRadius, y + h, bottomRightRadius) - } - // Bottom edge and bottom left corner - if (bottomLeftInverted) { - if (bottomLeftInvertedDirection === "horizontal") { - // Curves to the left + // Bottom edge and bottom left corner + if (bottomLeftInverted) { + if (bottomLeftInvertedDirection === "horizontal") { + // Curves to the left + ctx.lineTo(x + bottomLeftRadius, y + h) + ctx.lineTo(x - bottomLeftRadius, y + h) + ctx.quadraticCurveTo(x, y + h, x, y + h - bottomLeftRadius) + } else { + // Curves downward + ctx.lineTo(x, y + h) + ctx.lineTo(x, y + h + bottomLeftRadius) + ctx.quadraticCurveTo(x, y + h, x + bottomLeftRadius, y + h) + ctx.lineTo(x, y + h) + ctx.lineTo(x, y + h - bottomLeftRadius) + } + } else { ctx.lineTo(x + bottomLeftRadius, y + h) - ctx.lineTo(x - bottomLeftRadius, y + h) - ctx.quadraticCurveTo(x, y + h, x, y + h - bottomLeftRadius) - } else { - // Curves downward - ctx.lineTo(x, y + h) - ctx.lineTo(x, y + h + bottomLeftRadius) - ctx.quadraticCurveTo(x, y + h, x + bottomLeftRadius, y + h) - ctx.lineTo(x, y + h) - ctx.lineTo(x, y + h - bottomLeftRadius) + ctx.arcTo(x, y + h, x, y + h - bottomLeftRadius, bottomLeftRadius) } - } else { - ctx.lineTo(x + bottomLeftRadius, y + h) - ctx.arcTo(x, y + h, x, y + h - bottomLeftRadius, bottomLeftRadius) - } - // Left edge and back to top left corner - if (topLeftInverted) { - if (topLeftInvertedDirection === "horizontal") { - // Curves to the left - ctx.lineTo(x, y + topLeftRadius) - ctx.quadraticCurveTo(x, y, x - topLeftRadius, y) - ctx.lineTo(x, y) - ctx.lineTo(x + topLeftRadius, y) + // Left edge and back to top left corner + if (topLeftInverted) { + if (topLeftInvertedDirection === "horizontal") { + // Curves to the left + ctx.lineTo(x, y + topLeftRadius) + ctx.quadraticCurveTo(x, y, x - topLeftRadius, y) + ctx.lineTo(x, y) + ctx.lineTo(x + topLeftRadius, y) + } else { + // Curves upward + ctx.lineTo(x, y + topLeftRadius) + ctx.lineTo(x, y) + ctx.lineTo(x, y - topLeftRadius) + ctx.quadraticCurveTo(x, y, x + topLeftRadius, y) + } } else { - // Curves upward ctx.lineTo(x, y + topLeftRadius) - ctx.lineTo(x, y) - ctx.lineTo(x, y - topLeftRadius) - ctx.quadraticCurveTo(x, y, x + topLeftRadius, y) + ctx.arcTo(x, y, x + topLeftRadius, y, topLeftRadius) } - } else { - ctx.lineTo(x, y + topLeftRadius) - ctx.arcTo(x, y, x + topLeftRadius, y, topLeftRadius) - } - ctx.closePath() - ctx.fill() + ctx.closePath() + ctx.fill() + } } } + // Content layer: for child elements (NO layer effects - keeps text sharp) + // Child components can be added here and will render on top without blur + default property alias contentChildren: contentLayer.data + Item { + id: contentLayer + anchors.fill: parent + z: 1 + } + // Trigger repaint when properties change onTopLeftRadiusChanged: background.requestPaint() onTopRightRadiusChanged: background.requestPaint() diff --git a/shell.qml b/shell.qml index 04ade2a6..e8513daf 100644 --- a/shell.qml +++ b/shell.qml @@ -75,6 +75,73 @@ ShellRoot { } } + // ------------------------------ + // Define panel components (must be at ShellRoot level for NFullScreenWindow access) + Component { + id: launcherComponent + Launcher {} + } + + Component { + id: controlCenterComponent + ControlCenterPanel {} + } + + Component { + id: calendarComponent + CalendarPanel {} + } + + Component { + id: settingsComponent + SettingsPanel {} + } + + Component { + id: directWidgetSettingsComponent + DirectWidgetSettingsPanel {} + } + + Component { + id: notificationHistoryComponent + NotificationHistoryPanel {} + } + + Component { + id: sessionMenuComponent + SessionMenu {} + } + + Component { + id: wifiComponent + WiFiPanel {} + } + + Component { + id: bluetoothComponent + BluetoothPanel {} + } + + Component { + id: audioComponent + AudioPanel {} + } + + Component { + id: wallpaperComponent + WallpaperPanel {} + } + + Component { + id: batteryComponent + BatteryPanel {} + } + + Component { + id: barComp + Bar {} + } + Loader { active: i18nLoaded && settingsLoaded @@ -99,8 +166,7 @@ ShellRoot { Background {} Overview {} - ScreenCorners {} - Bar {} + Dock {} Notification { @@ -121,67 +187,118 @@ ShellRoot { // IPCService is treated as a service // but it's actually an Item that needs to exists in the shell. IPCService {} + } + } - // ------------------------------ - // All the NPanels - Launcher { - id: launcherPanel - objectName: "launcherPanel" + // ------------------------------ + // NFullScreenWindow for each screen (manages bar + all panels) + // Wrapped in Loader to optimize memory - only loads when screen needs it + Variants { + model: Quickshell.screens + delegate: Item { + required property ShellScreen modelData + + property bool shouldBeActive: { + if (!i18nLoaded || !settingsLoaded) + return false + if (!BarService.isVisible) + return false + if (!modelData || !modelData.name) + return false + + var monitors = Settings.data.bar.monitors || [] + var result = monitors.length === 0 || monitors.includes(modelData.name) + + Logger.d("Shell", "NFullScreenWindow Loader for", modelData?.name, "- shouldBeActive:", result, "- monitors:", JSON.stringify(monitors)) + return result } - ControlCenterPanel { - id: controlCenterPanel - objectName: "controlCenterPanel" + property bool windowLoaded: false + + Loader { + id: windowLoader + active: parent.shouldBeActive + asynchronous: false + + property ShellScreen loaderScreen: modelData + + onLoaded: { + // Signal that window is loaded so exclusion zone can be created + parent.windowLoaded = true + } + + sourceComponent: NFullScreenWindow { + screen: windowLoader.loaderScreen + + // Register all panel components + panelComponents: [{ + "id": "launcherPanel", + "component": launcherComponent, + "zIndex": 50 + }, { + "id": "controlCenterPanel", + "component": controlCenterComponent, + "zIndex": 50 + }, { + "id": "calendarPanel", + "component": calendarComponent, + "zIndex": 50 + }, { + "id": "settingsPanel", + "component": settingsComponent, + "zIndex": 50 + }, { + "id": "directWidgetSettingsPanel", + "component": directWidgetSettingsComponent, + "zIndex": 50 + }, { + "id": "notificationHistoryPanel", + "component": notificationHistoryComponent, + "zIndex": 50 + }, { + "id": "sessionMenuPanel", + "component": sessionMenuComponent, + "zIndex": 50 + }, { + "id": "wifiPanel", + "component": wifiComponent, + "zIndex": 50 + }, { + "id": "bluetoothPanel", + "component": bluetoothComponent, + "zIndex": 50 + }, { + "id": "audioPanel", + "component": audioComponent, + "zIndex": 50 + }, { + "id": "wallpaperPanel", + "component": wallpaperComponent, + "zIndex": 50 + }, { + "id": "batteryPanel", + "component": batteryComponent, + "zIndex": 50 + }] + + // Bar component + barComponent: barComp + } } - CalendarPanel { - id: calendarPanel - objectName: "calendarPanel" - } + // BarExclusionZone - created after NFullScreenWindow has fully loaded + // Must also be disabled when bar is disabled (follows shouldBeActive) + Loader { + active: parent.windowLoaded && parent.shouldBeActive + asynchronous: false - SettingsPanel { - id: settingsPanel - objectName: "settingsPanel" - } + sourceComponent: BarExclusionZone { + screen: modelData + } - DirectWidgetSettingsPanel { - id: directWidgetSettingsPanel - objectName: "directWidgetSettingsPanel" - } - - NotificationHistoryPanel { - id: notificationHistoryPanel - objectName: "notificationHistoryPanel" - } - - SessionMenu { - id: sessionMenuPanel - objectName: "sessionMenuPanel" - } - - WiFiPanel { - id: wifiPanel - objectName: "wifiPanel" - } - - BluetoothPanel { - id: bluetoothPanel - objectName: "bluetoothPanel" - } - - AudioPanel { - id: audioPanel - objectName: "audioPanel" - } - - WallpaperPanel { - id: wallpaperPanel - objectName: "wallpaperPanel" - } - - BatteryPanel { - id: batteryPanel - objectName: "batteryPanel" + onLoaded: { + Logger.d("Shell", "BarExclusionZone created for", modelData?.name) + } } } } From 1f353b67317af85a80a7b2c9c1412a18b769de9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Atoch?= Date: Mon, 3 Nov 2025 01:07:13 -0500 Subject: [PATCH 05/30] IPC: Fix IPC calls when there is only one screen. --- Services/IPCService.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Services/IPCService.qml b/Services/IPCService.qml index a9323518..b3117105 100644 --- a/Services/IPCService.qml +++ b/Services/IPCService.qml @@ -329,7 +329,7 @@ Item { // Single monitor setup can execute immediately if (Quickshell.screens.length === 1) { - pendingCallback(Quickshell.screens[0]) + callback(Quickshell.screens[0]) } else { // Multi-monitors setup needs to start async detection detectedScreen = null From f2c0cfe814aafd303ed2b33de7a4506479c479fe Mon Sep 17 00:00:00 2001 From: Rain Xelelo Date: Mon, 3 Nov 2025 09:56:24 +0200 Subject: [PATCH 06/30] ukraine language --- Assets/Translations/uk-UA.json | 1860 ++++++++++++++++++++++++++++++++ 1 file changed, 1860 insertions(+) create mode 100644 Assets/Translations/uk-UA.json diff --git a/Assets/Translations/uk-UA.json b/Assets/Translations/uk-UA.json new file mode 100644 index 00000000..9e238ecb --- /dev/null +++ b/Assets/Translations/uk-UA.json @@ -0,0 +1,1860 @@ +{ + "settings": { + "general": { + "title": "Загальні", + "launch-setup-wizard": "Запустити майстер налаштування", + "profile": { + "section": { + "label": "Профіль", + "description": "Редагуйте дані користувача та аватар." + }, + "picture": { + "label": "Фото профілю {user}", + "description": "Ваше фото профілю, що відображається в інтерфейсі." + }, + "select-avatar": "Вибрати зображення аватара" + }, + "screen-corners": { + "section": { + "label": "Кути екрана", + "description": "Налаштуйте заокруглення кутів екрана та візуальні ефекти." + }, + "show-corners": { + "label": "Показувати кути екрана", + "description": "Відображати заокруглені кути на краях екрана." + }, + "solid-black": { + "label": "Суцільно чорні кути", + "description": "Використовувати суцільний чорний колір замість кольору фону панелі." + }, + "radius": { + "label": "Радіус кутів екрана", + "description": "Налаштуйте заокруглення кутів екрана.", + "reset": "Скинути радіус кутів екрана" + } + }, + "fonts": { + "reset-scaling": "Скинути масштаб", + "section": { + "label": "Шрифти", + "description": "Виберіть шрифти, що використовуються в інтерфейсі." + }, + "default": { + "label": "Стандартний шрифт", + "description": "Основний шрифт, що використовується в інтерфейсі.", + "placeholder": "Виберіть стандартний шрифт...", + "search-placeholder": "Пошук шрифту...", + "scale": { + "label": "Розмір стандартного шрифту", + "description": "Збільшити або зменшити розмір звичайного тексту." + } + }, + "monospace": { + "label": "Моноширинний шрифт", + "description": "Моноширинний шрифт для відображення чисел та статистики.", + "placeholder": "Виберіть моноширинний шрифт...", + "search-placeholder": "Пошук моноширинного шрифту...", + "scale": { + "label": "Розмір моноширинного шрифту", + "description": "Збільшити або зменшити розмір моноширинного тексту." + } + } + }, + "language": { + "section": { + "label": "Мова", + "description": "Виберіть бажану мову програми." + }, + "select": { + "label": "Мова програми", + "description": "Виберіть мову інтерфейсу програми.", + "auto-detect": "Автоматично" + } + } + }, + "audio": { + "title": "Аудіо", + "volumes": { + "section": { + "label": "Гучність", + "description": "Налаштуйте регулятори гучності та рівні звуку." + }, + "output-volume": { + "label": "Вихідна гучність", + "description": "Системний рівень гучності." + }, + "mute-output": { + "label": "Вимкнути вихідний звук", + "description": "Вимкнути основний аудіовихід системи." + }, + "input-volume": { + "label": "Вхідна гучність", + "description": "Рівень гучності мікрофона." + }, + "mute-input": { + "label": "Вимкнути вхідний звук", + "description": "Вимкнути стандартний аудіовхід (мікрофон)." + }, + "step-size": { + "label": "Крок зміни гучності", + "description": "Налаштуйте крок зміни гучності (колесо миші, гарячі клавіші)." + }, + "volume-overdrive": { + "label": "Дозволити перегучність", + "description": "Дозволити підвищення гучності понад 100%. Може не підтримуватись усім обладнанням." + } + }, + "devices": { + "section": { + "label": "Аудіопристрої", + "description": "Налаштуйте доступні пристрої введення та виведення звуку." + }, + "output-device": { + "label": "Пристрій виведення", + "description": "Виберіть бажаний пристрій виведення звуку." + }, + "input-device": { + "label": "Пристрій введення", + "description": "Виберіть бажаний пристрій введення звуку." + } + }, + "media": { + "section": { + "label": "Медіаплеєри", + "description": "Встановіть бажані та ігноровані медіапрограми." + }, + "primary-player": { + "label": "Основний плеєр", + "description": "Введіть ключове слово для ідентифікації основного плеєра.", + "placeholder": "напр. spotify, vlc, mpv" + }, + "excluded-player": { + "label": "Виключений плеєр", + "description": "Додайте ключові слова для плеєрів, які система має ігнорувати. Кожне ключове слово на новому рядку.", + "placeholder": "введіть підрядок та натисніть +" + }, + "visualizer-type": { + "label": "Тип візуалізації", + "description": "Виберіть тип візуалізації для відтворення медіа" + }, + "frame-rate": { + "label": "Частота кадрів", + "description": "Вищі значення плавніші, але споживають більше ресурсів." + }, + "scrolling-title": { + "label": "Прокрутка назви", + "description": "Увімкнути безперервну прокрутку для довгих назв медіа" + }, + "scrolling-speed": { + "label": "Швидкість прокрутки", + "description": "Час у секундах для прокрутки назви від початку до кінця" + } + } + }, + "display": { + "title": "Дисплей", + "monitors": { + "section": { + "label": "Налаштування для кожного монітора", + "description": "Налаштуйте яскравість для кожного дисплея." + }, + "brightness": "Яскравість", + "brightness-step": { + "label": "Крок зміни яскравості", + "description": "Налаштуйте крок зміни яскравості (колесо миші та гарячі клавіші)." + }, + "enforce-minimum": { + "label": "Примусова мінімальна яскравість (1%)", + "description": "Вирішує проблему повного вимкнення підсвітки на деяких дисплеях при яскравості 0%." + } + }, + "night-light": { + "section": { + "label": "Нічне світло", + "description": "Зменшіть випромінювання синього світла для кращого сну та зменшення втоми очей." + }, + "enable": { + "label": "Увімкнути нічне світло", + "description": "Застосувати теплий колірний фільтр для зменшення випромінювання синього світла." + }, + "temperature": { + "label": "Колірна температура", + "description": "Встановіть теплоту кольору для нічного та денного часу.", + "night": "Ніч", + "day": "День" + }, + "auto-schedule": { + "label": "Автоматичне планування", + "description": "На основі часу заходу та сходу сонця в {location} - рекомендовано." + }, + "manual-schedule": { + "label": "Ручне планування", + "description": "Встановіть власний час сходу та заходу сонця.", + "sunrise": "Час сходу сонця", + "sunset": "Час заходу сонця", + "select-start": "Виберіть час початку", + "select-stop": "Виберіть час зупинки" + }, + "force-activation": { + "label": "Примусова активація", + "description": "Ігнорує розклад і негайно застосовує нічний фільтр." + } + } + }, + "bar": { + "title": "Панель", + "appearance": { + "section": { + "label": "Зовнішній вигляд", + "description": "Налаштуйте зовнішній вигляд та положення панелі." + }, + "position": { + "label": "Положення панелі", + "description": "Виберіть, де розмістити панель на екрані." + }, + "density": { + "label": "Щільність панелі", + "description": "Налаштуйте відступи панелі для компактного або просторого вигляду." + }, + "background-opacity": { + "label": "Непрозорість фону", + "description": "Налаштуйте непрозорість фону панелі." + }, + "show-capsule": { + "label": "Показувати капсулу", + "description": "Показувати фон віджетів." + }, + "floating": { + "label": "Плаваюча панель", + "description": "Відображати панель як плаваючу 'таблетку'. Примітка: Це перемістить кути екрана до країв." + }, + "margins": { + "label": "Поля", + "description": "Налаштуйте поля навколо плаваючої панелі.", + "vertical": "Вертикальні", + "horizontal": "Горизонтальні" + } + }, + "widgets": { + "section": { + "label": "Розміщення віджетів", + "description": "Перетягуйте віджети для зміни порядку. Позначки вказують використання: [L]івий, [C]ентр, [R]правий." + } + }, + "monitors": { + "section": { + "label": "Відображення на моніторах", + "description": "Показувати панель на певних моніторах. За замовчуванням на всіх, якщо не вибрано." + } + }, + "tray": { + "blacklist": { + "label": "Чорний список", + "description": "Додайте правила виключення з трея, підтримує шаблони (*).", + "placeholder": "напр., nm-applet, Fcitx*" + } + } + }, + "dock": { + "title": "Док", + "enabled": { + "label": "Увімкнути док", + "description": "Показати або сховати док повністю" + }, + "appearance": { + "section": { + "label": "Зовнішній вигляд", + "description": "Налаштуйте поведінку та зовнішній вигляд дока." + }, + "display": { + "label": "Відображення", + "description": "Виберіть, як поводиться док.", + "always-visible": "Завжди видимий", + "auto-hide": "Автоприховування", + "exclusive": "Ексклюзивний" + }, + "background-opacity": { + "label": "Непрозорість фону", + "description": "Налаштуйте непрозорість фону дока." + }, + "floating-distance": { + "label": "Відстань плавання дока", + "description": "Встановіть відстань між доком і краєм екрана." + }, + "icon-size": { + "label": "Розмір дока", + "description": "Налаштуйте загальний розмір дока." + }, + "colorize-icons": { + "label": "Розфарбувати значки", + "description": "Застосувати кольори теми до значків програм у доці (тільки неактивні програми)." + } + }, + "monitors": { + "section": { + "label": "Відображення на моніторах", + "description": "Показувати док на певних моніторах. За замовчуванням на всіх, якщо не вибрано." + }, + "only-same-output": { + "label": "Тільки програми з того ж виходу", + "description": "Показувати тільки програми з виходу, де розташований док." + } + } + }, + "launcher": { + "title": "Запускач", + "settings": { + "section": { + "label": "Зовнішній вигляд", + "description": "Налаштуйте поведінку та зовнішній вигляд запускача." + }, + "position": { + "label": "Положення", + "description": "Виберіть, де з'являється панель запускача." + }, + "background-opacity": { + "label": "Непрозорість фону", + "description": "Налаштуйте непрозорість фону запускача." + }, + "clipboard-history": { + "label": "Увімкнути історію буфера обміну", + "description": "Отримати доступ до раніше скопійованих елементів із запускача." + }, + "sort-by-usage": { + "label": "Сортувати за використанням", + "description": "Коли увімкнено, часто запускані програми з'являються першими в списку." + }, + "use-app2unit": { + "label": "Використовувати App2Unit для запуску програм", + "description": "Використовує альтернативний метод запуску для кращого управління процесами програм і запобігання проблемам." + }, + "terminal-command": { + "label": "Команда терміналу", + "description": "Команда для запуску терміналу. Напр., 'kitty -e' або 'gnome-terminal --'." + }, + "custom-launch-prefix": { + "label": "Власний префікс запуску", + "description": "Додати префікс до команд власним запускачем (напр., 'runapp' для інтеграції з systemd)." + }, + "custom-launch-prefix-enabled": { + "label": "Увімкнути власний префікс запуску", + "description": "Використовувати власний префікс для запуску програм замість стандартного методу." + } + } + }, + "notifications": { + "title": "Сповіщення", + "settings": { + "section": { + "label": "Зовнішній вигляд", + "description": "Налаштуйте зовнішній вигляд та поведінку сповіщень." + }, + "do-not-disturb": { + "label": "Не турбувати", + "description": "Вимкнути всі спливаючі сповіщення, коли увімкнено." + }, + "enable-osd": { + "label": "Увімкнути екранну індикацію", + "description": "Показувати зміни гучності та яскравості в реальному часі." + }, + "location": { + "label": "Розташування", + "description": "Де з'являються сповіщення на екрані." + }, + "always-on-top": { + "label": "Завжди зверху", + "description": "Відображати сповіщення поверх повноекранних вікон та інших шарів." + }, + "background-opacity": { + "label": "Непрозорість фону", + "description": "Налаштуйте непрозорість фону сповіщень." + } + }, + "duration": { + "section": { + "label": "Тривалість сповіщень", + "description": "Налаштуйте, як довго сповіщення залишаються видимими залежно від рівня терміновості." + }, + "respect-expire": { + "label": "Дотримуватись часу закінчення", + "description": "Використовувати час закінчення, встановлений у сповіщенні." + }, + "low-urgency": { + "label": "Низька терміновість", + "description": "Як довго залишаються видимими сповіщення низького пріоритету." + }, + "normal-urgency": { + "label": "Звичайна терміновість", + "description": "Як довго залишаються видимими сповіщення звичайного пріоритету." + }, + "critical-urgency": { + "label": "Критична терміновість", + "description": "Як довго залишаються видимими критичні сповіщення." + } + }, + "monitors": { + "section": { + "label": "Відображення на моніторах", + "description": "Показувати сповіщення на певних моніторах. За замовчуванням на всіх, якщо не вибрано." + } + } + }, + "osd": { + "title": "Екранна індикація", + "description": "Налаштуйте екранні індикатори, такі як накладки гучності та яскравості.", + "section": { + "general": { + "label": "Загальні", + "description": "Налаштуйте видимість та поведінку екранної індикації." + } + }, + "enabled": { + "label": "Увімкнути екранну індикацію", + "description": "Показувати зміни гучності та яскравості в реальному часі." + }, + "always-on-top": { + "label": "Завжди зверху", + "description": "Відображати екранну індикацію поверх повноекранних вікон та інших шарів." + }, + "location": { + "label": "Розташування", + "description": "Де з'являються екранні індикації." + }, + "duration": { + "section": { + "label": "Час автоприховування", + "description": "Як довго екранна індикація залишається видимою перед автоматичним приховуванням." + }, + "auto-hide": { + "label": "Автоприховування через", + "description": "Налаштуйте час до зникнення екранної індикації." + } + }, + "monitors": { + "section": { + "label": "Відображення на моніторах", + "description": "Показувати екранну індикацію на певних моніторах. За замовчуванням на всіх, якщо не вибрано." + } + } + }, + "wallpaper": { + "title": "Шпалери", + "settings": { + "section": { + "label": "Налаштування шпалер", + "description": "Керуйте управлінням та відображенням шпалер." + }, + "enable-management": { + "label": "Увімкнути управління шпалерами", + "description": "Керувати шпалерами за допомогою Noctalia. Вимкніть, якщо надаєте перевагу іншій програмі." + }, + "folder": { + "label": "Тека шпалер", + "description": "Шлях до основної теки шпалер.", + "tooltip": "Огляд теки шпалер" + }, + "recursive-search": { + "label": "Шукати в підтеках", + "description": "Також шукати шпалери в підтеках директорії шпалер." + }, + "monitor-specific": { + "label": "Окремі теки для моніторів", + "description": "Встановити різні теки шпалер для кожного монітора.", + "tooltip": "Огляд теки шпалер" + }, + "select-folder": "Вибрати теку шпалер", + "select-monitor-folder": "Вибрати теку шпалер монітора" + }, + "look-feel": { + "section": { + "label": "Вигляд і відчуття" + }, + "fill-mode": { + "label": "Режим заповнення", + "description": "Виберіть, як зображення має масштабуватися відповідно до роздільної здатності монітора." + }, + "fill-color": { + "label": "Колір заповнення", + "description": "Виберіть колір заповнення, що може з'являтися за шпалерами." + }, + "transition-type": { + "label": "Тип переходу", + "description": "Тип анімації при перемиканні між шпалерами." + }, + "transition-duration": { + "label": "Тривалість переходу", + "description": "Тривалість анімацій переходу в секундах." + }, + "edge-smoothness": { + "label": "Пом'якшення краю переходу", + "description": "Застосовує м'який, розмитий ефект до краю переходів." + } + }, + "automation": { + "section": { + "label": "Автоматизація" + }, + "random-wallpaper": { + "label": "Випадкові шпалери", + "description": "Планувати випадкову зміну шпалер через регулярні інтервали." + }, + "interval": { + "label": "Інтервал шпалер", + "description": "Як часто автоматично змінювати шпалери." + }, + "custom-interval": { + "label": "Власний інтервал", + "description": "Введіть час як ГГ:ХХ (напр., 01:30)." + } + } + }, + "color-scheme": { + "title": "Колірна схема", + "color-source": { + "section": { + "label": "Джерело кольорів", + "description": "Основні налаштування для кольорів Noctalia." + }, + "use-wallpaper-colors": { + "label": "Використовувати кольори шпалер", + "description": "Генерувати колірні схеми зі шпалер за допомогою Matugen. Автоматично витягує кольори для створення цілісної теми." + }, + "matugen-scheme-type": { + "label": "Тип схеми Matugen", + "description": { + "scheme-content": "Виводить кольори, що тісно збігаються з базовим зображенням", + "scheme-expressive": "Яскрава палітра з грайливою насиченістю", + "scheme-fidelity": "Високоточна палітра, що зберігає вихідні відтінки", + "scheme-fruit-salad": "Барвистий мікс яскравих контрастних акцентів", + "scheme-monochrome": "Мінімальна палітра на основі одного відтінку", + "scheme-neutral": "Приглушена палітра з стриманими та заспокійливими тонами", + "scheme-rainbow": "Різноманітна палітра, що охоплює весь спектр", + "scheme-tonal-spot": "Збалансована палітра з фокусованими акцентами" + } + } + }, + "dark-mode": { + "switch": { + "label": "Темний режим", + "description": "Перемикає на темнішу тему для зручного перегляду вночі." + }, + "mode": { + "label": "Розклад темного режиму", + "description": "Увімкнути автоматичне перемикання між світлим і темним режимом.", + "off": "Вимкнено", + "manual": "Вручну", + "location": "Розташування" + } + }, + "predefined": { + "section": { + "label": "Попередньо визначені колірні схеми", + "description": "Виберіть із колекції попередньо визначених колірних схем." + }, + "generate-templates": { + "label": "Генерувати шаблони для попередньо визначених схем", + "description": "Генерувати шаблони Matugen (GTK, теми терміналу тощо) при використанні попередньо визначених колірних схем." + } + }, + "templates": { + "section": { + "label": "Шаблони", + "description": "Застосовувати кольори до зовнішніх програм." + }, + "ui": { + "label": "Інтерфейс", + "description": "Оформлення робочого середовища та інструментарію інтерфейсу.", + "gtk": { + "description": "Записати {filepath}" + }, + "qt": { + "description": "Записати {filepath}" + }, + "kcolorscheme": { + "description": "Записати {filepath}" + } + }, + "terminal": { + "label": "Термінал", + "description": "Оформлення емулятора терміналу.", + "kitty": { + "description": "Записати {filepath} та перезавантажити", + "description-missing": "Потрібна установка {app}" + }, + "ghostty": { + "description": "Записати {filepath} та перезавантажити", + "description-missing": "Потрібна установка {app}" + }, + "foot": { + "description": "Записати {filepath} та перезавантажити", + "description-missing": "Потрібна установка {app}" + } + }, + "programs": { + "label": "Програми", + "description": "Оформлення окремих програм.", + "fuzzel": { + "description": "Записати {filepath} та перезавантажити", + "description-missing": "Потрібна установка {app}" + }, + "vicinae": { + "description": "Записати {filepath} та перезавантажити", + "description-missing": "Потрібна установка {app}" + }, + "walker": { + "description": "Записати {filepath} та встановити тему noctalia", + "description-missing": "Потрібна установка {app}" + }, + "discord": { + "description": "Записати {filepath} для {client}. Тему Hyprluna потрібно активувати вручну.", + "description-missing": "Клієнт Discord не виявлено. Встановіть vencord, vesktop, webcord, armcord, equibop, lightcord або dorion." + }, + "pywalfox": { + "description": "Записати {filepath} та запустити pywalfox update", + "description-missing": "Потрібна установка {app}" + }, + "code": { + "description": "Записати {filepath}. Тему Hyprluna потрібно встановити та активувати вручну.", + "description-missing": "Потрібна установка {app}" + } + }, + "misc": { + "label": "Різне", + "description": "Додаткові параметри конфігурації.", + "user-templates": { + "label": "Користувацькі шаблони", + "description": "Увімкнути визначений користувачем конфіг Matugen. Файл шаблону буде створено за адресою ~/.config/noctalia/user-templates.toml при першому увімкненні" + } + } + } + }, + "location": { + "title": "Розташування", + "location": { + "section": { + "label": "Ваше розташування", + "description": "Отримуйте точну погоду та планування нічного світла, встановивши ваше розташування." + }, + "search": { + "label": "Шукати розташування", + "description": "напр., Торонто, ОН", + "placeholder": "Введіть назву місця" + } + }, + "weather": { + "section": { + "label": "Погода", + "description": "Виберіть бажану одиницю температури." + }, + "enabled": { + "label": "Увімкнути погоду", + "description": "Показувати інформацію про погоду в інтерфейсі та отримувати дані про погоду. Коли вимкнено, всі елементи погоди будуть приховані та мережеві запити не виконуватимуться." + }, + "fahrenheit": { + "label": "Відображати температуру в Фаренгейтах (°F)", + "description": "Відображати температуру в Фаренгейтах замість Цельсія." + }, + "show-in-calendar": { + "label": "Відображати погоду в календарі", + "description": "Показувати щоденний прогноз погоди безпосередньо в календарі." + } + }, + "date-time": { + "section": { + "label": "Дата і час", + "description": "Налаштуйте, як відображаються дата та час." + }, + "12hour-format": { + "label": "Використовувати 12-годинний формат часу", + "description": "Відображати час у 12-годинному форматі на екрані блокування та в календарі. Годинник на панелі має власні налаштування." + }, + "week-numbers": { + "label": "Показувати номери тижнів", + "description": "Відображати номер тижня року (напр., Тиждень 38) у календарі." + }, + "first-day-of-week": { + "label": "Перший день тижня", + "description": "Виберіть, який день починає тиждень у календарі.", + "automatic": "Автоматично (використовувати системну локаль)" + }, + "show-events": { + "label": "Показувати події календаря", + "description": "Відображати події на панелі календаря." + }, + "use-analog": { + "label": "Використовувати аналоговий годинник", + "description": "Показувати аналоговий годинник на екрані календаря." + } + } + }, + "network": { + "title": "Мережа", + "section": { + "description": "Керувати підключеннями Wi-Fi та Bluetooth." + }, + "wifi": { + "label": "Увімкнути Wi-Fi" + }, + "bluetooth": { + "label": "Увімкнути Bluetooth" + } + }, + "screen-recorder": { + "title": "Запис екрана", + "general": { + "section": { + "label": "Загальні налаштування", + "description": "Керуйте виведенням та вмістом запису екрана." + }, + "output-folder": { + "label": "Тека виведення", + "description": "Тека, де зберігатимуться записи екрана.", + "tooltip": "Огляд теки виведення" + }, + "show-cursor": { + "label": "Показувати курсор", + "description": "Записувати курсор миші у відео." + }, + "select-output-folder": "Вибрати теку виведення" + }, + "video": { + "section": { + "label": "Налаштування відео", + "description": "Налаштуйте параметри запису відео." + }, + "video-source": { + "label": "Джерело відео", + "description": "Рекомендується Portal, якщо виникають артефакти, спробуйте Screen." + }, + "frame-rate": { + "label": "Частота кадрів", + "description": "Цільова частота кадрів для записів екрана." + }, + "video-quality": { + "label": "Якість відео", + "description": "Вища якість призводить до більшого розміру файлів." + }, + "video-codec": { + "label": "Кодек відео", + "description": "h264 є найпоширенішим кодеком." + }, + "color-range": { + "label": "Діапазон кольорів", + "description": "Обмежений рекомендується для кращої сумісності." + } + }, + "audio": { + "section": { + "label": "Налаштування аудіо", + "description": "Налаштуйте параметри запису аудіо." + }, + "audio-source": { + "label": "Джерело аудіо", + "description": "Джерело аудіо для захоплення під час запису." + }, + "audio-codec": { + "label": "Кодек аудіо", + "description": "Opus рекомендується для найкращої продуктивності та найменшого розміру аудіо." + } + } + }, + "about": { + "title": "Про програму", + "noctalia": { + "section": { + "label": "Оболонка Noctalia", + "description": "Елегантна та мінімалістична оболонка робочого столу, ретельно створена для Wayland, побудована на Quickshell." + }, + "latest-version": "Остання версія:", + "installed-version": "Встановлена версія:", + "download-latest": "Завантажити останній реліз" + }, + "contributors": { + "section": { + "label": "Учасники", + "description": "Вітаємо нашого {count} чудового учасника!", + "description_plural": "Вітаємо наших {count} чудових учасників!" + } + }, + "support": "Підтримати нас" + }, + "hooks": { + "title": "Хуки", + "system-hooks": { + "section": { + "label": "Системні хуки", + "description": "Налаштуйте команди для виконання при системних подіях." + }, + "enable": { + "label": "Увімкнути хуки", + "description": "Увімкнути або вимкнути всі команди хуків." + } + }, + "wallpaper-changed": { + "label": "Шпалери змінено", + "description": "Команда для виконання при зміні шпалер.", + "placeholder": "напр., notify-send \"Шпалери\" \"Змінено\"" + }, + "theme-changed": { + "label": "Тему змінено", + "description": "Команда для виконання при перемиканні теми між темним і світлим режимом.", + "placeholder": "напр., notify-send \"Тема\" \"Перемкнуто\"" + }, + "info": { + "command-info": { + "label": "Інформація про команди хуків", + "description": "• Команди виконуються через оболонку (sh -c)\n• Команди запускаються у фоні (відокремлено)\n• Кнопки тестування виконують з поточними значеннями" + }, + "parameters": { + "label": "Доступні параметри", + "description": "• Хук шпалер: $1 = шлях до шпалер, $2 = назва екрана\n• Хук перемикання теми: $1 = true/false (стан темного режиму)" + } + } + }, + "control-center": { + "title": "Центр керування", + "section": { + "label": "Зовнішній вигляд", + "description": "Налаштуйте положення та поведінку панелі Центру керування." + }, + "position": { + "label": "Положення", + "description": "Виберіть, де з'являється панель Центру керування при відкритті." + }, + "cards": { + "section": { + "label": "Картки", + "description": "Налаштуйте, які елементи керування з'являються в Центрі керування та в якому порядку." + } + }, + "shortcuts": { + "section": { + "label": "Віджети швидкого доступу", + "description": "Налаштуйте та керуйте віджетами швидкого доступу." + }, + "sectionLeft": "Лівий", + "sectionRight": "Правий" + } + }, + "user-interface": { + "title": "Користувацький інтерфейс", + "section": { + "label": "Зовнішній вигляд", + "description": "Налаштуйте вигляд, відчуття та поведінку інтерфейсу." + }, + "tooltips": { + "label": "Показувати підказки", + "description": "Увімкнути або вимкнути підказки в інтерфейсі." + }, + "scaling": { + "label": "Масштабування інтерфейсу", + "description": "Змінює розмір загального користувацького інтерфейсу, окрім панелі.", + "reset-scaling": "Скинути масштабування інтерфейсу" + }, + "border-radius": { + "label": "Радіус меж", + "description": "Керує заокругленістю кутів вікон, кнопок та інших елементів.", + "reset": "Скинути радіус меж" + }, + "animation-speed": { + "label": "Швидкість анімації", + "description": "Налаштувати глобальну швидкість анімації.", + "reset": "Скинути швидкість анімації" + }, + "animation-disable": { + "label": "Вимкнути анімації інтерфейсу", + "description": "Вимкнути всі анімації для швидшого, більш відгукового досвіду." + }, + "panels-attached-to-bar": { + "label": "Прикріпити панелі до панелі", + "description": "Коли увімкнено, панелі будуть прикріплені до панелі з красивим дизайном інвертованих кутів" + }, + "panels-overlay": { + "label": "Відкривати панелі на шарі накладення", + "description": "Панелі з'являтимуться поверх повноекранних вікон" + } + }, + "lock-screen": { + "title": "Екран блокування", + "compact-lockscreen": { + "label": "Компактний екран блокування", + "description": "Показувати тільки поле входу та системні елементи керування, приховуючи віджети погоди та медіа." + }, + "lock-on-suspend": { + "label": "Блокувати при призупиненні", + "description": "Автоматично блокувати екран при призупиненні системи." + } + } + }, + "widgets": { + "tooltip": { + "placeholder": "Заповнювач" + }, + "file-picker": { + "title": "Вибір файлу", + "select-folder": "Вибрати теку", + "select-file": "Вибрати файл", + "search-placeholder": "Пошук файлів та тек...", + "select-current": "Вибрати поточне", + "cancel": "Скасувати" + }, + "datetime-tokens": { + "common": { + "12hour-time-minutes": "12-годинний час з хвилинами", + "24hour-time-minutes": "24-годинний час з хвилинами", + "24hour-time-seconds": "24-годинний час з секундами", + "weekday-month-day": "День тижня, місяць і день", + "iso-date": "Формат дати ISO", + "us-date": "Формат дати США", + "european-date": "Європейський формат дати", + "weekday-date": "День тижня з датою" + }, + "hour": { + "no-leading-zero": "Година без нуля попереду (0-23) - 24-годинний формат", + "leading-zero": "Година з нулем попереду (00-23) - 24-годинний формат" + }, + "minute": { + "no-leading-zero": "Хвилина без нуля попереду (0-59)", + "leading-zero": "Хвилина з нулем попереду (00-59)" + }, + "second": { + "no-leading-zero": "Секунда без нуля попереду (0-59)", + "leading-zero": "Секунда з нулем попереду (00-59)" + }, + "ampm": { + "uppercase": "AM/PM великими літерами", + "lowercase": "am/pm малими літерами" + }, + "timezone": { + "abbreviation": "Абревіатура часового поясу" + }, + "year": { + "two-digit": "Рік як двозначне число (00-99)", + "four-digit": "Рік як чотиризначне число" + }, + "month": { + "number-no-zero": "Місяць як число без нуля попереду (1-12)", + "number-leading-zero": "Місяць як число з нулем попереду (01-12)", + "abbreviated": "Скорочена назва місяця", + "full": "Повна назва місяця" + }, + "day": { + "no-leading-zero": "День без нуля попереду (1-31)", + "leading-zero": "День з нулем попереду (01-31)", + "abbreviated": "Скорочена назва дня", + "full": "Повна назва дня" + } + }, + "icon-picker": { + "title": "Вибір значка", + "search": { + "label": "Пошук" + }, + "cancel": "Скасувати", + "apply": "Застосувати" + }, + "color-picker": { + "title": "Вибір кольору", + "hex": { + "label": "Hex-колір", + "description": "Введіть шістнадцятковий код кольору." + }, + "rgb": { + "label": "Значення RGB", + "description": "Налаштуйте значення червоного, зеленого, синього та яскравості." + }, + "brightness": "Яскравість", + "theme-colors": { + "label": "Кольори теми", + "description": "Швидкий доступ до колірної палітри вашої теми." + }, + "palette": { + "label": "Палітра", + "description": "Виберіть із широкого діапазону попередньо визначених кольорів." + }, + "cancel": "Скасувати", + "apply": "Застосувати" + }, + "text-input": { + "clear": "Очистити" + } + }, + "bar": { + "widget-settings": { + "dialog": { + "cancel": "Скасувати", + "apply": "Застосувати" + }, + "section-editor": { + "placeholder": "Виберіть віджет для додавання...", + "search-placeholder": "Пошук віджета..." + }, + "active-window": { + "hide-mode": { + "label": "Режим приховування", + "description": "Керує поведінкою віджета, коли немає активного вікна." + }, + "show-app-icon": { + "label": "Показувати значок програми", + "description": "Відображати значок програми біля заголовка вікна." + }, + "scrolling-mode": { + "label": "Режим прокрутки", + "description": "Керування, коли увімкнено прокрутку тексту для довгих заголовків вікон." + }, + "max-width": { + "label": "Максимальна ширина", + "description": "Встановлює максимальний горизонтальний розмір віджета. Віджет зменшується для коротшого вмісту." + }, + "use-fixed-width": { + "label": "Використовувати фіксовану ширину", + "description": "Коли увімкнено, віджет завжди використовуватиме максимальну ширину замість динамічного налаштування до вмісту." + }, + "colorize-icons": { + "label": "Розфарбувати значки", + "description": "Застосувати кольори теми до значка активного вікна." + } + }, + "system-monitor": { + "cpu-usage": { + "label": "Використання ЦП", + "description": "Відображати поточний відсоток використання ЦП." + }, + "cpu-temperature": { + "label": "Температура ЦП", + "description": "Показувати показники температури ЦП, якщо доступно." + }, + "memory-usage": { + "label": "Використання пам'яті", + "description": "Відображати інформацію про поточне використання оперативної пам'яті." + }, + "memory-percentage": { + "label": "Пам'ять у відсотках", + "description": "Показувати використання пам'яті у відсотках замість абсолютних значень." + }, + "network-traffic": { + "label": "Мережевий трафік", + "description": "Відображати швидкість завантаження та вивантаження в мережі." + }, + "storage-usage": { + "label": "Використання сховища", + "description": "Показувати інформацію про використання дискового простору." + } + }, + "notification-history": { + "show-unread-badge": { + "label": "Показувати значок непрочитаних", + "description": "Відображати значок з кількістю непрочитаних сповіщень." + }, + "hide-badge-when-zero": { + "label": "Приховувати значок при нулі", + "description": "Приховувати значок сповіщень, коли немає непрочитаних сповіщень." + } + }, + "battery": { + "display-mode": { + "label": "Режим відображення", + "description": "Виберіть, як ви хочете, щоб це значення відображалося." + }, + "low-battery-threshold": { + "label": "Поріг попередження про низький заряд батареї", + "description": "Показувати попередження, коли батарея падає нижче цього відсотка." + } + }, + "control-center": { + "use-distro-logo": { + "label": "Використовувати логотип дистрибутива замість значка", + "description": "Використовувати логотип вашого дистрибутива замість власного значка." + }, + "icon": { + "label": "Значок", + "description": "Вибрати значок з бібліотеки або власний файл." + }, + "browse-library": "Огляд бібліотеки", + "browse-file": "Огляд файлу", + "select-custom-icon": "Вибрати власний значок" + }, + "keyboard-layout": { + "display-mode": { + "label": "Режим відображення", + "description": "Виберіть, як ви хочете, щоб це значення відображалося." + } + }, + "volume": { + "display-mode": { + "label": "Режим відображення", + "description": "Виберіть, як ви хочете, щоб це значення відображалося." + } + }, + "workspace": { + "label-mode": { + "label": "Режим міток", + "description": "Виберіть, як відображаються мітки робочих просторів." + }, + "hide-unoccupied": { + "label": "Приховати незайняті", + "description": "Не відображати робочі простори без вікон." + }, + "character-count": { + "label": "Кількість символів", + "description": "Кількість символів для відображення з назв робочих просторів (1-10)." + } + }, + "microphone": { + "display-mode": { + "label": "Режим відображення", + "description": "Виберіть, як ви хочете, щоб це значення відображалося." + } + }, + "brightness": { + "display-mode": { + "label": "Режим відображення", + "description": "Виберіть, як ви хочете, щоб це значення відображалося." + } + }, + "spacer": { + "width": { + "label": "Ширина", + "description": "Ширина відступу в пікселях" + } + }, + "custom-button": { + "icon": { + "label": "Значок", + "description": "Вибрати значок з бібліотеки." + }, + "browse": "Огляд", + "left-click": { + "label": "Лівий клік", + "description": "Команда для виконання при лівому кліку на кнопку." + }, + "right-click": { + "label": "Правий клік", + "description": "Команда для виконання при правому кліку на кнопку." + }, + "middle-click": { + "label": "Середній клік", + "description": "Команда для виконання при середньому кліку на кнопку." + }, + "dynamic-text": "Динамічний текст", + "text-stream": { + "label": "Потік", + "description": "Потокові рядки з команди відображатимуться як текст на кнопці." + }, + "display-command-output": { + "label": "Відображати виведення команди", + "description": "Введіть команду для запуску з регулярним інтервалом. Перший рядок її виведення відображатиметься як текст.", + "stream-description": "Введіть команду для безперервного запуску." + }, + "refresh-interval": { + "label": "Інтервал оновлення", + "description": "Інтервал у мілісекундах." + }, + "collapse-condition": { + "label": "Умова згортання", + "description": "Якщо вихідний текст збігається з цим значенням, кнопка згорнеться." + }, + "parse-json": { + "label": "Розбирати виведення як JSON", + "description": "Розбирати виведення команди як JSON-об'єкт для динамічного встановлення тексту та значка." + } + }, + "media-mini": { + "hide-mode": { + "label": "Режим приховування", + "description": "Керує поведінкою віджета, коли медіа не відтворюється." + }, + "show-album-art": { + "label": "Показувати обкладинку альбому", + "description": "Відображати обкладинку альбому для поточного треку." + }, + "show-visualizer": { + "label": "Показувати візуалізатор", + "description": "Відображати аудіовізуалізатор під час відтворення музики." + }, + "visualizer-type": { + "label": "Тип візуалізатора", + "description": "Виберіть стиль аудіовізуалізатора для відображення." + }, + "max-width": { + "label": "Максимальна ширина", + "description": "Встановлює максимальний горизонтальний розмір віджета. Віджет зменшується для коротшого вмісту." + }, + "use-fixed-width": { + "label": "Використовувати фіксовану ширину", + "description": "Коли увімкнено, віджет завжди використовуватиме максимальну ширину замість динамічного налаштування до вмісту." + }, + "scrolling-mode": { + "label": "Режим прокрутки", + "description": "Керування, коли увімкнено прокрутку тексту для довгих назв треків." + }, + "no-active-player": "Немає активного плеєра" + }, + "clock": { + "use-primary-color": { + "label": "Використовувати основний колір", + "description": "Коли увімкнено, застосовується основний колір для акценту." + }, + "use-monospaced-font": { + "label": "Використовувати моноширинний шрифт", + "description": "Коли увімкнено, годинник використовуватиме моноширинний шрифт." + }, + "use-custom-font": { + "label": "Використовувати власний шрифт", + "description": "Замінити стандартний вибір шрифту власним шрифтом для годинника." + }, + "custom-font": { + "label": "Власний шрифт", + "description": "Вибрати власний шрифт для відображення годинника.", + "placeholder": "Вибрати власний шрифт...", + "search-placeholder": "Пошук шрифтів..." + }, + "clock-display": { + "label": "Відображення годинника", + "description": "Налаштуйте відображення годинника, додавши токени зі списку нижче. Для використання 12-годинного формату ви повинні включити токен 'AP'." + }, + "horizontal-bar": { + "label": "Горизонтальна панель", + "description": "Порада: Використовуйте \\n для створення розриву рядка." + }, + "vertical-bar": { + "label": "Вертикальна панель", + "description": "Використовуйте пробіл для розділення кожної частини на новий рядок." + }, + "preview": "Попередній перегляд" + }, + "taskbar": { + "hide-mode": { + "label": "Режим приховування", + "description": "Керує поведінкою віджета, коли немає відповідних вікон." + }, + "only-active-workspaces": { + "label": "Тільки з активних робочих просторів", + "description": "Показувати тільки програми з активних робочих просторів." + }, + "only-same-output": { + "label": "Тільки з того ж виходу", + "description": "Показувати тільки програми з виходу, де розташована панель." + }, + "colorize-icons": { + "label": "Розфарбувати значки", + "description": "Застосувати кольори теми до значків панелі завдань." + } + }, + "tray": { + "colorize-icons": { + "label": "Розфарбувати значки", + "description": "Застосувати кольори теми до значків трея." + } + }, + "audio-visualizer": { + "width": { + "label": "Ширина", + "description": "Власна ширина компонента." + }, + "color-name": { + "label": "Колір заповнення", + "description": "Вибрати колір для візуалізатора." + }, + "hide-when-idle": { + "label": "Приховати, коли медіа не відтворюється", + "description": "Коли увімкнено, візуалізатор приховано, якщо плеєр активно не відтворює." + } + }, + "lock-keys": { + "indicator-style": { + "label": "Стиль індикатора", + "description": "Стиль значків для індикаторів клавіш блокування", + "large": "Великі літери", + "small": "Малі літери", + "square": "Квадратні літери", + "square-round": "Заокруглені квадратні літери", + "circle": "Круглі літери", + "circle-dash": "Пунктирні круглі літери", + "circle-dot": "Точкові круглі літери", + "hex": "Шестикутні літери" + }, + "show-caps-lock": { + "label": "Caps Lock", + "description": "Відображати стан caps lock." + }, + "show-num-lock": { + "label": "Num Lock", + "description": "Відображати стан num lock." + }, + "show-scroll-lock": { + "label": "Scroll Lock", + "description": "Відображати стан scroll lock." + } + } + } + }, + "notifications": { + "panel": { + "title": "Сповіщення", + "no-notifications": "Немає сповіщень", + "description": "Ваші сповіщення з'являтимуться тут по мірі надходження.", + "click-to-expand": "Клацніть для розгортання" + } + }, + "wallpaper": { + "panel": { + "title": "Вибір шпалер", + "apply-all-monitors": { + "label": "Застосувати до всіх моніторів", + "description": "Застосувати вибрані шпалери до всіх моніторів одночасно." + }, + "search": "Пошук:" + }, + "transitions": { + "none": "Немає", + "random": "Випадковий", + "fade": "Затухання", + "disc": "Диск", + "stripes": "Смуги", + "wipe": "Змітання" + }, + "fill-modes": { + "center": "Центр", + "crop": "Обрізати (Заповнити)", + "fit": "Вмістити (Містити)", + "stretch": "Розтягнути" + }, + "no-match": "Збігів не знайдено.", + "no-wallpaper": "Шпалери не знайдено.", + "try-different-search": "Спробуйте інший пошуковий запит.", + "configure-directory": "Налаштуйте вашу теку шпалер зі зображеннями." + }, + "bluetooth": { + "panel": { + "title": "Bluetooth", + "disabled": "Bluetooth вимкнено", + "enable-message": "Увімкніть Bluetooth, щоб побачити доступні пристрої.", + "connected-devices": "Підключені пристрої", + "known-devices": "Відомі пристрої", + "available-devices": "Доступні пристрої", + "scanning": "Сканування пристроїв...", + "pairing-mode": "Переконайтеся, що ваш пристрій у режимі з'єднання." + } + }, + "wifi": { + "panel": { + "title": "Wi-Fi", + "disabled": "Wi-Fi вимкнено", + "enable-message": "Увімкніть Wi-Fi, щоб побачити доступні мережі.", + "searching": "Пошук близьких мереж...", + "connected": "Підключено", + "disconnecting": "Відключення...", + "forgetting": "Забування...", + "saved": "Збережено", + "disconnect": "Відключити", + "enter-password": "Введіть пароль...", + "connect": "Підключити", + "password": "Пароль", + "forget-network": "Забути цю мережу?", + "forget": "Забути", + "no-networks": "Мереж не знайдено", + "scan-again": "Сканувати знову" + } + }, + "tooltips": { + "refresh": "Оновити", + "close": "Закрити", + "up": "Вгору", + "home": "Додому", + "refresh-wallpaper-list": "Оновити список шпалер", + "refresh-devices": "Оновити пристрої", + "forget-network": "Забути мережу", + "clear-history": "Очистити історію", + "delete-notification": "Видалити сповіщення", + "previous-month": "Попередній місяць", + "next-month": "Наступний місяць", + "add-widget": "Додати віджет", + "widget-settings": "Налаштування віджета", + "remove-widget": "Видалити віджет", + "move-to-left-section": "Перемістити в ліву секцію", + "move-to-center-section": "Перемістити в центральну секцію", + "move-to-right-section": "Перемістити в праву секцію", + "open-settings": "Відкрити налаштування", + "session-menu": "Меню сеансу", + "cancel-timer": "Скасувати таймер", + "start-screen-recording": "Почати запис екрана", + "stop-screen-recording": "Зупинити запис екрана", + "screen-recorder-not-installed": "Запис екрана не встановлено", + "keep-awake": "Не спати", + "enable-keep-awake": "Увімкнути режим неспання", + "disable-keep-awake": "Вимкнути режим неспання", + "wallpaper-selector": "Лівий клік: Відкрити вибір шпалер.\nПравий клік: Встановити випадкові шпалери.", + "do-not-disturb-enabled": "'Не турбувати' увімкнено", + "do-not-disturb-disabled": "'Не турбувати' вимкнено", + "connect-disconnect-devices": "Лівий клік для підключення. Правий клік для забування.", + "set-power-profile": "Встановити профіль живлення \"{profile}\"", + "switch-to-light-mode": "Перемкнутися на світлий режим", + "switch-to-dark-mode": "Перемкнутися на темний режим", + "night-light-disabled": "Нічне світло вимкнено.\nЛівий клік для циклічного режиму.\nПравий клік для доступу до налаштувань.", + "night-light-enabled": "Нічне світло увімкнено.\nЛівий клік для циклічного режиму.\nПравий клік для доступу до налаштувань.", + "night-light-forced": "Нічне світло примусово.\nЛівий клік для циклічного режиму.\nПравий клік для доступу до налаштувань.", + "night-light-not-installed": "Нічне світло недоступне.\nwlsunset не встановлено.", + "click-to-start-recording": "Клацніть для початку запису", + "click-to-stop-recording": "Клацніть для зупинки запису", + "open-control-center": "Відкрити центр керування", + "volume-at": "Вихідна гучність на {volume}%\nЛівий клік для налаштувань. Правий клік для вимкнення звуку.\nПрокрутка для зміни гучності.", + "microphone-volume-at": "Гучність мікрофона на {volume}%\nЛівий клік для налаштувань. Правий клік для вимкнення звуку.\nПрокрутка для зміни гучності.", + "brightness-at": "Яскравість: {brightness}%\nПравий клік для налаштувань.\nПрокрутка для зміни яскравості.", + "manage-wifi": "Керувати Wi-Fi", + "bluetooth-devices": "Пристрої Bluetooth", + "open-notification-history-enable-dnd": "Відкрити історію сповіщень\nПравий клік для увімкнення \"Не турбувати\".", + "open-notification-history-disable-dnd": "Відкрити історію сповіщень\nПравий клік для вимкнення \"Не турбувати\".", + "open-wallpaper-selector": "Відкрити вибір шпалер", + "previous-media": "Попереднє медіа", + "pause": "Пауза", + "play": "Відтворити", + "next-media": "Наступне медіа", + "power-profile": "Профіль живлення '{profile}'", + "keyboard-layout": "Розкладка клавіатури {layout}", + "output-muted": "Перемкнути вимкнення виходу", + "input-muted": "Перемкнути вимкнення входу" + }, + "clock": { + "tooltip": "Відкрити календар" + }, + "calendar": { + "panel": { + "week": "Тиждень" + }, + "weather": { + "loading": "Завантаження погоди…" + } + }, + "dock": { + "menu": { + "focus": "Фокус", + "pin": "Закріпити", + "unpin": "Відкріпити", + "close": "Закрити" + } + }, + "launcher": { + "pin": "Закріпити в доці", + "unpin": "Відкріпити з доку" + }, + "placeholders": { + "search-icons": "напр., noctalia, niri, battery, cloud", + "profile-picture-path": "/home/user/.face", + "enter-width-pixels": "Введіть ширину в пікселях", + "enter-command": "Введіть команду для виконання (програма або власний скрипт)", + "command-example": "echo \"Привіт, Світ\"", + "enter-text-to-collapse": "напр., 'нічого не відтворюється'. Використовуйте /regex/ для шаблонів.", + "search-wallpapers": "Введіть для фільтрації шпалер...", + "search-launcher": "Пошук записів... або використовуйте > для команд", + "search": "Пошук...", + "select": "Вибрати", + "cancel": "Скасувати", + "test": "Тест" + }, + "options": { + "colors": { + "primary": "Основний", + "secondary": "Вторинний", + "tertiary": "Третинний", + "error": "Помилка", + "onSurface": "На поверхні" + }, + "bar": { + "position": { + "top": "Зверху", + "bottom": "Знизу", + "left": "Зліва", + "right": "Справа" + }, + "density": { + "mini": "Міні", + "compact": "Компактний", + "default": "Стандартний", + "comfortable": "Зручний" + } + }, + "launcher": { + "position": { + "center": "Центр (за замовчуванням)", + "top_left": "Зверху зліва", + "top_right": "Зверху справа", + "bottom_left": "Знизу зліва", + "bottom_right": "Знизу справа", + "bottom_center": "Знизу по центру", + "top_center": "Зверху по центру", + "center_left": "Зліва по центру", + "center_right": "Справа по центру" + } + }, + "control-center": { + "position": { + "close_to_bar_button": "Біля кнопки панелі", + "top_left": "Зверху зліва", + "top_right": "Зверху справа", + "bottom_left": "Знизу зліва", + "bottom_right": "Знизу справа", + "bottom_center": "Знизу по центру", + "top_center": "Зверху по центру", + "center": "Центр" + }, + "quickSettingsStyle": { + "modern": "Сучасний", + "classic": "Класичний", + "compact": "Компактний" + } + }, + "osd": { + "position": { + "top_left": "Зверху зліва", + "top_right": "Зверху справа", + "top_center": "Зверху по центру", + "bottom_left": "Знизу зліва", + "bottom_right": "Знизу справа", + "bottom_center": "Знизу по центру", + "center_left": "Зліва по центру", + "center_right": "Справа по центру" + } + }, + "display-mode": { + "on-hover": "При наведенні", + "always-show": "Завжди показувати", + "always-hide": "Завжди приховувати", + "force-open": "Примусово відкрити" + }, + "workspace-labels": { + "none": "Немає", + "index": "Індекс", + "name": "Назва" + }, + "visualizer-types": { + "none": "Немає", + "linear": "Лінійний", + "mirrored": "Дзеркальний", + "wave": "Хвиля" + }, + "scrolling-modes": { + "always": "Завжди прокручувати", + "hover": "Прокручувати при наведенні", + "never": "Ніколи не прокручувати" + }, + "hide-modes": { + "visible": "Завжди видимий", + "hidden": "Приховати, коли порожньо", + "transparent": "Прозорий, коли порожньо" + }, + "frame-rates": { + "fps": "{fps} к/с" + }, + "screen-recording": { + "sources": { + "portal": "Портал", + "screen": "Екран" + }, + "quality": { + "medium": "Середня", + "high": "Висока", + "very-high": "Дуже висока", + "ultra": "Ультра" + }, + "color-range": { + "limited": "Обмежений", + "full": "Повний" + }, + "audio-sources": { + "system-output": "Системний вихід", + "microphone-input": "Вхід мікрофона", + "both": "Системний вихід + вхід мікрофона" + } + } + }, + "session-menu": { + "title": "Меню сеансу", + "action-in-seconds": "{action} через {seconds} секунд...", + "lock": "Заблокувати", + "lock-and-suspend": "Заблокувати та призупинити", + "suspend": "Призупинити", + "reboot": "Перезавантажити", + "logout": "Вийти", + "shutdown": "Вимкнути" + }, + "plugins": { + "applications": "Програми", + "clipboard": "Історія буфера обміну", + "calculator": "Калькулятор", + "clipboard-search-description": "Пошук в історії буфера обміну", + "clipboard-clear-description": "Очистити всю історію буфера обміну", + "clipboard-history-disabled": "Історія буфера обміну вимкнена", + "clipboard-history-disabled-description": "Увімкніть історію буфера обміну в налаштуваннях або встановіть cliphist", + "clipboard-clear-history": "Очистити історію буфера обміну", + "clipboard-clear-description-full": "Видалити всі елементи з історії буфера обміну", + "clipboard-loading": "Завантаження історії буфера обміну...", + "clipboard-loading-description": "Зачекайте, будь ласка", + "calculator-description": "Калькулятор - обчислення математичних виразів", + "calculator-name": "Калькулятор", + "calculator-enter-expression": "Введіть математичний вираз", + "calculator-error": "Помилка" + }, + "system": { + "uptime": "Час роботи: {uptime}", + "welcome-back": "З поверненням,", + "monitor-description": "{model} ({width}x{height} @ {scale}x)", + "scaling-percentage": "{percentage}%", + "location-display": "{name} ({coordinates})", + "signal-strength": "{signal}%", + "cpu-temperature": "{temp}°C", + "disk-usage": "{percent}%", + "widget-settings-title": "Налаштування {widget}", + "unknown-app": "Невідома програма", + "no-media-player-detected": "Медіаплеєр не виявлено", + "user-requested": "На запит користувача", + "unknown": "Невідомо", + "unknown-version": "Невідомо", + "unknown-layout": "Невідомо" + }, + "lock-screen": { + "password": "Введіть ваш пароль...", + "welcome-back": "З поверненням,", + "authenticating": "Автентифікація...", + "authentication-failed": "Помилка автентифікації", + "shut-down": "Вимкнути", + "restart": "Перезапустити", + "suspend": "Призупинити" + }, + "quickSettings": { + "notifications": { + "label": { + "enabled": "Сповіщення", + "disabled": "Не турбувати" + }, + "tooltip": { + "action": "Лівий клік: Відкрити історію сповіщень\nПравий клік: Перемкнути \"Не турбувати\"" + } + }, + "screenRecorder": { + "label": { + "recording": "Зупинити", + "stopped": "Записати" + }, + "tooltip": { + "action": "Клацніть для початку/зупинки запису екрана" + } + }, + "powerProfile": { + "label": { + "unavailable": "Профіль живлення" + }, + "tooltip": { + "action": "Клацніть для циклічної зміни профілю живлення", + "disabled": "Встановіть power-profiles-daemon для використання профілів живлення" + } + }, + "wifi": { + "label": { + "ethernet": "Ethernet", + "wifi": "Wi-Fi", + "disconnected": "Wi-Fi відключено" + }, + "tooltip": { + "action": "Клацніть для керування підключеннями Wi-Fi" + } + }, + "bluetooth": { + "label": { + "enabled": "Bluetooth", + "disabled": "Bluetooth" + }, + "tooltip": { + "action": "Клацніть для керування пристроями Bluetooth" + } + }, + "nightLight": { + "label": { + "enabled": "Нічне світло", + "forced": "Нічне світло", + "disabled": "Нічне світло" + }, + "tooltip": { + "action": "Клацніть для циклічної зміни режиму нічного світла\nПравий клік: Відкрити налаштування" + } + }, + "wallpaperSelector": { + "label": "Шпалери", + "tooltip": { + "action": "Лівий клік: Відкрити вибір шпалер\nПравий клік: Встановити випадкові шпалери" + } + }, + "keepAwake": { + "label": { + "enabled": "Не спати", + "disabled": "Не спати" + }, + "tooltip": { + "action": "Клацніть для перемикання режиму неспання" + } + } + }, + "toast": { + "night-light": { + "enabled": "Увімкнено", + "disabled": "Вимкнено", + "not-installed": "wlsunset не встановлено", + "forced": "Примусова активація", + "normal": "Звичайний режим" + }, + "keep-awake": { + "enabled": "Увімкнено", + "disabled": "Вимкнено" + }, + "wallpaper-colors": { + "enabled": "Кольори шпалер увімкнено", + "disabled": "Кольори шпалер вимкнено", + "not-installed": "Matugen не встановлено - потрібний для витягу кольорів зі шпалер" + }, + "matugen": { + "failed": "Помилка обробки Matugen", + "failed-general": "Matugen зіткнувся з помилкою під час обробки шаблонів" + }, + "recording": { + "stopping": "Зупинка запису…", + "started": "Запис розпочато", + "saved": "Запис збережено", + "failed-start": "Не вдалося розпочати запис", + "failed-gpu": "gpu-screen-recorder несподівано завершився.", + "failed-general": "Рекордер завершився з помилкою.", + "no-portals": "Портали робочого столу не запущені", + "no-portals-desc": "Запустіть xdg-desktop-portal та портал композитора (wlr/hyprland/gnome/kde).", + "not-installed": "gpu-screen-recorder не встановлено", + "not-installed-desc": "Будь ласка, встановіть gpu-screen-recorder для використання функцій запису екрана." + }, + "clipboard": { + "unavailable": "Історія буфера обміну недоступна", + "unavailable-desc": "Програма 'cliphist' не встановлена. Будь ласка, встановіть її для використання функцій історії буфера обміну." + }, + "ipc": { + "powerpanel-deprecated": "PowerPanel перейменовано на SessionMenu, цей виклик IPC незабаром буде застарілим. Будь ласка, використовуйте \"ipc call sessionMenu toggle\" замість цього.", + "sidepanel-deprecated": "SidePanel перейменовано на ControlCenter, цей виклик IPC незабаром буде застарілим. Будь ласка, використовуйте \"ipc call controlCenter toggle\" замість цього." + }, + "wifi": { + "enabled": "Увімкнено", + "disabled": "Вимкнено", + "connected": "Підключено до '{ssid}'", + "disconnected": "Відключено від '{ssid}'" + }, + "bluetooth": { + "enabled": "Увімкнено", + "disabled": "Вимкнено" + }, + "airplane-mode": { + "title": "Режим польоту", + "enabled": "Увімкнено", + "disabled": "Вимкнено" + }, + "internet": { + "limited": "Підключено без інтернету" + }, + "kofi": { + "opened": "Сторінка Ko-fi відкрита у вашому браузері" + }, + "do-not-disturb": { + "enabled": "'Не турбувати' увімкнено", + "disabled": "'Не турбувати' вимкнено", + "enabled-desc": "Ви знайдете ці сповіщення в своїй історії.", + "disabled-desc": "Показ усіх сповіщень." + }, + "power-profile": { + "changed": "Профіль живлення змінено", + "profile-name": "\"{profile}\"" + }, + "battery": { + "low": "Низький заряд батареї", + "low-desc": "Батарея на {percent}%. Будь ласка, підключіть зарядний пристрій." + }, + "battery-manager": { + "title": "Поріг батареї", + "set-success-desc": "Поріг батареї встановлено на {percent}%", + "initial-setup": "Потрібне початкове налаштування", + "set-failed": "Не вдалося встановити поріг батареї", + "install-success": "Встановлено успішно", + "install-missing": "Необхідні файли відсутні", + "install-unsupported": "Система не підтримується", + "install-failed": "Помилка встановлення", + "uninstall-setup": "Видалення, потрібна автентифікація", + "uninstall-success": "Видалено успішно", + "uninstall-failed": "Помилка видалення" + }, + "missing-control-center": { + "label": "Останній віджет Центру керування видалено", + "description": "Віджет Центру керування видалено з панелі. Щоб знову отримати до нього доступ з панелі, вам потрібно повторно додати віджет. Ви також можете відкрити його, клацнувши правою кнопкою миші на панелі." + } + }, + "weather": { + "clear-sky": "Ясне небо", + "mainly-clear": "Переважно ясно", + "partly-cloudy": "Частково хмарно", + "overcast": "Похмуро", + "fog": "Туман", + "drizzle": "Мряка", + "snow": "Сніг", + "rain-showers": "Дощові зливи", + "thunderstorm": "Гроза", + "unknown": "Невідомо" + }, + "authentication": { + "failed": "Помилка автентифікації", + "error": "Помилка автентифікації" + }, + "general": { + "no-results": "Немає результатів", + "no-summary": "Немає резюме", + "unknown": "Невідомо" + }, + "battery": { + "no-battery-detected": "Батарею не виявлено.", + "charging-rate": "Швидкість зарядки: {rate} Вт.", + "discharging-rate": "Швидкість розрядки: {rate} Вт.", + "charging": "Зарядка.", + "discharging": "Розрядка.", + "plugged-in": "Підключено.", + "idle": "Бездіяльність.", + "time-left": "Залишилось часу: {time}.", + "time-until-full": "Час до повного заряду: {time}.", + "health": "Здоров'я: {percent}%", + "panel": { + "title": "Поріг зарядки", + "full": "Повна ємність ({percent}%)", + "balanced": "Збалансований ({percent}%)", + "lifespan": "Подовжений термін служби ({percent}%)", + "disabled": "Менеджер батареї вимкнено" + } + }, + "setup": { + "customize": { + "header": "Налаштуйте ваш досвід", + "subheader": "Налаштуйте положення панелі, щільність, масштабування та інше." + }, + "appearance": { + "header": "Зовнішній вигляд", + "subheader": "Виберіть темний режим та джерела кольорів (Matugen або попередньо визначені)." + }, + "wallpaper": { + "header": "Виберіть ваші шпалери", + "subheader": "Створіть настрій за допомогою красивого фону.", + "select-prompt": "Виберіть шпалери нижче", + "preview-error": "Не вдалося завантажити зображення", + "none-in-dir": "Шпалери не знайдено в теці", + "no-dir": "Тека шпалер не вибрана", + "no-valid": "Не знайдено дійсних файлів зображень у: {dir}", + "choose-dir": "Виберіть теку, що містить ваші зображення шпалер", + "dir": { + "label": "Тека шпалер", + "description": "Виберіть теку, що містить ваші шпалери", + "browse": "Огляд теки шпалер", + "select-title": "Вибрати теку шпалер" + } + }, + "welcome": { + "note": "Лише кілька основних речей для початку - повні опції в Налаштуваннях" + } + } +} \ No newline at end of file From 36192717f457d21e68164ce3e8190e8c14dacb62 Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Mon, 3 Nov 2025 07:22:57 -0500 Subject: [PATCH 07/30] Shadows: Unified in NFullScreenWindow --- Modules/Bar/Bar.qml | 20 +-------------- Widgets/NFullScreenWindow.qml | 48 ++++++++++++++++++++++++----------- Widgets/NPanel.qml | 2 +- Widgets/NShapedRectangle.qml | 28 +++----------------- shell.qml | 36 +++++++++----------------- 5 files changed, 50 insertions(+), 84 deletions(-) diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 0de76baa..b182dd02 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -49,7 +49,7 @@ Item { sourceComponent: Item { anchors.fill: parent - // Background fill with shadow + // Background fill NShapedRectangle { id: bar @@ -80,24 +80,6 @@ Item { // No border on the bar borderWidth: 0 - // Shadow configuration - shadowEnabled: true - shadowBlur: 0.5 - // Fade shadow progressively when a panel is attached to the bar to avoid visual disconnection - // shadowOpacity: { - // if (PanelService.openedPanel && PanelService.openedPanel.attachedToBar) { - // // Fade shadow out as panel opens (animationProgress goes from 0 to 1) - // return 1.0 - PanelService.openedPanel.animationProgress - // } - // return 1.0 - // } - Behavior on shadowOpacity { - NumberAnimation { - duration: Style.animationFast - easing.type: Easing.OutCubic - } - } - MouseArea { anchors.fill: parent acceptedButtons: Qt.RightButton diff --git a/Widgets/NFullScreenWindow.qml b/Widgets/NFullScreenWindow.qml index e5302156..6c6737a3 100644 --- a/Widgets/NFullScreenWindow.qml +++ b/Widgets/NFullScreenWindow.qml @@ -15,6 +15,16 @@ PanelWindow { required property var barComponent required property var panelComponents + // Shadow properties + property bool shadowEnabled: true + property real shadowOpacity: 1.0 + property real shadowBlur: 1.0 + property real shadowHorizontalOffset: 2 + property real shadowVerticalOffset: 3 + + property color black: "#000000" + property color white: "#ffffff" + Component.onCompleted: { Logger.d("NFullScreenWindow", "Initialized for screen:", screen?.name, "- Dimensions:", screen?.width, "x", screen?.height, "- Position:", screen?.x, ",", screen?.y) } @@ -182,6 +192,23 @@ PanelWindow { width: root.width height: root.height + // Apply shadow effect + layer.enabled: root.shadowEnabled + layer.smooth: true + // layer.textureSize: { + // var dpr = CompositorService.getDisplayScale(screen.name) + // return Qt.size(width * dpr, height * dpr) + // } + layer.effect: MultiEffect { + shadowEnabled: root.shadowEnabled + shadowOpacity: root.shadowOpacity + shadowHorizontalOffset: root.shadowHorizontalOffset + shadowVerticalOffset: root.shadowVerticalOffset + shadowColor: black + blur: root.shadowBlur + blurMax: 32 + } + // Screen corners (integrated to avoid separate PanelWindow) // Always positioned at actual screen edges Loader { @@ -195,7 +222,7 @@ PanelWindow { id: cornersRoot anchors.fill: parent - property color cornerColor: Settings.data.general.forceBlackScreenCorners ? Qt.rgba(0, 0, 0, 1) : Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity) + property color cornerColor: Settings.data.general.forceBlackScreenCorners ? black : Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity) property real cornerRadius: Style.screenRadius property real cornerSize: Style.screenRadius @@ -220,7 +247,7 @@ PanelWindow { ctx.fillStyle = Qt.rgba(cornersRoot.cornerColor.r, cornersRoot.cornerColor.g, cornersRoot.cornerColor.b, cornersRoot.cornerColor.a) ctx.fillRect(0, 0, width, height) ctx.globalCompositeOperation = "destination-out" - ctx.fillStyle = "#ffffff" + ctx.fillStyle = white ctx.beginPath() ctx.arc(width, height, cornersRoot.cornerRadius, 0, 2 * Math.PI) ctx.fill() @@ -253,7 +280,7 @@ PanelWindow { ctx.fillStyle = Qt.rgba(cornersRoot.cornerColor.r, cornersRoot.cornerColor.g, cornersRoot.cornerColor.b, cornersRoot.cornerColor.a) ctx.fillRect(0, 0, width, height) ctx.globalCompositeOperation = "destination-out" - ctx.fillStyle = "#ffffff" + ctx.fillStyle = white ctx.beginPath() ctx.arc(0, height, cornersRoot.cornerRadius, 0, 2 * Math.PI) ctx.fill() @@ -286,7 +313,7 @@ PanelWindow { ctx.fillStyle = Qt.rgba(cornersRoot.cornerColor.r, cornersRoot.cornerColor.g, cornersRoot.cornerColor.b, cornersRoot.cornerColor.a) ctx.fillRect(0, 0, width, height) ctx.globalCompositeOperation = "destination-out" - ctx.fillStyle = "#ffffff" + ctx.fillStyle = white ctx.beginPath() ctx.arc(width, 0, cornersRoot.cornerRadius, 0, 2 * Math.PI) ctx.fill() @@ -319,7 +346,7 @@ PanelWindow { ctx.fillStyle = Qt.rgba(cornersRoot.cornerColor.r, cornersRoot.cornerColor.g, cornersRoot.cornerColor.b, cornersRoot.cornerColor.a) ctx.fillRect(0, 0, width, height) ctx.globalCompositeOperation = "destination-out" - ctx.fillStyle = "#ffffff" + ctx.fillStyle = white ctx.beginPath() ctx.arc(0, 0, cornersRoot.cornerRadius, 0, 2 * Math.PI) ctx.fill() @@ -411,14 +438,8 @@ PanelWindow { if (item) { // Set unique objectName per screen BEFORE registration: "calendarPanel-DP-1" item.objectName = panelId + "-" + (panelScreen?.name || "unknown") - - // Set z-order for panels - item.z = panelZIndex item.screen = panelScreen - - // Now register with PanelService (after objectName is set) PanelService.registerPanel(item) - Logger.d("NFullScreenWindow", "Panel loaded with objectName:", item.objectName, "on screen:", panelScreen?.name) } } @@ -439,14 +460,11 @@ PanelWindow { onLoaded: { Logger.d("NFullScreenWindow", "Bar loaded:", item !== null) if (item) { - Logger.d("NFullScreenWindow", "Bar size:", item.width, "x", item.height) - // Bar always has highest z-index - item.z = 100 + Logger.d("NFullScreenWindow", "Bar screen", item.screen?.name, "size:", item.width, "x", item.height) // Bind screen to bar component (use binding for reactivity) item.screen = Qt.binding(function () { return barLoader.screen }) - Logger.d("NFullScreenWindow", "Bar screen set to:", item.screen?.name) } } } diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 2f301f53..2a1c20ac 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -266,7 +266,7 @@ Item { } // Animation offset for slide effect on bar-attached panels - readonly property real slideOffset: root.attachedToBar ? (1 - root.animationProgress) * 20 : 0 + readonly property real slideOffset: root.attachedToBar ? (1 - root.animationProgress) * 40 : 0 // Position the panel using explicit x/y coordinates (no anchors) // This makes coordinates clearer for the click-through mask system diff --git a/Widgets/NShapedRectangle.qml b/Widgets/NShapedRectangle.qml index 9da7ebb4..5888af2a 100644 --- a/Widgets/NShapedRectangle.qml +++ b/Widgets/NShapedRectangle.qml @@ -30,13 +30,6 @@ Item { property color borderColor: Color.mOutline property int borderWidth: Style.borderS - // Shadow properties - property bool shadowEnabled: true - property real shadowOpacity: 1.0 // 0.0 to 1.0 - property real shadowBlur: 0.9 - property real shadowHorizontalOffset: 3 - property real shadowVerticalOffset: 3 - // Check if any corner is inverted readonly property bool hasInvertedCorners: topLeftInverted || topRightInverted || bottomLeftInverted || bottomRightInverted @@ -46,26 +39,12 @@ Item { readonly property real leftPadding: Math.max((topLeftInverted && topLeftInvertedDirection === "horizontal") ? topLeftRadius : 0, (bottomLeftInverted && bottomLeftInvertedDirection === "horizontal") ? bottomLeftRadius : 0) readonly property real rightPadding: Math.max((topRightInverted && topRightInvertedDirection === "horizontal") ? topRightRadius : 0, (bottomRightInverted && bottomRightInvertedDirection === "horizontal") ? bottomRightRadius : 0) - // Background layer: shape with shadow effects (layer.enabled) + // Background layer Item { - id: shadowLayer + id: backgroundLayer anchors.fill: parent z: 0 - // Apply shadow effect to this layer only - layer.enabled: root.shadowEnabled - layer.smooth: true - // layer.textureSize: Qt.size(width * Screen.devicePixelRatio, height * Screen.devicePixelRatio) - layer.effect: MultiEffect { - shadowEnabled: root.shadowEnabled - shadowOpacity: root.shadowOpacity - shadowColor: "#000000" - shadowHorizontalOffset: root.shadowHorizontalOffset - shadowVerticalOffset: root.shadowVerticalOffset - blur: root.shadowBlur - blurMax: 32 - } - // Simple rectangle for non-inverted corners (better performance) Rectangle { id: simpleBackground @@ -205,8 +184,7 @@ Item { } } - // Content layer: for child elements (NO layer effects - keeps text sharp) - // Child components can be added here and will render on top without blur + // Content layer: for child elements default property alias contentChildren: contentLayer.data Item { id: contentLayer diff --git a/shell.qml b/shell.qml index e8513daf..fa6451f8 100644 --- a/shell.qml +++ b/shell.qml @@ -233,52 +233,40 @@ ShellRoot { // Register all panel components panelComponents: [{ "id": "launcherPanel", - "component": launcherComponent, - "zIndex": 50 + "component": launcherComponent }, { "id": "controlCenterPanel", - "component": controlCenterComponent, - "zIndex": 50 + "component": controlCenterComponent }, { "id": "calendarPanel", - "component": calendarComponent, - "zIndex": 50 + "component": calendarComponent }, { "id": "settingsPanel", - "component": settingsComponent, - "zIndex": 50 + "component": settingsComponent }, { "id": "directWidgetSettingsPanel", - "component": directWidgetSettingsComponent, - "zIndex": 50 + "component": directWidgetSettingsComponent }, { "id": "notificationHistoryPanel", - "component": notificationHistoryComponent, - "zIndex": 50 + "component": notificationHistoryComponent }, { "id": "sessionMenuPanel", - "component": sessionMenuComponent, - "zIndex": 50 + "component": sessionMenuComponent }, { "id": "wifiPanel", - "component": wifiComponent, - "zIndex": 50 + "component": wifiComponent }, { "id": "bluetoothPanel", - "component": bluetoothComponent, - "zIndex": 50 + "component": bluetoothComponent }, { "id": "audioPanel", - "component": audioComponent, - "zIndex": 50 + "component": audioComponent }, { "id": "wallpaperPanel", - "component": wallpaperComponent, - "zIndex": 50 + "component": wallpaperComponent }, { "id": "batteryPanel", - "component": batteryComponent, - "zIndex": 50 + "component": batteryComponent }] // Bar component From 72c5a9d6524faf2ae8297ec2f581e06254993723 Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Mon, 3 Nov 2025 08:01:02 -0500 Subject: [PATCH 08/30] ShapedRect: fixing hair line gap --- Widgets/NShapedRectangle.qml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Widgets/NShapedRectangle.qml b/Widgets/NShapedRectangle.qml index 5888af2a..309d53e7 100644 --- a/Widgets/NShapedRectangle.qml +++ b/Widgets/NShapedRectangle.qml @@ -147,11 +147,9 @@ Item { ctx.quadraticCurveTo(x, y + h, x, y + h - bottomLeftRadius) } else { // Curves downward - ctx.lineTo(x, y + h) + ctx.lineTo(x + bottomLeftRadius, y + h) + ctx.quadraticCurveTo(x, y + h, x, y + h + bottomLeftRadius) ctx.lineTo(x, y + h + bottomLeftRadius) - ctx.quadraticCurveTo(x, y + h, x + bottomLeftRadius, y + h) - ctx.lineTo(x, y + h) - ctx.lineTo(x, y + h - bottomLeftRadius) } } else { ctx.lineTo(x + bottomLeftRadius, y + h) From f4672df2e00a39486cc4188cd2eb960efde38420 Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Mon, 3 Nov 2025 08:27:49 -0500 Subject: [PATCH 09/30] ControlCenter: Fix laggy opening due to spinning disc --- Modules/ControlCenter/Cards/MediaCard.qml | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/Modules/ControlCenter/Cards/MediaCard.qml b/Modules/ControlCenter/Cards/MediaCard.qml index 0138a859..d8725fb2 100644 --- a/Modules/ControlCenter/Cards/MediaCard.qml +++ b/Modules/ControlCenter/Cards/MediaCard.qml @@ -62,7 +62,6 @@ NBox { border.width: 1 radius: Style.radiusM } - //} // Background visualizer on top of the artwork Loader { @@ -248,7 +247,7 @@ NBox { duration: index * 600 } NumberAnimation { - from: 0.5 + from: 0.6 to: 1.2 duration: 2000 easing.type: Easing.OutQuad @@ -263,14 +262,6 @@ NBox { icon: "disc" pointSize: Style.fontSizeXXXL * 3 color: Color.mOnSurfaceVariant - - RotationAnimator on rotation { - from: 0 - to: 360 - duration: 8000 - loops: Animation.Infinite - running: true - } } } From 2c3d7bc101672f2de91cd5f374a3fa0e43279e48 Mon Sep 17 00:00:00 2001 From: c2fc2f Date: Thu, 30 Oct 2025 19:21:12 +0100 Subject: [PATCH 10/30] fix: rename pkgs.system into pkgs.stdenv.hostPlatform.system --- flake.nix | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/flake.nix b/flake.nix index 5f985ddd..5fba4116 100644 --- a/flake.nix +++ b/flake.nix @@ -52,10 +52,12 @@ ... }: { imports = [./nix/home-module.nix]; - programs.noctalia-shell.package = lib.mkDefault self.packages.${pkgs.system}.default; + programs.noctalia-shell.package = + lib.mkDefault + self.packages.${pkgs.stdenv.hostPlatform.system}.default; programs.noctalia-shell.app2unit.package = lib.mkDefault - nixpkgs.legacyPackages.${pkgs.system}.app2unit; + nixpkgs.legacyPackages.${pkgs.stdenv.hostPlatform.system}.app2unit; }; nixosModules.default = { @@ -64,7 +66,9 @@ ... }: { imports = [./nix/nixos-module.nix]; - services.noctalia-shell.package = lib.mkDefault self.packages.${pkgs.system}.default; + services.noctalia-shell.package = + lib.mkDefault + self.packages.${pkgs.stdenv.hostPlatform.system}.default; }; }; } From fea06c2164ef863b772f6903b7633bcc40e9835e Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Mon, 3 Nov 2025 10:29:49 -0500 Subject: [PATCH 11/30] Shadows: conditional via settings q --- Assets/Translations/de.json | 4 ++++ Assets/Translations/en.json | 4 ++++ Assets/Translations/es.json | 4 ++++ Assets/Translations/fr.json | 4 ++++ Assets/Translations/pt.json | 4 ++++ Assets/Translations/zh-CN.json | 4 ++++ Assets/settings-default.json | 23 +++++++++++------- Commons/Settings.qml | 28 ++++++++++++---------- Modules/Settings/Tabs/UserInterfaceTab.qml | 7 ++++++ Widgets/NFullScreenWindow.qml | 7 +++--- Widgets/NPanel.qml | 23 ++++++++++++++---- 11 files changed, 81 insertions(+), 31 deletions(-) diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index 4949b25f..c7df24cf 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -884,6 +884,10 @@ "dim-desktop": { "description": "Den Desktop abdunkeln, wenn Fenster oder Menüs geöffnet sind.", "label": "Dimmer Schreibtisch" + }, + "shadows": { + "description": "Aktiviert Schlagschatten unter Balken und Panels.", + "label": "Schlagschatten" } }, "lock-screen": { diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index 8bee2a80..66f7f28b 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -859,6 +859,10 @@ "description": "Changes the size of the general user interface, excluding the bar.", "reset-scaling": "Reset interface scaling" }, + "shadows": { + "label": "Drop shadows", + "description": "Enables drop shadows under bars and panels." + }, "dim-desktop": { "label": "Dim desktop", "description": "Dim the desktop when panels or menus are open." diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index e4eb12b6..a6344bf9 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -884,6 +884,10 @@ "dim-desktop": { "description": "Atenuar el escritorio cuando los paneles o menús estén abiertos.", "label": "Dim escritorio" + }, + "shadows": { + "description": "Habilita sombras paralelas debajo de las barras y los paneles.", + "label": "Sombras paralelas" } }, "lock-screen": { diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index cadc7836..5e741f0a 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -884,6 +884,10 @@ "dim-desktop": { "description": "Atténuer le bureau lorsque des panneaux ou des menus sont ouverts.", "label": "Dim bureau" + }, + "shadows": { + "description": "Active les ombres portées sous les barres et les panneaux.", + "label": "Ombres portées" } }, "lock-screen": { diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index 41ca38e0..06021582 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -884,6 +884,10 @@ "dim-desktop": { "description": "Escurecer a área de trabalho quando painéis ou menus estiverem abertos.", "label": "Dim área de trabalho" + }, + "shadows": { + "description": "Ativa sombras projetadas sob barras e painéis.", + "label": "Sombras projetadas" } }, "lock-screen": { diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index 11a1b176..518e3ea2 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -884,6 +884,10 @@ "dim-desktop": { "description": "当面板或菜单打开时,桌面变暗。", "label": "昏暗的桌面" + }, + "shadows": { + "description": "启用条形图和面板下的阴影。", + "label": "阴影" } }, "lock-screen": { diff --git a/Assets/settings-default.json b/Assets/settings-default.json index 60096117..2c1e418a 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -10,6 +10,8 @@ "floating": false, "marginVertical": 0.25, "marginHorizontal": 0.25, + "outerCorners": true, + "exclusive": true, "widgets": { "left": [ { @@ -57,6 +59,7 @@ }, "general": { "avatarImage": "", + "dimDesktop": true, "showScreenCorners": false, "forceBlackScreenCorners": false, "scaleRatio": 1, @@ -66,8 +69,18 @@ "animationDisabled": false, "compactLockScreen": false, "lockOnSuspend": true, + "enableShadows": true, "language": "" }, + "ui": { + "fontDefault": "Roboto", + "fontFixed": "DejaVu Sans Mono", + "fontDefaultScale": 1, + "fontFixedScale": 1, + "tooltipsEnabled": true, + "panelsAttachedToBar": true, + "panelsOverlayLayer": true + }, "location": { "name": "Tokyo", "weatherEnabled": true, @@ -212,15 +225,6 @@ "mprisBlacklist": [], "preferredPlayer": "" }, - "ui": { - "fontDefault": "Roboto", - "fontFixed": "DejaVu Sans Mono", - "fontDefaultScale": 1, - "fontFixedScale": 1, - "tooltipsEnabled": true, - "panelsAttachedToBar": true, - "panelsOverlayLayer": true - }, "brightness": { "brightnessStep": 5, "enforceMinimum": true @@ -239,6 +243,7 @@ "gtk": false, "qt": false, "kcolorscheme": false, + "alacritty": false, "kitty": false, "ghostty": false, "foot": false, diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 105b6df4..b445ba61 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -54,8 +54,9 @@ Singleton { // This should only be activated once when the settings structure has changed // Then it should be commented out again, regular users don't need to generate // default settings on every start - // TODO: automate this someday! - //generateDefaultSettings() + if (isDebug) { + generateDefaultSettings() + } // Patch-in the local default, resolved to user's home adapter.general.avatarImage = defaultAvatar @@ -198,9 +199,21 @@ Singleton { property bool animationDisabled: false property bool compactLockScreen: false property bool lockOnSuspend: true + property bool enableShadows: true property string language: "" } + // ui + property JsonObject ui: JsonObject { + property string fontDefault: "Roboto" + property string fontFixed: "DejaVu Sans Mono" + property real fontDefaultScale: 1.0 + property real fontFixedScale: 1.0 + property bool tooltipsEnabled: true + property bool panelsAttachedToBar: true + property bool panelsOverlayLayer: true + } + // location property JsonObject location: JsonObject { property string name: defaultLocation @@ -353,17 +366,6 @@ Singleton { property string preferredPlayer: "" } - // ui - property JsonObject ui: JsonObject { - property string fontDefault: "Roboto" - property string fontFixed: "DejaVu Sans Mono" - property real fontDefaultScale: 1.0 - property real fontFixedScale: 1.0 - property bool tooltipsEnabled: true - property bool panelsAttachedToBar: true - property bool panelsOverlayLayer: true - } - // brightness property JsonObject brightness: JsonObject { property int brightnessStep: 5 diff --git a/Modules/Settings/Tabs/UserInterfaceTab.qml b/Modules/Settings/Tabs/UserInterfaceTab.qml index a466c7ec..f93c2e8e 100644 --- a/Modules/Settings/Tabs/UserInterfaceTab.qml +++ b/Modules/Settings/Tabs/UserInterfaceTab.qml @@ -40,6 +40,13 @@ ColumnLayout { onToggled: checked => Settings.data.ui.panelsAttachedToBar = checked } + NToggle { + label: I18n.tr("settings.user-interface.shadows.label") + description: I18n.tr("settings.user-interface.shadows.description") + checked: Settings.data.general.enableShadows + onToggled: checked => Settings.data.general.enableShadows = checked + } + NToggle { label: I18n.tr("settings.user-interface.panels-overlay.label") description: I18n.tr("settings.user-interface.panels-overlay.description") diff --git a/Widgets/NFullScreenWindow.qml b/Widgets/NFullScreenWindow.qml index 6c6737a3..911916c1 100644 --- a/Widgets/NFullScreenWindow.qml +++ b/Widgets/NFullScreenWindow.qml @@ -16,7 +16,6 @@ PanelWindow { required property var panelComponents // Shadow properties - property bool shadowEnabled: true property real shadowOpacity: 1.0 property real shadowBlur: 1.0 property real shadowHorizontalOffset: 2 @@ -193,14 +192,14 @@ PanelWindow { height: root.height // Apply shadow effect - layer.enabled: root.shadowEnabled + layer.enabled: Settings.data.general.enableShadows layer.smooth: true // layer.textureSize: { // var dpr = CompositorService.getDisplayScale(screen.name) // return Qt.size(width * dpr, height * dpr) // } layer.effect: MultiEffect { - shadowEnabled: root.shadowEnabled + shadowEnabled: true shadowOpacity: root.shadowOpacity shadowHorizontalOffset: root.shadowHorizontalOffset shadowVerticalOffset: root.shadowVerticalOffset @@ -213,7 +212,7 @@ PanelWindow { // Always positioned at actual screen edges Loader { id: screenCornersLoader - active: Settings.data.general.showScreenCorners && (!Settings.data.ui.panelsAttachedToBar || Settings.data.bar.backgroundOpacity >= 1 || Settings.data.bar.floating) + active: Settings.data.general.showScreenCorners anchors.fill: parent z: 1000 // Very high z-index to be on top of everything diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 2a1c20ac..a78ae508 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -52,6 +52,7 @@ Item { // Animation properties property real animationProgress: 0 + property bool isClosing: false // Keyboard event handlers - override these in specific panels to handle shortcuts // These are called from NFullScreenWindow's centralized shortcuts @@ -76,6 +77,13 @@ Item { NumberAnimation { duration: Style.animationFast easing.type: Easing.OutCubic + onRunningChanged: { + // When close animation finishes, actually hide the panel + if (!running && root.isClosing) { + root.isClosing = false + root.isPanelOpen = false + } + } } } @@ -106,7 +114,8 @@ Item { signal closed // Panel visibility and sizing - visible: isPanelOpen + // Keep visible during close animation + visible: isPanelOpen || isClosing width: parent ? parent.width : 0 height: parent ? parent.height : 0 @@ -156,15 +165,18 @@ Item { } function close() { - isPanelOpen = false + // Start close animation + isClosing = true animationProgress = 0 - // Notify PanelService + // Notify PanelService immediately PanelService.closedPanel(root) + // Emit closed signal closed() - Logger.d("NPanel", "Closed panel", objectName) + Logger.d("NPanel", "Closing panel with animation", objectName) + // isPanelOpen will be set to false when animation completes } function setPosition() {// Position calculation will be handled here @@ -175,7 +187,8 @@ Item { Loader { id: panelContentContainer anchors.fill: parent - active: root.isPanelOpen + // Keep active during close animation + active: root.isPanelOpen || root.isClosing asynchronous: false sourceComponent: Item { From e11934294f7352fc7c3bbe2693aac163b6410c07 Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Mon, 3 Nov 2025 10:46:29 -0500 Subject: [PATCH 12/30] FullScreenWindow: fix click-through but when panel closes. --- Widgets/NFullScreenWindow.qml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Widgets/NFullScreenWindow.qml b/Widgets/NFullScreenWindow.qml index 911916c1..7c3bb1ec 100644 --- a/Widgets/NFullScreenWindow.qml +++ b/Widgets/NFullScreenWindow.qml @@ -82,11 +82,12 @@ PanelWindow { } // Add regions for each open panel + // Only include panels that are open AND not closing (to allow click-through during close animation) for (var i = 0; i < panelMaskRepeater.count; i++) { var wrapperItem = panelMaskRepeater.itemAt(i) if (wrapperItem && wrapperItem.maskRegion) { var panelItem = wrapperItem.panelItem - if (panelItem && panelItem.isPanelOpen) { + if (panelItem && panelItem.isPanelOpen && !panelItem.isClosing) { var panelRegion = panelItem.panelRegion // Update the mask region's coordinates from the panel's actual region if (panelRegion) { @@ -111,7 +112,8 @@ PanelWindow { root.updateMask() } function onDidClose() { - root.updateMask() + // Delay mask update to ensure panel's isPanelOpen is updated first + Qt.callLater(() => root.updateMask()) } } @@ -156,6 +158,10 @@ PanelWindow { width: barRegion ? barRegion.width : 0 height: barRegion ? barRegion.height : 0 intersection: Intersection.Subtract + + // Update mask when bar geometry changes + onWidthChanged: Qt.callLater(() => root.updateMask()) + onHeightChanged: Qt.callLater(() => root.updateMask()) } } From 451784a82b2e5f5c8f3b62a069ea7c4d1479b64e Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Mon, 3 Nov 2025 10:50:47 -0500 Subject: [PATCH 13/30] FullScreenWindow: restored panelOverlayLayer functionality, since refactoring it means assigning both panels AND bar to the overlay layer as they share a PanelWindow. --- Assets/settings-default.json | 2 +- Commons/Settings.qml | 2 +- Widgets/NFullScreenWindow.qml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Assets/settings-default.json b/Assets/settings-default.json index 2c1e418a..296faf69 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -79,7 +79,7 @@ "fontFixedScale": 1, "tooltipsEnabled": true, "panelsAttachedToBar": true, - "panelsOverlayLayer": true + "panelsOverlayLayer": false }, "location": { "name": "Tokyo", diff --git a/Commons/Settings.qml b/Commons/Settings.qml index b445ba61..6b2c0766 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -211,7 +211,7 @@ Singleton { property real fontFixedScale: 1.0 property bool tooltipsEnabled: true property bool panelsAttachedToBar: true - property bool panelsOverlayLayer: true + property bool panelsOverlayLayer: false } // location diff --git a/Widgets/NFullScreenWindow.qml b/Widgets/NFullScreenWindow.qml index 7c3bb1ec..3dab0798 100644 --- a/Widgets/NFullScreenWindow.qml +++ b/Widgets/NFullScreenWindow.qml @@ -44,7 +44,7 @@ PanelWindow { // This ensures all keyboard shortcuts work reliably (Escape, etc.) // The centralized shortcuts in this window handle delegation to panels WlrLayershell.keyboardFocus: root.isPanelOpen ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None - WlrLayershell.layer: WlrLayer.Top + WlrLayershell.layer: Settings.data.ui.panelsOverlayLayer ? WlrLayer.Overlay : WlrLayer.Top WlrLayershell.namespace: "noctalia-screen-" + (screen?.name || "unknown") anchors { From fceaac029c5393071714cb8adc6bf4d1d10336d1 Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Mon, 3 Nov 2025 11:40:18 -0500 Subject: [PATCH 14/30] Attached panels: nice curves on screen edges --- Widgets/NFullScreenWindow.qml | 6 --- Widgets/NPanel.qml | 82 ++++++++++++++++++++++++++++------- 2 files changed, 67 insertions(+), 21 deletions(-) diff --git a/Widgets/NFullScreenWindow.qml b/Widgets/NFullScreenWindow.qml index 3dab0798..960e5a89 100644 --- a/Widgets/NFullScreenWindow.qml +++ b/Widgets/NFullScreenWindow.qml @@ -117,12 +117,6 @@ PanelWindow { } } - // Also update mask when isPanelOpen changes (defensive) - onIsPanelOpenChanged: { - Logger.d("NFullScreenWindow", "isPanelOpen changed to:", isPanelOpen) - Qt.callLater(() => root.updateMask()) - } - // Background region - for closing panels when clicking outside (separate from mask) Region { id: backgroundMaskRegion diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index a78ae508..d8480a06 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -236,18 +236,64 @@ Item { // Inverted corners based on bar attachment // When attached to bar AND effectively anchored to it, the corner(s) touching the bar should be inverted - topLeftInverted: root.attachedToBar && ((root.barPosition === "top" && !root.barIsVertical && root.effectivePanelAnchorTop) || (root.barPosition === "left" && root.barIsVertical && root.effectivePanelAnchorLeft)) - topRightInverted: root.attachedToBar && ((root.barPosition === "top" && !root.barIsVertical && root.effectivePanelAnchorTop) || (root.barPosition === "right" && root.barIsVertical && root.effectivePanelAnchorRight)) - bottomLeftInverted: root.attachedToBar && ((root.barPosition === "bottom" && !root.barIsVertical && root.effectivePanelAnchorBottom) || (root.barPosition === "left" && root.barIsVertical && root.effectivePanelAnchorLeft)) - bottomRightInverted: root.attachedToBar && ((root.barPosition === "bottom" && !root.barIsVertical && root.effectivePanelAnchorBottom) || (root.barPosition === "right" && root.barIsVertical && root.effectivePanelAnchorRight)) + // Also invert corners when touching screen edges (non-floating bar only) + topLeftInverted: { + // Bar attachment + var barInverted = root.attachedToBar && ((root.barPosition === "top" && !root.barIsVertical && root.effectivePanelAnchorTop) || (root.barPosition === "left" && root.barIsVertical && root.effectivePanelAnchorLeft)) + // Edge contact: top-left corner inverts when touching left edge (for horizontal bars) or top edge (for vertical bars) + var edgeInverted = (touchingLeftEdge && !root.barIsVertical) || (touchingTopEdge && root.barIsVertical) + return barInverted || edgeInverted + } + topRightInverted: { + var barInverted = root.attachedToBar && ((root.barPosition === "top" && !root.barIsVertical && root.effectivePanelAnchorTop) || (root.barPosition === "right" && root.barIsVertical && root.effectivePanelAnchorRight)) + // Edge contact: top-right corner inverts when touching right edge (for horizontal bars) or top edge (for vertical bars) + var edgeInverted = (touchingRightEdge && !root.barIsVertical) || (touchingTopEdge && root.barIsVertical) + return barInverted || edgeInverted + } + bottomLeftInverted: { + var barInverted = root.attachedToBar && ((root.barPosition === "bottom" && !root.barIsVertical && root.effectivePanelAnchorBottom) || (root.barPosition === "left" && root.barIsVertical && root.effectivePanelAnchorLeft)) + // Edge contact: bottom-left corner inverts when touching left edge (for horizontal bars) or bottom edge (for vertical bars) + var edgeInverted = (touchingLeftEdge && !root.barIsVertical) || (touchingBottomEdge && root.barIsVertical) + return barInverted || edgeInverted + } + bottomRightInverted: { + var barInverted = root.attachedToBar && ((root.barPosition === "bottom" && !root.barIsVertical && root.effectivePanelAnchorBottom) || (root.barPosition === "right" && root.barIsVertical && root.effectivePanelAnchorRight)) + // Edge contact: bottom-right corner inverts when touching right edge (for horizontal bars) or bottom edge (for vertical bars) + var edgeInverted = (touchingRightEdge && !root.barIsVertical) || (touchingBottomEdge && root.barIsVertical) + return barInverted || edgeInverted + } - // Set inverted corner direction based on which edge touches the bar - // For horizontal bars (top/bottom): left/right edges touch bar → horizontal curves - // For vertical bars (left/right): top/bottom edges touch bar → vertical curves - topLeftInvertedDirection: root.barIsVertical ? "vertical" : "horizontal" - topRightInvertedDirection: root.barIsVertical ? "vertical" : "horizontal" - bottomLeftInvertedDirection: root.barIsVertical ? "vertical" : "horizontal" - bottomRightInvertedDirection: root.barIsVertical ? "vertical" : "horizontal" + // Set inverted corner direction based on which edge touches + // Bar edges: horizontal bars → horizontal curves, vertical bars → vertical curves + // Screen edges: opposite - left/right edges → vertical curves, top/bottom edges → horizontal curves + topLeftInvertedDirection: { + if (touchingLeftEdge && !root.barIsVertical) + return "vertical" + if (touchingTopEdge && root.barIsVertical) + return "horizontal" + return root.barIsVertical ? "vertical" : "horizontal" + } + topRightInvertedDirection: { + if (touchingRightEdge && !root.barIsVertical) + return "vertical" + if (touchingTopEdge && root.barIsVertical) + return "horizontal" + return root.barIsVertical ? "vertical" : "horizontal" + } + bottomLeftInvertedDirection: { + if (touchingLeftEdge && !root.barIsVertical) + return "vertical" + if (touchingBottomEdge && root.barIsVertical) + return "horizontal" + return root.barIsVertical ? "vertical" : "horizontal" + } + bottomRightInvertedDirection: { + if (touchingRightEdge && !root.barIsVertical) + return "vertical" + if (touchingBottomEdge && root.barIsVertical) + return "horizontal" + return root.barIsVertical ? "vertical" : "horizontal" + } width: { var w // Priority 1: Content-driven size (dynamic) @@ -281,6 +327,12 @@ Item { // Animation offset for slide effect on bar-attached panels readonly property real slideOffset: root.attachedToBar ? (1 - root.animationProgress) * 40 : 0 + // Detect if panel is touching screen edges (only when bar is not floating) + readonly property bool touchingLeftEdge: !root.barFloating && root.attachedToBar && x <= (root.barMarginH + 1) + readonly property bool touchingRightEdge: !root.barFloating && root.attachedToBar && (x + width) >= (parent.width - root.barMarginH - 1) + readonly property bool touchingTopEdge: !root.barFloating && root.attachedToBar && y <= (root.barMarginV + 1) + readonly property bool touchingBottomEdge: !root.barFloating && root.attachedToBar && (y + height) >= (parent.height - root.barMarginV - 1) + // Position the panel using explicit x/y coordinates (no anchors) // This makes coordinates clearer for the click-through mask system x: { @@ -322,8 +374,8 @@ Item { // When attached, panel should not extend beyond bar edges if (root.attachedToBar) { // Inverted corners with horizontal direction extend left/right by radiusL - // When bar is floating, it also has rounded corners, so we need extra inset - var cornerInset = Style.radiusL + (root.barFloating ? Style.radiusL : 0) + // When bar is floating, it also has rounded corners, so we need extra insets + var cornerInset = root.barFloating ? Style.radiusL * 2 : 0 var barLeftEdge = root.barMarginH + cornerInset var barRightEdge = parent.width - root.barMarginH - cornerInset panelX = Math.max(barLeftEdge, Math.min(panelX, barRightEdge - width)) @@ -410,7 +462,7 @@ Item { // For vertical bars, center panel on button Y position var panelY = root.buttonPosition.y + root.buttonHeight / 2 - height / 2 // Clamp to bar bounds (account for floating bar margins and inverted corners) - var extraPadding = root.attachedToBar ? Style.radiusL : 0 + var extraPadding = (root.attachedToBar && root.barFloating) ? Style.radiusL : 0 if (root.attachedToBar) { // When attached, panel should not extend beyond bar edges (accounting for floating margins) // Inverted corners with vertical direction extend up/down by radiusL @@ -472,7 +524,7 @@ Item { // For vertical bars: center vertically on bar if (root.attachedToBar) { // When attached, respect bar bounds - var cornerInset = Style.radiusL + (root.barFloating ? Style.radiusL : 0) + var cornerInset = root.barFloating ? Style.radiusL * 2 : 0 var barTopEdge = root.barMarginV + cornerInset var barBottomEdge = parent.height - root.barMarginV - cornerInset var centeredY = (parent.height - height) / 2 From e4bb28dd5ea2dc9a22d9be364c05ded67ba3d0e3 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Mon, 3 Nov 2025 18:00:06 +0100 Subject: [PATCH 15/30] SetupWizard: add dimdesktop & dropshadow option --- Modules/Settings/Tabs/GeneralTab.qml | 16 +++- Modules/SetupWizard/SetupCustomizeStep.qml | 102 +++++++++++++++++++++ shell.qml | 38 +++++--- 3 files changed, 139 insertions(+), 17 deletions(-) diff --git a/Modules/Settings/Tabs/GeneralTab.qml b/Modules/Settings/Tabs/GeneralTab.qml index e01bb567..ca1c5ba0 100644 --- a/Modules/Settings/Tabs/GeneralTab.qml +++ b/Modules/Settings/Tabs/GeneralTab.qml @@ -235,8 +235,20 @@ ColumnLayout { visible: !DistroService.isNixOS text: I18n.tr("settings.general.launch-setup-wizard") onClicked: { - setupWizardLoader.active = false - setupWizardLoader.active = true + var targetScreen = PanelService.openedPanel ? PanelService.openedPanel.screen : (Quickshell.screens.length > 0 ? Quickshell.screens[0] : null) + if (!targetScreen) { + return + } + var setupPanel = PanelService.getPanel("setupWizardPanel", targetScreen) + if (setupPanel) { + setupPanel.open() + } else { + Qt.callLater(() => { + var sp = PanelService.getPanel("setupWizardPanel", targetScreen) + if (sp) + sp.open() + }) + } } } } diff --git a/Modules/SetupWizard/SetupCustomizeStep.qml b/Modules/SetupWizard/SetupCustomizeStep.qml index 0c9f5853..52da1576 100644 --- a/Modules/SetupWizard/SetupCustomizeStep.qml +++ b/Modules/SetupWizard/SetupCustomizeStep.qml @@ -442,6 +442,108 @@ ColumnLayout { } } + // Divider + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Color.mOutline + opacity: 0.2 + Layout.topMargin: Style.marginS + Layout.bottomMargin: Style.marginS + } + + // Dim Desktop toggle + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + Rectangle { + width: 32 + height: 32 + radius: Style.radiusM + color: Color.mSurface + NIcon { + icon: "screen-share" + pointSize: Style.fontSizeL + color: Color.mPrimary + anchors.centerIn: parent + } + } + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + NText { + text: I18n.tr("settings.user-interface.dim-desktop.label") + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + NText { + text: I18n.tr("settings.user-interface.dim-desktop.description") + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + NToggle { + checked: Settings.data.general.dimDesktop + onToggled: function (checked) { + Settings.data.general.dimDesktop = checked + } + } + } + + // Divider + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Color.mOutline + opacity: 0.2 + Layout.topMargin: Style.marginS + Layout.bottomMargin: Style.marginS + } + + // Drop Shadows toggle + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + Rectangle { + width: 32 + height: 32 + radius: Style.radiusM + color: Color.mSurface + NIcon { + icon: "shadow" + pointSize: Style.fontSizeL + color: Color.mPrimary + anchors.centerIn: parent + } + } + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + NText { + text: I18n.tr("settings.user-interface.shadows.label") + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + NText { + text: I18n.tr("settings.user-interface.shadows.description") + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + NToggle { + checked: Settings.data.general.enableShadows + onToggled: function (checked) { + Settings.data.general.enableShadows = checked + } + } + } + Item { Layout.fillWidth: true Layout.preferredHeight: Style.marginL diff --git a/shell.qml b/shell.qml index fa6451f8..6e739668 100644 --- a/shell.qml +++ b/shell.qml @@ -137,6 +137,11 @@ ShellRoot { BatteryPanel {} } + Component { + id: setupWizardComponent + SetupWizard {} + } + Component { id: barComp Bar {} @@ -267,6 +272,9 @@ ShellRoot { }, { "id": "batteryPanel", "component": batteryComponent + }, { + "id": "setupWizardPanel", + "component": setupWizardComponent }] // Bar component @@ -291,20 +299,6 @@ ShellRoot { } } - // ------------------------------ - // Setup Wizard - Loader { - id: setupWizardLoader - active: false - asynchronous: true - sourceComponent: SetupWizard {} - onLoaded: { - if (setupWizardLoader.item && setupWizardLoader.item.open) { - setupWizardLoader.item.open() - } - } - } - Connections { target: Settings function onSettingsLoaded() { @@ -329,7 +323,21 @@ ShellRoot { } if (Settings.data.settingsVersion >= Settings.settingsVersion) { - setupWizardLoader.active = true + // Open Setup Wizard as a panel in the same windowing system as Settings/ControlCenter + if (Quickshell.screens.length > 0) { + var targetScreen = Quickshell.screens[0] + var setupPanel = PanelService.getPanel("setupWizardPanel", targetScreen) + if (setupPanel) { + setupPanel.open() + } else { + // If not yet loaded, ensure it loads and try again shortly + Qt.callLater(() => { + var sp = PanelService.getPanel("setupWizardPanel", targetScreen) + if (sp) + sp.open() + }) + } + } } else { Settings.data.setupCompleted = true } From 3ad7271871d39ec82c8ad9a6580b56698bf8a838 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Mon, 3 Nov 2025 18:21:37 +0100 Subject: [PATCH 16/30] DisplayTab: toggle ddcutil, disabled by default --- Assets/Translations/de.json | 8 +++++++ Assets/Translations/en.json | 8 +++++++ Assets/Translations/es.json | 8 +++++++ Assets/Translations/fr.json | 8 +++++++ Assets/Translations/pt.json | 8 +++++++ Assets/Translations/zh-CN.json | 8 +++++++ Assets/settings-default.json | 3 ++- Commons/Settings.qml | 1 + Modules/Settings/Tabs/DisplayTab.qml | 35 +++++++++++++++++++++++++--- Services/BrightnessService.qml | 35 +++++++++++++++++++++++++--- 10 files changed, 115 insertions(+), 7 deletions(-) diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index c7df24cf..8d320903 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -166,6 +166,14 @@ "enforce-minimum": { "label": "Mindesthelligkeit erzwingen (1%)", "description": "Löst das Problem, dass die Hintergrundbeleuchtung auf einigen Displays bei 0% Helligkeit vollständig ausgeschaltet wird." + }, + "external-brightness": { + "label": "Externe Helligkeitsunterstützung", + "description": "DDCUtil-Unterstützung zum Steuern der Helligkeit externer Displays über das DDC/CI-Protokoll aktivieren." + }, + "brightness-unavailable": { + "ddc-disabled": "Helligkeitssteuerung nicht verfügbar. Aktivieren Sie \"Externe Helligkeitsunterstützung\", um die Helligkeit dieses Displays zu steuern.", + "generic": "Die Helligkeitssteuerung ist für dieses Display nicht verfügbar." } }, "night-light": { diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index 66f7f28b..4839d622 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -166,6 +166,14 @@ "enforce-minimum": { "label": "Enforce minimum brightness (1%)", "description": "Solves the problem of backlight completely turning off on some displays at 0% brightness." + }, + "external-brightness": { + "label": "External brightness support", + "description": "Enable DDCUtil support for controlling brightness on external displays via DDC/CI protocol." + }, + "brightness-unavailable": { + "ddc-disabled": "Brightness control unavailable. Enable \"External brightness support\" to control this display's brightness.", + "generic": "Brightness control is not available for this display." } }, "night-light": { diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index a6344bf9..79f8d2df 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -166,6 +166,14 @@ "enforce-minimum": { "label": "Forzar brillo mínimo (1%)", "description": "Resuelve el problema de la retroiluminación que se apaga completamente en algunas pantallas al 0% de brillo." + }, + "external-brightness": { + "label": "Soporte de brillo externo", + "description": "Habilitar soporte DDCUtil para controlar el brillo en pantallas externas mediante el protocolo DDC/CI." + }, + "brightness-unavailable": { + "ddc-disabled": "Control de brillo no disponible. Habilita \"Soporte de brillo externo\" para controlar el brillo de esta pantalla.", + "generic": "El control de brillo no está disponible para esta pantalla." } }, "night-light": { diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index 5e741f0a..44467e4d 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -166,6 +166,14 @@ "enforce-minimum": { "label": "Imposer une luminosité minimale (1%)", "description": "Résout le problème de l'extinction complète du rétroéclairage sur certains écrans à 0% de luminosité." + }, + "external-brightness": { + "label": "Prise en charge de la luminosité externe", + "description": "Activer le support DDCUtil pour contrôler la luminosité des écrans externes via le protocole DDC/CI." + }, + "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.", + "generic": "Le contrôle de la luminosité n'est pas disponible pour cet écran." } }, "night-light": { diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index 06021582..9f09faba 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -166,6 +166,14 @@ "enforce-minimum": { "label": "Forçar brilho mínimo (1%)", "description": "Resolve o problema da retroiluminação apagar completamente em alguns monitores com brilho em 0%." + }, + "external-brightness": { + "label": "Suporte de brilho externo", + "description": "Ativar suporte DDCUtil para controlar o brilho em monitores externos através do protocolo DDC/CI." + }, + "brightness-unavailable": { + "ddc-disabled": "Controle de brilho indisponível. Ative \"Suporte de brilho externo\" para controlar o brilho desta tela.", + "generic": "O controle de brilho não está disponível para esta tela." } }, "night-light": { diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index 518e3ea2..7077817a 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -166,6 +166,14 @@ "enforce-minimum": { "label": "限制最小亮度为 1%", "description": "解决亮度为 0% 时部分显示器背光完全关闭的问题。" + }, + "external-brightness": { + "label": "外部亮度支持", + "description": "启用 DDCUtil 支持,通过 DDC/CI 协议控制外部显示器的亮度。" + }, + "brightness-unavailable": { + "ddc-disabled": "亮度控制不可用。启用“外部亮度支持”以控制此显示器的亮度。", + "generic": "此显示器无法使用亮度控制。" } }, "night-light": { diff --git a/Assets/settings-default.json b/Assets/settings-default.json index 296faf69..2dfd7cf2 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -227,7 +227,8 @@ }, "brightness": { "brightnessStep": 5, - "enforceMinimum": true + "enforceMinimum": true, + "enableDdcSupport": false }, "colorSchemes": { "useWallpaperColors": false, diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 6b2c0766..6fdb283c 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -370,6 +370,7 @@ Singleton { property JsonObject brightness: JsonObject { property int brightnessStep: 5 property bool enforceMinimum: true + property bool enableDdcSupport: false } property JsonObject colorSchemes: JsonObject { diff --git a/Modules/Settings/Tabs/DisplayTab.qml b/Modules/Settings/Tabs/DisplayTab.qml index b835b50d..3690255e 100644 --- a/Modules/Settings/Tabs/DisplayTab.qml +++ b/Modules/Settings/Tabs/DisplayTab.qml @@ -113,12 +113,17 @@ ColumnLayout { to: 1 value: brightnessMonitor ? brightnessMonitor.brightness : 0.5 stepSize: 0.01 + enabled: brightnessMonitor ? brightnessMonitor.brightnessControlAvailable : false onMoved: value => { - if (brightnessMonitor.method === "internal") { + if (brightnessMonitor && brightnessMonitor.brightnessControlAvailable) { brightnessMonitor.setBrightness(value) } } - onPressedChanged: (pressed, value) => brightnessMonitor.setBrightness(value) + onPressedChanged: (pressed, value) => { + if (brightnessMonitor && brightnessMonitor.brightnessControlAvailable) { + brightnessMonitor.setBrightness(value) + } + } Layout.fillWidth: true } @@ -127,17 +132,29 @@ ColumnLayout { Layout.preferredWidth: 55 horizontalAlignment: Text.AlignRight Layout.alignment: Qt.AlignVCenter + opacity: brightnessMonitor && !brightnessMonitor.brightnessControlAvailable ? 0.5 : 1.0 } Item { Layout.preferredWidth: 30 Layout.fillHeight: true NIcon { - icon: brightnessMonitor.method == "internal" ? "device-laptop" : "device-desktop" + icon: brightnessMonitor && brightnessMonitor.method == "internal" ? "device-laptop" : "device-desktop" anchors.centerIn: parent + opacity: brightnessMonitor && !brightnessMonitor.brightnessControlAvailable ? 0.5 : 1.0 } } } + + // Show message when brightness control is not available + NText { + visible: brightnessMonitor && !brightnessMonitor.brightnessControlAvailable + text: !Settings.data.brightness.enableDdcSupport ? I18n.tr("settings.display.monitors.brightness-unavailable.ddc-disabled") : I18n.tr("settings.display.monitors.brightness-unavailable.generic") + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + Layout.fillWidth: true + wrapMode: Text.WordWrap + } } } } @@ -163,6 +180,18 @@ ColumnLayout { checked: Settings.data.brightness.enforceMinimum onToggled: checked => Settings.data.brightness.enforceMinimum = checked } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("settings.display.monitors.external-brightness.label") + description: I18n.tr("settings.display.monitors.external-brightness.description") + checked: Settings.data.brightness.enableDdcSupport + onToggled: checked => { + Settings.data.brightness.enableDdcSupport = checked + // DDC detection will run on next monitor change when enabled + // Monitors will stop using DDC immediately when disabled + } + } } NDivider { diff --git a/Services/BrightnessService.qml b/Services/BrightnessService.qml index fbaaf7b3..f47d9168 100644 --- a/Services/BrightnessService.qml +++ b/Services/BrightnessService.qml @@ -21,7 +21,7 @@ Singleton { function getAvailableMethods(): list { var methods = [] - if (monitors.some(m => m.isDdc)) + if (Settings.data.brightness.enableDdcSupport && monitors.some(m => m.isDdc)) methods.push("ddcutil") if (monitors.some(m => !m.isDdc)) methods.push("internal") @@ -47,11 +47,30 @@ Singleton { Component.onCompleted: { Logger.i("Brightness", "Service started") + if (Settings.data.brightness.enableDdcSupport) { + ddcProc.running = true + } } onMonitorsChanged: { ddcMonitors = [] - ddcProc.running = true + if (Settings.data.brightness.enableDdcSupport) { + ddcProc.running = true + } + } + + Connections { + target: Settings.data.brightness + function onEnableDdcSupportChanged() { + if (Settings.data.brightness.enableDdcSupport) { + // Re-detect DDC monitors when enabled + ddcMonitors = [] + ddcProc.running = true + } else { + // Clear DDC monitors when disabled + ddcMonitors = [] + } + } } Variants { @@ -101,11 +120,21 @@ Singleton { id: monitor required property ShellScreen modelData - readonly property bool isDdc: root.ddcMonitors.some(m => m.model === modelData.model) + readonly property bool isDdc: Settings.data.brightness.enableDdcSupport && root.ddcMonitors.some(m => m.model === modelData.model) readonly property string busNum: root.ddcMonitors.find(m => m.model === modelData.model)?.busNum ?? "" readonly property bool isAppleDisplay: root.appleDisplayPresent && modelData.model.startsWith("StudioDisplay") readonly property string method: isAppleDisplay ? "apple" : (isDdc ? "ddcutil" : "internal") + // Check if brightness control is available for this monitor + readonly property bool brightnessControlAvailable: { + if (isAppleDisplay) + return true + if (isDdc) + return true + // For internal displays, check if we have a brightness path + return brightnessPath !== "" + } + property real brightness property real lastBrightness: 0 property real queuedBrightness: NaN From b9d198a879b1848df152f32a0cdd54e0f009acc8 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Mon, 3 Nov 2025 18:23:41 +0100 Subject: [PATCH 17/30] SessionMenu: fix lockScreen button --- Modules/SessionMenu/SessionMenu.qml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/SessionMenu/SessionMenu.qml b/Modules/SessionMenu/SessionMenu.qml index 6d789d6e..877f68ef 100644 --- a/Modules/SessionMenu/SessionMenu.qml +++ b/Modules/SessionMenu/SessionMenu.qml @@ -87,9 +87,9 @@ NPanel { switch (action) { case "lock": - // Access lockScreen directly like IPCManager does - if (!lockScreen.active) { - lockScreen.active = true + // Access lockScreen via PanelService + if (PanelService.lockScreen && !PanelService.lockScreen.active) { + PanelService.lockScreen.active = true } break case "suspend": From 3c69d5b4da9c3586a72e823770acc57dec0f7f7d Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Mon, 3 Nov 2025 18:32:46 +0100 Subject: [PATCH 18/30] NClock: factorize NClockAnalog/Digital i18n: rephrase analog clock translation --- Assets/Translations/de.json | 2 +- Assets/Translations/en.json | 2 +- Assets/Translations/es.json | 2 +- Assets/Translations/fr.json | 2 +- Assets/Translations/pt.json | 2 +- Assets/Translations/zh-CN.json | 2 +- Modules/Bar/Calendar/CalendarPanel.qml | 3 +- Modules/LockScreen/LockScreen.qml | 3 +- Widgets/NClock.qml | 273 +++++++++++++++++++++++++ 9 files changed, 283 insertions(+), 8 deletions(-) create mode 100644 Widgets/NClock.qml diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index 8d320903..536ca936 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -693,7 +693,7 @@ "description": "Ereignisse im Kalender-Panel anzeigen." }, "use-analog": { - "description": "Eine Analoguhr auf dem Kalenderbildschirm anzeigen.", + "description": "Eine Analoguhr im Kalenderfenster und auf dem Sperrbildschirm anzeigen.", "label": "Analoge Uhr verwenden" }, "first-day-of-week": { diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index 4839d622..2650fcc7 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -699,7 +699,7 @@ }, "use-analog": { "label": "Use analog style clock", - "description": "Show an analog style clock on the calendar screen." + "description": "Show an analog style clock on the calendar window and lock screen." } } }, diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index 79f8d2df..96df1dd8 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -693,7 +693,7 @@ "description": "Mostrar eventos en el panel del calendario." }, "use-analog": { - "description": "Mostrar un reloj de estilo analógico en la pantalla del calendario.", + "description": "Mostrar un reloj de estilo analógico en la ventana del calendario y en la pantalla de bloqueo.", "label": "Usar reloj de estilo analógico" }, "first-day-of-week": { diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index 44467e4d..7578df55 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -693,7 +693,7 @@ "description": "Afficher les événements dans le panneau du calendrier." }, "use-analog": { - "description": "Afficher une horloge de style analogique sur l'écran du calendrier.", + "description": "Afficher une horloge de style analogique dans la fenêtre du calendrier et sur l'écran de verrouillage.", "label": "Utiliser une horloge de style analogique." }, "first-day-of-week": { diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index 9f09faba..e2c99b7f 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -655,7 +655,7 @@ "description": "Exibir eventos no painel do calendário." }, "use-analog": { - "description": "Mostrar um relógio estilo analógico na tela do calendário.", + "description": "Mostrar um relógio estilo analógico na janela do calendário e na tela de bloqueio.", "label": "Use um relógio de estilo analógico." }, "first-day-of-week": { diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index 7077817a..9930341b 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -693,7 +693,7 @@ "description": "在日历面板中显示事件。" }, "use-analog": { - "description": "在日历屏幕上显示一个模拟时钟。", + "description": "在日历窗口和锁定屏幕上显示模拟时钟。", "label": "使用模拟时钟样式" }, "first-day-of-week": { diff --git a/Modules/Bar/Calendar/CalendarPanel.qml b/Modules/Bar/Calendar/CalendarPanel.qml index a2317d61..307300ba 100644 --- a/Modules/Bar/Calendar/CalendarPanel.qml +++ b/Modules/Bar/Calendar/CalendarPanel.qml @@ -201,11 +201,12 @@ NPanel { } // Analog clock - ClockLoader { + NClock { id: clockLoader anchors.right: parent.right anchors.rightMargin: Style.marginXL anchors.verticalCenter: parent.verticalCenter + clockStyle: Settings.data.location.analogClockInCalendar ? "analog" : "digital" progressColor: Color.mOnPrimary Layout.alignment: Qt.AlignVCenter now: root.now diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index 61c16980..59bac6b3 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -365,8 +365,9 @@ Loader { } // Clock - ClockLoader { + NClock { now: Time.date + clockStyle: Settings.data.location.analogClockInCalendar ? "analog" : "digital" Layout.preferredWidth: 70 Layout.preferredHeight: 70 Layout.alignment: Qt.AlignVCenter diff --git a/Widgets/NClock.qml b/Widgets/NClock.qml new file mode 100644 index 00000000..c77774e9 --- /dev/null +++ b/Widgets/NClock.qml @@ -0,0 +1,273 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons +import qs.Widgets +import qs.Services +import Quickshell +import "../Helpers/ColorsConvert.js" as ColorsConvert + +Item { + id: root + + property var now: Time.date + + // Style: "analog" or "digital" + property string clockStyle: "analog" + + // Color properties + property color backgroundColor: Color.mPrimary + property color clockColor: Color.mOnPrimary + + property color secondHandColor: { + var defaultColor = Color.mError + var bestContrast = 1.0 // 1.0 is "no contrast" + var bestColor = defaultColor + var candidates = [Color.mSecondary, Color.mTertiary, Color.mError] + + const minContrast = 1.149 + + for (var i = 0; i < candidates.length; i++) { + var candidate = candidates[i] + var contrastClock = ColorsConvert.getContrastRatio(candidate.toString(), clockColor.toString()) + if (contrastClock < minContrast) { + continue + } + var contrastBg = ColorsConvert.getContrastRatio(candidate.toString(), backgroundColor.toString()) + if (contrastBg < minContrast) { + continue + } + + var currentWorstContrast = Math.min(contrastBg, contrastClock) + + if (currentWorstContrast > bestContrast) { + bestContrast = currentWorstContrast + bestColor = candidate + } + } + + return bestColor + } + + property color progressColor: root.secondHandColor + + height: Math.round((Style.fontSizeXXXL * 1.9) / 2 * Style.uiScaleRatio) * 2 + width: root.height + + Loader { + id: clockLoader + anchors.fill: parent + + sourceComponent: root.clockStyle === "analog" ? analogClockComponent : digitalClockComponent + + onLoaded: { + item.now = Qt.binding(function () { + return root.now + }) + item.backgroundColor = Qt.binding(function () { + return root.backgroundColor + }) + item.clockColor = Qt.binding(function () { + return root.clockColor + }) + if (item.hasOwnProperty("secondHandColor")) { + item.secondHandColor = Qt.binding(function () { + return root.secondHandColor + }) + } + if (item.hasOwnProperty("progressColor")) { + item.progressColor = Qt.binding(function () { + return root.progressColor + }) + } + } + } + + // Analog Clock Component + component NClockAnalog: Item { + property var now + property color backgroundColor: Color.mPrimary + property color clockColor: Color.mOnPrimary + property color secondHandColor: Color.mError + anchors.fill: parent + + Canvas { + id: clockCanvas + anchors.fill: parent + + property int hours: now.getHours() + property int minutes: now.getMinutes() + property int seconds: now.getSeconds() + + onPaint: { + const markAlpha = 0.7 + var ctx = getContext("2d") + ctx.reset() + ctx.translate(width / 2, height / 2) + var radius = Math.min(width, height) / 2 + + // Hour marks + ctx.strokeStyle = Qt.alpha(clockColor, markAlpha) + ctx.lineWidth = 2 * Style.uiScaleRatio + var scaleFactor = 0.7 + + for (var i = 0; i < 12; i++) { + var scaleFactor = 0.8 + if (i % 3 === 0) { + scaleFactor = 0.65 + } + ctx.save() + ctx.rotate(i * Math.PI / 6) + ctx.beginPath() + ctx.moveTo(0, -radius * scaleFactor) + ctx.lineTo(0, -radius) + ctx.stroke() + ctx.restore() + } + + // Hour hand + ctx.save() + var hourAngle = (hours % 12 + minutes / 60) * Math.PI / 6 + ctx.rotate(hourAngle) + ctx.strokeStyle = clockColor + ctx.lineWidth = 3 * Style.uiScaleRatio + ctx.lineCap = "round" + ctx.beginPath() + ctx.moveTo(0, 0) + ctx.lineTo(0, -radius * 0.6) + ctx.stroke() + ctx.restore() + + // Minute hand + ctx.save() + var minuteAngle = (minutes + seconds / 60) * Math.PI / 30 + ctx.rotate(minuteAngle) + ctx.strokeStyle = clockColor + ctx.lineWidth = 2 * Style.uiScaleRatio + ctx.lineCap = "round" + ctx.beginPath() + ctx.moveTo(0, 0) + ctx.lineTo(0, -radius * 0.9) + ctx.stroke() + ctx.restore() + + // Second hand + ctx.save() + var secondAngle = seconds * Math.PI / 30 + ctx.rotate(secondAngle) + ctx.strokeStyle = secondHandColor + ctx.lineWidth = 1.6 * Style.uiScaleRatio + ctx.lineCap = "round" + ctx.beginPath() + ctx.moveTo(0, 0) + ctx.lineTo(0, -radius) + ctx.stroke() + ctx.restore() + + // Center dot + ctx.beginPath() + ctx.arc(0, 0, 3 * Style.uiScaleRatio, 0, 2 * Math.PI) + ctx.fillStyle = clockColor + ctx.fill() + } + + Timer { + interval: 1000 + running: true + repeat: true + onTriggered: { + clockCanvas.hours = now.getHours() + clockCanvas.minutes = now.getMinutes() + clockCanvas.seconds = now.getSeconds() + clockCanvas.requestPaint() + } + } + + Component.onCompleted: requestPaint() + } + } + + // Digital Clock Component + component NClockDigital: Item { + property var now + property color backgroundColor: Color.mPrimary + property color clockColor: Color.mOnPrimary + property color progressColor: Color.mError + + anchors.fill: parent + + // Digital clock's seconds circular progress + Canvas { + id: secondsProgress + anchors.fill: parent + property real progress: now.getSeconds() / 60 + onProgressChanged: requestPaint() + Connections { + target: Time + function onDateChanged() { + const total = now.getSeconds() * 1000 + now.getMilliseconds() + secondsProgress.progress = total / 60000 + } + } + onPaint: { + var ctx = getContext("2d") + var centerX = width / 2 + var centerY = height / 2 + var radius = Math.min(width, height) / 2 - 3 + ctx.reset() + + // Background circle + ctx.beginPath() + ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI) + ctx.lineWidth = 2.5 + ctx.strokeStyle = Qt.alpha(clockColor, 0.15) + ctx.stroke() + + // Progress arc + ctx.beginPath() + ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + progress * 2 * Math.PI) + ctx.lineWidth = 2.5 + ctx.strokeStyle = progressColor + ctx.lineCap = "round" + ctx.stroke() + } + } + + // Digital clock + ColumnLayout { + anchors.centerIn: parent + spacing: -Style.marginXXS + + NText { + text: { + var t = Settings.data.location.use12hourFormat ? I18n.locale.toString(now, "hh AP") : I18n.locale.toString(now, "HH") + return t.split(" ")[0] + } + + pointSize: Style.fontSizeXS + font.weight: Style.fontWeightBold + color: clockColor + family: Settings.data.ui.fontFixed + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: Qt.formatTime(now, "mm") + pointSize: Style.fontSizeXXS + font.weight: Style.fontWeightBold + color: clockColor + family: Settings.data.ui.fontFixed + Layout.alignment: Qt.AlignHCenter + } + } + } + + Component { + id: analogClockComponent + NClockAnalog {} + } + + Component { + id: digitalClockComponent + NClockDigital {} + } +} From 81fbba4a01df15c5d180aa078c7259c63f79c29e Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Mon, 3 Nov 2025 18:42:27 +0100 Subject: [PATCH 19/30] Matugen/Discord: make clock easier to read --- Assets/MatugenTemplates/vesktop.css | 18 +++++ Modules/Bar/Calendar/AnalogClock.qml | 106 -------------------------- Modules/Bar/Calendar/ClockLoader.qml | 78 ------------------- Modules/Bar/Calendar/DigitalClock.qml | 80 ------------------- 4 files changed, 18 insertions(+), 264 deletions(-) delete mode 100644 Modules/Bar/Calendar/AnalogClock.qml delete mode 100644 Modules/Bar/Calendar/ClockLoader.qml delete mode 100644 Modules/Bar/Calendar/DigitalClock.qml diff --git a/Assets/MatugenTemplates/vesktop.css b/Assets/MatugenTemplates/vesktop.css index 17472dd3..03fb6f48 100644 --- a/Assets/MatugenTemplates/vesktop.css +++ b/Assets/MatugenTemplates/vesktop.css @@ -102,4 +102,22 @@ --purple-3: {{colors.on_secondary.default.hex}}; --purple-4: {{colors.on_secondary_container.default.hex}}; --purple-5: {{colors.secondary.default.hex}}; +} + +/* Improve timestamp/clock readability */ +/* Target timestamps with various selector patterns to work across Discord versions */ +[class*="timestamp"] time, +[class*="timestamp"] time[id*="message-timestamp"], +[id*="message-timestamp"] time, +span[class*="timestamp"] time { + color: var(--text-3) !important; /* Use normal text color for better contrast */ + opacity: 0.85 !important; /* Slightly muted but still readable */ +} + +/* Hover state for timestamps - make them fully visible */ +[class*="timestamp"]:hover time, +[id*="message-timestamp"]:hover time, +span[class*="timestamp"]:hover time { + color: var(--text-2) !important; /* Use heading color on hover */ + opacity: 1 !important; } \ No newline at end of file diff --git a/Modules/Bar/Calendar/AnalogClock.qml b/Modules/Bar/Calendar/AnalogClock.qml deleted file mode 100644 index 2b3c5694..00000000 --- a/Modules/Bar/Calendar/AnalogClock.qml +++ /dev/null @@ -1,106 +0,0 @@ -import QtQuick -import qs.Commons -import Quickshell - -Item { - property var now - property color backgroundColor: Color.mPrimary - property color clockColor: Color.mOnPrimary - property color secondHandColor: Color.mError - anchors.fill: parent - - Canvas { - id: clockCanvas - anchors.fill: parent - - property int hours: now.getHours() - property int minutes: now.getMinutes() - property int seconds: now.getSeconds() - - onPaint: { - const markAlpha = 0.7 - var ctx = getContext("2d") - ctx.reset() - ctx.translate(width / 2, height / 2) - var radius = Math.min(width, height) / 2 - - // Hour marks - ctx.strokeStyle = Qt.alpha(clockColor, markAlpha) - ctx.lineWidth = 2 * Style.uiScaleRatio - var scaleFactor = 0.7 - - for (var i = 0; i < 12; i++) { - var scaleFactor = 0.8 - if (i % 3 === 0) { - scaleFactor = 0.65 - } - ctx.save() - ctx.rotate(i * Math.PI / 6) - ctx.beginPath() - ctx.moveTo(0, -radius * scaleFactor) - ctx.lineTo(0, -radius) - ctx.stroke() - ctx.restore() - } - - // Hour hand - ctx.save() - var hourAngle = (hours % 12 + minutes / 60) * Math.PI / 6 - ctx.rotate(hourAngle) - ctx.strokeStyle = clockColor - ctx.lineWidth = 3 * Style.uiScaleRatio - ctx.lineCap = "round" - ctx.beginPath() - ctx.moveTo(0, 0) - ctx.lineTo(0, -radius * 0.6) - ctx.stroke() - ctx.restore() - - // Minute hand - ctx.save() - var minuteAngle = (minutes + seconds / 60) * Math.PI / 30 - ctx.rotate(minuteAngle) - ctx.strokeStyle = clockColor - ctx.lineWidth = 2 * Style.uiScaleRatio - ctx.lineCap = "round" - ctx.beginPath() - ctx.moveTo(0, 0) - ctx.lineTo(0, -radius * 0.9) - ctx.stroke() - ctx.restore() - - // Second hand - ctx.save() - var secondAngle = seconds * Math.PI / 30 - ctx.rotate(secondAngle) - ctx.strokeStyle = secondHandColor - ctx.lineWidth = 1.6 * Style.uiScaleRatio - ctx.lineCap = "round" - ctx.beginPath() - ctx.moveTo(0, 0) - ctx.lineTo(0, -radius) - ctx.stroke() - ctx.restore() - - // Center dot - ctx.beginPath() - ctx.arc(0, 0, 3 * Style.uiScaleRatio, 0, 2 * Math.PI) - ctx.fillStyle = clockColor - ctx.fill() - } - - Timer { - interval: 1000 - running: true - repeat: true - onTriggered: { - clockCanvas.hours = now.getHours() - clockCanvas.minutes = now.getMinutes() - clockCanvas.seconds = now.getSeconds() - clockCanvas.requestPaint() - } - } - - Component.onCompleted: requestPaint() - } -} diff --git a/Modules/Bar/Calendar/ClockLoader.qml b/Modules/Bar/Calendar/ClockLoader.qml deleted file mode 100644 index 253c5a86..00000000 --- a/Modules/Bar/Calendar/ClockLoader.qml +++ /dev/null @@ -1,78 +0,0 @@ -import QtQuick -import qs.Commons -import qs.Services -import Quickshell -import "../../../Helpers/ColorsConvert.js" as ColorsConvert - -Item { - id: clockRoot - property var now - - // Default colors - property color backgroundColor: Color.mPrimary - property color clockColor: Color.mOnPrimary - - property color secondHandColor: { - var defaultColor = Color.mError - var bestContrast = 1.0 // 1.0 is "no contrast" - var bestColor = defaultColor - var candidates = [Color.mSecondary, Color.mTertiary, Color.mError] - - const minContrast = 1.149 - - for (var i = 0; i < candidates.length; i++) { - var candidate = candidates[i] - var contrastClock = ColorsConvert.getContrastRatio(candidate.toString(), clockColor.toString()) - if (contrastClock < minContrast) { - continue - } - var contrastBg = ColorsConvert.getContrastRatio(candidate.toString(), backgroundColor.toString()) - if (contrastBg < minContrast) { - continue - } - - var currentWorstContrast = Math.min(contrastBg, contrastClock) - - if (currentWorstContrast > bestContrast) { - bestContrast = currentWorstContrast - bestColor = candidate - } - } - - return bestColor - } - - property color progressColor: clockRoot.secondHandColor - - height: Math.round((Style.fontSizeXXXL * 1.9) / 2 * Style.uiScaleRatio) * 2 - width: clockRoot.height - - Loader { - id: clockLoader - anchors.fill: parent - - source: Settings.data.location.analogClockInCalendar ? "AnalogClock.qml" : "DigitalClock.qml" - - onLoaded: { - item.now = Qt.binding(function () { - return clockRoot.now - }) - item.backgroundColor = Qt.binding(function () { - return clockRoot.backgroundColor - }) - item.clockColor = Qt.binding(function () { - return clockRoot.clockColor - }) - if (item.hasOwnProperty("secondHandColor")) { - item.secondHandColor = Qt.binding(function () { - return clockRoot.secondHandColor - }) - } - if (item.hasOwnProperty("progressColor")) { - item.progressColor = Qt.binding(function () { - return clockRoot.progressColor - }) - } - } - } -} diff --git a/Modules/Bar/Calendar/DigitalClock.qml b/Modules/Bar/Calendar/DigitalClock.qml deleted file mode 100644 index 3d373910..00000000 --- a/Modules/Bar/Calendar/DigitalClock.qml +++ /dev/null @@ -1,80 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import qs.Commons -import qs.Widgets -import Quickshell - -Item { - - property var now - property color backgroundColor: Color.mPrimary - property color clockColor: Color.mOnPrimary - property color progressColor: Color.mError - - anchors.fill: parent - - // Digital clock's seconds circular progress - Canvas { - id: secondsProgress - anchors.fill: parent - property real progress: now.getSeconds() / 60 - onProgressChanged: requestPaint() - Connections { - target: Time - function onDateChanged() { - const total = now.getSeconds() * 1000 + now.getMilliseconds() - secondsProgress.progress = total / 60000 - } - } - onPaint: { - var ctx = getContext("2d") - var centerX = width / 2 - var centerY = height / 2 - var radius = Math.min(width, height) / 2 - 3 - ctx.reset() - - // Background circle - ctx.beginPath() - ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI) - ctx.lineWidth = 2.5 - ctx.strokeStyle = Qt.alpha(clockColor, 0.15) - ctx.stroke() - - // Progress arc - ctx.beginPath() - ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + progress * 2 * Math.PI) - ctx.lineWidth = 2.5 - ctx.strokeStyle = progressColor - ctx.lineCap = "round" - ctx.stroke() - } - } - - // Digital clock - ColumnLayout { - anchors.centerIn: parent - spacing: -Style.marginXXS - - NText { - text: { - var t = Settings.data.location.use12hourFormat ? I18n.locale.toString(now, "hh AP") : I18n.locale.toString(now, "HH") - return t.split(" ")[0] - } - - pointSize: Style.fontSizeXS - font.weight: Style.fontWeightBold - color: clockColor - family: Settings.data.ui.fontFixed - Layout.alignment: Qt.AlignHCenter - } - - NText { - text: Qt.formatTime(now, "mm") - pointSize: Style.fontSizeXXS - font.weight: Style.fontWeightBold - color: clockColor - family: Settings.data.ui.fontFixed - Layout.alignment: Qt.AlignHCenter - } - } -} From 982f78971b3cbe0093d6379162735bb993603869 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Mon, 3 Nov 2025 18:50:52 +0100 Subject: [PATCH 20/30] Matugen/Discord: make slowmode easier to read --- Assets/MatugenTemplates/vesktop.css | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/Assets/MatugenTemplates/vesktop.css b/Assets/MatugenTemplates/vesktop.css index 03fb6f48..d3743c79 100644 --- a/Assets/MatugenTemplates/vesktop.css +++ b/Assets/MatugenTemplates/vesktop.css @@ -120,4 +120,29 @@ span[class*="timestamp"] time { span[class*="timestamp"]:hover time { color: var(--text-2) !important; /* Use heading color on hover */ opacity: 1 !important; +} + +/* Improve slowmode cooldown and typing indicator readability */ +[class*="cooldownText"], +[class*="cooldownWrapper"], +[class*="cooldownText"] svg, +.cooldownText_b21699, +.cooldownWrapper_b21699 { + color: var(--text-3) !important; /* Use normal text color for better contrast */ + opacity: 0.85 !important; /* Slightly muted but still readable */ +} + +/* Slowmode icon color */ +[class*="slowModeIcon"], +.slowModeIcon_b21699 { + color: var(--text-3) !important; + opacity: 0.85 !important; +} + +/* Typing indicator text */ +[class*="typing"] [class*="text"], +[class*="typingDots"] [class*="text"], +.typing_b88801 .text_b88801 { + color: var(--text-3) !important; + opacity: 0.85 !important; } \ No newline at end of file From acba085531c4d1ae2e3cf74a758a21a50dce5b69 Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Mon, 3 Nov 2025 13:09:01 -0500 Subject: [PATCH 21/30] NSlider: Fix and edge case where the slider would sometime loose focus while dragging. --- Widgets/NSlider.qml | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/Widgets/NSlider.qml b/Widgets/NSlider.qml index c2570386..994ced73 100644 --- a/Widgets/NSlider.qml +++ b/Widgets/NSlider.qml @@ -134,9 +134,8 @@ Slider { anchors.fill: parent cursorShape: Qt.PointingHandCursor hoverEnabled: true - // Pass through mouse events to the slider + acceptedButtons: Qt.NoButton // Don't accept any mouse buttons - only hover propagateComposedEvents: true - preventStealing: false onEntered: { root.hovering = true @@ -151,23 +150,15 @@ Slider { TooltipService.hide() } } + } - onPressed: function (mouse) { - if (root.tooltipText) { + // Hide tooltip when slider is pressed (anywhere on the slider) + Connections { + target: root + function onPressedChanged() { + if (root.pressed && root.tooltipText) { TooltipService.hide() } - // Pass the event through to the slider - mouse.accepted = false - } - - onReleased: function (mouse) { - // Pass the event through to the slider - mouse.accepted = false - } - - onPositionChanged: function (mouse) { - // Pass the event through to the slider - mouse.accepted = false } } } From 9efada7dd76e66dd18092cce92e0d56670400d57 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Mon, 3 Nov 2025 19:28:29 +0100 Subject: [PATCH 22/30] WallpaperPanel: reduce height --- Modules/Wallpaper/WallpaperPanel.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Wallpaper/WallpaperPanel.qml b/Modules/Wallpaper/WallpaperPanel.qml index 472cdfcc..bbb2aaa8 100644 --- a/Modules/Wallpaper/WallpaperPanel.qml +++ b/Modules/Wallpaper/WallpaperPanel.qml @@ -15,7 +15,7 @@ NPanel { preferredWidth: 800 * Style.uiScaleRatio preferredHeight: 600 * Style.uiScaleRatio preferredWidthRatio: 0.5 - preferredHeightRatio: 0.52 + preferredHeightRatio: 0.45 // Positioning - Use launcher position. This saves a setting... readonly property string launcherPosition: Settings.data.appLauncher.position From 15441d2d1bc2d4ee80929f4bb2f7e17cf801e3ae Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Mon, 3 Nov 2025 19:38:45 +0100 Subject: [PATCH 23/30] Matugen/Discord: fix thread text --- Assets/MatugenTemplates/vesktop.css | 33 +++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/Assets/MatugenTemplates/vesktop.css b/Assets/MatugenTemplates/vesktop.css index d3743c79..6c3dedf9 100644 --- a/Assets/MatugenTemplates/vesktop.css +++ b/Assets/MatugenTemplates/vesktop.css @@ -145,4 +145,37 @@ span[class*="timestamp"]:hover time { .typing_b88801 .text_b88801 { color: var(--text-3) !important; opacity: 0.85 !important; +} + +[class*="postTitleText"], +[class*="postTitleText"] span, +h3[class*="postTitleText"], +[class*="heading-lg"][class*="postTitleText"], +[data-text-variant*="heading"] [class*="postTitleText"] { + color: var(--text-2) !important; + opacity: 0.9 !important; +} + +[class*="postTitleText"]:hover, +[class*="postTitleText"]:hover span, +h3[class*="postTitleText"]:hover { + color: var(--text-2) !important; + opacity: 1 !important; +} + +/* Improve deleted message content readability */ +/* Target deleted message content with various selector patterns to work across Discord versions */ +[class*="messageContent"], +[class*="messageContent"] span, +[class*="text-sm"][class*="messageContent"], +[data-text-variant*="text-sm"][class*="messageContent"] { + color: var(--text-3) !important; /* Use normal text color for better contrast */ + opacity: 0.85 !important; /* Slightly muted but still readable */ +} + +/* Hover state for deleted message content - make them fully visible */ +[class*="messageContent"]:hover, +[class*="messageContent"]:hover span { + color: var(--text-2) !important; /* Use heading color on hover */ + opacity: 1 !important; } \ No newline at end of file From f42fe140c016643fdd1c1d379e3733de7ad0a810 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Mon, 3 Nov 2025 19:58:06 +0100 Subject: [PATCH 24/30] BarTab: if bar is floating, hide outer corners --- Modules/Settings/Tabs/BarTab.qml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Modules/Settings/Tabs/BarTab.qml b/Modules/Settings/Tabs/BarTab.qml index 639010e5..f138ccae 100644 --- a/Modules/Settings/Tabs/BarTab.qml +++ b/Modules/Settings/Tabs/BarTab.qml @@ -99,7 +99,16 @@ ColumnLayout { label: I18n.tr("settings.bar.appearance.floating.label") description: I18n.tr("settings.bar.appearance.floating.description") checked: Settings.data.bar.floating - onToggled: checked => Settings.data.bar.floating = checked + onToggled: checked => { + Settings.data.bar.floating = checked + if (checked) { + // Disable outer corners when floating is enabled + Settings.data.bar.outerCorners = false + } else { + // Enable outer corners when floating is disabled + Settings.data.bar.outerCorners = true + } + } } NToggle { @@ -107,6 +116,7 @@ ColumnLayout { label: I18n.tr("settings.bar.appearance.outer-corners.label") description: I18n.tr("settings.bar.appearance.outer-corners.description") checked: Settings.data.bar.outerCorners + visible: !Settings.data.bar.floating onToggled: checked => Settings.data.bar.outerCorners = checked } From 845b689ec899aa8d1c5c7d61a1f16cf3f34ba18d Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Mon, 3 Nov 2025 20:07:59 +0100 Subject: [PATCH 25/30] Matugen/Discord: make messages a tiny bit easier to read --- Assets/MatugenTemplates/vesktop.css | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/Assets/MatugenTemplates/vesktop.css b/Assets/MatugenTemplates/vesktop.css index 6c3dedf9..59bf5a6e 100644 --- a/Assets/MatugenTemplates/vesktop.css +++ b/Assets/MatugenTemplates/vesktop.css @@ -163,19 +163,22 @@ h3[class*="postTitleText"]:hover { opacity: 1 !important; } -/* Improve deleted message content readability */ -/* Target deleted message content with various selector patterns to work across Discord versions */ [class*="messageContent"], -[class*="messageContent"] span, -[class*="text-sm"][class*="messageContent"], -[data-text-variant*="text-sm"][class*="messageContent"] { - color: var(--text-3) !important; /* Use normal text color for better contrast */ - opacity: 0.85 !important; /* Slightly muted but still readable */ +[class*="markup"] { + line-height: 1.35 !important; + color: var(--text-3) !important; + opacity: 0.85 !important; } -/* Hover state for deleted message content - make them fully visible */ -[class*="messageContent"]:hover, -[class*="messageContent"]:hover span { - color: var(--text-2) !important; /* Use heading color on hover */ +[class*="messageContent"][class*="deleted"], +[class*="text-sm"][class*="messageContent"][class*="deleted"], +[data-text-variant*="text-sm"][class*="messageContent"][class*="deleted"] { + color: var(--text-3) !important; + opacity: 0.85 !important; +} + +[class*="messageContent"][class*="deleted"]:hover, +[class*="messageContent"][class*="deleted"]:hover span { + color: var(--text-2) !important; opacity: 1 !important; } \ No newline at end of file From e849fe2a13a8180cd94c48a3cf04d29f2cbd3fe0 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Mon, 3 Nov 2025 20:14:40 +0100 Subject: [PATCH 26/30] Matugen/Discord: make emoji tooltip more readable --- Assets/MatugenTemplates/vesktop.css | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Assets/MatugenTemplates/vesktop.css b/Assets/MatugenTemplates/vesktop.css index 59bf5a6e..a4b7da11 100644 --- a/Assets/MatugenTemplates/vesktop.css +++ b/Assets/MatugenTemplates/vesktop.css @@ -181,4 +181,20 @@ h3[class*="postTitleText"]:hover { [class*="messageContent"][class*="deleted"]:hover span { color: var(--text-2) !important; opacity: 1 !important; +} + +/* Improve tooltip readability */ +[class*="tooltip"], +[class*="tooltip"] *, +[role="tooltip"], +[role="tooltip"] * { + color: var(--text-2) !important; +} + +[class*="tooltipText"], +[class*="tooltipContent"], +[class*="tooltip"] span, +[class*="tooltip"] div { + color: var(--text-2) !important; + opacity: 1 !important; } \ No newline at end of file From af33eb7fe9bb700626f23462f63d4377a0adb76d Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Mon, 3 Nov 2025 14:18:09 -0500 Subject: [PATCH 27/30] NPanel: added animation on backgroundColor to avoid snap/jumps --- Widgets/NPanel.qml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index d8480a06..3fff1d91 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -206,6 +206,13 @@ Item { backgroundColor: root.attachedToBar ? Qt.alpha(root.panelBackgroundColor, Settings.data.bar.backgroundOpacity) : root.panelBackgroundColor + Behavior on backgroundColor { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.InOutQuad + } + } + // Animation properties opacity: root.animationProgress scale: root.attachedToBar ? 1 : (0.95 + root.animationProgress * 0.05) From 2c581e1f1f1255ac8ace9b149d915f911f2828d2 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Mon, 3 Nov 2025 21:20:15 +0100 Subject: [PATCH 28/30] Dock: possible auto-hide fix --- Modules/Dock/Dock.qml | 45 +++++++++++++++++++++++++++++++-------- Modules/Dock/DockMenu.qml | 3 --- 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml index f0caacbd..9c06ce3d 100644 --- a/Modules/Dock/Dock.qml +++ b/Modules/Dock/Dock.qml @@ -150,9 +150,16 @@ Variants { id: hideTimer interval: hideDelay onTriggered: { + // Force menuHovered to false if no menu is current or visible + if (!root.currentContextMenu || !root.currentContextMenu.visible) { + menuHovered = false + } if (autoHide && !dockHovered && !anyAppHovered && !peekHovered && !menuHovered) { hidden = true unloadTimer.restart() // Start unload timer when hiding + } else if (autoHide && !dockHovered && !peekHovered) { + // Restart timer if menu is closing (handles race condition) + restart() } } } @@ -226,6 +233,7 @@ Variants { // DOCK WINDOW Loader { + id: dockWindowLoader active: Settings.data.dock.enabled && (barIsReady || !hasBar) && modelData && (Settings.data.dock.monitors.length === 0 || Settings.data.dock.monitors.includes(modelData.name)) && dockLoaded && ToplevelManager && (dockApps.length > 0) sourceComponent: PanelWindow { @@ -434,24 +442,38 @@ Variants { // Context menu popup DockMenu { id: contextMenu - onHoveredChanged: menuHovered = hovered - onRequestClose: { - contextMenu.hide() - // Restart hide timer after menu action if auto-hide is enabled - if (autoHide && !dockHovered && !anyAppHovered && !peekHovered) { - hideTimer.restart() + onHoveredChanged: { + // Only update menuHovered if this menu is current and visible + if (root.currentContextMenu === contextMenu && contextMenu.visible) { + menuHovered = hovered + } else { + menuHovered = false + } + } + + Connections { + target: contextMenu + function onRequestClose() { + // Clear current menu immediately to prevent hover updates + root.currentContextMenu = null + hideTimer.stop() + contextMenu.hide() + menuHovered = false + anyAppHovered = false } } onAppClosed: root.updateDockApps // Force immediate dock update when app is closed onVisibleChanged: { if (visible) { root.currentContextMenu = contextMenu + anyAppHovered = false } else if (root.currentContextMenu === contextMenu) { root.currentContextMenu = null - // Reset menu hover state when menu becomes invisible + hideTimer.stop() menuHovered = false - // Restart hide timer if conditions are met - if (autoHide && !dockHovered && !anyAppHovered && !peekHovered) { + anyAppHovered = false + // Restart hide timer after menu closes + if (autoHide && !dockHovered && !anyAppHovered && !peekHovered && !menuHovered) { hideTimer.restart() } } @@ -460,6 +482,7 @@ Variants { MouseArea { id: appMouseArea + objectName: "appMouseArea" anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor @@ -482,6 +505,10 @@ Variants { onExited: { anyAppHovered = false TooltipService.hide() + // Clear menuHovered if no current menu or menu not visible + if (!root.currentContextMenu || !root.currentContextMenu.visible) { + menuHovered = false + } if (autoHide && !dockHovered && !peekHovered && !menuHovered) { hideTimer.restart() } diff --git a/Modules/Dock/DockMenu.qml b/Modules/Dock/DockMenu.qml index 060caa44..17fcbd29 100644 --- a/Modules/Dock/DockMenu.qml +++ b/Modules/Dock/DockMenu.qml @@ -121,7 +121,6 @@ PopupWindow { function show(item, toplevelData) { if (!item) { - Logger.w("DockMenu", "anchorItem is undefined, won't show menu.") return } @@ -175,8 +174,6 @@ PopupWindow { if (root.onAppClosed && typeof root.onAppClosed === "function") { Qt.callLater(root.onAppClosed) } - } else { - Logger.w("DockMenu", "Cannot close app - invalid toplevel reference") } root.hide() root.requestClose() From 9f656829b1d155e364c4cb3da918786496058d98 Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Mon, 3 Nov 2025 14:45:30 -0500 Subject: [PATCH 29/30] Panels: implemented snapping to screen edges. WallpaperPanel: settings to position the panel (similar to launcher) --- Assets/Translations/en.json | 7 +- Assets/settings-default.json | 5 +- Commons/Settings.qml | 3 +- Modules/Bar/Audio/AudioPanel.qml | 2 +- Modules/Launcher/Launcher.qml | 24 ++- Modules/Settings/Tabs/LauncherTab.qml | 4 +- Modules/Settings/Tabs/WallpaperTab.qml | 35 ++++ Modules/Wallpaper/WallpaperPanel.qml | 28 ++- Widgets/NPanel.qml | 270 +++++++++++++++---------- 9 files changed, 248 insertions(+), 130 deletions(-) diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index 2650fcc7..ca9893fc 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -474,6 +474,10 @@ "description": "Set a different wallpaper folder for each monitor.", "tooltip": "Browse for wallpaper folder" }, + "selector-position": { + "label": "Position", + "description": "Choose where the wallpaper selector panel appears." + }, "select-folder": "Select wallpaper folder", "select-monitor-folder": "Select monitor wallpaper folder" }, @@ -1500,7 +1504,8 @@ }, "launcher": { "position": { - "center": "Center (default)", + "follow_bar": "Follow bar (default)", + "center": "Center", "top_left": "Top left", "top_right": "Top right", "bottom_left": "Bottom left", diff --git a/Assets/settings-default.json b/Assets/settings-default.json index 2dfd7cf2..8162e410 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -117,11 +117,12 @@ "transitionDuration": 1500, "transitionType": "random", "transitionEdgeSmoothness": 0.05, - "monitors": [] + "monitors": [], + "selectorPosition": "follow_bar" }, "appLauncher": { "enableClipboardHistory": false, - "position": "center", + "position": "follow_bar", "backgroundOpacity": 1, "pinnedExecs": [], "useApp2Unit": false, diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 6fdb283c..68ce3b3f 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -14,7 +14,7 @@ Singleton { readonly property alias data: adapter property bool isLoaded: false property bool directoriesCreated: false - property int settingsVersion: 16 + property int settingsVersion: 17 property bool isDebug: Quickshell.env("NOCTALIA_DEBUG") === "1" // Define our app directories @@ -256,6 +256,7 @@ Singleton { property string transitionType: "random" property real transitionEdgeSmoothness: 0.05 property list monitors: [] + property string panelPosition: "folow_bar" } // applauncher diff --git a/Modules/Bar/Audio/AudioPanel.qml b/Modules/Bar/Audio/AudioPanel.qml index 7393f846..e1f240cc 100644 --- a/Modules/Bar/Audio/AudioPanel.qml +++ b/Modules/Bar/Audio/AudioPanel.qml @@ -17,7 +17,7 @@ NPanel { property bool localInputVolumeChanging: false preferredWidth: 380 * Style.uiScaleRatio - preferredHeight: 500 * Style.uiScaleRatio + preferredHeight: 420 * Style.uiScaleRatio // Connections to update local volumes when AudioService changes Connections { diff --git a/Modules/Launcher/Launcher.qml b/Modules/Launcher/Launcher.qml index 5f1ed23b..c996a9ea 100644 --- a/Modules/Launcher/Launcher.qml +++ b/Modules/Launcher/Launcher.qml @@ -20,13 +20,23 @@ NPanel { panelKeyboardFocus: true // Needs Exclusive focus for text input // Positioning - readonly property string launcherPosition: Settings.data.appLauncher.position - panelAnchorHorizontalCenter: launcherPosition === "center" || launcherPosition.endsWith("_center") - panelAnchorVerticalCenter: launcherPosition === "center" - panelAnchorLeft: launcherPosition !== "center" && launcherPosition.endsWith("_left") - panelAnchorRight: launcherPosition !== "center" && launcherPosition.endsWith("_right") - panelAnchorBottom: launcherPosition.startsWith("bottom_") - panelAnchorTop: launcherPosition.startsWith("top_") + readonly property string panelPosition: { + if (Settings.data.appLauncher.position === "follow_bar") { + if (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") { + return `center_${Settings.data.bar.position}` + } else { + return `${Settings.data.bar.position}_center` + } + } else { + return Settings.data.appLauncher.position + } + } + panelAnchorHorizontalCenter: panelPosition === "center" || panelPosition.endsWith("_center") + panelAnchorVerticalCenter: panelPosition === "center" + panelAnchorLeft: panelPosition !== "center" && panelPosition.endsWith("_left") + panelAnchorRight: panelPosition !== "center" && panelPosition.endsWith("_right") + panelAnchorBottom: panelPosition.startsWith("bottom_") + panelAnchorTop: panelPosition.startsWith("top_") // Core state property string searchText: "" diff --git a/Modules/Settings/Tabs/LauncherTab.qml b/Modules/Settings/Tabs/LauncherTab.qml index dd6cee27..43eb7075 100644 --- a/Modules/Settings/Tabs/LauncherTab.qml +++ b/Modules/Settings/Tabs/LauncherTab.qml @@ -15,11 +15,13 @@ ColumnLayout { } NComboBox { - id: launcherPosition label: I18n.tr("settings.launcher.settings.position.label") description: I18n.tr("settings.launcher.settings.position.description") Layout.fillWidth: true model: [{ + "key": "follow_bar", + "name": I18n.tr("options.launcher.position.follow_bar") + }, { "key": "center", "name": I18n.tr("options.launcher.position.center") }, { diff --git a/Modules/Settings/Tabs/WallpaperTab.qml b/Modules/Settings/Tabs/WallpaperTab.qml index 002c6996..5d7175aa 100644 --- a/Modules/Settings/Tabs/WallpaperTab.qml +++ b/Modules/Settings/Tabs/WallpaperTab.qml @@ -103,6 +103,41 @@ ColumnLayout { } } } + + NComboBox { + label: I18n.tr("settings.wallpaper.settings.selector-position.label") + description: I18n.tr("settings.wallpaper.settings.selector-position.description") + Layout.fillWidth: true + model: [{ + "key": "follow_bar", + "name": I18n.tr("options.launcher.position.follow_bar") + }, { + "key": "center", + "name": I18n.tr("options.launcher.position.center") + }, { + "key": "top_center", + "name": I18n.tr("options.launcher.position.top_center") + }, { + "key": "top_left", + "name": I18n.tr("options.launcher.position.top_left") + }, { + "key": "top_right", + "name": I18n.tr("options.launcher.position.top_right") + }, { + "key": "bottom_left", + "name": I18n.tr("options.launcher.position.bottom_left") + }, { + "key": "bottom_right", + "name": I18n.tr("options.launcher.position.bottom_right") + }, { + "key": "bottom_center", + "name": I18n.tr("options.launcher.position.bottom_center") + }] + currentKey: Settings.data.wallpaper.panelPosition + onSelected: function (key) { + Settings.data.wallpaper.panelPosition = key + } + } } NDivider { diff --git a/Modules/Wallpaper/WallpaperPanel.qml b/Modules/Wallpaper/WallpaperPanel.qml index bbb2aaa8..3cfa36fc 100644 --- a/Modules/Wallpaper/WallpaperPanel.qml +++ b/Modules/Wallpaper/WallpaperPanel.qml @@ -17,17 +17,25 @@ NPanel { preferredWidthRatio: 0.5 preferredHeightRatio: 0.45 - // Positioning - Use launcher position. This saves a setting... - readonly property string launcherPosition: Settings.data.appLauncher.position - panelAnchorHorizontalCenter: launcherPosition === "center" || launcherPosition.endsWith("_center") - panelAnchorVerticalCenter: launcherPosition === "center" - panelAnchorLeft: launcherPosition !== "center" && launcherPosition.endsWith("_left") - panelAnchorRight: launcherPosition !== "center" && launcherPosition.endsWith("_right") - panelAnchorBottom: launcherPosition.startsWith("bottom_") - panelAnchorTop: launcherPosition.startsWith("top_") + // Positioning + readonly property string panelPosition: { + if (Settings.data.wallpaper.panelPosition === "follow_bar") { + if (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") { + return `center_${Settings.data.bar.position}` + } else { + return `${Settings.data.bar.position}_center` + } + } else { + return Settings.data.wallpaper.panelPosition + } + } + panelAnchorHorizontalCenter: panelPosition === "center" || panelPosition.endsWith("_center") + panelAnchorVerticalCenter: panelPosition === "center" + panelAnchorLeft: panelPosition !== "center" && panelPosition.endsWith("_left") + panelAnchorRight: panelPosition !== "center" && panelPosition.endsWith("_right") + panelAnchorBottom: panelPosition.startsWith("bottom_") + panelAnchorTop: panelPosition.startsWith("top_") - // panelAnchorHorizontalCenter: true - // panelAnchorVerticalCenter: true panelKeyboardFocus: true // Needs Exclusive focus for text input (search) // Store direct reference to content for instant access diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 3fff1d91..92a97705 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -17,6 +17,9 @@ Item { property bool forceDetached: false // Force panel to be detached regardless of settings property bool attachedToBar: (Settings.data.ui.panelsAttachedToBar && Settings.data.bar.backgroundOpacity > opacityThreshold && !forceDetached) + // Edge snapping: if panel is within this distance (in pixels) from a screen edge, snap + property real edgeSnapDistance: 40 + // Keyboard focus documentation (not currently used for focus mode) // Just for documentation: true for panels with text input // NFullScreenWindow always uses Exclusive focus when any panel is open @@ -343,6 +346,8 @@ Item { // Position the panel using explicit x/y coordinates (no anchors) // This makes coordinates clearer for the click-through mask system x: { + var calculatedX + // If useButtonPosition is enabled, align panel X with button // Note: We check useButtonPosition, not buttonItem, because buttonItem may become invalid // after the source panel (e.g., ControlCenter) closes, but we still have valid position data @@ -357,7 +362,7 @@ Item { // Panel sits right at bar edge (inverted corners curve up/down) // Slide from the bar when opening // Shift left by 1px to eliminate any gap between bar and panel - return leftBarEdge - slideOffset - 1 + calculatedX = leftBarEdge - slideOffset - 1 } else { // right // Panel to the left of right bar @@ -365,14 +370,14 @@ Item { // Panel sits right at bar edge (inverted corners curve up/down) // Slide from the bar when opening // Shift right by 1px to eliminate any gap between bar and panel - return rightBarEdge - width + slideOffset + 1 + calculatedX = rightBarEdge - width + slideOffset + 1 } } else { // Detached panels: center on button X position var panelX = root.buttonPosition.x + root.buttonWidth / 2 - width / 2 // Clamp to screen bounds with margins panelX = Math.max(Style.marginL, Math.min(panelX, parent.width - width - Style.marginL)) - return panelX + calculatedX = panelX } } else { // For horizontal bars, center panel on button X position @@ -389,56 +394,87 @@ Item { } else { panelX = Math.max(Style.marginL, Math.min(panelX, parent.width - width - Style.marginL)) } - return panelX + calculatedX = panelX } - } - - // Standard anchor positioning - Logger.d("NPanel", "Fallback to standard anchor positioning") - - if (root.panelAnchorHorizontalCenter) { - Logger.d("NPanel", " -> Horizontal center") - return (parent.width - width) / 2 - } else if (root.effectivePanelAnchorRight) { - Logger.d("NPanel", " -> Right anchor") - return parent.width - width - Style.marginL - } else if (root.effectivePanelAnchorLeft) { - Logger.d("NPanel", " -> Left anchor") - return Style.marginL } else { - // No explicit anchor: default to centering on bar - Logger.d("NPanel", " -> Default to center (no explicit anchor)") - // For horizontal bars: center horizontally - // For vertical bars: center horizontally in available space - if (root.barIsVertical) { - // Center in the space not occupied by the bar - if (root.barPosition === "left") { - var availableStart = root.barMarginH + Style.barHeight - var availableWidth = parent.width - availableStart - Style.marginL - return availableStart + (availableWidth - width) / 2 + // Standard anchor positioning + Logger.d("NPanel", "Fallback to standard anchor positioning") + + if (root.panelAnchorHorizontalCenter) { + Logger.d("NPanel", " -> Horizontal center") + calculatedX = (parent.width - width) / 2 + } else if (root.effectivePanelAnchorRight) { + Logger.d("NPanel", " -> Right anchor") + // When attached to right vertical bar, position next to bar (like useButtonPosition does) + if (root.attachedToBar && root.barIsVertical && root.barPosition === "right") { + var rightBarEdge = parent.width - root.barMarginH - Style.barHeight + calculatedX = rightBarEdge - width + 1 // +1 to eliminate gap } else { - // right - var availableWidth = parent.width - root.barMarginH - Style.barHeight - Style.marginL - return Style.marginL + (availableWidth - width) / 2 + calculatedX = parent.width - width - Style.marginL + } + } else if (root.effectivePanelAnchorLeft) { + Logger.d("NPanel", " -> Left anchor") + // When attached to left vertical bar, position next to bar (like useButtonPosition does) + if (root.attachedToBar && root.barIsVertical && root.barPosition === "left") { + var leftBarEdge = root.barMarginH + Style.barHeight + calculatedX = leftBarEdge - 1 // -1 to eliminate gap + } else { + calculatedX = Style.marginL } } else { - // For horizontal bars: center horizontally, respect bar margins if attached - if (root.attachedToBar) { - // When attached, respect bar bounds (like button position does) - var cornerInset = Style.radiusL + (root.barFloating ? Style.radiusL : 0) - var barLeftEdge = root.barMarginH + cornerInset - var barRightEdge = parent.width - root.barMarginH - cornerInset - var centeredX = (parent.width - width) / 2 - return Math.max(barLeftEdge, Math.min(centeredX, barRightEdge - width)) + // No explicit anchor: default to centering on bar + Logger.d("NPanel", " -> Default to center (no explicit anchor)") + + // For horizontal bars: center horizontally + // For vertical bars: center horizontally in available space + if (root.barIsVertical) { + // Center in the space not occupied by the bar + if (root.barPosition === "left") { + var availableStart = root.barMarginH + Style.barHeight + var availableWidth = parent.width - availableStart - Style.marginL + calculatedX = availableStart + (availableWidth - width) / 2 + } else { + // right + var availableWidth = parent.width - root.barMarginH - Style.barHeight - Style.marginL + calculatedX = Style.marginL + (availableWidth - width) / 2 + } } else { - return (parent.width - width) / 2 + // For horizontal bars: center horizontally, respect bar margins if attached + if (root.attachedToBar) { + // When attached, respect bar bounds (like button position does) + var cornerInset = Style.radiusL + (root.barFloating ? Style.radiusL : 0) + var barLeftEdge = root.barMarginH + cornerInset + var barRightEdge = parent.width - root.barMarginH - cornerInset + var centeredX = (parent.width - width) / 2 + calculatedX = Math.max(barLeftEdge, Math.min(centeredX, barRightEdge - width)) + } else { + calculatedX = (parent.width - width) / 2 + } } } } + + // Edge snapping: snap to screen edges if close (only when attached and bar is not floating) + if (root.attachedToBar && !root.barFloating && parent.width > 0 && width > 0) { + var leftEdgePos = root.barMarginH + var rightEdgePos = parent.width - root.barMarginH - width + + // Snap to left edge if within snap distance + if (Math.abs(calculatedX - leftEdgePos) <= root.edgeSnapDistance) { + calculatedX = leftEdgePos + } // Snap to right edge if within snap distance + else if (Math.abs(calculatedX - rightEdgePos) <= root.edgeSnapDistance) { + calculatedX = rightEdgePos + } + } + + return calculatedX } y: { + var calculatedY + // If useButtonPosition is enabled, position panel relative to bar // Note: We check useButtonPosition, not buttonItem, because buttonItem may become invalid // after the source panel (e.g., ControlCenter) closes, but we still have valid position data @@ -450,9 +486,9 @@ Item { // Panel sits right at bar edge (inverted corners curve to the sides) // Slide from the bar when opening // Shift up by 1px to eliminate any gap between bar and panel - return topBarEdge - slideOffset - 1 + calculatedY = topBarEdge - slideOffset - 1 } else { - return topBarEdge + Style.marginM + calculatedY = topBarEdge + Style.marginM } } else if (root.barPosition === "bottom") { // Panel above bottom bar @@ -461,9 +497,9 @@ Item { // Panel sits right at bar edge (inverted corners curve to the sides) // Slide from the bar when opening // Shift down by 1px to eliminate any gap between bar and panel - return bottomBarEdge - height + slideOffset + 1 + calculatedY = bottomBarEdge - height + slideOffset + 1 } else { - return bottomBarEdge - height - Style.marginM + calculatedY = bottomBarEdge - height - Style.marginM } } else if (root.barIsVertical) { // For vertical bars, center panel on button Y position @@ -481,83 +517,103 @@ Item { } else { panelY = Math.max(Style.marginL + extraPadding, Math.min(panelY, parent.height - height - Style.marginL - extraPadding)) } - return panelY - } - } - - // Standard anchor positioning - // Calculate bar offset for detached panels - they should never overlap the bar - var barOffset = 0 - if (!root.attachedToBar) { - // For detached panels, always account for bar position - if (root.barPosition === "top") { - barOffset = root.barMarginV + Style.barHeight + Style.marginM - } else if (root.barPosition === "bottom") { - barOffset = root.barMarginV + Style.barHeight + Style.marginM + calculatedY = panelY } } else { - // For attached panels with explicit anchors - if (root.effectivePanelAnchorTop && root.barPosition === "top") { - // When attached to top bar: position right at bar edge (like useButtonPosition does) - // Shift up by 1px to eliminate gap between bar and panel - return root.barMarginV + Style.barHeight - slideOffset - 1 - } else if (root.effectivePanelAnchorBottom && root.barPosition === "bottom") { - // When attached to bottom bar: position right at bar edge - // Shift down by 1px to eliminate gap between bar and panel - return parent.height - root.barMarginV - Style.barHeight - height + slideOffset + 1 - } else if (!root.hasExplicitVerticalAnchor) { - // No explicit vertical anchor AND attached: default to attaching to bar edge + + // Standard anchor positioning + // Calculate bar offset for detached panels - they should never overlap the bar + var barOffset = 0 + if (!root.attachedToBar) { + // For detached panels, always account for bar position if (root.barPosition === "top") { - // Attach to top bar - return root.barMarginV + Style.barHeight - slideOffset - 1 + barOffset = root.barMarginV + Style.barHeight + Style.marginM } else if (root.barPosition === "bottom") { - // Attach to bottom bar - return parent.height - root.barMarginV - Style.barHeight - height + slideOffset + 1 - } - // For vertical bars with no explicit anchor: center vertically on bar - // This is handled in the else block below - } - } - - if (root.panelAnchorVerticalCenter) { - return (parent.height - height) / 2 - } else if (root.effectivePanelAnchorTop) { - return barOffset + Style.marginL - } else if (root.effectivePanelAnchorBottom) { - return parent.height - height - barOffset - Style.marginL - } else { - // No explicit vertical anchor - if (root.barIsVertical) { - // For vertical bars: center vertically on bar - if (root.attachedToBar) { - // When attached, respect bar bounds - var cornerInset = root.barFloating ? Style.radiusL * 2 : 0 - var barTopEdge = root.barMarginV + cornerInset - var barBottomEdge = parent.height - root.barMarginV - cornerInset - var centeredY = (parent.height - height) / 2 - return Math.max(barTopEdge, Math.min(centeredY, barBottomEdge - height)) - } else { - return (parent.height - height) / 2 + barOffset = root.barMarginV + Style.barHeight + Style.marginM } } else { - // For horizontal bars: attach to bar edge by default - if (root.attachedToBar) { + // For attached panels with explicit anchors + if (root.effectivePanelAnchorTop && root.barPosition === "top") { + // When attached to top bar: position right at bar edge (like useButtonPosition does) + // Shift up by 1px to eliminate gap between bar and panel + calculatedY = root.barMarginV + Style.barHeight - slideOffset - 1 + } else if (root.effectivePanelAnchorBottom && root.barPosition === "bottom") { + // When attached to bottom bar: position right at bar edge + // Shift down by 1px to eliminate gap between bar and panel + calculatedY = parent.height - root.barMarginV - Style.barHeight - height + slideOffset + 1 + } else if (!root.hasExplicitVerticalAnchor) { + // No explicit vertical anchor AND attached: default to attaching to bar edge if (root.barPosition === "top") { - return root.barMarginV + Style.barHeight - slideOffset - 1 + // Attach to top bar + calculatedY = root.barMarginV + Style.barHeight - slideOffset - 1 } else if (root.barPosition === "bottom") { - return parent.height - root.barMarginV - Style.barHeight - height + slideOffset + 1 + // Attach to bottom bar + calculatedY = parent.height - root.barMarginV - Style.barHeight - height + slideOffset + 1 } + // For vertical bars with no explicit anchor: fall through to center vertically on bar } - // Detached or no bar position: use default positioning - if (root.barPosition === "top") { - return barOffset + Style.marginL - } else if (root.barPosition === "bottom") { - return Style.marginL + } + + // Continue if calculatedY was already set above, or proceed with anchor positioning + if (calculatedY === undefined) { + if (root.panelAnchorVerticalCenter) { + calculatedY = (parent.height - height) / 2 + } else if (root.effectivePanelAnchorTop) { + calculatedY = barOffset + Style.marginL + } else if (root.effectivePanelAnchorBottom) { + calculatedY = parent.height - height - barOffset - Style.marginL } else { - return Style.marginL + // No explicit vertical anchor + if (root.barIsVertical) { + // For vertical bars: center vertically on bar + if (root.attachedToBar) { + // When attached, respect bar bounds + var cornerInset = root.barFloating ? Style.radiusL * 2 : 0 + var barTopEdge = root.barMarginV + cornerInset + var barBottomEdge = parent.height - root.barMarginV - cornerInset + var centeredY = (parent.height - height) / 2 + calculatedY = Math.max(barTopEdge, Math.min(centeredY, barBottomEdge - height)) + } else { + calculatedY = (parent.height - height) / 2 + } + } else { + // For horizontal bars: attach to bar edge by default + if (root.attachedToBar && !root.barIsVertical) { + if (root.barPosition === "top") { + calculatedY = root.barMarginV + Style.barHeight - slideOffset - 1 + } else if (root.barPosition === "bottom") { + calculatedY = parent.height - root.barMarginV - Style.barHeight - height + slideOffset + 1 + } + } else { + // Detached or no bar position: use default positioning + if (root.barPosition === "top") { + calculatedY = barOffset + Style.marginL + } else if (root.barPosition === "bottom") { + calculatedY = Style.marginL + } else { + calculatedY = Style.marginL + } + } + } } } } + + // Edge snapping: snap to screen edges if close (only when attached and bar is not floating) + if (root.attachedToBar && !root.barFloating && parent.height > 0 && height > 0) { + var topEdgePos = root.barMarginV + var bottomEdgePos = parent.height - root.barMarginV - height + + // Snap to top edge if within snap distance + if (Math.abs(calculatedY - topEdgePos) <= root.edgeSnapDistance) { + calculatedY = topEdgePos + } // Snap to bottom edge if within snap distance + else if (Math.abs(calculatedY - bottomEdgePos) <= root.edgeSnapDistance) { + calculatedY = bottomEdgePos + } + } + + return calculatedY } // MouseArea to catch clicks on the panel and prevent them from reaching the background From c47bb9820bb2a565583125a924f08cd05251e112 Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Mon, 3 Nov 2025 15:23:57 -0500 Subject: [PATCH 30/30] i18n: auto translate --- Assets/Translations/de.json | 9 +++++++-- Assets/Translations/es.json | 9 +++++++-- Assets/Translations/fr.json | 9 +++++++-- Assets/Translations/pt.json | 9 +++++++-- Assets/Translations/uk-UA.json | 35 +++++++++++++++++++++++++++++++--- Assets/Translations/zh-CN.json | 9 +++++++-- 6 files changed, 67 insertions(+), 13 deletions(-) diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index 536ca936..3e993850 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -475,7 +475,11 @@ "tooltip": "Nach Hintergrundbild-Ordner suchen" }, "select-folder": "Hintergrundbild-Ordner auswählen", - "select-monitor-folder": "Monitor-Hintergrundbild-Ordner auswählen" + "select-monitor-folder": "Monitor-Hintergrundbild-Ordner auswählen", + "selector-position": { + "description": "Wählen Sie aus, wo das Hintergrundbildauswahlfeld angezeigt wird.", + "label": "Position" + } }, "look-feel": { "section": { @@ -1518,7 +1522,8 @@ "bottom_center": "Unten mittig", "top_center": "Oben mittig", "center_left": "Links mittig", - "center_right": "Rechts mittig" + "center_right": "Rechts mittig", + "follow_bar": "Folge dem Balken (Standard)" } }, "control-center": { diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index 96df1dd8..dac5acfe 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -475,7 +475,11 @@ "tooltip": "Buscar carpeta de fondos de pantalla" }, "select-folder": "Seleccionar carpeta de fondos de pantalla", - "select-monitor-folder": "Seleccionar carpeta de fondos de pantalla del monitor" + "select-monitor-folder": "Seleccionar carpeta de fondos de pantalla del monitor", + "selector-position": { + "description": "Elige dónde aparece el panel selector de fondo de pantalla.", + "label": "Posición" + } }, "look-feel": { "section": { @@ -1501,7 +1505,8 @@ "bottom_center": "Inferior central", "top_center": "Superior central", "center_left": "Izquierda centrado", - "center_right": "Derecha centrado" + "center_right": "Derecha centrado", + "follow_bar": "Seguir barra (predeterminado)" } }, "control-center": { diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index 7578df55..2e67d8c2 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -475,7 +475,11 @@ "tooltip": "Parcourir le dossier des fonds d'écran" }, "select-folder": "Sélectionner le dossier des fonds d'écran", - "select-monitor-folder": "Sélectionner le dossier des fonds d'écran du moniteur" + "select-monitor-folder": "Sélectionner le dossier des fonds d'écran du moniteur", + "selector-position": { + "description": "Choisissez l'emplacement d'affichage du panneau de sélection du fond d'écran.", + "label": "Position" + } }, "look-feel": { "section": { @@ -1501,7 +1505,8 @@ "bottom_center": "En bas au centre", "top_center": "En haut au centre", "center_left": "Gauche centre", - "center_right": "Droite centre" + "center_right": "Droite centre", + "follow_bar": "Suivre la barre (par défaut)" } }, "control-center": { diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index e2c99b7f..c30f6956 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -437,7 +437,11 @@ "tooltip": "Procurar pasta de papéis de parede" }, "select-folder": "Selecionar pasta de papéis de parede", - "select-monitor-folder": "Selecionar pasta de papéis de parede do monitor" + "select-monitor-folder": "Selecionar pasta de papéis de parede do monitor", + "selector-position": { + "description": "Escolha onde o painel do seletor de papel de parede aparece.", + "label": "Posição" + } }, "look-feel": { "section": { @@ -1499,7 +1503,8 @@ "bottom_center": "Centro inferior", "top_center": "Centro superior", "center_left": "Centro à esquerda", - "center_right": "Centro à direita" + "center_right": "Centro à direita", + "follow_bar": "Seguir barra (padrão)" } }, "control-center": { diff --git a/Assets/Translations/uk-UA.json b/Assets/Translations/uk-UA.json index 9e238ecb..8b4f6c84 100644 --- a/Assets/Translations/uk-UA.json +++ b/Assets/Translations/uk-UA.json @@ -166,6 +166,14 @@ "enforce-minimum": { "label": "Примусова мінімальна яскравість (1%)", "description": "Вирішує проблему повного вимкнення підсвітки на деяких дисплеях при яскравості 0%." + }, + "brightness-unavailable": { + "ddc-disabled": "Регулювання яскравості недоступне. Увімкніть \"Підтримку зовнішньої яскравості\", щоб керувати яскравістю цього дисплея.", + "generic": "Регулювання яскравості для цього дисплея недоступне." + }, + "external-brightness": { + "description": "Увімкнути підтримку DDCUtil для керування яскравістю на зовнішніх дисплеях через протокол DDC/CI.", + "label": "Підтримка зовнішньої яскравості" } }, "night-light": { @@ -233,6 +241,10 @@ "description": "Налаштуйте поля навколо плаваючої панелі.", "vertical": "Вертикальні", "horizontal": "Горизонтальні" + }, + "outer-corners": { + "description": "Відображає назовні закруглені кути на панелі.", + "label": "Зовнішні кути" } }, "widgets": { @@ -463,7 +475,11 @@ "tooltip": "Огляд теки шпалер" }, "select-folder": "Вибрати теку шпалер", - "select-monitor-folder": "Вибрати теку шпалер монітора" + "select-monitor-folder": "Вибрати теку шпалер монітора", + "selector-position": { + "description": "Виберіть, де відображатиметься панель вибору шпалер.", + "label": "Позиція" + } }, "look-feel": { "section": { @@ -588,6 +604,10 @@ "foot": { "description": "Записати {filepath} та перезавантажити", "description-missing": "Потрібна установка {app}" + }, + "alacritty": { + "description": "Записати {filepath} та перезавантажити", + "description-missing": "Потрібно встановити {app}" } }, "programs": { @@ -872,6 +892,14 @@ "panels-overlay": { "label": "Відкривати панелі на шарі накладення", "description": "Панелі з'являтимуться поверх повноекранних вікон" + }, + "dim-desktop": { + "description": "Затемнювати робочий стіл, коли відкриті панелі або меню.", + "label": "Приглушити робочий стіл" + }, + "shadows": { + "description": "Увімкнути тіні під панелями та смугами.", + "label": "Тіні" } }, "lock-screen": { @@ -1484,7 +1512,8 @@ "bottom_center": "Знизу по центру", "top_center": "Зверху по центру", "center_left": "Зліва по центру", - "center_right": "Справа по центру" + "center_right": "Справа по центру", + "follow_bar": "Слідувати за панеллю (за замовчуванням)" } }, "control-center": { @@ -1857,4 +1886,4 @@ "note": "Лише кілька основних речей для початку - повні опції в Налаштуваннях" } } -} \ No newline at end of file +} diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index 9930341b..104a7cc3 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -475,7 +475,11 @@ "tooltip": "浏览壁纸文件夹" }, "select-folder": "选择壁纸文件夹", - "select-monitor-folder": "选择显示器壁纸文件夹" + "select-monitor-folder": "选择显示器壁纸文件夹", + "selector-position": { + "description": "选择壁纸选择面板的显示位置。", + "label": "职位" + } }, "look-feel": { "section": { @@ -1501,7 +1505,8 @@ "bottom_center": "底部居中", "top_center": "顶部居中", "center_left": "左侧居中", - "center_right": "右侧居中" + "center_right": "右侧居中", + "follow_bar": "跟随栏(默认)" } }, "control-center": {