Finish up edit field accessibility.

This commit is contained in:
ak47 committed 2026-10-10 00:18:22 -04:00
1 parent 6f6d4040c4
commit c5657ee5d4
9 files changed
+361 -39

No files matched your search

+36 -9
View File
@@ -2,6 +2,7 @@
#include "nvdaController.h"
#include "menu_bridge.h"
#include "edit_hook.h"
#include <commctrl.h>
#include <gio/gio.h>
@@ -28,6 +29,7 @@ std::deque<WineAccessTextEvent> textEvents;
bool editReaderActive = false;
std::string lastMenuEvent;
ULONGLONG lastMenuOwner = 0;
HWND lastTextControl = nullptr;
// Winegcc uses 16-bit wchar_t; Linux libc's wcs* functions expect 32-bit
// wchar_t. Use Wine's Unicode APIs or explicit code-unit comparisons.
@@ -183,6 +185,9 @@ 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 call_boolean("PresentAccessibleEvent",
g_variant_new(
"(sssssssiis)", sourceId.c_str(), eventType,
@@ -199,7 +204,13 @@ ControlDescription describe_standard_control(HWND window) {
wchar_t label[256] = {};
if (!GetClassNameW(window, className, std::size(className)))
return control;
get_control_text(window, text, std::size(text));
const LONG_PTR windowStyle = GetWindowLongPtrW(window, GWL_STYLE);
const bool isEdit = lstrcmpW(className, L"Edit") == 0 ||
lstrcmpW(className, L"RICHEDIT50W") == 0 ||
lstrcmpW(className, L"RichEdit20W") == 0 ||
lstrcmpW(className, L"RichEdit20A") == 0;
if (!isEdit || !(windowStyle & ES_PASSWORD))
get_control_text(window, text, std::size(text));
if (lstrcmpW(className, L"Button") == 0) {
control.name = to_utf8(text);
@@ -233,10 +244,6 @@ ControlDescription describe_standard_control(HWND window) {
return control;
}
const bool isEdit = lstrcmpW(className, L"Edit") == 0 ||
lstrcmpW(className, L"RICHEDIT50W") == 0 ||
lstrcmpW(className, L"RichEdit20W") == 0 ||
lstrcmpW(className, L"RichEdit20A") == 0;
if (isEdit) {
find_control_label(window, label, std::size(label));
control.name = to_utf8(label);
@@ -313,6 +320,10 @@ ControlDescription describe_standard_control(HWND window) {
}
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())
@@ -370,6 +381,9 @@ void present_text_event(const WineAccessTextEvent &event) {
if (owner == nullptr || owner != GetAncestor(GetForegroundWindow(), GA_ROOT) ||
!GetGUIThreadInfo(GetWindowThreadProcessId(owner, nullptr), &info) || info.hwndFocus != window)
return;
if ((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) {
@@ -397,7 +411,22 @@ void present_text_event(const WineAccessTextEvent &event) {
control.name = text.empty() ? "blank" : text;
}
}
present_accessible_event(source_id(window, "text"), "focus", control, to_utf8(event.title));
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 = GetWindowLongPtrW(window, GWL_STYLE) & ES_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);
// 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) {
@@ -471,9 +500,7 @@ void CALLBACK winevent_callback(HWINEVENTHOOK, DWORD event, HWND window,
event != EVENT_OBJECT_VALUECHANGE && event != EVENT_OBJECT_SELECTION &&
event != EVENT_OBJECT_STATECHANGE)
return;
wchar_t className[64] = {};
if (editReaderActive && GetClassNameW(window, className, std::size(className)) &&
lstrcmpW(className, L"Edit") == 0)
if (editReaderActive && GetPropW(window, EDIT_STATE_PROPERTY))
return;
IAccessible *accessible = nullptr;
VARIANT child{};