Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
d0d8ad8
Checking-Pointing progress for specifying input usages in editor, as …
RogPodge Apr 14, 2023
afca0e1
Style update, optimizing performance a bit
RogPodge Apr 14, 2023
8324432
added unit test
RogPodge Apr 14, 2023
361cf5f
Control usages now create the right path when combined with device us…
RogPodge Apr 14, 2023
a9f0cac
updated to show relevant information when the path doesn't match any …
RogPodge Apr 14, 2023
a338b28
Merge branch 'develop' into inputbinding-controlusages-UI
RogPodge Apr 27, 2023
57111f0
removing whitespace, retriggering formatting CI
RogPodge Apr 27, 2023
370d0da
fixing remaining formatting complaints by running format.ps1
RogPodge Apr 27, 2023
d469346
Merge branch 'inputbinding-controlusages-UI' of https://github.com/Un…
RogPodge Apr 27, 2023
d35d3d4
tweaked UI to PR suggestions
RogPodge May 1, 2023
0a241f1
Update Packages/com.unity.inputsystem/InputSystem/Controls/InputContr…
RogPodge May 10, 2023
3866b5d
Adjusted instances of ref to in, fixed instances where break should b…
RogPodge May 10, 2023
b01289d
Added a lookup dictionary in the EditorInputControlLayoutCache for de…
RogPodge May 23, 2023
99e0f61
Updated matching controls layout information to an expandable foldout
RogPodge May 25, 2023
7483e8d
fixed issue where invalid controls could be selected in the dropdown …
RogPodge Jun 2, 2023
e2130b8
fixed wording, fixed case where the binding path could break
RogPodge Jun 2, 2023
ddd4465
codestyle formatting pass
RogPodge Jun 2, 2023
5e35fa7
Merge branch 'develop' of https://github.com/Unity-Technologies/Input…
RogPodge Jun 5, 2023
35a27d7
fixed issue where device layouts were queried twice due to being chil…
RogPodge Jun 6, 2023
c9e3dae
Merge branch 'develop' of https://github.com/Unity-Technologies/Input…
RogPodge Jun 6, 2023
9ae08b1
applying PR feedback
RogPodge Jun 7, 2023
e740401
Updated No Matching Control paths message with PR suggestions
RogPodge Jun 8, 2023
f70b140
Merge branch 'develop' into inputbinding-controlusages-UI
RogPodge Jun 8, 2023
3e953ee
Matching controls display now shows control path's whose alias matche…
RogPodge Jun 9, 2023
4e05692
Merge branch 'inputbinding-controlusages-UI' of https://github.com/Un…
RogPodge Jun 9, 2023
9454776
fixed bug where controls wouldn't match if only usages were checked
RogPodge Jun 12, 2023
4409e53
added unit tests for the new control path layout capabilities
RogPodge Jun 13, 2023
95b17e9
Merge branch 'develop' into inputbinding-controlusages-UI
RogPodge Jun 13, 2023
f63051d
fixing formatting issues
RogPodge Jun 13, 2023
cb2995c
Removed reduntant control path listings when the control path specifi…
RogPodge Jun 14, 2023
5e522e3
formatting fix
RogPodge Jun 15, 2023
43d9c51
Merge branch 'develop' of https://github.com/Unity-Technologies/Input…
RogPodge Jun 27, 2023
fa82163
relabed the 'matched controls' field to be 'Derived controls'
RogPodge Jun 27, 2023
ce5edd2
Fixed to 'Derived Bindings'
RogPodge Jun 27, 2023
5443d0d
fixed case where the wildcard device moniker wasn't detected properly
RogPodge Jun 29, 2023
553ead3
fixed errors that would occur when malformed control paths are provided
RogPodge Jul 25, 2023
b36acbe
Merge branch 'develop' of https://github.com/Unity-Technologies/Input…
RogPodge Jul 25, 2023
edd86f9
style change to due to code analyzer results
RogPodge Jul 26, 2023
2beeb73
added missing in/ref changes
RogPodge Jul 26, 2023
74bd2f9
fixing formatter nits
RogPodge Jul 26, 2023
8b58789
Merge branch 'develop' into inputbinding-controlusages-UI
RogPodge Jul 27, 2023
9e8b471
Merge branch 'develop' into inputbinding-controlusages-UI
RogPodge Jul 31, 2023
778767a
Merge branch 'develop' into inputbinding-controlusages-UI
RogPodge Aug 3, 2023
00db0eb
Merge branch 'develop' into inputbinding-controlusages-UI
lyndon-unity Aug 7, 2023
5244c1d
add changelog entry
jamesmcgill Aug 7, 2023
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
44 changes: 44 additions & 0 deletions Assets/Tests/InputSystem/CoreTests_Layouts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2546,6 +2546,50 @@ public void Layouts_CanForceMixedVariantsThroughLayout()
Assert.That(device.allControls, Has.Exactly(1).With.Property("name").EqualTo("ButtonC"));
}

