// Copyright (c) Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using FlaxEditor.GUI.Timeline.Undo;
using FlaxEditor.Scripting;
using FlaxEditor.Utilities;
using FlaxEngine;
using FlaxEngine.GUI;
using FlaxEngine.Utilities;
namespace FlaxEditor.GUI.Timeline.Tracks
{
///
/// The timeline track for animating object property via keyframes collection.
///
///
public class KeyframesPropertyTrack : MemberTrack, IKeyframesEditorContext
{
///
/// Gets the archetype.
///
/// The archetype.
public static TrackArchetype GetArchetype()
{
return new TrackArchetype
{
TypeId = 9,
Name = "Property",
DisableSpawnViaGUI = true,
Create = options => new KeyframesPropertyTrack(ref options),
Load = LoadTrack,
Save = SaveTrack,
};
}
private static void LoadTrack(int version, Track track, BinaryReader stream)
{
var e = (KeyframesPropertyTrack)track;
e.ValueSize = stream.ReadInt32();
int propertyNameLength = stream.ReadInt32();
int propertyTypeNameLength = stream.ReadInt32();
int keyframesCount = stream.ReadInt32();
var propertyName = stream.ReadBytes(propertyNameLength);
e.MemberName = Encoding.UTF8.GetString(propertyName, 0, propertyNameLength);
if (stream.ReadChar() != 0)
throw new Exception("Invalid track data.");
var propertyTypeName = stream.ReadBytes(propertyTypeNameLength);
e.MemberTypeName = Encoding.UTF8.GetString(propertyTypeName, 0, propertyTypeNameLength);
if (stream.ReadChar() != 0)
throw new Exception("Invalid track data.");
var keyframes = new KeyframesEditor.Keyframe[keyframesCount];
var propertyType = TypeUtils.GetManagedType(e.MemberTypeName);
if (propertyType == null)
{
e.Keyframes.ResetKeyframes();
stream.ReadBytes(keyframesCount * (sizeof(float) + e.ValueSize));
if (!string.IsNullOrEmpty(e.MemberTypeName))
Editor.LogError("Cannot load track " + e.MemberName + " of type " + e.MemberTypeName + ". Failed to find the value type information.");
return;
}
if (e.ValueSize != 0)
{
// POD value type - use raw memory
var dataBuffer = new byte[e.ValueSize];
GCHandle handle = GCHandle.Alloc(dataBuffer, GCHandleType.Pinned);
for (int i = 0; i < keyframesCount; i++)
{
var time = stream.ReadSingle();
stream.Read(dataBuffer, 0, e.ValueSize);
var value = Utilities.Utils.ByteArrayToStructure(handle.AddrOfPinnedObject(), propertyType, e.ValueSize);
keyframes[i] = new KeyframesEditor.Keyframe
{
Time = time,
Value = value,
};
}
handle.Free();
}
else
{
// Generic value - use Json storage (as UTF-8)
for (int i = 0; i < keyframesCount; i++)
{
var time = stream.ReadSingle();
var len = stream.ReadInt32();
var value = len != 0 ? FlaxEngine.Json.JsonSerializer.Deserialize(Encoding.UTF8.GetString(stream.ReadBytes(len)), propertyType) : null;
keyframes[i] = new KeyframesEditor.Keyframe
{
Time = time,
Value = value,
};
}
}
e.Keyframes.DefaultValue = e.GetDefaultValue(propertyType);
e.Keyframes.SetKeyframes(keyframes);
}
private static void SaveTrack(Track track, BinaryWriter stream)
{
var e = (KeyframesPropertyTrack)track;
var propertyName = e.MemberName ?? string.Empty;
var propertyNameData = Encoding.UTF8.GetBytes(propertyName);
if (propertyNameData.Length != propertyName.Length)
throw new Exception(string.Format("The object member name bytes data has different size as UTF8 bytes. Type {0}.", propertyName));
var propertyTypeName = e.MemberTypeName ?? string.Empty;
var propertyTypeNameData = Encoding.UTF8.GetBytes(propertyTypeName);
if (propertyTypeNameData.Length != propertyTypeName.Length)
throw new Exception(string.Format("The object member typename bytes data has different size as UTF8 bytes. Type {0}.", propertyTypeName));
var keyframes = e.Keyframes.Keyframes;
stream.Write(e.ValueSize);
stream.Write(propertyNameData.Length);
stream.Write(propertyTypeNameData.Length);
stream.Write(keyframes.Count);
stream.Write(propertyNameData);
stream.Write('\0');
stream.Write(propertyTypeNameData);
stream.Write('\0');
if (e.ValueSize != 0)
{
// POD value type - use raw memory
var dataBuffer = new byte[e.ValueSize];
IntPtr ptr = Marshal.AllocHGlobal(e.ValueSize);
for (int i = 0; i < keyframes.Count; i++)
{
var keyframe = keyframes[i];
Utilities.Utils.StructureToByteArray(keyframe.Value, e.ValueSize, ptr, dataBuffer);
stream.Write(keyframe.Time);
stream.Write(dataBuffer);
}
Marshal.FreeHGlobal(ptr);
}
else
{
// Generic value - use Json storage (as UTF-8)
for (int i = 0; i < keyframes.Count; i++)
{
var keyframe = keyframes[i];
stream.Write(keyframe.Time);
var json = keyframe.Value != null ? FlaxEngine.Json.JsonSerializer.Serialize(keyframe.Value) : null;
var len = json?.Length ?? 0;
stream.Write(len);
if (len > 0)
stream.Write(Encoding.UTF8.GetBytes(json));
}
}
}
private byte[] _keyframesEditingStartData;
///
/// The keyframes editor.
///
public KeyframesEditor Keyframes;
///
public KeyframesPropertyTrack(ref TrackCreateOptions options)
: base(ref options)
{
Height = 20.0f;
// Keyframes editor
Keyframes = new KeyframesEditor
{
EnableZoom = false,
EnablePanning = false,
ScrollBars = ScrollBars.None,
};
Keyframes.Edited += OnKeyframesEdited;
Keyframes.EditingStart += OnKeyframesEditingStart;
Keyframes.EditingEnd += OnKeyframesEditingEnd;
Keyframes.UnlockChildrenRecursive();
_addKey.Clicked += OnAddKeyClicked;
_leftKey.Clicked += OnLeftKeyClicked;
_rightKey.Clicked += OnRightKeyClicked;
}
private void OnRightKeyClicked(Image image, MouseButton button)
{
if (button == MouseButton.Left && GetNextKeyframeFrame(Timeline.CurrentTime, out var frame))
{
Timeline.OnSeek(frame);
}
}
///
protected override void OnContextMenu(ContextMenu.ContextMenu menu)
{
base.OnContextMenu(menu);
menu.AddSeparator();
menu.AddButton("Copy Preview Value", () =>
{
var value = Keyframes.Evaluate(Timeline.CurrentTime);
Clipboard.Text = FlaxEngine.Json.JsonSerializer.Serialize(value);
}).LinkTooltip("Copies the current track value to the clipboard").Enabled = Timeline.ShowPreviewValues;
}
///
public override bool GetNextKeyframeFrame(float time, out int result)
{
for (int i = 0; i < Keyframes.Keyframes.Count; i++)
{
var k = Keyframes.Keyframes[i];
if (k.Time > time)
{
result = Mathf.FloorToInt(k.Time * Timeline.FramesPerSecond);
return true;
}
}
return base.GetNextKeyframeFrame(time, out result);
}
private void OnAddKeyClicked(Image image, MouseButton button)
{
if (button == MouseButton.Left)
{
// Evaluate a value
var time = Timeline.CurrentTime;
if (!TryGetValue(out var value))
value = Keyframes.Evaluate(time);
// Find keyframe at the current location
for (int i = Keyframes.Keyframes.Count - 1; i >= 0; i--)
{
var k = Keyframes.Keyframes[i];
var frame = Mathf.FloorToInt(k.Time * Timeline.FramesPerSecond);
if (frame == Timeline.CurrentFrame)
{
// Skip if value is the same
if (Equals(k.Value, value))
return;
// Update existing key value
Keyframes.SetKeyframe(i, value);
UpdatePreviewValue();
return;
}
}
// Add a new key
using (new TrackUndoBlock(this))
Keyframes.AddKeyframe(new KeyframesEditor.Keyframe(time, value));
}
}
private void OnLeftKeyClicked(Image image, MouseButton button)
{
if (button == MouseButton.Left && GetPreviousKeyframeFrame(Timeline.CurrentTime, out var frame))
{
Timeline.OnSeek(frame);
}
}
///
public override bool GetPreviousKeyframeFrame(float time, out int result)
{
for (int i = Keyframes.Keyframes.Count - 1; i >= 0; i--)
{
var k = Keyframes.Keyframes[i];
if (k.Time < time)
{
result = Mathf.FloorToInt(k.Time * Timeline.FramesPerSecond);
return true;
}
}
return base.GetPreviousKeyframeFrame(time, out result);
}
private void UpdateKeyframes()
{
if (Keyframes == null || Timeline == null)
return;
bool wasVisible = Keyframes.Visible;
Keyframes.Visible = Visible;
if (!Visible)
{
if (wasVisible)
Keyframes.ClearSelection();
return;
}
Keyframes.KeyframesEditorContext = Timeline;
Keyframes.CustomViewPanning = Timeline.OnKeyframesViewPanning;
Keyframes.Bounds = new Rectangle(Timeline.StartOffset, Y + 1.0f, Timeline.Duration * Timeline.UnitsPerSecond * Timeline.Zoom, Height - 2.0f);
Keyframes.ViewScale = new Float2(Timeline.Zoom, 1.0f);
Keyframes.UpdateKeyframes();
}
private void OnKeyframesEdited()
{
UpdatePreviewValue();
Timeline.MarkAsEdited();
}
private void OnKeyframesEditingStart()
{
_keyframesEditingStartData = EditTrackAction.CaptureData(this);
}
private void OnKeyframesEditingEnd()
{
var after = EditTrackAction.CaptureData(this);
if (!FlaxEngine.Utils.ArraysEqual(_keyframesEditingStartData, after))
Timeline.AddBatchedUndoAction(new EditTrackAction(Timeline, this, _keyframesEditingStartData, after));
_keyframesEditingStartData = null;
}
private void UpdatePreviewValue()
{
var time = Timeline.CurrentTime;
var value = Keyframes.Evaluate(time);
_previewValue.Text = GetValueText(value);
}
///
public override object Evaluate(float time)
{
if (Keyframes != null)
return Keyframes.Evaluate(time);
return base.Evaluate(time);
}
///
/// Gets the default value for the given property type.
///
/// The type of the property.
/// The default value.
protected virtual object GetDefaultValue(Type propertyType)
{
var value = TypeUtils.GetDefaultValue(new ScriptType(propertyType));
if (value == null)
value = Activator.CreateInstance(propertyType);
return value;
}
///
protected override void OnMemberChanged(MemberInfo value, Type type)
{
base.OnMemberChanged(value, type);
Keyframes.ResetKeyframes();
if (type != null)
{
Keyframes.DefaultValue = GetDefaultValue(type);
}
}
///
protected override void OnVisibleChanged()
{
base.OnVisibleChanged();
UpdateKeyframes();
}
///
public override void OnTimelineChanged(Timeline timeline)
{
base.OnTimelineChanged(timeline);
Keyframes.Parent = timeline?.MediaPanel;
Keyframes.FPS = timeline?.FramesPerSecond;
UpdateKeyframes();
UpdatePreviewValue();
}
///
public override void OnUndo()
{
base.OnUndo();
UpdatePreviewValue();
}
///
public override void OnTimelineZoomChanged()
{
base.OnTimelineZoomChanged();
UpdateKeyframes();
}
///
public override void OnTimelineArrange()
{
base.OnTimelineArrange();
UpdateKeyframes();
}
///
public override void OnTimelineFpsChanged(float before, float after)
{
base.OnTimelineFpsChanged(before, after);
Keyframes.FPS = after;
UpdatePreviewValue();
}
///
public override void OnTimelineCurrentFrameChanged(int frame)
{
base.OnTimelineCurrentFrameChanged(frame);
UpdatePreviewValue();
}
///
public override void OnDestroy()
{
if (Keyframes != null)
{
Keyframes.Dispose();
Keyframes = null;
}
base.OnDestroy();
}
///
public new void OnKeyframesDeselect(IKeyframesEditor editor)
{
if (Keyframes != null && Keyframes.Visible)
Keyframes.OnKeyframesDeselect(editor);
}
///
public new void OnKeyframesSelection(IKeyframesEditor editor, ContainerControl control, Rectangle selection)
{
if (Keyframes != null && Keyframes.Visible)
Keyframes.OnKeyframesSelection(editor, control, selection);
}
///
public new int OnKeyframesSelectionCount()
{
return Keyframes != null && Keyframes.Visible ? Keyframes.OnKeyframesSelectionCount() : 0;
}
///
public new void OnKeyframesDelete(IKeyframesEditor editor)
{
if (Keyframes != null && Keyframes.Visible)
Keyframes.OnKeyframesDelete(editor);
}
///
public new void OnKeyframesMove(IKeyframesEditor editor, ContainerControl control, Float2 location, bool start, bool end)
{
if (Keyframes != null && Keyframes.Visible)
Keyframes.OnKeyframesMove(editor, control, location, start, end);
}
///
public new void OnKeyframesCopy(IKeyframesEditor editor, float? timeOffset, StringBuilder data)
{
if (Keyframes != null && Keyframes.Visible)
Keyframes.OnKeyframesCopy(editor, timeOffset, data);
}
///
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);
}
}
}