Goldwave helped find more missing functionality.

This commit is contained in:
ak47 committed 2026-10-10 23:43:37 -04:00
1 parent df7879acb5
commit 67ebc28d15
9 files changed
+134 -8

No files matched your search

+7
View File
@@ -163,6 +163,13 @@ and values. Edit and combo-box labels can be visible siblings to the left,
even when the labels were created after the fields. Application-owned MSAA even when the labels were created after the fields. Application-owned MSAA
lists retain their own names, roles and item data. lists retain their own names, roles and item data.
Delphi VCL `TListView`, `TButton`, `TBitBtn`, and `TStaticText` subclasses
use these same native queries. List views with standard checkboxes report
checked/not-checked states and observe Space-key toggles even without a
selection event. This covers GoldWave 5.70's Setup list and standard buttons;
arbitrary custom Delphi controls and 32-bit menu/editor hooks remain outside
this support.
Control values and window titles retain literal ampersands. Button and static Control values and window titles retain literal ampersands. Button and static
label captions decode keyboard mnemonics and escaped `&&`; static labels with label captions decode keyboard mnemonics and escaped `&&`; static labels with
`SS_NOPREFIX` retain their literal text. `SS_NOPREFIX` retain their literal text.
+10
View File
@@ -7,6 +7,16 @@
// not edits merely because their name contains EDIT somewhere. // not edits merely because their name contains EDIT somewhere.
static inline BOOL wine_access_class_matches(const WCHAR *className, const WCHAR *nativeName) { static inline BOOL wine_access_class_matches(const WCHAR *className, const WCHAR *nativeName) {
if (lstrcmpiW(className, nativeName) == 0) return TRUE; if (lstrcmpiW(className, nativeName) == 0) return TRUE;
// Delphi VCL registers native subclasses under its own class names.
// Keep aliases explicit: an arbitrary T-prefixed custom control need not
// implement the corresponding Windows control messages.
if (lstrcmpiW(nativeName, L"SysListView32") == 0 &&
lstrcmpiW(className, L"TListView") == 0) return TRUE;
if (lstrcmpiW(nativeName, L"Button") == 0 &&
(lstrcmpiW(className, L"TButton") == 0 ||
lstrcmpiW(className, L"TBitBtn") == 0)) return TRUE;
if (lstrcmpiW(nativeName, L"Static") == 0 &&
lstrcmpiW(className, L"TStaticText") == 0) return TRUE;
if (lstrlenW(className) > 4 && if (lstrlenW(className) > 4 &&
CompareStringOrdinal(className, 4, L"ATL:", 4, TRUE) == CSTR_EQUAL && CompareStringOrdinal(className, 4, L"ATL:", 4, TRUE) == CSTR_EQUAL &&
lstrcmpiW(className + 4, nativeName) == 0) return TRUE; lstrcmpiW(className + 4, nativeName) == 0) return TRUE;
@@ -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=r17.gcbce835 pkgver=r18.gdf7879a
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')
+13
View File
@@ -65,6 +65,7 @@ ListViewRow read_list_view(HWND window) {
DWORD_PTR state = 0; DWORD_PTR state = 0;
if (query(window, LVM_GETITEMSTATE, row.index, LVIS_SELECTED, &state)) if (query(window, LVM_GETITEMSTATE, row.index, LVIS_SELECTED, &state))
row.selected = state & LVIS_SELECTED; row.selected = state & LVIS_SELECTED;
row.checked = list_view_checked(window, row.index);
native_control::RemoteBuffer remote(window, bufferSize); native_control::RemoteBuffer remote(window, bufferSize);
if (!remote) return row; if (!remote) return row;
const bool wow64 = remote.is32Bit(); const bool wow64 = remote.is32Bit();
@@ -104,3 +105,15 @@ ListViewRow read_list_view(HWND window) {
} }
return row; return row;
} }
int list_view_checked(HWND window, int index) {
DWORD_PTR style = 0, state = 0;
if (index < 0 ||
!query(window, LVM_GETEXTENDEDLISTVIEWSTYLE, 0, 0, &style) ||
!(style & LVS_EX_CHECKBOXES) ||
!query(window, LVM_GETITEMSTATE, index, LVIS_STATEIMAGEMASK, &state))
return -1;
const DWORD_PTR image = (state & LVIS_STATEIMAGEMASK) >> 12;
// Other state images are application-defined; do not invent a check state.
return image == 1 ? 0 : image == 2 ? 1 : -1;
}
+2
View File
@@ -8,8 +8,10 @@ struct ListViewRow {
int index = -1; int index = -1;
int count = 0; int count = 0;
bool selected = false; bool selected = false;
int checked = -1; // -1: no standard checkbox, 0: unchecked, 1: checked.
}; };
bool is_list_view(HWND window); bool is_list_view(HWND window);
ListViewRow read_list_view(HWND window); ListViewRow read_list_view(HWND window);
int list_view_index(HWND window, int *count); int list_view_index(HWND window, int *count);
int list_view_checked(HWND window, int index);
+11 -1
View File
@@ -38,6 +38,7 @@ HWND lastListControl = nullptr;
int lastListIndex = -1; int lastListIndex = -1;
int lastListCount = 0; int lastListCount = 0;
bool lastListSelected = false; bool lastListSelected = false;
int lastListChecked = -1;
HWND lastNativeControl = nullptr; HWND lastNativeControl = nullptr;
NativeItemKey lastNativeItem; NativeItemKey lastNativeItem;
@@ -332,10 +333,15 @@ ControlDescription describe_standard_control(HWND window) {
if (!row.text.empty()) control.position = row.index + 1; if (!row.text.empty()) control.position = row.index + 1;
control.count = row.count; control.count = row.count;
if (row.selected) control.states = "selected"; if (row.selected) control.states = "selected";
if (row.checked >= 0) {
if (!control.states.empty()) control.states += ", ";
control.states += row.checked ? "checked" : "not checked";
}
lastListControl = window; lastListControl = window;
lastListIndex = row.index; lastListIndex = row.index;
lastListCount = row.count; lastListCount = row.count;
lastListSelected = row.selected; lastListSelected = row.selected;
lastListChecked = row.checked;
return control; return control;
} }
@@ -427,6 +433,7 @@ void poll_control_focus(bool refresh = false) {
lastListIndex = -1; lastListIndex = -1;
lastListCount = 0; lastListCount = 0;
lastListSelected = false; lastListSelected = false;
lastListChecked = -1;
const auto key = native_item_key(info.hwndFocus); const auto key = native_item_key(info.hwndFocus);
if (info.hwndFocus != lastNativeControl) { if (info.hwndFocus != lastNativeControl) {
present_standard_focus(info.hwndFocus); present_standard_focus(info.hwndFocus);
@@ -448,9 +455,11 @@ void poll_control_focus(bool refresh = false) {
SendMessageTimeoutW(info.hwndFocus, LVM_GETITEMSTATE, index, LVIS_SELECTED, SendMessageTimeoutW(info.hwndFocus, LVM_GETITEMSTATE, index, LVIS_SELECTED,
SMTO_ABORTIFHUNG, 100, &state); SMTO_ABORTIFHUNG, 100, &state);
const bool selected = state & LVIS_SELECTED; const bool selected = state & LVIS_SELECTED;
const int checked = list_view_checked(info.hwndFocus, index);
if (info.hwndFocus != lastListControl) { if (info.hwndFocus != lastListControl) {
present_standard_focus(info.hwndFocus); present_standard_focus(info.hwndFocus);
} else if (refresh || index != lastListIndex || count != lastListCount || selected != lastListSelected) { } else if (refresh || index != lastListIndex || count != lastListCount ||
selected != lastListSelected || checked != lastListChecked) {
const auto control = describe_standard_control(info.hwndFocus); const auto control = describe_standard_control(info.hwndFocus);
present_accessible_event(source_id(info.hwndFocus, "standard"), "selection", present_accessible_event(source_id(info.hwndFocus, "standard"), "selection",
control, window_title(info.hwndFocus)); control, window_title(info.hwndFocus));
@@ -461,6 +470,7 @@ void poll_control_focus(bool refresh = false) {
lastListIndex = -1; lastListIndex = -1;
lastListCount = 0; lastListCount = 0;
lastListSelected = false; lastListSelected = false;
lastListChecked = -1;
} }
void present_menu_event(const WineAccessMenuEvent &event) { void present_menu_event(const WineAccessMenuEvent &event) {
+30
View File
@@ -161,6 +161,36 @@ 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 native list, normal menus, editor navigation, switching to a native Linux
application, and reader shutdown. Verify speech and physical braille separately. application, and reader shutdown. Verify speech and physical braille separately.
## Delphi Setup lists and checkboxes
The October 10 GoldWave 5.70 Setup inspection found a 32-bit `TListView`
with 14 checkbox rows, plus `TButton` and `TBitBtn` controls. Wine exposed
them as generic MSAA clients; the list client's name/value were empty. Focus
events were present for the buttons and on return to the list. The native
messages already exposed the option captions and checkbox state, but the
helper did not recognize these VCL class names or read list checkbox states.
The explicit VCL aliases and native checkbox query fix those shared gaps.
The updated query code read the actual open Setup's first option, `Disable
effect plug-ins`, `not checked`, position 1 of 14, and the Backup/Restore/OK/
Cancel button roles without changing settings or sending speech. A later
live registry query also confirmed `StartupOptions=0x3008` and Setup showed
`Force screen reader mode` checked; the earlier immediate persistence query
had still seen `0x1008`.
```sh
xvfb-run -a python3 tests/menu_integration.py --build-dir build --fixture delphi --32-bit
xvfb-run -a python3 tests/menu_integration.py --build-dir build --fixture delphi
```
Both fixtures passed with native controls registered under VCL names. They
check initial rows/labels, Down navigation, Space check/uncheck without
synthetic accessibility events, button roles, return to the checked row,
and empty-list state reset. The existing 32-bit plain-list regression also
passed. These are private D-Bus recordings; audible GoldWave speech and
physical braille remain a live acceptance check after installing the helper
and restarting Cthulhu. The fix does not add 32-bit menu or editor hooks.
## Native tabs, trees and named clients ## Native tabs, trees and named clients
```sh ```sh
+37 -3
View File
@@ -5,6 +5,9 @@
#include <commctrl.h> #include <commctrl.h>
static HWND list, button; static HWND list, button;
#ifdef WINE_ACCESS_DELPHI_FIXTURE
static HWND okButton;
#endif
static int step; static int step;
static LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wp, LPARAM lp) { static LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wp, LPARAM lp) {
@@ -15,18 +18,29 @@ static LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wp, LPARAM
SendMessageW(list, WM_KEYDOWN, VK_DOWN, 1); SendMessageW(list, WM_KEYDOWN, VK_DOWN, 1);
break; break;
case 3: case 3:
#ifdef WINE_ACCESS_DELPHI_FIXTURE
SendMessageW(list, WM_KEYDOWN, VK_SPACE, 1);
#else
SendMessageW(list, WM_KEYDOWN, VK_UP, 1); SendMessageW(list, WM_KEYDOWN, VK_UP, 1);
#endif
break; break;
case 4: SetFocus(button); break; case 4: SetFocus(button); break;
case 5: SetFocus(list); break; case 5: SetFocus(list); break;
case 6: { case 6: {
#ifdef WINE_ACCESS_DELPHI_FIXTURE
SendMessageW(list, WM_KEYDOWN, VK_SPACE, 1);
#else
LVITEMW item = {0}; LVITEMW item = {0};
item.pszText = L"Ready"; item.pszText = L"Ready";
SendMessageW(list, LVM_SETITEMTEXTW, 0, (LPARAM)&item); SendMessageW(list, LVM_SETITEMTEXTW, 0, (LPARAM)&item);
NotifyWinEvent(EVENT_OBJECT_VALUECHANGE, list, OBJID_CLIENT, CHILDID_SELF); NotifyWinEvent(EVENT_OBJECT_VALUECHANGE, list, OBJID_CLIENT, CHILDID_SELF);
#endif
break; break;
} }
case 7: SendMessageW(list, LVM_DELETEALLITEMS, 0, 0); break; case 7: SendMessageW(list, LVM_DELETEALLITEMS, 0, 0); break;
#ifdef WINE_ACCESS_DELPHI_FIXTURE
case 8: SetFocus(okButton); break;
#endif
default: DestroyWindow(window); default: DestroyWindow(window);
} }
return 0; return 0;
@@ -46,12 +60,32 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR args, int show)
RegisterClassW(&cls); RegisterClassW(&cls);
HWND window = CreateWindowW(cls.lpszClassName, L"Wine list fixture", WS_OVERLAPPEDWINDOW, HWND window = CreateWindowW(cls.lpszClassName, L"Wine list fixture", WS_OVERLAPPEDWINDOW,
120, 120, 600, 300, NULL, NULL, instance, NULL); 120, 120, 600, 300, NULL, NULL, instance, NULL);
button = CreateWindowW(L"Button", L"Return to control", WS_CHILD | WS_VISIBLE | WS_TABSTOP, #ifdef WINE_ACCESS_DELPHI_FIXTURE
const WCHAR *nativeNames[] = {WC_LISTVIEWW, L"Button", L"Button", L"Static"};
const WCHAR *delphiNames[] = {L"TListView", L"TBitBtn", L"TButton", L"TStaticText"};
for (int i = 0; i < 4; ++i) {
WNDCLASSEXW subclass = {0};
subclass.cbSize = sizeof(subclass);
if (!GetClassInfoExW(NULL, nativeNames[i], &subclass)) return 1;
subclass.hInstance = instance;
subclass.lpszClassName = delphiNames[i];
if (!RegisterClassExW(&subclass)) return 2;
}
const WCHAR *buttonClass = L"TBitBtn", *listClass = L"TListView", *labelClass = L"TStaticText";
okButton = CreateWindowW(L"TButton", L"&OK", WS_CHILD | WS_VISIBLE | WS_TABSTOP,
240, 240, 120, 30, window, NULL, instance, NULL);
#else
const WCHAR *buttonClass = L"Button", *listClass = WC_LISTVIEWW, *labelClass = L"Static";
#endif
button = CreateWindowW(buttonClass, L"Return to control", WS_CHILD | WS_VISIBLE | WS_TABSTOP,
10, 240, 200, 30, window, NULL, instance, NULL); 10, 240, 200, 30, window, NULL, instance, NULL);
CreateWindowW(L"Static", L"Required components", WS_CHILD | WS_VISIBLE, CreateWindowW(labelClass, L"Required components", WS_CHILD | WS_VISIBLE,
10, 10, 550, 30, window, NULL, instance, NULL); 10, 10, 550, 30, window, NULL, instance, NULL);
list = CreateWindowW(WC_LISTVIEWW, L"List1", WS_CHILD | WS_VISIBLE | WS_TABSTOP | LVS_REPORT, list = CreateWindowW(listClass, L"List1", WS_CHILD | WS_VISIBLE | WS_TABSTOP | LVS_REPORT,
10, 50, 550, 180, window, NULL, instance, NULL); 10, 50, 550, 180, window, NULL, instance, NULL);
#ifdef WINE_ACCESS_DELPHI_FIXTURE
SendMessageW(list, LVM_SETEXTENDEDLISTVIEWSTYLE, LVS_EX_CHECKBOXES, LVS_EX_CHECKBOXES);
#endif
LVCOLUMNW column = {0}; LVCOLUMNW column = {0};
column.mask = LVCF_TEXT | LVCF_WIDTH; column.mask = LVCF_TEXT | LVCF_WIDTH;
column.cx = 150; column.cx = 150;
+23 -3
View File
@@ -17,14 +17,14 @@ from gi.repository import Gio, GLib
parser = argparse.ArgumentParser() 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("--fixture", choices=("menu", "edit", "notepad", "list", "winforms", "controls", "scintilla"), default="menu") parser.add_argument("--fixture", choices=("menu", "edit", "notepad", "list", "delphi", "winforms", "controls", "scintilla"), default="menu")
parser.add_argument("--notepadpp-exe", type=Path) parser.add_argument("--notepadpp-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 ("list", "controls"): if args.is32Bit and args.fixture not in ("list", "delphi", "controls"):
parser.error("32-bit checks currently require --fixture list or controls") parser.error("32-bit checks currently require --fixture list, 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()):
@@ -82,8 +82,11 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
fixture = Path(temporary) / "menu-fixture.exe" fixture = Path(temporary) / "menu-fixture.exe"
compiler = "i686-w64-mingw32-gcc" if args.is32Bit else "x86_64-w64-mingw32-gcc" compiler = "i686-w64-mingw32-gcc" if args.is32Bit else "x86_64-w64-mingw32-gcc"
sourceName = "edit" if args.fixture == "winforms" else args.fixture sourceName = "edit" if args.fixture == "winforms" else args.fixture
if args.fixture == "delphi":
sourceName = "list"
subprocess.run([compiler, "-Wall", "-Wextra", "-mwindows", subprocess.run([compiler, "-Wall", "-Wextra", "-mwindows",
*(["-DWINE_ACCESS_WINFORMS_FIXTURE"] if args.fixture == "winforms" else []), *(["-DWINE_ACCESS_WINFORMS_FIXTURE"] if args.fixture == "winforms" else []),
*(["-DWINE_ACCESS_DELPHI_FIXTURE"] if args.fixture == "delphi" else []),
"-o", str(fixture), str(Path(__file__).with_name(sourceName + "_fixture.c")), "-o", str(fixture), str(Path(__file__).with_name(sourceName + "_fixture.c")),
"-luser32", "-lcomctl32", "-loleacc", "-lole32"], check=True) "-luser32", "-lcomctl32", "-loleacc", "-lole32"], check=True)
helper = None helper = None
@@ -160,6 +163,23 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
assert not any(e[2] == "Tree1" for e in events), "generic tree name leaked" assert not any(e[2] == "Tree1" for e in events), "generic tree name leaked"
print("PASS: native/ATL tabs, tree selection/states/return, named button, combo and edit labels, empty controls") print("PASS: native/ATL tabs, tree selection/states/return, named button, combo and edit labels, empty controls")
sys.exit(0) sys.exit(0)
if args.fixture == "delphi":
for event in events:
print(event[1:9])
assert any(e[1] == "focus" and e[2] == "Required components" and
e[3] == "list" and e[4] == "Pending, Runtime & tools β" and
e[5] == "not checked" and e[7:9] == (1, 2) for e in events), "Delphi initial row/label/state missing"
rows = [e for e in events if e[3] == "list" and e[4] == "Installed, Second component"]
assert any(e[1] == "selection" and e[5] == "selected, not checked" for e in rows), "Delphi Down missing"
assert any(e[1] == "selection" and e[5] == "selected, checked" for e in rows), "Space checkbox change missing"
assert any(e[1] == "focus" and e[5] == "selected, checked" for e in rows), "Delphi return lost checkbox"
assert sum(e[1] == "selection" and e[5] == "selected, not checked" for e in rows) >= 2, "Space uncheck missing"
for caption in ("Return to control", "OK"):
assert any(e[1] == "focus" and e[2] == caption and e[3] == "button" for e in events), f"Delphi button missing: {caption}"
assert any(e[3] == "list" and e[4] == "" and e[5] == "" and e[7:9] == (0, 0) for e in events), "empty Delphi list retained state"
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 == "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: