feat: extension panels

* Snapshot -- Switching branches
* fix classes and hooks for panel componet
* Restore button functionality
* feat: support for expandableToolMenu
* Shift cornerstoneTools config up a layer, and use globalToolSync
* Remaining initCornerstoneTools changes
* Pull in Segmentation Plugin
* Shift MeasurementsPanel to an extension
* Note regarding where data is coming from
* Make sure measurement callback is available
* Bump core version
* Shift MeasurementsTable to a local extensions directory
* Register appCommands as an extension
* Update package dependencies and yarn lock
* Support for panel width
* Ability to pass props to our panel component
* Add a safety check around calling our tacked on method for tool options
* Left sidebar plugins + fix activeIndex prop passed to sidebar.
* fix: viewer height
Now in a shared container w/ top bar, so we need to factor that in when calculating height
* lock file latest
* Update thumbnails if studies has changes
* bump cornerstone version to resolve globalToolSynch history issue
* Default panel to open; studies optional
* Bump minor version for ohif-cornestone-extension
* Simplify button logic
* Accommodate odd roundedButtonGroup value change emit
* Map viewers + selectedViewport instead of pulling off the window
This commit is contained in:
Danny Brown 2019-07-04 14:09:13 -04:00 committed by GitHub
parent 514609708d
commit 5ac9048ef0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 554 additions and 463 deletions

View File

@ -127,7 +127,7 @@ through the trouble of using AWS/GCP/Azure, it's more likely you're doing so to
avoid using a proxy or to simplify authentication.
If that is the case, check out some of our more advanced `docker` deployments
that target these providers from the left-hand sidebar.
that target these providers from the left-hand sidepanel.
These guides can be a bit longer and a update more frequently. To provide
accurate documentation, we will link to each provider's own recommended steps:

View File

@ -7,12 +7,28 @@ export default {
*/
id: 'example-extension',
getViewportModule() {},
/**
* LIFECYCLE HOOKS
*/
preRegistration(extensionConfiguration) {},
/**
* MODULE GETTERS
*/
getViewportModule() {
return '... react component ...';
},
getSopClassHandlerModule() {
return sopClassHandlerModule;
},
getPanelModule() {},
getToolbarModule() {},
getPanelModule() {
return panelModule;
},
getToolbarModule() {
return panelModule;
},
getCommandsModule(/* store */) {
return commandsModule;
},
@ -67,3 +83,23 @@ const sopClassHandlerModule = {
};
},
};
/**
*
*/
const panelModule = {
menuOptions: [
{
icon: 'th-list',
label: 'Segments',
target: 'segment-panel',
},
],
components: [
{
id: 'segment-panel',
component: '... react component ...',
},
],
defaultContext: ['VIEWER'],
};

View File

@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone",
"version": "0.0.37",
"version": "0.0.38",
"description": "OHIF extension for Cornerstone",
"author": "OHIF",
"license": "MIT",

View File

@ -12,6 +12,15 @@ const mapStateToProps = (state, ownProps) => {
dataFromStore = state.extensions.cornerstone;
}
// TODO: This should be extension configuration
// ...dataFromStore -->
// availableTools,
// onNewImage,
// onRightClick,
// onTouchPress,
// onTouchStart,
// onMouseClick,
// If this is the active viewport, enable prefetching.
const { viewportIndex } = ownProps; //.viewportData;
const isActive = viewportIndex === state.viewports.activeViewportIndex;

View File

