#pragma once #include "platform.h" #include namespace ks { // Read-only UI Automation: never select, move the caret, or touch the clipboard. class PasteProbe { IUIAutomation2 *automation{}; IUIAutomationElement *element{}; IUIAutomationTextPattern *pattern{}; public: std::wstring status = L"not-requested"; PasteProbe(bool enabled, const std::wstring &original) { if (!enabled) return; status = L"accessibility-unavailable"; if (FAILED(CoCreateInstance(CLSID_CUIAutomation8, nullptr, CLSCTX_INPROC_SERVER, IID_IUIAutomation2, (void **)&automation))) return; automation->put_ConnectionTimeout(200); automation->put_TransactionTimeout(200); if (FAILED(automation->GetFocusedElement(&element)) || !element) return; BOOL password = TRUE; if (FAILED(element->get_CurrentIsPassword(&password)) || password) { status = L"protected-or-inaccessible-field"; return; } if (FAILED(element->GetCurrentPatternAs(UIA_TextPatternId, IID_IUIAutomationTextPattern, (void **)&pattern)) || !pattern) return; auto selected = read(1, false); auto normalize = [](std::wstring value) { for (size_t at = 0; (at = value.find(L"\r\n", at)) != value.npos;) value.erase(at, 1); return value; }; if (!selected || normalize(*selected) != normalize(original)) { if (selected) status = L"initial-selection-mismatch"; if (pattern) pattern->Release(); pattern = nullptr; return; } status = L"ready"; } PasteProbe(const PasteProbe &) = delete; ~PasteProbe() { if (pattern) pattern->Release(); if (element) element->Release(); if (automation) automation->Release(); } std::optional read(int characters, bool allowCaret = true) { if (!pattern) return {}; if (characters <= 0 || characters > 4096) { status = L"readback-size-limit"; return {}; } IUIAutomationElement *focused{}; BOOL same = FALSE; if (SUCCEEDED(automation->GetFocusedElement(&focused)) && focused) { automation->CompareElements(element, focused, &same); } if (!same) { if (focused) focused->Release(); status = L"focused-control-changed"; return {}; } BOOL password = TRUE; if (FAILED(focused->get_CurrentIsPassword(&password)) || password) { focused->Release(); status = L"protected-or-inaccessible-field"; return {}; } // Native Edit accessibility proxies can retain a text snapshot in the // pattern object. Obtain a fresh pattern after paste, not the pre-paste one. pattern->Release(); pattern = nullptr; HRESULT refreshed = focused->GetCurrentPatternAs(UIA_TextPatternId, IID_IUIAutomationTextPattern, (void **)&pattern); focused->Release(); if (FAILED(refreshed) || !pattern) { status = L"accessibility-unavailable"; return {}; } IUIAutomationTextRangeArray *ranges{}; if (FAILED(pattern->GetSelection(&ranges)) || !ranges) { status = L"selection-unavailable"; return {}; } int count{}; ranges->get_Length(&count); IUIAutomationTextRange *range{}; if (count == 1) ranges->GetElement(0, &range); ranges->Release(); if (!range) { status = L"selection-ambiguous"; return {}; } int difference{}; HRESULT hr = range->CompareEndpoints(TextPatternRangeEndpoint_Start, range, TextPatternRangeEndpoint_End, &difference); if (SUCCEEDED(hr) && difference == 0) { if (!allowCaret) { range->Release(); status = L"initial-selection-unavailable"; return {}; } int moved{}; hr = range->MoveEndpointByUnit(TextPatternRangeEndpoint_Start, TextUnit_Character, -characters, &moved); if (SUCCEEDED(hr) && moved != -characters) hr = E_FAIL; } BSTR value{}; if (SUCCEEDED(hr)) hr = range->GetText(8193, &value); range->Release(); if (FAILED(hr)) { SysFreeString(value); status = L"range-read-failed"; return {}; } std::wstring text(value ? value : L"", SysStringLen(value)); SysFreeString(value); if (text.size() > 8192) { status = L"readback-size-limit"; return {}; } status = L"read-back-via-accessibility"; return text; } }; } // namespace ks