File size: 8,581 Bytes
18a519f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 | #if UNITY_EDITOR || PACKAGE_DOCS_GENERATION
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEngine.InputSystem.Layouts;
namespace UnityEngine.InputSystem.Editor
{
/// <summary>
/// Custom editor UI for editing control paths.
/// </summary>
/// <remarks>
/// This is the implementation underlying <see cref="InputControlPathDrawer"/>. It is useful primarily when
/// greater control is required than is offered by the <see cref="PropertyDrawer"/> mechanism. In particular,
/// it allows applying additional constraints such as requiring control paths to match ...
/// </remarks>
public sealed class InputControlPathEditor : IDisposable
{
/// <summary>
/// Initialize the control path editor.
/// </summary>
/// <param name="pathProperty"><see cref="string"/> type property that will receive the picked input control path.</param>
/// <param name="pickerState">Persistent editing state of the path editor. Used to retain state across domain reloads.</param>
/// <param name="onModified">Delegate that is called when the path has been modified.</param>
/// <param name="label">Optional label to display instead of display name of <paramref name="pathProperty"/>.</param>
/// <exception cref="ArgumentNullException"><paramref name="pathProperty"/> is <c>null</c>.</exception>
public InputControlPathEditor(SerializedProperty pathProperty, InputControlPickerState pickerState, Action onModified, GUIContent label = null)
{
if (pathProperty == null)
throw new ArgumentNullException(nameof(pathProperty));
this.pathProperty = pathProperty;
this.onModified = onModified;
m_PickerState = pickerState ?? new InputControlPickerState();
m_PathLabel = label ?? new GUIContent(pathProperty.displayName, pathProperty.GetTooltip());
}
public void Dispose()
{
m_PickerDropdown?.Dispose();
}
public void SetControlPathsToMatch(IEnumerable<string> controlPaths)
{
m_ControlPathsToMatch = controlPaths.ToArray();
m_PickerDropdown?.SetControlPathsToMatch(m_ControlPathsToMatch);
}
/// <summary>
/// Constrain the type of control layout that can be picked.
/// </summary>
/// <param name="expectedControlLayout">Name of the layout. This it the name as registered with
/// <see cref="InputSystem.RegisterLayout"/>.</param>.
/// <remarks>
/// <example>
/// <code>
/// // Pick only button controls.
/// editor.SetExpectedControlLayout("Button");
/// </code>
/// </example>
/// </remarks>
public void SetExpectedControlLayout(string expectedControlLayout)
{
m_ExpectedControlLayout = expectedControlLayout;
m_PickerDropdown?.SetExpectedControlLayout(m_ExpectedControlLayout);
}
public void SetExpectedControlLayoutFromAttribute()
{
var field = pathProperty.GetField();
if (field == null)
return;
var attribute = field.GetCustomAttribute<InputControlAttribute>();
if (attribute != null)
SetExpectedControlLayout(attribute.layout);
}
public void OnGUI()
{
EditorGUILayout.BeginHorizontal();
////FIXME: for some reason, the left edge doesn't align properly in GetRect()'s result; indentation issue?
var rect = GUILayoutUtility.GetRect(0, EditorGUIUtility.singleLineHeight);
rect.x += EditorGUIUtility.standardVerticalSpacing + 2;
rect.width -= EditorGUIUtility.standardVerticalSpacing * 2 + 4;
OnGUI(rect);
EditorGUILayout.EndHorizontal();
}
public void OnGUI(Rect rect, GUIContent label = null, SerializedProperty property = null, Action modifiedCallback = null)
{
var pathLabel = label ?? m_PathLabel;
var serializedProperty = property ?? pathProperty;
var lineRect = rect;
var labelRect = lineRect;
labelRect.width = EditorGUIUtility.labelWidth;
EditorGUI.LabelField(labelRect, pathLabel);
lineRect.x += labelRect.width;
lineRect.width -= labelRect.width;
var bindingTextRect = lineRect;
var editButtonRect = lineRect;
bindingTextRect.width -= 20;
editButtonRect.x += bindingTextRect.width;
editButtonRect.width = 20;
editButtonRect.height = 15;
var path = serializedProperty.stringValue;
////TODO: this should be cached; generates needless GC churn
var displayName = InputControlPath.ToHumanReadableString(path);
// Either show dropdown control that opens path picker or show path directly as
// text, if manual path editing is toggled on.
if (m_PickerState.manualPathEditMode)
{
////FIXME: for some reason the text field does not fill all the rect but rather adds large padding on the left
bindingTextRect.x -= 15;
bindingTextRect.width += 15;
EditorGUI.BeginChangeCheck();
path = EditorGUI.DelayedTextField(bindingTextRect, path);
if (EditorGUI.EndChangeCheck())
{
serializedProperty.stringValue = path;
serializedProperty.serializedObject.ApplyModifiedProperties();
(modifiedCallback ?? onModified).Invoke();
}
}
else
{
// Dropdown that shows binding text and allows opening control picker.
if (EditorGUI.DropdownButton(bindingTextRect, new GUIContent(displayName), FocusType.Keyboard))
{
SetExpectedControlLayoutFromAttribute(serializedProperty);
////TODO: for bindings that are part of composites, use the layout information from the [InputControl] attribute on the field
ShowDropdown(bindingTextRect, serializedProperty, modifiedCallback ?? onModified);
}
}
// Button to toggle between text edit mode.
m_PickerState.manualPathEditMode = GUI.Toggle(editButtonRect, m_PickerState.manualPathEditMode, "T",
EditorStyles.miniButton);
}
private void ShowDropdown(Rect rect, SerializedProperty serializedProperty, Action modifiedCallback)
{
if (m_PickerDropdown == null)
{
m_PickerDropdown = new InputControlPickerDropdown(
m_PickerState,
path =>
{
serializedProperty.stringValue = path;
m_PickerState.manualPathEditMode = false;
modifiedCallback();
});
}
m_PickerDropdown.SetPickedCallback(path =>
{
serializedProperty.stringValue = path;
m_PickerState.manualPathEditMode = false;
modifiedCallback();
});
m_PickerDropdown.SetControlPathsToMatch(m_ControlPathsToMatch);
m_PickerDropdown.SetExpectedControlLayout(m_ExpectedControlLayout);
m_PickerDropdown.Show(rect);
}
private void SetExpectedControlLayoutFromAttribute(SerializedProperty property)
{
var field = property.GetField();
if (field == null)
return;
var attribute = field.GetCustomAttribute<InputControlAttribute>();
if (attribute != null)
SetExpectedControlLayout(attribute.layout);
}
public SerializedProperty pathProperty { get; }
public Action onModified { get; }
private GUIContent m_PathLabel;
private string m_ExpectedControlLayout;
private string[] m_ControlPathsToMatch;
private InputControlScheme[] m_ControlSchemes;
private bool m_NeedToClearProgressBar;
private InputControlPickerDropdown m_PickerDropdown;
private readonly InputControlPickerState m_PickerState;
private InputActionRebindingExtensions.RebindingOperation m_RebindingOperation;
}
}
#endif // UNITY_EDITOR
|