暫無描述
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

InputActionTreeViewItems.cs 22KB

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