Files

556 lines
18 KiB
C++

// SPDX-License-Identifier: LGPL-2.1-or-later
#include "nvdaController.h"
#include <commctrl.h>
#include <gio/gio.h>
#include <oleacc.h>
#include <rpc.h>
#include <windows.h>
#include <cstdint>
#include <cstdlib>
#include <cwchar>
#include <iterator>
#include <string>
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<const gunichar2 *>(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<LPARAM>(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<std::uintptr_t>(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<DWORD_PTR>(-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<LONG_PTR>(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<LPARAM>(text), SMTO_ABORTIFHUNG,
200, &length)) {
tidy_text(text);
control.value = to_utf8(text);
}
control.position = static_cast<int>(selected) + 1;
}
if (static_cast<LONG_PTR>(count) >= 0)
control.count = static_cast<int>(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<LONG>(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<std::uintptr_t>(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<int>(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<RPC_WSTR>(const_cast<wchar_t *>(L"ncalrpc")),
RPC_C_PROTSEQ_MAX_REQS_DEFAULT,
reinterpret_cast<RPC_WSTR>(const_cast<wchar_t *>(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<RPC_WSTR>(const_cast<wchar_t *>(L"ncalrpc")),
RPC_C_PROTSEQ_MAX_REQS_DEFAULT, reinterpret_cast<RPC_WSTR>(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<error_status_t>(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;
}