From b0759678a22ba881aa3ddd9ccb4acd8bb29e44ae Mon Sep 17 00:00:00 2001 From: Mint de Wit Date: Wed, 18 Dec 2024 13:37:54 +0000 Subject: [PATCH] feat: edit mode for drag operations --- .../blueprints-integration/src/triggers.ts | 7 + .../src/triggers/RundownViewEventBus.ts | 7 + .../meteor-lib/src/triggers/actionFactory.ts | 13 + .../shared-lib/src/core/model/ShowStyle.ts | 1 + .../src/client/lib/ui/pieceUiClassNames.ts | 5 +- .../src/client/styles/elementSelected.scss | 28 +- packages/webui/src/client/ui/RundownView.tsx | 471 +++++++++--------- .../src/client/ui/RundownView/DragContext.ts | 5 + .../ui/RundownView/DragContextProvider.tsx | 25 +- .../ui/SegmentTimeline/SourceLayerItem.tsx | 27 +- .../actionSelector/ActionSelector.tsx | 49 ++ 11 files changed, 372 insertions(+), 266 deletions(-) diff --git a/packages/blueprints-integration/src/triggers.ts b/packages/blueprints-integration/src/triggers.ts index c360fa6567..49b662add5 100644 --- a/packages/blueprints-integration/src/triggers.ts +++ b/packages/blueprints-integration/src/triggers.ts @@ -265,6 +265,12 @@ export interface IShelfAction extends ITriggeredActionBase { filterChain: IGUIContextFilterLink[] } +export interface IEditModeAction extends ITriggeredActionBase { + action: ClientActions.editMode + state: true | false | 'toggle' + filterChain: IGUIContextFilterLink[] +} + export interface IGoToOnAirLineAction extends ITriggeredActionBase { action: ClientActions.goToOnAirLine filterChain: IGUIContextFilterLink[] @@ -318,6 +324,7 @@ export type SomeAction = | IRundownPlaylistResetAction | IRundownPlaylistResyncAction | IShelfAction + | IEditModeAction | IGoToOnAirLineAction | IRewindSegmentsAction | IShowEntireCurrentSegmentAction diff --git a/packages/meteor-lib/src/triggers/RundownViewEventBus.ts b/packages/meteor-lib/src/triggers/RundownViewEventBus.ts index 8460e67108..3419b98036 100644 --- a/packages/meteor-lib/src/triggers/RundownViewEventBus.ts +++ b/packages/meteor-lib/src/triggers/RundownViewEventBus.ts @@ -29,6 +29,7 @@ export enum RundownViewEvents { REVEAL_IN_SHELF = 'revealInShelf', SWITCH_SHELF_TAB = 'switchShelfTab', SHELF_STATE = 'shelfState', + EDIT_MODE = 'editMode', MINI_SHELF_QUEUE_ADLIB = 'miniShelfQueueAdLib', GO_TO_PART = 'goToPart', GO_TO_PART_INSTANCE = 'goToPartInstance', @@ -74,6 +75,10 @@ export interface ShelfStateEvent extends IEventContext { state: boolean | 'toggle' } +export interface EditModeEvent extends IEventContext { + state: boolean | 'toggle' +} + export interface MiniShelfQueueAdLibEvent extends IEventContext { forward: boolean } @@ -139,6 +144,7 @@ class RundownViewEventBus0 extends EventEmitter { emit(event: RundownViewEvents.SEGMENT_ZOOM_ON): boolean emit(event: RundownViewEvents.SEGMENT_ZOOM_OFF): boolean emit(event: RundownViewEvents.SHELF_STATE, e: ShelfStateEvent): boolean + emit(event: RundownViewEvents.EDIT_MODE, e: EditModeEvent): boolean emit(event: RundownViewEvents.REVEAL_IN_SHELF, e: RevealInShelfEvent): boolean emit(event: RundownViewEvents.SWITCH_SHELF_TAB, e: SwitchToShelfTabEvent): boolean emit(event: RundownViewEvents.MINI_SHELF_QUEUE_ADLIB, e: MiniShelfQueueAdLibEvent): boolean @@ -175,6 +181,7 @@ class RundownViewEventBus0 extends EventEmitter { on(event: RundownViewEvents.SEGMENT_ZOOM_OFF, listener: () => void): this on(event: RundownViewEvents.REVEAL_IN_SHELF, listener: (e: RevealInShelfEvent) => void): this on(event: RundownViewEvents.SHELF_STATE, listener: (e: ShelfStateEvent) => void): this + on(event: RundownViewEvents.EDIT_MODE, listener: (e: EditModeEvent) => void): this on(event: RundownViewEvents.SWITCH_SHELF_TAB, listener: (e: SwitchToShelfTabEvent) => void): this on(event: RundownViewEvents.MINI_SHELF_QUEUE_ADLIB, listener: (e: MiniShelfQueueAdLibEvent) => void): this on(event: RundownViewEvents.GO_TO_PART, listener: (e: GoToPartEvent) => void): this diff --git a/packages/meteor-lib/src/triggers/actionFactory.ts b/packages/meteor-lib/src/triggers/actionFactory.ts index 7796716ffc..23fad78dec 100644 --- a/packages/meteor-lib/src/triggers/actionFactory.ts +++ b/packages/meteor-lib/src/triggers/actionFactory.ts @@ -287,6 +287,17 @@ function createShelfAction(_filterChain: IGUIContextFilterLink[], state: boolean } } +function createEditModeAction(_filterChain: IGUIContextFilterLink[], state: boolean | 'toggle'): ExecutableAction { + return { + action: ClientActions.editMode, + execute: () => { + RundownViewEventBus.emit(RundownViewEvents.EDIT_MODE, { + state, + }) + }, + } +} + function createMiniShelfQueueAdLibAction(_filterChain: IGUIContextFilterLink[], forward: boolean): ExecutableAction { return { action: ClientActions.miniShelfQueueAdLib, @@ -443,6 +454,8 @@ export function createAction( switch (action.action) { case ClientActions.shelf: return createShelfAction(action.filterChain, action.state) + case ClientActions.editMode: + return createEditModeAction(action.filterChain, action.state) case ClientActions.goToOnAirLine: return createGoToOnAirLineAction(action.filterChain) case ClientActions.rewindSegments: diff --git a/packages/shared-lib/src/core/model/ShowStyle.ts b/packages/shared-lib/src/core/model/ShowStyle.ts index 9c1825ff28..283bb2f87d 100644 --- a/packages/shared-lib/src/core/model/ShowStyle.ts +++ b/packages/shared-lib/src/core/model/ShowStyle.ts @@ -104,6 +104,7 @@ export enum ClientActions { 'rewindSegments' = 'rewindSegments', 'showEntireCurrentSegment' = 'showEntireCurrentSegment', 'miniShelfQueueAdLib' = 'miniShelfQueueAdLib', + 'editMode' = 'editMode', } export enum DeviceActions { diff --git a/packages/webui/src/client/lib/ui/pieceUiClassNames.ts b/packages/webui/src/client/lib/ui/pieceUiClassNames.ts index a66a68e8fc..38c4afd70d 100644 --- a/packages/webui/src/client/lib/ui/pieceUiClassNames.ts +++ b/packages/webui/src/client/lib/ui/pieceUiClassNames.ts @@ -19,7 +19,8 @@ export function pieceUiClassNames( uiState?: { leftAnchoredWidth: number rightAnchoredWidth: number - } + }, + draggable?: boolean ): string { const typeClass = layerType ? RundownUtils.getSourceLayerClassName(layerType) : '' @@ -57,5 +58,7 @@ export function pieceUiClassNames( 'invert-flash': highlight, 'element-selected': selected, + + 'draggable-element': draggable, }) } diff --git a/packages/webui/src/client/styles/elementSelected.scss b/packages/webui/src/client/styles/elementSelected.scss index ed37ae296c..65a4c92e5c 100644 --- a/packages/webui/src/client/styles/elementSelected.scss +++ b/packages/webui/src/client/styles/elementSelected.scss @@ -3,16 +3,20 @@ $glow-color: rgba(255, 255, 255, 0.58); .element-selected { - box-shadow: inset 0 0 15px $glow-color; - animation: subtle-glow 1s ease-in-out infinite; + box-shadow: inset 0 0 15px $glow-color; + animation: subtle-glow 1s ease-in-out infinite; - @keyframes subtle-glow { - 0%, 100% { - box-shadow: inset 0 0 15px $glow-color; - } - 50% { - box-shadow: inset 0 0 25px $glow-color, - inset 0 0 35px $glow-color; - } - } -} \ No newline at end of file + @keyframes subtle-glow { + 0%, + 100% { + box-shadow: inset 0 0 15px $glow-color; + } + 50% { + box-shadow: inset 0 0 25px $glow-color, inset 0 0 35px $glow-color; + } + } +} + +.draggable-element { + border: dotted white 1px; +} diff --git a/packages/webui/src/client/ui/RundownView.tsx b/packages/webui/src/client/ui/RundownView.tsx index d07729e87d..2c1e519513 100644 --- a/packages/webui/src/client/ui/RundownView.tsx +++ b/packages/webui/src/client/ui/RundownView.tsx @@ -3020,242 +3020,245 @@ const RundownViewContent = translateWithTracker - - - {(selectionContext) => { - return ( -
0, - })} - style={this.getStyle()} - onWheelCapture={this.onWheel} - onContextMenu={this.onContextMenuTop} - > - {this.renderSegmentsList()} - - {this.props.matchedSegments && - this.props.matchedSegments.length > 0 && - this.props.userPermissions.studio && } - - - r._id)} - firstRundown={this.props.rundowns[0]} - onActivate={this.onActivate} - userPermissions={this.props.userPermissions} - inActiveRundownView={this.props.inActiveRundownView} - currentRundown={this.state.currentRundown || this.props.rundowns[0]} - layout={this.state.rundownHeaderLayout} - showStyleBase={showStyleBase} - showStyleVariant={showStyleVariant} - /> - - - {this.props.userPermissions.studio && !Settings.disableBlurBorder && ( - -
-
- )} -
- - - - {this.renderSorensenContext()} - - - {this.state.isNotificationsCenterOpen && ( - - )} - - {!this.state.isNotificationsCenterOpen && - selectionContext.listSelectedElements().length > 0 && ( -
- -
+ + + {(selectionContext) => { + return ( +
0, + })} + style={this.getStyle()} + onWheelCapture={this.onWheel} + onContextMenu={this.onContextMenuTop} + > + {this.renderSegmentsList()} + + {this.props.matchedSegments && + this.props.matchedSegments.length > 0 && + this.props.userPermissions.studio && } + + + r._id)} + firstRundown={this.props.rundowns[0]} + onActivate={this.onActivate} + userPermissions={this.props.userPermissions} + inActiveRundownView={this.props.inActiveRundownView} + currentRundown={this.state.currentRundown || this.props.rundowns[0]} + layout={this.state.rundownHeaderLayout} + showStyleBase={showStyleBase} + showStyleVariant={showStyleVariant} + /> + + + {this.props.userPermissions.studio && !Settings.disableBlurBorder && ( + +
+
)} - - {this.state.isSupportPanelOpen && ( - -
- -
- - {t('Take a Snapshot')} - -
- {this.props.userPermissions.studio && ( - <> - -
- - )} - {this.props.userPermissions.studio && - this.props.casparCGPlayoutDevices && - this.props.casparCGPlayoutDevices.map((i) => ( - - +
+ + {t('Take a Snapshot')} + +
+ {this.props.userPermissions.studio && ( + <> +
-
- ))} -
+ + )} + {this.props.userPermissions.studio && + this.props.casparCGPlayoutDevices && + this.props.casparCGPlayoutDevices.map((i) => ( + + +
+
+ ))} + + )} +
+
+ + {this.props.userPermissions.studio && ( + )} - - - - {this.props.userPermissions.studio && ( - + + + + + + selectionContext.clearAndSetSelection({ type: 'segment', elementId: id }) + } + onEditPartProps={(id) => + selectionContext.clearAndSetSelection({ type: 'part', elementId: id }) + } + studioMode={this.props.userPermissions.studio} + enablePlayFromAnywhere={!!studio.settings.enablePlayFromAnywhere} + enableQuickLoop={!!studio.settings.enableQuickLoop} + enableUserEdits={!!studio.settings.enableUserEdits} /> - )} - - - - - - - selectionContext.clearAndSetSelection({ type: 'segment', elementId: id }) - } - onEditPartProps={(id) => - selectionContext.clearAndSetSelection({ type: 'part', elementId: id }) - } - studioMode={this.props.userPermissions.studio} - enablePlayFromAnywhere={!!studio.settings.enablePlayFromAnywhere} - enableQuickLoop={!!studio.settings.enableQuickLoop} - enableUserEdits={!!studio.settings.enableUserEdits} - /> - - - {this.state.isClipTrimmerOpen && - this.state.selectedPiece && - RundownUtils.isPieceInstance(this.state.selectedPiece) && - (selectedPieceRundown === undefined ? ( - this.setState({ selectedPiece: undefined })} - title={t('Rundown not found')} - acceptText={t('Close')} - > - {t('Rundown for piece "{{pieceLabel}}" could not be found.', { - pieceLabel: this.state.selectedPiece.instance.piece.name, - })} - - ) : ( - this.setState({ isClipTrimmerOpen: false })} - /> - ))} - - - - - - - - - {this.props.playlist && this.props.studio && this.props.showStyleBase && ( - - )} - -
- ) - }} - { - // USE IN CASE OF DEBUGGING EMERGENCY - /* getDeveloperMode() &&
+ + {this.state.isClipTrimmerOpen && + this.state.selectedPiece && + RundownUtils.isPieceInstance(this.state.selectedPiece) && + (selectedPieceRundown === undefined ? ( + this.setState({ selectedPiece: undefined })} + title={t('Rundown not found')} + acceptText={t('Close')} + > + {t('Rundown for piece "{{pieceLabel}}" could not be found.', { + pieceLabel: this.state.selectedPiece.instance.piece.name, + })} + + ) : ( + this.setState({ isClipTrimmerOpen: false })} + /> + ))} + + + + + + + + + {this.props.playlist && this.props.studio && this.props.showStyleBase && ( + + )} + +
+ ) + }} + { + // USE IN CASE OF DEBUGGING EMERGENCY + /* getDeveloperMode() &&
*/ - } -
-
+ } + + diff --git a/packages/webui/src/client/ui/RundownView/DragContext.ts b/packages/webui/src/client/ui/RundownView/DragContext.ts index c47a03bb9a..dff67253e5 100644 --- a/packages/webui/src/client/ui/RundownView/DragContext.ts +++ b/packages/webui/src/client/ui/RundownView/DragContext.ts @@ -26,6 +26,11 @@ export interface IDragContext { */ setHoveredPart: (partId: PartInstanceId, segmentId: SegmentId, position: { x: number; y: number }) => void + /** + * Whether dragging is enabled + */ + enabled: boolean + /** * PieceId of the piece that is being dragged */ diff --git a/packages/webui/src/client/ui/RundownView/DragContextProvider.tsx b/packages/webui/src/client/ui/RundownView/DragContextProvider.tsx index 917be0b210..348c96af68 100644 --- a/packages/webui/src/client/ui/RundownView/DragContextProvider.tsx +++ b/packages/webui/src/client/ui/RundownView/DragContextProvider.tsx @@ -1,5 +1,5 @@ import { PartInstanceId, PieceInstanceId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { PropsWithChildren, useRef, useState } from 'react' +import { PropsWithChildren, useCallback, useEffect, useRef, useState } from 'react' import { dragContext, IDragContext } from './DragContext' import { PieceUi } from '../SegmentContainer/withResolvedSegment' import { doUserAction, UserAction } from '../../lib/clientUserAction' @@ -9,6 +9,10 @@ import { UIParts } from '../Collections' import { Segments } from '../../collections' import { literal } from '../../lib/tempLib' import { DefaultUserOperationRetimePiece, DefaultUserOperationsTypes } from '@sofie-automation/blueprints-integration' +import RundownViewEventBus, { + RundownViewEvents, + EditModeEvent, +} from '@sofie-automation/meteor-lib/dist/triggers/RundownViewEventBus' const DRAG_TIMEOUT = 10000 @@ -21,6 +25,8 @@ export function DragContextProvider({ t, children }: PropsWithChildren): const [pieceId, setPieceId] = useState(undefined) const [piece, setPiece] = useState(undefined) + const [enabled, setEnabled] = useState(false) + const partIdRef = useRef(undefined) const positionRef = useRef({ x: 0, y: 0 }) const segmentIdRef = useRef(undefined) @@ -133,10 +139,27 @@ export function DragContextProvider({ t, children }: PropsWithChildren): positionRef.current = pos } + const onSetEditMode = useCallback((e: EditModeEvent) => { + if (e.state === 'toggle') { + setEnabled((s) => !s) + } else { + setEnabled(e.state) + } + }, []) + + useEffect(() => { + RundownViewEventBus.on(RundownViewEvents.EDIT_MODE, onSetEditMode) + return () => { + RundownViewEventBus.off(RundownViewEvents.EDIT_MODE, onSetEditMode) + } + }) + const ctx = literal({ pieceId, piece, + enabled, + startDrag, setHoveredPart, }) diff --git a/packages/webui/src/client/ui/SegmentTimeline/SourceLayerItem.tsx b/packages/webui/src/client/ui/SegmentTimeline/SourceLayerItem.tsx index 588eb215f5..8c20b6baaa 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/SourceLayerItem.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/SourceLayerItem.tsx @@ -119,6 +119,9 @@ export const SourceLayerItem = (props: Readonly): JSX.Ele const [rightAnchoredWidth, setRightAnchoredWidth] = useState(0) const dragCtx = useContext(dragContext) + const hasDraggableElement = !!piece.instance.piece.userEditOperations?.find( + (op) => op.type === UserEditingType.SOFIE && op.id === DefaultUserOperationsTypes.RETIME_PIECE + ) const state = { highlight, @@ -189,25 +192,11 @@ export const SourceLayerItem = (props: Readonly): JSX.Ele (e: React.MouseEvent) => { e.preventDefault() e.stopPropagation() - console.log( - 'mousedown', - UserEditingType.SOFIE, - DefaultUserOperationsTypes, - piece.instance.piece.userEditOperations?.map( - (op) => op.type === UserEditingType.SOFIE && op.id === '__sofie-retime-piece' - ) - ) - if ( - !piece.instance.piece.userEditOperations?.find( - // (op) => op.type === UserEditingType.SOFIE && op.id === DefaultUserOperationsTypes.RETIME_PIECE - (op) => op.type === UserEditingType.SOFIE && op.id === '__sofie-retime-piece' - ) - ) - return + if (!hasDraggableElement) return const targetPos = (e.target as HTMLDivElement).getBoundingClientRect() - if (dragCtx) + if (dragCtx && dragCtx.enabled) dragCtx.startDrag( piece, timeScale, @@ -219,7 +208,7 @@ export const SourceLayerItem = (props: Readonly): JSX.Ele part.instance.segmentId ) }, - [piece, timeScale] + [piece, timeScale, dragCtx] ) const itemMouseUp = useCallback((e: any) => { const eM = e as MouseEvent @@ -570,8 +559,10 @@ export const SourceLayerItem = (props: Readonly): JSX.Ele layer.type, part.partId, highlight, - elementWidth + elementWidth, // this.state + undefined, + hasDraggableElement && dragCtx?.enabled )} data-obj-id={piece.instance._id} ref={setRef} diff --git a/packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx b/packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx index d9f150dcf3..a0af63d26e 100644 --- a/packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx +++ b/packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/actionSelector/ActionSelector.tsx @@ -89,6 +89,17 @@ function getArguments(t: TFunction, action: SomeAction): string[] { assertNever(action.state) } break + case ClientActions.editMode: + if (action.state === true) { + result.push(t('Enable')) + } else if (action.state === false) { + result.push(t('Disable')) + } else if (action.state === 'toggle') { + result.push(t('Toggle')) + } else { + assertNever(action.state) + } + break case ClientActions.goToOnAirLine: break case ClientActions.rewindSegments: @@ -143,6 +154,8 @@ function hasArguments(action: SomeAction): boolean { return false case ClientActions.shelf: return true + case ClientActions.editMode: + return true case ClientActions.goToOnAirLine: return false case ClientActions.rewindSegments: @@ -189,6 +202,8 @@ function actionToLabel(t: TFunction, action: SomeAction['action']): string { return t('Switch Route Set') case ClientActions.shelf: return t('Shelf') + case ClientActions.editMode: + return t('Edit Mode') case ClientActions.rewindSegments: return t('Rewind Segments to start') case ClientActions.goToOnAirLine: @@ -362,6 +377,40 @@ function getActionParametersEditor( />
) + case ClientActions.editMode: + return ( +
+ + + classNames="input text-input input-m" + value={action.state} + // placholder={t('State')} + options={[ + { + name: t('Enable'), + value: true, + i: 0, + }, + { + name: t('Disable'), + value: false, + i: 1, + }, + { + name: t('Toggle'), + value: 'toggle', + i: 2, + }, + ]} + handleUpdate={(newVal) => { + onChange({ + ...action, + state: newVal, + }) + }} + /> +
+ ) case ClientActions.goToOnAirLine: return null case ClientActions.rewindSegments: