Initial commit from prod-batam
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
import * as ContextMenuItemsBuilder from './ContextMenuItemsBuilder';
|
||||
import ContextMenu from '../../../../platform/ui/src/components/ContextMenu/ContextMenu';
|
||||
import { CommandsManager } from '@ohif/core';
|
||||
import { annotation as CsAnnotation } from '@cornerstonejs/tools';
|
||||
import { Menu, MenuItem, Point, ContextMenuProps } from './types';
|
||||
|
||||
/**
|
||||
* The context menu controller is a helper class that knows how
|
||||
* to manage context menus based on the UI Customization Service.
|
||||
* There are a few parts to this:
|
||||
* 1. Basic controls to manage displaying and hiding context menus
|
||||
* 2. Menu selection services, which use the UI customization service
|
||||
* to choose which menu to display
|
||||
* 3. Menu item adapter services to convert menu items into displayable and actionable items.
|
||||
*
|
||||
* The format for a menu is defined in the exported type MenuItem
|
||||
*/
|
||||
export default class ContextMenuController {
|
||||
commandsManager: CommandsManager;
|
||||
services: AppTypes.Services;
|
||||
menuItems: Menu[] | MenuItem[];
|
||||
|
||||
constructor(servicesManager: AppTypes.ServicesManager, commandsManager: CommandsManager) {
|
||||
this.services = servicesManager.services;
|
||||
this.commandsManager = commandsManager;
|
||||
}
|
||||
|
||||
closeContextMenu() {
|
||||
this.services.uiDialogService.dismiss({ id: 'context-menu' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Figures out which context menu is appropriate to display and shows it.
|
||||
*
|
||||
* @param contextMenuProps - the context menu properties, see ./types.ts
|
||||
* @param viewportElement - the DOM element this context menu is related to
|
||||
* @param defaultPointsPosition - a default position to show the context menu
|
||||
*/
|
||||
showContextMenu(
|
||||
contextMenuProps: ContextMenuProps,
|
||||
viewportElement,
|
||||
defaultPointsPosition
|
||||
): void {
|
||||
if (!this.services.uiDialogService) {
|
||||
console.warn('Unable to show dialog; no UI Dialog Service available.');
|
||||
return;
|
||||
}
|
||||
|
||||
const { event, subMenu, menuId, menus, selectorProps } = contextMenuProps;
|
||||
|
||||
const annotationManager = CsAnnotation.state.getAnnotationManager();
|
||||
const { locking } = CsAnnotation;
|
||||
const targetAnnotationId = selectorProps?.nearbyToolData?.annotationUID as string;
|
||||
const isLocked = locking.isAnnotationLocked(
|
||||
annotationManager.getAnnotation(targetAnnotationId)
|
||||
);
|
||||
|
||||
if (isLocked) {
|
||||
console.warn('Annotation is locked.');
|
||||
return;
|
||||
}
|
||||
|
||||
const items = ContextMenuItemsBuilder.getMenuItems(
|
||||
selectorProps || contextMenuProps,
|
||||
event,
|
||||
menus,
|
||||
menuId
|
||||
);
|
||||
|
||||
this.services.uiDialogService.dismiss({ id: 'context-menu' });
|
||||
this.services.uiDialogService.create({
|
||||
id: 'context-menu',
|
||||
isDraggable: false,
|
||||
preservePosition: false,
|
||||
preventCutOf: true,
|
||||
defaultPosition: ContextMenuController._getDefaultPosition(
|
||||
defaultPointsPosition,
|
||||
event?.detail,
|
||||
viewportElement
|
||||
),
|
||||
event,
|
||||
content: ContextMenu,
|
||||
|
||||
// This naming is part of the uiDialogService convention
|
||||
// Clicking outside simply closes the dialog box.
|
||||
onClickOutside: () => this.services.uiDialogService.dismiss({ id: 'context-menu' }),
|
||||
|
||||
contentProps: {
|
||||
items,
|
||||
selectorProps,
|
||||
menus,
|
||||
event,
|
||||
subMenu,
|
||||
eventData: event?.detail,
|
||||
|
||||
onClose: () => {
|
||||
this.services.uiDialogService.dismiss({ id: 'context-menu' });
|
||||
},
|
||||
|
||||
/**
|
||||
* Displays a sub-menu, removing this menu
|
||||
* @param {*} item
|
||||
* @param {*} itemRef
|
||||
* @param {*} subProps
|
||||
*/
|
||||
onShowSubMenu: (item, itemRef, subProps) => {
|
||||
if (!itemRef.subMenu) {
|
||||
console.warn('No submenu defined for', item, itemRef, subProps);
|
||||
return;
|
||||
}
|
||||
this.showContextMenu(
|
||||
{
|
||||
...contextMenuProps,
|
||||
menuId: itemRef.subMenu,
|
||||
},
|
||||
viewportElement,
|
||||
defaultPointsPosition
|
||||
);
|
||||
},
|
||||
|
||||
// Default is to run the specified commands.
|
||||
onDefault: (item, itemRef, subProps) => {
|
||||
this.commandsManager.run(item, {
|
||||
...selectorProps,
|
||||
...itemRef,
|
||||
subProps,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static getDefaultPosition = (): Point => {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
};
|
||||
|
||||
static _getEventDefaultPosition = eventDetail => ({
|
||||
x: eventDetail && eventDetail.currentPoints.client[0],
|
||||
y: eventDetail && eventDetail.currentPoints.client[1],
|
||||
});
|
||||
|
||||
static _getElementDefaultPosition = element => {
|
||||
if (element) {
|
||||
const boundingClientRect = element.getBoundingClientRect();
|
||||
return {
|
||||
x: boundingClientRect.x,
|
||||
y: boundingClientRect.y,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
x: undefined,
|
||||
y: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
static _getCanvasPointsPosition = (points = [], element) => {
|
||||
const viewerPos = ContextMenuController._getElementDefaultPosition(element);
|
||||
|
||||
for (let pointIndex = 0; pointIndex < points.length; pointIndex++) {
|
||||
const point = {
|
||||
x: points[pointIndex][0] || points[pointIndex]['x'],
|
||||
y: points[pointIndex][1] || points[pointIndex]['y'],
|
||||
};
|
||||
if (
|
||||
ContextMenuController._isValidPosition(point) &&
|
||||
ContextMenuController._isValidPosition(viewerPos)
|
||||
) {
|
||||
return {
|
||||
x: point.x + viewerPos.x,
|
||||
y: point.y + viewerPos.y,
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static _isValidPosition = (source): boolean => {
|
||||
return source && typeof source.x === 'number' && typeof source.y === 'number';
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the context menu default position. It look for the positions of: canvasPoints (got from selected), event that triggers it, current viewport element
|
||||
*/
|
||||
static _getDefaultPosition = (canvasPoints, eventDetail, viewerElement) => {
|
||||
function* getPositionIterator() {
|
||||
yield ContextMenuController._getCanvasPointsPosition(canvasPoints, viewerElement);
|
||||
yield ContextMenuController._getEventDefaultPosition(eventDetail);
|
||||
yield ContextMenuController._getElementDefaultPosition(viewerElement);
|
||||
yield ContextMenuController.getDefaultPosition();
|
||||
}
|
||||
|
||||
const positionIterator = getPositionIterator();
|
||||
|
||||
let current = positionIterator.next();
|
||||
let position = current.value;
|
||||
|
||||
while (!current.done) {
|
||||
position = current.value;
|
||||
|
||||
if (ContextMenuController._isValidPosition(position)) {
|
||||
positionIterator.return();
|
||||
}
|
||||
current = positionIterator.next();
|
||||
}
|
||||
|
||||
return position;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import * as ContextMenuItemsBuilder from './ContextMenuItemsBuilder';
|
||||
|
||||
const menus = [
|
||||
{
|
||||
id: 'one',
|
||||
selector: ({ value } = {}) => value === 'one',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
id: 'two',
|
||||
selector: ({ value } = {}) => value === 'two',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
id: 'default',
|
||||
items: [],
|
||||
},
|
||||
];
|
||||
|
||||
describe('ContextMenuItemsBuilder', () => {
|
||||
test('findMenuDefault', () => {
|
||||
expect(ContextMenuItemsBuilder.findMenuDefault(menus, {})).toBe(menus[2]);
|
||||
expect(
|
||||
ContextMenuItemsBuilder.findMenuDefault(menus, { selectorProps: { value: 'two' } })
|
||||
).toBe(menus[1]);
|
||||
expect(ContextMenuItemsBuilder.findMenuDefault([], {})).toBeUndefined();
|
||||
expect(ContextMenuItemsBuilder.findMenuDefault(undefined, undefined)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,176 @@
|
||||
import { Types } from '@ohif/ui';
|
||||
import { Menu, SelectorProps, MenuItem, ContextMenuProps } from './types';
|
||||
|
||||
type ContextMenuItem = Types.ContextMenuItem;
|
||||
|
||||
/**
|
||||
* Finds menu by menu id
|
||||
*
|
||||
* @returns Menu having the menuId
|
||||
*/
|
||||
export function findMenuById(menus: Menu[], menuId?: string): Menu {
|
||||
if (!menuId) {
|
||||
return;
|
||||
}
|
||||
|
||||
return menus.find(menu => menu.id === menuId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Default finding menu method. This method will go through
|
||||
* the list of menus until it finds the first one which
|
||||
* has no selector, OR has the selector, when applied to the
|
||||
* check props, return true.
|
||||
* The selectorProps are a set of provided properties which can be
|
||||
* passed into the selector function to determine when to display a menu.
|
||||
* For example, a selector function of:
|
||||
* `({displayset}) => displaySet?.SeriesDescription?.indexOf?.('Left')!==-1
|
||||
* would match series descriptions containing 'Left'.
|
||||
*
|
||||
* @param {Object[]} menus List of menus
|
||||
* @param {*} subProps
|
||||
* @returns
|
||||
*/
|
||||
export function findMenuDefault(menus: Menu[], subProps: Record<string, unknown>): Menu {
|
||||
if (!menus) {
|
||||
return null;
|
||||
}
|
||||
return menus.find(menu => !menu.selector || menu.selector(subProps.selectorProps));
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the menu to be used for different scenarios:
|
||||
* This will first look for a subMenu with the specified subMenuId
|
||||
* Next it will look for the first menu whose selector returns true.
|
||||
*
|
||||
* @param menus - List of menus
|
||||
* @param props - root props
|
||||
* @param menuIdFilter - menu id identifier (to be considered on selection)
|
||||
* This is intended to support other types of filtering in the future.
|
||||
*/
|
||||
export function findMenu(menus: Menu[], props?: Types.IProps, menuIdFilter?: string) {
|
||||
const { subMenu } = props;
|
||||
|
||||
function* findMenuIterator() {
|
||||
yield findMenuById(menus, menuIdFilter || subMenu);
|
||||
yield findMenuDefault(menus, props);
|
||||
}
|
||||
|
||||
const findIt = findMenuIterator();
|
||||
|
||||
let current = findIt.next();
|
||||
let menu = current.value;
|
||||
|
||||
while (!current.done) {
|
||||
menu = current.value;
|
||||
|
||||
if (menu) {
|
||||
findIt.return();
|
||||
}
|
||||
current = findIt.next();
|
||||
}
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the menu from a list of possible menus, based on the actual state of component props and tool data nearby.
|
||||
* This uses the findMenu command above to first find the appropriate
|
||||
* menu, and then it chooses the actual contents of that menu.
|
||||
* A menu item can be optional by implementing the 'selector',
|
||||
* which will be called with the selectorProps, and if it does not return true,
|
||||
* then the item is excluded.
|
||||
*
|
||||
* Other menus can be delegated to by setting the delegating value to
|
||||
* a string id for another menu. That menu's content will replace the
|
||||
* current menu item (only if the item would be included).
|
||||
*
|
||||
* This allows single id menus to be chosen by id, but have varying contents
|
||||
* based on the delegated menus.
|
||||
*
|
||||
* Finally, for each item, the adaptItem call is made. This allows
|
||||
* items to modify themselves before being displayed, such as
|
||||
* incorporating additional information from translation sources.
|
||||
* See the `test-mode` examples for details.
|
||||
*
|
||||
* @param selectorProps
|
||||
* @param {*} event event that originates the context menu
|
||||
* @param {*} menus List of menus
|
||||
* @param {*} menuIdFilter
|
||||
* @returns
|
||||
*/
|
||||
export function getMenuItems(
|
||||
selectorProps: SelectorProps,
|
||||
event: Event,
|
||||
menus: Menu[],
|
||||
menuIdFilter?: string
|
||||
): MenuItem[] | void {
|
||||
// Include both the check props and the ...check props as one is used
|
||||
// by the child menu and the other used by the selector function
|
||||
const subProps = { selectorProps, event };
|
||||
|
||||
const menu = findMenu(menus, subProps, menuIdFilter);
|
||||
|
||||
if (!menu) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!menu.items) {
|
||||
console.warn('Must define items in menu', menu);
|
||||
return [];
|
||||
}
|
||||
|
||||
let menuItems = [];
|
||||
menu.items.forEach(item => {
|
||||
const { delegating, selector, subMenu } = item;
|
||||
|
||||
if (!selector || selector(selectorProps)) {
|
||||
if (delegating) {
|
||||
menuItems = [...menuItems, ...getMenuItems(selectorProps, event, menus, subMenu)];
|
||||
} else {
|
||||
const toAdd = adaptItem(item, subProps);
|
||||
menuItems.push(toAdd);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return menuItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns item adapted to be consumed by ContextMenu component
|
||||
* and then goes through the item to add action behaviour for clicking the item,
|
||||
* making it compatible with the default ContextMenu display.
|
||||
*
|
||||
* @param {Object} item
|
||||
* @param {Object} subProps
|
||||
* @returns a MenuItem that is compatible with the base ContextMenu
|
||||
* This requires having a label and set of actions to be called.
|
||||
*/
|
||||
export function adaptItem(item: MenuItem, subProps: ContextMenuProps): ContextMenuItem {
|
||||
const newItem: ContextMenuItem = {
|
||||
...item,
|
||||
value: subProps.selectorProps?.value,
|
||||
};
|
||||
|
||||
if (item.actionType === 'ShowSubMenu' && !newItem.iconRight) {
|
||||
newItem.iconRight = 'chevron-menu';
|
||||
}
|
||||
if (!item.action) {
|
||||
newItem.action = (itemRef, componentProps) => {
|
||||
const { event = {} } = componentProps;
|
||||
const { detail = {} } = event;
|
||||
newItem.element = detail.element;
|
||||
|
||||
componentProps.onClose();
|
||||
const action = componentProps[`on${itemRef.actionType || 'Default'}`];
|
||||
if (action) {
|
||||
action.call(componentProps, newItem, itemRef, subProps);
|
||||
} else {
|
||||
console.warn('No action defined for', itemRef);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return newItem;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
const defaultContextMenu = {
|
||||
id: 'measurementsContextMenu',
|
||||
customizationType: 'ohif.contextMenu',
|
||||
menus: [
|
||||
// Get the items from the UI Customization for the menu name (and have a custom name)
|
||||
{
|
||||
id: 'forExistingMeasurement',
|
||||
selector: ({ nearbyToolData }) => !!nearbyToolData,
|
||||
items: [
|
||||
{
|
||||
label: 'Delete measurement',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'deleteMeasurement',
|
||||
// we only have support for cornerstoneTools context menu since
|
||||
// they are svg based
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Add Label',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setMeasurementLabel',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default defaultContextMenu;
|
||||
11
extensions/default/src/CustomizableContextMenu/index.ts
Normal file
11
extensions/default/src/CustomizableContextMenu/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import ContextMenuController from './ContextMenuController';
|
||||
import * as ContextMenuItemsBuilder from './ContextMenuItemsBuilder';
|
||||
import defaultContextMenu from './defaultContextMenu';
|
||||
import * as CustomizableContextMenuTypes from './types';
|
||||
|
||||
export {
|
||||
ContextMenuController,
|
||||
CustomizableContextMenuTypes,
|
||||
ContextMenuItemsBuilder,
|
||||
defaultContextMenu,
|
||||
};
|
||||
125
extensions/default/src/CustomizableContextMenu/types.ts
Normal file
125
extensions/default/src/CustomizableContextMenu/types.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { Types } from '@ohif/core';
|
||||
|
||||
/**
|
||||
* SelectorProps are properties used to decide whether to select a menu or
|
||||
* menu item for display.
|
||||
* An instance of SelectorProps is provided to the selector functions, which
|
||||
* return true to include the item or false to exclude it.
|
||||
* The point of this is to allow more specific context menus which hide
|
||||
* non-relevant menu options, optimizing the speed of selection of menus
|
||||
*/
|
||||
export interface SelectorProps {
|
||||
// If the context menu is invoked in the context of a measurement, then it
|
||||
// will contain the nearby tool data.
|
||||
nearbyToolData?: Record<string, unknown>;
|
||||
|
||||
// The tool name for the nearby tool
|
||||
toolName?: string;
|
||||
|
||||
// An annotation UID - this will be present if nearbyToolData is present.
|
||||
uid?: string;
|
||||
|
||||
// If the context menu is invoked on an active viewport, then it will contain
|
||||
// the first display set.
|
||||
displaySet?: Record<string, unknown>;
|
||||
|
||||
// The triggering event - can be used to determine key modifiers
|
||||
event?: Event;
|
||||
|
||||
// Any other properties
|
||||
[propertyName: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of item actually required for the ContextMenu UI display
|
||||
*/
|
||||
export type UIMenuItem = {
|
||||
label: string;
|
||||
// Called when the item is selected
|
||||
action?: (itemRef, componentProps) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* A MenuItem is a single line item within a menu, and specifies a selectable
|
||||
* value for the menu.
|
||||
*/
|
||||
export interface MenuItem {
|
||||
id?: string;
|
||||
/** The customization type is used to apply preset values to this item
|
||||
* when registered with the customization service.
|
||||
*/
|
||||
customizationType?: string;
|
||||
|
||||
// The label is the value to show in the menu for this item
|
||||
label?: string;
|
||||
|
||||
// Delegating items are used to include other sub-menus inline within
|
||||
// this menu. That allows sharing part of the menu structure, but also,
|
||||
// more importantly to use a single selector function to include/exclude
|
||||
// and entire section of sub-menu.
|
||||
// See the `siteSelectionSubMenu` within the example `findingsMenu`
|
||||
// for an example
|
||||
delegating?: boolean;
|
||||
|
||||
// A sub-menu is shown when this item is selected or is delegating.
|
||||
// This item gives the name of the sub-menu.
|
||||
subMenu?: string;
|
||||
|
||||
// The selector is used to determine if this menu entry will be shown
|
||||
// or more importantly, if the delegating subMenu will be included.
|
||||
selector?: (props: SelectorProps) => boolean;
|
||||
|
||||
/** Adapts the item by filling in additional properties as required */
|
||||
adaptItem?: (item: MenuItem, props: ContextMenuProps) => UIMenuItem;
|
||||
|
||||
/** List of commands to run when this item's action is taken. */
|
||||
commands?: Types.Command[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A menu is a list of menu items, plus a selector.
|
||||
* The selector is used to determine whether the menu should be displayed
|
||||
* in a given context. The parameters passed to the selector come from
|
||||
* the 'selectorProps' value in the options, and are intended to be context
|
||||
* specific values containing things like the selected object, the currently
|
||||
* displayed study etc so that the context menu can dynamically choose which
|
||||
* view to show.
|
||||
*/
|
||||
export interface Menu {
|
||||
id: string;
|
||||
|
||||
/** The customization type is used to apply preset values to this item
|
||||
* when registered with the customization service.
|
||||
*/
|
||||
customizationType?: string;
|
||||
|
||||
// Choose whether this menu applies.
|
||||
selector?: Types.Predicate;
|
||||
|
||||
items: MenuItem[];
|
||||
}
|
||||
|
||||
export type Point = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* ContextMenuProps is the top level argument used to invoke the context menu
|
||||
* itself. It contains the menus available for display, as well as the event
|
||||
* and selector props used to decide the menu.
|
||||
*/
|
||||
export type ContextMenuProps = {
|
||||
event?: EventTarget;
|
||||
menuCustomizationId?: string;
|
||||
menuId: string;
|
||||
element?: HTMLElement;
|
||||
|
||||
/** A set of menus to choose from for this context menu */
|
||||
menus: Menu[];
|
||||
|
||||
/** The properties used to decide the menu type */
|
||||
selectorProps: SelectorProps;
|
||||
|
||||
defaultPointsPosition?: [number, number] | [];
|
||||
};
|
||||
Reference in New Issue
Block a user