Read native Wine list-view rows in 32-bit and 64-bit applications

This commit is contained in:
Storm Dragon committed 2026-10-10 02:03:56 -04:00
1 parent 9aad6748e0
commit d7ea005a1f
8 files changed
+356 -4

No files matched your search

+26
View File
@@ -73,6 +73,32 @@ reader's `docs/testing/wine-accessibility.md` for current evidence and live
acceptance gates. Run Xvfb fixtures sequentially; concurrent automatic display
allocation can collide.
## List-view checks
```sh
xvfb-run -a python3 tests/menu_integration.py --build-dir build --fixture list
xvfb-run -a python3 tests/menu_integration.py --build-dir build --fixture list --32-bit
```
The second command requires `i686-w64-mingw32-gcc`. Both use a private prefix
and recording D-Bus service. They verify first-row reading without selecting
it, two columns, Unicode and literal ampersands, Down/Up, button/list focus
return, changed row text on a value event, and deletion of all rows without
stale output. Run them sequentially. Both architecture fixtures passed on
October 10, as did the existing menu, editor, actual Notepad and CTest MSAA
state checks. The reader's focused Wine race checks also passed.
The BARD Express bootstrapper was inspected in place on October 10. Its
32-bit `SysListView32` has one row: `Pending, Microsoft .NET Framework 4.7.2 Full`.
The new query code retrieved that row without changing the installer. This
does not establish audible output or successful prerequisite installation.
After installing the helper and restarting Cthulhu, Tab to the prerequisite
list: expect the instructions, list role, pending component and `1 of 1`.
Check arrows and Tab/Shift+Tab back to Install/Cancel. Also check a multirow
native list, normal menus, editor navigation, switching to a native Linux
application, and reader shutdown. Verify speech and physical braille separately.
## Live Notepad acceptance
Install all three helper files together and use the matching Cthulhu-Go source.
+87
View File
@@ -0,0 +1,87 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
#define UNICODE
#define _UNICODE
#include <windows.h>
#include <commctrl.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:
// Exercise keyboard processing, not synthetic accessibility events.
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: SetFocus(list); break;
case 6: {
LVITEMW item = {0};
item.pszText = L"Ready";
SendMessageW(list, LVM_SETITEMTEXTW, 0, (LPARAM)&item);
NotifyWinEvent(EVENT_OBJECT_VALUECHANGE, list, OBJID_CLIENT, CHILDID_SELF);
break;
}
case 7: SendMessageW(list, LVM_DELETEALLITEMS, 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;
INITCOMMONCONTROLSEX controls = {sizeof(controls), ICC_LISTVIEW_CLASSES};
InitCommonControlsEx(&controls);
WNDCLASSW cls = {0};
cls.hInstance = instance;
cls.lpszClassName = L"WineAccessListFixture";
cls.lpfnWndProc = window_proc;
RegisterClassW(&cls);
HWND window = CreateWindowW(cls.lpszClassName, L"Wine list fixture", WS_OVERLAPPEDWINDOW,
120, 120, 600, 300, NULL, NULL, instance, NULL);
button = CreateWindowW(L"Button", L"Return to control", WS_CHILD | WS_VISIBLE | WS_TABSTOP,
10, 240, 200, 30, window, NULL, instance, NULL);
CreateWindowW(L"Static", L"Required components", WS_CHILD | WS_VISIBLE,
10, 10, 550, 30, window, NULL, instance, NULL);
list = CreateWindowW(WC_LISTVIEWW, L"List1", WS_CHILD | WS_VISIBLE | WS_TABSTOP | LVS_REPORT,
10, 50, 550, 180, window, NULL, instance, NULL);
LVCOLUMNW column = {0};
column.mask = LVCF_TEXT | LVCF_WIDTH;
column.cx = 150;
column.pszText = L"Status";
SendMessageW(list, LVM_INSERTCOLUMNW, 0, (LPARAM)&column);
column.cx = 350;
column.pszText = L"Component";
SendMessageW(list, LVM_INSERTCOLUMNW, 1, (LPARAM)&column);
LVITEMW item = {0};
item.mask = LVIF_TEXT;
item.pszText = L"Pending";
SendMessageW(list, LVM_INSERTITEMW, 0, (LPARAM)&item);
item.iSubItem = 1;
item.pszText = L"Runtime & tools β";
SendMessageW(list, LVM_SETITEMTEXTW, 0, (LPARAM)&item);
item.iItem = 1;
item.iSubItem = 0;
item.pszText = L"Installed";
SendMessageW(list, LVM_INSERTITEMW, 0, (LPARAM)&item);
item.iSubItem = 1;
item.pszText = L"Second component";
SendMessageW(list, LVM_SETITEMTEXTW, 1, (LPARAM)&item);
ShowWindow(window, SW_SHOW);
SetForegroundWindow(window);
// Match the installer: initially no row is selected or focused.
SetFocus(list);
SetTimer(window, 1, 500, NULL);
MSG message;
while (GetMessageW(&message, NULL, 0, 0) > 0) {
TranslateMessage(&message); DispatchMessageW(&message);
}
return 0;
}
+27 -3
View File
@@ -16,10 +16,13 @@ from gi.repository import Gio, GLib
parser = argparse.ArgumentParser()
parser.add_argument("--build-dir", default="build")
parser.add_argument("--session", action="store_true")
parser.add_argument("--fixture", choices=("menu", "edit", "notepad"), default="menu")
parser.add_argument("--fixture", choices=("menu", "edit", "notepad", "list"), default="menu")
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 != "list":
parser.error("32-bit checks currently require --fixture list")
if (args.late_helper or args.restart_helper) and args.fixture != "edit":
parser.error("helper lifecycle checks require --fixture edit")
build = Path(args.build_dir).resolve()
@@ -27,6 +30,7 @@ if not args.session:
sys.exit(subprocess.call(["dbus-run-session", "--", sys.executable,
__file__, "--session", "--build-dir", str(build),
"--fixture", args.fixture,
*(["--32-bit"] if args.is32Bit else []),
*(["--late-helper"] if args.late_helper else []),
*(["--restart-helper"] if args.restart_helper else [])]))
@@ -56,9 +60,10 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
ready = Path(temporary) / "ready"
env["WINE_ACCESS_FIXTURE_READY"] = "Z:" + str(ready).replace("/", "\\")
fixture = Path(temporary) / "menu-fixture.exe"
subprocess.run(["x86_64-w64-mingw32-gcc", "-Wall", "-Wextra", "-mwindows",
compiler = "i686-w64-mingw32-gcc" if args.is32Bit else "x86_64-w64-mingw32-gcc"
subprocess.run([compiler, "-Wall", "-Wextra", "-mwindows",
"-o", str(fixture), str(Path(__file__).with_name(args.fixture + "_fixture.c")),
"-luser32"], check=True)
"-luser32", "-lcomctl32"], check=True)
helper = None
app = None
try:
@@ -104,6 +109,25 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
focused = event[0]
elif event[1] == "text":
assert event[0] == focused, f"text sent without establishing focus: {event!r}"
if args.fixture == "list":
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] == "Required components" and
e[4] == "Pending, Runtime & tools β" and e[7:9] == (1, 2)
and e[5] == "" for e in listEvents), "unselected initial row or label missing"
assert any(e[1] == "selection" and e[4] == "Installed, Second component" and
e[7:9] == (2, 2) for e in listEvents), "Down lost second row/columns"
assert any(e[1] == "selection" and e[4] == "Pending, Runtime & tools β" and
e[7:9] == (1, 2) for e in listEvents), "Up lost first row"
assert any(e[1] == "focus" and e[4] == "Pending, Runtime & tools β" and
e[5] == "selected" for e in listEvents), "return to list lost selection"
assert any(e[4] == "Ready, Runtime & tools β" for e in listEvents), "value event lost changed row text"
assert any(e[4] == "" and e[7:9] == (0, 0) for e in listEvents), "empty list retained stale row"
assert any(e[2] == "Return to control" for e in events), "button focus missing"
assert not any(e[2] == "List1" for e in events), "generic MSAA client leaked"
print("PASS: native list focus, columns, Unicode, navigation, return, and empty list")
sys.exit(0)
if args.fixture == "notepad":
dialogEvents = [e for e in events if e[2] in ("Yes", "No", "Cancel")]
for event in dialogEvents: