diff --git a/docs/latest/extensions/index.md b/docs/latest/extensions/index.md
index 6f33e7e06..de0659b36 100644
--- a/docs/latest/extensions/index.md
+++ b/docs/latest/extensions/index.md
@@ -172,6 +172,7 @@ project.
const extensionManager = new ExtensionManager({
commandsManager,
servicesManager,
+ hotkeysManager
});
// prettier-ignore
diff --git a/extensions/default/src/ViewerLayout/Header.jsx b/extensions/default/src/ViewerLayout/Header.jsx
deleted file mode 100644
index 6000e5c22..000000000
--- a/extensions/default/src/ViewerLayout/Header.jsx
+++ /dev/null
@@ -1,102 +0,0 @@
-import React, { useCallback } from 'react';
-import PropTypes from 'prop-types';
-import { useTranslation } from 'react-i18next';
-// TODO: This may fail if package is split from PWA build
-import { useHistory } from 'react-router-dom';
-//
-import { NavBar, Svg, Icon, IconButton, Dropdown, useModal } from '@ohif/ui';
-
-function Header({ children }) {
- const { t } = useTranslation();
- const history = useHistory();
- const { show } = useModal();
-
- // TODO: IT SHOULD BE REFACTORED WHEN THE MODAL CONTENT IS DEFINED
- const showAboutModal = useCallback(() => {
- const modalComponent = () => (
-
{t('AboutModal:OHIF Viewer - About')}
- );
- show({
- title: t('AboutModal:OHIF Viewer - About'),
- content: modalComponent,
- });
- }, [show, t]);
-
- // TODO: IT SHOULD BE REFACTORED WHEN THE MODAL CONTENT IS DEFINED
- const showPreferencesModal = useCallback(() => {
- const modalComponent = () => (
- {t('UserPreferencesModal:User Preferences')}
- );
- show({
- title: t('UserPreferencesModal:User Preferences'),
- content: modalComponent,
- });
- }, [show, t]);
-
- return (
-
-
-
- {/* // TODO: Should preserve filter/sort
- // Either injected service? Or context (like react router's `useLocation`?) */}
-
history.push('/')}
- >
-
-
-
-
-
-
-
{children}
-
-
- {t('Header:INVESTIGATIONAL USE ONLY')}
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-Header.propTypes = {
- children: PropTypes.any.isRequired,
-};
-
-export default Header;
diff --git a/extensions/default/src/ViewerLayout/index.jsx b/extensions/default/src/ViewerLayout/index.jsx
index ef6cabfcf..db5e38b1f 100644
--- a/extensions/default/src/ViewerLayout/index.jsx
+++ b/extensions/default/src/ViewerLayout/index.jsx
@@ -1,12 +1,13 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
-import { SidePanel, ErrorBoundary } from '@ohif/ui';
-import Header from './Header.jsx';
+import { useTranslation } from 'react-i18next';
+import { SidePanel, ErrorBoundary, UserPreferences, AboutModal, Header, useModal } from '@ohif/ui';
+
import NestedMenu from './ToolbarButtonNestedMenu.jsx';
// TODO: Having ToolbarPrimary and ToolbarSecondary is ugly, but
// these are going to be unified shortly so this is good enough for now.
-function ToolbarPrimary({servicesManager}) {
+function ToolbarPrimary({ servicesManager }) {
const { ToolBarService } = servicesManager.services;
const defaultTool = {
icon: 'tool-more-menu',
@@ -82,7 +83,7 @@ function ToolbarPrimary({servicesManager}) {
>
}
-function ToolbarSecondary({servicesManager}) {
+function ToolbarSecondary({ servicesManager }) {
const { ToolBarService } = servicesManager.services;
const defaultTool = {
icon: 'tool-more-menu',
@@ -137,19 +138,46 @@ function ToolbarSecondary({servicesManager}) {
>
}
-
function ViewerLayout({
// From Extension Module Params
extensionManager,
servicesManager,
- commandsManager,
+ hotkeysManager,
// From Modes
leftPanels,
rightPanels,
viewports,
- children,
ViewportGridComp,
}) {
+ const { t } = useTranslation();
+ const { show, hide } = useModal();
+
+ const { hotkeyDefinitions, hotkeyDefaults } = hotkeysManager;
+ const menuOptions = [
+ {
+ title: t('Header:About'),
+ icon: 'info',
+ onClick: () => show({ content: AboutModal, title: 'About OHIF Viewer' })
+ },
+ {
+ title: t('Header:Preferences'),
+ icon: 'settings',
+ onClick: () => show({
+ title: t('UserPreferencesModal:User Preferences'),
+ content: UserPreferences,
+ contentProps: {
+ hotkeyDefaults: hotkeysManager.getValidHotkeyDefinitions(hotkeyDefaults),
+ hotkeyDefinitions,
+ onCancel: hide,
+ onSubmit: ({ hotkeyDefinitions }) => {
+ hotkeysManager.setHotkeys(hotkeyDefinitions);
+ hide();
+ },
+ onReset: () => hotkeysManager.restoreDefaultBindings()
+ }
+ })
+ },
+ ];
/**
* Set body classes (tailwindcss) that don't allow vertical
@@ -194,10 +222,10 @@ function ViewerLayout({
return (
-
+
@@ -220,7 +248,7 @@ function ViewerLayout({
diff --git a/extensions/default/src/getLayoutTemplateModule.js b/extensions/default/src/getLayoutTemplateModule.js
index e70d961c1..793f002fa 100644
--- a/extensions/default/src/getLayoutTemplateModule.js
+++ b/extensions/default/src/getLayoutTemplateModule.js
@@ -5,16 +5,18 @@ import ViewerLayout from './ViewerLayout';
- Init layout based on the displaySets and the objects.
*/
-export default function({
+export default function ({
servicesManager,
extensionManager,
commandsManager,
+ hotkeysManager
}) {
function ViewerLayoutWithServices(props) {
return ViewerLayout({
servicesManager,
extensionManager,
commandsManager,
+ hotkeysManager,
...props,
});
}
diff --git a/modes/longitudinal/src/index.js b/modes/longitudinal/src/index.js
index ca7dad86a..97648fd6b 100644
--- a/modes/longitudinal/src/index.js
+++ b/modes/longitudinal/src/index.js
@@ -5,6 +5,7 @@ const ohif = {
layout: 'org.ohif.default.layoutTemplateModule.viewerLayout',
sopClassHandler: 'org.ohif.default.sopClassHandlerModule.stack',
};
+
const tracked = {
measurements: 'org.ohif.measurement-tracking.panelModule.trackedMeasurements',
thumbnailList: 'org.ohif.measurement-tracking.panelModule.seriesList',
diff --git a/platform/core/src/classes/HotkeysManager.js b/platform/core/src/classes/HotkeysManager.js
index 0848e0437..1ede273b0 100644
--- a/platform/core/src/classes/HotkeysManager.js
+++ b/platform/core/src/classes/HotkeysManager.js
@@ -61,8 +61,7 @@ export class HotkeysManager {
*/
setHotkeys(hotkeyDefinitions = []) {
try {
- const definitions = this._getValidDefinitions(hotkeyDefinitions);
-
+ const definitions = this.getValidDefinitions(hotkeyDefinitions);
definitions.forEach(definition => this.registerHotkeys(definition));
} catch (error) {
const { UINotificationService } = this._servicesManager.services;
@@ -81,8 +80,7 @@ export class HotkeysManager {
* @param {HotkeyDefinition[] | Object} [hotkeyDefinitions=[]] Contains hotkeys definitions
*/
setDefaultHotKeys(hotkeyDefinitions = []) {
- const definitions = this._getValidDefinitions(hotkeyDefinitions);
-
+ const definitions = this.getValidDefinitions(hotkeyDefinitions);
this.hotkeyDefaults = definitions;
}
@@ -92,7 +90,7 @@ export class HotkeysManager {
*
* @param {HotkeyDefinition[] | Object} [hotkeyDefinitions=[]] Contains hotkeys definitions
*/
- _getValidDefinitions(hotkeyDefinitions) {
+ getValidDefinitions(hotkeyDefinitions) {
const definitions = Array.isArray(hotkeyDefinitions)
? [...hotkeyDefinitions]
: this._parseToArrayLike(hotkeyDefinitions);
@@ -100,6 +98,24 @@ export class HotkeysManager {
return definitions;
}
+ /**
+ * Take hotkey definitions that can be an array and make sure that it
+ * returns an object of hotkeys definitions
+ *
+ * @param {HotkeyDefinition[]} [hotkeyDefinitions=[]] Contains hotkeys definitions
+ * @returns {Object}
+ */
+ getValidHotkeyDefinitions(hotkeyDefinitions) {
+ const definitions = this.getValidDefinitions(hotkeyDefinitions);
+ const objectDefinitions = {};
+ definitions.forEach(definition => {
+ const { commandName, commandOptions } = definition;
+ const commandHash = objectHash({ commandName, commandOptions });
+ objectDefinitions[commandHash] = definition;
+ });
+ return objectDefinitions;
+ }
+
/**
* 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
@@ -147,7 +163,7 @@ export class HotkeysManager {
* @param {String} extension
* @returns {undefined}
*/
- registerHotkeys({ commandName, commandOptions = {}, keys, label } = {}, extension) {
+ registerHotkeys({ commandName, commandOptions = {}, keys, label, isEditable } = {}, extension) {
if (!commandName) {
log.warn(`[hotkeys] No command was defined for hotkey "${keys}"`);
return;
@@ -164,7 +180,7 @@ export class HotkeysManager {
}
// Set definition & bind
- this.hotkeyDefinitions[commandHash] = { keys, label };
+ this.hotkeyDefinitions[commandHash] = { commandName, commandOptions, keys, label, isEditable };
this._bindHotkeys(commandName, commandOptions, keys);
log.info(`[hotkeys] Binding ${commandName} with ${options} options to ${keys}`);
}
diff --git a/platform/core/src/defaults/hotkeyBindings.js b/platform/core/src/defaults/hotkeyBindings.js
index fd0ede4bf..df7346d58 100644
--- a/platform/core/src/defaults/hotkeyBindings.js
+++ b/platform/core/src/defaults/hotkeyBindings.js
@@ -4,53 +4,64 @@ import windowLevelPresets from './windowLevelPresets';
* Supported Keys: https://craig.is/killing/mice
*/
export default [
- /** Global */
- {
- commandName: 'incrementActiveViewport',
- label: 'Next Viewport',
- keys: ['right'],
- },
- {
- commandName: 'decrementActiveViewport',
- label: 'Previous Viewport',
- keys: ['left'],
- },
- /** Viewport */
- { commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'] },
- { commandName: 'rotateViewportCCW', label: 'Rotate Left', keys: ['l'] },
- { commandName: 'invertViewport', label: 'Invert', keys: ['i'] },
- {
- commandName: 'cancelMeasurement',
- label: 'Cancel Cornerstone Measurement',
- keys: ['esc'],
- },
+ { commandName: 'setToolActive', commandOptions: { toolName: 'Zoom' }, label: 'Zoom', keys: ['z'], isEditable: true },
+ { commandName: 'scaleUpViewport', label: 'Zoom In', keys: ['+'], isEditable: true },
+ { commandName: 'scaleDownViewport', label: 'Zoom Out', keys: ['-'], isEditable: true },
+ { commandName: 'fitViewportToWindow', label: 'Zoom to Fit', keys: ['='], isEditable: true },
+ { commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'], isEditable: true },
+ { commandName: 'rotateViewportCCW', label: 'Rotate Left', keys: ['l'], isEditable: true },
{
commandName: 'flipViewportVertical',
label: 'Flip Horizontally',
keys: ['h'],
+ isEditable: true,
},
{
commandName: 'flipViewportHorizontal',
label: 'Flip Vertically',
keys: ['v'],
+ isEditable: true,
},
- { commandName: 'scaleUpViewport', label: 'Zoom In', keys: ['+'] },
- { commandName: 'scaleDownViewport', label: 'Zoom Out', keys: ['-'] },
- { commandName: 'fitViewportToWindow', label: 'Zoom to Fit', keys: ['='] },
- { commandName: 'resetViewport', label: 'Reset', keys: ['space'] },
- { commandName: 'nextImage', label: 'Next Image', keys: ['down'] },
- { commandName: 'previousImage', label: 'Previous Image', keys: ['up'] },
{
- commandName: 'previousViewportDisplaySet',
- label: 'Previous Series',
- keys: ['pagedown'],
+ commandName: 'invertViewport',
+ label: 'Invert',
+ keys: ['i'],
+ isEditable: true,
+ },
+ {
+ commandName: 'incrementActiveViewport',
+ label: 'Next Image Viewport',
+ keys: ['right'],
+ isEditable: true,
+ },
+ {
+ commandName: 'decrementActiveViewport',
+ label: 'Previous Image Viewport',
+ keys: ['left'],
+ isEditable: true,
},
{
commandName: 'nextViewportDisplaySet',
label: 'Next Series',
keys: ['pageup'],
+ isEditable: true,
+ },
+ {
+ commandName: 'previousViewportDisplaySet',
+ label: 'Previous Series',
+ keys: ['pagedown'],
+ isEditable: true,
+ },
+ { commandName: 'nextImage', label: 'Next Image', keys: ['down'], isEditable: true },
+ { commandName: 'previousImage', label: 'Previous Image', keys: ['up'], isEditable: true },
+ { commandName: 'firstImage', label: 'First Image', keys: ['home'], isEditable: true },
+ { commandName: 'lastImage', label: 'Last Image', keys: ['end'], isEditable: true },
+ { commandName: 'resetViewport', label: 'Reset', keys: ['space'], isEditable: true },
+ {
+ commandName: 'cancelMeasurement',
+ label: 'Cancel Cornerstone Measurement',
+ keys: ['esc'],
},
- /** Window level presets */
{
commandName: 'setWindowLevel',
commandOptions: windowLevelPresets[1],
diff --git a/platform/core/src/defaults/windowLevelPresets.js b/platform/core/src/defaults/windowLevelPresets.js
index ea2ae3b8d..258b3d18a 100644
--- a/platform/core/src/defaults/windowLevelPresets.js
+++ b/platform/core/src/defaults/windowLevelPresets.js
@@ -5,8 +5,8 @@ export default {
4: { description: 'Bone', window: '80', level: '40' },
5: { description: 'Brain', window: '2500', level: '480' },
6: { description: 'Trest', window: '1', level: '1' },
- 7: { description: '', window: '', level: '' },
- 8: { description: '', window: '', level: '' },
- 9: { description: '', window: '', level: '' },
- 10: { description: '', window: '', level: '' },
+ 7: { description: 'Empty1', window: 'Empty1', level: 'Empty1' },
+ 8: { description: 'Empty2', window: 'Empty2', level: 'Empty2' },
+ 9: { description: 'Empty3', window: 'Empty3', level: 'Empty3' },
+ 10: { description: 'Empty4', window: 'Empty4', level: 'Empty4' },
};
diff --git a/platform/core/src/extensions/ExtensionManager.js b/platform/core/src/extensions/ExtensionManager.js
index 07249389b..7ed8a1eba 100644
--- a/platform/core/src/extensions/ExtensionManager.js
+++ b/platform/core/src/extensions/ExtensionManager.js
@@ -2,13 +2,14 @@ import MODULE_TYPES from './MODULE_TYPES.js';
import log from './../log.js';
export default class ExtensionManager {
- constructor({ commandsManager, servicesManager, api, appConfig = {} }) {
+ constructor({ commandsManager, servicesManager, hotkeysManager, api, appConfig = {} }) {
this.modules = {};
this.registeredExtensionIds = [];
this.moduleTypeNames = Object.values(MODULE_TYPES);
//
this._commandsManager = commandsManager;
this._servicesManager = servicesManager;
+ this._hotkeysManager = hotkeysManager;
this._appConfig = appConfig;
this._api = api;
@@ -31,6 +32,7 @@ export default class ExtensionManager {
registeredExtensionIds,
_servicesManager,
_commandsManager,
+ _hotkeysManager,
_extensionLifeCycleHooks,
} = this;
@@ -51,6 +53,7 @@ export default class ExtensionManager {
onModeEnter({
servicesManager: _servicesManager,
commandsManager: _commandsManager,
+ hotkeysManager: _hotkeysManager
});
}
});
@@ -139,6 +142,7 @@ export default class ExtensionManager {
extension.preRegistration({
servicesManager: this._servicesManager,
commandsManager: this._commandsManager,
+ hotkeysManager: this._hotkeysManager,
appConfig: this._appConfig,
configuration,
});
@@ -239,8 +243,9 @@ export default class ExtensionManager {
appConfig: this._appConfig,
getDataSources: this.getDataSources, // Why pass this in if we're passing in `extensionManager`?
commandsManager: this._commandsManager,
- extensionManager: this,
servicesManager: this._servicesManager,
+ hotkeysManager: this._hotkeysManager,
+ extensionManager: this,
configuration,
api: this._api,
});
diff --git a/platform/i18n/src/index.js b/platform/i18n/src/index.js
index 59f2ae76e..482b53b7e 100644
--- a/platform/i18n/src/index.js
+++ b/platform/i18n/src/index.js
@@ -7,6 +7,7 @@ import { initReactI18next } from 'react-i18next';
import customDebug from './debugger';
import pkg from '../package.json';
import { debugMode, detectionOptions } from './config';
+import { getLanguageLabel, getAvailableLanguagesInfo } from './utils.js';
// Note: The index.js files inside src/locales are dynamically generated
// by the pullTranslations.sh script
@@ -127,7 +128,7 @@ function initI18n(
});
}
- return initialized.then(function(t) {
+ return initialized.then(function (t) {
i18n.T = t;
customDebug(`T function available.`, 'info');
});
@@ -138,9 +139,8 @@ customDebug(`version ${pkg.version} loaded.`, 'info');
i18n.initializing = initI18n();
i18n.initI18n = initI18n;
i18n.addLocales = addLocales;
-i18n.defaultLanguage = DEFAULT_LANGUAGE;
-
-import getAvailableLanguagesInfo from './getAvailableLanguagesInfo.js';
i18n.availableLanguages = getAvailableLanguagesInfo(locales);
+i18n.defaultLanguage = { label: getLanguageLabel(DEFAULT_LANGUAGE), value: DEFAULT_LANGUAGE };
+i18n.currentLanguage = () => ({ label: getLanguageLabel(i18n.language), value: i18n.language });
export default i18n;
diff --git a/platform/i18n/src/getAvailableLanguagesInfo.js b/platform/i18n/src/utils.js
similarity index 88%
rename from platform/i18n/src/getAvailableLanguagesInfo.js
rename to platform/i18n/src/utils.js
index 8dd1f257a..b62eedca1 100644
--- a/platform/i18n/src/getAvailableLanguagesInfo.js
+++ b/platform/i18n/src/utils.js
@@ -56,15 +56,21 @@ const languagesMap = {
'zh-TW': 'Chinese (Taiwan)',
};
+const getLanguageLabel = (language) => {
+ return languagesMap[language];
+};
+
export default function getAvailableLanguagesInfo(locales) {
const availableLanguagesInfo = [];
Object.keys(locales).forEach(key => {
availableLanguagesInfo.push({
value: key,
- label: languagesMap[key] || key,
+ label: getLanguageLabel(key) || key,
});
});
return availableLanguagesInfo;
}
+
+export { getAvailableLanguagesInfo, getLanguageLabel };
diff --git a/platform/ui/index.js b/platform/ui/index.js
index 81379ac85..8c1bde12d 100644
--- a/platform/ui/index.js
+++ b/platform/ui/index.js
@@ -30,6 +30,11 @@ export {
/** COMPONENTS */
export {
+ AboutModal,
+ HotkeyField,
+ Header,
+ UserPreferences,
+ HotkeysPreferences,
Button,
ButtonGroup,
ContextMenu,
diff --git a/platform/viewer/src/components/PreferencesDropdown/AboutModal.jsx b/platform/ui/src/components/AboutModal/AboutModal.jsx
similarity index 91%
rename from platform/viewer/src/components/PreferencesDropdown/AboutModal.jsx
rename to platform/ui/src/components/AboutModal/AboutModal.jsx
index af06e2bee..4f515a615 100644
--- a/platform/viewer/src/components/PreferencesDropdown/AboutModal.jsx
+++ b/platform/ui/src/components/AboutModal/AboutModal.jsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { Typography, Icon } from '@ohif/ui';
+import { Typography, Icon } from '../../components';
import detect from 'browser-detect';
const Link = ({ href, children, showIcon = false }) => {
@@ -29,14 +29,14 @@ const Row = ({ title, value, link }) => {
{link ? (
{value}
) : (
-
- {value}
-
- )}
+
+ {value}
+
+ )}
);
};
diff --git a/platform/ui/src/components/AboutModal/AboutModal.mdx b/platform/ui/src/components/AboutModal/AboutModal.mdx
new file mode 100644
index 000000000..917e20e66
--- /dev/null
+++ b/platform/ui/src/components/AboutModal/AboutModal.mdx
@@ -0,0 +1,30 @@
+---
+name: AboutModal
+menu: General
+route: components/aboutModal
+---
+
+import { Playground, Props } from 'docz';
+import { AboutModal } from '@ohif/ui';
+
+# AboutModal
+
+AboutModal are used to show application version information.
+
+## Import
+
+```javascript
+import { AboutModal } from '@ohif/ui';
+```
+
+## Usage
+
+
+
+
+
+## Properties
+
+
diff --git a/platform/ui/src/components/AboutModal/index.js b/platform/ui/src/components/AboutModal/index.js
new file mode 100644
index 000000000..a325d7555
--- /dev/null
+++ b/platform/ui/src/components/AboutModal/index.js
@@ -0,0 +1,2 @@
+import AboutModal from './AboutModal';
+export default AboutModal;
diff --git a/platform/ui/src/components/Button/Button.jsx b/platform/ui/src/components/Button/Button.jsx
index fdd5d9390..0efe7a11d 100644
--- a/platform/ui/src/components/Button/Button.jsx
+++ b/platform/ui/src/components/Button/Button.jsx
@@ -64,6 +64,8 @@ const variantClasses = {
'bg-white text-black hover:opacity-80 active:opacity-100 focus:opacity-80',
black:
'bg-black text-white hover:opacity-80 active:opacity-100 focus:opacity-80',
+ light:
+ 'border bg-primary-light border-primary-light text-black hover:opacity-80 active:opacity-100 focus:opacity-80',
},
};
@@ -154,6 +156,7 @@ Button.propTypes = {
'white',
'black',
'inherit',
+ 'light'
]),
fullWidth: PropTypes.bool,
disabled: PropTypes.bool,
diff --git a/platform/ui/src/components/Header/Header.jsx b/platform/ui/src/components/Header/Header.jsx
new file mode 100644
index 000000000..8b0a30f3b
--- /dev/null
+++ b/platform/ui/src/components/Header/Header.jsx
@@ -0,0 +1,71 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { useTranslation } from 'react-i18next';
+import classNames from 'classnames';
+// TODO: This may fail if package is split from PWA build
+import { useHistory } from 'react-router-dom';
+import { NavBar, Svg, Icon, IconButton, Dropdown } from '@ohif/ui';
+
+function Header({ children, menuOptions, isReturnEnabled }) {
+ const { t } = useTranslation();
+ const history = useHistory();
+
+ const onReturnHandler = () => {
+ if (isReturnEnabled) {
+ history.push('/');
+ }
+ };
+
+ return (
+
+
+
+ {/* // TODO: Should preserve filter/sort
+ // Either injected service? Or context (like react router's `useLocation`?) */}
+
+ {isReturnEnabled &&
}
+
+
+
+
{children}
+
+
+ {t('Header:INVESTIGATIONAL USE ONLY')}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+Header.propTypes = {
+ children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
+ isReturnEnabled: PropTypes.bool
+};
+
+Header.defaultProps = {
+ isReturnEnabled: true
+};
+
+export default Header;
diff --git a/platform/ui/src/components/Header/index.js b/platform/ui/src/components/Header/index.js
new file mode 100644
index 000000000..24421ce68
--- /dev/null
+++ b/platform/ui/src/components/Header/index.js
@@ -0,0 +1,2 @@
+import Header from './Header';
+export default Header;
diff --git a/platform/ui/src/components/HotkeyField/HotkeyField.jsx b/platform/ui/src/components/HotkeyField/HotkeyField.jsx
new file mode 100644
index 000000000..40bdbc362
--- /dev/null
+++ b/platform/ui/src/components/HotkeyField/HotkeyField.jsx
@@ -0,0 +1,63 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { hotkeys } from '@ohif/core';
+import { Input } from '@ohif/ui';
+
+import { getKeys, formatKeysForInput } from './utils';
+
+/**
+ * HotkeyField
+ * Renders a hotkey input that records keys
+ *
+ * @param {object} props component props
+ * @param {Array[]} props.keys keys to be controlled by this field
+ * @param {boolean} props.disabled disables the field
+ * @param {function} props.onChange callback with changed values
+ * @param {string} props.className input classes
+ * @param {Array[]} props.modifierKeys
+ */
+const HotkeyField = ({ disabled, keys, onChange, className, modifierKeys }) => {
+ const inputValue = formatKeysForInput(keys);
+
+ const onInputKeyDown = event => {
+ event.stopPropagation();
+ event.preventDefault();
+
+ hotkeys.record(sequence => {
+ const keys = getKeys({ sequence, modifierKeys });
+ hotkeys.unpause();
+ onChange(keys);
+ });
+ };
+
+ const onFocus = () => {
+ hotkeys.pause();
+ hotkeys.startRecording();
+ };
+
+ return (
+
+ );
+};
+
+HotkeyField.propTypes = {
+ keys: PropTypes.array.isRequired,
+ onChange: PropTypes.func.isRequired,
+ className: PropTypes.string,
+ modifierKeys: PropTypes.array,
+ disabled: PropTypes.bool,
+};
+
+HotkeyField.defaultProps = {
+ disabled: false
+};
+
+export default HotkeyField;
diff --git a/platform/ui/src/components/HotkeyField/index.js b/platform/ui/src/components/HotkeyField/index.js
new file mode 100644
index 000000000..890117cf6
--- /dev/null
+++ b/platform/ui/src/components/HotkeyField/index.js
@@ -0,0 +1,3 @@
+import HotkeyField from './HotkeyField.jsx';
+
+export default HotkeyField;
diff --git a/platform/ui/src/components/HotkeyField/utils.js b/platform/ui/src/components/HotkeyField/utils.js
new file mode 100644
index 000000000..fe1c7d064
--- /dev/null
+++ b/platform/ui/src/components/HotkeyField/utils.js
@@ -0,0 +1,31 @@
+/**
+ * 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, modifierKeys }) => {
+ const keysArray = sequence.join(' ').split('+');
+ let keys = [];
+ let modifiers = [];
+ keysArray.forEach(key => {
+ if (modifierKeys && modifierKeys.includes(key)) {
+ modifiers.push(key);
+ } else {
+ keys.push(key);
+ }
+ });
+ return [...modifiers, ...keys];
+};
+
+export {
+ getKeys,
+ formatKeysForInput
+};
diff --git a/platform/ui/src/components/HotkeysPreferences/HotkeysPreferences.jsx b/platform/ui/src/components/HotkeysPreferences/HotkeysPreferences.jsx
new file mode 100644
index 000000000..ab700ec80
--- /dev/null
+++ b/platform/ui/src/components/HotkeysPreferences/HotkeysPreferences.jsx
@@ -0,0 +1,111 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import { HotkeyField, Typography } from '@ohif/ui';
+
+/* TODO: Move these configs and utils to core? */
+import { MODIFIER_KEYS } from './hotkeysConfig';
+import { validate, splitHotkeyDefinitionsAndCreateTuples } from './utils';
+
+const HotkeysPreferences = ({ disabled, hotkeyDefinitions, errors: controlledErrors, onChange }) => {
+ const visibleHotkeys = Object.keys(hotkeyDefinitions)
+ .filter(key => hotkeyDefinitions[key].isEditable)
+ .reduce((obj, key) => {
+ obj[key] = hotkeyDefinitions[key];
+ return obj;
+ }, {});
+
+ const [errors, setErrors] = useState(controlledErrors);
+ const splitedHotkeys = splitHotkeyDefinitionsAndCreateTuples(visibleHotkeys);
+
+ if (!Object.keys(hotkeyDefinitions).length) {
+ return 'No hotkeys definitions';
+ }
+
+ const onHotkeyChangeHandler = (id, definition) => {
+ const { error } = validate({
+ commandName: id,
+ pressedKeys: definition.keys,
+ hotkeys: hotkeyDefinitions,
+ });
+
+ setErrors(prevState => {
+ const errors = { ...prevState, [id]: error };
+ onChange(id, definition, errors);
+ return errors;
+ });
+ };
+
+ return (
+
+
+ {splitedHotkeys.map((hotkeys, index) => {
+ return (
+
+
+ {hotkeys.map((hotkey, hotkeyIndex) => {
+ const [id, definition] = hotkey;
+ const isFirst = hotkeyIndex === 0;
+ const error = errors[id];
+
+ const onChangeHandler = keys => onHotkeyChangeHandler(id, { ...definition, keys });
+
+ return (
+
+
+
+ Function
+
+
+ {definition.label}
+
+
+
+
+ Shortcut
+
+
+
+ {error && {error}}
+
+
+
+ );
+ })}
+
+
+ );
+ })}
+
+
+ );
+};
+
+const noop = () => { };
+
+HotkeysPreferences.propTypes = {
+ onChange: PropTypes.func,
+ disabled: PropTypes.bool,
+ hotkeyDefinitions: PropTypes.object.isRequired,
+};
+
+HotkeysPreferences.defaultProps = {
+ onChange: noop,
+ disabled: false
+};
+
+export default HotkeysPreferences;
diff --git a/platform/ui/src/components/HotkeysPreferences/hotkeysConfig.js b/platform/ui/src/components/HotkeysPreferences/hotkeysConfig.js
new file mode 100644
index 000000000..23d286b19
--- /dev/null
+++ b/platform/ui/src/components/HotkeysPreferences/hotkeysConfig.js
@@ -0,0 +1,91 @@
+const range = (start, end) => {
+ return new Array(end - start).fill().map((d, i) => i + start);
+};
+
+export const MODIFIER_KEYS = ['ctrl', 'alt', 'shift'];
+
+export const DISALLOWED_COMBINATIONS = {
+ '': [],
+ alt: ['space'],
+ shift: [],
+ ctrl: [
+ 'f4',
+ 'f5',
+ 'f11',
+ 'w',
+ 'r',
+ 't',
+ 'o',
+ 'p',
+ 'a',
+ 'd',
+ 'f',
+ 'g',
+ 'h',
+ 'j',
+ 'l',
+ 'z',
+ 'x',
+ 'c',
+ 'v',
+ 'b',
+ 'n',
+ 'pagedown',
+ 'pageup',
+ ],
+ 'ctrl+shift': ['q', 'w', 'r', 't', 'p', 'a', 'h', 'v', 'b', 'n'],
+};
+
+export const SPECIAL_KEYS = {
+ 8: 'backspace',
+ 9: 'tab',
+ 13: 'return',
+ 16: 'shift',
+ 17: 'ctrl',
+ 18: 'alt',
+ 19: 'pause',
+ 20: 'capslock',
+ 27: 'esc',
+ 32: 'space',
+ 33: 'pageup',
+ 34: 'pagedown',
+ 35: 'end',
+ 36: 'home',
+ 37: 'left',
+ 38: 'up',
+ 39: 'right',
+ 40: 'down',
+ 45: 'insert',
+ 46: 'del',
+ 96: '0',
+ 97: '1',
+ 98: '2',
+ 99: '3',
+ 100: '4',
+ 101: '5',
+ 102: '6',
+ 103: '7',
+ 104: '8',
+ 105: '9',
+ 106: '*',
+ 107: '+',
+ 109: '-',
+ 110: '.',
+ 111: '/',
+ 112: 'f1',
+ 113: 'f2',
+ 114: 'f3',
+ 115: 'f4',
+ 116: 'f5',
+ 117: 'f6',
+ 118: 'f7',
+ 119: 'f8',
+ 120: 'f9',
+ 121: 'f10',
+ 122: 'f11',
+ 123: 'f12',
+ 144: 'numlock',
+ 145: 'scroll',
+ 191: '/',
+ 224: 'meta',
+};
diff --git a/platform/ui/src/components/HotkeysPreferences/hotkeysValidators.js b/platform/ui/src/components/HotkeysPreferences/hotkeysValidators.js
new file mode 100644
index 000000000..fbb2c1004
--- /dev/null
+++ b/platform/ui/src/components/HotkeysPreferences/hotkeysValidators.js
@@ -0,0 +1,87 @@
+import { MODIFIER_KEYS, DISALLOWED_COMBINATIONS } from './hotkeysConfig';
+
+const formatPressedKeys = pressedKeysArray => pressedKeysArray.join('+');
+
+const findConflictingCommand = (hotkeys, currentCommandName, pressedKeys) => {
+ let firstConflictingCommand = undefined;
+ const formatedPressedHotkeys = formatPressedKeys(pressedKeys);
+
+ for (const commandName in hotkeys) {
+ const toolHotkeys = hotkeys[commandName].keys;
+ const formatedToolHotkeys = formatPressedKeys(toolHotkeys);
+
+ if (
+ formatedPressedHotkeys === formatedToolHotkeys &&
+ commandName !== currentCommandName
+ ) {
+ firstConflictingCommand = hotkeys[commandName];
+ break;
+ }
+ }
+
+ return firstConflictingCommand;
+};
+
+const ERROR_MESSAGES = {
+ MODIFIER:
+ "It's not possible to define only modifier keys (ctrl, alt and shift) as a shortcut",
+ EMPTY: "Field can't be empty.",
+};
+
+// VALIDATORS
+
+const modifierValidator = ({ pressedKeys }) => {
+ const lastPressedKey = pressedKeys[pressedKeys.length - 1];
+ // Check if it has a valid modifier
+ const isModifier = MODIFIER_KEYS.includes(lastPressedKey);
+ if (isModifier) {
+ return { error: ERROR_MESSAGES.MODIFIER };
+ }
+};
+
+const emptyValidator = ({ pressedKeys = [] }) => {
+ if (!pressedKeys.length) {
+ return { error: ERROR_MESSAGES.EMPTY };
+ }
+};
+
+const conflictingValidator = ({ commandName, pressedKeys, hotkeys }) => {
+ const conflictingCommand = findConflictingCommand(
+ hotkeys,
+ commandName,
+ pressedKeys
+ );
+
+ if (conflictingCommand) {
+ return {
+ error: `"${conflictingCommand.label}" is already using the "${pressedKeys}" shortcut.`,
+ };
+ }
+};
+
+const disallowedValidator = ({ pressedKeys = [] }) => {
+ const lastPressedKey = pressedKeys[pressedKeys.length - 1];
+ const modifierCommand = formatPressedKeys(
+ pressedKeys.slice(0, pressedKeys.length - 1)
+ );
+
+ const disallowedCombination = DISALLOWED_COMBINATIONS[modifierCommand];
+ const hasDisallowedCombinations = disallowedCombination
+ ? disallowedCombination.includes(lastPressedKey)
+ : false;
+
+ if (hasDisallowedCombinations) {
+ return {
+ error: `"${formatPressedKeys(pressedKeys)}" shortcut combination is not allowed`,
+ };
+ }
+};
+
+const hotkeysValidators = [
+ emptyValidator,
+ modifierValidator,
+ conflictingValidator,
+ disallowedValidator,
+];
+
+export { hotkeysValidators };
diff --git a/platform/ui/src/components/HotkeysPreferences/index.js b/platform/ui/src/components/HotkeysPreferences/index.js
new file mode 100644
index 000000000..d3f9621c7
--- /dev/null
+++ b/platform/ui/src/components/HotkeysPreferences/index.js
@@ -0,0 +1,3 @@
+import HotkeysPreferences from './HotkeysPreferences.jsx';
+
+export default HotkeysPreferences;
diff --git a/platform/ui/src/components/HotkeysPreferences/utils.js b/platform/ui/src/components/HotkeysPreferences/utils.js
new file mode 100644
index 000000000..3b0fa0aef
--- /dev/null
+++ b/platform/ui/src/components/HotkeysPreferences/utils.js
@@ -0,0 +1,52 @@
+import { hotkeysValidators } from './hotkeysValidators';
+
+/**
+ * Split hotkeys definitions and create hotkey related tuples
+ *
+ * @param {array} hotkeyDefinitions
+ * @returns {array} array of tuples consisted of command name and hotkey definition
+ */
+const splitHotkeyDefinitionsAndCreateTuples = hotkeyDefinitions => {
+ const splitedHotkeys = [];
+ const arrayHotkeys = Object.entries(hotkeyDefinitions);
+
+ if (arrayHotkeys.length) {
+ const halfwayThrough = Math.ceil(arrayHotkeys.length / 2);
+ splitedHotkeys.push(arrayHotkeys.slice(0, halfwayThrough));
+ splitedHotkeys.push(
+ arrayHotkeys.slice(halfwayThrough, arrayHotkeys.length)
+ );
+ }
+
+ return splitedHotkeys;
+};
+
+/**
+ * Validate a hotkey change
+ *
+ * @param {Object} arguments
+ * @param {string} arguments.commandName command name or id
+ * @param {array} arguments.pressedKeys new keys
+ * @param {array} arguments.hotkeys current hotkeys
+ * @returns {Object} {error} validation error
+ */
+const validate = ({ commandName, pressedKeys, hotkeys }) => {
+ for (const validator of hotkeysValidators) {
+ const validation = validator({
+ commandName,
+ pressedKeys,
+ hotkeys,
+ });
+
+ if (validation && validation.error) {
+ return validation;
+ }
+ }
+
+ return { error: undefined };
+};
+
+export {
+ validate,
+ splitHotkeyDefinitionsAndCreateTuples
+};
diff --git a/platform/ui/src/components/Input/Input.jsx b/platform/ui/src/components/Input/Input.jsx
index 96f17001c..af981ae2a 100644
--- a/platform/ui/src/components/Input/Input.jsx
+++ b/platform/ui/src/components/Input/Input.jsx
@@ -4,7 +4,7 @@ import Label from '../Label';
import classnames from 'classnames';
const baseInputClasses =
- 'shadow transition duration-300 appearance-none border border-primary-main hover:border-gray-500 focus:border-gray-500 focus:outline-none rounded w-full py-2 px-3 mt-2 text-sm text-white leading-tight focus:outline-none';
+ 'shadow transition duration-300 appearance-none border border-primary-main hover:border-gray-500 focus:border-gray-500 focus:outline-none rounded w-full py-2 px-3 text-sm text-white leading-tight focus:outline-none';
const transparentClasses = {
true: 'bg-transparent',
@@ -23,6 +23,9 @@ const Input = ({
onFocus,
autoFocus,
onKeyPress,
+ onKeyDown,
+ readOnly,
+ disabled,
...otherProps
}) => {
return (
@@ -30,16 +33,21 @@ const Input = ({
@@ -57,7 +65,10 @@ Input.propTypes = {
onChange: PropTypes.func,
onFocus: PropTypes.func,
autoFocus: PropTypes.bool,
+ readOnly: PropTypes.bool,
onKeyPress: PropTypes.func,
+ onKeyDown: PropTypes.func,
+ disabled: PropTypes.bool
};
export default Input;
diff --git a/platform/ui/src/components/Select/Select.jsx b/platform/ui/src/components/Select/Select.jsx
index 0cc410a46..2cb76d728 100644
--- a/platform/ui/src/components/Select/Select.jsx
+++ b/platform/ui/src/components/Select/Select.jsx
@@ -74,14 +74,14 @@ const Select = ({
components={_components}
placeholder={placeholder}
options={options}
- value={selectedOptions}
+ value={isMulti ? selectedOptions : value}
onChange={(selectedOptions, { action }) => {
const newSelection = !selectedOptions.length
? selectedOptions
: selectedOptions.reduce((acc, curr) => acc.concat([curr.value]), []);
onChange(newSelection, action);
}}
- >
+ />
);
};
@@ -112,7 +112,7 @@ Select.propTypes = {
})
),
placeholder: PropTypes.string,
- value: PropTypes.arrayOf(PropTypes.string),
+ value: PropTypes.oneOfType(PropTypes.string, PropTypes.arrayOf(PropTypes.string)),
};
export default Select;
diff --git a/platform/ui/src/components/UserPreferences/UserPreferences.jsx b/platform/ui/src/components/UserPreferences/UserPreferences.jsx
new file mode 100644
index 000000000..a3ecf1743
--- /dev/null
+++ b/platform/ui/src/components/UserPreferences/UserPreferences.jsx
@@ -0,0 +1,144 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+import { Select, Typography, Button, HotkeysPreferences } from '@ohif/ui';
+import i18n from '@ohif/i18n';
+import { useTranslation } from 'react-i18next';
+const { availableLanguages, defaultLanguage, currentLanguage } = i18n;
+
+const UserPreferences = ({ disabled, hotkeyDefinitions, hotkeyDefaults, onCancel, onSubmit, onReset }) => {
+ const { t } = useTranslation('UserPreferencesModal');
+ const [state, setState] = useState({
+ isDisabled: disabled,
+ hotkeyErrors: {},
+ hotkeyDefinitions,
+ language: currentLanguage()
+ });
+
+ const onSubmitHandler = () => {
+ i18n.changeLanguage(state.language.value);
+ onSubmit(state);
+ };
+
+ const onResetHandler = () => {
+ setState(state => ({
+ ...state,
+ language: defaultLanguage,
+ hotkeyDefinitions: hotkeyDefaults,
+ hotkeyErrors: {},
+ isDisabled: disabled,
+ }));
+ onReset();
+ };
+
+ const onCancelHandler = () => {
+ setState({ hotkeyDefinitions });
+ onCancel();
+ };
+
+ const onLanguageChangeHandler = (value) => {
+ setState(state => ({ ...state, language: value }));
+ };
+
+ const onHotkeysChangeHandler = (id, definition, errors) => {
+ setState(state => ({
+ ...state,
+ isDisabled: Object.values(errors).every(e => e !== undefined),
+ hotkeyErrors: errors,
+ hotkeyDefinitions: {
+ ...state.hotkeyDefinitions,
+ [id]: definition,
+ }
+ }));
+ };
+
+ const Section = ({ title, children }) => (
+ <>
+
+
+ {title}
+
+
+
+ {children}
+
+ >
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const noop = () => { };
+
+UserPreferences.propTypes = {
+ disabled: PropTypes.bool,
+ hotkeyDefaults: PropTypes.object.isRequired,
+ hotkeyDefinitions: PropTypes.object.isRequired,
+ languageOptions: PropTypes.arrayOf(
+ PropTypes.shape({
+ label: PropTypes.string.isRequired,
+ value: PropTypes.any.isRequired,
+ })
+ ),
+ onCancel: PropTypes.func,
+ onSubmit: PropTypes.func,
+ onReset: PropTypes.func,
+};
+
+UserPreferences.defaultProps = {
+ languageOptions: [
+ { value: 'ONE', label: 'ONE' },
+ { value: 'TWO', label: 'TWO' },
+ ],
+ onCancel: noop,
+ onSubmit: noop,
+ onReset: noop,
+ disabled: true
+};
+
+export default UserPreferences;
diff --git a/platform/ui/src/components/UserPreferences/index.js b/platform/ui/src/components/UserPreferences/index.js
new file mode 100644
index 000000000..857d525bb
--- /dev/null
+++ b/platform/ui/src/components/UserPreferences/index.js
@@ -0,0 +1,3 @@
+import UserPreferences from './UserPreferences.jsx';
+
+export default UserPreferences;
diff --git a/platform/ui/src/components/index.js b/platform/ui/src/components/index.js
index ee1946abd..533a91977 100644
--- a/platform/ui/src/components/index.js
+++ b/platform/ui/src/components/index.js
@@ -1,3 +1,4 @@
+import AboutModal from './AboutModal';
import Button from './Button';
import ButtonGroup from './ButtonGroup';
import ContextMenu from './ContextMenu';
@@ -53,8 +54,17 @@ import ViewportActionBar from './ViewportActionBar';
import ViewportDownloadForm from './ViewportDownloadForm';
import ViewportGrid from './ViewportGrid';
import ViewportPane from './ViewportPane';
+import UserPreferences from './UserPreferences';
+import HotkeysPreferences from './HotkeysPreferences';
+import HotkeyField from './HotkeyField';
+import Header from './Header';
export {
+ AboutModal,
+ HotkeyField,
+ Header,
+ UserPreferences,
+ HotkeysPreferences,
Button,
ButtonGroup,
ContextMenu,
diff --git a/platform/viewer/src/appInit.js b/platform/viewer/src/appInit.js
index 6e3a34662..cd671c666 100644
--- a/platform/viewer/src/appInit.js
+++ b/platform/viewer/src/appInit.js
@@ -43,6 +43,7 @@ function appInit(appConfigOrFunc, defaultExtensions) {
const extensionManager = new ExtensionManager({
commandsManager,
servicesManager,
+ hotkeysManager,
appConfig,
});
diff --git a/platform/viewer/src/components/PreferencesDropdown/PreferencesDropdown.jsx b/platform/viewer/src/components/PreferencesDropdown/PreferencesDropdown.jsx
deleted file mode 100644
index b09188206..000000000
--- a/platform/viewer/src/components/PreferencesDropdown/PreferencesDropdown.jsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import React from 'react';
-import AboutModal from './AboutModal';
-import { Dropdown, IconButton, Icon, useModal } from '@ohif/ui';
-
-const PreferencesDropdown = () => {
- const { show } = useModal();
-
- const showAboutModal = () => {
- show({
- content: AboutModal,
- title: 'About OHIF Viewer',
- });
- };
-
- const showPreferencesModal = () => {
- const modalComponent = () => Preferences modal
;
- show({
- content: modalComponent,
- title: 'Preferences',
- });
- };
-
- return (
-
-
-
-
- {}}
- >
-
-
-
- );
-};
-
-export default PreferencesDropdown;
diff --git a/platform/viewer/src/components/PreferencesDropdown/index.js b/platform/viewer/src/components/PreferencesDropdown/index.js
deleted file mode 100644
index edf8641a2..000000000
--- a/platform/viewer/src/components/PreferencesDropdown/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './PreferencesDropdown';
diff --git a/platform/viewer/src/routes/Mode/Mode.jsx b/platform/viewer/src/routes/Mode/Mode.jsx
index a851c0f24..bff686da1 100644
--- a/platform/viewer/src/routes/Mode/Mode.jsx
+++ b/platform/viewer/src/routes/Mode/Mode.jsx
@@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
// TODO: DicomMetadataStore should be injected?
import { DicomMetadataStore, utils } from '@ohif/core';
import { DragAndDropProvider, ImageViewerProvider } from '@ohif/ui';
-//
import { useQuery } from '@hooks';
import ViewportGrid from '@components/ViewportGrid';
import Compose from './Compose';
@@ -88,12 +87,10 @@ export default function ModeRoute({
return;
}
- console.debug('[hotkeys] Setting up hotkeys...');
hotkeysManager.setDefaultHotKeys(hotkeys);
hotkeysManager.setHotkeys(hotkeys);
return () => {
- console.debug('[hotkeys] Removing hotkeys...');
hotkeysManager.destroy();
};
}, []);
@@ -282,4 +279,5 @@ ModeRoute.propTypes = {
dataSourceName: PropTypes.string,
extensionManager: PropTypes.object,
servicesManager: PropTypes.object,
+ hotkeysManager: PropTypes.object,
};
diff --git a/platform/viewer/src/routes/WorkList/WorkList.jsx b/platform/viewer/src/routes/WorkList/WorkList.jsx
index 440b1a75d..92f4e2698 100644
--- a/platform/viewer/src/routes/WorkList/WorkList.jsx
+++ b/platform/viewer/src/routes/WorkList/WorkList.jsx
@@ -5,25 +5,26 @@ import { Link } from 'react-router-dom';
import moment from 'moment';
import qs from 'query-string';
import isEqual from 'lodash.isequal';
-
+import { useTranslation } from 'react-i18next';
+//
import filtersMeta from './filtersMeta.js';
import { useAppConfig } from '@state';
import { useDebounce, useQuery } from '@hooks';
import { utils } from '@ohif/core';
-import PreferencesDropdown from '../../components/PreferencesDropdown';
-
import {
Icon,
StudyListExpandedRow,
Button,
- NavBar,
- Svg,
EmptyStudies,
StudyListTable,
StudyListPagination,
StudyListFilter,
TooltipClipboard,
+ Header,
+ useModal,
+ AboutModal,
+ UserPreferences
} from '@ohif/ui';
const seriesInStudiesMap = new Map();
@@ -32,7 +33,11 @@ const seriesInStudiesMap = new Map();
* TODO:
* - debounce `setFilterValues` (150ms?)
*/
-function WorkList({ history, data: studies, isLoadingData, dataSource }) {
+function WorkList({ history, data: studies, isLoadingData, dataSource, hotkeysManager }) {
+ const { hotkeyDefinitions, hotkeyDefaults } = hotkeysManager;
+ const { show, hide } = useModal();
+ const { t } = useTranslation();
+
// ~ Modes
const [appConfig] = useAppConfig();
// ~ Filters
@@ -340,25 +345,39 @@ function WorkList({ history, data: studies, isLoadingData, dataSource }) {
const hasStudies = numOfStudies > 0;
+ const menuOptions = [
+ {
+ title: t('Header:About'),
+ icon: 'info',
+ onClick: () => show({ content: AboutModal, title: 'About OHIF Viewer' })
+ },
+ {
+ title: t('Header:Preferences'),
+ icon: 'settings',
+ onClick: () => show({
+ title: t('UserPreferencesModal:User Preferences'),
+ content: UserPreferences,
+ contentProps: {
+ hotkeyDefaults: hotkeysManager.getValidHotkeyDefinitions(hotkeyDefaults),
+ hotkeyDefinitions,
+ onCancel: hide,
+ onSubmit: ({ hotkeyDefinitions }) => {
+ hotkeysManager.setHotkeys(hotkeyDefinitions);
+ hide();
+ },
+ onReset: () => hotkeysManager.restoreDefaultBindings()
+ }
+ })
+ },
+ ];
+
return (
-
-
-
-
- FOR INVESTIGATIONAL USE ONLY
-
-
-
-
+
(
// eslint-disable-next-line react/jsx-props-no-spreading
-
+
)}
/>