[Test]
[Category("Layouts")]
public void Layouts_CanMatchControlPath()
{
const string jsonBase = @"
{
""name"" : ""BaseLayout"",
""extend"" : ""DeviceWithLayoutVariantA"",
""controls"" : [
{ ""name"" : ""ControlFromBase"", ""layout"" : ""Button"" },
{ ""name"" : ""OtherControlFromBase"", ""layout"" : ""Axis"" },
{ ""name"" : ""ControlWithExplicitDefaultVariant"", ""layout"" : ""Axis"", ""variants"" : ""default"" },
{ ""name"" : ""StickControl"", ""layout"" : ""Stick"" },
{ ""name"" : ""StickControl/x"", ""offset"" : 14, ""variants"" : ""A"" }
]
}
";
const string jsonDerived = @"
{
""name"" : ""DerivedLayout"",
""extend"" : ""BaseLayout"",
""controls"" : [
{ ""name"" : ""ControlFromBase"", ""variants"" : ""A"", ""offset"" : 20, ""usages"" : [""Submit""], ""aliases"" : [""A""] }
]
}
";

InputSystem.RegisterLayout<DeviceWithLayoutVariantA>();
InputSystem.RegisterLayout(jsonBase);
InputSystem.RegisterLayout(jsonDerived);

var layout = InputSystem.LoadLayout("DerivedLayout");
var parsedPath = InputControlPath.Parse("<BaseLayout>/ControlWithExplicitDefaultVariant").ToArray()[1];
Assert.That(layout.m_Controls.Any(x => InputControlPath.MatchControlComponent(ref parsedPath, ref x)), Is.True);

// Verify that we can match alias's when provided
var parsedAliasPath = InputControlPath.Parse("<BaseLayout>/A").ToArray()[1];
Assert.That(layout.m_Controls.Any(x => InputControlPath.MatchControlComponent(ref parsedAliasPath, ref x, true)), Is.True);

// Verify that we match usages when it is the only control path component provided
var parsedUsagesPath = InputControlPath.Parse("<BaseLayout>/{Submit}").ToArray()[1];
Assert.That(layout.m_Controls.Any(x => InputControlPath.MatchControlComponent(ref parsedUsagesPath, ref x)), Is.True);
}

[Test]
[Category("Layouts")]
[Ignore("TODO")]
Expand Down
1 change: 1 addition & 0 deletions Packages/com.unity.inputsystem/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ however, it has to be formatted properly to pass verification tests.

### Added
- Preliminary support for visionOS.
- Show a list of `Derived Bindings` underneath the Binding Path editor to show all controls that matched.

### Changed
- Changed the `InputAction` constructors so it generates an ID for the action and the optional binding parameter. This is intended to improve the serialization of input actions on behaviors when created through API when the property drawer in the Inspector window does not have a chance to generate an ID.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,54 @@ public static bool Matches(string expected, InputControl control)
return MatchesRecursive(ref parser, control);
}

internal static bool MatchControlComponent(ref ParsedPathComponent expectedControlComponent, ref InputControlLayout.ControlItem controlItem, bool matchAlias = false)
{
bool controlItemNameMatched = false;
var anyUsageMatches = false;

// Check to see that there is a match with the name or alias if specified
// Exit early if we can't create a match.
if (!expectedControlComponent.m_Name.isEmpty)
{
if (StringMatches(expectedControlComponent.m_Name, controlItem.name))
controlItemNameMatched = true;
else if (matchAlias)
{
var aliases = controlItem.aliases;
for (var i = 0; i < aliases.Count; i++)
{
if (StringMatches(expectedControlComponent.m_Name, aliases[i]))
{
controlItemNameMatched = true;
break;
}
}
}
else
return false;
}

// All of usages should match to the one of usage in the control
foreach (var usage in expectedControlComponent.m_Usages)
{
if (!usage.isEmpty)
{
var usageCount = controlItem.usages.Count;
for (var i = 0; i < usageCount; ++i)
{
if (StringMatches(usage, controlItem.usages[i]))
{
anyUsageMatches = true;
break;
}
}
}
}

// Return whether or not we were able to match an alias or a usage
return controlItemNameMatched || anyUsageMatches;
}

