feat: Allow configurable context menus (#2894)
* feat: Context menu feat: Custom context menu Adding documentation PR updates * fix: Capture tool exception * PR updates * Add fully worked out examples in the basic test mode/extension * Fix the menu display * fix: Make the commands on clicks much more configurable * Wait for load before double clicking * docs * PR changes - nothing functional, just moving things endlessly * PR comments * PR changes - rename the default context menu * Renamed the cornerstoneContextMenu to measurementsContextMenu * Add chevron right to the sub-menus
This commit is contained in:
1 parent
4e94b588a7
commit
bc642fd2b6
40 files changed
+1472
-280
No files matched your search
@@ -0,0 +1,208 @@
|
||||
import * as ContextMenuItemsBuilder from './ContextMenuItemsBuilder';
|
||||
import ContextMenu from '../../../../platform/ui/src/components/ContextMenu/ContextMenu';
|
||||
import { CommandsManager, ServicesManager, Types } from '@ohif/core';
|
||||
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: Types.Services;
|
||||
menuItems: Menu[] | MenuItem[];
|
||||
|
||||
constructor(
|
||||
servicesManager: ServicesManager,
|
||||
commandsManager: CommandsManager
|
||||
) {
|
||||
this.services = servicesManager.services as Obj;
|
||||
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;
|
||||
|
||||
console.log('Getting items from', menus);
|
||||
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 hte uiDialogService convention
|
||||
// Clicking outside simpy 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 ContextMenuItemsBuilder from "./ContextMenuItemsBuilder";
|
||||
|
||||
const menus = [
|
||||
{
|
||||
id: 'one',
|
||||
selector: ({ value }) => value === 'one',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
id: 'two',
|
||||
selector: ({ value }) => value === 'two',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
id: 'default',
|
||||
items: [],
|
||||
},
|
||||
];
|
||||
|
||||
const menuBuilder = new ContextMenuItemsBuilder();
|
||||
|
||||
describe('ContextMenuItemsBuilder', () => {
|
||||
test('findMenuDefault', () => {
|
||||
expect(menuBuilder.findMenuDefault(menus, {})).toBe(menus[2]);
|
||||
expect(menuBuilder.findMenuDefault(menus, { value: 'two' })).toBe(menus[1]);
|
||||
expect(menuBuilder.findMenuDefault([], {})).toBeUndefined();
|
||||
expect(menuBuilder.findMenuDefault(undefined, undefined)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,193 @@
|
||||
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();
|
||||
}
|
||||
|
||||
console.log('Menu chosen', menu?.id || 'NONE');
|
||||
|
||||
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: Types.IProps,
|
||||
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-right';
|
||||
}
|
||||
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,31 @@
|
||||
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',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Add Label',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setMeasurementLabel',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default defaultContextMenu;
|
||||
@@ -0,0 +1,11 @@
|
||||
import ContextMenuController from './ContextMenuController';
|
||||
import ContextMenuItemsBuilder from './ContextMenuItemsBuilder';
|
||||
import defaultContextMenu from './defaultContextMenu';
|
||||
import * as CustomizeableContextMenuTypes from './types';
|
||||
|
||||
export {
|
||||
ContextMenuController,
|
||||
CustomizeableContextMenuTypes,
|
||||
ContextMenuItemsBuilder,
|
||||
defaultContextMenu,
|
||||
};
|
||||
@@ -0,0 +1,123 @@
|
||||
import { Types } from '@ohif/core';
|
||||
|
||||
/**
|
||||
* SelectorProps are properties used to decide whether to select a manu 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 conext menus which hide
|
||||
* non-relevant menu options, optimizing the speed of selection of menus
|
||||
* (See Bill Wallace's masters thesis for selection time versus complexity of user 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 nearyToolData 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 requried */
|
||||
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;
|
||||
subMenu?: string;
|
||||
menuId: string;
|
||||
|
||||
/** A set of menus to choose from for this context menu */
|
||||
menus: Menu[];
|
||||
|
||||
/** The properties used to decide the menu type */
|
||||
selectorProps: SelectorProps;
|
||||
};
|
||||
@@ -1,5 +1,9 @@
|
||||
import { DicomMetadataStore, ServicesManager } from '@ohif/core';
|
||||
import { ServicesManager, Types } from '@ohif/core';
|
||||
|
||||
import {
|
||||
ContextMenuController,
|
||||
defaultContextMenu,
|
||||
} from './CustomizeableContextMenu';
|
||||
import DicomTagBrowser from './DicomTagBrowser/DicomTagBrowser';
|
||||
import reuseCachedLayouts from './utils/reuseCachedLayouts';
|
||||
import findViewportsByPosition, {
|
||||
@@ -23,8 +27,12 @@ const isHangingProtocolCommand = command =>
|
||||
(command.commandName === 'setHangingProtocol' ||
|
||||
command.commandName === 'toggleHangingProtocol');
|
||||
|
||||
const commandsModule = ({ servicesManager, commandsManager }) => {
|
||||
const commandsModule = ({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
}: Types.Extensions.ExtensionParams): Types.Extensions.CommandsModule => {
|
||||
const {
|
||||
customizationService,
|
||||
measurementService,
|
||||
hangingProtocolService,
|
||||
uiNotificationService,
|
||||
@@ -34,7 +42,60 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
|
||||
toolbarService,
|
||||
} = (servicesManager as ServicesManager).services;
|
||||
|
||||
// Define a context menu controller for use with any context menus
|
||||
const contextMenuController = new ContextMenuController(
|
||||
servicesManager,
|
||||
commandsManager
|
||||
);
|
||||
|
||||
const actions = {
|
||||
/**
|
||||
* Show the context menu.
|
||||
* @param options.menuId defines the menu name to lookup, from customizationService
|
||||
* @param options.defaultMenu contains the default menu set to use
|
||||
* @param options.element is the element to show the menu within
|
||||
* @param options.event is the event that caused the context menu
|
||||
* @param options.selectorProps is the set of selection properties to use
|
||||
*/
|
||||
showContextMenu: options => {
|
||||
const {
|
||||
menuCustomizationId,
|
||||
element,
|
||||
event,
|
||||
selectorProps,
|
||||
defaultPointsPosition = [],
|
||||
} = options;
|
||||
|
||||
const optionsToUse = { ...options };
|
||||
|
||||
if (menuCustomizationId) {
|
||||
Object.assign(
|
||||
optionsToUse,
|
||||
customizationService.get(menuCustomizationId, defaultContextMenu)
|
||||
);
|
||||
}
|
||||
|
||||
// TODO - make the selectorProps richer by including the study metadata and display set.
|
||||
const { protocol, stage } = hangingProtocolService.getActiveProtocol();
|
||||
optionsToUse.selectorProps = {
|
||||
event,
|
||||
protocol,
|
||||
stage,
|
||||
...selectorProps,
|
||||
};
|
||||
|
||||
contextMenuController.showContextMenu(
|
||||
optionsToUse,
|
||||
element,
|
||||
defaultPointsPosition
|
||||
);
|
||||
},
|
||||
|
||||
/** Close a context menu currently displayed */
|
||||
closeContextMenu: () => {
|
||||
contextMenuController.closeContextMenu();
|
||||
},
|
||||
|
||||
displayNotification: ({ text, title, type }) => {
|
||||
uiNotificationService.show({
|
||||
title: title,
|
||||
@@ -336,6 +397,12 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
showContextMenu: {
|
||||
commandFn: actions.showContextMenu,
|
||||
},
|
||||
closeContextMenu: {
|
||||
commandFn: actions.closeContextMenu,
|
||||
},
|
||||
clearMeasurements: {
|
||||
commandFn: actions.clearMeasurements,
|
||||
storeContexts: [],
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CustomizationService } from '@ohif/core';
|
||||
import React from 'react';
|
||||
import DataSourceSelector from './Panels/DataSourceSelector';
|
||||
|
||||
@@ -82,7 +83,6 @@ export default function getCustomizationModule() {
|
||||
*/
|
||||
{
|
||||
id: 'ohif.overlayItem',
|
||||
uiType: 'uiType',
|
||||
content: function (props) {
|
||||
if (this.condition && !this.condition(props)) return null;
|
||||
|
||||
@@ -91,8 +91,8 @@ export default function getCustomizationModule() {
|
||||
instance && this.attribute
|
||||
? instance[this.attribute]
|
||||
: this.contentF && typeof this.contentF === 'function'
|
||||
? this.contentF(props)
|
||||
: null;
|
||||
? this.contentF(props)
|
||||
: null;
|
||||
if (!value) return null;
|
||||
|
||||
return (
|
||||
@@ -109,6 +109,26 @@ export default function getCustomizationModule() {
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
id: 'ohif.contextMenu',
|
||||
|
||||
/** Applies the customizationType to all the menu items */
|
||||
transform: function (customizationService: CustomizationService) {
|
||||
// Don't modify the children, as those are copied by reference
|
||||
const clonedObject = { ...this };
|
||||
clonedObject.menus = this.menus.map(it => ({ ...it }));
|
||||
|
||||
for (const menu of clonedObject.menus) {
|
||||
const { items: originalItems } = menu;
|
||||
menu.items = [];
|
||||
for (const item of originalItems) {
|
||||
menu.items.push(customizationService.transform(item));
|
||||
}
|
||||
}
|
||||
return clonedObject;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,32 +1,34 @@
|
||||
import { Types } from '@ohif/core';
|
||||
|
||||
import getDataSourcesModule from './getDataSourcesModule.js';
|
||||
import getLayoutTemplateModule from './getLayoutTemplateModule.js';
|
||||
import getPanelModule from './getPanelModule';
|
||||
import getSopClassHandlerModule from './getSopClassHandlerModule.js';
|
||||
import getToolbarModule from './getToolbarModule';
|
||||
import commandsModule from './commandsModule';
|
||||
import getCommandsModule from './commandsModule';
|
||||
import getHangingProtocolModule from './getHangingProtocolModule';
|
||||
import getStudiesForPatientByStudyInstanceUID from './Panels/getStudiesForPatientByStudyInstanceUID';
|
||||
import getCustomizationModule from './getCustomizationModule';
|
||||
import { id } from './id.js';
|
||||
import init from './init';
|
||||
import preRegistration from './init';
|
||||
import {
|
||||
ContextMenuController,
|
||||
CustomizeableContextMenuTypes,
|
||||
} from './CustomizeableContextMenu';
|
||||
|
||||
const defaultExtension = {
|
||||
const defaultExtension: Types.Extensions.Extension = {
|
||||
/**
|
||||
* Only required property. Should be a unique value across all extensions.
|
||||
*/
|
||||
id,
|
||||
preRegistration: ({ servicesManager, configuration = {} }) => {
|
||||
init({ servicesManager, configuration });
|
||||
},
|
||||
preRegistration,
|
||||
getDataSourcesModule,
|
||||
getLayoutTemplateModule,
|
||||
getPanelModule,
|
||||
getHangingProtocolModule,
|
||||
getSopClassHandlerModule,
|
||||
getToolbarModule,
|
||||
getCommandsModule({ servicesManager, commandsManager }) {
|
||||
return commandsModule({ servicesManager, commandsManager });
|
||||
},
|
||||
getCommandsModule,
|
||||
getUtilityModule({ servicesManager }) {
|
||||
return [
|
||||
{
|
||||
@@ -42,3 +44,5 @@ const defaultExtension = {
|
||||
};
|
||||
|
||||
export default defaultExtension;
|
||||
|
||||
export { ContextMenuController, CustomizeableContextMenuTypes };
|
||||
@@ -10,7 +10,7 @@ const metadataProvider = classes.MetadataProvider;
|
||||
* @param {Object} servicesManager
|
||||
* @param {Object} configuration
|
||||
*/
|
||||
export default function init({ servicesManager, configuration }) {
|
||||
export default function init({ servicesManager, configuration = {} }): void {
|
||||
const { stateSyncService } = servicesManager.services;
|
||||
// Add
|
||||
DicomMetadataStore.subscribe(
|
||||
Reference in new issue
Block a user