From ed09808dddfb3824d6c5ca2e627f48b0d9dbc355 Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Mon, 27 May 2024 12:10:48 -0400 Subject: [PATCH 01/23] feat: Multi monitor launch --- extensions/default/src/hpMNGrid.ts | 34 ++++++--- .../PanelStudyBrowserTracking.tsx | 19 +++-- platform/app/public/config/e2e.js | 62 +++++++++++++++ platform/app/src/appInit.js | 2 + platform/core/src/index.ts | 3 + .../core/src/services/MultiMonitorService.ts | 76 +++++++++++++++++++ platform/core/src/services/ServicesManager.ts | 7 +- platform/core/src/services/index.ts | 2 + platform/core/src/types/Services.ts | 2 + .../services/data/MultiMonitorService.md | 33 ++++++++ .../components/StudyBrowser/StudyBrowser.tsx | 2 + .../ui/src/components/StudyItem/StudyItem.tsx | 28 +++++++ 12 files changed, 249 insertions(+), 21 deletions(-) create mode 100644 platform/core/src/services/MultiMonitorService.ts create mode 100644 platform/docs/docs/platform/services/data/MultiMonitorService.md diff --git a/extensions/default/src/hpMNGrid.ts b/extensions/default/src/hpMNGrid.ts index b65cc0b70ed..b044b34353d 100644 --- a/extensions/default/src/hpMNGrid.ts +++ b/extensions/default/src/hpMNGrid.ts @@ -43,6 +43,24 @@ const hpMN: Types.HangingProtocol.Protocol = { ], }, }, + screens: [ + // This hanging protocol will match screens primary and secondary + { + matches: ['primary', 'secondary'], + primary: { + x: 0.5, + y: 0, + width: 0.5, + height: 1, + }, + secondary: { + x: 0.5, + y: 0, + width: 0.5, + height: 1, + }, + }, + ], defaultViewport: { viewportOptions: { viewportType: 'stack', @@ -58,7 +76,7 @@ const hpMN: Types.HangingProtocol.Protocol = { }, stages: [ { - id: '2x2', + name: '2x2', stageActivation: { enabled: { minViewportsMatched: 4, @@ -124,11 +142,7 @@ const hpMN: Types.HangingProtocol.Protocol = { // 3x1 stage { - id: '3x1', - // Obsolete settings: - requiredViewports: 1, - preferredViewports: 3, - // New equivalent: + name: '3x1', stageActivation: { enabled: { minViewportsMatched: 3, @@ -182,9 +196,7 @@ const hpMN: Types.HangingProtocol.Protocol = { // A 2x1 stage { - id: '2x1', - requiredViewports: 1, - preferredViewports: 2, + name: '2x1', stageActivation: { enabled: { minViewportsMatched: 2, @@ -226,9 +238,7 @@ const hpMN: Types.HangingProtocol.Protocol = { // A 1x1 stage - should be automatically activated if there is only 1 viewable instance { - id: '1x1', - requiredViewports: 1, - preferredViewports: 1, + name: '1x1', stageActivation: { enabled: { minViewportsMatched: 1, diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx index d6f6e084488..af77242a3f1 100644 --- a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx @@ -25,6 +25,7 @@ function PanelStudyBrowserTracking({ hangingProtocolService, uiNotificationService, measurementService, + multiMonitorService, } = servicesManager.services; const navigate = useNavigate(); @@ -279,18 +280,25 @@ function PanelStudyBrowserTracking({ hangingProtocolService ); + const _launchMultiMonitor = + multiMonitorService.numberOfScreens > 1 + ? (studyInstanceUID, screenDelta) => { + multiMonitorService.launchStudy(studyInstanceUID, screenDelta); + } + : null; + // TODO: Should not fire this on "close" - function _handleStudyClick(StudyInstanceUID) { - const shouldCollapseStudy = expandedStudyInstanceUIDs.includes(StudyInstanceUID); + function _handleStudyClick(studyInstanceUID) { + const shouldCollapseStudy = expandedStudyInstanceUIDs.includes(studyInstanceUID); const updatedExpandedStudyInstanceUIDs = shouldCollapseStudy - ? [...expandedStudyInstanceUIDs.filter(stdyUid => stdyUid !== StudyInstanceUID)] - : [...expandedStudyInstanceUIDs, StudyInstanceUID]; + ? [...expandedStudyInstanceUIDs.filter(studyUid => studyUid !== studyInstanceUID)] + : [...expandedStudyInstanceUIDs, studyInstanceUID]; setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs); if (!shouldCollapseStudy) { const madeInClient = true; - requestDisplaySetCreationForStudy(displaySetService, StudyInstanceUID, madeInClient); + requestDisplaySetCreationForStudy(displaySetService, studyInstanceUID, madeInClient); } } @@ -397,6 +405,7 @@ function PanelStudyBrowserTracking({ activeTabName={activeTabName} expandedStudyInstanceUIDs={expandedStudyInstanceUIDs} onClickStudy={_handleStudyClick} + onClickLaunch={_launchMultiMonitor} onClickTab={clickedTabName => { setActiveTabName(clickedTabName); }} diff --git a/platform/app/public/config/e2e.js b/platform/app/public/config/e2e.js index 896b07b4f03..cbe90a65f12 100644 --- a/platform/app/public/config/e2e.js +++ b/platform/app/public/config/e2e.js @@ -14,6 +14,68 @@ window.config = { investigationalUseDialog: { option: 'never', }, + // Defines multi-monitor layouts + multimonitor: [ + { + test: ({ multimonitor }) => multimonitor === 'split', + screens: [ + { + id: 'primary', + // This is the primary screen, so don't launch is separately, but use primary + launch: 'primary', + location: { + screen: 0, + width: 50, + height: 100, + left: 0, + top: 0, + }, + }, + { + id: 'secondary', + // This is a window instance, so launch as a url + launch: 'url', + location: { + screen: 0, + width: 50, + height: 100, + left: 50, + top: 0, + }, + }, + ], + }, + + { + test: ({ multimonitor }) => multimonitor === '2', + screens: [ + { + id: 'primary', + // This is the primary screen, so don't launch is separately, but use primary + launch: 'primary', + location: { + screen: 0, + width: 100, + height: 100, + left: 0, + top: 0, + }, + }, + { + id: 'secondary', + // This is a window instance, so launch as a url + launch: 'url', + location: { + screen: 1, + width: 100, + height: 100, + left: 50, + top: 0, + }, + }, + ], + }, + ], dataSources: [ { namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', diff --git a/platform/app/src/appInit.js b/platform/app/src/appInit.js index 4e26eee2149..232f97e2862 100644 --- a/platform/app/src/appInit.js +++ b/platform/app/src/appInit.js @@ -20,6 +20,7 @@ import { CustomizationService, PanelService, WorkflowStepsService, + MultiMonitorService, // utils, } from '@ohif/core'; @@ -56,6 +57,7 @@ async function appInit(appConfigOrFunc, defaultExtensions, defaultModes) { servicesManager.setExtensionManager(extensionManager); servicesManager.registerServices([ + [MultiMonitorService.REGISTRATION, appConfig.multimonitor], UINotificationService.REGISTRATION, UIModalService.REGISTRATION, UIDialogService.REGISTRATION, diff --git a/platform/core/src/index.ts b/platform/core/src/index.ts index 465cfe2f9a5..99b6760e8fc 100644 --- a/platform/core/src/index.ts +++ b/platform/core/src/index.ts @@ -33,6 +33,7 @@ import { StateSyncService, PanelService, WorkflowStepsService, + MultiMonitorService, } from './services'; import { DisplaySetMessage, DisplaySetMessageList } from './services/DisplaySetService'; @@ -78,6 +79,7 @@ const OHIF = { ViewportGridService, HangingProtocolService, UserAuthenticationService, + MultiMonitorService, IWebApiDataSource, DicomMetadataStore, pubSubServiceInterface, @@ -118,6 +120,7 @@ export { DisplaySetMessage, DisplaySetMessageList, MeasurementService, + MultiMonitorService, ToolbarService, ViewportGridService, HangingProtocolService, diff --git a/platform/core/src/services/MultiMonitorService.ts b/platform/core/src/services/MultiMonitorService.ts new file mode 100644 index 00000000000..78a0e497114 --- /dev/null +++ b/platform/core/src/services/MultiMonitorService.ts @@ -0,0 +1,76 @@ +/** + * This service manages multiple monitors or windows. + */ +export class MultiMonitorService { + public readonly numberOfScreens: number; + private screenConfig; + private screenInfo; + private launchWindows = []; + + public readonly screenNumber: number; + public readonly isMultimonitor: boolean; + + public static REGISTRATION = { + name: 'multiMonitorService', + create: ({ configuration }): MultiMonitorService => { + return new MultiMonitorService(configuration); + }, + }; + + constructor(configuration) { + const params = new URLSearchParams(window.location.search); + const screenNumber = params.get('screenNumber'); + const multimonitor = params.get('multimonitor'); + const testParams = { params, screenNumber, multimonitor }; + this.screenNumber = screenNumber ? Number(screenNumber) : 0; + for (const screenConfig of configuration || []) { + if (screenConfig.test(testParams)) { + this.isMultimonitor = true; + this.screenConfig = screenConfig; + this.numberOfScreens = screenConfig.screens.length; + this.screenInfo = this.screenConfig.screens[this.screenNumber]; + if (!this.screenInfo) { + throw new Error(`Screen ${screenNumber} not configured in ${this.screenConfig}`); + } + console.log( + '*** Multimonitor', + screenConfig, + this.numberOfScreens, + this.screenNumber, + this.screenInfo + ); + return; + } + } + console.log('*** Single monitor', screenNumber, this.screenNumber); + this.numberOfScreens = 1; + this.isMultimonitor = false; + } + + public launchStudy(studyUid: string, screenDelta = 1) { + const forScreen = (this.screenNumber + screenDelta) % this.numberOfScreens; + const forWindow = this.getOrCreateWindow(forScreen, studyUid); + console.log('Launched to', forWindow); + } + + public getOrCreateWindow(screenNumber, studyUid: string) { + if (screenNumber === this.screenNumber) { + return window; + } + if (!this.launchWindows[screenNumber]) { + const { pathname, search, origin } = window.location; + if (search.indexOf('multimonitor=secondary') !== -1) { + throw new Error('Launch from secondary not supported'); + } + + const width = 1024; + const height = 1024; + this.launchWindows[screenNumber] = window.open( + `${origin}${pathname}?multimonitor=secondary&screenNumber=${screenNumber}&StudyInstanceUIDs=${studyUid}`, + `${this.screenConfig.screens[screenNumber].id}`, + `screenX=${width + 1},top=0,width=${width},height=${height}` + ); + } + return this.launchWindows[screenNumber]; + } +} diff --git a/platform/core/src/services/ServicesManager.ts b/platform/core/src/services/ServicesManager.ts index 759a965f483..e30da049e5e 100644 --- a/platform/core/src/services/ServicesManager.ts +++ b/platform/core/src/services/ServicesManager.ts @@ -15,7 +15,7 @@ export default class ServicesManager { this.registeredServiceNames = []; } - setExtensionManager(extensionManager) { + public setExtensionManager(extensionManager) { this._extensionManager = extensionManager; } @@ -25,7 +25,7 @@ export default class ServicesManager { * @param {Object} service * @param {Object} configuration */ - registerService(service, configuration = {}) { + public registerService(service, configuration = {}) { if (!service) { log.warn('Attempting to register a null/undefined service. Exiting early.'); return; @@ -49,7 +49,6 @@ export default class ServicesManager { extensionManager: this._extensionManager, commandsManager: this._commandsManager, servicesManager: this, - extensionManager: this._extensionManager, }); if (service.altName) { console.log('Registering old name', service.altName); @@ -70,7 +69,7 @@ export default class ServicesManager { * * @param {Object[]} services - Array of services */ - registerServices(services) { + public registerServices(services) { services.forEach(service => { const hasConfiguration = Array.isArray(service); diff --git a/platform/core/src/services/index.ts b/platform/core/src/services/index.ts index f8d972057cc..01697d602a4 100644 --- a/platform/core/src/services/index.ts +++ b/platform/core/src/services/index.ts @@ -17,6 +17,7 @@ import CustomizationService from './CustomizationService'; import StateSyncService from './StateSyncService'; import PanelService from './PanelService'; import WorkflowStepsService from './WorkflowStepsService'; +import { MultiMonitorService } from './MultiMonitorService'; import type Services from '../types/Services'; @@ -32,6 +33,7 @@ export { UINotificationService, UIViewportDialogService, DicomMetadataStore, + MultiMonitorService, DisplaySetService, ToolbarService, ViewportGridService, diff --git a/platform/core/src/types/Services.ts b/platform/core/src/types/Services.ts index 119a2da876d..f0a6ae0abe1 100644 --- a/platform/core/src/types/Services.ts +++ b/platform/core/src/types/Services.ts @@ -14,6 +14,7 @@ import { PanelService, UIDialogService, UIViewportDialogService, + MultiMonitorService, } from '../services'; /** @@ -36,6 +37,7 @@ interface Services { uiDialogService?: UIDialogService; uiViewportDialogService?: UIViewportDialogService; panelService?: PanelService; + multiMonitorService?: MultiMonitorService; } export default Services; diff --git a/platform/docs/docs/platform/services/data/MultiMonitorService.md b/platform/docs/docs/platform/services/data/MultiMonitorService.md new file mode 100644 index 00000000000..94d05a3a18b --- /dev/null +++ b/platform/docs/docs/platform/services/data/MultiMonitorService.md @@ -0,0 +1,33 @@ +--- +sidebar_position: 5 +sidebar_label: Multi Monitor Service +--- + +# Multi Monitor Service + +The multi-monitor service provides detection, launch and communication support +for multiple monitors or windows/screens within a single monitor. + + + +## GUI Behaviour + +Double click study in study browser => set as active study for current screen and apply hanging protocol as +active study + +(later) +Right click study in study browser => brings up menu to select as prior, or apply specific HP + +Two modes + - independent left/right windows + - launch studies on left/right windows + - each applies own hanging protocol on "active" study for that window + +(later) + - coordinated hanging protocol + - determined by hanging protocol + - coordinates mixed studies + - applies full hanging protocol, but sub-selects it for specific screen + +(later) +Single "launch" button for launching to other screen diff --git a/platform/ui/src/components/StudyBrowser/StudyBrowser.tsx b/platform/ui/src/components/StudyBrowser/StudyBrowser.tsx index 89175ccbbad..1454cb9ad25 100644 --- a/platform/ui/src/components/StudyBrowser/StudyBrowser.tsx +++ b/platform/ui/src/components/StudyBrowser/StudyBrowser.tsx @@ -25,6 +25,7 @@ const StudyBrowser = ({ expandedStudyInstanceUIDs, onClickTab, onClickStudy, + onClickLaunch, onClickThumbnail, onDoubleClickThumbnail, onClickUntrack, @@ -51,6 +52,7 @@ const StudyBrowser = ({ onClick={() => { onClickStudy(studyInstanceUid); }} + onClickLaunch={onClickLaunch?.bind(null, studyInstanceUid)} data-cy="thumbnail-list" /> {isExpanded && displaySets && ( diff --git a/platform/ui/src/components/StudyItem/StudyItem.tsx b/platform/ui/src/components/StudyItem/StudyItem.tsx index 6e99c48d18e..d3897f73e7a 100644 --- a/platform/ui/src/components/StudyItem/StudyItem.tsx +++ b/platform/ui/src/components/StudyItem/StudyItem.tsx @@ -16,8 +16,22 @@ const StudyItem = ({ trackedSeries, isActive, onClick, + onClickLaunch, }) => { const { t } = useTranslation('StudyItem'); + + const onSetActive = evt => { + evt.stopPropagation(); + onClickLaunch(0); + return false; + }; + const onLaunchWindow = evt => { + onClickLaunch(1); + evt.stopPropagation(); + return false; + }; + console.log('Has onClickLaunch', !!onClickLaunch, onClickLaunch); + return (
{numInstances}
+ {!!onClickLaunch && ( +
+ + +
+ )}
{modalities}
From efe4abdcdcb46032f0fd2c425984987ee371750a Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Mon, 27 May 2024 12:52:29 -0400 Subject: [PATCH 02/23] fix: Added update to existing window --- .../core/src/services/MultiMonitorService.ts | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/platform/core/src/services/MultiMonitorService.ts b/platform/core/src/services/MultiMonitorService.ts index 78a0e497114..d0da20d6979 100644 --- a/platform/core/src/services/MultiMonitorService.ts +++ b/platform/core/src/services/MultiMonitorService.ts @@ -32,41 +32,35 @@ export class MultiMonitorService { if (!this.screenInfo) { throw new Error(`Screen ${screenNumber} not configured in ${this.screenConfig}`); } - console.log( - '*** Multimonitor', - screenConfig, - this.numberOfScreens, - this.screenNumber, - this.screenInfo - ); return; } } - console.log('*** Single monitor', screenNumber, this.screenNumber); this.numberOfScreens = 1; this.isMultimonitor = false; } public launchStudy(studyUid: string, screenDelta = 1) { const forScreen = (this.screenNumber + screenDelta) % this.numberOfScreens; - const forWindow = this.getOrCreateWindow(forScreen, studyUid); - console.log('Launched to', forWindow); + const url = this.createUrlForStudy(studyUid); + const forWindow = this.getOrCreateWindow(forScreen, url); + console.log('Launched to', forWindow, url); + forWindow.location = url; } - public getOrCreateWindow(screenNumber, studyUid: string) { + createUrlForStudy(studyUid) { + const { pathname, origin } = window.location; + return `${origin}${pathname}?StudyInstanceUIDs=${studyUid}`; + } + + public getOrCreateWindow(screenNumber, url = window.location.href) { if (screenNumber === this.screenNumber) { return window; } if (!this.launchWindows[screenNumber]) { - const { pathname, search, origin } = window.location; - if (search.indexOf('multimonitor=secondary') !== -1) { - throw new Error('Launch from secondary not supported'); - } - const width = 1024; const height = 1024; this.launchWindows[screenNumber] = window.open( - `${origin}${pathname}?multimonitor=secondary&screenNumber=${screenNumber}&StudyInstanceUIDs=${studyUid}`, + `${url}&multimonitor=secondary&screenNumber=${screenNumber}`, `${this.screenConfig.screens[screenNumber].id}`, `screenX=${width + 1},top=0,width=${width},height=${height}` ); From b6cf4c044d0c6ce7ce4842ed62046f599cfe7876 Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Mon, 27 May 2024 15:02:14 -0400 Subject: [PATCH 03/23] feat: Add preserved multimonitor mode --- .../default/src/ViewerLayout/ViewerHeader.tsx | 2 +- platform/app/public/config/e2e.js | 21 ++-- platform/app/src/routes/WorkList/WorkList.tsx | 5 +- .../core/src/services/MultiMonitorService.ts | 111 ++++++++++++++---- .../ui/src/components/StudyItem/StudyItem.tsx | 1 - 5 files changed, 105 insertions(+), 35 deletions(-) diff --git a/extensions/default/src/ViewerLayout/ViewerHeader.tsx b/extensions/default/src/ViewerLayout/ViewerHeader.tsx index 851db7f14cb..f93ab11a6b1 100644 --- a/extensions/default/src/ViewerLayout/ViewerHeader.tsx +++ b/extensions/default/src/ViewerLayout/ViewerHeader.tsx @@ -32,7 +32,7 @@ function ViewerHeader({ if (dataSourceIdx !== -1 && existingDataSource) { searchQuery.append('datasources', pathname.substring(dataSourceIdx + 1)); } - + servicesManager.services.multiMonitorService.appendQuery(searchQuery); if (configUrl) { searchQuery.append('configUrl', configUrl); } diff --git a/platform/app/public/config/e2e.js b/platform/app/public/config/e2e.js index cbe90a65f12..2a595f7b42d 100644 --- a/platform/app/public/config/e2e.js +++ b/platform/app/public/config/e2e.js @@ -17,16 +17,18 @@ window.config = { // Defines multi-monitor layouts multimonitor: [ { + id: 'split', test: ({ multimonitor }) => multimonitor === 'split', screens: [ { id: 'primary', // This is the primary screen, so don't launch is separately, but use primary launch: 'primary', + screen: null, location: { screen: 0, - width: 50, - height: 100, + width: 0.5, + height: 1, left: 0, top: 0, }, @@ -35,11 +37,11 @@ window.config = { id: 'secondary', // This is a window instance, so launch as a url launch: 'url', + screen: null, location: { - screen: 0, - width: 50, - height: 100, - left: 50, + width: 0.48, + height: 1, + left: 0.52, top: 0, }, }, @@ -47,14 +49,15 @@ window.config = { }, { + id: '2', test: ({ multimonitor }) => multimonitor === '2', screens: [ { id: 'primary', // This is the primary screen, so don't launch is separately, but use primary launch: 'primary', + screen: 0, location: { - screen: 0, width: 100, height: 100, left: 0, @@ -65,11 +68,11 @@ window.config = { id: 'secondary', // This is a window instance, so launch as a url launch: 'url', + screen: 1, location: { - screen: 1, width: 100, height: 100, - left: 50, + left: 0, top: 0, }, }, diff --git a/platform/app/src/routes/WorkList/WorkList.tsx b/platform/app/src/routes/WorkList/WorkList.tsx index 307c0b4e95b..8584bd19813 100644 --- a/platform/app/src/routes/WorkList/WorkList.tsx +++ b/platform/app/src/routes/WorkList/WorkList.tsx @@ -386,6 +386,7 @@ function WorkList({ if (filterValues.configUrl) { query.append('configUrl', filterValues.configUrl); } + servicesManager.services.multiMonitorService.appendQuery(query); query.append('StudyInstanceUIDs', studyInstanceUid); return ( mode.displayName && ( @@ -495,7 +496,7 @@ function WorkList({ }); } - const { customizationService } = servicesManager.services; + const { customizationService, multiMonitorService } = servicesManager.services; const { component: dicomUploadComponent } = customizationService.get('dicomUploadComponent') ?? {}; const uploadProps = @@ -605,6 +606,7 @@ const defaultFilterValues = { resultsPerPage: 25, datasources: '', configUrl: null, + multimonitor: null, }; function _tryParseInt(str, defaultValue) { @@ -634,6 +636,7 @@ function _getQueryFilterValues(params) { resultsPerPage: _tryParseInt(params.get('resultsPerPage'), undefined), datasources: params.get('datasources'), configUrl: params.get('configurl'), + multimonitor: params.get('multimonitor'), }; // Delete null/undefined keys diff --git a/platform/core/src/services/MultiMonitorService.ts b/platform/core/src/services/MultiMonitorService.ts index d0da20d6979..8c0540e97e7 100644 --- a/platform/core/src/services/MultiMonitorService.ts +++ b/platform/core/src/services/MultiMonitorService.ts @@ -3,8 +3,8 @@ */ export class MultiMonitorService { public readonly numberOfScreens: number; + private windowsConfig; private screenConfig; - private screenInfo; private launchWindows = []; public readonly screenNumber: number; @@ -13,7 +13,9 @@ export class MultiMonitorService { public static REGISTRATION = { name: 'multiMonitorService', create: ({ configuration }): MultiMonitorService => { - return new MultiMonitorService(configuration); + const service = new MultiMonitorService(configuration); + console.log('global multimonitor setup', (window as any).multimonitor); + return service; }, }; @@ -23,15 +25,24 @@ export class MultiMonitorService { const multimonitor = params.get('multimonitor'); const testParams = { params, screenNumber, multimonitor }; this.screenNumber = screenNumber ? Number(screenNumber) : 0; - for (const screenConfig of configuration || []) { - if (screenConfig.test(testParams)) { + (window as any).multimonitor ||= { + setLaunchWindows: this.setLaunchWindows, + launchWindows: this.launchWindows, + }; + this.launchWindows = (window as any).multimonitor?.launchWindows || this.launchWindows; + if (!this.screenNumber) { + this.launchWindows[0] = window; + } + for (const windowsConfig of configuration || []) { + if (windowsConfig.test(testParams)) { this.isMultimonitor = true; - this.screenConfig = screenConfig; - this.numberOfScreens = screenConfig.screens.length; - this.screenInfo = this.screenConfig.screens[this.screenNumber]; - if (!this.screenInfo) { - throw new Error(`Screen ${screenNumber} not configured in ${this.screenConfig}`); + this.numberOfScreens = windowsConfig.screens.length; + this.windowsConfig = windowsConfig; + this.screenConfig = windowsConfig.screens[this.screenNumber]; + if (!this.screenConfig) { + throw new Error(`Screen ${screenNumber} not configured in ${this.windowsConfig}`); } + window.name = this.screenConfig.id; return; } } @@ -39,31 +50,85 @@ export class MultiMonitorService { this.isMultimonitor = false; } - public launchStudy(studyUid: string, screenDelta = 1) { + /** + * Calls append ont he query with the multimonitor mode as appropriate. + */ + public appendQuery(query) { + if (!this.isMultimonitor) { + return; + } + query.append('multimonitor', this.windowsConfig.id); + if (this.screenNumber) { + query.append('screenNumber', String(this.screenNumber)); + } + } + + /** Sets the launch windows for later use, shared amongst all windows. */ + public setLaunchWindows = launchWindows => { + this.launchWindows = launchWindows; + (window as any).multimonitor.launchWindows = launchWindows; + }; + + public async launchStudy(studyUid: string, screenDelta = 1) { const forScreen = (this.screenNumber + screenDelta) % this.numberOfScreens; - const url = this.createUrlForStudy(studyUid); - const forWindow = this.getOrCreateWindow(forScreen, url); - console.log('Launched to', forWindow, url); + const url = this.createUrlForStudy(studyUid, forScreen); + const forWindow = await this.getOrCreateWindow(forScreen, url); forWindow.location = url; + forWindow.onload = () => { + if ((forWindow as any).multimonitor.setLaunchWindows) { + (forWindow as any).multimonitor.setLaunchWindows(this.launchWindows); + } else { + console.warn('At end of load, no launch windows array'); + } + forWindow.onload = null; + }; + forWindow.multimonitor?.setLaunchWindows?.(this.launchWindows); } - createUrlForStudy(studyUid) { + createUrlForStudy(studyUid, screenNumber) { const { pathname, origin } = window.location; - return `${origin}${pathname}?StudyInstanceUIDs=${studyUid}`; + return `${origin}${pathname}?StudyInstanceUIDs=${studyUid}&multimonitor=${this.windowsConfig.id}&screenNumber=${screenNumber}`; } - public getOrCreateWindow(screenNumber, url = window.location.href) { + /** + * Creates a new window showing the given url by default, or gets an existing + * window. + */ + public async getOrCreateWindow(screenNumber, url) { if (screenNumber === this.screenNumber) { return window; } - if (!this.launchWindows[screenNumber]) { - const width = 1024; - const height = 1024; - this.launchWindows[screenNumber] = window.open( + const screenInfo = this.windowsConfig.screens[screenNumber]; + if (!this.launchWindows[screenNumber] || this.launchWindows[screenNumber].closed) { + const screenDetails = await window.getScreenDetails?.(); + console.log('screenDetails=', screenDetails); + const screen = + (screenInfo.screen >= 0 && screenDetails.screens[screenInfo.screen]) || + screenDetails.currentScreen || + window.screen; + console.log('Chosen screen', screen); + const { width = 1024, height = 1024, availLeft = 0, availTop = 0 } = screen || {}; + const newScreen = this.windowsConfig.screens[screenNumber]; + const { + width: widthPercent = 1, + height: heightPercent = 1, + top: topPercent = 0, + left: leftPercent = 0, + } = newScreen.location || {}; + + const useLeft = Math.round(availLeft + leftPercent * width); + const useTop = Math.round(availTop + topPercent * height); + const useWidth = Math.round(width * widthPercent); + const useHeight = Math.round(height * heightPercent); + + const newWindow = (this.launchWindows[screenNumber] = window.open( `${url}&multimonitor=secondary&screenNumber=${screenNumber}`, - `${this.screenConfig.screens[screenNumber].id}`, - `screenX=${width + 1},top=0,width=${width},height=${height}` - ); + `${newScreen.id}`, + `screenX=${useLeft},screenY=${useTop},width=${useWidth},height=${useHeight}` + )); + (newWindow as any).multimonitor = { + launchWindows: this.launchWindows, + }; } return this.launchWindows[screenNumber]; } diff --git a/platform/ui/src/components/StudyItem/StudyItem.tsx b/platform/ui/src/components/StudyItem/StudyItem.tsx index d3897f73e7a..45814f4675b 100644 --- a/platform/ui/src/components/StudyItem/StudyItem.tsx +++ b/platform/ui/src/components/StudyItem/StudyItem.tsx @@ -30,7 +30,6 @@ const StudyItem = ({ evt.stopPropagation(); return false; }; - console.log('Has onClickLaunch', !!onClickLaunch, onClickLaunch); return (
Date: Mon, 27 May 2024 15:09:03 -0400 Subject: [PATCH 04/23] Fix the default mode to allow multimonitor --- platform/app/public/config/default.js | 65 +++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/platform/app/public/config/default.js b/platform/app/public/config/default.js index 62ff50cf9b9..cc583bdd8ec 100644 --- a/platform/app/public/config/default.js +++ b/platform/app/public/config/default.js @@ -23,6 +23,71 @@ const config = { prefetch: 25, }, // filterQueryParam: false, + // Defines multi-monitor layouts + multimonitor: [ + { + id: 'split', + test: ({ multimonitor }) => multimonitor === 'split', + screens: [ + { + id: 'primary', + // This is the primary screen, so don't launch is separately, but use primary + launch: 'primary', + screen: null, + location: { + screen: 0, + width: 0.5, + height: 1, + left: 0, + top: 0, + }, + }, + { + id: 'secondary', + // This is a window instance, so launch as a url + launch: 'url', + screen: null, + location: { + width: 0.48, + height: 1, + left: 0.52, + top: 0, + }, + }, + ], + }, + + { + id: '2', + test: ({ multimonitor }) => multimonitor === '2', + screens: [ + { + id: 'primary', + // This is the primary screen, so don't launch is separately, but use primary + launch: 'primary', + screen: 0, + location: { + width: 100, + height: 100, + left: 0, + top: 0, + }, + }, + { + id: 'secondary', + // This is a window instance, so launch as a url + launch: 'url', + screen: 1, + location: { + width: 100, + height: 100, + left: 0, + top: 0, + }, + }, + ], + }, + ], defaultDataSourceName: 'dicomweb', /* Dynamic config allows user to pass "configUrl" query string this allows to load config without recompiling application. The regex will ensure valid configuration source */ // dangerouslyUseDynamicConfig: { From 9abbc6cefc55a44d547a3c8159f3e4d10c26bc0b Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Mon, 27 May 2024 15:30:32 -0400 Subject: [PATCH 05/23] Fix load with multimonitor not configured --- platform/core/src/services/MultiMonitorService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/core/src/services/MultiMonitorService.ts b/platform/core/src/services/MultiMonitorService.ts index 8c0540e97e7..a01edd61ee1 100644 --- a/platform/core/src/services/MultiMonitorService.ts +++ b/platform/core/src/services/MultiMonitorService.ts @@ -33,7 +33,7 @@ export class MultiMonitorService { if (!this.screenNumber) { this.launchWindows[0] = window; } - for (const windowsConfig of configuration || []) { + for (const windowsConfig of Array.isArray(configuration) ? configuration : []) { if (windowsConfig.test(testParams)) { this.isMultimonitor = true; this.numberOfScreens = windowsConfig.screens.length; From bcfed7e9fc202c4a041e8d704db8aa24ac9a9e0b Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Mon, 27 May 2024 15:44:55 -0400 Subject: [PATCH 06/23] Exclude the app-config from terser mucking so it is readable. --- .webpack/webpack.base.js | 1 + 1 file changed, 1 insertion(+) diff --git a/.webpack/webpack.base.js b/.webpack/webpack.base.js index 9d9bf6508ca..8e2bc356eeb 100644 --- a/.webpack/webpack.base.js +++ b/.webpack/webpack.base.js @@ -214,6 +214,7 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => { config.optimization.minimizer = [ new TerserJSPlugin({ parallel: true, + exclude: /app-config.js/, terserOptions: {}, }), ]; From ab789d7687a23c53a12fa6a4f2f1f9a16ea3fee3 Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Mon, 27 May 2024 19:07:25 -0400 Subject: [PATCH 07/23] fix tests --- platform/app/package.json | 1 - platform/core/src/index.test.js | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/app/package.json b/platform/app/package.json index e95b2da6d07..c6edfda7b92 100644 --- a/platform/app/package.json +++ b/platform/app/package.json @@ -1,5 +1,4 @@ { - "name": "@ohif/app", "version": "3.9.0-beta.22", "productVersion": "3.4.0", "description": "OHIF Viewer", diff --git a/platform/core/src/index.test.js b/platform/core/src/index.test.js index 7649a87026c..56b45d488f3 100644 --- a/platform/core/src/index.test.js +++ b/platform/core/src/index.test.js @@ -34,6 +34,7 @@ describe('Top level exports', () => { 'UIViewportDialogService', 'DisplaySetService', 'MeasurementService', + 'MultiMonitorService', 'ToolbarService', 'Types', 'ViewportGridService', From 15d5744d6d010e81169e85123769164d7e4d7db4 Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Mon, 27 May 2024 19:20:56 -0400 Subject: [PATCH 08/23] Undid accidental removal of name in package.json --- platform/app/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/platform/app/package.json b/platform/app/package.json index c6edfda7b92..e95b2da6d07 100644 --- a/platform/app/package.json +++ b/platform/app/package.json @@ -1,4 +1,5 @@ { + "name": "@ohif/app", "version": "3.9.0-beta.22", "productVersion": "3.4.0", "description": "OHIF Viewer", From 7052f31b7182aa4693214ea884c07b195038ef0b Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Mon, 27 May 2024 20:17:55 -0400 Subject: [PATCH 09/23] Remove console logs --- platform/core/src/services/MultiMonitorService.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/platform/core/src/services/MultiMonitorService.ts b/platform/core/src/services/MultiMonitorService.ts index a01edd61ee1..0f164549620 100644 --- a/platform/core/src/services/MultiMonitorService.ts +++ b/platform/core/src/services/MultiMonitorService.ts @@ -14,7 +14,6 @@ export class MultiMonitorService { name: 'multiMonitorService', create: ({ configuration }): MultiMonitorService => { const service = new MultiMonitorService(configuration); - console.log('global multimonitor setup', (window as any).multimonitor); return service; }, }; @@ -101,12 +100,10 @@ export class MultiMonitorService { const screenInfo = this.windowsConfig.screens[screenNumber]; if (!this.launchWindows[screenNumber] || this.launchWindows[screenNumber].closed) { const screenDetails = await window.getScreenDetails?.(); - console.log('screenDetails=', screenDetails); const screen = (screenInfo.screen >= 0 && screenDetails.screens[screenInfo.screen]) || screenDetails.currentScreen || window.screen; - console.log('Chosen screen', screen); const { width = 1024, height = 1024, availLeft = 0, availTop = 0 } = screen || {}; const newScreen = this.windowsConfig.screens[screenNumber]; const { From e08ea7684f0385e040beb6f55053132674e0f462 Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Thu, 7 Nov 2024 10:11:58 -0500 Subject: [PATCH 10/23] docs --- .../services/data/MultiMonitorService.md | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/platform/docs/docs/platform/services/data/MultiMonitorService.md b/platform/docs/docs/platform/services/data/MultiMonitorService.md index 94d05a3a18b..ed63c7c247c 100644 --- a/platform/docs/docs/platform/services/data/MultiMonitorService.md +++ b/platform/docs/docs/platform/services/data/MultiMonitorService.md @@ -8,26 +8,25 @@ sidebar_label: Multi Monitor Service The multi-monitor service provides detection, launch and communication support for multiple monitors or windows/screens within a single monitor. +## GUI Behaviour +### Initial behaviour -## GUI Behaviour +On launching any mode into multi-monitor, apply the window resize to fill the +specified area on the specified monitor. -Double click study in study browser => set as active study for current screen and apply hanging protocol as -active study +On clicking view in second monitor, apply the default hanging protocol to that +display, and LINK the monitors rather than opening up entirely new monitors. + * Make sure M0 can launch M1 and vice versa, and that existing annotations are +preserved. -(later) -Right click study in study browser => brings up menu to select as prior, or apply specific HP +### Secondary behaviour -Two modes - - independent left/right windows - - launch studies on left/right windows - - each applies own hanging protocol on "active" study for that window +Add communications bus from Mi to all to communicate changes -(later) - - coordinated hanging protocol - - determined by hanging protocol - - coordinates mixed studies - - applies full hanging protocol, but sub-selects it for specific screen +Add sender/listener for: + * Viewed display sets + * Annotations -(later) -Single "launch" button for launching to other screen +Maybe add a simple "mode" with a worklist for a given patient that always launches +to secondary window (or maybe have selector for destination) to allow extending the launch. From 709c7c1fc70c920b81f83df2684b57cc52666dae Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Mon, 25 Nov 2024 19:29:51 -0500 Subject: [PATCH 11/23] fix: Launch using new multi-monitor setup --- .../PanelStudyBrowserTracking.tsx | 5 +++-- platform/core/src/services/MultiMonitorService.ts | 9 ++++++++- .../ui-next/src/components/StudyBrowser/StudyBrowser.tsx | 5 +++++ platform/ui-next/src/components/StudyItem/StudyItem.tsx | 6 +++++- 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx index c270ed76ff9..cee2c66bd73 100644 --- a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx @@ -368,8 +368,8 @@ function PanelStudyBrowserTracking({ const _launchMultiMonitor = multiMonitorService.numberOfScreens > 1 ? (studyInstanceUID, screenDelta) => { - multiMonitorService.launchStudy(studyInstanceUID, screenDelta); - } + multiMonitorService.launchStudy(studyInstanceUID, screenDelta); + } : null; // TODO: Should not fire this on "close" @@ -509,6 +509,7 @@ function PanelStudyBrowserTracking({ activeTabName={activeTabName} expandedStudyInstanceUIDs={expandedStudyInstanceUIDs} onClickStudy={_handleStudyClick} + onClickStudyInfo={_launchMultiMonitor} onClickTab={clickedTabName => { setActiveTabName(clickedTabName); }} diff --git a/platform/core/src/services/MultiMonitorService.ts b/platform/core/src/services/MultiMonitorService.ts index 0f164549620..8483142399e 100644 --- a/platform/core/src/services/MultiMonitorService.ts +++ b/platform/core/src/services/MultiMonitorService.ts @@ -24,6 +24,12 @@ export class MultiMonitorService { const multimonitor = params.get('multimonitor'); const testParams = { params, screenNumber, multimonitor }; this.screenNumber = screenNumber ? Number(screenNumber) : 0; + console.log( + '************* multimonitor', + multimonitor, + screenNumber, + (window as any).multimonitor + ); (window as any).multimonitor ||= { setLaunchWindows: this.setLaunchWindows, launchWindows: this.launchWindows, @@ -50,7 +56,7 @@ export class MultiMonitorService { } /** - * Calls append ont he query with the multimonitor mode as appropriate. + * Calls append to the query the multimonitor mode as appropriate. */ public appendQuery(query) { if (!this.isMultimonitor) { @@ -70,6 +76,7 @@ export class MultiMonitorService { public async launchStudy(studyUid: string, screenDelta = 1) { const forScreen = (this.screenNumber + screenDelta) % this.numberOfScreens; + console.log('*************** launch Study', studyUid, forScreen); const url = this.createUrlForStudy(studyUid, forScreen); const forWindow = await this.getOrCreateWindow(forScreen, url); forWindow.location = url; diff --git a/platform/ui-next/src/components/StudyBrowser/StudyBrowser.tsx b/platform/ui-next/src/components/StudyBrowser/StudyBrowser.tsx index 99ef692bb0c..d7c394463c2 100644 --- a/platform/ui-next/src/components/StudyBrowser/StudyBrowser.tsx +++ b/platform/ui-next/src/components/StudyBrowser/StudyBrowser.tsx @@ -24,6 +24,7 @@ const StudyBrowser = ({ expandedStudyInstanceUIDs, onClickTab = noop, onClickStudy = noop, + onClickStudyInfo = onClickStudy, onClickThumbnail = noop, onDoubleClickThumbnail = noop, onClickUntrack = noop, @@ -55,6 +56,10 @@ const StudyBrowser = ({ onClick={() => { onClickStudy(studyInstanceUid); }} + onClickStudyInfo={e => { + onClickStudyInfo(studyInstanceUid); + e.stopPropagation(); + }} onClickThumbnail={onClickThumbnail} onDoubleClickThumbnail={onDoubleClickThumbnail} onClickUntrack={onClickUntrack} diff --git a/platform/ui-next/src/components/StudyItem/StudyItem.tsx b/platform/ui-next/src/components/StudyItem/StudyItem.tsx index cd878cf01bd..c5dda800f0b 100644 --- a/platform/ui-next/src/components/StudyItem/StudyItem.tsx +++ b/platform/ui-next/src/components/StudyItem/StudyItem.tsx @@ -12,6 +12,7 @@ const StudyItem = ({ modalities, isActive, onClick, + onClickStudyInfo = onClick, isExpanded, displaySets, activeDisplaySetInstanceUIDs, @@ -34,7 +35,10 @@ const StudyItem = ({
-
+
{date}
From 6f7e5bf7b0608656fbfc741523969d0f4dada337 Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Wed, 4 Dec 2024 12:57:32 -0500 Subject: [PATCH 12/23] fix: Allow commandsManager commands to run on other window --- extensions/default/src/commandsModule.ts | 19 +++++++ .../default/src/hangingprotocols/hpMNGrid.ts | 18 ------- .../PanelStudyBrowserTracking.tsx | 22 ++++++--- .../core/src/services/MultiMonitorService.ts | 49 ++++++++++++++----- 4 files changed, 71 insertions(+), 37 deletions(-) diff --git a/extensions/default/src/commandsModule.ts b/extensions/default/src/commandsModule.ts index 8916911af3a..7d55a4df624 100644 --- a/extensions/default/src/commandsModule.ts +++ b/extensions/default/src/commandsModule.ts @@ -41,12 +41,28 @@ 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 { commands, screenDelta, studyInstanceUID } = options; + if (multiMonitorService.numberOfScreens < 2) { + return options.fallback?.(options); + } + + await multiMonitorService.launchWindow(studyInstanceUID, screenDelta, options); + if (commands) { + multiMonitorService.run(screenDelta, commands, options); + } + }, + /** * Show the context menu. * @param options.menuId defines the menu name to lookup, from customizationService @@ -562,6 +578,9 @@ const commandsModule = ({ }; const definitions = { + multimonitor: { + commandFn: actions.multimonitor, + }, showContextMenu: { commandFn: actions.showContextMenu, }, diff --git a/extensions/default/src/hangingprotocols/hpMNGrid.ts b/extensions/default/src/hangingprotocols/hpMNGrid.ts index 330cc7f8126..b7aac2b3c8d 100644 --- a/extensions/default/src/hangingprotocols/hpMNGrid.ts +++ b/extensions/default/src/hangingprotocols/hpMNGrid.ts @@ -58,24 +58,6 @@ const hpMN: Types.HangingProtocol.Protocol = { ], }, }, - screens: [ - // This hanging protocol will match screens primary and secondary - { - matches: ['primary', 'secondary'], - primary: { - x: 0.5, - y: 0, - width: 0.5, - height: 1, - }, - secondary: { - x: 0.5, - y: 0, - width: 0.5, - height: 1, - }, - }, - ], defaultViewport: { viewportOptions: { viewportType: 'stack', diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx index 24121a141eb..b1a2dc41706 100644 --- a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx @@ -375,13 +375,6 @@ export default function PanelStudyBrowserTracking({ const tabs = createStudyBrowserTabs(StudyInstanceUIDs, studyDisplayList, displaySets); - const _launchMultiMonitor = - multiMonitorService.numberOfScreens > 1 - ? (studyInstanceUID, screenDelta) => { - multiMonitorService.launchStudy(studyInstanceUID, screenDelta); - } - : null; - // TODO: Should not fire this on "close" function _handleStudyClick(StudyInstanceUID) { const shouldCollapseStudy = expandedStudyInstanceUIDs.includes(StudyInstanceUID); @@ -397,6 +390,21 @@ export default function PanelStudyBrowserTracking({ } } + const _launchMultiMonitor = (studyInstanceUID, screenDelta) => { + commandsManager.run('multimonitor', { + studyInstanceUID, + screenDelta, + activeStudyUID: studyInstanceUID, + fallback: () => _handleStudyClick(studyInstanceUID), + commands: { + commandName: 'setHangingProtocol', + commandOptions: { + protocolId: 'default', + }, + }, + }); + }; + useEffect(() => { if (jumpToDisplaySet) { // Get element by displaySetInstanceUID diff --git a/platform/core/src/services/MultiMonitorService.ts b/platform/core/src/services/MultiMonitorService.ts index 8483142399e..e45ce0785b7 100644 --- a/platform/core/src/services/MultiMonitorService.ts +++ b/platform/core/src/services/MultiMonitorService.ts @@ -6,38 +6,38 @@ export class MultiMonitorService { private windowsConfig; private screenConfig; private launchWindows = []; + private commandsManager; public readonly screenNumber: number; public readonly isMultimonitor: boolean; public static REGISTRATION = { name: 'multiMonitorService', - create: ({ configuration }): MultiMonitorService => { - const service = new MultiMonitorService(configuration); + create: ({ configuration, commandsManager }): MultiMonitorService => { + const service = new MultiMonitorService(configuration, commandsManager); return service; }, }; - constructor(configuration) { + constructor(configuration, commandsManager) { const params = new URLSearchParams(window.location.search); const screenNumber = params.get('screenNumber'); const multimonitor = params.get('multimonitor'); const testParams = { params, screenNumber, multimonitor }; this.screenNumber = screenNumber ? Number(screenNumber) : 0; - console.log( - '************* multimonitor', - multimonitor, - screenNumber, - (window as any).multimonitor - ); - (window as any).multimonitor ||= { + this.commandsManager = commandsManager; + const windowAny = window as any; + windowAny.multimonitor ||= { setLaunchWindows: this.setLaunchWindows, launchWindows: this.launchWindows, + commandsManager, }; + windowAny.multimonitor.commandsManager = commandsManager; this.launchWindows = (window as any).multimonitor?.launchWindows || this.launchWindows; if (!this.screenNumber) { this.launchWindows[0] = window; } + windowAny.commandsManager = (...args) => configuration.commandsManager; for (const windowsConfig of Array.isArray(configuration) ? configuration : []) { if (windowsConfig.test(testParams)) { this.isMultimonitor = true; @@ -55,6 +55,20 @@ export class MultiMonitorService { this.isMultimonitor = false; } + public run(screenDelta = 1, commands, options) { + const screenNumber = (this.screenNumber + (screenDelta ?? 1)) % this.numberOfScreens; + const otherWindow = this.getWindow(screenNumber); + if (!otherWindow) { + console.warn('No multimonitor found for screen', screenNumber, commands); + return; + } + if (!otherWindow.multimonitor?.commandsManager) { + console.warn("Didn't find a commands manager to run in the other window", otherWindow); + return; + } + otherWindow.multimonitor.commandsManager.run(commands, options); + } + /** * Calls append to the query the multimonitor mode as appropriate. */ @@ -74,9 +88,11 @@ export class MultiMonitorService { (window as any).multimonitor.launchWindows = launchWindows; }; - public async launchStudy(studyUid: string, screenDelta = 1) { + public async launchWindow(studyUid: string, screenDelta = 1) { const forScreen = (this.screenNumber + screenDelta) % this.numberOfScreens; - console.log('*************** launch Study', studyUid, forScreen); + if (this.getWindow(forScreen)) { + return; + } const url = this.createUrlForStudy(studyUid, forScreen); const forWindow = await this.getOrCreateWindow(forScreen, url); forWindow.location = url; @@ -96,6 +112,15 @@ export class MultiMonitorService { return `${origin}${pathname}?StudyInstanceUIDs=${studyUid}&multimonitor=${this.windowsConfig.id}&screenNumber=${screenNumber}`; } + public getWindow(screenNumber) { + if (screenNumber === this.screenNumber) { + return window; + } + if (this.launchWindows[screenNumber] && !this.launchWindows[screenNumber].closed) { + return this.launchWindows[screenNumber]; + } + } + /** * Creates a new window showing the given url by default, or gets an existing * window. From 652b3e611f6931ce1aa356078425118409d9fb1d Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Wed, 4 Dec 2024 18:54:42 -0500 Subject: [PATCH 13/23] fix: Preserve annotations by allowing commands to be run in other window Means that the study change doesn't have to occur by updating the URL, which clears all the internal data. --- .../src/Panels/WrappedPanelStudyBrowser.tsx | 4 +- .../requestDisplaySetCreationForStudy.js | 2 +- extensions/default/src/commandsModule.ts | 41 +++++++++++++- .../PanelStudyBrowserTracking.tsx | 20 ++++--- platform/core/src/classes/CommandsManager.ts | 54 +++++++++++++++++++ .../HangingProtocolService.ts | 17 +++++- .../core/src/services/MultiMonitorService.ts | 2 +- 7 files changed, 127 insertions(+), 13 deletions(-) diff --git a/extensions/default/src/Panels/WrappedPanelStudyBrowser.tsx b/extensions/default/src/Panels/WrappedPanelStudyBrowser.tsx index 80480f18114..f3ab52b5f03 100644 --- a/extensions/default/src/Panels/WrappedPanelStudyBrowser.tsx +++ b/extensions/default/src/Panels/WrappedPanelStudyBrowser.tsx @@ -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), diff --git a/extensions/default/src/Panels/requestDisplaySetCreationForStudy.js b/extensions/default/src/Panels/requestDisplaySetCreationForStudy.js index c10a5d97b2c..9d13180040e 100644 --- a/extensions/default/src/Panels/requestDisplaySetCreationForStudy.js +++ b/extensions/default/src/Panels/requestDisplaySetCreationForStudy.js @@ -13,7 +13,7 @@ function requestDisplaySetCreationForStudy( return; } - dataSource.retrieve.series.metadata({ StudyInstanceUID, madeInClient }); + return dataSource.retrieve.series.metadata({ StudyInstanceUID, madeInClient }); } export default requestDisplaySetCreationForStudy; diff --git a/extensions/default/src/commandsModule.ts b/extensions/default/src/commandsModule.ts index 7d55a4df624..75083644e26 100644 --- a/extensions/default/src/commandsModule.ts +++ b/extensions/default/src/commandsModule.ts @@ -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'; @@ -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; @@ -25,6 +26,26 @@ export type HangingProtocolParams = { reset?: false; }; +/** + * The studies from display sets gets the studies in study date + * order or in study instance UID order - not very useful, but + * if not specifically specified then at least making it consistent is useful. + */ +const getStudiesfromDisplaySets = displaySets => { + const studyMap = {}; + + const ret = displaySets.reduce((prev, curr) => { + const { StudyInstanceUID } = curr; + if (!studyMap[StudyInstanceUID]) { + const study = DicomMetadataStore.getStudy(StudyInstanceUID); + studyMap[StudyInstanceUID] = study; + prev.push(study); + } + return prev; + }, []); + return ret; +}; + export type UpdateViewportDisplaySetParams = { direction: number; excludeNonImageModalities?: boolean; @@ -33,6 +54,7 @@ export type UpdateViewportDisplaySetParams = { const commandsModule = ({ servicesManager, commandsManager, + extensionManager, }: Types.Extensions.ExtensionParams): Types.Extensions.CommandsModule => { const { customizationService, @@ -63,6 +85,20 @@ const commandsModule = ({ } }, + /** Ensures that the specified study is available for display */ + loadStudy: async options => { + const { studyInstanceUID } = options; + if (hangingProtocolService.hasStudyUID(studyInstanceUID)) { + return; + } + const [dataSource] = extensionManager.getActiveDataSource(); + await requestDisplaySetCreationForStudy(dataSource, displaySetService, studyInstanceUID); + const activeStudy = DicomMetadataStore.getStudy(studyInstanceUID); + hangingProtocolService.addStudy(activeStudy); + const displaySets = displaySetService.getActiveDisplaySets(); + hangingProtocolService.setDisplaySets(displaySets); + }, + /** * Show the context menu. * @param options.menuId defines the menu name to lookup, from customizationService @@ -581,6 +617,9 @@ const commandsModule = ({ multimonitor: { commandFn: actions.multimonitor, }, + loadStudy: { + commandFn: actions.loadStudy, + }, showContextMenu: { commandFn: actions.showContextMenu, }, diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx index b1a2dc41706..c791e443fab 100644 --- a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx @@ -9,7 +9,6 @@ import { StudyBrowser } from '@ohif/ui-next'; import { useTrackedMeasurements } from '../../getContextModule'; import { Separator } from '@ohif/ui-next'; import { PanelStudyBrowserHeader } from '@ohif/extension-default'; -import { useAppConfig } from '@state'; import { defaultActionIcons, defaultViewPresets } from './constants'; const { formatDate, createStudyBrowserTabs } = utils; @@ -44,7 +43,6 @@ export default function PanelStudyBrowserTracking({ measurementService, studyPrefetcherService, customizationService, - multiMonitorService, } = servicesManager.services; const navigate = useNavigate(); const { mode: studyMode } = customizationService.getCustomization('PanelStudyBrowser.studyMode', { @@ -396,12 +394,20 @@ export default function PanelStudyBrowserTracking({ screenDelta, activeStudyUID: studyInstanceUID, fallback: () => _handleStudyClick(studyInstanceUID), - commands: { - commandName: 'setHangingProtocol', - commandOptions: { - protocolId: 'default', + commands: [ + { + commandName: 'loadStudy', + commandOptions: { + protocolId: '@ohif/mnGrid', + }, }, - }, + { + commandName: 'setHangingProtocol', + commandOptions: { + protocolId: '@ohif/mnGrid', + }, + }, + ], }); }; diff --git a/platform/core/src/classes/CommandsManager.ts b/platform/core/src/classes/CommandsManager.ts index 0138521f90a..f34c54c5d7c 100644 --- a/platform/core/src/classes/CommandsManager.ts +++ b/platform/core/src/classes/CommandsManager.ts @@ -220,6 +220,60 @@ export class CommandsManager { return result; } + + /** Like run, but await each command before continuing */ + public async runAsync( + toRun: Command | Commands | Command[] | string | undefined, + options?: Record + ): unknown { + if (!toRun) { + return; + } + + // Normalize `toRun` to an array of `ComplexCommand` + let commands: ComplexCommand[] = []; + if (typeof toRun === 'string') { + commands = [{ commandName: toRun }]; + } else if ('commandName' in toRun) { + commands = [toRun as ComplexCommand]; + } else if ('commands' in toRun) { + const commandsInput = (toRun as Commands).commands; + commands = Array.isArray(commandsInput) + ? commandsInput.map(cmd => (typeof cmd === 'string' ? { commandName: cmd } : cmd)) + : [{ commandName: commandsInput }]; + } else if (Array.isArray(toRun)) { + commands = toRun.map(cmd => (typeof cmd === 'string' ? { commandName: cmd } : cmd)); + } + + if (commands.length === 0) { + console.log("Command isn't runnable", toRun); + return; + } + + // Execute each command in the array + let result: unknown; + for (const command of commands) { + const { commandName, commandOptions, context } = command; + if (commandName) { + result = await this.runCommand( + commandName, + { + ...commandOptions, + ...options, + }, + context + ); + } else { + if (typeof command === 'function') { + result = await command(); + } else { + console.warn('No command name supplied in', toRun); + } + } + } + + return result; + } } export default CommandsManager; diff --git a/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts b/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts index 10bc77344ab..eacc2456a5d 100644 --- a/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts +++ b/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts @@ -372,8 +372,23 @@ export default class HangingProtocolService extends PubSubService { * for example, a prior view hanging protocol will NOT show the active study * specifically, but will show another study instead. */ - public setActiveStudyUID(activeStudyUID: string): void { + public setActiveStudyUID(activeStudyUID: string) { this.activeStudy = this.studies.find(it => it.StudyInstanceUID === activeStudyUID); + return this.activeStudy; + } + + public hasStudyUID(studyUID: string): boolean { + return this.studies.some(it => it.StudyInstanceUID === studyUID); + } + + public addStudy(study) { + if (!this.hasStudyUID(study.StudyInstanceUID)) { + this.studies.push(study); + } + } + + public setDisplaySets(displaySets) { + this.displaySets = displaySets; } /** diff --git a/platform/core/src/services/MultiMonitorService.ts b/platform/core/src/services/MultiMonitorService.ts index e45ce0785b7..591b92f27e6 100644 --- a/platform/core/src/services/MultiMonitorService.ts +++ b/platform/core/src/services/MultiMonitorService.ts @@ -66,7 +66,7 @@ export class MultiMonitorService { console.warn("Didn't find a commands manager to run in the other window", otherWindow); return; } - otherWindow.multimonitor.commandsManager.run(commands, options); + otherWindow.multimonitor.commandsManager.runAsync(commands, options); } /** From d7aec6106dbb7287e9d7b07f5ecda1e12d15dd89 Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Wed, 11 Dec 2024 20:57:47 -0500 Subject: [PATCH 14/23] fix: Preserve multimonitor configuration --- .../default/src/ViewerLayout/ViewerHeader.tsx | 8 ++---- platform/app/src/index.js | 5 ++-- platform/app/src/routes/WorkList/WorkList.tsx | 7 +++-- .../app/src/utils/preserveQueryParameters.ts | 26 +++++++++++++++++++ 4 files changed, 35 insertions(+), 11 deletions(-) create mode 100644 platform/app/src/utils/preserveQueryParameters.ts diff --git a/extensions/default/src/ViewerLayout/ViewerHeader.tsx b/extensions/default/src/ViewerLayout/ViewerHeader.tsx index dec8e1de056..05670bb1caa 100644 --- a/extensions/default/src/ViewerLayout/ViewerHeader.tsx +++ b/extensions/default/src/ViewerLayout/ViewerHeader.tsx @@ -10,6 +10,7 @@ import { hotkeys } from '@ohif/core'; import { Toolbar } from '../Toolbar/Toolbar'; import HeaderPatientInfo from './HeaderPatientInfo'; import { PatientInfoVisibility } from './HeaderPatientInfo/HeaderPatientInfo'; +import { preserveQueryParameters } from '@ohif/app'; const { availableLanguages, defaultLanguage, currentLanguage } = i18n; @@ -25,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); @@ -35,10 +34,7 @@ function ViewerHeader({ if (dataSourceIdx !== -1 && existingDataSource) { searchQuery.append('datasources', pathname.substring(dataSourceIdx + 1)); } - servicesManager.services.multiMonitorService.appendQuery(searchQuery); - if (configUrl) { - searchQuery.append('configUrl', configUrl); - } + preserveQueryParameters(searchQuery); navigate({ pathname: '/', diff --git a/platform/app/src/index.js b/platform/app/src/index.js index 20b4cac7c45..7b2ff2d0907 100644 --- a/platform/app/src/index.js +++ b/platform/app/src/index.js @@ -5,7 +5,8 @@ import 'regenerator-runtime/runtime'; import { createRoot } from 'react-dom/client'; import App from './App'; import React from 'react'; -import { history } from './utils/history'; +export { history } from './utils/history'; +export { preserveQueryParameters, preserveQueryStrings } from './utils/preserveQueryParameters'; /** * EXTENSIONS AND MODES @@ -40,5 +41,3 @@ loadDynamicConfig(window.config).then(config_json => { const root = createRoot(container); root.render(React.createElement(App, appProps)); }); - -export { history }; diff --git a/platform/app/src/routes/WorkList/WorkList.tsx b/platform/app/src/routes/WorkList/WorkList.tsx index ab0807529e9..37d88ed60e4 100644 --- a/platform/app/src/routes/WorkList/WorkList.tsx +++ b/platform/app/src/routes/WorkList/WorkList.tsx @@ -36,6 +36,7 @@ import { Types } from '@ohif/ui'; import i18n from '@ohif/i18n'; import { Onboarding, ScrollArea } from '@ohif/ui-next'; +import { preserveQueryParameters, preserveQueryStrings } from '../../utils/preserveQueryParameters'; const PatientInfoVisibility = Types.PatientInfoVisibility; @@ -199,11 +200,12 @@ function WorkList({ } }); + preserveQueryStrings(queryString); + const search = qs.stringify(queryString, { skipNull: true, skipEmptyString: true, }); - navigate({ pathname: '/', search: search ? `?${search}` : undefined, @@ -392,6 +394,8 @@ function WorkList({ query.append('configUrl', filterValues.configUrl); } query.append('StudyInstanceUIDs', studyInstanceUid); + preserveQueryParameters(query); + return ( mode.displayName && ( Date: Wed, 11 Dec 2024 21:26:00 -0500 Subject: [PATCH 15/23] Fix paths when publicUrl is set --- extensions/default/src/ViewerLayout/ViewerHeader.tsx | 4 ++-- platform/app/public/html-templates/index.html | 6 +++--- platform/app/src/index.js | 1 + platform/app/src/routes/WorkList/WorkList.tsx | 3 ++- platform/app/src/routes/buildModeRoutes.tsx | 7 +++++-- platform/app/src/routes/index.tsx | 12 +++++++----- platform/app/src/utils/publicUrl.ts | 4 ++++ 7 files changed, 24 insertions(+), 13 deletions(-) create mode 100644 platform/app/src/utils/publicUrl.ts diff --git a/extensions/default/src/ViewerLayout/ViewerHeader.tsx b/extensions/default/src/ViewerLayout/ViewerHeader.tsx index 05670bb1caa..171ac2c0199 100644 --- a/extensions/default/src/ViewerLayout/ViewerHeader.tsx +++ b/extensions/default/src/ViewerLayout/ViewerHeader.tsx @@ -10,7 +10,7 @@ import { hotkeys } from '@ohif/core'; import { Toolbar } from '../Toolbar/Toolbar'; import HeaderPatientInfo from './HeaderPatientInfo'; import { PatientInfoVisibility } from './HeaderPatientInfo/HeaderPatientInfo'; -import { preserveQueryParameters } from '@ohif/app'; +import { preserveQueryParameters, publicUrl } from '@ohif/app'; const { availableLanguages, defaultLanguage, currentLanguage } = i18n; @@ -37,7 +37,7 @@ function ViewerHeader({ preserveQueryParameters(searchQuery); navigate({ - pathname: '/', + pathname: publicUrl, search: decodeURIComponent(searchQuery.toString()), }); }; diff --git a/platform/app/public/html-templates/index.html b/platform/app/public/html-templates/index.html index 25e718e79b1..225a00697c4 100644 --- a/platform/app/public/html-templates/index.html +++ b/platform/app/public/html-templates/index.html @@ -214,8 +214,9 @@ function browserImportFunction(moduleId) { return import(moduleId); } - + window.PUBLIC_URL = '<%= PUBLIC_URL %>'; +