UI 자동화를 위해 바인딩 기능 구현

- 유니티 에셋 인증 오류로 meta 재생성
This commit is contained in:
2026-01-25 01:31:34 +09:00
parent 2ceb28f55d
commit ce83f21c93
1861 changed files with 377882 additions and 211 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,748 @@
#if UNITY_EDITOR
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)
{
// Items in the TreeView which were previously Bindings had input explicitly unregistered.
// Since the input field is normally registered on creation, when using RefreshItem rather than Rebuild
// it must be re-registered here.
treeViewItem.RegisterInputField();
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
{
// Items in the TreeView which were previously Bindings had input explicitly unregistered.
// Since the input field is normally registered on creation, when using RefreshItem rather than Rebuild
// it must be re-registered here.
treeViewItem.RegisterInputField();
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);
// 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_ActionsTreeView.RefreshItems();
#else
m_ActionsTreeView.Rebuild();
#endif
if (viewState.newElementID != -1)
{
m_ActionsTreeView.SetSelectionById(viewState.newElementID);
m_ActionsTreeView.ScrollToItemById(viewState.newElementID);
}
RenameNewAction(viewState.newElementID);;
m_AddActionButton.SetEnabled(viewState.actionMapCount > 0);
// Don't want to show action properties if there's no actions.
m_PropertiesScrollview.visible = m_ActionsTreeView.GetTreeCount() > 0;
}
private void OnDraggedItem(DragPerformEvent evt)
{
bool discardDrag = false;
foreach (var index in m_ActionsTreeView.selectedIndices)
{
// currentTarget & target are always in TreeView as the event is registered on the TreeView - we need to discard drags into other parts of the editor (e.g. the maps list view)
var treeView = m_ActionsTreeView.panel.Pick(evt.mousePosition)?.GetFirstAncestorOfType<TreeView>();
if (treeView is null || treeView != m_ActionsTreeView)
{
discardDrag = true;
break;
}
var draggedItemData = m_ActionsTreeView.GetItemDataForIndex<ActionOrBindingData>(index);
var itemID = m_ActionsTreeView.GetIdForIndex(index);
var childIndex = m_ActionsTreeView.viewController.GetChildIndexForId(itemID);
var parentId = m_ActionsTreeView.viewController.GetParentId(itemID);
ActionOrBindingData? directParent = parentId == -1 ? null : m_ActionsTreeView.GetItemDataForIndex<ActionOrBindingData>(m_ActionsTreeView.viewController.GetIndexForId(parentId));
if (draggedItemData.isAction)
{
if (!MoveAction(directParent, draggedItemData, childIndex))
{
discardDrag = true;
break;
}
}
else if (!draggedItemData.isPartOfComposite)
{
if (!MoveBindingOrComposite(directParent, draggedItemData, childIndex))
{
discardDrag = true;
break;
}
}
else if (!MoveCompositeParts(directParent, childIndex, draggedItemData))
{
discardDrag = true;
break;
}
}
if (!discardDrag) return;
var selectedItem = m_ActionsTreeView.GetItemDataForIndex<ActionOrBindingData>(m_ActionsTreeView.selectedIndices.First());
Dispatch(selectedItem.isAction
? Commands.SelectAction(selectedItem.name)
: Commands.SelectBinding(selectedItem.bindingIndex));
//TODO find a better way to reject the drag (for better visual feedback & to not run an extra command)
}
private bool MoveAction(ActionOrBindingData? directParent, ActionOrBindingData draggedItemData, int childIndex)
{
if (directParent != null)
return false;
Dispatch(Commands.MoveAction(draggedItemData.actionIndex, childIndex));
return true;
}
private bool MoveBindingOrComposite(ActionOrBindingData? directParent, ActionOrBindingData draggedItemData, int childIndex)
{
if (directParent == null || !directParent.Value.isAction)
return false;
if (draggedItemData.isComposite)
Dispatch(Commands.MoveComposite(draggedItemData.bindingIndex, directParent.Value.actionIndex, childIndex));
else
Dispatch(Commands.MoveBinding(draggedItemData.bindingIndex, directParent.Value.actionIndex, childIndex));
return true;
}
private bool MoveCompositeParts(ActionOrBindingData? directParent, int childIndex, ActionOrBindingData draggedItemData)
{
if (directParent == null || !directParent.Value.isComposite)
return false;
var newBindingIndex = directParent.Value.bindingIndex + childIndex + (directParent.Value.bindingIndex > draggedItemData.bindingIndex ? 0 : 1);
Dispatch(Commands.MovePartOfComposite(draggedItemData.bindingIndex, newBindingIndex, directParent.Value.bindingIndex));
return true;
}
private void RenameNewAction(int id)
{
if (!m_RenameOnActionAdded || id == -1)
return;
m_ActionsTreeView.ScrollToItemById(id);
var treeViewItem = m_ActionsTreeView.GetRootElementForId(id)?.Q<InputActionsTreeViewItem>();
treeViewItem?.FocusOnRenameTextField();
}
internal void RenameActionItem(int index)
{
m_ActionsTreeView.ScrollToItem(index);
m_ActionsTreeView.GetRootElementForIndex(index)?.Q<InputActionsTreeViewItem>()?.FocusOnRenameTextField();
}
internal void AddAction()
{
Dispatch(Commands.AddAction());
m_RenameOnActionAdded = true;
}
internal void AddBinding(int index)
{
Dispatch(Commands.SelectAction(m_ActionsTreeView.GetItemDataForIndex<ActionOrBindingData>(index).actionIndex));
Dispatch(Commands.AddBinding());
}
internal void AddComposite(int index, string compositeType)
{
Dispatch(Commands.SelectAction(m_ActionsTreeView.GetItemDataForIndex<ActionOrBindingData>(index).actionIndex));
Dispatch(Commands.AddComposite(compositeType));
}
internal void DeleteItem(int selectedIndex)
{
var data = m_ActionsTreeView.GetItemDataForIndex<ActionOrBindingData>(selectedIndex);
if (data.isAction)
Dispatch(Commands.DeleteAction(data.actionMapIndex, data.name));
else
Dispatch(Commands.DeleteBinding(data.actionMapIndex, data.bindingIndex));
// Deleting an item sometimes causes the UI Panel to lose focus; make sure we keep it
m_ActionsTreeView.Focus();
}
internal void DuplicateItem(int selectedIndex)
{
var data = m_ActionsTreeView.GetItemDataForIndex<ActionOrBindingData>(selectedIndex);
Dispatch(data.isAction ? Commands.DuplicateAction() : Commands.DuplicateBinding());
}
internal void CopyItems()
{
Dispatch(Commands.CopyActionBindingSelection());
}
internal void CutItems()
{
Dispatch(Commands.CutActionsOrBindings());
}
internal void PasteItems()
{
Dispatch(Commands.PasteActionsOrBindings(InputActionsEditorView.s_OnPasteCutElements));
}
private void ChangeActionOrCompositName(ActionOrBindingData data, string newName)
{
m_RenameOnActionAdded = false;
if (data.isAction)
Dispatch(Commands.ChangeActionName(data.actionMapIndex, data.name, newName));
else if (data.isComposite)
Dispatch(Commands.ChangeCompositeName(data.actionMapIndex, data.bindingIndex, newName));
}
internal int GetMapCount()
{
return m_ActionMapsListView.itemsSource.Count;
}
private void OnExecuteCommand(ExecuteCommandEvent evt)
{
if (m_ActionsTreeView.selectedItem == null)
return;
if (allowUICommandExecution)
{
var data = (ActionOrBindingData)m_ActionsTreeView.selectedItem;
switch (evt.commandName)
{
case CmdEvents.Rename:
if (data.isAction || data.isComposite)
RenameActionItem(m_ActionsTreeView.selectedIndex);
else
return;
break;
case CmdEvents.Delete:
case CmdEvents.SoftDelete:
DeleteItem(m_ActionsTreeView.selectedIndex);
break;
case CmdEvents.Duplicate:
DuplicateItem(m_ActionsTreeView.selectedIndex);
break;
case CmdEvents.Copy:
CopyItems();
break;
case CmdEvents.Cut:
CutItems();
break;
case CmdEvents.Paste:
var hasPastableData = CopyPasteHelper.HasPastableClipboardData(data.isAction ? typeof(InputAction) : typeof(InputBinding));
if (hasPastableData)
PasteItems();
break;
default:
return; // Skip StopPropagation if we didn't execute anything
}
// Prevent any UI commands from executing until after UI has been updated
allowUICommandExecution = false;
}
evt.StopPropagation();
}
private void OnValidateCommand(ValidateCommandEvent evt)
{
// Mark commands as supported for Execute by stopping propagation of the event
switch (evt.commandName)
{
case CmdEvents.Rename:
case CmdEvents.Delete:
case CmdEvents.SoftDelete:
case CmdEvents.Duplicate:
case CmdEvents.Copy:
case CmdEvents.Cut:
case CmdEvents.Paste:
evt.StopPropagation();
break;
}
}
private void OnPointerDown(PointerDownEvent evt)
{
// Allow right clicks to select an item before we bring up the matching context menu.
if (evt.button == (int)MouseButton.RightMouse && evt.clickCount == 1)
{
// Look upwards to the immediate child of the scroll view, so we know what Index to use
var element = evt.target as VisualElement;
while (element != null && element.name != "unity-tree-view__item")
element = element.parent;
if (element == null)
return;
m_ActionsTreeView.SetSelection(element.parent.IndexOf(element));
}
}
private string GetPreviousActionNameFromViewTree(in ActionOrBindingData data)
{
Debug.Assert(data.isAction);
// If TreeView currently (before delete) has more than one Action, select the one immediately
// above or immediately below depending if data is first in the list
var treeView = ViewStateSelector.GetViewState(stateContainer.GetState()).treeViewData;
if (treeView.Count > 1)
{
string actionName = data.name;
int index = treeView.FindIndex(item => item.data.name == actionName);
if (index > 0)
index--;
else
index++; // Also handles case if actionName wasn't found; FindIndex() returns -1 that's incremented to 0
return treeView[index].data.name;
}
return string.Empty;
}
private int GetPreviousBindingIndexFromViewTree(in ActionOrBindingData data, out string parentActionName)
{
Debug.Assert(!data.isAction);
int retVal = -1;
parentActionName = string.Empty;
// The bindindIndex is global and doesn't correspond to the binding's "child index" within the TreeView.
// To find the "previous" Binding to select, after deleting the current one, we must:
// 1. Traverse the ViewTree to find the parent of the binding and its index under that parent
// 2. Identify the Binding to select after deletion and retrieve its bindingIndex
// 3. Return the bindingIndex and the parent Action name (select the Action if bindingIndex is invalid)
var treeView = ViewStateSelector.GetViewState(stateContainer.GetState()).treeViewData;
foreach (var action in treeView)
{
if (!action.hasChildren)
continue;
if (FindBindingOrComponentTreeViewParent(action, data.bindingIndex, out var parentNode, out int childIndex))
{
parentActionName = action.data.name;
if (parentNode.children.Count() > 1)
{
int prevIndex = Math.Max(childIndex - 1, 0);
var node = parentNode.children.ElementAt(prevIndex);
retVal = node.data.bindingIndex;
break;
}
}
}
return retVal;
}
private static bool FindBindingOrComponentTreeViewParent(TreeViewItemData<ActionOrBindingData> root, int bindingIndex, out TreeViewItemData<ActionOrBindingData> parent, out int childIndex)
{
Debug.Assert(root.hasChildren);
int index = 0;
foreach (var item in root.children)
{
if (item.data.bindingIndex == bindingIndex)
{
parent = root;
childIndex = index;
return true;
}
if (item.hasChildren && FindBindingOrComponentTreeViewParent(item, bindingIndex, out parent, out childIndex))
return true;
index++;
}
parent = default;
childIndex = -1;
return false;
}
internal class ViewState
{
public List<TreeViewItemData<ActionOrBindingData>> treeViewData;
public int actionMapCount;
public int newElementID;
}
}
internal struct ActionOrBindingData
{
public ActionOrBindingData(bool isAction, string name, int actionMapIndex, bool isComposite = false, bool isPartOfComposite = false, string controlLayout = "", int bindingIndex = -1, int actionIndex = -1, bool isCut = false)
{
this.name = name;
this.isComposite = isComposite;
this.isPartOfComposite = isPartOfComposite;
this.actionMapIndex = actionMapIndex;
this.controlLayout = controlLayout;
this.bindingIndex = bindingIndex;
this.isAction = isAction;
this.actionIndex = actionIndex;
this.isCut = isCut;
}
public string name { get; }
public bool isAction { get; }
public int actionMapIndex { get; }
public bool isComposite { get; }
public bool isPartOfComposite { get; }
public string controlLayout { get; }
public int bindingIndex { get; }
public int actionIndex { get; }
public bool isCut { get; }
}
internal static partial class Selectors
{
public static List<TreeViewItemData<ActionOrBindingData>> GetActionsAsTreeViewData(InputActionsEditorState state, Dictionary<Guid, int> idDictionary)
{
var actionMapIndex = state.selectedActionMapIndex;
var controlSchemes = state.serializedObject.FindProperty(nameof(InputActionAsset.m_ControlSchemes));
var actionMap = GetSelectedActionMap(state);
if (actionMap == null)
return new List<TreeViewItemData<ActionOrBindingData>>();
var actions = actionMap.Value.wrappedProperty
.FindPropertyRelative(nameof(InputActionMap.m_Actions))
.Select(sp => new SerializedInputAction(sp));
var bindings = actionMap.Value.wrappedProperty
.FindPropertyRelative(nameof(InputActionMap.m_Bindings))
.Select(sp => new SerializedInputBinding(sp))
.ToList();
var actionItems = new List<TreeViewItemData<ActionOrBindingData>>();
foreach (var action in actions)
{
var actionBindings = bindings.Where(spb => spb.action == action.name).ToList();
var bindingItems = new List<TreeViewItemData<ActionOrBindingData>>();
var actionId = new Guid(action.id);
for (var i = 0; i < actionBindings.Count; i++)
{
var serializedInputBinding = actionBindings[i];
var inputBindingId = new Guid(serializedInputBinding.id);
if (serializedInputBinding.isComposite)
{
var isLastBinding = i >= actionBindings.Count - 1;
var hasHiddenCompositeParts = false;
var compositeItems = new List<TreeViewItemData<ActionOrBindingData>>();
if (!isLastBinding)
{
var nextBinding = actionBindings[++i];
while (nextBinding.isPartOfComposite)
{
var isVisible = ShouldBindingBeVisible(nextBinding, state.selectedControlScheme, state.selectedDeviceRequirementIndex);
if (isVisible)
{
var name = GetHumanReadableCompositeName(nextBinding, state.selectedControlScheme, controlSchemes);
compositeItems.Add(new TreeViewItemData<ActionOrBindingData>(GetIdForGuid(new Guid(nextBinding.id), idDictionary),
new ActionOrBindingData(isAction: false, name, actionMapIndex, isComposite: false,
isPartOfComposite: true, GetControlLayout(nextBinding.path), bindingIndex: nextBinding.indexOfBinding, isCut: state.IsBindingCut(actionMapIndex, nextBinding.indexOfBinding))));
}
else
hasHiddenCompositeParts = true;
if (++i >= actionBindings.Count)
break;
nextBinding = actionBindings[i];
}
i--;
}
var shouldCompositeBeVisible = !(compositeItems.Count == 0 && hasHiddenCompositeParts); //hide composite if all parts are hidden
if (shouldCompositeBeVisible)
bindingItems.Add(new TreeViewItemData<ActionOrBindingData>(GetIdForGuid(inputBindingId, idDictionary),
new ActionOrBindingData(isAction: false, serializedInputBinding.name, actionMapIndex, isComposite: true, isPartOfComposite: false, action.expectedControlType, bindingIndex: serializedInputBinding.indexOfBinding, isCut: state.IsBindingCut(actionMapIndex, serializedInputBinding.indexOfBinding)),
compositeItems.Count > 0 ? compositeItems : null));
}
else
{
var isVisible = ShouldBindingBeVisible(serializedInputBinding, state.selectedControlScheme, state.selectedDeviceRequirementIndex);
if (isVisible)
bindingItems.Add(new TreeViewItemData<ActionOrBindingData>(GetIdForGuid(inputBindingId, idDictionary),
new ActionOrBindingData(isAction: false, GetHumanReadableBindingName(serializedInputBinding, state.selectedControlScheme, controlSchemes), actionMapIndex,
isComposite: false, isPartOfComposite: false, GetControlLayout(serializedInputBinding.path), bindingIndex: serializedInputBinding.indexOfBinding, isCut: state.IsBindingCut(actionMapIndex, serializedInputBinding.indexOfBinding))));
}
}
var actionIndex = action.wrappedProperty.GetIndexOfArrayElement();
actionItems.Add(new TreeViewItemData<ActionOrBindingData>(GetIdForGuid(actionId, idDictionary),
new ActionOrBindingData(isAction: true, action.name, actionMapIndex, isComposite: false, isPartOfComposite: false, action.expectedControlType, actionIndex: actionIndex, isCut: state.IsActionCut(actionMapIndex, actionIndex)), bindingItems.Count > 0 ? bindingItems : null));
}
return actionItems;
}
private static int GetIdForGuid(Guid guid, Dictionary<Guid, int> idDictionary)
{
// This method is used to ensure that the same Guid always gets the same id
// We use getHashCode instead of a counter, as we cannot guarantee that the same Guid will always be added in the same order
// There is a tiny chance of a collision, but it is it does happen it will only affect the expanded state of the tree view
if (!idDictionary.TryGetValue(guid, out var id))
{
id = guid.GetHashCode();
idDictionary.Add(guid, id);
}
return id;
}
private static string GetHumanReadableBindingName(SerializedInputBinding serializedInputBinding, InputControlScheme? currentControlScheme, SerializedProperty allControlSchemes)
{
var name = InputControlPath.ToHumanReadableString(serializedInputBinding.path);
if (String.IsNullOrEmpty(name))
name = "<No Binding>";
if (IsBindingAssignedToNoControlSchemes(serializedInputBinding, allControlSchemes, currentControlScheme))
name += " {GLOBAL}";
return name;
}
private static bool IsBindingAssignedToNoControlSchemes(SerializedInputBinding serializedInputBinding, SerializedProperty allControlSchemes, InputControlScheme? currentControlScheme)
{
if (allControlSchemes.arraySize <= 0 || !currentControlScheme.HasValue || string.IsNullOrEmpty(currentControlScheme.Value.name))
return false;
if (serializedInputBinding.controlSchemes.Length <= 0)
return true;
return false;
}
private static bool ShouldBindingBeVisible(SerializedInputBinding serializedInputBinding, InputControlScheme? currentControlScheme, int deviceIndex)
{
if (currentControlScheme.HasValue && !string.IsNullOrEmpty(currentControlScheme.Value.name))
{
var isMatchingDevice = true;
if (deviceIndex >= 0 && deviceIndex < currentControlScheme.Value.deviceRequirements.Count)
{
var devicePathToMatch = InputControlPath.TryGetDeviceLayout(currentControlScheme.Value.deviceRequirements.ElementAt(deviceIndex).controlPath);
var devicePath = InputControlPath.TryGetDeviceLayout(serializedInputBinding.path);
isMatchingDevice = string.Equals(devicePathToMatch, devicePath, StringComparison.InvariantCultureIgnoreCase) || InputControlLayout.s_Layouts.IsBasedOn(new InternedString(devicePath), new InternedString(devicePathToMatch));
}
var hasNoControlScheme = serializedInputBinding.controlSchemes.Length <= 0; //also show GLOBAL bindings
var isAssignedToCurrentControlScheme = serializedInputBinding.controlSchemes.Contains(currentControlScheme.Value.name);
return (isAssignedToCurrentControlScheme || hasNoControlScheme) && isMatchingDevice;
}
//if no control scheme selected then show all bindings
return true;
}
internal static string GetHumanReadableCompositeName(SerializedInputBinding binding, InputControlScheme? currentControlScheme, SerializedProperty allControlSchemes)
{
return $"{ObjectNames.NicifyVariableName(binding.name)}: " +
$"{GetHumanReadableBindingName(binding, currentControlScheme, allControlSchemes)}";
}
private static string GetControlLayout(string path)
{
var controlLayout = string.Empty;
try
{
controlLayout = InputControlPath.TryGetControlLayout(path);
}
catch (Exception)
{
}
return controlLayout;
}
}
}
#endif

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,238 @@
#if UNITY_EDITOR
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)
{
treeViewItem.OnContextualMenuPopulateEvent = (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);
});
}
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)
{
treeViewItem.OnContextualMenuPopulateEvent = (menuEvent =>
{
AppendRenameAction(menuEvent, treeView, index);
AppendDuplicateDeleteCutAndCopyActionsSection(menuEvent, treeView, index);
});
}
public static void GetContextMenuForBindingItem(ActionsTreeView treeView, InputActionsTreeViewItem treeViewItem, int index)
{
treeViewItem.OnContextualMenuPopulateEvent = (menuEvent =>
{
AppendDuplicateDeleteCutAndCopyActionsSection(menuEvent, treeView, index);
});
}
private static void AppendRenameAction(ContextualMenuPopulateEvent menuEvent, ActionsTreeView treeView, int index)
{
menuEvent.menu.AppendAction(rename_String, _ => treeView.RenameActionItem(index));
}
// These actions are always either all present, or all missing, so we can group their Append calls here.
private static void AppendDuplicateDeleteCutAndCopyActionsSection(ContextualMenuPopulateEvent menuEvent, ActionsTreeView actionsTreeView, int index)
{
menuEvent.menu.AppendAction(duplicate_String, _ => actionsTreeView.DuplicateItem(index));
menuEvent.menu.AppendAction(delete_String, _ => actionsTreeView.DeleteItem(index));
menuEvent.menu.AppendSeparator();
menuEvent.menu.AppendAction(copy_String, _ => actionsTreeView.CopyItems());
menuEvent.menu.AppendAction(cut_String, _ => actionsTreeView.CutItems());
}
#endregion
}
}
#endif

