Release v0.6.10: preserve typing during correction and add personal dictionaries

This commit is contained in:
2026-10-29 22:44:08 +02:00
parent 5c786e12a9
commit 8b5f632ad7
20 changed files with 362 additions and 35 deletions
+87 -2
View File
@@ -598,6 +598,27 @@ bool App::handleKeyboard(const KBDLLHOOKSTRUCT &key, bool down, UINT mods)
// down AND up events use exactly the same path as physical keyboard input.
if (ownKeyboardInput(key) || installingUpdate)
return false;
{
std::lock_guard lock(stateMutex);
if (typingTarget && GetForegroundWindow() == typingTarget)
{
INPUT input{};
input.type = INPUT_KEYBOARD;
input.ki.wVk = static_cast<WORD>(key.vkCode);
input.ki.wScan = static_cast<WORD>(key.scanCode);
input.ki.dwFlags = (down ? 0 : KEYEVENTF_KEYUP) |
((key.flags & LLKHF_EXTENDED) ? KEYEVENTF_EXTENDEDKEY : 0);
if (key.vkCode == VK_PACKET)
{
input.ki.wVk = 0;
input.ki.dwFlags |= KEYEVENTF_UNICODE;
}
// Replay as ordinary input so tracking and shortcuts see it once,
// after the replacement and layout switch have finished.
deferredTyping.push_back(input);
return true;
}
}
DWORD captureProcess{};
if (hotkeyCapture) GetWindowThreadProcessId(hotkeyCapture, &captureProcess);
if (hotkeyCapture && (!captureProcess || captureProcess != hotkeyCaptureProcess))
@@ -640,6 +661,41 @@ bool App::handleKeyboard(const KBDLLHOOKSTRUCT &key, bool down, UINT mods)
return true;
return false;
}
bool App::beginTypingDeferral(HWND target, uint64_t version)
{
std::lock_guard lock(stateMutex);
if (inputVersion != version || GetForegroundWindow() != target || !modifiersReleased())
return false;
typingTarget = target;
return true;
}
void App::replayTyping()
{
std::vector<INPUT> inputs;
HWND target;
{
std::lock_guard lock(stateMutex);
target = typingTarget;
typingTarget = nullptr;
inputs.swap(deferredTyping);
}
busy = false;
if (inputs.empty())
return;
if (GetForegroundWindow() != target)
{
resetInput();
report(L"Typing during correction was not replayed because the active window changed.");
return;
}
// Runs on the hook thread: no physical key can pass between releasing the
// gate and submitting the entire ordered batch to the Windows input queue.
if (SendInput(static_cast<UINT>(inputs.size()), inputs.data(), sizeof(INPUT)) != inputs.size())
{
resetInput();
report(L"Windows could not replay all typing buffered during correction.");
}
}
LRESULT CALLBACK App::keyProc(int code, WPARAM wp, LPARAM lp)
{
if (code == HC_ACTION && current &&
@@ -762,11 +818,16 @@ void App::transform(int command, int snippet)
struct BusyReset
{
std::atomic<bool> &b;
HWND window;
bool deferred{};
~BusyReset()
{
b = false;
if (deferred)
PostMessageW(window, ReplayTypingMessage, 0, 0);
else
b = false;
}
} reset{busy};
} reset{busy, window};
auto start = GetTickCount64();
std::wstring reason = L"cancelled-before-selection";
std::wstring text, result, expected;
@@ -849,6 +910,21 @@ void App::transform(int command, int snippet)
return;
}
reason = L"clipboard-snapshot";
if (command == AutoCorrect)
{
if (!beginTypingDeferral(target, version))
return;
reset.deferred = true;
}
// Collapse a temporary selection on every failed auto-correction,
// including clipboard exceptions, before queued typing is replayed.
bool autoSelected = false;
struct SelectionReset
{
bool &selected;
std::function<bool()> valid;
~SelectionReset() { if (selected && valid()) chord(VK_RIGHT); }
} selectionReset{autoSelected, valid};
Transaction tx(*this, target, version);
reason = L"selection-unavailable";
diagnosticEvent(L"Transform target=" + processName(target) + L" command=" + std::to_wstring(command) +
@@ -865,6 +941,7 @@ void App::transform(int command, int snippet)
// conversion, automatic correction must never fall back to a suffix.
if (!tx.valid() || !chord(VK_LEFT, MOD_CONTROL | MOD_SHIFT))
return;
autoSelected = true;
Sleep(25);
text = tx.copy();
// Some editors first select only ") ". Extend by whole-word
@@ -881,7 +958,10 @@ void App::transform(int command, int snippet)
if (!suggestion || !tx.valid() || !autoCorrectionAllowed(target, config))
{
if (tx.valid())
{
chord(VK_RIGHT);
autoSelected = false;
}
return;
}
to = suggestion->target;
@@ -1018,6 +1098,8 @@ void App::transform(int command, int snippet)
sent = tx.paste(textClip(result));
else if (automatic && tx.valid())
chord(VK_RIGHT);
if (sent || result == text)
autoSelected = false;
reason = result == text ? L"conversion-unchanged" :
sent ? L"paste-sent-unverified" : L"paste-cancelled";
if (result == text &&
@@ -1377,6 +1459,9 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
case WM_HOTKEY:
app->dispatch((int)wp);
return 0;
case ReplayTypingMessage:
app->replayTyping();
return 0;
case TrayMessage:
if (LOWORD(lp) == WM_CONTEXTMENU)
app->trayMenu();