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:
Danny Brown authored and GitHub committed 2019-10-11 12:03:59 -04:00
1 parent d6862e7418
commit b01b0108b0
33 files changed
+456 -613

No files matched your search

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