// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using System.IO; using System.Linq; using FlaxEditor.CustomEditors; using FlaxEditor.GUI.ContextMenu; using FlaxEditor.GUI.Drag; using FlaxEditor.GUI.Input; using FlaxEditor.GUI.Timeline.GUI; using FlaxEditor.GUI.Timeline.Tracks; using FlaxEditor.GUI.Timeline.Undo; using FlaxEngine; using FlaxEngine.GUI; namespace FlaxEditor.GUI.Timeline { /// /// The timeline control that contains tracks section and headers. Can be used to create time-based media interface for camera tracks editing, audio mixing and events tracking. /// /// public class Timeline : ContainerControl { private static readonly KeyValuePair[] FPSValues = { new KeyValuePair(12f, "12 fps"), new KeyValuePair(15f, "15 fps"), new KeyValuePair(23.976f, "23.97 (NTSC)"), new KeyValuePair(24f, "24 fps"), new KeyValuePair(25f, "25 (PAL)"), new KeyValuePair(30f, "30 fps"), new KeyValuePair(48f, "48 fps"), new KeyValuePair(50f, "50 (PAL)"), new KeyValuePair(60f, "60 fps"), new KeyValuePair(100f, "100 fps"), new KeyValuePair(120f, "120 fps"), new KeyValuePair(240f, "240 fps"), new KeyValuePair(0, "Custom"), }; internal const int FormatVersion = 3; private sealed class TimeIntervalsHeader : ContainerControl { private Timeline _timeline; private bool _isLeftMouseButtonDown; public TimeIntervalsHeader(Timeline timeline) { _timeline = timeline; } /// public override bool OnMouseDown(Vector2 location, MouseButton button) { if (base.OnMouseDown(location, button)) return true; if (button == MouseButton.Left) { _isLeftMouseButtonDown = true; _timeline._isMovingPositionHandle = true; StartMouseCapture(); Seek(ref location); Focus(); return true; } return false; } /// public override void OnMouseMove(Vector2 location) { base.OnMouseMove(location); if (_isLeftMouseButtonDown) { Seek(ref location); } } private void Seek(ref Vector2 location) { if (_timeline.PlaybackState == PlaybackStates.Disabled) return; var locationTimeline = PointToParent(_timeline, location); var locationTime = _timeline._backgroundArea.PointFromParent(_timeline, locationTimeline); var frame = (locationTime.X - StartOffset * 2.0f) / _timeline.Zoom / UnitsPerSecond * _timeline.FramesPerSecond; _timeline.OnSeek((int)frame); } /// public override bool OnMouseUp(Vector2 location, MouseButton button) { if (base.OnMouseUp(location, button)) return true; if (button == MouseButton.Left && _isLeftMouseButtonDown) { Seek(ref location); EndMouseCapture(); return true; } return false; } /// public override void OnEndMouseCapture() { _isLeftMouseButtonDown = false; _timeline._isMovingPositionHandle = false; base.OnEndMouseCapture(); } /// public override void OnDestroy() { _timeline = null; base.OnDestroy(); } } /// /// The base class for timeline properties proxy objects. /// /// The type of the timeline. public abstract class ProxyBase where TTimeline : Timeline { /// /// The timeline reference. /// [HideInEditor, NoSerialize] public TTimeline Timeline; /// /// Gets or sets the total duration of the timeline in the frames amount. /// [EditorDisplay("General"), EditorOrder(10), Limit(1), Tooltip("Total duration of the timeline event the frames amount.")] public int DurationFrames { get => Timeline.DurationFrames; set => Timeline.DurationFrames = value; } /// /// Initializes a new instance of the class. /// /// The timeline. protected ProxyBase(TTimeline timeline) { Timeline = timeline ?? throw new ArgumentNullException(nameof(timeline)); } } /// /// The time axis value formatting modes. /// public enum TimeShowModes { /// /// The frame numbers. /// Frames, /// /// The seconds amount. /// Seconds, /// /// The time. /// Time, } /// /// The timeline animation playback states. /// public enum PlaybackStates { /// /// The timeline animation feature is disabled. /// Disabled, /// /// The timeline animation feature is disabled except for current frame seeking. /// Seeking, /// /// The timeline animation is stopped. /// Stopped, /// /// The timeline animation is playing. /// Playing, /// /// The timeline animation is paused. /// Paused, } /// /// The timeline playback buttons types. /// [Flags] public enum PlaybackButtons { /// /// No buttons. /// None = 0, /// /// The play/pause button. /// Play = 1, /// /// The stop button. /// Stop = 2, /// /// The current frame navigation buttons (left/right frame, seep begin/end). /// Navigation = 4, } /// /// The header top area height (in pixels). /// public static readonly float HeaderTopAreaHeight = 22.0f; /// /// The timeline units per second (on time axis). /// public static readonly float UnitsPerSecond = 100.0f; /// /// The start offset for the timeline (on time axis). /// public static readonly float StartOffset = 50.0f; private bool _isChangingFps; private float _framesPerSecond = 30.0f; private int _durationFrames = 30 * 5; private int _currentFrame; private TimeShowModes _timeShowMode = TimeShowModes.Frames; private bool _showPreviewValues = true; private PlaybackStates _state = PlaybackStates.Disabled; /// /// Flag used to mark modified timeline data. /// protected bool _isModified; /// /// The tracks collection. /// protected readonly List _tracks = new List(); private SplitPanel _splitter; private TimeIntervalsHeader _timeIntervalsHeader; private ContainerControl _backgroundScroll; private Background _background; private Panel _backgroundArea; private TimelineEdge _leftEdge; private TimelineEdge _rightEdge; private Button _addTrackButton; private ComboBox _fpsComboBox; private Button _viewButton; private FloatValueBox _fpsCustomValue; private TracksPanelArea _tracksPanelArea; private VerticalPanel _tracksPanel; private Image[] _playbackNavigation; private Image _playbackStop; private Image _playbackPlay; private Label _noTracksLabel; private PositionHandle _positionHandle; private bool _isRightMouseButtonDown; private Vector2 _rightMouseButtonDownPos; private float _zoom = 1.0f; private bool _isMovingPositionHandle; private bool _canPlayPauseStop = true; /// /// Gets or sets the current time showing mode. /// public TimeShowModes TimeShowMode { get => _timeShowMode; set { if (_timeShowMode == value) return; _timeShowMode = value; TimeShowModeChanged?.Invoke(); } } /// /// Occurs when current time showing mode gets changed. /// public event Action TimeShowModeChanged; /// /// Gets or sets the preview values showing option value. /// public bool ShowPreviewValues { get => _showPreviewValues; set { if (_showPreviewValues == value) return; _showPreviewValues = value; ShowPreviewValuesChanged?.Invoke(); } } /// /// Occurs when preview values showing option gets changed. /// public event Action ShowPreviewValuesChanged; /// /// Gets or sets the current animation playback time position (frame number). /// public int CurrentFrame { get => _currentFrame; set { if (_currentFrame == value) return; _currentFrame = value; UpdatePositionHandle(); for (var i = 0; i < _tracks.Count; i++) { _tracks[i].OnTimelineCurrentFrameChanged(_currentFrame); } CurrentFrameChanged?.Invoke(); } } /// /// Gets the current animation time position (in seconds). /// public float CurrentTime => _currentFrame / _framesPerSecond; /// /// Occurs when current playback animation frame gets changed. /// public event Action CurrentFrameChanged; /// /// Gets or sets the amount of frames per second of the timeline animation. /// public float FramesPerSecond { get => _framesPerSecond; set { value = Mathf.Clamp(value, 0.1f, 1000.0f); if (Mathf.NearEqual(_framesPerSecond, value)) return; Undo?.AddAction(new EditFpsAction(this, _framesPerSecond, value)); SetFPS(value); } } internal void SetFPS(float value) { var oldDuration = Duration; var oldValue = _framesPerSecond; _isChangingFps = true; _framesPerSecond = value; if (_fpsComboBox != null) { int index = FPSValues.Length - 1; for (int i = 0; i < FPSValues.Length; i++) { if (Mathf.NearEqual(FPSValues[i].Key, value)) { index = i; break; } } _fpsComboBox.SelectedIndex = index; } if (_fpsCustomValue != null) _fpsCustomValue.Value = value; _isChangingFps = false; FramesPerSecondChanged?.Invoke(); // Preserve media events and duration for (var i = 0; i < _tracks.Count; i++) { _tracks[i].OnTimelineFpsChanged(oldValue, value); } Duration = oldDuration; // Update MarkAsEdited(); ArrangeTracks(); UpdatePositionHandle(); } /// /// Occurs when frames per second gets changed. /// public event Action FramesPerSecondChanged; /// /// Gets or sets the timeline animation duration in frames. /// /// public int DurationFrames { get => _durationFrames; set { value = Mathf.Max(value, 1); if (_durationFrames == value) return; _durationFrames = value; ArrangeTracks(); DurationFramesChanged?.Invoke(); } } /// /// Gets the timeline animation duration in seconds. /// /// public float Duration { get => _durationFrames / FramesPerSecond; set => DurationFrames = (int)(value * FramesPerSecond); } /// /// Occurs when timeline duration gets changed. /// public event Action DurationFramesChanged; /// /// Gets the collection of the tracks added to this timeline (read-only list). /// public IReadOnlyList Tracks => _tracks; /// /// Occurs when tracks collection gets changed. /// public event Action TracksChanged; /// /// Gets a value indicating whether this timeline was modified by the user (needs saving and flushing with data source). /// public bool IsModified => _isModified; /// /// Occurs when timeline gets modified (track edited, media moved, etc.). /// public event Action Modified; /// /// Occurs when timeline starts playing animation. /// public event Action Play; /// /// Occurs when timeline pauses playing animation. /// public event Action Pause; /// /// Occurs when timeline stops playing animation. /// public event Action Stop; /// /// Gets the splitter. /// public SplitPanel Splitter => _splitter; /// /// The track archetypes. /// public readonly List TrackArchetypes = new List(32); /// /// The selected tracks. /// public readonly List SelectedTracks = new List(); /// /// The selected media events. /// public readonly List SelectedMedia = new List(); /// /// Occurs when any collection of the selected objects in the timeline gets changed. /// public event Action SelectionChanged; /// /// Gets the media controls background panel (with scroll bars). /// public Panel MediaBackground => _backgroundArea; /// /// Gets the media controls parent panel. /// public Background MediaPanel => _background; /// /// Gets the track controls parent panel. /// public VerticalPanel TracksPanel => _tracksPanel; /// /// Gets the state of the timeline animation playback. /// public PlaybackStates PlaybackState { get => _state; protected set { if (_state == value) return; _state = value; UpdatePlaybackButtons(); } } /// /// The timeline properties editing proxy object. Assign it to add timeline properties editing support. /// public object PropertiesEditObject; /// /// Gets or sets the timeline view zoom. /// public float Zoom { get => _zoom; set { value = Mathf.Clamp(value, 0.0001f, 1000.0f); if (Mathf.NearEqual(_zoom, value)) return; _zoom = value; foreach (var track in _tracks) { track.OnTimelineZoomChanged(); } ArrangeTracks(); UpdatePositionHandle(); } } /// /// Gets or sets a value indicating whether user can sue Play/Pause/Stop buttons, otherwise those should be disabled. /// public bool CanPlayPauseStop { get => _canPlayPauseStop; set { if (_canPlayPauseStop == value) return; _canPlayPauseStop = value; UpdatePlaybackButtons(); } } /// /// Gets a value indicating whether user is moving position handle (seeking). /// public bool IsMovingPositionHandle => _isMovingPositionHandle; /// /// The drag and drop handler. /// public struct DragHandler { /// /// The drag and drop handler. /// public DragHelper Helper; /// /// The action. /// public Action Action; /// /// Initializes a new instance of the struct. /// /// The helper. /// The action. public DragHandler(DragHelper helper, Action action) { Helper = helper; Action = action; } } /// /// The drag handlers pairs of drag helper and the function that creates a track on drag drop. /// public readonly List DragHandlers = new List(); /// /// The camera cut thumbnail renderer. /// public CameraCutThumbnailRenderer CameraCutThumbnailRenderer; /// /// The undo system to use for the history actions recording (optional, can be null). /// public readonly FlaxEditor.Undo Undo; /// /// Initializes a new instance of the class. /// /// The playback buttons to use. /// The undo/redo to use for the history actions recording. Optional, can be null to disable undo support. /// True if user can modify the timeline FPS, otherwise it will be fixed or controlled from the code. /// True if user can add new tracks. public Timeline(PlaybackButtons playbackButtons, FlaxEditor.Undo undo = null, bool canChangeFps = true, bool canAddTracks = true) { Undo = undo; AutoFocus = false; _splitter = new SplitPanel(Orientation.Horizontal, ScrollBars.None, ScrollBars.None) { SplitterValue = 0.4f, AnchorPreset = AnchorPresets.StretchAll, Offsets = Margin.Zero, Parent = this }; var headerTopArea = new ContainerControl { AutoFocus = false, BackgroundColor = Style.Current.LightBackground, AnchorPreset = AnchorPresets.HorizontalStretchTop, Offsets = new Margin(0, 0, 0, HeaderTopAreaHeight), Parent = _splitter.Panel1 }; if (canAddTracks) { _addTrackButton = new Button(2, 2, 40.0f, 18.0f) { TooltipText = "Add new tracks to the timeline", Text = "Add", Parent = headerTopArea }; _addTrackButton.Clicked += OnAddTrackButtonClicked; } _viewButton = new Button((_addTrackButton?.Right ?? 0) + 2, 2, 40.0f, 18.0f) { TooltipText = "Change timeline view options", Text = "View", Parent = headerTopArea }; _viewButton.Clicked += OnViewButtonClicked; if (canChangeFps) { var changeFpsWidth = 70.0f; _fpsComboBox = new ComboBox(_viewButton.Right + 2, 2, changeFpsWidth) { TooltipText = "Change timeline frames per second", Parent = headerTopArea }; for (int i = 0; i < FPSValues.Length; i++) { _fpsComboBox.AddItem(FPSValues[i].Value); if (Mathf.NearEqual(_framesPerSecond, FPSValues[i].Key)) _fpsComboBox.SelectedIndex = i; } if (_fpsComboBox.SelectedIndex == -1) _fpsComboBox.SelectedIndex = FPSValues.Length - 1; _fpsComboBox.SelectedIndexChanged += OnFpsSelectedIndexChanged; _fpsComboBox.PopupShowing += OnFpsPopupShowing; } var playbackButtonsSize = 0.0f; if (playbackButtons != PlaybackButtons.None) { playbackButtonsSize = 24.0f; var icons = Editor.Instance.Icons; var playbackButtonsArea = new ContainerControl { AutoFocus = false, ClipChildren = false, BackgroundColor = Style.Current.LightBackground, AnchorPreset = AnchorPresets.HorizontalStretchBottom, Offsets = new Margin(0, 0, -playbackButtonsSize, playbackButtonsSize), Parent = _splitter.Panel1 }; var playbackButtonsPanel = new ContainerControl { AutoFocus = false, ClipChildren = false, AnchorPreset = AnchorPresets.VerticalStretchCenter, Offsets = Margin.Zero, Parent = playbackButtonsArea, }; if ((playbackButtons & PlaybackButtons.Navigation) == PlaybackButtons.Navigation) { _playbackNavigation = new Image[6]; _playbackNavigation[0] = new Image(playbackButtonsPanel.Width, 0, playbackButtonsSize, playbackButtonsSize) { TooltipText = "Rewind to timeline start (Home)", Brush = new SpriteBrush(icons.Step32), Enabled = false, Visible = false, Rotation = 180.0f, Parent = playbackButtonsPanel }; _playbackNavigation[0].Clicked += (image, button) => OnSeek(0); playbackButtonsPanel.Width += playbackButtonsSize; _playbackNavigation[1] = new Image(playbackButtonsPanel.Width, 0, playbackButtonsSize, playbackButtonsSize) { TooltipText = "Seek back to the previous keyframe (Page Down)", Brush = new SpriteBrush(icons.Next32), Enabled = false, Visible = false, Rotation = 180.0f, Parent = playbackButtonsPanel }; _playbackNavigation[1].Clicked += (image, button) => { if (GetPreviousKeyframeFrame(CurrentTime, out var time)) OnSeek(time); }; playbackButtonsPanel.Width += playbackButtonsSize; _playbackNavigation[2] = new Image(playbackButtonsPanel.Width, 0, playbackButtonsSize, playbackButtonsSize) { TooltipText = "Move one frame back (Left Arrow)", Brush = new SpriteBrush(icons.ArrowLeft32), Enabled = false, Visible = false, Parent = playbackButtonsPanel }; _playbackNavigation[2].Clicked += (image, button) => OnSeek(CurrentFrame - 1); playbackButtonsPanel.Width += playbackButtonsSize; } if ((playbackButtons & PlaybackButtons.Stop) == PlaybackButtons.Stop) { _playbackStop = new Image(playbackButtonsPanel.Width, 0, playbackButtonsSize, playbackButtonsSize) { TooltipText = "Stop playback", Brush = new SpriteBrush(icons.Stop32), Visible = false, Enabled = false, Parent = playbackButtonsPanel }; _playbackStop.Clicked += OnStopClicked; playbackButtonsPanel.Width += playbackButtonsSize; } if ((playbackButtons & PlaybackButtons.Play) == PlaybackButtons.Play) { _playbackPlay = new Image(playbackButtonsPanel.Width, 0, playbackButtonsSize, playbackButtonsSize) { TooltipText = "Play/pause playback (Space)", Brush = new SpriteBrush(icons.Play32), Visible = false, Tag = false, // Set to true if image is set to Pause, false if Play Parent = playbackButtonsPanel }; _playbackPlay.Clicked += OnPlayClicked; playbackButtonsPanel.Width += playbackButtonsSize; } if ((playbackButtons & PlaybackButtons.Navigation) == PlaybackButtons.Navigation) { _playbackNavigation[3] = new Image(playbackButtonsPanel.Width, 0, playbackButtonsSize, playbackButtonsSize) { TooltipText = "Move one frame forward (Right Arrow)", Brush = new SpriteBrush(icons.ArrowRight32), Enabled = false, Visible = false, Parent = playbackButtonsPanel }; _playbackNavigation[3].Clicked += (image, button) => OnSeek(CurrentFrame + 1); playbackButtonsPanel.Width += playbackButtonsSize; _playbackNavigation[4] = new Image(playbackButtonsPanel.Width, 0, playbackButtonsSize, playbackButtonsSize) { TooltipText = "Seek to the next keyframe (Page Up)", Brush = new SpriteBrush(icons.Next32), Enabled = false, Visible = false, Parent = playbackButtonsPanel }; _playbackNavigation[4].Clicked += (image, button) => { if (GetNextKeyframeFrame(CurrentTime, out var time)) OnSeek(time); }; playbackButtonsPanel.Width += playbackButtonsSize; _playbackNavigation[5] = new Image(playbackButtonsPanel.Width, 0, playbackButtonsSize, playbackButtonsSize) { TooltipText = "Rewind to timeline end (End)", Brush = new SpriteBrush(icons.Step32), Enabled = false, Visible = false, Parent = playbackButtonsPanel }; _playbackNavigation[5].Clicked += (image, button) => OnSeek(DurationFrames); playbackButtonsPanel.Width += playbackButtonsSize; } playbackButtonsPanel.X = (playbackButtonsPanel.Parent.Width - playbackButtonsPanel.Width) * 0.5f; } _tracksPanelArea = new TracksPanelArea(this) { AutoFocus = false, AnchorPreset = AnchorPresets.StretchAll, Offsets = new Margin(0, 0, HeaderTopAreaHeight, playbackButtonsSize), Parent = _splitter.Panel1, }; _tracksPanel = new VerticalPanel { AutoFocus = false, AnchorPreset = AnchorPresets.HorizontalStretchTop, Offsets = Margin.Zero, IsScrollable = true, BottomMargin = 40.0f, Parent = _tracksPanelArea }; _noTracksLabel = new Label { AnchorPreset = AnchorPresets.MiddleCenter, Offsets = Margin.Zero, TextColor = Color.Gray, TextColorHighlighted = Color.Gray * 1.1f, Text = "No tracks", Parent = _tracksPanelArea }; _timeIntervalsHeader = new TimeIntervalsHeader(this) { AutoFocus = false, BackgroundColor = Style.Current.Background.RGBMultiplied(0.9f), AnchorPreset = AnchorPresets.HorizontalStretchTop, Offsets = new Margin(0, 0, 0, HeaderTopAreaHeight), Parent = _splitter.Panel2 }; _backgroundArea = new Panel(ScrollBars.Both) { AutoFocus = false, ClipChildren = false, BackgroundColor = Style.Current.Background.RGBMultiplied(0.7f), AnchorPreset = AnchorPresets.StretchAll, Offsets = new Margin(0, 0, HeaderTopAreaHeight, 0), Parent = _splitter.Panel2 }; _backgroundScroll = new ContainerControl { AutoFocus = false, ClipChildren = false, Offsets = Margin.Zero, Parent = _backgroundArea }; _background = new Background(this) { AutoFocus = false, ClipChildren = false, Offsets = Margin.Zero, Parent = _backgroundArea }; _leftEdge = new TimelineEdge(this, true, false) { Offsets = Margin.Zero, Parent = _backgroundArea }; _rightEdge = new TimelineEdge(this, false, true) { Offsets = Margin.Zero, Parent = _backgroundArea }; _positionHandle = new PositionHandle(this) { ClipChildren = false, Visible = false, Parent = _backgroundArea, }; UpdatePositionHandle(); PlaybackState = PlaybackStates.Disabled; } private void UpdatePositionHandle() { var handleWidth = 12.0f; _positionHandle.Bounds = new Rectangle( StartOffset * 2.0f - handleWidth * 0.5f + _currentFrame / _framesPerSecond * UnitsPerSecond * Zoom, HeaderTopAreaHeight * -0.5f, handleWidth, HeaderTopAreaHeight * 0.5f ); } private void OnFpsPopupShowing(ComboBox comboBox) { if (_fpsCustomValue == null) { _fpsCustomValue = new FloatValueBox(_framesPerSecond, 63, 295, 45.0f, 0.1f, 1000.0f, 0.1f) { Parent = comboBox.Popup }; _fpsCustomValue.ValueChanged += OnFpsCustomValueChanged; } _fpsCustomValue.Value = FramesPerSecond; } private void OnFpsCustomValueChanged() { if (_isChangingFps || _fpsComboBox.SelectedIndex != FPSValues.Length - 1) return; // Custom value FramesPerSecond = _fpsCustomValue.Value; } private void OnFpsSelectedIndexChanged(ComboBox comboBox) { if (_isChangingFps) return; if (comboBox.SelectedIndex == FPSValues.Length - 1) { // Custom value FramesPerSecond = _fpsCustomValue.Value; } else { // Predefined value FramesPerSecond = FPSValues[comboBox.SelectedIndex].Key; } } private void OnAddTrackButtonClicked() { // TODO: maybe cache context menu object? var menu = new ContextMenu.ContextMenu(); for (int i = 0; i < TrackArchetypes.Count; i++) { var archetype = TrackArchetypes[i]; if (archetype.DisableSpawnViaGUI) continue; var button = menu.AddButton(archetype.Name, OnAddTrackOptionClicked); button.Tag = archetype; button.Icon = archetype.Icon; } menu.Show(_addTrackButton.Parent, _addTrackButton.BottomLeft); } private void OnAddTrackOptionClicked(ContextMenuButton button) { var archetype = (TrackArchetype)button.Tag; AddTrack(archetype); } private void OnViewButtonClicked() { var menu = new ContextMenu.ContextMenu(); var showTimeAs = menu.AddChildMenu("Show time as"); showTimeAs.ContextMenu.AddButton("Frames", () => TimeShowMode = TimeShowModes.Frames).Checked = TimeShowMode == TimeShowModes.Frames; showTimeAs.ContextMenu.AddButton("Seconds", () => TimeShowMode = TimeShowModes.Seconds).Checked = TimeShowMode == TimeShowModes.Seconds; showTimeAs.ContextMenu.AddButton("Time", () => TimeShowMode = TimeShowModes.Time).Checked = TimeShowMode == TimeShowModes.Time; menu.AddButton("Show preview values", () => ShowPreviewValues = !ShowPreviewValues).Checked = ShowPreviewValues; OnShowViewContextMenu(menu); menu.Show(_viewButton.Parent, _viewButton.BottomLeft); } /// /// Occurs when timeline shows the View context menu. Can be sued to add custom options. /// public event Action ShowViewContextMenu; /// /// Called when timeline shows the View context menu. Can be sued to add custom options. /// /// The menu. protected virtual void OnShowViewContextMenu(ContextMenu.ContextMenu menu) { ShowViewContextMenu?.Invoke(menu); } private void OnStopClicked(Image stop, MouseButton button) { if (button == MouseButton.Left) { OnStop(); } } private void OnPlayClicked(Image play, MouseButton button) { if (button == MouseButton.Left) { if ((bool)play.Tag) OnPause(); else OnPlay(); } } /// /// Called when animation should stop. /// public virtual void OnStop() { Stop?.Invoke(); PlaybackState = PlaybackStates.Stopped; } /// /// Called when animation should play. /// public virtual void OnPlay() { Play?.Invoke(); PlaybackState = PlaybackStates.Playing; } /// /// Called when animation should pause. /// public virtual void OnPause() { Pause?.Invoke(); PlaybackState = PlaybackStates.Paused; } /// /// Called when animation playback position should be changed. /// /// The frame. public virtual void OnSeek(int frame) { } /// /// Gets the frame of the next keyframe from all tracks (if found). /// /// The start time. /// The result value. /// True if found next keyframe, otherwise false. public bool GetNextKeyframeFrame(float time, out int result) { bool hasValid = false; int closestFrame = 0; for (int i = 0; i < _tracks.Count; i++) { if (_tracks[i].GetNextKeyframeFrame(time, out var frame) && (!hasValid || closestFrame > frame)) { hasValid = true; closestFrame = frame; } } result = closestFrame; return hasValid; } /// /// Gets the frame of the previous keyframe (if found). /// /// The start time. /// The result value. /// True if found previous keyframe, otherwise false. public bool GetPreviousKeyframeFrame(float time, out int result) { bool hasValid = false; int closestFrame = 0; for (int i = 0; i < _tracks.Count; i++) { if (_tracks[i].GetPreviousKeyframeFrame(time, out var frame) && (!hasValid || closestFrame < frame)) { hasValid = true; closestFrame = frame; } } result = closestFrame; return hasValid; } private void UpdatePlaybackButtons() { // Update buttons UI var icons = Editor.Instance.Icons; // TODO: cleanup this UI code switch (_state) { case PlaybackStates.Disabled: if (_playbackNavigation != null) { foreach (var e in _playbackNavigation) { e.Enabled = false; e.Visible = false; } } if (_playbackStop != null) { _playbackStop.Visible = false; } if (_playbackPlay != null) { _playbackPlay.Visible = false; } if (_positionHandle != null) { _positionHandle.Visible = false; } break; case PlaybackStates.Seeking: if (_playbackNavigation != null) { foreach (var e in _playbackNavigation) { e.Enabled = false; e.Visible = false; } } if (_playbackStop != null) { _playbackStop.Visible = false; } if (_playbackPlay != null) { _playbackPlay.Visible = false; } if (_positionHandle != null) { _positionHandle.Visible = true; } break; case PlaybackStates.Stopped: if (_playbackNavigation != null) { foreach (var e in _playbackNavigation) { e.Enabled = true; e.Visible = true; } } if (_playbackStop != null) { _playbackStop.Visible = true; _playbackStop.Enabled = false; } if (_playbackPlay != null) { _playbackPlay.Visible = true; _playbackPlay.Enabled = _canPlayPauseStop; _playbackPlay.Brush = new SpriteBrush(icons.Play32); _playbackPlay.Tag = false; } if (_positionHandle != null) { _positionHandle.Visible = true; } break; case PlaybackStates.Playing: if (_playbackNavigation != null) { foreach (var e in _playbackNavigation) { e.Enabled = true; e.Visible = true; } } if (_playbackStop != null) { _playbackStop.Visible = true; _playbackStop.Enabled = _canPlayPauseStop; } if (_playbackPlay != null) { _playbackPlay.Visible = true; _playbackPlay.Enabled = _canPlayPauseStop; _playbackPlay.Brush = new SpriteBrush(icons.Pause32); _playbackPlay.Tag = true; } if (_positionHandle != null) { _positionHandle.Visible = true; } break; case PlaybackStates.Paused: if (_playbackNavigation != null) { foreach (var e in _playbackNavigation) { e.Enabled = true; e.Visible = true; } } if (_playbackStop != null) { _playbackStop.Visible = true; _playbackStop.Enabled = _canPlayPauseStop; } if (_playbackPlay != null) { _playbackPlay.Visible = true; _playbackPlay.Enabled = _canPlayPauseStop; _playbackPlay.Brush = new SpriteBrush(icons.Play32); _playbackPlay.Tag = false; } if (_positionHandle != null) { _positionHandle.Visible = true; } break; default: throw new ArgumentOutOfRangeException(); } } /// /// Creates a new track object that can be later added using . /// /// The archetype. /// The created track object. public Track NewTrack(TrackArchetype archetype) { var options = new TrackCreateOptions { Archetype = archetype, Mute = false, }; return archetype.Create(options); } /// /// Loads the timeline data. /// /// The version. /// The input stream. protected virtual void LoadTimelineData(int version, BinaryReader stream) { } /// /// Saves the timeline data. /// /// The output stream. protected virtual void SaveTimelineData(BinaryWriter stream) { } /// /// Loads the timeline data after reading the timeline tracks. /// /// The version. /// The input stream. protected virtual void LoadTimelineCustomData(int version, BinaryReader stream) { } /// /// Saves the timeline data after saving the timeline tracks. /// /// The output stream. protected virtual void SaveTimelineCustomData(BinaryWriter stream) { } /// /// Loads the timeline from the specified data. /// /// The data. public virtual void Load(byte[] data) { Profiler.BeginEvent("Clear"); Clear(); LockChildrenRecursive(); Profiler.EndEvent(); using (var memory = new MemoryStream(data)) using (var stream = new BinaryReader(memory)) { Profiler.BeginEvent("LoadData"); int version = stream.ReadInt32(); switch (version) { case 1: { // [Deprecated on 23.07.2019, expires on 27.04.2021] // Load properties FramesPerSecond = stream.ReadSingle(); DurationFrames = stream.ReadInt32(); LoadTimelineData(version, stream); // Load tracks int tracksCount = stream.ReadInt32(); _tracks.Capacity = Math.Max(_tracks.Capacity, tracksCount); for (int i = 0; i < tracksCount; i++) { var type = stream.ReadByte(); var flag = stream.ReadByte(); Track track = null; var mute = (flag & 1) == 1; for (int j = 0; j < TrackArchetypes.Count; j++) { if (TrackArchetypes[j].TypeId == type) { var options = new TrackCreateOptions { Archetype = TrackArchetypes[j], Mute = mute, }; track = TrackArchetypes[j].Create(options); break; } } if (track == null) throw new Exception("Unknown timeline track type " + type); int parentIndex = stream.ReadInt32(); int childrenCount = stream.ReadInt32(); track.Name = Utilities.Utils.ReadStr(stream, -13); track.Tag = parentIndex; track.Archetype.Load(version, track, stream); AddLoadedTrack(track); } break; } case 2: case 3: { // Load properties FramesPerSecond = stream.ReadSingle(); DurationFrames = stream.ReadInt32(); LoadTimelineData(version, stream); // Load tracks int tracksCount = stream.ReadInt32(); _tracks.Capacity = Math.Max(_tracks.Capacity, tracksCount); for (int i = 0; i < tracksCount; i++) { var type = stream.ReadByte(); var flag = stream.ReadByte(); Track track = null; var mute = (flag & 1) == 1; var loop = (flag & 2) == 2; for (int j = 0; j < TrackArchetypes.Count; j++) { if (TrackArchetypes[j].TypeId == type) { var options = new TrackCreateOptions { Archetype = TrackArchetypes[j], Mute = mute, Loop = loop, }; track = TrackArchetypes[j].Create(options); break; } } if (track == null) throw new Exception("Unknown timeline track type " + type); int parentIndex = stream.ReadInt32(); int childrenCount = stream.ReadInt32(); track.Name = Utilities.Utils.ReadStr(stream, -13); track.Tag = parentIndex; track.Color = stream.ReadColor32(); Profiler.BeginEvent("LoadTack"); track.Archetype.Load(version, track, stream); Profiler.EndEvent(); AddLoadedTrack(track); } break; } default: throw new Exception("Unknown timeline version " + version); } LoadTimelineCustomData(version, stream); Profiler.EndEvent(); Profiler.BeginEvent("ParentTracks"); for (int i = 0; i < _tracks.Count; i++) { var parentIndex = (int)_tracks[i].Tag; _tracks[i].Tag = null; if (parentIndex != -1) _tracks[i].ParentTrack = _tracks[parentIndex]; } Profiler.EndEvent(); Profiler.BeginEvent("SetupTracks"); for (int i = 0; i < _tracks.Count; i++) { _tracks[i].OnLoaded(); } Profiler.EndEvent(); } Profiler.BeginEvent("ArrangeTracks"); ArrangeTracks(); PerformLayout(true); UnlockChildrenRecursive(); Profiler.EndEvent(); ClearEditedFlag(); } /// /// Saves the timeline data. /// /// The saved timeline data. public virtual byte[] Save() { // Serialize timeline to stream using (var memory = new MemoryStream(512)) using (var stream = new BinaryWriter(memory)) { // Save properties stream.Write(FormatVersion); stream.Write(FramesPerSecond); stream.Write(DurationFrames); SaveTimelineData(stream); // Save tracks int tracksCount = Tracks.Count; stream.Write(tracksCount); for (int i = 0; i < tracksCount; i++) { var track = Tracks[i]; stream.Write((byte)track.Archetype.TypeId); byte flag = 0; if (track.Mute) flag |= 1; if (track.Loop) flag |= 2; stream.Write(flag); stream.Write(_tracks.IndexOf(track.ParentTrack)); stream.Write(track.SubTracks.Count); Utilities.Utils.WriteStr(stream, track.Name, -13); stream.Write((Color32)track.Color); track.Archetype.Save(track, stream); } SaveTimelineCustomData(stream); return memory.ToArray(); } } /// /// Finds the track by the name. /// /// The name. /// The found track or null. public Track FindTrack(string name) { for (int i = 0; i < _tracks.Count; i++) { if (_tracks[i].Name == name) return _tracks[i]; } return null; } /// /// Adds the track. /// /// The archetype. public void AddTrack(TrackArchetype archetype) { AddTrack(NewTrack(archetype)); } /// /// Adds the track. /// /// The track. /// True if use undo/redo action for track adding. public virtual void AddTrack(Track track, bool withUndo = true) { // Ensure name is unique int idx = 1; var name = track.Name; while (!IsTrackNameValid(track.Name)) track.Name = string.Format("{0} {1}", name, idx++); // Add it to the timeline _tracks.Add(track); track.OnTimelineChanged(this); track.Parent = _tracksPanel; // Update OnTracksChanged(); if (track.ParentTrack != null) OnTracksOrderChanged(); track.OnSpawned(); _tracksPanelArea.ScrollViewTo(track); MarkAsEdited(); if (withUndo) Undo?.AddAction(new AddRemoveTrackAction(this, track, true)); } /// /// Adds the loaded track. Does not handle any UI updates. /// /// The track. protected void AddLoadedTrack(Track track) { _tracks.Add(track); track.OnTimelineChanged(this); track.Parent = _tracksPanel; } /// /// Removes the track. /// /// The track. public virtual void RemoveTrack(Track track) { track.Parent = null; track.OnTimelineChanged(null); _tracks.Remove(track); OnTracksChanged(); } /// /// Called when collection of the tracks gets changed. /// protected virtual void OnTracksChanged() { TracksChanged?.Invoke(); ArrangeTracks(); } /// /// Selects the specified track. /// /// The track. /// If set to true track will be added to selection, otherwise will clear selection before. public void Select(Track track, bool addToSelection = false) { if (SelectedTracks.Contains(track) && (addToSelection || (SelectedTracks.Count == 1 && SelectedMedia.Count == 0))) return; if (!addToSelection) { SelectedTracks.Clear(); SelectedMedia.Clear(); } SelectedTracks.Add(track); OnSelectionChanged(); } /// /// Deselects the specified track. /// /// The track. public void Deselect(Track track) { if (!SelectedTracks.Contains(track)) return; SelectedTracks.Remove(track); OnSelectionChanged(); } /// /// Selects the specified media event. /// /// The media. /// If set to true track will be added to selection, otherwise will clear selection before. public void Select(Media media, bool addToSelection = false) { if (SelectedMedia.Contains(media) && (addToSelection || (SelectedTracks.Count == 0 && SelectedMedia.Count == 1))) return; if (!addToSelection) { SelectedTracks.Clear(); SelectedMedia.Clear(); } SelectedMedia.Add(media); OnSelectionChanged(); } /// /// Deselects the specified media event. /// /// The media. public void Deselect(Media media) { if (!SelectedMedia.Contains(media)) return; SelectedMedia.Remove(media); OnSelectionChanged(); } /// /// Deselects all media and tracks. /// public void Deselect() { if (SelectedMedia.Count == 0 && SelectedTracks.Count == 0) return; SelectedTracks.Clear(); SelectedMedia.Clear(); OnSelectionChanged(); } /// /// Called when selection gets changed. /// protected virtual void OnSelectionChanged() { SelectionChanged?.Invoke(); } private void GetTracks(Track track, List tracks) { if (tracks.Contains(track)) return; tracks.Add(track); foreach (var subTrack in track.SubTracks) GetTracks(subTrack, tracks); } /// /// Deletes the selected tracks/media events. /// /// True if use undo/redo action for track removing. public void DeleteSelection(bool withUndo = true) { if (SelectedMedia.Count > 0) { throw new NotImplementedException("TODO: removing selected media events"); } if (SelectedTracks.Count > 0) { // Delete selected tracks var tracks = new List(SelectedTracks.Count); for (int i = 0; i < SelectedTracks.Count; i++) { GetTracks(SelectedTracks[i], tracks); } SelectedTracks.Clear(); if (withUndo & Undo != null) { if (tracks.Count == 1) { Undo.AddAction(new AddRemoveTrackAction(this, tracks[0], false)); } else { var actions = new List(); for (int i = tracks.Count - 1; i >= 0; i--) actions.Add(new AddRemoveTrackAction(this, tracks[i], false)); Undo.AddAction(new MultiUndoAction(actions, "Remove tracks")); } } for (int i = tracks.Count - 1; i >= 0; i--) { tracks[i].ParentTrack = null; OnDeleteTrack(tracks[i]); } OnTracksChanged(); MarkAsEdited(); } } /// /// Deletes the tracks. /// /// The track to delete (and its sub tracks). /// True if use undo/redo action for track removing. public void Delete(Track track, bool withUndo = true) { if (track == null) throw new ArgumentNullException(); // Delete tracks var tracks = new List(4); GetTracks(track, tracks); if (withUndo & Undo != null) { if (tracks.Count == 1) { Undo.AddAction(new AddRemoveTrackAction(this, tracks[0], false)); } else { var actions = new List(); for (int i = tracks.Count - 1; i >= 0; i--) actions.Add(new AddRemoveTrackAction(this, tracks[i], false)); Undo.AddAction(new MultiUndoAction(actions, "Remove tracks")); } } for (int i = tracks.Count - 1; i >= 0; i--) { tracks[i].ParentTrack = null; OnDeleteTrack(tracks[i]); } OnTracksChanged(); MarkAsEdited(); } /// /// Called to delete track. /// /// The track. protected virtual void OnDeleteTrack(Track track) { SelectedTracks.Remove(track); _tracks.Remove(track); track.OnDeleted(); } /// /// Called once to setup the drag drop handling for the timeline (lazy init on first drag action). /// protected virtual void SetupDragDrop() { } /// /// Mark timeline as edited. /// public void MarkAsEdited() { _isModified = true; Modified?.Invoke(); } /// /// Clears this instance. Removes all tracks, parameters and state. /// public void Clear() { Deselect(); // Remove all tracks var tracks = new List(_tracks); for (int i = 0; i < tracks.Count; i++) { OnDeleteTrack(tracks[i]); } OnTracksChanged(); ClearEditedFlag(); } /// /// Clears the modification flag. /// public void ClearEditedFlag() { if (_isModified) { _isModified = false; Modified?.Invoke(); } } internal void ChangeTrackIndex(Track track, int newIndex) { int oldIndex = _tracks.IndexOf(track); _tracks.RemoveAt(oldIndex); // Check if index is invalid if (newIndex < 0 || newIndex >= _tracks.Count) { // Append at the end _tracks.Add(track); } else { // Change order _tracks.Insert(newIndex, track); } } private void CollectTracks(Track track) { track.Parent = _tracksPanel; _tracks.Add(track); for (int i = 0; i < track.SubTracks.Count; i++) { CollectTracks(track.SubTracks[i]); } } /// /// Called when tracks order gets changed. /// public void OnTracksOrderChanged() { _tracksPanel.IsLayoutLocked = true; for (int i = 0; i < _tracks.Count; i++) { _tracks[i].Parent = null; } var rootTracks = new List(); foreach (var track in _tracks) { if (track.ParentTrack == null) rootTracks.Add(track); } _tracks.Clear(); foreach (var track in rootTracks) { CollectTracks(track); } ArrangeTracks(); _tracksPanel.IsLayoutLocked = false; _tracksPanel.PerformLayout(); ArrangeTracks(); } /// /// Determines whether the specified track name is valid. /// /// The name. /// true if is track name is valid; otherwise, false. public bool IsTrackNameValid(string name) { name = name?.Trim(); return !string.IsNullOrEmpty(name) && _tracks.All(x => x.Name != name); } /// /// Arranges the tracks. /// public void ArrangeTracks() { if (_noTracksLabel != null) { _noTracksLabel.Visible = _tracks.Count == 0; } for (int i = 0; i < _tracks.Count; i++) { _tracks[i].OnTimelineArrange(); } if (_background != null) { float height = _tracksPanel.Height; _background.Visible = _tracks.Count > 0; _background.Bounds = new Rectangle(StartOffset, 0, Duration * UnitsPerSecond * Zoom, height); var edgeWidth = 6.0f; _leftEdge.Bounds = new Rectangle(_background.Left - edgeWidth * 0.5f + StartOffset, HeaderTopAreaHeight * -0.5f, edgeWidth, height + HeaderTopAreaHeight * 0.5f); _rightEdge.Bounds = new Rectangle(_background.Right - edgeWidth * 0.5f + StartOffset, HeaderTopAreaHeight * -0.5f, edgeWidth, height + HeaderTopAreaHeight * 0.5f); _backgroundScroll.Bounds = new Rectangle(0, 0, _background.Width + 5 * StartOffset, height); } } /// protected override void PerformLayoutBeforeChildren() { base.PerformLayoutBeforeChildren(); ArrangeTracks(); } /// public override void Update(float deltaTime) { base.Update(deltaTime); // Synchronize scroll vertical bars for tracks and media panels to keep the view in sync var scroll1 = _tracksPanelArea.VScrollBar; var scroll2 = _backgroundArea.VScrollBar; if (scroll1.IsThumbClicked || _tracksPanelArea.IsMouseOver) scroll2.TargetValue = scroll1.Value; else scroll1.TargetValue = scroll2.Value; } /// public override bool OnMouseDown(Vector2 location, MouseButton button) { if (base.OnMouseDown(location, button)) return true; if (button == MouseButton.Right) { _isRightMouseButtonDown = true; _rightMouseButtonDownPos = location; Focus(); return true; } if (!ContainsFocus) { Focus(); return true; } return false; } /// public override bool OnMouseUp(Vector2 location, MouseButton button) { if (button == MouseButton.Right && _isRightMouseButtonDown) { _isRightMouseButtonDown = false; if (Vector2.Distance(ref location, ref _rightMouseButtonDownPos) < 4.0f) { if (!ContainsFocus) Focus(); var controlUnderMouse = GetChildAtRecursive(location); var mediaUnderMouse = controlUnderMouse; while (mediaUnderMouse != null && !(mediaUnderMouse is Media)) { mediaUnderMouse = mediaUnderMouse.Parent; } var menu = new ContextMenu.ContextMenu(); if (mediaUnderMouse is Media media) { media.OnTimelineShowContextMenu(menu, controlUnderMouse); if (media.PropertiesEditObject != null) { menu.AddButton("Edit media", () => ShowEditPopup(media.PropertiesEditObject, ref location, media.Track)); } } if (PropertiesEditObject != null) { menu.AddButton("Edit timeline", () => ShowEditPopup(PropertiesEditObject, ref location, this)); } menu.AddSeparator(); menu.AddButton("Reset zoom", () => Zoom = 1.0f); menu.AddButton("Show whole timeline", ShowWholeTimeline); OnShowContextMenu(menu); menu.Show(this, location); } } return base.OnMouseUp(location, button); } /// public override bool OnKeyDown(KeyboardKeys key) { if (base.OnKeyDown(key)) return true; switch (key) { case KeyboardKeys.ArrowLeft: OnSeek(CurrentFrame - 1); return true; case KeyboardKeys.ArrowRight: OnSeek(CurrentFrame + 1); return true; case KeyboardKeys.Home: OnSeek(0); return true; case KeyboardKeys.End: OnSeek(DurationFrames); return true; case KeyboardKeys.PageUp: if (GetNextKeyframeFrame(CurrentTime, out var nextKeyframeTime)) { OnSeek(nextKeyframeTime); return true; } break; case KeyboardKeys.PageDown: if (GetPreviousKeyframeFrame(CurrentTime, out var prevKeyframeTime)) { OnSeek(prevKeyframeTime); return true; } break; case KeyboardKeys.Spacebar: if (CanPlayPauseStop) { if (PlaybackState == PlaybackStates.Playing) OnPause(); else OnPlay(); return true; } break; } return false; } /// /// Shows the whole timeline. /// public void ShowWholeTimeline() { var viewWidth = Width; var timelineWidth = Duration * UnitsPerSecond * Zoom + 8 * StartOffset; _backgroundArea.ViewOffset = Vector2.Zero; Zoom = viewWidth / timelineWidth; } class PropertiesEditPopup : ContextMenuBase { private Timeline _timeline; private bool _isDirty; private byte[] _beforeData; private object _undoContext; public PropertiesEditPopup(Timeline timeline, object obj, object undoContext) { const float width = 280.0f; const float height = 160.0f; Size = new Vector2(width, height); var panel1 = new Panel(ScrollBars.Vertical) { Bounds = new Rectangle(0, 0.0f, width, height), Parent = this }; var editor = new CustomEditorPresenter(null); editor.Panel.AnchorPreset = AnchorPresets.HorizontalStretchTop; editor.Panel.IsScrollable = true; editor.Panel.Parent = panel1; editor.Modified += OnModified; editor.Select(obj); _timeline = timeline; if (timeline.Undo != null && undoContext != null) { _undoContext = undoContext; if (undoContext is Track track) _beforeData = EditTrackAction.CaptureData(track); else if (undoContext is Timeline) _beforeData = EditTimelineAction.CaptureData(timeline); } } private void OnModified() { _isDirty = true; } /// protected override void OnShow() { Focus(); base.OnShow(); } /// public override void Hide() { if (!Visible) return; Focus(null); if (_isDirty) { if (_beforeData != null) { if (_undoContext is Track track) { var after = EditTrackAction.CaptureData(track); if (!Utils.ArraysEqual(_beforeData, after)) _timeline.Undo.AddAction(new EditTrackAction(_timeline, track, _beforeData, after)); } else if (_undoContext is Timeline) { var after = EditTimelineAction.CaptureData(_timeline); if (!Utils.ArraysEqual(_beforeData, after)) _timeline.Undo.AddAction(new EditTimelineAction(_timeline, _beforeData, after)); } } _timeline.MarkAsEdited(); } base.Hide(); } /// public override bool OnKeyDown(KeyboardKeys key) { if (key == KeyboardKeys.Escape) { Hide(); return true; } return base.OnKeyDown(key); } /// public override void OnDestroy() { _timeline = null; _beforeData = null; _undoContext = null; base.OnDestroy(); } } class TracksPanelArea : Panel { private DragDropEffect _currentDragEffect = DragDropEffect.None; private Timeline _timeline; private bool _needSetup = true; public TracksPanelArea(Timeline timeline) : base(ScrollBars.Vertical) { _timeline = timeline; } /// public override DragDropEffect OnDragEnter(ref Vector2 location, DragData data) { var result = base.OnDragEnter(ref location, data); if (result == DragDropEffect.None) { if (_needSetup) { _needSetup = false; _timeline.SetupDragDrop(); } for (int i = 0; i < _timeline.DragHandlers.Count; i++) { var dragHelper = _timeline.DragHandlers[i].Helper; if (dragHelper.OnDragEnter(data)) { result = dragHelper.Effect; break; } } _currentDragEffect = result; } return result; } /// public override DragDropEffect OnDragMove(ref Vector2 location, DragData data) { var result = base.OnDragEnter(ref location, data); if (result == DragDropEffect.None) { result = _currentDragEffect; } return result; } /// public override void OnDragLeave() { _currentDragEffect = DragDropEffect.None; _timeline.DragHandlers.ForEach(x => x.Helper.OnDragLeave()); base.OnDragLeave(); } /// public override DragDropEffect OnDragDrop(ref Vector2 location, DragData data) { var result = base.OnDragDrop(ref location, data); if (result == DragDropEffect.None && _currentDragEffect != DragDropEffect.None) { for (int i = 0; i < _timeline.DragHandlers.Count; i++) { var e = _timeline.DragHandlers[i]; if (e.Helper.HasValidDrag) { e.Action(_timeline, e.Helper); } } } return result; } /// public override void Draw() { if (IsDragOver && _currentDragEffect != DragDropEffect.None) { var style = Style.Current; Render2D.FillRectangle(new Rectangle(Vector2.Zero, Size), style.BackgroundSelected * 0.4f); } base.Draw(); } /// public override void OnDestroy() { _timeline = null; base.OnDestroy(); } } /// /// Shows the timeline object editing popup. /// /// The object. /// The show location (in timeline space). /// The undo context object. protected virtual void ShowEditPopup(object obj, ref Vector2 location, object undoContext = null) { var popup = new PropertiesEditPopup(this, obj, undoContext); popup.Show(this, location); } /// /// Called when showing context menu to the user. Can be used to add custom buttons. /// /// The menu. protected virtual void OnShowContextMenu(ContextMenu.ContextMenu menu) { } /// public override void OnDestroy() { if (CameraCutThumbnailRenderer != null) { CameraCutThumbnailRenderer.Dispose(); CameraCutThumbnailRenderer = null; } // Clear references to the controls _splitter = null; _timeIntervalsHeader = null; _backgroundScroll = null; _background = null; _backgroundArea = null; _leftEdge = null; _rightEdge = null; _addTrackButton = null; _fpsComboBox = null; _viewButton = null; _fpsCustomValue = null; _tracksPanelArea = null; _tracksPanel = null; _playbackNavigation = null; _playbackStop = null; _playbackPlay = null; _noTracksLabel = null; _positionHandle = null; DragHandlers.Clear(); base.OnDestroy(); } } }