Read native Wine list-view rows in 32-bit and 64-bit applications
This commit is contained in:
1 parent
9aad6748e0
commit
d7ea005a1f
8 files changed
+356
-4
No files matched your search
+1
-1
@@ -32,7 +32,7 @@ add_custom_command(
|
|||||||
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/nvdaController.idl"
|
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/nvdaController.idl"
|
||||||
VERBATIM)
|
VERBATIM)
|
||||||
|
|
||||||
add_executable(cthulhu-wine-access main.cpp "${generated_server}")
|
add_executable(cthulhu-wine-access main.cpp list_view.cpp "${generated_server}")
|
||||||
add_dependencies(cthulhu-wine-access wine-access-menu)
|
add_dependencies(cthulhu-wine-access wine-access-menu)
|
||||||
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}")
|
||||||
|
|||||||
@@ -110,6 +110,16 @@ contains stable comma-separated state labels decoded from the MSAA bitmask;
|
|||||||
protected values are excluded. Readers should filter name/state changes to
|
protected values are excluded. Readers should filter name/state changes to
|
||||||
their focused source and distinguish these labels from editor diagnostics.
|
their focused source and distinguish these labels from editor diagnostics.
|
||||||
|
|
||||||
|
Standard `SysListView32` controls now report the current row's text columns,
|
||||||
|
selection and position, including 32-bit installers driven by the 64-bit
|
||||||
|
helper. Wine can expose these controls only as a generic client named `List1`,
|
||||||
|
so the helper queries the native control with target-architecture structures.
|
||||||
|
When a populated list has no focused or selected row, focus reads its first
|
||||||
|
row without changing selection. A bounded focus poll reports row navigation
|
||||||
|
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.
|
||||||
|
|
||||||
This is editing feedback, not a full text-review provider: Say All, review-cursor
|
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
|
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
|
remains the reader's responsibility. RichEdit and 32-bit editor hooks are not
|
||||||
|
|||||||
+125
@@ -0,0 +1,125 @@
|
|||||||
|
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
#include "list_view.h"
|
||||||
|
#include <commctrl.h>
|
||||||
|
#include <cstdint>
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// LVM_GETITEMTEXT is not marshalled by USER32. The structure and text buffer
|
||||||
|
// must live in the target process, with pointers matching its architecture.
|
||||||
|
struct Item32 {
|
||||||
|
uint32_t mask;
|
||||||
|
int32_t item, subItem;
|
||||||
|
uint32_t state, stateMask, text;
|
||||||
|
int32_t capacity;
|
||||||
|
};
|
||||||
|
struct Item64 {
|
||||||
|
uint32_t mask;
|
||||||
|
int32_t item, subItem;
|
||||||
|
uint32_t state, stateMask, padding;
|
||||||
|
uint64_t text;
|
||||||
|
int32_t capacity;
|
||||||
|
uint32_t padding2;
|
||||||
|
};
|
||||||
|
static_assert(offsetof(Item32, text) == 20);
|
||||||
|
static_assert(offsetof(Item64, text) == 24);
|
||||||
|
constexpr size_t textOffset = 128;
|
||||||
|
constexpr size_t textCapacity = 1024;
|
||||||
|
constexpr size_t bufferSize = textOffset + textCapacity * sizeof(wchar_t);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_list_view(HWND window) {
|
||||||
|
wchar_t className[64] = {};
|
||||||
|
return GetClassNameW(window, className, std::size(className)) &&
|
||||||
|
lstrcmpiW(className, L"SysListView32") == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int list_view_index(HWND window, int *count) {
|
||||||
|
DWORD_PTR result = 0;
|
||||||
|
*count = 0;
|
||||||
|
if (!query(window, LVM_GETITEMCOUNT, 0, 0, &result) ||
|
||||||
|
static_cast<LONG_PTR>(result) <= 0)
|
||||||
|
return -1;
|
||||||
|
*count = static_cast<int>(result);
|
||||||
|
if (query(window, LVM_GETNEXTITEM, static_cast<WPARAM>(-1), LVNI_FOCUSED,
|
||||||
|
&result) && static_cast<LONG_PTR>(result) >= 0)
|
||||||
|
return static_cast<int>(result);
|
||||||
|
if (query(window, LVM_GETNEXTITEM, static_cast<WPARAM>(-1), LVNI_SELECTED,
|
||||||
|
&result) && static_cast<LONG_PTR>(result) >= 0)
|
||||||
|
return static_cast<int>(result);
|
||||||
|
// An installer can focus a populated list without selecting any row.
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ListViewRow read_list_view(HWND window) {
|
||||||
|
ListViewRow row;
|
||||||
|
row.index = list_view_index(window, &row.count);
|
||||||
|
if (row.index < 0 || row.index >= row.count)
|
||||||
|
return row;
|
||||||
|
DWORD_PTR state = 0;
|
||||||
|
if (query(window, LVM_GETITEMSTATE, row.index, LVIS_SELECTED, &state))
|
||||||
|
row.selected = state & LVIS_SELECTED;
|
||||||
|
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 == nullptr) return row;
|
||||||
|
BOOL wow64 = FALSE;
|
||||||
|
if (!IsWow64Process(process, &wow64)) {
|
||||||
|
CloseHandle(process);
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
void *remote = VirtualAllocEx(process, nullptr, bufferSize,
|
||||||
|
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
||||||
|
if (remote == nullptr || (wow64 && reinterpret_cast<uintptr_t>(remote) > UINT32_MAX)) {
|
||||||
|
if (remote != nullptr) VirtualFreeEx(process, remote, 0, MEM_RELEASE);
|
||||||
|
CloseHandle(process);
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
DWORD_PTR header = 0, columnCount = 1;
|
||||||
|
if (query(window, LVM_GETHEADER, 0, 0, &header) && header != 0) {
|
||||||
|
DWORD_PTR columns = 0;
|
||||||
|
if (query(reinterpret_cast<HWND>(header), HDM_GETITEMCOUNT, 0, 0, &columns) &&
|
||||||
|
columns > 0) columnCount = columns;
|
||||||
|
}
|
||||||
|
// Keep provider work and the protocol payload bounded even for huge tables.
|
||||||
|
if (columnCount > 8) columnCount = 8;
|
||||||
|
for (DWORD_PTR column = 0; column < columnCount; ++column) {
|
||||||
|
BYTE buffer[bufferSize] = {};
|
||||||
|
const auto textAddress = reinterpret_cast<uintptr_t>(remote) + textOffset;
|
||||||
|
if (wow64) {
|
||||||
|
Item32 item{LVIF_TEXT, row.index, static_cast<int32_t>(column),
|
||||||
|
0, 0, static_cast<uint32_t>(textAddress), textCapacity};
|
||||||
|
memcpy(buffer, &item, sizeof(item));
|
||||||
|
} else {
|
||||||
|
Item64 item{LVIF_TEXT, row.index, static_cast<int32_t>(column),
|
||||||
|
0, 0, 0, textAddress, textCapacity, 0};
|
||||||
|
memcpy(buffer, &item, sizeof(item));
|
||||||
|
}
|
||||||
|
DWORD_PTR result = 0;
|
||||||
|
wchar_t text[textCapacity] = {};
|
||||||
|
if (!WriteProcessMemory(process, remote, buffer, sizeof(buffer), nullptr) ||
|
||||||
|
!query(window, LVM_GETITEMTEXTW, row.index, reinterpret_cast<LPARAM>(remote), &result) ||
|
||||||
|
!ReadProcessMemory(process, reinterpret_cast<void *>(textAddress), text,
|
||||||
|
sizeof(text), nullptr))
|
||||||
|
break;
|
||||||
|
text[textCapacity - 1] = L'\0';
|
||||||
|
if (!text[0]) continue;
|
||||||
|
char utf8[textCapacity * 4] = {};
|
||||||
|
if (!WideCharToMultiByte(CP_UTF8, 0, text, -1, utf8, sizeof(utf8), nullptr, nullptr))
|
||||||
|
continue;
|
||||||
|
if (!row.text.empty()) row.text += ", ";
|
||||||
|
row.text += utf8;
|
||||||
|
}
|
||||||
|
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
|
||||||
|
CloseHandle(process);
|
||||||
|
return row;
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
#pragma once
|
||||||
|
#include <windows.h>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
struct ListViewRow {
|
||||||
|
std::string text;
|
||||||
|
int index = -1;
|
||||||
|
int count = 0;
|
||||||
|
bool selected = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool is_list_view(HWND window);
|
||||||
|
ListViewRow read_list_view(HWND window);
|
||||||
|
int list_view_index(HWND window, int *count);
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "menu_bridge.h"
|
#include "menu_bridge.h"
|
||||||
#include "edit_hook.h"
|
#include "edit_hook.h"
|
||||||
#include "msaa_states.h"
|
#include "msaa_states.h"
|
||||||
|
#include "list_view.h"
|
||||||
|
|
||||||
#include <commctrl.h>
|
#include <commctrl.h>
|
||||||
#include <gio/gio.h>
|
#include <gio/gio.h>
|
||||||
@@ -31,6 +32,10 @@ bool editReaderActive = false;
|
|||||||
std::string lastMenuEvent;
|
std::string lastMenuEvent;
|
||||||
ULONGLONG lastMenuOwner = 0;
|
ULONGLONG lastMenuOwner = 0;
|
||||||
HWND lastTextControl = nullptr;
|
HWND lastTextControl = nullptr;
|
||||||
|
HWND lastListControl = nullptr;
|
||||||
|
int lastListIndex = -1;
|
||||||
|
int lastListCount = 0;
|
||||||
|
bool lastListSelected = false;
|
||||||
|
|
||||||
// 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.
|
||||||
@@ -266,6 +271,22 @@ ControlDescription describe_standard_control(HWND window) {
|
|||||||
return control;
|
return control;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (is_list_view(window)) {
|
||||||
|
find_control_label(window, label, std::size(label));
|
||||||
|
control.name = to_utf8(label);
|
||||||
|
control.role = "list";
|
||||||
|
const auto row = read_list_view(window);
|
||||||
|
control.value = row.text;
|
||||||
|
if (!row.text.empty()) control.position = row.index + 1;
|
||||||
|
control.count = row.count;
|
||||||
|
if (row.selected) control.states = "selected";
|
||||||
|
lastListControl = window;
|
||||||
|
lastListIndex = row.index;
|
||||||
|
lastListCount = row.count;
|
||||||
|
lastListSelected = row.selected;
|
||||||
|
return control;
|
||||||
|
}
|
||||||
|
|
||||||
if (lstrcmpW(className, L"ListBox") == 0 ||
|
if (lstrcmpW(className, L"ListBox") == 0 ||
|
||||||
lstrcmpW(className, L"ComboLBox") == 0) {
|
lstrcmpW(className, L"ComboLBox") == 0) {
|
||||||
find_control_label(window, label, std::size(label));
|
find_control_label(window, label, std::size(label));
|
||||||
@@ -333,6 +354,34 @@ bool present_standard_focus(HWND window) {
|
|||||||
control, window_title(window));
|
control, window_title(window));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void poll_list_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_list_view(info.hwndFocus)) {
|
||||||
|
int count = 0;
|
||||||
|
const int index = list_view_index(info.hwndFocus, &count);
|
||||||
|
DWORD_PTR state = 0;
|
||||||
|
if (index >= 0)
|
||||||
|
SendMessageTimeoutW(info.hwndFocus, LVM_GETITEMSTATE, index, LVIS_SELECTED,
|
||||||
|
SMTO_ABORTIFHUNG, 100, &state);
|
||||||
|
const bool selected = state & LVIS_SELECTED;
|
||||||
|
if (info.hwndFocus != lastListControl) {
|
||||||
|
present_standard_focus(info.hwndFocus);
|
||||||
|
} else if (refresh || index != lastListIndex || count != lastListCount || selected != lastListSelected) {
|
||||||
|
const auto control = describe_standard_control(info.hwndFocus);
|
||||||
|
present_accessible_event(source_id(info.hwndFocus, "standard"), "selection",
|
||||||
|
control, window_title(info.hwndFocus));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastListControl = nullptr;
|
||||||
|
lastListIndex = -1;
|
||||||
|
lastListCount = 0;
|
||||||
|
lastListSelected = false;
|
||||||
|
}
|
||||||
|
|
||||||
void present_menu_event(const WineAccessMenuEvent &event) {
|
void present_menu_event(const WineAccessMenuEvent &event) {
|
||||||
HWND owner = reinterpret_cast<HWND>(static_cast<UINT_PTR>(event.owner));
|
HWND owner = reinterpret_cast<HWND>(static_cast<UINT_PTR>(event.owner));
|
||||||
if (owner == nullptr)
|
if (owner == nullptr)
|
||||||
@@ -503,6 +552,13 @@ 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;
|
||||||
|
// 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 (event == EVENT_OBJECT_FOCUS) present_standard_focus(window);
|
||||||
|
else poll_list_focus(event == EVENT_OBJECT_NAMECHANGE || event == EVENT_OBJECT_VALUECHANGE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
IAccessible *accessible = nullptr;
|
IAccessible *accessible = nullptr;
|
||||||
VARIANT child{};
|
VARIANT child{};
|
||||||
VariantInit(&child);
|
VariantInit(&child);
|
||||||
@@ -764,10 +820,19 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) {
|
|||||||
static_cast<unsigned long>(GetLastError()));
|
static_cast<unsigned long>(GetLastError()));
|
||||||
}
|
}
|
||||||
MSG message{};
|
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.
|
||||||
|
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 &&
|
||||||
|
message.wParam == listTimer) {
|
||||||
|
poll_list_focus();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
TranslateMessage(&message);
|
TranslateMessage(&message);
|
||||||
DispatchMessageW(&message);
|
DispatchMessageW(&message);
|
||||||
}
|
}
|
||||||
|
if (listTimer) KillTimer(nullptr, listTimer);
|
||||||
if (hook != nullptr)
|
if (hook != nullptr)
|
||||||
UnhookWinEvent(hook);
|
UnhookWinEvent(hook);
|
||||||
if (menuHook != nullptr)
|
if (menuHook != nullptr)
|
||||||
|
|||||||
@@ -73,6 +73,32 @@ 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.
|
||||||
|
|
||||||
|
## List-view checks
|
||||||
|
|
||||||
|
```sh
|
||||||
|
xvfb-run -a python3 tests/menu_integration.py --build-dir build --fixture list
|
||||||
|
xvfb-run -a python3 tests/menu_integration.py --build-dir build --fixture list --32-bit
|
||||||
|
```
|
||||||
|
|
||||||
|
The second command requires `i686-w64-mingw32-gcc`. Both use a private prefix
|
||||||
|
and recording D-Bus service. They verify first-row reading without selecting
|
||||||
|
it, two columns, Unicode and literal ampersands, Down/Up, button/list focus
|
||||||
|
return, changed row text on a value event, and deletion of all rows without
|
||||||
|
stale output. Run them sequentially. Both architecture fixtures passed on
|
||||||
|
October 10, as did the existing menu, editor, actual Notepad and CTest MSAA
|
||||||
|
state checks. The reader's focused Wine race checks also passed.
|
||||||
|
|
||||||
|
The BARD Express bootstrapper was inspected in place on October 10. Its
|
||||||
|
32-bit `SysListView32` has one row: `Pending, Microsoft .NET Framework 4.7.2 Full`.
|
||||||
|
The new query code retrieved that row without changing the installer. This
|
||||||
|
does not establish audible output or successful prerequisite installation.
|
||||||
|
|
||||||
|
After installing the helper and restarting Cthulhu, Tab to the prerequisite
|
||||||
|
list: expect the instructions, list role, pending component and `1 of 1`.
|
||||||
|
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.
|
||||||
|
|
||||||
## Live Notepad acceptance
|
## Live Notepad acceptance
|
||||||
|
|
||||||
Install all three helper files together and use the matching Cthulhu-Go source.
|
Install all three helper files together and use the matching Cthulhu-Go source.
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
#define UNICODE
|
||||||
|
#define _UNICODE
|
||||||
|
#include <windows.h>
|
||||||
|
#include <commctrl.h>
|
||||||
|
|
||||||
|
static HWND list, button;
|
||||||
|
static int step;
|
||||||
|
|
||||||
|
static LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wp, LPARAM lp) {
|
||||||
|
if (message == WM_TIMER) {
|
||||||
|
switch (++step) {
|
||||||
|
case 1: case 2:
|
||||||
|
// Exercise keyboard processing, not synthetic accessibility events.
|
||||||
|
SendMessageW(list, WM_KEYDOWN, VK_DOWN, 1);
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
SendMessageW(list, WM_KEYDOWN, VK_UP, 1);
|
||||||
|
break;
|
||||||
|
case 4: SetFocus(button); break;
|
||||||
|
case 5: SetFocus(list); break;
|
||||||
|
case 6: {
|
||||||
|
LVITEMW item = {0};
|
||||||
|
item.pszText = L"Ready";
|
||||||
|
SendMessageW(list, LVM_SETITEMTEXTW, 0, (LPARAM)&item);
|
||||||
|
NotifyWinEvent(EVENT_OBJECT_VALUECHANGE, list, OBJID_CLIENT, CHILDID_SELF);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 7: SendMessageW(list, LVM_DELETEALLITEMS, 0, 0); break;
|
||||||
|
default: DestroyWindow(window);
|
||||||
|
}
|
||||||
|
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_LISTVIEW_CLASSES};
|
||||||
|
InitCommonControlsEx(&controls);
|
||||||
|
WNDCLASSW cls = {0};
|
||||||
|
cls.hInstance = instance;
|
||||||
|
cls.lpszClassName = L"WineAccessListFixture";
|
||||||
|
cls.lpfnWndProc = window_proc;
|
||||||
|
RegisterClassW(&cls);
|
||||||
|
HWND window = CreateWindowW(cls.lpszClassName, L"Wine list fixture", WS_OVERLAPPEDWINDOW,
|
||||||
|
120, 120, 600, 300, NULL, NULL, instance, NULL);
|
||||||
|
button = CreateWindowW(L"Button", L"Return to control", WS_CHILD | WS_VISIBLE | WS_TABSTOP,
|
||||||
|
10, 240, 200, 30, window, NULL, instance, NULL);
|
||||||
|
CreateWindowW(L"Static", L"Required components", WS_CHILD | WS_VISIBLE,
|
||||||
|
10, 10, 550, 30, window, NULL, instance, NULL);
|
||||||
|
list = CreateWindowW(WC_LISTVIEWW, L"List1", WS_CHILD | WS_VISIBLE | WS_TABSTOP | LVS_REPORT,
|
||||||
|
10, 50, 550, 180, window, NULL, instance, NULL);
|
||||||
|
LVCOLUMNW column = {0};
|
||||||
|
column.mask = LVCF_TEXT | LVCF_WIDTH;
|
||||||
|
column.cx = 150;
|
||||||
|
column.pszText = L"Status";
|
||||||
|
SendMessageW(list, LVM_INSERTCOLUMNW, 0, (LPARAM)&column);
|
||||||
|
column.cx = 350;
|
||||||
|
column.pszText = L"Component";
|
||||||
|
SendMessageW(list, LVM_INSERTCOLUMNW, 1, (LPARAM)&column);
|
||||||
|
LVITEMW item = {0};
|
||||||
|
item.mask = LVIF_TEXT;
|
||||||
|
item.pszText = L"Pending";
|
||||||
|
SendMessageW(list, LVM_INSERTITEMW, 0, (LPARAM)&item);
|
||||||
|
item.iSubItem = 1;
|
||||||
|
item.pszText = L"Runtime & tools β";
|
||||||
|
SendMessageW(list, LVM_SETITEMTEXTW, 0, (LPARAM)&item);
|
||||||
|
item.iItem = 1;
|
||||||
|
item.iSubItem = 0;
|
||||||
|
item.pszText = L"Installed";
|
||||||
|
SendMessageW(list, LVM_INSERTITEMW, 0, (LPARAM)&item);
|
||||||
|
item.iSubItem = 1;
|
||||||
|
item.pszText = L"Second component";
|
||||||
|
SendMessageW(list, LVM_SETITEMTEXTW, 1, (LPARAM)&item);
|
||||||
|
ShowWindow(window, SW_SHOW);
|
||||||
|
SetForegroundWindow(window);
|
||||||
|
// Match the installer: initially no row is selected or focused.
|
||||||
|
SetFocus(list);
|
||||||
|
SetTimer(window, 1, 500, NULL);
|
||||||
|
MSG message;
|
||||||
|
while (GetMessageW(&message, NULL, 0, 0) > 0) {
|
||||||
|
TranslateMessage(&message); DispatchMessageW(&message);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -16,10 +16,13 @@ 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"), default="menu")
|
parser.add_argument("--fixture", choices=("menu", "edit", "notepad", "list"), default="menu")
|
||||||
|
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 != "list":
|
||||||
|
parser.error("32-bit checks currently require --fixture list")
|
||||||
if (args.late_helper or args.restart_helper) and args.fixture != "edit":
|
if (args.late_helper or args.restart_helper) and args.fixture != "edit":
|
||||||
parser.error("helper lifecycle checks require --fixture edit")
|
parser.error("helper lifecycle checks require --fixture edit")
|
||||||
build = Path(args.build_dir).resolve()
|
build = Path(args.build_dir).resolve()
|
||||||
@@ -27,6 +30,7 @@ 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,
|
||||||
|
*(["--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 [])]))
|
||||||
|
|
||||||
@@ -56,9 +60,10 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
|
|||||||
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("/", "\\")
|
||||||
fixture = Path(temporary) / "menu-fixture.exe"
|
fixture = Path(temporary) / "menu-fixture.exe"
|
||||||
subprocess.run(["x86_64-w64-mingw32-gcc", "-Wall", "-Wextra", "-mwindows",
|
compiler = "i686-w64-mingw32-gcc" if args.is32Bit else "x86_64-w64-mingw32-gcc"
|
||||||
|
subprocess.run([compiler, "-Wall", "-Wextra", "-mwindows",
|
||||||
"-o", str(fixture), str(Path(__file__).with_name(args.fixture + "_fixture.c")),
|
"-o", str(fixture), str(Path(__file__).with_name(args.fixture + "_fixture.c")),
|
||||||
"-luser32"], check=True)
|
"-luser32", "-lcomctl32"], check=True)
|
||||||
helper = None
|
helper = None
|
||||||
app = None
|
app = None
|
||||||
try:
|
try:
|
||||||
@@ -104,6 +109,25 @@ 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}"
|
||||||
|
if args.fixture == "list":
|
||||||
|
listEvents = [e for e in events if e[3] == "list"]
|
||||||
|
for event in listEvents:
|
||||||
|
print(event[1:9])
|
||||||
|
assert any(e[1] == "focus" and e[2] == "Required components" and
|
||||||
|
e[4] == "Pending, Runtime & tools β" and e[7:9] == (1, 2)
|
||||||
|
and e[5] == "" for e in listEvents), "unselected initial row or label missing"
|
||||||
|
assert any(e[1] == "selection" and e[4] == "Installed, Second component" and
|
||||||
|
e[7:9] == (2, 2) for e in listEvents), "Down lost second row/columns"
|
||||||
|
assert any(e[1] == "selection" and e[4] == "Pending, Runtime & tools β" and
|
||||||
|
e[7:9] == (1, 2) for e in listEvents), "Up lost first row"
|
||||||
|
assert any(e[1] == "focus" and e[4] == "Pending, Runtime & tools β" and
|
||||||
|
e[5] == "selected" for e in listEvents), "return to list lost selection"
|
||||||
|
assert any(e[4] == "Ready, Runtime & tools β" for e in listEvents), "value event lost changed row text"
|
||||||
|
assert any(e[4] == "" and e[7:9] == (0, 0) for e in listEvents), "empty list retained stale row"
|
||||||
|
assert any(e[2] == "Return to control" for e in events), "button focus missing"
|
||||||
|
assert not any(e[2] == "List1" for e in events), "generic MSAA client leaked"
|
||||||
|
print("PASS: native list focus, columns, Unicode, navigation, return, and empty list")
|
||||||
|
sys.exit(0)
|
||||||
if args.fixture == "notepad":
|
if args.fixture == "notepad":
|
||||||
dialogEvents = [e for e in events if e[2] in ("Yes", "No", "Cancel")]
|
dialogEvents = [e for e in events if e[2] in ("Yes", "No", "Cancel")]
|
||||||
for event in dialogEvents:
|
for event in dialogEvents:
|
||||||
|
|||||||
Reference in new issue
Block a user