OHIF-332: Users should be able to see all available hotkeys and language settings in one place (#1895)
* OHIF-330: Update Modal Styles * feat/ohif-332: finish raw ui * feat/ohif-332: update mode configuration strategy * feat/ohif-332: fix hotkey errors * feat/ohif-332: update hotkey logic with recent merged changes * feat/ohif-322: wrap * feat/ohif-322: add disable state * ohif-332: cr updates * ohif-332: disable * ohif-332: cr updates * ohif-332: extract header component * ohif-332: cr update to fix merge conflicts and design issue Co-authored-by: Rodrigo Antinarelli <rodrigoantinarelli@gmail.com>
This commit is contained in:
1 parent
ae3d05ebf7
commit
3e944780dc
37 files changed
+913
-256
No files matched your search
@@ -0,0 +1,111 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { HotkeyField, Typography } from '@ohif/ui';
|
||||
|
||||
/* TODO: Move these configs and utils to core? */
|
||||
import { MODIFIER_KEYS } from './hotkeysConfig';
|
||||
import { validate, splitHotkeyDefinitionsAndCreateTuples } from './utils';
|
||||
|
||||
const HotkeysPreferences = ({ disabled, hotkeyDefinitions, errors: controlledErrors, onChange }) => {
|
||||
const visibleHotkeys = Object.keys(hotkeyDefinitions)
|
||||
.filter(key => hotkeyDefinitions[key].isEditable)
|
||||
.reduce((obj, key) => {
|
||||
obj[key] = hotkeyDefinitions[key];
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
const [errors, setErrors] = useState(controlledErrors);
|
||||
const splitedHotkeys = splitHotkeyDefinitionsAndCreateTuples(visibleHotkeys);
|
||||
|
||||
if (!Object.keys(hotkeyDefinitions).length) {
|
||||
return 'No hotkeys definitions';
|
||||
}
|
||||
|
||||
const onHotkeyChangeHandler = (id, definition) => {
|
||||
const { error } = validate({
|
||||
commandName: id,
|
||||
pressedKeys: definition.keys,
|
||||
hotkeys: hotkeyDefinitions,
|
||||
});
|
||||
|
||||
setErrors(prevState => {
|
||||
const errors = { ...prevState, [id]: error };
|
||||
onChange(id, definition, errors);
|
||||
return errors;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='flex flex-row justify-center'>
|
||||
<div className='flex flex-row justify-evenly w-full'>
|
||||
{splitedHotkeys.map((hotkeys, index) => {
|
||||
return (
|
||||
<div key={`HotkeyGroup@${index}`} className='flex flex-row'>
|
||||
<div className='p-2 text-right flex flex-col'>
|
||||
{hotkeys.map((hotkey, hotkeyIndex) => {
|
||||
const [id, definition] = hotkey;
|
||||
const isFirst = hotkeyIndex === 0;
|
||||
const error = errors[id];
|
||||
|
||||
const onChangeHandler = keys => onHotkeyChangeHandler(id, { ...definition, keys });
|
||||
|
||||
return (
|
||||
<div key={`HotkeyItem@${hotkeyIndex}`} className='flex flex-row justify-end mb-2'>
|
||||
<div className='flex flex-col items-center'>
|
||||
<Typography
|
||||
variant='subtitle'
|
||||
className={classNames('pr-6 w-full text-right text-primary-light', !isFirst && 'hidden')}
|
||||
>
|
||||
Function
|
||||
</Typography>
|
||||
<Typography
|
||||
variant='subtitle'
|
||||
className={classNames('pr-6 h-full flex flex-row items-center whitespace-no-wrap', isFirst && 'mt-5')}>
|
||||
{definition.label}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className='flex flex-col'>
|
||||
<Typography
|
||||
variant='subtitle'
|
||||
className={classNames('pr-6 pl-0 text-left text-primary-light', !isFirst && 'hidden')}
|
||||
>
|
||||
Shortcut
|
||||
</Typography>
|
||||
<div className={classNames('flex flex-col w-32', isFirst && 'mt-5')}>
|
||||
<HotkeyField
|
||||
disabled={disabled}
|
||||
keys={definition.keys}
|
||||
modifierKeys={MODIFIER_KEYS}
|
||||
onChange={onChangeHandler}
|
||||
className='text-lg h-8'
|
||||
/>
|
||||
{error && <span className='p-2 text-red-600 text-sm'>{error}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const noop = () => { };
|
||||
|
||||
HotkeysPreferences.propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
hotkeyDefinitions: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
HotkeysPreferences.defaultProps = {
|
||||
onChange: noop,
|
||||
disabled: false
|
||||
};
|
||||
|
||||
export default HotkeysPreferences;
|
||||
@@ -0,0 +1,91 @@
|
||||
const range = (start, end) => {
|
||||
return new Array(end - start).fill().map((d, i) => i + start);
|
||||
};
|
||||
|
||||
export const MODIFIER_KEYS = ['ctrl', 'alt', 'shift'];
|
||||
|
||||
export const DISALLOWED_COMBINATIONS = {
|
||||
'': [],
|
||||
alt: ['space'],
|
||||
shift: [],
|
||||
ctrl: [
|
||||
'f4',
|
||||
'f5',
|
||||
'f11',
|
||||
'w',
|
||||
'r',
|
||||
't',
|
||||
'o',
|
||||
'p',
|
||||
'a',
|
||||
'd',
|
||||
'f',
|
||||
'g',
|
||||
'h',
|
||||
'j',
|
||||
'l',
|
||||
'z',
|
||||
'x',
|
||||
'c',
|
||||
'v',
|
||||
'b',
|
||||
'n',
|
||||
'pagedown',
|
||||
'pageup',
|
||||
],
|
||||
'ctrl+shift': ['q', 'w', 'r', 't', 'p', 'a', 'h', 'v', 'b', 'n'],
|
||||
};
|
||||
|
||||
export const SPECIAL_KEYS = {
|
||||
8: 'backspace',
|
||||
9: 'tab',
|
||||
13: 'return',
|
||||
16: 'shift',
|
||||
17: 'ctrl',
|
||||
18: 'alt',
|
||||
19: 'pause',
|
||||
20: 'capslock',
|
||||
27: 'esc',
|
||||
32: 'space',
|
||||
33: 'pageup',
|
||||
34: 'pagedown',
|
||||
35: 'end',
|
||||
36: 'home',
|
||||
37: 'left',
|
||||
38: 'up',
|
||||
39: 'right',
|
||||
40: 'down',
|
||||
45: 'insert',
|
||||
46: 'del',
|
||||
96: '0',
|
||||
97: '1',
|
||||
98: '2',
|
||||
99: '3',
|
||||
100: '4',
|
||||
101: '5',
|
||||
102: '6',
|
||||
103: '7',
|
||||
104: '8',
|
||||
105: '9',
|
||||
106: '*',
|
||||
107: '+',
|
||||
109: '-',
|
||||
110: '.',
|
||||
111: '/',
|
||||
112: 'f1',
|
||||
113: 'f2',
|
||||
114: 'f3',
|
||||
115: 'f4',
|
||||
116: 'f5',
|
||||
117: 'f6',
|
||||
118: 'f7',
|
||||
119: 'f8',
|
||||
120: 'f9',
|
||||
121: 'f10',
|
||||
122: 'f11',
|
||||
123: 'f12',
|
||||
144: 'numlock',
|
||||
145: 'scroll',
|
||||
191: '/',
|
||||
224: 'meta',
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
import { MODIFIER_KEYS, DISALLOWED_COMBINATIONS } from './hotkeysConfig';
|
||||
|
||||
const formatPressedKeys = pressedKeysArray => pressedKeysArray.join('+');
|
||||
|
||||
const findConflictingCommand = (hotkeys, currentCommandName, pressedKeys) => {
|
||||
let firstConflictingCommand = undefined;
|
||||
const formatedPressedHotkeys = formatPressedKeys(pressedKeys);
|
||||
|
||||
for (const commandName in hotkeys) {
|
||||
const toolHotkeys = hotkeys[commandName].keys;
|
||||
const formatedToolHotkeys = formatPressedKeys(toolHotkeys);
|
||||
|
||||
if (
|
||||
formatedPressedHotkeys === formatedToolHotkeys &&
|
||||
commandName !== currentCommandName
|
||||
) {
|
||||
firstConflictingCommand = hotkeys[commandName];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return firstConflictingCommand;
|
||||
};
|
||||
|
||||
const ERROR_MESSAGES = {
|
||||
MODIFIER:
|
||||
"It's not possible to define only modifier keys (ctrl, alt and shift) as a shortcut",
|
||||
EMPTY: "Field can't be empty.",
|
||||
};
|
||||
|
||||
// VALIDATORS
|
||||
|
||||
const modifierValidator = ({ pressedKeys }) => {
|
||||
const lastPressedKey = pressedKeys[pressedKeys.length - 1];
|
||||
// Check if it has a valid modifier
|
||||
const isModifier = MODIFIER_KEYS.includes(lastPressedKey);
|
||||
if (isModifier) {
|
||||
return { error: ERROR_MESSAGES.MODIFIER };
|
||||
}
|
||||
};
|
||||
|
||||
const emptyValidator = ({ pressedKeys = [] }) => {
|
||||
if (!pressedKeys.length) {
|
||||
return { error: ERROR_MESSAGES.EMPTY };
|
||||
}
|
||||
};
|
||||
|
||||
const conflictingValidator = ({ commandName, pressedKeys, hotkeys }) => {
|
||||
const conflictingCommand = findConflictingCommand(
|
||||
hotkeys,
|
||||
commandName,
|
||||
pressedKeys
|
||||
);
|
||||
|
||||
if (conflictingCommand) {
|
||||
return {
|
||||
error: `"${conflictingCommand.label}" is already using the "${pressedKeys}" shortcut.`,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const disallowedValidator = ({ pressedKeys = [] }) => {
|
||||
const lastPressedKey = pressedKeys[pressedKeys.length - 1];
|
||||
const modifierCommand = formatPressedKeys(
|
||||
pressedKeys.slice(0, pressedKeys.length - 1)
|
||||
);
|
||||
|
||||
const disallowedCombination = DISALLOWED_COMBINATIONS[modifierCommand];
|
||||
const hasDisallowedCombinations = disallowedCombination
|
||||
? disallowedCombination.includes(lastPressedKey)
|
||||
: false;
|
||||
|
||||
if (hasDisallowedCombinations) {
|
||||
return {
|
||||
error: `"${formatPressedKeys(pressedKeys)}" shortcut combination is not allowed`,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const hotkeysValidators = [
|
||||
emptyValidator,
|
||||
modifierValidator,
|
||||
conflictingValidator,
|
||||
disallowedValidator,
|
||||
];
|
||||
|
||||
export { hotkeysValidators };
|
||||
@@ -0,0 +1,3 @@
|
||||
import HotkeysPreferences from './HotkeysPreferences.jsx';
|
||||
|
||||
export default HotkeysPreferences;
|
||||
@@ -0,0 +1,52 @@
|
||||
import { hotkeysValidators } from './hotkeysValidators';
|
||||
|
||||
/**
|
||||
* Split hotkeys definitions and create hotkey related tuples
|
||||
*
|
||||
* @param {array} hotkeyDefinitions
|
||||
* @returns {array} array of tuples consisted of command name and hotkey definition
|
||||
*/
|
||||
const splitHotkeyDefinitionsAndCreateTuples = hotkeyDefinitions => {
|
||||
const splitedHotkeys = [];
|
||||
const arrayHotkeys = Object.entries(hotkeyDefinitions);
|
||||
|
||||
if (arrayHotkeys.length) {
|
||||
const halfwayThrough = Math.ceil(arrayHotkeys.length / 2);
|
||||
splitedHotkeys.push(arrayHotkeys.slice(0, halfwayThrough));
|
||||
splitedHotkeys.push(
|
||||
arrayHotkeys.slice(halfwayThrough, arrayHotkeys.length)
|
||||
);
|
||||
}
|
||||
|
||||
return splitedHotkeys;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate a hotkey change
|
||||
*
|
||||
* @param {Object} arguments
|
||||
* @param {string} arguments.commandName command name or id
|
||||
* @param {array} arguments.pressedKeys new keys
|
||||
* @param {array} arguments.hotkeys current hotkeys
|
||||
* @returns {Object} {error} validation error
|
||||
*/
|
||||
const validate = ({ commandName, pressedKeys, hotkeys }) => {
|
||||
for (const validator of hotkeysValidators) {
|
||||
const validation = validator({
|
||||
commandName,
|
||||
pressedKeys,
|
||||
hotkeys,
|
||||
});
|
||||
|
||||
if (validation && validation.error) {
|
||||
return validation;
|
||||
}
|
||||
}
|
||||
|
||||
return { error: undefined };
|
||||
};
|
||||
|
||||
export {
|
||||
validate,
|
||||
splitHotkeyDefinitionsAndCreateTuples
|
||||
};
|
||||
Reference in new issue
Block a user