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;