Skip to content

Commit d3f1f4c

Browse files
committed
Clean up User Controls keyboard mouse capture and normalized options - PR_26162_045-user-controls-input-capture-cleanup
1 parent 27f0035 commit d3f1f4c

8 files changed

Lines changed: 848 additions & 676 deletions

account/user-controls-page.js

Lines changed: 174 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createControlsToolApiRepository } from "../toolbox/controls/controls-api-client.js";
22
import InputService from "../src/engine/input/InputService.js";
3+
import InputCaptureService from "../src/engine/input/InputCaptureService.js";
34
import { gamepadProfileInputNames } from "../src/engine/input/GamepadInputClassifier.js";
45
import {
56
normalizeProfileInputMappings,
@@ -9,8 +10,10 @@ import {
910
} from "../src/engine/input/NormalizedInputRegistry.js";
1011

1112
const DEVICE_POLL_INTERVAL_MS = 1200;
13+
const INPUT_CAPTURE_TIMEOUT_MS = 5000;
1214
const KEYBOARD_INPUTS = Object.freeze(["KeyW", "KeyA", "KeyS", "KeyD", "Space", "Enter", "Escape", "KeyP"]);
1315
const MOUSE_INPUTS = Object.freeze(["MouseButton0", "MouseButton2", "MouseX", "MouseY"]);
16+
const KEYBOARD_MOUSE_EXCLUDED_NORMALIZED_PREFIXES = Object.freeze(["dpad.", "trigger."]);
1417
const SUPPORTED_CONTROL_TYPES = Object.freeze([
1518
"Keyboard Key",
1619
"Mouse Button",
@@ -126,9 +129,11 @@ export class AccountUserControlsPage {
126129
this.root = root;
127130
this.repository = createControlsToolApiRepository();
128131
this.inputService = new InputService({ target: window });
132+
this.captureService = new InputCaptureService({ inputService: this.inputService });
129133
this.profiles = [];
130134
this.editingProfile = null;
131135
this.devicePollingTimer = null;
136+
this.inputCapture = null;
132137
this.elements = {
133138
addProfile: root.querySelector("[data-account-user-controls-add-profile]"),
134139
defaultLists: [...root.querySelectorAll("[data-account-user-controls-default-list]")],
@@ -398,6 +403,31 @@ export class AccountUserControlsPage {
398403
return "No game controller profiles saved yet.";
399404
}
400405

406+
tableColumnCountForFamily(family) {
407+
return family === "Keyboard" ? 4 : 7;
408+
}
409+
410+
normalizedOptionsForProfile(profile) {
411+
const allOptions = this.inputService.getNormalizedInputOptions();
412+
const normalizedFamily = this.profileListFamily(profile);
413+
const filteredOptions = normalizedFamily === "Keyboard" || normalizedFamily === "Mouse"
414+
? allOptions.filter((option) =>
415+
!KEYBOARD_MOUSE_EXCLUDED_NORMALIZED_PREFIXES.some((prefix) => option.value.startsWith(prefix)),
416+
)
417+
: allOptions;
418+
return [
419+
{ label: "Unassigned", value: "" },
420+
...filteredOptions,
421+
];
422+
}
423+
424+
generatedInputHeadings(profile) {
425+
if (this.profileListFamily(profile) === "Keyboard") {
426+
return ["Physical Input", "Normalized Control"];
427+
}
428+
return ["Physical Input", "Normalized Control", "Deadzone", "Invert", "Sensitivity"];
429+
}
430+
401431
renderProfiles() {
402432
if (!this.elements.lists.length) {
403433
return;
@@ -423,7 +453,7 @@ export class AccountUserControlsPage {
423453
if (!rows.length) {
424454
const row = document.createElement("tr");
425455
const cell = document.createElement("td");
426-
cell.colSpan = 7;
456+
cell.colSpan = this.tableColumnCountForFamily(family);
427457
cell.textContent = this.emptyProfileMessage(family);
428458
row.append(cell);
429459
rows.push(row);
@@ -475,14 +505,23 @@ export class AccountUserControlsPage {
475505
renderProfileRow(profile) {
476506
const row = document.createElement("tr");
477507
row.dataset.accountUserControlsProfileRow = profile.id;
478-
row.append(
479-
tableCell(profile.controllerName),
480-
tableCell(`${profile.inputMappings.length} Physical Inputs`),
481-
tableCell(this.profileInputSummary(profile)),
482-
tableCell(this.profileAnalogSummary(profile)),
483-
tableCell(profile.inputMappings.some((mapping) => mapping.invert) ? "Invert configured" : "No invert"),
484-
tableCell(this.profileSensitivitySummary(profile)),
485-
);
508+
const family = this.profileListFamily(profile);
509+
if (family === "Keyboard") {
510+
row.append(
511+
tableCell(profile.controllerName),
512+
tableCell(`${profile.inputMappings.length} Physical Inputs`),
513+
tableCell(this.profileInputSummary(profile)),
514+
);
515+
} else {
516+
row.append(
517+
tableCell(profile.controllerName),
518+
tableCell(`${profile.inputMappings.length} Physical Inputs`),
519+
tableCell(this.profileInputSummary(profile)),
520+
tableCell(this.profileAnalogSummary(profile)),
521+
tableCell(profile.inputMappings.some((mapping) => mapping.invert) ? "Invert configured" : "No invert"),
522+
tableCell(this.profileSensitivitySummary(profile)),
523+
);
524+
}
486525
const actions = document.createElement("td");
487526
const group = document.createElement("div");
488527
group.className = "action-group action-group--tight";
@@ -498,26 +537,27 @@ export class AccountUserControlsPage {
498537
inputControl(profile, inputMapping, index) {
499538
const row = document.createElement("tr");
500539
row.dataset.accountUserControlsInputPair = "true";
501-
const editablePhysicalInput = profile.deviceType === "Keyboard" || profile.deviceType === "Mouse";
540+
const family = this.profileListFamily(profile);
541+
const editablePhysicalInput = family === "Keyboard" || family === "Mouse";
502542
const physicalInputControl = editablePhysicalInput
503543
? document.createElement("input")
504544
: document.createElement("strong");
505545
const physicalInputCell = document.createElement("td");
506546
if (editablePhysicalInput) {
507547
physicalInputControl.type = "text";
508548
physicalInputControl.value = inputMapping.physicalInput;
549+
physicalInputControl.readOnly = true;
509550
physicalInputControl.dataset.accountUserControlsPhysicalInput = String(index);
510-
physicalInputControl.setAttribute("aria-label", `${profile.deviceType} physical input`);
551+
physicalInputControl.dataset.accountUserControlsCaptureDevice = family;
552+
physicalInputControl.title = `Click to capture the next ${family.toLowerCase()} input.`;
553+
physicalInputControl.setAttribute("aria-label", `${family} physical input`);
511554
} else {
512555
physicalInputControl.textContent = inputMapping.physicalInput;
513556
}
514557
physicalInputCell.append(physicalInputControl);
515558
const stack = document.createElement("div");
516559
stack.className = "content-stack content-stack--compact";
517-
const normalizedOptions = [
518-
{ label: "Unassigned", value: "" },
519-
...this.inputService.getNormalizedInputOptions(),
520-
];
560+
const normalizedOptions = this.normalizedOptionsForProfile(profile);
521561
if (physicalInputIsAnalog(inputMapping.physicalInput)) {
522562
const negativeSelect = selectControl({
523563
ariaLabel: `${inputMapping.physicalInput} negative normalized control`,
@@ -532,6 +572,13 @@ export class AccountUserControlsPage {
532572
});
533573
positiveSelect.dataset.accountUserControlsInputPositive = String(index);
534574
stack.append(labeledControl("Negative", negativeSelect), labeledControl("Positive", positiveSelect));
575+
} else if (family === "Keyboard") {
576+
const value = normalizeText(inputMapping.normalizedInput) || "Unassigned";
577+
const readonlyControl = document.createElement("span");
578+
readonlyControl.className = "status";
579+
readonlyControl.dataset.accountUserControlsInputNormalizedReadonly = String(index);
580+
readonlyControl.textContent = value;
581+
stack.append(readonlyControl);
535582
} else {
536583
const select = selectControl({
537584
ariaLabel: `${inputMapping.physicalInput} normalized control`,
@@ -546,6 +593,11 @@ export class AccountUserControlsPage {
546593
validation.dataset.accountUserControlsInputValidation = String(index);
547594
stack.append(validation);
548595

596+
if (family === "Keyboard") {
597+
row.append(physicalInputCell, controlCell(stack));
598+
return row;
599+
}
600+
549601
const deadzoneCell = document.createElement("td");
550602
if (physicalInputSupportsTuning(inputMapping.physicalInput)) {
551603
const deadzone = document.createElement("input");
@@ -598,6 +650,7 @@ export class AccountUserControlsPage {
598650

599651
renderEditingRows(values = {}) {
600652
const profile = this.normalizeProfile(values);
653+
const family = this.profileListFamily(profile);
601654
const row = document.createElement("tr");
602655
row.dataset.accountUserControlsEditingRow = "true";
603656
const controllerName = document.createElement("input");
@@ -621,20 +674,29 @@ export class AccountUserControlsPage {
621674
actionButton("Cancel", "accountUserControlsCancel"),
622675
);
623676
actions.append(group);
624-
row.append(
625-
controllerCell,
626-
tableCell(`${profile.inputMappings.length} Physical Inputs`),
627-
tableCell(this.profileInputSummary(profile)),
628-
tableCell(this.profileAnalogSummary(profile)),
629-
tableCell(profile.inputMappings.some((mapping) => mapping.invert) ? "Invert configured" : "No invert"),
630-
tableCell(this.profileSensitivitySummary(profile)),
631-
actions,
632-
);
677+
if (family === "Keyboard") {
678+
row.append(
679+
controllerCell,
680+
tableCell(`${profile.inputMappings.length} Physical Inputs`),
681+
tableCell(this.profileInputSummary(profile)),
682+
actions,
683+
);
684+
} else {
685+
row.append(
686+
controllerCell,
687+
tableCell(`${profile.inputMappings.length} Physical Inputs`),
688+
tableCell(this.profileInputSummary(profile)),
689+
tableCell(this.profileAnalogSummary(profile)),
690+
tableCell(profile.inputMappings.some((mapping) => mapping.invert) ? "Invert configured" : "No invert"),
691+
tableCell(this.profileSensitivitySummary(profile)),
692+
actions,
693+
);
694+
}
633695

634696
const detailsRow = document.createElement("tr");
635697
detailsRow.dataset.accountUserControlsEditingDetails = "true";
636698
const detailsCell = document.createElement("td");
637-
detailsCell.colSpan = 7;
699+
detailsCell.colSpan = this.tableColumnCountForFamily(family);
638700
const wrapper = document.createElement("div");
639701
wrapper.className = "table-wrapper";
640702
const table = document.createElement("table");
@@ -643,7 +705,7 @@ export class AccountUserControlsPage {
643705
table.setAttribute("aria-label", `${profile.controllerName} generated controller inputs`);
644706
const head = document.createElement("thead");
645707
const headRow = document.createElement("tr");
646-
["Physical Input", "Normalized Control", "Deadzone", "Invert", "Sensitivity"].forEach((heading) => {
708+
this.generatedInputHeadings(profile).forEach((heading) => {
647709
const cell = document.createElement("th");
648710
cell.textContent = heading;
649711
headRow.append(cell);
@@ -799,7 +861,83 @@ export class AccountUserControlsPage {
799861
});
800862
}
801863

864+
clearInputCapture({ restore = false } = {}) {
865+
if (!this.inputCapture) {
866+
return;
867+
}
868+
const { listeners, previousValue, target, timer } = this.inputCapture;
869+
if (timer) {
870+
window.clearTimeout(timer);
871+
}
872+
listeners.forEach(({ handler, type }) => {
873+
window.removeEventListener(type, handler, true);
874+
});
875+
if (restore && target?.isConnected) {
876+
target.value = previousValue;
877+
}
878+
this.inputCapture = null;
879+
}
880+
881+
capturedInputBinding(deviceType, event) {
882+
if (deviceType === "Keyboard") {
883+
return this.captureService.captureKeyboard(event)?.binding || "";
884+
}
885+
if (event.type === "wheel") {
886+
return this.captureService.captureWheel(event)?.binding || "";
887+
}
888+
return this.captureService.captureMouse(event)?.binding || "";
889+
}
890+
891+
beginPhysicalInputCapture(target) {
892+
const deviceType = target.dataset.accountUserControlsCaptureDevice || "";
893+
if (deviceType !== "Keyboard" && deviceType !== "Mouse") {
894+
return;
895+
}
896+
const previousValue = target.value;
897+
this.clearInputCapture({ restore: true });
898+
target.value = "";
899+
target.focus();
900+
const captureLabel = deviceType === "Keyboard" ? "keyboard key" : "mouse input";
901+
this.setStatus(`Waiting for ${captureLabel}. Previous value returns after 5 seconds with no input.`);
902+
const listeners = [];
903+
const finishCapture = (event) => {
904+
event.preventDefault();
905+
event.stopPropagation();
906+
const binding = this.capturedInputBinding(deviceType, event);
907+
if (!binding) {
908+
return;
909+
}
910+
this.clearInputCapture();
911+
if (target.isConnected) {
912+
target.value = binding;
913+
target.focus();
914+
}
915+
this.setStatus(`Captured ${binding}. Save the profile to keep it.`);
916+
};
917+
const timeout = window.setTimeout(() => {
918+
this.clearInputCapture({ restore: true });
919+
this.setStatus(`No ${captureLabel} captured. Restored ${previousValue}.`);
920+
}, INPUT_CAPTURE_TIMEOUT_MS);
921+
this.inputCapture = {
922+
listeners,
923+
previousValue,
924+
target,
925+
timer: timeout,
926+
};
927+
window.setTimeout(() => {
928+
if (!this.inputCapture || this.inputCapture.target !== target) {
929+
return;
930+
}
931+
const types = deviceType === "Keyboard" ? ["keydown"] : ["mousedown", "wheel"];
932+
types.forEach((type) => {
933+
window.addEventListener(type, finishCapture, true);
934+
listeners.push({ handler: finishCapture, type });
935+
});
936+
}, 0);
937+
}
938+
802939
saveEditingProfile() {
940+
this.clearInputCapture();
803941
const profile = this.profileFromEditingRow();
804942
const validation = this.validateProfile(profile);
805943
this.renderInputValidation(validation);
@@ -820,24 +958,34 @@ export class AccountUserControlsPage {
820958
}
821959

822960
handleListClick(event) {
961+
const physicalInputTarget = event.target instanceof Element
962+
? event.target.closest("[data-account-user-controls-physical-input]")
963+
: null;
964+
if (physicalInputTarget instanceof HTMLInputElement) {
965+
this.beginPhysicalInputCapture(physicalInputTarget);
966+
return;
967+
}
823968
const target = event.target instanceof Element ? event.target.closest("button") : null;
824969
if (!target) {
825970
return;
826971
}
827972
if (target.dataset.accountUserControlsSave !== undefined) {
828973
this.saveEditingProfile();
829974
} else if (target.dataset.accountUserControlsCancel !== undefined) {
975+
this.clearInputCapture({ restore: true });
830976
this.editingProfile = null;
831977
this.renderProfiles();
832978
this.setStatus("Canceled user control profile edit.");
833979
} else if (target.dataset.accountUserControlsEdit !== undefined) {
980+
this.clearInputCapture({ restore: true });
834981
const profile = this.profiles.find((candidate) => candidate.id === target.dataset.accountUserControlsEdit);
835982
if (profile) {
836983
this.editingProfile = { id: profile.id, values: profile };
837984
this.renderProfiles();
838985
this.setStatus(`Editing ${profile.mappingProfile}.`);
839986
}
840987
} else if (target.dataset.accountUserControlsTrash !== undefined) {
988+
this.clearInputCapture({ restore: true });
841989
const profileId = target.dataset.accountUserControlsTrash || "";
842990
this.profiles = this.profiles.filter((profile) => profile.id !== profileId);
843991
this.saveProfiles(this.profiles);

account/user-controls.html

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,6 @@ <h2>Physical Input Mapping</h2>
5757
<th>Physical Controller</th>
5858
<th>Physical Input</th>
5959
<th>Normalized Control</th>
60-
<th>Deadzone</th>
61-
<th>Invert</th>
62-
<th>Sensitivity</th>
6360
<th>Actions</th>
6461
</tr>
6562
</thead>
Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
11
# git status --short
22
M account/user-controls-page.js
3+
M account/user-controls.html
34
M docs_build/dev/reports/coverage_changed_js_guardrail.txt
45
M docs_build/dev/reports/playwright_v8_coverage_report.txt
56
M tests/playwright/tools/InputMappingV2Tool.spec.mjs
6-
M toolbox/controls/controls.js
7-
M toolbox/controls/index.html
8-
?? docs_build/dev/reports/controls-user-controls-copy-cleanup-report.md
7+
?? docs_build/dev/reports/user-controls-input-capture-cleanup-report.md
98

109
# git ls-files --others --exclude-standard
11-
docs_build/dev/reports/controls-user-controls-copy-cleanup-report.md
10+
docs_build/dev/reports/user-controls-input-capture-cleanup-report.md
1211

1312
# git diff --stat
14-
account/user-controls-page.js | 10 +-
15-
.../dev/reports/coverage_changed_js_guardrail.txt | 8 +-
16-
.../dev/reports/playwright_v8_coverage_report.txt | 13 +-
17-
tests/playwright/tools/InputMappingV2Tool.spec.mjs | 67 +++++----
18-
toolbox/controls/controls.js | 163 +--------------------
19-
toolbox/controls/index.html | 34 ++---
20-
6 files changed, 61 insertions(+), 234 deletions(-)
13+
account/user-controls-page.js | 200 ++++++++++++++++++---
14+
account/user-controls.html | 3 -
15+
.../dev/reports/coverage_changed_js_guardrail.txt | 9 +-
16+
.../dev/reports/playwright_v8_coverage_report.txt | 27 ++-
17+
tests/playwright/tools/InputMappingV2Tool.spec.mjs | 41 ++++-
18+
5 files changed, 230 insertions(+), 50 deletions(-)

0 commit comments

Comments
 (0)