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)
const [contextMenu] = useContextMenu(propNameContainer, {
items: stuff.contextMenuItems,
menuItems: stuff.contextMenuItems,
})
const color = shadeToColor[stuff.shade]

View file

@ -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: () => {

View file

@ -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',

View file

@ -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: () => {

View file

@ -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>
)

View file

@ -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',

View file

@ -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',

View file

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

View file

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

View file

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

View file

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

View file

@ -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

View file

@ -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>
)
}

View file

@ -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}
/>
)

View file

@ -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]
}