Initialize COM for application-owned Wine accessibility objects
This commit is contained in:
1 parent
25e38907d3
commit
6976c83294
4 files changed
+84
-3
No files matched your search
@@ -801,6 +801,18 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) {
|
|||||||
return 1;
|
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<unsigned long>(comResult));
|
||||||
|
g_object_unref(connection);
|
||||||
|
CloseHandle(singleton);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
HWND window = nullptr;
|
HWND window = nullptr;
|
||||||
bool controllerActive = false;
|
bool controllerActive = false;
|
||||||
if (controllerEnabled &&
|
if (controllerEnabled &&
|
||||||
@@ -881,6 +893,7 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) {
|
|||||||
}
|
}
|
||||||
if (window != nullptr)
|
if (window != nullptr)
|
||||||
DestroyWindow(window);
|
DestroyWindow(window);
|
||||||
|
CoUninitialize();
|
||||||
g_object_unref(connection);
|
g_object_unref(connection);
|
||||||
CloseHandle(singleton);
|
CloseHandle(singleton);
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ items; escaped ampersands; duplicate menu selections and reopening; restored
|
|||||||
button focus; and exiting with both reader capabilities disabled. Additional
|
button focus; and exiting with both reader capabilities disabled. Additional
|
||||||
`WM_MENUSELECT` messages exercise duplicate notifications deterministically.
|
`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 fixture window takes focus and closes automatically. Cleanup stops only
|
||||||
the Wine server for the temporary prefix. The recorded events never reach the
|
the Wine server for the temporary prefix. The recorded events never reach the
|
||||||
running reader's speech or braille service.
|
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
|
32-bit hook support, entirely custom menus without `WM_MENUSELECT`, and full
|
||||||
object review are outside the current implementation.
|
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
|
## Editor and actual Notepad checks
|
||||||
|
|
||||||
The CMake build also includes a small MSAA state decoder check. Run it with
|
The CMake build also includes a small MSAA state decoder check. Run it with
|
||||||
|
|||||||
+32
-1
@@ -1,13 +1,34 @@
|
|||||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
#define COBJMACROS
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
#include <initguid.h>
|
||||||
|
#include <oleacc.h>
|
||||||
|
|
||||||
static HMENU bar, file, edit, nested, context;
|
static HMENU bar, file, edit, nested, context;
|
||||||
static int step;
|
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) {
|
static LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wp, LPARAM lp) {
|
||||||
if (message == WM_TIMER) {
|
if (message == WM_TIMER) {
|
||||||
switch (++step) {
|
switch (++step) {
|
||||||
case 1:
|
case 1:
|
||||||
|
SetFocus(button);
|
||||||
// Enter File through the application's actual native menu loop.
|
// Enter File through the application's actual native menu loop.
|
||||||
PostMessageW(window, WM_SYSCOMMAND, SC_KEYMENU, 'f');
|
PostMessageW(window, WM_SYSCOMMAND, SC_KEYMENU, 'f');
|
||||||
break;
|
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) {
|
int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR args, int show) {
|
||||||
(void)previous; (void)args; (void)show;
|
(void)previous; (void)args; (void)show;
|
||||||
|
if (FAILED(CoInitializeEx(NULL, COINIT_APARTMENTTHREADED))) return 1;
|
||||||
WNDCLASSW cls = {0};
|
WNDCLASSW cls = {0};
|
||||||
cls.hInstance = instance;
|
cls.hInstance = instance;
|
||||||
cls.lpszClassName = L"WineAccessMenuFixture";
|
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,
|
HWND window = CreateWindowW(cls.lpszClassName, L"Wine menu fixture", WS_OVERLAPPEDWINDOW,
|
||||||
120, 120, 400, 240, NULL, bar, instance, NULL);
|
120, 120, 400, 240, NULL, bar, instance, NULL);
|
||||||
ShowWindow(window, SW_SHOW);
|
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);
|
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);
|
SetForegroundWindow(window);
|
||||||
SetFocus(button);
|
SetFocus(button);
|
||||||
|
SetFocus(marshaledControl);
|
||||||
|
NotifyWinEvent(EVENT_OBJECT_FOCUS, marshaledControl, OBJID_CLIENT, CHILDID_SELF);
|
||||||
SetTimer(window, 1, 400, NULL);
|
SetTimer(window, 1, 400, NULL);
|
||||||
MSG message;
|
MSG message;
|
||||||
while (GetMessageW(&message, NULL, 0, 0) > 0) {
|
while (GetMessageW(&message, NULL, 0, 0) > 0) {
|
||||||
TranslateMessage(&message); DispatchMessageW(&message);
|
TranslateMessage(&message); DispatchMessageW(&message);
|
||||||
}
|
}
|
||||||
DestroyMenu(context);
|
DestroyMenu(context);
|
||||||
|
CoUninitialize();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -65,7 +65,7 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
|
|||||||
subprocess.run([compiler, "-Wall", "-Wextra", "-mwindows",
|
subprocess.run([compiler, "-Wall", "-Wextra", "-mwindows",
|
||||||
*(["-DWINE_ACCESS_WINFORMS_FIXTURE"] if args.fixture == "winforms" else []),
|
*(["-DWINE_ACCESS_WINFORMS_FIXTURE"] if args.fixture == "winforms" else []),
|
||||||
"-o", str(fixture), str(Path(__file__).with_name(sourceName + "_fixture.c")),
|
"-o", str(fixture), str(Path(__file__).with_name(sourceName + "_fixture.c")),
|
||||||
"-luser32", "-lcomctl32"], check=True)
|
"-luser32", "-lcomctl32", "-loleacc", "-lole32"], check=True)
|
||||||
helper = None
|
helper = None
|
||||||
app = None
|
app = None
|
||||||
try:
|
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 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 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] == "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"),
|
subprocess.run([str(build / "cthulhu-wine-access.exe"),
|
||||||
"--no-dialog-reader", "--no-controller"],
|
"--no-dialog-reader", "--no-controller"],
|
||||||
env=env, check=True, timeout=5)
|
env=env, check=True, timeout=5)
|
||||||
print("PASS: native and nested menus, context menus, shortcuts, states, "
|
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:
|
finally:
|
||||||
# Only this test's newly created Wine prefix is affected.
|
# Only this test's newly created Wine prefix is affected.
|
||||||
subprocess.run(["wineserver", "-k"], env=env, timeout=10, check=False)
|
subprocess.run(["wineserver", "-k"], env=env, timeout=10, check=False)
|
||||||
|
|||||||
Reference in new issue
Block a user