#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEditor.IMGUI.Controls;
using UnityEditor.ShortcutManagement;
////TODO: Add "Revert" button
////TODO: add helpers to very quickly set up certain common configs (e.g. "FPS Controls" in add-action context menu;
//// "WASD Control" in add-binding context menu)
////REVIEW: should we listen for Unity project saves and save dirty .inputactions assets along with it?
////FIXME: when saving, processor/interaction selection is cleared
////TODO: persist view state of asset in Library/ folder
namespace UnityEngine.InputSystem.Editor
{
///
/// An editor window to edit .inputactions assets.
///
///
/// The .inputactions editor code does not really separate between model and view. Selection state is contained
/// in the tree views and persistent across domain reloads via .
///
internal class InputActionEditorWindow : EditorWindow, IDisposable
{
///
/// Open window if someone clicks on an .inputactions asset or an action inside of it.
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "line", Justification = "line parameter required by OnOpenAsset attribute")]
[OnOpenAsset]
public static bool OnOpenAsset(int instanceId, int line)
{
#if UNITY_INPUT_SYSTEM_UI_TK_ASSET_EDITOR
if (InputSystem.settings.IsFeatureEnabled(InputFeatureNames.kUseUIToolkitEditor))
return false;
#endif
var path = AssetDatabase.GetAssetPath(instanceId);
if (!path.EndsWith(k_FileExtension, StringComparison.InvariantCultureIgnoreCase))
return false;
string mapToSelect = null;
string actionToSelect = null;
// Grab InputActionAsset.
// NOTE: We defer checking out an asset until we save it. This allows a user to open an .inputactions asset and look at it
// without forcing a checkout.
var obj = EditorUtility.InstanceIDToObject(instanceId);
var asset = obj as InputActionAsset;
if (asset == null)
{
// Check if the user clicked on an action inside the asset.
var actionReference = obj as InputActionReference;
if (actionReference != null)
{
asset = actionReference.asset;
mapToSelect = actionReference.action.actionMap.name;
actionToSelect = actionReference.action.name;
}
else
return false;
}
var window = OpenEditor(asset);
// If user clicked on an action inside the asset, focus on that action (if we can find it).
if (actionToSelect != null && window.m_ActionMapsTree.TrySelectItem(mapToSelect))
{
window.OnActionMapTreeSelectionChanged();
window.m_ActionsTree.SelectItem(actionToSelect);
}
return true;
}
///
/// Open the specified in an editor window. Used when someone hits the "Edit Asset" button in the
/// importer inspector.
///
/// The InputActionAsset to open.
/// The editor window.
public static InputActionEditorWindow OpenEditor(InputActionAsset asset)
{
////REVIEW: It'd be great if the window got docked by default but the public EditorWindow API doesn't allow that
//// to be done for windows that aren't singletons (GetWindow() will only create one window and it's the
//// only way to get programmatic docking with the current API).
// See if we have an existing editor window that has the asset open.
var window = FindEditorForAsset(asset);
if (window == null)
{
// No, so create a new window.
window = CreateInstance();
window.SetAsset(asset);
}
window.Show();
window.Focus();
return window;
}
public static InputActionEditorWindow FindEditorForAsset(InputActionAsset asset)
{
var windows = Resources.FindObjectsOfTypeAll();
return windows.FirstOrDefault(w => w.m_ActionAssetManager.ImportedAssetObjectEquals(asset));
}
public static InputActionEditorWindow FindEditorForAssetWithGUID(string guid)
{
var windows = Resources.FindObjectsOfTypeAll();
return windows.FirstOrDefault(w => w.m_ActionAssetManager.guid == guid);
}
public static void RefreshAllOnAssetReimport()
{
if (s_RefreshPending)
return;
// We don't want to refresh right away but rather wait for the next editor update
// to then do one pass of refreshing action editor windows.
EditorApplication.delayCall += RefreshAllOnAssetReimportCallback;
s_RefreshPending = true;
}
public void SaveChangesToAsset()
{
m_ActionAssetManager.SaveChangesToAsset();
}
public void AddNewActionMap()
{
m_ActionMapsTree.AddNewActionMap();
}
public void AddNewAction()
{
// Make sure we have an action map. If we don't have an action map selected,
// refuse the operation.
var actionMapItem = m_ActionMapsTree.GetSelectedItems().OfType().FirstOrDefault();
if (actionMapItem == null)
{
EditorApplication.Beep();
return;
}
m_ActionsTree.AddNewAction(actionMapItem.property);
}
public void AddNewBinding()
{
// Make sure we have an action selected.
var actionItems = m_ActionsTree.GetSelectedItems().OfType();
if (!actionItems.Any())
{
EditorApplication.Beep();
return;
}
foreach (var item in actionItems)
m_ActionsTree.AddNewBinding(item.property, item.actionMapProperty);
}
private static void RefreshAllOnAssetReimportCallback()
{
s_RefreshPending = false;
// When the asset is modified outside of the editor
// and the importer settings are visible in the inspector
// the asset references in the importer inspector need to be force rebuild
// (otherwise we gets lots of exceptions)
ActiveEditorTracker.sharedTracker.ForceRebuild();
var windows = Resources.FindObjectsOfTypeAll();
foreach (var window in windows)
window.ReloadAssetFromFileIfNotDirty();
}
private bool ConfirmSaveChangesIfNeeded()
{
// Ask for confirmation if we have unsaved changes.
if (!m_ForceQuit && m_ActionAssetManager.dirty)
{
var result = EditorUtility.DisplayDialogComplex("Input Action Asset has been modified",
$"Do you want to save the changes you made in:\n{m_ActionAssetManager.path}\n\nYour changes will be lost if you don't save them.", "Save", "Cancel", "Don't Save");
switch (result)
{
case 0: // Save
m_ActionAssetManager.SaveChangesToAsset();
m_ActionAssetManager.Cleanup();
break;
case 1: // Cancel
Instantiate(this).Show();
// Cancel editor quit.
return false;
case 2: // Don't save, don't ask again.
m_ForceQuit = true;
break;
}
}
return true;
}
private bool EditorWantsToQuit()
{
return ConfirmSaveChangesIfNeeded();
}
private void OnEnable()
{
minSize = new Vector2(600, 300);
// Initialize toolbar. We keep the toolbar across domain reloads but we
// will lose the delegates.
if (m_Toolbar == null)
m_Toolbar = new InputActionEditorToolbar();
m_Toolbar.onSearchChanged = OnToolbarSearchChanged;
m_Toolbar.onSelectedSchemeChanged = OnControlSchemeSelectionChanged;
m_Toolbar.onSelectedDeviceChanged = OnControlSchemeSelectionChanged;
m_Toolbar.onSave = SaveChangesToAsset;
m_Toolbar.onControlSchemesChanged = OnControlSchemesModified;
m_Toolbar.onControlSchemeRenamed = OnControlSchemeRenamed;
m_Toolbar.onControlSchemeDeleted = OnControlSchemeDeleted;
EditorApplication.wantsToQuit += EditorWantsToQuit;
// Initialize after assembly reload.
if (m_ActionAssetManager != null)
{
if (!m_ActionAssetManager.Initialize())
{
// The asset we want to edit no longer exists.
Close();
return;
}
m_ActionAssetManager.onDirtyChanged = OnDirtyChanged;
InitializeTrees();
}
InputSystem.onSettingsChange += OnInputSettingsChanged;
}
private void OnDestroy()
{
ConfirmSaveChangesIfNeeded();
EditorApplication.wantsToQuit -= EditorWantsToQuit;
InputSystem.onSettingsChange -= OnInputSettingsChanged;
}
private void OnInputSettingsChanged()
{
Repaint();
}
// Set asset would usually only be called when the window is open
private void SetAsset(InputActionAsset asset)
{
if (asset == null)
return;
m_ActionAssetManager = new InputActionAssetManager(asset) {onDirtyChanged = OnDirtyChanged};
m_ActionAssetManager.Initialize();
InitializeTrees();
LoadControlSchemes();
// Select first action map in asset.
m_ActionMapsTree.SelectFirstToplevelItem();
UpdateWindowTitle();
}
private void UpdateWindowTitle()
{
var title = m_ActionAssetManager.name + " (Input Actions)";
m_Title = new GUIContent(title);
m_DirtyTitle = new GUIContent("(*) " + m_Title.text);
titleContent = m_Title;
}
private void LoadControlSchemes()
{
TransferControlSchemes(save: false);
}
private void TransferControlSchemes(bool save)
{
// The easiest way to load and save control schemes is using SerializedProperties to just transfer the data
// between the InputControlScheme array in the toolbar and the one in the asset. Doing it this way rather than
// just overwriting the array in m_AssetManager.m_AssetObjectForEditing directly will make undo work.
using (var editorWindowObject = new SerializedObject(this))
using (var controlSchemesArrayPropertyInWindow = editorWindowObject.FindProperty("m_Toolbar.m_ControlSchemes"))
using (var controlSchemesArrayPropertyInAsset = m_ActionAssetManager.serializedObject.FindProperty("m_ControlSchemes"))
{
Debug.Assert(controlSchemesArrayPropertyInWindow != null, $"Cannot find m_ControlSchemes in window");
Debug.Assert(controlSchemesArrayPropertyInAsset != null, $"Cannot find m_ControlSchemes in asset");
if (save)
{
var json = controlSchemesArrayPropertyInWindow.CopyToJson();
controlSchemesArrayPropertyInAsset.RestoreFromJson(json);
editorWindowObject.ApplyModifiedProperties();
}
else
{
// Load.
var json = controlSchemesArrayPropertyInAsset.CopyToJson();
controlSchemesArrayPropertyInWindow.RestoreFromJson(json);
editorWindowObject.ApplyModifiedPropertiesWithoutUndo();
}
}
}
private void OnControlSchemeSelectionChanged()
{
OnToolbarSearchChanged();
LoadPropertiesForSelection();
}
private void OnControlSchemesModified()
{
TransferControlSchemes(save: true);
// Control scheme changes may affect the search filter.
OnToolbarSearchChanged();
ApplyAndReloadTrees();
}
private void OnControlSchemeRenamed(string oldBindingGroup, string newBindingGroup)
{
InputActionSerializationHelpers.ReplaceBindingGroup(m_ActionAssetManager.serializedObject,
oldBindingGroup, newBindingGroup);
ApplyAndReloadTrees();
}
private void OnControlSchemeDeleted(string name, string bindingGroup)
{
Debug.Assert(!string.IsNullOrEmpty(name), "Control scheme name should not be empty");
Debug.Assert(!string.IsNullOrEmpty(bindingGroup), "Binding group should not be empty");
var asset = m_ActionAssetManager.m_AssetObjectForEditing;
var bindingMask = InputBinding.MaskByGroup(bindingGroup);
var schemeHasBindings = asset.actionMaps.Any(m => m.bindings.Any(b => bindingMask.Matches(ref b)));
if (!schemeHasBindings)
return;
////FIXME: this does not delete composites that have bindings in only one control scheme
////REVIEW: offer to do nothing and leave all bindings as is?
var deleteBindings =
EditorUtility.DisplayDialog("Delete Bindings?",
$"Delete bindings for '{name}' as well? If you select 'No', the bindings will only "
+ $"be unassigned from the '{name}' control scheme but otherwise left as is. Note that bindings "
+ $"that are assigned to '{name}' but also to other control schemes will be left in place either way.",
"Yes", "No");
InputActionSerializationHelpers.ReplaceBindingGroup(m_ActionAssetManager.serializedObject, bindingGroup, "",
deleteOrphanedBindings: deleteBindings);
ApplyAndReloadTrees();
}
private void InitializeTrees()
{
// We persist tree view states (most importantly, they contain our selection states),
// so only create those if we don't have any yet.
if (m_ActionMapsTreeState == null)
m_ActionMapsTreeState = new TreeViewState();
if (m_ActionsTreeState == null)
m_ActionsTreeState = new TreeViewState();
// Create tree in middle pane showing actions and bindings. We initially
// leave this tree empty and populate it by selecting an action map in the
// left pane tree.
m_ActionsTree = new InputActionTreeView(m_ActionAssetManager.serializedObject, m_ActionsTreeState)
{
onSelectionChanged = OnActionTreeSelectionChanged,
onSerializedObjectModified = ApplyAndReloadTrees,
onBindingAdded = p => InputActionSerializationHelpers.RemoveUnusedBindingGroups(p, m_Toolbar.controlSchemes),
drawMinusButton = false,
title = ("Actions", "A list of InputActions in the InputActionMap selected in the left pane. Also, for each InputAction, the list "
+ "of bindings that determine the controls that can trigger the action.\n\nThe name of each action must be unique within its InputActionMap."),
};
// Create tree in left pane showing action maps.
m_ActionMapsTree = new InputActionTreeView(m_ActionAssetManager.serializedObject, m_ActionMapsTreeState)
{
onBuildTree = () =>
InputActionTreeView.BuildWithJustActionMapsFromAsset(m_ActionAssetManager.serializedObject),
onSelectionChanged = OnActionMapTreeSelectionChanged,
onSerializedObjectModified = ApplyAndReloadTrees,
onHandleAddNewAction = m_ActionsTree.AddNewAction,
drawMinusButton = false,
title = ("Action Maps", "A list of InputActionMaps in the asset. Each map can be enabled and disabled separately at runtime and holds "
+ "its own collection of InputActions which are listed in the middle pane (along with their InputBindings).")
};
m_ActionMapsTree.Reload();
m_ActionMapsTree.ExpandAll();
RebuildActionTree();
LoadPropertiesForSelection();
// Sync current search status in toolbar.
OnToolbarSearchChanged();
}
///
/// Synchronize the search filter applied to the trees.
///
///
/// Note that only filter the action tree. The action map tree remains unfiltered.
///
private void OnToolbarSearchChanged()
{
// Rather than adding FilterCriterion instances directly, we go through the
// string-based format here. This allows typing queries directly into the search bar.
var searchStringBuffer = new StringBuilder();
// Plain-text search.
if (!string.IsNullOrEmpty(m_Toolbar.searchText))
searchStringBuffer.Append(m_Toolbar.searchText);
// Filter by binding group of selected control scheme.
if (m_Toolbar.selectedControlScheme != null)
{
searchStringBuffer.Append(" \"");
searchStringBuffer.Append(InputActionTreeView.FilterCriterion.k_BindingGroupTag);
searchStringBuffer.Append(m_Toolbar.selectedControlScheme.Value.bindingGroup);
searchStringBuffer.Append('\"');
}
// Filter by device layout.
if (m_Toolbar.selectedDeviceRequirement != null)
{
searchStringBuffer.Append(" \"");
searchStringBuffer.Append(InputActionTreeView.FilterCriterion.k_DeviceLayoutTag);
searchStringBuffer.Append(InputControlPath.TryGetDeviceLayout(m_Toolbar.selectedDeviceRequirement.Value.controlPath));
searchStringBuffer.Append('\"');
}
var searchString = searchStringBuffer.ToString();
if (string.IsNullOrEmpty(searchString))
m_ActionsTree.ClearItemSearchFilterAndReload();
else
m_ActionsTree.SetItemSearchFilterAndReload(searchStringBuffer.ToString());
// Have trees create new bindings with the right binding group.
var currentBindingGroup = m_Toolbar.selectedControlScheme?.bindingGroup;
m_ActionsTree.bindingGroupForNewBindings = currentBindingGroup;
m_ActionMapsTree.bindingGroupForNewBindings = currentBindingGroup;
}
///
/// Synchronize the display state to the currently selected action map.
///
private void OnActionMapTreeSelectionChanged()
{
// Re-configure action tree (middle pane) for currently select action map.
RebuildActionTree();
// If there's no actions in the selected action map or if there is no action map
// selected, make sure we wipe the property pane.
if (!m_ActionMapsTree.HasSelection() || !m_ActionsTree.rootItem.hasChildren)
{
LoadPropertiesForSelection();
}
else
{
// Otherwise select first action in map.
m_ActionsTree.SelectFirstToplevelItem();
}
}
private void RebuildActionTree()
{
var selectedActionMapItem =
m_ActionMapsTree.GetSelectedItems().OfType().FirstOrDefault();
if (selectedActionMapItem == null)
{
// Nothing selected. Wipe middle and right pane.
m_ActionsTree.onBuildTree = null;
}
else
{
m_ActionsTree.onBuildTree = () =>
InputActionTreeView.BuildWithJustActionsAndBindingsFromMap(selectedActionMapItem.property);
}
// Rebuild tree.
m_ActionsTree.Reload();
}
private void OnActionTreeSelectionChanged()
{
LoadPropertiesForSelection();
}
private void LoadPropertiesForSelection()
{
m_BindingPropertyView = null;
m_ActionPropertyView = null;
////TODO: preserve interaction/processor selection when reloading
// Nothing else to do if we don't have a selection in the middle pane or if
// multiple items are selected (we don't currently have the ability to multi-edit).
if (!m_ActionsTree.HasSelection() || m_ActionsTree.GetSelection().Count != 1)
return;
var item = m_ActionsTree.GetSelectedItems().FirstOrDefault();
if (item is BindingTreeItem)
{
// Grab the action for the binding and see if we have an expected control layout
// set on it. Pass that on to the control picking machinery.
var isCompositePartBinding = item is PartOfCompositeBindingTreeItem;
var actionItem = (isCompositePartBinding ? item.parent.parent : item.parent) as ActionTreeItem;
Debug.Assert(actionItem != null);
if (m_ControlPickerViewState == null)
m_ControlPickerViewState = new InputControlPickerState();
// The toolbar may constrain the set of devices we're currently interested in by either
// having one specific device selected from the current scheme or having at least a control
// scheme selected.
IEnumerable controlPathsToMatch;
if (m_Toolbar.selectedDeviceRequirement != null)
{
// Single device selected from set of devices in control scheme.
controlPathsToMatch = new[] {m_Toolbar.selectedDeviceRequirement.Value.controlPath};
}
else if (m_Toolbar.selectedControlScheme != null)
{
// Constrain to devices from current control scheme.
controlPathsToMatch =
m_Toolbar.selectedControlScheme.Value.deviceRequirements.Select(x => x.controlPath);
}
else
{
// If there's no device filter coming from a control scheme, filter by supported
// devices as given by settings.
controlPathsToMatch = InputSystem.settings.supportedDevices.Select(x => $"<{x}>");
}
// Show properties for binding.
m_BindingPropertyView =
new InputBindingPropertiesView(
item.property,
change =>
{
if (change == InputBindingPropertiesView.k_PathChanged ||
change == InputBindingPropertiesView.k_CompositePartAssignmentChanged ||
change == InputBindingPropertiesView.k_CompositeTypeChanged ||
change == InputBindingPropertiesView.k_GroupsChanged)
{
ApplyAndReloadTrees();
}
else
{
// Simple property change that doesn't affect the rest of the UI.
Apply();
}
},
m_ControlPickerViewState,
expectedControlLayout: item.expectedControlLayout,
controlSchemes: m_Toolbar.controlSchemes,
controlPathsToMatch: controlPathsToMatch);
}
else if (item is ActionTreeItem actionItem)
{
// Show properties for action.
m_ActionPropertyView =
new InputActionPropertiesView(
actionItem.property,
// Apply without reload is enough here as modifying the properties of an action will
// never change the structure of the data.
change => Apply());
}
}
private void ApplyAndReloadTrees()
{
Apply();
// This path here is meant to catch *any* edits made to the serialized data. I.e. also
// any arbitrary undo that may have changed some misc bit not visible in the trees.
m_ActionMapsTree.Reload();
RebuildActionTree();
m_ActionAssetManager.UpdateAssetDirtyState();
LoadControlSchemes();
LoadPropertiesForSelection();
}
private void Apply()
{
m_ActionAssetManager.ApplyChanges();
// Update dirty count, otherwise ReloadIfSerializedObjectHasBeenChanged will trigger a full ApplyAndReloadTrees
m_ActionMapsTree.UpdateSerializedObjectDirtyCount();
m_ActionsTree.UpdateSerializedObjectDirtyCount();
// If auto-save is active, immediately flush out the changes to disk. Otherwise just
// put us into dirty state.
if (InputEditorUserSettings.autoSaveInputActionAssets)
{
m_ActionAssetManager.SaveChangesToAsset();
}
else
{
m_ActionAssetManager.SetAssetDirty();
titleContent = m_DirtyTitle;
}
}
private void OnGUI()
{
// If the actions tree has lost the filters (because they would not match an item it tried to highlight),
// update the Toolbar UI to remove them.
if (!m_ActionsTree.hasFilter)
m_Toolbar.ResetSearchFilters();
// Allow switching between action map tree and action tree using arrow keys.
ToggleFocusUsingKeyboard(KeyCode.RightArrow, m_ActionMapsTree, m_ActionsTree);
ToggleFocusUsingKeyboard(KeyCode.LeftArrow, m_ActionsTree, m_ActionMapsTree);
// Route copy-paste events to tree views if they have focus.
if (m_ActionsTree.HasFocus())
m_ActionsTree.HandleCopyPasteCommandEvent(Event.current);
else if (m_ActionMapsTree.HasFocus())
m_ActionMapsTree.HandleCopyPasteCommandEvent(Event.current);
// Draw toolbar.
EditorGUILayout.BeginVertical();
m_Toolbar.OnGUI();
EditorGUILayout.Space();
// Draw columns.
EditorGUILayout.BeginHorizontal();
var columnAreaWidth = position.width - InputActionTreeView.Styles.backgroundWithBorder.margin.left -
InputActionTreeView.Styles.backgroundWithBorder.margin.left -
InputActionTreeView.Styles.backgroundWithBorder.margin.right;
var oldType = Event.current.type;
DrawActionMapsColumn(columnAreaWidth * 0.22f);
if (Event.current.type == EventType.Used && oldType != Event.current.type)
{
// When renaming an item, TreeViews will capture all mouse Events, and process any clicks outside the item
// being renamed to end the renaming process. However, since we have two TreeViews, if the action column is
// renaming an item, and then you double click on an item in the action map column, the action map column will
// get to use the mouse event before the action collumn gets to see it, which would cause the action map column
// to enter rename mode and use the event, before the action column gets a chance to see it and exit rename mode.
// Then we end up with two active renaming sessions, which does not work correctly.
// (See https://fogbugz.unity3d.com/f/cases/1140869/).
// Now, our fix to this problem is to force-end and accept any renaming session on the action column if we see
// that the action map column had processed the current event. This is not particularly elegant, but I cannot think
// of a better solution as we are limited by the public APIs exposed by TreeView.
m_ActionsTree.EndRename(forceAccept: true);
}
DrawActionsColumn(columnAreaWidth * 0.38f);
DrawPropertiesColumn(columnAreaWidth * 0.40f);
EditorGUILayout.EndHorizontal();
// Bottom margin.
GUILayout.Space(3);
EditorGUILayout.EndVertical();
}
private static void ToggleFocusUsingKeyboard(KeyCode key, InputActionTreeView fromTree,
InputActionTreeView toTree)
{
var uiEvent = Event.current;
if (uiEvent.type == EventType.KeyDown && uiEvent.keyCode == key && fromTree.HasFocus())
{
if (!toTree.HasSelection())
toTree.SelectFirstToplevelItem();
toTree.SetFocus();
uiEvent.Use();
}
}
private void DrawActionMapsColumn(float width)
{
DrawColumnWithTreeView(m_ActionMapsTree, width, true);
}
private void DrawActionsColumn(float width)
{
DrawColumnWithTreeView(m_ActionsTree, width, false);
}
private static void DrawColumnWithTreeView(TreeView treeView, float width, bool fixedWidth)
{
EditorGUILayout.BeginVertical(InputActionTreeView.Styles.backgroundWithBorder,
fixedWidth ? GUILayout.MaxWidth(width) : GUILayout.MinWidth(width),
GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(true));
GUILayout.FlexibleSpace();
EditorGUILayout.EndVertical();
var columnRect = GUILayoutUtility.GetLastRect();
treeView.OnGUI(columnRect);
}
private void DrawPropertiesColumn(float width)
{
EditorGUILayout.BeginVertical(InputActionTreeView.Styles.backgroundWithBorder, GUILayout.Width(width));
var rect = GUILayoutUtility.GetRect(0,
EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing * 2,
GUILayout.ExpandWidth(true));
rect.x -= 2;
rect.y -= 1;
rect.width += 4;
EditorGUI.LabelField(rect, GUIContent.none, InputActionTreeView.Styles.backgroundWithBorder);
var headerRect = new Rect(rect.x + 1, rect.y + 1, rect.width - 2, rect.height - 2);
if (m_BindingPropertyView != null)
{
if (m_BindingPropertiesTitle == null)
m_BindingPropertiesTitle = new GUIContent("Binding Properties", "The properties for the InputBinding selected in the "
+ "'Actions' pane on the left.");
EditorGUI.LabelField(headerRect, m_BindingPropertiesTitle, InputActionTreeView.Styles.columnHeaderLabel);
m_PropertiesScroll = EditorGUILayout.BeginScrollView(m_PropertiesScroll);
m_BindingPropertyView.OnGUI();
EditorGUILayout.EndScrollView();
}
else if (m_ActionPropertyView != null)
{
if (m_ActionPropertiesTitle == null)
m_ActionPropertiesTitle = new GUIContent("Action Properties", "The properties for the InputAction selected in the "
+ "'Actions' pane on the left.");
EditorGUI.LabelField(headerRect, m_ActionPropertiesTitle, InputActionTreeView.Styles.columnHeaderLabel);
m_PropertiesScroll = EditorGUILayout.BeginScrollView(m_PropertiesScroll);
m_ActionPropertyView.OnGUI();
EditorGUILayout.EndScrollView();
}
else
{
GUILayout.FlexibleSpace();
}
EditorGUILayout.EndVertical();
}
private void ReloadAssetFromFileIfNotDirty()
{
if (m_ActionAssetManager.dirty)
return;
// If our asset has disappeared from disk, just close the window.
if (string.IsNullOrEmpty(AssetDatabase.GUIDToAssetPath(m_ActionAssetManager.guid)))
{
Close();
return;
}
// Don't touch the UI state if the serialized data is still the same.
if (!m_ActionAssetManager.ReInitializeIfAssetHasChanged())
return;
// Unfortunately, on this path we lose the selection state of the interactions and processors lists
// in the properties view.
InitializeTrees();
LoadPropertiesForSelection();
Repaint();
}
////TODO: add shortcut to focus search box
////TODO: show shortcuts in tooltips
////FIXME: the shortcuts seem to have focus problems; often requires clicking away and then back to the window
[Shortcut("Input Action Editor/Save", typeof(InputActionEditorWindow), KeyCode.S, ShortcutModifiers.Alt)]
private static void SaveShortcut(ShortcutArguments arguments)
{
var window = (InputActionEditorWindow)arguments.context;
window.SaveChangesToAsset();
}
[Shortcut("Input Action Editor/Add Action Map", typeof(InputActionEditorWindow), KeyCode.M, ShortcutModifiers.Alt)]
private static void AddActionMapShortcut(ShortcutArguments arguments)
{
var window = (InputActionEditorWindow)arguments.context;
window.AddNewActionMap();
}
[Shortcut("Input Action Editor/Add Action", typeof(InputActionEditorWindow), KeyCode.A, ShortcutModifiers.Alt)]
private static void AddActionShortcut(ShortcutArguments arguments)
{
var window = (InputActionEditorWindow)arguments.context;
window.AddNewAction();
}
[Shortcut("Input Action Editor/Add Binding", typeof(InputActionEditorWindow), KeyCode.B, ShortcutModifiers.Alt)]
private static void AddBindingShortcut(ShortcutArguments arguments)
{
var window = (InputActionEditorWindow)arguments.context;
window.AddNewBinding();
}
private void OnDirtyChanged(bool dirty)
{
titleContent = dirty ? m_DirtyTitle : m_Title;
m_Toolbar.isDirty = dirty;
}
public void Dispose()
{
m_BindingPropertyView?.Dispose();
}
[SerializeField] private TreeViewState m_ActionMapsTreeState;
[SerializeField] private TreeViewState m_ActionsTreeState;
[SerializeField] private InputControlPickerState m_ControlPickerViewState;
[SerializeField] private InputActionAssetManager m_ActionAssetManager;
[SerializeField] private InputActionEditorToolbar m_Toolbar;
[SerializeField] private GUIContent m_DirtyTitle;
[SerializeField] private GUIContent m_Title;
[NonSerialized] private GUIContent m_ActionPropertiesTitle;
[NonSerialized] private GUIContent m_BindingPropertiesTitle;
private InputBindingPropertiesView m_BindingPropertyView;
private InputActionPropertiesView m_ActionPropertyView;
private InputActionTreeView m_ActionMapsTree;
private InputActionTreeView m_ActionsTree;
private static bool s_RefreshPending;
private static readonly string k_FileExtension = "." + InputActionAsset.Extension;
private Vector2 m_PropertiesScroll;
private bool m_ForceQuit;
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "Intantiated through reflection by Unity")]
private class ProcessAssetModifications : UnityEditor.AssetModificationProcessor
{
// Handle .inputactions asset being deleted.
// ReSharper disable once UnusedMember.Local
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "options", Justification = "options parameter required by Unity API")]
public static AssetDeleteResult OnWillDeleteAsset(string path, RemoveAssetOptions options)
{
if (!path.EndsWith(k_FileExtension, StringComparison.InvariantCultureIgnoreCase))
return default;
// See if we have an open window.
var guid = AssetDatabase.AssetPathToGUID(path);
var window = FindEditorForAssetWithGUID(guid);
if (window != null)
{
// If there's unsaved changes, ask for confirmation.
if (window.m_ActionAssetManager.dirty)
{
var result = EditorUtility.DisplayDialog("Unsaved changes",
$"You have unsaved changes for '{path}'. Do you want to discard the changes and delete the asset?",
"Yes, Delete", "No, Cancel");
if (!result)
{
// User canceled. Stop the deletion.
return AssetDeleteResult.FailedDelete;
}
window.m_ForceQuit = true;
}
window.Close();
}
return default;
}
#pragma warning disable CA1801 // unused parameters
// Handle .inputactions asset being moved.
// ReSharper disable once UnusedMember.Local
public static AssetMoveResult OnWillMoveAsset(string sourcePath, string destinationPath)
{
if (!sourcePath.EndsWith(k_FileExtension, StringComparison.InvariantCultureIgnoreCase))
return default;
var guid = AssetDatabase.AssetPathToGUID(sourcePath);
var window = FindEditorForAssetWithGUID(guid);
if (window != null)
{
window.UpdateWindowTitle();
}
return default;
}
#pragma warning restore CA1801
}
}
}
#endif // UNITY_EDITOR