Pull in master
This commit is contained in:
commit
e7501933dd
@ -1,112 +1,138 @@
|
||||
import ImageSet from '@ohif/core/src/classes/ImageSet';
|
||||
import { IWebApiDataSource } from '@ohif/core';
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export default {
|
||||
/**
|
||||
* Only required property. Should be a unique value across all extensions.
|
||||
*/
|
||||
id: 'example-extension',
|
||||
id: 'org.ohif.*',
|
||||
|
||||
/**
|
||||
* LIFECYCLE HOOKS
|
||||
*/
|
||||
|
||||
preRegistration({
|
||||
servicesManager = {},
|
||||
commandsManager = {},
|
||||
appConfig = {},
|
||||
configuration = {},
|
||||
}) {},
|
||||
preRegistration() {},
|
||||
beforeExtInit() {},
|
||||
beforeExtDestroy() {},
|
||||
|
||||
/**
|
||||
* MODULE GETTERS
|
||||
* MODULES
|
||||
*/
|
||||
getViewportModule() {
|
||||
return '... react component ...';
|
||||
},
|
||||
getSopClassHandlerModule() {
|
||||
return sopClassHandlerModule;
|
||||
},
|
||||
getPanelModule() {
|
||||
return panelModule;
|
||||
},
|
||||
getToolbarModule() {
|
||||
return panelModule;
|
||||
},
|
||||
getCommandsModule(/* store */) {
|
||||
return commandsModule;
|
||||
},
|
||||
getCommandsModule,
|
||||
getContextModule,
|
||||
getDataSourcesModule,
|
||||
getLayoutTemplateModule,
|
||||
getPanelModule,
|
||||
getSopClassHandlerModule,
|
||||
getToolbarModule() {},
|
||||
getViewportModule,
|
||||
};
|
||||
|
||||
// appConfig,
|
||||
// extensionConfig,
|
||||
// dataSources,
|
||||
// servicesManager,
|
||||
// extensionManager,
|
||||
// commandsManager,
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
const commandsModule = {
|
||||
actions: {
|
||||
// Store Contexts + Options
|
||||
exampleAction: ({ viewports, param1 }) => {
|
||||
console.log(`There are ${viewports.length} viewports`);
|
||||
console.log(`param1's value is: ${param1}`);
|
||||
},
|
||||
},
|
||||
const getCommandsModule = () => ({
|
||||
definitions: {
|
||||
exampleActionDef: {
|
||||
commandFn: this.actions.exampleAction,
|
||||
storeContexts: ['viewports'],
|
||||
commandFn: ({ param1 }) => {
|
||||
console.log(`param1's value is: ${param1}`);
|
||||
},
|
||||
// storeContexts: ['viewports'],
|
||||
options: { param1: 'hello world' },
|
||||
context: 'VIEWER', // optional
|
||||
},
|
||||
},
|
||||
};
|
||||
defaultContext: 'ACTIVE_VIEWPORT::DICOMSR',
|
||||
});
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
const sopClassHandlerModule = {
|
||||
id: 'OHIFDicomHtmlSopClassHandler',
|
||||
sopClassUIDs: Object.values({
|
||||
BASIC_TEXT_SR: '1.2.840.10008.5.1.4.1.1.88.11',
|
||||
ENHANCED_SR: '1.2.840.10008.5.1.4.1.1.88.22',
|
||||
COMPREHENSIVE_SR: '1.2.840.10008.5.1.4.1.1.88.33',
|
||||
PROCEDURE_LOG_STORAGE: '1.2.840.10008.5.1.4.1.1.88.40',
|
||||
MAMMOGRAPHY_CAD_SR: '1.2.840.10008.5.1.4.1.1.88.50',
|
||||
CHEST_CAD_SR: '1.2.840.10008.5.1.4.1.1.88.65',
|
||||
X_RAY_RADIATION_DOSE_SR: '1.2.840.10008.5.1.4.1.1.88.67',
|
||||
}),
|
||||
getDisplaySetFromSeries(series, study, dicomWebClient, authorizationHeaders) {
|
||||
const instance = series.getFirstInstance();
|
||||
const ExampleContext = React.createContext();
|
||||
|
||||
return {
|
||||
plugin: 'html',
|
||||
displaySetInstanceUID: 0, //utils.guid(),
|
||||
wadoRoot: study.getData().wadoRoot,
|
||||
wadoUri: instance.getData().wadouri,
|
||||
SOPInstanceUID: instance.getSOPInstanceUID(),
|
||||
SeriesInstanceUID: series.getSeriesInstanceUID(),
|
||||
StudyInstanceUID: study.getStudyInstanceUID(),
|
||||
authorizationHeaders,
|
||||
};
|
||||
function ExampleContextProvider({ children }) {
|
||||
return (
|
||||
<ExampleContext.Provider value={{ example: 'value' }}>
|
||||
{children}
|
||||
</TrackedMeasurementsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const getContextModule = () => [
|
||||
{
|
||||
name: 'ExampleContext',
|
||||
context: ExampleContext,
|
||||
provider: ExampleContextProvider,
|
||||
},
|
||||
];
|
||||
|
||||
const getDataSourcesModule = () => [
|
||||
{
|
||||
name: 'exampleDataSource',
|
||||
type: 'webApi', // 'webApi' | 'local' | 'other'
|
||||
createDataSource: dataSourceConfig => {
|
||||
return IWebApiDataSource.create(/* */);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const getLayoutTemplateModule = (/* ... */) => [
|
||||
{
|
||||
id: 'exampleLayout',
|
||||
name: 'exampleLayout',
|
||||
component: ExampleLayoutComponent,
|
||||
},
|
||||
];
|
||||
|
||||
const getPanelModule = () => {
|
||||
return [
|
||||
{
|
||||
name: 'exampleSidePanel',
|
||||
iconName: 'info-circle-o',
|
||||
iconLabel: 'Example',
|
||||
label: 'Hello World',
|
||||
isDisabled: studies => {}, // optional
|
||||
component: ExamplePanelContentComponent,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
const panelModule = {
|
||||
menuOptions: [
|
||||
const getSopClassHandlerModule = (/* ... */) => {
|
||||
const BASIC_TEXT_SR = '1.2.840.10008.5.1.4.1.1.88.11';
|
||||
|
||||
return [
|
||||
{
|
||||
icon: 'th-list',
|
||||
label: 'Segments',
|
||||
target: 'segment-panel',
|
||||
isDisabled: studies => {
|
||||
return false;
|
||||
name: 'ExampleSopClassHandle',
|
||||
sopClassUids: [BASIC_TEXT_SR],
|
||||
getDisplaySetsFromSeries: instances => {
|
||||
const imageSet = new ImageSet(instances);
|
||||
|
||||
imageSet.setAttributes(/** */);
|
||||
imageSet.sortBy((a, b) => 0);
|
||||
|
||||
return imageSet;
|
||||
},
|
||||
},
|
||||
],
|
||||
components: [
|
||||
{
|
||||
id: 'segment-panel',
|
||||
component: '... react component ...',
|
||||
},
|
||||
],
|
||||
defaultContext: ['VIEWER'],
|
||||
];
|
||||
};
|
||||
|
||||
const getToolbarModule = () => {};
|
||||
|
||||
// displaySet, viewportIndex, dataSource
|
||||
const getViewportModule = () => {
|
||||
const wrappedViewport = props => {
|
||||
return (
|
||||
<ExampleViewport
|
||||
{...props}
|
||||
onEvent={data => {
|
||||
commandsManager.runCommand('commandName', data);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return [{ name: 'example', component: wrappedViewport }];
|
||||
};
|
||||
|
||||
@ -145,4 +145,8 @@ CornerstoneViewportDownloadForm.propTypes = {
|
||||
activeViewportIndex: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export default CornerstoneViewportDownloadForm;
|
||||
// export default CornerstoneViewportDownloadForm;
|
||||
|
||||
export default function HelloWorld() {
|
||||
return <div>Hello World</div>;
|
||||
}
|
||||
|
||||
@ -148,13 +148,13 @@ const commandsModule = ({ servicesManager }) => {
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
return enabledElement;
|
||||
},
|
||||
showDownloadViewportModal: ({ title, viewports }) => {
|
||||
const activeViewportIndex = viewports.activeViewportIndex;
|
||||
showDownloadViewportModal: () => {
|
||||
const activeViewportIndex = 1; // viewports.activeViewportIndex;
|
||||
const { UIModalService } = servicesManager.services;
|
||||
if (UIModalService) {
|
||||
UIModalService.show({
|
||||
content: CornerstoneViewportDownloadForm,
|
||||
title,
|
||||
title: 'Download High Quality Image',
|
||||
contentProps: {
|
||||
activeViewportIndex,
|
||||
onClose: UIModalService.hide,
|
||||
@ -318,7 +318,7 @@ const commandsModule = ({ servicesManager }) => {
|
||||
},
|
||||
showDownloadViewportModal: {
|
||||
commandFn: actions.showDownloadViewportModal,
|
||||
storeContexts: ['viewports'],
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
getActiveViewportEnabledElement: {
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import init from './init.js';
|
||||
import commandsModule from './commandsModule.js';
|
||||
import toolbarModule from './toolbarModule.js';
|
||||
import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
|
||||
|
||||
const Component = React.lazy(() => {
|
||||
@ -48,9 +47,6 @@ export default {
|
||||
{ name: 'cornerstone', component: ExtendedOHIFCornerstoneViewport },
|
||||
];
|
||||
},
|
||||
getToolbarModule() {
|
||||
return toolbarModule;
|
||||
},
|
||||
getCommandsModule({ servicesManager }) {
|
||||
return commandsModule({ servicesManager });
|
||||
},
|
||||
|
||||
@ -1,14 +1,3 @@
|
||||
// TODO: A way to add Icons that don't already exist?
|
||||
// - Register them and add
|
||||
// - Include SVG Source/Inline?
|
||||
// - By URL, or own component?
|
||||
|
||||
// What KINDS of toolbar buttons do we have...
|
||||
// - One's that dispatch commands
|
||||
// - One's that set tool's active
|
||||
// - More custom, like CINE
|
||||
// - Built in for one's like this, or custom components?
|
||||
|
||||
// Visible?
|
||||
// Disabled?
|
||||
// Based on contexts or misc. criteria?
|
||||
@ -17,241 +6,118 @@
|
||||
// setToolActive commands should receive the button event that triggered
|
||||
// so we can do the "bind to this button" magic
|
||||
|
||||
const TOOLBAR_BUTTON_TYPES = {
|
||||
COMMAND: 'command',
|
||||
SET_TOOL_ACTIVE: 'setToolActive',
|
||||
BUILT_IN: 'builtIn',
|
||||
};
|
||||
// const definitions = [
|
||||
// // OLD
|
||||
// {
|
||||
// id: 'StackScroll',
|
||||
// label: 'Stack Scroll',
|
||||
// icon: 'bars',
|
||||
// //
|
||||
// type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||
// commandName: 'setToolActive',
|
||||
// commandOptions: { toolName: 'StackScroll' },
|
||||
// },
|
||||
// {
|
||||
// id: 'Reset',
|
||||
// label: 'Reset',
|
||||
// icon: 'reset',
|
||||
// //
|
||||
// type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
||||
// commandName: 'resetViewport',
|
||||
// },
|
||||
// {
|
||||
// id: 'Cine',
|
||||
// label: 'CINE',
|
||||
// icon: 'youtube',
|
||||
// //
|
||||
// type: TOOLBAR_BUTTON_TYPES.BUILT_IN,
|
||||
// options: {
|
||||
// behavior: TOOLBAR_BUTTON_BEHAVIORS.CINE,
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// id: 'More',
|
||||
// label: 'More',
|
||||
// icon: 'ellipse-circle',
|
||||
// buttons: [
|
||||
// {
|
||||
// id: 'Magnify',
|
||||
// label: 'Magnify',
|
||||
// icon: 'circle',
|
||||
// //
|
||||
// type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||
// commandName: 'setToolActive',
|
||||
// commandOptions: { toolName: 'Magnify' },
|
||||
// },
|
||||
// {
|
||||
// id: 'WwwcRegion',
|
||||
// label: 'ROI Window',
|
||||
// icon: 'stop',
|
||||
// //
|
||||
// type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||
// commandName: 'setToolActive',
|
||||
// commandOptions: { toolName: 'WwwcRegion' },
|
||||
// },
|
||||
// {
|
||||
// id: 'Invert',
|
||||
// label: 'Invert',
|
||||
// icon: 'adjust',
|
||||
// //
|
||||
// type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
||||
// commandName: 'invertViewport',
|
||||
// },
|
||||
// {
|
||||
// id: 'RotateRight',
|
||||
// label: 'Rotate Right',
|
||||
// icon: 'rotate-right',
|
||||
// //
|
||||
// type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
||||
// commandName: 'rotateViewportCW',
|
||||
// },
|
||||
// {
|
||||
// id: 'FlipH',
|
||||
// label: 'Flip H',
|
||||
// icon: 'ellipse-h',
|
||||
// //
|
||||
// type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
||||
// commandName: 'flipViewportHorizontal',
|
||||
// },
|
||||
// {
|
||||
// id: 'FlipV',
|
||||
// label: 'Flip V',
|
||||
// icon: 'ellipse-v',
|
||||
// //
|
||||
// type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
||||
// commandName: 'flipViewportVertical',
|
||||
// },
|
||||
// {
|
||||
// id: 'Clear',
|
||||
// label: 'Clear',
|
||||
// icon: 'trash',
|
||||
// //
|
||||
// type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
||||
// commandName: 'clearAnnotations',
|
||||
// },
|
||||
// {
|
||||
// id: 'Eraser',
|
||||
// label: 'Eraser',
|
||||
// icon: 'eraser',
|
||||
// //
|
||||
// type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||
// commandName: 'setToolActive',
|
||||
// commandOptions: { toolName: 'Eraser' },
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// id: 'Exit2DMPR',
|
||||
// label: 'Exit 2D MPR',
|
||||
// icon: 'times',
|
||||
// //
|
||||
// type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
||||
// commandName: 'setCornerstoneLayout',
|
||||
// context: 'ACTIVE_VIEWPORT::VTK',
|
||||
// },
|
||||
// ];
|
||||
|
||||
const TOOLBAR_BUTTON_BEHAVIORS = {
|
||||
CINE: 'CINE',
|
||||
DOWNLOAD_SCREEN_SHOT: 'DOWNLOAD_SCREEN_SHOT',
|
||||
};
|
||||
|
||||
/* TODO: Export enums through a extension manager. */
|
||||
const enums = {
|
||||
TOOLBAR_BUTTON_TYPES,
|
||||
TOOLBAR_BUTTON_BEHAVIORS,
|
||||
};
|
||||
|
||||
const definitions = [
|
||||
// UPDATED
|
||||
{
|
||||
id: 'Zoom',
|
||||
label: 'Zoom',
|
||||
icon: 'tool-zoom',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Zoom' },
|
||||
},
|
||||
{
|
||||
id: 'Wwwc',
|
||||
label: 'Levels',
|
||||
icon: 'tool-window-level',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Wwwc' },
|
||||
},
|
||||
{
|
||||
id: 'Pan',
|
||||
label: 'Pan',
|
||||
icon: 'tool-move',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Pan' },
|
||||
},
|
||||
{
|
||||
id: 'Capture',
|
||||
label: 'Capture',
|
||||
icon: 'tool-capture',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.BUILT_IN,
|
||||
options: {
|
||||
behavior: TOOLBAR_BUTTON_BEHAVIORS.DOWNLOAD_SCREEN_SHOT,
|
||||
togglable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Annotate',
|
||||
label: 'Annotate',
|
||||
icon: 'tool-annotate',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'ArrowAnnotate' },
|
||||
},
|
||||
{
|
||||
id: 'Bidirectional',
|
||||
label: 'Bidirectional',
|
||||
icon: 'tool-bidirectional',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Bidirectional' },
|
||||
},
|
||||
{
|
||||
id: 'Ellipse',
|
||||
label: 'Ellipse',
|
||||
icon: 'tool-elipse',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'EllipticalRoi' },
|
||||
},
|
||||
{
|
||||
id: 'Length',
|
||||
label: 'Length',
|
||||
icon: 'tool-length',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Length' },
|
||||
},
|
||||
// OLD
|
||||
{
|
||||
id: 'StackScroll',
|
||||
label: 'Stack Scroll',
|
||||
icon: 'bars',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'StackScroll' },
|
||||
},
|
||||
|
||||
{
|
||||
id: 'Angle',
|
||||
label: 'Angle',
|
||||
icon: 'angle-left',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Angle' },
|
||||
},
|
||||
{
|
||||
id: 'Reset',
|
||||
label: 'Reset',
|
||||
icon: 'reset',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
||||
commandName: 'resetViewport',
|
||||
},
|
||||
{
|
||||
id: 'Cine',
|
||||
label: 'CINE',
|
||||
icon: 'youtube',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.BUILT_IN,
|
||||
options: {
|
||||
behavior: TOOLBAR_BUTTON_BEHAVIORS.CINE,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'More',
|
||||
label: 'More',
|
||||
icon: 'ellipse-circle',
|
||||
buttons: [
|
||||
{
|
||||
id: 'Magnify',
|
||||
label: 'Magnify',
|
||||
icon: 'circle',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Magnify' },
|
||||
},
|
||||
{
|
||||
id: 'WwwcRegion',
|
||||
label: 'ROI Window',
|
||||
icon: 'stop',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'WwwcRegion' },
|
||||
},
|
||||
{
|
||||
id: 'DragProbe',
|
||||
label: 'Probe',
|
||||
icon: 'dot-circle',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'DragProbe' },
|
||||
},
|
||||
{
|
||||
id: 'RectangleRoi',
|
||||
label: 'Rectangle',
|
||||
icon: 'square-o',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'RectangleRoi' },
|
||||
},
|
||||
{
|
||||
id: 'Invert',
|
||||
label: 'Invert',
|
||||
icon: 'adjust',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
||||
commandName: 'invertViewport',
|
||||
},
|
||||
{
|
||||
id: 'RotateRight',
|
||||
label: 'Rotate Right',
|
||||
icon: 'rotate-right',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
||||
commandName: 'rotateViewportCW',
|
||||
},
|
||||
{
|
||||
id: 'FlipH',
|
||||
label: 'Flip H',
|
||||
icon: 'ellipse-h',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
||||
commandName: 'flipViewportHorizontal',
|
||||
},
|
||||
{
|
||||
id: 'FlipV',
|
||||
label: 'Flip V',
|
||||
icon: 'ellipse-v',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
||||
commandName: 'flipViewportVertical',
|
||||
},
|
||||
{
|
||||
id: 'Clear',
|
||||
label: 'Clear',
|
||||
icon: 'trash',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
||||
commandName: 'clearAnnotations',
|
||||
},
|
||||
{
|
||||
id: 'Eraser',
|
||||
label: 'Eraser',
|
||||
icon: 'eraser',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Eraser' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'Exit2DMPR',
|
||||
label: 'Exit 2D MPR',
|
||||
icon: 'times',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
||||
commandName: 'setCornerstoneLayout',
|
||||
context: 'ACTIVE_VIEWPORT::VTK',
|
||||
},
|
||||
];
|
||||
|
||||
export default {
|
||||
definitions,
|
||||
defaultContext: 'ACTIVE_VIEWPORT::CORNERSTONE',
|
||||
};
|
||||
export default [];
|
||||
|
||||
7
extensions/default/src/Toolbar/ToolbarDivider.jsx
Normal file
7
extensions/default/src/Toolbar/ToolbarDivider.jsx
Normal file
@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function ToolbarDivider() {
|
||||
return (
|
||||
<span className="self-center w-1 h-8 mx-2 border-l border-common-dark" />
|
||||
);
|
||||
}
|
||||
41
extensions/default/src/Toolbar/ToolbarLayoutSelector.jsx
Normal file
41
extensions/default/src/Toolbar/ToolbarLayoutSelector.jsx
Normal file
@ -0,0 +1,41 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { LayoutSelector as OHIFLayoutSelector, ToolbarButton } from '@ohif/ui';
|
||||
|
||||
function LayoutSelector() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
function LayoutSelector() {
|
||||
if (isOpen) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}
|
||||
window.addEventListener('click', LayoutSelector);
|
||||
return () => {
|
||||
window.removeEventListener('click', LayoutSelector);
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
const dropdownContent = isOpen ? OHIFLayoutSelector : undefined;
|
||||
|
||||
return (
|
||||
<ToolbarButton
|
||||
id="Layout"
|
||||
label="Grid Layout"
|
||||
icon="tool-layout"
|
||||
onClick={() => {
|
||||
setIsOpen(!isOpen);
|
||||
}}
|
||||
dropdownContent={dropdownContent}
|
||||
isActive={isOpen}
|
||||
type="primary"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
LayoutSelector.propTypes = {
|
||||
children: PropTypes.any.isRequired,
|
||||
};
|
||||
|
||||
export default LayoutSelector;
|
||||
@ -1,10 +1,9 @@
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
//
|
||||
import { NavBar, Svg, Icon, IconButton, Toolbar } from '@ohif/ui';
|
||||
import { NavBar, Svg, Icon, IconButton } from '@ohif/ui';
|
||||
|
||||
function Header({ tools, moreTools }) {
|
||||
const [activeTool, setActiveTool] = useState('Zoom');
|
||||
function Header({ children }) {
|
||||
// const dropdownContent = [
|
||||
// {
|
||||
// name: 'Soft tissue',
|
||||
@ -14,80 +13,16 @@ function Header({ tools, moreTools }) {
|
||||
// { name: 'Liver', value: '150 / 90' },
|
||||
// { name: 'Bone', value: '2500 / 480' },
|
||||
// { name: 'Brain', value: '80 / 40' },
|
||||
// ];
|
||||
// ]
|
||||
|
||||
// TODO -> In ToolBarManager => Consume commandName and commandOptions and create onClick?
|
||||
|
||||
/*
|
||||
const tools = [
|
||||
{
|
||||
id: 'Zoom',
|
||||
label: 'Zoom',
|
||||
icon: 'tool-zoom',
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Zoom' },
|
||||
onClick: () => setActiveTool('Zoom'),
|
||||
},
|
||||
{
|
||||
id: 'Wwwc',
|
||||
label: 'Levels',
|
||||
icon: 'tool-window-level',
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Wwwc' },
|
||||
onClick: () => setActiveTool('Wwwc'),
|
||||
dropdownContent: (
|
||||
<div>
|
||||
{dropdownContent.map((row, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex justify-between py-2 px-3 hover:bg-secondary-dark cursor-pointer"
|
||||
>
|
||||
<div>
|
||||
<span className="text-base text-white">{row.name}</span>
|
||||
<span className="text-base text-primary-light ml-3">
|
||||
{row.value}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-base text-primary-active ml-4">{i}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'Pan',
|
||||
label: 'Pan',
|
||||
icon: 'tool-move',
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Pan' },
|
||||
onClick: () => setActiveTool('Pan'),
|
||||
},
|
||||
{
|
||||
id: 'Capture',
|
||||
label: 'Capture',
|
||||
icon: 'tool-capture',
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Capture' },
|
||||
onClick: () => setActiveTool('Capture'),
|
||||
},
|
||||
{
|
||||
id: 'Layout',
|
||||
label: 'Layout',
|
||||
icon: 'tool-layout',
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Layout' },
|
||||
onClick: () => setActiveTool('Layout'),
|
||||
},
|
||||
];
|
||||
*/
|
||||
return (
|
||||
<NavBar className="justify-between border-b-4 border-black">
|
||||
<div className="flex flex-1 justify-between">
|
||||
<div className="flex justify-between flex-1">
|
||||
<div className="flex items-center">
|
||||
<div className="mr-3 inline-flex items-center">
|
||||
<div className="inline-flex items-center mr-3">
|
||||
<Icon
|
||||
name="chevron-left"
|
||||
className="text-primary-active w-8 cursor-pointer"
|
||||
className="w-8 cursor-pointer text-primary-active"
|
||||
onClick={() => alert('Navigate to previous page')}
|
||||
/>
|
||||
<a href="#" className="ml-4">
|
||||
@ -95,15 +30,9 @@ function Header({ tools, moreTools }) {
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">{children}</div>
|
||||
<div className="flex items-center">
|
||||
<Toolbar
|
||||
tools={tools}
|
||||
activeTool={activeTool}
|
||||
moreTools={moreTools}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="mr-3 text-common-light text-lg">
|
||||
<span className="mr-3 text-lg text-common-light">
|
||||
FOR INVESTIGATIONAL USE ONLY
|
||||
</span>
|
||||
<IconButton
|
||||
@ -123,8 +52,7 @@ function Header({ tools, moreTools }) {
|
||||
}
|
||||
|
||||
Header.propTypes = {
|
||||
tools: PropTypes.array.isRequired,
|
||||
moreTools: PropTypes.array.isRequired,
|
||||
children: PropTypes.any.isRequired,
|
||||
};
|
||||
|
||||
export default Header;
|
||||
|
||||
@ -0,0 +1,41 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ToolbarButton } from '@ohif/ui';
|
||||
|
||||
function NestedMenu({ children }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
function closeNestedMenu() {
|
||||
if (isOpen) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}
|
||||
window.addEventListener('click', closeNestedMenu);
|
||||
return () => {
|
||||
window.removeEventListener('click', closeNestedMenu);
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
const dropdownContent = isOpen ? children : undefined;
|
||||
|
||||
return (
|
||||
<ToolbarButton
|
||||
id="More"
|
||||
label="More"
|
||||
icon="tool-more-menu"
|
||||
onClick={() => {
|
||||
setIsOpen(!isOpen);
|
||||
}}
|
||||
dropdownContent={dropdownContent}
|
||||
isActive={isOpen}
|
||||
type="primary"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
NestedMenu.propTypes = {
|
||||
children: PropTypes.any.isRequired,
|
||||
};
|
||||
|
||||
export default NestedMenu;
|
||||
@ -1,8 +1,9 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import { SidePanel, Toolbar } from '@ohif/ui';
|
||||
//
|
||||
import { SidePanel } from '@ohif/ui';
|
||||
import Header from './Header.jsx';
|
||||
import NestedMenu from './ToolbarButtonNestedMenu.jsx';
|
||||
|
||||
function ViewerLayout({
|
||||
// From Extension Module Params
|
||||
@ -54,71 +55,23 @@ function ViewerLayout({
|
||||
};
|
||||
};
|
||||
|
||||
const handleToolBarSubscription = newToolBarLayout => {
|
||||
// Get buttons to pass to toolbars.
|
||||
console.log(commandsManager);
|
||||
|
||||
const firstTool = newToolBarLayout[0].tools[0];
|
||||
|
||||
const toolBarLayout = [];
|
||||
|
||||
newToolBarLayout.forEach(newToolBar => {
|
||||
const toolBar = { tools: [], moreTools: [] };
|
||||
|
||||
Object.keys(newToolBar).forEach(key => {
|
||||
if (newToolBar[key].length) {
|
||||
newToolBar[key].forEach(tool => {
|
||||
const commandOptions = tool.commandOptions || {};
|
||||
|
||||
toolBar[key].push({
|
||||
context: tool.context,
|
||||
icon: tool.icon,
|
||||
id: tool.id,
|
||||
label: tool.label,
|
||||
type: 'setToolActive',
|
||||
onClick: () => {
|
||||
commandsManager.runCommand(tool.commandName, commandOptions);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
toolBarLayout.push(toolBar);
|
||||
|
||||
// if (newToolBar.moreTools && newToolBar.moreTools.length) {
|
||||
// newToolBar.moreTools.forEach(tool => {
|
||||
// const commandOptions = tool.commandOptions || {};
|
||||
|
||||
// toolBar.push({
|
||||
// context: tool.context,
|
||||
// icon: tool.icon,
|
||||
// id: tool.id,
|
||||
// label: tool.label,
|
||||
// type: 'setToolActive',
|
||||
// command: () =>
|
||||
// commandsManager.runCommand(tool.commandName, commandOptions),
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
});
|
||||
|
||||
setToolBarLayout(toolBarLayout);
|
||||
};
|
||||
|
||||
const [toolBarLayout, setToolBarLayout] = useState([
|
||||
{ tools: [], moreTools: [] },
|
||||
{ tools: [] },
|
||||
]);
|
||||
const [toolbars, setToolbars] = useState({ primary: [], secondary: [] });
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = ToolBarService.subscribe(
|
||||
ToolBarService.EVENTS.TOOL_BAR_MODIFIED,
|
||||
handleToolBarSubscription
|
||||
() => {
|
||||
console.warn('~~~ TOOL BAR MODIFIED EVENT CAUGHT');
|
||||
const updatedToolbars = {
|
||||
primary: ToolBarService.getButtonSection('primary'),
|
||||
secondary: ToolBarService.getButtonSection('secondary'),
|
||||
};
|
||||
setToolbars(updatedToolbars);
|
||||
}
|
||||
);
|
||||
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
}, [ToolBarService]);
|
||||
|
||||
const leftPanelComponents = leftPanels.map(getPanelData);
|
||||
const rightPanelComponents = rightPanels.map(getPanelData);
|
||||
@ -126,10 +79,30 @@ function ViewerLayout({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header
|
||||
tools={toolBarLayout[0].tools}
|
||||
moreTools={toolBarLayout[0].moreTools}
|
||||
/>
|
||||
<Header>
|
||||
<div className="relative flex justify-center">
|
||||
{toolbars.primary.map(toolDef => {
|
||||
const isNested = Array.isArray(toolDef);
|
||||
|
||||
if (!isNested) {
|
||||
const { id, Component, componentProps } = toolDef;
|
||||
|
||||
return <Component key={id} id={id} {...componentProps} />;
|
||||
} else {
|
||||
return (
|
||||
<NestedMenu>
|
||||
<div className="flex">
|
||||
{toolDef.map(x => {
|
||||
const { id, Component, componentProps } = x;
|
||||
return <Component key={id} id={id} {...componentProps} />;
|
||||
})}
|
||||
</div>
|
||||
</NestedMenu>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</Header>
|
||||
<div
|
||||
className="flex flex-row flex-no-wrap items-stretch w-full overflow-hidden"
|
||||
style={{ height: 'calc(100vh - 57px' }}
|
||||
@ -145,67 +118,17 @@ function ViewerLayout({
|
||||
{/* TOOLBAR + GRID */}
|
||||
<div className="flex flex-col flex-1 h-full">
|
||||
<div className="flex h-12 border-b border-transparent flex-2 w-100">
|
||||
<Toolbar type="secondary" tools={toolBarLayout[1].tools} />
|
||||
{toolbars.secondary.map(toolDef => {
|
||||
const { id, Component, componentProps } = toolDef;
|
||||
|
||||
return <Component key={id} id={id} {...componentProps} />;
|
||||
})}
|
||||
</div>
|
||||
<div className="flex items-center justify-center flex-1 h-full pt-1 pb-2 overflow-hidden bg-black">
|
||||
<ViewportGridComp
|
||||
servicesManager={servicesManager}
|
||||
viewportComponents={viewportComponents}
|
||||
/>
|
||||
{/*
|
||||
viewportContents={[
|
||||
<Viewport
|
||||
viewportIndex={0}
|
||||
onSeriesChange={direction => alert(`Series ${direction}`)}
|
||||
studyData={{
|
||||
label: 'A',
|
||||
isTracked: true,
|
||||
isLocked: false,
|
||||
studyDate: '07-Sep-2011',
|
||||
currentSeries: 1,
|
||||
seriesDescription:
|
||||
'Series description lorem ipsum dolor sit Series description lorem ipsum dolor sit Series description lorem ipsum dolor sit ',
|
||||
modality: 'CT',
|
||||
patientInformation: {
|
||||
patientName: 'Smith, Jane',
|
||||
patientSex: 'F',
|
||||
patientAge: '59',
|
||||
MRN: '10000001',
|
||||
thickness: '5.0mm',
|
||||
spacing: '1.25mm',
|
||||
scanner: 'Aquilion',
|
||||
},
|
||||
}}
|
||||
>
|
||||
</Viewport>,
|
||||
<Viewport
|
||||
viewportIndex={1}
|
||||
onSeriesChange={direction => alert(`Series ${direction}`)}
|
||||
studyData={{
|
||||
label: 'A',
|
||||
isTracked: false,
|
||||
isLocked: true,
|
||||
studyDate: '07-Sep-2010',
|
||||
currentSeries: 2,
|
||||
seriesDescription:
|
||||
'Series description lorem ipsum dolor sit Series description lorem ipsum dolor sit Series description lorem ipsum dolor sit ',
|
||||
modality: 'SR',
|
||||
patientInformation: {
|
||||
patientName: 'Smith, Jane',
|
||||
patientSex: 'F',
|
||||
patientAge: '59',
|
||||
MRN: '10000001',
|
||||
thickness: '2.0mm',
|
||||
spacing: '1.25mm',
|
||||
scanner: 'Aquilion',
|
||||
},
|
||||
}}
|
||||
>
|
||||
</Viewport>,
|
||||
]}
|
||||
setActiveViewportIndex={setActiveViewportIndex}
|
||||
activeViewportIndex={activeViewportIndex}
|
||||
/>*/}
|
||||
</div>
|
||||
</div>
|
||||
{rightPanelComponents.length && (
|
||||
@ -227,14 +150,6 @@ ViewerLayout.propTypes = {
|
||||
}).isRequired,
|
||||
commandsManager: PropTypes.object,
|
||||
// From modes
|
||||
// TODO: Not in love with this shape,
|
||||
toolBarLayout: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
tools: PropTypes.array,
|
||||
moreTools: PropTypes.array,
|
||||
})
|
||||
).isRequired,
|
||||
//displaySetInstanceUids: PropTypes.any.isRequired,
|
||||
leftPanels: PropTypes.array,
|
||||
rightPanels: PropTypes.array,
|
||||
/** Responsible for rendering our grid of viewports; provided by consuming application */
|
||||
|
||||
@ -1,68 +0,0 @@
|
||||
// SEE:
|
||||
// https://github.com/OHIF/Viewers/blob/b58aa4575ab72fe3f493cc5a4261b4f8256516ab/platform/viewer/src/appExtensions/MeasurementsPanel/index.js#L18-L49
|
||||
import React from 'react';
|
||||
import { useViewportGrid } from '@ohif/ui';
|
||||
|
||||
function getCommandsModule({ servicesManager }) {
|
||||
const { UIDialogService } = servicesManager.services;
|
||||
|
||||
const definitions = {
|
||||
toggleLayoutSelectionDialog: {
|
||||
commandFn: () => {
|
||||
if (!UIDialogService) {
|
||||
window.alert(
|
||||
'Unable to show dialog; no UI Dialog Service available.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: use SimpleDialog component
|
||||
// TODO: update position on window resize
|
||||
// TODO: Expand service API to check if dialog w/ ID is already open
|
||||
// TODO: Import and call `useViewportGrid`
|
||||
UIDialogService.dismiss({ id: 'layoutSelection' });
|
||||
UIDialogService.create({
|
||||
id: 'layoutSelection',
|
||||
centralize: true,
|
||||
isDraggable: false,
|
||||
showOverlay: true,
|
||||
content: Test,
|
||||
});
|
||||
},
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
context: 'VIEWER',
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
definitions,
|
||||
defaultContext: 'VIEWER',
|
||||
};
|
||||
}
|
||||
|
||||
function Test() {
|
||||
const [
|
||||
{ numCols, numRows, activeViewportIndex, viewports },
|
||||
dispatch,
|
||||
] = useViewportGrid();
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => {
|
||||
dispatch({
|
||||
type: 'SET_LAYOUT',
|
||||
payload: {
|
||||
numCols: 2,
|
||||
numRows: 2,
|
||||
},
|
||||
});
|
||||
}}
|
||||
style={{ color: 'white' }}
|
||||
>
|
||||
Hello World!
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default getCommandsModule;
|
||||
@ -1,19 +1,98 @@
|
||||
const TOOLBAR_BUTTON_TYPES = {
|
||||
COMMAND: 'command',
|
||||
SET_TOOL_ACTIVE: 'setToolActive',
|
||||
BUILT_IN: 'builtIn',
|
||||
};
|
||||
import { ToolbarButton } from '@ohif/ui';
|
||||
import ToolbarDivider from './Toolbar/ToolbarDivider.jsx';
|
||||
import ToolbarLayoutSelector from './Toolbar/ToolbarLayoutSelector.jsx';
|
||||
|
||||
const definitions = [
|
||||
{
|
||||
id: 'Layout',
|
||||
label: 'Layout',
|
||||
icon: 'tool-layout',
|
||||
commandName: 'toggleLayoutSelectionDialog',
|
||||
commandOptions: { },
|
||||
},
|
||||
];
|
||||
export default function getToolbarModule({ commandsManager, servicesManager }) {
|
||||
const toolbarService = servicesManager.services.ToolBarService;
|
||||
|
||||
export default function getToolbarModule() {
|
||||
return { definitions, defaultContext: 'ACTIVE_VIEWPORT::CORNERSTONE' };
|
||||
return [
|
||||
{
|
||||
name: 'ohif.divider',
|
||||
defaultComponent: ToolbarDivider,
|
||||
clickHandler: () => {},
|
||||
},
|
||||
{
|
||||
name: 'ohif.action',
|
||||
defaultComponent: ToolbarButton,
|
||||
requiredConfig: [],
|
||||
optionalConfig: [],
|
||||
requiredProps: [],
|
||||
optionalProps: [],
|
||||
clickHandler: (evt, btn, btnSectionName) => {
|
||||
const { props } = btn;
|
||||
commandsManager.runCommand(props.commandName, props.commandOptions);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ohif.radioGroup',
|
||||
defaultComponent: ToolbarButton,
|
||||
requiredConfig: ['groupName'],
|
||||
optionalConfig: [],
|
||||
requiredProps: [],
|
||||
optionalProps: [],
|
||||
clickHandler: (evt, clickedBtn, btnSectionName) => {
|
||||
const { props } = clickedBtn;
|
||||
const allButtons = toolbarService.getButtons();
|
||||
|
||||
// Set all buttons in same group to inactive
|
||||
Object.keys(allButtons).forEach(btnName => {
|
||||
const btn = allButtons[btnName];
|
||||
const isRadioGroupBtn =
|
||||
btn.config &&
|
||||
btn.config.groupName &&
|
||||
btn.type === 'ohif.radioGroup';
|
||||
|
||||
if (
|
||||
isRadioGroupBtn &&
|
||||
clickedBtn.config.groupName === btn.config.groupName
|
||||
) {
|
||||
btn.props.isActive = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Set our clicked button to active
|
||||
allButtons[clickedBtn.id].props.isActive = true;
|
||||
|
||||
// Run button logic/command
|
||||
commandsManager.runCommand(props.commandName, props.commandOptions);
|
||||
|
||||
// Set buttons & trigger notification
|
||||
toolbarService.setButtons(allButtons);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ohif.layoutSelector',
|
||||
defaultComponent: ToolbarLayoutSelector,
|
||||
requiredConfig: [],
|
||||
optionalConfig: [],
|
||||
requiredProps: [],
|
||||
optionalProps: [],
|
||||
clickHandler: (evt, clickedBtn, btnSectionName) => {},
|
||||
},
|
||||
{
|
||||
name: 'ohif.toggle',
|
||||
defaultComponent: ToolbarButton,
|
||||
requiredConfig: [],
|
||||
optionalConfig: [],
|
||||
requiredProps: [],
|
||||
optionalProps: [],
|
||||
clickHandler: (evt, clickedBtn, btnSectionName) => {
|
||||
const { props } = clickedBtn;
|
||||
const allButtons = toolbarService.getButtons();
|
||||
const thisButton = allButtons[clickedBtn.id];
|
||||
|
||||
// Set our clicked button to active
|
||||
thisButton.props.isActive = !thisButton.props.isActive;
|
||||
|
||||
// Run button logic/command
|
||||
// MAKE SURE THIS SUPPORTS TOGGLE!
|
||||
// commandsManager.runCommand(props.commandName, props.commandOptions);
|
||||
// What if just toggled "content"?
|
||||
// commandName OR content?
|
||||
|
||||
// Set buttons & trigger notification
|
||||
toolbarService.setButtons(allButtons);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import getCommandsModule from './getCommandsModule.js';
|
||||
import getContextModule from './getContextModule.js';
|
||||
import getDataSourcesModule from './getDataSourcesModule.js';
|
||||
import getLayoutTemplateModule from './getLayoutTemplateModule.js';
|
||||
@ -12,7 +11,6 @@ export default {
|
||||
* Only required property. Should be a unique value across all extensions.
|
||||
*/
|
||||
id,
|
||||
getCommandsModule,
|
||||
getContextModule,
|
||||
getDataSourcesModule,
|
||||
getLayoutTemplateModule,
|
||||
|
||||
@ -1,68 +0,0 @@
|
||||
// SEE:
|
||||
// https://github.com/OHIF/Viewers/blob/b58aa4575ab72fe3f493cc5a4261b4f8256516ab/platform/viewer/src/appExtensions/MeasurementsPanel/index.js#L18-L49
|
||||
import React from 'react';
|
||||
import { useViewportGrid } from '@ohif/ui';
|
||||
|
||||
function getCommandsModule({ servicesManager }) {
|
||||
const { UIDialogService } = servicesManager.services;
|
||||
|
||||
const definitions = {
|
||||
toggleLayoutSelectionDialog: {
|
||||
commandFn: () => {
|
||||
if (!UIDialogService) {
|
||||
window.alert(
|
||||
'Unable to show dialog; no UI Dialog Service available.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: use SimpleDialog component
|
||||
// TODO: update position on window resize
|
||||
// TODO: Expand service API to check if dialog w/ ID is already open
|
||||
// TODO: Import and call `useViewportGrid`
|
||||
UIDialogService.dismiss({ id: 'layoutSelection' });
|
||||
UIDialogService.create({
|
||||
id: 'layoutSelection',
|
||||
centralize: true,
|
||||
isDraggable: false,
|
||||
showOverlay: true,
|
||||
content: Test,
|
||||
});
|
||||
},
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
context: 'VIEWER',
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
definitions,
|
||||
defaultContext: 'VIEWER',
|
||||
};
|
||||
}
|
||||
|
||||
function Test() {
|
||||
const [
|
||||
{ numCols, numRows, activeViewportIndex, viewports },
|
||||
dispatch,
|
||||
] = useViewportGrid();
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => {
|
||||
dispatch({
|
||||
type: 'SET_LAYOUT',
|
||||
payload: {
|
||||
numCols: 2,
|
||||
numRows: 2,
|
||||
},
|
||||
});
|
||||
}}
|
||||
style={{ color: 'white' }}
|
||||
>
|
||||
Hello World!
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default getCommandsModule;
|
||||
@ -1,4 +1,3 @@
|
||||
import getCommandsModule from './getCommandsModule.js';
|
||||
import getContextModule from './getContextModule.js';
|
||||
import getPanelModule from './getPanelModule.js';
|
||||
import getViewportModule from './getViewportModule.js';
|
||||
@ -8,7 +7,6 @@ export default {
|
||||
* Only required property. Should be a unique value across all extensions.
|
||||
*/
|
||||
id: 'org.ohif.measurement-tracking',
|
||||
getCommandsModule,
|
||||
getContextModule,
|
||||
getPanelModule,
|
||||
getViewportModule,
|
||||
|
||||
@ -128,7 +128,9 @@ const definitions = [
|
||||
},
|
||||
];
|
||||
|
||||
export default {
|
||||
definitions,
|
||||
defaultContext: 'ACTIVE_VIEWPORT::VTK',
|
||||
};
|
||||
export default [];
|
||||
|
||||
// export default {
|
||||
// definitions,
|
||||
// defaultContext: 'ACTIVE_VIEWPORT::VTK',
|
||||
// };
|
||||
|
||||
@ -55,17 +55,17 @@ export default function mode({ modeConfiguration }) {
|
||||
]);
|
||||
|
||||
// Could import layout selector here from org.ohif.default (when it exists!)
|
||||
ToolBarService.setToolBarLayout([
|
||||
// Primary
|
||||
{
|
||||
tools: ['Zoom', 'Levels', 'Pan', 'Capture', 'Layout'],
|
||||
moreTools: ['Zoom'],
|
||||
},
|
||||
// Secondary
|
||||
{
|
||||
tools: ['Annotate', 'Bidirectional', 'Ellipse', 'Length'],
|
||||
},
|
||||
]);
|
||||
// ToolBarService.setToolBarLayout([
|
||||
// // Primary
|
||||
// {
|
||||
// tools: ['Zoom', 'Levels', 'Pan', 'Capture', 'Layout'],
|
||||
// moreTools: ['Zoom'],
|
||||
// },
|
||||
// // Secondary
|
||||
// {
|
||||
// tools: ['Annotate', 'Bidirectional', 'Ellipse', 'Length'],
|
||||
// },
|
||||
// ]);
|
||||
},
|
||||
layoutTemplate: ({ routeProps }) => {
|
||||
return {
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import toolbarButtons from './toolbarButtons.js';
|
||||
|
||||
const ohif = {
|
||||
layout: 'org.ohif.default.layoutTemplateModule.viewerLayout',
|
||||
sopClassHandler: 'org.ohif.default.sopClassHandlerModule.stack',
|
||||
@ -27,57 +29,24 @@ export default function mode({ modeConfiguration }) {
|
||||
init: ({ servicesManager, extensionManager }) => {
|
||||
const { ToolBarService } = servicesManager.services;
|
||||
ToolBarService.init(extensionManager);
|
||||
ToolBarService.addButtons([
|
||||
{
|
||||
id: 'Zoom',
|
||||
namespace: 'org.ohif.cornerstone.toolbarModule.Zoom',
|
||||
},
|
||||
{
|
||||
id: 'Levels',
|
||||
namespace: 'org.ohif.cornerstone.toolbarModule.Wwwc',
|
||||
},
|
||||
{
|
||||
id: 'Pan',
|
||||
namespace: 'org.ohif.cornerstone.toolbarModule.Pan',
|
||||
},
|
||||
{
|
||||
id: 'Capture',
|
||||
namespace: 'org.ohif.cornerstone.toolbarModule.Capture',
|
||||
},
|
||||
{
|
||||
id: 'Layout',
|
||||
namespace: 'org.ohif.default.toolbarModule.Layout',
|
||||
},
|
||||
{
|
||||
id: 'Annotate',
|
||||
namespace: 'org.ohif.cornerstone.toolbarModule.Annotate',
|
||||
},
|
||||
{
|
||||
id: 'Bidirectional',
|
||||
namespace: 'org.ohif.cornerstone.toolbarModule.Bidirectional',
|
||||
},
|
||||
{
|
||||
id: 'Ellipse',
|
||||
namespace: 'org.ohif.cornerstone.toolbarModule.Ellipse',
|
||||
},
|
||||
{
|
||||
id: 'Length',
|
||||
namespace: 'org.ohif.cornerstone.toolbarModule.Length',
|
||||
},
|
||||
ToolBarService.addButtons(toolbarButtons);
|
||||
ToolBarService.createButtonSection('primary', [
|
||||
'Zoom',
|
||||
'Wwwc',
|
||||
'Pan',
|
||||
'Capture',
|
||||
'Layout',
|
||||
'Divider',
|
||||
['Zoom', 'Wwwc'],
|
||||
]);
|
||||
ToolBarService.createButtonSection('secondary', [
|
||||
'Annotate',
|
||||
'Bidirectional',
|
||||
'Ellipse',
|
||||
'Length',
|
||||
]);
|
||||
|
||||
// Could import layout selector here from org.ohif.default (when it exists!)
|
||||
ToolBarService.setToolBarLayout([
|
||||
// Primary
|
||||
{
|
||||
tools: ['Zoom', 'Levels', 'Pan', 'Capture', 'Layout'],
|
||||
moreTools: ['Zoom'],
|
||||
},
|
||||
// Secondary
|
||||
{
|
||||
tools: ['Annotate', 'Bidirectional', 'Ellipse', 'Length'],
|
||||
},
|
||||
]);
|
||||
},
|
||||
layoutTemplate: ({ routeProps }) => {
|
||||
return {
|
||||
|
||||
132
modes/longitudinal/src/toolbarButtons.js
Normal file
132
modes/longitudinal/src/toolbarButtons.js
Normal file
@ -0,0 +1,132 @@
|
||||
// TODO: torn, can either bake this here; or have to create a whole new button type
|
||||
// Only ways that you can pass in a custom React component for render :l
|
||||
|
||||
export default [
|
||||
// Divider
|
||||
{
|
||||
id: 'Divider',
|
||||
type: 'ohif.divider',
|
||||
},
|
||||
// ~~ Primary
|
||||
{
|
||||
id: 'Zoom',
|
||||
type: 'ohif.radioGroup',
|
||||
config: {
|
||||
groupName: 'primaryTool',
|
||||
},
|
||||
props: {
|
||||
isActive: false,
|
||||
icon: 'tool-zoom',
|
||||
label: 'Zoom',
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Zoom' },
|
||||
type: 'primary',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Wwwc',
|
||||
type: 'ohif.radioGroup',
|
||||
config: {
|
||||
groupName: 'primaryTool',
|
||||
},
|
||||
props: {
|
||||
isActive: true,
|
||||
icon: 'tool-window-level',
|
||||
label: 'Levels',
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Wwwc' },
|
||||
type: 'primary',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Pan',
|
||||
type: 'ohif.radioGroup',
|
||||
config: {
|
||||
groupName: 'primaryTool',
|
||||
},
|
||||
props: {
|
||||
isActive: false,
|
||||
icon: 'tool-move',
|
||||
label: 'Pan',
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Pan' },
|
||||
type: 'primary',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Capture',
|
||||
type: 'ohif.action',
|
||||
props: {
|
||||
icon: 'tool-capture',
|
||||
label: 'Capture',
|
||||
commandName: 'showDownloadViewportModal',
|
||||
type: 'primary',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Layout',
|
||||
type: 'ohif.layoutSelector',
|
||||
},
|
||||
// ~~ Primary: NESTED
|
||||
// ~~ Secondary
|
||||
{
|
||||
id: 'Annotate',
|
||||
type: 'ohif.radioGroup',
|
||||
config: {
|
||||
groupName: 'primaryTool',
|
||||
},
|
||||
props: {
|
||||
isActive: false,
|
||||
icon: 'tool-annotate',
|
||||
label: 'Annotate',
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'ArrowAnnotate' },
|
||||
type: 'secondary',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Bidirectional',
|
||||
type: 'ohif.radioGroup',
|
||||
config: {
|
||||
groupName: 'primaryTool',
|
||||
},
|
||||
props: {
|
||||
isActive: false,
|
||||
icon: 'tool-bidirectional',
|
||||
label: 'Bidirectional',
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Bidirectional' },
|
||||
type: 'secondary',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Ellipse',
|
||||
type: 'ohif.radioGroup',
|
||||
config: {
|
||||
groupName: 'primaryTool',
|
||||
},
|
||||
props: {
|
||||
isActive: false,
|
||||
icon: 'tool-elipse',
|
||||
label: 'Ellipse',
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'EllipticalRoi' },
|
||||
type: 'secondary',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Length',
|
||||
type: 'ohif.radioGroup',
|
||||
config: {
|
||||
groupName: 'primaryTool',
|
||||
},
|
||||
props: {
|
||||
isActive: false,
|
||||
icon: 'tool-length',
|
||||
label: 'Length',
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Length' },
|
||||
type: 'secondary',
|
||||
},
|
||||
},
|
||||
];
|
||||
@ -105,9 +105,6 @@ export default class ExtensionManager {
|
||||
);
|
||||
break;
|
||||
case MODULE_TYPES.TOOLBAR:
|
||||
this._initToolBarModule(extensionModule, extensionId);
|
||||
break;
|
||||
|
||||
case MODULE_TYPES.VIEWPORT:
|
||||
case MODULE_TYPES.PANEL:
|
||||
case MODULE_TYPES.SOP_CLASS_HANDLER:
|
||||
@ -115,7 +112,6 @@ export default class ExtensionManager {
|
||||
case MODULE_TYPES.LAYOUT_TEMPLATE:
|
||||
// Default for most extension points,
|
||||
// Just adds each entry ready for consumption by mode.
|
||||
|
||||
extensionModule.forEach(element => {
|
||||
this.modulesMap[
|
||||
`${extensionId}.${moduleType}.${element.name}`
|
||||
@ -148,27 +144,6 @@ export default class ExtensionManager {
|
||||
return this.dataSourceMap[dataSourceName];
|
||||
};
|
||||
|
||||
_initToolBarModule = (extensionModule, extensionId) => {
|
||||
let { definitions, defaultContext } = extensionModule;
|
||||
if (!definitions || Object.keys(definitions).length === 0) {
|
||||
log.warn('Commands Module contains no command definitions');
|
||||
return;
|
||||
}
|
||||
|
||||
defaultContext = defaultContext || 'VIEWER';
|
||||
|
||||
definitions.forEach(definition => {
|
||||
console.log(`${extensionId}.${MODULE_TYPES.TOOLBAR}.${definition.id}`);
|
||||
|
||||
// TODO -> Deep copy instead of mutation? We only do this once, but would be better.
|
||||
definition.context = definition.context || defaultContext;
|
||||
|
||||
this.modulesMap[
|
||||
`${extensionId}.${MODULE_TYPES.TOOLBAR}.${definition.id}`
|
||||
] = definition;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {string} moduleType
|
||||
@ -185,13 +160,13 @@ export default class ExtensionManager {
|
||||
|
||||
try {
|
||||
const extensionModule = getModuleFn({
|
||||
getDataSources: this.getDataSources, // Why pass this in if we're passing in `extensionManager`?
|
||||
servicesManager: this._servicesManager,
|
||||
commandsManager: this._commandsManager,
|
||||
appConfig: this._appConfig,
|
||||
getDataSources: this.getDataSources, // Why pass this in if we're passing in `extensionManager`?
|
||||
commandsManager: this._commandsManager,
|
||||
extensionManager: this,
|
||||
servicesManager: this._servicesManager,
|
||||
configuration,
|
||||
api: this._api,
|
||||
extensionManager: this,
|
||||
});
|
||||
|
||||
if (!extensionModule) {
|
||||
|
||||
@ -6,32 +6,116 @@ const EVENTS = {
|
||||
|
||||
export default class ToolBarService {
|
||||
constructor() {
|
||||
this.displaySets = {};
|
||||
this.EVENTS = EVENTS;
|
||||
this.listeners = {};
|
||||
this.buttons = {};
|
||||
this.buttonSections = {
|
||||
/**
|
||||
* primary: ['Zoom', 'Wwwc'],
|
||||
* secondary: ['Length', 'RectangleRoi']
|
||||
*/
|
||||
};
|
||||
|
||||
Object.assign(this, pubSubServiceInterface);
|
||||
}
|
||||
|
||||
init(extensionManager) {
|
||||
this.buttons = {};
|
||||
this.extensionManager = extensionManager;
|
||||
}
|
||||
|
||||
addButtons(buttons) {
|
||||
buttons.forEach(button => {
|
||||
const buttonDefinition = this.extensionManager.getModuleEntry(
|
||||
button.namespace
|
||||
getButtons() {
|
||||
return this.buttons;
|
||||
}
|
||||
|
||||
setButtons(buttons) {
|
||||
this.buttons = buttons;
|
||||
this._broadcastChange(this.EVENTS.TOOL_BAR_MODIFIED, {});
|
||||
}
|
||||
|
||||
_buttonTypes() {
|
||||
console.log(this.extensionManager.modules);
|
||||
const buttonTypes = {};
|
||||
const registeredToolbarModules = this.extensionManager.modules[
|
||||
'toolbarModule'
|
||||
];
|
||||
|
||||
if (
|
||||
Array.isArray(registeredToolbarModules) &&
|
||||
registeredToolbarModules.length
|
||||
) {
|
||||
registeredToolbarModules.forEach(toolbarModule =>
|
||||
toolbarModule.module.forEach(def => {
|
||||
buttonTypes[def.name] = def;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const id = button.id || buttonDefinition.id;
|
||||
return buttonTypes;
|
||||
}
|
||||
|
||||
this.buttons[id] = buttonDefinition;
|
||||
|
||||
createButtonSection(key, buttons) {
|
||||
// Maybe do this mapping at time of return, instead of time of create
|
||||
// Props check important for validation here...
|
||||
|
||||
this.buttonSections[key] = buttons;
|
||||
this._broadcastChange(this.EVENTS.TOOL_BAR_MODIFIED, {});
|
||||
}
|
||||
|
||||
getButtonSection(key) {
|
||||
const buttonSectionIds = this.buttonSections[key];
|
||||
const buttonsInSection = [];
|
||||
|
||||
if (!buttonSectionIds) {
|
||||
return buttonsInSection;
|
||||
}
|
||||
|
||||
buttonSectionIds.forEach(btnIdOrArray => {
|
||||
const isNested = Array.isArray(btnIdOrArray);
|
||||
|
||||
if (isNested) {
|
||||
const btnIds = btnIdOrArray;
|
||||
const nestedButtons = [];
|
||||
|
||||
btnIds.forEach(nestedBtnId => {
|
||||
const nestedBtn = this.buttons[nestedBtnId];
|
||||
const mappedNestedBtn = this._mapButtonToDisplay(nestedBtn, key);
|
||||
|
||||
nestedButtons.push(mappedNestedBtn);
|
||||
});
|
||||
|
||||
if (nestedButtons.length) {
|
||||
buttonsInSection.push(nestedButtons);
|
||||
}
|
||||
} else {
|
||||
const btnId = btnIdOrArray;
|
||||
const btn = this.buttons[btnId];
|
||||
const mappedBtn = this._mapButtonToDisplay(btn, key);
|
||||
|
||||
buttonsInSection.push(mappedBtn);
|
||||
}
|
||||
});
|
||||
|
||||
return buttonsInSection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts displaySetService changes.
|
||||
*
|
||||
* @param {object[]} buttons
|
||||
* @param {string} buttons[].id
|
||||
*/
|
||||
addButtons(buttons) {
|
||||
buttons.forEach(button => {
|
||||
if (!this.buttons[button.id]) {
|
||||
this.buttons[button.id] = button;
|
||||
}
|
||||
});
|
||||
|
||||
this._broadcastChange(this.EVENTS.TOOL_BAR_MODIFIED, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts toolbarService changes.
|
||||
*
|
||||
* @param {string} eventName The event name
|
||||
* @return void
|
||||
@ -47,33 +131,35 @@ export default class ToolBarService {
|
||||
}
|
||||
};
|
||||
|
||||
setToolBarLayout(layouts) {
|
||||
const toolBarLayout = [];
|
||||
/**
|
||||
*
|
||||
* @param {*} btn
|
||||
* @param {*} btnSection
|
||||
*/
|
||||
_mapButtonToDisplay(btn, btnSection) {
|
||||
const { id, type, component, props } = btn;
|
||||
const buttonType = this._buttonTypes()[type];
|
||||
|
||||
layouts.forEach(layout => {
|
||||
const toolBarDefinitions = { tools: [], moreTools: [] };
|
||||
if (!buttonType) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { tools, moreTools } = layout;
|
||||
const onClick = evt => {
|
||||
if (buttonType.clickHandler) {
|
||||
buttonType.clickHandler(evt, btn, btnSection);
|
||||
}
|
||||
if (btn.props.onClick) {
|
||||
btn.onClick(evt, btn, btnSection);
|
||||
}
|
||||
if (btn.props.clickHandler) {
|
||||
btn.clickHandler(evt, btn, btnSection);
|
||||
}
|
||||
};
|
||||
|
||||
tools &&
|
||||
tools.forEach(element => {
|
||||
const button = this.buttons[element];
|
||||
|
||||
toolBarDefinitions.tools.push(button);
|
||||
});
|
||||
|
||||
moreTools &&
|
||||
moreTools.forEach(element => {
|
||||
const button = this.buttons[element];
|
||||
|
||||
toolBarDefinitions.moreTools.push(button);
|
||||
});
|
||||
|
||||
toolBarLayout.push(toolBarDefinitions);
|
||||
});
|
||||
|
||||
this.toolBarLayout = toolBarLayout;
|
||||
|
||||
this._broadcastChange(this.EVENTS.TOOL_BAR_MODIFIED, toolBarLayout);
|
||||
return {
|
||||
id,
|
||||
Component: component || buttonType.defaultComponent,
|
||||
componentProps: Object.assign({}, props, { onClick }), //
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,6 +41,7 @@ export {
|
||||
InputMultiSelect,
|
||||
InputText,
|
||||
Label,
|
||||
LayoutSelector,
|
||||
MeasurementTable,
|
||||
Modal,
|
||||
NavBar,
|
||||
@ -67,7 +68,6 @@ export {
|
||||
ThumbnailNoImage,
|
||||
ThumbnailTracked,
|
||||
ThumbnailList,
|
||||
Toolbar,
|
||||
ToolbarButton,
|
||||
Tooltip,
|
||||
Typography,
|
||||
|
||||
80
platform/ui/src/components/LayoutSelector/LayoutSelector.jsx
Normal file
80
platform/ui/src/components/LayoutSelector/LayoutSelector.jsx
Normal file
@ -0,0 +1,80 @@
|
||||
// // SEE:
|
||||
// // https://github.com/OHIF/Viewers/blob/b58aa4575ab72fe3f493cc5a4261b4f8256516ab/platform/viewer/src/appExtensions/MeasurementsPanel/index.js#L18-L49
|
||||
// import React from 'react';
|
||||
// import { useViewportGrid } from '@ohif/ui';
|
||||
|
||||
// function getCommandsModule({ servicesManager }) {
|
||||
// const { UIDialogService } = servicesManager.services;
|
||||
|
||||
// const definitions = {
|
||||
// toggleLayoutSelectionDialog: {
|
||||
// commandFn: () => {
|
||||
// if (!UIDialogService) {
|
||||
// window.alert(
|
||||
// 'Unable to show dialog; no UI Dialog Service available.'
|
||||
// );
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // TODO: use SimpleDialog component
|
||||
// // TODO: update position on window resize
|
||||
// // TODO: Expand service API to check if dialog w/ ID is already open
|
||||
// // TODO: Import and call `useViewportGrid`
|
||||
// UIDialogService.dismiss({ id: 'layoutSelection' });
|
||||
// UIDialogService.create({
|
||||
// id: 'layoutSelection',
|
||||
// centralize: true,
|
||||
// isDraggable: false,
|
||||
// showOverlay: true,
|
||||
// content: Test,
|
||||
// });
|
||||
// },
|
||||
// storeContexts: [],
|
||||
// options: {},
|
||||
// context: 'VIEWER',
|
||||
// },
|
||||
// };
|
||||
|
||||
// return {
|
||||
// definitions,
|
||||
// defaultContext: 'VIEWER',
|
||||
// };
|
||||
// }
|
||||
|
||||
// function Test() {
|
||||
// const [
|
||||
// { numCols, numRows, activeViewportIndex, viewports },
|
||||
// dispatch,
|
||||
// ] = useViewportGrid();
|
||||
|
||||
// return (
|
||||
// <div
|
||||
// onClick={() => {
|
||||
// dispatch({
|
||||
// type: 'SET_LAYOUT',
|
||||
// payload: {
|
||||
// numCols: 2,
|
||||
// numRows: 2,
|
||||
// },
|
||||
// });
|
||||
// }}
|
||||
// style={{ color: 'white' }}
|
||||
// >
|
||||
// Hello World!
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
// export default getCommandsModule;
|
||||
|
||||
import React from 'react';
|
||||
|
||||
function LayoutSelector() {
|
||||
return (
|
||||
<>
|
||||
<div>LAYOUT SELECTOR PLACEHOLDER!</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default LayoutSelector;
|
||||
2
platform/ui/src/components/LayoutSelector/index.js
Normal file
2
platform/ui/src/components/LayoutSelector/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
import LayoutSelector from './LayoutSelector';
|
||||
export default LayoutSelector;
|
||||
@ -1,89 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { ToolbarButton, IconButton, Icon, Tooltip } from '@ohif/ui';
|
||||
|
||||
const classes = {
|
||||
type: {
|
||||
primary: '',
|
||||
secondary: 'w-full items-center bg-primary-dark px-3',
|
||||
},
|
||||
};
|
||||
|
||||
const Toolbar = ({ activeTool, tools, moreTools, type }) => {
|
||||
return (
|
||||
<div className={classnames('flex', classes.type[type])}>
|
||||
{tools.map((tool) => {
|
||||
const { id, onClick, icon, label, dropdownContent } = tool;
|
||||
const isActive = activeTool === tool.id;
|
||||
return (
|
||||
<div className="relative flex justify-center" key={tool.id}>
|
||||
<ToolbarButton
|
||||
id={id}
|
||||
isActive={isActive}
|
||||
onClick={onClick}
|
||||
icon={icon}
|
||||
label={label}
|
||||
dropdownContent={dropdownContent}
|
||||
type={type}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{!!moreTools.length && (
|
||||
<>
|
||||
<span className="w-1 border-l h-8 self-center mx-2 border-common-dark" />
|
||||
<Tooltip position="bottom" content="More tools">
|
||||
<IconButton
|
||||
className={classnames(
|
||||
'mx-1 text-common-bright hover:bg-primary-dark hover:text-primary-light'
|
||||
)}
|
||||
color="inherit"
|
||||
>
|
||||
<Icon name="tool-more-menu" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Toolbar.defaultProps = {
|
||||
activeTool: '',
|
||||
moreTools: [],
|
||||
type: 'primary',
|
||||
};
|
||||
|
||||
Toolbar.propTypes = {
|
||||
type: PropTypes.oneOf(['primary', 'secondary']),
|
||||
activeTool: PropTypes.string,
|
||||
tools: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
commandName: PropTypes.string,
|
||||
commandOptions: PropTypes.shape({
|
||||
toolName: PropTypes.string,
|
||||
}),
|
||||
onClick: PropTypes.func,
|
||||
dropdownContent: PropTypes.node,
|
||||
})
|
||||
).isRequired,
|
||||
moreTools: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
commandName: PropTypes.string,
|
||||
commandOptions: PropTypes.shape({
|
||||
toolName: PropTypes.string,
|
||||
}),
|
||||
onClick: PropTypes.func,
|
||||
})
|
||||
),
|
||||
};
|
||||
|
||||
export default Toolbar;
|
||||
@ -1,172 +0,0 @@
|
||||
---
|
||||
name: Toolbar
|
||||
menu: Data Display
|
||||
route: components/toolbar
|
||||
---
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Playground, Props } from 'docz';
|
||||
import classnames from 'classnames';
|
||||
import { Toolbar, Typography } from '@ohif/ui';
|
||||
|
||||
# Toolbar
|
||||
|
||||
A Toolbar is used to create a list of tools.
|
||||
|
||||
## Import
|
||||
|
||||
```javascript
|
||||
import { Toolbar } from '@ohif/ui';
|
||||
```
|
||||
|
||||
<Playground>
|
||||
{() => {
|
||||
const [activeTool, setActiveTool] = useState('Zoom');
|
||||
const dropdownContent = [
|
||||
{
|
||||
name: 'Soft tissue',
|
||||
value: '400/40',
|
||||
},
|
||||
{ name: 'Lung', value: '1500 / -600' },
|
||||
{ name: 'Liver', value: '150 / 90' },
|
||||
{ name: 'Bone', value: '2500 / 480' },
|
||||
{ name: 'Brain', value: '80 / 40' },
|
||||
];
|
||||
const tools = [
|
||||
{
|
||||
id: 'Zoom',
|
||||
label: 'Zoom',
|
||||
icon: 'tool-zoom',
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Zoom' },
|
||||
onClick: () => setActiveTool('Zoom'),
|
||||
},
|
||||
{
|
||||
id: 'Wwwc',
|
||||
label: 'Levels',
|
||||
icon: 'tool-window-level',
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Wwwc' },
|
||||
onClick: () => setActiveTool('Wwwc'),
|
||||
dropdownContent: (
|
||||
<div>
|
||||
{dropdownContent.map((row, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex justify-between py-2 px-3 hover:bg-secondary-dark cursor-pointer"
|
||||
>
|
||||
<div>
|
||||
<span className="text-base text-white">{row.name}</span>
|
||||
<span className="text-base text-primary-light ml-3">
|
||||
{row.value}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-base text-primary-active ml-4">{i}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'Pan',
|
||||
label: 'Pan',
|
||||
icon: 'tool-move',
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Pan' },
|
||||
onClick: () => setActiveTool('Pan'),
|
||||
},
|
||||
{
|
||||
id: 'Capture',
|
||||
label: 'Capture',
|
||||
icon: 'tool-capture',
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Capture' },
|
||||
onClick: () => setActiveTool('Capture'),
|
||||
},
|
||||
{
|
||||
id: 'Layout',
|
||||
label: 'Layout',
|
||||
icon: 'tool-layout',
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Layout' },
|
||||
onClick: () => setActiveTool('Layout'),
|
||||
},
|
||||
];
|
||||
const moreTools = [
|
||||
{
|
||||
id: 'Zoom 2',
|
||||
label: 'Zoom',
|
||||
icon: 'tool-zoom',
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Zoom' },
|
||||
onClick: () => setActiveTool('Zoom 2'),
|
||||
},
|
||||
{
|
||||
id: 'Layout 2',
|
||||
label: 'Layout',
|
||||
icon: 'tool-layout',
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Layout' },
|
||||
onClick: () => setActiveTool('Layout 2'),
|
||||
},
|
||||
];
|
||||
const secondaryTools = [
|
||||
{
|
||||
id: 'Annotate',
|
||||
label: 'Annotate',
|
||||
icon: 'tool-annotate',
|
||||
type: null,
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Annotate' },
|
||||
onClick: () => console.log('Activate Annotate'),
|
||||
},
|
||||
{
|
||||
id: 'Bidirectional',
|
||||
label: 'Bidirectional',
|
||||
icon: 'tool-bidirectional',
|
||||
type: null,
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Bidirectional' },
|
||||
onClick: () => console.log('Activate Bidirectional'),
|
||||
},
|
||||
{
|
||||
id: 'Elipse',
|
||||
label: 'Elipse',
|
||||
icon: 'tool-elipse',
|
||||
type: null,
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Elipse' },
|
||||
onClick: () => console.log('Activate Elipse'),
|
||||
},
|
||||
{
|
||||
id: 'Length',
|
||||
label: 'Length',
|
||||
icon: 'tool-length',
|
||||
type: null,
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Length' },
|
||||
onClick: () => console.log('Activate Length'),
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div className="py-10 px-4 flex flex-col justify-center">
|
||||
<div className="mb-4">
|
||||
<Typography className="mb-2">Primary Toolbar</Typography>
|
||||
<Toolbar
|
||||
moreTools={moreTools}
|
||||
activeTool={activeTool}
|
||||
tools={tools}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Typography className="mb-2">Secondary Toolbar</Typography>
|
||||
<Toolbar tools={secondaryTools} type="secondary" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Playground>
|
||||
|
||||
## Properties
|
||||
|
||||
<Props of={Toolbar} />
|
||||
@ -1,2 +0,0 @@
|
||||
import Toolbar from './Toolbar';
|
||||
export default Toolbar;
|
||||
@ -28,6 +28,7 @@ const ToolbarButton = ({
|
||||
return (
|
||||
<div key={id}>
|
||||
<Tooltip
|
||||
isSticky={shouldShowDropdown}
|
||||
content={shouldShowDropdown ? dropdownContent : label}
|
||||
tight={shouldShowDropdown}
|
||||
>
|
||||
@ -51,6 +52,7 @@ ToolbarButton.defaultProps = {
|
||||
};
|
||||
|
||||
ToolbarButton.propTypes = {
|
||||
/* Influences background/hover styling */
|
||||
type: PropTypes.oneOf(['primary', 'secondary']),
|
||||
id: PropTypes.string.isRequired,
|
||||
isActive: PropTypes.bool,
|
||||
@ -58,7 +60,7 @@ ToolbarButton.propTypes = {
|
||||
icon: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
/** Tooltip content can be replaced for a customized content by passing a node to this value. */
|
||||
dropdownContent: PropTypes.node,
|
||||
dropdownContent: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
|
||||
};
|
||||
|
||||
export default ToolbarButton;
|
||||
|
||||
@ -24,7 +24,7 @@ const arrowPositionStyle = {
|
||||
},
|
||||
};
|
||||
|
||||
const Tooltip = ({ position, content, tight, children }) => {
|
||||
const Tooltip = ({ content, isSticky, position, tight, children }) => {
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
|
||||
const handleMouseOver = () => {
|
||||
@ -39,6 +39,8 @@ const Tooltip = ({ position, content, tight, children }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const isOpen = isSticky || isActive;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative "
|
||||
@ -51,8 +53,8 @@ const Tooltip = ({ position, content, tight, children }) => {
|
||||
{children}
|
||||
<div
|
||||
className={classnames(`tooltip tooltip-${position}`, {
|
||||
block: isActive,
|
||||
hidden: !isActive,
|
||||
block: isOpen,
|
||||
hidden: !isOpen,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
@ -63,9 +65,9 @@ const Tooltip = ({ position, content, tight, children }) => {
|
||||
}
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
{typeof content === 'function' ? content() : content}
|
||||
<svg
|
||||
className="absolute text-primary-dark h-4 stroke-secondary-main"
|
||||
className="absolute h-4 text-primary-dark stroke-secondary-main"
|
||||
style={arrowPositionStyle[position]}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
@ -80,11 +82,12 @@ const Tooltip = ({ position, content, tight, children }) => {
|
||||
|
||||
Tooltip.defaultProps = {
|
||||
tight: false,
|
||||
isSticky: false,
|
||||
position: 'bottom',
|
||||
};
|
||||
|
||||
Tooltip.propTypes = {
|
||||
tight: PropTypes.bool,
|
||||
content: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
|
||||
position: PropTypes.oneOf([
|
||||
'bottom',
|
||||
'bottom-left',
|
||||
@ -92,8 +95,9 @@ Tooltip.propTypes = {
|
||||
'left',
|
||||
'right',
|
||||
]),
|
||||
isSticky: PropTypes.bool,
|
||||
tight: PropTypes.bool,
|
||||
children: PropTypes.node.isRequired,
|
||||
content: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default Tooltip;
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
.tooltip {
|
||||
@apply z-10 absolute;
|
||||
@apply absolute z-10;
|
||||
}
|
||||
|
||||
/* TOOLTIP WORKAROUND FOR ARROW UP */
|
||||
.tooltip.tooltip-bottom .tooltip-box::before {
|
||||
@apply bg-primary-dark absolute z-10;
|
||||
@apply absolute z-10 bg-primary-dark;
|
||||
content: '';
|
||||
width: 14px;
|
||||
height: 1px;
|
||||
@ -44,7 +44,7 @@
|
||||
}
|
||||
|
||||
.tooltip.tooltip-right .tooltip-box::before {
|
||||
@apply bg-primary-dark absolute z-10;
|
||||
@apply absolute z-10 bg-primary-dark;
|
||||
content: '';
|
||||
width: 2px;
|
||||
height: 15px;
|
||||
@ -54,7 +54,7 @@
|
||||
}
|
||||
|
||||
.tooltip.tooltip-left .tooltip-box::before {
|
||||
@apply bg-primary-dark absolute z-10;
|
||||
@apply absolute z-10 bg-primary-dark;
|
||||
content: '';
|
||||
width: 2px;
|
||||
height: 15px;
|
||||
@ -64,7 +64,7 @@
|
||||
}
|
||||
|
||||
.tooltip.tooltip-bottom-right .tooltip-box::before {
|
||||
@apply bg-primary-dark absolute z-10;
|
||||
@apply absolute z-10 bg-primary-dark;
|
||||
content: '';
|
||||
width: 15px;
|
||||
height: 2px;
|
||||
@ -73,19 +73,10 @@
|
||||
}
|
||||
|
||||
.tooltip.tooltip-bottom-left .tooltip-box::before {
|
||||
@apply bg-primary-dark absolute z-10;
|
||||
@apply absolute z-10 bg-primary-dark;
|
||||
content: '';
|
||||
width: 15px;
|
||||
height: 2px;
|
||||
left: 5px;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
/* TODO: REMOVE BELOW CLASS "showTooltipOnHover" AFTER TOOLTIP REPLACEMENT */
|
||||
.showTooltipOnHover .tooltip {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.showTooltipOnHover:hover .tooltip {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@ -184,7 +184,7 @@ const ViewportActionBar = ({ studyData, onSeriesChange }) => {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="relative flex justify-end showTooltipOnHover">
|
||||
<div className="relative flex justify-end">
|
||||
<div className="relative">
|
||||
<Icon name="profile" className="w-5 text-white" />
|
||||
<Icon
|
||||
|
||||
@ -12,6 +12,7 @@ import InputLabelWrapper from './InputLabelWrapper';
|
||||
import InputMultiSelect from './InputMultiSelect';
|
||||
import InputText from './InputText';
|
||||
import Label from './Label';
|
||||
import LayoutSelector from './LayoutSelector';
|
||||
import MeasurementTable from './MeasurementTable';
|
||||
import Modal from './Modal';
|
||||
import NavBar from './NavBar';
|
||||
@ -37,7 +38,6 @@ import Thumbnail from './Thumbnail';
|
||||
import ThumbnailNoImage from './ThumbnailNoImage';
|
||||
import ThumbnailTracked from './ThumbnailTracked';
|
||||
import ThumbnailList from './ThumbnailList';
|
||||
import Toolbar from './Toolbar';
|
||||
import ToolbarButton from './ToolbarButton';
|
||||
import Tooltip from './Tooltip';
|
||||
import Typography from './Typography';
|
||||
@ -61,6 +61,7 @@ export {
|
||||
InputMultiSelect,
|
||||
InputText,
|
||||
Label,
|
||||
LayoutSelector,
|
||||
MeasurementTable,
|
||||
Modal,
|
||||
NavBar,
|
||||
@ -87,7 +88,6 @@ export {
|
||||
ThumbnailNoImage,
|
||||
ThumbnailTracked,
|
||||
ThumbnailList,
|
||||
Toolbar,
|
||||
ToolbarButton,
|
||||
Tooltip,
|
||||
Typography,
|
||||
|
||||
@ -1,14 +1,7 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
SidePanel,
|
||||
StudyBrowser,
|
||||
DragAndDropProvider,
|
||||
ViewportGrid,
|
||||
ViewportPane,
|
||||
} from '@ohif/ui';
|
||||
import { SidePanel, StudyBrowser, DragAndDropProvider } from '@ohif/ui';
|
||||
|
||||
import Header from './components/Header';
|
||||
import ViewportToolbar from './components/ViewportToolBar';
|
||||
|
||||
const Viewer = () => {
|
||||
return (
|
||||
@ -16,7 +9,7 @@ const Viewer = () => {
|
||||
<div>
|
||||
<Header />
|
||||
<div
|
||||
className="flex flex-row flex-no-wrap flex-1 items-stretch overflow-hidden w-full"
|
||||
className="flex flex-row flex-no-wrap items-stretch flex-1 w-full overflow-hidden"
|
||||
style={{ height: 'calc(100vh - 57px' }}
|
||||
>
|
||||
<SidePanel
|
||||
@ -28,8 +21,8 @@ const Viewer = () => {
|
||||
>
|
||||
<StudyBrowser />
|
||||
</SidePanel>
|
||||
<div className="flex flex-1 h-100 overflow-hidden bg-primary-main items-center justify-center text-white">
|
||||
<ViewportToolbar />
|
||||
<div className="flex items-center justify-center flex-1 overflow-hidden text-white h-100 bg-primary-main">
|
||||
{/* <ViewportToolbar /> */}
|
||||
<div>CONTENT</div>
|
||||
</div>
|
||||
<SidePanel
|
||||
@ -39,7 +32,7 @@ const Viewer = () => {
|
||||
componentLabel="Measurements"
|
||||
defaultIsOpen={false}
|
||||
>
|
||||
<div className="flex justify-center text-white p-2">
|
||||
<div className="flex justify-center p-2 text-white">
|
||||
panel placeholder
|
||||
</div>
|
||||
</SidePanel>
|
||||
|
||||
@ -25,7 +25,6 @@ import {
|
||||
} from '@ohif/ui';
|
||||
|
||||
import Header from './components/Header';
|
||||
import ViewportToolbar from './components/ViewportToolBar';
|
||||
|
||||
import { tabs } from './studyBrowserMockData';
|
||||
|
||||
@ -76,7 +75,7 @@ import { tabs } from './studyBrowserMockData';
|
||||
{/* TOOLBAR + GRID */}
|
||||
<div className="flex flex-col flex-1 h-full">
|
||||
<div className="flex flex-2 w-100 border-b border-transparent h-12">
|
||||
<ViewportToolbar />
|
||||
{/* <ViewportToolbar /> Secondary Toolbar */}
|
||||
</div>
|
||||
{/* VIEWPORT GRID CONTAINER */}
|
||||
<div className="flex flex-1 h-full overflow-hidden bg-black items-center justify-center pb-2 pt-1">
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { NavBar, Svg, Icon, IconButton, Toolbar } from '@ohif/ui';
|
||||
import { NavBar, Svg, Icon, IconButton } from '@ohif/ui';
|
||||
|
||||
const Header = () => {
|
||||
const [activeTool, setActiveTool] = useState('Zoom');
|
||||
@ -34,15 +34,15 @@ const Header = () => {
|
||||
{dropdownContent.map((row, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex justify-between py-2 px-3 hover:bg-secondary-dark cursor-pointer"
|
||||
className="flex justify-between px-3 py-2 cursor-pointer hover:bg-secondary-dark"
|
||||
>
|
||||
<div>
|
||||
<span className="text-base text-white">{row.name}</span>
|
||||
<span className="text-base text-primary-light ml-3">
|
||||
<span className="ml-3 text-base text-primary-light">
|
||||
{row.value}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-base text-primary-active ml-4">{i}</span>
|
||||
<span className="ml-4 text-base text-primary-active">{i}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -75,12 +75,12 @@ const Header = () => {
|
||||
];
|
||||
return (
|
||||
<NavBar className="justify-between border-b-4 border-black">
|
||||
<div className="flex flex-1 justify-between">
|
||||
<div className="flex justify-between flex-1">
|
||||
<div className="flex items-center">
|
||||
<div className="mr-3 inline-flex items-center">
|
||||
<div className="inline-flex items-center mr-3">
|
||||
<Icon
|
||||
name="chevron-left"
|
||||
className="text-primary-active w-8 cursor-pointer"
|
||||
className="w-8 cursor-pointer text-primary-active"
|
||||
onClick={() => alert('Navigate to previous page')}
|
||||
/>
|
||||
<a href="#" className="ml-4">
|
||||
@ -89,10 +89,10 @@ const Header = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Toolbar tools={tools} activeTool={activeTool} moreTools={tools} />
|
||||
{/* <Toolbar tools={tools} activeTool={activeTool} moreTools={tools} /> */}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="mr-3 text-common-light text-lg">
|
||||
<span className="mr-3 text-lg text-common-light">
|
||||
FOR INVESTIGATIONAL USE ONLY
|
||||
</span>
|
||||
<IconButton
|
||||
|
||||
@ -1,47 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Toolbar } from '@ohif/ui';
|
||||
|
||||
const ViewportToolbar = () => {
|
||||
const tools = [
|
||||
{
|
||||
id: 'Annotate',
|
||||
label: 'Annotate',
|
||||
icon: 'tool-annotate',
|
||||
type: null,
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Annotate' },
|
||||
onClick: () => console.log('Activate Annotate'),
|
||||
},
|
||||
{
|
||||
id: 'Bidirectional',
|
||||
label: 'Bidirectional',
|
||||
icon: 'tool-bidirectional',
|
||||
type: null,
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Bidirectional' },
|
||||
onClick: () => console.log('Activate Bidirectional'),
|
||||
},
|
||||
{
|
||||
id: 'Elipse',
|
||||
label: 'Elipse',
|
||||
icon: 'tool-elipse',
|
||||
type: null,
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Elipse' },
|
||||
onClick: () => console.log('Activate Elipse'),
|
||||
},
|
||||
{
|
||||
id: 'Length',
|
||||
label: 'Length',
|
||||
icon: 'tool-length',
|
||||
type: null,
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Length' },
|
||||
onClick: () => console.log('Activate Length'),
|
||||
},
|
||||
];
|
||||
return <Toolbar type="secondary" tools={tools} />;
|
||||
};
|
||||
|
||||
export default ViewportToolbar;
|
||||
@ -1,57 +0,0 @@
|
||||
import OHIF from '@ohif/core';
|
||||
import { connect } from 'react-redux';
|
||||
import { StudyBrowser } from '@ohif/ui';
|
||||
import cloneDeep from 'lodash.clonedeep';
|
||||
import findDisplaySetByUID from './findDisplaySetByUID';
|
||||
|
||||
const { setActiveViewportSpecificData } = OHIF.redux.actions;
|
||||
|
||||
// TODO
|
||||
// - Determine in which display set is active from Redux (activeViewportIndex and layout viewportData)
|
||||
// - Pass in errors and stack loading progress from Redux
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
// If we know that the stack loading progress details have changed,
|
||||
// we can try to update the component state so that the thumbnail
|
||||
// progress bar is updated
|
||||
const stackLoadingProgressMap = state.loading.progress;
|
||||
const studiesWithLoadingData = cloneDeep(ownProps.studies);
|
||||
|
||||
studiesWithLoadingData.forEach(study => {
|
||||
study.thumbnails.forEach(data => {
|
||||
const { displaySetInstanceUID } = data;
|
||||
const stackId = `StackProgress:${displaySetInstanceUID}`;
|
||||
const stackProgressData = stackLoadingProgressMap[stackId];
|
||||
|
||||
let stackPercentComplete = 0;
|
||||
if (stackProgressData) {
|
||||
stackPercentComplete = stackProgressData.percentComplete;
|
||||
}
|
||||
|
||||
data.stackPercentComplete = stackPercentComplete;
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
studies: studiesWithLoadingData,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, ownProps) => {
|
||||
return {
|
||||
onThumbnailClick: displaySetInstanceUID => {
|
||||
const displaySet = findDisplaySetByUID(
|
||||
ownProps.studyMetadata,
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
dispatch(setActiveViewportSpecificData(displaySet));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedStudyBrowser = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(StudyBrowser);
|
||||
|
||||
export default ConnectedStudyBrowser;
|
||||
@ -1,37 +0,0 @@
|
||||
import OHIF from '@ohif/core';
|
||||
import ViewerMain from './ViewerMain';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
const {
|
||||
setViewportSpecificData,
|
||||
clearViewportSpecificData,
|
||||
} = OHIF.redux.actions;
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { activeViewportIndex, layout, viewportSpecificData } = state.viewports;
|
||||
|
||||
return {
|
||||
activeViewportIndex,
|
||||
layout,
|
||||
viewportSpecificData,
|
||||
viewports: state.viewports,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
setViewportSpecificData: (viewportIndex, data) => {
|
||||
dispatch(setViewportSpecificData(viewportIndex, data));
|
||||
},
|
||||
clearViewportSpecificData: () => {
|
||||
dispatch(clearViewportSpecificData());
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedViewerMain = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ViewerMain);
|
||||
|
||||
export default ConnectedViewerMain;
|
||||
@ -1,28 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import ViewerRetrieveStudyData from './ViewerRetrieveStudyData.js';
|
||||
import OHIF from '@ohif/core';
|
||||
|
||||
const { clearViewportSpecificData } = OHIF.redux.actions;
|
||||
const isActive = a => a.active === true;
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const activeServer = state.servers.servers.find(isActive);
|
||||
|
||||
return {
|
||||
server: ownProps.server || activeServer,
|
||||
};
|
||||
};
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
clearViewportSpecificData: () => {
|
||||
dispatch(clearViewportSpecificData());
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedViewerRetrieveStudyData = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ViewerRetrieveStudyData);
|
||||
|
||||
export default ConnectedViewerRetrieveStudyData;
|
||||
@ -1,114 +0,0 @@
|
||||
import { ContextMenu } from '@ohif/ui';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { commandsManager } from './../App.jsx';
|
||||
|
||||
const toolTypes = [
|
||||
'Angle',
|
||||
'Bidirectional',
|
||||
'Length',
|
||||
'FreehandMouse',
|
||||
'EllipticalRoi',
|
||||
'CircleRoi',
|
||||
'RectangleRoi',
|
||||
];
|
||||
|
||||
const ToolContextMenu = ({
|
||||
onSetLabel,
|
||||
onSetDescription,
|
||||
isTouchEvent,
|
||||
eventData,
|
||||
onClose,
|
||||
onDelete,
|
||||
}) => {
|
||||
const defaultDropdownItems = [
|
||||
{
|
||||
label: 'Delete measurement',
|
||||
actionType: 'Delete',
|
||||
action: ({ nearbyToolData, eventData }) =>
|
||||
onDelete(nearbyToolData, eventData),
|
||||
},
|
||||
{
|
||||
label: 'Relabel',
|
||||
actionType: 'setLabel',
|
||||
action: ({ nearbyToolData, eventData }) => {
|
||||
const { tool: measurementData } = nearbyToolData;
|
||||
onSetLabel(eventData, measurementData);
|
||||
},
|
||||
},
|
||||
{
|
||||
actionType: 'setDescription',
|
||||
action: ({ nearbyToolData, eventData }) => {
|
||||
const { tool: measurementData } = nearbyToolData;
|
||||
onSetDescription(eventData, measurementData);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const getDropdownItems = (eventData, isTouchEvent = false) => {
|
||||
const nearbyToolData = commandsManager.runCommand('getNearbyToolData', {
|
||||
element: eventData.element,
|
||||
canvasCoordinates: eventData.currentPoints.canvas,
|
||||
availableToolTypes: 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(item => {
|
||||
item.params = { eventData, nearbyToolData };
|
||||
|
||||
if (item.actionType === 'setDescription') {
|
||||
item.label = `${
|
||||
nearbyToolData.tool.description ? 'Edit' : 'Add'
|
||||
} Description`;
|
||||
}
|
||||
|
||||
dropdownItems.push(item);
|
||||
});
|
||||
}
|
||||
|
||||
return dropdownItems;
|
||||
};
|
||||
|
||||
const onClickHandler = ({ action, params }) => {
|
||||
action(params);
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const dropdownItems = getDropdownItems(eventData, isTouchEvent);
|
||||
|
||||
return (
|
||||
<div className="ToolContextMenu">
|
||||
<ContextMenu items={dropdownItems} onClick={onClickHandler} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ToolContextMenu.propTypes = {
|
||||
isTouchEvent: PropTypes.bool.isRequired,
|
||||
eventData: PropTypes.object,
|
||||
onClose: PropTypes.func,
|
||||
onSetDescription: PropTypes.func,
|
||||
onSetLabel: PropTypes.func,
|
||||
onDelete: PropTypes.func,
|
||||
};
|
||||
|
||||
ToolContextMenu.defaultProps = {
|
||||
isTouchEvent: false,
|
||||
};
|
||||
|
||||
export default ToolContextMenu;
|
||||
@ -1,386 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
import { MODULE_TYPES } from '@ohif/core';
|
||||
import {
|
||||
ExpandableToolMenu,
|
||||
RoundedButtonGroup,
|
||||
ToolbarButton,
|
||||
withModal,
|
||||
withDialog,
|
||||
} from '@ohif/ui';
|
||||
|
||||
import './ToolbarRow.css';
|
||||
import { commandsManager, extensionManager } from './../App.jsx';
|
||||
|
||||
import ConnectedCineDialog from './ConnectedCineDialog';
|
||||
import ConnectedLayoutButton from './ConnectedLayoutButton';
|
||||
import { withAppContext } from '../context/AppContext';
|
||||
|
||||
class ToolbarRow extends Component {
|
||||
// TODO: Simplify these? isOpen can be computed if we say "any" value for selected,
|
||||
// closed if selected is null/undefined
|
||||
static propTypes = {
|
||||
isLeftSidePanelOpen: PropTypes.bool.isRequired,
|
||||
isRightSidePanelOpen: PropTypes.bool.isRequired,
|
||||
selectedLeftSidePanel: PropTypes.string.isRequired,
|
||||
selectedRightSidePanel: PropTypes.string.isRequired,
|
||||
handleSidePanelChange: PropTypes.func.isRequired,
|
||||
activeContexts: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
studies: PropTypes.array,
|
||||
t: PropTypes.func.isRequired,
|
||||
// NOTE: withDialog, withModal HOCs
|
||||
dialog: PropTypes.any,
|
||||
modal: PropTypes.any,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
studies: [],
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const toolbarButtonDefinitions = _getVisibleToolbarButtons.call(this);
|
||||
// TODO:
|
||||
// If it's a tool that can be active... Mark it as active?
|
||||
// - Tools that are on/off?
|
||||
// - Tools that can be bound to multiple buttons?
|
||||
|
||||
// Normal ToolbarButtons...
|
||||
// Just how high do we need to hoist this state?
|
||||
// Why ToolbarRow instead of just Toolbar? Do we have any others?
|
||||
this.state = {
|
||||
toolbarButtons: toolbarButtonDefinitions,
|
||||
activeButtons: [],
|
||||
};
|
||||
|
||||
this.seriesPerStudyCount = [];
|
||||
|
||||
this._handleBuiltIn = _handleBuiltIn.bind(this);
|
||||
|
||||
this.updateButtonGroups();
|
||||
}
|
||||
|
||||
updateButtonGroups() {
|
||||
const panelModules = extensionManager.modules[MODULE_TYPES.PANEL];
|
||||
|
||||
this.buttonGroups = {
|
||||
left: [],
|
||||
right: [],
|
||||
};
|
||||
|
||||
// ~ FIND MENU OPTIONS
|
||||
panelModules.forEach(panelExtension => {
|
||||
const panelModule = panelExtension.module;
|
||||
const defaultContexts = Array.from(panelModule.defaultContext);
|
||||
|
||||
panelModule.menuOptions.forEach(menuOption => {
|
||||
const contexts = Array.from(menuOption.context || defaultContexts);
|
||||
const hasActiveContext = this.props.activeContexts.some(actx =>
|
||||
contexts.includes(actx)
|
||||
);
|
||||
|
||||
// It's a bit beefy to pass studies; probably only need to be reactive on `studyInstanceUIDs` and activeViewport?
|
||||
// Note: This does not cleanly handle `studies` prop updating with panel open
|
||||
const isDisabled =
|
||||
typeof menuOption.isDisabled === 'function' &&
|
||||
menuOption.isDisabled(this.props.studies);
|
||||
|
||||
if (hasActiveContext && !isDisabled) {
|
||||
const menuOptionEntry = {
|
||||
value: menuOption.target,
|
||||
icon: menuOption.icon,
|
||||
bottomLabel: menuOption.label,
|
||||
};
|
||||
const from = menuOption.from || 'right';
|
||||
|
||||
this.buttonGroups[from].push(menuOptionEntry);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: This should come from extensions, instead of being baked in
|
||||
this.buttonGroups.left.unshift({
|
||||
value: 'studies',
|
||||
icon: 'th-large',
|
||||
bottomLabel: this.props.t('Series'),
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const activeContextsChanged =
|
||||
prevProps.activeContexts !== this.props.activeContexts;
|
||||
|
||||
const prevStudies = prevProps.studies;
|
||||
const studies = this.props.studies;
|
||||
const seriesPerStudyCount = this.seriesPerStudyCount;
|
||||
|
||||
let studiesUpdated = false;
|
||||
|
||||
if (prevStudies.length !== studies.length) {
|
||||
studiesUpdated = true;
|
||||
} else {
|
||||
for (let i = 0; i < studies.length; i++) {
|
||||
if (studies[i].series.length !== seriesPerStudyCount[i]) {
|
||||
seriesPerStudyCount[i] = studies[i].series.length;
|
||||
|
||||
studiesUpdated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (studiesUpdated) {
|
||||
this.updateButtonGroups();
|
||||
}
|
||||
|
||||
if (activeContextsChanged) {
|
||||
this.setState(
|
||||
{
|
||||
toolbarButtons: _getVisibleToolbarButtons.call(this),
|
||||
},
|
||||
this.closeCineDialogIfNotApplicable
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
closeCineDialogIfNotApplicable = () => {
|
||||
const { dialog } = this.props;
|
||||
let { dialogId, activeButtons, toolbarButtons } = this.state;
|
||||
if (dialogId) {
|
||||
const cineButtonPresent = toolbarButtons.find(
|
||||
button => button.options && button.options.behavior === 'CINE'
|
||||
);
|
||||
if (!cineButtonPresent) {
|
||||
dialog.dismiss({ id: dialogId });
|
||||
activeButtons = activeButtons.filter(
|
||||
button => button.options && button.options.behavior !== 'CINE'
|
||||
);
|
||||
this.setState({ dialogId: null, activeButtons });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const buttonComponents = _getButtonComponents.call(
|
||||
this,
|
||||
this.state.toolbarButtons,
|
||||
this.state.activeButtons
|
||||
);
|
||||
|
||||
const onPress = (side, value) => {
|
||||
this.props.handleSidePanelChange(side, value);
|
||||
};
|
||||
const onPressLeft = onPress.bind(this, 'left');
|
||||
const onPressRight = onPress.bind(this, 'right');
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="ToolbarRow">
|
||||
<div className="pull-left m-t-1 p-y-1" style={{ padding: '10px' }}>
|
||||
<RoundedButtonGroup
|
||||
options={this.buttonGroups.left}
|
||||
value={this.props.selectedLeftSidePanel || ''}
|
||||
onValueChanged={onPressLeft}
|
||||
/>
|
||||
</div>
|
||||
{buttonComponents}
|
||||
<ConnectedLayoutButton />
|
||||
<div
|
||||
className="pull-right m-t-1 rm-x-1"
|
||||
style={{ marginLeft: 'auto' }}
|
||||
>
|
||||
{this.buttonGroups.right.length && (
|
||||
<RoundedButtonGroup
|
||||
options={this.buttonGroups.right}
|
||||
value={this.props.selectedRightSidePanel || ''}
|
||||
onValueChanged={onPressRight}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function _getCustomButtonComponent(button, activeButtons) {
|
||||
const CustomComponent = button.CustomComponent;
|
||||
const isValidComponent = typeof CustomComponent === 'function';
|
||||
|
||||
// Check if its a valid customComponent. Later on an CustomToolbarComponent interface could be implemented.
|
||||
if (isValidComponent) {
|
||||
const parentContext = this;
|
||||
const activeButtonsIds = activeButtons.map(button => button.id);
|
||||
const isActive = activeButtonsIds.includes(button.id);
|
||||
|
||||
return (
|
||||
<CustomComponent
|
||||
parentContext={parentContext}
|
||||
toolbarClickCallback={_handleToolbarButtonClick.bind(this)}
|
||||
button={button}
|
||||
key={button.id}
|
||||
activeButtons={activeButtonsIds}
|
||||
isActive={isActive}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function _getExpandableButtonComponent(button, activeButtons) {
|
||||
// Iterate over button definitions and update `onClick` behavior
|
||||
let activeCommand;
|
||||
const childButtons = button.buttons.map(childButton => {
|
||||
childButton.onClick = _handleToolbarButtonClick.bind(this, childButton);
|
||||
|
||||
if (activeButtons.map(button => button.id).indexOf(childButton.id) > -1) {
|
||||
activeCommand = childButton.id;
|
||||
}
|
||||
|
||||
return childButton;
|
||||
});
|
||||
|
||||
return (
|
||||
<ExpandableToolMenu
|
||||
key={button.id}
|
||||
label={button.label}
|
||||
icon={button.icon}
|
||||
buttons={childButtons}
|
||||
activeCommand={activeCommand}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function _getDefaultButtonComponent(button, activeButtons) {
|
||||
return (
|
||||
<ToolbarButton
|
||||
key={button.id}
|
||||
label={button.label}
|
||||
icon={button.icon}
|
||||
onClick={_handleToolbarButtonClick.bind(this, button)}
|
||||
isActive={activeButtons.map(button => button.id).includes(button.id)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Determine which extension buttons should be showing, if they're
|
||||
* active, and what their onClick behavior should be.
|
||||
*/
|
||||
function _getButtonComponents(toolbarButtons, activeButtons) {
|
||||
const _this = this;
|
||||
return toolbarButtons.map(button => {
|
||||
const hasCustomComponent = button.CustomComponent;
|
||||
const hasNestedButtonDefinitions = button.buttons && button.buttons.length;
|
||||
|
||||
if (hasCustomComponent) {
|
||||
return _getCustomButtonComponent.call(_this, button, activeButtons);
|
||||
}
|
||||
|
||||
if (hasNestedButtonDefinitions) {
|
||||
return _getExpandableButtonComponent.call(_this, button, activeButtons);
|
||||
}
|
||||
|
||||
return _getDefaultButtonComponent.call(_this, button, activeButtons);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: DEPRECATE
|
||||
* This is used exclusively in `extensions/cornerstone/src`
|
||||
* We have better ways with new UI Services to trigger "builtin" behaviors
|
||||
*
|
||||
* A handy way for us to handle different button types. IE. firing commands for
|
||||
* buttons, or initiation built in behavior.
|
||||
*
|
||||
* @param {*} button
|
||||
* @param {*} evt
|
||||
* @param {*} props
|
||||
*/
|
||||
function _handleToolbarButtonClick(button, evt, props) {
|
||||
const { activeButtons } = this.state;
|
||||
|
||||
if (button.commandName) {
|
||||
const options = Object.assign({ evt }, button.commandOptions);
|
||||
commandsManager.runCommand(button.commandName, options);
|
||||
}
|
||||
|
||||
// TODO: Use Types ENUM
|
||||
// TODO: We can update this to be a `getter` on the extension to query
|
||||
// For the active tools after we apply our updates?
|
||||
if (button.type === 'setToolActive') {
|
||||
const toggables = activeButtons.filter(
|
||||
({ options }) => options && !options.togglable
|
||||
);
|
||||
this.setState({ activeButtons: [...toggables, button] });
|
||||
} else if (button.type === 'builtIn') {
|
||||
this._handleBuiltIn(button);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function _getVisibleToolbarButtons() {
|
||||
const toolbarModules = extensionManager.modules[MODULE_TYPES.TOOLBAR];
|
||||
const toolbarButtonDefinitions = [];
|
||||
|
||||
toolbarModules.forEach(extension => {
|
||||
const { definitions, defaultContext } = extension.module;
|
||||
definitions.forEach(definition => {
|
||||
const context = definition.context || defaultContext;
|
||||
|
||||
if (this.props.activeContexts.includes(context)) {
|
||||
toolbarButtonDefinitions.push(definition);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return toolbarButtonDefinitions;
|
||||
}
|
||||
|
||||
function _handleBuiltIn(button) {
|
||||
/* TODO: Keep cine button active until its unselected. */
|
||||
const { dialog, t } = this.props;
|
||||
const { dialogId } = this.state;
|
||||
const { id, options } = button;
|
||||
|
||||
if (options.behavior === 'CINE') {
|
||||
if (dialogId) {
|
||||
dialog.dismiss({ id: dialogId });
|
||||
this.setState(state => ({
|
||||
dialogId: null,
|
||||
activeButtons: [
|
||||
...state.activeButtons.filter(button => button.id !== id),
|
||||
],
|
||||
}));
|
||||
} else {
|
||||
const spacing = 20;
|
||||
const { x, y } = document
|
||||
.querySelector(`.ViewerMain`)
|
||||
.getBoundingClientRect();
|
||||
const newDialogId = dialog.create({
|
||||
content: ConnectedCineDialog,
|
||||
defaultPosition: {
|
||||
x: x + spacing || 0,
|
||||
y: y + spacing || 0,
|
||||
},
|
||||
});
|
||||
this.setState(state => ({
|
||||
dialogId: newDialogId,
|
||||
activeButtons: [...state.activeButtons, button],
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (options.behavior === 'DOWNLOAD_SCREEN_SHOT') {
|
||||
commandsManager.runCommand('showDownloadViewportModal', {
|
||||
title: t('Download High Quality Image'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation(['Common', 'ViewportDownloadForm'])(
|
||||
withModal(withDialog(withAppContext(ToolbarRow)))
|
||||
);
|
||||
@ -1,400 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { MODULE_TYPES } from '@ohif/core';
|
||||
import OHIF, { DICOMSR } from '@ohif/core';
|
||||
import { withDialog } from '@ohif/ui';
|
||||
import moment from 'moment';
|
||||
import ConnectedHeader from './ConnectedHeader.js';
|
||||
import ToolbarRow from './ToolbarRow.js';
|
||||
import ConnectedStudyBrowser from './ConnectedStudyBrowser.js';
|
||||
import ConnectedViewerMain from './ConnectedViewerMain.js';
|
||||
import SidePanel from './../components/SidePanel.js';
|
||||
import { extensionManager } from './../App.jsx';
|
||||
|
||||
// Contexts
|
||||
import WhiteLabelingContext from '../context/WhiteLabelingContext.js';
|
||||
import UserManagerContext from '../context/UserManagerContext';
|
||||
import AppContext from '../context/AppContext';
|
||||
|
||||
import './Viewer.css';
|
||||
|
||||
class Viewer extends Component {
|
||||
static propTypes = {
|
||||
studies: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
StudyInstanceUID: PropTypes.string.isRequired,
|
||||
StudyDate: PropTypes.string,
|
||||
displaySets: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
displaySetInstanceUID: PropTypes.string.isRequired,
|
||||
SeriesDescription: PropTypes.string,
|
||||
SeriesNumber: PropTypes.number,
|
||||
InstanceNumber: PropTypes.number,
|
||||
numImageFrames: PropTypes.number,
|
||||
Modality: PropTypes.string.isRequired,
|
||||
images: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
getImageId: PropTypes.func.isRequired,
|
||||
})
|
||||
),
|
||||
})
|
||||
),
|
||||
})
|
||||
),
|
||||
studyInstanceUIDs: PropTypes.array,
|
||||
activeServer: PropTypes.shape({
|
||||
type: PropTypes.string,
|
||||
wadoRoot: PropTypes.string,
|
||||
}),
|
||||
onTimepointsUpdated: PropTypes.func,
|
||||
onMeasurementsUpdated: PropTypes.func,
|
||||
// window.store.getState().viewports.viewportSpecificData
|
||||
viewports: PropTypes.object.isRequired,
|
||||
// window.store.getState().viewports.activeViewportIndex
|
||||
activeViewportIndex: PropTypes.number.isRequired,
|
||||
isStudyLoaded: PropTypes.bool,
|
||||
dialog: PropTypes.object,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const { activeServer } = this.props;
|
||||
const server = Object.assign({}, activeServer);
|
||||
|
||||
OHIF.measurements.MeasurementApi.setConfiguration({
|
||||
dataExchange: {
|
||||
retrieve: DICOMSR.retrieveMeasurements,
|
||||
store: DICOMSR.storeMeasurements,
|
||||
},
|
||||
server,
|
||||
});
|
||||
|
||||
OHIF.measurements.TimepointApi.setConfiguration({
|
||||
dataExchange: {
|
||||
retrieve: this.retrieveTimepoints,
|
||||
store: this.storeTimepoints,
|
||||
remove: this.removeTimepoint,
|
||||
update: this.updateTimepoint,
|
||||
disassociate: this.disassociateStudy,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
state = {
|
||||
isLeftSidePanelOpen: true,
|
||||
isRightSidePanelOpen: false,
|
||||
selectedRightSidePanel: '',
|
||||
selectedLeftSidePanel: 'studies', // TODO: Don't hardcode this
|
||||
thumbnails: [],
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.props.dialog) {
|
||||
this.props.dialog.dismissAll();
|
||||
}
|
||||
}
|
||||
|
||||
retrieveTimepoints = filter => {
|
||||
OHIF.log.info('retrieveTimepoints');
|
||||
|
||||
// Get the earliest and latest study date
|
||||
let earliestDate = new Date().toISOString();
|
||||
let latestDate = new Date().toISOString();
|
||||
if (this.props.studies) {
|
||||
latestDate = new Date('1000-01-01').toISOString();
|
||||
this.props.studies.forEach(study => {
|
||||
const StudyDate = moment(study.StudyDate, 'YYYYMMDD').toISOString();
|
||||
if (StudyDate < earliestDate) {
|
||||
earliestDate = StudyDate;
|
||||
}
|
||||
if (StudyDate > latestDate) {
|
||||
latestDate = StudyDate;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Return a generic timepoint
|
||||
return Promise.resolve([
|
||||
{
|
||||
timepointType: 'baseline',
|
||||
timepointId: 'TimepointId',
|
||||
studyInstanceUIDs: this.props.studyInstanceUIDs,
|
||||
PatientID: filter.PatientID,
|
||||
earliestDate,
|
||||
latestDate,
|
||||
isLocked: false,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
storeTimepoints = timepointData => {
|
||||
OHIF.log.info('storeTimepoints');
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
updateTimepoint = (timepointData, query) => {
|
||||
OHIF.log.info('updateTimepoint');
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
removeTimepoint = timepointId => {
|
||||
OHIF.log.info('removeTimepoint');
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
disassociateStudy = (timepointIds, StudyInstanceUID) => {
|
||||
OHIF.log.info('disassociateStudy');
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
onTimepointsUpdated = timepoints => {
|
||||
if (this.props.onTimepointsUpdated) {
|
||||
this.props.onTimepointsUpdated(timepoints);
|
||||
}
|
||||
};
|
||||
|
||||
onMeasurementsUpdated = measurements => {
|
||||
if (this.props.onMeasurementsUpdated) {
|
||||
this.props.onMeasurementsUpdated(measurements);
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { studies, isStudyLoaded } = this.props;
|
||||
const { TimepointApi, MeasurementApi } = OHIF.measurements;
|
||||
const currentTimepointId = 'TimepointId';
|
||||
|
||||
const timepointApi = new TimepointApi(currentTimepointId, {
|
||||
onTimepointsUpdated: this.onTimepointsUpdated,
|
||||
});
|
||||
|
||||
const measurementApi = new MeasurementApi(timepointApi, {
|
||||
onMeasurementsUpdated: this.onMeasurementsUpdated,
|
||||
});
|
||||
|
||||
this.currentTimepointId = currentTimepointId;
|
||||
this.timepointApi = timepointApi;
|
||||
this.measurementApi = measurementApi;
|
||||
|
||||
if (studies) {
|
||||
const PatientID = studies[0] && studies[0].PatientID;
|
||||
|
||||
timepointApi.retrieveTimepoints({ PatientID });
|
||||
if (isStudyLoaded) {
|
||||
this.measurementApi.retrieveMeasurements(PatientID, [
|
||||
currentTimepointId,
|
||||
]);
|
||||
}
|
||||
this.setState({
|
||||
thumbnails: _mapStudiesToThumbnails(studies),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { studies, isStudyLoaded } = this.props;
|
||||
if (studies !== prevProps.studies) {
|
||||
this.setState({
|
||||
thumbnails: _mapStudiesToThumbnails(studies),
|
||||
});
|
||||
}
|
||||
if (isStudyLoaded && isStudyLoaded !== prevProps.isStudyLoaded) {
|
||||
const PatientID = studies[0] && studies[0].PatientID;
|
||||
const { currentTimepointId } = this;
|
||||
|
||||
this.timepointApi.retrieveTimepoints({ PatientID });
|
||||
this.measurementApi.retrieveMeasurements(PatientID, [currentTimepointId]);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let VisiblePanelLeft, VisiblePanelRight;
|
||||
const panelExtensions = extensionManager.modules[MODULE_TYPES.PANEL];
|
||||
|
||||
panelExtensions.forEach(panelExt => {
|
||||
panelExt.module.components.forEach(comp => {
|
||||
if (comp.id === this.state.selectedRightSidePanel) {
|
||||
VisiblePanelRight = comp.component;
|
||||
} else if (comp.id === this.state.selectedLeftSidePanel) {
|
||||
VisiblePanelLeft = comp.component;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* HEADER */}
|
||||
<WhiteLabelingContext.Consumer>
|
||||
{whiteLabeling => (
|
||||
<UserManagerContext.Consumer>
|
||||
{userManager => (
|
||||
<AppContext.Consumer>
|
||||
{appContext => (
|
||||
<ConnectedHeader
|
||||
linkText={
|
||||
appContext.appConfig.showStudyList
|
||||
? 'Study List'
|
||||
: undefined
|
||||
}
|
||||
linkPath={
|
||||
appContext.appConfig.showStudyList ? '/' : undefined
|
||||
}
|
||||
userManager={userManager}
|
||||
>
|
||||
{whiteLabeling &&
|
||||
whiteLabeling.createLogoComponentFn &&
|
||||
whiteLabeling.createLogoComponentFn(React)}
|
||||
</ConnectedHeader>
|
||||
)}
|
||||
</AppContext.Consumer>
|
||||
)}
|
||||
</UserManagerContext.Consumer>
|
||||
)}
|
||||
</WhiteLabelingContext.Consumer>
|
||||
|
||||
{/* TOOLBAR */}
|
||||
<ToolbarRow
|
||||
isLeftSidePanelOpen={this.state.isLeftSidePanelOpen}
|
||||
isRightSidePanelOpen={this.state.isRightSidePanelOpen}
|
||||
selectedLeftSidePanel={
|
||||
this.state.isLeftSidePanelOpen
|
||||
? this.state.selectedLeftSidePanel
|
||||
: ''
|
||||
}
|
||||
selectedRightSidePanel={
|
||||
this.state.isRightSidePanelOpen
|
||||
? this.state.selectedRightSidePanel
|
||||
: ''
|
||||
}
|
||||
handleSidePanelChange={(side, selectedPanel) => {
|
||||
const sideClicked = side && side[0].toUpperCase() + side.slice(1);
|
||||
const openKey = `is${sideClicked}SidePanelOpen`;
|
||||
const selectedKey = `selected${sideClicked}SidePanel`;
|
||||
const updatedState = Object.assign({}, this.state);
|
||||
|
||||
const isOpen = updatedState[openKey];
|
||||
const prevSelectedPanel = updatedState[selectedKey];
|
||||
// RoundedButtonGroup returns `null` if selected button is clicked
|
||||
const isSameSelectedPanel =
|
||||
prevSelectedPanel === selectedPanel || selectedPanel === null;
|
||||
|
||||
updatedState[selectedKey] = selectedPanel || prevSelectedPanel;
|
||||
|
||||
const isClosedOrShouldClose = !isOpen || isSameSelectedPanel;
|
||||
if (isClosedOrShouldClose) {
|
||||
updatedState[openKey] = !updatedState[openKey];
|
||||
}
|
||||
|
||||
this.setState(updatedState);
|
||||
}}
|
||||
studies={this.props.studies}
|
||||
/>
|
||||
|
||||
{/*<ConnectedStudyLoadingMonitor studies={this.props.studies} />*/}
|
||||
{/*<StudyPrefetcher studies={this.props.studies} />*/}
|
||||
|
||||
{/* VIEWPORTS + SIDEPANELS */}
|
||||
<div className="FlexboxLayout">
|
||||
{/* LEFT */}
|
||||
<SidePanel from="left" isOpen={this.state.isLeftSidePanelOpen}>
|
||||
{VisiblePanelLeft ? (
|
||||
<VisiblePanelLeft
|
||||
viewports={this.props.viewports}
|
||||
studies={this.props.studies}
|
||||
activeIndex={this.props.activeViewportIndex}
|
||||
/>
|
||||
) : (
|
||||
<ConnectedStudyBrowser
|
||||
studies={this.state.thumbnails}
|
||||
studyMetadata={this.props.studies}
|
||||
/>
|
||||
)}
|
||||
</SidePanel>
|
||||
|
||||
{/* MAIN */}
|
||||
<div className={classNames('main-content')}>
|
||||
<ConnectedViewerMain
|
||||
studies={this.props.studies}
|
||||
isStudyLoaded={this.props.isStudyLoaded}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* RIGHT */}
|
||||
<SidePanel from="right" isOpen={this.state.isRightSidePanelOpen}>
|
||||
{VisiblePanelRight && (
|
||||
<VisiblePanelRight
|
||||
isOpen={this.state.isRightSidePanelOpen}
|
||||
viewports={this.props.viewports}
|
||||
studies={this.props.studies}
|
||||
activeIndex={this.props.activeViewportIndex}
|
||||
/>
|
||||
)}
|
||||
</SidePanel>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withDialog(Viewer);
|
||||
|
||||
/**
|
||||
* What types are these? Why do we have "mapping" dropped in here instead of in
|
||||
* a mapping layer?
|
||||
*
|
||||
* TODO[react]:
|
||||
* - Add sorting of display sets
|
||||
* - Add showStackLoadingProgressBar option
|
||||
*
|
||||
* @param {Study[]} studies
|
||||
* @param {DisplaySet[]} studies[].displaySets
|
||||
*/
|
||||
const _mapStudiesToThumbnails = function(studies) {
|
||||
return studies.map(study => {
|
||||
const { StudyInstanceUID } = study;
|
||||
|
||||
const thumbnails = study.displaySets.map(displaySet => {
|
||||
const {
|
||||
displaySetInstanceUID,
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
InstanceNumber,
|
||||
numImageFrames,
|
||||
} = displaySet;
|
||||
|
||||
let imageId;
|
||||
let altImageText;
|
||||
|
||||
if (displaySet.Modality && displaySet.Modality === 'SEG') {
|
||||
// TODO: We want to replace this with a thumbnail showing
|
||||
// the segmentation map on the image, but this is easier
|
||||
// and better than what we have right now.
|
||||
altImageText = 'SEG';
|
||||
} else if (displaySet.images && displaySet.images.length) {
|
||||
const imageIndex = Math.floor(displaySet.images.length / 2);
|
||||
|
||||
imageId = displaySet.images[imageIndex].getImageId();
|
||||
} else {
|
||||
altImageText = displaySet.Modality ? displaySet.Modality : 'UN';
|
||||
}
|
||||
|
||||
return {
|
||||
imageId,
|
||||
altImageText,
|
||||
displaySetInstanceUID,
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
InstanceNumber,
|
||||
numImageFrames,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
StudyInstanceUID,
|
||||
thumbnails,
|
||||
};
|
||||
});
|
||||
};
|
||||
@ -1,151 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { metadata, utils } from '@ohif/core';
|
||||
|
||||
import ConnectedViewer from './ConnectedViewer.js';
|
||||
import PropTypes from 'prop-types';
|
||||
import { extensionManager } from './../App.jsx';
|
||||
import Dropzone from 'react-dropzone';
|
||||
import filesToStudies from '../lib/filesToStudies';
|
||||
import './ViewerLocalFileData.css';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
const { OHIFStudyMetadata } = metadata;
|
||||
const { studyMetadataManager } = utils;
|
||||
|
||||
const dropZoneLinkDialog = (onDrop, i18n, dir) => {
|
||||
return (
|
||||
<Dropzone onDrop={onDrop} noDrag>
|
||||
{({ getRootProps, getInputProps }) => (
|
||||
<span {...getRootProps()} className="link-dialog">
|
||||
{dir ? (
|
||||
<span>
|
||||
{i18n('Load folders')}
|
||||
<input
|
||||
{...getInputProps()}
|
||||
webkitdirectory="true"
|
||||
mozdirectory="true"
|
||||
/>
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
{i18n('Load files')}
|
||||
<input {...getInputProps()} />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</Dropzone>
|
||||
);
|
||||
};
|
||||
|
||||
const linksDialogMessage = (onDrop, i18n) => {
|
||||
return (
|
||||
<>
|
||||
{i18n('Or click to ')}
|
||||
{dropZoneLinkDialog(onDrop, i18n)}
|
||||
{i18n(' or ')}
|
||||
{dropZoneLinkDialog(onDrop, i18n, true)}
|
||||
{i18n(' from dialog')}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
class ViewerLocalFileData extends Component {
|
||||
static propTypes = {
|
||||
studies: PropTypes.array,
|
||||
};
|
||||
|
||||
state = {
|
||||
studies: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
updateStudies = studies => {
|
||||
// Render the viewer when the data is ready
|
||||
studyMetadataManager.purge();
|
||||
|
||||
// Map studies to new format, update metadata manager?
|
||||
const updatedStudies = studies.map(study => {
|
||||
const studyMetadata = new OHIFStudyMetadata(
|
||||
study,
|
||||
study.StudyInstanceUID
|
||||
);
|
||||
const sopClassHandlerModules =
|
||||
extensionManager.modules['sopClassHandlerModule'];
|
||||
|
||||
study.displaySets =
|
||||
study.displaySets ||
|
||||
studyMetadata.createDisplaySets(sopClassHandlerModules);
|
||||
studyMetadata.setDisplaySets(study.displaySets);
|
||||
|
||||
studyMetadata.forEachDisplaySet(displayset => {
|
||||
displayset.localFile = true;
|
||||
});
|
||||
|
||||
studyMetadataManager.add(studyMetadata);
|
||||
|
||||
return study;
|
||||
});
|
||||
|
||||
this.setState({
|
||||
studies: updatedStudies,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const onDrop = async acceptedFiles => {
|
||||
this.setState({ loading: true });
|
||||
|
||||
const studies = await filesToStudies(acceptedFiles);
|
||||
const updatedStudies = this.updateStudies(studies);
|
||||
|
||||
if (!updatedStudies) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ studies: updatedStudies, loading: false });
|
||||
};
|
||||
|
||||
if (this.state.error) {
|
||||
return <div>Error: {JSON.stringify(this.state.error)}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropzone onDrop={onDrop} noClick>
|
||||
{({ getRootProps, getInputProps }) => (
|
||||
<div {...getRootProps()} style={{ width: '100%', height: '100%' }}>
|
||||
{this.state.studies ? (
|
||||
<ConnectedViewer
|
||||
studies={this.state.studies}
|
||||
studyInstanceUIDs={
|
||||
this.state.studies &&
|
||||
this.state.studies.map(a => a.StudyInstanceUID)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div className={'drag-drop-instructions'}>
|
||||
<div className={'drag-drop-contents'}>
|
||||
{this.state.loading ? (
|
||||
<h3>{this.props.t('Loading...')}</h3>
|
||||
) : (
|
||||
<>
|
||||
<h3>
|
||||
{this.props.t(
|
||||
'Drag and Drop DICOM files here to load them in the Viewer'
|
||||
)}
|
||||
</h3>
|
||||
<h4>{linksDialogMessage(onDrop, this.props.t)}</h4>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Dropzone>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation('Common')(ViewerLocalFileData);
|
||||
@ -1,193 +0,0 @@
|
||||
import './ViewerMain.css';
|
||||
|
||||
import { Component } from 'react';
|
||||
import { ConnectedViewportGrid } from './../components/ViewportGrid/index.js';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import memoize from 'lodash/memoize';
|
||||
import _values from 'lodash/values';
|
||||
|
||||
var values = memoize(_values);
|
||||
|
||||
class ViewerMain extends Component {
|
||||
static propTypes = {
|
||||
activeViewportIndex: PropTypes.number.isRequired,
|
||||
studies: PropTypes.array,
|
||||
viewportSpecificData: PropTypes.object.isRequired,
|
||||
layout: PropTypes.object.isRequired,
|
||||
setViewportSpecificData: PropTypes.func.isRequired,
|
||||
clearViewportSpecificData: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
displaySets: [],
|
||||
};
|
||||
}
|
||||
|
||||
getDisplaySets(studies) {
|
||||
const displaySets = [];
|
||||
studies.forEach(study => {
|
||||
study.displaySets.forEach(dSet => {
|
||||
if (!dSet.plugin) {
|
||||
dSet.plugin = 'cornerstone';
|
||||
}
|
||||
displaySets.push(dSet);
|
||||
});
|
||||
});
|
||||
|
||||
return displaySets;
|
||||
}
|
||||
|
||||
findDisplaySet(studies, StudyInstanceUID, displaySetInstanceUID) {
|
||||
const study = studies.find(study => {
|
||||
return study.StudyInstanceUID === StudyInstanceUID;
|
||||
});
|
||||
|
||||
if (!study) {
|
||||
return;
|
||||
}
|
||||
|
||||
return study.displaySets.find(displaySet => {
|
||||
return displaySet.displaySetInstanceUID === displaySetInstanceUID;
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// Add beforeUnload event handler to check for unsaved changes
|
||||
//window.addEventListener('beforeunload', unloadHandlers.beforeUnload);
|
||||
|
||||
// Get all the display sets for the viewer studies
|
||||
if (this.props.studies) {
|
||||
const displaySets = this.getDisplaySets(this.props.studies);
|
||||
this.setState({ displaySets }, this.fillEmptyViewportPanes);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const prevViewportAmount = prevProps.layout.viewports.length;
|
||||
const viewportAmount = this.props.layout.viewports.length;
|
||||
const isVtk = this.props.layout.viewports.some(vp => !!vp.vtk);
|
||||
|
||||
if (
|
||||
this.props.studies !== prevProps.studies ||
|
||||
(viewportAmount !== prevViewportAmount && !isVtk)
|
||||
) {
|
||||
const displaySets = this.getDisplaySets(this.props.studies);
|
||||
this.setState({ displaySets }, this.fillEmptyViewportPanes);
|
||||
}
|
||||
}
|
||||
|
||||
fillEmptyViewportPanes = () => {
|
||||
// TODO: Here is the entry point for filling viewports on load.
|
||||
const dirtyViewportPanes = [];
|
||||
const { layout, viewportSpecificData } = this.props;
|
||||
const { displaySets } = this.state;
|
||||
|
||||
if (!displaySets || !displaySets.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < layout.viewports.length; i++) {
|
||||
const viewportPane = viewportSpecificData[i];
|
||||
const isNonEmptyViewport =
|
||||
viewportPane &&
|
||||
viewportPane.StudyInstanceUID &&
|
||||
viewportPane.displaySetInstanceUID;
|
||||
|
||||
if (isNonEmptyViewport) {
|
||||
dirtyViewportPanes.push({
|
||||
StudyInstanceUID: viewportPane.StudyInstanceUID,
|
||||
displaySetInstanceUID: viewportPane.displaySetInstanceUID,
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const foundDisplaySet =
|
||||
displaySets.find(
|
||||
ds =>
|
||||
!dirtyViewportPanes.some(
|
||||
v => v.displaySetInstanceUID === ds.displaySetInstanceUID
|
||||
)
|
||||
) || displaySets[displaySets.length - 1];
|
||||
|
||||
dirtyViewportPanes.push(foundDisplaySet);
|
||||
}
|
||||
|
||||
dirtyViewportPanes.forEach((vp, i) => {
|
||||
if (vp && vp.StudyInstanceUID) {
|
||||
this.setViewportData({
|
||||
viewportIndex: i,
|
||||
StudyInstanceUID: vp.StudyInstanceUID,
|
||||
displaySetInstanceUID: vp.displaySetInstanceUID,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
setViewportData = ({
|
||||
viewportIndex,
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
}) => {
|
||||
const displaySet = this.findDisplaySet(
|
||||
this.props.studies,
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
this.props.setViewportSpecificData(viewportIndex, displaySet);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { viewportSpecificData } = this.props;
|
||||
const viewportData = values(viewportSpecificData);
|
||||
|
||||
return (
|
||||
<div className="ViewerMain">
|
||||
{this.state.displaySets.length && (
|
||||
<ConnectedViewportGrid
|
||||
isStudyLoaded={this.props.isStudyLoaded}
|
||||
studies={this.props.studies}
|
||||
viewportData={viewportData}
|
||||
setViewportData={this.setViewportData}
|
||||
>
|
||||
{/* Children to add to each viewport that support children */}
|
||||
</ConnectedViewportGrid>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// Clear the entire viewport specific data
|
||||
const { viewportSpecificData } = this.props;
|
||||
Object.keys(viewportSpecificData).forEach(viewportIndex => {
|
||||
this.props.clearViewportSpecificData(viewportIndex);
|
||||
});
|
||||
|
||||
// TODO: These don't have to be viewer specific?
|
||||
// Could qualify for other routes?
|
||||
// hotkeys.destroy();
|
||||
|
||||
// Remove beforeUnload event handler...
|
||||
//window.removeEventListener('beforeunload', unloadHandlers.beforeUnload);
|
||||
// Destroy the synchronizer used to update reference lines
|
||||
//OHIF.viewer.updateImageSynchronizer.destroy();
|
||||
// TODO: Instruct all plugins to clean up themselves
|
||||
//
|
||||
// Clear references to all stacks in the StackManager
|
||||
//StackManager.clearStacks();
|
||||
// @TypeSafeStudies
|
||||
// Clears OHIF.viewer.Studies collection
|
||||
//OHIF.viewer.Studies.removeAll();
|
||||
// @TypeSafeStudies
|
||||
// Clears OHIF.viewer.StudyMetadataList collection
|
||||
//OHIF.viewer.StudyMetadataList.removeAll();
|
||||
}
|
||||
}
|
||||
|
||||
export default ViewerMain;
|
||||
@ -1,375 +0,0 @@
|
||||
import React, { useState, useEffect, useContext } from 'react';
|
||||
import { metadata, studies, utils, log } from '@ohif/core';
|
||||
import usePrevious from '../customHooks/usePrevious';
|
||||
|
||||
import ConnectedViewer from './ConnectedViewer.js';
|
||||
import PropTypes from 'prop-types';
|
||||
import { extensionManager } from './../App.jsx';
|
||||
import { useSnackbarContext } from '@ohif/ui';
|
||||
|
||||
const { OHIFStudyMetadata, OHIFSeriesMetadata } = metadata;
|
||||
const { retrieveStudiesMetadata, deleteStudyMetadataPromise } = studies;
|
||||
const { studyMetadataManager, makeCancelable } = utils;
|
||||
|
||||
// Contexts
|
||||
import AppContext from '../context/AppContext';
|
||||
|
||||
const _promoteToFront = (list, value, searchMethod) => {
|
||||
let response = [...list];
|
||||
let promoted = false;
|
||||
const index = response.findIndex(searchMethod.bind(undefined, value));
|
||||
|
||||
if (index > 0) {
|
||||
const first = response.splice(index, 1);
|
||||
response = [...first, ...response];
|
||||
}
|
||||
|
||||
if (index >= 0) {
|
||||
promoted = true;
|
||||
}
|
||||
|
||||
return {
|
||||
promoted,
|
||||
data: response,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Promote series to front if find found equivalent on filters object
|
||||
* @param {Object} study - study reference to promote series against
|
||||
* @param {Object} [filters] - Object containing filters to be applied
|
||||
* @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against
|
||||
* @param {boolean} isFilterStrategy - if filtering by query param strategy ON
|
||||
*/
|
||||
const _promoteList = (study, studyMetadata, filters, isFilterStrategy) => {
|
||||
let promoted = false;
|
||||
// Promote only if no filter should be applied
|
||||
if (!isFilterStrategy) {
|
||||
_sortStudyDisplaySet(study, studyMetadata);
|
||||
promoted = _promoteStudyDisplaySet(study, studyMetadata, filters);
|
||||
}
|
||||
|
||||
return promoted;
|
||||
};
|
||||
|
||||
const _promoteStudyDisplaySet = (study, studyMetadata, filters) => {
|
||||
let promoted = false;
|
||||
const queryParamsLength = Object.keys(filters).length;
|
||||
const shouldPromoteToFront = queryParamsLength > 0;
|
||||
|
||||
if (shouldPromoteToFront) {
|
||||
const { seriesInstanceUID } = filters;
|
||||
|
||||
const _seriesLookup = (valueToCompare, displaySet) => {
|
||||
return displaySet.SeriesInstanceUID === valueToCompare;
|
||||
};
|
||||
const promotedResponse = _promoteToFront(
|
||||
studyMetadata.getDisplaySets(),
|
||||
seriesInstanceUID,
|
||||
_seriesLookup
|
||||
);
|
||||
|
||||
study.displaySets = promotedResponse.data;
|
||||
promoted = promotedResponse.promoted;
|
||||
}
|
||||
|
||||
return promoted;
|
||||
};
|
||||
|
||||
/**
|
||||
* Method to identify if query param (from url) was applied to given list
|
||||
* @param {Object} study - study reference to promote series against
|
||||
* @param {Object} [filters] - Object containing filters to be applied
|
||||
* @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against
|
||||
* @param {boolean} isFilterStrategy - if filtering by query param strategy ON
|
||||
*/
|
||||
const _isQueryParamApplied = (study, filters = {}, isFilterStrategy) => {
|
||||
const { seriesInstanceUID } = filters;
|
||||
let applied = true;
|
||||
// skip in case no filter or no toast manager
|
||||
if (!seriesInstanceUID) {
|
||||
return applied;
|
||||
}
|
||||
|
||||
const { series = [], displaySets = [] } = study;
|
||||
const firstSeries = isFilterStrategy ? series[0] : displaySets[0];
|
||||
|
||||
if (!firstSeries || firstSeries.SeriesInstanceUID !== seriesInstanceUID) {
|
||||
applied = false;
|
||||
}
|
||||
|
||||
return applied;
|
||||
};
|
||||
const _showUserMessage = (queryParamApplied, message, dialog = {}) => {
|
||||
if (queryParamApplied) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { show: showUserMessage = () => {} } = dialog;
|
||||
showUserMessage({
|
||||
message,
|
||||
});
|
||||
};
|
||||
|
||||
const _addSeriesToStudy = (studyMetadata, series) => {
|
||||
const sopClassHandlerModules =
|
||||
extensionManager.modules['sopClassHandlerModule'];
|
||||
const study = studyMetadata.getData();
|
||||
const seriesMetadata = new OHIFSeriesMetadata(series, study);
|
||||
const existingSeries = studyMetadata.getSeriesByUID(series.SeriesInstanceUID);
|
||||
if (existingSeries) {
|
||||
studyMetadata.updateSeries(series.SeriesInstanceUID, seriesMetadata);
|
||||
} else {
|
||||
studyMetadata.addSeries(seriesMetadata);
|
||||
}
|
||||
|
||||
studyMetadata.createAndAddDisplaySetsForSeries(
|
||||
sopClassHandlerModules,
|
||||
seriesMetadata
|
||||
);
|
||||
study.displaySets = studyMetadata.getDisplaySets();
|
||||
_updateStudyMetadataManager(study, studyMetadata);
|
||||
};
|
||||
|
||||
const _updateStudyMetadataManager = (study, studyMetadata) => {
|
||||
const { StudyInstanceUID } = study;
|
||||
|
||||
if (!studyMetadataManager.get(StudyInstanceUID)) {
|
||||
studyMetadataManager.add(studyMetadata);
|
||||
}
|
||||
};
|
||||
|
||||
const _updateStudyDisplaySets = (study, studyMetadata) => {
|
||||
const sopClassHandlerModules =
|
||||
extensionManager.modules['sopClassHandlerModule'];
|
||||
|
||||
if (!study.displaySets) {
|
||||
study.displaySets = studyMetadata.createDisplaySets(sopClassHandlerModules);
|
||||
}
|
||||
|
||||
studyMetadata.setDisplaySets(study.displaySets);
|
||||
};
|
||||
|
||||
const _sortStudyDisplaySet = (study, studyMetadata) => {
|
||||
studyMetadata.sortDisplaySets(study.displaySets);
|
||||
};
|
||||
|
||||
function ViewerRetrieveStudyData({
|
||||
server,
|
||||
studyInstanceUIDs,
|
||||
seriesInstanceUIDs,
|
||||
clearViewportSpecificData,
|
||||
}) {
|
||||
// hooks
|
||||
const [error, setError] = useState(false);
|
||||
const [studies, setStudies] = useState([]);
|
||||
const [isStudyLoaded, setIsStudyLoaded] = useState(false);
|
||||
const snackbarContext = useSnackbarContext();
|
||||
const { appConfig = {} } = useContext(AppContext);
|
||||
const {
|
||||
filterQueryParam: isFilterStrategy = false,
|
||||
maxConcurrentMetadataRequests,
|
||||
} = appConfig;
|
||||
|
||||
let cancelableSeriesPromises;
|
||||
let cancelableStudiesPromises;
|
||||
/**
|
||||
* Callback method when study is totally loaded
|
||||
* @param {object} study study loaded
|
||||
* @param {object} studyMetadata studyMetadata for given study
|
||||
* @param {Object} [filters] - Object containing filters to be applied
|
||||
* @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against
|
||||
*/
|
||||
const studyDidLoad = (study, studyMetadata, filters) => {
|
||||
// User message
|
||||
const promoted = _promoteList(
|
||||
study,
|
||||
studyMetadata,
|
||||
filters,
|
||||
isFilterStrategy
|
||||
);
|
||||
|
||||
// Clear viewport to allow new promoted one to be displayed
|
||||
if (promoted) {
|
||||
clearViewportSpecificData(0);
|
||||
}
|
||||
|
||||
const isQueryParamApplied = _isQueryParamApplied(
|
||||
study,
|
||||
filters,
|
||||
isFilterStrategy
|
||||
);
|
||||
// Show message in case not promoted neither filtered but should to
|
||||
_showUserMessage(
|
||||
isQueryParamApplied,
|
||||
'Query parameters were not applied. Using original series list for given study.',
|
||||
snackbarContext
|
||||
);
|
||||
|
||||
setStudies([...studies, study]);
|
||||
setIsStudyLoaded(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Method to process studies. It will update displaySet, studyMetadata, load remaining series, ...
|
||||
* @param {Array} studiesData Array of studies retrieved from server
|
||||
* @param {Object} [filters] - Object containing filters to be applied
|
||||
* @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against
|
||||
*/
|
||||
const processStudies = (studiesData, filters) => {
|
||||
if (Array.isArray(studiesData) && studiesData.length > 0) {
|
||||
// Map studies to new format, update metadata manager?
|
||||
const studies = studiesData.map(study => {
|
||||
const studyMetadata = new OHIFStudyMetadata(
|
||||
study,
|
||||
study.StudyInstanceUID
|
||||
);
|
||||
|
||||
_updateStudyDisplaySets(study, studyMetadata);
|
||||
_updateStudyMetadataManager(study, studyMetadata);
|
||||
|
||||
// Attempt to load remaning series if any
|
||||
cancelableSeriesPromises[study.StudyInstanceUID] = makeCancelable(
|
||||
loadRemainingSeries(studyMetadata)
|
||||
)
|
||||
.then(result => {
|
||||
if (result && !result.isCanceled) {
|
||||
studyDidLoad(study, studyMetadata, filters);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (error && !error.isCanceled) {
|
||||
setError(true);
|
||||
log.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
return study;
|
||||
});
|
||||
|
||||
setStudies(studies);
|
||||
}
|
||||
};
|
||||
|
||||
const forceRerender = () => setStudies(studies => [...studies]);
|
||||
|
||||
const loadRemainingSeries = async studyMetadata => {
|
||||
const { seriesLoader } = studyMetadata.getData();
|
||||
if (!seriesLoader) return;
|
||||
|
||||
const loadNextSeries = async () => {
|
||||
if (!seriesLoader.hasNext()) return;
|
||||
const series = await seriesLoader.next();
|
||||
_addSeriesToStudy(studyMetadata, series);
|
||||
forceRerender();
|
||||
return loadNextSeries();
|
||||
};
|
||||
|
||||
const concurrentRequestsAllowed =
|
||||
maxConcurrentMetadataRequests || studyMetadata.getSeriesCount();
|
||||
const promises = Array(concurrentRequestsAllowed)
|
||||
.fill(null)
|
||||
.map(loadNextSeries);
|
||||
|
||||
return await Promise.all(promises);
|
||||
};
|
||||
|
||||
const loadStudies = async () => {
|
||||
try {
|
||||
const filters = {};
|
||||
// Use the first, discard others
|
||||
const seriesInstanceUID = seriesInstanceUIDs && seriesInstanceUIDs[0];
|
||||
|
||||
const retrieveParams = [server, studyInstanceUIDs];
|
||||
|
||||
if (seriesInstanceUID) {
|
||||
filters.seriesInstanceUID = seriesInstanceUID;
|
||||
// Query param filtering controlled by appConfig property
|
||||
if (isFilterStrategy) {
|
||||
retrieveParams.push(filters);
|
||||
}
|
||||
}
|
||||
|
||||
cancelableStudiesPromises[studyInstanceUIDs] = makeCancelable(
|
||||
retrieveStudiesMetadata(...retrieveParams)
|
||||
)
|
||||
.then(result => {
|
||||
if (result && !result.isCanceled) {
|
||||
processStudies(result, filters);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (error && !error.isCanceled) {
|
||||
setError(true);
|
||||
log.error(error);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
if (error) {
|
||||
setError(true);
|
||||
log.error(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const purgeCancellablePromises = () => {
|
||||
for (let studyInstanceUIDs in cancelableStudiesPromises) {
|
||||
if ('cancel' in cancelableStudiesPromises[studyInstanceUIDs]) {
|
||||
cancelableStudiesPromises[studyInstanceUIDs].cancel();
|
||||
}
|
||||
}
|
||||
|
||||
for (let studyInstanceUIDs in cancelableSeriesPromises) {
|
||||
if ('cancel' in cancelableSeriesPromises[studyInstanceUIDs]) {
|
||||
cancelableSeriesPromises[studyInstanceUIDs].cancel();
|
||||
deleteStudyMetadataPromise(studyInstanceUIDs);
|
||||
studyMetadataManager.remove(studyInstanceUIDs);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const prevStudyInstanceUIDs = usePrevious(studyInstanceUIDs);
|
||||
|
||||
useEffect(() => {
|
||||
const hasStudyInstanceUIDsChanged = !(
|
||||
prevStudyInstanceUIDs &&
|
||||
prevStudyInstanceUIDs.every(e => studyInstanceUIDs.includes(e))
|
||||
);
|
||||
|
||||
if (hasStudyInstanceUIDsChanged) {
|
||||
studyMetadataManager.purge();
|
||||
purgeCancellablePromises();
|
||||
}
|
||||
}, [studyInstanceUIDs]);
|
||||
|
||||
useEffect(() => {
|
||||
cancelableSeriesPromises = {};
|
||||
cancelableStudiesPromises = {};
|
||||
loadStudies();
|
||||
|
||||
return () => {
|
||||
purgeCancellablePromises();
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (error) {
|
||||
return <div>Error: {JSON.stringify(error)}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ConnectedViewer
|
||||
studies={studies}
|
||||
isStudyLoaded={isStudyLoaded}
|
||||
studyInstanceUIDs={studyInstanceUIDs}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
ViewerRetrieveStudyData.propTypes = {
|
||||
studyInstanceUIDs: PropTypes.array.isRequired,
|
||||
seriesInstanceUIDs: PropTypes.array,
|
||||
server: PropTypes.object,
|
||||
clearViewportSpecificData: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ViewerRetrieveStudyData;
|
||||
@ -1,25 +0,0 @@
|
||||
/**
|
||||
* Finds displaySet by UID across all displaySets inside studyMetadata
|
||||
* @param {Array} studyMetadata
|
||||
* @param {string} displaySetInstanceUID
|
||||
*/
|
||||
export default function findDisplaySetByUID(
|
||||
studyMetadata,
|
||||
displaySetInstanceUID
|
||||
) {
|
||||
if (!Array.isArray(studyMetadata)) return null;
|
||||
|
||||
const allDisplaySets = studyMetadata.reduce((all, current) => {
|
||||
let currentDisplaySet = [];
|
||||
if (current && Array.isArray(current.displaySets)) {
|
||||
currentDisplaySet = current.displaySets;
|
||||
}
|
||||
return all.concat(currentDisplaySet);
|
||||
}, []);
|
||||
|
||||
const bySetInstanceUID = ds =>
|
||||
ds.displaySetInstanceUID === displaySetInstanceUID;
|
||||
|
||||
const displaySet = allDisplaySets.find(bySetInstanceUID);
|
||||
return displaySet || null;
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
import findDisplaySetByUID from './findDisplaySetByUID';
|
||||
|
||||
describe('findDisplaySetByUID', () => {
|
||||
test('returns null when studyMetadata isnt an array', () => {
|
||||
const result = findDisplaySetByUID(undefined, 'hello');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('returns null when no match found', () => {
|
||||
const result = findDisplaySetByUID([], 'no-match');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('it handles missing displaySet arrays', () => {
|
||||
const expected = '9388-2291-a8fe';
|
||||
const studyMetadata = [
|
||||
{ displaySets: null },
|
||||
{
|
||||
displaySets: [{ displaySetInstanceUID: expected }],
|
||||
},
|
||||
null,
|
||||
7,
|
||||
];
|
||||
const result = findDisplaySetByUID(studyMetadata, expected);
|
||||
expect(result.displaySetInstanceUID).toBe(expected);
|
||||
});
|
||||
|
||||
test('returns correct displaySet by UID', () => {
|
||||
const expected = '1234-5678';
|
||||
const studyMetadata = [
|
||||
{ displaySets: [{ displaySetInstanceUID: '0011-2239' }] },
|
||||
{
|
||||
displaySets: [
|
||||
{ displaySetInstanceUID: '0392-2211' },
|
||||
{ displaySetInstanceUID: expected },
|
||||
],
|
||||
},
|
||||
{ displaySets: [{ displaySetInstanceUID: '3384-9933' }] },
|
||||
];
|
||||
const result = findDisplaySetByUID(studyMetadata, expected);
|
||||
expect(result.displaySetInstanceUID).toBe(expected);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user