Editor popover for SequenceLengthIndicator
This commit is contained in:
parent
3745ce02ff
commit
58e9d9ff8b
8 changed files with 134 additions and 20 deletions
|
@ -74,6 +74,7 @@
|
||||||
"react-error-boundary": "^3.1.3",
|
"react-error-boundary": "^3.1.3",
|
||||||
"react-icons": "^4.2.0",
|
"react-icons": "^4.2.0",
|
||||||
"react-is": "^17.0.2",
|
"react-is": "^17.0.2",
|
||||||
|
"react-merge-refs": "^1.1.0",
|
||||||
"react-shadow": "^19.0.2",
|
"react-shadow": "^19.0.2",
|
||||||
"react-use": "^17.2.4",
|
"react-use": "^17.2.4",
|
||||||
"react-use-gesture": "^9.1.3",
|
"react-use-gesture": "^9.1.3",
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
import type {Pointer} from '@theatre/dataverse'
|
||||||
|
import React, {useLayoutEffect, useMemo, useRef} from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import type {SequenceEditorPanelLayout} from '@theatre/studio/panels/SequenceEditorPanel/layout/layout'
|
||||||
|
import {usePrism, useVal} from '@theatre/dataverse-react'
|
||||||
|
import getStudio from '@theatre/studio/getStudio'
|
||||||
|
import BasicNumberInput from '@theatre/studio/uiComponents/form/BasicNumberInput'
|
||||||
|
import type {CommitOrDiscard} from '@theatre/studio/StudioStore/StudioStore'
|
||||||
|
import {propNameText} from '@theatre/studio/panels/ObjectEditorPanel/propEditors/utils/SingleRowPropEditor'
|
||||||
|
|
||||||
|
const greaterThanZero = (v: number) => isFinite(v) && v > 0
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
height: 28px;
|
||||||
|
align-items: center;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Label = styled.div`
|
||||||
|
${propNameText};
|
||||||
|
white-space: nowrap;
|
||||||
|
`
|
||||||
|
|
||||||
|
const LengthEditorPopover: React.FC<{
|
||||||
|
layoutP: Pointer<SequenceEditorPanelLayout>
|
||||||
|
/**
|
||||||
|
* Called when user hits enter/escape
|
||||||
|
*/
|
||||||
|
onRequestClose: () => void
|
||||||
|
}> = ({layoutP, onRequestClose}) => {
|
||||||
|
const sheet = useVal(layoutP.sheet)
|
||||||
|
|
||||||
|
const fns = useMemo(() => {
|
||||||
|
let tempTransaction: CommitOrDiscard | undefined
|
||||||
|
|
||||||
|
return {
|
||||||
|
temporarilySetValue(newLength: number): void {
|
||||||
|
if (tempTransaction) {
|
||||||
|
tempTransaction.discard()
|
||||||
|
tempTransaction = undefined
|
||||||
|
}
|
||||||
|
tempTransaction = getStudio()!.tempTransaction(({stateEditors}) => {
|
||||||
|
stateEditors.coreByProject.historic.sheetsById.sequence.setLength({
|
||||||
|
...sheet.address,
|
||||||
|
length: newLength,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
discardTemporaryValue(): void {
|
||||||
|
if (tempTransaction) {
|
||||||
|
tempTransaction.discard()
|
||||||
|
tempTransaction = undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
permenantlySetValue(newLength: number): void {
|
||||||
|
if (tempTransaction) {
|
||||||
|
tempTransaction.discard()
|
||||||
|
tempTransaction = undefined
|
||||||
|
}
|
||||||
|
getStudio()!.transaction(({stateEditors}) => {
|
||||||
|
stateEditors.coreByProject.historic.sheetsById.sequence.setLength({
|
||||||
|
...sheet.address,
|
||||||
|
length: newLength,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}, [layoutP, sheet])
|
||||||
|
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
inputRef.current!.focus()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return usePrism(() => {
|
||||||
|
const sequence = sheet.getSequence()
|
||||||
|
const sequenceLength = sequence.length
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Label>Sequence length</Label>
|
||||||
|
<BasicNumberInput
|
||||||
|
value={sequenceLength}
|
||||||
|
{...fns}
|
||||||
|
isValid={greaterThanZero}
|
||||||
|
inputRef={inputRef}
|
||||||
|
onBlur={onRequestClose}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}, [sheet, fns, inputRef])
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LengthEditorPopover
|
|
@ -17,6 +17,7 @@ import {
|
||||||
useLockFrameStampPosition,
|
useLockFrameStampPosition,
|
||||||
} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
|
||||||
import {GoChevronLeft, GoChevronRight} from 'react-icons/all'
|
import {GoChevronLeft, GoChevronRight} from 'react-icons/all'
|
||||||
|
import LengthEditorPopover from './LengthEditorPopover'
|
||||||
|
|
||||||
const coverWidth = 1000
|
const coverWidth = 1000
|
||||||
|
|
||||||
|
@ -130,9 +131,13 @@ type IProps = {
|
||||||
const LengthIndicator: React.FC<IProps> = ({layoutP}) => {
|
const LengthIndicator: React.FC<IProps> = ({layoutP}) => {
|
||||||
const [nodeRef, node] = useRefAndState<HTMLDivElement | null>(null)
|
const [nodeRef, node] = useRefAndState<HTMLDivElement | null>(null)
|
||||||
const [isDraggingD] = useDragBulge(node, {layoutP})
|
const [isDraggingD] = useDragBulge(node, {layoutP})
|
||||||
const [popoverNode, openPopover, _, isPopoverOpen] = usePopover(() => {
|
const [popoverNode, openPopover, closePopover, isPopoverOpen] = usePopover(
|
||||||
return <div>poppio</div>
|
() => {
|
||||||
})
|
return (
|
||||||
|
<LengthEditorPopover layoutP={layoutP} onRequestClose={closePopover} />
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
return usePrism(() => {
|
return usePrism(() => {
|
||||||
const sheet = val(layoutP.sheet)
|
const sheet = val(layoutP.sheet)
|
|
@ -5,7 +5,7 @@ import type {Pointer} from '@theatre/dataverse'
|
||||||
import {val} from '@theatre/dataverse'
|
import {val} from '@theatre/dataverse'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import LengthIndicator from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/LengthIndicator'
|
import LengthIndicator from '@theatre/studio/panels/SequenceEditorPanel/DopeSheet/Right/LengthIndicator/LengthIndicator'
|
||||||
import FrameStamp from './FrameStamp'
|
import FrameStamp from './FrameStamp'
|
||||||
import HorizontalScrollbar from './HorizontalScrollbar'
|
import HorizontalScrollbar from './HorizontalScrollbar'
|
||||||
import Playhead from './Playhead'
|
import Playhead from './Playhead'
|
||||||
|
|
|
@ -392,7 +392,7 @@ namespace stateEditors {
|
||||||
export function setLength(
|
export function setLength(
|
||||||
p: WithoutSheetInstance<SheetAddress> & {length: number},
|
p: WithoutSheetInstance<SheetAddress> & {length: number},
|
||||||
) {
|
) {
|
||||||
_ensure(p).length = p.length
|
_ensure(p).length = parseFloat(p.length.toFixed(2))
|
||||||
}
|
}
|
||||||
|
|
||||||
function _ensureTracksOfObject(
|
function _ensureTracksOfObject(
|
||||||
|
|
|
@ -6,8 +6,6 @@ import {createPortal} from 'react-dom'
|
||||||
import useWindowSize from 'react-use/esm/useWindowSize'
|
import useWindowSize from 'react-use/esm/useWindowSize'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
const minWidth = 190
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How far from the menu should the pointer travel to auto close the menu
|
* How far from the menu should the pointer travel to auto close the menu
|
||||||
*/
|
*/
|
||||||
|
@ -15,14 +13,11 @@ const defaultPointerDistanceThreshold = 200
|
||||||
|
|
||||||
const Container = styled.ul`
|
const Container = styled.ul`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
min-width: ${minWidth}px;
|
|
||||||
z-index: 10000;
|
z-index: 10000;
|
||||||
background: ${transparentize(0.2, '#111')};
|
background: ${transparentize(0.2, '#111')};
|
||||||
color: white;
|
color: white;
|
||||||
list-style-type: none;
|
padding: 0;
|
||||||
padding: 2px 0;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border-radius: 1px;
|
|
||||||
cursor: default;
|
cursor: default;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import {theme} from '@theatre/studio/css'
|
import {theme} from '@theatre/studio/css'
|
||||||
import {clamp, isInteger, round} from 'lodash-es'
|
import {clamp, isInteger, round} from 'lodash-es'
|
||||||
import {darken, lighten} from 'polished'
|
import {darken, lighten} from 'polished'
|
||||||
|
import type {MutableRefObject} from 'react'
|
||||||
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 DraggableArea from '@theatre/studio/uiComponents/DraggableArea'
|
import DraggableArea from '@theatre/studio/uiComponents/DraggableArea'
|
||||||
|
import mergeRefs from 'react-merge-refs'
|
||||||
|
|
||||||
type IMode = IState['mode']
|
type IMode = IState['mode']
|
||||||
|
|
||||||
|
@ -75,9 +77,8 @@ const FillIndicator = styled.div`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
function isValueAcceptable(s: string) {
|
function isFiniteFloat(s: string) {
|
||||||
const v = parseFloat(s)
|
return isFinite(parseFloat(s))
|
||||||
return !isNaN(v)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type IState_NoFocus = {
|
type IState_NoFocus = {
|
||||||
|
@ -98,6 +99,8 @@ type IState_Dragging = {
|
||||||
|
|
||||||
type IState = IState_NoFocus | IState_EditingViaKeyboard | IState_Dragging
|
type IState = IState_NoFocus | IState_EditingViaKeyboard | IState_Dragging
|
||||||
|
|
||||||
|
const alwaysValid = (v: number) => true
|
||||||
|
|
||||||
const BasicNumberInput: React.FC<{
|
const BasicNumberInput: React.FC<{
|
||||||
value: number
|
value: number
|
||||||
temporarilySetValue: (v: number) => void
|
temporarilySetValue: (v: number) => void
|
||||||
|
@ -105,13 +108,22 @@ const BasicNumberInput: React.FC<{
|
||||||
permenantlySetValue: (v: number) => void
|
permenantlySetValue: (v: number) => void
|
||||||
className?: string
|
className?: string
|
||||||
range?: [min: number, max: number]
|
range?: [min: number, max: number]
|
||||||
|
isValid?: (v: number) => boolean
|
||||||
|
inputRef?: MutableRefObject<HTMLInputElement | null>
|
||||||
|
/**
|
||||||
|
* Called when the user hits Enter. One of the *SetValue() callbacks will be called
|
||||||
|
* before this, so use this for UI purposes such as closing a popover.
|
||||||
|
*/
|
||||||
|
onBlur?: () => void
|
||||||
}> = (propsA) => {
|
}> = (propsA) => {
|
||||||
const [stateA, setState] = useState<IState>({mode: 'noFocus'})
|
const [stateA, setState] = useState<IState>({mode: 'noFocus'})
|
||||||
|
const isValid = propsA.isValid ?? alwaysValid
|
||||||
|
|
||||||
const refs = useRef({state: stateA, props: propsA})
|
const refs = useRef({state: stateA, props: propsA})
|
||||||
refs.current = {state: stateA, props: propsA}
|
refs.current = {state: stateA, props: propsA}
|
||||||
|
|
||||||
const inputRef = useRef<HTMLInputElement | null>(null)
|
const inputRef = useRef<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
const bodyCursorBeforeDrag = useRef<string | null>(null)
|
const bodyCursorBeforeDrag = useRef<string | null>(null)
|
||||||
|
|
||||||
const callbacks = useMemo(() => {
|
const callbacks = useMemo(() => {
|
||||||
|
@ -122,9 +134,9 @@ const BasicNumberInput: React.FC<{
|
||||||
|
|
||||||
setState({...curState, currentEditedValueInString: value})
|
setState({...curState, currentEditedValueInString: value})
|
||||||
|
|
||||||
if (!isValueAcceptable(value)) return
|
|
||||||
|
|
||||||
const valInFloat = parseFloat(value)
|
const valInFloat = parseFloat(value)
|
||||||
|
if (!isFinite(valInFloat) || !isValid(valInFloat)) return
|
||||||
|
|
||||||
refs.current.props.temporarilySetValue(valInFloat)
|
refs.current.props.temporarilySetValue(valInFloat)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,16 +144,17 @@ const BasicNumberInput: React.FC<{
|
||||||
if (refs.current.state.mode === 'editingViaKeyboard') {
|
if (refs.current.state.mode === 'editingViaKeyboard') {
|
||||||
commitKeyboardInput()
|
commitKeyboardInput()
|
||||||
setState({mode: 'noFocus'})
|
setState({mode: 'noFocus'})
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
|
if (propsA.onBlur) propsA.onBlur()
|
||||||
}
|
}
|
||||||
|
|
||||||
const commitKeyboardInput = () => {
|
const commitKeyboardInput = () => {
|
||||||
const curState = refs.current.state as IState_EditingViaKeyboard
|
const curState = refs.current.state as IState_EditingViaKeyboard
|
||||||
if (!isValueAcceptable(curState.currentEditedValueInString)) {
|
const value = parseFloat(curState.currentEditedValueInString)
|
||||||
|
|
||||||
|
if (!isFinite(value) || !isValid(value)) {
|
||||||
refs.current.props.discardTemporaryValue()
|
refs.current.props.discardTemporaryValue()
|
||||||
} else {
|
} else {
|
||||||
const value = parseFloat(curState.currentEditedValueInString)
|
|
||||||
if (curState.valueBeforeEditing === value) {
|
if (curState.valueBeforeEditing === value) {
|
||||||
refs.current.props.discardTemporaryValue()
|
refs.current.props.discardTemporaryValue()
|
||||||
} else {
|
} else {
|
||||||
|
@ -256,6 +269,9 @@ const BasicNumberInput: React.FC<{
|
||||||
value = 'NaN'
|
value = 'NaN'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const _refs = [inputRef]
|
||||||
|
if (propsA.inputRef) _refs.push(propsA.inputRef)
|
||||||
|
|
||||||
const theInput = (
|
const theInput = (
|
||||||
<Input
|
<Input
|
||||||
key="input"
|
key="input"
|
||||||
|
@ -266,7 +282,7 @@ const BasicNumberInput: React.FC<{
|
||||||
onKeyDown={callbacks.onInputKeyDown}
|
onKeyDown={callbacks.onInputKeyDown}
|
||||||
onClick={callbacks.onClick}
|
onClick={callbacks.onClick}
|
||||||
onFocus={callbacks.onFocus}
|
onFocus={callbacks.onFocus}
|
||||||
ref={inputRef}
|
ref={mergeRefs(_refs)}
|
||||||
onMouseDown={(e: React.MouseEvent) => {
|
onMouseDown={(e: React.MouseEvent) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -16436,6 +16436,7 @@ fsevents@^1.2.7:
|
||||||
react-error-boundary: ^3.1.3
|
react-error-boundary: ^3.1.3
|
||||||
react-icons: ^4.2.0
|
react-icons: ^4.2.0
|
||||||
react-is: ^17.0.2
|
react-is: ^17.0.2
|
||||||
|
react-merge-refs: ^1.1.0
|
||||||
react-shadow: ^19.0.2
|
react-shadow: ^19.0.2
|
||||||
react-use: ^17.2.4
|
react-use: ^17.2.4
|
||||||
react-use-gesture: ^9.1.3
|
react-use-gesture: ^9.1.3
|
||||||
|
|
Loading…
Reference in a new issue