mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-05-24 06:46:26 -04:00
This commit imports the C# engine code and game files, excluding C++ source code. [Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
766 lines
16 KiB
C#
766 lines
16 KiB
C#
using System.Linq;
|
|
using Sandbox.MovieMaker;
|
|
|
|
namespace Editor.MovieMaker;
|
|
|
|
#nullable enable
|
|
|
|
public partial class Timeline : GraphicsView, ISnapSource
|
|
{
|
|
[FromTheme]
|
|
public static Color PlayheadColor { get; set; } = Theme.Yellow;
|
|
|
|
[FromTheme]
|
|
public static Color PreviewColor { get; set; } = Theme.Blue;
|
|
|
|
/// <summary>
|
|
/// Color outside the current movie's time range.
|
|
/// </summary>
|
|
[FromTheme]
|
|
public static Color BackgroundColor { get; set; } = Theme.ControlBackground;
|
|
|
|
/// <summary>
|
|
/// Color outside the current sequence block's time range.
|
|
/// </summary>
|
|
[FromTheme]
|
|
public static Color OuterColor { get; set; } = Theme.ControlBackground.LerpTo( Theme.WidgetBackground, 0.5f );
|
|
|
|
/// <summary>
|
|
/// Color inside the current sequence block's time range.
|
|
/// </summary>
|
|
[FromTheme]
|
|
public static Color InnerColor { get; set; } = Theme.WidgetBackground;
|
|
|
|
public const int TrackHeight = 32;
|
|
public const int BlockHeight = 30;
|
|
public const int RootTrackSpacing = 8;
|
|
|
|
public static class Colors
|
|
{
|
|
public static Color ChannelBackground => Theme.ControlBackground;
|
|
public static Color HandleSelected => Color.White;
|
|
}
|
|
|
|
public Session Session { get; }
|
|
|
|
private readonly BackgroundItem _backgroundItem;
|
|
private readonly GridItem _gridItem;
|
|
private readonly SynchronizedSet<TrackView, TimelineTrack> _tracks;
|
|
|
|
private readonly TimeCursor _playhead;
|
|
private readonly TimeCursor _preview;
|
|
|
|
public ScrubBar ScrubBarTop { get; }
|
|
public ScrubBar ScrubBarBottom { get; }
|
|
|
|
public IEnumerable<TimelineTrack> Tracks => _tracks;
|
|
|
|
public Rect VisibleRect
|
|
{
|
|
get
|
|
{
|
|
var screenRect = ScreenRect;
|
|
var topLeft = FromScreen( screenRect.TopLeft );
|
|
var bottomRight = FromScreen( screenRect.BottomRight );
|
|
|
|
return ToScene( new Rect( topLeft, bottomRight - topLeft ) );
|
|
}
|
|
}
|
|
|
|
private readonly SynchronizedSet<(MovieTime Time, Rect SceneRect), GraphicsItem> _snapTargets;
|
|
|
|
public Timeline( Session session )
|
|
{
|
|
Session = session;
|
|
MinimumWidth = 256;
|
|
|
|
_tracks = new SynchronizedSet<TrackView, TimelineTrack>(
|
|
AddTrack, RemoveTrack, UpdateTrack );
|
|
|
|
_backgroundItem = new BackgroundItem( Session );
|
|
Add( _backgroundItem );
|
|
|
|
_gridItem = new GridItem( Session );
|
|
Add( _gridItem );
|
|
|
|
_playhead = new TimeCursor( this, PlayheadColor, true ) { LabelFadeTime = 1f };
|
|
Add( _playhead );
|
|
|
|
_preview = new TimeCursor( this, PreviewColor, false );
|
|
Add( _preview );
|
|
|
|
ScrubBarTop = new ScrubBar( Session.Editor, true ) { Width = Width };
|
|
Add( ScrubBarTop );
|
|
ScrubBarBottom = new ScrubBar( Session.Editor, false ) { Width = Width };
|
|
Add( ScrubBarBottom );
|
|
|
|
_snapTargets = new(
|
|
addFunc: _ =>
|
|
{
|
|
var item = new SnapTargetItem();
|
|
Add( item );
|
|
return item;
|
|
},
|
|
removeAction: item => item.Destroy(),
|
|
updateAction: ( target, item ) =>
|
|
{
|
|
var x = session.TimeToPixels( target.Time );
|
|
|
|
item.Position = new Vector2( x, target.SceneRect.Top );
|
|
item.Height = target.SceneRect.Height;
|
|
|
|
return true;
|
|
} );
|
|
|
|
Session.PlayheadChanged += UpdatePlayheadTime;
|
|
Session.PreviewChanged += UpdatePreviewTime;
|
|
Session.ViewChanged += UpdateView;
|
|
|
|
FocusMode = FocusMode.TabOrClickOrWheel;
|
|
|
|
AcceptDrops = true;
|
|
|
|
var bg = new Pixmap( 8 );
|
|
bg.Clear( BackgroundColor );
|
|
|
|
SetBackgroundImage( bg );
|
|
|
|
Antialiasing = true;
|
|
}
|
|
|
|
public override void OnDestroyed()
|
|
{
|
|
DeleteAllItems();
|
|
|
|
Session.PlayheadChanged -= UpdatePlayheadTime;
|
|
Session.PreviewChanged -= UpdatePreviewTime;
|
|
Session.ViewChanged -= UpdateView;
|
|
}
|
|
|
|
private int _lastState;
|
|
private int _lastVisibleRectHash;
|
|
|
|
[EditorEvent.Frame]
|
|
public void Frame()
|
|
{
|
|
_backgroundItem.Frame();
|
|
|
|
ScrubBarTop.Frame();
|
|
ScrubBarBottom.Frame();
|
|
|
|
UpdateScrubBars();
|
|
UpdateTracksIfNeeded();
|
|
|
|
var visibleRectHash = HashCode.Combine( VisibleRect, Session.PixelsPerSecond );
|
|
|
|
if ( visibleRectHash != _lastVisibleRectHash )
|
|
{
|
|
if ( (Application.KeyboardModifiers & KeyboardModifiers.Shift) != 0 && IsFocused )
|
|
{
|
|
UpdatePreviewTime( ToScene( _lastMouseLocalPos ) );
|
|
}
|
|
}
|
|
|
|
_lastVisibleRectHash = visibleRectHash;
|
|
|
|
if ( Session.PreviewTime is not null
|
|
&& (Application.KeyboardModifiers & KeyboardModifiers.Shift) == 0
|
|
&& (Application.MouseButtons & MouseButtons.Left) == 0 )
|
|
{
|
|
Session.PreviewTime = null;
|
|
}
|
|
}
|
|
|
|
private void UpdateTracksIfNeeded()
|
|
{
|
|
var state = HashCode.Combine( Session.PixelsPerSecond, Session.TimeOffset, Session.FrameRate, Session.TrackList.StateHash );
|
|
|
|
if ( state == _lastState ) return;
|
|
|
|
_lastState = state;
|
|
|
|
UpdateTracks();
|
|
Update();
|
|
}
|
|
|
|
private void UpdateView()
|
|
{
|
|
UpdateSceneFrame();
|
|
UpdateScrubBars();
|
|
|
|
UpdatePlayheadTime( Session.PlayheadTime );
|
|
UpdatePreviewTime( Session.PreviewTime );
|
|
|
|
UpdateTracksIfNeeded();
|
|
}
|
|
|
|
private void UpdateScrubBars()
|
|
{
|
|
_backgroundItem.Update();
|
|
|
|
ScrubBarTop.PrepareGeometryChange();
|
|
ScrubBarBottom.PrepareGeometryChange();
|
|
|
|
var visibleRect = VisibleRect;
|
|
|
|
ScrubBarTop.Position = visibleRect.TopLeft;
|
|
ScrubBarBottom.Position = visibleRect.BottomLeft - new Vector2( 0f, ScrubBar.Height );
|
|
|
|
ScrubBarTop.Width = Width;
|
|
ScrubBarBottom.Width = Width;
|
|
}
|
|
|
|
protected override void OnResize()
|
|
{
|
|
base.OnResize();
|
|
|
|
UpdateScrubBars();
|
|
UpdateTracks();
|
|
}
|
|
|
|
private void UpdatePlayheadTime( MovieTime time )
|
|
{
|
|
_playhead.Value = time;
|
|
}
|
|
|
|
private void UpdatePreviewTime( MovieTime? time )
|
|
{
|
|
_preview.PrepareGeometryChange();
|
|
|
|
if ( time is { } t )
|
|
{
|
|
_preview.Value = t;
|
|
}
|
|
else
|
|
{
|
|
_preview.PrepareGeometryChange();
|
|
_preview.Position = new Vector2( -50000f, 0f );
|
|
}
|
|
}
|
|
|
|
void UpdateSceneFrame()
|
|
{
|
|
const float leftMargin = 8f;
|
|
|
|
Session.TrackListViewHeight = Height - 64f;
|
|
|
|
var x = Session.TimeToPixels( Session.TimeOffset );
|
|
|
|
SceneRect = new Rect(
|
|
x - leftMargin,
|
|
Session.TrackListScrollPosition - Session.TrackListScrollOffset,
|
|
Width,
|
|
Height );
|
|
|
|
_backgroundItem.PrepareGeometryChange();
|
|
_backgroundItem.SceneRect = SceneRect;
|
|
_backgroundItem.Update();
|
|
|
|
_gridItem.PrepareGeometryChange();
|
|
_gridItem.SceneRect = SceneRect with { Left = SceneRect.Left + leftMargin };
|
|
_gridItem.Update();
|
|
|
|
UpdatePlayheadTime( Session.PlayheadTime );
|
|
UpdatePreviewTime( Session.PreviewTime );
|
|
}
|
|
|
|
public void UpdateTracks()
|
|
{
|
|
UpdateSceneFrame();
|
|
|
|
_tracks.Update( Session.TrackList.VisibleTracks );
|
|
|
|
Update();
|
|
}
|
|
|
|
private TimelineTrack AddTrack( TrackView source )
|
|
{
|
|
var item = new TimelineTrack( this, source );
|
|
|
|
Add( item );
|
|
|
|
return item;
|
|
}
|
|
|
|
private void RemoveTrack( TimelineTrack item ) => item.Destroy();
|
|
private bool UpdateTrack( TrackView source, TimelineTrack item )
|
|
{
|
|
item.UpdateLayout();
|
|
|
|
return true;
|
|
}
|
|
|
|
protected override void OnWheel( WheelEvent e )
|
|
{
|
|
base.OnWheel( e );
|
|
|
|
Session.EditMode?.MouseWheel( e );
|
|
|
|
if ( e.Accepted ) return;
|
|
|
|
// scoll
|
|
if ( e.HasShift )
|
|
{
|
|
Session.ScrollBySmooth( -e.Delta / 10.0f * (Session.PixelsPerSecond / 10.0f) );
|
|
Session.DispatchViewChanged();
|
|
|
|
e.Accept();
|
|
return;
|
|
}
|
|
|
|
// zoom
|
|
if ( e.HasCtrl )
|
|
{
|
|
Session.Zoom( e.Delta / 10.0f, _lastMouseTime );
|
|
e.Accept();
|
|
return;
|
|
}
|
|
|
|
// scrub
|
|
if ( e.HasAlt )
|
|
{
|
|
var dt = MovieTime.FromFrames( 1, Session.FrameRate );
|
|
var nextTime = Session.PlayheadTime.Round( dt ) + Math.Sign( e.Delta ) * dt;
|
|
|
|
Session.PlayheadTime = nextTime;
|
|
Session.ScrollToPlayheadTime();
|
|
|
|
e.Accept();
|
|
return;
|
|
}
|
|
|
|
Session.TrackListScrollPosition -= e.Delta / 5f;
|
|
e.Accept();
|
|
}
|
|
|
|
private Vector2 _lastMouseLocalPos;
|
|
private MovieTime _lastMouseTime;
|
|
|
|
public Vector2 MouseScenePos => ToScene( _lastMouseLocalPos );
|
|
|
|
protected override void OnMouseMove( MouseEvent e )
|
|
{
|
|
base.OnMouseMove( e );
|
|
|
|
var delta = e.LocalPosition - _lastMouseLocalPos;
|
|
var scenePos = ToScene( e.LocalPosition );
|
|
|
|
if ( e.HasShift )
|
|
{
|
|
UpdatePreviewTime( scenePos );
|
|
}
|
|
|
|
if ( e.ButtonState == MouseButtons.Left && IsDragging )
|
|
{
|
|
Drag( ToScene( e.LocalPosition ) );
|
|
e.Accepted = true;
|
|
return;
|
|
}
|
|
|
|
if ( e.ButtonState == 0 && !e.HasCtrl && GetItemAt( scenePos ) is { Selectable: true } item )
|
|
{
|
|
UpdateCursor( scenePos, item );
|
|
return;
|
|
}
|
|
|
|
if ( e.ButtonState == MouseButtons.Middle )
|
|
{
|
|
Session.ScrollByImmediate( delta.x );
|
|
Session.DispatchViewChanged();
|
|
}
|
|
|
|
if ( e.ButtonState == MouseButtons.Right )
|
|
{
|
|
ScrubBarTop.Scrub( e.KeyboardModifiers, scenePos );
|
|
}
|
|
|
|
_lastMouseLocalPos = e.LocalPosition;
|
|
_lastMouseTime = Session.PixelsToTime( ToScene( e.LocalPosition ).x );
|
|
|
|
Session.EditMode?.MouseMove( e );
|
|
}
|
|
|
|
public void UpdatePreviewTime( Vector2 scenePos )
|
|
{
|
|
Session.PreviewTime = Application.MouseButtons != 0
|
|
? Session.ScenePositionToTime( scenePos )
|
|
: Session.PixelsToTime( scenePos.x );
|
|
}
|
|
|
|
public new GraphicsItem? GetItemAt( Vector2 scenePosition )
|
|
{
|
|
// TODO: Is there a nicer way?
|
|
|
|
const int zIndexOffset = 100_000;
|
|
|
|
var nonSelectables = new HashSet<GraphicsItem>();
|
|
|
|
try
|
|
{
|
|
while ( true )
|
|
{
|
|
var item = base.GetItemAt( scenePosition );
|
|
|
|
if ( item is { Selectable: true } or null )
|
|
{
|
|
return item;
|
|
}
|
|
|
|
// If we found something unselectable, move it backwards
|
|
|
|
if ( nonSelectables.Add( item ) )
|
|
{
|
|
item.ZIndex -= zIndexOffset;
|
|
}
|
|
else
|
|
{
|
|
// There was nothing selectable, so we hit something unselectable twice!
|
|
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
// Move unselectables back to their original z-index.
|
|
|
|
foreach ( var item in nonSelectables )
|
|
{
|
|
item.ZIndex += zIndexOffset;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void UpdateSnapTargets( IEnumerable<(MovieTime Time, Rect SceneRect)> targets )
|
|
{
|
|
_snapTargets.Update( targets );
|
|
}
|
|
|
|
private MovieTime? _contextMenuTime;
|
|
private TimeSince _sinceClick;
|
|
private IMovieItem? _lastClickedItem;
|
|
|
|
private const float DoubleClickTime = 0.5f;
|
|
|
|
protected override void OnMousePress( MouseEvent e )
|
|
{
|
|
base.OnMousePress( e );
|
|
|
|
DragType = DragTypes.None;
|
|
_contextMenuTime = null;
|
|
|
|
if ( e.ButtonState == MouseButtons.Middle )
|
|
{
|
|
e.Accepted = true;
|
|
return;
|
|
}
|
|
|
|
var scenePos = ToScene( e.LocalPosition );
|
|
var time = Session.ScenePositionToTime( scenePos, new SnapOptions( source => source is not TimeCursor ) );
|
|
|
|
var accepted = false;
|
|
|
|
// Look for an item we clicked on. If shift is pressed, let us drag a time selection instead
|
|
|
|
if ( !e.HasShift && GetItemAt( scenePos ) is { Selectable: true } item and IMovieItem movieItem )
|
|
{
|
|
accepted = true;
|
|
|
|
// We're handling selection manually so we can multi-select etc. This means we
|
|
// need to Accept the event so the default selection logic doesn't run.
|
|
|
|
// If the item wants to handle the event itself, we don't set Accepted here,
|
|
// and assume it handles the selection logic itself.
|
|
|
|
if ( !movieItem.OverridesMouseEvents )
|
|
{
|
|
e.Accepted = true;
|
|
}
|
|
|
|
if ( !item.Selected )
|
|
{
|
|
// Ctrl multi-selects, if possible
|
|
|
|
foreach ( var selected in SelectedItems.ToArray() )
|
|
{
|
|
if ( selected is not IMovieItem { MultiSelectable: true } )
|
|
{
|
|
selected.Selected = false;
|
|
}
|
|
}
|
|
|
|
if ( !movieItem.MultiSelectable || !e.HasCtrl )
|
|
{
|
|
DeselectAll();
|
|
}
|
|
|
|
item.Selected = true;
|
|
|
|
if ( item is ITrackItem trackItem )
|
|
{
|
|
trackItem.Track.View.Select();
|
|
}
|
|
}
|
|
|
|
// Move the playhead to be within whatever we clicked on
|
|
|
|
if ( movieItem.MovePlayheadOnSelect || e.RightMouseButton )
|
|
{
|
|
time = time.Clamp( movieItem.TimeRange );
|
|
Session.PlayheadTime = time;
|
|
}
|
|
|
|
if ( e.LeftMouseButton )
|
|
{
|
|
// If we've clicked on something draggable, start dragging!
|
|
|
|
if ( StartDragging( scenePos, item ) ) return;
|
|
}
|
|
}
|
|
|
|
if ( !accepted )
|
|
{
|
|
Session.EditMode?.MousePress( e );
|
|
}
|
|
|
|
if ( e.LeftMouseButton )
|
|
{
|
|
if ( accepted ) return;
|
|
|
|
DragType = DragTypes.SelectionRect;
|
|
return;
|
|
}
|
|
|
|
if ( e.RightMouseButton )
|
|
{
|
|
_contextMenuTime = time;
|
|
|
|
if ( accepted ) return;
|
|
|
|
ScrubBarTop.StartScrubbing( time, e.KeyboardModifiers );
|
|
Session.PlayheadTime = time;
|
|
return;
|
|
}
|
|
}
|
|
|
|
protected override void OnMouseReleased( MouseEvent e )
|
|
{
|
|
base.OnMouseReleased( e );
|
|
|
|
UpdateSnapTargets( [] );
|
|
|
|
var scenePos = ToScene( e.LocalPosition );
|
|
var time = Session.ScenePositionToTime( scenePos, new SnapOptions( source => source is not TimeCursor ), showSnap: false );
|
|
|
|
// Detect double-click
|
|
|
|
if ( e.LeftMouseButton && !e.HasShift && !e.HasCtrl )
|
|
{
|
|
var item = GetItemAt( scenePos ) as IMovieItem;
|
|
|
|
if ( item is not null && _sinceClick < DoubleClickTime && item == _lastClickedItem )
|
|
{
|
|
e.Accepted = true;
|
|
|
|
item.DoubleClick();
|
|
|
|
_sinceClick = float.PositiveInfinity;
|
|
_lastClickedItem = null;
|
|
return;
|
|
}
|
|
|
|
_sinceClick = 0f;
|
|
_lastClickedItem = item;
|
|
}
|
|
|
|
// Don't open context menu if we right-click + drag
|
|
|
|
if ( e.RightMouseButton && time == _contextMenuTime )
|
|
{
|
|
e.Accepted = true;
|
|
|
|
OpenContextMenu( scenePos, time );
|
|
return;
|
|
}
|
|
|
|
ScrubBarTop.StopScrubbing();
|
|
|
|
if ( IsDragging )
|
|
{
|
|
StopDragging();
|
|
return;
|
|
}
|
|
|
|
Session.EditMode?.MouseRelease( e );
|
|
}
|
|
|
|
public void OpenContextMenu( Vector2 scenePos, MovieTime time )
|
|
{
|
|
var menu = new Menu();
|
|
var timelineTrack = Tracks.FirstOrDefault( x => x.SceneRect.IsInside( scenePos ) );
|
|
var titleLabel = menu.AddHeading( time.ToString() );
|
|
|
|
Session.CreateImportMenu( menu, time );
|
|
|
|
var ev = new EditMode.ContextMenuEvent( scenePos, time, timelineTrack, menu, titleLabel );
|
|
|
|
if ( GetItemAt( scenePos ) is { } item and IMovieContextMenu ctxMenuItem )
|
|
{
|
|
if ( !item.Selected )
|
|
{
|
|
item.Selected = true;
|
|
}
|
|
|
|
ctxMenuItem.ShowContextMenu( ev );
|
|
}
|
|
|
|
if ( !ev.Accepted )
|
|
{
|
|
Session.EditMode?.ContextMenu( ev );
|
|
}
|
|
|
|
menu.OpenAtCursor();
|
|
}
|
|
|
|
public void DeselectAll()
|
|
{
|
|
Session.TrackList.DeselectAll();
|
|
|
|
foreach ( var item in SelectedItems.ToArray() )
|
|
{
|
|
if ( !item.IsValid() ) continue;
|
|
|
|
item.Selected = false;
|
|
}
|
|
}
|
|
|
|
protected override void OnKeyPress( KeyEvent e )
|
|
{
|
|
base.OnKeyPress( e );
|
|
|
|
Session.EditMode?.KeyPress( e );
|
|
|
|
if ( e.Accepted ) return;
|
|
|
|
if ( e.Key == KeyCode.Shift )
|
|
{
|
|
e.Accepted = true;
|
|
Session.PreviewTime = Session.ScenePositionToTime( ToScene( _lastMouseLocalPos ) );
|
|
}
|
|
}
|
|
|
|
protected override void OnKeyRelease( KeyEvent e )
|
|
{
|
|
base.OnKeyRelease( e );
|
|
|
|
Session.EditMode?.KeyRelease( e );
|
|
}
|
|
|
|
private MovieResource? GetDraggedClip( DragData data )
|
|
{
|
|
if ( data.Assets.FirstOrDefault( x => x.AssetPath?.EndsWith( ".movie" ) ?? false ) is not { } assetData )
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var assetTask = assetData.GetAssetAsync();
|
|
|
|
if ( !assetTask.IsCompleted ) return null;
|
|
if ( assetTask.Result?.LoadResource<MovieResource>() is not { } resource ) return null;
|
|
|
|
if ( !Session.CanReferenceMovie( resource ) ) return null;
|
|
|
|
return resource;
|
|
}
|
|
|
|
private ProjectSequenceTrack? _draggedTrack;
|
|
private ProjectSequenceBlock? _draggedBlock;
|
|
private readonly HashSet<ITrackBlock> _draggedBlocks = new();
|
|
|
|
public override void OnDragHover( DragEvent ev )
|
|
{
|
|
if ( _draggedBlock is null || _draggedTrack is null )
|
|
{
|
|
if ( GetDraggedClip( ev.Data ) is not { } resource )
|
|
{
|
|
ev.Action = DropAction.Ignore;
|
|
return;
|
|
}
|
|
|
|
var clip = resource.GetCompiled();
|
|
|
|
_draggedTrack = Session.GetOrCreateTrack( resource );
|
|
_draggedBlock = _draggedTrack.AddBlock( (0d, clip.Duration), default, resource );
|
|
|
|
Session.TrackList.Update();
|
|
UpdateTracksIfNeeded();
|
|
}
|
|
|
|
_draggedBlocks.Clear();
|
|
_draggedBlocks.Add( _draggedBlock );
|
|
|
|
var time = Session.ScenePositionToTime( ToScene( ev.LocalPosition ) );
|
|
|
|
_draggedBlock.TimeRange = (time, time + _draggedBlock.TimeRange.Duration);
|
|
_draggedBlock.Transform = new MovieTransform( time );
|
|
|
|
Session.TrackList.Find( _draggedTrack )?.MarkValueChanged();
|
|
|
|
ev.Action = DropAction.Link;
|
|
}
|
|
|
|
public override void OnDragLeave()
|
|
{
|
|
base.OnDragLeave();
|
|
|
|
if ( _draggedBlock is { } block && _draggedTrack is { } track )
|
|
{
|
|
track.RemoveBlock( block );
|
|
|
|
if ( track.IsEmpty )
|
|
{
|
|
track.Remove();
|
|
}
|
|
|
|
_draggedTrack = null;
|
|
_draggedBlock = null;
|
|
}
|
|
}
|
|
|
|
public override void OnDragDrop( DragEvent ev )
|
|
{
|
|
if ( GetDraggedClip( ev.Data ) is not { } movie )
|
|
{
|
|
return;
|
|
}
|
|
|
|
_draggedTrack = null;
|
|
_draggedBlock = null;
|
|
}
|
|
|
|
Rect ISnapSource.SceneSnapBounds => SceneRect;
|
|
IEnumerable<SnapTarget> ISnapSource.GetSnapTargets( MovieTime sourceTime, bool isPrimary )
|
|
{
|
|
yield return MovieTime.Zero;
|
|
|
|
if ( !isPrimary || !Session.FrameSnap ) yield break;
|
|
|
|
yield return new SnapTarget( sourceTime.Round( MovieTime.FromFrames( 1, Session.FrameRate ) ), -3, false );
|
|
}
|
|
}
|
|
|
|
file sealed class SnapTargetItem : GraphicsItem
|
|
{
|
|
public SnapTargetItem()
|
|
{
|
|
Width = 1f;
|
|
ZIndex = 50_000;
|
|
}
|
|
|
|
protected override void OnPaint()
|
|
{
|
|
Paint.SetPen( Color.White.WithAlpha( 0.75f ), style: PenStyle.Dash );
|
|
Paint.DrawLine( LocalRect.TopLeft, LocalRect.BottomLeft );
|
|
}
|
|
}
|