fix(toolbar): allow customizable toolbar for active viewport and allow active tool to be deactivated via a click (#3608)

Co-authored-by: Joe Boccanfuso <joe.boccanfuso@radicalimaging.com>
This commit is contained in:
Bill Wallace 2023-10-25 13:08:12 -04:00 committed by GitHub
parent 2145b42920
commit dd6d9768bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1096 additions and 628 deletions

View File

@ -456,13 +456,8 @@ function commandsModule({
const { viewport } = enabledElement;
if (viewport instanceof StackViewport) {
viewport.resetProperties();
viewport.resetCamera();
} else {
viewport.resetProperties();
viewport.resetCamera();
}
viewport.resetProperties?.();
viewport.resetCamera();
viewport.render();
},

View File

@ -1,19 +1,34 @@
import React, { useEffect, useState, useCallback } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import classnames from 'classnames';
import { useViewportGrid } from '@ohif/ui';
export default function Toolbar({ servicesManager }) {
export default function Toolbar({
servicesManager,
}: Types.Extensions.ExtensionParams): React.ReactElement {
const { toolbarService } = servicesManager.services;
const [viewportGrid, viewportGridService] = useViewportGrid();
const [toolbarButtons, setToolbarButtons] = useState([]);
useEffect(() => {
const { unsubscribe } = toolbarService.subscribe(toolbarService.EVENTS.TOOL_BAR_MODIFIED, () =>
setToolbarButtons(toolbarService.getButtonSection('primary'))
const updateToolbar = () => {
const toolGroupId =
viewportGridService.getActiveViewportOptionByKey('toolGroupId') ?? 'default';
setToolbarButtons(toolbarService.getButtonSection(toolGroupId));
};
const { unsubscribe } = toolbarService.subscribe(
toolbarService.EVENTS.TOOL_BAR_MODIFIED,
updateToolbar
);
updateToolbar();
return () => {
unsubscribe();
};
}, [toolbarService]);
}, [toolbarService, viewportGrid]);
const onInteraction = useCallback(
args => toolbarService.recordInteraction(args),

View File

@ -65,7 +65,7 @@ ToolbarButtonWithServices.propTypes = {
state: PropTypes.shape({
primaryToolId: PropTypes.string,
toggles: PropTypes.objectOf(PropTypes.bool),
groups: PropTypes.objectOf(PropTypes.object),
groups: PropTypes.objectOf(PropTypes.any),
}).isRequired,
}).isRequired,
}).isRequired,

View File

@ -2,11 +2,16 @@ import { hotkeys } from '@ohif/core';
import toolbarButtons from './toolbarButtons';
import { id } from './id';
import initToolGroups from './initToolGroups';
import moreTools from './moreTools';
import moreToolsMpr from './moreToolsMpr';
// Allow this mode by excluding non-imaging modalities such as SR, SEG
// Also, SM is not a simple imaging modalities, so exclude it.
const NON_IMAGE_MODALITIES = ['SM', 'ECG', 'SR', 'SEG'];
const DEFAULT_TOOL_GROUP_ID = 'default';
const MPR_TOOL_GROUP_ID = 'mpr';
const ohif = {
layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout',
sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack',
@ -77,21 +82,23 @@ function modeFactory() {
]);
let unsubscribe;
toolbarService.setDefaultTool({
groupId: 'WindowLevel',
itemId: 'WindowLevel',
interactionType: 'tool',
commands: [
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'WindowLevel',
},
context: 'CORNERSTONE',
},
],
});
const activateTool = () => {
toolbarService.recordInteraction({
groupId: 'WindowLevel',
interactionType: 'tool',
commands: [
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'WindowLevel',
},
context: 'CORNERSTONE',
},
],
});
toolbarService.recordInteraction(toolbarService.getDefaultTool());
// We don't need to reset the active tool whenever a viewport is getting
// added to the toolGroup.
@ -106,8 +113,18 @@ function modeFactory() {
));
toolbarService.init(extensionManager);
toolbarService.addButtons(toolbarButtons);
toolbarService.createButtonSection('primary', [
toolbarService.addButtons([...toolbarButtons, ...moreTools, ...moreToolsMpr]);
toolbarService.createButtonSection(DEFAULT_TOOL_GROUP_ID, [
'MeasurementTools',
'Zoom',
'WindowLevel',
'Pan',
'Capture',
'Layout',
'MPR',
'MoreTools',
]);
toolbarService.createButtonSection(MPR_TOOL_GROUP_ID, [
'MeasurementTools',
'Zoom',
'WindowLevel',
@ -116,7 +133,7 @@ function modeFactory() {
'Layout',
'MPR',
'Crosshairs',
'MoreTools',
'MoreToolsMpr',
]);
},
onModeExit: ({ servicesManager }) => {

View File

@ -0,0 +1,231 @@
import type { RunCommand } from '@ohif/core/types';
import { EVENTS } from '@cornerstonejs/core';
import { ToolbarService } from '@ohif/core';
const ReferenceLinesCommands: RunCommand = [
{
commandName: 'setSourceViewportForReferenceLinesTool',
context: 'CORNERSTONE',
},
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'ReferenceLines',
},
context: 'CORNERSTONE',
},
];
const moreTools = [
{
id: 'MoreTools',
type: 'ohif.splitButton',
props: {
isRadio: true, // ?
groupId: 'MoreTools',
primary: ToolbarService._createActionButton(
'Reset',
'tool-reset',
'Reset View',
[
{
commandName: 'resetViewport',
},
],
'Reset'
),
secondary: {
icon: 'chevron-down',
label: '',
isActive: true,
tooltip: 'More Tools',
},
items: [
ToolbarService._createActionButton(
'Reset',
'tool-reset',
'Reset View',
[
{
commandName: 'resetViewport',
},
],
'Reset'
),
ToolbarService._createActionButton(
'rotate-right',
'tool-rotate-right',
'Rotate Right',
[
{
commandName: 'rotateViewportCW',
commandOptions: {},
context: 'CORNERSTONE',
},
],
'Rotate +90'
),
ToolbarService._createActionButton(
'flip-horizontal',
'tool-flip-horizontal',
'Flip Horizontally',
[
{
commandName: 'flipViewportHorizontal',
commandOptions: {},
context: 'CORNERSTONE',
},
],
'Flip Horizontally'
),
ToolbarService._createToggleButton(
'StackImageSync',
'link',
'Stack Image Sync',
[
{
commandName: 'toggleStackImageSync',
},
],
'Enable position synchronization on stack viewports',
{
listeners: {
[EVENTS.STACK_VIEWPORT_NEW_STACK]: {
commandName: 'toggleStackImageSync',
commandOptions: { toggledState: true },
},
},
}
),
ToolbarService._createToggleButton(
'ReferenceLines',
'tool-referenceLines', // change this with the new icon
'Reference Lines',
ReferenceLinesCommands,
'Show Reference Lines',
{
listeners: {
[EVENTS.STACK_VIEWPORT_NEW_STACK]: ReferenceLinesCommands,
[EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: ReferenceLinesCommands,
},
}
),
ToolbarService._createToolButton(
'StackScroll',
'tool-stack-scroll',
'Stack Scroll',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'StackScroll',
},
context: 'CORNERSTONE',
},
],
'Stack Scroll'
),
ToolbarService._createActionButton(
'invert',
'tool-invert',
'Invert',
[
{
commandName: 'invertViewport',
commandOptions: {},
context: 'CORNERSTONE',
},
],
'Invert Colors'
),
ToolbarService._createToolButton(
'Probe',
'tool-probe',
'Probe',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'DragProbe',
},
context: 'CORNERSTONE',
},
],
'Probe'
),
ToolbarService._createToggleButton(
'cine',
'tool-cine',
'Cine',
[
{
commandName: 'toggleCine',
context: 'CORNERSTONE',
},
],
'Cine'
),
ToolbarService._createToolButton(
'Angle',
'tool-angle',
'Angle',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'Angle',
},
context: 'CORNERSTONE',
},
],
'Angle'
),
ToolbarService._createToolButton(
'Magnify',
'tool-magnify',
'Magnify',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'Magnify',
},
context: 'CORNERSTONE',
},
],
'Magnify'
),
ToolbarService._createToolButton(
'Rectangle',
'tool-rectangle',
'Rectangle',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'RectangleROI',
},
context: 'CORNERSTONE',
},
],
'Rectangle'
),
ToolbarService._createActionButton(
'TagBrowser',
'list-bullets',
'Dicom Tag Browser',
[
{
commandName: 'openDICOMTagViewer',
commandOptions: {},
context: 'DEFAULT',
},
],
'Dicom Tag Browser'
),
],
},
},
];
export default moreTools;

