feat(hotkeys): Migrate hotkeys to customization service and fix issues with overrides (#4777)

This commit is contained in:
Alireza 2025-02-25 13:32:30 -05:00 committed by GitHub
parent 4559d39a91
commit 3e6913b097
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 814 additions and 1552 deletions

View File

@ -25,7 +25,6 @@ import {
colorPickerDialog,
} from '@ohif/extension-default';
import { vec3, mat4 } from 'gl-matrix';
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
import toggleImageSliceSync from './utils/imageSliceSync/toggleImageSliceSync';
import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/utils/selection';
@ -34,6 +33,7 @@ import toggleVOISliceSync from './utils/toggleVOISliceSync';
import { usePositionPresentationStore, useSegmentationPresentationStore } from './stores';
import { toolNames } from './initCornerstoneTools';
const { DefaultHistoryMemo } = csUtils.HistoryMemo;
const toggleSyncFunctions = {
imageSlice: toggleImageSliceSync,
voi: toggleVOISliceSync,
@ -430,6 +430,37 @@ function commandsModule({
actions.setViewportWindowLevel({ ...props, viewportId });
},
setWindowLevelPreset: ({ presetName, presetIndex }) => {
const windowLevelPresets = customizationService.getCustomization(
'cornerstone.windowLevelPresets'
);
const activeViewport = viewportGridService.getActiveViewportId();
const viewport = cornerstoneViewportService.getCornerstoneViewport(activeViewport);
const metadata = viewport.getImageData().metadata;
const modality = metadata.Modality;
if (!modality) {
return;
}
const windowLevelPresetForModality = windowLevelPresets[modality];
if (!windowLevelPresetForModality) {
return;
}
const windowLevelPreset =
windowLevelPresetForModality[presetName] ??
Object.values(windowLevelPresetForModality)[presetIndex];
actions.setViewportWindowLevel({
viewportId: activeViewport,
window: windowLevelPreset.window,
level: windowLevelPreset.level,
});
},
setToolEnabled: ({ toolName, toggle, toolGroupId }) => {
const { viewports } = viewportGridService.getState();
@ -1327,6 +1358,12 @@ function commandsModule({
measurementService.remove(activeAnnotationUID);
});
},
undo: () => {
DefaultHistoryMemo.undo();
},
redo: () => {
DefaultHistoryMemo.redo();
},
};
const definitions = {
@ -1391,6 +1428,9 @@ function commandsModule({
setWindowLevel: {
commandFn: actions.setWindowLevel,
},
setWindowLevelPreset: {
commandFn: actions.setWindowLevelPreset,
},
setToolActive: {
commandFn: actions.setToolActive,
},
@ -1590,6 +1630,8 @@ function commandsModule({
deleteActiveAnnotation: {
commandFn: actions.deleteActiveAnnotation,
},
undo: actions.undo,
redo: actions.redo,
};
return {

View File

@ -199,7 +199,7 @@ function ViewerLayout({
</ResizablePanelGroup>
</React.Fragment>
</div>
<Onboarding />
<Onboarding tours={customizationService.getCustomization('ohif.tours')} />
<InvestigationalUseDialog dialogConfiguration={appConfig?.investigationalUseDialog} />
</div>
);

View File

@ -0,0 +1,5 @@
import { defaults } from '@ohif/core';
export default {
'ohif.hotkeyBindings': defaults.hotkeyBindings,
};

View File

@ -0,0 +1,210 @@
function waitForElement(selector, maxAttempts = 20, interval = 25) {
return new Promise(resolve => {
let attempts = 0;
const checkForElement = setInterval(() => {
const element = document.querySelector(selector);
if (element || attempts >= maxAttempts) {
clearInterval(checkForElement);
resolve();
}
attempts++;
}, interval);
});
}
export default {
'ohif.tours': [
{
id: 'basicViewerTour',
route: '/viewer',
steps: [
{
id: 'scroll',
title: 'Scrolling Through Images',
text: 'You can scroll through the images using the mouse wheel or scrollbar.',
attachTo: {
element: '.viewport-element',
on: 'top',
},
advanceOn: {
selector: '.cornerstone-viewport-element',
event: 'CORNERSTONE_TOOLS_MOUSE_WHEEL',
},
beforeShowPromise: () => waitForElement('.viewport-element'),
},
{
id: 'zoom',
title: 'Zooming In and Out',
text: 'You can zoom the images using the right click.',
attachTo: {
element: '.viewport-element',
on: 'left',
},
advanceOn: {
selector: '.cornerstone-viewport-element',
event: 'CORNERSTONE_TOOLS_MOUSE_UP',
},
beforeShowPromise: () => waitForElement('.viewport-element'),
},
{
id: 'pan',
title: 'Panning the Image',
text: 'You can pan the images using the middle click.',
attachTo: {
element: '.viewport-element',
on: 'top',
},
advanceOn: {
selector: '.cornerstone-viewport-element',
event: 'CORNERSTONE_TOOLS_MOUSE_UP',
},
beforeShowPromise: () => waitForElement('.viewport-element'),
},
{
id: 'windowing',
title: 'Adjusting Window Level',
text: 'You can modify the window level using the left click.',
attachTo: {
element: '.viewport-element',
on: 'left',
},
advanceOn: {
selector: '.cornerstone-viewport-element',
event: 'CORNERSTONE_TOOLS_MOUSE_UP',
},
beforeShowPromise: () => waitForElement('.viewport-element'),
},
{
id: 'length',
title: 'Using the Measurement Tools',
text: 'You can measure the length of a region using the Length tool.',
attachTo: {
element: '[data-cy="MeasurementTools-split-button-primary"]',
on: 'bottom',
},
advanceOn: {
selector: '[data-cy="MeasurementTools-split-button-primary"]',
event: 'click',
},
beforeShowPromise: () =>
waitForElement('[data-cy="MeasurementTools-split-button-primary]'),
},
{
id: 'drawAnnotation',
title: 'Drawing Length Annotations',
text: 'Use the length tool on the viewport to measure the length of a region.',
attachTo: {
element: '.viewport-element',
on: 'right',
},
advanceOn: {
selector: 'body',
event: 'event::measurement_added',
},
beforeShowPromise: () => waitForElement('.viewport-element'),
},
{
id: 'trackMeasurement',
title: 'Tracking Measurements in the Panel',
text: 'Click yes to track the measurements in the measurement panel.',
attachTo: {
element: '[data-cy="prompt-begin-tracking-yes-btn"]',
on: 'bottom',
},
advanceOn: {
selector: '[data-cy="prompt-begin-tracking-yes-btn"]',
event: 'click',
},
beforeShowPromise: () => waitForElement('[data-cy="prompt-begin-tracking-yes-btn"]'),
},
{
id: 'openMeasurementPanel',
title: 'Opening the Measurements Panel',
text: 'Click the measurements button to open the measurements panel.',
attachTo: {
element: '#trackedMeasurements-btn',
on: 'left-start',
},
advanceOn: {
selector: '#trackedMeasurements-btn',
event: 'click',
},
beforeShowPromise: () => waitForElement('#trackedMeasurements-btn'),
},
{
id: 'scrollAwayFromMeasurement',
title: 'Scrolling Away from a Measurement',
text: 'Scroll the images using the mouse wheel away from the measurement.',
attachTo: {
element: '.viewport-element',
on: 'left',
},
advanceOn: {
selector: '.cornerstone-viewport-element',
event: 'CORNERSTONE_TOOLS_MOUSE_WHEEL',
},
beforeShowPromise: () => waitForElement('.viewport-element'),
},
{
id: 'jumpToMeasurement',
title: 'Jumping to Measurements in the Panel',
text: 'Click the measurement in the measurement panel to jump to it.',
attachTo: {
element: '[data-cy="data-row"]',
on: 'left-start',
},
advanceOn: {
selector: '[data-cy="data-row"]',
event: 'click',
},
beforeShowPromise: () => waitForElement('[data-cy="data-row"]'),
},
{
id: 'changeLayout',
title: 'Changing Layout',
text: 'You can change the layout of the viewer using the layout button.',
attachTo: {
element: '[data-cy="Layout"]',
on: 'bottom',
},
advanceOn: {
selector: '[data-cy="Layout"]',
event: 'click',
},
beforeShowPromise: () => waitForElement('[data-cy="Layout"]'),
},
{
id: 'selectLayout',
title: 'Selecting the MPR Layout',
text: 'Select the MPR layout to view the images in MPR mode.',
attachTo: {
element: '[data-cy="MPR"]',
on: 'left-start',
},
advanceOn: {
selector: '[data-cy="MPR"]',
event: 'click',
},
beforeShowPromise: () => waitForElement('[data-cy="MPR"]'),
},
],
tourOptions: {
useModalOverlay: true,
defaultStepOptions: {
buttons: [
{
text: 'Skip all',
action() {
this.complete();
},
secondary: true,
},
],
},
},
},
],
};

View File

@ -18,7 +18,8 @@ import progressLoadingBarCustomization from './customizations/progressLoadingBar
import viewportActionCornersCustomization from './customizations/viewportActionCornersCustomization';
import labellingFlowCustomization from './customizations/labellingFlowCustomization';
import viewportNotificationCustomization from './customizations/notificationCustomization';
import hotkeyBindingsCustomization from './customizations/hotkeyBindingsCustomization';
import onboardingCustomization from './customizations/onboardingCustomization';
/**
*
* Note: this is an example of how the customization module can be used
@ -62,6 +63,8 @@ export default function getCustomizationModule({ servicesManager, extensionManag
...labellingFlowCustomization,
...contextMenuUICustomization,
...viewportNotificationCustomization,
...hotkeyBindingsCustomization,
...onboardingCustomization,
},
},
];

View File

@ -171,7 +171,6 @@ function modeFactory({ modeConfiguration }) {
dicompdf.sopClassHandler,
dicomsr.sopClassHandler,
],
hotkeys: [...hotkeys.defaults.hotkeyBindings],
};
}

View File

@ -102,6 +102,22 @@ function modeFactory() {
'Crosshairs',
'MoreTools',
]);
customizationService.setCustomizations(
{
'ohif.hotkeyBindings': {
$push: [
{
commandName: 'undo',
label: 'Undo',
keys: ['ctrl+z'],
isEditable: true,
},
],
},
},
'mode'
);
},
onModeExit: ({ servicesManager }: withAppTypes) => {
const {
@ -209,7 +225,6 @@ function modeFactory() {
// Don't store the hotkeys for basic-test-mode under the same key
// because they get customized by tests
name: 'basic-test-hotkeys',
hotkeys: [...hotkeys.defaults.hotkeyBindings],
},
};
}

View File

@ -84,7 +84,8 @@ function modeFactory({ modeConfiguration }) {
* Lifecycle hooks
*/
onModeEnter: function ({ servicesManager, extensionManager, commandsManager }: withAppTypes) {
const { measurementService, toolbarService, toolGroupService } = servicesManager.services;
const { measurementService, toolbarService, toolGroupService, customizationService } =
servicesManager.services;
measurementService.clearMeasurements();
@ -238,7 +239,6 @@ function modeFactory({ modeConfiguration }) {
dicomsr.sopClassHandler,
dicomRT.sopClassHandler,
],
hotkeys: [...hotkeys.defaults.hotkeyBindings],
...modeConfiguration,
};
}

View File

@ -205,6 +205,32 @@ const toolbarButtons: Button[] = [
},
},
},
// {
// id: 'Undo',
// uiType: 'ohif.toolButton',
// props: {
// type: 'tool',
// icon: 'prev-arrow',
// label: 'Undo',
// commands: {
// commandName: 'undo',
// },
// evaluate: 'evaluate.action',
// },
// },
// {
// id: 'Redo',
// uiType: 'ohif.toolButton',
// props: {
// type: 'tool',
// icon: 'next-arrow',
// label: 'Redo',
// commands: {
// commandName: 'redo',
// },
// evaluate: 'evaluate.action',
// },
// },
];
export default toolbarButtons;

View File

@ -123,7 +123,6 @@ function modeFactory({ modeConfiguration }) {
dicomvideo.sopClassHandler,
dicompdf.sopClassHandler,
],
hotkeys: [...hotkeys.defaults.hotkeyBindings],
...modeConfiguration,
};
}

View File

@ -167,7 +167,6 @@ function modeFactory({ modeConfiguration }) {
// general handler needs to come last. For this case, the dicomvideo must
// come first to remove video transfer syntax before ohif uses images
sopClassHandlers: [ohif.chartSopClassHandler, ohif.defaultSopClassHandler],
hotkeys: [...hotkeys.defaults.hotkeyBindings],
};
}

View File

@ -1,4 +1,3 @@
import { hotkeys } from '@ohif/core';
import { id } from './id';
import toolbarButtons from './toolbarButtons';
import segmentationButtons from './segmentationButtons';
@ -49,7 +48,8 @@ function modeFactory({ modeConfiguration }) {
* Services and other resources.
*/
onModeEnter: ({ servicesManager, extensionManager, commandsManager }: withAppTypes) => {
const { measurementService, toolbarService, toolGroupService } = servicesManager.services;
const { measurementService, toolbarService, toolGroupService, customizationService } =
servicesManager.services;
measurementService.clearMeasurements();
@ -158,8 +158,6 @@ function modeFactory({ modeConfiguration }) {
// hangingProtocol: ['@ohif/mnGrid'],
/** SopClassHandlers used by the mode */
sopClassHandlers: [ohif.sopClassHandler, segmentation.sopClassHandler],
/** hotkeys for mode */
hotkeys: [...hotkeys.defaults.hotkeyBindings],
};
}

View File

@ -216,7 +216,6 @@ function modeFactory({ modeConfiguration }) {
extensions: extensionDependencies,
hangingProtocol: tmtv.hangingProtocol,
sopClassHandlers: [ohif.sopClassHandler],
hotkeys: [...hotkeys.defaults.hotkeyBindings],
...modeConfiguration,
};
}

View File

@ -57,92 +57,4 @@ window.config = {
// Could use services manager here to bring up a dialog/modal if needed.
console.warn('test, navigate to https://ohif.org/');
},
hotkeys: [
{
commandName: 'incrementActiveViewport',
label: 'Next Viewport',
keys: ['right'],
},
{
commandName: 'decrementActiveViewport',
label: 'Previous Viewport',
keys: ['left'],
},
{ commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'] },
{ commandName: 'rotateViewportCCW', label: 'Rotate Left', keys: ['l'] },
{ commandName: 'invertViewport', label: 'Invert', keys: ['i'] },
{
commandName: 'flipViewportHorizontal',
label: 'Flip Horizontally',
keys: ['h'],
},
{
commandName: 'flipViewportVertical',
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'],
// },
{ 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'],
},
],
};

View File

@ -293,305 +293,4 @@ window.config = {
// ))
// },
// },
hotkeys: [
{
commandName: 'incrementActiveViewport',
label: 'Next Viewport',
keys: ['right'],
},
{
commandName: 'decrementActiveViewport',
label: 'Previous Viewport',
keys: ['left'],
},
{ commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'] },
{ commandName: 'rotateViewportCCW', label: 'Rotate Left', keys: ['l'] },
{ commandName: 'invertViewport', label: 'Invert', keys: ['i'] },
{
commandName: 'flipViewportHorizontal',
label: 'Flip Horizontally',
keys: ['h'],
},
{
commandName: 'flipViewportVertical',
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'],
// },
{
commandName: 'setToolActive',
commandOptions: { toolName: 'Zoom' },
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'],
},
],
tours: [
{
id: 'basicViewerTour',
route: '/viewer',
steps: [
{
id: 'scroll',
title: 'Scrolling Through Images',
text: 'You can scroll through the images using the mouse wheel or scrollbar.',
attachTo: {
element: '.viewport-element',
on: 'top',
},
advanceOn: {
selector: '.cornerstone-viewport-element',
event: 'CORNERSTONE_TOOLS_MOUSE_WHEEL',
},
beforeShowPromise: () => waitForElement('.viewport-element'),
},
{
id: 'zoom',
title: 'Zooming In and Out',
text: 'You can zoom the images using the right click.',
attachTo: {
element: '.viewport-element',
on: 'left',
},
advanceOn: {
selector: '.cornerstone-viewport-element',
event: 'CORNERSTONE_TOOLS_MOUSE_UP',
},
beforeShowPromise: () => waitForElement('.viewport-element'),
},
{
id: 'pan',
title: 'Panning the Image',
text: 'You can pan the images using the middle click.',
attachTo: {
element: '.viewport-element',
on: 'top',
},
advanceOn: {
selector: '.cornerstone-viewport-element',
event: 'CORNERSTONE_TOOLS_MOUSE_UP',
},
beforeShowPromise: () => waitForElement('.viewport-element'),
},
{
id: 'windowing',
title: 'Adjusting Window Level',
text: 'You can modify the window level using the left click.',
attachTo: {
element: '.viewport-element',
on: 'left',
},
advanceOn: {
selector: '.cornerstone-viewport-element',
event: 'CORNERSTONE_TOOLS_MOUSE_UP',
},
beforeShowPromise: () => waitForElement('.viewport-element'),
},
{
id: 'length',
title: 'Using the Measurement Tools',
text: 'You can measure the length of a region using the Length tool.',
attachTo: {
element: '[data-cy="MeasurementTools-split-button-primary"]',
on: 'bottom',
},
advanceOn: {
selector: '[data-cy="MeasurementTools-split-button-primary"]',
event: 'click',
},
beforeShowPromise: () =>
waitForElement('[data-cy="MeasurementTools-split-button-primary]'),
},
{
id: 'drawAnnotation',
title: 'Drawing Length Annotations',
text: 'Use the length tool on the viewport to measure the length of a region.',
attachTo: {
element: '.viewport-element',
on: 'right',
},
advanceOn: {
selector: 'body',
event: 'event::measurement_added',
},
beforeShowPromise: () => waitForElement('.viewport-element'),
},
{
id: 'trackMeasurement',
title: 'Tracking Measurements in the Panel',
text: 'Click yes to track the measurements in the measurement panel.',
attachTo: {
element: '[data-cy="prompt-begin-tracking-yes-btn"]',
on: 'bottom',
},
advanceOn: {
selector: '[data-cy="prompt-begin-tracking-yes-btn"]',
event: 'click',
},
beforeShowPromise: () => waitForElement('[data-cy="prompt-begin-tracking-yes-btn"]'),
},
{
id: 'openMeasurementPanel',
title: 'Opening the Measurements Panel',
text: 'Click the measurements button to open the measurements panel.',
attachTo: {
element: '#trackedMeasurements-btn',
on: 'left-start',
},
advanceOn: {
selector: '#trackedMeasurements-btn',
event: 'click',
},
beforeShowPromise: () => waitForElement('#trackedMeasurements-btn'),
},
{
id: 'scrollAwayFromMeasurement',
title: 'Scrolling Away from a Measurement',
text: 'Scroll the images using the mouse wheel away from the measurement.',
attachTo: {
element: '.viewport-element',
on: 'left',
},
advanceOn: {
selector: '.cornerstone-viewport-element',
event: 'CORNERSTONE_TOOLS_MOUSE_WHEEL',
},
beforeShowPromise: () => waitForElement('.viewport-element'),
},
{
id: 'jumpToMeasurement',
title: 'Jumping to Measurements in the Panel',
text: 'Click the measurement in the measurement panel to jump to it.',
attachTo: {
element: '[data-cy="data-row"]',
on: 'left-start',
},
advanceOn: {
selector: '[data-cy="data-row"]',
event: 'click',
},
beforeShowPromise: () => waitForElement('[data-cy="data-row"]'),
},
{
id: 'changeLayout',
title: 'Changing Layout',
text: 'You can change the layout of the viewer using the layout button.',
attachTo: {
element: '[data-cy="Layout"]',
on: 'bottom',
},
advanceOn: {
selector: '[data-cy="Layout"]',
event: 'click',
},
beforeShowPromise: () => waitForElement('[data-cy="Layout"]'),
},
{
id: 'selectLayout',
title: 'Selecting the MPR Layout',
text: 'Select the MPR layout to view the images in MPR mode.',
attachTo: {
element: '[data-cy="MPR"]',
on: 'left-start',
},
advanceOn: {
selector: '[data-cy="MPR"]',
event: 'click',
},
beforeShowPromise: () => waitForElement('[data-cy="MPR"]'),
},
],
tourOptions: {
useModalOverlay: true,
defaultStepOptions: {
buttons: [
{
text: 'Skip all',
action() {
this.complete();
},
secondary: true,
},
],
},
},
},
],
};
function waitForElement(selector, maxAttempts = 20, interval = 25) {
return new Promise(resolve => {
let attempts = 0;
const checkForElement = setInterval(() => {
const element = document.querySelector(selector);
if (element || attempts >= maxAttempts) {
clearInterval(checkForElement);
resolve();
}
attempts++;
}, interval);
});
}

View File

@ -93,97 +93,4 @@ window.config = {
// },
// },
defaultDataSourceName: 'dicomweb',
hotkeys: [
{
commandName: 'incrementActiveViewport',
label: 'Next Viewport',
keys: ['right'],
},
{
commandName: 'decrementActiveViewport',
label: 'Previous Viewport',
keys: ['left'],
},
{ commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'] },
{ commandName: 'rotateViewportCCW', label: 'Rotate Left', keys: ['l'] },
{ commandName: 'invertViewport', label: 'Invert', keys: ['i'] },
{
commandName: 'flipViewportHorizontal',
label: 'Flip Horizontally',
keys: ['h'],
},
{
commandName: 'flipViewportVertical',
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'],
// },
{
commandName: 'setToolActive',
commandOptions: { toolName: 'Zoom' },
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'],
},
],
};

View File

@ -29,94 +29,6 @@ window.config = {
},
},
],
hotkeys: [
{
commandName: 'incrementActiveViewport',
label: 'Next Viewport',
keys: ['right'],
},
{
commandName: 'decrementActiveViewport',
label: 'Previous Viewport',
keys: ['left'],
},
{ commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'] },
{ commandName: 'rotateViewportCCW', label: 'Rotate Left', keys: ['l'] },
{ commandName: 'invertViewport', label: 'Invert', keys: ['i'] },
{
commandName: 'flipViewportHorizontal',
label: 'Flip Horizontally',
keys: ['h'],
},
{
commandName: 'flipViewportVertical',
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'],
// },
{ 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'],
},
],
i18n: {
LOCIZE_PROJECTID: 'a8da3f9a-e467-4dd6-af33-474d582a0294',
LOCIZE_API_KEY: null, // Developers can use this to do in-context editing. DO NOT COMMIT THIS KEY!

View File

@ -56,92 +56,4 @@ window.config = {
// Could use services manager here to bring up a dialog/modal if needed.
console.warn('test, navigate to https://ohif.org/');
},
hotkeys: [
{
commandName: 'incrementActiveViewport',
label: 'Next Viewport',
keys: ['right'],
},
{
commandName: 'decrementActiveViewport',
label: 'Previous Viewport',
keys: ['left'],
},
{ 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'],
// },
{ 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'],
},
],
};

View File

@ -31,23 +31,3 @@ window.config = {
],
studyListFunctionsEnabled: true,
};
// window.config = function(props) {
// var servicesManager = props.servicesManager;
// return {
// routerBasename: '/',
// enableGoogleCloudAdapter: true,
// enableGoogleCloudAdapterUI: false,
// showStudyList: true,
// httpErrorHandler: error => {
// // This is 429 when rejected from the public idc sandbox too often.
// console.warn(error.status);
// // Could use services manager here to bring up a dialog/modal if needed.
// console.warn('test, navigate to https://ohif.org/');
// window.location = 'https://ohif.org/';
// },
// healthcareApiEndpoint: 'https://idc-sandbox-002.appspot.com/v1beta1',
// };
// };

View File

@ -199,97 +199,4 @@ window.config = {
// ))
// },
// },
hotkeys: [
{
commandName: 'incrementActiveViewport',
label: 'Next Viewport',
keys: ['right'],
},
{
commandName: 'decrementActiveViewport',
label: 'Previous Viewport',
keys: ['left'],
},
{ commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'] },
{ commandName: 'rotateViewportCCW', label: 'Rotate Left', keys: ['l'] },
{ commandName: 'invertViewport', label: 'Invert', keys: ['i'] },
{
commandName: 'flipViewportHorizontal',
label: 'Flip Horizontally',
keys: ['h'],
},
{
commandName: 'flipViewportVertical',
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'],
// },
{
commandName: 'setToolActive',
commandOptions: { toolName: 'Zoom' },
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'],
},
],
};

View File

@ -69,92 +69,4 @@ window.config = {
// Could use services manager here to bring up a dialog/modal if needed.
console.warn('test, navigate to https://ohif.org/');
},
hotkeys: [
{
commandName: 'incrementActiveViewport',
label: 'Next Viewport',
keys: ['right'],
},
{
commandName: 'decrementActiveViewport',
label: 'Previous Viewport',
keys: ['left'],
},
{ 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'],
},
{ 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'],
},
],
};

View File

@ -131,92 +131,4 @@ window.config = {
// Could use services manager here to bring up a dialog/modal if needed.
console.warn('test, navigate to https://ohif.org/');
},
hotkeys: [
{
commandName: 'incrementActiveViewport',
label: 'Next Viewport',
keys: ['right'],
},
{
commandName: 'decrementActiveViewport',
label: 'Previous Viewport',
keys: ['left'],
},
{ commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'] },
{ commandName: 'rotateViewportCCW', label: 'Rotate Left', keys: ['l'] },
{ commandName: 'invertViewport', label: 'Invert', keys: ['i'] },
{
commandName: 'flipViewportHorizontal',
label: 'Flip Horizontally',
keys: ['h'],
},
{
commandName: 'flipViewportVertical',
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'],
// },
{ 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'],
},
],
};

View File

@ -158,285 +158,6 @@ window.config = {
// Could use services manager here to bring up a dialog/modal if needed.
console.warn('test, navigate to https://ohif.org/');
},
hotkeys: [
{
commandName: 'incrementActiveViewport',
label: 'Next Viewport',
keys: ['right'],
},
{
commandName: 'decrementActiveViewport',
label: 'Previous Viewport',
keys: ['left'],
},
{ commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'] },
{ commandName: 'rotateViewportCCW', label: 'Rotate Left', keys: ['l'] },
{ commandName: 'invertViewport', label: 'Invert', keys: ['i'] },
{
commandName: 'flipViewportHorizontal',
label: 'Flip Horizontally',
keys: ['h'],
},
{
commandName: 'flipViewportVertical',
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'],
// },
{ 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'],
},
],
tours: [
{
id: 'basicViewerTour',
route: '/viewer',
steps: [
{
id: 'scroll',
title: 'Scrolling Through Images',
text: 'You can scroll through the images using the mouse wheel or scrollbar.',
attachTo: {
element: '.viewport-element',
on: 'top',
},
advanceOn: {
selector: '.cornerstone-viewport-element',
event: 'CORNERSTONE_TOOLS_MOUSE_WHEEL',
},
beforeShowPromise: () => waitForElement('.viewport-element'),
},
{
id: 'zoom',
title: 'Zooming In and Out',
text: 'You can zoom the images using the right click.',
attachTo: {
element: '.viewport-element',
on: 'left',
},
advanceOn: {
selector: '.cornerstone-viewport-element',
event: 'CORNERSTONE_TOOLS_MOUSE_UP',
},
beforeShowPromise: () => waitForElement('.viewport-element'),
},
{
id: 'pan',
title: 'Panning the Image',
text: 'You can pan the images using the middle click.',
attachTo: {
element: '.viewport-element',
on: 'top',
},
advanceOn: {
selector: '.cornerstone-viewport-element',
event: 'CORNERSTONE_TOOLS_MOUSE_UP',
},
beforeShowPromise: () => waitForElement('.viewport-element'),
},
{
id: 'windowing',
title: 'Adjusting Window Level',
text: 'You can modify the window level using the left click.',
attachTo: {
element: '.viewport-element',
on: 'left',
},
advanceOn: {
selector: '.cornerstone-viewport-element',
event: 'CORNERSTONE_TOOLS_MOUSE_UP',
},
beforeShowPromise: () => waitForElement('.viewport-element'),
},
{
id: 'length',
title: 'Using the Measurement Tools',
text: 'You can measure the length of a region using the Length tool.',
attachTo: {
element: '[data-cy="MeasurementTools-split-button-primary"]',
on: 'bottom',
},
advanceOn: {
selector: '[data-cy="MeasurementTools-split-button-primary"]',
event: 'click',
},
beforeShowPromise: () =>
waitForElement('[data-cy="MeasurementTools-split-button-primary]'),
},
{
id: 'drawAnnotation',
title: 'Drawing Length Annotations',
text: 'Use the length tool on the viewport to measure the length of a region.',
attachTo: {
element: '.viewport-element',
on: 'right',
},
advanceOn: {
selector: 'body',
event: 'event::measurement_added',
},
beforeShowPromise: () => waitForElement('.viewport-element'),
},
{
id: 'trackMeasurement',
title: 'Tracking Measurements in the Panel',
text: 'Click yes to track the measurements in the measurement panel.',
attachTo: {
element: '[data-cy="prompt-begin-tracking-yes-btn"]',
on: 'bottom',
},
advanceOn: {
selector: '[data-cy="prompt-begin-tracking-yes-btn"]',
event: 'click',
},
beforeShowPromise: () => waitForElement('[data-cy="prompt-begin-tracking-yes-btn"]'),
},
{
id: 'openMeasurementPanel',
title: 'Opening the Measurements Panel',
text: 'Click the measurements button to open the measurements panel.',
attachTo: {
element: '#trackedMeasurements-btn',
on: 'left-start',
},
advanceOn: {
selector: '#trackedMeasurements-btn',
event: 'click',
},
beforeShowPromise: () => waitForElement('#trackedMeasurements-btn'),
},
{
id: 'scrollAwayFromMeasurement',
title: 'Scrolling Away from a Measurement',
text: 'Scroll the images using the mouse wheel away from the measurement.',
attachTo: {
element: '.viewport-element',
on: 'left',
},
advanceOn: {
selector: '.cornerstone-viewport-element',
event: 'CORNERSTONE_TOOLS_MOUSE_WHEEL',
},
beforeShowPromise: () => waitForElement('.viewport-element'),
},
{
id: 'jumpToMeasurement',
title: 'Jumping to Measurements in the Panel',
text: 'Click the measurement in the measurement panel to jump to it.',
attachTo: {
element: '[data-cy="data-row"]',
on: 'left-start',
},
advanceOn: {
selector: '[data-cy="data-row"]',
event: 'click',
},
beforeShowPromise: () => waitForElement('[data-cy="data-row"]'),
},
{
id: 'changeLayout',
title: 'Changing Layout',
text: 'You can change the layout of the viewer using the layout button.',
attachTo: {
element: '[data-cy="Layout"]',
on: 'bottom',
},
advanceOn: {
selector: '[data-cy="Layout"]',
event: 'click',
},
beforeShowPromise: () => waitForElement('[data-cy="Layout"]'),
},
{
id: 'selectLayout',
title: 'Selecting the MPR Layout',
text: 'Select the MPR layout to view the images in MPR mode.',
attachTo: {
element: '[data-cy="MPR"]',
on: 'left-start',
},
advanceOn: {
selector: '[data-cy="MPR"]',
event: 'click',
},
beforeShowPromise: () => waitForElement('[data-cy="MPR"]'),
},
],
tourOptions: {
useModalOverlay: true,
defaultStepOptions: {
buttons: [
{
text: 'Skip all',
action() {
this.complete();
},
secondary: true,
},
],
},
},
},
],
};
function waitForElement(selector, maxAttempts = 20, interval = 25) {

View File

@ -26,92 +26,4 @@ window.config = {
LOCIZE_API_KEY: null, // Developers can use this to do in-context editing. DO NOT COMMIT THIS KEY!
USE_LOCIZE: false,
},
hotkeys: [
{
commandName: 'incrementActiveViewport',
label: 'Next Viewport',
keys: ['right'],
},
{
commandName: 'decrementActiveViewport',
label: 'Previous Viewport',
keys: ['left'],
},
{ commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'] },
{ commandName: 'rotateViewportCCW', label: 'Rotate Left', keys: ['l'] },
{ commandName: 'invertViewport', label: 'Invert', keys: ['i'] },
{
commandName: 'flipViewportHorizontal',
label: 'Flip Horizontally',
keys: ['h'],
},
{
commandName: 'flipViewportVertical',
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'],
// },
{ 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'],
},
],
};

View File

@ -60,10 +60,15 @@ export default function ModeRoute({
locationRef.current = location;
}
const { displaySetService, panelService, hangingProtocolService, userAuthenticationService } =
servicesManager.services;
const {
displaySetService,
panelService,
hangingProtocolService,
userAuthenticationService,
customizationService,
} = servicesManager.services;
const { extensions, sopClassHandlers, hotkeys: hotkeyObj, hangingProtocol } = mode;
const { extensions, sopClassHandlers, hangingProtocol } = mode;
const runTimeHangingProtocolId = lowerCaseSearchParams.get('hangingprotocolid');
const runTimeStageId = lowerCaseSearchParams.get('stageid');
@ -73,10 +78,6 @@ export default function ModeRoute({
updateAuthServiceAndCleanUrl(token, location, userAuthenticationService);
}
// Preserve the old array interface for hotkeys
const hotkeys = Array.isArray(hotkeyObj) ? hotkeyObj : hotkeyObj?.hotkeys;
const hotkeyName = hotkeyObj?.name || 'hotkey-definitions';
// An undefined dataSourceName implies that the active data source that is already set in the ExtensionManager should be used.
if (dataSourceName !== undefined) {
extensionManager.setActiveDataSource(dataSourceName);
@ -167,26 +168,6 @@ export default function ModeRoute({
};
}, [studyInstanceUIDs, ExtensionDependenciesLoaded]);
useEffect(() => {
if (!hotkeys || !ExtensionDependenciesLoaded || !studyInstanceUIDs?.length) {
return;
}
hotkeysManager.setDefaultHotKeys(hotkeys);
const userPreferredHotkeys = JSON.parse(localStorage.getItem(hotkeyName));
if (userPreferredHotkeys?.length) {
hotkeysManager.setHotkeys(userPreferredHotkeys, hotkeyName);
} else {
hotkeysManager.setHotkeys(hotkeys, hotkeyName);
}
return () => {
hotkeysManager.destroy();
};
}, [ExtensionDependenciesLoaded, hotkeys, studyInstanceUIDs]);
useEffect(() => {
if (!layoutTemplateData.current || !ExtensionDependenciesLoaded || !studyInstanceUIDs?.length) {
return;
@ -239,6 +220,10 @@ export default function ModeRoute({
appConfig,
});
// Move hotkeys setup here, after onModeEnter
const hotkeys = customizationService.getCustomization('ohif.hotkeyBindings');
hotkeysManager.setDefaultHotKeys(hotkeys);
/**
* The next line should get all the query parameters provided by the URL
* - except the StudyInstanceUIDs - and create an object called filters
@ -298,8 +283,6 @@ export default function ModeRoute({
setupRouteInit().then(unsubs => {
unsubscriptions = unsubs;
// Some code may need to run after hanging protocol initialization
// (eg: workflowStepsService initialization on 4D mode)
mode?.onSetupRouteComplete?.({
servicesManager,
extensionManager,
@ -320,6 +303,9 @@ export default function ModeRoute({
} catch (e) {
console.warn('mode exit failure', e);
}
// Clean up hotkeys
hotkeysManager.destroy();
// The unsubscriptions must occur before the extension onModeExit
// in order to prevent exceptions during cleanup caused by spurious events
if (unsubscriptions) {

View File

@ -128,7 +128,6 @@ function modeFactory({ modeConfiguration }) {
/** SopClassHandlers used by the mode */
sopClassHandlers: [ohif.sopClassHandler],
/** hotkeys for mode */
hotkeys: [...hotkeys.defaults.hotkeyBindings],
};
}

View File

@ -30,14 +30,6 @@ describe('HotkeysManager', () => {
expect(containsAllExpectedProperties).toBe(true);
});
it('throws Error if instantiated without a commandsManager', () => {
expect(() => {
new HotkeysManager();
}).toThrow(
'HotkeysManager instantiated without a commandsManager. Hotkeys will be unable to find and run commands.'
);
});
describe('disable()', () => {
beforeEach(() => hotkeys.pause.mockClear());

View File

@ -1,7 +1,7 @@
import objectHash from 'object-hash';
import isEqual from 'lodash.isequal';
import { hotkeys } from '../utils';
import { hotkeys as mouseTrapAPI } from '../utils';
import Hotkey from './Hotkey';
import migrateOldHotkeyDefinitions from '../utils/hotkeys/migrateHotkeys';
/**
*
@ -12,23 +12,24 @@ import Hotkey from './Hotkey';
* @property {String} label - Display name for hotkey
* @property {String[]} keys - Keys to bind; Follows Mousetrap.js binding syntax
*/
export class HotkeysManager {
private _servicesManager: AppTypes.ServicesManager;
private _commandsManager: AppTypes.CommandsManager;
private isEnabled: boolean = true;
public hotkeyDefinitions: Record<string, any> = {};
public hotkeyDefaults: any[] = [];
constructor(commandsManager, servicesManager: AppTypes.ServicesManager) {
this.hotkeyDefinitions = {};
this.hotkeyDefaults = [];
this.isEnabled = true;
if (!commandsManager) {
throw new Error(
'HotkeysManager instantiated without a commandsManager. Hotkeys will be unable to find and run commands.'
);
}
constructor(
commandsManager: AppTypes.CommandsManager,
servicesManager: AppTypes.ServicesManager
) {
this._servicesManager = servicesManager;
this._commandsManager = commandsManager;
// Check for old hotkey definitions format and migrate if needed
migrateOldHotkeyDefinitions({
generateHash: this.generateHash,
});
}
/**
@ -37,7 +38,12 @@ export class HotkeysManager {
* @param {*} event
*/
record(event) {
return hotkeys.record(event);
return mouseTrapAPI.record(event);
}
cancel() {
mouseTrapAPI.stopRecord();
mouseTrapAPI.unpause();
}
/**
@ -46,7 +52,7 @@ export class HotkeysManager {
*/
disable() {
this.isEnabled = false;
hotkeys.pause();
mouseTrapAPI.pause();
}
/**
@ -54,22 +60,35 @@ export class HotkeysManager {
*/
enable() {
this.isEnabled = true;
hotkeys.unpause();
mouseTrapAPI.unpause();
}
/**
* Registers a list of hotkeydefinitions.
* Uses most recent
*
* @returns {undefined}
*/
restoreDefaultBindings() {
this.setHotkeys(this.hotkeyDefaults);
}
/**
*
*/
destroy() {
this.hotkeyDefaults = [];
this.hotkeyDefinitions = {};
mouseTrapAPI.reset();
}
/**
* Registers a list of hotkey definitions.
*
* @param {HotkeyDefinition[] | Object} [hotkeyDefinitions=[]] Contains hotkeys definitions
*/
setHotkeys(hotkeyDefinitions = [], name = 'hotkey-definitions') {
setHotkeys(hotkeyDefinitions = []) {
try {
const definitions = this.getValidDefinitions(hotkeyDefinitions);
if (isEqual(definitions, this.hotkeyDefaults)) {
localStorage.removeItem(name);
} else {
localStorage.setItem(name, JSON.stringify(definitions));
}
definitions.forEach(definition => this.registerHotkeys(definition));
} catch (error) {
const { uiNotificationService } = this._servicesManager.services;
@ -81,6 +100,13 @@ export class HotkeysManager {
}
}
generateHash(definition) {
return objectHash({
commandName: definition.commandName,
commandOptions: definition.commandOptions || {},
});
}
/**
* Set default hotkey bindings. These
* values are used in `this.restoreDefaultBindings`.
@ -90,6 +116,25 @@ export class HotkeysManager {
setDefaultHotKeys(hotkeyDefinitions = []) {
const definitions = this.getValidDefinitions(hotkeyDefinitions);
this.hotkeyDefaults = definitions;
// Get user preferred keys from localStorage
const userPreferredKeys = JSON.parse(localStorage.getItem('user-preferred-keys') || '{}');
// Update definitions with user preferred keys before setting
const updatedDefinitions = definitions.map(definition => {
const commandHash = this.generateHash(definition);
// If user has a preferred key binding, use it
if (userPreferredKeys[commandHash]) {
return {
...definition,
keys: userPreferredKeys[commandHash],
};
}
return definition;
});
this.setHotkeys(updatedDefinitions);
}
/**
@ -103,6 +148,13 @@ export class HotkeysManager {
? [...hotkeyDefinitions]
: this._parseToArrayLike(hotkeyDefinitions);
// make sure isEditable is true for all definitions if not provided
definitions.forEach(definition => {
if (definition.isEditable === undefined) {
definition.isEditable = true;
}
});
return definitions;
}
@ -142,18 +194,6 @@ export class HotkeysManager {
* Return HotkeyDefinition object like based on given property name and property value
* @param {string} propertyName property name of hotkey definition object
* @param {object} propertyValue property value of hotkey definition object
*
* @example
*
* const hotKeyObj = {hotKeyDefA: {keys:[],....}}
*
* const parsed = _parseToHotKeyObj(Object.keys(hotKeyDefA)[0], hotKeyObj[hotKeyDefA]);
* {
* commandName: hotKeyDefA,
* keys: [],
* ....
* }
*
*/
_parseToHotKeyObj(propertyName, propertyValue) {
return {
@ -163,65 +203,44 @@ export class HotkeysManager {
}
/**
* (unbinds and) binds the specified command to one or more key combinations.
* When a hotkey combination is triggered, the command name and active contexts
* are used to locate the correct command to call.
* (Unbinds and) binds the specified command to one or more key combinations.
* When the hotkey combination is triggered, the command name and active contexts
* are used to locate and execute the appropriate command.
*
* @param {HotkeyDefinition} command
* @param {String} extension
* @returns {undefined}
* @param hotkey - The hotkey definition object.
* @throws {Error} Throws an error if no commandName is provided.
*/
registerHotkeys(
{ commandName, commandOptions = {}, context, keys, label, isEditable }: Hotkey = {},
extension
) {
registerHotkeys({
commandName,
commandOptions = {},
context,
keys,
label,
isEditable,
}: Hotkey): void {
if (!commandName) {
throw new Error(`No command was defined for hotkey "${keys}"`);
}
const commandHash = objectHash({ commandName, commandOptions });
const options = Object.keys(commandOptions).length ? JSON.stringify(commandOptions) : 'no';
const previouslyRegisteredDefinition = this.hotkeyDefinitions[commandHash];
const commandHash = this.generateHash({ commandName, commandOptions });
const existingHotkey = this.hotkeyDefinitions[commandHash];
if (previouslyRegisteredDefinition) {
const previouslyRegisteredKeys = previouslyRegisteredDefinition.keys;
this._unbindHotkeys(commandName, previouslyRegisteredKeys);
// log.info(
// `[hotkeys] Unbinding ${commandName} with ${options} options from ${previouslyRegisteredKeys}`
// );
// If the hotkey has already been registered with the same keys, skip re-registration.
if (existingHotkey && existingHotkey.keys === keys) {
console.debug('HotkeysManager: Identical hotkey registration skipped.');
return;
}
// Set definition & bind
this.hotkeyDefinitions[commandHash] = {
commandName,
commandOptions,
keys,
label,
isEditable,
};
const userPreferredKeys = JSON.parse(localStorage.getItem('user-preferred-keys') || '{}');
if (existingHotkey) {
userPreferredKeys[commandHash] = keys;
localStorage.setItem('user-preferred-keys', JSON.stringify(userPreferredKeys));
this._unbindHotkeys(commandName, existingHotkey.keys);
}
this.hotkeyDefinitions[commandHash] = { commandName, commandOptions, keys, label, isEditable };
this._bindHotkeys(commandName, commandOptions, context, keys);
// log.info(
// `[hotkeys] Binding ${commandName} with ${options} from ${context ||
// 'default'} options to ${keys}`
// );
}
/**
* Uses most recent
*
* @returns {undefined}
*/
restoreDefaultBindings() {
this.setHotkeys(this.hotkeyDefaults);
}
/**
*
*/
destroy() {
this.hotkeyDefaults = [];
this.hotkeyDefinitions = {};
hotkeys.reset();
}
/**
@ -241,7 +260,7 @@ export class HotkeysManager {
const isKeyArray = keys instanceof Array;
const combinedKeys = isKeyArray ? keys.join('+') : keys;
hotkeys.bind(combinedKeys, evt => {
mouseTrapAPI.bind(combinedKeys, evt => {
evt.preventDefault();
evt.stopPropagation();
this._commandsManager.runCommand(commandName, { evt, ...commandOptions }, context);
@ -269,7 +288,7 @@ export class HotkeysManager {
return;
}
hotkeys.unbind(keys);
mouseTrapAPI.unbind(keys);
}
}

View File

@ -1,8 +1,3 @@
import windowLevelPresets from './windowLevelPresets';
/*
* Supported Keys: https://craig.is/killing/mice
*/
const bindings = [
{
commandName: 'setToolActive',
@ -144,63 +139,46 @@ const bindings = [
keys: ['esc'],
},
{
commandName: 'setWindowLevel',
commandOptions: windowLevelPresets[1],
commandName: 'setWindowLevelPreset',
commandOptions: { presetName: 'ct-soft-tissue', presetIndex: 0 },
label: 'W/L Preset 1',
keys: ['1'],
},
{
commandName: 'setWindowLevel',
commandOptions: windowLevelPresets[2],
commandName: 'setWindowLevelPreset',
commandOptions: { presetName: 'ct-lung', presetIndex: 1 },
label: 'W/L Preset 2',
keys: ['2'],
},
{
commandName: 'setWindowLevel',
commandOptions: windowLevelPresets[3],
commandName: 'setWindowLevelPreset',
commandOptions: { presetName: 'ct-bone', presetIndex: 2 },
label: 'W/L Preset 3',
keys: ['3'],
},
{
commandName: 'setWindowLevel',
commandOptions: windowLevelPresets[4],
commandName: 'setWindowLevelPreset',
commandOptions: { presetName: 'ct-brain', presetIndex: 3 },
label: 'W/L Preset 4',
keys: ['4'],
},
{
commandName: 'setWindowLevel',
commandOptions: windowLevelPresets[5],
label: 'W/L Preset 5',
keys: ['5'],
},
{
commandName: 'deleteActiveAnnotation',
label: 'Delete Annotation',
keys: ['backspace'],
},
// These don't exist, so don't try applying them....
// after we have the ui for undo/redo, we can add these back in
// {
// commandName: 'setWindowLevel',
// commandOptions: windowLevelPresets[6],
// label: 'W/L Preset 6',
// keys: ['6'],
// commandName: 'undo',
// label: 'Undo',
// keys: ['ctrl+z'],
// isEditable: true,
// },
// {
// 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'],
// commandName: 'redo',
// label: 'Redo',
// keys: ['ctrl+y'],
// isEditable: true,
// },
];

View File

@ -1,4 +1,7 @@
import hotkeyBindings from './hotkeyBindings';
import windowLevelPresets from './windowLevelPresets';
export { hotkeyBindings, windowLevelPresets };
export default { hotkeyBindings, windowLevelPresets };
export default {
hotkeyBindings,
windowLevelPresets,
};

View File

@ -147,12 +147,6 @@ declare global {
maxNumPrefetchRequests: number;
order: 'closest' | 'downward' | 'upward';
};
tours?: Array<{
id: string;
steps: StepOptions[];
tourOptions: TourOptions;
route: string;
}>;
}
export interface Test {

View File

@ -0,0 +1,76 @@
import defaults from '../../defaults';
const defaultHotkeyBindings = defaults.hotkeyBindings;
/**
* Migrates old hotkey definitions from localStorage to the new format
* Old format: 'hotkey-definitions' containing full hotkey definitions array
* New format: 'user-preferred-keys' containing hashed command keys with their key bindings
*
* @private
*/
function migrateOldHotkeyDefinitions({
generateHash,
}: {
generateHash: (definition: Record<string, unknown>) => string;
}): void {
try {
const oldHotkeyDefinitions = localStorage.getItem('hotkey-definitions');
const migrated = localStorage.getItem('hotkeys-migrated');
if (!oldHotkeyDefinitions || migrated === 'true') {
return;
}
const oldDefinitions = JSON.parse(oldHotkeyDefinitions);
if (!Array.isArray(oldDefinitions) || oldDefinitions.length === 0) {
return;
}
const defaultBindings = defaultHotkeyBindings || [];
const userPreferredKeys = JSON.parse(localStorage.getItem('user-preferred-keys') || '{}');
oldDefinitions.forEach(oldDefinition => {
if (!oldDefinition.commandName || !oldDefinition.keys) {
return;
}
const matchingDefault = defaultBindings.find(defaultBinding => {
const sameCommand = defaultBinding.commandName === oldDefinition.commandName;
const oldOptions = oldDefinition.commandOptions || {};
const defaultOptions = defaultBinding.commandOptions || {};
const sameOptions = JSON.stringify(oldOptions) === JSON.stringify(defaultOptions);
return sameCommand && sameOptions;
});
if (
!matchingDefault ||
JSON.stringify(matchingDefault.keys) !== JSON.stringify(oldDefinition.keys)
) {
const commandHash = generateHash({
commandName: oldDefinition.commandName,
commandOptions: oldDefinition.commandOptions || {},
});
userPreferredKeys[commandHash] = oldDefinition.keys;
console.debug(`HotkeysManager: Migrated custom hotkey for ${oldDefinition.commandName}`);
}
});
localStorage.setItem('user-preferred-keys', JSON.stringify(userPreferredKeys));
localStorage.setItem('hotkeys-migrated', 'true');
localStorage.removeItem('hotkey-definitions');
console.debug('HotkeysManager: Successfully migrated hotkey definitions to new format');
} catch (error) {
console.error('HotkeysManager: Error migrating hotkey definitions', error);
localStorage.setItem('hotkeys-migrated', 'false');
}
}
export default migrateOldHotkeyDefinitions;

View File

@ -13,62 +13,39 @@ Tours allow you to provide step-by-step guidance to users, explaining different
### Adding a Tour to your Configuration
Heres an example of adding a tour to your configuration file:
Here's how you can add a tour to your configuration file:
```javascript
window.config = {
tours: [
{
id: 'basicViewerTour',
route: '/viewer',
steps: [
customizationService: {
'ohif.tours': {
$set: [
{
id: 'scroll',
title: 'Scrolling Through Images',
text: 'You can scroll through the images using the mouse wheel or scrollbar.',
attachTo: {
element: '.viewport-element',
on: 'top',
},
advanceOn: {
selector: '.cornerstone-viewport-element',
event: 'CORNERSTONE_TOOLS_MOUSE_WHEEL',
},
},
{
id: 'zoom',
title: 'Zooming In and Out',
text: 'You can zoom the images using the right click.',
attachTo: {
element: '.viewport-element',
on: 'left',
},
advanceOn: {
selector: '.cornerstone-viewport-element',
event: 'CORNERSTONE_TOOLS_MOUSE_UP',
},
},
// Add more steps as needed
],
tourOptions: {
useModalOverlay: true,
defaultStepOptions: {
buttons: [
id: 'basicViewerTour',
route: '/viewer',
steps: [
{
text: 'Skip all',
action() {
this.complete();
id: 'zoom',
title: 'Zooming In and Out',
text: 'You can zoom the images using the right click.',
attachTo: {
element: '.viewport-element',
on: 'left',
},
advanceOn: {
selector: '.cornerstone-viewport-element',
event: 'CORNERSTONE_TOOLS_MOUSE_UP',
},
secondary: true,
},
],
},
},
],
},
],
},
};
```
## Explanation of Parameters
### `tours` Array

View File

@ -0,0 +1,129 @@
---
sidebar_position: 2
title: Hotkeys
---
## Key Changes:
* Hotkeys are no longer defined in mode factory via `hotkeys: [...hotkeys.defaults.hotkeyBindings]`
* Hotkeys are now managed through the `customizationService` under the key `ohif.hotkeyBindings`
* Default hotkeys are set automatically and can be customized using the customization service
* User-defined hotkey preferences are now stored in a new format in localStorage
* The `HotkeysManager` has undergone significant updates including better handling of defaults, key persistence, and cleanup
## Migration Steps:
### 1. Remove hotkeys array from mode factory definition
**Before:**
```diff
- function modeFactory({ modeConfiguration }) {
- return {
- id: 'basic',
- // ... other configuration
- hotkeys: [...hotkeys.defaults.hotkeyBindings],
- };
- }
```
**After:**
```diff
+ function modeFactory({ modeConfiguration }) {
+ return {
+ id: 'basic',
+ // ... other configuration
+ // No hotkeys array necessary
+ };
+ }
```
### 2. Set custom hotkeys using the customization service
There are several methods to modify hotkeys using the customization service:
#### a. Completely replace all hotkeys using `$set`:
```diff
+ onModeEnter: function ({ servicesManager }) {
+ const { customizationService } = servicesManager.services;
+ customizationService.setCustomizations({
+ 'ohif.hotkeyBindings': {
+ $set: [
+ {
+ commandName: 'setToolActive',
+ commandOptions: { toolName: 'Zoom' },
+ label: 'Zoom',
+ keys: ['z'],
+ isEditable: true,
+ },
+ ],
+ },
+ });
```
#### b. Add new hotkeys using `$push`:
```diff
+ onModeEnter: function ({ servicesManager }) {
+ const { customizationService } = servicesManager.services;
+ customizationService.setCustomizations({
+ 'ohif.hotkeyBindings': {
+ $push: [
+ {
+ commandName: 'myCustomCommand',
+ label: 'My Custom Function',
+ keys: ['ctrl+m'],
+ isEditable: true,
+ },
+ ],
+ },
+ });
+}
```
### 4. Update configuration file if you were setting window.config.hotkeys
If you were previously defining hotkeys in your window.config.js file, it was not really
taken into account. So you can safely remove it now.
**Before:**
```diff
- window.config = {
- // ...other config
- hotkeys: [
- {
- commandName: 'incrementActiveViewport',
- label: 'Next Viewport',
- keys: ['right'],
- },
- // ...more hotkeys
- ],
- };
```
**After:**
```diff
+ window.config = {
+ // ...other config
+ };
```
### 5. Be aware that user preferences are now handled differently
The new system automatically handles user-preferred hotkey mappings:
- User hotkey preferences are stored in `localStorage` under the key `user-preferred-keys`
- The format is a hash-based mapping rather than a full array of definitions
- There's a migration utility that converts old preferences to the new format
- You don't need to manually handle this, but be aware of it if you're accessing localStorage directly
## Benefits of the Change
1. **Consistent API**: Hotkeys now follow the same customization pattern as other OHIF features
2. **More flexible**: Easier to modify specific hotkeys without replacing the entire set
3. **Better user preferences**: User customizations are better preserved and migrated
4. **Runtime updates**: Hotkeys can be modified at runtime through the customization service
5. **Improved cleanup**: Better lifecycle management of hotkey bindings

View File

@ -0,0 +1,66 @@
---
title: Tours and Onboarding
---
## Migration Guide: Tours
* Tours are no longer defined directly in `window.config.tours` but through the customization service under the key `ohif.tours`
* The `waitForElement` utility function has been moved from the config file to a dedicated customization file
* The structure of tour definitions (steps, options, etc.) remains largely the same
## Migration Steps:
### 1. Update any direct references to window.config.tours
If you have any code that directly references window.config.tours, update it to use the customization service:
```diff
- const tours = window.config.tours;
+ const tours = customizationService.getCustomization('ohif.tours');
```
### 2. Use config update patterns for configuring tours
**Before:**
```diff
- window.config = {
- tours: [
- {
- id: 'basicViewerTour',
- route: '/viewer',
- steps: [
- // tour steps...
- ],
- tourOptions: {
- // tour options...
- },
- },
- ],
- };
```
**After:**
```javascript
window.config = {
customizationService: {
'ohif.tours': {
$set: [
{
id: 'basicViewerTour',
route: '/viewer',
steps: [
// Your tour steps
],
},
],
},
},
};
```
## Benefits of the Change
4. **Mode-specific Tours**: now you can have different tours for different modes

View File

@ -74,6 +74,7 @@ export default [
```
## Behind the Scene
When you `setHotkeys`, the `commandName` gets registered with the `commandsManager` and
get run after the key is pressed.
### Global vs Mode specific hotkeys
You can can set the global hotkeys and override them using the `$set` method
in the customization service.

View File

@ -148,6 +148,46 @@ window.config = {
];
export const customizations = [
{
id: 'ohif.hotkeyBindings',
description: 'Defines the hotkeys for the application.',
default: 'look at hotkeyBindingsCustomization.ts file',
configuration: `
window.config = {
// rest of window config
customizationService: [
{
// this will override the default hotkeys and only have one hotkey
'ohif.hotkeyBindings': {
$set: [
{
commandName: 'scaleDownViewport',
label: 'Zoom Out',
keys: ['-'],
isEditable: true,
},
],
},
},
],
// or lets say you want to change one key of the default hotkeys to default
// something else
customizationService: [
{
// this will override the default hotkeys and only have one hotkey
'ohif.hotkeyBindings': {
$filter: {
match: { commandName: 'scaleDownViewport' },
$set: {
keys: ['ctrl+shift+-'],
},
},
},
},
],
`,
},
{
id: 'measurementLabels',
description: 'Labels for measurement tools in the viewer that are automatically asked for.',

View File

@ -9,9 +9,27 @@ Throughout the development of the OHIF Viewer, we have participated in various
conferences and "hackathons". In this page, we will provide the presentations
and other resources that we have provided to the community in the past:
## 2025
### Machine Learning in Medical Imaging Consortium (MaLMIC) | January 2025
We presented two talks at the Machine Learning in Medical Imaging Consortium (MaLMIC) 2025 conference.
- Advanced Medical Imaging Visualization [Slides](https://docs.google.com/presentation/d/1HZDL-72nNe4BPawDxR3XnSFLB3oLo72RjExc-KHDZfo/edit?usp=sharing)
- Introducing Advanced Segmentation Tools in the OHIF Viewer and Cornerstone3D [Slides](https://docs.google.com/presentation/d/146oJ24PPsFZaDPHeFudRF1dmbL42K9yHzQdXXAXdWxk/edit?usp=sharing)
## 2024
### ITCR Sustainment Session 2024
Dr. Gordon Harris presented at ITCR sustainment session about the future of OHIF.
- OHIF Sustainability [Slides](https://docs.google.com/presentation/d/15380mjCzBKBj9PuysCW1Q9ODnyoypJrCDpj3atTtK6I/edit?usp=sharing)
### ITCR Sustainment Panel 2024
- Advanced Medical Imaging Visualization [Slides](https://docs.google.com/presentation/d/1alUp9uJpoJs3aAUE0KqrufGo6e6HHvXYmOAdJp-Rlkc/edit?usp=sharing)
### IMNO 2024 - March 19-20, 2024

View File

@ -7,22 +7,25 @@ import './Onboarding.css';
import { hasTourBeenShown, markTourAsShown, defaultShowHandler, middleware } from './utilities';
const Onboarding = () => {
const Shepherd = useShepherd();
const location = useLocation();
const tours = window.config.tours as Array<{
const Onboarding = ({
tours = [],
}: {
tours?: Array<{
id: string;
route: string;
tourOptions: TourOptions;
steps: StepOptions[];
}>;
}) => {
const Shepherd = useShepherd();
const location = useLocation();
/**
* Show the tour if it hasn't been shown yet based on the current route.
* Constructs a tour instance and adds steps to it based on the matching tour.
*/
useEffect(() => {
if (!tours) {
if (!tours.length) {
return;
}