#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS using System; using System.Collections.Generic; using System.Linq; using UnityEditor; namespace UnityEngine.InputSystem.Editor { [System.Serializable] internal class CutElement { private Guid id; internal Type type; public CutElement(Guid id, Type type) { this.id = id; this.type = type; } public int GetIndexOfProperty(InputActionsEditorState state) { if (type == typeof(InputActionMap)) { var actionMap = state.serializedObject ?.FindProperty(nameof(InputActionAsset.m_ActionMaps)) ?.FirstOrDefault(s => InputActionSerializationHelpers.GetId(s).Equals(id)); return actionMap.GetIndexOfArrayElement(); } if (type == typeof(InputAction)) { var action = Selectors.GetActionMapAtIndex(state, actionMapIndex(state))?.wrappedProperty.FindPropertyRelative("m_Actions").FirstOrDefault(a => InputActionSerializationHelpers.GetId(a).Equals(id)); return action.GetIndexOfArrayElement(); } if (type == typeof(InputBinding)) { var binding = Selectors.GetBindingForId(state, id.ToString(), out _); return binding.GetIndexOfArrayElement(); } return -1; } public int actionMapIndex(InputActionsEditorState state) => type == typeof(InputActionMap) ? GetIndexOfProperty(state) : GetActionMapIndex(state); private int GetActionMapIndex(InputActionsEditorState state) { var actionMaps = state.serializedObject?.FindProperty(nameof(InputActionAsset.m_ActionMaps)); var cutActionMapIndex = state.serializedObject ?.FindProperty(nameof(InputActionAsset.m_ActionMaps)) ?.FirstOrDefault(s => s.FindPropertyRelative("m_Id").stringValue.Equals(id)).GetIndexOfArrayElement(); if (type == typeof(InputBinding)) cutActionMapIndex = actionMaps.FirstOrDefault(map => map.FindPropertyRelative("m_Bindings").Select(InputActionSerializationHelpers.GetId).Contains(id)).GetIndexOfArrayElement(); else if (type == typeof(InputAction)) cutActionMapIndex = actionMaps.FirstOrDefault(map => map.FindPropertyRelative("m_Actions").Select(InputActionSerializationHelpers.GetId).Contains(id)).GetIndexOfArrayElement(); return cutActionMapIndex ?? -1; } } internal struct InputActionsEditorState { public int selectedActionMapIndex { get {return m_selectedActionMapIndex; } } public int selectedActionIndex { get {return m_selectedActionIndex; } } public int selectedBindingIndex { get {return m_selectedBindingIndex; } } public SelectionType selectionType { get {return m_selectionType; } } public SerializedObject serializedObject { get; } // Note that state doesn't own this disposable object private readonly List cutElements => m_CutElements; // Control schemes public int selectedControlSchemeIndex { get { return m_selectedControlSchemeIndex; } } public int selectedDeviceRequirementIndex { get {return m_selectedDeviceRequirementIndex; } } public InputControlScheme selectedControlScheme => m_ControlScheme; // TODO Bad this either po internal InputActionsEditorSessionAnalytic m_Analytics; [SerializeField] int m_selectedActionMapIndex; [SerializeField] int m_selectedActionIndex; [SerializeField] int m_selectedBindingIndex; [SerializeField] SelectionType m_selectionType; [SerializeField] int m_selectedControlSchemeIndex; [SerializeField] int m_selectedDeviceRequirementIndex; private List m_CutElements; internal bool hasCutElements => m_CutElements != null && m_CutElements.Count > 0; public InputActionsEditorState( InputActionsEditorSessionAnalytic analytics, SerializedObject inputActionAsset, int selectedActionMapIndex = 0, int selectedActionIndex = 0, int selectedBindingIndex = 0, SelectionType selectionType = SelectionType.Action, InputControlScheme selectedControlScheme = default, int selectedControlSchemeIndex = -1, int selectedDeviceRequirementIndex = -1, List cutElements = null) { Debug.Assert(inputActionAsset != null); m_Analytics = analytics; serializedObject = inputActionAsset; m_selectedActionMapIndex = selectedActionMapIndex; m_selectedActionIndex = selectedActionIndex; m_selectedBindingIndex = selectedBindingIndex; m_selectionType = selectionType; m_ControlScheme = selectedControlScheme; m_selectedControlSchemeIndex = selectedControlSchemeIndex; m_selectedDeviceRequirementIndex = selectedDeviceRequirementIndex; m_CutElements = cutElements; } public InputActionsEditorState(InputActionsEditorState other, SerializedObject asset) { m_Analytics = other.m_Analytics; // Assign serialized object, not that this might be equal to other.serializedObject, // a slight variation of it with any kind of changes or a completely different one. // Hence, we do our best here to keep any selections consistent by remapping objects // based on GUIDs (IDs) and when it fails, attempt to select first object and if that // fails revert to not having a selection. This would even be true for domain reloads // if the asset would be modified during domain reload. serializedObject = asset; if (other.Equals(default(InputActionsEditorState))) { // This instance was created by default constructor and thus is missing some appropriate defaults: other.m_selectionType = SelectionType.Action; other.m_selectedControlSchemeIndex = -1; other.m_selectedDeviceRequirementIndex = -1; } // Attempt to preserve action map selection by GUID, otherwise select first or last resort none var otherSelectedActionMap = other.GetSelectedActionMap(); var actionMapCount = Selectors.GetActionMapCount(asset); m_selectedActionMapIndex = otherSelectedActionMap != null ? Selectors.GetActionMapIndexFromId(asset, InputActionSerializationHelpers.GetId(otherSelectedActionMap)) : actionMapCount > 0 ? 0 : -1; var selectedActionMap = m_selectedActionMapIndex >= 0 ? Selectors.GetActionMapAtIndex(asset, m_selectedActionMapIndex)?.wrappedProperty : null; // Attempt to preserve action selection by GUID, otherwise select first or last resort none var otherSelectedAction = m_selectedActionMapIndex >= 0 ? Selectors.GetSelectedAction(other) : null; m_selectedActionIndex = selectedActionMap != null && otherSelectedAction.HasValue ? Selectors.GetActionIndexFromId(selectedActionMap, InputActionSerializationHelpers.GetId(otherSelectedAction.Value.wrappedProperty)) : Selectors.GetActionCount(selectedActionMap) > 0 ? 0 : -1; // Attempt to preserve binding selection by GUID, otherwise select first or none m_selectedBindingIndex = -1; if (m_selectedActionMapIndex >= 0) { var otherSelectedBinding = Selectors.GetSelectedBinding(other); if (otherSelectedBinding != null) { var otherSelectedBindingId = InputActionSerializationHelpers.GetId(otherSelectedBinding.Value.wrappedProperty); var binding = Selectors.GetBindingForId(asset, otherSelectedBindingId.ToString(), out _); if (binding != null) m_selectedBindingIndex = binding.GetIndexOfArrayElement(); } } // Sanity check selection type and override any previous selection if not valid given indices // since we have remapped GUIDs to selection indices for another asset (SerializedObject) if (other.m_selectionType == SelectionType.Binding && m_selectedBindingIndex < 0) m_selectionType = SelectionType.Action; else m_selectionType = other.m_selectionType; m_selectedControlSchemeIndex = other.m_selectedControlSchemeIndex; m_selectedDeviceRequirementIndex = other.m_selectedDeviceRequirementIndex; // Selected ControlScheme index is serialized but we have to recreated actual object after domain reload. // In case asset is different from from others asset the index might not even be valid range so we need // to reattempt to preserve selection but range adapt. // Note that control schemes and device requirements currently lack any GUID/ID to be uniquely identified. var controlSchemesArrayProperty = serializedObject.FindProperty(nameof(InputActionAsset.m_ControlSchemes)); if (m_selectedControlSchemeIndex >= 0 && controlSchemesArrayProperty.arraySize > 0) { if (m_selectedControlSchemeIndex >= controlSchemesArrayProperty.arraySize) m_selectedControlSchemeIndex = 0; m_ControlScheme = new InputControlScheme( controlSchemesArrayProperty.GetArrayElementAtIndex(other.m_selectedControlSchemeIndex)); // TODO Preserve device requirement index } else { m_selectedControlSchemeIndex = -1; m_selectedDeviceRequirementIndex = -1; m_ControlScheme = new InputControlScheme(); } m_CutElements = other.cutElements; } public InputActionsEditorState With( int? selectedActionMapIndex = null, int? selectedActionIndex = null, int? selectedBindingIndex = null, SelectionType? selectionType = null, InputControlScheme? selectedControlScheme = null, int? selectedControlSchemeIndex = null, int? selectedDeviceRequirementIndex = null, List cutElements = null) { return new InputActionsEditorState( m_Analytics, serializedObject, selectedActionMapIndex ?? this.selectedActionMapIndex, selectedActionIndex ?? this.selectedActionIndex, selectedBindingIndex ?? this.selectedBindingIndex, selectionType ?? this.selectionType, // Control schemes selectedControlScheme ?? this.selectedControlScheme, selectedControlSchemeIndex ?? this.selectedControlSchemeIndex, selectedDeviceRequirementIndex ?? this.selectedDeviceRequirementIndex, cutElements ?? m_CutElements ); } public InputActionsEditorState ClearCutElements() { return new InputActionsEditorState( m_Analytics, serializedObject, selectedActionMapIndex, selectedActionIndex, selectedBindingIndex, selectionType, selectedControlScheme, selectedControlSchemeIndex, selectedDeviceRequirementIndex, cutElements: null); } public SerializedProperty GetActionMapByName(string actionMapName) { return serializedObject .FindProperty(nameof(InputActionAsset.m_ActionMaps)) .FirstOrDefault(p => p.FindPropertyRelative(nameof(InputActionMap.m_Name)).stringValue == actionMapName); } public InputActionsEditorState SelectAction(string actionName) { var actionMap = GetSelectedActionMap(); var actions = actionMap.FindPropertyRelative(nameof(InputActionMap.m_Actions)); for (var i = 0; i < actions.arraySize; i++) { if (actions.GetArrayElementAtIndex(i) .FindPropertyRelative(nameof(InputAction.m_Name)).stringValue != actionName) continue; return With(selectedActionIndex: i, selectionType: SelectionType.Action); } // If we cannot find the desired map we should return invalid index return With(selectedActionIndex: -1, selectionType: SelectionType.Action); } public InputActionsEditorState SelectAction(SerializedProperty state) { var index = state.GetIndexOfArrayElement(); return With(selectedActionIndex: index, selectionType: SelectionType.Action); } public InputActionsEditorState SelectActionMap(SerializedProperty actionMap) { var index = actionMap.GetIndexOfArrayElement(); return With(selectedBindingIndex: 0, selectedActionMapIndex: index, selectedActionIndex: 0); } public InputActionsEditorState SelectActionMap(string actionMapName) { var actionMap = GetActionMapByName(actionMapName); return With(selectedBindingIndex: 0, selectedActionMapIndex: actionMap.GetIndexOfArrayElement(), selectedActionIndex: 0, selectionType: SelectionType.Action); } public InputActionsEditorState SelectBinding(int index) { //if no binding selected (due to no bindings in list) set selection type to action if (index == -1) return With(selectedBindingIndex: index, selectionType: SelectionType.Action); return With(selectedBindingIndex: index, selectionType: SelectionType.Binding); } public InputActionsEditorState SelectAction(int index) { //if no action selected (no actions available) set selection type to none if (index == -1) return With(selectedActionIndex: index, selectionType: SelectionType.None); return With(selectedActionIndex: index, selectionType: SelectionType.Action); } public InputActionsEditorState SelectActionMap(int index) { if (index == -1) return With(selectedActionMapIndex: index, selectionType: SelectionType.None); return With(selectedBindingIndex: 0, selectedActionMapIndex: index, selectedActionIndex: 0, selectionType: SelectionType.Action); } public InputActionsEditorState CutActionOrBinding() { m_CutElements = new List(); var type = selectionType == SelectionType.Action ? typeof(InputAction) : typeof(InputBinding); var property = selectionType == SelectionType.Action ? Selectors.GetSelectedAction(this)?.wrappedProperty : Selectors.GetSelectedBinding(this)?.wrappedProperty; cutElements.Add(new CutElement(InputActionSerializationHelpers.GetId(property), type)); return With(cutElements: cutElements); } public InputActionsEditorState CutActionMaps() { m_CutElements = new List { new(InputActionSerializationHelpers.GetId(Selectors.GetSelectedActionMap(this)?.wrappedProperty), typeof(InputActionMap)) }; return With(cutElements: cutElements); } public IEnumerable GetDisabledActionMaps(List allActionMaps) { if (cutElements == null || cutElements == null) return Enumerable.Empty(); var cutActionMaps = cutElements.Where(cut => cut.type == typeof(InputActionMap)); var state = this; return allActionMaps.Where(actionMapName => { return cutActionMaps.Any(am => am.GetIndexOfProperty(state) == allActionMaps.IndexOf(actionMapName)); }); } public readonly bool IsBindingCut(int actionMapIndex, int bindingIndex) { if (cutElements == null) return false; var state = this; return cutElements.Any(cutElement => cutElement.actionMapIndex(state) == actionMapIndex && cutElement.GetIndexOfProperty(state) == bindingIndex && cutElement.type == typeof(InputBinding)); } public readonly bool IsActionCut(int actionMapIndex, int actionIndex) { if (cutElements == null) return false; var state = this; return cutElements.Any(cutElement => cutElement.actionMapIndex(state) == actionMapIndex && cutElement.GetIndexOfProperty(state) == actionIndex && cutElement.type == typeof(InputAction)); } public readonly bool IsActionMapCut(int actionMapIndex) { if (cutElements == null) return false; var state = this; return cutElements.Any(cutElement => cutElement.GetIndexOfProperty(state) == actionMapIndex && cutElement.type == typeof(InputActionMap)); } public readonly List GetCutElements() { return m_CutElements; } private SerializedProperty GetSelectedActionMap() { return Selectors.GetActionMapAtIndex(serializedObject, selectedActionMapIndex)?.wrappedProperty; } private readonly InputControlScheme m_ControlScheme; } internal enum SelectionType { None, Action, Binding } } #endif