Updates for when prism doesn't behave.
This commit is contained in:
1 parent
0d9cc2651d
commit
6e92ad13d1
12 files changed
+415
-5
No files matched your search
+4
-2
@@ -19,7 +19,8 @@ add_custom_command(OUTPUT "${menu_hook}"
|
||||
-static-libgcc -o "${menu_hook}" "${CMAKE_CURRENT_SOURCE_DIR}/menu_hook.c"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/edit_hook.c"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/scintilla_hook.c" -luser32 -lcomctl32 -lgdi32
|
||||
DEPENDS menu_hook.c menu_bridge.h edit_hook.c edit_hook.h control_class.h scintilla_hook.c scintilla_messages.h
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/notification_hook.c" -loleaut32
|
||||
DEPENDS menu_hook.c menu_bridge.h notification_hook.c notification_hook.h edit_hook.c edit_hook.h control_class.h scintilla_hook.c scintilla_messages.h
|
||||
VERBATIM)
|
||||
add_custom_target(wine-access-menu ALL DEPENDS "${menu_hook}")
|
||||
|
||||
@@ -30,10 +31,11 @@ add_custom_command(OUTPUT "${menu_hook32}" "${menu_host32}"
|
||||
-static-libgcc -Wl,--kill-at -o "${menu_hook32}"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/menu_hook.c"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/edit_hook.c" -luser32 -lcomctl32
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/notification_hook.c" -loleaut32
|
||||
COMMAND "${MINGW32_C_COMPILER}" -std=c11 -O2 -Wall -Wextra -mwindows
|
||||
-static-libgcc -o "${menu_host32}"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/menu_host32.c" -luser32
|
||||
DEPENDS menu_hook.c menu_bridge.h menu_host32.c edit_hook.c edit_hook.h control_class.h
|
||||
DEPENDS menu_hook.c menu_bridge.h notification_hook.c notification_hook.h menu_host32.c edit_hook.c edit_hook.h control_class.h
|
||||
VERBATIM)
|
||||
add_custom_target(wine-access-menu32 ALL DEPENDS "${menu_hook32}" "${menu_host32}")
|
||||
|
||||
|
||||
@@ -5,11 +5,13 @@ and readers that implement its Wine D-Bus protocol. An Orca endpoint is
|
||||
available for future Orca-side integration; stock Orca does not implement
|
||||
the required Wine methods.
|
||||
|
||||
It provides two independent capabilities:
|
||||
It provides these capabilities:
|
||||
|
||||
- an MSAA/WinEvent reader for standard Wine controls;
|
||||
- NVDA Controller 1, 2, and 3 plus Tolk compatibility for applications using
|
||||
`nvdaControllerClient32.dll`, `nvdaControllerClient64.dll`, or `Tolk.dll`.
|
||||
`nvdaControllerClient32.dll`, `nvdaControllerClient64.dll`, or `Tolk.dll`;
|
||||
- a UIA notification bridge for applications whose announcements Wine discards,
|
||||
including Prism's UIA speech backend.
|
||||
|
||||
The compatibility window is hidden and non-activating. It is expected to be
|
||||
invisible; the important endpoint is the local Wine RPC server registered by
|
||||
@@ -17,6 +19,21 @@ the helper. The helper forwards speech, braille, cancellation, and standard
|
||||
control events to the selected reader on the session D-Bus. The default is
|
||||
`org.stormux.Cthulhu1.Service`.
|
||||
|
||||
Wine currently returns success from `UiaRaiseNotificationEvent` without
|
||||
delivering its text. The native application hooks redirect direct imports of
|
||||
that function to the reader. This works with both 32-bit and 64-bit applications;
|
||||
it requires no application files or per-game DLL overrides. Notifications from
|
||||
background processes are ignored. Important notifications interrupt speech,
|
||||
and an empty important notification cancels speech. Each notification is
|
||||
bounded to 4095 UTF-16 code units without splitting surrogate pairs.
|
||||
|
||||
The bridge covers direct named imports, rather than calls obtained dynamically
|
||||
with `GetProcAddress`. It leaves implementations that reject a missing provider
|
||||
untouched. The hook DLL remains mapped until the application exits so redirected
|
||||
imports remain valid across reader restarts. Close and reopen applications after
|
||||
updating Wine Access to load the new hooks. `--no-dialog-reader` disables these
|
||||
hooks along with standard control reading.
|
||||
|
||||
The executable is named `cthulhu-wine-access.exe` for compatibility with the
|
||||
Cthulhu-Go supervisor.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Maintainer: Storm Dragon <storm_dragon@stormux.org>
|
||||
pkgname=wine-access-git
|
||||
pkgver=r24.g0f59809
|
||||
pkgver=r29.g0d9cc26
|
||||
pkgrel=1
|
||||
pkgdesc='Wine accessibility helper with MSAA, NVDA Controller, and Tolk compatibility for Cthulhu'
|
||||
arch=('x86_64')
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "edit_hook.h"
|
||||
#include "menu_bridge.h"
|
||||
#include "control_class.h"
|
||||
#include "notification_hook.h"
|
||||
#include <commctrl.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
@@ -257,6 +258,7 @@ BOOL wine_access_watch_edit(HWND window) {
|
||||
|
||||
__declspec(dllexport) LRESULT CALLBACK WineAccessInputHook(int code, WPARAM wp, LPARAM lp) {
|
||||
if (code >= 0 && wp == PM_REMOVE) {
|
||||
wine_access_watch_notifications();
|
||||
HWND focus = GetFocus();
|
||||
if (wine_access_watch_edit(focus))
|
||||
SendMessageW(focus, RegisterWindowMessageW(EDIT_FOCUS_MESSAGE), 0, 0);
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <rpc.h>
|
||||
#include <windows.h>
|
||||
#include <shellapi.h>
|
||||
#include <uiautomationcore.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <bit>
|
||||
@@ -34,6 +35,7 @@ ReviewIdentityStore reviewIdentities;
|
||||
ReviewService reviewService;
|
||||
std::deque<WineAccessMenuEvent> menuEvents;
|
||||
std::deque<WineAccessTextEvent> textEvents;
|
||||
std::deque<WineAccessNotificationEvent> notificationEvents;
|
||||
bool editReaderActive = false;
|
||||
std::string lastMenuEvent;
|
||||
ULONGLONG lastMenuOwner = 0;
|
||||
@@ -668,6 +670,18 @@ void present_text_event(const WineAccessTextEvent &event) {
|
||||
LRESULT CALLBACK menu_bridge_proc(HWND window, UINT message, WPARAM wp, LPARAM lp) {
|
||||
if (message == WM_COPYDATA && lp != 0) {
|
||||
const auto *data = reinterpret_cast<const COPYDATASTRUCT *>(lp);
|
||||
if (data->dwData == WINE_ACCESS_NOTIFICATION_TAG &&
|
||||
data->cbData == sizeof(WineAccessNotificationEvent) &&
|
||||
data->lpData != nullptr && notificationEvents.size() < 32) {
|
||||
auto event = *static_cast<const WineAccessNotificationEvent *>(data->lpData);
|
||||
event.text[std::size(event.text) - 1] = L'\0';
|
||||
notificationEvents.push_back(event);
|
||||
if (!PostMessageW(window, WINE_ACCESS_MENU_MESSAGE + 2, 0, 0)) {
|
||||
notificationEvents.pop_back();
|
||||
return FALSE;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
if (data->dwData == WINE_ACCESS_TEXT_TAG && data->cbData == sizeof(WineAccessTextEvent) &&
|
||||
data->lpData != nullptr && textEvents.size() < 32) {
|
||||
auto event = *static_cast<const WineAccessTextEvent *>(data->lpData);
|
||||
@@ -710,6 +724,23 @@ LRESULT CALLBACK menu_bridge_proc(HWND window, UINT message, WPARAM wp, LPARAM l
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (message == WINE_ACCESS_MENU_MESSAGE + 2) {
|
||||
if (!notificationEvents.empty()) {
|
||||
const auto event = notificationEvents.front();
|
||||
notificationEvents.pop_front();
|
||||
DWORD foregroundProcess = 0;
|
||||
GetWindowThreadProcessId(GetForegroundWindow(), &foregroundProcess);
|
||||
if (event.process == foregroundProcess) {
|
||||
const bool interrupt = event.processing == NotificationProcessing_ImportantAll ||
|
||||
event.processing == NotificationProcessing_ImportantMostRecent;
|
||||
if (event.text[0])
|
||||
call_boolean("SpeakText", g_variant_new("(sb)", to_utf8(event.text).c_str(), interrupt));
|
||||
else if (interrupt)
|
||||
call_boolean("CancelSpeech", g_variant_new("()"));
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return DefWindowProcW(window, message, wp, lp);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,13 @@
|
||||
#define WINE_ACCESS_TEXT_INSERTED 8
|
||||
#define WINE_ACCESS_TEXT_SCINTILLA 1
|
||||
#define WINE_ACCESS_TEXT_READONLY 2
|
||||
#define WINE_ACCESS_NOTIFICATION_TAG 0x57414e31
|
||||
|
||||
typedef struct WineAccessNotificationEvent {
|
||||
DWORD process;
|
||||
UINT processing;
|
||||
WCHAR text[4096];
|
||||
} WineAccessNotificationEvent;
|
||||
|
||||
typedef struct WineAccessTextEvent {
|
||||
ULONGLONG owner;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#include "menu_bridge.h"
|
||||
#include "edit_hook.h"
|
||||
#include "notification_hook.h"
|
||||
|
||||
static void menu_text(WCHAR *output, UINT capacity, const WCHAR *input) {
|
||||
UINT used = 0;
|
||||
@@ -19,6 +20,7 @@ static void menu_text(WCHAR *output, UINT capacity, const WCHAR *input) {
|
||||
__declspec(dllexport) LRESULT CALLBACK WineAccessMenuHook(int code, WPARAM wp,
|
||||
LPARAM lp) {
|
||||
if (code >= 0) {
|
||||
wine_access_watch_notifications();
|
||||
const CWPSTRUCT *message = (const CWPSTRUCT *)lp;
|
||||
if (message->message == WM_SETFOCUS) wine_access_watch_edit(message->hwnd);
|
||||
if (message->message == WM_MENUSELECT || message->message == WM_EXITMENULOOP) {
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#define COBJMACROS
|
||||
#include "notification_hook.h"
|
||||
#include "menu_bridge.h"
|
||||
#include <oleauto.h>
|
||||
#include <tlhelp32.h>
|
||||
#include <uiautomationcore.h>
|
||||
#include <uiautomationcoreapi.h>
|
||||
#include <string.h>
|
||||
|
||||
// Wine currently drops UiaRaiseNotificationEvent. Replace direct imports in
|
||||
// applications already reached by our native hook; leave other UIA APIs alone.
|
||||
// Keep the DLL mapped for the application's lifetime once an import points here.
|
||||
static HRESULT WINAPI raise_notification(IRawElementProviderSimple *provider,
|
||||
enum NotificationKind kind, enum NotificationProcessing processing,
|
||||
BSTR displayString, BSTR activityId) {
|
||||
(void)kind; (void)activityId;
|
||||
if (!provider) return E_INVALIDARG;
|
||||
HWND bridge = FindWindowW(WINE_ACCESS_MENU_CLASS, NULL);
|
||||
if (!bridge) return S_OK;
|
||||
WineAccessNotificationEvent event = {0};
|
||||
event.process = GetCurrentProcessId();
|
||||
event.processing = processing;
|
||||
UINT length = displayString ? SysStringLen(displayString) : 0;
|
||||
if (length >= ARRAYSIZE(event.text)) length = ARRAYSIZE(event.text) - 1;
|
||||
if (length && displayString[length - 1] >= 0xd800 &&
|
||||
displayString[length - 1] <= 0xdbff) --length;
|
||||
if (length) memcpy(event.text, displayString, length * sizeof(WCHAR));
|
||||
COPYDATASTRUCT data = {WINE_ACCESS_NOTIFICATION_TAG, sizeof(event), &event};
|
||||
DWORD_PTR result = 0;
|
||||
// The receiver only copies and queues. No reader calls on an app's thread.
|
||||
SendMessageTimeoutW(bridge, WM_COPYDATA, 0, (LPARAM)&data,
|
||||
SMTO_ABORTIFHUNG | SMTO_BLOCK, 50, &result);
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
static void watch_module(BYTE *base) {
|
||||
IMAGE_DOS_HEADER *dos = (IMAGE_DOS_HEADER *)base;
|
||||
if (dos->e_magic != IMAGE_DOS_SIGNATURE) return;
|
||||
IMAGE_NT_HEADERS *header = (IMAGE_NT_HEADERS *)(base + dos->e_lfanew);
|
||||
if (header->Signature != IMAGE_NT_SIGNATURE) return;
|
||||
IMAGE_DATA_DIRECTORY directory =
|
||||
header->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
|
||||
if (!directory.VirtualAddress || !directory.Size) return;
|
||||
IMAGE_IMPORT_DESCRIPTOR *imports =
|
||||
(IMAGE_IMPORT_DESCRIPTOR *)(base + directory.VirtualAddress);
|
||||
for (; imports->Name; ++imports) {
|
||||
if (lstrcmpiA((char *)(base + imports->Name), "uiautomationcore.dll") ||
|
||||
!imports->OriginalFirstThunk) continue;
|
||||
IMAGE_THUNK_DATA *names = (IMAGE_THUNK_DATA *)(base + imports->OriginalFirstThunk);
|
||||
IMAGE_THUNK_DATA *addresses = (IMAGE_THUNK_DATA *)(base + imports->FirstThunk);
|
||||
for (; names->u1.AddressOfData; ++names, ++addresses) {
|
||||
if (IMAGE_SNAP_BY_ORDINAL(names->u1.Ordinal)) continue;
|
||||
IMAGE_IMPORT_BY_NAME *name = (IMAGE_IMPORT_BY_NAME *)(base + names->u1.AddressOfData);
|
||||
if (strcmp((char *)name->Name, "UiaRaiseNotificationEvent") ||
|
||||
addresses->u1.Function == (ULONG_PTR)raise_notification) continue;
|
||||
// The Wine stub even accepts a missing required provider. Leave an
|
||||
// implementation which rejects it (including native UIA) untouched.
|
||||
typedef HRESULT (WINAPI *RaiseNotification)(IRawElementProviderSimple *,
|
||||
enum NotificationKind, enum NotificationProcessing, BSTR, BSTR);
|
||||
RaiseNotification original = (RaiseNotification)(ULONG_PTR)addresses->u1.Function;
|
||||
if (original(NULL, NotificationKind_ActionCompleted,
|
||||
NotificationProcessing_All, NULL, NULL) != S_OK) continue;
|
||||
HMODULE pinnedModule = NULL;
|
||||
if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
|
||||
GET_MODULE_HANDLE_EX_FLAG_PIN, (LPCWSTR)(ULONG_PTR)raise_notification,
|
||||
&pinnedModule)) continue;
|
||||
DWORD protection;
|
||||
if (!VirtualProtect(&addresses->u1.Function, sizeof(addresses->u1.Function),
|
||||
PAGE_READWRITE, &protection)) continue;
|
||||
InterlockedExchangePointer((PVOID volatile *)&addresses->u1.Function,
|
||||
(PVOID)(ULONG_PTR)raise_notification);
|
||||
DWORD ignored;
|
||||
VirtualProtect(&addresses->u1.Function, sizeof(addresses->u1.Function),
|
||||
protection, &ignored);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void wine_access_watch_notifications(void) {
|
||||
static DWORD lastScan;
|
||||
static LONG scanning;
|
||||
DWORD now = GetTickCount();
|
||||
if (InterlockedCompareExchange(&scanning, 1, 0)) return;
|
||||
if (lastScan && now - lastScan < 1000) {
|
||||
InterlockedExchange(&scanning, 0);
|
||||
return;
|
||||
}
|
||||
lastScan = now;
|
||||
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, GetCurrentProcessId());
|
||||
if (snapshot != INVALID_HANDLE_VALUE) {
|
||||
MODULEENTRY32W module = {0};
|
||||
module.dwSize = sizeof(module);
|
||||
if (Module32FirstW(snapshot, &module)) {
|
||||
do {
|
||||
// Hold a reference while walking imports: another thread may unload it.
|
||||
HMODULE reference = NULL;
|
||||
if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
|
||||
(LPCWSTR)module.modBaseAddr, &reference)) {
|
||||
watch_module((BYTE *)reference);
|
||||
FreeLibrary(reference);
|
||||
}
|
||||
} while (Module32NextW(snapshot, &module));
|
||||
}
|
||||
CloseHandle(snapshot);
|
||||
}
|
||||
InterlockedExchange(&scanning, 0);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#pragma once
|
||||
#include <windows.h>
|
||||
|
||||
void wine_access_watch_notifications(void);
|
||||
@@ -1,5 +1,28 @@
|
||||
# Wine control integration
|
||||
|
||||
UIA notification routing uses the actual native application hooks and a private
|
||||
recording reader, without sending speech to the live reader:
|
||||
|
||||
```sh
|
||||
xvfb-run -a python3 tests/notification_integration.py --build-dir build
|
||||
xvfb-run -a python3 tests/notification_integration.py --build-dir build --32-bit --reader orca
|
||||
```
|
||||
|
||||
To exercise a real 64-bit Prism DLL through its UIA backend, provide the matching
|
||||
Prism C headers (`prism.h` and `prism_version.h`) without replacing the DLL:
|
||||
|
||||
```sh
|
||||
xvfb-run -a python3 tests/notification_integration.py --build-dir build \
|
||||
--prism-dll /path/to/prism.dll --prism-include /path/to/prism/headers
|
||||
```
|
||||
|
||||
These checks cover Unicode text, interruption, empty-message cancellation,
|
||||
bounded payloads, and ignoring a background application's announcements. They
|
||||
do not establish audible speech. After installing, restart the reader and game;
|
||||
in Arcadelux, verify the introduction and repeated Up/Down menu announcements,
|
||||
including switching away and back. Also check existing NVDA/Tolk speech and
|
||||
ordinary Wine menus and Edit controls before submitting the change.
|
||||
|
||||
## Reader endpoint selection
|
||||
|
||||
After building, run these checks sequentially on private displays:
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#define COBJMACROS
|
||||
#include <windows.h>
|
||||
#include <oleauto.h>
|
||||
#include <uiautomationcore.h>
|
||||
#include <uiautomationcoreapi.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#ifdef WINE_ACCESS_PRISM_FIXTURE
|
||||
#include <shellapi.h>
|
||||
#define PRISM_STATIC
|
||||
#include <prism.h>
|
||||
#define LOAD(name) static __typeof__(&name) p_##name
|
||||
LOAD(prism_config_init); LOAD(prism_init); LOAD(prism_registry_acquire_best);
|
||||
LOAD(prism_backend_name); LOAD(prism_backend_speak); LOAD(prism_shutdown);
|
||||
LOAD(prism_backend_stop);
|
||||
static PrismContext *context;
|
||||
static PrismBackend *backend;
|
||||
#endif
|
||||
|
||||
static HWND window;
|
||||
static int step = -2;
|
||||
|
||||
static void notify(const WCHAR *text, enum NotificationProcessing processing) {
|
||||
#ifdef WINE_ACCESS_PRISM_FIXTURE
|
||||
char utf8[20000];
|
||||
WideCharToMultiByte(CP_UTF8, 0, text, -1, utf8, sizeof(utf8), NULL, NULL);
|
||||
BOOL interrupt = processing == NotificationProcessing_ImportantMostRecent ||
|
||||
processing == NotificationProcessing_ImportantAll;
|
||||
// Prism rejects empty speak text; its stop command emits an empty important
|
||||
// UIA notification instead.
|
||||
PrismError result = utf8[0] ? p_prism_backend_speak(backend, utf8, interrupt) :
|
||||
interrupt ? p_prism_backend_stop(backend) : PRISM_OK;
|
||||
if (result != PRISM_OK) ExitProcess(4);
|
||||
#else
|
||||
BSTR displayString = SysAllocString(text);
|
||||
BSTR activity = SysAllocString(L"Fixture");
|
||||
IRawElementProviderSimple *provider = NULL;
|
||||
if (FAILED(UiaHostProviderFromHwnd(window, &provider)) || !provider) ExitProcess(2);
|
||||
HRESULT result = UiaRaiseNotificationEvent(provider, NotificationKind_ActionCompleted,
|
||||
processing, displayString, activity);
|
||||
IRawElementProviderSimple_Release(provider);
|
||||
SysFreeString(displayString);
|
||||
SysFreeString(activity);
|
||||
if (FAILED(result)) ExitProcess(3);
|
||||
#endif
|
||||
}
|
||||
|
||||
static LRESULT CALLBACK window_proc(HWND hwnd, UINT message, WPARAM wp, LPARAM lp) {
|
||||
if (message == WM_TIMER) {
|
||||
// Real UIA calls through normal imports, reached by the global native hook.
|
||||
switch (++step) {
|
||||
case -1: case 0: break;
|
||||
case 1: notify(L"Menu caf\u00e9 \U0001f600", NotificationProcessing_All); break;
|
||||
case 2: notify(L"Second item", NotificationProcessing_ImportantMostRecent); break;
|
||||
case 3: notify(L"", NotificationProcessing_ImportantMostRecent); break;
|
||||
case 4: notify(L"", NotificationProcessing_All); break;
|
||||
case 5: {
|
||||
WCHAR longText[4100];
|
||||
for (UINT i = 0; i < ARRAYSIZE(longText) - 1; ++i) longText[i] = L'x';
|
||||
longText[4094] = 0xd83d; longText[4095] = 0xde00;
|
||||
longText[ARRAYSIZE(longText) - 1] = 0;
|
||||
notify(longText, NotificationProcessing_All);
|
||||
break;
|
||||
}
|
||||
case 6: ShowWindow(hwnd, SW_HIDE); break;
|
||||
case 7: notify(L"Background must stay silent", NotificationProcessing_ImportantAll); break;
|
||||
default: DestroyWindow(hwnd); break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (message == WM_DESTROY) { PostQuitMessage(0); return 0; }
|
||||
return DefWindowProcW(hwnd, message, wp, lp);
|
||||
}
|
||||
|
||||
int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR args, int show) {
|
||||
(void)previous; (void)args; (void)show;
|
||||
CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
|
||||
WNDCLASSW cls = {0};
|
||||
cls.hInstance = instance; cls.lpfnWndProc = window_proc;
|
||||
cls.lpszClassName = L"WineAccessNotificationFixture";
|
||||
RegisterClassW(&cls);
|
||||
window = CreateWindowW(cls.lpszClassName, L"Notification fixture", WS_OVERLAPPEDWINDOW,
|
||||
10, 10, 300, 200, NULL, NULL, instance, NULL);
|
||||
if (!window) return 1;
|
||||
ShowWindow(window, SW_SHOW); SetForegroundWindow(window);
|
||||
#ifdef WINE_ACCESS_PRISM_FIXTURE
|
||||
int argumentCount = 0;
|
||||
WCHAR **arguments = CommandLineToArgvW(GetCommandLineW(), &argumentCount);
|
||||
HMODULE module = argumentCount == 2 ? LoadLibraryW(arguments[1]) : NULL;
|
||||
LocalFree(arguments);
|
||||
if (!module) return 5;
|
||||
#undef LOAD
|
||||
#define LOAD(name) do { FARPROC address = GetProcAddress(module, #name); \
|
||||
memcpy(&p_##name, &address, sizeof(p_##name)); if (!p_##name) return 6; } while (0)
|
||||
LOAD(prism_config_init); LOAD(prism_init); LOAD(prism_registry_acquire_best);
|
||||
LOAD(prism_backend_name); LOAD(prism_backend_speak); LOAD(prism_shutdown);
|
||||
LOAD(prism_backend_stop);
|
||||
PrismConfig config = p_prism_config_init();
|
||||
context = p_prism_init(&config);
|
||||
if (!context) return 7;
|
||||
backend = p_prism_registry_acquire_best(context);
|
||||
if (!backend || strcmp(p_prism_backend_name(backend), "UIA")) return 8;
|
||||
#endif
|
||||
SetTimer(window, 1, 500, NULL);
|
||||
MSG message;
|
||||
while (GetMessageW(&message, NULL, 0, 0) > 0) {
|
||||
TranslateMessage(&message); DispatchMessageW(&message);
|
||||
}
|
||||
#ifdef WINE_ACCESS_PRISM_FIXTURE
|
||||
p_prism_shutdown(context);
|
||||
#endif
|
||||
CoUninitialize();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
"""Route real UIA notifications through native hooks to a private reader."""
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
from gi.repository import Gio, GLib
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--build-dir", default="build")
|
||||
parser.add_argument("--reader", choices=("cthulhu", "orca"), default="cthulhu")
|
||||
parser.add_argument("--32-bit", dest="is32Bit", action="store_true")
|
||||
parser.add_argument("--session", action="store_true")
|
||||
parser.add_argument("--prism-dll", type=Path)
|
||||
parser.add_argument("--prism-include", type=Path)
|
||||
args = parser.parse_args()
|
||||
if bool(args.prism_dll) != bool(args.prism_include) or (args.prism_dll and args.is32Bit):
|
||||
parser.error("Prism checks require both --prism-dll and --prism-include, and a 64-bit DLL")
|
||||
build = Path(args.build_dir).resolve()
|
||||
if not args.session:
|
||||
sys.exit(subprocess.call(["dbus-run-session", "--", sys.executable, __file__,
|
||||
"--session", "--build-dir", str(build), "--reader", args.reader,
|
||||
*(["--prism-dll", str(args.prism_dll.resolve()),
|
||||
"--prism-include", str(args.prism_include.resolve())]
|
||||
if args.prism_dll else []),
|
||||
*(["--32-bit"] if args.is32Bit else [])]))
|
||||
|
||||
interface = "org.gnome.Orca1.Service" if args.reader == "orca" else "org.stormux.Cthulhu1.Service"
|
||||
objectPath = "/" + interface.replace(".", "/")
|
||||
xml = '<node><interface name="' + interface + '">'
|
||||
for method, inputs in (("SpeakText", "sb"), ("CancelSpeech", ""),
|
||||
("PresentAccessibleEvent", "sssssssiis")):
|
||||
xml += '<method name="' + method + '">'
|
||||
xml += ''.join('<arg type="' + kind + '" direction="in"/>' for kind in inputs)
|
||||
xml += '<arg type="b" direction="out"/></method>'
|
||||
xml += '</interface></node>'
|
||||
events = []
|
||||
|
||||
|
||||
def record(connection, sender, path, iface, method, parameters, invocation):
|
||||
if method != "PresentAccessibleEvent":
|
||||
events.append((method, parameters.unpack()))
|
||||
invocation.return_value(GLib.Variant("(b)", (True,)))
|
||||
|
||||
|
||||
connection = Gio.bus_get_sync(Gio.BusType.SESSION, None)
|
||||
connection.register_object(objectPath, Gio.DBusNodeInfo.new_for_xml(xml).interfaces[0], record, None, None)
|
||||
connection.call_sync("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus",
|
||||
"RequestName", GLib.Variant("(su)", (interface, 0)), None,
|
||||
Gio.DBusCallFlags.NONE, 1000, None)
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="wine-access-notifications-") as temporary:
|
||||
tempPath = Path(temporary)
|
||||
env = os.environ.copy()
|
||||
env.update(WINEPREFIX=str(tempPath / "prefix"), WINEDEBUG="-all",
|
||||
WINEDLLOVERRIDES="mscoree,mshtml=", CTHULHU_WINE_ACCESS_MANAGED="1")
|
||||
fixture = tempPath / "notification-fixture.exe"
|
||||
compiler = "i686-w64-mingw32-gcc" if args.is32Bit else "x86_64-w64-mingw32-gcc"
|
||||
subprocess.run([compiler, "-Wall", "-Wextra", "-mwindows", "-o", str(fixture),
|
||||
*(["-DWINE_ACCESS_PRISM_FIXTURE", "-I" + str(args.prism_include.resolve())]
|
||||
if args.prism_dll else []),
|
||||
str(Path(__file__).with_name("notification_fixture.c")),
|
||||
"-luser32", "-lole32", "-loleaut32", "-luiautomationcore", "-lshell32"], check=True)
|
||||
helper = app = None
|
||||
try:
|
||||
subprocess.run(["wineboot", "-u"], env=env, check=True, timeout=45)
|
||||
helper = subprocess.Popen([str(build / "cthulhu-wine-access.exe"), "--no-controller",
|
||||
"--reader", args.reader], env=env)
|
||||
time.sleep(.5)
|
||||
app = subprocess.Popen(["wine", str(fixture),
|
||||
*(["Z:" + str(args.prism_dll.resolve()).replace("/", "\\")]
|
||||
if args.prism_dll else [])], env=env)
|
||||
deadline = time.monotonic() + 20
|
||||
while app.poll() is None and time.monotonic() < deadline:
|
||||
while GLib.MainContext.default().iteration(False):
|
||||
pass
|
||||
time.sleep(.005)
|
||||
while GLib.MainContext.default().iteration(False):
|
||||
pass
|
||||
assert app.poll() == 0, f"notification fixture failed: {app.poll()}"
|
||||
assert helper.poll() is None, "helper exited during notifications"
|
||||
assert events == [
|
||||
("SpeakText", ("Menu caf\u00e9 \U0001f600", False)),
|
||||
("SpeakText", ("Second item", True)),
|
||||
("CancelSpeech", ()),
|
||||
("SpeakText", ("x" * 4094, False)),
|
||||
], events
|
||||
print(f"PASS: {32 if args.is32Bit else 64}-bit UIA text, interruption, cancellation, "
|
||||
f"Unicode, bounded text, and background silence through {args.reader}")
|
||||
finally:
|
||||
for child in (app, helper):
|
||||
if child is not None and child.poll() is None:
|
||||
child.terminate()
|
||||
child.wait(timeout=5)
|
||||
subprocess.run(["wineserver", "-k"], env=env, timeout=5)
|
||||
Reference in new issue
Block a user