feat(actions): simplify action corner api through the toolbarService (#5033)

This commit is contained in:
Alireza authored and GitHub committed 2025-05-15 12:08:12 -04:00
1 parent f71b88e639
commit 7c15bb8901
246 files changed
+7930 -4270

No files matched your search

@@ -1,103 +1,27 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import React, { ReactElement, useCallback } from 'react';
import { Switch } from '@ohif/ui-next';
import { ColorbarProps, ColorbarOptions } from '../../types/Colorbar';
import { utilities } from '@cornerstonejs/core';
import { useSystem } from '@ohif/core';
import { useViewportRendering } from '../../hooks/useViewportRendering';
export function setViewportColorbar(
viewportId: string,
commandsManager: any,
servicesManager: AppTypes.ServicesManager,
colorbarOptions: Partial<ColorbarOptions>
) {
const { cornerstoneViewportService, viewportGridService } = servicesManager.services;
const displaySetInstanceUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
export function Colorbar({ viewportId }: { viewportId?: string } = {}): ReactElement {
const { hasColorbar, toggleColorbar } = useViewportRendering(viewportId);
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
const backgroundColor = viewportInfo.getViewportOptions().background;
const isLight = backgroundColor ? utilities.isEqual(backgroundColor, [1, 1, 1]) : false;
if (isLight) {
colorbarOptions.ticks = {
position: 'left',
style: {
font: '13px Inter',
color: '#000000',
maxNumTicks: 8,
tickSize: 5,
tickWidth: 1,
labelMargin: 3,
},
};
}
commandsManager.run('toggleViewportColorbar', {
viewportId,
options: colorbarOptions,
displaySetInstanceUIDs,
});
}
export function Colorbar({
viewportId,
colorbarProperties,
}: withAppTypes<ColorbarProps>): ReactElement {
const { servicesManager, commandsManager } = useSystem();
const { colorbarService } = servicesManager.services;
const {
width: colorbarWidth,
colorbarTickPosition,
colorbarContainerPosition,
colormaps,
colorbarInitialColormap,
} = colorbarProperties;
const [showColorbar, setShowColorbar] = useState(colorbarService.hasColorbar(viewportId));
const onSetColorbar = useCallback(() => {
setViewportColorbar(viewportId, commandsManager, servicesManager, {
viewportId,
colormaps,
ticks: {
position: colorbarTickPosition,
},
width: colorbarWidth,
position: colorbarContainerPosition,
activeColormapName: colorbarInitialColormap,
});
}, [commandsManager]);
useEffect(() => {
const updateColorbarState = () => {
setShowColorbar(colorbarService.hasColorbar(viewportId));
};
const { unsubscribe } = colorbarService.subscribe(
colorbarService.EVENTS.STATE_CHANGED,
updateColorbarState
);
return () => {
unsubscribe();
};
}, [viewportId]);
const handleToggle = useCallback(() => {
toggleColorbar();
}, [toggleColorbar]);
return (
<div
className="hover:bg-accent flex h-8 w-full flex-shrink-0 cursor-pointer items-center px-2 text-base hover:rounded"
onClick={e => {
e.stopPropagation();
onSetColorbar();
}}
>
<div className="hover:bg-accent flex h-8 w-full flex-shrink-0 cursor-pointer items-center px-2 text-base hover:rounded">
<div className="flex w-7 flex-shrink-0 items-center justify-center"></div>
<span className="flex-grow">Display Color bar</span>
<span
className="flex-grow"
onClick={handleToggle}
>
Display Color bar
</span>
<Switch
className="ml-2 flex-shrink-0"
checked={showColorbar}
onClick={e => {
e.stopPropagation();
onSetColorbar();
}}
checked={!!hasColorbar}
onCheckedChange={handleToggle}
/>
</div>
);
@@ -1,91 +1,66 @@
import React, { ReactElement, useCallback, useEffect, useRef, useState } from 'react';
import { AllInOneMenu, Switch, Tabs, TabsList, TabsTrigger } from '@ohif/ui-next';
import React, { ReactElement, useEffect, useRef, useState } from 'react';
import { AllInOneMenu, ScrollArea, Switch, Tabs, TabsList, TabsTrigger } from '@ohif/ui-next';
import { useViewportRendering } from '../../hooks/useViewportRendering';
import { StackViewport, Types } from '@cornerstonejs/core';
import { ColormapProps } from '../../types/Colormap';
import { useSystem } from '@ohif/core';
export function Colormap({ viewportId }: { viewportId?: string } = {}): ReactElement {
const { viewportDisplaySets } = useViewportRendering(viewportId);
export function Colormap({ colormaps, viewportId, displaySets }: ColormapProps): ReactElement {
const { servicesManager, commandsManager } = useSystem();
const { cornerstoneViewportService } = servicesManager.services;
const [activeDisplaySetUID, setActiveDisplaySetUID] = useState<string | undefined>(
viewportDisplaySets?.[0]?.displaySetInstanceUID
);
const [activeDisplaySet, setActiveDisplaySet] = useState(displaySets[0]);
// Use the hook with the active display set
const { colorbarProperties, setColormap } = useViewportRendering(viewportId, {
displaySetInstanceUID: activeDisplaySetUID,
});
const { colormaps } = colorbarProperties;
const [showPreview, setShowPreview] = useState(false);
const [prePreviewColormap, setPrePreviewColormap] = useState(null);
const [currentColormap, setCurrentColormap] = useState(null);
const showPreviewRef = useRef(showPreview);
showPreviewRef.current = showPreview;
const prePreviewColormapRef = useRef(prePreviewColormap);
prePreviewColormapRef.current = prePreviewColormap;
const activeDisplaySetRef = useRef(activeDisplaySet);
activeDisplaySetRef.current = activeDisplaySet;
const onSetColorLUT = useCallback(
props => {
// TODO: Better way to check if it's a fusion
const oneOpacityColormaps = ['Grayscale', 'X Ray'];
const opacity =
displaySets.length > 1 && !oneOpacityColormaps.includes(props.colormap.name) ? 0.5 : 1;
commandsManager.run({
commandName: 'setViewportColormap',
commandOptions: {
...props,
opacity,
immediate: true,
},
context: 'CORNERSTONE',
});
},
[commandsManager]
);
const getViewportColormap = (viewportId, displaySet) => {
const { displaySetInstanceUID } = displaySet;
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (viewport instanceof StackViewport) {
const { colormap } = viewport.getProperties();
if (!colormap) {
return colormaps.find(c => c.Name === 'Grayscale') || colormaps[0];
}
return colormap;
}
const actorEntries = viewport.getActors();
const actorEntry = actorEntries?.find(entry =>
entry.referencedId.includes(displaySetInstanceUID)
);
const { colormap } = (viewport as Types.IVolumeViewport).getProperties(actorEntry.referencedId);
if (!colormap) {
return colormaps.find(c => c.Name === 'Grayscale') || colormaps[0];
}
return colormap;
};
const activeIndex = displaySets.findIndex(
ds => ds.displaySetInstanceUID === activeDisplaySetRef.current.displaySetInstanceUID
);
const currentColormapRef = useRef(currentColormap);
currentColormapRef.current = currentColormap;
useEffect(() => {
setActiveDisplaySet(displaySets[displaySets.length - 1]);
}, [displaySets]);
setCurrentColormap(null);
setPrePreviewColormap(null);
}, [activeDisplaySetUID]);
const handleSetColorLUT = (colormap, immediate = true) => {
// Check if it's a fusion viewport
const oneOpacityColormaps = ['Grayscale', 'X Ray'];
const opacity =
viewportDisplaySets.length > 1 && !oneOpacityColormaps.includes(colormap.name) ? 0.5 : 1;
setColormap({
colormap,
opacity,
immediate,
});
};
return (
<>
{displaySets.length > 1 && (
{viewportDisplaySets && viewportDisplaySets.length > 1 && (
<div className="flex h-8 w-full flex-shrink-0 items-center justify-center px-2 text-base">
<Tabs
value={String(activeIndex)}
onValueChange={val => {
const index = parseInt(val, 10);
setActiveDisplaySet(displaySets[index]);
value={activeDisplaySetUID}
onValueChange={displaySetUID => {
setActiveDisplaySetUID(displaySetUID);
setPrePreviewColormap(null);
}}
>
<TabsList>
{displaySets.map((ds, i) => (
{viewportDisplaySets.map(ds => (
<TabsTrigger
key={i}
value={String(i)}
key={ds.displaySetInstanceUID}
value={ds.displaySetInstanceUID}
>
{ds.Modality}
</TabsTrigger>
@@ -94,6 +69,7 @@ export function Colormap({ colormaps, viewportId, displaySets }: ColormapProps):
</Tabs>
</div>
)}
<div
className="hover:bg-accent flex h-8 w-full flex-shrink-0 cursor-pointer items-center px-2 text-base hover:rounded"
onClick={() => setShowPreview(!showPreview)}
@@ -104,49 +80,47 @@ export function Colormap({ colormaps, viewportId, displaySets }: ColormapProps):
checked={showPreview}
onCheckedChange={checked => {
setShowPreview(checked);
if (!checked && currentColormapRef.current) {
handleSetColorLUT(currentColormapRef.current);
}
}}
/>
</div>
<AllInOneMenu.DividerItem />
<AllInOneMenu.ItemPanel
maxHeight="calc(100vh - 250px)"
className="min-h-[200px] flex-grow"
>
{colormaps.map((colormap, index) => (
<AllInOneMenu.Item
key={index}
label={colormap.description}
useIconSpace={false}
onClick={() => {
onSetColorLUT({
viewportId,
colormap,
displaySetInstanceUID: activeDisplaySetRef.current.displaySetInstanceUID,
});
setPrePreviewColormap(null);
}}
onMouseEnter={() => {
if (showPreviewRef.current) {
setPrePreviewColormap(getViewportColormap(viewportId, activeDisplaySetRef.current));
onSetColorLUT({
viewportId,
colormap,
displaySetInstanceUID: activeDisplaySetRef.current.displaySetInstanceUID,
});
}
}}
onMouseLeave={() => {
if (showPreviewRef.current && prePreviewColormapRef.current) {
onSetColorLUT({
viewportId,
colormap: prePreviewColormapRef.current,
displaySetInstanceUID: activeDisplaySetRef.current.displaySetInstanceUID,
});
}
}}
></AllInOneMenu.Item>
))}
</AllInOneMenu.ItemPanel>
<div className="h-[300px] flex-grow">
<ScrollArea className="h-full w-full">
<div className="p-1">
{colormaps.map((colormap, index) => (
<AllInOneMenu.Item
key={index}
label={colormap.description}
useIconSpace={false}
onClick={() => {
setCurrentColormap(colormap);
handleSetColorLUT(colormap);
setPrePreviewColormap(null);
}}
onMouseEnter={() => {
if (showPreviewRef.current) {
if (!prePreviewColormapRef.current) {
setPrePreviewColormap(colormap);
}
handleSetColorLUT(colormap);
}
}}
onMouseLeave={() => {
if (showPreviewRef.current && prePreviewColormapRef.current) {
handleSetColorLUT(prePreviewColormapRef.current);
}
}}
/>
))}
</div>
</ScrollArea>
</div>
</>
);
}
@@ -1,15 +1,13 @@
import React, { ReactElement, useState } from 'react';
import { AllInOneMenu } from '@ohif/ui-next';
import { VolumeRenderingOptionsProps } from '../../types/ViewportPresets';
import { VolumeRenderingQuality } from './VolumeRenderingQuality';
import { VolumeShift } from './VolumeShift';
import { VolumeLighting } from './VolumeLighting';
import { VolumeShade } from './VolumeShade';
import { useViewportRendering } from '../../hooks/useViewportRendering';
export function VolumeRenderingOptions({
viewportId,
volumeRenderingQualityRange,
}: VolumeRenderingOptionsProps): ReactElement {
export function VolumeRenderingOptions({ viewportId }: { viewportId?: string } = {}): ReactElement {
const { volumeRenderingQualityRange } = useViewportRendering(viewportId);
const [hasShade, setShade] = useState(false);
return (
<AllInOneMenu.ItemPanel>
@@ -1,14 +1,12 @@
import { AllInOneMenu } from '@ohif/ui-next';
import { Icons } from '@ohif/ui-next';
import React, { ReactElement } from 'react';
import { VolumeRenderingPresetsProps } from '../../types/ViewportPresets';
import { VolumeRenderingPresetsContent } from './VolumeRenderingPresetsContent';
import { useSystem } from '@ohif/core';
import { useViewportRendering } from '../../hooks/useViewportRendering';
export function VolumeRenderingPresets({
viewportId,
volumeRenderingPresets,
}: VolumeRenderingPresetsProps): ReactElement {
export function VolumeRenderingPresets({ viewportId }: { viewportId?: string } = {}): ReactElement {
const { volumeRenderingPresets } = useViewportRendering(viewportId);
const { servicesManager } = useSystem();
const { uiDialogService } = servicesManager.services;
@@ -1,54 +1,122 @@
import React, { ReactElement, useCallback } from 'react';
import { AllInOneMenu } from '@ohif/ui-next';
import React, { ReactElement, useEffect, useRef, useState } from 'react';
import { AllInOneMenu, ScrollArea, Switch, Tabs, TabsList, TabsTrigger } from '@ohif/ui-next';
import { useViewportRendering } from '../../hooks/useViewportRendering';
import { WindowLevelPreset } from '../../types/WindowLevel';
import { useSystem } from '@ohif/core';
import { useTranslation } from 'react-i18next';
export type WindowLevelProps = {
viewportId: string;
presets: Array<Record<string, Array<WindowLevelPreset>>>;
};
export function WindowLevel({ viewportId, presets }: WindowLevelProps): ReactElement {
const { commandsManager } = useSystem();
const { t } = useTranslation('WindowLevelActionMenu');
const onSetWindowLevel = useCallback(
props => {
commandsManager.run({
commandName: 'setViewportWindowLevel',
commandOptions: {
...props,
viewportId,
},
context: 'CORNERSTONE',
});
},
[commandsManager, viewportId]
export function WindowLevel({ viewportId }: { viewportId?: string } = {}): ReactElement {
const { viewportDisplaySets } = useViewportRendering(viewportId);
const [activeDisplaySetUID, setActiveDisplaySetUID] = useState<string | undefined>(
viewportDisplaySets?.[0]?.displaySetInstanceUID
);
// Use the hook with the active display set
const { windowLevelPresets, setWindowLevel } = useViewportRendering(viewportId, {
displaySetInstanceUID: activeDisplaySetUID,
});
const [showPreview, setShowPreview] = useState(false);
const [prePreviewPreset, setPrePreviewPreset] = useState<WindowLevelPreset | null>(null);
const [currentPreset, setCurrentPreset] = useState<WindowLevelPreset | null>(null);
const showPreviewRef = useRef(showPreview);
showPreviewRef.current = showPreview;
const prePreviewPresetRef = useRef(prePreviewPreset);
prePreviewPresetRef.current = prePreviewPreset;
const currentPresetRef = useRef(currentPreset);
currentPresetRef.current = currentPreset;
// Reset presets when active display set changes
useEffect(() => {
setCurrentPreset(null);
setPrePreviewPreset(null);
}, [activeDisplaySetUID]);
// Handle applying window level preset
const handleSetWindowLevel = (preset: WindowLevelPreset, immediate = false) => {
setWindowLevel({
windowWidth: Number(preset.window),
windowCenter: Number(preset.level),
immediate,
});
};
return (
<AllInOneMenu.ItemPanel>
{presets.map((modalityPresets, modalityIndex) => (
<React.Fragment key={modalityIndex}>
{Object.entries(modalityPresets).map(([modality, presetsArray]) => (
<React.Fragment key={modality}>
<AllInOneMenu.HeaderItem>
{t('Modality Presets', { modality })}
</AllInOneMenu.HeaderItem>
{presetsArray.map((preset, index) => (
<AllInOneMenu.Item
key={`${modality}-${index}`}
label={preset.description}
secondaryLabel={`${preset.window} / ${preset.level}`}
useIconSpace={false}
onClick={() => onSetWindowLevel(preset)}
/>
<>
{viewportDisplaySets && viewportDisplaySets.length > 1 && (
<div className="flex h-8 w-full flex-shrink-0 items-center justify-center px-2 text-base">
<Tabs
value={activeDisplaySetUID}
onValueChange={displaySetUID => {
setActiveDisplaySetUID(displaySetUID);
}}
>
<TabsList>
{viewportDisplaySets.map(ds => (
<TabsTrigger
key={ds.displaySetInstanceUID}
value={ds.displaySetInstanceUID}
>
{ds.Modality}
</TabsTrigger>
))}
</React.Fragment>
))}
</React.Fragment>
))}
</AllInOneMenu.ItemPanel>
</TabsList>
</Tabs>
</div>
)}
<div
className="hover:bg-accent flex h-8 w-full flex-shrink-0 cursor-pointer items-center px-2 text-base hover:rounded"
onClick={() => setShowPreview(!showPreview)}
>
<span className="flex-shrink-0">Preview in viewport</span>
<Switch
className="ml-auto flex-shrink-0"
checked={showPreview}
onCheckedChange={checked => {
setShowPreview(checked);
// When turning off preview, restore the current preset if one exists
if (!checked && currentPresetRef.current) {
handleSetWindowLevel(currentPresetRef.current, true);
}
}}
/>
</div>
<AllInOneMenu.DividerItem />
<div className="h-[175px] flex-grow">
<ScrollArea className="h-full w-full">
<div className="p-1">
{windowLevelPresets.map((preset, index) => (
<AllInOneMenu.Item
key={index}
label={preset.description}
secondaryLabel={`${preset.window} / ${preset.level}`}
useIconSpace={false}
onClick={() => {
setCurrentPreset(preset);
handleSetWindowLevel(preset, true);
setPrePreviewPreset(null);
}}
onMouseEnter={() => {
if (showPreviewRef.current) {
if (!prePreviewPresetRef.current) {
setPrePreviewPreset(currentPresetRef.current || preset);
}
handleSetWindowLevel(preset, true);
}
}}
onMouseLeave={() => {
if (showPreviewRef.current && prePreviewPresetRef.current) {
handleSetWindowLevel(prePreviewPresetRef.current, true);
}
}}
/>
))}
</div>
</ScrollArea>
</div>
</>
);
}
@@ -1,114 +1,55 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import React, { ReactElement, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { AllInOneMenu, cn } from '@ohif/ui-next';
import { useViewportGrid } from '@ohif/ui-next';
import { AllInOneMenu } from '@ohif/ui-next';
import { Colormap } from './Colormap';
import { Colorbar } from './Colorbar';
import { setViewportColorbar } from './Colorbar';
import { WindowLevelPreset } from '../../types/WindowLevel';
import { ColorbarProperties } from '../../types/Colorbar';
import { VolumeRenderingQualityRange } from '../../types/ViewportPresets';
import { WindowLevel } from './WindowLevel';
import { VolumeRenderingPresets } from './VolumeRenderingPresets';
import { VolumeRenderingOptions } from './VolumeRenderingOptions';
import { ViewportPreset } from '../../types/ViewportPresets';
import { VolumeViewport3D } from '@cornerstonejs/core';
import { utilities } from '@cornerstonejs/core';
import { useSystem } from '@ohif/core';
import { useViewportRendering } from '../../hooks/useViewportRendering';
export type WindowLevelActionMenuProps = {
viewportId: string;
element: HTMLElement;
presets: Array<Record<string, Array<WindowLevelPreset>>>;
colorbarProperties: ColorbarProperties;
displaySets: Array<any>;
volumeRenderingPresets: Array<ViewportPreset>;
volumeRenderingQualityRange: VolumeRenderingQualityRange;
location: string;
element?: HTMLElement;
align?: 'start' | 'end' | 'center';
side?: 'top' | 'bottom' | 'left' | 'right';
};
export function WindowLevelActionMenu({
viewportId,
element,
presets,
verticalDirection,
horizontalDirection,
colorbarProperties,
displaySets,
volumeRenderingPresets,
volumeRenderingQualityRange,
}: withAppTypes<WindowLevelActionMenuProps>): ReactElement {
const { commandsManager, servicesManager } = useSystem();
const {
colormaps,
colorbarContainerPosition,
colorbarInitialColormap,
colorbarTickPosition,
width: colorbarWidth,
} = colorbarProperties;
const { colorbarService, cornerstoneViewportService } = servicesManager.services;
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const backgroundColor = viewportInfo?.getViewportOptions().background;
const isLight = backgroundColor ? utilities.isEqual(backgroundColor, [1, 1, 1]) : false;
align,
side,
}: WindowLevelActionMenuProps): ReactElement {
return (
<WindowLevelActionMenuContent
viewportId={viewportId}
align={align}
side={side}
/>
);
}
export function WindowLevelActionMenuContent({
viewportId,
align,
side,
}: {
viewportId: string;
align?: string;
side?: string;
}): ReactElement {
const { t } = useTranslation('WindowLevelActionMenu');
// Use a stable key for the menu to avoid infinite re-renders
const menuKey = useMemo(() => `${viewportId}`, [viewportId]);
const [viewportGrid] = useViewportGrid();
const { activeViewportId } = viewportGrid;
const [vpHeight, setVpHeight] = useState(element?.clientHeight);
const [menuKey, setMenuKey] = useState(0);
const [is3DVolume, setIs3DVolume] = useState(false);
const onSetColorbar = useCallback(() => {
setViewportColorbar(viewportId, commandsManager, servicesManager, {
colormaps,
ticks: {
position: colorbarTickPosition,
},
width: colorbarWidth,
position: colorbarContainerPosition,
activeColormapName: colorbarInitialColormap,
});
}, [commandsManager]);
useEffect(() => {
const newVpHeight = element?.clientHeight;
if (vpHeight !== newVpHeight) {
setVpHeight(newVpHeight);
}
}, [element, vpHeight]);
useEffect(() => {
if (!colorbarService.hasColorbar(viewportId)) {
return;
}
window.setTimeout(() => {
colorbarService.removeColorbar(viewportId);
onSetColorbar();
}, 0);
}, [viewportId, displaySets, viewport]);
useEffect(() => {
setMenuKey(menuKey + 1);
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (viewport instanceof VolumeViewport3D) {
setIs3DVolume(true);
} else {
setIs3DVolume(false);
}
}, [
displaySets,
viewportId,
presets,
volumeRenderingQualityRange,
volumeRenderingPresets,
const {
is3DVolume,
colorbarProperties,
activeViewportId,
viewportGrid,
]);
windowLevelPresets,
volumeRenderingPresets,
volumeRenderingQualityRange,
} = useViewportRendering(viewportId);
return (
<AllInOneMenu.Menu
@@ -116,60 +57,38 @@ export function WindowLevelActionMenu({
key={menuKey}
// the visibility is handled by the parent component
isVisible={true}
horizontalDirection={horizontalDirection}
verticalDirection={verticalDirection}
align={align}
side={side}
>
<AllInOneMenu.ItemPanel>
{!is3DVolume && (
<Colorbar
viewportId={viewportId}
displaySets={displaySets.filter(ds => ds.supportsWindowLevel === true)}
colorbarProperties={colorbarProperties}
/>
)}
{!is3DVolume && <Colorbar viewportId={viewportId} />}
{colormaps && !is3DVolume && (
{colorbarProperties?.colormaps && !is3DVolume && (
<AllInOneMenu.SubMenu
key="colorLUTPresets"
itemLabel="Color LUT"
itemIcon="icon-color-lut"
className="flex h-[calc(100%-32px)] flex-col"
>
<Colormap
className="flex h-full w-full flex-col"
colormaps={colormaps}
viewportId={viewportId}
displaySets={displaySets.filter(ds => ds.supportsWindowLevel === true)}
/>
<Colormap viewportId={viewportId} />
</AllInOneMenu.SubMenu>
)}
{presets && presets.length > 0 && !is3DVolume && (
{windowLevelPresets?.length > 0 && !is3DVolume && (
<AllInOneMenu.SubMenu
key="windowLevelPresets"
itemLabel={t('Modality Window Presets')}
itemIcon="viewport-window-level"
>
<WindowLevel
viewportId={viewportId}
presets={presets}
/>
<WindowLevel viewportId={viewportId} />
</AllInOneMenu.SubMenu>
)}
{volumeRenderingPresets && is3DVolume && (
<VolumeRenderingPresets
viewportId={viewportId}
volumeRenderingPresets={volumeRenderingPresets}
/>
)}
{volumeRenderingPresets && is3DVolume && <VolumeRenderingPresets viewportId={viewportId} />}
{volumeRenderingQualityRange && is3DVolume && (
<AllInOneMenu.SubMenu itemLabel="Rendering Options">
<VolumeRenderingOptions
viewportId={viewportId}
volumeRenderingQualityRange={volumeRenderingQualityRange}
/>
<VolumeRenderingOptions viewportId={viewportId} />
</AllInOneMenu.SubMenu>
)}
</AllInOneMenu.ItemPanel>
@@ -2,63 +2,75 @@ import React, { ReactNode } from 'react';
import { useSystem } from '@ohif/core';
import {
Button,
cn,
Icons,
Popover,
PopoverContent,
PopoverTrigger,
useViewportGrid,
useViewportActionCorners,
AllInOneMenu,
useIconPresentation,
} from '@ohif/ui-next';
import { WindowLevelActionMenu } from './WindowLevelActionMenu';
import { MENU_IDS } from '../menus/menu-ids';
import { useViewportDisplaySets } from '../../hooks/useViewportDisplaySets';
import { useViewportRendering } from '../../hooks';
export function WindowLevelActionMenuWrapper({
viewportId,
element,
location,
displaySets,
}: withAppTypes<{
viewportId: string;
element: HTMLElement;
location: string;
displaySets: Array<AppTypes.DisplaySet>;
}>): ReactNode {
const [viewportGrid] = useViewportGrid();
const [actionCornerState, viewportActionCornersAPI] = useViewportActionCorners();
const isActiveViewport = viewportId === viewportGrid.activeViewportId;
export function WindowLevelActionMenuWrapper(
props: withAppTypes<{
viewportId: string;
element?: HTMLElement;
location?: number;
isOpen?: boolean;
onOpen?: () => void;
onClose?: () => void;
displaySets?: Array<AppTypes.DisplaySet>;
disabled?: boolean;
isEmbedded?: boolean;
}>
): ReactNode {
const {
viewportId,
element,
location,
isOpen = false,
onOpen,
onClose,
disabled,
isEmbedded = false,
onInteraction: onInteractionProps,
hasEmbeddedVariantToUse,
...rest
} = props;
const [gridState] = useViewportGrid();
const viewportIdToUse = viewportId || gridState.activeViewportId;
const { viewportDisplaySets: displaySets } = useViewportDisplaySets(viewportIdToUse);
const { servicesManager } = useSystem();
const { customizationService } = servicesManager.services;
const presets = customizationService.getCustomization('cornerstone.windowLevelPresets');
const colorbarProperties = customizationService.getCustomization('cornerstone.colorbar');
const { volumeRenderingPresets, volumeRenderingQualityRange } =
customizationService.getCustomization('cornerstone.3dVolumeRendering');
const isMenuOpen =
actionCornerState.viewports[viewportId]?.[location]?.find(
item => item.id === MENU_IDS.WINDOW_LEVEL_MENU
)?.isOpen ?? false;
const { toolbarService } = servicesManager.services;
const { IconContainer, className: iconClassName, containerProps } = useIconPresentation();
const { hasColorbar, toggleColorbar } = useViewportRendering(viewportId);
const handleOpenChange = (openState: boolean) => {
const shouldToggleColorbar = hasColorbar && !isEmbedded;
if (isOpen && shouldToggleColorbar && openState) {
toggleColorbar();
onClose?.();
return;
}
if (!isOpen && openState && shouldToggleColorbar) {
toggleColorbar();
return;
}
if (openState) {
viewportActionCornersAPI.openItem?.(viewportId, MENU_IDS.WINDOW_LEVEL_MENU);
onOpen?.();
} else {
viewportActionCornersAPI.closeItem?.(viewportId, MENU_IDS.WINDOW_LEVEL_MENU);
onClose?.();
}
};
const { align, side, horizontalDirection, verticalDirection } = getMenuDirections(
location,
viewportActionCornersAPI
);
const displaySetPresets = displaySets
.filter(displaySet => presets[displaySet.Modality])
.map(displaySet => {
return { [displaySet.Modality]: presets[displaySet.Modality] };
});
const { align, side } = toolbarService.getAlignAndSide(location);
const modalities = displaySets.map(displaySet => displaySet.supportsWindowLevel);
@@ -66,25 +78,41 @@ export function WindowLevelActionMenuWrapper({
return null;
}
const Icon =
hasColorbar && !isEmbedded && hasEmbeddedVariantToUse ? (
<Icons.Close className={iconClassName} />
) : (
<Icons.ViewportWindowLevel className={iconClassName} />
);
return (
<Popover
open={isMenuOpen}
open={isOpen}
onOpenChange={handleOpenChange}
>
<PopoverTrigger
asChild
className="flex items-center justify-center"
>
<Button
variant="ghost"
size="icon"
className={cn(
isActiveViewport ? 'visible' : 'invisible group-hover/pane:visible',
'text-highlight'
<div>
{IconContainer ? (
<IconContainer
disabled={disabled}
{...rest}
{...containerProps}
>
{Icon}
</IconContainer>
) : (
<Button
variant="ghost"
size="icon"
disabled={disabled}
>
{Icon}
</Button>
)}
>
<Icons.ByName name="viewport-window-level" />
</Button>
</div>
</PopoverTrigger>
<PopoverContent
className="border-none bg-transparent p-0 shadow-none"
@@ -94,45 +122,12 @@ export function WindowLevelActionMenuWrapper({
sideOffset={5}
>
<WindowLevelActionMenu
viewportId={viewportId}
viewportId={viewportIdToUse}
element={element}
presets={displaySetPresets}
horizontalDirection={horizontalDirection}
verticalDirection={verticalDirection}
colorbarProperties={colorbarProperties}
displaySets={displaySets}
volumeRenderingPresets={volumeRenderingPresets}
volumeRenderingQualityRange={volumeRenderingQualityRange}
align={align}
side={side}
/>
</PopoverContent>
</Popover>
);
}
const getMenuDirections = (location, viewportActionCornersAPI) => {
let align = 'center';
let side = 'bottom';
if (location !== undefined) {
const positioning = viewportActionCornersAPI.getAlignAndSide(location);
align = positioning.align;
side = positioning.side;
}
let horizontalDirection;
let verticalDirection;
if (side === 'bottom') {
verticalDirection = AllInOneMenu.VerticalDirection.TopToBottom;
} else {
verticalDirection = AllInOneMenu.VerticalDirection.BottomToTop;
}
if (align === 'start') {
horizontalDirection = AllInOneMenu.HorizontalDirection.LeftToRight;
} else {
horizontalDirection = AllInOneMenu.HorizontalDirection.RightToLeft;
}
return { align, side, horizontalDirection, verticalDirection };
};
@@ -1,13 +0,0 @@
import React from 'react';
import { WindowLevelActionMenuWrapper } from './WindowLevelActionMenuWrapper';
export function getWindowLevelActionMenu(
props: withAppTypes<{
viewportId: string;
element: HTMLElement;
location: string;
displaySets: Array<any>;
}>
) {
return <WindowLevelActionMenuWrapper {...props} />;
}