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 /// &lt;Layout&gt;{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 /// "&lt;Gamepad&gt;" 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 /// "&lt;Keyboard&gt;/#(a)" 65 /// 66 /// // Matches the X axis of the left stick on a gamepad. 67 /// "&lt;Gamepad&gt;/leftStick/x" 68 /// 69 /// // Matches the orientation control of the right-hand XR controller: 70 /// "&lt;XRController&gt;{RightHand}/orientation" 71 /// 72 /// // Matches all buttons on a gamepad. 73 /// "&lt;Gamepad&gt;/&lt;Button&gt;" 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 "&lt;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, "&lt;XInputController&gt;/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("&lt;Gamepad&gt;/buttonSouth"); // -> "Button South [Gamepad]" 187 /// InputControlPath.ToHumanReadableString("&lt;XInputController&gt;/buttonSouth"); // -> "A [Xbox Controller]" 188 /// InputControlPath.ToHumanReadableString("&lt;Gamepad&gt;/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 "&lt;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>"&lt;Gamepad&gt;/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, "&lt;XInputController&gt;/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("&lt;Gamepad&gt;/buttonSouth"); // -> "Button South [Gamepad]" 236 /// InputControlPath.ToHumanReadableString("&lt;XInputController&gt;/buttonSouth"); // -> "A [Xbox Controller]" 237 /// InputControlPath.ToHumanReadableString("&lt;Gamepad&gt;/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 "/&lt;gamepad&gt;/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("/&lt;gamepad&gt;/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("&lt;Keyboard&gt;", Keyboard.current.aKey); 786 /// 787 /// // False as the path matches none of the controls leading to Keyboard.aKey. 788 /// InputControlPath.MatchesPrefix("&lt;Gamepad&gt;", Keyboard.current.aKey); 789 /// 790 /// // True as the path matches Keyboard.aKey itself. 791 /// InputControlPath.MatchesPrefix("&lt;Keyboard&gt;/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>"&lt;Gamepad&gt;/buttonSouth"</c>, there are two parts: <c>"&lt;Gamepad&gt;"</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 '&lt;' and '&gt;') referenced in the component or <c>null</c> if no layout 1205 /// is specified. In <c>"&lt;Gamepad&gt;/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>"&lt;XRController&gt;{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>"&lt;Gamepad&gt;/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>"&lt;Keyboard&gt;/#(*)"</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>"&lt;Gamepad&gt;/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("&lt;XRController&gt;{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}