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

+17 -1
View File
@@ -8,6 +8,7 @@ find_package(PkgConfig REQUIRED)
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
find_program(WIDL_EXECUTABLE widl REQUIRED)
find_program(MINGW_C_COMPILER x86_64-w64-mingw32-gcc REQUIRED)
find_program(MINGW32_C_COMPILER i686-w64-mingw32-gcc REQUIRED)
# A PE hook DLL can be loaded into ordinary Wine applications without any
# GLib dependencies. Keep it alongside the Winelib helper.
@@ -21,6 +22,19 @@ add_custom_command(OUTPUT "${menu_hook}"
VERBATIM)
add_custom_target(wine-access-menu ALL DEPENDS "${menu_hook}")
set(menu_hook32 "${CMAKE_CURRENT_BINARY_DIR}/wine-access-menu32.dll")
set(menu_host32 "${CMAKE_CURRENT_BINARY_DIR}/wine-access-menu-host32.exe")
add_custom_command(OUTPUT "${menu_hook32}" "${menu_host32}"
COMMAND "${MINGW32_C_COMPILER}" -std=c11 -O2 -Wall -Wextra -shared
-static-libgcc -Wl,--kill-at -DWINE_ACCESS_MENU_ONLY -o "${menu_hook32}"
"${CMAKE_CURRENT_SOURCE_DIR}/menu_hook.c" -luser32
COMMAND "${MINGW32_C_COMPILER}" -std=c11 -O2 -Wall -Wextra -mwindows
-static-libgcc -o "${menu_host32}"
"${CMAKE_CURRENT_SOURCE_DIR}/menu_host32.c" -luser32
DEPENDS menu_hook.c menu_bridge.h menu_host32.c
VERBATIM)
add_custom_target(wine-access-menu32 ALL DEPENDS "${menu_hook32}" "${menu_host32}")
set(generated_dir "${CMAKE_CURRENT_BINARY_DIR}/generated")
set(generated_header "${generated_dir}/nvdaController.h")
set(generated_server "${generated_dir}/nvdaController_s.c")
@@ -34,7 +48,7 @@ add_custom_command(
VERBATIM)
add_executable(cthulhu-wine-access main.cpp list_view.cpp native_items.cpp "${generated_server}")
add_dependencies(cthulhu-wine-access wine-access-menu)
add_dependencies(cthulhu-wine-access wine-access-menu wine-access-menu32)
target_compile_features(cthulhu-wine-access PRIVATE cxx_std_20)
target_include_directories(cthulhu-wine-access PRIVATE "${generated_dir}")
target_link_libraries(cthulhu-wine-access PRIVATE
@@ -62,4 +76,6 @@ install(PROGRAMS
install(FILES
"${CMAKE_CURRENT_BINARY_DIR}/cthulhu-wine-access.exe.so"
"${menu_hook}"
"${menu_hook32}"
"${menu_host32}"
DESTINATION "${WINE_ACCESS_INSTALL_DIR}")
+20 -8
View File
@@ -25,11 +25,12 @@ Building requires an x86_64 Linux host with:
- Make and native GCC/G++ (the script uses CMake's default Makefile generator);
- `pkg-config` and the GLib/GIO development files;
- Wine development tools: `winegcc`, `wineg++`, and `widl`;
- `x86_64-w64-mingw32-gcc` to build the native menu hook DLL
- `x86_64-w64-mingw32-gcc` and `i686-w64-mingw32-gcc` to build native menu hooks
(`mingw-w64-gcc` on Arch Linux).
The target is a Winelib executable. A 32-bit build is not provided by this
project yet; it needs a 32-bit Winegcc and 32-bit GLib/GIO development setup.
The D-Bus helper is a 64-bit Winelib executable. A small 32-bit Windows
companion supplies menu hooks for 32-bit applications, without requiring
32-bit Linux GLib/GIO development files.
## Build
@@ -54,7 +55,7 @@ development packages. Removal does not require these dependencies.
Use `./build.sh -r` to remove the local installation, or
`sudo ./build.sh -r` to remove the system installation. If a user has no local
installation but a system installation exists, the script requests the latter
command without running sudo itself. Removal deletes only the three helper
command without running sudo itself. Removal deletes only the five helper
files and leaves other directory contents untouched.
Alternative build and install locations may be supplied as positional
@@ -72,6 +73,8 @@ That installs the helper and its Winelib sidecar as:
$prefix/libexec/cthulhu/wine/cthulhu-wine-access.exe
$prefix/libexec/cthulhu/wine/cthulhu-wine-access.exe.so
$prefix/libexec/cthulhu/wine/wine-access-menu.dll
$prefix/libexec/cthulhu/wine/wine-access-menu32.dll
$prefix/libexec/cthulhu/wine/wine-access-menu-host32.exe
```
For Cthulhu-Go, either set `CTHULHU_WINE_ACCESS_HELPER` to the installed
@@ -98,15 +101,24 @@ The standard-control reader is deliberately limited to native Wine controls.
Custom-drawn, DirectX, and game-specific accessibility APIs remain the
responsibility of the application or its accessibility middleware.
Native menu reading uses an accompanying 64-bit Windows hook DLL. It observes
Native menu reading uses accompanying 64-bit and 32-bit Windows hook DLLs. They
observe
`WM_MENUSELECT` inside Wine applications and sends menu labels, displayed
shortcuts, submenu roles, and checked/unavailable states to the helper. The
helper queues these messages before forwarding them over D-Bus, suppresses
repeated selections, and restores standard-control focus reporting when menus
close. The hook applies to the helper's Wine desktop and prefix and is removed
on normal shutdown. It is disabled by `--no-dialog-reader`.
close. The hooks apply to the helper's Wine desktop and prefix and are removed
on normal shutdown. The 32-bit host attaches to the foreground 32-bit UI thread
so Wine's global hook chain cannot interfere with 64-bit menus. It exits when the helper dies or its bridge
window closes. Menu hooks are disabled by `--no-dialog-reader`. The 32-bit
companion handles menus; the editor subclass described below remains 64-bit.
The same DLL subclasses standard `Edit` controls, including Wine Notepad's
Focused combo boxes are checked every 100 ms to recover value changes that
Wine does not report through WinEvents. The combo's internal list events use
the same source identity as the focused combo, so the reader can accept the
new value without a Tab/Shift+Tab round trip.
The 64-bit DLL subclasses standard `Edit` controls, including Wine Notepad's
document area and `WindowsForms10.EDIT.app.*` controls. Windows Forms fields
use visible sibling labels immediately to their left, even when the labels
were created after the fields. Protected fields report their label and
+3 -1
View File
@@ -33,7 +33,8 @@ else
fi
prefix=${2:-"$defaultPrefix"}
installDir="${prefix%/}/libexec/cthulhu/wine"
installedFiles=(cthulhu-wine-access.exe cthulhu-wine-access.exe.so wine-access-menu.dll)
installedFiles=(cthulhu-wine-access.exe cthulhu-wine-access.exe.so wine-access-menu.dll
wine-access-menu32.dll wine-access-menu-host32.exe)
has_installation() {
local candidateDir=$1
@@ -69,6 +70,7 @@ check_prerequisites() {
cmake:cmake make:make gcc:gcc g++:gcc pkg-config:pkgconf
winegcc:wine wineg++:wine widl:wine
x86_64-w64-mingw32-gcc:mingw-w64-gcc
i686-w64-mingw32-gcc:mingw-w64-gcc
)
for requirement in "${requirements[@]}"; do
@@ -23,7 +23,8 @@ elsewhere. To use HTTPS after the repository becomes public:
WINE_ACCESS_GIT_URL=https://git.stormux.org/storm/wine-access.git makepkg -si
```
The package installs the Winelib launcher, its sidecar and the native hook DLL
The package installs the Winelib launcher, its sidecar, both native menu hook
DLLs, and the 32-bit menu host
under `/usr/libexec/cthulhu/wine/`. It provides and conflicts with the older
`cthulhu-wine-access` helper, including `cthulhu-wine-access-git`, so pacman
can replace that package in the installation transaction.
+112 -15
View File
@@ -41,6 +41,30 @@ bool lastListSelected = false;
int lastListChecked = -1;
HWND lastNativeControl = nullptr;
NativeItemKey lastNativeItem;
HWND lastComboControl = nullptr;
std::string lastComboValue;
bool is_combo_box(HWND window) {
wchar_t className[128] = {};
return GetClassNameW(window, className, std::size(className)) &&
(wine_access_class_matches(className, L"ComboBox") ||
wine_access_class_matches(className, L"ComboBoxEx32"));
}
HWND combo_event_control(HWND window, DWORD event) {
if (is_combo_box(window)) return window;
wchar_t className[128] = {};
const bool comboList = GetClassNameW(window, className, std::size(className)) &&
wine_access_class_matches(className, L"ComboLBox");
if (event != EVENT_OBJECT_FOCUS && !comboList) return nullptr;
GUITHREADINFO info{};
info.cbSize = sizeof(info);
HWND foreground = GetForegroundWindow();
if (foreground && GetGUIThreadInfo(GetWindowThreadProcessId(foreground, nullptr), &info) &&
is_combo_box(info.hwndFocus))
return info.hwndFocus;
return nullptr;
}
// Winegcc uses 16-bit wchar_t; Linux libc's wcs* functions expect 32-bit
// wchar_t. Use Wine's Unicode APIs or explicit code-unit comparisons.
@@ -321,6 +345,8 @@ ControlDescription describe_standard_control(HWND window) {
control.name = to_utf8(label);
control.role = "combo box";
control.value = to_utf8(text);
lastComboControl = window;
lastComboValue = control.value;
return control;
}
@@ -427,8 +453,14 @@ void poll_control_focus(bool refresh = false) {
GUITHREADINFO info{};
info.cbSize = sizeof(info);
HWND foreground = GetForegroundWindow();
if (foreground && GetGUIThreadInfo(GetWindowThreadProcessId(foreground, nullptr), &info) &&
!(info.flags & GUI_INMENUMODE) && is_native_item_control(info.hwndFocus)) {
const bool focused = foreground &&
GetGUIThreadInfo(GetWindowThreadProcessId(foreground, nullptr), &info) &&
!(info.flags & GUI_INMENUMODE);
if (!focused || !is_combo_box(info.hwndFocus)) {
lastComboControl = nullptr;
lastComboValue.clear();
}
if (focused && is_native_item_control(info.hwndFocus)) {
lastListControl = nullptr;
lastListIndex = -1;
lastListCount = 0;
@@ -446,8 +478,7 @@ void poll_control_focus(bool refresh = false) {
}
lastNativeControl = nullptr;
lastNativeItem = {};
if (foreground && GetGUIThreadInfo(GetWindowThreadProcessId(foreground, nullptr), &info) &&
!(info.flags & GUI_INMENUMODE) && is_list_view(info.hwndFocus)) {
if (focused && is_list_view(info.hwndFocus)) {
int count = 0;
const int index = list_view_index(info.hwndFocus, &count);
DWORD_PTR state = 0;
@@ -471,6 +502,19 @@ void poll_control_focus(bool refresh = false) {
lastListCount = 0;
lastListSelected = false;
lastListChecked = -1;
if (focused && is_combo_box(info.hwndFocus)) {
if (info.hwndFocus != lastComboControl) {
present_standard_focus(info.hwndFocus);
} else {
wchar_t text[512] = {};
get_control_text(info.hwndFocus, text, std::size(text));
if (refresh || to_utf8(text) != lastComboValue) {
const auto control = describe_standard_control(info.hwndFocus);
present_accessible_event(source_id(info.hwndFocus, "standard"), "value",
control, window_title(info.hwndFocus));
}
}
}
}
void present_menu_event(const WineAccessMenuEvent &event) {
@@ -625,21 +669,43 @@ LRESULT CALLBACK menu_bridge_proc(HWND window, UINT message, WPARAM wp, LPARAM l
return DefWindowProcW(window, message, wp, lp);
}
HMODULE load_menu_hook() {
wchar_t path[4096] = {};
const DWORD length = GetModuleFileNameW(nullptr, path, std::size(path));
if (length == 0 || length >= std::size(path))
return nullptr;
bool helper_file_path(wchar_t *path, size_t capacity, const wchar_t *name) {
const DWORD length = GetModuleFileNameW(nullptr, path, capacity);
if (length == 0 || length >= capacity)
return false;
wchar_t *separator = nullptr;
for (wchar_t *cursor = path; *cursor; ++cursor)
if (*cursor == L'\\' || *cursor == L'/') separator = cursor;
if (separator == nullptr)
return false;
const auto remaining = capacity - (separator + 1 - path);
if (remaining <= static_cast<size_t>(lstrlenW(name)))
return false;
lstrcpyW(separator + 1, name);
return true;
}
HMODULE load_menu_hook() {
wchar_t path[4096] = {};
return helper_file_path(path, std::size(path), L"wine-access-menu.dll")
? LoadLibraryW(path) : nullptr;
}
HANDLE start_menu_host32(HWND bridge) {
wchar_t path[4096] = {};
if (!helper_file_path(path, std::size(path), L"wine-access-menu-host32.exe"))
return nullptr;
const auto remaining = std::size(path) - (separator + 1 - path);
if (remaining <= static_cast<size_t>(lstrlenW(L"wine-access-menu.dll")))
wchar_t command[8192] = {};
wsprintfW(command, L"\"%s\" %lu %lu", path, GetCurrentProcessId(),
static_cast<DWORD>(reinterpret_cast<UINT_PTR>(bridge)));
STARTUPINFOW startup{};
startup.cb = sizeof(startup);
PROCESS_INFORMATION process{};
if (!CreateProcessW(path, command, nullptr, nullptr, FALSE, 0, nullptr,
nullptr, &startup, &process))
return nullptr;
lstrcpyW(separator + 1, L"wine-access-menu.dll");
return LoadLibraryW(path);
CloseHandle(process.hThread);
return process.hProcess;
}
void CALLBACK winevent_callback(HWINEVENTHOOK, DWORD event, HWND window,
@@ -650,6 +716,21 @@ void CALLBACK winevent_callback(HWINEVENTHOOK, DWORD event, HWND window,
return;
if (editReaderActive && GetPropW(window, EDIT_STATE_PROPERTY))
return;
if (HWND combo = combo_event_control(window, event)) {
// The combo's internal list can emit focus events even while the combo
// retains keyboard focus. Keep a single source for focus and value updates.
GUITHREADINFO info{};
info.cbSize = sizeof(info);
HWND foreground = GetForegroundWindow();
if (foreground && GetGUIThreadInfo(GetWindowThreadProcessId(foreground, nullptr), &info) &&
!(info.flags & GUI_INMENUMODE) && info.hwndFocus == combo) {
if (event == EVENT_OBJECT_FOCUS && window == combo)
present_standard_focus(combo);
else
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)) {
@@ -920,6 +1001,7 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) {
HHOOK inputHook = nullptr;
HMODULE menuModule = nullptr;
HWND menuBridge = nullptr;
HANDLE menuHost32 = nullptr;
if (dialogReaderEnabled) {
hook = SetWinEventHook(EVENT_MIN, EVENT_MAX, nullptr, winevent_callback, 0,
0, WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS);
@@ -943,10 +1025,16 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) {
if (menuHook == nullptr)
std::fprintf(stderr, "wine-access: native menu hook unavailable (error %lu)\n",
static_cast<unsigned long>(GetLastError()));
if (menuBridge != nullptr) {
menuHost32 = start_menu_host32(menuBridge);
if (menuHost32 == nullptr)
std::fprintf(stderr, "wine-access: 32-bit menu host unavailable (error %lu)\n",
static_cast<unsigned long>(GetLastError()));
}
}
MSG message{};
// Native lists, tabs and trees can omit selection WinEvents in Wine. Observe
// the focused item with scalar queries; retrieve text only when it changes.
// Native lists, tabs, trees and combo boxes can omit change WinEvents in Wine.
// Observe only the foreground control; never read background selections.
const UINT_PTR listTimer = dialogReaderEnabled ? SetTimer(nullptr, 0, 100, nullptr) : 0;
while (GetMessageW(&message, nullptr, 0, 0) > 0) {
if (listTimer && message.message == WM_TIMER && message.hwnd == nullptr &&
@@ -966,6 +1054,15 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) {
UnhookWindowsHookEx(inputHook);
if (menuBridge != nullptr)
DestroyWindow(menuBridge);
if (menuHost32 != nullptr) {
// Destroying the bridge ends the host's loop. Parent-process death also
// stops it, so a reader crash cannot leave an orphaned global menu hook.
if (WaitForSingleObject(menuHost32, 1000) == WAIT_TIMEOUT) {
TerminateProcess(menuHost32, 0);
WaitForSingleObject(menuHost32, 1000);
}
CloseHandle(menuHost32);
}
if (menuModule != nullptr)
FreeLibrary(menuModule);
if (controllerActive) {
+4
View File
@@ -1,6 +1,8 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "menu_bridge.h"
#ifndef WINE_ACCESS_MENU_ONLY
#include "edit_hook.h"
#endif
static void menu_text(WCHAR *output, UINT capacity, const WCHAR *input) {
UINT used = 0;
@@ -20,7 +22,9 @@ __declspec(dllexport) LRESULT CALLBACK WineAccessMenuHook(int code, WPARAM wp,
LPARAM lp) {
if (code >= 0) {
const CWPSTRUCT *message = (const CWPSTRUCT *)lp;
#ifndef WINE_ACCESS_MENU_ONLY
if (message->message == WM_SETFOCUS) wine_access_watch_edit(message->hwnd);
#endif
if (message->message == WM_MENUSELECT || message->message == WM_EXITMENULOOP) {
HWND bridge = FindWindowW(WINE_ACCESS_MENU_CLASS, NULL);
HWND owner = GetAncestor(message->hwnd, GA_ROOT);
+70
View File
@@ -0,0 +1,70 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "menu_bridge.h"
#include <stdlib.h>
#include <stdio.h>
static DWORD foreground_thread32(void) {
DWORD pid = 0;
DWORD thread = GetWindowThreadProcessId(GetForegroundWindow(), &pid);
HANDLE process = pid ? OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid) : NULL;
BOOL wow64 = FALSE;
BOOL known = process && IsWow64Process(process, &wow64);
if (process) CloseHandle(process);
return known && wow64 ? thread : 0;
}
// Hook DLLs must match the application's architecture. This small PE host
// supplies the 32-bit menu hook without requiring 32-bit Linux GLib/Winelib.
int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR args, int show) {
(void)instance; (void)previous; (void)show;
char *end;
DWORD parentId = strtoul(args, &end, 10);
HWND bridge = (HWND)(UINT_PTR)strtoul(end, &end, 10);
DWORD bridgeProcess = 0;
GetWindowThreadProcessId(bridge, &bridgeProcess);
if (!parentId || bridgeProcess != parentId) return 1;
HANDLE parent = OpenProcess(SYNCHRONIZE, FALSE, parentId);
if (!parent) return 1;
WCHAR path[4096] = {0};
DWORD length = GetModuleFileNameW(NULL, path, 4096);
WCHAR *name = NULL;
for (DWORD i = 0; i < length && i < 4096; ++i)
if (path[i] == L'\\' || path[i] == L'/') name = path + i + 1;
if (!length || length >= 4096 || !name ||
name - path + lstrlenW(L"wine-access-menu32.dll") >= 4096) {
CloseHandle(parent);
return 1;
}
lstrcpyW(name, L"wine-access-menu32.dll");
HMODULE module = LoadLibraryW(path);
HOOKPROC callback = module ? (HOOKPROC)GetProcAddress(module, "WineAccessMenuHook") : NULL;
HHOOK hook = NULL;
DWORD hookedThread = 0;
if (!callback)
fprintf(stderr, "wine-access: 32-bit menu hook unavailable (error %lu)\n", GetLastError());
if (callback) {
while (IsWindow(bridge)) {
// Wine's mixed-architecture global hook chain can suppress the 64-bit
// hook. Attach only to the foreground 32-bit UI thread, and release it
// when focus moves elsewhere. The 64-bit helper keeps its global hook.
DWORD thread = foreground_thread32();
if (thread != hookedThread) {
if (hook) UnhookWindowsHookEx(hook);
hook = thread ? SetWindowsHookExW(WH_CALLWNDPROC, callback, module, thread) : NULL;
hookedThread = hook ? thread : 0;
}
DWORD result = MsgWaitForMultipleObjects(1, &parent, FALSE, 100, QS_ALLINPUT);
if (result == WAIT_OBJECT_0 || result == WAIT_FAILED) break;
MSG message;
while (PeekMessageW(&message, NULL, 0, 0, PM_REMOVE)) {
TranslateMessage(&message);
DispatchMessageW(&message);
}
}
if (hook) UnhookWindowsHookEx(hook);
}
if (module) FreeLibrary(module);
CloseHandle(parent);
return callback ? 0 : 1;
}
+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])