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;