Files
FlaxEngine/Source/Editor/GUI/Timeline/GUI/KeyframesEditor.cs

1494 lines
50 KiB
C#

// Copyright (c) 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.Options;
using FlaxEngine;
using FlaxEngine.GUI;
using FlaxEngine.Json;
using FlaxEngine.Utilities;
namespace FlaxEditor.GUI
{
/// <summary>
/// The generic keyframes animation editor control.
/// </summary>
/// <seealso cref="FlaxEngine.GUI.ContainerControl" />
[HideInEditor]
public class KeyframesEditor : ContainerControl, IKeyframesEditor
{
/// <summary>
/// A single keyframe.
/// </summary>
public struct Keyframe : IComparable, IComparable<Keyframe>
{
/// <summary>
/// The time of the keyframe.
/// </summary>
[EditorOrder(0), Limit(float.MinValue, float.MaxValue, 0.01f), Tooltip("The time of the keyframe.")]
public float Time;
/// <summary>
/// The value of the keyframe.
/// </summary>
[EditorOrder(1), Limit(float.MinValue, float.MaxValue, 0.01f), Tooltip("The value of the keyframe.")]
public object Value;
/// <summary>
/// Initializes a new instance of the <see cref="Keyframe"/> struct.
/// </summary>
/// <param name="time">The time.</param>
/// <param name="value">The value.</param>
public Keyframe(float time, object value)
{
Time = time;
Value = value;
}
/// <inheritdoc />
public int CompareTo(object obj)
{
if (obj is Keyframe other)
return Time > other.Time ? 1 : 0;
return 1;
}
/// <inheritdoc />
public int CompareTo(Keyframe other)
{
return Time > other.Time ? 1 : 0;
}
/// <inheritdoc />
public override string ToString()
{
return Value?.ToString() ?? string.Empty;
}
}
/// <summary>
/// The keyframes contents container control.
/// </summary>
/// <seealso cref="FlaxEngine.GUI.ContainerControl" />
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;
/// <summary>
/// Initializes a new instance of the <see cref="Contents"/> class.
/// </summary>
/// <param name="editor">The editor.</param>
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(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;
}
/// <inheritdoc />
public override bool IntersectsContent(ref Float2 locationParent, out Float2 location)
{
// Pass all events
location = PointFromParent(ref locationParent);
return true;
}
/// <inheritdoc />
public override void OnMouseEnter(Float2 location)
{
_mousePos = location;
base.OnMouseEnter(location);
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
public override void OnLostFocus()
{
// Clear flags and state
if (_leftMouseDown)
{
_leftMouseDown = false;
}
if (_rightMouseDown)
{
_rightMouseDown = false;
Cursor = CursorType.Default;
}
_isMovingSelection = false;
base.OnLostFocus();
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
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(location, _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("Deselect all keyframes", _editor.DeselectAll).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;
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
protected override void SetScaleInternal(ref Float2 scale)
{
base.SetScaleInternal(ref scale);
_editor.UpdateKeyframes();
}
/// <summary>
/// Converts the input point from editor contents control space into the keyframes time/value coordinates.
/// </summary>
/// <param name="point">The point.</param>
/// <param name="keyframesContentAreaBounds">The keyframes contents area bounds.</param>
/// <returns>The result.</returns>
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
);
}
}
/// <summary>
/// The single keyframe control.
/// </summary>
private class KeyframePoint : Control
{
/// <summary>
/// The parent keyframes editor.
/// </summary>
public KeyframesEditor Editor;
/// <summary>
/// The keyframe index.
/// </summary>
public int Index;
/// <summary>
/// Flag for selected keyframes.
/// </summary>
public bool IsSelected;
/// <summary>
/// Gets the time of the keyframe.
/// </summary>
public float Time => Editor._keyframes[Index].Time;
/// <inheritdoc />
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);
}
/// <inheritdoc />
protected override void OnLocationChanged()
{
base.OnLocationChanged();
UpdateTooltip();
}
/// <inheritdoc />
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<int> { Index });
return true;
}
return false;
}
/// <summary>
/// Updates the tooltip.
/// </summary>
public void UpdateTooltip()
{
var k = Editor._keyframes[Index];
TooltipText = string.Format("Time: {0}, Value: {1}", k.Time, k.Value);
}
}
/// <summary>
/// The timeline units per second (on time axis).
/// </summary>
public static readonly float UnitsPerSecond = 100.0f;
/// <summary>
/// The keyframes size.
/// </summary>
private static readonly Float2 KeyframesSize = new Float2(7.0f);
private Contents _contents;
private Panel _mainPanel;
private readonly List<KeyframePoint> _points = new List<KeyframePoint>();
private bool _refreshAfterEdit;
private Popup _popup;
private float? _fps;
/// <summary>
/// The keyframes collection.
/// </summary>
protected readonly List<Keyframe> _keyframes = new List<Keyframe>();
/// <summary>
/// Occurs when keyframes collection gets changed (keyframe added or removed).
/// </summary>
public event Action KeyframesChanged;
/// <summary>
/// Gets the keyframes collection (read-only).
/// </summary>
public IReadOnlyList<Keyframe> Keyframes => _keyframes;
/// <summary>
/// Gets or sets the view offset (via scroll bars).
/// </summary>
public Float2 ViewOffset
{
get => _mainPanel.ViewOffset;
set => _mainPanel.ViewOffset = value;
}
/// <summary>
/// Gets or sets the view scale.
/// </summary>
public Float2 ViewScale
{
get => _contents.Scale;
set => _contents.Scale = Float2.Clamp(value, new Float2(0.0001f), new Float2(1000.0f));
}
/// <summary>
/// Occurs when keyframes data gets edited.
/// </summary>
public event Action Edited;
/// <summary>
/// Occurs when keyframes data editing starts (via UI).
/// </summary>
public event Action EditingStart;
/// <summary>
/// Occurs when keyframes data editing ends (via UI).
/// </summary>
public event Action EditingEnd;
/// <summary>
/// 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.
/// </summary>
public Func<Float2, Float2> CustomViewPanning;
/// <summary>
/// The maximum amount of keyframes to use.
/// </summary>
public int MaxKeyframes = ushort.MaxValue;
/// <summary>
/// True if enable view zooming. Otherwise user won't be able to zoom in or out.
/// </summary>
public bool EnableZoom = true;
/// <summary>
/// True if enable view panning. Otherwise user won't be able to move the view area.
/// </summary>
public bool EnablePanning = true;
/// <summary>
/// True if enable keyframes values editing (via popup).
/// </summary>
public bool EnableKeyframesValueEdit = true;
/// <summary>
/// True if enable view panning. Otherwise user won't be able to move the view area.
/// </summary>
public bool Enable = true;
/// <summary>
/// Gets a value indicating whether user is editing the keyframes.
/// </summary>
public bool IsUserEditing => _popup != null || _contents._leftMouseDown;
/// <summary>
/// Gets or sets the scroll bars usage.
/// </summary>
public ScrollBars ScrollBars
{
get => _mainPanel.ScrollBars;
set => _mainPanel.ScrollBars = value;
}
/// <summary>
/// The default value.
/// </summary>
public object DefaultValue;
/// <summary>
/// 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.
/// </summary>
public float? FPS
{
get => _fps;
set
{
if (_fps.HasValue == value.HasValue && (!value.HasValue || Mathf.NearEqual(_fps.Value, value.Value)))
return;
_fps = value;
UpdateFPS();
}
}
/// <summary>
/// Initializes a new instance of the <see cref="KeyframesEditor"/> class.
/// </summary>
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();
}
/// <summary>
/// Evaluates the keyframe value at the specified time.
/// </summary>
/// <param name="time">The time to evaluate the keyframe value.</param>
/// <returns>The evaluated value.</returns>
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;
}
/// <summary>
/// Resets the keyframes collection.
/// </summary>
public void ResetKeyframes()
{
if (_keyframes.Count == 0)
return;
_keyframes.Clear();
UpdateFPS();
OnKeyframesChanged();
}
/// <summary>
/// Sets the keyframes collection.
/// </summary>
/// <param name="keyframes">The keyframes.</param>
public void SetKeyframes(IEnumerable<Keyframe> 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;
}
}
}
/// <summary>
/// Called when keyframes collection gets changed (keyframe added or removed).
/// </summary>
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();
}
/// <summary>
/// Adds the new keyframe.
/// </summary>
/// <param name="k">The keyframe to add.</param>
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();
}
/// <summary>
/// Sets the existing keyframe value as boxed value.
/// </summary>
/// <param name="index">The keyframe index.</param>
/// <param name="value">The keyframe value.</param>
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 = Utilities.Utils.CloneValue(DefaultValue),
};
OnEditingStart();
AddKeyframe(k);
OnEditingEnd();
}
class Popup : ContextMenuBase
{
private CustomEditorPresenter _presenter;
private KeyframesEditor _editor;
private List<int> _keyframeIndices;
private bool _isDirty;
public Popup(KeyframesEditor editor, object[] selection, List<int> 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();
}
/// <inheritdoc />
protected override void OnShow()
{
Focus();
base.OnShow();
}
/// <inheritdoc />
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();
}
/// <inheritdoc />
public override bool OnKeyDown(KeyboardKeys key)
{
if (base.OnKeyDown(key))
return true;
if (key == KeyboardKeys.Escape)
{
Hide();
return true;
}
return false;
}
/// <inheritdoc />
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<int>();
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<int> 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<int, Keyframe>(_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();
}
/// <summary>
/// Shows the whole keyframes UI.
/// </summary>
public void ShowWholeKeyframes()
{
ViewScale = _mainPanel.Size / _contents.Size;
ViewOffset = -_mainPanel.ControlsBounds.Location;
UpdateKeyframes();
}
/// <summary>
/// Resets the view.
/// </summary>
public void ResetView()
{
ViewScale = Float2.One;
ViewOffset = Float2.Zero;
UpdateKeyframes();
}
/// <summary>
/// Updates the keyframes positioning.
/// </summary>
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;
}
}
/// <summary>
/// Clears the selection.
/// </summary>
public void ClearSelection()
{
for (int i = 0; i < _points.Count; i++)
{
_points[i].IsSelected = false;
}
}
private void BulkSelectUpdate(bool select = true)
{
for (int i = 0; i < _points.Count; i++)
{
_points[i].IsSelected = select;
}
}
/// <summary>
/// Selects all keyframes.
/// </summary>
public void SelectAll()
{
BulkSelectUpdate(true);
}
/// <summary>
/// Deselects all keyframes.
/// </summary>
public void DeselectAll()
{
BulkSelectUpdate(false);
}
/// <inheritdoc />
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, style.Selection);
Render2D.DrawRectangle(selectionRect, style.SelectionBorder);
}
base.Draw();
// Draw border
if (ContainsFocus)
{
Render2D.DrawRectangle(rect, style.BackgroundSelected);
}
}
/// <inheritdoc />
protected override void OnSizeChanged()
{
base.OnSizeChanged();
UpdateKeyframes();
}
/// <inheritdoc />
public override bool OnKeyDown(KeyboardKeys key)
{
if (base.OnKeyDown(key))
return true;
InputOptions options = Editor.Instance.Options.Options.Input;
if (options.SelectAll.Process(this))
{
SelectAll();
return true;
}
else if (options.DeselectAll.Process(this))
{
DeselectAll();
return true;
}
else if (options.Delete.Process(this))
{
RemoveKeyframes();
return true;
}
else if (options.Copy.Process(this))
{
CopyKeyframes();
return true;
}
else if (options.Paste.Process(this))
{
KeyframesEditorUtils.Paste(this);
return true;
}
return false;
}
/// <inheritdoc />
public override void OnDestroy()
{
// Clear references to the controls
_mainPanel = null;
_contents = null;
_popup = null;
// Cleanup
_points.Clear();
_keyframes.Clear();
KeyframesEditorContext = null;
base.OnDestroy();
}
/// <inheritdoc />
public IKeyframesEditorContext KeyframesEditorContext { get; set; }
/// <inheritdoc />
public void OnKeyframesDeselect(IKeyframesEditor editor)
{
ClearSelection();
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
public int OnKeyframesSelectionCount()
{
return SelectionCount;
}
/// <inheritdoc />
public void OnKeyframesDelete(IKeyframesEditor editor)
{
RemoveKeyframesInner();
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
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());
}
}
/// <inheritdoc />
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);
}
}
/// <inheritdoc />
public void OnKeyframesGet(string trackName, Action<string, float, object> get)
{
for (int i = 0; i < _keyframes.Count; i++)
{
var k = _keyframes[i];
get(trackName, k.Time, k);
}
}
/// <inheritdoc />
public void OnKeyframesSet(List<KeyValuePair<float, object>> 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();
}
}
}