From 6f6d4040c4898ca07cafd008dc470b444ba6e2c6 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Fri, 9 Oct 2026 15:57:45 -0400 Subject: [PATCH] Work started on editor support. --- CMakeLists.txt | 4 +- .../ArchLinux/wine-access-git/PKGBUILD | 2 +- edit_hook.c | 203 ++++++++++++++++++ edit_hook.h | 4 + main.cpp | 71 ++++++ menu_bridge.h | 17 ++ menu_hook.c | 2 + tests/edit_fixture.c | 81 +++++++ tests/menu_integration.py | 22 +- 9 files changed, 401 insertions(+), 5 deletions(-) create mode 100644 edit_hook.c create mode 100644 edit_hook.h create mode 100644 tests/edit_fixture.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 013158d..e810955 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,8 +15,8 @@ set(menu_hook "${CMAKE_CURRENT_BINARY_DIR}/wine-access-menu.dll") add_custom_command(OUTPUT "${menu_hook}" COMMAND "${MINGW_C_COMPILER}" -std=c11 -O2 -Wall -Wextra -shared -static-libgcc -o "${menu_hook}" "${CMAKE_CURRENT_SOURCE_DIR}/menu_hook.c" - -luser32 - DEPENDS menu_hook.c menu_bridge.h + "${CMAKE_CURRENT_SOURCE_DIR}/edit_hook.c" -luser32 -lcomctl32 + DEPENDS menu_hook.c menu_bridge.h edit_hook.c edit_hook.h VERBATIM) add_custom_target(wine-access-menu ALL DEPENDS "${menu_hook}") diff --git a/distro-packages/ArchLinux/wine-access-git/PKGBUILD b/distro-packages/ArchLinux/wine-access-git/PKGBUILD index 33fa252..cb588af 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=r3.gf365af3 +pkgver=r6.g0992ffe pkgrel=1 pkgdesc='Wine accessibility helper with MSAA, NVDA Controller, and Tolk compatibility for Cthulhu' arch=('x86_64') diff --git a/edit_hook.c b/edit_hook.c new file mode 100644 index 0000000..412fe41 --- /dev/null +++ b/edit_hook.c @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +#include "edit_hook.h" +#include "menu_bridge.h" +#include +#include + +#define EDIT_SUBCLASS_ID 0x57415431 +#define EDIT_STATE_PROPERTY L"CthulhuWineEditState" +// Bound memory and work inside applications. Navigation reuses the snapshot; +// only text changes refresh it. Large documents retain line/character reading. +#define MAX_SNAPSHOT_UNITS (1024 * 1024) + +typedef struct EditState { + WCHAR *text; + DWORD length, start, end; + BOOL busy, known, destroyed; +} EditState; + +static void selection(HWND window, DWORD *start, DWORD *end) { + SendMessageW(window, EM_GETSEL, (WPARAM)start, (LPARAM)end); +} + +static void snapshot(HWND window, EditState *state) { + free(state->text); + state->text = NULL; + state->length = GetWindowTextLengthW(window); + if (state->length <= MAX_SNAPSHOT_UNITS) { + state->text = calloc((size_t)state->length + 1, sizeof(WCHAR)); + if (state->text) + state->length = GetWindowTextW(window, state->text, state->length + 1); + } +} + +static void copy_range(WCHAR *output, const WCHAR *text, DWORD start, DWORD end) { + UINT used = 0; + while (start < end && used < 1023) output[used++] = text[start++]; + // Never leave half a UTF-16 surrogate pair at the truncation boundary. + if (used && output[used - 1] >= 0xd800 && output[used - 1] <= 0xdbff) --used; + output[used] = 0; +} + +static void current_line(HWND window, DWORD caret, WCHAR *output) { + LRESULT line = SendMessageW(window, EM_LINEFROMCHAR, caret, 0); + // EM_GETLINE expects the buffer capacity in its first WORD. + output[0] = 1023; + LRESULT copied = SendMessageW(window, EM_GETLINE, line, (LPARAM)output); + if (copied < 0 || copied > 1023) copied = 0; + output[copied] = 0; +} + +static BOOL word_space(WCHAR ch) { + return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n'; +} + +static void notify_text(HWND window, UINT kind, const WCHAR *text, DWORD start, DWORD end) { + HWND owner = GetAncestor(window, GA_ROOT); + HWND bridge = FindWindowW(WINE_ACCESS_MENU_CLASS, NULL); + if (!bridge || owner != GetAncestor(GetForegroundWindow(), GA_ROOT) || GetFocus() != window) + return; + WineAccessTextEvent event = {0}; + event.owner = (ULONGLONG)(UINT_PTR)owner; + event.control = (ULONGLONG)(UINT_PTR)window; + event.kind = kind; event.start = start; event.end = end; + lstrcpynW(event.text, text, 1024); + GetWindowTextW(owner, event.title, 256); + COPYDATASTRUCT data = {WINE_ACCESS_TEXT_TAG, sizeof(event), &event}; + DWORD_PTR result; + SendMessageTimeoutW(bridge, WM_COPYDATA, (WPARAM)window, (LPARAM)&data, + SMTO_ABORTIFHUNG | SMTO_BLOCK, 50, &result); +} + +static LRESULT CALLBACK edit_proc(HWND window, UINT message, WPARAM wp, LPARAM lp, + UINT_PTR id, DWORD_PTR reference) { + EditState *state = (EditState *)reference; + if (message == WM_NCDESTROY) { + RemoveWindowSubclass(window, edit_proc, id); + RemovePropW(window, EDIT_STATE_PROPERTY); + state->destroyed = TRUE; + if (!state->busy) { free(state->text); free(state); } + return DefSubclassProc(window, message, wp, lp); + } + if (state->busy) return DefSubclassProc(window, message, wp, lp); + BOOL navigation = message == WM_KEYDOWN && + (wp == VK_LEFT || wp == VK_RIGHT || wp == VK_UP || wp == VK_DOWN || + wp == VK_HOME || wp == VK_END || wp == VK_PRIOR || wp == VK_NEXT); + BOOL changing = message == WM_CHAR || (message == WM_KEYDOWN && wp == VK_DELETE) || + message == WM_PASTE || message == WM_CUT || message == WM_CLEAR || + message == WM_UNDO || message == EM_REPLACESEL || message == WM_SETTEXT; + BOOL focus = message == WM_SETFOCUS; + BOOL selected = message == EM_SETSEL; + if (!navigation && !changing && !focus && !selected && message != EM_SETPASSWORDCHAR) + return DefSubclassProc(window, message, wp, lp); + + state->busy = TRUE; + DWORD old_start = 0, old_end = 0; + selection(window, &old_start, &old_end); + BOOL password = (GetWindowLongPtrW(window, GWL_STYLE) & ES_PASSWORD) != 0; + if (!password && (!state->known || focus)) snapshot(window, state); + WCHAR removed[1024] = {0}; + if (!password && state->text && changing) { + DWORD first = old_start, last = old_end; + if (first == last) { + if (message == WM_CHAR && wp == VK_BACK && first) { + --first; + if (first && state->text[first] >= 0xdc00 && state->text[first] <= 0xdfff) --first; + } else if (message == WM_KEYDOWN && wp == VK_DELETE && last < state->length) { + ++last; + if (last < state->length && state->text[last - 1] >= 0xd800 && state->text[last - 1] <= 0xdbff) ++last; + } + } + if (last <= state->length) copy_range(removed, state->text, first, last); + } + LRESULT result = DefSubclassProc(window, message, wp, lp); + if (state->destroyed) { + free(state->text); free(state); + return result; + } + DWORD start = 0, end = 0; + selection(window, &start, &end); + password = (GetWindowLongPtrW(window, GWL_STYLE) & ES_PASSWORD) != 0; + if (password) { + free(state->text); state->text = NULL; state->known = FALSE; + if (focus) notify_text(window, WINE_ACCESS_TEXT_PASSWORD, L"", start, end); + state->busy = FALSE; + return result; + } + DWORD old_length = state->length; + if (changing || message == EM_SETPASSWORDCHAR) snapshot(window, state); + state->known = TRUE; + WCHAR text[1024] = {0}; + UINT kind = WINE_ACCESS_TEXT_READ; + BOOL report = FALSE; + if (focus) { + current_line(window, start, text); + kind = WINE_ACCESS_TEXT_FOCUS; report = TRUE; + } else if ((navigation || selected) && (start != old_start || end != old_end)) { + if (start != end && state->text && end <= state->length) { + // Speak the changed portion when extending or shrinking a selection. + DWORD first = start, last = end; + kind = WINE_ACCESS_TEXT_SELECTED; + if (old_start != old_end && start == old_start) { + first = end < old_end ? end : old_end; + last = end > old_end ? end : old_end; + if (end < old_end) kind = WINE_ACCESS_TEXT_UNSELECTED; + } else if (old_start != old_end && end == old_end) { + first = start < old_start ? start : old_start; + last = start > old_start ? start : old_start; + if (start > old_start) kind = WINE_ACCESS_TEXT_UNSELECTED; + } + copy_range(text, state->text, first, last); + } else if (old_start != old_end && state->text && old_end <= state->length) { + copy_range(text, state->text, old_start, old_end); + kind = WINE_ACCESS_TEXT_UNSELECTED; + } else if (navigation && (wp == VK_UP || wp == VK_DOWN || wp == VK_PRIOR || wp == VK_NEXT)) { + current_line(window, start, text); + } else if (state->text && start < state->length) { + DWORD last = start + 1; + if (GetKeyState(VK_CONTROL) & 0x8000) { + while (last < state->length && !word_space(state->text[last])) ++last; + } else if (state->text[start] >= 0xd800 && state->text[start] <= 0xdbff && last < state->length) { + ++last; + } + copy_range(text, state->text, start, last); + } else if (!state->text) { + current_line(window, start, text); + } + report = TRUE; + } else if (changing && removed[0] && old_length > state->length) { + lstrcpynW(text, removed, 1024); + kind = WINE_ACCESS_TEXT_DELETED; report = TRUE; + } + if (report) notify_text(window, kind, text, start, end); + state->start = start; state->end = end; + state->busy = FALSE; + return result; +} + +void wine_access_watch_edit(HWND window) { + WCHAR class_name[64]; + if (!window || !GetClassNameW(window, class_name, 64) || lstrcmpW(class_name, L"Edit")) return; + // Wine's comctl32 v5 exports GetWindowSubclass only by ordinal. Our own + // property also keeps attachment independent of the application's manifest. + if (GetPropW(window, EDIT_STATE_PROPERTY)) return; + EditState *state = calloc(1, sizeof(*state)); + if (!state) return; + if (!SetWindowSubclass(window, edit_proc, EDIT_SUBCLASS_ID, (DWORD_PTR)state)) { + free(state); return; + } + if (!SetPropW(window, EDIT_STATE_PROPERTY, state)) { + RemoveWindowSubclass(window, edit_proc, EDIT_SUBCLASS_ID); + free(state); return; + } + // Subclass callbacks can outlive the reader. Keep their module mapped until + // the application exits; each control frees its state on WM_NCDESTROY. + HMODULE module; + GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_PIN, + (LPCWSTR)(UINT_PTR)edit_proc, &module); +} + +__declspec(dllexport) LRESULT CALLBACK WineAccessInputHook(int code, WPARAM wp, LPARAM lp) { + if (code >= 0 && wp == PM_REMOVE) wine_access_watch_edit(GetFocus()); + return CallNextHookEx(NULL, code, wp, lp); +} diff --git a/edit_hook.h b/edit_hook.h new file mode 100644 index 0000000..5935792 --- /dev/null +++ b/edit_hook.h @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +#pragma once +#include +void wine_access_watch_edit(HWND window); diff --git a/main.cpp b/main.cpp index 0036a81..c49b7da 100644 --- a/main.cpp +++ b/main.cpp @@ -24,6 +24,8 @@ constexpr auto INTERFACE_NAME = "org.stormux.Cthulhu1.Service"; constexpr int DBUS_TIMEOUT_MS = 500; GDBusConnection *connection = nullptr; std::deque menuEvents; +std::deque textEvents; +bool editReaderActive = false; std::string lastMenuEvent; ULONGLONG lastMenuOwner = 0; @@ -360,9 +362,59 @@ void present_menu_event(const WineAccessMenuEvent &event) { } } +void present_text_event(const WineAccessTextEvent &event) { + HWND owner = reinterpret_cast(static_cast(event.owner)); + HWND window = reinterpret_cast(static_cast(event.control)); + GUITHREADINFO info{}; + info.cbSize = sizeof(info); + if (owner == nullptr || owner != GetAncestor(GetForegroundWindow(), GA_ROOT) || + !GetGUIThreadInfo(GetWindowThreadProcessId(owner, nullptr), &info) || info.hwndFocus != window) + return; + std::string text = to_utf8(event.text); + ControlDescription control; + if (event.kind == WINE_ACCESS_TEXT_PASSWORD) { + control.role = "edit"; + control.description = "password"; + } else if (event.kind == WINE_ACCESS_TEXT_FOCUS) { + wchar_t label[256] = {}; + find_control_label(window, label, std::size(label)); + control.name = to_utf8(label); + control.role = GetWindowLongPtrW(window, GWL_STYLE) & ES_READONLY ? "read only edit" : "edit"; + control.value = text.empty() ? "blank" : text; + } else { + if (text == " ") text = "space"; + else if (text == "\t") text = "tab"; + else if (text == "\r" || text == "\n" || text == "\r\n") text = "new line"; + const char *format = nullptr; + if (event.kind == WINE_ACCESS_TEXT_SELECTED) format = "%s selected"; + else if (event.kind == WINE_ACCESS_TEXT_UNSELECTED) format = "%s unselected"; + else if (event.kind == WINE_ACCESS_TEXT_DELETED) format = "%s deleted"; + if (format != nullptr) { + gchar *message = g_strdup_printf(format, text.c_str()); + control.name = message; + g_free(message); + } else { + control.name = text.empty() ? "blank" : text; + } + } + present_accessible_event(source_id(window, "text"), "focus", control, to_utf8(event.title)); +} + LRESULT CALLBACK menu_bridge_proc(HWND window, UINT message, WPARAM wp, LPARAM lp) { if (message == WM_COPYDATA && lp != 0) { const auto *data = reinterpret_cast(lp); + if (data->dwData == WINE_ACCESS_TEXT_TAG && data->cbData == sizeof(WineAccessTextEvent) && + data->lpData != nullptr && textEvents.size() < 32) { + auto event = *static_cast(data->lpData); + event.text[std::size(event.text) - 1] = L'\0'; + event.title[std::size(event.title) - 1] = L'\0'; + textEvents.push_back(event); + if (!PostMessageW(window, WINE_ACCESS_MENU_MESSAGE + 1, 0, 0)) { + textEvents.pop_back(); + return FALSE; + } + return TRUE; + } if (data->dwData != WINE_ACCESS_MENU_TAG || data->cbData != sizeof(WineAccessMenuEvent) || data->lpData == nullptr || menuEvents.size() >= 32) return FALSE; @@ -385,6 +437,14 @@ LRESULT CALLBACK menu_bridge_proc(HWND window, UINT message, WPARAM wp, LPARAM l } return 0; } + if (message == WINE_ACCESS_MENU_MESSAGE + 1) { + if (!textEvents.empty()) { + const auto event = textEvents.front(); + textEvents.pop_front(); + present_text_event(event); + } + return 0; + } return DefWindowProcW(window, message, wp, lp); } @@ -411,6 +471,10 @@ void CALLBACK winevent_callback(HWINEVENTHOOK, DWORD event, HWND window, event != EVENT_OBJECT_VALUECHANGE && event != EVENT_OBJECT_SELECTION && event != EVENT_OBJECT_STATECHANGE) return; + wchar_t className[64] = {}; + if (editReaderActive && GetClassNameW(window, className, std::size(className)) && + lstrcmpW(className, L"Edit") == 0) + return; IAccessible *accessible = nullptr; VARIANT child{}; VariantInit(&child); @@ -639,6 +703,7 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) { HWINEVENTHOOK hook = nullptr; HHOOK menuHook = nullptr; + HHOOK inputHook = nullptr; HMODULE menuModule = nullptr; HWND menuBridge = nullptr; if (dialogReaderEnabled) { @@ -656,6 +721,10 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) { const auto callback = std::bit_cast(GetProcAddress(menuModule, "WineAccessMenuHook")); if (callback != nullptr) menuHook = SetWindowsHookExW(WH_CALLWNDPROC, callback, menuModule, 0); + const auto inputCallback = std::bit_cast(GetProcAddress(menuModule, "WineAccessInputHook")); + if (inputCallback != nullptr) + inputHook = SetWindowsHookExW(WH_GETMESSAGE, inputCallback, menuModule, 0); + editReaderActive = menuHook != nullptr && inputHook != nullptr; } if (menuHook == nullptr) std::fprintf(stderr, "wine-access: native menu hook unavailable (error %lu)\n", @@ -670,6 +739,8 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) { UnhookWinEvent(hook); if (menuHook != nullptr) UnhookWindowsHookEx(menuHook); + if (inputHook != nullptr) + UnhookWindowsHookEx(inputHook); if (menuBridge != nullptr) DestroyWindow(menuBridge); if (menuModule != nullptr) diff --git a/menu_bridge.h b/menu_bridge.h index 6f12efc..3da75d9 100644 --- a/menu_bridge.h +++ b/menu_bridge.h @@ -9,6 +9,23 @@ #define WINE_ACCESS_MENU_SUBMENU 2 #define WINE_ACCESS_MENU_CHECKED 4 #define WINE_ACCESS_MENU_DISABLED 8 +#define WINE_ACCESS_TEXT_TAG 0x57415431 +#define WINE_ACCESS_TEXT_FOCUS 1 +#define WINE_ACCESS_TEXT_SELECTED 2 +#define WINE_ACCESS_TEXT_UNSELECTED 3 +#define WINE_ACCESS_TEXT_DELETED 4 +#define WINE_ACCESS_TEXT_READ 5 +#define WINE_ACCESS_TEXT_PASSWORD 6 + +typedef struct WineAccessTextEvent { + ULONGLONG owner; + ULONGLONG control; + UINT kind; + DWORD start; + DWORD end; + WCHAR text[1024]; + WCHAR title[256]; +} WineAccessTextEvent; typedef struct WineAccessMenuEvent { ULONGLONG owner; diff --git a/menu_hook.c b/menu_hook.c index 9d96eaf..7967e6c 100644 --- a/menu_hook.c +++ b/menu_hook.c @@ -1,5 +1,6 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "menu_bridge.h" +#include "edit_hook.h" static void menu_text(WCHAR *output, UINT capacity, const WCHAR *input) { UINT used = 0; @@ -19,6 +20,7 @@ __declspec(dllexport) LRESULT CALLBACK WineAccessMenuHook(int code, WPARAM wp, LPARAM lp) { if (code >= 0) { 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) { HWND bridge = FindWindowW(WINE_ACCESS_MENU_CLASS, NULL); HWND owner = GetAncestor(message->hwnd, GA_ROOT); diff --git a/tests/edit_fixture.c b/tests/edit_fixture.c new file mode 100644 index 0000000..2a4cf7a --- /dev/null +++ b/tests/edit_fixture.c @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +#include + +static HWND editor, password; +static int step; + +static void key(UINT key, BOOL control, BOOL shift) { + BYTE saved[256], state[256]; + GetKeyboardState(saved); + CopyMemory(state, saved, sizeof(state)); + state[VK_CONTROL] = control ? 0x80 : 0; + state[VK_SHIFT] = shift ? 0x80 : 0; + SetKeyboardState(state); + SendMessageW(editor, WM_KEYDOWN, key, 1); + SetKeyboardState(saved); +} + +static LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wp, LPARAM lp) { + if (message == WM_TIMER) { + switch (++step) { + case 1: key(VK_RIGHT, FALSE, FALSE); break; + case 2: key(VK_RIGHT, TRUE, FALSE); break; + case 3: key(VK_DOWN, FALSE, FALSE); break; + case 4: key(VK_HOME, FALSE, FALSE); break; + case 5: key(VK_RIGHT, FALSE, TRUE); break; + case 6: key(VK_RIGHT, FALSE, TRUE); break; + case 7: key(VK_LEFT, FALSE, TRUE); break; + case 8: key(VK_LEFT, FALSE, FALSE); break; + case 9: key(VK_DELETE, FALSE, FALSE); break; + case 10: + key(VK_RIGHT, FALSE, FALSE); + SendMessageW(editor, WM_CHAR, VK_BACK, 1); + break; + case 11: SendMessageW(editor, EM_SETSEL, 0, 5); break; + case 12: SendMessageW(editor, WM_CLEAR, 0, 0); break; + case 13: SetFocus(password); break; + case 14: SendMessageW(password, WM_KEYDOWN, VK_LEFT, 1); break; + case 15: SendMessageW(password, WM_CHAR, VK_BACK, 1); break; + case 16: + SetFocus(editor); + SetWindowTextW(editor, L"\U0001f600 cafe"); + SendMessageW(editor, EM_SETSEL, 2, 2); + break; + case 17: key(VK_HOME, FALSE, FALSE); break; + case 18: + SendMessageW(editor, EM_SETREADONLY, TRUE, 0); + SetFocus(password); SetFocus(editor); + break; + case 19: key(VK_DELETE, FALSE, FALSE); break; + default: + if (GetWindowTextLengthW(editor) != 7) ExitProcess(2); + DestroyWindow(window); break; + } + return 0; + } + 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.lpszClassName = L"WineAccessEditFixture"; + cls.lpfnWndProc = window_proc; RegisterClassW(&cls); + HWND window = CreateWindowW(cls.lpszClassName, L"Wine editor fixture", WS_OVERLAPPEDWINDOW, + 120, 120, 500, 300, NULL, NULL, instance, NULL); + editor = CreateWindowW(L"Edit", L"alpha beta\r\nsecond line", + WS_CHILD | WS_VISIBLE | WS_TABSTOP | ES_MULTILINE | ES_AUTOVSCROLL, + 10, 10, 450, 150, window, NULL, instance, NULL); + password = CreateWindowW(L"Edit", L"private-password-fixture", + WS_CHILD | WS_VISIBLE | ES_PASSWORD, 10, 180, 300, 30, + window, NULL, instance, NULL); + ShowWindow(window, SW_SHOW); SetForegroundWindow(window); SetFocus(editor); + SendMessageW(editor, EM_SETSEL, 0, 0); + SetTimer(window, 1, 350, NULL); + MSG message; + while (GetMessageW(&message, NULL, 0, 0) > 0) { + TranslateMessage(&message); DispatchMessageW(&message); + } + return 0; +} diff --git a/tests/menu_integration.py b/tests/menu_integration.py index 6e68424..0b4b939 100644 --- a/tests/menu_integration.py +++ b/tests/menu_integration.py @@ -16,11 +16,13 @@ from gi.repository import Gio, GLib parser = argparse.ArgumentParser() parser.add_argument("--build-dir", default="build") parser.add_argument("--session", action="store_true") +parser.add_argument("--fixture", choices=("menu", "edit"), default="menu") args = parser.parse_args() 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)])) + __file__, "--session", "--build-dir", str(build), + "--fixture", args.fixture])) interface = "org.stormux.Cthulhu1.Service" object_path = "/org/stormux/Cthulhu1/Service" @@ -47,7 +49,7 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary: WINEDLLOVERRIDES="mscoree,mshtml=", CTHULHU_WINE_ACCESS_MANAGED="1") fixture = Path(temporary) / "menu-fixture.exe" subprocess.run(["x86_64-w64-mingw32-gcc", "-Wall", "-Wextra", "-mwindows", - "-o", str(fixture), str(Path(__file__).with_name("menu_fixture.c")), + "-o", str(fixture), str(Path(__file__).with_name(args.fixture + "_fixture.c")), "-luser32"], check=True) helper = None app = None @@ -72,6 +74,22 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary: while GLib.MainContext.default().iteration(False): pass time.sleep(0.01) + if args.fixture == "edit": + text_events = [e for e in events if e[0].endswith(":text")] + for event in text_events: + print(event[2:7]) + spoken = [e[2] for e in text_events] + for expected in ("l", "beta", "second line", "s selected", "e selected", + "e unselected", "s unselected", "s deleted", "e deleted", + "alpha selected", "alpha deleted", "😀"): + assert expected in spoken, f"missing editor feedback: {expected!r}" + assert any(e[3] == "edit" and e[4] == "alpha beta" for e in text_events), "initial line missing" + assert any(e[3] == "read only edit" for e in text_events), "read-only focus missing" + assert any(e[6] == "password" for e in text_events), "password role missing" + assert not any("private-password-fixture" in str(e) for e in events), "password leaked" + assert "😀 deleted" not in spoken, "read-only control reported deletion" + print("PASS: editor focus, characters, words, lines, selection, deletion, Unicode, passwords, and read-only controls") + sys.exit(0) menus = [event for event in events if ":menu:" in event[0]] for event in menus: print(event[2:7])