#if UNITY_EDITOR using System; using System.Linq; using System.Reflection; using UnityEditor; using UnityEngine.InputSystem.Layouts; using UnityEngine.InputSystem.Utilities; ////TODO: resolving bindings to actions needs to take "{id}" form into account namespace UnityEngine.InputSystem.Editor { // Helpers for doctoring around in InputActions using SerializedProperties. internal static class InputActionSerializationHelpers { public static string GetName(SerializedProperty element) { using (var nameProperty = element.FindPropertyRelative("m_Name")) { Debug.Assert(nameProperty != null, $"Cannot find m_Name property in {element.propertyPath}"); return nameProperty.stringValue; } } public static Guid GetId(SerializedProperty element) { using (var idProperty = element.FindPropertyRelative("m_Id")) { Debug.Assert(idProperty != null, $"Cannot find m_Id property in {element.propertyPath}"); return new Guid(idProperty.stringValue); } } public static int GetIndex(SerializedProperty arrayProperty, Guid id) { Debug.Assert(arrayProperty.isArray, $"Property {arrayProperty.propertyPath} is not an array"); for (var i = 0; i < arrayProperty.arraySize; ++i) { using (var element = arrayProperty.GetArrayElementAtIndex(i)) if (GetId(element) == id) return i; } return -1; } public static int GetIndex(SerializedProperty arrayProperty, SerializedProperty arrayElement) { return GetIndex(arrayProperty, GetId(arrayElement)); } public static int GetIndex(SerializedProperty arrayElement) { var arrayProperty = arrayElement.GetArrayPropertyFromElement(); return GetIndex(arrayProperty, arrayElement); } /// /// Starting with the given binding, find the composite that the binding belongs to. The given binding /// must either be the composite or be part of a composite. /// public static int GetCompositeStartIndex(SerializedProperty bindingArrayProperty, int bindingIndex) { for (var i = bindingIndex; i >= 0; --i) { var bindingProperty = bindingArrayProperty.GetArrayElementAtIndex(i); var bindingFlags = (InputBinding.Flags)bindingProperty.FindPropertyRelative("m_Flags").intValue; if ((bindingFlags & InputBinding.Flags.Composite) != 0) return i; Debug.Assert((bindingFlags & InputBinding.Flags.PartOfComposite) != 0, "Binding is neither a composite nor part of a composite"); } return -1; } public static int GetCompositePartCount(SerializedProperty bindingArrayProperty, int bindingIndex) { var compositeStartIndex = GetCompositeStartIndex(bindingArrayProperty, bindingIndex); if (compositeStartIndex == -1) return 0; var numParts = 0; for (var i = compositeStartIndex + 1; i < bindingArrayProperty.arraySize; ++i, ++numParts) { var bindingProperty = bindingArrayProperty.GetArrayElementAtIndex(i); var bindingFlags = (InputBinding.Flags)bindingProperty.FindPropertyRelative("m_Flags").intValue; if ((bindingFlags & InputBinding.Flags.PartOfComposite) == 0) break; } return numParts; } public static int ConvertBindingIndexOnActionToBindingIndexInArray(SerializedProperty bindingArrayProperty, string actionName, int bindingIndexOnAction) { var bindingCount = bindingArrayProperty.arraySize; var indexOnAction = -1; var indexInArray = 0; for (; indexInArray < bindingCount; ++indexInArray) { var bindingActionName = bindingArrayProperty.GetArrayElementAtIndex(indexInArray).FindPropertyRelative("m_Action") .stringValue; if (actionName.Equals(bindingActionName, StringComparison.InvariantCultureIgnoreCase)) { ++indexOnAction; if (indexOnAction == bindingIndexOnAction) return indexInArray; } } return indexInArray; } public static SerializedProperty AddElement(SerializedProperty arrayProperty, string name, int index = -1) { var uniqueName = FindUniqueName(arrayProperty, name); if (index < 0) index = arrayProperty.arraySize; arrayProperty.InsertArrayElementAtIndex(index); var elementProperty = arrayProperty.GetArrayElementAtIndex(index); elementProperty.ResetValuesToDefault(); elementProperty.FindPropertyRelative("m_Name").stringValue = uniqueName; elementProperty.FindPropertyRelative("m_Id").stringValue = Guid.NewGuid().ToString(); return elementProperty; } public static SerializedProperty AddActionMap(SerializedObject asset, int index = -1) { if (!(asset.targetObject is InputActionAsset)) throw new InvalidOperationException( $"Can only add action maps to InputActionAsset objects (actual object is {asset.targetObject}"); var mapArrayProperty = asset.FindProperty("m_ActionMaps"); var name = FindUniqueName(mapArrayProperty, "New action map"); if (index < 0) index = mapArrayProperty.arraySize; mapArrayProperty.InsertArrayElementAtIndex(index); var mapProperty = mapArrayProperty.GetArrayElementAtIndex(index); mapProperty.FindPropertyRelative("m_Name").stringValue = name; mapProperty.FindPropertyRelative("m_Id").stringValue = Guid.NewGuid().ToString(); mapProperty.FindPropertyRelative("m_Actions").ClearArray(); mapProperty.FindPropertyRelative("m_Bindings").ClearArray(); return mapProperty; } public static void DeleteActionMap(SerializedObject asset, Guid id) { var mapArrayProperty = asset.FindProperty("m_ActionMaps"); var mapIndex = GetIndex(mapArrayProperty, id); if (mapIndex == -1) throw new ArgumentException($"No map with id {id} in {asset}", nameof(id)); mapArrayProperty.DeleteArrayElementAtIndex(mapIndex); } // Append a new action to the end of the set. public static SerializedProperty AddAction(SerializedProperty actionMap, int index = -1) { var actionsArrayProperty = actionMap.FindPropertyRelative("m_Actions"); if (index < 0) index = actionsArrayProperty.arraySize; var actionName = FindUniqueName(actionsArrayProperty, "New action"); actionsArrayProperty.InsertArrayElementAtIndex(index); var actionProperty = actionsArrayProperty.GetArrayElementAtIndex(index); actionProperty.FindPropertyRelative("m_Name").stringValue = actionName; actionProperty.FindPropertyRelative("m_Type").intValue = (int)InputActionType.Button; // Default to creating button actions. actionProperty.FindPropertyRelative("m_Id").stringValue = Guid.NewGuid().ToString(); actionProperty.FindPropertyRelative("m_ExpectedControlType").stringValue = "Button"; actionProperty.FindPropertyRelative("m_Flags").intValue = 0; actionProperty.FindPropertyRelative("m_Interactions").stringValue = ""; actionProperty.FindPropertyRelative("m_Processors").stringValue = ""; return actionProperty; } public static void DeleteActionAndBindings(SerializedProperty actionMap, Guid actionId) { using (var actionsArrayProperty = actionMap.FindPropertyRelative("m_Actions")) using (var bindingsArrayProperty = actionMap.FindPropertyRelative("m_Bindings")) { // Find index of action. var actionIndex = GetIndex(actionsArrayProperty, actionId); if (actionIndex == -1) throw new ArgumentException($"No action with ID {actionId} in {actionMap.propertyPath}", nameof(actionId)); using (var actionsProperty = actionsArrayProperty.GetArrayElementAtIndex(actionIndex)) { var actionName = GetName(actionsProperty); var actionIdString = actionId.ToString(); // Delete all bindings that refer to the action by ID or name. for (var i = 0; i < bindingsArrayProperty.arraySize; ++i) { using (var bindingProperty = bindingsArrayProperty.GetArrayElementAtIndex(i)) using (var bindingActionProperty = bindingProperty.FindPropertyRelative("m_Action")) { var targetAction = bindingActionProperty.stringValue; if (targetAction.Equals(actionName, StringComparison.InvariantCultureIgnoreCase) || targetAction == actionIdString) { bindingsArrayProperty.DeleteArrayElementAtIndex(i); --i; } } } } actionsArrayProperty.DeleteArrayElementAtIndex(actionIndex); } } // Equivalent to InputAction.AddBinding(). public static SerializedProperty AddBinding(SerializedProperty actionProperty, SerializedProperty actionMapProperty = null, SerializedProperty afterBinding = null, string groups = "", string path = "", string name = "", string interactions = "", string processors = "", InputBinding.Flags flags = InputBinding.Flags.None) { var bindingsArrayProperty = actionMapProperty != null ? actionMapProperty.FindPropertyRelative("m_Bindings") : actionProperty.FindPropertyRelative("m_SingletonActionBindings"); var bindingsCount = bindingsArrayProperty.arraySize; var actionName = actionProperty.FindPropertyRelative("m_Name").stringValue; int bindingIndex; if (afterBinding != null) { // If we're supposed to put the binding right after another binding, find the // binding's index. Also, if it's a composite, skip past all its parts. bindingIndex = GetIndex(bindingsArrayProperty, afterBinding); if (IsCompositeBinding(afterBinding)) bindingIndex += GetCompositePartCount(bindingsArrayProperty, bindingIndex); ++bindingIndex; // Put it *after* the binding. } else { // Find the index of the last binding for the action in the array. var indexOfLastBindingForAction = -1; for (var i = 0; i < bindingsCount; ++i) { var bindingProperty = bindingsArrayProperty.GetArrayElementAtIndex(i); var bindingActionName = bindingProperty.FindPropertyRelative("m_Action").stringValue; if (actionName.Equals(bindingActionName, StringComparison.InvariantCultureIgnoreCase)) indexOfLastBindingForAction = i; } // Insert after last binding or at end of array. bindingIndex = indexOfLastBindingForAction != -1 ? indexOfLastBindingForAction + 1 : bindingsCount; } ////TODO: bind using {id} rather than action name return AddBindingToBindingArray(bindingsArrayProperty, bindingIndex: bindingIndex, actionName: actionName, groups: groups, path: path, name: name, interactions: interactions, processors: processors, flags: flags); } public static SerializedProperty AddBindingToBindingArray(SerializedProperty bindingsArrayProperty, int bindingIndex = -1, string actionName = "", string groups = "", string path = "", string name = "", string interactions = "", string processors = "", InputBinding.Flags flags = InputBinding.Flags.None) { Debug.Assert(bindingsArrayProperty != null); Debug.Assert(bindingsArrayProperty.isArray, "SerializedProperty is not an array of bindings"); Debug.Assert(bindingIndex == -1 || (bindingIndex >= 0 && bindingIndex <= bindingsArrayProperty.arraySize)); if (bindingIndex == -1) bindingIndex = bindingsArrayProperty.arraySize; bindingsArrayProperty.InsertArrayElementAtIndex(bindingIndex); var newBindingProperty = bindingsArrayProperty.GetArrayElementAtIndex(bindingIndex); newBindingProperty.FindPropertyRelative("m_Path").stringValue = path; newBindingProperty.FindPropertyRelative("m_Groups").stringValue = groups; newBindingProperty.FindPropertyRelative("m_Interactions").stringValue = interactions; newBindingProperty.FindPropertyRelative("m_Processors").stringValue = processors; newBindingProperty.FindPropertyRelative("m_Flags").intValue = (int)flags; newBindingProperty.FindPropertyRelative("m_Action").stringValue = actionName; newBindingProperty.FindPropertyRelative("m_Name").stringValue = name; newBindingProperty.FindPropertyRelative("m_Id").stringValue = Guid.NewGuid().ToString(); ////FIXME: this likely leaves m_Bindings in the map for singleton actions unsync'd in some cases return newBindingProperty; } public static void ChangeBinding(SerializedProperty bindingProperty, string path = null, string groups = null, string interactions = null, string processors = null, string action = null) { // Path. if (!string.IsNullOrEmpty(path)) { var pathProperty = bindingProperty.FindPropertyRelative("m_Path"); pathProperty.stringValue = path; } // Groups. if (!string.IsNullOrEmpty(groups)) { var groupsProperty = bindingProperty.FindPropertyRelative("m_Groups"); groupsProperty.stringValue = groups; } // Interactions. if (!string.IsNullOrEmpty(interactions)) { var interactionsProperty = bindingProperty.FindPropertyRelative("m_Interactions"); interactionsProperty.stringValue = interactions; } // Processors. if (!string.IsNullOrEmpty(processors)) { var processorsProperty = bindingProperty.FindPropertyRelative("m_Processors"); processorsProperty.stringValue = processors; } // Action. if (!string.IsNullOrEmpty(action)) { var actionProperty = bindingProperty.FindPropertyRelative("m_Action"); actionProperty.stringValue = action; } } public static void DeleteBinding(SerializedProperty bindingArrayProperty, Guid id) { // If it's a composite, delete all its parts first. var bindingIndex = GetIndex(bindingArrayProperty, id); var bindingProperty = bindingArrayProperty.GetArrayElementAtIndex(bindingIndex); var bindingFlags = (InputBinding.Flags)bindingProperty.FindPropertyRelative("m_Flags").intValue; if ((bindingFlags & InputBinding.Flags.Composite) != 0) { for (var partIndex = bindingIndex + 1; partIndex < bindingArrayProperty.arraySize;) { var part = bindingArrayProperty.GetArrayElementAtIndex(partIndex); var flags = (InputBinding.Flags)part.FindPropertyRelative("m_Flags").intValue; if ((flags & InputBinding.Flags.PartOfComposite) == 0) break; bindingArrayProperty.DeleteArrayElementAtIndex(partIndex); } } bindingArrayProperty.DeleteArrayElementAtIndex(bindingIndex); } public static void AssignUniqueIDs(SerializedProperty element) { // Assign new ID to map. AssignUniqueID(element); // foreach (var child in element.GetChildren()) { if (!child.isArray) continue; var fieldType = child.GetFieldType(); if (fieldType == typeof(InputBinding[]) || fieldType == typeof(InputAction[]) || fieldType == typeof(InputActionMap)) { ////TODO: update bindings that refer to actions by {id} for (var i = 0; i < child.arraySize; ++i) using (var childElement = child.GetArrayElementAtIndex(i)) AssignUniqueIDs(childElement); } } } public static void AssignUniqueID(SerializedProperty property) { var idProperty = property.FindPropertyRelative("m_Id"); idProperty.stringValue = Guid.NewGuid().ToString(); } public static void EnsureUniqueName(SerializedProperty arrayElement) { var arrayProperty = arrayElement.GetArrayPropertyFromElement(); var arrayIndexOfElement = arrayElement.GetIndexOfArrayElement(); var nameProperty = arrayElement.FindPropertyRelative("m_Name"); var baseName = nameProperty.stringValue; nameProperty.stringValue = FindUniqueName(arrayProperty, baseName, ignoreIndex: arrayIndexOfElement); } [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Justification = "False positive (possibly caused by lambda expression?).")] public static string FindUniqueName(SerializedProperty arrayProperty, string baseName, int ignoreIndex = -1) { return StringHelpers.MakeUniqueName(baseName, Enumerable.Range(0, arrayProperty.arraySize), index => { if (index == ignoreIndex) return string.Empty; var elementProperty = arrayProperty.GetArrayElementAtIndex(index); var nameProperty = elementProperty.FindPropertyRelative("m_Name"); if (nameProperty == null) throw new ArgumentException($"Cannot find m_Name property in elements of array", nameof(arrayProperty)); return nameProperty.stringValue; }); } public static void RenameAction(SerializedProperty actionProperty, SerializedProperty actionMapProperty, string newName) { // Make sure name is unique. var actionsArrayProperty = actionMapProperty.FindPropertyRelative("m_Actions"); var uniqueName = FindUniqueName(actionsArrayProperty, newName, actionProperty.GetIndexOfArrayElement()); // Update all bindings that refer to the action. var nameProperty = actionProperty.FindPropertyRelative("m_Name"); var oldName = nameProperty.stringValue; var bindingsProperty = actionMapProperty.FindPropertyRelative("m_Bindings"); for (var i = 0; i < bindingsProperty.arraySize; i++) { var element = bindingsProperty.GetArrayElementAtIndex(i); var actionNameProperty = element.FindPropertyRelative("m_Action"); if (actionNameProperty.stringValue.Equals(oldName, StringComparison.InvariantCultureIgnoreCase)) actionNameProperty.stringValue = uniqueName; } // Update name. nameProperty.stringValue = uniqueName; } public static void RenameActionMap(SerializedProperty actionMapProperty, string newName) { // Make sure name is unique in InputActionAsset. var assetObject = actionMapProperty.serializedObject; var mapsArrayProperty = assetObject.FindProperty("m_ActionMaps"); var uniqueName = FindUniqueName(mapsArrayProperty, newName); // Assign to map. var nameProperty = actionMapProperty.FindPropertyRelative("m_Name"); nameProperty.stringValue = uniqueName; } public static void RenameComposite(SerializedProperty compositeGroupProperty, string newName) { var nameProperty = compositeGroupProperty.FindPropertyRelative("m_Name"); nameProperty.stringValue = newName; } public static SerializedProperty AddCompositeBinding(SerializedProperty actionProperty, SerializedProperty actionMapProperty, string compositeName, Type compositeType = null, string groups = "", bool addPartBindings = true) { var newProperty = AddBinding(actionProperty, actionMapProperty); newProperty.FindPropertyRelative("m_Name").stringValue = ObjectNames.NicifyVariableName(compositeName); newProperty.FindPropertyRelative("m_Path").stringValue = compositeName; newProperty.FindPropertyRelative("m_Flags").intValue = (int)InputBinding.Flags.Composite; if (addPartBindings) { var fields = compositeType.GetFields(BindingFlags.GetField | BindingFlags.Public | BindingFlags.Instance); foreach (var field in fields) { // Skip fields that aren't marked with [InputControl] attribute. if (field.GetCustomAttribute(false) == null) continue; var partProperty = AddBinding(actionProperty, actionMapProperty, groups: groups); partProperty.FindPropertyRelative("m_Name").stringValue = field.Name; partProperty.FindPropertyRelative("m_Flags").intValue = (int)InputBinding.Flags.PartOfComposite; } } return newProperty; } public static bool IsCompositeBinding(SerializedProperty bindingProperty) { using (var flagsProperty = bindingProperty.FindPropertyRelative("m_Flags")) { var flags = (InputBinding.Flags)flagsProperty.intValue; return (flags & InputBinding.Flags.Composite) != 0; } } public static SerializedProperty ChangeCompositeBindingType(SerializedProperty bindingProperty, NameAndParameters nameAndParameters) { var bindingsArrayProperty = bindingProperty.GetArrayPropertyFromElement(); Debug.Assert(bindingsArrayProperty != null, "SerializedProperty is not an array of bindings"); var bindingIndex = bindingProperty.GetIndexOfArrayElement(); Debug.Assert(IsCompositeBinding(bindingProperty), $"Binding {bindingProperty.propertyPath} is not a composite"); // If the composite still has the default name, change it to the default // one for the new composite type. var pathProperty = bindingProperty.FindPropertyRelative("m_Path"); var nameProperty = bindingProperty.FindPropertyRelative("m_Name"); if (nameProperty.stringValue == ObjectNames.NicifyVariableName(NameAndParameters.Parse(pathProperty.stringValue).name)) nameProperty.stringValue = ObjectNames.NicifyVariableName(nameAndParameters.name); pathProperty.stringValue = nameAndParameters.ToString(); // Adjust part bindings if we have information on the registered composite. If we don't have // a type, we don't know about the parts. In that case, leave part bindings untouched. var compositeType = InputBindingComposite.s_Composites.LookupTypeRegistration(nameAndParameters.name); if (compositeType != null) { var actionName = bindingProperty.FindPropertyRelative("m_Action").stringValue; // Repurpose existing part bindings for the new composite or add any part bindings that // we're missing. var fields = compositeType.GetFields(BindingFlags.GetField | BindingFlags.Public | BindingFlags.Instance); var partIndex = 0; var partBindingsStartIndex = bindingIndex + 1; foreach (var field in fields) { // Skip fields that aren't marked with [InputControl] attribute. if (field.GetCustomAttribute(false) == null) continue; // See if we can reuse an existing part binding. SerializedProperty partProperty = null; if (partBindingsStartIndex + partIndex < bindingsArrayProperty.arraySize) { ////REVIEW: this should probably look up part bindings by name rather than going sequentially var element = bindingsArrayProperty.GetArrayElementAtIndex(partBindingsStartIndex + partIndex); if (((InputBinding.Flags)element.FindPropertyRelative("m_Flags").intValue & InputBinding.Flags.PartOfComposite) != 0) partProperty = element; } // If not, insert a new binding. if (partProperty == null) { partProperty = AddBindingToBindingArray(bindingsArrayProperty, partBindingsStartIndex + partIndex, flags: InputBinding.Flags.PartOfComposite); } // Initialize. partProperty.FindPropertyRelative("m_Name").stringValue = ObjectNames.NicifyVariableName(field.Name); partProperty.FindPropertyRelative("m_Action").stringValue = actionName; ++partIndex; } ////REVIEW: when we allow adding the same part multiple times, we may want to do something smarter here // Delete extraneous part bindings. while (partBindingsStartIndex + partIndex < bindingsArrayProperty.arraySize) { var element = bindingsArrayProperty.GetArrayElementAtIndex(partBindingsStartIndex + partIndex); if (((InputBinding.Flags)element.FindPropertyRelative("m_Flags").intValue & InputBinding.Flags.PartOfComposite) == 0) break; bindingsArrayProperty.DeleteArrayElementAtIndex(partBindingsStartIndex + partIndex); // No incrementing of partIndex. } } return bindingProperty; } public static void ReplaceBindingGroup(SerializedObject asset, string oldBindingGroup, string newBindingGroup, bool deleteOrphanedBindings = false) { var mapArrayProperty = asset.FindProperty("m_ActionMaps"); var mapCount = mapArrayProperty.arraySize; for (var k = 0; k < mapCount; ++k) { var actionMapProperty = mapArrayProperty.GetArrayElementAtIndex(k); var bindingsArrayProperty = actionMapProperty.FindPropertyRelative("m_Bindings"); var bindingsCount = bindingsArrayProperty.arraySize; for (var i = 0; i < bindingsCount; ++i) { var bindingProperty = bindingsArrayProperty.GetArrayElementAtIndex(i); var groupsProperty = bindingProperty.FindPropertyRelative("m_Groups"); var groups = groupsProperty.stringValue; // Ignore bindings not belonging to any control scheme. if (string.IsNullOrEmpty(groups)) continue; var groupsArray = groups.Split(InputBinding.Separator); var numGroups = groupsArray.LengthSafe(); var didRename = false; for (var n = 0; n < numGroups; ++n) { if (string.Compare(groupsArray[n], oldBindingGroup, StringComparison.InvariantCultureIgnoreCase) != 0) continue; if (string.IsNullOrEmpty(newBindingGroup)) { ArrayHelpers.EraseAt(ref groupsArray, n); --n; --numGroups; } else groupsArray[n] = newBindingGroup; didRename = true; } if (!didRename) continue; if (groupsArray != null) groupsProperty.stringValue = string.Join(InputBinding.kSeparatorString, groupsArray); else { if (deleteOrphanedBindings) { // Binding no long belongs to any binding group. Delete it. bindingsArrayProperty.DeleteArrayElementAtIndex(i); --i; --bindingsCount; } else { groupsProperty.stringValue = string.Empty; } } } } } public static void RemoveUnusedBindingGroups(SerializedProperty binding, ReadOnlyArray controlSchemes) { var groupsProperty = binding.FindPropertyRelative(nameof(InputBinding.m_Groups)); groupsProperty.stringValue = string.Join(InputBinding.kSeparatorString, groupsProperty.stringValue .Split(InputBinding.Separator) .Where(g => controlSchemes.Any(c => c.bindingGroup.Equals(g, StringComparison.InvariantCultureIgnoreCase)))); } } } #endif // UNITY_EDITOR