566 lines
22 KiB
C#
566 lines
22 KiB
C#
#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
|