Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
37 changes: 37 additions & 0 deletions Assets/Tests/InputSystem/CoreTests_Layouts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2546,6 +2546,43 @@ 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 }
]
}
";

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

[Test]
[Category("Layouts")]
[Ignore("TODO")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,39 @@ public static bool Matches(string expected, InputControl control)
return MatchesRecursive(ref parser, control);
}

internal static bool MatchControlComponent(ref ParsedPathComponent expectedControlComponent, ref InputControlLayout.ControlItem controlItem)
{
// 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;
var anyUsageMatches = false;
for (var i = 0; i < usageCount; ++i)
{
if (StringMatches(usage, controlItem.usages[i]))
{
anyUsageMatches = true;
break;
}
}

if (!anyUsageMatches)
return false;
}
}

// Match name.
if (!expectedControlComponent.m_Name.isEmpty)
{
if (!StringMatches(expectedControlComponent.m_Name, controlItem.name))
return false;
}

return true;
}

/// <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 @@ -61,6 +62,7 @@ public void Dispose()
m_ControlPathEditor?.Dispose();
}

private static bool showPaths = false;
protected override void DrawGeneralProperties()
{
var currentPath = m_PathProperty.stringValue;
Expand Down Expand Up @@ -102,11 +104,110 @@ protected override void DrawGeneralProperties()
}
}

showPaths = EditorGUILayout.Toggle("Show Matching Paths", showPaths);
// Show the specific layouts that implement the control on this path
if (showPaths)
{
// Control scheme matrix.
DrawMatchingControlPaths();
}
// Control scheme matrix.
DrawUseInControlSchemes();
}
}

/// <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;
var deviceLayout = new InternedString(InputControlPath.TryGetDeviceLayout(path));
var parsedPath = InputControlPath.Parse(path).ToArray();

bool matchExists = false;
EditorGUILayout.BeginVertical();
if (parsedPath.Length == 2)
{
if (deviceLayout != InputControlPath.Wildcard)
{
var rootLayout = EditorInputControlLayoutCache.allLayouts.FirstOrDefault(x => x.isDeviceLayout && !x.isOverride && !x.hideInUI && x.name == deviceLayout);
if (rootLayout != null)
{
for (int i = 0; i < rootLayout.m_Controls.Length; i++)
{
if (InputControlPath.MatchControlComponent(ref parsedPath[1], ref rootLayout.m_Controls[i]))
{
EditorGUILayout.LabelField($"{rootLayout.displayName}/{rootLayout.m_Controls[i].displayName}");
matchExists = true;
continue;
}
}

EditorGUI.indentLevel++;
matchExists |= DrawMatchingControlPathsForLayout(deviceLayout, ref parsedPath[1]);
EditorGUI.indentLevel--;
}
}
else
{
matchExists |= DrawMatchingControlPathsForLayout(deviceLayout, ref parsedPath[1]);
}
}

if (!matchExists)
{
EditorGUILayout.LabelField("No registered control paths match this current binding");
}

EditorGUILayout.EndVertical();
}

/// <summary>
/// Finds all registered control paths implemented by concrete classes under a given device layout which match the current binding path and renders it.
/// Return true if there exist matching registered control paths, false otherwise.
/// </summary>
/// <param name="deviceLayout">The device layout to draw control paths for</param>
/// <param name="pathControlComponent">The parsed path component containing details of the Input Controls that can be matched</param>
private bool DrawMatchingControlPathsForLayout(InternedString deviceLayout, ref InputControlPath.ParsedPathComponent pathControlComponent)
{
var path = m_ControlPathEditor.pathProperty.stringValue;
var matchedChildLayouts = EditorInputControlLayoutCache.allLayouts
.Where(x => x.isDeviceLayout && !x.isOverride && !x.hideInUI && x.baseLayouts.Contains(deviceLayout)).OrderBy(x => x.displayName);

if (deviceLayout == InputControlPath.Wildcard)
{
matchedChildLayouts = EditorInputControlLayoutCache.allLayouts
.Where(x => x.isDeviceLayout && !x.isOverride && !x.hideInUI && x.isGenericTypeOfDevice).OrderBy(x => x.displayName);
}

bool matchExists = false;

if (matchedChildLayouts.Count() > 0)
{
foreach (var childLayout in matchedChildLayouts)
{
for(int i = 0; i < childLayout.m_Controls.Length;i++)
{
if (InputControlPath.MatchControlComponent(ref pathControlComponent, ref childLayout.m_Controls[i]))
{
EditorGUILayout.LabelField($"{childLayout.displayName}/{childLayout.m_Controls[i].displayName}");
matchExists = true;
continue;
}
}

EditorGUI.indentLevel++;
matchExists |= DrawMatchingControlPathsForLayout(childLayout.name, ref pathControlComponent);
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
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ protected override AdvancedDropdownItem BuildRoot()
// Usages.
if (m_Mode != InputControlPicker.Mode.PickDevice)
{
var usages = BuildTreeForUsages();
var usages = BuildTreeForControlUsages();
if (usages.children.Any())
{
root.AddChild(usages);
Expand Down Expand Up @@ -124,14 +124,14 @@ protected override void ItemSelected(AdvancedDropdownItem item)
m_OnPickCallback(path);
}

private AdvancedDropdownItem BuildTreeForUsages()
private AdvancedDropdownItem BuildTreeForControlUsages(string device = "", string usage = "")
{
var usageRoot = new AdvancedDropdownItem("Usages");
foreach (var usageAndLayouts in EditorInputControlLayoutCache.allUsages)
{
if (usageAndLayouts.Item2.Any(LayoutMatchesExpectedControlLayoutFilter))
{
var child = new UsageDropdownItem(usageAndLayouts.Item1);
var child = new ControlUsageDropdownItem(device, usage, usageAndLayouts.Item1);
usageRoot.AddChild(child);
}
}
Expand Down Expand Up @@ -183,19 +183,36 @@ private void AddDeviceTreeItemRecursive(InputControlLayout layout, AdvancedDropd

var defaultControlPickerLayout = new DefaultInputControlPickerLayout();

// Add common usage variants.
// Add common usage variants of the device
if (layout.commonUsages.Count > 0)
{
foreach (var usage in layout.commonUsages)
{
var usageItem = new DeviceDropdownItem(layout, usage);

// Add control usages to the device variants
var deviceVariantControlUsages = BuildTreeForControlUsages(layout.name, usage);
if (deviceVariantControlUsages.children.Any())
{
usageItem.AddChild(deviceVariantControlUsages);
usageItem.AddSeparator();
}

if (m_Mode == InputControlPicker.Mode.PickControl)
AddControlTreeItemsRecursive(defaultControlPickerLayout, layout, usageItem, layout.name, usage, searchable);
deviceItem.AddChild(usageItem);
}
deviceItem.AddSeparator();
}

// Add control usages
var deviceControlUsages = BuildTreeForControlUsages(layout.name);
if (deviceControlUsages.children.Any())
{
deviceItem.AddChild(deviceControlUsages);
deviceItem.AddSeparator();
}

// Add controls.
if (m_Mode != InputControlPicker.Mode.PickDevice)
{
Expand Down