From d4f572a7440bcfa0b0c419ccf1d12b405fcf942c Mon Sep 17 00:00:00 2001 From: Cole Lawrence Date: Fri, 10 Jun 2022 11:07:55 -0400 Subject: [PATCH] feat: Add inline keyframe editing in graph editor --- .../KeyframeEditor/SingleKeyframeDot.tsx | 47 ++++------------ .../useSingleKeyframeInlineEditorPopover.tsx | 53 +++++++++++++++++++ .../BasicKeyframedTrack.tsx | 1 + .../GraphEditorDotNonScalar.tsx | 37 +++++++++---- .../KeyframeEditor/GraphEditorDotScalar.tsx | 38 +++++++++---- .../KeyframeEditor/KeyframeEditor.tsx | 2 + .../uiComponents/Popover/TooltipWrapper.tsx | 2 +- .../src/uiComponents/Popover/usePopover.tsx | 4 +- 8 files changed, 124 insertions(+), 60 deletions(-) create mode 100644 theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/useSingleKeyframeInlineEditorPopover.tsx diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/SingleKeyframeDot.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/SingleKeyframeDot.tsx index 0cc379f..2f84364 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/SingleKeyframeDot.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/SingleKeyframeDot.tsx @@ -1,6 +1,5 @@ import React, {useMemo, useRef} from 'react' import styled from 'styled-components' -import last from 'lodash-es/last' import getStudio from '@theatre/studio/getStudio' import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore' @@ -12,11 +11,7 @@ import {val} from '@theatre/dataverse' import {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider' import {useCssCursorLock} from '@theatre/studio/uiComponents/PointerEventsHandler' import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap' -import usePopover from '@theatre/studio/uiComponents/Popover/usePopover' -import BasicPopover from '@theatre/studio/uiComponents/Popover/BasicPopover' -import {useTempTransactionEditingTools} from './useTempTransactionEditingTools' -import {DeterminePropEditorForSingleKeyframe} from './DeterminePropEditorForSingleKeyframe' import type {ISingleKeyframeEditorProps} from './SingleKeyframeEditor' import {absoluteDims} from '@theatre/studio/utils/absoluteDims' import {useLogger} from '@theatre/studio/uiComponents/useLogger' @@ -28,6 +23,7 @@ import { snapToNone, snapToSome, } from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/KeyframeSnapTarget' +import {useSingleKeyframeInlineEditorPopover} from './useSingleKeyframeInlineEditorPopover' export const DOT_SIZE_PX = 6 const DOT_HOVER_SIZE_PX = DOT_SIZE_PX + 2 @@ -95,7 +91,14 @@ const SingleKeyframeDot: React.VFC = (props) => { const [contextMenu] = useSingleKeyframeContextMenu(node, logger, props) const [inlineEditorPopover, openEditor, _, isInlineEditorPopoverOpen] = - useSingleKeyframeInlineEditorPopover(props) + useSingleKeyframeInlineEditorPopover({ + keyframe: props.keyframe, + pathToProp: props.leaf.pathToProp, + propConf: props.leaf.propConf, + sheetObject: props.leaf.sheetObject, + trackId: props.leaf.trackId, + }) + const [isDragging] = useDragForSingleKeyframeDot(node, props, { onClickFromDrag(dragStartEvent) { openEditor(dragStartEvent, ref.current!) @@ -180,38 +183,6 @@ function useSingleKeyframeContextMenu( }) } -/** The editor that pops up when directly clicking a Keyframe. */ -function useSingleKeyframeInlineEditorPopover(props: ISingleKeyframeDotProps) { - const editingTools = useEditingToolsForKeyframeEditorPopover(props) - const label = props.leaf.propConf.label ?? last(props.leaf.pathToProp) - - return usePopover({debugName: 'useKeyframeInlineEditorPopover'}, () => ( - - - - )) -} - -function useEditingToolsForKeyframeEditorPopover( - props: ISingleKeyframeDotProps, -) { - const obj = props.leaf.sheetObject - return useTempTransactionEditingTools(({stateEditors}, value) => { - const newKeyframe = {...props.keyframe, value} - stateEditors.coreByProject.historic.sheetsById.sequence.replaceKeyframes({ - ...obj.address, - trackId: props.leaf.trackId, - keyframes: [newKeyframe], - snappingFunction: obj.sheet.getSequence().closestGridPosition, - }) - }) -} - function useDragForSingleKeyframeDot( node: HTMLDivElement | null, props: ISingleKeyframeDotProps, diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/useSingleKeyframeInlineEditorPopover.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/useSingleKeyframeInlineEditorPopover.tsx new file mode 100644 index 0000000..2638983 --- /dev/null +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/useSingleKeyframeInlineEditorPopover.tsx @@ -0,0 +1,53 @@ +import React from 'react' +import last from 'lodash-es/last' +import usePopover from '@theatre/studio/uiComponents/Popover/usePopover' +import BasicPopover from '@theatre/studio/uiComponents/Popover/BasicPopover' +import {useTempTransactionEditingTools} from './useTempTransactionEditingTools' +import {DeterminePropEditorForSingleKeyframe} from './DeterminePropEditorForSingleKeyframe' +import type {SequenceTrackId} from '@theatre/shared/utils/ids' +import type {Keyframe} from '@theatre/core/projects/store/types/SheetState_Historic' +import type SheetObject from '@theatre/core/sheetObjects/SheetObject' +import type {PropTypeConfig} from '@theatre/core/propTypes' +import type {PathToProp} from '@theatre/shared/utils/addresses' + +/** The editor that pops up when directly clicking a Keyframe. */ +export function useSingleKeyframeInlineEditorPopover( + props: SingleKeyframeEditingOptions, +) { + const editingTools = useEditingToolsForKeyframeEditorPopover(props) + const label = props.propConf.label ?? last(props.pathToProp) + + return usePopover({debugName: 'useKeyframeInlineEditorPopover'}, () => ( + + + + )) +} + +type SingleKeyframeEditingOptions = { + keyframe: Keyframe + propConf: PropTypeConfig + sheetObject: SheetObject + trackId: SequenceTrackId + pathToProp: PathToProp +} + +function useEditingToolsForKeyframeEditorPopover( + props: SingleKeyframeEditingOptions, +) { + const obj = props.sheetObject + return useTempTransactionEditingTools(({stateEditors}, value) => { + const newKeyframe = {...props.keyframe, value} + stateEditors.coreByProject.historic.sheetsById.sequence.replaceKeyframes({ + ...obj.address, + trackId: props.trackId, + keyframes: [newKeyframe], + snappingFunction: obj.sheet.getSequence().closestGridPosition, + }) + }) +} diff --git a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/BasicKeyframedTrack.tsx b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/BasicKeyframedTrack.tsx index 6158a13..aded180 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/BasicKeyframedTrack.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/BasicKeyframedTrack.tsx @@ -94,6 +94,7 @@ const BasicKeyframedTrack: React.VFC<{ const keyframeEditors = trackData.keyframes.map((kf, index) => ( = (props) => { const curValue = props.which === 'left' ? 0 : 1 - const isDragging = useDragKeyframe(node, props) + const [inlineEditorPopover, openEditor, _, _isInlineEditorPopoverOpen] = + useSingleKeyframeInlineEditorPopover({ + keyframe: props.keyframe, + pathToProp: props.pathToProp, + propConf: props.propConfig, + sheetObject: props.sheetObject, + trackId: props.trackId, + }) + + const isDragging = useDragKeyframe({ + node, + props, + // dragging does not work with also having a click listener + onDetectedClick: (event) => + openEditor(event, event.target instanceof Element ? event.target : node!), + }) const cyInExtremumSpace = props.extremumSpace.fromValueSpace(curValue) @@ -88,6 +104,7 @@ const GraphEditorDotNonScalar: React.VFC = (props) => { cy: `calc((var(--graphEditorVerticalSpace) - var(--graphEditorVerticalSpace) * ${cyInExtremumSpace}) * 1px)`, }} /> + {inlineEditorPopover} {contextMenu} ) @@ -95,14 +112,15 @@ const GraphEditorDotNonScalar: React.VFC = (props) => { export default GraphEditorDotNonScalar -function useDragKeyframe( - node: SVGCircleElement | null, - _props: IProps, -): boolean { +function useDragKeyframe(options: { + node: SVGCircleElement | null + props: IProps + onDetectedClick: (event: MouseEvent) => void +}): boolean { const [isDragging, setIsDragging] = useState(false) - useLockFrameStampPosition(isDragging, _props.keyframe.position) - const propsRef = useRef(_props) - propsRef.current = _props + useLockFrameStampPosition(isDragging, options.props.keyframe.position) + const propsRef = useRef(options.props) + propsRef.current = options.props const gestureHandlers = useMemo[1]>(() => { return { @@ -158,6 +176,7 @@ function useDragKeyframe( tempTransaction?.commit() } else { tempTransaction?.discard() + options.onDetectedClick(event) } }, } @@ -165,7 +184,7 @@ function useDragKeyframe( } }, []) - useDrag(node, gestureHandlers) + useDrag(options.node, gestureHandlers) useCssCursorLock(isDragging, 'draggingPositionInSequenceEditor', 'ew-resize') return isDragging } diff --git a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/GraphEditorDotScalar.tsx b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/GraphEditorDotScalar.tsx index f685672..c23fb52 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/GraphEditorDotScalar.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/GraphEditorDotScalar.tsx @@ -16,6 +16,7 @@ import { useCssCursorLock, } from '@theatre/studio/uiComponents/PointerEventsHandler' import DopeSnap from '@theatre/studio/panels/SequenceEditorPanel/RightOverlay/DopeSnap' +import {useSingleKeyframeInlineEditorPopover} from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/useSingleKeyframeInlineEditorPopover' export const dotSize = 6 @@ -65,9 +66,23 @@ const GraphEditorDotScalar: React.VFC = (props) => { const curValue = cur.value as number - const isDragging = useDragKeyframe(node, props) - const cyInExtremumSpace = props.extremumSpace.fromValueSpace(curValue) + const [inlineEditorPopover, openEditor, _, _isInlineEditorPopoverOpen] = + useSingleKeyframeInlineEditorPopover({ + keyframe: props.keyframe, + pathToProp: props.pathToProp, + propConf: props.propConfig, + sheetObject: props.sheetObject, + trackId: props.trackId, + }) + + const isDragging = useDragKeyframe({ + node, + props, + // dragging does not work with also having a click listener + onDetectedClick: (event) => + openEditor(event, event.target instanceof Element ? event.target : node!), + }) return ( <> @@ -89,6 +104,7 @@ const GraphEditorDotScalar: React.VFC = (props) => { cy: `calc((var(--graphEditorVerticalSpace) - var(--graphEditorVerticalSpace) * ${cyInExtremumSpace}) * 1px)`, }} /> + {inlineEditorPopover} {contextMenu} ) @@ -96,14 +112,15 @@ const GraphEditorDotScalar: React.VFC = (props) => { export default GraphEditorDotScalar -function useDragKeyframe( - node: SVGCircleElement | null, - _props: IProps, -): boolean { +function useDragKeyframe(options: { + node: SVGCircleElement | null + props: IProps + onDetectedClick: (event: MouseEvent) => void +}): boolean { const [isDragging, setIsDragging] = useState(false) - useLockFrameStampPosition(isDragging, _props.keyframe.position) - const propsRef = useRef(_props) - propsRef.current = _props + useLockFrameStampPosition(isDragging, options.props.keyframe.position) + const propsRef = useRef(options.props) + propsRef.current = options.props const gestureHandlers = useMemo[1]>(() => { return { @@ -219,6 +236,7 @@ function useDragKeyframe( tempTransaction?.commit() } else { tempTransaction?.discard() + options.onDetectedClick(event) } }, } @@ -226,7 +244,7 @@ function useDragKeyframe( } }, []) - useDrag(node, gestureHandlers) + useDrag(options.node, gestureHandlers) useCssCursorLock(isDragging, 'draggingPositionInSequenceEditor', 'move') return isDragging } diff --git a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/KeyframeEditor.tsx b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/KeyframeEditor.tsx index f018fd3..8d19183 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/KeyframeEditor.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/GraphEditor/BasicKeyframedTrack/KeyframeEditor/KeyframeEditor.tsx @@ -16,6 +16,7 @@ import GraphEditorDotScalar from './GraphEditorDotScalar' import GraphEditorDotNonScalar from './GraphEditorDotNonScalar' import GraphEditorNonScalarDash from './GraphEditorNonScalarDash' import type {PropTypeConfig_AllSimples} from '@theatre/core/propTypes' +import type {PathToProp} from '@theatre/shared/utils/addresses' const Container = styled.g` /* position: absolute; */ @@ -30,6 +31,7 @@ type IKeyframeEditorProps = { layoutP: Pointer trackId: SequenceTrackId sheetObject: SheetObject + pathToProp: PathToProp extremumSpace: ExtremumSpace isScalar: boolean color: keyof typeof graphEditorColors diff --git a/theatre/studio/src/uiComponents/Popover/TooltipWrapper.tsx b/theatre/studio/src/uiComponents/Popover/TooltipWrapper.tsx index 234afbc..4ebb598 100644 --- a/theatre/studio/src/uiComponents/Popover/TooltipWrapper.tsx +++ b/theatre/studio/src/uiComponents/Popover/TooltipWrapper.tsx @@ -19,7 +19,7 @@ export type AbsolutePlacementBoxConstraints = { } const TooltipWrapper: React.FC<{ - target: HTMLElement | SVGElement + target: HTMLElement | SVGElement | Element onClickOutside?: (e: MouseEvent) => void children: () => React.ReactElement onPointerOutside?: { diff --git a/theatre/studio/src/uiComponents/Popover/usePopover.tsx b/theatre/studio/src/uiComponents/Popover/usePopover.tsx index f284069..dc47509 100644 --- a/theatre/studio/src/uiComponents/Popover/usePopover.tsx +++ b/theatre/studio/src/uiComponents/Popover/usePopover.tsx @@ -9,7 +9,7 @@ import {contextMenuShownContext} from '@theatre/studio/panels/DetailPanel/Detail export type OpenFn = ( e: React.MouseEvent | MouseEvent | {clientX: number; clientY: number}, - target: HTMLElement, + target: HTMLElement | SVGElement | Element, ) => void type CloseFn = (reason: string) => void type State = @@ -20,7 +20,7 @@ type State = clientX: number clientY: number } - target: HTMLElement + target: HTMLElement | SVGElement | Element opts: Opts onPointerOutside?: { threshold: number