Skip to content

Commit d4dcb3a

Browse files
author
Rene Damm
authored
CHANGE: Make ToHumanReadableString() respect display names (#804).
1 parent 63d1fcd commit d4dcb3a

File tree

9 files changed

+271
-85
lines changed

9 files changed

+271
-85
lines changed

Assets/Tests/InputSystem/CoreTests_Controls.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -996,11 +996,20 @@ public void Controls_DisplayNameForNestedControls_IncludesNameOfParentControl()
996996
public void Controls_CanTurnControlPathIntoHumanReadableText()
997997
{
998998
Assert.That(InputControlPath.ToHumanReadableString("*/{PrimaryAction}"), Is.EqualTo("PrimaryAction [Any]"));
999-
Assert.That(InputControlPath.ToHumanReadableString("<Gamepad>/leftStick"), Is.EqualTo("leftStick [Gamepad]"));
1000-
Assert.That(InputControlPath.ToHumanReadableString("<Gamepad>/leftStick/x"), Is.EqualTo("leftStick/x [Gamepad]"));
999+
Assert.That(InputControlPath.ToHumanReadableString("<Gamepad>/leftStick"), Is.EqualTo("Left Stick [Gamepad]"));
1000+
Assert.That(InputControlPath.ToHumanReadableString("<Gamepad>/leftStick/x"), Is.EqualTo("Left Stick/X [Gamepad]"));
10011001
Assert.That(InputControlPath.ToHumanReadableString("<XRController>{LeftHand}/position"), Is.EqualTo("position [LeftHand XRController]"));
10021002
Assert.That(InputControlPath.ToHumanReadableString("*/leftStick"), Is.EqualTo("leftStick [Any]"));
10031003
Assert.That(InputControlPath.ToHumanReadableString("*/{PrimaryMotion}/x"), Is.EqualTo("PrimaryMotion/x [Any]"));
1004+
Assert.That(InputControlPath.ToHumanReadableString("<Gamepad>/buttonSouth"), Is.EqualTo("Button South [Gamepad]"));
1005+
Assert.That(InputControlPath.ToHumanReadableString("<XInputController>/buttonSouth"), Is.EqualTo("A [Xbox Controller]"));
1006+
1007+
Assert.That(
1008+
InputControlPath.ToHumanReadableString("<Gamepad>/buttonSouth",
1009+
InputControlPath.HumanReadableStringOptions.OmitDevice), Is.EqualTo("Button South"));
1010+
Assert.That(
1011+
InputControlPath.ToHumanReadableString("*/{PrimaryAction}",
1012+
InputControlPath.HumanReadableStringOptions.OmitDevice), Is.EqualTo("PrimaryAction"));
10041013
}
10051014

10061015
[Test]

Packages/com.unity.inputsystem/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ however, it has to be formatted properly to pass verification tests.
1515

1616
#### Actions
1717

18+
- Binding paths now show the same way in the action editor UI as they do in the control picker.
19+
* For example, where before a binding to `<XInputController>/buttonSouth` was shown as `rightShoulder [XInputController]`, the same binding will now show as `A [Xbox Controller]`.
1820
- When deleting a control scheme, bindings are now updated. A dialog is presented that allows choosing between deleting the bindings or just unassigning them from the control scheme.
1921
- When renaming a control scheme, bindings are now updated. Previously the old name was in place on bindings.
2022
- Control scheme names can no longer be set to empty strings.
@@ -26,6 +28,8 @@ however, it has to be formatted properly to pass verification tests.
2628
- `InputUser.onUnpairedDeviceUsed` now receives a 2nd argument which is the event that triggered the callback.
2729
* Also, the callback is now triggered __BEFORE__ the given event is processed rather than after the event has already been written to the device. This allows updating the pairing state of the system before input is processed.
2830
* In practice, this means that, for example, if the user switches from keyboard&mouse to gamepad, the initial input that triggered the switch will get picked up right away.
31+
- `InputControlPath.ToHumanReadableString` now takes display names from registered `InputControlLayout` instances into account.
32+
* This means that the method can now be used to generate strings to display in rebinding UIs.
2933

3034
#### Actions
3135

Packages/com.unity.inputsystem/InputSystem/Controls/InputControlLayout.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@ public ControlItem this[string path]
343343
if (string.IsNullOrEmpty(path))
344344
throw new ArgumentNullException(nameof(path));
345345

346+
// Does not use FindControl so that we don't force-intern the given path string.
346347
if (m_Controls != null)
347348
{
348349
for (var i = 0; i < m_Controls.Length; ++i)
@@ -1825,5 +1826,48 @@ public InputControlLayout FindOrLoadLayout(string name)
18251826
throw new LayoutNotFoundException(name);
18261827
}
18271828
}
1829+
1830+
internal static Cache s_CacheInstance;
1831+
internal static int s_CacheInstanceRef;
1832+
1833+
// Constructing InputControlLayouts is very costly as it tends to involve lots of reflection and
1834+
// piecing data together. Thus, wherever possible, we want to keep layouts around for as long as
1835+
// we need them yet at the same time not keep them needlessly around while we don't.
1836+
//
1837+
// This property makes a cache of layouts available globally yet implements a resource acquisition
1838+
// based pattern to make sure we keep the cache alive only within specific execution scopes.
1839+
internal static ref Cache cache
1840+
{
1841+
get
1842+
{
1843+
Debug.Assert(s_CacheInstanceRef > 0, "Must hold an instance reference");
1844+
return ref s_CacheInstance;
1845+
}
1846+
}
1847+
1848+
internal static CacheRefInstance CacheRef()
1849+
{
1850+
++s_CacheInstanceRef;
1851+
return new CacheRefInstance {valid = true};
1852+
}
1853+
1854+
internal struct CacheRefInstance : IDisposable
1855+
{
1856+
public bool valid; // Make sure we can distinguish default-initialized instances.
1857+
public void Dispose()
1858+
{
1859+
if (!valid)
1860+
return;
1861+
1862+
--s_CacheInstanceRef;
1863+
if (s_CacheInstanceRef <= 0)
1864+
{
1865+
s_CacheInstance = default;
1866+
s_CacheInstanceRef = 0;
1867+
}
1868+
1869+
valid = false;
1870+
}
1871+
}
18281872
}
18291873
}

