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
@@ -8,7 +8,6 @@ describe('CommandsManager', () => {
|
||||
contextName = 'VTK',
|
||||
command = {
|
||||
commandFn: jest.fn().mockReturnValue(true),
|
||||
storeContexts: ['viewers'],
|
||||
options: { passMeToCommandFn: ':wave:' },
|
||||
},
|
||||
commandsManagerConfig = {
|
||||
@@ -139,7 +138,6 @@ describe('CommandsManager', () => {
|
||||
it('Logs a warning if command definition does not have a commandFn', () => {
|
||||
const commandWithNoCommmandFn = {
|
||||
commandFn: undefined,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import log from '../log.js';
|
||||
import { Command, Commands } from '../types/Command';
|
||||
import { Command, Commands, ComplexCommand } from '../types/Command';
|
||||
|
||||
/**
|
||||
* The definition of a command
|
||||
@@ -167,28 +167,44 @@ export class CommandsManager {
|
||||
* Run one or more commands with specified extra options.
|
||||
* Returns the result of the last command run.
|
||||
*
|
||||
* @param toRun - A specification of one or more commands
|
||||
* @param toRun - A specification of one or more commands,
|
||||
* typically an object of { commandName, commandOptions, context }
|
||||
* or an array of such objects. It can also be a single commandName as string
|
||||
* if no options are needed.
|
||||
* @param options - to include in the commands run beyond
|
||||
* the commandOptions specified in the base.
|
||||
*/
|
||||
public run(
|
||||
toRun: Command | Commands | Command[] | undefined,
|
||||
toRun: Command | Commands | Command[] | string | undefined,
|
||||
options?: Record<string, unknown>
|
||||
): unknown {
|
||||
if (!toRun) {
|
||||
return;
|
||||
}
|
||||
const commands =
|
||||
(Array.isArray(toRun) && toRun) ||
|
||||
((toRun as Command).commandName && [toRun]) ||
|
||||
(Array.isArray((toRun as Commands).commands) && (toRun as Commands).commands);
|
||||
if (!commands) {
|
||||
|
||||
// Normalize `toRun` to an array of `ComplexCommand`
|
||||
let commands: ComplexCommand[] = [];
|
||||
if (typeof toRun === 'string') {
|
||||
commands = [{ commandName: toRun }];
|
||||
} else if ('commandName' in toRun) {
|
||||
commands = [toRun as ComplexCommand];
|
||||
} else if ('commands' in toRun) {
|
||||
const commandsInput = (toRun as Commands).commands;
|
||||
commands = Array.isArray(commandsInput)
|
||||
? commandsInput.map(cmd => (typeof cmd === 'string' ? { commandName: cmd } : cmd))
|
||||
: [{ commandName: commandsInput }];
|
||||
} else if (Array.isArray(toRun)) {
|
||||
commands = toRun.map(cmd => (typeof cmd === 'string' ? { commandName: cmd } : cmd));
|
||||
}
|
||||
|
||||
if (commands.length === 0) {
|
||||
console.log("Command isn't runnable", toRun);
|
||||
return;
|
||||
}
|
||||
|
||||
let result;
|
||||
(commands as Command[]).forEach(({ commandName, commandOptions, context }) => {
|
||||
// Execute each command in the array
|
||||
let result: unknown;
|
||||
commands.forEach(({ commandName, commandOptions, context }) => {
|
||||
if (commandName) {
|
||||
result = this.runCommand(
|
||||
commandName,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import objectHash from 'object-hash';
|
||||
import log from '../log.js';
|
||||
import { hotkeys } from '../utils';
|
||||
import isequal from 'lodash.isequal';
|
||||
import Hotkey from './Hotkey';
|
||||
@@ -276,10 +275,3 @@ export class HotkeysManager {
|
||||
}
|
||||
|
||||
export default HotkeysManager;
|
||||
|
||||
// Commands Contexts:
|
||||
|
||||
// --> Name and Priority
|
||||
// GLOBAL: 0
|
||||
// VIEWER::CORNERSTONE: 1
|
||||
// VIEWER::VTK: 1
|
||||
@@ -247,7 +247,6 @@ describe('ExtensionManager.ts', () => {
|
||||
definitions: {
|
||||
exampleDefinition: {
|
||||
commandFn: () => {},
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -45,6 +45,7 @@ export interface Extension {
|
||||
getUtilityModule?: (p: ExtensionParams) => unknown;
|
||||
getCustomizationModule?: (p: ExtensionParams) => unknown;
|
||||
getSopClassHandlerModule?: (p: ExtensionParams) => unknown;
|
||||
getToolbarModule?: (p: ExtensionParams) => unknown;
|
||||
onModeEnter?: () => void;
|
||||
onModeExit?: () => void;
|
||||
}
|
||||
@@ -65,9 +66,24 @@ export default class ExtensionManager extends PubSubService {
|
||||
ACTIVE_DATA_SOURCE_CHANGED: 'event::activedatasourcechanged',
|
||||
};
|
||||
|
||||
public static readonly MODULE_TYPES = MODULE_TYPES;
|
||||
|
||||
private _commandsManager: CommandsManager;
|
||||
private _servicesManager: ServicesManager;
|
||||
private _hotkeysManager: HotkeysManager;
|
||||
private modulesMap: Record<string, unknown>;
|
||||
private modules: Record<string, any[]>;
|
||||
private registeredExtensionIds: string[];
|
||||
private moduleTypeNames: string[];
|
||||
private _appConfig: any;
|
||||
private _extensionLifeCycleHooks: {
|
||||
onModeEnter: Record<string, any>;
|
||||
onModeExit: Record<string, any>;
|
||||
};
|
||||
private dataSourceMap: Record<string, any>;
|
||||
private dataSourceDefs: Record<string, any>;
|
||||
private defaultDataSourceName: string;
|
||||
private activeDataSource: string;
|
||||
|
||||
constructor({
|
||||
commandsManager,
|
||||
@@ -265,57 +281,74 @@ export default class ExtensionManager extends PubSubService {
|
||||
configuration
|
||||
);
|
||||
|
||||
if (extensionModule) {
|
||||
switch (moduleType) {
|
||||
case MODULE_TYPES.COMMANDS:
|
||||
this._initCommandsModule(extensionModule);
|
||||
break;
|
||||
case MODULE_TYPES.DATA_SOURCE:
|
||||
this._initDataSourcesModule(extensionModule, extensionId, dataSources);
|
||||
break;
|
||||
case MODULE_TYPES.HANGING_PROTOCOL:
|
||||
this._initHangingProtocolsModule(extensionModule, extensionId);
|
||||
case MODULE_TYPES.TOOLBAR:
|
||||
case MODULE_TYPES.VIEWPORT:
|
||||
case MODULE_TYPES.PANEL:
|
||||
case MODULE_TYPES.SOP_CLASS_HANDLER:
|
||||
case MODULE_TYPES.CONTEXT:
|
||||
case MODULE_TYPES.LAYOUT_TEMPLATE:
|
||||
case MODULE_TYPES.CUSTOMIZATION:
|
||||
case MODULE_TYPES.STATE_SYNC:
|
||||
case MODULE_TYPES.UTILITY:
|
||||
// Default for most extension points,
|
||||
// Just adds each entry ready for consumption by mode.
|
||||
extensionModule.forEach(element => {
|
||||
if (!element.name) {
|
||||
throw new Error(
|
||||
`Extension ID ${extensionId} module ${moduleType} element has no name`
|
||||
);
|
||||
}
|
||||
const id = `${extensionId}.${moduleType}.${element.name}`;
|
||||
element.id = id;
|
||||
this.modulesMap[id] = element;
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Module type invalid: ${moduleType}`);
|
||||
}
|
||||
|
||||
this.modules[moduleType].push({
|
||||
extensionId,
|
||||
module: extensionModule,
|
||||
});
|
||||
if (!extensionModule) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (moduleType) {
|
||||
case MODULE_TYPES.COMMANDS:
|
||||
this._initCommandsModule(extensionModule);
|
||||
break;
|
||||
|
||||
case MODULE_TYPES.DATA_SOURCE:
|
||||
this._initDataSourcesModule(extensionModule, extensionId, dataSources);
|
||||
break;
|
||||
|
||||
case MODULE_TYPES.HANGING_PROTOCOL:
|
||||
this._initHangingProtocolsModule(extensionModule, extensionId);
|
||||
break;
|
||||
|
||||
case MODULE_TYPES.PANEL:
|
||||
this._initPanelModule(extensionModule, extensionId);
|
||||
break;
|
||||
|
||||
case MODULE_TYPES.TOOLBAR:
|
||||
this._initToolbarModule(extensionModule, extensionId);
|
||||
break;
|
||||
|
||||
case MODULE_TYPES.VIEWPORT:
|
||||
case MODULE_TYPES.SOP_CLASS_HANDLER:
|
||||
case MODULE_TYPES.CONTEXT:
|
||||
case MODULE_TYPES.LAYOUT_TEMPLATE:
|
||||
case MODULE_TYPES.CUSTOMIZATION:
|
||||
case MODULE_TYPES.STATE_SYNC:
|
||||
case MODULE_TYPES.UTILITY:
|
||||
this.processExtensionModule(extensionModule, extensionId, moduleType);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Module type invalid: ${moduleType}`);
|
||||
}
|
||||
|
||||
this.modules[moduleType].push({
|
||||
extensionId,
|
||||
module: extensionModule,
|
||||
});
|
||||
});
|
||||
|
||||
// Track extension registration
|
||||
this.registeredExtensionIds.push(extensionId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the module entry associated with the given string entry
|
||||
* @param stringEntry - The string entry to retrieve the module entry for which is
|
||||
* in the format of `${extensionId}.${moduleType}.${moduleName}`
|
||||
* @returns The module entry associated with the given string entry.
|
||||
*/
|
||||
getModuleEntry = stringEntry => {
|
||||
return this.modulesMap[stringEntry];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves all modules of a given type for all registered extensions.
|
||||
*
|
||||
* @param moduleType - The type of modules to retrieve.
|
||||
* @returns An array of modules of the specified type.
|
||||
*/
|
||||
getModulesByType = (moduleType: string) => {
|
||||
return this.modules[moduleType];
|
||||
};
|
||||
|
||||
getDataSources = dataSourceName => {
|
||||
if (dataSourceName === undefined) {
|
||||
// Default to the activeDataSource
|
||||
@@ -402,6 +435,38 @@ export default class ExtensionManager extends PubSubService {
|
||||
});
|
||||
};
|
||||
|
||||
_initPanelModule = (extensionModule, extensionId) => {
|
||||
this.processExtensionModule(extensionModule, extensionId, MODULE_TYPES.PANEL);
|
||||
};
|
||||
|
||||
_initToolbarModule = (extensionModule, extensionId) => {
|
||||
// check if the toolbar module has a handler function for evaluation of
|
||||
// the toolbar button state
|
||||
const { toolbarService } = this._servicesManager.services;
|
||||
extensionModule.forEach(toolbarButton => {
|
||||
if (toolbarButton.evaluate) {
|
||||
toolbarService.registerEvaluateFunction(toolbarButton.name, toolbarButton.evaluate);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Processes an extension module.
|
||||
* @param extensionModule - The extension module to process.
|
||||
* @param extensionId - The ID of the extension.
|
||||
* @param moduleType - The type of the module.
|
||||
*/
|
||||
private processExtensionModule(extensionModule, extensionId: string, moduleType: string) {
|
||||
extensionModule.forEach(element => {
|
||||
if (!element.name) {
|
||||
throw new Error(`Extension ID ${extensionId} module ${moduleType} element has no name`);
|
||||
}
|
||||
const id = `${extensionId}.${moduleType}.${element.name}`;
|
||||
element.id = id;
|
||||
this.modulesMap[id] = element;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given data source and optionally sets it as the active data source.
|
||||
* The method does this by first creating the data source.
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
export function useToolbar({ servicesManager, buttonSection = 'primary' }) {
|
||||
const { toolbarService, viewportGridService } = servicesManager.services;
|
||||
const { EVENTS } = toolbarService;
|
||||
|
||||
const [toolbarButtons, setToolbarButtons] = useState(
|
||||
toolbarService.getButtonSection(buttonSection)
|
||||
);
|
||||
|
||||
// Callback function for handling toolbar interactions
|
||||
const onInteraction = useCallback(
|
||||
args => {
|
||||
const viewportId = viewportGridService.getActiveViewportId();
|
||||
const refreshProps = {
|
||||
viewportId,
|
||||
};
|
||||
toolbarService.recordInteraction(args, {
|
||||
refreshProps,
|
||||
});
|
||||
},
|
||||
[toolbarService, viewportGridService]
|
||||
);
|
||||
|
||||
// Effect to handle toolbar modification events
|
||||
useEffect(() => {
|
||||
const handleToolbarModified = () => {
|
||||
setToolbarButtons(toolbarService.getButtonSection(buttonSection));
|
||||
};
|
||||
|
||||
const subs = [EVENTS.TOOL_BAR_MODIFIED, EVENTS.TOOL_BAR_STATE_MODIFIED].map(event => {
|
||||
return toolbarService.subscribe(event, handleToolbarModified);
|
||||
});
|
||||
|
||||
return () => {
|
||||
subs.forEach(sub => sub.unsubscribe());
|
||||
};
|
||||
}, [toolbarService]);
|
||||
|
||||
// Effect to handle active viewportId change event
|
||||
useEffect(() => {
|
||||
const events = [
|
||||
viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED,
|
||||
viewportGridService.EVENTS.VIEWPORTS_READY,
|
||||
];
|
||||
|
||||
const subscriptions = events.map(event => {
|
||||
return viewportGridService.subscribe(event, ({ viewportId }) => {
|
||||
viewportId = viewportId || viewportGridService.getActiveViewportId();
|
||||
toolbarService.refreshToolbarState({ viewportId });
|
||||
});
|
||||
});
|
||||
|
||||
return () => subscriptions.forEach(sub => sub.unsubscribe());
|
||||
}, [viewportGridService, toolbarService]);
|
||||
|
||||
return { toolbarButtons, onInteraction };
|
||||
}
|
||||
@@ -45,6 +45,7 @@ describe('Top level exports', () => {
|
||||
'pubSubServiceInterface',
|
||||
'PubSubService',
|
||||
'PanelService',
|
||||
'useToolbar',
|
||||
].sort();
|
||||
|
||||
const exports = Object.keys(OHIF).sort();
|
||||
|
||||
@@ -12,6 +12,7 @@ import utils from './utils';
|
||||
import defaults from './defaults';
|
||||
import * as Types from './types';
|
||||
import * as Enums from './enums';
|
||||
import { useToolbar } from './hooks/useToolbar';
|
||||
|
||||
import {
|
||||
CineService,
|
||||
@@ -81,6 +82,7 @@ const OHIF = {
|
||||
pubSubServiceInterface,
|
||||
PubSubService,
|
||||
PanelService,
|
||||
useToolbar,
|
||||
};
|
||||
|
||||
export {
|
||||
@@ -124,6 +126,7 @@ export {
|
||||
Enums,
|
||||
Types,
|
||||
PanelService,
|
||||
useToolbar,
|
||||
};
|
||||
|
||||
export { OHIF };
|
||||
|
||||
@@ -1,82 +1,80 @@
|
||||
const name = 'CineService';
|
||||
import { PubSubService } from '../_shared/pubSubServiceInterface';
|
||||
|
||||
const publicAPI = {
|
||||
name,
|
||||
getState: _getState,
|
||||
setCine: _setCine,
|
||||
setIsCineEnabled: _setIsCineEnabled,
|
||||
playClip: _playClip,
|
||||
stopClip: _stopClip,
|
||||
onModeExit: _onModeExit,
|
||||
setServiceImplementation,
|
||||
};
|
||||
class CineService extends PubSubService {
|
||||
public static readonly EVENTS = {
|
||||
CINE_STATE_CHANGED: 'event::cineStateChanged',
|
||||
};
|
||||
|
||||
const serviceImplementation = {
|
||||
_getState: () => console.warn('getState() NOT IMPLEMENTED'),
|
||||
_setCine: () => console.warn('setCine() NOT IMPLEMENTED'),
|
||||
_playClip: () => console.warn('playClip() NOT IMPLEMENTED'),
|
||||
_stopClip: () => console.warn('stopClip() NOT IMPLEMENTED'),
|
||||
_setIsCineEnabled: () => console.warn('setIsCineEnabled() NOT IMPLEMENTED'),
|
||||
};
|
||||
|
||||
function _getState() {
|
||||
return serviceImplementation._getState();
|
||||
}
|
||||
|
||||
function _setCine({ id, frameRate, isPlaying }) {
|
||||
return serviceImplementation._setCine({ id, frameRate, isPlaying });
|
||||
}
|
||||
|
||||
function _setIsCineEnabled(isCineEnabled) {
|
||||
return serviceImplementation._setIsCineEnabled(isCineEnabled);
|
||||
}
|
||||
|
||||
function _playClip(element, playClipOptions) {
|
||||
return serviceImplementation._playClip(element, playClipOptions);
|
||||
}
|
||||
|
||||
function _stopClip(element) {
|
||||
return serviceImplementation._stopClip(element);
|
||||
}
|
||||
|
||||
function _onModeExit() {
|
||||
_setIsCineEnabled(false);
|
||||
}
|
||||
|
||||
function setServiceImplementation({
|
||||
getState: getStateImplementation,
|
||||
setCine: setCineImplementation,
|
||||
setIsCineEnabled: setIsCineEnabledImplementation,
|
||||
playClip: playClipImplementation,
|
||||
stopClip: stopClipImplementation,
|
||||
}) {
|
||||
if (getStateImplementation) {
|
||||
serviceImplementation._getState = getStateImplementation;
|
||||
}
|
||||
if (setCineImplementation) {
|
||||
serviceImplementation._setCine = setCineImplementation;
|
||||
}
|
||||
if (setIsCineEnabledImplementation) {
|
||||
serviceImplementation._setIsCineEnabled = setIsCineEnabledImplementation;
|
||||
}
|
||||
|
||||
if (playClipImplementation) {
|
||||
serviceImplementation._playClip = playClipImplementation;
|
||||
}
|
||||
|
||||
if (stopClipImplementation) {
|
||||
serviceImplementation._stopClip = stopClipImplementation;
|
||||
}
|
||||
}
|
||||
|
||||
const CineService = {
|
||||
REGISTRATION: {
|
||||
altName: name,
|
||||
public static REGISTRATION = {
|
||||
name: 'cineService',
|
||||
altName: 'CineService',
|
||||
create: ({ configuration = {} }) => {
|
||||
return publicAPI;
|
||||
return new CineService();
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
serviceImplementation = {};
|
||||
|
||||
constructor() {
|
||||
super(CineService.EVENTS);
|
||||
this.serviceImplementation = {};
|
||||
}
|
||||
|
||||
public getState() {
|
||||
return this.serviceImplementation._getState();
|
||||
}
|
||||
|
||||
public setCine({ id, frameRate, isPlaying }) {
|
||||
return this.serviceImplementation._setCine({ id, frameRate, isPlaying });
|
||||
}
|
||||
|
||||
public setIsCineEnabled(isCineEnabled) {
|
||||
this.serviceImplementation._setIsCineEnabled(isCineEnabled);
|
||||
// Todo: for some reason i need to do this setTimeout since the
|
||||
// reducer state does not get updated right away and if we publish the
|
||||
// event and we use the cineService.getState() it will return the old state
|
||||
setTimeout(() => {
|
||||
this._broadcastEvent(this.EVENTS.CINE_STATE_CHANGED, isCineEnabled);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
public playClip(element, playClipOptions) {
|
||||
return this.serviceImplementation._playClip(element, playClipOptions);
|
||||
}
|
||||
|
||||
public stopClip(element) {
|
||||
return this.serviceImplementation._stopClip(element);
|
||||
}
|
||||
|
||||
public _onModeExit() {
|
||||
this.setIsCineEnabled(false);
|
||||
}
|
||||
|
||||
public setServiceImplementation({
|
||||
getState: getStateImplementation,
|
||||
setCine: setCineImplementation,
|
||||
setIsCineEnabled: setIsCineEnabledImplementation,
|
||||
playClip: playClipImplementation,
|
||||
stopClip: stopClipImplementation,
|
||||
}) {
|
||||
if (getStateImplementation) {
|
||||
this.serviceImplementation._getState = getStateImplementation;
|
||||
}
|
||||
if (setCineImplementation) {
|
||||
this.serviceImplementation._setCine = setCineImplementation;
|
||||
}
|
||||
if (setIsCineEnabledImplementation) {
|
||||
this.serviceImplementation._setIsCineEnabled = setIsCineEnabledImplementation;
|
||||
}
|
||||
|
||||
if (playClipImplementation) {
|
||||
this.serviceImplementation._playClip = playClipImplementation;
|
||||
}
|
||||
|
||||
if (stopClipImplementation) {
|
||||
this.serviceImplementation._stopClip = stopClipImplementation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default CineService;
|
||||
@@ -135,15 +135,15 @@ export default class CustomizationService extends PubSubService {
|
||||
return this.getModeCustomization(customizationId, defaultValue);
|
||||
}
|
||||
|
||||
/** Mode customizations are changes to the behaviour of the extensions
|
||||
/** Mode customizations are changes to the behavior of the extensions
|
||||
* when running in a given mode. Reset clears mode customizations.
|
||||
* Note that global customizations over-ride mode customizations.
|
||||
* @param defautlValue to return if no customization specified.
|
||||
* @param defaultValue to return if no customization specified.
|
||||
*/
|
||||
public getModeCustomization(
|
||||
customizationId: string,
|
||||
defaultValue?: Customization
|
||||
): Customization | void {
|
||||
): Customization {
|
||||
const customization =
|
||||
this.globalCustomizations[customizationId] ??
|
||||
this.modeCustomizations[customizationId] ??
|
||||
|
||||
@@ -113,7 +113,7 @@ class MeasurementService extends PubSubService {
|
||||
public readonly VALUE_TYPES = VALUE_TYPES;
|
||||
|
||||
private measurements = new Map();
|
||||
private unmappedMeasurements = new Set();
|
||||
private unmappedMeasurements = new Map();
|
||||
|
||||
constructor() {
|
||||
super(EVENTS);
|
||||
@@ -485,7 +485,15 @@ class MeasurementService extends PubSubService {
|
||||
measurement = toMeasurementSchema(sourceAnnotationDetail);
|
||||
measurement.source = source;
|
||||
} catch (error) {
|
||||
this.unmappedMeasurements.add(sourceAnnotationDetail.uid);
|
||||
// Todo: handle other
|
||||
this.unmappedMeasurements.set(sourceAnnotationDetail.uid, {
|
||||
...sourceAnnotationDetail,
|
||||
source: {
|
||||
name: source.name,
|
||||
version: source.version,
|
||||
uid: source.uid,
|
||||
},
|
||||
});
|
||||
|
||||
console.log('Failed to map', error);
|
||||
throw new Error(
|
||||
@@ -567,10 +575,9 @@ class MeasurementService extends PubSubService {
|
||||
}
|
||||
|
||||
clearMeasurements() {
|
||||
this.unmappedMeasurements.clear();
|
||||
|
||||
// Make a copy of the measurements
|
||||
const measurements = [...this.measurements.values()];
|
||||
const measurements = [...this.measurements.values(), ...this.unmappedMeasurements.values()];
|
||||
this.unmappedMeasurements.clear();
|
||||
this.measurements.clear();
|
||||
this._broadcastEvent(this.EVENTS.MEASUREMENTS_CLEARED, { measurements });
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import log from './../log.js';
|
||||
import Services from '../types/Services';
|
||||
import CommandsManager from '../classes/CommandsManager';
|
||||
import ExtensionManager from '../extensions/ExtensionManager';
|
||||
|
||||
export default class ServicesManager {
|
||||
public services: Services = {};
|
||||
public registeredServiceNames: string[] = [];
|
||||
private _commandsManager: CommandsManager;
|
||||
private _extensionManager: ExtensionManager;
|
||||
|
||||
constructor(commandsManager: CommandsManager) {
|
||||
this._commandsManager = commandsManager;
|
||||
@@ -11,6 +15,10 @@ export default class ServicesManager {
|
||||
this.registeredServiceNames = [];
|
||||
}
|
||||
|
||||
setExtensionManager(extensionManager) {
|
||||
this._extensionManager = extensionManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new service.
|
||||
*
|
||||
@@ -40,6 +48,7 @@ export default class ServicesManager {
|
||||
configuration,
|
||||
commandsManager: this._commandsManager,
|
||||
servicesManager: this,
|
||||
extensionManager: this._extensionManager,
|
||||
});
|
||||
if (service.altName) {
|
||||
console.log('Registering old name', service.altName);
|
||||
|
||||
@@ -1,109 +1,79 @@
|
||||
import merge from 'lodash.merge';
|
||||
import { CommandsManager } from '../../classes';
|
||||
import { ExtensionManager } from '../../extensions';
|
||||
import { PubSubService } from '../_shared/pubSubServiceInterface';
|
||||
import type { RunCommand, Commands } from '../../types/Command';
|
||||
import type { RunCommand } from '../../types/Command';
|
||||
import ServicesManager from '../ServicesManager';
|
||||
import { Button, ButtonProps, EvaluateFunction, EvaluatePublic, NestedButtonProps } from './types';
|
||||
|
||||
const EVENTS = {
|
||||
TOOL_BAR_MODIFIED: 'event::toolBarService:toolBarModified',
|
||||
TOOL_BAR_STATE_MODIFIED: 'event::toolBarService:toolBarStateModified',
|
||||
};
|
||||
|
||||
export type ButtonListeners = Record<string, RunCommand>;
|
||||
|
||||
export interface ButtonProps {
|
||||
primary?: Button;
|
||||
secondary?: Button;
|
||||
items?: Button[];
|
||||
}
|
||||
|
||||
export interface Button extends Commands {
|
||||
id: string;
|
||||
icon?: string;
|
||||
label?: string;
|
||||
type?: string;
|
||||
tooltip?: string;
|
||||
isActive?: boolean;
|
||||
listeners?: ButtonListeners;
|
||||
props?: ButtonProps;
|
||||
}
|
||||
|
||||
export interface ExtraButtonOptions {
|
||||
listeners?: ButtonListeners;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export default class ToolbarService extends PubSubService {
|
||||
public static REGISTRATION = {
|
||||
name: 'toolbarService',
|
||||
// Note the old name is ToolBarService, with an upper B
|
||||
altName: 'ToolBarService',
|
||||
create: ({ commandsManager }) => {
|
||||
return new ToolbarService(commandsManager);
|
||||
create: ({ commandsManager, extensionManager, servicesManager }) => {
|
||||
return new ToolbarService(commandsManager, extensionManager, servicesManager);
|
||||
},
|
||||
};
|
||||
|
||||
public static _createButton(
|
||||
type: string,
|
||||
id: string,
|
||||
icon: string,
|
||||
label: string,
|
||||
commands: Command | Commands,
|
||||
tooltip?: string,
|
||||
extraOptions?: ExtraButtonOptions
|
||||
): Button {
|
||||
public static createButton(options: {
|
||||
id: string;
|
||||
label: string;
|
||||
commands: RunCommand;
|
||||
icon?: string;
|
||||
tooltip?: string;
|
||||
evaluate?: EvaluatePublic;
|
||||
listeners?: Record<string, RunCommand>;
|
||||
}): ButtonProps {
|
||||
const { id, icon, label, commands, tooltip, evaluate, listeners } = options;
|
||||
return {
|
||||
id,
|
||||
icon,
|
||||
label,
|
||||
type,
|
||||
commands,
|
||||
tooltip,
|
||||
...extraOptions,
|
||||
tooltip: tooltip || label,
|
||||
evaluate,
|
||||
listeners,
|
||||
};
|
||||
}
|
||||
|
||||
public static _createActionButton = ToolbarService._createButton.bind(null, 'action');
|
||||
public static _createToggleButton = ToolbarService._createButton.bind(null, 'toggle');
|
||||
public static _createToolButton = ToolbarService._createButton.bind(null, 'tool');
|
||||
|
||||
buttons: Record<string, Button> = {};
|
||||
state: {
|
||||
primaryToolId: string;
|
||||
toggles: Record<string, boolean>;
|
||||
groups: Record<string, unknown>;
|
||||
} = { primaryToolId: '', toggles: {}, groups: {} };
|
||||
|
||||
buttonSections: Record<string, unknown> = {
|
||||
/**
|
||||
* primary: ['Zoom', 'Wwwc'],
|
||||
* secondary: ['Length', 'RectangleRoi']
|
||||
*/
|
||||
// all buttons in the toolbar with their props
|
||||
buttons: Record<string, Button>;
|
||||
// the buttons in the toolbar, grouped by section, with their ids
|
||||
buttonSections: Record<string, string[]>;
|
||||
} = {
|
||||
buttons: {},
|
||||
buttonSections: {},
|
||||
};
|
||||
|
||||
_commandsManager: CommandsManager;
|
||||
extensionManager: ExtensionManager;
|
||||
_extensionManager: ExtensionManager;
|
||||
_servicesManager: ServicesManager;
|
||||
_evaluateFunction: Record<string, EvaluateFunction> = {};
|
||||
_serviceSubscriptions = [];
|
||||
|
||||
defaultTool: Record<string, unknown>;
|
||||
|
||||
constructor(commandsManager: CommandsManager) {
|
||||
constructor(
|
||||
commandsManager: CommandsManager,
|
||||
extensionManager: ExtensionManager,
|
||||
servicesManager: ServicesManager
|
||||
) {
|
||||
super(EVENTS);
|
||||
this._commandsManager = commandsManager;
|
||||
}
|
||||
|
||||
public init(extensionManager: ExtensionManager): void {
|
||||
this.extensionManager = extensionManager;
|
||||
this._extensionManager = extensionManager;
|
||||
this._servicesManager = servicesManager;
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this.unsubscriptions.forEach(unsub => unsub());
|
||||
this.state = {
|
||||
primaryToolId: 'WindowLevel',
|
||||
toggles: {},
|
||||
groups: {},
|
||||
buttons: {},
|
||||
buttonSections: {},
|
||||
};
|
||||
this.unsubscriptions = [];
|
||||
this.buttonSections = {};
|
||||
this.buttons = {};
|
||||
}
|
||||
|
||||
public onModeEnter(): void {
|
||||
@@ -111,16 +81,52 @@ export default class ToolbarService extends PubSubService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the default tool that will be activated whenever the primary tool is
|
||||
* deactivated without activating another/different tool.
|
||||
* @param interaction the interaction command that will set the default tool active
|
||||
* Registers an evaluate function with the specified name.
|
||||
*
|
||||
* @param name - The name of the evaluate function.
|
||||
* @param handler - The evaluate function handler.
|
||||
*/
|
||||
public setDefaultTool(interaction) {
|
||||
this.defaultTool = interaction;
|
||||
public registerEvaluateFunction(name: string, handler: EvaluateFunction) {
|
||||
this._evaluateFunction[name] = handler;
|
||||
}
|
||||
|
||||
public getDefaultTool() {
|
||||
return this.defaultTool;
|
||||
/**
|
||||
* Registers a service and its event to listen for updates and refreshes the toolbar state when the event is triggered.
|
||||
* @param service - The service to register.
|
||||
* @param event - The event to listen for.
|
||||
*/
|
||||
public registerEventForToolbarUpdate(service, events) {
|
||||
const { viewportGridService } = this._servicesManager.services;
|
||||
const callback = () => {
|
||||
const viewportId = viewportGridService.getActiveViewportId();
|
||||
this.refreshToolbarState({ viewportId });
|
||||
};
|
||||
|
||||
const unsubscriptions = events.map(event => {
|
||||
if (service.subscribe) {
|
||||
return service.subscribe(event, callback);
|
||||
} else if (service.addEventListener) {
|
||||
return service.addEventListener(event, callback);
|
||||
}
|
||||
});
|
||||
|
||||
unsubscriptions.forEach(unsub => this._serviceSubscriptions.push(unsub));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds buttons to the toolbar.
|
||||
* @param buttons - The buttons to be added.
|
||||
*/
|
||||
public addButtons(buttons: Button[]): void {
|
||||
buttons.forEach(button => {
|
||||
if (!this.state.buttons[button.id]) {
|
||||
this.state.buttons[button.id] = button;
|
||||
}
|
||||
});
|
||||
|
||||
this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, {
|
||||
...this.state,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,160 +136,180 @@ export default class ToolbarService extends PubSubService {
|
||||
* used for calling the specified interaction. That is, the command is
|
||||
* called with {...commandOptions,...options}
|
||||
*/
|
||||
public recordInteraction(interaction, options?: Record<string, unknown>) {
|
||||
if (!interaction) {
|
||||
return;
|
||||
public recordInteraction(
|
||||
interaction,
|
||||
options?: {
|
||||
refreshProps: Record<string, unknown>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
const commandsManager = this._commandsManager;
|
||||
const { groupId, itemId, commands, type } = interaction;
|
||||
let { interactionType } = interaction;
|
||||
|
||||
// if not interaction type, assume the type can be used
|
||||
if (!interactionType) {
|
||||
interactionType = type;
|
||||
) {
|
||||
// if interaction is a string, we can assume it is the itemId
|
||||
// and get the props to get the other properties
|
||||
if (typeof interaction === 'string') {
|
||||
interaction = this.getButtonProps(interaction);
|
||||
}
|
||||
|
||||
switch (interactionType) {
|
||||
case 'action': {
|
||||
commandsManager.run(commands, options);
|
||||
break;
|
||||
}
|
||||
case 'tool': {
|
||||
try {
|
||||
const alternateInteraction =
|
||||
this.state.primaryToolId === itemId &&
|
||||
this.defaultTool?.itemId !== itemId &&
|
||||
this.getDefaultTool();
|
||||
if (alternateInteraction) {
|
||||
// Allow toggling the mode off
|
||||
return this.recordInteraction(alternateInteraction, options);
|
||||
}
|
||||
commandsManager.run(commands, options);
|
||||
const itemId = interaction.itemId ?? interaction.id;
|
||||
interaction.itemId = itemId;
|
||||
|
||||
// only set the primary tool if no error was thrown.
|
||||
// if the itemId is not undefined use it; otherwise, set the first tool in
|
||||
// the commands as the primary tool
|
||||
this.state.primaryToolId = itemId || commands[0].commandOptions?.toolName;
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
}
|
||||
const commands = Array.isArray(interaction.commands)
|
||||
? interaction.commands
|
||||
: [interaction.commands];
|
||||
|
||||
break;
|
||||
}
|
||||
case 'toggle': {
|
||||
const { commands } = interaction;
|
||||
let commandExecuted;
|
||||
|
||||
// only toggle if a command was executed
|
||||
this.state.toggles[itemId] =
|
||||
this.state.toggles[itemId] === undefined ? true : !this.state.toggles[itemId];
|
||||
|
||||
if (!commands) {
|
||||
break;
|
||||
}
|
||||
|
||||
commands.forEach(({ commandName, commandOptions, context }) => {
|
||||
if (!commandOptions) {
|
||||
commandOptions = {};
|
||||
}
|
||||
|
||||
if (commandName) {
|
||||
commandOptions.toggledState = this.state.toggles[itemId];
|
||||
|
||||
try {
|
||||
commandsManager.runCommand(commandName, commandOptions, context);
|
||||
commandExecuted = true;
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!commandExecuted) {
|
||||
// If no command was executed, we need to toggle the state back
|
||||
this.state.toggles[itemId] = !this.state.toggles[itemId];
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Invalid interaction type: ${interactionType}`);
|
||||
}
|
||||
|
||||
// Todo: comment out for now
|
||||
// Run command if there's one associated
|
||||
//
|
||||
// NOTE: Should probably just do this for tools as well?
|
||||
// But would be nice if we could enforce at least the command name?
|
||||
// let unsubscribe;
|
||||
// if (commandName) {
|
||||
// unsubscribe = commandsManager.runCommand(commandName, commandOptions);
|
||||
// }
|
||||
|
||||
// // Storing the unsubscribe for later resetting
|
||||
// if (unsubscribe && typeof unsubscribe === 'function') {
|
||||
// if (this.unsubscriptions.indexOf(unsubscribe) === -1) {
|
||||
// this.unsubscriptions.push(unsubscribe);
|
||||
// }
|
||||
// }
|
||||
|
||||
// Track last touched id for each group
|
||||
if (groupId) {
|
||||
this.state.groups[groupId] = itemId;
|
||||
}
|
||||
|
||||
this._broadcastEvent(this.EVENTS.TOOL_BAR_STATE_MODIFIED, { ...this.state });
|
||||
}
|
||||
|
||||
getButtons() {
|
||||
return this.buttons;
|
||||
}
|
||||
|
||||
getActiveTools() {
|
||||
const activeTools = [this.state.primaryToolId];
|
||||
Object.keys(this.state.toggles).forEach(key => {
|
||||
if (this.state.toggles[key]) {
|
||||
activeTools.push(key);
|
||||
}
|
||||
});
|
||||
return activeTools;
|
||||
}
|
||||
|
||||
getActivePrimaryTool() {
|
||||
return this.state.primaryToolId;
|
||||
}
|
||||
|
||||
/** Sets the toggle state of a button to the isToggled state */
|
||||
public setToggled(id: string, isToggled: boolean): void {
|
||||
if (isToggled) {
|
||||
this.state.toggles[id] = true;
|
||||
} else {
|
||||
delete this.state.toggles[id];
|
||||
}
|
||||
}
|
||||
|
||||
setButton(id, button) {
|
||||
if (this.buttons[id]) {
|
||||
this.buttons[id] = merge(this.buttons[id], button);
|
||||
this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, {
|
||||
buttons: this.buttons,
|
||||
button: this.buttons[id],
|
||||
buttonSections: this.buttonSections,
|
||||
if (!commands?.length) {
|
||||
this.refreshToolbarState({
|
||||
...options?.refreshProps,
|
||||
itemId,
|
||||
interaction,
|
||||
});
|
||||
}
|
||||
|
||||
const commandOptions = { ...options, ...interaction };
|
||||
|
||||
// Loop through commands and run them with the combined options
|
||||
this._commandsManager.run(commands, commandOptions);
|
||||
|
||||
this.refreshToolbarState({
|
||||
...options?.refreshProps,
|
||||
itemId,
|
||||
interaction,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Consolidates the state of the toolbar after an interaction, it accepts
|
||||
* props that get passed to the buttons
|
||||
*
|
||||
* @param refreshProps - The props that buttons need to get evaluated, they can be
|
||||
* { viewportId, toolGroup} for cornerstoneTools.
|
||||
*
|
||||
* Todo: right now refreshToolbarState should be used in the context where
|
||||
* we have access to the toolGroup and viewportId, but we should be able to
|
||||
* pass the props to the toolbar service and it should be able to decide
|
||||
* which buttons to evaluate based on the props
|
||||
*/
|
||||
public refreshToolbarState(refreshProps) {
|
||||
const buttons = this.state.buttons;
|
||||
|
||||
// Tracks evaluated buttons to avoid re-evaluating them (this will
|
||||
// cause issue for toggles where if the button is in primary
|
||||
// and secondary it will be evaluated twice)
|
||||
const evaluationResults = new Map();
|
||||
|
||||
const evaluateButtonProps = (button, props, refreshProps) => {
|
||||
if (evaluationResults.has(button.id)) {
|
||||
const { disabled, className, isActive } = evaluationResults.get(button.id);
|
||||
return { ...props, disabled, className, isActive };
|
||||
} else {
|
||||
const evaluated = props.evaluate?.({ ...refreshProps, button });
|
||||
const updatedProps = {
|
||||
...props,
|
||||
disabled: evaluated?.disabled || false,
|
||||
className: evaluated?.className || '',
|
||||
isActive: evaluated?.isActive, // isActive will be undefined for buttons without this prop
|
||||
};
|
||||
evaluationResults.set(button.id, updatedProps);
|
||||
return updatedProps;
|
||||
}
|
||||
};
|
||||
|
||||
const refreshedButtons = Object.values(buttons).reduce((acc, button: Button) => {
|
||||
const isNested = (button.props as NestedButtonProps)?.groupId;
|
||||
|
||||
if (!isNested) {
|
||||
this.handleEvaluate(button.props);
|
||||
const buttonProps = button.props as ButtonProps;
|
||||
|
||||
const updatedProps = evaluateButtonProps(button, buttonProps, refreshProps);
|
||||
acc[button.id] = {
|
||||
...button,
|
||||
props: updatedProps,
|
||||
};
|
||||
} else {
|
||||
let buttonProps = button.props as NestedButtonProps;
|
||||
// if it is nested we should perform evaluate on each item in the group
|
||||
this.handleEvaluateNested(buttonProps);
|
||||
|
||||
const { evaluate: groupEvaluate } = buttonProps;
|
||||
|
||||
const groupEvaluated = groupEvaluate?.({ ...refreshProps, button });
|
||||
// handle group evaluate function which might switch the primary
|
||||
// item in the group
|
||||
buttonProps = {
|
||||
...buttonProps,
|
||||
primary: groupEvaluated?.primary || buttonProps.primary,
|
||||
};
|
||||
|
||||
const { primary, items } = buttonProps;
|
||||
|
||||
// primary and items evaluate functions
|
||||
let updatedPrimary;
|
||||
if (primary) {
|
||||
updatedPrimary = evaluateButtonProps(primary, primary, refreshProps);
|
||||
}
|
||||
const updatedItems = items.map(item => evaluateButtonProps(item, item, refreshProps));
|
||||
buttonProps = {
|
||||
...buttonProps,
|
||||
primary: updatedPrimary,
|
||||
items: updatedItems,
|
||||
};
|
||||
|
||||
acc[button.id] = {
|
||||
...button,
|
||||
props: buttonProps,
|
||||
};
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
this.setButtons(refreshedButtons);
|
||||
return this.state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the buttons for the toolbar, don't use this method to record an
|
||||
* interaction, since it doesn't update the state of the buttons, use
|
||||
* this if you know the buttons you want to set and you want to set them
|
||||
* all at once.
|
||||
* @param buttons - The buttons to set.
|
||||
*/
|
||||
public setButtons(buttons) {
|
||||
this.state.buttons = buttons;
|
||||
this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, {
|
||||
buttons: this.state.buttons,
|
||||
buttonSections: this.state.buttonSections,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a button by its ID.
|
||||
* @param id - The ID of the button to retrieve.
|
||||
* @returns The button with the specified ID.
|
||||
*/
|
||||
public getButton(id: string): Button {
|
||||
return this.buttons[id];
|
||||
return this.state.buttons[id];
|
||||
}
|
||||
|
||||
/** Gets a nested button, found in the items/props for the children */
|
||||
public getNestedButton(id: string): Button {
|
||||
if (this.buttons[id]) {
|
||||
return this.buttons[id];
|
||||
}
|
||||
for (const buttonId of Object.keys(this.buttons)) {
|
||||
const { primary, items } = this.buttons[buttonId].props || {};
|
||||
/**
|
||||
* Retrieves the buttons from the toolbar service.
|
||||
* @returns An array of buttons.
|
||||
*/
|
||||
public getButtons() {
|
||||
return this.state.buttons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the button properties for the specified button ID.
|
||||
* It prioritizes nested buttons over regular buttons if the ID is found
|
||||
* in both.
|
||||
*
|
||||
* @param id - The ID of the button.
|
||||
* @returns The button properties.
|
||||
*/
|
||||
public getButtonProps(id: string): ButtonProps {
|
||||
for (const buttonId of Object.keys(this.state.buttons)) {
|
||||
const { primary, items } = (this.state.buttons[buttonId].props as NestedButtonProps) || {};
|
||||
if (primary?.id === id) {
|
||||
return primary;
|
||||
}
|
||||
@@ -292,97 +318,57 @@ export default class ToolbarService extends PubSubService {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
|
||||
// This should be checked after we checked the nested buttons, since
|
||||
// we are checking based on the ids, the nested objects are higher priority
|
||||
// and more specific
|
||||
if (this.state.buttons[id]) {
|
||||
return this.state.buttons[id].props as ButtonProps;
|
||||
}
|
||||
}
|
||||
|
||||
setButtons(buttons) {
|
||||
this.buttons = buttons;
|
||||
this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, {
|
||||
buttons: this.buttons,
|
||||
buttonSections: this.buttonSections,
|
||||
});
|
||||
}
|
||||
_getButtonUITypes() {
|
||||
const registeredToolbarModules = this._extensionManager.modules['toolbarModule'];
|
||||
|
||||
_buttonTypes() {
|
||||
const buttonTypes = {};
|
||||
const registeredToolbarModules = this.extensionManager.modules['toolbarModule'];
|
||||
|
||||
if (Array.isArray(registeredToolbarModules) && registeredToolbarModules.length) {
|
||||
registeredToolbarModules.forEach(toolbarModule =>
|
||||
toolbarModule.module.forEach(def => {
|
||||
buttonTypes[def.name] = def;
|
||||
})
|
||||
);
|
||||
if (!Array.isArray(registeredToolbarModules)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return buttonTypes;
|
||||
}
|
||||
|
||||
createButtonSection(key, buttons) {
|
||||
// Maybe do this mapping at time of return, instead of time of create
|
||||
// Props check important for validation here...
|
||||
|
||||
this.buttonSections[key] = buttons;
|
||||
this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, {});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Finds a button section by it's name/tool group id, then maps the list of string name
|
||||
* identifiers to schema/values that can be used to render the buttons.
|
||||
*
|
||||
* @param toolGroupId - the tool group id
|
||||
* @param props - optional properties to apply to every button of the section
|
||||
* @param defaultToolGroupId - the fallback section to return if the given toolGroupId section is not available
|
||||
*/
|
||||
getButtonSection(
|
||||
toolGroupId: string,
|
||||
props?: Record<string, unknown>,
|
||||
defaultToolGroupId = 'primary'
|
||||
) {
|
||||
const buttonSectionIds =
|
||||
this.buttonSections[toolGroupId] || this.buttonSections[defaultToolGroupId];
|
||||
const buttonsInSection = [];
|
||||
|
||||
if (buttonSectionIds && buttonSectionIds.length !== 0) {
|
||||
buttonSectionIds.forEach(btnId => {
|
||||
const btn = this.buttons[btnId];
|
||||
const metadata = {};
|
||||
const mappedBtn = this._mapButtonToDisplay(btn, toolGroupId, metadata, props);
|
||||
|
||||
buttonsInSection.push(mappedBtn);
|
||||
return registeredToolbarModules.reduce((buttonTypes, toolbarModule) => {
|
||||
toolbarModule.module.forEach(def => {
|
||||
buttonTypes[def.name] = def;
|
||||
});
|
||||
}
|
||||
|
||||
return buttonsInSection;
|
||||
return buttonTypes;
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object[]} buttons
|
||||
* @param {string} buttons[].id
|
||||
* Creates a button section with the specified key and buttons.
|
||||
* @param {string} key - The key of the button section.
|
||||
* @param {Array} buttons - The buttons to be added to the section.
|
||||
*/
|
||||
addButtons(buttons) {
|
||||
buttons.forEach(button => {
|
||||
if (!this.buttons[button.id]) {
|
||||
this.buttons[button.id] = button;
|
||||
}
|
||||
});
|
||||
this._setTogglesForButtonItems(buttons);
|
||||
|
||||
this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, {});
|
||||
createButtonSection(key, buttons) {
|
||||
this.state.buttonSections[key] = buttons;
|
||||
this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, { ...this.state });
|
||||
}
|
||||
|
||||
_setTogglesForButtonItems(buttons) {
|
||||
if (!buttons) {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Retrieves the button section with the specified sectionId.
|
||||
*
|
||||
* @param sectionId - The ID of the button section to retrieve.
|
||||
* @param props - Optional additional properties for mapping the button to display.
|
||||
* @returns An array of buttons in the specified section, mapped to their display representation.
|
||||
*/
|
||||
getButtonSection(sectionId: string, props?: Record<string, unknown>) {
|
||||
const buttonSectionIds = this.state.buttonSections[sectionId];
|
||||
|
||||
buttons.forEach(buttonItem => {
|
||||
if (buttonItem.type === 'toggle') {
|
||||
this.setToggled(buttonItem.id, buttonItem.isActive);
|
||||
}
|
||||
this._setTogglesForButtonItems(buttonItem.props?.items);
|
||||
});
|
||||
return (
|
||||
buttonSectionIds?.map(btnId => {
|
||||
const btn = this.state.buttons[btnId];
|
||||
return this._mapButtonToDisplay(btn, props);
|
||||
}) || []
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -392,18 +378,22 @@ export default class ToolbarService extends PubSubService {
|
||||
* @param {*} metadata
|
||||
* @param {*} props - Props set by the Viewer layer
|
||||
*/
|
||||
_mapButtonToDisplay(btn, btnSection, metadata, props) {
|
||||
_mapButtonToDisplay(btn, props) {
|
||||
if (!btn) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { id, type, component } = btn;
|
||||
const buttonType = this._buttonTypes()[type];
|
||||
const { id, uiType, component } = btn;
|
||||
const { groupId } = btn.props;
|
||||
|
||||
const buttonType = this._getButtonUITypes()[uiType];
|
||||
|
||||
if (!buttonType) {
|
||||
return;
|
||||
}
|
||||
|
||||
!groupId ? this.handleEvaluate(btn.props) : this.handleEvaluateNested(btn.props);
|
||||
|
||||
return {
|
||||
id,
|
||||
Component: component || buttonType.defaultComponent,
|
||||
@@ -411,7 +401,54 @@ export default class ToolbarService extends PubSubService {
|
||||
};
|
||||
}
|
||||
|
||||
handleEvaluateNested = props => {
|
||||
const { primary, items } = props;
|
||||
// handle group evaluate function
|
||||
this.handleEvaluate(props);
|
||||
|
||||
// primary and items evaluate functions
|
||||
if (primary) {
|
||||
this.handleEvaluate(primary);
|
||||
}
|
||||
items.forEach(item => this.handleEvaluate(item));
|
||||
};
|
||||
|
||||
handleEvaluate = props => {
|
||||
const { evaluate } = props;
|
||||
|
||||
if (typeof evaluate === 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof evaluate === 'string') {
|
||||
const evaluateFunction = this._evaluateFunction[evaluate];
|
||||
|
||||
if (evaluateFunction) {
|
||||
props.evaluate = evaluateFunction;
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Evaluate function not found for name: ${evaluate}, you can register an evaluate function with the getToolbarModule in your extensions`
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof evaluate === 'object') {
|
||||
const { name, options } = evaluate;
|
||||
const evaluateFunction = this._evaluateFunction[name];
|
||||
|
||||
if (evaluateFunction) {
|
||||
props.evaluate = args => evaluateFunction({ ...args, ...options });
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Evaluate function not found for name: ${name}, you can register an evaluate function with the getToolbarModule in your extensions`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
getButtonComponentForUIType(uiType: string) {
|
||||
return uiType ? this._buttonTypes()[uiType]?.defaultComponent ?? null : null;
|
||||
return uiType ? this._getButtonUITypes()[uiType]?.defaultComponent ?? null : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { RunCommand } from '../../types/Command';
|
||||
|
||||
export type EvaluatePublic = string | EvaluateFunction;
|
||||
|
||||
export type EvaluateFunction = (props: Record<string, unknown>) => {
|
||||
disabled: boolean;
|
||||
className: string;
|
||||
};
|
||||
|
||||
export type ButtonProps = {
|
||||
id: string;
|
||||
icon: string;
|
||||
label: string;
|
||||
tooltip?: string;
|
||||
commands?: RunCommand;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
evaluate?: EvaluatePublic;
|
||||
listeners?: Record<string, RunCommand>;
|
||||
};
|
||||
|
||||
export type NestedButtonProps = {
|
||||
groupId: string;
|
||||
// group evaluate which is different
|
||||
// from the evaluate function for the primary and items
|
||||
evaluate?: EvaluatePublic;
|
||||
items: ButtonProps[];
|
||||
primary: ButtonProps & {
|
||||
// Todo: this is really ugly but really we don't have any other option
|
||||
// the ui design requires this since the button should be rounded if
|
||||
// active otherwise it should not be rounded
|
||||
isActive?: boolean;
|
||||
};
|
||||
secondary: ButtonProps;
|
||||
};
|
||||
|
||||
export type Button = {
|
||||
id: string;
|
||||
props: ButtonProps | NestedButtonProps;
|
||||
// button ui type (e.g. 'ohif.splitButton', 'ohif.radioGroup')
|
||||
// extensions can provide custom components for these types
|
||||
uiType: string;
|
||||
};
|
||||
@@ -7,6 +7,7 @@ class ViewportGridService extends PubSubService {
|
||||
LAYOUT_CHANGED: 'event::layoutChanged',
|
||||
GRID_STATE_CHANGED: 'event::gridStateChanged',
|
||||
GRID_SIZE_CHANGED: 'event::gridSizeChanged',
|
||||
VIEWPORTS_READY: 'event::viewportsReady',
|
||||
};
|
||||
|
||||
public static REGISTRATION = {
|
||||
@@ -35,6 +36,7 @@ class ViewportGridService extends PubSubService {
|
||||
onModeExit: onModeExitImplementation,
|
||||
set: setImplementation,
|
||||
getNumViewportPanes: getNumViewportPanesImplementation,
|
||||
setViewportIsReady: setViewportIsReadyImplementation,
|
||||
}): void {
|
||||
if (getStateImplementation) {
|
||||
this.serviceImplementation._getState = getStateImplementation;
|
||||
@@ -61,6 +63,14 @@ class ViewportGridService extends PubSubService {
|
||||
if (getNumViewportPanesImplementation) {
|
||||
this.serviceImplementation._getNumViewportPanes = getNumViewportPanesImplementation;
|
||||
}
|
||||
|
||||
if (setViewportIsReadyImplementation) {
|
||||
this.serviceImplementation._setViewportIsReady = setViewportIsReadyImplementation;
|
||||
}
|
||||
}
|
||||
|
||||
public publishViewportsReady() {
|
||||
this._broadcastEvent(this.EVENTS.VIEWPORTS_READY, {});
|
||||
}
|
||||
|
||||
public setActiveViewportId(id: string) {
|
||||
@@ -74,6 +84,10 @@ class ViewportGridService extends PubSubService {
|
||||
return this.serviceImplementation._getState();
|
||||
}
|
||||
|
||||
public setViewportIsReady(viewportId, callback) {
|
||||
this.serviceImplementation._setViewportIsReady(viewportId, callback);
|
||||
}
|
||||
|
||||
public getActiveViewportId() {
|
||||
const state = this.getState();
|
||||
return state.activeViewportId;
|
||||
@@ -110,6 +124,17 @@ class ViewportGridService extends PubSubService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the display set instance UIDs for a given viewport.
|
||||
* @param viewportId The ID of the viewport.
|
||||
* @returns An array of display set instance UIDs.
|
||||
*/
|
||||
public getDisplaySetsUIDsForViewport(viewportId: string) {
|
||||
const state = this.getState();
|
||||
const viewport = state.viewports.get(viewportId);
|
||||
return viewport?.displaySetInstanceUIDs;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param numCols, numRows - the number of columns and rows to apply
|
||||
|
||||
@@ -13,11 +13,11 @@ import HangingProtocolService from './HangingProtocolService';
|
||||
import pubSubServiceInterface, { PubSubService } from './_shared/pubSubServiceInterface';
|
||||
import UserAuthenticationService from './UserAuthenticationService';
|
||||
import CustomizationService from './CustomizationService';
|
||||
|
||||
import Services from '../types/Services';
|
||||
import StateSyncService from './StateSyncService';
|
||||
import PanelService from './PanelService';
|
||||
|
||||
import type Services from '../types/Services';
|
||||
|
||||
export {
|
||||
Services,
|
||||
MeasurementService,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
export interface Command {
|
||||
export type SimpleCommand = string;
|
||||
export interface ComplexCommand {
|
||||
commandName: string;
|
||||
commandOptions?: Record<string, unknown>;
|
||||
context?: string;
|
||||
}
|
||||
|
||||
export type Command = SimpleCommand | ComplexCommand;
|
||||
export type RunCommand = Command | Command[];
|
||||
|
||||
/** A set of commands, typically contained in a tool item or other configuration */
|
||||
|
||||
Reference in new issue
Block a user