diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index 9cddd5c7..cd218ade 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -212,6 +212,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 83e9d189..cb3c00c4 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -213,6 +213,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 4646bdcf..e1feaa8a 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -212,6 +212,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 bfd5e07e..0ec0ddc7 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -212,6 +212,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 dbc6ab4e..5e39bc81 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -212,6 +212,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 251681be..79dee7d6 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -212,6 +212,10 @@ "label": "浮动状态栏", "description": "将状态栏显示为浮动样式(类似于一个药丸)。注意:这会将屏幕边角移动到边缘。" }, + "auto-hide": { + "label": "自动隐藏", + "description": "在不使用时自动隐藏状态栏。" + }, "margins": { "label": "边距", "description": "调整浮动状态栏周围的边距。", diff --git a/Assets/settings-default.json b/Assets/settings-default.json index 77f04499..63418a1b 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -9,6 +9,8 @@ "floating": false, "marginVertical": 0.25, "marginHorizontal": 0.25, + "autoHide": false, + "widgets": { "left": [ { diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 01d631e4..167254db 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -140,6 +140,9 @@ Singleton { 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 { diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index fa61bd95..8cc33280 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -17,6 +17,81 @@ Variants { id: root 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 + // 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 + 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 + + // 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.holdOpen) { + 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.holdOpen) { + root.barWindowVisible = false + } + } + } + + Connections { + target: ScalingService + function onScaleChanged(screenName, scale) { + if ((modelData !== null) && (screenName === modelData.name)) { + scaling = scale + } + } + } active: BarService.isVisible && modelData && modelData.name ? (Settings.data.bar.monitors.includes(modelData.name) || (Settings.data.bar.monitors.length === 0)) : false @@ -25,6 +100,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 : Style.barHeight implicitWidth: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? Style.barHeight : screen.width color: Color.transparent @@ -51,10 +131,79 @@ 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 + } + } + + // 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 && !root.holdOpen) { + hideTimer.restart() + } + } + } + // Background fill with shadow Rectangle { id: bar @@ -244,5 +393,113 @@ 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 && !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() + } + } } } diff --git a/Modules/Settings/Tabs/BarTab.qml b/Modules/Settings/Tabs/BarTab.qml index e1a9421a..2b466c8a 100644 --- a/Modules/Settings/Tabs/BarTab.qml +++ b/Modules/Settings/Tabs/BarTab.qml @@ -102,6 +102,14 @@ ColumnLayout { onToggled: checked => Settings.data.bar.floating = checked } + NToggle { + Layout.fillWidth: true + 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 + } + // Floating bar options - only show when floating is enabled ColumnLayout { visible: Settings.data.bar.floating