feat(segmentation): Enhanced segmentation panel design for TMTV (#3988)

Co-authored-by: Alireza <ar.sedghi@gmail.com>
This commit is contained in:
IbrahimandAlireza authored and GitHub committed 2024-04-02 23:33:04 -04:00
1 parent e8e209291c
commit 9f3235ff09
51 files changed
+2001 -572

No files matched your search

@@ -233,7 +233,8 @@
<body>
<noscript> You need to enable JavaScript to run this app. </noscript>
<div id="root"></div>
<div id="react-portal"></div>
<div id="root">
</div>
</body>
</html>
@@ -204,6 +204,7 @@ export default class ToolbarService extends PubSubService {
const evaluated = props.evaluate?.({ ...refreshProps, button });
const updatedProps = {
...props,
...evaluated,
disabled: evaluated?.disabled || false,
className: evaluated?.className || '',
isActive: evaluated?.isActive, // isActive will be undefined for buttons without this prop
@@ -386,7 +387,9 @@ export default class ToolbarService extends PubSubService {
const { id, uiType, component } = btn;
const { groupId } = btn.props;
const buttonType = this._getButtonUITypes()[uiType];
const buttonTypes = this._getButtonUITypes();
const buttonType = buttonTypes[uiType];
if (!buttonType) {
return;
@@ -414,7 +417,16 @@ export default class ToolbarService extends PubSubService {
};
handleEvaluate = props => {
const { evaluate } = props;
const { evaluate, options } = props;
if (typeof options === 'string') {
// get the custom option component from the extension manager and set it as the optionComponent
const buttonTypes = this._getButtonUITypes();
const optionComponent = buttonTypes[options]?.defaultComponent;
props.options = {
optionComponent,
};
}
if (typeof evaluate === 'function') {
return;
@@ -462,9 +474,8 @@ export default class ToolbarService extends PubSubService {
}
if (typeof evaluate === 'object') {
const { name, options } = evaluate;
const { name, ...options } = evaluate;
const evaluateFunction = this._evaluateFunction[name];
if (evaluateFunction) {
props.evaluate = args => evaluateFunction({ ...args, ...options });
return;
@@ -97,6 +97,7 @@ Let's look at one of the evaluators (for `evaluate.cornerstoneTool`)
return {
disabled: true,
className: '!text-common-bright ohif-disabled',
disabledText: 'Tool not available',
};
}
@@ -371,6 +372,104 @@ state will get synchronized with the toolbar service automatically.
![alt text](../../../assets/img/toolbox-modal.png)
## Toolbox With Options
Your toolbox toolbar buttons can have options, this is really useful
for advanced tools that require to change some parameters. For example, the brush tool that requires the brush size to change or the mode (2D or 3D).
currently we support three types of options
### Radio option
We use this in segmentation shapes to let the user choose between
three different modes
```js
{
id: 'Shapes',
uiType: 'ohif.radioGroup',
props: {
label: 'Shapes',
evaluate: {
name: 'evaluate.cornerstone.segmentation',
options: { toolNames: ['CircleScissor', 'SphereScissor', 'RectangleScissor'] },
},
icon: 'icon-tool-shape',
commands: _createSetToolActiveCommands('CircleScissor'),
options: [
{
name: 'Shape',
type: 'radio',
value: 'CircleScissor',
id: 'shape-mode',
values: [
{ value: 'CircleScissor', label: 'Circle' },
{ value: 'SphereScissor', label: 'Sphere' },
{ value: 'RectangleScissor', label: 'Rectangle' },
],
commands: 'setToolActiveToolbar',
},
],
},
},
```
### Range option
We use this for brush radius change
```js
{
id: 'Brush',
icon: 'icon-tool-brush',
label: 'Brush',
evaluate: {
name: 'evaluate.cornerstone.segmentation',
options: { toolNames: ['CircularBrush', 'SphereBrush'] },
disabledText: 'Create new segmentation to enable this tool.',
},
commands: _createSetToolActiveCommands('CircularBrush'),
options: [
{
name: 'Radius (mm)',
id: 'brush-radius',
type: 'range',
min: 0.5,
max: 99.5,
step: 0.5,
value: 25,
commands: {
commandName: 'setBrushSize',
commandOptions: { toolNames: ['CircularBrush', 'SphereBrush'] },
},
},
],
},
```
### Custom option
We use this pattern inside `tmtv` mode for `RectangleROIThreshold`
```js
{
id: 'RectangleROIStartEndThreshold',
uiType: 'ohif.radioGroup',
props: {
icon: 'tool-create-threshold',
label: 'Rectangle ROI Threshold',
commands: setToolActiveToolbar,
evaluate: {
name: 'evaluate.cornerstoneTool',
disabledText: 'Select the PT Axial to enable this tool',
},
options: 'tmtv.RectangleROIThresholdOptions',
},
},
```
Note that it is your job to provide the `tmvt.RectangleROIThresholdOptions` in the getToolbarModule of your extension
## Change Toolbar with hanging protocols
+2 -4
View File
@@ -1,9 +1,7 @@
/* CUSTOM OHIF SCROLLBAR */
.ohif-scrollbar {
scrollbar-color: #041c4a transparent;
scrollbar-gutter: stable;
scrollbar-width: thin;
scrollbar-color: #173239 transparent;
overflow-y: auto;
}
.study-min-height {
@@ -13,6 +13,10 @@ function ToolSettings({ options }) {
return null;
}
if (typeof options === 'function') {
return options();
}
return (
<div className="space-y-2 py-2 text-white">
{options?.map(option => {
@@ -27,15 +27,14 @@ const ButtonGroup = ({
vertical: 'flex-col',
};
const wrapperClasses = classnames('inline-flex', orientationClasses[orientation], className);
const wrapperClasses = classnames(
'items-stretch inline-flex',
orientationClasses[orientation],
className
);
return (
<div
className={classnames(
wrapperClasses,
'border-secondary-light rounded-[5px] border bg-black text-[13px]'
)}
>
<div className={classnames(wrapperClasses, 'text-[13px]')}>
{Children.map(children, (child, index) => {
if (React.isValidElement(child)) {
return cloneElement(child, {
@@ -1,6 +1,7 @@
import React, { useEffect, useCallback, useState, useRef } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import ReactDOM from 'react-dom';
import Icon from '../Icon';
import Typography from '../Typography';
@@ -21,7 +22,9 @@ const Dropdown = ({
maxCharactersPerLine,
}) => {
const [open, setOpen] = useState(false);
const element = useRef(null);
const elementRef = useRef(null);
const dropdownRef = useRef(null);
const [coords, setCoords] = useState({ x: 0, y: 0 });
// choose the max characters per line based on the longest title
const longestTitle = list.reduce((acc, item) => {
@@ -107,23 +110,54 @@ const Dropdown = ({
};
const handleClick = e => {
if (element.current && !element.current.contains(e.target)) {
if (elementRef.current && !elementRef.current.contains(e.target)) {
setOpen(false);
}
};
useEffect(() => {
if (elementRef.current && dropdownRef.current) {
const triggerRect = elementRef.current.getBoundingClientRect();
const dropdownRect = dropdownRef.current.getBoundingClientRect();
let x, y;
switch (alignment) {
case 'right':
x = triggerRect.right + window.scrollX - dropdownRect.width;
y = triggerRect.bottom + window.scrollY;
break;
case 'left':
x = triggerRect.left + window.scrollX;
y = triggerRect.bottom + window.scrollY;
break;
default:
x = triggerRect.left + window.scrollX;
y = triggerRect.bottom + window.scrollY;
break;
}
setCoords({ x, y });
}
}, [open, alignment, elementRef.current, dropdownRef.current]);
const renderList = () => {
return (
const portalElement = document.getElementById('react-portal');
const listElement = (
<div
className={classnames(
'top-100 border-secondary-main absolute z-10 mt-2 transform rounded border bg-black shadow transition duration-300',
'top-100 border-secondary-main w-max-content absolute mt-2 transform rounded border bg-black shadow transition duration-300',
{
'right-0 origin-top-right': alignment === 'right',
'left-0 origin-top-left': alignment === 'left',
'scale-0': !open,
'scale-100': open,
}
)}
ref={dropdownRef}
style={{
position: 'absolute',
top: `${coords.y}px`,
left: open ? `${coords.x}px` : -999999,
zIndex: 9999,
}}
data-cy={`${id}-dropdown`}
>
{list.map((item, idx) => (
@@ -137,6 +171,7 @@ const Dropdown = ({
))}
</div>
);
return ReactDOM.createPortal(listElement, portalElement);
};
useEffect(() => {
@@ -150,7 +185,7 @@ const Dropdown = ({
return (
<div
data-cy="dropdown"
ref={element}
ref={elementRef}
className="relative"
>
<div
@@ -33,6 +33,7 @@ const Input = ({
onKeyDown,
readOnly,
disabled,
labelChildren,
...otherProps
}) => {
return (
@@ -40,6 +41,7 @@ const Input = ({
<Label
className={labelClassName}
text={label}
children={labelChildren}
></Label>
<input
data-cy={`input-${id}`}
@@ -83,6 +85,7 @@ Input.propTypes = {
onKeyPress: PropTypes.func,
onKeyDown: PropTypes.func,
disabled: PropTypes.bool,
labelChildren: PropTypes.node,
};
export default Input;
@@ -37,7 +37,6 @@ const PanelSection = ({ title, children, actionIcons = [] }) => {
</div>
{areChildrenVisible && (
<>
<div className="h-[2px] bg-black"></div>
<div className="bg-primary-dark rounded-b-[4px]">{children}</div>
</>
)}
@@ -2,15 +2,14 @@ import React from 'react';
import Icon from '../Icon';
import { useTranslation } from 'react-i18next';
function AddSegmentRow({ onClick }) {
function AddSegmentRow({ onClick, onToggleSegmentationVisibility = null, segmentation = null }) {
const { t } = useTranslation('SegmentationTable');
return (
<div
className="flex hover:cursor-pointer"
onClick={onClick}
>
<div className="h-[28px] w-[28px]"></div>
<div className="group ml-2.5 mt-1">
<div className="flex justify-between bg-black pl-[34px] hover:cursor-pointer">
<div
className="group pt-[5px] pb-[5px]"
onClick={onClick}
>
<div className="text-primary-active group-hover:bg-secondary-dark flex items-center rounded-[4px] pr-2">
<div className="grid h-[28px] w-[28px] place-items-center">
<Icon name="icon-add" />
@@ -18,6 +17,26 @@ function AddSegmentRow({ onClick }) {
<span className="text-[13px]">{t('Add segment')}</span>
</div>
</div>
{segmentation && (
<div className="flex items-center">
<div
className="hover:bg-secondary-dark ml-3 mr-1 grid h-[28px] w-[28px] cursor-pointer place-items-center rounded-[4px]"
onClick={() => onToggleSegmentationVisibility(segmentation.id)}
>
{segmentation.isVisible ? (
<Icon
name="row-shown"
className="text-primary-active"
/>
) : (
<Icon
name="row-hidden"
className="text-primary-active"
/>
)}
</div>
</div>
)}
</div>
);
}
@@ -1,5 +1,5 @@
import React from 'react';
import { Select, Icon, Dropdown } from '../../components';
import { Select, Icon, Dropdown, Tooltip } from '../../components';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
@@ -31,7 +31,7 @@ function SegmentationDropDownRow({
}
return (
<div className="group mx-0.5 mt-[8px] flex items-center">
<div className="group mx-0.5 mt-[8px] flex items-center pb-[10px]">
<div
onClick={e => {
e.stopPropagation();
@@ -97,7 +97,7 @@ function SegmentationDropDownRow({
],
]}
>
<div className="hover:bg-secondary-dark mx-1 grid h-[28px] w-[28px] cursor-pointer place-items-center rounded-[4px]">
<div className="hover:bg-secondary-dark grid h-[28px] w-[28px] cursor-pointer place-items-center rounded-[4px]">
<Icon name="icon-more-menu"></Icon>
</div>
</Dropdown>
@@ -122,8 +122,22 @@ function SegmentationDropDownRow({
/>
)}
<div className="flex items-center">
<Tooltip
position="bottom-right"
content={
<div className="flex flex-col">
<div className="text-[13px] text-white">Series:</div>
<div className="text-aqua-pale text-[13px]">{activeSegmentation.description}</div>
</div>
}
>
<Icon
name="info-action"
className="text-primary-active"
/>
</Tooltip>
<div
className="hover:bg-secondary-dark ml-3 mr-1 grid h-[28px] w-[28px] cursor-pointer place-items-center rounded-[4px]"
className="hover:bg-secondary-dark mr-1 grid h-[28px] w-[28px] cursor-pointer place-items-center rounded-[4px]"
onClick={() => onToggleSegmentationVisibility(activeSegmentation.id)}
>
{activeSegmentation.isVisible ? (
@@ -20,6 +20,7 @@ const SegmentItem = ({
onColor,
onToggleVisibility,
onToggleLocked,
displayText,
}) => {
const [isNumberBoxHovering, setIsNumberBoxHovering] = useState(false);
@@ -27,7 +28,9 @@ const SegmentItem = ({
return (
<div
className={classnames('text-aqua-pale group/row flex min-h-[28px] bg-black')}
className={classnames('text-aqua-pale group/row bg-primary-dark flex min-h-[28px] flex-col', {
'bg-primary-light border-primary-light rounded-l-[4px] border text-black': isActive,
})}
onClick={e => {
e.stopPropagation();
onClick(segmentationId, segmentIndex);
@@ -35,114 +38,140 @@ const SegmentItem = ({
tabIndex={0}
data-cy={'segment-item'}
>
<div
className={classnames('bg-primary-dark group/number grid w-[32px] place-items-center', {
'!bg-primary-light border-primary-light rounded-l-[4px] border text-black': isActive,
'border-primary-dark border': !isActive,
})}
onMouseEnter={() => setIsNumberBoxHovering(true)}
onMouseLeave={() => setIsNumberBoxHovering(false)}
>
{isNumberBoxHovering && showDelete ? (
<Icon
name="close"
className={classnames('h-[8px] w-[8px]', {
'hover:cursor-pointer hover:opacity-60': !disableEditing,
})}
onClick={e => {
if (disableEditing) {
return;
}
e.stopPropagation();
onDelete(segmentationId, segmentIndex);
}}
/>
) : (
<div>{segmentIndex}</div>
)}
</div>
<div
className={classnames('relative flex w-full', {
'border-primary-light bg-primary-dark rounded-r-[4px] border border-l-0': isActive,
'border border-l-0 border-transparent': !isActive,
})}
>
<div className="group-hover/row:bg-primary-dark flex h-full w-full flex-grow items-center">
<div className="pl-2 pr-1.5">
<div
className={classnames('h-[8px] w-[8px] grow-0 rounded-full', {
<div className="flex min-h-[28px]">
<div
className={classnames('group/number grid w-[28px] place-items-center', {
'bg-primary-light border-primary-light rounded-l-[4px] border text-black': isActive,
'bg-primary-dark border-primary-dark border': !isActive,
})}
onMouseEnter={() => setIsNumberBoxHovering(true)}
onMouseLeave={() => setIsNumberBoxHovering(false)}
>
{isNumberBoxHovering && showDelete ? (
<Icon
name="close"
className={classnames('h-[8px] w-[8px]', {
'hover:cursor-pointer hover:opacity-60': !disableEditing,
})}
style={{ backgroundColor: cssColor }}
onClick={e => {
if (disableEditing) {
return;
}
e.stopPropagation();
onColor(segmentationId, segmentIndex);
onDelete(segmentationId, segmentIndex);
}}
/>
</div>
<div className="flex items-center py-1 hover:cursor-pointer">{label}</div>
) : (
<div>{segmentIndex}</div>
)}
</div>
<div
className={classnames(
'absolute right-3 top-0 flex flex-row-reverse rounded-lg pt-[3px]',
{}
)}
className=" h-[30px] bg-black"
style={{ width: '3px' }}
></div>
<div
className={classnames('text-aqua-pale relative flex w-full', {
'border border-l-0 border-transparent': !isActive,
})}
style={{
width: 'calc(100% - 40px)',
}}
>
<div className="group-hover/row:hidden">
{!isVisible && (
<Icon
name="row-hidden"
className="h-5 w-5 text-[#3d5871]"
<div className="bg-primary-dark flex h-full flex-grow items-center">
<div className="pl-2 pr-1.5">
<div
className={classnames('h-[8px] w-[8px] grow-0 rounded-full', {
'hover:cursor-pointer hover:opacity-60': !disableEditing,
})}
style={{ backgroundColor: cssColor }}
onClick={e => {
if (disableEditing) {
return;
}
e.stopPropagation();
onToggleVisibility(segmentationId, segmentIndex);
onColor(segmentationId, segmentIndex);
}}
/>
)}
</div>
<div className="flex items-center py-1 hover:cursor-pointer">{label}</div>
</div>
{/* Icon for 'row-lock' that shows when NOT hovering and 'isLocked' is true */}
<div className="group-hover/row:hidden">
{isLocked && (
<div className="flex">
<div
className={classnames(
'absolute right-3 top-0 flex flex-row-reverse rounded-lg pt-[3px]',
{}
)}
>
<div className="group-hover/row:hidden">
{!isVisible && (
<Icon
name="row-lock"
name="row-hidden"
className="h-5 w-5 text-[#3d5871]"
onClick={e => {
e.stopPropagation();
onToggleLocked(segmentationId, segmentIndex);
onToggleVisibility(segmentationId, segmentIndex);
}}
/>
)}
</div>
{/* This icon is visible when 'isVisible' is true */}
{isVisible && (
{/* Icon for 'row-lock' that shows when NOT hovering and 'isLocked' is true */}
<div className="group-hover/row:hidden">
{isLocked && (
<div className="flex">
<Icon
name="row-hidden"
className="h-5 w-5 opacity-0"
name="row-lock"
className="h-5 w-5 text-[#3d5871]"
onClick={e => {
e.stopPropagation();
onToggleLocked(segmentationId, segmentIndex);
}}
/>
)}
</div>
)}
</div>
{/* Icons that show only when hovering */}
<div className="hidden group-hover/row:flex">
<HoveringIcons
disableEditing={disableEditing}
onEdit={onEdit}
isLocked={isLocked}
isVisible={isVisible}
onToggleLocked={onToggleLocked}
onToggleVisibility={onToggleVisibility}
segmentationId={segmentationId}
segmentIndex={segmentIndex}
/>
{/* This icon is visible when 'isVisible' is true */}
{isVisible && (
<Icon
name="row-hidden"
className="h-5 w-5 opacity-0"
/>
)}
</div>
)}
</div>
{/* Icons that show only when hovering */}
<div className="hidden group-hover/row:flex">
<HoveringIcons
disableEditing={disableEditing}
onEdit={onEdit}
isLocked={isLocked}
isVisible={isVisible}
onToggleLocked={onToggleLocked}
onToggleVisibility={onToggleVisibility}
segmentationId={segmentationId}
segmentIndex={segmentIndex}
/>
</div>
</div>
</div>
</div>
{Array.isArray(displayText) ? (
<div className="flex flex-col py-[5px] pl-[43px]">
{displayText.map(text => (
<div
key={text}
className="text-aqua-pale flex h-full items-center bg-black text-[11px]"
>
{text}
</div>
))}
</div>
) : (
displayText && (
<div className="text-aqua-pale flex h-full items-center bg-black px-2 py-[5px] pl-[45px] text-[11px]">
{displayText}
</div>
)
)}
</div>
);
};
@@ -205,6 +234,7 @@ SegmentItem.propTypes = {
onDelete: PropTypes.func.isRequired,
onToggleVisibility: PropTypes.func.isRequired,
onToggleLocked: PropTypes.func,
displayText: PropTypes.string,
};
SegmentItem.defaultProps = {
@@ -100,7 +100,7 @@ const SegmentationGroupTable = ({
)}
<div className="bg-primary-dark">
{segmentations?.length === 0 ? (
<div className="select-none rounded-[4px]">
<div className="select-none bg-black pt-[5px] pb-[5px]">
{showAddSegmentation && !disableEditing && (
<NoSegmentationRow onSegmentationAdd={onSegmentationAdd} />
)}
@@ -127,7 +127,7 @@ const SegmentationGroupTable = ({
)}
</div>
{activeSegmentation && (
<div className="ohif-scrollbar mt-1.5 flex min-h-0 flex-col overflow-y-hidden">
<div className="ohif-scrollbar flex h-fit max-h-[500px] min-h-0 flex-col overflow-auto bg-black">
{activeSegmentation?.segments?.map(segment => {
if (!segment) {
return null;
@@ -0,0 +1,211 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { PanelSection } from '../../components';
import SegmentationConfig from './SegmentationConfig';
import NoSegmentationRow from './NoSegmentationRow';
import { useTranslation } from 'react-i18next';
import SegmentationItem from './SegmentationItem';
const SegmentationGroupTableExpanded = ({
segmentations,
// segmentation initial config
segmentationConfig,
// UI show/hide
disableEditing,
showAddSegmentation,
showAddSegment,
showDeleteSegment,
// segmentation/segment handlers
onSegmentationAdd,
onSegmentationEdit,
onSegmentationClick,
onSegmentationDelete,
onSegmentationDownload,
onSegmentationDownloadRTSS,
storeSegmentation,
// segment handlers
onSegmentClick,
onSegmentAdd,
onSegmentDelete,
onSegmentEdit,
onToggleSegmentationVisibility,
onToggleSegmentVisibility,
onToggleSegmentLock,
onSegmentColorClick,
// segmentation config handlers
setFillAlpha,
setFillAlphaInactive,
setOutlineWidthActive,
setOutlineOpacityActive,
setRenderFill,
setRenderInactiveSegmentations,
setRenderOutline,
}) => {
const [isConfigOpen, setIsConfigOpen] = useState(false);
const [activeSegmentationId, setActiveSegmentationId] = useState(null);
useEffect(() => {
// find the first active segmentation to set
let activeSegmentationIdToSet = segmentations?.find(segmentation => segmentation.isActive)?.id;
// If there is no active segmentation, set the first one to be active
if (!activeSegmentationIdToSet && segmentations?.length > 0) {
activeSegmentationIdToSet = segmentations[0].id;
}
// If there is no segmentation, set the active segmentation to null
if (segmentations?.length === 0) {
activeSegmentationIdToSet = null;
}
setActiveSegmentationId(activeSegmentationIdToSet);
}, [segmentations]);
const activeSegmentation = segmentations?.find(
segmentation => segmentation.id === activeSegmentationId
);
const { t } = useTranslation('SegmentationTable');
return (
<div className="flex min-h-0 flex-col bg-black text-[13px] font-[300]">
<PanelSection
title={t('Segmentation')}
actionIcons={
activeSegmentation && [
{
name: 'settings-bars',
onClick: () => setIsConfigOpen(isOpen => !isOpen),
},
]
}
>
{isConfigOpen && (
<SegmentationConfig
setFillAlpha={setFillAlpha}
setFillAlphaInactive={setFillAlphaInactive}
setOutlineWidthActive={setOutlineWidthActive}
setOutlineOpacityActive={setOutlineOpacityActive}
setRenderFill={setRenderFill}
setRenderInactiveSegmentations={setRenderInactiveSegmentations}
setRenderOutline={setRenderOutline}
segmentationConfig={segmentationConfig}
/>
)}
<div className="bg-primary-dark">
<div className="select-none bg-black pt-[5px] pb-[5px]">
{showAddSegmentation && !disableEditing && (
<NoSegmentationRow onSegmentationAdd={onSegmentationAdd} />
)}
</div>
{segmentations?.length > 0 && (
<div className="ohif-scrollbar flex max-h-[500px] select-none flex-col gap-[5px] overflow-auto bg-black">
{segmentations?.map(segmentation => {
return (
<div key={segmentation.id}>
<SegmentationItem
key={segmentation.id}
segmentation={segmentation}
disableEditing={disableEditing}
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
onSegmentationEdit={onSegmentationEdit}
onSegmentationDelete={onSegmentationDelete}
onSegmentationDownload={onSegmentationDownload}
onSegmentationDownloadRTSS={onSegmentationDownloadRTSS}
storeSegmentation={storeSegmentation}
showAddSegment={showAddSegment}
onSegmentAdd={onSegmentAdd}
onSegmentClick={onSegmentClick}
onSegmentDelete={onSegmentDelete}
onSegmentEdit={onSegmentEdit}
showDeleteSegment={showDeleteSegment}
onSegmentColorClick={onSegmentColorClick}
onToggleSegmentVisibility={onToggleSegmentVisibility}
onToggleSegmentLock={onToggleSegmentLock}
activeSegmentationId={activeSegmentationId}
/>
</div>
);
})}
</div>
)}
</div>
</PanelSection>
</div>
);
};
SegmentationGroupTableExpanded.propTypes = {
segmentations: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
isActive: PropTypes.bool.isRequired,
segments: PropTypes.arrayOf(
PropTypes.shape({
segmentIndex: PropTypes.number.isRequired,
color: PropTypes.array.isRequired,
label: PropTypes.string.isRequired,
isVisible: PropTypes.bool.isRequired,
isLocked: PropTypes.bool.isRequired,
})
),
})
),
segmentationConfig: PropTypes.object.isRequired,
disableEditing: PropTypes.bool,
showAddSegmentation: PropTypes.bool,
showAddSegment: PropTypes.bool,
showDeleteSegment: PropTypes.bool,
onSegmentationAdd: PropTypes.func.isRequired,
onSegmentationEdit: PropTypes.func.isRequired,
onSegmentationClick: PropTypes.func.isRequired,
onSegmentationDelete: PropTypes.func.isRequired,
onSegmentationDownload: PropTypes.func.isRequired,
onSegmentationDownloadRTSS: PropTypes.func,
storeSegmentation: PropTypes.func.isRequired,
onSegmentClick: PropTypes.func.isRequired,
onSegmentAdd: PropTypes.func.isRequired,
onSegmentDelete: PropTypes.func.isRequired,
onSegmentEdit: PropTypes.func.isRequired,
onToggleSegmentationVisibility: PropTypes.func.isRequired,
onToggleSegmentVisibility: PropTypes.func.isRequired,
onToggleSegmentLock: PropTypes.func.isRequired,
onSegmentColorClick: PropTypes.func.isRequired,
setFillAlpha: PropTypes.func.isRequired,
setFillAlphaInactive: PropTypes.func.isRequired,
setOutlineWidthActive: PropTypes.func.isRequired,
setOutlineOpacityActive: PropTypes.func.isRequired,
setRenderFill: PropTypes.func.isRequired,
setRenderInactiveSegmentations: PropTypes.func.isRequired,
setRenderOutline: PropTypes.func.isRequired,
};
SegmentationGroupTableExpanded.defaultProps = {
segmentations: [],
disableEditing: false,
showAddSegmentation: true,
showAddSegment: true,
showDeleteSegment: true,
onSegmentationAdd: () => {},
onSegmentationEdit: () => {},
onSegmentationClick: () => {},
onSegmentationDelete: () => {},
onSegmentationDownload: () => {},
onSemgnetationDownloadRTSS: () => {},
storeSegmentation: () => {},
onSegmentClick: () => {},
onSegmentAdd: () => {},
onSegmentDelete: () => {},
onSegmentEdit: () => {},
onToggleSegmentationVisibility: () => {},
onToggleSegmentVisibility: () => {},
onToggleSegmentLock: () => {},
onSegmentColorClick: () => {},
setFillAlpha: () => {},
setFillAlphaInactive: () => {},
setOutlineWidthActive: () => {},
setOutlineOpacityActive: () => {},
setRenderFill: () => {},
setRenderInactiveSegmentations: () => {},
setRenderOutline: () => {},
};
export default SegmentationGroupTableExpanded;
@@ -0,0 +1,211 @@
import React, { useState } from 'react';
import { Icon, Dropdown } from '../../components';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import AddSegmentRow from './AddSegmentRow';
import SegmentationGroupSegment from './SegmentationGroupSegment';
import { Tooltip } from '../../components';
function SegmentationItem({
segmentation,
disableEditing,
onSegmentationEdit,
onSegmentationDownload,
onSegmentationDownloadRTSS,
storeSegmentation,
onSegmentationDelete,
showAddSegment,
onToggleSegmentationVisibility,
onSegmentAdd,
onSegmentClick,
onSegmentDelete,
onSegmentEdit,
showDeleteSegment,
onSegmentColorClick,
onToggleSegmentVisibility,
onToggleSegmentLock,
activeSegmentationId,
}) {
const { t } = useTranslation('SegmentationTable');
const [areChildrenVisible, setChildrenVisible] = useState(true);
const handleHeaderClick = () => {
setChildrenVisible(!areChildrenVisible);
};
return (
<>
<div className="bg-secondary-dark group relative flex items-center justify-start gap-1">
<div
onClick={e => {
e.stopPropagation();
}}
className="flex"
>
<Dropdown
id="segmentation-dropdown"
showDropdownIcon={false}
alignment="left"
itemsClassName="text-primary-active"
showBorders={false}
maxCharactersPerLine={30}
list={[
...(!disableEditing
? [
{
title: t('Rename'),
onClick: () => {
onSegmentationEdit(segmentation.id);
},
},
]
: []),
{
title: t('Delete'),
onClick: () => {
onSegmentationDelete(segmentation.id);
},
},
...(!disableEditing
? [
{
title: t('Export DICOM SEG'),
onClick: () => {
storeSegmentation(segmentation.id);
},
},
]
: []),
...[
{
title: t('Download DICOM SEG'),
onClick: () => {
onSegmentationDownload(segmentation.id);
},
},
{
title: t('Download DICOM RTSTRUCT'),
onClick: () => {
onSegmentationDownloadRTSS(segmentation.id);
},
},
],
]}
>
<div className="hover:bg-secondary-dark grid h-[28px] w-[28px] cursor-pointer place-items-center rounded-[4px]">
<Icon name="icon-more-menu"></Icon>
</div>
</Dropdown>
<div
className=" h-[28px] bg-black"
style={{ width: '3px' }}
></div>
</div>
<div
className="flex h-full w-full cursor-pointer items-center justify-between pr-[8px]"
onClick={handleHeaderClick}
>
<div className="font-inter text-aqua-pale text-[13px]">{segmentation.label}</div>
<div className="flex h-[28px] items-center justify-center gap-2">
<Tooltip
position="bottom-right"
content={
<div className="flex flex-col">
<div className="text-[13px] text-white">Series:</div>
<div className="text-aqua-pale text-[13px]">{segmentation.description}</div>
</div>
}
>
<Icon
name="info-action"
className="text-primary-active"
/>
</Tooltip>
<div className={areChildrenVisible ? '' : 'mr-[4px]'}>
<Icon name={areChildrenVisible ? 'chevron-down-new' : 'chevron-left-new'} />
</div>
</div>
</div>
</div>
{areChildrenVisible && (
<>
{!disableEditing && showAddSegment && (
<AddSegmentRow
onClick={() => onSegmentAdd(segmentation.id)}
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
segmentation={segmentation}
/>
)}
<div
className={classNames('ohif-scrollbar flex min-h-0 flex-col overflow-y-hidden', {
'mt-1': disableEditing || !showAddSegment,
})}
>
{segmentation?.segments?.map(segment => {
if (!segment) {
return null;
}
const { segmentIndex, color, label, isVisible, isLocked, displayText } = segment;
return (
<div key={segmentIndex}>
<SegmentationGroupSegment
segmentationId={segmentation.id}
segmentIndex={segmentIndex}
label={label}
color={color}
isActive={
segmentation.activeSegmentIndex === segmentIndex &&
segmentation.id === activeSegmentationId
}
disableEditing={disableEditing}
isLocked={isLocked}
isVisible={isVisible}
onClick={onSegmentClick}
onEdit={onSegmentEdit}
onDelete={onSegmentDelete}
showDelete={showDeleteSegment}
onColor={onSegmentColorClick}
onToggleVisibility={onToggleSegmentVisibility}
onToggleLocked={onToggleSegmentLock}
displayText={displayText}
/>
</div>
);
})}
</div>
</>
)}
</>
);
}
SegmentationItem.propTypes = {
segmentation: PropTypes.object,
disableEditing: PropTypes.bool,
onToggleSegmentationVisibility: PropTypes.func,
onSegmentationEdit: PropTypes.func,
onSegmentationDownload: PropTypes.func,
onSegmentationDownloadRTSS: PropTypes.func,
storeSegmentation: PropTypes.func,
onSegmentationDelete: PropTypes.func,
showAddSegment: PropTypes.bool,
onSegmentAdd: PropTypes.func,
onSegmentClick: PropTypes.func,
onSegmentDelete: PropTypes.func,
onSegmentEdit: PropTypes.func,
showDeleteSegment: PropTypes.bool,
onSegmentColorClick: PropTypes.func,
onToggleSegmentVisibility: PropTypes.func,
onToggleSegmentLock: PropTypes.func,
activeSegmentationId: PropTypes.string,
};
SegmentationItem.defaultProps = {
segmentation: null,
disableEditing: false,
};
export default SegmentationItem;
@@ -1,3 +1,4 @@
import SegmentationGroupTable from './SegmentationGroupTable';
import SegmentationGroupTableExpanded from './SegmentationGroupTableExpanded';
export default SegmentationGroupTable;
export { SegmentationGroupTable, SegmentationGroupTableExpanded };
@@ -16,6 +16,7 @@ const ToolbarButton = ({
//
className,
disabled,
disabledText,
size,
toolTipClassName,
disableToolTip = false,
@@ -38,7 +39,7 @@ const ToolbarButton = ({
<Tooltip
isSticky={shouldShowDropdown}
content={shouldShowDropdown ? dropdownContent : label}
secondaryContent={disabled ? 'Not available on the current viewport' : null}
secondaryContent={disabledText}
tight={shouldShowDropdown}
className={toolTipClassNameToUse}
isDisabled={disableToolTip}
+18 -7
View File
@@ -43,8 +43,14 @@ function Toolbox({ servicesManager, buttonSectionId, commandsManager, title, ...
(accumulator, toolbarButton) => {
const { id: buttonId, componentProps } = toolbarButton;
const createEnhancedOptions = (options, parentId) =>
options.map(option => {
const createEnhancedOptions = (options, parentId) => {
const optionsToUse = Array.isArray(options) ? options : [options];
return optionsToUse.map(option => {
if (typeof option.optionComponent === 'function') {
return option;
}
return {
...option,
value:
@@ -72,13 +78,18 @@ function Toolbox({ servicesManager, buttonSectionId, commandsManager, title, ...
},
};
});
};
if (componentProps.items?.length) {
componentProps.items.forEach(item => {
accumulator[item.id] = createEnhancedOptions(item.options, item.id);
const { items, options } = componentProps;
if (items?.length) {
items.forEach(({ options, id }) => {
accumulator[id] = createEnhancedOptions(options, id);
});
} else if (componentProps.options?.length) {
accumulator[buttonId] = createEnhancedOptions(componentProps.options, buttonId);
} else if (options?.length) {
accumulator[buttonId] = createEnhancedOptions(options, buttonId);
} else if (options?.optionComponent) {
accumulator[buttonId] = options.optionComponent;
}
return accumulator;
@@ -53,8 +53,21 @@ function ToolboxUI(props) {
<Tooltip
position="bottom"
content={componentProps.label}
secondaryContent={'Not available on the current viewport'}
secondaryContent={componentProps.disabledText}
>
<div className="border-secondary-light rounded border bg-black">
<Component
{...componentProps}
{...props}
id={id}
servicesManager={servicesManager}
onInteraction={onInteraction}
size="toolbox"
/>
</div>
</Tooltip>
) : (
<div className="border-secondary-light rounded border bg-black">
<Component
{...componentProps}
{...props}
@@ -63,16 +76,7 @@ function ToolboxUI(props) {
onInteraction={onInteraction}
size="toolbox"
/>
</Tooltip>
) : (
<Component
{...componentProps}
{...props}
id={id}
servicesManager={servicesManager}
onInteraction={onInteraction}
size="toolbox"
/>
</div>
)}
</div>
);
+89 -33
View File
@@ -1,7 +1,8 @@
import React, { useState } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { useTranslation } from 'react-i18next';
import ReactDOM from 'react-dom';
import './tooltip.css';
@@ -42,6 +43,10 @@ const Tooltip = ({
}) => {
const [isActive, setIsActive] = useState(false);
const { t } = useTranslation('Buttons');
const tooltipContainer = document.getElementById('react-portal');
const [coords, setCoords] = useState({ x: 999999, y: 999999 });
const parentRef = useRef(null);
const tooltipRef = useRef(null);
const handleMouseOver = () => {
if (!isActive) {
@@ -57,8 +62,90 @@ const Tooltip = ({
const isOpen = (isSticky || isActive) && !isDisabled;
useEffect(() => {
if (parentRef.current && tooltipRef.current) {
const parentRect = parentRef.current.getBoundingClientRect();
const tooltipRect = tooltipRef.current.getBoundingClientRect();
const parentWidth = parentRect.width;
const parentHeight = parentRect.height;
const tooltipWidth = tooltipRect.width;
let newX = 0;
let newY = 0;
switch (position) {
case 'bottom':
newX = parentRect.left + parentWidth / 2;
newY = parentRect.top + parentHeight;
break;
case 'top':
newX = parentRect.left + parentWidth / 2;
newY = parentRect.top - parentHeight * 2;
break;
case 'right':
newX = parentRect.left + parentWidth;
newY = parentRect.top + parentHeight / 2;
break;
case 'left':
newX = parentRect.left - tooltipWidth - 10;
newY = parentRect.top + parentHeight / 2;
break;
case 'bottom-left':
newX = parentRect.left;
newY = parentRect.top + parentHeight;
break;
case 'bottom-right':
newX = parentRect.left - tooltipWidth + parentWidth;
newY = parentRect.top + parentHeight;
break;
default:
break;
}
setCoords({ x: newX, y: newY });
}
}, [isOpen, position, parentRef.current, tooltipRef.current]);
const tooltipContent = (
<div
className={classnames(`tooltip tooltip-${position} block`, 'z-50')}
style={{
position: 'fixed',
top: coords.y,
left: isOpen ? coords.x : 999999,
}}
>
<div
ref={tooltipRef}
className={classnames(
'tooltip-box bg-primary-dark border-secondary-light w-max-content relative inset-x-auto top-full rounded border text-base text-white',
{
'py-[6px] px-[8px]': !tight,
}
)}
>
<div>{typeof content === 'string' ? t(content) : content}</div>
<div className="text-aqua-pale">
{typeof secondaryContent === 'string' ? t(secondaryContent) : secondaryContent}
</div>
<svg
className="text-primary-dark stroke-secondary-light absolute h-4"
style={arrowPositionStyle[position]}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M24 22l-12-20l-12 20"
/>
</svg>
</div>
</div>
);
return (
<div
ref={parentRef}
className={classnames('relative', className)}
onMouseOver={handleMouseOver}
onFocus={handleMouseOver}
@@ -67,37 +154,7 @@ const Tooltip = ({
role="tooltip"
>
{children}
<div
className={classnames(`tooltip tooltip-${position}`, {
block: isOpen,
hidden: !isOpen,
})}
>
<div
className={classnames(
'tooltip-box bg-primary-dark border-secondary-light w-max-content relative inset-x-auto top-full rounded border text-base text-white',
{
'py-[6px] px-[8px]': !tight,
}
)}
>
<div>{typeof content === 'string' ? t(content) : content}</div>
<div className="text-aqua-pale">
{typeof secondaryContent === 'string' ? t(secondaryContent) : secondaryContent}
</div>
<svg
className="text-primary-dark stroke-secondary-light absolute h-4"
style={arrowPositionStyle[position]}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M24 22l-12-20l-12 20"
/>
</svg>
</div>
</div>
{tooltipContainer && ReactDOM.createPortal(tooltipContent, tooltipContainer)}
</div>
);
};
@@ -110,7 +167,6 @@ Tooltip.defaultProps = {
};
Tooltip.propTypes = {
/** prevents tooltip from rendering despite hover/active/sticky */
isDisabled: PropTypes.bool,
content: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
position: PropTypes.oneOf(['bottom', 'bottom-left', 'bottom-right', 'left', 'right', 'top']),
+2 -1
View File
@@ -29,7 +29,7 @@ import NavBar from './NavBar';
import Notification from './Notification';
import Select from './Select';
import SegmentationTable from './SegmentationTable';
import SegmentationGroupTable from './SegmentationGroupTable';
import { SegmentationGroupTable, SegmentationGroupTableExpanded } from './SegmentationGroupTable';
import SidePanel from './SidePanel';
import SplitButton from './SplitButton';
import StudyBrowser from './StudyBrowser';
@@ -144,6 +144,7 @@ export {
Select,
SegmentationTable,
SegmentationGroupTable,
SegmentationGroupTableExpanded,
SidePanel,
SplitButton,
StudyBrowser,
+1
View File
@@ -81,6 +81,7 @@ export {
Select,
SegmentationTable,
SegmentationGroupTable,
SegmentationGroupTableExpanded,
SidePanel,
SplitButton,
LegacySplitButton,