diff --git a/distro-packages/ArchLinux/wine-access-git/PKGBUILD b/distro-packages/ArchLinux/wine-access-git/PKGBUILD index 8809a0a..44de09a 100644 --- a/distro-packages/ArchLinux/wine-access-git/PKGBUILD +++ b/distro-packages/ArchLinux/wine-access-git/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: Storm Dragon pkgname=wine-access-git -pkgver=r41.gb16c696 +pkgver=r43.ge3de71e pkgrel=1 pkgdesc='Wine accessibility helper with MSAA, NVDA Controller, and Tolk compatibility for screen readers' arch=('x86_64') diff --git a/foreground_focus.cpp b/foreground_focus.cpp index 125627c..8c6174b 100644 --- a/foreground_focus.cpp +++ b/foreground_focus.cpp @@ -19,12 +19,12 @@ BOOL CALLBACK find_candidate(HWND window, LPARAM data) { } bool restore_virtual_desktop_focus() { - if (GetForegroundWindow() || !wine_x11_desktop_focused()) return false; + if (GetForegroundWindow()) return false; Candidate candidate; EnumWindows(find_candidate, reinterpret_cast(&candidate)); // Recheck focus after enumeration so a Linux/application transition cannot // turn a recovery into activation of a background Wine application. - if (candidate.count != 1 || GetForegroundWindow() || !wine_x11_desktop_focused()) + if (candidate.count != 1 || GetForegroundWindow() || !wine_x11_desktop_focused(candidate.window)) return false; return wine_x11_restore_focus(candidate.window); } diff --git a/review_x11.cpp b/review_x11.cpp index e421170..0ae3025 100644 --- a/review_x11.cpp +++ b/review_x11.cpp @@ -61,7 +61,7 @@ bool review_x11_geometry(HWND window, RECT &bounds, POINT &origin) { return true; } -bool wine_x11_desktop_focused() { +bool wine_x11_desktop_focused(HWND window) { USEROBJECTFLAGS flags{}; if (!GetUserObjectInformationW(GetThreadDesktop(GetCurrentThreadId()), UOI_FLAGS, &flags, sizeof(flags), nullptr) || !(flags.dwFlags & 0x80000000)) @@ -71,7 +71,24 @@ bool wine_x11_desktop_focused() { return false; const uint64_t deadline = GetTickCount64() + 20; auto focus = reply(xcb_get_input_focus(connection.value).sequence, deadline); - if (!focus || focus->focus != xid) return false; + if (!focus) return false; + if (focus->focus != xid) { + if (!window) return false; + const auto child = reinterpret_cast(GetPropW(window, L"__wine_x11_whole_window")); + if (!child || child > UINT32_MAX) return false; + auto tree = reply(xcb_query_tree(connection.value, child).sequence, deadline); + if (!tree || tree->parent != xid) return false; + // A focused native client can outlive Wine's remembered Win32 foreground. + // Accept only this application's hierarchy, never a sibling Wine child. + auto focused = focus->focus; + for (unsigned depth = 0; focused != child && depth < 64; ++depth) { + if (!focused || focused == XCB_INPUT_FOCUS_POINTER_ROOT || focused == xid) return false; + tree = reply(xcb_query_tree(connection.value, focused).sequence, deadline); + if (!tree || tree->parent == focused) return false; + focused = tree->parent; + } + if (focused != child) return false; + } auto size = reply(xcb_get_geometry(connection.value, xid).sequence, deadline); if (!size) return false; constexpr char name[] = "_NET_ACTIVE_WINDOW"; @@ -79,16 +96,16 @@ bool wine_x11_desktop_focused() { if (!atom || !atom->atom) return false; auto active = reply(xcb_get_property(connection.value, 0, size->root, atom->atom, XCB_ATOM_WINDOW, 0, 1).sequence, deadline); - uint32_t window = 0; + uint32_t activeWindow = 0; if (!active || active->type != XCB_ATOM_WINDOW || active->format != 32 || - xcb_get_property_value_length(active.get()) != sizeof(window)) + xcb_get_property_value_length(active.get()) != sizeof(activeWindow)) return false; - std::memcpy(&window, xcb_get_property_value(active.get()), sizeof(window)); - return window == xid; + std::memcpy(&activeWindow, xcb_get_property_value(active.get()), sizeof(activeWindow)); + return activeWindow == xid; } bool wine_x11_restore_focus(HWND window) { - if (!wine_x11_desktop_focused()) return false; + if (!wine_x11_desktop_focused(window)) return false; const auto xid = reinterpret_cast(GetPropW(window, L"__wine_x11_whole_window")); const auto desktop = reinterpret_cast(GetPropW(GetDesktopWindow(), L"__wine_x11_whole_window")); if (!xid || xid > UINT32_MAX) return false; @@ -105,7 +122,7 @@ bool wine_x11_restore_focus(HWND window) { sizeof(focusName) - 1, focusName).sequence, deadline); if (!protocols || !protocols->atom || !takeFocus || !takeFocus->atom) return false; if (GetForegroundWindow() || !IsWindowVisible(window) || !IsWindowEnabled(window) || - IsIconic(window) || !wine_x11_desktop_focused()) + IsIconic(window) || !wine_x11_desktop_focused(window)) return false; xcb_client_message_event_t event{}; event.response_type = XCB_CLIENT_MESSAGE; diff --git a/review_x11.h b/review_x11.h index 97ca2e0..5f178f3 100644 --- a/review_x11.h +++ b/review_x11.h @@ -7,8 +7,9 @@ bool review_x11_geometry(HWND window, RECT &bounds, POINT &origin); // True only when both the window manager and X11 focus select this prefix's -// virtual desktop itself, rather than another application or desktop child. -bool wine_x11_desktop_focused(); +// virtual desktop, with keyboard focus on the desktop itself or, when supplied, +// the verified application's X11 child hierarchy. +bool wine_x11_desktop_focused(HWND window = nullptr); // Deliver native focus to the application's actual child of this desktop. bool wine_x11_restore_focus(HWND window); diff --git a/tests/README.md b/tests/README.md index 02c5b22..e9b4577 100644 --- a/tests/README.md +++ b/tests/README.md @@ -202,7 +202,10 @@ Windows launcher fixture. It reproduces a visible launcher returned without activation, then checks recovery of its native foreground and button focus. It also checks Linux focus, mismatched window-manager/keyboard focus, disabled windows, multiple visible applications, hidden windows, existing foreground, -and repeated recovery. The fixture acknowledges ICCCM window visibility on +and repeated recovery. It also reproduces X11 focus on the launcher child +with no Win32 foreground, as seen after AudioQuake exits: recovery must send +Wine's activation notification even when X11 focus already selects that child. +The fixture acknowledges ICCCM window visibility on the private display; the runner propagates the fixture's actual exit status. After installing the updated helper and restarting the reader, open AudioQuake, diff --git a/tests/foreground_focus_test.cpp b/tests/foreground_focus_test.cpp index 7ab34d6..592160c 100644 --- a/tests/foreground_focus_test.cpp +++ b/tests/foreground_focus_test.cpp @@ -130,6 +130,38 @@ int main(int argc, char **argv) { focus(linuxWindow, linuxWindow); focus(desktop, desktop); ok &= check(restore_virtual_desktop_focus(), "later workspace return recovers again"); + pump(); + ShowWindow(launcher, SW_HIDE); + pump(); + ShowWindow(launcher, SW_SHOWNOACTIVATE); + acknowledge(launcher); + focus(linuxWindow, linuxWindow); + const auto launcherXid = static_cast(reinterpret_cast( + GetPropW(launcher, L"__wine_x11_whole_window"))); + focus(launcherXid, linuxWindow); + ok &= check(!restore_virtual_desktop_focus(), "focused child requires window manager desktop ownership"); + ShowWindow(other, SW_SHOWNOACTIVATE); + acknowledge(other); + focus(launcherXid, desktop); + ok &= check(!restore_virtual_desktop_focus(), "focused child does not bypass ambiguous applications"); + ShowWindow(other, SW_HIDE); + const auto siblingXid = xcb_generate_id(connection); + xcb_create_window(connection, XCB_COPY_FROM_PARENT, siblingXid, desktop, + 0, 0, 20, 20, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, screen->root_visual, 0, nullptr); + xcb_map_window(connection, siblingXid); + focus(siblingXid, desktop); + ok &= check(!restore_virtual_desktop_focus(), "unrelated desktop child is never redirected to launcher"); + xcb_destroy_window(connection, siblingXid); + focus(launcherXid, desktop); + EnableWindow(launcher, FALSE); + ok &= check(!restore_virtual_desktop_focus(), "focused child does not bypass disabled application guard"); + EnableWindow(launcher, TRUE); + ok &= check(!GetForegroundWindow(), "reproduce focused X11 child without Win32 foreground"); + ok &= check(restore_virtual_desktop_focus(), "focused launcher child recovers foreground"); + pump(); + GetGUIThreadInfo(GetWindowThreadProcessId(launcher, nullptr), &gui); + ok &= check(GetForegroundWindow() == launcher && gui.hwndFocus == button, + "focused launcher child restores its control focus"); DestroyWindow(other); PostMessageW(launcher, WM_CLOSE, 0, 0); WaitForSingleObject(process.hProcess, 5000);