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
@@ -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;
|
||||
Reference in new issue
Block a user