A game about forced loneliness, made by TACStudios
1#if UNITY_EDITOR
2using System;
3using System.Collections.Generic;
4using System.Linq;
5using UnityEngine.InputSystem.Layouts;
6using UnityEngine.InputSystem.DualShock;
7using UnityEngine.InputSystem.Utilities;
8
9namespace UnityEngine.InputSystem.Editor
10{
11 /// <summary>
12 /// Caches <see cref="InputControlLayout"/> instances.
13 /// </summary>
14 /// <remarks>
15 /// In the editor we need access to the <see cref="InputControlLayout">InputControlLayouts</see>
16 /// registered with the system in order to facilitate various UI features. Instead of
17 /// constructing layout instances over and over, we keep them around in here.
18 ///
19 /// This class is only available in the editor (when <c>UNITY_EDITOR</c> is true).
20 /// </remarks>
21 internal static class EditorInputControlLayoutCache
22 {
23 /// <summary>
24 /// Iterate over all control layouts in the system.
25 /// </summary>
26 public static IEnumerable<InputControlLayout> allLayouts
27 {
28 get
29 {
30 Refresh();
31 return InputControlLayout.cache.table.Values;
32 }
33 }
34
35 /// <summary>
36 /// Iterate over all unique usages and their respective lists of layouts that use them.
37 /// </summary>
38 public static IEnumerable<Tuple<string, IEnumerable<string>>> allUsages
39 {
40 get
41 {
42 Refresh();
43 return s_Usages.Select(pair => new Tuple<string, IEnumerable<string>>(pair.Key, pair.Value.Select(x => x.ToString())));
44 }
45 }
46
47 public static IEnumerable<InputControlLayout> allControlLayouts
48 {
49 get
50 {
51 Refresh();
52 foreach (var name in s_ControlLayouts)
53 yield return InputControlLayout.cache.FindOrLoadLayout(name.ToString());
54 }
55 }
56
57 public static IEnumerable<InputControlLayout> allDeviceLayouts
58 {
59 get
60 {
61 Refresh();
62 foreach (var name in s_DeviceLayouts)
63 yield return InputControlLayout.cache.FindOrLoadLayout(name.ToString());
64 }
65 }
66
67 public static IEnumerable<InputControlLayout> allProductLayouts
68 {
69 get
70 {
71 Refresh();
72 foreach (var name in s_ProductLayouts)
73 yield return InputControlLayout.cache.FindOrLoadLayout(name.ToString());
74 }
75 }
76
77 public static bool HasChildLayouts(string layoutName)
78 {
79 if (string.IsNullOrEmpty(layoutName))
80 throw new ArgumentException("Layout name cannot be null or empty", nameof(layoutName));
81
82 Refresh();
83
84 var internedLayout = new InternedString(layoutName);
85 // return nothing is the layout does not have any derivations
86 return s_DeviceChildLayouts.TryGetValue(internedLayout, out var derivations) && derivations.Count > 0;
87 }
88
89 public static IEnumerable<InputControlLayout> TryGetChildLayouts(string layoutName)
90 {
91 if (string.IsNullOrEmpty(layoutName))
92 throw new ArgumentException("Layout name cannot be null or empty", nameof(layoutName));
93
94 Refresh();
95
96 var internedLayout = new InternedString(layoutName);
97 // return nothing is the layout does not have any derivations
98 if (!s_DeviceChildLayouts.TryGetValue(internedLayout, out var derivations))
99 yield break;
100 else
101 {
102 foreach (var name in derivations)
103 yield return InputControlLayout.cache.FindOrLoadLayout(name.ToString());
104 }
105 }
106
107 public static InputControlLayout TryGetLayout(string layoutName)
108 {
109 if (string.IsNullOrEmpty(layoutName))
110 throw new ArgumentException("Layout name cannot be null or empty", nameof(layoutName));
111
112 Refresh();
113 return InputControlLayout.cache.FindOrLoadLayout(layoutName, throwIfNotFound: false);
114 }
115
116 public static Type GetValueType(string layoutName)
117 {
118 if (string.IsNullOrEmpty(layoutName))
119 throw new ArgumentException("Layout name cannot be null or empty", nameof(layoutName));
120
121 // Load layout.
122 var layout = TryGetLayout(layoutName);
123 if (layout == null)
124 return null;
125
126 // Grab type.
127 var type = layout.type;
128 Debug.Assert(type != null, "Layout should have associated type");
129 Debug.Assert(typeof(InputControl).IsAssignableFrom(type),
130 "Layout's associated type should be derived from InputControl");
131
132 return layout.GetValueType();
133 }
134
135 public static IEnumerable<InputDeviceMatcher> GetDeviceMatchers(string layoutName)
136 {
137 if (string.IsNullOrEmpty(layoutName))
138 throw new ArgumentException("Layout name cannot be null or empty", nameof(layoutName));
139
140 Refresh();
141 s_DeviceMatchers.TryGetValue(new InternedString(layoutName), out var matchers);
142 return matchers;
143 }
144
145 public static string GetDisplayName(string layoutName)
146 {
147 if (string.IsNullOrEmpty(layoutName))
148 throw new ArgumentException("Layout name cannot be null or empty", nameof(layoutName));
149
150 var layout = TryGetLayout(layoutName);
151 if (layout == null)
152 return layoutName;
153
154 if (!string.IsNullOrEmpty(layout.displayName))
155 return layout.displayName;
156 return layout.name;
157 }
158
159 /// <summary>
160 /// List the controls that may be present on controls or devices of the given layout by virtue
161 /// of being defined in other layouts based on it.
162 /// </summary>
163 /// <param name="layoutName"></param>
164 /// <returns></returns>
165 public static IEnumerable<OptionalControl> GetOptionalControlsForLayout(string layoutName)
166 {
167 if (string.IsNullOrEmpty(layoutName))
168 throw new ArgumentException("Layout name cannot be null or empty", nameof(layoutName));
169
170 Refresh();
171
172 if (!s_OptionalControls.TryGetValue(new InternedString(layoutName), out var list))
173 return Enumerable.Empty<OptionalControl>();
174
175 return list;
176 }
177
178 public static Texture2D GetIconForLayout(string layoutName)
179 {
180 if (string.IsNullOrEmpty(layoutName))
181 throw new ArgumentNullException(nameof(layoutName));
182
183 Refresh();
184
185 // See if we already have it in the cache.
186 var internedName = new InternedString(layoutName);
187 if (s_Icons.TryGetValue(internedName, out var icon))
188 return icon;
189
190 // No, so see if we have an icon on disk for exactly the layout
191 // we're looking at (i.e. with the same name).
192 icon = GUIHelpers.LoadIcon(layoutName);
193 if (icon != null)
194 {
195 s_Icons.Add(internedName, icon);
196 return icon;
197 }
198
199 // No, not that either so start walking up the inheritance chain
200 // until we either bump against the ceiling or find an icon.
201 var layout = TryGetLayout(layoutName);
202 if (layout != null)
203 {
204 foreach (var baseLayoutName in layout.baseLayouts)
205 {
206 icon = GetIconForLayout(baseLayoutName);
207 if (icon != null)
208 return icon;
209 }
210
211 // If it's a control and there's no specific icon, return a generic one.
212 if (layout.isControlLayout)
213 {
214 var genericIcon = GUIHelpers.LoadIcon("InputControl");
215 if (genericIcon != null)
216 {
217 s_Icons.Add(internedName, genericIcon);
218 return genericIcon;
219 }
220 }
221 }
222
223 // No icon for anything in this layout's chain.
224 return null;
225 }
226
227 public struct ControlSearchResult
228 {
229 public string controlPath;
230 public InputControlLayout layout;
231 public InputControlLayout.ControlItem item;
232 }
233
234 internal static void Clear()
235 {
236 s_LayoutRegistrationVersion = 0;
237 s_LayoutCacheRef.Dispose();
238 s_Usages.Clear();
239 s_ControlLayouts.Clear();
240 s_DeviceLayouts.Clear();
241 s_ProductLayouts.Clear();
242 s_DeviceMatchers.Clear();
243 s_Icons.Clear();
244 }
245
246 // If our layout data is outdated, rescan all the layouts in the system.
247 private static void Refresh()
248 {
249 var manager = InputSystem.s_Manager;
250 if (manager.m_LayoutRegistrationVersion == s_LayoutRegistrationVersion)
251 return;
252
253 Clear();
254
255 if (!s_LayoutCacheRef.valid)
256 {
257 // In the editor, we keep a permanent reference on the global layout
258 // cache. Means that in the editor, we always have all layouts loaded in full
259 // at all times whereas in the player, we load layouts only while we need
260 // them and then release them again.
261 s_LayoutCacheRef = InputControlLayout.CacheRef();
262 }
263
264 var layoutNames = manager.ListControlLayouts().ToArray();
265
266 // Remember which layout maps to which device matchers.
267 var layoutMatchers = InputControlLayout.s_Layouts.layoutMatchers;
268 foreach (var entry in layoutMatchers)
269 {
270 s_DeviceMatchers.TryGetValue(entry.layoutName, out var matchers);
271
272 matchers.Append(entry.deviceMatcher);
273 s_DeviceMatchers[entry.layoutName] = matchers;
274 }
275
276 // Load and store all layouts.
277 foreach (var layoutName in layoutNames)
278 {
279 ////FIXME: does not protect against exceptions
280 var layout = InputControlLayout.cache.FindOrLoadLayout(layoutName, throwIfNotFound: false);
281 if (layout == null)
282 continue;
283
284 ScanLayout(layout);
285
286 if (layout.isOverride)
287 continue;
288
289 if (layout.isControlLayout)
290 s_ControlLayouts.Add(layout.name);
291 else if (s_DeviceMatchers.ContainsKey(layout.name))
292 s_ProductLayouts.Add(layout.name);
293 else
294 s_DeviceLayouts.Add(layout.name);
295 }
296
297 // Move all device layouts without a device description but derived from
298 // a layout that has one over to the product list.
299 foreach (var name in s_DeviceLayouts)
300 {
301 var layout = InputControlLayout.cache.FindOrLoadLayout(name);
302
303 if (layout.m_BaseLayouts.length > 1)
304 throw new NotImplementedException();
305
306 for (var baseLayoutName = layout.baseLayouts.FirstOrDefault(); !baseLayoutName.IsEmpty();)
307 {
308 if (s_ProductLayouts.Contains(baseLayoutName))
309 {
310 // Defer removing from s_DeviceLayouts to keep iteration stable.
311 s_ProductLayouts.Add(name);
312 break;
313 }
314
315 var baseLayout = InputControlLayout.cache.FindOrLoadLayout(baseLayoutName, throwIfNotFound: false);
316 if (baseLayout == null)
317 continue;
318 if (baseLayout.m_BaseLayouts.length > 1)
319 throw new NotImplementedException();
320 baseLayoutName = baseLayout.baseLayouts.FirstOrDefault();
321 }
322 }
323
324 // Remove every product device layout now.
325 s_DeviceLayouts.ExceptWith(s_ProductLayouts);
326
327 s_LayoutRegistrationVersion = manager.m_LayoutRegistrationVersion;
328 }
329
330 private static int s_LayoutRegistrationVersion;
331 private static InputControlLayout.CacheRefInstance s_LayoutCacheRef;
332
333 private static readonly HashSet<InternedString> s_ControlLayouts = new HashSet<InternedString>();
334 private static readonly HashSet<InternedString> s_DeviceLayouts = new HashSet<InternedString>();
335 private static readonly HashSet<InternedString> s_ProductLayouts = new HashSet<InternedString>();
336 private static readonly Dictionary<InternedString, List<OptionalControl>> s_OptionalControls =
337 new Dictionary<InternedString, List<OptionalControl>>();
338 private static readonly Dictionary<InternedString, InlinedArray<InputDeviceMatcher>> s_DeviceMatchers =
339 new Dictionary<InternedString, InlinedArray<InputDeviceMatcher>>();
340 private static Dictionary<InternedString, Texture2D> s_Icons =
341 new Dictionary<InternedString, Texture2D>();
342
343 // We keep a map of the devices which a derived from a base device.
344 private static readonly Dictionary<InternedString, HashSet<InternedString>> s_DeviceChildLayouts =
345 new Dictionary<InternedString, HashSet<InternedString>>();
346
347
348 // We keep a map of all unique usages we find in layouts and also
349 // retain a list of the layouts they are used with.
350 private static readonly SortedDictionary<InternedString, HashSet<InternedString>> s_Usages =
351 new SortedDictionary<InternedString, HashSet<InternedString>>();
352
353 private static void ScanLayout(InputControlLayout layout)
354 {
355 var controls = layout.controls;
356 for (var i = 0; i < controls.Count; ++i)
357 {
358 var control = controls[i];
359
360 // If it's not just a control modifying some inner child control, add control to all base
361 // layouts as an optional control.
362 //
363 // NOTE: We're looking at layouts post-merging here. Means we have already picked up all the
364 // controls present on the base.
365 // Only controls which belong to UI-facing layouts are included, as optional controls are used solely by
366 // the InputControlPickerDropdown UI
367 if (control.isFirstDefinedInThisLayout && !control.isModifyingExistingControl && !control.layout.IsEmpty() && !layout.hideInUI)
368 {
369 foreach (var baseLayout in layout.baseLayouts)
370 AddOptionalControlRecursive(baseLayout, ref control);
371 }
372
373 // Collect unique usages and the layouts used with them.
374 foreach (var usage in control.usages)
375 {
376 // Empty usages can occur for controls that want to reset inherited usages.
377 if (string.IsNullOrEmpty(usage))
378 continue;
379
380 var internedUsage = new InternedString(usage);
381 var internedLayout = new InternedString(control.layout);
382
383 if (!s_Usages.TryGetValue(internedUsage, out var layoutSet))
384 {
385 layoutSet = new HashSet<InternedString> { internedLayout };
386 s_Usages[internedUsage] = layoutSet;
387 }
388 else
389 {
390 layoutSet.Add(internedLayout);
391 }
392 }
393
394 // Create a dependency tree matching each concrete device layout exposed in the UI
395 // to all of the layouts that are directly derived from it.
396 if (layout.isDeviceLayout && !layout.hideInUI)
397 {
398 foreach (var baseLayoutName in layout.baseLayouts)
399 {
400 if (!s_DeviceChildLayouts.TryGetValue(baseLayoutName, out var derivedSet))
401 {
402 derivedSet = new HashSet<InternedString> { layout.name };
403 s_DeviceChildLayouts[baseLayoutName] = derivedSet;
404 }
405 else
406 {
407 derivedSet.Add(layout.name);
408 }
409 }
410 }
411 }
412 }
413
414 private static void AddOptionalControlRecursive(InternedString layoutName, ref InputControlLayout.ControlItem controlItem)
415 {
416 Debug.Assert(!controlItem.isModifyingExistingControl);
417 Debug.Assert(!controlItem.layout.IsEmpty());
418
419 // Recurse into base.
420 if (InputControlLayout.s_Layouts.baseLayoutTable.TryGetValue(layoutName, out var baseLayoutName))
421 AddOptionalControlRecursive(baseLayoutName, ref controlItem);
422
423 // See if we already have this optional control.
424 var alreadyPresent = false;
425 if (!s_OptionalControls.TryGetValue(layoutName, out var list))
426 {
427 list = new List<OptionalControl>();
428 s_OptionalControls[layoutName] = list;
429 }
430 else
431 {
432 // See if we already have this control.
433 foreach (var item in list)
434 {
435 if (item.name == controlItem.name && item.layout == controlItem.layout)
436 {
437 alreadyPresent = true;
438 break;
439 }
440 }
441 }
442 if (!alreadyPresent)
443 list.Add(new OptionalControl {name = controlItem.name, layout = controlItem.layout});
444 }
445
446 /// <summary>
447 /// An optional control is a control that is not defined on a layout but which is defined
448 /// on a derived layout.
449 /// </summary>
450 /// <remarks>
451 /// An example is the "acceleration" control defined by some layouts based on <see cref="Gamepad"/> (e.g.
452 /// <see cref="DualShockGamepad.acceleration"/>. This means gamepads
453 /// MAY have a gyro and thus MAY have an "acceleration" control.
454 ///
455 /// In bindings (<see cref="InputBinding"/>), it is perfectly valid to deal with this opportunistically
456 /// and create a binding to <c>"<Gamepad>/acceleration"</c> which will bind correctly IF the gamepad has
457 /// an acceleration control but will do nothing if it doesn't.
458 ///
459 /// The concept of optional controls permits setting up such bindings in the UI by making controls that
460 /// are present on more specific layouts than the one currently looked at available directly on the
461 /// base layout.
462 /// </remarks>
463 public struct OptionalControl
464 {
465 public InternedString name;
466 public InternedString layout;
467 ////REVIEW: do we want to have the list of layouts that define the control?
468 }
469 }
470}
471#endif // UNITY_EDITOR