first commit

This commit is contained in:
lethanhsonvsp
2025-11-17 15:16:36 +07:00
commit a40d0921eb
17012 changed files with 2652386 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 665321577aaff5344b5a12d626dfcaf7
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,701 @@
#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine.InputSystem.Editor.Lists;
using UnityEngine.InputSystem.Utilities;
namespace UnityEngine.InputSystem.Editor
{
internal delegate InputActionsEditorState Command(in InputActionsEditorState state);
internal static class Commands
{
public static Command SelectAction(string actionName)
{
return (in InputActionsEditorState state) => state.SelectAction(actionName);
}
public static Command SelectAction(int index)
{
return (in InputActionsEditorState state) => state.SelectAction(index);
}
public static Command SelectActionMap(string actionMapName)
{
return (in InputActionsEditorState state) => state.SelectActionMap(actionMapName);
}
public static Command AddActionMap()
{
return (in InputActionsEditorState state) =>
{
var newMap = InputActionSerializationHelpers.AddActionMap(state.serializedObject);
var actionProperty = InputActionSerializationHelpers.AddAction(newMap);
InputActionSerializationHelpers.AddBinding(actionProperty, newMap);
state.serializedObject.ApplyModifiedProperties();
state.m_Analytics?.RegisterActionMapEdit();
return state.SelectActionMap(newMap);
};
}
public static Command AddAction()
{
return (in InputActionsEditorState state) =>
{
var actionMap = Selectors.GetSelectedActionMap(state)?.wrappedProperty;
if (actionMap == null)
{
Debug.LogError("Cannot add action without an action map selected");
return state;
}
var newAction = InputActionSerializationHelpers.AddAction(actionMap);
InputActionSerializationHelpers.AddBinding(newAction, actionMap);
state.serializedObject.ApplyModifiedProperties();
state.m_Analytics?.RegisterActionEdit();
return state.SelectAction(newAction);
};
}
public static Command AddBinding()
{
return (in InputActionsEditorState state) =>
{
var action = Selectors.GetSelectedAction(state)?.wrappedProperty;
var map = Selectors.GetSelectedActionMap(state)?.wrappedProperty;
if (action == null || map == null)
{
Debug.LogError("Cannot add binding without an action and action map selected");
return state;
}
var binding = InputActionSerializationHelpers.AddBinding(action, map);
var bindingIndex = new SerializedInputBinding(binding).indexOfBinding;
state.serializedObject.ApplyModifiedProperties();
state.m_Analytics?.RegisterBindingEdit();
return state.With(selectedBindingIndex: bindingIndex, selectionType: SelectionType.Binding);
};
}
public static Command AddComposite(string compositeName)
{
return (in InputActionsEditorState state) =>
{
var action = Selectors.GetSelectedAction(state)?.wrappedProperty;
var map = Selectors.GetSelectedActionMap(state)?.wrappedProperty;
var compositeType = InputBindingComposite.s_Composites.LookupTypeRegistration(compositeName);
var composite = InputActionSerializationHelpers.AddCompositeBinding(action, map, compositeName, compositeType);
var index = new SerializedInputBinding(composite).indexOfBinding;
state.serializedObject.ApplyModifiedProperties();
state.m_Analytics?.RegisterBindingEdit();
return state.With(selectedBindingIndex: index, selectionType: SelectionType.Binding);
};
}
public static Command DeleteActionMap(int actionMapIndex)
{
return (in InputActionsEditorState state) =>
{
var actionMap = Selectors.GetActionMapAtIndex(state, actionMapIndex)?.wrappedProperty;
var actionMapID = InputActionSerializationHelpers.GetId(actionMap);
var isCut = state.IsActionMapCut(actionMapIndex);
InputActionSerializationHelpers.DeleteActionMap(state.serializedObject, actionMapID);
state.serializedObject.ApplyModifiedProperties();
state.m_Analytics?.RegisterActionMapEdit();
if (state.selectedActionMapIndex == actionMapIndex)
return isCut ? SelectPrevActionMap(state).ClearCutElements() : SelectPrevActionMap(state);
if (isCut)
return state.SelectActionMap(state.selectedActionMapIndex > actionMapIndex ? state.selectedActionMapIndex - 1 : state.selectedActionMapIndex).ClearCutElements();
return state.SelectActionMap(state.selectedActionMapIndex > actionMapIndex ? state.selectedActionMapIndex - 1 : state.selectedActionMapIndex);
};
}
public static Command CopyActionMapSelection()
{
return (in InputActionsEditorState state) =>
{
CopyPasteHelper.CopyActionMap(state);
return state.ClearCutElements();
};
}
public static Command CutActionMapSelection()
{
return (in InputActionsEditorState state) =>
{
CopyPasteHelper.CutActionMap(state);
return state.CutActionMaps();
};
}
public static Command CopyActionBindingSelection()
{
return (in InputActionsEditorState state) =>
{
CopyPasteHelper.Copy(state);
return state.ClearCutElements();
};
}
public static Command CutActionsOrBindings()
{
return (in InputActionsEditorState state) =>
{
CopyPasteHelper.Cut(state);
return state.CutActionOrBinding();
};
}
public static Command PasteActionMaps(IReadOnlyList<IPasteListener> pasteListeners)
{
return (in InputActionsEditorState state) =>
{
var newIndex = -99;
if (state.hasCutElements)
newIndex = CopyPasteHelper.DeleteCutElements(state);
else
{
foreach (var pasteListener in pasteListeners)
pasteListener.OnPaste(state);
}
var lastPastedElement = CopyPasteHelper.PasteActionMapsFromClipboard(state.With(selectedActionMapIndex: newIndex >= -1 ? newIndex : state.selectedActionMapIndex));
if (lastPastedElement != null)
{
state.serializedObject.ApplyModifiedProperties();
return state.With(selectedActionMapIndex: lastPastedElement.GetIndexOfArrayElement()).ClearCutElements();
}
return state.ClearCutElements();
};
}
public static Command DeleteCutElements()
{
return (in InputActionsEditorState state) =>
{
if (!state.hasCutElements)
return state;
CopyPasteHelper.DeleteCutElements(state);
state.serializedObject.ApplyModifiedProperties();
return state.ClearCutElements();
};
}
public static Command PasteActionIntoActionMap(int actionMapIndex)
{
return (in InputActionsEditorState state) =>
{
CopyPasteHelper.Copy(state);
var action = Selectors.GetSelectedAction(state);
var actionMap = Selectors.GetActionMapForAction(state, action?.id);
var isCut = action.HasValue && state.IsActionCut(actionMap.GetIndexOfArrayElement(),
action.Value.wrappedProperty.GetIndexOfArrayElement());
InputActionSerializationHelpers.DeleteActionAndBindings(actionMap, InputActionSerializationHelpers.GetId(action?.wrappedProperty));
var lastPastedElement = CopyPasteHelper.PasteActionsOrBindingsFromClipboard(state, true, actionMapIndex);
if (lastPastedElement != null)
state.serializedObject.ApplyModifiedProperties();
EditorHelpers.SetSystemCopyBufferContents(string.Empty);
if (isCut)
return state.ClearCutElements();
return state;
};
}
public static Command PasteActionFromActionMap(List<IPasteListener> pasteListeners)
{
return (in InputActionsEditorState state) =>
{
var newIndex = -1;
if (state.hasCutElements)
newIndex = CopyPasteHelper.DeleteCutElements(state);
else
{
foreach (var pasteListener in pasteListeners)
pasteListener.OnPaste(state);
}
var lastPastedElement = CopyPasteHelper.PasteActionsOrBindingsFromClipboard(state.With(selectedActionIndex: newIndex >= 0 ? newIndex : state.selectedActionIndex), true);
if (lastPastedElement != null)
{
state.serializedObject.ApplyModifiedProperties();
return state.With(selectedActionIndex: lastPastedElement.GetIndexOfArrayElement(), selectionType: SelectionType.Action).ClearCutElements();
}
return state.ClearCutElements();
};
}
public static Command PasteActionsOrBindings(List<IPasteListener> pasteListeners)
{
return (in InputActionsEditorState state) =>
{
var typeOfCopiedData = CopyPasteHelper.GetCopiedClipboardType();
SerializedInputAction? relatedAction = null;
if (state.selectionType == SelectionType.Binding)
relatedAction = Selectors.GetRelatedInputAction(state);
var newIndex = -1;
if (state.hasCutElements)
newIndex = CopyPasteHelper.DeleteCutElements(state);
else
{
foreach (var pasteListener in pasteListeners)
pasteListener.OnPaste(state);
}
SerializedProperty lastPastedElement = null;
if (state.selectionType == SelectionType.Action)
{
var actionMap = Selectors.GetSelectedActionMap(state)?.wrappedProperty;
var actions = Selectors.GetActionCount(actionMap);
if (actions.HasValue && actions.Value > 0)
lastPastedElement = CopyPasteHelper.PasteActionsOrBindingsFromClipboard(
state.With(selectedActionIndex: newIndex >= 0 ? newIndex : state.selectedActionIndex),
typeOfCopiedData == typeof(InputBinding));
else
{
lastPastedElement =
CopyPasteHelper.PasteActionsOrBindingsFromClipboard(
state.With(selectedActionMapIndex: actionMap.GetIndexOfArrayElement()), addLast: true);
}
}
else if (state.selectionType == SelectionType.Binding)
{
if (relatedAction != null)
{
var bindings = Selectors.GetBindingsForAction(relatedAction.Value.name, state);
if (bindings.Count == 0) //add cutted binding into action instead if there are no bindings left for the action
lastPastedElement = CopyPasteHelper.PasteActionsOrBindingsFromClipboard(state.With(selectedActionIndex: relatedAction.Value.wrappedProperty.GetIndexOfArrayElement(), selectionType: SelectionType.Action));
else
lastPastedElement = CopyPasteHelper.PasteActionsOrBindingsFromClipboard(state.With(selectedBindingIndex: newIndex >= 0 ? newIndex : state.selectedBindingIndex));
lastPastedElement.FindPropertyRelative("m_Action").stringValue = relatedAction.Value.name;
}
}
if (lastPastedElement != null)
{
state.serializedObject.ApplyModifiedProperties();
if (typeOfCopiedData == typeof(InputAction))
return state.With(selectedActionIndex: lastPastedElement.GetIndexOfArrayElement()).ClearCutElements();
if (typeOfCopiedData == typeof(InputBinding))
return state.With(selectedBindingIndex: lastPastedElement.GetIndexOfArrayElement()).ClearCutElements();
}
return state.ClearCutElements();
};
}
public static Command DuplicateActionMap(int actionMapIndex)
{
return (in InputActionsEditorState state) =>
{
var actionMapArray = state.serializedObject.FindProperty(nameof(InputActionAsset.m_ActionMaps));
var actionMap = Selectors.GetActionMapAtIndex(state, actionMapIndex)?.wrappedProperty;
var name = actionMap?.FindPropertyRelative(nameof(InputAction.m_Name)).stringValue;
var newMap = CopyPasteHelper.DuplicateElement(actionMapArray, actionMap, name, actionMap.GetIndexOfArrayElement() + 1);
state.serializedObject.ApplyModifiedProperties();
state.m_Analytics?.RegisterActionMapEdit();
return state.SelectActionMap(newMap.FindPropertyRelative(nameof(InputAction.m_Name)).stringValue);
};
}
public static Command DuplicateAction()
{
return (in InputActionsEditorState state) =>
{
var action = Selectors.GetSelectedAction(state)?.wrappedProperty;
var actionMap = Selectors.GetActionMapAtIndex(state, state.selectedActionMapIndex)?.wrappedProperty;
var actionArray = actionMap?.FindPropertyRelative(nameof(InputActionMap.m_Actions));
CopyPasteHelper.DuplicateAction(actionArray, action, actionMap, state);
state.serializedObject.ApplyModifiedProperties();
state.m_Analytics?.RegisterActionEdit();
return state.SelectAction(state.selectedActionIndex + 1);
};
}
public static Command DuplicateBinding()
{
return (in InputActionsEditorState state) =>
{
var binding = Selectors.GetSelectedBinding(state)?.wrappedProperty;
var actionName = binding?.FindPropertyRelative("m_Action").stringValue;
var actionMap = Selectors.GetActionMapAtIndex(state, state.selectedActionMapIndex)?.wrappedProperty;
var bindingsArray = actionMap?.FindPropertyRelative(nameof(InputActionMap.m_Bindings));
var newIndex = CopyPasteHelper.DuplicateBinding(bindingsArray, binding, actionName, binding.GetIndexOfArrayElement() + 1);
state.serializedObject.ApplyModifiedProperties();
state.m_Analytics?.RegisterBindingEdit();
return state.SelectBinding(newIndex);
};
}
private static InputActionsEditorState SelectPrevActionMap(InputActionsEditorState state)
{
var count = Selectors.GetActionMapCount(state);
var index = 0;
if (count != null && count.Value > 0)
index = Math.Max(state.selectedActionMapIndex - 1, 0);
return state.SelectActionMap(index);
}
public static Command ReorderActionMap(int oldIndex, int newIndex)
{
return (in InputActionsEditorState state) =>
{
InputActionSerializationHelpers.MoveActionMap(state.serializedObject, oldIndex, newIndex);
state.serializedObject.ApplyModifiedProperties();
return state.SelectActionMap(newIndex);
};
}
public static Command MoveAction(int oldIndex, int newIndex)
{
return (in InputActionsEditorState state) =>
{
var actionMap = Selectors.GetSelectedActionMap(state)?.wrappedProperty;
InputActionSerializationHelpers.MoveAction(actionMap, oldIndex, newIndex);
state.serializedObject.ApplyModifiedProperties();
return state.SelectAction(newIndex);
};
}
public static Command MoveBinding(int oldIndex, int actionIndex, int childIndex)
{
return (in InputActionsEditorState state) =>
{
var newBindingIndex = MoveBindingOrComposite(state, oldIndex, actionIndex, childIndex);
state.serializedObject.ApplyModifiedProperties();
return state.SelectBinding(newBindingIndex);
};
}
public static Command MoveComposite(int oldIndex, int actionIndex, int childIndex)
{
return (in InputActionsEditorState state) =>
{
var actionMap = Selectors.GetSelectedActionMap(state)?.wrappedProperty;
var compositeBindings = CopyPasteHelper.GetBindingsForComposite(actionMap?.FindPropertyRelative(nameof(InputActionMap.m_Bindings)), oldIndex);
//move the composite element
var newBindingIndex = MoveBindingOrComposite(state, oldIndex, actionIndex, childIndex);
var actionTo = Selectors.GetActionForIndex(actionMap, actionIndex).FindPropertyRelative(nameof(InputAction.m_Name)).stringValue;
var toIndex = newBindingIndex;
foreach (var compositePart in compositeBindings)
{
// the index of the composite part stays the same if composite was moved down as previous elements are shifted down (the index seems to update async so it's safer to use the oldIndex)
// if the composite was moved up, the index of the composite part is not changing so we are safe to use it
var from = oldIndex < newBindingIndex ? oldIndex : compositePart.GetIndexOfArrayElement();
// if added below the old position the array changes as composite parts are added on top (increase the index)
// if added above the oldIndex, the index does not change
var to = oldIndex < newBindingIndex ? newBindingIndex : ++toIndex;
InputActionSerializationHelpers.MoveBinding(actionMap, from, to);
Selectors.GetCompositeOrBindingInMap(actionMap, to).wrappedProperty.FindPropertyRelative("m_Action").stringValue = actionTo;
}
state.m_Analytics?.RegisterBindingEdit();
state.serializedObject.ApplyModifiedProperties();
return state.SelectBinding(newBindingIndex);
};
}
private static int MoveBindingOrComposite(InputActionsEditorState state, int oldIndex, int actionIndex, int childIndex)
{
var actionMap = Selectors.GetSelectedActionMap(state)?.wrappedProperty;
var bindingsForAction = Selectors.GetBindingsForAction(state, actionMap, actionIndex);
var allBindings = actionMap?.FindPropertyRelative(nameof(InputActionMap.m_Bindings));
var actionTo = Selectors.GetActionForIndex(actionMap, actionIndex).FindPropertyRelative(nameof(InputAction.m_Name)).stringValue;
var actionFrom = Selectors.GetCompositeOrBindingInMap(actionMap, oldIndex).wrappedProperty.FindPropertyRelative("m_Action");
int newBindingIndex;
if (bindingsForAction.Count == 0) //if there are no bindings for an action retrieve the first binding index of a binding before (iterate previous actions)
newBindingIndex = Selectors.GetBindingIndexBeforeAction(allBindings, actionIndex, allBindings);
else
{
var toSkip = GetNumberOfCompositePartItemsToSkip(bindingsForAction, childIndex, oldIndex); //skip composite parts if there are - avoid moving into a composite
newBindingIndex = bindingsForAction[0].GetIndexOfArrayElement() + Math.Clamp(childIndex + toSkip, 0, bindingsForAction.Count);
newBindingIndex -= newBindingIndex > oldIndex && !actionTo.Equals(actionFrom.stringValue) ? 1 : 0; // reduce index by one in case the moved binding will be shifted underneath to another action
}
state.m_Analytics?.RegisterBindingEdit();
actionFrom.stringValue = actionTo;
InputActionSerializationHelpers.MoveBinding(actionMap, oldIndex, newBindingIndex);
return newBindingIndex;
}
private static int GetNumberOfCompositePartItemsToSkip(List<SerializedProperty> bindings, int childIndex, int oldIndex)
{
var toSkip = 0;
var normalBindings = 0;
foreach (var binding in bindings)
{
if (binding.GetIndexOfArrayElement() == oldIndex)
continue;
if (normalBindings > childIndex)
break;
if (binding.FindPropertyRelative(nameof(InputBinding.m_Flags)).intValue ==
(int)InputBinding.Flags.PartOfComposite)
toSkip++;
else
normalBindings++;
}
return toSkip;
}
public static Command MovePartOfComposite(int oldIndex, int newIndex, int compositeIndex)
{
return (in InputActionsEditorState state) =>
{
var actionMap = Selectors.GetSelectedActionMap(state)?.wrappedProperty;
var actionTo = actionMap?.FindPropertyRelative(nameof(InputActionMap.m_Bindings)).GetArrayElementAtIndex(compositeIndex).FindPropertyRelative("m_Action").stringValue;
InputActionSerializationHelpers.MoveBinding(actionMap, oldIndex, newIndex);
Selectors.GetCompositeOrBindingInMap(actionMap, newIndex).wrappedProperty.FindPropertyRelative("m_Action").stringValue = actionTo;
state.m_Analytics?.RegisterBindingEdit();
state.serializedObject.ApplyModifiedProperties();
return state.SelectBinding(newIndex);
};
}
public static Command DeleteAction(int actionMapIndex, string actionName)
{
return (in InputActionsEditorState state) =>
{
var actionMap = Selectors.GetActionMapAtIndex(state, actionMapIndex)?.wrappedProperty;
var action = Selectors.GetActionInMap(state, actionMapIndex, actionName).wrappedProperty;
var actionIndex = action.GetIndexOfArrayElement();
var actionID = InputActionSerializationHelpers.GetId(action);
var isCut = state.IsActionCut(actionMapIndex, actionIndex);
InputActionSerializationHelpers.DeleteActionAndBindings(actionMap, actionID);
state.serializedObject.ApplyModifiedProperties();
state.m_Analytics?.RegisterActionEdit();
if (isCut)
return state.With(selectedActionIndex: -1, selectionType: SelectionType.Action).ClearCutElements();
return state.With(selectedActionIndex: -1, selectionType: SelectionType.Action); // ActionsTreeView will dispatch a separate command to select the previous Action
};
}
public static Command DeleteBinding(int actionMapIndex, int bindingIndex)
{
return (in InputActionsEditorState state) =>
{
var actionMap = Selectors.GetActionMapAtIndex(state, actionMapIndex)?.wrappedProperty;
var binding = Selectors.GetCompositeOrBindingInMap(actionMap, bindingIndex).wrappedProperty;
var isCut = state.IsBindingCut(actionMapIndex, bindingIndex);
InputActionSerializationHelpers.DeleteBinding(binding, actionMap);
state.serializedObject.ApplyModifiedProperties();
state.m_Analytics?.RegisterBindingEdit();
if (isCut)
return state.With(selectedBindingIndex: -1, selectionType: SelectionType.Binding).ClearCutElements();
return state.With(selectedBindingIndex: -1, selectionType: SelectionType.Binding); // ActionsTreeView will dispatch a separate command to select the previous Binding
};
}
public static Command SelectBinding(int bindingIndex)
{
return (in InputActionsEditorState state) =>
state.With(selectedBindingIndex: bindingIndex, selectionType: SelectionType.Binding);
}
public static Command UpdatePathNameAndValues(NamedValue[] parameters, SerializedProperty pathProperty)
{
return (in InputActionsEditorState state) =>
{
var path = pathProperty.stringValue;
var nameAndParameters = NameAndParameters.Parse(path);
nameAndParameters.parameters = parameters;
pathProperty.stringValue = nameAndParameters.ToString();
state.serializedObject.ApplyModifiedProperties();
state.m_Analytics?.RegisterBindingEdit();
return state;
};
}
public static Command SetCompositeBindingType(SerializedInputBinding bindingProperty, IEnumerable<string> compositeTypes,
ParameterListView parameterListView, int selectedCompositeTypeIndex)
{
return (in InputActionsEditorState state) =>
{
var nameAndParameters = new NameAndParameters
{
name = compositeTypes.ElementAt(selectedCompositeTypeIndex),
parameters = parameterListView.GetParameters()
};
InputActionSerializationHelpers.ChangeCompositeBindingType(bindingProperty.wrappedProperty, nameAndParameters);
state.serializedObject.ApplyModifiedProperties();
state.m_Analytics?.RegisterBindingEdit(); // Questionable if action or binding edit?
return state;
};
}
public static Command SetCompositeBindingPartName(SerializedInputBinding bindingProperty, string partName)
{
return (in InputActionsEditorState state) =>
{
InputActionSerializationHelpers.SetBindingPartName(bindingProperty.wrappedProperty, partName);
state.serializedObject.ApplyModifiedProperties();
state.m_Analytics?.RegisterBindingEdit();
return state;
};
}
public static Command ChangeActionType(SerializedInputAction inputAction, InputActionType newValue)
{
return (in InputActionsEditorState state) =>
{
inputAction.wrappedProperty.FindPropertyRelative(nameof(InputAction.m_Type)).intValue = (int)newValue;
state.serializedObject.ApplyModifiedProperties();
state.m_Analytics?.RegisterActionEdit();
return state;
};
}
public static Command ChangeInitialStateCheck(SerializedInputAction inputAction, bool value)
{
return (in InputActionsEditorState state) =>
{
var property = inputAction.wrappedProperty.FindPropertyRelative(nameof(InputAction.m_Flags));
if (value)
property.intValue |= (int)InputAction.ActionFlags.WantsInitialStateCheck;
else
property.intValue &= ~(int)InputAction.ActionFlags.WantsInitialStateCheck;
state.serializedObject.ApplyModifiedProperties();
state.m_Analytics?.RegisterActionEdit();
return state;
};
}
public static Command ChangeActionControlType(SerializedInputAction inputAction, int controlTypeIndex)
{
return (in InputActionsEditorState state) =>
{
var controlTypes = Selectors.BuildControlTypeList(inputAction.type).ToList();
// ISX-1650: "Any" (in index 0) should not be put into an InputAction.expectedControlType. It's expected to be null in this case.
var controlType = (controlTypeIndex == 0) ? string.Empty : controlTypes[controlTypeIndex];
inputAction.wrappedProperty.FindPropertyRelative(nameof(InputAction.m_ExpectedControlType)).stringValue = controlType;
state.serializedObject.ApplyModifiedProperties();
state.m_Analytics?.RegisterActionEdit();
return state;
};
}
/// <summary>
/// Exists to integrate with some existing UI stuff, like InputControlPathEditor
/// </summary>
/// <returns></returns>
public static Command ApplyModifiedProperties()
{
return (in InputActionsEditorState state) =>
{
state.serializedObject.ApplyModifiedProperties();
return state;
};
}
public static Command SaveAsset(Action postSaveAction)
{
return (in InputActionsEditorState state) =>
{
// TODO This needs to callback to owning editor to save or have asset GUID
// TODO It makes more sense to call back to editor since editor owns target object?
//InputActionAssetManager.SaveAsset(state.serializedObject.targetObject as InputActionAsset);
postSaveAction?.Invoke();
state.m_Analytics?.RegisterExplicitSave();
return state;
};
}
public static Command ToggleAutoSave(bool newValue, Action postSaveAction)
{
return (in InputActionsEditorState state) =>
{
if (newValue != InputEditorUserSettings.autoSaveInputActionAssets)
{
// If it changed from disabled to enabled, perform an initial save.
if (newValue)
{
//InputActionAssetManager.SaveAsset(state.serializedObject.targetObject as InputActionAsset);
postSaveAction?.Invoke();
state.m_Analytics?.RegisterAutoSave();
}
InputEditorUserSettings.autoSaveInputActionAssets = newValue;
}
return state;
};
}
public static Command ChangeActionMapName(int index, string newName)
{
return (in InputActionsEditorState state) =>
{
var actionMap = Selectors.GetActionMapAtIndex(state, index)?.wrappedProperty;
InputActionSerializationHelpers.RenameActionMap(actionMap, newName);
state.serializedObject.ApplyModifiedProperties();
state.m_Analytics?.RegisterActionMapEdit();
return state;
};
}
public static Command ChangeActionName(int actionMapIndex, string oldName, string newName)
{
return (in InputActionsEditorState state) =>
{
var actionMap = Selectors.GetActionMapAtIndex(state, actionMapIndex)?.wrappedProperty;
var action = Selectors.GetActionInMap(state, actionMapIndex, oldName).wrappedProperty;
InputActionSerializationHelpers.RenameAction(action, actionMap, newName);
state.serializedObject.ApplyModifiedProperties();
state.m_Analytics?.RegisterActionEdit();
return state;
};
}
public static Command ChangeCompositeName(int actionMapIndex, int bindingIndex, string newName)
{
return (in InputActionsEditorState state) =>
{
var actionMap = Selectors.GetActionMapAtIndex(state, actionMapIndex)?.wrappedProperty;
var binding = Selectors.GetCompositeOrBindingInMap(actionMap, bindingIndex).wrappedProperty;
InputActionSerializationHelpers.RenameComposite(binding, newName);
state.serializedObject.ApplyModifiedProperties();
state.m_Analytics?.RegisterBindingEdit();
return state;
};
}
// Removes all action maps and their content from the associated serialized InputActionAsset.
public static Command ClearActionMaps()
{
return (in InputActionsEditorState state) =>
{
state.m_Analytics?.RegisterReset();
InputActionSerializationHelpers.DeleteAllActionMaps(state.serializedObject);
state.serializedObject.ApplyModifiedProperties();
return state.ClearCutElements();
};
}
// Replaces all action maps of the associated serialized InputActionAsset with the action maps contained in
// the given source asset.
public static Command ReplaceActionMaps(string inputActionAssetJsonContent)
{
return (in InputActionsEditorState state) =>
{
// First delete all existing data
InputActionSerializationHelpers.DeleteAllActionMaps(state.serializedObject);
InputActionSerializationHelpers.DeleteAllControlSchemes(state.serializedObject);
// Create new data based on source
var temp = InputActionAsset.FromJson(inputActionAssetJsonContent);
using (var tmp = new SerializedObject(temp))
{
InputActionSerializationHelpers.AddControlSchemes(state.serializedObject, tmp);
InputActionSerializationHelpers.AddActionMaps(state.serializedObject, tmp);
}
state.serializedObject.ApplyModifiedProperties();
state.m_Analytics.RegisterActionMapEdit();
return state.ClearCutElements();
};
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 60e5ec46fb7a7ff41856f37c60efb70f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,305 @@
#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine.InputSystem.Utilities;
namespace UnityEngine.InputSystem.Editor
{
internal static class ControlSchemeCommands
{
private const string kAllControlSchemesName = "All Control Schemes";
private const string kNewControlSchemeName = "New Control Scheme";
public static Command AddNewControlScheme()
{
return (in InputActionsEditorState state) =>
{
state.m_Analytics?.RegisterControlSchemeEdit();
return state.With(selectedControlScheme: new InputControlScheme(
MakeUniqueControlSchemeName(state, kNewControlSchemeName)), selectedDeviceRequirementIndex: -1);
};
}
public static Command AddDeviceRequirement(InputControlScheme.DeviceRequirement requirement)
{
return (in InputActionsEditorState state) =>
{
state.m_Analytics?.RegisterControlSchemeEdit();
return state.With(selectedControlScheme: new InputControlScheme(state.selectedControlScheme.name,
state.selectedControlScheme.deviceRequirements.Append(requirement)));
};
}
public static Command RemoveDeviceRequirement(int selectedDeviceIndex)
{
return (in InputActionsEditorState state) =>
{
state.m_Analytics?.RegisterControlSchemeEdit();
var newDeviceIndex =
Mathf.Clamp(
selectedDeviceIndex <= state.selectedDeviceRequirementIndex
? state.selectedDeviceRequirementIndex - 1
: state.selectedDeviceRequirementIndex, -1, state.selectedDeviceRequirementIndex);
return state.With(selectedControlScheme: new InputControlScheme(state.selectedControlScheme.name,
state.selectedControlScheme.deviceRequirements.Where((r, i) => i != selectedDeviceIndex)), selectedDeviceRequirementIndex: newDeviceIndex);
};
}
public static Command SaveControlScheme(string newControlSchemeName = "", bool updateExisting = false)
{
return (in InputActionsEditorState state) =>
{
var controlSchemeName = state.selectedControlScheme.name;
var controlSchemesArray = state.serializedObject.FindProperty(nameof(InputActionAsset.m_ControlSchemes));
var controlScheme = controlSchemesArray
.FirstOrDefault(sp => sp.FindPropertyRelative(nameof(InputControlScheme.m_Name)).stringValue == controlSchemeName);
var actionMaps = state.serializedObject.FindProperty(nameof(InputActionAsset.m_ActionMaps));
// If the control scheme is null, we're saving a new control scheme, otherwise editing an existing one
if (controlScheme == null && updateExisting)
throw new InvalidOperationException("Tried to update a non-existent control scheme.");
if (updateExisting == false)
{
controlSchemeName = MakeUniqueControlSchemeName(state, controlSchemeName);
controlSchemesArray.InsertArrayElementAtIndex(controlSchemesArray.arraySize);
controlScheme = controlSchemesArray.GetArrayElementAtIndex(controlSchemesArray.arraySize - 1);
}
// If we're renaming a control scheme, we need to update the bindings that use it and make a unique name
if (!string.IsNullOrEmpty(newControlSchemeName))
{
newControlSchemeName = MakeUniqueControlSchemeName(state, newControlSchemeName);
RenameBindingsControlSchemeHelper(controlScheme, actionMaps, controlSchemeName, newControlSchemeName);
}
controlScheme.FindPropertyRelative(nameof(InputControlScheme.m_Name)).stringValue = string.IsNullOrWhiteSpace(newControlSchemeName) ? controlSchemeName : newControlSchemeName;
controlScheme.FindPropertyRelative(nameof(InputControlScheme.m_BindingGroup)).stringValue = string.IsNullOrWhiteSpace(newControlSchemeName) ? controlSchemeName : newControlSchemeName;
var serializedDeviceRequirements = controlScheme.FindPropertyRelative(nameof(InputControlScheme.m_DeviceRequirements));
serializedDeviceRequirements.ClearArray();
for (var i = 0; i < state.selectedControlScheme.deviceRequirements.Count; i++)
{
var deviceRequirement = state.selectedControlScheme.deviceRequirements[i];
serializedDeviceRequirements.InsertArrayElementAtIndex(i);
var serializedRequirement = serializedDeviceRequirements.GetArrayElementAtIndex(i);
serializedRequirement
.FindPropertyRelative(nameof(InputControlScheme.DeviceRequirement.m_ControlPath))
.stringValue = deviceRequirement.controlPath;
serializedRequirement.FindPropertyRelative(nameof(InputControlScheme.DeviceRequirement.m_Flags))
.enumValueFlag = (int)deviceRequirement.m_Flags;
}
state.serializedObject.ApplyModifiedProperties();
return state.With(
selectedControlScheme: new InputControlScheme(controlScheme),
// Select the control scheme updated, otherwise select the new one it was added
selectedControlSchemeIndex: updateExisting? state.selectedControlSchemeIndex: controlSchemesArray.arraySize - 1);
};
}
static void RenameBindingsControlSchemeHelper(SerializedProperty controlScheme, SerializedProperty actionMaps, string controlSchemeName, string newName)
{
foreach (SerializedProperty actionMap in actionMaps)
{
var bindings = actionMap
.FindPropertyRelative(nameof(InputActionMap.m_Bindings))
.Select(sp => new SerializedInputBinding(sp))
.ToList();
var bindingsToRename = bindings.Where(b => b.controlSchemes.Contains(controlSchemeName)).ToList();
foreach (var binding in bindingsToRename)
{
var bindingGroups = binding.controlSchemes.ToList();
bindingGroups.Remove(controlSchemeName);
bindingGroups.Add(newName);
binding.wrappedProperty.FindPropertyRelative(nameof(InputBinding.m_Groups)).stringValue = bindingGroups.Join(InputBinding.kSeparatorString);
}
}
}
public static Command SelectControlScheme(int controlSchemeIndex)
{
return (in InputActionsEditorState state) =>
{
if (controlSchemeIndex == -1)
return state.With(selectedControlSchemeIndex: controlSchemeIndex, selectedControlScheme: new InputControlScheme());
var controlSchemeSerializedProperty = state.serializedObject
.FindProperty(nameof(InputActionAsset.m_ControlSchemes))
.GetArrayElementAtIndex(controlSchemeIndex);
return state.With(
selectedControlSchemeIndex: controlSchemeIndex,
selectedControlScheme: new InputControlScheme(controlSchemeSerializedProperty));
};
}
public static Command ResetSelectedControlScheme()
{
return (in InputActionsEditorState state) =>
{
var controlSchemeSerializedProperty = state.selectedControlSchemeIndex == -1 ? null :
state.serializedObject
.FindProperty(nameof(InputActionAsset.m_ControlSchemes))
.GetArrayElementAtIndex(state.selectedControlSchemeIndex);
if (controlSchemeSerializedProperty == null)
{
return state.With(
selectedControlSchemeIndex: -1,
selectedControlScheme: new InputControlScheme());
}
return state.With(
selectedControlScheme: new InputControlScheme(controlSchemeSerializedProperty));
};
}
public static Command SelectDeviceRequirement(int deviceRequirementIndex)
{
return (in InputActionsEditorState state) => state.With(selectedDeviceRequirementIndex: deviceRequirementIndex);
}
/// <summary>
/// Duplicate creates a new instance of the selected control scheme and places it in the selected
/// control scheme property of the state but doesn't persist anything.
/// </summary>
public static Command DuplicateSelectedControlScheme()
{
return (in InputActionsEditorState state) =>
{
state.m_Analytics?.RegisterControlSchemeEdit();
return state.With(selectedControlScheme: new InputControlScheme(
MakeUniqueControlSchemeName(state, state.selectedControlScheme.name),
state.selectedControlScheme.deviceRequirements));
};
}
public static Command DeleteSelectedControlScheme()
{
return (in InputActionsEditorState state) =>
{
var selectedControlSchemeName = state.selectedControlScheme.name;
var serializedArray = InputActionSerializationHelpers.GetControlSchemesArray(state.serializedObject);
var indexOfArrayElement = InputActionSerializationHelpers.IndexOfControlScheme(serializedArray, selectedControlSchemeName);
if (indexOfArrayElement < 0)
throw new InvalidOperationException("Control scheme doesn't exist in collection.");
// Ask for confirmation.
if (Dialog.Result.Cancel == Dialog.ControlScheme.ShowDeleteControlScheme(selectedControlSchemeName))
return state;
serializedArray.DeleteArrayElementAtIndex(indexOfArrayElement);
state.serializedObject.ApplyModifiedProperties();
if (serializedArray.arraySize == 0)
return state.With(
selectedControlSchemeIndex: -1,
selectedControlScheme: new InputControlScheme(),
selectedDeviceRequirementIndex: -1);
if (indexOfArrayElement > serializedArray.arraySize - 1)
return state.With(
selectedControlSchemeIndex: serializedArray.arraySize - 1,
selectedControlScheme: new InputControlScheme(serializedArray.GetArrayElementAtIndex(serializedArray.arraySize - 1)), selectedDeviceRequirementIndex: -1);
state.m_Analytics?.RegisterControlSchemeEdit();
return state.With(
selectedControlSchemeIndex: indexOfArrayElement,
selectedControlScheme: new InputControlScheme(serializedArray.GetArrayElementAtIndex(indexOfArrayElement)), selectedDeviceRequirementIndex: -1);
};
}
internal static string MakeUniqueControlSchemeName(InputActionsEditorState state, string name)
{
var controlSchemes = state.serializedObject.FindProperty(nameof(InputActionAsset.m_ControlSchemes));
IEnumerable<string> controlSchemeNames = Array.Empty<string>();
if (controlSchemes != null)
controlSchemeNames =
controlSchemes.Select(sp => sp.FindPropertyRelative(nameof(InputControlScheme.m_Name)).stringValue);
return StringHelpers.MakeUniqueName(name, controlSchemeNames.Append(kAllControlSchemesName), x => x);
}
public static Command ChangeDeviceRequirement(int deviceRequirementIndex, bool isRequired)
{
return (in InputActionsEditorState state) =>
{
var deviceRequirements = state.selectedControlScheme.deviceRequirements.ToList();
var requirement = deviceRequirements[deviceRequirementIndex];
requirement.isOptional = !isRequired;
deviceRequirements[deviceRequirementIndex] = requirement;
state.m_Analytics?.RegisterControlSchemeEdit();
return state.With(selectedControlScheme: new InputControlScheme(
state.selectedControlScheme.name,
deviceRequirements,
state.selectedControlScheme.bindingGroup));
};
}
public static Command ReorderDeviceRequirements(int oldPosition, int newPosition)
{
return (in InputActionsEditorState state) =>
{
var deviceRequirements = state.selectedControlScheme.deviceRequirements.ToList();
var requirement = deviceRequirements[oldPosition];
deviceRequirements.RemoveAt(oldPosition);
deviceRequirements.Insert(newPosition, requirement);
state.m_Analytics?.RegisterControlSchemeEdit();
return state.With(selectedControlScheme: new InputControlScheme(
state.selectedControlScheme.name,
deviceRequirements,
state.selectedControlScheme.bindingGroup));
};
}
public static Command ChangeSelectedBindingsControlSchemes(string controlScheme, bool add)
{
return (in InputActionsEditorState state) =>
{
var actionMapSO = state.serializedObject
?.FindProperty(nameof(InputActionAsset.m_ActionMaps))
?.GetArrayElementAtIndex(state.selectedActionMapIndex);
var serializedProperty = actionMapSO?.FindPropertyRelative(nameof(InputActionMap.m_Bindings))
?.GetArrayElementAtIndex(state.selectedBindingIndex);
var groupsProperty = serializedProperty.FindPropertyRelative(nameof(InputBinding.m_Groups));
var groups = groupsProperty.stringValue;
if (add)
groupsProperty.stringValue = groups
.Split(InputBinding.kSeparatorString)
.Append(controlScheme)
.Distinct()
.Join(InputBinding.kSeparatorString);
else
groupsProperty.stringValue = groups
.Split(InputBinding.kSeparatorString)
.Where(s => s != controlScheme)
.Join(InputBinding.kSeparatorString);
state.m_Analytics?.RegisterBindingEdit();
state.serializedObject.ApplyModifiedProperties();
return state;
};
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 99cfd7e6e4fd7294090b6bf84b177326
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,23 @@
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
namespace UnityEngine.InputSystem.Editor
{
internal static class EnumerableExtensions
{
public static void ForEach<T>(this IEnumerable<T> enumerable, Action<T, int> action)
{
int index = 0;
foreach (var item in enumerable)
action(item, index++);
}
public static string Join<T>(this IEnumerable<T> enumerable, string separator)
{
return string.Join(separator, enumerable);
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 255ca660c2e3b424bb54d7be66737f57
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,56 @@
#if UNITY_EDITOR
using System;
using System.Linq.Expressions;
using System.Reflection;
namespace UnityEngine.InputSystem.Editor
{
internal static class ExpressionUtils
{
public static PropertyInfo GetProperty<TEntity, TProperty>(Expression<Func<TEntity, TProperty>> expression)
{
var member = GetMemberExpression(expression).Member;
var property = member as PropertyInfo;
if (property == null)
throw new InvalidOperationException($"Member with Name '{member.Name}' is not a property.");
return property;
}
private static MemberExpression GetMemberExpression<TEntity, TProperty>(Expression<Func<TEntity, TProperty>> expression)
{
MemberExpression memberExpression = null;
switch (expression.Body.NodeType)
{
case ExpressionType.Convert:
{
var body = (UnaryExpression)expression.Body;
memberExpression = body.Operand as MemberExpression;
break;
}
case ExpressionType.MemberAccess:
memberExpression = expression.Body as MemberExpression;
break;
}
if (memberExpression == null)
throw new ArgumentException("Not a member access", nameof(expression));
return memberExpression;
}
public static Func<TEntity, TProperty> CreateGetter<TEntity, TProperty>(Expression<Func<TEntity, TProperty>> property)
{
var propertyInfo = GetProperty(property);
var instance = Expression.Parameter(typeof(TEntity), "instance");
var body = Expression.Call(instance, propertyInfo.GetGetMethod());
var parameters = new[] { instance };
return Expression.Lambda<Func<TEntity, TProperty>>(body, parameters).Compile();
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 35e258ef8ccb13146952199468118233
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,50 @@
#if UNITY_EDITOR
namespace UnityEngine.InputSystem.Editor
{
internal static class InputActionsEditorConstants
{
// Paths
public const string PackagePath = "Packages/com.unity.inputsystem";
public const string ResourcesPath = "/InputSystem/Editor/UITKAssetEditor/PackageResources";
/// Template names
public const string ProjectSettingsUxml = "/InputActionsProjectSettings.uxml";
public const string MainEditorViewNameUxml = "/InputActionsEditor.uxml";
public const string BindingsPanelRowTemplateUxml = "/BindingPanelRowTemplate.uxml";
public const string NameAndParametersListViewItemUxml = "/NameAndParameterListViewItemTemplate.uxml";
public const string CompositeBindingPropertiesViewUxml = "/CompositeBindingPropertiesEditor.uxml";
public const string CompositePartBindingPropertiesViewUxml = "/CompositePartBindingPropertiesEditor.uxml";
public const string ControlSchemeEditorViewUxml = "/ControlSchemeEditor.uxml";
public const string InputActionMapsTreeViewItemUxml = "/InputActionMapsTreeViewItem.uxml";
public const string InputActionsTreeViewItemUxml = "/InputActionsTreeViewItem.uxml";
/// Classes
public static readonly string HiddenStyleClassName = "unity-input-actions-editor-hidden";
public const string CompositePartAssignmentTooltip =
"The named part of the composite that the binding is assigned to. Multiple bindings may be assigned the same part. All controls from "
+ "all bindings that are assigned the same part will collectively feed values into that part of the composite.";
public const string CompositeTypeTooltip =
"Type of composite. Allows changing the composite type retroactively. Doing so will modify the bindings that are part of the composite.";
public const string InitialStateCheckTooltip =
"Whether in the next input update after the action was enabled, the action should "
+ "immediately trigger if any of its bound controls are currently in a non-default state. "
+ "This check happens implicitly for Value actions but can be explicitly enabled for Button and Pass-Through actions.";
public struct CommandEvents
{
public const string Rename = "Rename";
public const string Delete = "Delete";
public const string SoftDelete = "SoftDelete";
public const string Duplicate = "Duplicate";
public const string Copy = "Copy";
public const string Cut = "Cut";
public const string Paste = "Paste";
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 36827be967eb9244b973cbee1bbf53e1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,335 @@
#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEditor;
using UnityEditor.ShortcutManagement;
using UnityEngine.UIElements;
using UnityEditor.UIElements;
namespace UnityEngine.InputSystem.Editor
{
internal class InputActionsEditorSettingsProvider : SettingsProvider
{
private static InputActionsEditorSettingsProvider s_Provider;
public static string SettingsPath => InputSettingsPath.kSettingsRootPath;
[SerializeField] InputActionsEditorState m_State;
VisualElement m_RootVisualElement;
private bool m_HasEditFocus;
private bool m_IgnoreActionChangedCallback;
private bool m_IsActivated;
private static bool m_IMGUIDropdownVisible;
StateContainer m_StateContainer;
private static InputActionsEditorSettingsProvider m_ActiveSettingsProvider;
private InputActionsEditorView m_View;
private InputActionsEditorSessionAnalytic m_ActionEditorAnalytics;
public InputActionsEditorSettingsProvider(string path, SettingsScope scopes, IEnumerable<string> keywords = null)
: base(path, scopes, keywords)
{}
public override void OnActivate(string searchContext, VisualElement rootElement)
{
// There is an editor bug UUM-55238 that may cause OnActivate and OnDeactivate to be called in unexpected order.
// This flag avoids making assumptions and executing logic twice.
if (m_IsActivated)
return;
// Monitor play mode state changes
EditorApplication.playModeStateChanged += ModeChanged;
// Setup root element with focus monitoring
m_RootVisualElement = rootElement;
m_RootVisualElement.focusable = true;
m_RootVisualElement.RegisterCallback<FocusOutEvent>(OnFocusOut);
m_RootVisualElement.RegisterCallback<FocusInEvent>(OnFocusIn);
// Always begin a session when activated (note that OnActivate isn't called when navigating back
// to editor from another setting category)
m_ActionEditorAnalytics = new InputActionsEditorSessionAnalytic(
InputActionsEditorSessionAnalytic.Data.Kind.EmbeddedInProjectSettings);
m_ActionEditorAnalytics.Begin();
CreateUI();
// Monitor any changes to InputSystem.actions for as long as this editor is active
InputSystem.onActionsChange += BuildUI;
// Set the asset assigned with the editor which indirectly builds the UI based on setting
BuildUI();
// Note that focused element will be set if we are navigating back to an existing instance when switching
// setting in the left project settings panel since this doesn't recreate the editor.
if (m_RootVisualElement?.focusController?.focusedElement != null)
OnFocusIn();
m_IsActivated = true;
}
public override void OnDeactivate()
{
// There is an editor bug UUM-55238 that may cause OnActivate and OnDeactivate to be called in unexpected order.
// This flag avoids making assumptions and executing logic twice.
if (!m_IsActivated)
return;
// Stop monitoring play mode state changes
EditorApplication.playModeStateChanged -= ModeChanged;
if (m_RootVisualElement != null)
{
m_RootVisualElement.UnregisterCallback<FocusInEvent>(OnFocusIn);
m_RootVisualElement.UnregisterCallback<FocusOutEvent>(OnFocusOut);
}
// Make sure any remaining changes are actually saved
SaveAssetOnFocusLost();
// Note that OnDeactivate will also trigger when opening the Project Settings (existing instance).
// Hence we guard against duplicate OnDeactivate() calls.
if (m_HasEditFocus)
{
OnFocusOut();
m_HasEditFocus = false;
}
InputSystem.onActionsChange -= BuildUI;
m_IsActivated = false;
// Always end a session when deactivated.
m_ActionEditorAnalytics?.End();
m_View?.DestroyView();
}
private void OnFocusIn(FocusInEvent @event = null)
{
if (!m_HasEditFocus)
{
m_HasEditFocus = true;
m_ActionEditorAnalytics.RegisterEditorFocusIn();
m_ActiveSettingsProvider = this;
SetIMGUIDropdownVisible(false, false);
}
}
void SaveAssetOnFocusLost()
{
#if UNITY_INPUT_SYSTEM_INPUT_ACTIONS_EDITOR_AUTO_SAVE_ON_FOCUS_LOST
var asset = GetAsset();
if (asset != null)
ValidateAndSaveAsset(asset);
#endif
}
public static void SetIMGUIDropdownVisible(bool visible, bool optionWasSelected)
{
if (m_ActiveSettingsProvider == null)
return;
// If we selected an item from the dropdown, we *should* still be focused on this settings window - but
// since the IMGUI dropdown is technically a separate window, we have to refocus manually.
//
// If we didn't select a dropdown option, there's not a simple way to know where the focus has gone,
// so assume we lost focus and save if appropriate. ISXB-801
if (!visible && m_IMGUIDropdownVisible)
{
if (optionWasSelected)
m_ActiveSettingsProvider.m_RootVisualElement.Focus();
else
m_ActiveSettingsProvider.SaveAssetOnFocusLost();
}
else if (visible && !m_IMGUIDropdownVisible)
{
m_ActiveSettingsProvider.m_HasEditFocus = false;
}
m_IMGUIDropdownVisible = visible;
}
private async void DelayFocusLost(bool relatedTargetWasNull)
{
await Task.Delay(120);
// We delay this call to ensure that the IMGUI flag has a chance to change first.
if (relatedTargetWasNull && m_HasEditFocus && !m_IMGUIDropdownVisible)
{
m_HasEditFocus = false;
SaveAssetOnFocusLost();
}
}
private void OnFocusOut(FocusOutEvent @event = null)
{
// This can be used to detect focus lost events of container elements, but will not detect window focus.
// Note that `event.relatedTarget` contains the element that gains focus, which is null if we select
// elements outside of project settings Editor Window. Also note that @event is null when we call this
// from OnDeactivate().
var element = (VisualElement)@event?.relatedTarget;
m_ActionEditorAnalytics.RegisterEditorFocusOut();
DelayFocusLost(element == null);
}
private void OnStateChanged(InputActionsEditorState newState, UIRebuildMode editorRebuildMode)
{
#if UNITY_INPUT_SYSTEM_INPUT_ACTIONS_EDITOR_AUTO_SAVE_ON_FOCUS_LOST
// No action, auto-saved on edit-focus lost
#else
// Project wide input actions always auto save - don't check the asset auto save status
var asset = GetAsset();
if (asset != null)
ValidateAndSaveAsset(asset);
#endif
}
private void ValidateAndSaveAsset(InputActionAsset asset)
{
ProjectWideActionsAsset.Verify(asset); // Ignore verification result for save
EditorHelpers.SaveAsset(AssetDatabase.GetAssetPath(asset), asset.ToJson());
}
private void CreateUI()
{
var projectSettingsAsset = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
InputActionsEditorConstants.PackagePath +
InputActionsEditorConstants.ResourcesPath +
InputActionsEditorConstants.ProjectSettingsUxml);
projectSettingsAsset.CloneTree(m_RootVisualElement);
m_RootVisualElement.styleSheets.Add(InputActionsEditorWindowUtils.theme);
}
private void BuildUI()
{
// Construct from InputSystem.actions asset
var asset = InputSystem.actions;
var hasAsset = asset != null;
m_State = (asset != null) ? new InputActionsEditorState(m_ActionEditorAnalytics, new SerializedObject(asset)) : default;
// Dynamically show a section indicating that an asset is missing if not currently having an associated asset
var missingAssetSection = m_RootVisualElement.Q<VisualElement>("missing-asset-section");
if (missingAssetSection != null)
{
missingAssetSection.style.visibility = hasAsset ? Visibility.Hidden : Visibility.Visible;
missingAssetSection.style.display = hasAsset ? DisplayStyle.None : DisplayStyle.Flex;
}
// Allow the user to select an asset out of the assets available in the project via picker.
// Note that we show "None" (null) even if InputSystem.actions is currently a broken/missing reference.
var objectField = m_RootVisualElement.Q<ObjectField>("current-asset");
if (objectField != null)
{
objectField.value = (asset == null) ? null : asset;
objectField.RegisterCallback<ChangeEvent<Object>>((evt) =>
{
if (evt.newValue != asset)
InputSystem.actions = evt.newValue as InputActionAsset;
});
// Prevent reassignment in in editor which would result in exception during play-mode
objectField.SetEnabled(!EditorApplication.isPlayingOrWillChangePlaymode);
}
// Configure a button to allow the user to create and assign a new project-wide asset based on default template
var createAssetButton = m_RootVisualElement.Q<Button>("create-asset");
createAssetButton?.RegisterCallback<ClickEvent>(evt =>
{
var assetPath = ProjectWideActionsAsset.defaultAssetPath;
Dialog.Result result = Dialog.Result.Discard;
if (AssetDatabase.LoadAssetAtPath<Object>(assetPath) != null)
result = Dialog.InputActionAsset.ShowCreateAndOverwriteExistingAsset(assetPath);
if (result == Dialog.Result.Discard)
InputSystem.actions = ProjectWideActionsAsset.CreateDefaultAssetAtPath(assetPath);
});
// Remove input action editor if already present
{
VisualElement element = m_RootVisualElement.Q("action-editor");
if (element != null)
m_RootVisualElement.Remove(element);
}
// If the editor is associated with an asset we show input action editor
if (hasAsset)
{
m_StateContainer = new StateContainer(m_State, AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(asset)));
m_StateContainer.StateChanged += OnStateChanged;
m_View = new InputActionsEditorView(m_RootVisualElement, m_StateContainer, true, null);
m_StateContainer.Initialize(m_RootVisualElement.Q("action-editor"));
}
}
private InputActionAsset GetAsset()
{
return m_State.serializedObject?.targetObject as InputActionAsset;
}
private void SetObjectFieldEnabled(bool enabled)
{
// Update object picker enabled state based off editor play mode
if (m_RootVisualElement != null)
UQueryExtensions.Q<ObjectField>(m_RootVisualElement, "current-asset")?.SetEnabled(enabled);
}
private void ModeChanged(PlayModeStateChange change)
{
switch (change)
{
case PlayModeStateChange.EnteredEditMode:
SetObjectFieldEnabled(true);
break;
case PlayModeStateChange.ExitingEditMode:
// Ensure any changes are saved to the asset; FocusLost isn't always triggered when entering PlayMode.
SaveAssetOnFocusLost();
SetObjectFieldEnabled(false);
break;
case PlayModeStateChange.EnteredPlayMode:
case PlayModeStateChange.ExitingPlayMode:
default:
break;
}
}
[SettingsProvider]
public static SettingsProvider CreateGlobalInputActionsEditorProvider()
{
if (s_Provider == null)
s_Provider = new InputActionsEditorSettingsProvider(SettingsPath, SettingsScope.Project);
return s_Provider;
}
#region Shortcuts
[Shortcut("Input Action Editor/Project Settings/Add Action Map", null, KeyCode.M, ShortcutModifiers.Alt)]
private static void AddActionMapShortcut(ShortcutArguments arguments)
{
if (m_ActiveSettingsProvider is { m_HasEditFocus : true })
m_ActiveSettingsProvider.m_StateContainer.Dispatch(Commands.AddActionMap());
}
[Shortcut("Input Action Editor/Project Settings/Add Action", null, KeyCode.A, ShortcutModifiers.Alt)]
private static void AddActionShortcut(ShortcutArguments arguments)
{
if (m_ActiveSettingsProvider is { m_HasEditFocus : true })
m_ActiveSettingsProvider.m_StateContainer.Dispatch(Commands.AddAction());
}
[Shortcut("Input Action Editor/Project Settings/Add Binding", null, KeyCode.B, ShortcutModifiers.Alt)]
private static void AddBindingShortcut(ShortcutArguments arguments)
{
if (m_ActiveSettingsProvider is { m_HasEditFocus : true })
m_ActiveSettingsProvider.m_StateContainer.Dispatch(Commands.AddBinding());
}
#endregion
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 18d3e83390dcd6349b3086b9f70391b9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,391 @@
#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
namespace UnityEngine.InputSystem.Editor
{
[System.Serializable]
internal class CutElement
{
private Guid id;
internal Type type;
public CutElement(Guid id, Type type)
{
this.id = id;
this.type = type;
}
public int GetIndexOfProperty(InputActionsEditorState state)
{
if (type == typeof(InputActionMap))
{
var actionMap = state.serializedObject
?.FindProperty(nameof(InputActionAsset.m_ActionMaps))
?.FirstOrDefault(s => InputActionSerializationHelpers.GetId(s).Equals(id));
return actionMap.GetIndexOfArrayElement();
}
if (type == typeof(InputAction))
{
var action = Selectors.GetActionMapAtIndex(state, actionMapIndex(state))?.wrappedProperty.FindPropertyRelative("m_Actions").FirstOrDefault(a => InputActionSerializationHelpers.GetId(a).Equals(id));
return action.GetIndexOfArrayElement();
}
if (type == typeof(InputBinding))
{
var binding = Selectors.GetBindingForId(state, id.ToString(),
out _);
return binding.GetIndexOfArrayElement();
}
return -1;
}
public int actionMapIndex(InputActionsEditorState state) => type == typeof(InputActionMap) ? GetIndexOfProperty(state) : GetActionMapIndex(state);
private int GetActionMapIndex(InputActionsEditorState state)
{
var actionMaps = state.serializedObject?.FindProperty(nameof(InputActionAsset.m_ActionMaps));
var cutActionMapIndex = state.serializedObject
?.FindProperty(nameof(InputActionAsset.m_ActionMaps))
?.FirstOrDefault(s => s.FindPropertyRelative("m_Id").stringValue.Equals(id)).GetIndexOfArrayElement();
if (type == typeof(InputBinding))
cutActionMapIndex = actionMaps.FirstOrDefault(map => map.FindPropertyRelative("m_Bindings").Select(InputActionSerializationHelpers.GetId).Contains(id)).GetIndexOfArrayElement();
else if (type == typeof(InputAction))
cutActionMapIndex = actionMaps.FirstOrDefault(map => map.FindPropertyRelative("m_Actions").Select(InputActionSerializationHelpers.GetId).Contains(id)).GetIndexOfArrayElement();
return cutActionMapIndex ?? -1;
}
}
internal struct InputActionsEditorState
{
public int selectedActionMapIndex { get {return m_selectedActionMapIndex; } }
public int selectedActionIndex { get {return m_selectedActionIndex; } }
public int selectedBindingIndex { get {return m_selectedBindingIndex; } }
public SelectionType selectionType { get {return m_selectionType; } }
public SerializedObject serializedObject { get; } // Note that state doesn't own this disposable object
private readonly List<CutElement> cutElements => m_CutElements;
// Control schemes
public int selectedControlSchemeIndex { get { return m_selectedControlSchemeIndex; } }
public int selectedDeviceRequirementIndex { get {return m_selectedDeviceRequirementIndex; } }
public InputControlScheme selectedControlScheme => m_ControlScheme; // TODO Bad this either po
internal InputActionsEditorSessionAnalytic m_Analytics;
[SerializeField] int m_selectedActionMapIndex;
[SerializeField] int m_selectedActionIndex;
[SerializeField] int m_selectedBindingIndex;
[SerializeField] SelectionType m_selectionType;
[SerializeField] int m_selectedControlSchemeIndex;
[SerializeField] int m_selectedDeviceRequirementIndex;
private List<CutElement> m_CutElements;
internal bool hasCutElements => m_CutElements != null && m_CutElements.Count > 0;
public InputActionsEditorState(
InputActionsEditorSessionAnalytic analytics,
SerializedObject inputActionAsset,
int selectedActionMapIndex = 0,
int selectedActionIndex = 0,
int selectedBindingIndex = 0,
SelectionType selectionType = SelectionType.Action,
InputControlScheme selectedControlScheme = default,
int selectedControlSchemeIndex = -1,
int selectedDeviceRequirementIndex = -1,
List<CutElement> cutElements = null)
{
Debug.Assert(inputActionAsset != null);
m_Analytics = analytics;
serializedObject = inputActionAsset;
m_selectedActionMapIndex = selectedActionMapIndex;
m_selectedActionIndex = selectedActionIndex;
m_selectedBindingIndex = selectedBindingIndex;
m_selectionType = selectionType;
m_ControlScheme = selectedControlScheme;
m_selectedControlSchemeIndex = selectedControlSchemeIndex;
m_selectedDeviceRequirementIndex = selectedDeviceRequirementIndex;
m_CutElements = cutElements;
}
public InputActionsEditorState(InputActionsEditorState other, SerializedObject asset)
{
m_Analytics = other.m_Analytics;
// Assign serialized object, not that this might be equal to other.serializedObject,
// a slight variation of it with any kind of changes or a completely different one.
// Hence, we do our best here to keep any selections consistent by remapping objects
// based on GUIDs (IDs) and when it fails, attempt to select first object and if that
// fails revert to not having a selection. This would even be true for domain reloads
// if the asset would be modified during domain reload.
serializedObject = asset;
if (other.Equals(default(InputActionsEditorState)))
{
// This instance was created by default constructor and thus is missing some appropriate defaults:
other.m_selectionType = SelectionType.Action;
other.m_selectedControlSchemeIndex = -1;
other.m_selectedDeviceRequirementIndex = -1;
}
// Attempt to preserve action map selection by GUID, otherwise select first or last resort none
var otherSelectedActionMap = other.GetSelectedActionMap();
var actionMapCount = Selectors.GetActionMapCount(asset);
m_selectedActionMapIndex = otherSelectedActionMap != null
? Selectors.GetActionMapIndexFromId(asset,
InputActionSerializationHelpers.GetId(otherSelectedActionMap))
: actionMapCount > 0 ? 0 : -1;
var selectedActionMap = m_selectedActionMapIndex >= 0
? Selectors.GetActionMapAtIndex(asset, m_selectedActionMapIndex)?.wrappedProperty : null;
// Attempt to preserve action selection by GUID, otherwise select first or last resort none
var otherSelectedAction = m_selectedActionMapIndex >= 0 ?
Selectors.GetSelectedAction(other) : null;
m_selectedActionIndex = selectedActionMap != null && otherSelectedAction.HasValue
? Selectors.GetActionIndexFromId(selectedActionMap,
InputActionSerializationHelpers.GetId(otherSelectedAction.Value.wrappedProperty))
: Selectors.GetActionCount(selectedActionMap) > 0 ? 0 : -1;
// Attempt to preserve binding selection by GUID, otherwise select first or none
m_selectedBindingIndex = -1;
if (m_selectedActionMapIndex >= 0)
{
var otherSelectedBinding = Selectors.GetSelectedBinding(other);
if (otherSelectedBinding != null)
{
var otherSelectedBindingId =
InputActionSerializationHelpers.GetId(otherSelectedBinding.Value.wrappedProperty);
var binding = Selectors.GetBindingForId(asset, otherSelectedBindingId.ToString(), out _);
if (binding != null)
m_selectedBindingIndex = binding.GetIndexOfArrayElement();
}
}
// Sanity check selection type and override any previous selection if not valid given indices
// since we have remapped GUIDs to selection indices for another asset (SerializedObject)
if (other.m_selectionType == SelectionType.Binding && m_selectedBindingIndex < 0)
m_selectionType = SelectionType.Action;
else
m_selectionType = other.m_selectionType;
m_selectedControlSchemeIndex = other.m_selectedControlSchemeIndex;
m_selectedDeviceRequirementIndex = other.m_selectedDeviceRequirementIndex;
// Selected ControlScheme index is serialized but we have to recreated actual object after domain reload.
// In case asset is different from from others asset the index might not even be valid range so we need
// to reattempt to preserve selection but range adapt.
// Note that control schemes and device requirements currently lack any GUID/ID to be uniquely identified.
var controlSchemesArrayProperty = serializedObject.FindProperty(nameof(InputActionAsset.m_ControlSchemes));
if (m_selectedControlSchemeIndex >= 0 && controlSchemesArrayProperty.arraySize > 0)
{
if (m_selectedControlSchemeIndex >= controlSchemesArrayProperty.arraySize)
m_selectedControlSchemeIndex = 0;
m_ControlScheme = new InputControlScheme(
controlSchemesArrayProperty.GetArrayElementAtIndex(other.m_selectedControlSchemeIndex));
// TODO Preserve device requirement index
}
else
{
m_selectedControlSchemeIndex = -1;
m_selectedDeviceRequirementIndex = -1;
m_ControlScheme = new InputControlScheme();
}
m_CutElements = other.cutElements;
}
public InputActionsEditorState With(
int? selectedActionMapIndex = null,
int? selectedActionIndex = null,
int? selectedBindingIndex = null,
SelectionType? selectionType = null,
InputControlScheme? selectedControlScheme = null,
int? selectedControlSchemeIndex = null,
int? selectedDeviceRequirementIndex = null,
List<CutElement> cutElements = null)
{
return new InputActionsEditorState(
m_Analytics,
serializedObject,
selectedActionMapIndex ?? this.selectedActionMapIndex,
selectedActionIndex ?? this.selectedActionIndex,
selectedBindingIndex ?? this.selectedBindingIndex,
selectionType ?? this.selectionType,
// Control schemes
selectedControlScheme ?? this.selectedControlScheme,
selectedControlSchemeIndex ?? this.selectedControlSchemeIndex,
selectedDeviceRequirementIndex ?? this.selectedDeviceRequirementIndex,
cutElements ?? m_CutElements
);
}
public InputActionsEditorState ClearCutElements()
{
return new InputActionsEditorState(
m_Analytics,
serializedObject,
selectedActionMapIndex,
selectedActionIndex,
selectedBindingIndex,
selectionType,
selectedControlScheme,
selectedControlSchemeIndex,
selectedDeviceRequirementIndex,
cutElements: null);
}
public SerializedProperty GetActionMapByName(string actionMapName)
{
return serializedObject
.FindProperty(nameof(InputActionAsset.m_ActionMaps))
.FirstOrDefault(p => p.FindPropertyRelative(nameof(InputActionMap.m_Name)).stringValue == actionMapName);
}
public InputActionsEditorState SelectAction(string actionName)
{
var actionMap = GetSelectedActionMap();
var actions = actionMap.FindPropertyRelative(nameof(InputActionMap.m_Actions));
for (var i = 0; i < actions.arraySize; i++)
{
if (actions.GetArrayElementAtIndex(i)
.FindPropertyRelative(nameof(InputAction.m_Name)).stringValue != actionName) continue;
return With(selectedActionIndex: i, selectionType: SelectionType.Action);
}
// If we cannot find the desired map we should return invalid index
return With(selectedActionIndex: -1, selectionType: SelectionType.Action);
}
public InputActionsEditorState SelectAction(SerializedProperty state)
{
var index = state.GetIndexOfArrayElement();
return With(selectedActionIndex: index, selectionType: SelectionType.Action);
}
public InputActionsEditorState SelectActionMap(SerializedProperty actionMap)
{
var index = actionMap.GetIndexOfArrayElement();
return With(selectedBindingIndex: 0, selectedActionMapIndex: index, selectedActionIndex: 0);
}
public InputActionsEditorState SelectActionMap(string actionMapName)
{
var actionMap = GetActionMapByName(actionMapName);
return With(selectedBindingIndex: 0,
selectedActionMapIndex: actionMap.GetIndexOfArrayElement(),
selectedActionIndex: 0, selectionType: SelectionType.Action);
}
public InputActionsEditorState SelectBinding(int index)
{
//if no binding selected (due to no bindings in list) set selection type to action
if (index == -1)
return With(selectedBindingIndex: index, selectionType: SelectionType.Action);
return With(selectedBindingIndex: index, selectionType: SelectionType.Binding);
}
public InputActionsEditorState SelectAction(int index)
{
//if no action selected (no actions available) set selection type to none
if (index == -1)
return With(selectedActionIndex: index, selectionType: SelectionType.None);
return With(selectedActionIndex: index, selectionType: SelectionType.Action);
}
public InputActionsEditorState SelectActionMap(int index)
{
if (index == -1)
return With(selectedActionMapIndex: index, selectionType: SelectionType.None);
return With(selectedBindingIndex: 0,
selectedActionMapIndex: index,
selectedActionIndex: 0, selectionType: SelectionType.Action);
}
public InputActionsEditorState CutActionOrBinding()
{
m_CutElements = new List<CutElement>();
var type = selectionType == SelectionType.Action ? typeof(InputAction) : typeof(InputBinding);
var property = selectionType == SelectionType.Action ? Selectors.GetSelectedAction(this)?.wrappedProperty : Selectors.GetSelectedBinding(this)?.wrappedProperty;
cutElements.Add(new CutElement(InputActionSerializationHelpers.GetId(property), type));
return With(cutElements: cutElements);
}
public InputActionsEditorState CutActionMaps()
{
m_CutElements = new List<CutElement> { new(InputActionSerializationHelpers.GetId(Selectors.GetSelectedActionMap(this)?.wrappedProperty), typeof(InputActionMap)) };
return With(cutElements: cutElements);
}
public IEnumerable<string> GetDisabledActionMaps(List<string> allActionMaps)
{
if (cutElements == null || cutElements == null)
return Enumerable.Empty<string>();
var cutActionMaps = cutElements.Where(cut => cut.type == typeof(InputActionMap));
var state = this;
return allActionMaps.Where(actionMapName =>
{
return cutActionMaps.Any(am => am.GetIndexOfProperty(state) == allActionMaps.IndexOf(actionMapName));
});
}
public readonly bool IsBindingCut(int actionMapIndex, int bindingIndex)
{
if (cutElements == null)
return false;
var state = this;
return cutElements.Any(cutElement => cutElement.actionMapIndex(state) == actionMapIndex &&
cutElement.GetIndexOfProperty(state) == bindingIndex &&
cutElement.type == typeof(InputBinding));
}
public readonly bool IsActionCut(int actionMapIndex, int actionIndex)
{
if (cutElements == null)
return false;
var state = this;
return cutElements.Any(cutElement => cutElement.actionMapIndex(state) == actionMapIndex &&
cutElement.GetIndexOfProperty(state) == actionIndex &&
cutElement.type == typeof(InputAction));
}
public readonly bool IsActionMapCut(int actionMapIndex)
{
if (cutElements == null)
return false;
var state = this;
return cutElements.Any(cutElement => cutElement.GetIndexOfProperty(state) == actionMapIndex && cutElement.type == typeof(InputActionMap));
}
public readonly List<CutElement> GetCutElements()
{
return m_CutElements;
}
private SerializedProperty GetSelectedActionMap()
{
return Selectors.GetActionMapAtIndex(serializedObject, selectedActionMapIndex)?.wrappedProperty;
}
private readonly InputControlScheme m_ControlScheme;
}
internal enum SelectionType
{
None,
Action,
Binding
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7a8e0b5c429811847b8c97a058e5884f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,508 @@
// UITK TreeView is not supported in earlier versions
// Therefore the UITK version of the InputActionAsset Editor is not available on earlier Editor versions either.
#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
using System;
using System.Linq;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEditor.PackageManager.UI;
using UnityEditor.ShortcutManagement;
using UnityEngine.UIElements;
using UnityEditor.UIElements;
namespace UnityEngine.InputSystem.Editor
{
// TODO: Remove when UIToolkit editor is complete and set as the default editor
[InitializeOnLoad]
internal static class EnableUITKEditor
{
static EnableUITKEditor()
{
}
}
internal class InputActionsEditorWindow : EditorWindow, IInputActionAssetEditor
{
// Register editor type via static constructor to enable asset monitoring
static InputActionsEditorWindow()
{
InputActionAssetEditor.RegisterType<InputActionsEditorWindow>();
}
static readonly Vector2 k_MinWindowSize = new Vector2(740, 450);
// For UI testing purpose
internal InputActionAsset currentAssetInEditor => m_AssetObjectForEditing;
[SerializeField] private InputActionAsset m_AssetObjectForEditing;
[SerializeField] private InputActionsEditorState m_State;
[SerializeField] private string m_AssetGUID;
private string m_AssetJson;
private bool m_IsDirty;
private StateContainer m_StateContainer;
private InputActionsEditorView m_View;
private InputActionsEditorSessionAnalytic m_Analytics;
private InputActionsEditorSessionAnalytic analytics =>
m_Analytics ??= new InputActionsEditorSessionAnalytic(
InputActionsEditorSessionAnalytic.Data.Kind.EditorWindow);
[OnOpenAsset]
public static bool OpenAsset(int instanceId, int line)
{
if (InputSystem.settings.IsFeatureEnabled(InputFeatureNames.kUseIMGUIEditorForAssets))
return false;
if (!InputActionImporter.IsInputActionAssetPath(AssetDatabase.GetAssetPath(instanceId)))
return false;
// 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;
string actionMapToSelect = null;
string actionToSelect = null;
// Means we're dealing with an InputActionReference, e.g. when expanding the an .input action asset
// on the Asset window and selecting an Action.
if (asset == null)
{
var actionReference = obj as InputActionReference;
if (actionReference != null && actionReference.asset != null)
{
asset = actionReference.asset;
actionMapToSelect = actionReference.action.actionMap?.name;
actionToSelect = actionReference.action?.name;
}
else
{
return false;
}
}
OpenWindow(asset, actionMapToSelect, actionToSelect);
return true;
}
private static InputActionsEditorWindow OpenWindow(InputActionAsset asset, string actionMapToSelect = null, string actionToSelect = null)
{
////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<T>() 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 existingWindow = InputActionAssetEditor.FindOpenEditor<InputActionsEditorWindow>(AssetDatabase.GetAssetPath(asset));
if (existingWindow != null)
{
existingWindow.Focus();
return existingWindow;
}
var window = GetWindow<InputActionsEditorWindow>();
window.m_IsDirty = false;
window.minSize = k_MinWindowSize;
window.SetAsset(asset, actionToSelect, actionMapToSelect);
window.Show();
return window;
}
/// <summary>
/// Open the specified <paramref name="asset"/> in an editor window. Used when someone hits the "Edit Asset" button in the
/// importer inspector.
/// </summary>
/// <param name="asset">The InputActionAsset to open.</param>
/// <returns>The editor window.</returns>
public static InputActionsEditorWindow OpenEditor(InputActionAsset asset)
{
return OpenWindow(asset, null, null);
}
private static GUIContent GetEditorTitle(InputActionAsset asset, bool isDirty)
{
var text = asset.name + " (Input Actions Editor)";
if (isDirty)
text = "(*) " + text;
return new GUIContent(text);
}
private void SetAsset(InputActionAsset asset, string actionToSelect = null, string actionMapToSelect = null)
{
var existingWorkingCopy = m_AssetObjectForEditing;
try
{
// Obtain and persist GUID for the associated asset
Debug.Assert(AssetDatabase.TryGetGUIDAndLocalFileIdentifier(asset, out m_AssetGUID, out long _),
$"Failed to get asset {asset.name} GUID");
// Attempt to update editor and internals based on associated asset
if (!TryUpdateFromAsset())
return;
// Select the action that was selected on the Asset window.
if (actionMapToSelect != null && actionToSelect != null)
{
m_State = m_State.SelectActionMap(actionMapToSelect);
m_State = m_State.SelectAction(actionToSelect);
}
BuildUI();
}
catch (Exception e)
{
Debug.LogException(e);
}
finally
{
if (existingWorkingCopy != null)
DestroyImmediate(existingWorkingCopy);
}
}
private void CreateGUI() // Only domain reload
{
// When opening the window for the first time there will be no state or asset yet.
// In that case, we don't do anything as SetAsset() will be called later and at that point the UI can be created.
// Here we only recreate the UI e.g. after a domain reload.
if (string.IsNullOrEmpty(m_AssetGUID))
return;
// After domain reloads the state will be in a invalid state as some of the fields
// cannot be serialized and will become null.
// Therefore we recreate the state here using the fields which were saved.
if (m_State.serializedObject == null)
{
InputActionAsset workingCopy = null;
try
{
var assetPath = AssetDatabase.GUIDToAssetPath(m_AssetGUID);
var asset = AssetDatabase.LoadAssetAtPath<InputActionAsset>(assetPath);
if (asset == null)
throw new Exception($"Failed to load asset \"{assetPath}\". The file may have been deleted or moved.");
m_AssetJson = InputActionsEditorWindowUtils.ToJsonWithoutName(asset);
if (m_AssetObjectForEditing == null)
{
workingCopy = InputActionAssetManager.CreateWorkingCopy(asset);
if (m_State.m_Analytics == null)
m_State.m_Analytics = analytics;
m_State = new InputActionsEditorState(m_State, new SerializedObject(workingCopy));
m_AssetObjectForEditing = workingCopy;
}
else
m_State = new InputActionsEditorState(m_State, new SerializedObject(m_AssetObjectForEditing));
m_IsDirty = HasContentChanged();
}
catch (Exception e)
{
Debug.LogException(e);
if (workingCopy != null)
DestroyImmediate(workingCopy);
Close();
return;
}
}
BuildUI();
}
private void CleanupStateContainer()
{
if (m_StateContainer != null)
{
m_StateContainer.StateChanged -= OnStateChanged;
m_StateContainer = null;
}
}
private void BuildUI()
{
CleanupStateContainer();
if (m_State.m_Analytics == null)
m_State.m_Analytics = m_Analytics;
m_StateContainer = new StateContainer(m_State, m_AssetGUID);
m_StateContainer.StateChanged += OnStateChanged;
rootVisualElement.Clear();
if (!rootVisualElement.styleSheets.Contains(InputActionsEditorWindowUtils.theme))
rootVisualElement.styleSheets.Add(InputActionsEditorWindowUtils.theme);
m_View = new InputActionsEditorView(rootVisualElement, m_StateContainer, false, () => Save(isAutoSave: false));
m_StateContainer.Initialize(rootVisualElement.Q("action-editor"));
}
private void OnStateChanged(InputActionsEditorState newState, UIRebuildMode editorRebuildMode)
{
DirtyInputActionsEditorWindow(newState);
m_State = newState;
#if UNITY_INPUT_SYSTEM_INPUT_ACTIONS_EDITOR_AUTO_SAVE_ON_FOCUS_LOST
// No action taken apart from setting dirty flag, auto-save triggered as part of having a dirty asset
// and editor loosing focus instead.
#else
if (InputEditorUserSettings.autoSaveInputActionAssets)
Save(isAutoSave: false);
#endif
}
private void UpdateWindowTitle()
{
titleContent = GetEditorTitle(GetEditedAsset(), m_IsDirty);
}
private InputActionAsset GetEditedAsset()
{
return m_State.serializedObject.targetObject as InputActionAsset;
}
private void Save(bool isAutoSave)
{
var path = AssetDatabase.GUIDToAssetPath(m_AssetGUID);
#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
var projectWideActions = InputSystem.actions;
if (projectWideActions != null && path == AssetDatabase.GetAssetPath(projectWideActions))
ProjectWideActionsAsset.Verify(GetEditedAsset());
#endif
if (InputActionAssetManager.SaveAsset(path, GetEditedAsset().ToJson()))
TryUpdateFromAsset();
if (isAutoSave)
analytics.RegisterAutoSave();
else
analytics.RegisterExplicitSave();
}
private bool HasContentChanged()
{
var editedAsset = GetEditedAsset();
var editedAssetJson = InputActionsEditorWindowUtils.ToJsonWithoutName(editedAsset);
return editedAssetJson != m_AssetJson;
}
private void DirtyInputActionsEditorWindow(InputActionsEditorState newState)
{
#if UNITY_INPUT_SYSTEM_INPUT_ACTIONS_EDITOR_AUTO_SAVE_ON_FOCUS_LOST
// Window is dirty is equivalent to if asset has changed
var isWindowDirty = HasContentChanged();
#else
// Window is dirty is never true since every change is auto-saved
var isWindowDirty = !InputEditorUserSettings.autoSaveInputActionAssets && HasContentChanged();
#endif
if (m_IsDirty == isWindowDirty)
return;
m_IsDirty = isWindowDirty;
UpdateWindowTitle();
}
private void OnEnable()
{
analytics.Begin();
}
private void OnDisable()
{
analytics.End();
}
private void OnFocus()
{
analytics.RegisterEditorFocusIn();
}
private void OnLostFocus()
{
// Auto-save triggers on focus-lost instead of on every change
#if UNITY_INPUT_SYSTEM_INPUT_ACTIONS_EDITOR_AUTO_SAVE_ON_FOCUS_LOST
if (InputEditorUserSettings.autoSaveInputActionAssets && m_IsDirty)
Save(isAutoSave: true);
#endif
analytics.RegisterEditorFocusOut();
}
private void HandleOnDestroy()
{
// Do we have unsaved changes that we need to ask the user to save or discard?
if (!m_IsDirty)
return;
// Get target asset path from GUID, if this fails file no longer exists and we need to abort.
var assetPath = AssetDatabase.GUIDToAssetPath(m_AssetGUID);
if (string.IsNullOrEmpty(assetPath))
return;
// Prompt user with a dialog
var result = Dialog.InputActionAsset.ShowSaveChanges(assetPath);
switch (result)
{
case Dialog.Result.Save:
Save(isAutoSave: false);
break;
case Dialog.Result.Cancel:
// Cancel editor quit. (open new editor window with the edited asset)
ReshowEditorWindowWithUnsavedChanges();
break;
case Dialog.Result.Discard:
// Don't save, quit - reload the old asset from the json to prevent the asset from being dirtied
break;
default:
throw new ArgumentOutOfRangeException(nameof(result));
}
}
private void OnDestroy()
{
HandleOnDestroy();
// Clean-up
CleanupStateContainer();
if (m_AssetObjectForEditing != null)
DestroyImmediate(m_AssetObjectForEditing);
m_View?.DestroyView();
}
private void ReshowEditorWindowWithUnsavedChanges()
{
var window = CreateWindow<InputActionsEditorWindow>();
// Move/transfer ownership of m_AssetObjectForEditing to new window
window.m_AssetObjectForEditing = m_AssetObjectForEditing;
m_AssetObjectForEditing = null;
// Move/transfer ownership of m_State to new window (struct)
window.m_State = m_State;
m_State = new InputActionsEditorState();
// Just copy trivial arguments
window.m_AssetGUID = m_AssetGUID;
window.m_AssetJson = m_AssetJson;
window.m_IsDirty = m_IsDirty;
// Note that view and state container will get destroyed with this window instance
// and recreated for this window below
window.BuildUI();
window.Show();
// Make sure window title is up to date
window.UpdateWindowTitle();
}
private bool TryUpdateFromAsset()
{
Debug.Assert(!string.IsNullOrEmpty(m_AssetGUID), "Asset GUID is empty");
var assetPath = AssetDatabase.GUIDToAssetPath(m_AssetGUID);
if (assetPath == null)
{
Debug.LogWarning(
$"Failed to open InputActionAsset with GUID {m_AssetGUID}. The asset might have been deleted.");
return false;
}
InputActionAsset workingCopy = null;
try
{
var asset = AssetDatabase.LoadAssetAtPath<InputActionAsset>(assetPath);
workingCopy = InputActionAssetManager.CreateWorkingCopy(asset);
m_AssetJson = InputActionsEditorWindowUtils.ToJsonWithoutName(asset);
m_State = new InputActionsEditorState(m_State, new SerializedObject(workingCopy));
m_IsDirty = false;
}
catch (Exception e)
{
if (workingCopy != null)
DestroyImmediate(workingCopy);
Debug.LogException(e);
Close();
return false;
}
m_AssetObjectForEditing = workingCopy;
UpdateWindowTitle();
return true;
}
#region IInputActionEditorWindow
public string assetGUID => m_AssetGUID;
public bool isDirty => m_IsDirty;
public void OnAssetMoved()
{
// When an asset is moved, we only need to update window title since content is unchanged
UpdateWindowTitle();
}
public void OnAssetDeleted()
{
// When associated asset is deleted on disk, just close the editor, but also mark the editor
// as not being dirty to avoid prompting the user to save changes.
m_IsDirty = false;
Close();
}
public void OnAssetImported()
{
// If the editor has pending changes done by the user and the contents changes on disc, there
// is not much we can do about it but to ignore loading the changes. If the editors asset is
// unmodified, we can refresh the editor with the latest content from disc.
if (m_IsDirty)
return;
// If our asset has disappeared from disk, just close the window.
var assetPath = AssetDatabase.GUIDToAssetPath(assetGUID);
if (string.IsNullOrEmpty(assetPath))
{
m_IsDirty = false; // Avoid checks
Close();
return;
}
SetAsset(AssetDatabase.LoadAssetAtPath<InputActionAsset>(assetPath));
}
#endregion
#region Shortcuts
[Shortcut("Input Action Editor/Save", typeof(InputActionsEditorWindow), KeyCode.S, ShortcutModifiers.Action)]
private static void SaveShortcut(ShortcutArguments arguments)
{
var window = (InputActionsEditorWindow)arguments.context;
window.Save(isAutoSave: false);
}
[Shortcut("Input Action Editor/Add Action Map", typeof(InputActionsEditorWindow), KeyCode.M, ShortcutModifiers.Alt)]
private static void AddActionMapShortcut(ShortcutArguments arguments)
{
var window = (InputActionsEditorWindow)arguments.context;
window.m_StateContainer.Dispatch(Commands.AddActionMap());
}
[Shortcut("Input Action Editor/Add Action", typeof(InputActionsEditorWindow), KeyCode.A, ShortcutModifiers.Alt)]
private static void AddActionShortcut(ShortcutArguments arguments)
{
var window = (InputActionsEditorWindow)arguments.context;
window.m_StateContainer.Dispatch(Commands.AddAction());
}
[Shortcut("Input Action Editor/Add Binding", typeof(InputActionsEditorWindow), KeyCode.B, ShortcutModifiers.Alt)]
private static void AddBindingShortcut(ShortcutArguments arguments)
{
var window = (InputActionsEditorWindow)arguments.context;
window.m_StateContainer.Dispatch(Commands.AddBinding());
}
#endregion
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e68462a09b6ba104abce11d2d157d0b2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,38 @@
#if UNITY_EDITOR
using System;
using System.IO;
using UnityEditor;
using UnityEngine.UIElements;
namespace UnityEngine.InputSystem.Editor
{
internal static class InputActionsEditorWindowUtils
{
/// <summary>
/// Return a relative path to the currently active theme style sheet.
/// </summary>
public static StyleSheet theme => EditorGUIUtility.isProSkin
? AssetDatabase.LoadAssetAtPath<StyleSheet>(InputActionsEditorConstants.PackagePath + InputActionsEditorConstants.ResourcesPath + "/InputAssetEditorDark.uss")
: AssetDatabase.LoadAssetAtPath<StyleSheet>(InputActionsEditorConstants.PackagePath + InputActionsEditorConstants.ResourcesPath + "/InputAssetEditorLight.uss");
// Similar to InputActionAsset.WriteFileJson but excludes the name
[Serializable]
private struct WriteFileJsonNoName
{
public InputActionMap.WriteMapJson[] maps;
public InputControlScheme.SchemeJson[] controlSchemes;
}
// Similar to InputActionAsset.ToJson() but converts to JSON excluding the name property and any additional JSON
// content that may be part of the file not recognized as required data.
public static string ToJsonWithoutName(InputActionAsset asset)
{
return JsonUtility.ToJson(new WriteFileJsonNoName
{
maps = InputActionMap.WriteFileJson.FromMaps(asset.m_ActionMaps).maps,
controlSchemes = InputControlScheme.SchemeJson.ToJson(asset.m_ControlSchemes),
}, prettyPrint: true);
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3b8e35f20292579409c899395e30948f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7894838fa258e3a4197077ddbda0eec5
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,9 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi="http://www.w3.org/2001/XMLSchema-instance" engine="UnityEngine.UIElements" editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="True">
<ui:VisualElement name="item-row" class="unity-list-view__item" style="flex-direction: column; flex-grow: 1; justify-content: center; margin-left: 4px;">
<ui:VisualElement name="row" style="flex-direction: row; border-left-width: 0; border-left-color: rgb(89, 89, 89);">
<ui:Toggle name="expando" class="unity-foldout__toggle" style="visibility: hidden; margin-left: 3px;" />
<ui:VisualElement name="icon" style="justify-content: center; background-image: url(&apos;project://database/Packages/com.unity.inputsystem/InputSystem/Editor/Icons/d_InputControl.png?fileID=2800000&amp;guid=399cd90f4e31041e692a7d3a8b1aa4d0&amp;type=3#d_InputControl&apos;); width: 16px; height: 16px;" />
<ui:Label text="binding-name" display-tooltip-when-elided="true" name="name" style="flex-grow: 1; justify-content: center; align-items: stretch; margin-left: 4px; -unity-font-style: normal;" />
</ui:VisualElement>
</ui:VisualElement>
</ui:UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 449c8e5ca27af8e4a9db710c8be5e4df
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -0,0 +1,5 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi="http://www.w3.org/2001/XMLSchema-instance" engine="UnityEngine.UIElements" editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="True">
<ui:VisualElement>
<ui:DropdownField label="Composite Type" index="-1" choices="System.Collections.Generic.List`1[System.String]" name="composite-type-dropdown" tooltip="Type of composite. Allows changing the composite type retroactively. Doing so will modify the bindings that are part of the composite." />
</ui:VisualElement>
</ui:UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 2bd2d0548382d58489e8c2999daf4139
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -0,0 +1,6 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi="http://www.w3.org/2001/XMLSchema-instance" engine="UnityEngine.UIElements" editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="True">
<ui:VisualElement>
<ui:IMGUIContainer name="path-editor-container" />
<ui:DropdownField label="Composite Part" index="-1" choices="System.Collections.Generic.List`1[System.String]" name="composite-part-dropdown" tooltip="The named part of the composite that the binding is assigned to. Multiple bindings may be assigned the same part. All controls from all bindings that are assigned the same part will collectively feed values into that part of the composite." />
</ui:VisualElement>
</ui:UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: acfae125fa5d7904987a696b3993d432
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -0,0 +1,19 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi="http://www.w3.org/2001/XMLSchema-instance" engine="UnityEngine.UIElements" editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="True">
<ui:VisualElement>
<ui:VisualElement name="SchemeName">
<ui:TextField picking-mode="Ignore" label="Scheme Name" value="New control scheme" text="New Control Scheme" name="control-scheme-name" />
</ui:VisualElement>
<ui:VisualElement style="flex-direction: row; flex-grow: 0;">
<ui:MultiColumnListView focusable="true" reorderable="true" show-foldout-header="false" show-add-remove-footer="true" reorder-mode="Animated" show-border="true" show-bound-collection-size="false" name="control-schemes-list-view" show-alternating-row-backgrounds="ContentOnly" style="flex-grow: 1;">
<ui:Columns>
<ui:Column name="device-type" title="Device Type" width="250" resizable="false" sortable="false" />
<ui:Column name="required" title="Required" width="70" resizable="false" sortable="false" />
</ui:Columns>
</ui:MultiColumnListView>
</ui:VisualElement>
</ui:VisualElement>
<ui:VisualElement style="flex-direction: row; align-items: stretch; justify-content: space-around;">
<ui:Button text="Cancel" display-tooltip-when-elided="true" name="cancel-button" style="flex-grow: 1;" />
<ui:Button text="Save" display-tooltip-when-elided="true" name="save-button" style="flex-grow: 1;" />
</ui:VisualElement>
</ui:UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 303ef5b13740f9a4db252c50122d2c62
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -0,0 +1,10 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi="http://www.w3.org/2001/XMLSchema-instance" engine="UnityEngine.UIElements" editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="False">
<Style src="project://database/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/PackageResources/InputActionsEditorStyles.uss?fileID=7433441132597879392&amp;guid=7dac9c49a90bca4499371d0adc9b617b&amp;type=3#InputActionsEditorStyles" />
<ui:VisualElement name="item-row" class="unity-list-view__item" style="flex-direction: row; flex-grow: 1; justify-content: space-between; margin-left: 4px; flex-shrink: 0;">
<ui:VisualElement name="row" style="flex-direction: row; border-left-width: 0; border-left-color: rgb(89, 89, 89); justify-content: flex-start; align-items: center;">
<ui:TextField picking-mode="Ignore" name="rename-text-field" is-delayed="true" focusable="true" class="unity-input-actions-editor-hidden" style="visibility: visible; flex-shrink: 1;" />
<ui:Label text="binding-name" display-tooltip-when-elided="true" name="name" style="flex-grow: 1; justify-content: center; align-items: stretch; margin-left: 4px; -unity-font-style: normal;" />
</ui:VisualElement>
<ui:Button text="+" display-tooltip-when-elided="true" enable-rich-text="false" name="add-new-binding-button" style="opacity: 1; background-color: rgba(255, 255, 255, 0); border-left-color: rgba(255, 255, 255, 0); border-right-color: rgba(255, 255, 255, 0); border-top-color: rgba(255, 255, 255, 0); border-bottom-color: rgba(255, 255, 255, 0); display: none; align-items: flex-end; align-self: auto; flex-direction: row-reverse;" />
</ui:VisualElement>
</ui:UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 3fa44185ed614414ebab354dfe5a06b6
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -0,0 +1,60 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi="http://www.w3.org/2001/XMLSchema-instance" engine="UnityEngine.UIElements" editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="True">
<Style src="project://database/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/PackageResources/InputActionsEditorStyles.uss?fileID=7433441132597879392&amp;guid=7dac9c49a90bca4499371d0adc9b617b&amp;type=3#InputActionsEditorStyles" />
<ui:VisualElement name="action-editor" style="flex-direction: column; flex-grow: 1;">
<ui:VisualElement name="control-schemes-toolbar-container">
<uie:Toolbar>
<ui:VisualElement style="flex-direction: row; flex-grow: 1;">
<uie:ToolbarMenu display-tooltip-when-elided="true" text="No Control Schemes" name="control-schemes-toolbar-menu" style="min-width: 135px;" />
<uie:ToolbarMenu display-tooltip-when-elided="true" text="All Devices" enabled="false" name="control-schemes-filter-toolbar-menu" />
</ui:VisualElement>
<ui:VisualElement>
<uie:ToolbarSearchField focusable="true" name="search-actions-text-field" class="search-field" style="display: none;" />
</ui:VisualElement>
<ui:VisualElement name="save-asset-toolbar-container" style="flex-direction: row; justify-content: flex-end;">
<uie:ToolbarButton text="Save Asset" display-tooltip-when-elided="true" name="save-asset-toolbar-button" style="align-items: auto;" />
<uie:ToolbarToggle focusable="false" label="Auto-Save" name="auto-save-toolbar-toggle" style="width: 69px;" />
</ui:VisualElement>
</uie:Toolbar>
</ui:VisualElement>
<ui:VisualElement name="body" style="flex-direction: column; flex-grow: 1;">
<ui:TwoPaneSplitView name="actions-split-view" fixed-pane-initial-dimension="200">
<ui:VisualElement name="action-maps-container" class="body-panel-container actions-container">
<ui:VisualElement name="header" class="body-panel-header">
<ui:Label text="Action Maps" display-tooltip-when-elided="true" style="flex-grow: 1;" />
<ui:Button text="+" display-tooltip-when-elided="true" name="add-new-action-map-button" style="align-items: auto;" />
</ui:VisualElement>
<ui:VisualElement name="body">
<ui:ListView focusable="true" name="action-maps-list-view" />
</ui:VisualElement>
<ui:VisualElement name="rclick-area-to-add-new-action-map" style="flex-direction: column; flex-grow: 1;" />
</ui:VisualElement>
<ui:TwoPaneSplitView name="actions-and-properties-split-view" fixed-pane-index="1" fixed-pane-initial-dimension="320" style="height: auto; min-width: 450px;">
<ui:VisualElement name="actions-container" class="body-panel-container">
<ui:VisualElement name="header" class="body-panel-header" style="justify-content: space-between;">
<ui:Label text="Actions" display-tooltip-when-elided="true" name="actions-label" />
<ui:Button text="+" display-tooltip-when-elided="true" name="add-new-action-button" style="align-items: auto;" />
</ui:VisualElement>
<ui:VisualElement name="body">
<ui:TreeView view-data-key="unity-tree-view" focusable="true" name="actions-tree-view" show-border="false" reorderable="true" show-alternating-row-backgrounds="None" fixed-item-height="20" />
</ui:VisualElement>
<ui:VisualElement name="rclick-area-to-add-new-action" style="flex-direction: column; flex-grow: 1;" />
</ui:VisualElement>
<ui:VisualElement name="properties-container" class="body-panel-container body-panel-container" style="min-width: 310px;">
<ui:VisualElement name="header" class="body-panel-header">
<ui:Label text="Action Properties" display-tooltip-when-elided="true" name="properties-header-label" />
</ui:VisualElement>
<ui:ScrollView name="properties-scrollview">
<ui:Foldout text="Action Properties" name="properties-foldout" class="properties-foldout" />
<ui:Foldout text="Interactions" name="interactions-foldout" class="properties-foldout name-and-parameters-list-view">
<ui:Label text="No interactions have been added." name="no-parameters-added-label" display-tooltip-when-elided="true" class="name-and-parameter-empty-label" style="display: flex;" />
</ui:Foldout>
<ui:Foldout text="Processors" name="processors-foldout" class="properties-foldout name-and-parameters-list-view">
<ui:Label text="No processors have been added." name="no-parameters-added-label" display-tooltip-when-elided="true" class="name-and-parameter-empty-label" />
</ui:Foldout>
</ui:ScrollView>
</ui:VisualElement>
</ui:TwoPaneSplitView>
</ui:TwoPaneSplitView>
</ui:VisualElement>
</ui:VisualElement>
</ui:UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: c662e0d3cc3c4f948808f9847f715ef2
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -0,0 +1,215 @@
.unity-input-actions-editor-hidden {
display: none;
}
.asset-menu-button-dark-theme {
background-image: resource('d__Menu.png');
}
.asset-menu-button {
background-image: resource('_Menu.png');
-unity-background-scale-mode: scale-to-fit;
}
.body-panel-container {
min-width: 150px;
border-top-width: 1px;
border-left-color: rgb(25, 25, 25);
border-right-color: rgb(25, 25, 25);
border-top-color: rgb(25, 25, 25);
border-bottom-color: rgb(25, 25, 25);
border-right-width: 0;
}
#actions-container {
min-width: 225px;
}
#selected-action-map-dropdown > .unity-popup-field__input {
-unity-font-style: normal;
}
#add-new-action-map-button {
background-color: transparent;
border-width: 0;
}
#add-new-action-button {
background-color: transparent;
border-width: 0;
}
#bindings-container {
min-width: 225px;
border-left-width: 0;
margin-left: 0;
}
.add-interaction-processor-button {
padding-top: 0;
font-size: 12px;
border-width: 0;
background-color: transparent;
margin-right: 5px;
}
.header {
height: 22px;
flex-direction: row;
margin-left: 10px;
margin-right: 7px;
margin-top: 1px;
margin-bottom: 7px;
}
.header-label {
font-size: 19px;
-unity-font-style: bold;
margin: 0;
padding: 0;
width: auto;
flex-grow: 1;
}
.header-search-box {
justify-content: flex-start;
position: relative;
right: auto;
width: 171px;
border-left-color: rgb(23, 23, 23);
border-right-color: rgb(23, 23, 23);
border-top-color: rgb(23, 23, 23);
border-bottom-color: rgb(23, 23, 23);
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
margin-left: 3px;
margin-right: 3px;
margin-top: 1px;
margin-bottom: 1px;
align-items: auto;
padding-top: 7px;
padding-bottom: 7px;
}
.tree-view-item-icon{
justify-content: center;
background-image: resource('Packages/com.unity.inputsystem/InputSystem/Editor/Icons/d_InputControl.png');
width: 16px;
height: 16px;
}
.body-panel-header {
background-color: var(--unity-colors-toolbar-background);
border-bottom-color: var(--unity-colors-toolbar-border);
flex-direction: row;
margin-left: 0;
margin-right: 0;
margin-top: 0;
margin-bottom: 0;
padding-left: 5px;
padding-right: 3px;
padding-top: 4px;
padding-bottom: 4px;
border-bottom-width: 1px;
height: 28px;
min-height: 28px;
font-size: 14px;
-unity-font-style: bold;
align-items: center;
justify-content: flex-start;
-unity-text-align: middle-left;
}
.properties-foldout {
margin: 0;
padding-bottom: 15px;
}
.properties-foldout-toggle {
background-color: var(--input-editor-colors-properties-foldout);
}
.name-and-parameter-empty-label {
margin-top: 5px;
font-size: 12px;
color: rgb(130, 130, 130);
}
.name-and-parameters-list-foldout {
}
.name-and-parameters-list-view .name-and-parameters-list-foldout-button {
width: 12px;
height: 12px;
background-color: transparent;
border-width: 0;
}
.name-and-parameters-list-view .open-settings-button {
width: 120px;
align-items: flex-end;
}
.float-field {
width: 50px;
display: initial;
}
.name-and-parameters-list-view .up {
background-image: resource('Packages/com.unity.inputsystem/InputSystem/Editor/Icons/ChevronUp.png');
}
.name-and-parameters-list-view .upDarkTheme {
background-image: resource('Packages/com.unity.inputsystem/InputSystem/Editor/Icons/d_ChevronUp.png');
}
.name-and-parameters-list-view .down {
background-image: resource('Packages/com.unity.inputsystem/InputSystem/Editor/Icons/ChevronDown.png');
}
.name-and-parameters-list-view .downDarkTheme {
background-image: resource('Packages/com.unity.inputsystem/InputSystem/Editor/Icons/d_ChevronDown.png');
}
.name-and-parameters-list-view .delete {
background-image: resource('Toolbar Minus.png');
}
.name-and-parameters-list-view .deleteDarkTheme {
background-image: resource('d_Toolbar Minus.png');
}
.add-binging-button-dark-theme {
background-image: resource('d_Toolbar Plus More.png');
-unity-background-scale-mode: scale-to-fit;
}
.add-binging-button {
background-image: resource('Toolbar Plus More.png');
-unity-background-scale-mode: scale-to-fit;
}
.search-field {
display: none;
width: 190px;
}
.unity-two-pane-split-view__dragline-anchor {
background-color: rgb(25, 25, 25);
}
#control-scheme-usage-title {
margin: 3px;
-unity-font-style: bold;
}
.matching-controls {
display: flex;
flex-grow: 1;
}
.matching-controls-labels {
margin: 1px;
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7dac9c49a90bca4499371d0adc9b617b
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
disableValidation: 0

View File

@@ -0,0 +1,14 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi="http://www.w3.org/2001/XMLSchema-instance" engine="UnityEngine.UIElements" editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="True">
<Style src="project://database/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/PackageResources/InputActionsEditorStyles.uss?fileID=7433441132597879392&amp;guid=7dac9c49a90bca4499371d0adc9b617b&amp;type=3#InputActionsEditorStyles" />
<ui:VisualElement name="header" class="header">
<ui:Label text="Input Actions" display-tooltip-when-elided="true" name="title-label" class="header-label" />
<ui:VisualElement name="asset-menu" style="flex-grow: 0; background-color: rgba(0, 0, 0, 0); width: 16px; flex-shrink: 0; -unity-background-scale-mode: scale-to-fit;" />
</ui:VisualElement>
<ui:VisualElement name="current-asset-section">
<uie:ObjectField name="current-asset" type="UnityEngine.InputSystem.InputActionAsset, Unity.InputSystem" label="Project-wide Actions" />
</ui:VisualElement>
<ui:VisualElement name="missing-asset-section">
<ui:HelpBox text="Actions for the Input System are stored in an Action Asset. You can assign an Action Asset as project-wide in the field above to make them accessible directly through the InputSystem.actions API.&#10;&#10;Click the button below to create a new Action Asset containing default actions, which will be assigned as project-wide." message-type="Info"/>
<ui:Button text="Create and assign a default project-wide Action Asset" name="create-asset"/>
</ui:VisualElement>
</ui:UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 75a33987e21d5ac449deecea3dcd864f
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -0,0 +1,11 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi="http://www.w3.org/2001/XMLSchema-instance" engine="UnityEngine.UIElements" editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="False">
<Style src="project://database/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/PackageResources/InputActionsEditorStyles.uss?fileID=7433441132597879392&amp;guid=7dac9c49a90bca4499371d0adc9b617b&amp;type=3#InputActionsEditorStyles" />
<ui:VisualElement name="item-row" class="unity-list-view__item" style="flex-direction: row; flex-grow: 1; justify-content: space-between; margin-left: 4px; flex-shrink: 0;">
<ui:VisualElement name="row" style="flex-direction: row; border-left-width: 0; border-left-color: rgb(89, 89, 89); justify-content: flex-start; align-items: center;">
<ui:VisualElement name="icon" class="tree-view-item-icon" style="height: 19px; -unity-background-scale-mode: scale-to-fit;" />
<ui:TextField picking-mode="Ignore" name="rename-text-field" is-delayed="true" focusable="true" class="unity-input-actions-editor-hidden" style="visibility: visible; flex-shrink: 1;" />
<ui:Label text="binding-name" display-tooltip-when-elided="true" name="name" style="flex-grow: 1; justify-content: center; align-items: stretch; margin-left: 4px; -unity-font-style: normal;" />
</ui:VisualElement>
<ui:Button display-tooltip-when-elided="true" enable-rich-text="false" name="add-new-binding-button" style="opacity: 1; background-color: rgba(255, 255, 255, 0); border-left-color: rgba(255, 255, 255, 0); border-right-color: rgba(255, 255, 255, 0); border-top-color: rgba(255, 255, 255, 0); border-bottom-color: rgba(255, 255, 255, 0); display: none; align-items: flex-end; align-self: auto; flex-direction: row-reverse; margin-right: 6px;" />
</ui:VisualElement>
</ui:UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 452df07a8ad0c944c879a484d5890e61
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -0,0 +1,4 @@
:root {
--input-editor-colors-properties-foldout: rgb(34, 34, 34);
--input-editor-colors-properties-foldout-border: #808080;
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cca9ce0c8554d5244930f14c21489ac1
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
disableValidation: 0

View File

@@ -0,0 +1,4 @@
:root {
--input-editor-colors-properties-foldout: #DFDFDF;
--input-editor-colors-properties-foldout-border: #808080;
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5e7024ed0fd03984289320d04507bc0c
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
disableValidation: 0

View File

@@ -0,0 +1,6 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi="http://www.w3.org/2001/XMLSchema-instance" engine="UnityEngine.UIElements" editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="True">
<Style src="project://database/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/PackageResources/InputActionsEditorStyles.uss?fileID=7433441132597879392&amp;guid=7dac9c49a90bca4499371d0adc9b617b&amp;type=3#InputActionsEditorStyles" />
<ui:VisualElement style="margin-bottom: 0; border-bottom-width: 1px; padding-bottom: 5px;">
<ui:Foldout text="Foldout" name="Foldout" value="true" class="name-and-parameters-list-foldout" style="align-items: stretch;" />
</ui:VisualElement>
</ui:UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 9cbd73d51dc491b4888055ae65cd9545
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -0,0 +1,28 @@
#if UNITY_EDITOR
using System;
namespace UnityEngine.InputSystem.Editor
{
internal class ReactiveProperty<T>
{
private T m_Value;
public event Action<T> Changed;
public T value
{
get => m_Value;
set
{
m_Value = value;
Changed?.Invoke(m_Value);
}
}
public void SetValueWithoutChangeNotification(T value)
{
m_Value = value;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a6f2f33363708814e8530579e2d8cf2d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,90 @@
#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
using System;
using UnityEditor;
namespace UnityEngine.InputSystem.Editor
{
internal readonly struct SerializedInputAction
{
public SerializedInputAction(SerializedProperty serializedProperty)
{
// TODO: check that the passed serialized property actually is an InputAction. Reflect over all
// serialized fields and make sure they're present?
wrappedProperty = serializedProperty ?? throw new ArgumentNullException(nameof(serializedProperty));
id = serializedProperty.FindPropertyRelative(nameof(InputAction.m_Id)).stringValue;
name = serializedProperty.FindPropertyRelative(nameof(InputAction.m_Name)).stringValue;
expectedControlType = ReadExpectedControlType(serializedProperty);
type = (InputActionType)serializedProperty.FindPropertyRelative(nameof(InputAction.m_Type)).intValue;
interactions = serializedProperty.FindPropertyRelative(nameof(InputAction.m_Interactions)).stringValue;
processors = serializedProperty.FindPropertyRelative(nameof(InputAction.m_Processors)).stringValue;
propertyPath = wrappedProperty.propertyPath;
initialStateCheck = ReadInitialStateCheck(serializedProperty);
actionTypeTooltip = serializedProperty.FindPropertyRelative(nameof(InputAction.m_Type)).GetTooltip();
expectedControlTypeTooltip = serializedProperty.FindPropertyRelative(nameof(InputAction.m_ExpectedControlType)).GetTooltip();
}
public string id { get; }
public string name { get; }
public string expectedControlType { get; }
public InputActionType type { get; }
public string interactions { get; }
public string processors { get; }
public string propertyPath { get; }
public bool initialStateCheck { get; }
public string actionTypeTooltip { get; }
public string expectedControlTypeTooltip { get; }
public SerializedProperty wrappedProperty { get; }
private static string ReadExpectedControlType(SerializedProperty serializedProperty)
{
var controlType = serializedProperty.FindPropertyRelative(nameof(InputAction.m_ExpectedControlType)).stringValue;
if (!string.IsNullOrEmpty(controlType))
return controlType;
var actionType = serializedProperty.FindPropertyRelative(nameof(InputAction.m_Type)).intValue;
return actionType == (int)InputActionType.Button ? "Button" : null;
}
private static bool ReadInitialStateCheck(SerializedProperty serializedProperty)
{
var actionFlags = serializedProperty.FindPropertyRelative(nameof(InputAction.m_Flags));
return (actionFlags.intValue & (int)InputAction.ActionFlags.WantsInitialStateCheck) != 0;
}
public bool Equals(SerializedInputAction other)
{
return name == other.name
&& expectedControlType == other.expectedControlType
&& type == other.type
&& interactions == other.interactions
&& processors == other.processors
&& initialStateCheck == other.initialStateCheck
&& actionTypeTooltip == other.actionTypeTooltip
&& expectedControlTypeTooltip == other.expectedControlTypeTooltip
&& propertyPath == other.propertyPath;
}
public override bool Equals(object obj)
{
return obj is SerializedInputAction other && Equals(other);
}
public override int GetHashCode()
{
var hashCode = new HashCode();
hashCode.Add(name);
hashCode.Add(expectedControlType);
hashCode.Add((int)type);
hashCode.Add(interactions);
hashCode.Add(processors);
hashCode.Add(initialStateCheck);
hashCode.Add(actionTypeTooltip);
hashCode.Add(expectedControlTypeTooltip);
hashCode.Add(propertyPath);
return hashCode.ToHashCode();
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fcffff3de13f4304a98bb50b1222399a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,20 @@
#if UNITY_EDITOR
using System;
using UnityEditor;
namespace UnityEngine.InputSystem.Editor
{
internal readonly struct SerializedInputActionMap
{
public SerializedInputActionMap(SerializedProperty serializedProperty)
{
wrappedProperty = serializedProperty ?? throw new ArgumentNullException(nameof(serializedProperty));
name = serializedProperty.FindPropertyRelative(nameof(InputActionMap.m_Name)).stringValue;
}
public string name { get; }
public SerializedProperty wrappedProperty { get; }
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d92ba71f7bef7074eb5d8753a9d09521
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,121 @@
#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
using System;
using System.Linq;
using UnityEditor;
namespace UnityEngine.InputSystem.Editor
{
/// <summary>
/// A read-only view of the data in a SerializedProperty representing an InputBinding.
/// <remarks>
/// After construction this class loses all connection to the original SerializedProperty. You cannot
/// use it for edit operations.
/// </remarks>
/// </summary>
internal readonly struct SerializedInputBinding
{
public SerializedInputBinding(SerializedProperty serializedProperty)
{
wrappedProperty = serializedProperty ?? throw new ArgumentNullException(nameof(serializedProperty));
id = serializedProperty.FindPropertyRelative("m_Id").stringValue;
name = serializedProperty.FindPropertyRelative("m_Name").stringValue;
path = serializedProperty.FindPropertyRelative("m_Path").stringValue;
interactions = serializedProperty.FindPropertyRelative("m_Interactions").stringValue;
processors = serializedProperty.FindPropertyRelative("m_Processors").stringValue;
action = serializedProperty.FindPropertyRelative("m_Action").stringValue;
propertyPath = wrappedProperty.propertyPath;
var bindingGroups = serializedProperty.FindPropertyRelative(nameof(InputBinding.m_Groups)).stringValue;
controlSchemes = bindingGroups != null
? bindingGroups.Split(InputBinding.kSeparatorString, StringSplitOptions.RemoveEmptyEntries)
: Array.Empty<string>();
flags = (InputBinding.Flags)serializedProperty.FindPropertyRelative(nameof(InputBinding.m_Flags)).intValue;
indexOfBinding = serializedProperty.GetIndexOfArrayElement();
isComposite = (flags & InputBinding.Flags.Composite) == InputBinding.Flags.Composite;
isPartOfComposite = (flags & InputBinding.Flags.PartOfComposite) == InputBinding.Flags.PartOfComposite;
compositePath = string.Empty;
if (isPartOfComposite)
compositePath = GetCompositePath(serializedProperty);
}
public string name { get; }
public string id { get; }
public string path { get; }
public string interactions { get; }
public string processors { get; }
public string action { get; }
public string propertyPath { get; }
public string[] controlSchemes { get; }
public InputBinding.Flags flags { get; }
/// <summary>
/// The index of this binding in the array that it is stored in.
/// </summary>
public int indexOfBinding { get; }
public bool isComposite { get; }
public bool isPartOfComposite { get; }
/// <summary>
/// Get the composite path of this input binding, which must itself be a composite part.
/// </summary>
/// <remarks>
/// The composite path of a composite part is simply the path of the composite binding that the
/// part belongs to.
/// </remarks>
public string compositePath { get; }
public SerializedProperty wrappedProperty { get; }
private static string GetCompositePath(SerializedProperty serializedProperty)
{
var bindingArrayProperty = serializedProperty.GetArrayPropertyFromElement();
var partBindingIndex = InputActionSerializationHelpers.GetIndex(bindingArrayProperty, serializedProperty);
var compositeStartIndex =
InputActionSerializationHelpers.GetCompositeStartIndex(bindingArrayProperty, partBindingIndex);
var compositeBindingProperty = bindingArrayProperty.GetArrayElementAtIndex(compositeStartIndex);
return compositeBindingProperty.FindPropertyRelative("m_Path").stringValue;
}
public bool Equals(SerializedInputBinding other)
{
return name == other.name
&& path == other.path
&& interactions == other.interactions
&& processors == other.processors
&& action == other.action
&& flags == other.flags
&& indexOfBinding == other.indexOfBinding
&& isComposite == other.isComposite
&& isPartOfComposite == other.isPartOfComposite
&& compositePath == other.compositePath
&& controlSchemes.SequenceEqual(other.controlSchemes)
&& propertyPath == other.propertyPath;
}
public override bool Equals(object obj)
{
return obj is SerializedInputBinding other && Equals(other);
}
public override int GetHashCode()
{
var hashCode = new HashCode();
hashCode.Add(name);
hashCode.Add(path);
hashCode.Add(interactions);
hashCode.Add(processors);
hashCode.Add(action);
hashCode.Add((int)flags);
hashCode.Add(indexOfBinding);
hashCode.Add(isComposite);
hashCode.Add(isPartOfComposite);
hashCode.Add(compositePath);
hashCode.Add(controlSchemes);
hashCode.Add(propertyPath);
return hashCode.ToHashCode();
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2084fa9039e3711438d53087a63525f7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,131 @@
#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
using System;
using System.Linq.Expressions;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine.UIElements;
namespace UnityEngine.InputSystem.Editor
{
// Enum used to dictate if a state change should rebuild the Input Actions editor UI
internal enum UIRebuildMode
{
None,
Rebuild,
}
internal class StateContainer
{
public event Action<InputActionsEditorState, UIRebuildMode> StateChanged;
private VisualElement m_RootVisualElement;
private InputActionsEditorState m_State;
public readonly string assetGUID;
public StateContainer(InputActionsEditorState initialState, string assetGUID)
{
m_State = initialState;
this.assetGUID = assetGUID;
}
public void Dispatch(Command command, UIRebuildMode editorRebuildMode = UIRebuildMode.Rebuild)
{
if (command == null)
throw new ArgumentNullException(nameof(command));
m_State = command(m_State);
// why not just invoke the state changed event immediately you ask? The Dispatch method might have
// been called from inside a UI element event handler and if we raised the event immediately, a view
// might try to redraw itself *during* execution of the event handler.
m_RootVisualElement.schedule.Execute(() =>
{
// catch exceptions here or the UIToolkit scheduled event will keep firing forever.
try
{
StateChanged?.Invoke(m_State, editorRebuildMode);
}
catch (Exception e)
{
Debug.LogException(e);
}
});
}
public void Initialize(VisualElement rootVisualElement)
{
// We need to use a root element for the TrackSerializedObjectValue that is destroyed with the view.
// Using a root element from the settings window would not enable the tracking callback to be destroyed or garbage collected.
m_RootVisualElement = rootVisualElement;
m_RootVisualElement.Unbind();
m_RootVisualElement.TrackSerializedObjectValue(m_State.serializedObject, so =>
{
StateChanged?.Invoke(m_State, UIRebuildMode.Rebuild);
});
StateChanged?.Invoke(m_State, UIRebuildMode.Rebuild);
rootVisualElement.Bind(m_State.serializedObject);
}
/// <summary>
/// Return a copy of the state.
/// </summary>
/// <remarks>
/// It can sometimes be necessary to get access to the state outside of a state change event, like for example
/// when creating views in response to UI click events. This method is for those times.
/// </remarks>
/// <returns></returns>
public InputActionsEditorState GetState()
{
return m_State;
}
public void Bind<TValue>(Expression<Func<InputActionsEditorState, ReactiveProperty<TValue>>> expr,
Action<InputActionsEditorState> propertyChangedCallback)
{
WhenChanged(expr, propertyChangedCallback);
propertyChangedCallback(m_State);
}
public void Bind(Expression<Func<InputActionsEditorState, SerializedProperty>> expr,
Action<SerializedProperty> serializedPropertyChangedCallback)
{
var propertyGetterFunc = WhenChanged(expr, serializedPropertyChangedCallback);
serializedPropertyChangedCallback(propertyGetterFunc(m_State));
}
public Func<InputActionsEditorState, ReactiveProperty<TValue>> WhenChanged<TValue>(Expression<Func<InputActionsEditorState, ReactiveProperty<TValue>>> expr,
Action<InputActionsEditorState> propertyChangedCallback)
{
var func = ExpressionUtils.CreateGetter(expr);
if (func == null)
throw new ArgumentException($"Couldn't get property info from expression.");
var prop = func(m_State);
if (prop == null)
throw new InvalidOperationException($"ReactiveProperty {expr} has not been assigned.");
prop.Changed += _ => propertyChangedCallback(m_State);
return func;
}
public Func<InputActionsEditorState, SerializedProperty> WhenChanged(Expression<Func<InputActionsEditorState, SerializedProperty>> expr,
Action<SerializedProperty> serializedPropertyChangedCallback)
{
var serializedPropertyGetter = ExpressionUtils.CreateGetter(expr);
if (serializedPropertyGetter == null)
throw new ArgumentException($"Couldn't get property info from expression.");
var serializedProperty = serializedPropertyGetter(m_State);
if (serializedProperty == null)
throw new InvalidOperationException($"ReactiveProperty {expr} has not been assigned.");
m_RootVisualElement.TrackPropertyValue(serializedProperty, serializedPropertyChangedCallback);
return serializedPropertyGetter;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8b19af954eaf68841a157dc58e7e4814
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2db76e4ad9537964994bd20263e1c379
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,273 @@
#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
using CmdEvents = UnityEngine.InputSystem.Editor.InputActionsEditorConstants.CommandEvents;
using System.Collections.Generic;
using System.Linq;
using UnityEngine.UIElements;
namespace UnityEngine.InputSystem.Editor
{
/// <summary>
/// A list view to display the action maps of the currently opened input actions asset.
/// </summary>
internal class ActionMapsView : ViewBase<ActionMapsView.ViewState>
{
public ActionMapsView(VisualElement root, StateContainer stateContainer)
: base(root, stateContainer)
{
m_ListView = root.Q<ListView>("action-maps-list-view");
m_ListView.selectionType = UIElements.SelectionType.Single;
m_ListView.reorderable = true;
m_ListViewSelectionChangeFilter = new CollectionViewSelectionChangeFilter(m_ListView);
m_ListViewSelectionChangeFilter.selectedIndicesChanged += (selectedIndices) =>
{
Dispatch(Commands.SelectActionMap(((ActionMapData)m_ListView.selectedItem).mapName));
};
m_ListView.bindItem = (element, i) =>
{
var treeViewItem = (InputActionMapsTreeViewItem)element;
var mapData = (ActionMapData)m_ListView.itemsSource[i];
treeViewItem.label.text = mapData.mapName;
treeViewItem.EditTextFinishedCallback = newName => ChangeActionMapName(i, newName);
treeViewItem.EditTextFinished += treeViewItem.EditTextFinishedCallback;
treeViewItem.userData = i;
element.SetEnabled(!mapData.isDisabled);
treeViewItem.isDisabledActionMap = mapData.isDisabled;
ContextMenu.GetContextMenuForActionMapItem(this, treeViewItem, i);
};
m_ListView.makeItem = () => new InputActionMapsTreeViewItem();
m_ListView.unbindItem = (element, i) =>
{
var treeViewElement = (InputActionMapsTreeViewItem)element;
treeViewElement.Reset();
treeViewElement.EditTextFinished -= treeViewElement.EditTextFinishedCallback;
};
m_ListView.itemsChosen += objects =>
{
var item = m_ListView.GetRootElementForIndex(m_ListView.selectedIndex).Q<InputActionMapsTreeViewItem>();
item.FocusOnRenameTextField();
};
m_ListView.RegisterCallback<ExecuteCommandEvent>(OnExecuteCommand);
m_ListView.RegisterCallback<ValidateCommandEvent>(OnValidateCommand);
m_ListView.RegisterCallback<PointerDownEvent>(OnPointerDown, TrickleDown.TrickleDown);
// ISXB-748 - Scrolling the view causes a visual glitch with the rename TextField. As a work-around we
// need to cancel the rename operation in this scenario.
m_ListView.RegisterCallback<WheelEvent>(e => InputActionMapsTreeViewItem.CancelRename(), TrickleDown.TrickleDown);
var treeView = root.Q<TreeView>("actions-tree-view");
m_ListView.AddManipulator(new DropManipulator(OnDroppedHandler, treeView));
m_ListView.itemIndexChanged += OnReorder;
CreateSelector(Selectors.GetActionMapNames, Selectors.GetSelectedActionMap, (actionMapNames, actionMap, state) => new ViewState(actionMap, actionMapNames, state.GetDisabledActionMaps(actionMapNames.ToList())));
m_AddActionMapButton = root.Q<Button>("add-new-action-map-button");
m_AddActionMapButton.clicked += AddActionMap;
ContextMenu.GetContextMenuForActionMapsEmptySpace(this, root.Q<VisualElement>("rclick-area-to-add-new-action-map"));
}
void OnDroppedHandler(int mapIndex)
{
Dispatch(Commands.PasteActionIntoActionMap(mapIndex));
}
void OnReorder(int oldIndex, int newIndex)
{
Dispatch(Commands.ReorderActionMap(oldIndex, newIndex));
}
public override void RedrawUI(ViewState viewState)
{
m_ListView.itemsSource = viewState.actionMapData?.ToList() ?? new List<ActionMapData>();
if (viewState.selectedActionMap.HasValue)
{
var actionMapData = viewState.actionMapData?.Find(map => map.mapName.Equals(viewState.selectedActionMap.Value.name));
if (actionMapData.HasValue)
m_ListView.SetSelection(viewState.actionMapData.IndexOf(actionMapData.Value));
}
// UI toolkit doesn't behave the same on 6000.0 way when refreshing items
// On previous versions, we need to call Rebuild() to refresh the items since refreshItems() is less predicatable
#if UNITY_6000_0_OR_NEWER
m_ListView.RefreshItems();
#else
m_ListView.Rebuild();
#endif
RenameNewActionMaps();
}
public override void DestroyView()
{
m_AddActionMapButton.clicked -= AddActionMap;
}
private void RenameNewActionMaps()
{
if (!m_EnterRenamingMode)
return;
m_ListView.ScrollToItem(m_ListView.selectedIndex);
var element = m_ListView.GetRootElementForIndex(m_ListView.selectedIndex);
if (element == null)
return;
((InputActionMapsTreeViewItem)element).FocusOnRenameTextField();
}
internal void RenameActionMap(int index)
{
m_ListView.ScrollToItem(index);
var element = m_ListView.GetRootElementForIndex(index);
if (element == null)
return;
((InputActionMapsTreeViewItem)element).FocusOnRenameTextField();
}
internal void DeleteActionMap(int index)
{
Dispatch(Commands.DeleteActionMap(index));
}
internal void DuplicateActionMap(int index)
{
Dispatch(Commands.DuplicateActionMap(index));
}
internal void CopyItems()
{
Dispatch(Commands.CopyActionMapSelection());
}
internal void CutItems()
{
Dispatch(Commands.CutActionMapSelection());
}
internal void PasteItems(bool copiedAction)
{
Dispatch(copiedAction ? Commands.PasteActionFromActionMap(InputActionsEditorView.s_OnPasteCutElements) : Commands.PasteActionMaps(InputActionsEditorView.s_OnPasteCutElements));
}
private void ChangeActionMapName(int index, string newName)
{
m_EnterRenamingMode = false;
Dispatch(Commands.ChangeActionMapName(index, newName));
}
internal void AddActionMap()
{
Dispatch(Commands.AddActionMap());
m_EnterRenamingMode = true;
}
internal int GetMapCount()
{
return m_ListView.itemsSource.Count;
}
private void OnExecuteCommand(ExecuteCommandEvent evt)
{
var selectedItem = m_ListView.GetRootElementForIndex(m_ListView.selectedIndex);
if (selectedItem == null)
return;
if (allowUICommandExecution)
{
switch (evt.commandName)
{
case CmdEvents.Rename:
((InputActionMapsTreeViewItem)selectedItem).FocusOnRenameTextField();
break;
case CmdEvents.Delete:
case CmdEvents.SoftDelete:
DeleteActionMap(m_ListView.selectedIndex);
break;
case CmdEvents.Duplicate:
DuplicateActionMap(m_ListView.selectedIndex);
break;
case CmdEvents.Copy:
CopyItems();
break;
case CmdEvents.Cut:
CutItems();
break;
case CmdEvents.Paste:
var isActionCopied = CopyPasteHelper.GetCopiedClipboardType() == typeof(InputAction);
if (CopyPasteHelper.HasPastableClipboardData(typeof(InputActionMap)))
PasteItems(isActionCopied);
break;
default:
return; // Skip StopPropagation if we didn't execute anything
}
// Prevent any UI commands from executing until after UI has been updated
allowUICommandExecution = false;
}
evt.StopPropagation();
}
private void OnValidateCommand(ValidateCommandEvent evt)
{
// Mark commands as supported for Execute by stopping propagation of the event
switch (evt.commandName)
{
case CmdEvents.Rename:
case CmdEvents.Delete:
case CmdEvents.SoftDelete:
case CmdEvents.Duplicate:
case CmdEvents.Copy:
case CmdEvents.Cut:
case CmdEvents.Paste:
evt.StopPropagation();
break;
}
}
private void OnPointerDown(PointerDownEvent evt)
{
// Allow right clicks to select an item before we bring up the matching context menu.
if (evt.button == (int)MouseButton.RightMouse && evt.clickCount == 1)
{
var actionMap = (evt.target as VisualElement).GetFirstAncestorOfType<InputActionMapsTreeViewItem>();
if (actionMap != null)
m_ListView.SetSelection(actionMap.parent.IndexOf(actionMap));
}
}
private readonly CollectionViewSelectionChangeFilter m_ListViewSelectionChangeFilter;
private bool m_EnterRenamingMode;
private readonly ListView m_ListView;
private readonly Button m_AddActionMapButton;
internal struct ActionMapData
{
internal string mapName;
internal bool isDisabled;
public ActionMapData(string mapName, bool isDisabled)
{
this.mapName = mapName;
this.isDisabled = isDisabled;
}
}
internal class ViewState
{
public SerializedInputActionMap? selectedActionMap;
public List<ActionMapData> actionMapData;
public ViewState(SerializedInputActionMap? selectedActionMap, IEnumerable<string> actionMapNames, IEnumerable<string> disabledActionMapNames)
{
this.selectedActionMap = selectedActionMap;
actionMapData = new List<ActionMapData>();
foreach (var name in actionMapNames)
{
actionMapData.Add(new ActionMapData(name, disabledActionMapNames.Contains(name)));
}
}
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0226840818c8be84a9d8774a2529fe86
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,109 @@
#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine.UIElements;
namespace UnityEngine.InputSystem.Editor
{
internal class ActionPropertiesView : ViewBase<(SerializedInputAction?, List<string>)>
{
private readonly Foldout m_ParentFoldout;
private readonly int m_DropdownLabelWidth = 90;
public ActionPropertiesView(VisualElement root, Foldout foldout, StateContainer stateContainer)
: base(root, stateContainer)
{
m_ParentFoldout = foldout;
// TODO: Consider IEquatable<T> and how to compare selector data
CreateSelector(Selectors.GetSelectedAction,
(inputAction, _) =>
{
if (!inputAction.HasValue)
return (null, new List<string>());
return (inputAction.Value, Selectors.BuildControlTypeList(inputAction.Value.type).ToList());
});
}
public override void RedrawUI((SerializedInputAction ? , List<string>) viewState)
{
if (!viewState.Item1.HasValue)
return;
m_ParentFoldout.text = "Action";
var inputAction = viewState.Item1.Value;
rootElement.Clear();
var actionType = new EnumField("Action Type", inputAction.type)
{
tooltip = inputAction.actionTypeTooltip
};
// Tighten up the gap between the label and dropdown so the latter is more readable when the parent pane is at min width.
var actionLabel = actionType.Q<Label>();
actionLabel.style.minWidth = m_DropdownLabelWidth;
actionLabel.style.width = m_DropdownLabelWidth;
actionType.RegisterValueChangedCallback(evt =>
{
Dispatch(Commands.ChangeActionType(inputAction, (InputActionType)evt.newValue));
});
rootElement.Add(actionType);
if (inputAction.type != InputActionType.Button)
{
var controlTypes = viewState.Item2;
var controlType = new DropdownField("Control Type");
// Tighten up the gap between the label and dropdown so the latter is more readable when the parent pane is at min width.
var controlLabel = controlType.Q<Label>();
controlLabel.style.minWidth = m_DropdownLabelWidth;
controlLabel.style.width = m_DropdownLabelWidth;
controlType.choices.Clear();
controlType.choices.AddRange(controlTypes.Select(ObjectNames.NicifyVariableName).ToList());
var controlTypeIndex = controlTypes.FindIndex(s => s == inputAction.expectedControlType);
//if type changed and index is -1 clamp to 0, prevent overflowing indices
controlTypeIndex = Math.Clamp(controlTypeIndex, 0, controlTypes.Count - 1);
controlType.SetValueWithoutNotify(controlType.choices[controlTypeIndex]);
controlType.tooltip = inputAction.expectedControlTypeTooltip;
controlType.RegisterValueChangedCallback(evt =>
{
Dispatch(Commands.ChangeActionControlType(inputAction, controlType.index));
});
// ISX-1916 - When changing ActionType to a non-Button type, we must also update the ControlType
// to the currently selected value; the ValueChangedCallback is not fired in this scenario.
Dispatch(Commands.ChangeActionControlType(inputAction, controlType.index));
rootElement.Add(controlType);
}
else
{
// ISX-1916 - When changing ActionType to a Button, we must also reset the ControlType
Dispatch(Commands.ChangeActionControlType(inputAction, 0));
}
if (inputAction.type != InputActionType.Value)
{
var initialStateCheck = new Toggle("Initial State Check")
{
tooltip = InputActionsEditorConstants.InitialStateCheckTooltip
};
initialStateCheck.SetValueWithoutNotify(inputAction.initialStateCheck);
initialStateCheck.RegisterValueChangedCallback(evt =>
{
Dispatch(Commands.ChangeInitialStateCheck(inputAction, evt.newValue));
});
rootElement.Add(initialStateCheck);
}
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 954f8631d67512b409a5c4c526ae4f71
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,734 @@
// UITK TreeView is not supported in earlier versions
// Therefore the UITK version of the InputActionAsset Editor is not available on earlier Editor versions either.
#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
using CmdEvents = UnityEngine.InputSystem.Editor.InputActionsEditorConstants.CommandEvents;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.Utilities;
using UnityEngine.UIElements;
namespace UnityEngine.InputSystem.Editor
{
/// <summary>
/// A view for displaying the actions of the selected action map in a tree with bindings
/// as children.
/// </summary>
internal class ActionsTreeView : ViewBase<ActionsTreeView.ViewState>
{
private readonly ListView m_ActionMapsListView;
private readonly TreeView m_ActionsTreeView;
private readonly Button m_AddActionButton;
private readonly ScrollView m_PropertiesScrollview;
private bool m_RenameOnActionAdded;
private readonly CollectionViewSelectionChangeFilter m_ActionsTreeViewSelectionChangeFilter;
//save TreeView element id's of individual input actions and bindings to ensure saving of expanded state
private Dictionary<Guid, int> m_GuidToTreeViewId;
public ActionsTreeView(VisualElement root, StateContainer stateContainer)
: base(root, stateContainer)
{
m_ActionMapsListView = root.Q<ListView>("action-maps-list-view");
m_AddActionButton = root.Q<Button>("add-new-action-button");
m_PropertiesScrollview = root.Q<ScrollView>("properties-scrollview");
m_ActionsTreeView = root.Q<TreeView>("actions-tree-view");
//assign unique viewDataKey to store treeView states like expanded/collapsed items - make it unique to avoid conflicts with other TreeViews
m_ActionsTreeView.viewDataKey = $"InputActionTreeView_{stateContainer.assetGUID}";
m_GuidToTreeViewId = new Dictionary<Guid, int>();
m_ActionsTreeView.selectionType = UIElements.SelectionType.Single;
m_ActionsTreeView.makeItem = () => new InputActionsTreeViewItem();
m_ActionsTreeView.reorderable = true;
m_ActionsTreeView.bindItem = (e, i) =>
{
var item = m_ActionsTreeView.GetItemDataForIndex<ActionOrBindingData>(i);
e.Q<Label>("name").text = item.name;
var addBindingButton = e.Q<Button>("add-new-binding-button");
addBindingButton.AddToClassList(EditorGUIUtility.isProSkin ? "add-binging-button-dark-theme" : "add-binging-button");
var treeViewItem = (InputActionsTreeViewItem)e;
if (item.isComposite)
ContextMenu.GetContextMenuForCompositeItem(this, treeViewItem, i);
else if (item.isAction)
ContextMenu.GetContextMenuForActionItem(this, treeViewItem, item.controlLayout, i);
else
ContextMenu.GetContextMenuForBindingItem(this, treeViewItem, i);
if (item.isAction)
{
Action action = ContextMenu.GetContextMenuForActionAddItem(this, item.controlLayout, i);
addBindingButton.clicked += action;
addBindingButton.userData = action; // Store to use in unbindItem
addBindingButton.clickable.activators.Add(new ManipulatorActivationFilter(){button = MouseButton.RightMouse});
addBindingButton.style.display = DisplayStyle.Flex;
treeViewItem.EditTextFinishedCallback = newName =>
{
ChangeActionOrCompositName(item, newName);
};
treeViewItem.EditTextFinished += treeViewItem.EditTextFinishedCallback;
}
else
{
addBindingButton.style.display = DisplayStyle.None;
if (!item.isComposite)
treeViewItem.UnregisterInputField();
else
{
treeViewItem.EditTextFinishedCallback = newName =>
{
ChangeActionOrCompositName(item, newName);
};
treeViewItem.EditTextFinished += treeViewItem.EditTextFinishedCallback;
}
}
if (!string.IsNullOrEmpty(item.controlLayout))
e.Q<VisualElement>("icon").style.backgroundImage =
new StyleBackground(
EditorInputControlLayoutCache.GetIconForLayout(item.controlLayout));
else
e.Q<VisualElement>("icon").style.backgroundImage =
new StyleBackground(
EditorInputControlLayoutCache.GetIconForLayout("Control"));
e.SetEnabled(!item.isCut);
treeViewItem.isCut = item.isCut;
};
m_ActionsTreeView.itemsChosen += objects =>
{
var data = (ActionOrBindingData)objects.First();
if (!data.isAction && !data.isComposite)
return;
var item = m_ActionsTreeView.GetRootElementForIndex(m_ActionsTreeView.selectedIndex).Q<InputActionsTreeViewItem>();
item.FocusOnRenameTextField();
};
m_ActionsTreeView.unbindItem = (element, i) =>
{
var item = m_ActionsTreeView.GetItemDataForIndex<ActionOrBindingData>(i);
var treeViewItem = (InputActionsTreeViewItem)element;
//reset the editing variable before reassigning visual elements
if (item.isAction || item.isComposite)
treeViewItem.Reset();
if (item.isAction)
{
var button = element.Q<Button>("add-new-binding-button");
button.clicked -= button.userData as Action;
}
treeViewItem.EditTextFinished -= treeViewItem.EditTextFinishedCallback;
};
ContextMenu.GetContextMenuForActionListView(this, m_ActionsTreeView, m_ActionsTreeView.parent);
ContextMenu.GetContextMenuForActionsEmptySpace(this, m_ActionsTreeView, root.Q<VisualElement>("rclick-area-to-add-new-action"));
m_ActionsTreeViewSelectionChangeFilter = new CollectionViewSelectionChangeFilter(m_ActionsTreeView);
m_ActionsTreeViewSelectionChangeFilter.selectedIndicesChanged += (_) =>
{
if (m_ActionsTreeView.selectedIndex >= 0)
{
var item = m_ActionsTreeView.GetItemDataForIndex<ActionOrBindingData>(m_ActionsTreeView.selectedIndex);
Dispatch(item.isAction ? Commands.SelectAction(item.name) : Commands.SelectBinding(item.bindingIndex));
}
};
m_ActionsTreeView.RegisterCallback<ExecuteCommandEvent>(OnExecuteCommand);
m_ActionsTreeView.RegisterCallback<ValidateCommandEvent>(OnValidateCommand);
m_ActionsTreeView.RegisterCallback<PointerDownEvent>(OnPointerDown, TrickleDown.TrickleDown);
m_ActionsTreeView.RegisterCallback<DragPerformEvent>(OnDraggedItem);
// ISXB-748 - Scrolling the view causes a visual glitch with the rename TextField. As a work-around we
// need to cancel the rename operation in this scenario.
m_ActionsTreeView.RegisterCallback<WheelEvent>(e => InputActionsTreeViewItem.CancelRename(), TrickleDown.TrickleDown);
CreateSelector(Selectors.GetActionsForSelectedActionMap, Selectors.GetActionMapCount,
(_, count, state) =>
{
var treeData = Selectors.GetActionsAsTreeViewData(state, m_GuidToTreeViewId);
return new ViewState
{
treeViewData = treeData,
actionMapCount = count ?? 0,
newElementID = GetSelectedElementId(state, treeData)
};
});
m_AddActionButton.clicked += AddAction;
}
private int GetSelectedElementId(InputActionsEditorState state, List<TreeViewItemData<ActionOrBindingData>> treeData)
{
var id = -1;
if (state.selectionType == SelectionType.Action)
{
if (treeData.Count > state.selectedActionIndex && state.selectedActionIndex >= 0)
id = treeData[state.selectedActionIndex].id;
}
else if (state.selectionType == SelectionType.Binding)
id = GetComponentOrBindingID(treeData, state.selectedBindingIndex);
return id;
}
private int GetComponentOrBindingID(List<TreeViewItemData<ActionOrBindingData>> treeItemList, int selectedBindingIndex)
{
foreach (var actionItem in treeItemList)
{
// Look for the element ID by checking if the selected binding index matches the binding index of
// the ActionOrBindingData of the item. Deals with composite bindings as well.
foreach (var bindingOrComponentItem in actionItem.children)
{
if (bindingOrComponentItem.data.bindingIndex == selectedBindingIndex)
return bindingOrComponentItem.id;
if (bindingOrComponentItem.hasChildren)
{
foreach (var bindingItem in bindingOrComponentItem.children)
{
if (bindingOrComponentItem.data.bindingIndex == selectedBindingIndex)
return bindingItem.id;
}
}
}
}
return -1;
}
public override void DestroyView()
{
m_AddActionButton.clicked -= AddAction;
}
public override void RedrawUI(ViewState viewState)
{
m_ActionsTreeView.Clear();
m_ActionsTreeView.SetRootItems(viewState.treeViewData);
m_ActionsTreeView.Rebuild();
if (viewState.newElementID != -1)
{
m_ActionsTreeView.SetSelectionById(viewState.newElementID);
m_ActionsTreeView.ScrollToItemById(viewState.newElementID);
}
RenameNewAction(viewState.newElementID);;
m_AddActionButton.SetEnabled(viewState.actionMapCount > 0);
// Don't want to show action properties if there's no actions.
m_PropertiesScrollview.visible = m_ActionsTreeView.GetTreeCount() > 0;
}
private void OnDraggedItem(DragPerformEvent evt)
{
bool discardDrag = false;
foreach (var index in m_ActionsTreeView.selectedIndices)
{
// 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)
var treeView = m_ActionsTreeView.panel.Pick(evt.mousePosition)?.GetFirstAncestorOfType<TreeView>();
if (treeView is null || treeView != m_ActionsTreeView)
{
discardDrag = true;
break;
}
var draggedItemData = m_ActionsTreeView.GetItemDataForIndex<ActionOrBindingData>(index);
var itemID = m_ActionsTreeView.GetIdForIndex(index);
var childIndex = m_ActionsTreeView.viewController.GetChildIndexForId(itemID);
var parentId = m_ActionsTreeView.viewController.GetParentId(itemID);
ActionOrBindingData? directParent = parentId == -1 ? null : m_ActionsTreeView.GetItemDataForIndex<ActionOrBindingData>(m_ActionsTreeView.viewController.GetIndexForId(parentId));
if (draggedItemData.isAction)
{
if (!MoveAction(directParent, draggedItemData, childIndex))
{
discardDrag = true;
break;
}
}
else if (!draggedItemData.isPartOfComposite)
{
if (!MoveBindingOrComposite(directParent, draggedItemData, childIndex))
{
discardDrag = true;
break;
}
}
else if (!MoveCompositeParts(directParent, childIndex, draggedItemData))
{
discardDrag = true;
break;
}
}
if (!discardDrag) return;
var selectedItem = m_ActionsTreeView.GetItemDataForIndex<ActionOrBindingData>(m_ActionsTreeView.selectedIndices.First());
Dispatch(selectedItem.isAction
? Commands.SelectAction(selectedItem.name)
: Commands.SelectBinding(selectedItem.bindingIndex));
//TODO find a better way to reject the drag (for better visual feedback & to not run an extra command)
}
private bool MoveAction(ActionOrBindingData? directParent, ActionOrBindingData draggedItemData, int childIndex)
{
if (directParent != null)
return false;
Dispatch(Commands.MoveAction(draggedItemData.actionIndex, childIndex));
return true;
}
private bool MoveBindingOrComposite(ActionOrBindingData? directParent, ActionOrBindingData draggedItemData, int childIndex)
{
if (directParent == null || !directParent.Value.isAction)
return false;
if (draggedItemData.isComposite)
Dispatch(Commands.MoveComposite(draggedItemData.bindingIndex, directParent.Value.actionIndex, childIndex));
else
Dispatch(Commands.MoveBinding(draggedItemData.bindingIndex, directParent.Value.actionIndex, childIndex));
return true;
}
private bool MoveCompositeParts(ActionOrBindingData? directParent, int childIndex, ActionOrBindingData draggedItemData)
{
if (directParent == null || !directParent.Value.isComposite)
return false;
var newBindingIndex = directParent.Value.bindingIndex + childIndex + (directParent.Value.bindingIndex > draggedItemData.bindingIndex ? 0 : 1);
Dispatch(Commands.MovePartOfComposite(draggedItemData.bindingIndex, newBindingIndex, directParent.Value.bindingIndex));
return true;
}
private void RenameNewAction(int id)
{
if (!m_RenameOnActionAdded || id == -1)
return;
m_ActionsTreeView.ScrollToItemById(id);
var treeViewItem = m_ActionsTreeView.GetRootElementForId(id)?.Q<InputActionsTreeViewItem>();
treeViewItem?.FocusOnRenameTextField();
}
internal void RenameActionItem(int index)
{
m_ActionsTreeView.ScrollToItem(index);
m_ActionsTreeView.GetRootElementForIndex(index)?.Q<InputActionsTreeViewItem>()?.FocusOnRenameTextField();
}
internal void AddAction()
{
Dispatch(Commands.AddAction());
m_RenameOnActionAdded = true;
}
internal void AddBinding(int index)
{
Dispatch(Commands.SelectAction(m_ActionsTreeView.GetItemDataForIndex<ActionOrBindingData>(index).actionIndex));
Dispatch(Commands.AddBinding());
}
internal void AddComposite(int index, string compositeType)
{
Dispatch(Commands.SelectAction(m_ActionsTreeView.GetItemDataForIndex<ActionOrBindingData>(index).actionIndex));
Dispatch(Commands.AddComposite(compositeType));
}
internal void DeleteItem(int selectedIndex)
{
var data = m_ActionsTreeView.GetItemDataForIndex<ActionOrBindingData>(selectedIndex);
if (data.isAction)
Dispatch(Commands.DeleteAction(data.actionMapIndex, data.name));
else
Dispatch(Commands.DeleteBinding(data.actionMapIndex, data.bindingIndex));
// Deleting an item sometimes causes the UI Panel to lose focus; make sure we keep it
m_ActionsTreeView.Focus();
}
internal void DuplicateItem(int selectedIndex)
{
var data = m_ActionsTreeView.GetItemDataForIndex<ActionOrBindingData>(selectedIndex);
Dispatch(data.isAction ? Commands.DuplicateAction() : Commands.DuplicateBinding());
}
internal void CopyItems()
{
Dispatch(Commands.CopyActionBindingSelection());
}
internal void CutItems()
{
Dispatch(Commands.CutActionsOrBindings());
}
internal void PasteItems()
{
Dispatch(Commands.PasteActionsOrBindings(InputActionsEditorView.s_OnPasteCutElements));
}
private void ChangeActionOrCompositName(ActionOrBindingData data, string newName)
{
m_RenameOnActionAdded = false;
if (data.isAction)
Dispatch(Commands.ChangeActionName(data.actionMapIndex, data.name, newName));
else if (data.isComposite)
Dispatch(Commands.ChangeCompositeName(data.actionMapIndex, data.bindingIndex, newName));
}
internal int GetMapCount()
{
return m_ActionMapsListView.itemsSource.Count;
}
private void OnExecuteCommand(ExecuteCommandEvent evt)
{
if (m_ActionsTreeView.selectedItem == null)
return;
if (allowUICommandExecution)
{
var data = (ActionOrBindingData)m_ActionsTreeView.selectedItem;
switch (evt.commandName)
{
case CmdEvents.Rename:
if (data.isAction || data.isComposite)
RenameActionItem(m_ActionsTreeView.selectedIndex);
else
return;
break;
case CmdEvents.Delete:
case CmdEvents.SoftDelete:
DeleteItem(m_ActionsTreeView.selectedIndex);
break;
case CmdEvents.Duplicate:
DuplicateItem(m_ActionsTreeView.selectedIndex);
break;
case CmdEvents.Copy:
CopyItems();
break;
case CmdEvents.Cut:
CutItems();
break;
case CmdEvents.Paste:
var hasPastableData = CopyPasteHelper.HasPastableClipboardData(data.isAction ? typeof(InputAction) : typeof(InputBinding));
if (hasPastableData)
PasteItems();
break;
default:
return; // Skip StopPropagation if we didn't execute anything
}
// Prevent any UI commands from executing until after UI has been updated
allowUICommandExecution = false;
}
evt.StopPropagation();
}
private void OnValidateCommand(ValidateCommandEvent evt)
{
// Mark commands as supported for Execute by stopping propagation of the event
switch (evt.commandName)
{
case CmdEvents.Rename:
case CmdEvents.Delete:
case CmdEvents.SoftDelete:
case CmdEvents.Duplicate:
case CmdEvents.Copy:
case CmdEvents.Cut:
case CmdEvents.Paste:
evt.StopPropagation();
break;
}
}
private void OnPointerDown(PointerDownEvent evt)
{
// Allow right clicks to select an item before we bring up the matching context menu.
if (evt.button == (int)MouseButton.RightMouse && evt.clickCount == 1)
{
// Look upwards to the immediate child of the scroll view, so we know what Index to use
var element = evt.target as VisualElement;
while (element != null && element.name != "unity-tree-view__item")
element = element.parent;
if (element == null)
return;
m_ActionsTreeView.SetSelection(element.parent.IndexOf(element));
}
}
private string GetPreviousActionNameFromViewTree(in ActionOrBindingData data)
{
Debug.Assert(data.isAction);
// If TreeView currently (before delete) has more than one Action, select the one immediately
// above or immediately below depending if data is first in the list
var treeView = ViewStateSelector.GetViewState(stateContainer.GetState()).treeViewData;
if (treeView.Count > 1)
{
string actionName = data.name;
int index = treeView.FindIndex(item => item.data.name == actionName);
if (index > 0)
index--;
else
index++; // Also handles case if actionName wasn't found; FindIndex() returns -1 that's incremented to 0
return treeView[index].data.name;
}
return string.Empty;
}
private int GetPreviousBindingIndexFromViewTree(in ActionOrBindingData data, out string parentActionName)
{
Debug.Assert(!data.isAction);
int retVal = -1;
parentActionName = string.Empty;
// The bindindIndex is global and doesn't correspond to the binding's "child index" within the TreeView.
// To find the "previous" Binding to select, after deleting the current one, we must:
// 1. Traverse the ViewTree to find the parent of the binding and its index under that parent
// 2. Identify the Binding to select after deletion and retrieve its bindingIndex
// 3. Return the bindingIndex and the parent Action name (select the Action if bindingIndex is invalid)
var treeView = ViewStateSelector.GetViewState(stateContainer.GetState()).treeViewData;
foreach (var action in treeView)
{
if (!action.hasChildren)
continue;
if (FindBindingOrComponentTreeViewParent(action, data.bindingIndex, out var parentNode, out int childIndex))
{
parentActionName = action.data.name;
if (parentNode.children.Count() > 1)
{
int prevIndex = Math.Max(childIndex - 1, 0);
var node = parentNode.children.ElementAt(prevIndex);
retVal = node.data.bindingIndex;
break;
}
}
}
return retVal;
}
private static bool FindBindingOrComponentTreeViewParent(TreeViewItemData<ActionOrBindingData> root, int bindingIndex, out TreeViewItemData<ActionOrBindingData> parent, out int childIndex)
{
Debug.Assert(root.hasChildren);
int index = 0;
foreach (var item in root.children)
{
if (item.data.bindingIndex == bindingIndex)
{
parent = root;
childIndex = index;
return true;
}
if (item.hasChildren && FindBindingOrComponentTreeViewParent(item, bindingIndex, out parent, out childIndex))
return true;
index++;
}
parent = default;
childIndex = -1;
return false;
}
internal class ViewState
{
public List<TreeViewItemData<ActionOrBindingData>> treeViewData;
public int actionMapCount;
public int newElementID;
}
}
internal struct ActionOrBindingData
{
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)
{
this.name = name;
this.isComposite = isComposite;
this.isPartOfComposite = isPartOfComposite;
this.actionMapIndex = actionMapIndex;
this.controlLayout = controlLayout;
this.bindingIndex = bindingIndex;
this.isAction = isAction;
this.actionIndex = actionIndex;
this.isCut = isCut;
}
public string name { get; }
public bool isAction { get; }
public int actionMapIndex { get; }
public bool isComposite { get; }
public bool isPartOfComposite { get; }
public string controlLayout { get; }
public int bindingIndex { get; }
public int actionIndex { get; }
public bool isCut { get; }
}
internal static partial class Selectors
{
public static List<TreeViewItemData<ActionOrBindingData>> GetActionsAsTreeViewData(InputActionsEditorState state, Dictionary<Guid, int> idDictionary)
{
var actionMapIndex = state.selectedActionMapIndex;
var controlSchemes = state.serializedObject.FindProperty(nameof(InputActionAsset.m_ControlSchemes));
var actionMap = GetSelectedActionMap(state);
if (actionMap == null)
return new List<TreeViewItemData<ActionOrBindingData>>();
var actions = actionMap.Value.wrappedProperty
.FindPropertyRelative(nameof(InputActionMap.m_Actions))
.Select(sp => new SerializedInputAction(sp));
var bindings = actionMap.Value.wrappedProperty
.FindPropertyRelative(nameof(InputActionMap.m_Bindings))
.Select(sp => new SerializedInputBinding(sp))
.ToList();
var actionItems = new List<TreeViewItemData<ActionOrBindingData>>();
foreach (var action in actions)
{
var actionBindings = bindings.Where(spb => spb.action == action.name).ToList();
var bindingItems = new List<TreeViewItemData<ActionOrBindingData>>();
var actionId = new Guid(action.id);
for (var i = 0; i < actionBindings.Count; i++)
{
var serializedInputBinding = actionBindings[i];
var inputBindingId = new Guid(serializedInputBinding.id);
if (serializedInputBinding.isComposite)
{
var isLastBinding = i >= actionBindings.Count - 1;
var hasHiddenCompositeParts = false;
var compositeItems = new List<TreeViewItemData<ActionOrBindingData>>();
if (!isLastBinding)
{
var nextBinding = actionBindings[++i];
while (nextBinding.isPartOfComposite)
{
var isVisible = ShouldBindingBeVisible(nextBinding, state.selectedControlScheme, state.selectedDeviceRequirementIndex);
if (isVisible)
{
var name = GetHumanReadableCompositeName(nextBinding, state.selectedControlScheme, controlSchemes);
compositeItems.Add(new TreeViewItemData<ActionOrBindingData>(GetIdForGuid(new Guid(nextBinding.id), idDictionary),
new ActionOrBindingData(isAction: false, name, actionMapIndex, isComposite: false,
isPartOfComposite: true, GetControlLayout(nextBinding.path), bindingIndex: nextBinding.indexOfBinding, isCut: state.IsBindingCut(actionMapIndex, nextBinding.indexOfBinding))));
}
else
hasHiddenCompositeParts = true;
if (++i >= actionBindings.Count)
break;
nextBinding = actionBindings[i];
}
i--;
}
var shouldCompositeBeVisible = !(compositeItems.Count == 0 && hasHiddenCompositeParts); //hide composite if all parts are hidden
if (shouldCompositeBeVisible)
bindingItems.Add(new TreeViewItemData<ActionOrBindingData>(GetIdForGuid(inputBindingId, idDictionary),
new ActionOrBindingData(isAction: false, serializedInputBinding.name, actionMapIndex, isComposite: true, isPartOfComposite: false, action.expectedControlType, bindingIndex: serializedInputBinding.indexOfBinding, isCut: state.IsBindingCut(actionMapIndex, serializedInputBinding.indexOfBinding)),
compositeItems.Count > 0 ? compositeItems : null));
}
else
{
var isVisible = ShouldBindingBeVisible(serializedInputBinding, state.selectedControlScheme, state.selectedDeviceRequirementIndex);
if (isVisible)
bindingItems.Add(new TreeViewItemData<ActionOrBindingData>(GetIdForGuid(inputBindingId, idDictionary),
new ActionOrBindingData(isAction: false, GetHumanReadableBindingName(serializedInputBinding, state.selectedControlScheme, controlSchemes), actionMapIndex,
isComposite: false, isPartOfComposite: false, GetControlLayout(serializedInputBinding.path), bindingIndex: serializedInputBinding.indexOfBinding, isCut: state.IsBindingCut(actionMapIndex, serializedInputBinding.indexOfBinding))));
}
}
var actionIndex = action.wrappedProperty.GetIndexOfArrayElement();
actionItems.Add(new TreeViewItemData<ActionOrBindingData>(GetIdForGuid(actionId, idDictionary),
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));
}
return actionItems;
}
private static int GetIdForGuid(Guid guid, Dictionary<Guid, int> idDictionary)
{
// This method is used to ensure that the same Guid always gets the same id
// We use getHashCode instead of a counter, as we cannot guarantee that the same Guid will always be added in the same order
// There is a tiny chance of a collision, but it is it does happen it will only affect the expanded state of the tree view
if (!idDictionary.TryGetValue(guid, out var id))
{
id = guid.GetHashCode();
idDictionary.Add(guid, id);
}
return id;
}
private static string GetHumanReadableBindingName(SerializedInputBinding serializedInputBinding, InputControlScheme? currentControlScheme, SerializedProperty allControlSchemes)
{
var name = InputControlPath.ToHumanReadableString(serializedInputBinding.path);
if (String.IsNullOrEmpty(name))
name = "<No Binding>";
if (IsBindingAssignedToNoControlSchemes(serializedInputBinding, allControlSchemes, currentControlScheme))
name += " {GLOBAL}";
return name;
}
private static bool IsBindingAssignedToNoControlSchemes(SerializedInputBinding serializedInputBinding, SerializedProperty allControlSchemes, InputControlScheme? currentControlScheme)
{
if (allControlSchemes.arraySize <= 0 || !currentControlScheme.HasValue || string.IsNullOrEmpty(currentControlScheme.Value.name))
return false;
if (serializedInputBinding.controlSchemes.Length <= 0)
return true;
return false;
}
private static bool ShouldBindingBeVisible(SerializedInputBinding serializedInputBinding, InputControlScheme? currentControlScheme, int deviceIndex)
{
if (currentControlScheme.HasValue && !string.IsNullOrEmpty(currentControlScheme.Value.name))
{
var isMatchingDevice = true;
if (deviceIndex >= 0 && deviceIndex < currentControlScheme.Value.deviceRequirements.Count)
{
var devicePathToMatch = InputControlPath.TryGetDeviceLayout(currentControlScheme.Value.deviceRequirements.ElementAt(deviceIndex).controlPath);
var devicePath = InputControlPath.TryGetDeviceLayout(serializedInputBinding.path);
isMatchingDevice = string.Equals(devicePathToMatch, devicePath, StringComparison.InvariantCultureIgnoreCase) || InputControlLayout.s_Layouts.IsBasedOn(new InternedString(devicePath), new InternedString(devicePathToMatch));
}
var hasNoControlScheme = serializedInputBinding.controlSchemes.Length <= 0; //also show GLOBAL bindings
var isAssignedToCurrentControlScheme = serializedInputBinding.controlSchemes.Contains(currentControlScheme.Value.name);
return (isAssignedToCurrentControlScheme || hasNoControlScheme) && isMatchingDevice;
}
//if no control scheme selected then show all bindings
return true;
}
internal static string GetHumanReadableCompositeName(SerializedInputBinding binding, InputControlScheme? currentControlScheme, SerializedProperty allControlSchemes)
{
return $"{ObjectNames.NicifyVariableName(binding.name)}: " +
$"{GetHumanReadableBindingName(binding, currentControlScheme, allControlSchemes)}";
}
private static string GetControlLayout(string path)
{
var controlLayout = string.Empty;
try
{
controlLayout = InputControlPath.TryGetControlLayout(path);
}
catch (Exception)
{
}
return controlLayout;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 716898219fc23de4a81300652df80ef3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,185 @@
#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
using System.Linq;
using UnityEditor;
using UnityEngine.UIElements;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.Utilities;
using System.Collections.Generic;
namespace UnityEngine.InputSystem.Editor
{
internal class BindingPropertiesView : ViewBase<BindingPropertiesView.ViewState>
{
private readonly Foldout m_ParentFoldout;
private CompositeBindingPropertiesView m_CompositeBindingPropertiesView;
private CompositePartBindingPropertiesView m_CompositePartBindingPropertiesView;
public BindingPropertiesView(VisualElement root, Foldout foldout, StateContainer stateContainer)
: base(root, stateContainer)
{
m_ParentFoldout = foldout;
CreateSelector(state => state.selectedBindingIndex,
s => new ViewStateCollection<InputControlScheme>(Selectors.GetControlSchemes(s)),
(_, controlSchemes, s) => new ViewState
{
controlSchemes = controlSchemes,
currentControlScheme = s.selectedControlScheme,
selectedBinding = Selectors.GetSelectedBinding(s),
selectedBindingIndex = s.selectedBindingIndex,
selectedBindingPath = Selectors.GetSelectedBindingPath(s),
selectedInputAction = Selectors.GetSelectedAction(s)
});
}
public override void RedrawUI(ViewState viewState)
{
var selectedBindingIndex = viewState.selectedBindingIndex;
if (selectedBindingIndex == -1)
return;
rootElement.Clear();
var binding = viewState.selectedBinding;
if (!binding.HasValue)
return;
m_ParentFoldout.text = "Binding";
if (binding.Value.isComposite)
{
m_ParentFoldout.text = "Composite";
m_CompositeBindingPropertiesView = CreateChildView(new CompositeBindingPropertiesView(rootElement, stateContainer));
}
else if (binding.Value.isPartOfComposite)
{
m_CompositePartBindingPropertiesView = CreateChildView(new CompositePartBindingPropertiesView(rootElement, stateContainer));
DrawMatchingControlPaths(viewState);
DrawControlSchemeToggles(viewState, binding.Value);
}
else
{
var controlPathEditor = new InputControlPathEditor(viewState.selectedBindingPath, new InputControlPickerState(),
() => { Dispatch(Commands.ApplyModifiedProperties()); });
controlPathEditor.SetControlPathsToMatch(viewState.currentControlScheme.deviceRequirements.Select(x => x.controlPath));
var inputAction = viewState.selectedInputAction;
controlPathEditor.SetExpectedControlLayout(inputAction?.expectedControlType ?? "");
var controlPathContainer = new IMGUIContainer(controlPathEditor.OnGUI);
rootElement.Add(controlPathContainer);
DrawMatchingControlPaths(viewState);
DrawControlSchemeToggles(viewState, binding.Value);
}
}
static bool s_showMatchingLayouts = false;
internal void DrawMatchingControlPaths(ViewState viewState)
{
bool controlPathUsagePresent = false;
bool showPaths = s_showMatchingLayouts;
List<MatchingControlPath> matchingControlPaths = MatchingControlPath.CollectMatchingControlPaths(viewState.selectedBindingPath.stringValue, showPaths, ref controlPathUsagePresent);
var parentElement = rootElement;
if (matchingControlPaths == null || matchingControlPaths.Count != 0)
{
var controllingElement = new Foldout()
{
text = $"Show Derived Bindings",
value = showPaths
};
rootElement.Add(controllingElement);
controllingElement.RegisterValueChangedCallback(changeEvent =>
{
if (changeEvent.target == controllingElement) // only react to foldout and not tree elements
s_showMatchingLayouts = changeEvent.newValue;
});
parentElement = controllingElement;
}
if (matchingControlPaths == null)
{
var messageString = controlPathUsagePresent ? "No registered controls match this current binding. Some controls are only registered at runtime." :
"No other registered controls match this current binding. Some controls are only registered at runtime.";
var helpBox = new HelpBox(messageString, HelpBoxMessageType.Warning);
helpBox.AddToClassList("matching-controls");
parentElement.Add(helpBox);
}
else if (matchingControlPaths.Count > 0)
{
List<TreeViewItemData<MatchingControlPath>> treeViewMatchingControlPaths = MatchingControlPath.BuildMatchingControlPathsTreeData(matchingControlPaths);
var treeView = new TreeView();
parentElement.Add(treeView);
treeView.selectionType = UIElements.SelectionType.None;
treeView.AddToClassList("matching-controls");
treeView.fixedItemHeight = 20;
treeView.SetRootItems(treeViewMatchingControlPaths);
// Set TreeView.makeItem to initialize each node in the tree.
treeView.makeItem = () =>
{
var label = new Label();
label.AddToClassList("matching-controls-labels");
return label;
};
// Set TreeView.bindItem to bind an initialized node to a data item.
treeView.bindItem = (VisualElement element, int index) =>
{
var label = (element as Label);
var matchingControlPath = treeView.GetItemDataForIndex<MatchingControlPath>(index);
label.text = $"{matchingControlPath.deviceName} > {matchingControlPath.controlName}";
};
treeView.ExpandRootItems();
}
}
public override void DestroyView()
{
m_CompositeBindingPropertiesView?.DestroyView();
m_CompositePartBindingPropertiesView?.DestroyView();
}
private void DrawControlSchemeToggles(ViewState viewState, SerializedInputBinding binding)
{
if (!viewState.controlSchemes.Any()) return;
var useInControlSchemeLabel = new Label("Use in control scheme")
{
name = "control-scheme-usage-title"
};
rootElement.Add(useInControlSchemeLabel);
foreach (var controlScheme in viewState.controlSchemes)
{
var checkbox = new Toggle(controlScheme.name)
{
value = binding.controlSchemes.Any(scheme => controlScheme.name == scheme)
};
rootElement.Add(checkbox);
checkbox.RegisterValueChangedCallback(changeEvent =>
{
Dispatch(ControlSchemeCommands.ChangeSelectedBindingsControlSchemes(controlScheme.name, changeEvent.newValue));
});
}
}
internal class ViewState
{
public int selectedBindingIndex;
public SerializedInputBinding? selectedBinding;
public ViewStateCollection<InputControlScheme> controlSchemes;
public InputControlScheme currentControlScheme;
public SerializedProperty selectedBindingPath;
public SerializedInputAction? selectedInputAction;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9c9510ff4a99bf84c9277bf2182a67d5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,92 @@
#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine.UIElements;
namespace UnityEngine.InputSystem.Editor
{
/// <summary>
/// A helper class that provides a workaround to prevent deselection in a UI-Toolkit list or tree-view,
/// e.g. when the user is pressing the ESC key while the view has focus.
/// </summary>
/// <remarks>
///The workaround is based on reassigning the selected index based on last selection, constrained
/// to the available range items. The motivation behind this workaround is that there is no built-in support
/// in UI-Toolkit list or tree-view to prevent deselection via ESC key.
///
/// This workaround should be removed if this changes in the future and the functionality is provided by
/// the UI framework.
///
/// Define UNITY_INPUT_SYSTEM_INPUT_ASSET_EDITOR_ALLOWS_DESELECTION to disable this feature if desired
/// during development.
/// </remarks>>
internal class CollectionViewSelectionChangeFilter
{
private readonly BaseVerticalCollectionView m_View;
private List<int> m_SelectedIndices;
/// <summary>
/// Event triggered as an output to filtering the selected indices reported by the view.
/// </summary>
public event Action<IEnumerable<int>> selectedIndicesChanged;
public CollectionViewSelectionChangeFilter(BaseVerticalCollectionView view)
{
m_SelectedIndices = new List<int>();
m_View = view;
#if UNITY_INPUT_SYSTEM_INPUT_ASSET_EDITOR_ALLOWS_DESELECTION
m_View_.selectedIndicesChanged += OnSelectedIndicesChanged;
#else
m_View.selectedIndicesChanged += FilterSelectedIndicesChanged;
#endif
}
#if !UNITY_INPUT_SYSTEM_INPUT_ASSET_EDITOR_ALLOWS_DESELECTION
private void FilterSelectedIndicesChanged(IEnumerable<int> selectedIndices)
{
// Convert IEnumerable to a list to allow for multiple-iteration
var currentlySelectedIndices = selectedIndices.ToList();
// If the selection change is a deselection (transition from having a selection to having none)
if (currentlySelectedIndices.Count == 0)
{
if (m_SelectedIndices.Count > 0)
{
// Remove any stored selection indices that are no longer within valid range
var count = m_View.itemsSource.Count;
for (var i = m_SelectedIndices.Count - 1; i >= 0; --i)
{
if (m_SelectedIndices[i] >= count)
m_SelectedIndices.RemoveAt(i);
}
// Restore selection based on last known selection and return immediately since
// assignment will retrigger selection change.
m_View.SetSelection(m_SelectedIndices);
return;
}
}
else
{
// Store indices to allow preventing future deselect
m_SelectedIndices = currentlySelectedIndices;
}
OnSelectedIndicesChanged(this.m_SelectedIndices);
}
#endif
private void OnSelectedIndicesChanged(IEnumerable<int> selectedIndices)
{
var selectedIndicesChanged = this.selectedIndicesChanged;
selectedIndicesChanged?.Invoke(selectedIndices);
}
}
}
#endif

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 36e7567d9504499a8bbe97595096de5e
timeCreated: 1695966613

View File

@@ -0,0 +1,108 @@
#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine.InputSystem.Editor.Lists;
using UnityEngine.InputSystem.Utilities;
using UnityEngine.UIElements;
namespace UnityEngine.InputSystem.Editor
{
internal class CompositeBindingPropertiesView : ViewBase<CompositeBindingPropertiesView.ViewState>
{
private readonly DropdownField m_CompositeTypeField;
private EventCallback<ChangeEvent<string>> m_CompositeTypeFieldChangedHandler;
private const string UxmlName = InputActionsEditorConstants.PackagePath +
InputActionsEditorConstants.ResourcesPath +
InputActionsEditorConstants.CompositeBindingPropertiesViewUxml;
public CompositeBindingPropertiesView(VisualElement root, StateContainer stateContainer)
: base(root, stateContainer)
{
var visualTreeAsset = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(UxmlName);
var container = visualTreeAsset.CloneTree();
rootElement.Add(container);
m_CompositeTypeField = container.Q<DropdownField>("composite-type-dropdown");
CreateSelector(Selectors.GetSelectedBinding,
(binding, state) => binding == null ? null : Selectors.GetCompositeBindingViewState(state, binding.Value));
}
public override void RedrawUI(ViewState viewState)
{
m_CompositeTypeField.choices.Clear();
m_CompositeTypeField.choices.AddRange(viewState.compositeNames);
m_CompositeTypeField.SetValueWithoutNotify(viewState.selectedCompositeName);
m_CompositeTypeFieldChangedHandler = _ => OnCompositeTypeFieldChanged(viewState);
m_CompositeTypeField.RegisterValueChangedCallback(m_CompositeTypeFieldChangedHandler);
viewState.parameterListView.onChange = () =>
{
Dispatch(Commands.UpdatePathNameAndValues(viewState.parameterListView.GetParameters(), viewState.selectedBindingPath), UIRebuildMode.None);
};
viewState.parameterListView.OnDrawVisualElements(rootElement);
}
public override void DestroyView()
{
m_CompositeTypeField.UnregisterValueChangedCallback(m_CompositeTypeFieldChangedHandler);
}
private void OnCompositeTypeFieldChanged(ViewState viewState)
{
Dispatch(
Commands.SetCompositeBindingType(
viewState.selectedBinding,
viewState.compositeTypes,
viewState.parameterListView,
m_CompositeTypeField.index));
}
internal class ViewState
{
public SerializedInputBinding selectedBinding;
public IEnumerable<string> compositeTypes;
public SerializedProperty selectedBindingPath;
public ParameterListView parameterListView;
public string selectedCompositeName;
public IEnumerable<string> compositeNames;
}
}
internal static partial class Selectors
{
public static CompositeBindingPropertiesView.ViewState GetCompositeBindingViewState(in InputActionsEditorState state,
SerializedInputBinding binding)
{
var inputAction = GetSelectedAction(state);
var compositeNameAndParameters = NameAndParameters.Parse(binding.path);
var compositeName = compositeNameAndParameters.name;
var compositeType = InputBindingComposite.s_Composites.LookupTypeRegistration(compositeName);
var parameterListView = new ParameterListView();
if (compositeType != null)
parameterListView.Initialize(compositeType, compositeNameAndParameters.parameters);
var compositeTypes = GetCompositeTypes(binding.path, inputAction?.expectedControlType).ToList();
var compositeNames = compositeTypes.Select(ObjectNames.NicifyVariableName).ToList();
var selectedCompositeName = compositeNames[compositeTypes.FindIndex(str =>
InputBindingComposite.s_Composites.LookupTypeRegistration(str) == compositeType)];
return new CompositeBindingPropertiesView.ViewState
{
selectedBinding = binding,
selectedBindingPath = GetSelectedBindingPath(state),
compositeTypes = compositeTypes,
compositeNames = compositeNames,
parameterListView = parameterListView,
selectedCompositeName = selectedCompositeName
};
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2bd61b5c763f7444e9a5aa3c7623f60b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,90 @@
#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine.UIElements;
namespace UnityEngine.InputSystem.Editor
{
internal class CompositePartBindingPropertiesView : ViewBase<CompositePartBindingPropertiesView.ViewState>
{
private readonly DropdownField m_CompositePartField;
private readonly IMGUIContainer m_PathEditorContainer;
private const string UxmlName = InputActionsEditorConstants.PackagePath +
InputActionsEditorConstants.ResourcesPath +
InputActionsEditorConstants.CompositePartBindingPropertiesViewUxml;
public CompositePartBindingPropertiesView(VisualElement root, StateContainer stateContainer)
: base(root, stateContainer)
{
var visualTreeAsset = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(UxmlName);
var container = visualTreeAsset.CloneTree();
rootElement.Add(container);
m_PathEditorContainer = container.Q<IMGUIContainer>("path-editor-container");
m_CompositePartField = container.Q<DropdownField>("composite-part-dropdown");
CreateSelector(Selectors.GetSelectedBinding,
(b, s) => b.HasValue && b.Value.isPartOfComposite ? Selectors.GetCompositePartBindingViewState(b.Value, s) : null);
}
public override void RedrawUI(ViewState viewState)
{
if (viewState == null)
return;
// TODO: Persist control picker state
var controlPathEditor = new InputControlPathEditor(viewState.selectedBindingPath, new InputControlPickerState(),
() => { Dispatch(Commands.ApplyModifiedProperties()); });
controlPathEditor.SetControlPathsToMatch(viewState.currentControlScheme.deviceRequirements.Select(x => x.controlPath));
controlPathEditor.SetExpectedControlLayout(viewState.expectedControlLayoutName);
m_PathEditorContainer.onGUIHandler = controlPathEditor.OnGUI;
m_CompositePartField.choices.Clear();
m_CompositePartField.choices.AddRange(viewState.compositePartNames);
m_CompositePartField.SetValueWithoutNotify(viewState.selectedCompositePartName);
m_CompositePartField.RegisterValueChangedCallback(evt =>
{
Dispatch(Commands.SetCompositeBindingPartName(viewState.selectedBinding, evt.newValue));
});
}
internal class ViewState
{
public SerializedProperty selectedBindingPath;
public SerializedInputBinding selectedBinding;
public IEnumerable<string> compositePartNames;
public InputControlScheme currentControlScheme;
public string expectedControlLayoutName;
public string selectedCompositePartName;
}
}
internal static partial class Selectors
{
public static CompositePartBindingPropertiesView.ViewState GetCompositePartBindingViewState(SerializedInputBinding binding,
InputActionsEditorState state)
{
var compositeParts = GetCompositePartOptions(binding.name, binding.compositePath).ToList();
var selectedCompositePartName = ObjectNames.NicifyVariableName(
compositeParts.First(str => string.Equals(str, binding.name, StringComparison.OrdinalIgnoreCase)));
var compositePartBindingViewState = new CompositePartBindingPropertiesView.ViewState
{
selectedBinding = binding,
selectedBindingPath = GetSelectedBindingPath(state),
selectedCompositePartName = selectedCompositePartName,
currentControlScheme = state.selectedControlScheme,
compositePartNames = compositeParts.Select(ObjectNames.NicifyVariableName).ToList(),
expectedControlLayoutName = InputBindingComposite.GetExpectedControlLayoutName(binding.compositePath, binding.name) ?? ""
};
return compositePartBindingViewState;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1d3813199fcb56b45b916ac51d58dc80
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,238 @@
#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
using System;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.Utilities;
using UnityEngine.UIElements;
namespace UnityEngine.InputSystem.Editor
{
internal static class ContextMenu
{
private static readonly string copy_String = "Copy";
private static readonly string cut_String = "Cut";
private static readonly string paste_String = "Paste";
private static readonly string rename_String = "Rename";
private static readonly string duplicate_String = "Duplicate";
private static readonly string delete_String = "Delete";
private static readonly string add_Action_Map_String = "Add Action Map";
private static readonly string add_Action_String = "Add Action";
private static readonly string add_Binding_String = "Add Binding";
#region ActionMaps
// Determine whether current clipboard contents can can pasted into the ActionMaps view
//
// can always paste an ActionMap
// need an existing map to be able to paste an Action
//
private static bool CanPasteIntoActionMaps(ActionMapsView mapView)
{
bool haveMap = mapView.GetMapCount() > 0;
var copiedType = CopyPasteHelper.GetCopiedClipboardType();
bool copyIsMap = copiedType == typeof(InputActionMap);
bool copyIsAction = copiedType == typeof(InputAction);
bool hasPastableData = (copyIsMap || (copyIsAction && haveMap));
return hasPastableData;
}
public static void GetContextMenuForActionMapItem(ActionMapsView mapView, InputActionMapsTreeViewItem treeViewItem, int index)
{
treeViewItem.OnContextualMenuPopulateEvent = (menuEvent =>
{
// TODO: AddAction should enable m_RenameOnActionAdded
menuEvent.menu.AppendAction(add_Action_String, _ => mapView.Dispatch(Commands.AddAction()));
menuEvent.menu.AppendSeparator();
menuEvent.menu.AppendAction(rename_String, _ => mapView.RenameActionMap(index));
menuEvent.menu.AppendAction(duplicate_String, _ => mapView.DuplicateActionMap(index));
menuEvent.menu.AppendAction(delete_String, _ => mapView.DeleteActionMap(index));
menuEvent.menu.AppendSeparator();
menuEvent.menu.AppendAction(copy_String, _ => mapView.CopyItems());
menuEvent.menu.AppendAction(cut_String, _ => mapView.CutItems());
if (CanPasteIntoActionMaps(mapView))
{
bool copyIsAction = CopyPasteHelper.GetCopiedClipboardType() == typeof(InputAction);
if (CopyPasteHelper.HasPastableClipboardData(typeof(InputActionMap)))
menuEvent.menu.AppendAction(paste_String, _ => mapView.PasteItems(copyIsAction));
}
menuEvent.menu.AppendSeparator();
menuEvent.menu.AppendAction(add_Action_Map_String, _ => mapView.AddActionMap());
});
}
// Add "Add Action Map" option to empty space under the ListView. Matches with old IMGUI style (ISX-1519).
// Include Paste here as well, since it makes sense for adding ActionMaps.
public static void GetContextMenuForActionMapsEmptySpace(ActionMapsView mapView, VisualElement element)
{
_ = new ContextualMenuManipulator(menuEvent =>
{
if (CanPasteIntoActionMaps(mapView))
{
bool copyIsAction = CopyPasteHelper.GetCopiedClipboardType() == typeof(InputAction);
menuEvent.menu.AppendAction(paste_String, _ => mapView.PasteItems(copyIsAction));
menuEvent.menu.AppendSeparator();
}
menuEvent.menu.AppendAction(add_Action_Map_String, _ => mapView.AddActionMap());
}) { target = element };
}
#endregion
#region Actions
// Determine whether current clipboard contents can pasted into the Actions TreeView
//
// item selected => can paste either Action or Binding (depends on selected item context)
// empty view => can only paste Action
// no selection => can only paste Action
//
private static bool CanPasteIntoActions(TreeView treeView)
{
bool hasPastableData = false;
bool selected = treeView.selectedIndex != -1;
if (selected)
{
var item = treeView.GetItemDataForIndex<ActionOrBindingData>(treeView.selectedIndex);
var itemType = item.isAction ? typeof(InputAction) : typeof(InputBinding);
hasPastableData = CopyPasteHelper.HasPastableClipboardData(itemType);
}
else
{
// Cannot paste Binding when no Action is selected or into an empty view
bool copyIsBinding = CopyPasteHelper.GetCopiedClipboardType() == typeof(InputBinding);
hasPastableData = !copyIsBinding && CopyPasteHelper.HasPastableClipboardData(typeof(InputAction));
}
return hasPastableData;
}
// Add the "Paste" option to all elements in the Action area.
public static void GetContextMenuForActionListView(ActionsTreeView actionsTreeView, TreeView treeView, VisualElement target)
{
_ = new ContextualMenuManipulator(menuEvent =>
{
bool haveMap = actionsTreeView.GetMapCount() > 0;
if (haveMap)
{
bool hasPastableData = CanPasteIntoActions(treeView);
if (hasPastableData)
{
menuEvent.menu.AppendAction(paste_String, _ => actionsTreeView.PasteItems());
}
menuEvent.menu.AppendSeparator();
menuEvent.menu.AppendAction(add_Action_String, _ => actionsTreeView.AddAction());
}
}) { target = target };
}
// Add "Add Action" option to empty space under the TreeView. Matches with old IMGUI style (ISX-1519).
// Include Paste here as well, since it makes sense for Actions; thus users would expect it for Bindings too.
public static void GetContextMenuForActionsEmptySpace(ActionsTreeView actionsTreeView, TreeView treeView, VisualElement target, bool onlyShowIfTreeIsEmpty = false)
{
_ = new ContextualMenuManipulator(menuEvent =>
{
bool haveMap = actionsTreeView.GetMapCount() > 0;
if (haveMap && (!onlyShowIfTreeIsEmpty || treeView.GetTreeCount() == 0))
{
bool hasPastableData = CanPasteIntoActions(treeView);
if (hasPastableData)
{
menuEvent.menu.AppendAction(paste_String, _ => actionsTreeView.PasteItems());
menuEvent.menu.AppendSeparator();
}
menuEvent.menu.AppendAction(add_Action_String, _ => actionsTreeView.AddAction());
}
}) { target = target };
}
public static void GetContextMenuForActionItem(ActionsTreeView treeView, InputActionsTreeViewItem treeViewItem, string controlLayout, int index)
{
_ = new ContextualMenuManipulator(menuEvent =>
{
menuEvent.menu.AppendAction(add_Binding_String, _ => treeView.AddBinding(index));
AppendCompositeMenuItems(treeView, controlLayout, index, (name, action) => menuEvent.menu.AppendAction(name, _ => action.Invoke()));
menuEvent.menu.AppendSeparator();
AppendRenameAction(menuEvent, treeView, index);
AppendDuplicateDeleteCutAndCopyActionsSection(menuEvent, treeView, index);
}) { target = treeViewItem };
}
public static Action GetContextMenuForActionAddItem(ActionsTreeView treeView, string controlLayout, int index)
{
return () =>
{
GenericMenu menu = new GenericMenu();
menu.AddItem(new GUIContent(add_Binding_String), false, () => treeView.AddBinding(index));
AppendCompositeMenuItems(treeView, controlLayout, index, (name, action) => menu.AddItem(new GUIContent(name), false, action.Invoke));
menu.ShowAsContext();
};
}
private static void AppendCompositeMenuItems(ActionsTreeView treeView, string expectedControlLayout, int index, Action<string, Action> addToMenuAction)
{
foreach (var compositeName in InputBindingComposite.s_Composites.internedNames.Where(x =>
!InputBindingComposite.s_Composites.aliases.Contains(x)).OrderBy(x => x))
{
// Skip composites we should hide
var compositeType = InputBindingComposite.s_Composites.LookupTypeRegistration(compositeName);
var designTimeVisible = compositeType.GetCustomAttribute<DesignTimeVisibleAttribute>();
if (designTimeVisible != null && !designTimeVisible.Visible)
continue;
// Skip composites that don't match the expected control layout
// NOTE: "Any" is a special case and expected to be null
if (!string.IsNullOrEmpty(expectedControlLayout))
{
var valueType = InputBindingComposite.GetValueType(compositeName);
if (valueType != null &&
!InputControlLayout.s_Layouts.ValueTypeIsAssignableFrom(
new InternedString(expectedControlLayout), valueType))
continue;
}
var displayName = compositeType.GetCustomAttribute<DisplayNameAttribute>();
var niceName = displayName != null ? displayName.DisplayName.Replace('/', '\\') : ObjectNames.NicifyVariableName(compositeName) + " Composite";
addToMenuAction.Invoke($"Add {niceName}", () => treeView.AddComposite(index, compositeName));
}
}
public static void GetContextMenuForCompositeItem(ActionsTreeView treeView, InputActionsTreeViewItem treeViewItem, int index)
{
_ = new ContextualMenuManipulator(menuEvent =>
{
AppendRenameAction(menuEvent, treeView, index);
AppendDuplicateDeleteCutAndCopyActionsSection(menuEvent, treeView, index);
}) { target = treeViewItem };
}
public static void GetContextMenuForBindingItem(ActionsTreeView treeView, InputActionsTreeViewItem treeViewItem, int index)
{
_ = new ContextualMenuManipulator(menuEvent =>
{
AppendDuplicateDeleteCutAndCopyActionsSection(menuEvent, treeView, index);
}) { target = treeViewItem };
}
private static void AppendRenameAction(ContextualMenuPopulateEvent menuEvent, ActionsTreeView treeView, int index)
{
menuEvent.menu.AppendAction(rename_String, _ => treeView.RenameActionItem(index));
}
// These actions are always either all present, or all missing, so we can group their Append calls here.
private static void AppendDuplicateDeleteCutAndCopyActionsSection(ContextualMenuPopulateEvent menuEvent, ActionsTreeView actionsTreeView, int index)
{
menuEvent.menu.AppendAction(duplicate_String, _ => actionsTreeView.DuplicateItem(index));
menuEvent.menu.AppendAction(delete_String, _ => actionsTreeView.DeleteItem(index));
menuEvent.menu.AppendSeparator();
menuEvent.menu.AppendAction(copy_String, _ => actionsTreeView.CopyItems());
menuEvent.menu.AppendAction(cut_String, _ => actionsTreeView.CutItems());
}
#endregion
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 989e6ef705a83455c81a5d0a22910406
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,206 @@
#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine.UIElements;
using PopupWindow = UnityEngine.UIElements.PopupWindow;
namespace UnityEngine.InputSystem.Editor
{
internal class ControlSchemesView : ViewBase<InputControlScheme>
{
//is used to save the new name of the control scheme when renaming
private string m_NewName;
public event Action<ViewBase<InputControlScheme>> OnClosing;
public ControlSchemesView(VisualElement root, StateContainer stateContainer, bool updateExisting = false)
: base(root, stateContainer)
{
m_UpdateExisting = updateExisting;
var controlSchemeEditor = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
InputActionsEditorConstants.PackagePath +
InputActionsEditorConstants.ResourcesPath +
InputActionsEditorConstants.ControlSchemeEditorViewUxml);
var controlSchemeVisualElement = controlSchemeEditor.CloneTree();
controlSchemeVisualElement.Q<Button>(kCancelButton).clicked += Cancel;
controlSchemeVisualElement.Q<Button>(kSaveButton).clicked += SaveAndClose;
controlSchemeVisualElement.Q<TextField>(kControlSchemeNameTextField).RegisterCallback<FocusOutEvent>(evt =>
{
Dispatch((in InputActionsEditorState state) =>
{
// If the name is the same as the current name, don't change it
var newName = ((TextField)evt.currentTarget).value.Trim();
if (string.IsNullOrEmpty(newName) || String.Compare(newName, state.selectedControlScheme.name) == 0)
{
m_NewName = String.Empty;
// write back the value to the text field if the name was empty
((TextField)evt.currentTarget).value = state.selectedControlScheme.name;
}
else
{
m_NewName = ControlSchemeCommands.MakeUniqueControlSchemeName(state, newName);
// write back the value to the text field if the name was not unique
((TextField)evt.currentTarget).value = m_NewName;
}
return state.With(selectedControlScheme: state.selectedControlScheme);
});
});
m_ModalWindow = new VisualElement
{
style = { position = new StyleEnum<Position>(Position.Absolute) }
};
var popupWindow = new PopupWindow
{
text = "Add Control Scheme",
style = { position = new StyleEnum<Position>(Position.Absolute) }
};
popupWindow.contentContainer.Add(controlSchemeVisualElement);
m_ModalWindow.Add(popupWindow);
root.Add(m_ModalWindow);
m_ModalWindow.StretchToParentSize();
popupWindow.RegisterCallback<ClickEvent>(evt => evt.StopPropagation());
m_ListView = controlSchemeVisualElement.Q<MultiColumnListView>(kControlSchemesListView);
m_ListView.columns[kDeviceTypeColumnName].makeCell = () => new Label();
m_ListView.columns[kDeviceTypeColumnName].bindCell = BindDeviceTypeCell;
m_ListView.columns[kRequiredColumnName].makeCell = MakeRequiredCell;
m_ListView.columns[kRequiredColumnName].bindCell = BindDeviceRequiredCell;
m_ListView.columns[kRequiredColumnName].unbindCell = UnbindDeviceRequiredCell;
m_ListView.Q<Button>(kUnityListViewAddButton).clickable = new Clickable(AddDeviceRequirement);
m_ListView.Q<Button>(kUnityListViewRemoveButton).clickable = new Clickable(RemoveDeviceRequirement);
m_ListView.itemIndexChanged += (oldPosition, newPosition) =>
{
Dispatch(ControlSchemeCommands.ReorderDeviceRequirements(oldPosition, newPosition));
};
m_ListView.itemsSource = new List<string>();
CreateSelector(s => s.selectedControlScheme,
(_, s) => s.selectedControlScheme);
}
private void AddDeviceRequirement()
{
var dropdown = new InputControlPickerDropdown(new InputControlPickerState(), path =>
{
var requirement = new InputControlScheme.DeviceRequirement { controlPath = path, isOptional = false };
Dispatch(ControlSchemeCommands.AddDeviceRequirement(requirement));
}, mode: InputControlPicker.Mode.PickDevice);
dropdown.Show(new Rect(Event.current.mousePosition, Vector2.zero));
}
private void RemoveDeviceRequirement()
{
if (m_ListView.selectedIndex == -1)
return;
Dispatch(ControlSchemeCommands.RemoveDeviceRequirement(m_ListView.selectedIndex));
}
public override void RedrawUI(InputControlScheme viewState)
{
rootElement.Q<TextField>(kControlSchemeNameTextField).value = string.IsNullOrEmpty(m_NewName) ? viewState.name : m_NewName;
m_ListView.itemsSource?.Clear();
m_ListView.itemsSource = viewState.deviceRequirements.Count > 0 ?
viewState.deviceRequirements.Select(r => (r.controlPath, r.isOptional)).ToList() :
new List<(string, bool)>();
m_ListView.Rebuild();
}
public override void DestroyView()
{
m_ModalWindow.RemoveFromHierarchy();
}
private void SaveAndClose()
{
// Persist the current ControlScheme values to the SerializedProperty
Dispatch(ControlSchemeCommands.SaveControlScheme(m_NewName, m_UpdateExisting));
CloseView();
}
private void Cancel()
{
// Reload the selected ControlScheme values from the SerilaizedProperty and throw away any changes
Dispatch(ControlSchemeCommands.ResetSelectedControlScheme());
CloseView();
}
private void CloseView()
{
// Closing the View without explicitly selecting "Save" or "Cancel" holds the values in the
// current UI state but won't persist them; the Asset Editor state isn't dirtied.
//
// This means accidentally clicking outside of the View (closes the dialog) won't immediately
// cause loss of work: the "Edit Control Scheme" option can be selected to re-open the View
// the changes retained. However, if a different ControlScheme is selected or the Asset
// Editor window is closed, then the changes are lost.
m_NewName = string.Empty;
OnClosing?.Invoke(this);
}
private VisualElement MakeRequiredCell()
{
var ve = new VisualElement
{
style =
{
flexDirection = FlexDirection.Column,
flexGrow = 1,
alignContent = new StyleEnum<Align>(Align.Center)
}
};
ve.Add(new Toggle());
return ve;
}
private void BindDeviceRequiredCell(VisualElement visualElement, int rowIndex)
{
var toggle = visualElement.Q<Toggle>();
var rowItem = ((string path, bool optional))m_ListView.itemsSource[rowIndex];
toggle.value = !rowItem.optional;
var eventCallback = (EventCallback<ChangeEvent<bool>>)(evt =>
Dispatch(ControlSchemeCommands.ChangeDeviceRequirement(rowIndex, evt.newValue)));
toggle.userData = eventCallback;
toggle.RegisterValueChangedCallback(eventCallback);
}
private void UnbindDeviceRequiredCell(VisualElement visualElement, int rowIndex)
{
var toggle = visualElement.Q<Toggle>();
toggle.UnregisterValueChangedCallback((EventCallback<ChangeEvent<bool>>)toggle.userData);
}
private void BindDeviceTypeCell(VisualElement visualElement, int rowIndex)
{
((Label)visualElement).text = (((string, bool))m_ListView.itemsSource[rowIndex]).Item1;
}
private readonly bool m_UpdateExisting;
private MultiColumnListView m_ListView;
private VisualElement m_ModalWindow;
private const string kControlSchemeNameTextField = "control-scheme-name";
private const string kCancelButton = "cancel-button";
private const string kSaveButton = "save-button";
private const string kControlSchemesListView = "control-schemes-list-view";
private const string kDeviceTypeColumnName = "device-type";
private const string kRequiredColumnName = "required";
private const string kUnityListViewAddButton = "unity-list-view__add-button";
private const string kUnityListViewRemoveButton = "unity-list-view__remove-button";
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 66ed65ecd8caca54ba814d536ed4d5b2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,517 @@
#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
using System;
using System.Collections.Generic;
using System.Text;
using UnityEditor;
namespace UnityEngine.InputSystem.Editor
{
// TODO Make buffers an optional argument and only allocate if not already passed, reuse a common buffer
internal static class CopyPasteHelper
{
private const string k_CopyPasteMarker = "INPUTASSET ";
private const string k_StartOfText = "\u0002";
private const string k_EndOfTransmission = "\u0004";
private const string k_BindingData = "bindingData";
private const string k_EndOfBinding = "+++";
private static readonly Dictionary<Type, string> k_TypeMarker = new Dictionary<Type, string>
{
{typeof(InputActionMap), "InputActionMap"},
{typeof(InputAction), "InputAction"},
{typeof(InputBinding), "InputBinding"},
};
private static SerializedProperty s_lastAddedElement;
private static InputActionsEditorState s_State;
private static bool s_lastClipboardActionWasCut = false;
private static bool IsComposite(SerializedProperty property) => property.FindPropertyRelative("m_Flags").intValue == (int)InputBinding.Flags.Composite;
private static bool IsPartOfComposite(SerializedProperty property) => property.FindPropertyRelative("m_Flags").intValue == (int)InputBinding.Flags.PartOfComposite;
private static string PropertyName(SerializedProperty property) => property.FindPropertyRelative("m_Name").stringValue;
#region Cut
public static void CutActionMap(InputActionsEditorState state)
{
CopyActionMap(state);
s_lastClipboardActionWasCut = true;
}
public static void Cut(InputActionsEditorState state)
{
Copy(state);
s_lastClipboardActionWasCut = true;
}
#endregion
#region Copy
public static void CopyActionMap(InputActionsEditorState state)
{
var actionMap = Selectors.GetSelectedActionMap(state)?.wrappedProperty;
var selectedObject = Selectors.GetSelectedActionMap(state)?.wrappedProperty;
CopySelectedTreeViewItemsToClipboard(new List<SerializedProperty> {selectedObject}, typeof(InputActionMap), actionMap);
}
public static void Copy(InputActionsEditorState state)
{
var actionMap = Selectors.GetSelectedActionMap(state)?.wrappedProperty;
var selectedObject = Selectors.GetSelectedAction(state)?.wrappedProperty;
var type = typeof(InputAction);
if (state.selectionType == SelectionType.Binding)
{
selectedObject = Selectors.GetSelectedBinding(state)?.wrappedProperty;
type = typeof(InputBinding);
}
CopySelectedTreeViewItemsToClipboard(new List<SerializedProperty> {selectedObject}, type, actionMap);
}
private static void CopySelectedTreeViewItemsToClipboard(List<SerializedProperty> items, Type type, SerializedProperty actionMap = null)
{
var copyBuffer = new StringBuilder();
CopyItems(items, copyBuffer, type, actionMap);
EditorHelpers.SetSystemCopyBufferContents(copyBuffer.ToString());
s_lastClipboardActionWasCut = false;
}
internal static void CopyItems(List<SerializedProperty> items, StringBuilder buffer, Type type, SerializedProperty actionMap)
{
buffer.Append(k_CopyPasteMarker);
buffer.Append(k_TypeMarker[type]);
foreach (var item in items)
{
CopyItemData(item, buffer, type, actionMap);
buffer.Append(k_EndOfTransmission);
}
}
private static void CopyItemData(SerializedProperty item, StringBuilder buffer, Type type, SerializedProperty actionMap)
{
if (item == null)
return;
buffer.Append(k_StartOfText);
buffer.Append(item.CopyToJson(true));
if (type == typeof(InputAction))
AppendBindingDataForAction(buffer, actionMap, item);
if (type == typeof(InputBinding) && IsComposite(item))
AppendBindingDataForComposite(buffer, actionMap, item);
}
private static void AppendBindingDataForAction(StringBuilder buffer, SerializedProperty actionMap, SerializedProperty item)
{
buffer.Append(k_BindingData);
foreach (var binding in GetBindingsForActionInMap(actionMap, item))
{
buffer.Append(binding.CopyToJson(true));
buffer.Append(k_EndOfBinding);
}
}
private static void AppendBindingDataForComposite(StringBuilder buffer, SerializedProperty actionMap, SerializedProperty item)
{
var bindingsArray = actionMap.FindPropertyRelative(nameof(InputActionMap.m_Bindings));
buffer.Append(k_BindingData);
foreach (var binding in GetBindingsForComposite(bindingsArray, item.GetIndexOfArrayElement()))
{
buffer.Append(binding.CopyToJson(true));
buffer.Append(k_EndOfBinding);
}
}
private static IEnumerable<SerializedProperty> GetBindingsForActionInMap(SerializedProperty actionMap, SerializedProperty action)
{
var actionName = PropertyName(action);
var bindingsArray = actionMap.FindPropertyRelative(nameof(InputActionMap.m_Bindings));
var bindings = bindingsArray.Where(binding => binding.FindPropertyRelative("m_Action").stringValue.Equals(actionName));
return bindings;
}
#endregion
#region PasteChecks
public static bool HasPastableClipboardData(Type selectedType)
{
var clipboard = EditorHelpers.GetSystemCopyBufferContents();
if (clipboard.Length < k_CopyPasteMarker.Length)
return false;
var isInputAssetData = clipboard.StartsWith(k_CopyPasteMarker);
return isInputAssetData && IsMatchingType(selectedType, GetCopiedClipboardType());
}
private static bool IsMatchingType(Type selectedType, Type copiedType)
{
if (selectedType == typeof(InputActionMap))
return copiedType == typeof(InputActionMap) || copiedType == typeof(InputAction);
if (selectedType == typeof(InputAction))
return copiedType == typeof(InputAction) || copiedType == typeof(InputBinding);
//bindings and composites
return copiedType == typeof(InputBinding);
}
public static Type GetCopiedType(string buffer)
{
if (!buffer.StartsWith(k_CopyPasteMarker))
return null;
foreach (var typePair in k_TypeMarker)
{
if (buffer.Substring(k_CopyPasteMarker.Length).StartsWith(typePair.Value))
return typePair.Key;
}
return null;
}
public static Type GetCopiedClipboardType()
{
return GetCopiedType(EditorHelpers.GetSystemCopyBufferContents());
}
#endregion
#region Paste
public static SerializedProperty PasteActionMapsFromClipboard(InputActionsEditorState state)
{
s_lastAddedElement = null;
var typeOfCopiedData = GetCopiedClipboardType();
if (typeOfCopiedData != typeof(InputActionMap)) return null;
s_State = state;
var actionMapArray = state.serializedObject.FindProperty(nameof(InputActionAsset.m_ActionMaps));
PasteData(EditorHelpers.GetSystemCopyBufferContents(), new[] {state.selectedActionMapIndex}, actionMapArray);
// Don't want to be able to paste repeatedly after a cut - ISX-1821
if (s_lastAddedElement != null && s_lastClipboardActionWasCut)
EditorHelpers.SetSystemCopyBufferContents(string.Empty);
return s_lastAddedElement;
}
public static SerializedProperty PasteActionsOrBindingsFromClipboard(InputActionsEditorState state, bool addLast = false, int mapIndex = -1)
{
s_lastAddedElement = null;
s_State = state;
var typeOfCopiedData = GetCopiedClipboardType();
if (typeOfCopiedData == typeof(InputAction))
PasteActionsFromClipboard(state, addLast, mapIndex);
if (typeOfCopiedData == typeof(InputBinding))
PasteBindingsFromClipboard(state);
// Don't want to be able to paste repeatedly after a cut - ISX-1821
if (s_lastAddedElement != null && s_lastClipboardActionWasCut)
EditorHelpers.SetSystemCopyBufferContents(string.Empty);
return s_lastAddedElement;
}
private static void PasteActionsFromClipboard(InputActionsEditorState state, bool addLast, int mapIndex)
{
var actionMap = mapIndex >= 0 ? Selectors.GetActionMapAtIndex(state, mapIndex)?.wrappedProperty
: Selectors.GetSelectedActionMap(state)?.wrappedProperty;
var actionArray = actionMap?.FindPropertyRelative(nameof(InputActionMap.m_Actions));
if (actionArray == null) return;
var index = state.selectedActionIndex;
if (addLast)
index = actionArray.arraySize - 1;
PasteData(EditorHelpers.GetSystemCopyBufferContents(), new[] {index}, actionArray);
}
private static void PasteBindingsFromClipboard(InputActionsEditorState state)
{
var actionMap = Selectors.GetSelectedActionMap(state)?.wrappedProperty;
var bindingsArray = actionMap?.FindPropertyRelative(nameof(InputActionMap.m_Bindings));
if (bindingsArray == null) return;
int newBindingIndex;
if (state.selectionType == SelectionType.Action)
newBindingIndex = Selectors.GetLastBindingIndexForSelectedAction(state);
else
newBindingIndex = state.selectedBindingIndex;
PasteData(EditorHelpers.GetSystemCopyBufferContents(), new[] { newBindingIndex }, bindingsArray);
}
private static void PasteData(string copyBufferString, int[] indicesToInsert, SerializedProperty arrayToInsertInto)
{
if (!copyBufferString.StartsWith(k_CopyPasteMarker))
return;
PasteItems(copyBufferString, indicesToInsert, arrayToInsertInto);
}
internal static void PasteItems(string copyBufferString, int[] indicesToInsert, SerializedProperty arrayToInsertInto)
{
// Split buffer into transmissions and then into transmission blocks
var copiedType = GetCopiedType(copyBufferString);
int indexOffset = 0;
// If the array is empty, make sure we insert at index 0
if (arrayToInsertInto.arraySize == 0)
indexOffset = -1;
foreach (var transmission in copyBufferString.Substring(k_CopyPasteMarker.Length + k_TypeMarker[copiedType].Length)
.Split(new[] {k_EndOfTransmission}, StringSplitOptions.RemoveEmptyEntries))
{
indexOffset++;
foreach (var index in indicesToInsert)
PasteBlocks(transmission, index + indexOffset, arrayToInsertInto, copiedType);
}
}
private static void PasteBlocks(string transmission, int indexToInsert, SerializedProperty arrayToInsertInto, Type copiedType)
{
var block = transmission.Substring(transmission.IndexOf(k_StartOfText, StringComparison.Ordinal) + 1);
if (copiedType == typeof(InputActionMap))
PasteElement(arrayToInsertInto, block, indexToInsert, out _);
else if (copiedType == typeof(InputAction))
PasteAction(arrayToInsertInto, block, indexToInsert);
else
{
var actionName = Selectors.GetSelectedBinding(s_State)?.wrappedProperty.FindPropertyRelative("m_Action")
.stringValue;
if (s_State.selectionType == SelectionType.Action)
{
SerializedProperty property = Selectors.GetSelectedAction(s_State)?.wrappedProperty;
if (property == null)
return;
actionName = PropertyName(property);
}
if (actionName == null)
return;
PasteBindingOrComposite(arrayToInsertInto, block, indexToInsert, actionName);
}
}
private static SerializedProperty PasteElement(SerializedProperty arrayProperty, string json, int index, out string oldId, string name = "newElement", bool changeName = true, bool assignUniqueIDs = true)
{
var duplicatedProperty = AddElement(arrayProperty, name, index);
duplicatedProperty.RestoreFromJson(json);
oldId = duplicatedProperty.FindPropertyRelative("m_Id").stringValue;
if (changeName)
InputActionSerializationHelpers.EnsureUniqueName(duplicatedProperty);
if (assignUniqueIDs)
InputActionSerializationHelpers.AssignUniqueIDs(duplicatedProperty);
s_lastAddedElement = duplicatedProperty;
return duplicatedProperty;
}
private static void PasteAction(SerializedProperty arrayProperty, string jsonToInsert, int indexToInsert)
{
var json = jsonToInsert.Split(k_BindingData, StringSplitOptions.RemoveEmptyEntries);
var bindingJsons = new string[] {};
if (json.Length > 1)
bindingJsons = json[1].Split(k_EndOfBinding, StringSplitOptions.RemoveEmptyEntries);
var property = PasteElement(arrayProperty, json[0], indexToInsert, out _, "");
var newName = PropertyName(property);
var newId = property.FindPropertyRelative("m_Id").stringValue;
var actionMapTo = Selectors.GetActionMapForAction(s_State, newId);
var bindingArrayToInsertTo = actionMapTo.FindPropertyRelative(nameof(InputActionMap.m_Bindings));
var index = Mathf.Clamp(Selectors.GetBindingIndexBeforeAction(arrayProperty, indexToInsert, bindingArrayToInsertTo), 0, bindingArrayToInsertTo.arraySize);
foreach (var bindingJson in bindingJsons)
{
var newIndex = PasteBindingOrComposite(bindingArrayToInsertTo, bindingJson, index, newName, false);
index = newIndex;
}
s_lastAddedElement = property;
}
private static int PasteBindingOrComposite(SerializedProperty arrayProperty, string json, int index, string actionName, bool createCompositeParts = true)
{
var pastePartOfComposite = IsPartOfComposite(json);
bool currentPartOfComposite = false;
bool currentIsComposite = false;
if (arrayProperty.arraySize == 0)
index = 0;
if (index > 0)
{
var currentProperty = arrayProperty.GetArrayElementAtIndex(index - 1);
currentPartOfComposite = IsPartOfComposite(currentProperty);
currentIsComposite = IsComposite(currentProperty) || currentPartOfComposite;
if (pastePartOfComposite && !currentIsComposite) //prevent pasting part of composite into non-composite
return index;
}
// Update the target index for special cases when pasting a Binding
if (s_State.selectionType != SelectionType.Action && createCompositeParts)
{
// - Pasting into a Composite with CompositePart not the target, i.e. Composite "root" selected, paste at the end of the composite
// - Pasting a non-CompositePart, i.e. regular Binding, needs to skip all the CompositeParts (if any)
if ((pastePartOfComposite && !currentPartOfComposite) || !pastePartOfComposite)
index = Selectors.GetSelectedBindingIndexAfterCompositeBindings(s_State) + 1;
}
if (json.Contains(k_BindingData)) //copied data is composite with bindings - only true for directly copied composites, not for composites from copied actions
return PasteCompositeFromJson(arrayProperty, json, index, actionName);
var property = PasteElement(arrayProperty, json, index, out var oldId, "", false);
if (IsComposite(property))
return PasteComposite(arrayProperty, property, PropertyName(property), actionName, index, oldId, createCompositeParts); //Paste composites copied with actions
property.FindPropertyRelative("m_Action").stringValue = actionName;
return index + 1;
}
private static int PasteComposite(SerializedProperty bindingsArray, SerializedProperty duplicatedComposite, string name, string actionName, int index, string oldId, bool createCompositeParts)
{
duplicatedComposite.FindPropertyRelative("m_Name").stringValue = name;
duplicatedComposite.FindPropertyRelative("m_Action").stringValue = actionName;
if (createCompositeParts)
{
var composite = Selectors.GetBindingForId(s_State, oldId, out var bindingsFrom);
var bindings = GetBindingsForComposite(bindingsFrom, composite.GetIndexOfArrayElement());
PastePartsOfComposite(bindingsArray, bindings, ++index, actionName);
}
return index + 1;
}
private static int PastePartsOfComposite(SerializedProperty bindingsToInsertTo, List<SerializedProperty> bindingsOfComposite, int index, string actionName)
{
foreach (var binding in bindingsOfComposite)
{
var newBinding = DuplicateElement(bindingsToInsertTo, binding, PropertyName(binding), index++, false);
newBinding.FindPropertyRelative("m_Action").stringValue = actionName;
}
return index;
}
private static int PasteCompositeFromJson(SerializedProperty arrayProperty, string json, int index, string actionName)
{
var jsons = json.Split(k_BindingData, StringSplitOptions.RemoveEmptyEntries);
var property = PasteElement(arrayProperty, jsons[0], index, out _, "", false);
var bindingJsons = jsons[1].Split(k_EndOfBinding, StringSplitOptions.RemoveEmptyEntries);
property.FindPropertyRelative("m_Action").stringValue = actionName;
foreach (var bindingJson in bindingJsons)
PasteBindingOrComposite(arrayProperty, bindingJson, ++index, actionName, false);
return index + 1;
}
private static bool IsPartOfComposite(string json)
{
if (!json.Contains("m_Flags") || json.Contains(k_BindingData))
return false;
var ob = JsonUtility.FromJson<InputBinding>(json);
return ob.m_Flags == InputBinding.Flags.PartOfComposite;
}
private static SerializedProperty AddElement(SerializedProperty arrayProperty, string name, int index = -1)
{
var uniqueName = InputActionSerializationHelpers.FindUniqueName(arrayProperty, name);
if (index < 0)
index = arrayProperty.arraySize;
arrayProperty.InsertArrayElementAtIndex(index);
var elementProperty = arrayProperty.GetArrayElementAtIndex(index);
elementProperty.ResetValuesToDefault();
elementProperty.FindPropertyRelative("m_Name").stringValue = uniqueName;
elementProperty.FindPropertyRelative("m_Id").stringValue = Guid.NewGuid().ToString();
return elementProperty;
}
public static int DeleteCutElements(InputActionsEditorState state)
{
if (!state.hasCutElements)
return -1;
var cutElements = state.GetCutElements();
var index = state.selectedActionMapIndex;
if (cutElements[0].type == typeof(InputAction))
index = state.selectedActionIndex;
else if (cutElements[0].type == typeof(InputBinding))
index = state.selectionType == SelectionType.Binding ? state.selectedBindingIndex : state.selectedActionIndex;
foreach (var cutElement in cutElements)
{
var cutIndex = cutElement.GetIndexOfProperty(state);
var actionMapIndex = cutElement.actionMapIndex(state);
var actionMap = Selectors.GetActionMapAtIndex(state, actionMapIndex)?.wrappedProperty;
var isInsertBindingIntoAction = cutElement.type == typeof(InputBinding) && state.selectionType == SelectionType.Action;
if (cutElement.type == typeof(InputBinding) || cutElement.type == typeof(InputAction))
{
if (cutElement.type == typeof(InputAction))
{
var action = Selectors.GetActionForIndex(actionMap, cutIndex);
var id = InputActionSerializationHelpers.GetId(action);
InputActionSerializationHelpers.DeleteActionAndBindings(actionMap, id);
}
else
{
var binding = Selectors.GetCompositeOrBindingInMap(actionMap, cutIndex).wrappedProperty;
if (binding.FindPropertyRelative("m_Flags").intValue == (int)InputBinding.Flags.Composite && !isInsertBindingIntoAction)
index -= InputActionSerializationHelpers.GetCompositePartCount(Selectors.GetSelectedActionMap(state)?.wrappedProperty.FindPropertyRelative(nameof(InputActionMap.m_Bindings)), cutIndex);
InputActionSerializationHelpers.DeleteBinding(binding, actionMap);
}
if (cutIndex <= index && actionMapIndex == state.selectedActionMapIndex && !isInsertBindingIntoAction)
index--;
}
else if (cutElement.type == typeof(InputActionMap))
{
InputActionSerializationHelpers.DeleteActionMap(state.serializedObject, InputActionSerializationHelpers.GetId(actionMap));
if (cutIndex <= index)
index--;
}
}
return index;
}
#endregion
#region Duplicate
public static void DuplicateAction(SerializedProperty arrayProperty, SerializedProperty toDuplicate, SerializedProperty actionMap, InputActionsEditorState state)
{
s_State = state;
var buffer = new StringBuilder();
buffer.Append(toDuplicate.CopyToJson(true));
AppendBindingDataForAction(buffer, actionMap, toDuplicate);
PasteAction(arrayProperty, buffer.ToString(), toDuplicate.GetIndexOfArrayElement() + 1);
}
public static int DuplicateBinding(SerializedProperty arrayProperty, SerializedProperty toDuplicate, string newActionName, int index)
{
if (IsComposite(toDuplicate))
return DuplicateComposite(arrayProperty, toDuplicate, PropertyName(toDuplicate), newActionName, index, out _).GetIndexOfArrayElement();
var binding = DuplicateElement(arrayProperty, toDuplicate, newActionName, index, false);
binding.FindPropertyRelative("m_Action").stringValue = newActionName;
return index;
}
private static SerializedProperty DuplicateComposite(SerializedProperty bindingsArray, SerializedProperty compositeToDuplicate, string name, string actionName, int index, out int newIndex, bool increaseIndex = true)
{
if (increaseIndex)
index += InputActionSerializationHelpers.GetCompositePartCount(bindingsArray, compositeToDuplicate.GetIndexOfArrayElement());
var newComposite = DuplicateElement(bindingsArray, compositeToDuplicate, name, index++, false);
newComposite.FindPropertyRelative("m_Action").stringValue = actionName;
var bindings = GetBindingsForComposite(bindingsArray, compositeToDuplicate.GetIndexOfArrayElement());
newIndex = PastePartsOfComposite(bindingsArray, bindings, index, actionName);
return newComposite;
}
public static SerializedProperty DuplicateElement(SerializedProperty arrayProperty, SerializedProperty toDuplicate, string name, int index, bool changeName = true)
{
var json = toDuplicate.CopyToJson(true);
return PasteElement(arrayProperty, json, index, out _, name, changeName);
}
#endregion
internal static List<SerializedProperty> GetBindingsForComposite(SerializedProperty bindingsArray, int indexOfComposite)
{
var compositeBindings = new List<SerializedProperty>();
var compositeStartIndex = InputActionSerializationHelpers.GetCompositeStartIndex(bindingsArray, indexOfComposite);
if (compositeStartIndex == -1)
return compositeBindings;
for (var i = compositeStartIndex + 1; i < bindingsArray.arraySize; ++i)
{
var bindingProperty = bindingsArray.GetArrayElementAtIndex(i);
var bindingFlags = (InputBinding.Flags)bindingProperty.FindPropertyRelative("m_Flags").intValue;
if ((bindingFlags & InputBinding.Flags.PartOfComposite) == 0)
break;
compositeBindings.Add(bindingProperty);
}
return compositeBindings;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d384a7a7180814f16aa8fa9d86ac3b6c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,77 @@
#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
using System.Linq;
using UnityEditor;
using UnityEngine.UIElements;
namespace UnityEngine.InputSystem.Editor
{
/// <summary>
/// Class to handle drop from the actions tree view to the action maps list view.
/// </summary>
internal class DropManipulator : Manipulator
{
private EventCallback<int> DroppedPerformedCallback;
private VisualElement m_OtherVerticalList;
private ListView listView => target as ListView;
private TreeView treeView => m_OtherVerticalList as TreeView;
public DropManipulator(EventCallback<int> droppedPerformedCallback, VisualElement otherVerticalList)
{
DroppedPerformedCallback = droppedPerformedCallback;
m_OtherVerticalList = otherVerticalList;
}
protected override void RegisterCallbacksOnTarget()
{
m_OtherVerticalList.RegisterCallback<DragUpdatedEvent>(OnDragUpdatedEvent, TrickleDown.TrickleDown);
m_OtherVerticalList.RegisterCallback<DragPerformEvent>(OnDragPerformEvent, TrickleDown.TrickleDown);
}
protected override void UnregisterCallbacksFromTarget()
{
m_OtherVerticalList.UnregisterCallback<DragUpdatedEvent>(OnDragUpdatedEvent, TrickleDown.TrickleDown);
m_OtherVerticalList.UnregisterCallback<DragPerformEvent>(OnDragPerformEvent, TrickleDown.TrickleDown);
}
private void OnDragPerformEvent(DragPerformEvent evt)
{
var mapsViewItem = target.panel.Pick(evt.mousePosition)?.GetFirstAncestorOfType<InputActionMapsTreeViewItem>();
if (mapsViewItem == null)
return;
var index = treeView.selectedIndices.First();
var draggedItem = treeView.GetItemDataForIndex<ActionOrBindingData>(index);
if (!draggedItem.isAction)
return;
evt.StopImmediatePropagation();
DragAndDrop.AcceptDrag();
DroppedPerformedCallback.Invoke((int)mapsViewItem.userData);
Reset();
treeView.ReleaseMouse();
}
private int m_InitialIndex = -1;
private void OnDragUpdatedEvent(DragUpdatedEvent evt)
{
var mapsViewItem = target.panel.Pick(evt.mousePosition)?.GetFirstAncestorOfType<InputActionMapsTreeViewItem>();
if (mapsViewItem != null)
{
if (m_InitialIndex < 0 && listView != null)
m_InitialIndex = listView.selectedIndex;
//select map item to visualize the drop
listView?.SetSelectionWithoutNotify(new[] { (int)mapsViewItem.userData }); //the user data contains the index of the map item
DragAndDrop.visualMode = DragAndDropVisualMode.Move;
evt.StopImmediatePropagation();
}
else
Reset();
}
private void Reset()
{
if (m_InitialIndex >= 0)
listView?.SetSelectionWithoutNotify(new[] {m_InitialIndex}); //select the initial action map again
m_InitialIndex = -1;
}
}
}
#endif

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b52a4b3b22334b02a4d22d942a479be6
timeCreated: 1691762301

View File

@@ -0,0 +1,12 @@
#if UNITY_EDITOR
using System.Collections;
namespace UnityEngine.InputSystem.Editor
{
internal interface IViewStateCollection : IEnumerable
{
bool SequenceEqual(IViewStateCollection other);
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 49a488c9c543d82408e3f15c90d36980
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,148 @@
// UITK TreeView is not supported in earlier versions
// Therefore the UITK version of the InputActionAsset Editor is not available on earlier Editor versions either.
#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
using System;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine.InputSystem.Editor;
using UnityEngine.UIElements;
namespace UnityEngine.InputSystem.Editor
{
/// <summary>
/// A visual element that supports renaming of items.
/// </summary>
internal class InputActionMapsTreeViewItem : VisualElement
{
public EventCallback<string> EditTextFinishedCallback;
private const string kRenameTextField = "rename-text-field";
public event EventCallback<string> EditTextFinished;
public Action<ContextualMenuPopulateEvent> OnContextualMenuPopulateEvent;
// for testing purposes to know if the item is focused to accept input
internal bool IsFocused { get; private set; } = false;
private bool m_IsEditing;
private static InputActionMapsTreeViewItem s_EditingItem = null;
internal bool isDisabledActionMap { get; set; }
public InputActionMapsTreeViewItem()
{
var template = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
InputActionsEditorConstants.PackagePath +
InputActionsEditorConstants.ResourcesPath +
InputActionsEditorConstants.InputActionMapsTreeViewItemUxml);
template.CloneTree(this);
focusable = true;
delegatesFocus = false;
renameTextfield.selectAllOnFocus = true;
renameTextfield.selectAllOnMouseUp = false;
RegisterCallback<MouseDownEvent>(OnMouseDownEventForRename);
renameTextfield.RegisterCallback<FocusInEvent>(e => IsFocused = true);
renameTextfield.RegisterCallback<FocusOutEvent>(e => { OnEditTextFinished(); IsFocused = false; });
_ = new ContextualMenuManipulator(menuBuilder =>
{
OnContextualMenuPopulateEvent?.Invoke(menuBuilder);
})
{ target = this };
}
public Label label => this.Q<Label>();
private TextField renameTextfield => this.Q<TextField>(kRenameTextField);
public void UnregisterInputField()
{
renameTextfield.SetEnabled(false);
renameTextfield.selectAllOnFocus = false;
UnregisterCallback<MouseDownEvent>(OnMouseDownEventForRename);
renameTextfield.UnregisterCallback<FocusOutEvent>(e => OnEditTextFinished());
}
private double lastSingleClick;
private static InputActionMapsTreeViewItem selected;
private void OnMouseDownEventForRename(MouseDownEvent e)
{
if (e.clickCount != 1 || e.button != (int)MouseButton.LeftMouse || e.target == null)
return;
var now = EditorApplication.timeSinceStartup;
if (selected == this && now - lastSingleClick < 3)
{
FocusOnRenameTextField();
e.StopImmediatePropagation();
lastSingleClick = 0;
return;
}
lastSingleClick = now;
selected = this;
}
public void Reset()
{
if (m_IsEditing)
{
lastSingleClick = 0;
delegatesFocus = false;
renameTextfield.AddToClassList(InputActionsEditorConstants.HiddenStyleClassName);
label.RemoveFromClassList(InputActionsEditorConstants.HiddenStyleClassName);
s_EditingItem = null;
m_IsEditing = false;
}
EditTextFinished = null;
}
public void FocusOnRenameTextField()
{
if (m_IsEditing || isDisabledActionMap)
return;
delegatesFocus = true;
renameTextfield.SetValueWithoutNotify(label.text);
renameTextfield.RemoveFromClassList(InputActionsEditorConstants.HiddenStyleClassName);
label?.AddToClassList(InputActionsEditorConstants.HiddenStyleClassName);
//a bit hacky - e.StopImmediatePropagation() for events does not work like expected on ListViewItems or TreeViewItems because
//the listView/treeView reclaims the focus - this is a workaround with less overhead than rewriting the events
schedule.Execute(() => renameTextfield.Q<TextField>().Focus()).StartingIn(120);
renameTextfield.SelectAll();
s_EditingItem = this;
m_IsEditing = true;
}
public static void CancelRename()
{
s_EditingItem?.OnEditTextFinished();
}
private void OnEditTextFinished()
{
if (!m_IsEditing)
return;
lastSingleClick = 0;
delegatesFocus = false;
renameTextfield.AddToClassList(InputActionsEditorConstants.HiddenStyleClassName);
label.RemoveFromClassList(InputActionsEditorConstants.HiddenStyleClassName);
s_EditingItem = null;
m_IsEditing = false;
var text = renameTextfield.text?.Trim();
if (string.IsNullOrEmpty(text))
{
renameTextfield.schedule.Execute(() => renameTextfield.SetValueWithoutNotify(text));
return;
}
EditTextFinished?.Invoke(text);
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8b82aecdbfbdd1b49b90eb0d509b4166
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,287 @@
#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine.UIElements;
namespace UnityEngine.InputSystem.Editor
{
interface IPasteListener
{
void OnPaste(InputActionsEditorState state);
}
internal class InputActionsEditorView : ViewBase<InputActionsEditorView.ViewState>, IPasteListener
{
private const string saveButtonId = "save-asset-toolbar-button";
private const string autoSaveToggleId = "auto-save-toolbar-toggle";
private const string menuButtonId = "asset-menu";
private readonly ToolbarMenu m_ControlSchemesToolbar;
private readonly ToolbarMenu m_DevicesToolbar;
private readonly ToolbarButton m_SaveButton;
private readonly Action m_SaveAction;
public InputActionsEditorView(VisualElement root, StateContainer stateContainer, bool isProjectSettings,
Action saveAction)
: base(root, stateContainer)
{
m_SaveAction = saveAction;
var mainEditorAsset = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
InputActionsEditorConstants.PackagePath +
InputActionsEditorConstants.ResourcesPath +
InputActionsEditorConstants.MainEditorViewNameUxml);
mainEditorAsset.CloneTree(root);
var actionsTreeView = new ActionsTreeView(root, stateContainer);
CreateChildView(new ActionMapsView(root, stateContainer));
CreateChildView(actionsTreeView);
CreateChildView(new PropertiesView(root, stateContainer));
m_ControlSchemesToolbar = root.Q<ToolbarMenu>("control-schemes-toolbar-menu");
m_ControlSchemesToolbar.menu.AppendAction("Add Control Scheme...", _ => AddOrUpdateControlScheme(root));
m_ControlSchemesToolbar.menu.AppendAction("Edit Control Scheme...", _ => AddOrUpdateControlScheme(root, true), DropdownMenuAction.Status.Disabled);
m_ControlSchemesToolbar.menu.AppendAction("Duplicate Control Scheme...", _ => DuplicateControlScheme(root), DropdownMenuAction.Status.Disabled);
m_ControlSchemesToolbar.menu.AppendAction("Delete Control Scheme...", DeleteControlScheme, DropdownMenuAction.Status.Disabled);
m_DevicesToolbar = root.Q<ToolbarMenu>("control-schemes-filter-toolbar-menu");
m_DevicesToolbar.SetEnabled(false);
m_SaveButton = root.Q<ToolbarButton>(name: saveButtonId);
m_SaveButton.SetEnabled(InputEditorUserSettings.autoSaveInputActionAssets == false);
m_SaveButton.clicked += OnSaveButton;
var autoSaveToggle = root.Q<ToolbarToggle>(name: autoSaveToggleId);
autoSaveToggle.value = InputEditorUserSettings.autoSaveInputActionAssets;
autoSaveToggle.RegisterValueChangedCallback(OnAutoSaveToggle);
// Hide save toolbar if there is no save action provided since we cannot support it
if (saveAction == null)
{
var element = root.Q("save-asset-toolbar-container");
if (element != null)
{
element.style.visibility = Visibility.Hidden;
element.style.display = DisplayStyle.None;
}
}
VisualElement assetMenuButton = null;
try
{
// This only exists in the project settings version
assetMenuButton = root.Q<VisualElement>(name: menuButtonId);
}
catch {}
if (assetMenuButton != null)
{
assetMenuButton.visible = isProjectSettings;
assetMenuButton.AddToClassList(EditorGUIUtility.isProSkin ? "asset-menu-button-dark-theme" : "asset-menu-button");
var _ = new ContextualMenuManipulator(menuEvent =>
{
menuEvent.menu.AppendAction("Reset to Defaults", _ => OnReset());
menuEvent.menu.AppendAction("Remove All Action Maps", _ => OnClearActionMaps());
})
{ target = assetMenuButton, activators = { new ManipulatorActivationFilter() { button = MouseButton.LeftMouse } } };
}
// only register the state changed event here in the parent. Changes will be cascaded
// into child views.
stateContainer.StateChanged += OnStateChanged;
CreateSelector(
s => s.selectedControlSchemeIndex,
s => new ViewStateCollection<InputControlScheme>(Selectors.GetControlSchemes(s)),
(_, controlSchemes, state) => new ViewState
{
controlSchemes = controlSchemes,
selectedControlSchemeIndex = state.selectedControlSchemeIndex,
selectedDeviceIndex = state.selectedDeviceRequirementIndex
});
s_OnPasteCutElements.Add(this);
}
private void OnReset()
{
Dispatch(Commands.ReplaceActionMaps(ProjectWideActionsAsset.GetDefaultAssetJson()));
}
private void OnClearActionMaps()
{
Dispatch(Commands.ClearActionMaps());
}
private void OnSaveButton()
{
Dispatch(Commands.SaveAsset(m_SaveAction));
// Don't let focus linger after clicking (ISX-1482). Ideally this would be only applied on mouse click,
// rather than if the user is using tab to navigate UI, but there doesn't seem to be a way to differentiate
// between those interactions at the moment.
m_SaveButton.Blur();
}
private void OnAutoSaveToggle(ChangeEvent<bool> evt)
{
Dispatch(Commands.ToggleAutoSave(evt.newValue, m_SaveAction));
}
public override void RedrawUI(ViewState viewState)
{
SetUpControlSchemesMenu(viewState);
SetUpDevicesMenu(viewState);
m_SaveButton.SetEnabled(InputEditorUserSettings.autoSaveInputActionAssets == false);
}
private string SetupControlSchemeName(string name)
{
//On Windows the '&' is considered an accelerator character and will always be stripped.
//Since the ControlScheme menu isn't creating hotkeys, it can be safely assumed that they are meant to be text
//so we want to escape the character for MenuItem
if (Application.platform == RuntimePlatform.WindowsEditor)
{
name = name.Replace("&", "&&");
}
return name;
}
private void SetUpControlSchemesMenu(ViewState viewState)
{
m_ControlSchemesToolbar.menu.MenuItems().Clear();
if (viewState.controlSchemes.Any())
{
m_ControlSchemesToolbar.text = viewState.selectedControlSchemeIndex == -1
? "All Control Schemes"
: viewState.controlSchemes.ElementAt(viewState.selectedControlSchemeIndex).name;
m_ControlSchemesToolbar.menu.AppendAction("All Control Schemes", _ => SelectControlScheme(-1),
viewState.selectedControlSchemeIndex == -1 ? DropdownMenuAction.Status.Checked : DropdownMenuAction.Status.Normal);
viewState.controlSchemes.ForEach((scheme, i) =>
m_ControlSchemesToolbar.menu.AppendAction(SetupControlSchemeName(scheme.name), _ => SelectControlScheme(i),
viewState.selectedControlSchemeIndex == i ? DropdownMenuAction.Status.Checked : DropdownMenuAction.Status.Normal));
m_ControlSchemesToolbar.menu.AppendSeparator();
}
m_ControlSchemesToolbar.menu.AppendAction("Add Control Scheme...", _ => AddOrUpdateControlScheme(rootElement));
m_ControlSchemesToolbar.menu.AppendAction("Edit Control Scheme...", _ => AddOrUpdateControlScheme(rootElement, true),
viewState.selectedControlSchemeIndex != -1 ? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Disabled);
m_ControlSchemesToolbar.menu.AppendAction("Duplicate Control Scheme...", _ => DuplicateControlScheme(rootElement),
viewState.selectedControlSchemeIndex != -1 ? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Disabled);
m_ControlSchemesToolbar.menu.AppendAction("Delete Control Scheme...", DeleteControlScheme,
viewState.selectedControlSchemeIndex != -1 ? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Disabled);
}
private void SetUpDevicesMenu(ViewState viewState)
{
if (!viewState.controlSchemes.Any() || viewState.selectedControlSchemeIndex == -1)
{
m_DevicesToolbar.text = "All Devices";
m_DevicesToolbar.SetEnabled(false);
return;
}
m_DevicesToolbar.SetEnabled(true);
var currentControlScheme = viewState.controlSchemes.ElementAt(viewState.selectedControlSchemeIndex);
if (viewState.selectedDeviceIndex == -1)
m_DevicesToolbar.text = "All Devices";
m_DevicesToolbar.menu.MenuItems().Clear();
m_DevicesToolbar.menu.AppendAction("All Devices", _ => SelectDevice(-1), viewState.selectedDeviceIndex == -1
? DropdownMenuAction.Status.Checked
: DropdownMenuAction.Status.Normal);
currentControlScheme.deviceRequirements.ForEach(
(device, i) =>
{
InputControlPath.ToHumanReadableString(device.controlPath, out var name, out _);
m_DevicesToolbar.menu.AppendAction(name, _ => SelectDevice(i),
viewState.selectedDeviceIndex == i
? DropdownMenuAction.Status.Checked
: DropdownMenuAction.Status.Normal);
if (viewState.selectedDeviceIndex == i)
m_DevicesToolbar.text = name;
});
}
private void AddOrUpdateControlScheme(VisualElement parent, bool updateExisting = false)
{
if (!updateExisting)
Dispatch(ControlSchemeCommands.AddNewControlScheme());
ShowControlSchemeEditor(parent, updateExisting);
}
private void DuplicateControlScheme(VisualElement parent)
{
Dispatch(ControlSchemeCommands.DuplicateSelectedControlScheme());
ShowControlSchemeEditor(parent);
}
private void DeleteControlScheme(DropdownMenuAction obj)
{
Dispatch(ControlSchemeCommands.DeleteSelectedControlScheme());
}
private void ShowControlSchemeEditor(VisualElement parent, bool updateExisting = false)
{
var controlSchemesView = CreateChildView(new ControlSchemesView(parent, stateContainer, updateExisting));
controlSchemesView.UpdateView(stateContainer.GetState());
controlSchemesView.OnClosing += _ => DestroyChildView(controlSchemesView);
}
private void SelectControlScheme(int controlSchemeIndex)
{
Dispatch(ControlSchemeCommands.SelectControlScheme(controlSchemeIndex));
SelectDevice(-1);
}
private void SelectDevice(int deviceIndex)
{
Dispatch(ControlSchemeCommands.SelectDeviceRequirement(deviceIndex));
}
public class ViewState
{
public IEnumerable<InputControlScheme> controlSchemes;
public int selectedControlSchemeIndex;
public int selectedDeviceIndex;
}
internal static List<IPasteListener> s_OnPasteCutElements = new();
public override void DestroyView()
{
base.DestroyView();
s_OnPasteCutElements.Remove(this);
}
public void OnPaste(InputActionsEditorState state)
{
if (state.Equals(stateContainer.GetState()))
return;
Dispatch(Commands.DeleteCutElements());
}
}
internal static partial class Selectors
{
public static IEnumerable<InputControlScheme> GetControlSchemes(InputActionsEditorState state)
{
var controlSchemesArray = state.serializedObject.FindProperty(nameof(InputActionAsset.m_ControlSchemes));
if (controlSchemesArray == null)
yield break;
foreach (SerializedProperty controlScheme in controlSchemesArray)
{
yield return new InputControlScheme(controlScheme);
}
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 392e37dde5bf4a14bb315f2b4829bd6b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,131 @@
// UITK TreeView is not supported in earlier versions
// Therefore the UITK version of the InputActionAsset Editor is not available on earlier Editor versions either.
#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine.UIElements;
namespace UnityEngine.InputSystem.Editor
{
/// <summary>
/// A visual element that supports renaming of items.
/// </summary>
internal class InputActionsTreeViewItem : VisualElement
{
public EventCallback<string> EditTextFinishedCallback;
private const string kRenameTextField = "rename-text-field";
public event EventCallback<string> EditTextFinished;
private bool m_IsEditing;
private static InputActionsTreeViewItem s_EditingItem = null;
internal bool isCut { get; set; }
public InputActionsTreeViewItem()
{
var template = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
InputActionsEditorConstants.PackagePath +
InputActionsEditorConstants.ResourcesPath +
InputActionsEditorConstants.InputActionsTreeViewItemUxml);
template.CloneTree(this);
focusable = true;
delegatesFocus = false;
renameTextfield.selectAllOnFocus = true;
renameTextfield.selectAllOnMouseUp = false;
RegisterCallback<MouseDownEvent>(OnMouseDownEventForRename);
renameTextfield.RegisterCallback<FocusOutEvent>(e => OnEditTextFinished());
}
public Label label => this.Q<Label>();
private TextField renameTextfield => this.Q<TextField>(kRenameTextField);
public void UnregisterInputField()
{
renameTextfield.SetEnabled(false);
renameTextfield.selectAllOnFocus = false;
UnregisterCallback<MouseDownEvent>(OnMouseDownEventForRename);
renameTextfield.UnregisterCallback<FocusOutEvent>(e => OnEditTextFinished());
}
private float lastSingleClick;
private static InputActionsTreeViewItem selected;
private void OnMouseDownEventForRename(MouseDownEvent e)
{
if (e.clickCount != 1 || e.button != (int)MouseButton.LeftMouse || e.target == null)
return;
if (selected == this && Time.time - lastSingleClick < 3f)
{
FocusOnRenameTextField();
e.StopImmediatePropagation();
lastSingleClick = 0;
}
lastSingleClick = Time.time;
selected = this;
}
public void Reset()
{
EditTextFinished = null;
m_IsEditing = false;
}
public void FocusOnRenameTextField()
{
if (m_IsEditing || isCut)
return;
delegatesFocus = true;
renameTextfield.SetValueWithoutNotify(label.text);
renameTextfield.RemoveFromClassList(InputActionsEditorConstants.HiddenStyleClassName);
label?.AddToClassList(InputActionsEditorConstants.HiddenStyleClassName);
//a bit hacky - e.StopImmediatePropagation() for events does not work like expected on ListViewItems or TreeViewItems because
//the listView/treeView reclaims the focus - this is a workaround with less overhead than rewriting the events
DelayCall();
renameTextfield.SelectAll();
s_EditingItem = this;
m_IsEditing = true;
}
public static void CancelRename()
{
s_EditingItem?.OnEditTextFinished();
}
async void DelayCall()
{
await Task.Delay(120);
renameTextfield.Q<TextField>().Focus();
}
private void OnEditTextFinished()
{
if (!m_IsEditing)
return;
lastSingleClick = 0;
delegatesFocus = false;
renameTextfield.AddToClassList(InputActionsEditorConstants.HiddenStyleClassName);
label.RemoveFromClassList(InputActionsEditorConstants.HiddenStyleClassName);
s_EditingItem = null;
m_IsEditing = false;
var text = renameTextfield.text?.Trim();
if (string.IsNullOrEmpty(text))
{
renameTextfield.schedule.Execute(() => renameTextfield.SetValueWithoutNotify(text));
return;
}
EditTextFinished?.Invoke(text);
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a439a21d627cd7646978dd78132c4bb9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,206 @@
#if UNITY_EDITOR
using System.Linq;
using UnityEditor;
using UnityEngine.UIElements;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.Utilities;
using System.Collections.Generic;
namespace UnityEngine.InputSystem.Editor
{
internal class MatchingControlPath
{
public string deviceName
{
get;
}
public string controlName
{
get;
}
public bool isRoot
{
get;
}
public List<MatchingControlPath> children
{
get;
}
public MatchingControlPath(string deviceName, string controlName, bool isRoot)
{
this.deviceName = deviceName;
this.controlName = controlName;
this.isRoot = isRoot;
this.children = new List<MatchingControlPath>();
}
#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
public static List<TreeViewItemData<MatchingControlPath>> BuildMatchingControlPathsTreeData(List<MatchingControlPath> matchingControlPaths)
{
int id = 0;
return BuildMatchingControlPathsTreeDataRecursive(ref id, matchingControlPaths);
}
private static List<TreeViewItemData<MatchingControlPath>> BuildMatchingControlPathsTreeDataRecursive(ref int id, List<MatchingControlPath> matchingControlPaths)
{
var treeViewList = new List<TreeViewItemData<MatchingControlPath>>(matchingControlPaths.Count);
foreach (var matchingControlPath in matchingControlPaths)
{
var childTreeViewList = BuildMatchingControlPathsTreeDataRecursive(ref id, matchingControlPath.children);
var treeViewItem = new TreeViewItemData<MatchingControlPath>(id++, matchingControlPath, childTreeViewList);
treeViewList.Add(treeViewItem);
}
return treeViewList;
}
#endif
public static List<MatchingControlPath> CollectMatchingControlPaths(string path, bool showPaths, ref bool controlPathUsagePresent)
{
var matchingControlPaths = new List<MatchingControlPath>();
if (path == string.Empty)
return matchingControlPaths;
var deviceLayoutPath = InputControlPath.TryGetDeviceLayout(path);
var parsedPath = InputControlPath.Parse(path).ToArray();
// If the provided path is parseable into device and control components, draw UI which shows control layouts that match the path.
if (parsedPath.Length >= 2 && !string.IsNullOrEmpty(deviceLayoutPath))
{
bool matchExists = false;
var rootDeviceLayout = EditorInputControlLayoutCache.TryGetLayout(deviceLayoutPath);
bool isValidDeviceLayout = deviceLayoutPath == InputControlPath.Wildcard || (rootDeviceLayout != null && !rootDeviceLayout.isOverride && !rootDeviceLayout.hideInUI);
// Exit early if a malformed device layout was provided,
if (!isValidDeviceLayout)
return matchingControlPaths;
controlPathUsagePresent = parsedPath[1].usages.Count() > 0;
bool hasChildDeviceLayouts = deviceLayoutPath == InputControlPath.Wildcard || EditorInputControlLayoutCache.HasChildLayouts(rootDeviceLayout.name);
// If the path provided matches exactly one control path (i.e. has no ui-facing child device layouts or uses control usages), then exit early
if (!controlPathUsagePresent && !hasChildDeviceLayouts)
return matchingControlPaths;
// Otherwise, we will show either all controls that match the current binding (if control usages are used)
// or all controls in derived device layouts (if a no control usages are used).
// If our control path contains a usage, make sure we render the binding that belongs to the root device layout first
if (deviceLayoutPath != InputControlPath.Wildcard && controlPathUsagePresent)
{
matchExists |= CollectMatchingControlPathsForLayout(rootDeviceLayout, in parsedPath, true, matchingControlPaths);
}
// Otherwise, just render the bindings that belong to child device layouts. The binding that matches the root layout is
// already represented by the user generated control path itself.
else
{
IEnumerable<InputControlLayout> matchedChildLayouts = Enumerable.Empty<InputControlLayout>();
if (deviceLayoutPath == InputControlPath.Wildcard)
{
matchedChildLayouts = EditorInputControlLayoutCache.allLayouts
.Where(x => x.isDeviceLayout && !x.hideInUI && !x.isOverride && x.isGenericTypeOfDevice && x.baseLayouts.Count() == 0).OrderBy(x => x.displayName);
}
else
{
matchedChildLayouts = EditorInputControlLayoutCache.TryGetChildLayouts(rootDeviceLayout.name);
}
foreach (var childLayout in matchedChildLayouts)
{
matchExists |= CollectMatchingControlPathsForLayout(childLayout, in parsedPath, false, matchingControlPaths);
}
}
// Otherwise, indicate that no layouts match the current path.
if (!matchExists)
{
return null;
}
}
return matchingControlPaths;
}
/// <summary>
/// Returns true if the deviceLayout or any of its children has controls which match the provided parsed path. exist matching registered control paths.
/// </summary>
/// <param name="deviceLayout">The device layout to draw control paths for</param>
/// <param name="parsedPath">The parsed path containing details of the Input Controls that can be matched</param>
private static bool CollectMatchingControlPathsForLayout(InputControlLayout deviceLayout, in InputControlPath.ParsedPathComponent[] parsedPath, bool isRoot, List<MatchingControlPath> matchingControlPaths)
{
string deviceName = deviceLayout.displayName;
string controlName = string.Empty;
bool matchExists = false;
for (int i = 0; i < deviceLayout.m_Controls.Length; i++)
{
ref InputControlLayout.ControlItem controlItem = ref deviceLayout.m_Controls[i];
if (InputControlPath.MatchControlComponent(ref parsedPath[1], ref controlItem, true))
{
// If we've already located a match, append a ", " to the control name
// This is to accomodate cases where multiple control items match the same path within a single device layout
// Note, some controlItems have names but invalid displayNames (i.e. the Dualsense HID > leftTriggerButton)
// There are instance where there are 2 control items with the same name inside a layout definition, however they are not
// labeled significantly differently.
// The notable example is that the Android Xbox and Android Dualshock layouts have 2 d-pad definitions, one is a "button"
// while the other is an axis.
controlName += matchExists ? $", {controlItem.name}" : controlItem.name;
// if the parsePath has a 3rd component, try to match it with items in the controlItem's layout definition.
if (parsedPath.Length == 3)
{
var controlLayout = EditorInputControlLayoutCache.TryGetLayout(controlItem.layout);
if (controlLayout.isControlLayout && !controlLayout.hideInUI)
{
for (int j = 0; j < controlLayout.m_Controls.Count(); j++)
{
ref InputControlLayout.ControlItem controlLayoutItem = ref controlLayout.m_Controls[j];
if (InputControlPath.MatchControlComponent(ref parsedPath[2], ref controlLayoutItem))
{
controlName += $"/{controlLayoutItem.name}";
matchExists = true;
}
}
}
}
else
{
matchExists = true;
}
}
}
IEnumerable<InputControlLayout> matchedChildLayouts = EditorInputControlLayoutCache.TryGetChildLayouts(deviceLayout.name);
// If this layout does not have a match, or is the top level root layout,
// skip over trying to draw any items for it, and immediately try processing the child layouts
if (!matchExists)
{
foreach (var childLayout in matchedChildLayouts)
{
matchExists |= CollectMatchingControlPathsForLayout(childLayout, in parsedPath, false, matchingControlPaths);
}
}
// Otherwise, draw the items for it, and then only process the child layouts if the foldout is expanded.
else
{
var newMatchingControlPath = new MatchingControlPath(deviceName, controlName, isRoot);
matchingControlPaths.Add(newMatchingControlPath);
foreach (var childLayout in matchedChildLayouts)
{
CollectMatchingControlPathsForLayout(childLayout, in parsedPath, false, newMatchingControlPath.children);
}
}
return matchExists;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 890eded7b7fc53b489fcc92f0aeb774e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,189 @@
#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine.InputSystem.Editor.Lists;
using UnityEngine.InputSystem.Utilities;
using UnityEngine.UIElements;
namespace UnityEngine.InputSystem.Editor
{
internal class NameAndParametersListView : ViewBase<InputActionsEditorState>
{
private readonly Func<InputActionsEditorState, IEnumerable<ParameterListView>> m_ParameterListViewSelector;
private VisualElement m_ContentContainer;
private readonly Label m_NoParamsLabel;
private SerializedProperty m_ListProperty;
public NameAndParametersListView(VisualElement root, StateContainer stateContainer, SerializedProperty listProperty,
Func<InputActionsEditorState, IEnumerable<ParameterListView>> parameterListViewSelector)
: base(root, stateContainer)
{
m_ListProperty = listProperty;
m_NoParamsLabel = root.Q<Label>("no-parameters-added-label");
m_ParameterListViewSelector = parameterListViewSelector;
CreateSelector(state => state);
}
public void OnAddElement(string name)
{
if (m_ListProperty == null)
return;
var interactionsOrProcessorsList = NameAndParameters.ParseMultiple(m_ListProperty.stringValue).ToList();
var newElement = new NameAndParameters() { name = name};
interactionsOrProcessorsList.Add(newElement);
m_ListProperty.stringValue = ToSerializableString(interactionsOrProcessorsList);
m_ListProperty.serializedObject.ApplyModifiedProperties();
}
private void MoveElementUp(int index)
{
var newIndex = index - 1;
SwapElement(index, newIndex);
}
private void MoveElementDown(int index)
{
var newIndex = index + 1;
SwapElement(index, newIndex);
}
private void SwapElement(int oldIndex, int newIndex)
{
var interactionsOrProcessors = NameAndParameters.ParseMultiple(m_ListProperty.stringValue).ToArray();
var oldIndexIsValid = oldIndex >= 0 && oldIndex < interactionsOrProcessors.Length;
var newIndexIsValid = newIndex >= 0 && newIndex < interactionsOrProcessors.Length;
if (interactionsOrProcessors.Length == 0 || !newIndexIsValid || !oldIndexIsValid)
return;
MemoryHelpers.Swap(ref interactionsOrProcessors[oldIndex], ref interactionsOrProcessors[newIndex]);
m_ListProperty.stringValue = ToSerializableString(interactionsOrProcessors);
m_ListProperty.serializedObject.ApplyModifiedProperties();
}
private void DeleteElement(int index)
{
var interactionsOrProcessorsList = NameAndParameters.ParseMultiple(m_ListProperty.stringValue).ToList();
interactionsOrProcessorsList.RemoveAt(index);
m_ListProperty.stringValue = ToSerializableString(interactionsOrProcessorsList);
m_ListProperty.serializedObject.ApplyModifiedProperties();
}
private void OnParametersChanged(ParameterListView listView, int index)
{
var interactionsOrProcessorsList = NameAndParameters.ParseMultiple(m_ListProperty.stringValue).ToList();
interactionsOrProcessorsList[index] = new NameAndParameters { name = interactionsOrProcessorsList[index].name, parameters = listView.GetParameters() };
m_ListProperty.stringValue = ToSerializableString(interactionsOrProcessorsList);
m_ListProperty.serializedObject.ApplyModifiedProperties();
}
private static string ToSerializableString(IEnumerable<NameAndParameters> parametersForEachListItem)
{
if (parametersForEachListItem == null)
return string.Empty;
return string.Join(NamedValue.Separator,
parametersForEachListItem.Select(x => x.ToString()).ToArray());
}
public override void RedrawUI(InputActionsEditorState state)
{
if (m_ContentContainer != null)
rootElement.Remove(m_ContentContainer);
m_ContentContainer = new VisualElement();
rootElement.Add(m_ContentContainer);
var parameterListViews = m_ParameterListViewSelector(state).ToList();
if (parameterListViews.Count == 0)
{
m_NoParamsLabel.style.display = new StyleEnum<DisplayStyle>(DisplayStyle.Flex);
return;
}
m_NoParamsLabel.style.display = new StyleEnum<DisplayStyle>(DisplayStyle.None);
m_ContentContainer.Clear();
for (int i = 0; i < parameterListViews.Count; i++)
{
var index = i;
var buttonProperties = new ButtonProperties()
{
onClickDown = () => MoveElementDown(index),
onClickUp = () => MoveElementUp(index),
onDelete = () => DeleteElement(index),
isDownButtonActive = index < parameterListViews.Count - 1,
isUpButtonActive = index > 0
};
new NameAndParametersListViewItem(m_ContentContainer, parameterListViews[i], buttonProperties);
parameterListViews[i].onChange += () => OnParametersChanged(parameterListViews[index], index);
}
}
public override void DestroyView()
{
if (m_ContentContainer != null)
{
rootElement.Remove(m_ContentContainer);
m_ContentContainer = null;
}
}
}
internal struct ButtonProperties
{
public Action onClickUp;
public Action onClickDown;
public Action onDelete;
public bool isUpButtonActive;
public bool isDownButtonActive;
}
internal class NameAndParametersListViewItem
{
public NameAndParametersListViewItem(VisualElement root, ParameterListView parameterListView, ButtonProperties buttonProperties)
{
var itemTemplate = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
InputActionsEditorConstants.PackagePath +
InputActionsEditorConstants.ResourcesPath +
InputActionsEditorConstants.NameAndParametersListViewItemUxml);
var container = itemTemplate.CloneTree();
root.Add(container);
var header = container.Q<Toggle>();
var moveItemUpButton = new Button();
moveItemUpButton.AddToClassList(EditorGUIUtility.isProSkin ? "upDarkTheme" : "up");
moveItemUpButton.AddToClassList("name-and-parameters-list-foldout-button");
moveItemUpButton.SetEnabled(buttonProperties.isUpButtonActive);
moveItemUpButton.clicked += buttonProperties.onClickUp;
var moveItemDownButton = new Button();
moveItemDownButton.AddToClassList(EditorGUIUtility.isProSkin ? "downDarkTheme" : "down");
moveItemDownButton.AddToClassList("name-and-parameters-list-foldout-button");
moveItemDownButton.SetEnabled(buttonProperties.isDownButtonActive);
moveItemDownButton.clicked += buttonProperties.onClickDown;
var deleteItemButton = new Button();
deleteItemButton.AddToClassList(EditorGUIUtility.isProSkin ? "deleteDarkTheme" : "delete");
deleteItemButton.AddToClassList("name-and-parameters-list-foldout-button");
deleteItemButton.clicked += buttonProperties.onDelete;
header.Add(moveItemUpButton);
header.Add(moveItemDownButton);
header.Add(deleteItemButton);
var foldout = container.Q<Foldout>("Foldout");
foldout.text = parameterListView.name;
parameterListView.OnDrawVisualElements(foldout);
foldout.Add(new IMGUIContainer(parameterListView.OnGUI));
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 11330b6eb30fdb54fa20d0da9f0d60a5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,193 @@
#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
using System;
using System.Linq;
using UnityEditor;
using UnityEngine.UIElements;
namespace UnityEngine.InputSystem.Editor
{
internal class PropertiesView : ViewBase<PropertiesView.ViewState>
{
private ActionPropertiesView m_ActionPropertyView;
private BindingPropertiesView m_BindingPropertyView;
private NameAndParametersListView m_InteractionsListView;
private NameAndParametersListView m_ProcessorsListView;
private Foldout interactionsFoldout => rootElement.Q<Foldout>("interactions-foldout");
private Foldout processorsFoldout => rootElement.Q<Foldout>("processors-foldout");
private TextElement addInteractionButton;
private TextElement addProcessorButton;
public PropertiesView(VisualElement root, StateContainer stateContainer)
: base(root, stateContainer)
{
CreateSelector(
Selectors.GetSelectedAction,
Selectors.GetSelectedBinding,
state => state.selectionType,
(inputAction, inputBinding, selectionType, s) => new ViewState()
{
selectionType = selectionType,
serializedInputAction = inputAction,
inputBinding = inputBinding,
relatedInputAction = Selectors.GetRelatedInputAction(s)
});
var interactionsToggle = interactionsFoldout.Q<Toggle>();
interactionsToggle.AddToClassList("properties-foldout-toggle");
if (addInteractionButton == null)
{
addInteractionButton = CreateAddButton(interactionsToggle, "add-new-interaction-button");
new ContextualMenuManipulator(_ => {}){target = addInteractionButton, activators = {new ManipulatorActivationFilter(){button = MouseButton.LeftMouse}}};
}
var processorToggle = processorsFoldout.Q<Toggle>();
processorToggle.AddToClassList("properties-foldout-toggle");
if (addProcessorButton == null)
{
addProcessorButton = CreateAddButton(processorToggle, "add-new-processor-button");
new ContextualMenuManipulator(_ => {}){target = addProcessorButton, activators = {new ManipulatorActivationFilter(){button = MouseButton.LeftMouse}}};
}
}
private TextElement CreateAddButton(Toggle toggle, string name)
{
var addButton = new Button();
addButton.text = "+";
addButton.name = name;
addButton.focusable = false;
#if UNITY_EDITOR_OSX
addButton.clickable.activators.Clear();
#endif
addButton.AddToClassList("add-interaction-processor-button");
toggle.Add(addButton);
return addButton;
}
private void CreateContextMenuProcessor(string expectedControlType)
{
var processors = InputProcessor.s_Processors;
Type expectedValueType = string.IsNullOrEmpty(expectedControlType) ? null : EditorInputControlLayoutCache.GetValueType(expectedControlType);
addProcessorButton.RegisterCallback<ContextualMenuPopulateEvent>(evt =>
{
evt.menu.ClearItems();
foreach (var name in processors.internedNames.Where(x => !processors.ShouldHideInUI(x)).OrderBy(x => x.ToString()))
{
// Skip if not compatible with value type.
if (!IsValidProcessorForControl(expectedValueType, name))
continue;
var niceName = ObjectNames.NicifyVariableName(name);
evt.menu.AppendAction(niceName, _ => m_ProcessorsListView.OnAddElement(name.ToString()));
}
});
}
private bool IsValidProcessorForControl(Type expectedValueType, string name)
{
if (expectedValueType == null) return true;
var type = InputProcessor.s_Processors.LookupTypeRegistration(name);
var valueType = InputProcessor.GetValueTypeFromType(type);
if (valueType != null && !expectedValueType.IsAssignableFrom(valueType))
return false;
return true;
}
private void CreateContextMenuInteraction(string expectedControlType)
{
var interactions = InputInteraction.s_Interactions;
Type expectedValueType = string.IsNullOrEmpty(expectedControlType) ? null : EditorInputControlLayoutCache.GetValueType(expectedControlType);
addInteractionButton.RegisterCallback<ContextualMenuPopulateEvent>(evt =>
{
evt.menu.ClearItems();
foreach (var name in interactions.internedNames.Where(x => !interactions.ShouldHideInUI(x)).OrderBy(x => x.ToString()))
{
// Skip if not compatible with value type.
if (!IsValidInteractionForControl(expectedValueType, name))
continue;
var niceName = ObjectNames.NicifyVariableName(name);
evt.menu.AppendAction(niceName, _ => m_InteractionsListView.OnAddElement(name.ToString()));
}
});
}
private bool IsValidInteractionForControl(Type expectedValueType, string name)
{
if (expectedValueType == null) return true;
var type = InputInteraction.s_Interactions.LookupTypeRegistration(name);
var valueType = InputInteraction.GetValueType(type);
if (valueType != null && !expectedValueType.IsAssignableFrom(valueType))
return false;
return true;
}
public override void RedrawUI(ViewState viewState)
{
DestroyChildView(m_ActionPropertyView);
DestroyChildView(m_BindingPropertyView);
DestroyChildView(m_InteractionsListView);
DestroyChildView(m_ProcessorsListView);
var propertiesContainer = rootElement.Q<VisualElement>("properties-container");
var foldout = propertiesContainer.Q<Foldout>("properties-foldout");
foldout.Clear();
var visualElement = new VisualElement();
foldout.Add(visualElement);
foldout.Q<Toggle>().AddToClassList("properties-foldout-toggle");
var inputAction = viewState.serializedInputAction;
var inputActionOrBinding = inputAction?.wrappedProperty;
switch (viewState.selectionType)
{
case SelectionType.Action:
rootElement.Q<Label>("properties-header-label").text = "Action Properties";
m_ActionPropertyView = CreateChildView(new ActionPropertiesView(visualElement, foldout, stateContainer));
break;
case SelectionType.Binding:
rootElement.Q<Label>("properties-header-label").text = "Binding Properties";
m_BindingPropertyView = CreateChildView(new BindingPropertiesView(visualElement, foldout, stateContainer));
inputAction = viewState.relatedInputAction;
inputActionOrBinding = viewState.inputBinding?.wrappedProperty;
break;
}
CreateContextMenuProcessor(inputAction?.expectedControlType);
CreateContextMenuInteraction(inputAction?.expectedControlType);
var isPartOfComposite = viewState.selectionType == SelectionType.Binding &&
viewState.inputBinding?.isPartOfComposite == true;
//don't show for Bindings in Composites
if (!isPartOfComposite)
{
interactionsFoldout.style.display = DisplayStyle.Flex;
m_InteractionsListView = CreateChildView(new NameAndParametersListView(
interactionsFoldout,
stateContainer,
inputActionOrBinding?.FindPropertyRelative(nameof(InputAction.m_Interactions)),
state => Selectors.GetInteractionsAsParameterListViews(state, inputAction)));
}
else
interactionsFoldout.style.display = DisplayStyle.None;
m_ProcessorsListView = CreateChildView(new NameAndParametersListView(
processorsFoldout,
stateContainer,
inputActionOrBinding?.FindPropertyRelative(nameof(InputAction.m_Processors)),
state => Selectors.GetProcessorsAsParameterListViews(state, inputAction)));
}
internal class ViewState
{
public SerializedInputAction? relatedInputAction;
public SerializedInputBinding? inputBinding;
public SerializedInputAction? serializedInputAction;
public SelectionType selectionType;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8879a5884abf42c4c99aba5009bc081f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,377 @@
#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine.InputSystem.Controls;
using UnityEngine.InputSystem.Editor.Lists;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.Utilities;
namespace UnityEngine.InputSystem.Editor
{
internal static partial class Selectors
{
public static IEnumerable<string> GetActionMapNames(InputActionsEditorState state)
{
return state.serializedObject
?.FindProperty(nameof(InputActionAsset.m_ActionMaps))
?.Select(m => m.FindPropertyRelative(nameof(InputActionMap.m_Name))?.stringValue)
?? Enumerable.Empty<string>();
}
public static SerializedProperty GetActionMapForAction(InputActionsEditorState state, string id)
{
return state.serializedObject?.FindProperty(nameof(InputActionAsset.m_ActionMaps)) ?
.FirstOrDefault(map => map.FindPropertyRelative("m_Actions")
.Select(a => a.FindPropertyRelative("m_Id").stringValue)
.Contains(id));
}
public static IEnumerable<SerializedInputAction> GetActionsForSelectedActionMap(InputActionsEditorState state)
{
var actionMap = GetActionMapAtIndex(state, state.selectedActionMapIndex);
if (!actionMap.HasValue)
return Enumerable.Empty<SerializedInputAction>();
return actionMap.Value.wrappedProperty
.FindPropertyRelative(nameof(InputActionMap.m_Actions))
.Select(serializedProperty => new SerializedInputAction(serializedProperty));
}
public static SerializedInputActionMap? GetSelectedActionMap(InputActionsEditorState state)
{
return GetActionMapAtIndex(state, state.selectedActionMapIndex);
}
public static SerializedInputActionMap? GetActionMapAtIndex(InputActionsEditorState state, int index)
{
return GetActionMapAtIndex(state.serializedObject, index);
}
public static SerializedInputActionMap? GetActionMapAtIndex(SerializedObject serializedObject, int index)
{
var actionMaps = serializedObject?.FindProperty(nameof(InputActionAsset.m_ActionMaps));
if (actionMaps == null || index < 0 || index >= actionMaps.arraySize)
return null;
return new SerializedInputActionMap(actionMaps.GetArrayElementAtIndex(index));
}
public static int GetActionMapIndexFromId(SerializedObject serializedObject, Guid id)
{
Debug.Assert(serializedObject.targetObject is InputActionAsset);
return InputActionSerializationHelpers.GetIndex(
serializedObject.FindProperty(nameof(InputActionAsset.m_ActionMaps)), id);
}
public static int GetActionIndexFromId(SerializedProperty actionMapProperty, Guid id)
{
return InputActionSerializationHelpers.GetIndex(
actionMapProperty.FindPropertyRelative(nameof(InputActionMap.m_Actions)), id);
}
public static int? GetBindingCount(SerializedProperty actionMap)
{
return actionMap?.FindPropertyRelative(nameof(InputActionMap.m_Bindings))?.arraySize;
}
public static List<SerializedProperty> GetBindingsForAction(string actionName, InputActionsEditorState state)
{
var actionMap = GetSelectedActionMap(state);
var bindingsOfAction = actionMap?.wrappedProperty.FindPropertyRelative(nameof(InputActionMap.m_Bindings))
.Where(b => b.FindPropertyRelative("m_Action").stringValue == actionName).ToList();
return bindingsOfAction;
}
public static List<SerializedProperty> GetBindingsForAction(InputActionsEditorState state, SerializedProperty actionMap, int actionIndex)
{
var action = GetActionForIndex(actionMap, actionIndex);
return GetBindingsForAction(action.FindPropertyRelative(nameof(InputAction.m_Name)).stringValue, state);
}
public static int GetLastBindingIndexForSelectedAction(InputActionsEditorState state)
{
var actionName = GetSelectedAction(state)?.wrappedProperty.FindPropertyRelative("m_Name").stringValue;
var bindingsOfAction = GetBindingsForAction(actionName, state);
return bindingsOfAction.Count > 0 ? bindingsOfAction.Select(b => b.GetIndexOfArrayElement()).Max() : 0;
}
public static int GetSelectedBindingIndexAfterCompositeBindings(InputActionsEditorState state)
{
var bindings = GetSelectedActionMap(state)?.wrappedProperty.FindPropertyRelative(nameof(InputActionMap.m_Bindings));
var item = new SerializedInputBinding(bindings?.GetArrayElementAtIndex(state.selectedBindingIndex));
var index = state.selectedBindingIndex + (item.isComposite || item.isPartOfComposite ? 1 : 0);
var toSkip = 0;
while (index < bindings.arraySize && new SerializedInputBinding(bindings?.GetArrayElementAtIndex(index)).isPartOfComposite)
{
toSkip++;
index++;
}
return state.selectedBindingIndex + toSkip;
}
public static int GetBindingIndexBeforeAction(SerializedProperty arrayProperty, int indexToInsert, SerializedProperty bindingArrayToInsertTo)
{
// Need to guard against this case, as there is different behaviour when pasting actions vs actionmaps
if (indexToInsert < 0)
{
return -1;
}
Debug.Assert(indexToInsert <= arrayProperty.arraySize, "Invalid action index to insert bindings before.");
var offset = 1; //previous action offset
while (indexToInsert - offset >= 0)
{
var prevActionName = arrayProperty.GetArrayElementAtIndex(indexToInsert - offset).FindPropertyRelative("m_Name").stringValue;
var lastBindingOfAction = bindingArrayToInsertTo.FindLast(b => b.FindPropertyRelative("m_Action").stringValue.Equals(prevActionName));
if (lastBindingOfAction != null) //if action has no bindings lastBindingOfAction will be null
return lastBindingOfAction.GetIndexOfArrayElement() + 1;
offset++;
}
return -1; //no actions with bindings before paste index
}
public static int? GetActionCount(SerializedProperty actionMap)
{
return actionMap?.FindPropertyRelative(nameof(InputActionMap.m_Actions))?.arraySize;
}
public static int GetActionMapCount(SerializedObject serializedObject)
{
return serializedObject == null ? 0 : serializedObject.FindProperty(nameof(InputActionAsset.m_ActionMaps)).arraySize;
}
public static int? GetActionMapCount(InputActionsEditorState state)
{
return state.serializedObject?.FindProperty(nameof(InputActionAsset.m_ActionMaps))?.arraySize;
}
public static SerializedProperty GetActionForIndex(SerializedProperty actionMap, int actionIndex)
{
return actionMap.FindPropertyRelative(nameof(InputActionMap.m_Actions)).GetArrayElementAtIndex(actionIndex);
}
public static SerializedInputAction GetActionInMap(InputActionsEditorState state, int mapIndex, string name)
{
return new SerializedInputAction(state.serializedObject
?.FindProperty(nameof(InputActionAsset.m_ActionMaps))?.GetArrayElementAtIndex(mapIndex)
?.FindPropertyRelative(nameof(InputActionMap.m_Actions))
?.FirstOrDefault(p => p.FindPropertyRelative(nameof(InputAction.m_Name)).stringValue == name));
}
public static SerializedInputBinding GetCompositeOrBindingInMap(SerializedProperty actionMap, int bindingIndex)
{
return new SerializedInputBinding(actionMap
?.FindPropertyRelative(nameof(InputActionMap.m_Bindings))
?.GetArrayElementAtIndex(bindingIndex));
}
public static SerializedProperty GetBindingForId(InputActionsEditorState state, string id, out SerializedProperty bindingArray)
{
return GetBindingForId(state.serializedObject, id, out bindingArray);
}
public static SerializedProperty GetBindingForId(SerializedObject serializedObject, string id, out SerializedProperty bindingArray)
{
var actionMaps = serializedObject?.FindProperty(nameof(InputActionAsset.m_ActionMaps));
for (int i = 0; i < actionMaps?.arraySize; i++)
{
var bindings = actionMaps.GetArrayElementAtIndex(i).FindPropertyRelative(nameof(InputActionMap.m_Bindings));
for (int j = 0; j < bindings.arraySize; j++)
{
if (bindings.GetArrayElementAtIndex(j).FindPropertyRelative("m_Id").stringValue != id)
continue;
bindingArray = bindings;
return bindings.GetArrayElementAtIndex(j);
}
}
bindingArray = null;
return null;
}
public static SerializedProperty GetSelectedBindingPath(InputActionsEditorState state)
{
var selectedBinding = GetSelectedBinding(state);
return selectedBinding?.wrappedProperty.FindPropertyRelative("m_Path");
}
public static SerializedInputBinding? GetSelectedBinding(InputActionsEditorState state)
{
var actionMapSO = GetActionMapAtIndex(state, state.selectedActionMapIndex);
var bindings = actionMapSO?.wrappedProperty.FindPropertyRelative(nameof(InputActionMap.m_Bindings));
if (bindings == null || bindings.arraySize - 1 < state.selectedBindingIndex || state.selectedBindingIndex < 0)
return null;
return new SerializedInputBinding(bindings.GetArrayElementAtIndex(state.selectedBindingIndex));
}
public static SerializedInputAction? GetRelatedInputAction(InputActionsEditorState state)
{
var binding = GetSelectedBinding(state);
if (binding == null)
return null;
var actionName = binding.Value.wrappedProperty.FindPropertyRelative("m_Action").stringValue;
return GetActionInMap(state, state.selectedActionMapIndex, actionName);
}
public static IEnumerable<string> GetCompositeTypes(string path, string expectedControlLayout)
{
// Find name of current composite.
var compositeNameAndParameters = NameAndParameters.Parse(path);
var compositeName = compositeNameAndParameters.name;
var compositeType = InputBindingComposite.s_Composites.LookupTypeRegistration(compositeName);
// Collect all possible composite types.
var selectedCompositeIndex = -1;
var currentIndex = 0;
foreach (var composite in InputBindingComposite.s_Composites.internedNames.Where(x =>
!InputBindingComposite.s_Composites.aliases.Contains(x)).OrderBy(x => x))
{
if (!string.IsNullOrEmpty(expectedControlLayout))
{
var valueType = InputBindingComposite.GetValueType(composite);
if (valueType != null &&
!InputControlLayout.s_Layouts.ValueTypeIsAssignableFrom(
new InternedString(expectedControlLayout), valueType))
continue;
}
if (InputBindingComposite.s_Composites.LookupTypeRegistration(composite) == compositeType)
selectedCompositeIndex = currentIndex;
yield return composite;
++currentIndex;
}
// If the current composite type isn't a registered type, add it to the list as
// an extra option.
if (selectedCompositeIndex == -1)
yield return compositeName;
}
public static IEnumerable<string> GetCompositePartOptions(string bindingName, string compositeName)
{
var currentIndex = 0;
var selectedPartNameIndex = -1;
foreach (var partName in InputBindingComposite.GetPartNames(compositeName))
{
if (partName.Equals(bindingName, StringComparison.OrdinalIgnoreCase))
selectedPartNameIndex = currentIndex;
yield return partName;
++currentIndex;
}
// If currently selected part is not in list, add it as an option.
if (selectedPartNameIndex == -1)
yield return bindingName;
}
public static SerializedInputAction? GetSelectedAction(InputActionsEditorState state)
{
var actions = GetActionMapAtIndex(state, state.selectedActionMapIndex)
?.wrappedProperty.FindPropertyRelative(nameof(InputActionMap.m_Actions));
if (actions == null || actions.arraySize - 1 < state.selectedActionIndex || state.selectedActionIndex < 0)
return null;
// If we've currently selected a binding, get the parent input action for it.
if (state.selectionType == SelectionType.Binding)
return GetRelatedInputAction(state);
return new SerializedInputAction(actions.GetArrayElementAtIndex(state.selectedActionIndex));
}
public static IEnumerable<string> BuildControlTypeList(InputActionType selectedActionType)
{
var allLayouts = InputSystem.s_Manager.m_Layouts;
// "Any" is always in first position (index 0)
yield return "Any";
foreach (var layoutName in allLayouts.layoutTypes.Keys)
{
if (EditorInputControlLayoutCache.TryGetLayout(layoutName).hideInUI)
continue;
// If the action type is InputActionType.Value, skip button controls.
var type = allLayouts.layoutTypes[layoutName];
if (selectedActionType == InputActionType.Value && typeof(ButtonControl).IsAssignableFrom(type))
continue;
////TODO: skip aliases
if (typeof(InputControl).IsAssignableFrom(type) && !typeof(InputDevice).IsAssignableFrom(type))
yield return layoutName;
}
}
public static IEnumerable<ParameterListView> GetInteractionsAsParameterListViews(InputActionsEditorState state, SerializedInputAction? inputAction)
{
Type expectedValueType = null;
if (inputAction.HasValue && !string.IsNullOrEmpty(inputAction.Value.expectedControlType))
expectedValueType = EditorInputControlLayoutCache.GetValueType(inputAction.Value.expectedControlType);
var interactions = string.Empty;
if (inputAction.HasValue && state.selectionType == SelectionType.Action)
interactions = inputAction.Value.interactions;
else if (state.selectionType == SelectionType.Binding && GetSelectedBinding(state).HasValue)
interactions = GetSelectedBinding(state)?.interactions;
return CreateParameterListViews(
interactions,
expectedValueType,
InputInteraction.s_Interactions.LookupTypeRegistration,
InputInteraction.GetValueType);
}
public static IEnumerable<ParameterListView> GetProcessorsAsParameterListViews(InputActionsEditorState state, SerializedInputAction? inputAction)
{
var processors = string.Empty;
Type expectedValueType = null;
if (inputAction.HasValue && !string.IsNullOrEmpty(inputAction.Value.expectedControlType))
expectedValueType = EditorInputControlLayoutCache.GetValueType(inputAction.Value.expectedControlType);
if (inputAction.HasValue && state.selectionType == SelectionType.Action)
processors = inputAction.Value.processors;
else if (state.selectionType == SelectionType.Binding && GetSelectedBinding(state).HasValue)
processors = GetSelectedBinding(state)?.processors;
return CreateParameterListViews(
processors,
expectedValueType,
InputProcessor.s_Processors.LookupTypeRegistration,
InputProcessor.GetValueTypeFromType);
}
private static IEnumerable<ParameterListView> CreateParameterListViews(string interactions, Type expectedValueType,
Func<string, Type> typeLookup, Func<Type, Type> getGenericArgumentType)
{
return NameAndParameters.ParseMultiple(interactions)
.Select(p => (interaction: p, rowType: typeLookup(p.name)))
.Select(t =>
{
var(parameter, rowType) = t;
var parameterListView = new ParameterListView();
parameterListView.Initialize(rowType, parameter.parameters);
parameterListView.name = ObjectNames.NicifyVariableName(parameter.name);
if (rowType == null)
{
parameterListView.name += " (Obsolete)";
}
else if (expectedValueType != null)
{
var valueType = getGenericArgumentType(rowType);
if (valueType != null && !expectedValueType.IsAssignableFrom(valueType))
parameterListView.name += " (Incompatible Value Type)";
}
return parameterListView;
});
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b98fcf53d54c1fe4abdd9327cf57a371
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,298 @@
#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
using System;
using System.Collections.Generic;
using UnityEngine.UIElements;
namespace UnityEngine.InputSystem.Editor
{
internal interface IViewStateSelector<out TViewState>
{
bool HasStateChanged(InputActionsEditorState state);
TViewState GetViewState(InputActionsEditorState state);
}
internal interface IView
{
void UpdateView(InputActionsEditorState state);
void DestroyView();
}
internal abstract class ViewBase<TViewState> : IView
{
protected ViewBase(VisualElement root, StateContainer stateContainer)
{
this.rootElement = root;
this.stateContainer = stateContainer;
m_ChildViews = new List<IView>();
}
protected void OnStateChanged(InputActionsEditorState state, UIRebuildMode editorRebuildMode)
{
// Return early if rebuilding the editor UI isn't required (ISXB-1171)
if (editorRebuildMode == UIRebuildMode.None)
return;
UpdateView(state);
}
public void UpdateView(InputActionsEditorState state)
{
if (m_ViewStateSelector == null)
{
Debug.LogWarning(
$"View '{GetType().Name}' has no selector and will not render. Create a selector for the " +
$"view using the CreateSelector method.");
return;
}
if (m_ViewStateSelector.HasStateChanged(state) || m_IsFirstUpdate)
RedrawUI(m_ViewStateSelector.GetViewState(state));
m_IsFirstUpdate = false;
foreach (var view in m_ChildViews)
{
view.UpdateView(state);
}
// We can execute UI Commands now that the UI is fully updated
// NOTE: This isn't used with Input Commands
allowUICommandExecution = true;
}
public TView CreateChildView<TView>(TView view) where TView : IView
{
m_ChildViews.Add(view);
return view;
}
public void DestroyChildView<TView>(TView view) where TView : IView
{
if (view == null)
return;
m_ChildViews.Remove(view);
view.DestroyView();
}
public void Dispatch(Command command, UIRebuildMode editorRebuildMode = UIRebuildMode.Rebuild)
{
stateContainer.Dispatch(command, editorRebuildMode);
}
public abstract void RedrawUI(TViewState viewState);
/// <summary>
/// Called when a parent view is destroying this view to give it an opportunity to clean up any
/// resources or event handlers.
/// </summary>
public virtual void DestroyView()
{
}
protected void CreateSelector(Func<InputActionsEditorState, TViewState> selector)
{
m_ViewStateSelector = new ViewStateSelector<TViewState>(selector);
}
protected void CreateSelector<T1>(
Func<InputActionsEditorState, T1> func1,
Func<T1, InputActionsEditorState, TViewState> selector)
{
m_ViewStateSelector = new ViewStateSelector<T1, TViewState>(func1, selector);
}
protected void CreateSelector<T1, T2>(
Func<InputActionsEditorState, T1> func1,
Func<InputActionsEditorState, T2> func2,
Func<T1, T2, InputActionsEditorState, TViewState> selector)
{
m_ViewStateSelector = new ViewStateSelector<T1, T2, TViewState>(func1, func2, selector);
}
protected void CreateSelector<T1, T2, T3>(
Func<InputActionsEditorState, T1> func1,
Func<InputActionsEditorState, T2> func2,
Func<InputActionsEditorState, T3> func3,
Func<T1, T2, T3, InputActionsEditorState, TViewState> selector)
{
m_ViewStateSelector = new ViewStateSelector<T1, T2, T3, TViewState>(func1, func2, func3, selector);
}
protected readonly VisualElement rootElement;
protected readonly StateContainer stateContainer;
protected bool allowUICommandExecution { get; set; } = true;
protected IViewStateSelector<TViewState> ViewStateSelector => m_ViewStateSelector;
private IViewStateSelector<TViewState> m_ViewStateSelector;
private IList<IView> m_ChildViews;
private bool m_IsFirstUpdate = true;
}
internal class ViewStateSelector<TReturn> : IViewStateSelector<TReturn>
{
private readonly Func<InputActionsEditorState, TReturn> m_Selector;
public ViewStateSelector(Func<InputActionsEditorState, TReturn> selector)
{
m_Selector = selector;
}
public bool HasStateChanged(InputActionsEditorState state)
{
return true;
}
public TReturn GetViewState(InputActionsEditorState state)
{
return m_Selector(state);
}
}
// TODO: Make all args to view state selectors IEquatable<T>?
internal class ViewStateSelector<T1, TReturn> : IViewStateSelector<TReturn>
{
private readonly Func<InputActionsEditorState, T1> m_Func1;
private readonly Func<T1, InputActionsEditorState, TReturn> m_Selector;
private T1 m_PreviousT1;
public ViewStateSelector(Func<InputActionsEditorState, T1> func1,
Func<T1, InputActionsEditorState, TReturn> selector)
{
m_Func1 = func1;
m_Selector = selector;
}
public bool HasStateChanged(InputActionsEditorState state)
{
var valueOne = m_Func1(state);
if (valueOne is IViewStateCollection collection)
{
if (collection.SequenceEqual((IViewStateCollection)m_PreviousT1))
return false;
}
else if (valueOne.Equals(m_PreviousT1))
{
return false;
}
m_PreviousT1 = valueOne;
return true;
}
public TReturn GetViewState(InputActionsEditorState state)
{
return m_Selector(m_PreviousT1, state);
}
}
internal class ViewStateSelector<T1, T2, TReturn> : IViewStateSelector<TReturn>
{
private readonly Func<InputActionsEditorState, T1> m_Func1;
private readonly Func<InputActionsEditorState, T2> m_Func2;
private readonly Func<T1, T2, InputActionsEditorState, TReturn> m_Selector;
private T1 m_PreviousT1;
private T2 m_PreviousT2;
public ViewStateSelector(Func<InputActionsEditorState, T1> func1,
Func<InputActionsEditorState, T2> func2,
Func<T1, T2, InputActionsEditorState, TReturn> selector)
{
m_Func1 = func1;
m_Func2 = func2;
m_Selector = selector;
}
public bool HasStateChanged(InputActionsEditorState state)
{
var valueOne = m_Func1(state);
var valueTwo = m_Func2(state);
var valueOneHasChanged = false;
var valueTwoHasChanged = false;
if (valueOne is IViewStateCollection collection && !collection.SequenceEqual((IViewStateCollection)m_PreviousT1) ||
!valueOne.Equals(m_PreviousT1))
valueOneHasChanged = true;
if (valueTwo is IViewStateCollection collection2 && !collection2.SequenceEqual((IViewStateCollection)m_PreviousT2) ||
!valueTwo.Equals(m_PreviousT2))
valueTwoHasChanged = true;
if (!valueOneHasChanged && !valueTwoHasChanged)
return false;
m_PreviousT1 = valueOne;
m_PreviousT2 = valueTwo;
return true;
}
public TReturn GetViewState(InputActionsEditorState state)
{
return m_Selector(m_PreviousT1, m_PreviousT2, state);
}
}
internal class ViewStateSelector<T1, T2, T3, TReturn> : IViewStateSelector<TReturn>
{
private readonly Func<InputActionsEditorState, T1> m_Func1;
private readonly Func<InputActionsEditorState, T2> m_Func2;
private readonly Func<InputActionsEditorState, T3> m_Func3;
private readonly Func<T1, T2, T3, InputActionsEditorState, TReturn> m_Selector;
private T1 m_PreviousT1;
private T2 m_PreviousT2;
private T3 m_PreviousT3;
public ViewStateSelector(Func<InputActionsEditorState, T1> func1,
Func<InputActionsEditorState, T2> func2,
Func<InputActionsEditorState, T3> func3,
Func<T1, T2, T3, InputActionsEditorState, TReturn> selector)
{
m_Func1 = func1;
m_Func2 = func2;
m_Func3 = func3;
m_Selector = selector;
}
public bool HasStateChanged(InputActionsEditorState state)
{
var valueOne = m_Func1(state);
var valueTwo = m_Func2(state);
var valueThree = m_Func3(state);
var valueOneHasChanged = false;
var valueTwoHasChanged = false;
var valueThreeHasChanged = false;
if (valueOne is IViewStateCollection collection && !collection.SequenceEqual((IViewStateCollection)m_PreviousT1) ||
!valueOne.Equals(m_PreviousT1))
valueOneHasChanged = true;
if (valueTwo is IViewStateCollection collection2 && !collection2.SequenceEqual((IViewStateCollection)m_PreviousT2) ||
!valueTwo.Equals(m_PreviousT2))
valueTwoHasChanged = true;
if (valueThree is IViewStateCollection collection3 && !collection3.SequenceEqual((IViewStateCollection)m_PreviousT3) ||
!valueThree.Equals(m_PreviousT3))
valueThreeHasChanged = true;
if (!valueOneHasChanged && !valueTwoHasChanged && !valueThreeHasChanged)
return false;
m_PreviousT1 = valueOne;
m_PreviousT2 = valueTwo;
m_PreviousT3 = valueThree;
return true;
}
public TReturn GetViewState(InputActionsEditorState state)
{
return m_Selector(m_PreviousT1, m_PreviousT2, m_PreviousT3, state);
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 900c2de771722c542b5857931181ab2f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,58 @@
#if UNITY_EDITOR
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace UnityEngine.InputSystem.Editor
{
/// <summary>
/// A caching enumerator that will save all enumerated values on the first iteration, and when
/// subsequently iterated, return the saved values instead.
/// </summary>
/// <typeparam name="T"></typeparam>
internal class ViewStateCollection<T> : IViewStateCollection, IEnumerable<T>
{
private readonly IEnumerable<T> m_Collection;
private readonly IEqualityComparer<T> m_Comparer;
private IList<T> m_CachedCollection;
public ViewStateCollection(IEnumerable<T> collection, IEqualityComparer<T> comparer = null)
{
m_Collection = collection;
m_Comparer = comparer;
}
public bool SequenceEqual(IViewStateCollection other)
{
return other is ViewStateCollection<T> otherCollection && this.SequenceEqual(otherCollection, m_Comparer);
}
public IEnumerator<T> GetEnumerator()
{
if (m_CachedCollection == null)
{
m_CachedCollection = new List<T>();
using (var enumerator = m_Collection.GetEnumerator())
{
while (enumerator.MoveNext())
{
m_CachedCollection.Add(enumerator.Current);
}
}
}
using (var enumerator = m_CachedCollection.GetEnumerator())
{
while (enumerator.MoveNext())
yield return enumerator.Current;
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cdc588cfee2e9a84a9d748fb7314daff
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,22 @@
#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
using System;
using UnityEngine.UIElements;
namespace UnityEngine.InputSystem.Editor
{
internal static class VisualElementExtensions
{
public static TElement Q<TElement>(this VisualElement visualElement, string name) where TElement : VisualElement
{
var element = UQueryExtensions.Q<TElement>(visualElement, name);
if (element == null)
throw new InvalidOperationException(
$"Expected a visual element called '{name}' of type '{typeof(TElement)}' to exist " +
$"but none was found.");
return element;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 09dbcfc6e83454e45ace8503dacc86ad
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: