feat(actions): simplify action corner api through the toolbarService (#5033)
This commit is contained in:
1 parent
f71b88e639
commit
7c15bb8901
246 files changed
+7930
-4270
No files matched your search
@@ -37,8 +37,8 @@
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjpeg": "^1.2.4",
|
||||
"@cornerstonejs/codec-openjph": "^2.4.5",
|
||||
"@cornerstonejs/core": "^3.12.2",
|
||||
"@cornerstonejs/dicom-image-loader": "^3.12.2",
|
||||
"@cornerstonejs/core": "^3.15.1",
|
||||
"@cornerstonejs/dicom-image-loader": "^3.15.1",
|
||||
"@ohif/ui": "3.11.0-beta.32",
|
||||
"cornerstone-math": "0.1.9",
|
||||
"dicom-parser": "^1.8.21"
|
||||
|
||||
@@ -388,6 +388,10 @@ export default class ExtensionManager extends PubSubService {
|
||||
return this.dataSourceMap[this._activeDataSourceName];
|
||||
};
|
||||
|
||||
getActiveDataSourceOrNull = () => {
|
||||
return this.dataSourceMap[this._activeDataSourceName]?.[0] ?? null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the data source definition for the given data source name.
|
||||
* If no data source name is provided, the active data source definition is
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from './useToolbar';
|
||||
export * from './types';
|
||||
export * from './useViewportRef';
|
||||
export * from './useViewportSize';
|
||||
export * from './useViewportMousePosition';
|
||||
@@ -0,0 +1,27 @@
|
||||
export interface ToolbarButtonActions {
|
||||
// Lock/Unlock actions
|
||||
lockItem: (itemId: string, viewportId?: string) => void;
|
||||
unlockItem: (itemId: string, viewportId?: string) => void;
|
||||
toggleLock: (itemId: string, viewportId?: string) => void;
|
||||
isItemLocked: (itemId: string, viewportId?: string) => boolean;
|
||||
|
||||
// Visibility actions
|
||||
showItem: (itemId: string, viewportId?: string) => void;
|
||||
hideItem: (itemId: string, viewportId?: string) => void;
|
||||
toggleVisibility: (itemId: string, viewportId?: string) => void;
|
||||
isItemVisible: (itemId: string, viewportId?: string) => boolean;
|
||||
|
||||
// Open/Close actions (for menus)
|
||||
openItem: (itemId: string, viewportId?: string) => void;
|
||||
closeItem: (itemId: string, viewportId?: string) => void;
|
||||
closeAllItems: (viewportId?: string) => void;
|
||||
isItemOpen: (itemId: string, viewportId?: string) => boolean;
|
||||
|
||||
// Evaluation
|
||||
evaluateButtonForViewport: (itemId: string, viewportId?: string) => any;
|
||||
}
|
||||
|
||||
export interface ToolbarHookReturn extends ToolbarButtonActions {
|
||||
toolbarButtons: any[]; // The display representation of toolbar buttons
|
||||
onInteraction: (args: any) => void;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { DisplaySet } from '../types';
|
||||
|
||||
import { useSystem } from '../';
|
||||
/**
|
||||
* Hook that listens for changes in the active viewport and its display sets.
|
||||
* It returns the display sets associated with the active viewport.
|
||||
@@ -8,7 +8,8 @@ import { DisplaySet } from '../types';
|
||||
* @param servicesManager - Services manager instance
|
||||
* @returns Array of display sets for the active viewport
|
||||
*/
|
||||
const useActiveViewportDisplaySets = ({ servicesManager }): DisplaySet[] => {
|
||||
const useActiveViewportDisplaySets = (): DisplaySet[] => {
|
||||
const { servicesManager } = useSystem();
|
||||
const { displaySetService, viewportGridService } = servicesManager.services;
|
||||
// Move this function outside useEffect and memoize it
|
||||
const getDisplaySetsForViewport = useCallback(
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState, useMemo } from 'react';
|
||||
import { useSystem } from '../contextProviders/SystemProvider';
|
||||
import { ToolbarHookReturn } from './types';
|
||||
|
||||
export function useToolbar({ buttonSection = 'primary' }: withAppTypes) {
|
||||
export function useToolbar({ buttonSection = 'primary' }: withAppTypes): ToolbarHookReturn {
|
||||
const { commandsManager, servicesManager } = useSystem();
|
||||
const { toolbarService, viewportGridService } = servicesManager.services;
|
||||
const { EVENTS } = toolbarService;
|
||||
|
||||
// Store all buttons returned by the toolbar service
|
||||
const [toolbarButtons, setToolbarButtons] = useState(
|
||||
toolbarService.getButtonSection(buttonSection as string)
|
||||
toolbarService.getButtonSection(buttonSection as string).filter(Boolean)
|
||||
);
|
||||
|
||||
// Store state of open/closed menu items
|
||||
// Note: We keep this in local state to avoid re-evaluating the toolbar on every interaction
|
||||
const [openItemIds, setOpenItemIds] = useState<Record<string, boolean>>({});
|
||||
|
||||
// Callback function for handling toolbar interactions
|
||||
const onInteraction = useCallback(
|
||||
args => {
|
||||
args.event?.stopPropagation?.();
|
||||
const viewportId = viewportGridService.getActiveViewportId();
|
||||
const viewportId = args.viewportId || viewportGridService.getActiveViewportId();
|
||||
const refreshProps = { viewportId };
|
||||
|
||||
const buttonProps = toolbarService.getButtonProps(args.itemId);
|
||||
@@ -58,15 +64,17 @@ export function useToolbar({ buttonSection = 'primary' }: withAppTypes) {
|
||||
|
||||
buttonProps.commands = allCommands;
|
||||
}
|
||||
|
||||
toolbarService.recordInteraction({ ...args, ...buttonProps }, { refreshProps });
|
||||
},
|
||||
[toolbarService, viewportGridService, toolbarButtons]
|
||||
[toolbarService, viewportGridService]
|
||||
);
|
||||
|
||||
// Effect to handle toolbar modification events
|
||||
useEffect(() => {
|
||||
const handleToolbarModified = () => {
|
||||
setToolbarButtons(toolbarService.getButtonSection(buttonSection as string)?.filter(Boolean));
|
||||
const buttons = toolbarService.getButtonSection(buttonSection as string)?.filter(Boolean);
|
||||
setToolbarButtons(buttons);
|
||||
};
|
||||
|
||||
const subs = [EVENTS.TOOL_BAR_MODIFIED, EVENTS.TOOL_BAR_STATE_MODIFIED].map(event => {
|
||||
@@ -96,5 +104,163 @@ export function useToolbar({ buttonSection = 'primary' }: withAppTypes) {
|
||||
return () => subscriptions.forEach(sub => sub.unsubscribe());
|
||||
}, [viewportGridService, toolbarService]);
|
||||
|
||||
return { toolbarButtons, onInteraction };
|
||||
// Action API for toolbar buttons
|
||||
const actions = useMemo(() => {
|
||||
return {
|
||||
// Lock/Unlock actions
|
||||
lockItem: (itemId: string, viewportId?: string) => {
|
||||
const targetViewportId = viewportId || viewportGridService.getActiveViewportId();
|
||||
const button = toolbarService.getButton(itemId);
|
||||
|
||||
if (button) {
|
||||
// Set isLocked flag in button metadata
|
||||
button.props = {
|
||||
...button.props,
|
||||
isLocked: true,
|
||||
};
|
||||
|
||||
// Re-evaluate the button
|
||||
toolbarService.refreshToolbarState({ viewportId: targetViewportId, itemId });
|
||||
}
|
||||
},
|
||||
|
||||
unlockItem: (itemId: string, viewportId?: string) => {
|
||||
const targetViewportId = viewportId || viewportGridService.getActiveViewportId();
|
||||
const button = toolbarService.getButton(itemId);
|
||||
|
||||
if (button) {
|
||||
// Set isLocked flag in button metadata
|
||||
button.props = {
|
||||
...button.props,
|
||||
isLocked: false,
|
||||
};
|
||||
|
||||
// Re-evaluate the button
|
||||
toolbarService.refreshToolbarState({ viewportId: targetViewportId, itemId });
|
||||
}
|
||||
},
|
||||
|
||||
toggleLock: (itemId: string, viewportId?: string) => {
|
||||
const targetViewportId = viewportId || viewportGridService.getActiveViewportId();
|
||||
const button = toolbarService.getButton(itemId);
|
||||
|
||||
if (button) {
|
||||
// Toggle isLocked flag in button metadata
|
||||
button.props = {
|
||||
...button.props,
|
||||
isLocked: !button.props.isLocked,
|
||||
};
|
||||
|
||||
// Re-evaluate the button
|
||||
toolbarService.refreshToolbarState({ viewportId: targetViewportId, itemId });
|
||||
}
|
||||
},
|
||||
|
||||
isItemLocked: (itemId: string, viewportId?: string): boolean => {
|
||||
const button = toolbarService.getButton(itemId);
|
||||
return button?.props?.isLocked === true;
|
||||
},
|
||||
|
||||
// Visibility actions - controlled by evaluator functions in toolbar items
|
||||
showItem: (itemId: string, viewportId?: string) => {
|
||||
const targetViewportId = viewportId || viewportGridService.getActiveViewportId();
|
||||
const button = toolbarService.getButton(itemId);
|
||||
|
||||
if (button) {
|
||||
// Set isVisible flag in button metadata
|
||||
button.props = {
|
||||
...button.props,
|
||||
isVisible: true,
|
||||
};
|
||||
|
||||
// Re-evaluate the button
|
||||
toolbarService.refreshToolbarState({ viewportId: targetViewportId, itemId });
|
||||
}
|
||||
},
|
||||
|
||||
hideItem: (itemId: string, viewportId?: string) => {
|
||||
const targetViewportId = viewportId || viewportGridService.getActiveViewportId();
|
||||
const button = toolbarService.getButton(itemId);
|
||||
|
||||
if (button) {
|
||||
// Set isVisible flag in button metadata
|
||||
button.props = {
|
||||
...button.props,
|
||||
isVisible: false,
|
||||
};
|
||||
|
||||
// Re-evaluate the button
|
||||
toolbarService.refreshToolbarState({ viewportId: targetViewportId, itemId });
|
||||
}
|
||||
},
|
||||
|
||||
toggleVisibility: (itemId: string, viewportId?: string) => {
|
||||
const targetViewportId = viewportId || viewportGridService.getActiveViewportId();
|
||||
const button = toolbarService.getButton(itemId);
|
||||
|
||||
if (button) {
|
||||
// Toggle isVisible flag in button metadata
|
||||
button.props = {
|
||||
...button.props,
|
||||
isVisible: button.props.isVisible === false ? true : false,
|
||||
};
|
||||
|
||||
// Re-evaluate the button
|
||||
toolbarService.refreshToolbarState({ viewportId: targetViewportId, itemId });
|
||||
}
|
||||
},
|
||||
|
||||
isItemVisible: (itemId: string, viewportId?: string): boolean => {
|
||||
const button = toolbarService.getButton(itemId);
|
||||
// If isVisible is explicitly false, return false; otherwise return true
|
||||
return button?.props?.isVisible !== false;
|
||||
},
|
||||
|
||||
// Open/Close actions - managed in local state for performance
|
||||
openItem: (itemId: string, viewportId?: string) => {
|
||||
setOpenItemIds(prev => {
|
||||
// Close all other items
|
||||
const updated = {};
|
||||
// Then set the current item to open
|
||||
updated[itemId] = true;
|
||||
return updated;
|
||||
});
|
||||
},
|
||||
|
||||
closeItem: (itemId: string, viewportId?: string) => {
|
||||
setOpenItemIds(prev => ({
|
||||
...prev,
|
||||
[itemId]: false,
|
||||
}));
|
||||
},
|
||||
|
||||
closeAllItems: (viewportId?: string) => {
|
||||
setOpenItemIds({});
|
||||
},
|
||||
|
||||
isItemOpen: (itemId: string, viewportId?: string): boolean => {
|
||||
return openItemIds[itemId] === true;
|
||||
},
|
||||
};
|
||||
}, [viewportGridService, toolbarService, openItemIds]);
|
||||
|
||||
if (!toolbarButtons?.length) {
|
||||
return {
|
||||
toolbarButtons: [],
|
||||
onInteraction,
|
||||
...actions,
|
||||
};
|
||||
}
|
||||
|
||||
// filter out buttons that are disabled and have hideWhenDisabled set to true
|
||||
const filteredToolbarButtons = toolbarButtons.filter(button => {
|
||||
const props = button.componentProps;
|
||||
return props.visible !== false;
|
||||
});
|
||||
|
||||
return {
|
||||
toolbarButtons: filteredToolbarButtons,
|
||||
onInteraction,
|
||||
...actions,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useViewportRef } from './useViewportRef';
|
||||
import { useViewportSize } from './useViewportSize';
|
||||
|
||||
interface MousePosition {
|
||||
x: number;
|
||||
y: number;
|
||||
isInViewport: boolean;
|
||||
relativeY: number; // Position as percentage from top (0-1)
|
||||
isInBottomPercentage: (percentage: number) => boolean;
|
||||
}
|
||||
|
||||
function useViewportMousePosition(viewportId: string): MousePosition {
|
||||
const viewportRef = useViewportRef(viewportId);
|
||||
const { height, clientRect } = useViewportSize(viewportId);
|
||||
|
||||
const [mousePosition, setMousePosition] = useState<MousePosition>({
|
||||
x: 0,
|
||||
y: 0,
|
||||
isInViewport: false,
|
||||
relativeY: 0,
|
||||
isInBottomPercentage: (percentage: number) => false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!viewportRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
if (!clientRect) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get mouse position relative to viewport
|
||||
const x = event.clientX - clientRect.left;
|
||||
const y = event.clientY - clientRect.top;
|
||||
|
||||
const isInViewport = x >= 0 && x <= clientRect.width && y >= 0 && y <= clientRect.height;
|
||||
|
||||
const relativeY = Math.max(0, Math.min(1, y / height));
|
||||
|
||||
const isInBottomPercentage = (percentage: number) => {
|
||||
return relativeY >= 1 - percentage / 100;
|
||||
};
|
||||
|
||||
setMousePosition({
|
||||
x,
|
||||
y,
|
||||
isInViewport,
|
||||
relativeY,
|
||||
isInBottomPercentage,
|
||||
});
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
};
|
||||
}, [viewportRef, height, clientRect]);
|
||||
|
||||
return mousePosition;
|
||||
}
|
||||
|
||||
export default useViewportMousePosition;
|
||||
export { useViewportMousePosition };
|
||||
@@ -0,0 +1,58 @@
|
||||
import React, { createContext, useContext, useRef } from 'react';
|
||||
|
||||
type ViewportRefsContextType = {
|
||||
registerViewport: (viewportId: string, element: HTMLElement) => void;
|
||||
unregisterViewport: (viewportId: string) => void;
|
||||
getViewportElement: (viewportId: string) => HTMLElement | null;
|
||||
viewportRefs: Map<string, HTMLElement>;
|
||||
};
|
||||
|
||||
// comment
|
||||
const ViewportRefsContext = createContext<ViewportRefsContextType | undefined>(undefined);
|
||||
|
||||
export const ViewportRefsProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const viewportRefsRef = useRef<Map<string, HTMLElement>>(new Map());
|
||||
|
||||
const registerViewport = (viewportId: string, element: HTMLElement) => {
|
||||
viewportRefsRef.current.set(viewportId, element);
|
||||
};
|
||||
|
||||
const unregisterViewport = (viewportId: string) => {
|
||||
viewportRefsRef.current.delete(viewportId);
|
||||
};
|
||||
|
||||
const getViewportElement = (viewportId: string): HTMLElement | null => {
|
||||
return viewportRefsRef.current.get(viewportId) || null;
|
||||
};
|
||||
|
||||
const contextValue: ViewportRefsContextType = {
|
||||
registerViewport,
|
||||
unregisterViewport,
|
||||
getViewportElement,
|
||||
viewportRefs: viewportRefsRef.current,
|
||||
};
|
||||
|
||||
return React.createElement(ViewportRefsContext.Provider, { value: contextValue }, children);
|
||||
};
|
||||
|
||||
export const useViewportRefs = () => {
|
||||
const context = useContext(ViewportRefsContext);
|
||||
|
||||
if (context === undefined) {
|
||||
throw new Error('useViewportRefs must be used within a ViewportRefsProvider');
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export const useViewportRef = (viewportId: string) => {
|
||||
const { registerViewport, unregisterViewport, getViewportElement } = useViewportRefs();
|
||||
|
||||
const ref = {
|
||||
current: getViewportElement(viewportId),
|
||||
register: (element: HTMLElement) => registerViewport(viewportId, element),
|
||||
unregister: () => unregisterViewport(viewportId),
|
||||
};
|
||||
|
||||
return ref;
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
import { useEffect, useState, useCallback, useMemo } from 'react';
|
||||
import { useViewportRef } from './';
|
||||
|
||||
interface ViewportSize {
|
||||
width: number;
|
||||
height: number;
|
||||
offsetLeft: number;
|
||||
offsetTop: number;
|
||||
clientRect: DOMRect | null;
|
||||
isVisible: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook that provides viewport size dimensions and monitors for changes
|
||||
* @param viewportId - The ID of the viewport to monitor
|
||||
* @returns ViewportSize object containing width, height, and visibility info
|
||||
*/
|
||||
function useViewportSize(viewportId: string): ViewportSize {
|
||||
const viewportElementRef = useViewportRef(viewportId);
|
||||
|
||||
const [size, setSize] = useState<ViewportSize>({
|
||||
width: 0,
|
||||
height: 0,
|
||||
offsetLeft: 0,
|
||||
offsetTop: 0,
|
||||
clientRect: null,
|
||||
isVisible: false,
|
||||
});
|
||||
|
||||
// Update viewport dimensions
|
||||
const updateViewportSize = useCallback(() => {
|
||||
if (!viewportElementRef?.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const element = viewportElementRef.current;
|
||||
const clientRect = element.getBoundingClientRect();
|
||||
const newWidth = clientRect.width;
|
||||
const newHeight = clientRect.height;
|
||||
const newOffsetLeft = element.offsetLeft;
|
||||
const newOffsetTop = element.offsetTop;
|
||||
const newIsVisible = newWidth > 0 && newHeight > 0;
|
||||
|
||||
setSize(prevSize => {
|
||||
if (
|
||||
prevSize.width === newWidth &&
|
||||
prevSize.height === newHeight &&
|
||||
prevSize.offsetLeft === newOffsetLeft &&
|
||||
prevSize.offsetTop === newOffsetTop &&
|
||||
prevSize.isVisible === newIsVisible
|
||||
) {
|
||||
return prevSize;
|
||||
}
|
||||
|
||||
return {
|
||||
width: newWidth,
|
||||
height: newHeight,
|
||||
offsetLeft: newOffsetLeft,
|
||||
offsetTop: newOffsetTop,
|
||||
clientRect,
|
||||
isVisible: newIsVisible,
|
||||
};
|
||||
});
|
||||
}, [viewportElementRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!viewportId || !viewportElementRef?.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateViewportSize();
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
updateViewportSize();
|
||||
});
|
||||
|
||||
resizeObserver.observe(viewportElementRef.current);
|
||||
|
||||
window.addEventListener('resize', updateViewportSize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateViewportSize);
|
||||
|
||||
if (viewportElementRef.current) {
|
||||
resizeObserver.unobserve(viewportElementRef.current);
|
||||
}
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [viewportId, viewportElementRef, updateViewportSize]);
|
||||
|
||||
const memoizedSize = useMemo(() => size, [size]);
|
||||
|
||||
return memoizedSize;
|
||||
}
|
||||
|
||||
export default useViewportSize;
|
||||
export { useViewportSize };
|
||||
@@ -2,6 +2,7 @@ import { ExtensionManager, MODULE_TYPES } from './extensions';
|
||||
import { ServiceProvidersManager, ServicesManager } from './services';
|
||||
import classes, { CommandsManager, HotkeysManager } from './classes';
|
||||
import { SystemContextProvider, useSystem } from './contextProviders/SystemProvider';
|
||||
import { ViewportRefsProvider } from './hooks/useViewportRef';
|
||||
|
||||
import DICOMWeb from './DICOMWeb';
|
||||
import errorHandler from './errorHandler.js';
|
||||
@@ -13,7 +14,6 @@ import utils from './utils';
|
||||
import defaults from './defaults';
|
||||
import * as Types from './types';
|
||||
import * as Enums from './enums';
|
||||
import { useToolbar } from './hooks/useToolbar';
|
||||
import {
|
||||
CineService,
|
||||
UIDialogService,
|
||||
@@ -42,6 +42,8 @@ import { DisplaySetMessage, DisplaySetMessageList } from './services/DisplaySetS
|
||||
import IWebApiDataSource from './DataSources/IWebApiDataSource';
|
||||
import useActiveViewportDisplaySets from './hooks/useActiveViewportDisplaySets';
|
||||
|
||||
export * from './hooks';
|
||||
|
||||
const hotkeys = {
|
||||
...utils.hotkeys,
|
||||
defaults: { hotkeyBindings: defaults.hotkeyBindings },
|
||||
@@ -86,7 +88,6 @@ const OHIF = {
|
||||
pubSubServiceInterface,
|
||||
PubSubService,
|
||||
PanelService,
|
||||
useToolbar,
|
||||
useActiveViewportDisplaySets,
|
||||
WorkflowStepsService,
|
||||
StudyPrefetcherService,
|
||||
@@ -101,6 +102,7 @@ export {
|
||||
ServicesManager,
|
||||
ServiceProvidersManager,
|
||||
SystemContextProvider,
|
||||
ViewportRefsProvider,
|
||||
//
|
||||
defaults,
|
||||
utils,
|
||||
@@ -137,7 +139,6 @@ export {
|
||||
WorkflowStepsService,
|
||||
StudyPrefetcherService,
|
||||
useSystem,
|
||||
useToolbar,
|
||||
useActiveViewportDisplaySets,
|
||||
};
|
||||
|
||||
|
||||
@@ -2,13 +2,68 @@ import { CommandsManager } from '../../classes';
|
||||
import { ExtensionManager } from '../../extensions';
|
||||
import { PubSubService } from '../_shared/pubSubServiceInterface';
|
||||
import type { RunCommand } from '../../types/Command';
|
||||
import { Button, ButtonProps, EvaluateFunction, EvaluatePublic, NestedButtonProps } from './types';
|
||||
import { Button, ButtonProps, EvaluateFunction, EvaluatePublic } from './types';
|
||||
|
||||
const EVENTS = {
|
||||
TOOL_BAR_MODIFIED: 'event::toolBarService:toolBarModified',
|
||||
TOOL_BAR_STATE_MODIFIED: 'event::toolBarService:toolBarStateModified',
|
||||
};
|
||||
|
||||
/**
|
||||
* Predefined toolbar sections used throughout the application
|
||||
*/
|
||||
export const TOOLBAR_SECTIONS = {
|
||||
/**
|
||||
* Main toolbar
|
||||
*/
|
||||
primary: 'primary',
|
||||
|
||||
/**
|
||||
* Secondary toolbar
|
||||
*/
|
||||
secondary: 'secondary',
|
||||
|
||||
/**
|
||||
* Viewport action menu sections
|
||||
*/
|
||||
viewportActionMenu: {
|
||||
topLeft: 'viewportActionMenu.topLeft',
|
||||
topRight: 'viewportActionMenu.topRight',
|
||||
bottomLeft: 'viewportActionMenu.bottomLeft',
|
||||
bottomRight: 'viewportActionMenu.bottomRight',
|
||||
topMiddle: 'viewportActionMenu.topMiddle',
|
||||
bottomMiddle: 'viewportActionMenu.bottomMiddle',
|
||||
leftMiddle: 'viewportActionMenu.leftMiddle',
|
||||
rightMiddle: 'viewportActionMenu.rightMiddle',
|
||||
},
|
||||
|
||||
/**
|
||||
* Measurement tools section
|
||||
*/
|
||||
measurementSection: 'measurementSection',
|
||||
|
||||
/**
|
||||
* More tools section
|
||||
*/
|
||||
moreToolsSection: 'moreToolsSection',
|
||||
|
||||
/**
|
||||
* Advanced rendering controls section
|
||||
*/
|
||||
advancedRenderingControlsSection: 'advancedRenderingControlsSection',
|
||||
};
|
||||
|
||||
export enum ButtonLocation {
|
||||
TopLeft = 0,
|
||||
TopMiddle = 1,
|
||||
TopRight = 2,
|
||||
LeftMiddle = 3,
|
||||
RightMiddle = 4,
|
||||
BottomLeft = 5,
|
||||
BottomMiddle = 6,
|
||||
BottomRight = 7,
|
||||
}
|
||||
|
||||
export default class ToolbarService extends PubSubService {
|
||||
public static REGISTRATION = {
|
||||
name: 'toolbarService',
|
||||
@@ -18,6 +73,13 @@ export default class ToolbarService extends PubSubService {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Access to predefined toolbar sections for autocomplete support
|
||||
*/
|
||||
public get sections() {
|
||||
return TOOLBAR_SECTIONS;
|
||||
}
|
||||
|
||||
public static createButton(options: {
|
||||
id: string;
|
||||
label: string;
|
||||
@@ -130,7 +192,7 @@ export default class ToolbarService extends PubSubService {
|
||||
* @param buttons - The buttons to be added.
|
||||
* @param replace - Flag indicating if any existing button with the same id as one being added should be replaced
|
||||
*/
|
||||
public addButtons(buttons: Button[], replace: boolean = false): void {
|
||||
public register(buttons: Button[], replace: boolean = false): void {
|
||||
buttons.forEach(button => {
|
||||
if (replace || !this.state.buttons[button.id]) {
|
||||
if (!button.props) {
|
||||
@@ -231,7 +293,8 @@ export default class ToolbarService extends PubSubService {
|
||||
* which buttons to evaluate based on the props
|
||||
*/
|
||||
public refreshToolbarState(refreshProps) {
|
||||
const buttons = this.state.buttons;
|
||||
const originalButtons = this.state.buttons;
|
||||
const updatedButtons = { ...originalButtons };
|
||||
const evaluationResults = new Map();
|
||||
|
||||
const evaluateButtonProps = (button, props, refreshProps) => {
|
||||
@@ -239,6 +302,7 @@ export default class ToolbarService extends PubSubService {
|
||||
const { disabled, disabledText, className, isActive } = evaluationResults.get(button.id);
|
||||
return { ...props, disabled, disabledText, className, isActive };
|
||||
} else {
|
||||
const evaluateProps = props.evaluateProps;
|
||||
const evaluated =
|
||||
typeof props.evaluate === 'function'
|
||||
? props.evaluate({ ...refreshProps, button })
|
||||
@@ -247,6 +311,7 @@ export default class ToolbarService extends PubSubService {
|
||||
...props,
|
||||
...evaluated,
|
||||
disabled: evaluated?.disabled || false,
|
||||
visible: evaluateProps?.hideWhenDisabled && evaluated?.disabled ? false : true,
|
||||
className: evaluated?.className || '',
|
||||
isActive: evaluated?.isActive, // isActive will be undefined for buttons without this prop
|
||||
};
|
||||
@@ -255,7 +320,14 @@ export default class ToolbarService extends PubSubService {
|
||||
}
|
||||
};
|
||||
|
||||
Object.values(buttons).forEach(button => {
|
||||
const updatedIds = new Set();
|
||||
Object.values(originalButtons).forEach(button => {
|
||||
// Note: do not re-evaluate buttons that have already been evaluated
|
||||
// this will result in inconsistencies in the toolbar state
|
||||
if (updatedIds.has(button.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasSection = (button.props as NestedButtonProps)?.buttonSection;
|
||||
|
||||
if (!hasSection) {
|
||||
@@ -263,10 +335,12 @@ export default class ToolbarService extends PubSubService {
|
||||
const buttonProps = button.props as ButtonProps;
|
||||
|
||||
const updatedProps = evaluateButtonProps(button, buttonProps, refreshProps);
|
||||
buttons[button.id] = {
|
||||
updatedButtons[button.id] = {
|
||||
...button,
|
||||
props: updatedProps,
|
||||
};
|
||||
|
||||
updatedIds.add(button.id);
|
||||
} else {
|
||||
let buttonProps = button.props as NestedButtonProps;
|
||||
const { evaluate: groupEvaluate } = buttonProps;
|
||||
@@ -288,17 +362,27 @@ export default class ToolbarService extends PubSubService {
|
||||
}
|
||||
|
||||
toolButtonIds.forEach(buttonId => {
|
||||
const button = buttons[buttonId];
|
||||
const button = originalButtons[buttonId];
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (updatedIds.has(buttonId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedProps = evaluateButtonProps(button, button.props, refreshProps);
|
||||
buttons[buttonId] = {
|
||||
updatedButtons[buttonId] = {
|
||||
...button,
|
||||
props: updatedProps,
|
||||
};
|
||||
|
||||
updatedIds.add(buttonId);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.setButtons(buttons);
|
||||
this.setButtons(updatedButtons);
|
||||
return this.state;
|
||||
}
|
||||
|
||||
@@ -373,7 +457,7 @@ export default class ToolbarService extends PubSubService {
|
||||
* @param {string} key - The key of the button section.
|
||||
* @param {Array} buttons - The buttons to be added to the section.
|
||||
*/
|
||||
createButtonSection(key, buttons) {
|
||||
updateSection(key, buttons) {
|
||||
if (this.state.buttonSections[key]) {
|
||||
this.state.buttonSections[key].push(
|
||||
...buttons.filter(
|
||||
@@ -443,7 +527,7 @@ export default class ToolbarService extends PubSubService {
|
||||
}
|
||||
|
||||
const { id, uiType } = btn;
|
||||
const { groupId } = btn.props as NestedButtonProps;
|
||||
const { buttonSection } = btn.props;
|
||||
|
||||
const buttonTypes = this._getButtonUITypes();
|
||||
|
||||
@@ -457,7 +541,7 @@ export default class ToolbarService extends PubSubService {
|
||||
return;
|
||||
}
|
||||
|
||||
!groupId ? this.handleEvaluate(btn.props) : this.handleEvaluateNested(btn.props);
|
||||
!buttonSection ? this.handleEvaluate(btn.props) : this.handleEvaluateNested(btn.props);
|
||||
|
||||
const { id: buttonId, props: componentProps } = btn;
|
||||
|
||||
@@ -587,6 +671,7 @@ export default class ToolbarService extends PubSubService {
|
||||
return evaluateFunction;
|
||||
});
|
||||
|
||||
const evaluateProps = props.evaluate;
|
||||
props.evaluate = args => {
|
||||
const results = evaluators.map(evaluator => evaluator(args)).filter(Boolean);
|
||||
|
||||
@@ -607,6 +692,8 @@ export default class ToolbarService extends PubSubService {
|
||||
return mergedResult;
|
||||
};
|
||||
|
||||
props.evaluateProps = evaluateProps;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -627,7 +714,9 @@ export default class ToolbarService extends PubSubService {
|
||||
const { name, ...options } = evaluate;
|
||||
const evaluateFunction = this._evaluateFunction[name];
|
||||
if (evaluateFunction) {
|
||||
const evaluateProps = props.evaluate;
|
||||
props.evaluate = args => evaluateFunction({ ...args, ...options });
|
||||
props.evaluateProps = evaluateProps;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -645,4 +734,56 @@ export default class ToolbarService extends PubSubService {
|
||||
this.state.buttonSections[buttonSection] = [];
|
||||
this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, { ...this.state });
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a button exists in any toolbar section.
|
||||
*
|
||||
* @param buttonId - The button ID to check for
|
||||
* @returns True if the button exists in any section, false otherwise
|
||||
*/
|
||||
isInAnySection(buttonId: string): boolean {
|
||||
if (!buttonId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check all sections to see if the button ID exists in any of them
|
||||
return Object.values(this.state.buttonSections).some(
|
||||
section => Array.isArray(section) && section.includes(buttonId)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the alignment and side for a specific viewport corner location.
|
||||
* Used for menu positioning based on the corner location.
|
||||
*
|
||||
* @param location - The viewport corner location
|
||||
* @returns An object with align and side properties
|
||||
*/
|
||||
public getAlignAndSide(location: ButtonLocation | string): {
|
||||
align: 'start' | 'end' | 'center';
|
||||
side: 'top' | 'bottom' | 'left' | 'right';
|
||||
} {
|
||||
const locationNumber = Number(location);
|
||||
switch (locationNumber) {
|
||||
case ButtonLocation.TopLeft: // Enum 0, Original 0 (topLeft)
|
||||
return { align: 'start', side: 'bottom' };
|
||||
case ButtonLocation.TopMiddle: // Enum 1, Original 4 (topMiddle)
|
||||
return { align: 'center', side: 'bottom' };
|
||||
case ButtonLocation.TopRight: // Enum 2, Original 1 (topRight)
|
||||
return { align: 'end', side: 'bottom' };
|
||||
case ButtonLocation.LeftMiddle: // Enum 3, Original 6 (leftMiddle)
|
||||
return { align: 'start', side: 'right' };
|
||||
case ButtonLocation.RightMiddle: // Enum 4, Original 7 (rightMiddle)
|
||||
return { align: 'end', side: 'left' };
|
||||
case ButtonLocation.BottomLeft: // Enum 5, Original 2 (bottomLeft)
|
||||
return { align: 'start', side: 'top' };
|
||||
case ButtonLocation.BottomMiddle: // Enum 6, Original 5 (bottomMiddle)
|
||||
return { align: 'center', side: 'top' };
|
||||
case ButtonLocation.BottomRight: // Enum 7, Original 3 (bottomRight)
|
||||
return { align: 'end', side: 'top' };
|
||||
default:
|
||||
// Default to TopLeft behavior if an unexpected value is passed.
|
||||
return { align: 'start', side: 'bottom' };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
import ToolbarService from './ToolbarService';
|
||||
|
||||
import ToolbarService, { TOOLBAR_SECTIONS } from './ToolbarService';
|
||||
export default ToolbarService;
|
||||
export { TOOLBAR_SECTIONS };
|
||||
@@ -1,6 +1,22 @@
|
||||
import type { RunCommand } from '../../types/Command';
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Type definitions for toolbar sections
|
||||
*/
|
||||
export type ToolbarSections = {
|
||||
primary: string;
|
||||
secondary: string;
|
||||
viewportActionMenu: {
|
||||
topLeft: string;
|
||||
topRight: string;
|
||||
bottomLeft: string;
|
||||
bottomRight: string;
|
||||
};
|
||||
measurementSection: string;
|
||||
moreToolsSection: string;
|
||||
};
|
||||
|
||||
export type EvaluatePublic =
|
||||
| string
|
||||
| EvaluateFunction
|
||||
|
||||
@@ -137,7 +137,7 @@ class WorkflowStepsService extends PubSubService {
|
||||
|
||||
toUse.forEach(({ buttonSection, buttons }) => {
|
||||
toolbarService.clearButtonSection(buttonSection);
|
||||
toolbarService.createButtonSection(buttonSection, buttons);
|
||||
toolbarService.updateSection(buttonSection, buttons);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -35,9 +35,19 @@ export type DisplaySet = {
|
||||
*/
|
||||
renderedUrl?: string;
|
||||
|
||||
/**
|
||||
* The instance UID of the display set that this display set references.
|
||||
* This is used to determine if the display set is a referenced display set.
|
||||
* It usually is for SEG, RTSTRUCT, etc.
|
||||
*/
|
||||
referencedDisplaySetInstanceUID?: string;
|
||||
|
||||
SeriesDate?: string;
|
||||
SeriesTime?: string;
|
||||
instance?: InstanceMetadata;
|
||||
|
||||
isHydrated?: boolean;
|
||||
isRehydratable?: boolean;
|
||||
};
|
||||
|
||||
export type DisplaySetSeriesMetadataInvalidatedEvent = {
|
||||
|
||||
Reference in new issue
Block a user