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