A game about forced loneliness, made by TACStudios
at master 22 kB view raw
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