Skip to content

Commit

Permalink
Add copy/paste feature to keyframes and curves editors
Browse files Browse the repository at this point in the history
  • Loading branch information
mafiesto4 committed Sep 1, 2021
1 parent d062601 commit 0d5fa3e
Show file tree
Hide file tree
Showing 12 changed files with 519 additions and 15 deletions.
21 changes: 19 additions & 2 deletions Source/Editor/GUI/CurveEditor.Base.cs
Expand Up @@ -202,8 +202,19 @@ public void ResetView()
/// Adds the new keyframe (as boxed object).
/// </summary>
/// <param name="time">The keyframe time.</param>
/// <param name="value">The keyframe value.</param>
public abstract void AddKeyframe(float time, object value);
/// <param name="value">The keyframe value (boxed).</param>
/// <returns>The index of the keyframe.</returns>
public abstract int AddKeyframe(float time, object value);

/// <summary>
/// Adds the new keyframe (as boxed object).
/// </summary>
/// <param name="time">The keyframe time.</param>
/// <param name="value">The keyframe value (boxed).</param>
/// <param name="tangentIn">The keyframe 'In' tangent value (boxed).</param>
/// <param name="tangentOut">The keyframe 'Out' tangent value (boxed).</param>
/// <returns>The index of the keyframe.</returns>
public abstract int AddKeyframe(float time, object value, object tangentIn, object tangentOut);

/// <summary>
/// Gets the keyframe data (as boxed objects).
Expand Down Expand Up @@ -279,5 +290,11 @@ protected static Vector2 ApplyUseModeMask(UseMode mode, Vector2 value, Vector2 d

/// <inheritdoc />
public abstract void OnKeyframesMove(IKeyframesEditor editor, ContainerControl control, Vector2 location, bool start, bool end);

/// <inheritdoc />
public abstract void OnKeyframesCopy(IKeyframesEditor editor, float? timeOffset, System.Text.StringBuilder data);

/// <inheritdoc />
public abstract void OnKeyframesPaste(IKeyframesEditor editor, float? timeOffset, string[] datas, ref int index);
}
}
111 changes: 108 additions & 3 deletions Source/Editor/GUI/CurveEditor.Contents.cs
@@ -1,7 +1,13 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using FlaxEditor.Scripting;
using FlaxEngine;
using FlaxEngine.GUI;
using FlaxEngine.Json;

