bar: auto hide implementation

This commit is contained in:
Lemmy
2026-01-30 21:57:50 -05:00
parent 9f8ac95dd8
commit d17cbf2e00
13 changed files with 446 additions and 5 deletions
+9
View File
@@ -599,6 +599,8 @@
"density-default": "Default",
"density-mini": "Mini",
"density-spacious": "Spacious",
"display-mode-always-visible": "Always Visible",
"display-mode-auto-hide": "Auto-hide",
"type-floating": "Floating",
"type-framed": "Framed",
"type-simple": "Simple"
@@ -733,6 +735,13 @@
"appearance-frame-settings-description": "Adjust frame thickness and inner corner radius",
"appearance-frame-settings-label": "Frame Settings",
"appearance-frame-thickness": "Thickness",
"appearance-auto-hide-delay-description": "Time before bar hides after mouse leaves",
"appearance-auto-hide-delay-label": "Hide Delay",
"appearance-auto-hide-exclusive-note": "Note: Exclusive zone is automatically disabled when auto-hide is active",
"appearance-auto-show-delay-description": "Time before bar shows when mouse enters edge",
"appearance-auto-show-delay-label": "Show Delay",
"appearance-display-mode-description": "Choose when the bar is visible",
"appearance-display-mode-label": "Display Mode",
"appearance-hide-on-overview-description": "Hide the bar and close panels when the compositor overview is active.",
"appearance-hide-on-overview-label": "Hide bar on overview",
"appearance-margins-description": "Adjust the margins around the floating bar.",
+3
View File
@@ -18,6 +18,9 @@
"outerCorners": true,
"exclusive": true,
"hideOnOverview": false,
"displayMode": "always_visible",
"autoHideDelay": 500,
"autoShowDelay": 100,
"widgets": {
"left": [
{
+27
View File
@@ -260,6 +260,33 @@
"subTab": 0,
"subTabLabel": "common.appearance"
},
{
"labelKey": "panels.bar.appearance-display-mode-label",
"descriptionKey": "panels.bar.appearance-display-mode-description",
"widget": "NComboBox",
"tab": 4,
"tabLabel": "panels.bar.title",
"subTab": 0,
"subTabLabel": "common.appearance"
},
{
"labelKey": "panels.bar.appearance-auto-hide-delay-label",
"descriptionKey": "panels.bar.appearance-auto-hide-delay-description",
"widget": "NValueSlider",
"tab": 4,
"tabLabel": "panels.bar.title",
"subTab": 0,
"subTabLabel": "common.appearance"
},
{
"labelKey": "panels.bar.appearance-auto-show-delay-label",
"descriptionKey": "panels.bar.appearance-auto-show-delay-description",
"widget": "NValueSlider",
"tab": 4,
"tabLabel": "panels.bar.title",
"subTab": 0,
"subTabLabel": "common.appearance"
},
{
"labelKey": "panels.bar.appearance-hide-on-overview-label",
"descriptionKey": "panels.bar.appearance-hide-on-overview-description",
+5
View File
@@ -200,6 +200,11 @@ Singleton {
// Hide bar/panels when compositor overview is active
property bool hideOnOverview: false
// Auto-hide settings
property string displayMode: "always_visible" // "always_visible", "auto_hide"
property int autoHideDelay: 500 // ms before hiding after mouse leaves
property int autoShowDelay: 100 // ms before showing when mouse enters
// Widget configuration for modular bar system
property JsonObject widgets
widgets: JsonObject {
+26
View File
@@ -75,6 +75,32 @@ Variants {
}
}
// BarTriggerZone - thin invisible zone to reveal hidden bar
// Always loaded when auto-hide is enabled (it's just 1px, no performance impact)
Loader {
active: {
if (!parent.windowLoaded || !parent.shouldBeActive)
return false;
if (!BarService.effectivelyVisible)
return false;
if (Settings.data.bar.displayMode !== "auto_hide")
return false;
// Check if bar is configured for this screen
var monitors = Settings.data.bar.monitors || [];
return monitors.length === 0 || monitors.includes(modelData?.name);
}
asynchronous: false
sourceComponent: BarTriggerZone {
screen: modelData
}
onLoaded: {
Logger.d("AllScreens", "BarTriggerZone created for", modelData?.name);
}
}
// BarExclusionZone - created after MainScreen has fully loaded
// Disabled when bar is hidden or not configured for this screen
Repeater {
@@ -103,9 +103,20 @@ ShapePath {
readonly property real blMultY: bar ? ShapeCornerHelper.getMultY(bar.bottomLeftCornerState) : 1
readonly property real blRadius: bar ? getCornerRadius(bar.bottomLeftCornerState) : 0
// Auto-hide opacity factor (animates from 1 to 0 when hidden)
property real opacityFactor: (bar && bar.isHidden) ? 0 : 1
Behavior on opacityFactor {
enabled: bar && bar.autoHide
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutQuad
}
}
// ShapePath configuration
strokeWidth: -1 // No stroke, fill only
fillColor: backgroundColor
fillColor: Qt.rgba(backgroundColor.r, backgroundColor.g, backgroundColor.b, backgroundColor.a * opacityFactor)
fillRule: isFramed ? ShapePath.OddEvenFill : ShapePath.WindingFill
// Starting position
+175 -3
View File
@@ -20,6 +20,9 @@ PanelWindow {
// Note: screen property is inherited from PanelWindow and should be set by parent
color: "transparent" // Transparent - background is in MainScreen below
// Make window pass-through when content is unloaded
visible: contentLoaded
Component.onCompleted: {
Logger.d("BarContentWindow", "Bar content window created for screen:", barWindow.screen?.name);
}
@@ -39,6 +42,100 @@ PanelWindow {
readonly property real barMarginV: Math.ceil(barFloating ? Settings.data.bar.marginVertical : 0)
readonly property real barHeight: Style.getBarHeightForScreen(barWindow.screen?.name)
// Auto-hide properties
readonly property bool autoHide: Settings.data.bar.displayMode === "auto_hide"
readonly property int hideDelay: Settings.data.bar.autoHideDelay || 500
readonly property int showDelay: Settings.data.bar.autoShowDelay || 100
property bool isHidden: false
// Hover tracking
property bool barHovered: false
// Check if any panel is open on this screen
readonly property bool panelOpen: PanelService.openedPanel !== null
// Timer for delayed hide
Timer {
id: hideTimer
interval: barWindow.hideDelay
onTriggered: {
if (barWindow.autoHide && !barWindow.barHovered && !barWindow.panelOpen && !BarService.popupOpen) {
BarService.setScreenHidden(barWindow.screen?.name, true);
}
}
}
// Timer for delayed show
Timer {
id: showTimer
interval: barWindow.showDelay
onTriggered: {
// Only show if still hovered (via trigger zone or bar itself)
if (barWindow.autoHide && BarService.isBarHovered(barWindow.screen?.name)) {
BarService.setScreenHidden(barWindow.screen?.name, false);
}
}
}
// React to auto-hide state changes from BarService
Connections {
target: BarService
function onBarAutoHideStateChanged(screenName, hidden) {
Logger.d("BarContentWindow", "onBarAutoHideStateChanged:", screenName, hidden, "my screen:", barWindow.screen?.name);
if (screenName === barWindow.screen?.name) {
barWindow.isHidden = hidden;
}
}
function onBarHoverStateChanged(screenName, hovered) {
if (screenName === barWindow.screen?.name && barWindow.autoHide) {
if (hovered) {
hideTimer.stop();
// If bar is already visible, no need to delay
if (!barWindow.isHidden) {
showTimer.stop();
} else {
// Bar is hidden, use show delay
showTimer.restart();
}
} else if (!barWindow.barHovered && !barWindow.panelOpen) {
showTimer.stop();
hideTimer.restart();
}
}
}
}
// Don't hide when panel is open
onPanelOpenChanged: {
if (panelOpen && autoHide) {
hideTimer.stop();
BarService.setScreenHidden(barWindow.screen?.name, false);
} else if (!panelOpen && autoHide && !barHovered) {
hideTimer.restart();
}
}
// React to popup menu closing
Connections {
target: BarService
function onPopupOpenChanged() {
if (!BarService.popupOpen && barWindow.autoHide && !barWindow.barHovered && !barWindow.panelOpen) {
hideTimer.restart();
}
}
}
// React to displayMode changes
onAutoHideChanged: {
if (!autoHide) {
// Show bar when auto-hide is disabled
hideTimer.stop();
showTimer.stop();
barWindow.isHidden = false;
}
// When auto-hide is enabled, don't immediately hide - wait for mouse to leave
}
// Anchor to the bar's edge
anchors {
top: barPosition === "top" || barIsVertical
@@ -47,6 +144,32 @@ PanelWindow {
right: barPosition === "right" || !barIsVertical
}
// Track if content should be loaded (stays true during fade-out animation)
property bool contentLoaded: !isHidden
// Timer to delay unload until after fade animation
Timer {
id: unloadTimer
interval: Style.animationFast + 50
onTriggered: {
if (barWindow.isHidden) {
barWindow.contentLoaded = false;
}
}
}
// When hidden changes, handle load/unload
onIsHiddenChanged: {
if (isHidden) {
// Start fade out, then unload after animation
unloadTimer.restart();
} else {
// Load immediately when showing
unloadTimer.stop();
contentLoaded = true;
}
}
// Handle floating margins and framed mode offsets
margins {
top: (barPosition === "top") ? barMarginV : (isFramed ? frameThickness : barMarginV)
@@ -59,9 +182,58 @@ PanelWindow {
implicitWidth: barIsVertical ? barHeight : barWindow.screen.width
implicitHeight: barIsVertical ? barWindow.screen.height : barHeight
// Bar content - just the widgets, no background
Bar {
// Bar content loader - unloads when hidden to prevent input
Loader {
id: barLoader
anchors.fill: parent
screen: barWindow.screen
active: barWindow.contentLoaded
sourceComponent: Item {
anchors.fill: parent
// Fade animation
opacity: barWindow.isHidden ? 0 : 1
Behavior on opacity {
enabled: barWindow.autoHide
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutQuad
}
}
Bar {
id: barContent
anchors.fill: parent
screen: barWindow.screen
// Hover detection area overlaid on bar
MouseArea {
id: hoverArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
propagateComposedEvents: true
onEntered: {
barWindow.barHovered = true;
BarService.setScreenHovered(barWindow.screen?.name, true);
if (barWindow.autoHide) {
hideTimer.stop();
showTimer.restart();
}
}
onExited: {
barWindow.barHovered = false;
BarService.setScreenHovered(barWindow.screen?.name, false);
if (barWindow.autoHide && !barWindow.panelOpen) {
showTimer.stop();
hideTimer.restart();
}
}
}
}
}
}
}
+3 -1
View File
@@ -18,6 +18,7 @@ PanelWindow {
property real thickness: (edge === Settings.getBarPositionForScreen(screen?.name)) ? Style.getBarHeightForScreen(screen?.name) : (Settings.data.bar.frameThickness ?? 12)
readonly property bool exclusive: Settings.data.bar.exclusive
readonly property bool autoHide: Settings.data.bar.displayMode === "auto_hide"
readonly property bool barFloating: Settings.data.bar.floating || false
readonly property real barMarginH: (barFloating && edge === Settings.getBarPositionForScreen(screen?.name)) ? Math.ceil(Settings.data.bar.marginHorizontal) : 0
readonly property real barMarginV: (barFloating && edge === Settings.getBarPositionForScreen(screen?.name)) ? Math.ceil(Settings.data.bar.marginVertical) : 0
@@ -31,7 +32,8 @@ PanelWindow {
// Wayland layer shell configuration
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.namespace: "noctalia-bar-exclusion-" + edge + "-" + (screen?.name || "unknown")
WlrLayershell.exclusionMode: exclusive ? ExclusionMode.Auto : ExclusionMode.Ignore
// When auto-hide is enabled, never reserve space
WlrLayershell.exclusionMode: autoHide ? ExclusionMode.Ignore : (exclusive ? ExclusionMode.Auto : ExclusionMode.Ignore)
// Anchor based on specified edge
anchors {
+58
View File
@@ -0,0 +1,58 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services.UI
/**
* BarTriggerZone - Thin invisible window at screen edge to reveal hidden bar
*
* This window is only active when the bar is in auto-hide mode and hidden.
* When the mouse enters this zone, it triggers the bar to show.
*/
PanelWindow {
id: root
readonly property string barPosition: Settings.getBarPositionForScreen(screen?.name)
readonly property bool barIsVertical: barPosition === "left" || barPosition === "right"
readonly property int triggerSize: 1
// Invisible trigger zone
color: "transparent"
focusable: false
WlrLayershell.namespace: "noctalia-bar-trigger-" + (screen?.name || "unknown")
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.exclusionMode: ExclusionMode.Ignore
// Anchor to bar's edge
anchors {
top: barPosition === "top" || barIsVertical
bottom: barPosition === "bottom" || barIsVertical
left: barPosition === "left" || !barIsVertical
right: barPosition === "right" || !barIsVertical
}
// Size based on orientation - thin strip at edge
implicitWidth: barIsVertical ? triggerSize : 0
implicitHeight: !barIsVertical ? triggerSize : 0
MouseArea {
id: triggerArea
anchors.fill: parent
hoverEnabled: true
onEntered: {
// Signal hover - BarContentWindow will handle the show delay
BarService.setScreenHovered(root.screen?.name, true);
}
onExited: {
BarService.setScreenHovered(root.screen?.name, false);
}
}
Component.onCompleted: {
Logger.d("BarTriggerZone", "Created for screen:", screen?.name);
}
}
+13
View File
@@ -375,6 +375,19 @@ PanelWindow {
readonly property real barMarginV: barFloating ? Math.floor(Settings.data.bar.marginVertical) : 0
readonly property real barHeight: Style.getBarHeightForScreen(screen?.name)
// Auto-hide properties (read by AllBackgrounds for background fade)
readonly property bool autoHide: Settings.data.bar.displayMode === "auto_hide"
property bool isHidden: false
Connections {
target: BarService
function onBarAutoHideStateChanged(screenName, hidden) {
if (screenName === barPlaceholder.screen?.name) {
barPlaceholder.isHidden = hidden;
}
}
}
// Expose bar dimensions directly on this Item for BarBackground
// Use screen dimensions directly
x: {
+2
View File
@@ -85,6 +85,7 @@ PanelWindow {
function open() {
visible = true;
BarService.popupOpen = true;
}
// Show a context menu (temporarily replaces TrayMenu as content)
@@ -126,6 +127,7 @@ PanelWindow {
function close() {
visible = false;
BarService.popupOpen = false;
// Call close/hide method on current content
if (contentItem) {
if (typeof contentItem.hideMenu === "function") {
@@ -238,6 +238,68 @@ ColumnLayout {
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginS
}
NComboBox {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-display-mode-label") ?? "Display Mode"
description: I18n.tr("panels.bar.appearance-display-mode-description") ?? "Choose when the bar is visible"
model: [
{
"key": "always_visible",
"name": I18n.tr("options.bar.display-mode-always-visible") ?? "Always Visible"
},
{
"key": "auto_hide",
"name": I18n.tr("options.bar.display-mode-auto-hide") ?? "Auto-hide"
}
]
currentKey: Settings.data.bar.displayMode
defaultValue: Settings.getDefaultValue("bar.displayMode")
onSelected: key => Settings.data.bar.displayMode = key
}
ColumnLayout {
visible: Settings.data.bar.displayMode === "auto_hide"
spacing: Style.marginS
Layout.fillWidth: true
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-auto-hide-delay-label") ?? "Hide Delay"
description: I18n.tr("panels.bar.appearance-auto-hide-delay-description") ?? "Time before bar hides after mouse leaves"
from: 100
to: 2000
stepSize: 100
value: Settings.data.bar.autoHideDelay
defaultValue: Settings.getDefaultValue("bar.autoHideDelay")
onMoved: value => Settings.data.bar.autoHideDelay = value
text: Settings.data.bar.autoHideDelay + "ms"
}
NValueSlider {
Layout.fillWidth: true
label: I18n.tr("panels.bar.appearance-auto-show-delay-label") ?? "Show Delay"
description: I18n.tr("panels.bar.appearance-auto-show-delay-description") ?? "Time before bar shows when mouse enters edge"
from: 0
to: 500
stepSize: 50
value: Settings.data.bar.autoShowDelay
defaultValue: Settings.getDefaultValue("bar.autoShowDelay")
onMoved: value => Settings.data.bar.autoShowDelay = value
text: Settings.data.bar.autoShowDelay + "ms"
}
NLabel {
visible: Settings.data.bar.exclusive
label: ""
description: I18n.tr("panels.bar.appearance-auto-hide-exclusive-note") ?? "Note: Exclusive zone is automatically disabled when auto-hide is active"
}
}
NToggle {
Layout.fillWidth: true
visible: CompositorService.isNiri
+51
View File
@@ -34,6 +34,57 @@ Singleton {
signal activeWidgetsChanged
signal barReadyChanged(string screenName)
signal barAutoHideStateChanged(string screenName, bool hidden)
signal barHoverStateChanged(string screenName, bool hovered)
// Track if a popup menu is open from the bar (prevents auto-hide)
property bool popupOpen: false
// Auto-hide state per screen: { screenName: { hovered: bool, hidden: bool } }
property var screenAutoHideState: ({})
// Get or create auto-hide state for a screen
function getOrCreateAutoHideState(screenName) {
if (!screenAutoHideState[screenName]) {
screenAutoHideState[screenName] = {
"hovered": false,
"hidden": Settings.data.bar.displayMode === "auto_hide"
};
}
return screenAutoHideState[screenName];
}
// Set hover state for a screen
function setScreenHovered(screenName, hovered) {
var state = getOrCreateAutoHideState(screenName);
if (state.hovered !== hovered) {
state.hovered = hovered;
screenAutoHideState = Object.assign({}, screenAutoHideState);
barHoverStateChanged(screenName, hovered);
}
}
// Set hidden state for a screen
function setScreenHidden(screenName, hidden) {
var state = getOrCreateAutoHideState(screenName);
if (state.hidden !== hidden) {
state.hidden = hidden;
screenAutoHideState = Object.assign({}, screenAutoHideState);
barAutoHideStateChanged(screenName, hidden);
}
}
// Check if bar is hidden on a screen
function isBarHidden(screenName) {
var state = screenAutoHideState[screenName];
return state ? state.hidden : false;
}
// Check if bar is hovered on a screen
function isBarHovered(screenName) {
var state = screenAutoHideState[screenName];
return state ? state.hovered : false;
}
Component.onCompleted: {
Logger.i("BarService", "Service started");