Bug fixes and reorganization of code.

This commit is contained in:
ak47 committed 2026-10-10 22:27:12 -04:00
1 parent 5f78168c57
commit cbce83581a
10 files changed
+148 -81

No files matched your search

+8
View File
@@ -33,6 +33,10 @@ project yet; it needs a 32-bit Winegcc and 32-bit GLib/GIO development setup.
## Build ## 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 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 `$HOME/.local/libexec/cthulhu/wine`; running it as root installs system wide to
`/usr/libexec/cthulhu/wine`: `/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 even when the labels were created after the fields. Application-owned MSAA
lists retain their own names, roles and item data. 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 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
@@ -8,8 +8,8 @@ url='https://git.stormux.org/storm/wine-access'
license=('LGPL-2.1-or-later') license=('LGPL-2.1-or-later')
depends=('glib2' 'wine') depends=('glib2' 'wine')
makedepends=('cmake' 'git' 'pkgconf' 'mingw-w64-gcc') makedepends=('cmake' 'git' 'pkgconf' 'mingw-w64-gcc')
provides=('wine-access') provides=('wine-access' 'cthulhu-wine-access')
conflicts=('wine-access') conflicts=('wine-access' 'cthulhu-wine-access')
# Wine's import generation needs native object symbols, which LTO hides. # Wine's import generation needs native object symbols, which LTO hides.
options=('!lto') 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 WINE_ACCESS_GIT_URL=https://git.stormux.org/storm/wine-access.git makepkg -si
``` ```
The package installs both Winelib artifacts under The package installs the Winelib launcher, its sidecar and the native hook DLL
`/usr/libexec/cthulhu/wine/`. If your reader does not discover that system 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: location automatically, set this in the environment of the reader's service:
```sh ```sh
+9 -29
View File
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: LGPL-2.1-or-later // SPDX-License-Identifier: LGPL-2.1-or-later
#include "list_view.h" #include "list_view.h"
#include "control_class.h" #include "control_class.h"
#include "native_query.h"
#include <commctrl.h> #include <commctrl.h>
#include <cstdint> #include <cstdint>
#include <cstddef> #include <cstddef>
@@ -8,10 +9,7 @@
#include <iterator> #include <iterator>
namespace { namespace {
bool query(HWND window, UINT message, WPARAM wp, LPARAM lp, DWORD_PTR *result) { using native_control::query;
return SendMessageTimeoutW(window, message, wp, lp, SMTO_ABORTIFHUNG,
100, result) != 0;
}
// LVM_GETITEMTEXT is not marshalled by USER32. The structure and text buffer // LVM_GETITEMTEXT is not marshalled by USER32. The structure and text buffer
// must live in the target process, with pointers matching its architecture. // 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; DWORD_PTR state = 0;
if (query(window, LVM_GETITEMSTATE, row.index, LVIS_SELECTED, &state)) if (query(window, LVM_GETITEMSTATE, row.index, LVIS_SELECTED, &state))
row.selected = state & LVIS_SELECTED; row.selected = state & LVIS_SELECTED;
DWORD processId = 0; native_control::RemoteBuffer remote(window, bufferSize);
GetWindowThreadProcessId(window, &processId); if (!remote) return row;
HANDLE process = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_READ | const bool wow64 = remote.is32Bit();
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; DWORD_PTR header = 0, columnCount = 1;
if (query(window, LVM_GETHEADER, 0, 0, &header) && header != 0) { if (query(window, LVM_GETHEADER, 0, 0, &header) && header != 0) {
DWORD_PTR columns = 0; DWORD_PTR columns = 0;
@@ -95,7 +78,7 @@ ListViewRow read_list_view(HWND window) {
if (columnCount > 8) columnCount = 8; if (columnCount > 8) columnCount = 8;
for (DWORD_PTR column = 0; column < columnCount; ++column) { for (DWORD_PTR column = 0; column < columnCount; ++column) {
BYTE buffer[bufferSize] = {}; BYTE buffer[bufferSize] = {};
const auto textAddress = reinterpret_cast<uintptr_t>(remote) + textOffset; const auto textAddress = remote.address(textOffset);
if (wow64) { if (wow64) {
Item32 item{LVIF_TEXT, row.index, static_cast<int32_t>(column), Item32 item{LVIF_TEXT, row.index, static_cast<int32_t>(column),
0, 0, static_cast<uint32_t>(textAddress), textCapacity}; 0, 0, static_cast<uint32_t>(textAddress), textCapacity};
@@ -107,10 +90,9 @@ ListViewRow read_list_view(HWND window) {
} }
DWORD_PTR result = 0; DWORD_PTR result = 0;
wchar_t text[textCapacity] = {}; wchar_t text[textCapacity] = {};
if (!WriteProcessMemory(process, remote, buffer, sizeof(buffer), nullptr) || if (!remote.write(buffer, sizeof(buffer)) ||
!query(window, LVM_GETITEMTEXTW, row.index, reinterpret_cast<LPARAM>(remote), &result) || !query(window, LVM_GETITEMTEXTW, row.index, static_cast<LPARAM>(remote.address()), &result) ||
!ReadProcessMemory(process, reinterpret_cast<void *>(textAddress), text, !remote.read(text, sizeof(text), textOffset))
sizeof(text), nullptr))
break; break;
text[textCapacity - 1] = L'\0'; text[textCapacity - 1] = L'\0';
if (!text[0]) continue; if (!text[0]) continue;
@@ -120,7 +102,5 @@ ListViewRow read_list_view(HWND window) {
if (!row.text.empty()) row.text += ", "; if (!row.text.empty()) row.text += ", ";
row.text += utf8; row.text += utf8;
} }
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
CloseHandle(process);
return row; return row;
} }
+21 -10
View File
@@ -140,15 +140,21 @@ std::string accessible_property(
// Standard-control queries follow the LGPL wine-a11y reader published by // Standard-control queries follow the LGPL wine-a11y reader published by
// Mudb0y, while presentation remains on Cthulhu's D-Bus interface. // 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; wchar_t *output = text;
for (wchar_t *input = text; *input != L'\0'; ++input) { for (wchar_t *input = text; *input != L'\0'; ++input) {
if (*input != L'&') if (*input == L'&') {
*output++ = *input; if (input[1] != L'&') continue;
++input;
}
*output++ = *input;
} }
*output = L'\0'; *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) { 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, reinterpret_cast<LPARAM>(text), SMTO_ABORTIFHUNG,
200, &result)) 200, &result))
return false; 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'; 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)) { if (label && get_control_text(label, text, capacity)) {
tidy_text(text, true); trim_label(text);
return; 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")) if (!wine_access_class_matches(className, L"Static") && !wine_access_class_matches(className, L"Button"))
continue; continue;
if (get_control_text(previous, text, capacity)) if (get_control_text(previous, text, capacity))
tidy_text(text, true); trim_label(text);
return; return;
} }
} }
@@ -218,7 +231,6 @@ void find_control_label(HWND window, wchar_t *text, int capacity) {
std::string window_title(HWND window) { std::string window_title(HWND window) {
wchar_t title[512] = {}; wchar_t title[512] = {};
GetWindowTextW(GetAncestor(window, GA_ROOT), title, std::size(title)); GetWindowTextW(GetAncestor(window, GA_ROOT), title, std::size(title));
tidy_text(title);
return to_utf8(title); return to_utf8(title);
} }
@@ -362,7 +374,6 @@ ControlDescription describe_standard_control(HWND window) {
SendMessageTimeoutW(window, LB_GETTEXT, selected, SendMessageTimeoutW(window, LB_GETTEXT, selected,
reinterpret_cast<LPARAM>(text), SMTO_ABORTIFHUNG, reinterpret_cast<LPARAM>(text), SMTO_ABORTIFHUNG,
200, &length)) { 200, &length)) {
tidy_text(text);
control.value = to_utf8(text); control.value = to_utf8(text);
} }
control.position = static_cast<int>(selected) + 1; control.position = static_cast<int>(selected) + 1;
+9 -25
View File
@@ -1,16 +1,14 @@
// SPDX-License-Identifier: LGPL-2.1-or-later // SPDX-License-Identifier: LGPL-2.1-or-later
#include "native_items.h" #include "native_items.h"
#include "control_class.h" #include "control_class.h"
#include "native_query.h"
#include <commctrl.h> #include <commctrl.h>
#include <cstddef> #include <cstddef>
#include <cstring> #include <cstring>
#include <iterator> #include <iterator>
namespace { namespace {
bool query(HWND window, UINT message, WPARAM wp, LPARAM lp, DWORD_PTR *result) { using native_control::query;
return SendMessageTimeoutW(window, message, wp, lp, SMTO_ABORTIFHUNG,
100, result) != 0;
}
bool is_tab(HWND window) { bool is_tab(HWND window) {
wchar_t className[128] = {}; wchar_t className[128] = {};
return GetClassNameW(window, className, std::size(className)) && return GetClassNameW(window, className, std::size(className)) &&
@@ -75,23 +73,11 @@ NativeItem read_native_item(HWND window) {
const auto key = native_item_key(window); const auto key = native_item_key(window);
item.key = key; item.key = key;
if (!key.item) return item; if (!key.item) return item;
DWORD processId = 0; native_control::RemoteBuffer remote(window, bufferSize);
GetWindowThreadProcessId(window, &processId); if (!remote) return item;
HANDLE process = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_READ | const bool wow64 = remote.is32Bit();
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] = {}; BYTE buffer[bufferSize] = {};
const auto address = reinterpret_cast<uintptr_t>(remote) + textOffset; const auto address = remote.address(textOffset);
if (tab && wow64) { if (tab && wow64) {
const Tab32 data{TCIF_TEXT, 0, 0, static_cast<uint32_t>(address), textCapacity}; const Tab32 data{TCIF_TEXT, 0, 0, static_cast<uint32_t>(address), textCapacity};
memcpy(buffer, &data, sizeof(data)); memcpy(buffer, &data, sizeof(data));
@@ -108,10 +94,10 @@ NativeItem read_native_item(HWND window) {
memcpy(buffer, &data, sizeof(data)); memcpy(buffer, &data, sizeof(data));
} }
DWORD_PTR result = 0; 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, query(window, tab ? TCM_GETITEMW : TVM_GETITEMW, tab ? key.item - 1 : 0,
reinterpret_cast<LPARAM>(remote), &result) && result && static_cast<LPARAM>(remote.address()), &result) && result &&
ReadProcessMemory(process, remote, buffer, sizeof(buffer), nullptr)) { remote.read(buffer, sizeof(buffer))) {
wchar_t text[textCapacity] = {}; wchar_t text[textCapacity] = {};
memcpy(text, buffer + textOffset, sizeof(text)); memcpy(text, buffer + textOffset, sizeof(text));
text[textCapacity - 1] = L'\0'; 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; return item;
} }
+58
View File
@@ -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
+6
View File
@@ -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 remain regression checks; an application-owned MSAA provider in the menu
fixture must retain its role instead of being replaced by native queries. 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 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 retrieved `All Music`, `Album List`, and `Default Playlist` from the open
application. This identifies data missed by the old helper; it does not prove application. This identifies data missed by the old helper; it does not prove
+13 -8
View File
@@ -4,7 +4,7 @@
#include <windows.h> #include <windows.h>
#include <commctrl.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 HTREEITEM root;
static int step; 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 14: SendMessageW(atlTabs, TCM_DELETEALLITEMS, 0, 0); break;
case 15: focus_control(tree); break; case 15: focus_control(tree); break;
case 16: SendMessageW(tree, TVM_DELETEITEM, 0, (LPARAM)TVI_ROOT); break; case 16: SendMessageW(tree, TVM_DELETEITEM, 0, (LPARAM)TVI_ROOT); break;
case 17: focus_control(list); break;
default: DestroyWindow(window); break; default: DestroyWindow(window); break;
} }
return 0; return 0;
@@ -52,7 +53,7 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR args, int show)
cls.lpszClassName = L"WineAccessControlsFixture"; cls.lpszClassName = L"WineAccessControlsFixture";
cls.lpfnWndProc = window_proc; cls.lpfnWndProc = window_proc;
RegisterClassW(&cls); 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); 120, 120, 700, 440, NULL, NULL, instance, NULL);
WNDCLASSEXW tabClass = {0}; WNDCLASSEXW tabClass = {0};
tabClass.cbSize = sizeof(tabClass); 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 β"; node.item.pszText = L"Artist & album β";
SendMessageW(tree, TVM_INSERTITEMW, 0, (LPARAM)&node); SendMessageW(tree, TVM_INSERTITEMW, 0, (LPARAM)&node);
SendMessageW(tree, TVM_SELECTITEM, TVGN_CARET, (LPARAM)root); 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); 10, 310, 250, 30, window, NULL, instance, NULL);
combo = CreateWindowW(L"ComboBox", L"", WS_CHILD | WS_VISIBLE | WS_TABSTOP | CBS_DROPDOWNLIST, combo = CreateWindowW(L"ComboBox", L"", WS_CHILD | WS_VISIBLE | WS_TABSTOP | CBS_DROPDOWNLIST,
390, 110, 200, 150, window, NULL, instance, NULL); 390, 110, 200, 150, window, NULL, instance, NULL);
SendMessageW(combo, CB_ADDSTRING, 0, (LPARAM)L"by artist"); SendMessageW(combo, CB_ADDSTRING, 0, (LPARAM)L"artist & album");
SendMessageW(combo, CB_ADDSTRING, 0, (LPARAM)L"by year"); SendMessageW(combo, CB_ADDSTRING, 0, (LPARAM)L"year && date");
SendMessageW(combo, CB_SETCURSEL, 0, 0); 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); 390, 170, 200, 25, window, NULL, instance, NULL);
// Labels are deliberately created after their fields, like foobar2000. // 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); 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); 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); ShowWindow(window, SW_SHOW);
SetForegroundWindow(window); SetForegroundWindow(window);
focus_control(tabs); focus_control(tabs);
+10 -5
View File
@@ -121,14 +121,19 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
for e in events), "collapsed tree selection missing" 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] == "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[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 e in events), "named generic MSAA client hid button role"
for value in ("by artist", "by year"): for value in ("artist & album", "year && date"):
assert any(e[2] == "View" and e[3] == "combo box" and e[4] == value 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}" 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: if not args.is32Bit:
assert any(e[2] == "Filter" and e[3] == "edit" and e[4] == "search" assert any(e[2] == "Filter & query" and e[3] == "edit" and e[4] == "search & find"
for e in events), "late native edit label missing" 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) assert any(e[2] == "" and e[3] == "page tab" and e[7:9] == (0, 0)
for e in events), "empty tabs retained stale caption" 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 any(e[2] == "" and e[3] == "tree" for e in events), "empty tree retained stale item"