From 5688441127fa694ca0e8b6f6bae8ec00dbf05e25 Mon Sep 17 00:00:00 2001 From: Matheus Mol <80729568+oMatheusmol@users.noreply.github.com> Date: Thu, 18 Jun 2026 05:33:56 -0300 Subject: [PATCH] [KBM] Fix modifier key remapped to non-modifier delivering WM_SYSKEYDOWN (#47192) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary of the Pull Request When a modifier key (Ctrl/Alt/Shift) is remapped to a non-modifier key using Keyboard Manager, the injected key event is delivered to applications as WM_SYSKEYDOWN instead of WM_KEYDOWN. This causes unexpected behavior — for example, remapping Left Alt to Backspace results in whole words being deleted instead of single characters, because applications interpret WM_SYSKEYDOWN + VK_BACK as Alt+Backspace. The fix resets the modifier state with a suppress-flag key-up event before injecting the target key, consistent with the existing approach used for the Caps Lock remapping scenario. ## PR Checklist - [ ] Closes: #47191 - [ ] Communication: I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [x] Tests: Added/updated and all pass - [ ] Localization: All end-user-facing strings can be localized - [ ] Dev docs: Added/updated - [ ] New binaries: Added on the required places ## Detailed Description of the Pull Request / Additional comments The root cause is that SendInput is called inside the low-level keyboard hook callback before the original modifier event is suppressed. At that point the modifier state is still active, so the OS delivers the injected key as WM_SYSKEYDOWN (system key with Alt context) rather than WM_KEYDOWN. This is the same mechanism that was already fixed for the Ctrl/Alt/Shift ↔ Caps Lock case. This PR extends the fix to cover modifier → non-modifier remaps. ## Validation Steps Performed 1. Remapped Left Alt → Backspace in Keyboard Manager 2. Opened a text editor, typed text, pressed the remapped key 3. Confirmed single characters are deleted instead of whole words 4. All 93 unit tests pass (KeyboardManager.Engine.UnitTests) --- .../KeyboardEventHandlers.cpp | 8 +++++++ .../SingleKeyRemappingTests.cpp | 21 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/modules/keyboardmanager/KeyboardManagerEngineLibrary/KeyboardEventHandlers.cpp b/src/modules/keyboardmanager/KeyboardManagerEngineLibrary/KeyboardEventHandlers.cpp index 50f5166940..46958bfc14 100644 --- a/src/modules/keyboardmanager/KeyboardManagerEngineLibrary/KeyboardEventHandlers.cpp +++ b/src/modules/keyboardmanager/KeyboardManagerEngineLibrary/KeyboardEventHandlers.cpp @@ -139,6 +139,14 @@ namespace KeyboardEventHandlers if (data->wParam == WM_KEYDOWN || data->wParam == WM_SYSKEYDOWN) { ResetIfModifierKeyForLowerLevelKeyHandlers(ii, it->first, target); + + // If a Ctrl/Alt/Shift key is remapped to a non-modifier key, reset the modifier state to prevent the injected key from being delivered as WM_SYSKEYDOWN instead of WM_KEYDOWN + if (Helpers::IsModifierKey(it->first) && !Helpers::IsModifierKey(target) && target != VK_CAPITAL && !(it->first == VK_LWIN || it->first == VK_RWIN || it->first == CommonSharedConstants::VK_WIN_BOTH)) + { + std::vector suppressList; + Helpers::SetKeyEvent(suppressList, INPUT_KEYBOARD, static_cast(it->first), KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SUPPRESS_FLAG); + ii.SendVirtualInput(suppressList); + } } if (remapToKey) diff --git a/src/modules/keyboardmanager/KeyboardManagerEngineTest/SingleKeyRemappingTests.cpp b/src/modules/keyboardmanager/KeyboardManagerEngineTest/SingleKeyRemappingTests.cpp index de6597ae5c..efb03aae2e 100644 --- a/src/modules/keyboardmanager/KeyboardManagerEngineTest/SingleKeyRemappingTests.cpp +++ b/src/modules/keyboardmanager/KeyboardManagerEngineTest/SingleKeyRemappingTests.cpp @@ -226,6 +226,27 @@ namespace RemappingLogicTests Assert::AreEqual(1, mockedInputHandler.GetSendVirtualInputCallCount()); } + // Test if SendVirtualInput is sent exactly once with the suppress flag when a Ctrl/Alt/Shift key is remapped to a non-modifier key + TEST_METHOD (HandleSingleKeyRemapEvent_ShouldSendVirtualInputWithSuppressFlagExactlyOnce_WhenCtrlAltShiftIsMappedToNonModifierKey) + { + mockedInputHandler.SetSendVirtualInputTestHandler([](LowlevelKeyboardEvent* data) { + if (data->lParam->dwExtraInfo == KeyboardManagerConstants::KEYBOARDMANAGER_SUPPRESS_FLAG) + return true; + else + return false; + }); + + testState.AddSingleKeyRemap(VK_LMENU, (DWORD)VK_BACK); + + std::vector inputs{ + { .type = INPUT_KEYBOARD, .ki = { .wVk = VK_LMENU } }, + }; + + mockedInputHandler.SendVirtualInput(inputs); + + Assert::AreEqual(1, mockedInputHandler.GetSendVirtualInputCallCount()); + } + // Test if correct keyboard states are set for a single key to two key shortcut remap TEST_METHOD (RemappedKeyToTwoKeyShortcut_ShouldSetTargetKeyState_OnKeyEvent) {