Release v0.6.31: verify trailing spaces omitted by editor copy
This commit is contained in:
+60
-3
@@ -76,6 +76,8 @@ class Transaction
|
||||
mutable bool invalidLogged{};
|
||||
unsigned copyNumber{};
|
||||
std::wstring adopted;
|
||||
std::unique_ptr<PasteProbe> selectionProof;
|
||||
std::wstring provenSelection;
|
||||
|
||||
public:
|
||||
Transaction(App &a, HWND t, uint64_t v) : app(a), target(t), version(v)
|
||||
@@ -250,8 +252,27 @@ class Transaction
|
||||
}
|
||||
}
|
||||
}
|
||||
selectionProof.reset();
|
||||
provenSelection.clear();
|
||||
// Viber and some other editors omit trailing ASCII spaces from Ctrl+C.
|
||||
// Recover only from a real selection, never from the recorded keys alone.
|
||||
if (valid() && !text.empty() && text.size() < 8192 && text.back() != L' ')
|
||||
{
|
||||
auto proof = std::make_unique<PasteProbe>(true, target);
|
||||
auto selected = proof->read(1, false);
|
||||
if (selected && selected->size() > text.size() && selected->starts_with(text) &&
|
||||
selected->find_first_not_of(L' ', text.size()) == std::wstring::npos && valid())
|
||||
{
|
||||
diagnosticEvent(L"Copy: trailing spaces verified via accessibility copied=" +
|
||||
diagnosticText(text) + L" selected=" + diagnosticText(*selected));
|
||||
text = *selected;
|
||||
provenSelection = text;
|
||||
selectionProof = std::move(proof);
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
bool recoveredSelection() const { return selectionProof != nullptr; }
|
||||
bool select(size_t n)
|
||||
{
|
||||
return selectPrevious(n, [&] { return valid(); });
|
||||
@@ -324,6 +345,15 @@ class Transaction
|
||||
}
|
||||
bool paste(const Clip &clip)
|
||||
{
|
||||
if (selectionProof)
|
||||
{
|
||||
auto selected = selectionProof->read(1, false);
|
||||
if (!selected || *selected != provenSelection || !valid())
|
||||
{
|
||||
diagnosticEvent(L"Paste cancelled: recovered selection changed or unavailable");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!valid())
|
||||
return false;
|
||||
write(clip);
|
||||
@@ -1341,13 +1371,30 @@ void App::transform(int command, int snippet)
|
||||
}
|
||||
}
|
||||
bool sent = false;
|
||||
PasteProbe probe(config.writeLog && manual && result != text, text);
|
||||
const bool requireReadback = tx.recoveredSelection();
|
||||
PasteProbe probe(((config.writeLog && manual) || requireReadback) && result != text, text, target);
|
||||
if (requireReadback && result != text && probe.status != L"ready")
|
||||
{
|
||||
reason = L"recovered-selection-unavailable";
|
||||
std::lock_guard lock(stateMutex);
|
||||
last.reset();
|
||||
run.reset();
|
||||
return;
|
||||
}
|
||||
diagnosticEvent(L"Conversion source=" + std::to_wstring(hint) + L" target=" + std::to_wstring(to) +
|
||||
L" changed=" + std::to_wstring(result != text));
|
||||
if (result != text)
|
||||
sent = tx.paste(textClip(result));
|
||||
else if (automatic && tx.valid())
|
||||
chord(VK_RIGHT);
|
||||
if (requireReadback && result != text && !sent)
|
||||
{
|
||||
reason = L"recovered-selection-paste-cancelled";
|
||||
std::lock_guard lock(stateMutex);
|
||||
last.reset();
|
||||
run.reset();
|
||||
return;
|
||||
}
|
||||
if (sent || result == text)
|
||||
autoSelected = false;
|
||||
reason = result == text ? L"conversion-unchanged" :
|
||||
@@ -1357,7 +1404,7 @@ void App::transform(int command, int snippet)
|
||||
reason = L"Conversion unchanged";
|
||||
verification = L"calculated-result-equals-original";
|
||||
}
|
||||
if (sent && manual && config.writeLog)
|
||||
if (sent && ((manual && config.writeLog) || requireReadback))
|
||||
{
|
||||
verification = probe.status;
|
||||
if (tx.valid())
|
||||
@@ -1388,7 +1435,17 @@ void App::transform(int command, int snippet)
|
||||
return;
|
||||
}
|
||||
}
|
||||
else reason = L"Paste not verified";
|
||||
else
|
||||
{
|
||||
reason = L"Paste not verified";
|
||||
if (requireReadback)
|
||||
{
|
||||
std::lock_guard lock(stateMutex);
|
||||
last.reset();
|
||||
run.reset();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
else verification = L"context-changed-after-paste";
|
||||
}
|
||||
|
||||
+18
-1
@@ -13,7 +13,7 @@ class PasteProbe
|
||||
|
||||
public:
|
||||
std::wstring status = L"not-requested";
|
||||
PasteProbe(bool enabled, const std::wstring &original)
|
||||
PasteProbe(bool enabled, HWND target)
|
||||
{
|
||||
if (!enabled)
|
||||
return;
|
||||
@@ -25,6 +25,18 @@ class PasteProbe
|
||||
automation->put_TransactionTimeout(200);
|
||||
if (FAILED(automation->GetFocusedElement(&element)) || !element)
|
||||
return;
|
||||
if (target)
|
||||
{
|
||||
DWORD process{};
|
||||
GetWindowThreadProcessId(target, &process);
|
||||
int focusedProcess{};
|
||||
if (!process || FAILED(element->get_CurrentProcessId(&focusedProcess)) ||
|
||||
process != static_cast<DWORD>(focusedProcess) || GetForegroundWindow() != target)
|
||||
{
|
||||
status = L"focused-control-changed";
|
||||
return;
|
||||
}
|
||||
}
|
||||
BOOL password = TRUE;
|
||||
if (FAILED(element->get_CurrentIsPassword(&password)) || password)
|
||||
{
|
||||
@@ -35,6 +47,11 @@ class PasteProbe
|
||||
(void **)&pattern)) ||
|
||||
!pattern)
|
||||
return;
|
||||
}
|
||||
PasteProbe(bool enabled, const std::wstring &original, HWND target = nullptr)
|
||||
: PasteProbe(enabled, target)
|
||||
{
|
||||
if (!pattern) return;
|
||||
auto selected = read(1, false);
|
||||
auto normalize = [](std::wstring value)
|
||||
{
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
#include "platform.h"
|
||||
namespace ks
|
||||
{
|
||||
inline constexpr wchar_t AppVersion[] = L"0.6.30";
|
||||
inline constexpr wchar_t AppVersion[] = L"0.6.31";
|
||||
inline constexpr UINT UpdateMessage = WM_APP + 7;
|
||||
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
|
||||
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
|
||||
|
||||
Reference in New Issue
Block a user