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