Skip to content

Commit

Permalink
feat(help/input_event_bindings): Simplify input event binding help di…
Browse files Browse the repository at this point in the history
…splay

Previously, optional modifiers in a binding like
"alt?+control?+shift+keya" would result in 4 separate help entries for
each of the 4 combinations.

With this change, the optional modifiers are excluded altogether, as
they already were from the binding tooltips, resulting in a single help
entry for "shift+keya".

Additionally, patterns like "shift+key[a-z]" -> "tool-[A-Z]" and
"[1-9]" -> "toggle-layer-[1-9]" resulted in each binding being displayed
separately.  With this change, the pattern is recognized and displayed
as a single entry.

These changes together make the help display much, much more concise.
  • Loading branch information
jbms committed Feb 28, 2025
1 parent a647623 commit 9b7a239
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 41 deletions.
138 changes: 98 additions & 40 deletions src/help/input_event_bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ import {
import type { GlobalToolBinder } from "#src/ui/tool.js";
import { animationFrameDebounce } from "#src/util/animation_frame_debounce.js";
import { removeChildren } from "#src/util/dom.js";
import type { EventActionMap } from "#src/util/event_action_map.js";
import {
friendlyEventIdentifier,
type EventActionMap,
} from "#src/util/event_action_map.js";
import { emptyToUndefined } from "#src/util/json.js";

declare let NEUROGLANCER_BUILD_INFO:
Expand Down Expand Up @@ -74,6 +77,99 @@ export class HelpPanelState {
}
}

interface BindingList {
label: string;
entries: Map<string, string>;
}

function collectBindings(
bindings: Iterable<[string, EventActionMap]>,
): Map<EventActionMap, BindingList> {
const uniqueMaps = new Map<EventActionMap, BindingList>();
function addEntries(eventMap: EventActionMap, entries: Map<string, string>) {
for (const parent of eventMap.parents) {
if (parent.label !== undefined) {
addMap(parent.label, parent);
} else {
addEntries(parent, entries);
}
}
for (const [event, eventAction] of eventMap.bindings.entries()) {
entries.set(
friendlyEventIdentifier(eventAction.originalEventIdentifier ?? event),
eventAction.action,
);
}
}

function simplifyEntries(entries: Map<string, string>) {
const identifierMap = new Map<string, [string, string]>();

function increment(x: string, i: number) {
return (
x.slice(0, -1) + String.fromCharCode(x.charCodeAt(x.length - 1) + i)
);
}

function makeRange(x: string, count: number) {
const start = x.slice(-1);
const end = increment(start, count - 1);
return x.slice(0, -1) + "[" + start + "-" + end + "]";
}

for (const [identifier, action] of entries) {
// Check for a-z
if (
(identifier.endsWith("a") && action.toLowerCase().endsWith("a")) ||
(identifier.endsWith("1") && action.endsWith("1"))
) {
for (let i = 1; ; ++i) {
const otherIdentifier = increment(identifier, i);
const otherAction = increment(action, i);
if (entries.get(otherIdentifier) === otherAction) {
entries.delete(otherIdentifier);
continue;
}

if (i !== 1) {
identifierMap.set(identifier, [
makeRange(identifier, i),
makeRange(action, i),
]);
}
break;
}
}
}

const newEntries = new Map<string, string>();
for (let [identifier, action] of entries) {
const remapped = identifierMap.get(identifier);
[identifier, action] = remapped ?? [identifier, action];
newEntries.set(identifier, action);
}
return newEntries;
}

function addMap(label: string, map: EventActionMap) {
if (uniqueMaps.has(map)) {
return;
}
const list: BindingList = {
label,
entries: new Map(),
};
addEntries(map, list.entries);
list.entries = simplifyEntries(list.entries);
uniqueMaps.set(map, list);
}
for (const [label, eventMap] of bindings) {
addMap(label, eventMap);
}

return uniqueMaps;
}

export class InputEventBindingHelpDialog extends SidePanel {
scroll = document.createElement("div");

Expand Down Expand Up @@ -133,45 +229,7 @@ export class InputEventBindingHelpDialog extends SidePanel {
scroll.appendChild(buildInfoElement);
}

interface BindingList {
label: string;
entries: Map<string, string>;
}

const uniqueMaps = new Map<EventActionMap, BindingList>();
function addEntries(
eventMap: EventActionMap,
entries: Map<string, string>,
) {
for (const parent of eventMap.parents) {
if (parent.label !== undefined) {
addMap(parent.label, parent);
} else {
addEntries(parent, entries);
}
}
for (const [event, eventAction] of eventMap.bindings.entries()) {
const firstColon = event.indexOf(":");
const suffix = event.substring(firstColon + 1);
entries.set(suffix, eventAction.action);
}
}

function addMap(label: string, map: EventActionMap) {
if (uniqueMaps.has(map)) {
return;
}
const list: BindingList = {
label,
entries: new Map(),
};
addEntries(map, list.entries);
uniqueMaps.set(map, list);
}

for (const [label, eventMap] of bindings) {
addMap(label, eventMap);
}
const uniqueMaps = collectBindings(bindings);

const addGroup = (title: string, entries: Iterable<[string, string]>) => {
const header = document.createElement("h2");
Expand Down
2 changes: 1 addition & 1 deletion src/util/event_action_map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ export function normalizeEventAction(
}

// Strips the phase and optional modifiers.
function friendlyEventIdentifier(identifier: string): string {
export function friendlyEventIdentifier(identifier: string): string {
identifier = identifier.replace(
/^(?:at|bubble|capture)|(?:(?:shift|control|alt|meta)\?\+)/g,
"",
Expand Down

0 comments on commit 9b7a239

Please sign in to comment.