Restore wine access after program loses and regains focus.
This commit is contained in:
1 parent
f40e471938
commit
c3e981b4c2
12 files changed
+351
-2
No files matched your search
+16
-1
@@ -49,7 +49,7 @@ add_custom_command(
|
|||||||
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/nvdaController.idl"
|
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/nvdaController.idl"
|
||||||
VERBATIM)
|
VERBATIM)
|
||||||
|
|
||||||
add_executable(cthulhu-wine-access main.cpp list_view.cpp native_items.cpp review_capture.cpp review_x11.cpp review_service.cpp "${generated_server}")
|
add_executable(cthulhu-wine-access main.cpp foreground_focus.cpp list_view.cpp native_items.cpp review_capture.cpp review_x11.cpp review_service.cpp "${generated_server}")
|
||||||
add_dependencies(cthulhu-wine-access wine-access-menu wine-access-menu32)
|
add_dependencies(cthulhu-wine-access wine-access-menu wine-access-menu32)
|
||||||
target_compile_features(cthulhu-wine-access PRIVATE cxx_std_20)
|
target_compile_features(cthulhu-wine-access PRIVATE cxx_std_20)
|
||||||
target_include_directories(cthulhu-wine-access PRIVATE "${generated_dir}")
|
target_include_directories(cthulhu-wine-access PRIVATE "${generated_dir}")
|
||||||
@@ -61,6 +61,21 @@ target_compile_options(cthulhu-wine-access PRIVATE
|
|||||||
|
|
||||||
include(CTest)
|
include(CTest)
|
||||||
if(BUILD_TESTING)
|
if(BUILD_TESTING)
|
||||||
|
set(foreground_fixture "${CMAKE_CURRENT_BINARY_DIR}/foreground-fixture.exe")
|
||||||
|
add_custom_command(OUTPUT "${foreground_fixture}"
|
||||||
|
COMMAND "${MINGW_C_COMPILER}" -std=c11 -Wall -Wextra -mwindows
|
||||||
|
-o "${foreground_fixture}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/foreground_fixture.c" -luser32
|
||||||
|
DEPENDS tests/foreground_fixture.c VERBATIM)
|
||||||
|
add_custom_target(foreground-fixture ALL DEPENDS "${foreground_fixture}")
|
||||||
|
add_executable(foreground-focus-test tests/foreground_focus_test.cpp foreground_focus.cpp review_x11.cpp)
|
||||||
|
add_dependencies(foreground-focus-test foreground-fixture)
|
||||||
|
target_compile_features(foreground-focus-test PRIVATE cxx_std_20)
|
||||||
|
target_compile_options(foreground-focus-test PRIVATE -Wall -Wextra)
|
||||||
|
target_link_libraries(foreground-focus-test PRIVATE user32 PkgConfig::XCB)
|
||||||
|
add_test(NAME foreground-focus COMMAND python3
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/foreground_focus_test.py" "${CMAKE_CURRENT_BINARY_DIR}")
|
||||||
|
set_tests_properties(foreground-focus PROPERTIES TIMEOUT 90 ENVIRONMENT
|
||||||
|
"WINEPREFIX=${CMAKE_CURRENT_BINARY_DIR}/focus-prefix;CTHULHU_WINE_ACCESS_MANAGED=1;WINEDEBUG=-all;WINEDLLOVERRIDES=mscoree,mshtml=")
|
||||||
add_executable(msaa-states-test tests/msaa_states_test.cpp)
|
add_executable(msaa-states-test tests/msaa_states_test.cpp)
|
||||||
target_compile_features(msaa-states-test PRIVATE cxx_std_20)
|
target_compile_features(msaa-states-test PRIVATE cxx_std_20)
|
||||||
target_compile_options(msaa-states-test PRIVATE -Wall -Wextra)
|
target_compile_options(msaa-states-test PRIVATE -Wall -Wextra)
|
||||||
|
|||||||
@@ -89,6 +89,13 @@ The helper is started automatically when Cthulhu-Go detects an active Wine
|
|||||||
loader for the same user and prefix. It requires the Go service to be running;
|
loader for the same user and prefix. It requires the Go service to be running;
|
||||||
it exits if `org.stormux.Cthulhu1.Service` is unavailable.
|
it exits if `org.stormux.Cthulhu1.Service` is unavailable.
|
||||||
|
|
||||||
|
When a game closes inside a Wine virtual desktop, the desktop can regain X11
|
||||||
|
focus while Wine has no foreground application. The helper restores native
|
||||||
|
focus when exactly one visible, enabled application remains. Both X11 keyboard
|
||||||
|
focus and the window manager's active window must identify that same virtual
|
||||||
|
desktop. Existing application focus, Linux windows, minimized/disabled windows,
|
||||||
|
and ambiguous choices between multiple applications are left alone.
|
||||||
|
|
||||||
## Protocol and behavior
|
## Protocol and behavior
|
||||||
|
|
||||||
`nvdaController.idl` contains the compatible local RPC interfaces. The helper
|
`nvdaController.idl` contains the compatible local RPC interfaces. The helper
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Maintainer: Storm Dragon <storm_dragon@stormux.org>
|
# Maintainer: Storm Dragon <storm_dragon@stormux.org>
|
||||||
pkgname=wine-access-git
|
pkgname=wine-access-git
|
||||||
pkgver=r18.gdf7879a
|
pkgver=r22.gf40e471
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc='Wine accessibility helper with MSAA, NVDA Controller, and Tolk compatibility for Cthulhu'
|
pkgdesc='Wine accessibility helper with MSAA, NVDA Controller, and Tolk compatibility for Cthulhu'
|
||||||
arch=('x86_64')
|
arch=('x86_64')
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
#include "foreground_focus.h"
|
||||||
|
#include "review_x11.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
struct Candidate {
|
||||||
|
HWND window = nullptr;
|
||||||
|
unsigned count = 0;
|
||||||
|
};
|
||||||
|
BOOL CALLBACK find_candidate(HWND window, LPARAM data) {
|
||||||
|
if (!IsWindowVisible(window) || !IsWindowEnabled(window) || IsIconic(window) ||
|
||||||
|
(GetWindowLongPtrW(window, GWL_EXSTYLE) & (WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE)) ||
|
||||||
|
!GetPropW(window, L"__wine_x11_whole_window"))
|
||||||
|
return TRUE;
|
||||||
|
auto &candidate = *reinterpret_cast<Candidate *>(data);
|
||||||
|
candidate.window = window;
|
||||||
|
return ++candidate.count < 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool restore_virtual_desktop_focus() {
|
||||||
|
if (GetForegroundWindow() || !wine_x11_desktop_focused()) return false;
|
||||||
|
Candidate candidate;
|
||||||
|
EnumWindows(find_candidate, reinterpret_cast<LPARAM>(&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())
|
||||||
|
return false;
|
||||||
|
return wine_x11_restore_focus(candidate.window);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Recover a virtual desktop which regained X11 focus with no Win32 foreground.
|
||||||
|
// Leave existing focus, other desktops and ambiguous application sets alone.
|
||||||
|
bool restore_virtual_desktop_focus();
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
#include "control_class.h"
|
#include "control_class.h"
|
||||||
#include "native_items.h"
|
#include "native_items.h"
|
||||||
#include "review_service.h"
|
#include "review_service.h"
|
||||||
|
#include "foreground_focus.h"
|
||||||
|
|
||||||
#include <commctrl.h>
|
#include <commctrl.h>
|
||||||
#include <gio/gio.h>
|
#include <gio/gio.h>
|
||||||
@@ -1047,6 +1048,7 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) {
|
|||||||
while (GetMessageW(&message, nullptr, 0, 0) > 0) {
|
while (GetMessageW(&message, nullptr, 0, 0) > 0) {
|
||||||
if (listTimer && message.message == WM_TIMER && message.hwnd == nullptr &&
|
if (listTimer && message.message == WM_TIMER && message.hwnd == nullptr &&
|
||||||
message.wParam == listTimer) {
|
message.wParam == listTimer) {
|
||||||
|
restore_virtual_desktop_focus();
|
||||||
poll_control_focus();
|
poll_control_focus();
|
||||||
reviewService.focus_changed(reviewIdentities.observe_foreground());
|
reviewService.focus_changed(reviewIdentities.observe_foreground());
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <poll.h>
|
#include <poll.h>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
struct Connection {
|
struct Connection {
|
||||||
@@ -59,3 +60,62 @@ bool review_x11_geometry(HWND window, RECT &bounds, POINT &origin) {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool wine_x11_desktop_focused() {
|
||||||
|
USEROBJECTFLAGS flags{};
|
||||||
|
if (!GetUserObjectInformationW(GetThreadDesktop(GetCurrentThreadId()), UOI_FLAGS,
|
||||||
|
&flags, sizeof(flags), nullptr) || !(flags.dwFlags & 0x80000000))
|
||||||
|
return false;
|
||||||
|
const auto xid = reinterpret_cast<uintptr_t>(GetPropW(GetDesktopWindow(), L"__wine_x11_whole_window"));
|
||||||
|
if (!xid || xid > UINT32_MAX || !connection.value || xcb_connection_has_error(connection.value))
|
||||||
|
return false;
|
||||||
|
const uint64_t deadline = GetTickCount64() + 20;
|
||||||
|
auto focus = reply<xcb_get_input_focus_reply_t>(xcb_get_input_focus(connection.value).sequence, deadline);
|
||||||
|
if (!focus || focus->focus != xid) return false;
|
||||||
|
auto size = reply<xcb_get_geometry_reply_t>(xcb_get_geometry(connection.value, xid).sequence, deadline);
|
||||||
|
if (!size) return false;
|
||||||
|
constexpr char name[] = "_NET_ACTIVE_WINDOW";
|
||||||
|
auto atom = reply<xcb_intern_atom_reply_t>(xcb_intern_atom(connection.value, 1, sizeof(name) - 1, name).sequence, deadline);
|
||||||
|
if (!atom || !atom->atom) return false;
|
||||||
|
auto active = reply<xcb_get_property_reply_t>(xcb_get_property(connection.value, 0, size->root,
|
||||||
|
atom->atom, XCB_ATOM_WINDOW, 0, 1).sequence, deadline);
|
||||||
|
uint32_t window = 0;
|
||||||
|
if (!active || active->type != XCB_ATOM_WINDOW || active->format != 32 ||
|
||||||
|
xcb_get_property_value_length(active.get()) != sizeof(window))
|
||||||
|
return false;
|
||||||
|
std::memcpy(&window, xcb_get_property_value(active.get()), sizeof(window));
|
||||||
|
return window == xid;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool wine_x11_restore_focus(HWND window) {
|
||||||
|
if (!wine_x11_desktop_focused()) return false;
|
||||||
|
const auto xid = reinterpret_cast<uintptr_t>(GetPropW(window, L"__wine_x11_whole_window"));
|
||||||
|
const auto desktop = reinterpret_cast<uintptr_t>(GetPropW(GetDesktopWindow(), L"__wine_x11_whole_window"));
|
||||||
|
if (!xid || xid > UINT32_MAX) return false;
|
||||||
|
const uint64_t deadline = GetTickCount64() + 20;
|
||||||
|
auto tree = reply<xcb_query_tree_reply_t>(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_reply_t>(xcb_intern_atom(connection.value, 1,
|
||||||
|
sizeof(protocolsName) - 1, protocolsName).sequence, deadline);
|
||||||
|
auto takeFocus = reply<xcb_intern_atom_reply_t>(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())
|
||||||
|
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<const char *>(&event));
|
||||||
|
xcb_set_input_focus(connection.value, XCB_INPUT_FOCUS_PARENT, xid, XCB_CURRENT_TIME);
|
||||||
|
auto focus = reply<xcb_get_input_focus_reply_t>(xcb_get_input_focus(connection.value).sequence, deadline);
|
||||||
|
return focus && focus->focus == xid;
|
||||||
|
}
|
||||||
@@ -5,3 +5,10 @@
|
|||||||
// Query Wine's actual X11 window, not a title/geometry search. Origin translates
|
// Query Wine's actual X11 window, not a title/geometry search. Origin translates
|
||||||
// desktop-local Win32 positions to physical root coordinates in virtual mode.
|
// desktop-local Win32 positions to physical root coordinates in virtual mode.
|
||||||
bool review_x11_geometry(HWND window, RECT &bounds, POINT &origin);
|
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();
|
||||||
|
|
||||||
|
// Deliver native focus to the application's actual child of this desktop.
|
||||||
|
bool wine_x11_restore_focus(HWND window);
|
||||||
@@ -1,5 +1,24 @@
|
|||||||
# Wine control integration
|
# Wine control integration
|
||||||
|
|
||||||
|
## Virtual desktop focus recovery
|
||||||
|
|
||||||
|
`xvfb-run -a ctest --test-dir build -R foreground-focus --output-on-failure`
|
||||||
|
uses a disposable Wine prefix configured for a virtual desktop and a separate
|
||||||
|
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
|
||||||
|
the private display; the runner propagates the fixture's actual exit status.
|
||||||
|
|
||||||
|
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
|
||||||
|
and braille for each control. Switch to Linux and back to workspace 3 and repeat.
|
||||||
|
Recheck ordinary Wine menus, edits and lists, including password exclusion.
|
||||||
|
With two visible applications in one virtual desktop, recovery intentionally
|
||||||
|
does not choose between them; focus the intended application normally.
|
||||||
|
Automated focus checks do not prove audible speech or physical braille.
|
||||||
|
|
||||||
## Wine flat-review capture
|
## Wine flat-review capture
|
||||||
|
|
||||||
Build the helper and run `ctest --test-dir build --output-on-failure` on a
|
Build the helper and run `ctest --test-dir build --output-on-failure` on a
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
#define UNICODE
|
||||||
|
#define _UNICODE
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
static HWND button;
|
||||||
|
static LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wp, LPARAM lp) {
|
||||||
|
if ((message == WM_SETFOCUS || (message == WM_ACTIVATE && LOWORD(wp) != WA_INACTIVE)) && button)
|
||||||
|
SetFocus(button);
|
||||||
|
if (message == WM_DESTROY) { PostQuitMessage(0); return 0; }
|
||||||
|
return DefWindowProcW(window, message, wp, lp);
|
||||||
|
}
|
||||||
|
int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR args, int show) {
|
||||||
|
(void)previous; (void)args; (void)show;
|
||||||
|
WNDCLASSW cls = {0};
|
||||||
|
cls.hInstance = instance;
|
||||||
|
cls.lpfnWndProc = window_proc;
|
||||||
|
cls.lpszClassName = L"WineAccessForegroundFixture";
|
||||||
|
RegisterClassW(&cls);
|
||||||
|
HWND launcher = CreateWindowW(cls.lpszClassName, L"Launcher fixture", WS_OVERLAPPEDWINDOW,
|
||||||
|
50, 50, 300, 200, NULL, NULL, instance, NULL);
|
||||||
|
button = CreateWindowW(L"Button", L"Play fixture", WS_CHILD | WS_VISIBLE | WS_TABSTOP,
|
||||||
|
10, 10, 100, 30, launcher, (HMENU)1, instance, NULL);
|
||||||
|
ShowWindow(launcher, SW_SHOW);
|
||||||
|
SetForegroundWindow(launcher);
|
||||||
|
MSG message;
|
||||||
|
while (GetMessageW(&message, NULL, 0, 0) > 0) {
|
||||||
|
TranslateMessage(&message);
|
||||||
|
DispatchMessageW(&message);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
#include "../foreground_focus.h"
|
||||||
|
#include "../review_x11.h"
|
||||||
|
#pragma push_macro("_WIN32")
|
||||||
|
#undef _WIN32
|
||||||
|
#include <xcb/xcb.h>
|
||||||
|
#pragma pop_macro("_WIN32")
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
void pump() {
|
||||||
|
const auto deadline = GetTickCount64() + 100;
|
||||||
|
do {
|
||||||
|
MSG message{};
|
||||||
|
while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
|
||||||
|
TranslateMessage(&message);
|
||||||
|
DispatchMessageW(&message);
|
||||||
|
}
|
||||||
|
Sleep(5);
|
||||||
|
} while (GetTickCount64() < deadline);
|
||||||
|
}
|
||||||
|
bool check(bool passed, const char *message) {
|
||||||
|
if (!passed) std::fprintf(stderr, "FAIL: %s\n", message);
|
||||||
|
return passed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
if (argc != 2) return 2;
|
||||||
|
const std::string command = "\"Z:" + std::string(argv[1]) + "\"";
|
||||||
|
const int units = MultiByteToWideChar(CP_UTF8, 0, command.c_str(), -1, nullptr, 0);
|
||||||
|
std::vector<WCHAR> wideCommand(units);
|
||||||
|
MultiByteToWideChar(CP_UTF8, 0, command.c_str(), -1, wideCommand.data(), units);
|
||||||
|
STARTUPINFOW startup{};
|
||||||
|
startup.cb = sizeof(startup);
|
||||||
|
PROCESS_INFORMATION process{};
|
||||||
|
if (!CreateProcessW(nullptr, wideCommand.data(), nullptr, nullptr, FALSE, 0,
|
||||||
|
nullptr, nullptr, &startup, &process)) return 2;
|
||||||
|
HWND launcher = nullptr;
|
||||||
|
const auto deadline = GetTickCount64() + 5000;
|
||||||
|
while (!(launcher = FindWindowW(L"WineAccessForegroundFixture", nullptr)) &&
|
||||||
|
GetTickCount64() < deadline) Sleep(10);
|
||||||
|
if (!launcher) return 2;
|
||||||
|
HWND button = GetDlgItem(launcher, 1);
|
||||||
|
auto *connection = xcb_connect(nullptr, nullptr);
|
||||||
|
if (xcb_connection_has_error(connection)) return 2;
|
||||||
|
auto *screen = xcb_setup_roots_iterator(xcb_get_setup(connection)).data;
|
||||||
|
const auto desktop = static_cast<xcb_window_t>(reinterpret_cast<uintptr_t>(
|
||||||
|
GetPropW(GetDesktopWindow(), L"__wine_x11_whole_window")));
|
||||||
|
if (!desktop) return 2;
|
||||||
|
constexpr char name[] = "_NET_ACTIVE_WINDOW";
|
||||||
|
auto *atomReply = xcb_intern_atom_reply(connection,
|
||||||
|
xcb_intern_atom(connection, 0, sizeof(name) - 1, name), nullptr);
|
||||||
|
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<xcb_window_t>(reinterpret_cast<uintptr_t>(
|
||||||
|
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);
|
||||||
|
xcb_map_window(connection, linuxWindow);
|
||||||
|
auto focus = [&](xcb_window_t target, xcb_window_t active) {
|
||||||
|
xcb_change_property(connection, XCB_PROP_MODE_REPLACE, screen->root,
|
||||||
|
activeAtom, XCB_ATOM_WINDOW, 32, 1, &active);
|
||||||
|
xcb_set_input_focus(connection, XCB_INPUT_FOCUS_POINTER_ROOT, target, XCB_CURRENT_TIME);
|
||||||
|
xcb_flush(connection);
|
||||||
|
pump();
|
||||||
|
};
|
||||||
|
WNDCLASSW cls{};
|
||||||
|
cls.hInstance = GetModuleHandleW(nullptr);
|
||||||
|
cls.lpfnWndProc = DefWindowProcW;
|
||||||
|
cls.lpszClassName = L"WineAccessOtherFixture";
|
||||||
|
RegisterClassW(&cls);
|
||||||
|
HWND other = CreateWindowW(cls.lpszClassName, L"Second fixture", WS_OVERLAPPEDWINDOW,
|
||||||
|
380, 50, 300, 200, nullptr, nullptr, cls.hInstance, nullptr);
|
||||||
|
pump();
|
||||||
|
bool ok = true;
|
||||||
|
// Returning a launcher without activation leaves Wine's desktop with no
|
||||||
|
// remembered foreground, as when an external game window has closed.
|
||||||
|
ShowWindow(launcher, SW_HIDE);
|
||||||
|
pump();
|
||||||
|
ShowWindow(launcher, SW_SHOWNOACTIVATE);
|
||||||
|
acknowledge(launcher);
|
||||||
|
focus(linuxWindow, linuxWindow);
|
||||||
|
ok &= check(!GetForegroundWindow(), "Linux focus clears Wine foreground");
|
||||||
|
ok &= check(!restore_virtual_desktop_focus() && !GetForegroundWindow(), "Linux focus is never taken");
|
||||||
|
focus(desktop, linuxWindow);
|
||||||
|
ok &= check(!restore_virtual_desktop_focus(), "window manager must also select desktop");
|
||||||
|
focus(linuxWindow, desktop);
|
||||||
|
ok &= check(!restore_virtual_desktop_focus(), "X11 focus must also select desktop");
|
||||||
|
focus(desktop, desktop);
|
||||||
|
ok &= check(wine_x11_desktop_focused() && !GetForegroundWindow(), "reproduce orphaned virtual desktop focus");
|
||||||
|
EnableWindow(launcher, FALSE);
|
||||||
|
ok &= check(!restore_virtual_desktop_focus(), "disabled application is not activated");
|
||||||
|
EnableWindow(launcher, TRUE);
|
||||||
|
ShowWindow(other, SW_SHOWNOACTIVATE);
|
||||||
|
acknowledge(other);
|
||||||
|
focus(linuxWindow, linuxWindow);
|
||||||
|
focus(desktop, desktop);
|
||||||
|
ok &= check(!restore_virtual_desktop_focus() && !GetForegroundWindow(), "ambiguous applications are not chosen");
|
||||||
|
ShowWindow(other, SW_HIDE);
|
||||||
|
ok &= check(restore_virtual_desktop_focus(), "single application recovers foreground");
|
||||||
|
pump();
|
||||||
|
GUITHREADINFO gui{};
|
||||||
|
gui.cbSize = sizeof(gui);
|
||||||
|
GetGUIThreadInfo(GetWindowThreadProcessId(launcher, nullptr), &gui);
|
||||||
|
ok &= check(GetForegroundWindow() == launcher && gui.hwndFocus == button, "launcher restores its control focus");
|
||||||
|
ok &= check(!restore_virtual_desktop_focus() && GetForegroundWindow() == launcher, "existing Wine focus stays unchanged");
|
||||||
|
ShowWindow(launcher, SW_HIDE);
|
||||||
|
pump();
|
||||||
|
ShowWindow(launcher, SW_SHOWNOACTIVATE);
|
||||||
|
acknowledge(launcher);
|
||||||
|
focus(linuxWindow, linuxWindow);
|
||||||
|
focus(desktop, desktop);
|
||||||
|
ok &= check(restore_virtual_desktop_focus(), "later workspace return recovers again");
|
||||||
|
DestroyWindow(other);
|
||||||
|
PostMessageW(launcher, WM_CLOSE, 0, 0);
|
||||||
|
WaitForSingleObject(process.hProcess, 5000);
|
||||||
|
CloseHandle(process.hProcess);
|
||||||
|
CloseHandle(process.hThread);
|
||||||
|
xcb_destroy_window(connection, linuxWindow);
|
||||||
|
xcb_disconnect(connection);
|
||||||
|
if (ok) std::puts("PASS: virtual desktop foreground recovery and focus guards");
|
||||||
|
return ok ? 0 : 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
"""Run the foreground fixture in its own configured Wine virtual desktop."""
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
buildDir = Path(sys.argv[1]).resolve()
|
||||||
|
env = os.environ.copy()
|
||||||
|
env.update(WINEPREFIX=str(buildDir / "focus-prefix"),
|
||||||
|
CTHULHU_WINE_ACCESS_MANAGED="1", WINEDEBUG="-all",
|
||||||
|
WINEDLLOVERRIDES="mscoree,mshtml=")
|
||||||
|
try:
|
||||||
|
for key, value, data in (
|
||||||
|
(r"HKCU\Software\Wine\Explorer", "Desktop", "WineAccessFocusFixture"),
|
||||||
|
(r"HKCU\Software\Wine\Explorer\Desktops", "WineAccessFocusFixture", "800x600"),
|
||||||
|
):
|
||||||
|
subprocess.run(["wine", "reg", "add", key, "/v", value,
|
||||||
|
"/t", "REG_SZ", "/d", data, "/f"],
|
||||||
|
env=env, check=True, timeout=40)
|
||||||
|
# The next loader must create the configured desktop. Wait for this test
|
||||||
|
# prefix's old server to stop, without touching any user application.
|
||||||
|
subprocess.run(["wineserver", "-k"], env=env, check=True, timeout=5)
|
||||||
|
subprocess.run(["wineserver", "-w"], env=env, check=True, timeout=5)
|
||||||
|
result = subprocess.run([str(buildDir / "foreground-focus-test.exe"),
|
||||||
|
str(buildDir / "foreground-fixture.exe")],
|
||||||
|
env=env, timeout=20)
|
||||||
|
sys.exit(result.returncode)
|
||||||
|
finally:
|
||||||
|
subprocess.run(["wineserver", "-k"], env=env, timeout=5)
|
||||||
Reference in new issue
Block a user