@ -1,5 +1,3 @@
import './config';
import React, { Component } from 'react';
import ConnectedCornerstoneViewport from './ConnectedCornerstoneViewport';
@ -20,7 +18,7 @@ cornerstone.metaData.addProvider(
StackManager.setMetadataProvider(metadataProvider);
const SOP_CLASSES = {
SEGMENTATION_STORAGE: '1.2.840.10008.5.1.4.1.1.66.4'
SEGMENTATION_STORAGE: '1.2.840.10008.5.1.4.1.1.66.4',
};
const specialCaseHandlers = {};
@ -30,11 +28,11 @@ specialCaseHandlers[
class OHIFCornerstoneViewport extends Component {
state = {
viewportData: null
viewportData: null,
};
static defaultProps = {
customProps: {}
customProps: {},
};
static propTypes = {
@ -42,7 +40,7 @@ class OHIFCornerstoneViewport extends Component {
displaySet: PropTypes.object,
viewportIndex: PropTypes.number,
children: PropTypes.node,
customProps: PropTypes.object
customProps: PropTypes.object,
};
static id = 'OHIFCornerstoneViewport';
@ -185,7 +183,7 @@ class OHIFCornerstoneViewport extends Component {
viewportData = {
studyInstanceUid,
displaySetInstanceUid,
stack
stack,
};
break;
@ -201,7 +199,7 @@ class OHIFCornerstoneViewport extends Component {
displaySetInstanceUid,
sopClassUids,
sopInstanceUid,
frameIndex
frameIndex,
} = displaySet;
if (sopClassUids && sopClassUids.length > 1) {
@ -221,7 +219,7 @@ class OHIFCornerstoneViewport extends Component {
frameIndex
).then(viewportData => {
this.setState({
viewportData
viewportData,
});
});
}
@ -252,7 +250,7 @@ class OHIFCornerstoneViewport extends Component {
childrenWithProps = this.props.children.map((child, index) => {
return React.cloneElement(child, {
viewportIndex: this.props.viewportIndex,
key: index
key: index,
});
});
}

View File

@ -1,25 +0,0 @@
import Hammer from 'hammerjs';
import cornerstone from 'cornerstone-core';
import cornerstoneMath from 'cornerstone-math';
import cornerstoneTools from 'cornerstone-tools';
// For debugging
window.cornerstoneTools = cornerstoneTools;
cornerstoneTools.external.cornerstone = cornerstone;
cornerstoneTools.external.Hammer = Hammer;
cornerstoneTools.external.cornerstoneMath = cornerstoneMath;
cornerstoneTools.init();
// Set the tool font and font size
// context.font = "[style] [variant] [weight] [size]/[line height] [font family]";
const fontFamily =
'Roboto, OpenSans, HelveticaNeue-Light, Helvetica Neue Light, Helvetica Neue, Helvetica, Arial, Lucida Grande, sans-serif';
cornerstoneTools.textStyle.setFont(`16px ${fontFamily}`);
// Tool styles/colors
cornerstoneTools.toolStyle.setToolWidth(2);
cornerstoneTools.toolColors.setToolColor('rgb(255, 255, 0)');
cornerstoneTools.toolColors.setActiveColor('rgb(0, 255, 0)');
cornerstoneTools.store.state.touchProximity = 40;

Binary file not shown.

View File

@ -76,7 +76,7 @@
},
"dependencies": {
"@babel/runtime": "^7.4.5",
"@ohif/extension-cornerstone": "0.0.37",
"@ohif/extension-cornerstone": "0.0.38",
"@ohif/extension-dicom-html": "0.0.3",
"@ohif/extension-dicom-microscopy": "0.0.8",
"@ohif/extension-dicom-pdf": "0.0.7",
@ -86,7 +86,7 @@
"classnames": "^2.2.6",
"cornerstone-core": "^2.2.8",
"cornerstone-math": "^0.1.8",
"cornerstone-tools": "^3.13.0",
"cornerstone-tools": "^3.15.1",
"cornerstone-wado-image-loader": "^2.2.3",
"dcmjs": "^0.4.7",
"dicom-parser": "^1.8.3",

View File

@ -17,24 +17,32 @@ import {
} from './utils/index.js';
import { I18nextProvider } from 'react-i18next';
import initCornerstoneTools from './initCornerstoneTools.js';
// ~~ EXTENSIONS
import { GenericViewerCommands, MeasurementsPanel } from './appExtensions';
import OHIFCornerstoneExtension from '@ohif/extension-cornerstone';
import OHIFDicomHtmlExtension from '@ohif/extension-dicom-html';
import OHIFDicomMicroscopyExtension from '@ohif/extension-dicom-microscopy';
import OHIFDicomPDFExtension from '@ohif/extension-dicom-pdf';
import OHIFStandaloneViewer from './OHIFStandaloneViewer';
import OHIFVTKExtension from '@ohif/extension-vtk';
// ~~ EXTENSIONS
import { OidcProvider } from 'redux-oidc';
import PropTypes from 'prop-types';
import { Provider } from 'react-redux';
import { BrowserRouter as Router } from 'react-router-dom';
import WhiteLabellingContext from './WhiteLabellingContext';
import appCommands from './appCommands';
import { getActiveContexts } from './store/layout/selectors.js';
import i18n from '@ohif/i18n';
import setupTools from './setupTools';
import setupTools from './setupTools.js';
import store from './store';
// ~~~~ APP SETUP
initCornerstoneTools({
globalToolSyncEnabled: true,
});
const commandsManagerConfig = {
getAppState: () => store.getState(),
getActiveContexts: () => getActiveContexts(store.getState()),
@ -44,22 +52,16 @@ const commandsManager = new CommandsManager(commandsManagerConfig);
const hotkeysManager = new HotkeysManager(commandsManager);
const extensionManager = new ExtensionManager({ commandsManager });
// TODO: Should be done in extensions w/ commandsModule
// ~~ ADD COMMANDS
appCommands.init(commandsManager);
if (window.config.hotkeys) {
hotkeysManager.setHotkeys(window.config.hotkeys, true);
}
// ~~~~ END APP SETUP
// CornerstoneTools and labeling/measurements?
setupTools(store);
// const children = {
// viewport: [<ConnectedToolContextMenu key="tool-context" />],
// };
// ~~~~ END APP SETUP
/** TODO: extensions should be passed in as prop as soon as we have the extensions as separate packages and then registered by ExtensionsManager */
extensionManager.registerExtensions([
// Core
GenericViewerCommands,
MeasurementsPanel,
//
OHIFCornerstoneExtension,
OHIFVTKExtension,
OHIFDicomPDFExtension,
@ -67,6 +69,11 @@ extensionManager.registerExtensions([
OHIFDicomMicroscopyExtension,
]);
// Must run after extension commands are registered
if (window.config.hotkeys) {
hotkeysManager.setHotkeys(window.config.hotkeys, true);
}
// TODO[react] Use a provider when the whole tree is React
window.store = store;

View File

@ -1 +0,0 @@
# Commands

View File

@ -1,37 +0,0 @@
import viewerCommandDefinitions from './viewer.js';
const CONTEXTS = {
viewer: 'VIEWER',
};
/**
* Register all commands.
* TODO: Extensions should self-register their commands
*/
function init(commandsManager) {
_registerViewerCommands(commandsManager);
}
/**
* Register all Viewer commands
*
* @private
*/
function _registerViewerCommands(commandsManager) {
const commandContext = CONTEXTS.viewer;
commandsManager.createContext(commandContext);
Object.keys(viewerCommandDefinitions).forEach(commandName => {
const commandDefinition = viewerCommandDefinitions[commandName];
commandsManager.registerCommand(
commandContext,
commandName,
commandDefinition
);
});
}
export default {
init,
};

View File

@ -1,11 +1,11 @@
import { redux } from 'ohif-core';
import store from './../store';
import store from './../../store';
const { setViewportActive } = redux.actions;
const actions = {
updateViewportDisplaySet: ({ direction }) => {
// TODO
console.warn('updateDisplaySet: ', direction);
// console.warn('updateDisplaySet: ', direction);
},
updateActiveViewport: ({ viewports, direction }) => {
const { viewportSpecificData, activeViewportIndex } = viewports;
@ -33,4 +33,7 @@ const definitions = {
},
};
export default definitions;
export default {
definitions,
defaultContext: 'VIEWER',
};

View File

@ -0,0 +1,8 @@
import commandsModule from './commandsModule.js';
export default {
id: 'generic-viewer-commands',
getCommandsModule() {
return commandsModule;
},
};

View File

@ -3,8 +3,10 @@ import { MeasurementTable } from 'react-viewerbase';
import OHIF from 'ohif-core';
import moment from 'moment';
import cornerstone from 'cornerstone-core';
import jumpToRowItem from '../lib/jumpToRowItem.js';
import getMeasurementLocationCallback from '../lib/getMeasurementLocationCallback';
//
import jumpToRowItem from './jumpToRowItem.js';
import getMeasurementLocationCallback from './../../lib/getMeasurementLocationCallback';
const { setViewportSpecificData } = OHIF.redux.actions;
const { MeasurementApi } = OHIF.measurements;

View File

@ -0,0 +1,23 @@
import ConnectedMeasurementTable from './ConnectedMeasurementTable.js';
export default {
id: 'measurements-table',
getPanelModule() {
return {
menuOptions: [
{
icon: 'list',
label: 'Measurements',
target: 'measurement-panel',
},
],
components: [
{
id: 'measurement-panel',
component: ConnectedMeasurementTable,
},
],
defaultContext: ['VIEWER'],
};
},
};

View File

@ -0,0 +1,4 @@
import GenericViewerCommands from './GenericViewerCommands/index.js';
import MeasurementsPanel from './MeasurementsPanel/index.js';
export { GenericViewerCommands, MeasurementsPanel };

View File

@ -0,0 +1,40 @@
.FlexboxLayout {
display: flex;
flex: 1;
flex-flow: row nowrap;
align-items: stretch;
height: calc(100% - var(--toolbar-height) - var(--top-bar-height));
width: 100%;
overflow: hidden;
}
.sidepanel {
flex: 1;
height: 100%;
transition: var(--sidepanel-transition);
}
.from-left {
border-right: var(--ui-border-thickness) solid var(--ui-border-color);
margin-left: calc(var(--left-sidepanel-menu-width) * -1);
max-width: var(--left-sidepanel-menu-width);
}
.from-right {
border-left: var(--ui-border-thickness) solid var(--ui-border-color);
margin-right: calc(var(--right-sidepanel-menu-width) * -1);
max-width: var(--right-sidepanel-menu-width);
}
.sidepanel.is-open {
margin-right: 0;
margin-left: 0;
}
.main-content {
flex: 1;
height: 100%;
overflow: hidden;
transition: var(--sidepanel-transition);
width: 100%;
}

View File

@ -0,0 +1,41 @@
import './SidePanel.css';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
class SidePanel extends Component {
static propTypes = {
from: PropTypes.string.isRequired,
isOpen: PropTypes.bool.isRequired,
children: PropTypes.node,
width: PropTypes.string,
};
render() {
const fromSideClass =
this.props.from === 'right' ? 'from-right' : 'from-left';
const styles = this.props.width
? {
maxWidth: this.props.width,
marginRight: this.props.isOpen
? '0'
: Number.parseInt(this.props.width) * -1,
}
: {};
return (
<section
style={styles}
className={classNames('sidepanel', fromSideClass, {
'is-open': this.props.isOpen,
})}
>
{this.props.children}
</section>
);
}
}
export default SidePanel;

View File

@ -1,16 +0,0 @@
import { connect } from 'react-redux';
import FlexboxLayout from './FlexboxLayout';
const mapStateToProps = state => {
return {
leftSidebarOpen: state.ui.leftSidebarOpen,
rightSidebarOpen: state.ui.rightSidebarOpen,
};
};
const ConnectedFlexboxLayout = connect(
mapStateToProps,
null
)(FlexboxLayout);
export default ConnectedFlexboxLayout;

View File

@ -1,8 +1,5 @@
import {
setLeftSidebarOpen,
setRightSidebarOpen,
} from './../store/layout/actions.js';
// TODO: REPLACE THIS WITH A CONTEXT PROVIDER
// EVERYTHING IN `VIEWER.JS` COULD USE THIS FOR APPROPRIATE CONTEXT
import ToolbarRow from './ToolbarRow';
import { connect } from 'react-redux';
import { getActiveContexts } from './../store/layout/selectors.js';
@ -10,25 +7,9 @@ import { getActiveContexts } from './../store/layout/selectors.js';
const mapStateToProps = state => {
return {
activeContexts: getActiveContexts(state),
leftSidebarOpen: state.ui.leftSidebarOpen,
rightSidebarOpen: state.ui.rightSidebarOpen,
};
};
const mapDispatchToProps = dispatch => {
return {
setLeftSidebarOpen: state => {
dispatch(setLeftSidebarOpen(state));
},
setRightSidebarOpen: state => {
dispatch(setRightSidebarOpen(state));
},
};
};
const ConnectedToolbarRow = connect(
mapStateToProps,
mapDispatchToProps
)(ToolbarRow);
const ConnectedToolbarRow = connect(mapStateToProps)(ToolbarRow);
export default ConnectedToolbarRow;

View File

@ -4,6 +4,13 @@ import OHIF from 'ohif-core';
const { setTimepoints, setMeasurements } = OHIF.redux.actions;
const mapStateToProps = (state, ownProps) => {
return {
viewports: state.viewports.viewportSpecificData,
activeViewportIndex: state.viewports.activeViewportIndex,
};
};
const mapDispatchToProps = dispatch => {
return {
onTimepointsUpdated: timepoints => {
@ -16,7 +23,7 @@ const mapDispatchToProps = dispatch => {
};
const ConnectedViewer = connect(
null,
mapStateToProps,
mapDispatchToProps
)(Viewer);

View File

@ -1,46 +0,0 @@
.FlexboxLayout {
display: flex;
flex: 1;
flex-flow: row nowrap;
align-items: stretch;
height: calc(100% - var(--toolbar-height));
width: 100%;
overflow: hidden;
}
.sidebar-menu {
height: 100%;
transition: var(--sidebar-transition);
}
.sidebar-left {
border-right: var(--ui-border-thickness) solid var(--ui-border-color);
flex: 1;
margin-left: calc(var(--left-sidebar-menu-width) * -1);
max-width: var(--left-sidebar-menu-width);
order: 1;
}
.sidebar-left.sidebar-open {
margin-left: 0;
}
.main-content {
flex: 1;
height: 100%;
order: 2;
overflow: hidden;
transition: var(--sidebar-transition);
width: 100%;
}
.sidebar-right {
flex: 1;
margin-right: calc(var(--right-sidebar-menu-width) * -1);
max-width: var(--right-sidebar-menu-width);
order: 3;
}
.sidebar-right.sidebar-open {
margin-right: 0;
}

View File

@ -1,128 +0,0 @@
import './FlexboxLayout.css';
import React, { Component } from 'react';
import ConnectedMeasurementTable from './ConnectedMeasurementTable';
import ConnectedStudyBrowser from './ConnectedStudyBrowser.js';
import ConnectedViewerMain from './ConnectedViewerMain.js';
import PropTypes from 'prop-types';
class FlexboxLayout extends Component {
static propTypes = {
studies: PropTypes.array,
leftSidebarOpen: PropTypes.bool.isRequired,
rightSidebarOpen: PropTypes.bool.isRequired,
};
state = {
studiesForBrowser: [],
};
componentDidMount() {
if (this.props.studies) {
const studiesForBrowser = this.getStudiesForBrowser();
this.setState({
studiesForBrowser,
});
}
}
componentDidUpdate(prevProps) {
if (this.props.studies !== prevProps.studies) {
const studiesForBrowser = this.getStudiesForBrowser();
this.setState({
studiesForBrowser,
});
}
}
getStudiesForBrowser = () => {
const { studies } = this.props;
// TODO[react]:
// - Add sorting of display sets
// - Add useMiddleSeriesInstanceAsThumbnail
// - Add showStackLoadingProgressBar option
return studies.map(study => {
const { studyInstanceUid } = study;
const thumbnails = study.displaySets.map(displaySet => {
const {
displaySetInstanceUid,
seriesDescription,
seriesNumber,
instanceNumber,
numImageFrames,
// TODO: This is undefined
// modality,
} = displaySet;
let imageId;
let altImageText = ' '; // modality
if (displaySet.images && displaySet.images.length) {
imageId = displaySet.images[0].getImageId();
} else {
altImageText = 'SR';
}
return {
imageId,
altImageText,
displaySetInstanceUid,
seriesDescription,
seriesNumber,
instanceNumber,
numImageFrames,
};
});
return {
studyInstanceUid,
thumbnails,
};
});
};
render() {
let mainContentClassName = 'main-content';
if (this.props.leftSidebarOpen) {
mainContentClassName += ' sidebar-left-open';
}
if (this.props.rightSidebarOpen) {
mainContentClassName += ' sidebar-right-open';
}
// TODO[react]: Make ConnectedMeasurementTable extension with state.timepointManager
return (
<div className="FlexboxLayout">
<div
className={
this.props.leftSidebarOpen
? 'sidebar-menu sidebar-left sidebar-open'
: 'sidebar-menu sidebar-left'
}
>
<ConnectedStudyBrowser studies={this.state.studiesForBrowser} />
</div>
<div className={mainContentClassName}>
<ConnectedViewerMain studies={this.props.studies} />
</div>
<div
className={
this.props.rightSidebarOpen
? 'sidebar-menu sidebar-right sidebar-open'
: 'sidebar-menu sidebar-right'
}
>
<ConnectedMeasurementTable />
</div>
</div>
);
}
}
export default FlexboxLayout;

View File

@ -1,7 +1,11 @@
import './ToolbarRow.css';
import React, { Component } from 'react';
import { RoundedButtonGroup, ToolbarButton } from 'react-viewerbase';
import {
RoundedButtonGroup,
ToolbarButton,
ExpandableToolMenu,
} from 'react-viewerbase';
import { commandsManager, extensionManager } from './../App.js';
import ConnectedCineDialog from './ConnectedCineDialog';
@ -11,19 +15,17 @@ import { MODULE_TYPES } from 'ohif-core';
import PropTypes from 'prop-types';
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 = {
leftSidebarOpen: PropTypes.bool.isRequired,
rightSidebarOpen: PropTypes.bool.isRequired,
setLeftSidebarOpen: PropTypes.func,
setRightSidebarOpen: PropTypes.func,
isLeftSidePanelOpen: PropTypes.bool.isRequired,
isRightSidePanelOpen: PropTypes.bool.isRequired,
selectedLeftSidePanel: PropTypes.string.isRequired,
selectedRightSidePanel: PropTypes.string.isRequired,
handleSidePanelChange: PropTypes.func,
activeContexts: PropTypes.arrayOf(PropTypes.string).isRequired,
};
static defaultProps = {
leftSidebarOpen: false,
rightSidebarOpen: false,
};
constructor(props) {
super(props);
@ -43,6 +45,43 @@ class ToolbarRow extends Component {
};
this._handleBuiltIn = _handleBuiltIn.bind(this);
const panelModules = extensionManager.modules[MODULE_TYPES.PANEL];
this.buttonGroups = {
left: [
// TODO: This should come from extensions, instead of being baked in
{
value: 'studies',
icon: 'th-large',
bottomLabel: 'Series',
},
],
right: [],
};
panelModules.forEach(panelExtension => {
const panelModule = panelExtension.module;
const defaultContexts = Array.from(panelModule.defaultContext);
// MENU OPTIONS
panelModule.menuOptions.forEach(menuOption => {
const contexts = Array.from(menuOption.context || defaultContexts);
const activeContextIncludesAnyPanelContexts = this.props.activeContexts.some(
actx => contexts.includes(actx)
);
if (activeContextIncludesAnyPanelContexts) {
const menuOptionEntry = {
value: menuOption.target,
icon: menuOption.icon,
bottomLabel: menuOption.label,
};
const from = menuOption.from || 'right';
this.buttonGroups[from].push(menuOptionEntry);
}
});
});
}
componentDidUpdate(prevProps) {
@ -56,39 +95,7 @@ class ToolbarRow extends Component {
}
}
onLeftSidebarValueChanged = value => {
this.props.setLeftSidebarOpen(!!value);
};
onRightSidebarValueChanged = value => {
this.props.setRightSidebarOpen(!!value);
};
render() {
const leftSidebarToggle = [
{
value: 'studies',
icon: 'th-large',
bottomLabel: 'Series',
},
];
const rightSidebarToggle = [
{
value: 'measurements',
icon: 'list',
bottomLabel: 'Measurements',
},
];
const leftSidebarValue = this.props.leftSidebarOpen
? leftSidebarToggle[0].value
: null;
const rightSidebarValue = this.props.rightSidebarOpen
? rightSidebarToggle[0].value
: null;
const buttonComponents = _getButtonComponents.call(
this,
this.state.toolbarButtons,
@ -102,14 +109,20 @@ class ToolbarRow extends Component {
zIndex: 999,
};
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={leftSidebarToggle}
value={leftSidebarValue}
onValueChanged={this.onLeftSidebarValueChanged}
options={this.buttonGroups.left}
value={this.props.selectedLeftSidePanel || ''}
onValueChanged={onPressLeft}
/>
</div>
{buttonComponents}
@ -119,11 +132,13 @@ class ToolbarRow extends Component {
className="pull-right m-t-1 rm-x-1"
style={{ marginLeft: 'auto' }}
>
<RoundedButtonGroup
options={rightSidebarToggle}
value={rightSidebarValue}
onValueChanged={this.onRightSidebarValueChanged}
/>
{this.buttonGroups.right.length && (
<RoundedButtonGroup
options={this.buttonGroups.right}
value={this.props.selectedRightSidePanel || ''}
onValueChanged={onPressRight}
/>
)}
</div>
</div>
<div className="CineDialogContainer" style={cineDialogContainerStyle}>
@ -140,36 +155,64 @@ class ToolbarRow extends Component {
*/
function _getButtonComponents(toolbarButtons, activeButtons) {
return toolbarButtons.map((button, index) => {
// TODO: If `button.buttons`, use `ExpandedToolMenu`
// I don't believe any extensions currently leverage this
if (button.buttons) {
// Iterate over button definitions and update `onClick` behavior
const childButtons = button.buttons.map(childButton => {
childButton.onClick = _handleToolbarButtonClick.bind(this, childButton);
return childButton;
});
return (
<ExpandableToolMenu
key={button.id}
text={button.label}
icon={button.icon}
buttons={childButtons}
/>
);
}
return (
<ToolbarButton
key={button.id}
label={button.label}
icon={button.icon}
onClick={(evt, props) => {
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') {
this.setState({
activeButtons: [button.id],
});
} else if (button.type === 'builtIn') {
this._handleBuiltIn(button.options);
}
}}
onClick={_handleToolbarButtonClick.bind(this, button)}
isActive={activeButtons.includes(button.id)}
/>
);
});
}
/**
* 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) {
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') {
this.setState({
activeButtons: [button.id],
});
} else if (button.type === 'builtIn') {
this._handleBuiltIn(button.options);
}
}
/**
*
*/
function _getVisibleToolbarButtons() {
const toolbarModules = extensionManager.modules[MODULE_TYPES.TOOLBAR];
const toolbarButtonDefinitions = [];

View File

@ -1,15 +1,18 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
//import OHIF from 'ohif-core';
//import { CineDialog } from 'react-viewerbase';
import classNames from 'classnames';
import { MODULE_TYPES } from 'ohif-core';
import OHIF from 'ohif-core';
import moment from 'moment';
import WhiteLabellingContext from '../WhiteLabellingContext.js';
import ConnectedHeader from './ConnectedHeader.js';
import ConnectedFlexboxLayout from './ConnectedFlexboxLayout.js';
import ConnectedToolbarRow from './ConnectedToolbarRow.js';
import ConnectedLabellingOverlay from './ConnectedLabellingOverlay';
import ConnectedStudyBrowser from './ConnectedStudyBrowser.js';
import ConnectedViewerMain from './ConnectedViewerMain.js';
import SidePanel from './../components/SidePanel.js';
import { extensionManager } from './../App.js';
import './Viewer.css';
/**
* Inits OHIF Hanging Protocol's onReady.
@ -53,6 +56,10 @@ class Viewer extends Component {
studyInstanceUids: PropTypes.array,
onTimepointsUpdated: PropTypes.func,
onMeasurementsUpdated: PropTypes.func,
// window.store.getState().viewports.viewportSpecificData
viewports: PropTypes.object.isRequired,
// window.store.getState().viewports.activeViewportIndex
activeViewportIndex: PropTypes.number.isRequired,
};
constructor(props) {
@ -75,6 +82,14 @@ class Viewer extends Component {
});
}
state = {
isLeftSidePanelOpen: true,
isRightSidePanelOpen: false,
selectedRightSidePanel: '',
selectedLeftSidePanel: 'studies', // TODO: Don't hardcode this
thumbnails: [],
};
retrieveMeasurements = (patientId, timepointIds) => {
OHIF.log.info('retrieveMeasurements');
// TODO: Retrieve the measurements from the latest available SR
@ -171,8 +186,13 @@ class Viewer extends Component {
if (studies) {
const patientId = studies[0] && studies[0].patientId;
timepointApi.retrieveTimepoints({ patientId });
measurementApi.retrieveMeasurements(patientId, [currentTimepointId]);
this.setState({
thumbnails: _mapStudiesToThumbnails(studies),
});
}
}
@ -181,14 +201,33 @@ class Viewer extends Component {
const { studies } = this.props;
const patientId = studies[0] && studies[0].patientId;
const currentTimepointId = this.currentTimepointId;
this.timepointApi.retrieveTimepoints({ patientId });
this.measurementApi.retrieveMeasurements(patientId, [currentTimepointId]);
this.setState({
thumbnails: _mapStudiesToThumbnails(studies),
});
}
}
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 */}
<WhiteLabellingContext.Consumer>
{whiteLabelling => (
<ConnectedHeader home={false}>
@ -196,16 +235,132 @@ class Viewer extends Component {
</ConnectedHeader>
)}
</WhiteLabellingContext.Consumer>
<div id="viewer" className="Viewer">
<ConnectedToolbarRow />
{/*<ConnectedStudyLoadingMonitor studies={this.props.studies} />*/}
{/*<StudyPrefetcher studies={this.props.studies} />*/}
<ConnectedFlexboxLayout studies={this.props.studies} />
<ConnectedLabellingOverlay />
{/* TOOLBAR */}
<ConnectedToolbarRow
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);
}}
/>
{/*<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}
activeIndex={this.props.activeViewportIndex}
/>
) : (
<ConnectedStudyBrowser studies={this.state.thumbnails} />
)}
</SidePanel>
{/* MAIN */}
<div className={classNames('main-content')}>
<ConnectedViewerMain studies={this.props.studies} />
</div>
{/* RIGHT */}
<SidePanel from="right" isOpen={this.state.isRightSidePanelOpen}>
{VisiblePanelRight && (
<VisiblePanelRight
viewports={this.props.viewports}
activeIndex={this.props.activeViewportIndex}
/>
)}
</SidePanel>
</div>
<ConnectedLabellingOverlay />
</>
);
}
}
export default 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 useMiddleSeriesInstanceAsThumbnail
* - 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 = ' '; // modality
if (displaySet.images && displaySet.images.length) {
imageId = displaySet.images[0].getImageId();
} else {
altImageText = 'SR';
}
return {
imageId,
altImageText,
displaySetInstanceUid,
seriesDescription,
seriesNumber,
instanceNumber,
numImageFrames,
};
});
return {
studyInstanceUid,
thumbnails,
};
});
};

View File

@ -15,27 +15,7 @@
transition: all 0.3s ease;
}
.ViewerMain>div {
.ViewerMain > div {
width: 100%;
height: 100%;
}
.ViewerMain #imageViewerViewports .viewportContainer {
/*theme('border', '%s solid $uiBorderColorDark' % $viewportBorderThickness)*/
/*float: left;
position: relative;*/
outline: 0; /* Prevent blue outline in Chrome */
}
.ViewerMain #imageViewerViewports .viewportContainer:hover,
.ViewerMain #imageViewerViewports .viewportContainer:active,
.ViewerMain #imageViewerViewports .viewportContainer:hover.active {
outline: 0; /* Prevent blue outline in Chrome */
}
/* &:hover
//theme('border', '%s solid $uiBorderColor' % $viewportBorderThickness)
&.active, &:hover.active
//theme('border', '%s solid $uiBorderColorActive' % $viewportBorderThickness)
*/

View File

@ -139,9 +139,9 @@ class ViewerMain extends Component {
<div className="ViewerMain">
{this.state.displaySets.length && (
<ConnectedLayoutManager
studies={this.props.studies}
viewportData={this.getViewportData()}
setViewportData={this.setViewportData}
studies={this.props.studies}
viewportData={this.getViewportData()}
setViewportData={this.setViewportData}
>
{/* Children to add to each viewport that support children */}
<ConnectedToolContextMenu />

View File

@ -109,18 +109,9 @@ props.oidc = [
};*/
/*
UI settings
Plugins
- Custom tools / buttons
- Custom Sidebar module thing
- Custom Viewports
- Custom Sop Class Interpreters
*/
/*"PUBLIC_SETTINGS": {
"ui": {
"studyListFunctionsEnabled": true,
"leftSidebarOpen": false,
"displaySetNavigationLoopOverSeries": false,
"displaySetNavigationMultipleViewports": true,
"autoPositionMeasurementsTextCallOuts": "TRLB"

View File

@ -0,0 +1,27 @@
import Hammer from 'hammerjs';
import cornerstone from 'cornerstone-core';
import cornerstoneMath from 'cornerstone-math';
import cornerstoneTools from 'cornerstone-tools';
export default function(configuration = {}) {
// For debugging
window.cornerstoneTools = cornerstoneTools;
cornerstoneTools.external.cornerstone = cornerstone;
cornerstoneTools.external.Hammer = Hammer;
cornerstoneTools.external.cornerstoneMath = cornerstoneMath;
cornerstoneTools.init(configuration);
// Set the tool font and font size
// context.font = "[style] [variant] [weight] [size]/[line height] [font family]";
const fontFamily =
'Roboto, OpenSans, HelveticaNeue-Light, Helvetica Neue Light, Helvetica Neue, Helvetica, Arial, Lucida Grande, sans-serif';
cornerstoneTools.textStyle.setFont(`16px ${fontFamily}`);
// Tool styles/colors
cornerstoneTools.toolStyle.setToolWidth(2);
cornerstoneTools.toolColors.setToolColor('rgb(255, 255, 0)');
cornerstoneTools.toolColors.setActiveColor('rgb(0, 255, 0)');
cornerstoneTools.store.state.touchProximity = 40;
}

View File

@ -12,6 +12,18 @@ export default function getMeasurementLocationCallback(
const ToolInstance = cornerstoneTools.getToolForElement(element, toolType);
if (
!ToolInstance ||
!ToolInstance.configuration ||
!ToolInstance.configuration.getMeasurementLocationCallback
) {
console.warn(
'Tool instance configuration is missing: getMeasurementLocationCallback'
);
return;
}
ToolInstance.configuration.getMeasurementLocationCallback(
tool,
eventData,

View File

@ -147,7 +147,7 @@ export default function setupTools(store) {
{ name: 'Wwwc', mouseButtonMasks: [1] },
{
name: 'Bidirectional',
configuration: {
props: {
configuration: {
getMeasurementLocationCallback: toolLabellingFlowCallback,
},
@ -156,7 +156,7 @@ export default function setupTools(store) {
},
{
name: 'Length',
configuration: {
props: {
configuration: {
getMeasurementLocationCallback: toolLabellingFlowCallback,
},
@ -165,7 +165,7 @@ export default function setupTools(store) {
},
{
name: 'Angle',
configuration: {
props: {
configuration: {
getMeasurementLocationCallback: toolLabellingFlowCallback,
},
@ -185,7 +185,7 @@ export default function setupTools(store) {
},
{
name: 'EllipticalRoi',
configuration: {
props: {
configuration: {
getMeasurementLocationCallback: toolLabellingFlowCallback,
},
@ -194,7 +194,7 @@ export default function setupTools(store) {
},
{
name: 'CircleRoi',
configuration: {
props: {
configuration: {
getMeasurementLocationCallback: toolLabellingFlowCallback,
},
@ -203,7 +203,7 @@ export default function setupTools(store) {
},
{
name: 'RectangleRoi',
configuration: {
props: {
configuration: {
getMeasurementLocationCallback: toolLabellingFlowCallback,
},

View File

@ -1,16 +0,0 @@
export const setLeftSidebarOpen = state => ({
type: 'SET_LEFT_SIDEBAR_OPEN',
state,
});
export const setRightSidebarOpen = state => ({
type: 'SET_RIGHT_SIDEBAR_OPEN',
state,
});
const actions = {
setLeftSidebarOpen,
setRightSidebarOpen,
};
export default actions;

View File

@ -1,17 +1,10 @@
const defaultState = {
leftSidebarOpen: true,
rightSidebarOpen: false,
labelling: {},
contextMenu: {},
};
const ui = (state = defaultState, action) => {
switch (action.type) {
// ~ SIDEBAR
case 'SET_LEFT_SIDEBAR_OPEN':
return Object.assign({}, state, { leftSidebarOpen: action.state });
case 'SET_RIGHT_SIDEBAR_OPEN':
return Object.assign({}, state, { rightSidebarOpen: action.state });
case 'SET_LABELLING_FLOW_DATA': {
const labelling = Object.assign({}, action.labellingFlowData);

View File

@ -4,8 +4,8 @@
--top-bar-expanded-height: 160px;
--toolbar-height: 78px;
--toolbar-drawer-height: 62px;
--left-sidebar-menu-width: 307px;
--right-sidebar-menu-width: 323px;
--left-sidepanel-menu-width: 307px;
--right-sidepanel-menu-width: 323px;
--study-list-padding: 8%;
--study-list-padding-medium-screen: 10px;
}
@ -21,7 +21,7 @@
:root {
--transition-duration: 0.3s;
--transition-effect: ease;
--sidebar-transition: all 0.3s ease;
--sidepanel-transition: all 0.3s ease;
}
/* Thicknesses */

View File

@ -1214,10 +1214,10 @@
universal-user-agent "^2.0.0"
url-template "^2.0.8"
"@ohif/extension-cornerstone@0.0.37":
version "0.0.37"
resolved "https://registry.yarnpkg.com/@ohif/extension-cornerstone/-/extension-cornerstone-0.0.37.tgz#d9d31409ffbc773dde17767daa02b87f461cf11a"
integrity sha512-090WPGXOYIxZjQe0mIM7SipgR3ggUZpbtWpwg7huWC0PQObIM3Juy4L8mtss23OKaKsmrmVxUaFc5NRQhs+Z+w==
"@ohif/extension-cornerstone@0.0.38":
version "0.0.38"
resolved "https://registry.yarnpkg.com/@ohif/extension-cornerstone/-/extension-cornerstone-0.0.38.tgz#c1cdf2796bef441a293bcc045f9748c7b2c1395f"
integrity sha512-xWvK04s2xwjNlIvz4BpJ5pWP8wNmW9kpud1zkywR+TFYgthH2X64Tr01VZTOh8Mi+0Xkwe5IElb4Sw2aVA4SBg==
dependencies:
"@babel/runtime" "^7.2.0"
classnames "^2.2.6"
@ -3796,10 +3796,10 @@ cornerstone-math@^0.1.8:
resolved "https://registry.yarnpkg.com/cornerstone-math/-/cornerstone-math-0.1.8.tgz#68ab1f9e4fdcd7c5cb23a0d2eb4263f9f894f1c5"
integrity sha512-x7NEQHBtVG7j1yeyj/aRoKTpXv1Vh2/H9zNLMyqYJDtJkNng8C4Q8M3CgZ1qer0Yr7eVq2x+Ynmj6kfOm5jXKw==
cornerstone-tools@^3.13.0:
version "3.14.1"
resolved "https://registry.yarnpkg.com/cornerstone-tools/-/cornerstone-tools-3.14.1.tgz#9a15bca1c4ab96698e91b35f38b5cfa165d58032"
integrity sha512-gfYhRawZ7oZhft1klF2b6IcTciN3f05YM8v4AWT6sVcPiA+MkRPf9GVcrmUOUDsfUtg8RG0TAAO7lN9lRr92BQ==
cornerstone-tools@^3.15.1:
version "3.15.1"
resolved "https://registry.yarnpkg.com/cornerstone-tools/-/cornerstone-tools-3.15.1.tgz#9c14a0dde32d8d8cc60e1abf3c222b681964fb9d"
integrity sha512-nD9M6cbcQJoU5XYUfbrHyi8lt9SD32pQFJ87PfIVRgMvJQA3awobcWwnXZJAD7lB7xPvO1n763FzeuQ5CqWcpQ==
dependencies:
"@babel/runtime" "7.1.2"
cornerstone-math "0.1.7"