Files
takahashi 7c28d3006a [Keyboard Manager] Add "alone" (tap) condition for single-key remaps (dual-key / to_if_alone) (#49136)
## 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>
2026-08-31 10:34:38 +00:00
..