refactor/docs: Minor identifier tweaks for RightClick/ContextMenu

This commit is contained in:
Cole Lawrence 2022-04-27 14:38:13 -04:00
parent f04bc3e31a
commit 91794f550d
15 changed files with 99 additions and 62 deletions

View file

@ -119,7 +119,7 @@ export const SingleRowPropEditor: React.FC<{
useRefAndState<HTMLDivElement | null>(null) useRefAndState<HTMLDivElement | null>(null)
const [contextMenu] = useContextMenu(propNameContainer, { const [contextMenu] = useContextMenu(propNameContainer, {
items: stuff.contextMenuItems, menuItems: stuff.contextMenuItems,
}) })
const color = shadeToColor[stuff.shade] const color = shadeToColor[stuff.shade]

View file

@ -8,6 +8,7 @@ import {val} from '@theatre/dataverse'
import React from 'react' import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import KeyframeEditor from './KeyframeEditor/KeyframeEditor' import KeyframeEditor from './KeyframeEditor/KeyframeEditor'
import type {IContextMenuItem} from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu' import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
import useRefAndState from '@theatre/studio/utils/useRefAndState' import useRefAndState from '@theatre/studio/utils/useRefAndState'
import getStudio from '@theatre/studio/getStudio' import getStudio from '@theatre/studio/getStudio'
@ -85,7 +86,7 @@ function useBasicKeyframedTrackContextMenu(
props: BasicKeyframedTracksProps, props: BasicKeyframedTracksProps,
) { ) {
return useContextMenu(node, { return useContextMenu(node, {
items: () => { menuItems: () => {
const selectionKeyframes = const selectionKeyframes =
val(getStudio()!.atomP.ahistoric.clipboard.keyframes) || [] val(getStudio()!.atomP.ahistoric.clipboard.keyframes) || []
@ -101,7 +102,7 @@ function useBasicKeyframedTrackContextMenu(
function pasteKeyframesContextMenuItem( function pasteKeyframesContextMenuItem(
props: BasicKeyframedTracksProps, props: BasicKeyframedTracksProps,
keyframes: Keyframe[], keyframes: Keyframe[],
) { ): IContextMenuItem {
return { return {
label: 'Paste Keyframes', label: 'Paste Keyframes',
callback: () => { callback: () => {

View file

@ -12,7 +12,7 @@ import type {
SequenceEditorPanelLayout, SequenceEditorPanelLayout,
DopeSheetSelection, DopeSheetSelection,
} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout' } from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
import {DOT_SIZE_PX} from './Dot' import {DOT_SIZE_PX} from './KeyframeDot'
import type KeyframeEditor from './KeyframeEditor' import type KeyframeEditor from './KeyframeEditor'
import type Sequence from '@theatre/core/sequences/Sequence' import type Sequence from '@theatre/core/sequences/Sequence'
import usePopover from '@theatre/studio/uiComponents/Popover/usePopover' import usePopover from '@theatre/studio/uiComponents/Popover/usePopover'
@ -250,7 +250,7 @@ function useConnectorContextMenu(
) { ) {
const maybeKeyframeIds = selectedKeyframeIdsIfInSingleTrack(props.selection) const maybeKeyframeIds = selectedKeyframeIdsIfInSingleTrack(props.selection)
return useContextMenu(node, { return useContextMenu(node, {
items: () => { menuItems: () => {
return [ return [
{ {
label: maybeKeyframeIds ? 'Copy Selection' : 'Copy both Keyframes', label: maybeKeyframeIds ? 'Copy Selection' : 'Copy both Keyframes',

View file

@ -5,13 +5,14 @@ import type {
import getStudio from '@theatre/studio/getStudio' import getStudio from '@theatre/studio/getStudio'
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore' import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu' import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
import type {IContextMenuItem} from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
import useDrag from '@theatre/studio/uiComponents/useDrag' import useDrag from '@theatre/studio/uiComponents/useDrag'
import type {UseDragOpts} from '@theatre/studio/uiComponents/useDrag'
import useRefAndState from '@theatre/studio/utils/useRefAndState' import useRefAndState from '@theatre/studio/utils/useRefAndState'
import {val} from '@theatre/dataverse' import {val} from '@theatre/dataverse'
import {lighten} from 'polished' import {lighten} from 'polished'
import React, {useMemo, useRef, useState} from 'react' import React, {useMemo, useRef, useState} from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import type KeyframeEditor from './KeyframeEditor'
import {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider' import {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider' import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
import { import {
@ -20,6 +21,7 @@ import {
} from '@theatre/studio/uiComponents/PointerEventsHandler' } from '@theatre/studio/uiComponents/PointerEventsHandler'
import SnapCursor from './SnapCursor.svg' import SnapCursor from './SnapCursor.svg'
import selectedKeyframeIdsIfInSingleTrack from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/selectedKeyframeIdsIfInSingleTrack' import selectedKeyframeIdsIfInSingleTrack from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/selectedKeyframeIdsIfInSingleTrack'
import type {IKeyframeEditorProps} from './KeyframeEditor'
export const DOT_SIZE_PX = 6 export const DOT_SIZE_PX = 6
const HIT_ZONE_SIZE_PX = 12 const HIT_ZONE_SIZE_PX = 12
@ -42,6 +44,8 @@ const dotTheme = {
const Square = styled.div<{isSelected: boolean}>` const Square = styled.div<{isSelected: boolean}>`
position: absolute; position: absolute;
${dims(DOT_SIZE_PX)}
background: ${(props) => background: ${(props) =>
props.isSelected ? dotTheme.selectedColor : dotTheme.normalColor}; props.isSelected ? dotTheme.selectedColor : dotTheme.normalColor};
transform: rotateZ(45deg); transform: rotateZ(45deg);
@ -85,13 +89,14 @@ const HitZone = styled.div`
} }
` `
type IProps = Parameters<typeof KeyframeEditor>[0] type IKeyframeDotProps = IKeyframeEditorProps
const Dot: React.FC<IProps> = (props) => { /** The ◆ you can grab onto in "keyframe editor" (aka "dope sheet" in other programs) */
const KeyframeDot: React.FC<IKeyframeDotProps> = (props) => {
const [ref, node] = useRefAndState<HTMLDivElement | null>(null) const [ref, node] = useRefAndState<HTMLDivElement | null>(null)
const [contextMenu] = useKeyframeContextMenu(node, props)
const [isDragging] = useDragKeyframe(node, props) const [isDragging] = useDragKeyframe(node, props)
const [contextMenu] = useKeyframeContextMenu(node, props)
return ( return (
<> <>
@ -110,9 +115,12 @@ const Dot: React.FC<IProps> = (props) => {
) )
} }
export default Dot export default KeyframeDot
function useKeyframeContextMenu(node: HTMLDivElement | null, props: IProps) { function useKeyframeContextMenu(
target: HTMLDivElement | null,
props: IKeyframeDotProps,
) {
const maybeSelectedKeyframeIds = selectedKeyframeIdsIfInSingleTrack( const maybeSelectedKeyframeIds = selectedKeyframeIdsIfInSingleTrack(
props.selection, props.selection,
) )
@ -123,8 +131,8 @@ function useKeyframeContextMenu(node: HTMLDivElement | null, props: IProps) {
const deleteItem = deleteSelectionOrKeyframeContextMenuItem(props) const deleteItem = deleteSelectionOrKeyframeContextMenuItem(props)
return useContextMenu(node, { return useContextMenu(target, {
items: () => { menuItems: () => {
return [keyframeSelectionItem, deleteItem] return [keyframeSelectionItem, deleteItem]
}, },
}) })
@ -132,7 +140,7 @@ function useKeyframeContextMenu(node: HTMLDivElement | null, props: IProps) {
function useDragKeyframe( function useDragKeyframe(
node: HTMLDivElement | null, node: HTMLDivElement | null,
props: IProps, props: IKeyframeDotProps,
): [isDragging: boolean] { ): [isDragging: boolean] {
const [isDragging, setIsDragging] = useState(false) const [isDragging, setIsDragging] = useState(false)
useLockFrameStampPosition(isDragging, props.keyframe.position) useLockFrameStampPosition(isDragging, props.keyframe.position)
@ -140,10 +148,10 @@ function useDragKeyframe(
const propsRef = useRef(props) const propsRef = useRef(props)
propsRef.current = props propsRef.current = props
const gestureHandlers = useMemo<Parameters<typeof useDrag>[1]>(() => { const useDragOpts = useMemo<UseDragOpts>(() => {
let toUnitSpace: SequenceEditorPanelLayout['scaledSpace']['toUnitSpace'] let toUnitSpace: SequenceEditorPanelLayout['scaledSpace']['toUnitSpace']
let tempTransaction: CommitOrDiscard | undefined let tempTransaction: CommitOrDiscard | undefined
let propsAtStartOfDrag: IProps let propsAtStartOfDrag: IKeyframeDotProps
let selectionDragHandlers: let selectionDragHandlers:
| ReturnType<DopeSheetSelection['getDragHandlers']> | ReturnType<DopeSheetSelection['getDragHandlers']>
@ -151,6 +159,7 @@ function useDragKeyframe(
return { return {
debugName: 'Dot/useDragKeyframe', debugName: 'Dot/useDragKeyframe',
onDragStart(event) { onDragStart(event) {
setIsDragging(true) setIsDragging(true)
const props = propsRef.current const props = propsRef.current
@ -241,14 +250,16 @@ function useDragKeyframe(
} }
}, []) }, [])
useDrag(node, gestureHandlers) useDrag(node, useDragOpts)
useCssCursorLock(isDragging, 'draggingPositionInSequenceEditor', 'ew-resize') useCssCursorLock(isDragging, 'draggingPositionInSequenceEditor', 'ew-resize')
return [isDragging] return [isDragging]
} }
function deleteSelectionOrKeyframeContextMenuItem(props: IProps) { function deleteSelectionOrKeyframeContextMenuItem(
props: IKeyframeDotProps,
): IContextMenuItem {
return { return {
label: props.selection ? 'Delete Selection' : 'Delete Keyframe', label: props.selection ? 'Delete Selection' : 'Delete Keyframe',
callback: () => { callback: () => {
@ -269,7 +280,10 @@ function deleteSelectionOrKeyframeContextMenuItem(props: IProps) {
} }
} }
function copyKeyFrameContextMenuItem(props: IProps, keyframeIds: string[]) { function copyKeyFrameContextMenuItem(
props: IKeyframeDotProps,
keyframeIds: string[],
): IContextMenuItem {
return { return {
label: keyframeIds.length > 1 ? 'Copy selection' : 'Copy keyframe', label: keyframeIds.length > 1 ? 'Copy selection' : 'Copy keyframe',
callback: () => { callback: () => {

View file

@ -12,7 +12,7 @@ import {val} from '@theatre/dataverse'
import React from 'react' import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import Connector from './Connector' import Connector from './Connector'
import Dot from './Dot' import KeyframeDot from './KeyframeDot'
const Container = styled.div` const Container = styled.div`
position: absolute; position: absolute;
@ -20,14 +20,16 @@ const Container = styled.div`
const noConnector = <></> const noConnector = <></>
const KeyframeEditor: React.FC<{ export type IKeyframeEditorProps = {
index: number index: number
keyframe: Keyframe keyframe: Keyframe
trackData: TrackData trackData: TrackData
layoutP: Pointer<SequenceEditorPanelLayout> layoutP: Pointer<SequenceEditorPanelLayout>
leaf: SequenceEditorTree_PrimitiveProp leaf: SequenceEditorTree_PrimitiveProp
selection: undefined | DopeSheetSelection selection: undefined | DopeSheetSelection
}> = (props) => { }
const KeyframeEditor: React.FC<IKeyframeEditorProps> = (props) => {
const {index, trackData} = props const {index, trackData} = props
const cur = trackData.keyframes[index] const cur = trackData.keyframes[index]
const next = trackData.keyframes[index + 1] const next = trackData.keyframes[index + 1]
@ -45,7 +47,7 @@ const KeyframeEditor: React.FC<{
}px))`, }px))`,
}} }}
> >
<Dot {...props} /> <KeyframeDot {...props} />
{connected ? <Connector {...props} /> : noConnector} {connected ? <Connector {...props} /> : noConnector}
</Container> </Container>
) )

View file

@ -109,7 +109,7 @@ function useConnectorContextMenu(node: SVGElement | null, props: IProps) {
const next = trackData.keyframes[index + 1] const next = trackData.keyframes[index + 1]
return useContextMenu(node, { return useContextMenu(node, {
items: () => { menuItems: () => {
return [ return [
{ {
label: 'Delete', label: 'Delete',

View file

@ -243,7 +243,7 @@ function useOurDrags(node: SVGCircleElement | null, props: IProps): void {
function useOurContextMenu(node: SVGCircleElement | null, props: IProps) { function useOurContextMenu(node: SVGCircleElement | null, props: IProps) {
return useContextMenu(node, { return useContextMenu(node, {
items: () => { menuItems: () => {
return [ return [
{ {
label: 'Delete', label: 'Delete',

View file

@ -181,7 +181,7 @@ function useDragKeyframe(
function useKeyframeContextMenu(node: SVGCircleElement | null, props: IProps) { function useKeyframeContextMenu(node: SVGCircleElement | null, props: IProps) {
return useContextMenu(node, { return useContextMenu(node, {
items: () => { menuItems: () => {
return [ return [
{ {
label: 'Delete', label: 'Delete',

View file

@ -235,7 +235,7 @@ function useDragKeyframe(
function useKeyframeContextMenu(node: SVGCircleElement | null, props: IProps) { function useKeyframeContextMenu(node: SVGCircleElement | null, props: IProps) {
return useContextMenu(node, { return useContextMenu(node, {
items: () => { menuItems: () => {
return [ return [
{ {
label: 'Delete', label: 'Delete',

View file

@ -132,9 +132,8 @@ const FocusRangeStrip: React.FC<{
) )
const [contextMenu] = useContextMenu(rangeStripNode, { const [contextMenu] = useContextMenu(rangeStripNode, {
items: () => { menuItems: () => {
const sheet = val(layoutP.sheet) const sheet = val(layoutP.sheet)
const existingRange = existingRangeD.getValue() const existingRange = existingRangeD.getValue()
return [ return [
{ {

View file

@ -21,8 +21,8 @@ const Container = styled.div`
width: 100%; width: 100%;
left: 12px; left: 12px;
/* bottom: 8px; */ /* bottom: 8px; */
${pointerEventsAutoInNormalMode};
z-index: ${() => zIndexes.horizontalScrollbar}; z-index: ${() => zIndexes.horizontalScrollbar};
${pointerEventsAutoInNormalMode}
` `
const TimeThread = styled.div` const TimeThread = styled.div`

View file

@ -18,7 +18,7 @@ const minWidth = 190
*/ */
const pointerDistanceThreshold = 20 const pointerDistanceThreshold = 20
const Container = styled.ul` const MenuContainer = styled.ul`
position: absolute; position: absolute;
min-width: ${minWidth}px; min-width: ${minWidth}px;
z-index: 10000; z-index: 10000;
@ -34,6 +34,10 @@ const Container = styled.ul`
border-radius: 3px; border-radius: 3px;
` `
export type IContextMenuItemCustomNodeRenderFn = (controls: {
closeMenu(): void
}) => React.ReactChild
export type IContextMenuItem = { export type IContextMenuItem = {
label: string | ElementType label: string | ElementType
callback?: (e: React.MouseEvent) => void callback?: (e: React.MouseEvent) => void
@ -41,9 +45,13 @@ export type IContextMenuItem = {
// subs?: Item[] // subs?: Item[]
} }
const RightClickMenu: React.FC<{ export type IContextMenuItemsValue =
items: IContextMenuItem[] | (() => IContextMenuItem[]) | IContextMenuItem[]
rightClickPoint: {clientX: number; clientY: number} | (() => IContextMenuItem[])
const ContextMenu: React.FC<{
items: IContextMenuItemsValue
clickPoint: {clientX: number; clientY: number}
onRequestClose: () => void onRequestClose: () => void
}> = (props) => { }> = (props) => {
const [container, setContainer] = useState<HTMLElement | null>(null) const [container, setContainer] = useState<HTMLElement | null>(null)
@ -59,8 +67,8 @@ const RightClickMenu: React.FC<{
} }
const pos = { const pos = {
left: props.rightClickPoint.clientX - preferredAnchorPoint.left, left: props.clickPoint.clientX - preferredAnchorPoint.left,
top: props.rightClickPoint.clientY - preferredAnchorPoint.top, top: props.clickPoint.clientY - preferredAnchorPoint.top,
} }
if (pos.left < 0) { if (pos.left < 0) {
@ -94,7 +102,7 @@ const RightClickMenu: React.FC<{
return () => { return () => {
window.removeEventListener('mousemove', onMouseMove) window.removeEventListener('mousemove', onMouseMove)
} }
}, [rect, container, props.rightClickPoint, windowSize, props.onRequestClose]) }, [rect, container, props.clickPoint, windowSize, props.onRequestClose])
const portalLayer = useContext(PortalContext) const portalLayer = useContext(PortalContext)
useOnKeyDown((ev) => { useOnKeyDown((ev) => {
@ -104,7 +112,7 @@ const RightClickMenu: React.FC<{
const items = Array.isArray(props.items) ? props.items : props.items() const items = Array.isArray(props.items) ? props.items : props.items()
return createPortal( return createPortal(
<Container ref={setContainer}> <MenuContainer ref={setContainer}>
{items.map((item, i) => ( {items.map((item, i) => (
<Item <Item
key={`item-${i}`} key={`item-${i}`}
@ -118,9 +126,9 @@ const RightClickMenu: React.FC<{
}} }}
/> />
))} ))}
</Container>, </MenuContainer>,
portalLayer!, portalLayer!,
) )
} }
export default RightClickMenu export default ContextMenu

View file

@ -5,7 +5,7 @@ import styled from 'styled-components'
export const height = 26 export const height = 26
const Container = styled.li<{enabled: boolean}>` const ItemContainer = styled.li<{enabled: boolean}>`
height: ${height}px; height: ${height}px;
padding: 0 12px; padding: 0 12px;
margin: 0; margin: 0;
@ -32,7 +32,7 @@ const Container = styled.li<{enabled: boolean}>`
} }
` `
const Label = styled.span`` const ItemLabel = styled.span``
const Item: React.FC<{ const Item: React.FC<{
label: string | ElementType label: string | ElementType
@ -40,12 +40,12 @@ const Item: React.FC<{
enabled: boolean enabled: boolean
}> = (props) => { }> = (props) => {
return ( return (
<Container <ItemContainer
onClick={props.enabled ? props.onClick : noop} onClick={props.enabled ? props.onClick : noop}
enabled={props.enabled} enabled={props.enabled}
> >
<Label>{props.label}</Label> <ItemLabel>{props.label}</ItemLabel>
</Container> </ItemContainer>
) )
} }

View file

@ -1,28 +1,36 @@
import type {VoidFn} from '@theatre/shared/utils/types' import type {VoidFn} from '@theatre/shared/utils/types'
import React from 'react' import React from 'react'
import RightClickMenu from './RightClickMenu/RightClickMenu' import ContextMenu from './ContextMenu/ContextMenu'
import type {
IContextMenuItemsValue,
IContextMenuItem,
} from './ContextMenu/ContextMenu'
import useRequestContextMenu from './useRequestContextMenu' import useRequestContextMenu from './useRequestContextMenu'
export type {IContextMenuItem} from './RightClickMenu/RightClickMenu' import type {IRequestContextMenuOptions} from './useRequestContextMenu'
// re-exports
export type {
IContextMenuItemsValue,
IContextMenuItem,
IRequestContextMenuOptions,
}
const emptyNode = <></> const emptyNode = <></>
type IProps = Omit<
Parameters<typeof RightClickMenu>[0],
'rightClickPoint' | 'onRequestClose'
>
export default function useContextMenu( export default function useContextMenu(
target: HTMLElement | SVGElement | null, target: HTMLElement | SVGElement | null,
props: IProps, opts: IRequestContextMenuOptions & {
menuItems: IContextMenuItemsValue
},
): [node: React.ReactNode, close: VoidFn, isOpen: boolean] { ): [node: React.ReactNode, close: VoidFn, isOpen: boolean] {
const [status, close] = useRequestContextMenu(target) const [status, close] = useRequestContextMenu(target, opts)
const node = !status.isOpen ? ( const node = !status.isOpen ? (
emptyNode emptyNode
) : ( ) : (
<RightClickMenu <ContextMenu
items={props.items} items={opts.menuItems}
rightClickPoint={status.event} clickPoint={status.event}
onRequestClose={close} onRequestClose={close}
/> />
) )

View file

@ -5,28 +5,33 @@ type IState = {isOpen: true; event: MouseEvent} | {isOpen: false}
type CloseMenuFn = () => void type CloseMenuFn = () => void
export type IRequestContextMenuOptions = {
disabled?: boolean
}
const useRequestContextMenu = ( const useRequestContextMenu = (
target: HTMLElement | SVGElement | null, target: HTMLElement | SVGElement | null,
opts: IRequestContextMenuOptions,
): [state: IState, close: CloseMenuFn] => { ): [state: IState, close: CloseMenuFn] => {
const [state, setState] = useState<IState>({isOpen: false}) const [state, setState] = useState<IState>({isOpen: false})
const close = useCallback<CloseMenuFn>(() => setState({isOpen: false}), []) const close = useCallback<CloseMenuFn>(() => setState({isOpen: false}), [])
useEffect(() => { useEffect(() => {
if (!target) { if (!target || opts.disabled === true) {
setState({isOpen: false}) setState({isOpen: false})
return return
} }
const onContextMenu = (event: MouseEvent) => { const onTrigger = (event: MouseEvent) => {
setState({isOpen: true, event}) setState({isOpen: true, event})
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
} }
target.addEventListener('contextmenu', onContextMenu as $FixMe) target.addEventListener('contextmenu', onTrigger as $FixMe)
return () => { return () => {
target.removeEventListener('contextmenu', onContextMenu as $FixMe) target.removeEventListener('contextmenu', onTrigger as $FixMe)
} }
}, [target]) }, [target, opts.disabled])
return [state, close] return [state, close]
} }