Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(shortcuts): Improve shortcut registry documentation & style #8598

Merged
merged 3 commits into from
Oct 2, 2024
Merged
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 96 additions & 33 deletions core/shortcut_registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,31 +45,27 @@ export class ShortcutRegistry {
* Registers a keyboard shortcut.
*
* @param shortcut The shortcut for this key code.
* @param opt_allowOverrides True to prevent a warning when overriding an
* @param allowOverrides True to prevent a warning when overriding an
* already registered item.
* @throws {Error} if a shortcut with the same name already exists.
*/
register(shortcut: KeyboardShortcut, opt_allowOverrides?: boolean) {
register(shortcut: KeyboardShortcut, allowOverrides?: boolean) {
const registeredShortcut = this.shortcuts.get(shortcut.name);
if (registeredShortcut && !opt_allowOverrides) {
if (registeredShortcut && !allowOverrides) {
throw new Error(`Shortcut named "${shortcut.name}" already exists.`);
}
this.shortcuts.set(shortcut.name, shortcut);

const keyCodes = shortcut.keyCodes;
if (keyCodes && keyCodes.length > 0) {
for (let i = 0; i < keyCodes.length; i++) {
this.addKeyMapping(
keyCodes[i],
shortcut.name,
!!shortcut.allowCollision,
);
if (keyCodes?.length) {
for (const keyCode of keyCodes) {
this.addKeyMapping(keyCode, shortcut.name, !!shortcut.allowCollision);
}
}
}

/**
* Unregisters a keyboard shortcut registered with the given key code. This
* Unregisters a keyboard shortcut registered with the given name. This
* will also remove any key mappings that reference this shortcut.
*
* @param shortcutName The name of the shortcut to unregister.
Expand All @@ -92,27 +88,34 @@ export class ShortcutRegistry {
/**
* Adds a mapping between a keycode and a keyboard shortcut.
*
* Normally only one shortcut can be mapped to any given keycode,
* but setting allowCollisions to true allows a keyboard to be
* mapped to multiple shortcuts. In that case, when onKeyDown is
* called with the given keystroke, it will process the mapped
* shortcuts in reverse order, from the most- to least-recently
* mapped).
*
* @param keyCode The key code for the keyboard shortcut. If registering a key
* code with a modifier (ex: ctrl+c) use
* ShortcutRegistry.registry.createSerializedKey;
* @param shortcutName The name of the shortcut to execute when the given
* keycode is pressed.
* @param opt_allowCollision True to prevent an error when adding a shortcut
* @param allowCollision True to prevent an error when adding a shortcut
* to a key that is already mapped to a shortcut.
* @throws {Error} if the given key code is already mapped to a shortcut.
*/
addKeyMapping(
keyCode: string | number | KeyCodes,
shortcutName: string,
opt_allowCollision?: boolean,
allowCollision?: boolean,
) {
keyCode = `${keyCode}`;
const shortcutNames = this.keyMap.get(keyCode);
if (shortcutNames && !opt_allowCollision) {
if (shortcutNames && !allowCollision) {
throw new Error(
`Shortcut named "${shortcutName}" collides with shortcuts "${shortcutNames}"`,
);
} else if (shortcutNames && opt_allowCollision) {
} else if (shortcutNames && allowCollision) {
shortcutNames.unshift(shortcutName);
} else {
this.keyMap.set(keyCode, [shortcutName]);
Expand All @@ -127,19 +130,19 @@ export class ShortcutRegistry {
* ShortcutRegistry.registry.createSerializedKey;
* @param shortcutName The name of the shortcut to execute when the given
* keycode is pressed.
* @param opt_quiet True to not console warn when there is no shortcut to
* @param quiet True to not console warn when there is no shortcut to
* remove.
* @returns True if a key mapping was removed, false otherwise.
*/
removeKeyMapping(
keyCode: string,
shortcutName: string,
opt_quiet?: boolean,
quiet?: boolean,
): boolean {
const shortcutNames = this.keyMap.get(keyCode);

if (!shortcutNames) {
if (!opt_quiet) {
if (!quiet) {
console.warn(
`No keyboard shortcut named "${shortcutName}" registered with key code "${keyCode}"`,
);
Expand All @@ -155,7 +158,7 @@ export class ShortcutRegistry {
}
return true;
}
if (!opt_quiet) {
if (!quiet) {
console.warn(
`No keyboard shortcut named "${shortcutName}" registered with key code "${keyCode}"`,
);
Expand All @@ -172,7 +175,7 @@ export class ShortcutRegistry {
*/
removeAllKeyMappings(shortcutName: string) {
for (const keyCode of this.keyMap.keys()) {
this.removeKeyMapping(keyCode, shortcutName, true);
this.removeKeyMapping(keyCode, shortcutName, /* quiet= */ true);
}
}

Expand Down Expand Up @@ -219,24 +222,37 @@ export class ShortcutRegistry {
/**
* Handles key down events.
*
* - Any `KeyboardShortcut`(s) mapped to the keycodes that cause
* event `e` to be fired will be processed, in order from least-
* to most-recently registered.
* - If the shortcut's `preconditionFn` exists it will be called.
* If `preconditionFn` returns false the shortcut's `callback`
* function will be skipped. Processing will continue with the
* next shortcut, if any.
* - The shortcut's `callback` function will then be called. If it
* returns true, processing will terminate and `onKeyDown` will
* return true. If it returns false, processing will continue
cpcallen marked this conversation as resolved.
Show resolved Hide resolved
* with with the next shortcut, if any.
* - If all registered shortcuts for the given keycode have been
* processed without any having returned true, `onKeyDown` will
* return false.
*
* @param workspace The main workspace where the event was captured.
* @param e The key down event.
* @returns True if the event was handled, false otherwise.
*/
onKeyDown(workspace: WorkspaceSvg, e: KeyboardEvent): boolean {
const key = this.serializeKeyEvent_(e);
const shortcutNames = this.getShortcutNamesByKeyCode(key);
if (!shortcutNames) {
return false;
}
for (let i = 0, shortcutName; (shortcutName = shortcutNames[i]); i++) {
if (!shortcutNames) return false;
for (const shortcutName of shortcutNames) {
const shortcut = this.shortcuts.get(shortcutName);
if (!shortcut?.preconditionFn || shortcut?.preconditionFn(workspace)) {
// If the key has been handled, stop processing shortcuts.
if (shortcut?.callback && shortcut?.callback(workspace, e, shortcut)) {
return true;
}
if (!shortcut ||
(shortcut.preconditionFn && !shortcut.preconditionFn(workspace))) {
continue;
}
// If the key has been handled, stop processing shortcuts.
if (shortcut.callback?.(workspace, e, shortcut)) return true;
}
return false;
}
Expand Down Expand Up @@ -301,7 +317,7 @@ export class ShortcutRegistry {
* @throws {Error} if the modifier is not in the valid modifiers list.
*/
private checkModifiers_(modifiers: KeyCodes[]) {
for (let i = 0, modifier; (modifier = modifiers[i]); i++) {
for (const modifier of modifiers) {
if (!(modifier in ShortcutRegistry.modifierKeys)) {
throw new Error(modifier + ' is not a valid modifier key.');
}
Expand All @@ -313,7 +329,7 @@ export class ShortcutRegistry {
*
* @param keyCode Number code representing the key.
* @param modifiers List of modifier key codes to be used with the key. All
* valid modifiers can be found in the ShortcutRegistry.modifierKeys.
* valid modifiers can be found in the `ShortcutRegistry.modifierKeys`.
* @returns The serialized key code for the given modifiers and key.
*/
createSerializedKey(keyCode: number, modifiers: KeyCodes[] | null): string {
Expand Down Expand Up @@ -344,12 +360,59 @@ export class ShortcutRegistry {
}

export namespace ShortcutRegistry {
/** Interface defining a keyboard shortcut. */
export interface KeyboardShortcut {
callback?: (p1: WorkspaceSvg, p2: Event, p3: KeyboardShortcut) => boolean;
/**
* The function to be called when the shorctut is invoked.
*
* @param workspace The `WorkspaceSvg` when the shortcut was
* invoked.
* @param e The event that caused the shortcut to be activated.
* @param shortcut The `KeyboardShortcut` that was activated
* (i.e., the one this callback is attached to).
* @returns Returning true ends processing of the invoked keycode.
* Returning false causes processing to continue with the
* next-most-recently registered shortcut for the invoked
* keycode.
*/
callback?: (
workspace: WorkspaceSvg,
e: Event,
shortcut: KeyboardShortcut,
) => boolean;

/** The name of the shortcut. Should be unique. */
name: string;
preconditionFn?: (p1: WorkspaceSvg) => boolean;

/**
* A function to be called when the shortcut is invoked, before
* calling `callback`, to decide if this shortcut is applicable in
* the current situation.
*
* @param workspace The `WorkspaceSvg` where the shortcut was
* invoked.
* @returns True iff `callback` function should be called.
*/
preconditionFn?: (workspace: WorkspaceSvg) => boolean;

/** Optional arbitray extra data attached to the shortcut. */
metadata?: object;

/**
* Optional list of key codes to be bound (via
* ShortcutRegistry.prototype.addKeyMapping) to this shortcut.
*/
keyCodes?: (number | string)[];

/**
* Value of `allowCollision` to pass to `addKeyMapping` when
* binding this shortcut's `.keyCodes` (if any).
*
* N.B.: this is only used for binding keycodes at the time this
* shortcut is initially registered, not for any subsequent
* `addKeyMapping` calls that happen to reference this shortcut's
* name.
*/
allowCollision?: boolean;
}

Expand Down
Loading