Merge branch 'deedra'

Resolve tests/README.md by retaining both UIA notification and MSAA/reader reply stall guidance. Preserve master's notification hooks and Deedra's responsiveness fixes.

Validation: CMake configure/build, all four CTest checks, nine stall integration variants, 64-bit and 32-bit UIA notification checks, menus, 64-bit and 32-bit editing, actual Wine Notepad, and git diff --check. Live speech and physical braille acceptance remain pending.
This commit is contained in:
Storm Dragon committed 2026-10-13 19:34:31 -04:00
commit 95cb78d3e4
12 files changed
+984 -67

No files matched your search

+66 -66
View File
@@ -10,6 +10,8 @@
#include "review_service.h"
#include "foreground_focus.h"
#include "reader_endpoint.h"
#include "msaa_events.h"
#include "reader_events.h"
#include <commctrl.h>
#include <gio/gio.h>
@@ -33,6 +35,10 @@ constexpr int DBUS_TIMEOUT_MS = 500;
GDBusConnection *connection = nullptr;
ReviewIdentityStore reviewIdentities;
ReviewService reviewService;
MsaaEventReader msaaEvents;
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;
@@ -156,31 +162,6 @@ bool reader_is_available() {
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 uses the selected reader's D-Bus interface.
void strip_mnemonics(wchar_t *text) {
@@ -289,13 +270,12 @@ bool present_accessible_event(const std::string &sourceId,
// 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 call_boolean("PresentAccessibleEvent",
g_variant_new(
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())) == ERROR_SUCCESS;
control.count, windowTitle.c_str()));
}
ControlDescription describe_standard_control(HWND window) {
@@ -785,6 +765,14 @@ HANDLE start_menu_host32(HWND bridge) {
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) {
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)
@@ -824,46 +812,38 @@ void CALLBACK winevent_callback(HWINEVENTHOOK, DWORD event, HWND window,
else poll_control_focus(event == EVENT_OBJECT_NAMECHANGE || event == EVENT_OBJECT_VALUECHANGE);
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);
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 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;
LONG roleId = 0;
if (SUCCEEDED(accessible->get_accRole(child, &role)))
roleText = variant_text(role);
if (role.vt == VT_I4) roleId = role.lVal;
VariantClear(&role);
VARIANT state{};
VariantInit(&state);
std::string stateText;
bool password = false;
if (SUCCEEDED(accessible->get_accState(child, &state)) && state.vt == VT_I4) {
stateText = msaa_states(static_cast<DWORD>(state.lVal), roleId);
password = state.lVal & STATE_SYSTEM_PROTECTED;
}
VariantClear(&state);
LONG childCount = 0;
accessible->get_accChildCount(&childCount);
const bool nativeClient = child.vt == VT_I4 && child.lVal == CHILDID_SELF &&
(roleText.empty() || roleId == ROLE_SYSTEM_CLIENT);
accessible->Release();
VariantClear(&child);
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") &&
@@ -1083,7 +1063,17 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) {
}
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;
}
reviewService.start(connection,reviewIdentities,readerEndpoint);
if (dialogReaderEnabled && !msaaEvents.start()) {
std::fprintf(stderr, "wine-access: MSAA event worker unavailable\n");
}
bool controllerActive = false;
if (controllerEnabled &&
FindWindowW(L"wxWindowClassNR", L"NVDA") == nullptr) {
@@ -1148,6 +1138,14 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) {
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();
poll_control_focus();
reviewService.focus_changed(reviewIdentities.observe_foreground());
@@ -1157,6 +1155,8 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) {
DispatchMessageW(&message);
}
if (listTimer) KillTimer(nullptr, listTimer);
readerEvents.stop();
msaaEvents.stop();
reviewService.stop();
if (hook != nullptr)
UnhookWinEvent(hook);