View File

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

View File

@@ -0,0 +1,206 @@
#if UNITY_EDITOR
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), maxWidth = new StyleLength(new Length(100, LengthUnit.Percent)) }
};
popupWindow.contentContainer.Add(controlSchemeVisualElement);
m_ModalWindow.Add(popupWindow);
root.Add(m_ModalWindow);
m_ModalWindow.StretchToParentSize();
popupWindow.RegisterCallback<ClickEvent>(evt => evt.StopPropagation());
m_ListView = controlSchemeVisualElement.Q<MultiColumnListView>(kControlSchemesListView);
m_ListView.columns[kDeviceTypeColumnName].makeCell = () => new Label();
m_ListView.columns[kDeviceTypeColumnName].bindCell = BindDeviceTypeCell;
m_ListView.columns[kRequiredColumnName].makeCell = MakeRequiredCell;
m_ListView.columns[kRequiredColumnName].bindCell = BindDeviceRequiredCell;
m_ListView.columns[kRequiredColumnName].unbindCell = UnbindDeviceRequiredCell;
m_ListView.Q<Button>(kUnityListViewAddButton).clickable = new Clickable(AddDeviceRequirement);
m_ListView.Q<Button>(kUnityListViewRemoveButton).clickable = new Clickable(RemoveDeviceRequirement);
m_ListView.itemIndexChanged += (oldPosition, newPosition) =>
{
Dispatch(ControlSchemeCommands.ReorderDeviceRequirements(oldPosition, newPosition));
};
m_ListView.itemsSource = new List<string>();
CreateSelector(s => s.selectedControlScheme,
(_, s) => s.selectedControlScheme);
}
private void AddDeviceRequirement()
{
var dropdown = new InputControlPickerDropdown(new InputControlPickerState(), path =>
{
var requirement = new InputControlScheme.DeviceRequirement { controlPath = path, isOptional = false };
Dispatch(ControlSchemeCommands.AddDeviceRequirement(requirement));
}, mode: InputControlPicker.Mode.PickDevice);
dropdown.Show(new Rect(Event.current.mousePosition, Vector2.zero));
}
private void RemoveDeviceRequirement()
{
if (m_ListView.selectedIndex == -1)
return;
Dispatch(ControlSchemeCommands.RemoveDeviceRequirement(m_ListView.selectedIndex));
}
public override void RedrawUI(InputControlScheme viewState)
{
rootElement.Q<TextField>(kControlSchemeNameTextField).value = string.IsNullOrEmpty(m_NewName) ? viewState.name : m_NewName;
m_ListView.itemsSource?.Clear();
m_ListView.itemsSource = viewState.deviceRequirements.Count > 0 ?
viewState.deviceRequirements.Select(r => (r.controlPath, r.isOptional)).ToList() :
new List<(string, bool)>();
m_ListView.Rebuild();
}
public override void DestroyView()
{
m_ModalWindow.RemoveFromHierarchy();
}
private void SaveAndClose()
{
// Persist the current ControlScheme values to the SerializedProperty
Dispatch(ControlSchemeCommands.SaveControlScheme(m_NewName, m_UpdateExisting));
CloseView();
}
private void Cancel()
{
// Reload the selected ControlScheme values from the SerilaizedProperty and throw away any changes
Dispatch(ControlSchemeCommands.ResetSelectedControlScheme());
CloseView();
}
private void CloseView()
{
// Closing the View without explicitly selecting "Save" or "Cancel" holds the values in the
// current UI state but won't persist them; the Asset Editor state isn't dirtied.
//
// This means accidentally clicking outside of the View (closes the dialog) won't immediately
// cause loss of work: the "Edit Control Scheme" option can be selected to re-open the View
// the changes retained. However, if a different ControlScheme is selected or the Asset
// Editor window is closed, then the changes are lost.
m_NewName = string.Empty;
OnClosing?.Invoke(this);
}
private VisualElement MakeRequiredCell()
{
var ve = new VisualElement
{
style =
{
flexDirection = FlexDirection.Column,
flexGrow = 1,
alignContent = new StyleEnum<Align>(Align.Center)
}
};
ve.Add(new Toggle());
return ve;
}
private void BindDeviceRequiredCell(VisualElement visualElement, int rowIndex)
{
var toggle = visualElement.Q<Toggle>();
var rowItem = ((string path, bool optional))m_ListView.itemsSource[rowIndex];
toggle.value = !rowItem.optional;
var eventCallback = (EventCallback<ChangeEvent<bool>>)(evt =>
Dispatch(ControlSchemeCommands.ChangeDeviceRequirement(rowIndex, evt.newValue)));
toggle.userData = eventCallback;
toggle.RegisterValueChangedCallback(eventCallback);
}
private void UnbindDeviceRequiredCell(VisualElement visualElement, int rowIndex)
{
var toggle = visualElement.Q<Toggle>();
toggle.UnregisterValueChangedCallback((EventCallback<ChangeEvent<bool>>)toggle.userData);
}
private void BindDeviceTypeCell(VisualElement visualElement, int rowIndex)
{
((Label)visualElement).text = (((string, bool))m_ListView.itemsSource[rowIndex]).Item1;
}
private readonly bool m_UpdateExisting;
private MultiColumnListView m_ListView;
private VisualElement m_ModalWindow;
private const string kControlSchemeNameTextField = "control-scheme-name";
private const string kCancelButton = "cancel-button";
private const string kSaveButton = "save-button";
private const string kControlSchemesListView = "control-schemes-list-view";
private const string kDeviceTypeColumnName = "device-type";
private const string kRequiredColumnName = "required";
private const string kUnityListViewAddButton = "unity-list-view__add-button";
private const string kUnityListViewRemoveButton = "unity-list-view__remove-button";
}
}
#endif

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,154 @@
#if UNITY_EDITOR
using System;
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;
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 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.selectAllOnMouseUp = false;
RegisterInputField();
_ = new ContextualMenuManipulator(menuBuilder =>
{
OnContextualMenuPopulateEvent?.Invoke(menuBuilder);
})
{ target = this };
}
public Label label => this.Q<Label>();
private TextField renameTextfield => this.Q<TextField>(kRenameTextField);
public void RegisterInputField()
{
renameTextfield.SetEnabled(true);
renameTextfield.selectAllOnFocus = true;
RegisterCallback<MouseDownEvent>(OnMouseDownEventForRename);
renameTextfield.RegisterCallback<FocusInEvent>(e => IsFocused = true);
renameTextfield.RegisterCallback<FocusOutEvent>(e =>
{
OnEditTextFinished();
IsFocused = false;
});
}
public void UnregisterInputField()
{
renameTextfield.SetEnabled(false);
renameTextfield.selectAllOnFocus = false;
UnregisterCallback<MouseDownEvent>(OnMouseDownEventForRename);
renameTextfield.UnregisterCallback<FocusOutEvent>(e => OnEditTextFinished());
}
private double lastSingleClick;
private static InputActionsTreeViewItem 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 || 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
schedule.Execute(() => renameTextfield.Q<TextField>().Focus()).StartingIn(120);
renameTextfield.SelectAll();
s_EditingItem = this;
m_IsEditing = true;
}
public static void CancelRename()
{
s_EditingItem?.OnEditTextFinished();
}
private void OnEditTextFinished()
{
if (!m_IsEditing)
return;
lastSingleClick = 0;
delegatesFocus = false;
renameTextfield.AddToClassList(InputActionsEditorConstants.HiddenStyleClassName);
label.RemoveFromClassList(InputActionsEditorConstants.HiddenStyleClassName);
s_EditingItem = null;
m_IsEditing = false;
var text = renameTextfield.text?.Trim();
if (string.IsNullOrEmpty(text))
{
renameTextfield.schedule.Execute(() => renameTextfield.SetValueWithoutNotify(text));
return;
}
EditTextFinished?.Invoke(text);
}
}
}
#endif

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,178 @@
#if UNITY_EDITOR
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 = NameAndParameters.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 = NameAndParameters.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 = NameAndParameters.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 = NameAndParameters.ToSerializableString(interactionsOrProcessorsList);
m_ListProperty.serializedObject.ApplyModifiedProperties();
}
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);
}
}
}
#endif

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,382 @@
#if UNITY_EDITOR
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)
{
SerializedProperty property = state.serializedObject
?.FindProperty(nameof(InputActionAsset.m_ActionMaps))?.GetArrayElementAtIndex(mapIndex)
?.FindPropertyRelative(nameof(InputActionMap.m_Actions))
?.FirstOrDefault(p => p.FindPropertyRelative(nameof(InputAction.m_Name)).stringValue == name);
// If the action is not found, return null.
if (property == null)
return null;
return new SerializedInputAction(property);
}
public static SerializedInputBinding GetCompositeOrBindingInMap(SerializedProperty actionMap, int bindingIndex)
{
return new SerializedInputBinding(actionMap
?.FindPropertyRelative(nameof(InputActionMap.m_Bindings))
?.GetArrayElementAtIndex(bindingIndex));
}
public static SerializedProperty GetBindingForId(InputActionsEditorState state, string id, out SerializedProperty bindingArray)
{
return GetBindingForId(state.serializedObject, id, out bindingArray);
}
public static SerializedProperty GetBindingForId(SerializedObject serializedObject, string id, out SerializedProperty bindingArray)
{
var actionMaps = serializedObject?.FindProperty(nameof(InputActionAsset.m_ActionMaps));
for (int i = 0; i < actionMaps?.arraySize; i++)
{
var bindings = actionMaps.GetArrayElementAtIndex(i).FindPropertyRelative(nameof(InputActionMap.m_Bindings));
for (int j = 0; j < bindings.arraySize; j++)
{
if (bindings.GetArrayElementAtIndex(j).FindPropertyRelative("m_Id").stringValue != id)
continue;
bindingArray = bindings;
return bindings.GetArrayElementAtIndex(j);
}
}
bindingArray = null;
return null;
}
public static SerializedProperty GetSelectedBindingPath(InputActionsEditorState state)
{
var selectedBinding = GetSelectedBinding(state);
return selectedBinding?.wrappedProperty.FindPropertyRelative("m_Path");
}
public static SerializedInputBinding? GetSelectedBinding(InputActionsEditorState state)
{
var actionMapSO = GetActionMapAtIndex(state, state.selectedActionMapIndex);
var bindings = actionMapSO?.wrappedProperty.FindPropertyRelative(nameof(InputActionMap.m_Bindings));
if (bindings == null || bindings.arraySize - 1 < state.selectedBindingIndex || state.selectedBindingIndex < 0)
return null;
return new SerializedInputBinding(bindings.GetArrayElementAtIndex(state.selectedBindingIndex));
}
public static SerializedInputAction? GetRelatedInputAction(InputActionsEditorState state)
{
var binding = GetSelectedBinding(state);
if (binding == null)
return null;
var actionName = binding.Value.wrappedProperty.FindPropertyRelative("m_Action").stringValue;
return GetActionInMap(state, state.selectedActionMapIndex, actionName);
}
public static IEnumerable<string> GetCompositeTypes(string path, string expectedControlLayout)
{
// Find name of current composite.
var compositeNameAndParameters = NameAndParameters.Parse(path);
var compositeName = compositeNameAndParameters.name;
var compositeType = InputBindingComposite.s_Composites.LookupTypeRegistration(compositeName);
// Collect all possible composite types.
var selectedCompositeIndex = -1;
var currentIndex = 0;
foreach (var composite in InputBindingComposite.s_Composites.internedNames.Where(x =>
!InputBindingComposite.s_Composites.aliases.Contains(x)).OrderBy(x => x))
{
if (!string.IsNullOrEmpty(expectedControlLayout))
{
var valueType = InputBindingComposite.GetValueType(composite);
if (valueType != null &&
!InputControlLayout.s_Layouts.ValueTypeIsAssignableFrom(
new InternedString(expectedControlLayout), valueType))
continue;
}
if (InputBindingComposite.s_Composites.LookupTypeRegistration(composite) == compositeType)
selectedCompositeIndex = currentIndex;
yield return composite;
++currentIndex;
}
// If the current composite type isn't a registered type, add it to the list as
// an extra option.
if (selectedCompositeIndex == -1)
yield return compositeName;
}
public static IEnumerable<string> GetCompositePartOptions(string bindingName, string compositeName)
{
var currentIndex = 0;
var selectedPartNameIndex = -1;
foreach (var partName in InputBindingComposite.GetPartNames(compositeName))
{
if (partName.Equals(bindingName, StringComparison.OrdinalIgnoreCase))
selectedPartNameIndex = currentIndex;
yield return partName;
++currentIndex;
}
// If currently selected part is not in list, add it as an option.
if (selectedPartNameIndex == -1)
yield return bindingName;
}
public static SerializedInputAction? GetSelectedAction(InputActionsEditorState state)
{
var actions = GetActionMapAtIndex(state, state.selectedActionMapIndex)
?.wrappedProperty.FindPropertyRelative(nameof(InputActionMap.m_Actions));
if (actions == null || actions.arraySize - 1 < state.selectedActionIndex || state.selectedActionIndex < 0)
return null;
// If we've currently selected a binding, get the parent input action for it.
if (state.selectionType == SelectionType.Binding)
return GetRelatedInputAction(state);
return new SerializedInputAction(actions.GetArrayElementAtIndex(state.selectedActionIndex));
}
public static IEnumerable<string> BuildControlTypeList(InputActionType selectedActionType)
{
var allLayouts = InputSystem.s_Manager.m_Layouts;
// "Any" is always in first position (index 0)
yield return "Any";
foreach (var layoutName in allLayouts.layoutTypes.Keys)
{
if (EditorInputControlLayoutCache.TryGetLayout(layoutName).hideInUI)
continue;
// If the action type is InputActionType.Value, skip button controls.
var type = allLayouts.layoutTypes[layoutName];
if (selectedActionType == InputActionType.Value && typeof(ButtonControl).IsAssignableFrom(type))
continue;
////TODO: skip aliases
if (typeof(InputControl).IsAssignableFrom(type) && !typeof(InputDevice).IsAssignableFrom(type))
yield return layoutName;
}
}
public static IEnumerable<ParameterListView> GetInteractionsAsParameterListViews(InputActionsEditorState state, SerializedInputAction? inputAction)
{
Type expectedValueType = null;
if (inputAction.HasValue && !string.IsNullOrEmpty(inputAction.Value.expectedControlType))
expectedValueType = EditorInputControlLayoutCache.GetValueType(inputAction.Value.expectedControlType);
var interactions = string.Empty;
if (inputAction.HasValue && state.selectionType == SelectionType.Action)
interactions = inputAction.Value.interactions;
else if (state.selectionType == SelectionType.Binding && GetSelectedBinding(state).HasValue)
interactions = GetSelectedBinding(state)?.interactions;
return CreateParameterListViews(
interactions,
expectedValueType,
InputInteraction.s_Interactions.LookupTypeRegistration,
InputInteraction.GetValueType);
}
public static IEnumerable<ParameterListView> GetProcessorsAsParameterListViews(InputActionsEditorState state, SerializedInputAction? inputAction)
{
var processors = string.Empty;
Type expectedValueType = null;
if (inputAction.HasValue && !string.IsNullOrEmpty(inputAction.Value.expectedControlType))
expectedValueType = EditorInputControlLayoutCache.GetValueType(inputAction.Value.expectedControlType);
if (inputAction.HasValue && state.selectionType == SelectionType.Action)
processors = inputAction.Value.processors;
else if (state.selectionType == SelectionType.Binding && GetSelectedBinding(state).HasValue)
processors = GetSelectedBinding(state)?.processors;
return CreateParameterListViews(
processors,
expectedValueType,
InputProcessor.s_Processors.LookupTypeRegistration,
InputProcessor.GetValueTypeFromType);
}
private static IEnumerable<ParameterListView> CreateParameterListViews(string interactions, Type expectedValueType,
Func<string, Type> typeLookup, Func<Type, Type> getGenericArgumentType)
{
return NameAndParameters.ParseMultiple(interactions)
.Select(p => (interaction: p, rowType: typeLookup(p.name)))
.Select(t =>
{
var(parameter, rowType) = t;
var parameterListView = new ParameterListView();
parameterListView.Initialize(rowType, parameter.parameters);
parameterListView.name = ObjectNames.NicifyVariableName(parameter.name);
if (rowType == null)
{
parameterListView.name += " (Obsolete)";
}
else if (expectedValueType != null)
{
var valueType = getGenericArgumentType(rowType);
if (valueType != null && !expectedValueType.IsAssignableFrom(valueType))
parameterListView.name += " (Incompatible Value Type)";
}
return parameterListView;
});
}
}
}
#endif

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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