Initial commit
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 665321577aaff5344b5a12d626dfcaf7
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 60e5ec46fb7a7ff41856f37c60efb70f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 99cfd7e6e4fd7294090b6bf84b177326
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 255ca660c2e3b424bb54d7be66737f57
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 35e258ef8ccb13146952199468118233
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 36827be967eb9244b973cbee1bbf53e1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 18d3e83390dcd6349b3086b9f70391b9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7a8e0b5c429811847b8c97a058e5884f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e68462a09b6ba104abce11d2d157d0b2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3b8e35f20292579409c899395e30948f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7894838fa258e3a4197077ddbda0eec5
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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('project://database/Packages/com.unity.inputsystem/InputSystem/Editor/Icons/d_InputControl.png?fileID=2800000&guid=399cd90f4e31041e692a7d3a8b1aa4d0&type=3#d_InputControl'); 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>
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 449c8e5ca27af8e4a9db710c8be5e4df
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2bd2d0548382d58489e8c2999daf4139
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: acfae125fa5d7904987a696b3993d432
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 303ef5b13740f9a4db252c50122d2c62
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
||||
@@ -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&guid=7dac9c49a90bca4499371d0adc9b617b&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>
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3fa44185ed614414ebab354dfe5a06b6
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
||||
@@ -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&guid=7dac9c49a90bca4499371d0adc9b617b&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>
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c662e0d3cc3c4f948808f9847f715ef2
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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&guid=7dac9c49a90bca4499371d0adc9b617b&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. 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>
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 75a33987e21d5ac449deecea3dcd864f
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
||||
@@ -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&guid=7dac9c49a90bca4499371d0adc9b617b&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>
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 452df07a8ad0c944c879a484d5890e61
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
||||
@@ -0,0 +1,4 @@
|
||||
:root {
|
||||
--input-editor-colors-properties-foldout: rgb(34, 34, 34);
|
||||
--input-editor-colors-properties-foldout-border: #808080;
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,4 @@
|
||||
:root {
|
||||
--input-editor-colors-properties-foldout: #DFDFDF;
|
||||
--input-editor-colors-properties-foldout-border: #808080;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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&guid=7dac9c49a90bca4499371d0adc9b617b&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>
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9cbd73d51dc491b4888055ae65cd9545
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a6f2f33363708814e8530579e2d8cf2d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fcffff3de13f4304a98bb50b1222399a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d92ba71f7bef7074eb5d8753a9d09521
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2084fa9039e3711438d53087a63525f7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8b19af954eaf68841a157dc58e7e4814
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2db76e4ad9537964994bd20263e1c379
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0226840818c8be84a9d8774a2529fe86
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 954f8631d67512b409a5c4c526ae4f71
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 716898219fc23de4a81300652df80ef3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9c9510ff4a99bf84c9277bf2182a67d5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 36e7567d9504499a8bbe97595096de5e
|
||||
timeCreated: 1695966613
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2bd61b5c763f7444e9a5aa3c7623f60b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1d3813199fcb56b45b916ac51d58dc80
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 989e6ef705a83455c81a5d0a22910406
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 66ed65ecd8caca54ba814d536ed4d5b2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d384a7a7180814f16aa8fa9d86ac3b6c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b52a4b3b22334b02a4d22d942a479be6
|
||||
timeCreated: 1691762301
|
||||
@@ -0,0 +1,12 @@
|
||||
#if UNITY_EDITOR
|
||||
using System.Collections;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
internal interface IViewStateCollection : IEnumerable
|
||||
{
|
||||
bool SequenceEqual(IViewStateCollection other);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 49a488c9c543d82408e3f15c90d36980
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8b82aecdbfbdd1b49b90eb0d509b4166
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 392e37dde5bf4a14bb315f2b4829bd6b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a439a21d627cd7646978dd78132c4bb9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 890eded7b7fc53b489fcc92f0aeb774e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 11330b6eb30fdb54fa20d0da9f0d60a5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8879a5884abf42c4c99aba5009bc081f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b98fcf53d54c1fe4abdd9327cf57a371
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 900c2de771722c542b5857931181ab2f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cdc588cfee2e9a84a9d748fb7314daff
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 09dbcfc6e83454e45ace8503dacc86ad
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user