More fixes for menus and combo boxes.
This commit is contained in:
1 parent
67ebc28d15
commit
872388a59b
11 files changed
+392
-35
No files matched your search
+17
-1
@@ -8,6 +8,7 @@ find_package(PkgConfig REQUIRED)
|
|||||||
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
|
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
|
||||||
find_program(WIDL_EXECUTABLE widl REQUIRED)
|
find_program(WIDL_EXECUTABLE widl REQUIRED)
|
||||||
find_program(MINGW_C_COMPILER x86_64-w64-mingw32-gcc 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
|
# A PE hook DLL can be loaded into ordinary Wine applications without any
|
||||||
# GLib dependencies. Keep it alongside the Winelib helper.
|
# GLib dependencies. Keep it alongside the Winelib helper.
|
||||||
@@ -21,6 +22,19 @@ add_custom_command(OUTPUT "${menu_hook}"
|
|||||||
VERBATIM)
|
VERBATIM)
|
||||||
add_custom_target(wine-access-menu ALL DEPENDS "${menu_hook}")
|
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_dir "${CMAKE_CURRENT_BINARY_DIR}/generated")
|
||||||
set(generated_header "${generated_dir}/nvdaController.h")
|
set(generated_header "${generated_dir}/nvdaController.h")
|
||||||
set(generated_server "${generated_dir}/nvdaController_s.c")
|
set(generated_server "${generated_dir}/nvdaController_s.c")
|
||||||
@@ -34,7 +48,7 @@ add_custom_command(
|
|||||||
VERBATIM)
|
VERBATIM)
|
||||||
|
|
||||||
add_executable(cthulhu-wine-access main.cpp list_view.cpp native_items.cpp "${generated_server}")
|
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_compile_features(cthulhu-wine-access PRIVATE cxx_std_20)
|
||||||
target_include_directories(cthulhu-wine-access PRIVATE "${generated_dir}")
|
target_include_directories(cthulhu-wine-access PRIVATE "${generated_dir}")
|
||||||
target_link_libraries(cthulhu-wine-access PRIVATE
|
target_link_libraries(cthulhu-wine-access PRIVATE
|
||||||
@@ -62,4 +76,6 @@ install(PROGRAMS
|
|||||||
install(FILES
|
install(FILES
|
||||||
"${CMAKE_CURRENT_BINARY_DIR}/cthulhu-wine-access.exe.so"
|
"${CMAKE_CURRENT_BINARY_DIR}/cthulhu-wine-access.exe.so"
|
||||||
"${menu_hook}"
|
"${menu_hook}"
|
||||||
|
"${menu_hook32}"
|
||||||
|
"${menu_host32}"
|
||||||
DESTINATION "${WINE_ACCESS_INSTALL_DIR}")
|
DESTINATION "${WINE_ACCESS_INSTALL_DIR}")
|
||||||
@@ -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);
|
- Make and native GCC/G++ (the script uses CMake's default Makefile generator);
|
||||||
- `pkg-config` and the GLib/GIO development files;
|
- `pkg-config` and the GLib/GIO development files;
|
||||||
- Wine development tools: `winegcc`, `wineg++`, and `widl`;
|
- 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).
|
(`mingw-w64-gcc` on Arch Linux).
|
||||||
|
|
||||||
The target is a Winelib executable. A 32-bit build is not provided by this
|
The D-Bus helper is a 64-bit Winelib executable. A small 32-bit Windows
|
||||||
project yet; it needs a 32-bit Winegcc and 32-bit GLib/GIO development setup.
|
companion supplies menu hooks for 32-bit applications, without requiring
|
||||||
|
32-bit Linux GLib/GIO development files.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
@@ -54,7 +55,7 @@ development packages. Removal does not require these dependencies.
|
|||||||
Use `./build.sh -r` to remove the local installation, or
|
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
|
`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
|
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.
|
files and leaves other directory contents untouched.
|
||||||
|
|
||||||
Alternative build and install locations may be supplied as positional
|
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
|
||||||
$prefix/libexec/cthulhu/wine/cthulhu-wine-access.exe.so
|
$prefix/libexec/cthulhu/wine/cthulhu-wine-access.exe.so
|
||||||
$prefix/libexec/cthulhu/wine/wine-access-menu.dll
|
$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
|
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
|
Custom-drawn, DirectX, and game-specific accessibility APIs remain the
|
||||||
responsibility of the application or its accessibility middleware.
|
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
|
`WM_MENUSELECT` inside Wine applications and sends menu labels, displayed
|
||||||
shortcuts, submenu roles, and checked/unavailable states to the helper. The
|
shortcuts, submenu roles, and checked/unavailable states to the helper. The
|
||||||
helper queues these messages before forwarding them over D-Bus, suppresses
|
helper queues these messages before forwarding them over D-Bus, suppresses
|
||||||
repeated selections, and restores standard-control focus reporting when menus
|
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
|
close. The hooks apply to the helper's Wine desktop and prefix and are removed
|
||||||
on normal shutdown. It is disabled by `--no-dialog-reader`.
|
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
|
document area and `WindowsForms10.EDIT.app.*` controls. Windows Forms fields
|
||||||
use visible sibling labels immediately to their left, even when the labels
|
use visible sibling labels immediately to their left, even when the labels
|
||||||
were created after the fields. Protected fields report their label and
|
were created after the fields. Protected fields report their label and
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ else
|
|||||||
fi
|
fi
|
||||||
prefix=${2:-"$defaultPrefix"}
|
prefix=${2:-"$defaultPrefix"}
|
||||||
installDir="${prefix%/}/libexec/cthulhu/wine"
|
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() {
|
has_installation() {
|
||||||
local candidateDir=$1
|
local candidateDir=$1
|
||||||
@@ -69,6 +70,7 @@ check_prerequisites() {
|
|||||||
cmake:cmake make:make gcc:gcc g++:gcc pkg-config:pkgconf
|
cmake:cmake make:make gcc:gcc g++:gcc pkg-config:pkgconf
|
||||||
winegcc:wine wineg++:wine widl:wine
|
winegcc:wine wineg++:wine widl:wine
|
||||||
x86_64-w64-mingw32-gcc:mingw-w64-gcc
|
x86_64-w64-mingw32-gcc:mingw-w64-gcc
|
||||||
|
i686-w64-mingw32-gcc:mingw-w64-gcc
|
||||||
)
|
)
|
||||||
|
|
||||||
for requirement in "${requirements[@]}"; do
|
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
|
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
|
under `/usr/libexec/cthulhu/wine/`. It provides and conflicts with the older
|
||||||
`cthulhu-wine-access` helper, including `cthulhu-wine-access-git`, so pacman
|
`cthulhu-wine-access` helper, including `cthulhu-wine-access-git`, so pacman
|
||||||
can replace that package in the installation transaction.
|
can replace that package in the installation transaction.
|
||||||
|
|||||||
@@ -41,6 +41,30 @@ bool lastListSelected = false;
|
|||||||
int lastListChecked = -1;
|
int lastListChecked = -1;
|
||||||
HWND lastNativeControl = nullptr;
|
HWND lastNativeControl = nullptr;
|
||||||
NativeItemKey lastNativeItem;
|
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
|
// 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.
|
// 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.name = to_utf8(label);
|
||||||
control.role = "combo box";
|
control.role = "combo box";
|
||||||
control.value = to_utf8(text);
|
control.value = to_utf8(text);
|
||||||
|
lastComboControl = window;
|
||||||
|
lastComboValue = control.value;
|
||||||
return control;
|
return control;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,8 +453,14 @@ void poll_control_focus(bool refresh = false) {
|
|||||||
GUITHREADINFO info{};
|
GUITHREADINFO info{};
|
||||||
info.cbSize = sizeof(info);
|
info.cbSize = sizeof(info);
|
||||||
HWND foreground = GetForegroundWindow();
|
HWND foreground = GetForegroundWindow();
|
||||||
if (foreground && GetGUIThreadInfo(GetWindowThreadProcessId(foreground, nullptr), &info) &&
|
const bool focused = foreground &&
|
||||||
!(info.flags & GUI_INMENUMODE) && is_native_item_control(info.hwndFocus)) {
|
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;
|
lastListControl = nullptr;
|
||||||
lastListIndex = -1;
|
lastListIndex = -1;
|
||||||
lastListCount = 0;
|
lastListCount = 0;
|
||||||
@@ -446,8 +478,7 @@ void poll_control_focus(bool refresh = false) {
|
|||||||
}
|
}
|
||||||
lastNativeControl = nullptr;
|
lastNativeControl = nullptr;
|
||||||
lastNativeItem = {};
|
lastNativeItem = {};
|
||||||
if (foreground && GetGUIThreadInfo(GetWindowThreadProcessId(foreground, nullptr), &info) &&
|
if (focused && is_list_view(info.hwndFocus)) {
|
||||||
!(info.flags & GUI_INMENUMODE) && is_list_view(info.hwndFocus)) {
|
|
||||||
int count = 0;
|
int count = 0;
|
||||||
const int index = list_view_index(info.hwndFocus, &count);
|
const int index = list_view_index(info.hwndFocus, &count);
|
||||||
DWORD_PTR state = 0;
|
DWORD_PTR state = 0;
|
||||||
@@ -471,6 +502,19 @@ void poll_control_focus(bool refresh = false) {
|
|||||||
lastListCount = 0;
|
lastListCount = 0;
|
||||||
lastListSelected = false;
|
lastListSelected = false;
|
||||||
lastListChecked = -1;
|
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) {
|
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);
|
return DefWindowProcW(window, message, wp, lp);
|
||||||
}
|
}
|
||||||
|
|
||||||
HMODULE load_menu_hook() {
|
bool helper_file_path(wchar_t *path, size_t capacity, const wchar_t *name) {
|
||||||
wchar_t path[4096] = {};
|
const DWORD length = GetModuleFileNameW(nullptr, path, capacity);
|
||||||
const DWORD length = GetModuleFileNameW(nullptr, path, std::size(path));
|
if (length == 0 || length >= capacity)
|
||||||
if (length == 0 || length >= std::size(path))
|
return false;
|
||||||
return nullptr;
|
|
||||||
wchar_t *separator = nullptr;
|
wchar_t *separator = nullptr;
|
||||||
for (wchar_t *cursor = path; *cursor; ++cursor)
|
for (wchar_t *cursor = path; *cursor; ++cursor)
|
||||||
if (*cursor == L'\\' || *cursor == L'/') separator = cursor;
|
if (*cursor == L'\\' || *cursor == L'/') separator = cursor;
|
||||||
if (separator == nullptr)
|
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;
|
return nullptr;
|
||||||
const auto remaining = std::size(path) - (separator + 1 - path);
|
wchar_t command[8192] = {};
|
||||||
if (remaining <= static_cast<size_t>(lstrlenW(L"wine-access-menu.dll")))
|
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;
|
return nullptr;
|
||||||
lstrcpyW(separator + 1, L"wine-access-menu.dll");
|
CloseHandle(process.hThread);
|
||||||
return LoadLibraryW(path);
|
return process.hProcess;
|
||||||
}
|
}
|
||||||
|
|
||||||
void CALLBACK winevent_callback(HWINEVENTHOOK, DWORD event, HWND window,
|
void CALLBACK winevent_callback(HWINEVENTHOOK, DWORD event, HWND window,
|
||||||
@@ -650,6 +716,21 @@ void CALLBACK winevent_callback(HWINEVENTHOOK, DWORD event, HWND window,
|
|||||||
return;
|
return;
|
||||||
if (editReaderActive && GetPropW(window, EDIT_STATE_PROPERTY))
|
if (editReaderActive && GetPropW(window, EDIT_STATE_PROPERTY))
|
||||||
return;
|
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
|
// 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.
|
// their rows. Use the standard-control query even when that name is present.
|
||||||
if (is_list_view(window) || is_native_item_control(window)) {
|
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;
|
HHOOK inputHook = nullptr;
|
||||||
HMODULE menuModule = nullptr;
|
HMODULE menuModule = nullptr;
|
||||||
HWND menuBridge = nullptr;
|
HWND menuBridge = nullptr;
|
||||||
|
HANDLE menuHost32 = nullptr;
|
||||||
if (dialogReaderEnabled) {
|
if (dialogReaderEnabled) {
|
||||||
hook = SetWinEventHook(EVENT_MIN, EVENT_MAX, nullptr, winevent_callback, 0,
|
hook = SetWinEventHook(EVENT_MIN, EVENT_MAX, nullptr, winevent_callback, 0,
|
||||||
0, WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS);
|
0, WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS);
|
||||||
@@ -943,10 +1025,16 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) {
|
|||||||
if (menuHook == nullptr)
|
if (menuHook == nullptr)
|
||||||
std::fprintf(stderr, "wine-access: native menu hook unavailable (error %lu)\n",
|
std::fprintf(stderr, "wine-access: native menu hook unavailable (error %lu)\n",
|
||||||
static_cast<unsigned long>(GetLastError()));
|
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{};
|
MSG message{};
|
||||||
// Native lists, tabs and trees can omit selection WinEvents in Wine. Observe
|
// Native lists, tabs, trees and combo boxes can omit change WinEvents in Wine.
|
||||||
// the focused item with scalar queries; retrieve text only when it changes.
|
// Observe only the foreground control; never read background selections.
|
||||||
const UINT_PTR listTimer = dialogReaderEnabled ? SetTimer(nullptr, 0, 100, nullptr) : 0;
|
const UINT_PTR listTimer = dialogReaderEnabled ? SetTimer(nullptr, 0, 100, nullptr) : 0;
|
||||||
while (GetMessageW(&message, nullptr, 0, 0) > 0) {
|
while (GetMessageW(&message, nullptr, 0, 0) > 0) {
|
||||||
if (listTimer && message.message == WM_TIMER && message.hwnd == nullptr &&
|
if (listTimer && message.message == WM_TIMER && message.hwnd == nullptr &&
|
||||||
@@ -966,6 +1054,15 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) {
|
|||||||
UnhookWindowsHookEx(inputHook);
|
UnhookWindowsHookEx(inputHook);
|
||||||
if (menuBridge != nullptr)
|
if (menuBridge != nullptr)
|
||||||
DestroyWindow(menuBridge);
|
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)
|
if (menuModule != nullptr)
|
||||||
FreeLibrary(menuModule);
|
FreeLibrary(menuModule);
|
||||||
if (controllerActive) {
|
if (controllerActive) {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
#include "menu_bridge.h"
|
#include "menu_bridge.h"
|
||||||
|
#ifndef WINE_ACCESS_MENU_ONLY
|
||||||
#include "edit_hook.h"
|
#include "edit_hook.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
static void menu_text(WCHAR *output, UINT capacity, const WCHAR *input) {
|
static void menu_text(WCHAR *output, UINT capacity, const WCHAR *input) {
|
||||||
UINT used = 0;
|
UINT used = 0;
|
||||||
@@ -20,7 +22,9 @@ __declspec(dllexport) LRESULT CALLBACK WineAccessMenuHook(int code, WPARAM wp,
|
|||||||
LPARAM lp) {
|
LPARAM lp) {
|
||||||
if (code >= 0) {
|
if (code >= 0) {
|
||||||
const CWPSTRUCT *message = (const CWPSTRUCT *)lp;
|
const CWPSTRUCT *message = (const CWPSTRUCT *)lp;
|
||||||
|
#ifndef WINE_ACCESS_MENU_ONLY
|
||||||
if (message->message == WM_SETFOCUS) wine_access_watch_edit(message->hwnd);
|
if (message->message == WM_SETFOCUS) wine_access_watch_edit(message->hwnd);
|
||||||
|
#endif
|
||||||
if (message->message == WM_MENUSELECT || message->message == WM_EXITMENULOOP) {
|
if (message->message == WM_MENUSELECT || message->message == WM_EXITMENULOOP) {
|
||||||
HWND bridge = FindWindowW(WINE_ACCESS_MENU_CLASS, NULL);
|
HWND bridge = FindWindowW(WINE_ACCESS_MENU_CLASS, NULL);
|
||||||
HWND owner = GetAncestor(message->hwnd, GA_ROOT);
|
HWND owner = GetAncestor(message->hwnd, GA_ROOT);
|
||||||
|
|||||||
@@ -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
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Build the project, then run `python3 tests/menu_integration.py --build-dir build`
|
Build the project, then run `python3 tests/menu_integration.py --build-dir build`
|
||||||
from the repository root in a graphical session. Dependencies are Wine,
|
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
|
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
|
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
|
acceptance gates. Run Xvfb fixtures sequentially; concurrent automatic display
|
||||||
allocation can collide.
|
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
|
## Notepad++ / Scintilla checks
|
||||||
|
|
||||||
Use a locally available 64-bit Notepad++ executable:
|
Use a locally available 64-bit Notepad++ executable:
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ static LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wp, LPARAM
|
|||||||
case 10: focus_control(combo); break;
|
case 10: focus_control(combo); break;
|
||||||
case 11:
|
case 11:
|
||||||
SendMessageW(combo, CB_SETCURSEL, 1, 0);
|
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;
|
break;
|
||||||
case 12: focus_control(edit); break;
|
case 12: focus_control(edit); break;
|
||||||
case 13: focus_control(atlTabs); break;
|
case 13: focus_control(atlTabs); break;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -17,24 +17,28 @@ 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", "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("--notepadpp-exe", type=Path)
|
||||||
|
parser.add_argument("--goldwave-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", "delphi", "controls"):
|
if args.is32Bit and args.fixture not in ("menu", "list", "delphi", "controls"):
|
||||||
parser.error("32-bit checks currently require --fixture list, delphi or 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"):
|
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()):
|
||||||
parser.error("--fixture scintilla requires --notepadpp-exe pointing to a 64-bit Notepad++ executable")
|
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()
|
build = Path(args.build_dir).resolve()
|
||||||
if not args.session:
|
if not args.session:
|
||||||
sys.exit(subprocess.call(["dbus-run-session", "--", sys.executable,
|
sys.exit(subprocess.call(["dbus-run-session", "--", sys.executable,
|
||||||
__file__, "--session", "--build-dir", str(build),
|
__file__, "--session", "--build-dir", str(build),
|
||||||
"--fixture", args.fixture,
|
"--fixture", args.fixture,
|
||||||
*(["--notepadpp-exe", str(args.notepadpp_exe.resolve())] if args.notepadpp_exe else []),
|
*(["--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 []),
|
*(["--32-bit"] if args.is32Bit else []),
|
||||||
*(["--late-helper"] if args.late_helper else []),
|
*(["--late-helper"] if args.late_helper else []),
|
||||||
*(["--restart-helper"] if args.restart_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:
|
with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
|
||||||
env = os.environ.copy()
|
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")
|
WINEDLLOVERRIDES="mscoree,mshtml=", CTHULHU_WINE_ACCESS_MANAGED="1")
|
||||||
ready = Path(temporary) / "ready"
|
ready = Path(temporary) / "ready"
|
||||||
env["WINE_ACCESS_FIXTURE_READY"] = "Z:" + str(ready).replace("/", "\\")
|
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":
|
if args.fixture == "scintilla":
|
||||||
# Copy the application to isolate its configuration as well as its
|
# Copy the application to isolate its configuration as well as its
|
||||||
# document. Never open or edit files in the user's running instance.
|
# 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_NOTEPADPP_EXE"] = "Z:" + str(appDir / args.notepadpp_exe.name).replace("/", "\\")
|
||||||
env["WINE_ACCESS_SCINTILLA_DOCUMENT"] = "Z:" + str(document).replace("/", "\\")
|
env["WINE_ACCESS_SCINTILLA_DOCUMENT"] = "Z:" + str(document).replace("/", "\\")
|
||||||
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 or args.fixture == "goldwave" 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":
|
if args.fixture == "delphi":
|
||||||
sourceName = "list"
|
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_WINFORMS_FIXTURE"] if args.fixture == "winforms" else []),
|
||||||
*(["-DWINE_ACCESS_DELPHI_FIXTURE"] if args.fixture == "delphi" 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", "-ladvapi32"], check=True)
|
||||||
helper = None
|
helper = None
|
||||||
app = None
|
app = None
|
||||||
try:
|
try:
|
||||||
@@ -121,7 +129,7 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
|
|||||||
restarted = True
|
restarted = True
|
||||||
time.sleep(0.01)
|
time.sleep(0.01)
|
||||||
assert app.poll() == 0, "fixture failed or timed out"
|
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:
|
if args.restart_helper:
|
||||||
assert restarted, "fixture never triggered a helper restart"
|
assert restarted, "fixture never triggered a helper restart"
|
||||||
for _ in range(20):
|
for _ in range(20):
|
||||||
@@ -134,6 +142,18 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
|
|||||||
focused = event[0]
|
focused = event[0]
|
||||||
elif event[1] == "text":
|
elif event[1] == "text":
|
||||||
assert event[0] == focused, f"text sent without establishing focus: {event!r}"
|
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":
|
if args.fixture == "controls":
|
||||||
for event in events:
|
for event in events:
|
||||||
print(event[1:9])
|
print(event[1:9])
|
||||||
|
|||||||
Reference in new issue
Block a user