From 08386814653fc962dce7f82cd243d36e01cdae0b Mon Sep 17 00:00:00 2001 From: fanlusky <1730976608@qq.com> Date: Fri, 11 Sep 2026 01:34:00 +0800 Subject: [PATCH] fix(windows): restore bare-Shift CN/EN toggle in Word 70930df stopped eating the bare-Shift release so JetBrains double-Shift keeps working, and moved the toggle into OnTestKeyUp because TSF does not call OnKeyUp after a FALSE test result. Word never completes a bare-Shift release through ITfKeyEventSink at all, so neither callback runs and the CN/EN toggle was lost there. The mintty workaround already solves exactly this shape: a thread-local WH_KEYBOARD hook watches the real bare-Shift press/release and feeds it back into the deferred hotkey path, with _MarkBareShiftHandled() latching presses the key-event sink already toggled so hosts that do deliver the release never toggle twice. Install that hook for every host instead of only mintty.exe, and rename its members off the mintty-specific naming. Also log the three bare-Shift release paths under [issue47] -- key-up was previously uninstrumented, which is why the Word diagnostic log could not distinguish "release never arrived" from "toggle queued but dropped". --- windows/src/IME/MetasequoiaIME.cpp | 26 +++---- windows/src/IME/MetasequoiaIME.h | 41 ++++++----- windows/src/Key/KeyEventSink.cpp | 106 ++++++++++++++++------------- 3 files changed, 96 insertions(+), 77 deletions(-) diff --git a/windows/src/IME/MetasequoiaIME.cpp b/windows/src/IME/MetasequoiaIME.cpp index e1ac9ac4..3c6947f2 100644 --- a/windows/src/IME/MetasequoiaIME.cpp +++ b/windows/src/IME/MetasequoiaIME.cpp @@ -459,16 +459,16 @@ CMetasequoiaIME::CMetasequoiaIME() _shiftHotkeyArmed = false; _ctrlHotkeyArmed = false; _modifierHotkeyExpire = {}; - _minttyKeyboardHook = nullptr; - _minttyShiftDownMask = 0; - _minttyShiftArmed = false; - _minttyShiftSequence = 0; - _minttyShiftHandledSequence = 0; - _minttyShiftFocusGeneration = 0; - _minttyShiftExpireTick = 0; + _bareShiftHook = nullptr; + _bareShiftDownMask = 0; + _bareShiftArmed = false; + _bareShiftSequence = 0; + _bareShiftHandledSequence = 0; + _bareShiftFocusGeneration = 0; + _bareShiftExpireTick = 0; } -thread_local CMetasequoiaIME *CMetasequoiaIME::_minttyKeyboardHookOwner = nullptr; +thread_local CMetasequoiaIME *CMetasequoiaIME::_bareShiftHookOwner = nullptr; //+--------------------------------------------------------------------------- // @@ -478,7 +478,7 @@ thread_local CMetasequoiaIME *CMetasequoiaIME::_minttyKeyboardHookOwner = nullpt CMetasequoiaIME::~CMetasequoiaIME() { - _UninitMinttyKeyboardHook(); + _UninitBareShiftKeyboardHook(); _ClearDeferredKeyDowns(); // Deactivate normally owns the unbind. Keep this owner-aware fallback for // partial activation failures without allowing a delayed old service @@ -1423,7 +1423,7 @@ STDAPI CMetasequoiaIME::ActivateEx(ITfThreadMgr *pThreadMgr, TfClientId tfClient // Apply configured default CN/EN whenever switching back to this IME _pCompositionProcessorEngine->InitializeMetasequoiaIMECompartment(pThreadMgr, tfClientId); - _InitMinttyKeyboardHook(); + _InitBareShiftKeyboardHook(); // The first connect timer can run before the engine exists. Re-arm after // compartment initialization so an already-focused activation always @@ -1452,7 +1452,7 @@ ExitError: STDAPI CMetasequoiaIME::Deactivate() { - _UninitMinttyKeyboardHook(); + _UninitBareShiftKeyboardHook(); Global::HostUiLessMode = false; Global::CandidateUiLessMode = false; // Send this synchronously before destroying the message window. OnKill can @@ -2158,8 +2158,8 @@ LRESULT CALLBACK CMetasequoiaIME_WindowProc(HWND hWnd, UINT message, WPARAM wPar switch (message) { - case WM_MinttyShiftRelease: - pIME->_HandleMinttyShiftRelease(static_cast(wParam)); + case WM_BareShiftRelease: + pIME->_HandleHookedBareShiftRelease(static_cast(wParam)); return 0; case WM_CheckGlobalCompartment: { diff --git a/windows/src/IME/MetasequoiaIME.h b/windows/src/IME/MetasequoiaIME.h index 7b42f78e..0147c1a6 100644 --- a/windows/src/IME/MetasequoiaIME.h +++ b/windows/src/IME/MetasequoiaIME.h @@ -39,7 +39,7 @@ 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; -const DWORD WM_MinttyShiftRelease = WM_USER + 23; +const DWORD WM_BareShiftRelease = WM_USER + 23; const DWORD WM_UpdateVoiceComposition = WM_USER + 24; const DWORD WM_CommitVoiceComposition = WM_USER + 25; const DWORD WM_CancelVoiceComposition = WM_USER + 26; @@ -375,16 +375,21 @@ class CMetasequoiaIME : public ITfTextInputProcessorEx, bool _MatchModifierReleaseHotkey(WPARAM wParam, _Out_ GUID *hotkeyGuid); bool _QueueInputHotkey(_In_ ITfContext *pContext, REFGUID hotkeyGuid, _Out_ BOOL *pIsEaten); - // mintty exposes IME composition through the legacy IMM bridge, but some - // versions do not forward bare modifier key-up events to ITfKeyEventSink. - // Observe only this host thread and feed a missed bare-Shift release back - // into the normal deferred hotkey path. - void _InitMinttyKeyboardHook(); - void _UninitMinttyKeyboardHook(); - void _HandleMinttyShiftRelease(UINT sequence); - void _MarkMinttyShiftHandled(); - static LRESULT CALLBACK _MinttyKeyboardHookProc(int code, WPARAM wParam, LPARAM lParam); - static thread_local CMetasequoiaIME *_minttyKeyboardHookOwner; + // Not every host completes a bare-Shift release through ITfKeyEventSink: + // mintty routes composition over the legacy IMM bridge and drops bare + // modifier key-ups, and Word does not deliver them either. Since the + // release must be reported uneaten (JetBrains double-Shift depends on + // seeing it), OnTestKeyUp cannot rely on a follow-up OnKeyUp. Observe only + // this host thread and feed a missed bare-Shift release back into the + // normal deferred hotkey path. _MarkBareShiftHandled() latches the presses + // the key-event sink already toggled, so hosts that do deliver the release + // never toggle twice. + void _InitBareShiftKeyboardHook(); + void _UninitBareShiftKeyboardHook(); + void _HandleHookedBareShiftRelease(UINT sequence); + void _MarkBareShiftHandled(); + static LRESULT CALLBACK _BareShiftKeyboardHookProc(int code, WPARAM wParam, LPARAM lParam); + static thread_local CMetasequoiaIME *_bareShiftHookOwner; void _StartComposition(_In_ ITfContext *pContext); HRESULT _EndComposition(_In_opt_ ITfContext *pContext, _In_opt_ ITfComposition *expectedComposition = nullptr, @@ -592,13 +597,13 @@ class CMetasequoiaIME : public ITfTextInputProcessorEx, bool _ctrlHotkeyArmed; std::chrono::steady_clock::time_point _modifierHotkeyExpire; - HHOOK _minttyKeyboardHook; - BYTE _minttyShiftDownMask; - bool _minttyShiftArmed; - UINT _minttyShiftSequence; - UINT _minttyShiftHandledSequence; - uint64_t _minttyShiftFocusGeneration; - ULONGLONG _minttyShiftExpireTick; + HHOOK _bareShiftHook; + BYTE _bareShiftDownMask; + bool _bareShiftArmed; + UINT _bareShiftSequence; + UINT _bareShiftHandledSequence; + uint64_t _bareShiftFocusGeneration; + ULONGLONG _bareShiftExpireTick; LONG _refCount; diff --git a/windows/src/Key/KeyEventSink.cpp b/windows/src/Key/KeyEventSink.cpp index 508993d4..de054366 100644 --- a/windows/src/Key/KeyEventSink.cpp +++ b/windows/src/Key/KeyEventSink.cpp @@ -284,44 +284,43 @@ void ClearReleasedShiftModifierState() } } // namespace -void CMetasequoiaIME::_InitMinttyKeyboardHook() +void CMetasequoiaIME::_InitBareShiftKeyboardHook() { - if (_minttyKeyboardHook != nullptr || _minttyKeyboardHookOwner != nullptr || - CompareStringOrdinal(Global::current_process_name.c_str(), -1, L"mintty.exe", -1, TRUE) != CSTR_EQUAL) + if (_bareShiftHook != nullptr || _bareShiftHookOwner != nullptr) { return; } - _minttyKeyboardHookOwner = this; - _minttyKeyboardHook = SetWindowsHookExW(WH_KEYBOARD, _MinttyKeyboardHookProc, nullptr, GetCurrentThreadId()); - if (_minttyKeyboardHook == nullptr) + _bareShiftHookOwner = this; + _bareShiftHook = SetWindowsHookExW(WH_KEYBOARD, _BareShiftKeyboardHookProc, nullptr, GetCurrentThreadId()); + if (_bareShiftHook == nullptr) { - _minttyKeyboardHookOwner = nullptr; + _bareShiftHookOwner = nullptr; } } -void CMetasequoiaIME::_UninitMinttyKeyboardHook() +void CMetasequoiaIME::_UninitBareShiftKeyboardHook() { - HHOOK hook = _minttyKeyboardHook; - _minttyKeyboardHook = nullptr; - if (_minttyKeyboardHookOwner == this) + HHOOK hook = _bareShiftHook; + _bareShiftHook = nullptr; + if (_bareShiftHookOwner == this) { - _minttyKeyboardHookOwner = nullptr; + _bareShiftHookOwner = nullptr; } if (hook != nullptr) { UnhookWindowsHookEx(hook); } - _minttyShiftDownMask = 0; - _minttyShiftArmed = false; - _minttyShiftFocusGeneration = 0; - _minttyShiftExpireTick = 0; + _bareShiftDownMask = 0; + _bareShiftArmed = false; + _bareShiftFocusGeneration = 0; + _bareShiftExpireTick = 0; } -LRESULT CALLBACK CMetasequoiaIME::_MinttyKeyboardHookProc(int code, WPARAM wParam, LPARAM lParam) +LRESULT CALLBACK CMetasequoiaIME::_BareShiftKeyboardHookProc(int code, WPARAM wParam, LPARAM lParam) { - CMetasequoiaIME *owner = _minttyKeyboardHookOwner; + CMetasequoiaIME *owner = _bareShiftHookOwner; if (code == HC_ACTION && owner != nullptr && static_cast(GetMessageExtraInfo()) != SMART_PUNCTUATION_SENDINPUT_EXTRA_INFO) { @@ -338,57 +337,56 @@ LRESULT CALLBACK CMetasequoiaIME::_MinttyKeyboardHookProc(int code, WPARAM wPara { if (!wasDown) { - if (owner->_minttyShiftDownMask == 0) + if (owner->_bareShiftDownMask == 0) { - owner->_minttyShiftArmed = !IsOtherKeyboardKeyDown(); - ++owner->_minttyShiftSequence; - if (owner->_minttyShiftSequence == 0) + owner->_bareShiftArmed = !IsOtherKeyboardKeyDown(); + ++owner->_bareShiftSequence; + if (owner->_bareShiftSequence == 0) { - ++owner->_minttyShiftSequence; + ++owner->_bareShiftSequence; } - owner->_minttyShiftFocusGeneration = owner->_deferredKeyFocusGeneration; - owner->_minttyShiftExpireTick = + owner->_bareShiftFocusGeneration = owner->_deferredKeyFocusGeneration; + owner->_bareShiftExpireTick = GetTickCount64() + static_cast(kModifierHotkeyToggleLimit.count()); } - owner->_minttyShiftDownMask |= shiftBit; + owner->_bareShiftDownMask |= shiftBit; } } else { - owner->_minttyShiftDownMask &= static_cast(~shiftBit); - if (owner->_minttyShiftDownMask == 0) + owner->_bareShiftDownMask &= static_cast(~shiftBit); + if (owner->_bareShiftDownMask == 0) { - const bool shouldPost = - owner->_minttyShiftArmed && GetTickCount64() < owner->_minttyShiftExpireTick; - owner->_minttyShiftArmed = false; + const bool shouldPost = owner->_bareShiftArmed && GetTickCount64() < owner->_bareShiftExpireTick; + owner->_bareShiftArmed = false; if (shouldPost && owner->_msgWndHandle != nullptr) { - PostMessage(owner->_msgWndHandle, WM_MinttyShiftRelease, owner->_minttyShiftSequence, 0); + PostMessage(owner->_msgWndHandle, WM_BareShiftRelease, owner->_bareShiftSequence, 0); } } } } else if (!isKeyUp) { - owner->_minttyShiftArmed = false; + owner->_bareShiftArmed = false; } } - return CallNextHookEx(owner ? owner->_minttyKeyboardHook : nullptr, code, wParam, lParam); + return CallNextHookEx(owner ? owner->_bareShiftHook : nullptr, code, wParam, lParam); } -void CMetasequoiaIME::_MarkMinttyShiftHandled() +void CMetasequoiaIME::_MarkBareShiftHandled() { - if (_minttyKeyboardHook != nullptr) + if (_bareShiftHook != nullptr) { - _minttyShiftHandledSequence = _minttyShiftSequence; + _bareShiftHandledSequence = _bareShiftSequence; } } -void CMetasequoiaIME::_HandleMinttyShiftRelease(UINT sequence) +void CMetasequoiaIME::_HandleHookedBareShiftRelease(UINT sequence) { - if (_minttyKeyboardHook == nullptr || sequence == 0 || sequence != _minttyShiftSequence || - sequence == _minttyShiftHandledSequence || _minttyShiftFocusGeneration != _deferredKeyFocusGeneration || + if (_bareShiftHook == nullptr || sequence == 0 || sequence != _bareShiftSequence || + sequence == _bareShiftHandledSequence || _bareShiftFocusGeneration != _deferredKeyFocusGeneration || !Global::g_connected || _pThreadMgr == nullptr || _pCompositionProcessorEngine == nullptr || (GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0 || !FanyUtils::ReadConfiguredSwitchLanguageHotkeys().shift) { @@ -415,9 +413,13 @@ void CMetasequoiaIME::_HandleMinttyShiftRelease(UINT sequence) } BOOL eaten = FALSE; - if (_QueueInputHotkey(context, Global::MetasequoiaIMEGuidImeModePreserveKey, &eaten)) + const bool queued = _QueueInputHotkey(context, Global::MetasequoiaIMEGuidImeModePreserveKey, &eaten); + DebugTsfIssue47(L"bare-shift-hook-release", FANY_IME_NO_REQUEST_ID, VK_SHIFT, L'\0', 0, 0, queued ? 1 : 0, + _IsComposing(), + _pCompositionProcessorEngine ? _pCompositionProcessorEngine->GetVirtualKeyLength() : 0, S_OK); + if (queued) { - _minttyShiftHandledSequence = sequence; + _bareShiftHandledSequence = sequence; _shiftHotkeyArmed = false; _ctrlHotkeyArmed = false; Global::IsShiftKeyDownOnly = FALSE; @@ -2313,12 +2315,19 @@ STDAPI CMetasequoiaIME::OnTestKeyUp(ITfContext *pContext, WPARAM wParam, LPARAM { // TSF does not call OnKeyUp after a FALSE test result. Claim and // queue the bare-Shift toggle here while leaving its release visible. + // Hosts that never route the release here (Word, mintty) fall back to + // the bare-Shift keyboard hook, which _MarkBareShiftHandled() disarms. GUID hotkeyGuid = {}; BOOL queued = FALSE; - if (_MatchModifierReleaseHotkey(wParam, &hotkeyGuid) && _QueueInputHotkey(pContext, hotkeyGuid, &queued)) + const bool toggled = + _MatchModifierReleaseHotkey(wParam, &hotkeyGuid) && _QueueInputHotkey(pContext, hotkeyGuid, &queued); + if (toggled) { - _MarkMinttyShiftHandled(); + _MarkBareShiftHandled(); } + DebugTsfIssue47(L"bare-shift-testkeyup", FANY_IME_NO_REQUEST_ID, LOWORD(wParam), L'\0', 0, 0, toggled ? 1 : 0, + _IsComposing(), + _pCompositionProcessorEngine ? _pCompositionProcessorEngine->GetVirtualKeyLength() : 0, S_OK); ClearReleasedShiftModifierState(); *pIsEaten = FALSE; return S_OK; @@ -2384,10 +2393,15 @@ STDAPI CMetasequoiaIME::OnKeyUp(ITfContext *pContext, WPARAM wParam, LPARAM lPar // Defend against hosts that offer KeyUp without a preceding test. GUID hotkeyGuid = {}; BOOL queued = FALSE; - if (_MatchModifierReleaseHotkey(wParam, &hotkeyGuid) && _QueueInputHotkey(pContext, hotkeyGuid, &queued)) + const bool toggled = + _MatchModifierReleaseHotkey(wParam, &hotkeyGuid) && _QueueInputHotkey(pContext, hotkeyGuid, &queued); + if (toggled) { - _MarkMinttyShiftHandled(); + _MarkBareShiftHandled(); } + DebugTsfIssue47(L"bare-shift-keyup", FANY_IME_NO_REQUEST_ID, LOWORD(wParam), L'\0', 0, 0, toggled ? 1 : 0, + _IsComposing(), + _pCompositionProcessorEngine ? _pCompositionProcessorEngine->GetVirtualKeyLength() : 0, S_OK); ClearReleasedShiftModifierState(); *pIsEaten = FALSE; return S_OK;