feat(toolbar): new Toolbar to enable reactive state synchronization (#3983)
This commit is contained in:
1 parent
79d5c36bda
commit
566b25a544
155 files changed
+6311
-6577
No files matched your search
@@ -60,7 +60,6 @@ export default class ContextMenuController {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Getting items from', menus);
|
||||
const items = ContextMenuItemsBuilder.getMenuItems(
|
||||
selectorProps || contextMenuProps,
|
||||
event,
|
||||
|
||||
@@ -70,8 +70,6 @@ export function findMenu(menus: Menu[], props?: Types.IProps, menuIdFilter?: str
|
||||
current = findIt.next();
|
||||
}
|
||||
|
||||
console.log('Menu chosen', menu?.id || 'NONE');
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
function getImageSrcFromImageId(cornerstone, imageId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const canvas = document.createElement('canvas');
|
||||
// Note: the default width and height of the canvas is 300x150
|
||||
// but we need to set the width and height to the same number since
|
||||
// the thumbnails are usually square and we want to maintain the aspect ratio
|
||||
canvas.width = 128 / window.devicePixelRatio;
|
||||
canvas.height = 128 / window.devicePixelRatio;
|
||||
cornerstone.utilities
|
||||
.loadImageToCanvas({ canvas, imageId })
|
||||
.then(imageId => {
|
||||
|
||||
@@ -1,57 +1,53 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React from 'react';
|
||||
import { Tooltip } from '@ohif/ui';
|
||||
import classnames from 'classnames';
|
||||
import { useViewportGrid } from '@ohif/ui';
|
||||
import { useToolbar } from '@ohif/core';
|
||||
|
||||
export default function Toolbar({
|
||||
servicesManager,
|
||||
}: Types.Extensions.ExtensionParams): React.ReactElement {
|
||||
const { toolbarService } = servicesManager.services;
|
||||
export function Toolbar({ servicesManager }) {
|
||||
const { toolbarButtons, onInteraction } = useToolbar({
|
||||
servicesManager,
|
||||
buttonSection: 'primary',
|
||||
});
|
||||
|
||||
const [viewportGrid, viewportGridService] = useViewportGrid();
|
||||
|
||||
const [toolbarButtons, setToolbarButtons] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const updateToolbar = () => {
|
||||
const toolGroupId =
|
||||
viewportGridService.getActiveViewportOptionByKey('toolGroupId') ?? 'default';
|
||||
setToolbarButtons(toolbarService.getButtonSection(toolGroupId));
|
||||
};
|
||||
|
||||
const { unsubscribe } = toolbarService.subscribe(
|
||||
toolbarService.EVENTS.TOOL_BAR_MODIFIED,
|
||||
updateToolbar
|
||||
);
|
||||
|
||||
updateToolbar();
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [toolbarService, viewportGrid]);
|
||||
|
||||
const onInteraction = useCallback(
|
||||
args => toolbarService.recordInteraction(args),
|
||||
[toolbarService]
|
||||
);
|
||||
if (!toolbarButtons.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{toolbarButtons.map(toolDef => {
|
||||
if (!toolDef) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { id, Component, componentProps } = toolDef;
|
||||
return (
|
||||
// The margin for separating the tools on the toolbar should go here and NOT in each individual component (button) item.
|
||||
// This allows for the individual items to be included in other UI components where perhaps alternative margins are desired.
|
||||
const { disabled } = componentProps;
|
||||
|
||||
const tool = (
|
||||
<Component
|
||||
key={id}
|
||||
id={id}
|
||||
onInteraction={onInteraction}
|
||||
servicesManager={servicesManager}
|
||||
{...componentProps}
|
||||
/>
|
||||
);
|
||||
|
||||
return disabled ? (
|
||||
<Tooltip
|
||||
key={id}
|
||||
position="bottom"
|
||||
content={componentProps.label}
|
||||
secondaryContent={'Not available on the current viewport'}
|
||||
>
|
||||
<div className={classnames('mr-1')}>{tool}</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<div
|
||||
key={id}
|
||||
className={classnames('mr-1')}
|
||||
className="mr-1"
|
||||
>
|
||||
<Component
|
||||
id={id}
|
||||
{...componentProps}
|
||||
onInteraction={onInteraction}
|
||||
servicesManager={servicesManager}
|
||||
/>
|
||||
{tool}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { ToolbarButton, ButtonGroup } from '@ohif/ui';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
function ToolbarButtonGroupWithServices({ groupId, items, onInteraction, size }) {
|
||||
const getSplitButtonItems = useCallback(
|
||||
items =>
|
||||
items.map((item, index) => (
|
||||
<ToolbarButton
|
||||
key={item.id}
|
||||
icon={item.icon}
|
||||
label={item.label}
|
||||
disabled={item.disabled}
|
||||
className={item.className}
|
||||
id={item.id}
|
||||
size={size}
|
||||
onClick={() => {
|
||||
onInteraction({
|
||||
groupId,
|
||||
itemId: item.id,
|
||||
commands: item.commands,
|
||||
});
|
||||
}}
|
||||
// Note: this is necessary since tooltip will add
|
||||
// default styles to the tooltip container which
|
||||
// we don't want for groups
|
||||
toolTipClassName=""
|
||||
/>
|
||||
)),
|
||||
[onInteraction, groupId]
|
||||
);
|
||||
|
||||
return <ButtonGroup>{getSplitButtonItems(items)}</ButtonGroup>;
|
||||
}
|
||||
|
||||
export default ToolbarButtonGroupWithServices;
|
||||
@@ -1,75 +0,0 @@
|
||||
import { ToolbarButton } from '@ohif/ui';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function ToolbarButtonWithServices({
|
||||
id,
|
||||
type,
|
||||
commands,
|
||||
onInteraction,
|
||||
servicesManager,
|
||||
...props
|
||||
}) {
|
||||
const { toolbarService } = servicesManager?.services || {};
|
||||
|
||||
const [buttonsState, setButtonState] = useState({
|
||||
primaryToolId: '',
|
||||
toggles: {},
|
||||
groups: {},
|
||||
});
|
||||
const { primaryToolId } = buttonsState;
|
||||
|
||||
const isActive =
|
||||
(type === 'tool' && id === primaryToolId) ||
|
||||
(type === 'toggle' && buttonsState.toggles[id] === true);
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = toolbarService.subscribe(
|
||||
toolbarService.EVENTS.TOOL_BAR_STATE_MODIFIED,
|
||||
state => {
|
||||
setButtonState({ ...state });
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [toolbarService]);
|
||||
|
||||
return (
|
||||
<ToolbarButton
|
||||
commands={commands}
|
||||
id={id}
|
||||
type={type}
|
||||
isActive={isActive}
|
||||
onInteraction={onInteraction}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
ToolbarButtonWithServices.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
type: PropTypes.oneOf(['tool', 'action', 'toggle']).isRequired,
|
||||
commands: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
commandName: PropTypes.string.isRequired,
|
||||
context: PropTypes.string,
|
||||
})
|
||||
),
|
||||
onInteraction: PropTypes.func.isRequired,
|
||||
servicesManager: PropTypes.shape({
|
||||
services: PropTypes.shape({
|
||||
toolbarService: PropTypes.shape({
|
||||
subscribe: PropTypes.func.isRequired,
|
||||
state: PropTypes.shape({
|
||||
primaryToolId: PropTypes.string,
|
||||
toggles: PropTypes.objectOf(PropTypes.bool),
|
||||
groups: PropTypes.objectOf(PropTypes.any),
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default ToolbarButtonWithServices;
|
||||
@@ -3,21 +3,20 @@ import PropTypes from 'prop-types';
|
||||
import { LayoutSelector as OHIFLayoutSelector, ToolbarButton } from '@ohif/ui';
|
||||
import { ServicesManager } from '@ohif/core';
|
||||
|
||||
function ToolbarLayoutSelectorWithServices({ servicesManager, ...props }) {
|
||||
const { toolbarService } = servicesManager.services;
|
||||
function ToolbarLayoutSelectorWithServices({ servicesManager, commands, ...props }) {
|
||||
const { toolbarService, viewportGridService } = servicesManager.services;
|
||||
|
||||
const onSelection = useCallback(
|
||||
props => {
|
||||
toolbarService.recordInteraction({
|
||||
interactionType: 'action',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setViewportGridLayout',
|
||||
commandOptions: { ...props },
|
||||
context: 'DEFAULT',
|
||||
},
|
||||
],
|
||||
});
|
||||
const onInteraction = useCallback(
|
||||
args => {
|
||||
const viewportId = viewportGridService.getActiveViewportId();
|
||||
const refreshProps = {
|
||||
viewportId,
|
||||
};
|
||||
|
||||
if (props.onLayoutChange) {
|
||||
props.onLayoutChange(args);
|
||||
}
|
||||
toolbarService.recordInteraction({ commands }, { ...args, refreshProps });
|
||||
},
|
||||
[toolbarService]
|
||||
);
|
||||
@@ -25,12 +24,12 @@ function ToolbarLayoutSelectorWithServices({ servicesManager, ...props }) {
|
||||
return (
|
||||
<LayoutSelector
|
||||
{...props}
|
||||
onSelection={onSelection}
|
||||
onInteraction={onInteraction}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function LayoutSelector({ rows, columns, className, onSelection, ...rest }) {
|
||||
function LayoutSelector({ rows, columns, className, onInteraction, ...rest }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const closeOnOutsideClick = () => {
|
||||
@@ -62,7 +61,7 @@ function LayoutSelector({ rows, columns, className, onSelection, ...rest }) {
|
||||
<DropdownContent
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
onSelection={onSelection}
|
||||
onSelection={onInteraction}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { SplitButton, Icon, ToolbarButton } from '@ohif/ui';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { SplitButton, ToolbarButton } from '@ohif/ui';
|
||||
import React, { useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
function ToolbarSplitButtonWithServices({
|
||||
isRadio,
|
||||
isAction,
|
||||
groupId,
|
||||
primary,
|
||||
secondary,
|
||||
@@ -16,123 +13,35 @@ function ToolbarSplitButtonWithServices({
|
||||
}) {
|
||||
const { toolbarService } = servicesManager?.services;
|
||||
|
||||
const handleItemClick = (item, index) => {
|
||||
const { id, type, commands } = item;
|
||||
onInteraction({
|
||||
groupId,
|
||||
itemId: id,
|
||||
interactionType: type,
|
||||
commands,
|
||||
});
|
||||
|
||||
setState(state => ({
|
||||
...state,
|
||||
primary: !isAction && isRadio ? { ...item, index } : state.primary,
|
||||
isExpanded: false,
|
||||
items: getSplitButtonItems(items).filter(item =>
|
||||
isRadio && !isAction ? item.index !== index : true
|
||||
),
|
||||
}));
|
||||
};
|
||||
|
||||
/* Bubbles up individual item clicks */
|
||||
const getSplitButtonItems = items =>
|
||||
items.map((item, index) => ({
|
||||
...item,
|
||||
index,
|
||||
onClick: () => handleItemClick(item, index),
|
||||
}));
|
||||
|
||||
const [buttonsState, setButtonState] = useState({
|
||||
primaryToolId: '',
|
||||
toggles: {},
|
||||
groups: {},
|
||||
});
|
||||
|
||||
const [state, setState] = useState({
|
||||
primary,
|
||||
items: getSplitButtonItems(items).filter(item =>
|
||||
isRadio && !isAction ? item.id !== primary.id : true
|
||||
),
|
||||
});
|
||||
|
||||
const { primaryToolId, toggles } = buttonsState;
|
||||
|
||||
const isPrimaryToggle = state.primary.type === 'toggle';
|
||||
|
||||
const isPrimaryActive =
|
||||
(state.primary.type === 'tool' && primaryToolId === state.primary.id) ||
|
||||
(isPrimaryToggle && toggles[state.primary.id] === true);
|
||||
const getSplitButtonItems = useCallback(
|
||||
items =>
|
||||
items.map((item, index) => ({
|
||||
...item,
|
||||
index,
|
||||
onClick: () => {
|
||||
onInteraction({
|
||||
groupId,
|
||||
itemId: item.id,
|
||||
commands: item.commands,
|
||||
});
|
||||
},
|
||||
})),
|
||||
[]
|
||||
);
|
||||
|
||||
const PrimaryButtonComponent =
|
||||
toolbarService?.getButtonComponentForUIType(state.primary.uiType) ?? ToolbarButton;
|
||||
toolbarService?.getButtonComponentForUIType(primary.uiType) ?? ToolbarButton;
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = toolbarService.subscribe(
|
||||
toolbarService.EVENTS.TOOL_BAR_STATE_MODIFIED,
|
||||
state => {
|
||||
setButtonState({ ...state });
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [toolbarService]);
|
||||
|
||||
const updatedItems = state.items.map(item => {
|
||||
const isActive = item.type === 'tool' && primaryToolId === item.id;
|
||||
|
||||
// We could have added the
|
||||
// item.type === 'toggle' && toggles[item.id] === true
|
||||
// too but that makes the button active when the toggle is active under it
|
||||
// which feels weird
|
||||
return {
|
||||
...item,
|
||||
isActive,
|
||||
};
|
||||
});
|
||||
|
||||
const DefaultListItemRenderer = ({ type, icon, label, t, id }) => {
|
||||
const isActive = type === 'toggle' && toggles[id] === true;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'hover:bg-primary-dark flex h-8 w-full flex-row items-center p-3',
|
||||
'whitespace-pre text-base',
|
||||
isActive && 'bg-primary-dark',
|
||||
isActive
|
||||
? 'text-[#348CFD]'
|
||||
: 'text-common-bright hover:bg-primary-dark hover:text-primary-light'
|
||||
)}
|
||||
>
|
||||
{icon && (
|
||||
<span className="mr-4">
|
||||
<Icon
|
||||
name={icon}
|
||||
className="h-5 w-5"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
<span className="mr-5">{t(label)}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const listItemRenderer = renderer || DefaultListItemRenderer;
|
||||
const listItemRenderer = renderer;
|
||||
|
||||
return (
|
||||
<SplitButton
|
||||
isRadio={isRadio}
|
||||
isAction={isAction}
|
||||
primary={state.primary}
|
||||
primary={primary}
|
||||
secondary={secondary}
|
||||
items={updatedItems}
|
||||
items={getSplitButtonItems(items)}
|
||||
groupId={groupId}
|
||||
renderer={listItemRenderer}
|
||||
isActive={isPrimaryActive || updatedItems.some(item => item.isActive)}
|
||||
isToggle={isPrimaryToggle}
|
||||
onInteraction={onInteraction}
|
||||
Component={props => (
|
||||
<PrimaryButtonComponent
|
||||
@@ -146,11 +55,9 @@ function ToolbarSplitButtonWithServices({
|
||||
|
||||
ToolbarSplitButtonWithServices.propTypes = {
|
||||
isRadio: PropTypes.bool,
|
||||
isAction: PropTypes.bool,
|
||||
groupId: PropTypes.string,
|
||||
primary: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
type: PropTypes.oneOf(['tool', 'action', 'toggle']).isRequired,
|
||||
uiType: PropTypes.string,
|
||||
}),
|
||||
secondary: PropTypes.shape({
|
||||
@@ -158,15 +65,17 @@ ToolbarSplitButtonWithServices.propTypes = {
|
||||
icon: PropTypes.string.isRequired,
|
||||
label: PropTypes.string,
|
||||
tooltip: PropTypes.string.isRequired,
|
||||
isActive: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
}),
|
||||
items: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
type: PropTypes.oneOf(['tool', 'action', 'toggle']).isRequired,
|
||||
icon: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
tooltip: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
})
|
||||
),
|
||||
renderer: PropTypes.func,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ErrorBoundary, UserPreferences, AboutModal, Header, useModal } from '@o
|
||||
import i18n from '@ohif/i18n';
|
||||
import { hotkeys } from '@ohif/core';
|
||||
import { useAppConfig } from '@state';
|
||||
import Toolbar from '../Toolbar/Toolbar';
|
||||
import { Toolbar } from '../Toolbar/Toolbar';
|
||||
|
||||
const { availableLanguages, defaultLanguage, currentLanguage } = i18n;
|
||||
|
||||
|
||||
@@ -48,22 +48,13 @@ function ViewerLayout({
|
||||
const getComponent = id => {
|
||||
const entry = extensionManager.getModuleEntry(id);
|
||||
|
||||
if (!entry) {
|
||||
if (!entry || !entry.component) {
|
||||
throw new Error(
|
||||
`${id} is not valid for an extension module. Please verify your configuration or ensure that the extension is properly registered. It's also possible that your mode is utilizing a module from an extension that hasn't been included in its dependencies (add the extension to the "extensionDependencies" array in your mode's index.js file)`
|
||||
`${id} is not valid for an extension module or no component found from extension ${id}. Please verify your configuration or ensure that the extension is properly registered. It's also possible that your mode is utilizing a module from an extension that hasn't been included in its dependencies (add the extension to the "extensionDependencies" array in your mode's index.js file). Check the reference string to the extension in your Mode configuration`
|
||||
);
|
||||
}
|
||||
|
||||
let content;
|
||||
if (entry && entry.component) {
|
||||
content = entry.component;
|
||||
} else {
|
||||
throw new Error(
|
||||
`No component found from extension ${id}. Check the reference string to the extension in your Mode configuration`
|
||||
);
|
||||
}
|
||||
|
||||
return { entry, content };
|
||||
return { entry, content: entry.component };
|
||||
};
|
||||
|
||||
const getPanelData = id => {
|
||||
@@ -76,6 +67,7 @@ function ViewerLayout({
|
||||
label: entry.label,
|
||||
name: entry.name,
|
||||
content,
|
||||
contexts: entry.contexts,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ export type HangingProtocolParams = {
|
||||
stageIndex?: number;
|
||||
activeStudyUID?: string;
|
||||
stageId?: string;
|
||||
reset?: false;
|
||||
};
|
||||
|
||||
export type UpdateViewportDisplaySetParams = {
|
||||
@@ -107,41 +108,6 @@ const commandsModule = ({
|
||||
measurementService.clear();
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggles off all tools which contain a commandName of setHangingProtocol
|
||||
* or toggleHangingProtocol, and which match/don't match the protocol id/stage
|
||||
*/
|
||||
toggleHpTools: () => {
|
||||
const {
|
||||
protocol,
|
||||
stageIndex: toggleStageIndex,
|
||||
stage,
|
||||
} = hangingProtocolService.getActiveProtocol();
|
||||
const enableListener = button => {
|
||||
if (!button.id) {
|
||||
return;
|
||||
}
|
||||
const { commands, items, primary } = button.props || button;
|
||||
if (primary) {
|
||||
enableListener(primary);
|
||||
}
|
||||
if (items) {
|
||||
items.forEach(enableListener);
|
||||
}
|
||||
const hpCommand = commands?.find?.(isHangingProtocolCommand);
|
||||
if (!hpCommand) {
|
||||
return;
|
||||
}
|
||||
const { protocolId, stageIndex, stageId } = hpCommand.commandOptions;
|
||||
const isActive =
|
||||
(!protocolId || protocolId === protocol.id) &&
|
||||
(stageIndex === undefined || stageIndex === toggleStageIndex) &&
|
||||
(!stageId || stageId === stage.id);
|
||||
toolbarService.setToggled(button.id, isActive);
|
||||
};
|
||||
Object.values(toolbarService.getButtons()).forEach(enableListener);
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the specified protocol
|
||||
* 1. Records any existing state using the viewport grid service
|
||||
@@ -173,14 +139,12 @@ const commandsModule = ({
|
||||
stageIndex,
|
||||
reset = false,
|
||||
}: HangingProtocolParams): boolean => {
|
||||
const primaryToolBeforeHPChange = toolbarService.getActivePrimaryTool();
|
||||
try {
|
||||
// Stores in the state the display set selector id to displaySetUID mapping
|
||||
// Pass in viewportId for the active viewport. This item will get set as
|
||||
// the activeViewportId
|
||||
const state = viewportGridService.getState();
|
||||
const hpInfo = hangingProtocolService.getState();
|
||||
const { protocol: oldProtocol } = hangingProtocolService.getActiveProtocol();
|
||||
const stateSyncReduce = reuseCachedLayouts(state, hangingProtocolService, stateSyncService);
|
||||
const { hangingProtocolStageIndexMap, viewportGridStore, displaySetSelectorMap } =
|
||||
stateSyncReduce;
|
||||
@@ -243,33 +207,9 @@ const commandsModule = ({
|
||||
`${activeStudyUID || hpInfo.activeStudyUID}:activeDisplaySet:0`
|
||||
];
|
||||
stateSyncService.store(stateSyncReduce);
|
||||
// This is a default action applied
|
||||
actions.toggleHpTools();
|
||||
|
||||
// try to use the same tool in the new hanging protocol stage
|
||||
const primaryButton = toolbarService.getButton(primaryToolBeforeHPChange);
|
||||
if (primaryButton) {
|
||||
// is there any type of interaction on this button, if not it might be in the
|
||||
// items. This is a bit of a hack, but it works for now.
|
||||
|
||||
let interactionType = primaryButton.props?.interactionType;
|
||||
|
||||
if (!interactionType && primaryButton.props?.items) {
|
||||
const firstItem = primaryButton.props.items[0];
|
||||
interactionType = firstItem.props?.interactionType || firstItem.props?.type;
|
||||
}
|
||||
|
||||
if (interactionType) {
|
||||
toolbarService.recordInteraction({
|
||||
interactionType,
|
||||
...primaryButton.props,
|
||||
});
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
actions.toggleHpTools();
|
||||
uiNotificationService.show({
|
||||
title: 'Apply Hanging Protocol',
|
||||
message: 'The hanging protocol could not be applied.',
|
||||
@@ -639,56 +579,38 @@ const commandsModule = ({
|
||||
},
|
||||
clearMeasurements: {
|
||||
commandFn: actions.clearMeasurements,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
displayNotification: {
|
||||
commandFn: actions.displayNotification,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
setHangingProtocol: {
|
||||
commandFn: actions.setHangingProtocol,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
toggleHangingProtocol: {
|
||||
commandFn: actions.toggleHangingProtocol,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
navigateHistory: {
|
||||
commandFn: actions.navigateHistory,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
nextStage: {
|
||||
commandFn: actions.deltaStage,
|
||||
storeContexts: [],
|
||||
options: { direction: 1 },
|
||||
},
|
||||
previousStage: {
|
||||
commandFn: actions.deltaStage,
|
||||
storeContexts: [],
|
||||
options: { direction: -1 },
|
||||
},
|
||||
setViewportGridLayout: {
|
||||
commandFn: actions.setViewportGridLayout,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
toggleOneUp: {
|
||||
commandFn: actions.toggleOneUp,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
openDICOMTagViewer: {
|
||||
commandFn: actions.openDICOMTagViewer,
|
||||
},
|
||||
updateViewportDisplaySet: {
|
||||
commandFn: actions.updateViewportDisplaySet,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -93,8 +93,8 @@ export default function getCustomizationModule({ servicesManager, extensionManag
|
||||
instance && this.attribute
|
||||
? instance[this.attribute]
|
||||
: this.contentF && typeof this.contentF === 'function'
|
||||
? this.contentF(props)
|
||||
: null;
|
||||
? this.contentF(props)
|
||||
: null;
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,39 +1,67 @@
|
||||
import ToolbarDivider from './Toolbar/ToolbarDivider';
|
||||
import ToolbarLayoutSelectorWithServices from './Toolbar/ToolbarLayoutSelector';
|
||||
import ToolbarSplitButtonWithServices from './Toolbar/ToolbarSplitButtonWithServices';
|
||||
import ToolbarButtonWithServices from './Toolbar/ToolbarButtonWithServices';
|
||||
import ToolbarButtonGroupWithServices from './Toolbar/ToolbarButtonGroupWithServices';
|
||||
import { ToolbarButton } from '@ohif/ui';
|
||||
|
||||
const getClassName = isToggled => {
|
||||
return {
|
||||
className: isToggled
|
||||
? '!text-primary-active'
|
||||
: '!text-common-bright hover:!bg-primary-dark hover:text-primary-light',
|
||||
};
|
||||
};
|
||||
|
||||
export default function getToolbarModule({ commandsManager, servicesManager }) {
|
||||
const { cineService } = servicesManager.services;
|
||||
return [
|
||||
{
|
||||
name: 'ohif.radioGroup',
|
||||
defaultComponent: ToolbarButton,
|
||||
},
|
||||
{
|
||||
name: 'ohif.divider',
|
||||
defaultComponent: ToolbarDivider,
|
||||
clickHandler: () => {},
|
||||
},
|
||||
{
|
||||
name: 'ohif.action',
|
||||
defaultComponent: ToolbarButtonWithServices,
|
||||
clickHandler: () => {},
|
||||
},
|
||||
{
|
||||
name: 'ohif.radioGroup',
|
||||
defaultComponent: ToolbarButtonWithServices,
|
||||
clickHandler: () => {},
|
||||
},
|
||||
{
|
||||
name: 'ohif.splitButton',
|
||||
defaultComponent: ToolbarSplitButtonWithServices,
|
||||
clickHandler: () => {},
|
||||
},
|
||||
{
|
||||
name: 'ohif.layoutSelector',
|
||||
defaultComponent: ToolbarLayoutSelectorWithServices,
|
||||
clickHandler: (evt, clickedBtn, btnSectionName) => {},
|
||||
},
|
||||
{
|
||||
name: 'ohif.toggle',
|
||||
defaultComponent: ToolbarButtonWithServices,
|
||||
clickHandler: () => {},
|
||||
name: 'ohif.buttonGroup',
|
||||
defaultComponent: ToolbarButtonGroupWithServices,
|
||||
},
|
||||
{
|
||||
name: 'evaluate.group.promoteToPrimary',
|
||||
evaluate: ({ viewportId, button, itemId }) => {
|
||||
const { items } = button.props;
|
||||
|
||||
if (!itemId) {
|
||||
return {
|
||||
primary: button.props.primary,
|
||||
items,
|
||||
};
|
||||
}
|
||||
|
||||
// other wise we can move the clicked tool to the primary button
|
||||
const clickedItemProps = items.find(item => item.id === itemId || item.itemId === itemId);
|
||||
|
||||
return {
|
||||
primary: clickedItemProps,
|
||||
items,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'evaluate.cine',
|
||||
evaluate: () => {
|
||||
const isToggled = cineService.getState().isCineEnabled;
|
||||
return getClassName(isToggled);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -10,8 +10,13 @@ const metadataProvider = classes.MetadataProvider;
|
||||
* @param {Object} servicesManager
|
||||
* @param {Object} configuration
|
||||
*/
|
||||
export default function init({ servicesManager, configuration = {} }): void {
|
||||
const { stateSyncService } = servicesManager.services;
|
||||
export default function init({ servicesManager, configuration = {}, commandsManager }): void {
|
||||
const { stateSyncService, toolbarService, cineService, viewportGridService } =
|
||||
servicesManager.services;
|
||||
|
||||
toolbarService.registerEventForToolbarUpdate(cineService, [
|
||||
cineService.EVENTS.CINE_STATE_CHANGED,
|
||||
]);
|
||||
// Add
|
||||
DicomMetadataStore.subscribe(DicomMetadataStore.EVENTS.INSTANCES_ADDED, handlePETImageMetadata);
|
||||
|
||||
@@ -24,6 +29,10 @@ export default function init({ servicesManager, configuration = {} }): void {
|
||||
// Used to recover manual changes to the layout of a stage.
|
||||
stateSyncService.register('viewportGridStore', { clearOnModeExit: true });
|
||||
|
||||
// uiStateStore is a sync state which stores the relevant
|
||||
// UI state for the viewer
|
||||
stateSyncService.register('uiStateStore', { clearOnModeExit: true });
|
||||
|
||||
// displaySetSelectorMap stores a map from
|
||||
// `<activeStudyUID>:<displaySetSelectorId>:<matchOffset>` to
|
||||
// a displaySetInstanceUID, used to display named display sets in
|
||||
@@ -38,7 +47,7 @@ export default function init({ servicesManager, configuration = {} }): void {
|
||||
});
|
||||
|
||||
// Stores a map from the to be applied hanging protocols `<activeStudyUID>:<protocolId>`
|
||||
// to the previously applied hanging protolStageIndexMap key, in order to toggle
|
||||
// to the previously applied hanging protocolStageIndexMap key, in order to toggle
|
||||
// off the applied protocol and remember the old state.
|
||||
stateSyncService.register('toggleHangingProtocol', { clearOnModeExit: true });
|
||||
|
||||
@@ -46,6 +55,45 @@ export default function init({ servicesManager, configuration = {} }): void {
|
||||
// changes numRows and numCols, the viewports can be remembers and then replaced
|
||||
// afterwards.
|
||||
stateSyncService.register('viewportsByPosition', { clearOnModeExit: true });
|
||||
|
||||
// Function to process and subscribe to events for a given set of commands and listeners
|
||||
const subscribeToEvents = listeners => {
|
||||
Object.entries(listeners).forEach(([event, commands]) => {
|
||||
const supportedEvents = [
|
||||
viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED,
|
||||
viewportGridService.EVENTS.VIEWPORTS_READY,
|
||||
];
|
||||
|
||||
if (supportedEvents.includes(event)) {
|
||||
viewportGridService.subscribe(event, eventData => {
|
||||
const viewportId = eventData?.viewportId ?? viewportGridService.getActiveViewportId();
|
||||
|
||||
commandsManager.run(commands, { viewportId });
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
toolbarService.subscribe(toolbarService.EVENTS.TOOL_BAR_MODIFIED, state => {
|
||||
const { buttons } = state;
|
||||
for (const [id, button] of Object.entries(buttons)) {
|
||||
const { groupId, items, listeners } = button.props;
|
||||
|
||||
// Handle group items' listeners
|
||||
if (groupId && items) {
|
||||
items.forEach(item => {
|
||||
if (item.listeners) {
|
||||
subscribeToEvents(item.listeners);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle button listeners
|
||||
if (listeners) {
|
||||
subscribeToEvents(listeners);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const handlePETImageMetadata = ({ SeriesInstanceUID, StudyInstanceUID }) => {
|
||||
|
||||
Reference in new issue
Block a user