// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; using FlaxEditor.CustomEditors; using FlaxEditor.GUI.ContextMenu; using FlaxEditor.Scripting; using FlaxEngine; using FlaxEngine.GUI; using FlaxEngine.Json; using FlaxEngine.Utilities; namespace FlaxEditor.GUI { /// /// The generic keyframes animation editor control. /// /// [HideInEditor] public class KeyframesEditor : ContainerControl, IKeyframesEditor { /// /// A single keyframe. /// public struct Keyframe : IComparable, IComparable { /// /// The time of the keyframe. /// [EditorOrder(0), Limit(float.MinValue, float.MaxValue, 0.01f), Tooltip("The time of the keyframe.")] public float Time; /// /// The value of the keyframe. /// [EditorOrder(1), Limit(float.MinValue, float.MaxValue, 0.01f), Tooltip("The value of the keyframe.")] public object Value; /// /// Initializes a new instance of the struct. /// /// The time. /// The value. public Keyframe(float time, object value) { Time = time; Value = value; } /// public int CompareTo(object obj) { if (obj is Keyframe other) return Time > other.Time ? 1 : 0; return 1; } /// public int CompareTo(Keyframe other) { return Time > other.Time ? 1 : 0; } /// public override string ToString() { return Value?.ToString() ?? string.Empty; } } /// /// The keyframes contents container control. /// /// private class Contents : ContainerControl { private readonly KeyframesEditor _editor; internal bool _leftMouseDown; private bool _rightMouseDown; internal Float2 _leftMouseDownPos = Float2.Minimum; private Float2 _rightMouseDownPos = Float2.Minimum; internal Float2 _mousePos = Float2.Minimum; private Float2 _movingViewLastPos; internal bool _isMovingSelection; private bool _movedKeyframes; private float _movingSelectionStart; private float[] _movingSelectionOffsets; private Float2 _cmShowPos; /// /// Initializes a new instance of the class. /// /// The editor. public Contents(KeyframesEditor editor) { _editor = editor; } private void UpdateSelectionRectangle() { var selectionRect = Rectangle.FromPoints(_leftMouseDownPos, _mousePos); if (_editor.KeyframesEditorContext != null) _editor.KeyframesEditorContext.OnKeyframesSelection(_editor, this, selectionRect); else UpdateSelection(ref selectionRect); } internal void UpdateSelection(ref Rectangle selectionRect) { // Find controls to select for (int i = 0; i < Children.Count; i++) { if (Children[i] is KeyframePoint p) { p.IsSelected = p.Bounds.Intersects(ref selectionRect); } } } internal void OnMoveStart(Float2 location) { // Start moving selected nodes _isMovingSelection = true; _movedKeyframes = false; var viewRect = _editor._mainPanel.GetClientArea(); _movingSelectionStart = PointToKeyframes(location, ref viewRect).X; if (_movingSelectionOffsets == null || _movingSelectionOffsets.Length != _editor._keyframes.Count) _movingSelectionOffsets = new float[_editor._keyframes.Count]; for (int i = 0; i < _movingSelectionOffsets.Length; i++) _movingSelectionOffsets[i] = _editor._keyframes[i].Time - _movingSelectionStart; _editor.OnEditingStart(); } internal void OnMove(Float2 location) { var viewRect = _editor._mainPanel.GetClientArea(); var locationKeyframes = PointToKeyframes(location, ref viewRect); for (var i = 0; i < _editor._points.Count; i++) { var p = _editor._points[i]; if (p.IsSelected) { var k = _editor._keyframes[p.Index]; float minTime = p.Index != 0 ? _editor._keyframes[p.Index - 1].Time : float.MinValue; float maxTime = p.Index != _editor._keyframes.Count - 1 ? _editor._keyframes[p.Index + 1].Time : float.MaxValue; var offset = _movingSelectionOffsets[p.Index]; k.Time = locationKeyframes.X + offset; if (_editor.FPS.HasValue) { float fps = _editor.FPS.Value; k.Time = Mathf.Floor(k.Time * fps) / fps; } k.Time = Mathf.Clamp(k.Time, minTime, maxTime); // TODO: snapping keyframes to grid when moving _editor._keyframes[p.Index] = k; } _editor.UpdateKeyframes(); if (_editor.EnablePanning) { //_editor._mainPanel.ScrollViewTo(PointToParent(_editor._mainPanel, location)); } Cursor = CursorType.SizeAll; _movedKeyframes = true; } } internal void OnMoveEnd(Float2 location) { if (_movedKeyframes) { _editor.OnEdited(); _editor.OnEditingEnd(); _editor.UpdateKeyframes(); _movedKeyframes = false; } _isMovingSelection = false; } /// public override bool IntersectsContent(ref Float2 locationParent, out Float2 location) { // Pass all events location = PointFromParent(ref locationParent); return true; } /// public override void OnMouseEnter(Float2 location) { _mousePos = location; base.OnMouseEnter(location); } /// public override void OnMouseMove(Float2 location) { _mousePos = location; // Start moving selection if movement started from the keyframe if (_leftMouseDown && !_isMovingSelection && GetChildAt(_leftMouseDownPos) is KeyframePoint) { if (_editor.KeyframesEditorContext != null) _editor.KeyframesEditorContext.OnKeyframesMove(_editor, this, location, true, false); else OnMoveStart(location); } // Moving view if (_rightMouseDown) { // Calculate delta Float2 delta = location - _movingViewLastPos; if (delta.LengthSquared > 0.01f) { if (_editor.CustomViewPanning != null) delta = _editor.CustomViewPanning(delta); if (_editor.EnablePanning) { // Move view _editor.ViewOffset += delta * _editor.ViewScale; _movingViewLastPos = location; Cursor = CursorType.SizeAll; } else if (_editor.CustomViewPanning != null) Cursor = CursorType.SizeAll; } return; } // Moving selection else if (_isMovingSelection) { if (_editor.KeyframesEditorContext != null) _editor.KeyframesEditorContext.OnKeyframesMove(_editor, this, location, false, false); else OnMove(location); return; } // Selecting else if (_leftMouseDown) { UpdateSelectionRectangle(); return; } base.OnMouseMove(location); } /// public override void OnLostFocus() { // Clear flags and state if (_leftMouseDown) { _leftMouseDown = false; } if (_rightMouseDown) { _rightMouseDown = false; Cursor = CursorType.Default; } _isMovingSelection = false; base.OnLostFocus(); } /// public override bool OnMouseDown(Float2 location, MouseButton button) { if (base.OnMouseDown(location, button)) { // Clear flags _isMovingSelection = false; _rightMouseDown = false; _leftMouseDown = false; return true; } // Cache data _isMovingSelection = false; _mousePos = location; if (button == MouseButton.Left) { _leftMouseDown = true; _leftMouseDownPos = location; } if (button == MouseButton.Right) { _rightMouseDown = true; _rightMouseDownPos = location; _movingViewLastPos = location; } // Check if any node is under the mouse var underMouse = GetChildAt(location); if (underMouse is KeyframePoint keyframe) { if (_leftMouseDown) { if (Root.GetKey(KeyboardKeys.Control)) { // Toggle selection keyframe.IsSelected = !keyframe.IsSelected; } else if (Root.GetKey(KeyboardKeys.Shift)) { // Select range keyframe.IsSelected = true; int selectionStart = 0; for (; selectionStart < _editor._points.Count; selectionStart++) { if (_editor._points[selectionStart].IsSelected) break; } int selectionEnd = _editor._points.Count - 1; for (; selectionEnd > selectionStart; selectionEnd--) { if (_editor._points[selectionEnd].IsSelected) break; } selectionStart++; for (; selectionStart < selectionEnd; selectionStart++) _editor._points[selectionStart].IsSelected = true; } else if (!keyframe.IsSelected) { // Select node if (_editor.KeyframesEditorContext != null) _editor.KeyframesEditorContext.OnKeyframesDeselect(_editor); else _editor.ClearSelection(); keyframe.IsSelected = true; } StartMouseCapture(); Focus(); Tooltip?.Hide(); return true; } } else { if (_leftMouseDown) { // Start selecting StartMouseCapture(); if (_editor.KeyframesEditorContext != null) _editor.KeyframesEditorContext.OnKeyframesDeselect(_editor); else _editor.ClearSelection(); Focus(); return true; } if (_rightMouseDown) { // Start navigating StartMouseCapture(); Focus(); return true; } } Focus(); return true; } /// public override bool OnMouseUp(Float2 location, MouseButton button) { _mousePos = location; if (_leftMouseDown && button == MouseButton.Left) { _leftMouseDown = false; EndMouseCapture(); Cursor = CursorType.Default; // Moving keyframes if (_isMovingSelection) { if (_editor.KeyframesEditorContext != null) _editor.KeyframesEditorContext.OnKeyframesMove(_editor, this, location, false, true); else OnMoveEnd(location); } _isMovingSelection = false; _movedKeyframes = false; } if (_rightMouseDown && button == MouseButton.Right) { _rightMouseDown = false; EndMouseCapture(); Cursor = CursorType.Default; // Check if no move has been made at all if (Float2.Distance(ref location, ref _rightMouseDownPos) < 2.0f) { var selectionCount = _editor.SelectionCount; var point = GetChildAt(location) as KeyframePoint; if (selectionCount == 0 && point != null) { // Select node selectionCount = 1; point.IsSelected = true; } var viewRect = _editor._mainPanel.GetClientArea(); _cmShowPos = PointToKeyframes(location, ref viewRect); var cm = new ContextMenu.ContextMenu(); cm.AddButton("Add keyframe", () => _editor.AddKeyframe(_cmShowPos)).Enabled = _editor.Keyframes.Count < _editor.MaxKeyframes && _editor.DefaultValue != null; if (selectionCount > 0 && _editor.EnableKeyframesValueEdit) { cm.AddButton(selectionCount == 1 ? "Edit keyframe" : "Edit keyframes", () => _editor.EditKeyframes(this, location)); } var totalSelectionCount = _editor.KeyframesEditorContext?.OnKeyframesSelectionCount() ?? selectionCount; if (totalSelectionCount > 0) { cm.AddButton(totalSelectionCount == 1 ? "Remove keyframe" : "Remove keyframes", _editor.RemoveKeyframes); cm.AddButton(totalSelectionCount == 1 ? "Copy keyframe" : "Copy keyframes", () => _editor.CopyKeyframes(point)); } cm.AddButton("Paste keyframes", () => KeyframesEditorUtils.Paste(_editor, point?.Time ?? _cmShowPos.X)).Enabled = KeyframesEditorUtils.CanPaste(); cm.AddSeparator(); if (_editor.EnableKeyframesValueEdit) cm.AddButton("Edit all keyframes", () => _editor.EditAllKeyframes(this, location)); cm.AddButton("Select all keyframes", _editor.SelectAll).Enabled = _editor._points.Count > 0; cm.AddButton("Copy all keyframes", () => { _editor.SelectAll(); _editor.CopyKeyframes(point); }).Enabled = _editor.DefaultValue != null; if (_editor.EnableZoom && _editor.EnablePanning) { cm.AddSeparator(); cm.AddButton("Show whole keyframes", _editor.ShowWholeKeyframes); cm.AddButton("Reset view", _editor.ResetView); } cm.Show(this, location); } } if (base.OnMouseUp(location, button)) { // Clear flags _rightMouseDown = false; _leftMouseDown = false; return true; } return true; } /// public override bool OnMouseWheel(Float2 location, float delta) { if (base.OnMouseWheel(location, delta)) return true; // Zoom in/out if (_editor.EnableZoom && IsMouseOver && !_leftMouseDown) { // TODO: preserve the view center point for easier zooming _editor.ViewScale += delta * 0.1f; return true; } return false; } /// protected override void SetScaleInternal(ref Float2 scale) { base.SetScaleInternal(ref scale); _editor.UpdateKeyframes(); } /// /// Converts the input point from editor contents control space into the keyframes time/value coordinates. /// /// The point. /// The keyframes contents area bounds. /// The result. private Float2 PointToKeyframes(Float2 point, ref Rectangle keyframesContentAreaBounds) { // Contents -> Keyframes return new Float2( (point.X + Location.X) / UnitsPerSecond, (point.Y + Location.Y - keyframesContentAreaBounds.Height) / -UnitsPerSecond ); } } /// /// The single keyframe control. /// private class KeyframePoint : Control { /// /// The parent keyframes editor. /// public KeyframesEditor Editor; /// /// The keyframe index. /// public int Index; /// /// Flag for selected keyframes. /// public bool IsSelected; /// /// Gets the time of the keyframe. /// public float Time => Editor._keyframes[Index].Time; /// public override void Draw() { var rect = new Rectangle(Float2.Zero, Size); var color = Color.Gray; if (IsSelected) color = Editor.ContainsFocus ? Color.YellowGreen : Color.Lerp(Color.Gray, Color.YellowGreen, 0.4f); if (IsMouseOver) color *= 1.1f; Render2D.FillRectangle(rect, color); } /// protected override void OnLocationChanged() { base.OnLocationChanged(); UpdateTooltip(); } /// public override bool OnMouseDoubleClick(Float2 location, MouseButton button) { if (base.OnMouseDoubleClick(location, button)) return true; if (button == MouseButton.Left) { Editor.EditKeyframes(this, location, new List { Index }); return true; } return false; } /// /// Updates the tooltip. /// public void UpdateTooltip() { var k = Editor._keyframes[Index]; TooltipText = string.Format("Time: {0}, Value: {1}", k.Time, k.Value); } } /// /// The timeline units per second (on time axis). /// public static readonly float UnitsPerSecond = 100.0f; /// /// The keyframes size. /// private static readonly Float2 KeyframesSize = new Float2(7.0f); private Contents _contents; private Panel _mainPanel; private readonly List _points = new List(); private bool _refreshAfterEdit; private Popup _popup; private float? _fps; /// /// The keyframes collection. /// protected readonly List _keyframes = new List(); /// /// Occurs when keyframes collection gets changed (keyframe added or removed). /// public event Action KeyframesChanged; /// /// Gets the keyframes collection (read-only). /// public IReadOnlyList Keyframes => _keyframes; /// /// Gets or sets the view offset (via scroll bars). /// public Float2 ViewOffset { get => _mainPanel.ViewOffset; set => _mainPanel.ViewOffset = value; } /// /// Gets or sets the view scale. /// public Float2 ViewScale { get => _contents.Scale; set => _contents.Scale = Float2.Clamp(value, new Float2(0.0001f), new Float2(1000.0f)); } /// /// Occurs when keyframes data gets edited. /// public event Action Edited; /// /// Occurs when keyframes data editing starts (via UI). /// public event Action EditingStart; /// /// Occurs when keyframes data editing ends (via UI). /// public event Action EditingEnd; /// /// The function for custom view panning. Gets input movement delta (in keyframes editor control space) and returns the renaming input delta to process by keyframes editor itself. /// public Func CustomViewPanning; /// /// The maximum amount of keyframes to use. /// public int MaxKeyframes = ushort.MaxValue; /// /// True if enable view zooming. Otherwise user won't be able to zoom in or out. /// public bool EnableZoom = true; /// /// True if enable view panning. Otherwise user won't be able to move the view area. /// public bool EnablePanning = true; /// /// True if enable keyframes values editing (via popup). /// public bool EnableKeyframesValueEdit = true; /// /// True if enable view panning. Otherwise user won't be able to move the view area. /// public bool Enable = true; /// /// Gets a value indicating whether user is editing the keyframes. /// public bool IsUserEditing => _popup != null || _contents._leftMouseDown; /// /// Gets or sets the scroll bars usage. /// public ScrollBars ScrollBars { get => _mainPanel.ScrollBars; set => _mainPanel.ScrollBars = value; } /// /// The default value. /// public object DefaultValue; /// /// The amount of frames per second of the keyframes animation (optional). Can be used to restrict the keyframes time values to the given time quantization rate. /// public float? FPS { get => _fps; set { if (_fps.HasValue == value.HasValue && (!value.HasValue || Mathf.NearEqual(_fps.Value, value.Value))) return; _fps = value; UpdateFPS(); } } /// /// Initializes a new instance of the class. /// public KeyframesEditor() { _mainPanel = new Panel(ScrollBars.Both) { ScrollMargin = new Margin(150.0f), AlwaysShowScrollbars = true, AnchorPreset = AnchorPresets.StretchAll, Offsets = Margin.Zero, Parent = this }; _contents = new Contents(this) { ClipChildren = false, AutoFocus = false, Parent = _mainPanel }; UpdateKeyframes(); } private void OnEdited() { Edited?.Invoke(); } private void OnEditingStart() { EditingStart?.Invoke(); } private void OnEditingEnd() { EditingEnd?.Invoke(); } /// /// Evaluates the keyframe value at the specified time. /// /// The time to evaluate the keyframe value. /// The evaluated value. public object Evaluate(float time) { if (_keyframes.Count == 0) return DefaultValue; // Find the keyframe at time int start = 0; int searchLength = _keyframes.Count; while (searchLength > 0) { int half = searchLength >> 1; int mid = start + half; if (time < _keyframes[mid].Time) { searchLength = half; } else { start = mid + 1; searchLength -= (half + 1); } } int leftKey = Mathf.Max(0, start - 1); return _keyframes[leftKey].Value; } /// /// Resets the keyframes collection. /// public void ResetKeyframes() { if (_keyframes.Count == 0) return; _keyframes.Clear(); UpdateFPS(); OnKeyframesChanged(); } /// /// Sets the keyframes collection. /// /// The keyframes. public void SetKeyframes(IEnumerable keyframes) { if (keyframes == null) throw new ArgumentNullException(nameof(keyframes)); var keyframesArray = keyframes as Keyframe[] ?? keyframes.ToArray(); if (_keyframes.SequenceEqual(keyframesArray)) return; if (keyframesArray.Length > MaxKeyframes) { var tmp = keyframesArray; keyframesArray = new Keyframe[MaxKeyframes]; Array.Copy(tmp, keyframesArray, MaxKeyframes); } _keyframes.Clear(); _keyframes.AddRange(keyframesArray); _keyframes.Sort((a, b) => a.Time.CompareTo(b.Time)); UpdateFPS(); OnKeyframesChanged(); } private void UpdateFPS() { if (FPS.HasValue) { float fps = FPS.Value; for (int i = 0; i < _keyframes.Count; i++) { var k = _keyframes[i]; k.Time = Mathf.Floor(k.Time * fps) / fps; _keyframes[i] = k; } } } /// /// Called when keyframes collection gets changed (keyframe added or removed). /// protected virtual void OnKeyframesChanged() { while (_points.Count > _keyframes.Count) { var last = _points.Count - 1; _points[last].Dispose(); _points.RemoveAt(last); } while (_points.Count < _keyframes.Count) { _points.Add(new KeyframePoint { AutoFocus = false, Size = KeyframesSize, Editor = this, Index = _points.Count, Parent = _contents, }); _refreshAfterEdit = true; } UpdateKeyframes(); KeyframesChanged?.Invoke(); } /// /// Adds the new keyframe. /// /// The keyframe to add. public void AddKeyframe(Keyframe k) { var eps = Mathf.Epsilon; if (FPS.HasValue) { float fps = FPS.Value; k.Time = Mathf.Floor(k.Time * fps) / fps; eps = 1.0f / fps; } int pos = 0; while (pos < _keyframes.Count && _keyframes[pos].Time < k.Time) pos++; if (_keyframes.Count > pos && Mathf.Abs(_keyframes[pos].Time - k.Time) < eps) _keyframes[pos] = k; else _keyframes.Insert(pos, k); OnKeyframesChanged(); OnEdited(); } /// /// Sets the existing keyframe value as boxed value. /// /// The keyframe index. /// The keyframe value. public void SetKeyframe(int index, object value) { var k = _keyframes[index]; k.Value = value; _keyframes[index] = k; OnKeyframesChanged(); OnEdited(); } private void AddKeyframe(Float2 keyframesPos) { var k = new Keyframe { Time = keyframesPos.X, Value = DefaultValue, }; OnEditingStart(); AddKeyframe(k); OnEditingEnd(); } class Popup : ContextMenuBase { private CustomEditorPresenter _presenter; private KeyframesEditor _editor; private List _keyframeIndices; private bool _isDirty; public Popup(KeyframesEditor editor, object[] selection, List keyframeIndices = null, float height = 140.0f) : this(editor, height) { _presenter.Select(selection); _presenter.OpenAllGroups(); _keyframeIndices = keyframeIndices; if (keyframeIndices != null && selection.Length != keyframeIndices.Count) throw new Exception(); } private Popup(KeyframesEditor editor, float height = 140.0f) { _editor = editor; const float width = 280.0f; Size = new Float2(width, height); var panel1 = new Panel(ScrollBars.Vertical) { Bounds = new Rectangle(0, 0.0f, width, height), Parent = this }; _presenter = new CustomEditorPresenter(null); _presenter.Panel.AnchorPreset = AnchorPresets.HorizontalStretchTop; _presenter.Panel.IsScrollable = true; _presenter.Panel.Parent = panel1; _presenter.Modified += OnModified; } private void OnModified() { if (!_isDirty) { _editor.OnEditingStart(); } _isDirty = true; if (_keyframeIndices != null) { for (int i = 0; i < _presenter.SelectionCount; i++) { var keyframe = (Keyframe)_presenter.Selection[i]; var index = _keyframeIndices[i]; _editor._keyframes[index] = keyframe; } } else if (_presenter.Selection[0] is AllKeyframesProxy proxy) { _editor.SetKeyframes(proxy.Keyframes); } _editor.UpdateFPS(); _editor.UpdateKeyframes(); } /// protected override void OnShow() { Focus(); base.OnShow(); } /// public override void Hide() { if (!Visible) return; Focus(null); if (_isDirty) { _editor.OnEdited(); _editor.OnEditingEnd(); } if (_editor._popup == this) _editor._popup = null; _presenter = null; _keyframeIndices = null; _editor = null; base.Hide(); } /// public override bool OnKeyDown(KeyboardKeys key) { if (base.OnKeyDown(key)) return true; if (key == KeyboardKeys.Escape) { Hide(); return true; } return false; } /// public override void OnDestroy() { _editor = null; base.OnDestroy(); } } sealed class AllKeyframesProxy { [HideInEditor, NoSerialize] public KeyframesEditor Editor; [Collection(CanReorderItems = false, Spacing = 10)] public Keyframe[] Keyframes; } private void EditAllKeyframes(Control control, Float2 pos) { _popup = new Popup(this, new object[] { new AllKeyframesProxy { Editor = this, Keyframes = _keyframes.ToArray(), } }, null, 400.0f); _popup.Show(control, pos); } private void EditKeyframes(Control control, Float2 pos) { var keyframeIndices = new List(); for (int i = 0; i < _points.Count; i++) { var p = _points[i]; if (!p.IsSelected || keyframeIndices.Contains(p.Index)) continue; keyframeIndices.Add(p.Index); } EditKeyframes(control, pos, keyframeIndices); } private void EditKeyframes(Control control, Float2 pos, List keyframeIndices) { var selection = new object[keyframeIndices.Count]; for (int i = 0; i < keyframeIndices.Count; i++) selection[i] = _keyframes[keyframeIndices[i]]; _popup = new Popup(this, selection, keyframeIndices); _popup.Show(control, pos); } private void RemoveKeyframes() { if (KeyframesEditorContext != null) KeyframesEditorContext.OnKeyframesDelete(this); else RemoveKeyframesInner(); } private void CopyKeyframes(KeyframePoint point = null) { float? timeOffset = null; if (point != null) { timeOffset = -point.Time; } else { for (int i = 0; i < _points.Count; i++) { if (_points[i].IsSelected) { timeOffset = -_points[i].Time; break; } } } KeyframesEditorUtils.Copy(this, timeOffset); } private void RemoveKeyframesInner() { if (SelectionCount == 0) return; var keyframes = new Dictionary(_keyframes.Count); for (int i = 0; i < _points.Count; i++) { var p = _points[i]; if (!p.IsSelected) { keyframes[p.Index] = _keyframes[p.Index]; } else { p.IsSelected = false; } } OnEditingStart(); _keyframes.Clear(); _keyframes.AddRange(keyframes.Values); OnKeyframesChanged(); OnEdited(); OnEditingEnd(); } /// /// Shows the whole keyframes UI. /// public void ShowWholeKeyframes() { ViewScale = _mainPanel.Size / _contents.Size; ViewOffset = -_mainPanel.ControlsBounds.Location; UpdateKeyframes(); } /// /// Resets the view. /// public void ResetView() { ViewScale = Float2.One; ViewOffset = Float2.Zero; UpdateKeyframes(); } /// /// Updates the keyframes positioning. /// public virtual void UpdateKeyframes() { if (_points.Count == 0) { // No keyframes _contents.Bounds = Rectangle.Empty; return; } _mainPanel.LockChildrenRecursive(); // Place keyframes var viewScale = ViewScale; for (int i = 0; i < _points.Count; i++) { var p = _points[i]; var k = _keyframes[p.Index]; p.Size = new Float2(4.0f / viewScale.X, Height - 2.0f); p.Location = new Float2(k.Time * UnitsPerSecond - p.Width * 0.5f, 1.0f); p.UpdateTooltip(); } // Calculate bounds var bounds = _points[0].Bounds; for (var i = 1; i < _points.Count; i++) { bounds = Rectangle.Union(bounds, _points[i].Bounds); } // Adjust contents bounds to fill the keyframes area if (EnablePanning && !_contents._isMovingSelection) { _contents.Bounds = bounds; } // Offset the keyframes (parent container changed its location) var posOffset = _contents.Location; for (var i = 0; i < _points.Count; i++) { _points[i].Location -= posOffset; } _mainPanel.UnlockChildrenRecursive(); _mainPanel.PerformLayout(); } private int SelectionCount { get { int result = 0; for (int i = 0; i < _points.Count; i++) if (_points[i].IsSelected) result++; return result; } } /// /// Clears the selection. /// public void ClearSelection() { for (int i = 0; i < _points.Count; i++) { _points[i].IsSelected = false; } } /// /// Selects all keyframes. /// public void SelectAll() { for (int i = 0; i < _points.Count; i++) { _points[i].IsSelected = true; } } /// public override void Draw() { // Hack to refresh UI after keyframes edit if (_refreshAfterEdit) { _refreshAfterEdit = false; UpdateKeyframes(); } var style = Style.Current; var rect = new Rectangle(Float2.Zero, Size); // Draw selection rectangle if (_contents._leftMouseDown && !_contents._isMovingSelection) { var selectionRect = Rectangle.FromPoints ( _mainPanel.PointToParent(_contents.PointToParent(_contents._leftMouseDownPos)), _mainPanel.PointToParent(_contents.PointToParent(_contents._mousePos)) ); Render2D.FillRectangle(selectionRect, Color.Orange * 0.4f); Render2D.DrawRectangle(selectionRect, Color.Orange); } base.Draw(); // Draw border if (ContainsFocus) { Render2D.DrawRectangle(rect, style.BackgroundSelected); } } /// protected override void OnSizeChanged() { base.OnSizeChanged(); UpdateKeyframes(); } /// public override bool OnKeyDown(KeyboardKeys key) { if (base.OnKeyDown(key)) return true; switch (key) { case KeyboardKeys.Delete: RemoveKeyframes(); return true; case KeyboardKeys.A: if (Root.GetKey(KeyboardKeys.Control)) { SelectAll(); return true; } break; case KeyboardKeys.C: if (Root.GetKey(KeyboardKeys.Control)) { CopyKeyframes(); return true; } break; case KeyboardKeys.V: if (Root.GetKey(KeyboardKeys.Control)) { KeyframesEditorUtils.Paste(this); return true; } break; } return false; } /// public override void OnDestroy() { // Clear references to the controls _mainPanel = null; _contents = null; _popup = null; // Cleanup _points.Clear(); _keyframes.Clear(); KeyframesEditorContext = null; base.OnDestroy(); } /// public IKeyframesEditorContext KeyframesEditorContext { get; set; } /// public void OnKeyframesDeselect(IKeyframesEditor editor) { ClearSelection(); } /// public void OnKeyframesSelection(IKeyframesEditor editor, ContainerControl control, Rectangle selection) { if (_keyframes.Count == 0) return; var selectionRect = Rectangle.FromPoints(_contents.PointFromParent(control, selection.UpperLeft), _contents.PointFromParent(control, selection.BottomRight)); _contents.UpdateSelection(ref selectionRect); } /// public int OnKeyframesSelectionCount() { return SelectionCount; } /// public void OnKeyframesDelete(IKeyframesEditor editor) { RemoveKeyframesInner(); } /// public void OnKeyframesMove(IKeyframesEditor editor, ContainerControl control, Float2 location, bool start, bool end) { if (SelectionCount == 0) return; location = _contents.PointFromParent(control, location); if (start) _contents.OnMoveStart(location); else if (end) _contents.OnMoveEnd(location); else _contents.OnMove(location); } /// public void OnKeyframesCopy(IKeyframesEditor editor, float? timeOffset, StringBuilder data) { if (SelectionCount == 0 || DefaultValue == null) return; var offset = timeOffset ?? 0.0f; data.AppendLine(KeyframesEditorUtils.CopyPrefix); data.AppendLine(DefaultValue.GetType().FullName); for (int i = 0; i < _keyframes.Count; i++) { if (!_points[i].IsSelected) continue; var k = _keyframes[i]; data.AppendLine((k.Time + offset).ToString(CultureInfo.InvariantCulture)); data.AppendLine(JsonSerializer.Serialize(k.Value).RemoveNewLine()); } } /// public void OnKeyframesPaste(IKeyframesEditor editor, float? timeOffset, string[] datas, ref int index) { if (DefaultValue == null) return; if (index == -1) { if (editor == this) index = 0; else return; } else if (index >= datas.Length) return; var data = datas[index]; var offset = timeOffset ?? 0.0f; var eps = FPS.HasValue ? 0.5f / FPS.Value : Mathf.Epsilon; try { var lines = data.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries); if (lines.Length < 3 || lines.Length % 2 == 0) return; var type = TypeUtils.GetManagedType(lines[0]); if (type == null) throw new Exception($"Unknown type {lines[0]}."); if (type != DefaultValue.GetType()) throw new Exception($"Mismatching keyframes data type {type.FullName} when pasting into {DefaultValue.GetType().FullName}."); var count = (lines.Length - 1) / 2; var modified = false; index++; for (int i = 0; i < count; i++) { var k = new Keyframe { Time = float.Parse(lines[i * 2 + 1], CultureInfo.InvariantCulture) + offset, Value = JsonSerializer.Deserialize(lines[i * 2 + 2], type), }; if (FPS.HasValue) { float fps = FPS.Value; k.Time = Mathf.Floor(k.Time * fps) / fps; } int pos = 0; while (pos < _keyframes.Count && _keyframes[pos].Time < k.Time) pos++; if (_keyframes.Count > pos && Mathf.Abs(_keyframes[pos].Time - k.Time) < eps) { // Skip if the keyframe value won't change if (JsonSerializer.ValueEquals(_keyframes[pos].Value, k.Value)) continue; } if (!modified) { modified = true; OnEditingStart(); } AddKeyframe(k); _points[pos].IsSelected = true; } if (modified) OnEditingEnd(); } catch (Exception ex) { Editor.LogWarning("Failed to paste keyframes."); Editor.LogWarning(ex.Message); Editor.LogWarning(ex); } } /// public void OnKeyframesGet(string trackName, Action get) { for (int i = 0; i < _keyframes.Count; i++) { var k = _keyframes[i]; get(trackName, k.Time, k); } } /// public void OnKeyframesSet(List> keyframes) { OnEditingStart(); _keyframes.Clear(); if (keyframes != null) { foreach (var e in keyframes) { var k = (Keyframe)e.Value; _keyframes.Add(new Keyframe(e.Key, k.Value)); } } OnKeyframesChanged(); OnEdited(); OnEditingEnd(); } } }