// SPDX-License-Identifier: LGPL-2.1-or-later #include "nvdaController.h" #include #include #include #include #include #include #include #include #include #include namespace { constexpr auto BUS_NAME = "org.stormux.Cthulhu1.Service"; constexpr auto OBJECT_PATH = "/org/stormux/Cthulhu1/Service"; constexpr auto INTERFACE_NAME = "org.stormux.Cthulhu1.Service"; constexpr int DBUS_TIMEOUT_MS = 500; GDBusConnection *connection = nullptr; struct ControlDescription { std::string name; std::string role; std::string value; std::string states; std::string description; int position = 0; int count = 0; }; std::string to_utf8(const wchar_t *text) { if (text == nullptr) return {}; GError *error = nullptr; gchar *converted = g_utf16_to_utf8(reinterpret_cast(text), -1, nullptr, nullptr, &error); if (error != nullptr) g_error_free(error); if (converted == nullptr) return {}; std::string result{converted}; g_free(converted); return result; } DWORD call_boolean(const char *method, GVariant *parameters) { if (connection == nullptr) return ERROR_SERVICE_NOT_ACTIVE; GError *error = nullptr; GVariant *reply = g_dbus_connection_call_sync( connection, BUS_NAME, OBJECT_PATH, INTERFACE_NAME, method, parameters, G_VARIANT_TYPE("(b)"), G_DBUS_CALL_FLAGS_NONE, DBUS_TIMEOUT_MS, nullptr, &error); if (error != nullptr) g_error_free(error); if (reply == nullptr) return ERROR_GEN_FAILURE; gboolean result = FALSE; g_variant_get(reply, "(b)", &result); g_variant_unref(reply); return result == TRUE ? ERROR_SUCCESS : ERROR_GEN_FAILURE; } bool cthulhu_is_available() { GError *error = nullptr; GVariant *reply = g_dbus_connection_call_sync( connection, "org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "NameHasOwner", g_variant_new("(s)", BUS_NAME), G_VARIANT_TYPE("(b)"), G_DBUS_CALL_FLAGS_NONE, DBUS_TIMEOUT_MS, nullptr, &error); if (error != nullptr) g_error_free(error); if (reply == nullptr) return false; gboolean owned = FALSE; g_variant_get(reply, "(b)", &owned); g_variant_unref(reply); return owned == TRUE; } std::string variant_text(const VARIANT &value) { if (value.vt == VT_BSTR) return to_utf8(value.bstrVal); if (value.vt == VT_I4) { wchar_t buffer[128] = {}; if (GetRoleTextW(value.lVal, buffer, 128) > 0) return to_utf8(buffer); return std::to_string(value.lVal); } return {}; } std::string accessible_property( IAccessible *accessible, HRESULT (STDMETHODCALLTYPE IAccessible::*getter)(VARIANT, BSTR *), const VARIANT &child) { BSTR value = nullptr; const HRESULT result = (accessible->*getter)(child, &value); if (FAILED(result) || value == nullptr) return {}; const std::string text = to_utf8(value); SysFreeString(value); return text; } // Standard-control queries follow the LGPL wine-a11y reader published by // Mudb0y, while presentation remains on Cthulhu's D-Bus interface. void tidy_text(wchar_t *text, bool label = false) { wchar_t *output = text; for (wchar_t *input = text; *input != L'\0'; ++input) { if (*input != L'&') *output++ = *input; } *output = L'\0'; if (label && output > text && output[-1] == L':') output[-1] = L'\0'; } bool get_control_text(HWND window, wchar_t *text, int capacity) { DWORD_PTR result = 0; text[0] = L'\0'; if (!SendMessageTimeoutW(window, WM_GETTEXT, capacity, reinterpret_cast(text), SMTO_ABORTIFHUNG, 200, &result)) return false; tidy_text(text); return text[0] != L'\0'; } void find_control_label(HWND window, wchar_t *text, int capacity) { HWND previous = window; wchar_t className[64] = {}; text[0] = L'\0'; for (int hop = 0; hop < 3; ++hop) { previous = GetWindow(previous, GW_HWNDPREV); if (previous == nullptr || !GetClassNameW(previous, className, std::size(className))) return; if (wcscmp(className, L"Static") != 0 && wcscmp(className, L"Button") != 0) continue; if (get_control_text(previous, text, capacity)) tidy_text(text, true); return; } } std::string window_title(HWND window) { wchar_t title[512] = {}; GetWindowTextW(GetAncestor(window, GA_ROOT), title, std::size(title)); tidy_text(title); return to_utf8(title); } std::string source_id(HWND window, const char *suffix) { return std::to_string(reinterpret_cast(window)) + ":" + suffix; } bool present_accessible_event(const std::string &sourceId, const char *eventType, const ControlDescription &control, const std::string &windowTitle) { return call_boolean("PresentAccessibleEvent", g_variant_new( "(sssssssiis)", sourceId.c_str(), eventType, control.name.c_str(), control.role.c_str(), control.value.c_str(), control.states.c_str(), control.description.c_str(), control.position, control.count, windowTitle.c_str())) == ERROR_SUCCESS; } ControlDescription describe_standard_control(HWND window) { ControlDescription control; wchar_t className[64] = {}; wchar_t text[512] = {}; wchar_t label[256] = {}; if (!GetClassNameW(window, className, std::size(className))) return control; get_control_text(window, text, std::size(text)); if (wcscmp(className, L"Button") == 0) { control.name = to_utf8(text); const LONG_PTR style = GetWindowLongPtrW(window, GWL_STYLE); DWORD_PTR checked = 0; switch (style & BS_TYPEMASK) { case BS_CHECKBOX: case BS_AUTOCHECKBOX: case BS_3STATE: case BS_AUTO3STATE: control.role = "check box"; SendMessageTimeoutW(window, BM_GETCHECK, 0, 0, SMTO_ABORTIFHUNG, 200, &checked); control.description = checked == BST_CHECKED ? "checked" : checked == BST_INDETERMINATE ? "half checked" : "not checked"; break; case BS_RADIOBUTTON: case BS_AUTORADIOBUTTON: control.role = "radio button"; SendMessageTimeoutW(window, BM_GETCHECK, 0, 0, SMTO_ABORTIFHUNG, 200, &checked); control.description = checked == BST_CHECKED ? "checked" : "not checked"; break; case BS_GROUPBOX: control.role = "grouping"; break; default: control.role = "button"; } return control; } const bool isEdit = wcscmp(className, L"Edit") == 0 || wcscmp(className, L"RICHEDIT50W") == 0 || wcscmp(className, L"RichEdit20W") == 0 || wcscmp(className, L"RichEdit20A") == 0; if (isEdit) { find_control_label(window, label, std::size(label)); control.name = to_utf8(label); const LONG_PTR style = GetWindowLongPtrW(window, GWL_STYLE); control.role = style & ES_READONLY ? "read only edit" : "edit"; if (style & ES_PASSWORD) control.description = "password"; else control.value = to_utf8(text); return control; } if (wcscmp(className, L"ComboBox") == 0 || wcscmp(className, L"ComboBoxEx32") == 0) { find_control_label(window, label, std::size(label)); control.name = to_utf8(label); control.role = "combo box"; control.value = to_utf8(text); return control; } if (wcscmp(className, L"ListBox") == 0 || wcscmp(className, L"ComboLBox") == 0) { find_control_label(window, label, std::size(label)); control.name = to_utf8(label); control.role = "list"; DWORD_PTR selected = static_cast(-1); DWORD_PTR count = 0; DWORD_PTR length = 0; SendMessageTimeoutW(window, LB_GETCOUNT, 0, 0, SMTO_ABORTIFHUNG, 200, &count); SendMessageTimeoutW(window, LB_GETCURSEL, 0, 0, SMTO_ABORTIFHUNG, 200, &selected); const LONG_PTR style = GetWindowLongPtrW(window, GWL_STYLE); const bool stringItems = !(style & (LBS_OWNERDRAWFIXED | LBS_OWNERDRAWVARIABLE)) || (style & LBS_HASSTRINGS); if (static_cast(selected) >= 0) { SendMessageTimeoutW(window, LB_GETTEXTLEN, selected, 0, SMTO_ABORTIFHUNG, 200, &length); if (stringItems && length < std::size(text) && SendMessageTimeoutW(window, LB_GETTEXT, selected, reinterpret_cast(text), SMTO_ABORTIFHUNG, 200, &length)) { tidy_text(text); control.value = to_utf8(text); } control.position = static_cast(selected) + 1; } if (static_cast(count) >= 0) control.count = static_cast(count); return control; } if (wcscmp(className, L"msctls_trackbar32") == 0) { find_control_label(window, label, std::size(label)); control.name = to_utf8(label); control.role = "slider"; DWORD_PTR position = 0; SendMessageTimeoutW(window, TBM_GETPOS, 0, 0, SMTO_ABORTIFHUNG, 200, &position); control.value = std::to_string(static_cast(position)); return control; } if (wcscmp(className, L"Static") == 0) { control.name = to_utf8(text); control.role = "static text"; return control; } control.name = to_utf8(text); return control; } bool present_standard_focus(HWND window) { const ControlDescription control = describe_standard_control(window); if (control.name.empty() && control.role.empty() && control.value.empty() && control.description.empty()) return false; return present_accessible_event(source_id(window, "standard"), "focus", control, window_title(window)); } void CALLBACK winevent_callback(HWINEVENTHOOK, DWORD event, HWND window, LONG objectId, LONG childId, DWORD, DWORD) { if (event != EVENT_OBJECT_FOCUS && event != EVENT_OBJECT_NAMECHANGE && event != EVENT_OBJECT_VALUECHANGE && event != EVENT_OBJECT_SELECTION && event != EVENT_OBJECT_STATECHANGE) return; IAccessible *accessible = nullptr; VARIANT child{}; VariantInit(&child); if (FAILED(AccessibleObjectFromEvent(window, objectId, childId, &accessible, &child)) || accessible == nullptr) { if (event == EVENT_OBJECT_FOCUS) present_standard_focus(window); return; } const std::string name = accessible_property(accessible, &IAccessible::get_accName, child); const std::string value = accessible_property(accessible, &IAccessible::get_accValue, child); const std::string description = accessible_property(accessible, &IAccessible::get_accDescription, child); VARIANT role{}; VariantInit(&role); std::string roleText; if (SUCCEEDED(accessible->get_accRole(child, &role))) roleText = variant_text(role); VariantClear(&role); VARIANT state{}; VariantInit(&state); std::string stateText; if (SUCCEEDED(accessible->get_accState(child, &state))) stateText = variant_text(state); VariantClear(&state); LONG childCount = 0; accessible->get_accChildCount(&childCount); accessible->Release(); VariantClear(&child); if (event == EVENT_OBJECT_FOCUS && name.empty() && value.empty() && description.empty() && (roleText.empty() || roleText == "client") && present_standard_focus(window)) return; const std::string windowTitle = window_title(window); const std::string sourceId = std::to_string(reinterpret_cast(window)) + ":" + std::to_string(objectId) + ":" + std::to_string(childId); const char *eventType = nullptr; switch (event) { case EVENT_OBJECT_FOCUS: eventType = "focus"; break; case EVENT_OBJECT_NAMECHANGE: eventType = "name"; break; case EVENT_OBJECT_VALUECHANGE: eventType = "value"; break; case EVENT_OBJECT_SELECTION: eventType = "selection"; break; case EVENT_OBJECT_STATECHANGE: eventType = "state"; break; default: return; } ControlDescription control; control.name = name; control.role = roleText; control.value = value; control.states = stateText; control.description = description; control.count = static_cast(childCount); present_accessible_event(sourceId, eventType, control, windowTitle); } void reset_rpc_server() { RpcMgmtStopServerListening(nullptr); RpcServerUnregisterIf(nullptr, nullptr, FALSE); } bool register_rpc_server() { if (RpcServerUseProtseqEpW( reinterpret_cast(const_cast(L"ncalrpc")), RPC_C_PROTSEQ_MAX_REQS_DEFAULT, reinterpret_cast(const_cast(L"NvdaCtlr")), nullptr) != RPC_S_OK) return false; DWORD sessionId = 0; if (!ProcessIdToSessionId(GetCurrentProcessId(), &sessionId)) { reset_rpc_server(); return false; } wchar_t desktopName[256] = {}; DWORD bytesNeeded = 0; if (!GetUserObjectInformationW(GetThreadDesktop(GetCurrentThreadId()), UOI_NAME, desktopName, sizeof(desktopName), &bytesNeeded)) { reset_rpc_server(); return false; } wchar_t endpoint[512] = {}; wsprintfW(endpoint, L"NvdaCtlr.%lu.%s", sessionId, desktopName); if (RpcServerUseProtseqEpW( reinterpret_cast(const_cast(L"ncalrpc")), RPC_C_PROTSEQ_MAX_REQS_DEFAULT, reinterpret_cast(endpoint), nullptr) != RPC_S_OK) { reset_rpc_server(); return false; } for (RPC_IF_HANDLE spec : {NvdaController_v1_0_s_ifspec, NvdaController2_v1_0_s_ifspec, NvdaController3_v1_0_s_ifspec}) { if (RpcServerRegisterIf(spec, nullptr, nullptr) != RPC_S_OK) { reset_rpc_server(); return false; } } if (RpcServerListen(1, RPC_C_LISTEN_MAX_CALLS_DEFAULT, TRUE) == RPC_S_OK) return true; reset_rpc_server(); return false; } } // namespace extern "C" void *__RPC_USER MIDL_user_allocate(size_t size) { return malloc(size); } extern "C" void __RPC_USER MIDL_user_free(void *pointer) { free(pointer); } extern "C" error_status_t __stdcall testIfRunning(void) { return cthulhu_is_available() ? ERROR_SUCCESS : ERROR_SERVICE_NOT_ACTIVE; } extern "C" error_status_t __stdcall speakText(const wchar_t *text) { const std::string utf8 = to_utf8(text); // Tolk calls cancelSpeech separately when its interrupt argument is true. return call_boolean("SpeakText", g_variant_new("(sb)", utf8.c_str(), FALSE)); } extern "C" error_status_t __stdcall cancelSpeech(void) { return call_boolean("CancelSpeech", nullptr); } extern "C" error_status_t __stdcall brailleMessage(const wchar_t *text) { const std::string utf8 = to_utf8(text); return call_boolean("BrailleMessage", g_variant_new("(s)", utf8.c_str())); } extern "C" error_status_t __stdcall getProcessId(ULONG *pid) { if (pid == nullptr) return ERROR_INVALID_PARAMETER; *pid = GetCurrentProcessId(); return ERROR_SUCCESS; } extern "C" error_status_t __stdcall speakSsml(const wchar_t *ssml, SYMBOL_LEVEL, SPEECH_PRIORITY, boolean) { const std::string utf8 = to_utf8(ssml); GError *error = nullptr; GVariant *reply = g_dbus_connection_call_sync( connection, BUS_NAME, OBJECT_PATH, INTERFACE_NAME, "SpeakSsml", g_variant_new("(ss)", "wine-nvda-controller", utf8.c_str()), G_VARIANT_TYPE("(i)"), G_DBUS_CALL_FLAGS_NONE, DBUS_TIMEOUT_MS, nullptr, &error); if (error != nullptr) g_error_free(error); if (reply == nullptr) return ERROR_GEN_FAILURE; gint result = ERROR_GEN_FAILURE; g_variant_get(reply, "(i)", &result); g_variant_unref(reply); return static_cast(result); } extern "C" error_status_t __stdcall onSsmlMarkReached(const wchar_t *) { return ERROR_CALL_NOT_IMPLEMENTED; } extern "C" error_status_t __stdcall isSpeaking(boolean *speaking) { if (speaking == nullptr) return ERROR_INVALID_PARAMETER; *speaking = FALSE; return ERROR_CALL_NOT_IMPLEMENTED; } int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) { const wchar_t *commandLine = GetCommandLineW(); const bool dialogReaderEnabled = wcsstr(commandLine, L"--no-dialog-reader") == nullptr; const bool controllerEnabled = wcsstr(commandLine, L"--no-controller") == nullptr; if (!dialogReaderEnabled && !controllerEnabled) return 0; HANDLE singleton = CreateMutexW(nullptr, TRUE, L"Local\\cthulhu-wine-access-singleton"); if (singleton == nullptr) return 1; if (GetLastError() == ERROR_ALREADY_EXISTS) { CloseHandle(singleton); return 0; } GError *error = nullptr; connection = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, &error); if (error != nullptr) g_error_free(error); if (connection == nullptr || !cthulhu_is_available()) { CloseHandle(singleton); return 1; } HWND window = nullptr; bool controllerActive = false; if (controllerEnabled && FindWindowW(L"wxWindowClassNR", L"NVDA") == nullptr) { WNDCLASSW windowClass{}; windowClass.lpfnWndProc = DefWindowProcW; windowClass.hInstance = instance; windowClass.lpszClassName = L"wxWindowClassNR"; RegisterClassW(&windowClass); window = CreateWindowExW(WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW, windowClass.lpszClassName, L"NVDA", 0, 0, 0, 0, 0, nullptr, nullptr, instance, nullptr); if (window != nullptr) { ShowWindow(window, SW_HIDE); controllerActive = register_rpc_server(); } } if (!controllerActive && window != nullptr) { DestroyWindow(window); window = nullptr; } HWINEVENTHOOK hook = nullptr; if (dialogReaderEnabled) hook = SetWinEventHook(EVENT_MIN, EVENT_MAX, nullptr, winevent_callback, 0, 0, WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS); MSG message{}; while (GetMessageW(&message, nullptr, 0, 0) > 0) { TranslateMessage(&message); DispatchMessageW(&message); } if (hook != nullptr) UnhookWinEvent(hook); if (controllerActive) { reset_rpc_server(); } if (window != nullptr) DestroyWindow(window); g_object_unref(connection); CloseHandle(singleton); return 0; }