mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-09-01 19:51:34 +02:00
## 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 action when **tapped by itself** while still acting as its normal self **when combined** with other keys — via the keyboard **or the mouse**. Example use case: **Right Ctrl → IME On when tapped alone**, **Left Ctrl → IME Off when tapped alone**, while **Right/Left Ctrl + other keys / clicks / wheel** keep working as normal Ctrl. This is the Windows equivalent of the popular macOS "left/right ⌘ → 英数/かな" mapping. ## Demo Muted screen recording with on-screen captions (the physical keys I press aren't visible; a red ring marks the tray IME indicator あ / A so the On/Off switching is easy to follow): https://github.com/user-attachments/assets/0da27a7d-3ae9-4718-94c9-5e79f054ca58 What it shows: 1. Tap **Right Ctrl** alone → IME On (Japanese); tap **Left Ctrl** alone → IME Off (English) — directional and idempotent (repeated taps keep the same state). 2. The same keys still act as **normal modifiers in combination**: Right Ctrl + H/J/K/L → arrow keys (Vim-style), and Ctrl+A / Ctrl+C / Ctrl+V behave normally. 3. **Mouse combinations preserved**: a plain click follows a link, while **Ctrl+Click** opens it in a new tab (via the companion low-level mouse hook). 4. In Keyboard Manager the whole config is just two "Alone" single-key remaps plus the Ctrl+H/J/K/L arrows, and the edit dialog shows the new **Condition → "Alone (tap)"**. ## PR Checklist - [ ] Closes: #40647 - [ ] Related: #5679 (broader dual-key / `to_if_alone` tracking issue; #40647 was closed as a duplicate of it) - [ ] **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 - [x] **Localization:** All end-user-facing strings can be localized - [x] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places — *N/A: no new binaries/projects are introduced (existing engine, editor UI and test projects only).* - [ ] **Documentation updated:** If checked, please file a pull request on our docs repo and link it here: #xxx ## Detailed Description of the Pull Request / Additional comments **Engine (C++):** - New separate remap table `aloneSingleKeyReMap` (parallel to `singleKeyReMap`) with `Add/Clear/GetSingleKeyAloneRemap` (`MappingConfiguration`). - New handler `HandleSingleKeyAloneRemapEvent` using 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`. - **Mouse combinations:** a companion low-level mouse hook (`WH_MOUSE_LL`) is installed alongside the keyboard hook **only while alone remaps exist** (reconciled on settings reload). On a button-down or wheel event — but **not** mouse-move, which must not cancel a tap — it promotes the held alone key(s) to a real modifier via the shared `PromotePendingAloneKeysToCombination`, so **Ctrl+Click / Ctrl+Wheel** work while an alone-mapped Ctrl is held. The mouse event itself is never suppressed; `State::HasPendingAloneKeys` gives the hook a cheap early-out. - Settings JSON: an optional `"condition"` field per single-key remap (`"always"` default, or `"alone"`). Fully backward compatible; both **load** and **save** handle it, so hand-edited or UI-created alone remaps round-trip without loss. **Editor UI (WinUI3):** - Native wrapper exposes parallel alone add/get/delete over the alone table (reuses the existing `SingleKeyMapping` marshaling struct). - `SingleKeyRemapCondition` (Always / Alone) flows through the C# interop/service/model layers. - A localized **"Condition"** combo box in the add/edit dialog, shown **only for a single-key remap whose action is a key/shortcut** (the case the engine supports), plus a localized **"Alone" badge** on such rows in the list. - **Modifier-conflict validation fix:** two *distinct* modifier sides (e.g. Left Ctrl and Right Ctrl) are no longer flagged as a "conflicting modifier". The check now only fires when one side is the generic, side-agnostic modifier (Ctrl/Shift/Alt/Win) versus a specific side — which is exactly the left/right pairing this feature targets. - **Duplicate-entry fix:** the editor's own settings store now rejects exact-duplicate remaps (guarding the add path the way the engine tables already do, condition-aware so "always" vs "alone" stay distinct) and **self-heals** pre-existing duplicates on load, so a remap can no longer appear twice in the list after mixed GUI / hand-edited-config changes. **Known limitations / follow-ups (intentionally not in this PR):** - Hold-timeout not implemented (holding the alone key then releasing still fires the alone action; needs an injectable clock — deferred, noted in code). - Alone *target* supports a single key or a shortcut (not text/URL/app). ## Validation Steps Performed - Engine unit tests: **110/110** (alone tests + edge cases, incl. 2 new mouse-promotion cases). Editor C++ tests: **88/88**. - Editor UI project compiles clean (0 warnings / 0 errors). - **Manual E2E on real hardware** (self-built x64 Release, multiple machines): - Right Ctrl tapped alone → IME On; Left Ctrl tapped alone → IME Off (directional, idempotent). - Ctrl+C / V / Z / A and existing Ctrl-based custom shortcuts unaffected (keyboard modifier pass-through OK). - **Ctrl+Click and Ctrl+Wheel** work while an alone-mapped Ctrl is held (mouse-combo pass-through). - Left Ctrl and Right Ctrl can both be mapped at once with no false "conflicting modifier" error. - Editor UI: opening an existing alone remap shows Condition = "Alone (tap)"; saving round-trips a full real-world config (single-key + dozens of global and app-specific shortcuts) with **zero loss** and **no duplicate rows**; the "Alone" badge and the show/hide gating of the Condition combo both behave correctly. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Table of Contents
Public overview - Microsoft Learn
Quick Links
The devdocs for Keyboard Manager have been divided into the following modules: