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:
parent
4734b3bac6
commit
d5ff590dfc
@ -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);
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
92
extensions/cornerstone/src/initDoubleClick.ts
Normal file
92
extensions/cornerstone/src/initDoubleClick.ts
Normal 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;
|
||||||
21
extensions/cornerstone/src/utils/findNearbyToolData.ts
Normal file
21
extensions/cornerstone/src/utils/findNearbyToolData.ts
Normal 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'
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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[];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
37
platform/core/src/utils/subscribeToNextViewportGridChange.ts
Normal file
37
platform/core/src/utils/subscribeToNextViewportGridChange.ts
Normal 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 };
|
||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user