903 lines
45 KiB
C++
903 lines
45 KiB
C++
// SPDX-License-Identifier: LGPL-2.1-or-later
|
|
#include "review_capture.h"
|
|
#include "control_class.h"
|
|
#include "native_query.h"
|
|
#include "review_x11.h"
|
|
#include "review_query.h"
|
|
#include <commctrl.h>
|
|
#include <cstring>
|
|
#include <oleacc.h>
|
|
#include <algorithm>
|
|
#include <cstdio>
|
|
#include <limits>
|
|
#include <optional>
|
|
#include <unordered_set>
|
|
#include <unordered_map>
|
|
|
|
namespace {
|
|
// DF_WINE_VIRTUAL_DESKTOP from Wine's ntuser.h (that internal C header is not
|
|
// C++ compatible). GetUserObjectInformation returns these desktop flags.
|
|
constexpr DWORD wine_virtual_desktop = 0x80000000;
|
|
// Wine's import libraries do not export this IID as a Unix data symbol.
|
|
constexpr GUID accessible_iid{0x618736e0,0x3c3d,0x11cf,{0x81,0x0c,0x00,0xaa,0x00,0x38,0x9b,0x71}};
|
|
constexpr size_t max_source = 1000000, max_text = 1048576, max_nodes = 4096;
|
|
std::string utf8(const WCHAR *text, size_t length) {
|
|
if (!length) return {};
|
|
int size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, text,
|
|
static_cast<int>(length), nullptr, 0, nullptr, nullptr);
|
|
if (!size) return {};
|
|
std::string value(size, '\0');
|
|
WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, text,
|
|
static_cast<int>(length), value.data(), size, nullptr, nullptr);
|
|
return value;
|
|
}
|
|
std::string accessible_text(const WCHAR *text, size_t length) {
|
|
// Some MSAA BSTRs retain the window's reported length past its text's
|
|
// terminating NUL. D-Bus cannot represent those bytes, and the padding
|
|
// need not contain text. Keep only the terminated label for display;
|
|
// preserve the original UTF-16 separately for the stale check. Truncate
|
|
// before conversion: padding may contain malformed surrogate pairs.
|
|
const auto end=std::find(text,text+length,L'\0');
|
|
return utf8(text,end-text);
|
|
}
|
|
bool query(HWND window, UINT message, WPARAM wp, LPARAM lp, uint64_t deadline,
|
|
DWORD_PTR &result) {
|
|
uint64_t now = GetTickCount64();
|
|
return now < deadline && SendMessageTimeoutW(window, message, wp, lp,
|
|
SMTO_ABORTIFHUNG | SMTO_BLOCK, static_cast<UINT>(std::min<uint64_t>(100, deadline - now)), &result);
|
|
}
|
|
bool remote_query(HWND window, UINT message, WPARAM wp, LPARAM lp,
|
|
uint64_t deadline, DWORD_PTR &result) {
|
|
if (GetTickCount64() >= deadline) return false;
|
|
// Custom control messages are not marshalled. A timeout does not mean the
|
|
// target stopped dereferencing our remote buffer. Wait for real completion
|
|
// on this capture STA; the independent dispatcher expires the bus reply.
|
|
result=SendMessageW(window,message,wp,lp);
|
|
return GetTickCount64() < deadline;
|
|
}
|
|
bool password(HWND window);
|
|
bool read_text(HWND window, uint64_t deadline, size_t limit, std::vector<WCHAR> &text,
|
|
bool &limited) {
|
|
DWORD_PTR size = 0, copied = 0;
|
|
if (password(window) || !query(window, WM_GETTEXTLENGTH, 0, 0, deadline, size) || password(window)) return false;
|
|
if (size > limit) { limited = true; return false; }
|
|
text.assign(size + 1, 0);
|
|
if (!query(window, WM_GETTEXT, text.size(), reinterpret_cast<LPARAM>(text.data()), deadline, copied) || password(window)) { text.clear(); return false; }
|
|
// A grow/shrink between these messages is rejected, never truncated.
|
|
DWORD_PTR after = 0;
|
|
if (!query(window, WM_GETTEXTLENGTH, 0, 0, deadline, after) || copied != size || after != size) return false;
|
|
text.resize(size);
|
|
return true;
|
|
}
|
|
std::string caption(HWND window) {
|
|
// A foreign window's caption is held by USER32. Sending WM_GETTEXT here
|
|
// could deadlock against an editor hook currently sending WM_COPYDATA to
|
|
// the event thread, dropping ordinary focus feedback after its 50 ms wait.
|
|
int length=GetWindowTextLengthW(window);
|
|
if (length <= 0 || length > 4096) return {};
|
|
std::vector<WCHAR> value(length+1);
|
|
int copied=GetWindowTextW(window,value.data(),static_cast<int>(value.size()));
|
|
auto result = utf8(value.data(),copied);
|
|
return result.size() <= 4096 ? result : std::string{};
|
|
}
|
|
bool password(HWND window) {
|
|
WCHAR cls[128]{};
|
|
GetClassNameW(window, cls, 128);
|
|
return wine_access_class_matches(cls, L"Edit") && (GetWindowLongPtrW(window, GWL_STYLE) & ES_PASSWORD);
|
|
}
|
|
bool equal_focus(const ReviewFocus &a, const ReviewFocus &b) {
|
|
return a.instance == b.instance && a.window == b.window && a.focus == b.focus &&
|
|
a.revision == b.revision && a.protected_text == b.protected_text &&
|
|
EqualRect(&a.bounds, &b.bounds);
|
|
}
|
|
std::string mnemonic(const std::string &value) {
|
|
std::string out;
|
|
for (size_t i = 0; i < value.size(); ++i) {
|
|
if (value[i] == '&') {
|
|
if (i + 1 < value.size() && value[i + 1] == '&') { out += '&'; ++i; }
|
|
} else out += value[i];
|
|
}
|
|
return out;
|
|
}
|
|
bool inside(const RECT &child, const RECT &parent) {
|
|
return child.right > child.left && child.bottom > child.top &&
|
|
child.left >= parent.left && child.top >= parent.top &&
|
|
child.right <= parent.right && child.bottom <= parent.bottom;
|
|
}
|
|
struct EditLayout {
|
|
RECT format{};
|
|
DWORD_PTR first=0, lines=0, font=0;
|
|
LONG_PTR style=0;
|
|
int horizontal=0, vertical=0;
|
|
};
|
|
bool edit_layout(HWND window, uint64_t deadline, EditLayout &layout) {
|
|
DWORD_PTR answer=0;
|
|
layout.style=GetWindowLongPtrW(window,GWL_STYLE);
|
|
layout.horizontal=GetScrollPos(window,SB_HORZ);
|
|
layout.vertical=GetScrollPos(window,SB_VERT);
|
|
if (!query(window, EM_GETRECT, 0, reinterpret_cast<LPARAM>(&layout.format), deadline, answer)) return false;
|
|
std::vector<ReviewQuery> fields{{EM_GETFIRSTVISIBLELINE,0},{EM_GETLINECOUNT,0},{WM_GETFONT,0}};
|
|
if (!review_query_many(window, fields, deadline)) return false;
|
|
layout.first = fields[0].answer;
|
|
layout.lines = fields[1].answer;
|
|
layout.font = fields[2].answer;
|
|
return GetTickCount64() < deadline;
|
|
}
|
|
bool same_layout(const EditLayout &a, const EditLayout &b) {
|
|
return EqualRect(&a.format,&b.format) && a.first == b.first && a.lines == b.lines &&
|
|
a.font == b.font && a.style == b.style && a.horizontal == b.horizontal && a.vertical == b.vertical;
|
|
}
|
|
struct SourceRead {
|
|
HWND window; std::vector<WCHAR> text; RECT bounds;
|
|
std::optional<EditLayout> layout{};
|
|
std::vector<std::pair<size_t,DWORD_PTR>> positions{};
|
|
};
|
|
struct AccessibleRead {
|
|
IAccessible *object;
|
|
LONG child;
|
|
std::vector<WCHAR> name, value;
|
|
bool read_value;
|
|
RECT bounds;
|
|
};
|
|
struct ListRead { HWND window; size_t index; std::vector<WCHAR> text; RECT bounds; int column = -1; };
|
|
struct MdiRead { HWND window, active; RECT bounds; };
|
|
struct CaptionRead { HWND window; std::string text; RECT bounds; };
|
|
struct ReviewItem32 {
|
|
uint32_t mask; int32_t item, subitem; uint32_t state, state_mask, text; int32_t capacity;
|
|
};
|
|
struct ReviewItem64 {
|
|
uint32_t mask; int32_t item, subitem; uint32_t state, state_mask, padding;
|
|
uint64_t text; int32_t capacity; uint32_t padding2;
|
|
};
|
|
static_assert(offsetof(ReviewItem32,text) == 20);
|
|
static_assert(offsetof(ReviewItem64,text) == 24);
|
|
struct Capture {
|
|
ReviewIdentityStore &store;
|
|
ReviewCapture result;
|
|
uint64_t deadline;
|
|
size_t source_units = 0, text_bytes = 0;
|
|
size_t serialized_estimate = 512;
|
|
std::vector<SourceRead> reads;
|
|
std::vector<AccessibleRead> accessible_reads;
|
|
std::vector<ListRead> list_reads;
|
|
std::vector<MdiRead> mdi_reads;
|
|
std::vector<CaptionRead> caption_reads;
|
|
std::unordered_set<HWND> native;
|
|
size_t visited = 0;
|
|
bool failed = false;
|
|
~Capture() {
|
|
for (auto &read : accessible_reads) read.object->Release();
|
|
}
|
|
|
|
void fail(const char *status) { failed = true; result.status = status; }
|
|
bool add(ReviewNode node) {
|
|
if (GetTickCount64() >= deadline) { fail("unavailable"); return false; }
|
|
if (result.nodes.size() >= max_nodes || node.name.size() > 4096 || node.id.size() > 256) {
|
|
fail("limited"); return false;
|
|
}
|
|
text_bytes += node.name.size();
|
|
serialized_estimate += 512 + node.id.size() + node.parent.size() + node.role.size() + node.name.size();
|
|
for (const auto &range : node.ranges) {
|
|
text_bytes += range.text.size(); serialized_estimate += 128 + range.text.size();
|
|
}
|
|
// Reserve ample metadata space within the 2 MiB serialized bound.
|
|
if (text_bytes > max_text || serialized_estimate > 1800000) {
|
|
fail("limited"); return false;
|
|
}
|
|
result.nodes.push_back(std::move(node));
|
|
return true;
|
|
}
|
|
bool edit(HWND window, ReviewNode &node, const RECT &clip) {
|
|
// Exclude protected text before WM_GETTEXT or any provider value call.
|
|
if (node.protected_text) return true;
|
|
std::vector<WCHAR> text;
|
|
bool limited = false;
|
|
if (!read_text(window, deadline, max_source - source_units, text, limited)) {
|
|
fail(limited ? "limited" : "stale"); return false;
|
|
}
|
|
source_units += text.size();
|
|
EditLayout layout{};
|
|
if (!edit_layout(window,deadline,layout)) { fail("unavailable"); return false; }
|
|
std::vector<std::pair<size_t,DWORD_PTR>> positions;
|
|
std::unordered_map<size_t,DWORD_PTR> position_cache;
|
|
auto position_at = [&](size_t index, DWORD_PTR &position) {
|
|
auto cached = position_cache.find(index);
|
|
if (cached != position_cache.end()) { position = cached->second; return true; }
|
|
if (!query(window,EM_POSFROMCHAR,index,0,deadline,position) ||
|
|
position == static_cast<DWORD_PTR>(-1)) return false;
|
|
position_cache.emplace(index,position);
|
|
positions.emplace_back(index,position);
|
|
return true;
|
|
};
|
|
// Map every native position, including invisible prefixes. Interior
|
|
// surrogate boundaries stay invalid rather than splitting a character.
|
|
std::vector<uint64_t> offsets(text.size() + 1, UINT64_MAX);
|
|
uint64_t runes = 0;
|
|
for (size_t i = 0; i < text.size();) {
|
|
offsets[i] = runes++;
|
|
if (text[i] >= 0xd800 && text[i] <= 0xdbff) {
|
|
if (i + 1 >= text.size() || text[i + 1] < 0xdc00 || text[i + 1] > 0xdfff) {
|
|
fail("unavailable"); return false;
|
|
}
|
|
i += 2;
|
|
} else {
|
|
if (text[i] >= 0xdc00 && text[i] <= 0xdfff) { fail("unavailable"); return false; }
|
|
++i;
|
|
}
|
|
}
|
|
offsets.back() = runes;
|
|
node.text_known = true;
|
|
DWORD start = 0, end = 0; DWORD_PTR answer = 0;
|
|
if (query(window, EM_GETSEL, reinterpret_cast<WPARAM>(&start), reinterpret_cast<LPARAM>(&end), deadline, answer) &&
|
|
end < offsets.size() && offsets[end] != UINT64_MAX) {
|
|
node.caret_known = true; node.caret_utf16 = end; node.caret = offsets[end];
|
|
}
|
|
RECT format=layout.format;
|
|
POINT origin{0, 0}; ClientToScreen(window, &origin);
|
|
RECT client{}; GetClientRect(window, &client); OffsetRect(&client, origin.x, origin.y);
|
|
RECT visible{};
|
|
RECT original_bounds{}; GetWindowRect(window, &original_bounds);
|
|
if (!IntersectRect(&visible, &client, &clip)) {
|
|
reads.push_back({window,std::move(text),original_bounds,layout,{}}); return true;
|
|
}
|
|
HDC dc = GetDC(window);
|
|
if (!dc) { fail("unavailable"); return false; }
|
|
DWORD_PTR font = layout.font;
|
|
HGDIOBJ old = font ? SelectObject(dc, reinterpret_cast<HFONT>(font)) : nullptr;
|
|
TEXTMETRICW metrics{}; GetTextMetricsW(dc, &metrics);
|
|
DWORD_PTR lines=layout.lines, first=layout.first;
|
|
bool good=true;
|
|
const bool multiline = GetWindowLongPtrW(window, GWL_STYLE) & ES_MULTILINE;
|
|
if (!multiline) first = 0; // Single-line message returns character index.
|
|
const size_t native_first = first;
|
|
// Queue scalar lookups together so a frame-based application services a
|
|
// batch in one turn instead of sleeping between every character query.
|
|
// Skip rows above a parent clip using the standard Edit's fixed line
|
|
// height. Keep one extra row for a partly scrolled first line, and retain
|
|
// native positions below to check which rows are actually fully visible.
|
|
const LONG line_height = std::max<LONG>(1, metrics.tmHeight);
|
|
const int64_t hidden_height = static_cast<int64_t>(visible.top)-origin.y-format.top;
|
|
const size_t skipped = multiline && hidden_height > line_height ?
|
|
std::min<size_t>(lines > first ? lines-first : 0, hidden_height/line_height-1) : 0;
|
|
first += skipped;
|
|
size_t wanted_rows=static_cast<size_t>(std::max<int64_t>(1,
|
|
static_cast<int64_t>(visible.bottom)-origin.y-format.top)) /
|
|
line_height+2;
|
|
wanted_rows = wanted_rows > skipped ? wanted_rows-skipped : 0;
|
|
if (wanted_rows > 258 && lines > first && lines-first > 258) {
|
|
if (old) SelectObject(dc, old);
|
|
ReleaseDC(window, dc);
|
|
fail("limited"); return false;
|
|
}
|
|
size_t row_count=std::min<size_t>(lines>first ? lines-first:0,wanted_rows);
|
|
std::vector<ReviewQuery> indices,details,last_positions;
|
|
for (size_t row = 0; row < row_count; ++row) indices.push_back({EM_LINEINDEX, first+row});
|
|
good = review_query_many(window, indices, deadline);
|
|
if (good) for (const auto &index : indices) {
|
|
if (index.answer > text.size()) { good = false; break; }
|
|
details.push_back({EM_LINELENGTH, index.answer});
|
|
if (index.answer < text.size()) details.push_back({EM_POSFROMCHAR, index.answer});
|
|
}
|
|
if (good) good = review_query_many(window, details, deadline);
|
|
std::vector<DWORD_PTR> lengths;
|
|
if (good) {
|
|
size_t at = 0;
|
|
for (const auto &index : indices) {
|
|
auto length = details[at++].answer;
|
|
lengths.push_back(length);
|
|
if (length > text.size()-index.answer) { good = false; break; }
|
|
if (index.answer < text.size()) {
|
|
auto position = details[at++].answer;
|
|
position_cache.emplace(index.answer, position);
|
|
positions.emplace_back(index.answer, position);
|
|
if (static_cast<short>(HIWORD(position))+origin.y >= visible.bottom) continue;
|
|
}
|
|
if (length) {
|
|
size_t last = index.answer+length-1;
|
|
if (offsets[last] == UINT64_MAX) --last;
|
|
last_positions.push_back({EM_POSFROMCHAR, last});
|
|
}
|
|
}
|
|
}
|
|
if (good) good = review_query_many(window, last_positions, deadline);
|
|
if (good) for (const auto &position : last_positions)
|
|
if (position_cache.emplace(position.index, position.answer).second)
|
|
positions.emplace_back(position.index, position.answer);
|
|
size_t captured = 0;
|
|
for (size_t line = first; good && line-first < indices.size() && !failed; ++line) {
|
|
DWORD_PTR index = indices[line-first].answer, length = lengths[line-first], position = 0;
|
|
if (!good || index > text.size() || length > text.size() - index) { fail("stale"); break; }
|
|
// EM_POSFROMCHAR has no position for an empty final row. Its visual
|
|
// line geometry follows the control's actual font and format rectangle.
|
|
if (index < text.size()) {
|
|
good=position_at(index,position);
|
|
if (!good) { fail("unavailable"); break; }
|
|
} else {
|
|
position = MAKELONG(format.left, format.top + (line - native_first) * metrics.tmHeight);
|
|
}
|
|
LONG x = static_cast<short>(LOWORD(position)) + origin.x;
|
|
LONG y = static_cast<short>(HIWORD(position)) + origin.y;
|
|
if (y >= visible.bottom) break;
|
|
SIZE extent{};
|
|
if (length) GetTextExtentPoint32W(dc, text.data() + index, static_cast<int>(length), &extent);
|
|
RECT bounds{x, y, x + std::max<LONG>(1, extent.cx), y + metrics.tmHeight};
|
|
// Horizontal trimming cannot make a vertically clipped row visible.
|
|
// Avoid round trips for characters that will be excluded regardless.
|
|
if (bounds.top < visible.top || bounds.bottom > visible.bottom) continue;
|
|
size_t finish = index + length;
|
|
if (length) {
|
|
size_t last=finish-1;
|
|
if (offsets[last] == UINT64_MAX) --last;
|
|
DWORD_PTR last_position=0;
|
|
if (!position_at(last,last_position)) { good=false; break; }
|
|
SIZE last_extent{};
|
|
GetTextExtentPoint32W(dc,text.data()+last,static_cast<int>(finish-last),&last_extent);
|
|
LONG last_y=static_cast<short>(HIWORD(last_position))+origin.y;
|
|
if (last_y != y) { fail("stale"); break; }
|
|
bounds.right=static_cast<short>(LOWORD(last_position))+origin.x+std::max<LONG>(1,last_extent.cx);
|
|
}
|
|
if (length && !inside(bounds,visible)) {
|
|
// Read only fully displayed characters. Native positions locate the
|
|
// visible substring without scrolling or changing the selection;
|
|
// binary searches skip potentially huge horizontally hidden prefixes.
|
|
auto next = [&](size_t at) { return at+((text[at]>=0xd800 && text[at]<=0xdbff) ? 2 : 1); };
|
|
auto previous = [&](size_t at) {
|
|
--at; if (text[at]>=0xdc00 && text[at]<=0xdfff && at>index) --at; return at;
|
|
};
|
|
auto character_bounds = [&](size_t at, RECT &character) {
|
|
DWORD_PTR start_position=0, next_position=0;
|
|
if (!position_at(at,start_position)) return false;
|
|
LONG left=static_cast<short>(LOWORD(start_position))+origin.x;
|
|
LONG top=static_cast<short>(HIWORD(start_position))+origin.y;
|
|
SIZE size{};
|
|
GetTextExtentPoint32W(dc,text.data()+at,static_cast<int>(next(at)-at),&size);
|
|
LONG right=left+std::max<LONG>(1,size.cx);
|
|
if (next(at)<finish) {
|
|
if (!position_at(next(at),next_position)) return false;
|
|
LONG next_y=static_cast<short>(HIWORD(next_position))+origin.y;
|
|
LONG next_x=static_cast<short>(LOWORD(next_position))+origin.x;
|
|
if (next_y == top && next_x > left) right=next_x;
|
|
}
|
|
character={left,top,right,top+metrics.tmHeight};
|
|
return true;
|
|
};
|
|
RECT first_bounds{};
|
|
size_t begin=index, stop=finish;
|
|
if (!multiline) begin=std::min<size_t>(finish,std::max<size_t>(index,layout.first));
|
|
if (begin<stop && offsets[begin] == UINT64_MAX) ++begin;
|
|
if (begin<stop && !character_bounds(begin,first_bounds)) { good=false; break; }
|
|
if (begin<stop && first_bounds.left < visible.left) {
|
|
size_t low=begin, high=stop;
|
|
while (low<high) {
|
|
size_t mid=low+(high-low)/2;
|
|
if (offsets[mid] == UINT64_MAX) --mid;
|
|
RECT character{};
|
|
if (!character_bounds(mid,character)) { good=false; break; }
|
|
if (character.left < visible.left) low=next(mid); else high=mid;
|
|
}
|
|
if (!good) break;
|
|
begin=low;
|
|
}
|
|
if (begin == stop) continue;
|
|
RECT last_bounds{};
|
|
if (!character_bounds(previous(stop),last_bounds)) { good=false; break; }
|
|
if (last_bounds.right > visible.right) {
|
|
size_t low=begin, high=stop;
|
|
while (low<high) {
|
|
size_t mid=low+(high-low)/2;
|
|
if (offsets[mid] == UINT64_MAX) --mid;
|
|
RECT character{};
|
|
if (!character_bounds(mid,character)) { good=false; break; }
|
|
if (character.right <= visible.right) low=next(mid); else high=mid;
|
|
}
|
|
if (!good) break;
|
|
stop=low;
|
|
}
|
|
if (begin == stop) continue;
|
|
if (!character_bounds(begin,first_bounds) || !character_bounds(previous(stop),last_bounds)) { good=false; break; }
|
|
bounds={first_bounds.left,first_bounds.top,last_bounds.right,last_bounds.bottom};
|
|
if (!inside(bounds,visible)) continue;
|
|
index=begin; finish=stop;
|
|
}
|
|
if (!inside(bounds,visible)) continue;
|
|
// Keep only the actual LF of a blank CRLF row, with absolute offsets.
|
|
if (!length && index < text.size()) {
|
|
if (text[index] == '\r' && index + 1 < text.size() && text[index + 1] == '\n') ++index;
|
|
if (text[index] == '\n') finish = index + 1;
|
|
}
|
|
if (finish == index) continue;
|
|
if (offsets[index] == UINT64_MAX || offsets[finish] == UINT64_MAX) { fail("unavailable"); break; }
|
|
size_t count = offsets[finish] - offsets[index];
|
|
if (count > 4096 || captured + count > 65536 || node.ranges.size() >= 256) { fail("limited"); break; }
|
|
node.ranges.push_back({offsets[index], offsets[finish], index, finish,
|
|
utf8(text.data() + index, finish - index), bounds});
|
|
captured += count;
|
|
}
|
|
if (old) SelectObject(dc, old);
|
|
ReleaseDC(window, dc);
|
|
if (!good && !failed) fail("unavailable");
|
|
reads.push_back({window,std::move(text),original_bounds,layout,std::move(positions)});
|
|
return !failed;
|
|
}
|
|
bool list_item(HWND window, size_t index, std::vector<WCHAR> &text, RECT &bounds) {
|
|
DWORD_PTR length=0, result=0;
|
|
if (!query(window, LB_GETTEXTLEN, index, 0, deadline, length) || static_cast<LONG_PTR>(length) < 0) return false;
|
|
if (length > 4096) { fail("limited"); return false; }
|
|
text.assign(length+1,0);
|
|
if (!query(window, LB_GETTEXT, index, reinterpret_cast<LPARAM>(text.data()), deadline, result) || result != length ||
|
|
!query(window, LB_GETITEMRECT, index, reinterpret_cast<LPARAM>(&bounds), deadline, result) ||
|
|
static_cast<LONG_PTR>(result) == LB_ERR) return false;
|
|
text.resize(length);
|
|
POINT point{0,0}; ClientToScreen(window,&point); OffsetRect(&bounds,point.x,point.y);
|
|
return true;
|
|
}
|
|
void list(HWND window, const std::string &parent, const RECT &clip) {
|
|
LONG_PTR style=GetWindowLongPtrW(window,GWL_STYLE);
|
|
if ((style & (LBS_OWNERDRAWFIXED|LBS_OWNERDRAWVARIABLE)) && !(style & LBS_HASSTRINGS)) return;
|
|
DWORD_PTR count=0, first=0;
|
|
if (!query(window,LB_GETCOUNT,0,0,deadline,count) || static_cast<LONG_PTR>(count) < 0 ||
|
|
!query(window,LB_GETTOPINDEX,0,0,deadline,first)) { fail("unavailable"); return; }
|
|
for (size_t i=first; i<count && !failed; ++i) {
|
|
std::vector<WCHAR> text; RECT bounds{};
|
|
if (!list_item(window,i,text,bounds)) { if (!failed) fail("stale"); break; }
|
|
if (bounds.top >= clip.bottom) break;
|
|
if (!inside(bounds,clip)) continue;
|
|
auto value=utf8(text.data(),text.size());
|
|
ReviewNode node;
|
|
node.id=parent+"_l"+std::to_string(i); node.parent=parent; node.role="list item";
|
|
node.bounds=bounds; node.name=value; node.text_known=true;
|
|
uint64_t runes=0; for (unsigned char c:value) if ((c&0xc0)!=0x80) ++runes;
|
|
if (!value.empty()) node.ranges.push_back({0,runes,0,text.size(),value,bounds});
|
|
if (!add(std::move(node))) break;
|
|
list_reads.push_back({window,i,std::move(text),bounds});
|
|
}
|
|
}
|
|
bool table_item(HWND window, size_t index, int column, std::vector<WCHAR> &text, RECT &bounds) {
|
|
// Common-control messages contain pointers and require memory in the
|
|
// target process, including a target-width LVITEM for 32-bit clients.
|
|
constexpr size_t offset=128, capacity=4098, bytes=offset+capacity*sizeof(WCHAR);
|
|
native_control::RemoteBuffer remote(window,bytes);
|
|
if (!remote) return false;
|
|
std::vector<BYTE> buffer(bytes,0);
|
|
if (remote.is32Bit()) {
|
|
ReviewItem32 item{LVIF_TEXT,static_cast<int32_t>(index),column,0,0,
|
|
static_cast<uint32_t>(remote.address(offset)),capacity};
|
|
std::memcpy(buffer.data(),&item,sizeof(item));
|
|
} else {
|
|
ReviewItem64 item{LVIF_TEXT,static_cast<int32_t>(index),column,0,0,0,
|
|
remote.address(offset),capacity,0};
|
|
std::memcpy(buffer.data(),&item,sizeof(item));
|
|
}
|
|
DWORD_PTR copied=0, answer=0;
|
|
if (!remote.write(buffer.data(),bytes) ||
|
|
!remote_query(window,LVM_GETITEMTEXTW,index,remote.address(),deadline,copied)) return false;
|
|
if (copied >= capacity-1 || copied > max_source-source_units) { fail("limited"); return false; }
|
|
source_units+=copied;
|
|
text.assign(copied+1,0);
|
|
if (!remote.read(text.data(),text.size()*sizeof(WCHAR),offset) || text.back() != 0) return false;
|
|
text.resize(copied);
|
|
bool report=(GetWindowLongPtrW(window,GWL_STYLE)&LVS_TYPEMASK) == LVS_REPORT;
|
|
bounds={LVIR_LABEL,column,0,0};
|
|
if (!remote.write(&bounds,sizeof(bounds)) ||
|
|
!remote_query(window,report ? LVM_GETSUBITEMRECT : LVM_GETITEMRECT,index,remote.address(),deadline,answer) || !answer ||
|
|
!remote.read(&bounds,sizeof(bounds))) return false;
|
|
// Column zero's rectangle includes all report columns on some Wine
|
|
// versions. Restrict it to the actual header cell, never a guessed width.
|
|
DWORD_PTR header=0;
|
|
if (report &&
|
|
query(window,LVM_GETHEADER,0,0,deadline,header) && header) {
|
|
RECT cell{};
|
|
if (!remote.write(&cell,sizeof(cell)) ||
|
|
!remote_query(reinterpret_cast<HWND>(header),HDM_GETITEMRECT,column,remote.address(),deadline,answer) || !answer ||
|
|
!remote.read(&cell,sizeof(cell))) return false;
|
|
POINT origin{0,0}; ClientToScreen(reinterpret_cast<HWND>(header),&origin);
|
|
POINT table_origin{0,0}; ClientToScreen(window,&table_origin);
|
|
bounds.left=std::max(bounds.left,cell.left+origin.x-table_origin.x);
|
|
bounds.right=std::min(bounds.right,cell.right+origin.x-table_origin.x);
|
|
}
|
|
POINT origin{0,0}; ClientToScreen(window,&origin); OffsetRect(&bounds,origin.x,origin.y);
|
|
return true;
|
|
}
|
|
void table(HWND window, const std::string &parent, const RECT &clip) {
|
|
DWORD_PTR count=0, first=0, header=0, columns=1;
|
|
if (!query(window,LVM_GETITEMCOUNT,0,0,deadline,count) ||
|
|
!query(window,LVM_GETTOPINDEX,0,0,deadline,first)) { fail("unavailable"); return; }
|
|
if ((GetWindowLongPtrW(window,GWL_STYLE)&LVS_TYPEMASK) == LVS_REPORT &&
|
|
query(window,LVM_GETHEADER,0,0,deadline,header) && header &&
|
|
!query(reinterpret_cast<HWND>(header),HDM_GETITEMCOUNT,0,0,deadline,columns)) { fail("unavailable"); return; }
|
|
if (columns > 256 || count > INT32_MAX) { fail("limited"); return; }
|
|
for (size_t row=first; row<count && !failed; ++row) {
|
|
bool below=false;
|
|
for (DWORD_PTR column=0; column<columns && !failed; ++column) {
|
|
std::vector<WCHAR> text; RECT bounds{};
|
|
if (!table_item(window,row,column,text,bounds)) { if (!failed) fail("stale"); break; }
|
|
if (bounds.top >= clip.bottom) { below=true; break; }
|
|
if (!inside(bounds,clip)) continue;
|
|
auto value=utf8(text.data(),text.size());
|
|
if (!text.empty() && value.empty()) { fail("unavailable"); break; }
|
|
ReviewNode node;
|
|
node.id=parent+"_r"+std::to_string(row)+"_c"+std::to_string(column);
|
|
node.parent=parent; node.role="table cell"; node.bounds=bounds; node.name=value;
|
|
node.text_known=true;
|
|
uint64_t runes=0; for (unsigned char c:value) if ((c&0xc0)!=0x80) ++runes;
|
|
if (!value.empty()) node.ranges.push_back({0,runes,0,text.size(),value,bounds});
|
|
if (!add(std::move(node))) break;
|
|
list_reads.push_back({window,row,std::move(text),bounds,static_cast<int>(column)});
|
|
}
|
|
if (below) break;
|
|
}
|
|
}
|
|
void accessible(IAccessible *object, VARIANT child, const std::string &id,
|
|
const std::string &parent, const RECT &clip, unsigned depth) {
|
|
if (failed) return;
|
|
if (GetTickCount64() >= deadline) { fail("unavailable"); return; }
|
|
if (++visited > max_nodes || depth > 64) { fail("limited"); return; }
|
|
VARIANT state{};
|
|
if (FAILED(object->get_accState(child, &state))) return;
|
|
if (state.vt != VT_I4) { VariantClear(&state); return; }
|
|
const LONG flags = state.lVal; VariantClear(&state);
|
|
if (flags & (STATE_SYSTEM_INVISIBLE | STATE_SYSTEM_OFFSCREEN)) return;
|
|
ReviewNode node;
|
|
node.id = id; node.parent = parent;
|
|
node.protected_text = (flags & STATE_SYSTEM_PROTECTED) != 0;
|
|
LONG x=0, y=0, width=0, height=0;
|
|
if (FAILED(object->accLocation(&x, &y, &width, &height, child)) || width <= 0 || height <= 0 ||
|
|
static_cast<int64_t>(x)+width > INT32_MAX || static_cast<int64_t>(y)+height > INT32_MAX) return;
|
|
RECT bounds{x,y,x+width,y+height};
|
|
if (!IntersectRect(&node.bounds, &bounds, &clip)) return;
|
|
VARIANT role{};
|
|
object->get_accRole(child, &role);
|
|
LONG role_id = role.vt == VT_I4 ? role.lVal : 0;
|
|
WCHAR role_name[128]{};
|
|
if (role_id) GetRoleTextW(role_id, role_name, 128);
|
|
node.role = node.protected_text ? "password text" : utf8(role_name, lstrlenW(role_name));
|
|
if (node.role.empty()) node.role = "panel";
|
|
VariantClear(&role);
|
|
// Do not ask a protected provider for either its name or its value.
|
|
if (!node.protected_text) {
|
|
BSTR name = nullptr, value = nullptr;
|
|
std::vector<WCHAR> raw_name, raw_value;
|
|
object->get_accName(child, &name);
|
|
if (name) {
|
|
if (SysStringLen(name) > 4096) fail("limited");
|
|
else {
|
|
raw_name.assign(name,name+SysStringLen(name));
|
|
node.name = accessible_text(name,SysStringLen(name));
|
|
}
|
|
SysFreeString(name);
|
|
}
|
|
std::string text = node.name;
|
|
if (role_id == ROLE_SYSTEM_TEXT || role_id == ROLE_SYSTEM_COMBOBOX) {
|
|
object->get_accValue(child, &value);
|
|
if (value) {
|
|
if (SysStringLen(value) > 4096) fail("limited");
|
|
else {
|
|
raw_value.assign(value,value+SysStringLen(value));
|
|
text = accessible_text(value,SysStringLen(value));
|
|
}
|
|
SysFreeString(value);
|
|
}
|
|
}
|
|
if (!failed) {
|
|
object->AddRef();
|
|
accessible_reads.push_back({object, child.lVal, raw_name, raw_value,
|
|
role_id == ROLE_SYSTEM_TEXT || role_id == ROLE_SYSTEM_COMBOBOX, bounds});
|
|
}
|
|
if (role_id == ROLE_SYSTEM_PUSHBUTTON || role_id == ROLE_SYSTEM_STATICTEXT) {
|
|
node.name = mnemonic(node.name); text = mnemonic(text);
|
|
}
|
|
// Without per-line MSAA geometry, only a single exposed caption/value
|
|
// can be represented faithfully. Never flatten a multi-line provider.
|
|
bool caption_only=role_id == ROLE_SYSTEM_PUSHBUTTON || role_id == ROLE_SYSTEM_STATICTEXT;
|
|
if (caption_only && !inside(bounds,clip)) node.name.clear();
|
|
if (!caption_only && !failed && !text.empty() && text.find_first_of("\r\n") == std::string::npos && inside(bounds, clip)) {
|
|
uint64_t runes=0, units=0;
|
|
for (unsigned char c : text) {
|
|
if ((c & 0xc0) != 0x80) { ++runes; units += c >= 0xf0 ? 2 : 1; }
|
|
}
|
|
node.text_known = true;
|
|
node.ranges.push_back({0,runes,0,units,text,bounds});
|
|
}
|
|
}
|
|
const RECT child_clip=node.bounds;
|
|
if (failed || !add(std::move(node))) return;
|
|
if (child.vt != VT_I4 || child.lVal != CHILDID_SELF) return;
|
|
LONG count = 0;
|
|
if (FAILED(object->get_accChildCount(&count)) || count <= 0) return;
|
|
if (count > static_cast<LONG>(max_nodes)) { fail("limited"); return; }
|
|
std::vector<VARIANT> children(count);
|
|
LONG obtained = 0;
|
|
if (FAILED(AccessibleChildren(object, 0, count, children.data(), &obtained))) return;
|
|
for (LONG i=0; i<obtained; ++i) {
|
|
auto &item = children[i];
|
|
const auto child_id = id + "_" + std::to_string(i+1);
|
|
if (item.vt == VT_DISPATCH && item.pdispVal) {
|
|
IAccessible *nested=nullptr;
|
|
if (SUCCEEDED(item.pdispVal->QueryInterface(accessible_iid, reinterpret_cast<void**>(&nested)))) {
|
|
VARIANT self{}; self.vt=VT_I4; self.lVal=CHILDID_SELF;
|
|
accessible(nested, self, child_id, id, child_clip, depth+1);
|
|
nested->Release();
|
|
}
|
|
} else if (item.vt == VT_I4) accessible(object, item, child_id, id, child_clip, depth+1);
|
|
VariantClear(&item);
|
|
}
|
|
}
|
|
void window(HWND window, const std::string &parent, RECT clip, unsigned depth) {
|
|
if (failed || !IsWindowVisible(window)) return;
|
|
if (depth > 64) { fail("limited"); return; }
|
|
RECT bounds{}, clipped{};
|
|
if (!GetWindowRect(window, &bounds) || !IntersectRect(&clipped, &bounds, &clip)) return;
|
|
native.insert(window);
|
|
ReviewNode node;
|
|
node.id = store.identity(window); node.parent = parent; node.bounds = clipped;
|
|
WCHAR cls[128]{}; GetClassNameW(window, cls, 128);
|
|
node.protected_text = password(window);
|
|
bool is_edit = wine_access_class_matches(cls, L"Edit");
|
|
bool button = wine_access_class_matches(cls, L"Button");
|
|
bool label = wine_access_class_matches(cls, L"Static");
|
|
bool list_box = wine_access_class_matches(cls, L"ListBox");
|
|
bool list_view = wine_access_class_matches(cls, L"SysListView32");
|
|
node.role = is_edit ? (node.protected_text ? "password text" : "text") :
|
|
button ? "push button" : label ? "label" : "panel";
|
|
if (is_edit) {
|
|
if (!edit(window, node, clipped)) return;
|
|
} else if (button || label) {
|
|
std::vector<WCHAR> text; bool limited = false;
|
|
if (!read_text(window, deadline, 4096, text, limited)) { fail(limited ? "limited" : "unavailable"); return; }
|
|
auto value=utf8(text.data(),text.size());
|
|
node.name = label && (GetWindowLongPtrW(window,GWL_STYLE)&SS_NOPREFIX) ? value : mnemonic(value);
|
|
// Captions have no text-position API. Existing flat review represents
|
|
// their decoded accessible name using the control's native bounds;
|
|
// keep offsets unknown instead of inventing Edit-style UTF-16 ranges.
|
|
if (!inside(bounds,clip)) node.name.clear();
|
|
reads.push_back({window, std::move(text), bounds});
|
|
}
|
|
const auto id = node.id;
|
|
if (!add(std::move(node))) return;
|
|
RECT item_clip{}; GetClientRect(window,&item_clip);
|
|
POINT item_origin{0,0}; ClientToScreen(window,&item_origin); OffsetRect(&item_clip,item_origin.x,item_origin.y);
|
|
if (IntersectRect(&item_clip,&item_clip,&clipped)) {
|
|
if (list_box) list(window,id,item_clip);
|
|
if (list_view) table(window,id,item_clip);
|
|
}
|
|
if (!is_edit && !button && !label && !list_box && !list_view) {
|
|
IAccessible *object = nullptr;
|
|
DWORD_PTR response=0;
|
|
// A real provider supplies its marshalled object via WM_GETOBJECT.
|
|
// Synthesized container trees only repeat the native child hierarchy
|
|
// already visited here. Keep the standard fallback for native leaves.
|
|
HRESULT hr=E_FAIL;
|
|
// Match AccessibleObjectFromWindow's actual message completion: a late
|
|
// custom reply still owns a marshalled object that must be consumed.
|
|
bool timely=remote_query(window,WM_GETOBJECT,0xffffffff,OBJID_CLIENT,deadline,response);
|
|
if (response) hr = ObjectFromLresult(static_cast<LRESULT>(response), accessible_iid, 0, reinterpret_cast<void **>(&object));
|
|
else if (timely && !GetWindow(window, GW_CHILD))
|
|
hr = CreateStdAccessibleObject(window, OBJID_CLIENT, accessible_iid, reinterpret_cast<void **>(&object));
|
|
if (!timely) {
|
|
if (object) object->Release();
|
|
fail("unavailable"); return;
|
|
}
|
|
if (!response && GetWindow(window, GW_CHILD) &&
|
|
(GetWindowLongPtrW(window, GWL_STYLE) & WS_CAPTION) == WS_CAPTION && inside(bounds, clip)) {
|
|
// Preserve painted MDI titles only for the native fallback. A real
|
|
// provider retains authority over its own names and protection state.
|
|
ReviewNode title;
|
|
title.id = id + "_caption"; title.parent = id; title.role = "window";
|
|
title.name = caption(window); title.bounds = clipped;
|
|
caption_reads.push_back({window, title.name, bounds});
|
|
if (!add(std::move(title))) return;
|
|
}
|
|
if (SUCCEEDED(hr) && object) {
|
|
VARIANT self{}; self.vt = VT_I4; self.lVal = CHILDID_SELF;
|
|
accessible(object, self, id + "_a", id, clipped, depth + 1);
|
|
object->Release();
|
|
}
|
|
}
|
|
RECT client{}; GetClientRect(window, &client);
|
|
POINT point{0,0}; ClientToScreen(window, &point); OffsetRect(&client, point.x, point.y);
|
|
if (!IntersectRect(&client, &client, &clipped)) return;
|
|
if (wine_access_class_matches(cls, L"MDIClient")) {
|
|
DWORD_PTR answer = 0;
|
|
if (!query(window, WM_MDIGETACTIVE, 0, 0, deadline, answer)) { fail("unavailable"); return; }
|
|
HWND active = reinterpret_cast<HWND>(answer);
|
|
RECT active_bounds{};
|
|
if (active && (GetParent(active) != window || !GetWindowRect(active, &active_bounds))) { fail("stale"); return; }
|
|
mdi_reads.push_back({window, active, active_bounds});
|
|
// A maximized foreground document covers its MDI siblings even when
|
|
// their native WS_VISIBLE flags remain set. Do not read those copies.
|
|
if (active && IsWindowVisible(active) && inside(client, active_bounds)) {
|
|
this->window(active, id, client, depth+1); return;
|
|
}
|
|
}
|
|
for (HWND child = GetWindow(window, GW_CHILD); child && !failed; child = GetWindow(child, GW_HWNDNEXT))
|
|
this->window(child, id, client, depth + 1);
|
|
}
|
|
};
|
|
} // namespace
|
|
|
|
ReviewIdentityStore::ReviewIdentityStore() : state_(std::make_shared<State>()) {
|
|
GUID guid{}; CoCreateGuid(&guid);
|
|
char value[80];
|
|
std::snprintf(value, sizeof(value), "%08lx-%04x-%04x-%02x%02x%02x%02x%02x%02x%02x%02x",
|
|
static_cast<unsigned long>(guid.Data1), guid.Data2, guid.Data3,
|
|
guid.Data4[0], guid.Data4[1], guid.Data4[2], guid.Data4[3], guid.Data4[4], guid.Data4[5], guid.Data4[6], guid.Data4[7]);
|
|
state_->instance = value;
|
|
}
|
|
std::string ReviewIdentityStore::identity(HWND window) {
|
|
std::lock_guard<std::mutex> lock(state_->mutex);
|
|
auto &value = state_->identities[window];
|
|
if (value.empty()) {
|
|
char token[40];
|
|
std::snprintf(token,sizeof(token),"w%016llx_%016llx",
|
|
static_cast<unsigned long long>(reinterpret_cast<uintptr_t>(window)),
|
|
static_cast<unsigned long long>(++state_->next));
|
|
value=token;
|
|
}
|
|
return value;
|
|
}
|
|
ReviewFocus ReviewIdentityStore::observe_foreground() {
|
|
ReviewFocus notice;
|
|
HWND window = GetForegroundWindow();
|
|
if (!window) return notice;
|
|
DWORD thread = GetWindowThreadProcessId(window, nullptr);
|
|
GUITHREADINFO gui{}; gui.cbSize = sizeof(gui);
|
|
HWND focus = GetGUIThreadInfo(thread, &gui) && gui.hwndFocus ? gui.hwndFocus : window;
|
|
if (GetAncestor(focus, GA_ROOT) != window) return notice;
|
|
notice.instance = state_->instance; notice.window = identity(window); notice.focus = identity(focus);
|
|
WCHAR cls[128]{}; GetClassNameW(window,cls,128);
|
|
notice.title = caption(window); notice.role = lstrcmpW(cls,L"#32770")==0 ? "dialog" : "window";
|
|
notice.protected_text = password(focus);
|
|
// X11 exposes Wine's application client, not its simulated Win32 border.
|
|
// Use that same native client rectangle as the root of visible review.
|
|
GetClientRect(window, ¬ice.bounds);
|
|
POINT origin{0,0}; ClientToScreen(window,&origin);
|
|
OffsetRect(¬ice.bounds,origin.x,origin.y);
|
|
// Wine's desktop flags distinguish its real X11 root from a virtual
|
|
// desktop, whose children include the simulated Win32 window border.
|
|
USEROBJECTFLAGS desktop_flags{};
|
|
if (GetUserObjectInformationW(GetThreadDesktop(GetCurrentThreadId()),UOI_FLAGS,
|
|
&desktop_flags,sizeof(desktop_flags),nullptr) &&
|
|
(desktop_flags.dwFlags & wine_virtual_desktop))
|
|
GetWindowRect(window,¬ice.bounds);
|
|
RECT actual{}; POINT native_origin{};
|
|
if (review_x11_geometry(window,actual,native_origin)) notice.bounds=actual;
|
|
{
|
|
std::lock_guard<std::mutex> lock(state_->mutex);
|
|
if (state_->window != notice.window || state_->focus != notice.focus) {
|
|
++state_->revision; state_->window = notice.window; state_->focus = notice.focus;
|
|
}
|
|
notice.revision = state_->revision;
|
|
}
|
|
return notice;
|
|
}
|
|
void ReviewIdentityStore::destroyed(HWND window, LONG object, LONG child) {
|
|
if (object == OBJID_CARET || object == OBJID_CURSOR) return;
|
|
if (object == OBJID_WINDOW && child != CHILDID_SELF) return;
|
|
std::lock_guard<std::mutex> lock(state_->mutex);
|
|
auto found = state_->identities.find(window);
|
|
if (found == state_->identities.end()) return;
|
|
if (found->second == state_->focus || found->second == state_->window) ++state_->revision;
|
|
state_->identities.erase(found);
|
|
}
|
|
void ReviewIdentityStore::reordered(HWND window, LONG object) { destroyed(window, object, 0); }
|
|
void ReviewIdentityStore::retire() {
|
|
std::lock_guard<std::mutex> lock(state_->mutex);
|
|
state_->identities.clear(); state_->window.clear(); state_->focus.clear(); ++state_->revision;
|
|
}
|
|
ReviewCapture capture_review_window(ReviewIdentityStore &store, const ReviewFocus &expected, uint64_t deadline) {
|
|
Capture capture{store, {}, deadline, 0, 0, 512, {}, {}, {}, {}, {}, {}, 0, false};
|
|
capture.result.source = expected;
|
|
auto initial = store.observe_foreground();
|
|
if (expected.window.empty() || !equal_focus(expected, initial)) {
|
|
capture.result.status = "stale"; return capture.result;
|
|
}
|
|
HWND window = GetForegroundWindow();
|
|
RECT actual{}; POINT native_origin{};
|
|
const bool physical_geometry=review_x11_geometry(window,actual,native_origin);
|
|
if (physical_geometry && !EqualRect(&actual,&expected.bounds)) {
|
|
capture.result.status="stale"; return capture.result;
|
|
}
|
|
ReviewNode root;
|
|
root.id = expected.window; root.role = expected.role; root.name = expected.title; root.bounds = expected.bounds;
|
|
capture.add(std::move(root));
|
|
RECT client{}; GetClientRect(window, &client);
|
|
POINT point{0,0}; ClientToScreen(window, &point); OffsetRect(&client, point.x, point.y);
|
|
RECT physical_clip=expected.bounds;
|
|
OffsetRect(&physical_clip,-native_origin.x,-native_origin.y);
|
|
IntersectRect(&client,&client,&physical_clip);
|
|
for (HWND child = GetWindow(window, GW_CHILD); child && !capture.failed; child = GetWindow(child, GW_HWNDNEXT))
|
|
capture.window(child, expected.window, client, 1);
|
|
// Reread the entire bounded Edit/caption source: checking only its length
|
|
// would miss an equal-length replacement and permit a mixed snapshot.
|
|
for (const auto &read : capture.reads) {
|
|
if (capture.failed) break;
|
|
std::vector<WCHAR> current;
|
|
RECT bounds{};
|
|
if (password(read.window) || !IsWindowVisible(read.window) || !GetWindowRect(read.window, &bounds) ||
|
|
!EqualRect(&read.bounds, &bounds)) { capture.fail("stale"); break; }
|
|
if (read.text.size() > max_source - capture.source_units) { capture.fail("limited"); break; }
|
|
capture.source_units += read.text.size();
|
|
// The first read already supplies a bounded capacity. Copy once, then
|
|
// verify length, content and protection without another preliminary
|
|
// length round trip (growth must still reject a truncated old prefix).
|
|
current.assign(read.text.size()+1,0);
|
|
DWORD_PTR copied=0,length=0;
|
|
if(!query(read.window,WM_GETTEXT,current.size(),reinterpret_cast<LPARAM>(current.data()),deadline,copied) ||
|
|
password(read.window) || !query(read.window,WM_GETTEXTLENGTH,0,0,deadline,length) ||
|
|
password(read.window) || copied!=read.text.size() || length!=read.text.size())capture.fail("stale");
|
|
current.resize(read.text.size());
|
|
if(current!=read.text)capture.fail("stale");
|
|
if (read.layout && !capture.failed) {
|
|
EditLayout layout{};
|
|
if (!edit_layout(read.window,deadline,layout) || !same_layout(*read.layout,layout)) capture.fail("stale");
|
|
std::vector<ReviewQuery> positions;
|
|
for(const auto &[index,position]:read.positions)positions.push_back({EM_POSFROMCHAR,index});
|
|
if(!capture.failed && !review_query_many(read.window,positions,deadline))capture.fail("stale");
|
|
for(size_t i=0;!capture.failed && i<positions.size();++i)
|
|
if(positions[i].answer!=read.positions[i].second)capture.fail("stale");
|
|
}
|
|
}
|
|
for (const auto &read : capture.list_reads) {
|
|
if (capture.failed) break;
|
|
std::vector<WCHAR> text; RECT bounds{};
|
|
bool read_ok=read.column < 0 ? capture.list_item(read.window,read.index,text,bounds) :
|
|
capture.table_item(read.window,read.index,read.column,text,bounds);
|
|
if (!IsWindowVisible(read.window) || !read_ok ||
|
|
text != read.text || !EqualRect(&bounds,&read.bounds)) capture.fail("stale");
|
|
}
|
|
for (const auto &read : capture.accessible_reads) {
|
|
if (capture.failed) break;
|
|
if (GetTickCount64() >= deadline) { capture.fail("unavailable"); break; }
|
|
VARIANT child{}, state{}; child.vt = VT_I4; child.lVal = read.child;
|
|
if (FAILED(read.object->get_accState(child, &state)) || state.vt != VT_I4 ||
|
|
(state.lVal & (STATE_SYSTEM_PROTECTED | STATE_SYSTEM_INVISIBLE | STATE_SYSTEM_OFFSCREEN))) {
|
|
VariantClear(&state); capture.fail("stale"); break;
|
|
}
|
|
VariantClear(&state);
|
|
auto matches = [&](bool value, const std::vector<WCHAR> &expected_text) {
|
|
BSTR text = nullptr;
|
|
HRESULT hr = value ? read.object->get_accValue(child, &text) : read.object->get_accName(child, &text);
|
|
bool match = (SUCCEEDED(hr) || hr == S_FALSE) && (!text || SysStringLen(text) <= 4096) &&
|
|
(text ? std::equal(expected_text.begin(),expected_text.end(),text,text+SysStringLen(text)) : expected_text.empty());
|
|
if (text) SysFreeString(text);
|
|
return match;
|
|
};
|
|
if (!matches(false, read.name) || (read.read_value && !matches(true, read.value))) capture.fail("stale");
|
|
LONG x=0,y=0,width=0,height=0;
|
|
if (FAILED(read.object->accLocation(&x,&y,&width,&height,child)) ||
|
|
x != read.bounds.left || y != read.bounds.top ||
|
|
static_cast<int64_t>(x)+width != read.bounds.right || static_cast<int64_t>(y)+height != read.bounds.bottom) capture.fail("stale");
|
|
}
|
|
for(const auto &read:capture.mdi_reads) {
|
|
if(capture.failed)break;
|
|
DWORD_PTR active=0;RECT bounds{};
|
|
if(!query(read.window,WM_MDIGETACTIVE,0,0,deadline,active) || reinterpret_cast<HWND>(active)!=read.active ||
|
|
(read.active && (!GetWindowRect(read.active,&bounds) || !EqualRect(&bounds,&read.bounds))))capture.fail("stale");
|
|
}
|
|
for (const auto &read : capture.caption_reads) {
|
|
if (capture.failed) break;
|
|
RECT bounds{};
|
|
if (!IsWindowVisible(read.window) || !GetWindowRect(read.window, &bounds) ||
|
|
!EqualRect(&bounds, &read.bounds) ||
|
|
(GetWindowLongPtrW(read.window, GWL_STYLE) & WS_CAPTION) != WS_CAPTION ||
|
|
caption(read.window) != read.text) capture.fail("stale");
|
|
}
|
|
if (!capture.failed && !equal_focus(expected, store.observe_foreground())) capture.fail("stale");
|
|
if (!capture.failed && physical_geometry) {
|
|
POINT current_origin{};
|
|
if (!review_x11_geometry(window,actual,current_origin) || !EqualRect(&actual,&expected.bounds) ||
|
|
current_origin.x != native_origin.x || current_origin.y != native_origin.y) capture.fail("stale");
|
|
}
|
|
if (!capture.failed && GetTickCount64() >= deadline) capture.fail("unavailable");
|
|
if (capture.failed) capture.result.nodes.clear();
|
|
else {
|
|
for (size_t i=1;i<capture.result.nodes.size();++i) {
|
|
auto &node=capture.result.nodes[i];
|
|
OffsetRect(&node.bounds,native_origin.x,native_origin.y);
|
|
for (auto &range:node.ranges) OffsetRect(&range.bounds,native_origin.x,native_origin.y);
|
|
}
|
|
capture.result.status = "ok";
|
|
}
|
|
return std::move(capture.result);
|
|
}
|