More fixes for menus and combo boxes.

This commit is contained in:
ak47 committed 2026-10-11 00:10:21 -04:00
1 parent 67ebc28d15
commit 872388a59b
11 files changed
+392 -35

No files matched your search

+34 -1
View File
@@ -2,7 +2,7 @@
Build the project, then run `python3 tests/menu_integration.py --build-dir build`
from the repository root in a graphical session. Dependencies are Wine,
MinGW's x86_64 C compiler, PyGObject, and `dbus-run-session`.
MinGW's x86_64 and i686 C compilers, PyGObject, and `dbus-run-session`.
The test creates a temporary Wine prefix and a private D-Bus service. Its
64-bit PE application exercises the separately running Winelib helper's
@@ -108,6 +108,39 @@ reader's `docs/testing/wine-accessibility.md` for current evidence and live
acceptance gates. Run Xvfb fixtures sequentially; concurrent automatic display
allocation can collide.
## GoldWave menus and combo boxes
The helper supplies both 32-bit and 64-bit menu hooks. Check both architectures:
```sh
xvfb-run -a python3 tests/menu_integration.py --fixture menu
xvfb-run -a python3 tests/menu_integration.py --fixture menu --32-bit
xvfb-run -a python3 tests/menu_integration.py --fixture controls
xvfb-run -a python3 tests/menu_integration.py --fixture controls --32-bit
xvfb-run -a python3 tests/menu_integration.py --fixture goldwave --goldwave-exe /path/to/GoldWave.exe
```
The controls fixture changes a combo selection without emitting a value
WinEvent. It requires the new value while focus stays on the combo, with the
same source identity as its focus event. This catches both missing polling and
Wine's misleading focus notifications for the combo's internal list.
The optional GoldWave 5.70 fixture copies the application directory into a
temporary directory and uses a fresh Wine prefix with Force screen reader
mode enabled. It waits for the actual main form, dismisses the first-run
settings prompt in the test copy, and drives Alt+F, menu arrows, Ctrl+O and
file-type combo Down/Down/Up using SendInput. On October 11 it received File,
New, Open and unavailable Close menu events, followed by Wave/MPEG Audio/Wave
value updates without tabbing away. This confirms helper events through the
private D-Bus recorder; audible speech and physical braille need live checks.
Install all five helper artifacts together, restart Cthulhu, and reopen
GoldWave. With screen reader mode enabled, check File and other menus with
Alt and arrows, then Escape back to the previous control. In Open Sound,
check Files of type and Look in with arrows: expect the new value immediately
in speech and braille, with keyboard focus remaining on the combo. Repeat
Tab/Shift+Tab and Alt+Down/Escape dropdown opening and closing.
## Notepad++ / Scintilla checks
Use a locally available 64-bit Notepad++ executable:
+1 -1
View File
@@ -28,7 +28,7 @@ static LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wp, LPARAM
case 10: focus_control(combo); break;
case 11:
SendMessageW(combo, CB_SETCURSEL, 1, 0);
NotifyWinEvent(EVENT_OBJECT_VALUECHANGE, combo, OBJID_CLIENT, CHILDID_SELF);
// Common file dialogs can change the selection without a WinEvent.
break;
case 12: focus_control(edit); break;
case 13: focus_control(atlTabs); break;
+102
View File
@@ -0,0 +1,102 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Drive a disposable copy of GoldWave 5.70 using real Wine keyboard input.
#include <windows.h>
#include <dlgs.h>
static DWORD appPid;
static HWND app, dialog;
static BOOL CALLBACK dismiss_setup(HWND window, LPARAM unused) {
(void)unused;
WCHAR text[64] = {0};
GetWindowTextW(window, text, 64);
if (!lstrcmpW(text, L"No") || !lstrcmpW(text, L"&No"))
PostMessageW(window, BM_CLICK, 0, 0);
return TRUE;
}
static BOOL CALLBACK find_window(HWND window, LPARAM unused) {
(void)unused;
DWORD pid = 0;
WCHAR title[128] = {0};
GetWindowThreadProcessId(window, &pid);
GetWindowTextW(window, title, 128);
if (pid == appPid && IsWindowVisible(window)) {
WCHAR cls[128] = {0};
GetClassNameW(window, cls, 128);
if (!lstrcmpW(cls, L"TGWMainForm")) app = window;
if (GetDlgItem(window, cmb1)) dialog = window;
if (!lstrcmpW(cls, L"TMessageForm")) EnumChildWindows(window, dismiss_setup, 0);
}
return TRUE;
}
static void key(WORD vk, BOOL alt, BOOL control) {
INPUT input[6] = {0};
UINT count = 0;
if (alt) { input[count].ki.wVk = VK_MENU; ++count; }
if (control) { input[count].ki.wVk = VK_CONTROL; ++count; }
input[count++].ki.wVk = vk;
input[count].ki.wVk = vk; input[count++].ki.dwFlags = KEYEVENTF_KEYUP;
if (control) { input[count].ki.wVk = VK_CONTROL; input[count++].ki.dwFlags = KEYEVENTF_KEYUP; }
if (alt) { input[count].ki.wVk = VK_MENU; input[count++].ki.dwFlags = KEYEVENTF_KEYUP; }
for (UINT i = 0; i < count; ++i) input[i].type = INPUT_KEYBOARD;
if (SendInput(count, input, sizeof(INPUT)) != count) ExitProcess(3);
Sleep(400);
}
int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR args, int show) {
(void)instance; (void)previous; (void)args; (void)show;
// The runner supplies a fresh prefix; this setting never touches the user's
// configuration. Match the reported Force screen reader mode setup.
HKEY options;
if (RegCreateKeyExW(HKEY_CURRENT_USER, L"Software\\GoldWave\\GoldWave\\Options", 0,
NULL, 0, KEY_SET_VALUE, NULL, &options, NULL) != ERROR_SUCCESS) return 2;
DWORD startupOptions = 0x3008;
RegSetValueExW(options, L"StartupOptions", 0, REG_DWORD, (BYTE *)&startupOptions, sizeof(startupOptions));
RegCloseKey(options);
if (RegCreateKeyExW(HKEY_CURRENT_USER, L"Software\\GoldWave\\GoldWave", 0,
NULL, 0, KEY_SET_VALUE, NULL, &options, NULL) != ERROR_SUCCESS) return 2;
const WCHAR version[] = L"5.70";
RegSetValueExW(options, L"Version", 0, REG_SZ, (const BYTE *)version, sizeof(version));
RegCloseKey(options);
WCHAR executable[4096] = {0};
if (!GetEnvironmentVariableW(L"WINE_ACCESS_GOLDWAVE_EXE", executable, 4096)) return 2;
STARTUPINFOW startup = {0};
startup.cb = sizeof(startup);
PROCESS_INFORMATION process = {0};
if (!CreateProcessW(executable, NULL, NULL, NULL, FALSE, 0, NULL, NULL, &startup, &process)) return 2;
appPid = process.dwProcessId;
for (int i = 0; i < 200 && !app; ++i) { EnumWindows(find_window, 0); Sleep(50); }
if (!app) return 2;
SetForegroundWindow(app);
Sleep(500);
key('F', TRUE, FALSE);
key(VK_DOWN, FALSE, FALSE);
key(VK_DOWN, FALSE, FALSE);
key(VK_ESCAPE, FALSE, FALSE);
key(VK_ESCAPE, FALSE, FALSE);
key('O', FALSE, TRUE);
for (int i = 0; i < 100 && !dialog; ++i) { EnumWindows(find_window, 0); Sleep(50); }
if (!dialog) return 2;
HWND combo = GetDlgItem(dialog, cmb1);
SendMessageW(dialog, WM_NEXTDLGCTL, (WPARAM)combo, TRUE);
Sleep(500);
WCHAR before[512] = {0}, after[512] = {0};
SendMessageW(combo, WM_GETTEXT, 512, (LPARAM)before);
key(VK_DOWN, FALSE, FALSE);
SendMessageW(combo, WM_GETTEXT, 512, (LPARAM)after);
if (!lstrcmpW(before, after)) return 4;
GUITHREADINFO info = {0};
info.cbSize = sizeof(info);
GetGUIThreadInfo(GetWindowThreadProcessId(dialog, NULL), &info);
if (info.hwndFocus != combo) return 4;
key(VK_DOWN, FALSE, FALSE);
key(VK_UP, FALSE, FALSE);
key(VK_ESCAPE, FALSE, FALSE);
PostMessageW(app, WM_CLOSE, 0, 0);
int result = WaitForSingleObject(process.hProcess, 3000) == WAIT_OBJECT_0 ? 0 : 2;
CloseHandle(process.hThread);
CloseHandle(process.hProcess);
return result;
}
+27 -7
View File
@@ -17,24 +17,28 @@ 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", "list", "delphi", "winforms", "controls", "scintilla"), default="menu")
parser.add_argument("--fixture", choices=("menu", "edit", "notepad", "list", "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 ("list", "delphi", "controls"):
parser.error("32-bit checks currently require --fixture list, delphi or controls")
if args.is32Bit and args.fixture not in ("menu", "list", "delphi", "controls"):
parser.error("32-bit checks currently require --fixture menu, list, 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()):
parser.error("--fixture scintilla requires --notepadpp-exe pointing to a 64-bit Notepad++ executable")
if args.fixture == "goldwave" and (not args.goldwave_exe or not args.goldwave_exe.is_file()):
parser.error("--fixture goldwave requires --goldwave-exe pointing to GoldWave 5.70")
build = Path(args.build_dir).resolve()
if not args.session:
sys.exit(subprocess.call(["dbus-run-session", "--", sys.executable,
__file__, "--session", "--build-dir", str(build),
"--fixture", args.fixture,
*(["--notepadpp-exe", str(args.notepadpp_exe.resolve())] if args.notepadpp_exe else []),
*(["--goldwave-exe", str(args.goldwave_exe.resolve())] if args.goldwave_exe else []),
*(["--32-bit"] if args.is32Bit else []),
*(["--late-helper"] if args.late_helper else []),
*(["--restart-helper"] if args.restart_helper else [])]))
@@ -60,10 +64,14 @@ connection.call_sync("org.freedesktop.DBus", "/org/freedesktop/DBus",
with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
env = os.environ.copy()
env.update(WINEPREFIX=str(Path(temporary) / "prefix"), WINEDEBUG="-all",
env.update(WINEPREFIX=str(Path(temporary) / "prefix"), WINEDEBUG=os.environ.get("WINE_ACCESS_TEST_DEBUG", "-all"),
WINEDLLOVERRIDES="mscoree,mshtml=", CTHULHU_WINE_ACCESS_MANAGED="1")
ready = Path(temporary) / "ready"
env["WINE_ACCESS_FIXTURE_READY"] = "Z:" + str(ready).replace("/", "\\")
if args.fixture == "goldwave":
appDir = Path(temporary) / "goldwave"
shutil.copytree(args.goldwave_exe.parent, appDir)
env["WINE_ACCESS_GOLDWAVE_EXE"] = "Z:" + str(appDir / args.goldwave_exe.name).replace("/", "\\")
if args.fixture == "scintilla":
# Copy the application to isolate its configuration as well as its
# document. Never open or edit files in the user's running instance.
@@ -80,7 +88,7 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
env["WINE_ACCESS_NOTEPADPP_EXE"] = "Z:" + str(appDir / args.notepadpp_exe.name).replace("/", "\\")
env["WINE_ACCESS_SCINTILLA_DOCUMENT"] = "Z:" + str(document).replace("/", "\\")
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 or args.fixture == "goldwave" else "x86_64-w64-mingw32-gcc"
sourceName = "edit" if args.fixture == "winforms" else args.fixture
if args.fixture == "delphi":
sourceName = "list"
@@ -88,7 +96,7 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
*(["-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")),
"-luser32", "-lcomctl32", "-loleacc", "-lole32"], check=True)
"-luser32", "-lcomctl32", "-loleacc", "-lole32", "-ladvapi32"], check=True)
helper = None
app = None
try:
@@ -121,7 +129,7 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
restarted = True
time.sleep(0.01)
assert app.poll() == 0, "fixture failed or timed out"
assert helper.poll() is None, "helper exited during the fixture"
assert helper.poll() is None, f"helper exited during the fixture: {helper.returncode}"
if args.restart_helper:
assert restarted, "fixture never triggered a helper restart"
for _ in range(20):
@@ -134,6 +142,18 @@ 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}"
elif event[1] == "value" and event[3] == "combo box":
assert event[0] == focused, f"combo value sent for an unfocused source: {event!r}; focus={focused}; events={events!r}"
if args.fixture == "goldwave":
for event in events:
print(event[1:9])
menus = [e for e in events if ":menu:" in e[0]]
assert any(e[2] == "File" and e[3] == "submenu" for e in menus), "GoldWave File menu missing"
assert any(e[2].startswith("Open") and e[3] == "menu item" for e in menus), "GoldWave Open command missing"
values = [e[4] for e in events if e[1] == "value" and e[3] == "combo box"]
assert len(values) >= 3 and len(set(values)) >= 2, f"GoldWave combo arrow feedback missing: {values!r}"
print("PASS: actual 32-bit GoldWave menus and file-type combo arrows without tabbing away")
sys.exit(0)
if args.fixture == "controls":
for event in events:
print(event[1:9])