// 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
{
///
/// Timeline track media event (range-based). Can be added to the timeline track.
///
///
public abstract class Media : ContainerControl
{
///
/// The base class for media properties proxy objects.
///
/// The type of the track.
/// The type of the media.
public abstract class ProxyBase
where TTrack : Track
where TMedia : Media
{
///
/// The track reference/
///
[HideInEditor, NoSerialize]
public TTrack Track;
///
/// The media reference.
///
[HideInEditor, NoSerialize]
public TMedia Media;
///
/// Gets or sets the start frame of the media event (in frames).
///
[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;
}
///
/// Gets or sets the start frame of the media event (in seconds).
///
[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;
}
///
/// Gets or sets the total duration of the media event (in frames).
///
[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;
}
}
///
/// Gets or sets the total duration of the timeline (in seconds).
///
[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;
///
/// Initializes a new instance of the class.
///
/// The track.
/// The media.
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;
///
/// Gets or sets the start frame of the media event.
///
public int StartFrame
{
get => _startFrame;
set
{
if (_startFrame == value)
return;
_startFrame = value;
if (_timeline != null)
{
OnTimelineZoomChanged();
}
OnStartFrameChanged();
}
}
///
/// Occurs when start frame gets changed.
///
public event Action StartFrameChanged;
///
/// Gets the end frame of the media (start + duration).
///
public int EndFrame => _startFrame + _durationFrames;
///
/// Gets or sets the total duration of the media event in the timeline sequence frames amount.
///
public int DurationFrames
{
get => _durationFrames;
set
{
value = Math.Max(value, 1);
if (_durationFrames == value)
return;
_durationFrames = value;
if (_timeline != null)
{
OnTimelineZoomChanged();
}
OnDurationFramesChanged();
}
}
///
/// Occurs when media duration gets changed.
///
public event Action DurationFramesChanged;
///
/// Get or sets the media start time in seconds.
///
///
public float Start
{
get => _startFrame / _timeline.FramesPerSecond;
set => StartFrame = (int)(value * _timeline.FramesPerSecond);
}
///
/// Get or sets the media duration in seconds.
///
///
public float Duration
{
get => _durationFrames / _timeline.FramesPerSecond;
set => DurationFrames = (int)(value * _timeline.FramesPerSecond);
}
///
/// Gets the parent timeline.
///
public Timeline Timeline => _timeline;
///
/// Gets the track.
///
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);
///
/// The track properties editing proxy object. Assign it to add media properties editing support.
///
public object PropertiesEditObject;
///
/// Gets a value indicating whether this media can be split.
///
public bool CanSplit;
///
/// Gets a value indicating whether this media can be removed.
///
public bool CanDelete;
///
/// Gets a value indicating whether this media can be resized (duration changed).
///
public bool CanResize = true;
///
/// Initializes a new instance of the class.
///
protected Media()
{
AutoFocus = false;
}
///
/// Called when showing timeline context menu to the user. Can be used to add custom buttons.
///
/// The menu.
/// The time (in seconds) at which context menu is shown (user clicked on a timeline).
/// The found control under the mouse cursor.
public virtual void OnTimelineContextMenu(ContextMenu.ContextMenu menu, float time, Control controlUnderMouse)
{
if (CanDelete && Track.Media.Count > Track.MinMediaCount)
menu.AddButton("Delete media", Delete);
}
///
/// Called when parent track gets changed.
///
/// The track.
public virtual void OnTimelineChanged(Track track)
{
_timeline = track?.Timeline;
_tack = track;
Parent = _timeline?.MediaPanel;
if (_timeline != null)
{
OnTimelineZoomChanged();
}
}
///
/// Called when timeline FPS gets changed.
///
/// The before value.
/// The after value.
public virtual void OnTimelineFpsChanged(float before, float after)
{
StartFrame = (int)((_startFrame / before) * after);
DurationFrames = (int)((_durationFrames / before) * after);
}
///
/// Called when undo action gets reverted or applied for this media parent track data. Can be used to update UI.
///
public virtual void OnUndo()
{
}
///
/// Called when media gets removed by the user.
///
public virtual void OnDeleted()
{
Dispose();
}
///
/// Called when media start frame gets changed.
///
protected virtual void OnStartFrameChanged()
{
StartFrameChanged?.Invoke();
}
///
/// Called when media duration in frames gets changed.
///
protected virtual void OnDurationFramesChanged()
{
DurationFramesChanged?.Invoke();
}
///
/// Called when timeline zoom gets changed.
///
public virtual void OnTimelineZoomChanged()
{
X = Start * Timeline.UnitsPerSecond * _timeline.Zoom + Timeline.StartOffset;
Width = Duration * Timeline.UnitsPerSecond * _timeline.Zoom;
}
///
/// Splits the media at the specified frame.
///
/// The frame to split at.
/// The another media created after this media split.
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;
}
///
/// Deletes this media.
///
public void Delete()
{
_timeline.Delete(this);
}
///
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);
}
///
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();
}
///
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;
}
///
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);
}
}
///
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);
}
///
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;
}
///
public override void OnEndMouseCapture()
{
if (_isMoving)
{
EndMoving();
}
base.OnEndMouseCapture();
}
///
public override void OnLostFocus()
{
if (_isMoving)
{
EndMoving();
}
base.OnLostFocus();
}
///
public override void OnMouseEnter(Float2 location)
{
base.OnMouseEnter(location);
_mouseLocation = location;
}
///
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();
}
///
public override void OnDestroy()
{
if (_isMoving)
{
EndMoving();
}
_timeline = null;
_tack = null;
base.OnDestroy();
}
}
}