Pull in master

This commit is contained in:
James A. Petts 2020-06-15 09:23:35 +01:00
commit e7501933dd
47 changed files with 879 additions and 3000 deletions

View File

@ -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 }];
};

View File

@ -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>;
}

View File

@ -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: {

View File

@ -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 });
},

View File

@ -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 [];

View 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" />
);
}

View 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;

View File

@ -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;

View File

@ -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;

View File

@ -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 */

View File

@ -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;

View File

@ -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);
},
},
];
}

View File

@ -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,

View File

@ -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;

View File

@ -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,

View File

@ -128,7 +128,9 @@ const definitions = [
},
];
export default {
definitions,
defaultContext: 'ACTIVE_VIEWPORT::VTK',
};
export default [];
// export default {
// definitions,
// defaultContext: 'ACTIVE_VIEWPORT::VTK',
// };

View File

@ -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 {

View File

@ -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 {

View 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',
},
},
];

View File

@ -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) {

View File

@ -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 }), //
};
}
}

View File

@ -41,6 +41,7 @@ export {
InputMultiSelect,
InputText,
Label,
LayoutSelector,
MeasurementTable,
Modal,
NavBar,
@ -67,7 +68,6 @@ export {
ThumbnailNoImage,
ThumbnailTracked,
ThumbnailList,
Toolbar,
ToolbarButton,
Tooltip,
Typography,

View 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;

View File

@ -0,0 +1,2 @@
import LayoutSelector from './LayoutSelector';
export default LayoutSelector;

View File

@ -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;

View File

@ -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} />

View File

@ -1,2 +0,0 @@
import Toolbar from './Toolbar';
export default Toolbar;

View File

@ -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;

View File

@ -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;

View File

@ -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;
}

View File

@ -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

View File

@ -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,

View File

@ -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>

View File

@ -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">

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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)))
);

View File

@ -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,
};
});
};

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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;
}

View File

@ -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);
});
});