431 lines
16 KiB
C#
431 lines
16 KiB
C#
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using FlaxEditor.CustomEditors;
|
|
using FlaxEditor.GUI.ContextMenu;
|
|
using FlaxEditor.Scripting;
|
|
using FlaxEngine;
|
|
using FlaxEngine.GUI;
|
|
|
|
namespace FlaxEditor.GUI.Timeline.Tracks
|
|
{
|
|
/// <summary>
|
|
/// The base interface for <see cref="ObjectTrack"/>.
|
|
/// </summary>
|
|
[HideInEditor]
|
|
public interface IObjectTrack
|
|
{
|
|
/// <summary>
|
|
/// Gets the object instance (may be null if reference is invalid or data is missing).
|
|
/// </summary>
|
|
object Object { get; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The timeline track for animating managed objects.
|
|
/// </summary>
|
|
/// <seealso cref="FlaxEditor.GUI.Timeline.Track" />
|
|
public abstract class ObjectTrack : ConductorTrack, IObjectTrack
|
|
{
|
|
private bool _hasObject;
|
|
|
|
/// <summary>
|
|
/// The add button.
|
|
/// </summary>
|
|
protected Button _addButton;
|
|
|
|
/// <summary>
|
|
/// Gets the object instance (may be null if reference is invalid or data is missing).
|
|
/// </summary>
|
|
public abstract object Object { get; }
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="ObjectTrack"/> class.
|
|
/// </summary>
|
|
/// <param name="options">The track initial options.</param>
|
|
/// <param name="useProxyKeyframes">True if show sub-tracks keyframes as a proxy on this track, otherwise false.</param>
|
|
protected ObjectTrack(ref TrackCreateOptions options, bool useProxyKeyframes = true)
|
|
: base(ref options, useProxyKeyframes)
|
|
{
|
|
// Add track button
|
|
const float buttonSize = 14;
|
|
_addButton = new Button
|
|
{
|
|
Text = "+",
|
|
TooltipText = "Add sub-tracks",
|
|
AutoFocus = true,
|
|
AnchorPreset = AnchorPresets.MiddleRight,
|
|
IsScrollable = false,
|
|
Offsets = new Margin(-buttonSize - 2 + _muteCheckbox.Offsets.Left, buttonSize, buttonSize * -0.5f, buttonSize),
|
|
Parent = this,
|
|
};
|
|
_addButton.Clicked += OnAddButtonClicked;
|
|
}
|
|
|
|
private void OnAddButtonClicked()
|
|
{
|
|
var menu = new ContextMenu.ContextMenu();
|
|
OnShowAddContextMenu(menu);
|
|
menu.Show(_addButton.Parent, _addButton.BottomLeft);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void Update(float deltaTime)
|
|
{
|
|
base.Update(deltaTime);
|
|
|
|
var obj = Object;
|
|
var hasObject = obj != null;
|
|
TitleTintColor = hasObject ? (Flags.HasFlag(TrackFlags.PrefabObject) ? Style.Current.ProgressNormal : Color.White) : Color.Red;
|
|
if (hasObject != _hasObject)
|
|
OnObjectExistenceChanged(obj);
|
|
_hasObject = hasObject;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void OnDestroy()
|
|
{
|
|
_addButton = null;
|
|
|
|
base.OnDestroy();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when object existence gets changed (eg. object found after being not found).
|
|
/// </summary>
|
|
/// <param name="obj">The object.</param>
|
|
protected virtual void OnObjectExistenceChanged(object obj)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when showing the context menu for add button (for sub-tracks adding).
|
|
/// </summary>
|
|
/// <param name="menu">The menu.</param>
|
|
protected virtual void OnShowAddContextMenu(ContextMenu.ContextMenu menu)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// The data for add property track buttons tag.
|
|
/// </summary>
|
|
[HideInEditor]
|
|
public struct AddMemberTag
|
|
{
|
|
/// <summary>
|
|
/// The member.
|
|
/// </summary>
|
|
public MemberInfo Member;
|
|
|
|
/// <summary>
|
|
/// The archetype.
|
|
/// </summary>
|
|
public TrackArchetype Archetype;
|
|
}
|
|
|
|
private static bool IsEventParamInvalid(ParameterInfo p)
|
|
{
|
|
var t = p.ParameterType;
|
|
return !t.IsValueType;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds the object events track options to menu.
|
|
/// </summary>
|
|
/// <param name="parentTrack">The parent track.</param>
|
|
/// <param name="menu">The menu.</param>
|
|
/// <param name="type">The object type.</param>
|
|
/// <param name="memberCheck">The custom callback that can reject the members that should not be animated. Returns true if member is valid. Can be null to skip this feature.</param>
|
|
/// <returns>The added options count.</returns>
|
|
public static int AddEvents(Track parentTrack, ContextMenu.ContextMenu menu, Type type, Func<MemberInfo, bool> memberCheck = null)
|
|
{
|
|
int count = 0;
|
|
menu.Tag = parentTrack;
|
|
|
|
// TODO: implement editor-wide cache for animated properties per object type (add this in CodeEditingModule)
|
|
var members = type.GetMembers(BindingFlags.Public | BindingFlags.Instance);
|
|
|
|
var sb = new StringBuilder();
|
|
for (int i = 0; i < members.Length; i++)
|
|
{
|
|
var m = members[i] as MethodInfo;
|
|
if (m == null)
|
|
continue;
|
|
if (memberCheck != null && !memberCheck(m))
|
|
continue;
|
|
|
|
// Skip properties getters/setters and events add/remove
|
|
var mName = m.Name;
|
|
if (mName.StartsWith("get_", StringComparison.Ordinal) ||
|
|
mName.StartsWith("set_", StringComparison.Ordinal) ||
|
|
mName.StartsWith("add_", StringComparison.Ordinal) ||
|
|
mName.StartsWith("remove_", StringComparison.Ordinal))
|
|
continue;
|
|
|
|
// Allow to invoke only void functions with basic parameter types
|
|
var parameters = m.GetParameters();
|
|
if (m.ReturnType != typeof(void) ||
|
|
parameters.Length > 8 ||
|
|
m.IsGenericMethod ||
|
|
parameters.Any(IsEventParamInvalid))
|
|
continue;
|
|
|
|
var attributes = m.GetCustomAttributes();
|
|
|
|
// Check if has attribute to skip animating
|
|
if (attributes.Any(x => x is NoAnimateAttribute))
|
|
continue;
|
|
|
|
// Prevent from adding the same track twice
|
|
if (parentTrack.SubTracks.Any(x => x is MemberTrack y && y.MemberName == m.Name))
|
|
continue;
|
|
|
|
// Build function name for UI
|
|
sb.Clear();
|
|
sb.Append(mName);
|
|
sb.Append('(');
|
|
for (int j = 0; j < parameters.Length; j++)
|
|
{
|
|
if (j != 0)
|
|
sb.Append(", ");
|
|
var p = parameters[j];
|
|
if (!CustomEditorsUtil.InBuildTypeNames.TryGetValue(p.ParameterType, out var pName))
|
|
pName = p.ParameterType.Name;
|
|
sb.Append(pName);
|
|
sb.Append(' ');
|
|
sb.Append(p.Name);
|
|
}
|
|
sb.Append(')');
|
|
|
|
AddMemberTag tag;
|
|
tag.Member = m;
|
|
tag.Archetype = EventTrack.GetArchetype();
|
|
var tooltip = Surface.SurfaceUtils.GetVisualScriptMemberInfoDescription(new ScriptMemberInfo(m));
|
|
menu.AddButton(sb.ToString(), OnAddMemberTrack).LinkTooltip(tooltip).Tag = tag;
|
|
count++;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called on context menu button click to add new object property animation track. Button should have <see cref="AddMemberTag"/> value assigned to the <see cref="Control.Tag"/> field.
|
|
/// </summary>
|
|
/// <param name="button">The button (with <see cref="AddMemberTag"/> value assigned to the <see cref="Control.Tag"/> field.).</param>
|
|
public static void OnAddMemberTrack(ContextMenuButton button)
|
|
{
|
|
var tag = (AddMemberTag)button.Tag;
|
|
var parentTrack = (Track)button.ParentContextMenu.Tag;
|
|
|
|
var timeline = parentTrack.Timeline;
|
|
var track = (MemberTrack)timeline.NewTrack(tag.Archetype);
|
|
track.ParentTrack = parentTrack;
|
|
track.TrackIndex = parentTrack.TrackIndex + 1;
|
|
track.Name = Guid.NewGuid().ToString("N");
|
|
track.Member = tag.Member;
|
|
timeline.AddTrack(track);
|
|
|
|
parentTrack.Expand();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds the property or field track to this object track.
|
|
/// </summary>
|
|
/// <param name="m">The member (property or a field).</param>
|
|
/// <returns>The created track or null if failed.</returns>
|
|
public MemberTrack AddPropertyTrack(MemberInfo m)
|
|
{
|
|
if (SubTracks.Any(x => x is MemberTrack y && y.MemberName == m.Name))
|
|
return null;
|
|
if (GetPropertyTrackType(m, out var t))
|
|
return null;
|
|
if (GetPropertyTrackArchetype(t, out var archetype, out var name))
|
|
return null;
|
|
|
|
var timeline = Timeline;
|
|
var track = (MemberTrack)timeline.NewTrack(archetype);
|
|
track.ParentTrack = this;
|
|
track.TrackIndex = TrackIndex + 1;
|
|
track.Name = Guid.NewGuid().ToString("N");
|
|
track.Member = m;
|
|
timeline.AddTrack(track);
|
|
|
|
Expand();
|
|
|
|
return track;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds the object properties animation track options to menu.
|
|
/// </summary>
|
|
/// <param name="parentTrack">The parent track.</param>
|
|
/// <param name="menu">The menu.</param>
|
|
/// <param name="type">The object type.</param>
|
|
/// <param name="memberCheck">The custom callback that can reject the members that should not be animated. Returns true if member is valid. Can be null to skip this feature.</param>
|
|
/// <returns>The added options count.</returns>
|
|
public static int AddProperties(Track parentTrack, ContextMenu.ContextMenu menu, Type type, Func<MemberInfo, bool> memberCheck = null)
|
|
{
|
|
int count = 0;
|
|
menu.Tag = parentTrack;
|
|
|
|
// TODO: implement editor-wide cache for animated properties per object type (add this in CodeEditingModule)
|
|
var members = type.GetMembers(BindingFlags.Public | BindingFlags.Instance);
|
|
|
|
for (int i = 0; i < members.Length; i++)
|
|
{
|
|
var m = members[i];
|
|
|
|
// Prevent from adding the same track twice
|
|
if (parentTrack.SubTracks.Any(x => x is MemberTrack y && y.MemberName == m.Name))
|
|
continue;
|
|
|
|
if (GetPropertyTrackType(m, out var t))
|
|
continue;
|
|
|
|
if (memberCheck != null && !memberCheck(m))
|
|
continue;
|
|
|
|
var attributes = m.GetCustomAttributes();
|
|
|
|
// Check if has attribute to skip animating
|
|
if (attributes.Any(x => x is NoAnimateAttribute))
|
|
continue;
|
|
|
|
// Validate value type and pick the track archetype
|
|
if (GetPropertyTrackArchetype(t, out var archetype, out var name))
|
|
continue;
|
|
|
|
AddMemberTag tag;
|
|
tag.Member = m;
|
|
tag.Archetype = archetype;
|
|
var tooltip = Surface.SurfaceUtils.GetVisualScriptMemberInfoDescription(new ScriptMemberInfo(m));
|
|
menu.AddButton(name + " " + m.Name, OnAddMemberTrack).LinkTooltip(tooltip).Tag = tag;
|
|
count++;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
private static bool GetPropertyTrackType(MemberInfo m, out Type t)
|
|
{
|
|
t = null;
|
|
if (m is PropertyInfo p)
|
|
{
|
|
// Properties with read/write
|
|
if (!(p.CanRead && p.CanWrite && p.GetIndexParameters().GetLength(0) == 0))
|
|
return true;
|
|
|
|
t = p.PropertyType;
|
|
}
|
|
else if (m is FieldInfo f)
|
|
{
|
|
t = f.FieldType;
|
|
}
|
|
else
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool GetPropertyTrackArchetype(Type valueType, out TrackArchetype archetype, out string name)
|
|
{
|
|
// Validate value type and pick the track archetype
|
|
name = valueType.Name;
|
|
if (BasicTypesTrackArchetypes.TryGetValue(valueType, out archetype))
|
|
{
|
|
// Basic type
|
|
if (!CustomEditorsUtil.InBuildTypeNames.TryGetValue(valueType, out name))
|
|
name = valueType.Name;
|
|
}
|
|
else if (valueType.IsEnum)
|
|
{
|
|
// Enum
|
|
archetype = KeyframesPropertyTrack.GetArchetype();
|
|
}
|
|
else if (valueType.IsValueType)
|
|
{
|
|
// Structure
|
|
archetype = StructPropertyTrack.GetArchetype();
|
|
}
|
|
else if (typeof(FlaxEngine.Object).IsAssignableFrom(valueType))
|
|
{
|
|
// Flax object reference
|
|
archetype = ObjectReferencePropertyTrack.GetArchetype();
|
|
}
|
|
else if (CanAnimateObjectType(valueType))
|
|
{
|
|
// Nested object
|
|
archetype = ObjectPropertyTrack.GetArchetype();
|
|
}
|
|
else
|
|
{
|
|
// Not supported
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool CanAnimateObjectType(Type type)
|
|
{
|
|
if (InvalidGenericTypes.Contains(type) || (type.IsGenericType && InvalidGenericTypes.Contains(type.GetGenericTypeDefinition())))
|
|
return false;
|
|
|
|
// Skip delegates
|
|
if (typeof(MulticastDelegate).IsAssignableFrom(type.BaseType))
|
|
return false;
|
|
|
|
return !type.ContainsGenericParameters &&
|
|
!type.IsArray &&
|
|
!type.IsGenericType &&
|
|
type.IsClass;
|
|
}
|
|
|
|
private static readonly HashSet<Type> InvalidGenericTypes = new HashSet<Type>
|
|
{
|
|
typeof(Action), typeof(Action<>), typeof(Action<,>),
|
|
typeof(Func<>), typeof(Func<,>), typeof(Func<,,>),
|
|
};
|
|
|
|
/// <summary>
|
|
/// Maps the type to the default track archetype for it.
|
|
/// </summary>
|
|
protected static readonly Dictionary<Type, TrackArchetype> BasicTypesTrackArchetypes = new Dictionary<Type, TrackArchetype>
|
|
{
|
|
{ typeof(bool), KeyframesPropertyTrack.GetArchetype() },
|
|
{ typeof(byte), KeyframesPropertyTrack.GetArchetype() },
|
|
{ typeof(sbyte), KeyframesPropertyTrack.GetArchetype() },
|
|
{ typeof(char), KeyframesPropertyTrack.GetArchetype() },
|
|
{ typeof(short), KeyframesPropertyTrack.GetArchetype() },
|
|
{ typeof(ushort), KeyframesPropertyTrack.GetArchetype() },
|
|
{ typeof(int), KeyframesPropertyTrack.GetArchetype() },
|
|
{ typeof(uint), KeyframesPropertyTrack.GetArchetype() },
|
|
{ typeof(long), KeyframesPropertyTrack.GetArchetype() },
|
|
{ typeof(float), CurvePropertyTrack.GetArchetype() },
|
|
{ typeof(double), CurvePropertyTrack.GetArchetype() },
|
|
{ typeof(Vector2), CurvePropertyTrack.GetArchetype() },
|
|
{ typeof(Vector3), CurvePropertyTrack.GetArchetype() },
|
|
{ typeof(Vector4), CurvePropertyTrack.GetArchetype() },
|
|
{ typeof(Float2), CurvePropertyTrack.GetArchetype() },
|
|
{ typeof(Float3), CurvePropertyTrack.GetArchetype() },
|
|
{ typeof(Float4), CurvePropertyTrack.GetArchetype() },
|
|
{ typeof(Double2), CurvePropertyTrack.GetArchetype() },
|
|
{ typeof(Double3), CurvePropertyTrack.GetArchetype() },
|
|
{ typeof(Double4), CurvePropertyTrack.GetArchetype() },
|
|
{ typeof(Quaternion), CurvePropertyTrack.GetArchetype() },
|
|
{ typeof(Color), CurvePropertyTrack.GetArchetype() },
|
|
{ typeof(Color32), CurvePropertyTrack.GetArchetype() },
|
|
{ typeof(Guid), KeyframesPropertyTrack.GetArchetype() },
|
|
{ typeof(DateTime), KeyframesPropertyTrack.GetArchetype() },
|
|
{ typeof(TimeSpan), KeyframesPropertyTrack.GetArchetype() },
|
|
{ typeof(string), StringPropertyTrack.GetArchetype() },
|
|
};
|
|
}
|
|
}
|