feat(toolbar): new Toolbar to enable reactive state synchronization (#3983)

This commit is contained in:
Alireza authored and GitHub committed 2024-03-27 16:01:32 -04:00
1 parent 79d5c36bda
commit 566b25a544
155 files changed
+6311 -6577

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 441 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 KiB

-1
View File
@@ -649,7 +649,6 @@ onModeEnter: ({ servicesManager, extensionManager, commandsManager }) => {
// Init tool groups (see cornerstone3D for more details)
initToolGroups(extensionManager, toolGroupService, commandsManager);
toolbarService.init(extensionManager);
toolbarService.addButtons(toolbarButtons);
toolbarService.createButtonSection('primary', [
'MeasurementTools',
@@ -6,29 +6,24 @@ sidebar_label: Toolbar
# Module: Toolbar
An extension can register a Toolbar Module by defining a `getToolbarModule`
method. `OHIF-v3`'s `default` extension (`"@ohif/extension-default"`) provides 5 main
toolbar button types:
method. `OHIF-v3`'s `default` extension (`"@ohif/extension-default"`) provides the
following toolbar button `uiTypes`:
![toolbarModule](../../../assets/img/toolbar-module.png)
- `ohif.radioGroup`: which is a simple button that can be clicked
- `ohif.splitButton`: which is a button with a dropdown menu
- `ohif.divider`: which is a simple divider
## Example Toolbar Module
The Toolbar Module should return an array of `objects`. There are currently a
few different variations of definitions, each one is detailed further down.
There are two things that the toolbar module can provide, first
a component, and second evaluators.
### Components
```js
export default function getToolbarModule({ commandsManager, servicesManager }) {
return [
{
name: 'ohif.divider',
defaultComponent: ToolbarDivider,
clickHandler: () => {},
},
{
name: 'ohif.action',
defaultComponent: ToolbarButton,
clickHandler: () => {},
},
{
name: 'ohif.radioGroup',
defaultComponent: ToolbarButton,
@@ -53,214 +48,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
}
```
## Toolbar buttons consumed in modes
Below we can see a simplified version of the `longitudinal` mode that shows how
a mode can add buttons to the toolbar by calling
`ToolBarService.addButtons(toolbarButtons)`. `toolbarButtons` is an array of
`toolDefinitions` which we will learn next.
```js
function modeFactory({ modeConfiguration }) {
return {
id: 'viewer',
displayName: 'Basic Viewer',
onModeEnter: ({ servicesManager, extensionManager }) => {
const { ToolBarService } = servicesManager.services;
ToolBarService.init(extensionManager);
ToolBarService.addButtons(toolbarButtons);
},
routes: [
{
path: 'longitudinal',
layoutTemplate: ({ location, servicesManager }) => {
return {
/* */
};
},
},
],
};
}
```
## Button Definitions
The simplest toolbarButtons definition has the following properties:
![toolbarModule-zoom](../../../assets/img/toolbarModule-zoom.png)
```js
{
"id": "Zoom",
"type": "ohif.radioGroup",
"props": {
"type": "tool",
"icon": "tool-zoom",
"label": "Zoom",
"commands": [
{
"commandName": "setToolActive",
"commandOptions": {
"toolName": "Zoom"
},
"context": "CORNERSTONE"
}
]
}
}
```
| property | description | values |
| ---------------- | ----------------------------------------------------------------- | ------------------------------------------- |
| `id` | Unique string identifier for the definition | \* |
| `type` | Used to determine the button's behaviour | "tool", "toggle", "action" |
| `icon` | A string name for an icon supported by the consuming application. | \* |
| `label` | User/display friendly to show in UI | \* |
| `commands` | (optional) The commands to run when the button is used. It include a commandName, commandOptions, and/or a context | Any command registered by a `CommandModule` |
There are three main types of toolbar buttons:
- `tool`: buttons that enable a tool by running the `setToolActive` command with
the `commandOptions`
- `toggle`: buttons that acts as a toggle: e.g., linking viewports
- `action`: buttons that executes an action: e.g., capture button to save
screenshot
## Nested Buttons
You can use the `ohif.splitButton` type to build a button with extra tools in
the dropdown.
- First you need to give your `primary` tool definition to the split button. The primary
tool can specify a `uiType` property which can be one of the button types returned by
`getToolbarModule` that is a variation of `ToolbarButton`. If `uiType` is omitted then
`ToolbarButton` is used by default.
- The `secondary` properties can be a simple arrow down (`chevron-down` icon)
- For adding the extra tools add them to the `items` list.
You can see below how `longitudinal` mode is using the available toolbarModule
to create `MeasurementTools` nested button
![toolbarModule-nested-buttons](../../../assets/img/toolbarModule-nested-buttons.png)
```js title="modes/longitudinal/src/toolbarButtons.js"
{
"id": "MeasurementTools",
"type": "ohif.splitButton",
"props": {
"groupId": "MeasurementTools",
"isRadio": true,
"primary": {
"id": "Length",
"icon": "tool-length",
"label": "Length",
"type": "tool",
"commands": [
{
"commandName": "setToolActive",
"commandOptions": {
"toolName": "Length"
},
"context": "CORNERSTONE"
},
{
"commandName": "setToolActive",
"commandOptions": {
"toolName": "SRLength",
"toolGroupId": "SRToolGroup"
},
"context": "CORNERSTONE"
}
],
"tooltip": "Length"
},
"secondary": {
"icon": "chevron-down",
"label": "",
"isActive": true,
"tooltip": "More Measure Tools"
},
"items": [
{
"id": "Bidirectional",
"icon": "tool-bidirectional",
"label": "Bidirectional",
"type": "tool",
"commands": [
{
"commandName": "setToolActive",
"commandOptions": {
"toolName": "Bidirectional"
},
"context": "CORNERSTONE"
},
{
"commandName": "setToolActive",
"commandOptions": {
"toolName": "SRBidirectional",
"toolGroupId": "SRToolGroup"
},
"context": "CORNERSTONE"
}
],
"tooltip": "Bidirectional Tool"
},
{
"id": "ArrowAnnotate",
"icon": "tool-annotate",
"label": "Annotation",
"type": "tool",
"commands": [
{
"commandName": "setToolActive",
"commandOptions": {
"toolName": "ArrowAnnotate"
},
"context": "CORNERSTONE"
},
{
"commandName": "setToolActive",
"commandOptions": {
"toolName": "SRArrowAnnotate",
"toolGroupId": "SRToolGroup"
},
"context": "CORNERSTONE"
}
],
"tooltip": "Arrow Annotate"
},
]
}
}
```
<div style={{padding:"56.25% 0 0 0", position:"relative"}}>
<iframe src="https://player.vimeo.com/video/547957214?badge=0&amp;autopause=0&amp;player_id=0&amp;app_id=58479" allow="autoplay; fullscreen; picture-in-picture" allowFullScreen style= {{ position:"absolute",top:0,left:0,width:"100%",height:"100%"}} title="measurement-report"></iframe>
</div>
## Layout Template
Layout selector button and logic is also provided by the OHIF-v3 `default`
extension. To use it, you can just add the following definition to the list of
`toolDefinitions`
![toolbarModule-layout](../../../assets/img/toolbarModule-layout.png)
```js
{
id: 'Layout',
type: 'ohif.layoutSelector',
}
```
<div style={{padding:"56.25% 0 0 0", position:"relative"}}>
<iframe src="https://player.vimeo.com/video/545993263?badge=0&amp;autopause=0&amp;player_id=0&amp;app_id=58479" allow="autoplay; fullscreen; picture-in-picture" allowFullScreen style= {{ position:"absolute",top:0,left:0,width:"100%",height:"100%"}} title="measurement-report"></iframe>
</div>
## Custom Button
### Custom Components
You can also create your own extension, and add your new custom tool appearance
(e.g., split horizontally instead of vertically for split tool). Simply add
@@ -283,6 +71,323 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
}
```
## Custom tool
Check out how to assemble the toolbar in the [modes](../../modes/index.md) section.
**I want to create a new tool**
### Evaluators
Buttons may be equipped with evaluators, which are functions invoked by the toolbarService to assess the button's status. These evaluators are expected to return an object of `{className}` and may include additional details, as elaborated in the subsequent section.
Evaluators play a crucial role in determining the button's status based on the viewport. For example, users should be restricted from clicking on the mpr if the displaySet is not reconstructable. Additionally, certain buttons within the toolbar may be associated with specific toolGroups and should remain inactive for certain viewports.
Let's look at one of the evaluators (for `evaluate.cornerstoneTool`)
```js
{
name: 'evaluate.cornerstoneTool',
evaluate: ({ viewportId, button }) => {
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
if (!toolGroup) {
return;
}
const toolName = getToolNameForButton(button);
if (!toolGroup || !toolGroup.hasTool(toolName)) {
return {
disabled: true,
className: '!text-common-bright ohif-disabled',
};
}
const isPrimaryActive = toolGroup.getActivePrimaryMouseButtonTool() === toolName;
return {
disabled: false,
className: isPrimaryActive
? '!text-black bg-primary-light'
: '!text-common-bright hover:!bg-primary-dark hover:!text-primary-light',
};
},
},
```
as you can see the job of this evaluator is to determine if the button should be disabled or not. It does so by checking the `toolGroup` and the `toolName` and then returns an object with `disabled` and `className` properties.
The following evaluators are provided by us:
- `evaluate.cornerstoneTool`: If assigned to a button (see next), it will make the button react to the active viewport state based on its toolGroup.
- `evaluate.cornerstoneTool.toggle`: It is designed to consider tools with toggle behavior, such as reference lines and image overlay (either on or off).
- `evaluate.cornerstone.synchronizer`: This is designed to consider the synchronizer state of the viewport, whether it is synced or not.
- `evaluate.viewportProperties.toggle`: Some properties of the viewport are toggleable, such as invert, flip, rotate, etc. By assigning this evaluator to those buttons, they will react to the active viewport state based on its properties. This allows for dynamic buttons that change their appearance based on the active viewport state.
- `evaluate.mpr`: special evaluator for MPR since it needs to check if the displaySet is reconstructable or not.
Sometime you want to use the same `evaluator` for different purposes, in that case you can use an object
with `name` and `options` properties. For example, in `'evaluate.cornerstone.segmentation'` we use
this pattern, where multiple toolbar buttons are using the same evaluator but with different options (
in this case `toolNames`
)
```js
{
name: 'evaluate.cornerstone.segmentation',
options: {
toolNames: ['CircleBrush' , 'SphereBrush']
},
},
```
#### Group evaluators
Split buttons (see in [ToolbarService](../../services/data/ToolbarService.md) on how to define one) may feature a group evaluator, we provide two of them and you can write your own.
- `evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList`: determine the outcome of user interactions with the split buttons on what button should be promoted to the primary section. In the example above, the cornerstone tool's status is checked, and if it is not active in the list of buttons, the button is promoted to the primary section.
- `evaluate.group.promoteToPrimary`: disregarding the cornerstone tool's status and promoting the button to the primary section regardless.
Failure to specify a group evaluator will result in no action, leaving the button in the secondary section.
:::note
As you have learned so far, the extension modules only 'provides' the functionality
and it is the mode's job to consume it. You can next learn how to consume these components
and evaluators to build a toolbar in the
:::
#### Custom Evaluators
You can create your own evaluators. For instance, you have the option to design tri-state buttons, which are buttons with three states such as Show All, Show Some, or Show None of the Viewport Overlays.
## Toolbar buttons consumed in modes
Providing just the components is not enough. You need to add the buttons to the toolbar service and decide which ones are used for each section.
Below we can see a simplified version of the `longitudinal` (basic viewer) mode that shows how
a mode can add buttons to the toolbar by calling
`ToolBarService.addButtons(toolbarButtons)`. `toolbarButtons` is an array of
`toolDefinitions` which we will learn next.
```js
function modeFactory({ modeConfiguration }) {
return {
id: 'viewer',
displayName: 'Basic Viewer',
onModeEnter: ({ servicesManager, extensionManager }) => {
const { toolBarService } = servicesManager.services;
toolbarService.addButtons([...toolbarButtons, ...moreTools]);
toolbarService.createButtonSection('primary', [
'MeasurementTools',
'Zoom',
'info',
'WindowLevel',
'Pan',
'Capture',
'Layout',
'MPR',
'Crosshairs',
'MoreTools',
]);
},
routes: [
{
path: 'longitudinal',
layoutTemplate: ({ location, servicesManager }) => {
return {
/* */
};
},
},
],
};
}
```
:::note
By default OHIF's default layout (`extensions/default/src/ViewerLayout/index.tsx`) which is used in all modes use a Toolbar component that creates a
`primary` section for tools. That is why we are creating a `primary` section in the example above.
Layouts are also customizable, and you can create your own layout in your extensions and provide it to your modes view `getLayoutTemplateModule` module.
By default we use `@ohif/extension-default.layoutTemplateModule.viewerLayout` to use the default layout which provides a
- Header (with logo on left, toolbar in the middle and user menu on the right)
- Left panel
- Main viewport grid area
- Right panel
:::
## Alternative Toolbar sections
In your UI component, such as panels, you have the option to include a toolbar section template.
This allows you to easily add buttons to it later on. To ensure that the buttons are added properly
to the toolbar, respond to interactions correctly, and evaluate states accurately, simply utilize the `useToolbar` hook.
This hook grants you access to the `onInteraction` function and the `toolbarButtons` array, which you can customize within your UI as needed.
```js
function myCustomPanel({servicesManager}){
const { onInteraction, toolbarButtons } = useToolbar({
servicesManager,
buttonSection: 'myCustomSectionName'
});
// map the buttons to the UI
return (
<div>
{toolbarButtons.map((button, index) => {
return (
<button
key={index}
onClick={() => onInteraction(button)}
>
{button.label}
</button>
);
})}
</div>
);
}
```
We have provided a common component for toolbar buttons called `Toolbox`.
The Toolbox component serves as a versatile and configurable container for toolbar tools within your application.
It is designed to work in conjunction with the useToolbar hook to manage tool states, handle user interactions, and memorize options
using context API.
The `Toolbox` can be easily integrated into your application UI, requiring only the necessary services (servicesManager, commandsManager) and configuration parameters (buttonSectionId, title). Here's a simple usage scenario:
```js
function MyApplication({ servicesManager, commandsManager }) {
// Configuration for the toolbox container
const config = {
servicesManager,
commandsManager,
buttonSectionId: 'customButtonSection',
title: 'My Toolbox',
};
return <Toolbox {...config} />;
}
```
Then in your modes you can edit the tools in that button section.
```js
onModeEnter: ({ servicesManager, extensionManager }) => {
const { toolBarService } = servicesManager.services;
toolbarService.addButtons([...toolbarButtons, ...moreTools]);
toolbarService.createButtonSection('customButtonSection', [
'MeasurementTools',
'Zoom',
'info',
]);
},
```
Another example might be you want to open a modal to show some tool options when a button is clicked.
You can use this pattern
```js
// ToolbarButton in mode
{
id: 'Others',
uiType: 'ohif.radioGroup',
props: {
icon: 'info-action',
label: 'Others',
commands: 'showOthersModal',
},
},
```
and inside your mode factory
```js
// adding the 'Others' button to the primary section
toolbarService.createButtonSection('primary', [
'Others', // --------> this one
]);
// adding the shapes button to the 'Other' section
toolbarService.createButtonSection('other', ['Shapes']);
```
here as you see we are using a command `showOthersModal` which is defined in the commands module.
```js
// inside commandsModule of your extension
showOthersModal: () => {
const { uiModalService } = servicesManager.services;
uiModalService.show({
content: OthersModal,
title: 'Others',
customClassName: 'w-8',
movable: true,
contentProps: {
onClose: uiModalService.hide,
servicesManager,
commandsManager,
},
containerDimensions: 'h-[125px] w-[300px]',
contentDimensions: 'h-[125px] w-[300px]',
});
},
```
as you see it is opening a modal with `OthersModal` component (below) which contains the
`Toolbox` component.
```js
// Others modal
import { Toolbox } from '@ohif/ui';
function OthersModal({ servicesManager, commandsManager }) {
return (
<div className="px-2">
<Toolbox
buttonSectionId={'other'}
commandsManager={commandsManager}
servicesManager={servicesManager}
title={'other'}
useCollapsedPanel={false}
></Toolbox>
</div>
);
}
```
The result would be a modal with a toolbox inside it when the `Others` button is clicked, and the
state will get synchronized with the toolbar service automatically.
![alt text](../../../assets/img/toolbox-modal.png)
## Change Toolbar with hanging protocols
If you want to change the toolbar based on the hanging protocol, you can do a pattern like this.
```js
const { unsubscribe } = hangingProtocolService.subscribe(
hangingProtocolService.EVENTS.PROTOCOL_CHANGED,
() => {
toolbarService.createButtonSection('primary', [
'MeasurementTools',
'Zoom',
'WindowLevel',
]);
}
);
```
@@ -1,4 +1,4 @@
{
"label": "Managers",
"position": 11
"position": 10
}
@@ -1,4 +1,4 @@
{
"label": "Modes",
"position": 10
"position": 12
}
@@ -362,6 +362,11 @@ function modeFactory() {
// exports
```
### Toolbar
## Registration
Similar to extension registration, `viewer` will look inside the `pluginConfig.json` to
@@ -56,7 +56,6 @@ function modeFactory() {
// Init Default and SR ToolGroups
initToolGroups(extensionManager, ToolGroupService);
ToolBarService.init(extensionManager);
ToolBarService.addButtons(toolbarButtons);
ToolBarService.createButtonSection('primary', [
'MeasurementTools',
@@ -1,4 +1,4 @@
{
"label": "Services",
"position": 12
"position": 11
}
@@ -0,0 +1,147 @@
---
sidebar_position: 8
sidebar_label: SyncGroup Service
---
# Sync Group Service
## Overview
The `SyncGroupService` is responsible for managing synchronization groups in the OHIF Viewer. Synchronization groups allow multiple viewports to be synchronized based on various criteria, such as camera position, window level, zoom/pan, and image slice position. This service provides a centralized way to create, update, and manage synchronization groups.
Right now, synchronization groups can be defined in the hanging protocols or manually assigning buttons.
## API
- `getSyncCreatorForType(type)`: Returns the synchronizer creator function for the specified type.
- `addSynchronizerType(type, creator)`: Adds a new synchronizer type with a custom creator function.
- `getSynchronizer(id)`: Retrieves a synchronizer by its ID.
- `getSynchronizersOfType(type)`: Retrieves an array of synchronizers of the specified type.
- `addViewportToSyncGroup(viewportId, renderingEngineId, syncGroups)`: Adds a viewport to one or more synchronization groups.
- `destroy()`: Destroys all synchronizers.
- `getSynchronizersForViewport(viewportId)`: Retrieves an array of synchronizers associated with the specified viewport.
- `removeViewportFromSyncGroup(viewportId, renderingEngineId, syncGroupId?)`: Removes a viewport from a specific synchronization group or all synchronization groups if no group ID is provided.
## Usage
### Via hanging protocols
You can set up different types of synchronization groups for your viewports. For example, in the TMTV hanging protocol (`extensions/tmtv/src/getHangingProtocolModule.js`), we can see how different synchronization groups are defined for various viewports:
```javascript
const ptAXIAL = {
viewportOptions: {
// ...
syncGroups: [
{
type: 'cameraPosition',
id: 'axialSync',
source: true,
target: true,
},
{
type: 'voi',
id: 'ptWLSync',
source: true,
target: true,
},
{
type: 'voi',
id: 'ptFusionWLSync',
source: true,
target: false,
options: {
syncInvertState: false,
},
},
],
},
// ...
};
```
In this example, the `ptAXIAL` viewport is part of three synchronization groups:
1. `cameraPosition` group with the ID `'axialSync'`: This group synchronizes the camera position across viewports that are both source and target.
2. `voi` (Window Level) group with the ID `'ptWLSync'`: This group synchronizes the window level settings across viewports that are both source and target.
3. `voi` group with the ID `'ptFusionWLSync'`: This group synchronizes the window level settings, but the `ptAXIAL` viewport is only a source, not a target.
:::tip
You can control the state of the synchronizer via a toolbar button after you define the synchronization group in the hanging protocol.
```js
{
id: 'SyncToggle',
uiType: 'ohif.radioGroup',
props: {
icon: 'tool-info',
label: 'toggle',
commands: {
commandName: 'toggleSynchronizer',
commandOptions: {
syncId: 'axialSync'
}
}
},
},
```
as you can see by using the `toggleSynchronizer` command you can toggle the state of the synchronizer for the specified syncId.
:::
### Manually through a button
You can create a button on the toolbar that you provice the synchronization group type,
and it applys it to all viewports.
:::note
Currently we don't have a proper way to select viewports to apply the synchronization group to. It is applied to all applicable viewports
:::
For instance look at `imageSliceSync` button in the longitudinal mode (`modes/longitudinal/src/moreTools.ts`) and how it runs a command
```js
ToolbarService.createButton({
id: 'ImageSliceSync',
icon: 'link',
label: 'Image Slice Sync',
tooltip: 'Enable position synchronization on stack viewports',
commands: [
{
commandName: 'toggleSynchronizer',
commandOptions: {
type: 'imageSlice',
},
},
],
})
```
You can create another button to toggle 'voi' synchronization. Currently we group
viewports by modality and apply the voi synchronization to all viewports of the same modality.
```js
ToolbarService.createButton({
id: 'VoiSync',
icon: 'link',
label: 'VOI Sync',
tooltip: 'Enable VOI synchronization on viewports',
commands: [
{
commandName: 'toggleSynchronizer',
commandOptions: {
type: 'voi',
},
},
],
})
```
:::tip
For your custom synchronization groups, you can create a new synchronizer type and follow the
same pattern as the existing synchronizers.
:::
@@ -0,0 +1,89 @@
---
sidebar_position: 7
sidebar_label: ToolGroup Service
---
# Tool Group Service
## Overview
The `ToolGroupService` is responsible for managing tool groups in the OHIF Viewer.
:::tip
Read more about toolGroups [here](https://www.cornerstonejs.org/docs/concepts/cornerstone-tools/toolGroups)
:::
It allows you to create, update, and manage tool groups and the tools associated with them. Tool groups are used to organize and control the behavior of various tools in the viewer, such as window level, pan, zoom, measurements, and annotations.
## Events
The `ToolGroupService` emits the following events:
| Event | Description |
| ---------------------------------- | ----------------------------------------------- |
| `VIEWPORT_ADDED` | Fires when a viewport is added to a tool group |
| `TOOLGROUP_CREATED` | Fires when a new tool group is created |
## API
- `getToolGroup(toolGroupId?)`: Retrieves a tool group by its ID. If no ID is provided, it returns the tool group for the active viewport.
- `getToolGroupIds()`: Returns an array of all tool group IDs.
- `getToolGroupForViewport(viewportId)`: Returns the tool group associated with the specified viewport.
- `getActiveToolForViewport(viewportId)`: Returns the active tool for the specified viewport.
- `destroy()`: Destroys all tool groups.
- `destroyToolGroup(toolGroupId)`: Destroys the specified tool group.
- `removeViewportFromToolGroup(viewportId, renderingEngineId, deleteToolGroupIfEmpty?)`: Removes a viewport from a tool group. If `deleteToolGroupIfEmpty` is true and the tool group becomes empty after removing the viewport, it will be destroyed.
- `addViewportToToolGroup(viewportId, renderingEngineId, toolGroupId?)`: Adds a viewport to a tool group. If `toolGroupId` is not provided, the viewport will be added to all tool groups.
- `createToolGroup(toolGroupId)`: Creates a new tool group with the specified ID.
- `addToolsToToolGroup(toolGroupId, tools, configs?)`: Adds tools to the specified tool group with optional configurations.
- `createToolGroupAndAddTools(toolGroupId, tools)`: Creates a new tool group and adds the specified tools to it.
- `getToolConfiguration(toolGroupId, toolName)`: Retrieves the configuration for the specified tool in the given tool group.
- `setToolConfiguration(toolGroupId, toolName, config)`: Sets the configuration for the specified tool in the given tool group.
## Usage
Here's an example of how to create a new tool group and add tools to it in our basic viewer mode (modes/longitudinal/src/initToolGroups.js)
```js
import { initToolGroups } from '@ohif/extension-cornerstone';
import { ToolGroupService } from '@ohif/core';
const toolGroupService = new ToolGroupService();
// Create a new tool group
const defaultToolGroup = toolGroupService.createToolGroup('default');
// Define tools for the tool group
const tools = {
active: [
{ toolName: 'WindowLevel', bindings: [{ mouseButton: 1 }] },
{ toolName: 'Pan', bindings: [{ mouseButton: 2 }] },
{ toolName: 'Zoom', bindings: [{ mouseButton: 3 }] },
],
passive: [
{ toolName: 'Length' },
{ toolName: 'ArrowAnnotate' },
{ toolName: 'Bidirectional' },
],
};
// Add tools to the tool group
toolGroupService.addToolsToToolGroup('default', tools);
```
In this example, we create a new `ToolGroupService` instance and use it to create a new tool group with the ID `'default'`. We then define an object `tools` that contains the active and passive tools we want to add to the tool group. Finally, we call the `addToolsToToolGroup` method to add the tools to the newly created tool group.
:::tip
You can begin the viewer with certain toggle tools already active. For example, if you have your 'referencelines' tool enabled, it will be active when the viewer starts, and the icon state will be correctly set to active as well.
:::
```js
const tools = {
// the reset
// enabled
enabled: [{ toolName: toolNames.ImageOverlayViewer }, { toolName: toolNames.ReferenceLines }],
};
```
![alt text](../../../assets/img/reference-lines-from-start.png)
@@ -3,16 +3,15 @@ sidebar_position: 5
sidebar_label: Toolbar Service
---
# Toolbar Service
# Toolbar **Service**
## Overview
`ToolBarService` handles the toolbar section buttons, and what happens when a
button is clicked by the user.
The `ToolBarService` is a straightforward service designed to handle the toolbar. Its main tasks include adding buttons, configuring them, and organizing button sections. When a button is clicked, it executes the designated commands. In the past, this service was more intricate, managing button states and logic. However, all that functionality has now been transferred to the `ToolBarModule` and evaluators.
<div style={{padding:"56.25% 0 0 0", position:"relative"}}>
<iframe src="https://player.vimeo.com/video/547957214?badge=0&amp;autopause=0&amp;player_id=0&amp;app_id=58479" frameBorder="0" allow="autoplay; fullscreen; picture-in-picture" allowFullScreen style= {{ position:"absolute",top:0,left:0,width:"100%",height:"100%"}} title="measurement-report"></iframe>
</div>
<!-- <div style={{padding:"56.25% 0 0 0", position:"relative"}}>
<iframe src="https://player.vimeo.com/video/547957214?badge=0&amp;autopause=0&amp;player_id=0&amp;app_id=58479" frameBorder="0" allow="autoplay; fullscreen; picture-in-picture" allowFullScreen style= {{ position:"absolute",top:0,left:0,width:"100%",height:"100%"}} **title**="measurement-report"></iframe>
</div> -->
## Events
@@ -23,20 +22,7 @@ button is clicked by the user.
## API
- `recordInteraction(interaction)`: executes the provided interaction which is
an object providing the following properties to the ToolBarService:
- `interactionType`: can be `tool`, `toggle` and `action`. We will discuss
more each type below.
- `itemId`: tool name
- `groupId`: the Id for the tool button group; e.g., `Wwwc` which holds
presets.
- `commandName`: if tool has a command attached to run
- `commandOptions`: arguments for the command.
- `setActive`: Sets a given tool active (not as primary but as secondary)
- `reset`: reset the state of the toolbarService, set the primary tool to be
`Wwwc` and unsubscribe tools that have registered their functions.
- `createButtonSection(key, buttons)` : creates a section of buttons in the toolbar with the given key and button Ids
- `addButtons`: add the button definition to the service.
[See below for button definition](#button-definitions).
@@ -44,86 +30,26 @@ button is clicked by the user.
- `setButtons`: sets the buttons defined in the service. It overrides all the
previous buttons
- `getActiveTools`: returns the active tool + all the toggled-on tools
- `setDefaultTool`: sets the default tool that will be activated whenever the primary tool is deactivated without activating another/different tool
## State
ToolBarService has an internal state that gets updated per tool interaction and
tracks the active toolId, state of the buttons that have toggled state, and the
group buttons and which tool in each group is active.
```js
state = {
primaryToolId: 'Wwwc',
toggles: {
/* id: true/false */
},
groups: {
/* track most recent click per group...*/
},
};
```
## Interaction type
There are three main types that a tool can have which is defined in the
interaction object.
- `tool`: setting a tool to be active; e.g., measurement tools
- `toggle`: toggling state of a tool; e.g., viewport link (sync)
- `action`: performs a registered action outside of the ToolBarService; e.g.,
capture
A _simplified_ implementation of the ToolBarService is:
```js
export default class ToolBarService {
/** ... **/
recordInteraction(interaction) {
/** ... **/
switch (interactionType) {
case 'action': {
break;
}
case 'tool': {
this.state.primaryToolId = itemId;
commandsManager.runCommand('setToolActive', interaction.commandOptions);
break;
}
case 'toggle': {
this.state.toggles[itemId] =
this.state.toggles[itemId] === undefined
? true
: !this.state.toggles[itemId];
interaction.commandOptions.toggledState = this.state.toggles[itemId];
break;
}
default:
throw new Error(`Invalid interaction type: ${interactionType}`);
}
/** ... **/
}
/** ... **/
}
```
## Button Definitions
### Basic
The simplest toolbarButtons definition has the following properties:
![toolbarModule-zoom](../../../assets/img/toolbarModule-zoom.png)
```js
{
"id": "Zoom",
"type": "ohif.radioGroup",
"props": {
"type": "tool",
"icon": "tool-zoom",
"label": "Zoom",
id: 'Zoom',
uiType: 'ohif.radioGroup',
props: {
icon: 'tool-zoom',
label: 'Zoom',
"commands": [
{
"commandName": "setToolActive",
@@ -133,27 +59,20 @@ The simplest toolbarButtons definition has the following properties:
"context": "CORNERSTONE"
}
]
}
}
evaluate: 'evaluate.cornerstoneTool',
},
},
```
| property | description | values |
| ---------------- | ----------------------------------------------------------------- | ------------------------------------------- |
| `id` | Unique string identifier for the definition | \* |
| `type` | Used to determine the button's behaviour | "tool", "toggle", "action" |
| `icon` | A string name for an icon supported by the consuming application. | \* |
| `label` | User/display friendly to show in UI | \* |
| `commands` | (optional) The commands to run when the button is used. It include a commandName, commandOptions, and/or a context | Any command registered by a `CommandModule` |
There are three main types of toolbar buttons:
- `tool`: buttons that enable a tool by running the `setToolActive` command with
the `commandOptions`
- `toggle`: buttons that acts as a toggle: e.g., linking viewports
- `action`: buttons that executes an action: e.g., capture button to save
screenshot
## Nested Buttons
### Nested (dropdown)
You can use the `ohif.splitButton` type to build a button with extra tools in
the dropdown.
@@ -169,91 +88,178 @@ to create `MeasurementTools` nested button
```js title="modes/longitudinal/src/toolbarButtons.js"
{
"id": "MeasurementTools",
"type": "ohif.splitButton",
"props": {
"groupId": "MeasurementTools",
"isRadio": true,
"primary": {
"id": "Length",
"icon": "tool-length",
"label": "Length",
"type": "tool",
"commands": [
{
"commandName": "setToolActive",
"commandOptions": {
"toolName": "Length"
},
"context": "CORNERSTONE"
},
{
"commandName": "setToolActive",
"commandOptions": {
"toolName": "SRLength",
"toolGroupId": "SRToolGroup"
},
"context": "CORNERSTONE"
}
id: 'MeasurementTools',
uiType: 'ohif.splitButton',
props: {
groupId: 'MeasurementToolsGroupId',
// group evaluate to determine which item should move to the top
evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList',
primary: ToolbarService.createButton({
id: 'Length',
icon: 'tool-length',
label: 'Length',
tooltip: 'Length Tool',
commands: _createSetToolActiveCommands('Length'),
evaluate: 'evaluate.cornerstoneTool',
}),
secondary: {
icon: 'chevron-down',
tooltip: 'More Measure Tools',
},
items: [
ToolbarService.createButton({
id: 'Length',
icon: 'tool-length',
label: 'Length',
tooltip: 'Length Tool',
commands: [
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'Length',
},
context: 'CORNERSTONE',
},
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'SRLength',
toolGroupId: 'SRToolGroup',
},
// we can use the setToolActive command for this from Cornerstone commandsModule
context: 'CORNERSTONE',
},
],
evaluate: 'evaluate.cornerstoneTool',
}),
ToolbarService.createButton({
id: 'Bidirectional',
icon: 'tool-bidirectional',
label: 'Bidirectional',
tooltip: 'Bidirectional Tool',
commands: [
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'Bidirectional',
},
context: 'CORNERSTONE',
},
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'SRBidirectional',
toolGroupId: 'SRToolGroup',
},
context: 'CORNERSTONE',
},
],
evaluate: 'evaluate.cornerstoneTool',
}),
],
"tooltip": "Length"
},
"secondary": {
"icon": "chevron-down",
"label": "",
"isActive": true,
"tooltip": "More Measure Tools"
},
"items": [
{
"id": "Bidirectional",
"icon": "tool-bidirectional",
"label": "Bidirectional",
"type": "tool",
"commands": [
{
"commandName": "setToolActive",
"commandOptions": {
"toolName": "Bidirectional"
},
"context": "CORNERSTONE"
},
{
"commandName": "setToolActive",
"commandOptions": {
"toolName": "SRBidirectional",
"toolGroupId": "SRToolGroup"
},
"context": "CORNERSTONE"
}
],
"tooltip": "Bidirectional Tool"
},
{
"id": "ArrowAnnotate",
"icon": "tool-annotate",
"label": "Annotation",
"type": "tool",
"commands": [
{
"commandName": "setToolActive",
"commandOptions": {
"toolName": "ArrowAnnotate"
},
"context": "CORNERSTONE"
},
{
"commandName": "setToolActive",
"commandOptions": {
"toolName": "SRArrowAnnotate",
"toolGroupId": "SRToolGroup"
},
"context": "CORNERSTONE"
}
],
"tooltip": "Arrow Annotate"
},
]
}
}
},
```
:::tip
split buttons can have a group evaluator (in the above example `evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList`) which can decide what happens
when the user interacts with the buttons. In the above example, we are promoting the button to the primary section if the cornerstone tool is not active in the list of buttons.
There are other evaluators for instance `evaluate.group.promoteToPrimary`
which does not care about the cornerstone tool and promotes the button to the primary section anyway
:::
:::tip
If you don't provide a group evaluator nothing would happen and the button will stay in the secondary section.
:::
## Listeners
Sometimes you need a tool to listen to specific events in order to react properly.
You can add `listeners` for this purpose. We use this pattern for referencelineTools
which should set its source of reference upon active viewport change
Currently you can subscribe to the following events:
- `ViewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED`: when the active viewport changes
- `ViewportGridService.EVENTS.VIEWPORTS_READY`: when the viewports are ready in the grid
```js
const ReferenceLinesListeners: RunCommand = [
{
commandName: 'setSourceViewportForReferenceLinesTool',
context: 'CORNERSTONE',
},
];
ToolbarService.createButton({
id: 'ReferenceLines',
icon: 'tool-referenceLines',
label: 'Reference Lines',
tooltip: 'Show Reference Lines',
commands: [
{
commandName: 'setToolEnabled',
commandOptions: {
toolName: 'ReferenceLines',
toggle: true,
},
context: 'CORNERSTONE',
},
],
listeners: {
[ViewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: ReferenceLinesListeners,
[ViewportGridService.EVENTS.VIEWPORTS_READY]: ReferenceLinesListeners,
},
evaluate: 'evaluate.cornerstoneTool.toggle',
}),
```
## Button Sections
In order to organize the buttons, you can create button sections in the toolbar. And
assign buttons to each section separately.
OHIF provides a `primary` section by default. You can add more sections as needed in your UI
and use toolbarService to create and manage them. (You can look at the toolBox implementation
which take advantage of having a dedicated section for the tools with advanced options,
we use that in the segmentation mode).
## Example
For instance in `longitudinal` mode we are using the `onModeEnter` hook to
add the buttons to the toolbarService and assign them to the primary section.
```js title="modes/longitudinal/src/index.js"
toolbarService.addButtons([...toolbarButtons, ...moreTools]);
toolbarService.createButtonSection('primary', [
'MeasurementTools',
'Zoom',
'info',
'WindowLevel',
'Pan',
'Capture',
'Layout',
'MPR',
'Crosshairs',
'MoreTools',
]);
```
as you see we creating the button section and assigning buttons based on their Ids.
:::tip
You can even duplicate the same button in different sections and the button will be
in sync in all sections (thanks to the evaluation system).
:::
:::tip
we will add more section in the toolbar (other than primary) in the future.
:::
:::note
Don't forget to set up your toolGroups to ensure that your buttons function correctly. Buttons serve as a visual interface. When you interact with them, they execute their commands, and evaluators determine their state post-interaction.
:::
@@ -18,6 +18,8 @@ There are seven events that get publish in `ViewportGridService `:
| ACTIVE_VIEWPORT_ID_CHANGED | Fires the Id of the active viewport is changed |
| LAYOUT_CHANGED | Fires the layout is changed |
| GRID_STATE_CHANGED | Fires when the entire grid state is changed |
| VIEWPORTS_READY | Fires when the viewports are ready in the grid |
## Interface
For a more detailed look on the options and return values each of these methods