Skip to content

Commit cad7819

Browse files
committed
Fix user controls selected device placement, control type layout, and mouse capture loop - PR_26163_052-user-controls-device-selection-and-capture-fix
1 parent dc28342 commit cad7819

36 files changed

Lines changed: 1040 additions & 1767 deletions

account/user-controls-page.js

Lines changed: 121 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,25 @@ import {
1111

1212
const DEVICE_POLL_INTERVAL_MS = 1200;
1313
const INPUT_CAPTURE_TIMEOUT_MS = 5000;
14+
const INPUT_CAPTURE_CLICK_SUPPRESSION_MS = 250;
1415
const KEYBOARD_INPUTS = Object.freeze(["KeyW", "KeyA", "KeyS", "KeyD", "Space", "ShiftLeft", "ControlLeft", "Enter", "Backspace", "KeyP"]);
1516
const MOUSE_INPUTS = Object.freeze(["MouseButton0", "MouseButton2", "MouseButton1", "MouseWheelUp", "MouseWheelDown", "MouseX-", "MouseX+", "MouseY-", "MouseY+"]);
1617
const KEYBOARD_MOUSE_EXCLUDED_NORMALIZED_PREFIXES = Object.freeze(["dpad.", "trigger."]);
1718
const SUPPORTED_CONTROL_TYPES = Object.freeze([
18-
"Keyboard Key",
19-
"Mouse Button",
20-
"Mouse Axis",
21-
"Mouse Wheel",
22-
"Gamepad Button",
23-
"Gamepad Trigger",
19+
"Gamepad Axis",
2420
"Gamepad Bumper",
21+
"Gamepad Button",
2522
"Gamepad Stick Button",
26-
"Gamepad Axis",
27-
"Joystick Button",
23+
"Gamepad Trigger",
2824
"Joystick Axis",
29-
"Touch Button",
30-
"Touch Axis / Pad",
25+
"Joystick Button",
26+
"Keyboard Key",
27+
"Mouse Axis",
28+
"Mouse Button",
29+
"Mouse Wheel",
3130
"Pointer Drag",
31+
"Touch Axis / Pad",
32+
"Touch Button",
3233
]);
3334

3435
function normalizeText(value) {
@@ -149,7 +150,6 @@ export class AccountUserControlsPage {
149150
familyButtons: [...root.querySelectorAll("[data-account-user-controls-edit-family]")],
150151
lists: [...root.querySelectorAll("[data-account-user-controls-list]")],
151152
refresh: root.querySelector("[data-account-user-controls-refresh]"),
152-
selectedDeviceOptions: root.querySelector("[data-account-user-controls-selected-device-options]"),
153153
selectedDeviceStatus: root.querySelector("[data-account-user-controls-selected-device-status]"),
154154
status: root.querySelector("[data-account-user-controls-status]"),
155155
types: root.querySelector("[data-account-user-controls-types]"),
@@ -168,11 +168,11 @@ export class AccountUserControlsPage {
168168
this.elements.refresh?.addEventListener("click", () => {
169169
this.renderDeviceSelect();
170170
this.renderSelectedInputDevices();
171+
this.renderProfiles();
171172
this.setStatus(this.deviceRefreshMessage());
172173
});
173174
this.elements.addProfile?.addEventListener("click", () => this.addProfileForSelectedDevice());
174175
this.elements.deviceSelect?.addEventListener("change", () => this.renderDeviceStatus());
175-
this.elements.selectedDeviceOptions?.addEventListener("change", (event) => this.handleSelectedInputDeviceChange(event));
176176
this.elements.familyButtons.forEach((button) => {
177177
button.addEventListener("click", () => this.editFamilyMappings(button.dataset.accountUserControlsEditFamily || ""));
178178
});
@@ -426,68 +426,63 @@ export class AccountUserControlsPage {
426426
});
427427
}
428428

429+
defaultSelectionChoice(family) {
430+
const profile = this.defaultProfileForFamily(family);
431+
return {
432+
controllerId: profile.controllerId,
433+
deviceType: family,
434+
label: `${family} Default Profile`,
435+
profileId: "",
436+
selectionKey: `default:${family.toLowerCase()}`,
437+
selectionType: "default",
438+
};
439+
}
440+
441+
detectedDeviceSelectionChoice(device) {
442+
return {
443+
controllerId: device.controllerId,
444+
deviceType: "Gamepad",
445+
label: device.label,
446+
profileId: "",
447+
selectionKey: `device:gamepad:${keyFromText(device.controllerId || device.value)}`,
448+
selectionType: "device",
449+
};
450+
}
451+
452+
profileSelectionChoice(profile) {
453+
return {
454+
controllerId: profile.controllerId,
455+
deviceType: this.profileListFamily(profile),
456+
label: `${profile.controllerName} (${profile.mappingProfile})`,
457+
profileId: profile.id,
458+
selectionKey: `profile:${profile.id}`,
459+
selectionType: "profile",
460+
};
461+
}
462+
429463
selectedInputDeviceChoices() {
430464
const choices = [];
431465
const seenKeys = new Set();
432-
const profilesByFamily = new Map([
433-
["Keyboard", []],
434-
["Mouse", []],
435-
["Gamepad", []],
436-
]);
437-
this.profiles.forEach((profile) => {
438-
profilesByFamily.get(this.profileListFamily(profile))?.push(profile);
439-
});
440466
const addChoice = (choice) => {
441467
if (seenKeys.has(choice.selectionKey)) {
442468
return;
443469
}
444470
seenKeys.add(choice.selectionKey);
445471
choices.push(choice);
446472
};
447-
const addProfileChoice = (profile) => {
448-
addChoice({
449-
controllerId: profile.controllerId,
450-
deviceType: this.profileListFamily(profile),
451-
label: `${profile.controllerName} (${profile.mappingProfile})`,
452-
profileId: profile.id,
453-
selectionKey: `profile:${profile.id}`,
454-
selectionType: "profile",
455-
});
456-
};
457-
["Keyboard", "Mouse"].forEach((family) => {
458-
const profiles = profilesByFamily.get(family) || [];
459-
if (profiles.length) {
460-
profiles.forEach(addProfileChoice);
461-
return;
462-
}
463-
const device = this.familyDevice(family);
464-
addChoice({
465-
controllerId: device.controllerId,
466-
deviceType: family,
467-
label: device.label,
468-
profileId: "",
469-
selectionKey: `device:${device.value}`,
470-
selectionType: "device",
471-
});
472-
});
473-
const gamepadProfiles = profilesByFamily.get("Gamepad") || [];
473+
["Keyboard", "Mouse", "Gamepad"].forEach((family) => addChoice(this.defaultSelectionChoice(family)));
474474
const profiledGamepadIds = new Set();
475-
gamepadProfiles.forEach((profile) => {
476-
profiledGamepadIds.add(profile.controllerId);
477-
addProfileChoice(profile);
475+
this.profiles.forEach((profile) => {
476+
if (this.profileListFamily(profile) === "Gamepad") {
477+
profiledGamepadIds.add(profile.controllerId);
478+
}
479+
addChoice(this.profileSelectionChoice(profile));
478480
});
479481
this.deviceOptions().forEach((device) => {
480482
if (profiledGamepadIds.has(device.controllerId)) {
481483
return;
482484
}
483-
addChoice({
484-
controllerId: device.controllerId,
485-
deviceType: "Gamepad",
486-
label: device.label,
487-
profileId: "",
488-
selectionKey: `device:gamepad:${keyFromText(device.controllerId || device.value)}`,
489-
selectionType: "device",
490-
});
485+
addChoice(this.detectedDeviceSelectionChoice(device));
491486
});
492487
return choices;
493488
}
@@ -506,6 +501,9 @@ export class AccountUserControlsPage {
506501
}
507502
return this.deviceOptions().some((device) => device.controllerId === profile.controllerId);
508503
}
504+
if (selection.selectionType === "default") {
505+
return true;
506+
}
509507
if (selection.deviceType === "Keyboard" || selection.deviceType === "Mouse") {
510508
return true;
511509
}
@@ -530,23 +528,6 @@ export class AccountUserControlsPage {
530528
}
531529

532530
renderSelectedInputDevices() {
533-
if (!this.elements.selectedDeviceOptions) {
534-
this.renderSelectedInputDeviceStatus();
535-
return;
536-
}
537-
const choices = this.selectedInputDeviceChoices();
538-
const controls = choices.map((choice) => {
539-
const label = document.createElement("label");
540-
const input = document.createElement("input");
541-
input.type = "radio";
542-
input.name = "account-user-controls-selected-device";
543-
input.value = choice.selectionKey;
544-
input.dataset.accountUserControlsSelectedDevice = choice.selectionKey;
545-
input.checked = this.selectedInputDevice?.selectionKey === choice.selectionKey;
546-
label.append(input, document.createTextNode(` ${choice.label}`));
547-
return label;
548-
});
549-
this.elements.selectedDeviceOptions.replaceChildren(...controls);
550531
this.renderSelectedInputDeviceStatus();
551532
}
552533

@@ -648,7 +629,7 @@ export class AccountUserControlsPage {
648629
}
649630

650631
tableColumnCountForFamily(family) {
651-
return family === "Keyboard" ? 4 : 7;
632+
return family === "Keyboard" ? 5 : 8;
652633
}
653634

654635
normalizedOptionsForProfile(profile) {
@@ -687,6 +668,14 @@ export class AccountUserControlsPage {
687668
rows.push(this.renderReadonlyProfileDetailsRow(defaultProfile, family));
688669
}
689670
});
671+
const profiledGamepadIds = new Set(this.profiles
672+
.filter((profile) => this.profileListFamily(profile) === "Gamepad")
673+
.map((profile) => profile.controllerId));
674+
this.deviceOptions()
675+
.filter((device) => !profiledGamepadIds.has(device.controllerId))
676+
.forEach((device) => {
677+
rowsByFamily.get("Gamepad")?.push(this.renderDetectedDeviceRow(device));
678+
});
690679
if (this.editingProfile) {
691680
const family = this.profileListFamily(this.editingProfile.values);
692681
rowsByFamily.get(family)?.push(...this.renderEditingRows(this.editingProfile.values));
@@ -720,6 +709,19 @@ export class AccountUserControlsPage {
720709
this.renderSelectedInputDevices();
721710
}
722711

712+
selectedDeviceCell(choice) {
713+
const cell = document.createElement("td");
714+
const input = document.createElement("input");
715+
input.type = "radio";
716+
input.name = "account-user-controls-selected-device";
717+
input.value = choice.selectionKey;
718+
input.dataset.accountUserControlsSelectedDevice = choice.selectionKey;
719+
input.checked = this.selectedInputDevice?.selectionKey === choice.selectionKey;
720+
input.setAttribute("aria-label", `Select ${choice.label}`);
721+
cell.append(input);
722+
return cell;
723+
}
724+
723725
readonlyNormalizedSummary(mapping) {
724726
if (physicalInputIsAnalog(mapping.physicalInput)) {
725727
const negative = normalizeText(mapping.negativeNormalizedInput) || "Unassigned";
@@ -782,12 +784,14 @@ export class AccountUserControlsPage {
782784
row.dataset.accountUserControlsDefaultProfile = family;
783785
if (family === "Keyboard") {
784786
row.append(
787+
this.selectedDeviceCell(this.defaultSelectionChoice(family)),
785788
tableCell("Default Profile"),
786789
tableCell(`${profile.inputMappings.length} Physical Inputs`),
787790
tableCell(this.profileInputSummary(profile)),
788791
);
789792
} else {
790793
row.append(
794+
this.selectedDeviceCell(this.defaultSelectionChoice(family)),
791795
tableCell("Default Profile"),
792796
tableCell(`${profile.inputMappings.length} Physical Inputs`),
793797
tableCell(this.profileInputSummary(profile)),
@@ -805,6 +809,22 @@ export class AccountUserControlsPage {
805809
return row;
806810
}
807811

812+
renderDetectedDeviceRow(device) {
813+
const row = document.createElement("tr");
814+
row.dataset.accountUserControlsDetectedDevice = device.controllerId;
815+
row.append(
816+
this.selectedDeviceCell(this.detectedDeviceSelectionChoice(device)),
817+
tableCell(device.label),
818+
tableCell(`${device.inputs.length} Detected Inputs`),
819+
tableCell("Create my profile to customize"),
820+
tableCell("N/A"),
821+
tableCell("N/A"),
822+
tableCell("N/A"),
823+
tableCell("Detected"),
824+
);
825+
return row;
826+
}
827+
808828
profileInputSummary(profile) {
809829
const assigned = profile.inputMappings.reduce((count, mapping) => {
810830
if (!physicalInputIsAnalog(mapping.physicalInput)) {
@@ -845,12 +865,14 @@ export class AccountUserControlsPage {
845865
const family = this.profileListFamily(profile);
846866
if (family === "Keyboard") {
847867
row.append(
868+
this.selectedDeviceCell(this.profileSelectionChoice(profile)),
848869
tableCell(profile.controllerName),
849870
tableCell(`${profile.inputMappings.length} Physical Inputs`),
850871
tableCell(this.profileInputSummary(profile)),
851872
);
852873
} else {
853874
row.append(
875+
this.selectedDeviceCell(this.profileSelectionChoice(profile)),
854876
tableCell(profile.controllerName),
855877
tableCell(`${profile.inputMappings.length} Physical Inputs`),
856878
tableCell(this.profileInputSummary(profile)),
@@ -1006,13 +1028,15 @@ export class AccountUserControlsPage {
10061028
actions.append(group);
10071029
if (family === "Keyboard") {
10081030
row.append(
1031+
tableCell("Editing"),
10091032
controllerCell,
10101033
tableCell(`${profile.inputMappings.length} Physical Inputs`),
10111034
tableCell(this.profileInputSummary(profile)),
10121035
actions,
10131036
);
10141037
} else {
10151038
row.append(
1039+
tableCell("Editing"),
10161040
controllerCell,
10171041
tableCell(`${profile.inputMappings.length} Physical Inputs`),
10181042
tableCell(this.profileInputSummary(profile)),
@@ -1234,6 +1258,14 @@ export class AccountUserControlsPage {
12341258
if (target.isConnected) {
12351259
target.value = binding;
12361260
target.focus();
1261+
if (deviceType === "Mouse" && event.type === "mousedown") {
1262+
target.dataset.accountUserControlsSuppressCaptureClick = "true";
1263+
window.setTimeout(() => {
1264+
if (target.isConnected) {
1265+
delete target.dataset.accountUserControlsSuppressCaptureClick;
1266+
}
1267+
}, INPUT_CAPTURE_CLICK_SUPPRESSION_MS);
1268+
}
12371269
}
12381270
this.setStatus(`Captured ${binding}. Save the profile to keep it.`);
12391271
};
@@ -1289,6 +1321,11 @@ export class AccountUserControlsPage {
12891321
? event.target.closest("[data-account-user-controls-physical-input]")
12901322
: null;
12911323
if (physicalInputTarget instanceof HTMLInputElement) {
1324+
if (physicalInputTarget.dataset.accountUserControlsSuppressCaptureClick === "true") {
1325+
event.preventDefault();
1326+
event.stopPropagation();
1327+
return;
1328+
}
12921329
this.beginPhysicalInputCapture(physicalInputTarget);
12931330
return;
12941331
}
@@ -1328,7 +1365,14 @@ export class AccountUserControlsPage {
13281365
}
13291366
}
13301367

1331-
handleListChange() {
1368+
handleListChange(event) {
1369+
const selectedDeviceTarget = event.target instanceof Element
1370+
? event.target.closest("[data-account-user-controls-selected-device]")
1371+
: null;
1372+
if (selectedDeviceTarget instanceof HTMLInputElement) {
1373+
this.handleSelectedInputDeviceChange(event);
1374+
return;
1375+
}
13321376
this.setStatus("Unsaved user control profile changes.");
13331377
}
13341378

0 commit comments

Comments
 (0)