using System; using System.Collections.Generic; using System.Linq; using UnityEditor.IMGUI.Controls; using UnityEditorInternal; using UnityEngine; using UnityEngine.Timeline; #if !UNITY_2020_2_OR_NEWER using L10n = UnityEditor.Timeline.L10n; #endif namespace UnityEditor.Timeline { class BindingTreeViewDataSource : TreeViewDataSource { struct BindingGroup : IEquatable, IComparable { public readonly string GroupName; public readonly string Path; public readonly Type Type; public BindingGroup(string path, string groupName, Type type) { Path = path; GroupName = groupName; Type = type; } public string groupDisplayName => string.IsNullOrEmpty(Path) ? GroupName : string.Format($"{Path} : {GroupName}"); public bool Equals(BindingGroup other) => GroupName == other.GroupName && Type == other.Type && Path == other.Path; public int CompareTo(BindingGroup other) => GetHashCode() - other.GetHashCode(); public override bool Equals(object obj) => obj is BindingGroup other && Equals(other); public override int GetHashCode() { return HashUtility.CombineHash(GroupName != null ? GroupName.GetHashCode() : 0, Type != null ? Type.GetHashCode() : 0, Path != null ? Path.GetHashCode() : 0); } } static readonly string s_DefaultValue = L10n.Tr("{0} (Default Value)"); public const int RootID = int.MinValue; public const int GroupID = -1; private readonly AnimationClip m_Clip; private readonly CurveDataSource m_CurveDataSource; public BindingTreeViewDataSource( TreeViewController treeView, AnimationClip clip, CurveDataSource curveDataSource) : base(treeView) { m_Clip = clip; showRootItem = false; m_CurveDataSource = curveDataSource; } void SetupRootNodeSettings() { showRootItem = false; SetExpanded(RootID, true); SetExpanded(GroupID, true); } public override void FetchData() { if (m_Clip == null) return; var bindings = AnimationUtility.GetCurveBindings(m_Clip) .Union(AnimationUtility.GetObjectReferenceCurveBindings(m_Clip)) .ToArray(); // a sorted linear list of nodes var results = bindings.GroupBy(GetBindingGroup, p => p, CreateTuple) .OrderBy(t => t.Item1.Path) .ThenBy(NamePrioritySort) // this makes component ordering match the animation window .ThenBy(t => t.Item1.Type.ToString()) .ThenBy(t => t.Item1.GroupName).ToArray(); m_RootItem = new CurveTreeViewNode(RootID, null, "root", null) { children = new List(1) }; if (results.Any()) { var groupingNode = new CurveTreeViewNode(GroupID, m_RootItem, m_CurveDataSource.groupingName, bindings) { children = new List() }; m_RootItem.children.Add(groupingNode); foreach (var r in results) { var key = r.Item1; var nodeBindings = r.Item2; FillMissingTransformCurves(nodeBindings); if (nodeBindings.Count == 1) groupingNode.children.Add(CreateLeafNode(nodeBindings[0], groupingNode, PropertyName(nodeBindings[0], true))); else if (nodeBindings.Count > 1) { var childBindings = nodeBindings.OrderBy(BindingSort).ToArray(); var parent = new CurveTreeViewNode(key.GetHashCode(), groupingNode, key.groupDisplayName, childBindings) {children = new List()}; groupingNode.children.Add(parent); foreach (var b in childBindings) parent.children.Add(CreateLeafNode(b, parent, PropertyName(b, false))); } } SetupRootNodeSettings(); } m_NeedRefreshRows = true; } public void UpdateData() { m_TreeView.ReloadData(); } string GroupName(EditorCurveBinding binding) { var propertyName = m_CurveDataSource.ModifyPropertyDisplayName(binding.path, binding.propertyName); return CleanUpArrayBinding(AnimationWindowUtility.NicifyPropertyGroupName(binding.type, propertyName), true); } static string CleanUpArrayBinding(string propertyName, bool isGroup) { const string arrayIndicator = ".Array.data["; const string arrayDisplay = ".data["; var arrayIndex = propertyName.LastIndexOf(arrayIndicator, StringComparison.Ordinal); if (arrayIndex == -1) return propertyName; if (isGroup) propertyName = propertyName.Substring(0, arrayIndex); return propertyName.Replace(arrayIndicator, arrayDisplay); } string PropertyName(EditorCurveBinding binding, bool prependPathName) { var propertyName = m_CurveDataSource.ModifyPropertyDisplayName(binding.path, binding.propertyName); propertyName = CleanUpArrayBinding(AnimationWindowUtility.GetPropertyDisplayName(propertyName), false); if (binding.isPhantom) propertyName = string.Format(s_DefaultValue, propertyName); if (prependPathName && !string.IsNullOrEmpty(binding.path)) propertyName = $"{binding.path} : {propertyName}"; return propertyName; } BindingGroup GetBindingGroup(EditorCurveBinding binding) { return new BindingGroup(binding.path ?? string.Empty, GroupName(binding), binding.type); } static CurveTreeViewNode CreateLeafNode(EditorCurveBinding binding, TreeViewItem parent, string displayName) { return new CurveTreeViewNode(binding.GetHashCode(), parent, displayName, new[] { binding }, AnimationWindowUtility.ForceGrouping(binding)); } static void FillMissingTransformCurves(List bindings) { if (!AnimationWindowUtility.IsActualTransformCurve(bindings[0]) || bindings.Count >= 3) return; var binding = bindings[0]; var prefixPropertyName = binding.propertyName.Split('.').First(); binding.isPhantom = true; if (!bindings.Any(p => p.propertyName.EndsWith(".x"))) { binding.propertyName = prefixPropertyName + ".x"; bindings.Insert(0, binding); } if (!bindings.Any(p => p.propertyName.EndsWith(".y"))) { binding.propertyName = prefixPropertyName + ".y"; bindings.Insert(1, binding); } if (!bindings.Any(p => p.propertyName.EndsWith(".z"))) { binding.propertyName = prefixPropertyName + ".z"; bindings.Insert(2, binding); } } // make sure vectors and colors are sorted correctly in their subgroups static int BindingSort(EditorCurveBinding b) { return AnimationWindowUtility.GetComponentIndex(b.propertyName); } static int NamePrioritySort(ValueTuple> group) { if (group.Item1.Type != typeof(Transform)) return 0; switch (group.Item1.GroupName) { case "Position": return Int32.MinValue; case "Rotation": return Int32.MinValue + 1; case "Scale": return Int32.MinValue + 2; default: return 0; } } static ValueTuple> CreateTuple(BindingGroup key, IEnumerable items) { return new ValueTuple>() { Item1 = key, Item2 = items.ToList() }; } } }