pragma Singleton import QtQuick import Quickshell import qs.Commons import qs.Services.Compositor Singleton { id: root // A ref. to the lockScreen, so it's accessible from anywhere. property var lockScreen: null // Panels property var registeredPanels: ({}) property var openedPanel: null property var closingPanel: null property bool closedImmediately: false // Overlay launcher state (separate from normal panels) property bool overlayLauncherOpen: false property var overlayLauncherScreen: null property var overlayLauncherCore: null // Reference to LauncherCore when overlay is active // Brief window after panel opens where Exclusive keyboard is allowed on Hyprland // This allows text inputs to receive focus, then switches to OnDemand for click-to-close property bool isInitializingKeyboard: false // Global state for keybind recording components to block global shortcuts property bool isKeybindRecording: false signal willOpen signal didClose // Background slot assignments for dynamic panel background rendering // Slot 0: currently opening/open panel, Slot 1: closing panel property var backgroundSlotAssignments: [null, null] signal slotAssignmentChanged(int slotIndex, var panel) function assignToSlot(slotIndex, panel) { if (backgroundSlotAssignments[slotIndex] !== panel) { var newAssignments = backgroundSlotAssignments.slice(); newAssignments[slotIndex] = panel; backgroundSlotAssignments = newAssignments; slotAssignmentChanged(slotIndex, panel); } } // Popup menu windows (one per screen) - used for both tray menus and context menus property var popupMenuWindows: ({}) signal popupMenuWindowRegistered(var screen) // Register this panel (called after panel is loaded) function registerPanel(panel) { registeredPanels[panel.objectName] = panel; Logger.d("PanelService", "Registered panel:", panel.objectName); } // Register popup menu window for a screen function registerPopupMenuWindow(screen, window) { if (!screen || !window) return; var key = screen.name; popupMenuWindows[key] = window; Logger.d("PanelService", "Registered popup menu window for screen:", key); 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) return null; return popupMenuWindows[screen.name] || null; } // Show a context menu with proper handling for all compositors // Optional targetItem: if provided, menu will be horizontally centered on this item instead of anchorItem function showContextMenu(contextMenu, anchorItem, screen, targetItem) { if (!contextMenu || !anchorItem) return; // Close any previously opened context menu first closeContextMenu(screen); var popupMenuWindow = getPopupMenuWindow(screen); if (popupMenuWindow) { popupMenuWindow.showContextMenu(contextMenu); contextMenu.openAtItem(anchorItem, screen, targetItem); } } // Close any open context menu or popup menu window function closeContextMenu(screen) { var popupMenuWindow = getPopupMenuWindow(screen); if (popupMenuWindow && popupMenuWindow.visible) { popupMenuWindow.close(); } } // Show a tray menu with proper handling for all compositors // Returns true if menu was shown successfully function showTrayMenu(screen, trayItem, trayMenu, anchorItem, menuX, menuY, widgetSection, widgetIndex) { if (!trayItem || !trayMenu || !anchorItem) return false; // Close any previously opened menu first closeContextMenu(screen); trayMenu.trayItem = trayItem; trayMenu.widgetSection = widgetSection; trayMenu.widgetIndex = widgetIndex; var popupMenuWindow = getPopupMenuWindow(screen); if (popupMenuWindow) { popupMenuWindow.open(); trayMenu.showAt(anchorItem, menuX, menuY); } else { return false; } return true; } // Close tray menu function closeTrayMenu(screen) { var popupMenuWindow = getPopupMenuWindow(screen); if (popupMenuWindow) { // This closes both the window and calls hideMenu on the tray menu popupMenuWindow.close(); } } // Find a fallback screen, prioritizing 0x0 position (primary) function findFallbackScreen() { let primaryCandidate = null; let firstScreen = null; for (let i = 0; i < Quickshell.screens.length; i++) { const s = Quickshell.screens[i]; if (s.x === 0 && s.y === 0) { primaryCandidate = s; } if (!firstScreen) { firstScreen = s; } } return primaryCandidate || firstScreen || null; } // Returns a panel (loads it on-demand if not yet loaded) // By default, if panel not found on screen, tries other screens (favoring 0x0) // Pass fallback=false to disable this behavior function getPanel(name, screen, fallback = true) { if (!screen) { Logger.d("PanelService", "missing screen for getPanel:", name); // If no screen specified, return the first matching panel for (var key in registeredPanels) { if (key.startsWith(name + "-")) { return registeredPanels[key]; } } return null; } var panelKey = `${name}-${screen.name}`; // Check if panel is already loaded if (registeredPanels[panelKey]) { return registeredPanels[panelKey]; } // If fallback enabled, try to find panel on another screen if (fallback) { // First try the primary screen (0x0) var fallbackScreen = findFallbackScreen(); if (fallbackScreen && fallbackScreen.name !== screen.name) { var fallbackKey = `${name}-${fallbackScreen.name}`; if (registeredPanels[fallbackKey]) { Logger.d("PanelService", "Panel fallback from", screen.name, "to", fallbackScreen.name); return registeredPanels[fallbackKey]; } } // Try any other screen for (var key in registeredPanels) { if (key.startsWith(name + "-")) { Logger.d("PanelService", "Panel fallback to first available:", key); return registeredPanels[key]; } } } Logger.w("PanelService", "Panel not found:", panelKey); return null; } // Check if a panel exists function hasPanel(name) { return name in registeredPanels; } // Check if panels can be shown on a given screen (has bar enabled or allowPanelsOnScreenWithoutBar) function canShowPanelsOnScreen(screen) { const name = screen?.name || ""; const monitors = Settings.data.bar.monitors || []; const allowPanelsOnScreenWithoutBar = Settings.data.general.allowPanelsOnScreenWithoutBar; return allowPanelsOnScreenWithoutBar || monitors.length === 0 || monitors.includes(name); } // Find a screen that can show panels function findScreenForPanels() { for (let i = 0; i < Quickshell.screens.length; i++) { if (canShowPanelsOnScreen(Quickshell.screens[i])) { return Quickshell.screens[i]; } } return null; } // Timer to switch from Exclusive to OnDemand keyboard focus on Hyprland Timer { id: keyboardInitTimer interval: 100 repeat: false onTriggered: { root.isInitializingKeyboard = false; } } // Helper to keep only one panel open at any time function willOpenPanel(panel) { // Close overlay launcher if open if (overlayLauncherOpen) { overlayLauncherOpen = false; overlayLauncherScreen = null; } if (openedPanel && openedPanel !== panel) { // Move current panel to closing slot before closing it closingPanel = openedPanel; assignToSlot(1, closingPanel); openedPanel.close(); } // Assign new panel to open slot openedPanel = panel; assignToSlot(0, panel); // Start keyboard initialization period (for Hyprland workaround) if (panel && panel.exclusiveKeyboard) { isInitializingKeyboard = true; keyboardInitTimer.restart(); } // emit signal willOpen(); } // Open launcher panel (handles both normal and overlay mode) function openLauncher(screen) { if (Settings.data.appLauncher.overviewLayer) { // Close any regular panel first if (openedPanel) { closingPanel = openedPanel; assignToSlot(1, closingPanel); openedPanel.close(); openedPanel = null; } // Open overlay launcher overlayLauncherOpen = true; overlayLauncherScreen = screen; willOpen(); } else { // Normal mode - use the SmartPanel var panel = getPanel("launcherPanel", screen); if (panel) panel.open(); } } // Toggle launcher panel function toggleLauncher(screen) { if (Settings.data.appLauncher.overviewLayer) { if (overlayLauncherOpen && overlayLauncherScreen === screen) { closeOverlayLauncher(); } else { openLauncher(screen); } } else { var panel = getPanel("launcherPanel", screen); if (panel) panel.toggle(); } } // Close overlay launcher function closeOverlayLauncher() { if (overlayLauncherOpen) { overlayLauncherOpen = false; overlayLauncherScreen = null; didClose(); } } // Close overlay launcher immediately (for app launches) function closeOverlayLauncherImmediately() { if (overlayLauncherOpen) { closedImmediately = true; overlayLauncherOpen = false; overlayLauncherScreen = null; didClose(); } } // ==================== Unified Launcher API ==================== // These methods work for both normal (SmartPanel) and overlay modes function isLauncherOpen(screen) { if (Settings.data.appLauncher.overviewLayer) { return overlayLauncherOpen && overlayLauncherScreen === screen; } else { var panel = getPanel("launcherPanel", screen); return panel ? panel.isPanelOpen : false; } } function getLauncherSearchText(screen) { if (Settings.data.appLauncher.overviewLayer) { return overlayLauncherCore ? overlayLauncherCore.searchText : ""; } else { var panel = getPanel("launcherPanel", screen); return panel ? panel.searchText : ""; } } function setLauncherSearchText(screen, text) { if (Settings.data.appLauncher.overviewLayer) { if (overlayLauncherCore) overlayLauncherCore.setSearchText(text); } else { var panel = getPanel("launcherPanel", screen); if (panel) panel.setSearchText(text); } } function openLauncherWithSearch(screen, searchText) { if (Settings.data.appLauncher.overviewLayer) { openLauncher(screen); // Set search text after core is ready Qt.callLater(() => { if (overlayLauncherCore) overlayLauncherCore.setSearchText(searchText); }); } else { var panel = getPanel("launcherPanel", screen); if (panel) { panel.open(); panel.setSearchText(searchText); } } } function closeLauncher(screen) { if (Settings.data.appLauncher.overviewLayer) { closeOverlayLauncher(); } else { var panel = getPanel("launcherPanel", screen); if (panel) panel.close(); } } // Close any open panel (for general use) function closePanel() { if (overlayLauncherOpen) { closeOverlayLauncher(); } else if (openedPanel && openedPanel.close) { openedPanel.close(); } } function closedPanel(panel) { if (openedPanel && openedPanel === panel) { openedPanel = null; assignToSlot(0, null); } if (closingPanel && closingPanel === panel) { closingPanel = null; assignToSlot(1, null); } // Reset keyboard init state isInitializingKeyboard = false; keyboardInitTimer.stop(); // emit signal didClose(); } // Close panels when compositor overview opens (if setting is enabled) Connections { target: CompositorService enabled: Settings.data.bar.hideOnOverview function onOverviewActiveChanged() { if (CompositorService.overviewActive && root.openedPanel) { root.openedPanel.close(); } } } }