UnityGame/Library/PackageCache/com.unity.inputsystem/InputSystem/Editor/Debugger/InputDeviceDebuggerWindow.cs

507 lines
20 KiB
C#
Raw Normal View History

2024-10-27 10:53:47 +03:00
#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