Files
ohif-viewer/platform/viewer/src/connectedComponents/ToolbarRow.js
T

386 lines
11 KiB
JavaScript
Raw Normal View History

2019-05-30 00:46:45 -04:00
import React, { Component } from 'react';
2019-10-25 23:46:32 -03:00
import PropTypes from 'prop-types';
import { withTranslation } from 'react-i18next';
2019-12-02 14:02:35 -05:00
import { MODULE_TYPES } from '@ohif/core';
import {
ExpandableToolMenu,
RoundedButtonGroup,
ToolbarButton,
2019-11-05 23:07:03 -05:00
withModal,
2019-11-19 16:17:33 -03:00
withDialog,
2019-08-13 15:36:46 -04:00
} from '@ohif/ui';
2019-10-25 23:46:32 -03:00
import './ToolbarRow.css';
import { commandsManager, extensionManager } from './../App.js';
2019-05-29 23:47:26 -04:00
2019-06-20 20:30:12 -04:00
import ConnectedCineDialog from './ConnectedCineDialog';
2019-05-30 00:46:45 -04:00
import ConnectedLayoutButton from './ConnectedLayoutButton';
2020-05-06 14:17:44 -03:00
import { withAppContext } from '../context/AppContext';
2019-10-25 23:46:32 -03:00
2018-12-18 14:15:55 +01:00
class ToolbarRow extends Component {
2019-07-04 14:09:13 -04:00
// TODO: Simplify these? isOpen can be computed if we say "any" value for selected,
// closed if selected is null/undefined
static propTypes = {
2019-07-04 14:09:13 -04:00
isLeftSidePanelOpen: PropTypes.bool.isRequired,
isRightSidePanelOpen: PropTypes.bool.isRequired,
selectedLeftSidePanel: PropTypes.string.isRequired,
selectedRightSidePanel: PropTypes.string.isRequired,
handleSidePanelChange: PropTypes.func.isRequired,
2019-06-17 14:20:32 -04:00
activeContexts: PropTypes.arrayOf(PropTypes.string).isRequired,
studies: PropTypes.array,
t: PropTypes.func.isRequired,
2020-02-24 11:28:35 -05:00
// NOTE: withDialog, withModal HOCs
dialog: PropTypes.any,
modal: PropTypes.any,
};
static defaultProps = {
studies: [],
2019-05-30 00:46:45 -04:00
};
2018-12-19 14:53:03 +01:00
constructor(props) {
super(props);
2019-06-17 14:20:32 -04:00
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: [],
};
2019-06-20 20:30:12 -04:00
this.seriesPerStudyCount = [];
2019-06-20 20:30:12 -04:00
this._handleBuiltIn = _handleBuiltIn.bind(this);
2019-07-04 14:09:13 -04:00
this.updateButtonGroups();
}
updateButtonGroups() {
2019-07-04 14:09:13 -04:00
const panelModules = extensionManager.modules[MODULE_TYPES.PANEL];
2019-07-04 14:09:13 -04:00
this.buttonGroups = {
left: [],
2019-07-04 14:09:13 -04:00
right: [],
};
// ~ FIND MENU OPTIONS
2019-07-04 14:09:13 -04:00
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)
2019-07-04 14:09:13 -04:00
);
2020-02-24 11:20:10 -05:00
2020-02-24 11:16:24 -05:00
// It's a bit beefy to pass studies; probably only need to be reactive on `studyInstanceUIDs` and activeViewport?
2020-02-24 11:20:10 -05:00
// 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) {
2019-07-04 14:09:13 -04:00
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'),
});
}
2019-06-17 14:20:32 -04:00
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();
}
2019-06-17 14:20:32 -04:00
if (activeContextsChanged) {
this.setState(
{
toolbarButtons: _getVisibleToolbarButtons.call(this),
},
this.closeCineDialogIfNotApplicable
);
2019-06-17 14:20:32 -04:00
}
}
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
2019-06-13 09:51:19 -04:00
);
2019-07-04 14:09:13 -04:00
const onPress = (side, value) => {
this.props.handleSidePanelChange(side, value);
};
const onPressLeft = onPress.bind(this, 'left');
const onPressRight = onPress.bind(this, 'right');
return (
2019-06-20 20:30:12 -04:00
<>
<div className="ToolbarRow">
<div className="pull-left m-t-1 p-y-1" style={{ padding: '10px' }}>
<RoundedButtonGroup
2019-07-04 14:09:13 -04:00
options={this.buttonGroups.left}
value={this.props.selectedLeftSidePanel || ''}
onValueChanged={onPressLeft}
2019-06-20 20:30:12 -04:00
/>
</div>
{buttonComponents}
<ConnectedLayoutButton />
<div
className="pull-right m-t-1 rm-x-1"
style={{ marginLeft: 'auto' }}
>
2019-07-04 14:09:13 -04:00
{this.buttonGroups.right.length && (
<RoundedButtonGroup
options={this.buttonGroups.right}
value={this.props.selectedRightSidePanel || ''}
onValueChanged={onPressRight}
/>
)}
2019-06-20 20:30:12 -04:00
</div>
</div>
2019-06-20 20:30:12 -04:00
</>
2019-05-30 00:46:45 -04:00
);
}
2018-12-18 14:15:55 +01:00
}
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;
2019-11-19 16:17:33 -03:00
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}
2019-11-19 16:17:33 -03:00
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);
2019-11-19 16:17:33 -03:00
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)}
2019-11-19 16:17:33 -03:00
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);
}
2019-07-04 14:09:13 -04:00
if (hasNestedButtonDefinitions) {
return _getExpandableButtonComponent.call(_this, button, activeButtons);
2019-07-04 14:09:13 -04:00
}
return _getDefaultButtonComponent.call(_this, button, activeButtons);
});
}
2019-07-04 14:09:13 -04:00
/**
2020-02-24 11:28:35 -05:00
* TODO: DEPRECATE
* This is used exclusively in `extensions/cornerstone/src`
* We have better ways with new UI Services to trigger "builtin" behaviors
*
2019-07-04 14:09:13 -04:00
* 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) {
2019-11-19 16:17:33 -03:00
const { activeButtons } = this.state;
2019-07-04 14:09:13 -04:00
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') {
2019-11-19 16:17:33 -03:00
const toggables = activeButtons.filter(
({ options }) => options && !options.togglable
);
this.setState({ activeButtons: [...toggables, button] });
2019-07-04 14:09:13 -04:00
} else if (button.type === 'builtIn') {
2019-11-19 16:17:33 -03:00
this._handleBuiltIn(button);
2019-07-04 14:09:13 -04:00
}
}
/**
*
*/
2019-06-17 14:20:32 -04:00
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;
}
2019-11-19 16:17:33 -03:00
function _handleBuiltIn(button) {
/* TODO: Keep cine button active until its unselected. */
2020-02-24 11:28:35 -05:00
const { dialog, t } = this.props;
2019-11-19 16:17:33 -03:00
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],
}));
}
2019-06-20 20:30:12 -04:00
}
2019-10-25 23:46:32 -03:00
2019-11-19 16:17:33 -03:00
if (options.behavior === 'DOWNLOAD_SCREEN_SHOT') {
2019-12-02 14:02:35 -05:00
commandsManager.runCommand('showDownloadViewportModal', {
2019-11-19 16:17:33 -03:00
title: t('Download High Quality Image'),
2019-11-05 23:07:03 -05:00
});
2019-10-25 23:46:32 -03:00
}
2019-06-20 20:30:12 -04:00
}
2019-11-05 23:07:03 -05:00
export default withTranslation(['Common', 'ViewportDownloadForm'])(
2020-05-06 14:17:44 -03:00
withModal(withDialog(withAppContext(ToolbarRow)))
2019-11-05 23:07:03 -05:00
);