Skip to content

Commit 05b2316

Browse files
author
DavidQ
committed
Fix Input Mapping V2 capture flow and card sizing - PR_26140_110-fix-input-mapping-v2-capture-flow-and-card-sizing
1 parent e83a299 commit 05b2316

8 files changed

Lines changed: 241 additions & 307 deletions

File tree

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Input Mapping V2 Capture Flow And Card Sizing Report
2+
3+
Task: PR_26140_110-fix-input-mapping-v2-capture-flow-and-card-sizing
4+
5+
## Source Reading
6+
- Read `docs/dev/PROJECT_INSTRUCTIONS.md` before making changes.
7+
- Read `.codex/skills/repo-build/SKILL.md` and followed the repo BUILD workflow.
8+
- Read `docs/pr/BUILD_PR.md`; it still describes an unrelated overlay runtime hardening rebase, so the explicit PR110 request was used as the active build scope.
9+
10+
## Implementation
11+
- Increased Captured Mappings cards to 250px wide while preserving 225px height.
12+
- Changed captured mapping tokens from fixed 250px width to 100% of the tile content width.
13+
- Prevented capture from auto-creating mapping tiles. Captures now require a selected action tile created through Add.
14+
- Added an actionable WARN when capture is attempted without an existing tile.
15+
- Changed Combo gesture selection so it only selects gesture mode. Combo capture now starts only after a Capture button is pressed.
16+
- Preserved two-input combo commit behavior; Capture Keyboard waits for Ctrl + S before committing one combo mapping.
17+
- Moved wheel and pointer-drag descriptor commits behind Capture Mouse so selecting a gesture alone does not create mappings.
18+
- Preserved the pointer-drag snapshot selected before Capture Mouse so clicking the capture button does not overwrite drag bounds.
19+
- Removed visible `div#inputMappingV2UsedInputHighlights` duplicate output while preserving used-input highlight classes on Keyboard, Mouse, and Game Controller capture controls.
20+
- Preserved PR109 scroll position restoration and token click-to-delete behavior.
21+
22+
## Validation
23+
- `node --check tools/input-mapping-v2/js/ToolStarterApp.js`
24+
- `node --check tools/input-mapping-v2/js/services/InputMappingState.js`
25+
- `node --check tools/input-mapping-v2/js/controls/CaptureControl.js`
26+
- `node --check tools/input-mapping-v2/js/bootstrap.js`
27+
- `node --check tests/playwright/tools/WorkspaceManagerV2.spec.mjs`
28+
- `npx playwright test tests/playwright/tools/WorkspaceManagerV2.spec.mjs -g "Input Mapping V2"`: 7 passed.
29+
- `npm run test:workspace-v2`: 66 passed.
30+
- `git diff --check`: passed; PowerShell reported line-ending normalization warnings only.
31+
32+
## Playwright Coverage
33+
Playwright impacted: Yes.
34+
35+
Validated behavior:
36+
- mapping cards are 250px wide.
37+
- token display uses 100% flex basis inside each tile.
38+
- Combo selection alone does not create mappings.
39+
- Capture Keyboard waits for Ctrl + S before committing a combo.
40+
- capture does not auto-create a tile and warns when no tile exists.
41+
- Add creates the tile.
42+
- duplicate used-input highlight UI is absent.
43+
- used-input highlighting still appears on capture controls.
44+
- mapping scroll position remains preserved across selection, refresh, combo commit, and token deletion.
45+
46+
V8 coverage guardrail:
47+
- `(68%) tools/input-mapping-v2/js/controls/PreviewPanelControl.js`
48+
- `(86%) tools/input-mapping-v2/js/controls/CaptureControl.js`
49+
- `(92%) tools/input-mapping-v2/js/services/InputMappingState.js`
50+
- `(95%) tools/input-mapping-v2/js/ToolStarterApp.js`
51+
- `(100%) tools/input-mapping-v2/js/bootstrap.js`
52+
- Guardrail warnings: none.
53+
54+
## Manual Test Notes
55+
1. Open Input Mapping V2.
56+
2. Select an action without clicking Add, then click Capture Keyboard. Expected: WARN tells the user to select an action and click Add; no tile appears.
57+
3. Click Add for an action. Expected: a 250px wide mapping tile appears.
58+
4. Select Keyboard Combo, then press Ctrl and S without clicking Capture Keyboard. Expected: no mapping is created.
59+
5. Click Capture Keyboard, press Ctrl then S. Expected: one `Combo, Ctrl + S` token appears.
60+
6. Click a mapping token. Expected: only that token is removed and the tile remains.
61+
7. Select a tile with keyboard, mouse, or controller mappings. Expected: the related Capture button highlights; no duplicate used-input highlight panel appears.
62+
63+
Full samples smoke test was not run because this PR is scoped to Input Mapping V2 and Workspace V2 validation covers the impacted behavior.

tests/playwright/tools/WorkspaceManagerV2.spec.mjs

