Maze / Library /PackageCache /com.unity.inputsystem@1.6.1 /InputSystem /Editor /Debugger /InputDebuggerWindow.cs
| using System; | |
| using System.Collections.Generic; | |
| using System.IO; | |
| using System.Linq; | |
| using UnityEngine.InputSystem.LowLevel; | |
| using UnityEditor; | |
| using UnityEditorInternal; | |
| using UnityEditor.IMGUI.Controls; | |
| using UnityEditor.Networking.PlayerConnection; | |
| using UnityEngine.InputSystem.Layouts; | |
| using UnityEngine.InputSystem.Users; | |
| using UnityEngine.InputSystem.Utilities; | |
| ////FIXME: Generate proper IDs for the individual tree view items; the current sequential numbering scheme just causes lots of | |
| //// weird expansion/collapsing to happen. | |
| ////TODO: add way to load and replay event traces | |
| ////TODO: refresh metrics on demand | |
| ////TODO: when an action is triggered and when a device changes state, make them bold in the list for a brief moment | |
| ////TODO: show input users and their actions and devices | |
| ////TODO: append " (Disabled) to disabled devices and grey them out | |
| ////TODO: split 'Local' and 'Remote' at root rather than inside subnodes | |
| ////TODO: refresh when unrecognized device pops up | |
| namespace UnityEngine.InputSystem.Editor | |
| { | |
| // Allows looking at input activity in the editor. | |
| internal class InputDebuggerWindow : EditorWindow, ISerializationCallbackReceiver | |
| { | |
| private static int s_Disabled; | |
| private static InputDebuggerWindow s_Instance; | |
| [] | |
| public static void CreateOrShow() | |
| { | |
| if (s_Instance == null) | |
| { | |
| s_Instance = GetWindow<InputDebuggerWindow>(); | |
| s_Instance.Show(); | |
| s_Instance.titleContent = new GUIContent("Input Debug"); | |
| } | |
| else | |
| { | |
| s_Instance.Show(); | |
| s_Instance.Focus(); | |
| } | |
| } | |
| public static void Enable() | |
| { | |
| if (s_Disabled == 0) | |
| return; | |
| --s_Disabled; | |
| if (s_Disabled == 0 && s_Instance != null) | |
| { | |
| s_Instance.InstallHooks(); | |
| s_Instance.Refresh(); | |
| } | |
| } | |
| public static void Disable() | |
| { | |
| ++s_Disabled; | |
| if (s_Disabled == 1 && s_Instance != null) | |
| { | |
| s_Instance.UninstallHooks(); | |
| s_Instance.Refresh(); | |
| } | |
| } | |
| private void OnDeviceChange(InputDevice device, InputDeviceChange change) | |
| { | |
| // Update tree if devices are added or removed. | |
| if (change == InputDeviceChange.Added || change == InputDeviceChange.Removed) | |
| Refresh(); | |
| } | |
| private void OnLayoutChange(string name, InputControlLayoutChange change) | |
| { | |
| // Update tree if layout setup has changed. | |
| Refresh(); | |
| } | |
| private void OnActionChange(object actionOrMap, InputActionChange change) | |
| { | |
| switch (change) | |
| { | |
| // When an action is triggered, we only need a repaint. | |
| case InputActionChange.ActionStarted: | |
| case InputActionChange.ActionPerformed: | |
| case InputActionChange.ActionCanceled: | |
| Repaint(); | |
| break; | |
| case InputActionChange.ActionEnabled: | |
| case InputActionChange.ActionDisabled: | |
| case InputActionChange.ActionMapDisabled: | |
| case InputActionChange.ActionMapEnabled: | |
| case InputActionChange.BoundControlsChanged: | |
| Refresh(); | |
| break; | |
| } | |
| } | |
| private void OnSettingsChange() | |
| { | |
| Refresh(); | |
| } | |
| private string OnFindLayout(ref InputDeviceDescription description, string matchedLayout, | |
| InputDeviceExecuteCommandDelegate executeCommandDelegate) | |
| { | |
| // If there's no matched layout, there's a chance this device will go in | |
| // the unsupported list. There's no direct notification for that so we | |
| // preemptively trigger a refresh. | |
| if (string.IsNullOrEmpty(matchedLayout)) | |
| Refresh(); | |
| return null; | |
| } | |
| private void Refresh() | |
| { | |
| m_NeedReload = true; | |
| Repaint(); | |
| } | |
| public void OnDestroy() | |
| { | |
| UninstallHooks(); | |
| } | |
| private void InstallHooks() | |
| { | |
| InputSystem.onDeviceChange += OnDeviceChange; | |
| InputSystem.onLayoutChange += OnLayoutChange; | |
| InputSystem.onFindLayoutForDevice += OnFindLayout; | |
| InputSystem.onActionChange += OnActionChange; | |
| InputSystem.onSettingsChange += OnSettingsChange; | |
| } | |
| private void UninstallHooks() | |
| { | |
| InputSystem.onDeviceChange -= OnDeviceChange; | |
| InputSystem.onLayoutChange -= OnLayoutChange; | |
| InputSystem.onFindLayoutForDevice -= OnFindLayout; | |
| InputSystem.onActionChange -= OnActionChange; | |
| InputSystem.onSettingsChange -= OnSettingsChange; | |
| } | |
| private void Initialize() | |
| { | |
| InstallHooks(); | |
| var newTreeViewState = m_TreeViewState == null; | |
| if (newTreeViewState) | |
| m_TreeViewState = new TreeViewState(); | |
| m_TreeView = new InputSystemTreeView(m_TreeViewState); | |
| // Set default expansion states. | |
| if (newTreeViewState) | |
| m_TreeView.SetExpanded(m_TreeView.devicesItem.id, true); | |
| m_Initialized = true; | |
| } | |
| public void OnGUI() | |
| { | |
| if (s_Disabled > 0) | |
| { | |
| EditorGUILayout.LabelField("Disabled"); | |
| return; | |
| } | |
| // If the new backends aren't enabled, show a warning in the debugger. | |
| if (!EditorPlayerSettingHelpers.newSystemBackendsEnabled) | |
| { | |
| EditorGUILayout.HelpBox( | |
| "Platform backends for the new input system are not enabled. " + | |
| "No devices and input from hardware will come through in the new input system APIs.\n\n" + | |
| "To enable the backends, set 'Active Input Handling' in the player settings to either 'Input System (Preview)' " + | |
| "or 'Both' and restart the editor.", MessageType.Warning); | |
| } | |
| // This also brings us back online after a domain reload. | |
| if (!m_Initialized) | |
| { | |
| Initialize(); | |
| } | |
| else if (m_NeedReload) | |
| { | |
| m_TreeView.Reload(); | |
| m_NeedReload = false; | |
| } | |
| DrawToolbarGUI(); | |
| var rect = EditorGUILayout.GetControlRect(GUILayout.ExpandHeight(true)); | |
| m_TreeView.OnGUI(rect); | |
| } | |
| private static void ResetDevice(InputDevice device, bool hard) | |
| { | |
| var playerUpdateType = InputDeviceDebuggerWindow.DetermineUpdateTypeToShow(device); | |
| var currentUpdateType = InputState.currentUpdateType; | |
| InputStateBuffers.SwitchTo(InputSystem.s_Manager.m_StateBuffers, playerUpdateType); | |
| InputSystem.ResetDevice(device, alsoResetDontResetControls: hard); | |
| InputStateBuffers.SwitchTo(InputSystem.s_Manager.m_StateBuffers, currentUpdateType); | |
| } | |
| private static void ToggleAddDevicesNotSupportedByProject() | |
| { | |
| InputEditorUserSettings.addDevicesNotSupportedByProject = | |
| !InputEditorUserSettings.addDevicesNotSupportedByProject; | |
| } | |
| private void ToggleDiagnosticMode() | |
| { | |
| if (InputSystem.s_Manager.m_Diagnostics != null) | |
| { | |
| InputSystem.s_Manager.m_Diagnostics = null; | |
| } | |
| else | |
| { | |
| if (m_Diagnostics == null) | |
| m_Diagnostics = new InputDiagnostics(); | |
| InputSystem.s_Manager.m_Diagnostics = m_Diagnostics; | |
| } | |
| } | |
| private static void ToggleTouchSimulation() | |
| { | |
| InputEditorUserSettings.simulateTouch = !InputEditorUserSettings.simulateTouch; | |
| } | |
| private static void EnableRemoteDevices(bool enable = true) | |
| { | |
| foreach (var player in EditorConnection.instance.ConnectedPlayers) | |
| { | |
| EditorConnection.instance.Send(enable ? RemoteInputPlayerConnection.kStartSendingMsg : RemoteInputPlayerConnection.kStopSendingMsg, new byte[0], player.playerId); | |
| if (!enable) | |
| InputSystem.remoting.RemoveRemoteDevices(player.playerId); | |
| } | |
| } | |
| private static void DrawConnectionGUI() | |
| { | |
| if (GUILayout.Button("Remote Devices…", EditorStyles.toolbarDropDown)) | |
| { | |
| var menu = new GenericMenu(); | |
| var haveRemotes = InputSystem.devices.Any(x => x.remote); | |
| if (EditorConnection.instance.ConnectedPlayers.Count > 0) | |
| menu.AddItem(new GUIContent("Show remote devices"), haveRemotes, () => | |
| { | |
| EnableRemoteDevices(!haveRemotes); | |
| }); | |
| else | |
| menu.AddDisabledItem(new GUIContent("Show remote input devices")); | |
| menu.AddSeparator(""); | |
| var availableProfilers = ProfilerDriver.GetAvailableProfilers(); | |
| foreach (var profiler in availableProfilers) | |
| { | |
| var enabled = ProfilerDriver.IsIdentifierConnectable(profiler); | |
| var profilerName = ProfilerDriver.GetConnectionIdentifier(profiler); | |
| var isConnected = ProfilerDriver.connectedProfiler == profiler; | |
| if (enabled) | |
| menu.AddItem(new GUIContent(profilerName), isConnected, () => { | |
| ProfilerDriver.connectedProfiler = profiler; | |
| EnableRemoteDevices(); | |
| }); | |
| else | |
| menu.AddDisabledItem(new GUIContent(profilerName)); | |
| } | |
| foreach (var device in UnityEditor.Hardware.DevDeviceList.GetDevices()) | |
| { | |
| var supportsPlayerConnection = (device.features & UnityEditor.Hardware.DevDeviceFeatures.PlayerConnection) != 0; | |
| if (!device.isConnected || !supportsPlayerConnection) | |
| continue; | |
| var url = "device://" + device.id; | |
| var isConnected = ProfilerDriver.connectedProfiler == 0xFEEE && ProfilerDriver.directConnectionUrl == url; | |
| menu.AddItem(new GUIContent(device.name), isConnected, () => { | |
| ProfilerDriver.DirectURLConnect(url); | |
| EnableRemoteDevices(); | |
| }); | |
| } | |
| menu.ShowAsContext(); | |
| } | |
| } | |
| private void DrawToolbarGUI() | |
| { | |
| EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); | |
| if (GUILayout.Button(Contents.optionsContent, EditorStyles.toolbarDropDown)) | |
| { | |
| var menu = new GenericMenu(); | |
| menu.AddItem(Contents.addDevicesNotSupportedByProjectContent, InputEditorUserSettings.addDevicesNotSupportedByProject, | |
| ToggleAddDevicesNotSupportedByProject); | |
| menu.AddItem(Contents.diagnosticsModeContent, InputSystem.s_Manager.m_Diagnostics != null, | |
| ToggleDiagnosticMode); | |
| menu.AddItem(Contents.touchSimulationContent, InputEditorUserSettings.simulateTouch, ToggleTouchSimulation); | |
| // Add the inverse of "Copy Device Description" which adds a device with the description from | |
| // the clipboard to the system. This is most useful for debugging and makes it very easy to | |
| // have a first pass at device descriptions supplied by users. | |
| try | |
| { | |
| var copyBuffer = EditorGUIUtility.systemCopyBuffer; | |
| if (!string.IsNullOrEmpty(copyBuffer) && | |
| copyBuffer.StartsWith("{") && !InputDeviceDescription.FromJson(copyBuffer).empty) | |
| { | |
| menu.AddItem(Contents.pasteDeviceDescriptionAsDevice, false, () => | |
| { | |
| var description = InputDeviceDescription.FromJson(copyBuffer); | |
| InputSystem.AddDevice(description); | |
| }); | |
| } | |
| } | |
| catch (ArgumentException) | |
| { | |
| // Catch and ignore exception if buffer doesn't actually contain an InputDeviceDescription | |
| // in (proper) JSON format. | |
| } | |
| menu.ShowAsContext(); | |
| } | |
| DrawConnectionGUI(); | |
| GUILayout.FlexibleSpace(); | |
| EditorGUILayout.EndHorizontal(); | |
| } | |
| [] private TreeViewState m_TreeViewState; | |
| [] private InputDiagnostics m_Diagnostics; | |
| [] private InputSystemTreeView m_TreeView; | |
| [] private bool m_Initialized; | |
| [] private bool m_NeedReload; | |
| internal static void ReviveAfterDomainReload() | |
| { | |
| if (s_Instance != null) | |
| { | |
| // Trigger initial repaint. Will call Initialize() to install hooks and | |
| // refresh tree. | |
| s_Instance.Repaint(); | |
| } | |
| } | |
| private static class Contents | |
| { | |
| public static readonly GUIContent optionsContent = new GUIContent("Options"); | |
| public static readonly GUIContent touchSimulationContent = new GUIContent("Simulate Touch Input From Mouse or Pen"); | |
| public static readonly GUIContent pasteDeviceDescriptionAsDevice = new GUIContent("Paste Device Description as Device"); | |
| public static readonly GUIContent addDevicesNotSupportedByProjectContent = new GUIContent("Add Devices Not Listed in 'Supported Devices'"); | |
| public static readonly GUIContent diagnosticsModeContent = new GUIContent("Enable Event Diagnostics"); | |
| public static readonly GUIContent openDebugView = new GUIContent("Open Device Debug View"); | |
| public static readonly GUIContent copyDeviceDescription = new GUIContent("Copy Device Description"); | |
| public static readonly GUIContent copyLayoutAsJSON = new GUIContent("Copy Layout as JSON"); | |
| public static readonly GUIContent createDeviceFromLayout = new GUIContent("Create Device from Layout"); | |
| public static readonly GUIContent generateCodeFromLayout = new GUIContent("Generate Precompiled Layout"); | |
| public static readonly GUIContent removeDevice = new GUIContent("Remove Device"); | |
| public static readonly GUIContent enableDevice = new GUIContent("Enable Device"); | |
| public static readonly GUIContent disableDevice = new GUIContent("Disable Device"); | |
| public static readonly GUIContent syncDevice = new GUIContent("Try to Sync Device"); | |
| public static readonly GUIContent softResetDevice = new GUIContent("Reset Device (Soft)"); | |
| public static readonly GUIContent hardResetDevice = new GUIContent("Reset Device (Hard)"); | |
| } | |
| void ISerializationCallbackReceiver.OnBeforeSerialize() | |
| { | |
| } | |
| void ISerializationCallbackReceiver.OnAfterDeserialize() | |
| { | |
| s_Instance = this; | |
| } | |
| private class InputSystemTreeView : TreeView | |
| { | |
| public TreeViewItem actionsItem { get; private set; } | |
| public TreeViewItem devicesItem { get; private set; } | |
| public TreeViewItem layoutsItem { get; private set; } | |
| public TreeViewItem settingsItem { get; private set; } | |
| public TreeViewItem metricsItem { get; private set; } | |
| public TreeViewItem usersItem { get; private set; } | |
| public InputSystemTreeView(TreeViewState state) | |
| : base(state) | |
| { | |
| Reload(); | |
| } | |
| protected override void ContextClickedItem(int id) | |
| { | |
| var item = FindItem(id, rootItem); | |
| if (item == null) | |
| return; | |
| if (item is DeviceItem deviceItem) | |
| { | |
| var menu = new GenericMenu(); | |
| menu.AddItem(Contents.openDebugView, false, () => InputDeviceDebuggerWindow.CreateOrShowExisting(deviceItem.device)); | |
| menu.AddItem(Contents.copyDeviceDescription, false, | |
| () => EditorGUIUtility.systemCopyBuffer = deviceItem.device.description.ToJson()); | |
| menu.AddItem(Contents.removeDevice, false, () => InputSystem.RemoveDevice(deviceItem.device)); | |
| if (deviceItem.device.enabled) | |
| menu.AddItem(Contents.disableDevice, false, () => InputSystem.DisableDevice(deviceItem.device)); | |
| else | |
| menu.AddItem(Contents.enableDevice, false, () => InputSystem.EnableDevice(deviceItem.device)); | |
| menu.AddItem(Contents.syncDevice, false, () => InputSystem.TrySyncDevice(deviceItem.device)); | |
| menu.AddItem(Contents.softResetDevice, false, () => ResetDevice(deviceItem.device, false)); | |
| menu.AddItem(Contents.hardResetDevice, false, () => ResetDevice(deviceItem.device, true)); | |
| menu.ShowAsContext(); | |
| } | |
| if (item is UnsupportedDeviceItem unsupportedDeviceItem) | |
| { | |
| var menu = new GenericMenu(); | |
| menu.AddItem(Contents.copyDeviceDescription, false, | |
| () => EditorGUIUtility.systemCopyBuffer = unsupportedDeviceItem.description.ToJson()); | |
| menu.ShowAsContext(); | |
| } | |
| if (item is LayoutItem layoutItem) | |
| { | |
| var layout = EditorInputControlLayoutCache.TryGetLayout(layoutItem.layoutName); | |
| if (layout != null) | |
| { | |
| var menu = new GenericMenu(); | |
| menu.AddItem(Contents.copyLayoutAsJSON, false, | |
| () => EditorGUIUtility.systemCopyBuffer = layout.ToJson()); | |
| if (layout.isDeviceLayout) | |
| { | |
| menu.AddItem(Contents.createDeviceFromLayout, false, | |
| () => InputSystem.AddDevice(layout.name)); | |
| menu.AddItem(Contents.generateCodeFromLayout, false, () => | |
| { | |
| var fileName = EditorUtility.SaveFilePanel("Generate InputDevice Code", "", "Fast" + layoutItem.layoutName, "cs"); | |
| var isInAssets = fileName.StartsWith(Application.dataPath, StringComparison.OrdinalIgnoreCase); | |
| if (isInAssets) | |
| fileName = "Assets/" + fileName.Substring(Application.dataPath.Length + 1); | |
| if (!string.IsNullOrEmpty(fileName)) | |
| { | |
| var code = InputLayoutCodeGenerator.GenerateCodeFileForDeviceLayout(layoutItem.layoutName, fileName, prefix: "Fast"); | |
| File.WriteAllText(fileName, code); | |
| if (isInAssets) | |
| AssetDatabase.Refresh(); | |
| } | |
| }); | |
| } | |
| menu.ShowAsContext(); | |
| } | |
| } | |
| } | |
| protected override void DoubleClickedItem(int id) | |
| { | |
| var item = FindItem(id, rootItem); | |
| if (item is DeviceItem deviceItem) | |
| InputDeviceDebuggerWindow.CreateOrShowExisting(deviceItem.device); | |
| } | |
| protected override TreeViewItem BuildRoot() | |
| { | |
| var id = 0; | |
| var root = new TreeViewItem | |
| { | |
| id = id++, | |
| depth = -1 | |
| }; | |
| ////TODO: this will need to be improved for multi-user scenarios | |
| // Actions. | |
| m_EnabledActions.Clear(); | |
| InputSystem.ListEnabledActions(m_EnabledActions); | |
| if (m_EnabledActions.Count > 0) | |
| { | |
| actionsItem = AddChild(root, "", ref id); | |
| AddEnabledActions(actionsItem, ref id); | |
| if (!actionsItem.hasChildren) | |
| { | |
| // We are culling actions that are assigned to users so we may end up with an empty | |
| // list even if we have enabled actions. If we do, remove the "Actions" item from the tree. | |
| root.children.Remove(actionsItem); | |
| } | |
| else | |
| { | |
| // Update title to include action count. | |
| actionsItem.displayName = $"Actions ({actionsItem.children.Count})"; | |
| } | |
| } | |
| // Users. | |
| var userCount = InputUser.all.Count; | |
| if (userCount > 0) | |
| { | |
| usersItem = AddChild(root, $"Users ({userCount})", ref id); | |
| foreach (var user in InputUser.all) | |
| AddUser(usersItem, user, ref id); | |
| } | |
| // Devices. | |
| var devices = InputSystem.devices; | |
| devicesItem = AddChild(root, $"Devices ({devices.Count})", ref id); | |
| var haveRemotes = devices.Any(x => x.remote); | |
| TreeViewItem localDevicesNode = null; | |
| if (haveRemotes) | |
| { | |
| // Split local and remote devices into groups. | |
| localDevicesNode = AddChild(devicesItem, "Local", ref id); | |
| AddDevices(localDevicesNode, devices, ref id); | |
| var remoteDevicesNode = AddChild(devicesItem, "Remote", ref id); | |
| foreach (var player in EditorConnection.instance.ConnectedPlayers) | |
| { | |
| var playerNode = AddChild(remoteDevicesNode, player.name, ref id); | |
| AddDevices(playerNode, devices, ref id, player.playerId); | |
| } | |
| } | |
| else | |
| { | |
| // We don't have remote devices so don't add an extra group for local devices. | |
| // Put them all directly underneath the "Devices" node. | |
| AddDevices(devicesItem, devices, ref id); | |
| } | |
| ////TDO: unsupported and disconnected devices should also be shown for remotes | |
| if (m_UnsupportedDevices == null) | |
| m_UnsupportedDevices = new List<InputDeviceDescription>(); | |
| m_UnsupportedDevices.Clear(); | |
| InputSystem.GetUnsupportedDevices(m_UnsupportedDevices); | |
| if (m_UnsupportedDevices.Count > 0) | |
| { | |
| var parent = haveRemotes ? localDevicesNode : devicesItem; | |
| var unsupportedDevicesNode = AddChild(parent, $"Unsupported ({m_UnsupportedDevices.Count})", ref id); | |
| foreach (var device in m_UnsupportedDevices) | |
| { | |
| var item = new UnsupportedDeviceItem | |
| { | |
| id = id++, | |
| depth = unsupportedDevicesNode.depth + 1, | |
| displayName = device.ToString(), | |
| description = device | |
| }; | |
| unsupportedDevicesNode.AddChild(item); | |
| } | |
| unsupportedDevicesNode.children.Sort((a, b) => | |
| string.Compare(a.displayName, b.displayName, StringComparison.InvariantCulture)); | |
| } | |
| var disconnectedDevices = InputSystem.disconnectedDevices; | |
| if (disconnectedDevices.Count > 0) | |
| { | |
| var parent = haveRemotes ? localDevicesNode : devicesItem; | |
| var disconnectedDevicesNode = AddChild(parent, $"Disconnected ({disconnectedDevices.Count})", ref id); | |
| foreach (var device in disconnectedDevices) | |
| AddChild(disconnectedDevicesNode, device.ToString(), ref id); | |
| disconnectedDevicesNode.children.Sort((a, b) => | |
| string.Compare(a.displayName, b.displayName, StringComparison.InvariantCulture)); | |
| } | |
| // Layouts. | |
| layoutsItem = AddChild(root, "Layouts", ref id); | |
| AddControlLayouts(layoutsItem, ref id); | |
| ////FIXME: this shows local configuration only | |
| // Settings. | |
| var settings = InputSystem.settings; | |
| var settingsAssetPath = AssetDatabase.GetAssetPath(settings); | |
| var settingsLabel = "Settings"; | |
| if (!string.IsNullOrEmpty(settingsAssetPath)) | |
| settingsLabel = $"Settings ({Path.GetFileName(settingsAssetPath)})"; | |
| settingsItem = AddChild(root, settingsLabel, ref id); | |
| AddValueItem(settingsItem, "Update Mode", settings.updateMode, ref id); | |
| AddValueItem(settingsItem, "Compensate For Screen Orientation", settings.compensateForScreenOrientation, ref id); | |
| AddValueItem(settingsItem, "Default Button Press Point", settings.defaultButtonPressPoint, ref id); | |
| AddValueItem(settingsItem, "Default Deadzone Min", settings.defaultDeadzoneMin, ref id); | |
| AddValueItem(settingsItem, "Default Deadzone Max", settings.defaultDeadzoneMax, ref id); | |
| AddValueItem(settingsItem, "Default Tap Time", settings.defaultTapTime, ref id); | |
| AddValueItem(settingsItem, "Default Slow Tap Time", settings.defaultSlowTapTime, ref id); | |
| AddValueItem(settingsItem, "Default Hold Time", settings.defaultHoldTime, ref id); | |
| if (settings.supportedDevices.Count > 0) | |
| { | |
| var supportedDevices = AddChild(settingsItem, "Supported Devices", ref id); | |
| foreach (var item in settings.supportedDevices) | |
| { | |
| var icon = EditorInputControlLayoutCache.GetIconForLayout(item); | |
| AddChild(supportedDevices, item, ref id, icon); | |
| } | |
| } | |
| settingsItem.children.Sort((a, b) => string.Compare(a.displayName, b.displayName, StringComparison.InvariantCultureIgnoreCase)); | |
| // Metrics. | |
| var metrics = InputSystem.metrics; | |
| metricsItem = AddChild(root, "Metrics", ref id); | |
| AddChild(metricsItem, | |
| "Current State Size in Bytes: " + StringHelpers.NicifyMemorySize(metrics.currentStateSizeInBytes), | |
| ref id); | |
| AddValueItem(metricsItem, "Current Control Count", metrics.currentControlCount, ref id); | |
| AddValueItem(metricsItem, "Current Layout Count", metrics.currentLayoutCount, ref id); | |
| return root; | |
| } | |
| private void AddUser(TreeViewItem parent, InputUser user, ref int id) | |
| { | |
| ////REVIEW: can we get better identification? allow associating GameObject with user? | |
| var userItem = AddChild(parent, "User #" + user.index, ref id); | |
| // Control scheme. | |
| var controlScheme = user.controlScheme; | |
| if (controlScheme != null) | |
| AddChild(userItem, "Control Scheme: " + controlScheme, ref id); | |
| // Paired and lost devices. | |
| AddDeviceListToUser("Paired Devices", user.pairedDevices, ref id, userItem); | |
| AddDeviceListToUser("Lost Devices", user.lostDevices, ref id, userItem); | |
| // Actions. | |
| var actions = user.actions; | |
| if (actions != null) | |
| { | |
| var actionsItem = AddChild(userItem, "Actions", ref id); | |
| foreach (var action in actions) | |
| AddActionItem(actionsItem, action, ref id); | |
| parent.children?.Sort((a, b) => string.Compare(a.displayName, b.displayName, StringComparison.CurrentCultureIgnoreCase)); | |
| } | |
| } | |
| private void AddDeviceListToUser(string title, ReadOnlyArray<InputDevice> devices, ref int id, TreeViewItem userItem) | |
| { | |
| if (devices.Count == 0) | |
| return; | |
| var devicesItem = AddChild(userItem, title, ref id); | |
| foreach (var device in devices) | |
| { | |
| Debug.Assert(device != null, title + " has a null item!"); | |
| if (device == null) | |
| continue; | |
| var item = new DeviceItem | |
| { | |
| id = id++, | |
| depth = devicesItem.depth + 1, | |
| displayName = device.ToString(), | |
| device = device, | |
| icon = EditorInputControlLayoutCache.GetIconForLayout(device.layout), | |
| }; | |
| devicesItem.AddChild(item); | |
| } | |
| } | |
| private static void AddDevices(TreeViewItem parent, IEnumerable<InputDevice> devices, ref int id, int participantId = InputDevice.kLocalParticipantId) | |
| { | |
| foreach (var device in devices) | |
| { | |
| if (device.m_ParticipantId != participantId) | |
| continue; | |
| var displayName = device.name; | |
| if (device.usages.Count > 0) | |
| displayName += " (" + string.Join(",", device.usages) + ")"; | |
| var item = new DeviceItem | |
| { | |
| id = id++, | |
| depth = parent.depth + 1, | |
| displayName = displayName, | |
| device = device, | |
| icon = EditorInputControlLayoutCache.GetIconForLayout(device.layout), | |
| }; | |
| parent.AddChild(item); | |
| } | |
| parent.children?.Sort((a, b) => string.Compare(a.displayName, b.displayName)); | |
| } | |
| private void AddControlLayouts(TreeViewItem parent, ref int id) | |
| { | |
| // Split root into three different groups: | |
| // 1) Control layouts | |
| // 2) Device layouts that don't match specific products | |
| // 3) Device layouts that match specific products | |
| var controls = AddChild(parent, "Controls", ref id); | |
| var devices = AddChild(parent, "Abstract Devices", ref id); | |
| var products = AddChild(parent, "Specific Devices", ref id); | |
| foreach (var layout in EditorInputControlLayoutCache.allControlLayouts) | |
| AddControlLayoutItem(layout, controls, ref id); | |
| foreach (var layout in EditorInputControlLayoutCache.allDeviceLayouts) | |
| AddControlLayoutItem(layout, devices, ref id); | |
| foreach (var layout in EditorInputControlLayoutCache.allProductLayouts) | |
| { | |
| var rootBaseLayoutName = InputControlLayout.s_Layouts.GetRootLayoutName(layout.name).ToString(); | |
| var groupName = string.IsNullOrEmpty(rootBaseLayoutName) ? "Other" : rootBaseLayoutName + "s"; | |
| var group = products.children?.FirstOrDefault(x => x.displayName == groupName); | |
| if (group == null) | |
| { | |
| group = AddChild(products, groupName, ref id); | |
| if (!string.IsNullOrEmpty(rootBaseLayoutName)) | |
| group.icon = EditorInputControlLayoutCache.GetIconForLayout(rootBaseLayoutName); | |
| } | |
| AddControlLayoutItem(layout, group, ref id); | |
| } | |
| controls.children?.Sort((a, b) => string.Compare(a.displayName, b.displayName)); | |
| devices.children?.Sort((a, b) => string.Compare(a.displayName, b.displayName)); | |
| if (products.children != null) | |
| { | |
| products.children.Sort((a, b) => string.Compare(a.displayName, b.displayName)); | |
| foreach (var productGroup in products.children) | |
| productGroup.children.Sort((a, b) => string.Compare(a.displayName, b.displayName)); | |
| } | |
| } | |
| private TreeViewItem AddControlLayoutItem(InputControlLayout layout, TreeViewItem parent, ref int id) | |
| { | |
| var item = new LayoutItem | |
| { | |
| parent = parent, | |
| depth = parent.depth + 1, | |
| id = id++, | |
| displayName = layout.displayName ?? layout.name, | |
| layoutName = layout.name, | |
| }; | |
| item.icon = EditorInputControlLayoutCache.GetIconForLayout(layout.name); | |
| parent.AddChild(item); | |
| // Header. | |
| AddChild(item, "Type: " + layout.type?.Name, ref id); | |
| if (!string.IsNullOrEmpty(layout.m_DisplayName)) | |
| AddChild(item, "Display Name: " + layout.m_DisplayName, ref id); | |
| if (!string.IsNullOrEmpty(layout.name)) | |
| AddChild(item, "Name: " + layout.name, ref id); | |
| var baseLayouts = StringHelpers.Join(layout.baseLayouts, ", "); | |
| if (!string.IsNullOrEmpty(baseLayouts)) | |
| AddChild(item, "Extends: " + baseLayouts, ref id); | |
| if (layout.stateFormat != 0) | |
| AddChild(item, "Format: " + layout.stateFormat, ref id); | |
| if (layout.m_UpdateBeforeRender != null) | |
| { | |
| var value = layout.m_UpdateBeforeRender.Value ? "Update" : "Disabled"; | |
| AddChild(item, "Before Render: " + value, ref id); | |
| } | |
| if (layout.commonUsages.Count > 0) | |
| { | |
| AddChild(item, | |
| "Common Usages: " + | |
| string.Join(", ", layout.commonUsages.Select(x => x.ToString()).ToArray()), | |
| ref id); | |
| } | |
| if (layout.appliedOverrides.Count() > 0) | |
| { | |
| AddChild(item, | |
| "Applied Overrides: " + | |
| string.Join(", ", layout.appliedOverrides), | |
| ref id); | |
| } | |
| ////TODO: find a more elegant solution than multiple "Matching Devices" parents when having multiple | |
| //// matchers | |
| // Device matchers. | |
| foreach (var matcher in EditorInputControlLayoutCache.GetDeviceMatchers(layout.name)) | |
| { | |
| var node = AddChild(item, "Matching Devices", ref id); | |
| foreach (var pattern in matcher.patterns) | |
| AddChild(node, $"{pattern.Key} => \"{pattern.Value}\"", ref id); | |
| } | |
| // Controls. | |
| if (layout.controls.Count > 0) | |
| { | |
| var controls = AddChild(item, "Controls", ref id); | |
| foreach (var control in layout.controls) | |
| AddControlItem(control, controls, ref id); | |
| controls.children.Sort((a, b) => string.Compare(a.displayName, b.displayName)); | |
| } | |
| return item; | |
| } | |
| private void AddControlItem(InputControlLayout.ControlItem control, TreeViewItem parent, ref int id) | |
| { | |
| var item = AddChild(parent, control.variants.IsEmpty() ? control.name : string.Format("{0} ({1})", | |
| control.name, control.variants), ref id); | |
| if (!control.layout.IsEmpty()) | |
| item.icon = EditorInputControlLayoutCache.GetIconForLayout(control.layout); | |
| ////TODO: fully merge TreeViewItems from isModifyingExistingControl control layouts into the control they modify | |
| ////TODO: allow clicking this field to jump to the layout | |
| if (!control.layout.IsEmpty()) | |
| AddChild(item, $"Layout: {control.layout}", ref id); | |
| if (!control.variants.IsEmpty()) | |
| AddChild(item, $"Variant: {control.variants}", ref id); | |
| if (!string.IsNullOrEmpty(control.displayName)) | |
| AddChild(item, $"Display Name: {control.displayName}", ref id); | |
| if (!string.IsNullOrEmpty(control.shortDisplayName)) | |
| AddChild(item, $"Short Display Name: {control.shortDisplayName}", ref id); | |
| if (control.format != 0) | |
| AddChild(item, $"Format: {control.format}", ref id); | |
| if (control.offset != InputStateBlock.InvalidOffset) | |
| AddChild(item, $"Offset: {control.offset}", ref id); | |
| if (control.bit != InputStateBlock.InvalidOffset) | |
| AddChild(item, $"Bit: {control.bit}", ref id); | |
| if (control.sizeInBits != 0) | |
| AddChild(item, $"Size In Bits: {control.sizeInBits}", ref id); | |
| if (control.isArray) | |
| AddChild(item, $"Array Size: {control.arraySize}", ref id); | |
| if (!string.IsNullOrEmpty(control.useStateFrom)) | |
| AddChild(item, $"Use State From: {control.useStateFrom}", ref id); | |
| if (!control.defaultState.isEmpty) | |
| AddChild(item, $"Default State: {control.defaultState.ToString()}", ref id); | |
| if (!control.minValue.isEmpty) | |
| AddChild(item, $"Min Value: {control.minValue.ToString()}", ref id); | |
| if (!control.maxValue.isEmpty) | |
| AddChild(item, $"Max Value: {control.maxValue.ToString()}", ref id); | |
| if (control.usages.Count > 0) | |
| AddChild(item, "Usages: " + string.Join(", ", control.usages.Select(x => x.ToString()).ToArray()), ref id); | |
| if (control.aliases.Count > 0) | |
| AddChild(item, "Aliases: " + string.Join(", ", control.aliases.Select(x => x.ToString()).ToArray()), ref id); | |
| if (control.isNoisy || control.isSynthetic) | |
| { | |
| var flags = "Flags: "; | |
| if (control.isNoisy) | |
| flags += "Noisy"; | |
| if (control.isSynthetic) | |
| { | |
| if (control.isNoisy) | |
| flags += ", Synthetic"; | |
| else | |
| flags += "Synthetic"; | |
| } | |
| AddChild(item, flags, ref id); | |
| } | |
| if (control.parameters.Count > 0) | |
| { | |
| var parameters = AddChild(item, "Parameters", ref id); | |
| foreach (var parameter in control.parameters) | |
| AddChild(parameters, parameter.ToString(), ref id); | |
| } | |
| if (control.processors.Count > 0) | |
| { | |
| var processors = AddChild(item, "Processors", ref id); | |
| foreach (var processor in control.processors) | |
| { | |
| var processorItem = AddChild(processors, processor.name, ref id); | |
| foreach (var parameter in processor.parameters) | |
| AddChild(processorItem, parameter.ToString(), ref id); | |
| } | |
| } | |
| } | |
| private void AddValueItem<TValue>(TreeViewItem parent, string name, TValue value, ref int id) | |
| { | |
| var item = new ConfigurationItem | |
| { | |
| id = id++, | |
| depth = parent.depth + 1, | |
| displayName = $"{name}: {value.ToString()}", | |
| name = name | |
| }; | |
| parent.AddChild(item); | |
| } | |
| private void AddEnabledActions(TreeViewItem parent, ref int id) | |
| { | |
| foreach (var action in m_EnabledActions) | |
| { | |
| // If we have users, find out if the action is owned by a user. If so, don't display | |
| // it separately. | |
| var isOwnedByUser = false; | |
| foreach (var user in InputUser.all) | |
| { | |
| var userActions = user.actions; | |
| if (userActions != null && userActions.Contains(action)) | |
| { | |
| isOwnedByUser = true; | |
| break; | |
| } | |
| } | |
| if (!isOwnedByUser) | |
| AddActionItem(parent, action, ref id); | |
| } | |
| parent.children?.Sort((a, b) => string.Compare(a.displayName, b.displayName, StringComparison.CurrentCultureIgnoreCase)); | |
| } | |
| private unsafe void AddActionItem(TreeViewItem parent, InputAction action, ref int id) | |
| { | |
| // Add item for action. | |
| var name = action.actionMap != null ? $"{action.actionMap.name}/{action.name}" : action.name; | |
| if (!action.enabled) | |
| name += " (Disabled)"; | |
| var item = AddChild(parent, name, ref id); | |
| // Grab state. | |
| var actionMap = action.GetOrCreateActionMap(); | |
| actionMap.ResolveBindingsIfNecessary(); | |
| var state = actionMap.m_State; | |
| // Add list of resolved controls. | |
| var actionIndex = action.m_ActionIndexInState; | |
| var totalBindingCount = state.totalBindingCount; | |
| for (var i = 0; i < totalBindingCount; ++i) | |
| { | |
| ref var bindingState = ref state.bindingStates[i]; | |
| if (bindingState.actionIndex != actionIndex) | |
| continue; | |
| if (bindingState.isComposite) | |
| continue; | |
| var binding = state.GetBinding(i); | |
| var controlCount = bindingState.controlCount; | |
| var controlStartIndex = bindingState.controlStartIndex; | |
| for (var n = 0; n < controlCount; ++n) | |
| { | |
| var control = state.controls[controlStartIndex + n]; | |
| var interactions = | |
| StringHelpers.Join(new[] {binding.effectiveInteractions, action.interactions}, ","); | |
| var text = control.path; | |
| if (!string.IsNullOrEmpty(interactions)) | |
| { | |
| var namesAndParameters = NameAndParameters.ParseMultiple(interactions); | |
| text += " ["; | |
| text += string.Join(",", namesAndParameters.Select(x => x.name)); | |
| text += "]"; | |
| } | |
| AddChild(item, text, ref id); | |
| } | |
| } | |
| } | |
| private TreeViewItem AddChild(TreeViewItem parent, string displayName, ref int id, Texture2D icon = null) | |
| { | |
| var item = new TreeViewItem | |
| { | |
| id = id++, | |
| depth = parent.depth + 1, | |
| displayName = displayName, | |
| icon = icon, | |
| }; | |
| parent.AddChild(item); | |
| return item; | |
| } | |
| private List<InputDeviceDescription> m_UnsupportedDevices; | |
| private List<InputAction> m_EnabledActions = new List<InputAction>(); | |
| private class DeviceItem : TreeViewItem | |
| { | |
| public InputDevice device; | |
| } | |
| private class UnsupportedDeviceItem : TreeViewItem | |
| { | |
| public InputDeviceDescription description; | |
| } | |
| private class ConfigurationItem : TreeViewItem | |
| { | |
| public string name; | |
| } | |
| private class LayoutItem : TreeViewItem | |
| { | |
| public InternedString layoutName; | |
| } | |
| } | |
| } | |
| } | |