feat!: Ability to configure cornerstone tools via extension configuration (#1229)

* Fix ExtensionManager bug and add test to bandaid

* Add tools configuration to extension manager preinit

* Fix reducing of configs

* Merge internal with external configs

* Merge internal with external configs

* Remove dialog from init in measurementstable

* Testing injected configuration

* New way to set config

* Add new prop to dialog provider to allow disabling last position

* Remove code from preinit in cornerstone

* Add new prop to dialog provider to allow disabling last position

* Add centralize to dialogs

* Reorder dialogs when adding them

* Fix draggable styles (cursor)

* Remove repositioning methods from labelling flow and remove overlay from labelling manager

* Fix empty array being set in bringToFront

* Add new command to update table and pass commands manager to modules/preinit hook

* Ad UIContextMenu service / factory

* Use new contextmenu service in measurementspanel extension

* Use dialogs for arrow annotate in default

* Remove positioning funcionality from tool context menu

* Add context menu service

* Pass commandsModule to extension

* Update edit description dialog and simple dialog to position relative

* Remove style code from labelling flow and manager

* Remove eventdata from labelling

* Remove labelling code from measurement init

* Add commandsmanager to provider

* Update contextmenu provider and service

* Add touchstart and mouseclick to hide contextmenu

* Hide labelling if click/touch

* Remove labelling and context menu dead code

* Fix undefined bug if ViewerMain grid has no children

* Fix broken prop on context menu

* Update commandsmodule based on master

* Fix broken configuration

* Update script tag config

* Remove cornerstone from toolcontextmenu

* Remove cornerstone from toolcontextmenu

* Split labelling and context menu providers

* Split labelling and context menu providers

* Update test

* Destructure extensions into new array

* CR Update: Move default arrow config to cornerstone instead of default

* CR Update: Fix app configuration props structure

* CR Update: Fix app configuration prop in script tag and extract commands manager from providers

* CR Update: Create custom providers to use commandsManager

* CR Update: Use services directly in measurementspanel

* CR Update: Pass components to providers

* CR Update: Remove position from dialog

* CR Update: fix dialog prop check

* CR Update: Fix comments

* CR Update: Update documentation

* CR Update: Add test default configuration

* CR Update: Add default empy array to extensions

* CR Update: Update i18n configuration all ot match current function configuration

* CR Update: Add defaults to injected dependencies in configuration and extension configuration

* CR Update: Add defaults to configuration with no args

* Update documentation

* CR Update: Add default for tools

* CR Update: Update config object to i18n

* CR Update: spread defaults

* CR Update: Add tool configuration example to cornerstone extension

* CR Update: Add tool configuration to netlify (testing)

* CR Update: Remove netlify config for tools

* CR Update: Rollback changes to i18n to be fixed later

* CR Update: Update documentation and pass whole cornerstone config object instead of tools key

SEE: https://www.conventionalcommits.org/en/v1.0.0/#commit-message-with-both-and-breaking-change-footer

BREAKING CHANGE: modifies the exposed react <App /> components props. The contract for providing configuration for the app has changed. Please reference updated documentation for guidance.
This commit is contained in:
Igor Octaviano 2019-12-09 14:47:23 -03:00 committed by Danny Brown
parent fd7620ced9
commit 55a580659e
66 changed files with 1464 additions and 1223 deletions

View File

@ -66,6 +66,52 @@ window.config = {
};
```
The configuration can also be written as a JS Function in case you need to inject dependencies like external services:
```js
window.config = ({ servicesManager } = {}) => {
const { UIDialogService } = servicesManager.services;
return {
cornerstoneExtensionConfig: {
tools: {
ArrowAnnotate: {
configuration: {
getTextCallback: (callback, eventDetails) => UIDialogService.create({...
}
}
},
},
routerBasename: '/',
servers: {
dicomWeb: [
{
name: 'DCM4CHEE',
wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado',
qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
qidoSupportsIncludeField: true,
imageRendering: 'wadors',
thumbnailRendering: 'wadors',
},
],
},
};
};
```
You can also create a new config file and specify its path relative to the build
output's root by setting the `APP_CONFIG` environment variable. You can set the
value of this environment variable a few different ways:
- ~[Add a temporary environment variable in your shell](https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#adding-temporary-environment-variables-in-your-shell)~
- Previous `react-scripts` functionality that we need to duplicate with
`dotenv-webpack`
- ~[Add environment specific variables in `.env` file(s)](https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#adding-development-environment-variables-in-env)~
- Previous `react-scripts` functionality that we need to duplicate with
`dotenv-webpack`
- Using the `cross-env` package in an npm script:
- `"build": "cross-env APP_CONFIG=config/my-config.js react-scripts build"`
After updating the configuration, `yarn run build` to generate updated build
output.

View File

@ -24,12 +24,12 @@ include tags. Here's how it works:
</ul>
<ol start="2">
<li>Create a JS Object to hold the OHIF Viewer's configuration. Here are some
<li>Create a JS Object or Function to hold the OHIF Viewer's configuration. Here are some
example values that would allow the viewer to hit our public PACS:</li>
</ol>
```js
// Set before importing `ohif-viewer`
// Set before importing `ohif-viewer` (JS Object)
window.config = {
// default: '/'
routerBasename: '/',
@ -49,6 +49,9 @@ window.config = {
};
```
To learn more about how you can configure the OHIF Viewer, check out our
[Configuration Guide](./index.md).
<ol start="3"><li>
Render the viewer in the web page's target <code>div</code>
</li></ol>

View File

@ -12,8 +12,9 @@ export default {
*/
preRegistration({
servicesManager,
configuration: extensionConfiguration,
servicesManager = {},
commandsManager = {},
configuration = {},
}) {},
/**

View File

@ -67,6 +67,24 @@ Our Viewport wraps [cornerstonejs/react-cornerstone-viewport][react-viewport]
and is connected the redux store. This module is the most prone to change as we
hammer out our Viewport interface.
## Tool Configuration
Tools can be configured through extension configuration using the tools key:
```js
...
cornerstoneExtensionConfig: {
tools: {
ArrowAnnotate: {
configuration: {
getTextCallback: (callback, eventDetails) => callback(prompt('Enter your custom annotation')),
},
},
},
},
...
```
## Resources
### Repositories

View File

@ -47,6 +47,7 @@
"dependencies": {
"@babel/runtime": "^7.5.5",
"classnames": "^2.2.6",
"lodash.merge": "^4.6.2",
"lodash.throttle": "^4.1.1",
"query-string": "^6.8.3",
"react-cornerstone-viewport": "2.x.x"

View File

@ -248,10 +248,13 @@ class OHIFCornerstoneViewport extends Component {
// TODO: Does it make more sense to use Context?
if (this.props.children && this.props.children.length) {
childrenWithProps = this.props.children.map((child, index) => {
return React.cloneElement(child, {
viewportIndex: this.props.viewportIndex,
key: index,
});
return (
child &&
React.cloneElement(child, {
viewportIndex: this.props.viewportIndex,
key: index,
})
);
});
}

View File

@ -148,18 +148,118 @@ const commandsModule = ({ servicesManager }) => {
showDownloadViewportModal: ({ title, viewports }) => {
const activeViewportIndex = viewports.activeViewportIndex;
const { UIModalService } = servicesManager.services;
UIModalService.show({
content: CornerstoneViewportDownloadForm,
title,
contentProps: {
activeViewportIndex,
onClose: UIModalService.hide,
},
if (UIModalService) {
UIModalService.show({
content: CornerstoneViewportDownloadForm,
title,
contentProps: {
activeViewportIndex,
onClose: UIModalService.hide,
},
});
}
},
updateTableWithNewMeasurementData({
toolType,
measurementNumber,
location,
description,
}) {
// Update all measurements by measurement number
const measurementApi = OHIF.measurements.MeasurementApi.Instance;
const measurements = measurementApi.tools[toolType].filter(
m => m.measurementNumber === measurementNumber
);
measurements.forEach(measurement => {
measurement.location = location;
measurement.description = description;
measurementApi.updateMeasurement(measurement.toolType, measurement);
});
measurementApi.syncMeasurementsAndToolData();
// Update images in all active viewports
cornerstone.getEnabledElements().forEach(enabledElement => {
cornerstone.updateImage(enabledElement.element);
});
},
getNearbyToolData({ element, canvasCoordinates, availableToolTypes }) {
const nearbyTool = {};
let pointNearTool = false;
availableToolTypes.forEach(toolType => {
const elementToolData = cornerstoneTools.getToolState(
element,
toolType
);
if (!elementToolData) {
return;
}
elementToolData.data.forEach((toolData, index) => {
let elementToolInstance = cornerstoneTools.getToolForElement(
element,
toolType
);
if (!elementToolInstance) {
elementToolInstance = cornerstoneTools.getToolForElement(
element,
`${toolType}Tool`
);
}
if (!elementToolInstance) {
console.warn('Tool not found.');
return undefined;
}
if (
elementToolInstance.pointNearTool(
element,
toolData,
canvasCoordinates
)
) {
pointNearTool = true;
nearbyTool.tool = toolData;
nearbyTool.index = index;
nearbyTool.toolType = toolType;
}
});
if (pointNearTool) {
return false;
}
});
return pointNearTool ? nearbyTool : undefined;
},
removeToolState: ({ element, toolType, tool }) => {
cornerstoneTools.removeToolState(element, toolType, tool);
cornerstone.updateImage(element);
},
};
const definitions = {
getNearbyToolData: {
commandFn: actions.getNearbyToolData,
storeContexts: [],
options: {},
},
removeToolState: {
commandFn: actions.removeToolState,
storeContexts: [],
options: {},
},
updateTableWithNewMeasurementData: {
commandFn: actions.updateTableWithNewMeasurementData,
storeContexts: [],
options: {},
},
showDownloadViewportModal: {
commandFn: actions.showDownloadViewportModal,
storeContexts: ['viewports'],

View File

@ -4,6 +4,7 @@ import csTools from 'cornerstone-tools';
import initCornerstoneTools from './initCornerstoneTools.js';
import queryString from 'query-string';
import { SimpleDialog } from '@ohif/ui';
import merge from 'lodash.merge';
function fallbackMetaDataProvider(type, imageId) {
if (!imageId.includes('wado?requestType=WADO')) {
@ -26,30 +27,33 @@ cornerstone.metaData.addProvider(fallbackMetaDataProvider, -1);
/**
*
* @param {object} configuration
* @param {Object} servicesManager
* @param {Object} configuration
* @param {Object|Array} configuration.csToolsConfig
*/
export default function init({ servicesManager, configuration = {} }) {
const { UIDialogService } = servicesManager.services;
export default function init({ servicesManager, configuration }) {
const callInputDialog = (data, event, callback) => {
let dialogId = UIDialogService.create({
content: SimpleDialog.InputDialog,
defaultPosition: {
x: (event && event.currentPoints.canvas.x) || 0,
y: (event && event.currentPoints.canvas.y) || 0,
},
showOverlay: true,
contentProps: {
title: 'Enter your annotation',
label: 'New label',
measurementData: data ? { description: data.text } : {},
onClose: () => UIDialogService.dismiss({ id: dialogId }),
onSubmit: value => {
callback(value);
UIDialogService.dismiss({ id: dialogId });
const { UIDialogService } = servicesManager.services;
if (UIDialogService) {
let dialogId = UIDialogService.create({
centralize: true,
isDraggable: false,
content: SimpleDialog.InputDialog,
useLastPosition: false,
showOverlay: true,
contentProps: {
title: 'Enter your annotation',
label: 'New label',
measurementData: data ? { description: data.text } : {},
onClose: () => UIDialogService.dismiss({ id: dialogId }),
onSubmit: value => {
callback(value);
UIDialogService.dismiss({ id: dialogId });
},
},
},
});
});
}
};
const { csToolsConfig } = configuration;
@ -73,61 +77,55 @@ export default function init({ servicesManager, configuration = {} }) {
initCornerstoneTools(defaultCsToolsConfig);
// ~~ Toooools 🙌
const {
PanTool,
ZoomTool,
WwwcTool,
MagnifyTool,
StackScrollTool,
StackScrollMouseWheelTool,
// Touch
PanMultiTouchTool,
ZoomTouchPinchTool,
// Annotations
EraserTool,
BidirectionalTool,
LengthTool,
AngleTool,
FreehandRoiTool,
EllipticalRoiTool,
DragProbeTool,
RectangleRoiTool,
// Segmentation
BrushTool,
} = csTools;
const tools = [
PanTool,
ZoomTool,
WwwcTool,
MagnifyTool,
StackScrollTool,
StackScrollMouseWheelTool,
csTools.PanTool,
csTools.ZoomTool,
csTools.WwwcTool,
csTools.MagnifyTool,
csTools.StackScrollTool,
csTools.StackScrollMouseWheelTool,
// Touch
PanMultiTouchTool,
ZoomTouchPinchTool,
csTools.PanMultiTouchTool,
csTools.ZoomTouchPinchTool,
// Annotations
EraserTool,
BidirectionalTool,
LengthTool,
AngleTool,
FreehandRoiTool,
EllipticalRoiTool,
DragProbeTool,
RectangleRoiTool,
csTools.ArrowAnnotateTool,
csTools.EraserTool,
csTools.BidirectionalTool,
csTools.LengthTool,
csTools.AngleTool,
csTools.FreehandRoiTool,
csTools.EllipticalRoiTool,
csTools.DragProbeTool,
csTools.RectangleRoiTool,
// Segmentation
BrushTool,
csTools.BrushTool,
];
tools.forEach(tool => csTools.addTool(tool));
csTools.addTool(csTools.ArrowAnnotateTool, {
configuration: {
getTextCallback: (callback, eventDetails) =>
callInputDialog(null, eventDetails, callback),
changeTextCallback: (data, eventDetails, callback) =>
callInputDialog(data, eventDetails, callback),
/* Add extension tools configuration here. */
const extensionToolsConfiguration = {
ArrowAnnotate: {
configuration: {
getTextCallback: (callback, eventDetails) =>
callInputDialog(null, eventDetails, callback),
changeTextCallback: (data, eventDetails, callback) =>
callInputDialog(data, eventDetails, callback),
},
},
});
};
const isEmpty = obj => Object.keys(obj).length < 1;
if (!isEmpty(configuration.tools) || !isEmpty(extensionToolsConfiguration)) {
/* Add tools with its custom props through extension configuration. */
tools.forEach(tool => {
const toolName = tool.name.replace('Tool', '');
const configurationToolProps = configuration.tools[toolName] || {};
const extensionToolProps = extensionToolsConfiguration[toolName];
let props = merge(extensionToolProps, configurationToolProps);
csTools.addTool(tool, props);
});
} else {
tools.forEach(tool => csTools.addTool(tool));
}
csTools.setToolActive('Pan', { mouseButtonMask: 4 });
csTools.setToolActive('Zoom', { mouseButtonMask: 2 });

View File

@ -358,10 +358,13 @@ class OHIFVTKViewport extends Component {
// TODO: Does it make more sense to use Context?
if (this.props.children && this.props.children.length) {
childrenWithProps = this.props.children.map((child, index) => {
return React.cloneElement(child, {
viewportIndex: this.props.viewportIndex,
key: index,
});
return (
child &&
React.cloneElement(child, {
viewportIndex: this.props.viewportIndex,
key: index,
})
);
});
}

View File

@ -59,7 +59,7 @@ export class CommandsManager {
*
* @method
* @param {string} contextName - Namespace for commands
* @returs {Object} - the matched context
* @returns {Object} - the matched context
*/
getContext(contextName) {
const context = this.contexts[contextName];

View File

@ -26,7 +26,7 @@ export default class ExtensionManager {
const hasConfiguration = Array.isArray(extension);
if (hasConfiguration) {
const [ohifExtension, configuration] = extensions;
const [ohifExtension, configuration] = extension;
this.registerExtension(ohifExtension, configuration);
} else {
this.registerExtension(extension);

View File

@ -37,6 +37,24 @@ describe('ExtensionManager.js', () => {
// Assert
expect(extensionManager.registerExtension.mock.calls.length).toBe(3);
});
it('calls registerExtension() for each extension passing its configuration if tuple', () => {
const fakeConfiguration = { testing: true };
extensionManager.registerExtension = jest.fn();
// SUT
const fakeExtensions = [
{ one: '1' },
[{ two: '2' }, fakeConfiguration],
{ three: '3 ' },
];
extensionManager.registerExtensions(fakeExtensions);
// Assert
expect(extensionManager.registerExtension.mock.calls[1]).toContain(
fakeConfiguration
);
});
});
describe('registerExtension()', () => {

View File

@ -23,6 +23,8 @@ import {
createUINotificationService,
createUIModalService,
createUIDialogService,
createUIContextMenuService,
createUILabellingFlowService,
} from './services';
const OHIF = {
@ -53,6 +55,8 @@ const OHIF = {
createUINotificationService,
createUIModalService,
createUIDialogService,
createUIContextMenuService,
createUILabellingFlowService,
};
export {
@ -82,6 +86,8 @@ export {
createUINotificationService,
createUIModalService,
createUIDialogService,
createUIContextMenuService,
createUILabellingFlowService,
};
export { OHIF };

View File

@ -13,6 +13,8 @@ describe('Top level exports', () => {
'createUINotificationService',
'createUIModalService',
'createUIDialogService',
'createUIContextMenuService',
'createUILabellingFlowService',
//
'utils',
'studies',

View File

@ -0,0 +1,63 @@
/**
* UI Context Menu
*
* @typedef {Object} ContextMenuProps
* @property {Event} event The event with tool information.
*/
const uiContextMenuServicePublicAPI = {
name: 'UIContextMenuService',
hide,
show,
setServiceImplementation,
};
const uiContextMenuServiceImplementation = {
_show: () => console.warn('show() NOT IMPLEMENTED'),
_hide: () => console.warn('hide() NOT IMPLEMENTED'),
};
function createUIContextMenuService() {
return uiContextMenuServicePublicAPI;
}
/**
* Show a new UI ContextMenu dialog;
*
* @param {ContextMenuProps} props { event }
*/
function show({ event }) {
return uiContextMenuServiceImplementation._show({
event,
});
}
/**
* Hide a UI ContextMenu dialog;
*
*/
function hide() {
return uiContextMenuServiceImplementation._hide();
}
/**
*
*
* @param {*} {
* show: showImplementation,
* hide: hideImplementation,
* }
*/
function setServiceImplementation({
show: showImplementation,
hide: hideImplementation,
}) {
if (showImplementation) {
uiContextMenuServiceImplementation._show = showImplementation;
}
if (hideImplementation) {
uiContextMenuServiceImplementation._hide = hideImplementation;
}
}
export default createUIContextMenuService;

View File

@ -17,8 +17,9 @@
* @property {Object} contentProps The dialog content props.
* @property {boolean} [isDraggable=true] Controls if dialog content is draggable or not.
* @property {boolean} [showOverlay=false] Controls dialog overlay.
* @property {boolean} [centralize=false] Center the dialog on the screen.
* @property {boolean} [preservePosition=true] Use last position instead of default.
* @property {ElementPosition} defaultPosition Specifies the `x` and `y` that the dragged item should start at.
* @property {ElementPosition} position If this property is present, the item becomes 'controlled' and is not responsive to user input.
* @property {Function} onStart Called when dragging starts. If `false` is returned any handler, the action will cancel.
* @property {Function} onStop Called when dragging stops.
* @property {Function} onDrag Called while dragging.
@ -45,7 +46,7 @@ function createUIDialogService() {
/**
* Show a new UI dialog;
*
* @param {DialogProps} props { id, content, contentProps, onStart, onDrag, onStop, isDraggable, showOverlay, defaultPosition, position }
* @param {DialogProps} props { id, content, contentProps, onStart, onDrag, onStop, centralize, isDraggable, showOverlay, preservePosition, defaultPosition }
*/
function create({
id,
@ -54,10 +55,11 @@ function create({
onStart,
onDrag,
onStop,
centralize = false,
preservePosition = true,
isDraggable = true,
showOverlay = false,
defaultPosition,
position,
}) {
return uiDialogServiceImplementation._create({
id,
@ -66,10 +68,11 @@ function create({
onStart,
onDrag,
onStop,
centralize,
preservePosition,
isDraggable,
showOverlay,
defaultPosition,
position,
});
}

View File

@ -0,0 +1,68 @@
/**
* UI Labelling Flow
*
* @typedef {Object} LabellingFlowProps
* @property {Object} defaultPosition The position of the labelling dialog.
* @property {boolean} centralize conditional to center the labelling dialog.
* @property {Object} props The labelling props.
*
*/
const uiLabellingFlowServicePublicAPI = {
name: 'UILabellingFlowService',
show,
hide,
setServiceImplementation,
};
const uiLabellingFlowServiceImplementation = {
_show: () => console.warn('show() NOT IMPLEMENTED'),
_hide: () => console.warn('hide() NOT IMPLEMENTED'),
};
function createUILabellingFlowService() {
return uiLabellingFlowServicePublicAPI;
}
/**
* Hide a UI LabellingFlow dialog;
*
*/
function hide() {
return uiLabellingFlowServiceImplementation._hide();
}
/**
* Show a new UI LabellingFlow dialog;
*
* @param {LabellingFlowProps} props { defaultPosition, centralize, props }
*/
function show({ defaultPosition, centralize, props }) {
return uiLabellingFlowServiceImplementation._show({
defaultPosition,
centralize,
props,
});
}
/**
*
*
* @param {*} {
* show: showImplementation,
* hide: hideImplementation,
* }
*/
function setServiceImplementation({
show: showImplementation,
hide: hideImplementation,
}) {
if (showImplementation) {
uiLabellingFlowServiceImplementation._show = showImplementation;
}
if (hideImplementation) {
uiLabellingFlowServiceImplementation._hide = hideImplementation;
}
}
export default createUILabellingFlowService;

View File

@ -2,10 +2,14 @@ import ServicesManager from './ServicesManager.js';
import createUINotificationService from './UINotificationService';
import createUIModalService from './UIModalService';
import createUIDialogService from './UIDialogService';
import createUIContextMenuService from './UIContextMenuService';
import createUILabellingFlowService from './UILabellingFlowService';
export {
createUINotificationService,
createUIModalService,
createUIDialogService,
createUIContextMenuService,
createUILabellingFlowService,
ServicesManager,
};

View File

@ -6,9 +6,7 @@
position: relative
.simpleDialog
position: fixed;
top: 0px;
left: 0px;
position: relative;
z-index: 1000;
border: 0;
border-radius: 6px;

View File

@ -0,0 +1,147 @@
import React, {
createContext,
useContext,
useEffect,
useCallback,
} from 'react';
import PropTypes from 'prop-types';
import { useDialog } from '@ohif/ui';
import { useLabellingFlow } from '@ohif/ui';
const ContextMenuContext = createContext(null);
const { Provider } = ContextMenuContext;
export const useContextMenu = () => useContext(ContextMenuContext);
const ContextMenuProvider = ({
children,
service,
contextMenuComponent: ContextMenuComponent,
onDelete,
}) => {
const { create, dismiss } = useDialog();
const { show: showLabellingFlow } = useLabellingFlow();
/**
* Sets the implementation of a context menu service that can be used by extensions.
*
* @returns void
*/
useEffect(() => {
if (service) {
service.setServiceImplementation({
show,
hide,
});
}
}, [hide, service, show]);
const hide = useCallback(() => dismiss({ id: 'context-menu' }), [dismiss]);
/**
* Show the context menu and override its configuration props.
*
* @param {ContextMenuProps} props { eventData, isTouchEvent, onClose, visible }
* @returns void
*/
const show = useCallback(
({ event }) => {
hide();
create({
id: 'context-menu',
isDraggable: false,
preservePosition: false,
content: ContextMenuComponent,
contentProps: {
eventData: event,
onDelete: (nearbyToolData, eventData) =>
onDelete(nearbyToolData, eventData),
onClose: () => dismiss({ id: 'context-menu' }),
onSetLabel: (eventData, measurementData) =>
showLabellingFlow({
event: eventData,
centralize: true,
props: {
measurementData,
skipAddLabelButton: true,
editLocation: true,
},
}),
onSetDescription: (eventData, measurementData) =>
showLabellingFlow({
event: eventData,
centralize: false,
defaultPosition: _getDefaultPosition(eventData),
props: {
measurementData,
editDescriptionOnDialog: true,
},
}),
},
defaultPosition: _getDefaultPosition(event),
});
},
[ContextMenuComponent, create, dismiss, hide, onDelete, showLabellingFlow]
);
const _getDefaultPosition = event => ({
x: (event && event.currentPoints.client.x) || 0,
y: (event && event.currentPoints.client.y) || 0,
});
return (
<Provider
value={{
show,
hide,
}}
>
{children}
</Provider>
);
};
/**
* Higher Order Component to use the context menu methods through a Class Component.
*
* @returns
*/
export const withContextMenu = Component => {
return function WrappedComponent(props) {
const { show, hide } = useContextMenu();
return (
<Component
{...props}
modal={{
show,
hide,
}}
/>
);
};
};
ContextMenuProvider.defaultProps = {
service: null,
};
ContextMenuProvider.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]).isRequired,
service: PropTypes.shape({
setServiceImplementation: PropTypes.func,
}),
contextMenuComponent: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
PropTypes.func,
]).isRequired,
onDelete: PropTypes.func.isRequired,
};
export default ContextMenuProvider;
export const ContextMenuConsumer = ContextMenuContext.Consumer;

View File

@ -20,7 +20,30 @@ export const useDialog = () => useContext(DialogContext);
const DialogProvider = ({ children, service }) => {
const [isDragging, setIsDragging] = useState(false);
const [dialogs, setDialogs] = useState([]);
const [lastDialogId, setLastDialogId] = useState(null);
const [lastDialogPosition, setLastDialogPosition] = useState(null);
const [centerPositions, setCenterPositions] = useState([]);
useEffect(() => {
setCenterPositions(
dialogs.map(dialog => ({
id: dialog.id,
...getCenterPosition(dialog.id),
}))
);
}, [dialogs]);
const getCenterPosition = id => {
const root = document.querySelector('#root');
const centerX = root.offsetLeft + root.offsetWidth / 2;
const centerY = root.offsetTop + root.offsetHeight / 2;
const item = document.querySelector(`#draggableItem-${id}`);
const itemBounds = item.getBoundingClientRect();
return {
x: centerX - itemBounds.width / 2,
y: centerY - itemBounds.height / 2,
};
};
/**
* Sets the implementation of a dialog service that can be used by extensions.
@ -42,13 +65,16 @@ const DialogProvider = ({ children, service }) => {
* @property {Object} contentProps The dialog content props.
* @property {boolean} isDraggable Controls if dialog content is draggable or not.
* @property {boolean} showOverlay Controls dialog overlay.
* @property {boolean} centralize Center the dialog on the screen.
* @property {boolean} preservePosition Use last position instead of default.
* @property {ElementPosition} defaultPosition Specifies the `x` and `y` that the dragged item should start at.
* @property {ElementPosition} position If this property is present, the item becomes 'controlled' and is not responsive to user input.
* @property {Function} onStart Called when dragging starts. If `false` is returned any handler, the action will cancel.
* @property {Function} onStop Called when dragging stops.
* @property {Function} onDrag Called while dragging.
*/
useEffect(() => _bringToFront(lastDialogId), [_bringToFront, lastDialogId]);
/**
* Creates a new dialog and return its id.
*
@ -64,6 +90,7 @@ const DialogProvider = ({ children, service }) => {
}
setDialogs(dialogs => [...dialogs, { ...props, id: dialogId }]);
setLastDialogId(dialogId);
return dialogId;
}, []);
@ -75,9 +102,11 @@ const DialogProvider = ({ children, service }) => {
* @property {string} props.id The dialog id.
* @returns void
*/
const dismiss = useCallback(({ id }) => {
setDialogs(dialogs => dialogs.filter(dialog => dialog.id !== id));
}, []);
const dismiss = useCallback(
({ id }) =>
setDialogs(dialogs => dialogs.filter(dialog => dialog.id !== id)),
[]
);
/**
* Dismisses all dialogs.
@ -101,14 +130,14 @@ const DialogProvider = ({ children, service }) => {
* @param {string} id The dialog id.
* @returns void
*/
const _bringToFront = id => {
const _bringToFront = useCallback(id => {
setDialogs(dialogs => {
const topDialog = dialogs.find(dialog => dialog.id === id);
return topDialog
? [...dialogs.filter(dialog => dialog.id !== id), topDialog]
: [];
: dialogs;
});
};
}, []);
const renderDialogs = () =>
dialogs.map(dialog => {
@ -116,20 +145,27 @@ const DialogProvider = ({ children, service }) => {
id,
content: DialogContent,
contentProps,
position,
defaultPosition,
centralize = false,
preservePosition = true,
isDraggable = true,
onStart,
onStop,
onDrag,
} = dialog;
let position =
(preservePosition && lastDialogPosition) || defaultPosition;
if (centralize) {
position = centerPositions.find(position => position.id === id);
}
return (
<Draggable
key={id}
disabled={!isDraggable}
position={position}
defaultPosition={lastDialogPosition || defaultPosition}
defaultPosition={position}
bounds="parent"
onStart={event => {
const e = event || window.event;
@ -169,7 +205,11 @@ const DialogProvider = ({ children, service }) => {
>
<div
id={`draggableItem-${id}`}
className={classNames('DraggableItem', isDragging && 'dragging')}
className={classNames(
'DraggableItem',
isDragging && 'dragging',
isDraggable && 'draggable'
)}
style={{ zIndex: '999', position: 'absolute' }}
onClick={() => _bringToFront(id)}
>

View File

@ -1,8 +1,8 @@
.DraggableItem
.DraggableItem.draggable
div
cursor: grab !important
.DraggableItem.dragging
.DraggableItem.draggable.dragging
div
cursor: grabbing !important

View File

@ -0,0 +1,136 @@
import React, {
createContext,
useContext,
useEffect,
useCallback,
} from 'react';
import PropTypes from 'prop-types';
import { useDialog } from './DialogProvider';
const LabellingFlowContext = createContext(null);
const { Provider } = LabellingFlowContext;
export const useLabellingFlow = () => useContext(LabellingFlowContext);
const LabellingFlowProvider = ({
children,
service,
labellingComponent: LabellingComponent,
onUpdateLabelling,
}) => {
const { create, dismiss } = useDialog();
/**
* Sets the implementation of a labelling flow service that can be used by extensions.
*
* @returns void
*/
useEffect(() => {
if (service) {
service.setServiceImplementation({
show,
hide,
});
}
}, [hide, service, show]);
const hide = useCallback(() => dismiss({ id: 'labelling' }), [dismiss]);
const show = useCallback(
({ centralize, defaultPosition, props }) => {
hide();
create({
id: 'labelling',
centralize,
isDraggable: false,
showOverlay: true,
content: LabellingComponent,
defaultPosition,
contentProps: {
visible: true,
measurementData: props.measurementData,
labellingDoneCallback: () => dismiss({ id: 'labelling' }),
updateLabelling: labellingData =>
_updateLabellingHandler(labellingData, props.measurementData),
...props,
},
});
},
[LabellingComponent, _updateLabellingHandler, create, dismiss, hide]
);
const _updateLabellingHandler = useCallback(
(labellingData, measurementData) => {
const { location, description, response } = labellingData;
if (location) {
measurementData.location = location;
}
measurementData.description = description || '';
if (response) {
measurementData.response = response;
}
onUpdateLabelling(labellingData, measurementData);
},
[onUpdateLabelling]
);
return (
<Provider
value={{
show,
hide,
}}
>
{children}
</Provider>
);
};
/**
* Higher Order Component to use the labelling flow methods through a Class Component.
*
* @returns
*/
export const withLabellingFlow = Component => {
return function WrappedComponent(props) {
const { show, hide } = useLabellingFlow();
return (
<Component
{...props}
modal={{
show,
hide,
}}
/>
);
};
};
LabellingFlowProvider.defaultProps = {
service: null,
};
LabellingFlowProvider.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]).isRequired,
service: PropTypes.shape({
setServiceImplementation: PropTypes.func,
}),
labellingComponent: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
PropTypes.func,
]).isRequired,
onUpdateLabelling: PropTypes.func.isRequired,
};
export default LabellingFlowProvider;
export const LabellingFlowConsumer = LabellingFlowContext.Consumer;

View File

@ -18,3 +18,15 @@ export {
withDialog,
useDialog,
} from './DialogProvider.js';
export {
default as ContextMenuProvider,
withContextMenu,
useContextMenu,
ContextMenuConsumer,
} from './ContextMenuProvider.js';
export {
default as LabellingFlowProvider,
withLabellingFlow,
useLabellingFlow,
LabellingFlowConsumer,
} from './LabellingFlowProvider.js';

View File

@ -60,6 +60,14 @@ import {
ModalConsumer,
useModal,
withModal,
ContextMenuProvider,
ContextMenuConsumer,
useContextMenu,
withContextMenu,
LabellingFlowProvider,
LabellingFlowConsumer,
useLabellingFlow,
withLabellingFlow,
} from './contextProviders';
export {
@ -117,6 +125,14 @@ export {
DialogProvider,
withDialog,
useDialog,
ContextMenuProvider,
ContextMenuConsumer,
useContextMenu,
withContextMenu,
LabellingFlowProvider,
LabellingFlowConsumer,
useLabellingFlow,
withLabellingFlow,
// Hooks
useDebounce,
useMedia,

View File

@ -1,6 +1,7 @@
window.config = {
// default: '/'
routerBasename: '/',
whiteLabelling: {},
extensions: [],
showStudyList: true,
filterQueryParam: false,
@ -69,4 +70,5 @@ window.config = {
// ~ Cornerstone Tools
{ commandName: 'setZoomTool', label: 'Zoom', keys: ['z'] },
],
cornerstoneExtensionConfig: { tools: {} },
};

View File

@ -1,5 +1,6 @@
window.config = {
routerBasename: '/',
whiteLabelling: {},
extensions: [],
showStudyList: true,
servers: {

View File

@ -1,5 +1,6 @@
window.config = {
routerBasename: '/',
whiteLabelling: {},
showStudyList: true,
servers: {
dicomWeb: [

View File

@ -1,5 +1,6 @@
window.config = {
routerBasename: '/',
whiteLabelling: {},
showStudyList: true,
servers: {
// This is an array, but we'll only use the first entry for now

View File

@ -1,5 +1,6 @@
window.config = {
routerBasename: '/',
whiteLabelling: {},
showStudyList: true,
servers: {
// This is an array, but we'll only use the first entry for now

View File

@ -1,5 +1,6 @@
window.config = {
routerBasename: '/',
whiteLabelling: {},
enableGoogleCloudAdapter: true,
servers: {
// This is an array, but we'll only use the first entry for now
@ -14,7 +15,8 @@ window.config = {
client_id: 'YOURCLIENTID.apps.googleusercontent.com',
redirect_uri: '/callback', // `OHIFStandaloneViewer.js`
response_type: 'id_token token',
scope: 'email profile openid https://www.googleapis.com/auth/cloudplatformprojects.readonly https://www.googleapis.com/auth/cloud-healthcare', // email profile openid
scope:
'email profile openid https://www.googleapis.com/auth/cloudplatformprojects.readonly https://www.googleapis.com/auth/cloud-healthcare', // email profile openid
// ~ OPTIONAL
post_logout_redirect_uri: '/logout-redirect.html',
revoke_uri: 'https://accounts.google.com/o/oauth2/revoke?token=',
@ -23,4 +25,4 @@ window.config = {
},
],
studyListFunctionsEnabled: true,
}
};

View File

@ -1,6 +1,7 @@
window.config = {
// default: '/'
routerBasename: '/',
whiteLabelling: {},
// default: ''
showStudyList: true,
servers: {

View File

@ -1,5 +1,6 @@
window.config = {
routerBasename: '/pwa',
whiteLabelling: {},
showStudyList: true,
servers: {
dicomWeb: [

View File

@ -1,5 +1,6 @@
window.config = {
routerBasename: '/',
whiteLabelling: {},
showStudyList: true,
servers: {
dicomWeb: [

View File

@ -1,74 +1,102 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="theme-color" content="#000000" />
<meta name="mobile-web-app-capable" content="yes">
<meta name="application-name" content="OHIF Viewer">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="@ohif/viewer">
<meta name="msapplication-TileColor" content="#fff">
<meta name="msapplication-TileImage" content="<%= PUBLIC_URL %>assets/mstile-144x144.png">
<meta name="msapplication-config" content="<%= PUBLIC_URL %>assets/browserconfig.xml">
<link rel="manifest" href="<%= PUBLIC_URL %>manifest.json">
<link rel="shortcut icon" href="<%= PUBLIC_URL %>assets/favicon.ico">
<link rel="icon" type="image/png" sizes="16x16" href="<%= PUBLIC_URL %>assets/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="<%= PUBLIC_URL %>assets/favicon-32x32.png">
<link rel="apple-touch-icon" sizes="57x57" href="<%= PUBLIC_URL %>assets/apple-touch-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="<%= PUBLIC_URL %>assets/apple-touch-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="<%= PUBLIC_URL %>assets/apple-touch-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="<%= PUBLIC_URL %>assets/apple-touch-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="<%= PUBLIC_URL %>assets/apple-touch-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="<%= PUBLIC_URL %>assets/apple-touch-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="<%= PUBLIC_URL %>assets/apple-touch-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="<%= PUBLIC_URL %>assets/apple-touch-icon-152x152.png">
<link rel="apple-touch-icon" sizes="167x167" href="<%= PUBLIC_URL %>assets/apple-touch-icon-167x167.png">
<link rel="apple-touch-icon" sizes="180x180" href="<%= PUBLIC_URL %>assets/apple-touch-icon-180x180.png">
<link rel="apple-touch-icon" sizes="1024x1024" href="<%= PUBLIC_URL %>assets/apple-touch-icon-1024x1024.png">
<link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 480px) and (-webkit-device-pixel-ratio: 1)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-320x460.png">
<link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 480px) and (-webkit-device-pixel-ratio: 2)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-640x920.png">
<link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-640x1096.png">
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-750x1294.png">
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 3)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1182x2208.png">
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 3)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1242x2148.png">
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 1)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-748x1024.png">
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 1)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-768x1004.png">
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 2)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1496x2048.png">
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 2)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1536x2008.png">
<link rel="icon" type="image/png" sizes="228x228" href="<%= PUBLIC_URL %>assets/coast-228x228.png">
<link rel="yandex-tableau-widget" href="<%= PUBLIC_URL %>assets/yandex-browser-manifest.json">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="theme-color" content="#000000" />
<meta name="mobile-web-app-capable" content="yes">
<meta name="application-name" content="OHIF Viewer">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="@ohif/viewer">
<meta name="msapplication-TileColor" content="#fff">
<meta name="msapplication-TileImage" content="<%= PUBLIC_URL %>assets/mstile-144x144.png">
<meta name="msapplication-config" content="<%= PUBLIC_URL %>assets/browserconfig.xml">
<link rel="manifest" href="<%= PUBLIC_URL %>manifest.json">
<!-- Built with: https://polyfill.io/v3/url-builder/ -->
<!-- Targets IE11 -->
<script src="https://polyfill.io/v3/polyfill.min.js?flags=gated&features=default%2CObject.values%2CArray.prototype.flat%2CObject.entries%2CSymbol%2CArray.prototype.includes%2CString.prototype.repeat%2CArray.prototype.find"></script>
<script type="text/javascript">
window.PUBLIC_URL = '<%= PUBLIC_URL %>';
</script>
<script type="text/javascript" src="<%= PUBLIC_URL %>app-config.js"></script>
<script type="module" src="<%= PUBLIC_URL %>init-service-worker.js"></script>
<link rel="shortcut icon" href="<%= PUBLIC_URL %>assets/favicon.ico">
<link rel="icon" type="image/png" sizes="16x16" href="<%= PUBLIC_URL %>assets/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="<%= PUBLIC_URL %>assets/favicon-32x32.png">
<link rel="apple-touch-icon" sizes="57x57" href="<%= PUBLIC_URL %>assets/apple-touch-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="<%= PUBLIC_URL %>assets/apple-touch-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="<%= PUBLIC_URL %>assets/apple-touch-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="<%= PUBLIC_URL %>assets/apple-touch-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="<%= PUBLIC_URL %>assets/apple-touch-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="<%= PUBLIC_URL %>assets/apple-touch-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="<%= PUBLIC_URL %>assets/apple-touch-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="<%= PUBLIC_URL %>assets/apple-touch-icon-152x152.png">
<link rel="apple-touch-icon" sizes="167x167" href="<%= PUBLIC_URL %>assets/apple-touch-icon-167x167.png">
<link rel="apple-touch-icon" sizes="180x180" href="<%= PUBLIC_URL %>assets/apple-touch-icon-180x180.png">
<link rel="apple-touch-icon" sizes="1024x1024" href="<%= PUBLIC_URL %>assets/apple-touch-icon-1024x1024.png">
<link rel="apple-touch-startup-image"
media="(device-width: 320px) and (device-height: 480px) and (-webkit-device-pixel-ratio: 1)"
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-320x460.png">
<link rel="apple-touch-startup-image"
media="(device-width: 320px) and (device-height: 480px) and (-webkit-device-pixel-ratio: 2)"
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-640x920.png">
<link rel="apple-touch-startup-image"
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2)"
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-640x1096.png">
<link rel="apple-touch-startup-image"
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2)"
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-750x1294.png">
<link rel="apple-touch-startup-image"
media="(device-width: 414px) and (device-height: 736px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 3)"
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1182x2208.png">
<link rel="apple-touch-startup-image"
media="(device-width: 414px) and (device-height: 736px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 3)"
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1242x2148.png">
<link rel="apple-touch-startup-image"
media="(device-width: 768px) and (device-height: 1024px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 1)"
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-748x1024.png">
<link rel="apple-touch-startup-image"
media="(device-width: 768px) and (device-height: 1024px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 1)"
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-768x1004.png">
<link rel="apple-touch-startup-image"
media="(device-width: 768px) and (device-height: 1024px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 2)"
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1496x2048.png">
<link rel="apple-touch-startup-image"
media="(device-width: 768px) and (device-height: 1024px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 2)"
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1536x2008.png">
<link rel="icon" type="image/png" sizes="228x228" href="<%= PUBLIC_URL %>assets/coast-228x228.png">
<link rel="yandex-tableau-widget" href="<%= PUBLIC_URL %>assets/yandex-browser-manifest.json">
<title>OHIF Viewer</title>
<!-- Built with: https://polyfill.io/v3/url-builder/ -->
<!-- Targets IE11 -->
<script
src="https://polyfill.io/v3/polyfill.min.js?flags=gated&features=default%2CObject.values%2CArray.prototype.flat%2CObject.entries%2CSymbol%2CArray.prototype.includes%2CString.prototype.repeat%2CArray.prototype.find"></script>
<script type="text/javascript">
window.PUBLIC_URL = '<%= PUBLIC_URL %>';
</script>
<script type="text/javascript" src="<%= PUBLIC_URL %>app-config.js"></script>
<script type="module" src="<%= PUBLIC_URL %>init-service-worker.js"></script>
<!-- WEB FONTS -->
<link
href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700&display=swap"
rel="stylesheet"
/>
<title>OHIF Viewer</title>
<!-- EXTENSIONS -->
<!-- <script type="text/javascript" src="path/to/some-extension.js"></script>
<!-- WEB FONTS -->
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700&display=swap" rel="stylesheet" />
<!-- EXTENSIONS -->
<!-- <script type="text/javascript" src="path/to/some-extension.js"></script>
<script>
// If configuration file is a JS Object:
window.config.extensions = [SomeExtension];
// If configuration file is a JS Function:
window.config = () => {
return {
extensions: [SomeExtension]
};
};
</script> -->
</head>
</head>
<body>
<noscript> You need to enable JavaScript to run this app. </noscript>
<body>
<noscript> You need to enable JavaScript to run this app. </noscript>
<div id="root"></div>
</body>
<div id="root"></div>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@ -4,9 +4,13 @@ import { I18nextProvider } from 'react-i18next';
import PropTypes from 'prop-types';
import { Provider } from 'react-redux';
import { BrowserRouter as Router } from 'react-router-dom';
import OHIFCornerstoneExtension from '@ohif/extension-cornerstone';
import { hot } from 'react-hot-loader/root';
import OHIFCornerstoneExtension from '@ohif/extension-cornerstone';
import ToolContextMenu from './connectedComponents/ToolContextMenu';
import LabellingManager from './components/Labelling/LabellingManager';
import {
SnackbarProvider,
ModalProvider,
@ -14,6 +18,11 @@ import {
OHIFModal,
} from '@ohif/ui';
import {
LabellingFlowProvider,
ContextMenuProvider,
} from './appCustomProviders';
import {
CommandsManager,
ExtensionManager,
@ -22,8 +31,10 @@ import {
createUINotificationService,
createUIModalService,
createUIDialogService,
createUIContextMenuService,
createUILabellingFlowService,
utils,
redux as reduxOHIF
redux as reduxOHIF,
} from '@ohif/core';
import i18n from '@ohif/i18n';
@ -46,12 +57,12 @@ import OHIFStandaloneViewer from './OHIFStandaloneViewer';
/** Store */
import { getActiveContexts } from './store/layout/selectors.js';
import store from './store';
const { setUserPreferences } = reduxOHIF.actions;
/** Contexts */
import WhiteLabellingContext from './context/WhiteLabellingContext';
import UserManagerContext from './context/UserManagerContext';
import AppContext from './context/AppContext';
const { setUserPreferences } = reduxOHIF.actions;
/** ~~~~~~~~~~~~~ Application Setup */
const commandsManagerConfig = {
@ -63,6 +74,8 @@ const commandsManagerConfig = {
const UINotificationService = createUINotificationService();
const UIModalService = createUIModalService();
const UIDialogService = createUIDialogService();
const UIContextMenuService = createUIContextMenuService();
const UILabellingFlowService = createUILabellingFlowService();
/** Managers */
const commandsManager = new CommandsManager(commandsManagerConfig);
@ -79,23 +92,25 @@ window.store = store;
class App extends Component {
static propTypes = {
routerBasename: PropTypes.string.isRequired,
servers: PropTypes.object.isRequired,
//
oidc: PropTypes.array,
whiteLabelling: PropTypes.object,
extensions: PropTypes.arrayOf(
config: PropTypes.oneOfType([
PropTypes.func,
PropTypes.shape({
id: PropTypes.string.isRequired,
})
),
hotkeys: PropTypes.array,
routerBasename: PropTypes.string.isRequired,
oidc: PropTypes.array,
whiteLabelling: PropTypes.object,
extensions: PropTypes.array,
}),
]).isRequired,
defaultExtensions: PropTypes.array,
};
static defaultProps = {
whiteLabelling: {},
oidc: [],
extensions: [],
config: {
whiteLabelling: {},
oidc: [],
extensions: [],
},
defaultExtensions: [],
};
_appConfig;
@ -104,31 +119,59 @@ class App extends Component {
constructor(props) {
super(props);
this._appConfig = props;
const { config, defaultExtensions } = props;
const { servers, extensions, hotkeys, oidc } = props;
const appDefaultConfig = {
cornerstoneExtensionConfig: {},
extensions: [],
routerBasename: '/',
whiteLabelling: {},
};
this._appConfig = {
...appDefaultConfig,
...(typeof config === 'function' ? config({ servicesManager }) : config),
};
const {
servers,
hotkeys,
cornerstoneExtensionConfig,
extensions,
oidc,
} = this._appConfig;
this.initUserManager(oidc);
_initServices([UINotificationService, UIModalService, UIDialogService]);
_initExtensions(extensions, hotkeys);
_initServices([
UINotificationService,
UIModalService,
UIDialogService,
UIContextMenuService,
UILabellingFlowService,
]);
_initExtensions(
[...defaultExtensions, ...extensions],
cornerstoneExtensionConfig
);
/*
* Must run after extension commands are registered
* if there is no hotkeys from localStorage set up from config.
*/
_initHotkeys(hotkeys);
_initServers(servers);
initWebWorkers();
}
render() {
const { whiteLabelling, routerBasename } = this.props;
const userManager = this._userManager;
const config = {
appConfig: this._appConfig,
};
if (userManager) {
const { whiteLabelling, routerBasename } = this._appConfig;
if (this._userManager) {
return (
<AppContext.Provider value={config}>
<AppContext.Provider value={{ appConfig: this._appConfig }}>
<Provider store={store}>
<I18nextProvider i18n={i18n}>
<OidcProvider store={store} userManager={userManager}>
<UserManagerContext.Provider value={userManager}>
<OidcProvider store={store} userManager={this._userManager}>
<UserManagerContext.Provider value={this._userManager}>
<Router basename={routerBasename}>
<WhiteLabellingContext.Provider value={whiteLabelling}>
<SnackbarProvider service={UINotificationService}>
@ -137,7 +180,21 @@ class App extends Component {
modal={OHIFModal}
service={UIModalService}
>
<OHIFStandaloneViewer userManager={userManager} />
<LabellingFlowProvider
service={UILabellingFlowService}
labellingComponent={LabellingManager}
commandsManager={commandsManager}
>
<ContextMenuProvider
service={UIContextMenuService}
contextMenuComponent={ToolContextMenu}
commandsManager={commandsManager}
>
<OHIFStandaloneViewer
userManager={this._userManager}
/>
</ContextMenuProvider>
</LabellingFlowProvider>
</ModalProvider>
</DialogProvider>
</SnackbarProvider>
@ -152,7 +209,7 @@ class App extends Component {
}
return (
<AppContext.Provider value={config}>
<AppContext.Provider value={{ appConfig: this._appConfig }}>
<Provider store={store}>
<I18nextProvider i18n={i18n}>
<Router basename={routerBasename}>
@ -160,7 +217,19 @@ class App extends Component {
<SnackbarProvider service={UINotificationService}>
<DialogProvider service={UIDialogService}>
<ModalProvider modal={OHIFModal} service={UIModalService}>
<OHIFStandaloneViewer />
<LabellingFlowProvider
service={UILabellingFlowService}
labellingComponent={LabellingManager}
commandsManager={commandsManager}
>
<ContextMenuProvider
service={UIContextMenuService}
contextMenuComponent={ToolContextMenu}
commandsManager={commandsManager}
>
<OHIFStandaloneViewer />
</ContextMenuProvider>
</LabellingFlowProvider>
</ModalProvider>
</DialogProvider>
</SnackbarProvider>
@ -174,10 +243,10 @@ class App extends Component {
initUserManager(oidc) {
if (oidc && !!oidc.length) {
const firstOpenIdClient = this.props.oidc[0];
const firstOpenIdClient = this._appConfig.oidc[0];
const { protocol, host } = window.location;
const { routerBasename } = this.props;
const { routerBasename } = this._appConfig;
const baseUri = `${protocol}//${host}${routerBasename}`;
const redirect_uri = firstOpenIdClient.redirect_uri || '/callback';
@ -213,22 +282,22 @@ function _initServices(services) {
/**
* @param
*/
function _initExtensions(extensions, hotkeys) {
const defaultExtensions = [
function _initExtensions(extensions, cornerstoneExtensionConfig) {
const requiredExtensions = [
GenericViewerCommands,
OHIFCornerstoneExtension,
// WARNING: MUST BE REGISTERED _AFTER_ OHIFCORNERSTONEEXTENSION
[OHIFCornerstoneExtension, cornerstoneExtensionConfig],
/* WARNING: MUST BE REGISTERED _AFTER_ OHIFCornerstoneExtension */
MeasurementsPanel,
];
const mergedExtensions = defaultExtensions.concat(extensions);
const mergedExtensions = requiredExtensions.concat(extensions);
extensionManager.registerExtensions(mergedExtensions);
}
function _initHotkeys(hotkeys) {
const { hotkeyDefinitions = {} } = store.getState().preferences || {};
let updateStore = false;
let hotkeysToUse = hotkeyDefinitions;
// Must run after extension commands are registered
// if there is no hotkeys from localStorate set up from config
if (!Object.keys(hotkeyDefinitions).length) {
hotkeysToUse = hotkeys;
updateStore = true;
@ -236,7 +305,8 @@ function _initExtensions(extensions, hotkeys) {
if (hotkeysToUse) {
hotkeysManager.setHotkeys(hotkeysToUse);
// set default based on app config
/* Set hotkeys default based on app config. */
hotkeysManager.setDefaultHotKeys(hotkeys);
if (updateStore) {
@ -264,7 +334,9 @@ function _makeAbsoluteIfNecessary(url, base_url) {
return url;
}
// Make sure base_url and url are not duplicating slashes
/*
* Make sure base_url and url are not duplicating slashes.
*/
if (base_url[base_url.length - 1] === '/') {
base_url = base_url.slice(0, base_url.length - 1);
}
@ -272,7 +344,9 @@ function _makeAbsoluteIfNecessary(url, base_url) {
return base_url + url;
}
// Only wrap/use hot if in dev
/*
* Only wrap/use hot if in dev.
*/
const ExportedApp = process.env.NODE_ENV === 'development' ? hot(App) : App;
export default ExportedApp;

View File

@ -0,0 +1,51 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ContextMenuProvider } from '@ohif/ui';
const CustomContextMenuProvider = ({
children,
service,
contextMenuComponent,
commandsManager,
}) => {
const onDeleteHandler = (nearbyToolData, eventData) => {
const element = eventData.element;
commandsManager.runCommand('removeToolState', {
element,
toolType: nearbyToolData.toolType,
tool: nearbyToolData.tool,
});
};
return (
<ContextMenuProvider
service={service}
contextMenuComponent={contextMenuComponent}
onDelete={onDeleteHandler}
>
{children}
</ContextMenuProvider>
);
};
CustomContextMenuProvider.defaultProps = {
service: null,
};
CustomContextMenuProvider.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]).isRequired,
service: PropTypes.shape({
setServiceImplementation: PropTypes.func,
}),
contextMenuComponent: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
PropTypes.func,
]).isRequired,
commandsManager: PropTypes.object.isRequired,
};
export default CustomContextMenuProvider;

View File

@ -0,0 +1,49 @@
import React from 'react';
import PropTypes from 'prop-types';
import { LabellingFlowProvider } from '@ohif/ui';
const CustomLabellingFlowProvider = ({
children,
service,
labellingComponent,
commandsManager,
}) => {
const onUpdateLabellingHandler = (labellingData, measurementData) => {
commandsManager.runCommand(
'updateTableWithNewMeasurementData',
measurementData
);
};
return (
<LabellingFlowProvider
service={service}
labellingComponent={labellingComponent}
onUpdateLabelling={onUpdateLabellingHandler}
>
{children}
</LabellingFlowProvider>
);
};
CustomLabellingFlowProvider.defaultProps = {
service: null,
};
CustomLabellingFlowProvider.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]).isRequired,
service: PropTypes.shape({
setServiceImplementation: PropTypes.func,
}),
labellingComponent: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
PropTypes.func,
]).isRequired,
commandsManager: PropTypes.object.isRequired,
};
export default CustomLabellingFlowProvider;

View File

@ -0,0 +1,6 @@
export {
default as LabellingFlowProvider,
} from './LabellingFlowProvider/LabellingFlowProvider.js';
export {
default as ContextMenuProvider,
} from './ContextMenuProvider/ContextMenuProvider.js';

View File

@ -4,9 +4,7 @@ import OHIF from '@ohif/core';
import moment from 'moment';
import cornerstone from 'cornerstone-core';
//
import jumpToRowItem from './jumpToRowItem.js';
import getMeasurementLocationCallback from './getMeasurementLocationCallback';
const { setViewportSpecificData } = OHIF.redux.actions;
const { MeasurementApi } = OHIF.measurements;
@ -155,9 +153,11 @@ const mapStateToProps = state => {
};
};
const mapDispatchToProps = dispatch => {
const mapDispatchToProps = (dispatch, ownProps) => {
return {
dispatchRelabel: (event, measurementData, viewportsState) => {
event.persist();
const activeViewportIndex =
(viewportsState && viewportsState.activeViewportIndex) || 0;
@ -167,31 +167,21 @@ const mapDispatchToProps = dispatch => {
return;
}
const { element } = enabledElements[activeViewportIndex];
const eventData = {
event: {
clientX: event.clientX,
clientY: event.clientY,
},
element,
};
const { toolType, measurementId } = measurementData;
const tool = MeasurementApi.Instance.tools[toolType].find(measurement => {
return measurement._id === measurementId;
});
const options = {
skipAddLabelButton: true,
editLocation: true,
};
// Clone the tool not to set empty location initially
const toolForLocation = Object.assign({}, tool, { location: null });
getMeasurementLocationCallback(eventData, toolForLocation, options);
if (ownProps.onRelabel) {
ownProps.onRelabel(toolForLocation);
}
},
dispatchEditDescription: (event, measurementData, viewportsState) => {
event.persist();
const activeViewportIndex =
(viewportsState && viewportsState.activeViewportIndex) || 0;
@ -201,26 +191,14 @@ const mapDispatchToProps = dispatch => {
return;
}
const { element } = enabledElements[activeViewportIndex];
const eventData = {
event: {
clientX: event.clientX,
clientY: event.clientY,
},
element,
};
const { toolType, measurementId } = measurementData;
const tool = MeasurementApi.Instance.tools[toolType].find(measurement => {
return measurement._id === measurementId;
});
const options = {
editDescriptionOnDialog: true,
};
getMeasurementLocationCallback(eventData, tool, options);
if (ownProps.onEditDescription) {
ownProps.onEditDescription(tool);
}
},
dispatchJumpToRowItem: (
measurementData,

View File

@ -1,21 +0,0 @@
const setLabellingFlowDataAction = labellingFlowData => ({
type: 'SET_LABELLING_FLOW_DATA',
labellingFlowData,
});
const resetLabellingAndContextMenuAction = state => ({
type: 'RESET_LABELLING_AND_CONTEXT_MENU',
state,
});
const setToolContextMenuDataAction = (viewportIndex, toolContextMenuData) => ({
type: 'SET_TOOL_CONTEXT_MENU_DATA',
viewportIndex,
toolContextMenuData,
});
export {
resetLabellingAndContextMenuAction,
setLabellingFlowDataAction,
setToolContextMenuDataAction,
};

View File

@ -1,33 +0,0 @@
import cornerstoneTools from 'cornerstone-tools';
import updateTableWithNewMeasurementData from './updateTableWithNewMeasurementData.js';
export default function getMeasurementLocationCallback(
eventData,
tool,
options
) {
const { toolType } = tool;
const { element } = eventData;
const doneCallback = updateTableWithNewMeasurementData;
const ToolInstance = cornerstoneTools.getToolForElement(element, toolType);
if (
!ToolInstance ||
!ToolInstance.configuration ||
!ToolInstance.configuration.getMeasurementLocationCallback
) {
console.warn(
'Tool instance configuration is missing: getMeasurementLocationCallback'
);
return;
}
ToolInstance.configuration.getMeasurementLocationCallback(
tool,
eventData,
doneCallback,
options
);
}

View File

@ -1,3 +1,4 @@
import React from 'react';
import ConnectedMeasurementTable from './ConnectedMeasurementTable.js';
import init from './init.js';
@ -7,10 +8,38 @@ export default {
*/
id: 'measurements-table',
preRegistration({ servicesManager, configuration = {} }) {
init({ servicesManager, configuration });
preRegistration({ servicesManager, commandsManager, configuration = {} }) {
init({ servicesManager, commandsManager, configuration });
},
getPanelModule({ servicesManager }) {
getPanelModule({ servicesManager, commandsManager }) {
const { UILabellingFlowService } = servicesManager.services;
const ExtendedConnectedMeasurementTable = () => (
<ConnectedMeasurementTable
onRelabel={tool => {
if (UILabellingFlowService) {
UILabellingFlowService.show({
centralize: true,
props: {
skipAddLabelButton: true,
editLocation: true,
measurementData: tool,
},
});
}
}}
onEditDescription={tool => {
if (UILabellingFlowService) {
UILabellingFlowService.show({
centralize: true,
props: {
editDescriptionOnDialog: true,
measurementData: tool,
},
});
}
}}
/>
);
return {
menuOptions: [
{
@ -22,7 +51,7 @@ export default {
components: [
{
id: 'measurement-panel',
component: ConnectedMeasurementTable,
component: ExtendedConnectedMeasurementTable,
},
],
defaultContext: ['VIEWER'],

View File

@ -1,19 +1,7 @@
import OHIF from '@ohif/core';
import cornerstone from 'cornerstone-core';
import csTools from 'cornerstone-tools';
import {
getToolLabellingFlowCallback,
getOnRightClickCallback,
getOnTouchPressCallback,
getResetLabellingAndContextMenu,
} from './labelingFlowCallbacks.js';
import throttle from 'lodash.throttle';
import { SimpleDialog } from '@ohif/ui';
// TODO: This only works because we have a hard dependency on this extension
// We need to decouple and make stuff like this possible w/o bundling this at
// build time
import store from './../../store';
const {
onAdded,
@ -33,92 +21,18 @@ const MEASUREMENT_ACTION_MAP = {
*
*
* @export
* @param {*} configuration
* @param {Object} servicesManager
* @param {Object} configuration
*/
export default function init({ servicesManager, configuration = {} }) {
const { UIDialogService } = servicesManager.services;
const callInputDialog = (data, event, callback) => {
let dialogId = UIDialogService.create({
content: SimpleDialog.InputDialog,
defaultPosition: {
x: (event && event.currentPoints.canvas.x) || 0,
y: (event && event.currentPoints.canvas.y) || 0,
},
showOverlay: true,
contentProps: {
title: 'Enter your annotation',
label: 'New label',
defaultValue: data ? data.text : '',
onClose: () => UIDialogService.dismiss({ id: dialogId }),
onSubmit: value => {
callback(value);
UIDialogService.dismiss({ id: dialogId });
},
},
});
};
// If these tools were already added by a different extension, we want to replace
// them with the same tools that have an alternative configuration. By passing in
// our custom `getMeasurementLocationCallback`, we can...
const toolLabellingFlowCallback = getToolLabellingFlowCallback(store);
// Removes all tools from all enabled elements w/ provided name
// Not commonly used API, so :eyes: for unknown side-effects
csTools.removeTool('Bidirectional');
csTools.removeTool('Length');
csTools.removeTool('Angle');
csTools.removeTool('FreehandRoi');
csTools.removeTool('EllipticalRoi');
csTools.removeTool('CircleRoi');
csTools.removeTool('RectangleRoi');
csTools.removeTool('ArrowAnnotate');
// Re-add each tool w/ our custom configuration
csTools.addTool(csTools.BidirectionalTool, {
configuration: {
getMeasurementLocationCallback: toolLabellingFlowCallback,
},
});
csTools.addTool(csTools.LengthTool, {
configuration: {
getMeasurementLocationCallback: toolLabellingFlowCallback,
},
});
csTools.addTool(csTools.AngleTool, {
configuration: {
getMeasurementLocationCallback: toolLabellingFlowCallback,
},
});
csTools.addTool(csTools.FreehandRoiTool, {
configuration: {
getMeasurementLocationCallback: toolLabellingFlowCallback,
},
});
csTools.addTool(csTools.EllipticalRoiTool, {
configuration: {
getMeasurementLocationCallback: toolLabellingFlowCallback,
},
});
csTools.addTool(csTools.CircleRoiTool, {
configuration: {
getMeasurementLocationCallback: toolLabellingFlowCallback,
},
});
csTools.addTool(csTools.RectangleRoiTool, {
configuration: {
getMeasurementLocationCallback: toolLabellingFlowCallback,
},
});
csTools.addTool(csTools.ArrowAnnotateTool, {
configuration: {
getMeasurementLocationCallback: toolLabellingFlowCallback,
getTextCallback: (callback, eventDetails) =>
callInputDialog(null, eventDetails, callback),
changeTextCallback: (data, eventDetails, callback) =>
callInputDialog(data, eventDetails, callback),
},
});
export default function init({
servicesManager,
commandsManager,
configuration,
}) {
const {
UIContextMenuService,
UILabellingFlowService,
} = servicesManager.services;
// TODO: MEASUREMENT_COMPLETED (not present in initial implementation)
const onMeasurementsChanged = (action, event) => {
@ -131,15 +45,43 @@ export default function init({ servicesManager, configuration = {} }) {
this,
'labelmapModified'
);
//
const onRightClick = getOnRightClickCallback(store);
const onTouchPress = getOnTouchPressCallback(store);
const onNewImage = getResetLabellingAndContextMenu(store);
const onMouseClick = getResetLabellingAndContextMenu(store);
const onTouchStart = getResetLabellingAndContextMenu(store);
// Because click gives us the native "mouse up", buttons will always be `0`
// Need to fallback to event.which;
const onRightClick = event => {
if (UIContextMenuService) {
UIContextMenuService.show({ event: event.detail });
}
};
const onTouchPress = event => {
if (UIContextMenuService) {
UIContextMenuService.show({
event: event.detail,
props: {
isTouchEvent: true,
},
});
}
};
const onTouchStart = () => resetLabelligAndContextMenu();
const onMouseClick = () => resetLabelligAndContextMenu();
const resetLabelligAndContextMenu = () => {
if (UILabellingFlowService && UIContextMenuService) {
UILabellingFlowService.hide();
UIContextMenuService.hide();
}
};
// TODO: This makes scrolling painfully slow
// const onNewImage = ...
/*
* Because click gives us the native "mouse up", buttons will always be `0`
* Need to fallback to event.which;
*
*/
const handleClick = cornerstoneMouseClickEvent => {
const mouseUpEvent = cornerstoneMouseClickEvent.detail.event;
const isRightClick = mouseUpEvent.which === 3;
@ -170,10 +112,11 @@ export default function init({ servicesManager, configuration = {} }) {
csTools.EVENTS.LABELMAP_MODIFIED,
onLabelmapModified
);
//
element.addEventListener(csTools.EVENTS.TOUCH_PRESS, onTouchPress);
element.addEventListener(csTools.EVENTS.MOUSE_CLICK, handleClick);
element.addEventListener(csTools.EVENTS.TOUCH_START, onTouchStart);
// TODO: This makes scrolling painfully slow
// element.addEventListener(cornerstone.EVENTS.NEW_IMAGE, onNewImage);
}
@ -197,10 +140,12 @@ export default function init({ servicesManager, configuration = {} }) {
csTools.EVENTS.LABELMAP_MODIFIED,
onLabelmapModified
);
//
element.removeEventListener(csTools.EVENTS.TOUCH_PRESS, onTouchPress);
element.removeEventListener(csTools.EVENTS.MOUSE_CLICK, handleClick);
element.removeEventListener(csTools.EVENTS.TOUCH_START, onTouchStart);
// TODO: This makes scrolling painfully slow
// element.removeEventListener(cornerstone.EVENTS.NEW_IMAGE, onNewImage);
}

View File

@ -1,144 +0,0 @@
import {
resetLabellingAndContextMenuAction,
setToolContextMenuDataAction,
setLabellingFlowDataAction,
} from './actions.js';
import updateTableWithNewMeasurementData from './updateTableWithNewMeasurementData.js';
const VIEWPORT_INDEX = 0;
function getOnRightClickCallback(store) {
const setToolContextMenuData = (viewportIndex, toolContextMenuData) => {
store.dispatch(resetLabellingAndContextMenuAction());
store.dispatch(
setToolContextMenuDataAction(viewportIndex, toolContextMenuData)
);
};
const getOnCloseCallback = viewportIndex => {
return function onClose() {
const toolContextMenuData = {
visible: false,
};
store.dispatch(
setToolContextMenuDataAction(viewportIndex, toolContextMenuData)
);
};
};
return function onRightClick(event) {
const eventData = event.detail;
const viewportIndex = VIEWPORT_INDEX; // parseInt(eventData.element.dataset.viewportIndex, 10);
const toolContextMenuData = {
eventData,
isTouchEvent: false,
onClose: getOnCloseCallback(viewportIndex),
};
// setToolContextMenuData(viewportIndex, toolContextMenuData);
setToolContextMenuData(0, toolContextMenuData);
};
}
function getOnTouchPressCallback(store) {
const setToolContextMenuData = (viewportIndex, toolContextMenuData) => {
store.dispatch(resetLabellingAndContextMenuAction());
store.dispatch(
setToolContextMenuDataAction(viewportIndex, toolContextMenuData)
);
};
const getOnCloseCallback = viewportIndex => {
return function onClose() {
const toolContextMenuData = {
visible: false,
};
store.dispatch(
setToolContextMenuDataAction(viewportIndex, toolContextMenuData)
);
};
};
return function onTouchPress(event) {
const eventData = event.detail;
const viewportIndex = parseInt(eventData.element.dataset.viewportIndex, 10);
const toolContextMenuData = {
eventData,
isTouchEvent: true,
onClose: getOnCloseCallback(viewportIndex),
};
setToolContextMenuData(viewportIndex, toolContextMenuData);
};
}
function getResetLabellingAndContextMenu(store) {
return function resetLabellingAndContextMenu() {
store.dispatch(resetLabellingAndContextMenuAction());
};
}
/**
*
*
* @param {*} store
* @returns
*/
function getToolLabellingFlowCallback(store) {
const setLabellingFlowData = labellingFlowData => {
store.dispatch(setLabellingFlowDataAction(labellingFlowData));
};
return function toolLabellingFlowCallback(
measurementData,
eventData,
doneCallback,
options = {}
) {
const updateLabelling = ({ location, response, description }) => {
// Update the measurement data with the labelling parameters
if (location) {
measurementData.location = location;
}
measurementData.description = description || '';
if (response) {
measurementData.response = response;
}
updateTableWithNewMeasurementData(measurementData);
};
const labellingDoneCallback = () => {
setLabellingFlowData({ visible: false });
};
const labellingFlowData = {
visible: true,
eventData,
measurementData,
skipAddLabelButton: options.skipAddLabelButton,
editLocation: options.editLocation,
editDescription: options.editDescription,
editResponse: options.editResponse,
editDescriptionOnDialog: options.editDescriptionOnDialog,
labellingDoneCallback,
updateLabelling,
};
setLabellingFlowData(labellingFlowData);
};
}
export {
getToolLabellingFlowCallback,
getOnRightClickCallback,
getOnTouchPressCallback,
getResetLabellingAndContextMenu,
};

View File

@ -1,29 +0,0 @@
import OHIF from '@ohif/core';
import cornerstone from 'cornerstone-core';
export default function updateTableWithNewMeasurementData({
toolType,
measurementNumber,
location,
description,
}) {
// Update all measurements by measurement number
const measurementApi = OHIF.measurements.MeasurementApi.Instance;
const measurements = measurementApi.tools[toolType].filter(
m => m.measurementNumber === measurementNumber
);
measurements.forEach(measurement => {
measurement.location = location;
measurement.description = description;
measurementApi.updateMeasurement(measurement.toolType, measurement);
});
measurementApi.syncMeasurementsAndToolData();
// Update images in all active viewports
cornerstone.getEnabledElements().forEach(enabledElement => {
cornerstone.updateImage(enabledElement.element);
});
}

View File

@ -1,5 +1,5 @@
.editDescriptionDialog {
position: absolute;
position: relative;
z-index: 300;
width: 320px;
transition: all 300ms linear;

View File

@ -1,24 +1,15 @@
import { Component } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import SimpleDialog from '../SimpleDialog/SimpleDialog.js';
import bounding from '../../lib/utils/bounding.js';
import { getDialogStyle } from './../Labelling/labellingPositionUtils.js';
import './EditDescriptionDialog.css';
export default class EditDescriptionDialog extends Component {
static defaultProps = {
componentRef: React.createRef(),
componentStyle: {},
};
static propTypes = {
description: PropTypes.string,
measurementData: PropTypes.object.isRequired,
onCancel: PropTypes.func.isRequired,
componentRef: PropTypes.object,
componentStyle: PropTypes.object,
onUpdate: PropTypes.func.isRequired,
};
@ -28,14 +19,8 @@ export default class EditDescriptionDialog extends Component {
this.state = {
description: props.measurementData.description || '',
};
this.mainElement = React.createRef();
}
componentDidMount = () => {
bounding(this.mainElement);
};
componentDidUpdate(prevProps) {
if (this.props.description !== prevProps.description) {
this.setState({
@ -45,16 +30,12 @@ export default class EditDescriptionDialog extends Component {
}
render() {
const style = getDialogStyle(this.props.componentStyle);
return (
<SimpleDialog
headerTitle="Edit Description"
onClose={this.onClose}
onConfirm={this.onConfirm}
rootClass="editDescriptionDialog"
componentRef={this.mainElement}
componentStyle={style}
>
<input
value={this.state.description}

View File

@ -1,20 +1,16 @@
import { Icon, SelectTree } from '@ohif/ui';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import cloneDeep from 'lodash.clonedeep';
import LabellingTransition from './LabellingTransition.js';
import OHIFLabellingData from './OHIFLabellingData.js';
import PropTypes from 'prop-types';
import bounding from '../../lib/utils/bounding.js';
import cloneDeep from 'lodash.clonedeep';
import { getAddLabelButtonStyle } from './labellingPositionUtils.js';
export default class LabellingFlow extends Component {
static propTypes = {
eventData: PropTypes.object.isRequired,
measurementData: PropTypes.object.isRequired,
labellingDoneCallback: PropTypes.func.isRequired,
updateLabelling: PropTypes.func.isRequired,
initialTopDistance: PropTypes.number,
skipAddLabelButton: PropTypes.bool,
editLocation: PropTypes.bool,
@ -26,11 +22,6 @@ export default class LabellingFlow extends Component {
const { location, locationLabel, description } = props.measurementData;
let style = props.componentStyle;
if (!props.skipAddLabelButton) {
style = getAddLabelButtonStyle(props.measurementData, props.eventData);
}
this.state = {
location,
locationLabel,
@ -38,20 +29,17 @@ export default class LabellingFlow extends Component {
skipAddLabelButton: props.skipAddLabelButton,
editDescription: props.editDescription,
editLocation: props.editLocation,
componentStyle: style,
confirmationState: false,
displayComponent: true,
};
this.mainElement = React.createRef();
this.descriptionInput = React.createRef();
this.initialItems = OHIFLabellingData;
this.currentItems = cloneDeep(this.initialItems);
}
componentDidUpdate = () => {
this.repositionComponent();
if (this.state.editDescription) {
this.descriptionInput.current.focus();
}
@ -63,36 +51,14 @@ export default class LabellingFlow extends Component {
mainElementClassName += ' editDescription';
}
const style = Object.assign({}, this.state.componentStyle);
if (this.state.skipAddLabelButton) {
if (style.left - 160 < 0) {
style.left = 0;
} else {
style.left -= 160;
}
}
if (this.state.editLocation) {
style.maxHeight = '70vh';
if (!this.initialTopDistance) {
this.initialTopDistance = window.innerHeight - window.innerHeight * 0.3;
style.top = `${this.state.componentStyle.top -
this.initialTopDistance / 2}px`;
} else {
style.top = `${this.state.componentStyle.top}px`;
}
}
return (
<LabellingTransition
displayComponent={this.state.displayComponent}
onTransitionExit={this.props.labellingDoneCallback}
>
<>
<div className="labellingComponent-overlay"></div>
<div
className={mainElementClassName}
style={style}
ref={this.mainElement}
onMouseLeave={this.fadeOutAndLeave}
onMouseEnter={this.clearFadeOutTimer}
@ -130,9 +96,8 @@ export default class LabellingFlow extends Component {
<SelectTree
items={this.currentItems}
columns={1}
onSelected={this.selectTreeSelectCalback}
onSelected={this.selectTreeSelectCallback}
selectTreeFirstTitle="Assign Label"
onComponentChange={this.repositionComponent}
/>
);
} else {
@ -195,33 +160,17 @@ export default class LabellingFlow extends Component {
}
};
relabel = event => {
const viewportTopPosition = this.mainElement.current.offsetParent.offsetTop;
const componentStyle = {
top: event.nativeEvent.y - viewportTopPosition - 55,
left: event.nativeEvent.x,
};
this.setState({
editLocation: true,
componentStyle,
});
};
relabel = event => this.setState({ editLocation: true });
setDescriptionUpdateMode = () => {
this.descriptionInput.current.focus();
this.setState({
editDescription: true,
});
this.setState({ editDescription: true });
};
descriptionCancel = () => {
const { description = '' } = cloneDeep(this.state);
this.descriptionInput.current.value = description;
this.setState({
editDescription: false,
});
this.setState({ editDescription: false });
};
handleKeyPress = e => {
@ -240,22 +189,15 @@ export default class LabellingFlow extends Component {
});
};
selectTreeSelectCalback = (event, itemSelected) => {
selectTreeSelectCallback = (event, itemSelected) => {
const location = itemSelected.value;
this.props.updateLabelling({ location });
const viewportTopPosition = this.mainElement.current.offsetParent.offsetTop;
const componentStyle = {
top: event.nativeEvent.y - viewportTopPosition - 25,
left: event.nativeEvent.x,
};
this.setState({
editLocation: false,
confirmationState: true,
location: itemSelected.value,
locationLabel: itemSelected.label,
componentStyle,
});
if (this.isTouchScreen) {
@ -276,18 +218,13 @@ export default class LabellingFlow extends Component {
fadeOutAndLeave = () => {
// Wait for 1 sec to dismiss the labelling component
this.fadeOutTimer = setTimeout(() => {
this.setState({
displayComponent: false,
});
}, 1000);
this.fadeOutTimer = setTimeout(
() => this.setState({ displayComponent: false }),
1000
);
};
fadeOutAndLeaveFast = () => {
this.setState({
displayComponent: false,
});
};
fadeOutAndLeaveFast = () => this.setState({ displayComponent: false });
clearFadeOutTimer = () => {
if (!this.fadeOutTimer) {
@ -296,29 +233,4 @@ export default class LabellingFlow extends Component {
clearTimeout(this.fadeOutTimer);
};
calculateTopDistance = () => {
const height = window.innerHeight - window.innerHeight * 0.3;
let top = this.state.componentStyle.top - height / 2 + 55;
if (top < 0) {
top = 0;
} else {
if (top + height > window.innerHeight) {
top -= top + height - window.innerHeight;
}
}
return top;
};
repositionComponent = () => {
// SetTimeout for the css animation to end.
setTimeout(() => {
bounding(this.mainElement);
if (this.state.editLocation) {
this.mainElement.current.style.maxHeight = '70vh';
const top = this.calculateTopDistance();
this.mainElement.current.style.top = `${top}px`;
}
}, 200);
};
}

View File

@ -1,18 +1,9 @@
.labellingComponent-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10;
background-color: rgba(0, 0, 0, 0.8);
}
.labellingComponent {
position: absolute;
position: relative;
text-align: center;
z-index: 999;
transition: all 200ms linear;
max-height: 500px;
}
.labellingComponent .selectedLabel,
@ -87,9 +78,7 @@
border: none;
}
.labellingComponent.editDescription
.locationDescriptionWrapper
#descriptionInput {
.labellingComponent.editDescription .locationDescriptionWrapper #descriptionInput {
visibility: visible;
}

View File

@ -1,21 +1,16 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import cloneDeep from 'lodash.clonedeep';
import EditDescriptionDialog from './../EditDescriptionDialog/EditDescriptionDialog.js';
import LabellingFlow from './LabellingFlow.js';
import './LabellingManager.css';
export default class LabellingManager extends Component {
static propTypes = {
eventData: PropTypes.object.isRequired,
measurementData: PropTypes.object.isRequired,
labellingDoneCallback: PropTypes.func.isRequired,
updateLabelling: PropTypes.func.isRequired,
skipAddLabelButton: PropTypes.bool,
editLocation: PropTypes.bool,
editDescription: PropTypes.bool,
@ -41,7 +36,6 @@ export default class LabellingManager extends Component {
}
this.state = {
componentStyle: getComponentPosition(props.eventData),
skipAddLabelButton: props.skipAddLabelButton,
editLocation: editLocation,
editDescription: props.editDescription,
@ -75,20 +69,13 @@ export default class LabellingManager extends Component {
<EditDescriptionDialog
onCancel={this.props.labellingDoneCallback}
onUpdate={this.descriptionDialogUpdate}
componentRef={this.editDescriptionDialog}
componentStyle={this.state.componentStyle}
measurementData={measurementData}
/>
);
}
if (editLocation || editDescription) {
return (
<LabellingFlow
{...this.props}
componentStyle={this.state.componentStyle}
/>
);
return <LabellingFlow {...this.props} />;
}
};
@ -98,33 +85,19 @@ export default class LabellingManager extends Component {
if (editDescription) {
measurementData.description = undefined;
}
if (editLocation) {
measurementData.location = undefined;
}
};
responseDialogUpdate = response => {
this.props.updateLabelling({
response,
});
this.props.updateLabelling({ response });
this.props.labellingDoneCallback();
};
descriptionDialogUpdate = description => {
this.props.updateLabelling({
description,
});
this.props.updateLabelling({ description });
this.props.labellingDoneCallback();
};
}
function getComponentPosition(eventData) {
const {
event: { clientX: left, clientY: top },
} = eventData;
return {
left,
top,
};
}

View File

@ -1,53 +0,0 @@
import cornerstone from 'cornerstone-core';
const buttonSize = {
width: 96,
height: 28,
};
export function getAddLabelButtonStyle(measurementData, eventData) {
const { start, end } = measurementData.handles;
const { client } = eventData.currentPoints;
const clientStart = cornerstone.pixelToCanvas(eventData.element, start);
const clientEnd = cornerstone.pixelToCanvas(eventData.element, end);
const canvasOffSetLeft = client.x - clientStart.x;
const canvasOffSetTop = client.y - clientStart.y;
const position = {
left: clientEnd.x + canvasOffSetLeft,
top: clientEnd.y + canvasOffSetTop,
};
if (start.y > end.y) {
position.top -= buttonSize.height;
}
if (start.x > end.x) {
position.left -= buttonSize.width;
}
return position;
}
export function getDialogStyle(componentStyle) {
const style = Object.assign({}, componentStyle);
const dialogProps = {
width: 320,
height: 230,
};
// Get max values to avoid position out of the screen
const maxLeft = window.innerWidth - dialogProps.width;
const maxTop = window.innerHeight - dialogProps.height;
// Positioning the dialog with its center on the click event
style.left -= dialogProps.width / 2;
style.top -= dialogProps.height / 2;
if (style.left > maxLeft) {
style.left = maxLeft;
}
if (style.top > maxTop) {
style.top = maxTop;
}
return style;
}

View File

@ -1,24 +0,0 @@
import { connect } from 'react-redux';
import LabellingOverlay from './LabellingOverlay';
const mapStateToProps = state => {
if (!state.ui || !state.ui.labelling) {
return {
visible: false,
};
}
const labellingFlowData = state.ui.labelling;
return {
visible: false,
...labellingFlowData,
};
};
const ConnectedLabellingOverlay = connect(
mapStateToProps,
null
)(LabellingOverlay);
export default ConnectedLabellingOverlay;

View File

@ -1,24 +0,0 @@
import { connect } from 'react-redux';
import ToolContextMenu from './ToolContextMenu';
const mapStateToProps = (state, ownProps) => {
if (!state.ui || !state.ui.contextMenu) {
return {
visible: false,
};
}
const { viewportIndex } = ownProps;
const toolContextMenuData = state.ui.contextMenu[viewportIndex];
return {
...toolContextMenuData,
};
};
const ConnectedToolContextMenu = connect(
mapStateToProps,
null
)(ToolContextMenu);
export default ConnectedToolContextMenu;

View File

@ -35,7 +35,10 @@ const mapDispatchToProps = (dispatch, ownProps) => {
// set new language
i18n.changeLanguage(language);
ownProps.hide();
if (ownProps.hide) {
ownProps.hide();
}
dispatch(
setUserPreferences({
windowLevelData,

View File

@ -1,23 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import LabellingManager from '../components/Labelling/LabellingManager';
class LabellingOverlay extends Component {
static propTypes = {
visible: PropTypes.bool.isRequired,
};
static defaultProps = {
visible: false,
};
render() {
if (!this.props.visible) {
return null;
}
return <LabellingManager {...this.props} />;
}
}
export default LabellingOverlay;

View File

@ -1,5 +1,5 @@
.ToolContextMenu {
position: absolute;
position: relative;
background-color: white;
border: 1px solid white;
border-radius: 5px;

View File

@ -1,9 +1,6 @@
import React, { Component } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import cornerstone from 'cornerstone-core';
import cornerstoneTools from 'cornerstone-tools';
// This whole component should live in the Measurements Extension :thinking:
import getMeasurementLocationCallback from '../appExtensions/MeasurementsPanel/getMeasurementLocationCallback';
import { commandsManager } from './../App.js';
import './ToolContextMenu.css';
@ -17,232 +14,119 @@ const toolTypes = [
'RectangleRoi',
];
let defaultDropdownItems = [
{
actionType: 'Delete',
action: ({ nearbyToolData, eventData }) => {
const element = eventData.element;
cornerstoneTools.removeToolState(
element,
nearbyToolData.toolType,
nearbyToolData.tool
);
cornerstone.updateImage(element);
const ToolContextMenu = ({
onSetLabel,
onSetDescription,
isTouchEvent,
eventData,
onClose,
onDelete,
}) => {
const defaultDropdownItems = [
{
actionType: 'Delete',
action: ({ nearbyToolData, eventData }) =>
onDelete(nearbyToolData, eventData),
},
},
{
actionType: 'setLabel',
action: ({ nearbyToolData, eventData }) => {
const { tool } = nearbyToolData;
const options = {
skipAddLabelButton: true,
editLocation: true,
};
getMeasurementLocationCallback(eventData, tool, options);
{
actionType: 'setLabel',
action: ({ nearbyToolData, eventData }) => {
const { tool: measurementData } = nearbyToolData;
onSetLabel(eventData, measurementData);
},
},
},
{
actionType: 'setDescription',
action: ({ nearbyToolData, eventData }) => {
const { tool } = nearbyToolData;
const options = {
editDescriptionOnDialog: true,
};
getMeasurementLocationCallback(eventData, tool, options);
{
actionType: 'setDescription',
action: ({ nearbyToolData, eventData }) => {
const { tool: measurementData } = nearbyToolData;
onSetDescription(eventData, measurementData);
},
},
},
];
];
function getNearbyToolData(element, coords, toolTypes) {
const nearbyTool = {};
let pointNearTool = false;
const getDropdownItems = (eventData, isTouchEvent = false) => {
const nearbyToolData = commandsManager.runCommand('getNearbyToolData', {
element: eventData.element,
canvasCoordinates: eventData.currentPoints.canvas,
availableToolTypes: toolTypes,
});
toolTypes.forEach(toolType => {
const toolData = cornerstoneTools.getToolState(element, toolType);
if (!toolData) {
// Annotate tools for touch events already have a press handle to edit it, has a better UX for deleting it
if (
isTouchEvent &&
nearbyToolData &&
nearbyToolData.toolType === 'arrowAnnotate'
) {
return;
}
toolData.data.forEach(function(data, index) {
// TODO: Fix this, it's ugly
let toolInterface = cornerstoneTools.getToolForElement(element, toolType);
if (!toolInterface) {
toolInterface = cornerstoneTools.getToolForElement(
element,
`${toolType}Tool`
);
}
let dropdownItems = [];
if (nearbyToolData) {
defaultDropdownItems.forEach(item => {
item.params = {
eventData,
nearbyToolData,
};
if (!toolInterface) {
throw new Error('Tool not found.');
}
if (toolInterface.pointNearTool(element, data, coords)) {
pointNearTool = true;
nearbyTool.tool = data;
nearbyTool.index = index;
nearbyTool.toolType = toolType;
}
});
if (pointNearTool) {
return false;
}
});
return pointNearTool ? nearbyTool : undefined;
}
function getDropdownItems(eventData, isTouchEvent = false) {
const nearbyToolData = getNearbyToolData(
eventData.element,
eventData.currentPoints.canvas,
toolTypes
);
// Annotate tools for touch events already have a press handle to edit it, has a better UX for deleting it
if (
isTouchEvent &&
nearbyToolData &&
nearbyToolData.toolType === 'arrowAnnotate'
) {
return;
}
let dropdownItems = [];
if (nearbyToolData) {
defaultDropdownItems.forEach(function(item) {
item.params = {
eventData,
nearbyToolData,
};
if (item.actionType === 'Delete') {
item.text = 'Delete measurement';
}
if (item.actionType === 'setLabel') {
item.text = 'Relabel';
}
if (item.actionType === 'setDescription') {
item.text = `${
nearbyToolData.tool.description ? 'Edit' : 'Add'
} Description`;
}
dropdownItems.push(item);
});
}
return dropdownItems;
}
class ToolContextMenu extends Component {
static propTypes = {
isTouchEvent: PropTypes.bool.isRequired,
eventData: PropTypes.object,
onClose: PropTypes.func,
visible: PropTypes.bool.isRequired,
};
static defaultProps = {
visible: true,
isTouchEvent: false,
};
constructor(props) {
super(props);
this.mainElement = React.createRef();
}
render() {
if (!this.props.eventData) {
return null;
}
const { isTouchEvent, eventData } = this.props;
const dropdownItems = getDropdownItems(eventData, isTouchEvent);
// Skip if there is no dropdown item
if (!dropdownItems.length) {
return '';
}
const dropdownComponents = dropdownItems.map(item => {
const itemOnClick = event => {
item.action(item.params);
if (this.props.onClose) {
this.props.onClose();
if (item.actionType === 'Delete') {
item.text = 'Delete measurement';
}
};
return (
<li key={item.actionType}>
<button className="form-action" onClick={itemOnClick}>
<span key={item.actionType}>{item.text}</span>
</button>
</li>
);
});
if (item.actionType === 'setLabel') {
item.text = 'Relabel';
}
const position = {
top: `${eventData.currentPoints.canvas.y}px`,
left: `${eventData.currentPoints.canvas.x}px`,
};
if (item.actionType === 'setDescription') {
item.text = `${
nearbyToolData.tool.description ? 'Edit' : 'Add'
} Description`;
}
return (
<div className="ToolContextMenu" style={position} ref={this.mainElement}>
<ul className="bounded">{dropdownComponents}</ul>
dropdownItems.push(item);
});
}
return dropdownItems;
};
const itemOnClickHandler = (action, params, onClose) => {
action(params);
if (onClose) {
onClose();
}
};
const dropdownItems = getDropdownItems(eventData, isTouchEvent);
return (
dropdownItems.length &&
eventData && (
<div className="ToolContextMenu">
<ul className="bounded">
{dropdownItems.map(({ params, action, text, actionType }) => (
<li key={actionType}>
<button
className="form-action"
onClick={() => itemOnClickHandler(action, params, onClose)}
>
<span key={actionType}>{text}</span>
</button>
</li>
))}
</ul>
</div>
);
}
)
);
};
componentDidMount = () => {
if (this.mainElement.current) {
this.updateElementPosition();
}
};
ToolContextMenu.propTypes = {
isTouchEvent: PropTypes.bool.isRequired,
eventData: PropTypes.object,
onClose: PropTypes.func,
};
componentDidUpdate = () => {
if (this.mainElement.current) {
this.updateElementPosition();
}
};
updateElementPosition = () => {
const {
offsetParent,
offsetTop,
offsetHeight,
offsetWidth,
offsetLeft,
} = this.mainElement.current;
const { eventData } = this.props;
if (offsetTop + offsetHeight > offsetParent.offsetHeight) {
const offBoundPixels =
offsetTop + offsetHeight - offsetParent.offsetHeight;
const top = eventData.currentPoints.canvas.y - offBoundPixels;
this.mainElement.current.style.top = `${top > 0 ? top : 0}px`;
}
if (offsetLeft + offsetWidth > offsetParent.offsetWidth) {
const offBoundPixels =
offsetLeft + offsetWidth - offsetParent.offsetWidth;
const left = eventData.currentPoints.canvas.x - offBoundPixels;
this.mainElement.current.style.left = `${left > 0 ? left : 0}px`;
}
};
}
ToolContextMenu.defaultProps = {
isTouchEvent: false,
};
export default ToolContextMenu;

View File

@ -7,7 +7,6 @@ import OHIF from '@ohif/core';
import moment from 'moment';
import ConnectedHeader from './ConnectedHeader.js';
import ConnectedToolbarRow from './ConnectedToolbarRow.js';
import ConnectedLabellingOverlay from './ConnectedLabellingOverlay';
import ConnectedStudyBrowser from './ConnectedStudyBrowser.js';
import ConnectedViewerMain from './ConnectedViewerMain.js';
import SidePanel from './../components/SidePanel.js';
@ -317,7 +316,6 @@ class Viewer extends Component {
)}
</SidePanel>
</div>
<ConnectedLabellingOverlay />
</>
);
}

View File

@ -2,7 +2,6 @@ import './ViewerMain.css';
import { Component } from 'react';
import { ConnectedViewportGrid } from './../components/ViewportGrid/index.js';
import ConnectedToolContextMenu from './ConnectedToolContextMenu.js';
import PropTypes from 'prop-types';
import React from 'react';
@ -153,7 +152,6 @@ class ViewerMain extends Component {
setViewportData={this.setViewportData}
>
{/* Children to add to each viewport that support children */}
<ConnectedToolContextMenu />
</ConnectedViewportGrid>
)}
</div>

View File

@ -7,7 +7,7 @@ import React from 'react';
import ReactDOM from 'react-dom';
import App from './App.js';
function installViewer(props, containerId = 'root', callback) {
function installViewer(config, containerId = 'root', callback) {
const container = document.getElementById(containerId);
if (!container) {
@ -16,7 +16,7 @@ function installViewer(props, containerId = 'root', callback) {
);
}
return ReactDOM.render(<App {...props} />, container, callback);
return ReactDOM.render(<App config={config} />, container, callback);
}
export { App, installViewer };

View File

@ -18,35 +18,36 @@ import ReactDOM from 'react-dom';
* "baked in" to the published application.
*
* Depending on your use case/needs, you may want to consider not adding any extensions
* by default HERE, and instead provide them via the configuration specified at
* `window.config.extensions`, or by using the exported `App` component, and passing
* in your extensions as props.
* by default HERE, and instead provide them via the extensions configuration key or
* by using the exported `App` component, and passing in your extensions as props using
* the defaultExtensions property.
*/
import OHIFVTKExtension from '@ohif/extension-vtk';
import OHIFDicomHtmlExtension from '@ohif/extension-dicom-html';
import OHIFDicomMicroscopyExtension from '@ohif/extension-dicom-microscopy';
import OHIFDicomPDFExtension from '@ohif/extension-dicom-pdf';
// Default Settings
/*
* Default Settings
*/
let config = {};
const appDefaults = {
routerBasename: '/',
};
if (window) {
config = window.config || {};
config.extensions = [
}
const appProps = {
config,
defaultExtensions: [
OHIFVTKExtension,
OHIFDicomHtmlExtension,
OHIFDicomMicroscopyExtension,
OHIFDicomPDFExtension,
];
}
],
};
const appProps = Object.assign({}, appDefaults, config);
// Create App
/** Create App */
const app = React.createElement(App, appProps, null);
// Render
/** Render */
ReactDOM.render(app, document.getElementById('root'));

View File

@ -6,18 +6,16 @@ import {
} from 'redux/es/redux.js';
// import { createLogger } from 'redux-logger';
import layoutReducers from './layout/reducers.js';
import { reducer as oidcReducer } from 'redux-oidc';
import { redux } from '@ohif/core';
import thunkMiddleware from 'redux-thunk';
// Combine our @ohif/core, ui, and oidc reducers
// Combine our @ohif/core and oidc reducers
// Set init data, using values found in localStorage
const { reducers, localStorage, sessionStorage } = redux;
const middleware = [thunkMiddleware];
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
reducers.ui = layoutReducers;
reducers.oidc = oidcReducer;
const rootReducer = combineReducers(reducers);

View File

@ -1,33 +0,0 @@
const defaultState = {
labelling: {},
contextMenu: {},
};
const ui = (state = defaultState, action) => {
switch (action.type) {
case 'SET_LABELLING_FLOW_DATA': {
const labelling = Object.assign({}, action.labellingFlowData);
return Object.assign({}, state, { labelling });
}
case 'SET_TOOL_CONTEXT_MENU_DATA': {
const contextMenu = Object.assign({}, state.contextMenu);
contextMenu[action.viewportIndex] = Object.assign(
{},
action.toolContextMenuData
);
return Object.assign({}, state, { contextMenu });
}
case 'RESET_LABELLING_AND_CONTEXT_MENU':
return Object.assign({}, state, {
labelling: defaultState.labelling,
contextMenu: defaultState.contextMenu,
});
default:
return state;
}
};
export default ui;

View File

@ -12246,7 +12246,7 @@ lodash.memoize@^4.1.2:
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
lodash.merge@^4.4.0, lodash.merge@^4.6.1:
lodash.merge@^4.4.0, lodash.merge@^4.6.1, lodash.merge@^4.6.2:
version "4.6.2"
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==