View File

@ -0,0 +1,142 @@
import { ToolbarService } from '@ohif/core';
const moreToolsMpr = [
{
id: 'MoreToolsMpr',
type: 'ohif.splitButton',
props: {
isRadio: true, // ?
groupId: 'MoreTools',
primary: ToolbarService._createActionButton(
'Reset',
'tool-reset',
'Reset View',
[
{
commandName: 'resetViewport',
},
],
'Reset'
),
secondary: {
icon: 'chevron-down',
label: '',
isActive: true,
tooltip: 'More Tools',
},
items: [
ToolbarService._createActionButton(
'Reset',
'tool-reset',
'Reset View',
[
{
commandName: 'resetViewport',
},
],
'Reset'
),
ToolbarService._createToolButton(
'StackScroll',
'tool-stack-scroll',
'Stack Scroll',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'StackScroll',
},
context: 'CORNERSTONE',
},
],
'Stack Scroll'
),
ToolbarService._createActionButton(
'invert',
'tool-invert',
'Invert',
[
{
commandName: 'invertViewport',
commandOptions: {},
context: 'CORNERSTONE',
},
],
'Invert Colors'
),
ToolbarService._createToolButton(
'Probe',
'tool-probe',
'Probe',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'DragProbe',
},
context: 'CORNERSTONE',
},
],
'Probe'
),
ToolbarService._createToggleButton(
'cine',
'tool-cine',
'Cine',
[
{
commandName: 'toggleCine',
context: 'CORNERSTONE',
},
],
'Cine'
),
ToolbarService._createToolButton(
'Angle',
'tool-angle',
'Angle',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'Angle',
},
context: 'CORNERSTONE',
},
],
'Angle'
),
ToolbarService._createToolButton(
'Rectangle',
'tool-rectangle',
'Rectangle',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'RectangleROI',
},
context: 'CORNERSTONE',
},
],
'Rectangle'
),
ToolbarService._createActionButton(
'TagBrowser',
'list-bullets',
'Dicom Tag Browser',
[
{
commandName: 'openDICOMTagViewer',
commandOptions: {},
context: 'DEFAULT',
},
],
'Dicom Tag Browser'
),
],
},
},
];
export default moreToolsMpr;

View File

