|
2 | 2 | using System;
|
3 | 3 | using System.Collections.Generic;
|
4 | 4 | using System.Linq;
|
| 5 | +using System.Reflection; |
5 | 6 | using UnityEditor;
|
6 | 7 | using UnityEngine.InputSystem.Editor.Lists;
|
7 | 8 | using UnityEngine.InputSystem.Layouts;
|
@@ -102,11 +103,187 @@ protected override void DrawGeneralProperties()
|
102 | 103 | }
|
103 | 104 | }
|
104 | 105 |
|
| 106 | + // Show the specific controls which match the current path |
| 107 | + DrawMatchingControlPaths(); |
| 108 | + |
105 | 109 | // Control scheme matrix.
|
106 | 110 | DrawUseInControlSchemes();
|
107 | 111 | }
|
108 | 112 | }
|
109 | 113 |
|
| 114 | + /// <summary> |
| 115 | + /// Used to keep track of which foldouts are expanded. |
| 116 | + /// </summary> |
| 117 | + private static bool showMatchingLayouts = false; |
| 118 | + private static Dictionary<string, bool> showMatchingChildLayouts = new Dictionary<string, bool>(); |
| 119 | + |
| 120 | + /// <summary> |
| 121 | + /// Finds all registered control paths implemented by concrete classes which match the current binding path and renders it. |
| 122 | + /// </summary> |
| 123 | + private void DrawMatchingControlPaths() |
| 124 | + { |
| 125 | + var path = m_ControlPathEditor.pathProperty.stringValue; |
| 126 | + if (path == string.Empty) |
| 127 | + return; |
| 128 | + |
| 129 | + var deviceLayoutPath = InputControlPath.TryGetDeviceLayout(path); |
| 130 | + var parsedPath = InputControlPath.Parse(path).ToArray(); |
| 131 | + |
| 132 | + // If the provided path is parseable into device and control components, draw UI which shows control layouts that match the path. |
| 133 | + if (parsedPath.Length >= 2 && !string.IsNullOrEmpty(deviceLayoutPath)) |
| 134 | + { |
| 135 | + bool matchExists = false; |
| 136 | + |
| 137 | + var rootDeviceLayout = EditorInputControlLayoutCache.TryGetLayout(deviceLayoutPath); |
| 138 | + bool isValidDeviceLayout = deviceLayoutPath == InputControlPath.Wildcard || (rootDeviceLayout != null && !rootDeviceLayout.isOverride && !rootDeviceLayout.hideInUI); |
| 139 | + // Exit early if a malformed device layout was provided, |
| 140 | + if (!isValidDeviceLayout) |
| 141 | + return; |
| 142 | + |
| 143 | + bool controlPathUsagePresent = parsedPath[1].usages.Count() > 0; |
| 144 | + bool hasChildDeviceLayouts = deviceLayoutPath == InputControlPath.Wildcard || EditorInputControlLayoutCache.HasChildLayouts(rootDeviceLayout.name); |
| 145 | + |
| 146 | + // 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 |
| 147 | + if (!controlPathUsagePresent && !hasChildDeviceLayouts) |
| 148 | + return; |
| 149 | + |
| 150 | + // Otherwise, we will show either all controls that match the current binding (if control usages are used) |
| 151 | + // or all controls in derived device layouts (if a no control usages are used). |
| 152 | + EditorGUILayout.BeginVertical(); |
| 153 | + showMatchingLayouts = EditorGUILayout.Foldout(showMatchingLayouts, "Derived Bindings"); |
| 154 | + |
| 155 | + if (showMatchingLayouts) |
| 156 | + { |
| 157 | + // If our control path contains a usage, make sure we render the binding that belongs to the root device layout first |
| 158 | + if (deviceLayoutPath != InputControlPath.Wildcard && controlPathUsagePresent) |
| 159 | + { |
| 160 | + matchExists |= DrawMatchingControlPathsForLayout(rootDeviceLayout, in parsedPath, true); |
| 161 | + } |
| 162 | + // Otherwise, just render the bindings that belong to child device layouts. The binding that matches the root layout is |
| 163 | + // already represented by the user generated control path itself. |
| 164 | + else |
| 165 | + { |
| 166 | + IEnumerable<InputControlLayout> matchedChildLayouts = Enumerable.Empty<InputControlLayout>(); |
| 167 | + if (deviceLayoutPath == InputControlPath.Wildcard) |
| 168 | + { |
| 169 | + matchedChildLayouts = EditorInputControlLayoutCache.allLayouts |
| 170 | + .Where(x => x.isDeviceLayout && !x.hideInUI && !x.isOverride && x.isGenericTypeOfDevice && x.baseLayouts.Count() == 0).OrderBy(x => x.displayName); |
| 171 | + } |
| 172 | + else |
| 173 | + { |
| 174 | + matchedChildLayouts = EditorInputControlLayoutCache.TryGetChildLayouts(rootDeviceLayout.name); |
| 175 | + } |
| 176 | + |
| 177 | + foreach (var childLayout in matchedChildLayouts) |
| 178 | + { |
| 179 | + matchExists |= DrawMatchingControlPathsForLayout(childLayout, in parsedPath); |
| 180 | + } |
| 181 | + } |
| 182 | + |
| 183 | + // Otherwise, indicate that no layouts match the current path. |
| 184 | + if (!matchExists) |
| 185 | + { |
| 186 | + if (controlPathUsagePresent) |
| 187 | + EditorGUILayout.HelpBox("No registered controls match this current binding. Some controls are only registered at runtime.", MessageType.Warning); |
| 188 | + else |
| 189 | + EditorGUILayout.HelpBox("No other registered controls match this current binding. Some controls are only registered at runtime.", MessageType.Warning); |
| 190 | + } |
| 191 | + } |
| 192 | + |
| 193 | + EditorGUILayout.EndVertical(); |
| 194 | + } |
| 195 | + } |
| 196 | + |
| 197 | + /// <summary> |
| 198 | + /// Returns true if the deviceLayout or any of its children has controls which match the provided parsed path. exist matching registered control paths. |
| 199 | + /// </summary> |
| 200 | + /// <param name="deviceLayout">The device layout to draw control paths for</param> |
| 201 | + /// <param name="parsedPath">The parsed path containing details of the Input Controls that can be matched</param> |
| 202 | + private bool DrawMatchingControlPathsForLayout(InputControlLayout deviceLayout, in InputControlPath.ParsedPathComponent[] parsedPath, bool isRoot = false) |
| 203 | + { |
| 204 | + string deviceName = deviceLayout.displayName; |
| 205 | + string controlName = string.Empty; |
| 206 | + bool matchExists = false; |
| 207 | + |
| 208 | + for (int i = 0; i < deviceLayout.m_Controls.Length; i++) |
| 209 | + { |
| 210 | + ref InputControlLayout.ControlItem controlItem = ref deviceLayout.m_Controls[i]; |
| 211 | + if (InputControlPath.MatchControlComponent(ref parsedPath[1], ref controlItem, true)) |
| 212 | + { |
| 213 | + // If we've already located a match, append a ", " to the control name |
| 214 | + // This is to accomodate cases where multiple control items match the same path within a single device layout |
| 215 | + // Note, some controlItems have names but invalid displayNames (i.e. the Dualsense HID > leftTriggerButton) |
| 216 | + // There are instance where there are 2 control items with the same name inside a layout definition, however they are not |
| 217 | + // labeled significantly differently. |
| 218 | + // The notable example is that the Android Xbox and Android Dualshock layouts have 2 d-pad definitions, one is a "button" |
| 219 | + // while the other is an axis. |
| 220 | + controlName += matchExists ? $", {controlItem.name}" : controlItem.name; |
| 221 | + |
| 222 | + // if the parsePath has a 3rd component, try to match it with items in the controlItem's layout definition. |
| 223 | + if (parsedPath.Length == 3) |
| 224 | + { |
| 225 | + var controlLayout = EditorInputControlLayoutCache.TryGetLayout(controlItem.layout); |
| 226 | + if (controlLayout.isControlLayout && !controlLayout.hideInUI) |
| 227 | + { |
| 228 | + for (int j = 0; j < controlLayout.m_Controls.Count(); j++) |
| 229 | + { |
| 230 | + ref InputControlLayout.ControlItem controlLayoutItem = ref controlLayout.m_Controls[j]; |
| 231 | + if (InputControlPath.MatchControlComponent(ref parsedPath[2], ref controlLayoutItem)) |
| 232 | + { |
| 233 | + controlName += $"/{controlLayoutItem.name}"; |
| 234 | + matchExists = true; |
| 235 | + } |
| 236 | + } |
| 237 | + } |
| 238 | + } |
| 239 | + else |
| 240 | + { |
| 241 | + matchExists = true; |
| 242 | + } |
| 243 | + } |
| 244 | + } |
| 245 | + |
| 246 | + IEnumerable<InputControlLayout> matchedChildLayouts = EditorInputControlLayoutCache.TryGetChildLayouts(deviceLayout.name); |
| 247 | + |
| 248 | + // If this layout does not have a match, or is the top level root layout, |
| 249 | + // skip over trying to draw any items for it, and immediately try processing the child layouts |
| 250 | + if (!matchExists) |
| 251 | + { |
| 252 | + foreach (var childLayout in matchedChildLayouts) |
| 253 | + { |
| 254 | + matchExists |= DrawMatchingControlPathsForLayout(childLayout, in parsedPath); |
| 255 | + } |
| 256 | + } |
| 257 | + // Otherwise, draw the items for it, and then only process the child layouts if the foldout is expanded. |
| 258 | + else |
| 259 | + { |
| 260 | + bool showLayout = false; |
| 261 | + EditorGUI.indentLevel++; |
| 262 | + if (matchedChildLayouts.Count() > 0 && !isRoot) |
| 263 | + { |
| 264 | + showMatchingChildLayouts.TryGetValue(deviceName, out showLayout); |
| 265 | + showMatchingChildLayouts[deviceName] = EditorGUILayout.Foldout(showLayout, $"{deviceName} > {controlName}"); |
| 266 | + } |
| 267 | + else |
| 268 | + { |
| 269 | + EditorGUILayout.LabelField($"{deviceName} > {controlName}"); |
| 270 | + } |
| 271 | + |
| 272 | + showLayout |= isRoot; |
| 273 | + |
| 274 | + if (showLayout) |
| 275 | + { |
| 276 | + foreach (var childLayout in matchedChildLayouts) |
| 277 | + { |
| 278 | + DrawMatchingControlPathsForLayout(childLayout, in parsedPath); |
| 279 | + } |
| 280 | + } |
| 281 | + EditorGUI.indentLevel--; |
| 282 | + } |
| 283 | + |
| 284 | + return matchExists; |
| 285 | + } |
| 286 | + |
110 | 287 | /// <summary>
|
111 | 288 | /// Draw control scheme matrix that allows selecting which control schemes a particular
|
112 | 289 | /// binding appears in.
|
|
0 commit comments