설명 없음
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

InputControlPath.cs 71KB

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