#if UNITY_EDITOR using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using UnityEditor; using UnityEngine.InputSystem.Editor.Lists; using UnityEngine.InputSystem.Layouts; using UnityEngine.InputSystem.Utilities; ////REVIEW: when we start with a blank tree view state, we should initialize the control picker to select the control currently //// selected by the path property namespace UnityEngine.InputSystem.Editor { /// /// UI for editing properties of an . Right-most pane in action editor when /// binding is selected in middle pane. /// internal class InputBindingPropertiesView : PropertiesViewBase, IDisposable { public static FourCC k_GroupsChanged => new FourCC("GRPS"); public static FourCC k_PathChanged => new FourCC("PATH"); public static FourCC k_CompositeTypeChanged => new FourCC("COMP"); public static FourCC k_CompositePartAssignmentChanged => new FourCC("PART"); public InputBindingPropertiesView( SerializedProperty bindingProperty, Action onChange = null, InputControlPickerState controlPickerState = null, string expectedControlLayout = null, ReadOnlyArray controlSchemes = new ReadOnlyArray(), IEnumerable controlPathsToMatch = null) : base(InputActionSerializationHelpers.IsCompositeBinding(bindingProperty) ? "Composite" : "Binding", bindingProperty, onChange, expectedControlLayout) { m_BindingProperty = bindingProperty; m_GroupsProperty = bindingProperty.FindPropertyRelative("m_Groups"); m_PathProperty = bindingProperty.FindPropertyRelative("m_Path"); m_BindingGroups = m_GroupsProperty.stringValue .Split(new[] {InputBinding.Separator}, StringSplitOptions.RemoveEmptyEntries).ToList(); m_ExpectedControlLayout = expectedControlLayout; m_ControlSchemes = controlSchemes; var flags = (InputBinding.Flags)bindingProperty.FindPropertyRelative("m_Flags").intValue; m_IsPartOfComposite = (flags & InputBinding.Flags.PartOfComposite) != 0; m_IsComposite = (flags & InputBinding.Flags.Composite) != 0; // Set up control picker for m_Path. Not needed if the binding is a composite. if (!m_IsComposite) { m_ControlPickerState = controlPickerState ?? new InputControlPickerState(); m_ControlPathEditor = new InputControlPathEditor(m_PathProperty, m_ControlPickerState, OnPathChanged); m_ControlPathEditor.SetExpectedControlLayout(m_ExpectedControlLayout); if (controlPathsToMatch != null) m_ControlPathEditor.SetControlPathsToMatch(controlPathsToMatch); } } public void Dispose() { m_ControlPathEditor?.Dispose(); } protected override void DrawGeneralProperties() { var currentPath = m_PathProperty.stringValue; InputSystem.OnDrawCustomWarningForBindingPath(currentPath); if (m_IsComposite) { if (m_CompositeParameters == null) InitializeCompositeProperties(); // Composite type dropdown. var selectedCompositeType = EditorGUILayout.Popup(s_CompositeTypeLabel, m_SelectedCompositeType, m_CompositeTypeOptions); if (selectedCompositeType != m_SelectedCompositeType) { m_SelectedCompositeType = selectedCompositeType; OnCompositeTypeChanged(); } // Composite parameters. m_CompositeParameters.OnGUI(); } else { // Path. m_ControlPathEditor.OnGUI(); // Composite part. if (m_IsPartOfComposite) { if (m_CompositeParts == null) InitializeCompositePartProperties(); var selectedPart = EditorGUILayout.Popup(s_CompositePartAssignmentLabel, m_SelectedCompositePart, m_CompositePartOptions); if (selectedPart != m_SelectedCompositePart) { m_SelectedCompositePart = selectedPart; OnCompositePartAssignmentChanged(); } } // Show the specific controls which match the current path DrawMatchingControlPaths(); // Control scheme matrix. DrawUseInControlSchemes(); } } /// /// Used to keep track of which foldouts are expanded. /// private static bool showMatchingLayouts = false; private static Dictionary showMatchingChildLayouts = new Dictionary(); /// /// Finds all registered control paths implemented by concrete classes which match the current binding path and renders it. /// private void DrawMatchingControlPaths() { var path = m_ControlPathEditor.pathProperty.stringValue; if (path == string.Empty) return; var deviceLayoutPath = InputControlPath.TryGetDeviceLayout(path); var parsedPath = InputControlPath.Parse(path).ToArray(); // If the provided path is parseable into device and control components, draw UI which shows control layouts that match the path. if (parsedPath.Length >= 2 && !string.IsNullOrEmpty(deviceLayoutPath)) { bool matchExists = false; var rootDeviceLayout = EditorInputControlLayoutCache.TryGetLayout(deviceLayoutPath); bool isValidDeviceLayout = deviceLayoutPath == InputControlPath.Wildcard || (rootDeviceLayout != null && !rootDeviceLayout.isOverride && !rootDeviceLayout.hideInUI); // Exit early if a malformed device layout was provided, if (!isValidDeviceLayout) return; bool controlPathUsagePresent = parsedPath[1].usages.Count() > 0; bool hasChildDeviceLayouts = deviceLayoutPath == InputControlPath.Wildcard || EditorInputControlLayoutCache.HasChildLayouts(rootDeviceLayout.name); // If the path provided matches exactly one control path (i.e. has no ui-facing child device layouts or uses control usages), then exit early if (!controlPathUsagePresent && !hasChildDeviceLayouts) return; // Otherwise, we will show either all controls that match the current binding (if control usages are used) // or all controls in derived device layouts (if a no control usages are used). EditorGUILayout.BeginVertical(); showMatchingLayouts = EditorGUILayout.Foldout(showMatchingLayouts, "Derived Bindings"); if (showMatchingLayouts) { // If our control path contains a usage, make sure we render the binding that belongs to the root device layout first if (deviceLayoutPath != InputControlPath.Wildcard && controlPathUsagePresent) { matchExists |= DrawMatchingControlPathsForLayout(rootDeviceLayout, in parsedPath, true); } // Otherwise, just render the bindings that belong to child device layouts. The binding that matches the root layout is // already represented by the user generated control path itself. else { IEnumerable matchedChildLayouts = Enumerable.Empty(); if (deviceLayoutPath == InputControlPath.Wildcard) { matchedChildLayouts = EditorInputControlLayoutCache.allLayouts .Where(x => x.isDeviceLayout && !x.hideInUI && !x.isOverride && x.isGenericTypeOfDevice && x.baseLayouts.Count() == 0).OrderBy(x => x.displayName); } else { matchedChildLayouts = EditorInputControlLayoutCache.TryGetChildLayouts(rootDeviceLayout.name); } foreach (var childLayout in matchedChildLayouts) { matchExists |= DrawMatchingControlPathsForLayout(childLayout, in parsedPath); } } // Otherwise, indicate that no layouts match the current path. if (!matchExists) { if (controlPathUsagePresent) EditorGUILayout.HelpBox("No registered controls match this current binding. Some controls are only registered at runtime.", MessageType.Warning); else EditorGUILayout.HelpBox("No other registered controls match this current binding. Some controls are only registered at runtime.", MessageType.Warning); } } EditorGUILayout.EndVertical(); } } /// /// Returns true if the deviceLayout or any of its children has controls which match the provided parsed path. exist matching registered control paths. /// /// The device layout to draw control paths for /// The parsed path containing details of the Input Controls that can be matched private bool DrawMatchingControlPathsForLayout(InputControlLayout deviceLayout, in InputControlPath.ParsedPathComponent[] parsedPath, bool isRoot = false) { string deviceName = deviceLayout.displayName; string controlName = string.Empty; bool matchExists = false; for (int i = 0; i < deviceLayout.m_Controls.Length; i++) { ref InputControlLayout.ControlItem controlItem = ref deviceLayout.m_Controls[i]; if (InputControlPath.MatchControlComponent(ref parsedPath[1], ref controlItem, true)) { // If we've already located a match, append a ", " to the control name // This is to accomodate cases where multiple control items match the same path within a single device layout // Note, some controlItems have names but invalid displayNames (i.e. the Dualsense HID > leftTriggerButton) // There are instance where there are 2 control items with the same name inside a layout definition, however they are not // labeled significantly differently. // The notable example is that the Android Xbox and Android Dualshock layouts have 2 d-pad definitions, one is a "button" // while the other is an axis. controlName += matchExists ? $", {controlItem.name}" : controlItem.name; // if the parsePath has a 3rd component, try to match it with items in the controlItem's layout definition. if (parsedPath.Length == 3) { var controlLayout = EditorInputControlLayoutCache.TryGetLayout(controlItem.layout); if (controlLayout.isControlLayout && !controlLayout.hideInUI) { for (int j = 0; j < controlLayout.m_Controls.Count(); j++) { ref InputControlLayout.ControlItem controlLayoutItem = ref controlLayout.m_Controls[j]; if (InputControlPath.MatchControlComponent(ref parsedPath[2], ref controlLayoutItem)) { controlName += $"/{controlLayoutItem.name}"; matchExists = true; } } } } else { matchExists = true; } } } IEnumerable matchedChildLayouts = EditorInputControlLayoutCache.TryGetChildLayouts(deviceLayout.name); // If this layout does not have a match, or is the top level root layout, // skip over trying to draw any items for it, and immediately try processing the child layouts if (!matchExists) { foreach (var childLayout in matchedChildLayouts) { matchExists |= DrawMatchingControlPathsForLayout(childLayout, in parsedPath); } } // Otherwise, draw the items for it, and then only process the child layouts if the foldout is expanded. else { bool showLayout = false; EditorGUI.indentLevel++; if (matchedChildLayouts.Count() > 0 && !isRoot) { showMatchingChildLayouts.TryGetValue(deviceName, out showLayout); showMatchingChildLayouts[deviceName] = EditorGUILayout.Foldout(showLayout, $"{deviceName} > {controlName}"); } else { EditorGUILayout.LabelField($"{deviceName} > {controlName}"); } showLayout |= isRoot; if (showLayout) { foreach (var childLayout in matchedChildLayouts) { DrawMatchingControlPathsForLayout(childLayout, in parsedPath); } } EditorGUI.indentLevel--; } return matchExists; } /// /// Draw control scheme matrix that allows selecting which control schemes a particular /// binding appears in. /// private void DrawUseInControlSchemes() { if (m_ControlSchemes.Count <= 0) return; EditorGUILayout.Space(); EditorGUILayout.Space(); EditorGUILayout.LabelField(s_UseInControlSchemesLAbel, EditorStyles.boldLabel); EditorGUILayout.BeginVertical(); foreach (var scheme in m_ControlSchemes) { EditorGUI.BeginChangeCheck(); var result = EditorGUILayout.Toggle(scheme.name, m_BindingGroups.Contains(scheme.bindingGroup)); if (EditorGUI.EndChangeCheck()) { if (result) { m_BindingGroups.Add(scheme.bindingGroup); } else { m_BindingGroups.Remove(scheme.bindingGroup); } OnBindingGroupsChanged(); } } EditorGUILayout.EndVertical(); } private void InitializeCompositeProperties() { // Find name of current composite. var path = m_PathProperty.stringValue; var compositeNameAndParameters = NameAndParameters.Parse(path); var compositeName = compositeNameAndParameters.name; var compositeType = InputBindingComposite.s_Composites.LookupTypeRegistration(compositeName); // Collect all possible composite types. var selectedCompositeIndex = -1; var compositeTypeOptionsList = new List(); var compositeTypeList = new List(); var currentIndex = 0; foreach (var composite in InputBindingComposite.s_Composites.internedNames.Where(x => !InputBindingComposite.s_Composites.aliases.Contains(x)).OrderBy(x => x)) { if (!string.IsNullOrEmpty(m_ExpectedControlLayout)) { var valueType = InputBindingComposite.GetValueType(composite); if (valueType != null && !InputControlLayout.s_Layouts.ValueTypeIsAssignableFrom( new InternedString(m_ExpectedControlLayout), valueType)) continue; } if (InputBindingComposite.s_Composites.LookupTypeRegistration(composite) == compositeType) selectedCompositeIndex = currentIndex; var name = ObjectNames.NicifyVariableName(composite); compositeTypeOptionsList.Add(new GUIContent(name)); compositeTypeList.Add(composite); ++currentIndex; } // If the current composite type isn't a registered type, add it to the list as // an extra option. if (selectedCompositeIndex == -1) { selectedCompositeIndex = compositeTypeList.Count; compositeTypeOptionsList.Add(new GUIContent(ObjectNames.NicifyVariableName(compositeName))); compositeTypeList.Add(compositeName); } m_CompositeTypes = compositeTypeList.ToArray(); m_CompositeTypeOptions = compositeTypeOptionsList.ToArray(); m_SelectedCompositeType = selectedCompositeIndex; // Initialize parameters. m_CompositeParameters = new ParameterListView { onChange = OnCompositeParametersModified }; if (compositeType != null) m_CompositeParameters.Initialize(compositeType, compositeNameAndParameters.parameters); } private void InitializeCompositePartProperties() { var currentCompositePart = m_BindingProperty.FindPropertyRelative("m_Name").stringValue; ////REVIEW: this makes a lot of assumptions about the serialized data based on the one property we've been given in the ctor // Determine the name of the current composite type that the part belongs to. var bindingArrayProperty = m_BindingProperty.GetArrayPropertyFromElement(); var partBindingIndex = InputActionSerializationHelpers.GetIndex(bindingArrayProperty, m_BindingProperty); var compositeBindingIndex = InputActionSerializationHelpers.GetCompositeStartIndex(bindingArrayProperty, partBindingIndex); if (compositeBindingIndex == -1) return; var compositeBindingProperty = bindingArrayProperty.GetArrayElementAtIndex(compositeBindingIndex); var compositePath = compositeBindingProperty.FindPropertyRelative("m_Path").stringValue; var compositeNameAndParameters = NameAndParameters.Parse(compositePath); // Initialize option list from all parts available for the composite. var optionList = new List(); var nameList = new List(); var currentIndex = 0; var selectedPartNameIndex = -1; foreach (var partName in InputBindingComposite.GetPartNames(compositeNameAndParameters.name)) { if (partName.Equals(currentCompositePart, StringComparison.InvariantCultureIgnoreCase)) selectedPartNameIndex = currentIndex; var niceName = ObjectNames.NicifyVariableName(partName); optionList.Add(new GUIContent(niceName)); nameList.Add(partName); ++currentIndex; } // If currently selected part is not in list, add it as an option. if (selectedPartNameIndex == -1) { selectedPartNameIndex = nameList.Count; optionList.Add(new GUIContent(ObjectNames.NicifyVariableName(currentCompositePart))); nameList.Add(currentCompositePart); } m_CompositeParts = nameList.ToArray(); m_CompositePartOptions = optionList.ToArray(); m_SelectedCompositePart = selectedPartNameIndex; } private void OnCompositeParametersModified() { Debug.Assert(m_CompositeParameters != null); var path = m_PathProperty.stringValue; var nameAndParameters = NameAndParameters.Parse(path); nameAndParameters.parameters = m_CompositeParameters.GetParameters(); m_PathProperty.stringValue = nameAndParameters.ToString(); m_PathProperty.serializedObject.ApplyModifiedProperties(); OnPathChanged(); } private void OnBindingGroupsChanged() { m_GroupsProperty.stringValue = string.Join(InputBinding.kSeparatorString, m_BindingGroups.ToArray()); m_GroupsProperty.serializedObject.ApplyModifiedProperties(); onChange?.Invoke(k_GroupsChanged); } private void OnPathChanged() { m_BindingProperty.serializedObject.ApplyModifiedProperties(); onChange?.Invoke(k_PathChanged); } private void OnCompositeTypeChanged() { var nameAndParameters = new NameAndParameters { name = m_CompositeTypes[m_SelectedCompositeType], parameters = m_CompositeParameters.GetParameters() }; InputActionSerializationHelpers.ChangeCompositeBindingType(m_BindingProperty, nameAndParameters); m_PathProperty.serializedObject.ApplyModifiedProperties(); onChange?.Invoke(k_CompositeTypeChanged); } private void OnCompositePartAssignmentChanged() { m_BindingProperty.FindPropertyRelative("m_Name").stringValue = m_CompositeParts[m_SelectedCompositePart]; m_BindingProperty.serializedObject.ApplyModifiedProperties(); onChange?.Invoke(k_CompositePartAssignmentChanged); } private readonly bool m_IsComposite; private ParameterListView m_CompositeParameters; private int m_SelectedCompositeType; private GUIContent[] m_CompositeTypeOptions; private string[] m_CompositeTypes; private int m_SelectedCompositePart; private GUIContent[] m_CompositePartOptions; private string[] m_CompositeParts; private readonly SerializedProperty m_GroupsProperty; private readonly SerializedProperty m_BindingProperty; private readonly SerializedProperty m_PathProperty; private readonly InputControlPickerState m_ControlPickerState; private readonly InputControlPathEditor m_ControlPathEditor; private static readonly GUIContent s_CompositeTypeLabel = EditorGUIUtility.TrTextContent("Composite Type", "Type of composite. Allows changing the composite type retroactively. Doing so will modify the bindings that are part of the composite."); private static readonly GUIContent s_UseInControlSchemesLAbel = EditorGUIUtility.TrTextContent("Use in control scheme", "In which control schemes the binding is active. A binding can be used by arbitrary many control schemes. If a binding is not " + "assigned to a specific control schemes, it is active in all of them."); private static readonly GUIContent s_CompositePartAssignmentLabel = EditorGUIUtility.TrTextContent( "Composite Part", "The named part of the composite that the binding is assigned to. Multiple bindings may be assigned the same part. All controls from " + "all bindings that are assigned the same part will collectively feed values into that part of the composite."); private ReadOnlyArray m_ControlSchemes; private readonly List m_BindingGroups; private readonly string m_ExpectedControlLayout; } } #endif // UNITY_EDITOR