Provide fresh Wine review snapshots and 32-bit Edit hooks

Expose visible Wine window text to the matching Go reader's independent
keypad review commands, and extend standard Edit feedback to 32-bit programs
such as MUSHclient without requiring 32-bit Linux GLib/Winelib libraries.

Add lifetime-aware instance/window/control identities and bounded capture
of standard Edit text, button/static captions, native ListBox/ListView items,
and usable MSAA text. Preserve focus, caret, selection, scrolling, and the
clipboard. Exclude hidden/password content before retrieval, retain both
rune and UTF-16 offsets, clip visible ranges, and reject changing or
oversized sources rather than returning incomplete text as a full snapshot.

Export CaptureWindow on the version-1 WineReview D-Bus interface. Register
with the current reader service owner and publish copied focus metadata.
Authorize capture only for the current same-user owner. Use a dedicated
GLib dispatcher and separate COM worker with a 500 ms deadline, one active
job and one pending job, bounded payloads, and explicit stale/busy/limited/
unavailable replies. Keep menu and focus forwarding responsive when an MSAA
provider stalls; retain ordinary feedback with older readers.

Read the actual X11 geometry associated with Wine HWNDs using bounded XCB
queries and __wine_x11_whole_window. Translate virtual-desktop coordinates
and recheck physical geometry before returning captured text. Link XCB in
the helper/tests and declare libxcb in the Arch package dependencies.

Build edit_hook.c into the 32-bit DLL and attach both menu and input hooks
to the foreground 32-bit UI thread. Release both hooks on transitions or
attachment failure. Keep Scintilla-specific hooks confined to 64-bit builds.
Expand Edit integration checks with read-only multiline navigation and
unsuccessful deletion, plus 32-bit editor/Windows Forms lifecycle support.

Add 32-bit/64-bit native capture fixtures, wire-protocol integration checks,
and design/implementation documents. Document exact schema, capture limits,
installation dependencies, compatibility, and live acceptance procedures.

Validation during commit preparation:
- git diff --check passed.
- cmake --build build --parallel passed.
- CTest could not start because xvfb-run is unavailable in this environment.
- Further tests were skipped at the user's request; capture, protocol, and
  menu/editor integration tests were not run during commit preparation.

Live speech and physical braille checks remain necessary, including review
without moving the native caret, incoming output, plugin dialogs, and return
to Linux/Fenrir. Custom painted output requires an accessible provider;
RichEdit/Scintilla capture, semantic object review, Say All, typing echo, and
native Wine braille routing remain unsupported by this protocol.
This commit is contained in:
deedra committed 2026-10-11 20:50:56 -07:00
1 parent 872388a59b
commit a8cb350467
21 files changed
+2401 -38

No files matched your search

+139
View File
@@ -0,0 +1,139 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
#include <windows.h>
#include <oleacc.h>
#include <cstdio>
#include <algorithm>
#include <string>
#include <vector>
#if __has_include("../review_capture.h")
#include "../review_capture.h"
#define HAS_CAPTURE 1
#endif
static bool check(bool value, const char *message) {
if (!value) std::fprintf(stderr, "FAIL: %s\n", message);
return value;
}
int main(int argc, char **argv) {
if (argc != 3 || std::string(argv[1]) != "--fixture") return 2;
std::string dos = argv[2];
if (dos.starts_with('/')) dos = "Z:" + dos;
std::replace(dos.begin(), dos.end(), '/', '\\');
std::string quoted = "\"" + dos + "\"";
int units = MultiByteToWideChar(CP_UTF8, 0, quoted.c_str(), -1, nullptr, 0);
std::vector<WCHAR> command(units);
MultiByteToWideChar(CP_UTF8, 0, quoted.c_str(), -1, command.data(), units);
STARTUPINFOW startup{}; startup.cb = sizeof(startup);
PROCESS_INFORMATION process{};
if (!CreateProcessW(nullptr, command.data(), nullptr, nullptr, FALSE, 0,
nullptr, nullptr, &startup, &process)) {
std::fprintf(stderr, "FAIL: fixture starts (%s, Windows error %lu)\n", argv[2], static_cast<unsigned long>(GetLastError()));
return 2;
}
HWND window = nullptr;
ULONGLONG deadline = GetTickCount64() + 10000;
while (!(window = FindWindowW(L"WineAccessReviewFixture", nullptr)) && GetTickCount64() < deadline) Sleep(20);
bool ok = check(window != nullptr, "fixture becomes ready");
if (window) {
Sleep(100);
#ifndef HAS_CAPTURE
ok = check(false, "review capture unavailable");
#else
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
std::fprintf(stderr, "capture test: COM ready\n");
ReviewIdentityStore store;
auto capture = [&]() {
auto focus = store.observe_foreground();
std::fprintf(stderr, "capture test: focus %s\n", focus.focus.c_str());
return capture_review_window(store, focus, GetTickCount64() + 500);
};
auto joined = [](const ReviewCapture &data) {
std::string all;
for (const auto &node : data.nodes) {
all += node.name;
for (const auto &range : node.ranges) all += range.text + "\n";
}
return all;
};
auto data = capture();
std::fprintf(stderr, "capture test: initial %s\n", data.status.c_str());
ok &= check(data.status == "ok", "initial capture succeeds");
const auto text = joined(data);
ok &= check(text.find("alpha beta") != std::string::npos, "visible Edit text");
ok &= check(text.find("output second") != std::string::npos, "read-only output text");
ok &= check(text.find("Open & close") != std::string::npos, "caption mnemonics and literal ampersand");
ok &= check(text.find("native & item") != std::string::npos, "native MSAA list items retain literal ampersands");
ok &= check(text.find("private-review-secret") == std::string::npos && text.find("hidden-review-secret") == std::string::npos,
"password and hidden contents excluded");
ok &= check(SendMessageW(window, WM_APP + 11, 0, 0), "review preserves focus and selection");
for (int mode : {1, 2, 3, 4, 5, 6, 7, 8, 9, 14, 15, 17, 18, 21, 22, 23, 24, 25}) {
std::fprintf(stderr, "capture test: mode %d\n", mode);
SendMessageW(window, WM_APP + 10, mode, 0);
auto result = capture();
const auto value = joined(result);
if (mode == 7) ok &= check(result.status == "limited" && result.nodes.empty(), "source limit rejects partial capture");
else if (mode == 9) ok &= check(result.status == "stale" && result.nodes.empty(), "same-length update during capture rejects mixed data");
else if (mode == 14 || mode == 15 || mode == 18 || mode == 23) ok &= check(result.status == "stale" && result.nodes.empty(), "protection/list/viewport transition rejects old capture");
else if (mode == 21) ok &= check(result.status != "ok" && result.nodes.empty(), "delayed native call respects capture deadline");
else ok &= check(result.status == "ok", "configured capture succeeds");
ok &= check(value.find("private-review-secret") == std::string::npos && value.find("hidden-review-secret") == std::string::npos,
"all captures exclude protected/hidden text");
if (mode == 1) {
bool found = false;
for (const auto &node : result.nodes) for (const auto &range : node.ranges)
if (range.text == "beta") {
found = range.start == 8 && range.end == 12 && range.utf16_start == 9 && range.utf16_end == 13;
}
ok &= check(found, "surrogate before viewport retains absolute rune offsets");
}
if (mode == 2) {
int wrapped = 0; bool blank = false;
for (const auto &node : result.nodes) for (const auto &range : node.ranges) {
if (range.text.find("wrapped") != std::string::npos) ++wrapped;
if (range.text == "\n") blank = true;
}
ok &= check(wrapped > 1 && blank, "visual wraps and blank lines");
}
if (mode == 3) ok &= check(value.find("output second") != std::string::npos && !result.source.focus.empty(), "empty command remains review anchor");
if (mode == 5) ok &= check(value.find("alpha beta") == std::string::npos, "partly clipped text is excluded");
if (mode == 6) ok &= check(value.find("omega zeta") != std::string::npos && value.find("alpha beta") == std::string::npos, "same-length replacement refreshes text");
if (mode == 8) ok &= check(result.source.bounds.left < 0, "negative screen coordinates remain valid");
if (mode == 14) ok &= check(SendMessageW(window,WM_APP + 14,0,0), "password transition is excluded before copying text");
if (mode == 17) ok &= check(value.find("table & item") != std::string::npos && value.find("ready") != std::string::npos, "visible native ListView cells retain text and ampersands");
if (mode == 25) ok &= check(value.find("literal & label") != std::string::npos, "SS_NOPREFIX captions retain literal ampersands");
if (mode == 21) {
Sleep(750);
ok &= check(SendMessageW(window,WM_APP + 15,0,0), "remote buffer lives until the delayed target returns");
}
if (mode == 22 || mode == 24) {
bool visible=false;
auto field=store.identity(GetDlgItem(window,mode == 22 ? 109 : 110));
for (const auto &node:result.nodes) if (node.id == field) for (const auto &range:node.ranges)
if (range.utf16_start > 0 && !range.text.empty()) visible=true;
if (!visible) {
HWND control=GetDlgItem(window,mode == 22 ? 109 : 110);
std::fprintf(stderr,"scroll diagnostic first=%lld scroll=%d lines=%lld style=%llx\n",
static_cast<long long>(SendMessageW(control,EM_GETFIRSTVISIBLELINE,0,0)),GetScrollPos(control,SB_HORZ),
static_cast<long long>(SendMessageW(control,EM_GETLINECOUNT,0,0)),static_cast<unsigned long long>(GetWindowLongPtrW(control,GWL_STYLE)));
RECT format{}; SendMessageW(control,EM_GETRECT,0,reinterpret_cast<LPARAM>(&format));
auto length=SendMessageW(control,WM_GETTEXTLENGTH,0,0);
std::fprintf(stderr,"scroll length=%lld positions=%llx/%llx format=%d,%d,%d,%d\n",static_cast<long long>(length),
static_cast<unsigned long long>(SendMessageW(control,EM_POSFROMCHAR,0,0)),static_cast<unsigned long long>(SendMessageW(control,EM_POSFROMCHAR,length-1,0)),
format.left,format.top,format.right,format.bottom);
for (const auto &node:result.nodes) if (node.id == field) for (const auto &range:node.ranges)
std::fprintf(stderr,"scroll range %llu %llu %s\n",static_cast<unsigned long long>(range.utf16_start),static_cast<unsigned long long>(range.utf16_end),range.text.c_str());
}
ok &= check(visible, "horizontally scrolled Edit retains visible text and absolute offsets");
}
if (mode != 4) ok &= check(SendMessageW(window, WM_APP + 11, 0, 0), "capture leaves caret and focus unchanged");
}
CoUninitialize();
#endif
PostMessageW(window, WM_CLOSE, 0, 0);
}
if (WaitForSingleObject(process.hProcess, 3000) != WAIT_OBJECT_0) TerminateProcess(process.hProcess, 2);
CloseHandle(process.hThread); CloseHandle(process.hProcess);
if (ok) std::puts("PASS: native visible Wine review capture and independent caret");
return ok ? 0 : 1;
}