From db795f3cb03dd0a1bd2f295842f0aef394af7820 Mon Sep 17 00:00:00 2001 From: igoroctaviano Date: Mon, 29 Jun 2020 11:53:46 -0300 Subject: [PATCH] Add hotkeys config for modes and defaults to core --- modes/example/src/index.js | 4 + modes/longitudinal/src/index.js | 4 + platform/core/src/defaults/hotkeyBindings.js | 95 +++++++++++++++++++ platform/core/src/defaults/index.js | 2 + platform/core/src/index.js | 8 +- platform/viewer/src/App.jsx | 10 +- platform/viewer/src/appInit.js | 11 ++- platform/viewer/src/customHooks/useQuery.js | 2 +- platform/viewer/src/hooks/index.js | 3 +- platform/viewer/src/hooks/useHotkeys.js | 31 ++++++ platform/viewer/src/routes/Mode/Mode.jsx | 6 +- platform/viewer/src/routes/buildModeRoutes.js | 9 +- platform/viewer/src/routes/index.js | 20 ++-- 13 files changed, 182 insertions(+), 23 deletions(-) create mode 100644 platform/core/src/defaults/hotkeyBindings.js create mode 100644 platform/core/src/defaults/index.js create mode 100644 platform/viewer/src/hooks/useHotkeys.js 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/platform/core/src/defaults/hotkeyBindings.js b/platform/core/src/defaults/hotkeyBindings.js new file mode 100644 index 000000000..960ed605d --- /dev/null +++ b/platform/core/src/defaults/hotkeyBindings.js @@ -0,0 +1,95 @@ +export default [ + // ~ Global + { + commandName: 'incrementActiveViewport', + label: 'Next Viewport', + keys: ['right'], + }, + { + commandName: 'decrementActiveViewport', + label: 'Previous Viewport', + keys: ['left'], + }, + // Supported Keys: https://craig.is/killing/mice + // ~ Cornerstone Extension + { 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'] }, + // clearAnnotations + { commandName: 'nextImage', label: 'Next Image', keys: ['down'] }, + { commandName: 'previousImage', label: 'Previous Image', keys: ['up'] }, + // firstImage + // lastImage + { + commandName: 'previousViewportDisplaySet', + label: 'Previous Series', + keys: ['pagedown'], + }, + { + commandName: 'nextViewportDisplaySet', + label: 'Next Series', + keys: ['pageup'], + }, + // ~ Cornerstone Tools + { commandName: 'setZoomTool', label: 'Zoom', keys: ['z'] }, + // ~ Window level presets + { + commandName: 'windowLevelPreset1', + label: 'W/L Preset 1', + keys: ['1'], + }, + { + commandName: 'windowLevelPreset2', + label: 'W/L Preset 2', + keys: ['2'], + }, + { + commandName: 'windowLevelPreset3', + label: 'W/L Preset 3', + keys: ['3'], + }, + { + commandName: 'windowLevelPreset4', + label: 'W/L Preset 4', + keys: ['4'], + }, + { + commandName: 'windowLevelPreset5', + label: 'W/L Preset 5', + keys: ['5'], + }, + { + commandName: 'windowLevelPreset6', + label: 'W/L Preset 6', + keys: ['6'], + }, + { + commandName: 'windowLevelPreset7', + label: 'W/L Preset 7', + keys: ['7'], + }, + { + commandName: 'windowLevelPreset8', + label: 'W/L Preset 8', + keys: ['8'], + }, + { + commandName: 'windowLevelPreset9', + 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..7c87bc150 --- /dev/null +++ b/platform/core/src/defaults/index.js @@ -0,0 +1,2 @@ +import hotkeyBindings from './hotkeyBindings'; +export { hotkeyBindings }; diff --git a/platform/core/src/index.js b/platform/core/src/index.js index acfdd3fbf..d355da5cc 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 { hotkeyBindings } from './defaults'; import { UIDialogService, @@ -37,6 +38,11 @@ import { import IWebApiDataSource from './DataSources/IWebApiDataSource'; +const hotkeys = { + ...utils.hotkeys, + defaults: { hotkeyBindings } +}; + const OHIF = { MODULE_TYPES, // 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/customHooks/useQuery.js b/platform/viewer/src/customHooks/useQuery.js index d9ac2f95a..6a2ea87a9 100644 --- a/platform/viewer/src/customHooks/useQuery.js +++ b/platform/viewer/src/customHooks/useQuery.js @@ -6,6 +6,6 @@ import { useLocation } from 'react-router-dom'; * * @name useQuery */ -export default function() { +export default function () { return new URLSearchParams(useLocation().search); } diff --git a/platform/viewer/src/hooks/index.js b/platform/viewer/src/hooks/index.js index 8b80a355b..54693b08e 100644 --- a/platform/viewer/src/hooks/index.js +++ b/platform/viewer/src/hooks/index.js @@ -1,4 +1,5 @@ import useDebounce from './useDebounce.js'; import useQuery from './useQuery.js'; +import useHotkeys from './useHotkeys.js'; -export { useDebounce, useQuery }; +export { useDebounce, useQuery, useHotkeys }; diff --git a/platform/viewer/src/hooks/useHotkeys.js b/platform/viewer/src/hooks/useHotkeys.js new file mode 100644 index 000000000..2ee986e73 --- /dev/null +++ b/platform/viewer/src/hooks/useHotkeys.js @@ -0,0 +1,31 @@ +import { useEffect, useState } from 'react'; + +/** + * @param {Object} manager HotkeysManager instance + * @param {Object} hotkeys hotkey bindings + * @param {Object} defaultHotkeys default hotkey bindings + */ +const useHotkeys = (manager, hotkeys, defaultHotkeys) => { + const [isLoaded, setIsLoaded] = useState(false); + + useEffect(() => { + if (!hotkeys) { + console.warn('[hotkeys] No bindings defined for hotkeys hook!'); + return; + } + + console.debug('[hotkeys] Setting up hotkeys...'); + manager.setDefaultHotKeys(defaultHotkeys || hotkeys); + manager.setHotkeys(hotkeys); + setIsLoaded(true); + + return () => { + console.debug('[hotkeys] Removing hotkeys...'); + manager.destroy(); + }; + }, [manager, hotkeys, defaultHotkeys]); + + return isLoaded; +} + +export default useHotkeys; diff --git a/platform/viewer/src/routes/Mode/Mode.jsx b/platform/viewer/src/routes/Mode/Mode.jsx index 371aa4c0d..583f9350c 100644 --- a/platform/viewer/src/routes/Mode/Mode.jsx +++ b/platform/viewer/src/routes/Mode/Mode.jsx @@ -5,7 +5,7 @@ import PropTypes from 'prop-types'; import { DicomMetadataStore } from '@ohif/core'; import { DragAndDropProvider, ImageViewerProvider } from '@ohif/ui'; // -import { useQuery } from '@hooks'; +import { useQuery, useHotkeys } from '@hooks'; import ViewportGrid from '@components/ViewportGrid'; import Compose from './Compose'; @@ -15,7 +15,9 @@ export default function ModeRoute({ dataSourceName, extensionManager, servicesManager, + hotkeysManager }) { + const isHotkeysLoaded = useHotkeys(hotkeysManager, mode.hotkeys); // Parse route params/querystring const query = useQuery(); const queryStudyInstanceUIDs = query.get('StudyInstanceUIDs'); @@ -83,6 +85,8 @@ export default function ModeRoute({ route, servicesManager, extensionManager, + hotkeysManager, + isHotkeysLoaded ]); // 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,