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: Add simple multi-monitor support #4178

Open
wants to merge 35 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
ed09808
feat: Multi monitor launch
wayfarer3130 May 27, 2024
efe4abd
fix: Added update to existing window
wayfarer3130 May 27, 2024
b6cf4c0
feat: Add preserved multimonitor mode
wayfarer3130 May 27, 2024
26d181d
Fix the default mode to allow multimonitor
wayfarer3130 May 27, 2024
06a4fae
Merge remote-tracking branch 'origin/master' into feat/multi-monitor-…
wayfarer3130 May 27, 2024
9abbc6c
Fix load with multimonitor not configured
wayfarer3130 May 27, 2024
bcfed7e
Exclude the app-config from terser mucking so it is readable.
wayfarer3130 May 27, 2024
ab789d7
fix tests
wayfarer3130 May 27, 2024
15d5744
Undid accidental removal of name in package.json
wayfarer3130 May 27, 2024
7052f31
Remove console logs
wayfarer3130 May 28, 2024
00419a4
Merge remote-tracking branch 'ohif/master' into feat/multi-monitor-take2
wayfarer3130 Aug 6, 2024
e08ea76
docs
wayfarer3130 Nov 7, 2024
3bc9025
Merge remote-tracking branch 'origin/master' into feat/multi-monitor-…
wayfarer3130 Nov 25, 2024
709c7c1
fix: Launch using new multi-monitor setup
wayfarer3130 Nov 26, 2024
8eb5950
Merge remote-tracking branch 'origin/master' into feat/multi-monitor-…
wayfarer3130 Nov 29, 2024
d18ae3a
Merge remote-tracking branch 'origin/master' into feat/multi-monitor-…
wayfarer3130 Dec 4, 2024
6f7e5bf
fix: Allow commandsManager commands to run on other window
wayfarer3130 Dec 4, 2024
652b3e6
fix: Preserve annotations by allowing commands to be run in other window
wayfarer3130 Dec 4, 2024
d3b0df6
Merge remote-tracking branch 'origin/master' into feat/multi-monitor-…
wayfarer3130 Dec 12, 2024
d7aec61
fix: Preserve multimonitor configuration
wayfarer3130 Dec 12, 2024
fd17be7
Fix paths when publicUrl is set
wayfarer3130 Dec 12, 2024
90522c1
Merge remote-tracking branch 'origin/master' into feat/multi-monitor-…
wayfarer3130 Dec 13, 2024
46ec727
fix: Other monitor show command
wayfarer3130 Dec 13, 2024
96bae40
Remove console log
wayfarer3130 Dec 19, 2024
ebddf4d
Merge remote-tracking branch 'origin/master' into feat/multi-monitor-…
wayfarer3130 Dec 19, 2024
8a680dc
fix: Multi-monitor - mostly working variant
wayfarer3130 Dec 20, 2024
513d8c0
fix: Add some default options
wayfarer3130 Dec 20, 2024
351c909
Relaunch without refresh in any windows
wayfarer3130 Dec 20, 2024
7d06e7b
fix: Apply full hanging protocol when changing studies
wayfarer3130 Dec 30, 2024
879b221
Merge remote-tracking branch 'origin/master' into feat/multi-monitor-…
wayfarer3130 Dec 30, 2024
ddc7787
fix: loadStudy wasn't working on same window
wayfarer3130 Dec 31, 2024
f5e8873
Add the simplest multi-monitor display
wayfarer3130 Dec 31, 2024
cfa1a6a
fix: Improve context menu behavior by checking for annotation lock st…
sedghi Jan 9, 2025
9d09315
Merge branch 'master' of github.com:OHIF/Viewers into feat/multi-moni…
sedghi Jan 9, 2025
cf6640d
Merge remote-tracking branch 'origin/master' into feat/multi-monitor-…
wayfarer3130 Jan 10, 2025
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
1 change: 1 addition & 0 deletions .webpack/webpack.base.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
config.optimization.minimizer = [
new TerserJSPlugin({
parallel: true,
exclude: /app-config.js/,
Copy link
Member

Choose a reason for hiding this comment

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

I guess this is only in debug mode? Sometimes the app config is huge and it is desirable to minify it

terserOptions: {},
}),
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,22 @@ export default class ContextMenuController {
}

const { event, subMenu, menuId, menus, selectorProps } = contextMenuProps;
if (!menus) {
console.warn('No menus found for', menuId);
return;
}

const { locking, visibility } = CsAnnotation;
const targetAnnotationId = selectorProps?.nearbyToolData?.annotationUID as string;
const isLocked = locking.isAnnotationLocked(targetAnnotationId);
const isVisible = visibility.isAnnotationVisible(targetAnnotationId);

if (isLocked || !isVisible) {
console.warn(`Annotation is ${isLocked ? 'locked' : 'not visible'}.`);
return;
if (targetAnnotationId) {
const isLocked = locking.isAnnotationLocked(
annotationManager.getAnnotation(targetAnnotationId)
);

if (isLocked) {
console.warn('Annotation is locked.');
return;
}
}

const items = ContextMenuItemsBuilder.getMenuItems(
Expand All @@ -73,7 +80,7 @@ export default class ContextMenuController {
preventCutOf: true,
defaultPosition: ContextMenuController._getDefaultPosition(
defaultPointsPosition,
event?.detail,
event?.detail || event,
viewportElement
),
event,
Expand All @@ -89,7 +96,7 @@ export default class ContextMenuController {
menus,
event,
subMenu,
eventData: event?.detail,
eventData: event?.detail || event,

onClose: () => {
this.services.uiDialogService.dismiss({ id: 'context-menu' });
Expand Down Expand Up @@ -136,8 +143,8 @@ export default class ContextMenuController {
};

static _getEventDefaultPosition = eventDetail => ({
x: eventDetail && eventDetail.currentPoints.client[0],
y: eventDetail && eventDetail.currentPoints.client[1],
x: eventDetail?.currentPoints?.client[0] ?? eventDetail?.pageX,
y: eventDetail?.currentPoints?.client[1] ?? eventDetail?.pageY,
});

static _getElementDefaultPosition = element => {
Expand Down
4 changes: 2 additions & 2 deletions extensions/default/src/Panels/WrappedPanelStudyBrowser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ import requestDisplaySetCreationForStudy from './requestDisplaySetCreationForStu
* @param {object} commandsManager
* @param {object} extensionManager
*/
function WrappedPanelStudyBrowser({ commandsManager, extensionManager, servicesManager }) {
function WrappedPanelStudyBrowser({ extensionManager, servicesManager }) {
// TODO: This should be made available a different way; route should have
// already determined our datasource
const dataSource = extensionManager.getDataSources()[0];
const [dataSource] = extensionManager.getActiveDataSource();
const _getStudiesForPatientByMRN = getStudiesForPatientByMRN.bind(null, dataSource);
const _getImageSrcFromImageId = useCallback(
_createGetImageSrcFromImageIdFn(extensionManager),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function requestDisplaySetCreationForStudy(
return;
}

dataSource.retrieve.series.metadata({ StudyInstanceUID, madeInClient });
return dataSource.retrieve.series.metadata({ StudyInstanceUID, madeInClient });
Copy link
Member

Choose a reason for hiding this comment

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

There seems to be two files named requestDisplaySetCreationForStudy , can we re-use one

}

export default requestDisplaySetCreationForStudy;
7 changes: 1 addition & 6 deletions extensions/default/src/ViewerLayout/ViewerHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ function ViewerHeader({
const onClickReturnButton = () => {
const { pathname } = location;
const dataSourceIdx = pathname.indexOf('/', 1);
const query = new URLSearchParams(window.location.search);
const configUrl = query.get('configUrl');

const dataSourceName = pathname.substring(dataSourceIdx + 1);
const existingDataSource = extensionManager.getDataSources(dataSourceName);
Expand All @@ -36,10 +34,7 @@ function ViewerHeader({
if (dataSourceIdx !== -1 && existingDataSource) {
searchQuery.append('datasources', pathname.substring(dataSourceIdx + 1));
}

if (configUrl) {
searchQuery.append('configUrl', configUrl);
}
preserveQueryParameters(searchQuery);

navigate({
pathname: publicUrl,
Expand Down
79 changes: 71 additions & 8 deletions extensions/default/src/commandsModule.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Types } from '@ohif/core';
import { Types, DicomMetadataStore } from '@ohif/core';

import { ContextMenuController, defaultContextMenu } from './CustomizableContextMenu';
import DicomTagBrowser from './DicomTagBrowser/DicomTagBrowser';
Expand All @@ -16,6 +16,7 @@ import { useHangingProtocolStageIndexStore } from './stores/useHangingProtocolSt
import { useToggleHangingProtocolStore } from './stores/useToggleHangingProtocolStore';
import { useViewportsByPositionStore } from './stores/useViewportsByPositionStore';
import { useToggleOneUpViewportGridStore } from './stores/useToggleOneUpViewportGridStore';
import requestDisplaySetCreationForStudy from './Panels/requestDisplaySetCreationForStudy';

export type HangingProtocolParams = {
protocolId?: string;
Expand All @@ -33,6 +34,7 @@ export type UpdateViewportDisplaySetParams = {
const commandsModule = ({
servicesManager,
commandsManager,
extensionManager,
}: Types.Extensions.ExtensionParams): Types.Extensions.CommandsModule => {
const {
customizationService,
Expand All @@ -41,12 +43,56 @@ const commandsModule = ({
uiNotificationService,
viewportGridService,
displaySetService,
multiMonitorService,
} = servicesManager.services;

// Define a context menu controller for use with any context menus
const contextMenuController = new ContextMenuController(servicesManager, commandsManager);

const actions = {
/**
* Runs a command in multi-monitor mode. No-op if not multi-monitor.
*/
multimonitor: async options => {
const { screenDelta, StudyInstanceUID, commands, hashParams } = options;
if (multiMonitorService.numberOfScreens < 2) {
return options.fallback?.(options);
}

const newWindow = await multiMonitorService.launchWindow(
StudyInstanceUID,
screenDelta,
hashParams
);

// Only run commands if we successfully got a window with a commands manager
if (newWindow && commands) {
// Todo: fix this properly, but it takes time for the new window to load
// and then the commandsManager is available for it
setTimeout(() => {
multiMonitorService.run(screenDelta, commands, options);
}, 1000);
}
},

/**
* Ensures that the specified study is available for display
* Then, if commands is specified, runs the given commands list/instance
*/
loadStudy: async options => {
const { StudyInstanceUID } = options;
const displaySets = displaySetService.getActiveDisplaySets();
const isActive = displaySets.find(ds => ds.StudyInstanceUID === StudyInstanceUID);
if (isActive) {
return;
}
const [dataSource] = extensionManager.getActiveDataSource();
await requestDisplaySetCreationForStudy(dataSource, displaySetService, StudyInstanceUID);

const study = DicomMetadataStore.getStudy(StudyInstanceUID);
hangingProtocolService.addStudy(study);
},

/**
* Show the context menu.
* @param options.menuId defines the menu name to lookup, from customizationService
Expand Down Expand Up @@ -135,11 +181,13 @@ const commandsModule = ({
*/
setHangingProtocol: ({
activeStudyUID = '',
StudyInstanceUID = '',
protocolId,
stageId,
stageIndex,
reset = false,
}: HangingProtocolParams): boolean => {
const toUseStudyInstanceUID = activeStudyUID || StudyInstanceUID;
try {
// Stores in the state the display set selector id to displaySetUID mapping
// Pass in viewportId for the active viewport. This item will get set as
Expand All @@ -158,7 +206,7 @@ const commandsModule = ({
}
} else if (stageIndex === undefined && stageId === undefined) {
// Re-set the same stage as was previously used
const hangingId = `${activeStudyUID || hpInfo.activeStudyUID}:${protocolId}`;
const hangingId = `${toUseStudyInstanceUID || hpInfo.activeStudyUID}:${protocolId}`;
stageIndex = hangingProtocolStageIndexMap[hangingId]?.stageIndex;
}

Expand All @@ -169,21 +217,34 @@ const commandsModule = ({
stageIndex,
});

if (activeStudyUID) {
hangingProtocolService.setActiveStudyUID(activeStudyUID);
}
const activeStudyChanged = hangingProtocolService.setActiveStudyUID(toUseStudyInstanceUID);

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

const { viewportGridState } = useViewportGridStore.getState();
const restoreProtocol = !reset && viewportGridState[storedHanging];

if (
reset ||
(activeStudyChanged &&
!viewportGridState[storedHanging] &&
stageIndex === undefined &&
stageId === undefined)
) {
// Run the hanging protocol fresh, re-using the existing study data
// This is done on reset or when the study changes and we haven't yet
// applied it, and don't specify exact stage to use.
const displaySets = displaySetService.getActiveDisplaySets();
hangingProtocolService.run(
{ activeStudyUID: toUseStudyInstanceUID, displaySets },
protocolId
);
} else if (
protocolId === hpInfo.protocolId &&
useStageIdx === hpInfo.stageIndex &&
!activeStudyUID
!toUseStudyInstanceUID
) {
// Clear the HP setting to reset them
hangingProtocolService.setProtocol(protocolId, {
Expand All @@ -204,7 +265,7 @@ const commandsModule = ({
// Do this after successfully applying the update
const { setDisplaySetSelector } = useDisplaySetSelectorStore.getState();
setDisplaySetSelector(
`${activeStudyUID || hpInfo.activeStudyUID}:activeDisplaySet:0`,
`${toUseStudyInstanceUID || hpInfo.activeStudyUID}:activeDisplaySet:0`,
null
);
return true;
Expand Down Expand Up @@ -562,6 +623,8 @@ const commandsModule = ({
};

const definitions = {
multimonitor: actions.multimonitor,
loadStudy: actions.loadStudy,
showContextMenu: actions.showContextMenu,
closeContextMenu: actions.closeContextMenu,
clearMeasurements: actions.clearMeasurements,
Expand Down
81 changes: 81 additions & 0 deletions extensions/default/src/customizations/studyBrowserContextMenu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
export const studyBrowserContextMenu = {
id: 'StudyBrowser.studyContextMenu',
customizationType: 'ohif.contextMenu',
menus: [
{
id: 'studyBrowserContextMenu',
// selector restricts context menu to when there is nearbyToolData
items: [
{
label: 'Show in Grid',
commands: {
commandName: 'loadStudy',
commandOptions: {
commands: {
commandName: 'setHangingProtocol',
commandOptions: {
protocolId: '@ohif/mnGrid8',
},
},
},
},
},
{
label: 'Show in other monitor',
selector: ({ isMultimonitor }) => isMultimonitor,
commands: {
commandName: 'multimonitor',
commandOptions: {
commands: {
commandName: 'loadStudy',
commandOptions: {
commands: {
commandName: 'setHangingProtocol',
commandOptions: {
protocolId: '@ohif/mnGrid8',
},
},
},
},
},
},
},
{
label: 'Compare All',
selector: ({ isMultimonitor }) => isMultimonitor,
commands: [
{
commandName: 'loadStudy',
commandOptions: {
commands: {
commandName: 'setHangingProtocol',
commandOptions: {
protocolId: '@ohif/mnGrid',
},
},
},
},
{
commandName: 'multimonitor',
commandOptions: {
commands: {
commandName: 'loadStudy',
commandOptions: {
commands: {
commandName: 'setHangingProtocol',
commandOptions: {
protocolId: '@ohif/mnGridMonitor2',
},
},
},
},
},
},
],
},
],
},
],
};

export default studyBrowserContextMenu;
2 changes: 2 additions & 0 deletions extensions/default/src/getCustomizationModule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ProgressDropdownWithService } from './Components/ProgressDropdownWithSe
import DataSourceConfigurationComponent from './Components/DataSourceConfigurationComponent';
import { GoogleCloudDataSourceConfigurationAPI } from './DataSourceConfigurationAPI/GoogleCloudDataSourceConfigurationAPI';
import { utils } from '@ohif/core';
import studyBrowserContextMenu from './customizations/studyBrowserContextMenu';

const formatDate = utils.formatDate;

Expand Down Expand Up @@ -140,6 +141,7 @@ export default function getCustomizationModule({ servicesManager, extensionManag
return clonedObject;
},
},
studyBrowserContextMenu,

{
// the generic GUI component to configure a data source using an instance of a BaseDataSourceConfigurationAPI
Expand Down
14 changes: 11 additions & 3 deletions extensions/default/src/getHangingProtocolModule.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import hpMNGrid from './hangingprotocols/hpMNGrid';
import { hpMN, hpMN8, hpMNMonitor2 } from './hangingprotocols/hpMNGrid';
import hpMNCompare from './hangingprotocols/hpCompare';
import hpMammography from './hangingprotocols/hpMammo';
import hpScale from './hangingprotocols/hpScale';
Expand Down Expand Up @@ -142,8 +142,16 @@ function getHangingProtocolModule() {
},
// Create a MxN hanging protocol available by default
{
name: hpMNGrid.id,
protocol: hpMNGrid,
name: hpMN.id,
protocol: hpMN,
},
{
name: hpMN8.id,
protocol: hpMN8,
},
{
name: hpMNMonitor2.id,
protocol: hpMNMonitor2,
},
];
}
Expand Down
Loading
Loading