feat(app): Decouple colorbar UI from service and enable action menu programatic control (#5018)

This commit is contained in:
Alireza authored and GitHub committed 2025-05-08 12:51:30 -04:00
1 parent f14d9ebdd7
commit 1445e12124
41 files changed
+1872 -659

No files matched your search

@@ -1,15 +1,14 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import { Switch } from '@ohif/ui-next';
import { StackViewport } from '@cornerstonejs/core';
import { ColorbarProps } from '../../types/Colorbar';
import { ColorbarProps, ColorbarOptions } from '../../types/Colorbar';
import { utilities } from '@cornerstonejs/core';
import { useSystem } from '@ohif/core';
export function setViewportColorbar(
viewportId,
commandsManager,
viewportId: string,
commandsManager: any,
servicesManager: AppTypes.ServicesManager,
colorbarOptions
colorbarOptions: Partial<ColorbarOptions>
) {
const { cornerstoneViewportService, viewportGridService } = servicesManager.services;
const displaySetInstanceUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
@@ -1,7 +1,6 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import classNames from 'classnames';
import { AllInOneMenu } from '@ohif/ui-next';
import { AllInOneMenu, cn } from '@ohif/ui-next';
import { useViewportGrid } from '@ohif/ui-next';
import { Colormap } from './Colormap';
import { Colorbar } from './Colorbar';
@@ -25,6 +24,7 @@ export type WindowLevelActionMenuProps = {
displaySets: Array<any>;
volumeRenderingPresets: Array<ViewportPreset>;
volumeRenderingQualityRange: VolumeRenderingQualityRange;
location: string;
};
export function WindowLevelActionMenu({
@@ -111,20 +111,13 @@ export function WindowLevelActionMenu({
]);
return (
<AllInOneMenu.IconMenu
icon="viewport-window-level"
verticalDirection={verticalDirection}
horizontalDirection={horizontalDirection}
iconClassName={classNames(
activeViewportId === viewportId ? 'visible' : 'invisible group-hover/pane:visible',
'flex shrink-0 cursor-pointer rounded active:text-foreground text-highlight',
isLight ? ' hover:bg-primary/30' : 'hover:bg-primary/30'
)}
menuStyle={{ maxHeight: vpHeight - 32, minWidth: 218 }}
onVisibilityChange={() => {
setVpHeight(element.clientHeight);
}}
<AllInOneMenu.Menu
menuKey={menuKey}
key={menuKey}
// the visibility is handled by the parent component
isVisible={true}
horizontalDirection={horizontalDirection}
verticalDirection={verticalDirection}
>
<AllInOneMenu.ItemPanel>
{!is3DVolume && (
@@ -180,6 +173,6 @@ export function WindowLevelActionMenu({
</AllInOneMenu.SubMenu>
)}
</AllInOneMenu.ItemPanel>
</AllInOneMenu.IconMenu>
</AllInOneMenu.Menu>
);
}
@@ -0,0 +1,138 @@
import React, { ReactNode } from 'react';
import { useSystem } from '@ohif/core';
import {
Button,
cn,
Icons,
Popover,
PopoverContent,
PopoverTrigger,
useViewportGrid,
useViewportActionCorners,
AllInOneMenu,
} from '@ohif/ui-next';
import { WindowLevelActionMenu } from './WindowLevelActionMenu';
import { MENU_IDS } from '../menus/menu-ids';
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;
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 handleOpenChange = (openState: boolean) => {
if (openState) {
viewportActionCornersAPI.openItem?.(viewportId, MENU_IDS.WINDOW_LEVEL_MENU);
} else {
viewportActionCornersAPI.closeItem?.(viewportId, MENU_IDS.WINDOW_LEVEL_MENU);
}
};
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 modalities = displaySets.map(displaySet => displaySet.supportsWindowLevel);
if (modalities.length === 0) {
return null;
}
return (
<Popover
open={isMenuOpen}
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'
)}
>
<Icons.ByName name="viewport-window-level" />
</Button>
</PopoverTrigger>
<PopoverContent
className="border-none bg-transparent p-0 shadow-none"
side={side}
align={align}
alignOffset={0}
sideOffset={5}
>
<WindowLevelActionMenu
viewportId={viewportId}
element={element}
presets={displaySetPresets}
horizontalDirection={horizontalDirection}
verticalDirection={verticalDirection}
colorbarProperties={colorbarProperties}
displaySets={displaySets}
volumeRenderingPresets={volumeRenderingPresets}
volumeRenderingQualityRange={volumeRenderingQualityRange}
/>
</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,53 +1,13 @@
import React, { ReactNode } from 'react';
import { WindowLevelActionMenuProps } from './WindowLevelActionMenu';
import { useSystem } from '@ohif/core';
import { WindowLevelActionMenu as WindowLevelActionMenuComponent } from './WindowLevelActionMenu';
import React from 'react';
import { WindowLevelActionMenuWrapper } from './WindowLevelActionMenuWrapper';
function WindowLevelActionMenu({
viewportId,
element,
displaySets,
verticalDirection,
horizontalDirection,
}: withAppTypes<{
viewportId: string;
element: HTMLElement;
displaySets: AppTypes.DisplaySet[];
}>): ReactNode {
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 displaySetPresets = displaySets
.filter(displaySet => presets[displaySet.Modality])
.map(displaySet => {
return { [displaySet.Modality]: presets[displaySet.Modality] };
});
const modalities = displaySets.map(displaySet => displaySet.supportsWindowLevel);
if (modalities.length === 0) {
return null;
}
return (
<WindowLevelActionMenuComponent
viewportId={viewportId}
element={element}
presets={displaySetPresets}
verticalDirection={verticalDirection}
horizontalDirection={horizontalDirection}
colorbarProperties={colorbarProperties}
displaySets={displaySets}
volumeRenderingPresets={volumeRenderingPresets}
volumeRenderingQualityRange={volumeRenderingQualityRange}
/>
);
}
export function getWindowLevelActionMenu(props: WindowLevelActionMenuProps) {
return <WindowLevelActionMenu {...props} />;
export function getWindowLevelActionMenu(
props: withAppTypes<{
viewportId: string;
element: HTMLElement;
location: string;
displaySets: Array<any>;
}>
) {
return <WindowLevelActionMenuWrapper {...props} />;
}
@@ -0,0 +1,11 @@
export * from './Colorbar';
export * from './Colormap';
export * from './WindowLevel';
export * from './WindowLevelActionMenu';
export * from './WindowLevelActionMenuWrapper';
export * from './VolumeRenderingOptions';
export * from './VolumeRenderingPresets';
export * from './VolumeRenderingQuality';
export * from './VolumeLighting';
export * from './VolumeShade';
export * from './VolumeShift';