InputControlPath.cs 62 KB

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