[Keyboard Manager] Add "alone" (tap) condition for single-key remaps (dual-key / to_if_alone)#49136
[Keyboard Manager] Add "alone" (tap) condition for single-key remaps (dual-key / to_if_alone)#49136MasaYan24 wants to merge 7 commits into
Conversation
…y tap/hold) Add a dual-key remap capability to the KBM engine: a single key can carry an "Alone" action that applies only when the key is tapped alone, while using it in combination with other keys passes the original key through as a real key/modifier (equivalent to Karabiner's to_if_alone). Uses the lazy + release-fire approach so the combination path has zero added latency. - New aloneSingleKeyReMap table plus Add/Clear/GetSingleKeyAloneRemap on MappingConfiguration/State, kept separate from the existing singleKeyReMap. - HandleSingleKeyAloneRemapEvent: suppresses the physical key-down (lazy); on another key-down it injects the original key as a real modifier (combination); on release with no intervening key it injects the alone action as a tap. - Per-key runtime state (pending vs combination) tracked in State across the separate key-down/key-up hook invocations. - 3 unit tests: no-injection-on-keydown, tap-alone injection, combination pass-through. Timeout-based cancellation (holding too long is not a tap) is intentionally deferred; it needs an injectable clock to be unit-testable. The handler is not yet wired into the runtime dispatch chain. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ition" from settings - Call HandleSingleKeyAloneRemapEvent before HandleSingleKeyRemapEvent in the keyboard hook dispatch so alone-mapped keys are handled with priority (lazy key-down hold, tap-vs-combination decision). - Include aloneSingleKeyReMap in HasRegisteredRemappingsUnchecked so a config with only alone remaps still activates the engine. - Load an optional "condition" field on single-key remaps: "alone" routes the entry to the alone table; a missing field (or "always") keeps the legacy unconditional behavior, so existing settings files load unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…andler - Add unit tests for a disabled alone target (tapping injects nothing) and for a solo tap after a prior combination cycle (per-key runtime state resets cleanly). - Document HandleSingleKeyAloneRemapEvent and the optional "condition" settings field in the keyboard event handlers devdoc. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SaveSettingsToFile previously wrote only singleKeyReMap and never persisted the aloneSingleKeyReMap table, so an alone remap loaded from settings was silently dropped the next time settings were saved. Add a serialization loop over aloneSingleKeyReMap that appends each entry to the inProcess array tagged with condition="alone", round-tripping with the (already condition-aware) loader. Regular remaps still omit the field and load as "always", so existing settings files are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dition Wire the "Alone" single-key remap condition through the WinUI3 editor so it can be created, edited, saved and reloaded. Previously the engine understood the condition and round-tripped it in settings, but the editor had no way to set or show it. - Native wrapper: parallel GetSingleKeyAloneRemapCount / GetSingleKeyAloneRemap / AddSingleKeyAloneRemap / AddSingleKeyAloneToShortcutRemap / DeleteSingleKeyAloneRemap over the aloneSingleKeyReMap table (reuses the existing SingleKeyMapping struct, so no marshaling changes). - C# interop/service: expose the alone add/get/delete and tag read-back mappings with IsAlone. - Model: SingleKeyRemapCondition (Always/Alone) on Remapping and ShortcutKeyMapping (including Equals/GetHashCode); settings correlation carries it. - UI: a Condition combo box on the mapping dialog; save/load/delete route to the alone table when the condition is Alone. Localizing the combo box (x:Uid/resw) and gating its visibility to single-key remaps are follow-ups. Engine (108) and editor (88) C++ unit test suites pass; the editor UI project compiles. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Polish the "Condition" (Always / Alone) control added for single-key dual-key remaps so it is production-ready: - Localize the combo box: replace the hardcoded "Condition" / "Always" / "Alone (tap)" strings with x:Uid + resw entries (ConditionComboBox.Header, Condition_Always.Content, Condition_Alone.Content), matching the existing ElevationComboBox pattern. - Gate visibility: the condition combo now shows only for a single-key remap whose action is a key/shortcut (the only case the engine supports "alone"), via UpdateConditionVisibility() driven off the trigger key count and action type. It starts collapsed and only toggles Visibility, so a condition set on load via SetCondition is preserved. - Surface the condition in the list: add an "Alone" badge (localized AloneBadge.Text) to the single-key remap rows, bound to a new Remapping.IsAlone via BoolToVisibilityConverter, so alone remaps are distinguishable without opening the edit dialog. Editor UI project compiles clean (0 warnings, 0 errors). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@MasaYan24 please read the following Contributor License Agreement(CLA). If you agree with the CLA, please reply with the following information.
Contributor License AgreementContribution License AgreementThis Contribution License Agreement (“Agreement”) is agreed to by the party signing below (“You”),
|
@MasaYan24 can you accept the policy agreement and would you mind taking a screenrecording of the new behavior? |
There was a problem hiding this comment.
Pull request overview
This PR adds “tap-alone” (Karabiner-style to_if_alone) behavior for single-key remaps in Keyboard Manager, allowing a key to trigger an alternate action when tapped by itself while still behaving normally when used in combinations.
Changes:
- Engine: adds a separate
aloneSingleKeyReMaptable and a new hook handlerHandleSingleKeyAloneRemapEventthat defers decision until another key is pressed or the key is released. - Settings + interop: adds a
"condition"field (always/alone) for single-key remaps and exposes parallel get/add/delete APIs for the alone table. - Editor UI: adds a localized “Condition” combo box (Always / Alone) and an “Alone” badge in the remap list.
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/modules/keyboardmanager/KeyboardManagerEngineTest/TestHelpers.cpp | Resets alone-remap state between tests. |
| src/modules/keyboardmanager/KeyboardManagerEngineTest/SingleKeyRemappingTests.cpp | Adds unit tests covering basic alone key behavior. |
| src/modules/keyboardmanager/KeyboardManagerEngineLibrary/State.h | Adds runtime tracking for pending vs combination alone keys + API surface. |
| src/modules/keyboardmanager/KeyboardManagerEngineLibrary/State.cpp | Implements alone runtime state helpers and alone remap lookup. |
| src/modules/keyboardmanager/KeyboardManagerEngineLibrary/KeyboardManager.cpp | Wires alone handler into the hook chain and includes alone table in “has remaps”. |
| src/modules/keyboardmanager/KeyboardManagerEngineLibrary/KeyboardEventHandlers.h | Declares HandleSingleKeyAloneRemapEvent. |
| src/modules/keyboardmanager/KeyboardManagerEngineLibrary/KeyboardEventHandlers.cpp | Implements alone remap hook logic (lazy key-down, release-fire). |
| src/modules/keyboardmanager/KeyboardManagerEditorUI/Strings/en-US/Resources.resw | Adds localized strings for Condition UI and Alone badge. |
| src/modules/keyboardmanager/KeyboardManagerEditorUI/Settings/SettingsManager.cs | Maps service-layer “IsAlone” into editor “Condition”. |
| src/modules/keyboardmanager/KeyboardManagerEditorUI/Pages/MainPage.xaml.cs | Loads/saves remap condition and passes it into SaveMapping. |
| src/modules/keyboardmanager/KeyboardManagerEditorUI/Pages/MainPage.xaml | Adds Alone badge to remap list UI. |
| src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/ShortcutKeyMapping.cs | Introduces SingleKeyRemapCondition + includes it in equality/hash. |
| src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/KeyMapping.cs | Tags service-returned mappings with IsAlone. |
| src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/KeyboardMappingService.cs | Surfaces alone mappings and provides add/delete methods. |
| src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/KeyboardManagerInterop.cs | Adds P/Invoke exports for alone remap APIs. |
| src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/RemappingHelper.cs | Saves/deletes single-key mappings using condition to choose table. |
| src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/Remapping.cs | Stores condition and computes IsAlone for list badge binding. |
| src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/UnifiedMappingControl.xaml.cs | Adds condition getter/setter + visibility gating logic. |
| src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/UnifiedMappingControl.xaml | Adds Condition combo box to the mapping UI. |
| src/modules/keyboardmanager/KeyboardManagerEditorLibraryWrapper/KeyboardManagerEditorLibraryWrapper.h | Exposes alone remap APIs to the editor wrapper. |
| src/modules/keyboardmanager/KeyboardManagerEditorLibraryWrapper/KeyboardManagerEditorLibraryWrapper.cpp | Implements alone remap enumeration/add/delete in wrapper. |
| src/modules/keyboardmanager/common/MappingConfiguration.h | Adds alone remap table + APIs. |
| src/modules/keyboardmanager/common/MappingConfiguration.cpp | Loads/saves alone condition via optional "condition" field and serializes alone table. |
| src/modules/keyboardmanager/common/KeyboardManagerConstants.h | Adds "condition" setting constants (always/alone). |
| doc/devdocs/modules/keyboardmanager/keyboardeventhandlers.md | Documents the new alone handler behavior and settings format. |
| // Tapped alone: fire the remapped alone action as a tap (down + up). | ||
| std::vector<INPUT> keyEventList; | ||
| const auto& target = it->second; | ||
| if (target.index() == 0) | ||
| { | ||
| const DWORD targetKey = std::get<DWORD>(target); | ||
| // A disabled alone target means "tapping alone does nothing". | ||
| if (targetKey != CommonSharedConstants::VK_DISABLED) | ||
| { | ||
| Helpers::SetKeyEvent(keyEventList, INPUT_KEYBOARD, static_cast<WORD>(targetKey), 0, KeyboardManagerConstants::KEYBOARDMANAGER_SINGLEKEY_FLAG); | ||
| Helpers::SetKeyEvent(keyEventList, INPUT_KEYBOARD, static_cast<WORD>(targetKey), KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SINGLEKEY_FLAG); | ||
| } | ||
| } | ||
| // NOTE: Shortcut- and text-valued alone targets are a later phase (no producer yet). | ||
|
|
| // Step 1: a key-down of a DIFFERENT key while alone keys are pending means those pending keys | ||
| // are now being used in combination. Flush them by injecting their original key-down as a real | ||
| // key/modifier so the in-combination behavior (e.g. Right Ctrl acting as Ctrl) works. | ||
| if (isKeyDown) | ||
| { | ||
| for (const DWORD pendingKey : state.GetPendingAloneKeys()) |
| 3. [HandleShortcutRemapEvent](#HandleShortcutRemapEvent) | ||
| 3. [HandleOSLevelShortcutRemapEvent](#HandleOSLevelShortcutRemapEvent) | ||
| 4. [HandleAppSpecificShortcutRemapEvent](#HandleAppSpecificShortcutRemapEvent) | ||
| 5. [HandleSingleKeyToggleToModEvent (Obsolete))](#HandleSingleKeyToggleToModEvent-(Obsolete---Code-from-PoC-which-is-commented-out)) |
Summary of the Pull Request
Adds an optional "alone" condition to single-key remaps in Keyboard Manager, bringing
Karabiner-style
to_if_alone(dual-key / tap-vs-hold) behavior: a key can perform one actionwhen tapped by itself while still acting as its normal self when combined with other keys.
Example use case: Right Ctrl → IME On when tapped alone, Left Ctrl → IME Off when tapped alone,
while Right/Left Ctrl + other keys keep working as normal Ctrl-based shortcuts. This is the
Windows equivalent of the popular macOS "left/right ⌘ → 英数/かな" mapping.
PR Checklist
to_if_alonetracking issue; Keyboard Manager: Add support for single-key press detection, IME switching, and flexible remapping with modifier combinations #40647 was closed as a duplicate of it)Detailed Description of the Pull Request / Additional comments
Engine (C++):
aloneSingleKeyReMap(parallel tosingleKeyReMap) withAdd/Clear/GetSingleKeyAloneRemap(MappingConfiguration).HandleSingleKeyAloneRemapEventusing a lazy + release-fire strategy:suppress the alone key's key-down; on the first other key it flushes the original key-down
(promoting to a normal combination); on solo release it fires the alone action. Wired into the
hook dispatch before the normal single-key handler; per-key runtime state lives in
State."condition"field per single-key remap ("always"default, or"alone"). Fully backward compatible; both load and save handle it, so hand-edited orUI-created alone remaps round-trip without loss.
Editor UI (WinUI3):
SingleKeyMappingmarshaling struct).SingleKeyRemapCondition(Always / Alone) flows through the C# interop/service/model layers.remap whose action is a key/shortcut (the case the engine supports), plus a localized
"Alone" badge on such rows in the list.
Known limitations / follow-ups (intentionally not in this PR):
needs an injectable clock — deferred, noted in code).
Validation Steps Performed
full real-world config (single-key + 70 global + 16 app-specific shortcuts) with zero loss;
the "Alone" badge and the show/hide gating of the Condition combo both behave correctly.