refactor: layout manager to viewer (#1026)
* docs: formatting * Prefer numRows and numColumns to percentage width/height for layout * fix: LayoutManager --> ViewportGrid (get out of UI component library) * docs: remove outdated docs * Don't expose ExampleDropTarget * Revert thumnail entry drag source removal * Update screaming tests * fix drag-n-drop * fix vtk mpr2d CC: @jamesapetts * remove setSingleLayoutData * remove vtk qualifier
This commit is contained in:
1 parent
d6862e7418
commit
b01b0108b0
33 files changed
+456
-613
No files matched your search
+11
-7
@@ -1,7 +1,7 @@
|
||||
import { LayoutManager } from '@ohif/ui';
|
||||
import ViewportGrid from './ViewportGrid.js';
|
||||
import { MODULE_TYPES } from '@ohif/core';
|
||||
import { connect } from 'react-redux';
|
||||
import { extensionManager } from './../App.js';
|
||||
import { extensionManager } from './../../App.js';
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const availableViewportModules = {};
|
||||
@@ -18,9 +18,13 @@ const mapStateToProps = state => {
|
||||
defaultPlugin = viewportModules[0].extensionId;
|
||||
}
|
||||
|
||||
const { numRows, numColumns, layout, activeViewportIndex } = state.viewports;
|
||||
|
||||
return {
|
||||
layout: state.viewports.layout,
|
||||
activeViewportIndex: state.viewports.activeViewportIndex,
|
||||
numRows,
|
||||
numColumns,
|
||||
layout,
|
||||
activeViewportIndex,
|
||||
// TODO: rename `availableViewportModules`
|
||||
availablePlugins: availableViewportModules,
|
||||
// TODO: rename `defaultViewportModule`
|
||||
@@ -28,9 +32,9 @@ const mapStateToProps = state => {
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedLayoutManager = connect(
|
||||
const ConnectedViewportGrid = connect(
|
||||
mapStateToProps,
|
||||
null
|
||||
)(LayoutManager);
|
||||
)(ViewportGrid);
|
||||
|
||||
export default ConnectedLayoutManager;
|
||||
export default ConnectedViewportGrid;
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @export
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
export default function DefaultViewport(props) {
|
||||
return <div>{JSON.stringify(props)}</div>;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
.empty-viewport {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
color: var(--text-secondary-color);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
function EmptyViewport() {
|
||||
return (
|
||||
<div className="empty-viewport">
|
||||
<p>Please drag a stack here to view images.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
.viewport-container {
|
||||
float: left;
|
||||
position: relative;
|
||||
border: var(--viewport-border-thickness) solid var(--ui-border-color);
|
||||
}
|
||||
|
||||
.viewport-container.active {
|
||||
border: var(--viewport-border-thickness) solid var(--active-color);
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import './ViewportGrid.css';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
//
|
||||
import ViewportPane from './ViewportPane.js';
|
||||
import DefaultViewport from './DefaultViewport.js';
|
||||
import EmptyViewport from './EmptyViewport.js';
|
||||
|
||||
const ViewportGrid = function(props) {
|
||||
const {
|
||||
activeViewportIndex,
|
||||
availablePlugins,
|
||||
defaultPlugin: defaultPluginName,
|
||||
layout,
|
||||
numRows,
|
||||
numColumns,
|
||||
setViewportData,
|
||||
studies,
|
||||
viewportData,
|
||||
children,
|
||||
} = props;
|
||||
|
||||
const rowSize = 100 / numRows;
|
||||
const colSize = 100 / numColumns;
|
||||
|
||||
// http://grid.malven.co/
|
||||
if (!viewportData || !viewportData.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ViewportPanes = layout.viewports.map((layout, viewportIndex) => {
|
||||
const displaySet = viewportData[viewportIndex];
|
||||
const data = {
|
||||
displaySet,
|
||||
studies,
|
||||
};
|
||||
|
||||
// Use whichever plugin is currently in use in the panel
|
||||
// unless nothing is specified. If nothing is specified
|
||||
// and the display set has a plugin specified, use that.
|
||||
//
|
||||
// TODO: Change this logic to:
|
||||
// - Plugins define how capable they are of displaying a SopClass
|
||||
// - When updating a panel, ensure that the currently enabled plugin
|
||||
// in the viewport is capable of rendering this display set. If not
|
||||
// then use the most capable available plugin
|
||||
const pluginName =
|
||||
!layout.plugin && displaySet && displaySet.plugin
|
||||
? displaySet.plugin
|
||||
: layout.plugin;
|
||||
|
||||
const ViewportComponent = _getViewportComponent(
|
||||
data, // Why do we pass this as `ViewportData`, when that's not really what it is?
|
||||
viewportIndex,
|
||||
children,
|
||||
availablePlugins,
|
||||
pluginName,
|
||||
defaultPluginName
|
||||
);
|
||||
|
||||
return (
|
||||
<ViewportPane
|
||||
onDrop={({
|
||||
viewportIndex,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
}) => {
|
||||
setViewportData({
|
||||
viewportIndex,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
});
|
||||
}}
|
||||
viewportIndex={viewportIndex} // Needed by `setViewportData`
|
||||
className={classNames('viewport-container', {
|
||||
active: activeViewportIndex === viewportIndex,
|
||||
})}
|
||||
key={viewportIndex}
|
||||
>
|
||||
{ViewportComponent}
|
||||
</ViewportPane>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateRows: `repeat(${numRows}, ${rowSize}%)`,
|
||||
gridTemplateColumns: `repeat(${numColumns}, ${colSize}%)`,
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{ViewportPanes}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ViewportGrid.propTypes = {
|
||||
viewportData: PropTypes.array.isRequired,
|
||||
supportsDragAndDrop: PropTypes.bool.isRequired,
|
||||
activeViewportIndex: PropTypes.number.isRequired,
|
||||
layout: PropTypes.object.isRequired,
|
||||
availablePlugins: PropTypes.object.isRequired,
|
||||
setViewportData: PropTypes.func.isRequired,
|
||||
studies: PropTypes.array,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
ViewportGrid.defaultProps = {
|
||||
viewportData: [],
|
||||
numRows: 1,
|
||||
numColumns: 1,
|
||||
layout: {
|
||||
viewports: [{}],
|
||||
},
|
||||
activeViewportIndex: 0,
|
||||
supportsDragAndDrop: true,
|
||||
availablePlugins: {
|
||||
DefaultViewport,
|
||||
},
|
||||
defaultPlugin: 'defaultViewportPlugin',
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param {*} plugin
|
||||
* @param {*} viewportData
|
||||
* @param {*} viewportIndex
|
||||
* @param {*} children
|
||||
* @returns
|
||||
*/
|
||||
function _getViewportComponent(
|
||||
viewportData,
|
||||
viewportIndex,
|
||||
children,
|
||||
availablePlugins,
|
||||
pluginName,
|
||||
defaultPluginName
|
||||
) {
|
||||
if (viewportData.displaySet) {
|
||||
pluginName = pluginName || defaultPluginName;
|
||||
const ViewportComponent = availablePlugins[pluginName];
|
||||
|
||||
if (!ViewportComponent) {
|
||||
throw new Error(
|
||||
`No Viewport Component available for name ${pluginName}.
|
||||
Available plugins: ${JSON.stringify(availablePlugins)}`
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ViewportComponent
|
||||
viewportData={viewportData}
|
||||
viewportIndex={viewportIndex}
|
||||
children={[children]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <EmptyViewport />;
|
||||
}
|
||||
|
||||
export default ViewportGrid;
|
||||
@@ -0,0 +1,10 @@
|
||||
.viewport-drop-target {
|
||||
opacity: 1;
|
||||
position: relative; /* Locks in Scrollbar */
|
||||
transition: 0.3s all ease;
|
||||
}
|
||||
|
||||
.viewport-drop-target.hovered {
|
||||
opacity: 0.5;
|
||||
cursor: copy;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import { useDrop } from 'react-dnd';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import './ViewportPane.css';
|
||||
|
||||
const ViewportPane = function(props) {
|
||||
const { children, onDrop, viewportIndex, className: propClassName } = props;
|
||||
const [{ hovered, highlighted }, drop] = useDrop({
|
||||
accept: 'thumbnail',
|
||||
drop: (droppedItem, monitor) => {
|
||||
const canDrop = monitor.canDrop();
|
||||
const isOver = monitor.isOver();
|
||||
|
||||
if (canDrop && isOver && onDrop) {
|
||||
const { studyInstanceUid, displaySetInstanceUid } = droppedItem;
|
||||
|
||||
onDrop({ viewportIndex, studyInstanceUid, displaySetInstanceUid });
|
||||
}
|
||||
},
|
||||
// Monitor, and collect props.
|
||||
// Returned as values by `useDrop`
|
||||
collect: monitor => ({
|
||||
highlighted: monitor.canDrop(),
|
||||
hovered: monitor.isOver(),
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'viewport-drop-target',
|
||||
{ hovered: hovered },
|
||||
{ highlighted: highlighted },
|
||||
propClassName
|
||||
)}
|
||||
ref={drop}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ViewportPane.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
viewportIndex: PropTypes.number.isRequired,
|
||||
onDrop: PropTypes.func.isRequired,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ViewportPane;
|
||||
@@ -0,0 +1,5 @@
|
||||
import ConnectedViewportGrid from './ConnectedViewportGrid.js';
|
||||
import ViewportGrid from './ViewportGrid.js';
|
||||
|
||||
export default ViewportGrid;
|
||||
export { ConnectedViewportGrid, ViewportGrid };
|
||||
@@ -7,7 +7,7 @@ const { setLayout, setViewportActive } = OHIF.redux.actions;
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
currentLayout: state.viewports.layout,
|
||||
activeViewportIndex: state.viewports.activeViewportIndex
|
||||
activeViewportIndex: state.viewports.activeViewportIndex,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -15,10 +15,11 @@ const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
// TODO: Change if layout switched becomes more complex
|
||||
onChange: (selectedCell, currentLayout, activeViewportIndex) => {
|
||||
let viewports = [];
|
||||
const rows = selectedCell.row + 1;
|
||||
const columns = selectedCell.col + 1;
|
||||
const numViewports = rows * columns;
|
||||
const viewports = [];
|
||||
const numRows = selectedCell.row + 1;
|
||||
const numColumns = selectedCell.col + 1;
|
||||
const numViewports = numRows * numColumns;
|
||||
|
||||
for (let i = 0; i < numViewports; i++) {
|
||||
// Hacky way to allow users to exit MPR "mode"
|
||||
const viewport = currentLayout.viewports[i];
|
||||
@@ -28,16 +29,16 @@ const mapDispatchToProps = dispatch => {
|
||||
}
|
||||
|
||||
viewports.push({
|
||||
height: `${100 / rows}%`,
|
||||
width: `${100 / columns}%`,
|
||||
plugin,
|
||||
});
|
||||
}
|
||||
const layout = {
|
||||
numRows,
|
||||
numColumns,
|
||||
viewports,
|
||||
};
|
||||
|
||||
const maxActiveIndex = rows * columns - 1;
|
||||
const maxActiveIndex = numViewports - 1;
|
||||
if (activeViewportIndex > maxActiveIndex) {
|
||||
dispatch(setViewportActive(0));
|
||||
}
|
||||
@@ -52,9 +53,10 @@ const mergeProps = (propsFromState, propsFromDispatch) => {
|
||||
const { currentLayout, activeViewportIndex } = propsFromState;
|
||||
|
||||
return {
|
||||
onChange: selectedCell => onChangeFromDispatch(selectedCell, currentLayout, activeViewportIndex)
|
||||
onChange: selectedCell =>
|
||||
onChangeFromDispatch(selectedCell, currentLayout, activeViewportIndex),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const ConnectedLayoutButton = connect(
|
||||
mapStateToProps,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import OHIF from "@ohif/core";
|
||||
import PluginSwitch from "./PluginSwitch.js";
|
||||
import { commandsManager } from "./../App.js";
|
||||
import { connect } from "react-redux";
|
||||
// import OHIF from '@ohif/core';
|
||||
import PluginSwitch from './PluginSwitch.js';
|
||||
import { commandsManager } from './../App.js';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
const { setLayout } = OHIF.redux.actions;
|
||||
// const { setLayout } = OHIF.redux.actions;
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { activeViewportIndex, layout, viewportSpecificData } = state.viewports;
|
||||
@@ -11,17 +11,17 @@ const mapStateToProps = state => {
|
||||
return {
|
||||
activeViewportIndex,
|
||||
viewportSpecificData,
|
||||
layout
|
||||
layout,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
setLayout: data => {
|
||||
dispatch(setLayout(data));
|
||||
}
|
||||
};
|
||||
};
|
||||
// const mapDispatchToProps = dispatch => {
|
||||
// return {
|
||||
// setLayout: data => {
|
||||
// dispatch(setLayout(data));
|
||||
// }
|
||||
// };
|
||||
// };
|
||||
|
||||
/*function setSingleLayoutData(originalArray, viewportIndex, data) {
|
||||
const viewports = originalArray.slice();
|
||||
@@ -39,19 +39,17 @@ const mergeProps = (propsFromState, propsFromDispatch, ownProps) => {
|
||||
// TODO: Do not display certain options if the current display set
|
||||
// cannot be displayed using these view types
|
||||
const mpr = () => {
|
||||
commandsManager.runCommand("mpr2d");
|
||||
}
|
||||
;
|
||||
|
||||
commandsManager.runCommand('mpr2d');
|
||||
};
|
||||
return {
|
||||
mpr
|
||||
mpr,
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedPluginSwitch = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
null, // mapDispatchToProps
|
||||
mergeProps
|
||||
)(PluginSwitch);
|
||||
|
||||
export default ConnectedPluginSwitch;
|
||||
export default ConnectedPluginSwitch;
|
||||
@@ -13,9 +13,7 @@ class PluginSwitch extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="PluginSwitch">
|
||||
<ToolbarButton label = "2D MPR"
|
||||
icon = "cube"
|
||||
onClick = {this.props.mpr} />
|
||||
<ToolbarButton label="2D MPR" icon="cube" onClick={this.props.mpr} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import './ViewerMain.css';
|
||||
|
||||
import { Component } from 'react';
|
||||
import ConnectedLayoutManager from './ConnectedLayoutManager.js';
|
||||
import { ConnectedViewportGrid } from './../components/ViewportGrid/index.js';
|
||||
import ConnectedToolContextMenu from './ConnectedToolContextMenu.js';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
@@ -124,11 +124,15 @@ class ViewerMain extends Component {
|
||||
return viewportData;
|
||||
};
|
||||
|
||||
setViewportData = ({ viewportIndex, item }) => {
|
||||
setViewportData = ({
|
||||
viewportIndex,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
}) => {
|
||||
const displaySet = this.findDisplaySet(
|
||||
this.props.studies,
|
||||
item.studyInstanceUid,
|
||||
item.displaySetInstanceUid
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid
|
||||
);
|
||||
|
||||
this.props.setViewportSpecificData(viewportIndex, displaySet);
|
||||
@@ -138,14 +142,14 @@ class ViewerMain extends Component {
|
||||
return (
|
||||
<div className="ViewerMain">
|
||||
{this.state.displaySets.length && (
|
||||
<ConnectedLayoutManager
|
||||
<ConnectedViewportGrid
|
||||
studies={this.props.studies}
|
||||
viewportData={this.getViewportData()}
|
||||
setViewportData={this.setViewportData}
|
||||
>
|
||||
{/* Children to add to each viewport that support children */}
|
||||
<ConnectedToolContextMenu />
|
||||
</ConnectedLayoutManager>
|
||||
</ConnectedViewportGrid>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user