namespace FlaxEditor.GUI
{
Expand Down Expand Up @@ -440,12 +446,21 @@ public override bool OnMouseUp(Vector2 location, MouseButton button)
if (Vector2.Distance(ref location, ref _rightMouseDownPos) < 3.0f)
{
var selectionCount = _editor.SelectionCount;
var underMouse = GetChildAt(location);
if (selectionCount == 0 && underMouse is KeyframePoint point)
var point = GetChildAt(location) as KeyframePoint;
if (selectionCount == 0 && point != null)
{
// Select node
selectionCount = 1;
point.IsSelected = true;
if (_editor.ShowCollapsed)
{
for (int i = 0; i < _editor._points.Count; i++)
{
var p = _editor._points[i];
if (p.Index == point.Index)
p.IsSelected = point.IsSelected;
}
}
_editor.UpdateTangents();
}

Expand All @@ -459,11 +474,12 @@ public override bool OnMouseUp(Vector2 location, MouseButton button)
cm.AddButton(selectionCount == 1 ? "Edit keyframe" : "Edit keyframes", () => _editor.EditKeyframes(this, location));
}
var totalSelectionCount = _editor.KeyframesEditorContext?.OnKeyframesSelectionCount() ?? selectionCount;
Debug.Log(totalSelectionCount);
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.AddButton("Edit all keyframes", () => _editor.EditAllKeyframes(this, location));
if (_editor.EnableZoom != UseMode.Off || _editor.EnablePanning != UseMode.Off)
{
Expand Down Expand Up @@ -568,5 +584,94 @@ public override void OnKeyframesMove(IKeyframesEditor editor, ContainerControl c
else
_contents.OnMove(location);
}

/// <inheritdoc />
public override void OnKeyframesCopy(IKeyframesEditor editor, float? timeOffset, StringBuilder data)
{
List<int> selectedIndices = null;
for (int i = 0; i < _points.Count; i++)
{
var p = _points[i];
if (p.IsSelected)
{
if (selectedIndices == null)
selectedIndices = new List<int>();
if (!selectedIndices.Contains(p.Index))
selectedIndices.Add(p.Index);
}
}
if (selectedIndices == null)
return;
var offset = timeOffset ?? 0.0f;
data.AppendLine(KeyframesEditorUtils.CopyPrefix);
data.AppendLine(ValueType.FullName);
for (int i = 0; i < selectedIndices.Count; i++)
{
GetKeyframe(selectedIndices[i], out var time, out var value, out var tangentIn, out var tangentOut);
data.AppendLine((time + offset).ToString(CultureInfo.InvariantCulture));
data.AppendLine(JsonSerializer.Serialize(value).Replace(Environment.NewLine, ""));
data.AppendLine(JsonSerializer.Serialize(tangentIn).Replace(Environment.NewLine, ""));
data.AppendLine(JsonSerializer.Serialize(tangentOut).Replace(Environment.NewLine, ""));
}
}

/// <inheritdoc />
public override void OnKeyframesPaste(IKeyframesEditor editor, float? timeOffset, string[] datas, ref int index)
{
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;
try
{
var lines = data.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries);
if (lines.Length < 4)
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) / 4;
OnEditingStart();
index++;
for (int i = 0; i < count; i++)
{
var time = float.Parse(lines[i * 4 + 1], CultureInfo.InvariantCulture) + offset;
var value = JsonSerializer.Deserialize(lines[i * 4 + 2], type);
var tangentIn = JsonSerializer.Deserialize(lines[i * 4 + 3], type);
var tangentOut = JsonSerializer.Deserialize(lines[i * 4 + 4], type);
if (FPS.HasValue)
{
float fps = FPS.Value;
time = Mathf.Floor(time * fps) / fps;
}

var pos = AddKeyframe(time, value, tangentIn, tangentOut);
for (int j = 0; j < _points.Count; j++)
{
var p = _points[j];
if (p.Index == pos)
p.IsSelected = true;
}
}
OnEditingEnd();
UpdateKeyframes();
UpdateTangents();
}
catch (Exception ex)
{
Editor.LogWarning("Failed to paste keyframes.");
Editor.LogWarning(ex.Message);
Editor.LogWarning(ex);
}
}
}
}
89 changes: 82 additions & 7 deletions Source/Editor/GUI/CurveEditor.cs
Expand Up @@ -154,6 +154,11 @@ protected class KeyframePoint : Control
/// </summary>
public Vector2 Point => Editor.GetKeyframePoint(Index, Component);

/// <summary>
/// Gets the time of the keyframe point.
/// </summary>
public float Time => Editor.GetKeyframeTime(Index);

/// <inheritdoc />
public override void Draw()
{
Expand Down Expand Up @@ -483,7 +488,14 @@ protected virtual void OnKeyframesChanged()
public abstract void Evaluate(out T result, float time, bool loop = false);

/// <summary>
/// Gets the time of the keyframe
/// Gets the time of the keyframe.
/// </summary>
/// <param name="index">The keyframe index.</param>
/// <returns>The keyframe time.</returns>
protected abstract float GetKeyframeTime(int index);

/// <summary>
/// Gets the time of the keyframe.
/// </summary>
/// <param name="keyframe">The keyframe object.</param>
/// <returns>The keyframe time.</returns>
Expand Down Expand Up @@ -613,6 +625,27 @@ private void RemoveKeyframes()
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)
Expand Down Expand Up @@ -906,6 +939,20 @@ public override bool OnKeyDown(KeyboardKeys key)
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;
}
Expand Down Expand Up @@ -976,7 +1023,8 @@ public LinearCurveEditor()
/// Adds the new keyframe.
/// </summary>
/// <param name="k">The keyframe to add.</param>
public void AddKeyframe(LinearCurve<T>.Keyframe k)
/// <returns>The index of the keyframe.</returns>
public int AddKeyframe(LinearCurve<T>.Keyframe k)
{
if (FPS.HasValue)
{
Expand All @@ -990,6 +1038,7 @@ public void AddKeyframe(LinearCurve<T>.Keyframe k)

OnKeyframesChanged();
OnEdited();
return pos;
}

/// <summary>
Expand Down Expand Up @@ -1098,6 +1147,12 @@ public override void Evaluate(out T result, float time, bool loop = false)
curve.Evaluate(out result, time, loop);
}

