#if UNITY_EDITOR using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEditor.IMGUI.Controls; using UnityEngine.InputSystem.LowLevel; using UnityEngine.InputSystem.Utilities; ////TODO: allow selecting events and saving out only the selected ones ////TODO: add the ability for the debugger to just generate input on the device according to the controls it finds; good for testing ////TODO: add commands to event trace (also clickable) ////TODO: add diff-to-previous-event ability to event window ////FIXME: the repaint triggered from IInputStateCallbackReceiver somehow comes with a significant delay ////TODO: Add "Remote:" field in list that also has a button for local devices that allows to mirror them and their input //// into connected players ////TODO: this window should help diagnose problems in the event stream (e.g. ignored state events and why they were ignored) ////TODO: add toggle to that switches to displaying raw control values ////TODO: allow adding visualizers (or automatically add them in cases) to control that show value over time (using InputStateHistory) ////TODO: show default states of controls ////TODO: provide ability to save and load event traces; also ability to record directly to a file ////TODO: provide ability to scrub back and forth through history namespace UnityEngine.InputSystem.Editor { // Shows status and activity of a single input device in a separate window. // Can also be used to alter the state of a device by making up state events. internal sealed class InputDeviceDebuggerWindow : EditorWindow, ISerializationCallbackReceiver, IDisposable { // ATM the debugger window is super slow and repaints are very expensive. So keep the total // number of events we can fit at a relatively low size until we have fixed that problem. private const int kDefaultEventTraceSizeInKB = 512; private const int kMaxEventsPerTrace = 1024; internal static InlinedArray> s_OnToolbarGUIActions; public static event Action onToolbarGUI { add => s_OnToolbarGUIActions.Append(value); remove => s_OnToolbarGUIActions.Remove(value); } public static void CreateOrShowExisting(InputDevice device) { if (device == null) throw new ArgumentNullException(nameof(device)); // See if we have an existing window for the device and if so pop it // in front. if (s_OpenDebuggerWindows != null) { for (var i = 0; i < s_OpenDebuggerWindows.Count; ++i) { var existingWindow = s_OpenDebuggerWindows[i]; if (existingWindow.m_DeviceId == device.deviceId) { existingWindow.Show(); existingWindow.Focus(); return; } } } // No, so create a new one. var window = CreateInstance(); window.InitializeWith(device); window.minSize = new Vector2(270, 300); window.Show(); window.titleContent = new GUIContent(device.name); } internal void OnDestroy() { if (m_Device != null) { RemoveFromList(); InputSystem.onDeviceChange -= OnDeviceChange; InputState.onChange -= OnDeviceStateChange; InputSystem.onSettingsChange -= NeedControlValueRefresh; Application.focusChanged -= OnApplicationFocusChange; EditorApplication.playModeStateChanged += OnPlayModeChange; } m_EventTrace?.Dispose(); m_EventTrace = null; m_ReplayController?.Dispose(); m_ReplayController = null; } public void Dispose() { m_EventTrace?.Dispose(); m_ReplayController?.Dispose(); } internal void OnGUI() { // Find device again if we've gone through a domain reload. if (m_Device == null) { m_Device = InputSystem.GetDeviceById(m_DeviceId); if (m_Device == null) { EditorGUILayout.HelpBox(Styles.notFoundHelpText, MessageType.Warning); return; } InitializeWith(m_Device); } ////FIXME: with ExpandHeight(false), editor still expands height for some reason.... EditorGUILayout.BeginVertical("OL Box", GUILayout.Height(170));// GUILayout.ExpandHeight(false)); EditorGUILayout.LabelField("Name", m_Device.name); EditorGUILayout.LabelField("Layout", m_Device.layout); EditorGUILayout.LabelField("Type", m_Device.GetType().Name); if (!string.IsNullOrEmpty(m_Device.description.interfaceName)) EditorGUILayout.LabelField("Interface", m_Device.description.interfaceName); if (!string.IsNullOrEmpty(m_Device.description.product)) EditorGUILayout.LabelField("Product", m_Device.description.product); if (!string.IsNullOrEmpty(m_Device.description.manufacturer)) EditorGUILayout.LabelField("Manufacturer", m_Device.description.manufacturer); if (!string.IsNullOrEmpty(m_Device.description.serial)) EditorGUILayout.LabelField("Serial Number", m_Device.description.serial); EditorGUILayout.LabelField("Device ID", m_DeviceIdString); if (!string.IsNullOrEmpty(m_DeviceUsagesString)) EditorGUILayout.LabelField("Usages", m_DeviceUsagesString); if (!string.IsNullOrEmpty(m_DeviceFlagsString)) EditorGUILayout.LabelField("Flags", m_DeviceFlagsString); if (m_Device is Keyboard) EditorGUILayout.LabelField("Keyboard Layout", ((Keyboard)m_Device).keyboardLayout); EditorGUILayout.EndVertical(); DrawControlTree(); DrawEventList(); } private void DrawControlTree() { var label = m_InputUpdateTypeShownInControlTree == InputUpdateType.Editor ? Contents.editorStateContent : Contents.playerStateContent; GUILayout.BeginHorizontal(EditorStyles.toolbar); GUILayout.Label(label, GUILayout.MinWidth(100), GUILayout.ExpandWidth(true)); GUILayout.FlexibleSpace(); // Allow plugins to add toolbar buttons. for (var i = 0; i < s_OnToolbarGUIActions.length; ++i) s_OnToolbarGUIActions[i](m_Device); if (GUILayout.Button(Contents.stateContent, EditorStyles.toolbarButton)) { var window = CreateInstance(); window.InitializeWithControl(m_Device); window.Show(); } GUILayout.EndHorizontal(); if (m_NeedControlValueRefresh) { RefreshControlTreeValues(); m_NeedControlValueRefresh = false; } if (m_Device.disabledInFrontend) EditorGUILayout.HelpBox("Device is DISABLED. Control values will not receive updates. " + "To force-enable the device, you can right-click it in the input debugger and use 'Enable Device'.", MessageType.Info); var rect = EditorGUILayout.GetControlRect(GUILayout.ExpandHeight(true)); m_ControlTree.OnGUI(rect); } private void DrawEventList() { GUILayout.BeginHorizontal(EditorStyles.toolbar); GUILayout.Label("Events", GUILayout.MinWidth(100), GUILayout.ExpandWidth(true)); GUILayout.FlexibleSpace(); if (m_ReplayController != null && !m_ReplayController.finished) EditorGUILayout.LabelField("Playing...", EditorStyles.miniLabel); // Text field to determine size of event trace. var currentTraceSizeInKb = m_EventTrace.allocatedSizeInBytes / 1024; var oldSizeText = currentTraceSizeInKb + " KB"; var newSizeText = EditorGUILayout.DelayedTextField(oldSizeText, Styles.toolbarTextField, GUILayout.Width(75)); if (oldSizeText != newSizeText && StringHelpers.FromNicifiedMemorySize(newSizeText, out var newSizeInBytes, defaultMultiplier: 1024)) m_EventTrace.Resize(newSizeInBytes); // Button to clear event trace. if (GUILayout.Button(Contents.clearContent, Styles.toolbarButton)) { m_EventTrace.Clear(); m_EventTree.Reload(); } // Button to disable event tracing. // NOTE: We force-disable event tracing while a replay is in progress. using (new EditorGUI.DisabledScope(m_ReplayController != null && !m_ReplayController.finished)) { var eventTraceDisabledNow = GUILayout.Toggle(!m_EventTraceDisabled, Contents.pauseContent, Styles.toolbarButton); if (eventTraceDisabledNow != m_EventTraceDisabled) { m_EventTraceDisabled = eventTraceDisabledNow; if (eventTraceDisabledNow) m_EventTrace.Disable(); else m_EventTrace.Enable(); } } // Button to toggle recording of frame markers. m_EventTrace.recordFrameMarkers = GUILayout.Toggle(m_EventTrace.recordFrameMarkers, Contents.recordFramesContent, Styles.toolbarButton); // Button to save event trace to file. if (GUILayout.Button(Contents.saveContent, Styles.toolbarButton)) { var defaultName = m_Device?.displayName + ".inputtrace"; var fileName = EditorUtility.SaveFilePanel("Choose where to save event trace", string.Empty, defaultName, "inputtrace"); if (!string.IsNullOrEmpty(fileName)) m_EventTrace.WriteTo(fileName); } // Button to load event trace from file. if (GUILayout.Button(Contents.loadContent, Styles.toolbarButton)) { var fileName = EditorUtility.OpenFilePanel("Choose event trace to load", string.Empty, "inputtrace"); if (!string.IsNullOrEmpty(fileName)) { // If replay is in progress, stop it. if (m_ReplayController != null) { m_ReplayController.Dispose(); m_ReplayController = null; } // Make sure event trace isn't recording while we're playing. m_EventTrace.Disable(); m_EventTraceDisabled = true; m_EventTrace.ReadFrom(fileName); m_EventTree.Reload(); m_ReplayController = m_EventTrace.Replay() .PlayAllFramesOneByOne() .OnFinished(() => { m_ReplayController.Dispose(); m_ReplayController = null; Repaint(); }); } } GUILayout.EndHorizontal(); if (m_ReloadEventTree) { m_ReloadEventTree = false; m_EventTree.Reload(); } var rect = EditorGUILayout.GetControlRect(GUILayout.ExpandHeight(true)); m_EventTree.OnGUI(rect); } ////FIXME: some of the state in here doesn't get refreshed when it's changed on the device private void InitializeWith(InputDevice device) { m_Device = device; m_DeviceId = device.deviceId; m_DeviceIdString = device.deviceId.ToString(); m_DeviceUsagesString = string.Join(", ", device.usages.Select(x => x.ToString()).ToArray()); UpdateDeviceFlags(); // Set up event trace. The default trace size of 512kb fits a ton of events and will // likely bog down the UI if we try to display that many events. Instead, come up // with a more reasonable sized based on the state size of the device. if (m_EventTrace == null) { var deviceStateSize = (int)device.stateBlock.alignedSizeInBytes; var traceSizeInBytes = (kDefaultEventTraceSizeInKB * 1024).AlignToMultipleOf(deviceStateSize); if (traceSizeInBytes / deviceStateSize > kMaxEventsPerTrace) traceSizeInBytes = kMaxEventsPerTrace * deviceStateSize; m_EventTrace = new InputEventTrace(traceSizeInBytes) { deviceId = device.deviceId }; } m_EventTrace.onEvent += _ => m_ReloadEventTree = true; if (!m_EventTraceDisabled) m_EventTrace.Enable(); // Set up event tree. m_EventTree = InputEventTreeView.Create(m_Device, m_EventTrace, ref m_EventTreeState, ref m_EventTreeHeaderState); // Set up control tree. m_ControlTree = InputControlTreeView.Create(m_Device, 1, ref m_ControlTreeState, ref m_ControlTreeHeaderState); m_ControlTree.Reload(); m_ControlTree.ExpandAll(); AddToList(); InputSystem.onSettingsChange += NeedControlValueRefresh; InputSystem.onDeviceChange += OnDeviceChange; InputState.onChange += OnDeviceStateChange; Application.focusChanged += OnApplicationFocusChange; EditorApplication.playModeStateChanged += OnPlayModeChange; } private void UpdateDeviceFlags() { var flags = new List(); if (m_Device.native) flags.Add("Native"); if (m_Device.remote) flags.Add("Remote"); if (m_Device.updateBeforeRender) flags.Add("UpdateBeforeRender"); if (m_Device.hasStateCallbacks) flags.Add("HasStateCallbacks"); if (m_Device.hasEventMerger) flags.Add("HasEventMerger"); if (m_Device.hasEventPreProcessor) flags.Add("HasEventPreProcessor"); if (m_Device.disabledInFrontend) flags.Add("DisabledInFrontend"); if (m_Device.disabledInRuntime) flags.Add("DisabledInRuntime"); if (m_Device.disabledWhileInBackground) flags.Add("DisabledWhileInBackground"); m_DeviceFlags = m_Device.m_DeviceFlags; m_DeviceFlagsString = string.Join(", ", flags.ToArray()); } private void RefreshControlTreeValues() { m_InputUpdateTypeShownInControlTree = DetermineUpdateTypeToShow(m_Device); var currentUpdateType = InputState.currentUpdateType; InputStateBuffers.SwitchTo(InputSystem.s_Manager.m_StateBuffers, m_InputUpdateTypeShownInControlTree); m_ControlTree.RefreshControlValues(); InputStateBuffers.SwitchTo(InputSystem.s_Manager.m_StateBuffers, currentUpdateType); } [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "device", Justification = "Keep this for future implementation")] internal static InputUpdateType DetermineUpdateTypeToShow(InputDevice device) { if (EditorApplication.isPlaying) { // In play mode, while playing, we show player state. Period. switch (InputSystem.settings.updateMode) { case InputSettings.UpdateMode.ProcessEventsManually: return InputUpdateType.Manual; case InputSettings.UpdateMode.ProcessEventsInFixedUpdate: return InputUpdateType.Fixed; default: return InputUpdateType.Dynamic; } } // Outside of play mode, always show editor state. return InputUpdateType.Editor; } // We will lose our device on domain reload and then look it back up the first // time we hit a repaint after a reload. By that time, the input system should have // fully come back to life as well. private InputDevice m_Device; private string m_DeviceIdString; private string m_DeviceUsagesString; private string m_DeviceFlagsString; private InputDevice.DeviceFlags m_DeviceFlags; private InputControlTreeView m_ControlTree; private InputEventTreeView m_EventTree; private bool m_NeedControlValueRefresh; private bool m_ReloadEventTree; private InputEventTrace.ReplayController m_ReplayController; private InputEventTrace m_EventTrace; private InputUpdateType m_InputUpdateTypeShownInControlTree; [SerializeField] private int m_DeviceId = InputDevice.InvalidDeviceId; [SerializeField] private TreeViewState m_ControlTreeState; [SerializeField] private TreeViewState m_EventTreeState; [SerializeField] private MultiColumnHeaderState m_ControlTreeHeaderState; [SerializeField] private MultiColumnHeaderState m_EventTreeHeaderState; [SerializeField] private bool m_EventTraceDisabled; private static List s_OpenDebuggerWindows; private void AddToList() { if (s_OpenDebuggerWindows == null) s_OpenDebuggerWindows = new List(); if (!s_OpenDebuggerWindows.Contains(this)) s_OpenDebuggerWindows.Add(this); } private void RemoveFromList() { s_OpenDebuggerWindows?.Remove(this); } private void NeedControlValueRefresh() { m_NeedControlValueRefresh = true; Repaint(); } private void OnPlayModeChange(PlayModeStateChange change) { if (change == PlayModeStateChange.EnteredPlayMode || change == PlayModeStateChange.EnteredEditMode) NeedControlValueRefresh(); } private void OnApplicationFocusChange(bool focus) { NeedControlValueRefresh(); } private void OnDeviceChange(InputDevice device, InputDeviceChange change) { if (device.deviceId != m_DeviceId) return; if (change == InputDeviceChange.Removed) { Close(); } else { if (m_DeviceFlags != device.m_DeviceFlags) UpdateDeviceFlags(); Repaint(); } } private void OnDeviceStateChange(InputDevice device, InputEventPtr eventPtr) { if (device == m_Device) NeedControlValueRefresh(); } private static class Styles { public static string notFoundHelpText = "Device could not be found."; public static GUIStyle toolbarTextField; public static GUIStyle toolbarButton; static Styles() { toolbarTextField = new GUIStyle(EditorStyles.toolbarTextField); toolbarTextField.alignment = TextAnchor.MiddleRight; toolbarButton = new GUIStyle(EditorStyles.toolbarButton); toolbarButton.alignment = TextAnchor.MiddleCenter; } } private static class Contents { public static GUIContent clearContent = new GUIContent("Clear"); public static GUIContent pauseContent = new GUIContent("Pause"); public static GUIContent saveContent = new GUIContent("Save"); public static GUIContent loadContent = new GUIContent("Load"); public static GUIContent recordFramesContent = new GUIContent("Record Frames"); public static GUIContent stateContent = new GUIContent("State"); public static GUIContent editorStateContent = new GUIContent("Controls (Editor State)"); public static GUIContent playerStateContent = new GUIContent("Controls (Player State)"); } void ISerializationCallbackReceiver.OnBeforeSerialize() { } void ISerializationCallbackReceiver.OnAfterDeserialize() { AddToList(); } } } #endif // UNITY_EDITOR