mirror of https://github.com/vizality/vizality
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.
337 lines
11 KiB
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);
|
|
}
|
|
}
|
|
}
|