UI 자동화를 위해 바인딩 기능 구현
- 유니티 에셋 인증 오류로 meta 재생성
This commit is contained in:
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0226840818c8be84a9d8774a2529fe86
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,109 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
internal class ActionPropertiesView : ViewBase<(SerializedInputAction?, List<string>)>
|
||||
{
|
||||
private readonly Foldout m_ParentFoldout;
|
||||
private readonly int m_DropdownLabelWidth = 90;
|
||||
|
||||
public ActionPropertiesView(VisualElement root, Foldout foldout, StateContainer stateContainer)
|
||||
: base(root, stateContainer)
|
||||
{
|
||||
m_ParentFoldout = foldout;
|
||||
|
||||
// TODO: Consider IEquatable<T> and how to compare selector data
|
||||
CreateSelector(Selectors.GetSelectedAction,
|
||||
(inputAction, _) =>
|
||||
{
|
||||
if (!inputAction.HasValue)
|
||||
return (null, new List<string>());
|
||||
return (inputAction.Value, Selectors.BuildControlTypeList(inputAction.Value.type).ToList());
|
||||
});
|
||||
}
|
||||
|
||||
public override void RedrawUI((SerializedInputAction ? , List<string>) viewState)
|
||||
{
|
||||
if (!viewState.Item1.HasValue)
|
||||
return;
|
||||
|
||||
m_ParentFoldout.text = "Action";
|
||||
var inputAction = viewState.Item1.Value;
|
||||
|
||||
rootElement.Clear();
|
||||
|
||||
var actionType = new EnumField("Action Type", inputAction.type)
|
||||
{
|
||||
tooltip = inputAction.actionTypeTooltip
|
||||
};
|
||||
|
||||
// Tighten up the gap between the label and dropdown so the latter is more readable when the parent pane is at min width.
|
||||
var actionLabel = actionType.Q<Label>();
|
||||
actionLabel.style.minWidth = m_DropdownLabelWidth;
|
||||
actionLabel.style.width = m_DropdownLabelWidth;
|
||||
|
||||
actionType.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
Dispatch(Commands.ChangeActionType(inputAction, (InputActionType)evt.newValue));
|
||||
});
|
||||
rootElement.Add(actionType);
|
||||
|
||||
if (inputAction.type != InputActionType.Button)
|
||||
{
|
||||
var controlTypes = viewState.Item2;
|
||||
var controlType = new DropdownField("Control Type");
|
||||
|
||||
// Tighten up the gap between the label and dropdown so the latter is more readable when the parent pane is at min width.
|
||||
var controlLabel = controlType.Q<Label>();
|
||||
controlLabel.style.minWidth = m_DropdownLabelWidth;
|
||||
controlLabel.style.width = m_DropdownLabelWidth;
|
||||
|
||||
controlType.choices.Clear();
|
||||
controlType.choices.AddRange(controlTypes.Select(ObjectNames.NicifyVariableName).ToList());
|
||||
var controlTypeIndex = controlTypes.FindIndex(s => s == inputAction.expectedControlType);
|
||||
//if type changed and index is -1 clamp to 0, prevent overflowing indices
|
||||
controlTypeIndex = Math.Clamp(controlTypeIndex, 0, controlTypes.Count - 1);
|
||||
controlType.SetValueWithoutNotify(controlType.choices[controlTypeIndex]);
|
||||
controlType.tooltip = inputAction.expectedControlTypeTooltip;
|
||||
|
||||
controlType.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
Dispatch(Commands.ChangeActionControlType(inputAction, controlType.index));
|
||||
});
|
||||
|
||||
// ISX-1916 - When changing ActionType to a non-Button type, we must also update the ControlType
|
||||
// to the currently selected value; the ValueChangedCallback is not fired in this scenario.
|
||||
Dispatch(Commands.ChangeActionControlType(inputAction, controlType.index));
|
||||
|
||||
rootElement.Add(controlType);
|
||||
}
|
||||
else
|
||||
{
|
||||
// ISX-1916 - When changing ActionType to a Button, we must also reset the ControlType
|
||||
Dispatch(Commands.ChangeActionControlType(inputAction, 0));
|
||||
}
|
||||
|
||||
if (inputAction.type != InputActionType.Value)
|
||||
{
|
||||
var initialStateCheck = new Toggle("Initial State Check")
|
||||
{
|
||||
tooltip = InputActionsEditorConstants.InitialStateCheckTooltip
|
||||
};
|
||||
initialStateCheck.SetValueWithoutNotify(inputAction.initialStateCheck);
|
||||
initialStateCheck.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
Dispatch(Commands.ChangeInitialStateCheck(inputAction, evt.newValue));
|
||||
});
|
||||
rootElement.Add(initialStateCheck);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 954f8631d67512b409a5c4c526ae4f71
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 716898219fc23de4a81300652df80ef3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,185 @@
|
||||
#if UNITY_EDITOR
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine.UIElements;
|
||||
using UnityEngine.InputSystem.Layouts;
|
||||
using UnityEngine.InputSystem.Utilities;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
internal class BindingPropertiesView : ViewBase<BindingPropertiesView.ViewState>
|
||||
{
|
||||
private readonly Foldout m_ParentFoldout;
|
||||
private CompositeBindingPropertiesView m_CompositeBindingPropertiesView;
|
||||
private CompositePartBindingPropertiesView m_CompositePartBindingPropertiesView;
|
||||
|
||||
public BindingPropertiesView(VisualElement root, Foldout foldout, StateContainer stateContainer)
|
||||
: base(root, stateContainer)
|
||||
{
|
||||
m_ParentFoldout = foldout;
|
||||
|
||||
CreateSelector(state => state.selectedBindingIndex,
|
||||
s => new ViewStateCollection<InputControlScheme>(Selectors.GetControlSchemes(s)),
|
||||
(_, controlSchemes, s) => new ViewState
|
||||
{
|
||||
controlSchemes = controlSchemes,
|
||||
currentControlScheme = s.selectedControlScheme,
|
||||
selectedBinding = Selectors.GetSelectedBinding(s),
|
||||
selectedBindingIndex = s.selectedBindingIndex,
|
||||
selectedBindingPath = Selectors.GetSelectedBindingPath(s),
|
||||
selectedInputAction = Selectors.GetSelectedAction(s)
|
||||
});
|
||||
}
|
||||
|
||||
public override void RedrawUI(ViewState viewState)
|
||||
{
|
||||
var selectedBindingIndex = viewState.selectedBindingIndex;
|
||||
if (selectedBindingIndex == -1)
|
||||
return;
|
||||
|
||||
rootElement.Clear();
|
||||
|
||||
var binding = viewState.selectedBinding;
|
||||
if (!binding.HasValue)
|
||||
return;
|
||||
|
||||
m_ParentFoldout.text = "Binding";
|
||||
if (binding.Value.isComposite)
|
||||
{
|
||||
m_ParentFoldout.text = "Composite";
|
||||
m_CompositeBindingPropertiesView = CreateChildView(new CompositeBindingPropertiesView(rootElement, stateContainer));
|
||||
}
|
||||
else if (binding.Value.isPartOfComposite)
|
||||
{
|
||||
m_CompositePartBindingPropertiesView = CreateChildView(new CompositePartBindingPropertiesView(rootElement, stateContainer));
|
||||
DrawMatchingControlPaths(viewState);
|
||||
DrawControlSchemeToggles(viewState, binding.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
var controlPathEditor = new InputControlPathEditor(viewState.selectedBindingPath, new InputControlPickerState(),
|
||||
() => { Dispatch(Commands.ApplyModifiedProperties()); });
|
||||
controlPathEditor.SetControlPathsToMatch(viewState.currentControlScheme.deviceRequirements.Select(x => x.controlPath));
|
||||
|
||||
var inputAction = viewState.selectedInputAction;
|
||||
controlPathEditor.SetExpectedControlLayout(inputAction?.expectedControlType ?? "");
|
||||
|
||||
var controlPathContainer = new IMGUIContainer(controlPathEditor.OnGUI);
|
||||
rootElement.Add(controlPathContainer);
|
||||
|
||||
DrawMatchingControlPaths(viewState);
|
||||
DrawControlSchemeToggles(viewState, binding.Value);
|
||||
}
|
||||
}
|
||||
|
||||
static bool s_showMatchingLayouts = false;
|
||||
internal void DrawMatchingControlPaths(ViewState viewState)
|
||||
{
|
||||
bool controlPathUsagePresent = false;
|
||||
bool showPaths = s_showMatchingLayouts;
|
||||
List<MatchingControlPath> matchingControlPaths = MatchingControlPath.CollectMatchingControlPaths(viewState.selectedBindingPath.stringValue, showPaths, ref controlPathUsagePresent);
|
||||
|
||||
var parentElement = rootElement;
|
||||
if (matchingControlPaths == null || matchingControlPaths.Count != 0)
|
||||
{
|
||||
var controllingElement = new Foldout()
|
||||
{
|
||||
text = $"Show Derived Bindings",
|
||||
value = showPaths
|
||||
};
|
||||
rootElement.Add(controllingElement);
|
||||
|
||||
controllingElement.RegisterValueChangedCallback(changeEvent =>
|
||||
{
|
||||
if (changeEvent.target == controllingElement) // only react to foldout and not tree elements
|
||||
s_showMatchingLayouts = changeEvent.newValue;
|
||||
});
|
||||
|
||||
parentElement = controllingElement;
|
||||
}
|
||||
|
||||
if (matchingControlPaths == null)
|
||||
{
|
||||
var messageString = controlPathUsagePresent ? "No registered controls match this current binding. Some controls are only registered at runtime." :
|
||||
"No other registered controls match this current binding. Some controls are only registered at runtime.";
|
||||
|
||||
var helpBox = new HelpBox(messageString, HelpBoxMessageType.Warning);
|
||||
helpBox.AddToClassList("matching-controls");
|
||||
parentElement.Add(helpBox);
|
||||
}
|
||||
else if (matchingControlPaths.Count > 0)
|
||||
{
|
||||
List<TreeViewItemData<MatchingControlPath>> treeViewMatchingControlPaths = MatchingControlPath.BuildMatchingControlPathsTreeData(matchingControlPaths);
|
||||
|
||||
var treeView = new TreeView();
|
||||
parentElement.Add(treeView);
|
||||
treeView.selectionType = UIElements.SelectionType.None;
|
||||
treeView.AddToClassList("matching-controls");
|
||||
treeView.fixedItemHeight = 20;
|
||||
treeView.SetRootItems(treeViewMatchingControlPaths);
|
||||
|
||||
// Set TreeView.makeItem to initialize each node in the tree.
|
||||
treeView.makeItem = () =>
|
||||
{
|
||||
var label = new Label();
|
||||
label.AddToClassList("matching-controls-labels");
|
||||
return label;
|
||||
};
|
||||
|
||||
// Set TreeView.bindItem to bind an initialized node to a data item.
|
||||
treeView.bindItem = (VisualElement element, int index) =>
|
||||
{
|
||||
var label = (element as Label);
|
||||
var matchingControlPath = treeView.GetItemDataForIndex<MatchingControlPath>(index);
|
||||
label.text = $"{matchingControlPath.deviceName} > {matchingControlPath.controlName}";
|
||||
};
|
||||
|
||||
treeView.ExpandRootItems();
|
||||
}
|
||||
}
|
||||
|
||||
public override void DestroyView()
|
||||
{
|
||||
m_CompositeBindingPropertiesView?.DestroyView();
|
||||
m_CompositePartBindingPropertiesView?.DestroyView();
|
||||
}
|
||||
|
||||
private void DrawControlSchemeToggles(ViewState viewState, SerializedInputBinding binding)
|
||||
{
|
||||
if (!viewState.controlSchemes.Any()) return;
|
||||
|
||||
var useInControlSchemeLabel = new Label("Use in control scheme")
|
||||
{
|
||||
name = "control-scheme-usage-title"
|
||||
};
|
||||
|
||||
rootElement.Add(useInControlSchemeLabel);
|
||||
|
||||
foreach (var controlScheme in viewState.controlSchemes)
|
||||
{
|
||||
var checkbox = new Toggle(controlScheme.name)
|
||||
{
|
||||
value = binding.controlSchemes.Any(scheme => controlScheme.name == scheme)
|
||||
};
|
||||
rootElement.Add(checkbox);
|
||||
checkbox.RegisterValueChangedCallback(changeEvent =>
|
||||
{
|
||||
Dispatch(ControlSchemeCommands.ChangeSelectedBindingsControlSchemes(controlScheme.name, changeEvent.newValue));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
internal class ViewState
|
||||
{
|
||||
public int selectedBindingIndex;
|
||||
public SerializedInputBinding? selectedBinding;
|
||||
public ViewStateCollection<InputControlScheme> controlSchemes;
|
||||
public InputControlScheme currentControlScheme;
|
||||
public SerializedProperty selectedBindingPath;
|
||||
public SerializedInputAction? selectedInputAction;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9c9510ff4a99bf84c9277bf2182a67d5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,92 @@
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// A helper class that provides a workaround to prevent deselection in a UI-Toolkit list or tree-view,
|
||||
/// e.g. when the user is pressing the ESC key while the view has focus.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
///The workaround is based on reassigning the selected index based on last selection, constrained
|
||||
/// to the available range items. The motivation behind this workaround is that there is no built-in support
|
||||
/// in UI-Toolkit list or tree-view to prevent deselection via ESC key.
|
||||
///
|
||||
/// This workaround should be removed if this changes in the future and the functionality is provided by
|
||||
/// the UI framework.
|
||||
///
|
||||
/// Define UNITY_INPUT_SYSTEM_INPUT_ASSET_EDITOR_ALLOWS_DESELECTION to disable this feature if desired
|
||||
/// during development.
|
||||
/// </remarks>>
|
||||
internal class CollectionViewSelectionChangeFilter
|
||||
{
|
||||
private readonly BaseVerticalCollectionView m_View;
|
||||
private List<int> m_SelectedIndices;
|
||||
|
||||
/// <summary>
|
||||
/// Event triggered as an output to filtering the selected indices reported by the view.
|
||||
/// </summary>
|
||||
public event Action<IEnumerable<int>> selectedIndicesChanged;
|
||||
|
||||
public CollectionViewSelectionChangeFilter(BaseVerticalCollectionView view)
|
||||
{
|
||||
m_SelectedIndices = new List<int>();
|
||||
|
||||
m_View = view;
|
||||
#if UNITY_INPUT_SYSTEM_INPUT_ASSET_EDITOR_ALLOWS_DESELECTION
|
||||
m_View_.selectedIndicesChanged += OnSelectedIndicesChanged;
|
||||
#else
|
||||
m_View.selectedIndicesChanged += FilterSelectedIndicesChanged;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !UNITY_INPUT_SYSTEM_INPUT_ASSET_EDITOR_ALLOWS_DESELECTION
|
||||
private void FilterSelectedIndicesChanged(IEnumerable<int> selectedIndices)
|
||||
{
|
||||
// Convert IEnumerable to a list to allow for multiple-iteration
|
||||
var currentlySelectedIndices = selectedIndices.ToList();
|
||||
|
||||
// If the selection change is a deselection (transition from having a selection to having none)
|
||||
if (currentlySelectedIndices.Count == 0)
|
||||
{
|
||||
if (m_SelectedIndices.Count > 0)
|
||||
{
|
||||
// Remove any stored selection indices that are no longer within valid range
|
||||
var count = m_View.itemsSource.Count;
|
||||
for (var i = m_SelectedIndices.Count - 1; i >= 0; --i)
|
||||
{
|
||||
if (m_SelectedIndices[i] >= count)
|
||||
m_SelectedIndices.RemoveAt(i);
|
||||
}
|
||||
|
||||
// Restore selection based on last known selection and return immediately since
|
||||
// assignment will retrigger selection change.
|
||||
m_View.SetSelection(m_SelectedIndices);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Store indices to allow preventing future deselect
|
||||
m_SelectedIndices = currentlySelectedIndices;
|
||||
}
|
||||
|
||||
OnSelectedIndicesChanged(this.m_SelectedIndices);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
private void OnSelectedIndicesChanged(IEnumerable<int> selectedIndices)
|
||||
{
|
||||
var selectedIndicesChanged = this.selectedIndicesChanged;
|
||||
selectedIndicesChanged?.Invoke(selectedIndices);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 36e7567d9504499a8bbe97595096de5e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2bd61b5c763f7444e9a5aa3c7623f60b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,90 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
internal class CompositePartBindingPropertiesView : ViewBase<CompositePartBindingPropertiesView.ViewState>
|
||||
{
|
||||
private readonly DropdownField m_CompositePartField;
|
||||
private readonly IMGUIContainer m_PathEditorContainer;
|
||||
|
||||
private const string UxmlName = InputActionsEditorConstants.PackagePath +
|
||||
InputActionsEditorConstants.ResourcesPath +
|
||||
InputActionsEditorConstants.CompositePartBindingPropertiesViewUxml;
|
||||
|
||||
public CompositePartBindingPropertiesView(VisualElement root, StateContainer stateContainer)
|
||||
: base(root, stateContainer)
|
||||
{
|
||||
var visualTreeAsset = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(UxmlName);
|
||||
var container = visualTreeAsset.CloneTree();
|
||||
rootElement.Add(container);
|
||||
|
||||
m_PathEditorContainer = container.Q<IMGUIContainer>("path-editor-container");
|
||||
m_CompositePartField = container.Q<DropdownField>("composite-part-dropdown");
|
||||
|
||||
CreateSelector(Selectors.GetSelectedBinding,
|
||||
(b, s) => b.HasValue && b.Value.isPartOfComposite ? Selectors.GetCompositePartBindingViewState(b.Value, s) : null);
|
||||
}
|
||||
|
||||
public override void RedrawUI(ViewState viewState)
|
||||
{
|
||||
if (viewState == null)
|
||||
return;
|
||||
// TODO: Persist control picker state
|
||||
var controlPathEditor = new InputControlPathEditor(viewState.selectedBindingPath, new InputControlPickerState(),
|
||||
() => { Dispatch(Commands.ApplyModifiedProperties()); });
|
||||
|
||||
controlPathEditor.SetControlPathsToMatch(viewState.currentControlScheme.deviceRequirements.Select(x => x.controlPath));
|
||||
controlPathEditor.SetExpectedControlLayout(viewState.expectedControlLayoutName);
|
||||
|
||||
m_PathEditorContainer.onGUIHandler = controlPathEditor.OnGUI;
|
||||
|
||||
m_CompositePartField.choices.Clear();
|
||||
m_CompositePartField.choices.AddRange(viewState.compositePartNames);
|
||||
m_CompositePartField.SetValueWithoutNotify(viewState.selectedCompositePartName);
|
||||
|
||||
m_CompositePartField.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
Dispatch(Commands.SetCompositeBindingPartName(viewState.selectedBinding, evt.newValue));
|
||||
});
|
||||
}
|
||||
|
||||
internal class ViewState
|
||||
{
|
||||
public SerializedProperty selectedBindingPath;
|
||||
public SerializedInputBinding selectedBinding;
|
||||
public IEnumerable<string> compositePartNames;
|
||||
public InputControlScheme currentControlScheme;
|
||||
public string expectedControlLayoutName;
|
||||
public string selectedCompositePartName;
|
||||
}
|
||||
}
|
||||
|
||||
internal static partial class Selectors
|
||||
{
|
||||
public static CompositePartBindingPropertiesView.ViewState GetCompositePartBindingViewState(SerializedInputBinding binding,
|
||||
InputActionsEditorState state)
|
||||
{
|
||||
var compositeParts = GetCompositePartOptions(binding.name, binding.compositePath).ToList();
|
||||
var selectedCompositePartName = ObjectNames.NicifyVariableName(
|
||||
compositeParts.First(str => string.Equals(str, binding.name, StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
var compositePartBindingViewState = new CompositePartBindingPropertiesView.ViewState
|
||||
{
|
||||
selectedBinding = binding,
|
||||
selectedBindingPath = GetSelectedBindingPath(state),
|
||||
selectedCompositePartName = selectedCompositePartName,
|
||||
currentControlScheme = state.selectedControlScheme,
|
||||
compositePartNames = compositeParts.Select(ObjectNames.NicifyVariableName).ToList(),
|
||||
expectedControlLayoutName = InputBindingComposite.GetExpectedControlLayoutName(binding.compositePath, binding.name) ?? ""
|
||||
};
|
||||
return compositePartBindingViewState;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1d3813199fcb56b45b916ac51d58dc80
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,238 @@
|
||||
#if UNITY_EDITOR
|
||||
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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 989e6ef705a83455c81a5d0a22910406
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,206 @@
|
||||
#if UNITY_EDITOR
|
||||
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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 66ed65ecd8caca54ba814d536ed4d5b2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,517 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using UnityEditor;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
// TODO Make buffers an optional argument and only allocate if not already passed, reuse a common buffer
|
||||
|
||||
internal static class CopyPasteHelper
|
||||
{
|
||||
private const string k_CopyPasteMarker = "INPUTASSET ";
|
||||
private const string k_StartOfText = "\u0002";
|
||||
private const string k_EndOfTransmission = "\u0004";
|
||||
private const string k_BindingData = "bindingData";
|
||||
private const string k_EndOfBinding = "+++";
|
||||
private static readonly Dictionary<Type, string> k_TypeMarker = new Dictionary<Type, string>
|
||||
{
|
||||
{typeof(InputActionMap), "InputActionMap"},
|
||||
{typeof(InputAction), "InputAction"},
|
||||
{typeof(InputBinding), "InputBinding"},
|
||||
};
|
||||
|
||||
private static SerializedProperty s_lastAddedElement;
|
||||
private static InputActionsEditorState s_State;
|
||||
private static bool s_lastClipboardActionWasCut = false;
|
||||
|
||||
private static bool IsComposite(SerializedProperty property) => property.FindPropertyRelative("m_Flags").intValue == (int)InputBinding.Flags.Composite;
|
||||
private static bool IsPartOfComposite(SerializedProperty property) => property.FindPropertyRelative("m_Flags").intValue == (int)InputBinding.Flags.PartOfComposite;
|
||||
private static string PropertyName(SerializedProperty property) => property.FindPropertyRelative("m_Name").stringValue;
|
||||
|
||||
#region Cut
|
||||
|
||||
public static void CutActionMap(InputActionsEditorState state)
|
||||
{
|
||||
CopyActionMap(state);
|
||||
s_lastClipboardActionWasCut = true;
|
||||
}
|
||||
|
||||
public static void Cut(InputActionsEditorState state)
|
||||
{
|
||||
Copy(state);
|
||||
s_lastClipboardActionWasCut = true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Copy
|
||||
|
||||
public static void CopyActionMap(InputActionsEditorState state)
|
||||
{
|
||||
var actionMap = Selectors.GetSelectedActionMap(state)?.wrappedProperty;
|
||||
var selectedObject = Selectors.GetSelectedActionMap(state)?.wrappedProperty;
|
||||
CopySelectedTreeViewItemsToClipboard(new List<SerializedProperty> {selectedObject}, typeof(InputActionMap), actionMap);
|
||||
}
|
||||
|
||||
public static void Copy(InputActionsEditorState state)
|
||||
{
|
||||
var actionMap = Selectors.GetSelectedActionMap(state)?.wrappedProperty;
|
||||
var selectedObject = Selectors.GetSelectedAction(state)?.wrappedProperty;
|
||||
var type = typeof(InputAction);
|
||||
if (state.selectionType == SelectionType.Binding)
|
||||
{
|
||||
selectedObject = Selectors.GetSelectedBinding(state)?.wrappedProperty;
|
||||
type = typeof(InputBinding);
|
||||
}
|
||||
CopySelectedTreeViewItemsToClipboard(new List<SerializedProperty> {selectedObject}, type, actionMap);
|
||||
}
|
||||
|
||||
private static void CopySelectedTreeViewItemsToClipboard(List<SerializedProperty> items, Type type, SerializedProperty actionMap = null)
|
||||
{
|
||||
var copyBuffer = new StringBuilder();
|
||||
CopyItems(items, copyBuffer, type, actionMap);
|
||||
EditorHelpers.SetSystemCopyBufferContents(copyBuffer.ToString());
|
||||
s_lastClipboardActionWasCut = false;
|
||||
}
|
||||
|
||||
internal static void CopyItems(List<SerializedProperty> items, StringBuilder buffer, Type type, SerializedProperty actionMap)
|
||||
{
|
||||
buffer.Append(k_CopyPasteMarker);
|
||||
buffer.Append(k_TypeMarker[type]);
|
||||
foreach (var item in items)
|
||||
{
|
||||
CopyItemData(item, buffer, type, actionMap);
|
||||
buffer.Append(k_EndOfTransmission);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CopyItemData(SerializedProperty item, StringBuilder buffer, Type type, SerializedProperty actionMap)
|
||||
{
|
||||
if (item == null)
|
||||
return;
|
||||
|
||||
buffer.Append(k_StartOfText);
|
||||
buffer.Append(item.CopyToJson(true));
|
||||
if (type == typeof(InputAction))
|
||||
AppendBindingDataForAction(buffer, actionMap, item);
|
||||
if (type == typeof(InputBinding) && IsComposite(item))
|
||||
AppendBindingDataForComposite(buffer, actionMap, item);
|
||||
}
|
||||
|
||||
private static void AppendBindingDataForAction(StringBuilder buffer, SerializedProperty actionMap, SerializedProperty item)
|
||||
{
|
||||
buffer.Append(k_BindingData);
|
||||
foreach (var binding in GetBindingsForActionInMap(actionMap, item))
|
||||
{
|
||||
buffer.Append(binding.CopyToJson(true));
|
||||
buffer.Append(k_EndOfBinding);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AppendBindingDataForComposite(StringBuilder buffer, SerializedProperty actionMap, SerializedProperty item)
|
||||
{
|
||||
var bindingsArray = actionMap.FindPropertyRelative(nameof(InputActionMap.m_Bindings));
|
||||
buffer.Append(k_BindingData);
|
||||
foreach (var binding in GetBindingsForComposite(bindingsArray, item.GetIndexOfArrayElement()))
|
||||
{
|
||||
buffer.Append(binding.CopyToJson(true));
|
||||
buffer.Append(k_EndOfBinding);
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<SerializedProperty> GetBindingsForActionInMap(SerializedProperty actionMap, SerializedProperty action)
|
||||
{
|
||||
var actionName = PropertyName(action);
|
||||
var bindingsArray = actionMap.FindPropertyRelative(nameof(InputActionMap.m_Bindings));
|
||||
var bindings = bindingsArray.Where(binding => binding.FindPropertyRelative("m_Action").stringValue.Equals(actionName));
|
||||
return bindings;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PasteChecks
|
||||
public static bool HasPastableClipboardData(Type selectedType)
|
||||
{
|
||||
var clipboard = EditorHelpers.GetSystemCopyBufferContents();
|
||||
if (clipboard.Length < k_CopyPasteMarker.Length)
|
||||
return false;
|
||||
var isInputAssetData = clipboard.StartsWith(k_CopyPasteMarker);
|
||||
return isInputAssetData && IsMatchingType(selectedType, GetCopiedClipboardType());
|
||||
}
|
||||
|
||||
private static bool IsMatchingType(Type selectedType, Type copiedType)
|
||||
{
|
||||
if (selectedType == typeof(InputActionMap))
|
||||
return copiedType == typeof(InputActionMap) || copiedType == typeof(InputAction);
|
||||
if (selectedType == typeof(InputAction))
|
||||
return copiedType == typeof(InputAction) || copiedType == typeof(InputBinding);
|
||||
//bindings and composites
|
||||
return copiedType == typeof(InputBinding);
|
||||
}
|
||||
|
||||
public static Type GetCopiedType(string buffer)
|
||||
{
|
||||
if (!buffer.StartsWith(k_CopyPasteMarker))
|
||||
return null;
|
||||
foreach (var typePair in k_TypeMarker)
|
||||
{
|
||||
if (buffer.Substring(k_CopyPasteMarker.Length).StartsWith(typePair.Value))
|
||||
return typePair.Key;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Type GetCopiedClipboardType()
|
||||
{
|
||||
return GetCopiedType(EditorHelpers.GetSystemCopyBufferContents());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Paste
|
||||
|
||||
public static SerializedProperty PasteActionMapsFromClipboard(InputActionsEditorState state)
|
||||
{
|
||||
s_lastAddedElement = null;
|
||||
var typeOfCopiedData = GetCopiedClipboardType();
|
||||
if (typeOfCopiedData != typeof(InputActionMap)) return null;
|
||||
s_State = state;
|
||||
var actionMapArray = state.serializedObject.FindProperty(nameof(InputActionAsset.m_ActionMaps));
|
||||
PasteData(EditorHelpers.GetSystemCopyBufferContents(), new[] {state.selectedActionMapIndex}, actionMapArray);
|
||||
|
||||
// Don't want to be able to paste repeatedly after a cut - ISX-1821
|
||||
if (s_lastAddedElement != null && s_lastClipboardActionWasCut)
|
||||
EditorHelpers.SetSystemCopyBufferContents(string.Empty);
|
||||
|
||||
return s_lastAddedElement;
|
||||
}
|
||||
|
||||
public static SerializedProperty PasteActionsOrBindingsFromClipboard(InputActionsEditorState state, bool addLast = false, int mapIndex = -1)
|
||||
{
|
||||
s_lastAddedElement = null;
|
||||
s_State = state;
|
||||
var typeOfCopiedData = GetCopiedClipboardType();
|
||||
if (typeOfCopiedData == typeof(InputAction))
|
||||
PasteActionsFromClipboard(state, addLast, mapIndex);
|
||||
if (typeOfCopiedData == typeof(InputBinding))
|
||||
PasteBindingsFromClipboard(state);
|
||||
|
||||
// Don't want to be able to paste repeatedly after a cut - ISX-1821
|
||||
if (s_lastAddedElement != null && s_lastClipboardActionWasCut)
|
||||
EditorHelpers.SetSystemCopyBufferContents(string.Empty);
|
||||
|
||||
return s_lastAddedElement;
|
||||
}
|
||||
|
||||
private static void PasteActionsFromClipboard(InputActionsEditorState state, bool addLast, int mapIndex)
|
||||
{
|
||||
var actionMap = mapIndex >= 0 ? Selectors.GetActionMapAtIndex(state, mapIndex)?.wrappedProperty
|
||||
: Selectors.GetSelectedActionMap(state)?.wrappedProperty;
|
||||
var actionArray = actionMap?.FindPropertyRelative(nameof(InputActionMap.m_Actions));
|
||||
if (actionArray == null) return;
|
||||
var index = state.selectedActionIndex;
|
||||
if (addLast)
|
||||
index = actionArray.arraySize - 1;
|
||||
PasteData(EditorHelpers.GetSystemCopyBufferContents(), new[] {index}, actionArray);
|
||||
}
|
||||
|
||||
private static void PasteBindingsFromClipboard(InputActionsEditorState state)
|
||||
{
|
||||
var actionMap = Selectors.GetSelectedActionMap(state)?.wrappedProperty;
|
||||
var bindingsArray = actionMap?.FindPropertyRelative(nameof(InputActionMap.m_Bindings));
|
||||
if (bindingsArray == null) return;
|
||||
int newBindingIndex;
|
||||
if (state.selectionType == SelectionType.Action)
|
||||
newBindingIndex = Selectors.GetLastBindingIndexForSelectedAction(state);
|
||||
else
|
||||
newBindingIndex = state.selectedBindingIndex;
|
||||
|
||||
PasteData(EditorHelpers.GetSystemCopyBufferContents(), new[] { newBindingIndex }, bindingsArray);
|
||||
}
|
||||
|
||||
private static void PasteData(string copyBufferString, int[] indicesToInsert, SerializedProperty arrayToInsertInto)
|
||||
{
|
||||
if (!copyBufferString.StartsWith(k_CopyPasteMarker))
|
||||
return;
|
||||
PasteItems(copyBufferString, indicesToInsert, arrayToInsertInto);
|
||||
}
|
||||
|
||||
internal static void PasteItems(string copyBufferString, int[] indicesToInsert, SerializedProperty arrayToInsertInto)
|
||||
{
|
||||
// Split buffer into transmissions and then into transmission blocks
|
||||
var copiedType = GetCopiedType(copyBufferString);
|
||||
int indexOffset = 0;
|
||||
// If the array is empty, make sure we insert at index 0
|
||||
if (arrayToInsertInto.arraySize == 0)
|
||||
indexOffset = -1;
|
||||
foreach (var transmission in copyBufferString.Substring(k_CopyPasteMarker.Length + k_TypeMarker[copiedType].Length)
|
||||
.Split(new[] {k_EndOfTransmission}, StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
indexOffset++;
|
||||
foreach (var index in indicesToInsert)
|
||||
PasteBlocks(transmission, index + indexOffset, arrayToInsertInto, copiedType);
|
||||
}
|
||||
}
|
||||
|
||||
private static void PasteBlocks(string transmission, int indexToInsert, SerializedProperty arrayToInsertInto, Type copiedType)
|
||||
{
|
||||
var block = transmission.Substring(transmission.IndexOf(k_StartOfText, StringComparison.Ordinal) + 1);
|
||||
if (copiedType == typeof(InputActionMap))
|
||||
PasteElement(arrayToInsertInto, block, indexToInsert, out _);
|
||||
else if (copiedType == typeof(InputAction))
|
||||
PasteAction(arrayToInsertInto, block, indexToInsert);
|
||||
else
|
||||
{
|
||||
var actionName = Selectors.GetSelectedBinding(s_State)?.wrappedProperty.FindPropertyRelative("m_Action")
|
||||
.stringValue;
|
||||
|
||||
if (s_State.selectionType == SelectionType.Action)
|
||||
{
|
||||
SerializedProperty property = Selectors.GetSelectedAction(s_State)?.wrappedProperty;
|
||||
if (property == null)
|
||||
return;
|
||||
actionName = PropertyName(property);
|
||||
}
|
||||
if (actionName == null)
|
||||
return;
|
||||
|
||||
PasteBindingOrComposite(arrayToInsertInto, block, indexToInsert, actionName);
|
||||
}
|
||||
}
|
||||
|
||||
private static SerializedProperty PasteElement(SerializedProperty arrayProperty, string json, int index, out string oldId, string name = "newElement", bool changeName = true, bool assignUniqueIDs = true)
|
||||
{
|
||||
var duplicatedProperty = AddElement(arrayProperty, name, index);
|
||||
duplicatedProperty.RestoreFromJson(json);
|
||||
oldId = duplicatedProperty.FindPropertyRelative("m_Id").stringValue;
|
||||
if (changeName)
|
||||
InputActionSerializationHelpers.EnsureUniqueName(duplicatedProperty);
|
||||
if (assignUniqueIDs)
|
||||
InputActionSerializationHelpers.AssignUniqueIDs(duplicatedProperty);
|
||||
s_lastAddedElement = duplicatedProperty;
|
||||
return duplicatedProperty;
|
||||
}
|
||||
|
||||
private static void PasteAction(SerializedProperty arrayProperty, string jsonToInsert, int indexToInsert)
|
||||
{
|
||||
var json = jsonToInsert.Split(k_BindingData, StringSplitOptions.RemoveEmptyEntries);
|
||||
var bindingJsons = new string[] {};
|
||||
if (json.Length > 1)
|
||||
bindingJsons = json[1].Split(k_EndOfBinding, StringSplitOptions.RemoveEmptyEntries);
|
||||
var property = PasteElement(arrayProperty, json[0], indexToInsert, out _, "");
|
||||
var newName = PropertyName(property);
|
||||
var newId = property.FindPropertyRelative("m_Id").stringValue;
|
||||
var actionMapTo = Selectors.GetActionMapForAction(s_State, newId);
|
||||
var bindingArrayToInsertTo = actionMapTo.FindPropertyRelative(nameof(InputActionMap.m_Bindings));
|
||||
var index = Mathf.Clamp(Selectors.GetBindingIndexBeforeAction(arrayProperty, indexToInsert, bindingArrayToInsertTo), 0, bindingArrayToInsertTo.arraySize);
|
||||
foreach (var bindingJson in bindingJsons)
|
||||
{
|
||||
var newIndex = PasteBindingOrComposite(bindingArrayToInsertTo, bindingJson, index, newName, false);
|
||||
index = newIndex;
|
||||
}
|
||||
s_lastAddedElement = property;
|
||||
}
|
||||
|
||||
private static int PasteBindingOrComposite(SerializedProperty arrayProperty, string json, int index, string actionName, bool createCompositeParts = true)
|
||||
{
|
||||
var pastePartOfComposite = IsPartOfComposite(json);
|
||||
bool currentPartOfComposite = false;
|
||||
bool currentIsComposite = false;
|
||||
|
||||
if (arrayProperty.arraySize == 0)
|
||||
index = 0;
|
||||
|
||||
if (index > 0)
|
||||
{
|
||||
var currentProperty = arrayProperty.GetArrayElementAtIndex(index - 1);
|
||||
currentPartOfComposite = IsPartOfComposite(currentProperty);
|
||||
currentIsComposite = IsComposite(currentProperty) || currentPartOfComposite;
|
||||
if (pastePartOfComposite && !currentIsComposite) //prevent pasting part of composite into non-composite
|
||||
return index;
|
||||
}
|
||||
|
||||
// Update the target index for special cases when pasting a Binding
|
||||
if (s_State.selectionType != SelectionType.Action && createCompositeParts)
|
||||
{
|
||||
// - Pasting into a Composite with CompositePart not the target, i.e. Composite "root" selected, paste at the end of the composite
|
||||
// - Pasting a non-CompositePart, i.e. regular Binding, needs to skip all the CompositeParts (if any)
|
||||
if ((pastePartOfComposite && !currentPartOfComposite) || !pastePartOfComposite)
|
||||
index = Selectors.GetSelectedBindingIndexAfterCompositeBindings(s_State) + 1;
|
||||
}
|
||||
|
||||
if (json.Contains(k_BindingData)) //copied data is composite with bindings - only true for directly copied composites, not for composites from copied actions
|
||||
return PasteCompositeFromJson(arrayProperty, json, index, actionName);
|
||||
var property = PasteElement(arrayProperty, json, index, out var oldId, "", false);
|
||||
if (IsComposite(property))
|
||||
return PasteComposite(arrayProperty, property, PropertyName(property), actionName, index, oldId, createCompositeParts); //Paste composites copied with actions
|
||||
property.FindPropertyRelative("m_Action").stringValue = actionName;
|
||||
return index + 1;
|
||||
}
|
||||
|
||||
private static int PasteComposite(SerializedProperty bindingsArray, SerializedProperty duplicatedComposite, string name, string actionName, int index, string oldId, bool createCompositeParts)
|
||||
{
|
||||
duplicatedComposite.FindPropertyRelative("m_Name").stringValue = name;
|
||||
duplicatedComposite.FindPropertyRelative("m_Action").stringValue = actionName;
|
||||
if (createCompositeParts)
|
||||
{
|
||||
var composite = Selectors.GetBindingForId(s_State, oldId, out var bindingsFrom);
|
||||
var bindings = GetBindingsForComposite(bindingsFrom, composite.GetIndexOfArrayElement());
|
||||
PastePartsOfComposite(bindingsArray, bindings, ++index, actionName);
|
||||
}
|
||||
return index + 1;
|
||||
}
|
||||
|
||||
private static int PastePartsOfComposite(SerializedProperty bindingsToInsertTo, List<SerializedProperty> bindingsOfComposite, int index, string actionName)
|
||||
{
|
||||
foreach (var binding in bindingsOfComposite)
|
||||
{
|
||||
var newBinding = DuplicateElement(bindingsToInsertTo, binding, PropertyName(binding), index++, false);
|
||||
newBinding.FindPropertyRelative("m_Action").stringValue = actionName;
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
private static int PasteCompositeFromJson(SerializedProperty arrayProperty, string json, int index, string actionName)
|
||||
{
|
||||
var jsons = json.Split(k_BindingData, StringSplitOptions.RemoveEmptyEntries);
|
||||
var property = PasteElement(arrayProperty, jsons[0], index, out _, "", false);
|
||||
var bindingJsons = jsons[1].Split(k_EndOfBinding, StringSplitOptions.RemoveEmptyEntries);
|
||||
property.FindPropertyRelative("m_Action").stringValue = actionName;
|
||||
foreach (var bindingJson in bindingJsons)
|
||||
PasteBindingOrComposite(arrayProperty, bindingJson, ++index, actionName, false);
|
||||
return index + 1;
|
||||
}
|
||||
|
||||
private static bool IsPartOfComposite(string json)
|
||||
{
|
||||
if (!json.Contains("m_Flags") || json.Contains(k_BindingData))
|
||||
return false;
|
||||
var ob = JsonUtility.FromJson<InputBinding>(json);
|
||||
return ob.m_Flags == InputBinding.Flags.PartOfComposite;
|
||||
}
|
||||
|
||||
private static SerializedProperty AddElement(SerializedProperty arrayProperty, string name, int index = -1)
|
||||
{
|
||||
var uniqueName = InputActionSerializationHelpers.FindUniqueName(arrayProperty, name);
|
||||
if (index < 0)
|
||||
index = arrayProperty.arraySize;
|
||||
|
||||
arrayProperty.InsertArrayElementAtIndex(index);
|
||||
var elementProperty = arrayProperty.GetArrayElementAtIndex(index);
|
||||
elementProperty.ResetValuesToDefault();
|
||||
|
||||
elementProperty.FindPropertyRelative("m_Name").stringValue = uniqueName;
|
||||
elementProperty.FindPropertyRelative("m_Id").stringValue = Guid.NewGuid().ToString();
|
||||
|
||||
return elementProperty;
|
||||
}
|
||||
|
||||
public static int DeleteCutElements(InputActionsEditorState state)
|
||||
{
|
||||
if (!state.hasCutElements)
|
||||
return -1;
|
||||
var cutElements = state.GetCutElements();
|
||||
var index = state.selectedActionMapIndex;
|
||||
if (cutElements[0].type == typeof(InputAction))
|
||||
index = state.selectedActionIndex;
|
||||
else if (cutElements[0].type == typeof(InputBinding))
|
||||
index = state.selectionType == SelectionType.Binding ? state.selectedBindingIndex : state.selectedActionIndex;
|
||||
|
||||
foreach (var cutElement in cutElements)
|
||||
{
|
||||
var cutIndex = cutElement.GetIndexOfProperty(state);
|
||||
var actionMapIndex = cutElement.actionMapIndex(state);
|
||||
var actionMap = Selectors.GetActionMapAtIndex(state, actionMapIndex)?.wrappedProperty;
|
||||
var isInsertBindingIntoAction = cutElement.type == typeof(InputBinding) && state.selectionType == SelectionType.Action;
|
||||
if (cutElement.type == typeof(InputBinding) || cutElement.type == typeof(InputAction))
|
||||
{
|
||||
if (cutElement.type == typeof(InputAction))
|
||||
{
|
||||
var action = Selectors.GetActionForIndex(actionMap, cutIndex);
|
||||
var id = InputActionSerializationHelpers.GetId(action);
|
||||
InputActionSerializationHelpers.DeleteActionAndBindings(actionMap, id);
|
||||
}
|
||||
else
|
||||
{
|
||||
var binding = Selectors.GetCompositeOrBindingInMap(actionMap, cutIndex).wrappedProperty;
|
||||
if (binding.FindPropertyRelative("m_Flags").intValue == (int)InputBinding.Flags.Composite && !isInsertBindingIntoAction)
|
||||
index -= InputActionSerializationHelpers.GetCompositePartCount(Selectors.GetSelectedActionMap(state)?.wrappedProperty.FindPropertyRelative(nameof(InputActionMap.m_Bindings)), cutIndex);
|
||||
InputActionSerializationHelpers.DeleteBinding(binding, actionMap);
|
||||
}
|
||||
if (cutIndex <= index && actionMapIndex == state.selectedActionMapIndex && !isInsertBindingIntoAction)
|
||||
index--;
|
||||
}
|
||||
else if (cutElement.type == typeof(InputActionMap))
|
||||
{
|
||||
InputActionSerializationHelpers.DeleteActionMap(state.serializedObject, InputActionSerializationHelpers.GetId(actionMap));
|
||||
if (cutIndex <= index)
|
||||
index--;
|
||||
}
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Duplicate
|
||||
public static void DuplicateAction(SerializedProperty arrayProperty, SerializedProperty toDuplicate, SerializedProperty actionMap, InputActionsEditorState state)
|
||||
{
|
||||
s_State = state;
|
||||
var buffer = new StringBuilder();
|
||||
buffer.Append(toDuplicate.CopyToJson(true));
|
||||
AppendBindingDataForAction(buffer, actionMap, toDuplicate);
|
||||
PasteAction(arrayProperty, buffer.ToString(), toDuplicate.GetIndexOfArrayElement() + 1);
|
||||
}
|
||||
|
||||
public static int DuplicateBinding(SerializedProperty arrayProperty, SerializedProperty toDuplicate, string newActionName, int index)
|
||||
{
|
||||
if (IsComposite(toDuplicate))
|
||||
return DuplicateComposite(arrayProperty, toDuplicate, PropertyName(toDuplicate), newActionName, index, out _).GetIndexOfArrayElement();
|
||||
var binding = DuplicateElement(arrayProperty, toDuplicate, newActionName, index, false);
|
||||
binding.FindPropertyRelative("m_Action").stringValue = newActionName;
|
||||
return index;
|
||||
}
|
||||
|
||||
private static SerializedProperty DuplicateComposite(SerializedProperty bindingsArray, SerializedProperty compositeToDuplicate, string name, string actionName, int index, out int newIndex, bool increaseIndex = true)
|
||||
{
|
||||
if (increaseIndex)
|
||||
index += InputActionSerializationHelpers.GetCompositePartCount(bindingsArray, compositeToDuplicate.GetIndexOfArrayElement());
|
||||
var newComposite = DuplicateElement(bindingsArray, compositeToDuplicate, name, index++, false);
|
||||
newComposite.FindPropertyRelative("m_Action").stringValue = actionName;
|
||||
var bindings = GetBindingsForComposite(bindingsArray, compositeToDuplicate.GetIndexOfArrayElement());
|
||||
newIndex = PastePartsOfComposite(bindingsArray, bindings, index, actionName);
|
||||
return newComposite;
|
||||
}
|
||||
|
||||
public static SerializedProperty DuplicateElement(SerializedProperty arrayProperty, SerializedProperty toDuplicate, string name, int index, bool changeName = true)
|
||||
{
|
||||
var json = toDuplicate.CopyToJson(true);
|
||||
return PasteElement(arrayProperty, json, index, out _, name, changeName);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
internal static List<SerializedProperty> GetBindingsForComposite(SerializedProperty bindingsArray, int indexOfComposite)
|
||||
{
|
||||
var compositeBindings = new List<SerializedProperty>();
|
||||
var compositeStartIndex = InputActionSerializationHelpers.GetCompositeStartIndex(bindingsArray, indexOfComposite);
|
||||
if (compositeStartIndex == -1)
|
||||
return compositeBindings;
|
||||
|
||||
for (var i = compositeStartIndex + 1; i < bindingsArray.arraySize; ++i)
|
||||
{
|
||||
var bindingProperty = bindingsArray.GetArrayElementAtIndex(i);
|
||||
var bindingFlags = (InputBinding.Flags)bindingProperty.FindPropertyRelative("m_Flags").intValue;
|
||||
if ((bindingFlags & InputBinding.Flags.PartOfComposite) == 0)
|
||||
break;
|
||||
compositeBindings.Add(bindingProperty);
|
||||
}
|
||||
return compositeBindings;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d384a7a7180814f16aa8fa9d86ac3b6c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,77 @@
|
||||
#if UNITY_EDITOR
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Class to handle drop from the actions tree view to the action maps list view.
|
||||
/// </summary>
|
||||
internal class DropManipulator : Manipulator
|
||||
{
|
||||
private EventCallback<int> DroppedPerformedCallback;
|
||||
private VisualElement m_OtherVerticalList;
|
||||
private ListView listView => target as ListView;
|
||||
private TreeView treeView => m_OtherVerticalList as TreeView;
|
||||
|
||||
public DropManipulator(EventCallback<int> droppedPerformedCallback, VisualElement otherVerticalList)
|
||||
{
|
||||
DroppedPerformedCallback = droppedPerformedCallback;
|
||||
m_OtherVerticalList = otherVerticalList;
|
||||
}
|
||||
|
||||
protected override void RegisterCallbacksOnTarget()
|
||||
{
|
||||
m_OtherVerticalList.RegisterCallback<DragUpdatedEvent>(OnDragUpdatedEvent, TrickleDown.TrickleDown);
|
||||
m_OtherVerticalList.RegisterCallback<DragPerformEvent>(OnDragPerformEvent, TrickleDown.TrickleDown);
|
||||
}
|
||||
|
||||
protected override void UnregisterCallbacksFromTarget()
|
||||
{
|
||||
m_OtherVerticalList.UnregisterCallback<DragUpdatedEvent>(OnDragUpdatedEvent, TrickleDown.TrickleDown);
|
||||
m_OtherVerticalList.UnregisterCallback<DragPerformEvent>(OnDragPerformEvent, TrickleDown.TrickleDown);
|
||||
}
|
||||
|
||||
private void OnDragPerformEvent(DragPerformEvent evt)
|
||||
{
|
||||
var mapsViewItem = target.panel.Pick(evt.mousePosition)?.GetFirstAncestorOfType<InputActionMapsTreeViewItem>();
|
||||
if (mapsViewItem == null)
|
||||
return;
|
||||
var index = treeView.selectedIndices.First();
|
||||
var draggedItem = treeView.GetItemDataForIndex<ActionOrBindingData>(index);
|
||||
if (!draggedItem.isAction)
|
||||
return;
|
||||
evt.StopImmediatePropagation();
|
||||
DragAndDrop.AcceptDrag();
|
||||
DroppedPerformedCallback.Invoke((int)mapsViewItem.userData);
|
||||
Reset();
|
||||
treeView.ReleaseMouse();
|
||||
}
|
||||
|
||||
private int m_InitialIndex = -1;
|
||||
private void OnDragUpdatedEvent(DragUpdatedEvent evt)
|
||||
{
|
||||
var mapsViewItem = target.panel.Pick(evt.mousePosition)?.GetFirstAncestorOfType<InputActionMapsTreeViewItem>();
|
||||
if (mapsViewItem != null)
|
||||
{
|
||||
if (m_InitialIndex < 0 && listView != null)
|
||||
m_InitialIndex = listView.selectedIndex;
|
||||
//select map item to visualize the drop
|
||||
listView?.SetSelectionWithoutNotify(new[] { (int)mapsViewItem.userData }); //the user data contains the index of the map item
|
||||
DragAndDrop.visualMode = DragAndDropVisualMode.Move;
|
||||
evt.StopImmediatePropagation();
|
||||
}
|
||||
else
|
||||
Reset();
|
||||
}
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
if (m_InitialIndex >= 0)
|
||||
listView?.SetSelectionWithoutNotify(new[] {m_InitialIndex}); //select the initial action map again
|
||||
m_InitialIndex = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b52a4b3b22334b02a4d22d942a479be6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,12 @@
|
||||
#if UNITY_EDITOR
|
||||
using System.Collections;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
internal interface IViewStateCollection : IEnumerable
|
||||
{
|
||||
bool SequenceEqual(IViewStateCollection other);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 49a488c9c543d82408e3f15c90d36980
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8b82aecdbfbdd1b49b90eb0d509b4166
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,287 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
interface IPasteListener
|
||||
{
|
||||
void OnPaste(InputActionsEditorState state);
|
||||
}
|
||||
internal class InputActionsEditorView : ViewBase<InputActionsEditorView.ViewState>, IPasteListener
|
||||
{
|
||||
private const string saveButtonId = "save-asset-toolbar-button";
|
||||
private const string autoSaveToggleId = "auto-save-toolbar-toggle";
|
||||
private const string menuButtonId = "asset-menu";
|
||||
|
||||
private readonly ToolbarMenu m_ControlSchemesToolbar;
|
||||
private readonly ToolbarMenu m_DevicesToolbar;
|
||||
private readonly ToolbarButton m_SaveButton;
|
||||
|
||||
private readonly Action m_SaveAction;
|
||||
|
||||
public InputActionsEditorView(VisualElement root, StateContainer stateContainer, bool isProjectSettings,
|
||||
Action saveAction)
|
||||
: base(root, stateContainer)
|
||||
{
|
||||
m_SaveAction = saveAction;
|
||||
|
||||
var mainEditorAsset = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
|
||||
InputActionsEditorConstants.PackagePath +
|
||||
InputActionsEditorConstants.ResourcesPath +
|
||||
InputActionsEditorConstants.MainEditorViewNameUxml);
|
||||
|
||||
mainEditorAsset.CloneTree(root);
|
||||
var actionsTreeView = new ActionsTreeView(root, stateContainer);
|
||||
CreateChildView(new ActionMapsView(root, stateContainer));
|
||||
CreateChildView(actionsTreeView);
|
||||
CreateChildView(new PropertiesView(root, stateContainer));
|
||||
|
||||
m_ControlSchemesToolbar = root.Q<ToolbarMenu>("control-schemes-toolbar-menu");
|
||||
m_ControlSchemesToolbar.menu.AppendAction("Add Control Scheme...", _ => AddOrUpdateControlScheme(root));
|
||||
m_ControlSchemesToolbar.menu.AppendAction("Edit Control Scheme...", _ => AddOrUpdateControlScheme(root, true), DropdownMenuAction.Status.Disabled);
|
||||
m_ControlSchemesToolbar.menu.AppendAction("Duplicate Control Scheme...", _ => DuplicateControlScheme(root), DropdownMenuAction.Status.Disabled);
|
||||
m_ControlSchemesToolbar.menu.AppendAction("Delete Control Scheme...", DeleteControlScheme, DropdownMenuAction.Status.Disabled);
|
||||
|
||||
m_DevicesToolbar = root.Q<ToolbarMenu>("control-schemes-filter-toolbar-menu");
|
||||
m_DevicesToolbar.SetEnabled(false);
|
||||
|
||||
m_SaveButton = root.Q<ToolbarButton>(name: saveButtonId);
|
||||
m_SaveButton.SetEnabled(InputEditorUserSettings.autoSaveInputActionAssets == false);
|
||||
m_SaveButton.clicked += OnSaveButton;
|
||||
|
||||
var autoSaveToggle = root.Q<ToolbarToggle>(name: autoSaveToggleId);
|
||||
autoSaveToggle.value = InputEditorUserSettings.autoSaveInputActionAssets;
|
||||
autoSaveToggle.RegisterValueChangedCallback(OnAutoSaveToggle);
|
||||
|
||||
// Hide save toolbar if there is no save action provided since we cannot support it
|
||||
if (saveAction == null)
|
||||
{
|
||||
var element = root.Q("save-asset-toolbar-container");
|
||||
if (element != null)
|
||||
{
|
||||
element.style.visibility = Visibility.Hidden;
|
||||
element.style.display = DisplayStyle.None;
|
||||
}
|
||||
}
|
||||
|
||||
VisualElement assetMenuButton = null;
|
||||
try
|
||||
{
|
||||
// This only exists in the project settings version
|
||||
assetMenuButton = root.Q<VisualElement>(name: menuButtonId);
|
||||
}
|
||||
catch {}
|
||||
|
||||
if (assetMenuButton != null)
|
||||
{
|
||||
assetMenuButton.visible = isProjectSettings;
|
||||
assetMenuButton.AddToClassList(EditorGUIUtility.isProSkin ? "asset-menu-button-dark-theme" : "asset-menu-button");
|
||||
var _ = new ContextualMenuManipulator(menuEvent =>
|
||||
{
|
||||
menuEvent.menu.AppendAction("Reset to Defaults", _ => OnReset());
|
||||
menuEvent.menu.AppendAction("Remove All Action Maps", _ => OnClearActionMaps());
|
||||
})
|
||||
{ target = assetMenuButton, activators = { new ManipulatorActivationFilter() { button = MouseButton.LeftMouse } } };
|
||||
}
|
||||
|
||||
// only register the state changed event here in the parent. Changes will be cascaded
|
||||
// into child views.
|
||||
stateContainer.StateChanged += OnStateChanged;
|
||||
|
||||
CreateSelector(
|
||||
s => s.selectedControlSchemeIndex,
|
||||
s => new ViewStateCollection<InputControlScheme>(Selectors.GetControlSchemes(s)),
|
||||
(_, controlSchemes, state) => new ViewState
|
||||
{
|
||||
controlSchemes = controlSchemes,
|
||||
selectedControlSchemeIndex = state.selectedControlSchemeIndex,
|
||||
selectedDeviceIndex = state.selectedDeviceRequirementIndex
|
||||
});
|
||||
|
||||
s_OnPasteCutElements.Add(this);
|
||||
}
|
||||
|
||||
private void OnReset()
|
||||
{
|
||||
Dispatch(Commands.ReplaceActionMaps(ProjectWideActionsAsset.GetDefaultAssetJson()));
|
||||
}
|
||||
|
||||
private void OnClearActionMaps()
|
||||
{
|
||||
Dispatch(Commands.ClearActionMaps());
|
||||
}
|
||||
|
||||
private void OnSaveButton()
|
||||
{
|
||||
Dispatch(Commands.SaveAsset(m_SaveAction));
|
||||
|
||||
// Don't let focus linger after clicking (ISX-1482). Ideally this would be only applied on mouse click,
|
||||
// rather than if the user is using tab to navigate UI, but there doesn't seem to be a way to differentiate
|
||||
// between those interactions at the moment.
|
||||
m_SaveButton.Blur();
|
||||
}
|
||||
|
||||
private void OnAutoSaveToggle(ChangeEvent<bool> evt)
|
||||
{
|
||||
Dispatch(Commands.ToggleAutoSave(evt.newValue, m_SaveAction));
|
||||
}
|
||||
|
||||
public override void RedrawUI(ViewState viewState)
|
||||
{
|
||||
SetUpControlSchemesMenu(viewState);
|
||||
SetUpDevicesMenu(viewState);
|
||||
m_SaveButton.SetEnabled(InputEditorUserSettings.autoSaveInputActionAssets == false);
|
||||
}
|
||||
|
||||
private string SetupControlSchemeName(string name)
|
||||
{
|
||||
//On Windows the '&' is considered an accelerator character and will always be stripped.
|
||||
//Since the ControlScheme menu isn't creating hotkeys, it can be safely assumed that they are meant to be text
|
||||
//so we want to escape the character for MenuItem
|
||||
if (Application.platform == RuntimePlatform.WindowsEditor)
|
||||
{
|
||||
name = name.Replace("&", "&&");
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
private void SetUpControlSchemesMenu(ViewState viewState)
|
||||
{
|
||||
m_ControlSchemesToolbar.menu.MenuItems().Clear();
|
||||
|
||||
if (viewState.controlSchemes.Any())
|
||||
{
|
||||
m_ControlSchemesToolbar.text = viewState.selectedControlSchemeIndex == -1
|
||||
? "All Control Schemes"
|
||||
: viewState.controlSchemes.ElementAt(viewState.selectedControlSchemeIndex).name;
|
||||
|
||||
m_ControlSchemesToolbar.menu.AppendAction("All Control Schemes", _ => SelectControlScheme(-1),
|
||||
viewState.selectedControlSchemeIndex == -1 ? DropdownMenuAction.Status.Checked : DropdownMenuAction.Status.Normal);
|
||||
viewState.controlSchemes.ForEach((scheme, i) =>
|
||||
m_ControlSchemesToolbar.menu.AppendAction(SetupControlSchemeName(scheme.name), _ => SelectControlScheme(i),
|
||||
viewState.selectedControlSchemeIndex == i ? DropdownMenuAction.Status.Checked : DropdownMenuAction.Status.Normal));
|
||||
m_ControlSchemesToolbar.menu.AppendSeparator();
|
||||
}
|
||||
|
||||
m_ControlSchemesToolbar.menu.AppendAction("Add Control Scheme...", _ => AddOrUpdateControlScheme(rootElement));
|
||||
m_ControlSchemesToolbar.menu.AppendAction("Edit Control Scheme...", _ => AddOrUpdateControlScheme(rootElement, true),
|
||||
viewState.selectedControlSchemeIndex != -1 ? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Disabled);
|
||||
m_ControlSchemesToolbar.menu.AppendAction("Duplicate Control Scheme...", _ => DuplicateControlScheme(rootElement),
|
||||
viewState.selectedControlSchemeIndex != -1 ? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Disabled);
|
||||
m_ControlSchemesToolbar.menu.AppendAction("Delete Control Scheme...", DeleteControlScheme,
|
||||
viewState.selectedControlSchemeIndex != -1 ? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Disabled);
|
||||
}
|
||||
|
||||
private void SetUpDevicesMenu(ViewState viewState)
|
||||
{
|
||||
if (!viewState.controlSchemes.Any() || viewState.selectedControlSchemeIndex == -1)
|
||||
{
|
||||
m_DevicesToolbar.text = "All Devices";
|
||||
m_DevicesToolbar.SetEnabled(false);
|
||||
return;
|
||||
}
|
||||
m_DevicesToolbar.SetEnabled(true);
|
||||
var currentControlScheme = viewState.controlSchemes.ElementAt(viewState.selectedControlSchemeIndex);
|
||||
if (viewState.selectedDeviceIndex == -1)
|
||||
m_DevicesToolbar.text = "All Devices";
|
||||
|
||||
m_DevicesToolbar.menu.MenuItems().Clear();
|
||||
m_DevicesToolbar.menu.AppendAction("All Devices", _ => SelectDevice(-1), viewState.selectedDeviceIndex == -1
|
||||
? DropdownMenuAction.Status.Checked
|
||||
: DropdownMenuAction.Status.Normal);
|
||||
currentControlScheme.deviceRequirements.ForEach(
|
||||
(device, i) =>
|
||||
{
|
||||
InputControlPath.ToHumanReadableString(device.controlPath, out var name, out _);
|
||||
m_DevicesToolbar.menu.AppendAction(name, _ => SelectDevice(i),
|
||||
viewState.selectedDeviceIndex == i
|
||||
? DropdownMenuAction.Status.Checked
|
||||
: DropdownMenuAction.Status.Normal);
|
||||
if (viewState.selectedDeviceIndex == i)
|
||||
m_DevicesToolbar.text = name;
|
||||
});
|
||||
}
|
||||
|
||||
private void AddOrUpdateControlScheme(VisualElement parent, bool updateExisting = false)
|
||||
{
|
||||
if (!updateExisting)
|
||||
Dispatch(ControlSchemeCommands.AddNewControlScheme());
|
||||
|
||||
ShowControlSchemeEditor(parent, updateExisting);
|
||||
}
|
||||
|
||||
private void DuplicateControlScheme(VisualElement parent)
|
||||
{
|
||||
Dispatch(ControlSchemeCommands.DuplicateSelectedControlScheme());
|
||||
ShowControlSchemeEditor(parent);
|
||||
}
|
||||
|
||||
private void DeleteControlScheme(DropdownMenuAction obj)
|
||||
{
|
||||
Dispatch(ControlSchemeCommands.DeleteSelectedControlScheme());
|
||||
}
|
||||
|
||||
private void ShowControlSchemeEditor(VisualElement parent, bool updateExisting = false)
|
||||
{
|
||||
var controlSchemesView = CreateChildView(new ControlSchemesView(parent, stateContainer, updateExisting));
|
||||
controlSchemesView.UpdateView(stateContainer.GetState());
|
||||
|
||||
controlSchemesView.OnClosing += _ => DestroyChildView(controlSchemesView);
|
||||
}
|
||||
|
||||
private void SelectControlScheme(int controlSchemeIndex)
|
||||
{
|
||||
Dispatch(ControlSchemeCommands.SelectControlScheme(controlSchemeIndex));
|
||||
SelectDevice(-1);
|
||||
}
|
||||
|
||||
private void SelectDevice(int deviceIndex)
|
||||
{
|
||||
Dispatch(ControlSchemeCommands.SelectDeviceRequirement(deviceIndex));
|
||||
}
|
||||
|
||||
public class ViewState
|
||||
{
|
||||
public IEnumerable<InputControlScheme> controlSchemes;
|
||||
public int selectedControlSchemeIndex;
|
||||
public int selectedDeviceIndex;
|
||||
}
|
||||
|
||||
internal static List<IPasteListener> s_OnPasteCutElements = new();
|
||||
|
||||
public override void DestroyView()
|
||||
{
|
||||
base.DestroyView();
|
||||
s_OnPasteCutElements.Remove(this);
|
||||
}
|
||||
|
||||
public void OnPaste(InputActionsEditorState state)
|
||||
{
|
||||
if (state.Equals(stateContainer.GetState()))
|
||||
return;
|
||||
Dispatch(Commands.DeleteCutElements());
|
||||
}
|
||||
}
|
||||
|
||||
internal static partial class Selectors
|
||||
{
|
||||
public static IEnumerable<InputControlScheme> GetControlSchemes(InputActionsEditorState state)
|
||||
{
|
||||
var controlSchemesArray = state.serializedObject.FindProperty(nameof(InputActionAsset.m_ControlSchemes));
|
||||
if (controlSchemesArray == null)
|
||||
yield break;
|
||||
|
||||
foreach (SerializedProperty controlScheme in controlSchemesArray)
|
||||
{
|
||||
yield return new InputControlScheme(controlScheme);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 392e37dde5bf4a14bb315f2b4829bd6b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a439a21d627cd7646978dd78132c4bb9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,206 @@
|
||||
#if UNITY_EDITOR
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine.UIElements;
|
||||
using UnityEngine.InputSystem.Layouts;
|
||||
using UnityEngine.InputSystem.Utilities;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
internal class MatchingControlPath
|
||||
{
|
||||
public string deviceName
|
||||
{
|
||||
get;
|
||||
}
|
||||
public string controlName
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public bool isRoot
|
||||
{
|
||||
get;
|
||||
}
|
||||
public List<MatchingControlPath> children
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
|
||||
public MatchingControlPath(string deviceName, string controlName, bool isRoot)
|
||||
{
|
||||
this.deviceName = deviceName;
|
||||
this.controlName = controlName;
|
||||
this.isRoot = isRoot;
|
||||
this.children = new List<MatchingControlPath>();
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
public static List<TreeViewItemData<MatchingControlPath>> BuildMatchingControlPathsTreeData(List<MatchingControlPath> matchingControlPaths)
|
||||
{
|
||||
int id = 0;
|
||||
return BuildMatchingControlPathsTreeDataRecursive(ref id, matchingControlPaths);
|
||||
}
|
||||
|
||||
private static List<TreeViewItemData<MatchingControlPath>> BuildMatchingControlPathsTreeDataRecursive(ref int id, List<MatchingControlPath> matchingControlPaths)
|
||||
{
|
||||
var treeViewList = new List<TreeViewItemData<MatchingControlPath>>(matchingControlPaths.Count);
|
||||
foreach (var matchingControlPath in matchingControlPaths)
|
||||
{
|
||||
var childTreeViewList = BuildMatchingControlPathsTreeDataRecursive(ref id, matchingControlPath.children);
|
||||
|
||||
var treeViewItem = new TreeViewItemData<MatchingControlPath>(id++, matchingControlPath, childTreeViewList);
|
||||
treeViewList.Add(treeViewItem);
|
||||
}
|
||||
|
||||
return treeViewList;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
public static List<MatchingControlPath> CollectMatchingControlPaths(string path, bool showPaths, ref bool controlPathUsagePresent)
|
||||
{
|
||||
var matchingControlPaths = new List<MatchingControlPath>();
|
||||
|
||||
if (path == string.Empty)
|
||||
return matchingControlPaths;
|
||||
|
||||
var deviceLayoutPath = InputControlPath.TryGetDeviceLayout(path);
|
||||
var parsedPath = InputControlPath.Parse(path).ToArray();
|
||||
|
||||
// If the provided path is parseable into device and control components, draw UI which shows control layouts that match the path.
|
||||
if (parsedPath.Length >= 2 && !string.IsNullOrEmpty(deviceLayoutPath))
|
||||
{
|
||||
bool matchExists = false;
|
||||
|
||||
var rootDeviceLayout = EditorInputControlLayoutCache.TryGetLayout(deviceLayoutPath);
|
||||
bool isValidDeviceLayout = deviceLayoutPath == InputControlPath.Wildcard || (rootDeviceLayout != null && !rootDeviceLayout.isOverride && !rootDeviceLayout.hideInUI);
|
||||
// Exit early if a malformed device layout was provided,
|
||||
if (!isValidDeviceLayout)
|
||||
return matchingControlPaths;
|
||||
|
||||
controlPathUsagePresent = parsedPath[1].usages.Count() > 0;
|
||||
bool hasChildDeviceLayouts = deviceLayoutPath == InputControlPath.Wildcard || EditorInputControlLayoutCache.HasChildLayouts(rootDeviceLayout.name);
|
||||
|
||||
// If the path provided matches exactly one control path (i.e. has no ui-facing child device layouts or uses control usages), then exit early
|
||||
if (!controlPathUsagePresent && !hasChildDeviceLayouts)
|
||||
return matchingControlPaths;
|
||||
|
||||
// Otherwise, we will show either all controls that match the current binding (if control usages are used)
|
||||
// or all controls in derived device layouts (if a no control usages are used).
|
||||
|
||||
// If our control path contains a usage, make sure we render the binding that belongs to the root device layout first
|
||||
if (deviceLayoutPath != InputControlPath.Wildcard && controlPathUsagePresent)
|
||||
{
|
||||
matchExists |= CollectMatchingControlPathsForLayout(rootDeviceLayout, in parsedPath, true, matchingControlPaths);
|
||||
}
|
||||
// Otherwise, just render the bindings that belong to child device layouts. The binding that matches the root layout is
|
||||
// already represented by the user generated control path itself.
|
||||
else
|
||||
{
|
||||
IEnumerable<InputControlLayout> matchedChildLayouts = Enumerable.Empty<InputControlLayout>();
|
||||
if (deviceLayoutPath == InputControlPath.Wildcard)
|
||||
{
|
||||
matchedChildLayouts = EditorInputControlLayoutCache.allLayouts
|
||||
.Where(x => x.isDeviceLayout && !x.hideInUI && !x.isOverride && x.isGenericTypeOfDevice && x.baseLayouts.Count() == 0).OrderBy(x => x.displayName);
|
||||
}
|
||||
else
|
||||
{
|
||||
matchedChildLayouts = EditorInputControlLayoutCache.TryGetChildLayouts(rootDeviceLayout.name);
|
||||
}
|
||||
|
||||
foreach (var childLayout in matchedChildLayouts)
|
||||
{
|
||||
matchExists |= CollectMatchingControlPathsForLayout(childLayout, in parsedPath, false, matchingControlPaths);
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, indicate that no layouts match the current path.
|
||||
if (!matchExists)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return matchingControlPaths;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the deviceLayout or any of its children has controls which match the provided parsed path. exist matching registered control paths.
|
||||
/// </summary>
|
||||
/// <param name="deviceLayout">The device layout to draw control paths for</param>
|
||||
/// <param name="parsedPath">The parsed path containing details of the Input Controls that can be matched</param>
|
||||
private static bool CollectMatchingControlPathsForLayout(InputControlLayout deviceLayout, in InputControlPath.ParsedPathComponent[] parsedPath, bool isRoot, List<MatchingControlPath> matchingControlPaths)
|
||||
{
|
||||
string deviceName = deviceLayout.displayName;
|
||||
string controlName = string.Empty;
|
||||
bool matchExists = false;
|
||||
|
||||
for (int i = 0; i < deviceLayout.m_Controls.Length; i++)
|
||||
{
|
||||
ref InputControlLayout.ControlItem controlItem = ref deviceLayout.m_Controls[i];
|
||||
if (InputControlPath.MatchControlComponent(ref parsedPath[1], ref controlItem, true))
|
||||
{
|
||||
// If we've already located a match, append a ", " to the control name
|
||||
// This is to accomodate cases where multiple control items match the same path within a single device layout
|
||||
// Note, some controlItems have names but invalid displayNames (i.e. the Dualsense HID > leftTriggerButton)
|
||||
// There are instance where there are 2 control items with the same name inside a layout definition, however they are not
|
||||
// labeled significantly differently.
|
||||
// The notable example is that the Android Xbox and Android Dualshock layouts have 2 d-pad definitions, one is a "button"
|
||||
// while the other is an axis.
|
||||
controlName += matchExists ? $", {controlItem.name}" : controlItem.name;
|
||||
|
||||
// if the parsePath has a 3rd component, try to match it with items in the controlItem's layout definition.
|
||||
if (parsedPath.Length == 3)
|
||||
{
|
||||
var controlLayout = EditorInputControlLayoutCache.TryGetLayout(controlItem.layout);
|
||||
if (controlLayout.isControlLayout && !controlLayout.hideInUI)
|
||||
{
|
||||
for (int j = 0; j < controlLayout.m_Controls.Count(); j++)
|
||||
{
|
||||
ref InputControlLayout.ControlItem controlLayoutItem = ref controlLayout.m_Controls[j];
|
||||
if (InputControlPath.MatchControlComponent(ref parsedPath[2], ref controlLayoutItem))
|
||||
{
|
||||
controlName += $"/{controlLayoutItem.name}";
|
||||
matchExists = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
matchExists = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerable<InputControlLayout> matchedChildLayouts = EditorInputControlLayoutCache.TryGetChildLayouts(deviceLayout.name);
|
||||
|
||||
// If this layout does not have a match, or is the top level root layout,
|
||||
// skip over trying to draw any items for it, and immediately try processing the child layouts
|
||||
if (!matchExists)
|
||||
{
|
||||
foreach (var childLayout in matchedChildLayouts)
|
||||
{
|
||||
matchExists |= CollectMatchingControlPathsForLayout(childLayout, in parsedPath, false, matchingControlPaths);
|
||||
}
|
||||
}
|
||||
// Otherwise, draw the items for it, and then only process the child layouts if the foldout is expanded.
|
||||
else
|
||||
{
|
||||
var newMatchingControlPath = new MatchingControlPath(deviceName, controlName, isRoot);
|
||||
matchingControlPaths.Add(newMatchingControlPath);
|
||||
|
||||
foreach (var childLayout in matchedChildLayouts)
|
||||
{
|
||||
CollectMatchingControlPathsForLayout(childLayout, in parsedPath, false, newMatchingControlPath.children);
|
||||
}
|
||||
}
|
||||
|
||||
return matchExists;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 890eded7b7fc53b489fcc92f0aeb774e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 11330b6eb30fdb54fa20d0da9f0d60a5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,193 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
internal class PropertiesView : ViewBase<PropertiesView.ViewState>
|
||||
{
|
||||
private ActionPropertiesView m_ActionPropertyView;
|
||||
private BindingPropertiesView m_BindingPropertyView;
|
||||
private NameAndParametersListView m_InteractionsListView;
|
||||
private NameAndParametersListView m_ProcessorsListView;
|
||||
|
||||
private Foldout interactionsFoldout => rootElement.Q<Foldout>("interactions-foldout");
|
||||
private Foldout processorsFoldout => rootElement.Q<Foldout>("processors-foldout");
|
||||
|
||||
private TextElement addInteractionButton;
|
||||
private TextElement addProcessorButton;
|
||||
|
||||
public PropertiesView(VisualElement root, StateContainer stateContainer)
|
||||
: base(root, stateContainer)
|
||||
{
|
||||
CreateSelector(
|
||||
Selectors.GetSelectedAction,
|
||||
Selectors.GetSelectedBinding,
|
||||
state => state.selectionType,
|
||||
(inputAction, inputBinding, selectionType, s) => new ViewState()
|
||||
{
|
||||
selectionType = selectionType,
|
||||
serializedInputAction = inputAction,
|
||||
inputBinding = inputBinding,
|
||||
relatedInputAction = Selectors.GetRelatedInputAction(s)
|
||||
});
|
||||
|
||||
var interactionsToggle = interactionsFoldout.Q<Toggle>();
|
||||
interactionsToggle.AddToClassList("properties-foldout-toggle");
|
||||
if (addInteractionButton == null)
|
||||
{
|
||||
addInteractionButton = CreateAddButton(interactionsToggle, "add-new-interaction-button");
|
||||
new ContextualMenuManipulator(_ => {}){target = addInteractionButton, activators = {new ManipulatorActivationFilter(){button = MouseButton.LeftMouse}}};
|
||||
}
|
||||
var processorToggle = processorsFoldout.Q<Toggle>();
|
||||
processorToggle.AddToClassList("properties-foldout-toggle");
|
||||
if (addProcessorButton == null)
|
||||
{
|
||||
addProcessorButton = CreateAddButton(processorToggle, "add-new-processor-button");
|
||||
new ContextualMenuManipulator(_ => {}){target = addProcessorButton, activators = {new ManipulatorActivationFilter(){button = MouseButton.LeftMouse}}};
|
||||
}
|
||||
}
|
||||
|
||||
private TextElement CreateAddButton(Toggle toggle, string name)
|
||||
{
|
||||
var addButton = new Button();
|
||||
addButton.text = "+";
|
||||
addButton.name = name;
|
||||
addButton.focusable = false;
|
||||
#if UNITY_EDITOR_OSX
|
||||
addButton.clickable.activators.Clear();
|
||||
#endif
|
||||
addButton.AddToClassList("add-interaction-processor-button");
|
||||
toggle.Add(addButton);
|
||||
return addButton;
|
||||
}
|
||||
|
||||
private void CreateContextMenuProcessor(string expectedControlType)
|
||||
{
|
||||
var processors = InputProcessor.s_Processors;
|
||||
Type expectedValueType = string.IsNullOrEmpty(expectedControlType) ? null : EditorInputControlLayoutCache.GetValueType(expectedControlType);
|
||||
addProcessorButton.RegisterCallback<ContextualMenuPopulateEvent>(evt =>
|
||||
{
|
||||
evt.menu.ClearItems();
|
||||
foreach (var name in processors.internedNames.Where(x => !processors.ShouldHideInUI(x)).OrderBy(x => x.ToString()))
|
||||
{
|
||||
// Skip if not compatible with value type.
|
||||
if (!IsValidProcessorForControl(expectedValueType, name))
|
||||
continue;
|
||||
var niceName = ObjectNames.NicifyVariableName(name);
|
||||
evt.menu.AppendAction(niceName, _ => m_ProcessorsListView.OnAddElement(name.ToString()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private bool IsValidProcessorForControl(Type expectedValueType, string name)
|
||||
{
|
||||
if (expectedValueType == null) return true;
|
||||
var type = InputProcessor.s_Processors.LookupTypeRegistration(name);
|
||||
var valueType = InputProcessor.GetValueTypeFromType(type);
|
||||
if (valueType != null && !expectedValueType.IsAssignableFrom(valueType))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void CreateContextMenuInteraction(string expectedControlType)
|
||||
{
|
||||
var interactions = InputInteraction.s_Interactions;
|
||||
Type expectedValueType = string.IsNullOrEmpty(expectedControlType) ? null : EditorInputControlLayoutCache.GetValueType(expectedControlType);
|
||||
addInteractionButton.RegisterCallback<ContextualMenuPopulateEvent>(evt =>
|
||||
{
|
||||
evt.menu.ClearItems();
|
||||
foreach (var name in interactions.internedNames.Where(x => !interactions.ShouldHideInUI(x)).OrderBy(x => x.ToString()))
|
||||
{
|
||||
// Skip if not compatible with value type.
|
||||
if (!IsValidInteractionForControl(expectedValueType, name))
|
||||
continue;
|
||||
var niceName = ObjectNames.NicifyVariableName(name);
|
||||
evt.menu.AppendAction(niceName, _ => m_InteractionsListView.OnAddElement(name.ToString()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private bool IsValidInteractionForControl(Type expectedValueType, string name)
|
||||
{
|
||||
if (expectedValueType == null) return true;
|
||||
var type = InputInteraction.s_Interactions.LookupTypeRegistration(name);
|
||||
var valueType = InputInteraction.GetValueType(type);
|
||||
if (valueType != null && !expectedValueType.IsAssignableFrom(valueType))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public override void RedrawUI(ViewState viewState)
|
||||
{
|
||||
DestroyChildView(m_ActionPropertyView);
|
||||
DestroyChildView(m_BindingPropertyView);
|
||||
DestroyChildView(m_InteractionsListView);
|
||||
DestroyChildView(m_ProcessorsListView);
|
||||
|
||||
var propertiesContainer = rootElement.Q<VisualElement>("properties-container");
|
||||
|
||||
var foldout = propertiesContainer.Q<Foldout>("properties-foldout");
|
||||
foldout.Clear();
|
||||
|
||||
var visualElement = new VisualElement();
|
||||
foldout.Add(visualElement);
|
||||
foldout.Q<Toggle>().AddToClassList("properties-foldout-toggle");
|
||||
|
||||
var inputAction = viewState.serializedInputAction;
|
||||
var inputActionOrBinding = inputAction?.wrappedProperty;
|
||||
|
||||
switch (viewState.selectionType)
|
||||
{
|
||||
case SelectionType.Action:
|
||||
rootElement.Q<Label>("properties-header-label").text = "Action Properties";
|
||||
m_ActionPropertyView = CreateChildView(new ActionPropertiesView(visualElement, foldout, stateContainer));
|
||||
break;
|
||||
|
||||
case SelectionType.Binding:
|
||||
rootElement.Q<Label>("properties-header-label").text = "Binding Properties";
|
||||
m_BindingPropertyView = CreateChildView(new BindingPropertiesView(visualElement, foldout, stateContainer));
|
||||
inputAction = viewState.relatedInputAction;
|
||||
inputActionOrBinding = viewState.inputBinding?.wrappedProperty;
|
||||
break;
|
||||
}
|
||||
|
||||
CreateContextMenuProcessor(inputAction?.expectedControlType);
|
||||
CreateContextMenuInteraction(inputAction?.expectedControlType);
|
||||
|
||||
var isPartOfComposite = viewState.selectionType == SelectionType.Binding &&
|
||||
viewState.inputBinding?.isPartOfComposite == true;
|
||||
//don't show for Bindings in Composites
|
||||
if (!isPartOfComposite)
|
||||
{
|
||||
interactionsFoldout.style.display = DisplayStyle.Flex;
|
||||
m_InteractionsListView = CreateChildView(new NameAndParametersListView(
|
||||
interactionsFoldout,
|
||||
stateContainer,
|
||||
inputActionOrBinding?.FindPropertyRelative(nameof(InputAction.m_Interactions)),
|
||||
state => Selectors.GetInteractionsAsParameterListViews(state, inputAction)));
|
||||
}
|
||||
else
|
||||
interactionsFoldout.style.display = DisplayStyle.None;
|
||||
|
||||
|
||||
m_ProcessorsListView = CreateChildView(new NameAndParametersListView(
|
||||
processorsFoldout,
|
||||
stateContainer,
|
||||
inputActionOrBinding?.FindPropertyRelative(nameof(InputAction.m_Processors)),
|
||||
state => Selectors.GetProcessorsAsParameterListViews(state, inputAction)));
|
||||
}
|
||||
|
||||
internal class ViewState
|
||||
{
|
||||
public SerializedInputAction? relatedInputAction;
|
||||
public SerializedInputBinding? inputBinding;
|
||||
public SerializedInputAction? serializedInputAction;
|
||||
public SelectionType selectionType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8879a5884abf42c4c99aba5009bc081f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b98fcf53d54c1fe4abdd9327cf57a371
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,298 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
internal interface IViewStateSelector<out TViewState>
|
||||
{
|
||||
bool HasStateChanged(InputActionsEditorState state);
|
||||
TViewState GetViewState(InputActionsEditorState state);
|
||||
}
|
||||
|
||||
internal interface IView
|
||||
{
|
||||
void UpdateView(InputActionsEditorState state);
|
||||
void DestroyView();
|
||||
}
|
||||
|
||||
internal abstract class ViewBase<TViewState> : IView
|
||||
{
|
||||
protected ViewBase(VisualElement root, StateContainer stateContainer)
|
||||
{
|
||||
this.rootElement = root;
|
||||
this.stateContainer = stateContainer;
|
||||
m_ChildViews = new List<IView>();
|
||||
}
|
||||
|
||||
protected void OnStateChanged(InputActionsEditorState state, UIRebuildMode editorRebuildMode)
|
||||
{
|
||||
// Return early if rebuilding the editor UI isn't required (ISXB-1171)
|
||||
if (editorRebuildMode == UIRebuildMode.None)
|
||||
return;
|
||||
|
||||
UpdateView(state);
|
||||
}
|
||||
|
||||
public void UpdateView(InputActionsEditorState state)
|
||||
{
|
||||
if (m_ViewStateSelector == null)
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"View '{GetType().Name}' has no selector and will not render. Create a selector for the " +
|
||||
$"view using the CreateSelector method.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_ViewStateSelector.HasStateChanged(state) || m_IsFirstUpdate)
|
||||
RedrawUI(m_ViewStateSelector.GetViewState(state));
|
||||
|
||||
m_IsFirstUpdate = false;
|
||||
foreach (var view in m_ChildViews)
|
||||
{
|
||||
view.UpdateView(state);
|
||||
}
|
||||
|
||||
// We can execute UI Commands now that the UI is fully updated
|
||||
// NOTE: This isn't used with Input Commands
|
||||
allowUICommandExecution = true;
|
||||
}
|
||||
|
||||
public TView CreateChildView<TView>(TView view) where TView : IView
|
||||
{
|
||||
m_ChildViews.Add(view);
|
||||
return view;
|
||||
}
|
||||
|
||||
public void DestroyChildView<TView>(TView view) where TView : IView
|
||||
{
|
||||
if (view == null)
|
||||
return;
|
||||
|
||||
m_ChildViews.Remove(view);
|
||||
view.DestroyView();
|
||||
}
|
||||
|
||||
public void Dispatch(Command command, UIRebuildMode editorRebuildMode = UIRebuildMode.Rebuild)
|
||||
{
|
||||
stateContainer.Dispatch(command, editorRebuildMode);
|
||||
}
|
||||
|
||||
public abstract void RedrawUI(TViewState viewState);
|
||||
|
||||
/// <summary>
|
||||
/// Called when a parent view is destroying this view to give it an opportunity to clean up any
|
||||
/// resources or event handlers.
|
||||
/// </summary>
|
||||
public virtual void DestroyView()
|
||||
{
|
||||
}
|
||||
|
||||
protected void CreateSelector(Func<InputActionsEditorState, TViewState> selector)
|
||||
{
|
||||
m_ViewStateSelector = new ViewStateSelector<TViewState>(selector);
|
||||
}
|
||||
|
||||
protected void CreateSelector<T1>(
|
||||
Func<InputActionsEditorState, T1> func1,
|
||||
Func<T1, InputActionsEditorState, TViewState> selector)
|
||||
{
|
||||
m_ViewStateSelector = new ViewStateSelector<T1, TViewState>(func1, selector);
|
||||
}
|
||||
|
||||
protected void CreateSelector<T1, T2>(
|
||||
Func<InputActionsEditorState, T1> func1,
|
||||
Func<InputActionsEditorState, T2> func2,
|
||||
Func<T1, T2, InputActionsEditorState, TViewState> selector)
|
||||
{
|
||||
m_ViewStateSelector = new ViewStateSelector<T1, T2, TViewState>(func1, func2, selector);
|
||||
}
|
||||
|
||||
protected void CreateSelector<T1, T2, T3>(
|
||||
Func<InputActionsEditorState, T1> func1,
|
||||
Func<InputActionsEditorState, T2> func2,
|
||||
Func<InputActionsEditorState, T3> func3,
|
||||
Func<T1, T2, T3, InputActionsEditorState, TViewState> selector)
|
||||
{
|
||||
m_ViewStateSelector = new ViewStateSelector<T1, T2, T3, TViewState>(func1, func2, func3, selector);
|
||||
}
|
||||
|
||||
protected readonly VisualElement rootElement;
|
||||
protected readonly StateContainer stateContainer;
|
||||
protected bool allowUICommandExecution { get; set; } = true;
|
||||
|
||||
protected IViewStateSelector<TViewState> ViewStateSelector => m_ViewStateSelector;
|
||||
private IViewStateSelector<TViewState> m_ViewStateSelector;
|
||||
private IList<IView> m_ChildViews;
|
||||
private bool m_IsFirstUpdate = true;
|
||||
}
|
||||
|
||||
internal class ViewStateSelector<TReturn> : IViewStateSelector<TReturn>
|
||||
{
|
||||
private readonly Func<InputActionsEditorState, TReturn> m_Selector;
|
||||
|
||||
public ViewStateSelector(Func<InputActionsEditorState, TReturn> selector)
|
||||
{
|
||||
m_Selector = selector;
|
||||
}
|
||||
|
||||
public bool HasStateChanged(InputActionsEditorState state)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public TReturn GetViewState(InputActionsEditorState state)
|
||||
{
|
||||
return m_Selector(state);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Make all args to view state selectors IEquatable<T>?
|
||||
internal class ViewStateSelector<T1, TReturn> : IViewStateSelector<TReturn>
|
||||
{
|
||||
private readonly Func<InputActionsEditorState, T1> m_Func1;
|
||||
private readonly Func<T1, InputActionsEditorState, TReturn> m_Selector;
|
||||
|
||||
private T1 m_PreviousT1;
|
||||
|
||||
public ViewStateSelector(Func<InputActionsEditorState, T1> func1,
|
||||
Func<T1, InputActionsEditorState, TReturn> selector)
|
||||
{
|
||||
m_Func1 = func1;
|
||||
m_Selector = selector;
|
||||
}
|
||||
|
||||
public bool HasStateChanged(InputActionsEditorState state)
|
||||
{
|
||||
var valueOne = m_Func1(state);
|
||||
|
||||
if (valueOne is IViewStateCollection collection)
|
||||
{
|
||||
if (collection.SequenceEqual((IViewStateCollection)m_PreviousT1))
|
||||
return false;
|
||||
}
|
||||
else if (valueOne.Equals(m_PreviousT1))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
m_PreviousT1 = valueOne;
|
||||
return true;
|
||||
}
|
||||
|
||||
public TReturn GetViewState(InputActionsEditorState state)
|
||||
{
|
||||
return m_Selector(m_PreviousT1, state);
|
||||
}
|
||||
}
|
||||
|
||||
internal class ViewStateSelector<T1, T2, TReturn> : IViewStateSelector<TReturn>
|
||||
{
|
||||
private readonly Func<InputActionsEditorState, T1> m_Func1;
|
||||
private readonly Func<InputActionsEditorState, T2> m_Func2;
|
||||
private readonly Func<T1, T2, InputActionsEditorState, TReturn> m_Selector;
|
||||
|
||||
private T1 m_PreviousT1;
|
||||
private T2 m_PreviousT2;
|
||||
|
||||
public ViewStateSelector(Func<InputActionsEditorState, T1> func1,
|
||||
Func<InputActionsEditorState, T2> func2,
|
||||
Func<T1, T2, InputActionsEditorState, TReturn> selector)
|
||||
{
|
||||
m_Func1 = func1;
|
||||
m_Func2 = func2;
|
||||
m_Selector = selector;
|
||||
}
|
||||
|
||||
public bool HasStateChanged(InputActionsEditorState state)
|
||||
{
|
||||
var valueOne = m_Func1(state);
|
||||
var valueTwo = m_Func2(state);
|
||||
|
||||
var valueOneHasChanged = false;
|
||||
var valueTwoHasChanged = false;
|
||||
|
||||
if (valueOne is IViewStateCollection collection && !collection.SequenceEqual((IViewStateCollection)m_PreviousT1) ||
|
||||
!valueOne.Equals(m_PreviousT1))
|
||||
valueOneHasChanged = true;
|
||||
|
||||
if (valueTwo is IViewStateCollection collection2 && !collection2.SequenceEqual((IViewStateCollection)m_PreviousT2) ||
|
||||
!valueTwo.Equals(m_PreviousT2))
|
||||
valueTwoHasChanged = true;
|
||||
|
||||
if (!valueOneHasChanged && !valueTwoHasChanged)
|
||||
return false;
|
||||
|
||||
m_PreviousT1 = valueOne;
|
||||
m_PreviousT2 = valueTwo;
|
||||
return true;
|
||||
}
|
||||
|
||||
public TReturn GetViewState(InputActionsEditorState state)
|
||||
{
|
||||
return m_Selector(m_PreviousT1, m_PreviousT2, state);
|
||||
}
|
||||
}
|
||||
|
||||
internal class ViewStateSelector<T1, T2, T3, TReturn> : IViewStateSelector<TReturn>
|
||||
{
|
||||
private readonly Func<InputActionsEditorState, T1> m_Func1;
|
||||
private readonly Func<InputActionsEditorState, T2> m_Func2;
|
||||
private readonly Func<InputActionsEditorState, T3> m_Func3;
|
||||
private readonly Func<T1, T2, T3, InputActionsEditorState, TReturn> m_Selector;
|
||||
|
||||
private T1 m_PreviousT1;
|
||||
private T2 m_PreviousT2;
|
||||
private T3 m_PreviousT3;
|
||||
|
||||
public ViewStateSelector(Func<InputActionsEditorState, T1> func1,
|
||||
Func<InputActionsEditorState, T2> func2,
|
||||
Func<InputActionsEditorState, T3> func3,
|
||||
Func<T1, T2, T3, InputActionsEditorState, TReturn> selector)
|
||||
{
|
||||
m_Func1 = func1;
|
||||
m_Func2 = func2;
|
||||
m_Func3 = func3;
|
||||
m_Selector = selector;
|
||||
}
|
||||
|
||||
public bool HasStateChanged(InputActionsEditorState state)
|
||||
{
|
||||
var valueOne = m_Func1(state);
|
||||
var valueTwo = m_Func2(state);
|
||||
var valueThree = m_Func3(state);
|
||||
|
||||
var valueOneHasChanged = false;
|
||||
var valueTwoHasChanged = false;
|
||||
var valueThreeHasChanged = false;
|
||||
|
||||
if (valueOne is IViewStateCollection collection && !collection.SequenceEqual((IViewStateCollection)m_PreviousT1) ||
|
||||
!valueOne.Equals(m_PreviousT1))
|
||||
valueOneHasChanged = true;
|
||||
|
||||
if (valueTwo is IViewStateCollection collection2 && !collection2.SequenceEqual((IViewStateCollection)m_PreviousT2) ||
|
||||
!valueTwo.Equals(m_PreviousT2))
|
||||
valueTwoHasChanged = true;
|
||||
|
||||
if (valueThree is IViewStateCollection collection3 && !collection3.SequenceEqual((IViewStateCollection)m_PreviousT3) ||
|
||||
!valueThree.Equals(m_PreviousT3))
|
||||
valueThreeHasChanged = true;
|
||||
|
||||
if (!valueOneHasChanged && !valueTwoHasChanged && !valueThreeHasChanged)
|
||||
return false;
|
||||
|
||||
m_PreviousT1 = valueOne;
|
||||
m_PreviousT2 = valueTwo;
|
||||
m_PreviousT3 = valueThree;
|
||||
return true;
|
||||
}
|
||||
|
||||
public TReturn GetViewState(InputActionsEditorState state)
|
||||
{
|
||||
return m_Selector(m_PreviousT1, m_PreviousT2, m_PreviousT3, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 900c2de771722c542b5857931181ab2f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,58 @@
|
||||
#if UNITY_EDITOR
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// A caching enumerator that will save all enumerated values on the first iteration, and when
|
||||
/// subsequently iterated, return the saved values instead.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
internal class ViewStateCollection<T> : IViewStateCollection, IEnumerable<T>
|
||||
{
|
||||
private readonly IEnumerable<T> m_Collection;
|
||||
private readonly IEqualityComparer<T> m_Comparer;
|
||||
private IList<T> m_CachedCollection;
|
||||
|
||||
public ViewStateCollection(IEnumerable<T> collection, IEqualityComparer<T> comparer = null)
|
||||
{
|
||||
m_Collection = collection;
|
||||
m_Comparer = comparer;
|
||||
}
|
||||
|
||||
public bool SequenceEqual(IViewStateCollection other)
|
||||
{
|
||||
return other is ViewStateCollection<T> otherCollection && this.SequenceEqual(otherCollection, m_Comparer);
|
||||
}
|
||||
|
||||
public IEnumerator<T> GetEnumerator()
|
||||
{
|
||||
if (m_CachedCollection == null)
|
||||
{
|
||||
m_CachedCollection = new List<T>();
|
||||
using (var enumerator = m_Collection.GetEnumerator())
|
||||
{
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
m_CachedCollection.Add(enumerator.Current);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
using (var enumerator = m_CachedCollection.GetEnumerator())
|
||||
{
|
||||
while (enumerator.MoveNext())
|
||||
yield return enumerator.Current;
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cdc588cfee2e9a84a9d748fb7314daff
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,22 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
internal static class VisualElementExtensions
|
||||
{
|
||||
public static TElement Q<TElement>(this VisualElement visualElement, string name) where TElement : VisualElement
|
||||
{
|
||||
var element = UQueryExtensions.Q<TElement>(visualElement, name);
|
||||
if (element == null)
|
||||
throw new InvalidOperationException(
|
||||
$"Expected a visual element called '{name}' of type '{typeof(TElement)}' to exist " +
|
||||
$"but none was found.");
|
||||
|
||||
return element;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 09dbcfc6e83454e45ace8503dacc86ad
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user