A game about forced loneliness, made by TACStudios
1#if UNITY_EDITOR 2using System.Linq; 3using UnityEditor; 4using UnityEngine.UIElements; 5using UnityEngine.InputSystem.Layouts; 6using UnityEngine.InputSystem.Utilities; 7using System.Collections.Generic; 8 9namespace UnityEngine.InputSystem.Editor 10{ 11 internal class MatchingControlPath 12 { 13 public string deviceName 14 { 15 get; 16 } 17 public string controlName 18 { 19 get; 20 } 21 22 public bool isRoot 23 { 24 get; 25 } 26 public List<MatchingControlPath> children 27 { 28 get; 29 } 30 31 32 public MatchingControlPath(string deviceName, string controlName, bool isRoot) 33 { 34 this.deviceName = deviceName; 35 this.controlName = controlName; 36 this.isRoot = isRoot; 37 this.children = new List<MatchingControlPath>(); 38 } 39 40#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS 41 public static List<TreeViewItemData<MatchingControlPath>> BuildMatchingControlPathsTreeData(List<MatchingControlPath> matchingControlPaths) 42 { 43 int id = 0; 44 return BuildMatchingControlPathsTreeDataRecursive(ref id, matchingControlPaths); 45 } 46 47 private static List<TreeViewItemData<MatchingControlPath>> BuildMatchingControlPathsTreeDataRecursive(ref int id, List<MatchingControlPath> matchingControlPaths) 48 { 49 var treeViewList = new List<TreeViewItemData<MatchingControlPath>>(matchingControlPaths.Count); 50 foreach (var matchingControlPath in matchingControlPaths) 51 { 52 var childTreeViewList = BuildMatchingControlPathsTreeDataRecursive(ref id, matchingControlPath.children); 53 54 var treeViewItem = new TreeViewItemData<MatchingControlPath>(id++, matchingControlPath, childTreeViewList); 55 treeViewList.Add(treeViewItem); 56 } 57 58 return treeViewList; 59 } 60 61#endif 62 63 public static List<MatchingControlPath> CollectMatchingControlPaths(string path, bool showPaths, ref bool controlPathUsagePresent) 64 { 65 var matchingControlPaths = new List<MatchingControlPath>(); 66 67 if (path == string.Empty) 68 return matchingControlPaths; 69 70 var deviceLayoutPath = InputControlPath.TryGetDeviceLayout(path); 71 var parsedPath = InputControlPath.Parse(path).ToArray(); 72 73 // If the provided path is parseable into device and control components, draw UI which shows control layouts that match the path. 74 if (parsedPath.Length >= 2 && !string.IsNullOrEmpty(deviceLayoutPath)) 75 { 76 bool matchExists = false; 77 78 var rootDeviceLayout = EditorInputControlLayoutCache.TryGetLayout(deviceLayoutPath); 79 bool isValidDeviceLayout = deviceLayoutPath == InputControlPath.Wildcard || (rootDeviceLayout != null && !rootDeviceLayout.isOverride && !rootDeviceLayout.hideInUI); 80 // Exit early if a malformed device layout was provided, 81 if (!isValidDeviceLayout) 82 return matchingControlPaths; 83 84 controlPathUsagePresent = parsedPath[1].usages.Count() > 0; 85 bool hasChildDeviceLayouts = deviceLayoutPath == InputControlPath.Wildcard || EditorInputControlLayoutCache.HasChildLayouts(rootDeviceLayout.name); 86 87 // 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 88 if (!controlPathUsagePresent && !hasChildDeviceLayouts) 89 return matchingControlPaths; 90 91 // Otherwise, we will show either all controls that match the current binding (if control usages are used) 92 // or all controls in derived device layouts (if a no control usages are used). 93 94 // If our control path contains a usage, make sure we render the binding that belongs to the root device layout first 95 if (deviceLayoutPath != InputControlPath.Wildcard && controlPathUsagePresent) 96 { 97 matchExists |= CollectMatchingControlPathsForLayout(rootDeviceLayout, in parsedPath, true, matchingControlPaths); 98 } 99 // Otherwise, just render the bindings that belong to child device layouts. The binding that matches the root layout is 100 // already represented by the user generated control path itself. 101 else 102 { 103 IEnumerable<InputControlLayout> matchedChildLayouts = Enumerable.Empty<InputControlLayout>(); 104 if (deviceLayoutPath == InputControlPath.Wildcard) 105 { 106 matchedChildLayouts = EditorInputControlLayoutCache.allLayouts 107 .Where(x => x.isDeviceLayout && !x.hideInUI && !x.isOverride && x.isGenericTypeOfDevice && x.baseLayouts.Count() == 0).OrderBy(x => x.displayName); 108 } 109 else 110 { 111 matchedChildLayouts = EditorInputControlLayoutCache.TryGetChildLayouts(rootDeviceLayout.name); 112 } 113 114 foreach (var childLayout in matchedChildLayouts) 115 { 116 matchExists |= CollectMatchingControlPathsForLayout(childLayout, in parsedPath, false, matchingControlPaths); 117 } 118 } 119 120 // Otherwise, indicate that no layouts match the current path. 121 if (!matchExists) 122 { 123 return null; 124 } 125 } 126 127 return matchingControlPaths; 128 } 129 130 /// <summary> 131 /// Returns true if the deviceLayout or any of its children has controls which match the provided parsed path. exist matching registered control paths. 132 /// </summary> 133 /// <param name="deviceLayout">The device layout to draw control paths for</param> 134 /// <param name="parsedPath">The parsed path containing details of the Input Controls that can be matched</param> 135 private static bool CollectMatchingControlPathsForLayout(InputControlLayout deviceLayout, in InputControlPath.ParsedPathComponent[] parsedPath, bool isRoot, List<MatchingControlPath> matchingControlPaths) 136 { 137 string deviceName = deviceLayout.displayName; 138 string controlName = string.Empty; 139 bool matchExists = false; 140 141 for (int i = 0; i < deviceLayout.m_Controls.Length; i++) 142 { 143 ref InputControlLayout.ControlItem controlItem = ref deviceLayout.m_Controls[i]; 144 if (InputControlPath.MatchControlComponent(ref parsedPath[1], ref controlItem, true)) 145 { 146 // If we've already located a match, append a ", " to the control name 147 // This is to accomodate cases where multiple control items match the same path within a single device layout 148 // Note, some controlItems have names but invalid displayNames (i.e. the Dualsense HID > leftTriggerButton) 149 // There are instance where there are 2 control items with the same name inside a layout definition, however they are not 150 // labeled significantly differently. 151 // The notable example is that the Android Xbox and Android Dualshock layouts have 2 d-pad definitions, one is a "button" 152 // while the other is an axis. 153 controlName += matchExists ? $", {controlItem.name}" : controlItem.name; 154 155 // if the parsePath has a 3rd component, try to match it with items in the controlItem's layout definition. 156 if (parsedPath.Length == 3) 157 { 158 var controlLayout = EditorInputControlLayoutCache.TryGetLayout(controlItem.layout); 159 if (controlLayout.isControlLayout && !controlLayout.hideInUI) 160 { 161 for (int j = 0; j < controlLayout.m_Controls.Count(); j++) 162 { 163 ref InputControlLayout.ControlItem controlLayoutItem = ref controlLayout.m_Controls[j]; 164 if (InputControlPath.MatchControlComponent(ref parsedPath[2], ref controlLayoutItem)) 165 { 166 controlName += $"/{controlLayoutItem.name}"; 167 matchExists = true; 168 } 169 } 170 } 171 } 172 else 173 { 174 matchExists = true; 175 } 176 } 177 } 178 179 IEnumerable<InputControlLayout> matchedChildLayouts = EditorInputControlLayoutCache.TryGetChildLayouts(deviceLayout.name); 180 181 // If this layout does not have a match, or is the top level root layout, 182 // skip over trying to draw any items for it, and immediately try processing the child layouts 183 if (!matchExists) 184 { 185 foreach (var childLayout in matchedChildLayouts) 186 { 187 matchExists |= CollectMatchingControlPathsForLayout(childLayout, in parsedPath, false, matchingControlPaths); 188 } 189 } 190 // Otherwise, draw the items for it, and then only process the child layouts if the foldout is expanded. 191 else 192 { 193 var newMatchingControlPath = new MatchingControlPath(deviceName, controlName, isRoot); 194 matchingControlPaths.Add(newMatchingControlPath); 195 196 foreach (var childLayout in matchedChildLayouts) 197 { 198 CollectMatchingControlPathsForLayout(childLayout, in parsedPath, false, newMatchingControlPath.children); 199 } 200 } 201 202 return matchExists; 203 } 204 } 205} 206#endif