// 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(); } } }