Skip to content

Commit

Permalink
feat(DoubleClick): double click a viewport to one up and back (OHIF#3285
Browse files Browse the repository at this point in the history
)

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

Added a toggleOneUp command that puts the active viewport into a 1x1 grid layout
and it toggles out of 'one-up' by restoring its saved 'toggleOneUpViewportGridStore'
from the StateSyncService.
Added double click customization for the Cornerstone extension with the
default double click handling being the toggleOneUp command.
Added a cypress test for the double click functionality.

* PR feedback:
- tracked viewport measurements no longer show as dashed when toggling one up
- disallowed double clicking near a measurement
- updated cornerstone3D dependencies to fix double click of TMTV and volume viewport 3D
- created ViewportGridService.getLayoutOptionsFromState

* Updated the ViewportGridService docs.

* Switched to using 'cornerstoneViewportClickCommands' and consistency with the context menu clicks.
  • Loading branch information
jbocce authored and 徐忠元 committed Mar 30, 2023
1 parent 0d24179 commit bf7ee4d
Show file tree
Hide file tree
Showing 14 changed files with 397 additions and 37 deletions.
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);

// 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'
);
};
124 changes: 121 additions & 3 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 @@ -214,7 +218,7 @@ const commandsModule = ({
hangingProtocolService.getState().activeStudyUID
}:${protocolId}:${useStageIdx || 0}`;

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

if (
protocolId === hpInfo.protocolId &&
Expand Down Expand Up @@ -296,7 +300,11 @@ const commandsModule = ({
},
},
});
return actions.setHangingProtocol({ protocolId, stageIndex });
return actions.setHangingProtocol({
protocolId,
stageIndex,
reset: true,
});
}
},

Expand Down Expand Up @@ -367,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 @@ -442,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

0 comments on commit bf7ee4d

Please sign in to comment.