diff --git a/main.cpp b/main.cpp index af0e9b6..14fd956 100644 --- a/main.cpp +++ b/main.cpp @@ -801,6 +801,18 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) { return 1; } + // Application-owned MSAA objects (including Windows Forms menus) are + // marshaled across processes. Initialize their callback/message-loop thread; + // Wine's built-in control proxies can otherwise hide this missing setup. + const HRESULT comResult = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + if (FAILED(comResult)) { + std::fprintf(stderr, "wine-access: COM initialization failed (error %lx)\n", + static_cast(comResult)); + g_object_unref(connection); + CloseHandle(singleton); + return 1; + } + HWND window = nullptr; bool controllerActive = false; if (controllerEnabled && @@ -881,6 +893,7 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) { } if (window != nullptr) DestroyWindow(window); + CoUninitialize(); g_object_unref(connection); CloseHandle(singleton); return 0; diff --git a/tests/README.md b/tests/README.md index 0a29fba..6ab462b 100644 --- a/tests/README.md +++ b/tests/README.md @@ -16,6 +16,12 @@ items; escaped ampersands; duplicate menu selections and reopening; restored button focus; and exiting with both reader capabilities disabled. Additional `WM_MENUSELECT` messages exercise duplicate notifications deterministically. +The menu fixture also returns an application-owned MSAA object through +`WM_GETOBJECT`/`LresultFromObject`. The separate helper must retrieve its +`client` role through COM; native text fallback cannot satisfy the check. +This catches missing COM initialization that ordinary Wine control proxies +and the native menu hook did not expose. + The fixture window takes focus and closes automatically. Cleanup stops only the Wine server for the temporary prefix. The recorded events never reach the running reader's speech or braille service. @@ -32,6 +38,35 @@ live listening or physical braille acceptance. 32-bit hook support, entirely custom menus without `WM_MENUSELECT`, and full object review are outside the current implementation. +## BARD Express managed menus + +The October 10 investigation found two independent blockers. The helper's +message-loop thread did not initialize COM, so application-owned MSAA queries +failed with `CO_E_NOTINITIALIZED` and used native window text such as `MenuBar`. +The helper now initializes a single-threaded COM apartment before registering +callbacks and uninitializes it after removing them. The new marshaled-provider +check passes with the fix and fails with the previous helper. + +BARD's prefix also registered `IAccessible` against UIA type library +`{930299CE-9965-4DEC-B0F4-A54848D4B667}`, version `1.0`, while its installed +`uiautomationcore.dll` lacked the registered resource `3`. +An initialized read-only query failed with `TYPE_E_CANTLOADLIBRARY`. +After backing up the prefix registry, the 32-bit and 64-bit +`HKLM\Software\Classes\Interface\{618736E0-3C3D-11CF-810C-00AA00389B71}\TypeLib` +registrations were pointed to the loadable standard accessibility type library +`{1EA4DBF0-3C3B-11CF-810C-00AA00389B71}`, version `1.1`. +Read-only inspection then retrieved Account/View/Help and their command labels, +shortcuts and unavailable states from the open application. This repair is +specific to a verified prefix mismatch; the helper does not change registries. + +After installing the helper, restart Cthulhu to load it. In BARD, press Alt, +move Left/Right among Account, View and Help, then Down/Up among commands. +Check unavailable items while working offline and Escape back to the main +screen. Recheck Tab/Shift+Tab through buttons, preferences and login fields, +including password exclusion, and ordinary Wine Notepad menus/editing. +Verify audible speech and physical braille separately; querying menu labels +alone does not establish either output gate. + ## Editor and actual Notepad checks The CMake build also includes a small MSAA state decoder check. Run it with diff --git a/tests/menu_fixture.c b/tests/menu_fixture.c index c93c0e1..9310ef2 100644 --- a/tests/menu_fixture.c +++ b/tests/menu_fixture.c @@ -1,13 +1,34 @@ // SPDX-License-Identifier: LGPL-2.1-or-later +#define COBJMACROS #include +#include +#include static HMENU bar, file, edit, nested, context; static int step; +static HWND marshaledControl; +static HWND button; + +static LRESULT CALLBACK provider_proc(HWND window, UINT message, WPARAM wp, LPARAM lp) { + if (message == WM_GETOBJECT && (LONG)lp == OBJID_CLIENT) { + IAccessible *accessible = NULL; + if (SUCCEEDED(CreateStdAccessibleObject(window, OBJID_CLIENT, &IID_IAccessible, + (void **)&accessible))) { + // Returning a marshaled object forces the separate helper to use COM, + // rather than Wine's directly constructed native-control proxy. + LRESULT result = LresultFromObject(&IID_IAccessible, wp, (IUnknown *)accessible); + IAccessible_Release(accessible); + return result; + } + } + return DefWindowProcW(window, message, wp, lp); +} static LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wp, LPARAM lp) { if (message == WM_TIMER) { switch (++step) { case 1: + SetFocus(button); // Enter File through the application's actual native menu loop. PostMessageW(window, WM_SYSCOMMAND, SC_KEYMENU, 'f'); break; @@ -73,6 +94,7 @@ static LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wp, LPARAM int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR args, int show) { (void)previous; (void)args; (void)show; + if (FAILED(CoInitializeEx(NULL, COINIT_APARTMENTTHREADED))) return 1; WNDCLASSW cls = {0}; cls.hInstance = instance; cls.lpszClassName = L"WineAccessMenuFixture"; @@ -92,15 +114,24 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR args, int show) HWND window = CreateWindowW(cls.lpszClassName, L"Wine menu fixture", WS_OVERLAPPEDWINDOW, 120, 120, 400, 240, NULL, bar, instance, NULL); ShowWindow(window, SW_SHOW); - HWND button = CreateWindowW(L"Button", L"Return to control", WS_CHILD | WS_VISIBLE | WS_TABSTOP, + button = CreateWindowW(L"Button", L"Return to control", WS_CHILD | WS_VISIBLE | WS_TABSTOP, 10, 10, 200, 30, window, NULL, instance, NULL); + cls.lpszClassName = L"WineAccessMarshaledFixture"; + cls.lpfnWndProc = provider_proc; + RegisterClassW(&cls); + marshaledControl = CreateWindowW(cls.lpszClassName, L"Application-owned accessibility", + WS_CHILD | WS_VISIBLE | WS_TABSTOP, + 10, 60, 250, 30, window, NULL, instance, NULL); SetForegroundWindow(window); SetFocus(button); + SetFocus(marshaledControl); + NotifyWinEvent(EVENT_OBJECT_FOCUS, marshaledControl, OBJID_CLIENT, CHILDID_SELF); SetTimer(window, 1, 400, NULL); MSG message; while (GetMessageW(&message, NULL, 0, 0) > 0) { TranslateMessage(&message); DispatchMessageW(&message); } DestroyMenu(context); + CoUninitialize(); return 0; } diff --git a/tests/menu_integration.py b/tests/menu_integration.py index cf5a047..7c15f10 100644 --- a/tests/menu_integration.py +++ b/tests/menu_integration.py @@ -65,7 +65,7 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary: subprocess.run([compiler, "-Wall", "-Wextra", "-mwindows", *(["-DWINE_ACCESS_WINFORMS_FIXTURE"] if args.fixture == "winforms" else []), "-o", str(fixture), str(Path(__file__).with_name(sourceName + "_fixture.c")), - "-luser32", "-lcomctl32"], check=True) + "-luser32", "-lcomctl32", "-loleacc", "-lole32"], check=True) helper = None app = None try: @@ -190,11 +190,13 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary: assert len(saved) == 2, f"duplicate suppression/menu reset failed: {len(saved)}" assert all(e[4] == "Ctrl+P" and e[6] == "checked, unavailable" for e in saved) assert any(e[2] == "Return to control" and e[3] == "button" for e in events), "control focus missing" + assert any(e[2] == "Application-owned accessibility" and e[3] == "client" + for e in events), "marshaled application MSAA object unavailable" subprocess.run([str(build / "cthulhu-wine-access.exe"), "--no-dialog-reader", "--no-controller"], env=env, check=True, timeout=5) print("PASS: native and nested menus, context menus, shortcuts, states, " - "duplicate suppression, reopening, control focus, and disable options") + "duplicate suppression, reopening, marshaled MSAA, control focus, and disable options") finally: # Only this test's newly created Wine prefix is affected. subprocess.run(["wineserver", "-k"], env=env, timeout=10, check=False)