Release v0.6.18: verify conversion results and log problems only

This commit is contained in:
2026-10-31 01:36:56 +02:00
parent 4eb9f6c671
commit 0d3b2acae9
18 changed files with 915 additions and 291 deletions
+162
View File
@@ -0,0 +1,162 @@
#pragma once
#include "platform.h"
#include <uiautomation.h>
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<std::wstring> 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