diff --git a/distro-packages/ArchLinux/wine-access-git/PKGBUILD b/distro-packages/ArchLinux/wine-access-git/PKGBUILD index 44de09a..f8ad852 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=r43.ge3de71e +pkgver=r44.gea50950 pkgrel=1 pkgdesc='Wine accessibility helper with MSAA, NVDA Controller, and Tolk compatibility for screen readers' arch=('x86_64') diff --git a/foreground_focus.h b/foreground_focus.h index 2a9f52d..7c01a97 100644 --- a/foreground_focus.h +++ b/foreground_focus.h @@ -3,4 +3,5 @@ // Recover a virtual desktop which regained X11 focus with no Win32 foreground. // Leave existing focus, other desktops and ambiguous application sets alone. +// Returns whether activation was queued; the application restores control focus. bool restore_virtual_desktop_focus(); diff --git a/review_x11.cpp b/review_x11.cpp index 0ae3025..26f90a0 100644 --- a/review_x11.cpp +++ b/review_x11.cpp @@ -112,26 +112,17 @@ bool wine_x11_restore_focus(HWND window) { const uint64_t deadline = GetTickCount64() + 20; auto tree = reply(xcb_query_tree(connection.value, xid).sequence, deadline); if (!tree || tree->parent != desktop) return false; - // Native focus informs Wine's application thread of activation. A helper's - // SetForegroundWindow can be denied by the Win32 foreground-lock policy. - constexpr char protocolsName[] = "WM_PROTOCOLS"; - constexpr char focusName[] = "WM_TAKE_FOCUS"; - auto protocols = reply(xcb_intern_atom(connection.value, 1, - sizeof(protocolsName) - 1, protocolsName).sequence, deadline); - auto takeFocus = reply(xcb_intern_atom(connection.value, 1, - 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(window)) return false; - xcb_client_message_event_t event{}; - event.response_type = XCB_CLIENT_MESSAGE; - event.format = 32; - event.window = xid; - event.type = protocols->atom; - event.data.data32[0] = takeFocus->atom; - event.data.data32[1] = XCB_CURRENT_TIME; - xcb_send_event(connection.value, 0, xid, XCB_EVENT_MASK_NO_EVENT, reinterpret_cast(&event)); + // WM_TAKE_FOCUS can be ignored while Wine awaits WM_STATE, which can remain + // unset for virtual-desktop children. Queue activation on the owner's thread: + // a helper-thread SetForegroundWindow can be denied by Wine's input policy. + // Async dispatch avoids waiting on an unresponsive application's window proc. + if (!SetWindowPos(window, HWND_TOP, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_ASYNCWINDOWPOS) || + !wine_x11_desktop_focused(window)) + return false; xcb_set_input_focus(connection.value, XCB_INPUT_FOCUS_PARENT, xid, XCB_CURRENT_TIME); auto focus = reply(xcb_get_input_focus(connection.value).sequence, deadline); return focus && focus->focus == xid; diff --git a/review_x11.h b/review_x11.h index 5f178f3..3cabac8 100644 --- a/review_x11.h +++ b/review_x11.h @@ -11,5 +11,6 @@ bool review_x11_geometry(HWND window, RECT &bounds, POINT &origin); // 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. +// Queue Win32 activation on the verified child's thread and set native focus. +// A true result means dispatched; control focus arrives asynchronously. bool wine_x11_restore_focus(HWND window); diff --git a/tests/README.md b/tests/README.md index e9b4577..354424d 100644 --- a/tests/README.md +++ b/tests/README.md @@ -203,10 +203,19 @@ 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. 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. +with no Win32 foreground, as seen after AudioQuake exits. Recovery must restore +both Win32 activation/control focus and native keyboard focus, even when X11 +already selects that child. The fixture deliberately leaves `WM_STATE` unset, +matching Wine's real virtual-desktop children. Adding a synthetic window-manager +acknowledgement masks the failure: native `WM_TAKE_FOCUS` alone can otherwise be +ignored while Wine awaits that state change. The runner propagates the fixture's +actual exit status. + +Recovery queues `SetWindowPos` activation on the application's own window +thread and sets native X11 focus without changing its size or position. It +does not wait on the application's window procedure. A dispatched request +alone is not acceptance: the fixture then checks Win32 foreground and the +restored button focus after the application processes activation. After installing the updated helper and restarting the reader, open AudioQuake, play and close a game, then Tab and Shift+Tab through the launcher. Check speech diff --git a/tests/foreground_focus_test.cpp b/tests/foreground_focus_test.cpp index 592160c..eee97d4 100644 --- a/tests/foreground_focus_test.cpp +++ b/tests/foreground_focus_test.cpp @@ -57,21 +57,6 @@ int main(int argc, char **argv) { if (!atomReply) return 2; const auto activeAtom = atomReply->atom; std::free(atomReply); - constexpr char stateName[] = "WM_STATE"; - auto *stateReply = xcb_intern_atom_reply(connection, - xcb_intern_atom(connection, 0, sizeof(stateName) - 1, stateName), nullptr); - if (!stateReply) return 2; - const auto stateAtom = stateReply->atom; - std::free(stateReply); - auto acknowledge = [&](HWND window) { - const auto xid = static_cast(reinterpret_cast( - GetPropW(window, L"__wine_x11_whole_window"))); - const uint32_t state[] = {1, XCB_WINDOW_NONE}; // ICCCM NormalState. - xcb_change_property(connection, XCB_PROP_MODE_REPLACE, xid, - stateAtom, stateAtom, 32, 2, state); - xcb_flush(connection); - pump(); - }; const auto linuxWindow = xcb_generate_id(connection); xcb_create_window(connection, XCB_COPY_FROM_PARENT, linuxWindow, screen->root, 0, 0, 20, 20, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, screen->root_visual, 0, nullptr); @@ -97,7 +82,7 @@ int main(int argc, char **argv) { ShowWindow(launcher, SW_HIDE); pump(); ShowWindow(launcher, SW_SHOWNOACTIVATE); - acknowledge(launcher); + pump(); focus(linuxWindow, linuxWindow); ok &= check(!GetForegroundWindow(), "Linux focus clears Wine foreground"); ok &= check(!restore_virtual_desktop_focus() && !GetForegroundWindow(), "Linux focus is never taken"); @@ -111,7 +96,7 @@ int main(int argc, char **argv) { ok &= check(!restore_virtual_desktop_focus(), "disabled application is not activated"); EnableWindow(launcher, TRUE); ShowWindow(other, SW_SHOWNOACTIVATE); - acknowledge(other); + pump(); focus(linuxWindow, linuxWindow); focus(desktop, desktop); ok &= check(!restore_virtual_desktop_focus() && !GetForegroundWindow(), "ambiguous applications are not chosen"); @@ -126,7 +111,7 @@ int main(int argc, char **argv) { ShowWindow(launcher, SW_HIDE); pump(); ShowWindow(launcher, SW_SHOWNOACTIVATE); - acknowledge(launcher); + pump(); focus(linuxWindow, linuxWindow); focus(desktop, desktop); ok &= check(restore_virtual_desktop_focus(), "later workspace return recovers again"); @@ -134,14 +119,14 @@ int main(int argc, char **argv) { ShowWindow(launcher, SW_HIDE); pump(); ShowWindow(launcher, SW_SHOWNOACTIVATE); - acknowledge(launcher); + pump(); 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); + pump(); focus(launcherXid, desktop); ok &= check(!restore_virtual_desktop_focus(), "focused child does not bypass ambiguous applications"); ShowWindow(other, SW_HIDE);