Editor/MovieMaker/Modes/Motion/MotionEditMode.cs
using System.Linq;
using Sandbox.MovieMaker;
namespace Editor.MovieMaker;
#nullable enable
[Title( "Motion Editor" ), Icon( "brush" ), Order( 1 )]
[Description( "Sculpt changes on selected time ranges. Ideal for tweaking recordings." )]
public sealed partial class MotionEditMode : EditMode
{
private TimeSelection? _timeSelection;
private bool _newTimeSelection;
public TimeSelection? TimeSelection
{
get => _timeSelection;
set
{
_timeSelection = value;
SelectionChanged();
}
}
public InterpolationMode DefaultInterpolation { get; private set; } = InterpolationMode.QuadraticInOut;
private MovieTime? _selectionStartTime;
protected override void OnEnable()
{
var saveSequenceDisplay = new ToolBarItemDisplay( "Save As Sequence..", "theaters",
"Save the time selection as a new movie project, and reference it in this timeline as a sequence block." );
var editGroup = ToolBar.AddGroup();
var insertDisplay = new ToolBarItemDisplay( "Insert", "keyboard_tab",
"Insert time inside the selected range, right-shifting track data after the insertion point." );
var removeDisplay = new ToolBarItemDisplay( "Remove", "backspace",
"Delete track data inside the selected range, left-shifting everything after the deletion." );
var clearDisplay = new ToolBarItemDisplay( "Clear", "delete",
"Delete track data inside the selected range, without shifting anything after the deletion. This will leave an empty range without track data." );
editGroup.AddAction( insertDisplay, Insert, () => TimeSelection is not null );
editGroup.AddAction( removeDisplay, () => Delete( true ), () => TimeSelection is not null );
editGroup.AddAction( clearDisplay, () => Delete( false ), () => TimeSelection is not null );
editGroup.AddAction( saveSequenceDisplay,
() => Session.Editor.SaveAsDialog( "Save As Sequence..", () => CreateSequence( TimeSelection!.Value.TotalTimeRange ) ),
() => TimeSelection is not null );
ToolBarGroup? customGroup = null;
var modificationTypes = EditorTypeLibrary
.GetTypesWithAttribute<MovieModificationAttribute>()
.OrderBy( x => x.Attribute.Order );
foreach ( var (type, attribute) in modificationTypes )
{
if ( type.IsAbstract || type.IsGenericType ) continue;
customGroup ??= ToolBar.AddGroup();
var display = new ToolBarItemDisplay( attribute.Title, attribute.Icon, attribute.Description );
var toggle = customGroup.AddToggle( display,
() => Modification?.GetType() == type.TargetType,
value =>
{
if ( value && TimeSelection is { } selection )
{
var modification = SetModification( type.TargetType, selection );
modification.Start( selection );
}
else if ( !value )
{
ClearChanges();
}
} );
toggle.Bind( nameof(IconButton.Enabled) )
.ReadOnly()
.From( () => TimeSelection is not null, (Action<bool>?)null );
}
var selectionGroup = ToolBar.AddGroup();
selectionGroup.AddInterpolationSelector( () => DefaultInterpolation, value =>
{
DefaultInterpolation = value;
if ( TimeSelection is { } timeSelection )
{
TimeSelection = timeSelection.WithInterpolation( value );
}
} );
SelectionChanged();
}
protected override void OnDisable()
{
ClearChanges();
TimeSelection = null;
}
protected override void OnMousePress( MouseEvent e )
{
if ( !e.LeftMouseButton ) return;
var scenePos = Timeline.ToScene( e.LocalPosition );
if ( Timeline.GetItemAt( scenePos ) is TimeSelectionItem && !e.HasShift ) return;
var time = Session.ScenePositionToTime( scenePos );
_selectionStartTime = Session.ScenePositionToTime( scenePos );
_newTimeSelection = false;
Session.PlayheadTime = time;
e.Accepted = true;
}
protected override void OnMouseMove( MouseEvent e )
{
if ( (e.ButtonState & MouseButtons.Left) == 0 ) return;
if ( _selectionStartTime is not { } dragStartTime ) return;
e.Accepted = true;
var time = Session.ScenePositionToTime( Timeline.ToScene( e.LocalPosition ), SnapFlag.Selection );
// Only create a time selection when mouse has moved enough
if ( time == dragStartTime && TimeSelection is null ) return;
var (minTime, maxTime) = Session.VisibleTimeRange;
if ( time < minTime ) time = MovieTime.Zero;
if ( time > maxTime ) time = Session.Project!.Duration;
TimeSelection = new TimeSelection( (MovieTime.Min( time, dragStartTime ), MovieTime.Max( time, dragStartTime )), DefaultInterpolation );
_newTimeSelection = true;
Session.PreviewTime = time;
}
protected override void OnMouseRelease( MouseEvent e )
{
if ( _selectionStartTime is null ) return;
_selectionStartTime = null;
if ( !_newTimeSelection ) return;
_newTimeSelection = false;
if ( TimeSelection is not { } selection ) return;
var timeRange = selection.PeakTimeRange.Clamp( Session.VisibleTimeRange );
Session.PlayheadTime = MovieTime.FromTicks( (timeRange.Start.Ticks + timeRange.End.Ticks) / 2 );
Session.PreviewTime = null;
}
protected override void OnMouseWheel( WheelEvent e )
{
if ( !e.HasShift ) return;
if ( TimeSelection is not { } selection || !selection.PeakTimeRange.Contains( Session.PlayheadTime ) )
{
selection = new TimeSelection( Session.PlayheadTime, DefaultInterpolation );
}
var delta = Math.Sign( e.Delta ) * Session.MinorTick.Interval;
TimeSelection = selection.WithFadeDurationDelta( delta );
e.Accept();
}
private void SetInterpolation( InterpolationMode mode )
{
DefaultInterpolation = mode;
if ( Timeline.GetItemAt( Timeline.ToScene( Timeline.FromScreen( Application.CursorPosition ) ) ) is TimeSelectionFadeItem fade )
{
fade.Interpolation = mode;
}
}
protected override void OnGetSnapTimes( ref TimeSnapHelper snapHelper )
{
if ( TimeSelection is not { } selection ) return;
snapHelper.Add( SnapFlag.SelectionTotalStart, selection.TotalStart );
snapHelper.Add( SnapFlag.SelectionPeakStart, selection.PeakStart );
snapHelper.Add( SnapFlag.SelectionPeakEnd, selection.PeakEnd );
snapHelper.Add( SnapFlag.SelectionTotalEnd, selection.TotalEnd );
}
protected override Color GetTrailColor( MovieTime time )
{
if ( TimeSelection is not { } selection ) return base.GetTrailColor( time );
return Color.Gray.LerpTo( SelectionColor, selection.GetFadeValue( time ) );
}
}