refactor/docs: Minor identifier tweaks for RightClick/ContextMenu
This commit is contained in:
parent
f04bc3e31a
commit
91794f550d
15 changed files with 99 additions and 62 deletions
|
@ -119,7 +119,7 @@ export const SingleRowPropEditor: React.FC<{
|
|||
useRefAndState<HTMLDivElement | null>(null)
|
||||
|
||||
const [contextMenu] = useContextMenu(propNameContainer, {
|
||||
items: stuff.contextMenuItems,
|
||||
menuItems: stuff.contextMenuItems,
|
||||
})
|
||||
|
||||
const color = shadeToColor[stuff.shade]
|
||||
|
|
|
@ -8,6 +8,7 @@ import {val} from '@theatre/dataverse'
|
|||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import KeyframeEditor from './KeyframeEditor/KeyframeEditor'
|
||||
import type {IContextMenuItem} from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
|
||||
import useContextMenu from '@theatre/studio/uiComponents/simpleContextMenu/useContextMenu'
|
||||
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
||||
import getStudio from '@theatre/studio/getStudio'
|
||||
|
@ -85,7 +86,7 @@ function useBasicKeyframedTrackContextMenu(
|
|||
props: BasicKeyframedTracksProps,
|
||||
) {
|
||||
return useContextMenu(node, {
|
||||
items: () => {
|
||||
menuItems: () => {
|
||||
const selectionKeyframes =
|
||||
val(getStudio()!.atomP.ahistoric.clipboard.keyframes) || []
|
||||
|
||||
|
@ -101,7 +102,7 @@ function useBasicKeyframedTrackContextMenu(
|
|||
function pasteKeyframesContextMenuItem(
|
||||
props: BasicKeyframedTracksProps,
|
||||
keyframes: Keyframe[],
|
||||
) {
|
||||
): IContextMenuItem {
|
||||
return {
|
||||
label: 'Paste Keyframes',
|
||||
callback: () => {
|
||||
|
|
|
@ -12,7 +12,7 @@ import type {
|
|||
SequenceEditorPanelLayout,
|
||||
DopeSheetSelection,
|
||||
} 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 Sequence from '@theatre/core/sequences/Sequence'
|
||||
import usePopover from '@theatre/studio/uiComponents/Popover/usePopover'
|
||||
|
@ -250,7 +250,7 @@ function useConnectorContextMenu(
|
|||
) {
|
||||
const maybeKeyframeIds = selectedKeyframeIdsIfInSingleTrack(props.selection)
|
||||
return useContextMenu(node, {
|
||||
items: () => {
|
||||
menuItems: () => {
|
||||
return [
|
||||
{
|
||||
label: maybeKeyframeIds ? 'Copy Selection' : 'Copy both Keyframes',
|
||||
|
|
|
@ -5,13 +5,14 @@ import type {
|
|||
import getStudio from '@theatre/studio/getStudio'
|
||||
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
|
||||
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 type {UseDragOpts} from '@theatre/studio/uiComponents/useDrag'
|
||||
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
||||
import {val} from '@theatre/dataverse'
|
||||
import {lighten} from 'polished'
|
||||
import React, {useMemo, useRef, useState} from 'react'
|
||||
import styled from 'styled-components'
|
||||
import type KeyframeEditor from './KeyframeEditor'
|
||||
import {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||
import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||
import {
|
||||
|
@ -20,6 +21,7 @@ import {
|
|||
} from '@theatre/studio/uiComponents/PointerEventsHandler'
|
||||
import SnapCursor from './SnapCursor.svg'
|
||||
import selectedKeyframeIdsIfInSingleTrack from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/selectedKeyframeIdsIfInSingleTrack'
|
||||
import type {IKeyframeEditorProps} from './KeyframeEditor'
|
||||
|
||||
export const DOT_SIZE_PX = 6
|
||||
const HIT_ZONE_SIZE_PX = 12
|
||||
|
@ -42,6 +44,8 @@ const dotTheme = {
|
|||
|
||||
const Square = styled.div<{isSelected: boolean}>`
|
||||
position: absolute;
|
||||
${dims(DOT_SIZE_PX)}
|
||||
|
||||
background: ${(props) =>
|
||||
props.isSelected ? dotTheme.selectedColor : dotTheme.normalColor};
|
||||
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 [contextMenu] = useKeyframeContextMenu(node, props)
|
||||
const [isDragging] = useDragKeyframe(node, props)
|
||||
const [contextMenu] = useKeyframeContextMenu(node, props)
|
||||
|
||||
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(
|
||||
props.selection,
|
||||
)
|
||||
|
@ -123,8 +131,8 @@ function useKeyframeContextMenu(node: HTMLDivElement | null, props: IProps) {
|
|||
|
||||
const deleteItem = deleteSelectionOrKeyframeContextMenuItem(props)
|
||||
|
||||
return useContextMenu(node, {
|
||||
items: () => {
|
||||
return useContextMenu(target, {
|
||||
menuItems: () => {
|
||||
return [keyframeSelectionItem, deleteItem]
|
||||
},
|
||||
})
|
||||
|
@ -132,7 +140,7 @@ function useKeyframeContextMenu(node: HTMLDivElement | null, props: IProps) {
|
|||
|
||||
function useDragKeyframe(
|
||||
node: HTMLDivElement | null,
|
||||
props: IProps,
|
||||
props: IKeyframeDotProps,
|
||||
): [isDragging: boolean] {
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
useLockFrameStampPosition(isDragging, props.keyframe.position)
|
||||
|
@ -140,10 +148,10 @@ function useDragKeyframe(
|
|||
const propsRef = useRef(props)
|
||||
propsRef.current = props
|
||||
|
||||
const gestureHandlers = useMemo<Parameters<typeof useDrag>[1]>(() => {
|
||||
const useDragOpts = useMemo<UseDragOpts>(() => {
|
||||
let toUnitSpace: SequenceEditorPanelLayout['scaledSpace']['toUnitSpace']
|
||||
let tempTransaction: CommitOrDiscard | undefined
|
||||
let propsAtStartOfDrag: IProps
|
||||
let propsAtStartOfDrag: IKeyframeDotProps
|
||||
|
||||
let selectionDragHandlers:
|
||||
| ReturnType<DopeSheetSelection['getDragHandlers']>
|
||||
|
@ -151,6 +159,7 @@ function useDragKeyframe(
|
|||
|
||||
return {
|
||||
debugName: 'Dot/useDragKeyframe',
|
||||
|
||||
onDragStart(event) {
|
||||
setIsDragging(true)
|
||||
const props = propsRef.current
|
||||
|
@ -241,14 +250,16 @@ function useDragKeyframe(
|
|||
}
|
||||
}, [])
|
||||
|
||||
useDrag(node, gestureHandlers)
|
||||
useDrag(node, useDragOpts)
|
||||
|
||||
useCssCursorLock(isDragging, 'draggingPositionInSequenceEditor', 'ew-resize')
|
||||
|
||||
return [isDragging]
|
||||
}
|
||||
|
||||
function deleteSelectionOrKeyframeContextMenuItem(props: IProps) {
|
||||
function deleteSelectionOrKeyframeContextMenuItem(
|
||||
props: IKeyframeDotProps,
|
||||
): IContextMenuItem {
|
||||
return {
|
||||
label: props.selection ? 'Delete Selection' : 'Delete Keyframe',
|
||||
callback: () => {
|
||||
|
@ -269,7 +280,10 @@ function deleteSelectionOrKeyframeContextMenuItem(props: IProps) {
|
|||
}
|
||||
}
|
||||
|
||||
function copyKeyFrameContextMenuItem(props: IProps, keyframeIds: string[]) {
|
||||
function copyKeyFrameContextMenuItem(
|
||||
props: IKeyframeDotProps,
|
||||
keyframeIds: string[],
|
||||
): IContextMenuItem {
|
||||
return {
|
||||
label: keyframeIds.length > 1 ? 'Copy selection' : 'Copy keyframe',
|
||||
callback: () => {
|
|
@ -12,7 +12,7 @@ import {val} from '@theatre/dataverse'
|
|||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import Connector from './Connector'
|
||||
import Dot from './Dot'
|
||||
import KeyframeDot from './KeyframeDot'
|
||||
|
||||
const Container = styled.div`
|
||||
position: absolute;
|
||||
|
@ -20,14 +20,16 @@ const Container = styled.div`
|
|||
|
||||
const noConnector = <></>
|
||||
|
||||
const KeyframeEditor: React.FC<{
|
||||
export type IKeyframeEditorProps = {
|
||||
index: number
|
||||
keyframe: Keyframe
|
||||
trackData: TrackData
|
||||
layoutP: Pointer<SequenceEditorPanelLayout>
|
||||
leaf: SequenceEditorTree_PrimitiveProp
|
||||
selection: undefined | DopeSheetSelection
|
||||
}> = (props) => {
|
||||
}
|
||||
|
||||
const KeyframeEditor: React.FC<IKeyframeEditorProps> = (props) => {
|
||||
const {index, trackData} = props
|
||||
const cur = trackData.keyframes[index]
|
||||
const next = trackData.keyframes[index + 1]
|
||||
|
@ -45,7 +47,7 @@ const KeyframeEditor: React.FC<{
|
|||
}px))`,
|
||||
}}
|
||||
>
|
||||
<Dot {...props} />
|
||||
<KeyframeDot {...props} />
|
||||
{connected ? <Connector {...props} /> : noConnector}
|
||||
</Container>
|
||||
)
|
||||
|
|
|
@ -109,7 +109,7 @@ function useConnectorContextMenu(node: SVGElement | null, props: IProps) {
|
|||
const next = trackData.keyframes[index + 1]
|
||||
|
||||
return useContextMenu(node, {
|
||||
items: () => {
|
||||
menuItems: () => {
|
||||
return [
|
||||
{
|
||||
label: 'Delete',
|
||||
|
|
|
@ -243,7 +243,7 @@ function useOurDrags(node: SVGCircleElement | null, props: IProps): void {
|
|||
|
||||
function useOurContextMenu(node: SVGCircleElement | null, props: IProps) {
|
||||
return useContextMenu(node, {
|
||||
items: () => {
|
||||
menuItems: () => {
|
||||
return [
|
||||
{
|
||||
label: 'Delete',
|
||||
|
|
|
@ -181,7 +181,7 @@ function useDragKeyframe(
|
|||
|
||||
function useKeyframeContextMenu(node: SVGCircleElement | null, props: IProps) {
|
||||
return useContextMenu(node, {
|
||||
items: () => {
|
||||
menuItems: () => {
|
||||
return [
|
||||
{
|
||||
label: 'Delete',
|
||||
|
|
|
@ -235,7 +235,7 @@ function useDragKeyframe(
|
|||
|
||||
function useKeyframeContextMenu(node: SVGCircleElement | null, props: IProps) {
|
||||
return useContextMenu(node, {
|
||||
items: () => {
|
||||
menuItems: () => {
|
||||
return [
|
||||
{
|
||||
label: 'Delete',
|
||||
|
|
|
@ -132,9 +132,8 @@ const FocusRangeStrip: React.FC<{
|
|||
)
|
||||
|
||||
const [contextMenu] = useContextMenu(rangeStripNode, {
|
||||
items: () => {
|
||||
menuItems: () => {
|
||||
const sheet = val(layoutP.sheet)
|
||||
|
||||
const existingRange = existingRangeD.getValue()
|
||||
return [
|
||||
{
|
||||
|
|
|
@ -21,8 +21,8 @@ const Container = styled.div`
|
|||
width: 100%;
|
||||
left: 12px;
|
||||
/* bottom: 8px; */
|
||||
${pointerEventsAutoInNormalMode};
|
||||
z-index: ${() => zIndexes.horizontalScrollbar};
|
||||
${pointerEventsAutoInNormalMode}
|
||||
`
|
||||
|
||||
const TimeThread = styled.div`
|
||||
|
|
|
@ -18,7 +18,7 @@ const minWidth = 190
|
|||
*/
|
||||
const pointerDistanceThreshold = 20
|
||||
|
||||
const Container = styled.ul`
|
||||
const MenuContainer = styled.ul`
|
||||
position: absolute;
|
||||
min-width: ${minWidth}px;
|
||||
z-index: 10000;
|
||||
|
@ -34,6 +34,10 @@ const Container = styled.ul`
|
|||
border-radius: 3px;
|
||||
`
|
||||
|
||||
export type IContextMenuItemCustomNodeRenderFn = (controls: {
|
||||
closeMenu(): void
|
||||
}) => React.ReactChild
|
||||
|
||||
export type IContextMenuItem = {
|
||||
label: string | ElementType
|
||||
callback?: (e: React.MouseEvent) => void
|
||||
|
@ -41,9 +45,13 @@ export type IContextMenuItem = {
|
|||
// subs?: Item[]
|
||||
}
|
||||
|
||||
const RightClickMenu: React.FC<{
|
||||
items: IContextMenuItem[] | (() => IContextMenuItem[])
|
||||
rightClickPoint: {clientX: number; clientY: number}
|
||||
export type IContextMenuItemsValue =
|
||||
| IContextMenuItem[]
|
||||
| (() => IContextMenuItem[])
|
||||
|
||||
const ContextMenu: React.FC<{
|
||||
items: IContextMenuItemsValue
|
||||
clickPoint: {clientX: number; clientY: number}
|
||||
onRequestClose: () => void
|
||||
}> = (props) => {
|
||||
const [container, setContainer] = useState<HTMLElement | null>(null)
|
||||
|
@ -59,8 +67,8 @@ const RightClickMenu: React.FC<{
|
|||
}
|
||||
|
||||
const pos = {
|
||||
left: props.rightClickPoint.clientX - preferredAnchorPoint.left,
|
||||
top: props.rightClickPoint.clientY - preferredAnchorPoint.top,
|
||||
left: props.clickPoint.clientX - preferredAnchorPoint.left,
|
||||
top: props.clickPoint.clientY - preferredAnchorPoint.top,
|
||||
}
|
||||
|
||||
if (pos.left < 0) {
|
||||
|
@ -94,7 +102,7 @@ const RightClickMenu: React.FC<{
|
|||
return () => {
|
||||
window.removeEventListener('mousemove', onMouseMove)
|
||||
}
|
||||
}, [rect, container, props.rightClickPoint, windowSize, props.onRequestClose])
|
||||
}, [rect, container, props.clickPoint, windowSize, props.onRequestClose])
|
||||
const portalLayer = useContext(PortalContext)
|
||||
|
||||
useOnKeyDown((ev) => {
|
||||
|
@ -104,7 +112,7 @@ const RightClickMenu: React.FC<{
|
|||
const items = Array.isArray(props.items) ? props.items : props.items()
|
||||
|
||||
return createPortal(
|
||||
<Container ref={setContainer}>
|
||||
<MenuContainer ref={setContainer}>
|
||||
{items.map((item, i) => (
|
||||
<Item
|
||||
key={`item-${i}`}
|
||||
|
@ -118,9 +126,9 @@ const RightClickMenu: React.FC<{
|
|||
}}
|
||||
/>
|
||||
))}
|
||||
</Container>,
|
||||
</MenuContainer>,
|
||||
portalLayer!,
|
||||
)
|
||||
}
|
||||
|
||||
export default RightClickMenu
|
||||
export default ContextMenu
|
|
@ -5,7 +5,7 @@ import styled from 'styled-components'
|
|||
|
||||
export const height = 26
|
||||
|
||||
const Container = styled.li<{enabled: boolean}>`
|
||||
const ItemContainer = styled.li<{enabled: boolean}>`
|
||||
height: ${height}px;
|
||||
padding: 0 12px;
|
||||
margin: 0;
|
||||
|
@ -32,7 +32,7 @@ const Container = styled.li<{enabled: boolean}>`
|
|||
}
|
||||
`
|
||||
|
||||
const Label = styled.span``
|
||||
const ItemLabel = styled.span``
|
||||
|
||||
const Item: React.FC<{
|
||||
label: string | ElementType
|
||||
|
@ -40,12 +40,12 @@ const Item: React.FC<{
|
|||
enabled: boolean
|
||||
}> = (props) => {
|
||||
return (
|
||||
<Container
|
||||
<ItemContainer
|
||||
onClick={props.enabled ? props.onClick : noop}
|
||||
enabled={props.enabled}
|
||||
>
|
||||
<Label>{props.label}</Label>
|
||||
</Container>
|
||||
<ItemLabel>{props.label}</ItemLabel>
|
||||
</ItemContainer>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,28 +1,36 @@
|
|||
import type {VoidFn} from '@theatre/shared/utils/types'
|
||||
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'
|
||||
export type {IContextMenuItem} from './RightClickMenu/RightClickMenu'
|
||||
import type {IRequestContextMenuOptions} from './useRequestContextMenu'
|
||||
|
||||
// re-exports
|
||||
export type {
|
||||
IContextMenuItemsValue,
|
||||
IContextMenuItem,
|
||||
IRequestContextMenuOptions,
|
||||
}
|
||||
|
||||
const emptyNode = <></>
|
||||
|
||||
type IProps = Omit<
|
||||
Parameters<typeof RightClickMenu>[0],
|
||||
'rightClickPoint' | 'onRequestClose'
|
||||
>
|
||||
|
||||
export default function useContextMenu(
|
||||
target: HTMLElement | SVGElement | null,
|
||||
props: IProps,
|
||||
opts: IRequestContextMenuOptions & {
|
||||
menuItems: IContextMenuItemsValue
|
||||
},
|
||||
): [node: React.ReactNode, close: VoidFn, isOpen: boolean] {
|
||||
const [status, close] = useRequestContextMenu(target)
|
||||
const [status, close] = useRequestContextMenu(target, opts)
|
||||
|
||||
const node = !status.isOpen ? (
|
||||
emptyNode
|
||||
) : (
|
||||
<RightClickMenu
|
||||
items={props.items}
|
||||
rightClickPoint={status.event}
|
||||
<ContextMenu
|
||||
items={opts.menuItems}
|
||||
clickPoint={status.event}
|
||||
onRequestClose={close}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -5,28 +5,33 @@ type IState = {isOpen: true; event: MouseEvent} | {isOpen: false}
|
|||
|
||||
type CloseMenuFn = () => void
|
||||
|
||||
export type IRequestContextMenuOptions = {
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const useRequestContextMenu = (
|
||||
target: HTMLElement | SVGElement | null,
|
||||
opts: IRequestContextMenuOptions,
|
||||
): [state: IState, close: CloseMenuFn] => {
|
||||
const [state, setState] = useState<IState>({isOpen: false})
|
||||
const close = useCallback<CloseMenuFn>(() => setState({isOpen: false}), [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!target) {
|
||||
if (!target || opts.disabled === true) {
|
||||
setState({isOpen: false})
|
||||
return
|
||||
}
|
||||
|
||||
const onContextMenu = (event: MouseEvent) => {
|
||||
const onTrigger = (event: MouseEvent) => {
|
||||
setState({isOpen: true, event})
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
target.addEventListener('contextmenu', onContextMenu as $FixMe)
|
||||
target.addEventListener('contextmenu', onTrigger as $FixMe)
|
||||
return () => {
|
||||
target.removeEventListener('contextmenu', onContextMenu as $FixMe)
|
||||
target.removeEventListener('contextmenu', onTrigger as $FixMe)
|
||||
}
|
||||
}, [target])
|
||||
}, [target, opts.disabled])
|
||||
|
||||
return [state, close]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue