diff --git a/Source/Editor/GUI/CurveEditor.Base.cs b/Source/Editor/GUI/CurveEditor.Base.cs
index 930be3b65..0e57d0829 100644
--- a/Source/Editor/GUI/CurveEditor.Base.cs
+++ b/Source/Editor/GUI/CurveEditor.Base.cs
@@ -1,6 +1,7 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
using System;
+using System.Collections.Generic;
using FlaxEngine;
using FlaxEngine.GUI;
@@ -296,5 +297,11 @@ namespace FlaxEditor.GUI
///
public abstract void OnKeyframesPaste(IKeyframesEditor editor, float? timeOffset, string[] datas, ref int index);
+
+ ///
+ public abstract void OnKeyframesGet(string trackName, Action get);
+
+ ///
+ public abstract void OnKeyframesSet(List> keyframes);
}
}
diff --git a/Source/Editor/GUI/CurveEditor.Contents.cs b/Source/Editor/GUI/CurveEditor.Contents.cs
index effbc8603..98507d51f 100644
--- a/Source/Editor/GUI/CurveEditor.Contents.cs
+++ b/Source/Editor/GUI/CurveEditor.Contents.cs
@@ -615,9 +615,9 @@ namespace FlaxEditor.GUI
{
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, ""));
+ data.AppendLine(JsonSerializer.Serialize(value).RemoveNewLine());
+ data.AppendLine(JsonSerializer.Serialize(tangentIn).RemoveNewLine());
+ data.AppendLine(JsonSerializer.Serialize(tangentOut).RemoveNewLine());
}
}
diff --git a/Source/Editor/GUI/CurveEditor.cs b/Source/Editor/GUI/CurveEditor.cs
index e60b0554b..d4b31a9e7 100644
--- a/Source/Editor/GUI/CurveEditor.cs
+++ b/Source/Editor/GUI/CurveEditor.cs
@@ -1326,6 +1326,34 @@ namespace FlaxEditor.GUI
return new Vector2(k.Time, Accessor.GetCurveValue(ref k.Value, component));
}
+ ///
+ public override void OnKeyframesGet(string trackName, Action get)
+ {
+ for (int i = 0; i < _keyframes.Count; i++)
+ {
+ var k = _keyframes[i];
+ get(trackName, k.Time, k);
+ }
+ }
+
+ ///
+ public override void OnKeyframesSet(List> keyframes)
+ {
+ OnEditingStart();
+ _keyframes.Clear();
+ if (keyframes != null)
+ {
+ foreach (var e in keyframes)
+ {
+ var k = (LinearCurve.Keyframe)e.Value;
+ _keyframes.Add(new LinearCurve.Keyframe(e.Key, k.Value));
+ }
+ }
+ OnKeyframesChanged();
+ OnEdited();
+ OnEditingEnd();
+ }
+
///
public override int KeyframesCount => _keyframes.Count;
@@ -2019,6 +2047,34 @@ namespace FlaxEditor.GUI
return new Vector2(k.Time, Accessor.GetCurveValue(ref k.Value, component));
}
+ ///
+ public override void OnKeyframesGet(string trackName, Action get)
+ {
+ for (int i = 0; i < _keyframes.Count; i++)
+ {
+ var k = _keyframes[i];
+ get(trackName, k.Time, k);
+ }
+ }
+
+ ///
+ public override void OnKeyframesSet(List> keyframes)
+ {
+ OnEditingStart();
+ _keyframes.Clear();
+ if (keyframes != null)
+ {
+ foreach (var e in keyframes)
+ {
+ var k = (BezierCurve.Keyframe)e.Value;
+ _keyframes.Add(new BezierCurve.Keyframe(e.Key, k.Value, k.TangentIn, k.TangentOut));
+ }
+ }
+ OnKeyframesChanged();
+ OnEdited();
+ OnEditingEnd();
+ }
+
///
public override int KeyframesCount => _keyframes.Count;
diff --git a/Source/Editor/GUI/IKeyframesEditor.cs b/Source/Editor/GUI/IKeyframesEditor.cs
index 40ee390d6..7e659ff4c 100644
--- a/Source/Editor/GUI/IKeyframesEditor.cs
+++ b/Source/Editor/GUI/IKeyframesEditor.cs
@@ -1,5 +1,7 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
+using System;
+using System.Collections.Generic;
using FlaxEngine;
using FlaxEngine.GUI;
@@ -67,5 +69,18 @@ namespace FlaxEditor.GUI
/// The pasted data text.
/// The counter for the current data index. Set to -1 until the calling editor starts paste operation.
void OnKeyframesPaste(IKeyframesEditor editor, float? timeOffset, string[] datas, ref int index);
+
+ ///
+ /// Called when collecting keyframes from the context.
+ ///
+ /// The name of the track.
+ /// The getter function to call for all keyframes. Args are: track name, keyframe time, keyframe object.
+ void OnKeyframesGet(string trackName, Action get);
+
+ ///
+ /// Called when setting keyframes data (opposite to ).
+ ///
+ /// The list of keyframes, null for empty list. Keyframe data is: time and keyframe object.
+ void OnKeyframesSet(List> keyframes);
}
}
diff --git a/Source/Editor/GUI/IKeyframesEditorContext.cs b/Source/Editor/GUI/IKeyframesEditorContext.cs
index 6f3cdfb5e..accf55aef 100644
--- a/Source/Editor/GUI/IKeyframesEditorContext.cs
+++ b/Source/Editor/GUI/IKeyframesEditorContext.cs
@@ -1,5 +1,7 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
+using System;
+using System.Collections.Generic;
using FlaxEngine;
using FlaxEngine.GUI;
@@ -62,5 +64,17 @@ namespace FlaxEditor.GUI
/// The pasted data text.
/// The counter for the current data index. Set to -1 until the calling editor starts paste operation.
void OnKeyframesPaste(IKeyframesEditor editor, float? timeOffset, string[] datas, ref int index);
+
+ ///
+ /// Called when collecting keyframes from the context.
+ ///
+ /// The getter function to call for all keyframes. Args are: track name, keyframe time, keyframe object.
+ void OnKeyframesGet(Action get);
+
+ ///
+ /// Called when setting keyframes data (opposite to ).
+ ///
+ /// The list of keyframes, null for empty list. Keyframe data is: time and keyframe object.
+ void OnKeyframesSet(List> keyframes);
}
}
diff --git a/Source/Editor/GUI/Timeline/GUI/KeyframesEditor.cs b/Source/Editor/GUI/Timeline/GUI/KeyframesEditor.cs
index afe05129b..c3969de8d 100644
--- a/Source/Editor/GUI/Timeline/GUI/KeyframesEditor.cs
+++ b/Source/Editor/GUI/Timeline/GUI/KeyframesEditor.cs
@@ -62,6 +62,12 @@ namespace FlaxEditor.GUI
{
return Time > other.Time ? 1 : 0;
}
+
+ ///
+ public override string ToString()
+ {
+ return Value?.ToString() ?? string.Empty;
+ }
}
///
@@ -409,8 +415,8 @@ namespace FlaxEditor.GUI
_cmShowPos = PointToKeyframes(location, ref viewRect);
var cm = new ContextMenu.ContextMenu();
- cm.AddButton("Add keyframe", () => _editor.AddKeyframe(_cmShowPos)).Enabled = _editor.Keyframes.Count < _editor.MaxKeyframes;
- if (selectionCount > 0)
+ 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));
}
@@ -421,13 +427,14 @@ namespace FlaxEditor.GUI
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));
- cm.AddButton("Select all keyframes", _editor.SelectAll);
+ 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();
@@ -645,6 +652,16 @@ namespace FlaxEditor.GUI
///
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.
///
@@ -1353,7 +1370,7 @@ namespace FlaxEditor.GUI
continue;
var k = _keyframes[i];
data.AppendLine((k.Time + offset).ToString(CultureInfo.InvariantCulture));
- data.AppendLine(JsonSerializer.Serialize(k.Value).Replace(Environment.NewLine, ""));
+ data.AppendLine(JsonSerializer.Serialize(k.Value).RemoveNewLine());
}
}
@@ -1428,5 +1445,33 @@ namespace FlaxEditor.GUI
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();
+ }
}
}
diff --git a/Source/Editor/GUI/Timeline/Timeline.cs b/Source/Editor/GUI/Timeline/Timeline.cs
index f073da422..2b5349d2c 100644
--- a/Source/Editor/GUI/Timeline/Timeline.cs
+++ b/Source/Editor/GUI/Timeline/Timeline.cs
@@ -2137,5 +2137,20 @@ namespace FlaxEditor.GUI.Timeline
trackContext.OnKeyframesPaste(editor, timeOffset, datas, ref index);
}
}
+
+ ///
+ public void OnKeyframesGet(Action get)
+ {
+ for (int i = 0; i < _tracks.Count; i++)
+ {
+ if (_tracks[i] is IKeyframesEditorContext trackContext)
+ trackContext.OnKeyframesGet(get);
+ }
+ }
+
+ ///
+ public void OnKeyframesSet(List> keyframes)
+ {
+ }
}
}
diff --git a/Source/Editor/GUI/Timeline/Tracks/ActorTrack.cs b/Source/Editor/GUI/Timeline/Tracks/ActorTrack.cs
index 8a4e4b4d2..1366e9d44 100644
--- a/Source/Editor/GUI/Timeline/Tracks/ActorTrack.cs
+++ b/Source/Editor/GUI/Timeline/Tracks/ActorTrack.cs
@@ -65,9 +65,13 @@ namespace FlaxEditor.GUI.Timeline.Tracks
set => ActorID = value?.ID ?? Guid.Empty;
}
- ///
- public ActorTrack(ref TrackCreateOptions options)
- : base(ref options)
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The track initial options.
+ /// True if show sub-tracks keyframes as a proxy on this track, otherwise false.
+ public ActorTrack(ref TrackCreateOptions options, bool useProxyKeyframes = true)
+ : base(ref options, useProxyKeyframes)
{
// Select Actor button
const float buttonSize = 18;
diff --git a/Source/Editor/GUI/Timeline/Tracks/AudioTrack.cs b/Source/Editor/GUI/Timeline/Tracks/AudioTrack.cs
index e1ddac28c..ff4ddf492 100644
--- a/Source/Editor/GUI/Timeline/Tracks/AudioTrack.cs
+++ b/Source/Editor/GUI/Timeline/Tracks/AudioTrack.cs
@@ -1,6 +1,7 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
using System;
+using System.Collections.Generic;
using System.IO;
using FlaxEditor.GUI.Timeline.Undo;
using FlaxEditor.Viewport.Previews;
@@ -476,10 +477,12 @@ namespace FlaxEditor.GUI.Timeline.Tracks
{
if (_audioMedia == null || Curve == null || Timeline == null)
return;
+ bool wasVisible = Curve.Visible;
Curve.Visible = Visible;
if (!Visible)
{
- Curve.ClearSelection();
+ if (wasVisible)
+ Curve.ClearSelection();
return;
}
Curve.KeyframesEditorContext = Timeline;
@@ -702,5 +705,17 @@ namespace FlaxEditor.GUI.Timeline.Tracks
if (Curve != null && Curve.Visible)
Curve.OnKeyframesPaste(editor, timeOffset, datas, ref index);
}
+
+ ///
+ public void OnKeyframesGet(Action get)
+ {
+ Curve?.OnKeyframesGet(Name, get);
+ }
+
+ ///
+ public void OnKeyframesSet(List> keyframes)
+ {
+ Curve?.OnKeyframesSet(keyframes);
+ }
}
}
diff --git a/Source/Editor/GUI/Timeline/Tracks/CameraCutTrack.cs b/Source/Editor/GUI/Timeline/Tracks/CameraCutTrack.cs
index 751b695f6..d85a7edd9 100644
--- a/Source/Editor/GUI/Timeline/Tracks/CameraCutTrack.cs
+++ b/Source/Editor/GUI/Timeline/Tracks/CameraCutTrack.cs
@@ -728,7 +728,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks
///
public CameraCutTrack(ref TrackCreateOptions options)
- : base(ref options)
+ : base(ref options, false)
{
Height = CameraCutThumbnailRenderer.Height + 8;
diff --git a/Source/Editor/GUI/Timeline/Tracks/ConductorTrack.cs b/Source/Editor/GUI/Timeline/Tracks/ConductorTrack.cs
new file mode 100644
index 000000000..867643fd3
--- /dev/null
+++ b/Source/Editor/GUI/Timeline/Tracks/ConductorTrack.cs
@@ -0,0 +1,356 @@
+// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using FlaxEngine;
+using FlaxEngine.GUI;
+
+namespace FlaxEditor.GUI.Timeline.Tracks
+{
+ ///
+ /// The timeline track that can proxy the sub-tracks keyframes for easier batched editing.
+ ///
+ ///
+ public abstract class ConductorTrack : Track, IKeyframesEditorContext
+ {
+ private sealed class ConductionUndoAction : IUndoAction
+ {
+ public bool IsDo;
+ public Timeline Timeline;
+ public string Track;
+
+ private void Refresh()
+ {
+ Refresh(Timeline.FindTrack(Track));
+ }
+
+ private void Refresh(Track track)
+ {
+ if (track is ConductorTrack conductor && conductor.Proxy != null)
+ {
+ conductor.LoadProxy();
+ foreach (var subTrack in conductor.SubTracks)
+ Refresh(subTrack);
+ }
+ }
+
+ public string ActionString => "Edit track";
+
+ public void Do()
+ {
+ if (IsDo)
+ Refresh();
+ }
+
+ public void Undo()
+ {
+ if (!IsDo)
+ Refresh();
+ }
+
+ public void Dispose()
+ {
+ Timeline = null;
+ Track = null;
+ }
+ }
+
+ private HashSet _proxyTracks;
+
+ ///
+ /// The batched keyframes editor for all sub-tracks.
+ ///
+ protected KeyframesEditor Proxy;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The track initial options.
+ /// True if show sub-tracks keyframes as a proxy on this track, otherwise false.
+ protected ConductorTrack(ref TrackCreateOptions options, bool useProxyKeyframes = true)
+ : base(ref options)
+ {
+ if (useProxyKeyframes)
+ {
+ // Proxy keyframes editor
+ Proxy = new KeyframesEditor
+ {
+ EnableZoom = false,
+ EnablePanning = false,
+ EnableKeyframesValueEdit = false,
+ DefaultValue = new ProxyKey(),
+ ScrollBars = ScrollBars.None,
+ };
+ Proxy.Edited += OnProxyEdited;
+ Proxy.EditingEnd += OnProxyEditingEnd;
+ Proxy.UnlockChildrenRecursive();
+ }
+ }
+
+ private void UpdateProxyKeyframes()
+ {
+ if (Proxy == null || Timeline == null)
+ return;
+ bool wasVisible = Proxy.Visible;
+ Proxy.Visible = Visible && IsCollapsed && SubTracks.Any(x => x is IKeyframesEditorContext);
+ if (!Visible)
+ {
+ if (wasVisible)
+ Proxy.ClearSelection();
+ return;
+ }
+ Proxy.KeyframesEditorContext = Timeline;
+ Proxy.CustomViewPanning = Timeline.OnKeyframesViewPanning;
+ Proxy.Bounds = new Rectangle(Timeline.StartOffset, Y + 1.0f, Timeline.Duration * Timeline.UnitsPerSecond * Timeline.Zoom, Height - 2.0f);
+ Proxy.ViewScale = new Vector2(Timeline.Zoom, 1.0f);
+ if (!wasVisible)
+ LoadProxy();
+ else
+ Proxy.UpdateKeyframes();
+ }
+
+ private void OnProxyEdited()
+ {
+ Timeline.MarkAsEdited();
+ }
+
+ private void OnProxyEditingEnd()
+ {
+ SaveProxy();
+ }
+
+ ///
+ /// The proxy key data.
+ ///
+ public struct ProxyKey
+ {
+ ///
+ /// The keyframes.
+ ///
+ [EditorDisplay("Keyframes", EditorDisplayAttribute.InlineStyle), ExpandGroups]
+ [Collection(CanReorderItems = false, ReadOnly = true)]
+ public List> Keyframes;
+
+ ///
+ public override string ToString()
+ {
+ if (Keyframes == null || Keyframes.Count == 0)
+ return string.Empty;
+ var sb = new StringBuilder();
+ for (int i = 0; i < Keyframes.Count; i++)
+ {
+ if (i != 0)
+ sb.Append(", ");
+ var k = Keyframes[i];
+ if (k.Value != null)
+ sb.Append(k.Value);
+ }
+ return sb.ToString();
+ }
+ }
+
+ private void LoadProxy()
+ {
+ if (_proxyTracks == null)
+ _proxyTracks = new HashSet();
+ else
+ _proxyTracks.Clear();
+
+ // Build keyframes list that contains keys from all tracks to proxy
+ var keyframes = new List();
+ var eps = Proxy.FPS.HasValue ? 0.5f / Proxy.FPS.Value : Mathf.Epsilon;
+ Action getter = (trackName, time, value) =>
+ {
+ _proxyTracks.Add(trackName);
+ int idx = keyframes.FindIndex(x => Mathf.Abs(x.Time - time) <= eps);
+ if (idx == -1)
+ {
+ idx = keyframes.Count;
+ keyframes.Add(new KeyframesEditor.Keyframe(time, new ProxyKey { Keyframes = new List>() }));
+ }
+ ((ProxyKey)keyframes[idx].Value).Keyframes.Add(new KeyValuePair(trackName, value));
+ };
+ foreach (var subTrack in SubTracks)
+ {
+ var trackContext = subTrack as IKeyframesEditorContext;
+ trackContext?.OnKeyframesGet(getter);
+ }
+
+ Proxy.SetKeyframes(keyframes);
+ }
+
+ private void SaveProxy()
+ {
+ // Collect keyframes list per track
+ var perTrackKeyframes = new Dictionary>>();
+ var keyframes = Proxy.Keyframes;
+ foreach (var keyframe in keyframes)
+ {
+ var proxy = (ProxyKey)keyframe.Value;
+ if (proxy.Keyframes == null)
+ continue;
+ foreach (var e in proxy.Keyframes)
+ {
+ if (!perTrackKeyframes.TryGetValue(e.Key, out var trackKeyframes))
+ {
+ trackKeyframes = new List>();
+ perTrackKeyframes.Add(e.Key, trackKeyframes);
+ }
+ trackKeyframes.Add(new KeyValuePair(keyframe.Time, e.Value));
+ }
+ }
+
+ // Apply the new keyframes to all tracks in this proxy
+ Timeline.AddBatchedUndoAction(new ConductionUndoAction { IsDo = false, Timeline = Timeline, Track = Name });
+ foreach (var trackName in _proxyTracks)
+ {
+ var track = Timeline.FindTrack(trackName);
+ if (track == null)
+ {
+ Editor.LogWarning(string.Format("Failed to find track {0} for keyframes proxy on track {1}.", trackName, Title ?? Name));
+ continue;
+ }
+ var trackContext = track as IKeyframesEditorContext;
+ if (trackContext == null)
+ {
+ Editor.LogWarning(string.Format("Track {0} used keyframes proxy on track {1} is invalid.", track.Title ?? track.Name, Title ?? Name));
+ continue;
+ }
+ perTrackKeyframes.TryGetValue(trackName, out var trackKeyframes);
+ trackContext.OnKeyframesSet(trackKeyframes);
+ }
+ Timeline.AddBatchedUndoAction(new ConductionUndoAction { IsDo = true, Timeline = Timeline, Track = Name });
+ }
+
+ ///
+ protected override void OnVisibleChanged()
+ {
+ base.OnVisibleChanged();
+
+ UpdateProxyKeyframes();
+ }
+
+ ///
+ protected override void OnExpandedChanged()
+ {
+ base.OnExpandedChanged();
+
+ UpdateProxyKeyframes();
+ }
+
+ ///
+ public override void OnTimelineChanged(Timeline timeline)
+ {
+ base.OnTimelineChanged(timeline);
+
+ if (Proxy != null)
+ {
+ Proxy.Parent = timeline?.MediaPanel;
+ Proxy.FPS = timeline?.FramesPerSecond;
+ UpdateProxyKeyframes();
+ }
+ }
+
+ ///
+ public override void OnTimelineZoomChanged()
+ {
+ base.OnTimelineZoomChanged();
+
+ UpdateProxyKeyframes();
+ }
+
+ ///
+ public override void OnTimelineArrange()
+ {
+ base.OnTimelineArrange();
+
+ UpdateProxyKeyframes();
+ }
+
+ ///
+ public override void OnTimelineFpsChanged(float before, float after)
+ {
+ base.OnTimelineFpsChanged(before, after);
+
+ Proxy.FPS = after;
+ }
+
+ ///
+ public override void OnDestroy()
+ {
+ if (Proxy != null)
+ {
+ Proxy.Dispose();
+ Proxy = null;
+ _proxyTracks = null;
+ }
+
+ base.OnDestroy();
+ }
+
+ ///
+ public void OnKeyframesDeselect(IKeyframesEditor editor)
+ {
+ if (Proxy != null && Proxy.Visible)
+ Proxy.OnKeyframesDeselect(editor);
+ }
+
+ ///
+ public void OnKeyframesSelection(IKeyframesEditor editor, ContainerControl control, Rectangle selection)
+ {
+ if (Proxy != null && Proxy.Visible)
+ Proxy.OnKeyframesSelection(editor, control, selection);
+ }
+
+ ///
+ public int OnKeyframesSelectionCount()
+ {
+ return Proxy != null && Proxy.Visible ? Proxy.OnKeyframesSelectionCount() : 0;
+ }
+
+ ///
+ public void OnKeyframesDelete(IKeyframesEditor editor)
+ {
+ if (Proxy != null && Proxy.Visible)
+ Proxy.OnKeyframesDelete(editor);
+ }
+
+ ///
+ public void OnKeyframesMove(IKeyframesEditor editor, ContainerControl control, Vector2 location, bool start, bool end)
+ {
+ if (Proxy != null && Proxy.Visible)
+ Proxy.OnKeyframesMove(editor, control, location, start, end);
+ }
+
+ ///
+ public void OnKeyframesCopy(IKeyframesEditor editor, float? timeOffset, StringBuilder data)
+ {
+ if (Proxy != null && Proxy.Visible)
+ Proxy.OnKeyframesCopy(editor, timeOffset, data);
+ }
+
+ ///
+ public void OnKeyframesPaste(IKeyframesEditor editor, float? timeOffset, string[] datas, ref int index)
+ {
+ if (Proxy != null && Proxy.Visible)
+ Proxy.OnKeyframesPaste(editor, timeOffset, datas, ref index);
+ }
+
+ ///
+ public void OnKeyframesGet(Action get)
+ {
+ foreach (var subTrack in SubTracks)
+ {
+ if (subTrack is IKeyframesEditorContext trackContext)
+ trackContext.OnKeyframesGet(get);
+ }
+ }
+
+ ///
+ public void OnKeyframesSet(List> keyframes)
+ {
+ }
+ }
+}
diff --git a/Source/Editor/GUI/Timeline/Tracks/CurvePropertyTrack.cs b/Source/Editor/GUI/Timeline/Tracks/CurvePropertyTrack.cs
index fb88d6957..18b88f08f 100644
--- a/Source/Editor/GUI/Timeline/Tracks/CurvePropertyTrack.cs
+++ b/Source/Editor/GUI/Timeline/Tracks/CurvePropertyTrack.cs
@@ -1,6 +1,7 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
using System;
+using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
@@ -224,10 +225,12 @@ namespace FlaxEditor.GUI.Timeline.Tracks
{
if (Curve == null || Timeline == null)
return;
+ bool wasVisible = Curve.Visible;
Curve.Visible = Visible;
if (!Visible)
{
- Curve.ClearSelection();
+ if (wasVisible)
+ Curve.ClearSelection();
return;
}
var expanded = IsExpanded;
@@ -432,52 +435,64 @@ namespace FlaxEditor.GUI.Timeline.Tracks
}
///
- public void OnKeyframesDeselect(IKeyframesEditor editor)
+ public new void OnKeyframesDeselect(IKeyframesEditor editor)
{
if (Curve != null && Curve.Visible)
Curve.OnKeyframesDeselect(editor);
}
///
- public void OnKeyframesSelection(IKeyframesEditor editor, ContainerControl control, Rectangle selection)
+ public new void OnKeyframesSelection(IKeyframesEditor editor, ContainerControl control, Rectangle selection)
{
if (Curve != null && Curve.Visible)
Curve.OnKeyframesSelection(editor, control, selection);
}
///
- public int OnKeyframesSelectionCount()
+ public new int OnKeyframesSelectionCount()
{
return Curve != null && Curve.Visible ? Curve.OnKeyframesSelectionCount() : 0;
}
///
- public void OnKeyframesDelete(IKeyframesEditor editor)
+ public new void OnKeyframesDelete(IKeyframesEditor editor)
{
if (Curve != null && Curve.Visible)
Curve.OnKeyframesDelete(editor);
}
///
- public void OnKeyframesMove(IKeyframesEditor editor, ContainerControl control, Vector2 location, bool start, bool end)
+ public new void OnKeyframesMove(IKeyframesEditor editor, ContainerControl control, Vector2 location, bool start, bool end)
{
if (Curve != null && Curve.Visible)
Curve.OnKeyframesMove(editor, control, location, start, end);
}
///
- public void OnKeyframesCopy(IKeyframesEditor editor, float? timeOffset, StringBuilder data)
+ public new void OnKeyframesCopy(IKeyframesEditor editor, float? timeOffset, StringBuilder data)
{
if (Curve != null && Curve.Visible)
Curve.OnKeyframesCopy(editor, timeOffset, data);
}
///
- public void OnKeyframesPaste(IKeyframesEditor editor, float? timeOffset, string[] datas, ref int index)
+ public new void OnKeyframesPaste(IKeyframesEditor editor, float? timeOffset, string[] datas, ref int index)
{
if (Curve != null && Curve.Visible)
Curve.OnKeyframesPaste(editor, timeOffset, datas, ref index);
}
+
+ ///
+ public new void OnKeyframesGet(Action get)
+ {
+ Curve?.OnKeyframesGet(Name, get);
+ }
+
+ ///
+ public new void OnKeyframesSet(List> keyframes)
+ {
+ Curve?.OnKeyframesSet(keyframes);
+ }
}
///
diff --git a/Source/Editor/GUI/Timeline/Tracks/EventTrack.cs b/Source/Editor/GUI/Timeline/Tracks/EventTrack.cs
index b5f94bda2..1275a8328 100644
--- a/Source/Editor/GUI/Timeline/Tracks/EventTrack.cs
+++ b/Source/Editor/GUI/Timeline/Tracks/EventTrack.cs
@@ -1,6 +1,7 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
using System;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
@@ -50,7 +51,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks
{
e.EventParamsSizes[i] = stream.ReadInt32();
var paramTypeName = LoadName(stream);
- e.EventParamsTypes[i] = Scripting.TypeUtils.GetType(paramTypeName).Type;
+ e.EventParamsTypes[i] = Scripting.TypeUtils.GetManagedType(paramTypeName);
if (e.EventParamsTypes[i] == null)
isInvalid = true;
}
@@ -283,9 +284,11 @@ namespace FlaxEditor.GUI.Timeline.Tracks
{
if (Events == null || Timeline == null)
return;
+ bool wasVisible = Events.Visible;
Events.Visible = Visible;
if (!Visible)
{
+ if(wasVisible)
Events.ClearSelection();
return;
}
@@ -413,51 +416,63 @@ namespace FlaxEditor.GUI.Timeline.Tracks
}
///
- public void OnKeyframesDeselect(IKeyframesEditor editor)
+ public new void OnKeyframesDeselect(IKeyframesEditor editor)
{
if (Events != null && Events.Visible)
Events.OnKeyframesDeselect(editor);
}
///
- public void OnKeyframesSelection(IKeyframesEditor editor, ContainerControl control, Rectangle selection)
+ public new void OnKeyframesSelection(IKeyframesEditor editor, ContainerControl control, Rectangle selection)
{
if (Events != null && Events.Visible)
Events.OnKeyframesSelection(editor, control, selection);
}
///
- public int OnKeyframesSelectionCount()
+ public new int OnKeyframesSelectionCount()
{
return Events != null && Events.Visible ? Events.OnKeyframesSelectionCount() : 0;
}
///
- public void OnKeyframesDelete(IKeyframesEditor editor)
+ public new void OnKeyframesDelete(IKeyframesEditor editor)
{
if (Events != null && Events.Visible)
Events.OnKeyframesDelete(editor);
}
///
- public void OnKeyframesMove(IKeyframesEditor editor, ContainerControl control, Vector2 location, bool start, bool end)
+ public new void OnKeyframesMove(IKeyframesEditor editor, ContainerControl control, Vector2 location, bool start, bool end)
{
if (Events != null && Events.Visible)
Events.OnKeyframesMove(editor, control, location, start, end);
}
///
- public void OnKeyframesCopy(IKeyframesEditor editor, float? timeOffset, StringBuilder data)
+ public new void OnKeyframesCopy(IKeyframesEditor editor, float? timeOffset, StringBuilder data)
{
if (Events != null && Events.Visible)
Events.OnKeyframesCopy(editor, timeOffset, data);
}
///
- public void OnKeyframesPaste(IKeyframesEditor editor, float? timeOffset, string[] datas, ref int index)
+ public new void OnKeyframesPaste(IKeyframesEditor editor, float? timeOffset, string[] datas, ref int index)
{
if (Events != null && Events.Visible)
Events.OnKeyframesPaste(editor, timeOffset, datas, ref index);
}
+
+ ///
+ public new void OnKeyframesGet(Action get)
+ {
+ Events?.OnKeyframesGet(Name, get);
+ }
+
+ ///
+ public new void OnKeyframesSet(List> keyframes)
+ {
+ Events?.OnKeyframesSet(keyframes);
+ }
}
}
diff --git a/Source/Editor/GUI/Timeline/Tracks/FolderTrack.cs b/Source/Editor/GUI/Timeline/Tracks/FolderTrack.cs
index c01719b97..7f4a79229 100644
--- a/Source/Editor/GUI/Timeline/Tracks/FolderTrack.cs
+++ b/Source/Editor/GUI/Timeline/Tracks/FolderTrack.cs
@@ -12,7 +12,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks
/// The timeline track that represents a folder used to group and organize tracks.
///
///
- public class FolderTrack : Track
+ public class FolderTrack : ConductorTrack
{
///
/// Gets the archetype.
diff --git a/Source/Editor/GUI/Timeline/Tracks/KeyframesPropertyTrack.cs b/Source/Editor/GUI/Timeline/Tracks/KeyframesPropertyTrack.cs
index d76f6c1fd..1de532c5a 100644
--- a/Source/Editor/GUI/Timeline/Tracks/KeyframesPropertyTrack.cs
+++ b/Source/Editor/GUI/Timeline/Tracks/KeyframesPropertyTrack.cs
@@ -1,6 +1,7 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
using System;
+using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
@@ -55,7 +56,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks
var keyframes = new KeyframesEditor.Keyframe[keyframesCount];
var dataBuffer = new byte[e.ValueSize];
- var propertyType = Scripting.TypeUtils.GetType(e.MemberTypeName).Type;
+ var propertyType = Scripting.TypeUtils.GetManagedType(e.MemberTypeName);
if (propertyType == null)
{
e.Keyframes.ResetKeyframes();
@@ -250,10 +251,12 @@ namespace FlaxEditor.GUI.Timeline.Tracks
{
if (Keyframes == null || Timeline == null)
return;
+ bool wasVisible = Keyframes.Visible;
Keyframes.Visible = Visible;
if (!Visible)
{
- Keyframes.ClearSelection();
+ if (wasVisible)
+ Keyframes.ClearSelection();
return;
}
Keyframes.KeyframesEditorContext = Timeline;
@@ -393,51 +396,63 @@ namespace FlaxEditor.GUI.Timeline.Tracks
}
///
- public void OnKeyframesDeselect(IKeyframesEditor editor)
+ public new void OnKeyframesDeselect(IKeyframesEditor editor)
{
if (Keyframes != null && Keyframes.Visible)
Keyframes.OnKeyframesDeselect(editor);
}
///
- public void OnKeyframesSelection(IKeyframesEditor editor, ContainerControl control, Rectangle selection)
+ public new void OnKeyframesSelection(IKeyframesEditor editor, ContainerControl control, Rectangle selection)
{
if (Keyframes != null && Keyframes.Visible)
Keyframes.OnKeyframesSelection(editor, control, selection);
}
///
- public int OnKeyframesSelectionCount()
+ public new int OnKeyframesSelectionCount()
{
return Keyframes != null && Keyframes.Visible ? Keyframes.OnKeyframesSelectionCount() : 0;
}
///
- public void OnKeyframesDelete(IKeyframesEditor editor)
+ public new void OnKeyframesDelete(IKeyframesEditor editor)
{
if (Keyframes != null && Keyframes.Visible)
Keyframes.OnKeyframesDelete(editor);
}
///
- public void OnKeyframesMove(IKeyframesEditor editor, ContainerControl control, Vector2 location, bool start, bool end)
+ public new void OnKeyframesMove(IKeyframesEditor editor, ContainerControl control, Vector2 location, bool start, bool end)
{
if (Keyframes != null && Keyframes.Visible)
Keyframes.OnKeyframesMove(editor, control, location, start, end);
}
///
- public void OnKeyframesCopy(IKeyframesEditor editor, float? timeOffset, StringBuilder data)
+ public new void OnKeyframesCopy(IKeyframesEditor editor, float? timeOffset, StringBuilder data)
{
if (Keyframes != null && Keyframes.Visible)
Keyframes.OnKeyframesCopy(editor, timeOffset, data);
}
///
- public void OnKeyframesPaste(IKeyframesEditor editor, float? timeOffset, string[] datas, ref int index)
+ public new void OnKeyframesPaste(IKeyframesEditor editor, float? timeOffset, string[] datas, ref int index)
{
if (Keyframes != null && Keyframes.Visible)
Keyframes.OnKeyframesPaste(editor, timeOffset, datas, ref index);
}
+
+ ///
+ public new void OnKeyframesGet(Action get)
+ {
+ Keyframes?.OnKeyframesGet(Name, get);
+ }
+
+ ///
+ public new void OnKeyframesSet(List> keyframes)
+ {
+ Keyframes?.OnKeyframesSet(keyframes);
+ }
}
}
diff --git a/Source/Editor/GUI/Timeline/Tracks/MemberTrack.cs b/Source/Editor/GUI/Timeline/Tracks/MemberTrack.cs
index c906ae91f..87af02350 100644
--- a/Source/Editor/GUI/Timeline/Tracks/MemberTrack.cs
+++ b/Source/Editor/GUI/Timeline/Tracks/MemberTrack.cs
@@ -13,7 +13,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks
/// The timeline track for animating object member (managed object).
///
///
- public abstract class MemberTrack : Track
+ public abstract class MemberTrack : ConductorTrack
{
private float _previewValueLeft;
@@ -112,8 +112,9 @@ namespace FlaxEditor.GUI.Timeline.Tracks
/// The track initial options.
/// True if show keyframe navigation buttons, otherwise false.
/// True if show current value preview, otherwise false.
- protected MemberTrack(ref TrackCreateOptions options, bool useNavigationButtons = true, bool useValuePreview = true)
- : base(ref options)
+ /// True if show sub-tracks keyframes as a proxy on this track, otherwise false.
+ protected MemberTrack(ref TrackCreateOptions options, bool useNavigationButtons = true, bool useValuePreview = true, bool useProxyKeyframes = false)
+ : base(ref options, useProxyKeyframes)
{
var uiLeft = _muteCheckbox.Offsets.Left;
diff --git a/Source/Editor/GUI/Timeline/Tracks/ObjectPropertyTrack.cs b/Source/Editor/GUI/Timeline/Tracks/ObjectPropertyTrack.cs
index 9a41838e2..bfcb24991 100644
--- a/Source/Editor/GUI/Timeline/Tracks/ObjectPropertyTrack.cs
+++ b/Source/Editor/GUI/Timeline/Tracks/ObjectPropertyTrack.cs
@@ -86,7 +86,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks
///
public ObjectPropertyTrack(ref TrackCreateOptions options)
- : base(ref options, false, false)
+ : base(ref options, false, false, true)
{
// Add track button
const float buttonSize = 14;
diff --git a/Source/Editor/GUI/Timeline/Tracks/ObjectTrack.cs b/Source/Editor/GUI/Timeline/Tracks/ObjectTrack.cs
index e8ef8794b..17eac5a70 100644
--- a/Source/Editor/GUI/Timeline/Tracks/ObjectTrack.cs
+++ b/Source/Editor/GUI/Timeline/Tracks/ObjectTrack.cs
@@ -28,7 +28,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks
/// The timeline track for animating managed objects.
///
///
- public abstract class ObjectTrack : Track, IObjectTrack
+ public abstract class ObjectTrack : ConductorTrack, IObjectTrack
{
private bool _hasObject;
@@ -42,9 +42,13 @@ namespace FlaxEditor.GUI.Timeline.Tracks
///
public abstract object Object { get; }
- ///
- protected ObjectTrack(ref TrackCreateOptions options)
- : base(ref options)
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The track initial options.
+ /// True if show sub-tracks keyframes as a proxy on this track, otherwise false.
+ protected ObjectTrack(ref TrackCreateOptions options, bool useProxyKeyframes = true)
+ : base(ref options, useProxyKeyframes)
{
// Add track button
const float buttonSize = 14;
diff --git a/Source/Editor/GUI/Timeline/Tracks/StructPropertyTrack.cs b/Source/Editor/GUI/Timeline/Tracks/StructPropertyTrack.cs
index 4660c82b0..9b6cec9b9 100644
--- a/Source/Editor/GUI/Timeline/Tracks/StructPropertyTrack.cs
+++ b/Source/Editor/GUI/Timeline/Tracks/StructPropertyTrack.cs
@@ -87,7 +87,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks
///
public StructPropertyTrack(ref TrackCreateOptions options)
- : base(ref options, false, false)
+ : base(ref options, false, false, true)
{
// Add track button
const float buttonSize = 14;
diff --git a/Source/Engine/Animations/Curve.cs b/Source/Engine/Animations/Curve.cs
index 7232e9acf..6cd58e7a8 100644
--- a/Source/Engine/Animations/Curve.cs
+++ b/Source/Engine/Animations/Curve.cs
@@ -378,6 +378,12 @@ namespace FlaxEngine
{
return Time > other.Time ? 1 : 0;
}
+
+ ///
+ public override string ToString()
+ {
+ return Value?.ToString() ?? string.Empty;
+ }
}
///
@@ -655,6 +661,12 @@ namespace FlaxEngine
{
return Time > other.Time ? 1 : 0;
}
+
+ ///
+ public override string ToString()
+ {
+ return Value?.ToString() ?? string.Empty;
+ }
}
///
diff --git a/Source/Engine/Utilities/StringUtils.cs b/Source/Engine/Utilities/StringUtils.cs
index ff6beb4ec..57c018fb7 100644
--- a/Source/Engine/Utilities/StringUtils.cs
+++ b/Source/Engine/Utilities/StringUtils.cs
@@ -254,6 +254,16 @@ namespace FlaxEngine
return string.Concat(graphemes);
}
+ ///
+ /// Removes any new line characters (\r or \n) from the string.
+ ///
+ /// The string to process.
+ /// The single-line string.
+ public static string RemoveNewLine(this string s)
+ {
+ return s.Replace("\n", "").Replace("\r", "");
+ }
+
private static readonly Regex IncNameRegex1 = new Regex("(\\d+)$");
private static readonly Regex IncNameRegex2 = new Regex("\\((\\d+)\\)$");