feat: 🎸 Update hotkeys and user preferences modal (#1135)
* feat: 🎸 Update hotkeys and user preferences modal This feature fix incompatibility with existent hotkeys component and adds user preferences modal back Closes: #923 * Update preferences structure in store * Hide window level section of user preferences * Update modal to reflect current hotkey value * Clone object with hotkeys before passing to manager * CR Update: Extract hotkeys manager format code to manager * Fix broken cypress test * Use new modal provider * Rename hotkeyDefinitions in hotkeyspreferences and use array as representation * Update study test and remove unused styles
This commit is contained in:
1 parent
dc6e9d6bb5
commit
e62f5f8dd2
26 files changed
+263
-256
No files matched your search
@@ -6,8 +6,8 @@ import { TableList, TableListItem } from './tableList';
|
||||
import {
|
||||
AboutContent,
|
||||
UserPreferences,
|
||||
UserPreferencesModal,
|
||||
} from './userPreferencesModal';
|
||||
UserPreferencesForm,
|
||||
} from './userPreferencesForm';
|
||||
|
||||
import { Checkbox } from './checkbox';
|
||||
import { CineDialog } from './cineDialog';
|
||||
@@ -52,6 +52,6 @@ export {
|
||||
Tooltip,
|
||||
AboutContent,
|
||||
UserPreferences,
|
||||
UserPreferencesModal,
|
||||
UserPreferencesForm,
|
||||
OHIFModal,
|
||||
};
|
||||
File renamed without changes.
+23
-45
@@ -10,31 +10,14 @@ import PropTypes from 'prop-types';
|
||||
|
||||
export class HotKeysPreferences extends Component {
|
||||
static propTypes = {
|
||||
hotKeysData: PropTypes.objectOf(
|
||||
PropTypes.shape({
|
||||
keys: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
onChange: PropTypes.func,
|
||||
hotkeyDefinitions: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const hotkeyCommands = Object.keys(this.props.hotKeysData);
|
||||
const localHotKeys = hotkeyCommands.map(commandName => {
|
||||
const definition = this.props.hotKeysData[commandName];
|
||||
|
||||
return {
|
||||
commandName,
|
||||
keys: definition.keys,
|
||||
label: definition.label,
|
||||
};
|
||||
});
|
||||
|
||||
this.state = {
|
||||
hotKeys: localHotKeys,
|
||||
hotKeys: this.props.hotkeyDefinitions,
|
||||
errorMessages: {},
|
||||
};
|
||||
|
||||
@@ -53,24 +36,28 @@ export class HotKeysPreferences extends Component {
|
||||
const { ctrlKey, altKey, shiftKey } = keyDownEvent;
|
||||
|
||||
if (ctrlKey && !altKey) {
|
||||
keysPressedArray.push('CTRL');
|
||||
keysPressedArray.push('ctrl');
|
||||
}
|
||||
|
||||
if (shiftKey && !altKey) {
|
||||
keysPressedArray.push('SHIFT');
|
||||
keysPressedArray.push('shift');
|
||||
}
|
||||
|
||||
if (altKey && !ctrlKey) {
|
||||
keysPressedArray.push('ALT');
|
||||
keysPressedArray.push('alt');
|
||||
}
|
||||
|
||||
return keysPressedArray;
|
||||
}
|
||||
|
||||
getConflictingCommand(currentToolKey, hotKeyCommand) {
|
||||
return Object.keys(this.state.hotKeys).find(tool => {
|
||||
const value = this.state.hotKeys[tool].command;
|
||||
return value && value === hotKeyCommand && tool !== currentToolKey;
|
||||
getConflictingCommand(currentCommandName, currentHotKeys) {
|
||||
return this.state.hotKeys.find((tool, index) => {
|
||||
const toolHotKeys = tool.keys[0];
|
||||
return (
|
||||
toolHotKeys &&
|
||||
toolHotKeys === currentHotKeys &&
|
||||
tool.commandName !== currentCommandName
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -89,7 +76,7 @@ export class HotKeysPreferences extends Component {
|
||||
specialKeyName ||
|
||||
keyDownEvent.key ||
|
||||
String.fromCharCode(keyDownEvent.keyCode);
|
||||
pressedKeys.push(keyName.toUpperCase());
|
||||
pressedKeys.push(keyName);
|
||||
}
|
||||
|
||||
this.updateHotKeysState(commandName, pressedKeys.join('+'));
|
||||
@@ -136,17 +123,17 @@ export class HotKeysPreferences extends Component {
|
||||
const hotKey = this.state.hotKeys[hotKeyIndex];
|
||||
const keys = hotKey.keys[0];
|
||||
const pressedKeys = keys.split('+');
|
||||
const lastPressedKey = pressedKeys[pressedKeys.length - 1].toUpperCase();
|
||||
const lastPressedKey = pressedKeys[pressedKeys.length - 1];
|
||||
|
||||
// clear the prior errors
|
||||
this.setState({ errorMessages: {} }, () => {
|
||||
// Check if it has a valid modifier
|
||||
const isModifier = ['CTRL', 'ALT', 'SHIFT'].includes(lastPressedKey);
|
||||
const isModifier = ['ctrl', 'alt', 'shift'].includes(lastPressedKey);
|
||||
if (isModifier) {
|
||||
this.updateHotKeysState(commandName, '');
|
||||
this.updateErrorsState(
|
||||
commandName,
|
||||
"It's not possible to define only modifier keys (CTRL, ALT and SHIFT) as a shortcut"
|
||||
"It's not possible to define only modifier keys (ctrl, alt and shift) as a shortcut"
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -154,21 +141,13 @@ export class HotKeysPreferences extends Component {
|
||||
/*
|
||||
* Check if it has some conflict
|
||||
*/
|
||||
const conflictedCommandKey = this.getConflictingCommand(
|
||||
commandName,
|
||||
keys
|
||||
);
|
||||
if (conflictedCommandKey) {
|
||||
const conflictedCommand = this.state.hotKeys[conflictedCommandKey];
|
||||
|
||||
const conflictedCommand = this.getConflictingCommand(commandName, keys);
|
||||
if (conflictedCommand) {
|
||||
this.updateHotKeysState(commandName, '');
|
||||
this.updateErrorsState(
|
||||
commandName,
|
||||
`"${conflictedCommand.label}" is already using the "${
|
||||
conflictedCommand.command
|
||||
}" shortcut.`
|
||||
`"${conflictedCommand.label}" is already using the "${keys}" shortcut.`
|
||||
);
|
||||
this.updateErrorsState(conflictedCommandKey, '');
|
||||
this.updateHotKeysState(commandName, '');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -177,8 +156,7 @@ export class HotKeysPreferences extends Component {
|
||||
*/
|
||||
const modifierCommand = pressedKeys
|
||||
.slice(0, pressedKeys.length - 1)
|
||||
.join('+')
|
||||
.toUpperCase();
|
||||
.join('+');
|
||||
|
||||
const disallowedCombination = disallowedCombinations[modifierCommand];
|
||||
const hasDisallowedCombinations = disallowedCombination
|
||||
@@ -189,7 +167,7 @@ export class HotKeysPreferences extends Component {
|
||||
this.updateHotKeysState(commandName, '');
|
||||
this.updateErrorsState(
|
||||
commandName,
|
||||
"It's not possible to define only modifier keys (CTRL, ALT and SHIFT) as a shortcut"
|
||||
`"${pressedKeys.join('+')}" shortcut combination is not allowed`
|
||||
);
|
||||
return;
|
||||
}
|
||||
File renamed without changes.
+17
-13
@@ -8,14 +8,14 @@ import './UserPreferences.styl';
|
||||
|
||||
export class UserPreferences extends Component {
|
||||
static defaultProps = {
|
||||
hotKeysData: {},
|
||||
hotkeyDefinitions: [],
|
||||
windowLevelData: {},
|
||||
generalData: {},
|
||||
};
|
||||
|
||||
// TODO: Make this more generic. Tabs should not be restricted to these entries
|
||||
static propTypes = {
|
||||
hotKeysData: PropTypes.object.isRequired,
|
||||
hotkeyDefinitions: PropTypes.array.isRequired,
|
||||
windowLevelData: PropTypes.object.isRequired,
|
||||
generalData: PropTypes.object.isRequired,
|
||||
};
|
||||
@@ -32,7 +32,9 @@ export class UserPreferences extends Component {
|
||||
return (
|
||||
<form className="form-themed themed">
|
||||
<div className="form-content">
|
||||
<HotKeysPreferences hotKeysData={this.props.hotKeysData} />
|
||||
<HotKeysPreferences
|
||||
hotkeyDefinitions={this.props.hotkeyDefinitions}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
@@ -66,8 +68,8 @@ export class UserPreferences extends Component {
|
||||
switch (tabIndex) {
|
||||
case 0:
|
||||
return this.renderHotkeysTab();
|
||||
case 1:
|
||||
return this.renderWindowLevelTab();
|
||||
/* case 1:
|
||||
return this.renderWindowLevelTab(); */
|
||||
case 2:
|
||||
return this.renderGeneralTab();
|
||||
|
||||
@@ -93,14 +95,16 @@ export class UserPreferences extends Component {
|
||||
>
|
||||
<button>Hotkeys</button>
|
||||
</li>
|
||||
<li
|
||||
onClick={() => {
|
||||
this.tabClick(1);
|
||||
}}
|
||||
className={this.getTabClass(1)}
|
||||
>
|
||||
<button>Window Level</button>
|
||||
</li>
|
||||
{false && (
|
||||
<li
|
||||
onClick={() => {
|
||||
this.tabClick(1);
|
||||
}}
|
||||
className={this.getTabClass(1)}
|
||||
>
|
||||
<button>Window Level</button>
|
||||
</li>
|
||||
)}
|
||||
<li
|
||||
onClick={() => {
|
||||
this.tabClick(2);
|
||||
File renamed without changes.
@@ -0,0 +1,86 @@
|
||||
import './UserPreferencesForm.styl';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTranslation } from '../../utils/LanguageProvider';
|
||||
|
||||
import cloneDeep from 'lodash.clonedeep';
|
||||
import isEqual from 'lodash.isequal';
|
||||
import { UserPreferences } from './UserPreferences';
|
||||
|
||||
class UserPreferencesForm extends Component {
|
||||
// TODO: Make this component more generic to allow things other than W/L and hotkeys...
|
||||
static propTypes = {
|
||||
onClose: PropTypes.func,
|
||||
onSave: PropTypes.func,
|
||||
onResetToDefaults: PropTypes.func,
|
||||
windowLevelData: PropTypes.object,
|
||||
hotkeyDefinitions: PropTypes.array,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
windowLevelData: cloneDeep(props.windowLevelData),
|
||||
hotkeyDefinitions: cloneDeep(props.hotkeyDefinitions),
|
||||
};
|
||||
}
|
||||
|
||||
save = () => {
|
||||
this.props.onSave({
|
||||
windowLevelData: this.state.windowLevelData,
|
||||
hotkeyDefinitions: this.state.hotkeyDefinitions,
|
||||
});
|
||||
};
|
||||
|
||||
componentDidUpdate(prev, next) {
|
||||
const newStateData = {};
|
||||
|
||||
if (!isEqual(prev.windowLevelData, next.windowLevelData)) {
|
||||
newStateData.windowLevelData = prev.windowLevelData;
|
||||
}
|
||||
|
||||
if (!isEqual(prev.hotkeyDefinitions, next.hotkeyDefinitions)) {
|
||||
newStateData.hotkeyDefinitions = prev.hotkeyDefinitions;
|
||||
}
|
||||
|
||||
if (newStateData.hotkeyDefinitions || newStateData.windowLevelData) {
|
||||
this.setState(newStateData);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="UserPreferencesForm">
|
||||
<UserPreferences
|
||||
windowLevelData={this.state.windowLevelData}
|
||||
hotkeyDefinitions={this.state.hotkeyDefinitions}
|
||||
/>
|
||||
<div className="footer">
|
||||
<button
|
||||
className="btn btn-danger pull-left"
|
||||
onClick={this.props.onResetToDefaults}
|
||||
>
|
||||
{this.props.t('Reset to Defaults')}
|
||||
</button>
|
||||
<div>
|
||||
<div onClick={this.props.onClose} className="btn btn-default">
|
||||
{this.props.t('Cancel')}
|
||||
</div>
|
||||
<button className="btn btn-primary" onClick={this.save}>
|
||||
{this.props.t('Save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const connectedComponent = withTranslation('UserPreferencesForm')(
|
||||
UserPreferencesForm
|
||||
);
|
||||
export { connectedComponent as UserPreferencesForm };
|
||||
export default connectedComponent;
|
||||
+10
-3
@@ -12,6 +12,13 @@
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
opacity: .2;
|
||||
|
||||
.ModalHeader
|
||||
ol, ul
|
||||
margin-top: 0;
|
||||
.UserPreferencesForm
|
||||
.footer
|
||||
display: flex
|
||||
flex-direction: row
|
||||
padding-bottom: 20px
|
||||
justify-content: space-between
|
||||
|
||||
div
|
||||
button:last-child
|
||||
margin-left: 10px
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+8
-8
@@ -1,18 +1,18 @@
|
||||
---
|
||||
name: User Preferences Modal
|
||||
name: User Preferences Form
|
||||
menu: Components
|
||||
route: /components/user-preferences-modal
|
||||
route: /components/user-preferences-form
|
||||
---
|
||||
|
||||
import { Playground, Props } from 'docz'
|
||||
import { State } from 'react-powerplug'
|
||||
import { UserPreferencesModal } from './../index.js'
|
||||
import { UserPreferencesForm } from './../index.js'
|
||||
import NameSpace from '../../../__docs__/NameSpace'
|
||||
//
|
||||
import windowLevelDefaults from './windowLevelDefaults.js'
|
||||
import hotkeyDefaults from './hotkeyDefaults.js'
|
||||
|
||||
# User Preferences Modal
|
||||
# User Preferences Form
|
||||
|
||||
## Basic usage
|
||||
|
||||
@@ -20,7 +20,7 @@ import hotkeyDefaults from './hotkeyDefaults.js'
|
||||
<State initial={{
|
||||
isOpen: false,
|
||||
windowLevelData: windowLevelDefaults,
|
||||
hotKeysData: hotkeyDefaults,
|
||||
hotkeyDefinitions: hotkeyDefaults,
|
||||
}}>
|
||||
|
||||
{({ state, setState }) => (
|
||||
@@ -32,7 +32,7 @@ import hotkeyDefaults from './hotkeyDefaults.js'
|
||||
>
|
||||
Open user preferences
|
||||
</button>
|
||||
<UserPreferencesModal
|
||||
<UserPreferencesForm
|
||||
{...state}
|
||||
onCancel={() => setState({ isOpen: false })}
|
||||
onSave={() => alert('on save')}
|
||||
@@ -47,8 +47,8 @@ import hotkeyDefaults from './hotkeyDefaults.js'
|
||||
|
||||
## API
|
||||
|
||||
<Props of={UserPreferencesModal} />
|
||||
<Props of={UserPreferencesForm} />
|
||||
|
||||
## Translation Namespace
|
||||
|
||||
<NameSpace name="UserPreferencesModal" />
|
||||
<NameSpace name="UserPreferencesForm" />
|
||||
File renamed without changes.
+28
-28
@@ -4,38 +4,38 @@ const range = (start, end) => {
|
||||
|
||||
export const disallowedCombinations = {
|
||||
'': [],
|
||||
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',
|
||||
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'],
|
||||
'ctrl+shift': ['q', 'w', 'r', 't', 'p', 'a', 'h', 'v', 'b', 'n'],
|
||||
};
|
||||
|
||||
export const allowedKeys = [
|
||||
...[8, 13, 27, 32, 46], // BACKSPACE, ENTER, ESCAPE, SPACE, DELETE
|
||||
...[8, 13, 27, 32, 46], // backspace, enter, escape, space, delete
|
||||
...[12, 106, 107, 109, 110, 111], // Numpad keys
|
||||
...range(218, 220), // [\]
|
||||
...range(185, 190), // ;=,-./
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
export { UserPreferences } from './UserPreferences.js';
|
||||
export { AboutContent } from '../content/aboutContent/AboutContent.js';
|
||||
export { UserPreferencesModal } from './UserPreferencesModal.js';
|
||||
export { UserPreferencesForm } from './UserPreferencesForm.js';
|
||||
export { GeneralPreferences } from './GeneralPreferences.js';
|
||||
@@ -1,107 +0,0 @@
|
||||
import './UserPreferencesModal.styl';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from 'react-bootstrap-modal';
|
||||
import { withTranslation } from '../../utils/LanguageProvider';
|
||||
|
||||
import 'react-bootstrap-modal/lib/css/rbm-patch.css';
|
||||
import cloneDeep from 'lodash.clonedeep';
|
||||
import isEqual from 'lodash.isequal';
|
||||
import { UserPreferences } from './UserPreferences';
|
||||
|
||||
// TODO: Is this the only component importing these?
|
||||
import './../../design/styles/common/modal.styl';
|
||||
|
||||
class UserPreferencesModal extends Component {
|
||||
// TODO: Make this component more generic to allow things other than W/L and hotkeys...
|
||||
static propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onCancel: PropTypes.func,
|
||||
onSave: PropTypes.func,
|
||||
onResetToDefaults: PropTypes.func,
|
||||
windowLevelData: PropTypes.object,
|
||||
hotKeysData: PropTypes.object,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
windowLevelData: cloneDeep(props.windowLevelData),
|
||||
hotKeysData: cloneDeep(props.hotKeysData),
|
||||
};
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
isOpen: false,
|
||||
};
|
||||
|
||||
save = () => {
|
||||
this.props.onSave({
|
||||
windowLevelData: this.state.windowLevelData,
|
||||
hotKeysData: this.state.hotKeysData,
|
||||
});
|
||||
};
|
||||
|
||||
componentDidUpdate(prev, next) {
|
||||
const newStateData = {};
|
||||
|
||||
if (!isEqual(prev.windowLevelData, next.windowLevelData)) {
|
||||
newStateData.windowLevelData = prev.windowLevelData;
|
||||
}
|
||||
|
||||
if (!isEqual(prev.hotKeysData, next.hotKeysData)) {
|
||||
newStateData.hotKeysData = prev.hotKeysData;
|
||||
}
|
||||
|
||||
if (newStateData.hotKeysData || newStateData.windowLevelData) {
|
||||
this.setState(newStateData);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Modal
|
||||
show={this.props.isOpen}
|
||||
onHide={this.props.onCancel}
|
||||
aria-labelledby="ModalHeader"
|
||||
className="ModalHeader modal fade themed in"
|
||||
backdrop={false}
|
||||
large={true}
|
||||
keyboard={false}
|
||||
>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>{this.props.t('User Preferences')}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<UserPreferences
|
||||
windowLevelData={this.state.windowLevelData}
|
||||
hotKeysData={this.state.hotKeysData}
|
||||
/>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button
|
||||
className="btn btn-danger pull-left"
|
||||
onClick={this.props.onResetToDefaults}
|
||||
>
|
||||
{this.props.t('Reset to Defaults')}
|
||||
</button>
|
||||
<Modal.Dismiss className="btn btn-default">
|
||||
{this.props.t('Cancel')}
|
||||
</Modal.Dismiss>
|
||||
<button className="btn btn-primary" onClick={this.save}>
|
||||
{this.props.t('Save')}
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const connectedComponent = withTranslation('UserPreferencesModal')(
|
||||
UserPreferencesModal
|
||||
);
|
||||
export { connectedComponent as UserPreferencesModal };
|
||||
export default connectedComponent;
|
||||
Reference in new issue
Block a user