feat(multimonitor): Add simple multi-monitor support to open another study(#4178)
This commit is contained in:
parent
a4a6de07f4
commit
07c628e689
@ -1,15 +1,10 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { utils } from '@ohif/core';
|
import { utils } from '@ohif/core';
|
||||||
import { useViewportGrid } from '@ohif/ui-next';
|
|
||||||
import { MeasurementTable } from '@ohif/ui-next';
|
import { MeasurementTable } from '@ohif/ui-next';
|
||||||
import debounce from 'lodash.debounce';
|
import debounce from 'lodash.debounce';
|
||||||
import { useMeasurements } from '../hooks/useMeasurements';
|
import { useMeasurements } from '../hooks/useMeasurements';
|
||||||
|
|
||||||
const {
|
const { filterAdditionalFindings: filterAdditionalFinding, filterAny } = utils.MeasurementFilters;
|
||||||
filterAdditionalFindings: filterAdditionalFinding,
|
|
||||||
filterOr,
|
|
||||||
filterAny,
|
|
||||||
} = utils.MeasurementFilters;
|
|
||||||
|
|
||||||
export type withAppAndFilters = withAppTypes & {
|
export type withAppAndFilters = withAppTypes & {
|
||||||
measurementFilter: (item) => boolean;
|
measurementFilter: (item) => boolean;
|
||||||
|
|||||||
91
extensions/default/src/Components/MoreDropdownMenu.tsx
Normal file
91
extensions/default/src/Components/MoreDropdownMenu.tsx
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
Icons,
|
||||||
|
Button,
|
||||||
|
} from '@ohif/ui-next';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default sub-menu appearance and setup is defined here, but this can be
|
||||||
|
* replaced by
|
||||||
|
*/
|
||||||
|
const getMenuItemsDefault = ({ commandsManager, items, servicesManager, ...props }) => {
|
||||||
|
const { customizationService } = servicesManager.services;
|
||||||
|
|
||||||
|
// This allows replacing the default child item for menus, whereas the entire
|
||||||
|
// getMenuItems can also be replaced by providing it to the MoreDropdownMenu
|
||||||
|
const menuContent = customizationService.getCustomization('ohif.menuContent');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenuContent
|
||||||
|
hideWhenDetached
|
||||||
|
align="start"
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{items?.map(item =>
|
||||||
|
menuContent.content({
|
||||||
|
key: item.id,
|
||||||
|
item,
|
||||||
|
commandsManager,
|
||||||
|
servicesManager,
|
||||||
|
...props,
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The component provides a ... sub-menu for various components which appears
|
||||||
|
* on hover over the main component.
|
||||||
|
*
|
||||||
|
* @param bindProps - properties to define the sub-menu
|
||||||
|
* @returns Component bound to the bindProps
|
||||||
|
*/
|
||||||
|
export default function MoreDropdownMenu(bindProps) {
|
||||||
|
const {
|
||||||
|
menuItemsKey,
|
||||||
|
getMenuItems = getMenuItemsDefault,
|
||||||
|
commandsManager,
|
||||||
|
servicesManager,
|
||||||
|
} = bindProps;
|
||||||
|
const { customizationService } = servicesManager.services;
|
||||||
|
|
||||||
|
const items = customizationService.getCustomization(menuItemsKey)?.value;
|
||||||
|
|
||||||
|
if (!items) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function BoundMoreDropdownMenu(props) {
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="hidden group-hover:inline-flex data-[state=open]:inline-flex"
|
||||||
|
onClick={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icons.More />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
{getMenuItems({
|
||||||
|
...props,
|
||||||
|
commandsManager: commandsManager,
|
||||||
|
servicesManager: servicesManager,
|
||||||
|
items,
|
||||||
|
})}
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return BoundMoreDropdownMenu;
|
||||||
|
}
|
||||||
@ -47,9 +47,15 @@ export default class ContextMenuController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { event, subMenu, menuId, menus, selectorProps } = contextMenuProps;
|
const { event, subMenu, menuId, menus, selectorProps } = contextMenuProps;
|
||||||
|
if (!menus) {
|
||||||
|
console.warn('No menus found for', menuId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { locking, visibility } = CsAnnotation;
|
const { locking, visibility } = CsAnnotation;
|
||||||
const targetAnnotationId = selectorProps?.nearbyToolData?.annotationUID as string;
|
const targetAnnotationId = selectorProps?.nearbyToolData?.annotationUID as string;
|
||||||
|
|
||||||
|
if (targetAnnotationId) {
|
||||||
const isLocked = locking.isAnnotationLocked(targetAnnotationId);
|
const isLocked = locking.isAnnotationLocked(targetAnnotationId);
|
||||||
const isVisible = visibility.isAnnotationVisible(targetAnnotationId);
|
const isVisible = visibility.isAnnotationVisible(targetAnnotationId);
|
||||||
|
|
||||||
@ -57,6 +63,7 @@ export default class ContextMenuController {
|
|||||||
console.warn(`Annotation is ${isLocked ? 'locked' : 'not visible'}.`);
|
console.warn(`Annotation is ${isLocked ? 'locked' : 'not visible'}.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const items = ContextMenuItemsBuilder.getMenuItems(
|
const items = ContextMenuItemsBuilder.getMenuItems(
|
||||||
selectorProps || contextMenuProps,
|
selectorProps || contextMenuProps,
|
||||||
@ -73,7 +80,7 @@ export default class ContextMenuController {
|
|||||||
preventCutOf: true,
|
preventCutOf: true,
|
||||||
defaultPosition: ContextMenuController._getDefaultPosition(
|
defaultPosition: ContextMenuController._getDefaultPosition(
|
||||||
defaultPointsPosition,
|
defaultPointsPosition,
|
||||||
event?.detail,
|
event?.detail || event,
|
||||||
viewportElement
|
viewportElement
|
||||||
),
|
),
|
||||||
event,
|
event,
|
||||||
@ -89,7 +96,7 @@ export default class ContextMenuController {
|
|||||||
menus,
|
menus,
|
||||||
event,
|
event,
|
||||||
subMenu,
|
subMenu,
|
||||||
eventData: event?.detail,
|
eventData: event?.detail || event,
|
||||||
|
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
this.services.uiDialogService.dismiss({ id: 'context-menu' });
|
this.services.uiDialogService.dismiss({ id: 'context-menu' });
|
||||||
@ -136,8 +143,8 @@ export default class ContextMenuController {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static _getEventDefaultPosition = eventDetail => ({
|
static _getEventDefaultPosition = eventDetail => ({
|
||||||
x: eventDetail && eventDetail.currentPoints.client[0],
|
x: eventDetail?.currentPoints?.client[0] ?? eventDetail?.pageX,
|
||||||
y: eventDetail && eventDetail.currentPoints.client[1],
|
y: eventDetail?.currentPoints?.client[1] ?? eventDetail?.pageY,
|
||||||
});
|
});
|
||||||
|
|
||||||
static _getElementDefaultPosition = element => {
|
static _getElementDefaultPosition = element => {
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { Separator } from '@ohif/ui-next';
|
import { Separator } from '@ohif/ui-next';
|
||||||
import { PanelStudyBrowserHeader } from './PanelStudyBrowserHeader';
|
import { PanelStudyBrowserHeader } from './PanelStudyBrowserHeader';
|
||||||
import { defaultActionIcons, defaultViewPresets } from './constants';
|
import { defaultActionIcons, defaultViewPresets } from './constants';
|
||||||
|
import MoreDropdownMenu from '../../Components/MoreDropdownMenu';
|
||||||
|
|
||||||
const { sortStudyInstances, formatDate, createStudyBrowserTabs } = utils;
|
const { sortStudyInstances, formatDate, createStudyBrowserTabs } = utils;
|
||||||
|
|
||||||
@ -280,10 +281,6 @@ function PanelStudyBrowser({
|
|||||||
|
|
||||||
const activeDisplaySetInstanceUIDs = viewports.get(activeViewportId)?.displaySetInstanceUIDs;
|
const activeDisplaySetInstanceUIDs = viewports.get(activeViewportId)?.displaySetInstanceUIDs;
|
||||||
|
|
||||||
const onThumbnailContextMenu = (commandName, options) => {
|
|
||||||
commandsManager.runCommand(commandName, options);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<>
|
<>
|
||||||
@ -304,16 +301,26 @@ function PanelStudyBrowser({
|
|||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
servicesManager={servicesManager}
|
servicesManager={servicesManager}
|
||||||
activeTabName={activeTabName}
|
activeTabName={activeTabName}
|
||||||
onDoubleClickThumbnail={onDoubleClickThumbnailHandler}
|
|
||||||
activeDisplaySetInstanceUIDs={activeDisplaySetInstanceUIDs}
|
|
||||||
expandedStudyInstanceUIDs={expandedStudyInstanceUIDs}
|
expandedStudyInstanceUIDs={expandedStudyInstanceUIDs}
|
||||||
onClickStudy={_handleStudyClick}
|
onClickStudy={_handleStudyClick}
|
||||||
onClickTab={clickedTabName => {
|
onClickTab={clickedTabName => {
|
||||||
setActiveTabName(clickedTabName);
|
setActiveTabName(clickedTabName);
|
||||||
}}
|
}}
|
||||||
|
onClickThumbnail={() => {}}
|
||||||
|
onDoubleClickThumbnail={onDoubleClickThumbnailHandler}
|
||||||
|
activeDisplaySetInstanceUIDs={activeDisplaySetInstanceUIDs}
|
||||||
showSettings={actionIcons.find(icon => icon.id === 'settings').value}
|
showSettings={actionIcons.find(icon => icon.id === 'settings').value}
|
||||||
viewPresets={viewPresets}
|
viewPresets={viewPresets}
|
||||||
onThumbnailContextMenu={onThumbnailContextMenu}
|
ThumbnailMenuItems={MoreDropdownMenu({
|
||||||
|
commandsManager,
|
||||||
|
servicesManager,
|
||||||
|
menuItemsKey: 'studyBrowser.thumbnailMenuItems',
|
||||||
|
})}
|
||||||
|
StudyMenuItems={MoreDropdownMenu({
|
||||||
|
commandsManager,
|
||||||
|
servicesManager,
|
||||||
|
menuItemsKey: 'studyBrowser.studyMenuItems',
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -13,10 +13,10 @@ import requestDisplaySetCreationForStudy from './requestDisplaySetCreationForStu
|
|||||||
* @param {object} commandsManager
|
* @param {object} commandsManager
|
||||||
* @param {object} extensionManager
|
* @param {object} extensionManager
|
||||||
*/
|
*/
|
||||||
function WrappedPanelStudyBrowser({ commandsManager, extensionManager, servicesManager }) {
|
function WrappedPanelStudyBrowser({ extensionManager, servicesManager }) {
|
||||||
// TODO: This should be made available a different way; route should have
|
// TODO: This should be made available a different way; route should have
|
||||||
// already determined our datasource
|
// already determined our datasource
|
||||||
const dataSource = extensionManager.getDataSources()[0];
|
const [dataSource] = extensionManager.getActiveDataSource();
|
||||||
const _getStudiesForPatientByMRN = getStudiesForPatientByMRN.bind(null, dataSource);
|
const _getStudiesForPatientByMRN = getStudiesForPatientByMRN.bind(null, dataSource);
|
||||||
const _getImageSrcFromImageId = useCallback(
|
const _getImageSrcFromImageId = useCallback(
|
||||||
_createGetImageSrcFromImageIdFn(extensionManager),
|
_createGetImageSrcFromImageIdFn(extensionManager),
|
||||||
|
|||||||
@ -13,7 +13,7 @@ function requestDisplaySetCreationForStudy(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dataSource.retrieve.series.metadata({ StudyInstanceUID, madeInClient });
|
return dataSource.retrieve.series.metadata({ StudyInstanceUID, madeInClient });
|
||||||
}
|
}
|
||||||
|
|
||||||
export default requestDisplaySetCreationForStudy;
|
export default requestDisplaySetCreationForStudy;
|
||||||
|
|||||||
@ -25,8 +25,6 @@ function ViewerHeader({
|
|||||||
const onClickReturnButton = () => {
|
const onClickReturnButton = () => {
|
||||||
const { pathname } = location;
|
const { pathname } = location;
|
||||||
const dataSourceIdx = pathname.indexOf('/', 1);
|
const dataSourceIdx = pathname.indexOf('/', 1);
|
||||||
const query = new URLSearchParams(window.location.search);
|
|
||||||
const configUrl = query.get('configUrl');
|
|
||||||
|
|
||||||
const dataSourceName = pathname.substring(dataSourceIdx + 1);
|
const dataSourceName = pathname.substring(dataSourceIdx + 1);
|
||||||
const existingDataSource = extensionManager.getDataSources(dataSourceName);
|
const existingDataSource = extensionManager.getDataSources(dataSourceName);
|
||||||
@ -35,10 +33,7 @@ function ViewerHeader({
|
|||||||
if (dataSourceIdx !== -1 && existingDataSource) {
|
if (dataSourceIdx !== -1 && existingDataSource) {
|
||||||
searchQuery.append('datasources', pathname.substring(dataSourceIdx + 1));
|
searchQuery.append('datasources', pathname.substring(dataSourceIdx + 1));
|
||||||
}
|
}
|
||||||
|
preserveQueryParameters(searchQuery);
|
||||||
if (configUrl) {
|
|
||||||
searchQuery.append('configUrl', configUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
navigate({
|
navigate({
|
||||||
pathname: publicUrl,
|
pathname: publicUrl,
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Types } from '@ohif/core';
|
import { Types, DicomMetadataStore } from '@ohif/core';
|
||||||
|
|
||||||
import { ContextMenuController, defaultContextMenu } from './CustomizableContextMenu';
|
import { ContextMenuController, defaultContextMenu } from './CustomizableContextMenu';
|
||||||
import DicomTagBrowser from './DicomTagBrowser/DicomTagBrowser';
|
import DicomTagBrowser from './DicomTagBrowser/DicomTagBrowser';
|
||||||
@ -16,6 +16,7 @@ import { useHangingProtocolStageIndexStore } from './stores/useHangingProtocolSt
|
|||||||
import { useToggleHangingProtocolStore } from './stores/useToggleHangingProtocolStore';
|
import { useToggleHangingProtocolStore } from './stores/useToggleHangingProtocolStore';
|
||||||
import { useViewportsByPositionStore } from './stores/useViewportsByPositionStore';
|
import { useViewportsByPositionStore } from './stores/useViewportsByPositionStore';
|
||||||
import { useToggleOneUpViewportGridStore } from './stores/useToggleOneUpViewportGridStore';
|
import { useToggleOneUpViewportGridStore } from './stores/useToggleOneUpViewportGridStore';
|
||||||
|
import requestDisplaySetCreationForStudy from './Panels/requestDisplaySetCreationForStudy';
|
||||||
|
|
||||||
export type HangingProtocolParams = {
|
export type HangingProtocolParams = {
|
||||||
protocolId?: string;
|
protocolId?: string;
|
||||||
@ -33,6 +34,7 @@ export type UpdateViewportDisplaySetParams = {
|
|||||||
const commandsModule = ({
|
const commandsModule = ({
|
||||||
servicesManager,
|
servicesManager,
|
||||||
commandsManager,
|
commandsManager,
|
||||||
|
extensionManager,
|
||||||
}: Types.Extensions.ExtensionParams): Types.Extensions.CommandsModule => {
|
}: Types.Extensions.ExtensionParams): Types.Extensions.CommandsModule => {
|
||||||
const {
|
const {
|
||||||
customizationService,
|
customizationService,
|
||||||
@ -41,12 +43,56 @@ const commandsModule = ({
|
|||||||
uiNotificationService,
|
uiNotificationService,
|
||||||
viewportGridService,
|
viewportGridService,
|
||||||
displaySetService,
|
displaySetService,
|
||||||
|
multiMonitorService,
|
||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
|
|
||||||
// Define a context menu controller for use with any context menus
|
// Define a context menu controller for use with any context menus
|
||||||
const contextMenuController = new ContextMenuController(servicesManager, commandsManager);
|
const contextMenuController = new ContextMenuController(servicesManager, commandsManager);
|
||||||
|
|
||||||
const actions = {
|
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.
|
* Show the context menu.
|
||||||
* @param options.menuId defines the menu name to lookup, from customizationService
|
* @param options.menuId defines the menu name to lookup, from customizationService
|
||||||
@ -135,11 +181,13 @@ const commandsModule = ({
|
|||||||
*/
|
*/
|
||||||
setHangingProtocol: ({
|
setHangingProtocol: ({
|
||||||
activeStudyUID = '',
|
activeStudyUID = '',
|
||||||
|
StudyInstanceUID = '',
|
||||||
protocolId,
|
protocolId,
|
||||||
stageId,
|
stageId,
|
||||||
stageIndex,
|
stageIndex,
|
||||||
reset = false,
|
reset = false,
|
||||||
}: HangingProtocolParams): boolean => {
|
}: HangingProtocolParams): boolean => {
|
||||||
|
const toUseStudyInstanceUID = activeStudyUID || StudyInstanceUID;
|
||||||
try {
|
try {
|
||||||
// Stores in the state the display set selector id to displaySetUID mapping
|
// 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
|
// Pass in viewportId for the active viewport. This item will get set as
|
||||||
@ -158,7 +206,7 @@ const commandsModule = ({
|
|||||||
}
|
}
|
||||||
} else if (stageIndex === undefined && stageId === undefined) {
|
} else if (stageIndex === undefined && stageId === undefined) {
|
||||||
// Re-set the same stage as was previously used
|
// 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;
|
stageIndex = hangingProtocolStageIndexMap[hangingId]?.stageIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,11 +217,9 @@ const commandsModule = ({
|
|||||||
stageIndex,
|
stageIndex,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (activeStudyUID) {
|
const activeStudyChanged = hangingProtocolService.setActiveStudyUID(toUseStudyInstanceUID);
|
||||||
hangingProtocolService.setActiveStudyUID(activeStudyUID);
|
|
||||||
}
|
|
||||||
|
|
||||||
const storedHanging = `${hangingProtocolService.getState().activeStudyUID}:${protocolId}:${
|
const storedHanging = `${toUseStudyInstanceUID || hangingProtocolService.getState().activeStudyUID}:${protocolId}:${
|
||||||
useStageIdx || 0
|
useStageIdx || 0
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
@ -181,9 +227,25 @@ const commandsModule = ({
|
|||||||
const restoreProtocol = !reset && viewportGridState[storedHanging];
|
const restoreProtocol = !reset && viewportGridState[storedHanging];
|
||||||
|
|
||||||
if (
|
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();
|
||||||
|
const activeStudy = {
|
||||||
|
StudyInstanceUID: toUseStudyInstanceUID,
|
||||||
|
displaySets,
|
||||||
|
};
|
||||||
|
hangingProtocolService.run(activeStudy, protocolId);
|
||||||
|
} else if (
|
||||||
protocolId === hpInfo.protocolId &&
|
protocolId === hpInfo.protocolId &&
|
||||||
useStageIdx === hpInfo.stageIndex &&
|
useStageIdx === hpInfo.stageIndex &&
|
||||||
!activeStudyUID
|
!toUseStudyInstanceUID
|
||||||
) {
|
) {
|
||||||
// Clear the HP setting to reset them
|
// Clear the HP setting to reset them
|
||||||
hangingProtocolService.setProtocol(protocolId, {
|
hangingProtocolService.setProtocol(protocolId, {
|
||||||
@ -204,7 +266,7 @@ const commandsModule = ({
|
|||||||
// Do this after successfully applying the update
|
// Do this after successfully applying the update
|
||||||
const { setDisplaySetSelector } = useDisplaySetSelectorStore.getState();
|
const { setDisplaySetSelector } = useDisplaySetSelectorStore.getState();
|
||||||
setDisplaySetSelector(
|
setDisplaySetSelector(
|
||||||
`${activeStudyUID || hpInfo.activeStudyUID}:activeDisplaySet:0`,
|
`${toUseStudyInstanceUID || hpInfo.activeStudyUID}:activeDisplaySet:0`,
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
@ -562,6 +624,8 @@ const commandsModule = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const definitions = {
|
const definitions = {
|
||||||
|
multimonitor: actions.multimonitor,
|
||||||
|
loadStudy: actions.loadStudy,
|
||||||
showContextMenu: actions.showContextMenu,
|
showContextMenu: actions.showContextMenu,
|
||||||
closeContextMenu: actions.closeContextMenu,
|
closeContextMenu: actions.closeContextMenu,
|
||||||
clearMeasurements: actions.clearMeasurements,
|
clearMeasurements: actions.clearMeasurements,
|
||||||
|
|||||||
@ -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;
|
||||||
@ -5,6 +5,15 @@ import { ProgressDropdownWithService } from './Components/ProgressDropdownWithSe
|
|||||||
import DataSourceConfigurationComponent from './Components/DataSourceConfigurationComponent';
|
import DataSourceConfigurationComponent from './Components/DataSourceConfigurationComponent';
|
||||||
import { GoogleCloudDataSourceConfigurationAPI } from './DataSourceConfigurationAPI/GoogleCloudDataSourceConfigurationAPI';
|
import { GoogleCloudDataSourceConfigurationAPI } from './DataSourceConfigurationAPI/GoogleCloudDataSourceConfigurationAPI';
|
||||||
import { utils } from '@ohif/core';
|
import { utils } from '@ohif/core';
|
||||||
|
import studyBrowserContextMenu from './customizations/studyBrowserContextMenu';
|
||||||
|
import {
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
Icons,
|
||||||
|
} from '@ohif/ui-next';
|
||||||
|
|
||||||
const formatDate = utils.formatDate;
|
const formatDate = utils.formatDate;
|
||||||
|
|
||||||
@ -47,7 +56,74 @@ export default function getCustomizationModule({ servicesManager, extensionManag
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'multimonitor',
|
||||||
|
merge: 'Append',
|
||||||
|
value: {
|
||||||
|
id: 'studyBrowser.studyMenuItems',
|
||||||
|
customizationType: 'ohif.menuContent',
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
id: 'applyHangingProtocol',
|
||||||
|
label: 'Apply Hanging Protocol',
|
||||||
|
iconName: 'ViewportViews',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 'applyDefaultProtocol',
|
||||||
|
label: 'Default',
|
||||||
|
commands: [
|
||||||
|
'loadStudy',
|
||||||
|
{
|
||||||
|
commandName: 'setHangingProtocol',
|
||||||
|
commandOptions: {
|
||||||
|
protocolId: 'default',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'applyMPRProtocol',
|
||||||
|
label: '2x2 Grid',
|
||||||
|
commands: [
|
||||||
|
'loadStudy',
|
||||||
|
{
|
||||||
|
commandName: 'setHangingProtocol',
|
||||||
|
commandOptions: {
|
||||||
|
protocolId: '@ohif/mnGrid',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'showInOtherMonitor',
|
||||||
|
label: 'Launch On Second Monitor',
|
||||||
|
iconName: 'DicomTagBrowser',
|
||||||
|
// we should use evaluator for this, as these are basically toolbar buttons
|
||||||
|
selector: ({ servicesManager }) => {
|
||||||
|
const { multiMonitorService } = servicesManager.services;
|
||||||
|
return multiMonitorService.isMultimonitor;
|
||||||
|
},
|
||||||
|
commands: {
|
||||||
|
commandName: 'multimonitor',
|
||||||
|
commandOptions: {
|
||||||
|
hashParams: '&hangingProtocolId=@ohif/mnGrid8',
|
||||||
|
commands: [
|
||||||
|
'loadStudy',
|
||||||
|
{
|
||||||
|
commandName: 'setHangingProtocol',
|
||||||
|
commandOptions: {
|
||||||
|
protocolId: '@ohif/mnGrid8',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'default',
|
name: 'default',
|
||||||
value: [
|
value: [
|
||||||
@ -117,7 +193,6 @@ export default function getCustomizationModule({ servicesManager, extensionManag
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
id: 'ohif.contextMenu',
|
id: 'ohif.contextMenu',
|
||||||
|
|
||||||
@ -140,6 +215,7 @@ export default function getCustomizationModule({ servicesManager, extensionManag
|
|||||||
return clonedObject;
|
return clonedObject;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
studyBrowserContextMenu,
|
||||||
|
|
||||||
{
|
{
|
||||||
// the generic GUI component to configure a data source using an instance of a BaseDataSourceConfigurationAPI
|
// the generic GUI component to configure a data source using an instance of a BaseDataSourceConfigurationAPI
|
||||||
@ -200,6 +276,69 @@ export default function getCustomizationModule({ servicesManager, extensionManag
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'studyBrowser.thumbnailMenuItems',
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
id: 'tagBrowser',
|
||||||
|
label: 'Tag Browser',
|
||||||
|
iconName: 'DicomTagBrowser',
|
||||||
|
commands: 'openDICOMTagViewer',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ohif.menuContent',
|
||||||
|
content: function (props) {
|
||||||
|
const { item, commandsManager, servicesManager, ...rest } = props;
|
||||||
|
|
||||||
|
// If item has sub-items, render a submenu
|
||||||
|
if (item.items) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuSub>
|
||||||
|
<DropdownMenuSubTrigger className="gap-[6px]">
|
||||||
|
{item.iconName && (
|
||||||
|
<Icons.ByName
|
||||||
|
name={item.iconName}
|
||||||
|
className="-ml-1"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{item.label}
|
||||||
|
</DropdownMenuSubTrigger>
|
||||||
|
<DropdownMenuPortal>
|
||||||
|
<DropdownMenuSubContent>
|
||||||
|
{item.items.map(subItem => this.content({ ...props, item: subItem }))}
|
||||||
|
</DropdownMenuSubContent>
|
||||||
|
</DropdownMenuPortal>
|
||||||
|
</DropdownMenuSub>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular menu item
|
||||||
|
const isDisabled = item.selector && !item.selector({ servicesManager });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
disabled={isDisabled}
|
||||||
|
onSelect={() => {
|
||||||
|
commandsManager.runAsync(item.commands, {
|
||||||
|
...item.commandOptions,
|
||||||
|
...rest,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="gap-[6px]"
|
||||||
|
>
|
||||||
|
{item.iconName && (
|
||||||
|
<Icons.ByName
|
||||||
|
name={item.iconName}
|
||||||
|
className="-ml-1"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{item.label}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import hpMNGrid from './hangingprotocols/hpMNGrid';
|
import { hpMN, hpMN8, hpMNMonitor2 } from './hangingprotocols/hpMNGrid';
|
||||||
import hpMNCompare from './hangingprotocols/hpCompare';
|
import hpMNCompare from './hangingprotocols/hpCompare';
|
||||||
import hpMammography from './hangingprotocols/hpMammo';
|
import hpMammography from './hangingprotocols/hpMammo';
|
||||||
import hpScale from './hangingprotocols/hpScale';
|
import hpScale from './hangingprotocols/hpScale';
|
||||||
@ -142,8 +142,16 @@ function getHangingProtocolModule() {
|
|||||||
},
|
},
|
||||||
// Create a MxN hanging protocol available by default
|
// Create a MxN hanging protocol available by default
|
||||||
{
|
{
|
||||||
name: hpMNGrid.id,
|
name: hpMN.id,
|
||||||
protocol: hpMNGrid,
|
protocol: hpMN,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: hpMN8.id,
|
||||||
|
protocol: hpMN8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: hpMNMonitor2.id,
|
||||||
|
protocol: hpMNMonitor2,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
import { Types } from '@ohif/core';
|
import { Types } from '@ohif/core';
|
||||||
|
import { studyWithImages } from './utils/studySelectors';
|
||||||
|
import { seriesWithImages } from './utils/seriesSelectors';
|
||||||
|
import { viewportOptions } from './utils/viewportOptions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync group configuration for hydrating segmentations across viewports
|
* Sync group configuration for hydrating segmentations across viewports
|
||||||
@ -21,41 +24,15 @@ export const HYDRATE_SEG_SYNC_GROUP = {
|
|||||||
* `&hangingProtocolId=@ohif/mnGrid` added to the viewer URL
|
* `&hangingProtocolId=@ohif/mnGrid` added to the viewer URL
|
||||||
* It is not included in the viewer mode by default.
|
* It is not included in the viewer mode by default.
|
||||||
*/
|
*/
|
||||||
const hpMN: Types.HangingProtocol.Protocol = {
|
export const hpMN: Types.HangingProtocol.Protocol = {
|
||||||
id: '@ohif/mnGrid',
|
id: '@ohif/mnGrid',
|
||||||
description: 'Has various hanging protocol grid layouts',
|
description: 'Has various hanging protocol grid layouts',
|
||||||
name: '2x2',
|
name: '2x2',
|
||||||
protocolMatchingRules: [
|
protocolMatchingRules: studyWithImages,
|
||||||
{
|
|
||||||
id: 'OneOrMoreSeries',
|
|
||||||
weight: 25,
|
|
||||||
attribute: 'numberOfDisplaySetsWithImages',
|
|
||||||
constraint: {
|
|
||||||
greaterThan: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
toolGroupIds: ['default'],
|
toolGroupIds: ['default'],
|
||||||
displaySetSelectors: {
|
displaySetSelectors: {
|
||||||
defaultDisplaySetId: {
|
defaultDisplaySetId: {
|
||||||
seriesMatchingRules: [
|
seriesMatchingRules: seriesWithImages,
|
||||||
{
|
|
||||||
attribute: 'numImageFrames',
|
|
||||||
constraint: {
|
|
||||||
greaterThan: { value: 0 },
|
|
||||||
},
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
// This display set will select the specified items by preference
|
|
||||||
// It has no affect if nothing is specified in the URL.
|
|
||||||
{
|
|
||||||
attribute: 'isDisplaySetFromUrl',
|
|
||||||
weight: 20,
|
|
||||||
constraint: {
|
|
||||||
equals: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultViewport: {
|
defaultViewport: {
|
||||||
@ -90,21 +67,7 @@ const hpMN: Types.HangingProtocol.Protocol = {
|
|||||||
},
|
},
|
||||||
viewports: [
|
viewports: [
|
||||||
{
|
{
|
||||||
viewportOptions: {
|
viewportOptions,
|
||||||
toolGroupId: 'default',
|
|
||||||
allowUnmatchedView: true,
|
|
||||||
syncGroups: [
|
|
||||||
{
|
|
||||||
type: 'hydrateseg',
|
|
||||||
id: 'sameFORId',
|
|
||||||
source: true,
|
|
||||||
target: true,
|
|
||||||
options: {
|
|
||||||
matchingRules: ['sameFOR'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
displaySets: [
|
displaySets: [
|
||||||
{
|
{
|
||||||
id: 'defaultDisplaySetId',
|
id: 'defaultDisplaySetId',
|
||||||
@ -112,10 +75,7 @@ const hpMN: Types.HangingProtocol.Protocol = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
viewportOptions: {
|
viewportOptions,
|
||||||
toolGroupId: 'default',
|
|
||||||
allowUnmatchedView: true,
|
|
||||||
},
|
|
||||||
displaySets: [
|
displaySets: [
|
||||||
{
|
{
|
||||||
matchedDisplaySetsIndex: 1,
|
matchedDisplaySetsIndex: 1,
|
||||||
@ -124,21 +84,7 @@ const hpMN: Types.HangingProtocol.Protocol = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
viewportOptions: {
|
viewportOptions,
|
||||||
toolGroupId: 'default',
|
|
||||||
allowUnmatchedView: true,
|
|
||||||
syncGroups: [
|
|
||||||
{
|
|
||||||
type: 'hydrateseg',
|
|
||||||
id: 'sameFORId',
|
|
||||||
source: true,
|
|
||||||
target: true,
|
|
||||||
// options: {
|
|
||||||
// matchingRules: ['sameFOR'],
|
|
||||||
// },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
displaySets: [
|
displaySets: [
|
||||||
{
|
{
|
||||||
matchedDisplaySetsIndex: 2,
|
matchedDisplaySetsIndex: 2,
|
||||||
@ -147,21 +93,7 @@ const hpMN: Types.HangingProtocol.Protocol = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
viewportOptions: {
|
viewportOptions,
|
||||||
toolGroupId: 'default',
|
|
||||||
allowUnmatchedView: true,
|
|
||||||
syncGroups: [
|
|
||||||
{
|
|
||||||
type: 'hydrateseg',
|
|
||||||
id: 'sameFORId',
|
|
||||||
source: true,
|
|
||||||
target: true,
|
|
||||||
// options: {
|
|
||||||
// matchingRules: ['sameFOR'],
|
|
||||||
// },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
displaySets: [
|
displaySets: [
|
||||||
{
|
{
|
||||||
matchedDisplaySetsIndex: 3,
|
matchedDisplaySetsIndex: 3,
|
||||||
@ -174,11 +106,7 @@ const hpMN: Types.HangingProtocol.Protocol = {
|
|||||||
|
|
||||||
// 3x1 stage
|
// 3x1 stage
|
||||||
{
|
{
|
||||||
id: '3x1',
|
name: '3x1',
|
||||||
// Obsolete settings:
|
|
||||||
requiredViewports: 1,
|
|
||||||
preferredViewports: 3,
|
|
||||||
// New equivalent:
|
|
||||||
stageActivation: {
|
stageActivation: {
|
||||||
enabled: {
|
enabled: {
|
||||||
minViewportsMatched: 3,
|
minViewportsMatched: 3,
|
||||||
@ -193,10 +121,7 @@ const hpMN: Types.HangingProtocol.Protocol = {
|
|||||||
},
|
},
|
||||||
viewports: [
|
viewports: [
|
||||||
{
|
{
|
||||||
viewportOptions: {
|
viewportOptions,
|
||||||
toolGroupId: 'default',
|
|
||||||
allowUnmatchedView: true,
|
|
||||||
},
|
|
||||||
displaySets: [
|
displaySets: [
|
||||||
{
|
{
|
||||||
id: 'defaultDisplaySetId',
|
id: 'defaultDisplaySetId',
|
||||||
@ -204,10 +129,7 @@ const hpMN: Types.HangingProtocol.Protocol = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
viewportOptions: {
|
viewportOptions,
|
||||||
toolGroupId: 'default',
|
|
||||||
allowUnmatchedView: true,
|
|
||||||
},
|
|
||||||
displaySets: [
|
displaySets: [
|
||||||
{
|
{
|
||||||
id: 'defaultDisplaySetId',
|
id: 'defaultDisplaySetId',
|
||||||
@ -216,10 +138,7 @@ const hpMN: Types.HangingProtocol.Protocol = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
viewportOptions: {
|
viewportOptions,
|
||||||
toolGroupId: 'default',
|
|
||||||
allowUnmatchedView: true,
|
|
||||||
},
|
|
||||||
displaySets: [
|
displaySets: [
|
||||||
{
|
{
|
||||||
id: 'defaultDisplaySetId',
|
id: 'defaultDisplaySetId',
|
||||||
@ -232,9 +151,7 @@ const hpMN: Types.HangingProtocol.Protocol = {
|
|||||||
|
|
||||||
// A 2x1 stage
|
// A 2x1 stage
|
||||||
{
|
{
|
||||||
id: '2x1',
|
name: '2x1',
|
||||||
requiredViewports: 1,
|
|
||||||
preferredViewports: 2,
|
|
||||||
stageActivation: {
|
stageActivation: {
|
||||||
enabled: {
|
enabled: {
|
||||||
minViewportsMatched: 2,
|
minViewportsMatched: 2,
|
||||||
@ -249,10 +166,7 @@ const hpMN: Types.HangingProtocol.Protocol = {
|
|||||||
},
|
},
|
||||||
viewports: [
|
viewports: [
|
||||||
{
|
{
|
||||||
viewportOptions: {
|
viewportOptions,
|
||||||
toolGroupId: 'default',
|
|
||||||
allowUnmatchedView: true,
|
|
||||||
},
|
|
||||||
displaySets: [
|
displaySets: [
|
||||||
{
|
{
|
||||||
id: 'defaultDisplaySetId',
|
id: 'defaultDisplaySetId',
|
||||||
@ -260,10 +174,7 @@ const hpMN: Types.HangingProtocol.Protocol = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
viewportOptions: {
|
viewportOptions,
|
||||||
toolGroupId: 'default',
|
|
||||||
allowUnmatchedView: true,
|
|
||||||
},
|
|
||||||
displaySets: [
|
displaySets: [
|
||||||
{
|
{
|
||||||
matchedDisplaySetsIndex: 1,
|
matchedDisplaySetsIndex: 1,
|
||||||
@ -276,9 +187,7 @@ const hpMN: Types.HangingProtocol.Protocol = {
|
|||||||
|
|
||||||
// A 1x1 stage - should be automatically activated if there is only 1 viewable instance
|
// A 1x1 stage - should be automatically activated if there is only 1 viewable instance
|
||||||
{
|
{
|
||||||
id: '1x1',
|
name: '1x1',
|
||||||
requiredViewports: 1,
|
|
||||||
preferredViewports: 1,
|
|
||||||
stageActivation: {
|
stageActivation: {
|
||||||
enabled: {
|
enabled: {
|
||||||
minViewportsMatched: 1,
|
minViewportsMatched: 1,
|
||||||
@ -293,10 +202,7 @@ const hpMN: Types.HangingProtocol.Protocol = {
|
|||||||
},
|
},
|
||||||
viewports: [
|
viewports: [
|
||||||
{
|
{
|
||||||
viewportOptions: {
|
viewportOptions,
|
||||||
toolGroupId: 'default',
|
|
||||||
allowUnmatchedView: true,
|
|
||||||
},
|
|
||||||
displaySets: [
|
displaySets: [
|
||||||
{
|
{
|
||||||
id: 'defaultDisplaySetId',
|
id: 'defaultDisplaySetId',
|
||||||
@ -309,4 +215,341 @@ const hpMN: Types.HangingProtocol.Protocol = {
|
|||||||
numberOfPriorsReferenced: -1,
|
numberOfPriorsReferenced: -1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This hanging protocol can be activated on the primary mode by directly
|
||||||
|
* referencing it in a URL or by directly including it within a mode, e.g.:
|
||||||
|
* `&hangingProtocolId=@ohif/mnGrid8` added to the viewer URL
|
||||||
|
* It is not included in the viewer mode by default.
|
||||||
|
*/
|
||||||
|
export const hpMN8: Types.HangingProtocol.Protocol = {
|
||||||
|
...hpMN,
|
||||||
|
id: '@ohif/mnGrid8',
|
||||||
|
description: 'Has various hanging protocol grid layouts up to 4x2',
|
||||||
|
name: '4x2',
|
||||||
|
stages: [
|
||||||
|
{
|
||||||
|
id: '4x2',
|
||||||
|
name: '4x2',
|
||||||
|
stageActivation: {
|
||||||
|
enabled: {
|
||||||
|
minViewportsMatched: 7,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewportStructure: {
|
||||||
|
layoutType: 'grid',
|
||||||
|
properties: {
|
||||||
|
rows: 2,
|
||||||
|
columns: 4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewports: [
|
||||||
|
{
|
||||||
|
viewportOptions,
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions,
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
matchedDisplaySetsIndex: 1,
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions,
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
matchedDisplaySetsIndex: 2,
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions,
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
matchedDisplaySetsIndex: 3,
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions,
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
matchedDisplaySetsIndex: 4,
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions,
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
matchedDisplaySetsIndex: 5,
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions,
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
matchedDisplaySetsIndex: 6,
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions,
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
matchedDisplaySetsIndex: 7,
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: '3x2',
|
||||||
|
name: '3x2',
|
||||||
|
stageActivation: {
|
||||||
|
enabled: {
|
||||||
|
minViewportsMatched: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewportStructure: {
|
||||||
|
layoutType: 'grid',
|
||||||
|
properties: {
|
||||||
|
rows: 2,
|
||||||
|
columns: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewports: [
|
||||||
|
{
|
||||||
|
viewportOptions,
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions,
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
matchedDisplaySetsIndex: 1,
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions,
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
matchedDisplaySetsIndex: 2,
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions,
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
matchedDisplaySetsIndex: 3,
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions,
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
matchedDisplaySetsIndex: 4,
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions,
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
matchedDisplaySetsIndex: 5,
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
...hpMN.stages,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This hanging protocol extends the default protocol with additional
|
||||||
|
* images on the second monitor. It assumes the first four images are shown
|
||||||
|
* on monitor 0
|
||||||
|
*/
|
||||||
|
export const hpMNMonitor2: Types.HangingProtocol.Protocol = {
|
||||||
|
...hpMN,
|
||||||
|
id: '@ohif/mnGridMonitor2',
|
||||||
|
description: 'Second monitor HP with 2x2 grid',
|
||||||
|
name: '2x2 Monitor 2',
|
||||||
|
stages: [
|
||||||
|
{
|
||||||
|
id: '2x2',
|
||||||
|
name: '2x2',
|
||||||
|
stageActivation: {
|
||||||
|
enabled: {
|
||||||
|
minViewportsMatched: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewportStructure: {
|
||||||
|
layoutType: 'grid',
|
||||||
|
properties: {
|
||||||
|
rows: 2,
|
||||||
|
columns: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewports: [
|
||||||
|
{
|
||||||
|
viewportOptions,
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
matchedDisplaySetsIndex: 4,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions,
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
matchedDisplaySetsIndex: 5,
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions,
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
matchedDisplaySetsIndex: 6,
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions,
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
matchedDisplaySetsIndex: 7,
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// A 2x1 stage
|
||||||
|
{
|
||||||
|
name: '2x1',
|
||||||
|
stageActivation: {
|
||||||
|
enabled: {
|
||||||
|
minViewportsMatched: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewportStructure: {
|
||||||
|
layoutType: 'grid',
|
||||||
|
properties: {
|
||||||
|
rows: 1,
|
||||||
|
columns: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewports: [
|
||||||
|
{
|
||||||
|
viewportOptions,
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
matchedDisplaySetsIndex: 4,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions,
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
matchedDisplaySetsIndex: 5,
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: '1x1',
|
||||||
|
stageActivation: {
|
||||||
|
enabled: {
|
||||||
|
minViewportsMatched: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewportStructure: {
|
||||||
|
layoutType: 'grid',
|
||||||
|
properties: {
|
||||||
|
rows: 1,
|
||||||
|
columns: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewports: [
|
||||||
|
{
|
||||||
|
viewportOptions,
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
matchedDisplaySetsIndex: 4,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: '1x1 Base',
|
||||||
|
stageActivation: {
|
||||||
|
enabled: {
|
||||||
|
minViewportsMatched: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewportStructure: {
|
||||||
|
layoutType: 'grid',
|
||||||
|
properties: {
|
||||||
|
rows: 1,
|
||||||
|
columns: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewports: [
|
||||||
|
{
|
||||||
|
viewportOptions,
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'defaultDisplaySetId',
|
||||||
|
matchedDisplaySetsIndex: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
export default hpMN;
|
export default hpMN;
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import {
|
|||||||
LCCPrior,
|
LCCPrior,
|
||||||
RMLOPrior,
|
RMLOPrior,
|
||||||
LMLOPrior,
|
LMLOPrior,
|
||||||
} from './mammoDisplaySetSelector';
|
} from './utils/mammoDisplaySetSelector';
|
||||||
|
|
||||||
const rightDisplayArea = {
|
const rightDisplayArea = {
|
||||||
storeAsInitialCamera: true,
|
storeAsInitialCamera: true,
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import viewCodeAttribute from './viewCode';
|
import viewCodeAttribute from './utils/viewCode';
|
||||||
import lateralityAttribute from './laterality';
|
import lateralityAttribute from './utils/laterality';
|
||||||
import registerHangingProtocolAttributes from './registerHangingProtocolAttributes';
|
import registerHangingProtocolAttributes from './utils/registerHangingProtocolAttributes';
|
||||||
import hpMammography from './hpMammo';
|
import hpMammography from './hpMammo';
|
||||||
import hpMNGrid from './hpMNGrid';
|
import hpMNGrid from './hpMNGrid';
|
||||||
import hpCompare from './hpCompare';
|
import hpCompare from './hpCompare';
|
||||||
|
export * from './hpMNGrid';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
viewCodeAttribute,
|
viewCodeAttribute,
|
||||||
|
|||||||
@ -0,0 +1,23 @@
|
|||||||
|
import { Types } from '@ohif/core';
|
||||||
|
|
||||||
|
type MatchingRule = Types.HangingProtocol.MatchingRule;
|
||||||
|
|
||||||
|
export const seriesWithImages: MatchingRule[] = [
|
||||||
|
{
|
||||||
|
attribute: 'numImageFrames',
|
||||||
|
constraint: {
|
||||||
|
greaterThan: { value: 0 },
|
||||||
|
},
|
||||||
|
weight: 1,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
// This display set will select the specified items by preference
|
||||||
|
// It has no affect if nothing is specified in the URL.
|
||||||
|
{
|
||||||
|
attribute: 'isDisplaySetFromUrl',
|
||||||
|
weight: 20,
|
||||||
|
constraint: {
|
||||||
|
equals: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { Types } from '@ohif/core';
|
||||||
|
|
||||||
|
type MatchingRule = Types.HangingProtocol.MatchingRule;
|
||||||
|
|
||||||
|
export const studyWithImages: MatchingRule[] = [
|
||||||
|
{
|
||||||
|
id: 'OneOrMoreSeries',
|
||||||
|
weight: 25,
|
||||||
|
attribute: 'numberOfDisplaySetsWithImages',
|
||||||
|
constraint: {
|
||||||
|
greaterThan: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
/** A default viewport options */
|
||||||
|
export const viewportOptions = {
|
||||||
|
toolGroupId: 'default',
|
||||||
|
allowUnmatchedView: true,
|
||||||
|
syncGroups: [
|
||||||
|
{
|
||||||
|
type: 'hydrateseg',
|
||||||
|
id: 'sameFORId',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
options: {
|
||||||
|
matchingRules: ['sameFOR'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hydrateSegDefault = viewportOptions;
|
||||||
@ -37,6 +37,8 @@ import promptLabelAnnotation from './utils/promptLabelAnnotation';
|
|||||||
import usePatientInfo from './hooks/usePatientInfo';
|
import usePatientInfo from './hooks/usePatientInfo';
|
||||||
import { PanelStudyBrowserHeader } from './Panels/StudyBrowser/PanelStudyBrowserHeader';
|
import { PanelStudyBrowserHeader } from './Panels/StudyBrowser/PanelStudyBrowserHeader';
|
||||||
import * as utils from './utils';
|
import * as utils from './utils';
|
||||||
|
import MoreDropdownMenu from './Components/MoreDropdownMenu';
|
||||||
|
import requestDisplaySetCreationForStudy from './Panels/requestDisplaySetCreationForStudy';
|
||||||
|
|
||||||
const defaultExtension: Types.Extensions.Extension = {
|
const defaultExtension: Types.Extensions.Extension = {
|
||||||
/**
|
/**
|
||||||
@ -102,4 +104,6 @@ export {
|
|||||||
usePatientInfo,
|
usePatientInfo,
|
||||||
PanelStudyBrowserHeader,
|
PanelStudyBrowserHeader,
|
||||||
utils,
|
utils,
|
||||||
|
MoreDropdownMenu,
|
||||||
|
requestDisplaySetCreationForStudy,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,14 +4,13 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { utils } from '@ohif/core';
|
import { utils } from '@ohif/core';
|
||||||
import { useImageViewer, Dialog, ButtonEnums } from '@ohif/ui';
|
import { useImageViewer, Dialog, ButtonEnums } from '@ohif/ui';
|
||||||
import { useViewportGrid } from '@ohif/ui-next';
|
import { useViewportGrid, DropdownMenu, DropdownMenuTrigger, Icons, Button } from '@ohif/ui-next';
|
||||||
import { StudyBrowser } from '@ohif/ui-next';
|
import { StudyBrowser } from '@ohif/ui-next';
|
||||||
|
|
||||||
import { useTrackedMeasurements } from '../../getContextModule';
|
import { useTrackedMeasurements } from '../../getContextModule';
|
||||||
import { Separator } from '@ohif/ui-next';
|
import { Separator } from '@ohif/ui-next';
|
||||||
import { PanelStudyBrowserHeader } from '@ohif/extension-default';
|
import { PanelStudyBrowserHeader, MoreDropdownMenu } from '@ohif/extension-default';
|
||||||
import { defaultActionIcons, defaultViewPresets } from './constants';
|
import { defaultActionIcons, defaultViewPresets } from './constants';
|
||||||
|
|
||||||
const { formatDate, createStudyBrowserTabs } = utils;
|
const { formatDate, createStudyBrowserTabs } = utils;
|
||||||
const thumbnailNoImageModalities = [
|
const thumbnailNoImageModalities = [
|
||||||
'SR',
|
'SR',
|
||||||
@ -24,6 +23,7 @@ const thumbnailNoImageModalities = [
|
|||||||
'OT',
|
'OT',
|
||||||
'PMAP',
|
'PMAP',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {*} param0
|
* @param {*} param0
|
||||||
@ -485,10 +485,6 @@ export default function PanelStudyBrowserTracking({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onThumbnailContextMenu = (commandName, options) => {
|
|
||||||
commandsManager.runCommand(commandName, options);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<>
|
<>
|
||||||
@ -522,7 +518,16 @@ export default function PanelStudyBrowserTracking({
|
|||||||
activeDisplaySetInstanceUIDs={activeViewportDisplaySetInstanceUIDs}
|
activeDisplaySetInstanceUIDs={activeViewportDisplaySetInstanceUIDs}
|
||||||
showSettings={actionIcons.find(icon => icon.id === 'settings').value}
|
showSettings={actionIcons.find(icon => icon.id === 'settings').value}
|
||||||
viewPresets={viewPresets}
|
viewPresets={viewPresets}
|
||||||
onThumbnailContextMenu={onThumbnailContextMenu}
|
ThumbnailMenuItems={MoreDropdownMenu({
|
||||||
|
commandsManager,
|
||||||
|
servicesManager,
|
||||||
|
menuItemsKey: 'studyBrowser.thumbnailMenuItems',
|
||||||
|
})}
|
||||||
|
StudyMenuItems={MoreDropdownMenu({
|
||||||
|
commandsManager,
|
||||||
|
servicesManager,
|
||||||
|
menuItemsKey: 'studyBrowser.studyMenuItems',
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -592,7 +597,6 @@ function _mapDisplaySets(
|
|||||||
.forEach(ds => {
|
.forEach(ds => {
|
||||||
const imageSrc = thumbnailImageSrcMap[ds.displaySetInstanceUID];
|
const imageSrc = thumbnailImageSrcMap[ds.displaySetInstanceUID];
|
||||||
const componentType = _getComponentType(ds);
|
const componentType = _getComponentType(ds);
|
||||||
const numPanes = viewportGridService.getNumViewportPanes();
|
|
||||||
|
|
||||||
const array =
|
const array =
|
||||||
componentType === 'thumbnailTracked' ? thumbnailDisplaySets : thumbnailNoImageDisplaySets;
|
componentType === 'thumbnailTracked' ? thumbnailDisplaySets : thumbnailNoImageDisplaySets;
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import React, { useCallback, useEffect } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
//
|
//
|
||||||
import PanelStudyBrowserTracking from './PanelStudyBrowserTracking';
|
import PanelStudyBrowserTracking from './PanelStudyBrowserTracking';
|
||||||
import getImageSrcFromImageId from './getImageSrcFromImageId';
|
import getImageSrcFromImageId from './getImageSrcFromImageId';
|
||||||
import requestDisplaySetCreationForStudy from './requestDisplaySetCreationForStudy';
|
import { requestDisplaySetCreationForStudy } from '@ohif/extension-default';
|
||||||
|
|
||||||
function _getStudyForPatientUtility(extensionManager) {
|
function _getStudyForPatientUtility(extensionManager) {
|
||||||
const utilityModule = extensionManager.getModuleEntry(
|
const utilityModule = extensionManager.getModuleEntry(
|
||||||
|
|||||||
@ -1,18 +0,0 @@
|
|||||||
function requestDisplaySetCreationForStudy(
|
|
||||||
dataSource,
|
|
||||||
displaySetService,
|
|
||||||
StudyInstanceUID,
|
|
||||||
madeInClient
|
|
||||||
) {
|
|
||||||
if (
|
|
||||||
displaySetService.activeDisplaySets.some(
|
|
||||||
displaySet => displaySet.StudyInstanceUID === StudyInstanceUID
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dataSource.retrieve.series.metadata({ StudyInstanceUID, madeInClient });
|
|
||||||
}
|
|
||||||
|
|
||||||
export default requestDisplaySetCreationForStudy;
|
|
||||||
@ -25,6 +25,67 @@ window.config = {
|
|||||||
prefetch: 25,
|
prefetch: 25,
|
||||||
},
|
},
|
||||||
// filterQueryParam: false,
|
// filterQueryParam: false,
|
||||||
|
// Defines multi-monitor layouts
|
||||||
|
multimonitor: [
|
||||||
|
{
|
||||||
|
id: 'split',
|
||||||
|
test: ({ multimonitor }) => multimonitor === 'split',
|
||||||
|
screens: [
|
||||||
|
{
|
||||||
|
id: 'ohif0',
|
||||||
|
screen: null,
|
||||||
|
location: {
|
||||||
|
screen: 0,
|
||||||
|
width: 0.5,
|
||||||
|
height: 1,
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
},
|
||||||
|
options: 'location=no,menubar=no,scrollbars=no,status=no,titlebar=no',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ohif1',
|
||||||
|
screen: null,
|
||||||
|
location: {
|
||||||
|
width: 0.5,
|
||||||
|
height: 1,
|
||||||
|
left: 0.5,
|
||||||
|
top: 0,
|
||||||
|
},
|
||||||
|
options: 'location=no,menubar=no,scrollbars=no,status=no,titlebar=no',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
test: ({ multimonitor }) => multimonitor === '2',
|
||||||
|
screens: [
|
||||||
|
{
|
||||||
|
id: 'ohif0',
|
||||||
|
screen: 0,
|
||||||
|
location: {
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
},
|
||||||
|
options: 'fullscreen=yes,location=no,menubar=no,scrollbars=no,status=no,titlebar=no',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ohif1',
|
||||||
|
screen: 1,
|
||||||
|
location: {
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
},
|
||||||
|
options: 'fullscreen=yes,location=no,menubar=no,scrollbars=no,status=no,titlebar=no',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
defaultDataSourceName: 'dicomweb',
|
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 */
|
/* 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: {
|
// dangerouslyUseDynamicConfig: {
|
||||||
|
|||||||
@ -21,6 +21,67 @@ window.config = {
|
|||||||
investigationalUseDialog: {
|
investigationalUseDialog: {
|
||||||
option: 'never',
|
option: 'never',
|
||||||
},
|
},
|
||||||
|
// Defines multi-monitor layouts
|
||||||
|
multimonitor: [
|
||||||
|
{
|
||||||
|
id: 'split',
|
||||||
|
test: ({ multimonitor }) => multimonitor === 'split',
|
||||||
|
screens: [
|
||||||
|
{
|
||||||
|
id: 'ohif0',
|
||||||
|
screen: null,
|
||||||
|
location: {
|
||||||
|
screen: 0,
|
||||||
|
width: 0.5,
|
||||||
|
height: 1,
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
},
|
||||||
|
options: 'location=no,menubar=no,scrollbars=no,status=no,titlebar=no',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ohif1',
|
||||||
|
screen: null,
|
||||||
|
location: {
|
||||||
|
width: 0.5,
|
||||||
|
height: 1,
|
||||||
|
left: 0.5,
|
||||||
|
top: 0,
|
||||||
|
},
|
||||||
|
options: 'location=no,menubar=no,scrollbars=no,status=no,titlebar=no',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
test: ({ multimonitor }) => multimonitor === '2',
|
||||||
|
screens: [
|
||||||
|
{
|
||||||
|
id: 'ohif0',
|
||||||
|
screen: 0,
|
||||||
|
location: {
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
},
|
||||||
|
options: 'fullscreen=yes,location=no,menubar=no,scrollbars=no,status=no,titlebar=no',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ohif1',
|
||||||
|
screen: 1,
|
||||||
|
location: {
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
},
|
||||||
|
options: 'fullscreen=yes,location=no,menubar=no,scrollbars=no,status=no,titlebar=no',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
dataSources: [
|
dataSources: [
|
||||||
{
|
{
|
||||||
namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
|
namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import {
|
|||||||
PanelService,
|
PanelService,
|
||||||
WorkflowStepsService,
|
WorkflowStepsService,
|
||||||
StudyPrefetcherService,
|
StudyPrefetcherService,
|
||||||
|
MultiMonitorService,
|
||||||
// utils,
|
// utils,
|
||||||
} from '@ohif/core';
|
} from '@ohif/core';
|
||||||
|
|
||||||
@ -58,6 +59,7 @@ async function appInit(appConfigOrFunc, defaultExtensions, defaultModes) {
|
|||||||
servicesManager.setExtensionManager(extensionManager);
|
servicesManager.setExtensionManager(extensionManager);
|
||||||
|
|
||||||
servicesManager.registerServices([
|
servicesManager.registerServices([
|
||||||
|
[MultiMonitorService.REGISTRATION, appConfig.multimonitor],
|
||||||
UINotificationService.REGISTRATION,
|
UINotificationService.REGISTRATION,
|
||||||
UIModalService.REGISTRATION,
|
UIModalService.REGISTRATION,
|
||||||
UIDialogService.REGISTRATION,
|
UIDialogService.REGISTRATION,
|
||||||
|
|||||||
@ -3,13 +3,19 @@ import { useLocation } from 'react-router';
|
|||||||
/**
|
/**
|
||||||
* It returns a URLSearchParams of the query parameters in the URL, where the keys are
|
* It returns a URLSearchParams of the query parameters in the URL, where the keys are
|
||||||
* either lowercase or maintain their case based on the lowerCaseKeys parameter.
|
* either lowercase or maintain their case based on the lowerCaseKeys parameter.
|
||||||
|
* This will automatically include the hash parameters as preferred parameters
|
||||||
* @param {lowerCaseKeys:boolean} true to return lower case keys; false (default) to maintain casing;
|
* @param {lowerCaseKeys:boolean} true to return lower case keys; false (default) to maintain casing;
|
||||||
* @returns {URLSearchParams}
|
* @returns {URLSearchParams}
|
||||||
*/
|
*/
|
||||||
export default function useSearchParams(options = { lowerCaseKeys: false }) {
|
export default function useSearchParams(options = { lowerCaseKeys: false }) {
|
||||||
const { lowerCaseKeys } = options;
|
const { lowerCaseKeys } = options;
|
||||||
const searchParams = new URLSearchParams(useLocation().search);
|
const location = useLocation();
|
||||||
|
const searchParams = new URLSearchParams(location.search);
|
||||||
|
const hashParams = new URLSearchParams(location.hash?.substring(1) || '');
|
||||||
|
|
||||||
|
for (const [key, value] of hashParams) {
|
||||||
|
searchParams.set(key, value);
|
||||||
|
}
|
||||||
if (!lowerCaseKeys) {
|
if (!lowerCaseKeys) {
|
||||||
return searchParams;
|
return searchParams;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,8 +5,6 @@ import 'regenerator-runtime/runtime';
|
|||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { history } from './utils/history';
|
|
||||||
export { publicUrl } from './utils/publicUrl';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EXTENSIONS AND MODES
|
* EXTENSIONS AND MODES
|
||||||
@ -19,6 +17,9 @@ export { publicUrl } from './utils/publicUrl';
|
|||||||
*/
|
*/
|
||||||
import { modes as defaultModes, extensions as defaultExtensions } from './pluginImports';
|
import { modes as defaultModes, extensions as defaultExtensions } from './pluginImports';
|
||||||
import loadDynamicConfig from './loadDynamicConfig';
|
import loadDynamicConfig from './loadDynamicConfig';
|
||||||
|
export { history } from './utils/history';
|
||||||
|
export { preserveQueryParameters, preserveQueryStrings } from './utils/preserveQueryParameters';
|
||||||
|
export { publicUrl } from './utils/publicUrl';
|
||||||
|
|
||||||
loadDynamicConfig(window.config).then(config_json => {
|
loadDynamicConfig(window.config).then(config_json => {
|
||||||
// Reset Dynamic config if defined
|
// Reset Dynamic config if defined
|
||||||
@ -41,5 +42,3 @@ loadDynamicConfig(window.config).then(config_json => {
|
|||||||
const root = createRoot(container);
|
const root = createRoot(container);
|
||||||
root.render(React.createElement(App, appProps));
|
root.render(React.createElement(App, appProps));
|
||||||
});
|
});
|
||||||
|
|
||||||
export { history };
|
|
||||||
|
|||||||
@ -43,6 +43,7 @@ import {
|
|||||||
import { Types } from '@ohif/ui';
|
import { Types } from '@ohif/ui';
|
||||||
|
|
||||||
import i18n from '@ohif/i18n';
|
import i18n from '@ohif/i18n';
|
||||||
|
import { preserveQueryParameters, preserveQueryStrings } from '../../utils/preserveQueryParameters';
|
||||||
|
|
||||||
const PatientInfoVisibility = Types.PatientInfoVisibility;
|
const PatientInfoVisibility = Types.PatientInfoVisibility;
|
||||||
|
|
||||||
@ -206,11 +207,12 @@ function WorkList({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
preserveQueryStrings(queryString);
|
||||||
|
|
||||||
const search = qs.stringify(queryString, {
|
const search = qs.stringify(queryString, {
|
||||||
skipNull: true,
|
skipNull: true,
|
||||||
skipEmptyString: true,
|
skipEmptyString: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
navigate({
|
navigate({
|
||||||
pathname: publicUrl,
|
pathname: publicUrl,
|
||||||
search: search ? `?${search}` : undefined,
|
search: search ? `?${search}` : undefined,
|
||||||
@ -413,6 +415,7 @@ function WorkList({
|
|||||||
query.append('configUrl', filterValues.configUrl);
|
query.append('configUrl', filterValues.configUrl);
|
||||||
}
|
}
|
||||||
query.append('StudyInstanceUIDs', studyInstanceUid);
|
query.append('StudyInstanceUIDs', studyInstanceUid);
|
||||||
|
preserveQueryParameters(query);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
mode.displayName && (
|
mode.displayName && (
|
||||||
@ -640,7 +643,6 @@ const defaultFilterValues = {
|
|||||||
pageNumber: 1,
|
pageNumber: 1,
|
||||||
resultsPerPage: 25,
|
resultsPerPage: 25,
|
||||||
datasources: '',
|
datasources: '',
|
||||||
configUrl: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function _tryParseInt(str, defaultValue) {
|
function _tryParseInt(str, defaultValue) {
|
||||||
|
|||||||
@ -112,8 +112,7 @@ const createRoutes = ({
|
|||||||
const allRoutes = [
|
const allRoutes = [
|
||||||
...routes,
|
...routes,
|
||||||
...(showStudyList ? [WorkListRoute] : []),
|
...(showStudyList ? [WorkListRoute] : []),
|
||||||
// This next line adds a route on / to allow loading from the route and redirecting to the public url
|
...(publicUrl !== '/' && showStudyList ? [{ ...WorkListRoute, path: publicUrl }] : []),
|
||||||
...(publicUrl !== '/' && showStudyList ? [{ ...WorkListRoute, path: '/' }] : []),
|
|
||||||
...(customRoutes?.routes || []),
|
...(customRoutes?.routes || []),
|
||||||
...bakedInRoutes,
|
...bakedInRoutes,
|
||||||
customRoutes?.notFoundRoute || notFoundRoute,
|
customRoutes?.notFoundRoute || notFoundRoute,
|
||||||
|
|||||||
26
platform/app/src/utils/preserveQueryParameters.ts
Normal file
26
platform/app/src/utils/preserveQueryParameters.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
function preserve(query, current, key) {
|
||||||
|
const value = current.get(key);
|
||||||
|
if (value) {
|
||||||
|
query.append(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const preserveKeys = ['configUrl', 'multimonitor', 'screenNumber'];
|
||||||
|
|
||||||
|
export function preserveQueryParameters(
|
||||||
|
query,
|
||||||
|
current = new URLSearchParams(window.location.search)
|
||||||
|
) {
|
||||||
|
for (const key of preserveKeys) {
|
||||||
|
preserve(query, current, key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function preserveQueryStrings(query, current = new URLSearchParams(window.location.search)) {
|
||||||
|
for (const key of preserveKeys) {
|
||||||
|
const value = current.get(key);
|
||||||
|
if (value) {
|
||||||
|
query[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,8 @@
|
|||||||
import log from '../log.js';
|
import log from '../log.js';
|
||||||
import { Command, Commands, ComplexCommand } from '../types/Command';
|
import { Command, Commands, ComplexCommand } from '../types/Command';
|
||||||
|
|
||||||
|
export type RunInput = Command | Commands | Command[] | string | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The definition of a command
|
* 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.
|
* Run one or more commands with specified extra options.
|
||||||
* Returns the result of the last command run.
|
* Returns the result of the last command run.
|
||||||
@ -178,57 +218,34 @@ export class CommandsManager {
|
|||||||
* @param options - to include in the commands run beyond
|
* @param options - to include in the commands run beyond
|
||||||
* the commandOptions specified in the base.
|
* the commandOptions specified in the base.
|
||||||
*/
|
*/
|
||||||
public run(
|
public run(input: RunInput, options: Record<string, unknown> = {}): unknown[] {
|
||||||
toRun: Command | Commands | (Command | string)[] | string | undefined,
|
const commands = this.validate(input, options);
|
||||||
options?: Record<string, unknown>
|
|
||||||
): unknown {
|
|
||||||
if (!toRun) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize `toRun` to an array of `ComplexCommand`
|
const results: unknown[] = [];
|
||||||
let commands: ComplexCommand[] = [];
|
for (let i = 0; i < commands.length; i++) {
|
||||||
if (typeof toRun === 'string') {
|
const command = commands[i];
|
||||||
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 { commandName, commandOptions, context } = command;
|
const { commandName, commandOptions, context } = command;
|
||||||
if (commandName) {
|
results.push(this.runCommand(commandName, commandOptions, context));
|
||||||
result = this.runCommand(
|
|
||||||
commandName,
|
|
||||||
{
|
|
||||||
...commandOptions,
|
|
||||||
...options,
|
|
||||||
},
|
|
||||||
context
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
if (typeof command === 'function') {
|
|
||||||
result = command();
|
|
||||||
} else {
|
|
||||||
console.warn('No command name supplied in', toRun);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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,
|
PanelService,
|
||||||
WorkflowStepsService,
|
WorkflowStepsService,
|
||||||
StudyPrefetcherService,
|
StudyPrefetcherService,
|
||||||
|
MultiMonitorService,
|
||||||
} from './services';
|
} from './services';
|
||||||
|
|
||||||
import { DisplaySetMessage, DisplaySetMessageList } from './services/DisplaySetService';
|
import { DisplaySetMessage, DisplaySetMessageList } from './services/DisplaySetService';
|
||||||
@ -78,6 +79,7 @@ const OHIF = {
|
|||||||
ViewportGridService,
|
ViewportGridService,
|
||||||
HangingProtocolService,
|
HangingProtocolService,
|
||||||
UserAuthenticationService,
|
UserAuthenticationService,
|
||||||
|
MultiMonitorService,
|
||||||
IWebApiDataSource,
|
IWebApiDataSource,
|
||||||
DicomMetadataStore,
|
DicomMetadataStore,
|
||||||
pubSubServiceInterface,
|
pubSubServiceInterface,
|
||||||
@ -119,6 +121,7 @@ export {
|
|||||||
DisplaySetMessage,
|
DisplaySetMessage,
|
||||||
DisplaySetMessageList,
|
DisplaySetMessageList,
|
||||||
MeasurementService,
|
MeasurementService,
|
||||||
|
MultiMonitorService,
|
||||||
ToolbarService,
|
ToolbarService,
|
||||||
ViewportGridService,
|
ViewportGridService,
|
||||||
HangingProtocolService,
|
HangingProtocolService,
|
||||||
|
|||||||
@ -372,8 +372,22 @@ export default class HangingProtocolService extends PubSubService {
|
|||||||
* for example, a prior view hanging protocol will NOT show the active study
|
* for example, a prior view hanging protocol will NOT show the active study
|
||||||
* specifically, but will show another study instead.
|
* 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);
|
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,13 +410,18 @@ export default class HangingProtocolService extends PubSubService {
|
|||||||
public run({ studies, displaySets, activeStudy }, protocolId, options = {}) {
|
public run({ studies, displaySets, activeStudy }, protocolId, options = {}) {
|
||||||
this.studies = [...(studies || this.studies)];
|
this.studies = [...(studies || this.studies)];
|
||||||
this.displaySets = displaySets;
|
this.displaySets = displaySets;
|
||||||
this.setActiveStudyUID((activeStudy || studies[0])?.StudyInstanceUID);
|
this.setActiveStudyUID(
|
||||||
|
activeStudy?.StudyInstanceUID || (activeStudy || this.studies[0])?.StudyInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
this.protocolEngine = new ProtocolEngine(
|
this.protocolEngine = new ProtocolEngine(
|
||||||
this.getProtocols(),
|
this.getProtocols(),
|
||||||
this.customAttributeRetrievalCallbacks
|
this.customAttributeRetrievalCallbacks
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Resets the full protocol status here.
|
||||||
|
this.protocol = null;
|
||||||
|
|
||||||
if (protocolId && typeof protocolId === 'string') {
|
if (protocolId && typeof protocolId === 'string') {
|
||||||
const protocol = this.getProtocolById(protocolId);
|
const protocol = this.getProtocolById(protocolId);
|
||||||
this._setProtocol(protocol, options);
|
this._setProtocol(protocol, options);
|
||||||
@ -1201,6 +1220,7 @@ export default class HangingProtocolService extends PubSubService {
|
|||||||
viewportMatchDetails: Map<string, HangingProtocol.ViewportMatchDetails>;
|
viewportMatchDetails: Map<string, HangingProtocol.ViewportMatchDetails>;
|
||||||
displaySetMatchDetails: Map<string, HangingProtocol.DisplaySetMatchDetails>;
|
displaySetMatchDetails: Map<string, HangingProtocol.DisplaySetMatchDetails>;
|
||||||
} {
|
} {
|
||||||
|
this.activeStudy ||= this.studies[0];
|
||||||
let matchedViewports = 0;
|
let matchedViewports = 0;
|
||||||
stageModel.viewports.forEach(viewport => {
|
stageModel.viewports.forEach(viewport => {
|
||||||
const viewportId = viewport.viewportOptions.viewportId;
|
const viewportId = viewport.viewportOptions.viewportId;
|
||||||
|
|||||||
204
platform/core/src/services/MultiMonitorService.ts
Normal file
204
platform/core/src/services/MultiMonitorService.ts
Normal file
@ -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 = [];
|
this.registeredServiceNames = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
setExtensionManager(extensionManager) {
|
public setExtensionManager(extensionManager) {
|
||||||
this._extensionManager = extensionManager;
|
this._extensionManager = extensionManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,7 +25,7 @@ export default class ServicesManager {
|
|||||||
* @param {Object} service
|
* @param {Object} service
|
||||||
* @param {Object} configuration
|
* @param {Object} configuration
|
||||||
*/
|
*/
|
||||||
registerService(service, configuration = {}) {
|
public registerService(service, configuration = {}) {
|
||||||
if (!service) {
|
if (!service) {
|
||||||
log.warn('Attempting to register a null/undefined service. Exiting early.');
|
log.warn('Attempting to register a null/undefined service. Exiting early.');
|
||||||
return;
|
return;
|
||||||
@ -49,7 +49,6 @@ export default class ServicesManager {
|
|||||||
extensionManager: this._extensionManager,
|
extensionManager: this._extensionManager,
|
||||||
commandsManager: this._commandsManager,
|
commandsManager: this._commandsManager,
|
||||||
servicesManager: this,
|
servicesManager: this,
|
||||||
extensionManager: this._extensionManager,
|
|
||||||
});
|
});
|
||||||
if (service.altName) {
|
if (service.altName) {
|
||||||
// TODO - remove this registration
|
// TODO - remove this registration
|
||||||
@ -70,7 +69,7 @@ export default class ServicesManager {
|
|||||||
*
|
*
|
||||||
* @param {Object[]} services - Array of services
|
* @param {Object[]} services - Array of services
|
||||||
*/
|
*/
|
||||||
registerServices(services) {
|
public registerServices(services) {
|
||||||
services.forEach(service => {
|
services.forEach(service => {
|
||||||
const hasConfiguration = Array.isArray(service);
|
const hasConfiguration = Array.isArray(service);
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import CustomizationService from './CustomizationService';
|
|||||||
import PanelService from './PanelService';
|
import PanelService from './PanelService';
|
||||||
import WorkflowStepsService from './WorkflowStepsService';
|
import WorkflowStepsService from './WorkflowStepsService';
|
||||||
import StudyPrefetcherService from './StudyPrefetcherService';
|
import StudyPrefetcherService from './StudyPrefetcherService';
|
||||||
|
import { MultiMonitorService } from './MultiMonitorService';
|
||||||
|
|
||||||
import type Services from '../types/Services';
|
import type Services from '../types/Services';
|
||||||
|
|
||||||
@ -31,6 +32,7 @@ export {
|
|||||||
UINotificationService,
|
UINotificationService,
|
||||||
UIViewportDialogService,
|
UIViewportDialogService,
|
||||||
DicomMetadataStore,
|
DicomMetadataStore,
|
||||||
|
MultiMonitorService,
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
ToolbarService,
|
ToolbarService,
|
||||||
ViewportGridService,
|
ViewportGridService,
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import PanelServiceType from '../services/PanelService';
|
|||||||
import UIDialogServiceType from '../services/UIDialogService';
|
import UIDialogServiceType from '../services/UIDialogService';
|
||||||
import UIViewportDialogServiceType from '../services/UIViewportDialogService';
|
import UIViewportDialogServiceType from '../services/UIViewportDialogService';
|
||||||
import StudyPrefetcherServiceType from '../services/StudyPrefetcherService';
|
import StudyPrefetcherServiceType from '../services/StudyPrefetcherService';
|
||||||
|
import type { MultiMonitorService } from '../services/MultiMonitorService';
|
||||||
|
|
||||||
import ServicesManagerType from '../services/ServicesManager';
|
import ServicesManagerType from '../services/ServicesManager';
|
||||||
import CommandsManagerType from '../classes/CommandsManager';
|
import CommandsManagerType from '../classes/CommandsManager';
|
||||||
@ -55,6 +56,7 @@ declare global {
|
|||||||
export type UIViewportDialogService = UIViewportDialogServiceType;
|
export type UIViewportDialogService = UIViewportDialogServiceType;
|
||||||
export type PanelService = PanelServiceType;
|
export type PanelService = PanelServiceType;
|
||||||
export type StudyPrefetcherService = StudyPrefetcherServiceType;
|
export type StudyPrefetcherService = StudyPrefetcherServiceType;
|
||||||
|
export type MultiMonitorService;
|
||||||
|
|
||||||
export interface Managers {
|
export interface Managers {
|
||||||
servicesManager?: ServicesManager;
|
servicesManager?: ServicesManager;
|
||||||
@ -78,6 +80,7 @@ declare global {
|
|||||||
uiViewportDialogService?: UIViewportDialogServiceType;
|
uiViewportDialogService?: UIViewportDialogServiceType;
|
||||||
panelService?: PanelServiceType;
|
panelService?: PanelServiceType;
|
||||||
studyPrefetcherService?: StudyPrefetcherServiceType;
|
studyPrefetcherService?: StudyPrefetcherServiceType;
|
||||||
|
multiMonitorService?: MultiMonitorService;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import {
|
|||||||
PanelService,
|
PanelService,
|
||||||
UIDialogService,
|
UIDialogService,
|
||||||
UIViewportDialogService,
|
UIViewportDialogService,
|
||||||
|
MultiMonitorService,
|
||||||
} from '../services';
|
} from '../services';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -34,6 +35,7 @@ interface Services {
|
|||||||
uiDialogService?: UIDialogService;
|
uiDialogService?: UIDialogService;
|
||||||
uiViewportDialogService?: UIViewportDialogService;
|
uiViewportDialogService?: UIViewportDialogService;
|
||||||
panelService?: PanelService;
|
panelService?: PanelService;
|
||||||
|
multiMonitorService?: MultiMonitorService;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Services;
|
export default Services;
|
||||||
|
|||||||
@ -0,0 +1,71 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 5
|
||||||
|
sidebar_label: Multi Monitor Service
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
# Multi Monitor Service
|
||||||
|
|
||||||
|
::: info
|
||||||
|
|
||||||
|
We plan to enhance this service in the future. Currently, it offers a basic implementation of multi-monitor support, allowing you to manually open multiple windows on the same monitor. It is not yet a full multi-monitor solution!
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
The multi-monitor service provides detection, launch and communication support
|
||||||
|
for multiple monitors or windows/screens within a single monitor.
|
||||||
|
|
||||||
|
:::info
|
||||||
|
|
||||||
|
The multi-monitor service is currently applied via configuration file.
|
||||||
|
|
||||||
|
```js
|
||||||
|
customizationService: ['@ohif/extension-default.customizationModule.multimonitor'],
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Configurations
|
||||||
|
The service supports two predefined configurations:
|
||||||
|
|
||||||
|
1. **Split Screen (`multimonitor=split`)**
|
||||||
|
Splits the primary monitor into two windows.
|
||||||
|
|
||||||
|
2. **Multi-Monitor (`multimonitor=2`)**
|
||||||
|
Opens windows across separate physical monitors.
|
||||||
|
|
||||||
|
### Launch Methods
|
||||||
|
- Specify `&screenNumber=0` to designate the first window explicitly.
|
||||||
|
- Omit `screenNumber` to let the service handle window assignments dynamically.
|
||||||
|
- Use `launchAll` in the query parameters to launch all configured screens simultaneously.
|
||||||
|
|
||||||
|
#### Example URLs:
|
||||||
|
- **Split Screen:**
|
||||||
|
`http://viewer.ohif.org/.....&multimonitor=split`
|
||||||
|
Splits the primary monitor into two windows when a study is viewed.
|
||||||
|
|
||||||
|
- **Multi-Monitor with All Screens:**
|
||||||
|
`http://viewer.ohif.org/.....&multimonitor=2&screenNumber=0&launchAll`
|
||||||
|
Launches two monitors and opens all configured screens.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Behavior
|
||||||
|
|
||||||
|
### Refresh, Close and Open
|
||||||
|
If you refresh the base/original window, then all the other windows will also
|
||||||
|
refresh. However, you can safely refresh any single other window, and on the next
|
||||||
|
command to the other windows, it will re-create the other window links without
|
||||||
|
losing content in the other windows. You can also close any other window and
|
||||||
|
it will be reopened the next time you try to call to it.
|
||||||
|
|
||||||
|
|
||||||
|
## Executing Commands
|
||||||
|
The MultiMonitorService adds the ability to run commands on other specified windows.
|
||||||
|
This allows opening up a study on another window without needing to refresh
|
||||||
|
it's contents. The command below shows an example of how this can be done:
|
||||||
@ -5,17 +5,6 @@ import { StudyItem } from '../StudyItem';
|
|||||||
import { StudyBrowserSort } from '../StudyBrowserSort';
|
import { StudyBrowserSort } from '../StudyBrowserSort';
|
||||||
import { StudyBrowserViewOptions } from '../StudyBrowserViewOptions';
|
import { StudyBrowserViewOptions } from '../StudyBrowserViewOptions';
|
||||||
|
|
||||||
const getTrackedSeries = displaySets => {
|
|
||||||
let trackedSeries = 0;
|
|
||||||
displaySets.forEach(displaySet => {
|
|
||||||
if (displaySet.isTracked) {
|
|
||||||
trackedSeries++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return trackedSeries;
|
|
||||||
};
|
|
||||||
|
|
||||||
const noop = () => {};
|
const noop = () => {};
|
||||||
|
|
||||||
const StudyBrowser = ({
|
const StudyBrowser = ({
|
||||||
@ -31,7 +20,8 @@ const StudyBrowser = ({
|
|||||||
servicesManager,
|
servicesManager,
|
||||||
showSettings,
|
showSettings,
|
||||||
viewPresets,
|
viewPresets,
|
||||||
onThumbnailContextMenu,
|
ThumbnailMenuItems,
|
||||||
|
StudyMenuItems,
|
||||||
}: withAppTypes) => {
|
}: withAppTypes) => {
|
||||||
const getTabContent = () => {
|
const getTabContent = () => {
|
||||||
const tabData = tabs.find(tab => tab.name === activeTabName);
|
const tabData = tabs.find(tab => tab.name === activeTabName);
|
||||||
@ -50,18 +40,17 @@ const StudyBrowser = ({
|
|||||||
isExpanded={isExpanded}
|
isExpanded={isExpanded}
|
||||||
displaySets={displaySets}
|
displaySets={displaySets}
|
||||||
modalities={modalities}
|
modalities={modalities}
|
||||||
trackedSeries={getTrackedSeries(displaySets)}
|
|
||||||
isActive={isExpanded}
|
isActive={isExpanded}
|
||||||
onClick={() => {
|
onClick={() => onClickStudy(studyInstanceUid)}
|
||||||
onClickStudy(studyInstanceUid);
|
|
||||||
}}
|
|
||||||
onClickThumbnail={onClickThumbnail}
|
onClickThumbnail={onClickThumbnail}
|
||||||
onDoubleClickThumbnail={onDoubleClickThumbnail}
|
onDoubleClickThumbnail={onDoubleClickThumbnail}
|
||||||
onClickUntrack={onClickUntrack}
|
onClickUntrack={onClickUntrack}
|
||||||
activeDisplaySetInstanceUIDs={activeDisplaySetInstanceUIDs}
|
activeDisplaySetInstanceUIDs={activeDisplaySetInstanceUIDs}
|
||||||
data-cy="thumbnail-list"
|
data-cy="thumbnail-list"
|
||||||
viewPreset={viewPreset}
|
viewPreset={viewPreset}
|
||||||
onThumbnailContextMenu={onThumbnailContextMenu}
|
ThumbnailMenuItems={ThumbnailMenuItems}
|
||||||
|
StudyMenuItems={StudyMenuItems}
|
||||||
|
StudyInstanceUID={studyInstanceUid}
|
||||||
/>
|
/>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
@ -142,6 +131,7 @@ StudyBrowser.propTypes = {
|
|||||||
).isRequired,
|
).isRequired,
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
|
StudyMenuItems: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
export { StudyBrowser };
|
export { StudyBrowser };
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import {
|
|||||||
} from '../DropdownMenu/DropdownMenu';
|
} from '../DropdownMenu/DropdownMenu';
|
||||||
|
|
||||||
export function StudyBrowserSort({ servicesManager }: withAppTypes) {
|
export function StudyBrowserSort({ servicesManager }: withAppTypes) {
|
||||||
|
// Todo: this should not be here, no servicesManager should be in ui-next, only
|
||||||
|
// customization service
|
||||||
const { customizationService, displaySetService } = servicesManager.services;
|
const { customizationService, displaySetService } = servicesManager.services;
|
||||||
const { values: sortFunctions } = customizationService.get('studyBrowser.sortFunctions');
|
const { values: sortFunctions } = customizationService.get('studyBrowser.sortFunctions');
|
||||||
|
|
||||||
|
|||||||
@ -19,7 +19,9 @@ const StudyItem = ({
|
|||||||
onDoubleClickThumbnail,
|
onDoubleClickThumbnail,
|
||||||
onClickUntrack,
|
onClickUntrack,
|
||||||
viewPreset = 'thumbnails',
|
viewPreset = 'thumbnails',
|
||||||
onThumbnailContextMenu,
|
ThumbnailMenuItems,
|
||||||
|
StudyMenuItems,
|
||||||
|
StudyInstanceUID,
|
||||||
}: withAppTypes) => {
|
}: withAppTypes) => {
|
||||||
return (
|
return (
|
||||||
<Accordion
|
<Accordion
|
||||||
@ -32,19 +34,24 @@ const StudyItem = ({
|
|||||||
defaultValue={isActive ? 'study-item' : undefined}
|
defaultValue={isActive ? 'study-item' : undefined}
|
||||||
>
|
>
|
||||||
<AccordionItem value="study-item">
|
<AccordionItem value="study-item">
|
||||||
<AccordionTrigger className={classnames('hover:bg-accent bg-popover rounded')}>
|
<AccordionTrigger className={classnames('hover:bg-accent bg-popover group rounded')}>
|
||||||
<div className="flex h-[40px] flex-1 flex-row">
|
<div className="flex h-[40px] flex-1 flex-row">
|
||||||
<div className="flex w-full flex-row items-center justify-between">
|
<div className="flex w-full flex-row items-center">
|
||||||
<div className="flex flex-col items-start text-[13px]">
|
<div className="flex flex-col items-start text-[13px]">
|
||||||
<div className="text-white">{date}</div>
|
<div className="text-white">{date}</div>
|
||||||
<div className="text-muted-foreground h-[18px] max-w-[160px] overflow-hidden truncate whitespace-nowrap">
|
<div className="text-muted-foreground h-[18px] max-w-[160px] overflow-hidden truncate whitespace-nowrap">
|
||||||
{description}
|
{description}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-muted-foreground mr-2 flex flex-col items-end text-[12px]">
|
<div className="text-muted-foreground ml-auto flex flex-col items-end text-[12px]">
|
||||||
<div className="max-w-[150px] overflow-hidden text-ellipsis">{modalities}</div>
|
<div className="max-w-[150px] overflow-hidden text-ellipsis">{modalities}</div>
|
||||||
<div>{numInstances}</div>
|
<div>{numInstances}</div>
|
||||||
</div>
|
</div>
|
||||||
|
{StudyMenuItems && (
|
||||||
|
<div className="ml-2 flex items-center">
|
||||||
|
<StudyMenuItems StudyInstanceUID={StudyInstanceUID} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AccordionTrigger>
|
</AccordionTrigger>
|
||||||
@ -61,7 +68,7 @@ const StudyItem = ({
|
|||||||
onThumbnailDoubleClick={onDoubleClickThumbnail}
|
onThumbnailDoubleClick={onDoubleClickThumbnail}
|
||||||
onClickUntrack={onClickUntrack}
|
onClickUntrack={onClickUntrack}
|
||||||
viewPreset={viewPreset}
|
viewPreset={viewPreset}
|
||||||
onThumbnailContextMenu={onThumbnailContextMenu}
|
ThumbnailMenuItems={ThumbnailMenuItems}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
@ -75,7 +82,6 @@ StudyItem.propTypes = {
|
|||||||
description: PropTypes.string,
|
description: PropTypes.string,
|
||||||
modalities: PropTypes.string.isRequired,
|
modalities: PropTypes.string.isRequired,
|
||||||
numInstances: PropTypes.number.isRequired,
|
numInstances: PropTypes.number.isRequired,
|
||||||
trackedSeries: PropTypes.number,
|
|
||||||
isActive: PropTypes.bool,
|
isActive: PropTypes.bool,
|
||||||
onClick: PropTypes.func.isRequired,
|
onClick: PropTypes.func.isRequired,
|
||||||
isExpanded: PropTypes.bool,
|
isExpanded: PropTypes.bool,
|
||||||
@ -85,6 +91,8 @@ StudyItem.propTypes = {
|
|||||||
onDoubleClickThumbnail: PropTypes.func,
|
onDoubleClickThumbnail: PropTypes.func,
|
||||||
onClickUntrack: PropTypes.func,
|
onClickUntrack: PropTypes.func,
|
||||||
viewPreset: PropTypes.string,
|
viewPreset: PropTypes.string,
|
||||||
|
StudyMenuItems: PropTypes.func,
|
||||||
|
StudyInstanceUID: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export { StudyItem };
|
export { StudyItem };
|
||||||
|
|||||||
@ -5,13 +5,6 @@ import { useDrag } from 'react-dnd';
|
|||||||
import { Icons } from '../Icons';
|
import { Icons } from '../Icons';
|
||||||
import { DisplaySetMessageListTooltip } from '../DisplaySetMessageListTooltip';
|
import { DisplaySetMessageListTooltip } from '../DisplaySetMessageListTooltip';
|
||||||
import { TooltipTrigger, TooltipContent, Tooltip } from '../Tooltip';
|
import { TooltipTrigger, TooltipContent, Tooltip } from '../Tooltip';
|
||||||
import { Button } from '../Button';
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from '../DropdownMenu';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display a thumbnail for a display set.
|
* Display a thumbnail for a display set.
|
||||||
@ -34,12 +27,12 @@ const Thumbnail = ({
|
|||||||
viewPreset = 'thumbnails',
|
viewPreset = 'thumbnails',
|
||||||
modality,
|
modality,
|
||||||
isHydratedForDerivedDisplaySet = false,
|
isHydratedForDerivedDisplaySet = false,
|
||||||
|
isTracked = false,
|
||||||
canReject = false,
|
canReject = false,
|
||||||
onReject = () => {},
|
onReject = () => {},
|
||||||
isTracked = false,
|
|
||||||
thumbnailType = 'thumbnail',
|
thumbnailType = 'thumbnail',
|
||||||
onClickUntrack = () => {},
|
onClickUntrack = () => {},
|
||||||
onThumbnailContextMenu,
|
ThumbnailMenuItems = () => {},
|
||||||
}: withAppTypes): React.ReactNode => {
|
}: withAppTypes): React.ReactNode => {
|
||||||
// TODO: We should wrap our thumbnail to create a "DraggableThumbnail", as
|
// TODO: We should wrap our thumbnail to create a "DraggableThumbnail", as
|
||||||
// this will still allow for "drag", even if there is no drop target for the
|
// this will still allow for "drag", even if there is no drop target for the
|
||||||
@ -134,44 +127,11 @@ const Thumbnail = ({
|
|||||||
</div>
|
</div>
|
||||||
{/* bottom right */}
|
{/* bottom right */}
|
||||||
<div className="absolute bottom-0 right-0 flex items-center gap-[4px] p-[4px]">
|
<div className="absolute bottom-0 right-0 flex items-center gap-[4px] p-[4px]">
|
||||||
<DropdownMenu>
|
<ThumbnailMenuItems
|
||||||
<DropdownMenuTrigger asChild>
|
displaySetInstanceUID={displaySetInstanceUID}
|
||||||
<Button
|
canReject={canReject}
|
||||||
variant="ghost"
|
onReject={onReject}
|
||||||
size="icon"
|
/>
|
||||||
className="hidden group-hover:inline-flex data-[state=open]:inline-flex"
|
|
||||||
>
|
|
||||||
<Icons.More />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent
|
|
||||||
hideWhenDetached
|
|
||||||
align="start"
|
|
||||||
>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onSelect={() => {
|
|
||||||
onThumbnailContextMenu('openDICOMTagViewer', {
|
|
||||||
displaySetInstanceUID,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
className="gap-[6px]"
|
|
||||||
>
|
|
||||||
<Icons.DicomTagBrowser />
|
|
||||||
Tag Browser
|
|
||||||
</DropdownMenuItem>
|
|
||||||
{canReject && (
|
|
||||||
<DropdownMenuItem
|
|
||||||
onSelect={() => {
|
|
||||||
onReject();
|
|
||||||
}}
|
|
||||||
className="gap-[6px]"
|
|
||||||
>
|
|
||||||
<Icons.Trash className="h-5 w-5 text-red-500" />
|
|
||||||
Delete Report
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -243,7 +203,6 @@ const Thumbnail = ({
|
|||||||
messages={messages}
|
messages={messages}
|
||||||
id={`display-set-tooltip-${displaySetInstanceUID}`}
|
id={`display-set-tooltip-${displaySetInstanceUID}`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isTracked && (
|
{isTracked && (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
@ -271,41 +230,11 @@ const Thumbnail = ({
|
|||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
<DropdownMenu>
|
<ThumbnailMenuItems
|
||||||
<DropdownMenuTrigger asChild>
|
displaySetInstanceUID={displaySetInstanceUID}
|
||||||
<Button
|
canReject={canReject}
|
||||||
variant="ghost"
|
onReject={onReject}
|
||||||
size="icon"
|
/>
|
||||||
className="hidden group-hover:inline-flex data-[state=open]:inline-flex"
|
|
||||||
>
|
|
||||||
<Icons.More />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent hideWhenDetached>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onSelect={() => {
|
|
||||||
onThumbnailContextMenu('openDICOMTagViewer', {
|
|
||||||
displaySetInstanceUID,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
className="gap-[6px]"
|
|
||||||
>
|
|
||||||
<Icons.DicomTagBrowser />
|
|
||||||
Tag Browser
|
|
||||||
</DropdownMenuItem>
|
|
||||||
{canReject && (
|
|
||||||
<DropdownMenuItem
|
|
||||||
onSelect={() => {
|
|
||||||
onReject();
|
|
||||||
}}
|
|
||||||
className="gap-[6px]"
|
|
||||||
>
|
|
||||||
<Icons.Trash className="h-5 w-5 text-red-500" />
|
|
||||||
Delete Report
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -369,8 +298,6 @@ Thumbnail.propTypes = {
|
|||||||
viewPreset: PropTypes.string,
|
viewPreset: PropTypes.string,
|
||||||
modality: PropTypes.string,
|
modality: PropTypes.string,
|
||||||
isHydratedForDerivedDisplaySet: PropTypes.bool,
|
isHydratedForDerivedDisplaySet: PropTypes.bool,
|
||||||
canReject: PropTypes.bool,
|
|
||||||
onReject: PropTypes.func,
|
|
||||||
isTracked: PropTypes.bool,
|
isTracked: PropTypes.bool,
|
||||||
onClickUntrack: PropTypes.func,
|
onClickUntrack: PropTypes.func,
|
||||||
countIcon: PropTypes.string,
|
countIcon: PropTypes.string,
|
||||||
|
|||||||
@ -10,7 +10,7 @@ const ThumbnailList = ({
|
|||||||
onClickUntrack,
|
onClickUntrack,
|
||||||
activeDisplaySetInstanceUIDs = [],
|
activeDisplaySetInstanceUIDs = [],
|
||||||
viewPreset,
|
viewPreset,
|
||||||
onThumbnailContextMenu,
|
ThumbnailMenuItems,
|
||||||
}: withAppTypes) => {
|
}: withAppTypes) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -34,9 +34,9 @@ const ThumbnailList = ({
|
|||||||
modality,
|
modality,
|
||||||
componentType,
|
componentType,
|
||||||
countIcon,
|
countIcon,
|
||||||
isTracked,
|
|
||||||
canReject,
|
canReject,
|
||||||
onReject,
|
onReject,
|
||||||
|
isTracked,
|
||||||
imageSrc,
|
imageSrc,
|
||||||
messages,
|
messages,
|
||||||
imageAltText,
|
imageAltText,
|
||||||
@ -56,6 +56,8 @@ const ThumbnailList = ({
|
|||||||
imageAltText={imageAltText}
|
imageAltText={imageAltText}
|
||||||
messages={messages}
|
messages={messages}
|
||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
|
canReject={canReject}
|
||||||
|
onReject={onReject}
|
||||||
modality={modality}
|
modality={modality}
|
||||||
viewPreset={componentType === 'thumbnailNoImage' ? 'list' : viewPreset}
|
viewPreset={componentType === 'thumbnailNoImage' ? 'list' : viewPreset}
|
||||||
thumbnailType={componentType}
|
thumbnailType={componentType}
|
||||||
@ -65,9 +67,7 @@ const ThumbnailList = ({
|
|||||||
loadingProgress={loadingProgress}
|
loadingProgress={loadingProgress}
|
||||||
onClickUntrack={() => onClickUntrack(displaySetInstanceUID)}
|
onClickUntrack={() => onClickUntrack(displaySetInstanceUID)}
|
||||||
isHydratedForDerivedDisplaySet={isHydratedForDerivedDisplaySet}
|
isHydratedForDerivedDisplaySet={isHydratedForDerivedDisplaySet}
|
||||||
canReject={canReject}
|
ThumbnailMenuItems={ThumbnailMenuItems}
|
||||||
onReject={onReject}
|
|
||||||
onThumbnailContextMenu={onThumbnailContextMenu}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,6 +34,7 @@ const StudyBrowser = ({
|
|||||||
onClickThumbnail = noop,
|
onClickThumbnail = noop,
|
||||||
onDoubleClickThumbnail = noop,
|
onDoubleClickThumbnail = noop,
|
||||||
onClickUntrack = noop,
|
onClickUntrack = noop,
|
||||||
|
onClickLaunch,
|
||||||
activeDisplaySetInstanceUIDs,
|
activeDisplaySetInstanceUIDs,
|
||||||
servicesManager,
|
servicesManager,
|
||||||
}: withAppTypes) => {
|
}: withAppTypes) => {
|
||||||
@ -60,6 +61,7 @@ const StudyBrowser = ({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
onClickStudy(studyInstanceUid);
|
onClickStudy(studyInstanceUid);
|
||||||
}}
|
}}
|
||||||
|
onClickLaunch={onClickLaunch?.bind(null, studyInstanceUid)}
|
||||||
data-cy="thumbnail-list"
|
data-cy="thumbnail-list"
|
||||||
/>
|
/>
|
||||||
{isExpanded && displaySets && (
|
{isExpanded && displaySets && (
|
||||||
|
|||||||
@ -15,8 +15,21 @@ const StudyItem = ({
|
|||||||
trackedSeries,
|
trackedSeries,
|
||||||
isActive,
|
isActive,
|
||||||
onClick,
|
onClick,
|
||||||
|
onClickLaunch,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation('StudyItem');
|
const { t } = useTranslation('StudyItem');
|
||||||
|
|
||||||
|
const onSetActive = evt => {
|
||||||
|
evt.stopPropagation();
|
||||||
|
onClickLaunch(0);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
const onLaunchWindow = evt => {
|
||||||
|
onClickLaunch(1);
|
||||||
|
evt.stopPropagation();
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classnames(
|
className={classnames(
|
||||||
@ -35,6 +48,20 @@ const StudyItem = ({
|
|||||||
<Icons.GroupLayers className="mx-2 w-4 text-blue-300" />
|
<Icons.GroupLayers className="mx-2 w-4 text-blue-300" />
|
||||||
{numInstances}
|
{numInstances}
|
||||||
</div>
|
</div>
|
||||||
|
{!!onClickLaunch && (
|
||||||
|
<div className="items-right flex flex-row text-base text-blue-300">
|
||||||
|
<Icon
|
||||||
|
name="icon-play"
|
||||||
|
className="mx-2 w-4 text-blue-300"
|
||||||
|
onClick={onSetActive}
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
name="launch-arrow"
|
||||||
|
className="mx-2 w-4 text-blue-300"
|
||||||
|
onClick={onLaunchWindow}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row items-center py-1">
|
<div className="flex flex-row items-center py-1">
|
||||||
<div className="text-l flex items-center pr-5 text-blue-300">{modalities}</div>
|
<div className="text-l flex items-center pr-5 text-blue-300">{modalities}</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user