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

* 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.
This commit is contained in:
Joe Boccanfuso 2023-03-29 15:39:51 -04:00 committed by GitHub
parent 4734b3bac6
commit d5ff590dfc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 402 additions and 40 deletions

View File

@ -25,6 +25,7 @@ import interleaveCenterLoader from './utils/interleaveCenterLoader';
import nthLoader from './utils/nthLoader'; import nthLoader from './utils/nthLoader';
import interleaveTopToBottom from './utils/interleaveTopToBottom'; import interleaveTopToBottom from './utils/interleaveTopToBottom';
import initContextMenu from './initContextMenu'; import initContextMenu from './initContextMenu';
import initDoubleClick from './initDoubleClick';
// TODO: Cypress tests are currently grabbing this from the window? // TODO: Cypress tests are currently grabbing this from the window?
window.cornerstone = cornerstone; window.cornerstone = cornerstone;
@ -104,6 +105,12 @@ export default async function init({
clearOnModeExit: true, 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 = const labelmapRepresentation =
cornerstoneTools.Enums.SegmentationRepresentations.Labelmap; cornerstoneTools.Enums.SegmentationRepresentations.Labelmap;
@ -198,6 +205,11 @@ export default async function init({
commandsManager, commandsManager,
}); });
initDoubleClick({
customizationService,
commandsManager,
});
const newStackCallback = evt => { const newStackCallback = evt => {
const { element } = evt.detail; const { element } = evt.detail;
utilities.stackPrefetch.enable(element); utilities.stackPrefetch.enable(element);

View File

@ -1,6 +1,7 @@
import { eventTarget, EVENTS } from '@cornerstonejs/core'; import { eventTarget, EVENTS } from '@cornerstonejs/core';
import { Enums } from '@cornerstonejs/tools'; import { Enums } from '@cornerstonejs/tools';
import { setEnabledElement } from './state'; import { setEnabledElement } from './state';
import { findNearbyToolData } from './utils/findNearbyToolData';
const cs3DToolsEvents = Enums.Events; const cs3DToolsEvents = Enums.Events;
@ -47,28 +48,6 @@ function initContextMenu({
customizationService, customizationService,
commandsManager, commandsManager,
}): void { }): 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, * Run the commands associated with the given button press,
* defaults on button1 and button2 * defaults on button1 and button2
@ -80,7 +59,7 @@ function initContextMenu({
const toRun = customizations[name]; const toRun = customizations[name];
console.log('initContextMenu::cornerstoneViewportHandleEvent', name, toRun); console.log('initContextMenu::cornerstoneViewportHandleEvent', name, toRun);
const options = { const options = {
nearbyToolData: findNearbyToolData(evt), nearbyToolData: findNearbyToolData(commandsManager, evt),
event: evt, event: evt,
}; };
commandsManager.run(toRun, options); commandsManager.run(toRun, options);

View File

@ -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;

View File

@ -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'
);
};

View File

@ -1,4 +1,4 @@
import { ServicesManager, Types } from '@ohif/core'; import { ServicesManager, utils } from '@ohif/core';
import { import {
ContextMenuController, ContextMenuController,
@ -12,6 +12,8 @@ import findViewportsByPosition, {
import { ContextMenuProps } from './CustomizeableContextMenu/types'; import { ContextMenuProps } from './CustomizeableContextMenu/types';
const { subscribeToNextViewportGridChange } = utils;
export type HangingProtocolParams = { export type HangingProtocolParams = {
protocolId?: string; protocolId?: string;
stageIndex?: number; stageIndex?: number;
@ -159,12 +161,14 @@ const commandsModule = ({
* @param options.protocolId - the protocol ID to change to * @param options.protocolId - the protocol ID to change to
* @param options.stageId - the stageId to apply * @param options.stageId - the stageId to apply
* @param options.stageIndex - the index of the stage to go to. * @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: ({ setHangingProtocol: ({
activeStudyUID = '', activeStudyUID = '',
protocolId, protocolId,
stageId, stageId,
stageIndex, stageIndex,
reset = false,
}: HangingProtocolParams): boolean => { }: HangingProtocolParams): boolean => {
try { try {
// Stores in the state the reuseID to displaySetUID mapping // Stores in the state the reuseID to displaySetUID mapping
@ -210,10 +214,11 @@ const commandsModule = ({
hangingProtocolService.setActiveStudyUID(activeStudyUID); hangingProtocolService.setActiveStudyUID(activeStudyUID);
} }
const storedHanging = `${hangingProtocolService.getState().activeStudyUID const storedHanging = `${
hangingProtocolService.getState().activeStudyUID
}:${protocolId}:${useStageIdx || 0}`; }:${protocolId}:${useStageIdx || 0}`;
const restoreProtocol = !!viewportGridStore[storedHanging]; const restoreProtocol = !reset && viewportGridStore[storedHanging];
if ( if (
protocolId === hpInfo.protocolId && protocolId === hpInfo.protocolId &&
@ -273,8 +278,9 @@ const commandsModule = ({
activeStudy, activeStudy,
} = hangingProtocolService.getActiveProtocol(); } = hangingProtocolService.getActiveProtocol();
const { toggleHangingProtocol } = stateSyncService.getState(); const { toggleHangingProtocol } = stateSyncService.getState();
const storedHanging = `${activeStudy.StudyInstanceUID const storedHanging = `${
}:${protocolId}:${stageIndex | 0}`; activeStudy.StudyInstanceUID
}:${protocolId}:${stageIndex | 0}`;
if ( if (
protocol.id === protocolId && protocol.id === protocolId &&
(stageIndex === undefined || stageIndex === desiredStageIndex) (stageIndex === undefined || stageIndex === desiredStageIndex)
@ -294,7 +300,11 @@ const commandsModule = ({
}, },
}, },
}); });
return actions.setHangingProtocol({ protocolId, stageIndex }); return actions.setHangingProtocol({
protocolId,
stageIndex,
reset: true,
});
} }
}, },
@ -365,6 +375,111 @@ const commandsModule = ({
window.setTimeout(completeLayout, 0); 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() { openDICOMTagViewer() {
const { activeViewportIndex, viewports } = viewportGridService.getState(); const { activeViewportIndex, viewports } = viewportGridService.getState();
const activeViewportSpecificData = viewports[activeViewportIndex]; const activeViewportSpecificData = viewports[activeViewportIndex];
@ -440,6 +555,11 @@ const commandsModule = ({
storeContexts: [], storeContexts: [],
options: {}, options: {},
}, },
toggleOneUp: {
commandFn: actions.toggleOneUp,
storeContexts: [],
options: {},
},
openDICOMTagViewer: { openDICOMTagViewer: {
commandFn: actions.openDICOMTagViewer, commandFn: actions.openDICOMTagViewer,
}, },

View File

@ -80,7 +80,7 @@ function TrackedCornerstoneViewport(props) {
return; return;
} }
annotation.config.style.setViewportToolStyles(`viewport-${viewportIndex}`, { annotation.config.style.setViewportToolStyles(viewportId, {
global: { global: {
lineDash: '4,4', lineDash: '4,4',
}, },

View File

@ -2,6 +2,8 @@ import { PubSubService } from '../_shared/pubSubServiceInterface';
const EVENTS = { const EVENTS = {
ACTIVE_VIEWPORT_INDEX_CHANGED: 'event::activeviewportindexchanged', ACTIVE_VIEWPORT_INDEX_CHANGED: 'event::activeviewportindexchanged',
LAYOUT_CHANGED: 'event::layoutChanged',
GRID_STATE_CHANGED: 'event::gridStateChanged',
}; };
class ViewportGridService extends PubSubService { class ViewportGridService extends PubSubService {
@ -101,12 +103,26 @@ class ViewportGridService extends PubSubService {
* options that is initially provided as {} (eg to store intermediate state) * options that is initially provided as {} (eg to store intermediate state)
* The function returns a viewport object to use at the given position. * The function returns a viewport object to use at the given position.
*/ */
public setLayout({ numCols, numRows, findOrCreateViewport = undefined }) { public setLayout({
numCols,
numRows,
layoutOptions,
layoutType = 'grid',
activeViewportIndex = undefined,
findOrCreateViewport = undefined,
}) {
this.serviceImplementation._setLayout({ this.serviceImplementation._setLayout({
numCols, numCols,
numRows, numRows,
layoutOptions,
layoutType,
activeViewportIndex,
findOrCreateViewport, findOrCreateViewport,
}); });
this._broadcastEvent(this.EVENTS.LAYOUT_CHANGED, {
numCols,
numRows,
});
} }
public reset() { public reset() {
@ -125,11 +141,25 @@ class ViewportGridService extends PubSubService {
public set(state) { public set(state) {
this.serviceImplementation._set(state); this.serviceImplementation._set(state);
this._broadcastEvent(this.EVENTS.GRID_STATE_CHANGED, {
state,
});
} }
public getNumViewportPanes() { public getNumViewportPanes() {
return this.serviceImplementation._getNumViewportPanes(); return this.serviceImplementation._getNumViewportPanes();
} }
public getLayoutOptionsFromState(state) {
return state.viewports.map(viewport => {
return {
x: viewport.x,
y: viewport.y,
width: viewport.width,
height: viewport.height,
};
});
}
} }
export default ViewportGridService; export default ViewportGridService;

View File

@ -6,5 +6,5 @@ export interface Command {
/** A set of commands, typically contained in a tool item or other configuration */ /** A set of commands, typically contained in a tool item or other configuration */
export interface Commands { export interface Commands {
commands: Commands[]; commands: Command[];
} }

View File

@ -32,6 +32,7 @@ import {
sortingCriteria, sortingCriteria,
seriesSortCriteria, seriesSortCriteria,
} from './sortStudy'; } from './sortStudy';
import { subscribeToNextViewportGridChange } from './subscribeToNextViewportGridChange';
// Commented out unused functionality. // Commented out unused functionality.
// Need to implement new mechanism for derived displaySets using the displaySetManager. // Need to implement new mechanism for derived displaySets using the displaySetManager.
@ -69,6 +70,7 @@ const utils = {
debounce, debounce,
roundNumber, roundNumber,
downloadCSVReport, downloadCSVReport,
subscribeToNextViewportGridChange,
}; };
export { export {

View File

@ -35,6 +35,7 @@ describe('Top level exports', () => {
'resolveObjectPath', 'resolveObjectPath',
'hierarchicalListUtils', 'hierarchicalListUtils',
'progressTrackingUtils', 'progressTrackingUtils',
'subscribeToNextViewportGridChange',
].sort(); ].sort();
const exports = Object.keys(utils.default).sort(); const exports = Object.keys(utils.default).sort();

View File

@ -0,0 +1,37 @@
import { ViewportGridService } from '../services';
/**
* Subscribes to the very next LAYOUT_CHANGED or GRID_STATE_CHANGED event that
* is not currently on the event queue. The subscriptions are made on a 'zero'
* timeout so as to avoid responding to any of those events currently on the event queue.
* The subscription persists only for a single invocation of either event.
* Once either event is fired, the subscriptions are unsubscribed.
* @param viewportGridService the viewport grid service to subscribe to
* @param gridChangeCallback the callback
*/
function subscribeToNextViewportGridChange(
viewportGridService: ViewportGridService,
gridChangeCallback: (arg: unknown) => void
): void {
const subscriber = () => {
const callback = (callbackProps: unknown) => {
subscriptions.forEach(subscription => subscription.unsubscribe());
gridChangeCallback(callbackProps);
};
const subscriptions = [
viewportGridService.subscribe(
viewportGridService.EVENTS.LAYOUT_CHANGED,
callback
),
viewportGridService.subscribe(
viewportGridService.EVENTS.GRID_STATE_CHANGED,
callback
),
];
};
window.setTimeout(subscriber, 0);
}
export { subscribeToNextViewportGridChange };

View File

@ -9,6 +9,15 @@ sidebar_label: Viewport Grid Service
This is a new UI service, that handles the grid layout of the viewer. This is a new UI service, that handles the grid layout of the viewer.
## Events
There are seven events that get publish in `ViewportGridService `:
| Event | Description |
| ----------------------------- | --------------------------------------------------|
| ACTIVE_VIEWPORT_INDEX_CHANGED | Fires the index of the active viewport is changed |
| LAYOUT_CHANGED | Fires the layout is changed |
| GRID_STATE_CHANGED | Fires when the entire grid state is changed |
## Interface ## Interface
For a more detailed look on the options and return values each of these methods For a more detailed look on the options and return values each of these methods
@ -19,9 +28,10 @@ is expected to support, [check out it's interface in `@ohif/core`][interface]
| `setActiveViewportIndex(index)` | Sets the active viewport index in the app | | `setActiveViewportIndex(index)` | Sets the active viewport index in the app |
| `getState()` | Gets the states of the viewport (see below) | | `getState()` | Gets the states of the viewport (see below) |
| `setDisplaySetsForViewport({ viewportIndex, displaySetInstanceUID })` | Sets displaySet for viewport based on displaySet Id | | `setDisplaySetsForViewport({ viewportIndex, displaySetInstanceUID })` | Sets displaySet for viewport based on displaySet Id |
| `setLayout({numCols, numRows, keepExtraViewports})` | Sets rows and columns. When the total number of viewports decreases, optionally keep the extra/offscreen viewports. | | `setLayout({numCols, numRows, keepExtraViewports})` | Sets rows and columns. When the total number of viewports decreases, optionally keep the extra/offscreen viewports. |
| `reset()` | Resets the default states | | `reset()` | Resets the default states |
| `getNumViewportPanes()` | Gets the number of visible viewport panes | | `getNumViewportPanes()` | Gets the number of visible viewport panes |
| `getLayoutOptionsFromState(gridState)` | Utility method that produces a `ViewportLayoutOptions` based on the passed in state|
## Implementations ## Implementations

View File

@ -146,6 +146,7 @@ export function ViewportGridProvider({ children, service }) {
numRows, numRows,
layoutOptions, layoutOptions,
layoutType = 'grid', layoutType = 'grid',
activeViewportIndex,
findOrCreateViewport, findOrCreateViewport,
} = action.payload; } = action.payload;
@ -160,7 +161,7 @@ export function ViewportGridProvider({ children, service }) {
// haven't been viewed yet, and add them in the appropriate order. // haven't been viewed yet, and add them in the appropriate order.
const options = {}; const options = {};
let activeViewportIndex; let activeViewportIndexToSet = activeViewportIndex;
for (let row = 0; row < numRows; row++) { for (let row = 0; row < numRows; row++) {
for (let col = 0; col < numCols; col++) { for (let col = 0; col < numCols; col++) {
const pos = col + row * numCols; const pos = col + row * numCols;
@ -170,10 +171,11 @@ export function ViewportGridProvider({ children, service }) {
continue; continue;
} }
if ( if (
!activeViewportIndex || activeViewportIndexToSet == null &&
state.viewports[pos]?.positionId === positionId state.viewports[state.activeViewportIndex]?.positionId ===
positionId
) { ) {
activeViewportIndex = pos; activeViewportIndexToSet = pos;
} }
const viewport = findOrCreateViewport(pos, positionId, options); const viewport = findOrCreateViewport(pos, positionId, options);
if (!viewport) continue; if (!viewport) continue;
@ -199,6 +201,8 @@ export function ViewportGridProvider({ children, service }) {
} }
} }
activeViewportIndexToSet = activeViewportIndexToSet ?? 0;
const viewportIdSet = {}; const viewportIdSet = {};
for ( for (
let viewportIndex = 0; let viewportIndex = 0;
@ -223,7 +227,7 @@ export function ViewportGridProvider({ children, service }) {
const ret = { const ret = {
...state, ...state,
activeViewportIndex, activeViewportIndex: activeViewportIndexToSet,
layout: { layout: {
...state.layout, ...state.layout,
numCols, numCols,
@ -300,6 +304,7 @@ export function ViewportGridProvider({ children, service }) {
numRows, numRows,
numCols, numCols,
layoutOptions = [], layoutOptions = [],
activeViewportIndex,
findOrCreateViewport, findOrCreateViewport,
}) => }) =>
dispatch({ dispatch({
@ -309,6 +314,7 @@ export function ViewportGridProvider({ children, service }) {
numRows, numRows,
numCols, numCols,
layoutOptions, layoutOptions,
activeViewportIndex,
findOrCreateViewport, findOrCreateViewport,
}, },
}), }),
@ -375,9 +381,9 @@ export function ViewportGridProvider({ children, service }) {
setActiveViewportIndex: index => service.setActiveViewportIndex(index), // run it through the service itself since we want to publish events setActiveViewportIndex: index => service.setActiveViewportIndex(index), // run it through the service itself since we want to publish events
setDisplaySetsForViewport, setDisplaySetsForViewport,
setDisplaySetsForViewports, setDisplaySetsForViewports,
setLayout, setLayout: layout => service.setLayout(layout), // run it through the service itself since we want to publish events
reset, reset,
set, set: gridLayoutState => service.setState(gridLayoutState), // run it through the service itself since we want to publish events
getNumViewportPanes, getNumViewportPanes,
}; };

View File

@ -0,0 +1,52 @@
describe('OHIF Double Click', () => {
beforeEach(() => {
cy.checkStudyRouteInViewer(
'1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1',
'&hangingProtocolId=@ohif/hp-extension.mn'
);
cy.expectMinimumThumbnails(3);
cy.initCornerstoneToolsAliases();
cy.initCommonElementsAliases();
});
it('Should double click each viewport to one up and back', () => {
const numExpectedViewports = 3;
cy.get('[data-cy="viewport-pane"]')
.its('length')
.should('be.eq', numExpectedViewports);
for (let i = 0; i < numExpectedViewports; i += 1) {
// For whatever reason, with Cypress tests, we have to activate the
// viewport we are double clicking first.
cy.get('[data-cy="viewport-pane"]')
.eq(i)
.trigger('mousedown', 'center', { force: true })
.trigger('mouseup', 'center', { force: true });
// Wait for the viewport to be 'active'.
// TODO Is there a better way to do this?
cy.get('[data-cy="viewport-pane"]')
.eq(i)
.parent()
.find('[data-cy="viewport-pane"]')
.not('.pointer-events-none');
// The actual double click.
cy.get('[data-cy="viewport-pane"]')
.eq(i)
.trigger('dblclick', 'center');
cy.get('[data-cy="viewport-pane"]')
.its('length')
.should('be.eq', 1);
cy.get('[data-cy="viewport-pane"]')
.eq(0)
.trigger('dblclick', 'center');
cy.get('[data-cy="viewport-pane"]')
.its('length')
.should('be.eq', numExpectedViewports);
}
});
});