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