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.
This commit is contained in:
rope 2026-08-01 21:43:08 +01:00
parent 59c1b10334
commit dcfc7f9573
2 changed files with 45 additions and 165 deletions

View file

@ -287,22 +287,20 @@ in
suppress_event = "fullscreen maximize", suppress_event = "fullscreen maximize",
}) })
-- Battle.net spams window-activation events for as long as it is -- Battle.net requests activation on its own while running, which
-- running. Each one clears quickshell's focus grab, which closes the -- steals focus. Drop those events.
-- 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.
-- --
-- Non-Steam shortcuts have been BOTH steam_app_0 and -- Non-Steam shortcuts have been BOTH steam_app_0 and
-- steam_app_default across Steam versions (this rule matched only -- steam_app_default across Steam versions, and this rule matched only
-- steam_app_0, and silently stopped applying when Battle.net turned -- steam_app_0, so it silently stopped applying when Battle.net turned
-- up as steam_app_default that regression is the bug this alt -- up as steam_app_default. Match either.
-- fixes). Match either so a Steam update can't break it again. --
-- 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. -- WoW shares steam_app_default and so loses self-activation too.
-- Harmless: a game window is focused when it maps, which is not an -- Harmless: a game window is focused when it maps, which is not an

View file

@ -801,21 +801,10 @@ in
// Bound in hyprland.nix: Super+R launcher (bottom of the // Bound in hyprland.nix: Super+R launcher (bottom of the
// bar window), Super+L session menu (right edge). // 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 { IpcHandler {
target: "launcher" target: "launcher"
function toggle(): void { function toggle(): void { if (root.mainBar) root.mainBar.toggleLauncher(); }
if (!root.mainBar) { console.warn("ipc launcher.toggle: no bar registered"); return; } function powermenu(): void { if (root.mainBar) root.mainBar.toggleSession(); }
root.mainBar.toggleLauncher();
}
function powermenu(): void {
if (!root.mainBar) { console.warn("ipc launcher.powermenu: no bar registered"); return; }
root.mainBar.toggleSession();
}
} }
// Keybind cheat sheet Super+C in hyprland.nix. // Keybind cheat sheet Super+C in hyprland.nix.
@ -1021,18 +1010,11 @@ in
required property var shellRoot required property var shellRoot
screen: modelData screen: modelData
WlrLayershell.namespace: "quickshell-bar" 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 // OnDemand + HyprlandFocusGrab is the working combination
// (caelestia's): the grab redirects focus to this window and // (caelestia's): the grab redirects focus to this window and
// OnDemand lets the layer surface accept it. Exclusive fights // OnDemand lets the layer surface accept it. Exclusive fights
// the grab it self-clears and instantly closes the panel. // 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 // Vertical bar on the left edge. The surface still covers the
// whole screen (the frame band and every panel are drawn into // whole screen (the frame band and every panel are drawn into
@ -1049,32 +1031,6 @@ in
mask: Region { mask: Region {
item: barBgRect 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 { Region {
x: activeDropdown ? activeDropdown.x : 0 x: activeDropdown ? activeDropdown.x : 0
y: activeDropdown ? activeDropdown.y : 0 y: activeDropdown ? activeDropdown.y : 0
@ -1103,23 +1059,6 @@ in
width: Theme.barWidth 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 // Register the primary bar so shell.qml's IPC handler can
// reach the session menu. // reach the session menu.
Component.onCompleted: { Component.onCompleted: {
@ -1581,13 +1520,6 @@ in
// Non-empty when the query parses as arithmetic. // Non-empty when the query parses as arithmetic.
property string calcResult: "" 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: [ readonly property var catDefs: [
{ key: "All", label: "All apps", icon: "layout-grid" }, { key: "All", label: "All apps", icon: "layout-grid" },
{ key: "Network", label: "Internet", icon: "globe" }, { key: "Network", label: "Internet", icon: "globe" },
@ -1625,12 +1557,6 @@ in
function toggle() { function toggle() {
open = !open; 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) { if (open) {
searchInput.text = ""; searchInput.text = "";
activeCat = ""; activeCat = "";
@ -2993,53 +2919,46 @@ in
// window (or anywhere outside the bar) clears the grab and // window (or anywhere outside the bar) clears the grab and
// closes whatever is open. Suspended while screenshotPinned so // closes whatever is open. Suspended while screenshotPinned so
// slurp can grab input without dismissing the menu. // slurp can grab input without dismissing the menu.
// Kept as the backstop for focus leaving without a click a // KNOWN UNSOLVED: while an app that churns windows is running
// keybind that focuses another window, a workspace switch. Real // (Battle.net does it continuously), the launcher opens and this
// outside clicks are the MouseArea's job now, because with the // fires ~1ms later, so Super+R looks like it does nothing.
// input region covering the screen the grab never sees them.
// //
// Re-arming on a fast clear was tried and removed: when the clear // Cause, from Hyprland 0.55.4 InputManager.cpp mouseMoveUnified():
// comes from refocus() (see mask), the very next refocus() kills //
// the fresh grab too, so it just lost three times in a row before // if (m_seatGrab && !m_seatGrab->accepts(foundSurface) ...) {
// closing. The mask is what actually fixes it. // 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 { HyprlandFocusGrab {
id: focusGrab active: (launcherPanel.open || bar.activeDropdown !== null) && !bar.screenshotPinned
active: bar.panelOpen
windows: [bar] windows: [bar]
property double armedAt: 0
onActiveChanged: if (active) armedAt = Date.now()
onCleared: { onCleared: {
if (bar.screenshotPinned) return; 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; launcherPanel.open = false;
bar.closeAllDropdowns(); 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 property var activeDropdown: null
// Set by the screenshot keybind (via IPC) to hold menus open // Set by the screenshot keybind (via IPC) to hold menus open
@ -3072,19 +2991,6 @@ in
if (a.startsWith("/")) return "file://" + a; if (a.startsWith("/")) return "file://" + a;
return ""; 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) { function toggleDropdown(dd, setupFn) {
if (dd.visible && !dd.closing) { if (dd.visible && !dd.closing) {
dd.animateClose(); dd.animateClose();
@ -5913,18 +5819,6 @@ in
width: parent.width width: parent.width
height: Math.max(notifPreview.visible ? 48 : 0, ncBody.height) 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) // Image preview (album art, screenshot thumb)
ClippingRectangle { ClippingRectangle {
id: notifPreview id: notifPreview
@ -6205,18 +6099,6 @@ in
property string previewSource: bar.notifPreviewSource(toastItem.currentNotif) 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) // Image preview (screenshot thumb, album art)
ClippingRectangle { ClippingRectangle {
id: toastPreview id: toastPreview