2020-06-29 19:53:49 +02:00
|
|
|
import objectHash from 'object-hash';
|
2019-08-13 21:36:46 +02:00
|
|
|
import log from './../log.js';
|
2021-07-21 12:58:15 +02:00
|
|
|
import { hotkeys } from '../utils';
|
2019-08-13 21:36:46 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
*
|
|
|
|
|
* @typedef {Object} HotkeyDefinition
|
|
|
|
|
* @property {String} commandName - Command to call
|
2020-06-29 19:53:49 +02:00
|
|
|
* @property {Object} commandOptions - Command options
|
2019-08-13 21:36:46 +02:00
|
|
|
* @property {String} label - Display name for hotkey
|
|
|
|
|
* @property {String[]} keys - Keys to bind; Follows Mousetrap.js binding syntax
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
export class HotkeysManager {
|
2020-02-12 21:35:04 +01:00
|
|
|
constructor(commandsManager, servicesManager) {
|
2019-08-13 21:36:46 +02:00
|
|
|
this.hotkeyDefinitions = {};
|
|
|
|
|
this.hotkeyDefaults = [];
|
|
|
|
|
this.isEnabled = true;
|
|
|
|
|
|
|
|
|
|
if (!commandsManager) {
|
2021-03-01 16:52:54 +01:00
|
|
|
throw new Error(
|
|
|
|
|
'HotkeysManager instantiated without a commandsManager. Hotkeys will be unable to find and run commands.'
|
2019-08-13 21:36:46 +02:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-12 21:35:04 +01:00
|
|
|
this._servicesManager = servicesManager;
|
2019-08-13 21:36:46 +02:00
|
|
|
this._commandsManager = commandsManager;
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-12 21:35:04 +01:00
|
|
|
/**
|
|
|
|
|
* Exposes Mousetrap.js's `.record` method, added by the record plugin.
|
|
|
|
|
*
|
|
|
|
|
* @param {*} event
|
|
|
|
|
*/
|
|
|
|
|
record(event) {
|
|
|
|
|
return hotkeys.record(event);
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-13 21:36:46 +02:00
|
|
|
/**
|
|
|
|
|
* Disables all hotkeys. Hotkeys added while disabled will not listen for
|
|
|
|
|
* input.
|
|
|
|
|
*/
|
|
|
|
|
disable() {
|
|
|
|
|
this.isEnabled = false;
|
|
|
|
|
hotkeys.pause();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Enables all hotkeys.
|
|
|
|
|
*/
|
|
|
|
|
enable() {
|
|
|
|
|
this.isEnabled = true;
|
|
|
|
|
hotkeys.unpause();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2019-11-28 06:48:27 +01:00
|
|
|
* Registers a list of hotkeydefinitions.
|
2019-08-13 21:36:46 +02:00
|
|
|
*
|
2020-02-12 21:35:04 +01:00
|
|
|
* @param {HotkeyDefinition[] | Object} [hotkeyDefinitions=[]] Contains hotkeys definitions
|
2019-08-13 21:36:46 +02:00
|
|
|
*/
|
2020-02-12 21:35:04 +01:00
|
|
|
setHotkeys(hotkeyDefinitions = []) {
|
|
|
|
|
try {
|
2020-08-19 21:02:59 +02:00
|
|
|
const definitions = this.getValidDefinitions(hotkeyDefinitions);
|
2020-02-12 21:35:04 +01:00
|
|
|
definitions.forEach(definition => this.registerHotkeys(definition));
|
2021-08-19 14:05:33 +02:00
|
|
|
localStorage.setItem('hotkey-definitions', JSON.stringify(definitions));
|
2020-02-12 21:35:04 +01:00
|
|
|
} catch (error) {
|
|
|
|
|
const { UINotificationService } = this._servicesManager.services;
|
|
|
|
|
UINotificationService.show({
|
|
|
|
|
title: 'Hotkeys Manager',
|
2021-03-01 16:52:54 +01:00
|
|
|
message: 'Error while setting hotkeys',
|
2020-02-12 21:35:04 +01:00
|
|
|
type: 'error',
|
|
|
|
|
});
|
|
|
|
|
}
|
2019-11-28 06:48:27 +01:00
|
|
|
}
|
2019-08-13 21:36:46 +02:00
|
|
|
|
2019-11-28 06:48:27 +01:00
|
|
|
/**
|
|
|
|
|
* Set default hotkey bindings. These
|
|
|
|
|
* values are used in `this.restoreDefaultBindings`.
|
|
|
|
|
*
|
2020-02-12 21:35:04 +01:00
|
|
|
* @param {HotkeyDefinition[] | Object} [hotkeyDefinitions=[]] Contains hotkeys definitions
|
2019-11-28 06:48:27 +01:00
|
|
|
*/
|
2020-02-12 21:35:04 +01:00
|
|
|
setDefaultHotKeys(hotkeyDefinitions = []) {
|
2020-08-19 21:02:59 +02:00
|
|
|
const definitions = this.getValidDefinitions(hotkeyDefinitions);
|
2020-02-12 21:35:04 +01:00
|
|
|
this.hotkeyDefaults = definitions;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Take hotkey definitions that can be an array or object and make sure that it
|
|
|
|
|
* returns an array of hotkeys
|
|
|
|
|
*
|
|
|
|
|
* @param {HotkeyDefinition[] | Object} [hotkeyDefinitions=[]] Contains hotkeys definitions
|
|
|
|
|
*/
|
2020-08-19 21:02:59 +02:00
|
|
|
getValidDefinitions(hotkeyDefinitions) {
|
2019-11-28 06:48:27 +01:00
|
|
|
const definitions = Array.isArray(hotkeyDefinitions)
|
|
|
|
|
? [...hotkeyDefinitions]
|
|
|
|
|
: this._parseToArrayLike(hotkeyDefinitions);
|
|
|
|
|
|
2020-02-12 21:35:04 +01:00
|
|
|
return definitions;
|
2019-11-28 06:48:27 +01:00
|
|
|
}
|
|
|
|
|
|
2020-08-19 21:02:59 +02:00
|
|
|
/**
|
|
|
|
|
* Take hotkey definitions that can be an array and make sure that it
|
|
|
|
|
* returns an object of hotkeys definitions
|
|
|
|
|
*
|
|
|
|
|
* @param {HotkeyDefinition[]} [hotkeyDefinitions=[]] Contains hotkeys definitions
|
|
|
|
|
* @returns {Object}
|
|
|
|
|
*/
|
|
|
|
|
getValidHotkeyDefinitions(hotkeyDefinitions) {
|
|
|
|
|
const definitions = this.getValidDefinitions(hotkeyDefinitions);
|
|
|
|
|
const objectDefinitions = {};
|
|
|
|
|
definitions.forEach(definition => {
|
|
|
|
|
const { commandName, commandOptions } = definition;
|
|
|
|
|
const commandHash = objectHash({ commandName, commandOptions });
|
|
|
|
|
objectDefinitions[commandHash] = definition;
|
|
|
|
|
});
|
|
|
|
|
return objectDefinitions;
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-28 06:48:27 +01:00
|
|
|
/**
|
|
|
|
|
* It parses given object containing hotkeyDefinition to array like.
|
|
|
|
|
* Each property of given object will be mapped to an object of an array. And its property name will be the value of a property named as commandName
|
|
|
|
|
*
|
2020-02-12 21:35:04 +01:00
|
|
|
* @param {HotkeyDefinition[] | Object} [hotkeyDefinitions={}] Contains hotkeys definitions
|
2019-11-28 06:48:27 +01:00
|
|
|
* @returns {HotkeyDefinition[]}
|
|
|
|
|
*/
|
2020-02-12 21:35:04 +01:00
|
|
|
_parseToArrayLike(hotkeyDefinitionsObj = {}) {
|
2019-11-28 06:48:27 +01:00
|
|
|
const copy = { ...hotkeyDefinitionsObj };
|
|
|
|
|
return Object.entries(copy).map(entryValue =>
|
|
|
|
|
this._parseToHotKeyObj(entryValue[0], entryValue[1])
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Return HotkeyDefinition object like based on given property name and property value
|
|
|
|
|
* @param {string} propertyName property name of hotkey definition object
|
|
|
|
|
* @param {object} propertyValue property value of hotkey definition object
|
|
|
|
|
*
|
|
|
|
|
* @example
|
|
|
|
|
*
|
|
|
|
|
* const hotKeyObj = {hotKeyDefA: {keys:[],....}}
|
|
|
|
|
*
|
|
|
|
|
* const parsed = _parseToHotKeyObj(Object.keys(hotKeyDefA)[0], hotKeyObj[hotKeyDefA]);
|
|
|
|
|
* {
|
|
|
|
|
* commandName: hotKeyDefA,
|
|
|
|
|
* keys: [],
|
|
|
|
|
* ....
|
|
|
|
|
* }
|
|
|
|
|
*
|
|
|
|
|
*/
|
|
|
|
|
_parseToHotKeyObj(propertyName, propertyValue) {
|
|
|
|
|
return {
|
|
|
|
|
commandName: propertyName,
|
|
|
|
|
...propertyValue,
|
|
|
|
|
};
|
2019-08-13 21:36:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* (unbinds and) binds the specified command to one or more key combinations.
|
|
|
|
|
* When a hotkey combination is triggered, the command name and active contexts
|
|
|
|
|
* are used to locate the correct command to call.
|
|
|
|
|
*
|
2020-06-29 19:53:49 +02:00
|
|
|
* @param {HotkeyDefinition} command
|
2019-08-13 21:36:46 +02:00
|
|
|
* @param {String} extension
|
|
|
|
|
* @returns {undefined}
|
|
|
|
|
*/
|
2021-07-14 15:26:06 +02:00
|
|
|
registerHotkeys(
|
|
|
|
|
{ commandName, commandOptions = {}, keys, label, isEditable } = {},
|
|
|
|
|
extension
|
|
|
|
|
) {
|
2019-08-13 21:36:46 +02:00
|
|
|
if (!commandName) {
|
2021-03-01 16:52:54 +01:00
|
|
|
throw new Error(`No command was defined for hotkey "${keys}"`);
|
2019-08-13 21:36:46 +02:00
|
|
|
}
|
|
|
|
|
|
2020-06-29 19:53:49 +02:00
|
|
|
const commandHash = objectHash({ commandName, commandOptions });
|
2021-07-14 15:26:06 +02:00
|
|
|
const options = Object.keys(commandOptions).length
|
|
|
|
|
? JSON.stringify(commandOptions)
|
|
|
|
|
: 'no';
|
2020-06-29 19:53:49 +02:00
|
|
|
const previouslyRegisteredDefinition = this.hotkeyDefinitions[commandHash];
|
2019-08-13 21:36:46 +02:00
|
|
|
|
|
|
|
|
if (previouslyRegisteredDefinition) {
|
|
|
|
|
const previouslyRegisteredKeys = previouslyRegisteredDefinition.keys;
|
|
|
|
|
this._unbindHotkeys(commandName, previouslyRegisteredKeys);
|
2021-07-14 15:26:06 +02:00
|
|
|
log.info(
|
|
|
|
|
`[hotkeys] Unbinding ${commandName} with ${options} options from ${previouslyRegisteredKeys}`
|
|
|
|
|
);
|
2019-08-13 21:36:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set definition & bind
|
2021-07-14 15:26:06 +02:00
|
|
|
this.hotkeyDefinitions[commandHash] = {
|
|
|
|
|
commandName,
|
|
|
|
|
commandOptions,
|
|
|
|
|
keys,
|
|
|
|
|
label,
|
|
|
|
|
isEditable,
|
|
|
|
|
};
|
2020-06-29 19:53:49 +02:00
|
|
|
this._bindHotkeys(commandName, commandOptions, keys);
|
2021-07-14 15:26:06 +02:00
|
|
|
log.info(
|
|
|
|
|
`[hotkeys] Binding ${commandName} with ${options} options to ${keys}`
|
|
|
|
|
);
|
2019-08-13 21:36:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Uses most recent
|
|
|
|
|
*
|
|
|
|
|
* @returns {undefined}
|
|
|
|
|
*/
|
|
|
|
|
restoreDefaultBindings() {
|
|
|
|
|
this.setHotkeys(this.hotkeyDefaults);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
*/
|
|
|
|
|
destroy() {
|
|
|
|
|
this.hotkeyDefaults = [];
|
|
|
|
|
this.hotkeyDefinitions = {};
|
|
|
|
|
hotkeys.reset();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Binds one or more set of hotkey combinations for a given command
|
|
|
|
|
*
|
|
|
|
|
* @private
|
|
|
|
|
* @param {string} commandName - The name of the command to trigger when hotkeys are used
|
|
|
|
|
* @param {string[]} keys - One or more key combinations that should trigger command
|
|
|
|
|
* @returns {undefined}
|
|
|
|
|
*/
|
2020-06-29 19:53:49 +02:00
|
|
|
_bindHotkeys(commandName, commandOptions = {}, keys) {
|
2019-08-13 21:36:46 +02:00
|
|
|
const isKeyDefined = keys === '' || keys === undefined;
|
|
|
|
|
if (isKeyDefined) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const isKeyArray = keys instanceof Array;
|
2020-02-12 21:35:04 +01:00
|
|
|
const combinedKeys = isKeyArray ? keys.join('+') : keys;
|
2019-08-13 21:36:46 +02:00
|
|
|
|
2020-02-12 21:35:04 +01:00
|
|
|
hotkeys.bind(combinedKeys, evt => {
|
|
|
|
|
evt.preventDefault();
|
|
|
|
|
evt.stopPropagation();
|
2020-06-29 19:53:49 +02:00
|
|
|
this._commandsManager.runCommand(commandName, { evt, ...commandOptions });
|
2019-08-13 21:36:46 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* unbinds one or more set of hotkey combinations for a given command
|
|
|
|
|
*
|
|
|
|
|
* @private
|
|
|
|
|
* @param {string} commandName - The name of the previously bound command
|
|
|
|
|
* @param {string[]} keys - One or more sets of previously bound keys
|
|
|
|
|
* @returns {undefined}
|
|
|
|
|
*/
|
|
|
|
|
_unbindHotkeys(commandName, keys) {
|
|
|
|
|
const isKeyDefined = keys !== '' && keys !== undefined;
|
|
|
|
|
if (!isKeyDefined) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const isKeyArray = keys instanceof Array;
|
|
|
|
|
if (isKeyArray) {
|
2020-02-12 21:35:04 +01:00
|
|
|
const combinedKeys = keys.join('+');
|
|
|
|
|
this._unbindHotkeys(commandName, combinedKeys);
|
2019-08-13 21:36:46 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hotkeys.unbind(keys);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default HotkeysManager;
|
|
|
|
|
|
|
|
|
|
// Commands Contexts:
|
|
|
|
|
|
|
|
|
|
// --> Name and Priority
|
|
|
|
|
// GLOBAL: 0
|
|
|
|
|
// VIEWER::CORNERSTONE: 1
|
|
|
|
|
// VIEWER::VTK: 1
|