diff --git a/extensions/cornerstone/src/OHIFCornerstoneViewport.js b/extensions/cornerstone/src/OHIFCornerstoneViewport.js index e498c3421..6c2a03547 100644 --- a/extensions/cornerstone/src/OHIFCornerstoneViewport.js +++ b/extensions/cornerstone/src/OHIFCornerstoneViewport.js @@ -6,12 +6,6 @@ import debounce from 'lodash.debounce'; import { setEnabledElement } from './state'; - -// const cine = viewportSpecificData.cine; - -// isPlaying = cine.isPlaying === true; -// frameRate = cine.cineFrameRate || frameRate; - const { StackManager } = OHIF.utils; class OHIFCornerstoneViewport extends Component { diff --git a/extensions/default/src/ViewerLayout/index.jsx b/extensions/default/src/ViewerLayout/index.jsx index db5e38b1f..2b506f4f6 100644 --- a/extensions/default/src/ViewerLayout/index.jsx +++ b/extensions/default/src/ViewerLayout/index.jsx @@ -91,10 +91,10 @@ function ToolbarSecondary({ servicesManager }) { isActive: false, }; const [toolbars, setToolbars] = useState({ primary: [], secondary: [] }); - const [activeTool, setActiveTool] = useState(defaultTool); + const [nestedActiveTool, setNestedActiveTool] = useState(defaultTool); const setActiveToolHandler = (tool, isNested) => { - setActiveTool(isNested ? tool : defaultTool); + setNestedActiveTool(isNested ? tool : defaultTool); }; const onPrimaryClickHandler = (evt, btn) => { @@ -257,6 +257,7 @@ function ViewerLayout({ diff --git a/extensions/default/src/commandsModule.js b/extensions/default/src/commandsModule.js index 0066c0302..f1eb40d92 100644 --- a/extensions/default/src/commandsModule.js +++ b/extensions/default/src/commandsModule.js @@ -1,10 +1,17 @@ -const commandsModule = ({ servicesManager }) => { - const { MeasurementService } = servicesManager.services; +const commandsModule = ({ servicesManager, commandsManager }) => { + const { MeasurementService, ViewportGridService, ToolBarService, CineService } = servicesManager.services; const actions = { clearMeasurements: () => { MeasurementService.clear(); }, + toggleCine: () => { + const { viewports } = ViewportGridService.getState(); + const { isCineEnabled } = CineService.getState(); + CineService.setIsCineEnabled(!isCineEnabled); + ToolBarService.setButton('Cine', { props: { isActive: !isCineEnabled } }); + viewports.forEach((_, index) => CineService.setCine({ id: index, isPlaying: false })); + }, }; const definitions = { @@ -13,6 +20,11 @@ const commandsModule = ({ servicesManager }) => { storeContexts: [], options: {}, }, + toggleCine: { + commandFn: actions.toggleCine, + storeContexts: [], + options: {}, + }, }; return { diff --git a/extensions/default/src/getToolbarModule.js b/extensions/default/src/getToolbarModule.js index 293e5cc43..4e45c7ba8 100644 --- a/extensions/default/src/getToolbarModule.js +++ b/extensions/default/src/getToolbarModule.js @@ -85,8 +85,8 @@ export default function getToolbarModule({ commandsManager, servicesManager }) { thisButton.props.isActive = !thisButton.props.isActive; // Run button logic/command - // MAKE SURE THIS SUPPORTS TOGGLE! - // commandsManager.runCommand(props.commandName, props.commandOptions); + commandsManager.runCommand(props.commandName, props.commandOptions); + // What if just toggled "content"? // commandName OR content? diff --git a/extensions/default/src/index.js b/extensions/default/src/index.js index a87410ecc..dcaf70eef 100644 --- a/extensions/default/src/index.js +++ b/extensions/default/src/index.js @@ -18,7 +18,7 @@ export default { getPanelModule, getSopClassHandlerModule, getToolbarModule, - getCommandsModule({ servicesManager }) { - return commandsModule({ servicesManager }); + getCommandsModule({ servicesManager, commandsManager }) { + return commandsModule({ servicesManager, commandsManager }); }, }; diff --git a/extensions/measurement-tracking/src/getViewportModule.js b/extensions/measurement-tracking/src/getViewportModule.js index 5991db1f0..d53f5b77e 100644 --- a/extensions/measurement-tracking/src/getViewportModule.js +++ b/extensions/measurement-tracking/src/getViewportModule.js @@ -12,10 +12,10 @@ const OHIFCornerstoneViewport = props => { ); }; -function getViewportModule({ servicesManager }) { +function getViewportModule({ servicesManager, commandsManager }) { const ExtendedOHIFCornerstoneSRViewport = props => { return ( - + ); }; diff --git a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js index 71c63ccfd..72c03057e 100644 --- a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js +++ b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js @@ -7,6 +7,7 @@ import OHIF, { utils } from '@ohif/core'; import { Notification, ViewportActionBar, + useCine, useViewportGrid, useViewportDialog, } from '@ohif/ui'; @@ -35,10 +36,6 @@ const BaseAnnotationTool = cornerstoneTools.importInternal( 'base/BaseAnnotationTool' ); -// const cine = viewportSpecificData.cine; -// isPlaying = cine.isPlaying === true; -// frameRate = cine.cineFrameRate || frameRate; - const { StackManager } = OHIF.utils; function TrackedCornerstoneViewport({ @@ -47,6 +44,7 @@ function TrackedCornerstoneViewport({ displaySet, viewportIndex, servicesManager, + commandsManager }) { const { ToolBarService, @@ -54,12 +52,11 @@ function TrackedCornerstoneViewport({ MeasurementService, } = servicesManager.services; const [trackedMeasurements] = useTrackedMeasurements(); - const [{ activeViewportIndex, viewports }] = useViewportGrid(); - // viewportIndex, onSubmit + const [{ activeViewportIndex, viewports }, viewportGridService] = useViewportGrid(); + const [{ isCineEnabled, cines }, cineService] = useCine(); const [viewportDialogState, viewportDialogApi] = useViewportDialog(); const [viewportData, setViewportData] = useState(null); const [element, setElement] = useState(null); - const [isTracked, setIsTracked] = useState(false); const [trackedMeasurementId, setTrackedMeasurementId] = useState(null); @@ -71,6 +68,10 @@ function TrackedCornerstoneViewport({ }; }, []); + useEffect(() => { + cineService.setCine({ id: viewportIndex }); + }, [viewportIndex]); + useEffect(() => { const unsubcribeFromJumpToMeasurementEvents = _subscribeToJumpToMeasurementEvents( MeasurementService, @@ -298,7 +299,9 @@ function TrackedCornerstoneViewport({ ); } - // TODO -> disabled double click for now: onDoubleClick={_onDoubleClick} + const cine = cines[viewportIndex]; + const isPlaying = cine && cine.isPlaying || false; + const frameRate = cine && cine.frameRate || 24; return ( <> @@ -333,6 +336,14 @@ function TrackedCornerstoneViewport({ scanner: ManufacturerModelName || '', }, }} + showNavArrows={!isCineEnabled} + showCine={isCineEnabled} + cineProps={{ + isPlaying, + onClose: () => commandsManager.runCommand('toggleCine'), + onPlayPauseChange: isPlaying => cineService.setCine({ id: activeViewportIndex, isPlaying }), + onFrameRateChange: frameRate => cineService.setCine({ id: activeViewportIndex, frameRate }), + }} /> {/* TODO: Viewport interface to accept stack or layers of content like this? */}
@@ -347,8 +358,8 @@ function TrackedCornerstoneViewport({ // TODO: ViewportGrid Context? isActive={true} // todo isStackPrefetchEnabled={true} // todo - isPlaying={false} - frameRate={24} + isPlaying={isPlaying} + frameRate={frameRate} isOverlayVisible={true} loadingIndicatorComponent={ViewportLoadingIndicator} viewportOverlayComponent={props => { @@ -408,6 +419,7 @@ function _getCornerstoneStack(displaySet, dataSource) { return stack; } +// TODO -> disabled double click for now: onDoubleClick={_onDoubleClick} function _onDoubleClick() { const cancelActiveManipulatorsForElement = cornerstoneTools.getModule( 'manipulatorState' diff --git a/modes/longitudinal/src/toolbarButtons.js b/modes/longitudinal/src/toolbarButtons.js index 49a933365..1574ad586 100644 --- a/modes/longitudinal/src/toolbarButtons.js +++ b/modes/longitudinal/src/toolbarButtons.js @@ -189,14 +189,17 @@ export default [ type: 'primary', }, }, - // TODO: Toggle { id: 'Cine', - type: 'ohif.action', + type: 'ohif.toggle', + config: { + groupName: 'primaryTool', + }, props: { + isActive: false, icon: 'old-youtube', label: 'Cine', - commandName: '', + commandName: 'toggleCine', type: 'primary', }, }, diff --git a/platform/core/src/defaults/hotkeyBindings.js b/platform/core/src/defaults/hotkeyBindings.js index df7346d58..c8fd4d446 100644 --- a/platform/core/src/defaults/hotkeyBindings.js +++ b/platform/core/src/defaults/hotkeyBindings.js @@ -22,6 +22,11 @@ export default [ keys: ['v'], isEditable: true, }, + { + commandName: 'toggleCine', + label: 'Cine', + keys: ['c'], + }, { commandName: 'invertViewport', label: 'Invert', diff --git a/platform/core/src/index.js b/platform/core/src/index.js index df4ce4d39..9eb735a10 100644 --- a/platform/core/src/index.js +++ b/platform/core/src/index.js @@ -24,6 +24,7 @@ import utils from './utils/'; import defaults from './defaults'; import { + CineService, UIDialogService, UIModalService, UINotificationService, @@ -31,7 +32,7 @@ import { // DicomMetadataStore, DisplaySetService, - ToolBarSerivce, // TODO: Typo + ToolBarService, MeasurementService, ViewportGridService, HangingProtocolService, @@ -73,13 +74,14 @@ const OHIF = { measurements, hangingProtocols, // + CineService, UIDialogService, UIModalService, UINotificationService, UIViewportDialogService, DisplaySetService, MeasurementService, - ToolBarSerivce, // TODO: TYPO + ToolBarService, // TODO: TYPO ViewportGridService, HangingProtocolService, IWebApiDataSource, @@ -117,13 +119,14 @@ export { measurements, hangingProtocols, // + CineService, UIDialogService, UIModalService, UINotificationService, UIViewportDialogService, DisplaySetService, MeasurementService, - ToolBarSerivce, + ToolBarService, ViewportGridService, HangingProtocolService, IWebApiDataSource, diff --git a/platform/core/src/services/CineService/CineService.js b/platform/core/src/services/CineService/CineService.js new file mode 100644 index 000000000..4060ea864 --- /dev/null +++ b/platform/core/src/services/CineService/CineService.js @@ -0,0 +1,52 @@ +const name = 'CineService'; + +const publicAPI = { + name, + getState: _getState, + setCine: _setCine, + setIsCineEnabled: _setIsCineEnabled, + setServiceImplementation, +}; + +const serviceImplementation = { + _getState: () => console.warn('getState() NOT IMPLEMENTED'), + _setCine: () => + console.warn('setCine() 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 setServiceImplementation({ + getState: getStateImplementation, + setCine: setCineImplementation, + setIsCineEnabled: setIsCineEnabledImplementation, +}) { + if (getStateImplementation) { + serviceImplementation._getState = getStateImplementation; + } + if (setCineImplementation) { + serviceImplementation._setCine = setCineImplementation; + } + if (setIsCineEnabledImplementation) { + serviceImplementation._setIsCineEnabled = setIsCineEnabledImplementation; + } +} + +export default { + name, + create: ({ configuration = {} }) => { + return publicAPI; + }, +}; diff --git a/platform/core/src/services/CineService/index.js b/platform/core/src/services/CineService/index.js new file mode 100644 index 000000000..e9803dd24 --- /dev/null +++ b/platform/core/src/services/CineService/index.js @@ -0,0 +1,2 @@ +import CineService from './CineService'; +export default CineService; diff --git a/platform/core/src/services/ToolBarService/ToolBarService.js b/platform/core/src/services/ToolBarService/ToolBarService.js index c5c90d56b..daff7ebe4 100644 --- a/platform/core/src/services/ToolBarService/ToolBarService.js +++ b/platform/core/src/services/ToolBarService/ToolBarService.js @@ -1,3 +1,4 @@ +import merge from 'lodash.merge'; import pubSubServiceInterface from '../_shared/pubSubServiceInterface'; const EVENTS = { @@ -36,9 +37,23 @@ export default class ToolBarService { }); } + setButton(id, button) { + if (this.buttons[id]) { + this.buttons[id] = merge(this.buttons[id], button); + this._broadcastChange(this.EVENTS.TOOL_BAR_MODIFIED, { + buttons: this.buttons, + button: this.buttons[id], + buttonSections: this.buttonSections + }); + } + } + setButtons(buttons) { this.buttons = buttons; - this._broadcastChange(this.EVENTS.TOOL_BAR_MODIFIED, {}); + this._broadcastChange(this.EVENTS.TOOL_BAR_MODIFIED, { + buttons: this.buttons, + buttonSections: this.buttonSections + }); } _buttonTypes() { @@ -61,7 +76,6 @@ export default class ToolBarService { return buttonTypes; } - createButtonSection(key, buttons) { // Maybe do this mapping at time of return, instead of time of create // Props check important for validation here... diff --git a/platform/core/src/services/ViewportGridService/ViewportGridService.js b/platform/core/src/services/ViewportGridService/ViewportGridService.js index f72df1188..34a1a2211 100644 --- a/platform/core/src/services/ViewportGridService/ViewportGridService.js +++ b/platform/core/src/services/ViewportGridService/ViewportGridService.js @@ -50,6 +50,7 @@ function _set(state) { function _reset() { return serviceImplementation._reset({}); } + function _setCachedLayout({ numCols, numRows, viewports }) { return serviceImplementation._setLayout({ numCols, numRows, viewports }); } diff --git a/platform/core/src/services/index.js b/platform/core/src/services/index.js index 7d119333d..85eb748ac 100644 --- a/platform/core/src/services/index.js +++ b/platform/core/src/services/index.js @@ -6,8 +6,9 @@ import UINotificationService from './UINotificationService'; import UIViewportDialogService from './UIViewportDialogService'; import DicomMetadataStore from './DicomMetadataStore'; import DisplaySetService from './DisplaySetService'; -import ToolBarSerivce from './ToolBarService'; +import ToolBarService from './ToolBarService'; import ViewportGridService from './ViewportGridService'; +import CineService from './CineService'; import HangingProtocolService from './HangingProtocolService'; export { @@ -19,7 +20,8 @@ export { UIViewportDialogService, DicomMetadataStore, DisplaySetService, - ToolBarSerivce, + ToolBarService, ViewportGridService, HangingProtocolService, + CineService }; diff --git a/platform/ui/index.js b/platform/ui/index.js index 8c1bde12d..3571877e9 100644 --- a/platform/ui/index.js +++ b/platform/ui/index.js @@ -4,6 +4,8 @@ export { utils }; /** CONTEXT/HOOKS */ export { + useCine, + CineProvider, DialogProvider, useDialog, withDialog, @@ -38,6 +40,7 @@ export { Button, ButtonGroup, ContextMenu, + CinePlayer, DateRange, Dialog, Dropdown, diff --git a/platform/ui/src/assets/icons/old-play.svg b/platform/ui/src/assets/icons/old-play.svg new file mode 100644 index 000000000..b947e1934 --- /dev/null +++ b/platform/ui/src/assets/icons/old-play.svg @@ -0,0 +1,9 @@ + + Play + + diff --git a/platform/ui/src/assets/icons/old-stop.svg b/platform/ui/src/assets/icons/old-stop.svg new file mode 100644 index 000000000..264d09a2f --- /dev/null +++ b/platform/ui/src/assets/icons/old-stop.svg @@ -0,0 +1,9 @@ + + Stop + + diff --git a/platform/ui/src/components/CinePlayer/CinePlayer.jsx b/platform/ui/src/components/CinePlayer/CinePlayer.jsx new file mode 100644 index 000000000..7ff9bf466 --- /dev/null +++ b/platform/ui/src/components/CinePlayer/CinePlayer.jsx @@ -0,0 +1,97 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import debounce from 'lodash.debounce'; +import { IconButton, Icon } from '@ohif/ui'; + +import './CinePlayerCustomInputRange.css'; + +const CinePlayer = ({ + isPlaying, + minFrameRate, + maxFrameRate, + stepFrameRate, + frameRate: defaultFrameRate, + onFrameRateChange, + onPlayPauseChange, + onClose +}) => { + const [frameRate, setFrameRate] = useState(defaultFrameRate); + const debouncedSetFrameRate = debounce(onFrameRateChange, 300); + + const onFrameRateChangeHandler = ({ target }) => { + const frameRate = parseFloat(target.value); + debouncedSetFrameRate(frameRate); + setFrameRate(frameRate); + }; + + const onPlayPauseChangeHandler = () => onPlayPauseChange(!isPlaying); + + const action = { + false: { icon: 'old-play' }, + true: { icon: 'old-stop' } + }; + + return ( +
+ + + +
+ +

{`${frameRate.toFixed(1)} fps`}

+
+ + + +
+ ); +}; + +const noop = () => { }; + +CinePlayer.defaultProps = { + isPlaying: false, + minFrameRate: 1, + maxFrameRate: 90, + stepFrameRate: 1, + frameRate: 24, + onPlayPauseChange: noop, + onFrameRateChange: noop, + onClose: noop +}; + +CinePlayer.propTypes = { + /** Minimum value for range slider */ + minFrameRate: PropTypes.number.isRequired, + /** Maximum value for range slider */ + maxFrameRate: PropTypes.number.isRequired, + /** Increment range slider can "step" in either direction */ + stepFrameRate: PropTypes.number.isRequired, + frameRate: PropTypes.number.isRequired, + /** 'true' if playing, 'false' if paused */ + isPlaying: PropTypes.bool.isRequired, + onPlayPauseChange: PropTypes.func, + onFrameRateChange: PropTypes.func, + onClose: PropTypes.func, +}; + +export default CinePlayer; diff --git a/platform/ui/src/components/CinePlayer/CinePlayer.mdx b/platform/ui/src/components/CinePlayer/CinePlayer.mdx new file mode 100644 index 000000000..5abc93249 --- /dev/null +++ b/platform/ui/src/components/CinePlayer/CinePlayer.mdx @@ -0,0 +1,42 @@ +--- +name: CinePlayer +menu: General +route: components/CinePlayer +--- + +import { useState } from 'react'; +import { Playground, Props } from 'docz'; +import { CinePlayer } from '@ohif/ui'; + +# Input Text + +## Import + +```javascript +import { CinePlayer } from '@ohif/ui'; +``` + +## Basic usage + + + {() => { + return ( +
+
+ +
+
+ ) + }} + +
+ +## Properties + + diff --git a/platform/ui/src/components/CinePlayer/CinePlayerCustomInputRange.css b/platform/ui/src/components/CinePlayer/CinePlayerCustomInputRange.css new file mode 100644 index 000000000..76146efb8 --- /dev/null +++ b/platform/ui/src/components/CinePlayer/CinePlayerCustomInputRange.css @@ -0,0 +1,105 @@ +/* + * This is a custom input style scoped specifically to CinePlayer + * written in plain CSS with color variables from tailwind + * to avoid complex compatibility configuration. + */ +.CinePlayer input[type=range] { + -webkit-appearance: none; + background: transparent; + width: 100%; +} + +.CinePlayer input[type=range]:focus { + outline: none; +} + +.CinePlayer input[type=range]::-webkit-slider-runnable-track { + width: 100%; + height: 2px; + cursor: pointer; + animate: 0.2s; + box-shadow: 0px 0px 0px #000000; + @apply bg-primary-light; + border-radius: 5px; + border: 0px solid #000000; +} + +.CinePlayer input[type=range]::-webkit-slider-thumb { + box-shadow: 0px 0px 0px #000000; + border: 4px solid #000000; + height: 18px; + width: 17px; + border-radius: 50px; + @apply bg-primary-light; + cursor: pointer; + -webkit-appearance: none; + margin-top: -9px; +} + +.CinePlayer input[type=range]:focus::-webkit-slider-runnable-track { + @apply bg-primary-light; +} + +.CinePlayer input[type=range]::-moz-range-track { + width: 100%; + height: 2px; + cursor: pointer; + animate: 0.2s; + box-shadow: 0px 0px 0px #000000; + @apply bg-primary-light; + border-radius: 5px; + border: 0px solid #000000; +} + +.CinePlayer input[type=range]::-moz-range-thumb { + box-shadow: 0px 0px 0px #000000; + border: 4px solid #000000; + height: 18px; + width: 17px; + border-radius: 50px; + @apply bg-primary-light; + cursor: pointer; +} + +.CinePlayer input[type=range]::-ms-track { + width: 100%; + height: 2px; + cursor: pointer; + animate: 0.2s; + background: transparent; + border-color: transparent; + color: transparent; +} + +.CinePlayer input[type=range]::-ms-fill-lower { + @apply bg-primary-light; + border: 0px solid #000000; + border-radius: 10px; + box-shadow: 0px 0px 0px #000000; +} + +.CinePlayer input[type=range]::-ms-fill-upper { + @apply bg-primary-light; + border: 0px solid #000000; + border-radius: 10px; + box-shadow: 0px 0px 0px #000000; +} + +.CinePlayer input[type=range]::-ms-thumb { + margin-top: 1px; + box-shadow: 0px 0px 0px #000000; + border: 4px solid #000000; + height: 18px; + width: 17px; + border-radius: 50px; + @apply bg-primary-light; + cursor: pointer; +} + +.CinePlayer input[type=range]:focus::-ms-fill-lower { + @apply bg-primary-light; +} + +.CinePlayer input[type=range]:focus::-ms-fill-upper { + @apply bg-primary-light; +} diff --git a/platform/ui/src/components/CinePlayer/index.js b/platform/ui/src/components/CinePlayer/index.js new file mode 100644 index 000000000..3a0fa6fa3 --- /dev/null +++ b/platform/ui/src/components/CinePlayer/index.js @@ -0,0 +1,2 @@ +import CinePlayer from './CinePlayer'; +export default CinePlayer; diff --git a/platform/ui/src/components/Icon/getIcon.jsx b/platform/ui/src/components/Icon/getIcon.jsx index 0a05a372b..e7496ad02 100644 --- a/platform/ui/src/components/Icon/getIcon.jsx +++ b/platform/ui/src/components/Icon/getIcon.jsx @@ -60,6 +60,8 @@ import oldAngleLeft from './../../assets/icons/old-angle-left.svg'; import oldReset from './../../assets/icons/old-reset.svg'; import oldCircleO from './../../assets/icons/old-circle-o.svg'; import oldTrash from './../../assets/icons/old-trash.svg'; +import oldPlay from './../../assets/icons/old-play.svg'; +import oldStop from './../../assets/icons/old-stop.svg'; const ICONS = { 'arrow-down': arrowDown, @@ -121,6 +123,8 @@ const ICONS = { 'old-reset': oldReset, 'old-circle-o': oldCircleO, 'old-trash': oldTrash, + 'old-play': oldPlay, + 'old-stop': oldStop, }; /** diff --git a/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx b/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx index 8150281b8..e5303bf93 100644 --- a/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx +++ b/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx @@ -1,7 +1,7 @@ import React, { useState, useRef, useEffect } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import { Icon, ButtonGroup, Button, Tooltip } from '../'; +import { Icon, ButtonGroup, Button, Tooltip, CinePlayer } from '../'; import useOnClickOutside from '../../utils/useOnClickOutside'; const classes = { @@ -14,6 +14,8 @@ const classes = { const ViewportActionBar = ({ studyData, showNavArrows, + showCine, + cineProps, showPatientInfo: patientInfoVisibility, onSeriesChange, onDoubleClick, @@ -71,11 +73,11 @@ const ViewportActionBar = ({ if (modality === 'SR') { const TooltipMessage = isLocked ? () => ( -
- This SR is locked.
+
+ This SR is locked.
Measurements cannot be duplicated. -
- ) +
+ ) : () =>
This SR is unlocked.
; return ( <> @@ -117,13 +119,13 @@ const ViewportActionBar = ({ can be viewed
in the measurement panel ) : ( - <> - Measurements for + <> + Measurements for untracked series
will not be shown in the
measurements panel - )} + )}
@@ -137,10 +139,10 @@ const ViewportActionBar = ({ return (
-
+
{renderIconStatus()} {label} @@ -166,8 +168,8 @@ const ViewportActionBar = ({
- {showNavArrows && ( -
+ {showNavArrows && !showCine && ( +
)} -
+ {showCine && !showNavArrows && ( +
+ +
+ )} +
{ + switch (action.type) { + case 'SET_CINE': { + const { id, frameRate, isPlaying = undefined } = action.payload; + const cines = state.cines; + + if (!cines[id]) cines[id] = { id, ...DEFAULT_CINE }; + cines[id].frameRate = frameRate || cines[id].frameRate; + cines[id].isPlaying = isPlaying !== undefined ? isPlaying : cines[id].isPlaying; + + return { ...state, ...{ cines } }; + } + case 'SET_IS_CINE_ENABLED': { + return { ...state, ...{ isCineEnabled: action.payload } }; + } + default: + return action.payload; + } + }; + + const [state, dispatch] = useReducer( + reducer, + DEFAULT_STATE + ); + + const getState = useCallback(() => state, [state]); + + const setIsCineEnabled = useCallback( + isCineEnabled => dispatch({ type: 'SET_IS_CINE_ENABLED', payload: isCineEnabled }), + [dispatch] + ); + + const setCine = useCallback( + ({ id, frameRate, isPlaying }) => + dispatch({ + type: 'SET_CINE', + payload: { + id, + frameRate, + isPlaying, + }, + }), + [dispatch] + ); + + /** + * Sets the implementation of a modal service that can be used by extensions. + * + * @returns void + */ + useEffect(() => { + if (service) { + service.setServiceImplementation({ getState, setIsCineEnabled, setCine }); + } + }, [ + getState, + service, + setCine, + setIsCineEnabled, + ]); + + const api = { + getState, + setCine, + setIsCineEnabled, + }; + + return ( + + {children} + + ); +} + +CineProvider.propTypes = { + children: PropTypes.any, + service: PropTypes.shape({ + setServiceImplementation: PropTypes.func, + }).isRequired, +}; + +export const useCine = () => useContext(CineContext); diff --git a/platform/ui/src/contextProviders/ViewportGridProvider.jsx b/platform/ui/src/contextProviders/ViewportGridProvider.jsx index 04c5d7ab6..834fa5f73 100644 --- a/platform/ui/src/contextProviders/ViewportGridProvider.jsx +++ b/platform/ui/src/contextProviders/ViewportGridProvider.jsx @@ -11,9 +11,11 @@ const DEFAULT_STATE = { numRows: 1, numCols: 1, viewports: [ - // { - // displaySetInstanceUID: string, - // } + /* + * { + * displaySetInstanceUID: string, + * } + */ ], activeViewportIndex: 0, }; @@ -59,7 +61,9 @@ export function ViewportGridProvider({ children, service }) { numCols: 1, numRows: 1, activeViewportIndex: 0, - viewports: [{ displaySetInstanceUID: null }], + viewports: [{ + displaySetInstanceUID: null, + }], cachedLayout: null, }; } @@ -84,13 +88,13 @@ export function ViewportGridProvider({ children, service }) { DEFAULT_STATE ); - console.log('viewportGridState',viewportGridState) - const getState = useCallback(() => viewportGridState, [viewportGridState]); + const setActiveViewportIndex = useCallback( index => dispatch({ type: 'SET_ACTIVE_VIEWPORT_INDEX', payload: index }), [dispatch] ); + const setDisplaysetForViewport = useCallback( ({ viewportIndex, displaySetInstanceUID }) => dispatch({ @@ -123,6 +127,7 @@ export function ViewportGridProvider({ children, service }) { }), [dispatch] ); + const setCachedLayout = useCallback( payload => dispatch({ diff --git a/platform/ui/src/contextProviders/index.js b/platform/ui/src/contextProviders/index.js index 4a1e92908..c89f2db71 100644 --- a/platform/ui/src/contextProviders/index.js +++ b/platform/ui/src/contextProviders/index.js @@ -19,6 +19,12 @@ export { useImageViewer, } from './ImageViewerProvider'; +export { + CineContext, + default as CineProvider, + useCine, +} from './CineProvider'; + export { default as SnackbarProvider, useSnackbar, diff --git a/platform/viewer/src/App.jsx b/platform/viewer/src/App.jsx index 1c582d499..fba1d7846 100644 --- a/platform/viewer/src/App.jsx +++ b/platform/viewer/src/App.jsx @@ -13,6 +13,7 @@ import { ViewportDialogProvider, ViewportGridProvider, HangingProtocolProvider, + CineProvider, } from '@ohif/ui'; // Viewer Project // TODO: Should this influence study list? @@ -60,6 +61,7 @@ function App({ config, defaultExtensions }) { UIViewportDialogService, ViewportGridService, // TODO: Should this be a "UI" Service? HangingProtocolService, + CineService } = servicesManager.services; return ( @@ -70,13 +72,15 @@ function App({ config, defaultExtensions }) { - - - - {appRoutes} - - - + + + + + {appRoutes} + + + + diff --git a/platform/viewer/src/appInit.js b/platform/viewer/src/appInit.js index cd671c666..c554655ab 100644 --- a/platform/viewer/src/appInit.js +++ b/platform/viewer/src/appInit.js @@ -9,9 +9,10 @@ import { UIViewportDialogService, MeasurementService, DisplaySetService, - ToolBarSerivce, + ToolBarService, ViewportGridService, - HangingProtocolService + HangingProtocolService, + CineService // utils, // redux as reduxOHIF, } from '@ohif/core'; @@ -54,9 +55,10 @@ function appInit(appConfigOrFunc, defaultExtensions) { UIViewportDialogService, MeasurementService, DisplaySetService, - ToolBarSerivce, + ToolBarService, ViewportGridService, - HangingProtocolService + HangingProtocolService, + CineService ]); /** diff --git a/platform/viewer/src/components/ViewportGrid.jsx b/platform/viewer/src/components/ViewportGrid.jsx index 8d94dab3f..6d50730cd 100644 --- a/platform/viewer/src/components/ViewportGrid.jsx +++ b/platform/viewer/src/components/ViewportGrid.jsx @@ -1,12 +1,10 @@ /** * CSS Grid Reference: http://grid.malven.co/ */ -import React, { useEffect, useCallback, useState } from 'react'; +import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; import { ViewportGrid, ViewportPane, useViewportGrid } from '@ohif/ui'; import EmptyViewport from './EmptyViewport'; -import { classes } from '@ohif/core'; -const { ImageSet } = classes; import classNames from 'classnames'; function ViewerViewportGrid(props) { @@ -194,7 +192,7 @@ function ViewerViewportGrid(props) { isActive={isActive} >