pragma Singleton import QtQuick import Quickshell import qs.Commons import qs.Services.Compositor import qs.Services.UI Singleton { id: root property bool isVisible: true // Computed visibility that factors in compositor overview state readonly property bool effectivelyVisible: { if (!isVisible) { return false; } if (Settings.data.bar.hideOnOverview && CompositorService.overviewActive) { return false; } return true; } property var readyBars: ({}) // Revision counter - increment when widget list structure changes (add/remove/reorder) // This triggers Bar.qml to re-sync its ListModels property int widgetsRevision: 0 // Registry to store actual widget instances // Key format: "screenName|section|widgetId|index" property var widgetInstances: ({}) 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.getBarDisplayModeForScreen(screenName) === "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; } // Toggle bar visibility. In auto-hide mode, toggles the per-screen hidden // state without touching isVisible (so hover-to-show still works). // For non-auto-hide screens, toggles the global isVisible flag. function toggleVisibility() { // Check if any auto-hide screen is currently visible var anyAutoHideVisible = false; var hasAutoHideScreens = false; for (var screenName in screenAutoHideState) { if (Settings.getBarDisplayModeForScreen(screenName) === "auto_hide") { hasAutoHideScreens = true; if (!screenAutoHideState[screenName].hidden) { anyAutoHideVisible = true; break; } } } // Toggle auto-hide screens (per-screen hidden state only) if (hasAutoHideScreens) { for (var screenName in screenAutoHideState) { if (Settings.getBarDisplayModeForScreen(screenName) === "auto_hide") { setScreenHidden(screenName, anyAutoHideVisible); } } } // Only toggle global visibility when no auto-hide screens exist, // otherwise it would permanently disable hover-to-show if (!hasAutoHideScreens) { isVisible = !isVisible; } } // Show bar. In auto-hide mode, un-hides on screens with auto-hide enabled. // The bar stays visible until the user hovers and moves away. function show() { // Show auto-hide screens for (var screenName in screenAutoHideState) { if (Settings.getBarDisplayModeForScreen(screenName) === "auto_hide") { setScreenHidden(screenName, false); } } // Set global visibility (affects non-auto-hide screens) isVisible = true; } // Hide bar. In auto-hide mode, sets per-screen hidden state without touching // isVisible so hover-to-show still works. For non-auto-hide screens, sets // global visibility to false. function hide() { var hasAutoHideScreens = false; for (var screenName in screenAutoHideState) { if (Settings.getBarDisplayModeForScreen(screenName) === "auto_hide") { setScreenHidden(screenName, true); hasAutoHideScreens = true; } } // Only set global visibility off when no auto-hide screens exist, // otherwise it would permanently disable hover-to-show if (!hasAutoHideScreens) { isVisible = false; } } // Temporarily show the bar, then auto-hide after the configured delay. // Uses the same pattern as workspace switch: show, then emit unhover // to start the hide timer. function peek() { for (var screenName in screenAutoHideState) { if (Settings.getBarDisplayModeForScreen(screenName) === "auto_hide") { setScreenHidden(screenName, false); if (!isBarHovered(screenName)) { barHoverStateChanged(screenName, false); } } } } Component.onCompleted: { Logger.i("BarService", "Service started"); } // Bump widgetsRevision when settings are reloaded from an external file change // so Bar.qml re-syncs its widget ListModels with the updated widget configuration Connections { target: Settings function onSettingsReloaded() { Logger.d("BarService", "Settings reloaded externally, bumping widgetsRevision"); root.widgetsRevision++; } } // update bar's hidden state when mode changes Connections { target: Settings.data.bar function onDisplayModeChanged() { Logger.d("BarService", "Display mode changed to:", Settings.data.bar.displayMode); // Only affect screens without displayMode overrides for (let screenName in screenAutoHideState) { if (!Settings.hasScreenOverride(screenName, "displayMode")) { var displayMode = Settings.getBarDisplayModeForScreen(screenName); if (displayMode === "auto_hide") { setScreenHidden(screenName, true); } else { if (screenAutoHideState[screenName].hidden) { setScreenHidden(screenName, false); } } } } } function onScreenOverridesChanged() { Logger.d("BarService", "Screen overrides changed, re-evaluating auto-hide states"); // Re-evaluate auto-hide state for all screens for (let screenName in screenAutoHideState) { var displayMode = Settings.getBarDisplayModeForScreen(screenName); if (displayMode === "auto_hide") { if (!screenAutoHideState[screenName].hidden) { setScreenHidden(screenName, true); } } else { if (screenAutoHideState[screenName].hidden) { setScreenHidden(screenName, false); } } } } } // Track last workspace ID to detect actual workspace changes property var lastWorkspaceId: null // Debounce rapid workspace switches to reduce load/unload races (SIGSEGV in QV4) property string _pendingWorkspaceScreen: "" Timer { id: workspaceDebounceTimer interval: 80 repeat: false onTriggered: { var screen = root._pendingWorkspaceScreen; root._pendingWorkspaceScreen = ""; if (screen) { setScreenHidden(screen, false); if (!root.isBarHovered(screen)) { barHoverStateChanged(screen, false); } } } } // Workspace switch handler - directly show bar on the focused workspace screen Connections { target: CompositorService function onWorkspaceChanged() { if (!Settings.data.bar.showOnWorkspaceSwitch) return; if (Settings.data.bar.displayMode !== "auto_hide") return; var ws = CompositorService.getCurrentWorkspace(); if (!ws || !ws.output) { return; } // Only trigger if workspace actually changed var currentWsId = ws.id; if (currentWsId === root.lastWorkspaceId) { return; } root.lastWorkspaceId = currentWsId; var screenName = ws.output || ""; Logger.d("BarService", "Workspace switched to:", currentWsId, "on screen:", screenName); // Debounce: rapid switches (e.g. external monitor ↔ laptop) cause overlapping // bar load/unload; 80ms delay coalesces them and reduces QV4 incubation races root._pendingWorkspaceScreen = screenName; workspaceDebounceTimer.restart(); } } // Function for the Bar to call when it's ready function registerBar(screenName) { if (!readyBars[screenName]) { readyBars[screenName] = true; Logger.d("BarService", "Bar is ready on screen:", screenName); barReadyChanged(screenName); } } // Function for the Dock to check if the bar is ready function isBarReady(screenName) { return readyBars[screenName] || false; } // Register a widget instance function registerWidget(screenName, section, widgetId, index, instance) { const key = [screenName, section, widgetId, index].join("|"); widgetInstances[key] = { "key": key, "screenName": screenName, "section": section, "widgetId": widgetId, "index": index, "instance": instance }; Logger.d("BarService", "Registered widget:", key); root.activeWidgetsChanged(); } // Unregister a widget instance function unregisterWidget(screenName, section, widgetId, index) { const key = [screenName, section, widgetId, index].join("|"); delete widgetInstances[key]; Logger.d("BarService", "Unregistered widget:", key); root.activeWidgetsChanged(); } // Lookup a specific widget instance (returns the actual QML instance) function lookupWidget(widgetId, screenName = null, section = null, index = null) { // If looking for a specific instance if (screenName && section !== null) { for (var key in widgetInstances) { var widget = widgetInstances[key]; if (!widget) continue; if (widget.widgetId === widgetId && widget.screenName === screenName && widget.section === section) { if (index === null) { return widget.instance; } else if (widget.index == index) { return widget.instance; } } } } // Return first match if no specific screen/section specified for (var key in widgetInstances) { var widget = widgetInstances[key]; if (!widget) continue; if (widget.widgetId === widgetId) { if (!screenName || widget.screenName === screenName) { if (section === null || widget.section === section) { return widget.instance; } } } } return undefined; } // Get all instances of a widget type function getAllWidgetInstances(widgetId = null, screenName = null, section = null) { var instances = []; for (var key in widgetInstances) { var widget = widgetInstances[key]; if (!widget) continue; var matches = true; if (widgetId && widget.widgetId !== widgetId) matches = false; if (screenName && widget.screenName !== screenName) matches = false; if (section !== null && widget.section !== section) matches = false; if (matches) { instances.push(widget.instance); } } return instances; } // Get widget with full metadata function getWidgetWithMetadata(widgetId, screenName = null, section = null) { for (var key in widgetInstances) { var widget = widgetInstances[key]; if (!widget) continue; if (widget.widgetId === widgetId) { if (!screenName || widget.screenName === screenName) { if (section === null || widget.section === section) { return widget; } } } } return undefined; } // Get all widgets in a specific section function getWidgetsBySection(section, screenName = null) { var widgetEntries = []; for (var key in widgetInstances) { var widget = widgetInstances[key]; if (!widget) continue; if (widget.section === section) { if (!screenName || widget.screenName === screenName) { widgetEntries.push(widget); } } } // Sort by index to maintain order widgetEntries.sort(function (a, b) { return (a.index || 0) - (b.index || 0); }); // Return just the instances return widgetEntries.map(function (w) { return w.instance; }); } // Get all registered widgets (for debugging) function getAllRegisteredWidgets() { var result = []; for (var key in widgetInstances) { var widget = widgetInstances[key]; if (!widget) continue; result.push({ "key": key, "widgetId": widget.widgetId, "section": widget.section, "screenName": widget.screenName, "index": widget.index }); } return result; } // Check if a widget type exists in a section function hasWidget(widgetId, section = null, screenName = null) { for (var key in widgetInstances) { var widget = widgetInstances[key]; if (!widget) continue; if (widget.widgetId === widgetId) { if (section === null || widget.section === section) { if (!screenName || widget.screenName === screenName) { return true; } } } } return false; } // Unregister all widget instances for a plugin (used during hot reload) // Note: We don't destroy instances here - the Loader manages that when the component is unregistered function destroyPluginWidgetInstances(pluginId) { var widgetId = "plugin:" + pluginId; var keysToRemove = []; // Find all instances of this plugin's widget for (var key in widgetInstances) { var widget = widgetInstances[key]; if (widget && widget.widgetId === widgetId) { keysToRemove.push(key); Logger.d("BarService", "Unregistering plugin widget instance:", key); } } // Remove from registry for (var i = 0; i < keysToRemove.length; i++) { delete widgetInstances[keysToRemove[i]]; } if (keysToRemove.length > 0) { Logger.i("BarService", "Unregistered", keysToRemove.length, "instance(s) of plugin widget:", widgetId); root.activeWidgetsChanged(); } } // Get pill direction for a widget instance function getPillDirection(widgetInstance) { try { if (widgetInstance.section === "left") { return false; } else if (widgetInstance.section === "right") { return true; } else { // middle section if (widgetInstance.sectionWidgetIndex < widgetInstance.sectionWidgetsCount / 2) { return true; } else { return false; } } } catch (e) { Logger.e(e); } return true; } function getTooltipDirection(screenName) { const position = Settings.getBarPositionForScreen(screenName); switch (position) { case "right": return "left"; case "left": return "right"; case "bottom": return "top"; default: return "bottom"; } } // Helper to close any existing dialogs in a popup menu window function closeExistingDialogs(popupMenuWindow) { if (!popupMenuWindow || !popupMenuWindow.dialogParent) return; var dialogParent = popupMenuWindow.dialogParent; for (var i = dialogParent.children.length - 1; i >= 0; i--) { var child = dialogParent.children[i]; if (child && typeof child.close === "function") { child.close(); } } popupMenuWindow.hasDialog = false; } // Open widget settings dialog for a bar widget // Parameters: // screen: The screen to show the dialog on // section: Section id ("left", "center", "right") // index: Widget index in section // widgetId: Widget type id (e.g., "Volume") // widgetData: Current widget settings object function openWidgetSettings(screen, section, index, widgetId, widgetData) { // Get the popup menu window to use as parent (avoids clipping issues with bar height) var popupMenuWindow = PanelService.getPopupMenuWindow(screen); if (!popupMenuWindow) { Logger.e("BarService", "No popup menu window found for screen"); return; } // Close any existing dialogs first to prevent stacking closeExistingDialogs(popupMenuWindow); if (PanelService.openedPanel) { PanelService.openedPanel.close(); } var component = Qt.createComponent(Quickshell.shellDir + "/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml"); function instantiateAndOpen() { // Use dialogParent (Item) instead of window directly for proper Popup anchoring var dialog = component.createObject(popupMenuWindow.dialogParent, { "widgetIndex": index, "widgetData": widgetData, "widgetId": widgetId, "sectionId": section, "screen": screen }); if (dialog) { dialog.updateWidgetSettings.connect((sec, idx, settings) => { var screenName = screen?.name || ""; if (Settings.hasScreenOverride(screenName, "widgets")) { var overrideWidgets = Settings.getBarWidgetsForScreen(screenName); if (overrideWidgets && overrideWidgets[sec] && idx < overrideWidgets[sec].length) { overrideWidgets[sec][idx] = Object.assign({}, overrideWidgets[sec][idx], settings); Settings.setScreenOverride(screenName, "widgets", overrideWidgets); } } else { var widgets = Settings.data.bar.widgets[sec]; if (widgets && idx < widgets.length) { widgets[idx] = Object.assign({}, widgets[idx], settings); Settings.data.bar.widgets[sec] = widgets; Settings.saveImmediate(); } } }); // Enable keyboard focus for the popup menu window when dialog is open popupMenuWindow.hasDialog = true; // Close the popup menu window when dialog closes dialog.closed.connect(() => { popupMenuWindow.hasDialog = false; popupMenuWindow.close(); dialog.destroy(); }); // Show the popup menu window and open the dialog popupMenuWindow.open(); dialog.open(); } else { Logger.e("BarService", "Failed to create widget settings dialog"); } } if (component.status === Component.Ready) { instantiateAndOpen(); } else if (component.status === Component.Error) { Logger.e("BarService", "Error loading widget settings dialog:", component.errorString()); } else { component.statusChanged.connect(function () { if (component.status === Component.Ready) { instantiateAndOpen(); } else if (component.status === Component.Error) { Logger.e("BarService", "Error loading widget settings dialog:", component.errorString()); } }); } } // Open plugin settings dialog // Parameters: // screen: The screen to show the dialog on // pluginManifest: The plugin's manifest object (must have entryPoints.settings) function openPluginSettings(screen, pluginManifest) { if (!pluginManifest || !pluginManifest.entryPoints || !pluginManifest.entryPoints.settings) { Logger.e("BarService", "Cannot open plugin settings: no settings entry point"); return; } // Get the popup menu window to use as parent var popupMenuWindow = PanelService.getPopupMenuWindow(screen); if (!popupMenuWindow) { Logger.e("BarService", "No popup menu window found for screen"); return; } // Close any existing dialogs first to prevent stacking closeExistingDialogs(popupMenuWindow); var component = Qt.createComponent(Quickshell.shellDir + "/Widgets/NPluginSettingsPopup.qml"); function instantiateAndOpen() { var dialog = component.createObject(popupMenuWindow.dialogParent, { "showToastOnSave": true, "screen": screen }); if (dialog) { // Enable keyboard focus for the popup menu window when dialog is open popupMenuWindow.hasDialog = true; // Close the popup menu window when dialog closes dialog.closed.connect(() => { popupMenuWindow.hasDialog = false; popupMenuWindow.close(); dialog.destroy(); }); // Show the popup menu window and open the dialog popupMenuWindow.open(); dialog.openPluginSettings(pluginManifest); } else { Logger.e("BarService", "Failed to create plugin settings dialog"); } } if (component.status === Component.Ready) { instantiateAndOpen(); } else if (component.status === Component.Error) { Logger.e("BarService", "Error loading plugin settings dialog:", component.errorString()); } else { component.statusChanged.connect(function () { if (component.status === Component.Ready) { instantiateAndOpen(); } else if (component.status === Component.Error) { Logger.e("BarService", "Error loading plugin settings dialog:", component.errorString()); } }); } } }