From 62000eec1e93f585ab8094b8196907d7f317bd77 Mon Sep 17 00:00:00 2001 From: dwuggh Date: Sun, 12 Oct 2025 02:26:44 +0800 Subject: [PATCH 1/6] feat: option to auto-hide the bar with edge hover reveal - Keep bar as overlay while auto-hide is enabled (WlrLayershell.exclusionMode: Ignore) to avoid compositor relayouts and work-area changes. - Add 1px edge "peek" PanelWindow overlay to reveal on hover; - Animate bar with directional slide. --- Assets/settings-default.json | 1 + Commons/Settings.qml | 1 + Modules/Bar/Bar.qml | 182 ++++++++++++++++++++++++++++++- Modules/Settings/Tabs/BarTab.qml | 8 ++ 4 files changed, 191 insertions(+), 1 deletion(-) diff --git a/Assets/settings-default.json b/Assets/settings-default.json index 9b63d3ae..7f9e9a9f 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -9,6 +9,7 @@ "floating": false, "marginVertical": 0.25, "marginHorizontal": 0.25, + "autoHide": false, "widgets": { "left": [ diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 8375d67a..31143431 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -134,6 +134,7 @@ Singleton { property list monitors: [] property string density: "default" // "compact", "default", "comfortable" property bool showCapsule: true + property bool autoHide: false // Floating bar settings property bool floating: false diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index dd6d950c..8a7d3fc3 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -19,6 +19,68 @@ Variants { required property ShellScreen modelData property real scaling: ScalingService.getScreenScale(modelData) + // Auto-hide state and timings + property bool autoHide: Settings.data.bar.autoHide + property bool hidden: autoHide + property bool barHovered: false + property bool peekHovered: false + // Controls PanelWindow visibility while auto-hide is enabled + property bool barWindowVisible: !autoHide + readonly property int hideDelay: 500 + readonly property int showDelay: 120 + readonly property int hideAnimationDuration: Style.animationNormal + readonly property int showAnimationDuration: Style.animationNormal + + // Ensure internal state updates when the setting toggles + Connections { + target: Settings.data.bar + function onAutoHideChanged() { + root.autoHide = Settings.data.bar.autoHide + if (root.autoHide) { + root.hidden = true + root.barWindowVisible = false + } else { + root.hidden = false + root.barWindowVisible = true + } + } + } + + // Timers for reveal/hide + Timer { + id: showTimer + interval: root.showDelay + repeat: false + onTriggered: { + root.barWindowVisible = true + root.hidden = false + } + } + + Timer { + id: hideTimer + interval: root.hideDelay + repeat: false + onTriggered: { + if (root.autoHide && !root.peekHovered && !root.barHovered) { + root.hidden = true + unloadTimer.restart() + } + } + } + + // After hide animation, make the window invisible so it doesn't intercept input + Timer { + id: unloadTimer + interval: root.hideAnimationDuration + repeat: false + onTriggered: { + if (root.autoHide && !root.peekHovered && !root.barHovered) { + root.barWindowVisible = false + } + } + } + Connections { target: ScalingService function onScaleChanged(screenName, scale) { @@ -35,6 +97,11 @@ Variants { WlrLayershell.namespace: "noctalia-bar" + WlrLayershell.exclusionMode: root.autoHide ? ExclusionMode.Ignore : ExclusionMode.Auto + + // When auto-hide is enabled, actually toggle window visibility after animations + visible: root.autoHide ? root.barWindowVisible : true + implicitHeight: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? screen.height : Math.round(Style.barHeight * scaling) implicitWidth: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? Math.round(Style.barHeight * scaling) : screen.width color: Color.transparent @@ -61,10 +128,61 @@ Variants { } } + // Wrapper for animations when hiding/showing Item { + id: barContainer anchors.fill: parent clip: true + opacity: root.hidden ? 0.0 : 1.0 + + // Slide distance depends on bar orientation + readonly property real offX: (function () { + switch (Settings.data.bar.position) { + case "left": + return -barContainer.width + case "right": + return barContainer.width + default: + return 0 + } + })() + readonly property real offY: (function () { + switch (Settings.data.bar.position) { + case "top": + return -barContainer.height + case "bottom": + return barContainer.height + default: + return 0 + } + })() + + transform: Translate { + id: slide + x: root.hidden ? barContainer.offX : 0 + y: root.hidden ? barContainer.offY : 0 + Behavior on x { + NumberAnimation { + duration: root.hidden ? root.hideAnimationDuration : root.showAnimationDuration + easing.type: Easing.InOutCubic + } + } + Behavior on y { + NumberAnimation { + duration: root.hidden ? root.hideAnimationDuration : root.showAnimationDuration + easing.type: Easing.InOutCubic + } + } + } + + Behavior on opacity { + NumberAnimation { + duration: root.hidden ? root.hideAnimationDuration : root.showAnimationDuration + easing.type: Easing.InOutQuad + } + } + // Background fill with shadow Rectangle { id: bar @@ -79,7 +197,7 @@ Variants { MouseArea { anchors.fill: parent acceptedButtons: Qt.RightButton - hoverEnabled: false + hoverEnabled: true preventStealing: true onClicked: function (mouse) { if (mouse.button === Qt.RightButton) { @@ -88,6 +206,21 @@ Variants { mouse.accepted = true } } + onEntered: { + root.barHovered = true + if (root.autoHide) { + showTimer.stop() + hideTimer.stop() + root.barWindowVisible = true + root.hidden = false + } + } + onExited: { + root.barHovered = false + if (root.autoHide && !root.peekHovered) { + hideTimer.restart() + } + } } Loader { @@ -260,5 +393,52 @@ Variants { } } } + + // Peek window to reveal the bar when hovering at the screen edge + Loader { + id: peekLoader + active: root.modelData && root.autoHide + + sourceComponent: PanelWindow { + id: peekWindow + screen: root.modelData || null + color: Color.transparent + focusable: false + + WlrLayershell.namespace: "noctalia-bar-peek" + // Do not reserve space; keep as pure overlay so work area never changes + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + + anchors { + top: Settings.data.bar.position === "top" + bottom: Settings.data.bar.position === "bottom" + left: Settings.data.bar.position === "left" + right: Settings.data.bar.position === "right" + } + + // 1px reveal strip along the relevant edge + implicitHeight: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? screen.height : 1 + implicitWidth: (Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom") ? screen.width : 1 + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: { + root.peekHovered = true + if (root.autoHide && root.hidden) { + showTimer.restart() + } + } + onExited: { + root.peekHovered = false + if (root.autoHide && !root.barHovered) { + hideTimer.restart() + } + } + } + } + } } } diff --git a/Modules/Settings/Tabs/BarTab.qml b/Modules/Settings/Tabs/BarTab.qml index fff686ca..be5687ec 100644 --- a/Modules/Settings/Tabs/BarTab.qml +++ b/Modules/Settings/Tabs/BarTab.qml @@ -120,6 +120,14 @@ ColumnLayout { onToggled: checked => Settings.data.bar.floating = checked } + NToggle { + Layout.fillWidth: true + label: "Auto Hide" + description: "Automatically hide the bar when not in use." + checked: Settings.data.bar.autoHide + onToggled: checked => Settings.data.bar.autoHide = checked + } + // Floating bar options - only show when floating is enabled ColumnLayout { visible: Settings.data.bar.floating From afcad54fec06f4748da2b163e45e1eed14d4b884 Mon Sep 17 00:00:00 2001 From: dwuggh Date: Sun, 12 Oct 2025 03:37:26 +0800 Subject: [PATCH 2/6] respect the global disable animation option --- Modules/Bar/Bar.qml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 8a7d3fc3..8c40af84 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -26,8 +26,9 @@ Variants { property bool peekHovered: false // Controls PanelWindow visibility while auto-hide is enabled property bool barWindowVisible: !autoHide - readonly property int hideDelay: 500 - readonly property int showDelay: 120 + // Respect global animation toggle: no delays when animations are disabled + readonly property int hideDelay: Settings.data.general.animationDisabled ? 0 : 500 + readonly property int showDelay: Settings.data.general.animationDisabled ? 0 : 120 readonly property int hideAnimationDuration: Style.animationNormal readonly property int showAnimationDuration: Style.animationNormal From 32ffaf599b209c7e9ab3854e4d5366abee37803a Mon Sep 17 00:00:00 2001 From: dwuggh Date: Sun, 12 Oct 2025 03:50:22 +0800 Subject: [PATCH 3/6] respect settings order --- Commons/Settings.qml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 31143431..a9ff52c9 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -134,13 +134,15 @@ Singleton { property list monitors: [] property string density: "default" // "compact", "default", "comfortable" property bool showCapsule: true - property bool autoHide: false // Floating bar settings property bool floating: false property real marginVertical: 0.25 property real marginHorizontal: 0.25 + // auto hiding bar settings + property bool autoHide: false + // Widget configuration for modular bar system property JsonObject widgets widgets: JsonObject { From abd9586defd7234487f00992918189e0f59de8e4 Mon Sep 17 00:00:00 2001 From: dwuggh Date: Sun, 12 Oct 2025 04:00:45 +0800 Subject: [PATCH 4/6] fix(bar): keep bar visible while hovering any widget by using HoverHandler on container instead of relying on background MouseArea Problem: Parent MouseArea sat below bar content, so when the pointer hovered a child widget that handled hover, the parent did not see hover and barHovered became false, causing auto-hide to trigger unexpectedly. Solution: Add a non-invasive HoverHandler on the bar container to drive barHovered, without stealing events from child widgets. Remove hover handling from the background MouseArea (kept for right-click). Also respects auto-hide timers (cancel on enter; restart on exit). --- Modules/Bar/Bar.qml | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 8c40af84..593bfe15 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -184,6 +184,24 @@ Variants { } } + // Mark bar hovered without stealing events from child widgets + HoverHandler { + id: barHoverHandler + onHoveredChanged: { + root.barHovered = hovered + if (!root.autoHide) + return + if (hovered) { + showTimer.stop() + hideTimer.stop() + root.barWindowVisible = true + root.hidden = false + } else if (!root.peekHovered) { + hideTimer.restart() + } + } + } + // Background fill with shadow Rectangle { id: bar @@ -198,7 +216,7 @@ Variants { MouseArea { anchors.fill: parent acceptedButtons: Qt.RightButton - hoverEnabled: true + hoverEnabled: false preventStealing: true onClicked: function (mouse) { if (mouse.button === Qt.RightButton) { @@ -207,21 +225,6 @@ Variants { mouse.accepted = true } } - onEntered: { - root.barHovered = true - if (root.autoHide) { - showTimer.stop() - hideTimer.stop() - root.barWindowVisible = true - root.hidden = false - } - } - onExited: { - root.barHovered = false - if (root.autoHide && !root.peekHovered) { - hideTimer.restart() - } - } } Loader { From 4c6ba6afad578ac9135aff558e83f3ce7c5707b8 Mon Sep 17 00:00:00 2001 From: dwuggh Date: Sun, 12 Oct 2025 18:47:30 +0800 Subject: [PATCH 5/6] use i18n implementation for UI text strings --- 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 ++++ Modules/Settings/Tabs/BarTab.qml | 4 ++-- 7 files changed, 26 insertions(+), 2 deletions(-) diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index 369eee4a..6ba6381a 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -266,6 +266,10 @@ "label": "Schwebende Statusleiste", "description": "Statusleiste als schwebende 'Pille' anzeigen. Hinweis: Dies verschiebt die Bildschirmecken an die Ränder." }, + "auto-hide": { + "label": "Automatisch ausblenden", + "description": "Blendet die Leiste automatisch aus, wenn sie nicht verwendet wird." + }, "margins": { "label": "Ränder", "description": "Ränder um die schwebende Statusleiste anpassen.", diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index a90e24a7..361903a8 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -266,6 +266,10 @@ "label": "Floating bar", "description": "Displays the bar as a floating 'pill'. Note: This will move the screen corners to the edges." }, + "auto-hide": { + "label": "Auto hide", + "description": "Automatically hide the bar when not in use." + }, "margins": { "label": "Margins", "description": "Adjust the margins around the floating bar.", diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index 8c79a64f..08d7aac8 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -266,6 +266,10 @@ "label": "Barra flotante", "description": "Muestra la barra como una 'píldora' flotante. Nota: Esto moverá las esquinas de la pantalla a los bordes." }, + "auto-hide": { + "label": "Ocultar automáticamente", + "description": "Oculta automáticamente la barra cuando no esté en uso." + }, "margins": { "label": "Márgenes", "description": "Ajusta los márgenes alrededor de la barra flotante.", diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index 32c01982..5f7bec3a 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -266,6 +266,10 @@ "label": "Barre flottante", "description": "Affiche la barre sous forme de 'pilule' flottante. Note : Ceci déplacera les coins de l'écran vers les bords." }, + "auto-hide": { + "label": "Masquer automatiquement", + "description": "Masquer automatiquement la barre lorsqu’elle n’est pas utilisée." + }, "margins": { "label": "Marges", "description": "Ajustez les marges autour de la barre flottante.", diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index 67fa8ead..e46f67cb 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -266,6 +266,10 @@ "label": "Barra flutuante", "description": "Exibe a barra como uma 'pílula' flutuante. Nota: Isso moverá os cantos da tela para as bordas." }, + "auto-hide": { + "label": "Ocultar automaticamente", + "description": "Oculta automaticamente a barra quando não estiver em uso." + }, "margins": { "label": "Margens", "description": "Ajuste as margens ao redor da barra flutuante.", diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index 9f421d69..c6ff1c0f 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -266,6 +266,10 @@ "label": "浮动状态栏", "description": "将状态栏显示为浮动样式(类似于一个药丸)。注意:这会将屏幕边角移动到边缘。" }, + "auto-hide": { + "label": "自动隐藏", + "description": "在不使用时自动隐藏状态栏。" + }, "margins": { "label": "边距", "description": "调整浮动状态栏周围的边距。", diff --git a/Modules/Settings/Tabs/BarTab.qml b/Modules/Settings/Tabs/BarTab.qml index be5687ec..272740f3 100644 --- a/Modules/Settings/Tabs/BarTab.qml +++ b/Modules/Settings/Tabs/BarTab.qml @@ -122,8 +122,8 @@ ColumnLayout { NToggle { Layout.fillWidth: true - label: "Auto Hide" - description: "Automatically hide the bar when not in use." + label: I18n.tr("settings.bar.appearance.auto-hide.label") + description: I18n.tr("settings.bar.appearance.auto-hide.description") checked: Settings.data.bar.autoHide onToggled: checked => Settings.data.bar.autoHide = checked } From 52f0f6d44cde0e4bf09ba5f715caed1d351700ee Mon Sep 17 00:00:00 2001 From: dwuggh Date: Sun, 12 Oct 2025 19:26:02 +0800 Subject: [PATCH 6/6] fix: hold bar to be visible of a popup from the bar is shown --- Modules/Bar/Bar.qml | 71 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 4 deletions(-) diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 593bfe15..701d71c1 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -24,6 +24,8 @@ Variants { property bool hidden: autoHide property bool barHovered: false property bool peekHovered: false + // Keep bar visible while any panel or popup from the bar is open (global) + readonly property bool holdOpen: PanelService.hasOpenedPopup || (PanelService.openedPanel && ((PanelService.openedPanel.visible === true) || (PanelService.openedPanel.active === true))) // Controls PanelWindow visibility while auto-hide is enabled property bool barWindowVisible: !autoHide // Respect global animation toggle: no delays when animations are disabled @@ -63,7 +65,7 @@ Variants { interval: root.hideDelay repeat: false onTriggered: { - if (root.autoHide && !root.peekHovered && !root.barHovered) { + if (root.autoHide && !root.peekHovered && !root.barHovered && !root.holdOpen) { root.hidden = true unloadTimer.restart() } @@ -76,7 +78,7 @@ Variants { interval: root.hideAnimationDuration repeat: false onTriggered: { - if (root.autoHide && !root.peekHovered && !root.barHovered) { + if (root.autoHide && !root.peekHovered && !root.barHovered && !root.holdOpen) { root.barWindowVisible = false } } @@ -196,7 +198,7 @@ Variants { hideTimer.stop() root.barWindowVisible = true root.hidden = false - } else if (!root.peekHovered) { + } else if (!root.peekHovered && !root.holdOpen) { hideTimer.restart() } } @@ -437,12 +439,73 @@ Variants { } onExited: { root.peekHovered = false - if (root.autoHide && !root.barHovered) { + if (root.autoHide && !root.barHovered && !root.holdOpen) { hideTimer.restart() } } } } } + + // React to panel/popup lifecycle to keep the bar visible during interactions + Connections { + target: PanelService + // Any panel about to open -> show bar and cancel hides + function onWillOpen() { + if (!root.autoHide) + return + showTimer.stop() + hideTimer.stop() + root.barWindowVisible = true + root.hidden = false + } + // Popups opening/closing -> start/stop hide timer appropriately + function onPopupChanged() { + if (!root.autoHide) + return + if (PanelService.hasOpenedPopup) { + showTimer.stop() + hideTimer.stop() + root.barWindowVisible = true + root.hidden = false + } else if (!root.barHovered && !root.peekHovered && !root.holdOpen) { + hideTimer.restart() + } + } + // Track when the main panel closes (openedPanel becomes null) + function onOpenedPanelChanged() { + if (!root.autoHide) + return + if (PanelService.openedPanel !== null) { + showTimer.stop() + hideTimer.stop() + root.barWindowVisible = true + root.hidden = false + } else if (!root.barHovered && !root.peekHovered && !PanelService.hasOpenedPopup) { + hideTimer.restart() + } + } + } + + // Also listen to the current panel's own visible/active changes + Connections { + target: PanelService.openedPanel + enabled: root.autoHide + function onVisibleChanged() { + if (!PanelService.openedPanel) + return + if ((PanelService.openedPanel.visible === true) || (PanelService.openedPanel.active === true)) { + showTimer.stop() + hideTimer.stop() + root.barWindowVisible = true + root.hidden = false + } else if (!root.barHovered && !root.peekHovered && !PanelService.hasOpenedPopup) { + hideTimer.restart() + } + } + function onActiveChanged() { + onVisibleChanged() + } + } } }