@@ -113,51 +113,106 @@ public static string Combine(InputControl parent, string path)
113113 return $ "{ parent . path } /{ path } ";
114114 }
115115
116+ /// <summary>
117+ /// Options for customizing the behavior of <see cref="ToHumanReadableString"/>.
118+ /// </summary>
119+ [ Flags ]
120+ public enum HumanReadableStringOptions
121+ {
122+ /// <summary>
123+ /// The default behavior.
124+ /// </summary>
125+ None = 0 ,
126+
127+ /// <summary>
128+ /// Do not mention the device of the control. For example, instead of "A [Gamepad]",
129+ /// return just "A".
130+ /// </summary>
131+ OmitDevice = 1 << 1 ,
132+ }
133+
134+ ////TODO: factor out the part that looks up an InputControlLayout.ControlItem from a given path
135+ //// and make that available as a stand-alone API
136+ ////TODO: add option to customize path separation character
116137 /// <summary>
117138 /// Create a human readable string from the given control path.
118139 /// </summary>
119140 /// <param name="path">A control path such as "<XRController>{LeftHand}/position".</param>
120- /// <returns>A string such as "leftStick/x [Gamepad]".</returns>
121- public static string ToHumanReadableString ( string path )
141+ /// <param name="options">Customize the resulting string.</param>
142+ /// <returns>A string such as "Left Stick/X [Gamepad]".</returns>
143+ /// <remarks>
144+ /// This function is most useful for turning binding paths (see <see cref="InputBinding.path"/>)
145+ /// into strings that can be displayed in UIs (such as rebinding screens). It is used by
146+ /// the Unity editor itself to display binding paths in the UI.
147+ ///
148+ /// The method uses display names (see <see cref="InputControlAttribute.displayName"/>,
149+ /// <see cref="InputControlLayoutAttribute.displayName"/>, and <see cref="InputControlLayout.ControlItem.displayName"/>)
150+ /// where possible. For example, "<XInputController>/buttonSouth" will be returned as
151+ /// "A [Xbox Controller]" as the display name of <see cref="XInput.XInputController"/> is "XBox Controller"
152+ /// and the display name of its "buttonSouth" control is "A".
153+ ///
154+ /// Note that these lookups depend on the currently registered control layouts (see <see
155+ /// cref="InputControlLayout"/>) and different strings may thus be returned for the same control
156+ /// path depending on the layouts registered with the system.
157+ ///
158+ /// <example>
159+ /// <code>
160+ /// InputControlPath.ToHumanReadableString("*/{PrimaryAction"); // -> "PrimaryAction [Any]"
161+ /// InputControlPath.ToHumanReadableString("<Gamepad>/buttonSouth"); // -> "Button South [Gamepad]"
162+ /// InputControlPath.ToHumanReadableString("<XInputController>/buttonSouth"); // -> "A [Xbox Controller]"
163+ /// InputControlPath.ToHumanReadableString("<Gamepad>/leftStick/x"); // -> "Left Stick/X [Gamepad]"
164+ /// </code>
165+ /// </example>
166+ /// </remarks>
167+ /// <seealso cref="InputBinding.path"/>
168+ public static string ToHumanReadableString ( string path ,
169+ HumanReadableStringOptions options = HumanReadableStringOptions . None )
122170 {
123171 if ( string . IsNullOrEmpty ( path ) )
124172 return string . Empty ;
125173
126174 var buffer = new StringBuilder ( ) ;
127175 var parser = new PathParser ( path ) ;
128176
129- ////REVIEW: ideally, we'd use display names of controls rather than the control paths directly from the path
130-
131- // First level is taken to be device .
132- if ( parser . MoveToNextComponent ( ) )
177+ // For display names of controls and devices, we need to look at InputControlLayouts.
178+ // If none is in place here, we establish a temporary layout cache while we go through
179+ // the path. If one is in place already, we reuse what's already there .
180+ using ( InputControlLayout . CacheRef ( ) )
133181 {
134- var device = parser . current . ToHumanReadableString ( ) ;
135-
136- // Any additional levels (if present) are taken to form a control path on the device.
137- var isFirstControlLevel = true ;
138- while ( parser . MoveToNextComponent ( ) )
182+ // First level is taken to be device.
183+ if ( parser . MoveToNextComponent ( ) )
139184 {
140- if ( ! isFirstControlLevel )
141- buffer . Append ( '/' ) ;
185+ // Keep track of which control layout we're on (if any) as we're crawling
186+ // down the path.
187+ var device = parser . current . ToHumanReadableString ( null , out var currentLayoutName ) ;
142188
143- buffer . Append ( parser . current . ToHumanReadableString ( ) ) ;
144- isFirstControlLevel = false ;
145- }
189+ // Any additional levels (if present) are taken to form a control path on the device.
190+ var isFirstControlLevel = true ;
191+ while ( parser . MoveToNextComponent ( ) )
192+ {
193+ if ( ! isFirstControlLevel )
194+ buffer . Append ( '/' ) ;
146195
147- if ( ! string . IsNullOrEmpty ( device ) )
148- {
149- buffer . Append ( " [" ) ;
150- buffer . Append ( device ) ;
151- buffer . Append ( ']' ) ;
196+ buffer . Append ( parser . current . ToHumanReadableString (
197+ currentLayoutName , out currentLayoutName ) ) ;
198+ isFirstControlLevel = false ;
199+ }
200+
201+ if ( ( options & HumanReadableStringOptions . OmitDevice ) == 0 && ! string . IsNullOrEmpty ( device ) )
202+ {
203+ buffer . Append ( " [" ) ;
204+ buffer . Append ( device ) ;
205+ buffer . Append ( ']' ) ;
206+ }
152207 }
153- }
154208
155- // If we didn't manage to figure out a display name, default to displaying
156- // the path as is.
157- if ( buffer . Length == 0 )
158- return path ;
209+ // If we didn't manage to figure out a display name, default to displaying
210+ // the path as is.
211+ if ( buffer . Length == 0 )
212+ return path ;
159213
160- return buffer . ToString ( ) ;
214+ return buffer . ToString ( ) ;
215+ }
161216 }
162217
163218 public static string [ ] TryGetDeviceUsages ( string path )
@@ -171,7 +226,7 @@ public static string[] TryGetDeviceUsages(string path)
171226
172227 if ( parser . current . usages != null && parser . current . usages . Length > 0 )
173228 {
174- return Array . ConvertAll < Substring , string > ( parser . current . usages , i => { return i . ToString ( ) ; } ) ;
229+ return Array . ConvertAll ( parser . current . usages , i => { return i . ToString ( ) ; } ) ;
175230 }
176231
177232 return null ;
@@ -219,11 +274,6 @@ public static string TryGetDeviceLayout(string path)
219274 ////TODO: return Substring and use path parser; should get rid of allocations
220275
221276 // From the given control path, try to determine the control layout being used.
222- //
223- // NOTE: This function will only use information available in the path itself or
224- // in layouts referenced by the path. It will not look at actual devices
225- // in the system. This is to make the behavior predictable and not dependent
226- // on whether you currently have the right device connected or not.
227277 // NOTE: Allocates!
228278 public static string TryGetControlLayout ( string path )
229279 {
@@ -956,8 +1006,10 @@ internal struct ParsedPathComponent
9561006 public bool isWildcard => name == Wildcard ;
9571007 public bool isDoubleWildcard => name == DoubleWildcard ;
9581008
959- public string ToHumanReadableString ( )
1009+ public string ToHumanReadableString ( string parentLayoutName , out string referencedLayoutName )
9601010 {
1011+ referencedLayoutName = null ;
1012+
9611013 var result = string . Empty ;
9621014 if ( isWildcard )
9631015 result += "Any" ;
@@ -987,18 +1039,60 @@ public string ToHumanReadableString()
9871039
9881040 if ( ! layout . isEmpty )
9891041 {
1042+ referencedLayoutName = layout . ToString ( ) ;
1043+
1044+ // Where possible, use the displayName of the given layout rather than
1045+ // just the internal layout name.
1046+ string layoutString ;
1047+ var referencedLayout = InputControlLayout . cache . FindOrLoadLayout ( referencedLayoutName ) ;
1048+ if ( referencedLayout != null && ! string . IsNullOrEmpty ( referencedLayout . m_DisplayName ) )
1049+ layoutString = referencedLayout . m_DisplayName ;
1050+ else
1051+ layoutString = ToHumanReadableString ( layout ) ;
1052+
9901053 if ( ! string . IsNullOrEmpty ( result ) )
991- result += ' ' + ToHumanReadableString ( layout ) ;
1054+ result += ' ' + layoutString ;
9921055 else
993- result += ToHumanReadableString ( layout ) ;
1056+ result += layoutString ;
9941057 }
9951058
9961059 if ( ! name . isEmpty && ! isWildcard )
9971060 {
1061+ // If we have a layout from a preceding path component, try to find
1062+ // the control by name on the layout. If we find it, use its display
1063+ // name rather than the name referenced in the binding.
1064+ string nameString = null ;
1065+ if ( ! string . IsNullOrEmpty ( parentLayoutName ) )
1066+ {
1067+ // NOTE: This produces a fully merged layout. We should thus pick up display names
1068+ // from base layouts automatically wherever applicable.
1069+ var parentLayout = InputControlLayout . cache . FindOrLoadLayout ( new InternedString ( parentLayoutName ) ) ;
1070+ if ( parentLayout != null )
1071+ {
1072+ var controlName = new InternedString ( name . ToString ( ) ) ;
1073+ var control = parentLayout . FindControl ( controlName ) ;
1074+ if ( control != null )
1075+ {
1076+ if ( ! string . IsNullOrEmpty ( control . Value . displayName ) )
1077+ nameString = control . Value . displayName ;
1078+
1079+ // If we don't have an explicit <layout> part in the component,
1080+ // remember the name of the layout referenced by the control name so
1081+ // that path components further down the line can keep looking up their
1082+ // display names.
1083+ if ( string . IsNullOrEmpty ( referencedLayoutName ) )
1084+ referencedLayoutName = control . Value . layout ;
1085+ }
1086+ }
1087+ }
1088+
1089+ if ( nameString == null )
1090+ nameString = ToHumanReadableString ( name ) ;
1091+
9981092 if ( ! string . IsNullOrEmpty ( result ) )
999- result += ' ' + ToHumanReadableString ( name ) ;
1093+ result += ' ' + nameString ;
10001094 else
1001- result += ToHumanReadableString ( name ) ;
1095+ result += nameString ;
10021096 }
10031097
10041098 if ( ! displayName . isEmpty )
0 commit comments