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
@@ -117,8 +117,11 @@ function CustomizableViewportOverlay({
|
||||
viewportIndex,
|
||||
servicesManager,
|
||||
}) {
|
||||
const { toolbarService, cornerstoneViewportService, customizationService } =
|
||||
servicesManager.services;
|
||||
const {
|
||||
toolbarService,
|
||||
cornerstoneViewportService,
|
||||
customizationService,
|
||||
} = servicesManager.services;
|
||||
const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null });
|
||||
const [scale, setScale] = useState(1);
|
||||
const [activeTools, setActiveTools] = useState([]);
|
||||
@@ -202,10 +205,9 @@ function CustomizableViewportOverlay({
|
||||
previousCamera.parallelScale !== camera.parallelScale ||
|
||||
previousCamera.scale !== camera.scale
|
||||
) {
|
||||
const viewport =
|
||||
cornerstoneViewportService.getCornerstoneViewportByIndex(
|
||||
viewportIndex
|
||||
);
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewportByIndex(
|
||||
viewportIndex
|
||||
);
|
||||
|
||||
if (!viewport) {
|
||||
return;
|
||||
@@ -283,7 +285,7 @@ function CustomizableViewportOverlay({
|
||||
} else if (item.customizationType === 'ohif.overlayItem.instanceNumber') {
|
||||
return <InstanceNumberOverlayItem {...overlayItemProps} />;
|
||||
} else {
|
||||
const renderItem = customizationService.applyType(item);
|
||||
const renderItem = customizationService.transform(item);
|
||||
|
||||
if (typeof renderItem.content === 'function') {
|
||||
return renderItem.content(overlayItemProps);
|
||||
@@ -450,8 +452,9 @@ function _getInstanceNumberFromVolume(
|
||||
const volume = volumes[0];
|
||||
const { direction, imageIds } = volume;
|
||||
|
||||
const cornerstoneViewport =
|
||||
cornerstoneViewportService.getCornerstoneViewportByIndex(viewportIndex);
|
||||
const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewportByIndex(
|
||||
viewportIndex
|
||||
);
|
||||
|
||||
if (!cornerstoneViewport) {
|
||||
return;
|
||||
|
||||
@@ -16,13 +16,10 @@ import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownload
|
||||
import callInputDialog from './utils/callInputDialog';
|
||||
import { setColormap } from './utils/colormap/transferFunctionHelpers';
|
||||
import toggleStackImageSync from './utils/stackSync/toggleStackImageSync';
|
||||
import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/utils/selection';
|
||||
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
|
||||
|
||||
const commandsModule = ({
|
||||
servicesManager,
|
||||
}: {
|
||||
servicesManager: ServicesManager;
|
||||
}): React.FunctionComponent => {
|
||||
function commandsModule({ servicesManager, commandsManager }) {
|
||||
const {
|
||||
viewportGridService,
|
||||
toolGroupService,
|
||||
@@ -31,7 +28,12 @@ const commandsModule = ({
|
||||
uiDialogService,
|
||||
cornerstoneViewportService,
|
||||
uiNotificationService,
|
||||
} = servicesManager.services;
|
||||
customizationService,
|
||||
measurementService,
|
||||
hangingProtocolService,
|
||||
} = (servicesManager as ServicesManager).services;
|
||||
|
||||
const { measurementServiceSource } = this;
|
||||
|
||||
function _getActiveViewportEnabledElement() {
|
||||
return getActiveViewportEnabledElement(viewportGridService);
|
||||
@@ -72,9 +74,175 @@ const commandsModule = ({
|
||||
}
|
||||
|
||||
const actions = {
|
||||
getActiveViewportEnabledElement: () => {
|
||||
return _getActiveViewportEnabledElement();
|
||||
/**
|
||||
* Generates the selector props for the context menu, specific to
|
||||
* the cornerstone viewport, and then runs the context menu.
|
||||
*/
|
||||
showCornerstoneContextMenu: options => {
|
||||
const element = _getActiveViewportEnabledElement()?.viewport?.element;
|
||||
|
||||
const optionsToUse = { ...options, element };
|
||||
const { useSelectedAnnotation, nearbyToolData, event } = optionsToUse;
|
||||
|
||||
// This code is used to invoke the context menu via keyboard shortcuts
|
||||
if (useSelectedAnnotation && !nearbyToolData) {
|
||||
const firstAnnotationSelected = getFirstAnnotationSelected(element);
|
||||
// filter by allowed selected tools from config property (if there is any)
|
||||
const isToolAllowed =
|
||||
!optionsToUse.allowedSelectedTools ||
|
||||
optionsToUse.allowedSelectedTools.includes(
|
||||
firstAnnotationSelected?.metadata?.toolName
|
||||
);
|
||||
if (isToolAllowed) {
|
||||
optionsToUse.nearbyToolData = firstAnnotationSelected;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
optionsToUse.defaultPointsPosition = [];
|
||||
// if (optionsToUse.nearbyToolData) {
|
||||
// optionsToUse.defaultPointsPosition = commandsManager.runCommand(
|
||||
// 'getToolDataActiveCanvasPoints',
|
||||
// { toolData: optionsToUse.nearbyToolData }
|
||||
// );
|
||||
// }
|
||||
|
||||
// TODO - make the selectorProps richer by including the study metadata and display set.
|
||||
optionsToUse.selectorProps = {
|
||||
toolName: optionsToUse.nearbyToolData?.metadata?.toolName,
|
||||
value: optionsToUse.nearbyToolData,
|
||||
uid: optionsToUse.nearbyToolData?.annotationUID,
|
||||
nearbyToolData: optionsToUse.nearbyToolData,
|
||||
event,
|
||||
...optionsToUse.selectorProps,
|
||||
};
|
||||
|
||||
commandsManager.run(options, optionsToUse);
|
||||
},
|
||||
|
||||
getNearbyToolData({ nearbyToolData, element, canvasCoordinates }) {
|
||||
return (
|
||||
nearbyToolData ??
|
||||
cstUtils.getAnnotationNearPoint(element, canvasCoordinates)
|
||||
);
|
||||
},
|
||||
|
||||
// Measurement tool commands:
|
||||
|
||||
/** Delete the given measurement */
|
||||
deleteMeasurement: ({ uid }) => {
|
||||
if (uid) {
|
||||
measurementServiceSource.remove(uid);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Show the measurement labelling input dialog and update the label
|
||||
* on the measurement with a response if not cancelled.
|
||||
*/
|
||||
setMeasurementLabel: ({ uid }) => {
|
||||
const measurement = measurementService.getMeasurement(uid);
|
||||
|
||||
callInputDialog(
|
||||
uiDialogService,
|
||||
measurement,
|
||||
(label, actionId) => {
|
||||
if (actionId === 'cancel') {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedMeasurement = Object.assign({}, measurement, {
|
||||
label,
|
||||
});
|
||||
|
||||
measurementService.update(
|
||||
updatedMeasurement.uid,
|
||||
updatedMeasurement,
|
||||
true
|
||||
);
|
||||
},
|
||||
false
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param props - containing the updates to apply
|
||||
* @param props.measurementKey - chooses the measurement key to apply the
|
||||
* code to. This will typically be finding or site to apply a
|
||||
* finind code or a findingSites code.
|
||||
* @param props.code - A coding scheme value from DICOM, including:
|
||||
* * CodeValue - the language independent code, for example '1234'
|
||||
* * CodingSchemeDesignator - the issue of the code value
|
||||
* * CodeMeaning - the text value shown to the user
|
||||
* * ref - a string reference in the form `<designator>:<codeValue>`
|
||||
* * Other fields
|
||||
* Note it is a valid option to remove the finding or site values by
|
||||
* supplying null for the code.
|
||||
* @param props.uid - the measurement UID to find it with
|
||||
* @param props.label - the text value for the code. Has NOTHING to do with
|
||||
* the measurement label, which can be set with textLabel
|
||||
* @param props.textLabel is the measurement label to apply. Set to null to
|
||||
* delete.
|
||||
*
|
||||
* If the measurementKey is `site`, then the code will also be added/replace
|
||||
* the 0 element of findingSites. This behaviour is expected to be enhanced
|
||||
* in the future with ability to set other site information.
|
||||
*/
|
||||
updateMeasurement: props => {
|
||||
const { code, uid, textLabel, label } = props;
|
||||
const measurement = measurementService.getMeasurement(uid);
|
||||
const updatedMeasurement = {
|
||||
...measurement,
|
||||
};
|
||||
// Call it textLabel as the label value
|
||||
// TODO - remove the label setting when direct rendering of findingSites is enabled
|
||||
if (textLabel !== undefined) {
|
||||
updatedMeasurement.label = textLabel;
|
||||
}
|
||||
if (code !== undefined) {
|
||||
const measurementKey = code.type || 'finding';
|
||||
|
||||
if (code.ref && !code.CodeValue) {
|
||||
const split = code.ref.indexOf(':');
|
||||
code.CodeValue = code.ref.substring(split + 1);
|
||||
code.CodeMeaning = code.text || label;
|
||||
code.CodingSchemeDesignator = code.ref.substring(0, split);
|
||||
}
|
||||
updatedMeasurement[measurementKey] = code;
|
||||
// TODO - remove this line once the measurements table customizations are in
|
||||
if (measurementKey !== 'finding') {
|
||||
if (updatedMeasurement.findingSites) {
|
||||
updatedMeasurement.findingSites = updatedMeasurement.findingSites.filter(
|
||||
it => it.type !== measurementKey
|
||||
);
|
||||
updatedMeasurement.findingSites.push(code);
|
||||
} else {
|
||||
updatedMeasurement.findingSites = [code];
|
||||
}
|
||||
}
|
||||
// TODO - remove this once measurement items customization is ready
|
||||
const allCodes = [];
|
||||
if (textLabel) allCodes.push(textLabel);
|
||||
if (updatedMeasurement.finding) {
|
||||
allCodes.push(updatedMeasurement.finding.CodeMeaning);
|
||||
}
|
||||
(updatedMeasurement.findingSites || []).forEach(it =>
|
||||
allCodes.push(it.CodeMeaning)
|
||||
);
|
||||
updatedMeasurement.label = allCodes.join(', ');
|
||||
}
|
||||
measurementService.update(
|
||||
updatedMeasurement.uid,
|
||||
updatedMeasurement,
|
||||
true
|
||||
);
|
||||
},
|
||||
|
||||
// Retrieve value commands
|
||||
getActiveViewportEnabledElement: _getActiveViewportEnabledElement,
|
||||
|
||||
setViewportActive: ({ viewportId }) => {
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(
|
||||
viewportId
|
||||
@@ -457,6 +625,43 @@ const commandsModule = ({
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
// The command here is to show the viewer context menu, as being the
|
||||
// context menu
|
||||
showCornerstoneContextMenu: {
|
||||
commandFn: actions.showCornerstoneContextMenu,
|
||||
storeContexts: [],
|
||||
options: {
|
||||
menuCustomizationId: 'measurementsContextMenu',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'showContextMenu',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
getNearbyToolData: {
|
||||
commandFn: actions.getNearbyToolData,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
|
||||
deleteMeasurement: {
|
||||
commandFn: actions.deleteMeasurement,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
setMeasurementLabel: {
|
||||
commandFn: actions.setMeasurementLabel,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
updateMeasurement: {
|
||||
commandFn: actions.updateMeasurement,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
|
||||
setWindowLevel: {
|
||||
commandFn: actions.setWindowLevel,
|
||||
storeContexts: [],
|
||||
@@ -587,6 +792,6 @@ const commandsModule = ({
|
||||
definitions,
|
||||
defaultContext: 'CORNERSTONE',
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default commandsModule;
|
||||
@@ -10,7 +10,7 @@ import { Enums as cs3DToolsEnums } from '@cornerstonejs/tools';
|
||||
import { Types } from '@ohif/core';
|
||||
|
||||
import init from './init';
|
||||
import commandsModule from './commandsModule';
|
||||
import getCommandsModule from './commandsModule';
|
||||
import getHangingProtocolModule from './getHangingProtocolModule';
|
||||
import ToolGroupService from './services/ToolGroupService';
|
||||
import SyncGroupService from './services/SyncGroupService';
|
||||
@@ -51,7 +51,7 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
*/
|
||||
id,
|
||||
|
||||
onModeExit: () => {
|
||||
onModeExit: (): void => {
|
||||
// Empty out the image load and retrieval pools to prevent memory leaks
|
||||
// on the mode exits
|
||||
Object.values(cs3DEnums.RequestType).forEach(type => {
|
||||
@@ -68,12 +68,10 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
*
|
||||
* @param configuration.csToolsConfig - Passed directly to `initCornerstoneTools`
|
||||
*/
|
||||
async preRegistration({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
configuration = {},
|
||||
appConfig,
|
||||
}) {
|
||||
preRegistration: function (
|
||||
props: Types.Extensions.ExtensionParams
|
||||
): Promise<void> {
|
||||
const { servicesManager } = props;
|
||||
// Todo: we should be consistent with how services get registered. Use REGISTRATION static method for all
|
||||
servicesManager.registerService(
|
||||
CornerstoneViewportService(servicesManager)
|
||||
@@ -87,8 +85,9 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
CornerstoneCacheService.REGISTRATION(servicesManager)
|
||||
);
|
||||
|
||||
await init({ servicesManager, commandsManager, configuration, appConfig });
|
||||
return init.call(this, props);
|
||||
},
|
||||
|
||||
getHangingProtocolModule,
|
||||
getViewportModule({ servicesManager, commandsManager }) {
|
||||
const ExtendedOHIFCornerstoneViewport = props => {
|
||||
@@ -114,13 +113,7 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
},
|
||||
];
|
||||
},
|
||||
getCommandsModule({ servicesManager, commandsManager, extensionManager }) {
|
||||
return commandsModule({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
extensionManager,
|
||||
});
|
||||
},
|
||||
getCommandsModule,
|
||||
getUtilityModule({ servicesManager }) {
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import OHIF from '@ohif/core';
|
||||
import React from 'react';
|
||||
import { ContextMenuMeasurements } from '@ohif/ui';
|
||||
|
||||
import * as cornerstone from '@cornerstonejs/core';
|
||||
import * as cornerstoneTools from '@cornerstonejs/tools';
|
||||
@@ -21,15 +20,11 @@ import initWADOImageLoader from './initWADOImageLoader';
|
||||
import initCornerstoneTools from './initCornerstoneTools';
|
||||
|
||||
import { connectToolsToMeasurementService } from './initMeasurementService';
|
||||
import callInputDialog from './utils/callInputDialog';
|
||||
import initCineService from './initCineService';
|
||||
import interleaveCenterLoader from './utils/interleaveCenterLoader';
|
||||
import nthLoader from './utils/nthLoader';
|
||||
import interleaveTopToBottom from './utils/interleaveTopToBottom';
|
||||
|
||||
const cs3DToolsEvents = Enums.Events;
|
||||
|
||||
let CONTEXT_MENU_OPEN = false;
|
||||
import initContextMenu from './initContextMenu';
|
||||
|
||||
// TODO: Cypress tests are currently grabbing this from the window?
|
||||
window.cornerstone = cornerstone;
|
||||
@@ -42,7 +37,7 @@ export default async function init({
|
||||
commandsManager,
|
||||
configuration,
|
||||
appConfig,
|
||||
}) {
|
||||
}: Types.Extensions.ExtensionParams): Promise<void> {
|
||||
await cs3DInit();
|
||||
|
||||
// For debugging e2e tests that are failing on CI
|
||||
@@ -65,6 +60,7 @@ export default async function init({
|
||||
const {
|
||||
userAuthenticationService,
|
||||
measurementService,
|
||||
customizationService,
|
||||
displaySetService,
|
||||
uiDialogService,
|
||||
uiModalService,
|
||||
@@ -155,118 +151,12 @@ export default async function init({
|
||||
initWADOImageLoader(userAuthenticationService, appConfig);
|
||||
|
||||
/* Measurement Service */
|
||||
const measurementServiceSource = connectToolsToMeasurementService(
|
||||
this.measurementServiceSource = connectToolsToMeasurementService(
|
||||
servicesManager
|
||||
);
|
||||
|
||||
initCineService(cineService);
|
||||
|
||||
const _getDefaultPosition = event => ({
|
||||
x: (event && event.currentPoints.client[0]) || 0,
|
||||
y: (event && event.currentPoints.client[1]) || 0,
|
||||
});
|
||||
|
||||
const onRightClick = event => {
|
||||
if (!uiDialogService) {
|
||||
console.warn('Unable to show dialog; no UI Dialog Service available.');
|
||||
return;
|
||||
}
|
||||
|
||||
const onGetMenuItems = defaultMenuItems => {
|
||||
const { element, currentPoints } = event.detail;
|
||||
|
||||
const nearbyToolData = utilities.getAnnotationNearPoint(
|
||||
element,
|
||||
currentPoints.canvas
|
||||
);
|
||||
|
||||
const menuItems = [];
|
||||
if (nearbyToolData && nearbyToolData.metadata.toolName !== 'Crosshairs') {
|
||||
defaultMenuItems.forEach(item => {
|
||||
item.value = nearbyToolData;
|
||||
item.element = element;
|
||||
menuItems.push(item);
|
||||
});
|
||||
}
|
||||
|
||||
return menuItems;
|
||||
};
|
||||
|
||||
CONTEXT_MENU_OPEN = true;
|
||||
|
||||
uiDialogService.dismiss({ id: 'context-menu' });
|
||||
uiDialogService.create({
|
||||
id: 'context-menu',
|
||||
isDraggable: false,
|
||||
preservePosition: false,
|
||||
defaultPosition: _getDefaultPosition(event.detail),
|
||||
content: ContextMenuMeasurements,
|
||||
onClickOutside: () => {
|
||||
uiDialogService.dismiss({ id: 'context-menu' });
|
||||
CONTEXT_MENU_OPEN = false;
|
||||
},
|
||||
contentProps: {
|
||||
onGetMenuItems,
|
||||
eventData: event.detail,
|
||||
onDelete: item => {
|
||||
const { annotationUID } = item.value;
|
||||
|
||||
const uid = annotationUID;
|
||||
// Sync'd w/ Measurement Service
|
||||
if (uid) {
|
||||
measurementServiceSource.remove(uid, {
|
||||
element: item.element,
|
||||
});
|
||||
}
|
||||
CONTEXT_MENU_OPEN = false;
|
||||
},
|
||||
onClose: () => {
|
||||
CONTEXT_MENU_OPEN = false;
|
||||
uiDialogService.dismiss({ id: 'context-menu' });
|
||||
},
|
||||
onSetLabel: item => {
|
||||
const { annotationUID } = item.value;
|
||||
|
||||
const measurement = measurementService.getMeasurement(annotationUID);
|
||||
|
||||
callInputDialog(
|
||||
uiDialogService,
|
||||
measurement,
|
||||
(label, actionId) => {
|
||||
if (actionId === 'cancel') {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedMeasurement = Object.assign({}, measurement, {
|
||||
label,
|
||||
});
|
||||
|
||||
measurementService.update(
|
||||
updatedMeasurement.uid,
|
||||
updatedMeasurement,
|
||||
true
|
||||
);
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
CONTEXT_MENU_OPEN = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const resetContextMenu = () => {
|
||||
if (!uiDialogService) {
|
||||
console.warn('Unable to show dialog; no UI Dialog Service available.');
|
||||
return;
|
||||
}
|
||||
|
||||
CONTEXT_MENU_OPEN = false;
|
||||
|
||||
uiDialogService.dismiss({ id: 'context-menu' });
|
||||
};
|
||||
|
||||
// When a custom image load is performed, update the relevant viewports
|
||||
hangingProtocolService.subscribe(
|
||||
hangingProtocolService.EVENTS.CUSTOM_IMAGE_LOAD_PERFORMED,
|
||||
@@ -285,24 +175,11 @@ export default async function init({
|
||||
}
|
||||
);
|
||||
|
||||
/*
|
||||
* Because click gives us the native "mouse up", buttons will always be `0`
|
||||
* Need to fallback to event.which;
|
||||
*
|
||||
*/
|
||||
const contextMenuHandleClick = evt => {
|
||||
const mouseUpEvent = evt.detail.event;
|
||||
const isRightClick = mouseUpEvent.which === 3;
|
||||
|
||||
const clickMethodHandler = isRightClick ? onRightClick : resetContextMenu;
|
||||
clickMethodHandler(evt);
|
||||
};
|
||||
|
||||
// const cancelContextMenuIfOpen = evt => {
|
||||
// if (CONTEXT_MENU_OPEN) {
|
||||
// resetContextMenu();
|
||||
// }
|
||||
// };
|
||||
initContextMenu({
|
||||
cornerstoneViewportService,
|
||||
customizationService,
|
||||
commandsManager,
|
||||
});
|
||||
|
||||
const newStackCallback = evt => {
|
||||
const { element } = evt.detail;
|
||||
@@ -337,12 +214,6 @@ export default async function init({
|
||||
|
||||
function elementEnabledHandler(evt) {
|
||||
const { element } = evt.detail;
|
||||
|
||||
element.addEventListener(
|
||||
cs3DToolsEvents.MOUSE_CLICK,
|
||||
contextMenuHandleClick
|
||||
);
|
||||
|
||||
element.addEventListener(EVENTS.CAMERA_RESET, resetCrosshairs);
|
||||
|
||||
eventTarget.addEventListener(
|
||||
@@ -354,11 +225,6 @@ export default async function init({
|
||||
function elementDisabledHandler(evt) {
|
||||
const { element } = evt.detail;
|
||||
|
||||
element.removeEventListener(
|
||||
cs3DToolsEvents.MOUSE_CLICK,
|
||||
contextMenuHandleClick
|
||||
);
|
||||
|
||||
element.removeEventListener(EVENTS.CAMERA_RESET, resetCrosshairs);
|
||||
|
||||
// TODO - consider removing the callback when all elements are gone
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { eventTarget, EVENTS } from '@cornerstonejs/core';
|
||||
import { Enums } from '@cornerstonejs/tools';
|
||||
import { setEnabledElement } from './state';
|
||||
|
||||
const cs3DToolsEvents = Enums.Events;
|
||||
|
||||
const DEFAULT_CONTEXT_MENU_CLICKS = {
|
||||
button1: {
|
||||
commands: [
|
||||
{
|
||||
commandName: 'closeContextMenu',
|
||||
},
|
||||
],
|
||||
},
|
||||
button3: {
|
||||
commands: [
|
||||
{
|
||||
commandName: 'showCornerstoneContextMenu',
|
||||
commandOptions: {
|
||||
menuId: 'measurementsContextMenu',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a name, consisting of:
|
||||
* * alt when the alt key is down
|
||||
* * ctrl when the cctrl key is down
|
||||
* * shift when the shift key is down
|
||||
* * 'button' followed by the button number (1 left, 3 right etc)
|
||||
*/
|
||||
function getEventName(evt) {
|
||||
const button = evt.detail.event.which;
|
||||
const nameArr = [];
|
||||
if (evt.detail.event.altKey) nameArr.push('alt');
|
||||
if (evt.detail.event.ctrlKey) nameArr.push('ctrl');
|
||||
if (evt.detail.event.shiftKey) nameArr.push('shift');
|
||||
nameArr.push('button');
|
||||
nameArr.push(button);
|
||||
return nameArr.join('');
|
||||
}
|
||||
|
||||
function initContextMenu({
|
||||
cornerstoneViewportService,
|
||||
customizationService,
|
||||
commandsManager,
|
||||
}): void {
|
||||
/**
|
||||
* Finds tool nearby event position triggered.
|
||||
*
|
||||
* @param {Object} commandsManager mannager of commands
|
||||
* @param {Object} event that has being triggered
|
||||
* @returns cs toolData or undefined if not found.
|
||||
*/
|
||||
const findNearbyToolData = evt => {
|
||||
if (!evt?.detail) {
|
||||
return;
|
||||
}
|
||||
const { element, currentPoints } = evt.detail;
|
||||
return commandsManager.runCommand(
|
||||
'getNearbyToolData',
|
||||
{
|
||||
element,
|
||||
canvasCoordinates: currentPoints?.canvas,
|
||||
},
|
||||
'CORNERSTONE'
|
||||
);
|
||||
};
|
||||
|
||||
/*
|
||||
* Run the commands associated with the given button press,
|
||||
* defaults on button1 and button2
|
||||
*/
|
||||
const cornerstoneViewportHandleEvent = (name, evt) => {
|
||||
const customizations =
|
||||
customizationService.get('cornerstoneViewportClickCommands') ||
|
||||
DEFAULT_CONTEXT_MENU_CLICKS;
|
||||
const toRun = customizations[name];
|
||||
console.log('initContextMenu::cornerstoneViewportHandleEvent', name, toRun);
|
||||
const options = {
|
||||
nearbyToolData: findNearbyToolData(evt),
|
||||
event: evt,
|
||||
};
|
||||
commandsManager.run(toRun, options);
|
||||
};
|
||||
|
||||
const cornerstoneViewportHandleClick = evt => {
|
||||
const name = getEventName(evt);
|
||||
cornerstoneViewportHandleEvent(name, evt);
|
||||
};
|
||||
|
||||
function elementEnabledHandler(evt) {
|
||||
const { viewportId, element } = evt.detail;
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||
if (!viewportInfo) return;
|
||||
const viewportIndex = viewportInfo.getViewportIndex();
|
||||
// TODO check update upstream
|
||||
setEnabledElement(viewportIndex, element);
|
||||
|
||||
element.addEventListener(
|
||||
cs3DToolsEvents.MOUSE_CLICK,
|
||||
cornerstoneViewportHandleClick
|
||||
);
|
||||
}
|
||||
|
||||
function elementDisabledHandler(evt) {
|
||||
const { element } = evt.detail;
|
||||
|
||||
element.removeEventListener(
|
||||
cs3DToolsEvents.MOUSE_CLICK,
|
||||
cornerstoneViewportHandleClick
|
||||
);
|
||||
}
|
||||
|
||||
eventTarget.addEventListener(
|
||||
EVENTS.ELEMENT_ENABLED,
|
||||
elementEnabledHandler.bind(null)
|
||||
);
|
||||
|
||||
eventTarget.addEventListener(
|
||||
EVENTS.ELEMENT_DISABLED,
|
||||
elementDisabledHandler.bind(null)
|
||||
);
|
||||
}
|
||||
|
||||
export default initContextMenu;
|
||||
Reference in new issue
Block a user