Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Packages/com.unity.inputsystem/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ however, it has to be formatted properly to pass verification tests.
- [`InputAction.WasCompletedThisFrame`](xref:UnityEngine.InputSystem.InputAction.WasCompletedThisFrame) returns `true` on the frame that the action stopped being in the performed phase. This allows for similar functionality to [`WasPressedThisFrame`](xref:UnityEngine.InputSystem.InputAction.WasPressedThisFrame)/[`WasReleasedThisFrame`](xref:UnityEngine.InputSystem.InputAction.WasReleasedThisFrame) when paired with [`WasPerformedThisFrame`](xref:UnityEngine.InputSystem.InputAction.WasPerformedThisFrame) except it is directly based on the interactions driving the action. For example, you can use it to distinguish between the button being released or whether it was released after being held for long enough to perform when using the Hold interaction.
- Added Copy, Paste and Cut support for Action Maps, Actions and Bindings via context menu and key command shortcuts.
- Added Dual Sense Edge controller to be mapped to the same layout as the Dual Sense controller
- UI Toolkit input action editor now supports showing the derived bindings.

### Fixed
- Fixed syntax of code examples in API documentation for [`AxisComposite`](xref:UnityEngine.InputSystem.Composites.AxisComposite).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,21 @@
.unity-two-pane-split-view__dragline-anchor {
background-color: rgb(25, 25, 25);
}

#control-scheme-usage-title {
margin: 3px;
-unity-font-style: bold;
}

.matching-controls {
display: none;
}

.matching-controls-shown {
display: flex;
flex-grow: 1;
}

.matching-controls-labels {
margin: 1px;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
using System.Linq;
using UnityEditor;
using UnityEngine.UIElements;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.Utilities;
using System.Collections.Generic;

namespace UnityEngine.InputSystem.Editor
{
Expand Down Expand Up @@ -50,6 +53,7 @@ public override void RedrawUI(ViewState viewState)
else if (binding.Value.isPartOfComposite)
{
m_CompositePartBindingPropertiesView = CreateChildView(new CompositePartBindingPropertiesView(rootElement, stateContainer));
DrawMatchingControlPaths(viewState);
DrawControlSchemeToggles(viewState, binding.Value);
}
else
Expand All @@ -64,10 +68,79 @@ public override void RedrawUI(ViewState viewState)
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 foldout = new Foldout()
{
text = $"Show Derived Bindings",
value = showPaths
};
rootElement.Add(foldout);

foldout.RegisterValueChangedCallback(changeEvent =>
{
s_showMatchingLayouts = changeEvent.newValue;

rootElement.Q(className: "matching-controls").EnableInClassList("matching-controls-shown", changeEvent.newValue);
});

parentElement = foldout;
}

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");
helpBox.EnableInClassList("matching-controls-shown", showPaths);
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.EnableInClassList("matching-controls-shown", showPaths);
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);
label.text = treeView.GetItemDataForIndex<MatchingControlPath>(index).path;
};

treeView.ExpandRootItems();
}
}

public override void DestroyView()
{
m_CompositeBindingPropertiesView?.DestroyView();
Expand All @@ -78,7 +151,11 @@ private void DrawControlSchemeToggles(ViewState viewState, SerializedInputBindin
{
if (!viewState.controlSchemes.Any()) return;

var useInControlSchemeLabel = new Label("Use in control scheme");
var useInControlSchemeLabel = new Label("Use in control scheme")
{
name = "control-scheme-usage-title"
};

rootElement.Add(useInControlSchemeLabel);

foreach (var controlScheme in viewState.controlSchemes)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
using System.Linq;
using UnityEditor;
using UnityEngine.UIElements;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.Utilities;
using System.Collections.Generic;

namespace UnityEngine.InputSystem.Editor
{
internal class MatchingControlPath
{
public string path
{
get;
}
public List<MatchingControlPath> children
{
get;
}

public MatchingControlPath(string path)
{
this.path = path;
this.children = new List<MatchingControlPath>();
}

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;
}

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}");
matchingControlPaths.Add(newMatchingControlPath);

foreach (var childLayout in matchedChildLayouts)
{
CollectMatchingControlPathsForLayout(childLayout, in parsedPath, false, newMatchingControlPath.children);
}
}

return matchExists;
}
}
}

#endif

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.