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+)\\)$");