@ -5,16 +5,11 @@ import {
// ListMenu,
WindowLevelMenuItem,
} from '@ohif/ui';
import { defaults, ToolbarService } from '@ohif/core';
import type { Button, RunCommand } from '@ohif/core/types';
import { EVENTS } from '@cornerstonejs/core';
import { ToolbarService, defaults } from '@ohif/core';
import type { Button } from '@ohif/core/types';
const { windowLevelPresets } = defaults;
const _createActionButton = ToolbarService._createButton.bind(null, 'action');
const _createToggleButton = ToolbarService._createButton.bind(null, 'toggle');
const _createToolButton = ToolbarService._createButton.bind(null, 'tool');
/**
*
* @param {*} preset - preset number (from above import)
@ -39,20 +34,6 @@ function _createWwwcPreset(preset, title, subtitle) {
};
}
const ReferenceLinesCommands: RunCommand = [
{
commandName: 'setSourceViewportForReferenceLinesTool',
context: 'CORNERSTONE',
},
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'ReferenceLines',
},
context: 'CORNERSTONE',
},
];
const toolbarButtons: Button[] = [
// Measurement
{
@ -62,7 +43,7 @@ const toolbarButtons: Button[] = [
groupId: 'MeasurementTools',
isRadio: true, // ?
// Switch?
primary: _createToolButton(
primary: ToolbarService._createToolButton(
'Length',
'tool-length',
'Length',
@ -93,7 +74,7 @@ const toolbarButtons: Button[] = [
tooltip: 'More Measure Tools',
},
items: [
_createToolButton(
ToolbarService._createToolButton(
'Length',
'tool-length',
'Length',
@ -117,7 +98,7 @@ const toolbarButtons: Button[] = [
],
'Length Tool'
),
_createToolButton(
ToolbarService._createToolButton(
'Bidirectional',
'tool-bidirectional',
'Bidirectional',
@ -140,7 +121,7 @@ const toolbarButtons: Button[] = [
],
'Bidirectional Tool'
),
_createToolButton(
ToolbarService._createToolButton(
'ArrowAnnotate',
'tool-annotate',
'Annotation',
@ -163,7 +144,7 @@ const toolbarButtons: Button[] = [
],
'Arrow Annotate'
),
_createToolButton(
ToolbarService._createToolButton(
'EllipticalROI',
'tool-elipse',
'Ellipse',
@ -186,7 +167,7 @@ const toolbarButtons: Button[] = [
],
'Ellipse Tool'
),
_createToolButton(
ToolbarService._createToolButton(
'CircleROI',
'tool-circle',
'Circle',
@ -237,7 +218,7 @@ const toolbarButtons: Button[] = [
type: 'ohif.splitButton',
props: {
groupId: 'WindowLevel',
primary: _createToolButton(
primary: ToolbarService._createToolButton(
'WindowLevel',
'tool-window-level',
'Window Level',
@ -411,7 +392,6 @@ const toolbarButtons: Button[] = [
commandOptions: {
protocolId: 'mpr',
},
context: 'DEFAULT',
},
],
},
@ -430,225 +410,10 @@ const toolbarButtons: Button[] = [
toolGroupId: 'mpr',
toolName: 'Crosshairs',
},
context: 'CORNERSTONE',
},
],
},
},
// More...
{
id: 'MoreTools',
type: 'ohif.splitButton',
props: {
isRadio: true, // ?
groupId: 'MoreTools',
primary: _createActionButton(
'Reset',
'tool-reset',
'Reset View',
[
{
commandName: 'resetViewport',
commandOptions: {},
context: 'CORNERSTONE',
},
],
'Reset'
),
secondary: {
icon: 'chevron-down',
label: '',
isActive: true,
tooltip: 'More Tools',
},
items: [
_createActionButton(
'Reset',
'tool-reset',
'Reset View',
[
{
commandName: 'resetViewport',
commandOptions: {},
context: 'CORNERSTONE',
},
],
'Reset'
),
_createActionButton(
'rotate-right',
'tool-rotate-right',
'Rotate Right',
[
{
commandName: 'rotateViewportCW',
commandOptions: {},
context: 'CORNERSTONE',
},
],
'Rotate +90'
),
_createActionButton(
'flip-horizontal',
'tool-flip-horizontal',
'Flip Horizontally',
[
{
commandName: 'flipViewportHorizontal',
commandOptions: {},
context: 'CORNERSTONE',
},
],
'Flip Horizontally'
),
_createToggleButton(
'StackImageSync',
'link',
'Stack Image Sync',
[
{
commandName: 'toggleStackImageSync',
},
],
'Enable position synchronization on stack viewports',
{
listeners: {
[EVENTS.STACK_VIEWPORT_NEW_STACK]: {
commandName: 'toggleStackImageSync',
commandOptions: { toggledState: true },
},
},
}
),
_createToggleButton(
'ReferenceLines',
'tool-referenceLines', // change this with the new icon
'Reference Lines',
ReferenceLinesCommands,
'Show Reference Lines',
{
listeners: {
[EVENTS.STACK_VIEWPORT_NEW_STACK]: ReferenceLinesCommands,
[EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: ReferenceLinesCommands,
},
}
),
_createToolButton(
'StackScroll',
'tool-stack-scroll',
'Stack Scroll',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'StackScroll',
},
context: 'CORNERSTONE',
},
],
'Stack Scroll'
),
_createActionButton(
'invert',
'tool-invert',
'Invert',
[
{
commandName: 'invertViewport',
commandOptions: {},
context: 'CORNERSTONE',
},
],
'Invert Colors'
),
_createToolButton(
'Probe',
'tool-probe',
'Probe',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'DragProbe',
},
context: 'CORNERSTONE',
},
],
'Probe'
),
_createToggleButton(
'cine',
'tool-cine',
'Cine',
[
{
commandName: 'toggleCine',
context: 'CORNERSTONE',
},
],
'Cine'
),
_createToolButton(
'Angle',
'tool-angle',
'Angle',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'Angle',
},
context: 'CORNERSTONE',
},
],
'Angle'
),
_createToolButton(
'Magnify',
'tool-magnify',
'Magnify',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'Magnify',
},
context: 'CORNERSTONE',
},
],
'Magnify'
),
_createToolButton(
'Rectangle',
'tool-rectangle',
'Rectangle',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'RectangleROI',
},
context: 'CORNERSTONE',
},
],
'Rectangle'
),
_createActionButton(
'TagBrowser',
'list-bullets',
'Dicom Tag Browser',
[
{
commandName: 'openDICOMTagViewer',
commandOptions: {},
context: 'DEFAULT',
},
],
'Dicom Tag Browser'
),
],
},
},
];
export default toolbarButtons;

View File

@ -2,11 +2,16 @@ import { hotkeys } from '@ohif/core';
import toolbarButtons from './toolbarButtons';
import { id } from './id';
import initToolGroups from './initToolGroups';
import moreTools from './moreTools';
import moreToolsMpr from './moreToolsMpr';
// Allow this mode by excluding non-imaging modalities such as SR, SEG
// Also, SM is not a simple imaging modalities, so exclude it.
const NON_IMAGE_MODALITIES = ['SM', 'ECG', 'SR', 'SEG', 'RTSTRUCT'];
const DEFAULT_TOOL_GROUP_ID = 'default';
const MPR_TOOL_GROUP_ID = 'mpr';
const ohif = {
layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout',
sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack',
@ -83,21 +88,23 @@ function modeFactory({ modeConfiguration }) {
initToolGroups(extensionManager, toolGroupService, commandsManager);
let unsubscribe;
toolbarService.setDefaultTool({
groupId: 'WindowLevel',
itemId: 'WindowLevel',
interactionType: 'tool',
commands: [
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'WindowLevel',
},
context: 'CORNERSTONE',
},
],
});
const activateTool = () => {
toolbarService.recordInteraction({
groupId: 'WindowLevel',
interactionType: 'tool',
commands: [
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'WindowLevel',
},
context: 'CORNERSTONE',
},
],
});
toolbarService.recordInteraction(toolbarService.getDefaultTool());
// We don't need to reset the active tool whenever a viewport is getting
// added to the toolGroup.
@ -112,8 +119,18 @@ function modeFactory({ modeConfiguration }) {
));
toolbarService.init(extensionManager);
toolbarService.addButtons(toolbarButtons);
toolbarService.createButtonSection('primary', [
toolbarService.addButtons([...toolbarButtons, ...moreTools, ...moreToolsMpr]);
toolbarService.createButtonSection(DEFAULT_TOOL_GROUP_ID, [
'MeasurementTools',
'Zoom',
'WindowLevel',
'Pan',
'Capture',
'Layout',
'MPR',
'MoreTools',
]);
toolbarService.createButtonSection(MPR_TOOL_GROUP_ID, [
'MeasurementTools',
'Zoom',
'WindowLevel',
@ -122,7 +139,7 @@ function modeFactory({ modeConfiguration }) {
'Layout',
'MPR',
'Crosshairs',
'MoreTools',
'MoreToolsMpr',
]);
customizationService.addModeCustomizations([

View File

@ -0,0 +1,298 @@
import type { RunCommand } from '@ohif/core/types';
import { EVENTS } from '@cornerstonejs/core';
import { ToolbarService } from '@ohif/core';
const ReferenceLinesCommands: RunCommand = [
{
commandName: 'setSourceViewportForReferenceLinesTool',
context: 'CORNERSTONE',
},
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'ReferenceLines',
},
context: 'CORNERSTONE',
},
];
const moreTools = [
{
id: 'MoreTools',
type: 'ohif.splitButton',
props: {
isRadio: true, // ?
groupId: 'MoreTools',
primary: ToolbarService._createActionButton(
'Reset',
'tool-reset',
'Reset View',
[
{
commandName: 'resetViewport',
commandOptions: {},
context: 'CORNERSTONE',
},
],
'Reset'
),
secondary: {
icon: 'chevron-down',
label: '',
isActive: true,
tooltip: 'More Tools',
},
items: [
ToolbarService._createActionButton(
'Reset',
'tool-reset',
'Reset View',
[
{
commandName: 'resetViewport',
commandOptions: {},
context: 'CORNERSTONE',
},
],
'Reset'
),
ToolbarService._createActionButton(
'rotate-right',
'tool-rotate-right',
'Rotate Right',
[
{
commandName: 'rotateViewportCW',
commandOptions: {},
context: 'CORNERSTONE',
},
],
'Rotate +90'
),
ToolbarService._createActionButton(
'flip-horizontal',
'tool-flip-horizontal',
'Flip Horizontally',
[
{
commandName: 'flipViewportHorizontal',
commandOptions: {},
context: 'CORNERSTONE',
},
],
'Flip Horizontal'
),
ToolbarService._createToggleButton(
'StackImageSync',
'link',
'Stack Image Sync',
[
{
commandName: 'toggleStackImageSync',
},
],
'Enable position synchronization on stack viewports',
{
listeners: {
[EVENTS.STACK_VIEWPORT_NEW_STACK]: {
commandName: 'toggleStackImageSync',
commandOptions: { toggledState: true },
},
},
}
),
ToolbarService._createToggleButton(
'ReferenceLines',
'tool-referenceLines', // change this with the new icon
'Reference Lines',
ReferenceLinesCommands,
'Show Reference Lines',
{
listeners: {
[EVENTS.STACK_VIEWPORT_NEW_STACK]: ReferenceLinesCommands,
[EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: ReferenceLinesCommands,
},
}
),
ToolbarService._createToggleButton(
'ImageOverlayViewer',
'toggle-dicom-overlay',
'Image Overlay',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'ImageOverlayViewer',
},
context: 'CORNERSTONE',
},
],
'Image Overlay',
{ isActive: true }
),
ToolbarService._createToolButton(
'StackScroll',
'tool-stack-scroll',
'Stack Scroll',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'StackScroll',
},
context: 'CORNERSTONE',
},
],
'Stack Scroll'
),
ToolbarService._createActionButton(
'invert',
'tool-invert',
'Invert',
[
{
commandName: 'invertViewport',
commandOptions: {},
context: 'CORNERSTONE',
},
],
'Invert Colors'
),
ToolbarService._createToolButton(
'Probe',
'tool-probe',
'Probe',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'DragProbe',
},
context: 'CORNERSTONE',
},
],
'Probe'
),
ToolbarService._createToggleButton(
'cine',
'tool-cine',
'Cine',
[
{
commandName: 'toggleCine',
context: 'CORNERSTONE',
},
],
'Cine'
),
ToolbarService._createToolButton(
'Angle',
'tool-angle',
'Angle',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'Angle',
},
context: 'CORNERSTONE',
},
],
'Angle'
),
// Next two tools can be added once icons are added
// ToolbarService._createToolButton(
// 'Cobb Angle',
// 'tool-cobb-angle',
// 'Cobb Angle',
// [
// {
// commandName: 'setToolActive',
// commandOptions: {
// toolName: 'CobbAngle',
// },
// context: 'CORNERSTONE',
// },
// ],
// 'Cobb Angle'
// ),
// ToolbarService._createToolButton(
// 'Planar Freehand ROI',
// 'tool-freehand',
// 'PlanarFreehandROI',
// [
// {
// commandName: 'setToolActive',
// commandOptions: {
// toolName: 'PlanarFreehandROI',
// },
// context: 'CORNERSTONE',
// },
// ],
// 'Planar Freehand ROI'
// ),
ToolbarService._createToolButton(
'Magnify',
'tool-magnify',
'Magnify',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'Magnify',
},
context: 'CORNERSTONE',
},
],
'Magnify'
),
ToolbarService._createToolButton(
'Rectangle',
'tool-rectangle',
'Rectangle',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'RectangleROI',
},
context: 'CORNERSTONE',
},
],
'Rectangle'
),
ToolbarService._createToolButton(
'CalibrationLine',
'tool-calibration',
'Calibration',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'CalibrationLine',
},
context: 'CORNERSTONE',
},
],
'Calibration Line'
),
ToolbarService._createActionButton(
'TagBrowser',
'list-bullets',
'Dicom Tag Browser',
[
{
commandName: 'openDICOMTagViewer',
commandOptions: {},
context: 'DEFAULT',
},
],
'Dicom Tag Browser'
),
],
},
},
];
export default moreTools;

View File

@ -0,0 +1,242 @@
import type { RunCommand } from '@ohif/core/types';
import { EVENTS } from '@cornerstonejs/core';
import { ToolbarService } from '@ohif/core';
const ReferenceLinesCommands: RunCommand = [
{
commandName: 'setSourceViewportForReferenceLinesTool',
context: 'CORNERSTONE',
},
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'ReferenceLines',
},
context: 'CORNERSTONE',
},
];
const moreToolsMpr = [
{
id: 'MoreToolsMpr',
type: 'ohif.splitButton',
props: {
isRadio: true, // ?
groupId: 'MoreTools',
primary: ToolbarService._createActionButton(
'Reset',
'tool-reset',
'Reset View',
[
{
commandName: 'resetViewport',
commandOptions: {},
context: 'CORNERSTONE',
},
],
'Reset'
),
secondary: {
icon: 'chevron-down',
label: '',
isActive: true,
tooltip: 'More Tools',
},
items: [
ToolbarService._createActionButton(
'Reset',
'tool-reset',
'Reset View',
[
{
commandName: 'resetViewport',
commandOptions: {},
context: 'CORNERSTONE',
},
],
'Reset'
),
ToolbarService._createToggleButton(
'StackImageSync',
'link',
'Stack Image Sync',
[
{
commandName: 'toggleStackImageSync',
},
],
'Enable position synchronization on stack viewports',
{
listeners: {
[EVENTS.STACK_VIEWPORT_NEW_STACK]: {
commandName: 'toggleStackImageSync',
commandOptions: { toggledState: true },
},
},
}
),
ToolbarService._createToggleButton(
'ReferenceLines',
'tool-referenceLines', // change this with the new icon
'Reference Lines',
ReferenceLinesCommands,
'Show Reference Lines',
{
listeners: {
[EVENTS.STACK_VIEWPORT_NEW_STACK]: ReferenceLinesCommands,
[EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: ReferenceLinesCommands,
},
}
),
ToolbarService._createToggleButton(
'ImageOverlayViewer',
'toggle-dicom-overlay',
'Image Overlay',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'ImageOverlayViewer',
},
context: 'CORNERSTONE',
},
],
'Image Overlay',
{ isActive: true }
),
ToolbarService._createToolButton(
'StackScroll',
'tool-stack-scroll',
'Stack Scroll',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'StackScroll',
},
context: 'CORNERSTONE',
},
],
'Stack Scroll'
),
ToolbarService._createActionButton(
'invert',
'tool-invert',
'Invert',
[
{
commandName: 'invertViewport',
commandOptions: {},
context: 'CORNERSTONE',
},
],
'Invert Colors'
),
ToolbarService._createToolButton(
'Probe',
'tool-probe',
'Probe',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'DragProbe',
},
context: 'CORNERSTONE',
},
],
'Probe'
),
ToolbarService._createToggleButton(
'cine',
'tool-cine',
'Cine',
[
{
commandName: 'toggleCine',
context: 'CORNERSTONE',
},
],
'Cine'
),
ToolbarService._createToolButton(
'Angle',
'tool-angle',
'Angle',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'Angle',
},
context: 'CORNERSTONE',
},
],
'Angle'
),
// Next two tools can be added once icons are added
// ToolbarService._createToolButton(
// 'Cobb Angle',
// 'tool-cobb-angle',
// 'Cobb Angle',
// [
// {
// commandName: 'setToolActive',
// commandOptions: {
// toolName: 'CobbAngle',
// },
// context: 'CORNERSTONE',
// },
// ],
// 'Cobb Angle'
// ),
// ToolbarService._createToolButton(
// 'Planar Freehand ROI',
// 'tool-freehand',
// 'PlanarFreehandROI',
// [
// {
// commandName: 'setToolActive',
// commandOptions: {
// toolName: 'PlanarFreehandROI',
// },
// context: 'CORNERSTONE',
// },
// ],
// 'Planar Freehand ROI'
// ),
ToolbarService._createToolButton(
'Rectangle',
'tool-rectangle',
'Rectangle',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'RectangleROI',
},
context: 'CORNERSTONE',
},
],
'Rectangle'
),
ToolbarService._createActionButton(
'TagBrowser',
'list-bullets',
'Dicom Tag Browser',
[
{
commandName: 'openDICOMTagViewer',
commandOptions: {},
context: 'DEFAULT',
},
],
'Dicom Tag Browser'
),
],
},
},
];
export default moreToolsMpr;

View File

@ -6,15 +6,10 @@ import {
WindowLevelMenuItem,
} from '@ohif/ui';
import { defaults, ToolbarService } from '@ohif/core';
import type { Button, RunCommand } from '@ohif/core/types';
import { EVENTS } from '@cornerstonejs/core';
import type { Button } from '@ohif/core/types';
const { windowLevelPresets } = defaults;
const _createActionButton = ToolbarService._createButton.bind(null, 'action');
const _createToggleButton = ToolbarService._createButton.bind(null, 'toggle');
const _createToolButton = ToolbarService._createButton.bind(null, 'tool');
/**
*
* @param {*} preset - preset number (from above import)
@ -59,20 +54,6 @@ function _createSetToolActiveCommands(toolName) {
return temp;
}
const ReferenceLinesCommands: RunCommand = [
{
commandName: 'setSourceViewportForReferenceLinesTool',
context: 'CORNERSTONE',
},
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'ReferenceLines',
},
context: 'CORNERSTONE',
},
];
const toolbarButtons: Button[] = [
// Measurement
{
@ -82,7 +63,7 @@ const toolbarButtons: Button[] = [
groupId: 'MeasurementTools',
isRadio: true, // ?
// Switch?
primary: _createToolButton(
primary: ToolbarService._createToolButton(
'Length',
'tool-length',
'Length',
@ -113,7 +94,7 @@ const toolbarButtons: Button[] = [
tooltip: 'More Measure Tools',
},
items: [
_createToolButton(
ToolbarService._createToolButton(
'Length',
'tool-length',
'Length',
@ -137,7 +118,7 @@ const toolbarButtons: Button[] = [
],
'Length Tool'
),
_createToolButton(
ToolbarService._createToolButton(
'Bidirectional',
'tool-bidirectional',
'Bidirectional',
@ -160,7 +141,7 @@ const toolbarButtons: Button[] = [
],
'Bidirectional Tool'
),
_createToolButton(
ToolbarService._createToolButton(
'ArrowAnnotate',
'tool-annotate',
'Annotation',
@ -183,7 +164,7 @@ const toolbarButtons: Button[] = [
],
'Arrow Annotate'
),
_createToolButton(
ToolbarService._createToolButton(
'EllipticalROI',
'tool-elipse',
'Ellipse',
@ -206,7 +187,7 @@ const toolbarButtons: Button[] = [
],
'Ellipse Tool'
),
_createToolButton(
ToolbarService._createToolButton(
'CircleROI',
'tool-circle',
'Circle',
@ -249,7 +230,7 @@ const toolbarButtons: Button[] = [
type: 'ohif.splitButton',
props: {
groupId: 'WindowLevel',
primary: _createToolButton(
primary: ToolbarService._createToolButton(
'WindowLevel',
'tool-window-level',
'Window Level',
@ -354,282 +335,6 @@ const toolbarButtons: Button[] = [
},
},
// More...
{
id: 'MoreTools',
type: 'ohif.splitButton',
props: {
isRadio: true, // ?
groupId: 'MoreTools',
primary: _createActionButton(
'Reset',
'tool-reset',
'Reset View',
[
{
commandName: 'resetViewport',
commandOptions: {},
context: 'CORNERSTONE',
},
],
'Reset'
),
secondary: {
icon: 'chevron-down',
label: '',
isActive: true,
tooltip: 'More Tools',
},
items: [
_createActionButton(
'Reset',
'tool-reset',
'Reset View',
[
{
commandName: 'resetViewport',
commandOptions: {},
context: 'CORNERSTONE',
},
],
'Reset'
),
_createActionButton(
'rotate-right',
'tool-rotate-right',
'Rotate Right',
[
{
commandName: 'rotateViewportCW',
commandOptions: {},
context: 'CORNERSTONE',
},
],
'Rotate +90'
),
_createActionButton(
'flip-horizontal',
'tool-flip-horizontal',
'Flip Horizontally',
[
{
commandName: 'flipViewportHorizontal',
commandOptions: {},
context: 'CORNERSTONE',
},
],
'Flip Horizontal'
),
_createToggleButton(
'StackImageSync',
'link',
'Stack Image Sync',
[
{
commandName: 'toggleStackImageSync',
},
],
'Enable position synchronization on stack viewports',
{
listeners: {
[EVENTS.STACK_VIEWPORT_NEW_STACK]: {
commandName: 'toggleStackImageSync',
commandOptions: { toggledState: true },
},
},
}
),
_createToggleButton(
'ReferenceLines',
'tool-referenceLines', // change this with the new icon
'Reference Lines',
ReferenceLinesCommands,
'Show Reference Lines',
{
listeners: {
[EVENTS.STACK_VIEWPORT_NEW_STACK]: ReferenceLinesCommands,
[EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: ReferenceLinesCommands,
},
}
),
_createToggleButton(
'ImageOverlayViewer',
'toggle-dicom-overlay',
'Image Overlay',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'ImageOverlayViewer',
},
context: 'CORNERSTONE',
},
],
'Image Overlay',
{ isActive: true }
),
_createToolButton(
'StackScroll',
'tool-stack-scroll',
'Stack Scroll',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'StackScroll',
},
context: 'CORNERSTONE',
},
],
'Stack Scroll'
),
_createActionButton(
'invert',
'tool-invert',
'Invert',
[
{
commandName: 'invertViewport',
commandOptions: {},
context: 'CORNERSTONE',
},
],
'Invert Colors'
),
_createToolButton(
'Probe',
'tool-probe',
'Probe',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'DragProbe',
},
context: 'CORNERSTONE',
},
],
'Probe'
),
_createToggleButton(
'cine',
'tool-cine',
'Cine',
[
{
commandName: 'toggleCine',
context: 'CORNERSTONE',
},
],
'Cine'
),
_createToolButton(
'Angle',
'tool-angle',
'Angle',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'Angle',
},
context: 'CORNERSTONE',
},
],
'Angle'
),
// Next two tools can be added once icons are added
// _createToolButton(
// 'Cobb Angle',
// 'tool-cobb-angle',
// 'Cobb Angle',
// [
// {
// commandName: 'setToolActive',
// commandOptions: {
// toolName: 'CobbAngle',
// },
// context: 'CORNERSTONE',
// },
// ],
// 'Cobb Angle'
// ),
// _createToolButton(
// 'Planar Freehand ROI',
// 'tool-freehand',
// 'PlanarFreehandROI',
// [
// {
// commandName: 'setToolActive',
// commandOptions: {
// toolName: 'PlanarFreehandROI',
// },
// context: 'CORNERSTONE',
// },
// ],
// 'Planar Freehand ROI'
// ),
_createToolButton(
'Magnify',
'tool-magnify',
'Magnify',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'Magnify',
},
context: 'CORNERSTONE',
},
],
'Magnify'
),
_createToolButton(
'Rectangle',
'tool-rectangle',
'Rectangle',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'RectangleROI',
},
context: 'CORNERSTONE',
},
],
'Rectangle'
),
_createToolButton(
'CalibrationLine',
'tool-calibration',
'Calibration',
[
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'CalibrationLine',
},
context: 'CORNERSTONE',
},
],
'Calibration Line'
),
_createActionButton(
'TagBrowser',
'list-bullets',
'Dicom Tag Browser',
[
{
commandName: 'openDICOMTagViewer',
commandOptions: {},
context: 'DEFAULT',
},
],
'Dicom Tag Browser'
),
],
},
},
];
export default toolbarButtons;

View File

@ -48,6 +48,7 @@ describe('OHIF Measurement Panel', function () {
cy.scrollToIndex(13);
// Reset to default tool so that the new add length works
cy.addLengthMeasurement([100, 100], [200, 200]); //Adding measurement in the viewport
cy.get('@viewportInfoTopRight').should('contains.text', '(14/');

View File

@ -88,19 +88,7 @@ describe('OHIF MPR', () => {
});
it('should correctly render Crosshairs for MPR', () => {
cy.wait(250);
cy.get('[data-cy="Crosshairs"]').click();
cy.window()
.its('cornerstoneTools')
.then(cornerstoneTools => {
const state = cornerstoneTools.annotation.state.getAnnotationManager();
const fORMap = state.annotations;
// it should not have crosshairs yet
expect(Object.keys(fORMap)).to.have.length(0);
});
cy.get('[data-cy="Crosshairs"]').should('not.exist');
cy.get(':nth-child(3) > [data-cy="study-browser-thumbnail"]').dblclick();
cy.get('[data-cy="MPR"]').click();
cy.get('[data-cy="Crosshairs"]').click();
@ -131,4 +119,19 @@ describe('OHIF MPR', () => {
);
});
});
it('should activate window level when the active Crosshairs tool for MPR is clicked', () => {
cy.get(':nth-child(3) > [data-cy="study-browser-thumbnail"]').dblclick();
cy.get('[data-cy="MPR"]').click();
cy.get('[data-cy="Crosshairs"]').click();
// wait for the crosshairs tool to be active
cy.get('[data-cy="Crosshairs"].active');
// Click the crosshairs button to deactivate it.
cy.get('[data-cy="Crosshairs"]').click();
// wait for the window level button to be active
cy.get('[data-cy="WindowLevel-split-button-primary"].active');
});
});

View File

@ -262,7 +262,14 @@ Cypress.Commands.add(
cy.get('@measurementToolsBtnPrimary').as('lengthButton');
cy.get('@lengthButton').should('have.attr', 'data-tool', 'Length');
cy.get('@lengthButton').click();
cy.get('@lengthButton').then(button => {
// Only click the length tool if it is not active, in case the length tool is set up to
// toggle to inactive.
if (!button.is('.active')) {
cy.wrap(button).click();
}
});
cy.get('@lengthButton').should('have.class', 'active');

View File

@ -63,12 +63,17 @@ export default class ToolbarService extends PubSubService {
};
}
public static _createActionButton = ToolbarService._createButton.bind(null, 'action');
public static _createToggleButton = ToolbarService._createButton.bind(null, 'toggle');
public static _createToolButton = ToolbarService._createButton.bind(null, 'tool');
buttons: Record<string, Button> = {};
state: {
primaryToolId: string;
toggles: Record<string, boolean>;
groups: Record<string, unknown>;
} = { primaryToolId: 'WindowLevel', toggles: {}, groups: {} };
} = { primaryToolId: '', toggles: {}, groups: {} };
buttonSections: Record<string, unknown> = {
/**
* primary: ['Zoom', 'Wwwc'],
@ -78,6 +83,8 @@ export default class ToolbarService extends PubSubService {
_commandsManager: CommandsManager;
extensionManager: ExtensionManager;
defaultTool: Record<string, unknown>;
constructor(commandsManager: CommandsManager) {
super(EVENTS);
this._commandsManager = commandsManager;
@ -103,6 +110,19 @@ export default class ToolbarService extends PubSubService {
this.reset();
}
/**
* Sets the default tool that will be activated whenever the primary tool is
* deactivated without activating another/different tool.
* @param interaction the interaction command that will set the default tool active
*/
public setDefaultTool(interaction) {
this.defaultTool = interaction;
}
public getDefaultTool() {
return this.defaultTool;
}
/**
*
* @param {*} interaction - can be undefined to run nothing
@ -125,25 +145,20 @@ export default class ToolbarService extends PubSubService {
switch (interactionType) {
case 'action': {
commands.forEach(({ commandName, commandOptions, context }) => {
if (commandName) {
commandsManager.runCommand(
commandName,
{
...commandOptions,
...options,
},
context
);
}
});
commandsManager.run(commands, options);
break;
}
case 'tool': {
try {
commands.forEach(({ commandName = 'setToolActive', commandOptions, context }) => {
commandsManager.runCommand(commandName, commandOptions, context);
});
const alternateInteraction =
this.state.primaryToolId === itemId &&
this.defaultTool?.itemId !== itemId &&
this.getDefaultTool();
if (alternateInteraction) {
// Allow toggling the mode off
return this.recordInteraction(alternateInteraction, options);
}
commandsManager.run(commands, options);
// only set the primary tool if no error was thrown.
// if the itemId is not undefined use it; otherwise, set the first tool in
@ -312,21 +327,27 @@ export default class ToolbarService extends PubSubService {
/**
*
* Finds a button section by it's name, then maps the list of string name
* Finds a button section by it's name/tool group id, then maps the list of string name
* identifiers to schema/values that can be used to render the buttons.
*
* @param {string} key
* @param {*} props
* @param toolGroupId - the tool group id
* @param props - optional properties to apply to every button of the section
* @param defaultToolGroupId - the fallback section to return if the given toolGroupId section is not available
*/
getButtonSection(key, props) {
const buttonSectionIds = this.buttonSections[key];
getButtonSection(
toolGroupId: string,
props?: Record<string, unknown>,
defaultToolGroupId = 'primary'
) {
const buttonSectionIds =
this.buttonSections[toolGroupId] || this.buttonSections[defaultToolGroupId];
const buttonsInSection = [];
if (buttonSectionIds && buttonSectionIds.length !== 0) {
buttonSectionIds.forEach(btnId => {
const btn = this.buttons[btnId];
const metadata = {};
const mappedBtn = this._mapButtonToDisplay(btn, key, metadata, props);
const mappedBtn = this._mapButtonToDisplay(btn, toolGroupId, metadata, props);
buttonsInSection.push(mappedBtn);
});

View File

@ -46,6 +46,8 @@ button is clicked by the user.
- `getActiveTools`: returns the active tool + all the toggled-on tools
- `setDefaultTool`: sets the default tool that will be activated whenever the primary tool is deactivated without activating another/different tool
## State
ToolBarService has an internal state that gets updated per tool interaction and

View File

@ -33,6 +33,7 @@ is expected to support, [check out it's interface in `@ohif/core`][interface]
| `getNumViewportPanes()` | Gets the number of visible viewport panes |
| `getLayoutOptionsFromState(gridState)` | Utility method that produces a `ViewportLayoutOptions` based on the passed in state|
| `getActiveViewportId()` | Returns the viewport Id of the active viewport in the grid|
| `getActiveViewportOptionByKey(key)` | Gets the specified viewport option field (key) for the active viewport |
## Implementations

View File

@ -300,6 +300,11 @@ export function ViewportGridProvider({ children, service }) {
return viewportGridState;
}, [viewportGridState]);
const getActiveViewportOptionByKey = (key: string) => {
const { viewports, activeViewportId } = viewportGridState;
return viewports.get(activeViewportId)?.viewportOptions?.[key];
};
const setActiveViewportId = useCallback(
index => dispatch({ type: 'SET_ACTIVE_VIEWPORT_ID', payload: index }),
[dispatch]
@ -400,6 +405,7 @@ export function ViewportGridProvider({ children, service }) {
reset: () => service.reset(),
set: gridLayoutState => service.setState(gridLayoutState), // run it through the service itself since we want to publish events
getNumViewportPanes,
getActiveViewportOptionByKey,
};
return (