#if UNITY_EDITOR using System; using System.Collections.Generic; using System.Linq; using UnityEngine.InputSystem.Layouts; using UnityEngine.InputSystem.DualShock; using UnityEngine.InputSystem.Utilities; namespace UnityEngine.InputSystem.Editor { /// /// Caches instances. /// /// /// In the editor we need access to the InputControlLayouts /// registered with the system in order to facilitate various UI features. Instead of /// constructing layout instances over and over, we keep them around in here. /// /// This class is only available in the editor (when UNITY_EDITOR is true). /// internal static class EditorInputControlLayoutCache { /// /// Iterate over all control layouts in the system. /// public static IEnumerable allLayouts { get { Refresh(); return InputControlLayout.cache.table.Values; } } /// /// Iterate over all unique usages and their respective lists of layouts that use them. /// public static IEnumerable>> allUsages { get { Refresh(); return s_Usages.Select(pair => new Tuple>(pair.Key, pair.Value.Select(x => x.ToString()))); } } public static IEnumerable allControlLayouts { get { Refresh(); foreach (var name in s_ControlLayouts) yield return InputControlLayout.cache.FindOrLoadLayout(name.ToString()); } } public static IEnumerable allDeviceLayouts { get { Refresh(); foreach (var name in s_DeviceLayouts) yield return InputControlLayout.cache.FindOrLoadLayout(name.ToString()); } } public static IEnumerable allProductLayouts { get { Refresh(); foreach (var name in s_ProductLayouts) yield return InputControlLayout.cache.FindOrLoadLayout(name.ToString()); } } public static bool HasChildLayouts(string layoutName) { if (string.IsNullOrEmpty(layoutName)) throw new ArgumentException("Layout name cannot be null or empty", nameof(layoutName)); Refresh(); var internedLayout = new InternedString(layoutName); // return nothing is the layout does not have any derivations return s_DeviceChildLayouts.TryGetValue(internedLayout, out var derivations) && derivations.Count > 0; } public static IEnumerable TryGetChildLayouts(string layoutName) { if (string.IsNullOrEmpty(layoutName)) throw new ArgumentException("Layout name cannot be null or empty", nameof(layoutName)); Refresh(); var internedLayout = new InternedString(layoutName); // return nothing is the layout does not have any derivations if (!s_DeviceChildLayouts.TryGetValue(internedLayout, out var derivations)) yield break; else { foreach (var name in derivations) yield return InputControlLayout.cache.FindOrLoadLayout(name.ToString()); } } public static InputControlLayout TryGetLayout(string layoutName) { if (string.IsNullOrEmpty(layoutName)) throw new ArgumentException("Layout name cannot be null or empty", nameof(layoutName)); Refresh(); return InputControlLayout.cache.FindOrLoadLayout(layoutName, throwIfNotFound: false); } public static Type GetValueType(string layoutName) { if (string.IsNullOrEmpty(layoutName)) throw new ArgumentException("Layout name cannot be null or empty", nameof(layoutName)); // Load layout. var layout = TryGetLayout(layoutName); if (layout == null) return null; // Grab type. var type = layout.type; Debug.Assert(type != null, "Layout should have associated type"); Debug.Assert(typeof(InputControl).IsAssignableFrom(type), "Layout's associated type should be derived from InputControl"); return layout.GetValueType(); } public static IEnumerable GetDeviceMatchers(string layoutName) { if (string.IsNullOrEmpty(layoutName)) throw new ArgumentException("Layout name cannot be null or empty", nameof(layoutName)); Refresh(); s_DeviceMatchers.TryGetValue(new InternedString(layoutName), out var matchers); return matchers; } public static string GetDisplayName(string layoutName) { if (string.IsNullOrEmpty(layoutName)) throw new ArgumentException("Layout name cannot be null or empty", nameof(layoutName)); var layout = TryGetLayout(layoutName); if (layout == null) return layoutName; if (!string.IsNullOrEmpty(layout.displayName)) return layout.displayName; return layout.name; } /// /// List the controls that may be present on controls or devices of the given layout by virtue /// of being defined in other layouts based on it. /// /// /// public static IEnumerable GetOptionalControlsForLayout(string layoutName) { if (string.IsNullOrEmpty(layoutName)) throw new ArgumentException("Layout name cannot be null or empty", nameof(layoutName)); Refresh(); if (!s_OptionalControls.TryGetValue(new InternedString(layoutName), out var list)) return Enumerable.Empty(); return list; } public static Texture2D GetIconForLayout(string layoutName) { if (string.IsNullOrEmpty(layoutName)) throw new ArgumentNullException(nameof(layoutName)); Refresh(); // See if we already have it in the cache. var internedName = new InternedString(layoutName); if (s_Icons.TryGetValue(internedName, out var icon)) return icon; // No, so see if we have an icon on disk for exactly the layout // we're looking at (i.e. with the same name). icon = GUIHelpers.LoadIcon(layoutName); if (icon != null) { s_Icons.Add(internedName, icon); return icon; } // No, not that either so start walking up the inheritance chain // until we either bump against the ceiling or find an icon. var layout = TryGetLayout(layoutName); if (layout != null) { foreach (var baseLayoutName in layout.baseLayouts) { icon = GetIconForLayout(baseLayoutName); if (icon != null) return icon; } // If it's a control and there's no specific icon, return a generic one. if (layout.isControlLayout) { var genericIcon = GUIHelpers.LoadIcon("InputControl"); if (genericIcon != null) { s_Icons.Add(internedName, genericIcon); return genericIcon; } } } // No icon for anything in this layout's chain. return null; } public struct ControlSearchResult { public string controlPath; public InputControlLayout layout; public InputControlLayout.ControlItem item; } internal static void Clear() { s_LayoutRegistrationVersion = 0; s_LayoutCacheRef.Dispose(); s_Usages.Clear(); s_ControlLayouts.Clear(); s_DeviceLayouts.Clear(); s_ProductLayouts.Clear(); s_DeviceMatchers.Clear(); s_Icons.Clear(); } // If our layout data is outdated, rescan all the layouts in the system. private static void Refresh() { var manager = InputSystem.s_Manager; if (manager.m_LayoutRegistrationVersion == s_LayoutRegistrationVersion) return; Clear(); if (!s_LayoutCacheRef.valid) { // In the editor, we keep a permanent reference on the global layout // cache. Means that in the editor, we always have all layouts loaded in full // at all times whereas in the player, we load layouts only while we need // them and then release them again. s_LayoutCacheRef = InputControlLayout.CacheRef(); } var layoutNames = manager.ListControlLayouts().ToArray(); // Remember which layout maps to which device matchers. var layoutMatchers = InputControlLayout.s_Layouts.layoutMatchers; foreach (var entry in layoutMatchers) { s_DeviceMatchers.TryGetValue(entry.layoutName, out var matchers); matchers.Append(entry.deviceMatcher); s_DeviceMatchers[entry.layoutName] = matchers; } // Load and store all layouts. foreach (var layoutName in layoutNames) { ////FIXME: does not protect against exceptions var layout = InputControlLayout.cache.FindOrLoadLayout(layoutName, throwIfNotFound: false); if (layout == null) continue; ScanLayout(layout); if (layout.isOverride) continue; if (layout.isControlLayout) s_ControlLayouts.Add(layout.name); else if (s_DeviceMatchers.ContainsKey(layout.name)) s_ProductLayouts.Add(layout.name); else s_DeviceLayouts.Add(layout.name); } // Move all device layouts without a device description but derived from // a layout that has one over to the product list. foreach (var name in s_DeviceLayouts) { var layout = InputControlLayout.cache.FindOrLoadLayout(name); if (layout.m_BaseLayouts.length > 1) throw new NotImplementedException(); for (var baseLayoutName = layout.baseLayouts.FirstOrDefault(); !baseLayoutName.IsEmpty();) { if (s_ProductLayouts.Contains(baseLayoutName)) { // Defer removing from s_DeviceLayouts to keep iteration stable. s_ProductLayouts.Add(name); break; } var baseLayout = InputControlLayout.cache.FindOrLoadLayout(baseLayoutName, throwIfNotFound: false); if (baseLayout == null) continue; if (baseLayout.m_BaseLayouts.length > 1) throw new NotImplementedException(); baseLayoutName = baseLayout.baseLayouts.FirstOrDefault(); } } // Remove every product device layout now. s_DeviceLayouts.ExceptWith(s_ProductLayouts); s_LayoutRegistrationVersion = manager.m_LayoutRegistrationVersion; } private static int s_LayoutRegistrationVersion; private static InputControlLayout.CacheRefInstance s_LayoutCacheRef; private static readonly HashSet s_ControlLayouts = new HashSet(); private static readonly HashSet s_DeviceLayouts = new HashSet(); private static readonly HashSet s_ProductLayouts = new HashSet(); private static readonly Dictionary> s_OptionalControls = new Dictionary>(); private static readonly Dictionary> s_DeviceMatchers = new Dictionary>(); private static Dictionary s_Icons = new Dictionary(); // We keep a map of the devices which a derived from a base device. private static readonly Dictionary> s_DeviceChildLayouts = new Dictionary>(); // We keep a map of all unique usages we find in layouts and also // retain a list of the layouts they are used with. private static readonly SortedDictionary> s_Usages = new SortedDictionary>(); private static void ScanLayout(InputControlLayout layout) { var controls = layout.controls; for (var i = 0; i < controls.Count; ++i) { var control = controls[i]; // If it's not just a control modifying some inner child control, add control to all base // layouts as an optional control. // // NOTE: We're looking at layouts post-merging here. Means we have already picked up all the // controls present on the base. // Only controls which belong to UI-facing layouts are included, as optional controls are used solely by // the InputControlPickerDropdown UI if (control.isFirstDefinedInThisLayout && !control.isModifyingExistingControl && !control.layout.IsEmpty() && !layout.hideInUI) { foreach (var baseLayout in layout.baseLayouts) AddOptionalControlRecursive(baseLayout, ref control); } // Collect unique usages and the layouts used with them. foreach (var usage in control.usages) { // Empty usages can occur for controls that want to reset inherited usages. if (string.IsNullOrEmpty(usage)) continue; var internedUsage = new InternedString(usage); var internedLayout = new InternedString(control.layout); if (!s_Usages.TryGetValue(internedUsage, out var layoutSet)) { layoutSet = new HashSet { internedLayout }; s_Usages[internedUsage] = layoutSet; } else { layoutSet.Add(internedLayout); } } // Create a dependency tree matching each concrete device layout exposed in the UI // to all of the layouts that are directly derived from it. if (layout.isDeviceLayout && !layout.hideInUI) { foreach (var baseLayoutName in layout.baseLayouts) { if (!s_DeviceChildLayouts.TryGetValue(baseLayoutName, out var derivedSet)) { derivedSet = new HashSet { layout.name }; s_DeviceChildLayouts[baseLayoutName] = derivedSet; } else { derivedSet.Add(layout.name); } } } } } private static void AddOptionalControlRecursive(InternedString layoutName, ref InputControlLayout.ControlItem controlItem) { Debug.Assert(!controlItem.isModifyingExistingControl); Debug.Assert(!controlItem.layout.IsEmpty()); // Recurse into base. if (InputControlLayout.s_Layouts.baseLayoutTable.TryGetValue(layoutName, out var baseLayoutName)) AddOptionalControlRecursive(baseLayoutName, ref controlItem); // See if we already have this optional control. var alreadyPresent = false; if (!s_OptionalControls.TryGetValue(layoutName, out var list)) { list = new List(); s_OptionalControls[layoutName] = list; } else { // See if we already have this control. foreach (var item in list) { if (item.name == controlItem.name && item.layout == controlItem.layout) { alreadyPresent = true; break; } } } if (!alreadyPresent) list.Add(new OptionalControl {name = controlItem.name, layout = controlItem.layout}); } /// /// An optional control is a control that is not defined on a layout but which is defined /// on a derived layout. /// /// /// An example is the "acceleration" control defined by some layouts based on (e.g. /// . This means gamepads /// MAY have a gyro and thus MAY have an "acceleration" control. /// /// In bindings (), it is perfectly valid to deal with this opportunistically /// and create a binding to "<Gamepad>/acceleration" which will bind correctly IF the gamepad has /// an acceleration control but will do nothing if it doesn't. /// /// The concept of optional controls permits setting up such bindings in the UI by making controls that /// are present on more specific layouts than the one currently looked at available directly on the /// base layout. /// public struct OptionalControl { public InternedString name; public InternedString layout; ////REVIEW: do we want to have the list of layouts that define the control? } } } #endif // UNITY_EDITOR