diff --git a/src/Composition/Composition.cpp b/src/Composition/Composition.cpp index 7ae55232..3bfcd65b 100644 --- a/src/Composition/Composition.cpp +++ b/src/Composition/Composition.cpp @@ -142,13 +142,64 @@ WCHAR CMetasequoiaIME::_GetPrecedingCharForSmartPunctuation(TfEditCookie ec, _In return _GetPrecedingDocumentChar(ec, pContext); } -void CMetasequoiaIME::_ResetSmartPunctuationHistory() -{ +void CMetasequoiaIME::_ResetSmartPunctuationHistory() +{ _smartPunctuationKey = 0; _smartPunctuationPrecedingChar = 0; - _smartPunctuationCommittedAscii = false; - _smartPunctuationAsciiRejected = false; -} + _smartPunctuationCommittedAscii = false; + _smartPunctuationAsciiRejected = false; + _smartPunctuationCommitTick = 0; + _smartPunctuationFocusToken = 0; + _smartPunctuationForegroundWindow = nullptr; +} + +bool CMetasequoiaIME::_QueueRepeatedSmartPunctuationReplacement(WCHAR wch) +{ + if (!_smartPunctuationCommittedAscii || _smartPunctuationKey != wch || + _smartPunctuationCommitTick == 0 || _msgWndHandle == nullptr || + _pCompositionProcessorEngine == nullptr || _IsComposing() || + _candidateMode != CANDIDATE_NONE || + !Global::SmartPunctuationEnabled.load(std::memory_order_relaxed) || + !Global::SmartPunctuationRepeatToChineseEnabled.load(std::memory_order_relaxed)) + { + return false; + } + + const ULONGLONG now = GetTickCount64(); + if (now - _smartPunctuationCommitTick > SMART_PUNCTUATION_REPEAT_INTERVAL_MS || + !_IsFocusSessionCurrent(_smartPunctuationFocusToken) || + GetForegroundWindow() != _smartPunctuationForegroundWindow) + { + return false; + } + + const WCHAR *chinese = _pCompositionProcessorEngine->GetPunctuation(wch); + if (chinese == nullptr || chinese[0] == L'\0' || chinese[1] != L'\0') + { + return false; + } + + _pendingSmartPunctuationReplacement = chinese[0]; + _pendingSmartPunctuationFocusToken = _smartPunctuationFocusToken; + _pendingSmartPunctuationForegroundWindow = _smartPunctuationForegroundWindow; + _pendingSmartPunctuationDeadline = + _smartPunctuationCommitTick + SMART_PUNCTUATION_REPEAT_INTERVAL_MS; + + const uint64_t focusToken = _pendingSmartPunctuationFocusToken; + if (!PostMessage(_msgWndHandle, WM_ReplaceRepeatedSmartPunctuation, + static_cast(focusToken & 0xFFFFFFFFULL), + static_cast((focusToken >> 32) & 0xFFFFFFFFULL))) + { + _pendingSmartPunctuationReplacement = 0; + _pendingSmartPunctuationFocusToken = 0; + _pendingSmartPunctuationForegroundWindow = nullptr; + _pendingSmartPunctuationDeadline = 0; + return false; + } + + _ResetSmartPunctuationHistory(); + return true; +} void CMetasequoiaIME::_InvalidateSmartPunctuationShadow() { @@ -211,9 +262,20 @@ void CMetasequoiaIME::_UpdateSmartPunctuationShadow(UINT code, WCHAR wch, bool i _smartPunctuationShadowValid = true; } -void CMetasequoiaIME::_NoteKeyForSmartPunctuation(UINT code, WCHAR wch, bool isEaten) -{ - _UpdateSmartPunctuationShadow(code, wch, isEaten); +void CMetasequoiaIME::_NoteKeyForSmartPunctuation(UINT code, WCHAR wch, bool isEaten) +{ + // The replacement message normally runs before another input event. If it + // does not, never let a later key leave the queued Backspace targeting an + // unrelated character. + if (_pendingSmartPunctuationReplacement != 0) + { + _pendingSmartPunctuationReplacement = 0; + _pendingSmartPunctuationFocusToken = 0; + _pendingSmartPunctuationForegroundWindow = nullptr; + _pendingSmartPunctuationDeadline = 0; + } + + _UpdateSmartPunctuationShadow(code, wch, isEaten); if (_smartPunctuationKey == 0) { @@ -296,8 +358,20 @@ std::wstring CMetasequoiaIME::_ResolveSmartPunctuation(WCHAR wch, WCHAR precedin _smartPunctuationKey = wch; _smartPunctuationPrecedingChar = precedingChar; - _smartPunctuationCommittedAscii = committedAscii; - _smartPunctuationAsciiRejected = asciiRejected; + _smartPunctuationCommittedAscii = committedAscii; + _smartPunctuationAsciiRejected = asciiRejected; + if (committedAscii) + { + _smartPunctuationCommitTick = GetTickCount64(); + _smartPunctuationFocusToken = _CaptureFocusSessionToken(); + _smartPunctuationForegroundWindow = GetForegroundWindow(); + } + else + { + _smartPunctuationCommitTick = 0; + _smartPunctuationFocusToken = 0; + _smartPunctuationForegroundWindow = nullptr; + } if (!resolved.empty()) { _smartPunctuationShadowChar = resolved.back(); diff --git a/src/IME/MetasequoiaIME.cpp b/src/IME/MetasequoiaIME.cpp index 51d6baf9..5fa67d2a 100644 --- a/src/IME/MetasequoiaIME.cpp +++ b/src/IME/MetasequoiaIME.cpp @@ -1671,6 +1671,8 @@ void CMetasequoiaIME::IpcWorkerThread(CMetasequoiaIME *pIME) } } if (validFrame && (buf.msg_type == Global::DataToTsfWorkerThreadMsgType::SmartPunctuationChanged || + buf.msg_type == + Global::DataToTsfWorkerThreadMsgType::SmartPunctuationRepeatToChineseChanged || buf.msg_type == Global::DataToTsfWorkerThreadMsgType::PairedPunctuationChanged || buf.msg_type == Global::DataToTsfWorkerThreadMsgType::MicrosoftShuangpinChanged)) { @@ -1720,6 +1722,7 @@ void CMetasequoiaIME::IpcWorkerThread(CMetasequoiaIME *pIME) // Soft config/control frames: drop without killing the worker pipe. if (buf.msg_type == Global::DataToTsfWorkerThreadMsgType::PagingCommaPeriodChanged || buf.msg_type == Global::DataToTsfWorkerThreadMsgType::SmartPunctuationChanged || + buf.msg_type == Global::DataToTsfWorkerThreadMsgType::SmartPunctuationRepeatToChineseChanged || buf.msg_type == Global::DataToTsfWorkerThreadMsgType::PairedPunctuationChanged || buf.msg_type == Global::DataToTsfWorkerThreadMsgType::MicrosoftShuangpinChanged || buf.msg_type == Global::DataToTsfWorkerThreadMsgType::PipeReady || @@ -1777,6 +1780,10 @@ void CMetasequoiaIME::IpcWorkerThread(CMetasequoiaIME *pIME) { Global::SmartPunctuationEnabled.store(buf.data[0] == L'1', std::memory_order_relaxed); } + else if (buf.msg_type == Global::DataToTsfWorkerThreadMsgType::SmartPunctuationRepeatToChineseChanged) + { + Global::SmartPunctuationRepeatToChineseEnabled.store(buf.data[0] == L'1', std::memory_order_relaxed); + } else if (buf.msg_type == Global::DataToTsfWorkerThreadMsgType::PairedPunctuationChanged) { Global::PairedPunctuationEnabled.store(buf.data[0] == L'1', std::memory_order_relaxed); @@ -2506,6 +2513,52 @@ LRESULT CALLBACK CMetasequoiaIME_WindowProc(HWND hWnd, UINT message, WPARAM wPar SendInput(ARRAYSIZE(inputs), inputs, sizeof(INPUT)); break; } + case WM_ReplaceRepeatedSmartPunctuation: { + const uint64_t focusToken = static_cast(static_cast(wParam)) | + (static_cast(static_cast(lParam)) << 32); + const WCHAR replacement = pIME->_pendingSmartPunctuationReplacement; + const bool requestCurrent = + replacement != 0 && focusToken != 0 && + focusToken == pIME->_pendingSmartPunctuationFocusToken && + Global::SmartPunctuationRepeatToChineseEnabled.load(std::memory_order_relaxed) && + pIME->_IsFocusSessionCurrent(focusToken) && + GetForegroundWindow() == pIME->_pendingSmartPunctuationForegroundWindow && + GetTickCount64() <= pIME->_pendingSmartPunctuationDeadline; + + pIME->_pendingSmartPunctuationReplacement = 0; + pIME->_pendingSmartPunctuationFocusToken = 0; + pIME->_pendingSmartPunctuationForegroundWindow = nullptr; + pIME->_pendingSmartPunctuationDeadline = 0; + if (!requestCurrent) + { + break; + } + + INPUT inputs[4] = {}; + inputs[0].type = INPUT_KEYBOARD; + inputs[0].ki.wVk = VK_BACK; + inputs[0].ki.dwExtraInfo = SMART_PUNCTUATION_SENDINPUT_EXTRA_INFO; + inputs[1] = inputs[0]; + inputs[1].ki.dwFlags = KEYEVENTF_KEYUP; + + inputs[2].type = INPUT_KEYBOARD; + inputs[2].ki.wScan = replacement; + inputs[2].ki.dwFlags = KEYEVENTF_UNICODE; + inputs[2].ki.dwExtraInfo = SMART_PUNCTUATION_SENDINPUT_EXTRA_INFO; + inputs[3] = inputs[2]; + inputs[3].ki.dwFlags |= KEYEVENTF_KEYUP; + + if (SendInput(ARRAYSIZE(inputs), inputs, sizeof(INPUT)) == ARRAYSIZE(inputs)) + { + pIME->_smartPunctuationShadowChar = replacement; + pIME->_smartPunctuationShadowValid = true; + } + else + { + pIME->_InvalidateSmartPunctuationShadow(); + } + break; + } case WM_SETTINGCHANGE: { // Registry may not be flushed yet when the broadcast arrives. if (lParam && _wcsicmp(reinterpret_cast(lParam), L"ImmersiveColorSet") == 0) diff --git a/src/IME/MetasequoiaIME.h b/src/IME/MetasequoiaIME.h index 0f38874b..0d2c8e04 100644 --- a/src/IME/MetasequoiaIME.h +++ b/src/IME/MetasequoiaIME.h @@ -38,6 +38,9 @@ const DWORD WM_DrainDeferredKeyDown = WM_USER + 18; const DWORD WM_InsertText = WM_USER + 19; const DWORD WM_RefreshLanguageBarTheme = WM_USER + 20; const DWORD WM_PairedPunctuationMoveLeft = WM_USER + 21; +const DWORD WM_ReplaceRepeatedSmartPunctuation = WM_USER + 22; +constexpr ULONG_PTR SMART_PUNCTUATION_SENDINPUT_EXTRA_INFO = 0x4D535050u; +constexpr ULONGLONG SMART_PUNCTUATION_REPEAT_INTERVAL_MS = 2000; constexpr UINT_PTR TIMER_CONNECT_ALL_NAMEDPIPE = 1; constexpr UINT_PTR TIMER_CONNECT_TO_TSF_NAMEDPIPE = 2; constexpr UINT_PTR TIMER_REFRESH_LANG_BAR_THEME = 3; @@ -165,10 +168,11 @@ class CMetasequoiaIME : public ITfTextInputProcessorEx, // Shadow first, document read as the fallback. See _smartPunctuationShadowChar. WCHAR _GetPrecedingCharForSmartPunctuation(TfEditCookie ec, _In_ ITfContext *pContext); - // Smart punctuation: backspacing the ASCII punctuation we just committed - // means that form was unwanted, so the spot stays on Chinese punctuation. - std::wstring _ResolveSmartPunctuation(WCHAR wch, WCHAR precedingChar); - void _NoteKeyForSmartPunctuation(UINT code, WCHAR wch, bool isEaten); + // Smart punctuation: backspacing the ASCII punctuation we just committed + // means that form was unwanted, so the spot stays on Chinese punctuation. + std::wstring _ResolveSmartPunctuation(WCHAR wch, WCHAR precedingChar); + bool _QueueRepeatedSmartPunctuationReplacement(WCHAR wch); + void _NoteKeyForSmartPunctuation(UINT code, WCHAR wch, bool isEaten); void _ResetSmartPunctuationHistory(); void _UpdateSmartPunctuationShadow(UINT code, WCHAR wch, bool isEaten); void _InvalidateSmartPunctuationShadow(); @@ -491,9 +495,16 @@ class CMetasequoiaIME : public ITfTextInputProcessorEx, // Last smart-punctuation commit, used to detect a backspace rejection. WCHAR _smartPunctuationKey = 0; - WCHAR _smartPunctuationPrecedingChar = 0; - bool _smartPunctuationCommittedAscii = false; - bool _smartPunctuationAsciiRejected = false; + WCHAR _smartPunctuationPrecedingChar = 0; + bool _smartPunctuationCommittedAscii = false; + bool _smartPunctuationAsciiRejected = false; + ULONGLONG _smartPunctuationCommitTick = 0; + uint64_t _smartPunctuationFocusToken = 0; + HWND _smartPunctuationForegroundWindow = nullptr; + WCHAR _pendingSmartPunctuationReplacement = 0; + uint64_t _pendingSmartPunctuationFocusToken = 0; + HWND _pendingSmartPunctuationForegroundWindow = nullptr; + ULONGLONG _pendingSmartPunctuationDeadline = 0; // Last character known to have reached the application. Hosts such as the // VS Code terminal back the context with a proxy text store that only ever diff --git a/src/IPC/Ipc.h b/src/IPC/Ipc.h index 9e428583..3cd68fc5 100644 --- a/src/IPC/Ipc.h +++ b/src/IPC/Ipc.h @@ -322,15 +322,19 @@ constexpr UINT SmartPunctuationChanged = 11; constexpr UINT PairedPunctuationChanged = 12; // Whether ';' is an input key for the Microsoft shuangpin profile. constexpr UINT MicrosoftShuangpinChanged = 13; +// Replace a repeated smart ASCII punctuation with its Chinese mapping. +constexpr UINT SmartPunctuationRepeatToChineseChanged = 14; // Highest opcode this build understands. Unknown higher opcodes must be // ignored by the worker reader (never tear down the pipe). -constexpr UINT MaxKnown = MicrosoftShuangpinChanged; +constexpr UINT MaxKnown = SmartPunctuationRepeatToChineseChanged; } // namespace DataToTsfWorkerThreadMsgType inline std::atomic_bool PagingCommaPeriodEnabled{false}; // Default on: matches rime-ice digit_separators behavior until Server syncs. inline std::atomic_bool SmartPunctuationEnabled{true}; // Default on until the Server sends the persisted setting. +inline std::atomic_bool SmartPunctuationRepeatToChineseEnabled{true}; +// Default on until the Server sends the persisted setting. inline std::atomic_bool PairedPunctuationEnabled{true}; inline std::atomic_bool MicrosoftShuangpinEnabled{false}; inline thread_local bool g_connected = false; diff --git a/src/Key/KeyEventSink.cpp b/src/Key/KeyEventSink.cpp index 7e3f3af2..dc4bb528 100644 --- a/src/Key/KeyEventSink.cpp +++ b/src/Key/KeyEventSink.cpp @@ -1128,6 +1128,11 @@ STDAPI CMetasequoiaIME::OnTestKeyDown(ITfContext *pContext, WPARAM wParam, LPARA { return E_INVALIDARG; } + if (static_cast(GetMessageExtraInfo()) == SMART_PUNCTUATION_SENDINPUT_EXTRA_INFO) + { + *pIsEaten = FALSE; + return S_OK; + } PerfTimer onTestKeyDownTimer; Global::UpdateModifiers(wParam, lParam); _TrackModifierHotkeyArming(wParam, lParam, false); @@ -1649,6 +1654,11 @@ STDAPI CMetasequoiaIME::OnKeyDown(ITfContext *pContext, WPARAM wParam, LPARAM lP { return E_INVALIDARG; } + if (static_cast(GetMessageExtraInfo()) == SMART_PUNCTUATION_SENDINPUT_EXTRA_INFO) + { + *pIsEaten = FALSE; + return S_OK; + } PerfTimer onKeyDownTimer; const uint64_t focusGeneration = _deferredKeyFocusGeneration; (void)_DispatchKeyDown(pContext, wParam, lParam, pIsEaten, nullptr, nullptr, @@ -2016,6 +2026,11 @@ STDAPI CMetasequoiaIME::OnTestKeyUp(ITfContext *pContext, WPARAM wParam, LPARAM { return E_INVALIDARG; } + if (static_cast(GetMessageExtraInfo()) == SMART_PUNCTUATION_SENDINPUT_EXTRA_INFO) + { + *pIsEaten = FALSE; + return S_OK; + } Global::UpdateModifiers(wParam, lParam); @@ -2059,6 +2074,11 @@ STDAPI CMetasequoiaIME::OnKeyUp(ITfContext *pContext, WPARAM wParam, LPARAM lPar { return E_INVALIDARG; } + if (static_cast(GetMessageExtraInfo()) == SMART_PUNCTUATION_SENDINPUT_EXTRA_INFO) + { + *pIsEaten = FALSE; + return S_OK; + } Global::UpdateModifiers(wParam, lParam); if (_HasDeferredKeyBarrier()) diff --git a/src/Key/KeyHandler.cpp b/src/Key/KeyHandler.cpp index 822ca284..42f5c2fa 100644 --- a/src/Key/KeyHandler.cpp +++ b/src/Key/KeyHandler.cpp @@ -988,6 +988,12 @@ HRESULT CMetasequoiaIME::_HandleCompositionPunctuation(TfEditCookie ec, _In_ ITf { HRESULT hr = S_OK; PerfTimer timer; + + if (_QueueRepeatedSmartPunctuationReplacement(wch)) + { + return S_OK; + } + // // Get punctuation char from composition processor engine //