/// <summary>
/// Check whether the given path matches <paramref name="control"/> or any of its parents.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEngine.InputSystem.Editor.Lists;
using UnityEngine.InputSystem.Layouts;
Expand Down Expand Up @@ -102,11 +103,187 @@ protected override void DrawGeneralProperties()
}
}

// Show the specific controls which match the current path
DrawMatchingControlPaths();

// Control scheme matrix.
DrawUseInControlSchemes();
}
}

/// <summary>
/// Used to keep track of which foldouts are expanded.
/// </summary>
private static bool showMatchingLayouts = false;
private static Dictionary<string, bool> showMatchingChildLayouts = new Dictionary<string, bool>();

/// <summary>
/// Finds all registered control paths implemented by concrete classes which match the current binding path and renders it.
/// </summary>
private void DrawMatchingControlPaths()
{
var path = m_ControlPathEditor.pathProperty.stringValue;
if (path == string.Empty)
return;

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;

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

// 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).
EditorGUILayout.BeginVertical();
showMatchingLayouts = EditorGUILayout.Foldout(showMatchingLayouts, "Derived Bindings");

if (showMatchingLayouts)
{
// 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 |= DrawMatchingControlPathsForLayout(rootDeviceLayout, in parsedPath, true);
}
// 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 |= DrawMatchingControlPathsForLayout(childLayout, in parsedPath);
}
}

// Otherwise, indicate that no layouts match the current path.
if (!matchExists)
{
if (controlPathUsagePresent)
EditorGUILayout.HelpBox("No registered controls match this current binding. Some controls are only registered at runtime.", MessageType.Warning);
else
EditorGUILayout.HelpBox("No other registered controls match this current binding. Some controls are only registered at runtime.", MessageType.Warning);
}
}

EditorGUILayout.EndVertical();
}
}

/// <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 bool DrawMatchingControlPathsForLayout(InputControlLayout deviceLayout, in InputControlPath.ParsedPathComponent[] parsedPath, bool isRoot = false)
{
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 |= DrawMatchingControlPathsForLayout(childLayout, in parsedPath);
}
}
// Otherwise, draw the items for it, and then only process the child layouts if the foldout is expanded.
else
{
bool showLayout = false;
EditorGUI.indentLevel++;
if (matchedChildLayouts.Count() > 0 && !isRoot)
{
showMatchingChildLayouts.TryGetValue(deviceName, out showLayout);
showMatchingChildLayouts[deviceName] = EditorGUILayout.Foldout(showLayout, $"{deviceName} > {controlName}");
}
else
{
EditorGUILayout.LabelField($"{deviceName} > {controlName}");
}

showLayout |= isRoot;

if (showLayout)
{
foreach (var childLayout in matchedChildLayouts)
{
DrawMatchingControlPathsForLayout(childLayout, in parsedPath);
}
}
EditorGUI.indentLevel--;
}

return matchExists;
}

/// <summary>
/// Draw control scheme matrix that allows selecting which control schemes a particular
/// binding appears in.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,31 @@ public OptionalControlDropdownItem(EditorInputControlLayoutCache.OptionalControl
}
}

internal sealed class UsageDropdownItem : InputControlDropdownItem
internal sealed class ControlUsageDropdownItem : InputControlDropdownItem
{
public override string controlPathWithDevice => $"{m_Device}/{{{m_ControlPath}}}";
public override string controlPathWithDevice => BuildControlPath();
private string BuildControlPath()
{
if (m_Device == "*")
{
var path = new StringBuilder(m_Device);
if (!string.IsNullOrEmpty(m_Usage))
path.Append($"{{{m_Usage}}}");
if (!string.IsNullOrEmpty(m_ControlPath))
path.Append($"/{m_ControlPath}");
return path.ToString();
}
else
return base.controlPathWithDevice;
}

public UsageDropdownItem(string usage)
public ControlUsageDropdownItem(string device, string usage, string controlUsage)
: base(usage)
{
m_Device = "*";
m_ControlPath = usage;
m_Device = string.IsNullOrEmpty(device) ? "*" : device;
m_Usage = usage;
m_ControlPath = $"{{{ controlUsage }}}";
name = controlUsage;
id = controlPathWithDevice.GetHashCode();
m_Searchable = true;
}
Expand Down
Loading