Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(DoubleClick): double click a viewport to one up and back #3285

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions extensions/cornerstone/src/init.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import interleaveCenterLoader from './utils/interleaveCenterLoader';
import nthLoader from './utils/nthLoader';
import interleaveTopToBottom from './utils/interleaveTopToBottom';
import initContextMenu from './initContextMenu';
import initDoubleClick from './initDoubleClick';

// TODO: Cypress tests are currently grabbing this from the window?
window.cornerstone = cornerstone;
Expand Down Expand Up @@ -104,6 +105,12 @@ export default async function init({
clearOnModeExit: true,
});

// Stores the entire ViewportGridService getState when toggling to one up
// (e.g. via a double click) so that it can be restored when toggling back.
stateSyncService.register('toggleOneUpViewportGridStore', {
clearOnModeExit: true,
});

const labelmapRepresentation =
cornerstoneTools.Enums.SegmentationRepresentations.Labelmap;

Expand Down Expand Up @@ -198,6 +205,11 @@ export default async function init({
commandsManager,
});

initDoubleClick({
customizationService,
commandsManager,
});

const newStackCallback = evt => {
const { element } = evt.detail;
utilities.stackPrefetch.enable(element);
Expand Down
25 changes: 2 additions & 23 deletions extensions/cornerstone/src/initContextMenu.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { eventTarget, EVENTS } from '@cornerstonejs/core';
import { Enums } from '@cornerstonejs/tools';
import { setEnabledElement } from './state';
import { findNearbyToolData } from './utils/findNearbyToolData';

const cs3DToolsEvents = Enums.Events;

Expand Down Expand Up @@ -47,28 +48,6 @@ function initContextMenu({
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
Expand All @@ -80,7 +59,7 @@ function initContextMenu({
const toRun = customizations[name];
console.log('initContextMenu::cornerstoneViewportHandleEvent', name, toRun);
const options = {
nearbyToolData: findNearbyToolData(evt),
nearbyToolData: findNearbyToolData(commandsManager, evt),
event: evt,
};
commandsManager.run(toRun, options);
Expand Down
92 changes: 92 additions & 0 deletions extensions/cornerstone/src/initDoubleClick.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { eventTarget, EVENTS } from '@cornerstonejs/core';
import { Enums } from '@cornerstonejs/tools';
import { CommandsManager, CustomizationService, Types } from '@ohif/core';
import { findNearbyToolData } from './utils/findNearbyToolData';

const cs3DToolsEvents = Enums.Events;

const DEFAULT_DOUBLE_CLICK = {
doubleClick: {
commandName: 'toggleOneUp',
commandOptions: {},
},
};

/**
* Generates a double click event name, consisting of:
* * alt when the alt key is down
* * ctrl when the cctrl key is down
* * shift when the shift key is down
* * 'doubleClick'
*/
function getDoubleClickEventName(evt: CustomEvent) {
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('doubleClick');
return nameArr.join('');
}

export type initDoubleClickArgs = {
customizationService: CustomizationService;
commandsManager: CommandsManager;
};

function initDoubleClick({
customizationService,
commandsManager,
}: initDoubleClickArgs): void {
const cornerstoneViewportHandleDoubleClick = (evt: CustomEvent) => {
// Do not allow double click on a tool.
const nearbyToolData = findNearbyToolData(commandsManager, evt);
if (nearbyToolData) {
return;
}

const eventName = getDoubleClickEventName(evt);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you muchly.


// Allows for the customization of the double click on a viewport.
const customizations =
customizationService.get('cornerstoneViewportClickCommands') ||
DEFAULT_DOUBLE_CLICK;

const toRun = customizations[eventName];

if (!toRun) {
return;
}

commandsManager.run(toRun);
};

function elementEnabledHandler(evt: CustomEvent) {
const { element } = evt.detail;

element.addEventListener(
cs3DToolsEvents.MOUSE_DOUBLE_CLICK,
cornerstoneViewportHandleDoubleClick
);
}

function elementDisabledHandler(evt: CustomEvent) {
const { element } = evt.detail;

element.removeEventListener(
cs3DToolsEvents.MOUSE_DOUBLE_CLICK,
cornerstoneViewportHandleDoubleClick
);
}

eventTarget.addEventListener(
EVENTS.ELEMENT_ENABLED,
elementEnabledHandler.bind(null)
);

eventTarget.addEventListener(
EVENTS.ELEMENT_DISABLED,
elementDisabledHandler.bind(null)
);
}

export default initDoubleClick;
21 changes: 21 additions & 0 deletions extensions/cornerstone/src/utils/findNearbyToolData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* 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.
*/
export const findNearbyToolData = (commandsManager, evt) => {
if (!evt?.detail) {
return;
}
const { element, currentPoints } = evt.detail;
return commandsManager.runCommand(
'getNearbyToolData',
{
element,
canvasCoordinates: currentPoints?.canvas,
},
'CORNERSTONE'
);
};
132 changes: 126 additions & 6 deletions extensions/default/src/commandsModule.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ServicesManager, Types } from '@ohif/core';
import { ServicesManager, utils } from '@ohif/core';

import {
ContextMenuController,
Expand All @@ -12,6 +12,8 @@ import findViewportsByPosition, {

import { ContextMenuProps } from './CustomizeableContextMenu/types';

const { subscribeToNextViewportGridChange } = utils;

export type HangingProtocolParams = {
protocolId?: string;
stageIndex?: number;
Expand Down Expand Up @@ -159,12 +161,14 @@ const commandsModule = ({
* @param options.protocolId - the protocol ID to change to
* @param options.stageId - the stageId to apply
* @param options.stageIndex - the index of the stage to go to.
* @param options.reset - flag to indicate if the HP should be reset to its original and not restored to a previous state
*/
setHangingProtocol: ({
activeStudyUID = '',
protocolId,
stageId,
stageIndex,
reset = false,
}: HangingProtocolParams): boolean => {
try {
// Stores in the state the reuseID to displaySetUID mapping
Expand Down Expand Up @@ -210,10 +214,11 @@ const commandsModule = ({
hangingProtocolService.setActiveStudyUID(activeStudyUID);
}

const storedHanging = `${hangingProtocolService.getState().activeStudyUID
const storedHanging = `${
hangingProtocolService.getState().activeStudyUID
}:${protocolId}:${useStageIdx || 0}`;

const restoreProtocol = !!viewportGridStore[storedHanging];
const restoreProtocol = !reset && viewportGridStore[storedHanging];

if (
protocolId === hpInfo.protocolId &&
Expand Down Expand Up @@ -273,8 +278,9 @@ const commandsModule = ({
activeStudy,
} = hangingProtocolService.getActiveProtocol();
const { toggleHangingProtocol } = stateSyncService.getState();
const storedHanging = `${activeStudy.StudyInstanceUID
}:${protocolId}:${stageIndex | 0}`;
const storedHanging = `${
activeStudy.StudyInstanceUID
}:${protocolId}:${stageIndex | 0}`;
if (
protocol.id === protocolId &&
(stageIndex === undefined || stageIndex === desiredStageIndex)
Expand All @@ -294,7 +300,11 @@ const commandsModule = ({
},
},
});
return actions.setHangingProtocol({ protocolId, stageIndex });
return actions.setHangingProtocol({
protocolId,
stageIndex,
reset: true,
});
}
},

Expand Down Expand Up @@ -365,6 +375,111 @@ const commandsModule = ({
window.setTimeout(completeLayout, 0);
},

toggleOneUp() {
const viewportGridState = viewportGridService.getState();
const { activeViewportIndex, viewports, layout } = viewportGridState;
const {
displaySetInstanceUIDs,
displaySetOptions,
viewportOptions,
} = viewports[activeViewportIndex];

if (layout.numCols === 1 && layout.numRows === 1) {
// The viewer is in one-up. Check if there is a state to restore/toggle back to.
const { toggleOneUpViewportGridStore } = stateSyncService.getState();

if (!toggleOneUpViewportGridStore.layout) {
return;
}
// There is a state to toggle back to. The viewport that was
// originally toggled to one up was the former active viewport.
const viewportIndexToUpdate =
toggleOneUpViewportGridStore.activeViewportIndex;

// Determine which viewports need to be updated. This is particularly
// important when MPR is toggled to one up and a different reconstructable
// is swapped in. Note that currently HangingProtocolService.getViewportsRequireUpdate
// does not support viewport with multiple display sets.
const updatedViewports =
displaySetInstanceUIDs.length > 1
? []
: displaySetInstanceUIDs
.map(displaySetInstanceUID =>
hangingProtocolService.getViewportsRequireUpdate(
viewportIndexToUpdate,
displaySetInstanceUID
)
)
.flat();

// This findOrCreateViewport returns either one of the updatedViewports
// returned from the HP service OR if there is not one from the HP service then
// simply returns what was in the previous state.
const findOrCreateViewport = (viewportIndex: number) => {
const viewport = updatedViewports.find(
viewport => viewport.viewportIndex === viewportIndex
);

return viewport
? { viewportOptions, displaySetOptions, ...viewport }
: toggleOneUpViewportGridStore.viewports[viewportIndex];
};

const layoutOptions = viewportGridService.getLayoutOptionsFromState(
toggleOneUpViewportGridStore
);

// Restore the previous layout including the active viewport.
viewportGridService.setLayout({
numRows: toggleOneUpViewportGridStore.layout.numRows,
numCols: toggleOneUpViewportGridStore.layout.numCols,
activeViewportIndex: viewportIndexToUpdate,
layoutOptions,
findOrCreateViewport,
});
} else {
// We are not in one-up, so toggle to one up.

// Store the current viewport grid state so we can toggle it back later.
stateSyncService.store({
toggleOneUpViewportGridStore: viewportGridState,
});

// This findOrCreateViewport only return one viewport - the active
// one being toggled to one up.
const findOrCreateViewport = () => {
return {
displaySetInstanceUIDs,
displaySetOptions,
viewportOptions,
};
};

// Set the layout to be 1x1/one-up.
viewportGridService.setLayout({
numRows: 1,
numCols: 1,
findOrCreateViewport,
});

// Subscribe to ANY (i.e. manual and hanging protocol) layout changes so that
// any grid layout state to toggle to from one up is cleared. This is performed on
// a timeout to avoid clearing the state for the actual to one up change.
// Whenever the next layout change event is fired, the subscriptions are unsubscribed.
const clearToggleOneUpViewportGridStore = () => {
const toggleOneUpViewportGridStore = {};
stateSyncService.store({
toggleOneUpViewportGridStore,
});
};

subscribeToNextViewportGridChange(
viewportGridService,
clearToggleOneUpViewportGridStore
);
}
},

openDICOMTagViewer() {
const { activeViewportIndex, viewports } = viewportGridService.getState();
const activeViewportSpecificData = viewports[activeViewportIndex];
Expand Down Expand Up @@ -440,6 +555,11 @@ const commandsModule = ({
storeContexts: [],
options: {},
},
toggleOneUp: {
commandFn: actions.toggleOneUp,
storeContexts: [],
options: {},
},
openDICOMTagViewer: {
commandFn: actions.openDICOMTagViewer,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ function TrackedCornerstoneViewport(props) {
return;
}

annotation.config.style.setViewportToolStyles(`viewport-${viewportIndex}`, {
annotation.config.style.setViewportToolStyles(viewportId, {
global: {
lineDash: '4,4',
},
Expand Down
Loading