From bc6b0ea773add827565e8cac5f86037f437bb7b6 Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Thu, 12 Mar 2026 20:20:42 +1000 Subject: [PATCH 01/12] fix(windows): change caps on updateable This change also adds a check in aiTIP.cpp to check the thread data stored last key press and scan code. Before if the scan code had been cleared then we would process the synthasized caps lock key presses which was just for the system and not for the current Keyman app. --- windows/src/engine/keyman32/appint/aiTIP.cpp | 7 +++- .../src/engine/keyman32/kmhook_getmessage.cpp | 1 + .../src/engine/keyman32/kmprocessactions.cpp | 36 +++++++++++++------ 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/windows/src/engine/keyman32/appint/aiTIP.cpp b/windows/src/engine/keyman32/appint/aiTIP.cpp index f56bd98620..1332b13e57 100644 --- a/windows/src/engine/keyman32/appint/aiTIP.cpp +++ b/windows/src/engine/keyman32/appint/aiTIP.cpp @@ -138,7 +138,12 @@ BOOL TIPProcessKeyInternal( SendDebugMessageFormat("Scan code was zero so using cached scan code %x", scan); } - if(scan == SCAN_FLAG_KEYMAN_KEY_EVENT) { // I4370 + + // We need to check the last key is the wparam or VK_PROCESSKEY (as that is what keyup comes for synthesised keys on the KeyUP). + // Just the lastscan code is not sufficent becasue we have the case where Shift press appears + // here before the low level keyboard hook. + if (((_td->LastKey == wParam || _td->LastKey == VK_PROCESSKEY) && _td->LastScanCode == SCAN_FLAG_KEYMAN_KEY_EVENT) + || scan == SCAN_FLAG_KEYMAN_KEY_EVENT) { if (wParam == VK_CAPITAL && !isUp) { // Must also record toggle state change when Keyman has generated // a Caps Lock event diff --git a/windows/src/engine/keyman32/kmhook_getmessage.cpp b/windows/src/engine/keyman32/kmhook_getmessage.cpp index 4fee835254..22eecd46ef 100644 --- a/windows/src/engine/keyman32/kmhook_getmessage.cpp +++ b/windows/src/engine/keyman32/kmhook_getmessage.cpp @@ -174,6 +174,7 @@ LRESULT _kmnGetMessageProc(int nCode, WPARAM wParam, LPARAM lParam) if (mp->wParam != VK_BACK) { if(scan == SCAN_FLAG_KEYMAN_KEY_EVENT) { mp->lParam = (mp->lParam & 0xFF00FFFFL) | (MapVirtualKey((UINT)mp->wParam, 0) << 16); + SendDebugMessageFormat("wm_keyman clear Keyman scan flag new_wParam=%x new_lParam=%x LastKey=%x LastScanCode=%x", mp->wParam, mp->lParam, _td->LastKey, _td->LastScanCode); } } } diff --git a/windows/src/engine/keyman32/kmprocessactions.cpp b/windows/src/engine/keyman32/kmprocessactions.cpp index bdc8c69e66..d72a7c75c8 100644 --- a/windows/src/engine/keyman32/kmprocessactions.cpp +++ b/windows/src/engine/keyman32/kmprocessactions.cpp @@ -79,16 +79,27 @@ processPersistOpt(km_core_actions const* actions, LPINTKEYBOARDINFO activeKeyboa static void processCapsLock(const km_core_caps_state caps_lock_state, BOOL isUp, BOOL Updateable, BOOL externalEvent) { + /// For Debuging + // TODO: 15594 - remove this debug message and associated code after testing + SendDebugMessageFormat("ACTION CAPS STATE:%d FIsUp=%d Updateable=%d ExternalEvent=%d CapsState=%d", + caps_lock_state, isUp, Updateable, externalEvent, IsCapsLockOn()); + + /// // We only want to process the Caps Lock key event once -- - // in the first pass (!Updateable). - if (Updateable){ + // it has to be updateble as TSF does not have updateable=0 events. + if (!Updateable){ return; } if (caps_lock_state == KM_CORE_CAPS_ON) { // This case would occur for the keyboard system store setting `store(&CapsOnOnly) '1'` + if (!isUp && !IsCapsLockOn()) { // I267 - 24/11/2006 invert GetKeyState test + SendDebugMessageFormat("TURN CAPS ON key down: FIsUp=%d CapsState=%d", isUp, IsCapsLockOn()); + keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, 0, 0); + keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, KEYEVENTF_KEYUP, 0); + } if (isUp && !IsCapsLockOn()) { // I267 - 24/11/2006 invert GetKeyState test - SendDebugMessageFormat("TURN CAPS ON: FIsUp=%d CapsState=%d", isUp, IsCapsLockOn()); + SendDebugMessageFormat("TURN CAPS ON key up: FIsUp=%d CapsState=%d", isUp, IsCapsLockOn()); keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, 0, 0); keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, KEYEVENTF_KEYUP, 0); } @@ -96,16 +107,21 @@ static void processCapsLock(const km_core_caps_state caps_lock_state, BOOL isUp, // This case would occur for the keyboard system store setting `store(&CapsAlwaysOff) '1'` // A trick is being played here of synthesising a release the CAPSLOCK key event // then a depress CAPSLOCK key event - else if (!isUp && IsCapsLockOn()) { // I267 - 24/11/2006 invert GetKeyState test - SendDebugMessageFormat("TURN CAPS OFF: FIsUp=%d CapsState=%d", isUp, IsCapsLockOn()); - keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, KEYEVENTF_KEYUP, 0); - keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, 0, 0); - } + //else if (!isUp && IsCapsLockOn()) { // I267 - 24/11/2006 invert GetKeyState test + // SendDebugMessageFormat("TURN CAPS OFF: FIsUp=%d CapsState=%d", isUp, IsCapsLockOn()); + // keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, KEYEVENTF_KEYUP, 0); + // keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, 0, 0); + //} } else if (caps_lock_state == KM_CORE_CAPS_OFF) { // This case would occur for the keyboard system store setting `store(&ShiftFreesCaps) '1'` // OR selecting a keyboard with CAPs always off rule if ((!isUp && IsCapsLockOn()) || (externalEvent && IsCapsLockOn())) { - SendDebugMessageFormat("TURN CAPS OFF: FIsUp=%d CapsState=%d", isUp, IsCapsLockOn()); + SendDebugMessageFormat("TURN CAPS OFF key down: FIsUp=%d CapsState=%d", isUp, IsCapsLockOn()); + keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, 0, 0); + keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, KEYEVENTF_KEYUP, 0); + } + if ((isUp && IsCapsLockOn()) || (externalEvent && IsCapsLockOn())) { + SendDebugMessageFormat("TURN CAPS OFF key up: FIsUp=%d CapsState=%d", isUp, IsCapsLockOn()); keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, 0, 0); keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, KEYEVENTF_KEYUP, 0); } @@ -151,7 +167,7 @@ ProcessActionsNonUpdatableParse(BOOL* emitKeystroke) { km_core_actions const* core_actions = km_core_state_get_actions(_td->lpActiveKeyboard->lpCoreKeyboardState); - processCapsLock(core_actions->new_caps_lock_state, !_td->state.isDown, _td->TIPFUpdateable, FALSE); + if (core_actions->emit_keystroke) { *emitKeystroke = TRUE; SendDebugMessageFormat("EMIT_KEYSTROKE"); From 95a7c6008bfdebaaef081a7d197e79b2cc155820 Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:16:48 +1000 Subject: [PATCH 02/12] fix(windows): simplify the processCapsLock if else also update external processsCaps Lock call to handle the Updateable change --- .../src/engine/keyman32/kmprocessactions.cpp | 54 +++++-------------- 1 file changed, 14 insertions(+), 40 deletions(-) diff --git a/windows/src/engine/keyman32/kmprocessactions.cpp b/windows/src/engine/keyman32/kmprocessactions.cpp index d72a7c75c8..d76b16fe6a 100644 --- a/windows/src/engine/keyman32/kmprocessactions.cpp +++ b/windows/src/engine/keyman32/kmprocessactions.cpp @@ -77,54 +77,28 @@ processPersistOpt(km_core_actions const* actions, LPINTKEYBOARDINFO activeKeyboa } } -static void processCapsLock(const km_core_caps_state caps_lock_state, BOOL isUp, BOOL Updateable, BOOL externalEvent) { - +static void processCapsLock(const km_core_caps_state caps_state_change, BOOL isUp, BOOL Updateable, BOOL externalEvent) { + // Turn three state value into a boolean for whether caps lock should be on or off, we only want to process the key event if the + // state is changing + BOOL required_caps_state = (caps_state_change == KM_CORE_CAPS_ON); + BOOL isCapsOn = IsCapsLockOn(); /// For Debuging // TODO: 15594 - remove this debug message and associated code after testing - SendDebugMessageFormat("ACTION CAPS STATE:%d FIsUp=%d Updateable=%d ExternalEvent=%d CapsState=%d", - caps_lock_state, isUp, Updateable, externalEvent, IsCapsLockOn()); + SendDebugMessageFormat("ACTION CAPS STATE:%d FIsUp=%d Updateable=%d ExternalEvent=%d CapsState=%d", caps_state_change, isUp, Updateable, + externalEvent, isCapsOn); - /// // We only want to process the Caps Lock key event once -- // it has to be updateble as TSF does not have updateable=0 events. if (!Updateable){ return; } - - if (caps_lock_state == KM_CORE_CAPS_ON) { - // This case would occur for the keyboard system store setting `store(&CapsOnOnly) '1'` - if (!isUp && !IsCapsLockOn()) { // I267 - 24/11/2006 invert GetKeyState test - SendDebugMessageFormat("TURN CAPS ON key down: FIsUp=%d CapsState=%d", isUp, IsCapsLockOn()); - keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, 0, 0); - keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, KEYEVENTF_KEYUP, 0); - } - if (isUp && !IsCapsLockOn()) { // I267 - 24/11/2006 invert GetKeyState test - SendDebugMessageFormat("TURN CAPS ON key up: FIsUp=%d CapsState=%d", isUp, IsCapsLockOn()); - keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, 0, 0); - keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, KEYEVENTF_KEYUP, 0); - } - - // This case would occur for the keyboard system store setting `store(&CapsAlwaysOff) '1'` - // A trick is being played here of synthesising a release the CAPSLOCK key event - // then a depress CAPSLOCK key event - //else if (!isUp && IsCapsLockOn()) { // I267 - 24/11/2006 invert GetKeyState test - // SendDebugMessageFormat("TURN CAPS OFF: FIsUp=%d CapsState=%d", isUp, IsCapsLockOn()); - // keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, KEYEVENTF_KEYUP, 0); - // keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, 0, 0); - //} - } else if (caps_lock_state == KM_CORE_CAPS_OFF) { - // This case would occur for the keyboard system store setting `store(&ShiftFreesCaps) '1'` - // OR selecting a keyboard with CAPs always off rule - if ((!isUp && IsCapsLockOn()) || (externalEvent && IsCapsLockOn())) { - SendDebugMessageFormat("TURN CAPS OFF key down: FIsUp=%d CapsState=%d", isUp, IsCapsLockOn()); - keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, 0, 0); - keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, KEYEVENTF_KEYUP, 0); - } - if ((isUp && IsCapsLockOn()) || (externalEvent && IsCapsLockOn())) { - SendDebugMessageFormat("TURN CAPS OFF key up: FIsUp=%d CapsState=%d", isUp, IsCapsLockOn()); - keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, 0, 0); - keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, KEYEVENTF_KEYUP, 0); - } + + if (isCapsOn != required_caps_state) { + SendDebugMessageFormat( + "Simulate CAPS %s: FIsUp=%d CurrentCapsState=%d ExternalEvent=%d", + required_caps_state ? "ON" : "OFF", isUp, isCapsOn, externalEvent); + keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, 0, 0); + keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, KEYEVENTF_KEYUP, 0); } } From 6277735dbbdd6609a2c898adbd86379c927e75ef Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Mon, 16 Mar 2026 12:02:26 +1000 Subject: [PATCH 03/12] fix(windows): fix typos and grammar in commments --- windows/src/engine/keyman32/appint/aiTIP.cpp | 7 ++++--- windows/src/engine/keyman32/kmhook_getmessage.cpp | 3 ++- windows/src/engine/keyman32/kmprocessactions.cpp | 13 +++++++------ 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/windows/src/engine/keyman32/appint/aiTIP.cpp b/windows/src/engine/keyman32/appint/aiTIP.cpp index 1332b13e57..438fbcc48c 100644 --- a/windows/src/engine/keyman32/appint/aiTIP.cpp +++ b/windows/src/engine/keyman32/appint/aiTIP.cpp @@ -139,9 +139,10 @@ BOOL TIPProcessKeyInternal( } - // We need to check the last key is the wparam or VK_PROCESSKEY (as that is what keyup comes for synthesised keys on the KeyUP). - // Just the lastscan code is not sufficent becasue we have the case where Shift press appears - // here before the low level keyboard hook. + // We need to check if the last key is the same as `wparam` or `VK_PROCESSKEY` + // (VK_PROCESSKEY is what synthesised keys on the KeyUP have). + // Just the lastscan code is not sufficient because we have the case where + // Shift press is processed here before the low level keyboard hook. if (((_td->LastKey == wParam || _td->LastKey == VK_PROCESSKEY) && _td->LastScanCode == SCAN_FLAG_KEYMAN_KEY_EVENT) || scan == SCAN_FLAG_KEYMAN_KEY_EVENT) { if (wParam == VK_CAPITAL && !isUp) { diff --git a/windows/src/engine/keyman32/kmhook_getmessage.cpp b/windows/src/engine/keyman32/kmhook_getmessage.cpp index 22eecd46ef..f13e3fa6a1 100644 --- a/windows/src/engine/keyman32/kmhook_getmessage.cpp +++ b/windows/src/engine/keyman32/kmhook_getmessage.cpp @@ -174,7 +174,8 @@ LRESULT _kmnGetMessageProc(int nCode, WPARAM wParam, LPARAM lParam) if (mp->wParam != VK_BACK) { if(scan == SCAN_FLAG_KEYMAN_KEY_EVENT) { mp->lParam = (mp->lParam & 0xFF00FFFFL) | (MapVirtualKey((UINT)mp->wParam, 0) << 16); - SendDebugMessageFormat("wm_keyman clear Keyman scan flag new_wParam=%x new_lParam=%x LastKey=%x LastScanCode=%x", mp->wParam, mp->lParam, _td->LastKey, _td->LastScanCode); + SendDebugMessageFormat("wm_keyman clear `SCAN_FLAG_KEYMAN_KEY_EVENT` new_wParam=%x new_lParam=%x LastKey=%x LastScanCode=%x", + mp->wParam, mp->lParam, _td->LastKey, _td->LastScanCode); } } } diff --git a/windows/src/engine/keyman32/kmprocessactions.cpp b/windows/src/engine/keyman32/kmprocessactions.cpp index d76b16fe6a..e389b820e0 100644 --- a/windows/src/engine/keyman32/kmprocessactions.cpp +++ b/windows/src/engine/keyman32/kmprocessactions.cpp @@ -78,8 +78,8 @@ processPersistOpt(km_core_actions const* actions, LPINTKEYBOARDINFO activeKeyboa } static void processCapsLock(const km_core_caps_state caps_state_change, BOOL isUp, BOOL Updateable, BOOL externalEvent) { - // Turn three state value into a boolean for whether caps lock should be on or off, we only want to process the key event if the - // state is changing + // Turn three state value into a boolean for whether caps lock should be on or off, + // we only want to process the key event if the state is changing. BOOL required_caps_state = (caps_state_change == KM_CORE_CAPS_ON); BOOL isCapsOn = IsCapsLockOn(); /// For Debuging @@ -87,12 +87,13 @@ static void processCapsLock(const km_core_caps_state caps_state_change, BOOL isU SendDebugMessageFormat("ACTION CAPS STATE:%d FIsUp=%d Updateable=%d ExternalEvent=%d CapsState=%d", caps_state_change, isUp, Updateable, externalEvent, isCapsOn); - // We only want to process the Caps Lock key event once -- - // it has to be updateble as TSF does not have updateable=0 events. + // We only want to process the Caps Lock key event once; + // it has to be when updateble=1 as TSF does not consistently + // have updateable=0 events. if (!Updateable){ return; } - + if (isCapsOn != required_caps_state) { SendDebugMessageFormat( "Simulate CAPS %s: FIsUp=%d CurrentCapsState=%d ExternalEvent=%d", @@ -157,6 +158,6 @@ ProcessActionsExternalEvent() { return FALSE; } km_core_actions const* core_actions = km_core_state_get_actions(_td->lpActiveKeyboard->lpCoreKeyboardState); - processCapsLock(core_actions->new_caps_lock_state, !_td->state.isDown, FALSE, TRUE); + processCapsLock(core_actions->new_caps_lock_state, !_td->state.isDown, TRUE, TRUE); return TRUE; } From f4c5d9df027b711e4a78e4b6dafaf232739d6acb Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:00:12 +1000 Subject: [PATCH 04/12] fix(windows): fix logic error with caps_changed_state Return early if caps lock state is unchanged. --- windows/src/engine/keyman32/kmprocessactions.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/windows/src/engine/keyman32/kmprocessactions.cpp b/windows/src/engine/keyman32/kmprocessactions.cpp index e389b820e0..93b6cb609a 100644 --- a/windows/src/engine/keyman32/kmprocessactions.cpp +++ b/windows/src/engine/keyman32/kmprocessactions.cpp @@ -78,21 +78,21 @@ processPersistOpt(km_core_actions const* actions, LPINTKEYBOARDINFO activeKeyboa } static void processCapsLock(const km_core_caps_state caps_state_change, BOOL isUp, BOOL Updateable, BOOL externalEvent) { - // Turn three state value into a boolean for whether caps lock should be on or off, - // we only want to process the key event if the state is changing. - BOOL required_caps_state = (caps_state_change == KM_CORE_CAPS_ON); BOOL isCapsOn = IsCapsLockOn(); /// For Debuging // TODO: 15594 - remove this debug message and associated code after testing SendDebugMessageFormat("ACTION CAPS STATE:%d FIsUp=%d Updateable=%d ExternalEvent=%d CapsState=%d", caps_state_change, isUp, Updateable, externalEvent, isCapsOn); - // We only want to process the Caps Lock key event once; // it has to be when updateble=1 as TSF does not consistently // have updateable=0 events. - if (!Updateable){ + if (!Updateable || caps_state_change == KM_CORE_CAPS_UNCHANGED) + { return; } + // Turn three state value into a boolean for whether caps lock should be on or off, + // we only want to process the key event if the state is changing. + BOOL required_caps_state = (caps_state_change == KM_CORE_CAPS_ON); if (isCapsOn != required_caps_state) { SendDebugMessageFormat( From c1c3952717206b232e47996e42262b61820c0c90 Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Fri, 20 Mar 2026 09:22:35 +1000 Subject: [PATCH 05/12] fix(windows): reduce the number of places km scan flag The SCAN_FLAG_KEYMAN_KEY_EVENT was checked in OnKey pressess as well as an early return option in _KeymanProcessKeystroke. This change removes the extra and allows _KeymanProcessKeystroke, to be the central place for code readablitiy and maintainablity. It also adds a processToggleChange call in kmhook_getmessage as it is sometimes called before the TIP hook. the call is also idempotent --- windows/src/engine/keyman32/appint/aiTIP.cpp | 5 +- .../src/engine/keyman32/kmhook_getmessage.cpp | 7 ++- windows/src/engine/kmtip/keys.cpp | 51 +++++++------------ windows/src/engine/kmtip/kmkey.cpp | 6 +-- 4 files changed, 28 insertions(+), 41 deletions(-) diff --git a/windows/src/engine/keyman32/appint/aiTIP.cpp b/windows/src/engine/keyman32/appint/aiTIP.cpp index 438fbcc48c..d184d18350 100644 --- a/windows/src/engine/keyman32/appint/aiTIP.cpp +++ b/windows/src/engine/keyman32/appint/aiTIP.cpp @@ -142,7 +142,7 @@ BOOL TIPProcessKeyInternal( // We need to check if the last key is the same as `wparam` or `VK_PROCESSKEY` // (VK_PROCESSKEY is what synthesised keys on the KeyUP have). // Just the lastscan code is not sufficient because we have the case where - // Shift press is processed here before the low level keyboard hook. + // Shift press is processed here before the kmhook_getmessage hook. if (((_td->LastKey == wParam || _td->LastKey == VK_PROCESSKEY) && _td->LastScanCode == SCAN_FLAG_KEYMAN_KEY_EVENT) || scan == SCAN_FLAG_KEYMAN_KEY_EVENT) { if (wParam == VK_CAPITAL && !isUp) { @@ -150,7 +150,8 @@ BOOL TIPProcessKeyInternal( // a Caps Lock event ProcessToggleChange((UINT)wParam); // I4793 } - SendDebugMessageFormat("Virtual Key was generated by Keyman [Scan=0xFF]"); + // TODO: should we clear the scan code flag even though we are now finished? + SendDebugMessageFormat("Virtual Key was generated by Keyman [Scan=%x wParam=%x] [LastKey=%x LastScanCode=%x]"); return_SendDebugExit(FALSE); } diff --git a/windows/src/engine/keyman32/kmhook_getmessage.cpp b/windows/src/engine/keyman32/kmhook_getmessage.cpp index f13e3fa6a1..7903b789f9 100644 --- a/windows/src/engine/keyman32/kmhook_getmessage.cpp +++ b/windows/src/engine/keyman32/kmhook_getmessage.cpp @@ -174,8 +174,11 @@ LRESULT _kmnGetMessageProc(int nCode, WPARAM wParam, LPARAM lParam) if (mp->wParam != VK_BACK) { if(scan == SCAN_FLAG_KEYMAN_KEY_EVENT) { mp->lParam = (mp->lParam & 0xFF00FFFFL) | (MapVirtualKey((UINT)mp->wParam, 0) << 16); - SendDebugMessageFormat("wm_keyman clear `SCAN_FLAG_KEYMAN_KEY_EVENT` new_wParam=%x new_lParam=%x LastKey=%x LastScanCode=%x", - mp->wParam, mp->lParam, _td->LastKey, _td->LastScanCode); + if (mp->wParam == VK_CAPITAL) { + ProcessToggleChange(VK_CAPITAL); + } + SendDebugMessageFormat("WMKEY=%x clear `SCAN_FLAG_KEYMAN_KEY_EVENT` new_wParam=%x new_lParam=%x LastKey=%x LastScanCode=%x", + mp->message, mp->wParam, mp->lParam, _td->LastKey, _td->LastScanCode); } } } diff --git a/windows/src/engine/kmtip/keys.cpp b/windows/src/engine/kmtip/keys.cpp index 50f2ade3b9..86d6470c73 100644 --- a/windows/src/engine/kmtip/keys.cpp +++ b/windows/src/engine/kmtip/keys.cpp @@ -178,17 +178,10 @@ STDAPI CKMTipTextService::OnTestKeyDown(ITfContext *pContext, WPARAM wParam, LPA { SendDebugEntry(); LogKey(0, wParam, lParam); - // If the keystroke is a Keyman-generated key, ignore it - // But we need to pass Caps Lock through, even if we generated it, so we can track Caps Lock state. - // TODO: Fix magic constants - if ((lParam & 0x00FF0000L) == 0xFF0000L && - wParam != VK_CAPITAL) { - *pfEaten = FALSE; - } - else { - *pfEaten = _KeymanProcessKeystroke(pContext, wParam, lParam, FALSE, FALSE); // I3588 -// SendDebugMessageFormat("pfEaten=%s", *pfEaten ? "TRUE" : "FALSE"); - } + *pfEaten = _KeymanProcessKeystroke(pContext, wParam, lParam, FALSE, FALSE); // I3588 + + SendDebugMessageFormat(L"pfEaten=%s, wParam=%x, lParam=%x", *pfEaten ? L"TRUE" : L"FALSE", wParam, lParam); + // SendDebugMessageFormat("pfEaten=%s", *pfEaten ? "TRUE" : "FALSE"); SendDebugExit(); return S_OK; } @@ -205,8 +198,9 @@ STDAPI CKMTipTextService::OnKeyDown(ITfContext *pContext, WPARAM wParam, LPARAM { SendDebugEntry(); LogKey(1, wParam, lParam); - fEatenBuf[wParam] = *pfEaten = _KeymanProcessKeystroke(pContext, wParam, lParam, TRUE, FALSE); // I3588 -// SendDebugMessageFormat("pfEaten=%s", *pfEaten ? "TRUE" : "FALSE"); + fEatenBuf[wParam] = *pfEaten = _KeymanProcessKeystroke(pContext, wParam, lParam, TRUE, FALSE); // I3588 + SendDebugMessageFormat(L"pfEaten=%s wParam=%x lParam=%x", *pfEaten ? L"TRUE" : L"FALSE", wParam, lParam); + // SendDebugMessageFormat("pfEaten=%s", *pfEaten ? "TRUE" : "FALSE"); SendDebugExit(); return S_OK; } @@ -222,17 +216,11 @@ STDAPI CKMTipTextService::OnTestKeyUp(ITfContext *pContext, WPARAM wParam, LPARA { SendDebugEntry(); LogKey(2, wParam, lParam); - // If the keystroke is a Keyman-generated key, ignore it - // But we need to pass Caps Lock through, even if we generated it, so we can track Caps Lock state. - if ((lParam & 0x00FF0000L) == 0xFF0000L && - wParam != VK_CAPITAL) { // I3566 - *pfEaten = FALSE; - } - else { - _KeymanProcessKeystroke(pContext, wParam, lParam, FALSE, FALSE); // I3588 - *pfEaten = fEatenBuf[wParam]; - } -// SendDebugMessageFormat("pfEaten=%s", *pfEaten ? "TRUE" : "FALSE"); + _KeymanProcessKeystroke(pContext, wParam, lParam, FALSE, FALSE); // I3588 + *pfEaten = fEatenBuf[wParam]; + + // SendDebugMessageFormat("pfEaten=%s", *pfEaten ? "TRUE" : "FALSE"); + SendDebugMessageFormat(L"pfEaten=%s wParam=%x lParam=%x", *pfEaten ? L"TRUE" : L"FALSE", wParam, lParam); SendDebugExit(); return S_OK; } @@ -251,16 +239,11 @@ STDAPI CKMTipTextService::OnKeyUp(ITfContext *pContext, WPARAM wParam, LPARAM lP LogKey(3, wParam, lParam); // If the keystroke is a Keyman-generated key, ignore it // But we need to pass Caps Lock through, even if we generated it, so we can track Caps Lock state. - if ((lParam & 0x00FF0000L) == 0xFF0000L && - wParam != VK_CAPITAL) { // I3566 // I3605 - *pfEaten = FALSE; - } - else - { - _KeymanProcessKeystroke(pContext, wParam, lParam, TRUE, FALSE); // I3588 // I3605 - *pfEaten = fEatenBuf[wParam]; - } -// SendDebugMessageFormat("pfEaten=%s", *pfEaten ? "TRUE" : "FALSE"); + + _KeymanProcessKeystroke(pContext, wParam, lParam, TRUE, FALSE); // I3588 // I3605 + *pfEaten = fEatenBuf[wParam]; + + SendDebugMessageFormat(L"pfEaten=%s wParam=%x lParam=%x", *pfEaten ? L"TRUE" : L"FALSE", wParam, lParam); SendDebugExit(); return S_OK; } diff --git a/windows/src/engine/kmtip/kmkey.cpp b/windows/src/engine/kmtip/kmkey.cpp index 609c98e431..7d9326b335 100644 --- a/windows/src/engine/kmtip/kmkey.cpp +++ b/windows/src/engine/kmtip/kmkey.cpp @@ -102,15 +102,15 @@ BOOL CKMTipTextService::_KeymanProcessKeystroke(ITfContext *pContext, WPARAM wPa // Don't process Unicode characters injected or ProcessKey events which are generated by Windows if (wParam == VK_PACKET || wParam == VK_PROCESSKEY) { + SendDebugMessageFormat(L"wParam=%x early return FALSE", wParam); return FALSE; // I3608 // I4201 } SendDebugEntry(); SendDebugMessageFormat(L"%x %x %s %s ex=%x", wParam, lParam, fUpdate ? L"update" : L"", fPreserved ? L"preserved" : L"", GetMessageExtraInfo()); // I4378 - // Don't process keystrokes generated by Keyman (scan code = 0xFF) - // But we need to pass Caps Lock through, even if we generated it, so we can track Caps Lock state. - // TODO: This is done in multiple places, but we probably only need to do it once + // Don't process keystrokes generated by Keyman (scan code = 0xFF = SCAN_FLAG_KEYMAN_KEY_EVENT) + // But we need to pass Caps Lock through to the keyman engine (but not the core), even if we generated it, so we can track Caps Lock state. if ((lParam & 0xFF0000) == 0xFF0000 && wParam != VK_CAPITAL) { return_SendDebugExit(FALSE); From a1c493665e0208e53d43274c99c9df27bb7963a1 Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:16:26 +1000 Subject: [PATCH 06/12] fix(windows): add key transition to better track key press Added a Key transition member to the thread globals. This help so better insure we actually matching the key value that may have been cleared by kmhook_getmessage to the right key. More importantly makes sure we are not matching it to the wrong key press. This was happening when a synthazised capslock key press was followed by and actuall capslock key press. --- windows/src/engine/keyman32/appint/aiTIP.cpp | 8 ++++---- windows/src/engine/keyman32/globals.h | 1 + windows/src/engine/keyman32/keyman64.h | 4 ++++ windows/src/engine/keyman32/kmhook_getmessage.cpp | 6 ++++-- windows/src/engine/kmtip/keys.cpp | 6 +++--- windows/src/engine/kmtip/kmtip.h | 1 + 6 files changed, 17 insertions(+), 9 deletions(-) diff --git a/windows/src/engine/keyman32/appint/aiTIP.cpp b/windows/src/engine/keyman32/appint/aiTIP.cpp index d184d18350..14cd3124dd 100644 --- a/windows/src/engine/keyman32/appint/aiTIP.cpp +++ b/windows/src/engine/keyman32/appint/aiTIP.cpp @@ -128,7 +128,7 @@ BOOL TIPProcessKeyInternal( BOOL isUp = keyFlags & KF_UP ? TRUE : FALSE; BOOL extended = keyFlags & KF_EXTENDED ? TRUE : FALSE; BYTE scan = keyFlags & 0xFF; - + BYTE keyTransition = (BYTE)((HIWORD(lParam) & (KF_UP | KF_REPEAT)) >> 14); SendDebugEntry(); SendDebugMessageFormat("VirtualKey=%s lParam=%x IsUp=%d Extended=%d Updateable=%d Preserved=%d", Debug_VirtualKey((WORD) wParam), lParam, isUp, extended, Updateable, Preserved); @@ -143,15 +143,15 @@ BOOL TIPProcessKeyInternal( // (VK_PROCESSKEY is what synthesised keys on the KeyUP have). // Just the lastscan code is not sufficient because we have the case where // Shift press is processed here before the kmhook_getmessage hook. - if (((_td->LastKey == wParam || _td->LastKey == VK_PROCESSKEY) && _td->LastScanCode == SCAN_FLAG_KEYMAN_KEY_EVENT) + if ((_td->LastKey == wParam && _td->LastScanCode == SCAN_FLAG_KEYMAN_KEY_EVENT && _td->LastTransition == keyTransition) || scan == SCAN_FLAG_KEYMAN_KEY_EVENT) { if (wParam == VK_CAPITAL && !isUp) { // Must also record toggle state change when Keyman has generated // a Caps Lock event ProcessToggleChange((UINT)wParam); // I4793 } - // TODO: should we clear the scan code flag even though we are now finished? - SendDebugMessageFormat("Virtual Key was generated by Keyman [Scan=%x wParam=%x] [LastKey=%x LastScanCode=%x]"); + // TODO: should we clear the scan code flag even though we are now finished? + SendDebugMessageFormat("Virtual Key was generated by Keyman [Scan=%x wParam=%x] [LastKey=%x LastScanCode=%x LastTransition=%x]", scan, wParam, _td->LastKey, _td->LastScanCode, _td->LastTransition); return_SendDebugExit(FALSE); } diff --git a/windows/src/engine/keyman32/globals.h b/windows/src/engine/keyman32/globals.h index 3c71163446..9db8044394 100644 --- a/windows/src/engine/keyman32/globals.h +++ b/windows/src/engine/keyman32/globals.h @@ -239,6 +239,7 @@ typedef struct tagKEYMAN64THREADDATA WPARAM LastKey; // I4642 BYTE LastScanCode; // I4642 + BYTE LastTransition; /* Serialized key events */ diff --git a/windows/src/engine/keyman32/keyman64.h b/windows/src/engine/keyman32/keyman64.h index e46dad1782..49ce6bab33 100644 --- a/windows/src/engine/keyman32/keyman64.h +++ b/windows/src/engine/keyman32/keyman64.h @@ -118,8 +118,12 @@ #define KEYMSG_FLAG_DLGMODE(lParam) (HIWORD(lParam) & KF_DLGMODE ? 1 : 0) #define KEYMSG_FLAG_MENUMODE(lParam) (HIWORD(lParam) & KF_MENUMODE ? 1 : 0) #define KEYMSG_FLAG_ALTDOWN(lParam) (HIWORD(lParam) & KF_ALTDOWN ? 1 : 0) +// Repeat is actually previous "up" value of the key #define KEYMSG_FLAG_REPEAT(lParam) (HIWORD(lParam) & KF_REPEAT ? 1 : 0) #define KEYMSG_FLAG_UP(lParam) (HIWORD(lParam) & KF_UP ? 1 : 0) +// Combine the transition flags into a single value for higher chance of identification +// 0 = key down, 1 = repeat, 2 = keyup +#define KEYMSG_FLAG_TRANSITION(lParam) ((BYTE)((HIWORD(lParam) & (KF_UP | KF_REPEAT)) >> 14)) // TODO: Deprecate overloading of scancodes and use dwExtraInfo instead #define SCAN_FLAG_KEYMAN_KEY_EVENT 0xFF diff --git a/windows/src/engine/keyman32/kmhook_getmessage.cpp b/windows/src/engine/keyman32/kmhook_getmessage.cpp index 7903b789f9..6c0419c5f4 100644 --- a/windows/src/engine/keyman32/kmhook_getmessage.cpp +++ b/windows/src/engine/keyman32/kmhook_getmessage.cpp @@ -156,9 +156,11 @@ LRESULT _kmnGetMessageProc(int nCode, WPARAM wParam, LPARAM lParam) } BYTE scan = KEYMSG_LPARAM_SCAN(mp->lParam); + BYTE keyTransitionEvent = KEYMSG_FLAG_TRANSITION(mp->lParam); CheckScheduledRefresh(); _td->LastScanCode = scan; _td->LastKey = mp->wParam; + _td->LastTransition = keyTransitionEvent; switch (mp->wParam) { case VK_MENU: @@ -177,8 +179,8 @@ LRESULT _kmnGetMessageProc(int nCode, WPARAM wParam, LPARAM lParam) if (mp->wParam == VK_CAPITAL) { ProcessToggleChange(VK_CAPITAL); } - SendDebugMessageFormat("WMKEY=%x clear `SCAN_FLAG_KEYMAN_KEY_EVENT` new_wParam=%x new_lParam=%x LastKey=%x LastScanCode=%x", - mp->message, mp->wParam, mp->lParam, _td->LastKey, _td->LastScanCode); + SendDebugMessageFormat("WMKEY=%x clear `SCAN_FLAG_KEYMAN_KEY_EVENT` new_wParam=%x new_lParam=%x LastKey=%x LastScanCode=%x LastTransition=%x", + mp->message, mp->wParam, mp->lParam, _td->LastKey, _td->LastScanCode, _td->LastTransition); } } } diff --git a/windows/src/engine/kmtip/keys.cpp b/windows/src/engine/kmtip/keys.cpp index 86d6470c73..382c671bc3 100644 --- a/windows/src/engine/kmtip/keys.cpp +++ b/windows/src/engine/kmtip/keys.cpp @@ -76,6 +76,7 @@ BOOL CKMTipTextService::_InitKeystrokeSink() pKeystrokeMgr->Release(); memset(fEatenBuf, 0, sizeof(fEatenBuf)); + memset(fTestEatenBuf, 0, sizeof(fTestEatenBuf)); return_SendDebugExit(_keystrokeSinkInitialized = (hr == S_OK)); } @@ -178,8 +179,7 @@ STDAPI CKMTipTextService::OnTestKeyDown(ITfContext *pContext, WPARAM wParam, LPA { SendDebugEntry(); LogKey(0, wParam, lParam); - *pfEaten = _KeymanProcessKeystroke(pContext, wParam, lParam, FALSE, FALSE); // I3588 - + fTestEatenBuf[wParam] = *pfEaten = _KeymanProcessKeystroke(pContext, wParam, lParam, FALSE, FALSE); // I3588 SendDebugMessageFormat(L"pfEaten=%s, wParam=%x, lParam=%x", *pfEaten ? L"TRUE" : L"FALSE", wParam, lParam); // SendDebugMessageFormat("pfEaten=%s", *pfEaten ? "TRUE" : "FALSE"); SendDebugExit(); @@ -217,7 +217,7 @@ STDAPI CKMTipTextService::OnTestKeyUp(ITfContext *pContext, WPARAM wParam, LPARA SendDebugEntry(); LogKey(2, wParam, lParam); _KeymanProcessKeystroke(pContext, wParam, lParam, FALSE, FALSE); // I3588 - *pfEaten = fEatenBuf[wParam]; + *pfEaten = fTestEatenBuf[wParam]; // SendDebugMessageFormat("pfEaten=%s", *pfEaten ? "TRUE" : "FALSE"); SendDebugMessageFormat(L"pfEaten=%s wParam=%x lParam=%x", *pfEaten ? L"TRUE" : L"FALSE", wParam, lParam); diff --git a/windows/src/engine/kmtip/kmtip.h b/windows/src/engine/kmtip/kmtip.h index b4f307488b..7df21f7428 100644 --- a/windows/src/engine/kmtip/kmtip.h +++ b/windows/src/engine/kmtip/kmtip.h @@ -117,6 +117,7 @@ private: BOOL _keystrokeSinkInitialized; BOOL fEatenBuf[256]; + BOOL fTestEatenBuf[256]; ITfThreadMgr *_pThreadMgr; TfClientId _tfClientId; From 7084d64b7b9f1094a754d2926177c3f971452ce6 Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Thu, 26 Mar 2026 22:44:47 +1000 Subject: [PATCH 07/12] fix(windows): clean up debug logs --- windows/src/engine/keyman32/appint/aiTIP.cpp | 13 ++++++------- windows/src/engine/keyman32/appint/aiTIP.h | 4 +++- windows/src/engine/keyman32/kmhook_getmessage.cpp | 2 +- windows/src/engine/keyman32/kmprocessactions.cpp | 14 +++++++------- windows/src/engine/kmtip/keys.cpp | 8 ++------ windows/src/engine/kmtip/kmkey.cpp | 3 ++- 6 files changed, 21 insertions(+), 23 deletions(-) diff --git a/windows/src/engine/keyman32/appint/aiTIP.cpp b/windows/src/engine/keyman32/appint/aiTIP.cpp index 14cd3124dd..33c6e31043 100644 --- a/windows/src/engine/keyman32/appint/aiTIP.cpp +++ b/windows/src/engine/keyman32/appint/aiTIP.cpp @@ -138,11 +138,10 @@ BOOL TIPProcessKeyInternal( SendDebugMessageFormat("Scan code was zero so using cached scan code %x", scan); } - - // We need to check if the last key is the same as `wparam` or `VK_PROCESSKEY` - // (VK_PROCESSKEY is what synthesised keys on the KeyUP have). - // Just the lastscan code is not sufficient because we have the case where - // Shift press is processed here before the kmhook_getmessage hook. + // If this key event was generated by Keyman, then we should return FALSE without + // processing it in the core processor. If it is a CapsLock key we update our + // Globals::ShiftState. Since this could run before kmhook_getmessage processes the event, + // we also check for the scan code flag that we set when generating a key event from Keyman. if ((_td->LastKey == wParam && _td->LastScanCode == SCAN_FLAG_KEYMAN_KEY_EVENT && _td->LastTransition == keyTransition) || scan == SCAN_FLAG_KEYMAN_KEY_EVENT) { if (wParam == VK_CAPITAL && !isUp) { @@ -150,8 +149,8 @@ BOOL TIPProcessKeyInternal( // a Caps Lock event ProcessToggleChange((UINT)wParam); // I4793 } - // TODO: should we clear the scan code flag even though we are now finished? - SendDebugMessageFormat("Virtual Key was generated by Keyman [Scan=%x wParam=%x] [LastKey=%x LastScanCode=%x LastTransition=%x]", scan, wParam, _td->LastKey, _td->LastScanCode, _td->LastTransition); + SendDebugMessageFormat("Virtual Key was generated by Keyman [Scan=%x wParam=%x] [LastKey=%x LastScanCode=%x LastTransition=%x]", + scan, wParam, _td->LastKey, _td->LastScanCode, _td->LastTransition); return_SendDebugExit(FALSE); } diff --git a/windows/src/engine/keyman32/appint/aiTIP.h b/windows/src/engine/keyman32/appint/aiTIP.h index a5d84f92a8..2e6b105d98 100644 --- a/windows/src/engine/keyman32/appint/aiTIP.h +++ b/windows/src/engine/keyman32/appint/aiTIP.h @@ -80,7 +80,9 @@ public: /** * ProcessToggleChange - * Toggles the state of FLAGS in the Globals::ShiftState bit mask + * Sets or clears the state of FLAGS in the Globals::ShiftState bit mask + * Using the status of the key as determined by GetKeyState, + * ensuring it is consistent with the actual state of the key. * Supports VK_CAPITAL and VK_NUMLOCK * It DOES NOT generate a system event change for these flags * @param key diff --git a/windows/src/engine/keyman32/kmhook_getmessage.cpp b/windows/src/engine/keyman32/kmhook_getmessage.cpp index 6c0419c5f4..fc8540d210 100644 --- a/windows/src/engine/keyman32/kmhook_getmessage.cpp +++ b/windows/src/engine/keyman32/kmhook_getmessage.cpp @@ -179,7 +179,7 @@ LRESULT _kmnGetMessageProc(int nCode, WPARAM wParam, LPARAM lParam) if (mp->wParam == VK_CAPITAL) { ProcessToggleChange(VK_CAPITAL); } - SendDebugMessageFormat("WMKEY=%x clear `SCAN_FLAG_KEYMAN_KEY_EVENT` new_wParam=%x new_lParam=%x LastKey=%x LastScanCode=%x LastTransition=%x", + SendDebugMessageFormat("WMKEY=%x Clear `SCAN_FLAG_KEYMAN_KEY_EVENT` before LastKey=%x LastScanCode=%x LastTransition=%x after wParam=%x lParam=%x", mp->message, mp->wParam, mp->lParam, _td->LastKey, _td->LastScanCode, _td->LastTransition); } } diff --git a/windows/src/engine/keyman32/kmprocessactions.cpp b/windows/src/engine/keyman32/kmprocessactions.cpp index 93b6cb609a..64089c0670 100644 --- a/windows/src/engine/keyman32/kmprocessactions.cpp +++ b/windows/src/engine/keyman32/kmprocessactions.cpp @@ -79,10 +79,11 @@ processPersistOpt(km_core_actions const* actions, LPINTKEYBOARDINFO activeKeyboa static void processCapsLock(const km_core_caps_state caps_state_change, BOOL isUp, BOOL Updateable, BOOL externalEvent) { BOOL isCapsOn = IsCapsLockOn(); - /// For Debuging - // TODO: 15594 - remove this debug message and associated code after testing - SendDebugMessageFormat("ACTION CAPS STATE:%d FIsUp=%d Updateable=%d ExternalEvent=%d CapsState=%d", caps_state_change, isUp, Updateable, - externalEvent, isCapsOn); + + // This debug message is useful for understanding the sequence of events around caps lock changes + //SendDebugMessageFormat("ACTION CAPS STATE:%d FIsUp=%d Updateable=%d ExternalEvent=%d CapsState=%d", caps_state_change, isUp, Updateable, + // externalEvent, isCapsOn); + // We only want to process the Caps Lock key event once; // it has to be when updateble=1 as TSF does not consistently // have updateable=0 events. @@ -90,13 +91,13 @@ static void processCapsLock(const km_core_caps_state caps_state_change, BOOL isU { return; } - // Turn three state value into a boolean for whether caps lock should be on or off, + // Turn three state value into a boolean for whether capslock should be on or off, // we only want to process the key event if the state is changing. BOOL required_caps_state = (caps_state_change == KM_CORE_CAPS_ON); if (isCapsOn != required_caps_state) { SendDebugMessageFormat( - "Simulate CAPS %s: FIsUp=%d CurrentCapsState=%d ExternalEvent=%d", + "Simulate CapsLock %s: FIsUp=%d CurrentCapsState=%d ExternalEvent=%d", required_caps_state ? "ON" : "OFF", isUp, isCapsOn, externalEvent); keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, 0, 0); keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, KEYEVENTF_KEYUP, 0); @@ -142,7 +143,6 @@ ProcessActionsNonUpdatableParse(BOOL* emitKeystroke) { km_core_actions const* core_actions = km_core_state_get_actions(_td->lpActiveKeyboard->lpCoreKeyboardState); - if (core_actions->emit_keystroke) { *emitKeystroke = TRUE; SendDebugMessageFormat("EMIT_KEYSTROKE"); diff --git a/windows/src/engine/kmtip/keys.cpp b/windows/src/engine/kmtip/keys.cpp index 382c671bc3..afddecfef0 100644 --- a/windows/src/engine/kmtip/keys.cpp +++ b/windows/src/engine/kmtip/keys.cpp @@ -75,8 +75,8 @@ BOOL CKMTipTextService::_InitKeystrokeSink() pKeystrokeMgr->Release(); - memset(fEatenBuf, 0, sizeof(fEatenBuf)); - memset(fTestEatenBuf, 0, sizeof(fTestEatenBuf)); + memset(fEatenBuf, 0, sizeof(fEatenBuf)); // OnKeyDown/Up + memset(fTestEatenBuf, 0, sizeof(fTestEatenBuf)); // OnTestKeyDown/Up return_SendDebugExit(_keystrokeSinkInitialized = (hr == S_OK)); } @@ -181,7 +181,6 @@ STDAPI CKMTipTextService::OnTestKeyDown(ITfContext *pContext, WPARAM wParam, LPA LogKey(0, wParam, lParam); fTestEatenBuf[wParam] = *pfEaten = _KeymanProcessKeystroke(pContext, wParam, lParam, FALSE, FALSE); // I3588 SendDebugMessageFormat(L"pfEaten=%s, wParam=%x, lParam=%x", *pfEaten ? L"TRUE" : L"FALSE", wParam, lParam); - // SendDebugMessageFormat("pfEaten=%s", *pfEaten ? "TRUE" : "FALSE"); SendDebugExit(); return S_OK; } @@ -200,7 +199,6 @@ STDAPI CKMTipTextService::OnKeyDown(ITfContext *pContext, WPARAM wParam, LPARAM LogKey(1, wParam, lParam); fEatenBuf[wParam] = *pfEaten = _KeymanProcessKeystroke(pContext, wParam, lParam, TRUE, FALSE); // I3588 SendDebugMessageFormat(L"pfEaten=%s wParam=%x lParam=%x", *pfEaten ? L"TRUE" : L"FALSE", wParam, lParam); - // SendDebugMessageFormat("pfEaten=%s", *pfEaten ? "TRUE" : "FALSE"); SendDebugExit(); return S_OK; } @@ -218,8 +216,6 @@ STDAPI CKMTipTextService::OnTestKeyUp(ITfContext *pContext, WPARAM wParam, LPARA LogKey(2, wParam, lParam); _KeymanProcessKeystroke(pContext, wParam, lParam, FALSE, FALSE); // I3588 *pfEaten = fTestEatenBuf[wParam]; - - // SendDebugMessageFormat("pfEaten=%s", *pfEaten ? "TRUE" : "FALSE"); SendDebugMessageFormat(L"pfEaten=%s wParam=%x lParam=%x", *pfEaten ? L"TRUE" : L"FALSE", wParam, lParam); SendDebugExit(); return S_OK; diff --git a/windows/src/engine/kmtip/kmkey.cpp b/windows/src/engine/kmtip/kmkey.cpp index 7d9326b335..1b767f4946 100644 --- a/windows/src/engine/kmtip/kmkey.cpp +++ b/windows/src/engine/kmtip/kmkey.cpp @@ -110,7 +110,8 @@ BOOL CKMTipTextService::_KeymanProcessKeystroke(ITfContext *pContext, WPARAM wPa SendDebugMessageFormat(L"%x %x %s %s ex=%x", wParam, lParam, fUpdate ? L"update" : L"", fPreserved ? L"preserved" : L"", GetMessageExtraInfo()); // I4378 // Don't process keystrokes generated by Keyman (scan code = 0xFF = SCAN_FLAG_KEYMAN_KEY_EVENT) - // But we need to pass Caps Lock through to the keyman engine (but not the core), even if we generated it, so we can track Caps Lock state. + // But we need to pass Caps Lock through to the keyman engine (but not the core), + // even if we generated it, so we can track Caps Lock state. if ((lParam & 0xFF0000) == 0xFF0000 && wParam != VK_CAPITAL) { return_SendDebugExit(FALSE); From f0cf996976c6325681c6c35a2890b03b2d705cfa Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Mon, 30 Mar 2026 13:43:11 +1000 Subject: [PATCH 08/12] fix(windows): make log code message fix --- windows/src/engine/keyman32/kmhook_getmessage.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/windows/src/engine/keyman32/kmhook_getmessage.cpp b/windows/src/engine/keyman32/kmhook_getmessage.cpp index fc8540d210..da01903963 100644 --- a/windows/src/engine/keyman32/kmhook_getmessage.cpp +++ b/windows/src/engine/keyman32/kmhook_getmessage.cpp @@ -179,8 +179,8 @@ LRESULT _kmnGetMessageProc(int nCode, WPARAM wParam, LPARAM lParam) if (mp->wParam == VK_CAPITAL) { ProcessToggleChange(VK_CAPITAL); } - SendDebugMessageFormat("WMKEY=%x Clear `SCAN_FLAG_KEYMAN_KEY_EVENT` before LastKey=%x LastScanCode=%x LastTransition=%x after wParam=%x lParam=%x", - mp->message, mp->wParam, mp->lParam, _td->LastKey, _td->LastScanCode, _td->LastTransition); + SendDebugMessageFormat("WMKEY=%x Clear `SCAN_FLAG_KEYMAN_KEY_EVENT` wParam=%x lParam=%x Set LastKey=%x LastScanCode=%x LastTransition=%x", + mp->message, mp->wParam, mp->lParam, _td->LastKey, _td->LastScanCode, _td->LastTransition); } } } From d7370c472ef64f566f2f0550778cbbe10e5680c7 Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Tue, 31 Mar 2026 22:13:45 +1000 Subject: [PATCH 09/12] fix(windows): update lastkey in tip processor Update the lastkey, lastscankey and lasttransition in aaitp. This ensures that when the pfEaten is FALSE and the get_message_hook does not recieve the key press the "last" value is correct. --- windows/src/engine/keyman32/appint/aiTIP.cpp | 25 +++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/windows/src/engine/keyman32/appint/aiTIP.cpp b/windows/src/engine/keyman32/appint/aiTIP.cpp index 33c6e31043..497c177c92 100644 --- a/windows/src/engine/keyman32/appint/aiTIP.cpp +++ b/windows/src/engine/keyman32/appint/aiTIP.cpp @@ -114,6 +114,20 @@ void ProcessToggleChange(UINT key) { // I4793 } } +/** + * Update the cache of the last key event received, this is set here - TIP processing, and by + * the GetMessage hook. + * @param _td Theread data to update + * @param wParam WPARAM of the key event + * @param scan Scan code of the key event + * @param keyTransition Transition state of the key event (0 = key down, 1 = repeat, 3 = key up) + */ +void UpdateLastKeyCache(PKEYMAN64THREADDATA _td, WPARAM wParam, BYTE scan, BYTE keyTransition) { + _td->LastKey = wParam; + _td->LastScanCode = scan; + _td->LastTransition = keyTransition; +} + BOOL TIPProcessKeyInternal( PKEYMAN64THREADDATA _td, WPARAM wParam, @@ -129,6 +143,11 @@ BOOL TIPProcessKeyInternal( BOOL extended = keyFlags & KF_EXTENDED ? TRUE : FALSE; BYTE scan = keyFlags & 0xFF; BYTE keyTransition = (BYTE)((HIWORD(lParam) & (KF_UP | KF_REPEAT)) >> 14); + WPARAM prevKey = _td->LastKey; + BYTE prevScanCode = _td->LastScanCode; + BYTE prevKeyTransition = _td->LastTransition; + + UpdateLastKeyCache(_td, wParam, scan, keyTransition); SendDebugEntry(); SendDebugMessageFormat("VirtualKey=%s lParam=%x IsUp=%d Extended=%d Updateable=%d Preserved=%d", Debug_VirtualKey((WORD) wParam), lParam, isUp, extended, Updateable, Preserved); @@ -142,15 +161,15 @@ BOOL TIPProcessKeyInternal( // processing it in the core processor. If it is a CapsLock key we update our // Globals::ShiftState. Since this could run before kmhook_getmessage processes the event, // we also check for the scan code flag that we set when generating a key event from Keyman. - if ((_td->LastKey == wParam && _td->LastScanCode == SCAN_FLAG_KEYMAN_KEY_EVENT && _td->LastTransition == keyTransition) + if ((prevKey == wParam && prevScanCode == SCAN_FLAG_KEYMAN_KEY_EVENT && prevKeyTransition == keyTransition) || scan == SCAN_FLAG_KEYMAN_KEY_EVENT) { if (wParam == VK_CAPITAL && !isUp) { // Must also record toggle state change when Keyman has generated // a Caps Lock event ProcessToggleChange((UINT)wParam); // I4793 } - SendDebugMessageFormat("Virtual Key was generated by Keyman [Scan=%x wParam=%x] [LastKey=%x LastScanCode=%x LastTransition=%x]", - scan, wParam, _td->LastKey, _td->LastScanCode, _td->LastTransition); + SendDebugMessageFormat("Virtual Key was generated by Keyman [Scan=%x wParam=%x] [prevKey=%x prevScanCode=%x prevKeyTransition=%x]", + scan, wParam, prevKey, prevScanCode, prevKeyTransition); return_SendDebugExit(FALSE); } From d2012ed79b336ad10c3690d5af568c31adec640e Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Thu, 2 Apr 2026 13:41:51 +1000 Subject: [PATCH 10/12] fix(windows): provide clarity to log message --- windows/src/engine/keyman32/appint/aiTIP.cpp | 2 +- windows/src/engine/keyman32/kmprocess.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/windows/src/engine/keyman32/appint/aiTIP.cpp b/windows/src/engine/keyman32/appint/aiTIP.cpp index 497c177c92..a64e96010c 100644 --- a/windows/src/engine/keyman32/appint/aiTIP.cpp +++ b/windows/src/engine/keyman32/appint/aiTIP.cpp @@ -152,7 +152,7 @@ BOOL TIPProcessKeyInternal( SendDebugMessageFormat("VirtualKey=%s lParam=%x IsUp=%d Extended=%d Updateable=%d Preserved=%d", Debug_VirtualKey((WORD) wParam), lParam, isUp, extended, Updateable, Preserved); - if(_td->LastKey == wParam && scan == 0) { // I4642 + if (_td->LastKey == wParam && scan == 0) { // I4642 - handle issue with Logos application. scan = _td->LastScanCode; SendDebugMessageFormat("Scan code was zero so using cached scan code %x", scan); } diff --git a/windows/src/engine/keyman32/kmprocess.cpp b/windows/src/engine/keyman32/kmprocess.cpp index 8ba4c22a8b..6e1eff08e3 100644 --- a/windows/src/engine/keyman32/kmprocess.cpp +++ b/windows/src/engine/keyman32/kmprocess.cpp @@ -131,7 +131,7 @@ BOOL ProcessHook() _td->lpActiveKeyboard->lpCoreKeyboardState, KM_CORE_DEBUG_CONTEXT_CACHED ); - SendDebugMessageFormatW(L"Key %s: %hs Context '%s'", + SendDebugMessageFormatW(L"Key %s: %hs Core Cached Context '%s'", _td->state.isDown ? L"pressed" : L"released", Debug_VirtualKey(_td->state.vkey), debug_context); km_core_cu_dispose(debug_context); From c23284ea511e0ab4d028aad65f44bd626f8c1bca Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Thu, 2 Apr 2026 16:19:45 +1000 Subject: [PATCH 11/12] fix(windows): rename fTestEatenBuf to fOnTestEatenBuf --- windows/src/engine/kmtip/keys.cpp | 6 +++--- windows/src/engine/kmtip/kmtip.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/windows/src/engine/kmtip/keys.cpp b/windows/src/engine/kmtip/keys.cpp index afddecfef0..9bf67a5f99 100644 --- a/windows/src/engine/kmtip/keys.cpp +++ b/windows/src/engine/kmtip/keys.cpp @@ -76,7 +76,7 @@ BOOL CKMTipTextService::_InitKeystrokeSink() pKeystrokeMgr->Release(); memset(fEatenBuf, 0, sizeof(fEatenBuf)); // OnKeyDown/Up - memset(fTestEatenBuf, 0, sizeof(fTestEatenBuf)); // OnTestKeyDown/Up + memset(fOnTestEatenBuf, 0, sizeof(fOnTestEatenBuf)); // OnTestKeyDown/Up return_SendDebugExit(_keystrokeSinkInitialized = (hr == S_OK)); } @@ -179,7 +179,7 @@ STDAPI CKMTipTextService::OnTestKeyDown(ITfContext *pContext, WPARAM wParam, LPA { SendDebugEntry(); LogKey(0, wParam, lParam); - fTestEatenBuf[wParam] = *pfEaten = _KeymanProcessKeystroke(pContext, wParam, lParam, FALSE, FALSE); // I3588 + fOnTestEatenBuf[wParam] = *pfEaten = _KeymanProcessKeystroke(pContext, wParam, lParam, FALSE, FALSE); // I3588 SendDebugMessageFormat(L"pfEaten=%s, wParam=%x, lParam=%x", *pfEaten ? L"TRUE" : L"FALSE", wParam, lParam); SendDebugExit(); return S_OK; @@ -215,7 +215,7 @@ STDAPI CKMTipTextService::OnTestKeyUp(ITfContext *pContext, WPARAM wParam, LPARA SendDebugEntry(); LogKey(2, wParam, lParam); _KeymanProcessKeystroke(pContext, wParam, lParam, FALSE, FALSE); // I3588 - *pfEaten = fTestEatenBuf[wParam]; + *pfEaten = fOnTestEatenBuf[wParam]; SendDebugMessageFormat(L"pfEaten=%s wParam=%x lParam=%x", *pfEaten ? L"TRUE" : L"FALSE", wParam, lParam); SendDebugExit(); return S_OK; diff --git a/windows/src/engine/kmtip/kmtip.h b/windows/src/engine/kmtip/kmtip.h index 7df21f7428..a067617d44 100644 --- a/windows/src/engine/kmtip/kmtip.h +++ b/windows/src/engine/kmtip/kmtip.h @@ -117,7 +117,7 @@ private: BOOL _keystrokeSinkInitialized; BOOL fEatenBuf[256]; - BOOL fTestEatenBuf[256]; + BOOL fOnTestEatenBuf[256]; ITfThreadMgr *_pThreadMgr; TfClientId _tfClientId; From 02db51c9b61611d140413ee1c41c91714ea8e571 Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Tue, 28 Apr 2026 23:16:17 +1000 Subject: [PATCH 12/12] fix(windows): address review comments --- windows/src/engine/keyman32/appint/aiTIP.cpp | 12 ++---------- windows/src/engine/keyman32/appint/aiTIP.h | 15 +++++++++++++++ windows/src/engine/keyman32/keyman64.h | 7 +++++-- windows/src/engine/keyman32/kmhook_getmessage.cpp | 4 +--- windows/src/engine/keyman32/kmprocessactions.cpp | 7 +++---- 5 files changed, 26 insertions(+), 19 deletions(-) diff --git a/windows/src/engine/keyman32/appint/aiTIP.cpp b/windows/src/engine/keyman32/appint/aiTIP.cpp index a64e96010c..a2baada55b 100644 --- a/windows/src/engine/keyman32/appint/aiTIP.cpp +++ b/windows/src/engine/keyman32/appint/aiTIP.cpp @@ -114,14 +114,6 @@ void ProcessToggleChange(UINT key) { // I4793 } } -/** - * Update the cache of the last key event received, this is set here - TIP processing, and by - * the GetMessage hook. - * @param _td Theread data to update - * @param wParam WPARAM of the key event - * @param scan Scan code of the key event - * @param keyTransition Transition state of the key event (0 = key down, 1 = repeat, 3 = key up) - */ void UpdateLastKeyCache(PKEYMAN64THREADDATA _td, WPARAM wParam, BYTE scan, BYTE keyTransition) { _td->LastKey = wParam; _td->LastScanCode = scan; @@ -142,7 +134,7 @@ BOOL TIPProcessKeyInternal( BOOL isUp = keyFlags & KF_UP ? TRUE : FALSE; BOOL extended = keyFlags & KF_EXTENDED ? TRUE : FALSE; BYTE scan = keyFlags & 0xFF; - BYTE keyTransition = (BYTE)((HIWORD(lParam) & (KF_UP | KF_REPEAT)) >> 14); + BYTE keyTransition = KEYMSG_FLAG_TRANSITION(lParam); WPARAM prevKey = _td->LastKey; BYTE prevScanCode = _td->LastScanCode; BYTE prevKeyTransition = _td->LastTransition; @@ -152,7 +144,7 @@ BOOL TIPProcessKeyInternal( SendDebugMessageFormat("VirtualKey=%s lParam=%x IsUp=%d Extended=%d Updateable=%d Preserved=%d", Debug_VirtualKey((WORD) wParam), lParam, isUp, extended, Updateable, Preserved); - if (_td->LastKey == wParam && scan == 0) { // I4642 - handle issue with Logos application. + if (_td->LastKey == wParam && scan == 0) { // I4642 - handle issue with Logos application. scan = _td->LastScanCode; SendDebugMessageFormat("Scan code was zero so using cached scan code %x", scan); } diff --git a/windows/src/engine/keyman32/appint/aiTIP.h b/windows/src/engine/keyman32/appint/aiTIP.h index 2e6b105d98..e379b49941 100644 --- a/windows/src/engine/keyman32/appint/aiTIP.h +++ b/windows/src/engine/keyman32/appint/aiTIP.h @@ -88,4 +88,19 @@ public: * @param key */ void ProcessToggleChange(UINT key); + +// Forward declaration - final in globals.h +struct tagKEYMAN64THREADDATA; +typedef struct tagKEYMAN64THREADDATA *PKEYMAN64THREADDATA; + +/** + * Update the cache of the last key event received, this is set here - TIP processing, and by + * the GetMessage hook. + * @param _td Thread data to update + * @param wParam WPARAM of the key event + * @param scan Scan code of the key event + * @param keyTransition Transition state of the key event (00b = key down, 01b = repeat, 11b = keyup) + */ +void UpdateLastKeyCache(PKEYMAN64THREADDATA _td, WPARAM wParam, BYTE scan, BYTE keyTransition); + #endif diff --git a/windows/src/engine/keyman32/keyman64.h b/windows/src/engine/keyman32/keyman64.h index 49ce6bab33..eb199722c7 100644 --- a/windows/src/engine/keyman32/keyman64.h +++ b/windows/src/engine/keyman32/keyman64.h @@ -118,11 +118,14 @@ #define KEYMSG_FLAG_DLGMODE(lParam) (HIWORD(lParam) & KF_DLGMODE ? 1 : 0) #define KEYMSG_FLAG_MENUMODE(lParam) (HIWORD(lParam) & KF_MENUMODE ? 1 : 0) #define KEYMSG_FLAG_ALTDOWN(lParam) (HIWORD(lParam) & KF_ALTDOWN ? 1 : 0) -// Repeat is actually previous "up" value of the key +// Repeat is actually previous KF_UP value of the key. +// It is always set to 1 for WM_KEYUP and WM_SYSKEYUP messages. +// It is set to 1 for WM_KEYDOWN and WM_SYSKEYDOWN keystroke messages generated by the automatic repeat feature. +// see https://learn.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input#previous-key-state-flag #define KEYMSG_FLAG_REPEAT(lParam) (HIWORD(lParam) & KF_REPEAT ? 1 : 0) #define KEYMSG_FLAG_UP(lParam) (HIWORD(lParam) & KF_UP ? 1 : 0) // Combine the transition flags into a single value for higher chance of identification -// 0 = key down, 1 = repeat, 2 = keyup +// 00b = key down, 01b = repeat, 11b = keyup #define KEYMSG_FLAG_TRANSITION(lParam) ((BYTE)((HIWORD(lParam) & (KF_UP | KF_REPEAT)) >> 14)) // TODO: Deprecate overloading of scancodes and use dwExtraInfo instead diff --git a/windows/src/engine/keyman32/kmhook_getmessage.cpp b/windows/src/engine/keyman32/kmhook_getmessage.cpp index da01903963..4c11dc31b5 100644 --- a/windows/src/engine/keyman32/kmhook_getmessage.cpp +++ b/windows/src/engine/keyman32/kmhook_getmessage.cpp @@ -158,9 +158,7 @@ LRESULT _kmnGetMessageProc(int nCode, WPARAM wParam, LPARAM lParam) BYTE scan = KEYMSG_LPARAM_SCAN(mp->lParam); BYTE keyTransitionEvent = KEYMSG_FLAG_TRANSITION(mp->lParam); CheckScheduledRefresh(); - _td->LastScanCode = scan; - _td->LastKey = mp->wParam; - _td->LastTransition = keyTransitionEvent; + UpdateLastKeyCache(_td, mp->wParam, scan, keyTransitionEvent); switch (mp->wParam) { case VK_MENU: diff --git a/windows/src/engine/keyman32/kmprocessactions.cpp b/windows/src/engine/keyman32/kmprocessactions.cpp index 64089c0670..2694658762 100644 --- a/windows/src/engine/keyman32/kmprocessactions.cpp +++ b/windows/src/engine/keyman32/kmprocessactions.cpp @@ -79,16 +79,15 @@ processPersistOpt(km_core_actions const* actions, LPINTKEYBOARDINFO activeKeyboa static void processCapsLock(const km_core_caps_state caps_state_change, BOOL isUp, BOOL Updateable, BOOL externalEvent) { BOOL isCapsOn = IsCapsLockOn(); - + // This debug message is useful for understanding the sequence of events around caps lock changes //SendDebugMessageFormat("ACTION CAPS STATE:%d FIsUp=%d Updateable=%d ExternalEvent=%d CapsState=%d", caps_state_change, isUp, Updateable, // externalEvent, isCapsOn); - + // We only want to process the Caps Lock key event once; // it has to be when updateble=1 as TSF does not consistently // have updateable=0 events. - if (!Updateable || caps_state_change == KM_CORE_CAPS_UNCHANGED) - { + if (!Updateable || caps_state_change == KM_CORE_CAPS_UNCHANGED) { return; } // Turn three state value into a boolean for whether capslock should be on or off,