From dcfc7f95738aa730e9e79f554a957678f6f06d73 Mon Sep 17 00:00:00 2001 From: rope Date: Sat, 1 Aug 2026 21:43:08 +0100 Subject: [PATCH] Revert the launcher focus-grab fix attempts None of them worked. quickshell.nix is back to a8eb767 apart from the hidden-directory search change, which was a separate request and does work. Removed: the toggle/grab/compositor-event logging, the grab re-arm, and the full-screen input mask with its click catcher. Kept the Battle.net window rule's class match (steam_app_0 was stale, and the tray-icon rule below it needs the widened match), but its comment no longer claims to fix the launcher. Recorded the diagnosis at the focus grab so this doesn't get re-derived: Hyprland's mouseMoveUnified() destroys the seat grab on any refocus() while the cursor is over a surface the grab doesn't own, and every window map/unmap calls refocus(). No user input involved, nothing to suppress. --- settings/hyprland.nix | 26 +++--- settings/quickshell.nix | 184 +++++++--------------------------------- 2 files changed, 45 insertions(+), 165 deletions(-) diff --git a/settings/hyprland.nix b/settings/hyprland.nix index ca79036..844fe2d 100644 --- a/settings/hyprland.nix +++ b/settings/hyprland.nix @@ -287,22 +287,20 @@ in suppress_event = "fullscreen maximize", }) - -- Battle.net spams window-activation events for as long as it is - -- running. Each one clears quickshell's focus grab, which closes the - -- launcher / power menu exactly like a click-outside would — so with - -- Battle.net open, Super+R appears to do nothing at all: the panel - -- opens and is killed within the same frame. Drop those events. - -- - -- Matched on class, not title: a title-scoped rule would have to be - -- re-evaluated after the CEF window sets its title, and this needs to - -- hold from map onwards. It also catches the launcher's login and - -- update windows, which are the same class under other titles. + -- Battle.net requests activation on its own while running, which + -- steals focus. Drop those events. -- -- Non-Steam shortcuts have been BOTH steam_app_0 and - -- steam_app_default across Steam versions (this rule matched only - -- steam_app_0, and silently stopped applying when Battle.net turned - -- up as steam_app_default — that regression is the bug this alt - -- fixes). Match either so a Steam update can't break it again. + -- steam_app_default across Steam versions, and this rule matched only + -- steam_app_0, so it silently stopped applying when Battle.net turned + -- up as steam_app_default. Match either. + -- + -- Does NOT fix the launcher dying while Battle.net runs. That is + -- Hyprland destroying quickshell's seat grab from inside + -- mouseMoveUnified() whenever it refocuses with the cursor over a + -- surface the grab doesn't own — no activation request involved, so + -- there is nothing here to suppress. Unsolved; see the note in + -- settings/quickshell.nix. -- -- WoW shares steam_app_default and so loses self-activation too. -- Harmless: a game window is focused when it maps, which is not an diff --git a/settings/quickshell.nix b/settings/quickshell.nix index f7125de..14ce93d 100644 --- a/settings/quickshell.nix +++ b/settings/quickshell.nix @@ -801,21 +801,10 @@ in // Bound in hyprland.nix: Super+R → launcher (bottom of the // bar window), Super+L → session menu (right edge). - // mainBar is claimed by the primary screen's Bar in its - // Component.onCompleted. If it is somehow unset — an output - // change recreating the Variants, a reload racing the keybind — - // Super+R has nothing to call, and used to fail dead silently. - // Say so instead: journalctl --user -u quickshell. IpcHandler { target: "launcher" - function toggle(): void { - if (!root.mainBar) { console.warn("ipc launcher.toggle: no bar registered"); return; } - root.mainBar.toggleLauncher(); - } - function powermenu(): void { - if (!root.mainBar) { console.warn("ipc launcher.powermenu: no bar registered"); return; } - root.mainBar.toggleSession(); - } + function toggle(): void { if (root.mainBar) root.mainBar.toggleLauncher(); } + function powermenu(): void { if (root.mainBar) root.mainBar.toggleSession(); } } // Keybind cheat sheet — Super+C in hyprland.nix. @@ -1021,18 +1010,11 @@ in required property var shellRoot screen: modelData WlrLayershell.namespace: "quickshell-bar" - // One source of truth for "a panel is up and wants the seat": - // the keyboard focus mode, the input mask and the focus grab all - // key off this, and they have to agree or the grab dies (see the - // mask below). - readonly property bool panelOpen: (launcherPanel.open || bar.activeDropdown !== null) - && !bar.screenshotPinned - // OnDemand + HyprlandFocusGrab is the working combination // (caelestia's): the grab redirects focus to this window and // OnDemand lets the layer surface accept it. Exclusive fights // the grab — it self-clears and instantly closes the panel. - WlrLayershell.keyboardFocus: bar.panelOpen ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None + WlrLayershell.keyboardFocus: (launcherPanel.open || bar.activeDropdown !== null) && !bar.screenshotPinned ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None // Vertical bar on the left edge. The surface still covers the // whole screen (the frame band and every panel are drawn into @@ -1049,32 +1031,6 @@ in mask: Region { item: barBgRect - // While a panel is open the input region is the WHOLE screen, - // and that is load-bearing rather than cosmetic. - // - // Hyprland destroys a seat grab whenever it refocuses while - // the cursor sits over a surface the grab doesn't own: - // - // // InputManager.cpp, mouseMoveUnified() - // if (m_seatGrab && !m_seatGrab->accepts(foundSurface) ...) { - // if (m_hardInput || refocus) { - // g_pSeatManager->setGrab(nullptr); - // - // `refocus` is true for all 20 CInputManager::refocus() call - // sites, which include every window map and unmap. So an app - // that churns windows (Battle.net does, continuously) fires - // refocus() over and over, and with the cursor anywhere but - // this window's input region the launcher was being killed - // ~1ms after opening, every single time. - // - // Owning the pointer everywhere makes accepts() always true, - // so there is nothing left to tear the grab down. The grab no - // longer sees outside clicks either, which is what the - // dismiss catcher below is for. - Region { - width: bar.panelOpen ? bar.width : 0 - height: bar.panelOpen ? bar.height : 0 - } Region { x: activeDropdown ? activeDropdown.x : 0 y: activeDropdown ? activeDropdown.y : 0 @@ -1103,23 +1059,6 @@ in width: Theme.barWidth } - // Click-outside dismissal. The focus grab used to do this, but it - // can only see clicks that land outside our input region — and - // the region is now the whole screen (see mask). Declared here, - // before the bar widgets and every panel, so it sits UNDERNEATH - // them in stacking order and only catches what they don't. - // - // Transparent, not dimmed: this is the launcher, not a modal. - MouseArea { - anchors.fill: parent - enabled: bar.panelOpen - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - onPressed: { - launcherPanel.open = false; - bar.closeAllDropdowns(); - } - } - // Register the primary bar so shell.qml's IPC handler can // reach the session menu. Component.onCompleted: { @@ -1581,13 +1520,6 @@ in // Non-empty when the query parses as arithmetic. property string calcResult: "" - // Diagnostic clock (ms since the last toggle). Deliberately - // NOT `open`-gated: the first version of the compositor trace - // keyed off `open`, and the focus-grab clear sets open=false - // before Hyprland's IPC event arrives, so it hid the one - // event worth seeing. - property double traceFrom: 0 - readonly property var catDefs: [ { key: "All", label: "All apps", icon: "layout-grid" }, { key: "Network", label: "Internet", icon: "globe" }, @@ -1625,12 +1557,6 @@ in function toggle() { open = !open; - // Logged so a "Super+R did nothing" can be told apart - // from "Super+R never arrived": if the journal shows - // `-> open` with no matching focusgrab line after it, - // the panel really did open and something drew over it. - traceFrom = Date.now(); - console.log("launcher toggle -> " + (open ? "open" : "closed")); if (open) { searchInput.text = ""; activeCat = ""; @@ -2993,53 +2919,46 @@ in // window (or anywhere outside the bar) clears the grab and // closes whatever is open. Suspended while screenshotPinned so // slurp can grab input without dismissing the menu. - // Kept as the backstop for focus leaving without a click — a - // keybind that focuses another window, a workspace switch. Real - // outside clicks are the MouseArea's job now, because with the - // input region covering the screen the grab never sees them. + // KNOWN UNSOLVED: while an app that churns windows is running + // (Battle.net does it continuously), the launcher opens and this + // fires ~1ms later, so Super+R looks like it does nothing. // - // Re-arming on a fast clear was tried and removed: when the clear - // comes from refocus() (see mask), the very next refocus() kills - // the fresh grab too, so it just lost three times in a row before - // closing. The mask is what actually fixes it. + // Cause, from Hyprland 0.55.4 InputManager.cpp mouseMoveUnified(): + // + // if (m_seatGrab && !m_seatGrab->accepts(foundSurface) ...) { + // if (m_hardInput || refocus) { + // g_pSeatManager->setGrab(nullptr); + // + // foundSurface is the surface under the CURSOR, and `refocus` is + // true for all 20 CInputManager::refocus() call sites, which + // include every window map and unmap. So each window a busy app + // churns destroys the grab unless the cursor happens to be over + // this window's input region. No user input is involved, which is + // why it is invisible from the client side. It also explains the + // workaround: click a bar widget first (cursor lands on the bar) + // and the launcher stays up. + // + // Tried and reverted, both failed: + // - suppress_event activate/activatefocus on the app (nothing is + // requesting activation, so there is nothing to suppress) + // - re-arming the grab on a suspiciously fast clear (the next + // refocus() kills the fresh grab too — lost 3/3, every time) + // - expanding the input mask to the whole screen while open so + // accepts() is always true, with a MouseArea for outside + // clicks (behaviour changed, still died) + // + // Probably needs fixing upstream, or the launcher needs to stop + // depending on a seat grab at all. HyprlandFocusGrab { - id: focusGrab - active: bar.panelOpen + active: (launcherPanel.open || bar.activeDropdown !== null) && !bar.screenshotPinned windows: [bar] - property double armedAt: 0 - onActiveChanged: if (active) armedAt = Date.now() - onCleared: { if (bar.screenshotPinned) return; - if (launcherPanel.open || bar.activeDropdown !== null) { - const top = Hyprland.activeToplevel; - const cls = top && top.lastIpcObject ? top.lastIpcObject["class"] : "?"; - console.log("focusgrab cleared +" + (Date.now() - focusGrab.armedAt) - + "ms: launcher=" + launcherPanel.open - + " dropdown=" + (bar.activeDropdown !== null) - + " focus=" + (top ? cls + " / " + top.title : "none")); - } launcherPanel.open = false; bar.closeAllDropdowns(); } } - // Diagnostic for the "launcher opens and dies in the same frame - // while Battle.net runs" bug. Gated on a panel actually being - // open, so this is a handful of lines around each Super+R rather - // than a firehose — the whole point is to see which compositor - // event lands between `launcher toggle -> open` and `focusgrab - // cleared`. Read with `journalctl --user -u quickshell -f -o - // short-precise`; second resolution is too coarse here. - Connections { - target: Hyprland - function onRawEvent(event) { - const dt = Date.now() - launcherPanel.traceFrom; - if (dt < 3000) - console.log("hypr event +" + dt + "ms: " + event.name + " | " + event.data); - } - } - property var activeDropdown: null // Set by the screenshot keybind (via IPC) to hold menus open @@ -3072,19 +2991,6 @@ in if (a.startsWith("/")) return "file://" + a; return ""; } - // Clicking a notification jumps to the sending app: invoke its - // default action (apps use it to open the right chat/tab) and - // focus the window ourselves — misc:focus_on_activate is off, - // so the app's own activation request would be ignored. - function activateNotif(notif) { - if (!notif) return; - for (const a of notif.actions) - if (a.identifier === "default") { a.invoke(); break; } - const entry = (notif.desktopEntry || "").replace(/\.desktop$/, ""); - const m = [entry, notif.appName].filter(s => s !== "").join("|"); - if (m !== "") Hyprland.dispatch("focuswindow class:(?i)^(" + m + ")$"); - } - function toggleDropdown(dd, setupFn) { if (dd.visible && !dd.closing) { dd.animateClose(); @@ -5913,18 +5819,6 @@ in width: parent.width height: Math.max(notifPreview.visible ? 48 : 0, ncBody.height) - // First child, so the dismiss button and - // action chips still get their clicks. - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: { - bar.activateNotif(notifItem.modelData); - notifItem.modelData.dismiss(); - bar.closeAllDropdowns(); - } - } - // Image preview (album art, screenshot thumb…) ClippingRectangle { id: notifPreview @@ -6205,18 +6099,6 @@ in property string previewSource: bar.notifPreviewSource(toastItem.currentNotif) - // First child, so the dismiss button and action - // chips still get their clicks. - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: { - bar.activateNotif(toastItem.currentNotif); - toastItem.currentNotif.dismiss(); - toastItem.dismiss(); - } - } - // Image preview (screenshot thumb, album art…) ClippingRectangle { id: toastPreview