A game about forced loneliness, made by TACStudios
1#if UNITY_EDITOR
2using System;
3using System.Collections.Generic;
4using System.Linq;
5using System.Reflection;
6using UnityEditor;
7using UnityEngine.InputSystem.Editor.Lists;
8using UnityEngine.InputSystem.Layouts;
9using UnityEngine.InputSystem.Utilities;
10
11////REVIEW: when we start with a blank tree view state, we should initialize the control picker to select the control currently
12//// selected by the path property
13
14namespace UnityEngine.InputSystem.Editor
15{
16 /// <summary>
17 /// UI for editing properties of an <see cref="InputBinding"/>. Right-most pane in action editor when
18 /// binding is selected in middle pane.
19 /// </summary>
20 internal class InputBindingPropertiesView : PropertiesViewBase, IDisposable
21 {
22 public static FourCC k_GroupsChanged => new FourCC("GRPS");
23 public static FourCC k_PathChanged => new FourCC("PATH");
24 public static FourCC k_CompositeTypeChanged => new FourCC("COMP");
25 public static FourCC k_CompositePartAssignmentChanged => new FourCC("PART");
26
27 public InputBindingPropertiesView(
28 SerializedProperty bindingProperty,
29 Action<FourCC> onChange = null,
30 InputControlPickerState controlPickerState = null,
31 string expectedControlLayout = null,
32 ReadOnlyArray<InputControlScheme> controlSchemes = new ReadOnlyArray<InputControlScheme>(),
33 IEnumerable<string> controlPathsToMatch = null)
34 : base(InputActionSerializationHelpers.IsCompositeBinding(bindingProperty) ? "Composite" : "Binding",
35 bindingProperty, onChange, expectedControlLayout)
36 {
37 m_BindingProperty = bindingProperty;
38 m_GroupsProperty = bindingProperty.FindPropertyRelative("m_Groups");
39 m_PathProperty = bindingProperty.FindPropertyRelative("m_Path");
40 m_BindingGroups = m_GroupsProperty.stringValue
41 .Split(new[] {InputBinding.Separator}, StringSplitOptions.RemoveEmptyEntries).ToList();
42 m_ExpectedControlLayout = expectedControlLayout;
43 m_ControlSchemes = controlSchemes;
44
45 var flags = (InputBinding.Flags)bindingProperty.FindPropertyRelative("m_Flags").intValue;
46 m_IsPartOfComposite = (flags & InputBinding.Flags.PartOfComposite) != 0;
47 m_IsComposite = (flags & InputBinding.Flags.Composite) != 0;
48
49 // Set up control picker for m_Path. Not needed if the binding is a composite.
50 if (!m_IsComposite)
51 {
52 m_ControlPickerState = controlPickerState ?? new InputControlPickerState();
53 m_ControlPathEditor = new InputControlPathEditor(m_PathProperty, m_ControlPickerState, OnPathChanged);
54 m_ControlPathEditor.SetExpectedControlLayout(m_ExpectedControlLayout);
55 if (controlPathsToMatch != null)
56 m_ControlPathEditor.SetControlPathsToMatch(controlPathsToMatch);
57 }
58 }
59
60 public void Dispose()
61 {
62 m_ControlPathEditor?.Dispose();
63 }
64
65 protected override void DrawGeneralProperties()
66 {
67 var currentPath = m_PathProperty.stringValue;
68 InputSystem.OnDrawCustomWarningForBindingPath(currentPath);
69
70 if (m_IsComposite)
71 {
72 if (m_CompositeParameters == null)
73 InitializeCompositeProperties();
74
75 // Composite type dropdown.
76 var selectedCompositeType = EditorGUILayout.Popup(s_CompositeTypeLabel, m_SelectedCompositeType, m_CompositeTypeOptions);
77 if (selectedCompositeType != m_SelectedCompositeType)
78 {
79 m_SelectedCompositeType = selectedCompositeType;
80 OnCompositeTypeChanged();
81 }
82
83 // Composite parameters.
84 m_CompositeParameters.OnGUI();
85 }
86 else
87 {
88 // Path.
89 m_ControlPathEditor.OnGUI();
90
91 // Composite part.
92 if (m_IsPartOfComposite)
93 {
94 if (m_CompositeParts == null)
95 InitializeCompositePartProperties();
96
97 var selectedPart = EditorGUILayout.Popup(s_CompositePartAssignmentLabel, m_SelectedCompositePart,
98 m_CompositePartOptions);
99 if (selectedPart != m_SelectedCompositePart)
100 {
101 m_SelectedCompositePart = selectedPart;
102 OnCompositePartAssignmentChanged();
103 }
104 }
105
106 // Show the specific controls which match the current path
107 DrawMatchingControlPaths();
108
109 // Control scheme matrix.
110 DrawUseInControlSchemes();
111 }
112 }
113
114 /// <summary>
115 /// Used to keep track of which foldouts are expanded.
116 /// </summary>
117 private static bool showMatchingLayouts = false;
118 private static Dictionary<string, bool> showMatchingChildLayouts = new Dictionary<string, bool>();
119
120 private static void DrawMatchingControlPaths(List<MatchingControlPath> matchingControlPaths)
121 {
122 foreach (var matchingControlPath in matchingControlPaths)
123 {
124 bool showLayout = false;
125 EditorGUI.indentLevel++;
126
127 var text = $"{matchingControlPath.deviceName} > {matchingControlPath.controlName}";
128 if (matchingControlPath.children.Count() > 0 && !matchingControlPath.isRoot)
129 {
130 showMatchingChildLayouts.TryGetValue(matchingControlPath.deviceName, out showLayout);
131 showMatchingChildLayouts[matchingControlPath.deviceName] = EditorGUILayout.Foldout(showLayout, text);
132 }
133 else
134 {
135 EditorGUILayout.LabelField(text);
136 }
137
138 showLayout |= matchingControlPath.isRoot;
139 if (showLayout)
140 DrawMatchingControlPaths(matchingControlPath.children);
141
142 EditorGUI.indentLevel--;
143 }
144 }
145
146 /// <summary>
147 /// Finds all registered control paths implemented by concrete classes which match the current binding path and renders it.
148 /// </summary>
149 private void DrawMatchingControlPaths()
150 {
151 bool controlPathUsagePresent = false;
152 List<MatchingControlPath> matchingControlPaths = MatchingControlPath.CollectMatchingControlPaths(m_ControlPathEditor.pathProperty.stringValue, showMatchingLayouts, ref controlPathUsagePresent);
153 if (matchingControlPaths == null || matchingControlPaths.Count != 0)
154 {
155 EditorGUILayout.BeginVertical();
156 showMatchingLayouts = EditorGUILayout.Foldout(showMatchingLayouts, "Derived Bindings");
157
158 if (showMatchingLayouts)
159 {
160 if (matchingControlPaths == null)
161 {
162 if (controlPathUsagePresent)
163 EditorGUILayout.HelpBox("No registered controls match this current binding. Some controls are only registered at runtime.", MessageType.Warning);
164 else
165 EditorGUILayout.HelpBox("No other registered controls match this current binding. Some controls are only registered at runtime.", MessageType.Warning);
166 }
167 else
168 {
169 DrawMatchingControlPaths(matchingControlPaths);
170 }
171 }
172
173 EditorGUILayout.EndVertical();
174 }
175 }
176
177 /// <summary>
178 /// Draw control scheme matrix that allows selecting which control schemes a particular
179 /// binding appears in.
180 /// </summary>
181 private void DrawUseInControlSchemes()
182 {
183 if (m_ControlSchemes.Count <= 0)
184 return;
185
186 EditorGUILayout.Space();
187 EditorGUILayout.Space();
188 EditorGUILayout.LabelField(s_UseInControlSchemesLAbel, EditorStyles.boldLabel);
189 EditorGUILayout.BeginVertical();
190
191 foreach (var scheme in m_ControlSchemes)
192 {
193 EditorGUI.BeginChangeCheck();
194 var result = EditorGUILayout.Toggle(scheme.name, m_BindingGroups.Contains(scheme.bindingGroup));
195 if (EditorGUI.EndChangeCheck())
196 {
197 if (result)
198 {
199 m_BindingGroups.Add(scheme.bindingGroup);
200 }
201 else
202 {
203 m_BindingGroups.Remove(scheme.bindingGroup);
204 }
205 OnBindingGroupsChanged();
206 }
207 }
208
209 EditorGUILayout.EndVertical();
210 }
211
212 private void InitializeCompositeProperties()
213 {
214 // Find name of current composite.
215 var path = m_PathProperty.stringValue;
216 var compositeNameAndParameters = NameAndParameters.Parse(path);
217 var compositeName = compositeNameAndParameters.name;
218 var compositeType = InputBindingComposite.s_Composites.LookupTypeRegistration(compositeName);
219
220 // Collect all possible composite types.
221 var selectedCompositeIndex = -1;
222 var compositeTypeOptionsList = new List<GUIContent>();
223 var compositeTypeList = new List<string>();
224 var currentIndex = 0;
225 foreach (var composite in InputBindingComposite.s_Composites.internedNames.Where(x =>
226 !InputBindingComposite.s_Composites.aliases.Contains(x)).OrderBy(x => x))
227 {
228 if (!string.IsNullOrEmpty(m_ExpectedControlLayout))
229 {
230 var valueType = InputBindingComposite.GetValueType(composite);
231 if (valueType != null &&
232 !InputControlLayout.s_Layouts.ValueTypeIsAssignableFrom(
233 new InternedString(m_ExpectedControlLayout), valueType))
234 continue;
235 }
236
237 if (InputBindingComposite.s_Composites.LookupTypeRegistration(composite) == compositeType)
238 selectedCompositeIndex = currentIndex;
239 var name = ObjectNames.NicifyVariableName(composite);
240 compositeTypeOptionsList.Add(new GUIContent(name));
241 compositeTypeList.Add(composite);
242 ++currentIndex;
243 }
244
245 // If the current composite type isn't a registered type, add it to the list as
246 // an extra option.
247 if (selectedCompositeIndex == -1)
248 {
249 selectedCompositeIndex = compositeTypeList.Count;
250 compositeTypeOptionsList.Add(new GUIContent(ObjectNames.NicifyVariableName(compositeName)));
251 compositeTypeList.Add(compositeName);
252 }
253
254 m_CompositeTypes = compositeTypeList.ToArray();
255 m_CompositeTypeOptions = compositeTypeOptionsList.ToArray();
256 m_SelectedCompositeType = selectedCompositeIndex;
257
258 // Initialize parameters.
259 m_CompositeParameters = new ParameterListView
260 {
261 onChange = OnCompositeParametersModified
262 };
263 if (compositeType != null)
264 m_CompositeParameters.Initialize(compositeType, compositeNameAndParameters.parameters);
265 }
266
267 private void InitializeCompositePartProperties()
268 {
269 var currentCompositePart = m_BindingProperty.FindPropertyRelative("m_Name").stringValue;
270
271 ////REVIEW: this makes a lot of assumptions about the serialized data based on the one property we've been given in the ctor
272 // Determine the name of the current composite type that the part belongs to.
273 var bindingArrayProperty = m_BindingProperty.GetArrayPropertyFromElement();
274 var partBindingIndex = InputActionSerializationHelpers.GetIndex(bindingArrayProperty, m_BindingProperty);
275 var compositeBindingIndex =
276 InputActionSerializationHelpers.GetCompositeStartIndex(bindingArrayProperty, partBindingIndex);
277 if (compositeBindingIndex == -1)
278 return;
279 var compositeBindingProperty = bindingArrayProperty.GetArrayElementAtIndex(compositeBindingIndex);
280 var compositePath = compositeBindingProperty.FindPropertyRelative("m_Path").stringValue;
281 var compositeNameAndParameters = NameAndParameters.Parse(compositePath);
282
283 // Initialize option list from all parts available for the composite.
284 var optionList = new List<GUIContent>();
285 var nameList = new List<string>();
286 var currentIndex = 0;
287 var selectedPartNameIndex = -1;
288 foreach (var partName in InputBindingComposite.GetPartNames(compositeNameAndParameters.name))
289 {
290 if (partName.Equals(currentCompositePart, StringComparison.InvariantCultureIgnoreCase))
291 selectedPartNameIndex = currentIndex;
292 var niceName = ObjectNames.NicifyVariableName(partName);
293 optionList.Add(new GUIContent(niceName));
294 nameList.Add(partName);
295 ++currentIndex;
296 }
297
298 // If currently selected part is not in list, add it as an option.
299 if (selectedPartNameIndex == -1)
300 {
301 selectedPartNameIndex = nameList.Count;
302 optionList.Add(new GUIContent(ObjectNames.NicifyVariableName(currentCompositePart)));
303 nameList.Add(currentCompositePart);
304 }
305
306 m_CompositeParts = nameList.ToArray();
307 m_CompositePartOptions = optionList.ToArray();
308 m_SelectedCompositePart = selectedPartNameIndex;
309 }
310
311 private void OnCompositeParametersModified()
312 {
313 Debug.Assert(m_CompositeParameters != null);
314
315 var path = m_PathProperty.stringValue;
316 var nameAndParameters = NameAndParameters.Parse(path);
317 nameAndParameters.parameters = m_CompositeParameters.GetParameters();
318
319 m_PathProperty.stringValue = nameAndParameters.ToString();
320 m_PathProperty.serializedObject.ApplyModifiedProperties();
321
322 OnPathChanged();
323 }
324
325 private void OnBindingGroupsChanged()
326 {
327 m_GroupsProperty.stringValue = string.Join(InputBinding.kSeparatorString, m_BindingGroups.ToArray());
328 m_GroupsProperty.serializedObject.ApplyModifiedProperties();
329
330 onChange?.Invoke(k_GroupsChanged);
331 }
332
333 private void OnPathChanged()
334 {
335 m_BindingProperty.serializedObject.ApplyModifiedProperties();
336 onChange?.Invoke(k_PathChanged);
337 }
338
339 private void OnCompositeTypeChanged()
340 {
341 var nameAndParameters = new NameAndParameters
342 {
343 name = m_CompositeTypes[m_SelectedCompositeType],
344 parameters = m_CompositeParameters.GetParameters()
345 };
346
347 InputActionSerializationHelpers.ChangeCompositeBindingType(m_BindingProperty, nameAndParameters);
348 m_PathProperty.serializedObject.ApplyModifiedProperties();
349
350 onChange?.Invoke(k_CompositeTypeChanged);
351 }
352
353 private void OnCompositePartAssignmentChanged()
354 {
355 m_BindingProperty.FindPropertyRelative("m_Name").stringValue = m_CompositeParts[m_SelectedCompositePart];
356 m_BindingProperty.serializedObject.ApplyModifiedProperties();
357
358 onChange?.Invoke(k_CompositePartAssignmentChanged);
359 }
360
361 private readonly bool m_IsComposite;
362 private ParameterListView m_CompositeParameters;
363 private int m_SelectedCompositeType;
364 private GUIContent[] m_CompositeTypeOptions;
365 private string[] m_CompositeTypes;
366
367 private int m_SelectedCompositePart;
368 private GUIContent[] m_CompositePartOptions;
369 private string[] m_CompositeParts;
370
371 private readonly SerializedProperty m_GroupsProperty;
372 private readonly SerializedProperty m_BindingProperty;
373 private readonly SerializedProperty m_PathProperty;
374
375 private readonly InputControlPickerState m_ControlPickerState;
376 private readonly InputControlPathEditor m_ControlPathEditor;
377
378 private static readonly GUIContent s_CompositeTypeLabel = EditorGUIUtility.TrTextContent("Composite Type",
379 "Type of composite. Allows changing the composite type retroactively. Doing so will modify the bindings that are part of the composite.");
380 private static readonly GUIContent s_UseInControlSchemesLAbel = EditorGUIUtility.TrTextContent("Use in control scheme",
381 "In which control schemes the binding is active. A binding can be used by arbitrary many control schemes. If a binding is not "
382 + "assigned to a specific control schemes, it is active in all of them.");
383 private static readonly GUIContent s_CompositePartAssignmentLabel = EditorGUIUtility.TrTextContent(
384 "Composite Part",
385 "The named part of the composite that the binding is assigned to. Multiple bindings may be assigned the same part. All controls from "
386 + "all bindings that are assigned the same part will collectively feed values into that part of the composite.");
387
388 private ReadOnlyArray<InputControlScheme> m_ControlSchemes;
389 private readonly List<string> m_BindingGroups;
390 private readonly string m_ExpectedControlLayout;
391 }
392}
393#endif // UNITY_EDITOR