Lines changed: 90 additions & 45 deletions
Large diffs are not rendered by default.

tools/input-mapping-v2/index.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,6 @@ <h2 class="tools-platform-frame__eyebrow">Input Sources and Action Mapping</h2>
135135
<button id="inputMappingV2CaptureMouseButton" class="input-mapping-v2__capture-button" type="button">Capture Mouse</button>
136136
</div>
137137
<div id="inputMappingV2GamepadCaptureButtons" class="input-mapping-v2__gamepad-buttons" aria-label="Detected gamepad capture buttons"></div>
138-
<div id="inputMappingV2UsedInputHighlights" class="input-mapping-v2__used-input-highlights" aria-label="Used controls for selected action"></div>
139138
<p id="inputMappingV2CaptureMessage" class="tool-starter__hint">Keyboard, mouse, wheel, drag, and game controller sources are provided by src/engine/input.</p>
140139
<hr class="input-mapping-v2__capture-separator">
141140
<button id="inputMappingV2RefreshGamepadsButton" type="button">Refresh Gamepads</button>

tools/input-mapping-v2/js/ToolStarterApp.js

Lines changed: 74 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@ export class ToolStarterApp {
3434
this.workspaceRoot = workspaceRoot;
3535
this.window = windowRef;
3636
this.captureMode = "";
37+
this.activeCaptureId = "";
3738
this.activeGamepadIndex = null;
3839
this.comboCaptureInputs = [];
40+
this.pendingGestureInput = null;
3941
this.selectedGesture = null;
4042
this.rumbleSettingsByActionId = new Map();
4143
this.captureTimeoutMs = 8000;
@@ -155,6 +157,13 @@ export class ToolStarterApp {
155157
}
156158

157159
startKeyboardCapture() {
160+
if (!this.ensureSelectedTileForCapture()) {
161+
return;
162+
}
163+
if (this.selectedGesture?.captureKind === "combo") {
164+
this.startComboCapture(this.selectedGesture.deviceLabel, "keyboard");
165+
return;
166+
}
158167
if (this.isCaptureActive("keyboard")) {
159168
this.cancelCapture("Keyboard");
160169
return;
@@ -165,6 +174,17 @@ export class ToolStarterApp {
165174
}
166175

167176
startMouseCapture() {
177+
if (!this.ensureSelectedTileForCapture()) {
178+
return;
179+
}
180+
if (this.selectedGesture?.captureKind === "combo") {
181+
this.startComboCapture(this.selectedGesture.deviceLabel, "mouse");
182+
return;
183+
}
184+
if (this.selectedGesture?.captureKind === "pointer-drag" || this.selectedGesture?.captureKind === "wheel") {
185+
this.captureSelectedGesture();
186+
return;
187+
}
168188
if (this.isCaptureActive("mouse")) {
169189
this.cancelCapture("Mouse");
170190
return;
@@ -174,24 +194,36 @@ export class ToolStarterApp {
174194
this.statusLog.ok(`Mouse capture armed for ${this.state.selectedActionLabel()}.`);
175195
}
176196

177-
startComboCapture(deviceLabel = "Combo") {
178-
if (this.isCaptureActive("combo")) {
197+
startComboCapture(deviceLabel = "Combo", captureId = "combo") {
198+
if (!this.ensureSelectedTileForCapture()) {
199+
return;
200+
}
201+
if (this.isCaptureActive(captureId)) {
179202
this.cancelCapture(deviceLabel);
180203
return;
181204
}
182205
this.comboCaptureInputs = [];
183-
this.beginCapture("combo");
206+
this.beginCapture(captureId, { mode: "combo" });
184207
this.capture.showMessage(`Combo capture: press any two keyboard, mouse, wheel, or game controller inputs for ${this.state.selectedActionLabel()}.`);
185208
this.statusLog.ok(`${deviceLabel} combo capture armed for ${this.state.selectedActionLabel()}.`);
186-
this.tryCaptureComboGamepad();
209+
if (captureId.startsWith("gamepad:")) {
210+
this.tryCaptureComboGamepad();
211+
}
187212
}
188213

189214
startGamepadCapture(gamepadIndex) {
215+
if (!this.ensureSelectedTileForCapture()) {
216+
return;
217+
}
190218
const selectedIndex = Number(gamepadIndex);
191219
if (!Number.isInteger(selectedIndex)) {
192220
this.statusLog.warn("Gamepad capture unavailable: choose a detected gamepad device before capturing.");
193221
return;
194222
}
223+
if (this.selectedGesture?.captureKind === "combo") {
224+
this.startComboCapture(this.selectedGesture.deviceLabel, `gamepad:${selectedIndex}`);
225+
return;
226+
}
195227
if (this.isCaptureActive(`gamepad:${selectedIndex}`)) {
196228
this.cancelCapture(`Gamepad ${selectedIndex}`);
197229
return;
@@ -204,20 +236,17 @@ export class ToolStarterApp {
204236

205237
selectGesture(gesture) {
206238
this.selectedGesture = gesture;
239+
this.pendingGestureInput = null;
207240
if (gesture.captureKind === "combo") {
208-
this.startComboCapture(gesture.deviceLabel);
241+
this.statusLog.ok(`${gesture.deviceLabel} ${gesture.label} selected. Press a Capture button to record a two-input combo.`);
209242
this.refreshActions();
210243
return;
211244
}
212245
if (gesture.captureKind === "pointer-drag" || gesture.captureKind === "wheel") {
213246
const result = this.engineInputSources.captureGesture(gesture.binding, this.enabledDeviceIds);
214-
if (!result.ok) {
215-
this.statusLog.warn(result.message);
216-
this.refreshActions();
217-
return;
247+
if (result.ok) {
248+
this.pendingGestureInput = result.input;
218249
}
219-
this.addCapturedInput(result.input);
220-
return;
221250
}
222251
this.statusLog.ok(`${gesture.deviceLabel} ${gesture.label} selected for next capture.`);
223252
this.refreshActions();
@@ -503,13 +532,14 @@ export class ToolStarterApp {
503532
|| [...this.activeInputBindings].some((binding) => !previous.has(binding));
504533
}
505534

506-
beginCapture(captureId) {
535+
beginCapture(captureId, { mode = "" } = {}) {
507536
this.clearCapture();
537+
const captureMode = mode || (captureId.startsWith("gamepad:") ? "gamepad" : captureId);
538+
this.captureMode = captureMode;
539+
this.activeCaptureId = captureId;
508540
if (captureId.startsWith("gamepad:")) {
509-
this.captureMode = "gamepad";
510541
this.activeGamepadIndex = Number(captureId.slice("gamepad:".length));
511542
} else {
512-
this.captureMode = captureId;
513543
this.activeGamepadIndex = null;
514544
}
515545
this.capture.setActiveCapture(captureId);
@@ -528,6 +558,7 @@ export class ToolStarterApp {
528558
}
529559
this.captureTimeoutTimer = null;
530560
this.captureMode = "";
561+
this.activeCaptureId = "";
531562
this.activeGamepadIndex = null;
532563
this.comboCaptureInputs = [];
533564
this.capture.clearActiveCapture();
@@ -541,10 +572,7 @@ export class ToolStarterApp {
541572
}
542573

543574
isCaptureActive(captureId) {
544-
if (this.captureMode === "gamepad" && Number.isInteger(this.activeGamepadIndex)) {
545-
return captureId === `gamepad:${this.activeGamepadIndex}`;
546-
}
547-
return captureId === this.captureMode;
575+
return captureId === this.activeCaptureId;
548576
}
549577

550578
captureTimeoutDelay() {
@@ -582,6 +610,34 @@ export class ToolStarterApp {
582610
return true;
583611
}
584612

613+
captureSelectedGesture() {
614+
const pendingInput = this.pendingGestureInput?.binding === this.selectedGesture.binding
615+
? this.pendingGestureInput
616+
: null;
617+
const result = pendingInput
618+
? { ok: true, input: pendingInput }
619+
: this.engineInputSources.captureGesture(this.selectedGesture.binding, this.enabledDeviceIds);
620+
if (!result.ok) {
621+
this.statusLog.warn(result.message);
622+
this.refreshActions();
623+
return;
624+
}
625+
this.pendingGestureInput = null;
626+
this.addCapturedInput(result.input);
627+
}
628+
629+
ensureSelectedTileForCapture() {
630+
if (this.state.selectedActionHasTile()) {
631+
return true;
632+
}
633+
const message = "Capture requires an existing selected mapping tile. Select an action and click Add before capturing input.";
634+
this.capture.showMessage(message);
635+
this.statusLog.warn(message);
636+
this.clearCapture();
637+
this.refreshActions();
638+
return false;
639+
}
640+
585641
selectedRumbleSettings() {
586642
return this.rumbleSettingsByActionId.get(this.state.selectedActionId) ?? {
587643
durationMs: 80,

tools/input-mapping-v2/js/bootstrap.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,7 @@ window.addEventListener("DOMContentLoaded", () => {
5252
disableContextCheckbox: requireElement("#inputMappingV2DisableContextCheckbox"),
5353
refreshGamepadsButton: requireElement("#inputMappingV2RefreshGamepadsButton"),
5454
selectedActionLabel: requireElement("#inputMappingV2SelectedActionLabel"),
55-
suppressShortcutsCheckbox: requireElement("#inputMappingV2SuppressShortcutsCheckbox"),
56-
usedInputHighlights: requireElement("#inputMappingV2UsedInputHighlights")
55+
suppressShortcutsCheckbox: requireElement("#inputMappingV2SuppressShortcutsCheckbox")
5756
}),
5857
deviceList: new DeviceListControl({
5958
container: requireElement("#inputMappingV2DeviceList"),

0 commit comments

Comments
 (0)