diff --git a/modes/example/src/index.js b/modes/example/src/index.js index 33aae7ad1..597f179ca 100644 --- a/modes/example/src/index.js +++ b/modes/example/src/index.js @@ -1,4 +1,5 @@ import toolbarButtons from './toolbarButtons.js'; +import { hotkeys } from '@ohif/core'; export default function mode({ modeConfiguration }) { return { @@ -68,6 +69,9 @@ export default function mode({ modeConfiguration }) { ], extensions: ['org.ohif.default', 'org.ohif.cornerstone'], sopClassHandlers: ['org.ohif.default.sopClassHandlerModule.stack'], + hotkeys: [ + ...hotkeys.defaults.hotkeyBindings + ] }; } diff --git a/modes/longitudinal/src/index.js b/modes/longitudinal/src/index.js index 71c249341..2bf100c09 100644 --- a/modes/longitudinal/src/index.js +++ b/modes/longitudinal/src/index.js @@ -1,4 +1,5 @@ import toolbarButtons from './toolbarButtons.js'; +import { hotkeys } from '@ohif/core'; const ohif = { layout: 'org.ohif.default.layoutTemplateModule.viewerLayout', @@ -94,6 +95,9 @@ export default function mode({ modeConfiguration }) { 'org.ohif.dicom-sr', ], sopClassHandlers: [ohif.sopClassHandler, dicomsr.sopClassHandler], + hotkeys: [ + ...hotkeys.defaults.hotkeyBindings + ] }; } diff --git a/modes/longitudinal/src/toolbarButtons.js b/modes/longitudinal/src/toolbarButtons.js index c2e5cd5e4..b0a271638 100644 --- a/modes/longitudinal/src/toolbarButtons.js +++ b/modes/longitudinal/src/toolbarButtons.js @@ -1,8 +1,11 @@ // TODO: torn, can either bake this here; or have to create a whole new button type // Only ways that you can pass in a custom React component for render :l -import { ExpandableToolbarButton, ListMenu } from '@ohif/ui'; import React from 'react'; import classnames from 'classnames'; +import { ExpandableToolbarButton, ListMenu } from '@ohif/ui'; +import { defaults } from '@ohif/core'; + +const { windowLevelPresets } = defaults; export default [ // Divider @@ -39,36 +42,36 @@ export default [ commandName: 'setToolActive', commandOptions: { toolName: 'Wwwc' }, commands: { - 'windowLevelPreset1': { - commandName: 'windowLevelPreset1', - commandOptions: {}, + 1: { + commandName: 'setWindowLevel', + commandOptions: windowLevelPresets[1], }, - 'windowLevelPreset2': { - commandName: 'windowLevelPreset2', - commandOptions: {}, + 2: { + commandName: 'setWindowLevel', + commandOptions: windowLevelPresets[2], }, - 'windowLevelPreset3': { - commandName: 'windowLevelPreset3', - commandOptions: {}, + 3: { + commandName: 'setWindowLevel', + commandOptions: windowLevelPresets[3], }, - 'windowLevelPreset4': { - commandName: 'windowLevelPreset4', - commandOptions: {}, + 4: { + commandName: 'setWindowLevel', + commandOptions: windowLevelPresets[4], }, - 'windowLevelPreset5': { - commandName: 'windowLevelPreset5', - commandOptions: {}, + 5: { + commandName: 'setWindowLevel', + commandOptions: windowLevelPresets[5], } }, type: 'primary', content: ListMenu, contentProps: { options: [ - { value: 'windowLevelPreset1', title: 'Soft tissue', subtitle: '400 / 40' }, - { value: 'windowLevelPreset2', title: 'Lung', subtitle: '1500 / -600' }, - { value: 'windowLevelPreset3', title: 'Liver', subtitle: '150 / 90' }, - { value: 'windowLevelPreset4', title: 'Bone', subtitle: '80 / 40' }, - { value: 'windowLevelPreset5', title: 'Brain', subtitle: '2500 / 480' }, + { value: 1, title: 'Soft tissue', subtitle: '400 / 40' }, + { value: 2, title: 'Lung', subtitle: '1500 / -600' }, + { value: 3, title: 'Liver', subtitle: '150 / 90' }, + { value: 4, title: 'Bone', subtitle: '80 / 40' }, + { value: 5, title: 'Brain', subtitle: '2500 / 480' }, ], renderer: ({ title, subtitle, isActive, index }) => ( <> diff --git a/platform/core/src/classes/HotkeysManager.js b/platform/core/src/classes/HotkeysManager.js index ee65f08a3..0848e0437 100644 --- a/platform/core/src/classes/HotkeysManager.js +++ b/platform/core/src/classes/HotkeysManager.js @@ -1,3 +1,4 @@ +import objectHash from 'object-hash'; import hotkeys from './../utils/hotkeys'; import log from './../log.js'; @@ -6,6 +7,7 @@ import log from './../log.js'; * * @typedef {Object} HotkeyDefinition * @property {String} commandName - Command to call + * @property {Object} commandOptions - Command options * @property {String} label - Display name for hotkey * @property {String[]} keys - Keys to bind; Follows Mousetrap.js binding syntax */ @@ -18,7 +20,7 @@ export class HotkeysManager { if (!commandsManager) { log.warn( - 'HotkeysManager instantiated without a commandsManager. Hotkeys will be unable to find and run commands.' + '[hotkeys] HotkeysManager instantiated without a commandsManager. Hotkeys will be unable to find and run commands.' ); } @@ -141,28 +143,30 @@ export class HotkeysManager { * When a hotkey combination is triggered, the command name and active contexts * are used to locate the correct command to call. * - * @param {HotkeyDefinition} commandName + * @param {HotkeyDefinition} command * @param {String} extension * @returns {undefined} */ - registerHotkeys({ commandName, keys, label } = {}, extension) { + registerHotkeys({ commandName, commandOptions = {}, keys, label } = {}, extension) { if (!commandName) { - log.warn(`No command was defined for hotkey "${keys}"`); + log.warn(`[hotkeys] No command was defined for hotkey "${keys}"`); return; } - const previouslyRegisteredDefinition = this.hotkeyDefinitions[commandName]; + const commandHash = objectHash({ commandName, commandOptions }); + const options = Object.keys(commandOptions).length ? JSON.stringify(commandOptions) : 'no'; + const previouslyRegisteredDefinition = this.hotkeyDefinitions[commandHash]; if (previouslyRegisteredDefinition) { const previouslyRegisteredKeys = previouslyRegisteredDefinition.keys; this._unbindHotkeys(commandName, previouslyRegisteredKeys); - log.info(`Unbinding ${commandName} from ${previouslyRegisteredKeys}`); + log.info(`[hotkeys] Unbinding ${commandName} with ${options} options from ${previouslyRegisteredKeys}`); } // Set definition & bind - this.hotkeyDefinitions[commandName] = { keys, label }; - this._bindHotkeys(commandName, keys); - log.info(`Binding ${commandName} to ${keys}`); + this.hotkeyDefinitions[commandHash] = { keys, label }; + this._bindHotkeys(commandName, commandOptions, keys); + log.info(`[hotkeys] Binding ${commandName} with ${options} options to ${keys}`); } /** @@ -191,7 +195,7 @@ export class HotkeysManager { * @param {string[]} keys - One or more key combinations that should trigger command * @returns {undefined} */ - _bindHotkeys(commandName, keys) { + _bindHotkeys(commandName, commandOptions = {}, keys) { const isKeyDefined = keys === '' || keys === undefined; if (isKeyDefined) { return; @@ -203,7 +207,7 @@ export class HotkeysManager { hotkeys.bind(combinedKeys, evt => { evt.preventDefault(); evt.stopPropagation(); - this._commandsManager.runCommand(commandName, { evt }); + this._commandsManager.runCommand(commandName, { evt, ...commandOptions }); }); } diff --git a/platform/core/src/defaults/hotkeyBindings.js b/platform/core/src/defaults/hotkeyBindings.js new file mode 100644 index 000000000..cbce9728b --- /dev/null +++ b/platform/core/src/defaults/hotkeyBindings.js @@ -0,0 +1,103 @@ +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: 'flipViewportVertical', + label: 'Flip Horizontally', + keys: ['h'], + }, + { + commandName: 'flipViewportHorizontal', + label: 'Flip Vertically', + keys: ['v'], + }, + { 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: 'nextViewportDisplaySet', + label: 'Next Series', + keys: ['pageup'], + }, + /** Window level presets */ + { + commandName: 'setWindowLevel', + commandOptions: windowLevelPresets[1], + label: 'W/L Preset 1', + keys: ['1'], + }, + { + commandName: 'setWindowLevel', + commandOptions: windowLevelPresets[2], + label: 'W/L Preset 2', + keys: ['2'], + }, + { + commandName: 'setWindowLevel', + commandOptions: windowLevelPresets[3], + label: 'W/L Preset 3', + keys: ['3'], + }, + { + commandName: 'setWindowLevel', + commandOptions: windowLevelPresets[4], + label: 'W/L Preset 4', + keys: ['4'], + }, + { + commandName: 'setWindowLevel', + commandOptions: windowLevelPresets[5], + label: 'W/L Preset 5', + keys: ['5'], + }, + { + commandName: 'setWindowLevel', + commandOptions: windowLevelPresets[6], + label: 'W/L Preset 6', + keys: ['6'], + }, + { + commandName: 'setWindowLevel', + commandOptions: windowLevelPresets[7], + label: 'W/L Preset 7', + keys: ['7'], + }, + { + commandName: 'setWindowLevel', + commandOptions: windowLevelPresets[8], + label: 'W/L Preset 8', + keys: ['8'], + }, + { + commandName: 'setWindowLevel', + commandOptions: windowLevelPresets[9], + label: 'W/L Preset 9', + keys: ['9'], + }, +]; diff --git a/platform/core/src/defaults/index.js b/platform/core/src/defaults/index.js new file mode 100644 index 000000000..f8c065d7b --- /dev/null +++ b/platform/core/src/defaults/index.js @@ -0,0 +1,4 @@ +import hotkeyBindings from './hotkeyBindings'; +import windowLevelPresets from './windowLevelPresets'; +export { hotkeyBindings, windowLevelPresets }; +export default { hotkeyBindings, windowLevelPresets }; diff --git a/platform/core/src/defaults/windowLevelPresets.js b/platform/core/src/defaults/windowLevelPresets.js new file mode 100644 index 000000000..83a7321ba --- /dev/null +++ b/platform/core/src/defaults/windowLevelPresets.js @@ -0,0 +1,12 @@ +export default { + 1: { description: 'Soft tissue', window: '400', level: '40' }, + 2: { description: 'Lung', window: '1500', level: '-600' }, + 3: { description: 'Liver', window: '150', level: '90' }, + 4: { description: 'Bone', window: '2500', level: '480' }, + 5: { description: 'Brain', window: '80', level: '40' }, + 6: { description: 'Trest', window: '1', level: '1' }, + 7: { description: '', window: '', level: '' }, + 8: { description: '', window: '', level: '' }, + 9: { description: '', window: '', level: '' }, + 10: { description: '', window: '', level: '' }, +}; diff --git a/platform/core/src/index.js b/platform/core/src/index.js index acfdd3fbf..a16604cc0 100644 --- a/platform/core/src/index.js +++ b/platform/core/src/index.js @@ -20,7 +20,8 @@ import studies from './studies/'; import ui from './ui'; import user from './user.js'; import { ViewModelProvider, useViewModel } from './ViewModelContext'; -import utils, { hotkeys } from './utils/'; +import utils from './utils/'; +import defaults from './defaults'; import { UIDialogService, @@ -37,6 +38,11 @@ import { import IWebApiDataSource from './DataSources/IWebApiDataSource'; +const hotkeys = { + ...utils.hotkeys, + defaults: { hotkeyBindings: defaults.hotkeyBindings } +}; + const OHIF = { MODULE_TYPES, // @@ -45,6 +51,7 @@ const OHIF = { HotkeysManager, ServicesManager, // + defaults, utils, hotkeys, studies, @@ -88,6 +95,7 @@ export { HotkeysManager, ServicesManager, // + defaults, utils, hotkeys, studies, diff --git a/platform/core/src/index.test.js b/platform/core/src/index.test.js index 47ee09488..d7aef6f69 100644 --- a/platform/core/src/index.test.js +++ b/platform/core/src/index.test.js @@ -15,6 +15,7 @@ describe('Top level exports', () => { 'UIDialogService', 'MeasurementService', // + 'defaults', 'utils', 'hotkeys', 'studies', diff --git a/platform/core/src/redux/reducers/preferences.js b/platform/core/src/redux/reducers/preferences.js index 2ec01c917..a9eca66e3 100644 --- a/platform/core/src/redux/reducers/preferences.js +++ b/platform/core/src/redux/reducers/preferences.js @@ -1,16 +1,7 @@ +import { windowLevelPresets } from '../../defaults'; + const defaultState = { - windowLevelData: { - 1: { description: 'Soft tissue', window: '550', level: '40' }, - 2: { description: 'Lung', window: '150', level: '-600' }, - 3: { description: 'Liver', window: '150', level: '90' }, - 4: { description: 'Bone', window: '2500', level: '480' }, - 5: { description: 'Brain', window: '80', level: '40' }, - 6: { description: 'Trest', window: '1', level: '1' }, - 7: { description: '', window: '', level: '' }, - 8: { description: '', window: '', level: '' }, - 9: { description: '', window: '', level: '' }, - 10: { description: '', window: '', level: '' }, - }, + windowLevelData: windowLevelPresets, generalPreferences: { // language: 'en-US' }, diff --git a/platform/ui/src/components/ViewportPane/ViewportPane.jsx b/platform/ui/src/components/ViewportPane/ViewportPane.jsx index b1374ca14..7c9bea5bf 100644 --- a/platform/ui/src/components/ViewportPane/ViewportPane.jsx +++ b/platform/ui/src/components/ViewportPane/ViewportPane.jsx @@ -14,6 +14,7 @@ function ViewportPane({ onInteraction, acceptDropsFor, }) { + let dropElement = null; const [{ isHovered, isHighlighted }, drop] = useDrop({ accept: acceptDropsFor, // TODO: pass in as prop? @@ -22,7 +23,7 @@ function ViewportPane({ const isOver = monitor.isOver(); if (canDrop && isOver && onDrop) { - onInteraction(); + onInteractionHandler(); onDrop(droppedItem); } }, @@ -33,16 +34,32 @@ function ViewportPane({ }), }); + const focus = () => { + if (dropElement) { + dropElement.focus(); + } + }; + + const onInteractionHandler = () => { + onInteraction(); + focus(); + }; + + const refHandler = element => { + drop(element); + dropElement = element; + }; + return (
{}; +const noop = () => { }; ViewportPane.defaultProps = { onInteraction: noop, diff --git a/platform/viewer/src/App.jsx b/platform/viewer/src/App.jsx index 5ab382d4f..1cb815d60 100644 --- a/platform/viewer/src/App.jsx +++ b/platform/viewer/src/App.jsx @@ -30,7 +30,7 @@ const Router = JSON.parse(process.env.USE_HASH_ROUTER) ? HashRouter : BrowserRouter; -let commandsManager, extensionManager, servicesManager; +let commandsManager, extensionManager, servicesManager, hotkeysManager; function App({ config, defaultExtensions }) { const init = appInit(config, defaultExtensions); @@ -39,17 +39,19 @@ function App({ config, defaultExtensions }) { commandsManager = init.commandsManager; extensionManager = init.extensionManager; servicesManager = init.servicesManager; + hotkeysManager = init.hotkeysManager; // Set appConfig const appConfigState = init.appConfig; const { routerBasename, modes, dataSources } = appConfigState; // Use config to create routes - const appRoutes = createRoutes( + const appRoutes = createRoutes({ modes, dataSources, extensionManager, - servicesManager - ); + servicesManager, + hotkeysManager + }); const { UIDialogService, UIModalService, diff --git a/platform/viewer/src/appInit.js b/platform/viewer/src/appInit.js index 2f5433f54..eb14613ce 100644 --- a/platform/viewer/src/appInit.js +++ b/platform/viewer/src/appInit.js @@ -2,7 +2,7 @@ import { CommandsManager, ExtensionManager, ServicesManager, - // HotkeysManager, + HotkeysManager, UINotificationService, UIModalService, UIDialogService, @@ -31,13 +31,14 @@ function appInit(appConfigOrFunc, defaultExtensions) { // TODO: Wire this up to Rodrigo's basic Context "ContextService" const commandsManagerConfig = { /** Used by commands to inject `viewports` from "redux" */ - getAppState: () => {}, + getAppState: () => { }, /** Used by commands to determine active context */ getActiveContexts: () => ['VIEWER', 'ACTIVE_VIEWPORT::CORNERSTONE'], }; - const commandsManager = new CommandsManager(commandsManagerConfig); + const servicesManager = new ServicesManager(); - // const hotkeysManager = new HotkeysManager(commandsManager, servicesManager); + const commandsManager = new CommandsManager(commandsManagerConfig); + const hotkeysManager = new HotkeysManager(commandsManager, servicesManager); const extensionManager = new ExtensionManager({ commandsManager, servicesManager, @@ -64,7 +65,6 @@ function appInit(appConfigOrFunc, defaultExtensions) { appConfig.dataSources ); - // TODO: Init global hotkeys, or the hotkeys manager? // TODO: We no longer use `utils.addServer` // TODO: We no longer init webWorkers at app level // TODO: We no longer init the user Manager @@ -80,6 +80,7 @@ function appInit(appConfigOrFunc, defaultExtensions) { commandsManager, extensionManager, servicesManager, + hotkeysManager }; } diff --git a/platform/viewer/src/components/ViewportGrid.jsx b/platform/viewer/src/components/ViewportGrid.jsx index e6384b82e..4052cbd0a 100644 --- a/platform/viewer/src/components/ViewportGrid.jsx +++ b/platform/viewer/src/components/ViewportGrid.jsx @@ -115,15 +115,17 @@ function ViewerViewportGrid(props) { viewportComponents ); + const onInterationHandler = () => { + setActiveViewportIndex(viewportIndex); + }; + viewportPanes[i] = ( { - setActiveViewportIndex(viewportIndex); - }} + onInteraction={onInterationHandler} isActive={activeViewportIndex === viewportIndex} > { + if (!hotkeys) { + console.warn('[hotkeys] No bindings defined for hotkeys hook!'); + return; + } + + console.debug('[hotkeys] Setting up hotkeys...'); + hotkeysManager.setDefaultHotKeys(hotkeys); + hotkeysManager.setHotkeys(hotkeys); + + return () => { + console.debug('[hotkeys] Removing hotkeys...'); + hotkeysManager.destroy(); + }; + }, []); + useEffect(() => { route.init({ servicesManager, extensionManager }); }, [ @@ -83,6 +100,7 @@ export default function ModeRoute({ route, servicesManager, extensionManager, + hotkeysManager ]); // This queries for series, but... What does it do with them? diff --git a/platform/viewer/src/routes/buildModeRoutes.js b/platform/viewer/src/routes/buildModeRoutes.js index bd63e3076..8087b6703 100644 --- a/platform/viewer/src/routes/buildModeRoutes.js +++ b/platform/viewer/src/routes/buildModeRoutes.js @@ -22,12 +22,13 @@ import { ViewModelProvider } from '@ohif/core'; /:modeId/:modeRoute/?queryParameters=example */ -export default function buildModeRoutes( +export default function buildModeRoutes({ modes, dataSources, extensionManager, - servicesManager -) { + servicesManager, + hotkeysManager +}) { const routes = []; // const dataSources = Object.keys(extensionManager.dataSourceMap).map(a => @@ -58,6 +59,7 @@ export default function buildModeRoutes( dataSourceName={dataSourceName} extensionManager={extensionManager} servicesManager={servicesManager} + hotkeysManager={hotkeysManager} /> ); @@ -83,6 +85,7 @@ export default function buildModeRoutes( dataSourceName={defaultDataSourceName} extensionManager={extensionManager} servicesManager={servicesManager} + hotkeysManager={hotkeysManager} /> ); diff --git a/platform/viewer/src/routes/index.js b/platform/viewer/src/routes/index.js index fe66c51fd..2a1eb8087 100644 --- a/platform/viewer/src/routes/index.js +++ b/platform/viewer/src/routes/index.js @@ -21,19 +21,25 @@ const bakedInRoutes = [ { component: NotFound }, ]; -const createRoutes = ( +const createRoutes = ({ modes, dataSources, extensionManager, - servicesManager -) => { - const routes = - buildModeRoutes(modes, dataSources, extensionManager, servicesManager) || - []; + servicesManager, + hotkeysManager +}) => { + const routes = buildModeRoutes({ + modes, + dataSources, + extensionManager, + servicesManager, + hotkeysManager + }) || []; + const allRoutes = [...routes, ...bakedInRoutes]; console.log( - 'Creating Routes: ', + 'Creating Routes:', modes, dataSources, routes,