Some lists were not auto reading results. Hopefully fixed.
This commit is contained in:
1 parent
0f59809e9a
commit
d9aa40e16c
6 files changed
+151
-4
No files matched your search
@@ -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
|
||||
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
|
||||
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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Maintainer: Storm Dragon <storm_dragon@stormux.org>
|
||||
pkgname=wine-access-git
|
||||
pkgver=r22.gf40e471
|
||||
pkgver=r24.g0f59809
|
||||
pkgrel=1
|
||||
pkgdesc='Wine accessibility helper with MSAA, NVDA Controller, and Tolk compatibility for Cthulhu'
|
||||
arch=('x86_64')
|
||||
|
||||
@@ -43,11 +43,21 @@ int lastListIndex = -1;
|
||||
int lastListCount = 0;
|
||||
bool lastListSelected = false;
|
||||
int lastListChecked = -1;
|
||||
HWND lastListBoxControl = nullptr;
|
||||
int lastListBoxIndex = -1;
|
||||
int lastListBoxCount = 0;
|
||||
HWND lastNativeControl = nullptr;
|
||||
NativeItemKey lastNativeItem;
|
||||
HWND lastComboControl = nullptr;
|
||||
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) {
|
||||
wchar_t className[128] = {};
|
||||
return GetClassNameW(window, className, std::size(className)) &&
|
||||
@@ -418,6 +428,9 @@ ControlDescription describe_standard_control(HWND window) {
|
||||
}
|
||||
if (static_cast<LONG_PTR>(count) >= 0)
|
||||
control.count = static_cast<int>(count);
|
||||
lastListBoxControl = window;
|
||||
lastListBoxIndex = static_cast<int>(selected);
|
||||
lastListBoxCount = control.count;
|
||||
return control;
|
||||
}
|
||||
|
||||
@@ -462,6 +475,12 @@ void poll_control_focus(bool refresh = false) {
|
||||
const bool focused = foreground &&
|
||||
GetGUIThreadInfo(GetWindowThreadProcessId(foreground, nullptr), &info) &&
|
||||
!(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)) {
|
||||
lastComboControl = nullptr;
|
||||
lastComboValue.clear();
|
||||
@@ -508,6 +527,25 @@ void poll_control_focus(bool refresh = false) {
|
||||
lastListCount = 0;
|
||||
lastListSelected = false;
|
||||
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 (info.hwndFocus != lastComboControl) {
|
||||
present_standard_focus(info.hwndFocus);
|
||||
@@ -741,6 +779,13 @@ void CALLBACK winevent_callback(HWINEVENTHOOK, DWORD event, HWND window,
|
||||
}
|
||||
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
|
||||
// their rows. Use the standard-control query even when that name is present.
|
||||
if (is_list_view(window) || is_native_item_control(window)) {
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
`xvfb-run -a ctest --test-dir build -R foreground-focus --output-on-failure`
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -18,15 +18,15 @@ parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--build-dir", default="build")
|
||||
parser.add_argument("--session", action="store_true")
|
||||
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("--goldwave-exe", type=Path)
|
||||
parser.add_argument("--32-bit", dest="is32Bit", action="store_true")
|
||||
parser.add_argument("--late-helper", action="store_true")
|
||||
parser.add_argument("--restart-helper", action="store_true")
|
||||
args = parser.parse_args()
|
||||
if args.is32Bit and args.fixture not in ("menu", "edit", "winforms", "list", "delphi", "controls"):
|
||||
parser.error("32-bit checks currently require --fixture menu, edit, winforms, list, delphi or 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, listbox, delphi or controls")
|
||||
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")
|
||||
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"
|
||||
print("PASS: Delphi list labels, rows, keyboard navigation, checkbox toggles, return, buttons and empty state")
|
||||
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":
|
||||
listEvents = [e for e in events if e[3] == "list"]
|
||||
for event in listEvents:
|
||||
|
||||
Reference in new issue
Block a user