More accessibility fixes using foobar2000 as a test.
This commit is contained in:
1 parent
22f50402c4
commit
5f78168c57
9 files changed
+407
-14
No files matched your search
+1
-1
@@ -32,7 +32,7 @@ add_custom_command(
|
||||
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/nvdaController.idl"
|
||||
VERBATIM)
|
||||
|
||||
add_executable(cthulhu-wine-access main.cpp list_view.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)
|
||||
target_compile_features(cthulhu-wine-access PRIVATE cxx_std_20)
|
||||
target_include_directories(cthulhu-wine-access PRIVATE "${generated_dir}")
|
||||
|
||||
@@ -139,6 +139,15 @@ when Wine omits selection events. Up to eight columns and 1023 UTF-16 units
|
||||
per column are read. This does not add 32-bit menu or editor hooks, column
|
||||
navigation, or routable object review.
|
||||
|
||||
Native tab controls report their focused caption and position; tree controls
|
||||
report the selected node and its expansion state. A bounded focus poll follows
|
||||
navigation when Wine omits selection events. These queries work with 32-bit
|
||||
and 64-bit controls, including standard controls registered with `ATL:` names.
|
||||
Named generic MSAA clients no longer hide known native button/combo-box roles
|
||||
and values. Edit and combo-box labels can be visible siblings to the left,
|
||||
even when the labels were created after the fields. Application-owned MSAA
|
||||
lists retain their own names, roles and item data.
|
||||
|
||||
This is editing feedback, not a full text-review provider: Say All, review-cursor
|
||||
commands and braille routing into Wine text are not implemented. Typing echo
|
||||
remains the reader's responsibility. RichEdit and 32-bit editor hooks are not
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
// not edits merely because their name contains EDIT somewhere.
|
||||
static inline BOOL wine_access_class_matches(const WCHAR *className, const WCHAR *nativeName) {
|
||||
if (lstrcmpiW(className, nativeName) == 0) return TRUE;
|
||||
if (lstrlenW(className) > 4 &&
|
||||
CompareStringOrdinal(className, 4, L"ATL:", 4, TRUE) == CSTR_EQUAL &&
|
||||
lstrcmpiW(className + 4, nativeName) == 0) return TRUE;
|
||||
WCHAR expected[128] = L"WindowsForms10.";
|
||||
if (lstrlenW(nativeName) > 80) return FALSE;
|
||||
lstrcatW(expected, nativeName);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "msaa_states.h"
|
||||
#include "list_view.h"
|
||||
#include "control_class.h"
|
||||
#include "native_items.h"
|
||||
|
||||
#include <commctrl.h>
|
||||
#include <gio/gio.h>
|
||||
@@ -37,6 +38,8 @@ HWND lastListControl = nullptr;
|
||||
int lastListIndex = -1;
|
||||
int lastListCount = 0;
|
||||
bool lastListSelected = false;
|
||||
HWND lastNativeControl = nullptr;
|
||||
NativeItemKey lastNativeItem;
|
||||
|
||||
// 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.
|
||||
@@ -163,11 +166,16 @@ void find_control_label(HWND window, wchar_t *text, int capacity) {
|
||||
HWND previous = window;
|
||||
wchar_t className[64] = {};
|
||||
text[0] = L'\0';
|
||||
// Managed controls need not be created in label/control order. Prefer a
|
||||
const bool knownClass = GetClassNameW(window, className, std::size(className));
|
||||
const bool managedEdit = knownClass && lstrcmpiW(className, L"Edit") != 0 &&
|
||||
wine_access_class_matches(className, L"Edit");
|
||||
// Controls need not be created in label/control order. Prefer a
|
||||
// visible static label immediately to the left on the same row, among
|
||||
// siblings only. Keep the existing native-control ordering below.
|
||||
if (GetClassNameW(window, className, std::size(className)) &&
|
||||
lstrcmpiW(className, L"Edit") != 0 && wine_access_class_matches(className, L"Edit")) {
|
||||
if (knownClass &&
|
||||
(wine_access_class_matches(className, L"Edit") ||
|
||||
wine_access_class_matches(className, L"ComboBox") ||
|
||||
wine_access_class_matches(className, L"ComboBoxEx32"))) {
|
||||
RECT controlRect{};
|
||||
if (GetWindowRect(window, &controlRect)) {
|
||||
HWND label = nullptr;
|
||||
@@ -188,9 +196,11 @@ void find_control_label(HWND window, wchar_t *text, int capacity) {
|
||||
}
|
||||
if (label && get_control_text(label, text, capacity)) {
|
||||
tidy_text(text, true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
// Retain ordered labels for controls without a same-row sibling.
|
||||
if (managedEdit) return;
|
||||
}
|
||||
for (int hop = 0; hop < 3; ++hop) {
|
||||
previous = GetWindow(previous, GW_HWNDPREV);
|
||||
@@ -317,6 +327,18 @@ ControlDescription describe_standard_control(HWND window) {
|
||||
return control;
|
||||
}
|
||||
|
||||
if (is_native_item_control(window)) {
|
||||
const auto item = read_native_item(window);
|
||||
control.name = item.text;
|
||||
control.role = item.role;
|
||||
control.states = item.states;
|
||||
control.position = item.position;
|
||||
control.count = item.count;
|
||||
lastNativeControl = window;
|
||||
lastNativeItem = item.key;
|
||||
return control;
|
||||
}
|
||||
|
||||
if (wine_access_class_matches(className, L"ListBox") ||
|
||||
wine_access_class_matches(className, L"ComboLBox")) {
|
||||
find_control_label(window, label, std::size(label));
|
||||
@@ -384,10 +406,28 @@ bool present_standard_focus(HWND window) {
|
||||
control, window_title(window));
|
||||
}
|
||||
|
||||
void poll_list_focus(bool refresh = false) {
|
||||
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)) {
|
||||
lastListControl = nullptr;
|
||||
lastListIndex = -1;
|
||||
lastListCount = 0;
|
||||
lastListSelected = false;
|
||||
const auto key = native_item_key(info.hwndFocus);
|
||||
if (info.hwndFocus != lastNativeControl) {
|
||||
present_standard_focus(info.hwndFocus);
|
||||
} else if (refresh || !(key == lastNativeItem)) {
|
||||
const auto control = describe_standard_control(info.hwndFocus);
|
||||
present_accessible_event(source_id(info.hwndFocus, "standard"), "selection",
|
||||
control, window_title(info.hwndFocus));
|
||||
}
|
||||
return;
|
||||
}
|
||||
lastNativeControl = nullptr;
|
||||
lastNativeItem = {};
|
||||
if (foreground && GetGUIThreadInfo(GetWindowThreadProcessId(foreground, nullptr), &info) &&
|
||||
!(info.flags & GUI_INMENUMODE) && is_list_view(info.hwndFocus)) {
|
||||
int count = 0;
|
||||
@@ -587,9 +627,9 @@ void CALLBACK winevent_callback(HWINEVENTHOOK, DWORD event, HWND window,
|
||||
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)) {
|
||||
if (is_list_view(window) || is_native_item_control(window)) {
|
||||
if (event == EVENT_OBJECT_FOCUS) present_standard_focus(window);
|
||||
else poll_list_focus(event == EVENT_OBJECT_NAMECHANGE || event == EVENT_OBJECT_VALUECHANGE);
|
||||
else poll_control_focus(event == EVENT_OBJECT_NAMECHANGE || event == EVENT_OBJECT_VALUECHANGE);
|
||||
return;
|
||||
}
|
||||
IAccessible *accessible = nullptr;
|
||||
@@ -628,6 +668,8 @@ void CALLBACK winevent_callback(HWINEVENTHOOK, DWORD event, HWND window,
|
||||
VariantClear(&state);
|
||||
LONG childCount = 0;
|
||||
accessible->get_accChildCount(&childCount);
|
||||
const bool nativeClient = child.vt == VT_I4 && child.lVal == CHILDID_SELF &&
|
||||
(roleText.empty() || roleId == ROLE_SYSTEM_CLIENT);
|
||||
accessible->Release();
|
||||
VariantClear(&child);
|
||||
|
||||
@@ -667,6 +709,19 @@ void CALLBACK winevent_callback(HWINEVENTHOOK, DWORD event, HWND window,
|
||||
control.states = stateText;
|
||||
control.description = description;
|
||||
control.count = static_cast<int>(childCount);
|
||||
// Wine can return named generic clients even for standard buttons and
|
||||
// combo boxes. Their names alone must not hide the native role/value.
|
||||
// Application-owned list items and other meaningful MSAA roles stay intact.
|
||||
if (nativeClient) {
|
||||
const auto native = describe_standard_control(window);
|
||||
if (!native.role.empty()) {
|
||||
control = native;
|
||||
if (control.name.empty()) control.name = name;
|
||||
if (control.states.empty()) control.states = stateText;
|
||||
if (control.description.empty()) control.description = description;
|
||||
if (password) control.value.clear();
|
||||
}
|
||||
}
|
||||
present_accessible_event(sourceId, eventType, control, windowTitle);
|
||||
}
|
||||
|
||||
@@ -865,13 +920,13 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) {
|
||||
static_cast<unsigned long>(GetLastError()));
|
||||
}
|
||||
MSG message{};
|
||||
// Native list views also omit row-selection WinEvents in Wine. Observe the
|
||||
// focused row with scalar queries; retrieve its text only when it changes.
|
||||
// Native lists, tabs and trees can omit selection WinEvents in Wine. Observe
|
||||
// the focused item with scalar queries; retrieve text only when it changes.
|
||||
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 &&
|
||||
message.wParam == listTimer) {
|
||||
poll_list_focus();
|
||||
poll_control_focus();
|
||||
continue;
|
||||
}
|
||||
TranslateMessage(&message);
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#include "native_items.h"
|
||||
#include "control_class.h"
|
||||
#include <commctrl.h>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <iterator>
|
||||
|
||||
namespace {
|
||||
bool query(HWND window, UINT message, WPARAM wp, LPARAM lp, DWORD_PTR *result) {
|
||||
return SendMessageTimeoutW(window, message, wp, lp, SMTO_ABORTIFHUNG,
|
||||
100, result) != 0;
|
||||
}
|
||||
bool is_tab(HWND window) {
|
||||
wchar_t className[128] = {};
|
||||
return GetClassNameW(window, className, std::size(className)) &&
|
||||
wine_access_class_matches(className, L"SysTabControl32");
|
||||
}
|
||||
struct Tab32 { uint32_t mask, state, stateMask, text; int32_t capacity; };
|
||||
struct Tab64 { uint32_t mask, state, stateMask, padding; uint64_t text; int32_t capacity; };
|
||||
struct Tree32 {
|
||||
uint32_t mask, item, state, stateMask, text;
|
||||
int32_t capacity, image, selectedImage, children;
|
||||
};
|
||||
struct Tree64 {
|
||||
uint32_t mask, padding;
|
||||
uint64_t item;
|
||||
uint32_t state, stateMask;
|
||||
uint64_t text;
|
||||
int32_t capacity, image, selectedImage, children;
|
||||
};
|
||||
static_assert(offsetof(Tab32, text) == 12 && offsetof(Tab64, text) == 16);
|
||||
static_assert(offsetof(Tree32, text) == 16 && offsetof(Tree64, text) == 24);
|
||||
constexpr size_t textOffset = 128;
|
||||
constexpr int textCapacity = 1024;
|
||||
constexpr size_t bufferSize = textOffset + textCapacity * sizeof(wchar_t);
|
||||
}
|
||||
|
||||
bool is_native_item_control(HWND window) {
|
||||
wchar_t className[128] = {};
|
||||
return GetClassNameW(window, className, std::size(className)) &&
|
||||
(wine_access_class_matches(className, L"SysTabControl32") ||
|
||||
wine_access_class_matches(className, L"SysTreeView32"));
|
||||
}
|
||||
|
||||
NativeItemKey native_item_key(HWND window) {
|
||||
NativeItemKey key;
|
||||
DWORD_PTR count = 0, item = 0, state = 0;
|
||||
if (is_tab(window)) {
|
||||
if (!query(window, TCM_GETITEMCOUNT, 0, 0, &count) || count == 0) return key;
|
||||
// Keyboard focus can move before selection in button-style tab controls.
|
||||
if (!query(window, TCM_GETCURFOCUS, 0, 0, &item) ||
|
||||
static_cast<LONG_PTR>(item) < 0)
|
||||
if (!query(window, TCM_GETCURSEL, 0, 0, &item)) return key;
|
||||
if (item >= count) return key;
|
||||
key.item = item + 1; // Zero means no item for both control types.
|
||||
DWORD_PTR selected = 0;
|
||||
if (query(window, TCM_GETCURSEL, 0, 0, &selected) && selected == item)
|
||||
key.state = TVIS_SELECTED;
|
||||
} else {
|
||||
if (!query(window, TVM_GETCOUNT, 0, 0, &count) ||
|
||||
!query(window, TVM_GETNEXTITEM, TVGN_CARET, 0, &item)) return key;
|
||||
key.item = item;
|
||||
if (item && query(window, TVM_GETITEMSTATE, item,
|
||||
TVIS_SELECTED | TVIS_EXPANDED, &state)) key.state = state;
|
||||
}
|
||||
key.count = static_cast<int>(count);
|
||||
return key;
|
||||
}
|
||||
|
||||
NativeItem read_native_item(HWND window) {
|
||||
NativeItem item;
|
||||
const bool tab = is_tab(window);
|
||||
item.role = tab ? "page tab" : "tree";
|
||||
const auto key = native_item_key(window);
|
||||
item.key = key;
|
||||
if (!key.item) return item;
|
||||
DWORD processId = 0;
|
||||
GetWindowThreadProcessId(window, &processId);
|
||||
HANDLE process = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_READ |
|
||||
PROCESS_VM_WRITE | PROCESS_QUERY_LIMITED_INFORMATION,
|
||||
FALSE, processId);
|
||||
if (!process) return item;
|
||||
BOOL wow64 = FALSE;
|
||||
if (!IsWow64Process(process, &wow64)) { CloseHandle(process); return item; }
|
||||
void *remote = VirtualAllocEx(process, nullptr, bufferSize,
|
||||
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
||||
if (!remote || (wow64 && reinterpret_cast<uintptr_t>(remote) > UINT32_MAX)) {
|
||||
if (remote) VirtualFreeEx(process, remote, 0, MEM_RELEASE);
|
||||
CloseHandle(process);
|
||||
return item;
|
||||
}
|
||||
BYTE buffer[bufferSize] = {};
|
||||
const auto address = reinterpret_cast<uintptr_t>(remote) + textOffset;
|
||||
if (tab && wow64) {
|
||||
const Tab32 data{TCIF_TEXT, 0, 0, static_cast<uint32_t>(address), textCapacity};
|
||||
memcpy(buffer, &data, sizeof(data));
|
||||
} else if (tab) {
|
||||
const Tab64 data{TCIF_TEXT, 0, 0, 0, address, textCapacity};
|
||||
memcpy(buffer, &data, sizeof(data));
|
||||
} else if (wow64) {
|
||||
const Tree32 data{TVIF_TEXT | TVIF_CHILDREN, static_cast<uint32_t>(key.item),
|
||||
0, 0, static_cast<uint32_t>(address), textCapacity, 0, 0, 0};
|
||||
memcpy(buffer, &data, sizeof(data));
|
||||
} else {
|
||||
const Tree64 data{TVIF_TEXT | TVIF_CHILDREN, 0, key.item, 0, 0,
|
||||
address, textCapacity, 0, 0, 0};
|
||||
memcpy(buffer, &data, sizeof(data));
|
||||
}
|
||||
DWORD_PTR result = 0;
|
||||
if (WriteProcessMemory(process, remote, buffer, sizeof(buffer), nullptr) &&
|
||||
query(window, tab ? TCM_GETITEMW : TVM_GETITEMW, tab ? key.item - 1 : 0,
|
||||
reinterpret_cast<LPARAM>(remote), &result) && result &&
|
||||
ReadProcessMemory(process, remote, buffer, sizeof(buffer), nullptr)) {
|
||||
wchar_t text[textCapacity] = {};
|
||||
memcpy(text, buffer + textOffset, sizeof(text));
|
||||
text[textCapacity - 1] = L'\0';
|
||||
char utf8[textCapacity * 4] = {};
|
||||
if (WideCharToMultiByte(CP_UTF8, 0, text, -1, utf8, sizeof(utf8), nullptr, nullptr))
|
||||
item.text = utf8;
|
||||
if (key.state & TVIS_SELECTED) item.states = "selected";
|
||||
if (tab) { item.position = static_cast<int>(key.item); item.count = key.count; }
|
||||
else {
|
||||
item.role = "tree item";
|
||||
int32_t children = 0;
|
||||
memcpy(&children, buffer + (wow64 ? offsetof(Tree32, children) : offsetof(Tree64, children)), sizeof(children));
|
||||
if (children != 0) {
|
||||
if (!item.states.empty()) item.states += ", ";
|
||||
item.states += key.state & TVIS_EXPANDED ? "expanded" : "collapsed";
|
||||
}
|
||||
}
|
||||
}
|
||||
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
|
||||
CloseHandle(process);
|
||||
return item;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#pragma once
|
||||
#include <windows.h>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
struct NativeItemKey {
|
||||
uintptr_t item = 0;
|
||||
int count = 0;
|
||||
DWORD state = 0;
|
||||
bool operator==(const NativeItemKey &) const = default;
|
||||
};
|
||||
struct NativeItem {
|
||||
NativeItemKey key;
|
||||
std::string text;
|
||||
std::string role;
|
||||
std::string states;
|
||||
int position = 0;
|
||||
int count = 0;
|
||||
};
|
||||
bool is_native_item_control(HWND window);
|
||||
NativeItemKey native_item_key(HWND window);
|
||||
NativeItem read_native_item(HWND window);
|
||||
@@ -134,6 +134,37 @@ Check arrows and Tab/Shift+Tab back to Install/Cancel. Also check a multirow
|
||||
native list, normal menus, editor navigation, switching to a native Linux
|
||||
application, and reader shutdown. Verify speech and physical braille separately.
|
||||
|
||||
## Native tabs, trees and named clients
|
||||
|
||||
```sh
|
||||
xvfb-run -a python3 tests/menu_integration.py --build-dir build --fixture controls
|
||||
xvfb-run -a python3 tests/menu_integration.py --build-dir build --fixture controls --32-bit
|
||||
```
|
||||
|
||||
These private fixtures exercise native and `ATL:SysTabControl32` tab captions,
|
||||
positions and arrow navigation, tree selection/expansion/navigation and focus
|
||||
return, named native buttons exposed as generic MSAA clients, and combo boxes
|
||||
with labels created after their fields. The 64-bit fixture also checks the
|
||||
editor hook's late native label. Empty tabs/trees must drop their previous
|
||||
caption. The existing menu, editor, Notepad, list and managed-field fixtures
|
||||
remain regression checks; an application-owned MSAA provider in the menu
|
||||
fixture must retain its role instead of being replaced by native queries.
|
||||
|
||||
Read-only inspection of foobar2000 v2.26.1 with the new native query code
|
||||
retrieved `All Music`, `Album List`, and `Default Playlist` from the open
|
||||
application. This identifies data missed by the old helper; it does not prove
|
||||
live speech, physical braille, or complete foobar2000 support. Its custom
|
||||
playlist MSAA provider is separate from native tabs and trees.
|
||||
|
||||
After rebuilding/installing all three helper files, restart the reader and
|
||||
reopen foobar2000 after saving work. Tab and Shift+Tab through the album tree,
|
||||
View combo box, Filter edit, album/property tabs and playlist tabs. Expect
|
||||
captions plus their roles; arrows should read the new selected tab/tree node,
|
||||
with expanded/collapsed states where appropriate. Check native buttons,
|
||||
combo-box value changes, custom playlist item reading, Alt menus and Escape
|
||||
return, ordinary Notepad editing, password exclusion, and switching back to a
|
||||
native Linux application. Verify speech and physical braille separately.
|
||||
|
||||
## Windows Forms login fields
|
||||
|
||||
```sh
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#define UNICODE
|
||||
#define _UNICODE
|
||||
#include <windows.h>
|
||||
#include <commctrl.h>
|
||||
|
||||
static HWND tabs, atlTabs, tree, combo, edit, button;
|
||||
static HTREEITEM root;
|
||||
static int step;
|
||||
|
||||
static void focus_control(HWND window) {
|
||||
SetFocus(window);
|
||||
NotifyWinEvent(EVENT_OBJECT_FOCUS, window, OBJID_CLIENT, CHILDID_SELF);
|
||||
}
|
||||
|
||||
static LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wp, LPARAM lp) {
|
||||
if (message == WM_TIMER) {
|
||||
switch (++step) {
|
||||
case 1: SendMessageW(tabs, WM_KEYDOWN, VK_RIGHT, 1); break;
|
||||
case 2: focus_control(button); break;
|
||||
case 3: focus_control(atlTabs); break;
|
||||
case 4: SendMessageW(atlTabs, WM_KEYDOWN, VK_RIGHT, 1); break;
|
||||
case 5: focus_control(tree); break;
|
||||
case 6: SendMessageW(tree, TVM_EXPAND, TVE_EXPAND, (LPARAM)root); break;
|
||||
case 7: SendMessageW(tree, WM_KEYDOWN, VK_DOWN, 1); break;
|
||||
case 8: focus_control(button); break;
|
||||
case 9: focus_control(tree); break;
|
||||
case 10: focus_control(combo); break;
|
||||
case 11:
|
||||
SendMessageW(combo, CB_SETCURSEL, 1, 0);
|
||||
NotifyWinEvent(EVENT_OBJECT_VALUECHANGE, combo, OBJID_CLIENT, CHILDID_SELF);
|
||||
break;
|
||||
case 12: focus_control(edit); break;
|
||||
case 13: focus_control(atlTabs); break;
|
||||
case 14: SendMessageW(atlTabs, TCM_DELETEALLITEMS, 0, 0); break;
|
||||
case 15: focus_control(tree); break;
|
||||
case 16: SendMessageW(tree, TVM_DELETEITEM, 0, (LPARAM)TVI_ROOT); break;
|
||||
default: DestroyWindow(window); break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (message == WM_DESTROY) { PostQuitMessage(0); return 0; }
|
||||
return DefWindowProcW(window, message, wp, lp);
|
||||
}
|
||||
|
||||
int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR args, int show) {
|
||||
(void)previous; (void)args; (void)show;
|
||||
INITCOMMONCONTROLSEX controls = {sizeof(controls), ICC_TAB_CLASSES | ICC_TREEVIEW_CLASSES};
|
||||
InitCommonControlsEx(&controls);
|
||||
WNDCLASSW cls = {0};
|
||||
cls.hInstance = instance;
|
||||
cls.lpszClassName = L"WineAccessControlsFixture";
|
||||
cls.lpfnWndProc = window_proc;
|
||||
RegisterClassW(&cls);
|
||||
HWND window = CreateWindowW(cls.lpszClassName, L"Wine controls fixture", WS_OVERLAPPEDWINDOW,
|
||||
120, 120, 700, 440, NULL, NULL, instance, NULL);
|
||||
WNDCLASSEXW tabClass = {0};
|
||||
tabClass.cbSize = sizeof(tabClass);
|
||||
if (!GetClassInfoExW(NULL, WC_TABCONTROLW, &tabClass)) return 1;
|
||||
tabClass.hInstance = instance;
|
||||
tabClass.lpszClassName = L"ATL:SysTabControl32";
|
||||
if (!RegisterClassExW(&tabClass)) return 2;
|
||||
tabs = CreateWindowW(WC_TABCONTROLW, L"", WS_CHILD | WS_VISIBLE | WS_TABSTOP,
|
||||
10, 10, 300, 80, window, NULL, instance, NULL);
|
||||
atlTabs = CreateWindowW(tabClass.lpszClassName, L"", WS_CHILD | WS_VISIBLE | WS_TABSTOP,
|
||||
330, 10, 300, 80, window, NULL, instance, NULL);
|
||||
TCITEMW tab = {0};
|
||||
tab.mask = TCIF_TEXT;
|
||||
tab.pszText = L"Albums & artists β";
|
||||
SendMessageW(tabs, TCM_INSERTITEMW, 0, (LPARAM)&tab);
|
||||
tab.pszText = L"Details";
|
||||
SendMessageW(tabs, TCM_INSERTITEMW, 1, (LPARAM)&tab);
|
||||
tab.pszText = L"Default Playlist";
|
||||
SendMessageW(atlTabs, TCM_INSERTITEMW, 0, (LPARAM)&tab);
|
||||
tab.pszText = L"Second Playlist";
|
||||
SendMessageW(atlTabs, TCM_INSERTITEMW, 1, (LPARAM)&tab);
|
||||
tree = CreateWindowW(WC_TREEVIEWW, L"Tree1", WS_CHILD | WS_VISIBLE | WS_TABSTOP,
|
||||
10, 110, 300, 170, window, NULL, instance, NULL);
|
||||
TVINSERTSTRUCTW node = {0};
|
||||
node.hParent = TVI_ROOT;
|
||||
node.hInsertAfter = TVI_LAST;
|
||||
node.item.mask = TVIF_TEXT;
|
||||
node.item.pszText = L"All music";
|
||||
root = (HTREEITEM)SendMessageW(tree, TVM_INSERTITEMW, 0, (LPARAM)&node);
|
||||
node.hParent = root;
|
||||
node.item.pszText = L"Artist & album β";
|
||||
SendMessageW(tree, TVM_INSERTITEMW, 0, (LPARAM)&node);
|
||||
SendMessageW(tree, TVM_SELECTITEM, TVGN_CARET, (LPARAM)root);
|
||||
button = CreateWindowW(L"Button", L"Named native button", WS_CHILD | WS_VISIBLE | WS_TABSTOP,
|
||||
10, 310, 250, 30, window, NULL, instance, NULL);
|
||||
combo = CreateWindowW(L"ComboBox", L"", WS_CHILD | WS_VISIBLE | WS_TABSTOP | CBS_DROPDOWNLIST,
|
||||
390, 110, 200, 150, window, NULL, instance, NULL);
|
||||
SendMessageW(combo, CB_ADDSTRING, 0, (LPARAM)L"by artist");
|
||||
SendMessageW(combo, CB_ADDSTRING, 0, (LPARAM)L"by year");
|
||||
SendMessageW(combo, CB_SETCURSEL, 0, 0);
|
||||
edit = CreateWindowW(L"Edit", L"search", WS_CHILD | WS_VISIBLE | WS_TABSTOP,
|
||||
390, 170, 200, 25, window, NULL, instance, NULL);
|
||||
// Labels are deliberately created after their fields, like foobar2000.
|
||||
CreateWindowW(L"Static", L"View:", WS_CHILD | WS_VISIBLE,
|
||||
330, 110, 50, 25, window, NULL, instance, NULL);
|
||||
CreateWindowW(L"Static", L"Filter:", WS_CHILD | WS_VISIBLE,
|
||||
330, 170, 50, 25, window, NULL, instance, NULL);
|
||||
ShowWindow(window, SW_SHOW);
|
||||
SetForegroundWindow(window);
|
||||
focus_control(tabs);
|
||||
SetTimer(window, 1, 400, NULL);
|
||||
MSG message;
|
||||
while (GetMessageW(&message, NULL, 0, 0) > 0) {
|
||||
TranslateMessage(&message); DispatchMessageW(&message);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -16,13 +16,13 @@ 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", "winforms"), default="menu")
|
||||
parser.add_argument("--fixture", choices=("menu", "edit", "notepad", "list", "winforms", "controls"), default="menu")
|
||||
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 != "list":
|
||||
parser.error("32-bit checks currently require --fixture list")
|
||||
if args.is32Bit and args.fixture not in ("list", "controls"):
|
||||
parser.error("32-bit checks currently require --fixture list or controls")
|
||||
if (args.late_helper or args.restart_helper) and args.fixture not in ("edit", "winforms"):
|
||||
parser.error("helper lifecycle checks require --fixture edit or --fixture winforms")
|
||||
build = Path(args.build_dir).resolve()
|
||||
@@ -111,6 +111,30 @@ 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}"
|
||||
if args.fixture == "controls":
|
||||
for event in events:
|
||||
print(event[1:9])
|
||||
for caption in ("Albums & artists β", "Details", "Default Playlist", "Second Playlist"):
|
||||
assert any(e[2] == caption and e[3] == "page tab" and e[7] in (1, 2)
|
||||
and e[8] == 2 for e in events), f"native/ATL tab missing: {caption}"
|
||||
assert any(e[2] == "All music" and e[3] == "tree item" and "collapsed" in e[5]
|
||||
for e in events), "collapsed tree selection missing"
|
||||
assert any(e[2] == "All music" and "expanded" in e[5] for e in events), "tree expansion missing"
|
||||
assert any(e[2] == "Artist & album β" and e[3] == "tree item" for e in events), "tree navigation missing"
|
||||
assert any(e[1] == "focus" and e[2] == "Named native button" and e[3] == "button"
|
||||
for e in events), "named generic MSAA client hid button role"
|
||||
for value in ("by artist", "by year"):
|
||||
assert any(e[2] == "View" and e[3] == "combo box" and e[4] == value
|
||||
for e in events), f"combo label/role/value missing: {value}"
|
||||
if not args.is32Bit:
|
||||
assert any(e[2] == "Filter" and e[3] == "edit" and e[4] == "search"
|
||||
for e in events), "late native edit label missing"
|
||||
assert any(e[2] == "" and e[3] == "page tab" and e[7:9] == (0, 0)
|
||||
for e in events), "empty tabs retained stale caption"
|
||||
assert any(e[2] == "" and e[3] == "tree" for e in events), "empty tree retained stale item"
|
||||
assert not any(e[2] == "Tree1" for e in events), "generic tree name leaked"
|
||||
print("PASS: native/ATL tabs, tree selection/states/return, named button, combo and edit labels, empty controls")
|
||||
sys.exit(0)
|
||||
if args.fixture == "list":
|
||||
listEvents = [e for e in events if e[3] == "list"]
|
||||
for event in listEvents:
|
||||
|
||||
Reference in new issue
Block a user