Packages/com.unity.inputsystem/InputSystem/Controls/InputControlPath.cs

Lines changed: 132 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -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 "&lt;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, "&lt;XInputController&gt;/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("&lt;Gamepad&gt;/buttonSouth"); // -> "Button South [Gamepad]"
162+
/// InputControlPath.ToHumanReadableString("&lt;XInputController&gt;/buttonSouth"); // -> "A [Xbox Controller]"
163+
/// InputControlPath.ToHumanReadableString("&lt;Gamepad&gt;/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)

Packages/com.unity.inputsystem/InputSystem/Devices/InputDeviceBuilder.cs

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,13 @@ namespace UnityEngine.InputSystem.Layouts
4343
/// Existing controls may be reused while at the same time the hierarchy and even the device instance
4444
/// itself may change.
4545
/// </remarks>
46-
internal struct InputDeviceBuilder
46+
internal struct InputDeviceBuilder : IDisposable
4747
{
4848
public void Setup(InternedString layout, InternedString variants,
4949
InputDeviceDescription deviceDescription = default)
5050
{
51+
m_LayoutCacheRef = InputControlLayout.CacheRef();
52+
5153
InstantiateLayout(layout, variants, new InternedString(), null);
5254
FinalizeControlHierarchy();
5355

@@ -67,12 +69,16 @@ public InputDevice Finish()
6769
return device;
6870
}
6971

70-
internal InputDevice m_Device;
72+
public void Dispose()
73+
{
74+
m_LayoutCacheRef.Dispose();
75+
}
76+
77+
private InputDevice m_Device;
7178

72-
// We construct layouts lazily as we go but keep them cached while we
73-
// set up hierarchies so that we don't re-construct the same Button layout
74-
// 256 times for a keyboard.
75-
private InputControlLayout.Cache m_LayoutCache;
79+
// Make sure the global layout cache sticks around for at least as long
80+
// as the device builder so that we don't load layouts over and over.
81+
private InputControlLayout.CacheRefInstance m_LayoutCacheRef;
7682

7783
// Table mapping (lower-cased) control paths to control layouts that contain
7884
// overrides for the control at the given path.
@@ -725,7 +731,8 @@ private static void SetFormat(InputControl control, InputControlLayout.ControlIt
725731

726732
private InputControlLayout FindOrLoadLayout(string name)
727733
{
728-
return m_LayoutCache.FindOrLoadLayout(name);
734+
Debug.Assert(InputControlLayout.s_CacheInstanceRef > 0, "Should have acquired layout cache reference");
735+
return InputControlLayout.cache.FindOrLoadLayout(name);
729736
}
730737

731738
private static void ComputeStateLayout(InputControl control)
@@ -896,7 +903,7 @@ internal static ref InputDeviceBuilder instance
896903
}
897904
}
898905

899-
public static RefInstance Ref()
906+
internal static RefInstance Ref()
900907
{
901908
Debug.Assert(s_Instance.m_Device == null,
902909
"InputDeviceBuilder is already in use! Cannot use the builder recursively");
@@ -911,8 +918,12 @@ internal struct RefInstance : IDisposable
911918
public void Dispose()
912919
{
913920
--s_InstanceRef;
914-
if (s_InstanceRef == 0)
921+
if (s_InstanceRef <= 0)
922+
{
923+
s_Instance.Dispose();
915924
s_Instance = default;
925+
s_InstanceRef = 0;
926+
}
916927
else
917928
// Make sure we reset when there is an exception.
918929
s_Instance.Reset();

0 commit comments

Comments
 (0)