Files
FlaxEngine/Source/Editor/GUI/Timeline/Media.cs
2023-01-10 15:29:37 +01:00

572 lines
19 KiB
C#

// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
using System;
using FlaxEditor.GUI.Timeline.Undo;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.GUI.Timeline
{
/// <summary>
/// Timeline track media event (range-based). Can be added to the timeline track.
/// </summary>
/// <seealso cref="FlaxEngine.GUI.ContainerControl" />
public abstract class Media : ContainerControl
{
/// <summary>
/// The base class for media properties proxy objects.
/// </summary>
/// <typeparam name="TTrack">The type of the track.</typeparam>
/// <typeparam name="TMedia">The type of the media.</typeparam>
public abstract class ProxyBase<TTrack, TMedia>
where TTrack : Track
where TMedia : Media
{
/// <summary>
/// The track reference/
/// </summary>
[HideInEditor, NoSerialize]
public TTrack Track;
/// <summary>
/// The media reference.
/// </summary>
[HideInEditor, NoSerialize]
public TMedia Media;
/// <summary>
/// Gets or sets the start frame of the media event (in frames).
/// </summary>
[EditorDisplay("General"), EditorOrder(-10010), VisibleIf(nameof(UseFrames)), Tooltip("Start frame of the media event (in frames).")]
public int StartFrame
{
get => Media.StartFrame;
set => Media.StartFrame = value;
}
/// <summary>
/// Gets or sets the start frame of the media event (in seconds).
/// </summary>
[EditorDisplay("General"), EditorOrder(-10010), VisibleIf(nameof(UseFrames), true), Tooltip("Start frame of the media event (in seconds).")]
public float Start
{
get => Media.Start;
set => Media.Start = value;
}
/// <summary>
/// Gets or sets the total duration of the media event (in frames).
/// </summary>
[EditorDisplay("General"), EditorOrder(-1000), Limit(1), VisibleIf(nameof(UseFrames)), Tooltip("Total duration of the media event (in frames).")]
public int DurationFrames
{
get => Media.DurationFrames;
set
{
if (Media.CanResize)
Media.DurationFrames = value;
}
}
/// <summary>
/// Gets or sets the total duration of the timeline (in seconds).
/// </summary>
[EditorDisplay("General"), EditorOrder(-1000), Limit(0.0f, float.MaxValue, 0.001f), VisibleIf(nameof(UseFrames), true), Tooltip("Total duration of the timeline (in seconds).")]
public float Duration
{
get => Media.Duration;
set
{
if (Media.CanResize)
Media.Duration = value;
}
}
private bool UseFrames => Media.Timeline.TimeShowMode == Timeline.TimeShowModes.Frames;
/// <summary>
/// Initializes a new instance of the <see cref="ProxyBase{TTrack,TMedia}"/> class.
/// </summary>
/// <param name="track">The track.</param>
/// <param name="media">The media.</param>
protected ProxyBase(TTrack track, TMedia media)
{
Track = track;
Media = media ?? throw new ArgumentNullException(nameof(media));
}
}
private Timeline _timeline;
private Track _tack;
private int _startFrame, _durationFrames;
private Float2 _mouseLocation = Float2.Minimum;
private bool _isMoving;
private Float2 _startMoveLocation;
private int _startMoveStartFrame;
private int _startMoveDuration;
private bool _startMoveLeftEdge;
private bool _startMoveRightEdge;
/// <summary>
/// Gets or sets the start frame of the media event.
/// </summary>
public int StartFrame
{
get => _startFrame;
set
{
if (_startFrame == value)
return;
_startFrame = value;
if (_timeline != null)
{
OnTimelineZoomChanged();
}
OnStartFrameChanged();
}
}
/// <summary>
/// Occurs when start frame gets changed.
/// </summary>
public event Action StartFrameChanged;
/// <summary>
/// Gets the end frame of the media (start + duration).
/// </summary>
public int EndFrame => _startFrame + _durationFrames;
/// <summary>
/// Gets or sets the total duration of the media event in the timeline sequence frames amount.
/// </summary>
public int DurationFrames
{
get => _durationFrames;
set
{
value = Math.Max(value, 1);
if (_durationFrames == value)
return;
_durationFrames = value;
if (_timeline != null)
{
OnTimelineZoomChanged();
}
OnDurationFramesChanged();
}
}
/// <summary>
/// Occurs when media duration gets changed.
/// </summary>
public event Action DurationFramesChanged;
/// <summary>
/// Get or sets the media start time in seconds.
/// </summary>
/// <seealso cref="StartFrame"/>
public float Start
{
get => _startFrame / _timeline.FramesPerSecond;
set => StartFrame = (int)(value * _timeline.FramesPerSecond);
}
/// <summary>
/// Get or sets the media duration in seconds.
/// </summary>
/// <seealso cref="DurationFrames"/>
public float Duration
{
get => _durationFrames / _timeline.FramesPerSecond;
set => DurationFrames = (int)(value * _timeline.FramesPerSecond);
}
/// <summary>
/// Gets the parent timeline.
/// </summary>
public Timeline Timeline => _timeline;
/// <summary>
/// Gets the track.
/// </summary>
public Track Track => _tack;
private Rectangle MoveLeftEdgeRect => new Rectangle(-5, -5, 10, Height + 10);
private Rectangle MoveRightEdgeRect => new Rectangle(Width - 5, -5, 10, Height + 10);
/// <summary>
/// The track properties editing proxy object. Assign it to add media properties editing support.
/// </summary>
public object PropertiesEditObject;
/// <summary>
/// Gets a value indicating whether this media can be split.
/// </summary>
public bool CanSplit;
/// <summary>
/// Gets a value indicating whether this media can be removed.
/// </summary>
public bool CanDelete;
/// <summary>
/// Gets a value indicating whether this media can be resized (duration changed).
/// </summary>
public bool CanResize = true;
/// <summary>
/// Initializes a new instance of the <see cref="Media"/> class.
/// </summary>
protected Media()
{
AutoFocus = false;
}
/// <summary>
/// Called when showing timeline context menu to the user. Can be used to add custom buttons.
/// </summary>
/// <param name="menu">The menu.</param>
/// <param name="time">The time (in seconds) at which context menu is shown (user clicked on a timeline).</param>
/// <param name="controlUnderMouse">The found control under the mouse cursor.</param>
public virtual void OnTimelineContextMenu(ContextMenu.ContextMenu menu, float time, Control controlUnderMouse)
{
if (CanDelete && Track.Media.Count > Track.MinMediaCount)
menu.AddButton("Delete media", Delete);
}
/// <summary>
/// Called when parent track gets changed.
/// </summary>
/// <param name="track">The track.</param>
public virtual void OnTimelineChanged(Track track)
{
_timeline = track?.Timeline;
_tack = track;
Parent = _timeline?.MediaPanel;
if (_timeline != null)
{
OnTimelineZoomChanged();
}
}
/// <summary>
/// Called when timeline FPS gets changed.
/// </summary>
/// <param name="before">The before value.</param>
/// <param name="after">The after value.</param>
public virtual void OnTimelineFpsChanged(float before, float after)
{
StartFrame = (int)((_startFrame / before) * after);
DurationFrames = (int)((_durationFrames / before) * after);
}
/// <summary>
/// Called when undo action gets reverted or applied for this media parent track data. Can be used to update UI.
/// </summary>
public virtual void OnUndo()
{
}
/// <summary>
/// Called when media gets removed by the user.
/// </summary>
public virtual void OnDeleted()
{
Dispose();
}
/// <summary>
/// Called when media start frame gets changed.
/// </summary>
protected virtual void OnStartFrameChanged()
{
StartFrameChanged?.Invoke();
}
/// <summary>
/// Called when media duration in frames gets changed.
/// </summary>
protected virtual void OnDurationFramesChanged()
{
DurationFramesChanged?.Invoke();
}
/// <summary>
/// Called when timeline zoom gets changed.
/// </summary>
public virtual void OnTimelineZoomChanged()
{
X = Start * Timeline.UnitsPerSecond * _timeline.Zoom + Timeline.StartOffset;
Width = Duration * Timeline.UnitsPerSecond * _timeline.Zoom;
}
/// <summary>
/// Splits the media at the specified frame.
/// </summary>
/// <param name="frame">The frame to split at.</param>
/// <returns>The another media created after this media split.</returns>
public virtual Media Split(int frame)
{
var clone = (Media)Activator.CreateInstance(GetType());
clone.StartFrame = frame;
clone.DurationFrames = EndFrame - frame;
DurationFrames = DurationFrames - clone.DurationFrames;
Track?.AddMedia(clone);
Timeline?.MarkAsEdited();
return clone;
}
/// <summary>
/// Deletes this media.
/// </summary>
public void Delete()
{
_timeline.Delete(this);
}
/// <inheritdoc />
public override void GetDesireClientArea(out Rectangle rect)
{
base.GetDesireClientArea(out rect);
// Add some margin to allow to drag media edges
rect = rect.MakeExpanded(-6.0f);
}
/// <inheritdoc />
public override void Draw()
{
var style = Style.Current;
var bounds = new Rectangle(Float2.Zero, Size);
var fillColor = BackgroundColor.A > 0.0f ? BackgroundColor : style.Background * 1.5f;
Render2D.FillRectangle(bounds, fillColor);
var isMovingWholeMedia = _isMoving && !_startMoveRightEdge && !_startMoveLeftEdge;
var borderHighlightColor = style.BorderHighlighted;
var moveColor = style.ProgressNormal;
var selectedColor = style.BackgroundSelected;
var moveThickness = 2.0f;
var borderColor = isMovingWholeMedia ? moveColor : (Timeline.SelectedMedia.Contains(this) ? selectedColor : (IsMouseOver ? borderHighlightColor : style.BorderNormal));
Render2D.DrawRectangle(bounds, borderColor, isMovingWholeMedia ? moveThickness : 1.0f);
if (_startMoveLeftEdge)
{
Render2D.DrawLine(bounds.UpperLeft, bounds.BottomLeft, moveColor, moveThickness);
}
else if (IsMouseOver && CanResize && MoveLeftEdgeRect.Contains(ref _mouseLocation))
{
Render2D.DrawLine(bounds.UpperLeft, bounds.BottomLeft, Color.Yellow);
}
if (_startMoveRightEdge)
{
Render2D.DrawLine(bounds.UpperRight, bounds.BottomRight, moveColor, moveThickness);
}
else if (IsMouseOver && CanResize && MoveRightEdgeRect.Contains(ref _mouseLocation))
{
Render2D.DrawLine(bounds.UpperRight, bounds.BottomRight, Color.Yellow);
}
DrawChildren();
}
/// <inheritdoc />
public override bool OnMouseDown(Float2 location, MouseButton button)
{
if (base.OnMouseDown(location, button))
return true;
if (button == MouseButton.Left)
{
_isMoving = true;
_startMoveLocation = Root.MousePosition;
_startMoveStartFrame = StartFrame;
_startMoveDuration = DurationFrames;
_startMoveLeftEdge = MoveLeftEdgeRect.Contains(ref location) && CanResize;
_startMoveRightEdge = MoveRightEdgeRect.Contains(ref location) && CanResize;
StartMouseCapture(true);
if (_startMoveLeftEdge || _startMoveRightEdge)
return true;
if (Root.GetKey(KeyboardKeys.Control))
{
// Add/Remove selection
if (_timeline.SelectedMedia.Contains(this))
_timeline.Deselect(this);
else
_timeline.Select(this, true);
}
else
{
// Select
if (!_timeline.SelectedMedia.Contains(this))
_timeline.Select(this);
}
_timeline.OnKeyframesMove(null, this, location, true, false);
return true;
}
return false;
}
/// <inheritdoc />
public override void OnMouseMove(Float2 location)
{
_mouseLocation = location;
if (_isMoving)
{
var moveLocation = Root.MousePosition;
var moveLocationDelta = moveLocation - _startMoveLocation;
var moveDelta = (int)(moveLocationDelta.X / (Timeline.UnitsPerSecond * _timeline.Zoom) * _timeline.FramesPerSecond);
var startFrame = StartFrame;
var durationFrames = DurationFrames;
if (_startMoveLeftEdge)
{
StartFrame = _startMoveStartFrame + moveDelta;
DurationFrames = _startMoveDuration - moveDelta;
}
else if (_startMoveRightEdge)
{
DurationFrames = _startMoveDuration + moveDelta;
}
else
{
// Move with global timeline selection
_timeline.OnKeyframesMove(null, this, location, false, false);
}
if (StartFrame != startFrame || DurationFrames != durationFrames)
{
_timeline.MarkAsEdited();
}
}
else
{
base.OnMouseMove(location);
}
}
/// <inheritdoc />
public override bool OnMouseUp(Float2 location, MouseButton button)
{
if (button == MouseButton.Left && _isMoving)
{
if (!_startMoveLeftEdge && !_startMoveRightEdge && !Root.GetKey(KeyboardKeys.Control))
{
var moveLocationDelta = Root.MousePosition - _startMoveLocation;
if (moveLocationDelta.Length < 4.0f)
{
// No move so just select itself
_timeline.Select(this);
}
}
EndMoving();
return true;
}
return base.OnMouseUp(location, button);
}
/// <inheritdoc />
public override bool OnMouseDoubleClick(Float2 location, MouseButton button)
{
if (base.OnMouseDoubleClick(location, button))
return true;
if (PropertiesEditObject != null)
{
Timeline.ShowEditPopup(PropertiesEditObject, PointToParent(Timeline, location), Track);
return true;
}
return false;
}
/// <inheritdoc />
public override void OnEndMouseCapture()
{
if (_isMoving)
{
EndMoving();
}
base.OnEndMouseCapture();
}
/// <inheritdoc />
public override void OnLostFocus()
{
if (_isMoving)
{
EndMoving();
}
base.OnLostFocus();
}
/// <inheritdoc />
public override void OnMouseEnter(Float2 location)
{
base.OnMouseEnter(location);
_mouseLocation = location;
}
/// <inheritdoc />
public override void OnMouseLeave()
{
base.OnMouseLeave();
_mouseLocation = Float2.Minimum;
}
private void EndMoving()
{
_isMoving = false;
if (_startMoveLeftEdge || _startMoveRightEdge)
{
_startMoveLeftEdge = false;
_startMoveRightEdge = false;
// Re-assign the media start/duration inside the undo recording block
if (_startMoveStartFrame != _startFrame || _startMoveDuration != _durationFrames)
{
var endMoveStartFrame = _startFrame;
var endMoveDuration = _durationFrames;
_startFrame = _startMoveStartFrame;
_durationFrames = _startMoveDuration;
using (new TrackUndoBlock(_tack))
{
_startFrame = endMoveStartFrame;
_durationFrames = endMoveDuration;
}
}
}
else
{
// Global timeline selection moving end
_timeline.OnKeyframesMove(null, this, _mouseLocation, false, true);
}
EndMouseCapture();
}
/// <inheritdoc />
public override void OnDestroy()
{
if (_isMoving)
{
EndMoving();
}
_timeline = null;
_tack = null;
base.OnDestroy();
}
}
}