feat(multimonitor): Add simple multi-monitor support to open another study(#4178)
This commit is contained in:
1 parent
a4a6de07f4
commit
07c628e689
49 files changed
+1476
-367
No files matched your search
@@ -1,6 +1,8 @@
|
||||
import log from '../log.js';
|
||||
import { Command, Commands, ComplexCommand } from '../types/Command';
|
||||
|
||||
export type RunInput = Command | Commands | Command[] | string | undefined;
|
||||
|
||||
/**
|
||||
* The definition of a command
|
||||
*
|
||||
@@ -157,6 +159,44 @@ export class CommandsManager {
|
||||
}
|
||||
}
|
||||
|
||||
public static convertCommands(toRun: Command | Commands | Command[] | string) {
|
||||
if (typeof toRun === 'string') {
|
||||
return [{ commandName: toRun }];
|
||||
}
|
||||
if ('commandName' in toRun) {
|
||||
return [toRun as ComplexCommand];
|
||||
}
|
||||
if ('commands' in toRun) {
|
||||
const commandsInput = (toRun as Commands).commands;
|
||||
return this.convertCommands(commandsInput);
|
||||
}
|
||||
if (Array.isArray(toRun)) {
|
||||
return toRun.map(command => CommandsManager.convertCommands(command)[0]);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private validate(input: RunInput, options: Record<string, unknown> = {}): ComplexCommand[] {
|
||||
if (!input) {
|
||||
console.debug('No command to run');
|
||||
return [];
|
||||
}
|
||||
|
||||
// convert commands
|
||||
const converted: ComplexCommand[] = CommandsManager.convertCommands(input);
|
||||
if (!converted.length) {
|
||||
console.debug('Command is not runnable', input);
|
||||
return [];
|
||||
}
|
||||
|
||||
return converted.map(command => ({
|
||||
commandName: command.commandName,
|
||||
commandOptions: { ...options, ...command.commandOptions },
|
||||
context: command.context,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Run one or more commands with specified extra options.
|
||||
* Returns the result of the last command run.
|
||||
@@ -178,57 +218,34 @@ export class CommandsManager {
|
||||
* @param options - to include in the commands run beyond
|
||||
* the commandOptions specified in the base.
|
||||
*/
|
||||
public run(
|
||||
toRun: Command | Commands | (Command | string)[] | string | undefined,
|
||||
options?: Record<string, unknown>
|
||||
): unknown {
|
||||
if (!toRun) {
|
||||
return;
|
||||
}
|
||||
public run(input: RunInput, options: Record<string, unknown> = {}): unknown[] {
|
||||
const commands = this.validate(input, options);
|
||||
|
||||
// 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;
|
||||
commands.forEach(command => {
|
||||
const results: unknown[] = [];
|
||||
for (let i = 0; i < commands.length; i++) {
|
||||
const command = commands[i];
|
||||
const { commandName, commandOptions, context } = command;
|
||||
if (commandName) {
|
||||
result = this.runCommand(
|
||||
commandName,
|
||||
{
|
||||
...commandOptions,
|
||||
...options,
|
||||
},
|
||||
context
|
||||
);
|
||||
} else {
|
||||
if (typeof command === 'function') {
|
||||
result = command();
|
||||
} else {
|
||||
console.warn('No command name supplied in', toRun);
|
||||
}
|
||||
}
|
||||
});
|
||||
results.push(this.runCommand(commandName, commandOptions, context));
|
||||
}
|
||||
|
||||
return result;
|
||||
return results;
|
||||
}
|
||||
|
||||
/** Like run, but await each command before continuing */
|
||||
public async runAsync(
|
||||
input: RunInput,
|
||||
options: Record<string, unknown> = {}
|
||||
): Promise<unknown[]> {
|
||||
const commands = this.validate(input, options);
|
||||
|
||||
const results: unknown[] = [];
|
||||
for (let i = 0; i < commands.length; i++) {
|
||||
const command = commands[i];
|
||||
const { commandName, commandOptions, context } = command;
|
||||
results.push(await this.runCommand(commandName, commandOptions, context));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
PanelService,
|
||||
WorkflowStepsService,
|
||||
StudyPrefetcherService,
|
||||
MultiMonitorService,
|
||||
} from './services';
|
||||
|
||||
import { DisplaySetMessage, DisplaySetMessageList } from './services/DisplaySetService';
|
||||
@@ -78,6 +79,7 @@ const OHIF = {
|
||||
ViewportGridService,
|
||||
HangingProtocolService,
|
||||
UserAuthenticationService,
|
||||
MultiMonitorService,
|
||||
IWebApiDataSource,
|
||||
DicomMetadataStore,
|
||||
pubSubServiceInterface,
|
||||
@@ -119,6 +121,7 @@ export {
|
||||
DisplaySetMessage,
|
||||
DisplaySetMessageList,
|
||||
MeasurementService,
|
||||
MultiMonitorService,
|
||||
ToolbarService,
|
||||
ViewportGridService,
|
||||
HangingProtocolService,
|
||||
|
||||
@@ -372,8 +372,22 @@ 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) {
|
||||
if (!activeStudyUID || activeStudyUID === this.activeStudy?.StudyInstanceUID) {
|
||||
return;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -396,17 +410,22 @@ export default class HangingProtocolService extends PubSubService {
|
||||
public run({ studies, displaySets, activeStudy }, protocolId, options = {}) {
|
||||
this.studies = [...(studies || this.studies)];
|
||||
this.displaySets = displaySets;
|
||||
this.setActiveStudyUID((activeStudy || studies[0])?.StudyInstanceUID);
|
||||
this.setActiveStudyUID(
|
||||
activeStudy?.StudyInstanceUID || (activeStudy || this.studies[0])?.StudyInstanceUID
|
||||
);
|
||||
|
||||
this.protocolEngine = new ProtocolEngine(
|
||||
this.getProtocols(),
|
||||
this.customAttributeRetrievalCallbacks
|
||||
);
|
||||
|
||||
// Resets the full protocol status here.
|
||||
this.protocol = null;
|
||||
|
||||
if (protocolId && typeof protocolId === 'string') {
|
||||
const protocol = this.getProtocolById(protocolId);
|
||||
this._setProtocol(protocol, options);
|
||||
}else {
|
||||
} else {
|
||||
const matchedProtocol = this.protocolEngine.run({
|
||||
studies: this.studies,
|
||||
activeStudy,
|
||||
@@ -1201,6 +1220,7 @@ export default class HangingProtocolService extends PubSubService {
|
||||
viewportMatchDetails: Map<string, HangingProtocol.ViewportMatchDetails>;
|
||||
displaySetMatchDetails: Map<string, HangingProtocol.DisplaySetMatchDetails>;
|
||||
} {
|
||||
this.activeStudy ||= this.studies[0];
|
||||
let matchedViewports = 0;
|
||||
stageModel.viewports.forEach(viewport => {
|
||||
const viewportId = viewport.viewportOptions.viewportId;
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* This service manages multiple monitors or windows.
|
||||
*/
|
||||
export class MultiMonitorService {
|
||||
public readonly numberOfScreens: number;
|
||||
private windowsConfig;
|
||||
private screenConfig;
|
||||
private launchWindows = [];
|
||||
private basePath: string;
|
||||
|
||||
public readonly screenNumber: number;
|
||||
public readonly isMultimonitor: boolean;
|
||||
|
||||
public static readonly SOURCE_SCREEN = {
|
||||
id: 'source',
|
||||
// This is the primary screen, so don't launch is separately, but use primary
|
||||
launch: 'source',
|
||||
screen: null,
|
||||
location: {
|
||||
screen: null,
|
||||
width: 1,
|
||||
height: 1,
|
||||
left: 0,
|
||||
top: 0,
|
||||
},
|
||||
};
|
||||
|
||||
public static REGISTRATION = {
|
||||
name: 'multiMonitorService',
|
||||
create: ({ configuration, commandsManager }): MultiMonitorService => {
|
||||
const service = new MultiMonitorService(configuration, commandsManager);
|
||||
return service;
|
||||
},
|
||||
};
|
||||
|
||||
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) : -1;
|
||||
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 !== -1) {
|
||||
this.launchWindows[this.screenNumber] = window;
|
||||
}
|
||||
windowAny.commandsManager = (...args) => configuration.commandsManager;
|
||||
for (const windowsConfig of Array.isArray(configuration) ? configuration : []) {
|
||||
if (windowsConfig.test(testParams)) {
|
||||
this.isMultimonitor = true;
|
||||
this.numberOfScreens = windowsConfig.screens.length;
|
||||
this.windowsConfig = windowsConfig;
|
||||
if (this.screenNumber === -1 || this.screenNumber === null) {
|
||||
this.screenConfig = MultiMonitorService.SOURCE_SCREEN;
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
this.numberOfScreens = 1;
|
||||
this.isMultimonitor = false;
|
||||
}
|
||||
}
|
||||
|
||||
public async run(screenDelta = 1, commands, options) {
|
||||
const screenNumber = (this.screenNumber + (screenDelta ?? 1)) % this.numberOfScreens;
|
||||
const otherWindow = await 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.runAsync(commands, options);
|
||||
}
|
||||
|
||||
/** 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 launchWindow(studyUid: string, screenDelta = 1, hashParams = '') {
|
||||
const forScreen = (this.screenNumber + screenDelta) % this.numberOfScreens;
|
||||
return this.getWindow(forScreen, studyUid ? `StudyInstanceUIDs=${studyUid}${hashParams}` : '');
|
||||
}
|
||||
|
||||
public async getWindow(screenNumber, hashParam?: string) {
|
||||
if (screenNumber === this.screenNumber) {
|
||||
return window;
|
||||
}
|
||||
if (this.launchWindows[screenNumber] && !this.launchWindows[screenNumber].closed) {
|
||||
return this.launchWindows[screenNumber];
|
||||
}
|
||||
return await this.createWindow(screenNumber, hashParam);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new window showing the given url by default, or gets an existing
|
||||
* window.
|
||||
*/
|
||||
public async createWindow(screenNumber, urlToUse?: string) {
|
||||
if (screenNumber === this.screenNumber) {
|
||||
return window;
|
||||
}
|
||||
const screenInfo = this.windowsConfig.screens[screenNumber];
|
||||
const screenDetails = await window.getScreenDetails?.();
|
||||
const screen =
|
||||
(screenInfo.screen >= 0 && screenDetails.screens[screenInfo.screen]) ||
|
||||
screenDetails.currentScreen ||
|
||||
window.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 baseFinalUrl = `${this.basePath}&screenNumber=${screenNumber}`;
|
||||
const finalUrl = urlToUse ? `${baseFinalUrl}#${urlToUse}` : baseFinalUrl;
|
||||
|
||||
const newId = newScreen.id;
|
||||
const options = newScreen.options || '';
|
||||
const position = `screenX=${useLeft},screenY=${useTop},width=${useWidth},height=${useHeight},${options}`;
|
||||
|
||||
let newWindow = window.open('', newId, position);
|
||||
if (!newWindow?.location.href.startsWith(baseFinalUrl)) {
|
||||
newWindow = window.open(finalUrl, newId, position);
|
||||
}
|
||||
if (!newWindow) {
|
||||
console.warn('Unable to launch window', finalUrl, 'called', newId, 'at', position);
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for the window to fully load
|
||||
await new Promise<void>(resolve => {
|
||||
if (newWindow.document.readyState === 'complete') {
|
||||
resolve();
|
||||
} else {
|
||||
newWindow.addEventListener('load', () => resolve());
|
||||
}
|
||||
});
|
||||
|
||||
this.launchWindows[screenNumber] = newWindow;
|
||||
return newWindow;
|
||||
}
|
||||
|
||||
/** Launches all the windows using the initial configuration */
|
||||
public launchAll() {
|
||||
for (let i = 0; i < this.numberOfScreens; i++) {
|
||||
this.createWindow(i);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the base path to use for launching other windows, based on the
|
||||
* original base path without hash values in order to preserve consistent
|
||||
* URLs so that windows are refreshed on relaunch.
|
||||
*/
|
||||
public setBasePath() {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete('screenNumber');
|
||||
url.searchParams.delete('protocolId');
|
||||
url.searchParams.delete('launchAll');
|
||||
url.searchParams.set('multimonitor', url.searchParams.get('multimonitor') || 'split');
|
||||
url.hash = '';
|
||||
this.basePath = url.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Try moving the screen to the correct location - this will only work with
|
||||
* screens opened with openWindow containing no more than 1 tab.
|
||||
*/
|
||||
public async onModeEnter() {
|
||||
this.setBasePath();
|
||||
|
||||
if (
|
||||
(this.isMultimonitor && this.screenNumber === -1) ||
|
||||
window.location.href.toLowerCase().indexOf('launchall') !== -1
|
||||
) {
|
||||
this.launchAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
// TODO - remove this registration
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import CustomizationService from './CustomizationService';
|
||||
import PanelService from './PanelService';
|
||||
import WorkflowStepsService from './WorkflowStepsService';
|
||||
import StudyPrefetcherService from './StudyPrefetcherService';
|
||||
import { MultiMonitorService } from './MultiMonitorService';
|
||||
|
||||
import type Services from '../types/Services';
|
||||
|
||||
@@ -31,6 +32,7 @@ export {
|
||||
UINotificationService,
|
||||
UIViewportDialogService,
|
||||
DicomMetadataStore,
|
||||
MultiMonitorService,
|
||||
DisplaySetService,
|
||||
ToolbarService,
|
||||
ViewportGridService,
|
||||
|
||||
@@ -14,6 +14,7 @@ import PanelServiceType from '../services/PanelService';
|
||||
import UIDialogServiceType from '../services/UIDialogService';
|
||||
import UIViewportDialogServiceType from '../services/UIViewportDialogService';
|
||||
import StudyPrefetcherServiceType from '../services/StudyPrefetcherService';
|
||||
import type { MultiMonitorService } from '../services/MultiMonitorService';
|
||||
|
||||
import ServicesManagerType from '../services/ServicesManager';
|
||||
import CommandsManagerType from '../classes/CommandsManager';
|
||||
@@ -55,6 +56,7 @@ declare global {
|
||||
export type UIViewportDialogService = UIViewportDialogServiceType;
|
||||
export type PanelService = PanelServiceType;
|
||||
export type StudyPrefetcherService = StudyPrefetcherServiceType;
|
||||
export type MultiMonitorService;
|
||||
|
||||
export interface Managers {
|
||||
servicesManager?: ServicesManager;
|
||||
@@ -78,6 +80,7 @@ declare global {
|
||||
uiViewportDialogService?: UIViewportDialogServiceType;
|
||||
panelService?: PanelServiceType;
|
||||
studyPrefetcherService?: StudyPrefetcherServiceType;
|
||||
multiMonitorService?: MultiMonitorService;
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
PanelService,
|
||||
UIDialogService,
|
||||
UIViewportDialogService,
|
||||
MultiMonitorService,
|
||||
} from '../services';
|
||||
|
||||
/**
|
||||
@@ -34,6 +35,7 @@ interface Services {
|
||||
uiDialogService?: UIDialogService;
|
||||
uiViewportDialogService?: UIViewportDialogService;
|
||||
panelService?: PanelService;
|
||||
multiMonitorService?: MultiMonitorService;
|
||||
}
|
||||
|
||||
export default Services;
|
||||
Reference in new issue
Block a user