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:
parent
fd7620ced9
commit
55a580659e
@ -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
|
After updating the configuration, `yarn run build` to generate updated build
|
||||||
output.
|
output.
|
||||||
|
|
||||||
|
|||||||
@ -24,12 +24,12 @@ include tags. Here's how it works:
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<ol start="2">
|
<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>
|
example values that would allow the viewer to hit our public PACS:</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// Set before importing `ohif-viewer`
|
// Set before importing `ohif-viewer` (JS Object)
|
||||||
window.config = {
|
window.config = {
|
||||||
// default: '/'
|
// default: '/'
|
||||||
routerBasename: '/',
|
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>
|
<ol start="3"><li>
|
||||||
Render the viewer in the web page's target <code>div</code>
|
Render the viewer in the web page's target <code>div</code>
|
||||||
</li></ol>
|
</li></ol>
|
||||||
|
|||||||
@ -12,8 +12,9 @@ export default {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
preRegistration({
|
preRegistration({
|
||||||
servicesManager,
|
servicesManager = {},
|
||||||
configuration: extensionConfiguration,
|
commandsManager = {},
|
||||||
|
configuration = {},
|
||||||
}) {},
|
}) {},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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
|
and is connected the redux store. This module is the most prone to change as we
|
||||||
hammer out our Viewport interface.
|
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
|
## Resources
|
||||||
|
|
||||||
### Repositories
|
### Repositories
|
||||||
|
|||||||
@ -47,6 +47,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.5.5",
|
"@babel/runtime": "^7.5.5",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
|
"lodash.merge": "^4.6.2",
|
||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"query-string": "^6.8.3",
|
"query-string": "^6.8.3",
|
||||||
"react-cornerstone-viewport": "2.x.x"
|
"react-cornerstone-viewport": "2.x.x"
|
||||||
|
|||||||
@ -248,10 +248,13 @@ class OHIFCornerstoneViewport extends Component {
|
|||||||
// TODO: Does it make more sense to use Context?
|
// TODO: Does it make more sense to use Context?
|
||||||
if (this.props.children && this.props.children.length) {
|
if (this.props.children && this.props.children.length) {
|
||||||
childrenWithProps = this.props.children.map((child, index) => {
|
childrenWithProps = this.props.children.map((child, index) => {
|
||||||
return React.cloneElement(child, {
|
return (
|
||||||
viewportIndex: this.props.viewportIndex,
|
child &&
|
||||||
key: index,
|
React.cloneElement(child, {
|
||||||
});
|
viewportIndex: this.props.viewportIndex,
|
||||||
|
key: index,
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -148,18 +148,118 @@ const commandsModule = ({ servicesManager }) => {
|
|||||||
showDownloadViewportModal: ({ title, viewports }) => {
|
showDownloadViewportModal: ({ title, viewports }) => {
|
||||||
const activeViewportIndex = viewports.activeViewportIndex;
|
const activeViewportIndex = viewports.activeViewportIndex;
|
||||||
const { UIModalService } = servicesManager.services;
|
const { UIModalService } = servicesManager.services;
|
||||||
UIModalService.show({
|
if (UIModalService) {
|
||||||
content: CornerstoneViewportDownloadForm,
|
UIModalService.show({
|
||||||
title,
|
content: CornerstoneViewportDownloadForm,
|
||||||
contentProps: {
|
title,
|
||||||
activeViewportIndex,
|
contentProps: {
|
||||||
onClose: UIModalService.hide,
|
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 = {
|
const definitions = {
|
||||||
|
getNearbyToolData: {
|
||||||
|
commandFn: actions.getNearbyToolData,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
removeToolState: {
|
||||||
|
commandFn: actions.removeToolState,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
updateTableWithNewMeasurementData: {
|
||||||
|
commandFn: actions.updateTableWithNewMeasurementData,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
showDownloadViewportModal: {
|
showDownloadViewportModal: {
|
||||||
commandFn: actions.showDownloadViewportModal,
|
commandFn: actions.showDownloadViewportModal,
|
||||||
storeContexts: ['viewports'],
|
storeContexts: ['viewports'],
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import csTools from 'cornerstone-tools';
|
|||||||
import initCornerstoneTools from './initCornerstoneTools.js';
|
import initCornerstoneTools from './initCornerstoneTools.js';
|
||||||
import queryString from 'query-string';
|
import queryString from 'query-string';
|
||||||
import { SimpleDialog } from '@ohif/ui';
|
import { SimpleDialog } from '@ohif/ui';
|
||||||
|
import merge from 'lodash.merge';
|
||||||
|
|
||||||
function fallbackMetaDataProvider(type, imageId) {
|
function fallbackMetaDataProvider(type, imageId) {
|
||||||
if (!imageId.includes('wado?requestType=WADO')) {
|
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
|
* @param {Object|Array} configuration.csToolsConfig
|
||||||
*/
|
*/
|
||||||
export default function init({ servicesManager, configuration = {} }) {
|
export default function init({ servicesManager, configuration }) {
|
||||||
const { UIDialogService } = servicesManager.services;
|
|
||||||
const callInputDialog = (data, event, callback) => {
|
const callInputDialog = (data, event, callback) => {
|
||||||
let dialogId = UIDialogService.create({
|
const { UIDialogService } = servicesManager.services;
|
||||||
content: SimpleDialog.InputDialog,
|
|
||||||
defaultPosition: {
|
if (UIDialogService) {
|
||||||
x: (event && event.currentPoints.canvas.x) || 0,
|
let dialogId = UIDialogService.create({
|
||||||
y: (event && event.currentPoints.canvas.y) || 0,
|
centralize: true,
|
||||||
},
|
isDraggable: false,
|
||||||
showOverlay: true,
|
content: SimpleDialog.InputDialog,
|
||||||
contentProps: {
|
useLastPosition: false,
|
||||||
title: 'Enter your annotation',
|
showOverlay: true,
|
||||||
label: 'New label',
|
contentProps: {
|
||||||
measurementData: data ? { description: data.text } : {},
|
title: 'Enter your annotation',
|
||||||
onClose: () => UIDialogService.dismiss({ id: dialogId }),
|
label: 'New label',
|
||||||
onSubmit: value => {
|
measurementData: data ? { description: data.text } : {},
|
||||||
callback(value);
|
onClose: () => UIDialogService.dismiss({ id: dialogId }),
|
||||||
UIDialogService.dismiss({ id: dialogId });
|
onSubmit: value => {
|
||||||
|
callback(value);
|
||||||
|
UIDialogService.dismiss({ id: dialogId });
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
});
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const { csToolsConfig } = configuration;
|
const { csToolsConfig } = configuration;
|
||||||
@ -73,61 +77,55 @@ export default function init({ servicesManager, configuration = {} }) {
|
|||||||
initCornerstoneTools(defaultCsToolsConfig);
|
initCornerstoneTools(defaultCsToolsConfig);
|
||||||
|
|
||||||
// ~~ Toooools 🙌
|
// ~~ 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 = [
|
const tools = [
|
||||||
PanTool,
|
csTools.PanTool,
|
||||||
ZoomTool,
|
csTools.ZoomTool,
|
||||||
WwwcTool,
|
csTools.WwwcTool,
|
||||||
MagnifyTool,
|
csTools.MagnifyTool,
|
||||||
StackScrollTool,
|
csTools.StackScrollTool,
|
||||||
StackScrollMouseWheelTool,
|
csTools.StackScrollMouseWheelTool,
|
||||||
// Touch
|
// Touch
|
||||||
PanMultiTouchTool,
|
csTools.PanMultiTouchTool,
|
||||||
ZoomTouchPinchTool,
|
csTools.ZoomTouchPinchTool,
|
||||||
// Annotations
|
// Annotations
|
||||||
EraserTool,
|
csTools.ArrowAnnotateTool,
|
||||||
BidirectionalTool,
|
csTools.EraserTool,
|
||||||
LengthTool,
|
csTools.BidirectionalTool,
|
||||||
AngleTool,
|
csTools.LengthTool,
|
||||||
FreehandRoiTool,
|
csTools.AngleTool,
|
||||||
EllipticalRoiTool,
|
csTools.FreehandRoiTool,
|
||||||
DragProbeTool,
|
csTools.EllipticalRoiTool,
|
||||||
RectangleRoiTool,
|
csTools.DragProbeTool,
|
||||||
|
csTools.RectangleRoiTool,
|
||||||
// Segmentation
|
// Segmentation
|
||||||
BrushTool,
|
csTools.BrushTool,
|
||||||
];
|
];
|
||||||
|
|
||||||
tools.forEach(tool => csTools.addTool(tool));
|
/* Add extension tools configuration here. */
|
||||||
|
const extensionToolsConfiguration = {
|
||||||
csTools.addTool(csTools.ArrowAnnotateTool, {
|
ArrowAnnotate: {
|
||||||
configuration: {
|
configuration: {
|
||||||
getTextCallback: (callback, eventDetails) =>
|
getTextCallback: (callback, eventDetails) =>
|
||||||
callInputDialog(null, eventDetails, callback),
|
callInputDialog(null, eventDetails, callback),
|
||||||
changeTextCallback: (data, eventDetails, callback) =>
|
changeTextCallback: (data, eventDetails, callback) =>
|
||||||
callInputDialog(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('Pan', { mouseButtonMask: 4 });
|
||||||
csTools.setToolActive('Zoom', { mouseButtonMask: 2 });
|
csTools.setToolActive('Zoom', { mouseButtonMask: 2 });
|
||||||
|
|||||||
@ -358,10 +358,13 @@ class OHIFVTKViewport extends Component {
|
|||||||
// TODO: Does it make more sense to use Context?
|
// TODO: Does it make more sense to use Context?
|
||||||
if (this.props.children && this.props.children.length) {
|
if (this.props.children && this.props.children.length) {
|
||||||
childrenWithProps = this.props.children.map((child, index) => {
|
childrenWithProps = this.props.children.map((child, index) => {
|
||||||
return React.cloneElement(child, {
|
return (
|
||||||
viewportIndex: this.props.viewportIndex,
|
child &&
|
||||||
key: index,
|
React.cloneElement(child, {
|
||||||
});
|
viewportIndex: this.props.viewportIndex,
|
||||||
|
key: index,
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -59,7 +59,7 @@ export class CommandsManager {
|
|||||||
*
|
*
|
||||||
* @method
|
* @method
|
||||||
* @param {string} contextName - Namespace for commands
|
* @param {string} contextName - Namespace for commands
|
||||||
* @returs {Object} - the matched context
|
* @returns {Object} - the matched context
|
||||||
*/
|
*/
|
||||||
getContext(contextName) {
|
getContext(contextName) {
|
||||||
const context = this.contexts[contextName];
|
const context = this.contexts[contextName];
|
||||||
|
|||||||
@ -26,7 +26,7 @@ export default class ExtensionManager {
|
|||||||
const hasConfiguration = Array.isArray(extension);
|
const hasConfiguration = Array.isArray(extension);
|
||||||
|
|
||||||
if (hasConfiguration) {
|
if (hasConfiguration) {
|
||||||
const [ohifExtension, configuration] = extensions;
|
const [ohifExtension, configuration] = extension;
|
||||||
this.registerExtension(ohifExtension, configuration);
|
this.registerExtension(ohifExtension, configuration);
|
||||||
} else {
|
} else {
|
||||||
this.registerExtension(extension);
|
this.registerExtension(extension);
|
||||||
|
|||||||
@ -37,6 +37,24 @@ describe('ExtensionManager.js', () => {
|
|||||||
// Assert
|
// Assert
|
||||||
expect(extensionManager.registerExtension.mock.calls.length).toBe(3);
|
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()', () => {
|
describe('registerExtension()', () => {
|
||||||
|
|||||||
@ -23,6 +23,8 @@ import {
|
|||||||
createUINotificationService,
|
createUINotificationService,
|
||||||
createUIModalService,
|
createUIModalService,
|
||||||
createUIDialogService,
|
createUIDialogService,
|
||||||
|
createUIContextMenuService,
|
||||||
|
createUILabellingFlowService,
|
||||||
} from './services';
|
} from './services';
|
||||||
|
|
||||||
const OHIF = {
|
const OHIF = {
|
||||||
@ -53,6 +55,8 @@ const OHIF = {
|
|||||||
createUINotificationService,
|
createUINotificationService,
|
||||||
createUIModalService,
|
createUIModalService,
|
||||||
createUIDialogService,
|
createUIDialogService,
|
||||||
|
createUIContextMenuService,
|
||||||
|
createUILabellingFlowService,
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -82,6 +86,8 @@ export {
|
|||||||
createUINotificationService,
|
createUINotificationService,
|
||||||
createUIModalService,
|
createUIModalService,
|
||||||
createUIDialogService,
|
createUIDialogService,
|
||||||
|
createUIContextMenuService,
|
||||||
|
createUILabellingFlowService,
|
||||||
};
|
};
|
||||||
|
|
||||||
export { OHIF };
|
export { OHIF };
|
||||||
|
|||||||
@ -13,6 +13,8 @@ describe('Top level exports', () => {
|
|||||||
'createUINotificationService',
|
'createUINotificationService',
|
||||||
'createUIModalService',
|
'createUIModalService',
|
||||||
'createUIDialogService',
|
'createUIDialogService',
|
||||||
|
'createUIContextMenuService',
|
||||||
|
'createUILabellingFlowService',
|
||||||
//
|
//
|
||||||
'utils',
|
'utils',
|
||||||
'studies',
|
'studies',
|
||||||
|
|||||||
63
platform/core/src/services/UIContextMenuService/index.js
Normal file
63
platform/core/src/services/UIContextMenuService/index.js
Normal 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;
|
||||||
@ -17,8 +17,9 @@
|
|||||||
* @property {Object} contentProps The dialog content props.
|
* @property {Object} contentProps The dialog content props.
|
||||||
* @property {boolean} [isDraggable=true] Controls if dialog content is draggable or not.
|
* @property {boolean} [isDraggable=true] Controls if dialog content is draggable or not.
|
||||||
* @property {boolean} [showOverlay=false] Controls dialog overlay.
|
* @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} 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} onStart Called when dragging starts. If `false` is returned any handler, the action will cancel.
|
||||||
* @property {Function} onStop Called when dragging stops.
|
* @property {Function} onStop Called when dragging stops.
|
||||||
* @property {Function} onDrag Called while dragging.
|
* @property {Function} onDrag Called while dragging.
|
||||||
@ -45,7 +46,7 @@ function createUIDialogService() {
|
|||||||
/**
|
/**
|
||||||
* Show a new UI dialog;
|
* 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({
|
function create({
|
||||||
id,
|
id,
|
||||||
@ -54,10 +55,11 @@ function create({
|
|||||||
onStart,
|
onStart,
|
||||||
onDrag,
|
onDrag,
|
||||||
onStop,
|
onStop,
|
||||||
|
centralize = false,
|
||||||
|
preservePosition = true,
|
||||||
isDraggable = true,
|
isDraggable = true,
|
||||||
showOverlay = false,
|
showOverlay = false,
|
||||||
defaultPosition,
|
defaultPosition,
|
||||||
position,
|
|
||||||
}) {
|
}) {
|
||||||
return uiDialogServiceImplementation._create({
|
return uiDialogServiceImplementation._create({
|
||||||
id,
|
id,
|
||||||
@ -66,10 +68,11 @@ function create({
|
|||||||
onStart,
|
onStart,
|
||||||
onDrag,
|
onDrag,
|
||||||
onStop,
|
onStop,
|
||||||
|
centralize,
|
||||||
|
preservePosition,
|
||||||
isDraggable,
|
isDraggable,
|
||||||
showOverlay,
|
showOverlay,
|
||||||
defaultPosition,
|
defaultPosition,
|
||||||
position,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
68
platform/core/src/services/UILabellingFlowService/index.js
Normal file
68
platform/core/src/services/UILabellingFlowService/index.js
Normal 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;
|
||||||
@ -2,10 +2,14 @@ import ServicesManager from './ServicesManager.js';
|
|||||||
import createUINotificationService from './UINotificationService';
|
import createUINotificationService from './UINotificationService';
|
||||||
import createUIModalService from './UIModalService';
|
import createUIModalService from './UIModalService';
|
||||||
import createUIDialogService from './UIDialogService';
|
import createUIDialogService from './UIDialogService';
|
||||||
|
import createUIContextMenuService from './UIContextMenuService';
|
||||||
|
import createUILabellingFlowService from './UILabellingFlowService';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
createUINotificationService,
|
createUINotificationService,
|
||||||
createUIModalService,
|
createUIModalService,
|
||||||
createUIDialogService,
|
createUIDialogService,
|
||||||
|
createUIContextMenuService,
|
||||||
|
createUILabellingFlowService,
|
||||||
ServicesManager,
|
ServicesManager,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,9 +6,7 @@
|
|||||||
position: relative
|
position: relative
|
||||||
|
|
||||||
.simpleDialog
|
.simpleDialog
|
||||||
position: fixed;
|
position: relative;
|
||||||
top: 0px;
|
|
||||||
left: 0px;
|
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|||||||
147
platform/ui/src/contextProviders/ContextMenuProvider.js
Normal file
147
platform/ui/src/contextProviders/ContextMenuProvider.js
Normal 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;
|
||||||
@ -20,7 +20,30 @@ export const useDialog = () => useContext(DialogContext);
|
|||||||
const DialogProvider = ({ children, service }) => {
|
const DialogProvider = ({ children, service }) => {
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const [dialogs, setDialogs] = useState([]);
|
const [dialogs, setDialogs] = useState([]);
|
||||||
|
const [lastDialogId, setLastDialogId] = useState(null);
|
||||||
const [lastDialogPosition, setLastDialogPosition] = 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.
|
* 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 {Object} contentProps The dialog content props.
|
||||||
* @property {boolean} isDraggable Controls if dialog content is draggable or not.
|
* @property {boolean} isDraggable Controls if dialog content is draggable or not.
|
||||||
* @property {boolean} showOverlay Controls dialog overlay.
|
* @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} 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} onStart Called when dragging starts. If `false` is returned any handler, the action will cancel.
|
||||||
* @property {Function} onStop Called when dragging stops.
|
* @property {Function} onStop Called when dragging stops.
|
||||||
* @property {Function} onDrag Called while dragging.
|
* @property {Function} onDrag Called while dragging.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
useEffect(() => _bringToFront(lastDialogId), [_bringToFront, lastDialogId]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new dialog and return its id.
|
* Creates a new dialog and return its id.
|
||||||
*
|
*
|
||||||
@ -64,6 +90,7 @@ const DialogProvider = ({ children, service }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setDialogs(dialogs => [...dialogs, { ...props, id: dialogId }]);
|
setDialogs(dialogs => [...dialogs, { ...props, id: dialogId }]);
|
||||||
|
setLastDialogId(dialogId);
|
||||||
|
|
||||||
return dialogId;
|
return dialogId;
|
||||||
}, []);
|
}, []);
|
||||||
@ -75,9 +102,11 @@ const DialogProvider = ({ children, service }) => {
|
|||||||
* @property {string} props.id The dialog id.
|
* @property {string} props.id The dialog id.
|
||||||
* @returns void
|
* @returns void
|
||||||
*/
|
*/
|
||||||
const dismiss = useCallback(({ id }) => {
|
const dismiss = useCallback(
|
||||||
setDialogs(dialogs => dialogs.filter(dialog => dialog.id !== id));
|
({ id }) =>
|
||||||
}, []);
|
setDialogs(dialogs => dialogs.filter(dialog => dialog.id !== id)),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dismisses all dialogs.
|
* Dismisses all dialogs.
|
||||||
@ -101,14 +130,14 @@ const DialogProvider = ({ children, service }) => {
|
|||||||
* @param {string} id The dialog id.
|
* @param {string} id The dialog id.
|
||||||
* @returns void
|
* @returns void
|
||||||
*/
|
*/
|
||||||
const _bringToFront = id => {
|
const _bringToFront = useCallback(id => {
|
||||||
setDialogs(dialogs => {
|
setDialogs(dialogs => {
|
||||||
const topDialog = dialogs.find(dialog => dialog.id === id);
|
const topDialog = dialogs.find(dialog => dialog.id === id);
|
||||||
return topDialog
|
return topDialog
|
||||||
? [...dialogs.filter(dialog => dialog.id !== id), topDialog]
|
? [...dialogs.filter(dialog => dialog.id !== id), topDialog]
|
||||||
: [];
|
: dialogs;
|
||||||
});
|
});
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const renderDialogs = () =>
|
const renderDialogs = () =>
|
||||||
dialogs.map(dialog => {
|
dialogs.map(dialog => {
|
||||||
@ -116,20 +145,27 @@ const DialogProvider = ({ children, service }) => {
|
|||||||
id,
|
id,
|
||||||
content: DialogContent,
|
content: DialogContent,
|
||||||
contentProps,
|
contentProps,
|
||||||
position,
|
|
||||||
defaultPosition,
|
defaultPosition,
|
||||||
|
centralize = false,
|
||||||
|
preservePosition = true,
|
||||||
isDraggable = true,
|
isDraggable = true,
|
||||||
onStart,
|
onStart,
|
||||||
onStop,
|
onStop,
|
||||||
onDrag,
|
onDrag,
|
||||||
} = dialog;
|
} = dialog;
|
||||||
|
|
||||||
|
let position =
|
||||||
|
(preservePosition && lastDialogPosition) || defaultPosition;
|
||||||
|
if (centralize) {
|
||||||
|
position = centerPositions.find(position => position.id === id);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Draggable
|
<Draggable
|
||||||
key={id}
|
key={id}
|
||||||
disabled={!isDraggable}
|
disabled={!isDraggable}
|
||||||
position={position}
|
position={position}
|
||||||
defaultPosition={lastDialogPosition || defaultPosition}
|
defaultPosition={position}
|
||||||
bounds="parent"
|
bounds="parent"
|
||||||
onStart={event => {
|
onStart={event => {
|
||||||
const e = event || window.event;
|
const e = event || window.event;
|
||||||
@ -169,7 +205,11 @@ const DialogProvider = ({ children, service }) => {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
id={`draggableItem-${id}`}
|
id={`draggableItem-${id}`}
|
||||||
className={classNames('DraggableItem', isDragging && 'dragging')}
|
className={classNames(
|
||||||
|
'DraggableItem',
|
||||||
|
isDragging && 'dragging',
|
||||||
|
isDraggable && 'draggable'
|
||||||
|
)}
|
||||||
style={{ zIndex: '999', position: 'absolute' }}
|
style={{ zIndex: '999', position: 'absolute' }}
|
||||||
onClick={() => _bringToFront(id)}
|
onClick={() => _bringToFront(id)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
.DraggableItem
|
.DraggableItem.draggable
|
||||||
div
|
div
|
||||||
cursor: grab !important
|
cursor: grab !important
|
||||||
|
|
||||||
.DraggableItem.dragging
|
.DraggableItem.draggable.dragging
|
||||||
div
|
div
|
||||||
cursor: grabbing !important
|
cursor: grabbing !important
|
||||||
|
|
||||||
|
|||||||
136
platform/ui/src/contextProviders/LabellingFlowProvider.js
Normal file
136
platform/ui/src/contextProviders/LabellingFlowProvider.js
Normal 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;
|
||||||
@ -18,3 +18,15 @@ export {
|
|||||||
withDialog,
|
withDialog,
|
||||||
useDialog,
|
useDialog,
|
||||||
} from './DialogProvider.js';
|
} from './DialogProvider.js';
|
||||||
|
export {
|
||||||
|
default as ContextMenuProvider,
|
||||||
|
withContextMenu,
|
||||||
|
useContextMenu,
|
||||||
|
ContextMenuConsumer,
|
||||||
|
} from './ContextMenuProvider.js';
|
||||||
|
export {
|
||||||
|
default as LabellingFlowProvider,
|
||||||
|
withLabellingFlow,
|
||||||
|
useLabellingFlow,
|
||||||
|
LabellingFlowConsumer,
|
||||||
|
} from './LabellingFlowProvider.js';
|
||||||
|
|||||||
@ -60,6 +60,14 @@ import {
|
|||||||
ModalConsumer,
|
ModalConsumer,
|
||||||
useModal,
|
useModal,
|
||||||
withModal,
|
withModal,
|
||||||
|
ContextMenuProvider,
|
||||||
|
ContextMenuConsumer,
|
||||||
|
useContextMenu,
|
||||||
|
withContextMenu,
|
||||||
|
LabellingFlowProvider,
|
||||||
|
LabellingFlowConsumer,
|
||||||
|
useLabellingFlow,
|
||||||
|
withLabellingFlow,
|
||||||
} from './contextProviders';
|
} from './contextProviders';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -117,6 +125,14 @@ export {
|
|||||||
DialogProvider,
|
DialogProvider,
|
||||||
withDialog,
|
withDialog,
|
||||||
useDialog,
|
useDialog,
|
||||||
|
ContextMenuProvider,
|
||||||
|
ContextMenuConsumer,
|
||||||
|
useContextMenu,
|
||||||
|
withContextMenu,
|
||||||
|
LabellingFlowProvider,
|
||||||
|
LabellingFlowConsumer,
|
||||||
|
useLabellingFlow,
|
||||||
|
withLabellingFlow,
|
||||||
// Hooks
|
// Hooks
|
||||||
useDebounce,
|
useDebounce,
|
||||||
useMedia,
|
useMedia,
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
window.config = {
|
window.config = {
|
||||||
// default: '/'
|
// default: '/'
|
||||||
routerBasename: '/',
|
routerBasename: '/',
|
||||||
|
whiteLabelling: {},
|
||||||
extensions: [],
|
extensions: [],
|
||||||
showStudyList: true,
|
showStudyList: true,
|
||||||
filterQueryParam: false,
|
filterQueryParam: false,
|
||||||
@ -69,4 +70,5 @@ window.config = {
|
|||||||
// ~ Cornerstone Tools
|
// ~ Cornerstone Tools
|
||||||
{ commandName: 'setZoomTool', label: 'Zoom', keys: ['z'] },
|
{ commandName: 'setZoomTool', label: 'Zoom', keys: ['z'] },
|
||||||
],
|
],
|
||||||
|
cornerstoneExtensionConfig: { tools: {} },
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
window.config = {
|
window.config = {
|
||||||
routerBasename: '/',
|
routerBasename: '/',
|
||||||
|
whiteLabelling: {},
|
||||||
extensions: [],
|
extensions: [],
|
||||||
showStudyList: true,
|
showStudyList: true,
|
||||||
servers: {
|
servers: {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
window.config = {
|
window.config = {
|
||||||
routerBasename: '/',
|
routerBasename: '/',
|
||||||
|
whiteLabelling: {},
|
||||||
showStudyList: true,
|
showStudyList: true,
|
||||||
servers: {
|
servers: {
|
||||||
dicomWeb: [
|
dicomWeb: [
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
window.config = {
|
window.config = {
|
||||||
routerBasename: '/',
|
routerBasename: '/',
|
||||||
|
whiteLabelling: {},
|
||||||
showStudyList: true,
|
showStudyList: true,
|
||||||
servers: {
|
servers: {
|
||||||
// This is an array, but we'll only use the first entry for now
|
// This is an array, but we'll only use the first entry for now
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
window.config = {
|
window.config = {
|
||||||
routerBasename: '/',
|
routerBasename: '/',
|
||||||
|
whiteLabelling: {},
|
||||||
showStudyList: true,
|
showStudyList: true,
|
||||||
servers: {
|
servers: {
|
||||||
// This is an array, but we'll only use the first entry for now
|
// This is an array, but we'll only use the first entry for now
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
window.config = {
|
window.config = {
|
||||||
routerBasename: '/',
|
routerBasename: '/',
|
||||||
|
whiteLabelling: {},
|
||||||
enableGoogleCloudAdapter: true,
|
enableGoogleCloudAdapter: true,
|
||||||
servers: {
|
servers: {
|
||||||
// This is an array, but we'll only use the first entry for now
|
// 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',
|
client_id: 'YOURCLIENTID.apps.googleusercontent.com',
|
||||||
redirect_uri: '/callback', // `OHIFStandaloneViewer.js`
|
redirect_uri: '/callback', // `OHIFStandaloneViewer.js`
|
||||||
response_type: 'id_token token',
|
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
|
// ~ OPTIONAL
|
||||||
post_logout_redirect_uri: '/logout-redirect.html',
|
post_logout_redirect_uri: '/logout-redirect.html',
|
||||||
revoke_uri: 'https://accounts.google.com/o/oauth2/revoke?token=',
|
revoke_uri: 'https://accounts.google.com/o/oauth2/revoke?token=',
|
||||||
@ -23,4 +25,4 @@ window.config = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
studyListFunctionsEnabled: true,
|
studyListFunctionsEnabled: true,
|
||||||
}
|
};
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
window.config = {
|
window.config = {
|
||||||
// default: '/'
|
// default: '/'
|
||||||
routerBasename: '/',
|
routerBasename: '/',
|
||||||
|
whiteLabelling: {},
|
||||||
// default: ''
|
// default: ''
|
||||||
showStudyList: true,
|
showStudyList: true,
|
||||||
servers: {
|
servers: {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
window.config = {
|
window.config = {
|
||||||
routerBasename: '/pwa',
|
routerBasename: '/pwa',
|
||||||
|
whiteLabelling: {},
|
||||||
showStudyList: true,
|
showStudyList: true,
|
||||||
servers: {
|
servers: {
|
||||||
dicomWeb: [
|
dicomWeb: [
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
window.config = {
|
window.config = {
|
||||||
routerBasename: '/',
|
routerBasename: '/',
|
||||||
|
whiteLabelling: {},
|
||||||
showStudyList: true,
|
showStudyList: true,
|
||||||
servers: {
|
servers: {
|
||||||
dicomWeb: [
|
dicomWeb: [
|
||||||
|
|||||||
@ -1,74 +1,102 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<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">
|
<head>
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="<%= PUBLIC_URL %>assets/favicon-16x16.png">
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="<%= PUBLIC_URL %>assets/favicon-32x32.png">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||||
<link rel="apple-touch-icon" sizes="57x57" href="<%= PUBLIC_URL %>assets/apple-touch-icon-57x57.png">
|
<meta name="theme-color" content="#000000" />
|
||||||
<link rel="apple-touch-icon" sizes="60x60" href="<%= PUBLIC_URL %>assets/apple-touch-icon-60x60.png">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<link rel="apple-touch-icon" sizes="72x72" href="<%= PUBLIC_URL %>assets/apple-touch-icon-72x72.png">
|
<meta name="application-name" content="OHIF Viewer">
|
||||||
<link rel="apple-touch-icon" sizes="76x76" href="<%= PUBLIC_URL %>assets/apple-touch-icon-76x76.png">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<link rel="apple-touch-icon" sizes="114x114" href="<%= PUBLIC_URL %>assets/apple-touch-icon-114x114.png">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
<link rel="apple-touch-icon" sizes="120x120" href="<%= PUBLIC_URL %>assets/apple-touch-icon-120x120.png">
|
<meta name="apple-mobile-web-app-title" content="@ohif/viewer">
|
||||||
<link rel="apple-touch-icon" sizes="144x144" href="<%= PUBLIC_URL %>assets/apple-touch-icon-144x144.png">
|
<meta name="msapplication-TileColor" content="#fff">
|
||||||
<link rel="apple-touch-icon" sizes="152x152" href="<%= PUBLIC_URL %>assets/apple-touch-icon-152x152.png">
|
<meta name="msapplication-TileImage" content="<%= PUBLIC_URL %>assets/mstile-144x144.png">
|
||||||
<link rel="apple-touch-icon" sizes="167x167" href="<%= PUBLIC_URL %>assets/apple-touch-icon-167x167.png">
|
<meta name="msapplication-config" content="<%= PUBLIC_URL %>assets/browserconfig.xml">
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="<%= PUBLIC_URL %>assets/apple-touch-icon-180x180.png">
|
<link rel="manifest" href="<%= PUBLIC_URL %>manifest.json">
|
||||||
<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">
|
|
||||||
|
|
||||||
<!-- Built with: https://polyfill.io/v3/url-builder/ -->
|
<link rel="shortcut icon" href="<%= PUBLIC_URL %>assets/favicon.ico">
|
||||||
<!-- Targets IE11 -->
|
<link rel="icon" type="image/png" sizes="16x16" href="<%= PUBLIC_URL %>assets/favicon-16x16.png">
|
||||||
<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>
|
<link rel="icon" type="image/png" sizes="32x32" href="<%= PUBLIC_URL %>assets/favicon-32x32.png">
|
||||||
<script type="text/javascript">
|
<link rel="apple-touch-icon" sizes="57x57" href="<%= PUBLIC_URL %>assets/apple-touch-icon-57x57.png">
|
||||||
window.PUBLIC_URL = '<%= PUBLIC_URL %>';
|
<link rel="apple-touch-icon" sizes="60x60" href="<%= PUBLIC_URL %>assets/apple-touch-icon-60x60.png">
|
||||||
</script>
|
<link rel="apple-touch-icon" sizes="72x72" href="<%= PUBLIC_URL %>assets/apple-touch-icon-72x72.png">
|
||||||
<script type="text/javascript" src="<%= PUBLIC_URL %>app-config.js"></script>
|
<link rel="apple-touch-icon" sizes="76x76" href="<%= PUBLIC_URL %>assets/apple-touch-icon-76x76.png">
|
||||||
<script type="module" src="<%= PUBLIC_URL %>init-service-worker.js"></script>
|
<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 -->
|
<title>OHIF Viewer</title>
|
||||||
<link
|
|
||||||
href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700&display=swap"
|
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- EXTENSIONS -->
|
<!-- WEB FONTS -->
|
||||||
<!-- <script type="text/javascript" src="path/to/some-extension.js"></script>
|
<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>
|
<script>
|
||||||
|
// If configuration file is a JS Object:
|
||||||
window.config.extensions = [SomeExtension];
|
window.config.extensions = [SomeExtension];
|
||||||
|
|
||||||
|
// If configuration file is a JS Function:
|
||||||
|
window.config = () => {
|
||||||
|
return {
|
||||||
|
extensions: [SomeExtension]
|
||||||
|
};
|
||||||
|
};
|
||||||
</script> -->
|
</script> -->
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<noscript> You need to enable JavaScript to run this app. </noscript>
|
<noscript> You need to enable JavaScript to run this app. </noscript>
|
||||||
|
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
|
||||||
<div id="root"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -4,9 +4,13 @@ import { I18nextProvider } from 'react-i18next';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { BrowserRouter as Router } from 'react-router-dom';
|
import { BrowserRouter as Router } from 'react-router-dom';
|
||||||
import OHIFCornerstoneExtension from '@ohif/extension-cornerstone';
|
|
||||||
import { hot } from 'react-hot-loader/root';
|
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 {
|
import {
|
||||||
SnackbarProvider,
|
SnackbarProvider,
|
||||||
ModalProvider,
|
ModalProvider,
|
||||||
@ -14,6 +18,11 @@ import {
|
|||||||
OHIFModal,
|
OHIFModal,
|
||||||
} from '@ohif/ui';
|
} from '@ohif/ui';
|
||||||
|
|
||||||
|
import {
|
||||||
|
LabellingFlowProvider,
|
||||||
|
ContextMenuProvider,
|
||||||
|
} from './appCustomProviders';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CommandsManager,
|
CommandsManager,
|
||||||
ExtensionManager,
|
ExtensionManager,
|
||||||
@ -22,8 +31,10 @@ import {
|
|||||||
createUINotificationService,
|
createUINotificationService,
|
||||||
createUIModalService,
|
createUIModalService,
|
||||||
createUIDialogService,
|
createUIDialogService,
|
||||||
|
createUIContextMenuService,
|
||||||
|
createUILabellingFlowService,
|
||||||
utils,
|
utils,
|
||||||
redux as reduxOHIF
|
redux as reduxOHIF,
|
||||||
} from '@ohif/core';
|
} from '@ohif/core';
|
||||||
|
|
||||||
import i18n from '@ohif/i18n';
|
import i18n from '@ohif/i18n';
|
||||||
@ -46,12 +57,12 @@ import OHIFStandaloneViewer from './OHIFStandaloneViewer';
|
|||||||
/** Store */
|
/** Store */
|
||||||
import { getActiveContexts } from './store/layout/selectors.js';
|
import { getActiveContexts } from './store/layout/selectors.js';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
const { setUserPreferences } = reduxOHIF.actions;
|
|
||||||
|
|
||||||
/** Contexts */
|
/** Contexts */
|
||||||
import WhiteLabellingContext from './context/WhiteLabellingContext';
|
import WhiteLabellingContext from './context/WhiteLabellingContext';
|
||||||
import UserManagerContext from './context/UserManagerContext';
|
import UserManagerContext from './context/UserManagerContext';
|
||||||
import AppContext from './context/AppContext';
|
import AppContext from './context/AppContext';
|
||||||
|
const { setUserPreferences } = reduxOHIF.actions;
|
||||||
|
|
||||||
/** ~~~~~~~~~~~~~ Application Setup */
|
/** ~~~~~~~~~~~~~ Application Setup */
|
||||||
const commandsManagerConfig = {
|
const commandsManagerConfig = {
|
||||||
@ -63,6 +74,8 @@ const commandsManagerConfig = {
|
|||||||
const UINotificationService = createUINotificationService();
|
const UINotificationService = createUINotificationService();
|
||||||
const UIModalService = createUIModalService();
|
const UIModalService = createUIModalService();
|
||||||
const UIDialogService = createUIDialogService();
|
const UIDialogService = createUIDialogService();
|
||||||
|
const UIContextMenuService = createUIContextMenuService();
|
||||||
|
const UILabellingFlowService = createUILabellingFlowService();
|
||||||
|
|
||||||
/** Managers */
|
/** Managers */
|
||||||
const commandsManager = new CommandsManager(commandsManagerConfig);
|
const commandsManager = new CommandsManager(commandsManagerConfig);
|
||||||
@ -79,23 +92,25 @@ window.store = store;
|
|||||||
|
|
||||||
class App extends Component {
|
class App extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
routerBasename: PropTypes.string.isRequired,
|
config: PropTypes.oneOfType([
|
||||||
servers: PropTypes.object.isRequired,
|
PropTypes.func,
|
||||||
//
|
|
||||||
oidc: PropTypes.array,
|
|
||||||
whiteLabelling: PropTypes.object,
|
|
||||||
extensions: PropTypes.arrayOf(
|
|
||||||
PropTypes.shape({
|
PropTypes.shape({
|
||||||
id: PropTypes.string.isRequired,
|
routerBasename: PropTypes.string.isRequired,
|
||||||
})
|
oidc: PropTypes.array,
|
||||||
),
|
whiteLabelling: PropTypes.object,
|
||||||
hotkeys: PropTypes.array,
|
extensions: PropTypes.array,
|
||||||
|
}),
|
||||||
|
]).isRequired,
|
||||||
|
defaultExtensions: PropTypes.array,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
whiteLabelling: {},
|
config: {
|
||||||
oidc: [],
|
whiteLabelling: {},
|
||||||
extensions: [],
|
oidc: [],
|
||||||
|
extensions: [],
|
||||||
|
},
|
||||||
|
defaultExtensions: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
_appConfig;
|
_appConfig;
|
||||||
@ -104,31 +119,59 @@ class App extends Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(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);
|
this.initUserManager(oidc);
|
||||||
_initServices([UINotificationService, UIModalService, UIDialogService]);
|
_initServices([
|
||||||
_initExtensions(extensions, hotkeys);
|
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);
|
_initServers(servers);
|
||||||
initWebWorkers();
|
initWebWorkers();
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { whiteLabelling, routerBasename } = this.props;
|
const { whiteLabelling, routerBasename } = this._appConfig;
|
||||||
const userManager = this._userManager;
|
if (this._userManager) {
|
||||||
const config = {
|
|
||||||
appConfig: this._appConfig,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (userManager) {
|
|
||||||
return (
|
return (
|
||||||
<AppContext.Provider value={config}>
|
<AppContext.Provider value={{ appConfig: this._appConfig }}>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<I18nextProvider i18n={i18n}>
|
<I18nextProvider i18n={i18n}>
|
||||||
<OidcProvider store={store} userManager={userManager}>
|
<OidcProvider store={store} userManager={this._userManager}>
|
||||||
<UserManagerContext.Provider value={userManager}>
|
<UserManagerContext.Provider value={this._userManager}>
|
||||||
<Router basename={routerBasename}>
|
<Router basename={routerBasename}>
|
||||||
<WhiteLabellingContext.Provider value={whiteLabelling}>
|
<WhiteLabellingContext.Provider value={whiteLabelling}>
|
||||||
<SnackbarProvider service={UINotificationService}>
|
<SnackbarProvider service={UINotificationService}>
|
||||||
@ -137,7 +180,21 @@ class App extends Component {
|
|||||||
modal={OHIFModal}
|
modal={OHIFModal}
|
||||||
service={UIModalService}
|
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>
|
</ModalProvider>
|
||||||
</DialogProvider>
|
</DialogProvider>
|
||||||
</SnackbarProvider>
|
</SnackbarProvider>
|
||||||
@ -152,7 +209,7 @@ class App extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppContext.Provider value={config}>
|
<AppContext.Provider value={{ appConfig: this._appConfig }}>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<I18nextProvider i18n={i18n}>
|
<I18nextProvider i18n={i18n}>
|
||||||
<Router basename={routerBasename}>
|
<Router basename={routerBasename}>
|
||||||
@ -160,7 +217,19 @@ class App extends Component {
|
|||||||
<SnackbarProvider service={UINotificationService}>
|
<SnackbarProvider service={UINotificationService}>
|
||||||
<DialogProvider service={UIDialogService}>
|
<DialogProvider service={UIDialogService}>
|
||||||
<ModalProvider modal={OHIFModal} service={UIModalService}>
|
<ModalProvider modal={OHIFModal} service={UIModalService}>
|
||||||
<OHIFStandaloneViewer />
|
<LabellingFlowProvider
|
||||||
|
service={UILabellingFlowService}
|
||||||
|
labellingComponent={LabellingManager}
|
||||||
|
commandsManager={commandsManager}
|
||||||
|
>
|
||||||
|
<ContextMenuProvider
|
||||||
|
service={UIContextMenuService}
|
||||||
|
contextMenuComponent={ToolContextMenu}
|
||||||
|
commandsManager={commandsManager}
|
||||||
|
>
|
||||||
|
<OHIFStandaloneViewer />
|
||||||
|
</ContextMenuProvider>
|
||||||
|
</LabellingFlowProvider>
|
||||||
</ModalProvider>
|
</ModalProvider>
|
||||||
</DialogProvider>
|
</DialogProvider>
|
||||||
</SnackbarProvider>
|
</SnackbarProvider>
|
||||||
@ -174,10 +243,10 @@ class App extends Component {
|
|||||||
|
|
||||||
initUserManager(oidc) {
|
initUserManager(oidc) {
|
||||||
if (oidc && !!oidc.length) {
|
if (oidc && !!oidc.length) {
|
||||||
const firstOpenIdClient = this.props.oidc[0];
|
const firstOpenIdClient = this._appConfig.oidc[0];
|
||||||
|
|
||||||
const { protocol, host } = window.location;
|
const { protocol, host } = window.location;
|
||||||
const { routerBasename } = this.props;
|
const { routerBasename } = this._appConfig;
|
||||||
const baseUri = `${protocol}//${host}${routerBasename}`;
|
const baseUri = `${protocol}//${host}${routerBasename}`;
|
||||||
|
|
||||||
const redirect_uri = firstOpenIdClient.redirect_uri || '/callback';
|
const redirect_uri = firstOpenIdClient.redirect_uri || '/callback';
|
||||||
@ -213,22 +282,22 @@ function _initServices(services) {
|
|||||||
/**
|
/**
|
||||||
* @param
|
* @param
|
||||||
*/
|
*/
|
||||||
function _initExtensions(extensions, hotkeys) {
|
function _initExtensions(extensions, cornerstoneExtensionConfig) {
|
||||||
const defaultExtensions = [
|
const requiredExtensions = [
|
||||||
GenericViewerCommands,
|
GenericViewerCommands,
|
||||||
OHIFCornerstoneExtension,
|
[OHIFCornerstoneExtension, cornerstoneExtensionConfig],
|
||||||
// WARNING: MUST BE REGISTERED _AFTER_ OHIFCORNERSTONEEXTENSION
|
/* WARNING: MUST BE REGISTERED _AFTER_ OHIFCornerstoneExtension */
|
||||||
MeasurementsPanel,
|
MeasurementsPanel,
|
||||||
];
|
];
|
||||||
const mergedExtensions = defaultExtensions.concat(extensions);
|
const mergedExtensions = requiredExtensions.concat(extensions);
|
||||||
extensionManager.registerExtensions(mergedExtensions);
|
extensionManager.registerExtensions(mergedExtensions);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _initHotkeys(hotkeys) {
|
||||||
const { hotkeyDefinitions = {} } = store.getState().preferences || {};
|
const { hotkeyDefinitions = {} } = store.getState().preferences || {};
|
||||||
let updateStore = false;
|
let updateStore = false;
|
||||||
let hotkeysToUse = hotkeyDefinitions;
|
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) {
|
if (!Object.keys(hotkeyDefinitions).length) {
|
||||||
hotkeysToUse = hotkeys;
|
hotkeysToUse = hotkeys;
|
||||||
updateStore = true;
|
updateStore = true;
|
||||||
@ -236,7 +305,8 @@ function _initExtensions(extensions, hotkeys) {
|
|||||||
|
|
||||||
if (hotkeysToUse) {
|
if (hotkeysToUse) {
|
||||||
hotkeysManager.setHotkeys(hotkeysToUse);
|
hotkeysManager.setHotkeys(hotkeysToUse);
|
||||||
// set default based on app config
|
|
||||||
|
/* Set hotkeys default based on app config. */
|
||||||
hotkeysManager.setDefaultHotKeys(hotkeys);
|
hotkeysManager.setDefaultHotKeys(hotkeys);
|
||||||
|
|
||||||
if (updateStore) {
|
if (updateStore) {
|
||||||
@ -264,7 +334,9 @@ function _makeAbsoluteIfNecessary(url, base_url) {
|
|||||||
return 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] === '/') {
|
if (base_url[base_url.length - 1] === '/') {
|
||||||
base_url = base_url.slice(0, 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;
|
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;
|
const ExportedApp = process.env.NODE_ENV === 'development' ? hot(App) : App;
|
||||||
|
|
||||||
export default ExportedApp;
|
export default ExportedApp;
|
||||||
|
|||||||
@ -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;
|
||||||
@ -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;
|
||||||
6
platform/viewer/src/appCustomProviders/index.js
Normal file
6
platform/viewer/src/appCustomProviders/index.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export {
|
||||||
|
default as LabellingFlowProvider,
|
||||||
|
} from './LabellingFlowProvider/LabellingFlowProvider.js';
|
||||||
|
export {
|
||||||
|
default as ContextMenuProvider,
|
||||||
|
} from './ContextMenuProvider/ContextMenuProvider.js';
|
||||||
@ -4,9 +4,7 @@ import OHIF from '@ohif/core';
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import cornerstone from 'cornerstone-core';
|
import cornerstone from 'cornerstone-core';
|
||||||
|
|
||||||
//
|
|
||||||
import jumpToRowItem from './jumpToRowItem.js';
|
import jumpToRowItem from './jumpToRowItem.js';
|
||||||
import getMeasurementLocationCallback from './getMeasurementLocationCallback';
|
|
||||||
|
|
||||||
const { setViewportSpecificData } = OHIF.redux.actions;
|
const { setViewportSpecificData } = OHIF.redux.actions;
|
||||||
const { MeasurementApi } = OHIF.measurements;
|
const { MeasurementApi } = OHIF.measurements;
|
||||||
@ -155,9 +153,11 @@ const mapStateToProps = state => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
const mapDispatchToProps = (dispatch, ownProps) => {
|
||||||
return {
|
return {
|
||||||
dispatchRelabel: (event, measurementData, viewportsState) => {
|
dispatchRelabel: (event, measurementData, viewportsState) => {
|
||||||
|
event.persist();
|
||||||
|
|
||||||
const activeViewportIndex =
|
const activeViewportIndex =
|
||||||
(viewportsState && viewportsState.activeViewportIndex) || 0;
|
(viewportsState && viewportsState.activeViewportIndex) || 0;
|
||||||
|
|
||||||
@ -167,31 +167,21 @@ const mapDispatchToProps = dispatch => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { element } = enabledElements[activeViewportIndex];
|
|
||||||
|
|
||||||
const eventData = {
|
|
||||||
event: {
|
|
||||||
clientX: event.clientX,
|
|
||||||
clientY: event.clientY,
|
|
||||||
},
|
|
||||||
element,
|
|
||||||
};
|
|
||||||
|
|
||||||
const { toolType, measurementId } = measurementData;
|
const { toolType, measurementId } = measurementData;
|
||||||
const tool = MeasurementApi.Instance.tools[toolType].find(measurement => {
|
const tool = MeasurementApi.Instance.tools[toolType].find(measurement => {
|
||||||
return measurement._id === measurementId;
|
return measurement._id === measurementId;
|
||||||
});
|
});
|
||||||
|
|
||||||
const options = {
|
|
||||||
skipAddLabelButton: true,
|
|
||||||
editLocation: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Clone the tool not to set empty location initially
|
// Clone the tool not to set empty location initially
|
||||||
const toolForLocation = Object.assign({}, tool, { location: null });
|
const toolForLocation = Object.assign({}, tool, { location: null });
|
||||||
getMeasurementLocationCallback(eventData, toolForLocation, options);
|
|
||||||
|
if (ownProps.onRelabel) {
|
||||||
|
ownProps.onRelabel(toolForLocation);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
dispatchEditDescription: (event, measurementData, viewportsState) => {
|
dispatchEditDescription: (event, measurementData, viewportsState) => {
|
||||||
|
event.persist();
|
||||||
|
|
||||||
const activeViewportIndex =
|
const activeViewportIndex =
|
||||||
(viewportsState && viewportsState.activeViewportIndex) || 0;
|
(viewportsState && viewportsState.activeViewportIndex) || 0;
|
||||||
|
|
||||||
@ -201,26 +191,14 @@ const mapDispatchToProps = dispatch => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { element } = enabledElements[activeViewportIndex];
|
|
||||||
|
|
||||||
const eventData = {
|
|
||||||
event: {
|
|
||||||
clientX: event.clientX,
|
|
||||||
clientY: event.clientY,
|
|
||||||
},
|
|
||||||
element,
|
|
||||||
};
|
|
||||||
|
|
||||||
const { toolType, measurementId } = measurementData;
|
const { toolType, measurementId } = measurementData;
|
||||||
const tool = MeasurementApi.Instance.tools[toolType].find(measurement => {
|
const tool = MeasurementApi.Instance.tools[toolType].find(measurement => {
|
||||||
return measurement._id === measurementId;
|
return measurement._id === measurementId;
|
||||||
});
|
});
|
||||||
|
|
||||||
const options = {
|
if (ownProps.onEditDescription) {
|
||||||
editDescriptionOnDialog: true,
|
ownProps.onEditDescription(tool);
|
||||||
};
|
}
|
||||||
|
|
||||||
getMeasurementLocationCallback(eventData, tool, options);
|
|
||||||
},
|
},
|
||||||
dispatchJumpToRowItem: (
|
dispatchJumpToRowItem: (
|
||||||
measurementData,
|
measurementData,
|
||||||
|
|||||||
@ -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,
|
|
||||||
};
|
|
||||||
@ -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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react';
|
||||||
import ConnectedMeasurementTable from './ConnectedMeasurementTable.js';
|
import ConnectedMeasurementTable from './ConnectedMeasurementTable.js';
|
||||||
import init from './init.js';
|
import init from './init.js';
|
||||||
|
|
||||||
@ -7,10 +8,38 @@ export default {
|
|||||||
*/
|
*/
|
||||||
id: 'measurements-table',
|
id: 'measurements-table',
|
||||||
|
|
||||||
preRegistration({ servicesManager, configuration = {} }) {
|
preRegistration({ servicesManager, commandsManager, configuration = {} }) {
|
||||||
init({ servicesManager, 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 {
|
return {
|
||||||
menuOptions: [
|
menuOptions: [
|
||||||
{
|
{
|
||||||
@ -22,7 +51,7 @@ export default {
|
|||||||
components: [
|
components: [
|
||||||
{
|
{
|
||||||
id: 'measurement-panel',
|
id: 'measurement-panel',
|
||||||
component: ConnectedMeasurementTable,
|
component: ExtendedConnectedMeasurementTable,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
defaultContext: ['VIEWER'],
|
defaultContext: ['VIEWER'],
|
||||||
|
|||||||
@ -1,19 +1,7 @@
|
|||||||
import OHIF from '@ohif/core';
|
import OHIF from '@ohif/core';
|
||||||
import cornerstone from 'cornerstone-core';
|
import cornerstone from 'cornerstone-core';
|
||||||
import csTools from 'cornerstone-tools';
|
import csTools from 'cornerstone-tools';
|
||||||
import {
|
|
||||||
getToolLabellingFlowCallback,
|
|
||||||
getOnRightClickCallback,
|
|
||||||
getOnTouchPressCallback,
|
|
||||||
getResetLabellingAndContextMenu,
|
|
||||||
} from './labelingFlowCallbacks.js';
|
|
||||||
import throttle from 'lodash.throttle';
|
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 {
|
const {
|
||||||
onAdded,
|
onAdded,
|
||||||
@ -33,92 +21,18 @@ const MEASUREMENT_ACTION_MAP = {
|
|||||||
*
|
*
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
* @param {*} configuration
|
* @param {Object} servicesManager
|
||||||
|
* @param {Object} configuration
|
||||||
*/
|
*/
|
||||||
export default function init({ servicesManager, configuration = {} }) {
|
export default function init({
|
||||||
const { UIDialogService } = servicesManager.services;
|
servicesManager,
|
||||||
const callInputDialog = (data, event, callback) => {
|
commandsManager,
|
||||||
let dialogId = UIDialogService.create({
|
configuration,
|
||||||
content: SimpleDialog.InputDialog,
|
}) {
|
||||||
defaultPosition: {
|
const {
|
||||||
x: (event && event.currentPoints.canvas.x) || 0,
|
UIContextMenuService,
|
||||||
y: (event && event.currentPoints.canvas.y) || 0,
|
UILabellingFlowService,
|
||||||
},
|
} = servicesManager.services;
|
||||||
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),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: MEASUREMENT_COMPLETED (not present in initial implementation)
|
// TODO: MEASUREMENT_COMPLETED (not present in initial implementation)
|
||||||
const onMeasurementsChanged = (action, event) => {
|
const onMeasurementsChanged = (action, event) => {
|
||||||
@ -131,15 +45,43 @@ export default function init({ servicesManager, configuration = {} }) {
|
|||||||
this,
|
this,
|
||||||
'labelmapModified'
|
'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`
|
const onRightClick = event => {
|
||||||
// Need to fallback to event.which;
|
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 handleClick = cornerstoneMouseClickEvent => {
|
||||||
const mouseUpEvent = cornerstoneMouseClickEvent.detail.event;
|
const mouseUpEvent = cornerstoneMouseClickEvent.detail.event;
|
||||||
const isRightClick = mouseUpEvent.which === 3;
|
const isRightClick = mouseUpEvent.which === 3;
|
||||||
@ -170,10 +112,11 @@ export default function init({ servicesManager, configuration = {} }) {
|
|||||||
csTools.EVENTS.LABELMAP_MODIFIED,
|
csTools.EVENTS.LABELMAP_MODIFIED,
|
||||||
onLabelmapModified
|
onLabelmapModified
|
||||||
);
|
);
|
||||||
//
|
|
||||||
element.addEventListener(csTools.EVENTS.TOUCH_PRESS, onTouchPress);
|
element.addEventListener(csTools.EVENTS.TOUCH_PRESS, onTouchPress);
|
||||||
element.addEventListener(csTools.EVENTS.MOUSE_CLICK, handleClick);
|
element.addEventListener(csTools.EVENTS.MOUSE_CLICK, handleClick);
|
||||||
element.addEventListener(csTools.EVENTS.TOUCH_START, onTouchStart);
|
element.addEventListener(csTools.EVENTS.TOUCH_START, onTouchStart);
|
||||||
|
|
||||||
// TODO: This makes scrolling painfully slow
|
// TODO: This makes scrolling painfully slow
|
||||||
// element.addEventListener(cornerstone.EVENTS.NEW_IMAGE, onNewImage);
|
// element.addEventListener(cornerstone.EVENTS.NEW_IMAGE, onNewImage);
|
||||||
}
|
}
|
||||||
@ -197,10 +140,12 @@ export default function init({ servicesManager, configuration = {} }) {
|
|||||||
csTools.EVENTS.LABELMAP_MODIFIED,
|
csTools.EVENTS.LABELMAP_MODIFIED,
|
||||||
onLabelmapModified
|
onLabelmapModified
|
||||||
);
|
);
|
||||||
//
|
|
||||||
element.removeEventListener(csTools.EVENTS.TOUCH_PRESS, onTouchPress);
|
element.removeEventListener(csTools.EVENTS.TOUCH_PRESS, onTouchPress);
|
||||||
element.removeEventListener(csTools.EVENTS.MOUSE_CLICK, handleClick);
|
element.removeEventListener(csTools.EVENTS.MOUSE_CLICK, handleClick);
|
||||||
element.removeEventListener(csTools.EVENTS.TOUCH_START, onTouchStart);
|
element.removeEventListener(csTools.EVENTS.TOUCH_START, onTouchStart);
|
||||||
|
|
||||||
|
// TODO: This makes scrolling painfully slow
|
||||||
// element.removeEventListener(cornerstone.EVENTS.NEW_IMAGE, onNewImage);
|
// element.removeEventListener(cornerstone.EVENTS.NEW_IMAGE, onNewImage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
|
||||||
};
|
|
||||||
@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
.editDescriptionDialog {
|
.editDescriptionDialog {
|
||||||
position: absolute;
|
position: relative;
|
||||||
z-index: 300;
|
z-index: 300;
|
||||||
width: 320px;
|
width: 320px;
|
||||||
transition: all 300ms linear;
|
transition: all 300ms linear;
|
||||||
|
|||||||
@ -1,24 +1,15 @@
|
|||||||
import { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import SimpleDialog from '../SimpleDialog/SimpleDialog.js';
|
import SimpleDialog from '../SimpleDialog/SimpleDialog.js';
|
||||||
|
|
||||||
import bounding from '../../lib/utils/bounding.js';
|
|
||||||
import { getDialogStyle } from './../Labelling/labellingPositionUtils.js';
|
|
||||||
|
|
||||||
import './EditDescriptionDialog.css';
|
import './EditDescriptionDialog.css';
|
||||||
|
|
||||||
export default class EditDescriptionDialog extends Component {
|
export default class EditDescriptionDialog extends Component {
|
||||||
static defaultProps = {
|
|
||||||
componentRef: React.createRef(),
|
|
||||||
componentStyle: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
description: PropTypes.string,
|
||||||
measurementData: PropTypes.object.isRequired,
|
measurementData: PropTypes.object.isRequired,
|
||||||
onCancel: PropTypes.func.isRequired,
|
onCancel: PropTypes.func.isRequired,
|
||||||
componentRef: PropTypes.object,
|
|
||||||
componentStyle: PropTypes.object,
|
|
||||||
onUpdate: PropTypes.func.isRequired,
|
onUpdate: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -28,14 +19,8 @@ export default class EditDescriptionDialog extends Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
description: props.measurementData.description || '',
|
description: props.measurementData.description || '',
|
||||||
};
|
};
|
||||||
|
|
||||||
this.mainElement = React.createRef();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount = () => {
|
|
||||||
bounding(this.mainElement);
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (this.props.description !== prevProps.description) {
|
if (this.props.description !== prevProps.description) {
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -45,16 +30,12 @@ export default class EditDescriptionDialog extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const style = getDialogStyle(this.props.componentStyle);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SimpleDialog
|
<SimpleDialog
|
||||||
headerTitle="Edit Description"
|
headerTitle="Edit Description"
|
||||||
onClose={this.onClose}
|
onClose={this.onClose}
|
||||||
onConfirm={this.onConfirm}
|
onConfirm={this.onConfirm}
|
||||||
rootClass="editDescriptionDialog"
|
rootClass="editDescriptionDialog"
|
||||||
componentRef={this.mainElement}
|
|
||||||
componentStyle={style}
|
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
value={this.state.description}
|
value={this.state.description}
|
||||||
|
|||||||
@ -1,20 +1,16 @@
|
|||||||
import { Icon, SelectTree } from '@ohif/ui';
|
import { Icon, SelectTree } from '@ohif/ui';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import cloneDeep from 'lodash.clonedeep';
|
||||||
|
|
||||||
import LabellingTransition from './LabellingTransition.js';
|
import LabellingTransition from './LabellingTransition.js';
|
||||||
import OHIFLabellingData from './OHIFLabellingData.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 {
|
export default class LabellingFlow extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
eventData: PropTypes.object.isRequired,
|
|
||||||
measurementData: PropTypes.object.isRequired,
|
measurementData: PropTypes.object.isRequired,
|
||||||
labellingDoneCallback: PropTypes.func.isRequired,
|
labellingDoneCallback: PropTypes.func.isRequired,
|
||||||
updateLabelling: PropTypes.func.isRequired,
|
updateLabelling: PropTypes.func.isRequired,
|
||||||
|
|
||||||
initialTopDistance: PropTypes.number,
|
initialTopDistance: PropTypes.number,
|
||||||
skipAddLabelButton: PropTypes.bool,
|
skipAddLabelButton: PropTypes.bool,
|
||||||
editLocation: PropTypes.bool,
|
editLocation: PropTypes.bool,
|
||||||
@ -26,11 +22,6 @@ export default class LabellingFlow extends Component {
|
|||||||
|
|
||||||
const { location, locationLabel, description } = props.measurementData;
|
const { location, locationLabel, description } = props.measurementData;
|
||||||
|
|
||||||
let style = props.componentStyle;
|
|
||||||
if (!props.skipAddLabelButton) {
|
|
||||||
style = getAddLabelButtonStyle(props.measurementData, props.eventData);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
location,
|
location,
|
||||||
locationLabel,
|
locationLabel,
|
||||||
@ -38,20 +29,17 @@ export default class LabellingFlow extends Component {
|
|||||||
skipAddLabelButton: props.skipAddLabelButton,
|
skipAddLabelButton: props.skipAddLabelButton,
|
||||||
editDescription: props.editDescription,
|
editDescription: props.editDescription,
|
||||||
editLocation: props.editLocation,
|
editLocation: props.editLocation,
|
||||||
componentStyle: style,
|
|
||||||
confirmationState: false,
|
confirmationState: false,
|
||||||
displayComponent: true,
|
displayComponent: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.mainElement = React.createRef();
|
this.mainElement = React.createRef();
|
||||||
this.descriptionInput = React.createRef();
|
this.descriptionInput = React.createRef();
|
||||||
|
|
||||||
this.initialItems = OHIFLabellingData;
|
this.initialItems = OHIFLabellingData;
|
||||||
this.currentItems = cloneDeep(this.initialItems);
|
this.currentItems = cloneDeep(this.initialItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate = () => {
|
componentDidUpdate = () => {
|
||||||
this.repositionComponent();
|
|
||||||
if (this.state.editDescription) {
|
if (this.state.editDescription) {
|
||||||
this.descriptionInput.current.focus();
|
this.descriptionInput.current.focus();
|
||||||
}
|
}
|
||||||
@ -63,36 +51,14 @@ export default class LabellingFlow extends Component {
|
|||||||
mainElementClassName += ' editDescription';
|
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 (
|
return (
|
||||||
<LabellingTransition
|
<LabellingTransition
|
||||||
displayComponent={this.state.displayComponent}
|
displayComponent={this.state.displayComponent}
|
||||||
onTransitionExit={this.props.labellingDoneCallback}
|
onTransitionExit={this.props.labellingDoneCallback}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
<div className="labellingComponent-overlay"></div>
|
|
||||||
<div
|
<div
|
||||||
className={mainElementClassName}
|
className={mainElementClassName}
|
||||||
style={style}
|
|
||||||
ref={this.mainElement}
|
ref={this.mainElement}
|
||||||
onMouseLeave={this.fadeOutAndLeave}
|
onMouseLeave={this.fadeOutAndLeave}
|
||||||
onMouseEnter={this.clearFadeOutTimer}
|
onMouseEnter={this.clearFadeOutTimer}
|
||||||
@ -130,9 +96,8 @@ export default class LabellingFlow extends Component {
|
|||||||
<SelectTree
|
<SelectTree
|
||||||
items={this.currentItems}
|
items={this.currentItems}
|
||||||
columns={1}
|
columns={1}
|
||||||
onSelected={this.selectTreeSelectCalback}
|
onSelected={this.selectTreeSelectCallback}
|
||||||
selectTreeFirstTitle="Assign Label"
|
selectTreeFirstTitle="Assign Label"
|
||||||
onComponentChange={this.repositionComponent}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -195,33 +160,17 @@ export default class LabellingFlow extends Component {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
relabel = event => {
|
relabel = event => this.setState({ editLocation: true });
|
||||||
const viewportTopPosition = this.mainElement.current.offsetParent.offsetTop;
|
|
||||||
const componentStyle = {
|
|
||||||
top: event.nativeEvent.y - viewportTopPosition - 55,
|
|
||||||
left: event.nativeEvent.x,
|
|
||||||
};
|
|
||||||
this.setState({
|
|
||||||
editLocation: true,
|
|
||||||
componentStyle,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
setDescriptionUpdateMode = () => {
|
setDescriptionUpdateMode = () => {
|
||||||
this.descriptionInput.current.focus();
|
this.descriptionInput.current.focus();
|
||||||
|
this.setState({ editDescription: true });
|
||||||
this.setState({
|
|
||||||
editDescription: true,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
descriptionCancel = () => {
|
descriptionCancel = () => {
|
||||||
const { description = '' } = cloneDeep(this.state);
|
const { description = '' } = cloneDeep(this.state);
|
||||||
this.descriptionInput.current.value = description;
|
this.descriptionInput.current.value = description;
|
||||||
|
this.setState({ editDescription: false });
|
||||||
this.setState({
|
|
||||||
editDescription: false,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
handleKeyPress = e => {
|
handleKeyPress = e => {
|
||||||
@ -240,22 +189,15 @@ export default class LabellingFlow extends Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
selectTreeSelectCalback = (event, itemSelected) => {
|
selectTreeSelectCallback = (event, itemSelected) => {
|
||||||
const location = itemSelected.value;
|
const location = itemSelected.value;
|
||||||
this.props.updateLabelling({ location });
|
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({
|
this.setState({
|
||||||
editLocation: false,
|
editLocation: false,
|
||||||
confirmationState: true,
|
confirmationState: true,
|
||||||
location: itemSelected.value,
|
location: itemSelected.value,
|
||||||
locationLabel: itemSelected.label,
|
locationLabel: itemSelected.label,
|
||||||
componentStyle,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.isTouchScreen) {
|
if (this.isTouchScreen) {
|
||||||
@ -276,18 +218,13 @@ export default class LabellingFlow extends Component {
|
|||||||
|
|
||||||
fadeOutAndLeave = () => {
|
fadeOutAndLeave = () => {
|
||||||
// Wait for 1 sec to dismiss the labelling component
|
// Wait for 1 sec to dismiss the labelling component
|
||||||
this.fadeOutTimer = setTimeout(() => {
|
this.fadeOutTimer = setTimeout(
|
||||||
this.setState({
|
() => this.setState({ displayComponent: false }),
|
||||||
displayComponent: false,
|
1000
|
||||||
});
|
);
|
||||||
}, 1000);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fadeOutAndLeaveFast = () => {
|
fadeOutAndLeaveFast = () => this.setState({ displayComponent: false });
|
||||||
this.setState({
|
|
||||||
displayComponent: false,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
clearFadeOutTimer = () => {
|
clearFadeOutTimer = () => {
|
||||||
if (!this.fadeOutTimer) {
|
if (!this.fadeOutTimer) {
|
||||||
@ -296,29 +233,4 @@ export default class LabellingFlow extends Component {
|
|||||||
|
|
||||||
clearTimeout(this.fadeOutTimer);
|
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);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
.labellingComponent {
|
||||||
position: absolute;
|
position: relative;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
transition: all 200ms linear;
|
transition: all 200ms linear;
|
||||||
|
max-height: 500px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.labellingComponent .selectedLabel,
|
.labellingComponent .selectedLabel,
|
||||||
@ -87,9 +78,7 @@
|
|||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.labellingComponent.editDescription
|
.labellingComponent.editDescription .locationDescriptionWrapper #descriptionInput {
|
||||||
.locationDescriptionWrapper
|
|
||||||
#descriptionInput {
|
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,21 +1,16 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import cloneDeep from 'lodash.clonedeep';
|
import cloneDeep from 'lodash.clonedeep';
|
||||||
|
|
||||||
import EditDescriptionDialog from './../EditDescriptionDialog/EditDescriptionDialog.js';
|
import EditDescriptionDialog from './../EditDescriptionDialog/EditDescriptionDialog.js';
|
||||||
import LabellingFlow from './LabellingFlow.js';
|
import LabellingFlow from './LabellingFlow.js';
|
||||||
|
|
||||||
import './LabellingManager.css';
|
import './LabellingManager.css';
|
||||||
|
|
||||||
export default class LabellingManager extends Component {
|
export default class LabellingManager extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
eventData: PropTypes.object.isRequired,
|
|
||||||
measurementData: PropTypes.object.isRequired,
|
measurementData: PropTypes.object.isRequired,
|
||||||
|
|
||||||
labellingDoneCallback: PropTypes.func.isRequired,
|
labellingDoneCallback: PropTypes.func.isRequired,
|
||||||
updateLabelling: PropTypes.func.isRequired,
|
updateLabelling: PropTypes.func.isRequired,
|
||||||
|
|
||||||
skipAddLabelButton: PropTypes.bool,
|
skipAddLabelButton: PropTypes.bool,
|
||||||
editLocation: PropTypes.bool,
|
editLocation: PropTypes.bool,
|
||||||
editDescription: PropTypes.bool,
|
editDescription: PropTypes.bool,
|
||||||
@ -41,7 +36,6 @@ export default class LabellingManager extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
componentStyle: getComponentPosition(props.eventData),
|
|
||||||
skipAddLabelButton: props.skipAddLabelButton,
|
skipAddLabelButton: props.skipAddLabelButton,
|
||||||
editLocation: editLocation,
|
editLocation: editLocation,
|
||||||
editDescription: props.editDescription,
|
editDescription: props.editDescription,
|
||||||
@ -75,20 +69,13 @@ export default class LabellingManager extends Component {
|
|||||||
<EditDescriptionDialog
|
<EditDescriptionDialog
|
||||||
onCancel={this.props.labellingDoneCallback}
|
onCancel={this.props.labellingDoneCallback}
|
||||||
onUpdate={this.descriptionDialogUpdate}
|
onUpdate={this.descriptionDialogUpdate}
|
||||||
componentRef={this.editDescriptionDialog}
|
|
||||||
componentStyle={this.state.componentStyle}
|
|
||||||
measurementData={measurementData}
|
measurementData={measurementData}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (editLocation || editDescription) {
|
if (editLocation || editDescription) {
|
||||||
return (
|
return <LabellingFlow {...this.props} />;
|
||||||
<LabellingFlow
|
|
||||||
{...this.props}
|
|
||||||
componentStyle={this.state.componentStyle}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -98,33 +85,19 @@ export default class LabellingManager extends Component {
|
|||||||
if (editDescription) {
|
if (editDescription) {
|
||||||
measurementData.description = undefined;
|
measurementData.description = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (editLocation) {
|
if (editLocation) {
|
||||||
measurementData.location = undefined;
|
measurementData.location = undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
responseDialogUpdate = response => {
|
responseDialogUpdate = response => {
|
||||||
this.props.updateLabelling({
|
this.props.updateLabelling({ response });
|
||||||
response,
|
|
||||||
});
|
|
||||||
this.props.labellingDoneCallback();
|
this.props.labellingDoneCallback();
|
||||||
};
|
};
|
||||||
|
|
||||||
descriptionDialogUpdate = description => {
|
descriptionDialogUpdate = description => {
|
||||||
this.props.updateLabelling({
|
this.props.updateLabelling({ description });
|
||||||
description,
|
|
||||||
});
|
|
||||||
this.props.labellingDoneCallback();
|
this.props.labellingDoneCallback();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getComponentPosition(eventData) {
|
|
||||||
const {
|
|
||||||
event: { clientX: left, clientY: top },
|
|
||||||
} = eventData;
|
|
||||||
|
|
||||||
return {
|
|
||||||
left,
|
|
||||||
top,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
@ -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;
|
|
||||||
@ -35,7 +35,10 @@ const mapDispatchToProps = (dispatch, ownProps) => {
|
|||||||
// set new language
|
// set new language
|
||||||
i18n.changeLanguage(language);
|
i18n.changeLanguage(language);
|
||||||
|
|
||||||
ownProps.hide();
|
if (ownProps.hide) {
|
||||||
|
ownProps.hide();
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
setUserPreferences({
|
setUserPreferences({
|
||||||
windowLevelData,
|
windowLevelData,
|
||||||
|
|||||||
@ -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;
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
.ToolContextMenu {
|
.ToolContextMenu {
|
||||||
position: absolute;
|
position: relative;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border: 1px solid white;
|
border: 1px solid white;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
|||||||
@ -1,9 +1,6 @@
|
|||||||
import React, { Component } from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import cornerstone from 'cornerstone-core';
|
import { commandsManager } from './../App.js';
|
||||||
import cornerstoneTools from 'cornerstone-tools';
|
|
||||||
// This whole component should live in the Measurements Extension :thinking:
|
|
||||||
import getMeasurementLocationCallback from '../appExtensions/MeasurementsPanel/getMeasurementLocationCallback';
|
|
||||||
|
|
||||||
import './ToolContextMenu.css';
|
import './ToolContextMenu.css';
|
||||||
|
|
||||||
@ -17,232 +14,119 @@ const toolTypes = [
|
|||||||
'RectangleRoi',
|
'RectangleRoi',
|
||||||
];
|
];
|
||||||
|
|
||||||
let defaultDropdownItems = [
|
const ToolContextMenu = ({
|
||||||
{
|
onSetLabel,
|
||||||
actionType: 'Delete',
|
onSetDescription,
|
||||||
action: ({ nearbyToolData, eventData }) => {
|
isTouchEvent,
|
||||||
const element = eventData.element;
|
eventData,
|
||||||
|
onClose,
|
||||||
cornerstoneTools.removeToolState(
|
onDelete,
|
||||||
element,
|
}) => {
|
||||||
nearbyToolData.toolType,
|
const defaultDropdownItems = [
|
||||||
nearbyToolData.tool
|
{
|
||||||
);
|
actionType: 'Delete',
|
||||||
cornerstone.updateImage(element);
|
action: ({ nearbyToolData, eventData }) =>
|
||||||
|
onDelete(nearbyToolData, eventData),
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
actionType: 'setLabel',
|
||||||
actionType: 'setLabel',
|
action: ({ nearbyToolData, eventData }) => {
|
||||||
action: ({ nearbyToolData, eventData }) => {
|
const { tool: measurementData } = nearbyToolData;
|
||||||
const { tool } = nearbyToolData;
|
onSetLabel(eventData, measurementData);
|
||||||
|
},
|
||||||
const options = {
|
|
||||||
skipAddLabelButton: true,
|
|
||||||
editLocation: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
getMeasurementLocationCallback(eventData, tool, options);
|
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
actionType: 'setDescription',
|
||||||
actionType: 'setDescription',
|
action: ({ nearbyToolData, eventData }) => {
|
||||||
action: ({ nearbyToolData, eventData }) => {
|
const { tool: measurementData } = nearbyToolData;
|
||||||
const { tool } = nearbyToolData;
|
onSetDescription(eventData, measurementData);
|
||||||
|
},
|
||||||
const options = {
|
|
||||||
editDescriptionOnDialog: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
getMeasurementLocationCallback(eventData, tool, options);
|
|
||||||
},
|
},
|
||||||
},
|
];
|
||||||
];
|
|
||||||
|
|
||||||
function getNearbyToolData(element, coords, toolTypes) {
|
const getDropdownItems = (eventData, isTouchEvent = false) => {
|
||||||
const nearbyTool = {};
|
const nearbyToolData = commandsManager.runCommand('getNearbyToolData', {
|
||||||
let pointNearTool = false;
|
element: eventData.element,
|
||||||
|
canvasCoordinates: eventData.currentPoints.canvas,
|
||||||
|
availableToolTypes: toolTypes,
|
||||||
|
});
|
||||||
|
|
||||||
toolTypes.forEach(toolType => {
|
// Annotate tools for touch events already have a press handle to edit it, has a better UX for deleting it
|
||||||
const toolData = cornerstoneTools.getToolState(element, toolType);
|
if (
|
||||||
if (!toolData) {
|
isTouchEvent &&
|
||||||
|
nearbyToolData &&
|
||||||
|
nearbyToolData.toolType === 'arrowAnnotate'
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
toolData.data.forEach(function(data, index) {
|
let dropdownItems = [];
|
||||||
// TODO: Fix this, it's ugly
|
if (nearbyToolData) {
|
||||||
let toolInterface = cornerstoneTools.getToolForElement(element, toolType);
|
defaultDropdownItems.forEach(item => {
|
||||||
if (!toolInterface) {
|
item.params = {
|
||||||
toolInterface = cornerstoneTools.getToolForElement(
|
eventData,
|
||||||
element,
|
nearbyToolData,
|
||||||
`${toolType}Tool`
|
};
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!toolInterface) {
|
if (item.actionType === 'Delete') {
|
||||||
throw new Error('Tool not found.');
|
item.text = 'Delete measurement';
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
if (item.actionType === 'setLabel') {
|
||||||
<li key={item.actionType}>
|
item.text = 'Relabel';
|
||||||
<button className="form-action" onClick={itemOnClick}>
|
}
|
||||||
<span key={item.actionType}>{item.text}</span>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const position = {
|
if (item.actionType === 'setDescription') {
|
||||||
top: `${eventData.currentPoints.canvas.y}px`,
|
item.text = `${
|
||||||
left: `${eventData.currentPoints.canvas.x}px`,
|
nearbyToolData.tool.description ? 'Edit' : 'Add'
|
||||||
};
|
} Description`;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
dropdownItems.push(item);
|
||||||
<div className="ToolContextMenu" style={position} ref={this.mainElement}>
|
});
|
||||||
<ul className="bounded">{dropdownComponents}</ul>
|
}
|
||||||
|
|
||||||
|
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>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
);
|
||||||
|
};
|
||||||
|
|
||||||
componentDidMount = () => {
|
ToolContextMenu.propTypes = {
|
||||||
if (this.mainElement.current) {
|
isTouchEvent: PropTypes.bool.isRequired,
|
||||||
this.updateElementPosition();
|
eventData: PropTypes.object,
|
||||||
}
|
onClose: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidUpdate = () => {
|
ToolContextMenu.defaultProps = {
|
||||||
if (this.mainElement.current) {
|
isTouchEvent: false,
|
||||||
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`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ToolContextMenu;
|
export default ToolContextMenu;
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import OHIF from '@ohif/core';
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import ConnectedHeader from './ConnectedHeader.js';
|
import ConnectedHeader from './ConnectedHeader.js';
|
||||||
import ConnectedToolbarRow from './ConnectedToolbarRow.js';
|
import ConnectedToolbarRow from './ConnectedToolbarRow.js';
|
||||||
import ConnectedLabellingOverlay from './ConnectedLabellingOverlay';
|
|
||||||
import ConnectedStudyBrowser from './ConnectedStudyBrowser.js';
|
import ConnectedStudyBrowser from './ConnectedStudyBrowser.js';
|
||||||
import ConnectedViewerMain from './ConnectedViewerMain.js';
|
import ConnectedViewerMain from './ConnectedViewerMain.js';
|
||||||
import SidePanel from './../components/SidePanel.js';
|
import SidePanel from './../components/SidePanel.js';
|
||||||
@ -317,7 +316,6 @@ class Viewer extends Component {
|
|||||||
)}
|
)}
|
||||||
</SidePanel>
|
</SidePanel>
|
||||||
</div>
|
</div>
|
||||||
<ConnectedLabellingOverlay />
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import './ViewerMain.css';
|
|||||||
|
|
||||||
import { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import { ConnectedViewportGrid } from './../components/ViewportGrid/index.js';
|
import { ConnectedViewportGrid } from './../components/ViewportGrid/index.js';
|
||||||
import ConnectedToolContextMenu from './ConnectedToolContextMenu.js';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
@ -153,7 +152,6 @@ class ViewerMain extends Component {
|
|||||||
setViewportData={this.setViewportData}
|
setViewportData={this.setViewportData}
|
||||||
>
|
>
|
||||||
{/* Children to add to each viewport that support children */}
|
{/* Children to add to each viewport that support children */}
|
||||||
<ConnectedToolContextMenu />
|
|
||||||
</ConnectedViewportGrid>
|
</ConnectedViewportGrid>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import React from 'react';
|
|||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import App from './App.js';
|
import App from './App.js';
|
||||||
|
|
||||||
function installViewer(props, containerId = 'root', callback) {
|
function installViewer(config, containerId = 'root', callback) {
|
||||||
const container = document.getElementById(containerId);
|
const container = document.getElementById(containerId);
|
||||||
|
|
||||||
if (!container) {
|
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 };
|
export { App, installViewer };
|
||||||
|
|||||||
@ -18,35 +18,36 @@ import ReactDOM from 'react-dom';
|
|||||||
* "baked in" to the published application.
|
* "baked in" to the published application.
|
||||||
*
|
*
|
||||||
* Depending on your use case/needs, you may want to consider not adding any extensions
|
* 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
|
* by default HERE, and instead provide them via the extensions configuration key or
|
||||||
* `window.config.extensions`, or by using the exported `App` component, and passing
|
* by using the exported `App` component, and passing in your extensions as props using
|
||||||
* in your extensions as props.
|
* the defaultExtensions property.
|
||||||
*/
|
*/
|
||||||
import OHIFVTKExtension from '@ohif/extension-vtk';
|
import OHIFVTKExtension from '@ohif/extension-vtk';
|
||||||
import OHIFDicomHtmlExtension from '@ohif/extension-dicom-html';
|
import OHIFDicomHtmlExtension from '@ohif/extension-dicom-html';
|
||||||
import OHIFDicomMicroscopyExtension from '@ohif/extension-dicom-microscopy';
|
import OHIFDicomMicroscopyExtension from '@ohif/extension-dicom-microscopy';
|
||||||
import OHIFDicomPDFExtension from '@ohif/extension-dicom-pdf';
|
import OHIFDicomPDFExtension from '@ohif/extension-dicom-pdf';
|
||||||
|
|
||||||
// Default Settings
|
/*
|
||||||
|
* Default Settings
|
||||||
|
*/
|
||||||
let config = {};
|
let config = {};
|
||||||
const appDefaults = {
|
|
||||||
routerBasename: '/',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (window) {
|
if (window) {
|
||||||
config = window.config || {};
|
config = window.config || {};
|
||||||
config.extensions = [
|
}
|
||||||
|
|
||||||
|
const appProps = {
|
||||||
|
config,
|
||||||
|
defaultExtensions: [
|
||||||
OHIFVTKExtension,
|
OHIFVTKExtension,
|
||||||
OHIFDicomHtmlExtension,
|
OHIFDicomHtmlExtension,
|
||||||
OHIFDicomMicroscopyExtension,
|
OHIFDicomMicroscopyExtension,
|
||||||
OHIFDicomPDFExtension,
|
OHIFDicomPDFExtension,
|
||||||
];
|
],
|
||||||
}
|
};
|
||||||
|
|
||||||
const appProps = Object.assign({}, appDefaults, config);
|
/** Create App */
|
||||||
|
|
||||||
// Create App
|
|
||||||
const app = React.createElement(App, appProps, null);
|
const app = React.createElement(App, appProps, null);
|
||||||
|
|
||||||
// Render
|
/** Render */
|
||||||
ReactDOM.render(app, document.getElementById('root'));
|
ReactDOM.render(app, document.getElementById('root'));
|
||||||
|
|||||||
@ -6,18 +6,16 @@ import {
|
|||||||
} from 'redux/es/redux.js';
|
} from 'redux/es/redux.js';
|
||||||
|
|
||||||
// import { createLogger } from 'redux-logger';
|
// import { createLogger } from 'redux-logger';
|
||||||
import layoutReducers from './layout/reducers.js';
|
|
||||||
import { reducer as oidcReducer } from 'redux-oidc';
|
import { reducer as oidcReducer } from 'redux-oidc';
|
||||||
import { redux } from '@ohif/core';
|
import { redux } from '@ohif/core';
|
||||||
import thunkMiddleware from 'redux-thunk';
|
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
|
// Set init data, using values found in localStorage
|
||||||
const { reducers, localStorage, sessionStorage } = redux;
|
const { reducers, localStorage, sessionStorage } = redux;
|
||||||
const middleware = [thunkMiddleware];
|
const middleware = [thunkMiddleware];
|
||||||
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||||
|
|
||||||
reducers.ui = layoutReducers;
|
|
||||||
reducers.oidc = oidcReducer;
|
reducers.oidc = oidcReducer;
|
||||||
|
|
||||||
const rootReducer = combineReducers(reducers);
|
const rootReducer = combineReducers(reducers);
|
||||||
|
|||||||
@ -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;
|
|
||||||
@ -12246,7 +12246,7 @@ lodash.memoize@^4.1.2:
|
|||||||
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
|
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
|
||||||
integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
|
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"
|
version "4.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
|
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
|
||||||
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
|
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user