A game about forced loneliness, made by TACStudios
1#if UNITY_EDITOR 2using System; 3using System.Collections.Generic; 4using System.Linq; 5using System.Text; 6using UnityEditor; 7using UnityEditor.Callbacks; 8using UnityEditor.IMGUI.Controls; 9using UnityEditor.PackageManager.UI; 10using UnityEditor.ShortcutManagement; 11 12////TODO: Add "Revert" button 13 14////TODO: add helpers to very quickly set up certain common configs (e.g. "FPS Controls" in add-action context menu; 15//// "WASD Control" in add-binding context menu) 16 17////REVIEW: should we listen for Unity project saves and save dirty .inputactions assets along with it? 18 19////FIXME: when saving, processor/interaction selection is cleared 20 21////TODO: persist view state of asset in Library/ folder 22 23namespace UnityEngine.InputSystem.Editor 24{ 25 /// <summary> 26 /// An editor window to edit .inputactions assets. 27 /// </summary> 28 /// <remarks> 29 /// The .inputactions editor code does not really separate between model and view. Selection state is contained 30 /// in the tree views and persistent across domain reloads via <see cref="TreeViewState"/>. 31 /// </remarks> 32 internal class InputActionEditorWindow : EditorWindow, IDisposable, IInputActionAssetEditor 33 { 34 // Register editor type via static constructor to enable asset monitoring 35 static InputActionEditorWindow() 36 { 37 InputActionAssetEditor.RegisterType<InputActionEditorWindow>(); 38 } 39 40 /// <summary> 41 /// Open window if someone clicks on an .inputactions asset or an action inside of it. 42 /// </summary> 43 [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "line", Justification = "line parameter required by OnOpenAsset attribute")] 44 [OnOpenAsset] 45 public static bool OnOpenAsset(int instanceId, int line) 46 { 47#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS 48 if (!InputSystem.settings.IsFeatureEnabled(InputFeatureNames.kUseIMGUIEditorForAssets)) 49 return false; 50#endif 51 var path = AssetDatabase.GetAssetPath(instanceId); 52 if (!InputActionImporter.IsInputActionAssetPath(path)) 53 return false; 54 55 string mapToSelect = null; 56 string actionToSelect = null; 57 58 // Grab InputActionAsset. 59 // NOTE: We defer checking out an asset until we save it. This allows a user to open an .inputactions asset and look at it 60 // without forcing a checkout. 61 var obj = EditorUtility.InstanceIDToObject(instanceId); 62 var asset = obj as InputActionAsset; 63 if (asset == null) 64 { 65 // Check if the user clicked on an action inside the asset. 66 var actionReference = obj as InputActionReference; 67 if (actionReference != null) 68 { 69 asset = actionReference.asset; 70 mapToSelect = actionReference.action.actionMap.name; 71 actionToSelect = actionReference.action.name; 72 } 73 else 74 return false; 75 } 76 77 var window = OpenEditor(asset); 78 79 // If user clicked on an action inside the asset, focus on that action (if we can find it). 80 if (actionToSelect != null && window.m_ActionMapsTree.TrySelectItem(mapToSelect)) 81 { 82 window.OnActionMapTreeSelectionChanged(); 83 window.m_ActionsTree.SelectItem(actionToSelect); 84 } 85 86 return true; 87 } 88 89 /// <summary> 90 /// Open the specified <paramref name="asset"/> in an editor window. Used when someone hits the "Edit Asset" button in the 91 /// importer inspector. 92 /// </summary> 93 /// <param name="asset">The InputActionAsset to open.</param> 94 /// <returns>The editor window.</returns> 95 public static InputActionEditorWindow OpenEditor(InputActionAsset asset) 96 { 97 ////REVIEW: It'd be great if the window got docked by default but the public EditorWindow API doesn't allow that 98 //// to be done for windows that aren't singletons (GetWindow<T>() will only create one window and it's the 99 //// only way to get programmatic docking with the current API). 100 // See if we have an existing editor window that has the asset open. 101 var window = FindEditorForAsset(asset); 102 if (window == null) 103 { 104 // No, so create a new window. 105 window = CreateInstance<InputActionEditorWindow>(); 106 window.SetAsset(asset); 107 } 108 window.Show(); 109 window.Focus(); 110 111 return window; 112 } 113 114 private static InputActionEditorWindow FindEditorForAsset(InputActionAsset asset) 115 { 116 var guid = EditorHelpers.GetAssetGUID(asset); 117 return guid == null ? null : FindEditorForAssetWithGUID(guid); 118 } 119 120 public static InputActionEditorWindow FindEditorForAssetWithGUID(string guid) 121 { 122 var windows = Resources.FindObjectsOfTypeAll<InputActionEditorWindow>(); 123 return windows.FirstOrDefault(w => w.m_ActionAssetManager.guid == guid); 124 } 125 126 public void SaveChangesToAsset() 127 { 128 m_ActionAssetManager.SaveChangesToAsset(); 129 } 130 131 public void AddNewActionMap() 132 { 133 m_ActionMapsTree.AddNewActionMap(); 134 } 135 136 public void AddNewAction() 137 { 138 // Make sure we have an action map. If we don't have an action map selected, 139 // refuse the operation. 140 var actionMapItem = m_ActionMapsTree.GetSelectedItems().OfType<ActionMapTreeItem>().FirstOrDefault(); 141 if (actionMapItem == null) 142 { 143 EditorApplication.Beep(); 144 return; 145 } 146 147 m_ActionsTree.AddNewAction(actionMapItem.property); 148 } 149 150 public void AddNewBinding() 151 { 152 // Make sure we have an action selected. 153 var actionItems = m_ActionsTree.GetSelectedItems().OfType<ActionTreeItem>(); 154 if (!actionItems.Any()) 155 { 156 EditorApplication.Beep(); 157 return; 158 } 159 160 foreach (var item in actionItems) 161 m_ActionsTree.AddNewBinding(item.property, item.actionMapProperty); 162 } 163 164 private bool ConfirmSaveChangesIfNeeded() 165 { 166 // Ask for confirmation if we have unsaved changes. 167 if (!m_ForceQuit && m_ActionAssetManager.dirty) 168 { 169 var result = Dialog.InputActionAsset.ShowSaveChanges(m_ActionAssetManager.path); 170 switch (result) 171 { 172 case Dialog.Result.Save: 173 m_ActionAssetManager.SaveChangesToAsset(); 174 m_ActionAssetManager.Cleanup(); 175 break; 176 case Dialog.Result.Cancel: 177 Instantiate(this).Show(); 178 // Cancel editor quit. 179 return false; 180 case Dialog.Result.Discard: 181 // Don't save, don't ask again. 182 m_ForceQuit = true; 183 break; 184 default: 185 throw new ArgumentOutOfRangeException(nameof(result)); 186 } 187 } 188 return true; 189 } 190 191 private bool EditorWantsToQuit() 192 { 193 return ConfirmSaveChangesIfNeeded(); 194 } 195 196 private void OnEnable() 197 { 198 minSize = new Vector2(600, 300); 199 200 // Initialize toolbar. We keep the toolbar across domain reloads but we 201 // will lose the delegates. 202 if (m_Toolbar == null) 203 m_Toolbar = new InputActionEditorToolbar(); 204 m_Toolbar.onSearchChanged = OnToolbarSearchChanged; 205 m_Toolbar.onSelectedSchemeChanged = OnControlSchemeSelectionChanged; 206 m_Toolbar.onSelectedDeviceChanged = OnControlSchemeSelectionChanged; 207 m_Toolbar.onSave = SaveChangesToAsset; 208 m_Toolbar.onControlSchemesChanged = OnControlSchemesModified; 209 m_Toolbar.onControlSchemeRenamed = OnControlSchemeRenamed; 210 m_Toolbar.onControlSchemeDeleted = OnControlSchemeDeleted; 211 EditorApplication.wantsToQuit += EditorWantsToQuit; 212 213 // Initialize after assembly reload. 214 if (m_ActionAssetManager != null) 215 { 216 if (!m_ActionAssetManager.Initialize()) 217 { 218 // The asset we want to edit no longer exists. 219 Close(); 220 return; 221 } 222 m_ActionAssetManager.onDirtyChanged = OnDirtyChanged; 223 224 InitializeTrees(); 225 } 226 227 InputSystem.onSettingsChange += OnInputSettingsChanged; 228 } 229 230 private void OnDestroy() 231 { 232 ConfirmSaveChangesIfNeeded(); 233 EditorApplication.wantsToQuit -= EditorWantsToQuit; 234 InputSystem.onSettingsChange -= OnInputSettingsChanged; 235 } 236 237 private void OnInputSettingsChanged() 238 { 239 Repaint(); 240 } 241 242 // Set asset would usually only be called when the window is open 243 private void SetAsset(InputActionAsset asset) 244 { 245 if (asset == null) 246 return; 247 248 m_ActionAssetManager = new InputActionAssetManager(asset) {onDirtyChanged = OnDirtyChanged}; 249 //m_ActionAssetManager.Initialize(); // TODO No longer needed when using constructor 250 251 InitializeTrees(); 252 LoadControlSchemes(); 253 254 // Select first action map in asset. 255 m_ActionMapsTree.SelectFirstToplevelItem(); 256 257 UpdateWindowTitle(); 258 } 259 260 private void UpdateWindowTitle() 261 { 262 var title = m_ActionAssetManager.name + " (Input Actions)"; 263 m_Title = new GUIContent(title); 264 m_DirtyTitle = new GUIContent("(*) " + m_Title.text); 265 titleContent = m_Title; 266 } 267 268 private void LoadControlSchemes() 269 { 270 TransferControlSchemes(save: false); 271 } 272 273 private void TransferControlSchemes(bool save) 274 { 275 // The easiest way to load and save control schemes is using SerializedProperties to just transfer the data 276 // between the InputControlScheme array in the toolbar and the one in the asset. Doing it this way rather than 277 // just overwriting the array in m_AssetManager.m_AssetObjectForEditing directly will make undo work. 278 using (var editorWindowObject = new SerializedObject(this)) 279 using (var controlSchemesArrayPropertyInWindow = editorWindowObject.FindProperty("m_Toolbar.m_ControlSchemes")) 280 using (var controlSchemesArrayPropertyInAsset = m_ActionAssetManager.serializedObject.FindProperty("m_ControlSchemes")) 281 { 282 Debug.Assert(controlSchemesArrayPropertyInWindow != null, $"Cannot find m_ControlSchemes in window"); 283 Debug.Assert(controlSchemesArrayPropertyInAsset != null, $"Cannot find m_ControlSchemes in asset"); 284 285 if (save) 286 { 287 var json = controlSchemesArrayPropertyInWindow.CopyToJson(); 288 controlSchemesArrayPropertyInAsset.RestoreFromJson(json); 289 editorWindowObject.ApplyModifiedProperties(); 290 } 291 else 292 { 293 // Load. 294 var json = controlSchemesArrayPropertyInAsset.CopyToJson(); 295 controlSchemesArrayPropertyInWindow.RestoreFromJson(json); 296 editorWindowObject.ApplyModifiedPropertiesWithoutUndo(); 297 } 298 } 299 } 300 301 private void OnControlSchemeSelectionChanged() 302 { 303 OnToolbarSearchChanged(); 304 LoadPropertiesForSelection(); 305 } 306 307 private void OnControlSchemesModified() 308 { 309 TransferControlSchemes(save: true); 310 311 // Control scheme changes may affect the search filter. 312 OnToolbarSearchChanged(); 313 314 ApplyAndReloadTrees(); 315 } 316 317 private void OnControlSchemeRenamed(string oldBindingGroup, string newBindingGroup) 318 { 319 InputActionSerializationHelpers.ReplaceBindingGroup(m_ActionAssetManager.serializedObject, 320 oldBindingGroup, newBindingGroup); 321 ApplyAndReloadTrees(); 322 } 323 324 private void OnControlSchemeDeleted(string name, string bindingGroup) 325 { 326 Debug.Assert(!string.IsNullOrEmpty(name), "Control scheme name should not be empty"); 327 Debug.Assert(!string.IsNullOrEmpty(bindingGroup), "Binding group should not be empty"); 328 329 var asset = m_ActionAssetManager.editedAsset; 330 331 var bindingMask = InputBinding.MaskByGroup(bindingGroup); 332 var schemeHasBindings = asset.actionMaps.Any(m => m.bindings.Any(b => bindingMask.Matches(ref b))); 333 if (!schemeHasBindings) 334 return; 335 336 ////FIXME: this does not delete composites that have bindings in only one control scheme 337 ////REVIEW: offer to do nothing and leave all bindings as is? 338 var deleteBindings = 339 EditorUtility.DisplayDialog("Delete Bindings?", 340 $"Delete bindings for '{name}' as well? If you select 'No', the bindings will only " 341 + $"be unassigned from the '{name}' control scheme but otherwise left as is. Note that bindings " 342 + $"that are assigned to '{name}' but also to other control schemes will be left in place either way.", 343 "Yes", "No"); 344 345 InputActionSerializationHelpers.ReplaceBindingGroup(m_ActionAssetManager.serializedObject, bindingGroup, "", 346 deleteOrphanedBindings: deleteBindings); 347 348 ApplyAndReloadTrees(); 349 } 350 351 private void InitializeTrees() 352 { 353 // We persist tree view states (most importantly, they contain our selection states), 354 // so only create those if we don't have any yet. 355 if (m_ActionMapsTreeState == null) 356 m_ActionMapsTreeState = new TreeViewState(); 357 if (m_ActionsTreeState == null) 358 m_ActionsTreeState = new TreeViewState(); 359 360 // Create tree in middle pane showing actions and bindings. We initially 361 // leave this tree empty and populate it by selecting an action map in the 362 // left pane tree. 363 m_ActionsTree = new InputActionTreeView(m_ActionAssetManager.serializedObject, m_ActionsTreeState) 364 { 365 onSelectionChanged = OnActionTreeSelectionChanged, 366 onSerializedObjectModified = ApplyAndReloadTrees, 367 onBindingAdded = p => InputActionSerializationHelpers.RemoveUnusedBindingGroups(p, m_Toolbar.controlSchemes), 368 drawMinusButton = false, 369 title = ("Actions", "A list of InputActions in the InputActionMap selected in the left pane. Also, for each InputAction, the list " 370 + "of bindings that determine the controls that can trigger the action.\n\nThe name of each action must be unique within its InputActionMap."), 371 }; 372 373 // Create tree in left pane showing action maps. 374 m_ActionMapsTree = new InputActionTreeView(m_ActionAssetManager.serializedObject, m_ActionMapsTreeState) 375 { 376 onBuildTree = () => 377 InputActionTreeView.BuildWithJustActionMapsFromAsset(m_ActionAssetManager.serializedObject), 378 onSelectionChanged = OnActionMapTreeSelectionChanged, 379 onSerializedObjectModified = ApplyAndReloadTrees, 380 onHandleAddNewAction = m_ActionsTree.AddNewAction, 381 drawMinusButton = false, 382 title = ("Action Maps", "A list of InputActionMaps in the asset. Each map can be enabled and disabled separately at runtime and holds " 383 + "its own collection of InputActions which are listed in the middle pane (along with their InputBindings).") 384 }; 385 m_ActionMapsTree.Reload(); 386 m_ActionMapsTree.ExpandAll(); 387 388 RebuildActionTree(); 389 LoadPropertiesForSelection(); 390 391 // Sync current search status in toolbar. 392 OnToolbarSearchChanged(); 393 } 394 395 /// <summary> 396 /// Synchronize the search filter applied to the trees. 397 /// </summary> 398 /// <remarks> 399 /// Note that only filter the action tree. The action map tree remains unfiltered. 400 /// </remarks> 401 private void OnToolbarSearchChanged() 402 { 403 // Rather than adding FilterCriterion instances directly, we go through the 404 // string-based format here. This allows typing queries directly into the search bar. 405 406 var searchStringBuffer = new StringBuilder(); 407 408 // Plain-text search. 409 if (!string.IsNullOrEmpty(m_Toolbar.searchText)) 410 searchStringBuffer.Append(m_Toolbar.searchText); 411 412 // Filter by binding group of selected control scheme. 413 if (m_Toolbar.selectedControlScheme != null) 414 { 415 searchStringBuffer.Append(" \""); 416 searchStringBuffer.Append(InputActionTreeView.FilterCriterion.k_BindingGroupTag); 417 searchStringBuffer.Append(m_Toolbar.selectedControlScheme.Value.bindingGroup); 418 searchStringBuffer.Append('\"'); 419 } 420 421 // Filter by device layout. 422 if (m_Toolbar.selectedDeviceRequirement != null) 423 { 424 searchStringBuffer.Append(" \""); 425 searchStringBuffer.Append(InputActionTreeView.FilterCriterion.k_DeviceLayoutTag); 426 searchStringBuffer.Append(InputControlPath.TryGetDeviceLayout(m_Toolbar.selectedDeviceRequirement.Value.controlPath)); 427 searchStringBuffer.Append('\"'); 428 } 429 430 var searchString = searchStringBuffer.ToString(); 431 if (string.IsNullOrEmpty(searchString)) 432 m_ActionsTree.ClearItemSearchFilterAndReload(); 433 else 434 m_ActionsTree.SetItemSearchFilterAndReload(searchStringBuffer.ToString()); 435 436 // Have trees create new bindings with the right binding group. 437 var currentBindingGroup = m_Toolbar.selectedControlScheme?.bindingGroup; 438 m_ActionsTree.bindingGroupForNewBindings = currentBindingGroup; 439 m_ActionMapsTree.bindingGroupForNewBindings = currentBindingGroup; 440 } 441 442 /// <summary> 443 /// Synchronize the display state to the currently selected action map. 444 /// </summary> 445 private void OnActionMapTreeSelectionChanged() 446 { 447 // Re-configure action tree (middle pane) for currently select action map. 448 RebuildActionTree(); 449 450 // If there's no actions in the selected action map or if there is no action map 451 // selected, make sure we wipe the property pane. 452 if (!m_ActionMapsTree.HasSelection() || !m_ActionsTree.rootItem.hasChildren) 453 { 454 LoadPropertiesForSelection(); 455 } 456 else 457 { 458 // Otherwise select first action in map. 459 m_ActionsTree.SelectFirstToplevelItem(); 460 } 461 } 462 463 private void RebuildActionTree() 464 { 465 var selectedActionMapItem = 466 m_ActionMapsTree.GetSelectedItems().OfType<ActionMapTreeItem>().FirstOrDefault(); 467 if (selectedActionMapItem == null) 468 { 469 // Nothing selected. Wipe middle and right pane. 470 m_ActionsTree.onBuildTree = null; 471 } 472 else 473 { 474 m_ActionsTree.onBuildTree = () => 475 InputActionTreeView.BuildWithJustActionsAndBindingsFromMap(selectedActionMapItem.property); 476 } 477 478 // Rebuild tree. 479 m_ActionsTree.Reload(); 480 } 481 482 private void OnActionTreeSelectionChanged() 483 { 484 LoadPropertiesForSelection(); 485 } 486 487 private void LoadPropertiesForSelection() 488 { 489 m_BindingPropertyView = null; 490 m_ActionPropertyView = null; 491 492 ////TODO: preserve interaction/processor selection when reloading 493 494 // Nothing else to do if we don't have a selection in the middle pane or if 495 // multiple items are selected (we don't currently have the ability to multi-edit). 496 if (!m_ActionsTree.HasSelection() || m_ActionsTree.GetSelection().Count != 1) 497 return; 498 499 var item = m_ActionsTree.GetSelectedItems().FirstOrDefault(); 500 if (item is BindingTreeItem) 501 { 502 // Grab the action for the binding and see if we have an expected control layout 503 // set on it. Pass that on to the control picking machinery. 504 var isCompositePartBinding = item is PartOfCompositeBindingTreeItem; 505 var actionItem = (isCompositePartBinding ? item.parent.parent : item.parent) as ActionTreeItem; 506 Debug.Assert(actionItem != null); 507 508 if (m_ControlPickerViewState == null) 509 m_ControlPickerViewState = new InputControlPickerState(); 510 511 // The toolbar may constrain the set of devices we're currently interested in by either 512 // having one specific device selected from the current scheme or having at least a control 513 // scheme selected. 514 IEnumerable<string> controlPathsToMatch; 515 if (m_Toolbar.selectedDeviceRequirement != null) 516 { 517 // Single device selected from set of devices in control scheme. 518 controlPathsToMatch = new[] {m_Toolbar.selectedDeviceRequirement.Value.controlPath}; 519 } 520 else if (m_Toolbar.selectedControlScheme != null) 521 { 522 // Constrain to devices from current control scheme. 523 controlPathsToMatch = 524 m_Toolbar.selectedControlScheme.Value.deviceRequirements.Select(x => x.controlPath); 525 } 526 else 527 { 528 // If there's no device filter coming from a control scheme, filter by supported 529 // devices as given by settings. 530 controlPathsToMatch = InputSystem.settings.supportedDevices.Select(x => $"<{x}>"); 531 } 532 533 // Show properties for binding. 534 m_BindingPropertyView = 535 new InputBindingPropertiesView( 536 item.property, 537 change => 538 { 539 if (change == InputBindingPropertiesView.k_PathChanged || 540 change == InputBindingPropertiesView.k_CompositePartAssignmentChanged || 541 change == InputBindingPropertiesView.k_CompositeTypeChanged || 542 change == InputBindingPropertiesView.k_GroupsChanged) 543 { 544 ApplyAndReloadTrees(); 545 } 546 else 547 { 548 // Simple property change that doesn't affect the rest of the UI. 549 Apply(); 550 } 551 }, 552 m_ControlPickerViewState, 553 expectedControlLayout: item.expectedControlLayout, 554 controlSchemes: m_Toolbar.controlSchemes, 555 controlPathsToMatch: controlPathsToMatch); 556 } 557 else if (item is ActionTreeItem actionItem) 558 { 559 // Show properties for action. 560 m_ActionPropertyView = 561 new InputActionPropertiesView( 562 actionItem.property, 563 // Apply without reload is enough here as modifying the properties of an action will 564 // never change the structure of the data. 565 change => Apply()); 566 } 567 } 568 569 private void ApplyAndReloadTrees() 570 { 571 Apply(); 572 573 // This path here is meant to catch *any* edits made to the serialized data. I.e. also 574 // any arbitrary undo that may have changed some misc bit not visible in the trees. 575 576 m_ActionMapsTree.Reload(); 577 RebuildActionTree(); 578 m_ActionAssetManager.UpdateAssetDirtyState(); 579 LoadControlSchemes(); 580 581 LoadPropertiesForSelection(); 582 } 583 584 #if UNITY_INPUT_SYSTEM_INPUT_ACTIONS_EDITOR_AUTO_SAVE_ON_FOCUS_LOST 585 private void OnLostFocus() 586 { 587 if (InputEditorUserSettings.autoSaveInputActionAssets) 588 m_ActionAssetManager.SaveChangesToAsset(); 589 } 590 591 #endif 592 593 private void Apply() 594 { 595 m_ActionAssetManager.ApplyChanges(); 596 597 // Update dirty count, otherwise ReloadIfSerializedObjectHasBeenChanged will trigger a full ApplyAndReloadTrees 598 m_ActionMapsTree.UpdateSerializedObjectDirtyCount(); 599 m_ActionsTree.UpdateSerializedObjectDirtyCount(); 600 601 #if UNITY_INPUT_SYSTEM_INPUT_ACTIONS_EDITOR_AUTO_SAVE_ON_FOCUS_LOST 602 // If auto-save should be triggered on focus lost, only mark asset as dirty 603 m_ActionAssetManager.MarkDirty(); 604 titleContent = m_DirtyTitle; 605 #else 606 // If auto-save is active, immediately flush out the changes to disk. Otherwise just 607 // put us into dirty state. 608 if (InputEditorUserSettings.autoSaveInputActionAssets) 609 { 610 m_ActionAssetManager.SaveChangesToAsset(); 611 } 612 else 613 { 614 m_ActionAssetManager.MarkDirty(); 615 titleContent = m_DirtyTitle; 616 } 617 #endif 618 } 619 620 private void OnGUI() 621 { 622 // If the actions tree has lost the filters (because they would not match an item it tried to highlight), 623 // update the Toolbar UI to remove them. 624 if (!m_ActionsTree.hasFilter) 625 m_Toolbar.ResetSearchFilters(); 626 627 // Allow switching between action map tree and action tree using arrow keys. 628 ToggleFocusUsingKeyboard(KeyCode.RightArrow, m_ActionMapsTree, m_ActionsTree); 629 ToggleFocusUsingKeyboard(KeyCode.LeftArrow, m_ActionsTree, m_ActionMapsTree); 630 631 // Route copy-paste events to tree views if they have focus. 632 if (m_ActionsTree.HasFocus()) 633 m_ActionsTree.HandleCopyPasteCommandEvent(Event.current); 634 else if (m_ActionMapsTree.HasFocus()) 635 m_ActionMapsTree.HandleCopyPasteCommandEvent(Event.current); 636 637 // Draw toolbar. 638 EditorGUILayout.BeginVertical(); 639 m_Toolbar.OnGUI(); 640 EditorGUILayout.Space(); 641 642 // Draw columns. 643 EditorGUILayout.BeginHorizontal(); 644 var columnAreaWidth = position.width - InputActionTreeView.Styles.backgroundWithBorder.margin.left - 645 InputActionTreeView.Styles.backgroundWithBorder.margin.left - 646 InputActionTreeView.Styles.backgroundWithBorder.margin.right; 647 648 var oldType = Event.current.type; 649 DrawActionMapsColumn(columnAreaWidth * 0.22f); 650 if (Event.current.type == EventType.Used && oldType != Event.current.type) 651 { 652 // When renaming an item, TreeViews will capture all mouse Events, and process any clicks outside the item 653 // being renamed to end the renaming process. However, since we have two TreeViews, if the action column is 654 // renaming an item, and then you double click on an item in the action map column, the action map column will 655 // get to use the mouse event before the action collumn gets to see it, which would cause the action map column 656 // to enter rename mode and use the event, before the action column gets a chance to see it and exit rename mode. 657 // Then we end up with two active renaming sessions, which does not work correctly. 658 // (See https://fogbugz.unity3d.com/f/cases/1140869/). 659 // Now, our fix to this problem is to force-end and accept any renaming session on the action column if we see 660 // that the action map column had processed the current event. This is not particularly elegant, but I cannot think 661 // of a better solution as we are limited by the public APIs exposed by TreeView. 662 m_ActionsTree.EndRename(forceAccept: true); 663 } 664 DrawActionsColumn(columnAreaWidth * 0.38f); 665 DrawPropertiesColumn(columnAreaWidth * 0.40f); 666 EditorGUILayout.EndHorizontal(); 667 668 // Bottom margin. 669 GUILayout.Space(3); 670 EditorGUILayout.EndVertical(); 671 } 672 673 private static void ToggleFocusUsingKeyboard(KeyCode key, InputActionTreeView fromTree, 674 InputActionTreeView toTree) 675 { 676 var uiEvent = Event.current; 677 if (uiEvent.type == EventType.KeyDown && uiEvent.keyCode == key && fromTree.HasFocus()) 678 { 679 if (!toTree.HasSelection()) 680 toTree.SelectFirstToplevelItem(); 681 toTree.SetFocus(); 682 uiEvent.Use(); 683 } 684 } 685 686 private void DrawActionMapsColumn(float width) 687 { 688 DrawColumnWithTreeView(m_ActionMapsTree, width, true); 689 } 690 691 private void DrawActionsColumn(float width) 692 { 693 DrawColumnWithTreeView(m_ActionsTree, width, false); 694 } 695 696 private static void DrawColumnWithTreeView(TreeView treeView, float width, bool fixedWidth) 697 { 698 EditorGUILayout.BeginVertical(InputActionTreeView.Styles.backgroundWithBorder, 699 fixedWidth ? GUILayout.MaxWidth(width) : GUILayout.MinWidth(width), 700 GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(true)); 701 GUILayout.FlexibleSpace(); 702 EditorGUILayout.EndVertical(); 703 704 var columnRect = GUILayoutUtility.GetLastRect(); 705 706 treeView.OnGUI(columnRect); 707 } 708 709 private void DrawPropertiesColumn(float width) 710 { 711 EditorGUILayout.BeginVertical(InputActionTreeView.Styles.backgroundWithBorder, GUILayout.Width(width)); 712 713 var rect = GUILayoutUtility.GetRect(0, 714 EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing * 2, 715 GUILayout.ExpandWidth(true)); 716 rect.x -= 2; 717 rect.y -= 1; 718 rect.width += 4; 719 720 EditorGUI.LabelField(rect, GUIContent.none, InputActionTreeView.Styles.backgroundWithBorder); 721 var headerRect = new Rect(rect.x + 1, rect.y + 1, rect.width - 2, rect.height - 2); 722 723 if (m_BindingPropertyView != null) 724 { 725 if (m_BindingPropertiesTitle == null) 726 m_BindingPropertiesTitle = new GUIContent("Binding Properties", "The properties for the InputBinding selected in the " 727 + "'Actions' pane on the left."); 728 EditorGUI.LabelField(headerRect, m_BindingPropertiesTitle, InputActionTreeView.Styles.columnHeaderLabel); 729 m_PropertiesScroll = EditorGUILayout.BeginScrollView(m_PropertiesScroll); 730 m_BindingPropertyView.OnGUI(); 731 EditorGUILayout.EndScrollView(); 732 } 733 else if (m_ActionPropertyView != null) 734 { 735 if (m_ActionPropertiesTitle == null) 736 m_ActionPropertiesTitle = new GUIContent("Action Properties", "The properties for the InputAction selected in the " 737 + "'Actions' pane on the left."); 738 EditorGUI.LabelField(headerRect, m_ActionPropertiesTitle, InputActionTreeView.Styles.columnHeaderLabel); 739 m_PropertiesScroll = EditorGUILayout.BeginScrollView(m_PropertiesScroll); 740 m_ActionPropertyView.OnGUI(); 741 EditorGUILayout.EndScrollView(); 742 } 743 else 744 { 745 GUILayout.FlexibleSpace(); 746 } 747 748 EditorGUILayout.EndVertical(); 749 } 750 751 private void ReloadAssetFromFileIfNotDirty() 752 { 753 if (m_ActionAssetManager.dirty) 754 return; 755 756 // If our asset has disappeared from disk, just close the window. 757 if (string.IsNullOrEmpty(AssetDatabase.GUIDToAssetPath(m_ActionAssetManager.guid))) 758 { 759 Close(); 760 return; 761 } 762 763 // Don't touch the UI state if the serialized data is still the same. 764 if (!m_ActionAssetManager.ReInitializeIfAssetHasChanged()) 765 return; 766 767 // Unfortunately, on this path we lose the selection state of the interactions and processors lists 768 // in the properties view. 769 770 InitializeTrees(); 771 LoadPropertiesForSelection(); 772 Repaint(); 773 } 774 775#if !UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS 776 ////TODO: add shortcut to focus search box 777 778 ////TODO: show shortcuts in tooltips 779 ////FIXME: the shortcuts seem to have focus problems; often requires clicking away and then back to the window 780 [Shortcut("Input Action Editor/Save", typeof(InputActionEditorWindow), KeyCode.S, ShortcutModifiers.Alt)] 781 private static void SaveShortcut(ShortcutArguments arguments) 782 { 783 var window = (InputActionEditorWindow)arguments.context; 784 window.SaveChangesToAsset(); 785 } 786 787 [Shortcut("Input Action Editor/Add Action Map", typeof(InputActionEditorWindow), KeyCode.M, ShortcutModifiers.Alt)] 788 private static void AddActionMapShortcut(ShortcutArguments arguments) 789 { 790 var window = (InputActionEditorWindow)arguments.context; 791 window.AddNewActionMap(); 792 } 793 794 [Shortcut("Input Action Editor/Add Action", typeof(InputActionEditorWindow), KeyCode.A, ShortcutModifiers.Alt)] 795 private static void AddActionShortcut(ShortcutArguments arguments) 796 { 797 var window = (InputActionEditorWindow)arguments.context; 798 window.AddNewAction(); 799 } 800 801 [Shortcut("Input Action Editor/Add Binding", typeof(InputActionEditorWindow), KeyCode.B, ShortcutModifiers.Alt)] 802 private static void AddBindingShortcut(ShortcutArguments arguments) 803 { 804 var window = (InputActionEditorWindow)arguments.context; 805 window.AddNewBinding(); 806 } 807 808#endif 809 810 private void OnDirtyChanged(bool dirty) 811 { 812 titleContent = dirty ? m_DirtyTitle : m_Title; 813 m_Toolbar.isDirty = dirty; 814 } 815 816 public void Dispose() 817 { 818 m_BindingPropertyView?.Dispose(); 819 } 820 821 [SerializeField] private TreeViewState m_ActionMapsTreeState; 822 [SerializeField] private TreeViewState m_ActionsTreeState; 823 [SerializeField] private InputControlPickerState m_ControlPickerViewState; 824 [SerializeField] private InputActionAssetManager m_ActionAssetManager; 825 [SerializeField] private InputActionEditorToolbar m_Toolbar; 826 [SerializeField] private GUIContent m_DirtyTitle; 827 [SerializeField] private GUIContent m_Title; 828 [NonSerialized] private GUIContent m_ActionPropertiesTitle; 829 [NonSerialized] private GUIContent m_BindingPropertiesTitle; 830 831 private InputBindingPropertiesView m_BindingPropertyView; 832 private InputActionPropertiesView m_ActionPropertyView; 833 private InputActionTreeView m_ActionMapsTree; 834 private InputActionTreeView m_ActionsTree; 835 836 private static bool s_RefreshPending; 837 838 private Vector2 m_PropertiesScroll; 839 private bool m_ForceQuit; 840 841 #region IInputActionAssetEditor 842 843 public void OnAssetImported() => ReloadAssetFromFileIfNotDirty(); 844 845 public void OnAssetMoved() => UpdateWindowTitle(); 846 847 public void OnAssetDeleted() 848 { 849 m_ForceQuit = true; 850 Close(); 851 } 852 853 public string assetGUID => m_ActionAssetManager.guid; 854 public bool isDirty => m_ActionAssetManager.dirty; 855 856 #endregion 857 } 858} 859#endif // UNITY_EDITOR