Fix padded Wine review strings
This commit is contained in:
1 parent
a8cb350467
commit
f40e471938
7 files changed
+416
-33
No files matched your search
@@ -268,6 +268,9 @@ line, 65536 per control, one million native source code units, 1 MiB of text
|
|||||||
and 2 MiB of serialized data. Non-OK replies have no nodes; stale, busy,
|
and 2 MiB of serialized data. Non-OK replies have no nodes; stale, busy,
|
||||||
limited and unavailable captures never reuse cached text. Capture runs on a
|
limited and unavailable captures never reuse cached text. Capture runs on a
|
||||||
separate COM worker so blocked MSAA providers do not block menu/focus events.
|
separate COM worker so blocked MSAA providers do not block menu/focus events.
|
||||||
|
Scalar Edit queries are batched so applications with periodic message loops can
|
||||||
|
return visible text within the capture deadline. Native container children are
|
||||||
|
enumerated once, and a maximized MDI document excludes the covered documents.
|
||||||
See [tests/README.md](tests/README.md) for checks and live acceptance.
|
See [tests/README.md](tests/README.md) for checks and live acceptance.
|
||||||
|
|
||||||
## AI-assisted contributions
|
## AI-assisted contributions
|
||||||
|
|||||||
+167
-31
@@ -3,6 +3,7 @@
|
|||||||
#include "control_class.h"
|
#include "control_class.h"
|
||||||
#include "native_query.h"
|
#include "native_query.h"
|
||||||
#include "review_x11.h"
|
#include "review_x11.h"
|
||||||
|
#include "review_query.h"
|
||||||
#include <commctrl.h>
|
#include <commctrl.h>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <oleacc.h>
|
#include <oleacc.h>
|
||||||
@@ -11,6 +12,7 @@
|
|||||||
#include <limits>
|
#include <limits>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
// DF_WINE_VIRTUAL_DESKTOP from Wine's ntuser.h (that internal C header is not
|
// DF_WINE_VIRTUAL_DESKTOP from Wine's ntuser.h (that internal C header is not
|
||||||
@@ -29,6 +31,15 @@ std::string utf8(const WCHAR *text, size_t length) {
|
|||||||
static_cast<int>(length), value.data(), size, nullptr, nullptr);
|
static_cast<int>(length), value.data(), size, nullptr, nullptr);
|
||||||
return value;
|
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,
|
bool query(HWND window, UINT message, WPARAM wp, LPARAM lp, uint64_t deadline,
|
||||||
DWORD_PTR &result) {
|
DWORD_PTR &result) {
|
||||||
uint64_t now = GetTickCount64();
|
uint64_t now = GetTickCount64();
|
||||||
@@ -104,17 +115,12 @@ bool edit_layout(HWND window, uint64_t deadline, EditLayout &layout) {
|
|||||||
layout.style=GetWindowLongPtrW(window,GWL_STYLE);
|
layout.style=GetWindowLongPtrW(window,GWL_STYLE);
|
||||||
layout.horizontal=GetScrollPos(window,SB_HORZ);
|
layout.horizontal=GetScrollPos(window,SB_HORZ);
|
||||||
layout.vertical=GetScrollPos(window,SB_VERT);
|
layout.vertical=GetScrollPos(window,SB_VERT);
|
||||||
for (auto message:{EM_GETRECT,EM_GETFIRSTVISIBLELINE,EM_GETLINECOUNT}) {
|
if (!query(window, EM_GETRECT, 0, reinterpret_cast<LPARAM>(&layout.format), deadline, answer)) return false;
|
||||||
LPARAM lp=message == EM_GETRECT ? reinterpret_cast<LPARAM>(&layout.format) : 0;
|
std::vector<ReviewQuery> fields{{EM_GETFIRSTVISIBLELINE,0},{EM_GETLINECOUNT,0},{WM_GETFONT,0}};
|
||||||
DWORD_PTR *result=message == EM_GETFIRSTVISIBLELINE ? &layout.first :
|
if (!review_query_many(window, fields, deadline)) return false;
|
||||||
message == EM_GETLINECOUNT ? &layout.lines : &answer;
|
layout.first = fields[0].answer;
|
||||||
if (!query(window,message,0,lp,deadline,*result)) {
|
layout.lines = fields[1].answer;
|
||||||
return false;
|
layout.font = fields[2].answer;
|
||||||
}
|
|
||||||
}
|
|
||||||
// Wine reports failure when marshalling a null WM_GETFONT result. Null is
|
|
||||||
// the native default font; preserve the optional query used by Edit capture.
|
|
||||||
query(window,WM_GETFONT,0,0,deadline,layout.font);
|
|
||||||
return GetTickCount64() < deadline;
|
return GetTickCount64() < deadline;
|
||||||
}
|
}
|
||||||
bool same_layout(const EditLayout &a, const EditLayout &b) {
|
bool same_layout(const EditLayout &a, const EditLayout &b) {
|
||||||
@@ -129,11 +135,13 @@ struct SourceRead {
|
|||||||
struct AccessibleRead {
|
struct AccessibleRead {
|
||||||
IAccessible *object;
|
IAccessible *object;
|
||||||
LONG child;
|
LONG child;
|
||||||
std::string name, value;
|
std::vector<WCHAR> name, value;
|
||||||
bool read_value;
|
bool read_value;
|
||||||
RECT bounds;
|
RECT bounds;
|
||||||
};
|
};
|
||||||
struct ListRead { HWND window; size_t index; std::vector<WCHAR> text; RECT bounds; int column = -1; };
|
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 {
|
struct ReviewItem32 {
|
||||||
uint32_t mask; int32_t item, subitem; uint32_t state, state_mask, text; int32_t capacity;
|
uint32_t mask; int32_t item, subitem; uint32_t state, state_mask, text; int32_t capacity;
|
||||||
};
|
};
|
||||||
@@ -152,6 +160,8 @@ struct Capture {
|
|||||||
std::vector<SourceRead> reads;
|
std::vector<SourceRead> reads;
|
||||||
std::vector<AccessibleRead> accessible_reads;
|
std::vector<AccessibleRead> accessible_reads;
|
||||||
std::vector<ListRead> list_reads;
|
std::vector<ListRead> list_reads;
|
||||||
|
std::vector<MdiRead> mdi_reads;
|
||||||
|
std::vector<CaptionRead> caption_reads;
|
||||||
std::unordered_set<HWND> native;
|
std::unordered_set<HWND> native;
|
||||||
size_t visited = 0;
|
size_t visited = 0;
|
||||||
bool failed = false;
|
bool failed = false;
|
||||||
@@ -189,9 +199,13 @@ struct Capture {
|
|||||||
EditLayout layout{};
|
EditLayout layout{};
|
||||||
if (!edit_layout(window,deadline,layout)) { fail("unavailable"); return false; }
|
if (!edit_layout(window,deadline,layout)) { fail("unavailable"); return false; }
|
||||||
std::vector<std::pair<size_t,DWORD_PTR>> positions;
|
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 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) ||
|
if (!query(window,EM_POSFROMCHAR,index,0,deadline,position) ||
|
||||||
position == static_cast<DWORD_PTR>(-1)) return false;
|
position == static_cast<DWORD_PTR>(-1)) return false;
|
||||||
|
position_cache.emplace(index,position);
|
||||||
positions.emplace_back(index,position);
|
positions.emplace_back(index,position);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
@@ -235,11 +249,63 @@ struct Capture {
|
|||||||
bool good=true;
|
bool good=true;
|
||||||
const bool multiline = GetWindowLongPtrW(window, GWL_STYLE) & ES_MULTILINE;
|
const bool multiline = GetWindowLongPtrW(window, GWL_STYLE) & ES_MULTILINE;
|
||||||
if (!multiline) first = 0; // Single-line message returns character index.
|
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;
|
size_t captured = 0;
|
||||||
for (size_t line = first; good && line < lines && !failed; ++line) {
|
for (size_t line = first; good && line-first < indices.size() && !failed; ++line) {
|
||||||
DWORD_PTR index = 0, length = 0, position = 0;
|
DWORD_PTR index = indices[line-first].answer, length = lengths[line-first], position = 0;
|
||||||
good = query(window, EM_LINEINDEX, line, 0, deadline, index) &&
|
|
||||||
query(window, EM_LINELENGTH, index, 0, deadline, length);
|
|
||||||
if (!good || index > text.size() || length > text.size() - index) { fail("stale"); break; }
|
if (!good || index > text.size() || length > text.size() - index) { fail("stale"); break; }
|
||||||
// EM_POSFROMCHAR has no position for an empty final row. Its visual
|
// EM_POSFROMCHAR has no position for an empty final row. Its visual
|
||||||
// line geometry follows the control's actual font and format rectangle.
|
// line geometry follows the control's actual font and format rectangle.
|
||||||
@@ -247,7 +313,7 @@ struct Capture {
|
|||||||
good=position_at(index,position);
|
good=position_at(index,position);
|
||||||
if (!good) { fail("unavailable"); break; }
|
if (!good) { fail("unavailable"); break; }
|
||||||
} else {
|
} else {
|
||||||
position = MAKELONG(format.left, format.top + (line - first) * metrics.tmHeight);
|
position = MAKELONG(format.left, format.top + (line - native_first) * metrics.tmHeight);
|
||||||
}
|
}
|
||||||
LONG x = static_cast<short>(LOWORD(position)) + origin.x;
|
LONG x = static_cast<short>(LOWORD(position)) + origin.x;
|
||||||
LONG y = static_cast<short>(HIWORD(position)) + origin.y;
|
LONG y = static_cast<short>(HIWORD(position)) + origin.y;
|
||||||
@@ -255,6 +321,9 @@ struct Capture {
|
|||||||
SIZE extent{};
|
SIZE extent{};
|
||||||
if (length) GetTextExtentPoint32W(dc, text.data() + index, static_cast<int>(length), &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};
|
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;
|
size_t finish = index + length;
|
||||||
if (length) {
|
if (length) {
|
||||||
size_t last=finish-1;
|
size_t last=finish-1;
|
||||||
@@ -487,11 +556,14 @@ struct Capture {
|
|||||||
// Do not ask a protected provider for either its name or its value.
|
// Do not ask a protected provider for either its name or its value.
|
||||||
if (!node.protected_text) {
|
if (!node.protected_text) {
|
||||||
BSTR name = nullptr, value = nullptr;
|
BSTR name = nullptr, value = nullptr;
|
||||||
std::string raw_name, raw_value;
|
std::vector<WCHAR> raw_name, raw_value;
|
||||||
object->get_accName(child, &name);
|
object->get_accName(child, &name);
|
||||||
if (name) {
|
if (name) {
|
||||||
if (SysStringLen(name) > 4096) fail("limited");
|
if (SysStringLen(name) > 4096) fail("limited");
|
||||||
else raw_name = node.name = utf8(name, SysStringLen(name));
|
else {
|
||||||
|
raw_name.assign(name,name+SysStringLen(name));
|
||||||
|
node.name = accessible_text(name,SysStringLen(name));
|
||||||
|
}
|
||||||
SysFreeString(name);
|
SysFreeString(name);
|
||||||
}
|
}
|
||||||
std::string text = node.name;
|
std::string text = node.name;
|
||||||
@@ -499,7 +571,10 @@ struct Capture {
|
|||||||
object->get_accValue(child, &value);
|
object->get_accValue(child, &value);
|
||||||
if (value) {
|
if (value) {
|
||||||
if (SysStringLen(value) > 4096) fail("limited");
|
if (SysStringLen(value) > 4096) fail("limited");
|
||||||
else raw_value = text = utf8(value, SysStringLen(value));
|
else {
|
||||||
|
raw_value.assign(value,value+SysStringLen(value));
|
||||||
|
text = accessible_text(value,SysStringLen(value));
|
||||||
|
}
|
||||||
SysFreeString(value);
|
SysFreeString(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -587,7 +662,32 @@ struct Capture {
|
|||||||
}
|
}
|
||||||
if (!is_edit && !button && !label && !list_box && !list_view) {
|
if (!is_edit && !button && !label && !list_box && !list_view) {
|
||||||
IAccessible *object = nullptr;
|
IAccessible *object = nullptr;
|
||||||
if (SUCCEEDED(AccessibleObjectFromWindow(window, OBJID_CLIENT, accessible_iid, reinterpret_cast<void**>(&object)))) {
|
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;
|
VARIANT self{}; self.vt = VT_I4; self.lVal = CHILDID_SELF;
|
||||||
accessible(object, self, id + "_a", id, clipped, depth + 1);
|
accessible(object, self, id + "_a", id, clipped, depth + 1);
|
||||||
object->Release();
|
object->Release();
|
||||||
@@ -596,6 +696,19 @@ struct Capture {
|
|||||||
RECT client{}; GetClientRect(window, &client);
|
RECT client{}; GetClientRect(window, &client);
|
||||||
POINT point{0,0}; ClientToScreen(window, &point); OffsetRect(&client, point.x, point.y);
|
POINT point{0,0}; ClientToScreen(window, &point); OffsetRect(&client, point.x, point.y);
|
||||||
if (!IntersectRect(&client, &client, &clipped)) return;
|
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))
|
for (HWND child = GetWindow(window, GW_CHILD); child && !failed; child = GetWindow(child, GW_HWNDNEXT))
|
||||||
this->window(child, id, client, depth + 1);
|
this->window(child, id, client, depth + 1);
|
||||||
}
|
}
|
||||||
@@ -672,7 +785,7 @@ void ReviewIdentityStore::retire() {
|
|||||||
state_->identities.clear(); state_->window.clear(); state_->focus.clear(); ++state_->revision;
|
state_->identities.clear(); state_->window.clear(); state_->focus.clear(); ++state_->revision;
|
||||||
}
|
}
|
||||||
ReviewCapture capture_review_window(ReviewIdentityStore &store, const ReviewFocus &expected, uint64_t deadline) {
|
ReviewCapture capture_review_window(ReviewIdentityStore &store, const ReviewFocus &expected, uint64_t deadline) {
|
||||||
Capture capture{store, {}, deadline, 0, 0, 512, {}, {}, {}, {}, 0, false};
|
Capture capture{store, {}, deadline, 0, 0, 512, {}, {}, {}, {}, {}, {}, 0, false};
|
||||||
capture.result.source = expected;
|
capture.result.source = expected;
|
||||||
auto initial = store.observe_foreground();
|
auto initial = store.observe_foreground();
|
||||||
if (expected.window.empty() || !equal_focus(expected, initial)) {
|
if (expected.window.empty() || !equal_focus(expected, initial)) {
|
||||||
@@ -698,21 +811,30 @@ ReviewCapture capture_review_window(ReviewIdentityStore &store, const ReviewFocu
|
|||||||
// would miss an equal-length replacement and permit a mixed snapshot.
|
// would miss an equal-length replacement and permit a mixed snapshot.
|
||||||
for (const auto &read : capture.reads) {
|
for (const auto &read : capture.reads) {
|
||||||
if (capture.failed) break;
|
if (capture.failed) break;
|
||||||
std::vector<WCHAR> current; bool limited = false;
|
std::vector<WCHAR> current;
|
||||||
RECT bounds{};
|
RECT bounds{};
|
||||||
if (password(read.window) || !IsWindowVisible(read.window) || !GetWindowRect(read.window, &bounds) ||
|
if (password(read.window) || !IsWindowVisible(read.window) || !GetWindowRect(read.window, &bounds) ||
|
||||||
!EqualRect(&read.bounds, &bounds)) { capture.fail("stale"); break; }
|
!EqualRect(&read.bounds, &bounds)) { capture.fail("stale"); break; }
|
||||||
if (read.text.size() > max_source - capture.source_units) { capture.fail("limited"); break; }
|
if (read.text.size() > max_source - capture.source_units) { capture.fail("limited"); break; }
|
||||||
capture.source_units += read.text.size();
|
capture.source_units += read.text.size();
|
||||||
if (!read_text(read.window, deadline, read.text.size(), current, limited) || current != read.text) capture.fail("stale");
|
// 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) {
|
if (read.layout && !capture.failed) {
|
||||||
EditLayout layout{};
|
EditLayout layout{};
|
||||||
if (!edit_layout(read.window,deadline,layout) || !same_layout(*read.layout,layout)) capture.fail("stale");
|
if (!edit_layout(read.window,deadline,layout) || !same_layout(*read.layout,layout)) capture.fail("stale");
|
||||||
for (const auto &[index,expected_position]:read.positions) {
|
std::vector<ReviewQuery> positions;
|
||||||
if (capture.failed) break;
|
for(const auto &[index,position]:read.positions)positions.push_back({EM_POSFROMCHAR,index});
|
||||||
DWORD_PTR position=0;
|
if(!capture.failed && !review_query_many(read.window,positions,deadline))capture.fail("stale");
|
||||||
if (!query(read.window,EM_POSFROMCHAR,index,0,deadline,position) || position != expected_position) 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) {
|
for (const auto &read : capture.list_reads) {
|
||||||
@@ -732,11 +854,11 @@ ReviewCapture capture_review_window(ReviewIdentityStore &store, const ReviewFocu
|
|||||||
VariantClear(&state); capture.fail("stale"); break;
|
VariantClear(&state); capture.fail("stale"); break;
|
||||||
}
|
}
|
||||||
VariantClear(&state);
|
VariantClear(&state);
|
||||||
auto matches = [&](bool value, const std::string &expected_text) {
|
auto matches = [&](bool value, const std::vector<WCHAR> &expected_text) {
|
||||||
BSTR text = nullptr;
|
BSTR text = nullptr;
|
||||||
HRESULT hr = value ? read.object->get_accValue(child, &text) : read.object->get_accName(child, &text);
|
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) &&
|
bool match = (SUCCEEDED(hr) || hr == S_FALSE) && (!text || SysStringLen(text) <= 4096) &&
|
||||||
(text ? utf8(text, SysStringLen(text)) : std::string{}) == expected_text;
|
(text ? std::equal(expected_text.begin(),expected_text.end(),text,text+SysStringLen(text)) : expected_text.empty());
|
||||||
if (text) SysFreeString(text);
|
if (text) SysFreeString(text);
|
||||||
return match;
|
return match;
|
||||||
};
|
};
|
||||||
@@ -746,6 +868,20 @@ ReviewCapture capture_review_window(ReviewIdentityStore &store, const ReviewFocu
|
|||||||
x != read.bounds.left || y != read.bounds.top ||
|
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");
|
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 && !equal_focus(expected, store.observe_foreground())) capture.fail("stale");
|
||||||
if (!capture.failed && physical_geometry) {
|
if (!capture.failed && physical_geometry) {
|
||||||
POINT current_origin{};
|
POINT current_origin{};
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
#pragma once
|
||||||
|
#include <windows.h>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
struct ReviewQuery {
|
||||||
|
UINT message;
|
||||||
|
WPARAM index;
|
||||||
|
DWORD_PTR answer = 0;
|
||||||
|
bool complete = false;
|
||||||
|
};
|
||||||
|
inline void CALLBACK review_query_completed(HWND, UINT, ULONG_PTR data, LRESULT result) {
|
||||||
|
auto &reply = *reinterpret_cast<ReviewQuery *>(data);
|
||||||
|
reply.answer = result;
|
||||||
|
reply.complete = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only scalar messages may use this path. The capture STA owns one batch;
|
||||||
|
// late callbacks retain their storage until all replies arrive or it exits.
|
||||||
|
inline bool review_query_many(HWND window, std::vector<ReviewQuery> &requests, uint64_t deadline) {
|
||||||
|
static thread_local std::unique_ptr<std::vector<ReviewQuery>> pending;
|
||||||
|
auto pump = [deadline] {
|
||||||
|
MSG message{};
|
||||||
|
while (GetTickCount64() < deadline && PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
|
||||||
|
TranslateMessage(&message);
|
||||||
|
DispatchMessageW(&message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (requests.empty()) return GetTickCount64() < deadline;
|
||||||
|
auto complete = [](const auto &items) {
|
||||||
|
return std::all_of(items.begin(), items.end(), [](const auto &item) { return item.complete; });
|
||||||
|
};
|
||||||
|
pump();
|
||||||
|
if (GetTickCount64() >= deadline) return false;
|
||||||
|
if (pending && !complete(*pending)) return false;
|
||||||
|
pending = std::make_unique<std::vector<ReviewQuery>>(requests);
|
||||||
|
for (auto &item : *pending) {
|
||||||
|
if (!SendMessageCallbackW(window, item.message, item.index, 0, review_query_completed,
|
||||||
|
reinterpret_cast<ULONG_PTR>(&item))) {
|
||||||
|
// Keep any requests already sent alive, including during failure.
|
||||||
|
item.complete = true;
|
||||||
|
item.answer = static_cast<DWORD_PTR>(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (!complete(*pending)) {
|
||||||
|
auto now = GetTickCount64();
|
||||||
|
if (now >= deadline) return false;
|
||||||
|
MsgWaitForMultipleObjectsEx(0, nullptr, static_cast<DWORD>(deadline-now), QS_ALLINPUT, MWMO_INPUTAVAILABLE);
|
||||||
|
pump();
|
||||||
|
}
|
||||||
|
if (GetTickCount64() >= deadline) return false;
|
||||||
|
requests = *pending;
|
||||||
|
pending.reset();
|
||||||
|
return std::none_of(requests.begin(), requests.end(), [](const auto &item) {
|
||||||
|
return item.answer == static_cast<DWORD_PTR>(-1);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -8,6 +8,15 @@ text, Unicode offsets before the viewport, wrapping/blank lines, an empty
|
|||||||
focus anchor, hidden/password exclusion, password transitions before copying,
|
focus anchor, hidden/password exclusion, password transitions before copying,
|
||||||
clipping, same-length Edit/ListBox/ListView races, native ListView cells,
|
clipping, same-length Edit/ListBox/ListView races, native ListView cells,
|
||||||
negative coordinates, source limits and unchanged focus/selection.
|
negative coordinates, source limits and unchanged focus/selection.
|
||||||
|
It also covers output serviced by a 16 ms message loop, late scalar replies,
|
||||||
|
nested native containers, deeply clipped text, and maximized versus side-by-side
|
||||||
|
MDI documents. Covered documents are excluded while visible titles remain
|
||||||
|
available. These checks reproduce capture failures that can make review report
|
||||||
|
that it cannot find the current location despite valid Wine focus.
|
||||||
|
The padded MSAA status-name case checks that text after its terminating NUL
|
||||||
|
does not reach D-Bus and that range lengths match the text actually sent.
|
||||||
|
A local provider also checks that malformed UTF-16 padding cannot hide the
|
||||||
|
valid label.
|
||||||
|
|
||||||
Run `python3 tests/review_integration.py --build-dir build` and its `--32-bit`
|
Run `python3 tests/review_integration.py --build-dir build` and its `--32-bit`
|
||||||
variant sequentially. These exercise the exact wire types, actual-owner
|
variant sequentially. These exercise the exact wire types, actual-owner
|
||||||
|
|||||||
@@ -9,6 +9,17 @@
|
|||||||
#include "../review_capture.h"
|
#include "../review_capture.h"
|
||||||
#define HAS_CAPTURE 1
|
#define HAS_CAPTURE 1
|
||||||
#endif
|
#endif
|
||||||
|
static LRESULT CALLBACK padded_name_proc(HWND window,UINT message,WPARAM wp,LPARAM lp) {
|
||||||
|
const WCHAR text[]={L'R',L'e',L'a',L'd',L'y',0,0xd800,0};
|
||||||
|
if(message==WM_GETTEXTLENGTH)return 7;
|
||||||
|
if(message==WM_GETTEXT && wp) {
|
||||||
|
const size_t copied=std::min<size_t>(7,wp-1);
|
||||||
|
std::copy_n(text,copied,reinterpret_cast<WCHAR*>(lp));
|
||||||
|
reinterpret_cast<WCHAR*>(lp)[copied]=0;
|
||||||
|
return copied;
|
||||||
|
}
|
||||||
|
return DefWindowProcW(window,message,wp,lp);
|
||||||
|
}
|
||||||
|
|
||||||
static bool check(bool value, const char *message) {
|
static bool check(bool value, const char *message) {
|
||||||
if (!value) std::fprintf(stderr, "FAIL: %s\n", message);
|
if (!value) std::fprintf(stderr, "FAIL: %s\n", message);
|
||||||
@@ -67,15 +78,18 @@ int main(int argc, char **argv) {
|
|||||||
ok &= check(text.find("private-review-secret") == std::string::npos && text.find("hidden-review-secret") == std::string::npos,
|
ok &= check(text.find("private-review-secret") == std::string::npos && text.find("hidden-review-secret") == std::string::npos,
|
||||||
"password and hidden contents excluded");
|
"password and hidden contents excluded");
|
||||||
ok &= check(SendMessageW(window, WM_APP + 11, 0, 0), "review preserves focus and selection");
|
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}) {
|
for (int mode : {1, 2, 3, 4, 5, 6, 7, 8, 9, 14, 15, 17, 18, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33}) {
|
||||||
std::fprintf(stderr, "capture test: mode %d\n", mode);
|
std::fprintf(stderr, "capture test: mode %d\n", mode);
|
||||||
SendMessageW(window, WM_APP + 10, mode, 0);
|
SendMessageW(window, WM_APP + 10, mode, 0);
|
||||||
|
auto started=GetTickCount64();
|
||||||
auto result = capture();
|
auto result = capture();
|
||||||
|
std::fprintf(stderr,"capture test: status=%s elapsed=%llu ms\n",result.status.c_str(),static_cast<unsigned long long>(GetTickCount64()-started));
|
||||||
const auto value = joined(result);
|
const auto value = joined(result);
|
||||||
if (mode == 7) ok &= check(result.status == "limited" && result.nodes.empty(), "source limit rejects partial capture");
|
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 == 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 == 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 if (mode == 21) ok &= check(result.status != "ok" && result.nodes.empty(), "delayed native call respects capture deadline");
|
||||||
|
else if (mode == 27) ok &= check(result.status != "ok" && result.nodes.empty() && GetTickCount64()-started<600,"delayed scalar batch respects capture deadline");
|
||||||
else ok &= check(result.status == "ok", "configured capture succeeds");
|
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,
|
ok &= check(value.find("private-review-secret") == std::string::npos && value.find("hidden-review-secret") == std::string::npos,
|
||||||
"all captures exclude protected/hidden text");
|
"all captures exclude protected/hidden text");
|
||||||
@@ -102,6 +116,37 @@ int main(int argc, char **argv) {
|
|||||||
if (mode == 14) ok &= check(SendMessageW(window,WM_APP + 14,0,0), "password transition is excluded before copying text");
|
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 == 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 == 25) ok &= check(value.find("literal & label") != std::string::npos, "SS_NOPREFIX captions retain literal ampersands");
|
||||||
|
if (mode == 26) {
|
||||||
|
size_t visible_lines=0;
|
||||||
|
for(const auto &node:result.nodes)for(const auto &range:node.ranges)
|
||||||
|
if(range.text=="stable buffered output line")++visible_lines;
|
||||||
|
ok &= check(visible_lines>=15,"frame-pumped multiline output captures within the deadline");
|
||||||
|
}
|
||||||
|
if(mode==27){
|
||||||
|
Sleep(750);SendMessageW(window,WM_APP+10,0,0);
|
||||||
|
auto recovered=capture();
|
||||||
|
ok &= check(recovered.status=="ok" && joined(recovered).find("alpha beta")!=std::string::npos,"late scalar callbacks preserve storage and allow a fresh capture");
|
||||||
|
}
|
||||||
|
if(mode==28)ok &= check(value.find("top clipped visible line")!=std::string::npos,"top-clipped Edit captures its lower visible rows");
|
||||||
|
if(mode==29)ok &= check(value.find("nested output ready")!=std::string::npos && SendMessageW(window,WM_APP+16,0,0),"native containers do not synthesize duplicate accessibility trees");
|
||||||
|
if(mode==30)ok &= check(value.find("active mdi text")!=std::string::npos && value.find("covered mdi text")==std::string::npos,"maximized MDI child excludes covered background text");
|
||||||
|
if(mode==31)ok &= check(value.find("active mdi text")!=std::string::npos && value.find("covered mdi text")!=std::string::npos,"side-by-side MDI children retain both visible documents");
|
||||||
|
if (mode == 31) ok &= check(value.find("Active") != std::string::npos && value.find("Background") != std::string::npos,
|
||||||
|
"side-by-side MDI window titles remain available for review");
|
||||||
|
if (mode == 32) ok &= check(value.find("deep clipped visible line") != std::string::npos,
|
||||||
|
"hundreds of clipped rows do not hide the visible tail");
|
||||||
|
if (mode == 33) {
|
||||||
|
bool safe=true, status_present=false;
|
||||||
|
const auto status_id=store.identity(GetDlgItem(window,114))+"_a";
|
||||||
|
for(const auto &node:result.nodes) {
|
||||||
|
if(node.id==status_id)status_present=true;
|
||||||
|
safe &= node.name.find('\0')==std::string::npos;
|
||||||
|
for(const auto &range:node.ranges)safe &= range.text.find('\0')==std::string::npos;
|
||||||
|
}
|
||||||
|
ok &= check(status_present && safe,"padded MSAA status name produces D-Bus-safe accessibility text");
|
||||||
|
ok &= check(value.find("Ready")!=std::string::npos,
|
||||||
|
"status bar retains its exposed label");
|
||||||
|
}
|
||||||
if (mode == 21) {
|
if (mode == 21) {
|
||||||
Sleep(750);
|
Sleep(750);
|
||||||
ok &= check(SendMessageW(window,WM_APP + 15,0,0), "remote buffer lives until the delayed target returns");
|
ok &= check(SendMessageW(window,WM_APP + 15,0,0), "remote buffer lives until the delayed target returns");
|
||||||
@@ -128,6 +173,23 @@ int main(int argc, char **argv) {
|
|||||||
}
|
}
|
||||||
if (mode != 4) ok &= check(SendMessageW(window, WM_APP + 11, 0, 0), "capture leaves caret and focus unchanged");
|
if (mode != 4) ok &= check(SendMessageW(window, WM_APP + 11, 0, 0), "capture leaves caret and focus unchanged");
|
||||||
}
|
}
|
||||||
|
// A local provider avoids cross-process WM_GETTEXT marshalling, which
|
||||||
|
// itself truncates at the first NUL. Its BSTR retains malformed padding.
|
||||||
|
WNDCLASSW local{};local.hInstance=GetModuleHandleW(nullptr);
|
||||||
|
local.lpfnWndProc=DefWindowProcW;local.lpszClassName=L"WineReviewLocalRoot";
|
||||||
|
RegisterClassW(&local);
|
||||||
|
HWND local_root=CreateWindowW(local.lpszClassName,L"Padded MSAA name",WS_OVERLAPPEDWINDOW,
|
||||||
|
100,100,400,200,nullptr,nullptr,local.hInstance,nullptr);
|
||||||
|
local.lpfnWndProc=padded_name_proc;local.lpszClassName=L"WineReviewPaddedName";
|
||||||
|
RegisterClassW(&local);
|
||||||
|
HWND padded=CreateWindowW(local.lpszClassName,L"",WS_CHILD|WS_VISIBLE,
|
||||||
|
10,10,200,30,local_root,nullptr,local.hInstance,nullptr);
|
||||||
|
ShowWindow(local_root,SW_SHOW);SetForegroundWindow(local_root);SetFocus(padded);
|
||||||
|
auto padded_focus=store.observe_foreground();
|
||||||
|
auto padded_capture=capture_review_window(store,padded_focus,GetTickCount64()+500);
|
||||||
|
ok &= check(padded_capture.status=="ok" && joined(padded_capture).find("Ready")!=std::string::npos,
|
||||||
|
"malformed UTF-16 padding does not discard the valid MSAA label");
|
||||||
|
DestroyWindow(local_root);
|
||||||
CoUninitialize();
|
CoUninitialize();
|
||||||
#endif
|
#endif
|
||||||
PostMessageW(window, WM_CLOSE, 0, 0);
|
PostMessageW(window, WM_CLOSE, 0, 0);
|
||||||
|
|||||||
+100
-1
@@ -11,6 +11,7 @@
|
|||||||
#define REVIEW_PRIVACY_CHECK (WM_APP + 14)
|
#define REVIEW_PRIVACY_CHECK (WM_APP + 14)
|
||||||
#define REVIEW_REMOTE_CHECK (WM_APP + 15)
|
#define REVIEW_REMOTE_CHECK (WM_APP + 15)
|
||||||
static HWND editor, output, password, hidden;
|
static HWND editor, output, password, hidden;
|
||||||
|
static HWND mdi, mdi_active, mdi_background;
|
||||||
static WCHAR *large;
|
static WCHAR *large;
|
||||||
static WNDPROC original_edit;
|
static WNDPROC original_edit;
|
||||||
static BOOL replace_during_read;
|
static BOOL replace_during_read;
|
||||||
@@ -24,6 +25,27 @@ static WNDPROC original_list;
|
|||||||
static WNDPROC original_table;
|
static WNDPROC original_table;
|
||||||
static BOOL replace_table_during_read;
|
static BOOL replace_table_during_read;
|
||||||
static BOOL delay_table_read, freed_table_memory, scroll_during_position;
|
static BOOL delay_table_read, freed_table_memory, scroll_during_position;
|
||||||
|
static BOOL frame_pump;
|
||||||
|
static BOOL delay_batch;
|
||||||
|
static unsigned int synthetic_containers;
|
||||||
|
static LRESULT CALLBACK status_proc(HWND window,UINT message,WPARAM wp,LPARAM lp) {
|
||||||
|
// Like MUSHclient's status bar, report a length beyond the first NUL.
|
||||||
|
// The standard MSAA fallback retains that padding in its BSTR name.
|
||||||
|
const WCHAR text[]=L"Ready\0Connected";
|
||||||
|
if(message==WM_GETTEXTLENGTH)return (sizeof(text)/sizeof(*text))-1;
|
||||||
|
if(message==WM_GETTEXT && wp) {
|
||||||
|
size_t copied=(sizeof(text)/sizeof(*text))-1;
|
||||||
|
if(copied>wp-1)copied=wp-1;
|
||||||
|
memcpy((void*)lp,text,copied*sizeof(*text));
|
||||||
|
((WCHAR*)lp)[copied]=0;
|
||||||
|
return copied;
|
||||||
|
}
|
||||||
|
return DefWindowProcW(window,message,wp,lp);
|
||||||
|
}
|
||||||
|
static LRESULT CALLBACK container_proc(HWND window,UINT message,WPARAM wp,LPARAM lp) {
|
||||||
|
if(message==WM_GETOBJECT && lp==OBJID_QUERYCLASSNAMEIDX){++synthetic_containers;Sleep(25);}
|
||||||
|
return DefWindowProcW(window,message,wp,lp);
|
||||||
|
}
|
||||||
static LRESULT CALLBACK table_proc(HWND window, UINT message, WPARAM wp, LPARAM lp) {
|
static LRESULT CALLBACK table_proc(HWND window, UINT message, WPARAM wp, LPARAM lp) {
|
||||||
if (message == LVM_GETITEMTEXTW && delay_table_read) {
|
if (message == LVM_GETITEMTEXTW && delay_table_read) {
|
||||||
delay_table_read=FALSE;
|
delay_table_read=FALSE;
|
||||||
@@ -59,6 +81,7 @@ static LRESULT CALLBACK stall_proc(HWND window, UINT message, WPARAM wp, LPARAM
|
|||||||
return DefWindowProcW(window,message,wp,lp);
|
return DefWindowProcW(window,message,wp,lp);
|
||||||
}
|
}
|
||||||
static LRESULT CALLBACK edit_proc(HWND window, UINT message, WPARAM wp, LPARAM lp) {
|
static LRESULT CALLBACK edit_proc(HWND window, UINT message, WPARAM wp, LPARAM lp) {
|
||||||
|
if(message==EM_LINEINDEX && delay_batch){delay_batch=FALSE;Sleep(650);}
|
||||||
if (message == WM_GETTEXT && (GetWindowLongPtrW(window,GWL_STYLE) & ES_PASSWORD)) ++protected_text_reads;
|
if (message == WM_GETTEXT && (GetWindowLongPtrW(window,GWL_STYLE) & ES_PASSWORD)) ++protected_text_reads;
|
||||||
if (message == WM_KEYDOWN) ++key_down;
|
if (message == WM_KEYDOWN) ++key_down;
|
||||||
if (message == WM_KEYUP) ++key_up;
|
if (message == WM_KEYUP) ++key_up;
|
||||||
@@ -87,6 +110,12 @@ static LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wp, LPARAM
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (message == REVIEW_CONFIG) {
|
if (message == REVIEW_CONFIG) {
|
||||||
|
frame_pump=FALSE;
|
||||||
|
delay_batch=FALSE;
|
||||||
|
synthetic_containers=0;
|
||||||
|
ShowWindow(GetDlgItem(window,112),SW_HIDE);
|
||||||
|
ShowWindow(GetDlgItem(window,114),SW_HIDE);
|
||||||
|
ShowWindow(mdi,SW_HIDE);
|
||||||
SendMessageW(editor,EM_SETPASSWORDCHAR,0,0);
|
SendMessageW(editor,EM_SETPASSWORDCHAR,0,0);
|
||||||
protect_during_length = replace_list_during_read = FALSE;
|
protect_during_length = replace_list_during_read = FALSE;
|
||||||
replace_table_during_read=FALSE;
|
replace_table_during_read=FALSE;
|
||||||
@@ -105,6 +134,55 @@ static LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wp, LPARAM
|
|||||||
SendMessageW(editor, EM_SETSEL, 0, 0);
|
SendMessageW(editor, EM_SETSEL, 0, 0);
|
||||||
MoveWindow(editor, 20, 35, 320, 90, TRUE);
|
MoveWindow(editor, 20, 35, 320, 90, TRUE);
|
||||||
switch (wp) {
|
switch (wp) {
|
||||||
|
case 33: ShowWindow(GetDlgItem(window,114),SW_SHOW); break;
|
||||||
|
case 32: {
|
||||||
|
WCHAR text[16384]; unsigned used = 0;
|
||||||
|
for (unsigned row = 0; row < 400; ++row) {
|
||||||
|
const WCHAR line[] = L"deep clipped visible line\r\n";
|
||||||
|
for (unsigned j = 0; j < (sizeof(line)/sizeof(*line))-1; ++j) text[used++] = line[j];
|
||||||
|
}
|
||||||
|
text[used] = 0;
|
||||||
|
SetWindowTextW(editor, text);
|
||||||
|
MoveWindow(editor, 20, -5000, 320, 5100, TRUE);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 30:
|
||||||
|
ShowWindow(mdi,SW_SHOW);
|
||||||
|
SendMessageW(mdi,WM_MDIACTIVATE,(WPARAM)mdi_active,0);
|
||||||
|
SendMessageW(mdi,WM_MDIMAXIMIZE,(WPARAM)mdi_active,0);
|
||||||
|
break;
|
||||||
|
case 31:
|
||||||
|
ShowWindow(mdi,SW_SHOW);
|
||||||
|
SendMessageW(mdi,WM_MDIRESTORE,(WPARAM)mdi_active,0);
|
||||||
|
SendMessageW(mdi,WM_MDIRESTORE,(WPARAM)mdi_background,0);
|
||||||
|
MoveWindow(mdi_background,0,0,240,300,TRUE);
|
||||||
|
MoveWindow(mdi_active,245,0,240,300,TRUE);
|
||||||
|
SendMessageW(mdi,WM_MDIACTIVATE,(WPARAM)mdi_active,0);
|
||||||
|
break;
|
||||||
|
case 28: {
|
||||||
|
WCHAR text[4096];unsigned used=0;
|
||||||
|
for(unsigned row=0;row<80;++row) {
|
||||||
|
const WCHAR line[]=L"top clipped visible line\r\n";
|
||||||
|
for(unsigned j=0;j<(sizeof(line)/sizeof(*line))-1;++j)text[used++]=line[j];
|
||||||
|
}
|
||||||
|
text[used]=0;SetWindowTextW(editor,text);
|
||||||
|
MoveWindow(editor,20,-250,320,380,TRUE);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 29:ShowWindow(GetDlgItem(window,112),SW_SHOW);break;
|
||||||
|
case 27:delay_batch=TRUE;break;
|
||||||
|
case 26: {
|
||||||
|
WCHAR text[4096];unsigned used=0;
|
||||||
|
for(unsigned row=0;row<80;++row) {
|
||||||
|
const WCHAR line[]=L"stable buffered output line\r\n";
|
||||||
|
for(unsigned j=0;j<(sizeof(line)/sizeof(*line))-1;++j)text[used++]=line[j];
|
||||||
|
}
|
||||||
|
text[used]=0;SetWindowTextW(editor,text);
|
||||||
|
MoveWindow(editor,20,35,320,380,TRUE);
|
||||||
|
ShowWindow(output,SW_HIDE);ShowWindow(GetDlgItem(window,106),SW_HIDE);
|
||||||
|
frame_pump=TRUE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 1: // surrogate before first displayed row, absolute offsets
|
case 1: // surrogate before first displayed row, absolute offsets
|
||||||
SetWindowTextW(editor, L"\U0001f600alpha\r\nbeta\r\nlast");
|
SetWindowTextW(editor, L"\U0001f600alpha\r\nbeta\r\nlast");
|
||||||
SendMessageW(editor, EM_LINESCROLL, 0, 1);
|
SendMessageW(editor, EM_LINESCROLL, 0, 1);
|
||||||
@@ -166,6 +244,7 @@ static LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wp, LPARAM
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
if (message == REVIEW_PRIVACY_CHECK) return protected_text_reads == 0;
|
if (message == REVIEW_PRIVACY_CHECK) return protected_text_reads == 0;
|
||||||
|
if (message == WM_APP+16) return synthetic_containers==0;
|
||||||
if (message == REVIEW_REMOTE_CHECK) return !freed_table_memory;
|
if (message == REVIEW_REMOTE_CHECK) return !freed_table_memory;
|
||||||
if (message == REVIEW_CHECK || message == REVIEW_CHECK_RIGHT || message == REVIEW_CHECK_STRICT) {
|
if (message == REVIEW_CHECK || message == REVIEW_CHECK_RIGHT || message == REVIEW_CHECK_STRICT) {
|
||||||
DWORD start = 0, end = 0;
|
DWORD start = 0, end = 0;
|
||||||
@@ -243,11 +322,31 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR args, int show)
|
|||||||
CreateWindowW(L"Static",L"literal & label",WS_CHILD|SS_NOPREFIX,
|
CreateWindowW(L"Static",L"literal & label",WS_CHILD|SS_NOPREFIX,
|
||||||
20,365,200,20,window,(HMENU)111,instance,NULL);
|
20,365,200,20,window,(HMENU)111,instance,NULL);
|
||||||
CreateWindowW(stall.lpszClassName,L"",WS_CHILD|WS_VISIBLE,350,150,150,20,window,NULL,instance,NULL);
|
CreateWindowW(stall.lpszClassName,L"",WS_CHILD|WS_VISIBLE,350,150,150,20,window,NULL,instance,NULL);
|
||||||
|
WNDCLASSW container={0};container.lpfnWndProc=container_proc;container.hInstance=instance;container.lpszClassName=L"WineAccessNativeContainer";
|
||||||
|
RegisterClassW(&container);
|
||||||
|
HWND parent=CreateWindowW(container.lpszClassName,L"",WS_CHILD,20,35,320,100,window,(HMENU)112,instance,NULL);
|
||||||
|
for(unsigned level=0;level<8;++level)parent=CreateWindowW(container.lpszClassName,L"",WS_CHILD|WS_VISIBLE,0,0,320,100,parent,NULL,instance,NULL);
|
||||||
|
CreateWindowW(L"Edit",L"nested output ready",WS_CHILD|WS_VISIBLE|ES_READONLY,0,0,320,80,parent,NULL,instance,NULL);
|
||||||
|
WNDCLASSW mdi_child={0};mdi_child.lpfnWndProc=DefMDIChildProcW;mdi_child.hInstance=instance;mdi_child.lpszClassName=L"WineAccessMdiChild";
|
||||||
|
RegisterClassW(&mdi_child);
|
||||||
|
CLIENTCREATESTRUCT client_create={NULL,200};
|
||||||
|
mdi=CreateWindowW(L"MDIClient",L"",WS_CHILD|WS_CLIPCHILDREN,20,35,500,350,window,(HMENU)113,instance,&client_create);
|
||||||
|
mdi_background=CreateMDIWindowW(mdi_child.lpszClassName,L"Background",WS_OVERLAPPEDWINDOW|WS_VISIBLE,0,0,240,300,mdi,instance,0);
|
||||||
|
CreateWindowW(L"Edit",L"covered mdi text",WS_CHILD|WS_VISIBLE|ES_READONLY,0,0,220,100,mdi_background,NULL,instance,NULL);
|
||||||
|
mdi_active=CreateMDIWindowW(mdi_child.lpszClassName,L"Active",WS_OVERLAPPEDWINDOW|WS_VISIBLE,245,0,240,300,mdi,instance,0);
|
||||||
|
CreateWindowW(L"Edit",L"active mdi text",WS_CHILD|WS_VISIBLE|ES_READONLY,0,0,220,100,mdi_active,NULL,instance,NULL);
|
||||||
|
WNDCLASSW status=cls;status.lpszClassName=L"WineAccessStatusFixture";status.lpfnWndProc=status_proc;
|
||||||
|
RegisterClassW(&status);
|
||||||
|
CreateWindowW(status.lpszClassName,L"",WS_CHILD,20,420,480,20,window,(HMENU)114,instance,NULL);
|
||||||
ShowWindow(window, SW_SHOW);
|
ShowWindow(window, SW_SHOW);
|
||||||
SendMessageW(window, REVIEW_CONFIG, 0, 0);
|
SendMessageW(window, REVIEW_CONFIG, 0, 0);
|
||||||
if (secondary) SetTimer(window,1,400,NULL);
|
if (secondary) SetTimer(window,1,400,NULL);
|
||||||
MSG message;
|
MSG message;
|
||||||
while (GetMessageW(&message, NULL, 0, 0) > 0) {
|
for (;;) {
|
||||||
|
if (frame_pump) {
|
||||||
|
if (!PeekMessageW(&message,NULL,0,0,PM_REMOVE)) { Sleep(16);continue; }
|
||||||
|
if (message.message==WM_QUIT)break;
|
||||||
|
} else if (GetMessageW(&message,NULL,0,0)<=0)break;
|
||||||
TranslateMessage(&message); DispatchMessageW(&message);
|
TranslateMessage(&message); DispatchMessageW(&message);
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -115,6 +115,20 @@ with tempfile.TemporaryDirectory(prefix='wine-review-wire-') as temporary:
|
|||||||
assert isinstance(wrong[0],GLib.Error) and 'AccessDenied' in str(wrong[0])
|
assert isinstance(wrong[0],GLib.Error) and 'AccessDenied' in str(wrong[0])
|
||||||
other.close_sync(None)
|
other.close_sync(None)
|
||||||
print('PASS: test_wrong_caller')
|
print('PASS: test_wrong_caller')
|
||||||
|
configure(33)
|
||||||
|
reply,_ = capture(); data=reply.unpack()[0]
|
||||||
|
assert data['status']=='ok', data['status']
|
||||||
|
ready=False
|
||||||
|
for node in data['nodes']:
|
||||||
|
assert '\0' not in node['name']
|
||||||
|
for start,end,native_start,native_end,text,bounds in node['ranges']:
|
||||||
|
assert '\0' not in text
|
||||||
|
assert end-start==len(text)
|
||||||
|
assert native_end-native_start==len(text.encode('utf-16-le'))//2
|
||||||
|
if text=='Ready': ready=True
|
||||||
|
assert ready, ('Padded MSAA status name lost its exposed text', data)
|
||||||
|
subprocess.run(['wine',str(fixture),'--check'],env=env,check=True,timeout=5)
|
||||||
|
print('PASS: test_padded_status_name_wire_lengths')
|
||||||
configure(9)
|
configure(9)
|
||||||
reply,_ = capture(); data=reply.unpack()[0]
|
reply,_ = capture(); data=reply.unpack()[0]
|
||||||
assert data['status'] in ('ok','stale')
|
assert data['status'] in ('ok','stale')
|
||||||
|
|||||||
Reference in new issue
Block a user