From 3c5245914f9ddbbcee49ea3d4ba69770429ce16c Mon Sep 17 00:00:00 2001 From: bbedward Date: Tue, 14 Jul 2026 22:22:14 -0400 Subject: [PATCH] qs: use asynchronous loaders to load shell core --- quickshell/DMSShell.qml | 297 ++---------------- quickshell/Modules/DankBar/DankBarContent.qml | 115 +++---- quickshell/Modules/DankBar/DankBarWindow.qml | 45 +-- .../Modules/DankBar/Widgets/NotepadButton.qml | 9 +- quickshell/Services/PopoutService.qml | 1 + quickshell/ShellCore.qml | 289 +++++++++++++++++ quickshell/shell.qml | 37 ++- 7 files changed, 436 insertions(+), 357 deletions(-) create mode 100644 quickshell/ShellCore.qml diff --git a/quickshell/DMSShell.qml b/quickshell/DMSShell.qml index b32254b0a..94698343c 100644 --- a/quickshell/DMSShell.qml +++ b/quickshell/DMSShell.qml @@ -20,9 +20,7 @@ import qs.Widgets import qs.Modules.Notifications.Popup import qs.Modules.OSD import qs.Modules.ProcessList -import qs.Modules.DankBar import qs.Modules.DankBar.Popouts -import qs.Modules.Frame import qs.Modules.WorkspaceOverlays import qs.Modules.Settings.DisplayConfig import qs.Services @@ -32,7 +30,9 @@ Item { readonly property var log: Log.scoped("DMSShell") readonly property var _sessionsServiceRef: SessionsService - property bool osdSurfacesLoaded: true + property var core: null + + property bool osdSurfacesLoaded: false property int pendingOsdResumeReloads: 0 function recreateOsdSurfaces() { @@ -41,16 +41,6 @@ Item { osdSurfaceReloadTimer.restart(); } - Loader { - id: blurredWallpaperBackgroundLoader - active: SettingsData.blurredWallpaperLayer && CompositorService.isNiri - asynchronous: false - - sourceComponent: BlurredWallpaperBackground {} - } - - WallpaperBackground {} - DesktopWidgetLayer {} Lock { @@ -149,132 +139,18 @@ Item { } } - property bool barSurfacesLoaded: true - property int pendingFrameTransitionRevision: 0 + Connections { + target: root.core - function recreateBarSurfaces() { - log.info("Recreating bar surfaces, screens:", Quickshell.screens.length, Quickshell.screens.map(s => s.name).join(",")); - if (barSurfacesLoaded) - barSurfacesLoaded = false; - barSurfaceReloadAction.schedule(); - } - - // Holds the bar rebuild until the compositor applies the layout, so the swap lands in one pass - function runPendingFrameTransition() { - if (pendingFrameTransitionRevision <= 0 || !CompositorService.frameCompositorLayoutReady) - return; - recreateBarSurfaces(); - } - - DeferredAction { - id: barSurfaceReloadAction - onTriggered: { - // Ack first so the latch flips and new bars build directly in the post-transition state - if (root.pendingFrameTransitionRevision > 0 && CompositorService.frameCompositorLayoutReady) { - FrameTransitionState.acknowledge(root.pendingFrameTransitionRevision); - root.pendingFrameTransitionRevision = 0; - } - root.barSurfacesLoaded = true; - } - } - - property string _barLayoutStateJson: { - if (!barSurfacesLoaded) - return "[]"; - const configs = SettingsData.barConfigs; - const mapped = configs.map(c => ({ - id: c.id, - position: c.position, - autoHide: c.autoHide, - visible: c.visible - })).sort((a, b) => { - const aVertical = a.position === SettingsData.Position.Left || a.position === SettingsData.Position.Right; - const bVertical = b.position === SettingsData.Position.Left || b.position === SettingsData.Position.Right; - if (aVertical !== bVertical) { - return aVertical - bVertical; - } - return String(a.id).localeCompare(String(b.id)); - }); - return JSON.stringify(mapped); - } - - on_BarLayoutStateJsonChanged: { - if (typeof dockRecreateDebounce !== "undefined") { + function on_BarLayoutStateJsonChanged() { dockRecreateDebounce.restart(); } - } - Connections { - target: FrameTransitionState - function onTransitionRequested(revision) { - root.pendingFrameTransitionRevision = Math.max(root.pendingFrameTransitionRevision, revision); - root.runPendingFrameTransition(); - } - } - - Connections { - target: CompositorService - function onFrameCompositorLayoutReadyChanged() { - root.runPendingFrameTransition(); - } - } - - Connections { - target: SettingsData - function onForceDankBarLayoutRefresh() { - root.recreateBarSurfaces(); - } - } - - property bool frameSurfacesLoaded: true - - Loader { - active: root.frameSurfacesLoaded - asynchronous: false - sourceComponent: Frame {} - } - - Loader { - active: FrameTransitionState.effectiveFrameEnabled && SettingsData.frameLauncherEdgeHover - asynchronous: false - sourceComponent: FrameLauncherHoverZone {} - } - - DeferredAction { - id: frameSurfaceReloadAction - onTriggered: root.frameSurfacesLoaded = true - } - - Repeater { - id: dankBarRepeater - model: ScriptModel { - id: barRepeaterModel - values: JSON.parse(root._barLayoutStateJson) - } - - Component.onCompleted: BarWidgetService.dankBarRepeater = dankBarRepeater - - property var hyprlandOverviewLoaderRef: hyprlandOverviewLoader - - delegate: Loader { - id: barLoader - required property var modelData - property var barConfig: SettingsData.barConfigs.find(cfg => cfg.id === modelData.id) || null - active: root.barSurfacesLoaded && (barConfig?.enabled ?? false) - asynchronous: false - - sourceComponent: DankBar { - barConfig: barLoader.barConfig - hyprlandOverviewLoader: dankBarRepeater.hyprlandOverviewLoaderRef - - onColorPickerRequested: { - if (colorPickerModal.shouldBeVisible) { - colorPickerModal.close(); - } else { - colorPickerModal.show(); - } - } - } + function onSurfaceRecoveryPass() { + root.dockEnabled = false; + Qt.callLater(() => { + root.dockEnabled = true; + }); } } @@ -329,132 +205,17 @@ Item { onTriggered: root.osdSurfacesLoaded = true } - property bool hadRealScreen: true - property var previousRealScreenNames: [] - // Guards for the screen-reconnect recovery path (see scheduleScreenReconnectRecovery). - property bool _screenRecoveryCooldown: false - property bool _screenRecoveryPending: false - - function _getRealScreenNames() { - const names = []; - for (let i = 0; i < Quickshell.screens.length; i++) { - if (Quickshell.screens[i].name.length > 0) - names.push(Quickshell.screens[i].name); - } - return names; - } - - function _hasRealScreen() { - for (let i = 0; i < Quickshell.screens.length; i++) { - if (Quickshell.screens[i].name.length > 0) - return true; - } - return false; - } - - function triggerSurfaceRecovery(source) { - log.info("Surface recovery triggered by:", source, "screens:", Quickshell.screens.length, Quickshell.screens.map(s => s.name).join(","), "barLoaded:", root.barSurfacesLoaded, "frameLoaded:", root.frameSurfacesLoaded, "dockEnabled:", root.dockEnabled); - surfaceResumeRecoveryTimer.pass = 0; - surfaceResumeRecoveryTimer.interval = 800; - surfaceResumeRecoveryTimer.restart(); - } - - Connections { - target: Quickshell - function onScreensChanged() { - const hasReal = root._hasRealScreen(); - const currentNames = root._getRealScreenNames(); - log.info("Screens changed:", Quickshell.screens.length, Quickshell.screens.map(s => "'" + s.name + "'").join(","), "hasReal:", hasReal, "hadReal:", root.hadRealScreen); - const fullReconnect = !root.hadRealScreen && hasReal; - const partialReconnect = root.previousRealScreenNames.length > 0 && currentNames.some(name => !root.previousRealScreenNames.includes(name)); - if (fullReconnect || partialReconnect) { - log.info("Screen reconnect detected, scheduling surface recovery", "full:", fullReconnect, "partial:", partialReconnect); - root.scheduleScreenReconnectRecovery(); - } - root.hadRealScreen = hasReal; - root.previousRealScreenNames = currentNames; - } - } - - // A DPMS off/on cycle removes an output from the screen list and re-adds it, - // which is indistinguishable here from a hotplug. Recovering immediately on - // every such event lets a flapping monitor (or a recovery that itself perturbs - // the output) drive an endless recovery storm that power-cycles the display - // (#2642). Debounce a burst of changes into a single pass, then hold a cooldown - // so repeated flaps trigger at most one recovery per window. Recovery still runs - // once per resume, so a partial DPMS resume keeps redrawing its surfaces (#2579). - function scheduleScreenReconnectRecovery() { - if (root._screenRecoveryCooldown) { - root._screenRecoveryPending = true; - return; - } - screenReconnectDebounce.restart(); - } - Timer { - id: screenReconnectDebounce - // Wide enough to collapse the output-remove + output-re-add pair that one - // DPMS off/on cycle emits as two near-simultaneous events into one recovery. - interval: 450 + id: osdStartupTimer + interval: 1000 repeat: false - onTriggered: { - root._screenRecoveryCooldown = true; - root._screenRecoveryPending = false; - screenReconnectCooldown.restart(); - root.triggerSurfaceRecovery("screen-reconnect"); - } - } - - Timer { - id: screenReconnectCooldown - // Must exceed the full two-pass surfaceResumeRecoveryTimer sequence - // (800 + 2000 ms) so the cooldown still covers an in-flight recovery; - // raise this if those passes are lengthened. - interval: 4000 - repeat: false - onTriggered: { - root._screenRecoveryCooldown = false; - if (root._screenRecoveryPending) { - root._screenRecoveryPending = false; - screenReconnectDebounce.restart(); - } - } - } - - Timer { - id: surfaceResumeRecoveryTimer - interval: 800 - repeat: false - property int pass: 0 - onTriggered: { - pass++; - log.info("Surface recovery pass", pass, "screens:", Quickshell.screens.length, Quickshell.screens.map(s => s.name).join(",")); - - root.recreateBarSurfaces(); - - if (root.frameSurfacesLoaded) { - root.frameSurfacesLoaded = false; - frameSurfaceReloadAction.schedule(); - } - - root.dockEnabled = false; - Qt.callLater(() => { - root.dockEnabled = true; - }); - - if (pass < 2) { - interval = 2000; - restart(); - } else { - pass = 0; - interval = 800; - } - } + onTriggered: root.osdSurfacesLoaded = true } Component.onCompleted: { dockRecreateDebounce.start(); loginSoundTimer.start(); + osdStartupTimer.start(); // These are dummy references just to trigger the singletons onCompleted to trigger PolkitService.polkitAvailable; @@ -1035,21 +796,9 @@ Item { target: SessionService function onSessionResumed() { - log.info("Session resumed: screens:", Quickshell.screens.length, Quickshell.screens.map(s => s.name).join(","), "barLoaded:", root.barSurfacesLoaded, "frameLoaded:", root.frameSurfacesLoaded, "dockEnabled:", root.dockEnabled); - root.pendingOsdResumeReloads = 2; osdResumeRecreateTimer.interval = 400; osdResumeRecreateTimer.restart(); - - // This path runs its own recovery directly, so drop any queued or - // in-flight screen-reconnect recovery to avoid a redundant pass once - // its cooldown expires. - screenReconnectDebounce.stop(); - screenReconnectCooldown.stop(); - root._screenRecoveryCooldown = false; - root._screenRecoveryPending = false; - - root.triggerSurfaceRecovery("sessionResumed"); } } @@ -1200,6 +949,10 @@ Item { active: false + Component.onCompleted: { + PopoutService.powerMenuModalLoader = powerMenuModalLoader; + } + PowerMenuModal { id: powerMenuModal @@ -1292,8 +1045,8 @@ Item { dankDashPopoutLoader: dankDashPopoutLoader notepadSlideoutVariants: notepadSlideoutVariants hyprKeybindsModalLoader: hyprKeybindsModalLoader - dankBarRepeater: dankBarRepeater - hyprlandOverviewLoader: hyprlandOverviewLoader + dankBarRepeater: root.core?.dankBarRepeater ?? null + hyprlandOverviewLoader: root.core?.hyprlandOverviewLoader ?? null workspaceRenameModalLoader: workspaceRenameModalLoader windowRuleModalLoader: windowRuleModalLoader } @@ -1389,14 +1142,6 @@ Item { } } - LazyLoader { - id: hyprlandOverviewLoader - active: CompositorService.isHyprland - component: HyprlandOverview { - id: hyprlandOverview - } - } - LazyLoader { id: niriOverviewOverlayLoader active: CompositorService.isNiri && SettingsData.niriOverviewOverlayEnabled diff --git a/quickshell/Modules/DankBar/DankBarContent.qml b/quickshell/Modules/DankBar/DankBarContent.qml index 37b06115b..572500867 100644 --- a/quickshell/Modules/DankBar/DankBarContent.qml +++ b/quickshell/Modules/DankBar/DankBarContent.qml @@ -894,11 +894,11 @@ Item { axis: barWindow.axis section: topBarContent.getWidgetSection(parent) parentScreen: barWindow.screen - popoutTarget: clipboardHistoryPopoutLoader.item ?? null + popoutTarget: PopoutService.clipboardHistoryPopoutLoader?.item ?? null function openClipboardPopout(initialTab, mode) { openWidgetPopout({ - loader: clipboardHistoryPopoutLoader, + loader: PopoutService.clipboardHistoryPopoutLoader, widgetItem: clipboardWidget, section: topBarContent.getWidgetSection(parent) || "right", triggerSource: "clipboard", @@ -915,8 +915,11 @@ Item { onShowSavedItemsRequested: openClipboardPopout("saved") onClearAllRequested: { - clipboardHistoryPopoutLoader.active = true; - const popout = clipboardHistoryPopoutLoader.item; + const loader = PopoutService.clipboardHistoryPopoutLoader; + if (!loader) + return; + loader.active = true; + const popout = loader.item; if (!popout?.confirmDialog) { return; } @@ -941,16 +944,17 @@ Item { section: topBarContent.getWidgetSection(parent) parentScreen: barWindow.screen onClicked: { - if (!powerMenuModalLoader) + const loader = PopoutService.powerMenuModalLoader; + if (!loader) return; - powerMenuModalLoader.active = true; - if (!powerMenuModalLoader.item) + loader.active = true; + if (!loader.item) return; - if (powerMenuModalLoader.item.shouldBeVisible) { - powerMenuModalLoader.item.close(); + if (loader.item.shouldBeVisible) { + loader.item.close(); return; } - powerMenuModalLoader.item.openCentered(); + loader.item.openCentered(); } } } @@ -964,23 +968,26 @@ Item { widgetThickness: barWindow.widgetThickness barThickness: barWindow.effectiveBarThickness section: topBarContent.getWidgetSection(parent) - popoutTarget: appDrawerLoader.item + popoutTarget: PopoutService.appDrawerLoader?.item parentScreen: barWindow.screen hyprlandOverviewLoader: barWindow ? barWindow.hyprlandOverviewLoader : null function _preparePopout() { - appDrawerLoader.active = true; - if (!appDrawerLoader.item) + const loader = PopoutService.appDrawerLoader; + if (!loader) + return false; + loader.active = true; + if (!loader.item) return false; const effectiveBarConfig = topBarContent.barConfig; const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); - if (appDrawerLoader.item.setBarContext) - appDrawerLoader.item.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0); - if (appDrawerLoader.item.setTriggerPosition) { + if (loader.item.setBarContext) + loader.item.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0); + if (loader.item.setTriggerPosition) { const globalPos = launcherButton.visualContent.mapToItem(null, 0, 0); const currentScreen = barWindow.screen; const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barWindow.effectiveBarThickness, launcherButton.visualWidth, effectiveBarConfig?.spacing ?? 4, barPosition, effectiveBarConfig); - appDrawerLoader.item.setTriggerPosition(pos.x, pos.y, pos.width, launcherButton.section, currentScreen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig); + loader.item.setTriggerPosition(pos.x, pos.y, pos.width, launcherButton.section, currentScreen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig); } return true; } @@ -988,30 +995,30 @@ Item { function openWithMode(mode) { if (!_preparePopout()) return; - appDrawerLoader.item.openWithMode(mode); + PopoutService.appDrawerLoader.item.openWithMode(mode); } function toggleWithMode(mode) { if (!_preparePopout()) return; - appDrawerLoader.item.toggleWithMode(mode); + PopoutService.appDrawerLoader.item.toggleWithMode(mode); } function openWithQuery(query) { if (!_preparePopout()) return; - appDrawerLoader.item.openWithQuery(query); + PopoutService.appDrawerLoader.item.openWithQuery(query); } function toggleWithQuery(query) { if (!_preparePopout()) return; - appDrawerLoader.item.toggleWithQuery(query); + PopoutService.appDrawerLoader.item.toggleWithQuery(query); } onClicked: { topBarContent.openWidgetPopout({ - loader: appDrawerLoader, + loader: PopoutService.appDrawerLoader, widgetItem: launcherButton, section: launcherButton.section, triggerSource: "appDrawer", @@ -1090,7 +1097,7 @@ Item { barThickness: barWindow.effectiveBarThickness widgetThickness: barWindow.widgetThickness section: topBarContent.getWidgetSection(parent) || "center" - popoutTarget: dankDashPopoutLoader.item ?? null + popoutTarget: PopoutService.dankDashPopoutLoader?.item ?? null parentScreen: barWindow.screen Component.onCompleted: { @@ -1106,7 +1113,7 @@ Item { onClockClicked: { const section = topBarContent.getWidgetSection(parent) || "center"; topBarContent.openWidgetPopout({ - loader: dankDashPopoutLoader, + loader: PopoutService.dankDashPopoutLoader, widgetItem: clockWidget, section, triggerSource: topBarContent._dashTriggerSource(section, "overview"), @@ -1129,12 +1136,12 @@ Item { barThickness: barWindow.effectiveBarThickness widgetThickness: barWindow.widgetThickness section: topBarContent.getWidgetSection(parent) || "center" - popoutTarget: dankDashPopoutLoader.item ?? null + popoutTarget: PopoutService.dankDashPopoutLoader?.item ?? null parentScreen: barWindow.screen onClicked: { const section = topBarContent.getWidgetSection(parent) || "center"; topBarContent.openWidgetPopout({ - loader: dankDashPopoutLoader, + loader: PopoutService.dankDashPopoutLoader, widgetItem: mediaWidget, section, triggerSource: topBarContent._dashTriggerSource(section, "media"), @@ -1156,12 +1163,12 @@ Item { barThickness: barWindow.effectiveBarThickness widgetThickness: barWindow.widgetThickness section: topBarContent.getWidgetSection(parent) || "center" - popoutTarget: dankDashPopoutLoader.item ?? null + popoutTarget: PopoutService.dankDashPopoutLoader?.item ?? null parentScreen: barWindow.screen onClicked: { const section = topBarContent.getWidgetSection(parent) || "center"; topBarContent.openWidgetPopout({ - loader: dankDashPopoutLoader, + loader: PopoutService.dankDashPopoutLoader, widgetItem: weatherWidget, section, triggerSource: topBarContent._dashTriggerSource(section, "weather"), @@ -1211,12 +1218,12 @@ Item { widgetThickness: barWindow.widgetThickness axis: barWindow.axis section: topBarContent.getWidgetSection(parent) || "right" - popoutTarget: processListPopoutLoader.item ?? null + popoutTarget: PopoutService.processListPopoutLoader?.item ?? null parentScreen: barWindow.screen widgetData: parent.widgetData onCpuClicked: { topBarContent.openWidgetPopout({ - loader: processListPopoutLoader, + loader: PopoutService.processListPopoutLoader, widgetItem: cpuWidget, section: topBarContent.getWidgetSection(parent) || "right", triggerSource: "cpu", @@ -1235,12 +1242,12 @@ Item { widgetThickness: barWindow.widgetThickness axis: barWindow.axis section: topBarContent.getWidgetSection(parent) || "right" - popoutTarget: processListPopoutLoader.item ?? null + popoutTarget: PopoutService.processListPopoutLoader?.item ?? null parentScreen: barWindow.screen widgetData: parent.widgetData onRamClicked: { topBarContent.openWidgetPopout({ - loader: processListPopoutLoader, + loader: PopoutService.processListPopoutLoader, widgetItem: ramWidget, section: topBarContent.getWidgetSection(parent) || "right", triggerSource: "memory", @@ -1273,12 +1280,12 @@ Item { widgetThickness: barWindow.widgetThickness axis: barWindow.axis section: topBarContent.getWidgetSection(parent) || "right" - popoutTarget: processListPopoutLoader.item ?? null + popoutTarget: PopoutService.processListPopoutLoader?.item ?? null parentScreen: barWindow.screen widgetData: parent.widgetData onCpuTempClicked: { topBarContent.openWidgetPopout({ - loader: processListPopoutLoader, + loader: PopoutService.processListPopoutLoader, widgetItem: cpuTempWidget, section: topBarContent.getWidgetSection(parent) || "right", triggerSource: "cpu_temp", @@ -1297,12 +1304,12 @@ Item { widgetThickness: barWindow.widgetThickness axis: barWindow.axis section: topBarContent.getWidgetSection(parent) || "right" - popoutTarget: processListPopoutLoader.item ?? null + popoutTarget: PopoutService.processListPopoutLoader?.item ?? null parentScreen: barWindow.screen widgetData: parent.widgetData onGpuTempClicked: { topBarContent.openWidgetPopout({ - loader: processListPopoutLoader, + loader: PopoutService.processListPopoutLoader, widgetItem: gpuTempWidget, section: topBarContent.getWidgetSection(parent) || "right", triggerSource: "gpu_temp", @@ -1324,16 +1331,16 @@ Item { NotificationCenterButton { id: notificationButton hasUnread: barWindow.notificationCount > 0 - isActive: notificationCenterLoader.item ? notificationCenterLoader.item.shouldBeVisible : false + isActive: PopoutService.notificationCenterLoader?.item ? PopoutService.notificationCenterLoader?.item.shouldBeVisible : false widgetThickness: barWindow.widgetThickness barThickness: barWindow.effectiveBarThickness axis: barWindow.axis section: topBarContent.getWidgetSection(parent) || "right" - popoutTarget: notificationCenterLoader.item ?? null + popoutTarget: PopoutService.notificationCenterLoader?.item ?? null parentScreen: barWindow.screen onClicked: { topBarContent.openWidgetPopout({ - loader: notificationCenterLoader, + loader: PopoutService.notificationCenterLoader, widgetItem: notificationButton, section: topBarContent.getWidgetSection(parent) || "right", triggerSource: "notifications", @@ -1349,18 +1356,18 @@ Item { Battery { id: batteryWidget - batteryPopupVisible: batteryPopoutLoader.item ? batteryPopoutLoader.item.shouldBeVisible : false + batteryPopupVisible: PopoutService.batteryPopoutLoader?.item ? PopoutService.batteryPopoutLoader?.item.shouldBeVisible : false widgetThickness: barWindow.widgetThickness barThickness: barWindow.effectiveBarThickness axis: barWindow.axis section: topBarContent.getWidgetSection(parent) || "right" barSpacing: barConfig?.spacing ?? 4 barConfig: topBarContent.barConfig - popoutTarget: batteryPopoutLoader.item ?? null + popoutTarget: PopoutService.batteryPopoutLoader?.item ?? null parentScreen: barWindow.screen onToggleBatteryPopup: { topBarContent.openWidgetPopout({ - loader: batteryPopoutLoader, + loader: PopoutService.batteryPopoutLoader, widgetItem: batteryWidget, section: topBarContent.getWidgetSection(parent) || "right", triggerSource: "battery", @@ -1375,16 +1382,16 @@ Item { DWLLayout { id: layoutWidget - layoutPopupVisible: layoutPopoutLoader.item ? layoutPopoutLoader.item.shouldBeVisible : false + layoutPopupVisible: PopoutService.layoutPopoutLoader?.item ? PopoutService.layoutPopoutLoader?.item.shouldBeVisible : false widgetThickness: barWindow.widgetThickness barThickness: barWindow.effectiveBarThickness axis: barWindow.axis section: topBarContent.getWidgetSection(parent) || "center" - popoutTarget: layoutPopoutLoader.item ?? null + popoutTarget: PopoutService.layoutPopoutLoader?.item ?? null parentScreen: barWindow.screen onToggleLayoutPopup: { topBarContent.openWidgetPopout({ - loader: layoutPopoutLoader, + loader: PopoutService.layoutPopoutLoader, widgetItem: layoutWidget, section: topBarContent.getWidgetSection(parent) || "center", triggerSource: "layout", @@ -1406,11 +1413,11 @@ Item { barSpacing: barConfig?.spacing ?? 4 barConfig: topBarContent.barConfig isAutoHideBar: topBarContent.barConfig?.autoHide ?? false - popoutTarget: vpnPopoutLoader.item ?? null + popoutTarget: PopoutService.vpnPopoutLoader?.item ?? null parentScreen: barWindow.screen onToggleVpnPopup: { topBarContent.openWidgetPopout({ - loader: vpnPopoutLoader, + loader: PopoutService.vpnPopoutLoader, widgetItem: vpnWidget, section: topBarContent.getWidgetSection(parent) || "right", triggerSource: "vpn", @@ -1425,12 +1432,12 @@ Item { ControlCenterButton { id: controlCenterButton - isActive: controlCenterLoader.item ? controlCenterLoader.item.shouldBeVisible : false + isActive: PopoutService.controlCenterLoader?.item ? PopoutService.controlCenterLoader?.item.shouldBeVisible : false widgetThickness: barWindow.widgetThickness barThickness: barWindow.effectiveBarThickness axis: barWindow.axis section: topBarContent.getWidgetSection(parent) || "right" - popoutTarget: controlCenterLoader.item ?? null + popoutTarget: PopoutService.controlCenterLoader?.item ?? null parentScreen: barWindow.screen screenName: barWindow.screen?.name || "" screenModel: barWindow.screen?.model || "" @@ -1448,14 +1455,14 @@ Item { onClicked: { topBarContent.openWidgetPopout({ - loader: controlCenterLoader, + loader: PopoutService.controlCenterLoader, widgetItem: controlCenterButton, section: topBarContent.getWidgetSection(parent) || "right", triggerSource: "controlCenter", mode: "click", setTriggerScreen: true }); - if (controlCenterLoader.item?.shouldBeVisible && NetworkService.wifiEnabled) + if (PopoutService.controlCenterLoader?.item?.shouldBeVisible && NetworkService.wifiEnabled) NetworkService.scanWifi(); } } @@ -1567,12 +1574,12 @@ Item { SystemUpdate { id: systemUpdateWidget - isActive: systemUpdateLoader.item ? systemUpdateLoader.item.shouldBeVisible : false + isActive: PopoutService.systemUpdateLoader?.item ? PopoutService.systemUpdateLoader?.item.shouldBeVisible : false widgetThickness: barWindow.widgetThickness barThickness: barWindow.effectiveBarThickness axis: barWindow.axis section: topBarContent.getWidgetSection(parent) || "right" - popoutTarget: systemUpdateLoader.item ?? null + popoutTarget: PopoutService.systemUpdateLoader?.item ?? null parentScreen: barWindow.screen Component.onCompleted: { @@ -1586,7 +1593,7 @@ Item { onClicked: { topBarContent.openWidgetPopout({ - loader: systemUpdateLoader, + loader: PopoutService.systemUpdateLoader, widgetItem: systemUpdateWidget, section: topBarContent.getWidgetSection(parent) || "right", triggerSource: "systemUpdate", diff --git a/quickshell/Modules/DankBar/DankBarWindow.qml b/quickshell/Modules/DankBar/DankBarWindow.qml index ce104199c..2a6eb16e2 100644 --- a/quickshell/Modules/DankBar/DankBarWindow.qml +++ b/quickshell/Modules/DankBar/DankBarWindow.qml @@ -27,10 +27,13 @@ PanelWindow { property var systemUpdateButtonRef: null function triggerSystemUpdate() { - systemUpdateLoader.active = true; - if (!systemUpdateLoader.item) + const loader = PopoutService.systemUpdateLoader; + if (!loader) return; - const popout = systemUpdateLoader.item; + loader.active = true; + if (!loader.item) + return; + const popout = loader.item; const barPosition = axis?.edge === "left" ? 2 : (axis?.edge === "right" ? 3 : (axis?.edge === "top" ? 0 : 1)); if (systemUpdateButtonRef && popout.setTriggerPosition) { const screenPos = systemUpdateButtonRef.mapToItem(null, 0, 0); @@ -44,35 +47,41 @@ PanelWindow { } function triggerControlCenter() { - controlCenterLoader.active = true; - if (!controlCenterLoader.item) { + const loader = PopoutService.controlCenterLoader; + if (!loader) + return; + loader.active = true; + if (!loader.item) { return; } - if (controlCenterButtonRef && controlCenterLoader.item.setTriggerPosition) { + if (controlCenterButtonRef && loader.item.setTriggerPosition) { const screenPos = controlCenterButtonRef.mapToItem(null, 0, 0); const barPosition = axis?.edge === "left" ? 2 : (axis?.edge === "right" ? 3 : (axis?.edge === "top" ? 0 : 1)); const pos = SettingsData.getPopupTriggerPosition(screenPos, barWindow.screen, barWindow.effectiveBarThickness, controlCenterButtonRef.width, barConfig?.spacing ?? 4, barPosition, barConfig); const section = controlCenterButtonRef.section || "right"; - controlCenterLoader.item.setTriggerPosition(pos.x, pos.y, pos.width, section, barWindow.screen, barPosition, barWindow.effectiveBarThickness, barConfig?.spacing ?? 4, barConfig); + loader.item.setTriggerPosition(pos.x, pos.y, pos.width, section, barWindow.screen, barPosition, barWindow.effectiveBarThickness, barConfig?.spacing ?? 4, barConfig); } else { - controlCenterLoader.item.triggerScreen = barWindow.screen; + loader.item.triggerScreen = barWindow.screen; } - controlCenterLoader.item.toggle(); - if (controlCenterLoader.item.shouldBeVisible && NetworkService.wifiEnabled) { + loader.item.toggle(); + if (loader.item.shouldBeVisible && NetworkService.wifiEnabled) { NetworkService.scanWifi(); } } function triggerDashTab(tabId) { - dankDashPopoutLoader.active = true; - if (!dankDashPopoutLoader.item) { + const loader = PopoutService.dankDashPopoutLoader; + if (!loader) + return false; + loader.active = true; + if (!loader.item) { return false; } let section = "center"; - if (clockButtonRef && clockButtonRef.visualContent && dankDashPopoutLoader.item.setTriggerPosition) { + if (clockButtonRef && clockButtonRef.visualContent && loader.item.setTriggerPosition) { const barPosition = axis?.edge === "left" ? 2 : (axis?.edge === "right" ? 3 : (axis?.edge === "top" ? 0 : 1)); section = clockButtonRef.section || "center"; @@ -98,14 +107,14 @@ PanelWindow { } const pos = SettingsData.getPopupTriggerPosition(triggerPos, barWindow.screen, barWindow.effectiveBarThickness, triggerWidth, barConfig?.spacing ?? 4, barPosition, barConfig); - dankDashPopoutLoader.item.setTriggerPosition(pos.x, pos.y, pos.width, section, barWindow.screen, barPosition, barWindow.effectiveBarThickness, barConfig?.spacing ?? 4, barConfig); + loader.item.setTriggerPosition(pos.x, pos.y, pos.width, section, barWindow.screen, barPosition, barWindow.effectiveBarThickness, barConfig?.spacing ?? 4, barConfig); } else { - dankDashPopoutLoader.item.triggerScreen = barWindow.screen; + loader.item.triggerScreen = barWindow.screen; } - if (dankDashPopoutLoader.item.requestTab) - dankDashPopoutLoader.item.requestTab(tabId); - PopoutManager.requestPopout(dankDashPopoutLoader.item, undefined, (barConfig?.id ?? "default") + "-" + section + "-" + tabId); + if (loader.item.requestTab) + loader.item.requestTab(tabId); + PopoutManager.requestPopout(loader.item, undefined, (barConfig?.id ?? "default") + "-" + section + "-" + tabId); return true; } diff --git a/quickshell/Modules/DankBar/Widgets/NotepadButton.qml b/quickshell/Modules/DankBar/Widgets/NotepadButton.qml index b5b30eeb5..bb0648194 100644 --- a/quickshell/Modules/DankBar/Widgets/NotepadButton.qml +++ b/quickshell/Modules/DankBar/Widgets/NotepadButton.qml @@ -14,21 +14,22 @@ BasePill { readonly property string targetScreenName: parentScreen?.name || focusedScreenName function resolveNotepadInstance() { - if (typeof notepadSlideoutVariants === "undefined" || !notepadSlideoutVariants || !notepadSlideoutVariants.instances) { + const slideouts = PopoutService.notepadSlideouts; + if (!slideouts || slideouts.length === 0) { return null; } const targetScreen = targetScreenName; if (targetScreen) { - for (var i = 0; i < notepadSlideoutVariants.instances.length; i++) { - var slideout = notepadSlideoutVariants.instances[i]; + for (var i = 0; i < slideouts.length; i++) { + var slideout = slideouts[i]; if (slideout.modelData && slideout.modelData.name === targetScreen) { return slideout; } } } - return notepadSlideoutVariants.instances.length > 0 ? notepadSlideoutVariants.instances[0] : null; + return slideouts[0]; } readonly property var notepadInstance: resolveNotepadInstance() diff --git a/quickshell/Services/PopoutService.qml b/quickshell/Services/PopoutService.qml index 00e1292a4..e89ab2585 100644 --- a/quickshell/Services/PopoutService.qml +++ b/quickshell/Services/PopoutService.qml @@ -38,6 +38,7 @@ Singleton { property var spotlightBarModal: null property var spotlightBarModalLoader: null property var powerMenuModal: null + property var powerMenuModalLoader: null property var processListModal: null property var processListModalLoader: null property var colorPickerModal: null diff --git a/quickshell/ShellCore.qml b/quickshell/ShellCore.qml new file mode 100644 index 000000000..cdf62d3f5 --- /dev/null +++ b/quickshell/ShellCore.qml @@ -0,0 +1,289 @@ +import QtQuick +import Quickshell +import qs.Common +import qs.Modules.DankBar +import qs.Modules.Frame +import qs.Modules.WorkspaceOverlays +import qs.Services + +Item { + id: root + + readonly property var log: Log.scoped("ShellCore") + + property bool barSurfacesLoaded: true + property int pendingFrameTransitionRevision: 0 + property bool frameSurfacesLoaded: true + + property alias dankBarRepeater: dankBarRepeater + property alias hyprlandOverviewLoader: hyprlandOverviewLoader + + signal surfaceRecoveryPass + + property string _barLayoutStateJson: { + if (!barSurfacesLoaded) + return "[]"; + const configs = SettingsData.barConfigs; + const mapped = configs.map(c => ({ + id: c.id, + position: c.position, + autoHide: c.autoHide, + visible: c.visible + })).sort((a, b) => { + const aVertical = a.position === SettingsData.Position.Left || a.position === SettingsData.Position.Right; + const bVertical = b.position === SettingsData.Position.Left || b.position === SettingsData.Position.Right; + if (aVertical !== bVertical) { + return aVertical - bVertical; + } + return String(a.id).localeCompare(String(b.id)); + }); + return JSON.stringify(mapped); + } + + function recreateBarSurfaces() { + log.info("Recreating bar surfaces, screens:", Quickshell.screens.length, Quickshell.screens.map(s => s.name).join(",")); + if (barSurfacesLoaded) + barSurfacesLoaded = false; + barSurfaceReloadAction.schedule(); + } + + // Holds the bar rebuild until the compositor applies the layout, so the swap lands in one pass + function runPendingFrameTransition() { + if (pendingFrameTransitionRevision <= 0 || !CompositorService.frameCompositorLayoutReady) + return; + recreateBarSurfaces(); + } + + DeferredAction { + id: barSurfaceReloadAction + onTriggered: { + // Ack first so the latch flips and new bars build directly in the post-transition state + if (root.pendingFrameTransitionRevision > 0 && CompositorService.frameCompositorLayoutReady) { + FrameTransitionState.acknowledge(root.pendingFrameTransitionRevision); + root.pendingFrameTransitionRevision = 0; + } + root.barSurfacesLoaded = true; + } + } + + Connections { + target: FrameTransitionState + function onTransitionRequested(revision) { + root.pendingFrameTransitionRevision = Math.max(root.pendingFrameTransitionRevision, revision); + root.runPendingFrameTransition(); + } + } + + Connections { + target: CompositorService + function onFrameCompositorLayoutReadyChanged() { + root.runPendingFrameTransition(); + } + } + + Connections { + target: SettingsData + function onForceDankBarLayoutRefresh() { + root.recreateBarSurfaces(); + } + } + + Loader { + active: root.frameSurfacesLoaded + asynchronous: false + sourceComponent: Frame {} + } + + Loader { + active: FrameTransitionState.effectiveFrameEnabled && SettingsData.frameLauncherEdgeHover + asynchronous: false + sourceComponent: FrameLauncherHoverZone {} + } + + DeferredAction { + id: frameSurfaceReloadAction + onTriggered: root.frameSurfacesLoaded = true + } + + Repeater { + id: dankBarRepeater + model: ScriptModel { + id: barRepeaterModel + values: JSON.parse(root._barLayoutStateJson) + } + + Component.onCompleted: BarWidgetService.dankBarRepeater = dankBarRepeater + + property var hyprlandOverviewLoaderRef: hyprlandOverviewLoader + + delegate: Loader { + id: barLoader + required property var modelData + property var barConfig: SettingsData.barConfigs.find(cfg => cfg.id === modelData.id) || null + active: root.barSurfacesLoaded && (barConfig?.enabled ?? false) + asynchronous: false + + sourceComponent: DankBar { + barConfig: barLoader.barConfig + hyprlandOverviewLoader: dankBarRepeater.hyprlandOverviewLoaderRef + + onColorPickerRequested: { + const modal = PopoutService.colorPickerModal; + if (!modal) + return; + if (modal.shouldBeVisible) { + modal.close(); + } else { + modal.show(); + } + } + } + } + } + + property bool hadRealScreen: true + property var previousRealScreenNames: [] + // Guards for the screen-reconnect recovery path (see scheduleScreenReconnectRecovery). + property bool _screenRecoveryCooldown: false + property bool _screenRecoveryPending: false + + function _getRealScreenNames() { + const names = []; + for (let i = 0; i < Quickshell.screens.length; i++) { + if (Quickshell.screens[i].name.length > 0) + names.push(Quickshell.screens[i].name); + } + return names; + } + + function _hasRealScreen() { + for (let i = 0; i < Quickshell.screens.length; i++) { + if (Quickshell.screens[i].name.length > 0) + return true; + } + return false; + } + + function triggerSurfaceRecovery(source) { + log.info("Surface recovery triggered by:", source, "screens:", Quickshell.screens.length, Quickshell.screens.map(s => s.name).join(","), "barLoaded:", root.barSurfacesLoaded, "frameLoaded:", root.frameSurfacesLoaded); + surfaceResumeRecoveryTimer.pass = 0; + surfaceResumeRecoveryTimer.interval = 800; + surfaceResumeRecoveryTimer.restart(); + } + + Connections { + target: Quickshell + function onScreensChanged() { + const hasReal = root._hasRealScreen(); + const currentNames = root._getRealScreenNames(); + log.info("Screens changed:", Quickshell.screens.length, Quickshell.screens.map(s => "'" + s.name + "'").join(","), "hasReal:", hasReal, "hadReal:", root.hadRealScreen); + const fullReconnect = !root.hadRealScreen && hasReal; + const partialReconnect = root.previousRealScreenNames.length > 0 && currentNames.some(name => !root.previousRealScreenNames.includes(name)); + if (fullReconnect || partialReconnect) { + log.info("Screen reconnect detected, scheduling surface recovery", "full:", fullReconnect, "partial:", partialReconnect); + root.scheduleScreenReconnectRecovery(); + } + root.hadRealScreen = hasReal; + root.previousRealScreenNames = currentNames; + } + } + + // A DPMS off/on cycle removes an output from the screen list and re-adds it, + // which is indistinguishable here from a hotplug. Recovering immediately on + // every such event lets a flapping monitor (or a recovery that itself perturbs + // the output) drive an endless recovery storm that power-cycles the display + // (#2642). Debounce a burst of changes into a single pass, then hold a cooldown + // so repeated flaps trigger at most one recovery per window. Recovery still runs + // once per resume, so a partial DPMS resume keeps redrawing its surfaces (#2579). + function scheduleScreenReconnectRecovery() { + if (root._screenRecoveryCooldown) { + root._screenRecoveryPending = true; + return; + } + screenReconnectDebounce.restart(); + } + + Timer { + id: screenReconnectDebounce + // Wide enough to collapse the output-remove + output-re-add pair that one + // DPMS off/on cycle emits as two near-simultaneous events into one recovery. + interval: 450 + repeat: false + onTriggered: { + root._screenRecoveryCooldown = true; + root._screenRecoveryPending = false; + screenReconnectCooldown.restart(); + root.triggerSurfaceRecovery("screen-reconnect"); + } + } + + Timer { + id: screenReconnectCooldown + // Must exceed the full two-pass surfaceResumeRecoveryTimer sequence + // (800 + 2000 ms) so the cooldown still covers an in-flight recovery; + // raise this if those passes are lengthened. + interval: 4000 + repeat: false + onTriggered: { + root._screenRecoveryCooldown = false; + if (root._screenRecoveryPending) { + root._screenRecoveryPending = false; + screenReconnectDebounce.restart(); + } + } + } + + Timer { + id: surfaceResumeRecoveryTimer + interval: 800 + repeat: false + property int pass: 0 + onTriggered: { + pass++; + log.info("Surface recovery pass", pass, "screens:", Quickshell.screens.length, Quickshell.screens.map(s => s.name).join(",")); + + root.recreateBarSurfaces(); + + if (root.frameSurfacesLoaded) { + root.frameSurfacesLoaded = false; + frameSurfaceReloadAction.schedule(); + } + + root.surfaceRecoveryPass(); + + if (pass < 2) { + interval = 2000; + restart(); + } else { + pass = 0; + interval = 800; + } + } + } + + Connections { + target: SessionService + + function onSessionResumed() { + log.info("Session resumed: screens:", Quickshell.screens.length, Quickshell.screens.map(s => s.name).join(","), "barLoaded:", root.barSurfacesLoaded, "frameLoaded:", root.frameSurfacesLoaded); + + // This path runs its own recovery directly, so drop any queued or + // in-flight screen-reconnect recovery to avoid a redundant pass once + // its cooldown expires. + screenReconnectDebounce.stop(); + screenReconnectCooldown.stop(); + root._screenRecoveryCooldown = false; + root._screenRecoveryPending = false; + + root.triggerSurfaceRecovery("sessionResumed"); + } + } + + LazyLoader { + id: hyprlandOverviewLoader + active: CompositorService.isHyprland + component: HyprlandOverview { + id: hyprlandOverview + } + } +} diff --git a/quickshell/shell.qml b/quickshell/shell.qml index 7c84fc2c8..64add30a4 100644 --- a/quickshell/shell.qml +++ b/quickshell/shell.qml @@ -9,6 +9,9 @@ import QtQuick import Quickshell +import qs.Common +import qs.Modules +import qs.Services ShellRoot { id: entrypoint @@ -21,16 +24,40 @@ ShellRoot { } Loader { - id: dmsShellLoader - asynchronous: false - sourceComponent: DMSShell {} + id: wallpaperLoader active: !entrypoint.runGreeter + asynchronous: false + + sourceComponent: Scope { + WallpaperBackground {} + + Loader { + active: SettingsData.blurredWallpaperLayer && CompositorService.isNiri + asynchronous: false + sourceComponent: BlurredWallpaperBackground {} + } + } + } + + Loader { + id: shellCoreLoader + active: !entrypoint.runGreeter + asynchronous: true + source: "ShellCore.qml" + onLoaded: dmsShellLoader.setSource("DMSShell.qml", { + core: item + }) + } + + Loader { + id: dmsShellLoader + asynchronous: true } Loader { id: dmsGreeterLoader - asynchronous: false - sourceComponent: DMSGreeter {} active: entrypoint.runGreeter + asynchronous: false + source: "DMSGreeter.qml" } }