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
This commit is contained in:
Danny Brown authored and GitHub committed 2020-09-14 21:18:46 -04:00
1 parent 365ba2fa72
commit cae54b0779
78 files changed
+1678 -1263

No files matched your search

@@ -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) {
@@ -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
@@ -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 (
<div className="p-4 bg-primary-dark">
<Input
autoFocus
className="mt-2 bg-black border-primary-main"
type="text"
containerClassName="mr-2"
value={value.label}
onChange={event => {
event.persist();
setValue(value => ({ ...value, label: event.target.value }));
}}
onKeyPress={event => {
if (event.key === 'Enter') {
onSubmitHandler({ value, action: { id: 'save' } });
}
}}
/>
</div>
);
},
},
});
}
}
export default callInputDialog;
+36 -4
View File
@@ -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;
}
+10 -4
View File
@@ -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 (
<OHIFCornerstoneViewport {...props} onNewImage={onNewImageHandler} />
<OHIFCornerstoneViewport
{...props}
ToolBarService={ToolBarService}
onNewImage={onNewImageHandler}
/>
);
};
@@ -47,8 +53,8 @@ export default {
{ name: 'cornerstone', component: ExtendedOHIFCornerstoneViewport },
];
},
getCommandsModule({ servicesManager }) {
return commandsModule({ servicesManager });
getCommandsModule({ servicesManager, commandsManager }) {
return commandsModule({ servicesManager, commandsManager });
},
};
+104 -200
View File
@@ -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 (
<div className="p-4 bg-primary-dark">
<Input
autoFocus
className="mt-2 bg-black border-primary-main"
type="text"
containerClassName="mr-2"
value={value.label}
onChange={onChangeHandler}
onKeyPress={onKeyPressHandler}
/>
</div>
);
},
},
});
}
};
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;
}
+14 -4
View File
@@ -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 };
@@ -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 };
@@ -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', {});
}