From cae54b0779c82ece372b43118c13c44f5f0a4c7c Mon Sep 17 00:00:00 2001 From: Danny Brown Date: Mon, 14 Sep 2020 21:18:46 -0400 Subject: [PATCH] Fix/update branch cherry pick (#2039) * Update all the icons * Turn off global tool sync; watch as the world burns * Shift a bunch of things around so we can start tracking/setting per element * ToolBarService to initiate one of three different calls; callback passed to all button types * SplitButton and Toolbar Button to use new `onInteraction` prop and new toolbar state * Changes to toolbar button interface and config * Fix broken layout selector * Update SR viewport to activate tools in viewport component * Duplicate activation logic in measurement tracking extension * Add alternative/dashed variants for SR Viewport * pass through "setToolActive" commands for other viewport types * fix small overlay bugs (no wwwc or scale info) * Show SpacingBetweenSlices instead of PixelSpacing in patient information dialog * Fix prop-types * Update tracked viewport to have alternative tracked styling * alt styling for SR viewports * Update to support isLocked + isRehydratable * fix broken logic * fix broken logic * switch icon style * fix icon styles * hover and click to start flow * expedited workflow when data is not dirty (just after SR hydration) * fix: setting elliptical roi tool * fix arrow annotate dialog * fix: do not show learn more button for now * remove dead code * simpler cache invalidation * simpler cache invalidation part 2 * Fix for unable to spand study cards on separate pages --- .../src/CornerstoneViewportDownloadForm.js | 4 +- .../src/OHIFCornerstoneViewport.js | 13 +- extensions/cornerstone/src/callInputDialog.js | 82 ++++ extensions/cornerstone/src/commandsModule.js | 40 +- extensions/cornerstone/src/index.js | 14 +- extensions/cornerstone/src/init.js | 304 ++++-------- extensions/cornerstone/src/state.js | 18 +- extensions/cornerstone/src/utils/getTools.js | 36 ++ .../setActiveAndPassiveToolsForElement.js | 21 + .../src/Toolbar/ToolbarLayoutSelector.jsx | 5 +- .../src/Toolbar/ToolbarSplitButton.jsx | 3 + extensions/default/src/ViewerLayout/index.jsx | 214 +++------ extensions/default/src/getToolbarModule.js | 57 +-- .../dicom-sr/src/OHIFCornerstoneSRViewport.js | 128 ++++- .../dicom-sr/src/getSopClassHandlerModule.js | 4 +- extensions/dicom-sr/src/index.js | 46 ++ extensions/dicom-sr/src/init.js | 3 - .../src/_shared/getTools.js | 36 ++ .../setActiveAndPassiveToolsForElement.js | 21 + .../TrackedMeasurementsContext.jsx | 2 +- .../measurementTrackingMachine.js | 35 +- .../promptTrackNewSeries.js | 4 +- .../promptTrackNewStudy.js | 4 +- extensions/measurement-tracking/src/index.js | 21 + .../PanelMeasurementTableTracking/index.js | 5 +- .../PanelStudyBrowserTracking.jsx | 45 +- .../viewports/TrackedCornerstoneViewport.js | 40 +- modes/longitudinal/src/index.js | 26 +- modes/longitudinal/src/toolbarButtons.js | 448 +++++++----------- modes/segmentation/src/toolbarButtons.js | 20 +- platform/core/src/classes/CommandsManager.js | 16 +- platform/core/src/services/ServicesManager.js | 4 +- .../services/ToolBarService/ToolBarService.js | 134 ++++-- .../core/src/services/ToolBarService/index.js | 4 +- platform/ui/src/assets/icons/arrow-left.svg | 6 + platform/ui/src/assets/icons/exclamation.svg | 9 + .../ui/src/assets/icons/old-angle-left.svg | 9 - platform/ui/src/assets/icons/old-bars.svg | 9 - platform/ui/src/assets/icons/old-circle-o.svg | 9 - platform/ui/src/assets/icons/old-circle.svg | 9 - .../ui/src/assets/icons/old-dot-circle.svg | 9 - .../ui/src/assets/icons/old-ellipse-h.svg | 9 - platform/ui/src/assets/icons/old-invert.svg | 9 - platform/ui/src/assets/icons/old-reset.svg | 9 - .../ui/src/assets/icons/old-rotate-right.svg | 9 - platform/ui/src/assets/icons/old-youtube.svg | 9 - platform/ui/src/assets/icons/tool-angle.svg | 7 + .../ui/src/assets/icons/tool-annotate.svg | 4 +- .../src/assets/icons/tool-bidirectional.svg | 15 +- platform/ui/src/assets/icons/tool-capture.svg | 2 +- platform/ui/src/assets/icons/tool-cine.svg | 6 + platform/ui/src/assets/icons/tool-elipse.svg | 6 +- .../src/assets/icons/tool-flip-horizontal.svg | 7 + platform/ui/src/assets/icons/tool-invert.svg | 6 + platform/ui/src/assets/icons/tool-length.svg | 7 +- platform/ui/src/assets/icons/tool-magnify.svg | 6 + platform/ui/src/assets/icons/tool-move.svg | 12 +- platform/ui/src/assets/icons/tool-probe.svg | 7 + .../ui/src/assets/icons/tool-rectangle.svg | 8 + platform/ui/src/assets/icons/tool-reset.svg | 6 + .../ui/src/assets/icons/tool-rotate-right.svg | 9 + .../ui/src/assets/icons/tool-stack-scroll.svg | 7 + .../ui/src/assets/icons/tool-window-level.svg | 4 +- platform/ui/src/assets/icons/tool-zoom.svg | 7 +- platform/ui/src/components/Icon/getIcon.jsx | 44 +- .../components/SplitButton/SplitButton.jsx | 260 ++++++---- .../StudyListFilter/StudyListFilter.jsx | 40 +- .../ToolbarButton/ToolbarButton.jsx | 27 +- .../ui/src/components/Tooltip/Tooltip.jsx | 13 +- .../ui/src/components/Viewport/Viewport.jsx | 2 + .../ui/src/components/Viewport/Viewport.mdx | 1 + .../ViewportActionBar/ViewportActionBar.jsx | 176 +++++-- .../ViewportActionBar/ViewportActionBar.mdx | 67 +-- platform/ui/src/views/Viewer/Viewer.mdx.todo | 1 + platform/ui/tailwind.config.js | 9 + platform/viewer/src/appInit.js | 14 +- .../viewer/src/routes/DataSourceWrapper.jsx | 102 ++-- .../viewer/src/routes/WorkList/WorkList.jsx | 97 ++-- 78 files changed, 1678 insertions(+), 1263 deletions(-) create mode 100644 extensions/cornerstone/src/callInputDialog.js create mode 100644 extensions/cornerstone/src/utils/getTools.js create mode 100644 extensions/cornerstone/src/utils/setActiveAndPassiveToolsForElement.js create mode 100644 extensions/default/src/Toolbar/ToolbarSplitButton.jsx create mode 100644 extensions/measurement-tracking/src/_shared/getTools.js create mode 100644 extensions/measurement-tracking/src/_shared/setActiveAndPassiveToolsForElement.js create mode 100644 platform/ui/src/assets/icons/arrow-left.svg create mode 100644 platform/ui/src/assets/icons/exclamation.svg delete mode 100644 platform/ui/src/assets/icons/old-angle-left.svg delete mode 100644 platform/ui/src/assets/icons/old-bars.svg delete mode 100644 platform/ui/src/assets/icons/old-circle-o.svg delete mode 100644 platform/ui/src/assets/icons/old-circle.svg delete mode 100644 platform/ui/src/assets/icons/old-dot-circle.svg delete mode 100644 platform/ui/src/assets/icons/old-ellipse-h.svg delete mode 100644 platform/ui/src/assets/icons/old-invert.svg delete mode 100644 platform/ui/src/assets/icons/old-reset.svg delete mode 100644 platform/ui/src/assets/icons/old-rotate-right.svg delete mode 100644 platform/ui/src/assets/icons/old-youtube.svg create mode 100644 platform/ui/src/assets/icons/tool-angle.svg create mode 100644 platform/ui/src/assets/icons/tool-cine.svg create mode 100644 platform/ui/src/assets/icons/tool-flip-horizontal.svg create mode 100644 platform/ui/src/assets/icons/tool-invert.svg create mode 100644 platform/ui/src/assets/icons/tool-magnify.svg create mode 100644 platform/ui/src/assets/icons/tool-probe.svg create mode 100644 platform/ui/src/assets/icons/tool-rectangle.svg create mode 100644 platform/ui/src/assets/icons/tool-reset.svg create mode 100644 platform/ui/src/assets/icons/tool-rotate-right.svg create mode 100644 platform/ui/src/assets/icons/tool-stack-scroll.svg diff --git a/extensions/cornerstone/src/CornerstoneViewportDownloadForm.js b/extensions/cornerstone/src/CornerstoneViewportDownloadForm.js index 7582c0960..1aeccd18c 100644 --- a/extensions/cornerstone/src/CornerstoneViewportDownloadForm.js +++ b/extensions/cornerstone/src/CornerstoneViewportDownloadForm.js @@ -13,7 +13,9 @@ const DEFAULT_SIZE = 512; const MAX_TEXTURE_SIZE = 10000; const CornerstoneViewportDownloadForm = ({ onClose, activeViewportIndex }) => { - const activeEnabledElement = getEnabledElement(activeViewportIndex); + const { enabledElement: activeEnabledElement } = getEnabledElement( + activeViewportIndex + ); const enableViewport = viewportElement => { if (viewportElement) { diff --git a/extensions/cornerstone/src/OHIFCornerstoneViewport.js b/extensions/cornerstone/src/OHIFCornerstoneViewport.js index 6c2a03547..e06447784 100644 --- a/extensions/cornerstone/src/OHIFCornerstoneViewport.js +++ b/extensions/cornerstone/src/OHIFCornerstoneViewport.js @@ -1,8 +1,11 @@ import React, { Component } from 'react'; import CornerstoneViewport from 'react-cornerstone-viewport'; import OHIF from '@ohif/core'; +import csTools from 'cornerstone-tools'; import PropTypes from 'prop-types'; import debounce from 'lodash.debounce'; +import getTools from './utils/getTools.js'; +import setActiveAndPassiveToolsForElement from './utils/setActiveAndPassiveToolsForElement'; import { setEnabledElement } from './state'; @@ -23,6 +26,7 @@ class OHIFCornerstoneViewport extends Component { dataSource: PropTypes.object, children: PropTypes.node, customProps: PropTypes.object, + ToolBarService: PropTypes.object, }; static name = 'OHIFCornerstoneViewport'; @@ -131,7 +135,7 @@ class OHIFCornerstoneViewport extends Component { if ( displaySet.displaySetInstanceUID !== - prevDisplaySet.displaySetInstanceUID || + prevDisplaySet.displaySetInstanceUID || displaySet.SOPInstanceUID !== prevDisplaySet.SOPInstanceUID || displaySet.imageIndex !== prevDisplaySet.imageIndex ) { @@ -203,7 +207,14 @@ class OHIFCornerstoneViewport extends Component { // Need to expose viewportGrid as a "UI Service" onElementEnabled={evt => { const enabledElement = evt.detail.element; + const tools = getTools(); + const toolAlias = ToolBarService.state.primaryToolId; + setEnabledElement(viewportIndex, enabledElement); + setActiveAndPassiveToolsForElement(enabledElement, tools); + csTools.setToolActiveForElement(enabledElement, toolAlias, { + mouseButtonMask: 1, + }); }} // Sync resize throttle w/ sidepanel animation duration to prevent // seizure inducing strobe blinking effect diff --git a/extensions/cornerstone/src/callInputDialog.js b/extensions/cornerstone/src/callInputDialog.js new file mode 100644 index 000000000..a996f18c6 --- /dev/null +++ b/extensions/cornerstone/src/callInputDialog.js @@ -0,0 +1,82 @@ +import React from 'react'; +import { Input, Dialog } from '@ohif/ui'; + +/** + * + * @param {*} data + * @param {*} data.text + * @param {*} data.label + * @param {*} event + * @param {*} callback + * @param {*} isArrowAnnotateInputDialog + */ +function callInputDialog( + UIDialogService, + data, + callback, + isArrowAnnotateInputDialog = true +) { + const dialogId = 'enter-annotation'; + const label = data + ? isArrowAnnotateInputDialog + ? data.text + : data.label + : ''; + + const onSubmitHandler = ({ action, value }) => { + switch (action.id) { + case 'save': + callback(value.label, action.id); + break; + case 'cancel': + callback('', action.id); + break; + } + UIDialogService.dismiss({ id: dialogId }); + }; + + if (UIDialogService) { + UIDialogService.create({ + id: dialogId, + centralize: true, + isDraggable: false, + showOverlay: true, + content: Dialog, + contentProps: { + title: 'Enter your annotation', + value: { label }, + noCloseButton: true, + onClose: () => UIDialogService.dismiss({ id: dialogId }), + actions: [ + { id: 'cancel', text: 'Cancel', type: 'secondary' }, + { id: 'save', text: 'Save', type: 'primary' }, + ], + onSubmit: onSubmitHandler, + body: ({ value, setValue }) => { + return ( +
+ { + event.persist(); + setValue(value => ({ ...value, label: event.target.value })); + }} + onKeyPress={event => { + if (event.key === 'Enter') { + onSubmitHandler({ value, action: { id: 'save' } }); + } + }} + /> +
+ ); + }, + }, + }); + } +} + +export default callInputDialog; diff --git a/extensions/cornerstone/src/commandsModule.js b/extensions/cornerstone/src/commandsModule.js index 00d4098cd..676aef487 100644 --- a/extensions/cornerstone/src/commandsModule.js +++ b/extensions/cornerstone/src/commandsModule.js @@ -10,12 +10,13 @@ const scroll = cornerstoneTools.import('util/scroll'); const { studyMetadataManager } = OHIF.utils; const { setViewportSpecificData } = OHIF.redux.actions; -const commandsModule = ({ servicesManager }) => { +const commandsModule = ({ servicesManager, commandsManager }) => { const { ViewportGridService } = servicesManager.services; function _getActiveViewportsEnabledElement() { const { activeViewportIndex } = ViewportGridService.getState(); - return getEnabledElement(activeViewportIndex); + const { element } = getEnabledElement(activeViewportIndex) || {}; + return element; } const actions = { @@ -98,7 +99,38 @@ const commandsModule = ({ servicesManager }) => { if (!toolName) { console.warn('No toolname provided to setToolActive command'); } - cornerstoneTools.setToolActive(toolName, { mouseButtonMask: 1 }); + + // Find total number of tool indexes + const { viewports } = ViewportGridService.getState(); + for (let i = 0; i < viewports.length; i++) { + const viewport = viewports[i]; + const hasDisplaySet = viewport.displaySetInstanceUID !== undefined; + + if (!hasDisplaySet) { + continue; + } + + const viewportInfo = getEnabledElement(i); + const hasCornerstoneContext = + viewportInfo.context == 'ACTIVE_VIEWPORT::CORNERSTONE'; + + if (hasCornerstoneContext) { + cornerstoneTools.setToolActiveForElement( + viewportInfo.enabledElement, + toolName, + { mouseButtonMask: 1 } + ); + } else { + commandsManager.runCommand( + 'setToolActive', + { + element: viewportInfo.element, + toolName, + }, + viewportInfo.context + ); + } + } }, clearAnnotations: () => { const element = _getActiveViewportsEnabledElement(); @@ -106,7 +138,7 @@ const commandsModule = ({ servicesManager }) => { return; } - const enabledElement = cornerstone.getEnabledElement(element); + const { enabledElement } = cornerstone.getEnabledElement(element) || {}; if (!enabledElement || !enabledElement.image) { return; } diff --git a/extensions/cornerstone/src/index.js b/extensions/cornerstone/src/index.js index 0d0ec725f..c85d2c7b7 100644 --- a/extensions/cornerstone/src/index.js +++ b/extensions/cornerstone/src/index.js @@ -33,13 +33,19 @@ export default { preRegistration({ servicesManager, commandsManager, configuration = {} }) { init({ servicesManager, commandsManager, configuration }); }, - getViewportModule({ commandsManager }) { + getViewportModule({ servicesManager, commandsManager }) { const ExtendedOHIFCornerstoneViewport = props => { const onNewImageHandler = jumpData => { commandsManager.runCommand('jumpToImage', jumpData); }; + const { ToolBarService } = servicesManager; + return ( - + ); }; @@ -47,8 +53,8 @@ export default { { name: 'cornerstone', component: ExtendedOHIFCornerstoneViewport }, ]; }, - getCommandsModule({ servicesManager }) { - return commandsModule({ servicesManager }); + getCommandsModule({ servicesManager, commandsManager }) { + return commandsModule({ servicesManager, commandsManager }); }, }; diff --git a/extensions/cornerstone/src/init.js b/extensions/cornerstone/src/init.js index e65efa36b..bc0e1f686 100644 --- a/extensions/cornerstone/src/init.js +++ b/extensions/cornerstone/src/init.js @@ -1,19 +1,19 @@ -import React from 'react'; import OHIF from '@ohif/core'; -import { Input, Dialog, ContextMenuMeasurements } from '@ohif/ui'; +import { ContextMenuMeasurements } from '@ohif/ui'; import cs from 'cornerstone-core'; import csTools from 'cornerstone-tools'; import merge from 'lodash.merge'; +import getTools, { toolsGroupedByType } from './utils/getTools.js'; import initCornerstoneTools from './initCornerstoneTools.js'; import './initWADOImageLoader.js'; import getCornerstoneMeasurementById from './utils/getCornerstoneMeasurementById'; import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory'; import { setEnabledElement } from './state'; +import callInputDialog from './callInputDialog.js'; // TODO -> Global "context menu open state", or lots of expensive searches on drag? let CONTEXT_MENU_OPEN = false; - const { globalImageIdSpecificToolStateManager } = csTools; const TOOL_TYPES_WITH_CONTEXT_MENU = [ @@ -30,6 +30,24 @@ const TOOL_TYPES_WITH_CONTEXT_MENU = [ const _refreshViewports = () => cs.getEnabledElements().forEach(({ element }) => cs.updateImage(element)); +/* Add extension tools configuration here. */ +const _createInternalToolsConfig = UIDialogService => { + return { + ArrowAnnotate: { + configuration: { + getTextCallback: (callback, eventDetails) => + callInputDialog(UIDialogService, null, callback), + changeTextCallback: (data, eventDetails, callback) => + callInputDialog(UIDialogService, data, callback), + allowEmptyLabel: true, + }, + }, + DragProbe: { + defaultStrategy: 'minimal', + }, + }; +}; + /** * * @param {Object} servicesManager @@ -45,7 +63,11 @@ export default function init({ UIDialogService, MeasurementService, DisplaySetService, + ToolBarService, } = servicesManager.services; + const tools = getTools(); + + console.log(servicesManager.services); /* Measurement Service */ const measurementServiceSource = _connectToolsToMeasurementService( @@ -112,6 +134,7 @@ export default function init({ ); callInputDialog( + UIDialogService, measurement, (label, actionId) => { if (actionId === 'cancel') { @@ -180,8 +203,18 @@ export default function init({ } }; - function elementEnabledHandler(evt) { + // TODO: This is the handler for ALL ENABLED ELEMENT EVENTS + // ... Activation logic should take place per element, not for all (diff behavior per ext) + function elementEnabledHandler(tools, evt) { const element = evt.detail.element; + + _addConfiguredToolsForElement( + UIDialogService, + element, + tools, + configuration + ); + element.addEventListener(csTools.EVENTS.TOUCH_PRESS, onTouchPress); element.addEventListener( csTools.EVENTS.MOUSE_CLICK, @@ -200,85 +233,6 @@ export default function init({ element.removeEventListener(cs.EVENTS.NEW_IMAGE, cancelContextMenuIfOpen); } - /** - * - * @param {*} data - * @param {*} event - * @param {*} callback - * @param {*} isArrowAnnotateInputDialog - */ - const callInputDialog = ( - data, - callback, - isArrowAnnotateInputDialog = true - ) => { - const dialogId = 'enter-annotation'; - const label = data - ? isArrowAnnotateInputDialog - ? data.text - : data.label - : ''; - - const onSubmitHandler = ({ action, value }) => { - switch (action.id) { - case 'save': - callback(value.label, action.id); - break; - case 'cancel': - callback('', action.id); - break; - } - UIDialogService.dismiss({ id: dialogId }); - }; - - if (UIDialogService) { - UIDialogService.create({ - id: dialogId, - centralize: true, - isDraggable: false, - showOverlay: true, - content: Dialog, - contentProps: { - title: 'Enter your annotation', - value: { label }, - noCloseButton: true, - onClose: () => UIDialogService.dismiss({ id: dialogId }), - actions: [ - { id: 'cancel', text: 'Cancel', type: 'secondary' }, - { id: 'save', text: 'Save', type: 'primary' }, - ], - onSubmit: onSubmitHandler, - body: ({ value, setValue }) => { - const onChangeHandler = event => { - event.persist(); - setValue(value => ({ ...value, label: event.target.value })); - }; - - const onKeyPressHandler = event => { - if (event.key === 'Enter') { - onSubmitHandler({ value, action: { id: 'save' } }); - } - }; - - return ( -
- -
- ); - }, - }, - }); - } - }; - const { csToolsConfig } = configuration; const metadataProvider = OHIF.cornerstone.metadataProvider; @@ -286,7 +240,7 @@ export default function init({ // ~~ const defaultCsToolsConfig = csToolsConfig || { - globalToolSyncEnabled: true, + globalToolSyncEnabled: false, // hold on to your pants! showSVGCursors: false, autoResizeViewports: false, }; @@ -298,10 +252,10 @@ export default function init({ // THIS // is a way for extensions that "depend" on this extension to notify it of // new cornerstone enabled elements so it's commands continue to work. - const handleOhifCornerstoneEnabledElementEvent = function (evt) { - const { viewportIndex, enabledElement } = evt.detail; + const handleOhifCornerstoneEnabledElementEvent = function(evt) { + const { context, viewportIndex, enabledElement } = evt.detail; - setEnabledElement(viewportIndex, enabledElement); + setEnabledElement(viewportIndex, enabledElement, context); }; document.addEventListener( @@ -309,120 +263,10 @@ export default function init({ handleOhifCornerstoneEnabledElementEvent ); - const toolsGroupedByType = { - touch: [csTools.PanMultiTouchTool, csTools.ZoomTouchPinchTool], - annotations: [ - csTools.ArrowAnnotateTool, - csTools.BidirectionalTool, - csTools.LengthTool, - csTools.AngleTool, - csTools.FreehandRoiTool, - csTools.EllipticalRoiTool, - csTools.DragProbeTool, - csTools.RectangleRoiTool, - ], - other: [ - csTools.PanTool, - csTools.ZoomTool, - csTools.WwwcTool, - csTools.WwwcRegionTool, - csTools.MagnifyTool, - csTools.StackScrollTool, - csTools.StackScrollMouseWheelTool, - csTools.OverlayTool, - ], - }; - - let tools = []; - Object.keys(toolsGroupedByType).forEach(toolsGroup => - tools.push(...toolsGroupedByType[toolsGroup]) + cs.events.addEventListener( + cs.EVENTS.ELEMENT_ENABLED, + elementEnabledHandler.bind(null, tools) ); - - /* Add extension tools configuration here. */ - const internalToolsConfig = { - ArrowAnnotate: { - configuration: { - getTextCallback: (callback, eventDetails) => - callInputDialog(null, callback), - changeTextCallback: (data, eventDetails, callback) => - callInputDialog(data, callback), - allowEmptyLabel: true, - }, - }, - DragProbe: { - defaultStrategy: 'minimal', - }, - }; - - /* Abstract tools configuration using extension configuration. */ - const parseToolProps = (props, tool) => { - const { annotations } = toolsGroupedByType; - // An alternative approach would be to remove the `drawHandlesOnHover` config - // from the supported configuration properties in `cornerstone-tools` - const toolsWithHideableHandles = annotations.filter( - tool => !['RectangleRoiTool', 'EllipticalRoiTool'].includes(tool.name) - ); - - let parsedProps = { ...props }; - - /** - * drawHandles - Never/Always show handles - * drawHandlesOnHover - Only show handles on handle hover (pointNearHandle) - * hideHandlesIfMoving - Hides the handles whilst you are moving them, for better visibility. - * - * Does not apply to tools where handles aren't placed in predictable - * locations. - */ - if ( - configuration.hideHandles !== false && - toolsWithHideableHandles.includes(tool) - ) { - if (props.configuration) { - parsedProps.configuration.drawHandlesOnHover = true; - parsedProps.configuration.hideHandlesIfMoving = true; - } else { - parsedProps.configuration = { - drawHandlesOnHover: true, - hideHandlesIfMoving: true, - }; - } - } - - return parsedProps; - }; - - /* Add tools with its custom props through extension configuration. */ - tools.forEach(tool => { - const toolName = new tool().name; - const externalToolsConfig = configuration.tools || {}; - const externalToolProps = externalToolsConfig[toolName] || {}; - const internalToolProps = internalToolsConfig[toolName] || {}; - const props = merge( - internalToolProps, - parseToolProps(externalToolProps, tool) - ); - csTools.addTool(tool, props); - }); - - // TODO -> We need a better way to do this with maybe global tool state setting all tools passive. - const BaseAnnotationTool = csTools.importInternal('base/BaseAnnotationTool'); - tools.forEach(tool => { - if (tool.prototype instanceof BaseAnnotationTool) { - // BaseAnnotationTool would likely come from csTools lib exports - const toolName = new tool().name; - csTools.setToolPassive(toolName); // there may be a better place to determine name; may not be on uninstantiated class - } - }); - - csTools.setToolActive('Pan', { mouseButtonMask: 4 }); - csTools.setToolActive('Zoom', { mouseButtonMask: 2 }); - csTools.setToolActive('Wwwc', { mouseButtonMask: 1 }); - csTools.setToolActive('StackScrollMouseWheel', {}); // TODO: Empty options should not be required - csTools.setToolActive('PanMultiTouch', { pointers: 2 }); // TODO: Better error if no options - csTools.setToolActive('ZoomTouchPinch', {}); - csTools.setToolEnabled('Overlay', {}); - - cs.events.addEventListener(cs.EVENTS.ELEMENT_ENABLED, elementEnabledHandler); cs.events.addEventListener( cs.EVENTS.ELEMENT_DISABLED, elementDisabledHandler @@ -646,3 +490,63 @@ const _getDefaultPosition = event => ({ x: (event && event.currentPoints.client.x) || 0, y: (event && event.currentPoints.client.y) || 0, }); + +/** + * @private + */ +function _addConfiguredToolsForElement( + UIDialogService, + element, + tools, + configuration +) { + const internalToolsConfig = _createInternalToolsConfig(UIDialogService); + /* Add tools with its custom props through extension configuration. */ + tools.forEach(tool => { + const toolName = new tool().name; + const externalToolsConfig = configuration.tools || {}; + const externalToolProps = externalToolsConfig[toolName] || {}; + const internalToolProps = internalToolsConfig[toolName] || {}; + const props = merge( + internalToolProps, + _parseToolProps(configuration, externalToolProps, tool) + ); + csTools.addToolForElement(element, tool, props); + }); +} +/* Abstract tools configuration using extension configuration. */ +function _parseToolProps(configuration, props, tool) { + const { annotations } = toolsGroupedByType; + // An alternative approach would be to remove the `drawHandlesOnHover` config + // from the supported configuration properties in `cornerstone-tools` + const toolsWithHideableHandles = annotations.filter( + tool => !['RectangleRoiTool', 'EllipticalRoiTool'].includes(tool.name) + ); + + let parsedProps = { ...props }; + + /** + * drawHandles - Never/Always show handles + * drawHandlesOnHover - Only show handles on handle hover (pointNearHandle) + * hideHandlesIfMoving - Hides the handles whilst you are moving them, for better visibility. + * + * Does not apply to tools where handles aren't placed in predictable + * locations. + */ + if ( + configuration.hideHandles !== false && + toolsWithHideableHandles.includes(tool) + ) { + if (props.configuration) { + parsedProps.configuration.drawHandlesOnHover = true; + parsedProps.configuration.hideHandlesIfMoving = true; + } else { + parsedProps.configuration = { + drawHandlesOnHover: true, + hideHandlesIfMoving: true, + }; + } + } + + return parsedProps; +} diff --git a/extensions/cornerstone/src/state.js b/extensions/cornerstone/src/state.js index c7139a00b..fa87917c8 100644 --- a/extensions/cornerstone/src/state.js +++ b/extensions/cornerstone/src/state.js @@ -1,4 +1,6 @@ const state = { + // The `defaultContext` of an extension's commandsModule + DEFAULT_CONTEXT: 'ACTIVE_VIEWPORT::CORNERSTONE', enabledElements: {}, }; @@ -7,14 +9,22 @@ const state = { * @param {HTMLElement} dom Active viewport element. * @return void */ -const setEnabledElement = (viewportIndex, element) => - (state.enabledElements[viewportIndex] = element); +const setEnabledElement = (viewportIndex, element, context) => { + const targetContext = context || DEFAULT_CONTEXT; + + state.enabledElements[viewportIndex] = { + element, + context: targetContext, + }; +}; /** - * Grabs the enabled element `dom` reference of an active viewport. + * Grabs the enabled element `dom` reference of an adective viewport. * * @return {HTMLElement} Active viewport element. */ -const getEnabledElement = viewportIndex => state.enabledElements[viewportIndex]; +const getEnabledElement = viewportIndex => { + return state.enabledElements[viewportIndex]; +}; export { setEnabledElement, getEnabledElement }; diff --git a/extensions/cornerstone/src/utils/getTools.js b/extensions/cornerstone/src/utils/getTools.js new file mode 100644 index 000000000..c3d9b9e77 --- /dev/null +++ b/extensions/cornerstone/src/utils/getTools.js @@ -0,0 +1,36 @@ +import csTools from 'cornerstone-tools'; + +const toolsGroupedByType = { + touch: [csTools.PanMultiTouchTool, csTools.ZoomTouchPinchTool], + annotations: [ + csTools.ArrowAnnotateTool, + csTools.BidirectionalTool, + csTools.LengthTool, + csTools.AngleTool, + csTools.FreehandRoiTool, + csTools.EllipticalRoiTool, + csTools.DragProbeTool, + csTools.RectangleRoiTool, + ], + other: [ + csTools.PanTool, + csTools.ZoomTool, + csTools.WwwcTool, + csTools.WwwcRegionTool, + csTools.MagnifyTool, + csTools.StackScrollTool, + csTools.StackScrollMouseWheelTool, + csTools.OverlayTool, + ], +}; + +export default function getTools() { + const tools = []; + Object.keys(toolsGroupedByType).forEach(toolsGroup => + tools.push(...toolsGroupedByType[toolsGroup]) + ); + + return tools; +} + +export { toolsGroupedByType }; diff --git a/extensions/cornerstone/src/utils/setActiveAndPassiveToolsForElement.js b/extensions/cornerstone/src/utils/setActiveAndPassiveToolsForElement.js new file mode 100644 index 000000000..b8348c990 --- /dev/null +++ b/extensions/cornerstone/src/utils/setActiveAndPassiveToolsForElement.js @@ -0,0 +1,21 @@ +import csTools from 'cornerstone-tools'; + +export default function _setActiveAndPassiveToolsForElement(element, tools) { + const BaseAnnotationTool = csTools.importInternal('base/BaseAnnotationTool'); + + tools.forEach(tool => { + if (tool.prototype instanceof BaseAnnotationTool) { + // BaseAnnotationTool would likely come from csTools lib exports + const toolName = new tool().name; + csTools.setToolPassiveForElement(element, toolName); // there may be a better place to determine name; may not be on uninstantiated class + } + }); + + csTools.setToolActiveForElement(element, 'Pan', { mouseButtonMask: 4 }); + csTools.setToolActiveForElement(element, 'Zoom', { mouseButtonMask: 2 }); + csTools.setToolActiveForElement(element, 'Wwwc', { mouseButtonMask: 1 }); + csTools.setToolActiveForElement(element, 'StackScrollMouseWheel', {}); // TODO: Empty options should not be required + csTools.setToolActiveForElement(element, 'PanMultiTouch', { pointers: 2 }); // TODO: Better error if no options + csTools.setToolActiveForElement(element, 'ZoomTouchPinch', {}); + csTools.setToolEnabledForElement(element, 'Overlay', {}); +} diff --git a/extensions/default/src/Toolbar/ToolbarLayoutSelector.jsx b/extensions/default/src/Toolbar/ToolbarLayoutSelector.jsx index 898d74651..47358ccc2 100644 --- a/extensions/default/src/Toolbar/ToolbarLayoutSelector.jsx +++ b/extensions/default/src/Toolbar/ToolbarLayoutSelector.jsx @@ -29,8 +29,7 @@ function LayoutSelector() { }; }, []); - const onClickHandler = () => setIsOpen(!isOpen); - + const onInteractionHandler = () => setIsOpen(!isOpen); const DropdownContent = isOpen ? OHIFLayoutSelector : null; return ( @@ -38,7 +37,7 @@ function LayoutSelector() { id="Layout" label="Grid Layout" icon="tool-layout" - onClick={onClickHandler} + onInteraction={onInteractionHandler} dropdownContent={ DropdownContent !== null && ( { - setActiveTool(isNested ? tool : defaultTool); - }; - - const onPrimaryClickHandler = (evt, btn) => { - if ( - btn.props && - btn.props.commands && - evt.value && - btn.props.commands[evt.value] - ) { - const { commandName, commandOptions } = btn.props.commands[evt.value]; - commandsManager.runCommand(commandName, commandOptions); - } - }; + const [toolbarButtons, setToolbarButtons] = useState([]); + const [buttonState, setButtonState] = useState({ + primaryToolId: '', + toggles: {}, + groups: {}, + }); + // Could track buttons and state separately...? useEffect(() => { - const { unsubscribe } = ToolBarService.subscribe( + const { unsubscribe: unsub1 } = ToolBarService.subscribe( ToolBarService.EVENTS.TOOL_BAR_MODIFIED, - () => { - console.warn('~~~ TOOL BAR MODIFIED EVENT CAUGHT'); - const updatedToolbars = { - primary: ToolBarService.getButtonSection('primary', { - onClick: onPrimaryClickHandler, - setActiveTool: setActiveToolHandler, - }), - secondary: ToolBarService.getButtonSection('secondary', { - setActiveTool: setActiveToolHandler, - }), - }; - setToolbars(updatedToolbars); - } + () => setToolbarButtons(ToolBarService.getButtonSection('primary')) + ); + const { unsubscribe: unsub2 } = ToolBarService.subscribe( + ToolBarService.EVENTS.TOOL_BAR_STATE_MODIFIED, + () => setButtonState({ ...ToolBarService.state }) ); - return unsubscribe; + return () => { + unsub1(); + unsub2(); + }; }, [ToolBarService]); - return <> - {toolbars.primary.map((toolDef, index) => { - const isNested = Array.isArray(toolDef); - if (!isNested) { + return ( + <> + {toolbarButtons.map((toolDef, index) => { const { id, Component, componentProps } = toolDef; - return ; - } else { + // TODO: ... + + // isActive if: + // - id is primary? + // - id is in list of "toggled on"? + + // Also need... to filter list for splitButton, and set primary based on most recently clicked + // Also need to kill the radioGroup button's magic logic + // Everything should be reactive off these props, so commands can inform ToolbarService + + // These can... Trigger toolbar events based on updates? + // Then sync using useEffect, or simply modify the state here? return ( - -
- {toolDef.map(x => { - const { id, Component, componentProps } = x; - return ( - - ); - })} -
-
+ ToolBarService.recordInteraction(args)} + /> ); - } - })} - -} - -function ToolbarSecondary({ servicesManager }) { - const { ToolBarService } = servicesManager.services; - const defaultTool = { - icon: 'tool-more-menu', - label: 'More', - isActive: false, - }; - const [toolbars, setToolbars] = useState({ primary: [], secondary: [] }); - const [nestedActiveTool, setNestedActiveTool] = useState(defaultTool); - - const setActiveToolHandler = (tool, isNested) => { - setNestedActiveTool(isNested ? tool : defaultTool); - }; - - const onPrimaryClickHandler = (evt, btn) => { - if ( - btn.props && - btn.props.commands && - evt.item && evt.item.value && - btn.props.commands[evt.item.value] - ) { - const { commandName, commandOptions } = btn.props.commands[evt.item.value]; - commandsManager.runCommand(commandName, commandOptions); - } - }; - - useEffect(() => { - const { unsubscribe } = ToolBarService.subscribe( - ToolBarService.EVENTS.TOOL_BAR_MODIFIED, - () => { - console.warn('~~~ TOOL BAR MODIFIED EVENT CAUGHT'); - const updatedToolbars = { - primary: ToolBarService.getButtonSection('primary', { - onClick: onPrimaryClickHandler, - setActiveTool: setActiveToolHandler, - }), - secondary: ToolBarService.getButtonSection('secondary', { - setActiveTool: setActiveToolHandler, - }), - }; - setToolbars(updatedToolbars); - } - ); - - return unsubscribe; - }, [ToolBarService]); - - return <> - {toolbars.secondary.map(toolDef => { - const { id, Component, componentProps } = toolDef; - return ; - })} - + })} + + ); } function ViewerLayout({ @@ -158,25 +86,28 @@ function ViewerLayout({ { title: t('Header:About'), icon: 'info', - onClick: () => show({ content: AboutModal, title: 'About OHIF Viewer' }) + onClick: () => show({ content: AboutModal, title: 'About OHIF Viewer' }), }, { title: t('Header:Preferences'), icon: 'settings', - onClick: () => show({ - title: t('UserPreferencesModal:User Preferences'), - content: UserPreferences, - contentProps: { - hotkeyDefaults: hotkeysManager.getValidHotkeyDefinitions(hotkeyDefaults), - hotkeyDefinitions, - onCancel: hide, - onSubmit: ({ hotkeyDefinitions }) => { - hotkeysManager.setHotkeys(hotkeyDefinitions); - hide(); + onClick: () => + show({ + title: t('UserPreferencesModal:User Preferences'), + content: UserPreferences, + contentProps: { + hotkeyDefaults: hotkeysManager.getValidHotkeyDefinitions( + hotkeyDefaults + ), + hotkeyDefinitions, + onCancel: hide, + onSubmit: ({ hotkeyDefinitions }) => { + hotkeysManager.setHotkeys(hotkeyDefinitions); + hide(); + }, + onReset: () => hotkeysManager.restoreDefaultBindings(), }, - onReset: () => hotkeysManager.restoreDefaultBindings() - } - }) + }), }, ]; @@ -226,7 +157,7 @@ function ViewerLayout({
- +
@@ -246,13 +177,6 @@ function ViewerLayout({ )} {/* TOOLBAR + GRID */}
-
- -
- -
-
-
{ }, + clickHandler: () => {}, }, { name: 'ohif.action', defaultComponent: ToolbarButton, - requiredConfig: [], - optionalConfig: [], - requiredProps: [], - optionalProps: [], - clickHandler: (evt, btn, btnSectionName) => { - const { props } = btn; - commandsManager.runCommand(props.commandName, props.commandOptions); - }, + clickHandler: () => {}, }, { name: 'ohif.radioGroup', defaultComponent: ToolbarButton, - requiredConfig: ['groupName'], - optionalConfig: [], - requiredProps: [], - optionalProps: [], - clickHandler: (evt, clickedBtn, btnSectionName, metadata, viewerProps) => { - const { props } = clickedBtn; - const allButtons = toolbarService.getButtons(); - - // Set all buttons in same group to inactive - Object.keys(allButtons).forEach(btnName => { - const btn = allButtons[btnName]; - const isRadioGroupBtn = - btn.config && - btn.config.groupName && - btn.type === 'ohif.radioGroup'; - - if ( - isRadioGroupBtn && - clickedBtn.config.groupName === btn.config.groupName - ) { - btn.props.isActive = false; - - if (viewerProps.setActiveTool) { - viewerProps.setActiveTool(props, metadata.isNested); - } - } - }); - - // Set our clicked button to active - allButtons[clickedBtn.id].props.isActive = true; - - // Run button logic/command - commandsManager.runCommand(props.commandName, props.commandOptions); - - // Set buttons & trigger notification - toolbarService.setButtons(allButtons); - }, + clickHandler: () => {}, + }, + { + name: 'ohif.splitButton', + defaultComponent: ToolbarSplitButton, + clickHandler: () => {}, }, { name: 'ohif.layoutSelector', defaultComponent: ToolbarLayoutSelector, - clickHandler: (evt, clickedBtn, btnSectionName) => { }, + clickHandler: (evt, clickedBtn, btnSectionName) => {}, }, { name: 'ohif.toggle', diff --git a/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js index bfda0b808..436c5dd33 100644 --- a/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js +++ b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js @@ -4,6 +4,7 @@ import cornerstoneTools from 'cornerstone-tools'; import cornerstone from 'cornerstone-core'; import CornerstoneViewport from 'react-cornerstone-viewport'; import OHIF, { DicomMetadataStore, utils } from '@ohif/core'; +import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool'; import { Notification, ViewportActionBar, @@ -32,7 +33,11 @@ function OHIFCornerstoneSRViewport({ servicesManager, extensionManager, }) { - const { DisplaySetService, MeasurementService } = servicesManager.services; + const { + DisplaySetService, + MeasurementService, + ToolBarService, + } = servicesManager.services; const [viewportGrid, viewportGridService] = useViewportGrid(); const [viewportDialogState, viewportDialogApi] = useViewportDialog(); const [measurementSelected, setMeasurementSelected] = useState(0); @@ -45,15 +50,19 @@ function OHIFCornerstoneSRViewport({ useEffect(() => { const onDisplaySetsRemovedSubscription = DisplaySetService.subscribe( - DisplaySetService.EVENTS.DISPLAY_SETS_REMOVED, ({ displaySetInstanceUIDs }) => { + DisplaySetService.EVENTS.DISPLAY_SETS_REMOVED, + ({ displaySetInstanceUIDs }) => { const activeViewport = viewports[activeViewportIndex]; - if (displaySetInstanceUIDs.includes(activeViewport.displaySetInstanceUID)) { + if ( + displaySetInstanceUIDs.includes(activeViewport.displaySetInstanceUID) + ) { viewportGridService.setDisplaysetForViewport({ viewportIndex: activeViewportIndex, displaySetInstanceUID: undefined, }); } - }); + } + ); return () => { onDisplaySetsRemovedSubscription.unsubscribe(); @@ -64,6 +73,8 @@ function OHIFCornerstoneSRViewport({ let trackedMeasurements; let sendTrackedMeasurementsEvent; + // TODO: this is a hook that fails if we register/de-register + // if ( extensionManager.registeredExtensionIds.includes( MEASUREMENT_TRACKING_EXTENSION_ID @@ -81,25 +92,89 @@ function OHIFCornerstoneSRViewport({ ] = useTrackedMeasurements(); } + // Locked if tracking any series + let isLocked = trackedMeasurements?.context?.trackedSeries?.length > 0; + useEffect(() => { + isLocked = trackedMeasurements?.context?.trackedSeries?.length > 0; + }, [trackedMeasurements]); + + function _getToolAlias() { + const primaryToolId = ToolBarService.state.primaryToolId; + let toolAlias = primaryToolId; + + switch (primaryToolId) { + case 'Length': + toolAlias = 'SRLength'; + break; + case 'Bidirectional': + toolAlias = 'SRBidirectional'; + break; + case 'ArrowAnnotate': + toolAlias = 'SRArrowAnnotate'; + break; + case 'EllipticalRoi': + toolAlias = 'SREllipticalRoi'; + break; + } + + return toolAlias; + } + const onElementEnabled = evt => { const eventData = evt.detail; const targetElement = eventData.element; + const toolAlias = _getToolAlias(); // These are 1:1 for built-in only - // TODO -> This will only be temporary until we set a tool on, and isn't very customizable. - // Need to discuss how to deal with tools in general in the redesign, since we - // Previously just had Tool mode state global across the entire viewer. - const globalTools = cornerstoneTools.store.state.globalTools; - const globalToolNames = Object.keys(globalTools); - - globalToolNames.forEach(globalToolName => { - cornerstoneTools.setToolDisabledForElement(targetElement, globalToolName); - }); - + // ~~ MAGIC + cornerstoneTools.addToolForElement(targetElement, DICOMSRDisplayTool); cornerstoneTools.setToolEnabledForElement( targetElement, TOOL_NAMES.DICOM_SR_DISPLAY_TOOL ); + // ~~ Variants + cornerstoneTools.addToolForElement( + targetElement, + cornerstoneTools.LengthTool, + { + name: 'SRLength', + configuration: { + renderDashed: true, + }, + } + ); + cornerstoneTools.addToolForElement( + targetElement, + cornerstoneTools.ArrowAnnotateTool, + { + name: 'SRArrowAnnotate', + configuration: { + renderDashed: true, + }, + } + ); + cornerstoneTools.addToolForElement( + targetElement, + cornerstoneTools.BidirectionalTool, + { + name: 'SRBidirectional', + configuration: { + renderDashed: true, + }, + } + ); + cornerstoneTools.addToolForElement( + targetElement, + cornerstoneTools.EllipticalRoiTool, + { + name: 'SREllipticalRoi', + configuration: { + renderDashed: true, + }, + } + ); + + // ~~ Business as usual cornerstoneTools.setToolActiveForElement(targetElement, 'PanMultiTouch', { pointers: 2, }); @@ -109,7 +184,9 @@ function OHIFCornerstoneSRViewport({ {} ); - cornerstoneTools.setToolActiveForElement(targetElement, 'Wwwc', { + // TODO: Add always dashed tool alternative aliases + // TODO: or same name... alternative config? + cornerstoneTools.setToolActiveForElement(targetElement, toolAlias, { mouseButtonMask: 1, }); cornerstoneTools.setToolActiveForElement(targetElement, 'Pan', { @@ -131,6 +208,7 @@ function OHIFCornerstoneSRViewport({ 'ohif-cornerstone-enabled-element-event', { detail: { + context: 'ACTIVE_VIEWPORT::STRUCTURED_REPORT', enabledElement: targetElement, viewportIndex, }, @@ -262,7 +340,7 @@ function OHIFCornerstoneSRViewport({ StudyDate, SeriesDescription, SeriesInstanceUID, - PixelSpacing, + SpacingBetweenSlices, SeriesNumber, displaySetInstanceUID, } = activeDisplaySetData; @@ -298,11 +376,19 @@ function OHIFCornerstoneSRViewport({ evt.stopPropagation(); evt.preventDefault(); }} + onPillClick={() => { + sendTrackedMeasurementsEvent('PROMPT_HYDRATE_SR', { + displaySetInstanceUID: displaySet.displaySetInstanceUID, + viewportIndex, + }); + }} onSeriesChange={onMeasurementChange} studyData={{ label, + useAltStyling: true, isTracked: false, - isLocked: displaySet.isLocked, + isLocked, + isRehydratable: displaySet.isRehydratable, isHydrated, studyDate: formatDate(StudyDate), currentSeries: SeriesNumber, @@ -317,10 +403,8 @@ function OHIFCornerstoneSRViewport({ MRN: PatientID || '', thickness: SliceThickness ? `${SliceThickness.toFixed(2)}mm` : '', spacing: - PixelSpacing && PixelSpacing.length - ? `${PixelSpacing[0].toFixed(2)}mm x ${PixelSpacing[1].toFixed( - 2 - )}mm` + SpacingBetweenSlices !== undefined + ? `${SpacingBetweenSlices.toFixed(2)}mm` : '', scanner: ManufacturerModelName || '', }, @@ -458,7 +542,7 @@ async function _getViewportAndActiveDisplaySetData( SeriesInstanceUID: image0.SeriesInstanceUID, SeriesNumber: image0.SeriesNumber, ManufacturerModelName: image0.ManufacturerModelName, - PixelSpacing: image0.PixelSpacing, + SpacingBetweenSlices: image0.SpacingBetweenSlices, displaySetInstanceUID, }; diff --git a/extensions/dicom-sr/src/getSopClassHandlerModule.js b/extensions/dicom-sr/src/getSopClassHandlerModule.js index c8e5e5c0b..2882460f5 100644 --- a/extensions/dicom-sr/src/getSopClassHandlerModule.js +++ b/extensions/dicom-sr/src/getSopClassHandlerModule.js @@ -122,7 +122,9 @@ function _load(displaySet, servicesManager, extensionManager) { ); displaySet.isHydrated = false; - displaySet.isLocked = isRehydratable(displaySet, mappings) ? false : true; + displaySet.isRehydratable = isRehydratable(displaySet, mappings) + ? true + : false; displaySet.isLoaded = true; // Check currently added displaySets and add measurements if the sources exist. diff --git a/extensions/dicom-sr/src/index.js b/extensions/dicom-sr/src/index.js index ec5121b13..237f007e8 100644 --- a/extensions/dicom-sr/src/index.js +++ b/extensions/dicom-sr/src/index.js @@ -60,6 +60,52 @@ export default { return [{ name: 'dicom-sr', component: ExtendedOHIFCornerstoneSRViewport }]; }, + getCommandsModule({ servicesManager }) { + return { + definitions: { + setToolActive: { + commandFn: ({ toolName, element }) => { + if (!toolName) { + console.warn('No toolname provided to setToolActive command'); + } + + console.warn('DICOM SR VIEWPORT SETTOOLACTIVE'); + + // Set same tool or alt tool + const toolAlias = _getToolAlias(toolName); + + cornerstoneTools.setToolActiveForElement(element, toolAlias, { + mouseButtonMask: 1, + }); + }, + storeContexts: [], + options: {}, + }, + }, + defaultContext: 'ACTIVE_VIEWPORT::STRUCTURED_REPORT', + }; + }, getSopClassHandlerModule, onModeEnter, }; + +function _getToolAlias(toolName) { + let toolAlias = toolName; + + switch (toolName) { + case 'Length': + toolAlias = 'SRLength'; + break; + case 'Bidirectional': + toolAlias = 'SRBidirectional'; + break; + case 'ArrowAnnotate': + toolAlias = 'SRArrowAnnotate'; + break; + case 'EllipticalRoi': + toolAlias = 'SREllipticalRoi'; + break; + } + + return toolAlias; +} diff --git a/extensions/dicom-sr/src/init.js b/extensions/dicom-sr/src/init.js index 03d203167..d11b2088d 100644 --- a/extensions/dicom-sr/src/init.js +++ b/extensions/dicom-sr/src/init.js @@ -1,5 +1,4 @@ import cornerstoneTools from 'cornerstone-tools'; -import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool'; import dicomSRModule from './tools/modules/dicomSRModule'; import id from './id'; @@ -20,6 +19,4 @@ export default function init({ configuration = {} }) { TOOL_NAMES.DICOM_SR_DISPLAY_TOOL = conifg.TOOL_NAMES.DICOM_SR_DISPLAY_TOOL; cornerstoneTools.register('module', id, dicomSRModule); - cornerstoneTools.addTool(DICOMSRDisplayTool); - cornerstoneTools.setToolEnabled(TOOL_NAMES.DICOM_SR_DISPLAY_TOOL); } diff --git a/extensions/measurement-tracking/src/_shared/getTools.js b/extensions/measurement-tracking/src/_shared/getTools.js new file mode 100644 index 000000000..c3d9b9e77 --- /dev/null +++ b/extensions/measurement-tracking/src/_shared/getTools.js @@ -0,0 +1,36 @@ +import csTools from 'cornerstone-tools'; + +const toolsGroupedByType = { + touch: [csTools.PanMultiTouchTool, csTools.ZoomTouchPinchTool], + annotations: [ + csTools.ArrowAnnotateTool, + csTools.BidirectionalTool, + csTools.LengthTool, + csTools.AngleTool, + csTools.FreehandRoiTool, + csTools.EllipticalRoiTool, + csTools.DragProbeTool, + csTools.RectangleRoiTool, + ], + other: [ + csTools.PanTool, + csTools.ZoomTool, + csTools.WwwcTool, + csTools.WwwcRegionTool, + csTools.MagnifyTool, + csTools.StackScrollTool, + csTools.StackScrollMouseWheelTool, + csTools.OverlayTool, + ], +}; + +export default function getTools() { + const tools = []; + Object.keys(toolsGroupedByType).forEach(toolsGroup => + tools.push(...toolsGroupedByType[toolsGroup]) + ); + + return tools; +} + +export { toolsGroupedByType }; diff --git a/extensions/measurement-tracking/src/_shared/setActiveAndPassiveToolsForElement.js b/extensions/measurement-tracking/src/_shared/setActiveAndPassiveToolsForElement.js new file mode 100644 index 000000000..b8348c990 --- /dev/null +++ b/extensions/measurement-tracking/src/_shared/setActiveAndPassiveToolsForElement.js @@ -0,0 +1,21 @@ +import csTools from 'cornerstone-tools'; + +export default function _setActiveAndPassiveToolsForElement(element, tools) { + const BaseAnnotationTool = csTools.importInternal('base/BaseAnnotationTool'); + + tools.forEach(tool => { + if (tool.prototype instanceof BaseAnnotationTool) { + // BaseAnnotationTool would likely come from csTools lib exports + const toolName = new tool().name; + csTools.setToolPassiveForElement(element, toolName); // there may be a better place to determine name; may not be on uninstantiated class + } + }); + + csTools.setToolActiveForElement(element, 'Pan', { mouseButtonMask: 4 }); + csTools.setToolActiveForElement(element, 'Zoom', { mouseButtonMask: 2 }); + csTools.setToolActiveForElement(element, 'Wwwc', { mouseButtonMask: 1 }); + csTools.setToolActiveForElement(element, 'StackScrollMouseWheel', {}); // TODO: Empty options should not be required + csTools.setToolActiveForElement(element, 'PanMultiTouch', { pointers: 2 }); // TODO: Better error if no options + csTools.setToolActiveForElement(element, 'ZoomTouchPinch', {}); + csTools.setToolEnabledForElement(element, 'Overlay', {}); +} diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.jsx b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.jsx index c3079b54c..9d6fe7df2 100644 --- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.jsx +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.jsx @@ -141,7 +141,7 @@ function TrackedMeasurementsContextProvider( if ( displaySet.SOPClassHandlerId === 'org.ohif.dicom-sr.sopClassHandlerModule.dicom-sr' && - !displaySet.isLocked + displaySet.isRehydratable === true ) { console.log('sending event...', trackedMeasurements); sendTrackedMeasurementsEvent('PROMPT_HYDRATE_SR', { diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js index 1e2c5e6d3..525581e3b 100644 --- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js @@ -21,6 +21,8 @@ const machineConfiguration = { prevTrackedStudy: '', prevTrackedSeries: [], prevIgnoredSeries: [], + // + isDirty: false, }, states: { off: { @@ -33,7 +35,7 @@ const machineConfiguration = { SET_TRACKED_SERIES: [ { target: 'tracking', - actions: ['setTrackedStudyAndMultipleSeries'], + actions: ['setTrackedStudyAndMultipleSeries', 'setIsDirtyToClean'], }, ], PROMPT_HYDRATE_SR: 'promptHydrateStructuredReport', @@ -90,6 +92,16 @@ const machineConfiguration = { }, ], SAVE_REPORT: 'promptSaveReport', + SET_DIRTY: [ + { + target: 'tracking', + actions: ['setIsDirty'], + cond: 'shouldSetDirty', + }, + { + target: 'tracking', + }, + ], }, }, promptTrackNewSeries: { @@ -98,7 +110,7 @@ const machineConfiguration = { onDone: [ { target: 'tracking', - actions: ['addTrackedSeries'], + actions: ['addTrackedSeries', 'setIsDirty'], cond: 'shouldAddSeries', }, { @@ -106,6 +118,7 @@ const machineConfiguration = { actions: [ 'discardPreviouslyTrackedMeasurements', 'setTrackedStudyAndSeries', + 'setIsDirty', ], cond: 'shouldSetStudyAndSeries', }, @@ -131,6 +144,7 @@ const machineConfiguration = { actions: [ 'discardPreviouslyTrackedMeasurements', 'setTrackedStudyAndSeries', + 'setIsDirty', ], cond: 'shouldSetStudyAndSeries', }, @@ -197,6 +211,7 @@ const machineConfiguration = { actions: [ 'setTrackedStudyAndMultipleSeries', 'showSeriesInActiveViewport', + 'setIsDirtyToClean', ], cond: 'shouldHydrateStructuredReport', }, @@ -274,6 +289,15 @@ const defaultOptions = { ignoredSeries: [], }; }), + setIsDirtyToClean: assign((ctx, evt) => ({ + isDirty: false, + })), + setIsDirty: assign((ctx, evt) => { + debugger; + return { + isDirty: true, + }; + }), ignoreSeries: assign((ctx, evt) => ({ prevIgnoredSeries: [...ctx.ignoredSeries], ignoredSeries: [...ctx.ignoredSeries, evt.data.SeriesInstanceUID], @@ -292,6 +316,13 @@ const defaultOptions = { })), }, guards: { + shouldSetDirty: (ctx, evt) => { + debugger; + return ( + evt.SeriesInstanceUID === undefined || + ctx.trackedSeries.includes(evt.SeriesInstanceUID) + ); + }, shouldKillMachine: (ctx, evt) => evt.data && evt.data.userResponse === RESPONSE.NO_NEVER, shouldAddSeries: (ctx, evt) => diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewSeries.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewSeries.js index bbadc45ff..23cfe4ff1 100644 --- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewSeries.js +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewSeries.js @@ -17,11 +17,13 @@ function promptUser({ servicesManager, extensionManager }, ctx, evt) { viewportIndex ); - if (promptResult === RESPONSE.CREATE_REPORT) { + if (ctx.isDirty && promptResult === RESPONSE.CREATE_REPORT) { promptResult = await _askSaveDiscardOrCancel( UIViewportDialogService, viewportIndex ); + } else { + promptResult = RESPONSE.SET_STUDY_AND_SERIES; } resolve({ diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewStudy.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewStudy.js index e90fc6d41..f74e48adc 100644 --- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewStudy.js +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewStudy.js @@ -17,11 +17,13 @@ function promptUser({ servicesManager, extensionManager }, ctx, evt) { viewportIndex ); - if (promptResult === RESPONSE.SET_STUDY_AND_SERIES) { + if (ctx.isDirty && promptResult === RESPONSE.SET_STUDY_AND_SERIES) { promptResult = await _askSaveDiscardOrCancel( UIViewportDialogService, viewportIndex ); + } else { + promptResult = RESPONSE.SET_STUDY_AND_SERIES; } resolve({ diff --git a/extensions/measurement-tracking/src/index.js b/extensions/measurement-tracking/src/index.js index bbc50e2a0..b184ed64d 100644 --- a/extensions/measurement-tracking/src/index.js +++ b/extensions/measurement-tracking/src/index.js @@ -10,4 +10,25 @@ export default { getContextModule, getPanelModule, getViewportModule, + getCommandsModule({ servicesManager }) { + return { + definitions: { + setToolActive: { + commandFn: ({ toolName, element }) => { + if (!toolName) { + console.warn('No toolname provided to setToolActive command'); + } + + // Set same tool or alt tool + cornerstoneTools.setToolActiveForElement(element, toolName, { + mouseButtonMask: 1, + }); + }, + storeContexts: [], + options: {}, + }, + }, + defaultContext: 'ACTIVE_VIEWPORT::TRACKED', + }; + }, }; diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js index 154773bef..b9e1d2474 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js @@ -361,8 +361,11 @@ function _getOneBasedImageIdIndex(displaySets, SOPInstanceUID) { /** * - * @param {*} points + * @param {*} measurement * @param {*} pixelSpacing + * @param {*} seriesNumber + * @param {*} instanceNumber + * @param {*} types */ function _getDisplayText( measurement, diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx index b0d58a289..6c14c6c16 100644 --- a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx @@ -1,7 +1,12 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { utils } from '@ohif/core'; -import { StudyBrowser, useImageViewer, useViewportGrid, Dialog } from '@ohif/ui'; +import { + StudyBrowser, + useImageViewer, + useViewportGrid, + Dialog, +} from '@ohif/ui'; import { useTrackedMeasurements } from '../../getContextModule'; const { formatDate } = utils; @@ -63,6 +68,7 @@ function PanelStudyBrowserTracking({ referenceStudyUID: StudyInstanceUID, } = measurement; + sendTrackedMeasurementsEvent('SET_DIRTY', { SeriesInstanceUID }); sendTrackedMeasurementsEvent('TRACK_SERIES', { viewportIndex: activeViewportIndex, StudyInstanceUID, @@ -238,10 +244,10 @@ function PanelStudyBrowserTracking({ ); const updatedExpandedStudyInstanceUIDs = shouldCollapseStudy ? [ - ...expandedStudyInstanceUIDs.filter( - stdyUid => stdyUid !== StudyInstanceUID - ), - ] + ...expandedStudyInstanceUIDs.filter( + stdyUid => stdyUid !== StudyInstanceUID + ), + ] : [...expandedStudyInstanceUIDs, StudyInstanceUID]; setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs); @@ -315,7 +321,7 @@ function PanelStudyBrowserTracking({ SeriesInstanceUID: displaySet.SeriesInstanceUID, }); }} - onClickThumbnail={() => { }} + onClickThumbnail={() => {}} onDoubleClickThumbnail={onDoubleClickThumbnailHandler} activeDisplaySetInstanceUID={activeDisplaySetInstanceUID} /> @@ -385,11 +391,11 @@ function _mapDisplaySets( const viewportIdentificator = isSingleViewport ? [] : viewports.reduce((acc, viewportData, index) => { - if (viewportData.displaySetInstanceUID === ds.displaySetInstanceUID) { - acc.push(_viewportLabels[index]); - } - return acc; - }, []); + if (viewportData.displaySetInstanceUID === ds.displaySetInstanceUID) { + acc.push(_viewportLabels[index]); + } + return acc; + }, []); const array = componentType === 'thumbnailTracked' @@ -430,7 +436,7 @@ function _mapDisplaySets( contentProps: { title: 'Reject Report', body: () => ( -
+

This is a destructive action.

Are you sure you want to continue?

@@ -444,7 +450,10 @@ function _mapDisplaySets( switch (action.id) { case 'save': try { - await dataSource.reject.series(ds.StudyInstanceUID, ds.SeriesInstanceUID); + await dataSource.reject.series( + ds.StudyInstanceUID, + ds.SeriesInstanceUID + ); DisplaySetService.deleteDisplaySet(displaySetInstanceUID); UIDialogService.dismiss({ id: 'ds-reject-sr' }); UINotificationService.show({ @@ -525,9 +534,11 @@ function _createStudyBrowserTabs( const displaySetsForStudy = displaySets.filter( ds => ds.StudyInstanceUID === study.studyInstanceUid ); - - // Sort them - const sortedDisplaySetsForStudy = utils.sortBySeriesDate(displaySetsForStudy); + + // Sort them + const sortedDisplaySetsForStudy = utils.sortBySeriesDate( + displaySetsForStudy + ); /* Sort by series number, then by series date displaySetsForStudy.sort((a, b) => { @@ -541,7 +552,7 @@ function _createStudyBrowserTabs( return seriesDateA - seriesDateB; }); */ - + // Map the study to it's tab/view representation const tabStudy = Object.assign({}, study, { displaySets: displaySetsForStudy, diff --git a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js index 72c03057e..9199a83fb 100644 --- a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js +++ b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js @@ -16,6 +16,8 @@ import { useTrackedMeasurements } from './../getContextModule'; import ViewportOverlay from './ViewportOverlay'; import ViewportLoadingIndicator from './ViewportLoadingIndicator'; import setCornerstoneMeasurementActive from '../_shared/setCornerstoneMeasurementActive'; +import setActiveAndPassiveToolsForElement from '../_shared/setActiveAndPassiveToolsForElement'; +import getTools from '../_shared/getTools'; const scrollToIndex = cornerstoneTools.importInternal('util/scrollToIndex'); const { formatDate } = utils; @@ -44,7 +46,7 @@ function TrackedCornerstoneViewport({ displaySet, viewportIndex, servicesManager, - commandsManager + commandsManager, }) { const { ToolBarService, @@ -52,7 +54,10 @@ function TrackedCornerstoneViewport({ MeasurementService, } = servicesManager.services; const [trackedMeasurements] = useTrackedMeasurements(); - const [{ activeViewportIndex, viewports }, viewportGridService] = useViewportGrid(); + const [ + { activeViewportIndex, viewports }, + viewportGridService, + ] = useViewportGrid(); const [{ isCineEnabled, cines }, cineService] = useCine(); const [viewportDialogState, viewportDialogApi] = useViewportDialog(); const [viewportData, setViewportData] = useState(null); @@ -126,9 +131,17 @@ function TrackedCornerstoneViewport({ const onElementEnabled = evt => { const eventData = evt.detail; const targetElement = eventData.element; + const tools = getTools(); + const toolAlias = ToolBarService.state.primaryToolId; + // Activate appropriate tool bindings for element + setActiveAndPassiveToolsForElement(targetElement, tools); + cornerstoneTools.setToolActiveForElement(targetElement, toolAlias, { + mouseButtonMask: 1, + }); + + // Set dashed, based on tracking, for this viewport const allTools = cornerstoneTools.store.state.tools; - const toolsForElement = allTools.filter( tool => tool.element === targetElement ); @@ -154,6 +167,7 @@ function TrackedCornerstoneViewport({ } }); + // Update image after setting tool config const enabledElement = cornerstone.getEnabledElement(targetElement); if (enabledElement.image) { @@ -166,6 +180,7 @@ function TrackedCornerstoneViewport({ 'ohif-cornerstone-enabled-element-event', { detail: { + context: 'ACTIVE_VIEWPORT::TRACKED', enabledElement: targetElement, viewportIndex, }, @@ -256,7 +271,7 @@ function TrackedCornerstoneViewport({ PatientSex, PatientAge, SliceThickness, - PixelSpacing, + SpacingBetweenSlices, ManufacturerModelName, } = displaySet.images[0]; @@ -300,8 +315,8 @@ function TrackedCornerstoneViewport({ } const cine = cines[viewportIndex]; - const isPlaying = cine && cine.isPlaying || false; - const frameRate = cine && cine.frameRate || 24; + const isPlaying = (cine && cine.isPlaying) || false; + const frameRate = (cine && cine.frameRate) || 24; return ( <> @@ -315,6 +330,7 @@ function TrackedCornerstoneViewport({ label, isTracked, isLocked: false, + isRehydratable: false, studyDate: formatDate(SeriesDate), // TODO: This is series date. Is that ok? currentSeries: SeriesNumber, seriesDescription: SeriesDescription, @@ -328,10 +344,8 @@ function TrackedCornerstoneViewport({ MRN: PatientID || '', thickness: SliceThickness ? `${SliceThickness.toFixed(2)}mm` : '', spacing: - PixelSpacing && PixelSpacing.length - ? `${PixelSpacing[0].toFixed(2)}mm x ${PixelSpacing[1].toFixed( - 2 - )}mm` + SpacingBetweenSlices !== undefined + ? `${SpacingBetweenSlices.toFixed(2)}mm` : '', scanner: ManufacturerModelName || '', }, @@ -341,8 +355,10 @@ function TrackedCornerstoneViewport({ cineProps={{ isPlaying, onClose: () => commandsManager.runCommand('toggleCine'), - onPlayPauseChange: isPlaying => cineService.setCine({ id: activeViewportIndex, isPlaying }), - onFrameRateChange: frameRate => cineService.setCine({ id: activeViewportIndex, frameRate }), + onPlayPauseChange: isPlaying => + cineService.setCine({ id: activeViewportIndex, isPlaying }), + onFrameRateChange: frameRate => + cineService.setCine({ id: activeViewportIndex, frameRate }), }} /> {/* TODO: Viewport interface to accept stack or layers of content like this? */} diff --git a/modes/longitudinal/src/index.js b/modes/longitudinal/src/index.js index 97648fd6b..531d303aa 100644 --- a/modes/longitudinal/src/index.js +++ b/modes/longitudinal/src/index.js @@ -39,34 +39,14 @@ export default function mode({ modeConfiguration }) { ToolBarService.init(extensionManager); ToolBarService.addButtons(toolbarButtons); ToolBarService.createButtonSection('primary', [ + 'MeasurementTools', 'Zoom', - 'Wwwc', + 'WindowLevel', 'Pan', 'Capture', 'Layout', - 'Divider', - [ - 'ResetView', - 'RotateClockwise', - 'FlipHorizontally', - 'StackScroll', - 'Magnify', - 'Invert', - 'Cine', - 'Angle', - 'Probe', - 'RectangleRoi', - ], + 'MoreTools', ]); - ToolBarService.createButtonSection('secondary', [ - 'Annotate', - 'Bidirectional', - 'Ellipse', - 'Length', - 'Clear', - ]); - - // Could import layout selector here from org.ohif.default (when it exists!) }, layoutTemplate: ({ routeProps }) => { return { diff --git a/modes/longitudinal/src/toolbarButtons.js b/modes/longitudinal/src/toolbarButtons.js index b9673cb99..271961367 100644 --- a/modes/longitudinal/src/toolbarButtons.js +++ b/modes/longitudinal/src/toolbarButtons.js @@ -1,93 +1,149 @@ // TODO: torn, can either bake this here; or have to create a whole new button type // Only ways that you can pass in a custom React component for render :l -import { ExpandableToolbarButton, ListMenu, WindowLevelMenuItem } from '@ohif/ui'; +import { + // ExpandableToolbarButton, + // ListMenu, + WindowLevelMenuItem, +} from '@ohif/ui'; import { defaults } from '@ohif/core'; const { windowLevelPresets } = defaults; +/** + * + * @param {*} type - 'tool' | 'action' | 'toggle' + * @param {*} id + * @param {*} icon + * @param {*} label + */ +function _createButton(type, id, icon, label, commandName, commandOptions) { + return { + id, + icon, + label, + type, + commandName, + commandOptions, + }; +} + +const _createActionButton = _createButton.bind(null, 'action'); +const _createToggleButton = _createButton.bind(null, 'toggle'); +const _createToolButton = _createButton.bind(null, 'tool'); + +/** + * + * @param {*} preset - preset number (from above import) + * @param {*} title + * @param {*} subtitle + */ +function _createWwwcPreset(preset, title, subtitle) { + return { + id: preset, + title, + subtitle, + type: 'action', + commandName: 'setWindowLevel', + commandOptions: windowLevelPresets[preset], + }; +} export default [ - // Divider + // Measurement { - id: 'Divider', - type: 'ohif.divider', + id: 'MeasurementTools', + type: 'ohif.splitButton', + props: { + groupId: 'MeasurementTools', + isRadio: true, // ? + // Switch? + primary: _createToolButton('Length', 'tool-length', 'Length', undefined, { + toolName: 'Length', + }), + secondary: { + icon: 'chevron-down', + label: '', + isActive: true, + tooltip: 'More Measure Tools', + }, + items: [ + _createToolButton('Length', 'tool-length', 'Length', undefined, { + toolName: 'Length', + }), + _createToolButton( + 'Bidirectional', + 'tool-bidirectional', + 'Bidirectional', + undefined, + { toolName: 'Bidirectional' } + ), + _createToolButton( + 'ArrowAnnotate', + 'tool-annotate', + 'Annotation', + undefined, + { toolName: 'ArrowAnnotate' } + ), + _createToolButton( + 'EllipticalRoi', + 'tool-elipse', + 'Ellipse', + undefined, + { + toolName: 'EllipticalRoi', + } + ), + ], + }, }, - // ~~ Primary + // Zoom.. { id: 'Zoom', type: 'ohif.radioGroup', - config: { - groupName: 'primaryTool', - }, props: { - isActive: false, + type: 'tool', icon: 'tool-zoom', label: 'Zoom', - commandName: 'setToolActive', commandOptions: { toolName: 'Zoom' }, - type: 'primary', }, }, + // Window Level + Presets... { - id: 'Wwwc', - type: 'ohif.radioGroup', - config: { - groupName: 'primaryTool', - }, - component: ExpandableToolbarButton, + id: 'WindowLevel', + type: 'ohif.splitButton', props: { - isActive: true, - icon: 'tool-window-level', - commandName: 'setToolActive', - commandOptions: { toolName: 'Wwwc' }, - commands: { - 1: { - commandName: 'setWindowLevel', - commandOptions: windowLevelPresets[1], - }, - 2: { - commandName: 'setWindowLevel', - commandOptions: windowLevelPresets[2], - }, - 3: { - commandName: 'setWindowLevel', - commandOptions: windowLevelPresets[3], - }, - 4: { - commandName: 'setWindowLevel', - commandOptions: windowLevelPresets[4], - }, - 5: { - commandName: 'setWindowLevel', - commandOptions: windowLevelPresets[5], - } + primary: _createToolButton( + 'Wwwc', + 'tool-window-level', + 'Window Level', + undefined, + { toolName: 'Wwwc' } + ), + secondary: { + icon: 'chevron-down', + label: '', + isActive: true, + tooltip: 'More Measure Tools', }, - type: 'primary', - content: ListMenu, - contentProps: { - items: [ - { value: 1, title: 'Soft tissue', subtitle: '400 / 40' }, - { value: 2, title: 'Lung', subtitle: '1500 / -600' }, - { value: 3, title: 'Liver', subtitle: '150 / 90' }, - { value: 4, title: 'Bone', subtitle: '80 / 40' }, - { value: 5, title: 'Brain', subtitle: '2500 / 480' }, - ], - renderer: WindowLevelMenuItem - } + isAction: true, // ? + renderer: WindowLevelMenuItem, + items: [ + _createWwwcPreset(1, 'Soft tissue', '400 / 40'), + _createWwwcPreset(2, 'Lung', '1500 / -600'), + _createWwwcPreset(3, 'Liver', '150 / 90'), + _createWwwcPreset(4, 'Bone', '80 / 40'), + _createWwwcPreset(5, 'Brain', '2500 / 480'), + ], }, }, + // Pan... { id: 'Pan', type: 'ohif.radioGroup', - config: { - groupName: 'primaryTool', - }, props: { - isActive: false, + type: 'tool', icon: 'tool-move', label: 'Pan', - commandName: 'setToolActive', commandOptions: { toolName: 'Pan' }, - type: 'primary', }, }, { @@ -96,219 +152,83 @@ export default [ props: { icon: 'tool-capture', label: 'Capture', + type: 'action', commandName: 'showDownloadViewportModal', - type: 'primary', }, }, { id: 'Layout', type: 'ohif.layoutSelector', }, - // ~~ Primary: NESTED + // More... { - id: 'ResetView', - type: 'ohif.action', + id: 'MoreTools', + type: 'ohif.splitButton', props: { - icon: 'old-reset', - label: 'Reset View', - commandName: 'resetViewport', - type: 'primary', - }, - }, - { - id: 'RotateClockwise', - type: 'ohif.action', - props: { - icon: 'old-rotate-right', - label: 'Rotate Right', - commandName: 'rotateViewportCW', - type: 'primary', - }, - }, - { - id: 'FlipHorizontally', - type: 'ohif.action', - props: { - icon: 'old-ellipse-h', - label: 'Flip Horizontally', - commandName: 'flipViewportHorizontal', - type: 'primary', - }, - }, - { - id: 'StackScroll', - type: 'ohif.radioGroup', - config: { - groupName: 'primaryTool', - }, - props: { - isActive: false, - icon: 'old-bars', - label: 'Stack Scroll', - commandName: 'setToolActive', - commandOptions: { toolName: 'StackScroll' }, - type: 'primary', - }, - }, - { - id: 'Magnify', - type: 'ohif.radioGroup', - config: { - groupName: 'primaryTool', - }, - props: { - isActive: false, - icon: 'old-circle', - label: 'Magnify', - commandName: 'setToolActive', - commandOptions: { toolName: 'Magnify' }, - type: 'primary', - }, - }, - { - id: 'Invert', - type: 'ohif.action', - props: { - icon: 'old-invert', - label: 'Invert', - commandName: 'invertViewport', - type: 'primary', - }, - }, - { - id: 'Cine', - type: 'ohif.toggle', - config: { - groupName: 'primaryTool', - }, - props: { - isActive: false, - icon: 'old-youtube', - label: 'Cine', - commandName: 'toggleCine', - type: 'primary', - }, - }, - // TODO: 2D MPR: We had said this was off the table? - { - id: 'Angle', - type: 'ohif.radioGroup', - config: { - groupName: 'primaryTool', - }, - props: { - isActive: false, - icon: 'old-angle-left', - label: 'Angle', - commandName: 'setToolActive', - commandOptions: { toolName: 'Angle' }, - type: 'primary', - }, - }, - { - id: 'Probe', - type: 'ohif.radioGroup', - config: { - groupName: 'primaryTool', - }, - props: { - isActive: false, - icon: 'old-dot-circle', - label: 'Probe', - commandName: 'setToolActive', - commandOptions: { toolName: 'DragProbe' }, - type: 'primary', - }, - }, - { - id: 'RectangleRoi', - type: 'ohif.radioGroup', - config: { - groupName: 'primaryTool', - }, - props: { - isActive: false, - icon: 'old-circle-o', - label: 'Rectangle', - commandName: 'setToolActive', - commandOptions: { toolName: 'RectangleRoi' }, - type: 'primary', - }, - }, - // ~~ Secondary - { - id: 'Annotate', - type: 'ohif.radioGroup', - config: { - groupName: 'primaryTool', - }, - props: { - isActive: false, - icon: 'tool-annotate', - label: 'Annotate', - commandName: 'setToolActive', - commandOptions: { toolName: 'ArrowAnnotate' }, - type: 'secondary', - }, - }, - { - id: 'Bidirectional', - type: 'ohif.radioGroup', - config: { - groupName: 'primaryTool', - }, - props: { - isActive: false, - icon: 'tool-bidirectional', - label: 'Bidirectional', - commandName: 'setToolActive', - commandOptions: { toolName: 'Bidirectional' }, - type: 'secondary', - }, - }, - { - id: 'Ellipse', - type: 'ohif.radioGroup', - config: { - groupName: 'primaryTool', - }, - props: { - isActive: false, - icon: 'tool-elipse', - label: 'Ellipse', - commandName: 'setToolActive', - commandOptions: { toolName: 'EllipticalRoi' }, - type: 'secondary', - }, - }, - { - id: 'Length', - type: 'ohif.radioGroup', - config: { - groupName: 'primaryTool', - }, - props: { - isActive: false, - icon: 'tool-length', - label: 'Length', - commandName: 'setToolActive', - commandOptions: { toolName: 'Length' }, - type: 'secondary', - }, - }, - { - id: 'Clear', - type: 'ohif.action', - config: { - groupName: 'primaryTool', - }, - props: { - isActive: false, - icon: 'old-trash', - label: 'Clear', - commandName: 'clearMeasurements', - commandOptions: {}, - type: 'secondary', + isRadio: true, // ? + groupId: 'MoreTools', + primary: _createActionButton( + 'reset', + 'tool-reset', + 'Reset View', + 'resetViewport' + ), + secondary: { + icon: 'chevron-down', + label: '', + isActive: true, + tooltip: 'More Measure Tools', + }, + items: [ + _createActionButton( + 'reset', + 'tool-reset', + 'Reset View', + 'resetViewport' + ), + _createActionButton( + 'rotate-right', + 'tool-rotate-right', + 'Rotate Right', + 'rotateViewportCW' + ), + _createActionButton( + 'flip-horizontal', + 'tool-flip-horizontal', + 'Flip Horizontally', + 'flipViewportHorizontal' + ), + _createToolButton( + 'StackScroll', + 'tool-stack-scroll', + 'Stack Scroll', + undefined, + { toolName: 'StackScroll' } + ), + _createToolButton('Magnify', 'tool-magnify', 'Magnify', undefined, { + toolName: 'Magnify', + }), + _createActionButton( + 'invert', + 'tool-invert', + 'Invert', + 'invertViewport' + ), + _createToggleButton('cine', 'tool-cine', 'Cine', 'toggleCine'), + _createToolButton('Angle', 'tool-angle', 'Angle', undefined, { + toolName: 'Angle', + }), + _createToolButton('DragProbe', 'tool-probe', 'Probe', undefined, { + toolName: 'DragProbe', + }), + _createToolButton( + 'Rectangle', + 'tool-rectangle', + 'Rectangle', + undefined, + { toolName: 'RectangleRoi' } + ), + ], }, }, ]; diff --git a/modes/segmentation/src/toolbarButtons.js b/modes/segmentation/src/toolbarButtons.js index d4a3a7a59..04b7c98c0 100644 --- a/modes/segmentation/src/toolbarButtons.js +++ b/modes/segmentation/src/toolbarButtons.js @@ -72,7 +72,7 @@ export default [ id: 'ResetView', type: 'ohif.action', props: { - icon: 'old-reset', + icon: 'tool-reset', label: 'Reset View', commandName: 'resetViewport', type: 'primary', @@ -82,7 +82,7 @@ export default [ id: 'RotateClockwise', type: 'ohif.action', props: { - icon: 'old-rotate-right', + icon: 'tool-rotate-right', label: 'Rotate Right', commandName: 'rotateViewportCW', type: 'primary', @@ -92,7 +92,7 @@ export default [ id: 'FlipHorizontally', type: 'ohif.action', props: { - icon: 'old-ellipse-h', + icon: 'tool-flip-horizontal', label: 'Flip Horizontally', commandName: 'flipViewportHorizontal', type: 'primary', @@ -106,7 +106,7 @@ export default [ }, props: { isActive: false, - icon: 'old-bars', + icon: 'tool-stack-scroll', label: 'Stack Scroll', commandName: 'setToolActive', commandOptions: { toolName: 'StackScroll' }, @@ -121,7 +121,7 @@ export default [ }, props: { isActive: false, - icon: 'old-circle', + icon: 'tool-magnify', label: 'Magnify', commandName: 'setToolActive', commandOptions: { toolName: 'Magnify' }, @@ -132,7 +132,7 @@ export default [ id: 'Invert', type: 'ohif.action', props: { - icon: 'old-invert', + icon: 'tool-invert', label: 'Invert', commandName: 'invertViewport', type: 'primary', @@ -143,7 +143,7 @@ export default [ id: 'Cine', type: 'ohif.action', props: { - icon: 'old-youtube', + icon: 'tool-cine', label: 'Cine', commandName: '', type: 'primary', @@ -158,7 +158,7 @@ export default [ }, props: { isActive: false, - icon: 'old-angle-left', + icon: 'tool-angle', label: 'Angle', commandName: 'setToolActive', commandOptions: { toolName: 'Angle' }, @@ -173,7 +173,7 @@ export default [ }, props: { isActive: false, - icon: 'old-dot-circle', + icon: 'tool-probe', label: 'Probe', commandName: 'setToolActive', commandOptions: { toolName: 'Probe' }, @@ -188,7 +188,7 @@ export default [ }, props: { isActive: false, - icon: 'old-circle-o', + icon: 'tool-rectangle', label: 'Rectangle', commandName: 'setToolActive', commandOptions: { toolName: 'RectangleRoi' }, diff --git a/platform/core/src/classes/CommandsManager.js b/platform/core/src/classes/CommandsManager.js index 995bc0988..8e993c2cc 100644 --- a/platform/core/src/classes/CommandsManager.js +++ b/platform/core/src/classes/CommandsManager.js @@ -5,7 +5,6 @@ import log from '../log.js'; * * @typedef {Object} CommandDefinition * @property {Function} commandFn - Command to call - * @property {Array} storeContexts - Array of string of modules required from store * @property {Object} options - Object of params to pass action */ @@ -161,19 +160,10 @@ export class CommandsManager { return; } - const { commandFn, storeContexts = [] } = definition; - const definitionOptions = definition.options; - - let commandParams = {}; - const appState = this._getAppState(); - storeContexts.forEach(context => { - commandParams[context] = appState[context]; - }); - - commandParams = Object.assign( + const { commandFn } = definition; + const commandParams = Object.assign( {}, - commandParams, // Required store contexts - definitionOptions, // "Command configuration" + definition.options, // "Command configuration" options // "Time of call" info ); diff --git a/platform/core/src/services/ServicesManager.js b/platform/core/src/services/ServicesManager.js index db5dec800..ad36592ef 100644 --- a/platform/core/src/services/ServicesManager.js +++ b/platform/core/src/services/ServicesManager.js @@ -1,7 +1,8 @@ import log from './../log.js'; export default class ServicesManager { - constructor() { + constructor(commandsManager) { + this._commandsManager = commandsManager; this.services = {}; this.registeredServiceNames = []; } @@ -35,6 +36,7 @@ export default class ServicesManager { if (service.create) { this.services[service.name] = service.create({ configuration, + commandsManager: this._commandsManager, }); } else { log.warn(`Service create factory function not defined. Exiting early.`); diff --git a/platform/core/src/services/ToolBarService/ToolBarService.js b/platform/core/src/services/ToolBarService/ToolBarService.js index daff7ebe4..025d9fb34 100644 --- a/platform/core/src/services/ToolBarService/ToolBarService.js +++ b/platform/core/src/services/ToolBarService/ToolBarService.js @@ -3,10 +3,13 @@ import pubSubServiceInterface from '../_shared/pubSubServiceInterface'; const EVENTS = { TOOL_BAR_MODIFIED: 'event::toolBarService:toolBarModified', + TOOL_BAR_STATE_MODIFIED: 'event::toolBarService:toolBarStateModified', }; export default class ToolBarService { - constructor() { + constructor(commandsManager) { + this._commandsManager = commandsManager; + // this.EVENTS = EVENTS; this.listeners = {}; this.buttons = {}; @@ -17,6 +20,18 @@ export default class ToolBarService { */ }; + // TODO: Do we need to track per context? Or do we allow for a mixed + // definition that adapts based on context? + this.state = { + primaryToolId: 'Wwwc', + toggles: { + /* id: true/false */ + }, + groups: { + /* track most recent click per group...? */ + }, + }; + Object.assign(this, pubSubServiceInterface); } @@ -24,17 +39,59 @@ export default class ToolBarService { this.extensionManager = extensionManager; } + /** + * + * @param {*} interaction + */ + recordInteraction(interaction) { + const commandsManager = this._commandsManager; + const { groupId, itemId, interactionType } = interaction; + + switch (interactionType) { + case 'action': { + break; + } + case 'tool': { + this.state.primaryToolId = itemId; + // TODO: Force run this for all contexts? Even inactive? + // or... They'll just detect primaryToolId when they spin up and apply... + commandsManager.runCommand('setToolActive', interaction.commandOptions); + break; + } + case 'toggle': { + this.state.toggles[itemId] = + this.state.toggles[itemId] === undefined + ? true + : !this.state.toggles[itemId]; + break; + } + } + + // Run command if there's one associated + // + // NOTE: Should probably just do this for tools as well? + // But would be nice if we could enforce at least the command name? + if (interaction.commandName) { + commandsManager.runCommand( + interaction.commandName, + interaction.commandOptions + ); + } + + // Track last touched id for each group + if (groupId) { + this.state.groups[groupId] = itemId; + } + + this._broadcastChange(this.EVENTS.TOOL_BAR_STATE_MODIFIED, {}); + } + getButtons() { return this.buttons; } getActiveTools() { - return Object.keys(this.buttons).filter(key => { - const button = this.buttons[key]; - if (button && button.props && button.props.isActive) { - return button; - } - }); + return [this.state.primaryToolId, ...Object.keys(this.state.toggles)]; } setButton(id, button) { @@ -43,7 +100,7 @@ export default class ToolBarService { this._broadcastChange(this.EVENTS.TOOL_BAR_MODIFIED, { buttons: this.buttons, button: this.buttons[id], - buttonSections: this.buttonSections + buttonSections: this.buttonSections, }); } } @@ -52,7 +109,7 @@ export default class ToolBarService { this.buttons = buttons; this._broadcastChange(this.EVENTS.TOOL_BAR_MODIFIED, { buttons: this.buttons, - buttonSections: this.buttonSections + buttonSections: this.buttonSections, }); } @@ -84,41 +141,27 @@ export default class ToolBarService { this._broadcastChange(this.EVENTS.TOOL_BAR_MODIFIED, {}); } + /** + * + * Finds a button section by it's name, then maps the list of string name + * identifiers to schema/values that can be used to render the buttons. + * + * @param {string} key + * @param {*} props + */ getButtonSection(key, props) { const buttonSectionIds = this.buttonSections[key]; const buttonsInSection = []; - if (!buttonSectionIds) { - return buttonsInSection; - } - - buttonSectionIds.forEach(btnIdOrArray => { - const isNested = Array.isArray(btnIdOrArray); - - if (isNested) { - const btnIds = btnIdOrArray; - const nestedButtons = []; - - btnIds.forEach(nestedBtnId => { - const nestedBtn = this.buttons[nestedBtnId]; - const metadata = { isNested: true }; - const mappedNestedBtn = this._mapButtonToDisplay(nestedBtn, key, metadata, props); - - nestedButtons.push(mappedNestedBtn); - }); - - if (nestedButtons.length) { - buttonsInSection.push(nestedButtons); - } - } else { - const btnId = btnIdOrArray; + if (buttonSectionIds && buttonSectionIds.length !== 0) { + buttonSectionIds.forEach(btnId => { const btn = this.buttons[btnId]; - const metadata = { isNested: false }; + const metadata = {}; const mappedBtn = this._mapButtonToDisplay(btn, key, metadata, props); buttonsInSection.push(mappedBtn); - } - }); + }); + } return buttonsInSection; } @@ -159,6 +202,8 @@ export default class ToolBarService { * * @param {*} btn * @param {*} btnSection + * @param {*} metadata + * @param {*} props - Props set by the Viewer layer */ _mapButtonToDisplay(btn, btnSection, metadata, props) { const { id, type, component } = btn; @@ -168,25 +213,10 @@ export default class ToolBarService { return; } - const onClick = evt => { - if (buttonType.clickHandler) { - buttonType.clickHandler(evt, btn, btnSection, metadata, props); - } - if (btn.props.onClick) { - btn.onClick(evt, btn, btnSection); - } - if (btn.props.clickHandler) { - btn.clickHandler(evt, btn, btnSection); - } - if (props && props.onClick) { - props.onClick(evt, btn, btnSection, props); - } - }; - return { id, Component: component || buttonType.defaultComponent, - componentProps: Object.assign({}, btn.props, { onClick }), // + componentProps: Object.assign({}, btn.props, props), }; } } diff --git a/platform/core/src/services/ToolBarService/index.js b/platform/core/src/services/ToolBarService/index.js index bbe3dd317..e283314d8 100644 --- a/platform/core/src/services/ToolBarService/index.js +++ b/platform/core/src/services/ToolBarService/index.js @@ -2,7 +2,7 @@ import ToolBarService from './ToolBarService'; export default { name: 'ToolBarService', - create: ({ configuration = {} }) => { - return new ToolBarService(); + create: ({ configuration = {}, commandsManager }) => { + return new ToolBarService(commandsManager); }, }; diff --git a/platform/ui/src/assets/icons/arrow-left.svg b/platform/ui/src/assets/icons/arrow-left.svg new file mode 100644 index 000000000..307f8ffdf --- /dev/null +++ b/platform/ui/src/assets/icons/arrow-left.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/platform/ui/src/assets/icons/exclamation.svg b/platform/ui/src/assets/icons/exclamation.svg new file mode 100644 index 000000000..5b6ae388b --- /dev/null +++ b/platform/ui/src/assets/icons/exclamation.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/platform/ui/src/assets/icons/old-angle-left.svg b/platform/ui/src/assets/icons/old-angle-left.svg deleted file mode 100644 index 5b87c1da3..000000000 --- a/platform/ui/src/assets/icons/old-angle-left.svg +++ /dev/null @@ -1,9 +0,0 @@ - - Angle Left - - diff --git a/platform/ui/src/assets/icons/old-bars.svg b/platform/ui/src/assets/icons/old-bars.svg deleted file mode 100644 index d7e430f0c..000000000 --- a/platform/ui/src/assets/icons/old-bars.svg +++ /dev/null @@ -1,9 +0,0 @@ - - Bars - - diff --git a/platform/ui/src/assets/icons/old-circle-o.svg b/platform/ui/src/assets/icons/old-circle-o.svg deleted file mode 100644 index 4242c992f..000000000 --- a/platform/ui/src/assets/icons/old-circle-o.svg +++ /dev/null @@ -1,9 +0,0 @@ - - Square Outline - - diff --git a/platform/ui/src/assets/icons/old-circle.svg b/platform/ui/src/assets/icons/old-circle.svg deleted file mode 100644 index 2cad5edcb..000000000 --- a/platform/ui/src/assets/icons/old-circle.svg +++ /dev/null @@ -1,9 +0,0 @@ - - Circle - - diff --git a/platform/ui/src/assets/icons/old-dot-circle.svg b/platform/ui/src/assets/icons/old-dot-circle.svg deleted file mode 100644 index 625f3b247..000000000 --- a/platform/ui/src/assets/icons/old-dot-circle.svg +++ /dev/null @@ -1,9 +0,0 @@ - - Dot Circle - - diff --git a/platform/ui/src/assets/icons/old-ellipse-h.svg b/platform/ui/src/assets/icons/old-ellipse-h.svg deleted file mode 100644 index 1d95148f7..000000000 --- a/platform/ui/src/assets/icons/old-ellipse-h.svg +++ /dev/null @@ -1,9 +0,0 @@ - - Ellipse Horizontal - - diff --git a/platform/ui/src/assets/icons/old-invert.svg b/platform/ui/src/assets/icons/old-invert.svg deleted file mode 100644 index f859c45bc..000000000 --- a/platform/ui/src/assets/icons/old-invert.svg +++ /dev/null @@ -1,9 +0,0 @@ - - Adjust - - diff --git a/platform/ui/src/assets/icons/old-reset.svg b/platform/ui/src/assets/icons/old-reset.svg deleted file mode 100644 index 116676d9c..000000000 --- a/platform/ui/src/assets/icons/old-reset.svg +++ /dev/null @@ -1,9 +0,0 @@ - - Reset - - diff --git a/platform/ui/src/assets/icons/old-rotate-right.svg b/platform/ui/src/assets/icons/old-rotate-right.svg deleted file mode 100644 index 11646928f..000000000 --- a/platform/ui/src/assets/icons/old-rotate-right.svg +++ /dev/null @@ -1,9 +0,0 @@ - - Rotate Right - - diff --git a/platform/ui/src/assets/icons/old-youtube.svg b/platform/ui/src/assets/icons/old-youtube.svg deleted file mode 100644 index 433789f12..000000000 --- a/platform/ui/src/assets/icons/old-youtube.svg +++ /dev/null @@ -1,9 +0,0 @@ - - YouTube Logo - - diff --git a/platform/ui/src/assets/icons/tool-angle.svg b/platform/ui/src/assets/icons/tool-angle.svg new file mode 100644 index 000000000..08d55baa8 --- /dev/null +++ b/platform/ui/src/assets/icons/tool-angle.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/platform/ui/src/assets/icons/tool-annotate.svg b/platform/ui/src/assets/icons/tool-annotate.svg index 99fbfe3a3..34787d5d5 100644 --- a/platform/ui/src/assets/icons/tool-annotate.svg +++ b/platform/ui/src/assets/icons/tool-annotate.svg @@ -1,5 +1,5 @@ - - + + diff --git a/platform/ui/src/assets/icons/tool-bidirectional.svg b/platform/ui/src/assets/icons/tool-bidirectional.svg index cd896bb9b..da047bf71 100644 --- a/platform/ui/src/assets/icons/tool-bidirectional.svg +++ b/platform/ui/src/assets/icons/tool-bidirectional.svg @@ -1,12 +1,9 @@ - - - - - + + + + + - - - - + diff --git a/platform/ui/src/assets/icons/tool-capture.svg b/platform/ui/src/assets/icons/tool-capture.svg index 8c9be558a..405d53c8c 100644 --- a/platform/ui/src/assets/icons/tool-capture.svg +++ b/platform/ui/src/assets/icons/tool-capture.svg @@ -1,5 +1,5 @@ - + diff --git a/platform/ui/src/assets/icons/tool-cine.svg b/platform/ui/src/assets/icons/tool-cine.svg new file mode 100644 index 000000000..2c442a9f2 --- /dev/null +++ b/platform/ui/src/assets/icons/tool-cine.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/platform/ui/src/assets/icons/tool-elipse.svg b/platform/ui/src/assets/icons/tool-elipse.svg index 6a7905448..90e4de705 100644 --- a/platform/ui/src/assets/icons/tool-elipse.svg +++ b/platform/ui/src/assets/icons/tool-elipse.svg @@ -1,6 +1,6 @@ - - - + + + diff --git a/platform/ui/src/assets/icons/tool-flip-horizontal.svg b/platform/ui/src/assets/icons/tool-flip-horizontal.svg new file mode 100644 index 000000000..adf1de555 --- /dev/null +++ b/platform/ui/src/assets/icons/tool-flip-horizontal.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/platform/ui/src/assets/icons/tool-invert.svg b/platform/ui/src/assets/icons/tool-invert.svg new file mode 100644 index 000000000..0f92a4c37 --- /dev/null +++ b/platform/ui/src/assets/icons/tool-invert.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/platform/ui/src/assets/icons/tool-length.svg b/platform/ui/src/assets/icons/tool-length.svg index ab01c1028..873460e7e 100644 --- a/platform/ui/src/assets/icons/tool-length.svg +++ b/platform/ui/src/assets/icons/tool-length.svg @@ -1,7 +1,6 @@ - - - - + + + diff --git a/platform/ui/src/assets/icons/tool-magnify.svg b/platform/ui/src/assets/icons/tool-magnify.svg new file mode 100644 index 000000000..e26496594 --- /dev/null +++ b/platform/ui/src/assets/icons/tool-magnify.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/platform/ui/src/assets/icons/tool-move.svg b/platform/ui/src/assets/icons/tool-move.svg index d1c9d5dfb..095a22527 100644 --- a/platform/ui/src/assets/icons/tool-move.svg +++ b/platform/ui/src/assets/icons/tool-move.svg @@ -1,8 +1,8 @@ - - - - - - + + + + + + diff --git a/platform/ui/src/assets/icons/tool-probe.svg b/platform/ui/src/assets/icons/tool-probe.svg new file mode 100644 index 000000000..12587c8f1 --- /dev/null +++ b/platform/ui/src/assets/icons/tool-probe.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/platform/ui/src/assets/icons/tool-rectangle.svg b/platform/ui/src/assets/icons/tool-rectangle.svg new file mode 100644 index 000000000..58f95cc95 --- /dev/null +++ b/platform/ui/src/assets/icons/tool-rectangle.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/platform/ui/src/assets/icons/tool-reset.svg b/platform/ui/src/assets/icons/tool-reset.svg new file mode 100644 index 000000000..4360043c0 --- /dev/null +++ b/platform/ui/src/assets/icons/tool-reset.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/platform/ui/src/assets/icons/tool-rotate-right.svg b/platform/ui/src/assets/icons/tool-rotate-right.svg new file mode 100644 index 000000000..a30a64971 --- /dev/null +++ b/platform/ui/src/assets/icons/tool-rotate-right.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/platform/ui/src/assets/icons/tool-stack-scroll.svg b/platform/ui/src/assets/icons/tool-stack-scroll.svg new file mode 100644 index 000000000..ea717b660 --- /dev/null +++ b/platform/ui/src/assets/icons/tool-stack-scroll.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/platform/ui/src/assets/icons/tool-window-level.svg b/platform/ui/src/assets/icons/tool-window-level.svg index 67ecbcd06..d423da4cb 100644 --- a/platform/ui/src/assets/icons/tool-window-level.svg +++ b/platform/ui/src/assets/icons/tool-window-level.svg @@ -1,5 +1,5 @@ - - + + diff --git a/platform/ui/src/assets/icons/tool-zoom.svg b/platform/ui/src/assets/icons/tool-zoom.svg index f0627cfb5..b6b35814a 100644 --- a/platform/ui/src/assets/icons/tool-zoom.svg +++ b/platform/ui/src/assets/icons/tool-zoom.svg @@ -1,3 +1,6 @@ - - + + + + + diff --git a/platform/ui/src/components/Icon/getIcon.jsx b/platform/ui/src/components/Icon/getIcon.jsx index e7496ad02..8b71da63d 100644 --- a/platform/ui/src/components/Icon/getIcon.jsx +++ b/platform/ui/src/components/Icon/getIcon.jsx @@ -2,6 +2,7 @@ import React from 'react'; // Icons import arrowDown from './../../assets/icons/arrow-down.svg'; +import arrowLeft from './../../assets/icons/arrow-left.svg'; import calendar from './../../assets/icons/calendar.svg'; import cancel from './../../assets/icons/cancel.svg'; import clipboard from './../../assets/icons/clipboard.svg'; @@ -13,6 +14,7 @@ import chevronLeft from './../../assets/icons/chevron-left.svg'; import chevronRight from './../../assets/icons/chevron-right.svg'; import eyeVisible from './../../assets/icons/eye-visible.svg'; import eyeHidden from './../../assets/icons/eye-hidden.svg'; +import exclamation from './../../assets/icons/exclamation.svg'; import externalLink from './../../assets/icons/external-link.svg'; import groupLayers from './../../assets/icons/group-layers.svg'; import info from './../../assets/icons/info.svg'; @@ -47,18 +49,18 @@ import toolAnnotate from './../../assets/icons/tool-annotate.svg'; import toolBidirectional from './../../assets/icons/tool-bidirectional.svg'; import toolElipse from './../../assets/icons/tool-elipse.svg'; import toolLength from './../../assets/icons/tool-length.svg'; +import toolStackScroll from './../../assets/icons/tool-stack-scroll.svg'; +import toolMagnify from './../../assets/icons/tool-magnify.svg'; +import toolFlipHorizontal from './../../assets/icons/tool-flip-horizontal.svg'; +import toolInvert from './../../assets/icons/tool-invert.svg'; +import toolRotateRight from './../../assets/icons/tool-rotate-right.svg'; +import toolCine from './../../assets/icons/tool-cine.svg'; +import toolProbe from './../../assets/icons/tool-probe.svg'; +import toolAngle from './../../assets/icons/tool-angle.svg'; +import toolReset from './../../assets/icons/tool-reset.svg'; +import toolRectangle from './../../assets/icons/tool-rectangle.svg'; /** Old OHIF */ -import oldBars from './../../assets/icons/old-bars.svg'; -import oldCircle from './../../assets/icons/old-circle.svg'; -import oldEllipseH from './../../assets/icons/old-ellipse-h.svg'; -import oldInvert from './../../assets/icons/old-invert.svg'; -import oldRotateRight from './../../assets/icons/old-rotate-right.svg'; -import oldYoutube from './../../assets/icons/old-youtube.svg'; -import oldDotCircle from './../../assets/icons/old-dot-circle.svg'; -import oldAngleLeft from './../../assets/icons/old-angle-left.svg'; -import oldReset from './../../assets/icons/old-reset.svg'; -import oldCircleO from './../../assets/icons/old-circle-o.svg'; import oldTrash from './../../assets/icons/old-trash.svg'; import oldPlay from './../../assets/icons/old-play.svg'; import oldStop from './../../assets/icons/old-stop.svg'; @@ -80,6 +82,7 @@ const ICONS = { 'group-layers': groupLayers, info: info, 'info-link': infoLink, + 'arrow-left': arrowLeft, 'launch-arrow': launchArrow, 'launch-info': launchInfo, link: link, @@ -87,6 +90,7 @@ const ICONS = { lock: lock, 'logo-ohif-small': logoOhifSmall, magnifier: magnifier, + exclamation: exclamation, 'notificationwarning-diamond': notificationwarningDiamond, pencil: pencil, profile: profile, @@ -110,18 +114,18 @@ const ICONS = { 'tool-bidirectional': toolBidirectional, 'tool-elipse': toolElipse, 'tool-length': toolLength, + 'tool-stack-scroll': toolStackScroll, + 'tool-magnify': toolMagnify, + 'tool-flip-horizontal': toolFlipHorizontal, + 'tool-invert': toolInvert, + 'tool-rotate-right': toolRotateRight, + 'tool-cine': toolCine, + 'tool-probe': toolProbe, + 'tool-angle': toolAngle, + 'tool-reset': toolReset, + 'tool-rectangle': toolRectangle, /** Old OHIF */ - 'old-bars': oldBars, - 'old-circle': oldCircle, - 'old-ellipse-h': oldEllipseH, - 'old-invert': oldInvert, - 'old-rotate-right': oldRotateRight, - 'old-youtube': oldYoutube, - 'old-dot-circle': oldDotCircle, - 'old-angle-left': oldAngleLeft, - 'old-reset': oldReset, - 'old-circle-o': oldCircleO, 'old-trash': oldTrash, 'old-play': oldPlay, 'old-stop': oldStop, diff --git a/platform/ui/src/components/SplitButton/SplitButton.jsx b/platform/ui/src/components/SplitButton/SplitButton.jsx index cd7538dfd..f26fb54e7 100644 --- a/platform/ui/src/components/SplitButton/SplitButton.jsx +++ b/platform/ui/src/components/SplitButton/SplitButton.jsx @@ -6,117 +6,210 @@ import OutsideClickHandler from 'react-outside-click-handler'; import { Icon, Tooltip, ListMenu } from '@ohif/ui'; const baseClasses = { - Button: 'h-12 flex items-center rounded-md border-transparent border-2 cursor-pointer', - Primary: 'h-full flex flex-1 items-center rounded-md rounded-tr-none rounded-br-none', - Secondary: 'h-full flex items-center justify-center rounded-tr-md rounded-br-md w-4', + Button: + 'h-12 flex items-center rounded-md border-transparent border-2 cursor-pointer', + Primary: + 'h-full flex flex-1 items-center rounded-md rounded-tr-none rounded-br-none', + Secondary: + 'h-full flex items-center justify-center rounded-tr-md rounded-br-md w-4', PrimaryIcon: 'w-5 h-5', SecondaryIcon: 'w-4 h-full stroke-1', Separator: 'border-l pt-2 pb-2', - Content: 'absolute z-10 top-0 mt-16' + Content: 'absolute z-10 top-0 mt-16', }; const classes = { - Button: ({ isExpanded, primary }) => classNames( - baseClasses.Button, - !isExpanded && !primary.isActive && 'hover:bg-primary-dark hover:border-primary-dark' - ), + Button: ({ isExpanded, primary }) => + classNames( + baseClasses.Button, + !isExpanded && + !primary.isActive && + 'hover:bg-primary-dark hover:border-primary-dark' + ), Interface: 'h-full flex flex-row items-center', - Primary: ({ primary, isExpanded }) => classNames( - baseClasses.Primary, - primary.isActive && !isExpanded ? 'bg-primary-light rounded-tr-md rounded-br-md' : - isExpanded ? 'bg-primary-dark' : 'bg-secondary-dark hover:bg-primary-dark' - ), - Secondary: ({ isExpanded, primary }) => classNames( - baseClasses.Secondary, - isExpanded ? 'bg-primary-light rounded-tr-md rounded-br-md' - : primary.isActive ? 'bg-secondary-dark' : 'hover:bg-primary-dark bg-secondary-dark' - ), - PrimaryIcon: ({ primary, isExpanded }) => classNames( - baseClasses.PrimaryIcon, - primary.isActive && !isExpanded ? 'text-primary-dark' : 'text-common-bright' - ), - SecondaryIcon: ({ isExpanded }) => classNames( - baseClasses.SecondaryIcon, - isExpanded ? 'text-primary-dark' : 'text-primary-active hover:text-common-bright' - ), - Separator: ({ primary, isExpanded, isHovering }) => classNames( - baseClasses.Separator, - isHovering || isExpanded || primary.isActive ? 'border-transparent' : 'border-primary-active' - ), - Content: ({ isExpanded }) => classNames(baseClasses.Content, isExpanded ? 'block' : 'hidden') + Primary: ({ primary, isExpanded }) => + classNames( + baseClasses.Primary, + primary.isActive && !isExpanded + ? 'bg-primary-light rounded-tr-md rounded-br-md' + : isExpanded + ? 'bg-primary-dark' + : 'bg-secondary-dark hover:bg-primary-dark' + ), + Secondary: ({ isExpanded, primary }) => + classNames( + baseClasses.Secondary, + isExpanded + ? 'bg-primary-light rounded-tr-md rounded-br-md' + : primary.isActive + ? 'bg-secondary-dark' + : 'hover:bg-primary-dark bg-secondary-dark' + ), + PrimaryIcon: ({ primary, isExpanded }) => + classNames( + baseClasses.PrimaryIcon, + primary.isActive && !isExpanded + ? 'text-primary-dark' + : 'text-common-bright' + ), + SecondaryIcon: ({ isExpanded }) => + classNames( + baseClasses.SecondaryIcon, + isExpanded + ? 'text-primary-dark' + : 'text-primary-active hover:text-common-bright' + ), + Separator: ({ primary, isExpanded, isHovering }) => + classNames( + baseClasses.Separator, + isHovering || isExpanded || primary.isActive + ? 'border-transparent' + : 'border-primary-active' + ), + Content: ({ isExpanded }) => + classNames(baseClasses.Content, isExpanded ? 'block' : 'hidden'), }; const SplitButton = ({ isRadio, isAction, + // + bState, + // + groupId, primary: _primary, secondary, - onClick, items: _items, renderer, + onInteraction, }) => { + const { primaryToolId, toggles, groups } = bState; /* Bubbles up individual item clicks */ - const getSplitButtonItems = items => items.map((item, index) => ({ - ...item, - index, - onClick: () => { - if (item.onClick) item.onClick({ ...item, index }); - onClick({ item, index }); + const getSplitButtonItems = items => + items.map((item, index) => ({ + ...item, + index, + onClick: () => { + onInteraction({ + groupId, + // + itemId: item.id, + interactionType: item.type, + // splitButtonId? (so we can track group?) + // info to fire item's command/event? + commandName: item.commandName, + commandOptions: item.commandOptions, + }); - setState(state => ({ - ...state, - primary: !isAction ? { ...item, index } : state.primary, - isExpanded: false, - items: getSplitButtonItems(_items).filter(item => isRadio && !isAction ? item.index !== index : true) - })); - } - })); + setState(state => ({ + ...state, + primary: !isAction ? { ...item, index } : state.primary, + isExpanded: false, + items: getSplitButtonItems(_items).filter(item => + isRadio && !isAction ? item.index !== index : true + ), + })); + }, + })); const [state, setState] = useState({ primary: _primary, - items: getSplitButtonItems(_items), + items: getSplitButtonItems(_items).filter(item => + isRadio && !isAction ? item.id !== _primary.id : true + ), isHovering: false, - isExpanded: false + isExpanded: false, }); - const onSecondaryClickHandler = () => setState(state => ({ ...state, isExpanded: !state.isExpanded })); - const onMouseEnterHandler = () => setState(state => ({ ...state, isHovering: true })); - const onMouseLeaveHandler = () => setState(state => ({ ...state, isHovering: false })); - const outsideClickHandler = () => setState(state => ({ ...state, isExpanded: false })); + const onSecondaryClickHandler = () => + setState(state => ({ ...state, isExpanded: !state.isExpanded })); + const onMouseEnterHandler = () => + setState(state => ({ ...state, isHovering: true })); + const onMouseLeaveHandler = () => + setState(state => ({ ...state, isHovering: false })); + const outsideClickHandler = () => + setState(state => ({ ...state, isExpanded: false })); const onPrimaryClickHandler = () => { - const primary = { ...state.primary, isActive: !state.primary.isActive }; - state.primary.onClick(primary); - setState(state => ({ ...state, isExpanded: false, primary })); + onInteraction({ + groupId, + itemId: state.primary.id, + interactionType: state.primary.type, + // splitButtonId? (so we can track group?) + // info to fire item's command/event? + // + commandName: state.primary.commandName, + commandOptions: state.primary.commandOptions, + }); }; + const isPrimaryActive = + (state.primary.type === 'tool' && primaryToolId === state.primary.id) || + (state.primary.type === 'toggle' && toggles[state.primary.id] === true); + return ( -
+
-
- -
- +
+ +
+
-
-
+
+
- +
+ {/* EXPANDED LIST OF OPTIONS */}
@@ -126,21 +219,20 @@ const SplitButton = ({ }; const DefaultListItemRenderer = ({ icon, label, isActive }) => ( -
- - + + - - {label} - -
+ {label} +
); -const noop = () => { }; +const noop = () => {}; SplitButton.defaultProps = { isRadio: false, @@ -148,47 +240,45 @@ SplitButton.defaultProps = { primary: { label: null, tooltip: null, - isActive: true, - onClick: noop }, secondary: { icon: 'chevron-down', label: null, isActive: true, - tooltip: 'More Measure Tools' + tooltip: 'More Measure Tools', }, items: [], renderer: DefaultListItemRenderer, - onClick: noop }; SplitButton.propTypes = { primary: PropTypes.shape({ - id: PropTypes.string, + id: PropTypes.string.isRequired, icon: PropTypes.string, label: PropTypes.string, + type: PropTypes.oneOf(['tool', 'action', 'toggle']).isRequired, tooltip: PropTypes.string, - isActive: PropTypes.bool, }), secondary: PropTypes.shape({ id: PropTypes.string, icon: PropTypes.string, label: PropTypes.string, tooltip: PropTypes.string, - isActive: PropTypes.bool + isActive: PropTypes.bool, }), - onClick: PropTypes.func, renderer: PropTypes.func, items: PropTypes.arrayOf( PropTypes.shape({ id: PropTypes.string.isRequired, icon: PropTypes.string, label: PropTypes.string, + type: PropTypes.oneOf(['tool', 'action', 'toggle']).isRequired, tooltip: PropTypes.string, - onClick: PropTypes.func, isActive: PropTypes.bool, }) - ) + ), + /** Callback function to inform ToolbarService of important events */ + onInteraction: PropTypes.func.isRequired, }; export default SplitButton; diff --git a/platform/ui/src/components/StudyListFilter/StudyListFilter.jsx b/platform/ui/src/components/StudyListFilter/StudyListFilter.jsx index 50fa5cac7..ab71cd0fe 100644 --- a/platform/ui/src/components/StudyListFilter/StudyListFilter.jsx +++ b/platform/ui/src/components/StudyListFilter/StudyListFilter.jsx @@ -2,7 +2,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Button, Icon, Typography, InputGroup } from '../../components'; -import { useModal } from '../../contextProviders'; const StudyListFilter = ({ filtersMeta, @@ -21,42 +20,17 @@ const StudyListFilter = ({ }); }; const isSortingEnable = numOfStudies > 0 && numOfStudies <= 100; - const { show } = useModal(); - - const showLearnMoreContent = () => { - const modalContent = () =>
Search Instructions
; - - show({ - content: modalContent, - title: 'Learn More', - }); - }; return (
-
-
+
+
- + Study list -
- -
{isFiltering && ( @@ -64,7 +38,7 @@ const StudyListFilter = ({ rounded="full" variant="outlined" color="primary" - className="text-primary-active border-primary-active mx-8" + className="mx-8 text-primary-active border-primary-active" startIcon={} onClick={clearFilters} > @@ -76,7 +50,7 @@ const StudyListFilter = ({ Studies @@ -89,7 +63,7 @@ const StudyListFilter = ({ className="sticky z-10 border-b-4 border-black" style={{ top: '57px' }} > -
+
{numOfStudies > 100 && (
-
+

Filter list to 100 studies or less to enable sorting

diff --git a/platform/ui/src/components/ToolbarButton/ToolbarButton.jsx b/platform/ui/src/components/ToolbarButton/ToolbarButton.jsx index b1ed3646a..eca4b85dc 100644 --- a/platform/ui/src/components/ToolbarButton/ToolbarButton.jsx +++ b/platform/ui/src/components/ToolbarButton/ToolbarButton.jsx @@ -7,12 +7,19 @@ import { IconButton, Icon, Tooltip } from '../'; const ToolbarButton = ({ type, id, - isActive, - onClick, icon, label, + commandName, + commandOptions, + onInteraction, dropdownContent, + // + isActive: _isActive, + bState = {}, + // }) => { + const { primaryToolId, toggles, groups } = bState; + const isActive = _isActive || (type === 'tool' && id === primaryToolId); const classes = { type: { primary: isActive @@ -23,7 +30,6 @@ const ToolbarButton = ({ : 'text-white hover:bg-secondary-dark hover:text-white focus:bg-secondary-dark focus:text-white', }, }; - const shouldShowDropdown = !!isActive && !!dropdownContent; return ( @@ -36,7 +42,14 @@ const ToolbarButton = ({ { + onInteraction({ + itemId: id, + interactionType: type, + commandName: commandName, + commandOptions: commandOptions, + }); + }} key={id} > @@ -49,15 +62,15 @@ const ToolbarButton = ({ ToolbarButton.defaultProps = { dropdownContent: null, isActive: false, - type: 'primary', + type: 'action', }; ToolbarButton.propTypes = { /* Influences background/hover styling */ - type: PropTypes.oneOf(['primary', 'secondary']), + type: PropTypes.oneOf(['action', 'toggle', 'tool']), id: PropTypes.string.isRequired, isActive: PropTypes.bool, - onClick: PropTypes.func.isRequired, + onInteraction: PropTypes.func.isRequired, icon: PropTypes.string.isRequired, label: PropTypes.string.isRequired, /** Tooltip content can be replaced for a customized content by passing a node to this value. */ diff --git a/platform/ui/src/components/Tooltip/Tooltip.jsx b/platform/ui/src/components/Tooltip/Tooltip.jsx index b8ed67e0f..8f8378052 100644 --- a/platform/ui/src/components/Tooltip/Tooltip.jsx +++ b/platform/ui/src/components/Tooltip/Tooltip.jsx @@ -24,7 +24,14 @@ const arrowPositionStyle = { }, }; -const Tooltip = ({ content, isSticky, position, tight, children, isDisabled }) => { +const Tooltip = ({ + content, + isSticky, + position, + tight, + children, + isDisabled, +}) => { const [isActive, setIsActive] = useState(false); const handleMouseOver = () => { @@ -43,7 +50,7 @@ const Tooltip = ({ content, isSticky, position, tight, children, isDisabled }) = return (
{ const [showPatientInfo, setShowPatientInfo] = useState(patientInfoVisibility); @@ -33,6 +35,8 @@ const ViewportActionBar = ({ label, isTracked, isLocked, + isRehydratable, + useAltStyling, modality, studyDate, currentSeries, @@ -52,7 +56,6 @@ const ViewportActionBar = ({ const onPatientInfoClick = () => setShowPatientInfo(!showPatientInfo); const closePatientInfo = () => setShowPatientInfo(false); - const showPatientInfoRef = useRef(null); const clickOutsideListener = useOnClickOutside( showPatientInfoRef, @@ -71,30 +74,121 @@ const ViewportActionBar = ({ const renderIconStatus = () => { if (modality === 'SR') { - const TooltipMessage = isLocked - ? () => ( -
- This SR is locked.
- Measurements cannot be duplicated. -
- ) - : () =>
This SR is unlocked.
; + // 1 - Incompatible + // 2 - Locked + // 3 - Rehydratable / Open + const state = + isRehydratable && !isLocked ? 3 : isRehydratable && isLocked ? 2 : 1; + let ToolTipMessage = null; + let StatusIcon = null; + + switch (state) { + case 1: + StatusIcon = () => ( +
+ +
+ ); + + ToolTipMessage = () => ( +
+ This structured report is not compatible +
+ with this application. +
+ ); + break; + case 2: + StatusIcon = () => ( +
+ +
+ ); + + ToolTipMessage = () => ( +
+ This structured report is currently read-only +
+ because you are tracking measurements in +
+ another viewport. +
+ ); + break; + case 3: + StatusIcon = () => ( +
+ +
+ ); + + ToolTipMessage = () =>
Click to restore measurements.
; + } + + const StatusPill = () => ( +
{ + if (state === 3) { + onPillClick?.(); + } + }} + > + + SR + + +
+ ); + return ( <> - } position="bottom-left"> -
- - SR - - {isLocked && ( - - )} -
-
+ {ToolTipMessage && ( + } position="bottom-left"> + + + )} + {!ToolTipMessage && } ); } @@ -119,13 +213,13 @@ const ViewportActionBar = ({ can be viewed
in the measurement panel ) : ( - <> - Measurements for + <> + Measurements for untracked series
will not be shown in the
measurements panel - )} + )}
@@ -137,15 +231,26 @@ const ViewportActionBar = ({ ); }; + const borderColor = useAltStyling ? '#365A6A' : '#1D205A'; + const backgroundColor = useAltStyling + ? '#031923' + : isTracked + ? '#020424' + : null; + return (
-
+
+ {label} {renderIconStatus()} - {label}
@@ -173,14 +278,14 @@ const ViewportActionBar = ({
)} {showCine && !showNavArrows && ( -
+
)} -
+
alert(`Series ${direction}`)} + onSeriesChange={direction => alert(`Series ${direction}`)} studyData={{ label: 'A', isTracked: true, isLocked: false, + isRehydratable: false, studyDate: '07-Sep-2010', currentSeries: 1, seriesDescription: @@ -45,42 +46,42 @@ import { ViewportActionBar } from '@ohif/ui'; />
-
alert(`Series ${direction}`)} - studyData={{ - label: 'A', - isTracked: false, - isLocked: true, - studyDate: '07-Sep-2010', - currentSeries: 1, - seriesDescription: - 'Series description lorem ipsum dolor sit Series description lorem ipsum dolor sit Series description lorem ipsum dolor sit ', - modality: 'CT', - patientInformation: { - patientName: 'Smith, Jane', - patientSex: 'F', - patientAge: '59', - MRN: '10000001', - thickness: '2.0mm', - spacing: '1.25mm', - scanner: 'Aquilion', - }, - }} - /> -
-
- - -
- alert(`Series ${direction}`)} + onSeriesChange={direction => alert(`Series ${direction}`)} studyData={{ label: 'A', isTracked: false, isLocked: false, + isRehydratable: true, + studyDate: '07-Sep-2010', + currentSeries: 1, + seriesDescription: + 'Series description lorem ipsum dolor sit Series description lorem ipsum dolor sit Series description lorem ipsum dolor sit ', + modality: 'CT', + patientInformation: { + patientName: 'Smith, Jane', + patientSex: 'F', + patientAge: '59', + MRN: '10000001', + thickness: '2.0mm', + spacing: '1.25mm', + scanner: 'Aquilion', + }, + }} + /> +
+
+ +
+ alert(`Series ${direction}`)} + studyData={{ + label: 'A', + isTracked: false, + isLocked: false, + isRehydratable: false, studyDate: '07-Sep-2010', currentSeries: 1, seriesDescription: @@ -99,15 +100,15 @@ import { ViewportActionBar } from '@ohif/ui'; />
-
alert(`Series ${direction}`)} + onSeriesChange={direction => alert(`Series ${direction}`)} studyData={{ label: 'A', isTracked: false, - isLocked: true, + isLocked: false, + isRehydratable: true, studyDate: '07-Sep-2010', currentSeries: 1, seriesDescription: diff --git a/platform/ui/src/views/Viewer/Viewer.mdx.todo b/platform/ui/src/views/Viewer/Viewer.mdx.todo index 8ba09fbf4..eb370881a 100644 --- a/platform/ui/src/views/Viewer/Viewer.mdx.todo +++ b/platform/ui/src/views/Viewer/Viewer.mdx.todo @@ -88,6 +88,7 @@ import { tabs } from './studyBrowserMockData'; label: 'A', isTracked: true, isLocked: false, + isRehydratable: false, studyDate: '07-Sep-2011', currentSeries: 1, seriesDescription: diff --git a/platform/ui/tailwind.config.js b/platform/ui/tailwind.config.js index c0a075701..6600e878b 100644 --- a/platform/ui/tailwind.config.js +++ b/platform/ui/tailwind.config.js @@ -46,6 +46,15 @@ module.exports = { active: '#2c3074', }, + customgreen: { + 100: '#05D97C', + }, + + customblue: { + 100: '#c4fdff', + 200: '#38daff', + }, + gray: { 100: '#f7fafc', 200: '#edf2f7', diff --git a/platform/viewer/src/appInit.js b/platform/viewer/src/appInit.js index c554655ab..ed8206a54 100644 --- a/platform/viewer/src/appInit.js +++ b/platform/viewer/src/appInit.js @@ -12,7 +12,7 @@ import { ToolBarService, ViewportGridService, HangingProtocolService, - CineService + CineService, // utils, // redux as reduxOHIF, } from '@ohif/core'; @@ -33,13 +33,17 @@ function appInit(appConfigOrFunc, defaultExtensions) { // TODO: Wire this up to Rodrigo's basic Context "ContextService" const commandsManagerConfig = { /** Used by commands to inject `viewports` from "redux" */ - getAppState: () => { }, + getAppState: () => {}, /** Used by commands to determine active context */ - getActiveContexts: () => ['VIEWER', 'DEFAULT', 'ACTIVE_VIEWPORT::CORNERSTONE'], + getActiveContexts: () => [ + 'VIEWER', + 'DEFAULT', + 'ACTIVE_VIEWPORT::CORNERSTONE', + ], }; - const servicesManager = new ServicesManager(); const commandsManager = new CommandsManager(commandsManagerConfig); + const servicesManager = new ServicesManager(commandsManager); const hotkeysManager = new HotkeysManager(commandsManager, servicesManager); const extensionManager = new ExtensionManager({ commandsManager, @@ -58,7 +62,7 @@ function appInit(appConfigOrFunc, defaultExtensions) { ToolBarService, ViewportGridService, HangingProtocolService, - CineService + CineService, ]); /** diff --git a/platform/viewer/src/routes/DataSourceWrapper.jsx b/platform/viewer/src/routes/DataSourceWrapper.jsx index 35e8b7298..205cd670c 100644 --- a/platform/viewer/src/routes/DataSourceWrapper.jsx +++ b/platform/viewer/src/routes/DataSourceWrapper.jsx @@ -6,9 +6,6 @@ import { MODULE_TYPES } from '@ohif/core'; import { useAppConfig } from '@state'; import { extensionManager } from '../App.jsx'; -let cacheMap = {}; -let total = {}; - /** * Uses route properties to determine the data source that should be passed * to the child layout template. In some instances, initiates requests and @@ -48,64 +45,56 @@ function DataSourceWrapper(props) { // But only for LayoutTemplate type of 'list'? // Or no data fetching here, and just hand down my source const STUDIES_LIMIT = 101; - const [data, setData] = useState({ studies: [], total: 0 }); + const [data, setData] = useState({ + studies: [], + total: 0, + resultsPerPage: 25, + pageNumber: 1, + }); const [isLoading, setIsLoading] = useState(false); useEffect(() => { + const queryFilterValues = _getQueryFilterValues( + history.location.search, + STUDIES_LIMIT + ); + // 204: no content async function getData() { setIsLoading(true); - const limit = STUDIES_LIMIT - 1; - const queryFilterValues = _getQueryFilterValues(history.location.search); - const { resultsPerPage = 25, pageNumber = 1 } = queryFilterValues; - const reachedLimits = parseInt((resultsPerPage * pageNumber) / STUDIES_LIMIT); - const cacheKey = `${pageNumber}-${resultsPerPage}`; - - const getFromCache = async ({ cacheKey, pageNumber, resultsPerPage, limit, options }) => { - const pagesAmount = limit / resultsPerPage; - const pageToRequest = parseInt((resultsPerPage * pageNumber) / STUDIES_LIMIT); - - let length = 0; - if (!cacheMap[cacheKey]) { - length = pageToRequest > 0 ? (pageToRequest * STUDIES_LIMIT) : 1; - const studiesPromise = dataSource.query.studies.search(options); - - for (let pageNum = 0; pageNum < pagesAmount; pageNum++) { - const currentPageNumber = (pageNum + 1) + (pageToRequest * pagesAmount); - cacheMap[`${currentPageNumber}-${resultsPerPage}`] = studiesPromise.then(function (results) { - const slicedResult = results.slice((pageNum * resultsPerPage), ((pageNum + 1) * resultsPerPage)); - length += slicedResult.length; - return slicedResult; - }); - } - } - - const cache = await cacheMap[cacheKey]; - return { cache, length, index: pageToRequest }; - }; - - const { cache: studies, index, length } = await getFromCache({ - cacheKey, - pageNumber, - resultsPerPage, - limit, - options: { ...queryFilterValues, ...{ offset: reachedLimits * limit } } - }); - - const totalKey = `${resultsPerPage}-${index}`; - total[totalKey] = total[totalKey] ? total[totalKey] + length : length; - const totals = Object.keys(total).map(key => total[key]); - const biggestIndex = totals.indexOf(Math.max(...totals)); - const biggestKey = Object.keys(total)[biggestIndex]; - const biggestTotal = total[biggestKey]; + const studies = await dataSource.query.studies.search(queryFilterValues); setIsLoading(false); - setData({ studies, total: biggestTotal }); + setData({ + studies, + total: studies.length, + resultsPerPage: queryFilterValues.resultsPerPage, + pageNumber: queryFilterValues.pageNumber, + }); } try { - getData(); + // Cache invalidation :thinking: + // - Anytime change is not just next/previous page + // - And we didn't cross a result offset range + const isFirstLoad = data.studies.length === 0; + const isSamePage = data.pageNumber === queryFilterValues.pageNumber; + const previousOffset = + Math.floor((data.pageNumber * data.resultsPerPage) / STUDIES_LIMIT) * + (STUDIES_LIMIT - 1); + const newOffset = + Math.floor( + (queryFilterValues.pageNumber * queryFilterValues.resultsPerPage) / + STUDIES_LIMIT + ) * + (STUDIES_LIMIT - 1); + const isDataInvalid = + isFirstLoad || isSamePage || newOffset !== previousOffset; + + if (isDataInvalid) { + getData(); + } } catch (ex) { console.warn(ex); } @@ -138,25 +127,32 @@ export default DataSourceWrapper; * Need generic that can be shared? Isn't this what qs is for? * @param {*} query */ -function _getQueryFilterValues(query) { +function _getQueryFilterValues(query, queryLimit) { query = new URLSearchParams(query); + const pageNumber = _tryParseInt(query.get('pageNumber'), 1); + const resultsPerPage = _tryParseInt(query.get('resultsPerPage'), 25); + const queryFilterValues = { // DCM patientId: query.get('mrn'), patientName: query.get('patientName'), studyDescription: query.get('description'), - modalitiesInStudy: query.get('modalities') && query.get('modalities').split(','), + modalitiesInStudy: + query.get('modalities') && query.get('modalities').split(','), accessionNumber: query.get('accession'), // startDate: query.get('startDate'), endDate: query.get('endDate'), page: _tryParseInt(query.get('page'), undefined), - pageNumber: _tryParseInt(query.get('pageNumber'), undefined), - resultsPerPage: _tryParseInt(query.get('resultsPerPage'), undefined), + pageNumber, + resultsPerPage, // Rarely supported server-side sortBy: query.get('sortBy'), sortDirection: query.get('sortDirection'), + // Offset... + offset: + Math.floor((pageNumber * resultsPerPage) / queryLimit) * (queryLimit - 1), }; // patientName: good diff --git a/platform/viewer/src/routes/WorkList/WorkList.jsx b/platform/viewer/src/routes/WorkList/WorkList.jsx index 08e99bc0d..ae7bc0ab3 100644 --- a/platform/viewer/src/routes/WorkList/WorkList.jsx +++ b/platform/viewer/src/routes/WorkList/WorkList.jsx @@ -24,7 +24,7 @@ import { Header, useModal, AboutModal, - UserPreferences + UserPreferences, } from '@ohif/ui'; const seriesInStudiesMap = new Map(); @@ -33,7 +33,14 @@ const seriesInStudiesMap = new Map(); * TODO: * - debounce `setFilterValues` (150ms?) */ -function WorkList({ history, data: studies, dataTotal: studiesTotal, isLoadingData, dataSource, hotkeysManager }) { +function WorkList({ + history, + data: studies, + dataTotal: studiesTotal, + isLoadingData, + dataSource, + hotkeysManager, +}) { const { hotkeyDefinitions, hotkeyDefaults } = hotkeysManager; const { show, hide } = useModal(); const { t } = useTranslation(); @@ -94,7 +101,6 @@ function WorkList({ history, data: studies, dataTotal: studiesTotal, isLoadingDa const [expandedRows, setExpandedRows] = useState([]); const [studiesWithSeriesData, setStudiesWithSeriesData] = useState([]); const numOfStudies = studiesTotal; - const totalPages = Math.floor(numOfStudies / resultsPerPage) + 1; const setFilterValues = val => { if (filterValues.pageNumber === val.pageNumber) { @@ -105,7 +111,15 @@ function WorkList({ history, data: studies, dataTotal: studiesTotal, isLoadingDa }; const onPageNumberChange = newPageNumber => { - if (newPageNumber > totalPages) { + debugger; + const oldPageNumber = filterValues.pageNumber; + const rollingPageNumberMod = Math.floor(101 / filterValues.resultsPerPage); + const rollingPageNumber = oldPageNumber % rollingPageNumberMod; + const isNextPage = newPageNumber > oldPageNumber; + const hasNextPage = + Math.max(rollingPageNumber, 1) * resultsPerPage < numOfStudies; + + if (isNextPage && !hasNextPage) { return; } @@ -170,9 +184,13 @@ function WorkList({ history, data: studies, dataTotal: studiesTotal, isLoadingDa // Query for series information useEffect(() => { const fetchSeries = async studyInstanceUid => { + debugger; try { const series = await dataSource.query.series.search(studyInstanceUid); - seriesInStudiesMap.set(studyInstanceUid, utils.sortBySeriesDate(series)); + seriesInStudiesMap.set( + studyInstanceUid, + utils.sortBySeriesDate(series) + ); setStudiesWithSeriesData([...studiesWithSeriesData, studyInstanceUid]); } catch (ex) { // TODO: UI Notification Service @@ -199,6 +217,10 @@ function WorkList({ history, data: studies, dataTotal: studiesTotal, isLoadingDa return !isEqual(filterValues, defaultFilterValues); }; + const rollingPageNumberMod = Math.floor(101 / resultsPerPage); + const rollingPageNumber = (pageNumber - 1) % rollingPageNumberMod; + const offset = resultsPerPage * rollingPageNumber; + const offsetAndTake = offset + resultsPerPage; const tableDataSource = sortedStudies.map((study, key) => { const rowKey = key + 1; const isExpanded = expandedRows.some(k => k === rowKey); @@ -229,8 +251,8 @@ function WorkList({ history, data: studies, dataTotal: studiesTotal, isLoadingDa content: patientName ? ( {patientName} ) : ( - (Empty) - ), + (Empty) + ), gridCol: 4, }, { @@ -294,13 +316,13 @@ function WorkList({ history, data: studies, dataTotal: studiesTotal, isLoadingDa seriesTableDataSource={ seriesInStudiesMap.has(studyInstanceUid) ? seriesInStudiesMap.get(studyInstanceUid).map(s => { - return { - description: s.description || '(empty)', - seriesNumber: s.seriesNumber || '', - modality: s.modality || '', - instances: s.numSeriesInstances || '', - }; - }) + return { + description: s.description || '(empty)', + seriesNumber: s.seriesNumber || '', + modality: s.modality || '', + instances: s.numSeriesInstances || '', + }; + }) : [] } > @@ -317,7 +339,7 @@ function WorkList({ history, data: studies, dataTotal: studiesTotal, isLoadingDa @@ -348,25 +370,28 @@ function WorkList({ history, data: studies, dataTotal: studiesTotal, isLoadingDa { title: t('Header:About'), icon: 'info', - onClick: () => show({ content: AboutModal, title: 'About OHIF Viewer' }) + onClick: () => show({ content: AboutModal, title: 'About OHIF Viewer' }), }, { title: t('Header:Preferences'), icon: 'settings', - onClick: () => show({ - title: t('UserPreferencesModal:User Preferences'), - content: UserPreferences, - contentProps: { - hotkeyDefaults: hotkeysManager.getValidHotkeyDefinitions(hotkeyDefaults), - hotkeyDefinitions, - onCancel: hide, - onSubmit: ({ hotkeyDefinitions }) => { - hotkeysManager.setHotkeys(hotkeyDefinitions); - hide(); + onClick: () => + show({ + title: t('UserPreferencesModal:User Preferences'), + content: UserPreferences, + contentProps: { + hotkeyDefaults: hotkeysManager.getValidHotkeyDefinitions( + hotkeyDefaults + ), + hotkeyDefinitions, + onCancel: hide, + onSubmit: ({ hotkeyDefinitions }) => { + hotkeysManager.setHotkeys(hotkeyDefinitions); + hide(); + }, + onReset: () => hotkeysManager.restoreDefaultBindings(), }, - onReset: () => hotkeysManager.restoreDefaultBindings() - } - }) + }), }, ]; @@ -378,7 +403,7 @@ function WorkList({ history, data: studies, dataTotal: studiesTotal, isLoadingDa >
100 ? 101 : numOfStudies} filtersMeta={filtersMeta} filterValues={{ ...filterValues, ...defaultSortValues }} onChange={setFilterValues} @@ -388,7 +413,7 @@ function WorkList({ history, data: studies, dataTotal: studiesTotal, isLoadingDa {hasStudies ? ( <> @@ -400,10 +425,10 @@ function WorkList({ history, data: studies, dataTotal: studiesTotal, isLoadingDa /> ) : ( -
- -
- )} +
+ +
+ )}
); }