123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565 |
- #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<ActionTreeItemBase> m_HiddenChildren;
- public bool hasChildrenIncludingHidden => hasChildren || (m_HiddenChildren != null && m_HiddenChildren.Count > 0);
- public IEnumerable<ActionTreeItemBase> hiddenChildren => m_HiddenChildren ?? Enumerable.Empty<ActionTreeItemBase>();
- public IEnumerable<ActionTreeItemBase> 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()");
- }
-
- /// <summary>
- /// Delete serialized data for the tree item and its children.
- /// </summary>
- public abstract void DeleteData();
-
- public abstract bool AcceptsDrop(ActionTreeItemBase item);
-
- /// <summary>
- /// Get information about where to drop an item of the given type and (optionally) the given index.
- /// </summary>
- 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<Texture2D>($"{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");
- }
- }
-
- /// <summary>
- /// Tree view item for an action map.
- /// </summary>
- /// <seealso cref="InputActionMap"/>
- 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);
- }
- }
- }
-
- /// <summary>
- /// Tree view item for an action.
- /// </summary>
- /// <see cref="InputAction"/>
- 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;
- }
-
- /// <summary>
- /// Add items for the bindings of just this action to the given parent tree item.
- /// </summary>
- 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;
- }
- }
- }
- }
-
- /// <summary>
- /// Tree view item for a binding.
- /// </summary>
- /// <seealso cref="InputBinding"/>
- 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) : "<No Binding>";
-
- 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;
- }
- }
-
- /// <summary>
- /// Tree view item for a composite binding.
- /// </summary>
- /// <seealso cref="InputBinding.isComposite"/>
- 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;
- }
- }
-
- /// <summary>
- /// Tree view item for bindings that are parts of composites.
- /// </summary>
- /// <see cref="InputBinding.isPartOfComposite"/>
- 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
|