fix(toolbar): allow customizable toolbar for active viewport and allow active tool to be deactivated via a click (#3608)
Co-authored-by: Joe Boccanfuso <joe.boccanfuso@radicalimaging.com>
This commit is contained in:
1 parent
2145b42920
commit
dd6d9768bb
18 files changed
+1096
-628
No files matched your search
@@ -48,6 +48,7 @@ describe('OHIF Measurement Panel', function () {
|
||||
|
||||
cy.scrollToIndex(13);
|
||||
|
||||
// Reset to default tool so that the new add length works
|
||||
cy.addLengthMeasurement([100, 100], [200, 200]); //Adding measurement in the viewport
|
||||
|
||||
cy.get('@viewportInfoTopRight').should('contains.text', '(14/');
|
||||
|
||||
@@ -88,19 +88,7 @@ describe('OHIF MPR', () => {
|
||||
});
|
||||
|
||||
it('should correctly render Crosshairs for MPR', () => {
|
||||
cy.wait(250);
|
||||
|
||||
cy.get('[data-cy="Crosshairs"]').click();
|
||||
cy.window()
|
||||
.its('cornerstoneTools')
|
||||
.then(cornerstoneTools => {
|
||||
const state = cornerstoneTools.annotation.state.getAnnotationManager();
|
||||
|
||||
const fORMap = state.annotations;
|
||||
// it should not have crosshairs yet
|
||||
expect(Object.keys(fORMap)).to.have.length(0);
|
||||
});
|
||||
|
||||
cy.get('[data-cy="Crosshairs"]').should('not.exist');
|
||||
cy.get(':nth-child(3) > [data-cy="study-browser-thumbnail"]').dblclick();
|
||||
cy.get('[data-cy="MPR"]').click();
|
||||
cy.get('[data-cy="Crosshairs"]').click();
|
||||
@@ -131,4 +119,19 @@ describe('OHIF MPR', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should activate window level when the active Crosshairs tool for MPR is clicked', () => {
|
||||
cy.get(':nth-child(3) > [data-cy="study-browser-thumbnail"]').dblclick();
|
||||
cy.get('[data-cy="MPR"]').click();
|
||||
cy.get('[data-cy="Crosshairs"]').click();
|
||||
|
||||
// wait for the crosshairs tool to be active
|
||||
cy.get('[data-cy="Crosshairs"].active');
|
||||
|
||||
// Click the crosshairs button to deactivate it.
|
||||
cy.get('[data-cy="Crosshairs"]').click();
|
||||
|
||||
// wait for the window level button to be active
|
||||
cy.get('[data-cy="WindowLevel-split-button-primary"].active');
|
||||
});
|
||||
});
|
||||
@@ -262,7 +262,14 @@ Cypress.Commands.add(
|
||||
cy.get('@measurementToolsBtnPrimary').as('lengthButton');
|
||||
|
||||
cy.get('@lengthButton').should('have.attr', 'data-tool', 'Length');
|
||||
cy.get('@lengthButton').click();
|
||||
|
||||
cy.get('@lengthButton').then(button => {
|
||||
// Only click the length tool if it is not active, in case the length tool is set up to
|
||||
// toggle to inactive.
|
||||
if (!button.is('.active')) {
|
||||
cy.wrap(button).click();
|
||||
}
|
||||
});
|
||||
|
||||
cy.get('@lengthButton').should('have.class', 'active');
|
||||
|
||||
|
||||
@@ -63,12 +63,17 @@ export default class ToolbarService extends PubSubService {
|
||||
};
|
||||
}
|
||||
|
||||
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: 'WindowLevel', toggles: {}, groups: {} };
|
||||
} = { primaryToolId: '', toggles: {}, groups: {} };
|
||||
|
||||
buttonSections: Record<string, unknown> = {
|
||||
/**
|
||||
* primary: ['Zoom', 'Wwwc'],
|
||||
@@ -78,6 +83,8 @@ export default class ToolbarService extends PubSubService {
|
||||
_commandsManager: CommandsManager;
|
||||
extensionManager: ExtensionManager;
|
||||
|
||||
defaultTool: Record<string, unknown>;
|
||||
|
||||
constructor(commandsManager: CommandsManager) {
|
||||
super(EVENTS);
|
||||
this._commandsManager = commandsManager;
|
||||
@@ -103,6 +110,19 @@ export default class ToolbarService extends PubSubService {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public setDefaultTool(interaction) {
|
||||
this.defaultTool = interaction;
|
||||
}
|
||||
|
||||
public getDefaultTool() {
|
||||
return this.defaultTool;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} interaction - can be undefined to run nothing
|
||||
@@ -125,25 +145,20 @@ export default class ToolbarService extends PubSubService {
|
||||
|
||||
switch (interactionType) {
|
||||
case 'action': {
|
||||
commands.forEach(({ commandName, commandOptions, context }) => {
|
||||
if (commandName) {
|
||||
commandsManager.runCommand(
|
||||
commandName,
|
||||
{
|
||||
...commandOptions,
|
||||
...options,
|
||||
},
|
||||
context
|
||||
);
|
||||
}
|
||||
});
|
||||
commandsManager.run(commands, options);
|
||||
break;
|
||||
}
|
||||
case 'tool': {
|
||||
try {
|
||||
commands.forEach(({ commandName = 'setToolActive', commandOptions, context }) => {
|
||||
commandsManager.runCommand(commandName, commandOptions, context);
|
||||
});
|
||||
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);
|
||||
|
||||
// only set the primary tool if no error was thrown.
|
||||
// if the itemId is not undefined use it; otherwise, set the first tool in
|
||||
@@ -312,21 +327,27 @@ export default class ToolbarService extends PubSubService {
|
||||
|
||||
/**
|
||||
*
|
||||
* Finds a button section by it's name, then maps the list of string name
|
||||
* 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 {string} key
|
||||
* @param {*} props
|
||||
* @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(key, props) {
|
||||
const buttonSectionIds = this.buttonSections[key];
|
||||
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, key, metadata, props);
|
||||
const mappedBtn = this._mapButtonToDisplay(btn, toolGroupId, metadata, props);
|
||||
|
||||
buttonsInSection.push(mappedBtn);
|
||||
});
|
||||
|
||||
@@ -46,6 +46,8 @@ button is clicked by the user.
|
||||
|
||||
- `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
|
||||
|
||||
@@ -33,6 +33,7 @@ is expected to support, [check out it's interface in `@ohif/core`][interface]
|
||||
| `getNumViewportPanes()` | Gets the number of visible viewport panes |
|
||||
| `getLayoutOptionsFromState(gridState)` | Utility method that produces a `ViewportLayoutOptions` based on the passed in state|
|
||||
| `getActiveViewportId()` | Returns the viewport Id of the active viewport in the grid|
|
||||
| `getActiveViewportOptionByKey(key)` | Gets the specified viewport option field (key) for the active viewport |
|
||||
|
||||
## Implementations
|
||||
|
||||
|
||||
@@ -300,6 +300,11 @@ export function ViewportGridProvider({ children, service }) {
|
||||
return viewportGridState;
|
||||
}, [viewportGridState]);
|
||||
|
||||
const getActiveViewportOptionByKey = (key: string) => {
|
||||
const { viewports, activeViewportId } = viewportGridState;
|
||||
return viewports.get(activeViewportId)?.viewportOptions?.[key];
|
||||
};
|
||||
|
||||
const setActiveViewportId = useCallback(
|
||||
index => dispatch({ type: 'SET_ACTIVE_VIEWPORT_ID', payload: index }),
|
||||
[dispatch]
|
||||
@@ -400,6 +405,7 @@ export function ViewportGridProvider({ children, service }) {
|
||||
reset: () => service.reset(),
|
||||
set: gridLayoutState => service.setState(gridLayoutState), // run it through the service itself since we want to publish events
|
||||
getNumViewportPanes,
|
||||
getActiveViewportOptionByKey,
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
Reference in new issue
Block a user