diff --git a/Assets/Tests/InputSystem/CoreTests_Layouts.cs b/Assets/Tests/InputSystem/CoreTests_Layouts.cs index a3b76d5f88..fc9eaa7b75 100644 --- a/Assets/Tests/InputSystem/CoreTests_Layouts.cs +++ b/Assets/Tests/InputSystem/CoreTests_Layouts.cs @@ -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(); + InputSystem.RegisterLayout(jsonBase); + InputSystem.RegisterLayout(jsonDerived); + + var layout = InputSystem.LoadLayout("DerivedLayout"); + var parsedPath = InputControlPath.Parse("/ControlWithExplicitDefaultVariant").ToArray()[1]; + + Assert.That(layout.m_Controls.Any(x => InputControlPath.MatchControlComponent(ref parsedPath, ref x)), Is.True); + } + [Test] [Category("Layouts")] [Ignore("TODO")] diff --git a/Packages/com.unity.inputsystem/InputSystem/Controls/InputControlPath.cs b/Packages/com.unity.inputsystem/InputSystem/Controls/InputControlPath.cs index 5cbfa4a41c..0edc1e4b6a 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Controls/InputControlPath.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Controls/InputControlPath.cs @@ -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; + } + /// /// Check whether the given path matches or any of its parents. /// diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/AssetEditor/InputBindingPropertiesView.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/AssetEditor/InputBindingPropertiesView.cs index d8de344c92..2abb958468 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/AssetEditor/InputBindingPropertiesView.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/AssetEditor/InputBindingPropertiesView.cs @@ -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; @@ -61,6 +62,7 @@ public void Dispose() m_ControlPathEditor?.Dispose(); } + private static bool showPaths = false; protected override void DrawGeneralProperties() { var currentPath = m_PathProperty.stringValue; @@ -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(); } } + /// + /// Finds all registered control paths implemented by concrete classes which match the current binding path and renders it. + /// + 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(); + } + + /// + /// 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. + /// + /// The device layout to draw control paths for + /// The parsed path component containing details of the Input Controls that can be matched + 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; + } + + + /// /// Draw control scheme matrix that allows selecting which control schemes a particular /// binding appears in. diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/ControlPicker/InputControlDropdownItem.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/ControlPicker/InputControlDropdownItem.cs index f473cdb830..a1bc18709d 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/ControlPicker/InputControlDropdownItem.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/ControlPicker/InputControlDropdownItem.cs @@ -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; } diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/ControlPicker/InputControlPickerDropdown.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/ControlPicker/InputControlPickerDropdown.cs index 27aeb10d5a..5f595a6a1d 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/ControlPicker/InputControlPickerDropdown.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/ControlPicker/InputControlPickerDropdown.cs @@ -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); @@ -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); } } @@ -183,12 +183,21 @@ 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); @@ -196,6 +205,14 @@ private void AddDeviceTreeItemRecursive(InputControlLayout layout, AdvancedDropd 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) {