Bug fixes and reorganization of code.
This commit is contained in:
1 parent
5f78168c57
commit
cbce83581a
10 files changed
+148
-81
No files matched your search
@@ -33,6 +33,10 @@ project yet; it needs a 32-bit Winegcc and 32-bit GLib/GIO development setup.
|
||||
|
||||
## Build
|
||||
|
||||
On Arch Linux, the [PKGBUILD](distro-packages/ArchLinux/wine-access-git/README.md)
|
||||
provides package-managed installation and upgrades. Choose one installation
|
||||
method to avoid a local copy taking precedence over the system package.
|
||||
|
||||
The convenience script builds in `./build`. Running it as a user installs to
|
||||
`$HOME/.local/libexec/cthulhu/wine`; running it as root installs system wide to
|
||||
`/usr/libexec/cthulhu/wine`:
|
||||
@@ -148,6 +152,10 @@ 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.
|
||||
|
||||
Control values and window titles retain literal ampersands. Button and static
|
||||
label captions decode keyboard mnemonics and escaped `&&`; static labels with
|
||||
`SS_NOPREFIX` retain their literal text.
|
||||
|
||||
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
|
||||
|
||||
@@ -8,8 +8,8 @@ url='https://git.stormux.org/storm/wine-access'
|
||||
license=('LGPL-2.1-or-later')
|
||||
depends=('glib2' 'wine')
|
||||
makedepends=('cmake' 'git' 'pkgconf' 'mingw-w64-gcc')
|
||||
provides=('wine-access')
|
||||
conflicts=('wine-access')
|
||||
provides=('wine-access' 'cthulhu-wine-access')
|
||||
conflicts=('wine-access' 'cthulhu-wine-access')
|
||||
# Wine's import generation needs native object symbols, which LTO hides.
|
||||
options=('!lto')
|
||||
|
||||
|
||||
@@ -23,8 +23,18 @@ elsewhere. To use HTTPS after the repository becomes public:
|
||||
WINE_ACCESS_GIT_URL=https://git.stormux.org/storm/wine-access.git makepkg -si
|
||||
```
|
||||
|
||||
The package installs both Winelib artifacts under
|
||||
`/usr/libexec/cthulhu/wine/`. If your reader does not discover that system
|
||||
The package installs the Winelib launcher, its sidecar and the native hook DLL
|
||||
under `/usr/libexec/cthulhu/wine/`. It provides and conflicts with the older
|
||||
`cthulhu-wine-access` helper, including `cthulhu-wine-access-git`, so pacman
|
||||
can replace that package in the installation transaction.
|
||||
|
||||
Use this package for subsequent updates rather than also installing through
|
||||
`build.sh`. If an earlier local installation exists under
|
||||
`~/.local/libexec/cthulhu/wine`, remove it with `./build.sh -r` from the project
|
||||
root before using the package: readers may prefer that local copy. Do not
|
||||
remove package-owned system files with `build.sh -r`.
|
||||
|
||||
If your reader does not discover the system
|
||||
location automatically, set this in the environment of the reader's service:
|
||||
|
||||
```sh
|
||||
|
||||
+9
-29
@@ -1,6 +1,7 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#include "list_view.h"
|
||||
#include "control_class.h"
|
||||
#include "native_query.h"
|
||||
#include <commctrl.h>
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
@@ -8,10 +9,7 @@
|
||||
#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;
|
||||
}
|
||||
using native_control::query;
|
||||
|
||||
// LVM_GETITEMTEXT is not marshalled by USER32. The structure and text buffer
|
||||
// must live in the target process, with pointers matching its architecture.
|
||||
@@ -67,24 +65,9 @@ ListViewRow read_list_view(HWND window) {
|
||||
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;
|
||||
}
|
||||
native_control::RemoteBuffer remote(window, bufferSize);
|
||||
if (!remote) return row;
|
||||
const bool wow64 = remote.is32Bit();
|
||||
DWORD_PTR header = 0, columnCount = 1;
|
||||
if (query(window, LVM_GETHEADER, 0, 0, &header) && header != 0) {
|
||||
DWORD_PTR columns = 0;
|
||||
@@ -95,7 +78,7 @@ ListViewRow read_list_view(HWND window) {
|
||||
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;
|
||||
const auto textAddress = remote.address(textOffset);
|
||||
if (wow64) {
|
||||
Item32 item{LVIF_TEXT, row.index, static_cast<int32_t>(column),
|
||||
0, 0, static_cast<uint32_t>(textAddress), textCapacity};
|
||||
@@ -107,10 +90,9 @@ ListViewRow read_list_view(HWND window) {
|
||||
}
|
||||
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))
|
||||
if (!remote.write(buffer, sizeof(buffer)) ||
|
||||
!query(window, LVM_GETITEMTEXTW, row.index, static_cast<LPARAM>(remote.address()), &result) ||
|
||||
!remote.read(text, sizeof(text), textOffset))
|
||||
break;
|
||||
text[textCapacity - 1] = L'\0';
|
||||
if (!text[0]) continue;
|
||||
@@ -120,7 +102,5 @@ ListViewRow read_list_view(HWND window) {
|
||||
if (!row.text.empty()) row.text += ", ";
|
||||
row.text += utf8;
|
||||
}
|
||||
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
|
||||
CloseHandle(process);
|
||||
return row;
|
||||
}
|
||||
@@ -140,15 +140,21 @@ std::string accessible_property(
|
||||
|
||||
// Standard-control queries follow the LGPL wine-a11y reader published by
|
||||
// Mudb0y, while presentation remains on Cthulhu's D-Bus interface.
|
||||
void tidy_text(wchar_t *text, bool label = false) {
|
||||
void strip_mnemonics(wchar_t *text) {
|
||||
wchar_t *output = text;
|
||||
for (wchar_t *input = text; *input != L'\0'; ++input) {
|
||||
if (*input != L'&')
|
||||
*output++ = *input;
|
||||
if (*input == L'&') {
|
||||
if (input[1] != L'&') continue;
|
||||
++input;
|
||||
}
|
||||
*output++ = *input;
|
||||
}
|
||||
*output = L'\0';
|
||||
if (label && output > text && output[-1] == L':')
|
||||
output[-1] = L'\0';
|
||||
}
|
||||
|
||||
void trim_label(wchar_t *text) {
|
||||
const int length = lstrlenW(text);
|
||||
if (length && text[length - 1] == L':') text[length - 1] = L'\0';
|
||||
}
|
||||
|
||||
bool get_control_text(HWND window, wchar_t *text, int capacity) {
|
||||
@@ -158,7 +164,14 @@ bool get_control_text(HWND window, wchar_t *text, int capacity) {
|
||||
reinterpret_cast<LPARAM>(text), SMTO_ABORTIFHUNG,
|
||||
200, &result))
|
||||
return false;
|
||||
tidy_text(text);
|
||||
// Only captions interpret mnemonic markers. Values and window titles
|
||||
// contain literal text; even captions escape a literal ampersand as &&.
|
||||
wchar_t className[128] = {};
|
||||
if (GetClassNameW(window, className, std::size(className)) &&
|
||||
(wine_access_class_matches(className, L"Button") ||
|
||||
(wine_access_class_matches(className, L"Static") &&
|
||||
!(GetWindowLongPtrW(window, GWL_STYLE) & SS_NOPREFIX))))
|
||||
strip_mnemonics(text);
|
||||
return text[0] != L'\0';
|
||||
}
|
||||
|
||||
@@ -195,7 +208,7 @@ void find_control_label(HWND window, wchar_t *text, int capacity) {
|
||||
}
|
||||
}
|
||||
if (label && get_control_text(label, text, capacity)) {
|
||||
tidy_text(text, true);
|
||||
trim_label(text);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -210,7 +223,7 @@ void find_control_label(HWND window, wchar_t *text, int capacity) {
|
||||
if (!wine_access_class_matches(className, L"Static") && !wine_access_class_matches(className, L"Button"))
|
||||
continue;
|
||||
if (get_control_text(previous, text, capacity))
|
||||
tidy_text(text, true);
|
||||
trim_label(text);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -218,7 +231,6 @@ void find_control_label(HWND window, wchar_t *text, int capacity) {
|
||||
std::string window_title(HWND window) {
|
||||
wchar_t title[512] = {};
|
||||
GetWindowTextW(GetAncestor(window, GA_ROOT), title, std::size(title));
|
||||
tidy_text(title);
|
||||
return to_utf8(title);
|
||||
}
|
||||
|
||||
@@ -362,7 +374,6 @@ ControlDescription describe_standard_control(HWND window) {
|
||||
SendMessageTimeoutW(window, LB_GETTEXT, selected,
|
||||
reinterpret_cast<LPARAM>(text), SMTO_ABORTIFHUNG,
|
||||
200, &length)) {
|
||||
tidy_text(text);
|
||||
control.value = to_utf8(text);
|
||||
}
|
||||
control.position = static_cast<int>(selected) + 1;
|
||||
|
||||
+9
-25
@@ -1,16 +1,14 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#include "native_items.h"
|
||||
#include "control_class.h"
|
||||
#include "native_query.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;
|
||||
}
|
||||
using native_control::query;
|
||||
bool is_tab(HWND window) {
|
||||
wchar_t className[128] = {};
|
||||
return GetClassNameW(window, className, std::size(className)) &&
|
||||
@@ -75,23 +73,11 @@ NativeItem read_native_item(HWND window) {
|
||||
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;
|
||||
}
|
||||
native_control::RemoteBuffer remote(window, bufferSize);
|
||||
if (!remote) return item;
|
||||
const bool wow64 = remote.is32Bit();
|
||||
BYTE buffer[bufferSize] = {};
|
||||
const auto address = reinterpret_cast<uintptr_t>(remote) + textOffset;
|
||||
const auto address = remote.address(textOffset);
|
||||
if (tab && wow64) {
|
||||
const Tab32 data{TCIF_TEXT, 0, 0, static_cast<uint32_t>(address), textCapacity};
|
||||
memcpy(buffer, &data, sizeof(data));
|
||||
@@ -108,10 +94,10 @@ NativeItem read_native_item(HWND window) {
|
||||
memcpy(buffer, &data, sizeof(data));
|
||||
}
|
||||
DWORD_PTR result = 0;
|
||||
if (WriteProcessMemory(process, remote, buffer, sizeof(buffer), nullptr) &&
|
||||
if (remote.write(buffer, sizeof(buffer)) &&
|
||||
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)) {
|
||||
static_cast<LPARAM>(remote.address()), &result) && result &&
|
||||
remote.read(buffer, sizeof(buffer))) {
|
||||
wchar_t text[textCapacity] = {};
|
||||
memcpy(text, buffer + textOffset, sizeof(text));
|
||||
text[textCapacity - 1] = L'\0';
|
||||
@@ -130,7 +116,5 @@ NativeItem read_native_item(HWND window) {
|
||||
}
|
||||
}
|
||||
}
|
||||
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
|
||||
CloseHandle(process);
|
||||
return item;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#pragma once
|
||||
#include <windows.h>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace native_control {
|
||||
inline bool query(HWND window, UINT message, WPARAM wp, LPARAM lp, DWORD_PTR *result) {
|
||||
return SendMessageTimeoutW(window, message, wp, lp, SMTO_ABORTIFHUNG,
|
||||
100, result) != 0;
|
||||
}
|
||||
|
||||
// Common-control messages above WM_USER do not marshal embedded pointers.
|
||||
// Own the target allocation and process handle together, including failures.
|
||||
class RemoteBuffer {
|
||||
public:
|
||||
RemoteBuffer(HWND window, size_t size) {
|
||||
DWORD processId = 0;
|
||||
GetWindowThreadProcessId(window, &processId);
|
||||
process_ = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_READ |
|
||||
PROCESS_VM_WRITE | PROCESS_QUERY_LIMITED_INFORMATION,
|
||||
FALSE, processId);
|
||||
if (!process_ || !IsWow64Process(process_, &wow64_)) return;
|
||||
memory_ = VirtualAllocEx(process_, nullptr, size,
|
||||
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
||||
// Every byte of the allocation must be addressable by the target.
|
||||
if (memory_ && wow64_ &&
|
||||
(address() > UINT32_MAX || size - 1 > UINT32_MAX - address())) {
|
||||
VirtualFreeEx(process_, memory_, 0, MEM_RELEASE);
|
||||
memory_ = nullptr;
|
||||
}
|
||||
}
|
||||
~RemoteBuffer() {
|
||||
if (memory_) VirtualFreeEx(process_, memory_, 0, MEM_RELEASE);
|
||||
if (process_) CloseHandle(process_);
|
||||
}
|
||||
RemoteBuffer(const RemoteBuffer &) = delete;
|
||||
RemoteBuffer &operator=(const RemoteBuffer &) = delete;
|
||||
|
||||
explicit operator bool() const { return memory_ != nullptr; }
|
||||
bool is32Bit() const { return wow64_ != FALSE; }
|
||||
uintptr_t address(size_t offset = 0) const {
|
||||
return reinterpret_cast<uintptr_t>(memory_) + offset;
|
||||
}
|
||||
bool write(const void *buffer, size_t size) const {
|
||||
return WriteProcessMemory(process_, memory_, buffer, size, nullptr) != 0;
|
||||
}
|
||||
bool read(void *buffer, size_t size, size_t offset = 0) const {
|
||||
return ReadProcessMemory(process_, reinterpret_cast<const void *>(address(offset)),
|
||||
buffer, size, nullptr) != 0;
|
||||
}
|
||||
|
||||
private:
|
||||
HANDLE process_ = nullptr;
|
||||
void *memory_ = nullptr;
|
||||
BOOL wow64_ = FALSE;
|
||||
};
|
||||
} // namespace native_control
|
||||
@@ -150,6 +150,12 @@ 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.
|
||||
|
||||
The controls fixture also verifies literal ampersands in combo-box, edit and
|
||||
list-box values and window titles, escaped ampersands in button/label captions,
|
||||
and literal `SS_NOPREFIX` labels. Both control architectures exercise the
|
||||
shared remote allocation used by native tabs and trees; the list fixtures
|
||||
exercise the same allocation for multi-column rows.
|
||||
|
||||
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
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#include <windows.h>
|
||||
#include <commctrl.h>
|
||||
|
||||
static HWND tabs, atlTabs, tree, combo, edit, button;
|
||||
static HWND tabs, atlTabs, tree, combo, edit, button, list;
|
||||
static HTREEITEM root;
|
||||
static int step;
|
||||
|
||||
@@ -35,6 +35,7 @@ static LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wp, LPARAM
|
||||
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;
|
||||
case 17: focus_control(list); break;
|
||||
default: DestroyWindow(window); break;
|
||||
}
|
||||
return 0;
|
||||
@@ -52,7 +53,7 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR args, int show)
|
||||
cls.lpszClassName = L"WineAccessControlsFixture";
|
||||
cls.lpfnWndProc = window_proc;
|
||||
RegisterClassW(&cls);
|
||||
HWND window = CreateWindowW(cls.lpszClassName, L"Wine controls fixture", WS_OVERLAPPEDWINDOW,
|
||||
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);
|
||||
@@ -86,20 +87,24 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR args, int show)
|
||||
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,
|
||||
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_ADDSTRING, 0, (LPARAM)L"artist & album");
|
||||
SendMessageW(combo, CB_ADDSTRING, 0, (LPARAM)L"year && date");
|
||||
SendMessageW(combo, CB_SETCURSEL, 0, 0);
|
||||
edit = CreateWindowW(L"Edit", L"search", WS_CHILD | WS_VISIBLE | WS_TABSTOP,
|
||||
edit = CreateWindowW(L"Edit", L"search & find", 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,
|
||||
CreateWindowW(L"Static", L"&View && sort:", WS_CHILD | WS_VISIBLE,
|
||||
330, 110, 50, 25, window, NULL, instance, NULL);
|
||||
CreateWindowW(L"Static", L"Filter:", WS_CHILD | WS_VISIBLE,
|
||||
CreateWindowW(L"Static", L"Filter & query:", WS_CHILD | WS_VISIBLE | SS_NOPREFIX,
|
||||
330, 170, 50, 25, window, NULL, instance, NULL);
|
||||
list = CreateWindowW(L"ListBox", L"", WS_CHILD | WS_VISIBLE | WS_TABSTOP,
|
||||
390, 220, 200, 80, window, NULL, instance, NULL);
|
||||
SendMessageW(list, LB_ADDSTRING, 0, (LPARAM)L"Rock & roll && blues");
|
||||
SendMessageW(list, LB_SETCURSEL, 0, 0);
|
||||
ShowWindow(window, SW_SHOW);
|
||||
SetForegroundWindow(window);
|
||||
focus_control(tabs);
|
||||
|
||||
@@ -121,14 +121,19 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
|
||||
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"
|
||||
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 value in ("artist & album", "year && date"):
|
||||
assert any(e[2] == "View & sort" and e[3] == "combo box" and e[4] == value
|
||||
for e in events), f"combo label/role/value missing: {value}"
|
||||
assert any(e[4] == "search & find" for e in events), "edit value changed"
|
||||
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] == "Filter & query" and e[3] == "edit" and e[4] == "search & find"
|
||||
for e in events), "late native edit label/value missing"
|
||||
assert any(e[3] == "list" and e[4] == "Rock & roll && blues"
|
||||
for e in events), "list box lost literal ampersands"
|
||||
controlEvents = [e for e in events if e[3] in ("page tab", "tree item", "button", "combo box", "edit")]
|
||||
assert controlEvents and all(e[9] == "Wine & controls fixture" for e in controlEvents), "window title changed"
|
||||
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"
|
||||
|
||||
Reference in new issue
Block a user