diff --git a/extensions/cornerstone/src/utils/mpr/toggleMPRHangingProtocol.ts b/extensions/cornerstone/src/utils/mpr/toggleMPRHangingProtocol.ts index cce8869a3..dfe18f5f4 100644 --- a/extensions/cornerstone/src/utils/mpr/toggleMPRHangingProtocol.ts +++ b/extensions/cornerstone/src/utils/mpr/toggleMPRHangingProtocol.ts @@ -45,6 +45,10 @@ export default function toggleMPRHangingProtocol({ ToolBarService, } = servicesManager.services; + // TODO Introduce a service to persist the state of the current hanging protocol/app. + // So all of the code to persist the state here will no longer be needed. Perhaps + // just the id of the current hanging protocol to toggle MPR off is needed. + const { activeViewportIndex, viewports, diff --git a/extensions/default/src/Toolbar/Toolbar.tsx b/extensions/default/src/Toolbar/Toolbar.tsx index 3a4bd8164..68a064754 100644 --- a/extensions/default/src/Toolbar/Toolbar.tsx +++ b/extensions/default/src/Toolbar/Toolbar.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react'; +import classnames from 'classnames'; export default function Toolbar({ servicesManager }) { const { ToolBarService } = servicesManager.services; @@ -46,15 +47,18 @@ export default function Toolbar({ servicesManager }) { // These can... Trigger toolbar events based on updates? // Then sync using useEffect, or simply modify the state here? return ( - ToolBarService.recordInteraction(args)} - servicesManager={servicesManager} - /> + // The margin for separating the tools on the toolbar should go here and NOT in each individual component (button) item. + // This allows for the individual items to be included in other UI components where perhaps alternative margins are desired. +
+ ToolBarService.recordInteraction(args)} + servicesManager={servicesManager} + /> +
); })} diff --git a/extensions/default/src/Toolbar/ToolbarLayoutSelector.tsx b/extensions/default/src/Toolbar/ToolbarLayoutSelector.tsx index a2e4509cf..56efcda34 100644 --- a/extensions/default/src/Toolbar/ToolbarLayoutSelector.tsx +++ b/extensions/default/src/Toolbar/ToolbarLayoutSelector.tsx @@ -6,12 +6,18 @@ import { useViewportGrid, } from '@ohif/ui'; -function LayoutSelector({ rows, columns, servicesManager }) { +function LayoutSelector({ + rows, + columns, + className, + servicesManager, + ...rest +}) { const [isOpen, setIsOpen] = useState(false); const [disableSelector, setDisableSelector] = useState(false); const [viewportGridState, viewportGridService] = useViewportGrid(); - const { HangingProtocolService } = servicesManager.services; + const { HangingProtocolService, ToolBarService } = servicesManager.services; const closeOnOutsideClick = () => { if (isOpen) { @@ -24,12 +30,6 @@ function LayoutSelector({ rows, columns, servicesManager }) { HangingProtocolService.EVENTS.PROTOCOL_CHANGED, evt => { const { protocol } = evt; - - if (protocol.id === 'mpr') { - setDisableSelector(true); - } else { - setDisableSelector(false); - } } ); @@ -55,20 +55,42 @@ function LayoutSelector({ rows, columns, servicesManager }) { const onInteractionHandler = () => setIsOpen(!isOpen); const DropdownContent = isOpen ? OHIFLayoutSelector : null; + const onSelectionHandler = ({ numRows, numCols }) => { + // TODO Introduce a service to persist the state of the current hanging protocol/app. + + // TODO Here the layout change will amount to a change of hanging protocol as specified by the extension for this layout selector tool + // followed by the change of the grid itself. + if (HangingProtocolService.getActiveProtocol().protocol.id === 'mpr') { + ToolBarService.recordInteraction({ + groupId: 'MPR', + itemId: 'MPR', + interactionType: 'toggle', + commands: [ + { + commandName: 'toggleMPR', + commandOptions: {}, + context: 'CORNERSTONE', + }, + ], + }); + } + viewportGridService.setLayout({ numRows, numCols }); + }; + return ( { - viewportGridService.setLayout({ numRows, numCols }); - }} + onSelection={onSelectionHandler} /> ) } diff --git a/extensions/default/src/getToolbarModule.tsx b/extensions/default/src/getToolbarModule.tsx index 260ef111b..f29744d60 100644 --- a/extensions/default/src/getToolbarModule.tsx +++ b/extensions/default/src/getToolbarModule.tsx @@ -35,27 +35,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }) { { name: 'ohif.toggle', defaultComponent: ToolbarButton, - requiredConfig: [], - optionalConfig: [], - requiredProps: [], - optionalProps: [], - clickHandler: (evt, clickedBtn, btnSectionName) => { - const { props } = clickedBtn; - const allButtons = toolbarService.getButtons(); - const thisButton = allButtons[clickedBtn.id]; - - // Set our clicked button to active - thisButton.props.isActive = !thisButton.props.isActive; - - // Run button logic/command - commandsManager.runCommand(props.commandName, props.commandOptions); - - // What if just toggled "content"? - // commandName OR content? - - // Set buttons & trigger notification - toolbarService.setButtons(allButtons); - }, + clickHandler: () => {}, }, ]; } diff --git a/modes/longitudinal/src/toolbarButtons.js b/modes/longitudinal/src/toolbarButtons.js index c6dcd101f..a8c8a8535 100644 --- a/modes/longitudinal/src/toolbarButtons.js +++ b/modes/longitudinal/src/toolbarButtons.js @@ -15,7 +15,7 @@ const { windowLevelPresets } = defaults; * @param {*} icon * @param {*} label */ -function _createButton(type, id, icon, label, commands, tooltip) { +function _createButton(type, id, icon, label, commands, tooltip, uiType) { return { id, icon, @@ -23,6 +23,7 @@ function _createButton(type, id, icon, label, commands, tooltip) { type, commands, tooltip, + uiType, }; } diff --git a/platform/core/src/services/ToolBarService/ToolBarService.ts b/platform/core/src/services/ToolBarService/ToolBarService.ts index 908899f21..3aa745bb2 100644 --- a/platform/core/src/services/ToolBarService/ToolBarService.ts +++ b/platform/core/src/services/ToolBarService/ToolBarService.ts @@ -287,4 +287,10 @@ export default class ToolBarService { componentProps: Object.assign({}, btn.props, props), }; } + + getButtonComponentForUIType(uiType: string) { + return uiType + ? this._buttonTypes()[uiType]?.defaultComponent ?? null + : null; + } } diff --git a/platform/docs/docs/platform/extensions/modules/toolbar.md b/platform/docs/docs/platform/extensions/modules/toolbar.md index 95ff04649..bf48fda6c 100644 --- a/platform/docs/docs/platform/extensions/modules/toolbar.md +++ b/platform/docs/docs/platform/extensions/modules/toolbar.md @@ -44,6 +44,11 @@ export default function getToolbarModule({ commandsManager, servicesManager }) { defaultComponent: ToolbarLayoutSelector, clickHandler: (evt, clickedBtn, btnSectionName) => {}, }, + { + name: 'ohif.toggle', + defaultComponent: ToolbarButton, + clickHandler: () => {}, + }, ]; } ``` @@ -129,8 +134,11 @@ There are three main types of toolbar 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 `secondary` properties can be a simple arrow down (`chevron-down` icon) +- 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 diff --git a/platform/ui/src/components/IconButton/IconButton.tsx b/platform/ui/src/components/IconButton/IconButton.tsx index 93bff34c5..d073443d7 100644 --- a/platform/ui/src/components/IconButton/IconButton.tsx +++ b/platform/ui/src/components/IconButton/IconButton.tsx @@ -140,8 +140,8 @@ const IconButton = ({ ref={buttonElement} onClick={handleOnClick} type={type} - data-cy={id} - {...rest} + data-cy={rest['data-cy'] ?? id} + data-tool={rest['data-tool']} > {React.cloneElement(children, { className: classnames(iconSizeClasses[size], 'fill-current'), diff --git a/platform/ui/src/components/SplitButton/SplitButton.tsx b/platform/ui/src/components/SplitButton/SplitButton.tsx index f70c566af..b9d998c94 100644 --- a/platform/ui/src/components/SplitButton/SplitButton.tsx +++ b/platform/ui/src/components/SplitButton/SplitButton.tsx @@ -4,16 +4,17 @@ import classNames from 'classnames'; import OutsideClickHandler from 'react-outside-click-handler'; import { useTranslation } from 'react-i18next'; -import { Icon, Tooltip, ListMenu } from '../'; +import { Icon, Tooltip, ListMenu, ToolbarButton } from '../'; const baseClasses = { Button: - 'flex items-center rounded-md border-transparent border-2 cursor-pointer group/button', + 'flex items-center rounded-md border-transparent cursor-pointer group/button', Primary: - 'h-full flex flex-1 items-center rounded-md rounded-tr-none rounded-br-none group/primary', + // By default border on left, top and bottom for hover effect and only rounded on left side. + // Extra padding on right to componensate for no right border. + 'h-full border-l-2 border-t-2 border-b-2 rounded-tl-md rounded-bl-md group/primary !pl-2 !py-2', Secondary: - 'h-full flex items-center justify-center rounded-tr-md rounded-br-md w-4 group/secondary', - PrimaryIcon: 'w-5 h-5', + 'h-full flex items-center justify-center rounded-tr-md rounded-br-md w-4 border-2 border-transparent group/secondary', SecondaryIcon: 'w-4 h-full stroke-1', Separator: 'border-l py-2.5', Content: 'absolute z-10 top-0 mt-12', @@ -31,29 +32,31 @@ const classes = { Primary: ({ primary, isExpanded }) => classNames( baseClasses.Primary, - primary.isActive && !isExpanded - ? 'bg-primary-light rounded-tr-md rounded-br-md active' - : isExpanded - ? 'bg-primary-dark' - : 'bg-secondary-dark hover:bg-primary-dark' + primary.isActive + ? isExpanded + ? 'border-primary-dark !bg-primary-dark hover:border-primary-dark text-primary-light' + : `${ + primary.isToggle + ? 'border-secondary-dark bg-secondary-light' + : 'border-primary-light bg-primary-light' + } + border-2 rounded-md !p-2` // Full, rounded border with less right padding when active. + : `focus:!text-black focus:!rounded-md focus:!border-primary-light focus:!bg-primary-light + ${ + isExpanded + ? 'border-primary-dark bg-primary-dark !text-primary-light' + : 'border-secondary-dark bg-secondary-dark group-hover/button:border-primary-dark group-hover/button:text-primary-light hover:bg-primary-dark hover:border-primary-dark focus:!text-black' + } + ` ), Secondary: ({ isExpanded, primary }) => classNames( baseClasses.Secondary, isExpanded - ? 'bg-primary-light rounded-tr-md rounded-br-md' + ? 'bg-primary-light !rounded-tr-md !rounded-br-md' : primary.isActive ? 'bg-secondary-dark' - : 'hover:bg-primary-dark bg-secondary-dark' - ), - PrimaryIcon: ({ primary, isExpanded }) => - classNames( - baseClasses.PrimaryIcon, - !primary.isActive && - 'group-hover/primary:text-primary-light group-hover/secondary:text-primary-light group-hover/button:text-primary-light', - primary.isActive && !isExpanded - ? 'text-primary-dark ' - : 'text-common-bright' + : 'hover:bg-primary-dark bg-secondary-dark group-hover/button:border-primary-dark' ), SecondaryIcon: ({ isExpanded }) => classNames( @@ -85,9 +88,12 @@ const SplitButton = ({ items: _items, renderer, onInteraction, + servicesManager, }) => { const { t } = useTranslation('Buttons'); + const { ToolBarService } = servicesManager.services; + const { primaryToolId, toggles } = bState; /* Bubbles up individual item clicks */ const getSplitButtonItems = items => @@ -107,7 +113,7 @@ const SplitButton = ({ setState(state => ({ ...state, - primary: !isAction ? { ...item, index } : state.primary, + primary: !isAction && isRadio ? { ...item, index } : state.primary, isExpanded: false, items: getSplitButtonItems(_items).filter(item => isRadio && !isAction ? item.index !== index : true @@ -133,21 +139,44 @@ const SplitButton = ({ setState(state => ({ ...state, isHovering: false })); const outsideClickHandler = () => setState(state => ({ ...state, isExpanded: false })); - const onPrimaryClickHandler = () => { - onInteraction({ - groupId, - itemId: state.primary.id, - interactionType: state.primary.type, - // splitButtonId? (so we can track group?) - // info to fire item's command/event? - // - commands: state.primary.commands, - }); - }; + const isPrimaryToggle = state.primary.type === 'toggle'; const isPrimaryActive = (state.primary.type === 'tool' && primaryToolId === state.primary.id) || - (state.primary.type === 'toggle' && toggles[state.primary.id] === true); + (isPrimaryToggle && toggles[state.primary.id] === true); + + const PrimaryButtonComponent = + ToolBarService.getButtonComponentForUIType(state.primary.uiType) ?? + ToolbarButton; + + const primaryButtonClassName = classes.Primary({ + ...state, + primary: { isActive: isPrimaryActive, isToggle: isPrimaryToggle }, + }); + + const DefaultListItemRenderer = ({ type, icon, label, t, id }) => { + const isActive = type === 'toggle' && toggles[id] === true; + + return ( +
+ + + + {t(label)} +
+ ); + }; + + const listItemRenderer = renderer || DefaultListItemRenderer; return ( @@ -162,32 +191,20 @@ const SplitButton = ({ onMouseLeave={onMouseLeaveHandler} >
-
- -
- -
-
+
+ ToolBarService.recordInteraction(args)} + servicesManager={servicesManager} + // All rounding is taken care of by className + rounded="none" + className={primaryButtonClassName} + data-tool={state.primary.id} + data-cy={`${groupId}-split-button-primary`} + />
renderer({ ...args, t })} + bState={bState} + renderer={args => listItemRenderer({ ...args, t })} />
@@ -234,24 +252,6 @@ const SplitButton = ({ ); }; -const DefaultListItemRenderer = ({ icon, label, isActive, t }) => ( -
- - - - - {t(label)} - -
-); - -const noop = () => {}; - SplitButton.defaultProps = { isRadio: false, isAction: false, @@ -266,7 +266,7 @@ SplitButton.defaultProps = { tooltip: 'More Measure Tools', }, items: [], - renderer: DefaultListItemRenderer, + renderer: null, }; SplitButton.propTypes = { diff --git a/platform/ui/src/components/ToolbarButton/ToolbarButton.tsx b/platform/ui/src/components/ToolbarButton/ToolbarButton.tsx index 76cce19d6..b2fb20da6 100644 --- a/platform/ui/src/components/ToolbarButton/ToolbarButton.tsx +++ b/platform/ui/src/components/ToolbarButton/ToolbarButton.tsx @@ -14,7 +14,9 @@ const ToolbarButton = ({ dropdownContent, // isActive: _isActive, + className, bState = {}, + ...rest // }) => { const { primaryToolId } = bState; @@ -49,7 +51,7 @@ const ToolbarButton = ({ variant={isActive ? 'contained' : 'text'} bgColor={bgClasses[type]} size="toolbar" - className={classnames('mx-1', activeClass, classes[type])} + className={classnames(activeClass, classes[type], className)} onClick={() => { onInteraction({ itemId: id, @@ -60,6 +62,7 @@ const ToolbarButton = ({ name={label} key={id} id={id} + {...rest} >