From 84f2c5c785132dd1641caf30e87be7b84a7e5791 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Sat, 12 Sep 2026 19:13:52 +0200 Subject: fix(window-switcher): focus after the overlay is really gone Picking a window focused nothing, and focus returned to whatever had it before. The overlay holds keyboard focus exclusively, and the compositor will not move window focus out from under that grab: the dispatch is accepted, reports ok, and is then ignored. Nothing in the log says so, which is why this looked like a dispatch that had worked. Closing first in the same turn is not enough either, because close() only clears a property and the layer surface survives until the frame after, so the dispatch still lands inside the grab. The target is now remembered and focused once the surface is actually gone. Tested in both directions, since the case that exposed it was cross-monitor: DP-1 to a window on DP-3, and back. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SYg4wYHq5XNbiVmMeKRb1S --- window-switcher/Switcher.qml | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) (limited to 'window-switcher/Switcher.qml') diff --git a/window-switcher/Switcher.qml b/window-switcher/Switcher.qml index 2e911b7..960cb58 100644 --- a/window-switcher/Switcher.qml +++ b/window-switcher/Switcher.qml @@ -29,6 +29,29 @@ Scope { // since the last showing would otherwise be missing from the grid. onOpenChanged: if (open) Windows.refresh(); + // A focus dispatched while the overlay is up is accepted and then ignored: + // the layer surface holds keyboard focus exclusively, and the compositor + // will not move window focus out from under that grab. Closing in the same + // turn does not help either, because close() only clears a property and the + // surface survives until the frame after. So the target is remembered and + // focused once the grab is really gone. + property var pendingFocus: null + + function focusAfterClose(toplevel) { + root.pendingFocus = toplevel; + root.close(); + focusTimer.restart(); + } + + Timer { + id: focusTimer + interval: 60 + onTriggered: { + if (root.pendingFocus) Windows.focus(root.pendingFocus); + root.pendingFocus = null; + } + } + // A config with no visible window exits, reporting nothing. This 1x1 // click-through window is what holds the shell open while the overlay // is hidden, which is almost always. @@ -71,10 +94,8 @@ Scope { function commit() { const list = Windows.windows; - if (selectedIndex >= 0 && selectedIndex < list.length) { - Windows.focus(list[selectedIndex]); - root.close(); - } + if (selectedIndex >= 0 && selectedIndex < list.length) + root.focusAfterClose(list[selectedIndex]); } function move(delta) { -- cgit v1.2.3