/// <inheritdoc />
protected override float GetKeyframeTime(int index)
{
return _keyframes[index].Time;
}

/// <inheritdoc />
protected override float GetKeyframeTime(object keyframe)
{
Expand Down Expand Up @@ -1220,9 +1275,15 @@ public override void SetKeyframes(object[] keyframes)
}

/// <inheritdoc />
public override void AddKeyframe(float time, object value)
public override int AddKeyframe(float time, object value)
{
AddKeyframe(new LinearCurve<T>.Keyframe(time, (T)value));
return AddKeyframe(new LinearCurve<T>.Keyframe(time, (T)value));
}

/// <inheritdoc />
public override int AddKeyframe(float time, object value, object tangentIn, object tangentOut)
{
return AddKeyframe(new LinearCurve<T>.Keyframe(time, (T)value));
}

/// <inheritdoc />
Expand Down Expand Up @@ -1449,7 +1510,8 @@ public BezierCurveEditor()
/// Adds the new keyframe.
/// </summary>
/// <param name="k">The keyframe to add.</param>
public void AddKeyframe(BezierCurve<T>.Keyframe k)
/// <returns>The index of the keyframe.</returns>
public int AddKeyframe(BezierCurve<T>.Keyframe k)
{
if (FPS.HasValue)
{
Expand All @@ -1463,6 +1525,7 @@ public void AddKeyframe(BezierCurve<T>.Keyframe k)

OnKeyframesChanged();
OnEdited();
return pos;
}

/// <summary>
Expand Down Expand Up @@ -1760,6 +1823,12 @@ public override void Evaluate(out T result, float time, bool loop = false)
curve.Evaluate(out result, time, loop);
}

/// <inheritdoc />
protected override float GetKeyframeTime(int index)
{
return _keyframes[index].Time;
}

/// <inheritdoc />
protected override float GetKeyframeTime(object keyframe)
{
Expand Down Expand Up @@ -1892,9 +1961,15 @@ public override void SetKeyframes(object[] keyframes)
}

/// <inheritdoc />
public override void AddKeyframe(float time, object value)
public override int AddKeyframe(float time, object value)
{
return AddKeyframe(new BezierCurve<T>.Keyframe(time, (T)value));
}

/// <inheritdoc />
public override int AddKeyframe(float time, object value, object tangentIn, object tangentOut)
{
AddKeyframe(new BezierCurve<T>.Keyframe(time, (T)value));
return AddKeyframe(new BezierCurve<T>.Keyframe(time, (T)value, (T)tangentIn, (T)tangentOut));
}

/// <inheritdoc />
Expand Down
17 changes: 17 additions & 0 deletions Source/Editor/GUI/IKeyframesEditor.cs
Expand Up @@ -50,5 +50,22 @@ public interface IKeyframesEditor
/// <param name="start">The movement start flag.</param>
/// <param name="end">The movement end flag.</param>
void OnKeyframesMove(IKeyframesEditor editor, ContainerControl control, Vector2 location, bool start, bool end);

/// <summary>
/// Called when keyframes selection should be copied.
/// </summary>
/// <param name="editor">The source editor.</param>
/// <param name="timeOffset">The additional time offset to apply to the copied keyframes (optional).</param>
/// <param name="data">The result copy data text stream.</param>
void OnKeyframesCopy(IKeyframesEditor editor, float? timeOffset, System.Text.StringBuilder data);

/// <summary>
/// Called when keyframes should be pasted (from clipboard).
/// </summary>
/// <param name="editor">The source editor.</param>
/// <param name="timeOffset">The additional time offset to apply to the pasted keyframes (optional).</param>
/// <param name="datas">The pasted data text.</param>
/// <param name="index">The counter for the current data index. Set to -1 until the calling editor starts paste operation.</param>
void OnKeyframesPaste(IKeyframesEditor editor, float? timeOffset, string[] datas, ref int index);
}
}

0 comments on commit 0d5fa3e

Please sign in to comment.