123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506 |
- #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<Action<InputDevice>> s_OnToolbarGUIActions;
-
- public static event Action<InputDevice> 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<InputDeviceDebuggerWindow>();
- 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<InputStateWindow>();
- 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<string>();
- 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<InputDeviceDebuggerWindow> s_OpenDebuggerWindows;
-
- private void AddToList()
- {
- if (s_OpenDebuggerWindows == null)
- s_OpenDebuggerWindows = new List<InputDeviceDebuggerWindow>();
- 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
|