A game about forced loneliness, made by TACStudios
1using System;
2using System.Text;
3using System.Collections.Generic;
4using System.Globalization;
5using System.Linq;
6using Unity.Collections;
7using UnityEngine.InputSystem.Layouts;
8using UnityEngine.InputSystem.Utilities;
9
10////TODO: allow stuff like "/gamepad/**/<button>"
11////TODO: add support for | (e.g. "<Gamepad>|<Joystick>/{PrimaryMotion}"
12////TODO: handle arrays
13////TODO: add method to extract control path
14
15////REVIEW: change "*/{PrimaryAction}" to "*/**/{PrimaryAction}" so that the hierarchy crawling becomes explicit?
16
17////REVIEW: rename to `InputPath`?
18
19namespace UnityEngine.InputSystem
20{
21 /// <summary>
22 /// Functions for working with control path specs (like "/gamepad/*stick").
23 /// </summary>
24 /// <remarks>
25 /// Control paths are a mini-language similar to regular expressions. They are used throughout
26 /// the input system as string "addresses" of input controls. At runtime, they can be matched
27 /// against the devices and controls present in the system to retrieve the actual endpoints to
28 /// receive input from.
29 ///
30 /// Like on a file system, a path is made up of components that are each separated by a
31 /// forward slash (<c>/</c>). Each such component in turn is made up of a set of fields that are
32 /// individually optional. However, one of the fields must be present (e.g. at least a name or
33 /// a wildcard).
34 ///
35 /// <example>
36 /// Field structure of each path component
37 /// <code>
38 /// <Layout>{Usage}#(DisplayName)Name
39 /// </code>
40 /// </example>
41 ///
42 /// * <c>Layout</c>: The name of the layout that the control must be based on (either directly or indirectly).
43 /// * <c>Usage</c>: The usage that the control or device has to have, i.e. must be found in <see
44 /// cref="InputControl.usages"/> This field can be repeated several times to require
45 /// multiple usages (e.g. <c>"{LeftHand}{Vertical}"</c>).
46 /// * <c>DisplayName</c>: The name that <see cref="InputControl.displayName"/> of the control or device
47 /// must match.
48 /// * <c>Name</c>: The name that <see cref="InputControl.name"/> or one of the entries in
49 /// <see cref="InputControl.aliases"/> must match. Alternatively, this can be a
50 /// wildcard (<c>*</c>) to match any name.
51 ///
52 /// Note that all matching is case-insensitive.
53 ///
54 /// <example>
55 /// Various examples of control paths
56 /// <code>
57 /// // Matches all gamepads (also gamepads *based* on the Gamepad layout):
58 /// "<Gamepad>"
59 ///
60 /// // Matches the "Submit" control on all devices:
61 /// "*/{Submit}"
62 ///
63 /// // Matches the key that prints the "a" character on the current keyboard layout:
64 /// "<Keyboard>/#(a)"
65 ///
66 /// // Matches the X axis of the left stick on a gamepad.
67 /// "<Gamepad>/leftStick/x"
68 ///
69 /// // Matches the orientation control of the right-hand XR controller:
70 /// "<XRController>{RightHand}/orientation"
71 ///
72 /// // Matches all buttons on a gamepad.
73 /// "<Gamepad>/<Button>"
74 /// </code>
75 /// </example>
76 ///
77 /// The structure of the API of this class is similar in spirit to <c>System.IO.Path</c>, i.e. it offers
78 /// a range of static methods that perform various operations on path strings.
79 ///
80 /// To query controls on devices present in the system using control paths, use
81 /// <see cref="InputSystem.FindControls"/>. Also, control paths can be used with
82 /// <see cref="InputControl.this[string]"/> on every control. This makes it possible
83 /// to do things like:
84 ///
85 /// <example>
86 /// Find key that prints "t" on current keyboard:
87 /// <code>
88 /// Keyboard.current["#(t)"]
89 /// </code>
90 /// </example>
91 /// </remarks>
92 /// <seealso cref="InputControl.path"/>
93 /// <seealso cref="InputSystem.FindControls"/>
94 public static class InputControlPath
95 {
96 public const string Wildcard = "*";
97 public const string DoubleWildcard = "**";
98
99 public const char Separator = '/';
100
101 // We consider / a reserved character in control names. So, when this character does creep
102 // in there (e.g. from a device product name), we need to do something about it. We replace
103 // it with this character here.
104 // NOTE: Display names have no such restriction.
105 // NOTE: There are some Unicode characters that look sufficiently like a slash (e.g. FULLWIDTH SOLIDUS)
106 // but that only makes for rather confusing output. So we just replace with a blank.
107 internal const char SeparatorReplacement = ' ';
108
109 internal static string CleanSlashes(this String pathComponent)
110 {
111 return pathComponent.Replace(Separator, SeparatorReplacement);
112 }
113
114 public static string Combine(InputControl parent, string path)
115 {
116 if (parent == null)
117 {
118 if (string.IsNullOrEmpty(path))
119 return string.Empty;
120
121 if (path[0] != Separator)
122 return Separator + path;
123
124 return path;
125 }
126 if (string.IsNullOrEmpty(path))
127 return parent.path;
128
129 return $"{parent.path}/{path}";
130 }
131
132 /// <summary>
133 /// Options for customizing the behavior of <see cref="ToHumanReadableString(string,HumanReadableStringOptions,InputControl)"/>.
134 /// </summary>
135 [Flags]
136 public enum HumanReadableStringOptions
137 {
138 /// <summary>
139 /// The default behavior.
140 /// </summary>
141 None = 0,
142
143 /// <summary>
144 /// Do not mention the device of the control. For example, instead of "A [Gamepad]",
145 /// return just "A".
146 /// </summary>
147 OmitDevice = 1 << 1,
148
149 /// <summary>
150 /// When available, use short display names instead of long ones. For example, instead of "Left Button",
151 /// return "LMB".
152 /// </summary>
153 UseShortNames = 1 << 2,
154 }
155
156 ////TODO: factor out the part that looks up an InputControlLayout.ControlItem from a given path
157 //// and make that available as a stand-alone API
158 ////TODO: add option to customize path separation character
159 /// <summary>
160 /// Create a human readable string from the given control path.
161 /// </summary>
162 /// <param name="path">A control path such as "<XRController>{LeftHand}/position".</param>
163 /// <param name="options">Customize the resulting string.</param>
164 /// <param name="control">An optional control. If supplied and the control or one of its children
165 /// matches the given <paramref name="path"/>, display names will be based on the matching control
166 /// rather than on static information available from <see cref="InputControlLayout"/>s.</param>
167 /// <returns>A string such as "Left Stick/X [Gamepad]".</returns>
168 /// <remarks>
169 /// This function is most useful for turning binding paths (see <see cref="InputBinding.path"/>)
170 /// into strings that can be displayed in UIs (such as rebinding screens). It is used by
171 /// the Unity editor itself to display binding paths in the UI.
172 ///
173 /// The method uses display names (see <see cref="InputControlAttribute.displayName"/>,
174 /// <see cref="InputControlLayoutAttribute.displayName"/>, and <see cref="InputControlLayout.ControlItem.displayName"/>)
175 /// where possible. For example, "<XInputController>/buttonSouth" will be returned as
176 /// "A [Xbox Controller]" as the display name of <see cref="XInput.XInputController"/> is "XBox Controller"
177 /// and the display name of its "buttonSouth" control is "A".
178 ///
179 /// Note that these lookups depend on the currently registered control layouts (see <see
180 /// cref="InputControlLayout"/>) and different strings may thus be returned for the same control
181 /// path depending on the layouts registered with the system.
182 ///
183 /// <example>
184 /// <code>
185 /// InputControlPath.ToHumanReadableString("*/{PrimaryAction"); // -> "PrimaryAction [Any]"
186 /// InputControlPath.ToHumanReadableString("<Gamepad>/buttonSouth"); // -> "Button South [Gamepad]"
187 /// InputControlPath.ToHumanReadableString("<XInputController>/buttonSouth"); // -> "A [Xbox Controller]"
188 /// InputControlPath.ToHumanReadableString("<Gamepad>/leftStick/x"); // -> "Left Stick/X [Gamepad]"
189 /// </code>
190 /// </example>
191 /// </remarks>
192 /// <seealso cref="InputBinding.path"/>
193 /// <seealso cref="InputBinding.ToDisplayString(InputBinding.DisplayStringOptions,InputControl)"/>
194 /// <seealso cref="InputActionRebindingExtensions.GetBindingDisplayString(InputAction,int,InputBinding.DisplayStringOptions)"/>
195 public static string ToHumanReadableString(string path,
196 HumanReadableStringOptions options = HumanReadableStringOptions.None,
197 InputControl control = null)
198 {
199 return ToHumanReadableString(path, out _, out _, options, control);
200 }
201
202 /// <summary>
203 /// Create a human readable string from the given control path.
204 /// </summary>
205 /// <param name="path">A control path such as "<XRController>{LeftHand}/position".</param>
206 /// <param name="deviceLayoutName">Receives the name of the device layout that the control path was resolved to.
207 /// This is useful </param>
208 /// <param name="controlPath">Receives the path to the referenced control on the device or <c>null</c> if not applicable.
209 /// For example, with a <paramref name="path"/> of <c>"<Gamepad>/dpad/up"</c>, the resulting control path
210 /// will be <c>"dpad/up"</c>. This is useful when trying to look up additional resources (such as images) based on the
211 /// control that is being referenced.</param>
212 /// <param name="options">Customize the resulting string.</param>
213 /// <param name="control">An optional control. If supplied and the control or one of its children
214 /// matches the given <paramref name="path"/>, display names will be based on the matching control
215 /// rather than on static information available from <see cref="InputControlLayout"/>s.</param>
216 /// <returns>A string such as "Left Stick/X [Gamepad]".</returns>
217 /// <remarks>
218 /// This function is most useful for turning binding paths (see <see cref="InputBinding.path"/>)
219 /// into strings that can be displayed in UIs (such as rebinding screens). It is used by
220 /// the Unity editor itself to display binding paths in the UI.
221 ///
222 /// The method uses display names (see <see cref="InputControlAttribute.displayName"/>,
223 /// <see cref="InputControlLayoutAttribute.displayName"/>, and <see cref="InputControlLayout.ControlItem.displayName"/>)
224 /// where possible. For example, "<XInputController>/buttonSouth" will be returned as
225 /// "A [Xbox Controller]" as the display name of <see cref="XInput.XInputController"/> is "XBox Controller"
226 /// and the display name of its "buttonSouth" control is "A".
227 ///
228 /// Note that these lookups depend on the currently registered control layouts (see <see
229 /// cref="InputControlLayout"/>) and different strings may thus be returned for the same control
230 /// path depending on the layouts registered with the system.
231 ///
232 /// <example>
233 /// <code>
234 /// InputControlPath.ToHumanReadableString("*/{PrimaryAction"); // -> "PrimaryAction [Any]"
235 /// InputControlPath.ToHumanReadableString("<Gamepad>/buttonSouth"); // -> "Button South [Gamepad]"
236 /// InputControlPath.ToHumanReadableString("<XInputController>/buttonSouth"); // -> "A [Xbox Controller]"
237 /// InputControlPath.ToHumanReadableString("<Gamepad>/leftStick/x"); // -> "Left Stick/X [Gamepad]"
238 /// </code>
239 /// </example>
240 /// </remarks>
241 /// <seealso cref="InputBinding.path"/>
242 /// <seealso cref="InputBinding.ToDisplayString(InputBinding.DisplayStringOptions,InputControl)"/>
243 /// <seealso cref="InputActionRebindingExtensions.GetBindingDisplayString(InputAction,int,InputBinding.DisplayStringOptions)"/>
244 public static string ToHumanReadableString(string path,
245 out string deviceLayoutName,
246 out string controlPath,
247 HumanReadableStringOptions options = HumanReadableStringOptions.None,
248 InputControl control = null)
249 {
250 deviceLayoutName = null;
251 controlPath = null;
252
253 if (string.IsNullOrEmpty(path))
254 return string.Empty;
255
256 // If we have a control, see if the path matches something in its hierarchy. If so,
257 // don't both parsing the path and just use the matched control for creating a display
258 // string.
259 if (control != null)
260 {
261 var childControl = TryFindControl(control, path);
262 var matchedControl = childControl ?? (Matches(path, control) ? control : null);
263
264 if (matchedControl != null)
265 {
266 var text = (options & HumanReadableStringOptions.UseShortNames) != 0 &&
267 !string.IsNullOrEmpty(matchedControl.shortDisplayName)
268 ? matchedControl.shortDisplayName
269 : matchedControl.displayName;
270
271 if ((options & HumanReadableStringOptions.OmitDevice) == 0)
272 text = $"{text} [{matchedControl.device.displayName}]";
273
274 deviceLayoutName = matchedControl.device.layout;
275 if (!(matchedControl is InputDevice))
276 controlPath = matchedControl.path.Substring(matchedControl.device.path.Length + 1);
277
278 return text;
279 }
280 }
281
282 var buffer = new StringBuilder();
283 var parser = new PathParser(path);
284
285 // For display names of controls and devices, we need to look at InputControlLayouts.
286 // If none is in place here, we establish a temporary layout cache while we go through
287 // the path. If one is in place already, we reuse what's already there.
288 using (InputControlLayout.CacheRef())
289 {
290 // First level is taken to be device.
291 if (parser.MoveToNextComponent())
292 {
293 // Keep track of which control layout we're on (if any) as we're crawling
294 // down the path.
295 var device = parser.current.ToHumanReadableString(null, null, out var currentLayoutName, out var _, options);
296 deviceLayoutName = currentLayoutName;
297
298 // Any additional levels (if present) are taken to form a control path on the device.
299 var isFirstControlLevel = true;
300 while (parser.MoveToNextComponent())
301 {
302 if (!isFirstControlLevel)
303 buffer.Append('/');
304
305 buffer.Append(parser.current.ToHumanReadableString(
306 currentLayoutName, controlPath, out currentLayoutName, out controlPath, options));
307 isFirstControlLevel = false;
308 }
309
310 if ((options & HumanReadableStringOptions.OmitDevice) == 0 && !string.IsNullOrEmpty(device))
311 {
312 buffer.Append(" [");
313 buffer.Append(device);
314 buffer.Append(']');
315 }
316 }
317
318 // If we didn't manage to figure out a display name, default to displaying
319 // the path as is.
320 if (buffer.Length == 0)
321 return path;
322
323 return buffer.ToString();
324 }
325 }
326
327 public static string[] TryGetDeviceUsages(string path)
328 {
329 if (path == null)
330 throw new ArgumentNullException(nameof(path));
331
332 var parser = new PathParser(path);
333 if (!parser.MoveToNextComponent())
334 return null;
335
336 if (parser.current.m_Usages.length > 0)
337 return parser.current.m_Usages.ToArray(x => x.ToString());
338
339 return null;
340 }
341
342 /// <summary>
343 /// From the given control path, try to determine the device layout being used.
344 /// </summary>
345 /// <remarks>
346 /// This function will only use information available in the path itself or
347 /// in layouts referenced by the path. It will not look at actual devices
348 /// in the system. This is to make the behavior predictable and not dependent
349 /// on whether you currently have the right device connected or not.
350 /// </remarks>
351 /// <param name="path">A control path (like "/<gamepad>/leftStick")</param>
352 /// <returns>The name of the device layout used by the given control path or null
353 /// if the path does not specify a device layout or does so in a way that is not
354 /// supported by the function.</returns>
355 /// <exception cref="ArgumentNullException"><paramref name="path"/> is null</exception>
356 /// <example>
357 /// <code>
358 /// InputControlPath.TryGetDeviceLayout("/<gamepad>/leftStick"); // Returns "gamepad".
359 /// InputControlPath.TryGetDeviceLayout("/*/leftStick"); // Returns "*".
360 /// InputControlPath.TryGetDeviceLayout("/gamepad/leftStick"); // Returns null. "gamepad" is a device name here.
361 /// </code>
362 /// </example>
363 public static string TryGetDeviceLayout(string path)
364 {
365 if (path == null)
366 throw new ArgumentNullException(nameof(path));
367
368 var parser = new PathParser(path);
369 if (!parser.MoveToNextComponent())
370 return null;
371
372 if (parser.current.m_Layout.length > 0)
373 return parser.current.m_Layout.ToString().Unescape();
374
375 if (parser.current.isWildcard)
376 return Wildcard;
377
378 return null;
379 }
380
381 ////TODO: return Substring and use path parser; should get rid of allocations
382
383 // From the given control path, try to determine the control layout being used.
384 // NOTE: Allocates!
385 public static string TryGetControlLayout(string path)
386 {
387 if (path == null)
388 throw new ArgumentNullException(nameof(path));
389 var pathLength = path.Length;
390
391 var indexOfLastSlash = path.LastIndexOf('/');
392 if (indexOfLastSlash == -1 || indexOfLastSlash == 0)
393 {
394 // If there's no '/' at all in the path, it surely does not mention
395 // a control. Same if the '/' is the first thing in the path.
396 return null;
397 }
398
399 // Simplest case where control layout is mentioned explicitly with '<..>'.
400 // Note this will only catch if the control is *only* referenced by layout and not by anything else
401 // in addition (like usage or name).
402 if (pathLength > indexOfLastSlash + 2 && path[indexOfLastSlash + 1] == '<' && path[pathLength - 1] == '>')
403 {
404 var layoutNameStart = indexOfLastSlash + 2;
405 var layoutNameLength = pathLength - layoutNameStart - 1;
406 return path.Substring(layoutNameStart, layoutNameLength);
407 }
408
409 // Have to actually look at the path in detail.
410 var parser = new PathParser(path);
411 if (!parser.MoveToNextComponent())
412 return null;
413
414 if (parser.current.isWildcard)
415 throw new NotImplementedException();
416
417 if (parser.current.m_Layout.length == 0)
418 return null;
419
420 var deviceLayoutName = parser.current.m_Layout.ToString();
421 if (!parser.MoveToNextComponent())
422 return null; // No control component.
423
424 if (parser.current.isWildcard)
425 return Wildcard;
426
427 return FindControlLayoutRecursive(ref parser, deviceLayoutName.Unescape());
428 }
429
430 private static string FindControlLayoutRecursive(ref PathParser parser, string layoutName)
431 {
432 using (InputControlLayout.CacheRef())
433 {
434 // Load layout.
435 var layout = InputControlLayout.cache.FindOrLoadLayout(new InternedString(layoutName), throwIfNotFound: false);
436 if (layout == null)
437 return null;
438
439 // Search for control layout. May have to jump to other layouts
440 // and search in them.
441 return FindControlLayoutRecursive(ref parser, layout);
442 }
443 }
444
445 private static string FindControlLayoutRecursive(ref PathParser parser, InputControlLayout layout)
446 {
447 string currentResult = null;
448
449 var controlCount = layout.controls.Count;
450 for (var i = 0; i < controlCount; ++i)
451 {
452 ////TODO: shortcut the search if we have a match and there's no wildcards to consider
453
454 // Skip control layout if it doesn't match.
455 if (!ControlLayoutMatchesPathComponent(ref layout.m_Controls[i], ref parser))
456 continue;
457
458 var controlLayoutName = layout.m_Controls[i].layout;
459
460 // If there's more in the path, try to dive into children by jumping to the
461 // control's layout.
462 if (!parser.isAtEnd)
463 {
464 var childPathParser = parser;
465 if (childPathParser.MoveToNextComponent())
466 {
467 var childControlLayoutName = FindControlLayoutRecursive(ref childPathParser, controlLayoutName);
468 if (childControlLayoutName != null)
469 {
470 if (currentResult != null && childControlLayoutName != currentResult)
471 return null;
472 currentResult = childControlLayoutName;
473 }
474 }
475 }
476 else if (currentResult != null && controlLayoutName != currentResult)
477 return null;
478 else
479 currentResult = controlLayoutName.ToString();
480 }
481
482 return currentResult;
483 }
484
485 private static bool ControlLayoutMatchesPathComponent(ref InputControlLayout.ControlItem controlItem, ref PathParser parser)
486 {
487 // Match layout.
488 var layout = parser.current.m_Layout;
489 if (layout.length > 0)
490 {
491 if (!StringMatches(layout, controlItem.layout))
492 return false;
493 }
494
495 // Match usage.
496 if (parser.current.m_Usages.length > 0)
497 {
498 // All of usages should match to the one of usage in the control
499 for (int usageIndex = 0; usageIndex < parser.current.m_Usages.length; ++usageIndex)
500 {
501 var usage = parser.current.m_Usages[usageIndex];
502
503 if (usage.length > 0)
504 {
505 var usageCount = controlItem.usages.Count;
506 var anyUsageMatches = false;
507 for (var i = 0; i < usageCount; ++i)
508 {
509 if (StringMatches(usage, controlItem.usages[i]))
510 {
511 anyUsageMatches = true;
512 break;
513 }
514 }
515
516 if (!anyUsageMatches)
517 return false;
518 }
519 }
520 }
521
522 // Match name.
523 var name = parser.current.m_Name;
524 if (name.length > 0)
525 {
526 if (!StringMatches(name, controlItem.name))
527 return false;
528 }
529
530 return true;
531 }
532
533 // Match two name strings allowing for wildcards.
534 // 'str' may contain wildcards. 'matchTo' may not.
535 private static bool StringMatches(Substring str, InternedString matchTo)
536 {
537 var strLength = str.length;
538 var matchToLength = matchTo.length;
539
540 // Can't compare lengths here because str may contain wildcards and
541 // thus be shorter than matchTo and still match.
542
543 var matchToLowerCase = matchTo.ToLower();
544
545 // We manually walk the string here so that we can deal with "normal"
546 // comparisons as well as with wildcards.
547 var posInMatchTo = 0;
548 var posInStr = 0;
549 while (posInStr < strLength && posInMatchTo < matchToLength)
550 {
551 var nextChar = str[posInStr];
552 if (nextChar == '\\' && posInStr + 1 < strLength)
553 nextChar = str[++posInStr];
554 if (nextChar == '*')
555 {
556 ////TODO: make sure we don't end up with ** here
557
558 if (posInStr == strLength - 1)
559 return true; // Wildcard at end of string so rest is matched.
560
561 ++posInStr;
562 nextChar = char.ToLower(str[posInStr], CultureInfo.InvariantCulture);
563
564 while (posInMatchTo < matchToLength && matchToLowerCase[posInMatchTo] != nextChar)
565 ++posInMatchTo;
566
567 if (posInMatchTo == matchToLength)
568 return false; // Matched all the way to end of matchTo but there's more in str after the wildcard.
569 }
570 else if (char.ToLower(nextChar, CultureInfo.InvariantCulture) != matchToLowerCase[posInMatchTo])
571 {
572 return false;
573 }
574
575 ++posInMatchTo;
576 ++posInStr;
577 }
578
579 return posInMatchTo == matchToLength && posInStr == strLength; // Check if we have consumed all input. Prevent prefix-only match.
580 }
581
582 public static InputControl TryFindControl(InputControl control, string path, int indexInPath = 0)
583 {
584 return TryFindControl<InputControl>(control, path, indexInPath);
585 }
586
587 public static InputControl[] TryFindControls(InputControl control, string path, int indexInPath = 0)
588 {
589 var matches = new InputControlList<InputControl>(Allocator.Temp);
590 try
591 {
592 TryFindControls(control, path, indexInPath, ref matches);
593 return matches.ToArray();
594 }
595 finally
596 {
597 matches.Dispose();
598 }
599 }
600
601 public static int TryFindControls(InputControl control, string path, ref InputControlList<InputControl> matches, int indexInPath = 0)
602 {
603 return TryFindControls(control, path, indexInPath, ref matches);
604 }
605
606 /// <summary>
607 /// Return the first child control that matches the given path.
608 /// </summary>
609 /// <param name="control">Control root at which to start the search.</param>
610 /// <param name="path">Path of the control to find. Can be <c>null</c> or empty, in which case <c>null</c>
611 /// is returned.</param>
612 /// <param name="indexInPath">Index in <paramref name="path"/> at which to start parsing. Defaults to
613 /// 0, i.e. parsing starts at the first character in the path.</param>
614 /// <returns>The first (direct or indirect) child control of <paramref name="control"/> that matches
615 /// <paramref name="path"/>.</returns>
616 /// <exception cref="ArgumentNullException"><paramref name="control"/> is <c>null</c>.</exception>
617 /// <remarks>
618 /// Does not allocate.
619 ///
620 /// Note that if multiple child controls match the given path, which one is returned depends on the
621 /// ordering of controls. The result should be considered indeterministic in this case.
622 ///
623 /// <example>
624 /// <code>
625 /// // Find X control of left stick on current gamepad.
626 /// InputControlPath.TryFindControl(Gamepad.current, "leftStick/x");
627 ///
628 /// // Find control with PrimaryAction usage on current mouse.
629 /// InputControlPath.TryFindControl(Mouse.current, "{PrimaryAction}");
630 /// </code>
631 /// </example>
632 /// </remarks>
633 /// <seealso cref="InputControl.this[string]"/>
634 public static TControl TryFindControl<TControl>(InputControl control, string path, int indexInPath = 0)
635 where TControl : InputControl
636 {
637 if (control == null)
638 throw new ArgumentNullException(nameof(control));
639 if (string.IsNullOrEmpty(path))
640 return null;
641
642 if (indexInPath == 0 && path[0] == '/')
643 ++indexInPath;
644
645 var none = new InputControlList<TControl>();
646 return MatchControlsRecursive(control, path, indexInPath, ref none, matchMultiple: false);
647 }
648
649 /// <summary>
650 /// Perform a search for controls starting with the given control as root and matching
651 /// the given path from the given position. Puts all matching controls on the list and
652 /// returns the number of controls that have been matched.
653 /// </summary>
654 /// <param name="control">Control at which the given path is rooted.</param>
655 /// <param name="path"></param>
656 /// <param name="indexInPath"></param>
657 /// <param name="matches"></param>
658 /// <typeparam name="TControl"></typeparam>
659 /// <returns></returns>
660 /// <exception cref="ArgumentNullException"></exception>
661 /// <remarks>
662 /// Matching is case-insensitive.
663 ///
664 /// Does not allocate managed memory.
665 /// </remarks>
666 public static int TryFindControls<TControl>(InputControl control, string path, int indexInPath,
667 ref InputControlList<TControl> matches)
668 where TControl : InputControl
669 {
670 if (control == null)
671 throw new ArgumentNullException(nameof(control));
672 if (path == null)
673 throw new ArgumentNullException(nameof(path));
674
675 if (indexInPath == 0 && path[0] == '/')
676 ++indexInPath;
677
678 var countBefore = matches.Count;
679 MatchControlsRecursive(control, path, indexInPath, ref matches, matchMultiple: true);
680 return matches.Count - countBefore;
681 }
682
683 ////REVIEW: what's the difference between TryFindChild and TryFindControl??
684
685 public static InputControl TryFindChild(InputControl control, string path, int indexInPath = 0)
686 {
687 return TryFindChild<InputControl>(control, path, indexInPath);
688 }
689
690 public static TControl TryFindChild<TControl>(InputControl control, string path, int indexInPath = 0)
691 where TControl : InputControl
692 {
693 if (control == null)
694 throw new ArgumentNullException(nameof(control));
695 if (path == null)
696 throw new ArgumentNullException(nameof(path));
697
698 var children = control.children;
699 var childCount = children.Count;
700 for (var i = 0; i < childCount; ++i)
701 {
702 var child = children[i];
703 var match = TryFindControl<TControl>(child, path, indexInPath);
704 if (match != null)
705 return match;
706 }
707
708 return null;
709 }
710
711 ////REVIEW: probably would be good to have a Matches(string,string) version
712
713 public static bool Matches(string expected, InputControl control)
714 {
715 if (string.IsNullOrEmpty(expected))
716 throw new ArgumentNullException(nameof(expected));
717 if (control == null)
718 throw new ArgumentNullException(nameof(control));
719
720 var parser = new PathParser(expected);
721 return MatchesRecursive(ref parser, control);
722 }
723
724 internal static bool MatchControlComponent(ref ParsedPathComponent expectedControlComponent, ref InputControlLayout.ControlItem controlItem, bool matchAlias = false)
725 {
726 bool controlItemNameMatched = false;
727 var anyUsageMatches = false;
728
729 // Check to see that there is a match with the name or alias if specified
730 // Exit early if we can't create a match.
731 if (!expectedControlComponent.m_Name.isEmpty)
732 {
733 if (StringMatches(expectedControlComponent.m_Name, controlItem.name))
734 controlItemNameMatched = true;
735 else if (matchAlias)
736 {
737 var aliases = controlItem.aliases;
738 for (var i = 0; i < aliases.Count; i++)
739 {
740 if (StringMatches(expectedControlComponent.m_Name, aliases[i]))
741 {
742 controlItemNameMatched = true;
743 break;
744 }
745 }
746 }
747 else
748 return false;
749 }
750
751 // All of usages should match to the one of usage in the control
752 foreach (var usage in expectedControlComponent.m_Usages)
753 {
754 if (!usage.isEmpty)
755 {
756 var usageCount = controlItem.usages.Count;
757 for (var i = 0; i < usageCount; ++i)
758 {
759 if (StringMatches(usage, controlItem.usages[i]))
760 {
761 anyUsageMatches = true;
762 break;
763 }
764 }
765 }
766 }
767
768 // Return whether or not we were able to match an alias or a usage
769 return controlItemNameMatched || anyUsageMatches;
770 }
771
772 /// <summary>
773 /// Check whether the given path matches <paramref name="control"/> or any of its parents.
774 /// </summary>
775 /// <param name="expected">A control path.</param>
776 /// <param name="control">An input control.</param>
777 /// <returns>True if the given path matches at least a partial path to <paramref name="control"/>.</returns>
778 /// <exception cref="ArgumentNullException"><paramref name="expected"/> is <c>null</c> or empty -or-
779 /// <paramref name="control"/> is <c>null</c>.</exception>
780 /// <remarks>
781 /// <example>
782 /// <code>
783 /// // True as the path matches the Keyboard device itself, i.e. the parent of
784 /// // Keyboard.aKey.
785 /// InputControlPath.MatchesPrefix("<Keyboard>", Keyboard.current.aKey);
786 ///
787 /// // False as the path matches none of the controls leading to Keyboard.aKey.
788 /// InputControlPath.MatchesPrefix("<Gamepad>", Keyboard.current.aKey);
789 ///
790 /// // True as the path matches Keyboard.aKey itself.
791 /// InputControlPath.MatchesPrefix("<Keyboard>/a", Keyboard.current.aKey);
792 /// </code>
793 /// </example>
794 /// </remarks>
795 public static bool MatchesPrefix(string expected, InputControl control)
796 {
797 if (string.IsNullOrEmpty(expected))
798 throw new ArgumentNullException(nameof(expected));
799 if (control == null)
800 throw new ArgumentNullException(nameof(control));
801
802 var parser = new PathParser(expected);
803 if (MatchesRecursive(ref parser, control, prefixOnly: true) && parser.isAtEnd)
804 return true;
805
806 return false;
807 }
808
809 private static bool MatchesRecursive(ref PathParser parser, InputControl currentControl, bool prefixOnly = false)
810 {
811 // Recurse into parent before looking at the current control. This
812 // will advance the parser to where our control is in the path.
813 var parent = currentControl.parent;
814 if (parent != null && !MatchesRecursive(ref parser, parent, prefixOnly))
815 return false;
816
817 // Stop if there's no more path left.
818 if (!parser.MoveToNextComponent())
819 return prefixOnly; // Failure if we match full path, success if we match prefix only.
820
821 // Match current path component against current control.
822 return parser.current.Matches(currentControl);
823 }
824
825 ////TODO: refactor this to use the new PathParser
826
827 /// <summary>
828 /// Recursively match path elements in <paramref name="path"/>.
829 /// </summary>
830 /// <param name="control">Current control we're at.</param>
831 /// <param name="path">Control path we are matching against.</param>
832 /// <param name="indexInPath">Index of current component in <paramref name="path"/>.</param>
833 /// <param name="matches"></param>
834 /// <param name="matchMultiple"></param>
835 /// <typeparam name="TControl"></typeparam>
836 /// <returns></returns>
837 private static TControl MatchControlsRecursive<TControl>(InputControl control, string path, int indexInPath,
838 ref InputControlList<TControl> matches, bool matchMultiple)
839 where TControl : InputControl
840 {
841 var pathLength = path.Length;
842
843 // Try to get a match. A path spec has three components:
844 // "<layout>{usage}name"
845 // All are optional but at least one component must be present.
846 // Names can be aliases, too.
847 // We don't tap InputControl.path strings of controls so as to not create a
848 // bunch of string objects while feeling our way down the hierarchy.
849
850 var controlIsMatch = true;
851
852 // Match by layout.
853 if (path[indexInPath] == '<')
854 {
855 ++indexInPath;
856 controlIsMatch =
857 MatchPathComponent(control.layout, path, ref indexInPath, PathComponentType.Layout);
858
859 // If the layout isn't a match, walk up the base layout
860 // chain and match each base layout.
861 if (!controlIsMatch)
862 {
863 var baseLayout = control.m_Layout;
864 while (InputControlLayout.s_Layouts.baseLayoutTable.TryGetValue(baseLayout, out baseLayout))
865 {
866 controlIsMatch = MatchPathComponent(baseLayout, path, ref indexInPath,
867 PathComponentType.Layout);
868 if (controlIsMatch)
869 break;
870 }
871 }
872 }
873
874 // Match by usage.
875 while (indexInPath < pathLength && path[indexInPath] == '{' && controlIsMatch)
876 {
877 ++indexInPath;
878
879 for (var i = 0; i < control.usages.Count; ++i)
880 {
881 controlIsMatch = MatchPathComponent(control.usages[i], path, ref indexInPath, PathComponentType.Usage);
882 if (controlIsMatch)
883 break;
884 }
885 }
886
887 // Match by display name.
888 if (indexInPath < pathLength - 1 && controlIsMatch && path[indexInPath] == '#' &&
889 path[indexInPath + 1] == '(')
890 {
891 indexInPath += 2;
892 controlIsMatch = MatchPathComponent(control.displayName, path, ref indexInPath,
893 PathComponentType.DisplayName);
894 }
895
896 // Match by name.
897 if (indexInPath < pathLength && controlIsMatch && path[indexInPath] != '/')
898 {
899 // Normal name match.
900 controlIsMatch = MatchPathComponent(control.name, path, ref indexInPath, PathComponentType.Name);
901
902 // Alternative match by alias.
903 if (!controlIsMatch)
904 {
905 for (var i = 0; i < control.aliases.Count && !controlIsMatch; ++i)
906 {
907 controlIsMatch = MatchPathComponent(control.aliases[i], path, ref indexInPath,
908 PathComponentType.Name);
909 }
910 }
911 }
912
913 // If we have a match, return it or, if there's children, recurse into them.
914 if (controlIsMatch)
915 {
916 // If we ended up on a wildcard, we've successfully matched it.
917 if (indexInPath < pathLength && path[indexInPath] == '*')
918 ++indexInPath;
919
920 // If we've reached the end of the path, we have a match.
921 if (indexInPath == pathLength)
922 {
923 // Check type.
924 if (!(control is TControl match))
925 return null;
926
927 if (matchMultiple)
928 matches.Add(match);
929 return match;
930 }
931
932 // If we've reached a separator, dive into our children.
933 if (path[indexInPath] == '/')
934 {
935 ++indexInPath;
936
937 // Silently accept trailing slashes.
938 if (indexInPath == pathLength)
939 {
940 // Check type.
941 if (!(control is TControl match))
942 return null;
943
944 if (matchMultiple)
945 matches.Add(match);
946 return match;
947 }
948
949 // See if we want to match children by usage or by name.
950 TControl lastMatch;
951 if (path[indexInPath] == '{')
952 {
953 // Usages are kind of like entry points that can route to anywhere else
954 // on a device's control hierarchy and then we keep going from that re-routed
955 // point.
956 lastMatch = MatchByUsageAtDeviceRootRecursive(control.device, path, indexInPath, ref matches, matchMultiple);
957 }
958 else
959 {
960 // Go through children and see what we can match.
961 lastMatch = MatchChildrenRecursive(control, path, indexInPath, ref matches, matchMultiple);
962 }
963
964 return lastMatch;
965 }
966 }
967
968 return null;
969 }
970
971 private static TControl MatchByUsageAtDeviceRootRecursive<TControl>(InputDevice device, string path, int indexInPath,
972 ref InputControlList<TControl> matches, bool matchMultiple)
973 where TControl : InputControl
974 {
975 // NOTE: m_UsagesForEachControl includes usages for the device. m_UsageToControl does not.
976
977 var usages = device.m_UsagesForEachControl;
978 if (usages == null)
979 return null;
980
981 var usageCount = device.m_UsageToControl.LengthSafe();
982 var startIndex = indexInPath + 1;
983 var pathCanMatchMultiple = PathComponentCanYieldMultipleMatches(path, indexInPath);
984 var pathLength = path.Length;
985
986 Debug.Assert(path[indexInPath] == '{');
987 ++indexInPath;
988 if (indexInPath == pathLength)
989 throw new ArgumentException($"Invalid path spec '{path}'; trailing '{{'", nameof(path));
990
991 TControl lastMatch = null;
992
993 for (var i = 0; i < usageCount; ++i)
994 {
995 var usage = usages[i];
996 Debug.Assert(!string.IsNullOrEmpty(usage), "Usage entry is empty");
997
998 // Match usage against path.
999 var usageIsMatch = MatchPathComponent(usage, path, ref indexInPath, PathComponentType.Usage);
1000
1001 // If it isn't a match, go to next usage.
1002 if (!usageIsMatch)
1003 {
1004 indexInPath = startIndex;
1005 continue;
1006 }
1007
1008 var controlMatchedByUsage = device.m_UsageToControl[i];
1009
1010 // If there's more to go in the path, dive into the children of the control.
1011 if (indexInPath < pathLength && path[indexInPath] == '/')
1012 {
1013 lastMatch = MatchChildrenRecursive(controlMatchedByUsage, path, indexInPath + 1,
1014 ref matches, matchMultiple);
1015
1016 // We can stop going through usages if we matched something and the
1017 // path component covering usage does not contain wildcards.
1018 if (lastMatch != null && !pathCanMatchMultiple)
1019 break;
1020
1021 // We can stop going through usages if we have a match and are only
1022 // looking for a single one.
1023 if (lastMatch != null && !matchMultiple)
1024 break;
1025 }
1026 else
1027 {
1028 lastMatch = controlMatchedByUsage as TControl;
1029 if (lastMatch != null)
1030 {
1031 if (matchMultiple)
1032 matches.Add(lastMatch);
1033 else
1034 {
1035 // Only looking for single match and we have one.
1036 break;
1037 }
1038 }
1039 }
1040 }
1041
1042 return lastMatch;
1043 }
1044
1045 private static TControl MatchChildrenRecursive<TControl>(InputControl control, string path, int indexInPath,
1046 ref InputControlList<TControl> matches, bool matchMultiple)
1047 where TControl : InputControl
1048 {
1049 var children = control.children;
1050 var childCount = children.Count;
1051 TControl lastMatch = null;
1052 var pathCanMatchMultiple = PathComponentCanYieldMultipleMatches(path, indexInPath);
1053
1054 for (var i = 0; i < childCount; ++i)
1055 {
1056 var child = children[i];
1057 var childMatch = MatchControlsRecursive(child, path, indexInPath, ref matches, matchMultiple);
1058
1059 if (childMatch == null)
1060 continue;
1061
1062 // If the child matched something an there's no wildcards in the child
1063 // portion of the path, we can stop searching.
1064 if (!pathCanMatchMultiple)
1065 return childMatch;
1066
1067 // If we are only looking for the first match and a child matched,
1068 // we can also stop.
1069 if (!matchMultiple)
1070 return childMatch;
1071
1072 // Otherwise we have to go hunting through the hierarchy in case there are
1073 // more matches.
1074 lastMatch = childMatch;
1075 }
1076
1077 return lastMatch;
1078 }
1079
1080 private enum PathComponentType
1081 {
1082 Name,
1083 DisplayName,
1084 Usage,
1085 Layout
1086 }
1087
1088 private static bool MatchPathComponent(string component, string path, ref int indexInPath, PathComponentType componentType, int startIndexInComponent = 0)
1089 {
1090 Debug.Assert(component != null, "Component string is null");
1091 Debug.Assert(path != null, "Path is null");
1092
1093 var componentLength = component.Length;
1094 var pathLength = path.Length;
1095 var startIndex = indexInPath;
1096
1097 // Try to walk the name as far as we can.
1098 var indexInComponent = startIndexInComponent;
1099 while (indexInPath < pathLength)
1100 {
1101 // Check if we've reached a terminator in the path.
1102 var nextCharInPath = path[indexInPath];
1103 if (nextCharInPath == '\\' && indexInPath + 1 < pathLength)
1104 {
1105 // Escaped character. Bypass treatment of special characters below.
1106 ++indexInPath;
1107 nextCharInPath = path[indexInPath];
1108 }
1109 else
1110 {
1111 if (nextCharInPath == '/' && componentType == PathComponentType.Name)
1112 break;
1113 if ((nextCharInPath == '>' && componentType == PathComponentType.Layout)
1114 || (nextCharInPath == '}' && componentType == PathComponentType.Usage)
1115 || (nextCharInPath == ')' && componentType == PathComponentType.DisplayName))
1116 {
1117 ++indexInPath;
1118 break;
1119 }
1120
1121 ////TODO: allow only single '*' and recognize '**'
1122 // If we've reached a '*' in the path, skip character in name.
1123 if (nextCharInPath == '*')
1124 {
1125 // But first let's see if we have something after the wildcard that matches the rest of the component.
1126 // This could be when, for example, we hit "T" on matching "leftTrigger" against "*Trigger". We have to stop
1127 // gobbling up characters for the wildcard when reaching "Trigger" in the component name.
1128 //
1129 // NOTE: Just looking at the very next character only is *NOT* enough. We need to match the entire rest of
1130 // the path. Otherwise, in the example above, we would stop on seeing the lowercase 't' and then be left
1131 // trying to match "tTrigger" against "Trigger".
1132 var indexAfterWildcard = indexInPath + 1;
1133 if (indexInPath < (pathLength - 1) &&
1134 indexInComponent < componentLength &&
1135 MatchPathComponent(component, path, ref indexAfterWildcard, componentType, indexInComponent))
1136 {
1137 indexInPath = indexAfterWildcard;
1138 return true;
1139 }
1140
1141 if (indexInComponent < componentLength)
1142 ++indexInComponent;
1143 else
1144 return true;
1145
1146 continue;
1147 }
1148 }
1149
1150 // If we've reached the end of the component name, we did so before
1151 // we've reached a terminator
1152 if (indexInComponent == componentLength)
1153 {
1154 indexInPath = startIndex;
1155 return false;
1156 }
1157
1158 var charInComponent = component[indexInComponent];
1159 if (charInComponent == nextCharInPath || char.ToLower(charInComponent, CultureInfo.InvariantCulture) == char.ToLower(nextCharInPath, CultureInfo.InvariantCulture))
1160 {
1161 ++indexInComponent;
1162 ++indexInPath;
1163 }
1164 else
1165 {
1166 // Name isn't a match.
1167 indexInPath = startIndex;
1168 return false;
1169 }
1170 }
1171
1172 if (indexInComponent == componentLength)
1173 return true;
1174
1175 indexInPath = startIndex;
1176 return false;
1177 }
1178
1179 private static bool PathComponentCanYieldMultipleMatches(string path, int indexInPath)
1180 {
1181 var indexOfNextSlash = path.IndexOf('/', indexInPath);
1182 if (indexOfNextSlash == -1)
1183 return path.IndexOf('*', indexInPath) != -1 || path.IndexOf('<', indexInPath) != -1;
1184
1185 var length = indexOfNextSlash - indexInPath;
1186 return path.IndexOf('*', indexInPath, length) != -1 || path.IndexOf('<', indexInPath, length) != -1;
1187 }
1188
1189 /// <summary>
1190 /// A single component of a parsed control path as returned by <see cref="Parse"/>. For example, in the
1191 /// control path <c>"<Gamepad>/buttonSouth"</c>, there are two parts: <c>"<Gamepad>"</c>
1192 /// and <c>"buttonSouth"</c>.
1193 /// </summary>
1194 /// <seealso cref="Parse"/>
1195 public struct ParsedPathComponent
1196 {
1197 // Accessing these means no allocations (except when there are multiple usages).
1198 internal Substring m_Layout;
1199 internal InlinedArray<Substring> m_Usages;
1200 internal Substring m_Name;
1201 internal Substring m_DisplayName;
1202
1203 /// <summary>
1204 /// Name of the layout (the part between '<' and '>') referenced in the component or <c>null</c> if no layout
1205 /// is specified. In <c>"<Gamepad>/buttonSouth"</c> the first component will return
1206 /// <c>"Gamepad"</c> from this property and the second component will return <c>null</c>.
1207 /// </summary>
1208 /// <seealso cref="InputControlLayout"/>
1209 /// <seealso cref="InputSystem.LoadLayout"/>
1210 /// <seealso cref="InputControl.layout"/>
1211 public string layout => m_Layout.ToString();
1212
1213 /// <summary>
1214 /// List of device or control usages (the part between '{' and '}') referenced in the component or an empty
1215 /// enumeration. In <c>"<XRController>{RightHand}/trigger"</c>, for example, the
1216 /// first component will have a single element <c>"RightHand"</c> in the enumeration
1217 /// and the second component will have an empty enumeration.
1218 /// </summary>
1219 /// <seealso cref="InputControl.usages"/>
1220 /// <seealso cref="InputSystem.AddDeviceUsage(InputDevice,string)"/>
1221 public IEnumerable<string> usages => m_Usages.Select(x => x.ToString());
1222
1223 /// <summary>
1224 /// Name of the device or control referenced in the component or <c>null</c> In
1225 /// <c>"<Gamepad>/buttonSouth"</c>, for example, the first component will
1226 /// have a <c>null</c> name and the second component will have <c>"buttonSouth"</c>
1227 /// in the name.
1228 /// </summary>
1229 /// <seealso cref="InputControl.name"/>
1230 public string name => m_Name.ToString();
1231
1232 /// <summary>
1233 /// Display name of the device or control (the part inside of '#(...)') referenced in the component
1234 /// or <c>null</c>. In <c>"<Keyboard>/#(*)"</c>, for example, the first component will
1235 /// have a null displayName and the second component will have a displayName of <c>"*"</c>.
1236 /// </summary>
1237 /// <seealso cref="InputControl.displayName"/>
1238 public string displayName => m_DisplayName.ToString();
1239
1240 ////REVIEW: This one isn't well-designed enough yet to be exposed. And double-wildcards are not yet supported.
1241 internal bool isWildcard => m_Name == Wildcard;
1242 internal bool isDoubleWildcard => m_Name == DoubleWildcard;
1243
1244 internal string ToHumanReadableString(string parentLayoutName, string parentControlPath, out string referencedLayoutName,
1245 out string controlPath, HumanReadableStringOptions options)
1246 {
1247 referencedLayoutName = null;
1248 controlPath = null;
1249
1250 var result = string.Empty;
1251 if (isWildcard)
1252 result += "Any";
1253
1254 if (m_Usages.length > 0)
1255 {
1256 var combinedUsages = string.Empty;
1257
1258 for (var i = 0; i < m_Usages.length; ++i)
1259 {
1260 if (m_Usages[i].isEmpty)
1261 continue;
1262
1263 if (combinedUsages != string.Empty)
1264 combinedUsages += " & " + ToHumanReadableString(m_Usages[i]);
1265 else
1266 combinedUsages = ToHumanReadableString(m_Usages[i]);
1267 }
1268 if (combinedUsages != string.Empty)
1269 {
1270 if (result != string.Empty)
1271 result += ' ' + combinedUsages;
1272 else
1273 result += combinedUsages;
1274 }
1275 }
1276
1277 if (!m_Layout.isEmpty)
1278 {
1279 referencedLayoutName = m_Layout.ToString();
1280
1281 // Where possible, use the displayName of the given layout rather than
1282 // just the internal layout name.
1283 string layoutString;
1284 var referencedLayout = InputControlLayout.cache.FindOrLoadLayout(referencedLayoutName, throwIfNotFound: false);
1285 if (referencedLayout != null && !string.IsNullOrEmpty(referencedLayout.m_DisplayName))
1286 layoutString = referencedLayout.m_DisplayName;
1287 else
1288 layoutString = ToHumanReadableString(m_Layout);
1289
1290 if (!string.IsNullOrEmpty(result))
1291 result += ' ' + layoutString;
1292 else
1293 result += layoutString;
1294 }
1295
1296 if (!m_Name.isEmpty && !isWildcard)
1297 {
1298 // If we have a layout from a preceding path component, try to find
1299 // the control by name on the layout. If we find it, use its display
1300 // name rather than the name referenced in the binding.
1301 string nameString = null;
1302 if (!string.IsNullOrEmpty(parentLayoutName))
1303 {
1304 // NOTE: This produces a fully merged layout. We should thus pick up display names
1305 // from base layouts automatically wherever applicable.
1306 var parentLayout =
1307 InputControlLayout.cache.FindOrLoadLayout(new InternedString(parentLayoutName), throwIfNotFound: false);
1308 if (parentLayout != null)
1309 {
1310 var controlName = new InternedString(m_Name.ToString());
1311 var control = parentLayout.FindControlIncludingArrayElements(controlName, out var arrayIndex);
1312 if (control != null)
1313 {
1314 // Synthesize path of control.
1315 if (string.IsNullOrEmpty(parentControlPath))
1316 {
1317 if (arrayIndex != -1)
1318 controlPath = $"{control.Value.name}{arrayIndex}";
1319 else
1320 controlPath = control.Value.name;
1321 }
1322 else
1323 {
1324 if (arrayIndex != -1)
1325 controlPath = $"{parentControlPath}/{control.Value.name}{arrayIndex}";
1326 else
1327 controlPath = $"{parentControlPath}/{control.Value.name}";
1328 }
1329
1330 var shortDisplayName = (options & HumanReadableStringOptions.UseShortNames) != 0
1331 ? control.Value.shortDisplayName
1332 : null;
1333
1334 var displayName = !string.IsNullOrEmpty(shortDisplayName)
1335 ? shortDisplayName
1336 : control.Value.displayName;
1337
1338 if (!string.IsNullOrEmpty(displayName))
1339 {
1340 if (arrayIndex != -1)
1341 nameString = $"{displayName} #{arrayIndex}";
1342 else
1343 nameString = displayName;
1344 }
1345
1346 // If we don't have an explicit <layout> part in the component,
1347 // remember the name of the layout referenced by the control name so
1348 // that path components further down the line can keep looking up their
1349 // display names.
1350 if (string.IsNullOrEmpty(referencedLayoutName))
1351 referencedLayoutName = control.Value.layout;
1352 }
1353 }
1354 }
1355
1356 if (nameString == null)
1357 nameString = ToHumanReadableString(m_Name);
1358
1359 if (!string.IsNullOrEmpty(result))
1360 result += ' ' + nameString;
1361 else
1362 result += nameString;
1363 }
1364
1365 if (!m_DisplayName.isEmpty)
1366 {
1367 var str = $"\"{ToHumanReadableString(m_DisplayName)}\"";
1368 if (!string.IsNullOrEmpty(result))
1369 result += ' ' + str;
1370 else
1371 result += str;
1372 }
1373
1374 return result;
1375 }
1376
1377 private static string ToHumanReadableString(Substring substring)
1378 {
1379 return substring.ToString().Unescape("/*{<", "/*{<");
1380 }
1381
1382 /// <summary>
1383 /// Whether the given control matches the constraints of this path component.
1384 /// </summary>
1385 /// <param name="control">Control to match against the path spec.</param>
1386 /// <returns>True if <paramref name="control"/> matches the constraints.</returns>
1387 public bool Matches(InputControl control)
1388 {
1389 // Match layout.
1390 if (!m_Layout.isEmpty)
1391 {
1392 // Check for direct match.
1393 var layoutMatches = ComparePathElementToString(m_Layout, control.layout);
1394 if (!layoutMatches)
1395 {
1396 // No direct match but base layout may match.
1397 var baseLayout = control.m_Layout;
1398 while (InputControlLayout.s_Layouts.baseLayoutTable.TryGetValue(baseLayout, out baseLayout) && !layoutMatches)
1399 layoutMatches = ComparePathElementToString(m_Layout, baseLayout.ToString());
1400 }
1401
1402 if (!layoutMatches)
1403 return false;
1404 }
1405
1406 // Match usage.
1407 if (m_Usages.length > 0)
1408 {
1409 for (var i = 0; i < m_Usages.length; ++i)
1410 {
1411 if (!m_Usages[i].isEmpty)
1412 {
1413 var controlUsages = control.usages;
1414 var haveUsageMatch = false;
1415 for (var ci = 0; ci < controlUsages.Count; ++ci)
1416 if (ComparePathElementToString(m_Usages[i], controlUsages[ci]))
1417 {
1418 haveUsageMatch = true;
1419 break;
1420 }
1421
1422 if (!haveUsageMatch)
1423 return false;
1424 }
1425 }
1426 }
1427
1428 // Match name.
1429 if (!m_Name.isEmpty && !isWildcard)
1430 {
1431 ////FIXME: unlike the matching path we have in MatchControlsRecursive, this does not take aliases into account
1432 if (!ComparePathElementToString(m_Name, control.name))
1433 return false;
1434 }
1435
1436 // Match display name.
1437 if (!m_DisplayName.isEmpty)
1438 {
1439 if (!ComparePathElementToString(m_DisplayName, control.displayName))
1440 return false;
1441 }
1442
1443 return true;
1444 }
1445
1446 // In a path, characters may be escaped so in those cases, we can't just compare
1447 // character-by-character.
1448 private static bool ComparePathElementToString(Substring pathElement, string element)
1449 {
1450 var pathElementLength = pathElement.length;
1451 var elementLength = element.Length;
1452
1453 for (int i = 0, j = 0;; i++, j++)
1454 {
1455 var pathElementDone = i == pathElementLength;
1456 var elementDone = j == elementLength;
1457
1458 if (pathElementDone || elementDone)
1459 return pathElementDone == elementDone;
1460
1461 var ch = pathElement[i];
1462 if (ch == '\\' && i + 1 < pathElementLength)
1463 ch = pathElement[++i];
1464
1465 if (char.ToLowerInvariant(ch) != char.ToLowerInvariant(element[j]))
1466 return false;
1467 }
1468 }
1469 }
1470
1471 /// <summary>
1472 /// Splits a control path into its separate components.
1473 /// </summary>
1474 /// <param name="path">A control path such as <c>"<Gamepad>/buttonSouth"</c>.</param>
1475 /// <returns>An enumeration of the parsed components. The enumeration is empty if the given
1476 /// <paramref name="path"/> is empty.</returns>
1477 /// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c> or empty.</exception>
1478 /// <remarks>
1479 /// You can use this method, for example, to separate out the components in a binding's <see cref="InputBinding.path"/>.
1480 ///
1481 /// <example>
1482 /// <code>
1483 /// var parsed = InputControlPath.Parse("<XRController>{LeftHand}/trigger").ToArray();
1484 ///
1485 /// Debug.Log(parsed.Length); // Prints 2.
1486 /// Debug.Log(parsed[0].layout); // Prints "XRController".
1487 /// Debug.Log(parsed[0].name); // Prints an empty string.
1488 /// Debug.Log(parsed[0].usages.First()); // Prints "LeftHand".
1489 /// Debug.Log(parsed[1].layout); // Prints null.
1490 /// Debug.Log(parsed[1].name); // Prints "trigger".
1491 ///
1492 /// // Find out if the given device layout is based on "TrackedDevice".
1493 /// Debug.Log(InputSystem.IsFirstLayoutBasedOnSecond(parsed[0].layout, "TrackedDevice")); // Prints true.
1494 ///
1495 /// // Load the device layout referenced by the path.
1496 /// var layout = InputSystem.LoadLayout(parsed[0].layout);
1497 /// Debug.Log(layout.baseLayouts.First()); // Prints "TrackedDevice".
1498 /// </code>
1499 /// </example>
1500 /// </remarks>
1501 /// <seealso cref="InputBinding.path"/>
1502 /// <seealso cref="InputSystem.FindControl"/>
1503 public static IEnumerable<ParsedPathComponent> Parse(string path)
1504 {
1505 if (string.IsNullOrEmpty(path))
1506 throw new ArgumentNullException(nameof(path));
1507
1508 var parser = new PathParser(path);
1509 while (parser.MoveToNextComponent())
1510 yield return parser.current;
1511 }
1512
1513 // NOTE: Must not allocate!
1514 private struct PathParser
1515 {
1516 private string path;
1517 private int length;
1518 private int leftIndexInPath;
1519 private int rightIndexInPath; // Points either to a '/' character or one past the end of the path string.
1520
1521 public ParsedPathComponent current;
1522
1523 public bool isAtEnd => rightIndexInPath == length;
1524
1525 public PathParser(string path)
1526 {
1527 Debug.Assert(path != null);
1528
1529 this.path = path;
1530 length = path.Length;
1531 leftIndexInPath = 0;
1532 rightIndexInPath = 0;
1533 current = new ParsedPathComponent();
1534 }
1535
1536 // Update parsing state and 'current' to next component in path.
1537 // Returns true if the was another component or false if the end of the path was reached.
1538 public bool MoveToNextComponent()
1539 {
1540 // See if we've the end of the path string.
1541 if (rightIndexInPath == length)
1542 return false;
1543
1544 // Make our current right index our new left index and find
1545 // a new right index from there.
1546 leftIndexInPath = rightIndexInPath;
1547 if (path[leftIndexInPath] == '/')
1548 {
1549 ++leftIndexInPath;
1550 rightIndexInPath = leftIndexInPath;
1551 if (leftIndexInPath == length)
1552 return false;
1553 }
1554
1555 // Parse <...> layout part, if present.
1556 var layout = new Substring();
1557 if (rightIndexInPath < length && path[rightIndexInPath] == '<')
1558 layout = ParseComponentPart('>');
1559
1560 ////FIXME: with multiple usages, this will allocate
1561 ////FIXME: Why the heck is this allocating? Should not allocate here! Worse yet, we do ToArray() down there.
1562 // Parse {...} usage part, if present.
1563 var usages = new InlinedArray<Substring>();
1564 while (rightIndexInPath < length && path[rightIndexInPath] == '{')
1565 usages.AppendWithCapacity(ParseComponentPart('}'));
1566
1567 // Parse display name part, if present.
1568 var displayName = new Substring();
1569 if (rightIndexInPath < length - 1 && path[rightIndexInPath] == '#' && path[rightIndexInPath + 1] == '(')
1570 {
1571 ++rightIndexInPath;
1572 displayName = ParseComponentPart(')');
1573 }
1574
1575 // Parse name part, if present.
1576 var name = new Substring();
1577 if (rightIndexInPath < length && path[rightIndexInPath] != '/')
1578 name = ParseComponentPart('/');
1579
1580 current = new ParsedPathComponent
1581 {
1582 m_Layout = layout,
1583 m_Usages = usages,
1584 m_Name = name,
1585 m_DisplayName = displayName
1586 };
1587
1588 return leftIndexInPath != rightIndexInPath;
1589 }
1590
1591 private Substring ParseComponentPart(char terminator)
1592 {
1593 if (terminator != '/') // Name has no corresponding left side terminator.
1594 ++rightIndexInPath;
1595
1596 var partStartIndex = rightIndexInPath;
1597 while (rightIndexInPath < length && path[rightIndexInPath] != terminator)
1598 {
1599 if (path[rightIndexInPath] == '\\' && rightIndexInPath + 1 < length)
1600 ++rightIndexInPath;
1601 ++rightIndexInPath;
1602 }
1603
1604 var partLength = rightIndexInPath - partStartIndex;
1605 if (rightIndexInPath < length && terminator != '/')
1606 ++rightIndexInPath; // Skip past terminator.
1607
1608 return new Substring(path, partStartIndex, partLength);
1609 }
1610 }
1611 }
1612}