暫無描述
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.

InputActionTreeView.cs 70KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705
  1. #if UNITY_EDITOR
  2. using System;
  3. using System.Collections.Generic;
  4. using System.ComponentModel;
  5. using System.Linq;
  6. using System.Reflection;
  7. using System.Text;
  8. using UnityEditor;
  9. using UnityEditor.IMGUI.Controls;
  10. using UnityEngine.InputSystem.Layouts;
  11. using UnityEngine.InputSystem.Utilities;
  12. // The action tree view illustrates one of the weaknesses of Unity's editing model. While operating directly
  13. // on serialized data does have a number of advantages (the built-in undo system being one of them), making the
  14. // persistence model equivalent to the edit model doesn't work well. Serialized data will be laid out for persistence,
  15. // not for the convenience of editing operations. This means that editing operations have to constantly jump through
  16. // hoops to map themselves onto the persistence model of the data.
  17. ////TODO: With many actions and bindings the list becomes really hard to grok; make things more visually distinctive
  18. ////TODO: add context menu items for reordering action and binging entries (like "Move Up" and "Move Down")
  19. ////FIXME: context menu cannot be brought up when there's no items in the tree
  20. ////FIXME: RMB context menu for actions displays composites that aren't applicable to the action
  21. namespace UnityEngine.InputSystem.Editor
  22. {
  23. /// <summary>
  24. /// A tree view showing action maps, actions, and bindings. This is the core piece around which the various
  25. /// pieces of action editing functionality revolve.
  26. /// </summary>
  27. /// <remarks>
  28. /// The tree view can be flexibly used to contain only parts of a specific action setup. For example,
  29. /// by only adding items for action maps (<see cref="ActionMapTreeItem"/> to the tree, the tree view
  30. /// will become a flat list of action maps. Or by adding only items for actions (<see cref="ActionTreeItem"/>
  31. /// and items for their bindings (<see cref="BindingTreeItem"/>) to the tree, it will become an action-only
  32. /// tree view.
  33. ///
  34. /// This is used by the action asset editor to separate action maps and their actions into two separate
  35. /// tree views (the leftmost and the middle column of the editor).
  36. ///
  37. /// Each action tree comes with copy-paste and context menu support.
  38. /// </remarks>
  39. internal class InputActionTreeView : TreeView
  40. {
  41. #region Creation
  42. public InputActionTreeView(SerializedObject serializedObject, TreeViewState state = null)
  43. : base(state ?? new TreeViewState())
  44. {
  45. Debug.Assert(serializedObject != null, "Must have serialized object");
  46. this.serializedObject = serializedObject;
  47. UpdateSerializedObjectDirtyCount();
  48. foldoutOverride = DrawFoldout;
  49. drawHeader = true;
  50. drawPlusButton = true;
  51. drawMinusButton = true;
  52. m_ForceAcceptRename = false;
  53. m_Title = new GUIContent("");
  54. }
  55. /// <summary>
  56. /// Build an action tree that shows only the bindings for the given action.
  57. /// </summary>
  58. public static TreeViewItem BuildWithJustBindingsFromAction(SerializedProperty actionProperty, SerializedProperty actionMapProperty = null)
  59. {
  60. Debug.Assert(actionProperty != null, "Action property cannot be null");
  61. var root = new ActionTreeItem(actionMapProperty, actionProperty);
  62. root.depth = -1;
  63. root.AddBindingsTo(root);
  64. return root;
  65. }
  66. /// <summary>
  67. /// Build an action tree that shows only the actions and bindings for the given action map.
  68. /// </summary>
  69. public static TreeViewItem BuildWithJustActionsAndBindingsFromMap(SerializedProperty actionMapProperty)
  70. {
  71. Debug.Assert(actionMapProperty != null, "Action map property cannot be null");
  72. var root = new ActionMapTreeItem(actionMapProperty);
  73. root.depth = -1;
  74. root.AddActionsAndBindingsTo(root);
  75. return root;
  76. }
  77. /// <summary>
  78. /// Build an action tree that contains only the action maps from the given .inputactions asset.
  79. /// </summary>
  80. public static TreeViewItem BuildWithJustActionMapsFromAsset(SerializedObject assetObject)
  81. {
  82. Debug.Assert(assetObject != null, "Asset object cannot be null");
  83. var root = new ActionMapListItem {id = 0, depth = -1};
  84. ActionMapTreeItem.AddActionMapsFromAssetTo(root, assetObject);
  85. return root;
  86. }
  87. public static TreeViewItem BuildFullTree(SerializedObject assetObject)
  88. {
  89. Debug.Assert(assetObject != null, "Asset object cannot be null");
  90. var root = new TreeViewItem {id = 0, depth = -1};
  91. ActionMapTreeItem.AddActionMapsFromAssetTo(root, assetObject);
  92. if (root.hasChildren)
  93. foreach (var child in root.children)
  94. ((ActionMapTreeItem)child).AddActionsAndBindingsTo(child);
  95. return root;
  96. }
  97. protected override TreeViewItem BuildRoot()
  98. {
  99. var root = onBuildTree?.Invoke() ?? new TreeViewItem(0, -1);
  100. // If we have a filter, remove unwanted items from the tree.
  101. // NOTE: We use this method rather than TreeView's built-in search functionality as we want
  102. // to keep the tree structure fully intact whereas searchString switches the tree into
  103. // a different view mode.
  104. if (m_ItemFilterCriteria.LengthSafe() > 0 && root.hasChildren)
  105. {
  106. foreach (var child in root.children.OfType<ActionTreeItemBase>().ToArray())
  107. PruneTreeByItemSearchFilter(child);
  108. }
  109. // Root node is required to have `children` not be null. Add empty list,
  110. // if necessary. Can happen, for example, if we have a singleton action at the
  111. // root but no bindings on it.
  112. if (root.children == null)
  113. root.children = new List<TreeViewItem>();
  114. return root;
  115. }
  116. #endregion
  117. #region Filtering
  118. internal bool hasFilter => m_ItemFilterCriteria != null;
  119. public void ClearItemSearchFilterAndReload()
  120. {
  121. if (m_ItemFilterCriteria == null)
  122. return;
  123. m_ItemFilterCriteria = null;
  124. Reload();
  125. }
  126. public void SetItemSearchFilterAndReload(string criteria)
  127. {
  128. SetItemSearchFilterAndReload(FilterCriterion.FromString(criteria));
  129. }
  130. public void SetItemSearchFilterAndReload(IEnumerable<FilterCriterion> criteria)
  131. {
  132. m_ItemFilterCriteria = criteria.ToArray();
  133. Reload();
  134. }
  135. private void PruneTreeByItemSearchFilter(ActionTreeItemBase item)
  136. {
  137. // Prune subtree if item is forced out by any of our criteria.
  138. if (m_ItemFilterCriteria.Any(x => x.Matches(item) == FilterCriterion.Match.Failure))
  139. {
  140. item.parent.children.Remove(item);
  141. // Add to list of hidden children.
  142. if (item.parent is ActionTreeItemBase parent)
  143. {
  144. if (parent.m_HiddenChildren == null)
  145. parent.m_HiddenChildren = new List<ActionTreeItemBase>();
  146. parent.m_HiddenChildren.Add(item);
  147. }
  148. return;
  149. }
  150. ////REVIEW: should we *always* do this? (regardless of whether a control scheme is selected)
  151. // When filtering by binding group, we tag bindings that are not in any binding group as "{GLOBAL}".
  152. // This helps when having a specific control scheme selected, to also see the bindings that are active
  153. // in that control scheme by virtue of not being associated with *any* specific control scheme.
  154. if (item is BindingTreeItem bindingItem &&
  155. !(item is CompositeBindingTreeItem) &&
  156. string.IsNullOrEmpty(bindingItem.groups) &&
  157. m_ItemFilterCriteria.Any(x => x.type == FilterCriterion.Type.ByBindingGroup))
  158. {
  159. item.displayName += " {GLOBAL}";
  160. }
  161. // Prune children.
  162. if (item.hasChildren)
  163. {
  164. foreach (var child in item.children.OfType<ActionTreeItemBase>().ToArray()) // We're modifying the child list so copy.
  165. PruneTreeByItemSearchFilter(child);
  166. }
  167. }
  168. #endregion
  169. #region Finding Items
  170. public ActionTreeItemBase FindItemByPath(string path)
  171. {
  172. var components = path.Split('/');
  173. var current = rootItem;
  174. foreach (var component in components)
  175. {
  176. if (current.hasChildren)
  177. {
  178. var found = false;
  179. foreach (var child in current.children)
  180. {
  181. if (child.displayName.Equals(component, StringComparison.InvariantCultureIgnoreCase))
  182. {
  183. current = child;
  184. found = true;
  185. break;
  186. }
  187. }
  188. if (found)
  189. continue;
  190. }
  191. return null;
  192. }
  193. return (ActionTreeItemBase)current;
  194. }
  195. public ActionTreeItemBase FindItemByPropertyPath(string propertyPath)
  196. {
  197. return FindFirstItem<ActionTreeItemBase>(x => x.property.propertyPath == propertyPath);
  198. }
  199. public ActionTreeItemBase FindItemFor(SerializedProperty element)
  200. {
  201. // We may be looking at a SerializedProperty that refers to the same element but is
  202. // its own instance different from the one we're using in the tree. Compare properties
  203. // by path, not by object instance.
  204. return FindFirstItem<ActionTreeItemBase>(x => x.property.propertyPath == element.propertyPath);
  205. }
  206. public TItem FindFirstItem<TItem>(Func<TItem, bool> predicate)
  207. where TItem : ActionTreeItemBase
  208. {
  209. return FindFirstItemRecursive(rootItem, predicate);
  210. }
  211. private static TItem FindFirstItemRecursive<TItem>(TreeViewItem current, Func<TItem, bool> predicate)
  212. where TItem : ActionTreeItemBase
  213. {
  214. if (current is TItem itemOfType && predicate(itemOfType))
  215. return itemOfType;
  216. if (current.hasChildren)
  217. foreach (var child in current.children)
  218. {
  219. var item = FindFirstItemRecursive(child, predicate);
  220. if (item != null)
  221. return item;
  222. }
  223. return null;
  224. }
  225. #endregion
  226. #region Selection
  227. public void ClearSelection()
  228. {
  229. SetSelection(new int[0], TreeViewSelectionOptions.FireSelectionChanged);
  230. }
  231. public void SelectItems(IEnumerable<ActionTreeItemBase> items)
  232. {
  233. SetSelection(items.Select(x => x.id).ToList(), TreeViewSelectionOptions.FireSelectionChanged);
  234. }
  235. public void SelectItem(SerializedProperty element, bool additive = false)
  236. {
  237. var item = FindItemFor(element);
  238. if (item == null)
  239. throw new ArgumentException($"Cannot find item for property path '{element.propertyPath}'", nameof(element));
  240. SelectItem(item, additive);
  241. }
  242. public void SelectItem(string path, bool additive = false)
  243. {
  244. if (!TrySelectItem(path, additive))
  245. throw new ArgumentException($"Cannot find item with path 'path'", nameof(path));
  246. }
  247. public bool TrySelectItem(string path, bool additive = false)
  248. {
  249. var item = FindItemByPath(path);
  250. if (item == null)
  251. return false;
  252. SelectItem(item, additive);
  253. return true;
  254. }
  255. public void SelectItem(ActionTreeItemBase item, bool additive = false)
  256. {
  257. if (additive)
  258. {
  259. var selection = new List<int>();
  260. selection.AddRange(GetSelection());
  261. selection.Add(item.id);
  262. SetSelection(selection, TreeViewSelectionOptions.FireSelectionChanged);
  263. }
  264. else
  265. {
  266. SetSelection(new[] { item.id }, TreeViewSelectionOptions.FireSelectionChanged);
  267. }
  268. }
  269. public IEnumerable<ActionTreeItemBase> GetSelectedItems()
  270. {
  271. foreach (var id in GetSelection())
  272. {
  273. if (FindItem(id, rootItem) is ActionTreeItemBase item)
  274. yield return item;
  275. }
  276. }
  277. /// <summary>
  278. /// Same as <see cref="GetSelectedItems"/> but with items that are selected but are children of other items
  279. /// that also selected being filtered out.
  280. /// </summary>
  281. /// <remarks>
  282. /// This is useful for operations such as copy-paste where copy a parent will implicitly copy the child.
  283. /// </remarks>
  284. public IEnumerable<ActionTreeItemBase> GetSelectedItemsWithChildrenFilteredOut()
  285. {
  286. var selectedItems = GetSelectedItems().ToArray();
  287. foreach (var item in selectedItems)
  288. {
  289. if (selectedItems.Any(x => x.IsParentOf(item)))
  290. continue;
  291. yield return item;
  292. }
  293. }
  294. public IEnumerable<TItem> GetSelectedItemsOrParentsOfType<TItem>()
  295. where TItem : ActionTreeItemBase
  296. {
  297. // If there is no selection and the root item has the type we're looking for,
  298. // consider it selected. This allows adding items at the toplevel.
  299. if (!HasSelection() && rootItem is TItem root)
  300. {
  301. yield return root;
  302. }
  303. else
  304. {
  305. foreach (var id in GetSelection())
  306. {
  307. var item = FindItem(id, rootItem);
  308. while (item != null)
  309. {
  310. if (item is TItem itemOfType)
  311. yield return itemOfType;
  312. item = item.parent;
  313. }
  314. }
  315. }
  316. }
  317. public void SelectFirstToplevelItem()
  318. {
  319. if (rootItem.children.Any())
  320. SetSelection(new[] {rootItem.children[0].id}, TreeViewSelectionOptions.FireSelectionChanged);
  321. }
  322. protected override void SelectionChanged(IList<int> selectedIds)
  323. {
  324. onSelectionChanged?.Invoke();
  325. }
  326. #endregion
  327. #region Renaming
  328. public new void BeginRename(TreeViewItem item)
  329. {
  330. // If a rename is already in progress, force it to end first.
  331. EndRename();
  332. onBeginRename?.Invoke((ActionTreeItemBase)item);
  333. base.BeginRename(item);
  334. }
  335. protected override bool CanRename(TreeViewItem item)
  336. {
  337. return item is ActionTreeItemBase actionTreeItem && actionTreeItem.canRename;
  338. }
  339. protected override void RenameEnded(RenameEndedArgs args)
  340. {
  341. if (!(FindItem(args.itemID, rootItem) is ActionTreeItemBase actionItem))
  342. return;
  343. if (!(args.acceptedRename || m_ForceAcceptRename) || args.originalName == args.newName)
  344. return;
  345. Debug.Assert(actionItem.canRename, "Cannot rename " + actionItem);
  346. actionItem.Rename(args.newName);
  347. OnSerializedObjectModified();
  348. }
  349. public void EndRename(bool forceAccept)
  350. {
  351. m_ForceAcceptRename = forceAccept;
  352. EndRename();
  353. m_ForceAcceptRename = false;
  354. }
  355. protected override void DoubleClickedItem(int id)
  356. {
  357. if (!(FindItem(id, rootItem) is ActionTreeItemBase item))
  358. return;
  359. // If we have a double-click handler, give it control over what happens.
  360. if (onDoubleClick != null)
  361. {
  362. onDoubleClick(item);
  363. }
  364. else if (item.canRename)
  365. {
  366. // Otherwise, perform a rename by default.
  367. BeginRename(item);
  368. }
  369. }
  370. #endregion
  371. #region Drag&Drop
  372. protected override bool CanStartDrag(CanStartDragArgs args)
  373. {
  374. return true;
  375. }
  376. protected override void SetupDragAndDrop(SetupDragAndDropArgs args)
  377. {
  378. DragAndDrop.PrepareStartDrag();
  379. DragAndDrop.SetGenericData("itemIDs", args.draggedItemIDs.ToArray());
  380. DragAndDrop.SetGenericData("tree", this);
  381. DragAndDrop.StartDrag(string.Join(",", args.draggedItemIDs.Select(id => FindItem(id, rootItem).displayName)));
  382. }
  383. protected override DragAndDropVisualMode HandleDragAndDrop(DragAndDropArgs args)
  384. {
  385. var sourceTree = DragAndDrop.GetGenericData("tree") as InputActionTreeView;
  386. if (sourceTree == null)
  387. return DragAndDropVisualMode.Rejected;
  388. var itemIds = (int[])DragAndDrop.GetGenericData("itemIDs");
  389. var altKeyIsDown = Event.current.alt;
  390. // Reject the drag if the parent item does not accept the drop.
  391. if (args.parentItem is ActionTreeItemBase parentItem)
  392. {
  393. if (itemIds.Any(id =>
  394. !parentItem.AcceptsDrop((ActionTreeItemBase)sourceTree.FindItem(id, sourceTree.rootItem))))
  395. return DragAndDropVisualMode.Rejected;
  396. }
  397. else
  398. {
  399. // If the root item isn't an ActionTreeItemBase, we're looking at a tree that starts
  400. // all the way up at the InputActionAsset. Require the drop to be all action maps.
  401. if (itemIds.Any(id => !(sourceTree.FindItem(id, sourceTree.rootItem) is ActionMapTreeItem)))
  402. return DragAndDropVisualMode.Rejected;
  403. }
  404. // Handle drop using copy-paste. This allows handling all the various operations
  405. // using a single code path.
  406. var isMove = !altKeyIsDown;
  407. if (args.performDrop)
  408. {
  409. // Copy item data.
  410. var copyBuffer = new StringBuilder();
  411. var items = itemIds.Select(id => (ActionTreeItemBase)sourceTree.FindItem(id, sourceTree.rootItem));
  412. CopyItems(items, copyBuffer);
  413. // If we're moving items within the same tree, no need to generate new IDs.
  414. var assignNewIDs = !(isMove && sourceTree == this);
  415. // Determine where we are moving/copying the data.
  416. var target = args.parentItem ?? rootItem;
  417. int? childIndex = null;
  418. if (args.dragAndDropPosition == DragAndDropPosition.BetweenItems)
  419. childIndex = args.insertAtIndex;
  420. // If alt isn't down (i.e. we're not duplicating), delete old items.
  421. // Do this *before* pasting so that assigning new names will not cause names to
  422. // change when just moving items around.
  423. if (isMove)
  424. {
  425. // Don't use DeleteDataOfSelectedItems() as that will record as a separate operation.
  426. foreach (var item in items)
  427. {
  428. // If we're dropping *between* items on the same parent as the current item and the
  429. // index we're dropping at (in the parent, NOT in the array) is coming *after* this item,
  430. // then deleting the item will shift the target index down by one.
  431. if (item.parent == target && childIndex != null && childIndex > target.children.IndexOf(item))
  432. --childIndex;
  433. item.DeleteData();
  434. }
  435. }
  436. // Paste items onto target.
  437. var oldBindingGroupForNewBindings = bindingGroupForNewBindings;
  438. try
  439. {
  440. // With drag&drop, preserve binding groups.
  441. bindingGroupForNewBindings = null;
  442. PasteItems(copyBuffer.ToString(),
  443. new[] { new InsertLocation { item = target, childIndex = childIndex } },
  444. assignNewIDs: assignNewIDs);
  445. }
  446. finally
  447. {
  448. bindingGroupForNewBindings = oldBindingGroupForNewBindings;
  449. }
  450. DragAndDrop.AcceptDrag();
  451. }
  452. return isMove ? DragAndDropVisualMode.Move : DragAndDropVisualMode.Copy;
  453. }
  454. #endregion
  455. #region Copy&Paste
  456. // These need to correspond to what the editor is sending from the "Edit" menu.
  457. public const string k_CopyCommand = "Copy";
  458. public const string k_PasteCommand = "Paste";
  459. public const string k_DuplicateCommand = "Duplicate";
  460. public const string k_CutCommand = "Cut";
  461. public const string k_DeleteCommand = "Delete";
  462. public const string k_SoftDeleteCommand = "SoftDelete";
  463. public void HandleCopyPasteCommandEvent(Event uiEvent)
  464. {
  465. if (uiEvent.type == EventType.ValidateCommand)
  466. {
  467. switch (uiEvent.commandName)
  468. {
  469. case k_CopyCommand:
  470. case k_CutCommand:
  471. case k_DuplicateCommand:
  472. case k_DeleteCommand:
  473. case k_SoftDeleteCommand:
  474. if (HasSelection())
  475. uiEvent.Use();
  476. break;
  477. case k_PasteCommand:
  478. var systemCopyBuffer = EditorHelpers.GetSystemCopyBufferContents();
  479. if (systemCopyBuffer != null && systemCopyBuffer.StartsWith(k_CopyPasteMarker))
  480. uiEvent.Use();
  481. break;
  482. }
  483. }
  484. else if (uiEvent.type == EventType.ExecuteCommand)
  485. {
  486. switch (uiEvent.commandName)
  487. {
  488. case k_CopyCommand:
  489. CopySelectedItemsToClipboard();
  490. break;
  491. case k_PasteCommand:
  492. PasteDataFromClipboard();
  493. break;
  494. case k_CutCommand:
  495. CopySelectedItemsToClipboard();
  496. DeleteDataOfSelectedItems();
  497. break;
  498. case k_DuplicateCommand:
  499. DuplicateSelection();
  500. break;
  501. case k_DeleteCommand:
  502. case k_SoftDeleteCommand:
  503. DeleteDataOfSelectedItems();
  504. break;
  505. default:
  506. return;
  507. }
  508. uiEvent.Use();
  509. }
  510. }
  511. private void DuplicateSelection()
  512. {
  513. var buffer = new StringBuilder();
  514. // If we have a multi-selection, we want to perform the duplication as if each item
  515. // was duplicated individually. Meaning we paste each duplicate right after the item
  516. // it was duplicated from. So if, say, an action is selected at the beginning of the
  517. // tree and one is selected from the end of it, we still paste the copies into the
  518. // two separate locations correctly.
  519. //
  520. // Technically, if both parents and children are selected, we're order dependent here
  521. // but not sure we really need to care.
  522. var selection = GetSelection();
  523. ClearSelection();
  524. // Copy-paste each selected item in turn.
  525. var newItemIds = new List<int>();
  526. foreach (var id in selection)
  527. {
  528. SetSelection(new[] { id });
  529. buffer.Length = 0;
  530. CopySelectedItemsTo(buffer);
  531. PasteDataFrom(buffer.ToString());
  532. newItemIds.AddRange(GetSelection());
  533. }
  534. SetSelection(newItemIds);
  535. }
  536. internal const string k_CopyPasteMarker = "INPUTASSET ";
  537. private const string k_StartOfText = "\u0002";
  538. private const string k_StartOfHeading = "\u0001";
  539. private const string k_EndOfTransmission = "\u0004";
  540. private const string k_EndOfTransmissionBlock = "\u0017";
  541. /// <summary>
  542. /// Copy the currently selected items to the clipboard.
  543. /// </summary>
  544. /// <seealso cref="EditorGUIUtility.systemCopyBuffer"/>
  545. public void CopySelectedItemsToClipboard()
  546. {
  547. var copyBuffer = new StringBuilder();
  548. CopySelectedItemsTo(copyBuffer);
  549. EditorHelpers.SetSystemCopyBufferContents(copyBuffer.ToString());
  550. }
  551. public void CopySelectedItemsTo(StringBuilder buffer)
  552. {
  553. CopyItems(GetSelectedItemsWithChildrenFilteredOut(), buffer);
  554. }
  555. public static void CopyItems(IEnumerable<ActionTreeItemBase> items, StringBuilder buffer)
  556. {
  557. buffer.Append(k_CopyPasteMarker);
  558. foreach (var item in items)
  559. {
  560. CopyItemData(item, buffer);
  561. buffer.Append(k_EndOfTransmission);
  562. }
  563. }
  564. private static void CopyItemData(ActionTreeItemBase item, StringBuilder buffer)
  565. {
  566. buffer.Append(k_StartOfHeading);
  567. buffer.Append(item.GetType().Name);
  568. buffer.Append(k_StartOfText);
  569. // InputActionMaps have back-references to InputActionAssets. Make sure we ignore those.
  570. buffer.Append(item.property.CopyToJson(ignoreObjectReferences: true));
  571. buffer.Append(k_EndOfTransmissionBlock);
  572. if (!item.serializedDataIncludesChildren && item.hasChildrenIncludingHidden)
  573. foreach (var child in item.childrenIncludingHidden)
  574. CopyItemData(child, buffer);
  575. }
  576. /// <summary>
  577. /// Remove the data from the currently selected items from the <see cref="SerializedObject"/>
  578. /// referenced by the tree's data.
  579. /// </summary>
  580. public void DeleteDataOfSelectedItems()
  581. {
  582. // When deleting data, indices will shift around. However, we do not delete elements by array indices
  583. // directly but rather by GUIDs which are used to look up array indices dynamically. This means that
  584. // we can safely delete the items without worrying about one deletion affecting the next.
  585. //
  586. // NOTE: It is important that we first fetch *all* of the selection filtered for parent/child duplicates.
  587. // If we don't do so up front, the deletions happening later may start interacting with our
  588. // parent/child test.
  589. var selection = GetSelectedItemsWithChildrenFilteredOut().ToArray();
  590. // Clear our current selection. If we don't do this first, TreeView will implicitly
  591. // clear the selection as items disappear but we will not see a selection change notification
  592. // being triggered.
  593. ClearSelection();
  594. DeleteItems(selection);
  595. }
  596. public void DeleteItems(IEnumerable<ActionTreeItemBase> items)
  597. {
  598. foreach (var item in items)
  599. item.DeleteData();
  600. OnSerializedObjectModified();
  601. }
  602. public bool HavePastableClipboardData()
  603. {
  604. var clipboard = EditorHelpers.GetSystemCopyBufferContents();
  605. return clipboard.StartsWith(k_CopyPasteMarker);
  606. }
  607. public void PasteDataFromClipboard()
  608. {
  609. PasteDataFrom(EditorHelpers.GetSystemCopyBufferContents());
  610. }
  611. public void PasteDataFrom(string copyBufferString)
  612. {
  613. if (!copyBufferString.StartsWith(k_CopyPasteMarker))
  614. return;
  615. var locations = GetSelectedItemsWithChildrenFilteredOut().Select(x => new InsertLocation { item = x }).ToList();
  616. if (locations.Count == 0)
  617. locations.Add(new InsertLocation { item = rootItem });
  618. ////REVIEW: filtering out children may remove the very item we need to get the right match for a copy block?
  619. PasteItems(copyBufferString, locations);
  620. }
  621. public struct InsertLocation
  622. {
  623. public TreeViewItem item;
  624. public int? childIndex;
  625. }
  626. public void PasteItems(string copyBufferString, IEnumerable<InsertLocation> locations, bool assignNewIDs = true, bool selectNewItems = true)
  627. {
  628. var newItemPropertyPaths = new List<string>();
  629. // Split buffer into transmissions and then into transmission blocks. Each transmission is an item subtree
  630. // meant to be pasted as a whole and each transmission block is a single chunk of serialized data.
  631. foreach (var transmission in copyBufferString.Substring(k_CopyPasteMarker.Length)
  632. .Split(new[] {k_EndOfTransmission}, StringSplitOptions.RemoveEmptyEntries))
  633. {
  634. foreach (var location in locations)
  635. PasteBlocks(transmission, location, assignNewIDs, newItemPropertyPaths);
  636. }
  637. OnSerializedObjectModified();
  638. // If instructed to do so, go and select all newly added items.
  639. if (selectNewItems && newItemPropertyPaths.Count > 0)
  640. {
  641. // We may have pasted into a different tree view. Only select the items if we can find them in
  642. // our current tree view.
  643. var newItems = newItemPropertyPaths.Select(FindItemByPropertyPath).Where(x => x != null);
  644. if (newItems.Any())
  645. SelectItems(newItems);
  646. }
  647. }
  648. private const string k_ActionMapTag = k_StartOfHeading + "ActionMapTreeItem" + k_StartOfText;
  649. private const string k_ActionTag = k_StartOfHeading + "ActionTreeItem" + k_StartOfText;
  650. private const string k_BindingTag = k_StartOfHeading + "BindingTreeItem" + k_StartOfText;
  651. private const string k_CompositeBindingTag = k_StartOfHeading + "CompositeBindingTreeItem" + k_StartOfText;
  652. private const string k_PartOfCompositeBindingTag = k_StartOfHeading + "PartOfCompositeBindingTreeItem" + k_StartOfText;
  653. private void PasteBlocks(string transmission, InsertLocation location, bool assignNewIDs, List<string> newItemPropertyPaths)
  654. {
  655. Debug.Assert(location.item != null, "Should have drop target");
  656. var blocks = transmission.Split(new[] {k_EndOfTransmissionBlock},
  657. StringSplitOptions.RemoveEmptyEntries);
  658. if (blocks.Length < 1)
  659. return;
  660. Type CopyTagToType(string tagName)
  661. {
  662. switch (tagName)
  663. {
  664. case k_ActionMapTag: return typeof(ActionMapTreeItem);
  665. case k_ActionTag: return typeof(ActionTreeItem);
  666. case k_BindingTag: return typeof(BindingTreeItem);
  667. case k_CompositeBindingTag: return typeof(CompositeBindingTreeItem);
  668. case k_PartOfCompositeBindingTag: return typeof(PartOfCompositeBindingTreeItem);
  669. default:
  670. throw new Exception($"Unrecognized copy block tag '{tagName}'");
  671. }
  672. }
  673. SplitTagAndData(blocks[0], out var tag, out var data);
  674. // Determine where to drop the item.
  675. SerializedProperty array = null;
  676. var arrayIndex = -1;
  677. var itemType = CopyTagToType(tag);
  678. if (location.item is ActionTreeItemBase dropTarget)
  679. {
  680. if (!dropTarget.GetDropLocation(itemType, location.childIndex, ref array, ref arrayIndex))
  681. return;
  682. }
  683. else if (tag == k_ActionMapTag)
  684. {
  685. // Paste into InputActionAsset.
  686. array = serializedObject.FindProperty("m_ActionMaps");
  687. arrayIndex = location.childIndex ?? array.arraySize;
  688. }
  689. else
  690. {
  691. throw new InvalidOperationException($"Cannot paste {tag} into {location.item.displayName}");
  692. }
  693. // If not given a specific index, we paste onto the end of the array.
  694. if (arrayIndex == -1 || arrayIndex > array.arraySize)
  695. arrayIndex = array.arraySize;
  696. // Determine action to assign to pasted bindings.
  697. string actionForNewBindings = null;
  698. if (location.item is ActionTreeItem actionItem)
  699. actionForNewBindings = actionItem.name;
  700. else if (location.item is BindingTreeItem bindingItem)
  701. actionForNewBindings = bindingItem.action;
  702. // Paste new element.
  703. var newElement = PasteBlock(tag, data, array, arrayIndex, assignNewIDs, actionForNewBindings);
  704. newItemPropertyPaths.Add(newElement.propertyPath);
  705. // If the element can have children, read whatever blocks are following the current one (if any).
  706. if ((tag == k_ActionTag || tag == k_CompositeBindingTag) && blocks.Length > 1)
  707. {
  708. var bindingArray = array;
  709. if (tag == k_ActionTag)
  710. {
  711. // We don't support pasting actions separately into action maps in the same paste operations so
  712. // there must be an ActionMapTreeItem in the hierarchy we pasted into.
  713. var actionMapItem = location.item.TryFindItemInHierarchy<ActionMapTreeItem>();
  714. Debug.Assert(actionMapItem != null, "Cannot find ActionMapTreeItem in hierarchy of pasted action");
  715. bindingArray = actionMapItem.bindingsArrayProperty;
  716. actionForNewBindings = InputActionSerializationHelpers.GetName(newElement);
  717. }
  718. for (var i = 1; i < blocks.Length; ++i)
  719. {
  720. SplitTagAndData(blocks[i], out var blockTag, out var blockData);
  721. PasteBlock(blockTag, blockData, bindingArray,
  722. tag == k_CompositeBindingTag ? arrayIndex + i : -1,
  723. assignNewIDs,
  724. actionForNewBindings);
  725. }
  726. }
  727. }
  728. private static void SplitTagAndData(string block, out string tag, out string data)
  729. {
  730. var indexOfStartOfTextChar = block.IndexOf(k_StartOfText);
  731. if (indexOfStartOfTextChar == -1)
  732. throw new ArgumentException($"Incorrect copy data format: Expecting '{k_StartOfText}' in '{block}'",
  733. nameof(block));
  734. tag = block.Substring(0, indexOfStartOfTextChar + 1);
  735. data = block.Substring(indexOfStartOfTextChar + 1);
  736. }
  737. public static SerializedProperty AddElement(SerializedProperty arrayProperty, string name, int index = -1)
  738. {
  739. var uniqueName = InputActionSerializationHelpers.FindUniqueName(arrayProperty, name);
  740. if (index < 0)
  741. index = arrayProperty.arraySize;
  742. arrayProperty.InsertArrayElementAtIndex(index);
  743. var elementProperty = arrayProperty.GetArrayElementAtIndex(index);
  744. elementProperty.ResetValuesToDefault();
  745. elementProperty.FindPropertyRelative("m_Name").stringValue = uniqueName;
  746. elementProperty.FindPropertyRelative("m_Id").stringValue = Guid.NewGuid().ToString();
  747. return elementProperty;
  748. }
  749. private SerializedProperty PasteBlock(string tag, string data, SerializedProperty array, int arrayIndex,
  750. bool assignNewIDs, string actionForNewBindings = null)
  751. {
  752. // Add an element to the array. Then read the serialized properties stored in the copy data
  753. // back into the element.
  754. var property = AddElement(array, "tempName", arrayIndex);
  755. property.RestoreFromJson(data);
  756. if (tag == k_ActionTag || tag == k_ActionMapTag)
  757. InputActionSerializationHelpers.EnsureUniqueName(property);
  758. if (assignNewIDs)
  759. {
  760. // Assign new IDs to the element as well as to any elements it contains. This means
  761. // that for action maps, we will also assign new IDs to every action and binding in the map.
  762. InputActionSerializationHelpers.AssignUniqueIDs(property);
  763. }
  764. // If the element is a binding, update its action target and binding group, if necessary.
  765. if (tag == k_BindingTag || tag == k_CompositeBindingTag || tag == k_PartOfCompositeBindingTag)
  766. {
  767. ////TODO: use {id} rather than plain name
  768. // Update action to refer to given action.
  769. InputActionSerializationHelpers.ChangeBinding(property, action: actionForNewBindings);
  770. // If we have a binding group to set for new bindings, overwrite the binding's
  771. // group with it.
  772. if (!string.IsNullOrEmpty(bindingGroupForNewBindings) && tag != k_CompositeBindingTag)
  773. {
  774. InputActionSerializationHelpers.ChangeBinding(property,
  775. groups: bindingGroupForNewBindings);
  776. }
  777. onBindingAdded?.Invoke(property);
  778. }
  779. return property;
  780. }
  781. #endregion
  782. #region Context Menus
  783. public void BuildContextMenuFor(Type itemType, GenericMenu menu, bool multiSelect, ActionTreeItem actionItem = null, bool noSelection = false)
  784. {
  785. var canRename = false;
  786. if (itemType == typeof(ActionMapTreeItem))
  787. {
  788. menu.AddItem(s_AddActionLabel, false, AddNewAction);
  789. }
  790. else if (itemType == typeof(ActionTreeItem))
  791. {
  792. canRename = true;
  793. BuildMenuToAddBindings(menu, actionItem);
  794. }
  795. else if (itemType == typeof(CompositeBindingTreeItem))
  796. {
  797. canRename = true;
  798. }
  799. else if (itemType == typeof(ActionMapListItem))
  800. {
  801. menu.AddItem(s_AddActionMapLabel, false, AddNewActionMap);
  802. }
  803. // Common menu entries shared by all types of items.
  804. menu.AddSeparator("");
  805. if (noSelection)
  806. {
  807. menu.AddDisabledItem(s_CutLabel);
  808. menu.AddDisabledItem(s_CopyLabel);
  809. }
  810. else
  811. {
  812. menu.AddItem(s_CutLabel, false, () =>
  813. {
  814. CopySelectedItemsToClipboard();
  815. DeleteDataOfSelectedItems();
  816. });
  817. menu.AddItem(s_CopyLabel, false, CopySelectedItemsToClipboard);
  818. }
  819. if (HavePastableClipboardData())
  820. menu.AddItem(s_PasteLabel, false, PasteDataFromClipboard);
  821. else
  822. menu.AddDisabledItem(s_PasteLabel);
  823. menu.AddSeparator("");
  824. if (!noSelection && canRename && !multiSelect)
  825. menu.AddItem(s_RenameLabel, false, () => BeginRename(GetSelectedItems().First()));
  826. else if (canRename)
  827. menu.AddDisabledItem(s_RenameLabel);
  828. if (noSelection)
  829. {
  830. menu.AddDisabledItem(s_DuplicateLabel);
  831. menu.AddDisabledItem(s_DeleteLabel);
  832. }
  833. else
  834. {
  835. menu.AddItem(s_DuplicateLabel, false, DuplicateSelection);
  836. menu.AddItem(s_DeleteLabel, false, DeleteDataOfSelectedItems);
  837. }
  838. if (itemType != typeof(ActionMapTreeItem))
  839. {
  840. menu.AddSeparator("");
  841. menu.AddItem(s_ExpandAllLabel, false, ExpandAll);
  842. menu.AddItem(s_CollapseAllLabel, false, CollapseAll);
  843. }
  844. }
  845. public void BuildMenuToAddBindings(GenericMenu menu, ActionTreeItem actionItem = null)
  846. {
  847. // Add entry to add "normal" bindings.
  848. menu.AddItem(s_AddBindingLabel, false,
  849. () =>
  850. {
  851. if (actionItem != null)
  852. AddNewBinding(actionItem.property, actionItem.actionMapProperty);
  853. else
  854. AddNewBinding();
  855. });
  856. // Add one entry for each registered type of composite binding.
  857. var expectedControlLayout = new InternedString(actionItem?.expectedControlLayout);
  858. foreach (var compositeName in InputBindingComposite.s_Composites.internedNames.Where(x =>
  859. !InputBindingComposite.s_Composites.aliases.Contains(x)).OrderBy(x => x))
  860. {
  861. // Skip composites we should hide from the UI.
  862. var compositeType = InputBindingComposite.s_Composites.LookupTypeRegistration(compositeName);
  863. var designTimeVisible = compositeType.GetCustomAttribute<DesignTimeVisibleAttribute>();
  864. if (designTimeVisible != null && !designTimeVisible.Visible)
  865. continue;
  866. // If the action is expected a specific control layout, check
  867. // whether the value type use by the composite matches that of
  868. // the layout.
  869. if (!expectedControlLayout.IsEmpty())
  870. {
  871. var valueType = InputBindingComposite.GetValueType(compositeName);
  872. if (valueType != null &&
  873. !InputControlLayout.s_Layouts.ValueTypeIsAssignableFrom(expectedControlLayout, valueType))
  874. continue;
  875. }
  876. var displayName = compositeType.GetCustomAttribute<DisplayNameAttribute>();
  877. var niceName = displayName != null ? displayName.DisplayName.Replace('/', '\\') : ObjectNames.NicifyVariableName(compositeName) + " Composite";
  878. menu.AddItem(new GUIContent($"Add {niceName}"), false,
  879. () =>
  880. {
  881. if (actionItem != null)
  882. AddNewComposite(actionItem.property, actionItem.actionMapProperty, compositeName);
  883. else
  884. AddNewComposite(compositeName);
  885. });
  886. }
  887. }
  888. private void PopUpContextMenu()
  889. {
  890. // See if we have a selection of mixed types.
  891. var selected = GetSelectedItems().ToList();
  892. var mixedSelection = selected.Select(x => x.GetType()).Distinct().Count() > 1;
  893. var noSelection = selected.Count == 0;
  894. // Create and pop up context menu.
  895. var menu = new GenericMenu();
  896. if (noSelection)
  897. {
  898. BuildContextMenuFor(rootItem.GetType(), menu, true, noSelection: noSelection);
  899. }
  900. else if (mixedSelection)
  901. {
  902. BuildContextMenuFor(typeof(ActionTreeItemBase), menu, true, noSelection: noSelection);
  903. }
  904. else
  905. {
  906. var item = selected.First();
  907. BuildContextMenuFor(item.GetType(), menu, GetSelection().Count > 1, actionItem: item as ActionTreeItem);
  908. }
  909. menu.ShowAsContext();
  910. }
  911. protected override void ContextClickedItem(int id)
  912. {
  913. // When right-clicking an unselected item, TreeView does change the selection to the
  914. // clicked item but the visual feedback only comes in the *next* repaint. This means that
  915. // if we pop up a context menu right away here, the user does not correctly see which item
  916. // is affected.
  917. //
  918. // So, instead we force a repaint and open the context menu on the next OnGUI() call. Note
  919. // that we can't use something like EditorApplication.delayCall here as ShowAsContext()
  920. // can only be called from UI callbacks (otherwise it will simply be ignored).
  921. m_InitiateContextMenuOnNextRepaint = true;
  922. Repaint();
  923. Event.current.Use();
  924. }
  925. protected override void ContextClicked()
  926. {
  927. ClearSelection();
  928. m_InitiateContextMenuOnNextRepaint = true;
  929. Repaint();
  930. Event.current.Use();
  931. }
  932. #endregion
  933. #region Add New Items
  934. /// <summary>
  935. /// Add a new action map to the toplevel <see cref="InputActionAsset"/>.
  936. /// </summary>
  937. public void AddNewActionMap()
  938. {
  939. var actionMapProperty = InputActionSerializationHelpers.AddActionMap(serializedObject);
  940. var actionProperty = InputActionSerializationHelpers.AddAction(actionMapProperty);
  941. InputActionSerializationHelpers.AddBinding(actionProperty, actionMapProperty, groups: bindingGroupForNewBindings);
  942. OnNewItemAdded(actionMapProperty);
  943. }
  944. /// <summary>
  945. /// Add new action to the currently active action map(s).
  946. /// </summary>
  947. public void AddNewAction()
  948. {
  949. foreach (var actionMapItem in GetSelectedItemsOrParentsOfType<ActionMapTreeItem>())
  950. AddNewAction(actionMapItem.property);
  951. }
  952. public void AddNewAction(SerializedProperty actionMapProperty)
  953. {
  954. if (onHandleAddNewAction != null)
  955. onHandleAddNewAction(actionMapProperty);
  956. else
  957. {
  958. var actionProperty = InputActionSerializationHelpers.AddAction(actionMapProperty);
  959. InputActionSerializationHelpers.AddBinding(actionProperty, actionMapProperty, groups: bindingGroupForNewBindings);
  960. OnNewItemAdded(actionProperty);
  961. }
  962. }
  963. public void AddNewBinding()
  964. {
  965. foreach (var actionItem in GetSelectedItemsOrParentsOfType<ActionTreeItem>())
  966. AddNewBinding(actionItem.property, actionItem.actionMapProperty);
  967. }
  968. public void AddNewBinding(SerializedProperty actionProperty, SerializedProperty actionMapProperty)
  969. {
  970. var bindingProperty = InputActionSerializationHelpers.AddBinding(actionProperty, actionMapProperty,
  971. groups: bindingGroupForNewBindings);
  972. onBindingAdded?.Invoke(bindingProperty);
  973. OnNewItemAdded(bindingProperty);
  974. }
  975. public void AddNewComposite(string compositeType)
  976. {
  977. foreach (var actionItem in GetSelectedItemsOrParentsOfType<ActionTreeItem>())
  978. AddNewComposite(actionItem.property, actionItem.actionMapProperty, compositeType);
  979. }
  980. public void AddNewComposite(SerializedProperty actionProperty, SerializedProperty actionMapProperty, string compositeName)
  981. {
  982. var compositeType = InputBindingComposite.s_Composites.LookupTypeRegistration(compositeName);
  983. if (compositeType == null)
  984. throw new ArgumentException($"Cannot find composite registration for {compositeName}",
  985. nameof(compositeName));
  986. var compositeProperty = InputActionSerializationHelpers.AddCompositeBinding(actionProperty,
  987. actionMapProperty, compositeName, compositeType, groups: bindingGroupForNewBindings);
  988. onBindingAdded?.Invoke(compositeProperty);
  989. OnNewItemAdded(compositeProperty);
  990. }
  991. private void OnNewItemAdded(SerializedProperty property)
  992. {
  993. OnSerializedObjectModified();
  994. SelectItemAndBeginRename(property);
  995. }
  996. private void SelectItemAndBeginRename(SerializedProperty property)
  997. {
  998. var item = FindItemFor(property);
  999. if (item == null)
  1000. {
  1001. // if we could not find the item, try clearing search filters.
  1002. ClearItemSearchFilterAndReload();
  1003. item = FindItemFor(property);
  1004. }
  1005. Debug.Assert(item != null, $"Cannot find newly created item for {property.propertyPath}");
  1006. SetExpandedRecursive(item.id, true);
  1007. SelectItem(item);
  1008. SetFocus();
  1009. FrameItem(item.id);
  1010. if (item.canRename)
  1011. BeginRename(item);
  1012. }
  1013. #endregion
  1014. #region Drawing
  1015. public override void OnGUI(Rect rect)
  1016. {
  1017. if (m_InitiateContextMenuOnNextRepaint)
  1018. {
  1019. m_InitiateContextMenuOnNextRepaint = false;
  1020. PopUpContextMenu();
  1021. }
  1022. if (ReloadIfSerializedObjectHasBeenChanged())
  1023. return;
  1024. // Draw border rect.
  1025. EditorGUI.LabelField(rect, GUIContent.none, Styles.backgroundWithBorder);
  1026. rect.x += 1;
  1027. rect.y += 1;
  1028. rect.height -= 1;
  1029. rect.width -= 2;
  1030. if (drawHeader)
  1031. DrawHeader(ref rect);
  1032. base.OnGUI(rect);
  1033. HandleCopyPasteCommandEvent(Event.current);
  1034. }
  1035. private void DrawHeader(ref Rect rect)
  1036. {
  1037. var headerRect = rect;
  1038. headerRect.height = EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
  1039. rect.y += headerRect.height;
  1040. rect.height -= headerRect.height;
  1041. // Draw label.
  1042. EditorGUI.LabelField(headerRect, m_Title, Styles.columnHeaderLabel);
  1043. // Draw minus button.
  1044. var buttonRect = headerRect;
  1045. buttonRect.width = EditorGUIUtility.singleLineHeight;
  1046. buttonRect.x += rect.width - buttonRect.width - EditorGUIUtility.standardVerticalSpacing;
  1047. if (drawMinusButton)
  1048. {
  1049. var minusButtonDisabled = !HasSelection();
  1050. using (new EditorGUI.DisabledScope(minusButtonDisabled))
  1051. {
  1052. if (GUI.Button(buttonRect, minusIcon, GUIStyle.none))
  1053. DeleteDataOfSelectedItems();
  1054. }
  1055. buttonRect.x -= buttonRect.width + EditorGUIUtility.standardVerticalSpacing;
  1056. }
  1057. // Draw plus button.
  1058. if (drawPlusButton)
  1059. {
  1060. var plusIconDisabled = onBuildTree == null;
  1061. using (new EditorGUI.DisabledScope(plusIconDisabled))
  1062. {
  1063. if (GUI.Button(buttonRect, plusIcon, GUIStyle.none))
  1064. {
  1065. if (rootItem is ActionMapTreeItem mapItem)
  1066. {
  1067. AddNewAction(mapItem.property);
  1068. }
  1069. else if (rootItem is ActionTreeItem actionItem)
  1070. {
  1071. // Adding a composite has multiple options. Pop up a menu.
  1072. var menu = new GenericMenu();
  1073. BuildMenuToAddBindings(menu, actionItem);
  1074. menu.ShowAsContext();
  1075. }
  1076. else
  1077. {
  1078. AddNewActionMap();
  1079. }
  1080. }
  1081. buttonRect.x -= buttonRect.width + EditorGUIUtility.standardVerticalSpacing;
  1082. }
  1083. }
  1084. // Draw action properties button.
  1085. if (drawActionPropertiesButton && rootItem is ActionTreeItem item)
  1086. {
  1087. if (GUI.Button(buttonRect, s_ActionPropertiesIcon, GUIStyle.none))
  1088. onDoubleClick?.Invoke(item);
  1089. }
  1090. }
  1091. // For each item, we draw
  1092. // 1) color tag
  1093. // 2) foldout
  1094. // 3) display name
  1095. // 4) Line underneath item
  1096. private const int kColorTagWidth = 6;
  1097. private const int kFoldoutWidth = 15;
  1098. ////FIXME: foldout hover region is way too large; partly overlaps the text of items
  1099. private bool DrawFoldout(Rect position, bool expandedState, GUIStyle style)
  1100. {
  1101. // We don't get the depth of the item we're drawing the foldout for but we can
  1102. // infer it by the amount that the given rectangle was indented.
  1103. var indent = (int)(position.x / kFoldoutWidth);
  1104. var indentLevel = EditorGUI.indentLevel;
  1105. // When drawing input actions in the input actions editor, we don't want to offset the foldout
  1106. // icon any further than the position that's passed in to this function, so take advantage of
  1107. // the fact that indentLevel is always zero in that editor.
  1108. position.x = EditorGUI.IndentedRect(position).x * Mathf.Clamp01(indentLevel) + kColorTagWidth + 2 + indent * kColorTagWidth;
  1109. position.width = kFoldoutWidth;
  1110. var hierarchyMode = EditorGUIUtility.hierarchyMode;
  1111. // We remove the editor indent level and set hierarchy mode to false when drawing the foldout
  1112. // arrow so that in the inspector we don't get additional padding on the arrow for the inspector
  1113. // gutter, and so that the indent level doesn't apply because we've done that ourselves.
  1114. EditorGUI.indentLevel = 0;
  1115. EditorGUIUtility.hierarchyMode = false;
  1116. var foldoutExpanded = EditorGUI.Foldout(position, expandedState, GUIContent.none, true, style);
  1117. EditorGUI.indentLevel = indentLevel;
  1118. EditorGUIUtility.hierarchyMode = hierarchyMode;
  1119. return foldoutExpanded;
  1120. }
  1121. protected override void RowGUI(RowGUIArgs args)
  1122. {
  1123. var item = (ActionTreeItemBase)args.item;
  1124. var isRepaint = Event.current.type == EventType.Repaint;
  1125. // Color tag at beginning of line.
  1126. var colorTagRect = EditorGUI.IndentedRect(args.rowRect);
  1127. colorTagRect.x += item.depth * kColorTagWidth;
  1128. colorTagRect.width = kColorTagWidth;
  1129. if (isRepaint)
  1130. item.colorTagStyle.Draw(colorTagRect, GUIContent.none, false, false, false, false);
  1131. // Text.
  1132. // NOTE: When renaming, the renaming overlay gets drawn outside of our control so don't draw the label in that case
  1133. // as otherwise it will peak out from underneath the overlay.
  1134. if (!args.isRenaming && isRepaint)
  1135. {
  1136. var text = item.displayName;
  1137. var textRect = GetTextRect(args.rowRect, item);
  1138. var style = args.selected ? Styles.selectedText : Styles.text;
  1139. if (item.showWarningIcon)
  1140. {
  1141. var content = new GUIContent(text, EditorGUIUtility.FindTexture("console.warnicon.sml"));
  1142. style.Draw(textRect, content, false, false, args.selected, args.focused);
  1143. }
  1144. else
  1145. style.Draw(textRect, text, false, false, args.selected, args.focused);
  1146. }
  1147. // Bottom line.
  1148. var lineRect = EditorGUI.IndentedRect(args.rowRect);
  1149. lineRect.y += lineRect.height - 1;
  1150. lineRect.height = 1;
  1151. if (isRepaint)
  1152. Styles.border.Draw(lineRect, GUIContent.none, false, false, false, false);
  1153. // For action items, add a dropdown menu to add bindings.
  1154. if (item is ActionTreeItem actionItem)
  1155. {
  1156. var buttonRect = args.rowRect;
  1157. buttonRect.x = buttonRect.width - (EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing);
  1158. buttonRect.width = EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
  1159. if (GUI.Button(buttonRect, s_PlusBindingIcon, GUIStyle.none))
  1160. {
  1161. var menu = new GenericMenu();
  1162. BuildMenuToAddBindings(menu, actionItem);
  1163. menu.ShowAsContext();
  1164. }
  1165. }
  1166. }
  1167. protected override float GetCustomRowHeight(int row, TreeViewItem item)
  1168. {
  1169. return 18;
  1170. }
  1171. protected override Rect GetRenameRect(Rect rowRect, int row, TreeViewItem item)
  1172. {
  1173. var textRect = GetTextRect(rowRect, item, false);
  1174. textRect.x += 2;
  1175. textRect.height -= 2;
  1176. return textRect;
  1177. }
  1178. private Rect GetTextRect(Rect rowRect, TreeViewItem item, bool applyIndent = true)
  1179. {
  1180. var indent = (item.depth + 1) * kColorTagWidth + kFoldoutWidth;
  1181. var textRect = applyIndent ? EditorGUI.IndentedRect(rowRect) : rowRect;
  1182. textRect.x += indent;
  1183. return textRect;
  1184. }
  1185. #endregion
  1186. // Undo is a problem. When an undo or redo is performed, the SerializedObject may change behind
  1187. // our backs which means that the information shown in the tree may be outdated now.
  1188. //
  1189. // We do have the Undo.undoRedoPerformed global callback but because PropertyDrawers
  1190. // have no observable life cycle, we cannot always easily hook into the callback and force a reload
  1191. // of the tree. Also, while returning false from PropertyDrawer.CanCacheInspectorGUI() might one make suspect that
  1192. // a PropertyDrawer would automatically be thrown away and recreated if the SerializedObject
  1193. // is modified by undo, that does not happen in practice.
  1194. //
  1195. // We could just Reload() the tree all the time but TreeView.Reload() itself forces a repaint and
  1196. // this will thus easily lead to infinite repaints.
  1197. //
  1198. // So, what we do is make use of the built-in dirty count we can get for Unity objects. If the count
  1199. // changes and it wasn't caused by us, we reload the tree. Means we still reload unnecessarily if
  1200. // some other property on a component changes but at least we don't reload all the time.
  1201. //
  1202. // A positive side-effect is that we will catch *any* change to the SerializedObject, not just
  1203. // undo/redo and we can do so without having to hook into Undo.undoRedoPerformed anywhere.
  1204. private void OnSerializedObjectModified()
  1205. {
  1206. serializedObject.ApplyModifiedProperties();
  1207. UpdateSerializedObjectDirtyCount();
  1208. Reload();
  1209. onSerializedObjectModified?.Invoke();
  1210. }
  1211. public void UpdateSerializedObjectDirtyCount()
  1212. {
  1213. m_SerializedObjectDirtyCount = serializedObject != null ? EditorUtility.GetDirtyCount(serializedObject.targetObject) : 0;
  1214. }
  1215. private bool ReloadIfSerializedObjectHasBeenChanged()
  1216. {
  1217. var oldCount = m_SerializedObjectDirtyCount;
  1218. UpdateSerializedObjectDirtyCount();
  1219. if (oldCount != m_SerializedObjectDirtyCount)
  1220. {
  1221. Reload();
  1222. onSerializedObjectModified?.Invoke();
  1223. return true;
  1224. }
  1225. return false;
  1226. }
  1227. public SerializedObject serializedObject { get; }
  1228. public string bindingGroupForNewBindings { get; set; }
  1229. public new TreeViewItem rootItem => base.rootItem;
  1230. public Action onSerializedObjectModified { get; set; }
  1231. public Action onSelectionChanged { get; set; }
  1232. public Action<ActionTreeItemBase> onDoubleClick { get; set; }
  1233. public Action<ActionTreeItemBase> onBeginRename { get; set; }
  1234. public Func<TreeViewItem> onBuildTree { get; set; }
  1235. public Action<SerializedProperty> onBindingAdded { get; set; }
  1236. public bool drawHeader { get; set; }
  1237. public bool drawPlusButton { get; set; }
  1238. public bool drawMinusButton { get; set; }
  1239. public bool drawActionPropertiesButton { get; set; }
  1240. public Action<SerializedProperty> onHandleAddNewAction { get; set; }
  1241. public (string, string) title
  1242. {
  1243. get => (m_Title?.text, m_Title?.tooltip);
  1244. set => m_Title = new GUIContent(value.Item1, value.Item2);
  1245. }
  1246. public new float totalHeight
  1247. {
  1248. get
  1249. {
  1250. var height = base.totalHeight;
  1251. if (drawHeader)
  1252. height += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
  1253. height += 1; // Border.
  1254. return height;
  1255. }
  1256. }
  1257. public ActionTreeItemBase this[string path]
  1258. {
  1259. get
  1260. {
  1261. var item = FindItemByPath(path);
  1262. if (item == null)
  1263. throw new KeyNotFoundException(path);
  1264. return item;
  1265. }
  1266. }
  1267. private GUIContent plusIcon
  1268. {
  1269. get
  1270. {
  1271. if (rootItem is ActionMapTreeItem)
  1272. return s_PlusActionIcon;
  1273. if (rootItem is ActionTreeItem)
  1274. return s_PlusBindingIcon;
  1275. return s_PlusActionMapIcon;
  1276. }
  1277. }
  1278. private GUIContent minusIcon => s_DeleteSectionIcon;
  1279. private FilterCriterion[] m_ItemFilterCriteria;
  1280. private GUIContent m_Title;
  1281. private bool m_InitiateContextMenuOnNextRepaint;
  1282. private bool m_ForceAcceptRename;
  1283. private int m_SerializedObjectDirtyCount;
  1284. private static readonly GUIContent s_AddBindingLabel = EditorGUIUtility.TrTextContent("Add Binding");
  1285. private static readonly GUIContent s_AddActionLabel = EditorGUIUtility.TrTextContent("Add Action");
  1286. private static readonly GUIContent s_AddActionMapLabel = EditorGUIUtility.TrTextContent("Add Action Map");
  1287. private static readonly GUIContent s_PlusBindingIcon = EditorGUIUtility.TrIconContent("Toolbar Plus More", "Add Binding");
  1288. private static readonly GUIContent s_PlusActionIcon = EditorGUIUtility.TrIconContent("Toolbar Plus", "Add Action");
  1289. private static readonly GUIContent s_PlusActionMapIcon = EditorGUIUtility.TrIconContent("Toolbar Plus", "Add Action Map");
  1290. private static readonly GUIContent s_DeleteSectionIcon = EditorGUIUtility.TrIconContent("Toolbar Minus", "Delete Selection");
  1291. private static readonly GUIContent s_ActionPropertiesIcon = EditorGUIUtility.TrIconContent("Settings", "Action Properties");
  1292. private static readonly GUIContent s_CutLabel = EditorGUIUtility.TrTextContent("Cut");
  1293. private static readonly GUIContent s_CopyLabel = EditorGUIUtility.TrTextContent("Copy");
  1294. private static readonly GUIContent s_PasteLabel = EditorGUIUtility.TrTextContent("Paste");
  1295. private static readonly GUIContent s_DeleteLabel = EditorGUIUtility.TrTextContent("Delete");
  1296. private static readonly GUIContent s_DuplicateLabel = EditorGUIUtility.TrTextContent("Duplicate");
  1297. private static readonly GUIContent s_RenameLabel = EditorGUIUtility.TrTextContent("Rename");
  1298. private static readonly GUIContent s_ExpandAllLabel = EditorGUIUtility.TrTextContent("Expand All");
  1299. private static readonly GUIContent s_CollapseAllLabel = EditorGUIUtility.TrTextContent("Collapse All");
  1300. public static string SharedResourcesPath = "Packages/com.unity.inputsystem/InputSystem/Editor/AssetEditor/Resources/";
  1301. public static string ResourcesPath
  1302. {
  1303. get
  1304. {
  1305. if (EditorGUIUtility.isProSkin)
  1306. return SharedResourcesPath + "pro/";
  1307. return SharedResourcesPath + "personal/";
  1308. }
  1309. }
  1310. public struct FilterCriterion
  1311. {
  1312. public enum Type
  1313. {
  1314. ByName,
  1315. ByBindingGroup,
  1316. ByDeviceLayout,
  1317. }
  1318. public enum Match
  1319. {
  1320. Success,
  1321. Failure,
  1322. None,
  1323. }
  1324. public string text;
  1325. public Type type;
  1326. public static string k_BindingGroupTag = "g:";
  1327. public static string k_DeviceLayoutTag = "d:";
  1328. public Match Matches(ActionTreeItemBase item)
  1329. {
  1330. Debug.Assert(item != null, "Item cannot be null");
  1331. switch (type)
  1332. {
  1333. case Type.ByName:
  1334. {
  1335. // NOTE: Composite items have names (and part bindings in a way, too) but we don't filter on them.
  1336. if (item is ActionMapTreeItem || item is ActionTreeItem)
  1337. {
  1338. var matchesSelf = item.displayName.Contains(text, StringComparison.InvariantCultureIgnoreCase);
  1339. // Name filters behave recursively. I.e. if any item in the subtree is matched by the name filter,
  1340. // the item is included.
  1341. if (!matchesSelf && CheckChildrenFor(Match.Success, item))
  1342. return Match.Success;
  1343. return matchesSelf ? Match.Success : Match.Failure;
  1344. }
  1345. break;
  1346. }
  1347. case Type.ByBindingGroup:
  1348. {
  1349. if (item is BindingTreeItem bindingItem)
  1350. {
  1351. // For composites, succeed the match if any children match.
  1352. if (item is CompositeBindingTreeItem)
  1353. return CheckChildrenFor(Match.Success, item) ? Match.Success : Match.Failure;
  1354. // Items that are in no binding group match any binding group.
  1355. if (string.IsNullOrEmpty(bindingItem.groups))
  1356. return Match.Success;
  1357. var groups = bindingItem.groups.Split(InputBinding.Separator);
  1358. var bindingGroup = text;
  1359. return groups.Any(x => x.Equals(bindingGroup, StringComparison.InvariantCultureIgnoreCase))
  1360. ? Match.Success
  1361. : Match.Failure;
  1362. }
  1363. break;
  1364. }
  1365. case Type.ByDeviceLayout:
  1366. {
  1367. if (item is BindingTreeItem bindingItem)
  1368. {
  1369. // For composites, succeed the match if any children match.
  1370. if (item is CompositeBindingTreeItem)
  1371. return CheckChildrenFor(Match.Success, item) ? Match.Success : Match.Failure;
  1372. var deviceLayout = InputControlPath.TryGetDeviceLayout(bindingItem.path);
  1373. return string.Equals(deviceLayout, text, StringComparison.InvariantCultureIgnoreCase)
  1374. || InputControlLayout.s_Layouts.IsBasedOn(new InternedString(deviceLayout), new InternedString(text))
  1375. ? Match.Success
  1376. : Match.Failure;
  1377. }
  1378. break;
  1379. }
  1380. }
  1381. return Match.None;
  1382. }
  1383. private bool CheckChildrenFor(Match match, ActionTreeItemBase item)
  1384. {
  1385. if (!item.hasChildren)
  1386. return false;
  1387. foreach (var child in item.children.OfType<ActionTreeItemBase>())
  1388. if (Matches(child) == match)
  1389. return true;
  1390. return false;
  1391. }
  1392. public static FilterCriterion ByName(string name)
  1393. {
  1394. return new FilterCriterion {text = name, type = Type.ByName};
  1395. }
  1396. public static FilterCriterion ByBindingGroup(string group)
  1397. {
  1398. return new FilterCriterion {text = group, type = Type.ByBindingGroup};
  1399. }
  1400. public static FilterCriterion ByDeviceLayout(string layout)
  1401. {
  1402. return new FilterCriterion {text = layout, type = Type.ByDeviceLayout};
  1403. }
  1404. public static List<FilterCriterion> FromString(string criteria)
  1405. {
  1406. if (string.IsNullOrEmpty(criteria))
  1407. return null;
  1408. var list = new List<FilterCriterion>();
  1409. foreach (var substring in criteria.Tokenize())
  1410. {
  1411. if (substring.StartsWith(k_DeviceLayoutTag))
  1412. list.Add(ByDeviceLayout(substring.Substr(2).Unescape()));
  1413. else if (substring.StartsWith(k_BindingGroupTag))
  1414. list.Add(ByBindingGroup(substring.Substr(2).Unescape()));
  1415. else
  1416. list.Add(ByName(substring.ToString().Unescape()));
  1417. }
  1418. return list;
  1419. }
  1420. public static string ToString(IEnumerable<FilterCriterion> criteria)
  1421. {
  1422. var builder = new StringBuilder();
  1423. foreach (var criterion in criteria)
  1424. {
  1425. if (builder.Length > 0)
  1426. builder.Append(' ');
  1427. if (criterion.type == Type.ByBindingGroup)
  1428. builder.Append(k_BindingGroupTag);
  1429. else if (criterion.type == Type.ByDeviceLayout)
  1430. builder.Append(k_DeviceLayoutTag);
  1431. builder.Append(criterion.text);
  1432. }
  1433. return builder.ToString();
  1434. }
  1435. }
  1436. public static class Styles
  1437. {
  1438. public static readonly GUIStyle text = new GUIStyle("Label").WithAlignment(TextAnchor.MiddleLeft);
  1439. public static readonly GUIStyle selectedText = new GUIStyle("Label").WithAlignment(TextAnchor.MiddleLeft).WithNormalTextColor(Color.white);
  1440. public static readonly GUIStyle backgroundWithoutBorder = new GUIStyle("Label")
  1441. .WithNormalBackground(AssetDatabase.LoadAssetAtPath<Texture2D>(ResourcesPath + "actionTreeBackgroundWithoutBorder.png"));
  1442. public static readonly GUIStyle border = new GUIStyle("Label")
  1443. .WithNormalBackground(AssetDatabase.LoadAssetAtPath<Texture2D>(ResourcesPath + "actionTreeBackground.png"))
  1444. .WithBorder(new RectOffset(0, 0, 0, 1));
  1445. public static readonly GUIStyle backgroundWithBorder = new GUIStyle("Label")
  1446. .WithNormalBackground(AssetDatabase.LoadAssetAtPath<Texture2D>(ResourcesPath + "actionTreeBackground.png"))
  1447. .WithBorder(new RectOffset(3, 3, 3, 3))
  1448. .WithMargin(new RectOffset(4, 4, 4, 4));
  1449. public static readonly GUIStyle columnHeaderLabel = new GUIStyle(EditorStyles.toolbar)
  1450. .WithAlignment(TextAnchor.MiddleLeft)
  1451. .WithFontStyle(FontStyle.Bold)
  1452. .WithPadding(new RectOffset(10, 6, 0, 0));
  1453. }
  1454. // Just so that we can tell apart TreeViews containing only maps.
  1455. internal class ActionMapListItem : TreeViewItem
  1456. {
  1457. }
  1458. }
  1459. }
  1460. #endif // UNITY_EDITOR