#if UNITY_EDITOR using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEditor.IMGUI.Controls; using UnityEngine.InputSystem.Utilities; ////TODO: sync expanded state of SerializedProperties to expanded state of tree (will help preserving expansion in inspector) ////REVIEW: would be great to align all "[device]" parts of binding strings neatly in a column namespace UnityEngine.InputSystem.Editor { internal abstract class ActionTreeItemBase : TreeViewItem { public SerializedProperty property { get; } public virtual string expectedControlLayout => string.Empty; public virtual bool canRename => true; public virtual bool serializedDataIncludesChildren => false; public abstract GUIStyle colorTagStyle { get; } public string name { get; } public Guid guid { get; } public virtual bool showWarningIcon => false; // For some operations (like copy-paste), we want to include information that we have filtered out. internal List m_HiddenChildren; public bool hasChildrenIncludingHidden => hasChildren || (m_HiddenChildren != null && m_HiddenChildren.Count > 0); public IEnumerable hiddenChildren => m_HiddenChildren ?? Enumerable.Empty(); public IEnumerable childrenIncludingHidden { get { if (hasChildren) foreach (var child in children) if (child is ActionTreeItemBase item) yield return item; if (m_HiddenChildren != null) foreach (var child in m_HiddenChildren) yield return child; } } // Action data is generally stored in arrays. Action maps are stored in m_ActionMaps arrays in assets, // actions are stored in m_Actions arrays on maps and bindings are stored in m_Bindings arrays on maps. public SerializedProperty arrayProperty => property.GetArrayPropertyFromElement(); // Dynamically look up the array index instead of just taking it from `property`. // This makes sure whatever insertions or deletions we perform on the serialized data, // we get the right array index from an item. public int arrayIndex => InputActionSerializationHelpers.GetIndex(arrayProperty, guid); protected ActionTreeItemBase(SerializedProperty property) { this.property = property; // Look up name. var nameProperty = property.FindPropertyRelative("m_Name"); Debug.Assert(nameProperty != null, $"Cannot find m_Name property on {property.propertyPath}"); name = nameProperty.stringValue; // Look up ID. var idProperty = property.FindPropertyRelative("m_Id"); Debug.Assert(idProperty != null, $"Cannot find m_Id property on {property.propertyPath}"); var idPropertyString = idProperty.stringValue; if (string.IsNullOrEmpty(idPropertyString)) { // This is somewhat questionable but we can't operate if we don't have IDs on the data used in the tree. // Rather than requiring users of the tree to set this up consistently, we assign IDs // on the fly, if necessary. guid = Guid.NewGuid(); idPropertyString = guid.ToString(); idProperty.stringValue = idPropertyString; idProperty.serializedObject.ApplyModifiedPropertiesWithoutUndo(); } else { guid = new Guid(idPropertyString); } // All our elements (maps, actions, bindings) carry unique IDs. We use their hash // codes as item IDs in the tree. This should result in stable item IDs that keep // identifying the right item across all reloads and tree mutations. id = guid.GetHashCode(); } public virtual void Rename(string newName) { Debug.Assert(!canRename, "Item is marked as allowing renames yet does not implement Rename()"); } /// /// Delete serialized data for the tree item and its children. /// public abstract void DeleteData(); public abstract bool AcceptsDrop(ActionTreeItemBase item); /// /// Get information about where to drop an item of the given type and (optionally) the given index. /// public abstract bool GetDropLocation(Type itemType, int? childIndex, ref SerializedProperty array, ref int arrayIndex); protected static class Styles { private static GUIStyle StyleWithBackground(string fileName) { return new GUIStyle("Label").WithNormalBackground(AssetDatabase.LoadAssetAtPath($"{InputActionTreeView.SharedResourcesPath}{fileName}.png")); } public static readonly GUIStyle yellowRect = StyleWithBackground("yellow"); public static readonly GUIStyle greenRect = StyleWithBackground("green"); public static readonly GUIStyle blueRect = StyleWithBackground("blue"); public static readonly GUIStyle pinkRect = StyleWithBackground("pink"); } } /// /// Tree view item for an action map. /// /// internal class ActionMapTreeItem : ActionTreeItemBase { public ActionMapTreeItem(SerializedProperty actionMapProperty) : base(actionMapProperty) { } public override GUIStyle colorTagStyle => Styles.yellowRect; public SerializedProperty bindingsArrayProperty => property.FindPropertyRelative("m_Bindings"); public SerializedProperty actionsArrayProperty => property.FindPropertyRelative("m_Actions"); public override bool serializedDataIncludesChildren => true; public override void Rename(string newName) { InputActionSerializationHelpers.RenameActionMap(property, newName); } public override void DeleteData() { var assetObject = property.serializedObject; if (!(assetObject.targetObject is InputActionAsset)) throw new InvalidOperationException( $"Action map must be part of InputActionAsset but is in {assetObject.targetObject} instead"); InputActionSerializationHelpers.DeleteActionMap(assetObject, guid); } public override bool AcceptsDrop(ActionTreeItemBase item) { return item is ActionTreeItem; } public override bool GetDropLocation(Type itemType, int? childIndex, ref SerializedProperty array, ref int arrayIndex) { // Drop actions into action array. if (itemType == typeof(ActionTreeItem)) { array = actionsArrayProperty; arrayIndex = childIndex ?? -1; return true; } // For action maps in assets, drop other action maps next to them. if (itemType == typeof(ActionMapTreeItem) && property.serializedObject.targetObject is InputActionAsset) { array = property.GetArrayPropertyFromElement(); arrayIndex = this.arrayIndex + 1; return true; } ////REVIEW: would be nice to be able to replace the entire contents of a map in the inspector by dropping in another map return false; } public static ActionMapTreeItem AddTo(TreeViewItem parent, SerializedProperty actionMapProperty) { var item = new ActionMapTreeItem(actionMapProperty); item.depth = parent.depth + 1; item.displayName = item.name; parent.AddChild(item); return item; } public void AddActionsTo(TreeViewItem parent) { AddActionsTo(parent, addBindings: false); } public void AddActionsAndBindingsTo(TreeViewItem parent) { AddActionsTo(parent, addBindings: true); } private void AddActionsTo(TreeViewItem parent, bool addBindings) { var actionsArrayProperty = this.actionsArrayProperty; Debug.Assert(actionsArrayProperty != null, $"Cannot find m_Actions in {property}"); for (var i = 0; i < actionsArrayProperty.arraySize; i++) { var actionProperty = actionsArrayProperty.GetArrayElementAtIndex(i); var actionItem = ActionTreeItem.AddTo(parent, property, actionProperty); if (addBindings) actionItem.AddBindingsTo(actionItem); } } public static void AddActionMapsFromAssetTo(TreeViewItem parent, SerializedObject assetObject) { var actionMapsArrayProperty = assetObject.FindProperty("m_ActionMaps"); Debug.Assert(actionMapsArrayProperty != null, $"Cannot find m_ActionMaps in {assetObject}"); Debug.Assert(actionMapsArrayProperty.isArray, $"m_ActionMaps in {assetObject} is not an array"); var mapCount = actionMapsArrayProperty.arraySize; for (var i = 0; i < mapCount; ++i) { var mapProperty = actionMapsArrayProperty.GetArrayElementAtIndex(i); AddTo(parent, mapProperty); } } } /// /// Tree view item for an action. /// /// internal class ActionTreeItem : ActionTreeItemBase { public ActionTreeItem(SerializedProperty actionMapProperty, SerializedProperty actionProperty) : base(actionProperty) { this.actionMapProperty = actionMapProperty; } public SerializedProperty actionMapProperty { get; } public override GUIStyle colorTagStyle => Styles.greenRect; public bool isSingletonAction => actionMapProperty == null; public override string expectedControlLayout { get { var expectedControlType = property.FindPropertyRelative("m_ExpectedControlType").stringValue; if (!string.IsNullOrEmpty(expectedControlType)) return expectedControlType; var type = property.FindPropertyRelative("m_Type").intValue; if (type == (int)InputActionType.Button) return "Button"; return null; } } public SerializedProperty bindingsArrayProperty => isSingletonAction ? property.FindPropertyRelative("m_SingletonActionBindings") : actionMapProperty.FindPropertyRelative("m_Bindings"); // If we're a singleton action (no associated action map property), we include all our bindings in the // serialized data. public override bool serializedDataIncludesChildren => actionMapProperty == null; public override void Rename(string newName) { InputActionSerializationHelpers.RenameAction(property, actionMapProperty, newName); } public override void DeleteData() { InputActionSerializationHelpers.DeleteActionAndBindings(actionMapProperty, guid); } public override bool AcceptsDrop(ActionTreeItemBase item) { return item is BindingTreeItem && !(item is PartOfCompositeBindingTreeItem); } public override bool GetDropLocation(Type itemType, int? childIndex, ref SerializedProperty array, ref int arrayIndex) { // Drop bindings into binding array. if (typeof(BindingTreeItem).IsAssignableFrom(itemType)) { array = bindingsArrayProperty; // Indexing by tree items is relative to each action but indexing in // binding array is global for all actions in a map. Adjust index accordingly. // NOTE: Bindings for any one action need not be stored contiguously in the binding array // so we can't just add something to the index of the first binding to the action. arrayIndex = InputActionSerializationHelpers.ConvertBindingIndexOnActionToBindingIndexInArray( array, name, childIndex ?? -1); return true; } // Drop other actions next to us. if (itemType == typeof(ActionTreeItem)) { array = arrayProperty; arrayIndex = this.arrayIndex + 1; return true; } return false; } public static ActionTreeItem AddTo(TreeViewItem parent, SerializedProperty actionMapProperty, SerializedProperty actionProperty) { var item = new ActionTreeItem(actionMapProperty, actionProperty); item.depth = parent.depth + 1; item.displayName = item.name; parent.AddChild(item); return item; } /// /// Add items for the bindings of just this action to the given parent tree item. /// public void AddBindingsTo(TreeViewItem parent) { var isSingleton = actionMapProperty == null; var bindingsArrayProperty = isSingleton ? property.FindPropertyRelative("m_SingletonActionBindings") : actionMapProperty.FindPropertyRelative("m_Bindings"); var bindingsCountInMap = bindingsArrayProperty.arraySize; var currentComposite = (CompositeBindingTreeItem)null; for (var i = 0; i < bindingsCountInMap; ++i) { var bindingProperty = bindingsArrayProperty.GetArrayElementAtIndex(i); // Skip if binding is not for action. var actionProperty = bindingProperty.FindPropertyRelative("m_Action"); Debug.Assert(actionProperty != null, $"Could not find m_Action in {bindingProperty}"); if (!actionProperty.stringValue.Equals(name, StringComparison.InvariantCultureIgnoreCase)) continue; // See what kind of binding we have. var flagsProperty = bindingProperty.FindPropertyRelative("m_Flags"); Debug.Assert(actionProperty != null, $"Could not find m_Flags in {bindingProperty}"); var flags = (InputBinding.Flags)flagsProperty.intValue; if ((flags & InputBinding.Flags.PartOfComposite) != 0 && currentComposite != null) { // Composite part binding. PartOfCompositeBindingTreeItem.AddTo(currentComposite, bindingProperty); } else if ((flags & InputBinding.Flags.Composite) != 0) { // Composite binding. currentComposite = CompositeBindingTreeItem.AddTo(parent, bindingProperty); } else { // "Normal" binding. BindingTreeItem.AddTo(parent, bindingProperty); currentComposite = null; } } } } /// /// Tree view item for a binding. /// /// internal class BindingTreeItem : ActionTreeItemBase { public BindingTreeItem(SerializedProperty bindingProperty) : base(bindingProperty) { path = property.FindPropertyRelative("m_Path").stringValue; groups = property.FindPropertyRelative("m_Groups").stringValue; action = property.FindPropertyRelative("m_Action").stringValue; } public string path { get; } public string groups { get; } public string action { get; } public override bool showWarningIcon => InputSystem.ShouldDrawWarningIconForBinding(path); public override bool canRename => false; public override GUIStyle colorTagStyle => Styles.blueRect; public string displayPath => !string.IsNullOrEmpty(path) ? InputControlPath.ToHumanReadableString(path) : ""; private ActionTreeItem actionItem { get { // Find the action we're under. for (var node = parent; node != null; node = node.parent) if (node is ActionTreeItem item) return item; return null; } } public override string expectedControlLayout { get { var currentActionItem = actionItem; return currentActionItem != null ? currentActionItem.expectedControlLayout : string.Empty; } } public override void DeleteData() { var currentActionItem = actionItem; Debug.Assert(currentActionItem != null, "BindingTreeItem should always have a parent action"); var bindingsArrayProperty = currentActionItem.bindingsArrayProperty; InputActionSerializationHelpers.DeleteBinding(bindingsArrayProperty, guid); } public override bool AcceptsDrop(ActionTreeItemBase item) { return false; } public override bool GetDropLocation(Type itemType, int? childIndex, ref SerializedProperty array, ref int arrayIndex) { // Drop bindings next to us. if (typeof(BindingTreeItem).IsAssignableFrom(itemType)) { array = arrayProperty; arrayIndex = this.arrayIndex + 1; return true; } return false; } public static BindingTreeItem AddTo(TreeViewItem parent, SerializedProperty bindingProperty) { var item = new BindingTreeItem(bindingProperty); item.depth = parent.depth + 1; item.displayName = item.displayPath; parent.AddChild(item); return item; } } /// /// Tree view item for a composite binding. /// /// internal class CompositeBindingTreeItem : BindingTreeItem { public CompositeBindingTreeItem(SerializedProperty bindingProperty) : base(bindingProperty) { } public override GUIStyle colorTagStyle => Styles.blueRect; public override bool canRename => true; public string compositeName => NameAndParameters.ParseName(path); public override void Rename(string newName) { InputActionSerializationHelpers.RenameComposite(property, newName); } public override bool AcceptsDrop(ActionTreeItemBase item) { return item is PartOfCompositeBindingTreeItem; } public override bool GetDropLocation(Type itemType, int? childIndex, ref SerializedProperty array, ref int arrayIndex) { // Drop part binding into composite. if (itemType == typeof(PartOfCompositeBindingTreeItem)) { array = arrayProperty; // Adjust child index by index of composite item itself. arrayIndex = childIndex != null ? this.arrayIndex + 1 + childIndex.Value // Dropping at #0 should put as our index plus one. : this.arrayIndex + 1 + InputActionSerializationHelpers.GetCompositePartCount(array, this.arrayIndex); return true; } // Drop other bindings next to us. if (typeof(BindingTreeItem).IsAssignableFrom(itemType)) { array = arrayProperty; arrayIndex = this.arrayIndex + 1 + InputActionSerializationHelpers.GetCompositePartCount(array, this.arrayIndex); return true; } return false; } public new static CompositeBindingTreeItem AddTo(TreeViewItem parent, SerializedProperty bindingProperty) { var item = new CompositeBindingTreeItem(bindingProperty); item.depth = parent.depth + 1; item.displayName = !string.IsNullOrEmpty(item.name) ? item.name : ObjectNames.NicifyVariableName(NameAndParameters.ParseName(item.path)); parent.AddChild(item); return item; } } /// /// Tree view item for bindings that are parts of composites. /// /// internal class PartOfCompositeBindingTreeItem : BindingTreeItem { public PartOfCompositeBindingTreeItem(SerializedProperty bindingProperty) : base(bindingProperty) { } public override GUIStyle colorTagStyle => Styles.pinkRect; public override bool canRename => false; public override string expectedControlLayout { get { if (m_ExpectedControlLayout == null) { var partName = name; var compositeName = ((CompositeBindingTreeItem)parent).compositeName; var layoutName = InputBindingComposite.GetExpectedControlLayoutName(compositeName, partName); m_ExpectedControlLayout = layoutName ?? ""; } return m_ExpectedControlLayout; } } private string m_ExpectedControlLayout; public new static PartOfCompositeBindingTreeItem AddTo(TreeViewItem parent, SerializedProperty bindingProperty) { var item = new PartOfCompositeBindingTreeItem(bindingProperty); item.depth = parent.depth + 1; item.displayName = $"{ObjectNames.NicifyVariableName(item.name)}: {item.displayPath}"; parent.AddChild(item); return item; } } } #endif // UNITY_EDITOR