diff --git a/src/modules/keyboardmanager/KeyboardManagerEngineLibrary/KeyboardEventHandlers.cpp b/src/modules/keyboardmanager/KeyboardManagerEngineLibrary/KeyboardEventHandlers.cpp index 7c977c782f..4dcb797e2c 100644 --- a/src/modules/keyboardmanager/KeyboardManagerEngineLibrary/KeyboardEventHandlers.cpp +++ b/src/modules/keyboardmanager/KeyboardManagerEngineLibrary/KeyboardEventHandlers.cpp @@ -122,6 +122,21 @@ namespace KeyboardEventHandlers key_count = std::get(it->second).Size(); } + const DWORD sourceKey = data->lParam->vkCode; + const bool isKeyUp = (data->wParam == WM_KEYUP || data->wParam == WM_SYSKEYUP); + + // If the matching key-down injection was blocked earlier, we passed the + // original key-down through to the foreground app to keep the key alive. + // The corresponding key-up must be passed through as well; otherwise the + // physical key is stranded DOWN (its down reached the app, but its up would + // be swallowed by the remap). Key-down and key-up arrive as separate hook + // events, so this is the cross-invocation counterpart of the key-down + // passthrough handled below. + if (isKeyUp && state.ConsumeSingleKeyRemapInjectionFailed(sourceKey)) + { + return 0; + } + std::vector keyEventList; // Handle remaps to VK_WIN_BOTH @@ -177,7 +192,25 @@ namespace KeyboardEventHandlers } } - ii.SendVirtualInput(keyEventList); + if (!ii.SendVirtualInput(keyEventList)) + { + // Injection was blocked (e.g. by UIPI). Return 0 so the ORIGINAL key is + // passed through instead of being swallowed, leaving no dead key. For a + // key-down, remember that we passed it through so the matching key-up is + // passed through too (handled above), preventing a key stranded DOWN. + if (!isKeyUp) + { + state.SetSingleKeyRemapInjectionFailed(sourceKey, true); + } + return 0; + } + + // Injection succeeded; drop any stale passthrough marker for this key so its + // key-up follows the normal (suppressed) path. + if (!isKeyUp) + { + state.SetSingleKeyRemapInjectionFailed(sourceKey, false); + } if (data->wParam == WM_KEYDOWN || data->wParam == WM_SYSKEYDOWN) { @@ -552,9 +585,12 @@ namespace KeyboardEventHandlers // Send modifier release events first, then send text directly // (SendTextInput handles multiline by flushing between chunks) - ii.SendVirtualInput(keyEventList); + if (!ii.SendVirtualInput(keyEventList)) + { + return 0; + } keyEventList.clear(); - Helpers::SendTextInput(remapping); + Helpers::SendTextInput(remapping, ii); } it->second.isShortcutInvoked = true; @@ -566,7 +602,10 @@ namespace KeyboardEventHandlers Logger::trace(L"ChordKeyboardHandler:keyEventList.size:{}", keyEventList.size()); - ii.SendVirtualInput(keyEventList); + if (!ii.SendVirtualInput(keyEventList)) + { + return 0; + } if (activatedApp.has_value()) { if (remapToKey) @@ -705,7 +744,10 @@ namespace KeyboardEventHandlers state.SetActivatedApp(KeyboardManagerConstants::NoActivatedApp); } - ii.SendVirtualInput(keyEventList); + if (!ii.SendVirtualInput(keyEventList)) + { + return 0; + } return 1; } @@ -735,12 +777,14 @@ namespace KeyboardEventHandlers else if (remapToText) { auto& remapping = std::get(it->second.targetShortcut); - ii.SendVirtualInput(keyEventList); - Helpers::SendTextInput(remapping); + Helpers::SendTextInput(remapping, ii); return 1; } - ii.SendVirtualInput(keyEventList); + if (!ii.SendVirtualInput(keyEventList)) + { + return 0; + } return 1; } @@ -827,7 +871,10 @@ namespace KeyboardEventHandlers } } - ii.SendVirtualInput(keyEventList); + if (!ii.SendVirtualInput(keyEventList)) + { + return 0; + } return 1; } @@ -952,7 +999,10 @@ namespace KeyboardEventHandlers state.SetActivatedApp(KeyboardManagerConstants::NoActivatedApp); } - ii.SendVirtualInput(keyEventList); + if (!ii.SendVirtualInput(keyEventList)) + { + return 0; + } return 1; } else @@ -1021,7 +1071,10 @@ namespace KeyboardEventHandlers state.SetActivatedApp(KeyboardManagerConstants::NoActivatedApp); } - ii.SendVirtualInput(keyEventList); + if (!ii.SendVirtualInput(keyEventList)) + { + return 0; + } return 1; } else @@ -1799,8 +1852,9 @@ namespace KeyboardEventHandlers return 0; } - // Only send the text on keydown event - if (data->wParam != WM_KEYDOWN) + // Only send the text on key-down events. WM_SYSKEYDOWN is sent instead of + // WM_KEYDOWN while Alt is held, so accept it too or the remap silently drops. + if (data->wParam != WM_KEYDOWN && data->wParam != WM_SYSKEYDOWN) { return 0; } @@ -1811,7 +1865,43 @@ namespace KeyboardEventHandlers return 0; } - Helpers::SendTextInput(*remapping); + // Release held modifiers before text injection to prevent Ctrl+text corruption + constexpr int modifierKeys[] = { VK_LCONTROL, VK_RCONTROL, VK_LSHIFT, VK_RSHIFT, VK_LMENU, VK_RMENU, VK_LWIN, VK_RWIN }; + std::vector releaseEvents; + + // A dummy key event must precede the modifier releases so that releasing a + // held Win (Start Menu) or Alt (menu bar) does not trigger its lone-press + // action when we inject the modifier key-up. + Helpers::SetDummyKeyEvent(releaseEvents, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG); + + bool anyModifierHeld = false; + for (int vk : modifierKeys) + { + if (ii.GetVirtualKeyState(vk)) + { + Helpers::SetKeyEvent(releaseEvents, INPUT_KEYBOARD, static_cast(vk), KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG); + anyModifierHeld = true; + } + } + + // Only inject the dummy + modifier releases when a modifier was actually held. + if (anyModifierHeld) + { + if (!ii.SendVirtualInput(releaseEvents)) + { + return 0; + } + } + + Helpers::SendTextInput(*remapping, ii); + + // Intentionally do NOT re-press the released modifiers. Once we inject a + // KEYUP for a modifier, GetAsyncKeyState (and therefore GetVirtualKeyState) + // reports it as up, so there is no reliable way to tell whether the user is + // still physically holding the key or has released it. Re-pressing + // unconditionally would risk leaving a modifier stuck down if the user let + // go during injection — the exact failure this change set prevents. Leaving + // the modifier released is always safe: the user taps it again to re-engage. return 1; } diff --git a/src/modules/keyboardmanager/KeyboardManagerEngineLibrary/State.cpp b/src/modules/keyboardmanager/KeyboardManagerEngineLibrary/State.cpp index e56b1dc0e6..4a2d07861c 100644 --- a/src/modules/keyboardmanager/KeyboardManagerEngineLibrary/State.cpp +++ b/src/modules/keyboardmanager/KeyboardManagerEngineLibrary/State.cpp @@ -73,3 +73,20 @@ std::wstring State::GetActivatedApp() { return activatedAppSpecificShortcutTarget; } + +void State::SetSingleKeyRemapInjectionFailed(const DWORD sourceKey, const bool failed) +{ + if (failed) + { + singleKeyRemapInjectionFailedKeys.insert(sourceKey); + } + else + { + singleKeyRemapInjectionFailedKeys.erase(sourceKey); + } +} + +bool State::ConsumeSingleKeyRemapInjectionFailed(const DWORD sourceKey) +{ + return singleKeyRemapInjectionFailedKeys.erase(sourceKey) > 0; +} diff --git a/src/modules/keyboardmanager/KeyboardManagerEngineLibrary/State.h b/src/modules/keyboardmanager/KeyboardManagerEngineLibrary/State.h index 53b476302a..1a5d085861 100644 --- a/src/modules/keyboardmanager/KeyboardManagerEngineLibrary/State.h +++ b/src/modules/keyboardmanager/KeyboardManagerEngineLibrary/State.h @@ -1,5 +1,6 @@ #pragma once #include +#include class State : public MappingConfiguration { @@ -7,6 +8,12 @@ private: // Stores the activated target application in app-specific shortcut std::wstring activatedAppSpecificShortcutTarget; + // Source keys whose single-key remap key-down injection was blocked, so the original + // key-down was passed through to the foreground app. The matching key-up must be + // passed through too; otherwise the physical key is stranded DOWN. Only accessed from + // the (serialized) low-level keyboard hook thread. + std::unordered_set singleKeyRemapInjectionFailedKeys; + public: // Function to get the iterator of a single key remap given the source key. Returns nullopt if it isn't remapped std::optional GetSingleKeyRemap(const DWORD& originalKey); @@ -26,4 +33,14 @@ public: // Gets the activated target application in app-specific shortcut std::wstring GetActivatedApp(); + + // Records (failed == true) or clears (failed == false) that the single-key remap + // key-down injection for sourceKey was blocked and the original key-down was passed + // through to the foreground app. + void SetSingleKeyRemapInjectionFailed(const DWORD sourceKey, const bool failed); + + // Returns true and clears the marker if sourceKey's single-key remap key-down + // injection was previously blocked, indicating that its key-up should be passed + // through as well. + bool ConsumeSingleKeyRemapInjectionFailed(const DWORD sourceKey); }; \ No newline at end of file diff --git a/src/modules/keyboardmanager/KeyboardManagerEngineTest/MockedInput.cpp b/src/modules/keyboardmanager/KeyboardManagerEngineTest/MockedInput.cpp index 5a307817d0..4a97f8fa09 100644 --- a/src/modules/keyboardmanager/KeyboardManagerEngineTest/MockedInput.cpp +++ b/src/modules/keyboardmanager/KeyboardManagerEngineTest/MockedInput.cpp @@ -10,8 +10,14 @@ void MockedInput::SetHookProc(std::function ho } // Function to simulate keyboard input - arguments and return value based on SendInput function (https://learn.microsoft.com/windows/win32/api/winuser/nf-winuser-sendinput) -void MockedInput::SendVirtualInput(const std::vector& inputs) +bool MockedInput::SendVirtualInput(const std::vector& inputs) { + // Simulate an injection failure (e.g. SendInput blocked) when configured. + if (sendVirtualInputShouldFail != nullptr && sendVirtualInputShouldFail(inputs)) + { + return false; + } + // Iterate over inputs for (const INPUT& input : inputs) { @@ -107,6 +113,7 @@ void MockedInput::SendVirtualInput(const std::vector& inputs) } } } + return true; } // Function to simulate keyboard hook behavior @@ -129,6 +136,12 @@ bool MockedInput::GetVirtualKeyState(int key) return keyboardState[key]; } +// Function to set the state of a particular key for test setup +void MockedInput::SetKeyboardState(int key, bool state) +{ + keyboardState[key] = state; +} + // Function to reset the mocked keyboard state void MockedInput::ResetKeyboardState() { @@ -142,6 +155,12 @@ void MockedInput::SetSendVirtualInputTestHandler(std::function&)> condition) +{ + sendVirtualInputShouldFail = condition; +} + // Function to get SendVirtualInput call count int MockedInput::GetSendVirtualInputCallCount() { diff --git a/src/modules/keyboardmanager/KeyboardManagerEngineTest/MockedInput.h b/src/modules/keyboardmanager/KeyboardManagerEngineTest/MockedInput.h index c7fac51696..bbdba63f26 100644 --- a/src/modules/keyboardmanager/KeyboardManagerEngineTest/MockedInput.h +++ b/src/modules/keyboardmanager/KeyboardManagerEngineTest/MockedInput.h @@ -22,6 +22,10 @@ namespace KeyboardManagerInput int sendVirtualInputCallCount = 0; std::function sendVirtualInputCallCondition; + // Optional predicate; when set and it returns true for a SendVirtualInput + // call, that call fails (returns false) to simulate a SendInput failure. + std::function&)> sendVirtualInputShouldFail; + std::wstring currentProcess; public: @@ -34,7 +38,7 @@ namespace KeyboardManagerInput void SetHookProc(std::function hookProcedure); // Function to simulate keyboard input - void SendVirtualInput(const std::vector& inputs); + bool SendVirtualInput(const std::vector& inputs); // Function to simulate keyboard hook behavior intptr_t MockedKeyboardHook(LowlevelKeyboardEvent* data); @@ -42,12 +46,18 @@ namespace KeyboardManagerInput // Function to get the state of a particular key bool GetVirtualKeyState(int key); + // Function to set the state of a particular key for test setup + void SetKeyboardState(int key, bool state); + // Function to reset the mocked keyboard state void ResetKeyboardState(); // Function to set SendVirtualInput call count condition void SetSendVirtualInputTestHandler(std::function condition); + // Function to force SendVirtualInput to fail for calls matching a predicate + void SetSendVirtualInputShouldFail(std::function&)> condition); + // Function to get SendVirtualInput call count int GetSendVirtualInputCallCount(); diff --git a/src/modules/keyboardmanager/KeyboardManagerEngineTest/SingleKeyRemappingTests.cpp b/src/modules/keyboardmanager/KeyboardManagerEngineTest/SingleKeyRemappingTests.cpp index efb03aae2e..16b0a5b926 100644 --- a/src/modules/keyboardmanager/KeyboardManagerEngineTest/SingleKeyRemappingTests.cpp +++ b/src/modules/keyboardmanager/KeyboardManagerEngineTest/SingleKeyRemappingTests.cpp @@ -63,6 +63,84 @@ namespace RemappingLogicTests Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x42), false); } + // When injecting the remapped key fails (e.g. SendInput is blocked by UIPI or + // another hook), the handler must let the ORIGINAL key through instead of + // silently swallowing it, so the user is never left with a dead key. This + // exercises the stuck-key hardening that checks SendVirtualInput's return value. + TEST_METHOD (RemappedKey_ShouldPassOriginalKeyThrough_WhenInjectionFails) + { + // Remap A to B + testState.AddSingleKeyRemap(0x41, (DWORD)0x42); + + // Fail only KBM-injected events (tagged with a non-zero dwExtraInfo), + // leaving the test's own driving input (dwExtraInfo == 0) untouched. + mockedInputHandler.SetSendVirtualInputShouldFail([](const std::vector& inputs) { + for (const auto& input : inputs) + { + if (input.ki.dwExtraInfo != 0) + { + return true; + } + } + return false; + }); + + std::vector inputs{ + { .type = INPUT_KEYBOARD, .ki = { .wVk = 'A' } }, + }; + + // Send A keydown - injection of B fails, so A must pass through + mockedInputHandler.SendVirtualInput(inputs); + + // The original A is let through (state true); B was never injected (false) + Assert::AreEqual(true, mockedInputHandler.GetVirtualKeyState(0x41)); + Assert::AreEqual(false, mockedInputHandler.GetVirtualKeyState(0x42)); + } + + // When the remapped key-DOWN injection is blocked but the later key-UP injection + // would succeed, the handler must still let the ORIGINAL key-up through. The + // key-down was passed through to the app (key is physically DOWN), so swallowing + // the key-up would strand the physical key DOWN. This guards the asymmetric + // injection-failure stuck-key edge case, where key-down and key-up arrive as + // separate hook events. + TEST_METHOD (RemappedKey_ShouldReleaseOriginalKey_WhenKeyDownInjectionFailedButKeyUpSucceeds) + { + // Remap A to B + testState.AddSingleKeyRemap(0x41, (DWORD)0x42); + + // Fail only KBM-injected key-DOWN events; allow injected key-ups (and the + // test's own driving input, which has dwExtraInfo == 0) through. + mockedInputHandler.SetSendVirtualInputShouldFail([](const std::vector& inputs) { + for (const auto& input : inputs) + { + if (input.ki.dwExtraInfo != 0 && (input.ki.dwFlags & KEYEVENTF_KEYUP) == 0) + { + return true; + } + } + return false; + }); + + std::vector keyDown{ + { .type = INPUT_KEYBOARD, .ki = { .wVk = 'A' } }, + }; + + // Send A keydown - injection of B fails, so A passes through and is now DOWN + mockedInputHandler.SendVirtualInput(keyDown); + Assert::AreEqual(true, mockedInputHandler.GetVirtualKeyState(0x41)); + Assert::AreEqual(false, mockedInputHandler.GetVirtualKeyState(0x42)); + + std::vector keyUp{ + { .type = INPUT_KEYBOARD, .ki = { .wVk = 'A', .dwFlags = KEYEVENTF_KEYUP } }, + }; + + // Send A keyup - even though injecting B's key-up would succeed, the original A + // key-up must pass through so the physical A key is released, not stranded down + mockedInputHandler.SendVirtualInput(keyUp); + Assert::AreEqual(false, mockedInputHandler.GetVirtualKeyState(0x41)); + Assert::AreEqual(false, mockedInputHandler.GetVirtualKeyState(0x42)); + } + // Test if key is suppressed if a key is disabled by single key remap TEST_METHOD (RemappedKeyDisabled_ShouldNotChangeKeyState_OnKeyEvent) { @@ -350,4 +428,148 @@ namespace RemappingLogicTests Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x56), false); } }; + + // Tests for single key to text remap modifier release logic + TEST_CLASS (SingleKeyToTextRemapModifierTests) + { + private: + KeyboardManagerInput::MockedInput mockedInputHandler; + State testState; + + public: + TEST_METHOD_INITIALIZE(InitializeTestEnv) + { + TestHelpers::ResetTestEnv(mockedInputHandler, testState); + + // Set HandleSingleKeyToTextRemapEvent as the hook procedure + std::function currentHookProc = std::bind(&KeyboardEventHandlers::HandleSingleKeyToTextRemapEvent, std::ref(mockedInputHandler), std::placeholders::_1, std::ref(testState)); + mockedInputHandler.SetHookProc(currentHookProc); + } + + // A held Win key must be released before the text is injected and then left + // released — never re-pressed — so it can never be left stuck down. + TEST_METHOD (HandleSingleKeyToTextRemapEvent_ShouldReleaseWinKeyAndNotRestore_WhenWinKeyIsHeld) + { + // Remap X to text "hello" + testState.AddSingleKeyToTextRemap(0x58, L"hello"); + + // Simulate LWin being held down + mockedInputHandler.SetKeyboardState(VK_LWIN, true); + Assert::AreEqual(true, mockedInputHandler.GetVirtualKeyState(VK_LWIN)); + + std::vector inputs{ + { .type = INPUT_KEYBOARD, .ki = { .wVk = 0x58 } }, + }; + + // Send X keydown — handler releases LWin before the text and does not restore it + mockedInputHandler.SendVirtualInput(inputs); + + // LWin must be left released so it can never be stuck down + Assert::AreEqual(false, mockedInputHandler.GetVirtualKeyState(VK_LWIN)); + } + + // A held Ctrl must be released before the text and left released afterwards. + TEST_METHOD (HandleSingleKeyToTextRemapEvent_ShouldReleaseCtrlAndNotRestore_WhenCtrlIsHeld) + { + // Remap X to text "hello" + testState.AddSingleKeyToTextRemap(0x58, L"hello"); + + // Simulate LCtrl being held down + mockedInputHandler.SetKeyboardState(VK_LCONTROL, true); + Assert::AreEqual(true, mockedInputHandler.GetVirtualKeyState(VK_LCONTROL)); + + std::vector inputs{ + { .type = INPUT_KEYBOARD, .ki = { .wVk = 0x58 } }, + }; + + // Send X keydown + mockedInputHandler.SendVirtualInput(inputs); + + // LCtrl must be left released so it can never be stuck down + Assert::AreEqual(false, mockedInputHandler.GetVirtualKeyState(VK_LCONTROL)); + } + + // Every modifier that was held should be released, and none re-pressed. + TEST_METHOD (HandleSingleKeyToTextRemapEvent_ShouldReleaseAllHeldModifiers_AndNotRestore) + { + // Remap X to text "hello" + testState.AddSingleKeyToTextRemap(0x58, L"hello"); + + // Simulate LCtrl and LShift being held down together + mockedInputHandler.SetKeyboardState(VK_LCONTROL, true); + mockedInputHandler.SetKeyboardState(VK_LSHIFT, true); + + std::vector inputs{ + { .type = INPUT_KEYBOARD, .ki = { .wVk = 0x58 } }, + }; + + // Send X keydown + mockedInputHandler.SendVirtualInput(inputs); + + // Both modifiers must be left released + Assert::AreEqual(false, mockedInputHandler.GetVirtualKeyState(VK_LCONTROL)); + Assert::AreEqual(false, mockedInputHandler.GetVirtualKeyState(VK_LSHIFT)); + } + + // The handler must never inject a modifier key-down (re-press) event. Doing + // so could leave a modifier stuck down if the user released it during text + // injection, since GetAsyncKeyState cannot distinguish a still-held key from + // one we just released ourselves. + TEST_METHOD (HandleSingleKeyToTextRemapEvent_ShouldNeverRePressModifier_WhenModifierIsHeld) + { + // Remap X to text "hello" + testState.AddSingleKeyToTextRemap(0x58, L"hello"); + + // Simulate LCtrl being held down + mockedInputHandler.SetKeyboardState(VK_LCONTROL, true); + + // Count any modifier key-down events the handler injects (i.e. a re-press) + mockedInputHandler.SetSendVirtualInputTestHandler([](LowlevelKeyboardEvent* keyEvent) { + const DWORD vk = keyEvent->lParam->vkCode; + const bool isModifier = (vk == VK_LCONTROL || vk == VK_RCONTROL || vk == VK_LSHIFT || vk == VK_RSHIFT || vk == VK_LMENU || vk == VK_RMENU || vk == VK_LWIN || vk == VK_RWIN); + return isModifier && keyEvent->wParam == WM_KEYDOWN; + }); + + std::vector inputs{ + { .type = INPUT_KEYBOARD, .ki = { .wVk = 0x58 } }, + }; + + // Send X keydown + mockedInputHandler.SendVirtualInput(inputs); + + // No modifier re-press should ever be injected + Assert::AreEqual(0, mockedInputHandler.GetSendVirtualInputCallCount()); + } + + // A key-to-text remap must still fire while Alt is held. Windows delivers a + // key pressed with Alt down as WM_SYSKEYDOWN rather than WM_KEYDOWN, so a + // handler that only accepted WM_KEYDOWN would silently drop the remap. Alt + // being held also drives the modifier-release path, so the proof that the + // WM_SYSKEYDOWN event was accepted and processed is that the held Alt ends + // up released. If WM_SYSKEYDOWN were rejected the handler would return + // before the release loop and Alt would remain down. + TEST_METHOD (HandleSingleKeyToTextRemapEvent_ShouldFireAndReleaseAlt_WhenAltIsHeld) + { + // Remap X to text "hello" + testState.AddSingleKeyToTextRemap(0x58, L"hello"); + + // Simulate Left Alt being held. VK_MENU makes the mock deliver the key + // as WM_SYSKEYDOWN (as the OS does while Alt is down); VK_LMENU is the + // physical key the handler sees as held and must release. + mockedInputHandler.SetKeyboardState(VK_MENU, true); + mockedInputHandler.SetKeyboardState(VK_LMENU, true); + Assert::AreEqual(true, mockedInputHandler.GetVirtualKeyState(VK_LMENU)); + + std::vector inputs{ + { .type = INPUT_KEYBOARD, .ki = { .wVk = 0x58 } }, + }; + + // Send X keydown — arrives as WM_SYSKEYDOWN because Alt is held + mockedInputHandler.SendVirtualInput(inputs); + + // The remap fired: the held Alt was released and never re-pressed, so it + // can never be left stuck down. + Assert::AreEqual(false, mockedInputHandler.GetVirtualKeyState(VK_LMENU)); + } + }; } diff --git a/src/modules/keyboardmanager/KeyboardManagerEngineTest/TestHelpers.cpp b/src/modules/keyboardmanager/KeyboardManagerEngineTest/TestHelpers.cpp index c0986e123b..68ad3c8f78 100644 --- a/src/modules/keyboardmanager/KeyboardManagerEngineTest/TestHelpers.cpp +++ b/src/modules/keyboardmanager/KeyboardManagerEngineTest/TestHelpers.cpp @@ -11,10 +11,12 @@ namespace TestHelpers input.ResetKeyboardState(); input.SetHookProc(nullptr); input.SetSendVirtualInputTestHandler(nullptr); + input.SetSendVirtualInputShouldFail(nullptr); input.SetForegroundProcess(L""); state.ClearSingleKeyRemaps(); state.ClearOSLevelShortcuts(); state.ClearAppSpecificShortcuts(); + state.ClearSingleKeyToTextRemaps(); // Allocate memory for the keyboardManagerState activatedApp member to avoid CRT assert errors std::wstring maxLengthString; diff --git a/src/modules/keyboardmanager/common/Helpers.cpp b/src/modules/keyboardmanager/common/Helpers.cpp index a2bded6f71..1c29c63ae3 100644 --- a/src/modules/keyboardmanager/common/Helpers.cpp +++ b/src/modules/keyboardmanager/common/Helpers.cpp @@ -6,6 +6,7 @@ #include #include "KeyboardManagerConstants.h" +#include "InputInterface.h" namespace Helpers { @@ -313,7 +314,7 @@ namespace Helpers // Shift+Enter. Each character is sent individually to avoid a synchronization // error across key-down and key-up events that causes repeated or dropped characters // when large batches of KEYEVENTF_UNICODE events are sent at once. - void SendTextInput(const std::wstring& text) + void SendTextInput(const std::wstring& text, KeyboardManagerInput::InputInterface& ii) { for (size_t i = 0; i < text.size(); ++i) { @@ -359,7 +360,7 @@ namespace Helpers returnInputs[3].ki.wScan = static_cast(MapVirtualKey(VK_SHIFT, MAPVK_VK_TO_VSC)); returnInputs[3].ki.dwExtraInfo = KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG; - SendInput(ARRAYSIZE(returnInputs), returnInputs, sizeof(INPUT)); + ii.SendVirtualInput(std::vector(returnInputs, returnInputs + ARRAYSIZE(returnInputs))); continue; } @@ -374,7 +375,7 @@ namespace Helpers charInputs[1].ki.dwExtraInfo = KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG; charInputs[1].ki.wScan = c; - SendInput(ARRAYSIZE(charInputs), charInputs, sizeof(INPUT)); + ii.SendVirtualInput(std::vector(charInputs, charInputs + ARRAYSIZE(charInputs))); } } diff --git a/src/modules/keyboardmanager/common/Helpers.h b/src/modules/keyboardmanager/common/Helpers.h index 84b34e89c2..39639181aa 100644 --- a/src/modules/keyboardmanager/common/Helpers.h +++ b/src/modules/keyboardmanager/common/Helpers.h @@ -4,6 +4,11 @@ class LayoutMap; +namespace KeyboardManagerInput +{ + class InputInterface; +} + namespace Helpers { // Type to distinguish between keys @@ -41,7 +46,7 @@ namespace Helpers // Function to send text input directly, with multiline support. // Sends each line via KEYEVENTF_UNICODE and newlines via VK_RETURN // as separate SendInput calls to avoid mixing event types. - void SendTextInput(const std::wstring& text); + void SendTextInput(const std::wstring& text, KeyboardManagerInput::InputInterface& ii); // Function to return window handle for a full screen UWP app HWND GetFullscreenUWPWindowHandle(); diff --git a/src/modules/keyboardmanager/common/Input.h b/src/modules/keyboardmanager/common/Input.h index 552182079d..3409ccee2e 100644 --- a/src/modules/keyboardmanager/common/Input.h +++ b/src/modules/keyboardmanager/common/Input.h @@ -11,17 +11,41 @@ namespace KeyboardManagerInput class Input : public InputInterface { public: - // Function to simulate input - void SendVirtualInput(const std::vector& inputs) + // Function to simulate input. Returns false only when nothing could be injected + // (the call was fully blocked); returns true on full or partial success. A partial + // injection means some remap events already reached the system, so passing the + // original key through on top of them would corrupt the input stream (e.g. leave a + // modifier stuck). In that rare case we suppress the original and log a warning. + bool SendVirtualInput(const std::vector& inputs) { + if (inputs.empty()) + { + return true; + } + std::vector copy = inputs; UINT eventCount = SendInput(static_cast(copy.size()), copy.data(), sizeof(INPUT)); - if (eventCount != copy.size()) + if (eventCount == 0) { + // Nothing was injected (e.g. blocked by UIPI). The caller passes the + // original key through so the user is never left with a dead key. Logger::error( L"Failed to send input events. {}", get_last_error_or_default(GetLastError())); + return false; } + if (eventCount != copy.size()) + { + // Partial injection: SendInput stopped after some events. Report success so + // the caller suppresses the original event rather than layering it on top of + // a half-applied remap, which could strand a key or modifier down. + Logger::warn( + L"Partially sent input events ({} of {}). {}", + eventCount, + static_cast(copy.size()), + get_last_error_or_default(GetLastError())); + } + return true; } // Function to get the state of a particular key diff --git a/src/modules/keyboardmanager/common/InputInterface.h b/src/modules/keyboardmanager/common/InputInterface.h index f6728d92d0..5efd4420e5 100644 --- a/src/modules/keyboardmanager/common/InputInterface.h +++ b/src/modules/keyboardmanager/common/InputInterface.h @@ -10,8 +10,9 @@ namespace KeyboardManagerInput class InputInterface { public: - // Function to simulate input - virtual void SendVirtualInput(const std::vector& inputs) = 0; + // Function to simulate input. Returns false only when nothing could be injected + // (the call was fully blocked); returns true on full or partial success. + virtual bool SendVirtualInput(const std::vector& inputs) = 0; // Function to get the state of a particular key virtual bool GetVirtualKeyState(int key) = 0;