#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 children { get; } public MatchingControlPath(string deviceName, string controlName, bool isRoot) { this.deviceName = deviceName; this.controlName = controlName; this.isRoot = isRoot; this.children = new List(); } #if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS public static List> BuildMatchingControlPathsTreeData(List matchingControlPaths) { int id = 0; return BuildMatchingControlPathsTreeDataRecursive(ref id, matchingControlPaths); } private static List> BuildMatchingControlPathsTreeDataRecursive(ref int id, List matchingControlPaths) { var treeViewList = new List>(matchingControlPaths.Count); foreach (var matchingControlPath in matchingControlPaths) { var childTreeViewList = BuildMatchingControlPathsTreeDataRecursive(ref id, matchingControlPath.children); var treeViewItem = new TreeViewItemData(id++, matchingControlPath, childTreeViewList); treeViewList.Add(treeViewItem); } return treeViewList; } #endif public static List CollectMatchingControlPaths(string path, bool showPaths, ref bool controlPathUsagePresent) { var matchingControlPaths = new List(); 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 matchedChildLayouts = Enumerable.Empty(); 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; } /// /// Returns true if the deviceLayout or any of its children has controls which match the provided parsed path. exist matching registered control paths. /// /// The device layout to draw control paths for /// The parsed path containing details of the Input Controls that can be matched private static bool CollectMatchingControlPathsForLayout(InputControlLayout deviceLayout, in InputControlPath.ParsedPathComponent[] parsedPath, bool isRoot, List 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 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