diff --git a/extensions/default/src/Toolbar/ToolbarLayoutSelector.tsx b/extensions/default/src/Toolbar/ToolbarLayoutSelector.tsx index 8bd76d90e..7d314b0cc 100644 --- a/extensions/default/src/Toolbar/ToolbarLayoutSelector.tsx +++ b/extensions/default/src/Toolbar/ToolbarLayoutSelector.tsx @@ -1,169 +1,201 @@ -import React, { useEffect, useState, useCallback, useRef } from 'react'; +// Updated ToolbarLayoutSelector.tsx +import React, { useCallback } from 'react'; import PropTypes from 'prop-types'; -import { LayoutSelector as OHIFLayoutSelector, ToolbarButton, LayoutPreset } from '@ohif/ui'; +import { CommandsManager } from '@ohif/core'; + +import { LayoutSelector } from '../../../../platform/ui-next/src/components/LayoutSelector'; function ToolbarLayoutSelectorWithServices({ commandsManager, servicesManager, + rows = 3, + columns = 4, ...props -}: withAppTypes) { - const [isDisabled, setIsDisabled] = useState(false); +}) { + const { customizationService } = servicesManager.services; - const handleMouseEnter = () => { - setIsDisabled(false); - }; + // Get the presets from the customization service + const commonPresets = customizationService?.getCustomization('layoutSelector.commonPresets') || [ + { + icon: 'layout-single', + commandOptions: { + numRows: 1, + numCols: 1, + }, + }, + { + icon: 'layout-side-by-side', + commandOptions: { + numRows: 1, + numCols: 2, + }, + }, + { + icon: 'layout-four-up', + commandOptions: { + numRows: 2, + numCols: 2, + }, + }, + { + icon: 'layout-three-row', + commandOptions: { + numRows: 3, + numCols: 1, + }, + }, + ]; - const onSelection = useCallback(props => { - commandsManager.run({ - commandName: 'setViewportGridLayout', - commandOptions: { ...props }, - }); - setIsDisabled(true); - }, []); + // Get the advanced presets generator from the customization service + const advancedPresetsGenerator = customizationService?.getCustomization( + 'layoutSelector.advancedPresetGenerator' + ); - const onSelectionPreset = useCallback(props => { - commandsManager.run({ - commandName: 'setHangingProtocol', - commandOptions: { ...props }, - }); - setIsDisabled(true); - }, []); + // Generate the advanced presets + const advancedPresets = advancedPresetsGenerator + ? advancedPresetsGenerator({ servicesManager }) + : [ + { + title: 'MPR', + icon: 'layout-three-col', + commandOptions: { + protocolId: 'mpr', + }, + }, + { + title: '3D four up', + icon: 'layout-four-up', + commandOptions: { + protocolId: '3d-four-up', + }, + }, + { + title: '3D main', + icon: 'layout-three-row', + commandOptions: { + protocolId: '3d-main', + }, + }, + { + title: 'Axial Primary', + icon: 'layout-side-by-side', + commandOptions: { + protocolId: 'axial-primary', + }, + }, + { + title: '3D only', + icon: 'layout-single', + commandOptions: { + protocolId: '3d-only', + }, + }, + { + title: '3D primary', + icon: 'layout-side-by-side', + commandOptions: { + protocolId: '3d-primary', + }, + }, + { + title: 'Frame View', + icon: 'icon-stack', + commandOptions: { + protocolId: 'frame-view', + }, + }, + ]; + + // Unified selection handler that dispatches to the appropriate command + const handleSelectionChange = useCallback( + (commandOptions, isPreset) => { + if (isPreset) { + // Advanced preset selection + commandsManager.run({ + commandName: 'setHangingProtocol', + commandOptions, + }); + } else { + // Common preset or custom grid selection + commandsManager.run({ + commandName: 'setViewportGridLayout', + commandOptions, + }); + } + }, + [commandsManager] + ); return ( -
+
+ > + + + {/* Left side - Presets */} + {(commonPresets.length > 0 || advancedPresets.length > 0) && ( +
+ {commonPresets.length > 0 && ( + <> + + {commonPresets.map((preset, index) => ( + + ))} + + + + )} + + {advancedPresets.length > 0 && ( + + {advancedPresets.map((preset, index) => ( + + ))} + + )} +
+ )} + + {/* Right Side - Grid Layout */} +
+
Custom
+ + + Hover to select
+ rows and columns
Click to apply +
+
+
+
); } -function LayoutSelector({ - rows = 3, - columns = 4, - onLayoutChange = () => {}, - className, - onSelection, - onSelectionPreset, - servicesManager, - tooltipDisabled, - ...rest -}: withAppTypes) { - const [isOpen, setIsOpen] = useState(false); - const dropdownRef = useRef(null); - - const { customizationService } = servicesManager.services; - - const commonPresets = customizationService.getCustomization('layoutSelector.commonPresets'); - const advancedPresetsGenerator = customizationService.getCustomization( - 'layoutSelector.advancedPresetGenerator' - ); - - const advancedPresets = advancedPresetsGenerator({ servicesManager }); - - const closeOnOutsideClick = event => { - if (isOpen && dropdownRef.current) { - setIsOpen(false); - } - }; - - useEffect(() => { - if (!isOpen) { - return; - } - - setTimeout(() => { - window.addEventListener('click', closeOnOutsideClick); - }, 0); - return () => { - window.removeEventListener('click', closeOnOutsideClick); - dropdownRef.current = null; - }; - }, [isOpen]); - - const onInteractionHandler = () => { - setIsOpen(!isOpen); - }; - const DropdownContent = isOpen ? OHIFLayoutSelector : null; - - return ( - -
-
Common
- -
- {commonPresets.map((preset, index) => ( - - ))} -
- -
- -
Advanced
- -
- {advancedPresets.map((preset, index) => ( - - ))} -
-
- -
-
Custom
- -

- Hover to select

rows and columns

Click to apply -

-
-
- ) - } - isActive={isOpen} - type="toggle" - /> - ); -} - -LayoutSelector.propTypes = { +ToolbarLayoutSelectorWithServices.propTypes = { + commandsManager: PropTypes.instanceOf(CommandsManager), + servicesManager: PropTypes.object, rows: PropTypes.number, columns: PropTypes.number, - onLayoutChange: PropTypes.func, - servicesManager: PropTypes.object.isRequired, }; export default ToolbarLayoutSelectorWithServices; diff --git a/platform/ui-next/src/components/LayoutSelector/LayoutSelector.tsx b/platform/ui-next/src/components/LayoutSelector/LayoutSelector.tsx new file mode 100644 index 000000000..c5a9fb943 --- /dev/null +++ b/platform/ui-next/src/components/LayoutSelector/LayoutSelector.tsx @@ -0,0 +1,365 @@ +import React, { createContext, useContext, useState, useCallback } from 'react'; +import { Popover, PopoverTrigger, PopoverContent } from '../Popover/Popover'; +import { Tooltip, TooltipTrigger, TooltipContent } from '../Tooltip'; +import { Button } from '../Button'; +import { cn } from '../../lib/utils'; +import { Icons } from '../Icons'; +import * as PropTypes from 'prop-types'; + +// Types +type LayoutCommandOptions = { + numRows?: number; + numCols?: number; + protocolId?: string; + [key: string]: any; +}; + +type LayoutPresetType = { + title?: string; + icon: string; + commandOptions: LayoutCommandOptions; + disabled?: boolean; +}; + +// Context +type LayoutSelectorContextType = { + isOpen: boolean; + setIsOpen: (isOpen: boolean) => void; + onSelection: (commandOptions: LayoutCommandOptions) => void; + onSelectionPreset: (commandOptions: LayoutCommandOptions) => void; +}; + +const LayoutSelectorContext = createContext(undefined); + +const useLayoutSelector = () => { + const context = useContext(LayoutSelectorContext); + if (context === undefined) { + throw new Error('useLayoutSelector must be used within a LayoutSelector component'); + } + return context; +}; + +// Main component +type LayoutSelectorProps = { + onSelectionChange?: (commandOptions: LayoutCommandOptions, isPreset: boolean) => void; + onSelection?: (commandOptions: LayoutCommandOptions) => void; + onSelectionPreset?: (commandOptions: LayoutCommandOptions) => void; + children: React.ReactNode; + open?: boolean; + onOpenChange?: (open: boolean) => void; + tooltipDisabled?: boolean; // Keep this prop for now as it might be used elsewhere +}; + +const LayoutSelector = ({ + onSelectionChange, + onSelection = commandOptions => {}, + onSelectionPreset = commandOptions => {}, + children, + open, + onOpenChange, + tooltipDisabled, +}: LayoutSelectorProps) => { + const [isOpenInternal, setIsOpenInternal] = useState(false); + + const isControlled = open !== undefined; + const isOpen = isControlled ? open : isOpenInternal; + const setIsOpen = isControlled ? onOpenChange! : setIsOpenInternal; + + const handleSelection = useCallback( + (commandOptions: LayoutCommandOptions) => { + onSelection(commandOptions); + if (onSelectionChange) { + onSelectionChange(commandOptions, false); + } + setIsOpen(false); + }, + [onSelection, onSelectionChange, setIsOpen] + ); + + const handlePresetSelection = useCallback( + (commandOptions: LayoutCommandOptions) => { + onSelectionPreset(commandOptions); + if (onSelectionChange) { + onSelectionChange(commandOptions, true); + } + setIsOpen(false); + }, + [onSelectionPreset, onSelectionChange, setIsOpen] + ); + + return ( + + + {children} + + + ); +}; + +// Sub-components +type TriggerProps = { + children?: React.ReactNode; + className?: string; + tooltip?: string; + disabled?: boolean; + disabledText?: string; +}; + +const Trigger = ({ + children, + className, + tooltip = 'Change layout', + disabled = false, + disabledText, +}: TriggerProps) => { + const { isOpen } = useLayoutSelector(); + + // Style constants matching ToolButton + const baseClasses = '!rounded-lg inline-flex items-center justify-center'; + const defaultClasses = + 'bg-transparent text-foreground/80 hover:bg-background hover:text-highlight'; + const activeClasses = 'bg-background text-foreground/80'; + const disabledClasses = + 'text-common-bright hover:bg-primary-dark hover:text-primary-light opacity-40 cursor-not-allowed'; + const buttonSizeClass = 'w-10 h-10'; + const iconSizeClass = 'h-7 w-7'; + + const buttonClasses = cn( + baseClasses, + buttonSizeClass, + disabled ? disabledClasses : isOpen ? activeClasses : defaultClasses + ); + + const hasTooltip = tooltip || (disabled && disabledText); + + if (children) { + return ( + + {children} + + ); + } + + return ( + + + + + + + + + {hasTooltip && ( + + {tooltip &&
{tooltip}
} + {disabled && disabledText &&
{disabledText}
} +
+ )} +
+ ); +}; + +type ContentProps = { + children: React.ReactNode; + className?: string; + align?: 'center' | 'start' | 'end'; + sideOffset?: number; +}; + +const Content = ({ children, className, align = 'center', sideOffset = 8 }: ContentProps) => { + return ( + +
{children}
+
+ ); +}; + +type PresetSectionProps = { + children: React.ReactNode; + title: string; + className?: string; +}; + +const PresetSection = ({ children, title, className }: PresetSectionProps) => { + return ( +
+
{title}
+ {React.Children.count(children) > 0 && ( +
+ {children} +
+ )} +
+ ); +}; + +type PresetProps = LayoutPresetType & { + className?: string; + isPreset?: boolean; + iconSize?: string; // Add new prop for icon size +}; + +const Preset = ({ + title, + icon, + commandOptions, + disabled = false, + className, + isPreset = false, + iconSize, // New prop +}: PresetProps) => { + const { onSelection, onSelectionPreset } = useLayoutSelector(); + + const handleClick = () => { + if (disabled) { + return; + } + + if (isPreset) { + onSelectionPreset(commandOptions); + } else { + onSelection(commandOptions); + } + }; + + return ( +
+
+ +
+ {title &&
{title}
} +
+ ); +}; + +type GridSelectorProps = { + rows?: number; + columns?: number; + className?: string; +}; + +const GridSelector = ({ rows = 3, columns = 4, className }: GridSelectorProps) => { + const [hoveredIndex, setHoveredIndex] = useState(undefined); + const { onSelection } = useLayoutSelector(); + + const hoverX = hoveredIndex !== undefined ? hoveredIndex % columns : -1; + const hoverY = hoveredIndex !== undefined ? Math.floor(hoveredIndex / columns) : -1; + + const isHovered = (index: number) => { + if (hoveredIndex === undefined) { + return false; + } + const x = index % columns; + const y = Math.floor(index / columns); + + return x <= hoverX && y <= hoverY; + }; + + const handleSelection = (index: number) => { + const x = index % columns; + const y = Math.floor(index / columns); + onSelection({ + numRows: y + 1, + numCols: x + 1, + }); + }; + + return ( +
+ {Array.from(Array(rows * columns).keys()).map(index => ( +
handleSelection(index)} + onMouseEnter={() => setHoveredIndex(index)} + onMouseLeave={() => setHoveredIndex(undefined)} + /> + ))} +
+ ); +}; + +const Divider = ({ className }: { className?: string }) => ( +
+); + +const HelpText = ({ children, className }: { children: React.ReactNode; className?: string }) => ( +

{children}

+); + +// Assemble the compound component +LayoutSelector.Trigger = Trigger; +LayoutSelector.Content = Content; +LayoutSelector.PresetSection = PresetSection; +LayoutSelector.Preset = Preset; +LayoutSelector.GridSelector = GridSelector; +LayoutSelector.Divider = Divider; +LayoutSelector.HelpText = HelpText; + +// PropTypes +LayoutSelector.propTypes = { + onSelectionChange: PropTypes.func, + onSelection: PropTypes.func, + onSelectionPreset: PropTypes.func, + children: PropTypes.node.isRequired, + open: PropTypes.bool, + onOpenChange: PropTypes.func, + tooltipDisabled: PropTypes.bool, +}; + +export { LayoutSelector }; +export default LayoutSelector; diff --git a/platform/ui-next/src/components/LayoutSelector/index.ts b/platform/ui-next/src/components/LayoutSelector/index.ts new file mode 100644 index 000000000..627927a9c --- /dev/null +++ b/platform/ui-next/src/components/LayoutSelector/index.ts @@ -0,0 +1,4 @@ +import { LayoutSelector } from './LayoutSelector'; + +export { LayoutSelector }; +export default LayoutSelector;