You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
vizality/renderer/src/api/Keybinds.js

337 lines
11 KiB

/**
* The keybinds API is meant for registering keyboard shortcuts to perform some task.
* Includes options to activate keybinds both globally (while the app is not focused) and
* locally (while the app is focused).
* @module Keybinds
* @memberof API
* @namespace API.Keybinds
* @version 1.0.0
*/
/**
* Vizality keybind object.
* @typedef VizalityKeybind
* @property {string} [id] Keybind ID
* @property {string} shortcut Keyboard shortcut
* @property {Function} executor Keybind executor
* @property {object} [options] Keybind options
* @property {boolean} [options.blurred=false] Whether the keybind should activate while Discord is unfocused
* @property {boolean} [options.focused=true] Whether the keybind should activate while Discord is focused
* @property {boolean} [options.keydown=false] Whether the keybind should activate on keydown
* @property {boolean} [options.keyup=true] Whether the keybind should activate on keyup
* @property {Array<Array<number>>} keyCode Matrix of keycodes. This property is set automatically based on the provided shortcut.
* @property {string} eventId Keybind event ID. This property is set automatically.
* @property {string} caller Addon ID of keybind registrar. This property is set automatically.
*/
import { assertString, toSnakeCase } from '@vizality/util/string';
import { assertObject } from '@vizality/util/object';
import { getCaller } from '@vizality/util/file';
import { getModule } from '@vizality/webpack';
import { Events } from '@vizality/constants';
import { API } from '@vizality/entities';
const discordUtils = DiscordNative?.nativeModules?.requireModule('discord_utils');
/**
* All currently registered keybinds.
* Accessed with `getAllKeybinds` below.
*/
let keybinds = [];
/**
* @extends API
* @extends Events
*/
export default class Keybinds extends API {
/**
* Shuts down the API, removing all listeners and stored objects.
*/
stop () {
this.unregisterAllKeybinds();
this.removeAllListeners();
delete vizality.api.keybinds;
}
/**
* Registers a keybind.
* @param {VizalityKeybind} keybind Keybind to register
* @emits Keybinds#Events.VIZALITY_KEYBIND_ADD
*/
registerKeybind (keybind) {
try {
assertObject(keybind);
keybind.caller = getCaller();
keybind.id = keybind.id || `${toSnakeCase(keybind.caller?.id).toUpperCase()}_KEYBIND_${this.getKeybindsByCaller(keybind.caller?.id)?.length + 1 || '1'}`;
assertString(keybind.id);
if (this.isKeybind(keybind.id)) {
throw new Error(`Keybind "${keybind.id}" is already registered!`);
}
if (!keybind.shortcut) {
throw new Error('Keybind must contain a shortcut!');
}
assertString(keybind.shortcut);
if (!keybind.executor) {
throw new Error('Keybind must contain an executor!');
}
if (typeof keybind.executor !== 'function') {
throw new TypeError('Keybind executor must be a function!');
}
const options = {
blurred: false,
focused: true,
keydown: false,
keyup: true
};
// Just assigning the event ID a randomly large number
keybind.eventId = Math.floor(100000 + Math.random() * 900000);
keybind.options = keybind.options || options;
assertObject(keybind.options);
keybind.shortcut = keybind.shortcut.toLowerCase();
if (this.getKeybindByShortcut(keybind.shortcut)) {
throw new TypeError(`Keybind shortcut "${keybind.shortcut} is already in use!`);
}
keybind.keyCode = this.shortcutToKeyCode(keybind.shortcut);
// Utilize Discord's internal keybind registrar
discordUtils.inputEventRegister(keybind.eventId, keybind.keyCode, keybind.executor, keybind.options);
keybinds.push(keybind);
this.emit(Events.VIZALITY_KEYBIND_ADD, keybind.id);
} catch (err) {
return this.error(this._labels.concat('registerKeybind'), err);
}
}
/**
* Invokes a keybind executor.
* @param {string} keybindId Keybind ID
*/
async invokeKeybind (keybindId) {
try {
assertString(keybindId);
if (!this.isKeybind(keybindId)) {
throw new Error(`Keybind "${keybindId}" could not be found!`);
}
try {
await this.getKeybindById(keybindId).executor();
} catch (err) {
return this.error(this._labels.concat('invokeKeybind'), `There was a problem invoking keybind "${keybindId}" executor!`, err);
}
} catch (err) {
return this.error(this._labels.concat('invokeKeybind'), err);
}
}
/**
* Changes a keybind.
* @param {string} keybindId ID of the keybind to unregister
* @param {string} newShortcut New shortcut to bind
*/
changeKeybindShortcut (keybindId, newShortcut) {
try {
assertString(keybindId);
assertString(newShortcut);
const keybind = this.getKeybindById(keybindId);
if (!keybind) {
throw new Error(`Keybind "${keybindId}" is not registered!`);
}
this.unregisterKeybind(keybindId);
keybind.shortcut = newShortcut;
this.registerKeybind(keybind);
} catch (err) {
return this.error(this._labels.concat('changeKeybindShortcut'), err);
}
}
/**
* Checks if a keybind is registered.
* @param {string} keybindIdOrShortcut Keybind ID or keybind shortcut
* @returns {boolean} Whether a keybind with a given ID or shortcut is registered
*/
isKeybind (keybindIdOrShortcut) {
try {
assertString(keybindIdOrShortcut);
return keybinds.some(keybind => keybind.id === keybindIdOrShortcut || keybind.shortcut === keybindIdOrShortcut);
} catch (err) {
return this.error(this._labels.concat('isKeybind'), err);
}
}
/**
* Gets the first keybind found matching a given filter.
* @param {Function} filter Function to use to filter keybinds by
* @returns {object|null} Keybind matching a given filter
*/
getKeybind (filter) {
try {
if (!filter?.length) return null;
return keybinds.find(filter);
} catch (err) {
return this.error(this._labels.concat('getKeybind'), err);
}
}
/**
* Gets a keybind matching a given ID.
* @param {string} keybindId Keybind ID
* @returns {object|null} Keybind matching a given ID
*/
getKeybindById (keybindId) {
try {
assertString(keybindId);
return keybinds.find(keybind => keybind.id === keybindId);
} catch (err) {
return this.error(this._labels.concat('getKeybindById'), err);
}
}
/**
* Gets a keybind matching a given shortcut.
* @param {string} shortcut Keybind shortcut
* @returns {object|null} Keybind matching a given shortcut
*/
getKeybindByShortcut (shortcut) {
try {
assertString(shortcut);
return keybinds.find(keybind => keybind.shortcut === shortcut);
} catch (err) {
return this.error(this._labels.concat('getKeybindByShortcut'), err);
}
}
/**
* Gets all keybinds found matching a given filter.
* @param {Function} filter Function to use to filter keybinds by
* @returns {Array<object|null>} Keybinds matching a given filter
*/
getKeybinds (filter) {
try {
if (!filter?.length) return null;
return keybinds.filter(filter);
} catch (err) {
return this.error(this._labels.concat('getKeybinds'), err);
}
}
/**
* Gets all keybinds matching a given caller.
* @param {string} addonId Addon ID
* @returns {Array<object|null>} Keybinds matching a given caller
*/
getKeybindsByCaller (addonId) {
try {
assertString(addonId);
return keybinds.filter(keybind => keybind.caller?.id === addonId);
} catch (err) {
return this.error(this._labels.concat('getKeybindsByCaller'), err);
}
}
/**
* Gets all keybinds.
* @returns {Array<object|null>} All keybinds
*/
getAllKeybinds () {
try {
return keybinds;
} catch (err) {
return this.error(this._labels.concat('getAllKeybinds'), err);
}
}
/**
* Converts a key or modifier into a usable keycode.
* @param {string} key Key or modifier
* @returns {key} Key code (event.which)
*/
getVirtualKeyCode (key) {
assertString(key);
const os = DiscordNative?.process?.platform;
const { keyToCode } = getModule('keyToCode');
if (os === 'linux') {
if (key === 'ctrl') key = 'left ctrl';
if (key === 'alt') key = 'left alt';
if (key === 'shift') key = 'left shift';
}
if (key === 'rctrl') key = 'right ctrl';
if (key === 'ralt') key = 'right alt';
if (key === 'rshift') key = 'right shift';
return keyToCode(key);
}
/**
* Converts a string shortcut matrix of keycodes.
* @see {@link https://github.com/ianstormtaylor/is-hotkey}
* @param {string} shortcut Keybind shortcut
* @returns {Array<Array<number>>} Returns a matrix of keycodes
*/
shortcutToKeyCode (shortcut) {
try {
assertString(shortcut);
const keysHolder = [];
const keys = shortcut.split('+');
for (let key of keys) {
key = this.getVirtualKeyCode(key);
keysHolder.push([ 0, key ]);
}
return keysHolder;
} catch (err) {
return this.error(this._labels.concat('shortcutToKeyCode'), err);
}
}
/**
* Unregisters a keybind.
* @param {string} keybindId Keybind ID
* @emits Keybinds#Events.VIZALITY_KEYBIND_REMOVE
*/
unregisterKeybind (keybindId) {
try {
assertString(keybindId);
const keybind = this.getKeybindById(keybindId);
if (!keybind) {
throw new Error(`Keybind "${keybindId}" is not registered, so it cannot be unregistered!`);
}
discordUtils.inputEventUnregister(keybind.eventId);
keybinds = this.getKeybinds(keybind => keybind.id !== keybindId);
this.emit(Events.VIZALITY_KEYBIND_REMOVE, keybindId);
} catch (err) {
return this.error(this._labels.concat('unregisterKeybind'), err);
}
}
/**
* Unregisters all keybinds matching a given caller.
* @param {string} addonId Addon ID
* @emits Keybinds#Events.VIZALITY_KEYBIND_REMOVE_ALL_BY_CALLER
*/
unregisterKeybindsByCaller (addonId) {
try {
assertString(addonId);
keybinds = keybinds.filter(keybind => keybind.caller?.id !== addonId);
this.emit(Events.VIZALITY_KEYBIND_REMOVE_ALL_BY_CALLER, addonId);
} catch (err) {
return this.error(this._labels.concat('unregisterKeybindsByCaller'), err);
}
}
/**
* Unregisters all keybinds.
* @emits Keybinds#Events.VIZALITY_KEYBIND_REMOVE_ALL
*/
unregisterAllKeybinds () {
try {
for (const keybind of keybinds) {
this.unregisterKeybind(keybind.id);
}
/*
* They should already be cleared out from the above unregister function, but let's
* make sure.
*/
keybinds = [];
this.emit(Events.VIZALITY_KEYBIND_REMOVE_ALL);
} catch (err) {
return this.error(this._labels.concat('unregisterAllKeybinds'), err);
}
}
}