Skip to content

Pass focus when KWin falls back to the desktop window on close/minimize (issue 136) - #195

Open
dev-ahad-ali wants to merge 1 commit into
peterfajdiga:masterfrom
dev-ahad-ali:fix-focus-passing-offscreen-neighbors
Open

Pass focus when KWin falls back to the desktop window on close/minimize (issue 136)#195
dev-ahad-ali wants to merge 1 commit into
peterfajdiga:masterfrom
dev-ahad-ali:fix-focus-passing-offscreen-neighbors

Conversation

@dev-ahad-ali

Copy link
Copy Markdown

Fixes #136. Very likely also fixes #146 (same trigger: only one window visible, neighbors entirely off-screen).

Problem

With no gaps and margins, a full-width column's neighbors lie entirely off-screen. Closing (or minimizing) that column's window then leaves no window focused.

Cause

On Wayland, KWin activates another window before it emits windowRemoved (Workspace::removeWaylandWindowactivateNextWindow) and before minimizedChanged (XdgToplevelWindow::doMinimizeactivateNextWindow).

Since Plasma 6, SeparateScreenFocus defaults to true (kwin.kcfg). With it on, FocusChain::isUsableFocusCandidate requires Window::isOnOutput, which is output->geometry().intersects(frameGeometry()). A neighbor that is entirely off-screen fails that check, so KWin finds no candidate and falls back to activating the desktop window (findDesktop).

Karousel receives windowActivated(desktopWindow) first, records it as the last focused client, and then ClientManager.removeClient sees the closed window is no longer the last focused client and downgrades passFocus to None. Karousel's own neighbor-focus logic never runs. With a left/right margin the neighbor intersects the screen by a few pixels, KWin's chain accepts it, and everything works, which is why the bug only shows with zero margins.

(Verified against the Plasma/6.7 sources: activation.cpp, focuschain.cpp, window.cpp, xdgshellwindow.cpp, kwin.kcfg.)

Fix

ClientManager now remembers the previously focused client. If the last focused client is a desktop window and the client being removed/minimized is the one focused right before it, treat that as KWin's fallback rather than a real focus change and still pass focus. Karousel's Window.focus() goes through Workspace.activeWindow = …activateWindowrequestFocus, which has no on-screen check, so focusing the off-screen neighbor succeeds and the resulting focus event scrolls it into view.

desktopWindow is added to the KwinClient interface (KWin scripting API: Q_PROPERTY(bool desktopWindow READ isDesktop CONSTANT)).

Tests

  • MockWorkspace.removeWindow and the mock minimized setter now mimic Workspace::activateNextWindow: only windows intersecting the screen are candidates, otherwise the desktop window gets activated (a desktop window is now always present in the mock workspace).
  • Two new flows in passFocus.ts cover close and minimize with zero gaps and full-width windows. Both fail on master (focus stays on Desktop) and pass with the fix.
  • MockQSignal.fire no longer delivers an emission to handlers connected (or disconnected) during that emission, matching Qt. Without this, minimizing a tiled window in the mock ping-pongs between Tiled and TiledMinimized forever.

make lint and make test (51 tests) pass.

When the active window is closed or minimized, KWin activates another
window before it emits `windowRemoved` or `minimizedChanged`. With KWin's
default `SeparateScreenFocus=true`, its focus chain only considers windows
whose frame intersects the screen. When all other windows are entirely
off-screen (e.g. a full-width column with no gaps and margins), KWin finds
no candidate and activates the desktop window instead.

Karousel then saw the desktop window as the last focused client, so it
skipped passing focus from the closed window, leaving no window focused.

Treat a desktop window that got activated right after the closed/minimized
window as a KWin fallback rather than a real focus change, and still pass
focus to the neighboring window in that case.

Tests: model KWin's pre-removal activation (including the desktop window
fallback) in the mock workspace, and make MockQSignal match Qt by not
delivering an emission to handlers connected during that emission.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant