Files
sbox-public/game/editor/MovieMaker/Code/Modes/Keyframe/KeyframeHandle.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

240 lines
5.5 KiB
C#

using Sandbox.MovieMaker;
using System.Collections.Immutable;
using System.Linq;
using Editor.MovieMaker.BlockDisplays;
namespace Editor.MovieMaker;
#nullable enable
public sealed class KeyframeHandle : GraphicsItem, IComparable<KeyframeHandle>, ITrackItem, IMovieDraggable, IMovieContextMenu, ISnapSource
{
private Keyframe _keyframe;
private bool _isDragging;
public new TimelineTrack Parent { get; }
public Session Session { get; }
public TrackView View { get; }
public KeyframeEditMode? EditMode => Session.EditMode as KeyframeEditMode;
public Keyframe Keyframe
{
get => _keyframe;
set
{
if ( _keyframe == value ) return;
_keyframe = value;
ToolTip = $"{View.Track.Name} = {Keyframe.Value?.ToString() ?? "null"}";
UpdatePosition();
}
}
public MovieTime Time
{
get => Keyframe.Time;
set => Keyframe = Keyframe with { Time = value };
}
bool IMovieItem.MultiSelectable => true;
TimelineTrack ITrackItem.Track => Parent;
public KeyframeHandle( TimelineTrack parent, Keyframe keyframe )
: base( parent )
{
Parent = parent;
Session = parent.Session;
View = parent.View;
HandlePosition = new Vector2( 0.5f, 0f );
ZIndex = 100;
HoverEvents = true;
Focusable = true;
Selectable = true;
Cursor = CursorShape.Finger;
Keyframe = keyframe;
}
public void UpdatePosition()
{
PrepareGeometryChange();
Position = new Vector2( Session.TimeToPixels( Time ), 0f );
Size = new Vector2( 16f, Parent.Height );
Update();
}
protected override void OnSelectionChanged()
{
base.OnSelectionChanged();
UpdatePosition();
ZIndex = Selected ? 101 : 100;
}
protected override void OnPaint()
{
if ( View.IsLocked ) return;
Paint.ClearPen();
Paint.SetBrushRadial( LocalRect.Center, Width * 0.5f, Timeline.Colors.ChannelBackground, Color.Transparent );
Paint.DrawRect( LocalRect );
var c = PaintExtensions.PaintSelectColor( Parent.HandleColor.WithAlpha( 0.5f ), Parent.HandleColor, Timeline.Colors.HandleSelected );
Paint.SetBrushAndPen( c );
switch ( Keyframe.Interpolation )
{
case KeyframeInterpolation.Step:
Paint.DrawRect( new Rect( Size * 0.5f, 0f ).Grow( 4f ), 1f );
break;
case KeyframeInterpolation.Linear:
PaintExtensions.PaintTriangle( Size * 0.5f, 10 );
break;
case KeyframeInterpolation.Quadratic:
Paint.DrawCircle( Size * 0.5f, 8f );
break;
case KeyframeInterpolation.Cubic:
Paint.DrawCircle( Size * 0.5f, 6f );
Paint.ClearBrush();
Paint.SetPen( c );
Paint.DrawCircle( Size * 0.5f, 10f );
break;
}
Paint.SetPen( c.WithAlphaMultiplied( 0.3f ) );
Paint.DrawLine( new Vector2( Width * 0.5f, 0 ), new Vector2( Width * 0.5f, Height ) );
}
public void ShowContextMenu( EditMode.ContextMenuEvent ev )
{
if ( EditMode is not { } editMode ) return;
ev.Accepted = true;
editMode.Session.PlayheadTime = Keyframe.Time;
var selection = GraphicsView.SelectedItems
.OfType<KeyframeHandle>()
.ToImmutableArray();
ev.Menu.AddHeading( $"Selected Keyframe{(selection.Length > 1 ? "s" : "")}" );
CreateInterpolationMenu( selection, ev.Menu );
ev.Menu.AddHeading( "Clipboard" );
ev.Menu.AddOption( "Copy", "content_copy", () => editMode.Copy() );
ev.Menu.AddOption( "Cut", "content_cut", () => editMode.Cut() );
if ( GetOverlappingClipboard( selection ) is { } clipboard )
{
ev.Menu.AddOption( "Paste", "content_paste", () => editMode.Paste( clipboard, Time - clipboard.Time ) );
}
ev.Menu.AddOption( "Delete", "delete", () => editMode.Delete() );
}
private KeyframeEditMode.ClipboardData? GetOverlappingClipboard( IReadOnlyList<KeyframeHandle> selection )
{
if ( EditMode?.Clipboard is not { } clipboard ) return null;
if ( !selection.Any( x => clipboard.Keyframes.Any( y => y.Guid == x.View.Track.Id ) ) )
{
return null;
}
return clipboard;
}
private void CreateInterpolationMenu( IReadOnlyList<KeyframeHandle> selection, Menu parent )
{
var menu = parent.AddMenu( "Interpolation Mode", "gradient" );
var currentMode = selection.All( x => x.Keyframe.Interpolation == selection[0].Keyframe.Interpolation )
? selection[0].Keyframe.Interpolation
: KeyframeInterpolation.Unknown;
foreach ( var value in Enum.GetValues<KeyframeInterpolation>() )
{
if ( value < 0 ) continue;
var option = menu.AddOption( value.ToString().ToTitleCase(), action: () =>
{
foreach ( var handle in selection )
{
handle.Keyframe = handle.Keyframe with { Interpolation = value };
}
EditMode?.UpdateTracksFromHandles( selection );
} );
option.Checkable = true;
option.Checked = value == currentMode;
}
}
public int CompareTo( KeyframeHandle? other )
{
if ( ReferenceEquals( this, other ) )
{
return 0;
}
if ( other is null )
{
return 1;
}
var timeCompare = Time.CompareTo( other.Time );
if ( timeCompare != 0 )
{
return timeCompare;
}
// When overlapping, put selected first
return -Selected.CompareTo( other.Selected );
}
MovieTimeRange IMovieItem.TimeRange => Keyframe.Time;
void IMovieDraggable.StartDrag() => _isDragging = true;
void IMovieDraggable.Drag( MovieTime delta )
{
Time += delta;
}
void IMovieDraggable.EndDrag() => _isDragging = false;
public IEnumerable<SnapTarget> GetSnapTargets( MovieTime sourceTime, bool isPrimary ) => _isDragging ? [] : [Time];
public bool SnapFilter( ISnapSource source )
{
if ( source == this ) return false;
if ( source is not BlockItem block ) return true;
var view = View;
while ( view is not null )
{
if ( view == block.Parent.View ) return false;
view = view.Parent;
}
return true;
}
}