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
+21
-25
@@ -53,7 +53,6 @@ import store from './store';
|
||||
import WhiteLabellingContext from './context/WhiteLabellingContext';
|
||||
import UserManagerContext from './context/UserManagerContext';
|
||||
import AppContext from './context/AppContext';
|
||||
const { setUserPreferences } = reduxOHIF.actions;
|
||||
|
||||
/** ~~~~~~~~~~~~~ Application Setup */
|
||||
const commandsManagerConfig = {
|
||||
@@ -63,8 +62,8 @@ const commandsManagerConfig = {
|
||||
|
||||
/** Managers */
|
||||
const commandsManager = new CommandsManager(commandsManagerConfig);
|
||||
const hotkeysManager = new HotkeysManager(commandsManager);
|
||||
const servicesManager = new ServicesManager();
|
||||
const hotkeysManager = new HotkeysManager(commandsManager, servicesManager);
|
||||
let extensionManager;
|
||||
/** ~~~~~~~~~~~~~ End Application Setup */
|
||||
|
||||
@@ -116,7 +115,7 @@ class App extends Component {
|
||||
|
||||
const {
|
||||
servers,
|
||||
hotkeys,
|
||||
hotkeys: appConfigHotkeys,
|
||||
cornerstoneExtensionConfig,
|
||||
extensions,
|
||||
oidc,
|
||||
@@ -139,7 +138,7 @@ class App extends Component {
|
||||
* Must run after extension commands are registered
|
||||
* if there is no hotkeys from localStorage set up from config.
|
||||
*/
|
||||
_initHotkeys(hotkeys);
|
||||
_initHotkeys(appConfigHotkeys);
|
||||
_initServers(servers);
|
||||
initWebWorkers();
|
||||
}
|
||||
@@ -263,30 +262,27 @@ function _initExtensions(extensions, cornerstoneExtensionConfig, appConfig) {
|
||||
extensionManager.registerExtensions(mergedExtensions);
|
||||
}
|
||||
|
||||
function _initHotkeys(hotkeys) {
|
||||
const { hotkeyDefinitions = {} } = store.getState().preferences || {};
|
||||
let updateStore = false;
|
||||
let hotkeysToUse = hotkeyDefinitions;
|
||||
/**
|
||||
*
|
||||
* @param {Object} appConfigHotkeys - Default hotkeys, as defined by app config
|
||||
*/
|
||||
function _initHotkeys(appConfigHotkeys) {
|
||||
// TODO: Use something more resilient
|
||||
// TODO: Mozilla has a special library for this
|
||||
const userPreferredHotkeys = JSON.parse(
|
||||
localStorage.getItem('hotkey-definitions') || '{}'
|
||||
);
|
||||
|
||||
if (!Object.keys(hotkeyDefinitions).length) {
|
||||
hotkeysToUse = hotkeys;
|
||||
updateStore = true;
|
||||
// TODO: hotkeysManager.isValidDefinitionObject(/* */)
|
||||
const hasUserPreferences =
|
||||
userPreferredHotkeys && Object.keys(userPreferredHotkeys).length > 0;
|
||||
if (hasUserPreferences) {
|
||||
hotkeysManager.setHotkeys(userPreferredHotkeys);
|
||||
} else {
|
||||
hotkeysManager.setHotkeys(appConfigHotkeys);
|
||||
}
|
||||
|
||||
if (hotkeysToUse) {
|
||||
hotkeysManager.setHotkeys(hotkeysToUse);
|
||||
|
||||
/* Set hotkeys default based on app config. */
|
||||
hotkeysManager.setDefaultHotKeys(hotkeys);
|
||||
|
||||
if (updateStore) {
|
||||
const { hotkeyDefinitions } = hotkeysManager;
|
||||
const windowLevelData = {};
|
||||
store.dispatch(
|
||||
setUserPreferences({ windowLevelData, hotkeyDefinitions })
|
||||
);
|
||||
}
|
||||
}
|
||||
hotkeysManager.setDefaultHotKeys(appConfigHotkeys);
|
||||
}
|
||||
|
||||
function _initServers(servers) {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link, withRouter } from 'react-router-dom';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import ConnectedUserPreferencesForm from '../../connectedComponents/ConnectedUserPreferencesForm';
|
||||
import { Dropdown, AboutContent, withModal } from '@ohif/ui';
|
||||
|
||||
import { UserPreferences } from './../UserPreferences';
|
||||
import OHIFLogo from '../OHIFLogo/OHIFLogo.js';
|
||||
import './Header.css';
|
||||
|
||||
@@ -42,7 +44,7 @@ function Header(props) {
|
||||
},
|
||||
onClick: () =>
|
||||
show({
|
||||
content: ConnectedUserPreferencesForm,
|
||||
content: UserPreferences,
|
||||
title: t('User Preferences'),
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import React, { useState, useSelector } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import i18n from '@ohif/i18n';
|
||||
|
||||
import { TabFooter, LanguageSwitcher, useSnackbarContext } from '@ohif/ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import './GeneralPreferences.styl';
|
||||
|
||||
/**
|
||||
* General Preferences tab
|
||||
* It renders the General Preferences content
|
||||
*
|
||||
* @param {object} props component props
|
||||
* @param {function} props.onClose
|
||||
*/
|
||||
function GeneralPreferences({ onClose }) {
|
||||
const { t } = useTranslation('UserPreferencesModal');
|
||||
const snackbar = useSnackbarContext();
|
||||
const currentLanguage = i18n.language;
|
||||
const { availableLanguages } = i18n;
|
||||
|
||||
const [language, setLanguage] = useState(currentLanguage);
|
||||
|
||||
const onResetPreferences = () => {
|
||||
setLanguage(i18n.defaultLanguage);
|
||||
};
|
||||
|
||||
const onSave = () => {
|
||||
i18n.changeLanguage(language);
|
||||
|
||||
onClose();
|
||||
|
||||
snackbar.show({
|
||||
message: t('SaveMessage'),
|
||||
type: 'success',
|
||||
});
|
||||
};
|
||||
|
||||
const hasErrors = false;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="GeneralPreferences">
|
||||
<div className="language">
|
||||
<label htmlFor="language-select" className="languageLabel">
|
||||
Language
|
||||
</label>
|
||||
<LanguageSwitcher
|
||||
language={language}
|
||||
onLanguageChange={setLanguage}
|
||||
languages={availableLanguages}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<TabFooter
|
||||
onResetPreferences={onResetPreferences}
|
||||
onSave={onSave}
|
||||
onCancel={onClose}
|
||||
hasErrors={hasErrors}
|
||||
t={t}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
GeneralPreferences.propTypes = {
|
||||
onClose: PropTypes.func,
|
||||
};
|
||||
|
||||
export { GeneralPreferences };
|
||||
@@ -0,0 +1,18 @@
|
||||
.GeneralPreferences
|
||||
display: flex;
|
||||
padding: 20px 0;
|
||||
|
||||
.language
|
||||
display: flex;
|
||||
width: 50%;
|
||||
|
||||
.languageLabel
|
||||
display: flex;
|
||||
flex-basis: 0;
|
||||
flex-grow: 1.5;
|
||||
justify-content: flex-end;
|
||||
padding-right: 20px;
|
||||
|
||||
.language-select
|
||||
flex-grow: 1.5;
|
||||
flex-basis: 0;
|
||||
@@ -0,0 +1,201 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { useSnackbarContext, TabFooter, HotkeyField } from '@ohif/ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { hotkeysValidators } from './hotkeysValidators';
|
||||
import { MODIFIER_KEYS } from './hotkeysConfig';
|
||||
|
||||
import { hotkeysManager } from '../../App';
|
||||
|
||||
import './HotkeysPreferences.styl';
|
||||
/**
|
||||
* Take hotkeyDefenintions and build an initialState to be used into the component state
|
||||
*
|
||||
* @param {Object} hotkeyDefinitions
|
||||
* @returns {Object} initialState
|
||||
*/
|
||||
const initialState = hotkeyDefinitions => ({
|
||||
hotkeys: { ...hotkeyDefinitions },
|
||||
errors: {},
|
||||
});
|
||||
/**
|
||||
* Take the updated command and keys and validate the changes with all validators
|
||||
*
|
||||
* @param {Object} arguments
|
||||
* @param {string} arguments.commandName command name string to be updated
|
||||
* @param {array} arguments.pressedKeys new array of keys to be added for the commandName
|
||||
* @param {array} arguments.hotkeys all hotkeys currently into the app
|
||||
* @returns {Object} {errorMessage} errorMessage coming from any of the validator or undefined if none
|
||||
*/
|
||||
const validateCommandKey = ({ commandName, pressedKeys, hotkeys }) => {
|
||||
for (const validator of hotkeysValidators) {
|
||||
const validation = validator({
|
||||
commandName,
|
||||
pressedKeys,
|
||||
hotkeys,
|
||||
});
|
||||
if (validation && validation.hasError) {
|
||||
return validation;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
errorMessage: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Take all hotkeys and split the list into two lists
|
||||
*
|
||||
* @param {array} hotkeys list of all hotkeys
|
||||
* @returns {array} array containing two arrays of keys
|
||||
*/
|
||||
const splitHotkeys = hotkeys => {
|
||||
const splitedHotkeys = [];
|
||||
const arrayHotkeys = Object.entries(hotkeys);
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
* HotkeysPreferences tab
|
||||
* It renders all hotkeys displayed into columns/rows
|
||||
*
|
||||
* It stores current state and whenever it changes, component messages parent of new value (through function callback)
|
||||
* @param {object} props component props
|
||||
* @param {string} props.onClose
|
||||
*/
|
||||
function HotkeysPreferences({ onClose }) {
|
||||
const { t } = useTranslation('UserPreferencesModal');
|
||||
const { hotkeyDefaults, hotkeyDefinitions } = hotkeysManager;
|
||||
|
||||
const [state, setState] = useState(initialState(hotkeyDefinitions));
|
||||
|
||||
const snackbar = useSnackbarContext();
|
||||
|
||||
const onResetPreferences = () => {
|
||||
const defaultHotKeyDefinitions = {};
|
||||
|
||||
hotkeyDefaults.map(item => {
|
||||
const { commandName, ...values } = item;
|
||||
defaultHotKeyDefinitions[commandName] = { ...values };
|
||||
});
|
||||
|
||||
setState(initialState(defaultHotKeyDefinitions));
|
||||
};
|
||||
|
||||
const onSave = () => {
|
||||
const { hotkeys } = state;
|
||||
|
||||
hotkeysManager.setHotkeys(hotkeys);
|
||||
|
||||
localStorage.setItem('hotkey-definitions', JSON.stringify(hotkeys));
|
||||
|
||||
onClose();
|
||||
|
||||
snackbar.show({
|
||||
message: t('SaveMessage'),
|
||||
type: 'success',
|
||||
});
|
||||
};
|
||||
|
||||
const onHotkeyChanged = (commandName, hotkeyDefinition, keys) => {
|
||||
const { errorMessage } = validateCommandKey({
|
||||
commandName,
|
||||
pressedKeys: keys,
|
||||
hotkeys: state.hotkeys,
|
||||
});
|
||||
|
||||
setState(prevState => ({
|
||||
hotkeys: {
|
||||
...prevState.hotkeys,
|
||||
[commandName]: { ...hotkeyDefinition, keys },
|
||||
},
|
||||
errors: {
|
||||
...prevState.errors,
|
||||
[commandName]: errorMessage,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const hasErrors = Object.keys(state.errors).some(key => !!state.errors[key]);
|
||||
const hasHotkeys = Object.keys(state.hotkeys).length;
|
||||
const splitedHotkeys = splitHotkeys(state.hotkeys);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="HotkeysPreferences">
|
||||
{hasHotkeys ? (
|
||||
<div className="hotkeyTable">
|
||||
{splitedHotkeys.map((hotkeys, index) => {
|
||||
return (
|
||||
<div className="hotkeyColumn" key={index}>
|
||||
<div className="hotkeyHeader">
|
||||
<div className="headerItemText text-right">Function</div>
|
||||
<div className="headerItemText text-center">Shortcut</div>
|
||||
</div>
|
||||
{hotkeys.map(hotkey => {
|
||||
const commandName = hotkey[0];
|
||||
const hotkeyDefinition = hotkey[1];
|
||||
const { keys, label } = hotkeyDefinition;
|
||||
const errorMessage = state.errors[hotkey[0]];
|
||||
const handleChange = keys => {
|
||||
onHotkeyChanged(commandName, hotkeyDefinition, keys);
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={commandName} className="hotkeyRow">
|
||||
<div className="hotkeyLabel">{label}</div>
|
||||
<div
|
||||
data-key="defaultTool"
|
||||
className={classnames(
|
||||
'wrapperHotkeyInput',
|
||||
errorMessage ? 'stateError' : ''
|
||||
)}
|
||||
>
|
||||
<HotkeyField
|
||||
keys={keys}
|
||||
modifier_keys={MODIFIER_KEYS}
|
||||
handleChange={handleChange}
|
||||
classNames={'hotkeyInput'}
|
||||
></HotkeyField>
|
||||
<span className="errorMessage">{errorMessage}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
'Hotkeys definitions is empty'
|
||||
)}
|
||||
</div>
|
||||
<TabFooter
|
||||
onResetPreferences={onResetPreferences}
|
||||
onSave={onSave}
|
||||
onCancel={onClose}
|
||||
hasErrors={hasErrors}
|
||||
t={t}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
HotkeysPreferences.propTypes = {
|
||||
onClose: PropTypes.func,
|
||||
};
|
||||
|
||||
export { HotkeysPreferences };
|
||||
@@ -0,0 +1,73 @@
|
||||
.HotkeysPreferences
|
||||
display: flex
|
||||
padding: 20px
|
||||
|
||||
.errorMessage
|
||||
color: var(--state-error-text)
|
||||
font-size: 10px
|
||||
text-transform: uppercase
|
||||
|
||||
.hotkeyTable
|
||||
display: flex
|
||||
flex-direction: row
|
||||
flex-basis: 0
|
||||
flex-grow: 1.5
|
||||
|
||||
.hotkeyColumn
|
||||
display: flex
|
||||
flex-direction: column
|
||||
flex-basis: 0
|
||||
flex-grow: 1.5
|
||||
|
||||
.hotkeyHeader
|
||||
display: flex
|
||||
flex-direction: row
|
||||
margin-bottom: 10px
|
||||
|
||||
.headerItemText
|
||||
flex-basis: 0
|
||||
flex-grow: 1.5
|
||||
padding: 5px 15px 5px 0
|
||||
|
||||
.hotkeyRow
|
||||
display: flex
|
||||
|
||||
.wrapperHotkeyInput
|
||||
margin-bottom: 5px
|
||||
cursor: pointer
|
||||
display: flex
|
||||
flex-direction: column
|
||||
flex-basis: 0
|
||||
flex-grow: 1.5
|
||||
|
||||
.hotkeyInput
|
||||
font-weight: 400
|
||||
cursor: pointer
|
||||
transition: background-color .3s ease,border-color .3s ease
|
||||
background-color: var(--ui-gray)
|
||||
color: var(--text-primary-color)
|
||||
border-color: var(--ui-border-coolor)
|
||||
border: 0
|
||||
border-radius: 2px
|
||||
font-size: 14px
|
||||
height: 30px
|
||||
width: 100%
|
||||
line-height: 16px
|
||||
padding: 8px 9px 6px
|
||||
text-align: center
|
||||
|
||||
.hotkeyInput:focus
|
||||
border-color: var(--active-color)
|
||||
background-color: var(--ui-gray-dark)
|
||||
box-shadow: 0 0 0 2px var(--active-color) !important
|
||||
outline: 0
|
||||
|
||||
.hotkeyLabel
|
||||
padding: 5px 15px 5px 0
|
||||
text-align: right
|
||||
flex-basis: 0
|
||||
flex-grow: 1.5
|
||||
|
||||
.stateError
|
||||
.hotkeyInput
|
||||
background-color: var(--state-error)
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { TabComponents } from '@ohif/ui';
|
||||
|
||||
// Tabs
|
||||
import { HotkeysPreferences } from './HotkeysPreferences';
|
||||
import { WindowLevelPreferences } from './WindowLevelPreferences';
|
||||
import { GeneralPreferences } from './GeneralPreferences';
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
name: 'Hotkeys',
|
||||
Component: HotkeysPreferences,
|
||||
customProps: {},
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
name: 'General',
|
||||
Component: GeneralPreferences,
|
||||
customProps: {},
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
name: 'Window Level',
|
||||
Component: WindowLevelPreferences,
|
||||
customProps: {},
|
||||
hidden: true,
|
||||
},
|
||||
];
|
||||
|
||||
function UserPreferences({ hide }) {
|
||||
const customProps = {
|
||||
onClose: hide,
|
||||
};
|
||||
return <TabComponents tabs={tabs} customProps={customProps} />;
|
||||
}
|
||||
|
||||
UserPreferences.propTypes = {
|
||||
hide: PropTypes.func,
|
||||
};
|
||||
|
||||
export { UserPreferences };
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { TabFooter } from '@ohif/ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function WindowLevelPreferences({ onClose }) {
|
||||
const { t } = useTranslation('UserPreferencesModal');
|
||||
const onResetPreferences = () => {};
|
||||
const onSave = () => {};
|
||||
const hasErrors = false;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="">Component content: {name}</div>
|
||||
<div className="">TDB!</div>
|
||||
<TabFooter
|
||||
onResetPreferences={onResetPreferences}
|
||||
onSave={onSave}
|
||||
onCancel={onClose}
|
||||
hasErrors={hasErrors}
|
||||
t={t}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
WindowLevelPreferences.propTypes = {
|
||||
onClose: PropTypes.func,
|
||||
};
|
||||
|
||||
export { WindowLevelPreferences };
|
||||
@@ -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,97 @@
|
||||
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 {
|
||||
hasError: true,
|
||||
errorMessage: ERROR_MESSAGES.MODIFIER,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const emptyValidator = ({ pressedKeys = [] }) => {
|
||||
if (!pressedKeys.length) {
|
||||
return {
|
||||
hasError: true,
|
||||
errorMessage: ERROR_MESSAGES.EMPTY,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const conflictingValidator = ({ commandName, pressedKeys, hotkeys }) => {
|
||||
const conflictingCommand = findConflictingCommand(
|
||||
hotkeys,
|
||||
commandName,
|
||||
pressedKeys
|
||||
);
|
||||
|
||||
if (conflictingCommand) {
|
||||
return {
|
||||
hasError: true,
|
||||
errorMessage: `"${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 {
|
||||
hasError: true,
|
||||
errorMessage: `"${formatPressedKeys(
|
||||
pressedKeys
|
||||
)}" shortcut combination is not allowed`,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const hotkeysValidators = [
|
||||
emptyValidator,
|
||||
modifierValidator,
|
||||
conflictingValidator,
|
||||
disallowedValidator,
|
||||
];
|
||||
|
||||
export { hotkeysValidators };
|
||||
@@ -0,0 +1 @@
|
||||
export { UserPreferences } from './UserPreferences';
|
||||
@@ -1,58 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { UserPreferencesForm } from '@ohif/ui';
|
||||
import OHIF from '@ohif/core';
|
||||
import i18n from '@ohif/i18n';
|
||||
|
||||
import { hotkeysManager } from '../App.js';
|
||||
|
||||
const { setUserPreferences } = OHIF.redux.actions;
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const { defaultLanguage } = i18n;
|
||||
const { hotkeyDefinitions, windowLevelData = {}, generalPreferences } =
|
||||
state.preferences || {};
|
||||
const { hotkeyDefaults } = hotkeysManager;
|
||||
|
||||
return {
|
||||
onClose: ownProps.hide,
|
||||
windowLevelData,
|
||||
hotkeyDefinitions,
|
||||
generalPreferences,
|
||||
hotkeysManager,
|
||||
hotkeyDefaults,
|
||||
defaultLanguage,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, ownProps) => {
|
||||
return {
|
||||
onSave: ({ windowLevelData, hotkeyDefinitions, generalPreferences }) => {
|
||||
// TODO improve this strategy on windowLevel implementation
|
||||
hotkeysManager.setHotkeys(hotkeyDefinitions);
|
||||
|
||||
const { language } = generalPreferences;
|
||||
|
||||
// set new language
|
||||
i18n.changeLanguage(language);
|
||||
|
||||
if (ownProps.hide) {
|
||||
ownProps.hide();
|
||||
}
|
||||
|
||||
dispatch(
|
||||
setUserPreferences({
|
||||
windowLevelData,
|
||||
hotkeyDefinitions,
|
||||
generalPreferences,
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedUserPreferencesForm = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(UserPreferencesForm);
|
||||
|
||||
export default ConnectedUserPreferencesForm;
|
||||
Reference in new issue
Block a user