Files
wine-access/main.cpp
T

1401 lines
56 KiB
C++

// SPDX-License-Identifier: LGPL-2.1-or-later
#include "nvdaController.h"
#include "menu_bridge.h"
#include "edit_hook.h"
#include "msaa_states.h"
#include "list_view.h"
#include "control_class.h"
#include "native_items.h"
#include "review_service.h"
#include "foreground_focus.h"
#include "foreground_hooks.h"
#include "reader_endpoint.h"
#include "msaa_events.h"
#include "reader_events.h"
#include "bootstrapper_controls.h"
#include "bootstrapper_hook.h"
#include "version.h"
#include "text_limits.h"
#include <commctrl.h>
#include <gio/gio.h>
#include <oleacc.h>
#include <rpc.h>
#include <windows.h>
#include <shellapi.h>
#include <uiautomationcore.h>
#include <cstdint>
#include <bit>
#include <cstdlib>
#include <cstdio>
#include <deque>
#include <iterator>
#include <string>
namespace {
// Read directly by readers without starting Wine or initializing a prefix.
// Keep this in the sidecar so it identifies the installed executable itself.
__attribute__((used, section(".wine_access.version")))
const char installedVersion[] = WINE_ACCESS_VERSION;
ReaderEndpoint readerEndpoint = CthulhuEndpoint;
constexpr int DBUS_TIMEOUT_MS = 500;
GDBusConnection *connection = nullptr;
ReviewIdentityStore reviewIdentities;
ReviewService reviewService;
MsaaEventReader msaaEvents;
BootstrapperControlReader bootstrapperReader;
HWND lastBootstrapperWindow = nullptr;
std::string lastBootstrapperIdentity, lastBootstrapperPresentation;
WineAccessBootstrapperState bootstrapperState{};
ReaderEventQueue readerEvents;
uint64_t msaaGeneration = 0;
MsaaEvent pendingMsaaFocus; // Event-thread owned; retired by focus changes, results or timeout.
std::deque<WineAccessMenuEvent> menuEvents;
std::deque<WineAccessTextEvent> textEvents;
std::deque<WineAccessNotificationEvent> notificationEvents;
bool editReaderActive = false;
std::string lastMenuEvent;
ULONGLONG lastMenuOwner = 0;
HWND lastTextControl = nullptr;
HWND lastListControl = nullptr;
int lastListIndex = -1;
int lastListCount = 0;
bool lastListSelected = false;
int lastListChecked = -1;
HWND lastListBoxControl = nullptr;
int lastListBoxIndex = -1;
int lastListBoxCount = 0;
HWND lastNativeControl = nullptr;
NativeItemKey lastNativeItem;
HWND lastComboControl = nullptr;
std::string lastComboValue;
bool is_list_box(HWND window) {
wchar_t className[128] = {};
return GetClassNameW(window, className, std::size(className)) &&
(wine_access_class_matches(className, L"ListBox") ||
wine_access_class_matches(className, L"ComboLBox"));
}
bool is_combo_box(HWND window) {
wchar_t className[128] = {};
return GetClassNameW(window, className, std::size(className)) &&
(wine_access_class_matches(className, L"ComboBox") ||
wine_access_class_matches(className, L"ComboBoxEx32"));
}
HWND combo_event_control(HWND window, DWORD event) {
if (is_combo_box(window)) return window;
wchar_t className[128] = {};
const bool comboList = GetClassNameW(window, className, std::size(className)) &&
wine_access_class_matches(className, L"ComboLBox");
if (event != EVENT_OBJECT_FOCUS && !comboList) return nullptr;
GUITHREADINFO info{};
info.cbSize = sizeof(info);
HWND foreground = GetForegroundWindow();
if (foreground && GetGUIThreadInfo(GetWindowThreadProcessId(foreground, nullptr), &info) &&
is_combo_box(info.hwndFocus))
return info.hwndFocus;
return 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, readerEndpoint.busName, readerEndpoint.objectPath,
readerEndpoint.interfaceName, 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 reader_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)", readerEndpoint.busName),
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;
}
bool reader_supports_protocol(bool controllerEnabled, bool dialogReaderEnabled) {
GError *error = nullptr;
GVariant *reply = g_dbus_connection_call_sync(
connection, readerEndpoint.busName, readerEndpoint.objectPath,
"org.freedesktop.DBus.Introspectable", "Introspect", nullptr,
G_VARIANT_TYPE("(s)"), G_DBUS_CALL_FLAGS_NONE, DBUS_TIMEOUT_MS, nullptr,
&error);
if (error) g_error_free(error);
if (!reply) {
std::fprintf(stderr, "wine-access: reader object %s is unavailable\n",
readerEndpoint.objectPath);
return false;
}
const char *xml = nullptr;
g_variant_get(reply, "(&s)", &xml);
GDBusNodeInfo *node = g_dbus_node_info_new_for_xml(xml, nullptr);
g_variant_unref(reply);
GDBusInterfaceInfo *interfaceInfo = node ?
g_dbus_node_info_lookup_interface(node, readerEndpoint.interfaceName) : nullptr;
struct RequiredMethod {
const char *name;
const char *inputs;
const char *outputs;
bool required;
};
const RequiredMethod methods[] = {
// UIA notifications use speech directly even without the RPC controller.
{"SpeakText", "sb", "b", controllerEnabled || dialogReaderEnabled},
{"CancelSpeech", "", "b", controllerEnabled || dialogReaderEnabled},
{"BrailleMessage", "s", "b", controllerEnabled},
{"SpeakSsml", "ss", "i", controllerEnabled},
{"PresentAccessibleEvent", "sssssssiis", "b", dialogReaderEnabled}};
bool supported = interfaceInfo != nullptr;
if (!interfaceInfo)
std::fprintf(stderr, "wine-access: reader interface %s is unavailable\n",
readerEndpoint.interfaceName);
for (const auto &required : methods) {
if (!interfaceInfo || !required.required) continue;
GDBusMethodInfo *method = g_dbus_interface_info_lookup_method(interfaceInfo, required.name);
std::string inputs, outputs;
if (method) {
for (auto args = method->in_args; args && *args; ++args) inputs += (*args)->signature;
for (auto args = method->out_args; args && *args; ++args) outputs += (*args)->signature;
}
if (!method || inputs != required.inputs || outputs != required.outputs) {
std::fprintf(stderr, "wine-access: reader method %s requires %s -> %s\n",
required.name, required.inputs, required.outputs);
supported = false;
}
}
if (node) g_dbus_node_info_unref(node);
return supported;
}
// Standard-control queries follow the LGPL wine-a11y reader published by
// Mudb0y, while presentation uses the selected reader's D-Bus interface.
void strip_mnemonics(wchar_t *text) {
wchar_t *output = text;
for (wchar_t *input = text; *input != L'\0'; ++input) {
if (*input == L'&') {
if (input[1] != L'&') continue;
++input;
}
*output++ = *input;
}
*output = L'\0';
}
void trim_label(wchar_t *text) {
const int length = lstrlenW(text);
if (length && text[length - 1] == L':') text[length - 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;
// Only captions interpret mnemonic markers. Values and window titles
// contain literal text; even captions escape a literal ampersand as &&.
wchar_t className[128] = {};
if (GetClassNameW(window, className, std::size(className)) &&
(wine_access_class_matches(className, L"Button") ||
(wine_access_class_matches(className, L"Static") &&
!(GetWindowLongPtrW(window, GWL_STYLE) & SS_NOPREFIX))))
strip_mnemonics(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';
const bool knownClass = GetClassNameW(window, className, std::size(className));
const bool managedEdit = knownClass && lstrcmpiW(className, L"Edit") != 0 &&
wine_access_class_matches(className, L"Edit");
// Controls need not be created in label/control order. Prefer a
// visible static label immediately to the left on the same row, among
// siblings only. Keep the existing native-control ordering below.
if (knownClass &&
(wine_access_class_matches(className, L"Edit") ||
wine_access_class_matches(className, L"ComboBox") ||
wine_access_class_matches(className, L"ComboBoxEx32"))) {
RECT controlRect{};
if (GetWindowRect(window, &controlRect)) {
HWND label = nullptr;
LONG bestGap = 201;
HWND sibling = GetWindow(GetParent(window), GW_CHILD);
for (int hop = 0; sibling && hop < 128; ++hop, sibling = GetWindow(sibling, GW_HWNDNEXT)) {
RECT labelRect{};
if (sibling == window || !IsWindowVisible(sibling) ||
!GetClassNameW(sibling, className, std::size(className)) ||
!wine_access_class_matches(className, L"Static") || !GetWindowRect(sibling, &labelRect))
continue;
const LONG gap = controlRect.left - labelRect.right;
if (gap >= 0 && gap < bestGap && labelRect.top < controlRect.bottom &&
labelRect.bottom > controlRect.top) {
label = sibling;
bestGap = gap;
}
}
if (label && get_control_text(label, text, capacity)) {
trim_label(text);
return;
}
}
// Retain ordered labels for controls without a same-row sibling.
if (managedEdit) return;
}
for (int hop = 0; hop < 3; ++hop) {
previous = GetWindow(previous, GW_HWNDPREV);
if (previous == nullptr ||
!GetClassNameW(previous, className, std::size(className)))
return;
if (!wine_access_class_matches(className, L"Static") && !wine_access_class_matches(className, L"Button"))
continue;
if (get_control_text(previous, text, capacity))
trim_label(text);
return;
}
}
std::string window_title(HWND window) {
wchar_t title[512] = {};
GetWindowTextW(GetAncestor(window, GA_ROOT), title, std::size(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) {
// Readers can update their focus owner even when they suppress its output.
if (std::string(eventType) == "focus" && sourceId != source_id(lastTextControl, "text"))
lastTextControl = nullptr;
return readerEvents.submit(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()));
}
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;
const LONG_PTR windowStyle = GetWindowLongPtrW(window, GWL_STYLE);
const bool isEdit = wine_access_class_matches(className, L"Edit") ||
wine_access_class_matches(className, L"RICHEDIT50W") ||
wine_access_class_matches(className, L"RichEdit20W") ||
wine_access_class_matches(className, L"RichEdit20A");
if (!isEdit || !(windowStyle & ES_PASSWORD))
get_control_text(window, text, std::size(text));
if (wine_access_class_matches(className, L"Button")) {
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;
}
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 (wine_access_class_matches(className, L"ComboBox") ||
wine_access_class_matches(className, L"ComboBoxEx32")) {
find_control_label(window, label, std::size(label));
control.name = to_utf8(label);
control.role = "combo box";
control.value = to_utf8(text);
lastComboControl = window;
lastComboValue = control.value;
return control;
}
if (is_list_view(window)) {
find_control_label(window, label, std::size(label));
control.name = to_utf8(label);
control.role = "list";
const auto row = read_list_view(window);
control.value = row.text;
if (!row.text.empty()) control.position = row.index + 1;
control.count = row.count;
if (row.selected) control.states = "selected";
if (row.checked >= 0) {
if (!control.states.empty()) control.states += ", ";
control.states += row.checked ? "checked" : "not checked";
}
if (row.available) {
lastListControl = window;
lastListIndex = row.index;
lastListCount = row.count;
lastListSelected = row.selected;
lastListChecked = row.checked;
} else lastListControl = nullptr; // Retry after a late native reply.
return control;
}
if (is_native_item_control(window)) {
const auto item = read_native_item(window);
control.name = item.text;
control.role = item.role;
control.states = item.states;
control.position = item.position;
control.count = item.count;
if (item.available) {
lastNativeControl = window;
lastNativeItem = item.key;
} else lastNativeControl = nullptr;
return control;
}
if (wine_access_class_matches(className, L"ListBox") ||
wine_access_class_matches(className, L"ComboLBox")) {
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)) {
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);
lastListBoxControl = window;
lastListBoxIndex = static_cast<int>(selected);
lastListBoxCount = control.count;
return control;
}
if (wine_access_class_matches(className, L"msctls_trackbar32")) {
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 (wine_access_class_matches(className, L"Static")) {
control.name = to_utf8(text);
control.role = "static text";
return control;
}
control.name = to_utf8(text);
return control;
}
bool present_standard_focus(HWND window) {
// Read through the subclass so returning from a menu reports the caret's
// line, not the beginning of the document. Never block on the callback.
if (editReaderActive && GetPropW(window, EDIT_STATE_PROPERTY))
return PostMessageW(window, RegisterWindowMessageW(EDIT_FOCUS_MESSAGE), 0, 0);
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 poll_control_focus(bool refresh = false) {
GUITHREADINFO info{};
info.cbSize = sizeof(info);
HWND foreground = GetForegroundWindow();
const bool focused = foreground &&
GetGUIThreadInfo(GetWindowThreadProcessId(foreground, nullptr), &info) &&
!(info.flags & GUI_INMENUMODE);
HWND bootstrapper = focused && info.hwndFocus == foreground &&
is_bootstrapper_window(foreground) ? foreground : nullptr;
bootstrapperReader.observe(bootstrapper);
if (bootstrapper != lastBootstrapperWindow) {
bootstrapperState = {};
lastBootstrapperWindow = bootstrapper;
lastBootstrapperIdentity.clear(); lastBootstrapperPresentation.clear();
}
// Keep one native query outstanding even if the application stops pumping
// messages. The input hook clears the marker after the query completes.
HWND bridge = bootstrapper ? FindWindowW(WINE_ACCESS_MENU_CLASS, nullptr) : nullptr;
if (bootstrapper && bridge && GetPropW(bootstrapper, BOOTSTRAPPER_STATE_READY) ==
reinterpret_cast<HANDLE>(bridge) && !GetPropW(bootstrapper, BOOTSTRAPPER_STATE_PENDING) &&
SetPropW(bootstrapper, BOOTSTRAPPER_STATE_PENDING, reinterpret_cast<HANDLE>(1))) {
if (!PostMessageW(bootstrapper, RegisterWindowMessageW(BOOTSTRAPPER_STATE_MESSAGE), 0, 0))
RemovePropW(bootstrapper, BOOTSTRAPPER_STATE_PENDING);
}
BootstrapperControl result;
if (bootstrapperReader.take(result) && result.available &&
result.window == bootstrapper && GetTickCount64() < result.deadline) {
DWORD process = 0;
const DWORD thread = GetWindowThreadProcessId(bootstrapper, &process);
if (thread == result.thread && process == result.process) {
ControlDescription control;
control.name = result.name; control.role = result.role;
control.value = result.value; control.states = result.states;
if (control.role == "check box" && bootstrapperState.owner ==
static_cast<ULONGLONG>(reinterpret_cast<UINT_PTR>(bootstrapper)) &&
GetTickCount64() - bootstrapperState.tick < 500 &&
to_utf8(bootstrapperState.name) == control.name) {
control.states = bootstrapperState.state == 0 ? "not checked" :
bootstrapperState.state == 1 ? "checked" : "half checked";
if (bootstrapperState.flags & WINE_ACCESS_BOOTSTRAPPER_PASSWORD) control.states += ", password";
if (bootstrapperState.flags & WINE_ACCESS_BOOTSTRAPPER_DISABLED) control.states += ", unavailable";
}
// Deduplicate the actual presentation, not superseded UIA state flags.
const std::string presentation = result.identity + "\n" + control.name + "\n" +
control.role + "\n" + control.value + "\n" + control.states;
const bool focusChanged = result.identity != lastBootstrapperIdentity;
if (presentation != lastBootstrapperPresentation &&
present_accessible_event(source_id(bootstrapper, "bootstrapper-uia") + result.identity,
focusChanged ? "focus" : control.role == "check box" ? "state" : "value",
control, window_title(bootstrapper))) {
lastBootstrapperIdentity = result.identity; lastBootstrapperPresentation = presentation;
}
}
}
const bool focusedListBox = focused && is_list_box(info.hwndFocus);
if (!focusedListBox) {
lastListBoxControl = nullptr;
lastListBoxIndex = -1;
lastListBoxCount = 0;
}
if (!focused || !is_combo_box(info.hwndFocus)) {
lastComboControl = nullptr;
lastComboValue.clear();
}
if (focused && is_native_item_control(info.hwndFocus)) {
lastListControl = nullptr;
lastListIndex = -1;
lastListCount = 0;
lastListSelected = false;
lastListChecked = -1;
const auto key = native_item_key(info.hwndFocus);
if (info.hwndFocus != lastNativeControl) {
present_standard_focus(info.hwndFocus);
} else if (refresh || !(key == lastNativeItem)) {
const auto control = describe_standard_control(info.hwndFocus);
present_accessible_event(source_id(info.hwndFocus, "standard"), "selection",
control, window_title(info.hwndFocus));
}
return;
}
lastNativeControl = nullptr;
lastNativeItem = {};
if (focused && is_list_view(info.hwndFocus)) {
int count = 0;
const int index = list_view_index(info.hwndFocus, &count);
DWORD_PTR state = 0;
if (index >= 0)
SendMessageTimeoutW(info.hwndFocus, LVM_GETITEMSTATE, index, LVIS_SELECTED,
SMTO_ABORTIFHUNG, 100, &state);
const bool selected = state & LVIS_SELECTED;
const int checked = list_view_checked(info.hwndFocus, index);
if (info.hwndFocus != lastListControl) {
present_standard_focus(info.hwndFocus);
} else if (refresh || index != lastListIndex || count != lastListCount ||
selected != lastListSelected || checked != lastListChecked) {
const auto control = describe_standard_control(info.hwndFocus);
present_accessible_event(source_id(info.hwndFocus, "standard"), "selection",
control, window_title(info.hwndFocus));
}
return;
}
lastListControl = nullptr;
lastListIndex = -1;
lastListCount = 0;
lastListSelected = false;
lastListChecked = -1;
if (focusedListBox) {
DWORD_PTR selected = static_cast<DWORD_PTR>(-1);
DWORD_PTR count = 0;
if (!SendMessageTimeoutW(info.hwndFocus, LB_GETCURSEL, 0, 0,
SMTO_ABORTIFHUNG, 100, &selected) ||
!SendMessageTimeoutW(info.hwndFocus, LB_GETCOUNT, 0, 0,
SMTO_ABORTIFHUNG, 100, &count) ||
static_cast<LONG_PTR>(count) < 0)
return;
if (info.hwndFocus != lastListBoxControl) {
present_standard_focus(info.hwndFocus);
} else if (refresh || static_cast<int>(selected) != lastListBoxIndex ||
static_cast<int>(count) != lastListBoxCount) {
const auto control = describe_standard_control(info.hwndFocus);
present_accessible_event(source_id(info.hwndFocus, "standard"), "selection",
control, window_title(info.hwndFocus));
}
return;
}
if (focused && is_combo_box(info.hwndFocus)) {
if (info.hwndFocus != lastComboControl) {
present_standard_focus(info.hwndFocus);
} else {
wchar_t text[512] = {};
get_control_text(info.hwndFocus, text, std::size(text));
if (refresh || to_utf8(text) != lastComboValue) {
const auto control = describe_standard_control(info.hwndFocus);
present_accessible_event(source_id(info.hwndFocus, "standard"), "value",
control, window_title(info.hwndFocus));
}
}
}
}
void present_menu_event(const WineAccessMenuEvent &event) {
HWND owner = reinterpret_cast<HWND>(static_cast<UINT_PTR>(event.owner));
if (owner == nullptr)
return;
if (event.flags & WINE_ACCESS_MENU_CLOSED) {
if (!lastMenuEvent.empty() && event.owner == lastMenuOwner) {
lastMenuEvent.clear();
lastMenuOwner = 0;
GUITHREADINFO info{};
info.cbSize = sizeof(info);
if (owner == GetAncestor(GetForegroundWindow(), GA_ROOT) &&
GetGUIThreadInfo(GetWindowThreadProcessId(owner, nullptr), &info) &&
info.hwndFocus != nullptr)
present_standard_focus(info.hwndFocus);
}
return;
}
if (owner != GetAncestor(GetForegroundWindow(), GA_ROOT))
return;
ControlDescription control;
control.name = to_utf8(event.name);
control.role = event.flags & WINE_ACCESS_MENU_SUBMENU ? "submenu" : "menu item";
control.value = to_utf8(event.shortcut);
if (event.flags & WINE_ACCESS_MENU_CHECKED)
control.description = "checked";
if (event.flags & WINE_ACCESS_MENU_DISABLED) {
if (!control.description.empty()) control.description += ", ";
control.description += "unavailable";
}
const std::string id = std::to_string(event.owner) + ":menu:" +
std::to_string(event.menu) + ":" + std::to_string(event.item);
const std::string key = id + "\n" + control.name + "\n" + control.value +
"\n" + std::to_string(event.flags);
if (key == lastMenuEvent)
return;
if (present_accessible_event(id, "focus", control, to_utf8(event.title))) {
lastMenuEvent = key;
lastMenuOwner = event.owner;
}
}
void present_text_event(const WineAccessTextEvent &event) {
HWND owner = reinterpret_cast<HWND>(static_cast<UINT_PTR>(event.owner));
HWND window = reinterpret_cast<HWND>(static_cast<UINT_PTR>(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;
const bool scintilla = event.flags & WINE_ACCESS_TEXT_SCINTILLA;
const bool readOnly = scintilla ? (event.flags & WINE_ACCESS_TEXT_READONLY) != 0
: (GetWindowLongPtrW(window, GWL_STYLE) & ES_READONLY) != 0;
if (!scintilla && (GetWindowLongPtrW(window, GWL_STYLE) & ES_PASSWORD) &&
event.kind != WINE_ACCESS_TEXT_PASSWORD)
return;
std::string text = to_utf8(event.text);
ControlDescription control;
if (event.kind == WINE_ACCESS_TEXT_PASSWORD) {
wchar_t label[256] = {};
find_control_label(window, label, std::size(label));
control.name = to_utf8(label);
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 = 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;
}
}
const bool focus = event.kind == WINE_ACCESS_TEXT_FOCUS || event.kind == WINE_ACCESS_TEXT_PASSWORD;
const auto source = source_id(window, "text");
const auto title = to_utf8(event.title);
// A reader restart can encounter an editor whose subclass already exists.
// Re-establish focus before its first action without re-reading the document.
if (!focus && lastTextControl != window) {
ControlDescription focused;
focused.role = readOnly ? "read only edit" : "edit";
if (!present_accessible_event(source, "focus", focused, title)) return;
}
if (event.kind != WINE_ACCESS_TEXT_PASSWORD)
control.states = "start=" + std::to_string(event.start) + " end=" +
std::to_string(event.end) + " kind=" + std::to_string(event.kind);
if (scintilla) control.states += " offset_unit=byte";
// Text commands are ordered actions, not transient focus notifications.
if (present_accessible_event(source, focus ? "focus" : "text", control, title))
lastTextControl = window;
}
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_BOOTSTRAPPER_TAG &&
data->cbData == sizeof(WineAccessBootstrapperState) && data->lpData) {
const auto event = *static_cast<const WineAccessBootstrapperState *>(data->lpData);
HWND owner = reinterpret_cast<HWND>(static_cast<UINT_PTR>(event.owner));
if (owner != GetForegroundWindow() || !is_bootstrapper_window(owner) ||
event.state > 2 || GetTickCount64() - event.tick >= 500) return FALSE;
bootstrapperState = event;
bootstrapperState.name[std::size(bootstrapperState.name) - 1] = 0;
return TRUE;
}
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);
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;
auto event = *static_cast<const WineAccessMenuEvent *>(data->lpData);
event.name[std::size(event.name) - 1] = L'\0';
event.shortcut[std::size(event.shortcut) - 1] = L'\0';
event.title[std::size(event.title) - 1] = L'\0';
menuEvents.push_back(event);
if (!PostMessageW(window, WINE_ACCESS_MENU_MESSAGE, 0, 0)) {
menuEvents.pop_back();
return FALSE;
}
return TRUE;
}
if (message == WINE_ACCESS_MENU_MESSAGE) {
if (!menuEvents.empty()) {
const auto event = menuEvents.front();
menuEvents.pop_front();
present_menu_event(event);
}
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;
}
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);
}
bool helper_file_path(wchar_t *path, size_t capacity, const wchar_t *name) {
const DWORD length = GetModuleFileNameW(nullptr, path, capacity);
if (length == 0 || length >= capacity)
return false;
wchar_t *separator = nullptr;
for (wchar_t *cursor = path; *cursor; ++cursor)
if (*cursor == L'\\' || *cursor == L'/') separator = cursor;
if (separator == nullptr)
return false;
const auto remaining = capacity - (separator + 1 - path);
if (remaining <= static_cast<size_t>(lstrlenW(name)))
return false;
lstrcpyW(separator + 1, name);
return true;
}
HMODULE load_menu_hook() {
wchar_t path[4096] = {};
return helper_file_path(path, std::size(path), L"wine-access-menu.dll")
? LoadLibraryW(path) : nullptr;
}
HANDLE start_menu_host32(HWND bridge) {
wchar_t path[4096] = {};
if (!helper_file_path(path, std::size(path), L"wine-access-menu-host32.exe"))
return nullptr;
wchar_t command[8192] = {};
wsprintfW(command, L"\"%s\" %lu %lu", path, GetCurrentProcessId(),
static_cast<DWORD>(reinterpret_cast<UINT_PTR>(bridge)));
STARTUPINFOW startup{};
startup.cb = sizeof(startup);
PROCESS_INFORMATION process{};
if (!CreateProcessW(path, command, nullptr, nullptr, FALSE, 0, nullptr,
nullptr, &startup, &process))
return nullptr;
CloseHandle(process.hThread);
return process.hProcess;
}
void CALLBACK winevent_callback(HWINEVENTHOOK, DWORD event, HWND window,
LONG objectId, LONG childId, DWORD, DWORD) {
const bool presentationEvent = event == EVENT_OBJECT_FOCUS || event == EVENT_OBJECT_NAMECHANGE ||
event == EVENT_OBJECT_VALUECHANGE || event == EVENT_OBJECT_SELECTION || event == EVENT_OBJECT_STATECHANGE;
if (presentationEvent && !msaa_event_in_foreground(window)) return;
if (event == EVENT_OBJECT_FOCUS || event == EVENT_SYSTEM_FOREGROUND) {
bootstrapperReader.observe(nullptr);
bootstrapperState = {};
lastBootstrapperWindow = nullptr;
lastBootstrapperIdentity.clear(); lastBootstrapperPresentation.clear();
pendingMsaaFocus = {};
++msaaGeneration;
msaaEvents.invalidate(msaaGeneration);
}
if (event == EVENT_OBJECT_DESTROY) reviewIdentities.destroyed(window,objectId,childId);
else if (event == EVENT_OBJECT_REORDER) reviewIdentities.reordered(window,objectId);
if (event == EVENT_OBJECT_FOCUS || event == EVENT_SYSTEM_FOREGROUND)
reviewService.focus_changed(reviewIdentities.observe_foreground());
if (event != EVENT_OBJECT_FOCUS && event != EVENT_OBJECT_NAMECHANGE &&
event != EVENT_OBJECT_VALUECHANGE && event != EVENT_OBJECT_SELECTION &&
event != EVENT_OBJECT_STATECHANGE)
return;
if (editReaderActive && GetPropW(window, EDIT_STATE_PROPERTY))
return;
if (HWND combo = combo_event_control(window, event)) {
// The combo's internal list can emit focus events even while the combo
// retains keyboard focus. Keep a single source for focus and value updates.
GUITHREADINFO info{};
info.cbSize = sizeof(info);
HWND foreground = GetForegroundWindow();
if (foreground && GetGUIThreadInfo(GetWindowThreadProcessId(foreground, nullptr), &info) &&
!(info.flags & GUI_INMENUMODE) && info.hwndFocus == combo) {
if (event == EVENT_OBJECT_FOCUS && window == combo)
present_standard_focus(combo);
else
poll_control_focus(event == EVENT_OBJECT_NAMECHANGE || event == EVENT_OBJECT_VALUECHANGE);
}
return;
}
// List boxes can omit selection WinEvents just like list views. Route all
// their events through the foreground poll, keeping one source and avoiding
// announcements of background selection changes.
if (is_list_box(window)) {
poll_control_focus(event == EVENT_OBJECT_NAMECHANGE || event == EVENT_OBJECT_VALUECHANGE);
return;
}
// Wine's MSAA provider exposes native list views as named clients, without
// their rows. Use the standard-control query even when that name is present.
if (is_list_view(window) || is_native_item_control(window)) {
if (event == EVENT_OBJECT_FOCUS) present_standard_focus(window);
else poll_control_focus(event == EVENT_OBJECT_NAMECHANGE || event == EVENT_OBJECT_VALUECHANGE);
return;
}
DWORD process = 0;
DWORD thread = GetWindowThreadProcessId(window, &process);
MsaaEvent request{window, event, process, thread, objectId, childId,
msaaGeneration, GetTickCount64() + 500};
if (event == EVENT_OBJECT_FOCUS) pendingMsaaFocus = request;
msaaEvents.submit(request);
}
bool current_msaa_source(const MsaaEvent &source) {
DWORD process = 0;
DWORD thread = GetWindowThreadProcessId(source.window, &process);
return source.generation == msaaGeneration && IsWindow(source.window) &&
process == source.process && thread == source.thread &&
msaa_event_in_foreground(source.window);
}
void present_msaa_result(const MsaaResult &result) {
const auto &source = result.source;
if (!current_msaa_source(source) || GetTickCount64() >= source.deadline) return;
if (source.event == EVENT_OBJECT_FOCUS && pendingMsaaFocus.generation == source.generation)
pendingMsaaFocus = {};
HWND window = source.window;
DWORD event = source.event;
LONG objectId = source.object, childId = source.child;
if (!result.available) {
if (event == EVENT_OBJECT_FOCUS) present_standard_focus(window);
return;
}
const auto &name = result.name, &value = result.value, &description = result.description;
const auto &roleText = result.role, &stateText = result.states;
bool password = result.password, nativeClient = result.native_client;
LONG childCount = result.count;
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 = password ? "" : value;
control.states = stateText;
control.description = description;
control.count = static_cast<int>(childCount);
// Wine can return named generic clients even for standard buttons and
// combo boxes. Their names alone must not hide the native role/value.
// Application-owned list items and other meaningful MSAA roles stay intact.
if (nativeClient) {
const auto native = describe_standard_control(window);
if (!native.role.empty()) {
control = native;
if (control.name.empty()) control.name = name;
if (control.states.empty()) control.states = stateText;
if (control.description.empty()) control.description = description;
if (password) control.value.clear();
}
}
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 reader_is_available() ? ERROR_SUCCESS : ERROR_SERVICE_NOT_ACTIVE;
}
extern "C" error_status_t __stdcall speakText(const wchar_t *text) {
if (!controller_text_fits(text)) return ERROR_INVALID_PARAMETER;
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) {
if (!controller_text_fits(text)) return ERROR_INVALID_PARAMETER;
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) {
if (!controller_text_fits(ssml)) return ERROR_INVALID_PARAMETER;
const std::string utf8 = to_utf8(ssml);
GError *error = nullptr;
GVariant *reply = g_dbus_connection_call_sync(
connection, readerEndpoint.busName, readerEndpoint.objectPath,
readerEndpoint.interfaceName, "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();
int argumentCount = 0;
wchar_t **arguments = CommandLineToArgvW(commandLine, &argumentCount);
if (arguments == nullptr) return 1;
// Version queries must work without a reader, even while another helper owns
// the singleton. The identity is embedded in this executable's sidecar.
for (int index = 1; index < argumentCount; ++index) {
if (lstrcmpW(arguments[index], L"--version") == 0) {
std::puts(WINE_ACCESS_BUILD);
LocalFree(arguments);
return 0;
}
}
std::fprintf(stderr, "wine-access: identity {\"version\":\"%s\",\"revision\":\"%s\",\"modified\":%s}\n",
WINE_ACCESS_VERSION, WINE_ACCESS_REVISION, WINE_ACCESS_MODIFIED);
bool validReader = true;
bool validEndpoint = true;
std::string readerPath, readerInterface;
bool dialogReaderEnabled = true, controllerEnabled = true;
for (int index = 1; index < argumentCount; ++index) {
if (lstrcmpW(arguments[index], L"--no-dialog-reader") == 0) {
dialogReaderEnabled = false;
continue;
}
if (lstrcmpW(arguments[index], L"--no-controller") == 0) {
controllerEnabled = false;
continue;
}
const bool pathOption = lstrcmpW(arguments[index], L"--reader-path") == 0;
const bool interfaceOption = lstrcmpW(arguments[index], L"--reader-interface") == 0;
if (pathOption || interfaceOption) {
if (++index == argumentCount) { validEndpoint = false; break; }
std::string value = to_utf8(arguments[index]);
if (pathOption ? !g_variant_is_object_path(value.c_str()) :
!g_dbus_is_interface_name(value.c_str())) {
validEndpoint = false;
break;
}
if (pathOption) readerPath = value;
else readerInterface = value;
continue;
}
if (lstrcmpW(arguments[index], L"--reader") != 0) continue;
if (++index == argumentCount) { validReader = false; break; }
if (lstrcmpW(arguments[index], L"cthulhu") == 0)
readerEndpoint = CthulhuEndpoint;
else if (lstrcmpW(arguments[index], L"orca") == 0)
readerEndpoint = OrcaEndpoint;
else { validReader = false; break; }
}
LocalFree(arguments);
if (!validReader) {
std::fprintf(stderr, "wine-access: --reader requires cthulhu or orca\n");
return 1;
}
if (!validEndpoint) {
std::fprintf(stderr, "wine-access: --reader-path and --reader-interface require valid D-Bus values\n");
return 1;
}
// Apply overrides after reader selection, independent of argument order.
// These strings remain alive until the workers have stopped at shutdown.
if (!readerPath.empty()) readerEndpoint.objectPath = readerPath.c_str();
if (!readerInterface.empty()) readerEndpoint.interfaceName = readerInterface.c_str();
if (!dialogReaderEnabled && !controllerEnabled) {
std::fprintf(stderr, "wine-access: disabled by command-line options\n");
return 0;
}
HANDLE singleton =
CreateMutexW(nullptr, TRUE, L"Local\\cthulhu-wine-access-singleton");
if (singleton == nullptr) {
std::fprintf(stderr, "wine-access: singleton creation failed (error %lu)\n",
static_cast<unsigned long>(GetLastError()));
return 1;
}
if (GetLastError() == ERROR_ALREADY_EXISTS) {
std::fprintf(stderr, "wine-access: another helper already owns this Wine desktop\n");
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 || !reader_is_available()) {
std::fprintf(stderr, "wine-access: reader service %s is unavailable\n",
readerEndpoint.busName);
if (connection != nullptr) g_object_unref(connection);
CloseHandle(singleton);
return 1;
}
if (!reader_supports_protocol(controllerEnabled, dialogReaderEnabled)) {
g_object_unref(connection);
CloseHandle(singleton);
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;
if (dialogReaderEnabled && !readerEvents.start(connection, readerEndpoint)) {
std::fprintf(stderr, "wine-access: reader event worker unavailable\n");
CoUninitialize();
g_object_unref(connection);
CloseHandle(singleton);
return 1;
}
const bool reviewActive = reviewService.start(connection,reviewIdentities,readerEndpoint);
if (!reviewActive)
std::fprintf(stderr, "wine-access: review service unavailable\n");
const bool msaaActive = dialogReaderEnabled && msaaEvents.start();
if (dialogReaderEnabled && !msaaActive) {
std::fprintf(stderr, "wine-access: MSAA event worker unavailable\n");
}
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;
}
if (controllerEnabled && !controllerActive)
std::fprintf(stderr, "wine-access: controller endpoint unavailable\n");
HWINEVENTHOOK hook = nullptr;
WineAccessForegroundHooks foregroundHooks{};
HMODULE menuModule = nullptr;
HWND menuBridge = nullptr;
HANDLE menuHost32 = nullptr;
DWORD menuHookError = 0, inputHookError = 0;
if (dialogReaderEnabled) {
hook = SetWinEventHook(EVENT_MIN, EVENT_MAX, nullptr, winevent_callback, 0,
0, WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS);
if (hook == nullptr)
std::fprintf(stderr, "wine-access: WinEvent hook unavailable (error %lu)\n",
static_cast<unsigned long>(GetLastError()));
WNDCLASSW bridgeClass{};
bridgeClass.lpfnWndProc = menu_bridge_proc;
bridgeClass.hInstance = instance;
bridgeClass.lpszClassName = WINE_ACCESS_MENU_CLASS;
RegisterClassW(&bridgeClass);
menuBridge = CreateWindowExW(WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW,
WINE_ACCESS_MENU_CLASS, L"", 0, 0, 0, 0, 0,
nullptr, nullptr, instance, nullptr);
if (menuBridge == nullptr) {
menuHookError = inputHookError = GetLastError();
} else if ((menuModule = load_menu_hook()) == nullptr) {
menuHookError = inputHookError = GetLastError();
} else {
foregroundHooks.module = menuModule;
foregroundHooks.menu_callback = std::bit_cast<HOOKPROC>(GetProcAddress(menuModule, "WineAccessMenuHook"));
if (!foregroundHooks.menu_callback) menuHookError = GetLastError();
foregroundHooks.input_callback = std::bit_cast<HOOKPROC>(GetProcAddress(menuModule, "WineAccessInputHook"));
if (!foregroundHooks.input_callback) inputHookError = GetLastError();
editReaderActive = foregroundHooks.menu_callback && foregroundHooks.input_callback;
wine_access_update_hooks(&foregroundHooks, FALSE);
}
if (menuHookError)
std::fprintf(stderr, "wine-access: native menu hook unavailable (error %lu)\n",
static_cast<unsigned long>(menuHookError));
if (inputHookError)
std::fprintf(stderr, "wine-access: native input hook unavailable (error %lu)\n",
static_cast<unsigned long>(inputHookError));
if (menuBridge != nullptr) {
menuHost32 = start_menu_host32(menuBridge);
if (menuHost32 == nullptr)
std::fprintf(stderr, "wine-access: 32-bit menu host unavailable (error %lu)\n",
static_cast<unsigned long>(GetLastError()));
// Existing subclasses keep working after foreground hooks move away,
// including those attached by the 32-bit host.
editReaderActive = editReaderActive || menuHost32 != nullptr;
}
}
MSG message{};
// Native lists, tabs, trees and combo boxes can omit change WinEvents in Wine.
// Observe only the foreground control; never read background selections.
const UINT_PTR listTimer = dialogReaderEnabled ? SetTimer(nullptr, 0, 100, nullptr) : 0;
if (dialogReaderEnabled && !bootstrapperReader.start())
std::fprintf(stderr, "wine-access: bootstrapper UIA worker unavailable\n");
if (dialogReaderEnabled && !listTimer)
std::fprintf(stderr, "wine-access: control polling timer unavailable\n");
// Callback availability is distinct from foreground attachment. There may
// be no matching application when this startup snapshot is emitted.
std::fprintf(stderr, "wine-access: ready {\"reader\":\"%s\",\"controls_enabled\":%s,"
"\"controller\":%s,\"review\":%s,\"msaa_worker\":%s,\"win_events\":%s,"
"\"menu_hook\":%s,\"input_hook\":%s,"
"\"menu_hook_available\":%s,\"input_hook_available\":%s,"
"\"host32_started\":%s,\"control_poll\":%s}\n",
readerEndpoint.busName, dialogReaderEnabled ? "true" : "false",
controllerActive ? "true" : "false", reviewActive ? "true" : "false",
msaaActive ? "true" : "false", hook ? "true" : "false",
foregroundHooks.menu ? "true" : "false", foregroundHooks.input ? "true" : "false",
foregroundHooks.menu_callback ? "true" : "false",
foregroundHooks.input_callback ? "true" : "false",
menuHost32 ? "true" : "false", listTimer ? "true" : "false");
while (GetMessageW(&message, nullptr, 0, 0) > 0) {
if (listTimer && message.message == WM_TIMER && message.hwnd == nullptr &&
message.wParam == listTimer) {
MsaaResult result;
for (unsigned handled = 0; handled < 32 && msaaEvents.take(result); ++handled)
present_msaa_result(result);
if (pendingMsaaFocus.window && GetTickCount64() >= pendingMsaaFocus.deadline) {
const auto source = pendingMsaaFocus;
pendingMsaaFocus = {};
if (current_msaa_source(source)) present_standard_focus(source.window);
}
restore_virtual_desktop_focus();
wine_access_update_hooks(&foregroundHooks, FALSE);
poll_control_focus();
reviewService.focus_changed(reviewIdentities.observe_foreground());
continue;
}
TranslateMessage(&message);
DispatchMessageW(&message);
}
if (listTimer) KillTimer(nullptr, listTimer);
readerEvents.stop();
msaaEvents.stop();
bootstrapperReader.stop();
reviewService.stop();
if (hook != nullptr)
UnhookWinEvent(hook);
wine_access_release_hooks(&foregroundHooks);
if (menuBridge != nullptr)
DestroyWindow(menuBridge);
if (menuHost32 != nullptr) {
// Destroying the bridge ends the host's loop. Parent-process death also
// stops it, so a reader crash cannot leave an orphaned foreground hook.
if (WaitForSingleObject(menuHost32, 1000) == WAIT_TIMEOUT) {
TerminateProcess(menuHost32, 0);
WaitForSingleObject(menuHost32, 1000);
}
CloseHandle(menuHost32);
}
if (menuModule != nullptr)
FreeLibrary(menuModule);
if (controllerActive) {
reset_rpc_server();
}
if (window != nullptr)
DestroyWindow(window);
CoUninitialize();
g_object_unref(connection);
CloseHandle(singleton);
return 0;
}