// Copyright (c) 2012-2021 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 FlaxEngine; using FlaxEngine.GUI; namespace FlaxEditor.GUI.Timeline.Tracks { /// /// The base interface for . /// public interface IObjectTrack { /// /// Gets the object instance (may be null if reference is invalid or data is missing). /// object Object { get; } } /// /// The timeline track for animating managed objects. /// /// public abstract class ObjectTrack : Track, IObjectTrack { private bool _hasObject; /// /// The add button. /// protected Button _addButton; /// /// Gets the object instance (may be null if reference is invalid or data is missing). /// public abstract object Object { get; } /// protected ObjectTrack(ref TrackCreateOptions options) : base(ref options) { // 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); } /// public override void Update(float deltaTime) { base.Update(deltaTime); var obj = Object; var hasObject = obj != null; TitleTintColor = hasObject ? Color.White : Color.Red; if (hasObject != _hasObject) OnObjectExistenceChanged(obj); _hasObject = hasObject; } /// public override void OnDestroy() { _addButton = null; base.OnDestroy(); } /// /// Called when object existence gets changed (eg. object found after being not found). /// /// The object. protected virtual void OnObjectExistenceChanged(object obj) { } /// /// Called when showing the context menu for add button (for sub-tracks adding). /// /// The menu. protected virtual void OnShowAddContextMenu(ContextMenu.ContextMenu menu) { } /// /// The data for add property track buttons tag. /// public struct AddMemberTag { /// /// The member. /// public MemberInfo Member; /// /// The archetype. /// public TrackArchetype Archetype; } private static bool IsEventParamInvalid(ParameterInfo p) { var t = p.ParameterType; return !t.IsValueType; } /// /// Adds the object events track options to menu. /// /// The parent track. /// The menu. /// The object type. /// 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. /// The added options count. public static int AddEvents(Track parentTrack, ContextMenu.ContextMenu menu, Type type, Func 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(); menu.AddButton(sb.ToString(), OnAddMemberTrack).Tag = tag; count++; } return count; } /// /// Called on context menu button click to add new object property animation track. Button should have value assigned to the field. /// /// The button (with value assigned to the field.). 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(); } /// /// Adds the property or field track to this object track. /// /// The member (property or a field). /// The created track or null if failed. 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; } /// /// Adds the object properties animation track options to menu. /// /// The parent track. /// The menu. /// The object type. /// 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. /// The added options count. public static int AddProperties(Track parentTrack, ContextMenu.ContextMenu menu, Type type, Func 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; menu.AddButton(name + " " + m.Name, OnAddMemberTrack).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 InvalidGenericTypes = new HashSet { typeof(Action), typeof(Action<>), typeof(Action<,>), typeof(Func<>), typeof(Func<,>), typeof(Func<,,>), }; /// /// Maps the type to the default track archetype for it. /// protected static readonly Dictionary BasicTypesTrackArchetypes = new Dictionary { { 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(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() }, }; } }