fix: Combined Hotkeys for special characters (#1233)
* fix: Combined Hotkeys for special characters * add record method to hotkey manager * fix record plugin * remove unused component * add record to modal props * rename record method * replace handlers to use hotkeyRecord * fix combined keys * change expected result count from 18 to 17 * autoformat * Remove duplicate test, that was testing the wrong things; fix label; update configs * Revert "Remove duplicate test, that was testing the wrong things; fix label; update configs" This reverts commit 4292f4fe67351962d61cae623b920dcbd87dd71d. * Fix the record plugin's registration * fix exposed record method usage * adding logging for info level items * Hotkey definitions don't need to be globally reactive; use localstorage/appconfig as sources of truth; not redux * Tidy up test * Remove unused code from UserPreferencesForm * Log info when we run a command * fix hotkey preference restore * use application configured hotkeys if there are no user preferred * Avoid logging circular ref * Fix callouts * Fix small issue with array * Fix langua issue after refactor and merge * Refactor on recordCurrentCombo as Rodrigo did before * Separating components in 2 files * WIP Refactor to simplify the user preferences and move into each form the save and controll functionalities * Remove context * Remove unused import * Initial work on Field treatment * Refactor General preferences * Small refactor removing type from HotkeyField * small update on style * Refactor and layout fixed * Make hotkeys preferences working with old hotkeys row * Move error handling out of hotkey row/input component * WIP custom form * Moving validation function to component * Exposing hotkeyRecord as it does not depend on HotkeyManager Class * Making hotkeyField as much detached possible from parent component * Small refactors * Refactor on user preferences * Clean up into the changes * Small fix to let save working * Style finish * move about docs into about folder * Fix double tap on single keys * Style refactor * Remove log * Fix log issues on unit tests * Fix unit test breaking on ohif/core index * Fixing hotkeys unpause unit test issue * Rename file to adopt lowercase * Rename file to adopt lowercase * Fixing callouts * Big refactor miving some of the components into viewer and creating small components into ohif/ui * Typo on folder name * Updating ohif ui docs * Remove comments * Fix binding of combo keys * Fix some cypress tests failures * Fixing onCancel button * Fixing e2e tests * Small style update * Fixing unit tests failing after fix issue * Remove some not used code * Remove left over after debug * Adding prevent default on hotkeys events * Fixinf existing hotkeys validator with 3 keys pressed * Exposing hotkeys as root level on ohif-core * Clean up * Exposing all availableLanguages with labels and fixing an issue on language switcher * Fixing e2e cypress tests * Preveinting some simple errors * Treating error once we try to set hotkey definitions * Adding ui notification on setHotkeys errors * Implementing a service queue request to hold until functions are implemented * Making sure toFixed is only called on Numbers Co-authored-by: Danny Brown <danny.ri.brown@gmail.com> Co-authored-by: Gustavo André Lelis <galelis@gmail.com>
This commit is contained in:
69 files changed
+1211
-1632
No files matched your search
@@ -1,4 +1,5 @@
|
||||
export default {
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
info: jest.fn(),
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import hotkeys from './hotkeys';
|
||||
import hotkeys from './../utils/hotkeys';
|
||||
import log from './../log.js';
|
||||
|
||||
/**
|
||||
@@ -11,7 +11,7 @@ import log from './../log.js';
|
||||
*/
|
||||
|
||||
export class HotkeysManager {
|
||||
constructor(commandsManager) {
|
||||
constructor(commandsManager, servicesManager) {
|
||||
this.hotkeyDefinitions = {};
|
||||
this.hotkeyDefaults = [];
|
||||
this.isEnabled = true;
|
||||
@@ -22,9 +22,19 @@ export class HotkeysManager {
|
||||
);
|
||||
}
|
||||
|
||||
this._servicesManager = servicesManager;
|
||||
this._commandsManager = commandsManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposes Mousetrap.js's `.record` method, added by the record plugin.
|
||||
*
|
||||
* @param {*} event
|
||||
*/
|
||||
record(event) {
|
||||
return hotkeys.record(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables all hotkeys. Hotkeys added while disabled will not listen for
|
||||
* input.
|
||||
@@ -45,38 +55,57 @@ export class HotkeysManager {
|
||||
/**
|
||||
* Registers a list of hotkeydefinitions.
|
||||
*
|
||||
* @param {HotkeyDefinition[] | Object} hotkeyDefinitions Contains hotkeys definitions
|
||||
* @param {HotkeyDefinition[] | Object} [hotkeyDefinitions=[]] Contains hotkeys definitions
|
||||
*/
|
||||
setHotkeys(hotkeyDefinitions) {
|
||||
const definitions = Array.isArray(hotkeyDefinitions)
|
||||
? [...hotkeyDefinitions]
|
||||
: this._parseToArrayLike(hotkeyDefinitions);
|
||||
setHotkeys(hotkeyDefinitions = []) {
|
||||
try {
|
||||
const definitions = this._getValidDefinitions(hotkeyDefinitions);
|
||||
|
||||
definitions.forEach(definition => this.registerHotkeys(definition));
|
||||
definitions.forEach(definition => this.registerHotkeys(definition));
|
||||
} catch (error) {
|
||||
const { UINotificationService } = this._servicesManager.services;
|
||||
UINotificationService.show({
|
||||
title: 'Hotkeys Manager',
|
||||
message: 'Erro while setting hotkeys',
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default hotkey bindings. These
|
||||
* values are used in `this.restoreDefaultBindings`.
|
||||
*
|
||||
* @param {HotkeyDefinition[] | Object} hotkeyDefinitions Contains hotkeys definitions
|
||||
* @param {HotkeyDefinition[] | Object} [hotkeyDefinitions=[]] Contains hotkeys definitions
|
||||
*/
|
||||
setDefaultHotKeys(hotkeyDefinitions) {
|
||||
setDefaultHotKeys(hotkeyDefinitions = []) {
|
||||
const definitions = this._getValidDefinitions(hotkeyDefinitions);
|
||||
|
||||
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
|
||||
*/
|
||||
_getValidDefinitions(hotkeyDefinitions) {
|
||||
const definitions = Array.isArray(hotkeyDefinitions)
|
||||
? [...hotkeyDefinitions]
|
||||
: this._parseToArrayLike(hotkeyDefinitions);
|
||||
|
||||
this.hotkeyDefaults = definitions;
|
||||
return definitions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @param {HotkeyDefinition[] | Object} hotkeyDefinitions Contains hotkeys definitions
|
||||
* @param {HotkeyDefinition[] | Object} [hotkeyDefinitions={}] Contains hotkeys definitions
|
||||
* @returns {HotkeyDefinition[]}
|
||||
*/
|
||||
_parseToArrayLike(hotkeyDefinitionsObj) {
|
||||
_parseToArrayLike(hotkeyDefinitionsObj = {}) {
|
||||
const copy = { ...hotkeyDefinitionsObj };
|
||||
return Object.entries(copy).map(entryValue =>
|
||||
this._parseToHotKeyObj(entryValue[0], entryValue[1])
|
||||
@@ -127,11 +156,13 @@ export class HotkeysManager {
|
||||
if (previouslyRegisteredDefinition) {
|
||||
const previouslyRegisteredKeys = previouslyRegisteredDefinition.keys;
|
||||
this._unbindHotkeys(commandName, previouslyRegisteredKeys);
|
||||
log.info(`Unbinding ${commandName} from ${previouslyRegisteredKeys}`);
|
||||
}
|
||||
|
||||
// Set definition & bind
|
||||
this.hotkeyDefinitions[commandName] = { keys, label };
|
||||
this._bindHotkeys(commandName, keys);
|
||||
log.info(`Binding ${commandName} to ${keys}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,12 +198,11 @@ export class HotkeysManager {
|
||||
}
|
||||
|
||||
const isKeyArray = keys instanceof Array;
|
||||
if (isKeyArray) {
|
||||
keys.forEach(key => this._bindHotkeys(commandName, key));
|
||||
return;
|
||||
}
|
||||
const combinedKeys = isKeyArray ? keys.join('+') : keys;
|
||||
|
||||
hotkeys.bind(keys, evt => {
|
||||
hotkeys.bind(combinedKeys, evt => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
this._commandsManager.runCommand(commandName, { evt });
|
||||
});
|
||||
}
|
||||
@@ -193,7 +223,8 @@ export class HotkeysManager {
|
||||
|
||||
const isKeyArray = keys instanceof Array;
|
||||
if (isKeyArray) {
|
||||
keys.forEach(key => this._unbindHotkeys(commandName, key));
|
||||
const combinedKeys = keys.join('+');
|
||||
this._unbindHotkeys(commandName, combinedKeys);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import CommandsManager from './CommandsManager.js';
|
||||
import HotkeysManager from './HotkeysManager.js';
|
||||
import hotkeys from './hotkeys';
|
||||
import hotkeys from './../utils/hotkeys';
|
||||
import log from './../log.js';
|
||||
|
||||
jest.mock('./CommandsManager.js');
|
||||
jest.mock('./hotkeys');
|
||||
jest.mock('./../utils/hotkeys');
|
||||
jest.mock('./../log.js');
|
||||
|
||||
describe('HotkeysManager', () => {
|
||||
@@ -60,7 +60,10 @@ describe('HotkeysManager', () => {
|
||||
});
|
||||
|
||||
describe('enable()', () => {
|
||||
beforeEach(() => hotkeys.unpause.mockClear());
|
||||
beforeEach(() => {
|
||||
hotkeys.unpause = jest.fn();
|
||||
hotkeys.unpause.mockClear();
|
||||
});
|
||||
|
||||
it('sets isEnabled property to true', () => {
|
||||
hotkeysManager.disable();
|
||||
@@ -139,20 +142,18 @@ describe('HotkeysManager', () => {
|
||||
expectedHotkeyDefinition
|
||||
);
|
||||
});
|
||||
it('calls hotkeys.bind for all keys in array', () => {
|
||||
const definition = { commandName: 'dance', keys: ['h', 'e', 'l', 'o'] };
|
||||
it('calls hotkeys.bind for the group of keys', () => {
|
||||
const definition = { commandName: 'dance', keys: ['shift', 'e'] };
|
||||
|
||||
hotkeysManager.registerHotkeys(definition);
|
||||
|
||||
expect(hotkeys.bind.mock.calls.length).toBe(definition.keys.length);
|
||||
definition.keys.forEach((key, i) =>
|
||||
expect(hotkeys.bind.mock.calls[i][0]).toBe(key)
|
||||
);
|
||||
expect(hotkeys.bind.mock.calls.length).toBe(1);
|
||||
expect(hotkeys.bind.mock.calls[0][0]).toBe('shift+e');
|
||||
});
|
||||
it('calls hotkeys.unbind if commandName was previously registered, for each previously registered set of keys', () => {
|
||||
const firstDefinition = {
|
||||
commandName: 'dance',
|
||||
keys: ['h', 'e', 'l', 'o'],
|
||||
keys: ['alt', 'e'],
|
||||
};
|
||||
const secondDefinition = { commandName: 'dance', keys: 'a' };
|
||||
|
||||
@@ -161,12 +162,8 @@ describe('HotkeysManager', () => {
|
||||
// Second call
|
||||
hotkeysManager.registerHotkeys(secondDefinition);
|
||||
|
||||
expect(hotkeys.unbind.mock.calls.length).toBe(
|
||||
firstDefinition.keys.length
|
||||
);
|
||||
firstDefinition.keys.forEach((key, i) =>
|
||||
expect(hotkeys.unbind.mock.calls[i][0]).toBe(key)
|
||||
);
|
||||
expect(hotkeys.unbind.mock.calls.length).toBe(1);
|
||||
expect(hotkeys.unbind.mock.calls[0][0]).toBe('alt+e');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
// Only imported in environment w/ `window`
|
||||
// So we need to mock these for tests
|
||||
import Mousetrap from 'mousetrap';
|
||||
import pausePlugin from 'mousetrap/plugins/pause/mousetrap-pause.js';
|
||||
import recordPlugin from 'mousetrap/plugins/record/mousetrap-record.js';
|
||||
|
||||
// import pausePlugin from './pausePlugin.js';
|
||||
// import recordPlugin from './recordPlugin.js';
|
||||
|
||||
// // // TODO: May need to bind these so Mousetrap = this in plugins;
|
||||
// pausePlugin(Mousetrap);
|
||||
// recordPlugin(Mousetrap);
|
||||
|
||||
// console.log(Mousetrap);
|
||||
// console.log(Object.keys(Mousetrap));
|
||||
|
||||
export default Mousetrap;
|
||||
@@ -18,7 +18,7 @@ import string from './string.js';
|
||||
import studies from './studies/';
|
||||
import ui from './ui';
|
||||
import user from './user.js';
|
||||
import utils from './utils/';
|
||||
import utils, { hotkeys } from './utils/';
|
||||
|
||||
import {
|
||||
UINotificationService,
|
||||
@@ -36,6 +36,7 @@ const OHIF = {
|
||||
ServicesManager,
|
||||
//
|
||||
utils,
|
||||
hotkeys,
|
||||
studies,
|
||||
redux,
|
||||
classes,
|
||||
@@ -68,6 +69,7 @@ export {
|
||||
ServicesManager,
|
||||
//
|
||||
utils,
|
||||
hotkeys,
|
||||
studies,
|
||||
redux,
|
||||
classes,
|
||||
|
||||
@@ -16,6 +16,7 @@ describe('Top level exports', () => {
|
||||
'MeasurementService',
|
||||
//
|
||||
'utils',
|
||||
'hotkeys',
|
||||
'studies',
|
||||
'redux',
|
||||
'classes',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const displayFunction = data => {
|
||||
let text = '';
|
||||
if (data.rAngle) {
|
||||
if (data.rAngle && !isNaN(data.rAngle)) {
|
||||
text = data.rAngle.toFixed(2) + String.fromCharCode(parseInt('00B0', 16));
|
||||
}
|
||||
return text;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const displayFunction = data => {
|
||||
let meanValue = '';
|
||||
const { cachedStats } = data;
|
||||
if (cachedStats && cachedStats.mean) {
|
||||
if (cachedStats && cachedStats.mean && !isNaN(cachedStats.mean)) {
|
||||
meanValue = cachedStats.mean.toFixed(2) + ' HU';
|
||||
}
|
||||
return meanValue;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const displayFunction = data => {
|
||||
let meanValue = '';
|
||||
const { cachedStats } = data;
|
||||
if (cachedStats && cachedStats.mean) {
|
||||
if (cachedStats && cachedStats.mean && !isNaN(cachedStats.mean)) {
|
||||
meanValue = cachedStats.mean.toFixed(2) + ' HU';
|
||||
}
|
||||
return meanValue;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const displayFunction = data => {
|
||||
let meanValue = '';
|
||||
if (data.meanStdDev && data.meanStdDev.mean) {
|
||||
if (data.meanStdDev && data.meanStdDev.mean && !isNaN(data.meanStdDev.mean)) {
|
||||
meanValue = data.meanStdDev.mean.toFixed(2) + ' HU';
|
||||
}
|
||||
return meanValue;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const displayFunction = data => {
|
||||
let lengthValue = '';
|
||||
if (data.length) {
|
||||
if (data.length && !isNaN(data.length)) {
|
||||
lengthValue = data.length.toFixed(2) + ' mm';
|
||||
}
|
||||
return lengthValue;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const displayFunction = data => {
|
||||
let meanValue = '';
|
||||
const { cachedStats } = data;
|
||||
if (cachedStats && cachedStats.mean) {
|
||||
if (cachedStats && cachedStats.mean && !isNaN(cachedStats.mean)) {
|
||||
meanValue = cachedStats.mean.toFixed(2) + ' HU';
|
||||
}
|
||||
return meanValue;
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import cloneDeep from 'lodash.clonedeep';
|
||||
|
||||
const defaultState = {
|
||||
// First tab
|
||||
hotkeyDefinitions: [
|
||||
// commandName, label, keys
|
||||
// [{ zoom: { label: 'Zoom', keys: ['z'] }}]
|
||||
],
|
||||
// Second tab
|
||||
windowLevelData: {
|
||||
// order, description, window (int), level (int)
|
||||
// 0: { description: 'Soft tissue', window: '', level: '' },
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
|
||||
const name = 'UINotificationService';
|
||||
|
||||
const serviceShowRequestQueue = [];
|
||||
|
||||
const publicAPI = {
|
||||
name,
|
||||
hide: _hide,
|
||||
@@ -21,7 +23,11 @@ const publicAPI = {
|
||||
|
||||
const serviceImplementation = {
|
||||
_hide: () => console.warn('hide() NOT IMPLEMENTED'),
|
||||
_show: () => console.warn('show() NOT IMPLEMENTED'),
|
||||
_show: showArguments => {
|
||||
serviceShowRequestQueue.push(showArguments);
|
||||
|
||||
console.warn('show() NOT IMPLEMENTED');
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -76,6 +82,11 @@ function setServiceImplementation({
|
||||
}
|
||||
if (showImplementation) {
|
||||
serviceImplementation._show = showImplementation;
|
||||
|
||||
while (serviceShowRequestQueue.length > 0) {
|
||||
const showArguments = serviceShowRequestQueue.pop();
|
||||
serviceImplementation._show(showArguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import Mousetrap from 'mousetrap';
|
||||
import pausePlugin from './pausePlugin';
|
||||
import recordPlugin from './recordPlugin';
|
||||
|
||||
recordPlugin(Mousetrap);
|
||||
pausePlugin(Mousetrap);
|
||||
|
||||
export default Mousetrap;
|
||||
File renamed without changes.
+26
-8
@@ -66,7 +66,7 @@ export default function(Mousetrap) {
|
||||
_recordCurrentCombo();
|
||||
}
|
||||
|
||||
for (i = 0; i < modifiers.length; ++i) {
|
||||
for (let i = 0; i < modifiers.length; ++i) {
|
||||
_recordKey(modifiers[i]);
|
||||
}
|
||||
_recordKey(character);
|
||||
@@ -85,10 +85,8 @@ export default function(Mousetrap) {
|
||||
* @returns void
|
||||
*/
|
||||
function _recordKey(key) {
|
||||
var i;
|
||||
|
||||
// one-off implementation of Array.indexOf, since IE6-9 don't support it
|
||||
for (i = 0; i < _currentRecordedKeys.length; ++i) {
|
||||
for (let i = 0; i < _currentRecordedKeys.length; ++i) {
|
||||
if (_currentRecordedKeys[i] === key) {
|
||||
return;
|
||||
}
|
||||
@@ -111,7 +109,7 @@ export default function(Mousetrap) {
|
||||
_recordedSequence.push(_currentRecordedKeys);
|
||||
_currentRecordedKeys = [];
|
||||
_recordedCharacterKey = false;
|
||||
_restartRecordTimer();
|
||||
_finishRecording();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,9 +122,7 @@ export default function(Mousetrap) {
|
||||
* @returns void
|
||||
*/
|
||||
function _normalizeSequence(sequence) {
|
||||
var i;
|
||||
|
||||
for (i = 0; i < sequence.length; ++i) {
|
||||
for (let i = 0; i < sequence.length; ++i) {
|
||||
sequence[i].sort(function(x, y) {
|
||||
// modifier keys always come first, in alphabetical order
|
||||
if (x.length > 1 && y.length === 1) {
|
||||
@@ -191,6 +187,28 @@ export default function(Mousetrap) {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* stop recording
|
||||
*
|
||||
* @param {Function} callback
|
||||
* @returns void
|
||||
*/
|
||||
Mousetrap.prototype.stopRecord = function() {
|
||||
var self = this;
|
||||
self.recording = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* start recording
|
||||
*
|
||||
* @param {Function} callback
|
||||
* @returns void
|
||||
*/
|
||||
Mousetrap.prototype.startRecording = function() {
|
||||
var self = this;
|
||||
self.recording = true;
|
||||
};
|
||||
|
||||
Mousetrap.prototype.handleKey = function() {
|
||||
var self = this;
|
||||
_handleKey.apply(self, arguments);
|
||||
@@ -12,6 +12,7 @@ import DicomLoaderService from './dicomLoaderService.js';
|
||||
import b64toBlob from './b64toBlob.js';
|
||||
import * as urlUtil from './urlUtil';
|
||||
import makeCancelable from './makeCancelable';
|
||||
import hotkeys from './hotkeys';
|
||||
|
||||
const utils = {
|
||||
guid,
|
||||
@@ -29,6 +30,7 @@ const utils = {
|
||||
DicomLoaderService,
|
||||
urlUtil,
|
||||
makeCancelable,
|
||||
hotkeys,
|
||||
};
|
||||
|
||||
export {
|
||||
@@ -47,6 +49,7 @@ export {
|
||||
DicomLoaderService,
|
||||
urlUtil,
|
||||
makeCancelable,
|
||||
hotkeys,
|
||||
};
|
||||
|
||||
export default utils;
|
||||
@@ -18,6 +18,7 @@ describe('Top level exports', () => {
|
||||
'DicomLoaderService',
|
||||
'urlUtil',
|
||||
'makeCancelable',
|
||||
'hotkeys',
|
||||
].sort();
|
||||
|
||||
const exports = Object.keys(utils.default).sort();
|
||||
|
||||
Reference in new issue
Block a user