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

+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) {