1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705 |
- #if UNITY_EDITOR
- using System;
- using System.Collections.Generic;
- using System.ComponentModel;
- using System.Linq;
- using System.Reflection;
- using System.Text;
- using UnityEditor;
- using UnityEditor.IMGUI.Controls;
- using UnityEngine.InputSystem.Layouts;
- using UnityEngine.InputSystem.Utilities;
-
- // The action tree view illustrates one of the weaknesses of Unity's editing model. While operating directly
- // on serialized data does have a number of advantages (the built-in undo system being one of them), making the
- // persistence model equivalent to the edit model doesn't work well. Serialized data will be laid out for persistence,
- // not for the convenience of editing operations. This means that editing operations have to constantly jump through
- // hoops to map themselves onto the persistence model of the data.
-
- ////TODO: With many actions and bindings the list becomes really hard to grok; make things more visually distinctive
-
- ////TODO: add context menu items for reordering action and binging entries (like "Move Up" and "Move Down")
-
- ////FIXME: context menu cannot be brought up when there's no items in the tree
-
- ////FIXME: RMB context menu for actions displays composites that aren't applicable to the action
-
- namespace UnityEngine.InputSystem.Editor
- {
- /// <summary>
- /// A tree view showing action maps, actions, and bindings. This is the core piece around which the various
- /// pieces of action editing functionality revolve.
- /// </summary>
- /// <remarks>
- /// The tree view can be flexibly used to contain only parts of a specific action setup. For example,
- /// by only adding items for action maps (<see cref="ActionMapTreeItem"/> to the tree, the tree view
- /// will become a flat list of action maps. Or by adding only items for actions (<see cref="ActionTreeItem"/>
- /// and items for their bindings (<see cref="BindingTreeItem"/>) to the tree, it will become an action-only
- /// tree view.
- ///
- /// This is used by the action asset editor to separate action maps and their actions into two separate
- /// tree views (the leftmost and the middle column of the editor).
- ///
- /// Each action tree comes with copy-paste and context menu support.
- /// </remarks>
- internal class InputActionTreeView : TreeView
- {
- #region Creation
-
- public InputActionTreeView(SerializedObject serializedObject, TreeViewState state = null)
- : base(state ?? new TreeViewState())
- {
- Debug.Assert(serializedObject != null, "Must have serialized object");
- this.serializedObject = serializedObject;
- UpdateSerializedObjectDirtyCount();
- foldoutOverride = DrawFoldout;
- drawHeader = true;
- drawPlusButton = true;
- drawMinusButton = true;
- m_ForceAcceptRename = false;
- m_Title = new GUIContent("");
- }
-
- /// <summary>
- /// Build an action tree that shows only the bindings for the given action.
- /// </summary>
- public static TreeViewItem BuildWithJustBindingsFromAction(SerializedProperty actionProperty, SerializedProperty actionMapProperty = null)
- {
- Debug.Assert(actionProperty != null, "Action property cannot be null");
- var root = new ActionTreeItem(actionMapProperty, actionProperty);
- root.depth = -1;
- root.AddBindingsTo(root);
- return root;
- }
-
- /// <summary>
- /// Build an action tree that shows only the actions and bindings for the given action map.
- /// </summary>
- public static TreeViewItem BuildWithJustActionsAndBindingsFromMap(SerializedProperty actionMapProperty)
- {
- Debug.Assert(actionMapProperty != null, "Action map property cannot be null");
- var root = new ActionMapTreeItem(actionMapProperty);
- root.depth = -1;
- root.AddActionsAndBindingsTo(root);
- return root;
- }
-
- /// <summary>
- /// Build an action tree that contains only the action maps from the given .inputactions asset.
- /// </summary>
- public static TreeViewItem BuildWithJustActionMapsFromAsset(SerializedObject assetObject)
- {
- Debug.Assert(assetObject != null, "Asset object cannot be null");
- var root = new ActionMapListItem {id = 0, depth = -1};
- ActionMapTreeItem.AddActionMapsFromAssetTo(root, assetObject);
- return root;
- }
-
- public static TreeViewItem BuildFullTree(SerializedObject assetObject)
- {
- Debug.Assert(assetObject != null, "Asset object cannot be null");
- var root = new TreeViewItem {id = 0, depth = -1};
- ActionMapTreeItem.AddActionMapsFromAssetTo(root, assetObject);
- if (root.hasChildren)
- foreach (var child in root.children)
- ((ActionMapTreeItem)child).AddActionsAndBindingsTo(child);
- return root;
- }
-
- protected override TreeViewItem BuildRoot()
- {
- var root = onBuildTree?.Invoke() ?? new TreeViewItem(0, -1);
-
- // If we have a filter, remove unwanted items from the tree.
- // NOTE: We use this method rather than TreeView's built-in search functionality as we want
- // to keep the tree structure fully intact whereas searchString switches the tree into
- // a different view mode.
- if (m_ItemFilterCriteria.LengthSafe() > 0 && root.hasChildren)
- {
- foreach (var child in root.children.OfType<ActionTreeItemBase>().ToArray())
- PruneTreeByItemSearchFilter(child);
- }
-
- // Root node is required to have `children` not be null. Add empty list,
- // if necessary. Can happen, for example, if we have a singleton action at the
- // root but no bindings on it.
- if (root.children == null)
- root.children = new List<TreeViewItem>();
-
- return root;
- }
-
- #endregion
-
- #region Filtering
-
- internal bool hasFilter => m_ItemFilterCriteria != null;
-
- public void ClearItemSearchFilterAndReload()
- {
- if (m_ItemFilterCriteria == null)
- return;
- m_ItemFilterCriteria = null;
- Reload();
- }
-
- public void SetItemSearchFilterAndReload(string criteria)
- {
- SetItemSearchFilterAndReload(FilterCriterion.FromString(criteria));
- }
-
- public void SetItemSearchFilterAndReload(IEnumerable<FilterCriterion> criteria)
- {
- m_ItemFilterCriteria = criteria.ToArray();
- Reload();
- }
-
- private void PruneTreeByItemSearchFilter(ActionTreeItemBase item)
- {
- // Prune subtree if item is forced out by any of our criteria.
- if (m_ItemFilterCriteria.Any(x => x.Matches(item) == FilterCriterion.Match.Failure))
- {
- item.parent.children.Remove(item);
-
- // Add to list of hidden children.
- if (item.parent is ActionTreeItemBase parent)
- {
- if (parent.m_HiddenChildren == null)
- parent.m_HiddenChildren = new List<ActionTreeItemBase>();
- parent.m_HiddenChildren.Add(item);
- }
-
- return;
- }
-
- ////REVIEW: should we *always* do this? (regardless of whether a control scheme is selected)
- // When filtering by binding group, we tag bindings that are not in any binding group as "{GLOBAL}".
- // This helps when having a specific control scheme selected, to also see the bindings that are active
- // in that control scheme by virtue of not being associated with *any* specific control scheme.
- if (item is BindingTreeItem bindingItem &&
- !(item is CompositeBindingTreeItem) &&
- string.IsNullOrEmpty(bindingItem.groups) &&
- m_ItemFilterCriteria.Any(x => x.type == FilterCriterion.Type.ByBindingGroup))
- {
- item.displayName += " {GLOBAL}";
- }
-
- // Prune children.
- if (item.hasChildren)
- {
- foreach (var child in item.children.OfType<ActionTreeItemBase>().ToArray()) // We're modifying the child list so copy.
- PruneTreeByItemSearchFilter(child);
- }
- }
-
- #endregion
-
- #region Finding Items
-
- public ActionTreeItemBase FindItemByPath(string path)
- {
- var components = path.Split('/');
- var current = rootItem;
- foreach (var component in components)
- {
- if (current.hasChildren)
- {
- var found = false;
- foreach (var child in current.children)
- {
- if (child.displayName.Equals(component, StringComparison.InvariantCultureIgnoreCase))
- {
- current = child;
- found = true;
- break;
- }
- }
-
- if (found)
- continue;
- }
-
- return null;
- }
- return (ActionTreeItemBase)current;
- }
-
- public ActionTreeItemBase FindItemByPropertyPath(string propertyPath)
- {
- return FindFirstItem<ActionTreeItemBase>(x => x.property.propertyPath == propertyPath);
- }
-
- public ActionTreeItemBase FindItemFor(SerializedProperty element)
- {
- // We may be looking at a SerializedProperty that refers to the same element but is
- // its own instance different from the one we're using in the tree. Compare properties
- // by path, not by object instance.
- return FindFirstItem<ActionTreeItemBase>(x => x.property.propertyPath == element.propertyPath);
- }
-
- public TItem FindFirstItem<TItem>(Func<TItem, bool> predicate)
- where TItem : ActionTreeItemBase
- {
- return FindFirstItemRecursive(rootItem, predicate);
- }
-
- private static TItem FindFirstItemRecursive<TItem>(TreeViewItem current, Func<TItem, bool> predicate)
- where TItem : ActionTreeItemBase
- {
- if (current is TItem itemOfType && predicate(itemOfType))
- return itemOfType;
-
- if (current.hasChildren)
- foreach (var child in current.children)
- {
- var item = FindFirstItemRecursive(child, predicate);
- if (item != null)
- return item;
- }
-
- return null;
- }
-
- #endregion
-
- #region Selection
-
- public void ClearSelection()
- {
- SetSelection(new int[0], TreeViewSelectionOptions.FireSelectionChanged);
- }
-
- public void SelectItems(IEnumerable<ActionTreeItemBase> items)
- {
- SetSelection(items.Select(x => x.id).ToList(), TreeViewSelectionOptions.FireSelectionChanged);
- }
-
- public void SelectItem(SerializedProperty element, bool additive = false)
- {
- var item = FindItemFor(element);
- if (item == null)
- throw new ArgumentException($"Cannot find item for property path '{element.propertyPath}'", nameof(element));
-
- SelectItem(item, additive);
- }
-
- public void SelectItem(string path, bool additive = false)
- {
- if (!TrySelectItem(path, additive))
- throw new ArgumentException($"Cannot find item with path 'path'", nameof(path));
- }
-
- public bool TrySelectItem(string path, bool additive = false)
- {
- var item = FindItemByPath(path);
- if (item == null)
- return false;
-
- SelectItem(item, additive);
- return true;
- }
-
- public void SelectItem(ActionTreeItemBase item, bool additive = false)
- {
- if (additive)
- {
- var selection = new List<int>();
- selection.AddRange(GetSelection());
- selection.Add(item.id);
- SetSelection(selection, TreeViewSelectionOptions.FireSelectionChanged);
- }
- else
- {
- SetSelection(new[] { item.id }, TreeViewSelectionOptions.FireSelectionChanged);
- }
- }
-
- public IEnumerable<ActionTreeItemBase> GetSelectedItems()
- {
- foreach (var id in GetSelection())
- {
- if (FindItem(id, rootItem) is ActionTreeItemBase item)
- yield return item;
- }
- }
-
- /// <summary>
- /// Same as <see cref="GetSelectedItems"/> but with items that are selected but are children of other items
- /// that also selected being filtered out.
- /// </summary>
- /// <remarks>
- /// This is useful for operations such as copy-paste where copy a parent will implicitly copy the child.
- /// </remarks>
- public IEnumerable<ActionTreeItemBase> GetSelectedItemsWithChildrenFilteredOut()
- {
- var selectedItems = GetSelectedItems().ToArray();
- foreach (var item in selectedItems)
- {
- if (selectedItems.Any(x => x.IsParentOf(item)))
- continue;
- yield return item;
- }
- }
-
- public IEnumerable<TItem> GetSelectedItemsOrParentsOfType<TItem>()
- where TItem : ActionTreeItemBase
- {
- // If there is no selection and the root item has the type we're looking for,
- // consider it selected. This allows adding items at the toplevel.
- if (!HasSelection() && rootItem is TItem root)
- {
- yield return root;
- }
- else
- {
- foreach (var id in GetSelection())
- {
- var item = FindItem(id, rootItem);
- while (item != null)
- {
- if (item is TItem itemOfType)
- yield return itemOfType;
- item = item.parent;
- }
- }
- }
- }
-
- public void SelectFirstToplevelItem()
- {
- if (rootItem.children.Any())
- SetSelection(new[] {rootItem.children[0].id}, TreeViewSelectionOptions.FireSelectionChanged);
- }
-
- protected override void SelectionChanged(IList<int> selectedIds)
- {
- onSelectionChanged?.Invoke();
- }
-
- #endregion
-
- #region Renaming
-
- public new void BeginRename(TreeViewItem item)
- {
- // If a rename is already in progress, force it to end first.
- EndRename();
- onBeginRename?.Invoke((ActionTreeItemBase)item);
- base.BeginRename(item);
- }
-
- protected override bool CanRename(TreeViewItem item)
- {
- return item is ActionTreeItemBase actionTreeItem && actionTreeItem.canRename;
- }
-
- protected override void RenameEnded(RenameEndedArgs args)
- {
- if (!(FindItem(args.itemID, rootItem) is ActionTreeItemBase actionItem))
- return;
-
- if (!(args.acceptedRename || m_ForceAcceptRename) || args.originalName == args.newName)
- return;
-
- Debug.Assert(actionItem.canRename, "Cannot rename " + actionItem);
-
- actionItem.Rename(args.newName);
- OnSerializedObjectModified();
- }
-
- public void EndRename(bool forceAccept)
- {
- m_ForceAcceptRename = forceAccept;
- EndRename();
- m_ForceAcceptRename = false;
- }
-
- protected override void DoubleClickedItem(int id)
- {
- if (!(FindItem(id, rootItem) is ActionTreeItemBase item))
- return;
-
- // If we have a double-click handler, give it control over what happens.
- if (onDoubleClick != null)
- {
- onDoubleClick(item);
- }
- else if (item.canRename)
- {
- // Otherwise, perform a rename by default.
- BeginRename(item);
- }
- }
-
- #endregion
-
- #region Drag&Drop
-
- protected override bool CanStartDrag(CanStartDragArgs args)
- {
- return true;
- }
-
- protected override void SetupDragAndDrop(SetupDragAndDropArgs args)
- {
- DragAndDrop.PrepareStartDrag();
- DragAndDrop.SetGenericData("itemIDs", args.draggedItemIDs.ToArray());
- DragAndDrop.SetGenericData("tree", this);
- DragAndDrop.StartDrag(string.Join(",", args.draggedItemIDs.Select(id => FindItem(id, rootItem).displayName)));
- }
-
- protected override DragAndDropVisualMode HandleDragAndDrop(DragAndDropArgs args)
- {
- var sourceTree = DragAndDrop.GetGenericData("tree") as InputActionTreeView;
- if (sourceTree == null)
- return DragAndDropVisualMode.Rejected;
-
- var itemIds = (int[])DragAndDrop.GetGenericData("itemIDs");
- var altKeyIsDown = Event.current.alt;
-
- // Reject the drag if the parent item does not accept the drop.
- if (args.parentItem is ActionTreeItemBase parentItem)
- {
- if (itemIds.Any(id =>
- !parentItem.AcceptsDrop((ActionTreeItemBase)sourceTree.FindItem(id, sourceTree.rootItem))))
- return DragAndDropVisualMode.Rejected;
- }
- else
- {
- // If the root item isn't an ActionTreeItemBase, we're looking at a tree that starts
- // all the way up at the InputActionAsset. Require the drop to be all action maps.
- if (itemIds.Any(id => !(sourceTree.FindItem(id, sourceTree.rootItem) is ActionMapTreeItem)))
- return DragAndDropVisualMode.Rejected;
- }
-
- // Handle drop using copy-paste. This allows handling all the various operations
- // using a single code path.
- var isMove = !altKeyIsDown;
- if (args.performDrop)
- {
- // Copy item data.
- var copyBuffer = new StringBuilder();
- var items = itemIds.Select(id => (ActionTreeItemBase)sourceTree.FindItem(id, sourceTree.rootItem));
- CopyItems(items, copyBuffer);
-
- // If we're moving items within the same tree, no need to generate new IDs.
- var assignNewIDs = !(isMove && sourceTree == this);
-
- // Determine where we are moving/copying the data.
- var target = args.parentItem ?? rootItem;
- int? childIndex = null;
- if (args.dragAndDropPosition == DragAndDropPosition.BetweenItems)
- childIndex = args.insertAtIndex;
-
- // If alt isn't down (i.e. we're not duplicating), delete old items.
- // Do this *before* pasting so that assigning new names will not cause names to
- // change when just moving items around.
- if (isMove)
- {
- // Don't use DeleteDataOfSelectedItems() as that will record as a separate operation.
- foreach (var item in items)
- {
- // If we're dropping *between* items on the same parent as the current item and the
- // index we're dropping at (in the parent, NOT in the array) is coming *after* this item,
- // then deleting the item will shift the target index down by one.
- if (item.parent == target && childIndex != null && childIndex > target.children.IndexOf(item))
- --childIndex;
-
- item.DeleteData();
- }
- }
-
- // Paste items onto target.
- var oldBindingGroupForNewBindings = bindingGroupForNewBindings;
- try
- {
- // With drag&drop, preserve binding groups.
- bindingGroupForNewBindings = null;
-
- PasteItems(copyBuffer.ToString(),
- new[] { new InsertLocation { item = target, childIndex = childIndex } },
- assignNewIDs: assignNewIDs);
- }
- finally
- {
- bindingGroupForNewBindings = oldBindingGroupForNewBindings;
- }
-
- DragAndDrop.AcceptDrag();
- }
-
- return isMove ? DragAndDropVisualMode.Move : DragAndDropVisualMode.Copy;
- }
-
- #endregion
-
- #region Copy&Paste
-
- // These need to correspond to what the editor is sending from the "Edit" menu.
- public const string k_CopyCommand = "Copy";
- public const string k_PasteCommand = "Paste";
- public const string k_DuplicateCommand = "Duplicate";
- public const string k_CutCommand = "Cut";
- public const string k_DeleteCommand = "Delete";
- public const string k_SoftDeleteCommand = "SoftDelete";
-
- public void HandleCopyPasteCommandEvent(Event uiEvent)
- {
- if (uiEvent.type == EventType.ValidateCommand)
- {
- switch (uiEvent.commandName)
- {
- case k_CopyCommand:
- case k_CutCommand:
- case k_DuplicateCommand:
- case k_DeleteCommand:
- case k_SoftDeleteCommand:
- if (HasSelection())
- uiEvent.Use();
- break;
-
- case k_PasteCommand:
- var systemCopyBuffer = EditorHelpers.GetSystemCopyBufferContents();
- if (systemCopyBuffer != null && systemCopyBuffer.StartsWith(k_CopyPasteMarker))
- uiEvent.Use();
- break;
- }
- }
- else if (uiEvent.type == EventType.ExecuteCommand)
- {
- switch (uiEvent.commandName)
- {
- case k_CopyCommand:
- CopySelectedItemsToClipboard();
- break;
- case k_PasteCommand:
- PasteDataFromClipboard();
- break;
- case k_CutCommand:
- CopySelectedItemsToClipboard();
- DeleteDataOfSelectedItems();
- break;
- case k_DuplicateCommand:
- DuplicateSelection();
- break;
- case k_DeleteCommand:
- case k_SoftDeleteCommand:
- DeleteDataOfSelectedItems();
- break;
- default:
- return;
- }
- uiEvent.Use();
- }
- }
-
- private void DuplicateSelection()
- {
- var buffer = new StringBuilder();
-
- // If we have a multi-selection, we want to perform the duplication as if each item
- // was duplicated individually. Meaning we paste each duplicate right after the item
- // it was duplicated from. So if, say, an action is selected at the beginning of the
- // tree and one is selected from the end of it, we still paste the copies into the
- // two separate locations correctly.
- //
- // Technically, if both parents and children are selected, we're order dependent here
- // but not sure we really need to care.
-
- var selection = GetSelection();
- ClearSelection();
-
- // Copy-paste each selected item in turn.
- var newItemIds = new List<int>();
- foreach (var id in selection)
- {
- SetSelection(new[] { id });
-
- buffer.Length = 0;
- CopySelectedItemsTo(buffer);
- PasteDataFrom(buffer.ToString());
-
- newItemIds.AddRange(GetSelection());
- }
-
- SetSelection(newItemIds);
- }
-
- internal const string k_CopyPasteMarker = "INPUTASSET ";
- private const string k_StartOfText = "\u0002";
- private const string k_StartOfHeading = "\u0001";
- private const string k_EndOfTransmission = "\u0004";
- private const string k_EndOfTransmissionBlock = "\u0017";
-
- /// <summary>
- /// Copy the currently selected items to the clipboard.
- /// </summary>
- /// <seealso cref="EditorGUIUtility.systemCopyBuffer"/>
- public void CopySelectedItemsToClipboard()
- {
- var copyBuffer = new StringBuilder();
- CopySelectedItemsTo(copyBuffer);
- EditorHelpers.SetSystemCopyBufferContents(copyBuffer.ToString());
- }
-
- public void CopySelectedItemsTo(StringBuilder buffer)
- {
- CopyItems(GetSelectedItemsWithChildrenFilteredOut(), buffer);
- }
-
- public static void CopyItems(IEnumerable<ActionTreeItemBase> items, StringBuilder buffer)
- {
- buffer.Append(k_CopyPasteMarker);
- foreach (var item in items)
- {
- CopyItemData(item, buffer);
- buffer.Append(k_EndOfTransmission);
- }
- }
-
- private static void CopyItemData(ActionTreeItemBase item, StringBuilder buffer)
- {
- buffer.Append(k_StartOfHeading);
- buffer.Append(item.GetType().Name);
- buffer.Append(k_StartOfText);
- // InputActionMaps have back-references to InputActionAssets. Make sure we ignore those.
- buffer.Append(item.property.CopyToJson(ignoreObjectReferences: true));
- buffer.Append(k_EndOfTransmissionBlock);
-
- if (!item.serializedDataIncludesChildren && item.hasChildrenIncludingHidden)
- foreach (var child in item.childrenIncludingHidden)
- CopyItemData(child, buffer);
- }
-
- /// <summary>
- /// Remove the data from the currently selected items from the <see cref="SerializedObject"/>
- /// referenced by the tree's data.
- /// </summary>
- public void DeleteDataOfSelectedItems()
- {
- // When deleting data, indices will shift around. However, we do not delete elements by array indices
- // directly but rather by GUIDs which are used to look up array indices dynamically. This means that
- // we can safely delete the items without worrying about one deletion affecting the next.
- //
- // NOTE: It is important that we first fetch *all* of the selection filtered for parent/child duplicates.
- // If we don't do so up front, the deletions happening later may start interacting with our
- // parent/child test.
- var selection = GetSelectedItemsWithChildrenFilteredOut().ToArray();
-
- // Clear our current selection. If we don't do this first, TreeView will implicitly
- // clear the selection as items disappear but we will not see a selection change notification
- // being triggered.
- ClearSelection();
-
- DeleteItems(selection);
- }
-
- public void DeleteItems(IEnumerable<ActionTreeItemBase> items)
- {
- foreach (var item in items)
- item.DeleteData();
-
- OnSerializedObjectModified();
- }
-
- public bool HavePastableClipboardData()
- {
- var clipboard = EditorHelpers.GetSystemCopyBufferContents();
- return clipboard.StartsWith(k_CopyPasteMarker);
- }
-
- public void PasteDataFromClipboard()
- {
- PasteDataFrom(EditorHelpers.GetSystemCopyBufferContents());
- }
-
- public void PasteDataFrom(string copyBufferString)
- {
- if (!copyBufferString.StartsWith(k_CopyPasteMarker))
- return;
-
- var locations = GetSelectedItemsWithChildrenFilteredOut().Select(x => new InsertLocation { item = x }).ToList();
- if (locations.Count == 0)
- locations.Add(new InsertLocation { item = rootItem });
-
- ////REVIEW: filtering out children may remove the very item we need to get the right match for a copy block?
- PasteItems(copyBufferString, locations);
- }
-
- public struct InsertLocation
- {
- public TreeViewItem item;
- public int? childIndex;
- }
-
- public void PasteItems(string copyBufferString, IEnumerable<InsertLocation> locations, bool assignNewIDs = true, bool selectNewItems = true)
- {
- var newItemPropertyPaths = new List<string>();
-
- // Split buffer into transmissions and then into transmission blocks. Each transmission is an item subtree
- // meant to be pasted as a whole and each transmission block is a single chunk of serialized data.
- foreach (var transmission in copyBufferString.Substring(k_CopyPasteMarker.Length)
- .Split(new[] {k_EndOfTransmission}, StringSplitOptions.RemoveEmptyEntries))
- {
- foreach (var location in locations)
- PasteBlocks(transmission, location, assignNewIDs, newItemPropertyPaths);
- }
-
- OnSerializedObjectModified();
-
- // If instructed to do so, go and select all newly added items.
- if (selectNewItems && newItemPropertyPaths.Count > 0)
- {
- // We may have pasted into a different tree view. Only select the items if we can find them in
- // our current tree view.
- var newItems = newItemPropertyPaths.Select(FindItemByPropertyPath).Where(x => x != null);
- if (newItems.Any())
- SelectItems(newItems);
- }
- }
-
- private const string k_ActionMapTag = k_StartOfHeading + "ActionMapTreeItem" + k_StartOfText;
- private const string k_ActionTag = k_StartOfHeading + "ActionTreeItem" + k_StartOfText;
- private const string k_BindingTag = k_StartOfHeading + "BindingTreeItem" + k_StartOfText;
- private const string k_CompositeBindingTag = k_StartOfHeading + "CompositeBindingTreeItem" + k_StartOfText;
- private const string k_PartOfCompositeBindingTag = k_StartOfHeading + "PartOfCompositeBindingTreeItem" + k_StartOfText;
-
- private void PasteBlocks(string transmission, InsertLocation location, bool assignNewIDs, List<string> newItemPropertyPaths)
- {
- Debug.Assert(location.item != null, "Should have drop target");
-
- var blocks = transmission.Split(new[] {k_EndOfTransmissionBlock},
- StringSplitOptions.RemoveEmptyEntries);
- if (blocks.Length < 1)
- return;
-
- Type CopyTagToType(string tagName)
- {
- switch (tagName)
- {
- case k_ActionMapTag: return typeof(ActionMapTreeItem);
- case k_ActionTag: return typeof(ActionTreeItem);
- case k_BindingTag: return typeof(BindingTreeItem);
- case k_CompositeBindingTag: return typeof(CompositeBindingTreeItem);
- case k_PartOfCompositeBindingTag: return typeof(PartOfCompositeBindingTreeItem);
- default:
- throw new Exception($"Unrecognized copy block tag '{tagName}'");
- }
- }
-
- SplitTagAndData(blocks[0], out var tag, out var data);
-
- // Determine where to drop the item.
- SerializedProperty array = null;
- var arrayIndex = -1;
- var itemType = CopyTagToType(tag);
- if (location.item is ActionTreeItemBase dropTarget)
- {
- if (!dropTarget.GetDropLocation(itemType, location.childIndex, ref array, ref arrayIndex))
- return;
- }
- else if (tag == k_ActionMapTag)
- {
- // Paste into InputActionAsset.
- array = serializedObject.FindProperty("m_ActionMaps");
- arrayIndex = location.childIndex ?? array.arraySize;
- }
- else
- {
- throw new InvalidOperationException($"Cannot paste {tag} into {location.item.displayName}");
- }
-
- // If not given a specific index, we paste onto the end of the array.
- if (arrayIndex == -1 || arrayIndex > array.arraySize)
- arrayIndex = array.arraySize;
-
- // Determine action to assign to pasted bindings.
- string actionForNewBindings = null;
- if (location.item is ActionTreeItem actionItem)
- actionForNewBindings = actionItem.name;
- else if (location.item is BindingTreeItem bindingItem)
- actionForNewBindings = bindingItem.action;
-
- // Paste new element.
- var newElement = PasteBlock(tag, data, array, arrayIndex, assignNewIDs, actionForNewBindings);
- newItemPropertyPaths.Add(newElement.propertyPath);
-
- // If the element can have children, read whatever blocks are following the current one (if any).
- if ((tag == k_ActionTag || tag == k_CompositeBindingTag) && blocks.Length > 1)
- {
- var bindingArray = array;
-
- if (tag == k_ActionTag)
- {
- // We don't support pasting actions separately into action maps in the same paste operations so
- // there must be an ActionMapTreeItem in the hierarchy we pasted into.
- var actionMapItem = location.item.TryFindItemInHierarchy<ActionMapTreeItem>();
- Debug.Assert(actionMapItem != null, "Cannot find ActionMapTreeItem in hierarchy of pasted action");
- bindingArray = actionMapItem.bindingsArrayProperty;
- actionForNewBindings = InputActionSerializationHelpers.GetName(newElement);
- }
-
- for (var i = 1; i < blocks.Length; ++i)
- {
- SplitTagAndData(blocks[i], out var blockTag, out var blockData);
-
- PasteBlock(blockTag, blockData, bindingArray,
- tag == k_CompositeBindingTag ? arrayIndex + i : -1,
- assignNewIDs,
- actionForNewBindings);
- }
- }
- }
-
- private static void SplitTagAndData(string block, out string tag, out string data)
- {
- var indexOfStartOfTextChar = block.IndexOf(k_StartOfText);
- if (indexOfStartOfTextChar == -1)
- throw new ArgumentException($"Incorrect copy data format: Expecting '{k_StartOfText}' in '{block}'",
- nameof(block));
-
- tag = block.Substring(0, indexOfStartOfTextChar + 1);
- data = block.Substring(indexOfStartOfTextChar + 1);
- }
-
- public static SerializedProperty AddElement(SerializedProperty arrayProperty, string name, int index = -1)
- {
- var uniqueName = InputActionSerializationHelpers.FindUniqueName(arrayProperty, name);
- if (index < 0)
- index = arrayProperty.arraySize;
-
- arrayProperty.InsertArrayElementAtIndex(index);
- var elementProperty = arrayProperty.GetArrayElementAtIndex(index);
- elementProperty.ResetValuesToDefault();
-
- elementProperty.FindPropertyRelative("m_Name").stringValue = uniqueName;
- elementProperty.FindPropertyRelative("m_Id").stringValue = Guid.NewGuid().ToString();
-
- return elementProperty;
- }
-
- private SerializedProperty PasteBlock(string tag, string data, SerializedProperty array, int arrayIndex,
- bool assignNewIDs, string actionForNewBindings = null)
- {
- // Add an element to the array. Then read the serialized properties stored in the copy data
- // back into the element.
- var property = AddElement(array, "tempName", arrayIndex);
- property.RestoreFromJson(data);
- if (tag == k_ActionTag || tag == k_ActionMapTag)
- InputActionSerializationHelpers.EnsureUniqueName(property);
- if (assignNewIDs)
- {
- // Assign new IDs to the element as well as to any elements it contains. This means
- // that for action maps, we will also assign new IDs to every action and binding in the map.
- InputActionSerializationHelpers.AssignUniqueIDs(property);
- }
-
- // If the element is a binding, update its action target and binding group, if necessary.
- if (tag == k_BindingTag || tag == k_CompositeBindingTag || tag == k_PartOfCompositeBindingTag)
- {
- ////TODO: use {id} rather than plain name
- // Update action to refer to given action.
- InputActionSerializationHelpers.ChangeBinding(property, action: actionForNewBindings);
-
- // If we have a binding group to set for new bindings, overwrite the binding's
- // group with it.
- if (!string.IsNullOrEmpty(bindingGroupForNewBindings) && tag != k_CompositeBindingTag)
- {
- InputActionSerializationHelpers.ChangeBinding(property,
- groups: bindingGroupForNewBindings);
- }
-
- onBindingAdded?.Invoke(property);
- }
-
- return property;
- }
-
- #endregion
-
- #region Context Menus
-
- public void BuildContextMenuFor(Type itemType, GenericMenu menu, bool multiSelect, ActionTreeItem actionItem = null, bool noSelection = false)
- {
- var canRename = false;
- if (itemType == typeof(ActionMapTreeItem))
- {
- menu.AddItem(s_AddActionLabel, false, AddNewAction);
- }
- else if (itemType == typeof(ActionTreeItem))
- {
- canRename = true;
- BuildMenuToAddBindings(menu, actionItem);
- }
- else if (itemType == typeof(CompositeBindingTreeItem))
- {
- canRename = true;
- }
- else if (itemType == typeof(ActionMapListItem))
- {
- menu.AddItem(s_AddActionMapLabel, false, AddNewActionMap);
- }
-
- // Common menu entries shared by all types of items.
- menu.AddSeparator("");
- if (noSelection)
- {
- menu.AddDisabledItem(s_CutLabel);
- menu.AddDisabledItem(s_CopyLabel);
- }
- else
- {
- menu.AddItem(s_CutLabel, false, () =>
- {
- CopySelectedItemsToClipboard();
- DeleteDataOfSelectedItems();
- });
- menu.AddItem(s_CopyLabel, false, CopySelectedItemsToClipboard);
- }
- if (HavePastableClipboardData())
- menu.AddItem(s_PasteLabel, false, PasteDataFromClipboard);
- else
- menu.AddDisabledItem(s_PasteLabel);
- menu.AddSeparator("");
- if (!noSelection && canRename && !multiSelect)
- menu.AddItem(s_RenameLabel, false, () => BeginRename(GetSelectedItems().First()));
- else if (canRename)
- menu.AddDisabledItem(s_RenameLabel);
- if (noSelection)
- {
- menu.AddDisabledItem(s_DuplicateLabel);
- menu.AddDisabledItem(s_DeleteLabel);
- }
- else
- {
- menu.AddItem(s_DuplicateLabel, false, DuplicateSelection);
- menu.AddItem(s_DeleteLabel, false, DeleteDataOfSelectedItems);
- }
-
- if (itemType != typeof(ActionMapTreeItem))
- {
- menu.AddSeparator("");
- menu.AddItem(s_ExpandAllLabel, false, ExpandAll);
- menu.AddItem(s_CollapseAllLabel, false, CollapseAll);
- }
- }
-
- public void BuildMenuToAddBindings(GenericMenu menu, ActionTreeItem actionItem = null)
- {
- // Add entry to add "normal" bindings.
- menu.AddItem(s_AddBindingLabel, false,
- () =>
- {
- if (actionItem != null)
- AddNewBinding(actionItem.property, actionItem.actionMapProperty);
- else
- AddNewBinding();
- });
-
- // Add one entry for each registered type of composite binding.
- var expectedControlLayout = new InternedString(actionItem?.expectedControlLayout);
- foreach (var compositeName in InputBindingComposite.s_Composites.internedNames.Where(x =>
- !InputBindingComposite.s_Composites.aliases.Contains(x)).OrderBy(x => x))
- {
- // Skip composites we should hide from the UI.
- var compositeType = InputBindingComposite.s_Composites.LookupTypeRegistration(compositeName);
- var designTimeVisible = compositeType.GetCustomAttribute<DesignTimeVisibleAttribute>();
- if (designTimeVisible != null && !designTimeVisible.Visible)
- continue;
-
- // If the action is expected a specific control layout, check
- // whether the value type use by the composite matches that of
- // the layout.
- if (!expectedControlLayout.IsEmpty())
- {
- var valueType = InputBindingComposite.GetValueType(compositeName);
- if (valueType != null &&
- !InputControlLayout.s_Layouts.ValueTypeIsAssignableFrom(expectedControlLayout, valueType))
- continue;
- }
-
- var displayName = compositeType.GetCustomAttribute<DisplayNameAttribute>();
- var niceName = displayName != null ? displayName.DisplayName.Replace('/', '\\') : ObjectNames.NicifyVariableName(compositeName) + " Composite";
- menu.AddItem(new GUIContent($"Add {niceName}"), false,
- () =>
- {
- if (actionItem != null)
- AddNewComposite(actionItem.property, actionItem.actionMapProperty, compositeName);
- else
- AddNewComposite(compositeName);
- });
- }
- }
-
- private void PopUpContextMenu()
- {
- // See if we have a selection of mixed types.
- var selected = GetSelectedItems().ToList();
- var mixedSelection = selected.Select(x => x.GetType()).Distinct().Count() > 1;
- var noSelection = selected.Count == 0;
-
- // Create and pop up context menu.
- var menu = new GenericMenu();
- if (noSelection)
- {
- BuildContextMenuFor(rootItem.GetType(), menu, true, noSelection: noSelection);
- }
- else if (mixedSelection)
- {
- BuildContextMenuFor(typeof(ActionTreeItemBase), menu, true, noSelection: noSelection);
- }
- else
- {
- var item = selected.First();
- BuildContextMenuFor(item.GetType(), menu, GetSelection().Count > 1, actionItem: item as ActionTreeItem);
- }
- menu.ShowAsContext();
- }
-
- protected override void ContextClickedItem(int id)
- {
- // When right-clicking an unselected item, TreeView does change the selection to the
- // clicked item but the visual feedback only comes in the *next* repaint. This means that
- // if we pop up a context menu right away here, the user does not correctly see which item
- // is affected.
- //
- // So, instead we force a repaint and open the context menu on the next OnGUI() call. Note
- // that we can't use something like EditorApplication.delayCall here as ShowAsContext()
- // can only be called from UI callbacks (otherwise it will simply be ignored).
-
- m_InitiateContextMenuOnNextRepaint = true;
- Repaint();
-
- Event.current.Use();
- }
-
- protected override void ContextClicked()
- {
- ClearSelection();
- m_InitiateContextMenuOnNextRepaint = true;
- Repaint();
-
- Event.current.Use();
- }
-
- #endregion
-
- #region Add New Items
-
- /// <summary>
- /// Add a new action map to the toplevel <see cref="InputActionAsset"/>.
- /// </summary>
- public void AddNewActionMap()
- {
- var actionMapProperty = InputActionSerializationHelpers.AddActionMap(serializedObject);
- var actionProperty = InputActionSerializationHelpers.AddAction(actionMapProperty);
- InputActionSerializationHelpers.AddBinding(actionProperty, actionMapProperty, groups: bindingGroupForNewBindings);
- OnNewItemAdded(actionMapProperty);
- }
-
- /// <summary>
- /// Add new action to the currently active action map(s).
- /// </summary>
- public void AddNewAction()
- {
- foreach (var actionMapItem in GetSelectedItemsOrParentsOfType<ActionMapTreeItem>())
- AddNewAction(actionMapItem.property);
- }
-
- public void AddNewAction(SerializedProperty actionMapProperty)
- {
- if (onHandleAddNewAction != null)
- onHandleAddNewAction(actionMapProperty);
- else
- {
- var actionProperty = InputActionSerializationHelpers.AddAction(actionMapProperty);
- InputActionSerializationHelpers.AddBinding(actionProperty, actionMapProperty, groups: bindingGroupForNewBindings);
- OnNewItemAdded(actionProperty);
- }
- }
-
- public void AddNewBinding()
- {
- foreach (var actionItem in GetSelectedItemsOrParentsOfType<ActionTreeItem>())
- AddNewBinding(actionItem.property, actionItem.actionMapProperty);
- }
-
- public void AddNewBinding(SerializedProperty actionProperty, SerializedProperty actionMapProperty)
- {
- var bindingProperty = InputActionSerializationHelpers.AddBinding(actionProperty, actionMapProperty,
- groups: bindingGroupForNewBindings);
- onBindingAdded?.Invoke(bindingProperty);
- OnNewItemAdded(bindingProperty);
- }
-
- public void AddNewComposite(string compositeType)
- {
- foreach (var actionItem in GetSelectedItemsOrParentsOfType<ActionTreeItem>())
- AddNewComposite(actionItem.property, actionItem.actionMapProperty, compositeType);
- }
-
- public void AddNewComposite(SerializedProperty actionProperty, SerializedProperty actionMapProperty, string compositeName)
- {
- var compositeType = InputBindingComposite.s_Composites.LookupTypeRegistration(compositeName);
- if (compositeType == null)
- throw new ArgumentException($"Cannot find composite registration for {compositeName}",
- nameof(compositeName));
- var compositeProperty = InputActionSerializationHelpers.AddCompositeBinding(actionProperty,
- actionMapProperty, compositeName, compositeType, groups: bindingGroupForNewBindings);
- onBindingAdded?.Invoke(compositeProperty);
- OnNewItemAdded(compositeProperty);
- }
-
- private void OnNewItemAdded(SerializedProperty property)
- {
- OnSerializedObjectModified();
- SelectItemAndBeginRename(property);
- }
-
- private void SelectItemAndBeginRename(SerializedProperty property)
- {
- var item = FindItemFor(property);
- if (item == null)
- {
- // if we could not find the item, try clearing search filters.
- ClearItemSearchFilterAndReload();
- item = FindItemFor(property);
- }
- Debug.Assert(item != null, $"Cannot find newly created item for {property.propertyPath}");
- SetExpandedRecursive(item.id, true);
- SelectItem(item);
- SetFocus();
- FrameItem(item.id);
- if (item.canRename)
- BeginRename(item);
- }
-
- #endregion
-
- #region Drawing
-
- public override void OnGUI(Rect rect)
- {
- if (m_InitiateContextMenuOnNextRepaint)
- {
- m_InitiateContextMenuOnNextRepaint = false;
- PopUpContextMenu();
- }
-
- if (ReloadIfSerializedObjectHasBeenChanged())
- return;
-
- // Draw border rect.
- EditorGUI.LabelField(rect, GUIContent.none, Styles.backgroundWithBorder);
- rect.x += 1;
- rect.y += 1;
- rect.height -= 1;
- rect.width -= 2;
-
- if (drawHeader)
- DrawHeader(ref rect);
-
- base.OnGUI(rect);
-
- HandleCopyPasteCommandEvent(Event.current);
- }
-
- private void DrawHeader(ref Rect rect)
- {
- var headerRect = rect;
- headerRect.height = EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
-
- rect.y += headerRect.height;
- rect.height -= headerRect.height;
-
- // Draw label.
- EditorGUI.LabelField(headerRect, m_Title, Styles.columnHeaderLabel);
-
- // Draw minus button.
- var buttonRect = headerRect;
- buttonRect.width = EditorGUIUtility.singleLineHeight;
- buttonRect.x += rect.width - buttonRect.width - EditorGUIUtility.standardVerticalSpacing;
- if (drawMinusButton)
- {
- var minusButtonDisabled = !HasSelection();
- using (new EditorGUI.DisabledScope(minusButtonDisabled))
- {
- if (GUI.Button(buttonRect, minusIcon, GUIStyle.none))
- DeleteDataOfSelectedItems();
- }
-
- buttonRect.x -= buttonRect.width + EditorGUIUtility.standardVerticalSpacing;
- }
-
- // Draw plus button.
- if (drawPlusButton)
- {
- var plusIconDisabled = onBuildTree == null;
- using (new EditorGUI.DisabledScope(plusIconDisabled))
- {
- if (GUI.Button(buttonRect, plusIcon, GUIStyle.none))
- {
- if (rootItem is ActionMapTreeItem mapItem)
- {
- AddNewAction(mapItem.property);
- }
- else if (rootItem is ActionTreeItem actionItem)
- {
- // Adding a composite has multiple options. Pop up a menu.
- var menu = new GenericMenu();
- BuildMenuToAddBindings(menu, actionItem);
- menu.ShowAsContext();
- }
- else
- {
- AddNewActionMap();
- }
- }
-
- buttonRect.x -= buttonRect.width + EditorGUIUtility.standardVerticalSpacing;
- }
- }
-
- // Draw action properties button.
- if (drawActionPropertiesButton && rootItem is ActionTreeItem item)
- {
- if (GUI.Button(buttonRect, s_ActionPropertiesIcon, GUIStyle.none))
- onDoubleClick?.Invoke(item);
- }
- }
-
- // For each item, we draw
- // 1) color tag
- // 2) foldout
- // 3) display name
- // 4) Line underneath item
-
- private const int kColorTagWidth = 6;
- private const int kFoldoutWidth = 15;
-
- ////FIXME: foldout hover region is way too large; partly overlaps the text of items
- private bool DrawFoldout(Rect position, bool expandedState, GUIStyle style)
- {
- // We don't get the depth of the item we're drawing the foldout for but we can
- // infer it by the amount that the given rectangle was indented.
- var indent = (int)(position.x / kFoldoutWidth);
- var indentLevel = EditorGUI.indentLevel;
-
- // When drawing input actions in the input actions editor, we don't want to offset the foldout
- // icon any further than the position that's passed in to this function, so take advantage of
- // the fact that indentLevel is always zero in that editor.
- position.x = EditorGUI.IndentedRect(position).x * Mathf.Clamp01(indentLevel) + kColorTagWidth + 2 + indent * kColorTagWidth;
-
- position.width = kFoldoutWidth;
-
- var hierarchyMode = EditorGUIUtility.hierarchyMode;
-
- // We remove the editor indent level and set hierarchy mode to false when drawing the foldout
- // arrow so that in the inspector we don't get additional padding on the arrow for the inspector
- // gutter, and so that the indent level doesn't apply because we've done that ourselves.
- EditorGUI.indentLevel = 0;
- EditorGUIUtility.hierarchyMode = false;
-
- var foldoutExpanded = EditorGUI.Foldout(position, expandedState, GUIContent.none, true, style);
-
- EditorGUI.indentLevel = indentLevel;
- EditorGUIUtility.hierarchyMode = hierarchyMode;
-
- return foldoutExpanded;
- }
-
- protected override void RowGUI(RowGUIArgs args)
- {
- var item = (ActionTreeItemBase)args.item;
- var isRepaint = Event.current.type == EventType.Repaint;
-
- // Color tag at beginning of line.
- var colorTagRect = EditorGUI.IndentedRect(args.rowRect);
- colorTagRect.x += item.depth * kColorTagWidth;
- colorTagRect.width = kColorTagWidth;
- if (isRepaint)
- item.colorTagStyle.Draw(colorTagRect, GUIContent.none, false, false, false, false);
-
- // Text.
- // NOTE: When renaming, the renaming overlay gets drawn outside of our control so don't draw the label in that case
- // as otherwise it will peak out from underneath the overlay.
- if (!args.isRenaming && isRepaint)
- {
- var text = item.displayName;
- var textRect = GetTextRect(args.rowRect, item);
-
- var style = args.selected ? Styles.selectedText : Styles.text;
-
- if (item.showWarningIcon)
- {
- var content = new GUIContent(text, EditorGUIUtility.FindTexture("console.warnicon.sml"));
- style.Draw(textRect, content, false, false, args.selected, args.focused);
- }
- else
- style.Draw(textRect, text, false, false, args.selected, args.focused);
- }
-
- // Bottom line.
- var lineRect = EditorGUI.IndentedRect(args.rowRect);
- lineRect.y += lineRect.height - 1;
- lineRect.height = 1;
- if (isRepaint)
- Styles.border.Draw(lineRect, GUIContent.none, false, false, false, false);
-
- // For action items, add a dropdown menu to add bindings.
- if (item is ActionTreeItem actionItem)
- {
- var buttonRect = args.rowRect;
- buttonRect.x = buttonRect.width - (EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing);
- buttonRect.width = EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
- if (GUI.Button(buttonRect, s_PlusBindingIcon, GUIStyle.none))
- {
- var menu = new GenericMenu();
- BuildMenuToAddBindings(menu, actionItem);
- menu.ShowAsContext();
- }
- }
- }
-
- protected override float GetCustomRowHeight(int row, TreeViewItem item)
- {
- return 18;
- }
-
- protected override Rect GetRenameRect(Rect rowRect, int row, TreeViewItem item)
- {
- var textRect = GetTextRect(rowRect, item, false);
- textRect.x += 2;
- textRect.height -= 2;
- return textRect;
- }
-
- private Rect GetTextRect(Rect rowRect, TreeViewItem item, bool applyIndent = true)
- {
- var indent = (item.depth + 1) * kColorTagWidth + kFoldoutWidth;
- var textRect = applyIndent ? EditorGUI.IndentedRect(rowRect) : rowRect;
- textRect.x += indent;
- return textRect;
- }
-
- #endregion
-
- // Undo is a problem. When an undo or redo is performed, the SerializedObject may change behind
- // our backs which means that the information shown in the tree may be outdated now.
- //
- // We do have the Undo.undoRedoPerformed global callback but because PropertyDrawers
- // have no observable life cycle, we cannot always easily hook into the callback and force a reload
- // of the tree. Also, while returning false from PropertyDrawer.CanCacheInspectorGUI() might one make suspect that
- // a PropertyDrawer would automatically be thrown away and recreated if the SerializedObject
- // is modified by undo, that does not happen in practice.
- //
- // We could just Reload() the tree all the time but TreeView.Reload() itself forces a repaint and
- // this will thus easily lead to infinite repaints.
- //
- // So, what we do is make use of the built-in dirty count we can get for Unity objects. If the count
- // changes and it wasn't caused by us, we reload the tree. Means we still reload unnecessarily if
- // some other property on a component changes but at least we don't reload all the time.
- //
- // A positive side-effect is that we will catch *any* change to the SerializedObject, not just
- // undo/redo and we can do so without having to hook into Undo.undoRedoPerformed anywhere.
-
- private void OnSerializedObjectModified()
- {
- serializedObject.ApplyModifiedProperties();
- UpdateSerializedObjectDirtyCount();
- Reload();
- onSerializedObjectModified?.Invoke();
- }
-
- public void UpdateSerializedObjectDirtyCount()
- {
- m_SerializedObjectDirtyCount = serializedObject != null ? EditorUtility.GetDirtyCount(serializedObject.targetObject) : 0;
- }
-
- private bool ReloadIfSerializedObjectHasBeenChanged()
- {
- var oldCount = m_SerializedObjectDirtyCount;
- UpdateSerializedObjectDirtyCount();
- if (oldCount != m_SerializedObjectDirtyCount)
- {
- Reload();
- onSerializedObjectModified?.Invoke();
- return true;
- }
- return false;
- }
-
- public SerializedObject serializedObject { get; }
- public string bindingGroupForNewBindings { get; set; }
- public new TreeViewItem rootItem => base.rootItem;
-
- public Action onSerializedObjectModified { get; set; }
- public Action onSelectionChanged { get; set; }
- public Action<ActionTreeItemBase> onDoubleClick { get; set; }
- public Action<ActionTreeItemBase> onBeginRename { get; set; }
- public Func<TreeViewItem> onBuildTree { get; set; }
- public Action<SerializedProperty> onBindingAdded { get; set; }
-
- public bool drawHeader { get; set; }
- public bool drawPlusButton { get; set; }
- public bool drawMinusButton { get; set; }
- public bool drawActionPropertiesButton { get; set; }
-
- public Action<SerializedProperty> onHandleAddNewAction { get; set; }
-
- public (string, string) title
- {
- get => (m_Title?.text, m_Title?.tooltip);
- set => m_Title = new GUIContent(value.Item1, value.Item2);
- }
-
- public new float totalHeight
- {
- get
- {
- var height = base.totalHeight;
- if (drawHeader)
- height += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
- height += 1; // Border.
- return height;
- }
- }
-
- public ActionTreeItemBase this[string path]
- {
- get
- {
- var item = FindItemByPath(path);
- if (item == null)
- throw new KeyNotFoundException(path);
- return item;
- }
- }
-
- private GUIContent plusIcon
- {
- get
- {
- if (rootItem is ActionMapTreeItem)
- return s_PlusActionIcon;
- if (rootItem is ActionTreeItem)
- return s_PlusBindingIcon;
- return s_PlusActionMapIcon;
- }
- }
-
- private GUIContent minusIcon => s_DeleteSectionIcon;
-
- private FilterCriterion[] m_ItemFilterCriteria;
- private GUIContent m_Title;
- private bool m_InitiateContextMenuOnNextRepaint;
- private bool m_ForceAcceptRename;
- private int m_SerializedObjectDirtyCount;
-
- private static readonly GUIContent s_AddBindingLabel = EditorGUIUtility.TrTextContent("Add Binding");
- private static readonly GUIContent s_AddActionLabel = EditorGUIUtility.TrTextContent("Add Action");
- private static readonly GUIContent s_AddActionMapLabel = EditorGUIUtility.TrTextContent("Add Action Map");
- private static readonly GUIContent s_PlusBindingIcon = EditorGUIUtility.TrIconContent("Toolbar Plus More", "Add Binding");
- private static readonly GUIContent s_PlusActionIcon = EditorGUIUtility.TrIconContent("Toolbar Plus", "Add Action");
- private static readonly GUIContent s_PlusActionMapIcon = EditorGUIUtility.TrIconContent("Toolbar Plus", "Add Action Map");
- private static readonly GUIContent s_DeleteSectionIcon = EditorGUIUtility.TrIconContent("Toolbar Minus", "Delete Selection");
- private static readonly GUIContent s_ActionPropertiesIcon = EditorGUIUtility.TrIconContent("Settings", "Action Properties");
-
- private static readonly GUIContent s_CutLabel = EditorGUIUtility.TrTextContent("Cut");
- private static readonly GUIContent s_CopyLabel = EditorGUIUtility.TrTextContent("Copy");
- private static readonly GUIContent s_PasteLabel = EditorGUIUtility.TrTextContent("Paste");
- private static readonly GUIContent s_DeleteLabel = EditorGUIUtility.TrTextContent("Delete");
- private static readonly GUIContent s_DuplicateLabel = EditorGUIUtility.TrTextContent("Duplicate");
- private static readonly GUIContent s_RenameLabel = EditorGUIUtility.TrTextContent("Rename");
- private static readonly GUIContent s_ExpandAllLabel = EditorGUIUtility.TrTextContent("Expand All");
- private static readonly GUIContent s_CollapseAllLabel = EditorGUIUtility.TrTextContent("Collapse All");
-
- public static string SharedResourcesPath = "Packages/com.unity.inputsystem/InputSystem/Editor/AssetEditor/Resources/";
- public static string ResourcesPath
- {
- get
- {
- if (EditorGUIUtility.isProSkin)
- return SharedResourcesPath + "pro/";
- return SharedResourcesPath + "personal/";
- }
- }
-
- public struct FilterCriterion
- {
- public enum Type
- {
- ByName,
- ByBindingGroup,
- ByDeviceLayout,
- }
-
- public enum Match
- {
- Success,
- Failure,
- None,
- }
-
- public string text;
- public Type type;
-
- public static string k_BindingGroupTag = "g:";
- public static string k_DeviceLayoutTag = "d:";
-
- public Match Matches(ActionTreeItemBase item)
- {
- Debug.Assert(item != null, "Item cannot be null");
-
- switch (type)
- {
- case Type.ByName:
- {
- // NOTE: Composite items have names (and part bindings in a way, too) but we don't filter on them.
- if (item is ActionMapTreeItem || item is ActionTreeItem)
- {
- var matchesSelf = item.displayName.Contains(text, StringComparison.InvariantCultureIgnoreCase);
-
- // Name filters behave recursively. I.e. if any item in the subtree is matched by the name filter,
- // the item is included.
- if (!matchesSelf && CheckChildrenFor(Match.Success, item))
- return Match.Success;
-
- return matchesSelf ? Match.Success : Match.Failure;
- }
- break;
- }
-
- case Type.ByBindingGroup:
- {
- if (item is BindingTreeItem bindingItem)
- {
- // For composites, succeed the match if any children match.
- if (item is CompositeBindingTreeItem)
- return CheckChildrenFor(Match.Success, item) ? Match.Success : Match.Failure;
-
- // Items that are in no binding group match any binding group.
- if (string.IsNullOrEmpty(bindingItem.groups))
- return Match.Success;
-
- var groups = bindingItem.groups.Split(InputBinding.Separator);
- var bindingGroup = text;
- return groups.Any(x => x.Equals(bindingGroup, StringComparison.InvariantCultureIgnoreCase))
- ? Match.Success
- : Match.Failure;
- }
- break;
- }
-
- case Type.ByDeviceLayout:
- {
- if (item is BindingTreeItem bindingItem)
- {
- // For composites, succeed the match if any children match.
- if (item is CompositeBindingTreeItem)
- return CheckChildrenFor(Match.Success, item) ? Match.Success : Match.Failure;
-
- var deviceLayout = InputControlPath.TryGetDeviceLayout(bindingItem.path);
- return string.Equals(deviceLayout, text, StringComparison.InvariantCultureIgnoreCase)
- || InputControlLayout.s_Layouts.IsBasedOn(new InternedString(deviceLayout), new InternedString(text))
- ? Match.Success
- : Match.Failure;
- }
- break;
- }
- }
-
- return Match.None;
- }
-
- private bool CheckChildrenFor(Match match, ActionTreeItemBase item)
- {
- if (!item.hasChildren)
- return false;
-
- foreach (var child in item.children.OfType<ActionTreeItemBase>())
- if (Matches(child) == match)
- return true;
-
- return false;
- }
-
- public static FilterCriterion ByName(string name)
- {
- return new FilterCriterion {text = name, type = Type.ByName};
- }
-
- public static FilterCriterion ByBindingGroup(string group)
- {
- return new FilterCriterion {text = group, type = Type.ByBindingGroup};
- }
-
- public static FilterCriterion ByDeviceLayout(string layout)
- {
- return new FilterCriterion {text = layout, type = Type.ByDeviceLayout};
- }
-
- public static List<FilterCriterion> FromString(string criteria)
- {
- if (string.IsNullOrEmpty(criteria))
- return null;
-
- var list = new List<FilterCriterion>();
- foreach (var substring in criteria.Tokenize())
- {
- if (substring.StartsWith(k_DeviceLayoutTag))
- list.Add(ByDeviceLayout(substring.Substr(2).Unescape()));
- else if (substring.StartsWith(k_BindingGroupTag))
- list.Add(ByBindingGroup(substring.Substr(2).Unescape()));
- else
- list.Add(ByName(substring.ToString().Unescape()));
- }
-
- return list;
- }
-
- public static string ToString(IEnumerable<FilterCriterion> criteria)
- {
- var builder = new StringBuilder();
- foreach (var criterion in criteria)
- {
- if (builder.Length > 0)
- builder.Append(' ');
-
- if (criterion.type == Type.ByBindingGroup)
- builder.Append(k_BindingGroupTag);
- else if (criterion.type == Type.ByDeviceLayout)
- builder.Append(k_DeviceLayoutTag);
-
- builder.Append(criterion.text);
- }
- return builder.ToString();
- }
- }
-
- public static class Styles
- {
- public static readonly GUIStyle text = new GUIStyle("Label").WithAlignment(TextAnchor.MiddleLeft);
- public static readonly GUIStyle selectedText = new GUIStyle("Label").WithAlignment(TextAnchor.MiddleLeft).WithNormalTextColor(Color.white);
- public static readonly GUIStyle backgroundWithoutBorder = new GUIStyle("Label")
- .WithNormalBackground(AssetDatabase.LoadAssetAtPath<Texture2D>(ResourcesPath + "actionTreeBackgroundWithoutBorder.png"));
- public static readonly GUIStyle border = new GUIStyle("Label")
- .WithNormalBackground(AssetDatabase.LoadAssetAtPath<Texture2D>(ResourcesPath + "actionTreeBackground.png"))
- .WithBorder(new RectOffset(0, 0, 0, 1));
- public static readonly GUIStyle backgroundWithBorder = new GUIStyle("Label")
- .WithNormalBackground(AssetDatabase.LoadAssetAtPath<Texture2D>(ResourcesPath + "actionTreeBackground.png"))
- .WithBorder(new RectOffset(3, 3, 3, 3))
- .WithMargin(new RectOffset(4, 4, 4, 4));
- public static readonly GUIStyle columnHeaderLabel = new GUIStyle(EditorStyles.toolbar)
- .WithAlignment(TextAnchor.MiddleLeft)
- .WithFontStyle(FontStyle.Bold)
- .WithPadding(new RectOffset(10, 6, 0, 0));
- }
-
- // Just so that we can tell apart TreeViews containing only maps.
- internal class ActionMapListItem : TreeViewItem
- {
- }
- }
- }
- #endif // UNITY_EDITOR
|