diff --git a/extensions/cornerstone/src/commandsModule.ts b/extensions/cornerstone/src/commandsModule.ts
index 5d2e34bc5..9926d9811 100644
--- a/extensions/cornerstone/src/commandsModule.ts
+++ b/extensions/cornerstone/src/commandsModule.ts
@@ -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 {
diff --git a/extensions/default/src/ViewerLayout/index.tsx b/extensions/default/src/ViewerLayout/index.tsx
index a2ed2ba39..764690083 100644
--- a/extensions/default/src/ViewerLayout/index.tsx
+++ b/extensions/default/src/ViewerLayout/index.tsx
@@ -199,7 +199,7 @@ function ViewerLayout({
-
+
);
diff --git a/extensions/default/src/customizations/hotkeyBindingsCustomization.ts b/extensions/default/src/customizations/hotkeyBindingsCustomization.ts
new file mode 100644
index 000000000..a07795626
--- /dev/null
+++ b/extensions/default/src/customizations/hotkeyBindingsCustomization.ts
@@ -0,0 +1,5 @@
+import { defaults } from '@ohif/core';
+
+export default {
+ 'ohif.hotkeyBindings': defaults.hotkeyBindings,
+};
diff --git a/extensions/default/src/customizations/onboardingCustomization.ts b/extensions/default/src/customizations/onboardingCustomization.ts
new file mode 100644
index 000000000..225743f7d
--- /dev/null
+++ b/extensions/default/src/customizations/onboardingCustomization.ts
@@ -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,
+ },
+ ],
+ },
+ },
+ },
+ ],
+};
diff --git a/extensions/default/src/getCustomizationModule.tsx b/extensions/default/src/getCustomizationModule.tsx
index 8588fc522..b202eceae 100644
--- a/extensions/default/src/getCustomizationModule.tsx
+++ b/extensions/default/src/getCustomizationModule.tsx
@@ -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,
},
},
];
diff --git a/modes/basic-dev-mode/src/index.ts b/modes/basic-dev-mode/src/index.ts
index 8bc884e37..a1ca916e1 100644
--- a/modes/basic-dev-mode/src/index.ts
+++ b/modes/basic-dev-mode/src/index.ts
@@ -171,7 +171,6 @@ function modeFactory({ modeConfiguration }) {
dicompdf.sopClassHandler,
dicomsr.sopClassHandler,
],
- hotkeys: [...hotkeys.defaults.hotkeyBindings],
};
}
diff --git a/modes/basic-test-mode/src/index.ts b/modes/basic-test-mode/src/index.ts
index 13663925e..1ad5c62da 100644
--- a/modes/basic-test-mode/src/index.ts
+++ b/modes/basic-test-mode/src/index.ts
@@ -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],
},
};
}
diff --git a/modes/longitudinal/src/index.ts b/modes/longitudinal/src/index.ts
index 7c2e922d9..11f85ebce 100644
--- a/modes/longitudinal/src/index.ts
+++ b/modes/longitudinal/src/index.ts
@@ -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,
};
}
diff --git a/modes/longitudinal/src/toolbarButtons.ts b/modes/longitudinal/src/toolbarButtons.ts
index 3a8d4d592..003620223 100644
--- a/modes/longitudinal/src/toolbarButtons.ts
+++ b/modes/longitudinal/src/toolbarButtons.ts
@@ -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;
diff --git a/modes/microscopy/src/index.tsx b/modes/microscopy/src/index.tsx
index 1ae9f57f7..bdef6ef3f 100644
--- a/modes/microscopy/src/index.tsx
+++ b/modes/microscopy/src/index.tsx
@@ -123,7 +123,6 @@ function modeFactory({ modeConfiguration }) {
dicomvideo.sopClassHandler,
dicompdf.sopClassHandler,
],
- hotkeys: [...hotkeys.defaults.hotkeyBindings],
...modeConfiguration,
};
}
diff --git a/modes/preclinical-4d/src/index.tsx b/modes/preclinical-4d/src/index.tsx
index 13081e2d5..a0c494dc7 100644
--- a/modes/preclinical-4d/src/index.tsx
+++ b/modes/preclinical-4d/src/index.tsx
@@ -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],
};
}
diff --git a/modes/segmentation/src/index.tsx b/modes/segmentation/src/index.tsx
index 17ce6d1c2..80e7ce2cd 100644
--- a/modes/segmentation/src/index.tsx
+++ b/modes/segmentation/src/index.tsx
@@ -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],
};
}
diff --git a/modes/tmtv/src/index.ts b/modes/tmtv/src/index.ts
index 464c9f87e..eb070f0de 100644
--- a/modes/tmtv/src/index.ts
+++ b/modes/tmtv/src/index.ts
@@ -216,7 +216,6 @@ function modeFactory({ modeConfiguration }) {
extensions: extensionDependencies,
hangingProtocol: tmtv.hangingProtocol,
sopClassHandlers: [ohif.sopClassHandler],
- hotkeys: [...hotkeys.defaults.hotkeyBindings],
...modeConfiguration,
};
}
diff --git a/platform/app/public/config/aws.js b/platform/app/public/config/aws.js
index fdba3e035..be11fa859 100644
--- a/platform/app/public/config/aws.js
+++ b/platform/app/public/config/aws.js
@@ -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'],
- },
- ],
};
diff --git a/platform/app/public/config/default.js b/platform/app/public/config/default.js
index 901f8ea30..81240a424 100644
--- a/platform/app/public/config/default.js
+++ b/platform/app/public/config/default.js
@@ -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);
- });
-}
diff --git a/platform/app/public/config/default_16bit.js b/platform/app/public/config/default_16bit.js
index d1b6558ca..6c08bd24b 100644
--- a/platform/app/public/config/default_16bit.js
+++ b/platform/app/public/config/default_16bit.js
@@ -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'],
- },
- ],
};
diff --git a/platform/app/public/config/demo.js b/platform/app/public/config/demo.js
index eb4e41015..791a5c3b7 100644
--- a/platform/app/public/config/demo.js
+++ b/platform/app/public/config/demo.js
@@ -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!
diff --git a/platform/app/public/config/dicomweb_relative.js b/platform/app/public/config/dicomweb_relative.js
index 69752ea3a..a814ac9cb 100644
--- a/platform/app/public/config/dicomweb_relative.js
+++ b/platform/app/public/config/dicomweb_relative.js
@@ -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'],
- },
- ],
};
diff --git a/platform/app/public/config/idc.js b/platform/app/public/config/idc.js
index 5f08c83e5..3a4e55038 100644
--- a/platform/app/public/config/idc.js
+++ b/platform/app/public/config/idc.js
@@ -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',
-// };
-// };
diff --git a/platform/app/public/config/kheops.js b/platform/app/public/config/kheops.js
index 3e7bc7926..e1ee3aac1 100644
--- a/platform/app/public/config/kheops.js
+++ b/platform/app/public/config/kheops.js
@@ -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'],
- },
- ],
};
diff --git a/platform/app/public/config/local_orthanc.js b/platform/app/public/config/local_orthanc.js
index b17dc3fbe..68d3b9b02 100644
--- a/platform/app/public/config/local_orthanc.js
+++ b/platform/app/public/config/local_orthanc.js
@@ -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'],
- },
- ],
};
diff --git a/platform/app/public/config/local_static.js b/platform/app/public/config/local_static.js
index 193ecb40c..696632ddb 100644
--- a/platform/app/public/config/local_static.js
+++ b/platform/app/public/config/local_static.js
@@ -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'],
- },
- ],
};
diff --git a/platform/app/public/config/netlify.js b/platform/app/public/config/netlify.js
index 678b88a6c..6b4e3b9cf 100644
--- a/platform/app/public/config/netlify.js
+++ b/platform/app/public/config/netlify.js
@@ -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) {
diff --git a/platform/app/public/config/public_dicomweb.js b/platform/app/public/config/public_dicomweb.js
index 0eb9a5f6b..9f1eb764d 100644
--- a/platform/app/public/config/public_dicomweb.js
+++ b/platform/app/public/config/public_dicomweb.js
@@ -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'],
- },
- ],
};
diff --git a/platform/app/src/routes/Mode/Mode.tsx b/platform/app/src/routes/Mode/Mode.tsx
index 27351e672..86651aa0d 100644
--- a/platform/app/src/routes/Mode/Mode.tsx
+++ b/platform/app/src/routes/Mode/Mode.tsx
@@ -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) {
diff --git a/platform/cli/templates/mode/src/index.tsx b/platform/cli/templates/mode/src/index.tsx
index 87d867349..4f2a57c6f 100644
--- a/platform/cli/templates/mode/src/index.tsx
+++ b/platform/cli/templates/mode/src/index.tsx
@@ -128,7 +128,6 @@ function modeFactory({ modeConfiguration }) {
/** SopClassHandlers used by the mode */
sopClassHandlers: [ohif.sopClassHandler],
/** hotkeys for mode */
- hotkeys: [...hotkeys.defaults.hotkeyBindings],
};
}
diff --git a/platform/core/src/classes/HotkeysManager.test.js b/platform/core/src/classes/HotkeysManager.test.js
index 2eb63def2..971c17114 100644
--- a/platform/core/src/classes/HotkeysManager.test.js
+++ b/platform/core/src/classes/HotkeysManager.test.js
@@ -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());
diff --git a/platform/core/src/classes/HotkeysManager.ts b/platform/core/src/classes/HotkeysManager.ts
index 64d2b0f81..30305c7d8 100644
--- a/platform/core/src/classes/HotkeysManager.ts
+++ b/platform/core/src/classes/HotkeysManager.ts
@@ -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 = {};
+ 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);
}
}
diff --git a/platform/core/src/defaults/hotkeyBindings.js b/platform/core/src/defaults/hotkeyBindings.ts
similarity index 71%
rename from platform/core/src/defaults/hotkeyBindings.js
rename to platform/core/src/defaults/hotkeyBindings.ts
index 34c22f52e..eeea06e97 100644
--- a/platform/core/src/defaults/hotkeyBindings.js
+++ b/platform/core/src/defaults/hotkeyBindings.ts
@@ -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,
// },
];
diff --git a/platform/core/src/defaults/index.js b/platform/core/src/defaults/index.js
index f8c065d7b..7054805b9 100644
--- a/platform/core/src/defaults/index.js
+++ b/platform/core/src/defaults/index.js
@@ -1,4 +1,7 @@
import hotkeyBindings from './hotkeyBindings';
import windowLevelPresets from './windowLevelPresets';
-export { hotkeyBindings, windowLevelPresets };
-export default { hotkeyBindings, windowLevelPresets };
+
+export default {
+ hotkeyBindings,
+ windowLevelPresets,
+};
diff --git a/platform/core/src/types/AppTypes.ts b/platform/core/src/types/AppTypes.ts
index 1ed544f15..cf38365e4 100644
--- a/platform/core/src/types/AppTypes.ts
+++ b/platform/core/src/types/AppTypes.ts
@@ -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 {
diff --git a/platform/core/src/utils/hotkeys/migrateHotkeys.ts b/platform/core/src/utils/hotkeys/migrateHotkeys.ts
new file mode 100644
index 000000000..45eb9c08b
--- /dev/null
+++ b/platform/core/src/utils/hotkeys/migrateHotkeys.ts
@@ -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;
+}): 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;
diff --git a/platform/docs/docs/configuration/tours.md b/platform/docs/docs/configuration/tours.md
index 849c82f1b..340800053 100644
--- a/platform/docs/docs/configuration/tours.md
+++ b/platform/docs/docs/configuration/tours.md
@@ -13,62 +13,39 @@ Tours allow you to provide step-by-step guidance to users, explaining different
### Adding a Tour to your Configuration
-Here’s 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
diff --git a/platform/docs/docs/migration-guide/3p9-to-3p10/General/hotkeys.md b/platform/docs/docs/migration-guide/3p9-to-3p10/General/hotkeys.md
new file mode 100644
index 000000000..80fc94a31
--- /dev/null
+++ b/platform/docs/docs/migration-guide/3p9-to-3p10/General/hotkeys.md
@@ -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
diff --git a/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-Tourss.md b/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-Tourss.md
new file mode 100644
index 000000000..16ac94663
--- /dev/null
+++ b/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-Tourss.md
@@ -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
diff --git a/platform/docs/docs/platform/managers/hotkeys.md b/platform/docs/docs/platform/managers/hotkeys.md
index ebdd2bd13..75d4ead30 100644
--- a/platform/docs/docs/platform/managers/hotkeys.md
+++ b/platform/docs/docs/platform/managers/hotkeys.md
@@ -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.
diff --git a/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx b/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx
index 1fb0e45f2..5b3bd93f0 100644
--- a/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx
+++ b/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx
@@ -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.',
diff --git a/platform/docs/docs/resources.md b/platform/docs/docs/resources.md
index 0ae9d1add..bbdd4a18f 100644
--- a/platform/docs/docs/resources.md
+++ b/platform/docs/docs/resources.md
@@ -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
diff --git a/platform/ui-next/src/components/Onboarding/Onboarding.tsx b/platform/ui-next/src/components/Onboarding/Onboarding.tsx
index 7b9fc0852..a5ecf16af 100644
--- a/platform/ui-next/src/components/Onboarding/Onboarding.tsx
+++ b/platform/ui-next/src/components/Onboarding/Onboarding.tsx
@@ -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;
}