bar-shell: prevent bar crash on rapid visibility toggle and convert IPCService to singleton

This commit is contained in:
Lemmy
2026-02-12 08:54:32 -05:00
parent 83e9666e8e
commit 81d0a034c8
7 changed files with 112 additions and 16 deletions
+7 -3
View File
@@ -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
+34 -2
View File
@@ -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)
+4
View File
@@ -49,6 +49,10 @@ PanelWindow {
PanelService.registerPopupMenuWindow(screen, root);
}
Component.onDestruction: {
PanelService.unregisterPopupMenuWindow(screen);
}
// Load TrayMenu as the default content
Loader {
id: trayMenuLoader
+13 -6
View File
@@ -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") {
+43
View File
@@ -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");
}
+9
View File
@@ -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)
+2 -5
View File
@@ -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 {