More accessibility fixes using foobar2000 as a test.

This commit is contained in:
Storm Dragon committed 2026-10-10 15:30:23 -04:00
1 parent 22f50402c4
commit 5f78168c57
9 files changed
+407 -14

No files matched your search

+65 -10
View File
@@ -6,6 +6,7 @@
#include "msaa_states.h"
#include "list_view.h"
#include "control_class.h"
#include "native_items.h"
#include <commctrl.h>
#include <gio/gio.h>
@@ -37,6 +38,8 @@ HWND lastListControl = nullptr;
int lastListIndex = -1;
int lastListCount = 0;
bool lastListSelected = false;
HWND lastNativeControl = nullptr;
NativeItemKey lastNativeItem;
// 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.
@@ -163,11 +166,16 @@ void find_control_label(HWND window, wchar_t *text, int capacity) {
HWND previous = window;
wchar_t className[64] = {};
text[0] = L'\0';
// Managed controls need not be created in label/control order. Prefer a
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 (GetClassNameW(window, className, std::size(className)) &&
lstrcmpiW(className, L"Edit") != 0 && wine_access_class_matches(className, L"Edit")) {
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;
@@ -188,9 +196,11 @@ void find_control_label(HWND window, wchar_t *text, int capacity) {
}
if (label && get_control_text(label, text, capacity)) {
tidy_text(text, true);
return;
}
}
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);
@@ -317,6 +327,18 @@ ControlDescription describe_standard_control(HWND window) {
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;
lastNativeControl = window;
lastNativeItem = item.key;
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));
@@ -384,10 +406,28 @@ bool present_standard_focus(HWND window) {
control, window_title(window));
}
void poll_list_focus(bool refresh = false) {
void poll_control_focus(bool refresh = false) {
GUITHREADINFO info{};
info.cbSize = sizeof(info);
HWND foreground = GetForegroundWindow();
if (foreground && GetGUIThreadInfo(GetWindowThreadProcessId(foreground, nullptr), &info) &&
!(info.flags & GUI_INMENUMODE) && is_native_item_control(info.hwndFocus)) {
lastListControl = nullptr;
lastListIndex = -1;
lastListCount = 0;
lastListSelected = false;
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 (foreground && GetGUIThreadInfo(GetWindowThreadProcessId(foreground, nullptr), &info) &&
!(info.flags & GUI_INMENUMODE) && is_list_view(info.hwndFocus)) {
int count = 0;
@@ -587,9 +627,9 @@ void CALLBACK winevent_callback(HWINEVENTHOOK, DWORD event, HWND window,
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)) {
if (is_list_view(window) || is_native_item_control(window)) {
if (event == EVENT_OBJECT_FOCUS) present_standard_focus(window);
else poll_list_focus(event == EVENT_OBJECT_NAMECHANGE || event == EVENT_OBJECT_VALUECHANGE);
else poll_control_focus(event == EVENT_OBJECT_NAMECHANGE || event == EVENT_OBJECT_VALUECHANGE);
return;
}
IAccessible *accessible = nullptr;
@@ -628,6 +668,8 @@ void CALLBACK winevent_callback(HWINEVENTHOOK, DWORD event, HWND window,
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);
@@ -667,6 +709,19 @@ void CALLBACK winevent_callback(HWINEVENTHOOK, DWORD event, HWND window,
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);
}
@@ -865,13 +920,13 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) {
static_cast<unsigned long>(GetLastError()));
}
MSG message{};
// Native list views also omit row-selection WinEvents in Wine. Observe the
// focused row with scalar queries; retrieve its text only when it changes.
// Native lists, tabs and trees can omit selection WinEvents in Wine. Observe
// the focused item with scalar queries; retrieve text only when it changes.
const UINT_PTR listTimer = dialogReaderEnabled ? SetTimer(nullptr, 0, 100, nullptr) : 0;
while (GetMessageW(&message, nullptr, 0, 0) > 0) {
if (listTimer && message.message == WM_TIMER && message.hwnd == nullptr &&
message.wParam == listTimer) {
poll_list_focus();
poll_control_focus();
continue;
}
TranslateMessage(&message);