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:
commit
95cb78d3e4
12 files changed
+984
-67
No files matched your search
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user