Some lists were not auto reading results. Hopefully fixed.

This commit is contained in:
Storm Dragon committed 2026-10-12 04:55:31 -04:00
1 parent 0f59809e9a
commit d9aa40e16c
6 files changed
+151 -4

No files matched your search

+5
View File
@@ -236,6 +236,11 @@ when Wine omits selection events. Up to eight columns and 1023 UTF-16 units
per column are read. These queries do not add column navigation or routable per column are read. These queries do not add column navigation or routable
object review. object review.
Standard `ListBox` controls report the selected item's text and position when
Up/Down changes the selection, including AudioQuake's map chooser. The same
foreground poll detects changes when Wine omits selection events, with one
source identity for focus and selection. Background lists remain silent.
Native tab controls report their focused caption and position; tree controls Native tab controls report their focused caption and position; tree controls
report the selected node and its expansion state. A bounded focus poll follows report the selected node and its expansion state. A bounded focus poll follows
navigation when Wine omits selection events. These queries work with 32-bit navigation when Wine omits selection events. These queries work with 32-bit
@@ -1,6 +1,6 @@
# Maintainer: Storm Dragon <storm_dragon@stormux.org> # Maintainer: Storm Dragon <storm_dragon@stormux.org>
pkgname=wine-access-git pkgname=wine-access-git
pkgver=r22.gf40e471 pkgver=r24.g0f59809
pkgrel=1 pkgrel=1
pkgdesc='Wine accessibility helper with MSAA, NVDA Controller, and Tolk compatibility for Cthulhu' pkgdesc='Wine accessibility helper with MSAA, NVDA Controller, and Tolk compatibility for Cthulhu'
arch=('x86_64') arch=('x86_64')
+45
View File
@@ -43,11 +43,21 @@ int lastListIndex = -1;
int lastListCount = 0; int lastListCount = 0;
bool lastListSelected = false; bool lastListSelected = false;
int lastListChecked = -1; int lastListChecked = -1;
HWND lastListBoxControl = nullptr;
int lastListBoxIndex = -1;
int lastListBoxCount = 0;
HWND lastNativeControl = nullptr; HWND lastNativeControl = nullptr;
NativeItemKey lastNativeItem; NativeItemKey lastNativeItem;
HWND lastComboControl = nullptr; HWND lastComboControl = nullptr;
std::string lastComboValue; 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) { bool is_combo_box(HWND window) {
wchar_t className[128] = {}; wchar_t className[128] = {};
return GetClassNameW(window, className, std::size(className)) && return GetClassNameW(window, className, std::size(className)) &&
@@ -418,6 +428,9 @@ ControlDescription describe_standard_control(HWND window) {
} }
if (static_cast<LONG_PTR>(count) >= 0) if (static_cast<LONG_PTR>(count) >= 0)
control.count = static_cast<int>(count); control.count = static_cast<int>(count);
lastListBoxControl = window;
lastListBoxIndex = static_cast<int>(selected);
lastListBoxCount = control.count;
return control; return control;
} }
@@ -462,6 +475,12 @@ void poll_control_focus(bool refresh = false) {
const bool focused = foreground && const bool focused = foreground &&
GetGUIThreadInfo(GetWindowThreadProcessId(foreground, nullptr), &info) && GetGUIThreadInfo(GetWindowThreadProcessId(foreground, nullptr), &info) &&
!(info.flags & GUI_INMENUMODE); !(info.flags & GUI_INMENUMODE);
const bool focusedListBox = focused && is_list_box(info.hwndFocus);
if (!focusedListBox) {
lastListBoxControl = nullptr;
lastListBoxIndex = -1;
lastListBoxCount = 0;
}
if (!focused || !is_combo_box(info.hwndFocus)) { if (!focused || !is_combo_box(info.hwndFocus)) {
lastComboControl = nullptr; lastComboControl = nullptr;
lastComboValue.clear(); lastComboValue.clear();
@@ -508,6 +527,25 @@ void poll_control_focus(bool refresh = false) {
lastListCount = 0; lastListCount = 0;
lastListSelected = false; lastListSelected = false;
lastListChecked = -1; 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 (focused && is_combo_box(info.hwndFocus)) {
if (info.hwndFocus != lastComboControl) { if (info.hwndFocus != lastComboControl) {
present_standard_focus(info.hwndFocus); present_standard_focus(info.hwndFocus);
@@ -741,6 +779,13 @@ void CALLBACK winevent_callback(HWINEVENTHOOK, DWORD event, HWND window,
} }
return; 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 // 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. // their rows. Use the standard-control query even when that name is present.
if (is_list_view(window) || is_native_item_control(window)) { if (is_list_view(window) || is_native_item_control(window)) {
+17
View File
@@ -31,6 +31,23 @@ characters, selections, deletion, protected fields and keypad review with live
speech and braille. When an Orca integration exists, repeat those workflows speech and braille. When an Orca integration exists, repeat those workflows
with `--reader orca`, including return to Linux controls and reader restart. with `--reader orca`, including return to Linux controls and reader restart.
## List-box navigation
For list-box navigation, run these checks sequentially:
```sh
xvfb-run -a python3 tests/menu_integration.py --build-dir build --fixture listbox
xvfb-run -a python3 tests/menu_integration.py --build-dir build --fixture listbox --32-bit
```
They cover native Up/Down navigation, Unicode text, positions, focus return,
duplicate suppression, background changes and an emptied list. After installing
the helper and restarting the reader, open AudioQuake's LDL tutorial/example map
chooser and use Up/Down without leaving the list. Check that speech and braille
follow each map and position, then Tab to OK/Cancel and Shift+Tab back. Recheck
combo-box arrows, ordinary list views, tabs/trees and return from Wine menus.
Recorded events alone do not establish audible speech or physical braille.
## Virtual desktop focus recovery ## Virtual desktop focus recovery
`xvfb-run -a ctest --test-dir build -R foreground-focus --output-on-failure` `xvfb-run -a ctest --test-dir build -R foreground-focus --output-on-failure`
+62
View File
@@ -0,0 +1,62 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
#include <windows.h>
static HWND list, button;
static int step;
static LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wp, LPARAM lp) {
if (message == WM_TIMER) {
switch (++step) {
case 1: case 2: SendMessageW(list, WM_KEYDOWN, VK_DOWN, 1); break;
case 3: SendMessageW(list, WM_KEYDOWN, VK_UP, 1); break;
case 4: SetFocus(button); break;
case 5:
// A background change must not be announced or steal focus.
SendMessageW(list, LB_INSERTSTRING, 0, (LPARAM)L"Background-only map");
SendMessageW(list, LB_SETCURSEL, 0, 0);
NotifyWinEvent(EVENT_OBJECT_VALUECHANGE, list, OBJID_CLIENT, CHILDID_SELF);
break;
case 6:
if (GetFocus() != button) return 0;
SendMessageW(list, LB_DELETESTRING, 0, 0);
SendMessageW(list, LB_SETCURSEL, 0, 0);
SetFocus(list);
break;
case 7: SendMessageW(list, LB_RESETCONTENT, 0, 0); break;
default: DestroyWindow(window);
}
return 0;
}
if (message == WM_DESTROY) { PostQuitMessage(0); return 0; }
return DefWindowProcW(window, message, wp, lp);
}
int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR args, int show) {
(void)previous; (void)args; (void)show;
WNDCLASSW cls = {0};
cls.hInstance = instance;
cls.lpszClassName = L"WineAccessListBoxFixture";
cls.lpfnWndProc = window_proc;
RegisterClassW(&cls);
HWND window = CreateWindowW(cls.lpszClassName, L"Select map", WS_OVERLAPPEDWINDOW,
120, 120, 600, 300, NULL, NULL, instance, NULL);
CreateWindowW(L"Static", L"Tutorial maps", WS_CHILD | WS_VISIBLE,
10, 10, 550, 30, window, NULL, instance, NULL);
list = CreateWindowW(L"ListBox", L"", WS_CHILD | WS_VISIBLE | WS_TABSTOP | LBS_NOTIFY,
10, 50, 550, 160, window, NULL, instance, NULL);
button = CreateWindowW(L"Button", L"OK", WS_CHILD | WS_VISIBLE | WS_TABSTOP,
10, 220, 100, 30, window, NULL, instance, NULL);
SendMessageW(list, LB_ADDSTRING, 0, (LPARAM)L"First map");
SendMessageW(list, LB_ADDSTRING, 0, (LPARAM)L"Second map β");
SendMessageW(list, LB_ADDSTRING, 0, (LPARAM)L"Third map");
SendMessageW(list, LB_SETCURSEL, 0, 0);
ShowWindow(window, SW_SHOW);
SetForegroundWindow(window);
SetFocus(list);
SetTimer(window, 1, 500, NULL);
MSG message;
while (GetMessageW(&message, NULL, 0, 0) > 0) {
TranslateMessage(&message); DispatchMessageW(&message);
}
return 0;
}
+21 -3
View File
@@ -18,15 +18,15 @@ parser = argparse.ArgumentParser()
parser.add_argument("--build-dir", default="build") parser.add_argument("--build-dir", default="build")
parser.add_argument("--session", action="store_true") parser.add_argument("--session", action="store_true")
parser.add_argument("--reader", choices=("cthulhu", "orca")) parser.add_argument("--reader", choices=("cthulhu", "orca"))
parser.add_argument("--fixture", choices=("menu", "edit", "notepad", "list", "delphi", "winforms", "controls", "scintilla", "goldwave"), default="menu") parser.add_argument("--fixture", choices=("menu", "edit", "notepad", "list", "listbox", "delphi", "winforms", "controls", "scintilla", "goldwave"), default="menu")
parser.add_argument("--notepadpp-exe", type=Path) parser.add_argument("--notepadpp-exe", type=Path)
parser.add_argument("--goldwave-exe", type=Path) parser.add_argument("--goldwave-exe", type=Path)
parser.add_argument("--32-bit", dest="is32Bit", action="store_true") parser.add_argument("--32-bit", dest="is32Bit", action="store_true")
parser.add_argument("--late-helper", action="store_true") parser.add_argument("--late-helper", action="store_true")
parser.add_argument("--restart-helper", action="store_true") parser.add_argument("--restart-helper", action="store_true")
args = parser.parse_args() args = parser.parse_args()
if args.is32Bit and args.fixture not in ("menu", "edit", "winforms", "list", "delphi", "controls"): if args.is32Bit and args.fixture not in ("menu", "edit", "winforms", "list", "listbox", "delphi", "controls"):
parser.error("32-bit checks currently require --fixture menu, edit, winforms, list, delphi or controls") parser.error("32-bit checks currently require --fixture menu, edit, winforms, list, listbox, delphi or controls")
if (args.late_helper or args.restart_helper) and args.fixture not in ("edit", "winforms", "scintilla"): if (args.late_helper or args.restart_helper) and args.fixture not in ("edit", "winforms", "scintilla"):
parser.error("helper lifecycle checks require --fixture edit, winforms or scintilla") parser.error("helper lifecycle checks require --fixture edit, winforms or scintilla")
if args.fixture == "scintilla" and (not args.notepadpp_exe or not args.notepadpp_exe.is_file()): if args.fixture == "scintilla" and (not args.notepadpp_exe or not args.notepadpp_exe.is_file()):
@@ -203,6 +203,24 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
assert not any(e[2] == "List1" for e in events), "Delphi generic client leaked" assert not any(e[2] == "List1" for e in events), "Delphi generic client leaked"
print("PASS: Delphi list labels, rows, keyboard navigation, checkbox toggles, return, buttons and empty state") print("PASS: Delphi list labels, rows, keyboard navigation, checkbox toggles, return, buttons and empty state")
sys.exit(0) sys.exit(0)
if args.fixture == "listbox":
listEvents = [e for e in events if e[3] == "list"]
for event in listEvents:
print(event[1:9])
assert any(e[1] == "focus" and e[2] == "Tutorial maps" and
e[4] == "First map" and e[7:9] == (1, 3)
for e in listEvents), "initial map or label missing"
selections = [(e[4], e[7], e[8]) for e in listEvents if e[1] == "selection"]
assert selections == [("Second map β", 2, 3), ("Third map", 3, 3),
("Second map β", 2, 3), ("", 0, 0)], \
f"map navigation/empty state missing or duplicated: {selections}"
assert sum(e[1] == "focus" and e[4] == "First map"
for e in listEvents) == 2, "return to map list missing or duplicated"
assert any(e[1] == "focus" and e[2] == "OK" for e in events), "button focus missing"
assert not any("Background-only" in str(e) for e in events), "background map leaked"
assert len({e[0] for e in listEvents}) == 1, "map events changed source identity"
print("PASS: list-box arrows, Unicode, positions, focus return, background exclusion and empty state")
sys.exit(0)
if args.fixture == "list": if args.fixture == "list":
listEvents = [e for e in events if e[3] == "list"] listEvents = [e for e in events if e[3] == "list"]
for event in listEvents: for event in listEvents: