diff --git a/Modules/MainScreen/AllScreens.qml b/Modules/MainScreen/AllScreens.qml index 32aa0302f..699a01155 100644 --- a/Modules/MainScreen/AllScreens.qml +++ b/Modules/MainScreen/AllScreens.qml @@ -54,9 +54,12 @@ Variants { } // Bar content in separate windows to prevent fullscreen redraws + // Note: Window stays alive when bar is hidden (visible=false) to avoid + // rapid Wayland surface destruction/creation that can crash compositors. + // Content is debounce-unloaded inside BarContentWindow. Loader { active: { - if (!parent.windowLoaded || !parent.shouldBeActive || !BarService.effectivelyVisible) + if (!parent.windowLoaded || !parent.shouldBeActive) return false; // Check if bar is configured for this screen @@ -130,7 +133,8 @@ Variants { } // PopupMenuWindow - reusable popup window for both tray menus and context menus - // Active when bar is visible on this screen, OR when desktop widgets edit mode is active + // Stays alive when bar is hidden to avoid Wayland surface churn crashes. + // PopupMenuWindow manages its own visibility internally. Loader { active: { // Desktop widgets edit mode needs popup window on ALL screens @@ -139,7 +143,7 @@ Variants { } // Normal bar-based condition - if (!parent.windowLoaded || !parent.shouldBeActive || !BarService.effectivelyVisible) + if (!parent.windowLoaded || !parent.shouldBeActive) return false; // Check if bar is configured for this screen diff --git a/Modules/MainScreen/BarContentWindow.qml b/Modules/MainScreen/BarContentWindow.qml index 4d0556009..fafdb14bf 100644 --- a/Modules/MainScreen/BarContentWindow.qml +++ b/Modules/MainScreen/BarContentWindow.qml @@ -20,8 +20,8 @@ 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 + // Make window pass-through when content is unloaded or bar is hidden via IPC + visible: contentLoaded && BarService.effectivelyVisible Component.onCompleted: { Logger.d("BarContentWindow", "Bar content window created for screen:", barWindow.screen?.name); @@ -169,10 +169,42 @@ PanelWindow { } else { // Load immediately when showing unloadTimer.stop(); + deferredUnloadTimer.stop(); contentLoaded = true; } } + // Debounced content unload when bar visibility is toggled. + // Rapid toggles keep widgets alive; content is only unloaded after the bar + // has been continuously hidden for the debounce period. + Timer { + id: deferredUnloadTimer + interval: 1000 + onTriggered: { + if (!BarService.effectivelyVisible) { + barWindow.barHovered = false; + barWindow.contentLoaded = false; + Logger.d("BarContentWindow", "Debounced content unload for screen:", barWindow.screen?.name); + } + } + } + + Connections { + target: BarService + function onEffectivelyVisibleChanged() { + if (!BarService.effectivelyVisible) { + // Bar hidden — start debounced unload + deferredUnloadTimer.restart(); + } else { + // Bar shown — cancel pending unload, ensure content is loaded + deferredUnloadTimer.stop(); + if (!barWindow.isHidden) { + barWindow.contentLoaded = true; + } + } + } + } + // Handle floating margins and framed mode offsets margins { top: (barPosition === "top") ? barMarginV : (isFramed ? frameThickness : barMarginV) diff --git a/Modules/MainScreen/PopupMenuWindow.qml b/Modules/MainScreen/PopupMenuWindow.qml index bc7ee7ff8..eb034a451 100644 --- a/Modules/MainScreen/PopupMenuWindow.qml +++ b/Modules/MainScreen/PopupMenuWindow.qml @@ -49,6 +49,10 @@ PanelWindow { PanelService.registerPopupMenuWindow(screen, root); } + Component.onDestruction: { + PanelService.unregisterPopupMenuWindow(screen); + } + // Load TrayMenu as the default content Loader { id: trayMenuLoader diff --git a/Services/Control/IPCService.qml b/Services/Control/IPCService.qml index fc13f9513..74c217390 100644 --- a/Services/Control/IPCService.qml +++ b/Services/Control/IPCService.qml @@ -1,3 +1,5 @@ +pragma Singleton + import QtQuick import Quickshell import Quickshell.Io @@ -16,22 +18,27 @@ import qs.Services.System import qs.Services.Theming import qs.Services.UI -Item { +Singleton { id: root - // Screen detector passed from shell.qml - required property CurrentScreenDetector screenDetector + // Screen detector, set via init() + property var screenDetector: null + + function init(detector) { + root.screenDetector = detector; + Logger.i("IPCService", "Service started"); + } IpcHandler { target: "bar" function toggle() { - BarService.isVisible = !BarService.isVisible; + BarService.toggleVisibility(); } function hideBar() { - BarService.isVisible = false; + BarService.hide(); } function showBar() { - BarService.isVisible = true; + BarService.show(); } function setDisplayMode(mode: string) { if (mode === "always_visible" || mode === "non_exclusive" || mode === "auto_hide") { diff --git a/Services/UI/BarService.qml b/Services/UI/BarService.qml index f9d1e012b..0327cfd55 100644 --- a/Services/UI/BarService.qml +++ b/Services/UI/BarService.qml @@ -86,6 +86,49 @@ Singleton { return state ? state.hovered : false; } + // Toggle bar visibility. In auto-hide mode, toggles the auto-hide state + // on all screens instead of setting the global isVisible flag. + function toggleVisibility() { + if (Settings.data.bar.displayMode === "auto_hide") { + // Check if any screen is currently visible (not hidden) + var anyVisible = false; + for (var screenName in screenAutoHideState) { + if (!screenAutoHideState[screenName].hidden) { + anyVisible = true; + break; + } + } + // Toggle all screens + for (var screenName in screenAutoHideState) { + setScreenHidden(screenName, anyVisible); + } + } else { + isVisible = !isVisible; + } + } + + // Show bar. In auto-hide mode, un-hides on all screens. + function show() { + if (Settings.data.bar.displayMode === "auto_hide") { + for (var screenName in screenAutoHideState) { + setScreenHidden(screenName, false); + } + } else { + isVisible = true; + } + } + + // Hide bar. In auto-hide mode, hides on all screens. + function hide() { + if (Settings.data.bar.displayMode === "auto_hide") { + for (var screenName in screenAutoHideState) { + setScreenHidden(screenName, true); + } + } else { + isVisible = false; + } + } + Component.onCompleted: { Logger.i("BarService", "Service started"); } diff --git a/Services/UI/PanelService.qml b/Services/UI/PanelService.qml index 2a49ce294..89993f94b 100644 --- a/Services/UI/PanelService.qml +++ b/Services/UI/PanelService.qml @@ -65,6 +65,15 @@ Singleton { popupMenuWindowRegistered(screen); } + // Unregister popup menu window for a screen (called on destruction) + function unregisterPopupMenuWindow(screen) { + if (!screen) + return; + var key = screen.name; + delete popupMenuWindows[key]; + Logger.d("PanelService", "Unregistered popup menu window for screen:", key); + } + // Get popup menu window for a screen function getPopupMenuWindow(screen) { if (!screen) diff --git a/shell.qml b/shell.qml index f2c0aab12..af4d6661d 100644 --- a/shell.qml +++ b/shell.qml @@ -113,6 +113,7 @@ ShellRoot { GitHubService.init(); SupporterService.init(); CustomButtonIPCService.init(); + IPCService.init(screenDetector); }); delayedInitTimer.running = true; @@ -145,11 +146,7 @@ ShellRoot { id: screenDetector } - // IPCService is treated as a service but it must be in graphics scene. - IPCService { - id: ipcService - screenDetector: screenDetector - } + // IPCService is a singleton, initialized via init() in deferred services block // Container for plugins Main.qml instances (must be in graphics scene) Item {