A game about forced loneliness, made by TACStudios
1// UITK TreeView is not supported in earlier versions
2// Therefore the UITK version of the InputActionAsset Editor is not available on earlier Editor versions either.
3#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
4using CmdEvents = UnityEngine.InputSystem.Editor.InputActionsEditorConstants.CommandEvents;
5using System;
6using System.Collections.Generic;
7using System.Linq;
8using UnityEditor;
9using UnityEngine.InputSystem.Layouts;
10using UnityEngine.InputSystem.Utilities;
11using UnityEngine.UIElements;
12
13namespace UnityEngine.InputSystem.Editor
14{
15 /// <summary>
16 /// A view for displaying the actions of the selected action map in a tree with bindings
17 /// as children.
18 /// </summary>
19 internal class ActionsTreeView : ViewBase<ActionsTreeView.ViewState>
20 {
21 private readonly ListView m_ActionMapsListView;
22 private readonly TreeView m_ActionsTreeView;
23 private readonly Button m_AddActionButton;
24 private readonly ScrollView m_PropertiesScrollview;
25
26 private bool m_RenameOnActionAdded;
27 private readonly CollectionViewSelectionChangeFilter m_ActionsTreeViewSelectionChangeFilter;
28
29 //save TreeView element id's of individual input actions and bindings to ensure saving of expanded state
30 private Dictionary<Guid, int> m_GuidToTreeViewId;
31
32 public ActionsTreeView(VisualElement root, StateContainer stateContainer)
33 : base(root, stateContainer)
34 {
35 m_ActionMapsListView = root.Q<ListView>("action-maps-list-view");
36 m_AddActionButton = root.Q<Button>("add-new-action-button");
37 m_PropertiesScrollview = root.Q<ScrollView>("properties-scrollview");
38 m_ActionsTreeView = root.Q<TreeView>("actions-tree-view");
39 //assign unique viewDataKey to store treeView states like expanded/collapsed items - make it unique to avoid conflicts with other TreeViews
40 m_ActionsTreeView.viewDataKey = $"InputActionTreeView_{stateContainer.assetGUID}";
41 m_GuidToTreeViewId = new Dictionary<Guid, int>();
42 m_ActionsTreeView.selectionType = UIElements.SelectionType.Single;
43 m_ActionsTreeView.makeItem = () => new InputActionsTreeViewItem();
44 m_ActionsTreeView.reorderable = true;
45 m_ActionsTreeView.bindItem = (e, i) =>
46 {
47 var item = m_ActionsTreeView.GetItemDataForIndex<ActionOrBindingData>(i);
48 e.Q<Label>("name").text = item.name;
49 var addBindingButton = e.Q<Button>("add-new-binding-button");
50 addBindingButton.AddToClassList(EditorGUIUtility.isProSkin ? "add-binging-button-dark-theme" : "add-binging-button");
51 var treeViewItem = (InputActionsTreeViewItem)e;
52 if (item.isComposite)
53 ContextMenu.GetContextMenuForCompositeItem(this, treeViewItem, i);
54 else if (item.isAction)
55 ContextMenu.GetContextMenuForActionItem(this, treeViewItem, item.controlLayout, i);
56 else
57 ContextMenu.GetContextMenuForBindingItem(this, treeViewItem, i);
58
59 if (item.isAction)
60 {
61 Action action = ContextMenu.GetContextMenuForActionAddItem(this, item.controlLayout, i);
62 addBindingButton.clicked += action;
63 addBindingButton.userData = action; // Store to use in unbindItem
64 addBindingButton.clickable.activators.Add(new ManipulatorActivationFilter(){button = MouseButton.RightMouse});
65 addBindingButton.style.display = DisplayStyle.Flex;
66 treeViewItem.EditTextFinishedCallback = newName =>
67 {
68 ChangeActionOrCompositName(item, newName);
69 };
70 treeViewItem.EditTextFinished += treeViewItem.EditTextFinishedCallback;
71 }
72 else
73 {
74 addBindingButton.style.display = DisplayStyle.None;
75 if (!item.isComposite)
76 treeViewItem.UnregisterInputField();
77 else
78 {
79 treeViewItem.EditTextFinishedCallback = newName =>
80 {
81 ChangeActionOrCompositName(item, newName);
82 };
83 treeViewItem.EditTextFinished += treeViewItem.EditTextFinishedCallback;
84 }
85 }
86
87 if (!string.IsNullOrEmpty(item.controlLayout))
88 e.Q<VisualElement>("icon").style.backgroundImage =
89 new StyleBackground(
90 EditorInputControlLayoutCache.GetIconForLayout(item.controlLayout));
91 else
92 e.Q<VisualElement>("icon").style.backgroundImage =
93 new StyleBackground(
94 EditorInputControlLayoutCache.GetIconForLayout("Control"));
95
96 e.SetEnabled(!item.isCut);
97 };
98
99 m_ActionsTreeView.itemsChosen += objects =>
100 {
101 var data = (ActionOrBindingData)objects.First();
102 if (!data.isAction && !data.isComposite)
103 return;
104 var item = m_ActionsTreeView.GetRootElementForIndex(m_ActionsTreeView.selectedIndex).Q<InputActionsTreeViewItem>();
105 item.FocusOnRenameTextField();
106 };
107
108 m_ActionsTreeView.unbindItem = (element, i) =>
109 {
110 var item = m_ActionsTreeView.GetItemDataForIndex<ActionOrBindingData>(i);
111 var treeViewItem = (InputActionsTreeViewItem)element;
112 //reset the editing variable before reassigning visual elements
113 if (item.isAction || item.isComposite)
114 treeViewItem.Reset();
115
116 if (item.isAction)
117 {
118 var button = element.Q<Button>("add-new-binding-button");
119 button.clicked -= button.userData as Action;
120 }
121
122 treeViewItem.EditTextFinished -= treeViewItem.EditTextFinishedCallback;
123 };
124
125 ContextMenu.GetContextMenuForActionListView(this, m_ActionsTreeView, m_ActionsTreeView.parent);
126 ContextMenu.GetContextMenuForActionsEmptySpace(this, m_ActionsTreeView, root.Q<VisualElement>("rclick-area-to-add-new-action"));
127 // Only bring up this context menu for the Tree when it's empty, so we can treat it like right-clicking the empty space:
128 ContextMenu.GetContextMenuForActionsEmptySpace(this, m_ActionsTreeView, m_ActionsTreeView, onlyShowIfTreeIsEmpty: true);
129
130 m_ActionsTreeViewSelectionChangeFilter = new CollectionViewSelectionChangeFilter(m_ActionsTreeView);
131 m_ActionsTreeViewSelectionChangeFilter.selectedIndicesChanged += (_) =>
132 {
133 if (m_ActionsTreeView.selectedIndex >= 0)
134 {
135 var item = m_ActionsTreeView.GetItemDataForIndex<ActionOrBindingData>(m_ActionsTreeView.selectedIndex);
136 Dispatch(item.isAction ? Commands.SelectAction(item.name) : Commands.SelectBinding(item.bindingIndex));
137 }
138 };
139
140 m_ActionsTreeView.RegisterCallback<ExecuteCommandEvent>(OnExecuteCommand);
141 m_ActionsTreeView.RegisterCallback<ValidateCommandEvent>(OnValidateCommand);
142 m_ActionsTreeView.RegisterCallback<PointerDownEvent>(OnPointerDown, TrickleDown.TrickleDown);
143 m_ActionsTreeView.RegisterCallback<DragPerformEvent>(OnDraggedItem);
144
145 // ISXB-748 - Scrolling the view causes a visual glitch with the rename TextField. As a work-around we
146 // need to cancel the rename operation in this scenario.
147 m_ActionsTreeView.RegisterCallback<WheelEvent>(e => InputActionsTreeViewItem.CancelRename(), TrickleDown.TrickleDown);
148
149 CreateSelector(Selectors.GetActionsForSelectedActionMap, Selectors.GetActionMapCount,
150 (_, count, state) =>
151 {
152 var treeData = Selectors.GetActionsAsTreeViewData(state, m_GuidToTreeViewId);
153 return new ViewState
154 {
155 treeViewData = treeData,
156 actionMapCount = count ?? 0,
157 newElementID = GetSelectedElementId(state, treeData)
158 };
159 });
160
161 m_AddActionButton.clicked += AddAction;
162 }
163
164 private int GetSelectedElementId(InputActionsEditorState state, List<TreeViewItemData<ActionOrBindingData>> treeData)
165 {
166 var id = -1;
167 if (state.selectionType == SelectionType.Action)
168 {
169 if (treeData.Count > state.selectedActionIndex && state.selectedActionIndex >= 0)
170 id = treeData[state.selectedActionIndex].id;
171 }
172 else if (state.selectionType == SelectionType.Binding)
173 id = GetComponentOrBindingID(treeData, state.selectedBindingIndex);
174 return id;
175 }
176
177 private int GetComponentOrBindingID(List<TreeViewItemData<ActionOrBindingData>> treeItemList, int selectedBindingIndex)
178 {
179 foreach (var actionItem in treeItemList)
180 {
181 // Look for the element ID by checking if the selected binding index matches the binding index of
182 // the ActionOrBindingData of the item. Deals with composite bindings as well.
183 foreach (var bindingOrComponentItem in actionItem.children)
184 {
185 if (bindingOrComponentItem.data.bindingIndex == selectedBindingIndex)
186 return bindingOrComponentItem.id;
187 if (bindingOrComponentItem.hasChildren)
188 {
189 foreach (var bindingItem in bindingOrComponentItem.children)
190 {
191 if (bindingOrComponentItem.data.bindingIndex == selectedBindingIndex)
192 return bindingItem.id;
193 }
194 }
195 }
196 }
197 return -1;
198 }
199
200 public override void DestroyView()
201 {
202 m_AddActionButton.clicked -= AddAction;
203 }
204
205 public override void RedrawUI(ViewState viewState)
206 {
207 m_ActionsTreeView.Clear();
208 m_ActionsTreeView.SetRootItems(viewState.treeViewData);
209 m_ActionsTreeView.Rebuild();
210 if (viewState.newElementID != -1)
211 {
212 m_ActionsTreeView.SetSelectionById(viewState.newElementID);
213 m_ActionsTreeView.ScrollToItemById(viewState.newElementID);
214 }
215 RenameNewAction(viewState.newElementID);;
216 m_AddActionButton.SetEnabled(viewState.actionMapCount > 0);
217
218 // Don't want to show action properties if there's no actions.
219 m_PropertiesScrollview.visible = m_ActionsTreeView.GetTreeCount() > 0;
220 }
221
222 private void OnDraggedItem(DragPerformEvent evt)
223 {
224 bool discardDrag = false;
225 foreach (var index in m_ActionsTreeView.selectedIndices)
226 {
227 // currentTarget & target are always in TreeView as the event is registered on the TreeView - we need to discard drags into other parts of the editor (e.g. the maps list view)
228 var treeView = m_ActionsTreeView.panel.Pick(evt.mousePosition)?.GetFirstAncestorOfType<TreeView>();
229 if (treeView is null || treeView != m_ActionsTreeView)
230 {
231 discardDrag = true;
232 break;
233 }
234 var draggedItemData = m_ActionsTreeView.GetItemDataForIndex<ActionOrBindingData>(index);
235 var itemID = m_ActionsTreeView.GetIdForIndex(index);
236 var childIndex = m_ActionsTreeView.viewController.GetChildIndexForId(itemID);
237 var parentId = m_ActionsTreeView.viewController.GetParentId(itemID);
238 ActionOrBindingData? directParent = parentId == -1 ? null : m_ActionsTreeView.GetItemDataForIndex<ActionOrBindingData>(m_ActionsTreeView.viewController.GetIndexForId(parentId));
239 if (draggedItemData.isAction)
240 {
241 if (!MoveAction(directParent, draggedItemData, childIndex))
242 {
243 discardDrag = true;
244 break;
245 }
246 }
247 else if (!draggedItemData.isPartOfComposite)
248 {
249 if (!MoveBindingOrComposite(directParent, draggedItemData, childIndex))
250 {
251 discardDrag = true;
252 break;
253 }
254 }
255 else if (!MoveCompositeParts(directParent, childIndex, draggedItemData))
256 {
257 discardDrag = true;
258 break;
259 }
260 }
261
262 if (!discardDrag) return;
263 var selectedItem = m_ActionsTreeView.GetItemDataForIndex<ActionOrBindingData>(m_ActionsTreeView.selectedIndices.First());
264 Dispatch(selectedItem.isAction
265 ? Commands.SelectAction(selectedItem.name)
266 : Commands.SelectBinding(selectedItem.bindingIndex));
267 //TODO find a better way to reject the drag (for better visual feedback & to not run an extra command)
268 }
269
270 private bool MoveAction(ActionOrBindingData? directParent, ActionOrBindingData draggedItemData, int childIndex)
271 {
272 if (directParent != null)
273 return false;
274 Dispatch(Commands.MoveAction(draggedItemData.actionIndex, childIndex));
275 return true;
276 }
277
278 private bool MoveBindingOrComposite(ActionOrBindingData? directParent, ActionOrBindingData draggedItemData, int childIndex)
279 {
280 if (directParent == null || !directParent.Value.isAction)
281 return false;
282 if (draggedItemData.isComposite)
283 Dispatch(Commands.MoveComposite(draggedItemData.bindingIndex, directParent.Value.actionIndex, childIndex));
284 else
285 Dispatch(Commands.MoveBinding(draggedItemData.bindingIndex, directParent.Value.actionIndex, childIndex));
286 return true;
287 }
288
289 private bool MoveCompositeParts(ActionOrBindingData? directParent, int childIndex, ActionOrBindingData draggedItemData)
290 {
291 if (directParent == null || !directParent.Value.isComposite)
292 return false;
293 var newBindingIndex = directParent.Value.bindingIndex + childIndex + (directParent.Value.bindingIndex > draggedItemData.bindingIndex ? 0 : 1);
294 Dispatch(Commands.MovePartOfComposite(draggedItemData.bindingIndex, newBindingIndex, directParent.Value.bindingIndex));
295 return true;
296 }
297
298 private void RenameNewAction(int id)
299 {
300 if (!m_RenameOnActionAdded || id == -1)
301 return;
302 m_ActionsTreeView.ScrollToItemById(id);
303 var treeViewItem = m_ActionsTreeView.GetRootElementForId(id)?.Q<InputActionsTreeViewItem>();
304 treeViewItem?.FocusOnRenameTextField();
305 }
306
307 internal void RenameActionItem(int index)
308 {
309 m_ActionsTreeView.ScrollToItem(index);
310 m_ActionsTreeView.GetRootElementForIndex(index)?.Q<InputActionsTreeViewItem>()?.FocusOnRenameTextField();
311 }
312
313 internal void AddAction()
314 {
315 Dispatch(Commands.AddAction());
316 m_RenameOnActionAdded = true;
317 }
318
319 internal void AddBinding(int index)
320 {
321 Dispatch(Commands.SelectAction(m_ActionsTreeView.GetItemDataForIndex<ActionOrBindingData>(index).actionIndex));
322 Dispatch(Commands.AddBinding());
323 }
324
325 internal void AddComposite(int index, string compositeType)
326 {
327 Dispatch(Commands.SelectAction(m_ActionsTreeView.GetItemDataForIndex<ActionOrBindingData>(index).actionIndex));
328 Dispatch(Commands.AddComposite(compositeType));
329 }
330
331 internal void DeleteItem(int selectedIndex)
332 {
333 var data = m_ActionsTreeView.GetItemDataForIndex<ActionOrBindingData>(selectedIndex);
334
335 if (data.isAction)
336 Dispatch(Commands.DeleteAction(data.actionMapIndex, data.name));
337 else
338 Dispatch(Commands.DeleteBinding(data.actionMapIndex, data.bindingIndex));
339
340 // Deleting an item sometimes causes the UI Panel to lose focus; make sure we keep it
341 m_ActionsTreeView.Focus();
342 }
343
344 internal void DuplicateItem(int selectedIndex)
345 {
346 var data = m_ActionsTreeView.GetItemDataForIndex<ActionOrBindingData>(selectedIndex);
347
348 Dispatch(data.isAction ? Commands.DuplicateAction() : Commands.DuplicateBinding());
349 }
350
351 internal void CopyItems()
352 {
353 Dispatch(Commands.CopyActionBindingSelection());
354 }
355
356 internal void CutItems()
357 {
358 Dispatch(Commands.CutActionsOrBindings());
359 }
360
361 internal void PasteItems()
362 {
363 Dispatch(Commands.PasteActionsOrBindings(InputActionsEditorView.s_OnPasteCutElements));
364 }
365
366 private void ChangeActionOrCompositName(ActionOrBindingData data, string newName)
367 {
368 m_RenameOnActionAdded = false;
369
370 if (data.isAction)
371 Dispatch(Commands.ChangeActionName(data.actionMapIndex, data.name, newName));
372 else if (data.isComposite)
373 Dispatch(Commands.ChangeCompositeName(data.actionMapIndex, data.bindingIndex, newName));
374 }
375
376 internal int GetMapCount()
377 {
378 return m_ActionMapsListView.itemsSource.Count;
379 }
380
381 private void OnExecuteCommand(ExecuteCommandEvent evt)
382 {
383 if (m_ActionsTreeView.selectedItem == null)
384 return;
385
386 if (allowUICommandExecution)
387 {
388 var data = (ActionOrBindingData)m_ActionsTreeView.selectedItem;
389 switch (evt.commandName)
390 {
391 case CmdEvents.Rename:
392 if (data.isAction || data.isComposite)
393 RenameActionItem(m_ActionsTreeView.selectedIndex);
394 else
395 return;
396 break;
397 case CmdEvents.Delete:
398 case CmdEvents.SoftDelete:
399 DeleteItem(m_ActionsTreeView.selectedIndex);
400 break;
401 case CmdEvents.Duplicate:
402 DuplicateItem(m_ActionsTreeView.selectedIndex);
403 break;
404 case CmdEvents.Copy:
405 CopyItems();
406 break;
407 case CmdEvents.Cut:
408 CutItems();
409 break;
410 case CmdEvents.Paste:
411 var hasPastableData = CopyPasteHelper.HasPastableClipboardData(data.isAction ? typeof(InputAction) : typeof(InputBinding));
412 if (hasPastableData)
413 PasteItems();
414 break;
415 default:
416 return; // Skip StopPropagation if we didn't execute anything
417 }
418
419 // Prevent any UI commands from executing until after UI has been updated
420 allowUICommandExecution = false;
421 }
422 evt.StopPropagation();
423 }
424
425 private void OnValidateCommand(ValidateCommandEvent evt)
426 {
427 // Mark commands as supported for Execute by stopping propagation of the event
428 switch (evt.commandName)
429 {
430 case CmdEvents.Rename:
431 case CmdEvents.Delete:
432 case CmdEvents.SoftDelete:
433 case CmdEvents.Duplicate:
434 case CmdEvents.Copy:
435 case CmdEvents.Cut:
436 case CmdEvents.Paste:
437 evt.StopPropagation();
438 break;
439 }
440 }
441
442 private void OnPointerDown(PointerDownEvent evt)
443 {
444 // Allow right clicks to select an item before we bring up the matching context menu.
445 if (evt.button == (int)MouseButton.RightMouse && evt.clickCount == 1)
446 {
447 // Look upwards to the immediate child of the scroll view, so we know what Index to use
448 var element = evt.target as VisualElement;
449 while (element != null && element.name != "unity-tree-view__item")
450 element = element.parent;
451
452 if (element == null)
453 return;
454
455 m_ActionsTreeView.SetSelection(element.parent.IndexOf(element));
456 }
457 }
458
459 private string GetPreviousActionNameFromViewTree(in ActionOrBindingData data)
460 {
461 Debug.Assert(data.isAction);
462
463 // If TreeView currently (before delete) has more than one Action, select the one immediately
464 // above or immediately below depending if data is first in the list
465 var treeView = ViewStateSelector.GetViewState(stateContainer.GetState()).treeViewData;
466 if (treeView.Count > 1)
467 {
468 string actionName = data.name;
469 int index = treeView.FindIndex(item => item.data.name == actionName);
470 if (index > 0)
471 index--;
472 else
473 index++; // Also handles case if actionName wasn't found; FindIndex() returns -1 that's incremented to 0
474
475 return treeView[index].data.name;
476 }
477
478 return string.Empty;
479 }
480
481 private int GetPreviousBindingIndexFromViewTree(in ActionOrBindingData data, out string parentActionName)
482 {
483 Debug.Assert(!data.isAction);
484
485 int retVal = -1;
486 parentActionName = string.Empty;
487
488 // The bindindIndex is global and doesn't correspond to the binding's "child index" within the TreeView.
489 // To find the "previous" Binding to select, after deleting the current one, we must:
490 // 1. Traverse the ViewTree to find the parent of the binding and its index under that parent
491 // 2. Identify the Binding to select after deletion and retrieve its bindingIndex
492 // 3. Return the bindingIndex and the parent Action name (select the Action if bindingIndex is invalid)
493
494 var treeView = ViewStateSelector.GetViewState(stateContainer.GetState()).treeViewData;
495 foreach (var action in treeView)
496 {
497 if (!action.hasChildren)
498 continue;
499
500 if (FindBindingOrComponentTreeViewParent(action, data.bindingIndex, out var parentNode, out int childIndex))
501 {
502 parentActionName = action.data.name;
503 if (parentNode.children.Count() > 1)
504 {
505 int prevIndex = Math.Max(childIndex - 1, 0);
506 var node = parentNode.children.ElementAt(prevIndex);
507 retVal = node.data.bindingIndex;
508 break;
509 }
510 }
511 }
512
513 return retVal;
514 }
515
516 private static bool FindBindingOrComponentTreeViewParent(TreeViewItemData<ActionOrBindingData> root, int bindingIndex, out TreeViewItemData<ActionOrBindingData> parent, out int childIndex)
517 {
518 Debug.Assert(root.hasChildren);
519
520 int index = 0;
521 foreach (var item in root.children)
522 {
523 if (item.data.bindingIndex == bindingIndex)
524 {
525 parent = root;
526 childIndex = index;
527 return true;
528 }
529
530 if (item.hasChildren && FindBindingOrComponentTreeViewParent(item, bindingIndex, out parent, out childIndex))
531 return true;
532
533 index++;
534 }
535
536 parent = default;
537 childIndex = -1;
538 return false;
539 }
540
541 internal class ViewState
542 {
543 public List<TreeViewItemData<ActionOrBindingData>> treeViewData;
544 public int actionMapCount;
545 public int newElementID;
546 }
547 }
548
549 internal struct ActionOrBindingData
550 {
551 public ActionOrBindingData(bool isAction, string name, int actionMapIndex, bool isComposite = false, bool isPartOfComposite = false, string controlLayout = "", int bindingIndex = -1, int actionIndex = -1, bool isCut = false)
552 {
553 this.name = name;
554 this.isComposite = isComposite;
555 this.isPartOfComposite = isPartOfComposite;
556 this.actionMapIndex = actionMapIndex;
557 this.controlLayout = controlLayout;
558 this.bindingIndex = bindingIndex;
559 this.isAction = isAction;
560 this.actionIndex = actionIndex;
561 this.isCut = isCut;
562 }
563
564 public string name { get; }
565 public bool isAction { get; }
566 public int actionMapIndex { get; }
567 public bool isComposite { get; }
568 public bool isPartOfComposite { get; }
569 public string controlLayout { get; }
570 public int bindingIndex { get; }
571 public int actionIndex { get; }
572 public bool isCut { get; }
573 }
574
575 internal static partial class Selectors
576 {
577 public static List<TreeViewItemData<ActionOrBindingData>> GetActionsAsTreeViewData(InputActionsEditorState state, Dictionary<Guid, int> idDictionary)
578 {
579 var actionMapIndex = state.selectedActionMapIndex;
580 var controlSchemes = state.serializedObject.FindProperty(nameof(InputActionAsset.m_ControlSchemes));
581 var actionMap = GetSelectedActionMap(state);
582
583 if (actionMap == null)
584 return new List<TreeViewItemData<ActionOrBindingData>>();
585
586 var actions = actionMap.Value.wrappedProperty
587 .FindPropertyRelative(nameof(InputActionMap.m_Actions))
588 .Select(sp => new SerializedInputAction(sp));
589
590 var bindings = actionMap.Value.wrappedProperty
591 .FindPropertyRelative(nameof(InputActionMap.m_Bindings))
592 .Select(sp => new SerializedInputBinding(sp))
593 .ToList();
594
595 var actionItems = new List<TreeViewItemData<ActionOrBindingData>>();
596 foreach (var action in actions)
597 {
598 var actionBindings = bindings.Where(spb => spb.action == action.name).ToList();
599 var bindingItems = new List<TreeViewItemData<ActionOrBindingData>>();
600 var actionId = new Guid(action.id);
601
602 for (var i = 0; i < actionBindings.Count; i++)
603 {
604 var serializedInputBinding = actionBindings[i];
605 var inputBindingId = new Guid(serializedInputBinding.id);
606
607 if (serializedInputBinding.isComposite)
608 {
609 var isLastBinding = i >= actionBindings.Count - 1;
610 var hasHiddenCompositeParts = false;
611
612 var compositeItems = new List<TreeViewItemData<ActionOrBindingData>>();
613
614 if (!isLastBinding)
615 {
616 var nextBinding = actionBindings[++i];
617
618 while (nextBinding.isPartOfComposite)
619 {
620 var isVisible = ShouldBindingBeVisible(nextBinding, state.selectedControlScheme, state.selectedDeviceRequirementIndex);
621 if (isVisible)
622 {
623 var name = GetHumanReadableCompositeName(nextBinding, state.selectedControlScheme, controlSchemes);
624 compositeItems.Add(new TreeViewItemData<ActionOrBindingData>(GetIdForGuid(new Guid(nextBinding.id), idDictionary),
625 new ActionOrBindingData(isAction: false, name, actionMapIndex, isComposite: false,
626 isPartOfComposite: true, GetControlLayout(nextBinding.path), bindingIndex: nextBinding.indexOfBinding, isCut: state.IsBindingCut(actionMapIndex, nextBinding.indexOfBinding))));
627 }
628 else
629 hasHiddenCompositeParts = true;
630
631 if (++i >= actionBindings.Count)
632 break;
633
634 nextBinding = actionBindings[i];
635 }
636
637 i--;
638 }
639
640 var shouldCompositeBeVisible = !(compositeItems.Count == 0 && hasHiddenCompositeParts); //hide composite if all parts are hidden
641 if (shouldCompositeBeVisible)
642 bindingItems.Add(new TreeViewItemData<ActionOrBindingData>(GetIdForGuid(inputBindingId, idDictionary),
643 new ActionOrBindingData(isAction: false, serializedInputBinding.name, actionMapIndex, isComposite: true, isPartOfComposite: false, action.expectedControlType, bindingIndex: serializedInputBinding.indexOfBinding, isCut: state.IsBindingCut(actionMapIndex, serializedInputBinding.indexOfBinding)),
644 compositeItems.Count > 0 ? compositeItems : null));
645 }
646 else
647 {
648 var isVisible = ShouldBindingBeVisible(serializedInputBinding, state.selectedControlScheme, state.selectedDeviceRequirementIndex);
649 if (isVisible)
650 bindingItems.Add(new TreeViewItemData<ActionOrBindingData>(GetIdForGuid(inputBindingId, idDictionary),
651 new ActionOrBindingData(isAction: false, GetHumanReadableBindingName(serializedInputBinding, state.selectedControlScheme, controlSchemes), actionMapIndex,
652 isComposite: false, isPartOfComposite: false, GetControlLayout(serializedInputBinding.path), bindingIndex: serializedInputBinding.indexOfBinding, isCut: state.IsBindingCut(actionMapIndex, serializedInputBinding.indexOfBinding))));
653 }
654 }
655 var actionIndex = action.wrappedProperty.GetIndexOfArrayElement();
656 actionItems.Add(new TreeViewItemData<ActionOrBindingData>(GetIdForGuid(actionId, idDictionary),
657 new ActionOrBindingData(isAction: true, action.name, actionMapIndex, isComposite: false, isPartOfComposite: false, action.expectedControlType, actionIndex: actionIndex, isCut: state.IsActionCut(actionMapIndex, actionIndex)), bindingItems.Count > 0 ? bindingItems : null));
658 }
659 return actionItems;
660 }
661
662 private static int GetIdForGuid(Guid guid, Dictionary<Guid, int> idDictionary)
663 {
664 if (!idDictionary.TryGetValue(guid, out var id))
665 {
666 id = idDictionary.Values.Count > 0 ? idDictionary.Values.Max() + 1 : 0;
667 idDictionary.Add(guid, id);
668 }
669 return id;
670 }
671
672 private static string GetHumanReadableBindingName(SerializedInputBinding serializedInputBinding, InputControlScheme? currentControlScheme, SerializedProperty allControlSchemes)
673 {
674 var name = InputControlPath.ToHumanReadableString(serializedInputBinding.path);
675 if (String.IsNullOrEmpty(name))
676 name = "<No Binding>";
677 if (IsBindingAssignedToNoControlSchemes(serializedInputBinding, allControlSchemes, currentControlScheme))
678 name += " {GLOBAL}";
679 return name;
680 }
681
682 private static bool IsBindingAssignedToNoControlSchemes(SerializedInputBinding serializedInputBinding, SerializedProperty allControlSchemes, InputControlScheme? currentControlScheme)
683 {
684 if (allControlSchemes.arraySize <= 0 || !currentControlScheme.HasValue || string.IsNullOrEmpty(currentControlScheme.Value.name))
685 return false;
686 if (serializedInputBinding.controlSchemes.Length <= 0)
687 return true;
688 return false;
689 }
690
691 private static bool ShouldBindingBeVisible(SerializedInputBinding serializedInputBinding, InputControlScheme? currentControlScheme, int deviceIndex)
692 {
693 if (currentControlScheme.HasValue && !string.IsNullOrEmpty(currentControlScheme.Value.name))
694 {
695 var isMatchingDevice = true;
696 if (deviceIndex >= 0)
697 {
698 var devicePathToMatch = InputControlPath.TryGetDeviceLayout(currentControlScheme.Value.deviceRequirements.ElementAt(deviceIndex).controlPath);
699 var devicePath = InputControlPath.TryGetDeviceLayout(serializedInputBinding.path);
700 isMatchingDevice = string.Equals(devicePathToMatch, devicePath, StringComparison.InvariantCultureIgnoreCase) || InputControlLayout.s_Layouts.IsBasedOn(new InternedString(devicePath), new InternedString(devicePathToMatch));
701 }
702 var hasNoControlScheme = serializedInputBinding.controlSchemes.Length <= 0; //also show GLOBAL bindings
703 var isAssignedToCurrentControlScheme = serializedInputBinding.controlSchemes.Contains(currentControlScheme.Value.name);
704 return (isAssignedToCurrentControlScheme || hasNoControlScheme) && isMatchingDevice;
705 }
706 //if no control scheme selected then show all bindings
707 return true;
708 }
709
710 internal static string GetHumanReadableCompositeName(SerializedInputBinding binding, InputControlScheme? currentControlScheme, SerializedProperty allControlSchemes)
711 {
712 return $"{ObjectNames.NicifyVariableName(binding.name)}: " +
713 $"{GetHumanReadableBindingName(binding, currentControlScheme, allControlSchemes)}";
714 }
715
716 private static string GetControlLayout(string path)
717 {
718 var controlLayout = string.Empty;
719 try
720 {
721 controlLayout = InputControlPath.TryGetControlLayout(path);
722 }
723 catch (Exception)
724 {
725 }
726
727 return controlLayout;
728 }
729 }
730}
731
732#endif