A game about forced loneliness, made by TACStudios
1#if UNITY_EDITOR
2using System;
3using System.Collections.Generic;
4using System.Linq;
5using UnityEditor;
6using UnityEditor.IMGUI.Controls;
7using UnityEngine.InputSystem.Utilities;
8
9////TODO: sync expanded state of SerializedProperties to expanded state of tree (will help preserving expansion in inspector)
10
11////REVIEW: would be great to align all "[device]" parts of binding strings neatly in a column
12
13namespace UnityEngine.InputSystem.Editor
14{
15 internal abstract class ActionTreeItemBase : TreeViewItem
16 {
17 public SerializedProperty property { get; }
18 public virtual string expectedControlLayout => string.Empty;
19 public virtual bool canRename => true;
20 public virtual bool serializedDataIncludesChildren => false;
21 public abstract GUIStyle colorTagStyle { get; }
22 public string name { get; }
23 public Guid guid { get; }
24 public virtual bool showWarningIcon => false;
25
26 // For some operations (like copy-paste), we want to include information that we have filtered out.
27 internal List<ActionTreeItemBase> m_HiddenChildren;
28 public bool hasChildrenIncludingHidden => hasChildren || (m_HiddenChildren != null && m_HiddenChildren.Count > 0);
29 public IEnumerable<ActionTreeItemBase> hiddenChildren => m_HiddenChildren ?? Enumerable.Empty<ActionTreeItemBase>();
30 public IEnumerable<ActionTreeItemBase> childrenIncludingHidden
31 {
32 get
33 {
34 if (hasChildren)
35 foreach (var child in children)
36 if (child is ActionTreeItemBase item)
37 yield return item;
38 if (m_HiddenChildren != null)
39 foreach (var child in m_HiddenChildren)
40 yield return child;
41 }
42 }
43
44 // Action data is generally stored in arrays. Action maps are stored in m_ActionMaps arrays in assets,
45 // actions are stored in m_Actions arrays on maps and bindings are stored in m_Bindings arrays on maps.
46 public SerializedProperty arrayProperty => property.GetArrayPropertyFromElement();
47
48 // Dynamically look up the array index instead of just taking it from `property`.
49 // This makes sure whatever insertions or deletions we perform on the serialized data,
50 // we get the right array index from an item.
51 public int arrayIndex => InputActionSerializationHelpers.GetIndex(arrayProperty, guid);
52
53 protected ActionTreeItemBase(SerializedProperty property)
54 {
55 this.property = property;
56
57 // Look up name.
58 var nameProperty = property.FindPropertyRelative("m_Name");
59 Debug.Assert(nameProperty != null, $"Cannot find m_Name property on {property.propertyPath}");
60 name = nameProperty.stringValue;
61
62 // Look up ID.
63 var idProperty = property.FindPropertyRelative("m_Id");
64 Debug.Assert(idProperty != null, $"Cannot find m_Id property on {property.propertyPath}");
65 var idPropertyString = idProperty.stringValue;
66 if (string.IsNullOrEmpty(idPropertyString))
67 {
68 // This is somewhat questionable but we can't operate if we don't have IDs on the data used in the tree.
69 // Rather than requiring users of the tree to set this up consistently, we assign IDs
70 // on the fly, if necessary.
71 guid = Guid.NewGuid();
72 idPropertyString = guid.ToString();
73 idProperty.stringValue = idPropertyString;
74 idProperty.serializedObject.ApplyModifiedPropertiesWithoutUndo();
75 }
76 else
77 {
78 guid = new Guid(idPropertyString);
79 }
80
81 // All our elements (maps, actions, bindings) carry unique IDs. We use their hash
82 // codes as item IDs in the tree. This should result in stable item IDs that keep
83 // identifying the right item across all reloads and tree mutations.
84 id = guid.GetHashCode();
85 }
86
87 public virtual void Rename(string newName)
88 {
89 Debug.Assert(!canRename, "Item is marked as allowing renames yet does not implement Rename()");
90 }
91
92 /// <summary>
93 /// Delete serialized data for the tree item and its children.
94 /// </summary>
95 public abstract void DeleteData();
96
97 public abstract bool AcceptsDrop(ActionTreeItemBase item);
98
99 /// <summary>
100 /// Get information about where to drop an item of the given type and (optionally) the given index.
101 /// </summary>
102 public abstract bool GetDropLocation(Type itemType, int? childIndex, ref SerializedProperty array, ref int arrayIndex);
103
104 protected static class Styles
105 {
106 private static GUIStyle StyleWithBackground(string fileName)
107 {
108 return new GUIStyle("Label").WithNormalBackground(AssetDatabase.LoadAssetAtPath<Texture2D>($"{InputActionTreeView.SharedResourcesPath}{fileName}.png"));
109 }
110
111 public static readonly GUIStyle yellowRect = StyleWithBackground("yellow");
112 public static readonly GUIStyle greenRect = StyleWithBackground("green");
113 public static readonly GUIStyle blueRect = StyleWithBackground("blue");
114 public static readonly GUIStyle pinkRect = StyleWithBackground("pink");
115 }
116 }
117
118 /// <summary>
119 /// Tree view item for an action map.
120 /// </summary>
121 /// <seealso cref="InputActionMap"/>
122 internal class ActionMapTreeItem : ActionTreeItemBase
123 {
124 public ActionMapTreeItem(SerializedProperty actionMapProperty)
125 : base(actionMapProperty)
126 {
127 }
128
129 public override GUIStyle colorTagStyle => Styles.yellowRect;
130 public SerializedProperty bindingsArrayProperty => property.FindPropertyRelative("m_Bindings");
131 public SerializedProperty actionsArrayProperty => property.FindPropertyRelative("m_Actions");
132 public override bool serializedDataIncludesChildren => true;
133
134 public override void Rename(string newName)
135 {
136 InputActionSerializationHelpers.RenameActionMap(property, newName);
137 }
138
139 public override void DeleteData()
140 {
141 var assetObject = property.serializedObject;
142 if (!(assetObject.targetObject is InputActionAsset))
143 throw new InvalidOperationException(
144 $"Action map must be part of InputActionAsset but is in {assetObject.targetObject} instead");
145
146 InputActionSerializationHelpers.DeleteActionMap(assetObject, guid);
147 }
148
149 public override bool AcceptsDrop(ActionTreeItemBase item)
150 {
151 return item is ActionTreeItem;
152 }
153
154 public override bool GetDropLocation(Type itemType, int? childIndex, ref SerializedProperty array, ref int arrayIndex)
155 {
156 // Drop actions into action array.
157 if (itemType == typeof(ActionTreeItem))
158 {
159 array = actionsArrayProperty;
160 arrayIndex = childIndex ?? -1;
161 return true;
162 }
163
164 // For action maps in assets, drop other action maps next to them.
165 if (itemType == typeof(ActionMapTreeItem) && property.serializedObject.targetObject is InputActionAsset)
166 {
167 array = property.GetArrayPropertyFromElement();
168 arrayIndex = this.arrayIndex + 1;
169 return true;
170 }
171
172 ////REVIEW: would be nice to be able to replace the entire contents of a map in the inspector by dropping in another map
173
174 return false;
175 }
176
177 public static ActionMapTreeItem AddTo(TreeViewItem parent, SerializedProperty actionMapProperty)
178 {
179 var item = new ActionMapTreeItem(actionMapProperty);
180
181 item.depth = parent.depth + 1;
182 item.displayName = item.name;
183 parent.AddChild(item);
184
185 return item;
186 }
187
188 public void AddActionsTo(TreeViewItem parent)
189 {
190 AddActionsTo(parent, addBindings: false);
191 }
192
193 public void AddActionsAndBindingsTo(TreeViewItem parent)
194 {
195 AddActionsTo(parent, addBindings: true);
196 }
197
198 private void AddActionsTo(TreeViewItem parent, bool addBindings)
199 {
200 var actionsArrayProperty = this.actionsArrayProperty;
201 Debug.Assert(actionsArrayProperty != null, $"Cannot find m_Actions in {property}");
202
203 for (var i = 0; i < actionsArrayProperty.arraySize; i++)
204 {
205 var actionProperty = actionsArrayProperty.GetArrayElementAtIndex(i);
206 var actionItem = ActionTreeItem.AddTo(parent, property, actionProperty);
207
208 if (addBindings)
209 actionItem.AddBindingsTo(actionItem);
210 }
211 }
212
213 public static void AddActionMapsFromAssetTo(TreeViewItem parent, SerializedObject assetObject)
214 {
215 var actionMapsArrayProperty = assetObject.FindProperty("m_ActionMaps");
216 Debug.Assert(actionMapsArrayProperty != null, $"Cannot find m_ActionMaps in {assetObject}");
217 Debug.Assert(actionMapsArrayProperty.isArray, $"m_ActionMaps in {assetObject} is not an array");
218
219 var mapCount = actionMapsArrayProperty.arraySize;
220 for (var i = 0; i < mapCount; ++i)
221 {
222 var mapProperty = actionMapsArrayProperty.GetArrayElementAtIndex(i);
223 AddTo(parent, mapProperty);
224 }
225 }
226 }
227
228 /// <summary>
229 /// Tree view item for an action.
230 /// </summary>
231 /// <see cref="InputAction"/>
232 internal class ActionTreeItem : ActionTreeItemBase
233 {
234 public ActionTreeItem(SerializedProperty actionMapProperty, SerializedProperty actionProperty)
235 : base(actionProperty)
236 {
237 this.actionMapProperty = actionMapProperty;
238 }
239
240 public SerializedProperty actionMapProperty { get; }
241 public override GUIStyle colorTagStyle => Styles.greenRect;
242 public bool isSingletonAction => actionMapProperty == null;
243
244 public override string expectedControlLayout
245 {
246 get
247 {
248 var expectedControlType = property.FindPropertyRelative("m_ExpectedControlType").stringValue;
249 if (!string.IsNullOrEmpty(expectedControlType))
250 return expectedControlType;
251
252 var type = property.FindPropertyRelative("m_Type").intValue;
253 if (type == (int)InputActionType.Button)
254 return "Button";
255
256 return null;
257 }
258 }
259
260 public SerializedProperty bindingsArrayProperty => isSingletonAction
261 ? property.FindPropertyRelative("m_SingletonActionBindings")
262 : actionMapProperty.FindPropertyRelative("m_Bindings");
263
264 // If we're a singleton action (no associated action map property), we include all our bindings in the
265 // serialized data.
266 public override bool serializedDataIncludesChildren => actionMapProperty == null;
267
268 public override void Rename(string newName)
269 {
270 InputActionSerializationHelpers.RenameAction(property, actionMapProperty, newName);
271 }
272
273 public override void DeleteData()
274 {
275 InputActionSerializationHelpers.DeleteActionAndBindings(actionMapProperty, guid);
276 }
277
278 public override bool AcceptsDrop(ActionTreeItemBase item)
279 {
280 return item is BindingTreeItem && !(item is PartOfCompositeBindingTreeItem);
281 }
282
283 public override bool GetDropLocation(Type itemType, int? childIndex, ref SerializedProperty array, ref int arrayIndex)
284 {
285 // Drop bindings into binding array.
286 if (typeof(BindingTreeItem).IsAssignableFrom(itemType))
287 {
288 array = bindingsArrayProperty;
289
290 // Indexing by tree items is relative to each action but indexing in
291 // binding array is global for all actions in a map. Adjust index accordingly.
292 // NOTE: Bindings for any one action need not be stored contiguously in the binding array
293 // so we can't just add something to the index of the first binding to the action.
294 arrayIndex =
295 InputActionSerializationHelpers.ConvertBindingIndexOnActionToBindingIndexInArray(
296 array, name, childIndex ?? -1);
297
298 return true;
299 }
300
301 // Drop other actions next to us.
302 if (itemType == typeof(ActionTreeItem))
303 {
304 array = arrayProperty;
305 arrayIndex = this.arrayIndex + 1;
306 return true;
307 }
308
309 return false;
310 }
311
312 public static ActionTreeItem AddTo(TreeViewItem parent, SerializedProperty actionMapProperty, SerializedProperty actionProperty)
313 {
314 var item = new ActionTreeItem(actionMapProperty, actionProperty);
315
316 item.depth = parent.depth + 1;
317 item.displayName = item.name;
318 parent.AddChild(item);
319
320 return item;
321 }
322
323 /// <summary>
324 /// Add items for the bindings of just this action to the given parent tree item.
325 /// </summary>
326 public void AddBindingsTo(TreeViewItem parent)
327 {
328 var isSingleton = actionMapProperty == null;
329 var bindingsArrayProperty = isSingleton
330 ? property.FindPropertyRelative("m_SingletonActionBindings")
331 : actionMapProperty.FindPropertyRelative("m_Bindings");
332
333 var bindingsCountInMap = bindingsArrayProperty.arraySize;
334 var currentComposite = (CompositeBindingTreeItem)null;
335 for (var i = 0; i < bindingsCountInMap; ++i)
336 {
337 var bindingProperty = bindingsArrayProperty.GetArrayElementAtIndex(i);
338
339 // Skip if binding is not for action.
340 var actionProperty = bindingProperty.FindPropertyRelative("m_Action");
341 Debug.Assert(actionProperty != null, $"Could not find m_Action in {bindingProperty}");
342 if (!actionProperty.stringValue.Equals(name, StringComparison.InvariantCultureIgnoreCase))
343 continue;
344
345 // See what kind of binding we have.
346 var flagsProperty = bindingProperty.FindPropertyRelative("m_Flags");
347 Debug.Assert(actionProperty != null, $"Could not find m_Flags in {bindingProperty}");
348 var flags = (InputBinding.Flags)flagsProperty.intValue;
349 if ((flags & InputBinding.Flags.PartOfComposite) != 0 && currentComposite != null)
350 {
351 // Composite part binding.
352 PartOfCompositeBindingTreeItem.AddTo(currentComposite, bindingProperty);
353 }
354 else if ((flags & InputBinding.Flags.Composite) != 0)
355 {
356 // Composite binding.
357 currentComposite = CompositeBindingTreeItem.AddTo(parent, bindingProperty);
358 }
359 else
360 {
361 // "Normal" binding.
362 BindingTreeItem.AddTo(parent, bindingProperty);
363 currentComposite = null;
364 }
365 }
366 }
367 }
368
369 /// <summary>
370 /// Tree view item for a binding.
371 /// </summary>
372 /// <seealso cref="InputBinding"/>
373 internal class BindingTreeItem : ActionTreeItemBase
374 {
375 public BindingTreeItem(SerializedProperty bindingProperty)
376 : base(bindingProperty)
377 {
378 path = property.FindPropertyRelative("m_Path").stringValue;
379 groups = property.FindPropertyRelative("m_Groups").stringValue;
380 action = property.FindPropertyRelative("m_Action").stringValue;
381 }
382
383 public string path { get; }
384 public string groups { get; }
385 public string action { get; }
386 public override bool showWarningIcon => InputSystem.ShouldDrawWarningIconForBinding(path);
387
388 public override bool canRename => false;
389 public override GUIStyle colorTagStyle => Styles.blueRect;
390
391 public string displayPath =>
392 !string.IsNullOrEmpty(path) ? InputControlPath.ToHumanReadableString(path) : "<No Binding>";
393
394 private ActionTreeItem actionItem
395 {
396 get
397 {
398 // Find the action we're under.
399 for (var node = parent; node != null; node = node.parent)
400 if (node is ActionTreeItem item)
401 return item;
402 return null;
403 }
404 }
405
406 public override string expectedControlLayout
407 {
408 get
409 {
410 var currentActionItem = actionItem;
411 return currentActionItem != null ? currentActionItem.expectedControlLayout : string.Empty;
412 }
413 }
414
415 public override void DeleteData()
416 {
417 var currentActionItem = actionItem;
418 Debug.Assert(currentActionItem != null, "BindingTreeItem should always have a parent action");
419 var bindingsArrayProperty = currentActionItem.bindingsArrayProperty;
420 InputActionSerializationHelpers.DeleteBinding(bindingsArrayProperty, guid);
421 }
422
423 public override bool AcceptsDrop(ActionTreeItemBase item)
424 {
425 return false;
426 }
427
428 public override bool GetDropLocation(Type itemType, int? childIndex, ref SerializedProperty array, ref int arrayIndex)
429 {
430 // Drop bindings next to us.
431 if (typeof(BindingTreeItem).IsAssignableFrom(itemType))
432 {
433 array = arrayProperty;
434 arrayIndex = this.arrayIndex + 1;
435 return true;
436 }
437
438 return false;
439 }
440
441 public static BindingTreeItem AddTo(TreeViewItem parent, SerializedProperty bindingProperty)
442 {
443 var item = new BindingTreeItem(bindingProperty);
444
445 item.depth = parent.depth + 1;
446 item.displayName = item.displayPath;
447 parent.AddChild(item);
448
449 return item;
450 }
451 }
452
453 /// <summary>
454 /// Tree view item for a composite binding.
455 /// </summary>
456 /// <seealso cref="InputBinding.isComposite"/>
457 internal class CompositeBindingTreeItem : BindingTreeItem
458 {
459 public CompositeBindingTreeItem(SerializedProperty bindingProperty)
460 : base(bindingProperty)
461 {
462 }
463
464 public override GUIStyle colorTagStyle => Styles.blueRect;
465 public override bool canRename => true;
466
467 public string compositeName => NameAndParameters.ParseName(path);
468
469 public override void Rename(string newName)
470 {
471 InputActionSerializationHelpers.RenameComposite(property, newName);
472 }
473
474 public override bool AcceptsDrop(ActionTreeItemBase item)
475 {
476 return item is PartOfCompositeBindingTreeItem;
477 }
478
479 public override bool GetDropLocation(Type itemType, int? childIndex, ref SerializedProperty array, ref int arrayIndex)
480 {
481 // Drop part binding into composite.
482 if (itemType == typeof(PartOfCompositeBindingTreeItem))
483 {
484 array = arrayProperty;
485
486 // Adjust child index by index of composite item itself.
487 arrayIndex = childIndex != null
488 ? this.arrayIndex + 1 + childIndex.Value // Dropping at #0 should put as our index plus one.
489 : this.arrayIndex + 1 + InputActionSerializationHelpers.GetCompositePartCount(array, this.arrayIndex);
490
491 return true;
492 }
493
494 // Drop other bindings next to us.
495 if (typeof(BindingTreeItem).IsAssignableFrom(itemType))
496 {
497 array = arrayProperty;
498 arrayIndex = this.arrayIndex + 1 +
499 InputActionSerializationHelpers.GetCompositePartCount(array, this.arrayIndex);
500 return true;
501 }
502
503 return false;
504 }
505
506 public new static CompositeBindingTreeItem AddTo(TreeViewItem parent, SerializedProperty bindingProperty)
507 {
508 var item = new CompositeBindingTreeItem(bindingProperty);
509
510 item.depth = parent.depth + 1;
511 item.displayName = !string.IsNullOrEmpty(item.name)
512 ? item.name
513 : ObjectNames.NicifyVariableName(NameAndParameters.ParseName(item.path));
514
515 parent.AddChild(item);
516
517 return item;
518 }
519 }
520
521 /// <summary>
522 /// Tree view item for bindings that are parts of composites.
523 /// </summary>
524 /// <see cref="InputBinding.isPartOfComposite"/>
525 internal class PartOfCompositeBindingTreeItem : BindingTreeItem
526 {
527 public PartOfCompositeBindingTreeItem(SerializedProperty bindingProperty)
528 : base(bindingProperty)
529 {
530 }
531
532 public override GUIStyle colorTagStyle => Styles.pinkRect;
533 public override bool canRename => false;
534
535 public override string expectedControlLayout
536 {
537 get
538 {
539 if (m_ExpectedControlLayout == null)
540 {
541 var partName = name;
542 var compositeName = ((CompositeBindingTreeItem)parent).compositeName;
543 var layoutName = InputBindingComposite.GetExpectedControlLayoutName(compositeName, partName);
544 m_ExpectedControlLayout = layoutName ?? "";
545 }
546
547 return m_ExpectedControlLayout;
548 }
549 }
550
551 private string m_ExpectedControlLayout;
552
553 public new static PartOfCompositeBindingTreeItem AddTo(TreeViewItem parent, SerializedProperty bindingProperty)
554 {
555 var item = new PartOfCompositeBindingTreeItem(bindingProperty);
556
557 item.depth = parent.depth + 1;
558 item.displayName = $"{ObjectNames.NicifyVariableName(item.name)}: {item.displayPath}";
559 parent.AddChild(item);
560
561 return item;
562 }
563 }
564}
565#endif // UNITY_EDITOR