From 62000eec1e93f585ab8094b8196907d7f317bd77 Mon Sep 17 00:00:00 2001 From: dwuggh Date: Sun, 12 Oct 2025 02:26:44 +0800 Subject: [PATCH] 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