From ded3cf3fb45f3ed5b2a0b71143394b6eca2bdeff Mon Sep 17 00:00:00 2001 From: fanlusky <1730976608@qq.com> Date: Sat, 22 Aug 2026 16:51:48 +0800 Subject: [PATCH] Attempt to fix the flickering issue with Microsoft Shuangpin. --- src/IME/MetasequoiaIME.cpp | 2 + src/IME/MetasequoiaIME.h | 8 ++-- src/Key/KeyEventSink.cpp | 98 ++++++++++++++++++++++++++++++++------ 3 files changed, 91 insertions(+), 17 deletions(-) diff --git a/src/IME/MetasequoiaIME.cpp b/src/IME/MetasequoiaIME.cpp index 2c237597..c92121a1 100644 --- a/src/IME/MetasequoiaIME.cpp +++ b/src/IME/MetasequoiaIME.cpp @@ -448,6 +448,8 @@ CMetasequoiaIME::CMetasequoiaIME() _deferredProjectedPunctuationOpen = false; _deferredProjectedDoubleSingleByteOpen = false; _deferredProjectedInputLength = 0; + _deferredProjectedRawInput.clear(); + _deferredProjectedCaret = 0; _deferredProjectedCandidateActive = false; _deferredProjectedUnicodeMode = false; _deferredKeyFocusGeneration = 1; diff --git a/src/IME/MetasequoiaIME.h b/src/IME/MetasequoiaIME.h index b8c924c9..b6d004fb 100644 --- a/src/IME/MetasequoiaIME.h +++ b/src/IME/MetasequoiaIME.h @@ -591,9 +591,11 @@ class CMetasequoiaIME : public ITfTextInputProcessorEx, bool _deferredKeyProjectionValid; bool _deferredProjectedImeOpen; bool _deferredProjectedPunctuationOpen; - bool _deferredProjectedDoubleSingleByteOpen; - size_t _deferredProjectedInputLength; - bool _deferredProjectedCandidateActive; + bool _deferredProjectedDoubleSingleByteOpen; + size_t _deferredProjectedInputLength; + std::wstring _deferredProjectedRawInput; + size_t _deferredProjectedCaret; + bool _deferredProjectedCandidateActive; bool _deferredProjectedUnicodeMode; uint64_t _deferredKeyFocusGeneration; bool _deferredKeyDrainPosted; diff --git a/src/Key/KeyEventSink.cpp b/src/Key/KeyEventSink.cpp index 7ba7d38b..0b901952 100644 --- a/src/Key/KeyEventSink.cpp +++ b/src/Key/KeyEventSink.cpp @@ -36,6 +36,8 @@ struct DeferredShadowState bool punctuationOpen = false; bool doubleSingleByteOpen = false; size_t inputLength = 0; + std::wstring rawInput; + size_t caret = 0; bool candidateActive = false; bool unicodeMode = false; }; @@ -64,6 +66,14 @@ bool IsBareModifierKey(UINT code) void ApplyDeferredKeyState(DeferredShadowState &shadow, const _KEYSTROKE_STATE &keyState, WCHAR wch = 0) { + const auto clearComposition = [&shadow]() { + shadow.inputLength = 0; + shadow.rawInput.clear(); + shadow.caret = 0; + shadow.candidateActive = false; + shadow.unicodeMode = false; + }; + switch (keyState.Function) { case FUNCTION_INPUT: @@ -71,23 +81,38 @@ void ApplyDeferredKeyState(DeferredShadowState &shadow, { shadow.unicodeMode = (wch == L'U'); } - if (shadow.inputLength < MAX_PINYIN_LENGTH) + shadow.caret = min(shadow.caret, shadow.rawInput.size()); + if (shadow.rawInput.size() < MAX_PINYIN_LENGTH && wch != L'\0') { - ++shadow.inputLength; + const bool duplicateSeparator = + wch == L'\'' && + ((shadow.caret > 0 && shadow.rawInput[shadow.caret - 1] == L'\'') || + (shadow.caret < shadow.rawInput.size() && shadow.rawInput[shadow.caret] == L'\'')); + if (!duplicateSeparator) + { + shadow.rawInput.insert(shadow.caret, 1, wch); + ++shadow.caret; + } } + shadow.inputLength = shadow.rawInput.size(); shadow.candidateActive = false; break; case FUNCTION_FINALIZE_TEXTSTORE_AND_INPUT: case FUNCTION_FINALIZE_CANDIDATELIST_AND_INPUT: - shadow.inputLength = 1; + shadow.rawInput.assign(wch == L'\0' ? 0 : 1, wch); + shadow.caret = shadow.rawInput.size(); + shadow.inputLength = shadow.rawInput.size(); shadow.candidateActive = false; shadow.unicodeMode = (wch == L'U'); break; case FUNCTION_BACKSPACE: - if (shadow.inputLength > 0) + shadow.caret = min(shadow.caret, shadow.rawInput.size()); + if (shadow.caret > 0) { - --shadow.inputLength; + shadow.rawInput.erase(shadow.caret - 1, 1); + --shadow.caret; } + shadow.inputLength = shadow.rawInput.size(); if (shadow.inputLength == 0) { shadow.candidateActive = false; @@ -100,14 +125,22 @@ void ApplyDeferredKeyState(DeferredShadowState &shadow, case FUNCTION_CONVERT: // This TIP routes Space+Convert to WM_AsyncFinalizeCandidate, which // commits and ends the composition rather than merely opening a list. - shadow.inputLength = 0; - shadow.candidateActive = false; - shadow.unicodeMode = false; + clearComposition(); break; case FUNCTION_CANCEL: - shadow.inputLength = 0; - shadow.candidateActive = false; - shadow.unicodeMode = false; + clearComposition(); + break; + case FUNCTION_MOVE_LEFT: + if (shadow.caret > 0) + { + --shadow.caret; + } + break; + case FUNCTION_MOVE_RIGHT: + if (shadow.caret < shadow.rawInput.size()) + { + ++shadow.caret; + } break; case FUNCTION_FINALIZE_TEXTSTORE: case FUNCTION_FINALIZE_CANDIDATELIST: @@ -116,9 +149,7 @@ void ApplyDeferredKeyState(DeferredShadowState &shadow, case FUNCTION_TOGGLE_IME_MODE: case FUNCTION_PUNCTUATION: case FUNCTION_DOUBLE_SINGLE_BYTE: - shadow.inputLength = 0; - shadow.candidateActive = false; - shadow.unicodeMode = false; + clearComposition(); break; default: break; @@ -912,6 +943,18 @@ void CMetasequoiaIME::_EnsureDeferredKeyProjection() static_cast( _pCompositionProcessorEngine->GetVirtualKeyLength())) : 0; + _deferredProjectedRawInput.clear(); + _deferredProjectedCaret = 0; + if (_pCompositionProcessorEngine) + { + const CStringRange &buffer = _pCompositionProcessorEngine->GetKeystrokeBuffer(); + if (buffer.Get() && buffer.GetLength() > 0) + { + _deferredProjectedRawInput.assign(buffer.Get(), buffer.GetLength()); + } + _deferredProjectedCaret = min(static_cast(_pCompositionProcessorEngine->GetCaretPosition()), + _deferredProjectedRawInput.size()); + } // Incremental candidates are still the ordinary composing path: another // letter extends the same raw input. Only an explicit/original candidate // list makes the next input a finalize-and-start-new boundary. @@ -930,10 +973,14 @@ void CMetasequoiaIME::_ApplyDeferredKeyProjection( shadow.punctuationOpen = _deferredProjectedPunctuationOpen; shadow.doubleSingleByteOpen = _deferredProjectedDoubleSingleByteOpen; shadow.inputLength = _deferredProjectedInputLength; + shadow.rawInput = _deferredProjectedRawInput; + shadow.caret = _deferredProjectedCaret; shadow.candidateActive = _deferredProjectedCandidateActive; shadow.unicodeMode = _deferredProjectedUnicodeMode; ApplyDeferredKeyState(shadow, keyState, wch); _deferredProjectedInputLength = shadow.inputLength; + _deferredProjectedRawInput = std::move(shadow.rawInput); + _deferredProjectedCaret = shadow.caret; _deferredProjectedCandidateActive = shadow.candidateActive; _deferredProjectedUnicodeMode = shadow.unicodeMode; } @@ -952,6 +999,8 @@ void CMetasequoiaIME::_ApplyDeferredPreservedKeyProjection( _deferredProjectedImeOpen = !_deferredProjectedImeOpen; _deferredProjectedPunctuationOpen = _deferredProjectedImeOpen; _deferredProjectedInputLength = 0; + _deferredProjectedRawInput.clear(); + _deferredProjectedCaret = 0; _deferredProjectedCandidateActive = false; _deferredProjectedUnicodeMode = false; break; @@ -1129,6 +1178,8 @@ bool CMetasequoiaIME::_ClassifyDeferredKeyDown( shadow.doubleSingleByteOpen = _deferredProjectedDoubleSingleByteOpen; shadow.inputLength = _deferredProjectedInputLength; + shadow.rawInput = _deferredProjectedRawInput; + shadow.caret = _deferredProjectedCaret; shadow.candidateActive = _deferredProjectedCandidateActive; shadow.unicodeMode = _deferredProjectedUnicodeMode; } @@ -1145,6 +1196,13 @@ bool CMetasequoiaIME::_ClassifyDeferredKeyDown( static_cast(MAX_PINYIN_LENGTH), static_cast( _pCompositionProcessorEngine->GetVirtualKeyLength())); + const CStringRange &buffer = _pCompositionProcessorEngine->GetKeystrokeBuffer(); + if (buffer.Get() && buffer.GetLength() > 0) + { + shadow.rawInput.assign(buffer.Get(), buffer.GetLength()); + } + shadow.caret = min(static_cast(_pCompositionProcessorEngine->GetCaretPosition()), + shadow.rawInput.size()); shadow.candidateActive = _candidateMode == CANDIDATE_ORIGINAL; shadow.unicodeMode = _pCompositionProcessorEngine->IsUnicodeModeComposition() != FALSE; @@ -1172,6 +1230,14 @@ bool CMetasequoiaIME::_ClassifyDeferredKeyDown( { isInputKey = true; } + if (!isInputKey && Global::MicrosoftShuangpinEnabled.load(std::memory_order_relaxed) && + *classifiedCode == VK_OEM_1 && *classifiedWch == L';' && !shadow.rawInput.empty()) + { + const size_t caret = min(shadow.caret, shadow.rawInput.size()); + const size_t separator = caret == 0 ? std::wstring::npos : shadow.rawInput.rfind(L'\'', caret - 1); + const size_t chunkStart = separator == std::wstring::npos ? 0 : separator + 1; + isInputKey = (caret - chunkStart) % 2 == 1; + } if (shadow.inputLength == 0 && (GetKeyState(VK_CAPITAL) & 0x0001) != 0 && *classifiedWch >= L'A' && *classifiedWch <= L'Z' && @@ -1478,6 +1544,8 @@ void CMetasequoiaIME::_ClearDeferredKeyDowns() _deferredProjectedPunctuationOpen = false; _deferredProjectedDoubleSingleByteOpen = false; _deferredProjectedInputLength = 0; + _deferredProjectedRawInput.clear(); + _deferredProjectedCaret = 0; _deferredProjectedCandidateActive = false; _deferredProjectedUnicodeMode = false; _shiftHotkeyArmed = false; @@ -1502,6 +1570,8 @@ void CMetasequoiaIME::_CompleteDeferredKeyReplay(uint64_t replayToken) // to rebuild the composition. _deferredKeyProjectionValid = false; _deferredProjectedInputLength = 0; + _deferredProjectedRawInput.clear(); + _deferredProjectedCaret = 0; _deferredProjectedCandidateActive = false; _deferredProjectedUnicodeMode = false; (void)_RefreshDeferredRecoveryPrefix(context);