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:
Rodrigo Antinarelli 2020-02-12 17:35:04 -03:00 committed by GitHub
parent 9a62c28b3f
commit 2f30e7a821
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
69 changed files with 1211 additions and 1632 deletions

View File

@ -1,4 +1,5 @@
export default {
warn: jest.fn(),
error: jest.fn(),
info: jest.fn(),
};

View File

@ -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;
}

View File

@ -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');
});
});

View File

@ -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;

View File

@ -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,

View File

@ -16,6 +16,7 @@ describe('Top level exports', () => {
'MeasurementService',
//
'utils',
'hotkeys',
'studies',
'redux',
'classes',

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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: '' },

View File

@ -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);
}
}
}

View File

@ -0,0 +1,8 @@
import Mousetrap from 'mousetrap';
import pausePlugin from './pausePlugin';
import recordPlugin from './recordPlugin';
recordPlugin(Mousetrap);
pausePlugin(Mousetrap);
export default Mousetrap;

View File

@ -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);

View File

@ -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;

View File

@ -18,6 +18,7 @@ describe('Top level exports', () => {
'DicomLoaderService',
'urlUtil',
'makeCancelable',
'hotkeys',
].sort();
const exports = Object.keys(utils.default).sort();

View File

@ -0,0 +1,70 @@
const languagesMap = {
ar: 'Arabic',
am: 'Amharic',
bg: 'Bulgarian',
bn: 'Bengali',
ca: 'Catalan',
cs: 'Czech',
da: 'Danish',
de: 'German',
el: 'Greek',
en: 'English',
'en-GB': 'English (Great Britain)',
'en-US': 'English (USA)',
es: 'Spanish',
et: 'Estonian',
fa: 'Persian',
fi: 'Finnish',
fil: 'Filipino',
fr: 'French',
gu: 'Gujarati',
he: 'Hebrew',
hi: 'Hindi',
hr: 'Croatian',
hu: 'Hungarian',
id: 'Indonesian',
it: 'Italian',
ja: 'Japanese',
'ja-JP': 'Japanese (Japan)',
kn: 'Kannada',
ko: 'Korean',
lt: 'Lithuanian',
lv: 'Latvian',
ml: 'Malayalam',
mr: 'Marathi',
ms: 'Malay',
nl: 'Dutch',
no: 'Norwegian',
pl: 'Polish',
'pt-BR': 'Portuguese (Brazil)',
'pt-PT': 'Portuguese (Portugal)',
ro: 'Romanian',
ru: 'Russian',
sk: 'Slovak',
sl: 'Slovenian',
sr: 'Serbian',
sv: 'Swedish',
sw: 'Swahili',
ta: 'Tamil',
te: 'Telugu',
th: 'Thai',
tr: 'Turkish',
uk: 'Ukrainian',
vi: 'Vietnamese',
zh: 'Chinese',
'zh-CN': 'Chinese (China)',
'zh-TW': 'Chinese (Taiwan)',
};
export default function getAvailableLanguagesInfo(locales) {
const availableLanguagesInfo = [];
Object.keys(locales).forEach(key => {
availableLanguagesInfo.push({
value: key,
label: languagesMap[key] || key,
});
});
return availableLanguagesInfo;
}

View File

@ -140,4 +140,7 @@ i18n.initI18n = initI18n;
i18n.addLocales = addLocales;
i18n.defaultLanguage = DEFAULT_LANGUAGE;
import getAvailableLanguagesInfo from './getAvailableLanguagesInfo.js';
i18n.availableLanguages = getAvailableLanguagesInfo(locales);
export default i18n;

View File

@ -0,0 +1,82 @@
import React from 'react';
import PropTypes from 'prop-types';
import { hotkeys } from '@ohif/core';
/**
* Take the pressed key array and return the readable string for the keys
*
* @param {Array} [keys=[]]
* @returns {string} string representation of an array of keys
*/
const formatKeysForInput = (keys = []) => keys.join('+');
/**
* formats given keys sequence to insert the modifier keys in the first index of the array
* @param {string} sequence keys sequence from MouseTrap Record -> "shift+left"
* @returns {Array} keys in array-format -> ['shift','left']
*/
const getKeys = ({ sequence, modifier_keys }) => {
const keysArray = sequence.join(' ').split('+');
let keys = [];
let modifiers = [];
keysArray.forEach(key => {
if (modifier_keys && modifier_keys.includes(key)) {
modifiers.push(key);
} else {
keys.push(key);
}
});
return [...modifiers, ...keys];
};
/**
* HotkeyField
* Renders a hotkey input
*
* @param {object} props component props
* @param {Array[]} props.keys array of keys to be controlled by this field
* @param {function} props.handleChange Callback function to communicate parent once value is changed
* @param {string} props.classNames string caontaining classes to be added in the input field
* @param {Array[]} props.modifier_keys
*/
function HotkeyField({ keys, handleChange, classNames, modifier_keys }) {
const inputValue = formatKeysForInput(keys);
const onInputKeyDown = event => {
event.stopPropagation();
event.preventDefault();
hotkeys.record(sequence => {
const keys = getKeys({ sequence, modifier_keys });
hotkeys.unpause();
handleChange(keys);
});
};
const onFocus = () => {
hotkeys.pause();
hotkeys.startRecording();
};
return (
<input
readOnly={true}
type="text"
value={inputValue}
className={classNames}
onKeyDown={onInputKeyDown}
onFocus={onFocus}
/>
);
}
HotkeyField.propTypes = {
keys: PropTypes.array.isRequired,
handleChange: PropTypes.func.isRequired,
classNames: PropTypes.string,
modifier_keys: PropTypes.array,
allowed_keys: PropTypes.array,
};
export { HotkeyField };

View File

@ -0,0 +1 @@
export { HotkeyField } from './HotkeyField';

View File

@ -3,11 +3,10 @@ import { LayoutButton, LayoutChooser } from './layoutButton';
import { MeasurementTable, MeasurementTableItem } from './measurementTable';
import { Overlay, OverlayTrigger } from './overlayTrigger';
import { TableList, TableListItem } from './tableList';
import {
AboutContent,
UserPreferences,
UserPreferencesForm,
} from './userPreferencesForm';
import { AboutContent } from './content/aboutContent/AboutContent';
import { TabComponents, TabFooter } from './tabComponents';
import { HotkeyField } from './customForm';
import { LanguageSwitcher } from './languageSwitcher';
import { Checkbox } from './checkbox';
import { CineDialog } from './cineDialog';
@ -48,12 +47,14 @@ export {
TableList,
TableListItem,
Thumbnail,
TabComponents,
TabFooter,
HotkeyField,
LanguageSwitcher,
TableSearchFilter,
TablePagination,
ToolbarSection,
Tooltip,
AboutContent,
UserPreferences,
UserPreferencesForm,
OHIFModal,
};

View File

@ -2,26 +2,11 @@ import React from 'react';
import PropTypes from 'prop-types';
import './LanguageSwitcher.styl';
import { withTranslation } from '../../contextProviders';
const LanguageSwitcher = ({ language, onLanguageChange }) => {
const parseLanguage = lang => lang.split('-')[0];
const languages = [
// TODO: list of available languages should come from i18n.options.resources
{
value: 'en',
label: 'English',
},
{
value: 'es',
label: 'Spanish',
},
];
const LanguageSwitcher = ({ language, onLanguageChange, languages }) => {
const onChange = event => {
const { value } = event.target;
onLanguageChange(parseLanguage(value));
onLanguageChange(value);
};
return (
@ -29,7 +14,7 @@ const LanguageSwitcher = ({ language, onLanguageChange }) => {
name="language-select"
id="language-select"
className="language-select"
value={parseLanguage(language)}
value={language}
onChange={onChange}
>
{languages.map(lng => (
@ -43,7 +28,8 @@ const LanguageSwitcher = ({ language, onLanguageChange }) => {
LanguageSwitcher.propTypes = {
language: PropTypes.string.isRequired,
languages: PropTypes.array.isRequired,
onLanguageChange: PropTypes.func.isRequired,
};
export default withTranslation('UserPreferencesModal')(LanguageSwitcher);
export { LanguageSwitcher };

View File

@ -1 +1 @@
export { default } from './LanguageSwitcher';
export { LanguageSwitcher } from './LanguageSwitcher';

View File

@ -0,0 +1,105 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
// Style
import './TabComponents.styl';
/**
* Take name of the tab and create the data-cy value for it
*
* @param {string} [name=''] tab name
* @returns {string} data-cy value
*/
const getDataCy = (name = '') => {
return name
.split(' ')
.join('-')
.toLowerCase();
};
/**
* Single tab data information
*
* @typedef {Object} tabData
* @property {string} name - name of the tab
* @property {Object} Component - tab component to be rendered
* @property {Object} customProps - tab custom properties
* @property {bool} hidden - bool to define if tab is hidden of not
*/
/**
* Take a list of components data and render then into tabs
*
* @param {Object} props
* @param {[tabData]} props.tabs array of tab data
* @param {Object} props.customProps common custom properties
*/
function TabComponents({ tabs, customProps = {} }) {
const [currentTabIndex, setCurrentTabIndex] = useState(0);
return (
tabs.length > 0 && (
<div className="TabComponents">
<div className="TabComponents_tabHeader">
<div className="TabComponents_tabHeader_selector">
<div className="dialog-separator-after">
<ul className="nav nav-tabs">
{tabs.map((tab, index) => {
const { name, hidden } = tab;
return (
!hidden && (
<li
key={index}
onClick={() => {
setCurrentTabIndex(index);
}}
className={classnames(
'nav-link',
index === currentTabIndex && 'active'
)}
data-cy={getDataCy(name)}
>
<button>{name}</button>
</li>
)
);
})}
</ul>
</div>
</div>
</div>
{tabs.map((tab, index) => {
const { Component, customProps: tabCustomProps, hidden } = tab;
return (
!hidden && (
<div
key={index}
className={classnames(
'TabComponents_content',
index === currentTabIndex && 'active'
)}
>
<Component {...customProps} {...tabCustomProps} />
</div>
)
);
})}
</div>
)
);
}
TabComponents.propTypes = {
tabs: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string,
Component: PropTypes.any,
customProps: PropTypes.object,
hidden: PropTypes.bool,
})
),
customProps: PropTypes.object,
};
export { TabComponents };

View File

@ -0,0 +1,21 @@
@import './../../design/styles/common/navbar.styl'
.TabComponents
.TabComponents_tabHeader
display: flex
flex-direction: column
margin-left: -20px
margin-right: -20px
.TabComponents_tabHeader_selector
border-bottom: 3px solid black
padding: 0 25px
.TabComponents_content
min-height: 450px
display: none
&.active
display: flex
flex-direction: column
justify-content: space-between

View File

@ -0,0 +1,54 @@
import React from 'react';
import PropTypes from 'prop-types';
import './TabFooter.styl';
// In case translate is not passed
const translate = word => word;
function TabFooter({
onResetPreferences,
onSave,
onCancel,
hasErrors,
t = translate,
}) {
return (
<div className="footer">
<button
className="btn btn-danger pull-left"
data-cy="reset-default-btn"
onClick={onResetPreferences}
>
{t('Reset to Defaults')}
</button>
<div>
<div
onClick={onCancel}
data-cy="cancel-btn"
className="btn btn-default"
>
{t('Cancel')}
</div>
<button
className="btn btn-primary"
data-cy="save-btn"
disabled={hasErrors}
onClick={onSave}
>
{t('Save')}
</button>
</div>
</div>
);
}
TabFooter.propTypes = {
onResetPreferences: PropTypes.func,
onSave: PropTypes.func,
onCancel: PropTypes.func,
hasErrors: PropTypes.bool,
t: PropTypes.func,
};
export { TabFooter };

View File

@ -0,0 +1,11 @@
.footer
display: flex
flex-direction: row
justify-content: space-between
padding: 20px 20px 0 20px
margin: 0 -20px
border-top: 3px solid var(--primary-background-color)
div
button:last-child
margin-left: 10px

View File

@ -0,0 +1,27 @@
---
name: Tab Components
menu: Components
route: /components/tab-components
---
import { Playground, Props } from 'docz'
import { State } from 'react-powerplug'
import { TabComponents } from './../index.js'
// Data
import tabs from './tabs.js'
# Tab Components
## Basic usage
<Playground>
<TabComponents
tabs={tabs}
customProps={}
/>
</Playground>
## API
<Props of={TabComponents} />

View File

@ -0,0 +1,26 @@
export default tabs = [
{
name: 'tabName1',
Component: () => {
return <div>tab 1 Content</div>;
},
customProps: {},
hidden: false,
},
{
name: 'tabName2',
Component: () => {
return <div>tab 2 Content</div>;
},
customProps: {},
hidden: false,
},
{
name: 'tabName3',
Component: () => {
return <div>tab 3 Content</div>;
},
customProps: {},
hidden: true,
},
];

View File

@ -0,0 +1,2 @@
export { TabComponents } from './TabComponents';
export { TabFooter } from './TabFooter';

View File

@ -1,57 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import LanguageSwitcher from '../languageSwitcher';
import i18n from '@ohif/i18n';
/**
* General Preferences tab
*/
/**
* General Preferences tab
* It renders the General Preferences content
*
* 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.name Tab`s name
* @param {object} props.generalPreferences Data for initial state
* @param {function} props.onTabStateChanged Callback function to communicate parent in case its states changes
* @param {function} props.onTabErrorChanged Callback Function in case any error on tab
*/
function GeneralPreferences({
generalPreferences,
name,
onTabStateChanged,
onTabErrorChanged,
}) {
const { language = i18n.language } = generalPreferences;
const onLanguageChange = language => {
onTabStateChanged(name, {
generalPreferences: { ...generalPreferences, language },
});
};
return (
<div className="general-preferences-wrapper">
<div className="col-sm-3">
<label htmlFor="language-select" className="p-r-1">
Language
</label>
<LanguageSwitcher
language={language}
onLanguageChange={onLanguageChange}
/>
</div>
</div>
);
}
GeneralPreferences.propTypes = {
generalPreferences: PropTypes.any,
name: PropTypes.string,
onTabStateChanged: PropTypes.func,
onTabErrorChanged: PropTypes.func,
};
export { GeneralPreferences };

View File

@ -1,452 +0,0 @@
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import './HotKeysPreferences.styl';
import {
allowedKeys,
disallowedCombinations,
specialKeys,
} from './hotKeysConfig.js';
import isEqual from 'lodash.isequal';
const getKeysPressedArray = keyDownEvent => {
const keysPressedArray = [];
const { ctrlKey, altKey, shiftKey } = keyDownEvent;
if (ctrlKey && !altKey) {
keysPressedArray.push('ctrl');
}
if (shiftKey && !altKey) {
keysPressedArray.push('shift');
}
if (altKey && !ctrlKey) {
keysPressedArray.push('alt');
}
return keysPressedArray;
};
const findConflictingCommand = (
originalHotKeys,
currentCommandName,
currentHotKeys
) => {
let firstConflictingCommand = undefined;
for (const commandName in originalHotKeys) {
const toolHotKeys = originalHotKeys[commandName].keys;
if (
isEqual(toolHotKeys, currentHotKeys) &&
commandName !== currentCommandName
) {
firstConflictingCommand = originalHotKeys[commandName];
break;
}
}
return firstConflictingCommand;
};
/**
* Splits given keysObj into arrays. Each array item will be a representation of column
* @param {obj} keysObj objects to be splitted into columns
* @param {number} columnSize How many rows per column
*/
const getHotKeysArrayColumns = (keysObj = {}, columnSize) => {
if (isNaN(columnSize)) {
return keysObj;
}
const keys = Object.keys(keysObj);
const keysValues = Object.values(keysObj);
const keysLength = keys.length;
// Columns from left should be bigger;
let currentColumn = 0;
const dividedKeys = [];
for (
let it = 0;
it < keysLength;
it++, it % columnSize === 0 ? currentColumn++ : currentColumn
) {
if (!dividedKeys[currentColumn]) {
dividedKeys[currentColumn] = [];
}
dividedKeys[currentColumn][keys[it]] = keysValues[it];
}
return dividedKeys;
};
const NO_FIELD_ERROR_MESSAGE = undefined;
const formatPressedKeys = pressedKeysArray => pressedKeysArray.join('+');
const unFormatPressedKeys = (pressedKeysStr = '') => pressedKeysStr.split('+');
const inputValidators = (
commandName,
inputValue,
pressedKeys,
lastPressedKey,
originalHotKeys
) => {
let hasError = false;
let errorMessage = NO_FIELD_ERROR_MESSAGE;
const modifierValidator = ({ lastPressedKey }) => {
// Check if it has a valid modifier
const isModifier = ['ctrl', 'alt', 'shift'].includes(lastPressedKey);
if (isModifier) {
hasError = true;
errorMessage =
"It's not possible to define only modifier keys (ctrl, alt and shift) as a shortcut";
return {
hasError,
errorMessage,
};
}
};
const emptyValidator = ({ inputValue }) => {
if (!inputValue) {
hasError = true;
errorMessage = "Field can't be empty.";
return {
hasError,
errorMessage,
};
}
};
const conflictingValidator = ({
commandName,
pressedKeys,
originalHotKeys,
}) => {
const conflictingCommand = findConflictingCommand(
originalHotKeys,
commandName,
pressedKeys
);
if (conflictingCommand) {
hasError = true;
errorMessage = `"${conflictingCommand.label}" is already using the "${pressedKeys}" shortcut.`;
return {
hasError,
errorMessage,
};
}
};
const disallowedValidator = ({ inputValue, pressedKeys, lastPressedKey }) => {
const modifierCommand = formatPressedKeys(
pressedKeys.slice(0, pressedKeys.length - 1)
);
const disallowedCombination = disallowedCombinations[modifierCommand];
const hasDisallowedCombinations = disallowedCombination
? disallowedCombination.includes(lastPressedKey)
: false;
if (hasDisallowedCombinations) {
hasError = true;
errorMessage = `"${inputValue}" shortcut combination is not allowed`;
return {
hasError,
errorMessage,
};
}
};
const validators = [
emptyValidator,
modifierValidator,
conflictingValidator,
disallowedValidator,
];
for (const validator of validators) {
const validation = validator({
commandName,
inputValue,
pressedKeys,
lastPressedKey,
originalHotKeys,
});
if (validation && validation.hasError) {
return validation;
}
}
// validation has passed successfully
return {
hasError,
errorMessage,
};
};
/**
* HotKeysPreferencesRow
* Renders row for hotkey preference
* 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.commandName command name associated to given row
* @param {string[]} props.hotkeys keys associated to given command
* @param {object} props.originalHotKeys original hotkeys values
* @param {function} props.onSuccessChanged Callback function to communicate parent in case its states changes
* @param {function} props.onFailureChanged Callback Function in case any error on row
*/
function HotKeyPreferencesRow({
commandName,
hotkeys,
label,
originalHotKeys,
tabError,
onSuccessChanged,
onFailureChanged,
}) {
const [inputValue, setInputValue] = useState(formatPressedKeys(hotkeys));
const [error, setError] = useState(false);
const [fieldErrorMessage, setFieldErrorMessage] = useState(
NO_FIELD_ERROR_MESSAGE
);
// reset error count if tab has no errors
useEffect(() => {
if (!tabError) {
setError(false);
setFieldErrorMessage(NO_FIELD_ERROR_MESSAGE);
setInputValue(formatPressedKeys(hotkeys));
}
}, [tabError]);
// update state values if props changes
useEffect(() => {
setInputValue(formatPressedKeys(hotkeys));
}, [hotkeys]);
const updateInputText = (keyDownEvent, displayPressedKey = false) => {
const pressedKeys = getKeysPressedArray(keyDownEvent);
if (displayPressedKey) {
const specialKeyName = specialKeys[keyDownEvent.which];
const keyName =
specialKeyName ||
keyDownEvent.key ||
String.fromCharCode(keyDownEvent.keyCode);
// ensure lowerCase
pressedKeys.push(keyName.toLowerCase());
}
setInputValue(formatPressedKeys(pressedKeys));
};
// validate input value
const validateInput = () => {
const pressedKeys = unFormatPressedKeys(inputValue);
const lastPressedKey = pressedKeys[pressedKeys.length - 1];
const {
hasError = false,
errorMessage = NO_FIELD_ERROR_MESSAGE,
} = inputValidators(
commandName,
inputValue,
pressedKeys,
lastPressedKey,
originalHotKeys
);
if (hasError) {
setInputValue('');
} else {
onSuccessChanged([inputValue]);
}
if (hasError !== error) {
setError(hasError);
}
setFieldErrorMessage(errorMessage);
};
useEffect(() => {
onFailureChanged(error);
}, [error]);
const onInputKeyDown = event => {
// Prevent ESC key from propagating and closing the modal
if (event.key === 'Escape') {
event.stopPropagation();
}
updateInputText(event, allowedKeys.includes(event.keyCode));
event.preventDefault();
};
return (
<tr key={commandName}>
<td className="text-right p-r-1">{label}</td>
<td width="200">
<label
className={`wrapperLabel ${
fieldErrorMessage !== undefined ? 'state-error' : ''
} `}
data-key="defaultTool"
>
<input
readOnly={true}
type="text"
value={inputValue}
className="form-control hotkey text-center"
onKeyDown={onInputKeyDown}
onBlur={validateInput}
/>
<span className="wrapperText" />
<span className="errorMessage">{fieldErrorMessage}</span>
</label>
</td>
</tr>
);
}
HotKeyPreferencesRow.propTypes = {
commandName: PropTypes.string.isRequired,
hotkeys: PropTypes.array.isRequired,
label: PropTypes.string.isRequired,
originalHotKeys: PropTypes.object.isRequired,
tabError: PropTypes.bool.isRequired,
onSuccessChanged: PropTypes.func.isRequired,
onFailureChanged: PropTypes.func.isRequired,
};
/**
* 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.name Tab`s name
* @param {object} props.hotkeyDefinitions Data for initial state
* @param {function} props.onTabStateChanged Callback function to communicate parent in case its states changes
* @param {function} props.onTabErrorChanged Callback Function in case any error on tab
*/
function HotKeysPreferences({
hotkeyDefinitions,
name,
tabError,
onTabStateChanged,
onTabErrorChanged,
}) {
const [tabState, setTabState] = useState(hotkeyDefinitions);
const [tabErrorCounter, setTabErrorCounter] = useState(0);
const [numColumns] = useState(2);
const [columnSize] = useState(() =>
Math.ceil(Object.keys(tabState || {}).length / numColumns)
);
const splittedHotKeys = getHotKeysArrayColumns(tabState, columnSize);
const onHotKeyChanged = (commandName, hotkeyDefinition, keys) => {
const newState = {
...tabState,
[commandName]: { ...hotkeyDefinition, keys },
};
setTabState(newState);
onTabStateChanged(name, { hotkeyDefinitions: newState });
};
const onErrorChanged = (toInc = true) => {
const increment = toInc ? 1 : -1;
const newValue = tabErrorCounter + increment;
if (newValue >= 0) {
setTabErrorCounter(newValue);
}
};
// reset error count if tab has no errors
useEffect(() => {
if (!tabError) {
setTabErrorCounter(0);
// update tab state
setTabState({ ...hotkeyDefinitions });
}
}, [tabError]);
// tell parent to update its state
useEffect(() => {
if (tabErrorCounter === 0) {
onTabErrorChanged(name, false);
}
if (tabErrorCounter === 1) {
onTabErrorChanged(name, true);
}
}, [tabErrorCounter]);
// update local state if parent updates
useEffect(() => {
setTabState({ ...hotkeyDefinitions });
}, [hotkeyDefinitions]);
return (
<div className="HotKeysPreferences">
{splittedHotKeys.length > 0
? splittedHotKeys.map((columnHotKeys, index) => {
return (
<div className="column" key={index}>
<table className="full-width">
<thead>
<tr>
<th className="text-right p-r-1">Function</th>
<th className="text-center">Shortcut</th>
</tr>
</thead>
<tbody>
{Object.entries(columnHotKeys).map(
hotkeyDefinitionTuple => (
<HotKeyPreferencesRow
key={hotkeyDefinitionTuple[0]}
commandName={hotkeyDefinitionTuple[0]}
hotkeys={hotkeyDefinitionTuple[1].keys}
label={hotkeyDefinitionTuple[1].label}
originalHotKeys={tabState}
tabError={tabError}
onSuccessChanged={keys =>
onHotKeyChanged(
hotkeyDefinitionTuple[0],
hotkeyDefinitionTuple[1],
keys
)
}
onFailureChanged={onErrorChanged}
></HotKeyPreferencesRow>
)
)}
</tbody>
</table>
</div>
);
})
: null}
</div>
);
}
HotKeysPreferences.propTypes = {
hotkeyDefinitions: PropTypes.any,
name: PropTypes.string,
tabError: PropTypes.bool,
onTabStateChanged: PropTypes.func,
onTabErrorChanged: PropTypes.func,
};
export { HotKeysPreferences };

View File

@ -1,12 +0,0 @@
.HotKeysPreferences
display: flex;
.column
width: 50%;
padding-right: 15px;
padding-left: 15px;
.column-full
width: 100%;
padding-right: 15px;
padding-left: 15px;

View File

@ -1,132 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { HotKeysPreferences } from './HotKeysPreferences';
import { WindowLevelPreferences } from './WindowLevelPreferences';
import { GeneralPreferences } from './GeneralPreferences';
import './UserPreferences.styl';
export class UserPreferences extends Component {
static defaultProps = {
hotkeyDefinitions: [],
windowLevelData: {},
generalPreferences: {},
};
// TODO: Make this more generic. Tabs should not be restricted to these entries
static propTypes = {
hotkeyDefinitions: PropTypes.arrayOf(
PropTypes.shape({
commandName: PropTypes.string,
keys: PropTypes.arrayOf(PropTypes.string),
label: PropTypes.string,
})
).isRequired,
windowLevelData: PropTypes.object.isRequired,
generalPreferences: PropTypes.object.isRequired,
updatePropValue: PropTypes.func.isRequired,
};
state = {
tabIndex: 0,
};
tabClick(tabIndex) {
this.setState({ tabIndex });
}
renderHotkeysTab() {
return (
<form className="form-themed themed">
<div className="form-content">
<HotKeysPreferences
hotkeyDefinitions={this.props.hotkeyDefinitions}
/>
</div>
</form>
);
}
renderWindowLevelTab() {
if (this.props.windowLevelData) {
return (
<form className="form-themed themed">
<div className="form-content">
<WindowLevelPreferences
windowLevelData={this.props.windowLevelData}
/>
</div>
</form>
);
}
}
renderGeneralTab() {
return (
<form className="form-themed themed">
<div className="form-content">
<GeneralPreferences
generalPreferences={this.props.generalPreferences}
updatePropValue={this.props.updatePropValue}
/>
</div>
</form>
);
}
renderTabs(tabIndex) {
switch (tabIndex) {
case 0:
return this.renderHotkeysTab();
/* case 1:
return this.renderWindowLevelTab(); */
case 2:
return this.renderGeneralTab();
default:
break;
}
}
getTabClass(tabIndex) {
return tabIndex === this.state.tabIndex ? 'nav-link active' : 'nav-link';
}
render() {
return (
<div className="UserPreferences">
<div className="UserPreferences__selector">
<ul className="nav nav-tabs">
<li
onClick={() => {
this.tabClick(0);
}}
className={this.getTabClass(0)}
>
<button>Hotkeys</button>
</li>
{false && (
<li
onClick={() => {
this.tabClick(1);
}}
className={this.getTabClass(1)}
>
<button>Window Level</button>
</li>
)}
<li
onClick={() => {
this.tabClick(2);
}}
className={this.getTabClass(2)}
>
<button>General</button>
</li>
</ul>
</div>
{this.renderTabs(this.state.tabIndex)}
</div>
);
}
}

View File

@ -1,219 +0,0 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { useSnackbarContext } from '@ohif/ui';
import './UserPreferencesForm.styl';
import { useTranslation } from 'react-i18next';
// Tabs Component wrapper
import { UserPreferencesTabs } from './UserPreferencesTabs';
// Tabs
import { HotKeysPreferences } from './HotKeysPreferences';
import { WindowLevelPreferences } from './WindowLevelPreferences';
import { GeneralPreferences } from './GeneralPreferences';
/**
@typedef TabObject
@type {Object}
@property {string} name Name for given tab
@property {ReactComponent} Component React component for given tab.
@property {object} props Props State for given tab component
@property {boolean} [hidden] To hidden tab or not
*/
/**
* Create tabs obj.
* @returns {TabObject[]} Array of TabObjs.
*/
const createTabs = () => {
return [
{
name: 'Hotkeys',
Component: HotKeysPreferences,
props: {},
},
{
name: 'General',
Component: GeneralPreferences,
props: {},
},
{
name: 'Window Level',
Component: WindowLevelPreferences,
props: {},
hidden: true,
},
];
};
/**
* Main form component to render preferences tabs and buttons
* @param {object} props component props
* @param {string} props.name Tab`s name
* @param {object} props.hotkeyDefinitions Hotkeys Data
* @param {object} props.windowLevelData Window level data
* @param {function} props.onSave Callback function when saving
* @param {function} props.onClose Callback function when closing
* @param {function} props.onResetToDefaults Callback function when resetting
*/
function UserPreferencesForm({
onClose,
onSave,
onResetToDefaults,
windowLevelData,
hotkeyDefinitions,
generalPreferences,
hotkeysManager,
defaultLanguage,
hotkeyDefaults,
}) {
const [tabs, setTabs] = useState(createTabs());
const createTabsState = (
windowLevelData,
hotkeyDefinitions,
generalPreferences
) => {
return {
Hotkeys: { hotkeyDefinitions },
'Window Level': { windowLevelData },
General: { generalPreferences },
};
};
const [tabsState, setTabsState] = useState(
createTabsState(windowLevelData, hotkeyDefinitions, generalPreferences)
);
const [tabsError, setTabsError] = useState(
tabs.reduce((acc, tab) => {
acc[tab.name] = false;
return acc;
}, {})
);
const snackbar = useSnackbarContext();
const { t, ready: translationsAreReady } = useTranslation(
'UserPreferencesModal'
);
const onTabStateChanged = (tabName, newState) => {
setTabsState({ ...tabsState, [tabName]: newState });
};
const onTabErrorChanged = (tabName, hasError) => {
setTabsError({ ...tabsError, [tabName]: hasError });
};
const hasAnyError = () => {
return Object.values(tabsError).reduce((acc, value) => acc || value);
};
const onResetPreferences = () => {
const defaultHotKeyDefitions = {};
hotkeyDefaults.map(item => {
const { commandName, ...values } = item;
defaultHotKeyDefitions[commandName] = { ...values };
});
// update local state
setTabsState({
...tabsState,
Hotkeys: { hotkeyDefinitions: defaultHotKeyDefitions },
General: { generalPreferences: { language: defaultLanguage } },
});
// update tabs state
setTabs(createTabs(windowLevelData, hotkeyDefinitions, generalPreferences));
// reset errors
setTabsError(
tabs.reduce((acc, tab) => {
acc[tab.name] = false;
return acc;
}, {})
);
snackbar.show({
message: (
<div dangerouslySetInnerHTML={{ __html: t('ResetDefaultMessage') }} />
),
type: 'info',
});
};
const onSavePreferences = event => {
const toSave = Object.values(tabsState).reduce((acc, tabState) => {
return { ...acc, ...tabState };
}, {});
onSave(toSave);
snackbar.show({
message: t('SaveMessage'),
type: 'success',
});
};
// update local state if prop values changes
useEffect(() => {
setTabsState(
createTabsState(windowLevelData, hotkeyDefinitions, generalPreferences)
);
}, [windowLevelData, hotkeyDefinitions, generalPreferences]);
return translationsAreReady ? (
<div className="UserPreferencesForm">
<UserPreferencesTabs
tabs={tabs}
tabsState={tabsState}
tabsError={tabsError}
onTabStateChanged={onTabStateChanged}
onTabErrorChanged={onTabErrorChanged}
/>
<div className="footer">
<button
className="btn btn-danger pull-left"
data-cy="reset-default-btn"
onClick={onResetPreferences}
>
{t('Reset to Defaults')}
</button>
<div>
<div
onClick={onClose}
data-cy="cancel-btn"
className="btn btn-default"
>
{t('Cancel')}
</div>
<button
className="btn btn-primary"
data-cy="save-btn"
disabled={hasAnyError()}
onClick={onSavePreferences}
>
{t('Save')}
</button>
</div>
</div>
</div>
) : null;
}
UserPreferencesForm.propTypes = {
onClose: PropTypes.func,
onSave: PropTypes.func,
onResetToDefaults: PropTypes.func,
windowLevelData: PropTypes.object,
hotkeyDefinitions: PropTypes.object,
generalPreferences: PropTypes.object,
hotkeysManager: PropTypes.object,
defaultLanguage: PropTypes.string,
hotkeyDefaults: PropTypes.array,
};
export { UserPreferencesForm };

View File

@ -1,23 +0,0 @@
@import './../../design/styles/common/navbar.styl'
@import './../../design/styles/common/global.styl'
@import './../../design/styles/common/modal.styl'
@import './../../design/styles/common/button.styl'
.close
float: right;
font-size: 21px;
font-weight: 700;
line-height: 1;
color: #000;
text-shadow: 0 1px 0 #fff;
opacity: .2;
.UserPreferencesForm
.footer
display: flex
flex-direction: row
justify-content: space-between
div
button:last-child
margin-left: 10px

View File

@ -1,107 +0,0 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import './UserPreferencesTabs.styl';
/**
* Render tab component
* @param {object} tab TabObject containing tab data
* @param {function} onTabStateChanged Callback Function in case tab changes its state
* @param {function} onTabErrorChanged Callback Function in case any error on tab
*/
const renderTab = (
tab = {},
tabsState,
tabsError,
onTabStateChanged,
onTabErrorChanged
) => {
const { props, Component, name, hidden = false } = tab;
const tabState = tabsState[name];
const tabError = tabsError[name];
return !hidden ? (
<form className="form-themed themed tabs">
<div className="form-content">
<Component
key={name}
{...tabState}
{...props}
name={name}
tabError={tabError}
onTabStateChanged={onTabStateChanged}
onTabErrorChanged={onTabErrorChanged}
></Component>
</div>
</form>
) : null;
};
const renderTabsHeader = (tabs, activeTabIndex, onHeaderChanged) => {
return tabs.length > 0
? tabs.map((tab, index) => {
const { name, hidden = false } = tab;
const cypressSelectorId = name.toLowerCase();
const tabClass =
index === activeTabIndex ? 'nav-link active' : 'nav-link';
return !hidden ? (
<li
key={name}
onClick={() => {
onHeaderChanged(index);
}}
className={tabClass}
data-cy={cypressSelectorId}
>
<button>{name}</button>
</li>
) : null;
})
: null;
};
/**
* Component to render tabs based on currentActiveTabIndex
*
* In case any tab changes its state this current component tells parent through function callback
* @param {object} props Component props
*/
function UserPreferencesTabs({
tabs,
tabsState,
tabsError,
onTabStateChanged,
onTabErrorChanged,
}) {
const [activeTabIndex, setActiveTabIndex] = useState(0);
return (
<div className="UserPreferencesTabs">
<div className="UserPreferencesTabs__selector">
<div className="dialog-separator-after">
<ul className="nav nav-tabs">
{renderTabsHeader(tabs, activeTabIndex, setActiveTabIndex)}
</ul>
</div>
</div>
{renderTab(
tabs[activeTabIndex],
tabsState,
tabsError,
onTabStateChanged,
onTabErrorChanged
)}
</div>
);
}
UserPreferencesTabs.propTypes = {
tabs: PropTypes.array.isRequired,
tabsState: PropTypes.object.isRequired,
tabsError: PropTypes.object.isRequired,
onTabStateChanged: PropTypes.func.isRequired,
onTabErrorChanged: PropTypes.func.isRequired,
};
export { UserPreferencesTabs };

View File

@ -1,29 +0,0 @@
@import './../../design/styles/common/form.styl'
@import './../../design/styles/common/navbar.styl'
@import './../../design/styles/common/state.styl'
@import './../../design/styles/common/global.styl'
.UserPreferencesTabs
display: flex
flex-direction: column
&__selector
border-bottom: 3px solid black
.errorMessage
color: var(--state-error-text)
font-size: 10px
text-transform: uppercase;
.form-content
border-bottom: 3px solid var(--primary-background-color)
margin-bottom: 20px
margin-left: -20px
margin-right: -20px
max-height: 70vh
overflow-y: auto
padding: 20px
min-height: 500px
.popover
width: 300px

View File

@ -1,132 +0,0 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import './WindowLevelPreferences.styl';
/**
* WindowLevelPreferencesRow
* Renders row for window level preference
* 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.description description for given preset
* @param {number} props.window window value
* @param {number} props.level level value
* @param {string} props.rowName name of given row to identify it
* @param {function} props.onSuccessChanged Callback function to communicate parent in case its states changes
*/
function WindowLevelPreferencesRow({
description,
window,
level,
rowName,
onSuccessChanged,
// onFailureChanged
}) {
const [rowState, setRowState] = useState({ description, window, level });
const onInputChanged = (event, name) => {
const newValue = event.target.value;
setRowState({ ...rowState, [name]: newValue });
};
useEffect(() => {
onSuccessChanged(rowName, rowState);
}, [rowState]);
const renderTd = (value, name, type) => {
return (
<td className="p-r-1">
<label className="wrapperLabel">
<input
value={value}
type={type}
className="form-control"
onChange={event => {
onInputChanged(event, name);
}}
/>
</label>
</td>
);
};
return (
<tr key={rowName}>
<td className="p-r-1 text-center">{rowName}</td>
{renderTd(rowState.description, 'description', 'text')}
{renderTd(rowState.window, 'window', 'number')}
{renderTd(rowState.level, 'level', 'number')}
</tr>
);
}
WindowLevelPreferencesRow.propTypes = {
description: PropTypes.string.isRequired,
window: PropTypes.number.isRequired,
level: PropTypes.number.isRequired,
rowName: PropTypes.string.isRequired,
onSuccessChanged: PropTypes.func.isRequired,
//onFailureChanged: PropTypes.func.isRequired,
};
/**
* WindowLevelPreferences tab
* It renders all window level presets
*
* 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.name Tab`s name
* @param {object} props.windowLevelData Data for initial state
* @param {function} props.onTabStateChanged Callback function to communicate parent in case its states changes
*/
function WindowLevelPreferences({
windowLevelData,
name,
onTabStateChanged /*onTabErrorChanged*/,
}) {
const [tabState, setTabState] = useState(windowLevelData);
// TODO to be used once error handling is implemented
//const [tabError, setTabError] = useState(false);
const onWindowLevelChanged = (key, state) => {
setTabState({ ...tabState, [key]: state });
};
// tell parent to update its state
useEffect(() => {
onTabStateChanged(name, { windowLevelData: tabState });
}, [tabState]);
return (
<table className="full-width">
<thead>
<tr>
<th className="p-x-1 text-center presetIndex">Preset</th>
<th className="p-x-1">Description</th>
<th className="p-x-1">Window</th>
<th className="p-x-1">Level</th>
</tr>
</thead>
<tbody>
{Object.keys(tabState).map(objKey => (
<WindowLevelPreferencesRow
onSuccessChanged={onWindowLevelChanged}
rowName={objKey}
key={objKey}
description={tabState[objKey].description}
window={tabState[objKey].window}
level={tabState[objKey].level}
></WindowLevelPreferencesRow>
))}
</tbody>
</table>
);
}
WindowLevelPreferences.propTypes = {
windowLevelData: PropTypes.object.isRequired,
name: PropTypes.string.isRequired,
onTabStateChanged: PropTypes.func.isRequired,
//onTabErrorChanged: PropTypes.func.isRequired,
};
export { WindowLevelPreferences };

View File

@ -1,4 +0,0 @@
@import './UserPreferencesTabs.styl'
.presetIndex
padding: 0px 10px 0px 10px

View File

@ -1,14 +0,0 @@
export default {
currentLanguage: 'en',
languages: [
{
value: 'en',
label: 'English',
},
{
value: 'es',
label: 'Spanish',
},
],
onChange: language => {},
};

View File

@ -1,75 +0,0 @@
export default {
defaultTool: { label: 'Default Tool', keys: ['ESC'], column: 0 },
zoom: { label: 'Zoom', keys: ['Z'], column: 0 },
wwwc: { label: 'W/L', keys: ['W'], column: 0 },
pan: { label: 'Pan', keys: ['P'], column: 0 },
angle: { label: 'Angle measurement', keys: ['A'], column: 0 },
stackScroll: { label: 'Scroll stack', keys: ['S'], column: 0 },
magnify: { label: 'Magnify', keys: ['M'], column: 0 },
length: { label: 'Length measurement', keys: [''], column: 0 },
annotate: { label: 'Annotate', keys: [''], column: 0 },
dragProbe: { label: 'Pixel probe', keys: [''], column: 0 },
ellipticalRoi: { label: 'Elliptical ROI', keys: [''], column: 0 },
rectangleRoi: { label: 'Rectangle ROI', keys: [''], column: 0 },
// Viewport hotkeys
flipH: { label: 'Flip Horizontally', keys: ['H'], column: 0 },
flipV: { label: 'Flip Vertically', keys: ['V'], column: 0 },
rotateR: { label: 'Rotate Right', keys: ['R'], column: 0 },
rotateL: { label: 'Rotate Left', keys: ['L'], column: 0 },
invert: { label: 'Invert', keys: ['I'], column: 0 },
zoomIn: { label: 'Zoom In', keys: [''], column: 0 },
zoomOut: { label: 'Zoom Out', keys: [''], column: 0 },
zoomToFit: { label: 'Zoom to Fit', keys: [''], column: 0 },
resetViewport: { label: 'Reset', keys: [''], column: 0 },
clearTools: { label: 'Clear Tools', keys: [''], column: 0 },
// 2nd column
// Viewport navigation hotkeys
scrollDown: { label: 'Scroll Down', keys: ['DOWN'], column: 1 },
scrollUp: { label: 'Scroll Up', keys: ['UP'], column: 1 },
scrollLastImage: { label: 'Scroll to Last Image', keys: ['END'], column: 1 },
scrollFirstImage: {
label: 'Scroll to First Image',
keys: ['HOME'],
column: 1,
},
previousDisplaySet: {
label: 'Previous Series',
keys: ['PAGEUP'],
column: 1,
},
nextDisplaySet: { label: 'Next Series', keys: ['PAGEDOWN'], column: 1 },
nextPanel: { label: 'Next Image Viewport', keys: ['RIGHT'], column: 1 },
previousPanel: {
label: 'Previous Image Viewport',
keys: ['LEFT'],
column: 1,
},
// Miscellaneous hotkeys
toggleOverlayTags: {
label: 'Toggle Image Info Overlay',
keys: ['O'],
column: 1,
},
toggleCinePlay: { label: 'Play/Pause Cine', keys: ['SPACE'], column: 1 },
toggleCineDialog: {
label: 'Show/Hide Cine Controls',
keys: [''],
column: 1,
},
// Preset hotkeys
WLPreset0: { label: 'W/L Preset 0 (Soft Tissue)', keys: ['1'], column: 1 },
WLPreset1: { label: 'W/L Preset 1 (Lung)', keys: ['2'], column: 1 },
WLPreset2: { label: 'W/L Preset 2 (Liver)', keys: ['3'], column: 1 },
WLPreset3: { label: 'W/L Preset 3 (Bone)', keys: ['4'], column: 1 },
WLPreset4: { label: 'W/L Preset 4 (Brain)', keys: ['5'], column: 1 },
WLPreset5: { label: 'W/L Preset 5', keys: ['6'], column: 1 },
WLPreset6: { label: 'W/L Preset 6', keys: ['7'], column: 1 },
WLPreset7: { label: 'W/L Preset 7', keys: ['8'], column: 1 },
WLPreset8: { label: 'W/L Preset 8', keys: ['9'], column: 1 },
WLPreset9: { label: 'W/L Preset 0', keys: ['0'], column: 1 },
};

View File

@ -1,54 +0,0 @@
---
name: User Preferences Form
menu: Components
route: /components/user-preferences-form
---
import { Playground, Props } from 'docz'
import { State } from 'react-powerplug'
import { UserPreferencesForm } from './../index.js'
import NameSpace from '../../../__docs__/NameSpace'
//
import windowLevelDefaults from './windowLevelDefaults.js'
import hotkeyDefaults from './hotkeyDefaults.js'
# User Preferences Form
## Basic usage
<Playground>
<State initial={{
isOpen: false,
windowLevelData: windowLevelDefaults,
hotkeyDefinitions: hotkeyDefaults,
}}>
{({ state, setState }) => (
<React.Fragment>
<button
className="btn btn-primary"
type="button"
onClick={ () => setState({ isOpen: true }) }
>
Open user preferences
</button>
<UserPreferencesForm
{...state}
onCancel={() => setState({ isOpen: false })}
onSave={() => alert('on save')}
onResetToDefaults={() => alert('on reset')}
/>
</React.Fragment>
)}
</State>
</Playground>
## API
<Props of={UserPreferencesForm} />
## Translation Namespace
<NameSpace name="UserPreferencesForm" />

View File

@ -1,13 +0,0 @@
export default {
0: { description: 'Soft tissue', window: 400, level: 40 },
1: { description: 'Lung', window: 1500, level: -600 },
2: { description: 'Liver', window: 150, level: 90 },
3: { description: 'Bone', window: 2500, level: 480 },
4: { description: 'Brain', window: 80, level: 40 },
5: { description: '', window: '', level: '' },
6: { description: '', window: '', level: '' },
7: { description: '', window: '', level: '' },
8: { description: '', window: '', level: '' },
9: { description: '', window: '', level: '' },
10: { description: '', window: '', level: '' },
};

View File

@ -1,4 +0,0 @@
export { UserPreferencesTabs } from './UserPreferencesTabs.js';
export { AboutContent } from '../content/aboutContent/AboutContent.js';
export { UserPreferencesForm } from './UserPreferencesForm.js';
export { GeneralPreferences } from './GeneralPreferences.js';

View File

@ -19,13 +19,15 @@ import {
TableList,
TableListItem,
Thumbnail,
TabComponents,
TabFooter,
HotkeyField,
LanguageSwitcher,
TableSearchFilter,
TablePagination,
ToolbarSection,
Tooltip,
AboutContent,
UserPreferences,
UserPreferencesForm,
OHIFModal,
} from './components';
import { useDebounce, useMedia } from './hooks';
@ -98,6 +100,10 @@ export {
TableList,
TableListItem,
Thumbnail,
TabComponents,
TabFooter,
HotkeyField,
LanguageSwitcher,
TableSearchFilter,
TablePagination,
Toolbar,
@ -105,8 +111,6 @@ export {
ToolbarSection,
Tooltip,
AboutContent,
UserPreferences,
UserPreferencesForm,
ViewerbaseDragDropContext,
SnackbarProvider,
useSnackbarContext,

View File

@ -30,9 +30,8 @@ describe('OHIF User Preferences', () => {
});
it('checks translation by selecting Spanish language', function() {
cy.get('@userPreferencesGeneralTab')
.click()
.should('have.class', 'active');
cy.changePreferencesTab('@userPreferencesGeneralTab');
cy.get('@userPreferencesGeneralTab').should('have.class', 'active');
// Language dropdown should be displayed
cy.get('#language-select').should('be.visible');
@ -65,14 +64,14 @@ describe('OHIF User Preferences', () => {
cy.get('[data-cy="options-menu"]').click();
});
it('checks if user can cancel the language selection and application will be in English', function() {
it('checks if user can cancel the language selection and application will be in "English (USA)"', function() {
// Set language to English and save
cy.setLanguage('English');
cy.setLanguage('English (USA)');
// Set language to Spanish and cancel
cy.setLanguage('Spanish', false);
// Header should be kept in English
// Header should be kept in "English (USA)"
cy.get('.research-use')
.scrollIntoView()
.should('have.text', 'INVESTIGATIONAL USE ONLY');
@ -93,7 +92,7 @@ describe('OHIF User Preferences', () => {
cy.get('[data-cy="options-menu"]').click();
});
it('checks if user can restore to default the language selection and application will be in English', function() {
it('checks if user can restore to default the language selection and application will be in "English (USA)"', function() {
// Set language to Spanish
cy.setLanguage('Spanish');
@ -101,7 +100,7 @@ describe('OHIF User Preferences', () => {
cy.openPreferences();
// Go to general tab
cy.get('@userPreferencesGeneralTab').click();
cy.changePreferencesTab('@userPreferencesGeneralTab');
cy.get('@restoreBtn')
.scrollIntoView()
@ -112,12 +111,12 @@ describe('OHIF User Preferences', () => {
.scrollIntoView()
.click();
// Header should be in English
// Header should be in "English (USA)"
cy.get('.research-use')
.scrollIntoView()
.should('have.text', 'INVESTIGATIONAL USE ONLY');
// Options menu should be in English
// Options menu should be in "English (USA)"
cy.get('[data-cy="options-menu"]')
.should('have.text', 'Options')
.click();
@ -135,9 +134,8 @@ describe('OHIF User Preferences', () => {
it('checks if Preferences set in Study List Page will be consistent on Viewer Page', function() {
// Go go hotkeys tab
cy.get('@userPreferencesHotkeysTab')
.click()
.should('have.class', 'active');
cy.changePreferencesTab('@userPreferencesHotkeysTab');
cy.get('@userPreferencesHotkeysTab').should('have.class', 'active');
// Set new hotkey for 'Rotate Right' function
cy.setNewHotkeyShortcutOnUserPreferencesModal('Rotate Right', '{shift}Q');
@ -150,7 +148,7 @@ describe('OHIF User Preferences', () => {
cy.openPreferences();
// Go to General tab
cy.get('@userPreferencesGeneralTab').click();
cy.changePreferencesTab('@userPreferencesGeneralTab');
// Set language to Spanish
cy.setLanguage('Spanish');
@ -221,9 +219,8 @@ describe('OHIF User Preferences', () => {
});
it('checks translation by selecting Spanish language', function() {
cy.get('@userPreferencesGeneralTab')
.click()
.should('have.class', 'active');
cy.changePreferencesTab('@userPreferencesGeneralTab');
cy.get('@userPreferencesGeneralTab').should('have.class', 'active');
// Language dropdown should be displayed
cy.get('#language-select').should('be.visible');
@ -256,14 +253,14 @@ describe('OHIF User Preferences', () => {
cy.get('[data-cy="options-menu"]').click();
});
it('checks if user can cancel the language selection and application will be in English', function() {
it('checks if user can cancel the language selection and application will be in "English (USA)"', function() {
// Set language to English and save
cy.setLanguage('English');
cy.setLanguage('English (USA)');
// Set language to Spanish and cancel
cy.setLanguage('Spanish', false);
// Header should be kept in English
// Header should be kept in "English (USA)"
cy.get('.research-use')
.scrollIntoView()
.should('have.text', 'INVESTIGATIONAL USE ONLY');
@ -283,10 +280,9 @@ describe('OHIF User Preferences', () => {
cy.get('[data-cy="options-menu"]').click();
});
it('checks if user can restore to default the language selection and application will be in English', function() {
cy.get('@userPreferencesGeneralTab')
.click()
.should('have.class', 'active');
it('checks if user can restore to default the language selection and application will be in "English (USA)', function() {
cy.changePreferencesTab('@userPreferencesGeneralTab');
cy.get('@userPreferencesGeneralTab').should('have.class', 'active');
// Language dropdown should be displayed
cy.get('#language-select').should('be.visible');
@ -298,7 +294,7 @@ describe('OHIF User Preferences', () => {
cy.openPreferences();
// Go to general tab
cy.get('@userPreferencesGeneralTab').click();
cy.changePreferencesTab('@userPreferencesGeneralTab');
cy.get('@restoreBtn')
.scrollIntoView()
@ -308,12 +304,12 @@ describe('OHIF User Preferences', () => {
.scrollIntoView()
.click();
// Header should be in English
// Header should be in "English (USA)""
cy.get('.research-use')
.scrollIntoView()
.should('have.text', 'INVESTIGATIONAL USE ONLY');
// Options menu should be in English
// Options menu should be in "English (USA)"
cy.get('[data-cy="options-menu"]')
.should('have.text', 'Options')
.click();
@ -330,9 +326,8 @@ describe('OHIF User Preferences', () => {
it('checks new hotkeys for "Rotate Right" and "Rotate Left"', function() {
// Go go hotkeys tab
cy.get('@userPreferencesHotkeysTab')
.click()
.should('have.class', 'active');
cy.changePreferencesTab('@userPreferencesHotkeysTab');
cy.get('@userPreferencesHotkeysTab').should('have.class', 'active');
// Set new hotkey for 'Rotate Right' function
cy.setNewHotkeyShortcutOnUserPreferencesModal(
@ -360,24 +355,17 @@ describe('OHIF User Preferences', () => {
});
it('checks new hotkeys for "Next" and "Previous" Image on Viewport', function() {
// Go go hotkeys tab
cy.get('@userPreferencesHotkeysTab')
.click()
.should('have.class', 'active');
// Set new hotkey for 'Next Image Viewport' function
// Update hotkeys for 'Next/Previous Viewport'
cy.changePreferencesTab('@userPreferencesHotkeysTab');
cy.get('@userPreferencesHotkeysTab').should('have.class', 'active');
cy.setNewHotkeyShortcutOnUserPreferencesModal(
'Next Image Viewport',
'Next Viewport',
'{shift}{rightarrow}'
);
// Set new hotkey for 'Previous Image Viewport' function
cy.setNewHotkeyShortcutOnUserPreferencesModal(
'Previous Image Viewport',
'Previous Viewport',
'{shift}{leftarrow}'
);
// Save new hotkeys
cy.get('@saveBtn')
.scrollIntoView()
.click();
@ -386,20 +374,25 @@ describe('OHIF User Preferences', () => {
cy.setLayout(3, 1);
cy.waitViewportImageLoading();
// Rotate Right and Invert colors on Viewport #1
cy.get('body').type('RI');
// Check that image was rotated
// Reset, Rotate Right and Invert colors on Viewport #1
cy.get('body').type(' ');
cy.get('body').type('r');
cy.get('body').type('i');
// Shift active viewport to next
// Reset, Rotate Left and Invert colors on Viewport #2
cy.get('body').type('{shift}{rightarrow}');
cy.get('body').type(' ');
cy.get('body').type('l');
cy.get('body').type('i');
// Verify 1st viewport was rotated
cy.get('@viewportInfoMidTop').should('contains.text', 'R');
//Move to Next Viewport
cy.get('body').type('{shift}{rightarrow}');
// Rotate Left and Invert colors on Viewport #2
cy.get('body').type('LI');
// Get overlay information from viewport #2
// Verify 2nd viewport was rotated
cy.get(
':nth-child(2) > .viewport-wrapper > .viewport-element > .ViewportOrientationMarkers.noselect > .top-mid.orientation-marker'
).as('viewport2InfoMidTop');
// Check that image was rotated
cy.get('@viewport2InfoMidTop').should('contains.text', 'P');
//Move to Previous Viewport
@ -417,24 +410,18 @@ describe('OHIF User Preferences', () => {
it('checks error message when duplicated hotkeys are inserted', function() {
// Go go hotkeys tab
cy.get('@userPreferencesHotkeysTab').click();
cy.changePreferencesTab('@userPreferencesHotkeysTab');
// Set duplicated hotkey for 'Rotate Right' function
cy.setNewHotkeyShortcutOnUserPreferencesModal(
'Rotate Right',
'{rightarrow}'
);
cy.setNewHotkeyShortcutOnUserPreferencesModal('Rotate Right', '{i}');
// Check error message
cy.get('.HotKeysPreferences').within(() => {
cy.get('.HotkeysPreferences').within(() => {
cy.contains('Rotate Right') // label we're looking for
.parent()
.find('.errorMessage')
.as('errorMsg')
.should(
'have.text',
'"Next Image Viewport" is already using the "right" shortcut.'
);
.should('have.text', '"Invert" is already using the "i" shortcut.');
});
//Cancel hotkeys
cy.get('@cancelBtn')
@ -444,13 +431,13 @@ describe('OHIF User Preferences', () => {
it('checks error message when invalid hotkey is inserted', function() {
// Go go hotkeys tab
cy.get('@userPreferencesHotkeysTab').click();
cy.changePreferencesTab('@userPreferencesHotkeysTab');
// Set invalid hotkey for 'Rotate Right' function
cy.setNewHotkeyShortcutOnUserPreferencesModal('Rotate Right', '{ctrl}Z');
// Check error message
cy.get('.HotKeysPreferences').within(() => {
cy.get('.HotkeysPreferences').within(() => {
cy.contains('Rotate Right') // label we're looking for
.parent()
.find('.errorMessage')
@ -466,12 +453,12 @@ describe('OHIF User Preferences', () => {
it('checks error message when only modifier keys are inserted', function() {
// Go go hotkeys tab
cy.get('@userPreferencesHotkeysTab').click();
cy.changePreferencesTab('@userPreferencesHotkeysTab');
// Set invalid modifier key: ctrl
cy.setNewHotkeyShortcutOnUserPreferencesModal('Zoom Out', '{ctrl}');
// Check error message
cy.get('.HotKeysPreferences').within(() => {
cy.get('.HotkeysPreferences').within(() => {
cy.contains('Zoom Out') // label we're looking for
.parent()
.find('.errorMessage')
@ -506,7 +493,7 @@ describe('OHIF User Preferences', () => {
it('checks if user can cancel changes made on User Preferences Hotkeys tab', function() {
// Go go hotkeys tab
cy.get('@userPreferencesHotkeysTab').click();
cy.changePreferencesTab('@userPreferencesHotkeysTab');
// Set new hotkey for 'Rotate Right' function
cy.setNewHotkeyShortcutOnUserPreferencesModal(
@ -523,7 +510,7 @@ describe('OHIF User Preferences', () => {
cy.openPreferences();
//Check that hotkey for 'Rotate Right' function was not changed
cy.get('.HotKeysPreferences').within(() => {
cy.get('.HotkeysPreferences').within(() => {
cy.contains('Rotate Right') // label we're looking for
.parent()
.find('input')
@ -534,7 +521,7 @@ describe('OHIF User Preferences', () => {
it('checks if user can reset to default values on User Preferences Hotkeys tab', function() {
// Go go hotkeys tab
cy.get('@userPreferencesHotkeysTab').click();
cy.changePreferencesTab('@userPreferencesHotkeysTab');
// Set new hotkey for 'Rotate Right' function
cy.setNewHotkeyShortcutOnUserPreferencesModal(
@ -559,7 +546,7 @@ describe('OHIF User Preferences', () => {
cy.openPreferences();
//Check that hotkey for 'Rotate Right' function was not changed
cy.get('.HotKeysPreferences').within(() => {
cy.get('.HotkeysPreferences').within(() => {
cy.contains('Rotate Right') // label we're looking for
.parent()
.find('input')

View File

@ -21,9 +21,8 @@ describe('Visual Regression - OHIF User Preferences', () => {
});
it('checks translation by selecting Spanish language', function() {
cy.get('@userPreferencesGeneralTab')
.click()
.should('have.class', 'active');
cy.changePreferencesTab('@userPreferencesGeneralTab');
cy.get('@userPreferencesGeneralTab').should('have.class', 'active');
// Language dropdown should be displayed
cy.get('#language-select').should('be.visible');
@ -76,9 +75,8 @@ describe('Visual Regression - OHIF User Preferences', () => {
});
it('checks translation by selecting Spanish language', function() {
cy.get('@userPreferencesGeneralTab')
.click()
.should('have.class', 'active');
cy.changePreferencesTab('@userPreferencesGeneralTab');
cy.get('@userPreferencesGeneralTab').should('have.class', 'active');
// Visual comparison
cy.percyCanvasSnapshot(
@ -102,9 +100,8 @@ describe('Visual Regression - OHIF User Preferences', () => {
});
it('checks if user can restore to default the language selection and application will be in English', function() {
cy.get('@userPreferencesGeneralTab')
.click()
.should('have.class', 'active');
cy.changePreferencesTab('@userPreferencesGeneralTab');
cy.get('@userPreferencesGeneralTab').should('have.class', 'active');
// Set language to Spanish
cy.setLanguage('Spanish');
@ -113,7 +110,7 @@ describe('Visual Regression - OHIF User Preferences', () => {
cy.openPreferences();
// Go to general tab
cy.get('@userPreferencesGeneralTab').click();
cy.changePreferencesTab('@userPreferencesGeneralTab');
cy.get('@restoreBtn')
.scrollIntoView()
@ -139,9 +136,8 @@ describe('Visual Regression - OHIF User Preferences', () => {
it('checks new hotkeys for "Next" and "Previous" Image on Viewport', function() {
// Go go hotkeys tab
cy.get('@userPreferencesHotkeysTab')
.click()
.should('have.class', 'active');
cy.changePreferencesTab('@userPreferencesHotkeysTab');
cy.get('@userPreferencesHotkeysTab').should('have.class', 'active');
// Set new hotkey for 'Next Image Viewport' function
cy.setNewHotkeyShortcutOnUserPreferencesModal(

View File

@ -84,7 +84,12 @@ export function initPreferencesModalAliases() {
cy.get('.OHIFModal').as('preferencesModal');
cy.get('[data-cy="hotkeys"]').as('userPreferencesHotkeysTab');
cy.get('[data-cy="general"]').as('userPreferencesGeneralTab');
cy.get('[data-cy="reset-default-btn"]').as('restoreBtn');
cy.get('[data-cy="cancel-btn"]').as('cancelBtn');
cy.get('[data-cy="save-btn"]').as('saveBtn');
initPreferencesModalFooterBtnAliases();
}
//Creating aliases for User Preferences modal
export function initPreferencesModalFooterBtnAliases() {
cy.get('.active [data-cy="reset-default-btn"]').as('restoreBtn');
cy.get('.active [data-cy="cancel-btn"]').as('cancelBtn');
cy.get('.active [data-cy="save-btn"]').as('saveBtn');
}

View File

@ -9,6 +9,7 @@ import {
initStudyListAliasesOnDesktop,
initStudyListAliasesOnTablet,
initPreferencesModalAliases,
initPreferencesModalFooterBtnAliases,
} from './aliases.js';
// ***********************************************
@ -169,13 +170,13 @@ Cypress.Commands.add(
);
Cypress.Commands.add('expectMinimumThumbnails', (seriesToWait = 1) => {
cy.get('[data-cy=thumbnail-list]', { timeout: 20000 }).should($itemList => {
cy.get('[data-cy=thumbnail-list]', { timeout: 50000 }).should($itemList => {
expect($itemList.length >= seriesToWait).to.be.true;
});
});
//Command to wait DICOM image to load into the viewport
Cypress.Commands.add('waitDicomImage', (timeout = 20000) => {
Cypress.Commands.add('waitDicomImage', (timeout = 50000) => {
const loaded = cy.isPageLoaded();
if (loaded) {
@ -441,6 +442,12 @@ Cypress.Commands.add('openPreferences', () => {
});
});
Cypress.Commands.add('changePreferencesTab', tabAlias => {
cy.initPreferencesModalAliases();
cy.get(tabAlias).click();
initPreferencesModalFooterBtnAliases();
});
Cypress.Commands.add('resetUserHoktkeyPreferences', () => {
// Open User Preferences modal
cy.openPreferences();
@ -457,7 +464,7 @@ Cypress.Commands.add(
(function_label, shortcut) => {
// Within scopes all `.get` and `.contains` to within the matched elements
// dom instead of checking from document
cy.get('.HotKeysPreferences')
cy.get('.HotkeysPreferences')
.within(() => {
cy.contains(function_label) // label we're looking for
.parent()
@ -487,16 +494,16 @@ Cypress.Commands.add('setLanguage', (language, save = true) => {
.click()
.should('have.class', 'active');
initPreferencesModalFooterBtnAliases();
// Language dropdown should be displayed
cy.get('#language-select').should('be.visible');
// Select Language and Save/Cancel
cy.get('#language-select')
.select(language)
.then(() => {
const toClick = save ? '@saveBtn' : '@cancelBtn';
cy.get(toClick)
.scrollIntoView()
.click();
});
cy.get('#language-select').select(language);
const toClick = save ? '@saveBtn' : '@cancelBtn';
cy.get(toClick)
.scrollIntoView()
.click();
});

View File

@ -25,12 +25,12 @@ window.config = {
// ~ Global
{
commandName: 'incrementActiveViewport',
label: 'Next Image Viewport',
label: 'Next Viewport',
keys: ['right'],
},
{
commandName: 'decrementActiveViewport',
label: 'Previous Image Viewport',
label: 'Previous Viewport',
keys: ['left'],
},
// Supported Keys: https://craig.is/killing/mice

View File

@ -20,12 +20,12 @@ window.config = {
hotkeys: [
{
commandName: 'incrementActiveViewport',
label: 'Next Image Viewport',
label: 'Next Viewport',
keys: ['right'],
},
{
commandName: 'decrementActiveViewport',
label: 'Previous Image Viewport',
label: 'Previous Viewport',
keys: ['left'],
},
{ commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'] },

View File

@ -20,12 +20,12 @@ window.config = {
// ~ Global
{
commandName: 'incrementActiveViewport',
label: 'Next Image Viewport',
label: 'Next Viewport',
keys: ['right'],
},
{
commandName: 'decrementActiveViewport',
label: 'Previous Image Viewport',
label: 'Previous Viewport',
keys: ['left'],
},
// Supported Keys: https://craig.is/killing/mice

View File

@ -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) {

View File

@ -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'),
}),
},

View File

@ -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 };

View File

@ -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;

View File

@ -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 };

View File

@ -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)

View File

@ -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 };

View File

@ -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 };

View File

@ -2,7 +2,9 @@ const range = (start, end) => {
return new Array(end - start).fill().map((d, i) => i + start);
};
export const disallowedCombinations = {
export const MODIFIER_KEYS = ['ctrl', 'alt', 'shift'];
export const DISALLOWED_COMBINATIONS = {
'': [],
alt: ['space'],
shift: [],
@ -34,18 +36,7 @@ export const disallowedCombinations = {
'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
...[12, 106, 107, 109, 110, 111], // Numpad keys
...range(218, 220), // [\]
...range(185, 190), // ;=,-./
...range(111, 131), // F1-F19
...range(32, 41), // arrow keys, home/end, pg dn/up
...range(47, 58), // 0-9
...range(64, 91), // A-Z
];
export const specialKeys = {
export const SPECIAL_KEYS = {
8: 'backspace',
9: 'tab',
13: 'return',

View File

@ -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 };

View File

@ -0,0 +1 @@
export { UserPreferences } from './UserPreferences';

View File

@ -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;