A game about forced loneliness, made by TACStudios
1#if UNITY_EDITOR
2using System;
3using System.Collections.Generic;
4using System.Linq;
5using UnityEditor;
6using UnityEditor.IMGUI.Controls;
7using UnityEngine.InputSystem.LowLevel;
8using UnityEngine.InputSystem.Utilities;
9
10////TODO: allow selecting events and saving out only the selected ones
11
12////TODO: add the ability for the debugger to just generate input on the device according to the controls it finds; good for testing
13
14////TODO: add commands to event trace (also clickable)
15
16////TODO: add diff-to-previous-event ability to event window
17
18////FIXME: the repaint triggered from IInputStateCallbackReceiver somehow comes with a significant delay
19
20////TODO: Add "Remote:" field in list that also has a button for local devices that allows to mirror them and their input
21//// into connected players
22
23////TODO: this window should help diagnose problems in the event stream (e.g. ignored state events and why they were ignored)
24
25////TODO: add toggle to that switches to displaying raw control values
26
27////TODO: allow adding visualizers (or automatically add them in cases) to control that show value over time (using InputStateHistory)
28
29////TODO: show default states of controls
30
31////TODO: provide ability to save and load event traces; also ability to record directly to a file
32////TODO: provide ability to scrub back and forth through history
33
34namespace UnityEngine.InputSystem.Editor
35{
36 // Shows status and activity of a single input device in a separate window.
37 // Can also be used to alter the state of a device by making up state events.
38 internal sealed class InputDeviceDebuggerWindow : EditorWindow, ISerializationCallbackReceiver, IDisposable
39 {
40 // ATM the debugger window is super slow and repaints are very expensive. So keep the total
41 // number of events we can fit at a relatively low size until we have fixed that problem.
42 private const int kDefaultEventTraceSizeInKB = 512;
43 private const int kMaxEventsPerTrace = 1024;
44
45 internal static InlinedArray<Action<InputDevice>> s_OnToolbarGUIActions;
46
47 public static event Action<InputDevice> onToolbarGUI
48 {
49 add => s_OnToolbarGUIActions.Append(value);
50 remove => s_OnToolbarGUIActions.Remove(value);
51 }
52
53 public static void CreateOrShowExisting(InputDevice device)
54 {
55 if (device == null)
56 throw new ArgumentNullException(nameof(device));
57
58 // See if we have an existing window for the device and if so pop it
59 // in front.
60 if (s_OpenDebuggerWindows != null)
61 {
62 for (var i = 0; i < s_OpenDebuggerWindows.Count; ++i)
63 {
64 var existingWindow = s_OpenDebuggerWindows[i];
65 if (existingWindow.m_DeviceId == device.deviceId)
66 {
67 existingWindow.Show();
68 existingWindow.Focus();
69 return;
70 }
71 }
72 }
73
74 // No, so create a new one.
75 var window = CreateInstance<InputDeviceDebuggerWindow>();
76 window.InitializeWith(device);
77 window.minSize = new Vector2(270, 300);
78 window.Show();
79 window.titleContent = new GUIContent(device.name);
80 }
81
82 internal void OnDestroy()
83 {
84 if (m_Device != null)
85 {
86 RemoveFromList();
87
88 InputSystem.onDeviceChange -= OnDeviceChange;
89 InputState.onChange -= OnDeviceStateChange;
90 InputSystem.onSettingsChange -= NeedControlValueRefresh;
91 Application.focusChanged -= OnApplicationFocusChange;
92 EditorApplication.playModeStateChanged += OnPlayModeChange;
93 }
94
95 m_EventTrace?.Dispose();
96 m_EventTrace = null;
97
98 m_ReplayController?.Dispose();
99 m_ReplayController = null;
100 }
101
102 public void Dispose()
103 {
104 m_EventTrace?.Dispose();
105 m_ReplayController?.Dispose();
106 }
107
108 internal void OnGUI()
109 {
110 // Find device again if we've gone through a domain reload.
111 if (m_Device == null)
112 {
113 m_Device = InputSystem.GetDeviceById(m_DeviceId);
114
115 if (m_Device == null)
116 {
117 EditorGUILayout.HelpBox(Styles.notFoundHelpText, MessageType.Warning);
118 return;
119 }
120
121 InitializeWith(m_Device);
122 }
123
124 ////FIXME: with ExpandHeight(false), editor still expands height for some reason....
125 EditorGUILayout.BeginVertical("OL Box", GUILayout.Height(170));// GUILayout.ExpandHeight(false));
126 EditorGUILayout.LabelField("Name", m_Device.name);
127 EditorGUILayout.LabelField("Layout", m_Device.layout);
128 EditorGUILayout.LabelField("Type", m_Device.GetType().Name);
129 if (!string.IsNullOrEmpty(m_Device.description.interfaceName))
130 EditorGUILayout.LabelField("Interface", m_Device.description.interfaceName);
131 if (!string.IsNullOrEmpty(m_Device.description.product))
132 EditorGUILayout.LabelField("Product", m_Device.description.product);
133 if (!string.IsNullOrEmpty(m_Device.description.manufacturer))
134 EditorGUILayout.LabelField("Manufacturer", m_Device.description.manufacturer);
135 if (!string.IsNullOrEmpty(m_Device.description.serial))
136 EditorGUILayout.LabelField("Serial Number", m_Device.description.serial);
137 EditorGUILayout.LabelField("Device ID", m_DeviceIdString);
138 if (!string.IsNullOrEmpty(m_DeviceUsagesString))
139 EditorGUILayout.LabelField("Usages", m_DeviceUsagesString);
140 if (!string.IsNullOrEmpty(m_DeviceFlagsString))
141 EditorGUILayout.LabelField("Flags", m_DeviceFlagsString);
142 if (m_Device is Keyboard)
143 EditorGUILayout.LabelField("Keyboard Layout", ((Keyboard)m_Device).keyboardLayout);
144 EditorGUILayout.EndVertical();
145
146 DrawControlTree();
147 DrawEventList();
148 }
149
150 private void DrawControlTree()
151 {
152 var label = m_InputUpdateTypeShownInControlTree == InputUpdateType.Editor
153 ? Contents.editorStateContent
154 : Contents.playerStateContent;
155
156 GUILayout.BeginHorizontal(EditorStyles.toolbar);
157 GUILayout.Label(label, GUILayout.MinWidth(100), GUILayout.ExpandWidth(true));
158 GUILayout.FlexibleSpace();
159
160 // Allow plugins to add toolbar buttons.
161 for (var i = 0; i < s_OnToolbarGUIActions.length; ++i)
162 s_OnToolbarGUIActions[i](m_Device);
163
164 if (GUILayout.Button(Contents.stateContent, EditorStyles.toolbarButton))
165 {
166 var window = CreateInstance<InputStateWindow>();
167 window.InitializeWithControl(m_Device);
168 window.Show();
169 }
170
171 GUILayout.EndHorizontal();
172
173 if (m_NeedControlValueRefresh)
174 {
175 RefreshControlTreeValues();
176 m_NeedControlValueRefresh = false;
177 }
178
179 if (m_Device.disabledInFrontend)
180 EditorGUILayout.HelpBox("Device is DISABLED. Control values will not receive updates. "
181 + "To force-enable the device, you can right-click it in the input debugger and use 'Enable Device'.", MessageType.Info);
182
183 var rect = EditorGUILayout.GetControlRect(GUILayout.ExpandHeight(true));
184 m_ControlTree.OnGUI(rect);
185 }
186
187 private void DrawEventList()
188 {
189 GUILayout.BeginHorizontal(EditorStyles.toolbar);
190 GUILayout.Label("Events", GUILayout.MinWidth(100), GUILayout.ExpandWidth(true));
191 GUILayout.FlexibleSpace();
192
193 if (m_ReplayController != null && !m_ReplayController.finished)
194 EditorGUILayout.LabelField("Playing...", EditorStyles.miniLabel);
195
196 // Text field to determine size of event trace.
197 var currentTraceSizeInKb = m_EventTrace.allocatedSizeInBytes / 1024;
198 var oldSizeText = currentTraceSizeInKb + " KB";
199 var newSizeText = EditorGUILayout.DelayedTextField(oldSizeText, Styles.toolbarTextField, GUILayout.Width(75));
200 if (oldSizeText != newSizeText && StringHelpers.FromNicifiedMemorySize(newSizeText, out var newSizeInBytes, defaultMultiplier: 1024))
201 m_EventTrace.Resize(newSizeInBytes);
202
203 // Button to clear event trace.
204 if (GUILayout.Button(Contents.clearContent, Styles.toolbarButton))
205 {
206 m_EventTrace.Clear();
207 m_EventTree.Reload();
208 }
209
210 // Button to disable event tracing.
211 // NOTE: We force-disable event tracing while a replay is in progress.
212 using (new EditorGUI.DisabledScope(m_ReplayController != null && !m_ReplayController.finished))
213 {
214 var eventTraceDisabledNow = GUILayout.Toggle(!m_EventTraceDisabled, Contents.pauseContent, Styles.toolbarButton);
215 if (eventTraceDisabledNow != m_EventTraceDisabled)
216 {
217 m_EventTraceDisabled = eventTraceDisabledNow;
218 if (eventTraceDisabledNow)
219 m_EventTrace.Disable();
220 else
221 m_EventTrace.Enable();
222 }
223 }
224
225 // Button to toggle recording of frame markers.
226 m_EventTrace.recordFrameMarkers =
227 GUILayout.Toggle(m_EventTrace.recordFrameMarkers, Contents.recordFramesContent, Styles.toolbarButton);
228
229 // Button to save event trace to file.
230 if (GUILayout.Button(Contents.saveContent, Styles.toolbarButton))
231 {
232 var defaultName = m_Device?.displayName + ".inputtrace";
233 var fileName = EditorUtility.SaveFilePanel("Choose where to save event trace", string.Empty, defaultName, "inputtrace");
234 if (!string.IsNullOrEmpty(fileName))
235 m_EventTrace.WriteTo(fileName);
236 }
237
238 // Button to load event trace from file.
239 if (GUILayout.Button(Contents.loadContent, Styles.toolbarButton))
240 {
241 var fileName = EditorUtility.OpenFilePanel("Choose event trace to load", string.Empty, "inputtrace");
242 if (!string.IsNullOrEmpty(fileName))
243 {
244 // If replay is in progress, stop it.
245 if (m_ReplayController != null)
246 {
247 m_ReplayController.Dispose();
248 m_ReplayController = null;
249 }
250
251 // Make sure event trace isn't recording while we're playing.
252 m_EventTrace.Disable();
253 m_EventTraceDisabled = true;
254
255 m_EventTrace.ReadFrom(fileName);
256 m_EventTree.Reload();
257
258 m_ReplayController = m_EventTrace.Replay()
259 .PlayAllFramesOneByOne()
260 .OnFinished(() =>
261 {
262 m_ReplayController.Dispose();
263 m_ReplayController = null;
264 Repaint();
265 });
266 }
267 }
268
269 GUILayout.EndHorizontal();
270
271 if (m_ReloadEventTree)
272 {
273 m_ReloadEventTree = false;
274 m_EventTree.Reload();
275 }
276
277 var rect = EditorGUILayout.GetControlRect(GUILayout.ExpandHeight(true));
278 m_EventTree.OnGUI(rect);
279 }
280
281 ////FIXME: some of the state in here doesn't get refreshed when it's changed on the device
282 private void InitializeWith(InputDevice device)
283 {
284 m_Device = device;
285 m_DeviceId = device.deviceId;
286 m_DeviceIdString = device.deviceId.ToString();
287 m_DeviceUsagesString = string.Join(", ", device.usages.Select(x => x.ToString()).ToArray());
288
289 UpdateDeviceFlags();
290
291 // Set up event trace. The default trace size of 512kb fits a ton of events and will
292 // likely bog down the UI if we try to display that many events. Instead, come up
293 // with a more reasonable sized based on the state size of the device.
294 if (m_EventTrace == null)
295 {
296 var deviceStateSize = (int)device.stateBlock.alignedSizeInBytes;
297 var traceSizeInBytes = (kDefaultEventTraceSizeInKB * 1024).AlignToMultipleOf(deviceStateSize);
298 if (traceSizeInBytes / deviceStateSize > kMaxEventsPerTrace)
299 traceSizeInBytes = kMaxEventsPerTrace * deviceStateSize;
300
301 m_EventTrace =
302 new InputEventTrace(traceSizeInBytes)
303 {
304 deviceId = device.deviceId
305 };
306 }
307
308 m_EventTrace.onEvent += _ => m_ReloadEventTree = true;
309 if (!m_EventTraceDisabled)
310 m_EventTrace.Enable();
311
312 // Set up event tree.
313 m_EventTree = InputEventTreeView.Create(m_Device, m_EventTrace, ref m_EventTreeState, ref m_EventTreeHeaderState);
314
315 // Set up control tree.
316 m_ControlTree = InputControlTreeView.Create(m_Device, 1, ref m_ControlTreeState, ref m_ControlTreeHeaderState);
317 m_ControlTree.Reload();
318 m_ControlTree.ExpandAll();
319
320 AddToList();
321
322 InputSystem.onSettingsChange += NeedControlValueRefresh;
323 InputSystem.onDeviceChange += OnDeviceChange;
324 InputState.onChange += OnDeviceStateChange;
325 Application.focusChanged += OnApplicationFocusChange;
326 EditorApplication.playModeStateChanged += OnPlayModeChange;
327 }
328
329 private void UpdateDeviceFlags()
330 {
331 var flags = new List<string>();
332 if (m_Device.native)
333 flags.Add("Native");
334 if (m_Device.remote)
335 flags.Add("Remote");
336 if (m_Device.updateBeforeRender)
337 flags.Add("UpdateBeforeRender");
338 if (m_Device.hasStateCallbacks)
339 flags.Add("HasStateCallbacks");
340 if (m_Device.hasEventMerger)
341 flags.Add("HasEventMerger");
342 if (m_Device.hasEventPreProcessor)
343 flags.Add("HasEventPreProcessor");
344 if (m_Device.disabledInFrontend)
345 flags.Add("DisabledInFrontend");
346 if (m_Device.disabledInRuntime)
347 flags.Add("DisabledInRuntime");
348 if (m_Device.disabledWhileInBackground)
349 flags.Add("DisabledWhileInBackground");
350 if (m_Device.canDeviceRunInBackground)
351 flags.Add("CanRunInBackground");
352 m_DeviceFlags = m_Device.m_DeviceFlags;
353 m_DeviceFlagsString = string.Join(", ", flags.ToArray());
354 }
355
356 private void RefreshControlTreeValues()
357 {
358 m_InputUpdateTypeShownInControlTree = DetermineUpdateTypeToShow(m_Device);
359 var currentUpdateType = InputState.currentUpdateType;
360
361 InputStateBuffers.SwitchTo(InputSystem.s_Manager.m_StateBuffers, m_InputUpdateTypeShownInControlTree);
362 m_ControlTree.RefreshControlValues();
363 InputStateBuffers.SwitchTo(InputSystem.s_Manager.m_StateBuffers, currentUpdateType);
364 }
365
366 [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "device", Justification = "Keep this for future implementation")]
367 internal static InputUpdateType DetermineUpdateTypeToShow(InputDevice device)
368 {
369 if (EditorApplication.isPlaying)
370 {
371 // In play mode, while playing, we show player state. Period.
372
373 switch (InputSystem.settings.updateMode)
374 {
375 case InputSettings.UpdateMode.ProcessEventsManually:
376 return InputUpdateType.Manual;
377
378 case InputSettings.UpdateMode.ProcessEventsInFixedUpdate:
379 return InputUpdateType.Fixed;
380
381 default:
382 return InputUpdateType.Dynamic;
383 }
384 }
385
386 // Outside of play mode, always show editor state.
387 return InputUpdateType.Editor;
388 }
389
390 // We will lose our device on domain reload and then look it back up the first
391 // time we hit a repaint after a reload. By that time, the input system should have
392 // fully come back to life as well.
393 private InputDevice m_Device;
394 private string m_DeviceIdString;
395 private string m_DeviceUsagesString;
396 private string m_DeviceFlagsString;
397 private InputDevice.DeviceFlags m_DeviceFlags;
398 private InputControlTreeView m_ControlTree;
399 private InputEventTreeView m_EventTree;
400 private bool m_NeedControlValueRefresh;
401 private bool m_ReloadEventTree;
402 private InputEventTrace.ReplayController m_ReplayController;
403 private InputEventTrace m_EventTrace;
404 private InputUpdateType m_InputUpdateTypeShownInControlTree;
405
406 [SerializeField] private int m_DeviceId = InputDevice.InvalidDeviceId;
407 [SerializeField] private TreeViewState m_ControlTreeState;
408 [SerializeField] private TreeViewState m_EventTreeState;
409 [SerializeField] private MultiColumnHeaderState m_ControlTreeHeaderState;
410 [SerializeField] private MultiColumnHeaderState m_EventTreeHeaderState;
411 [SerializeField] private bool m_EventTraceDisabled;
412
413 private static List<InputDeviceDebuggerWindow> s_OpenDebuggerWindows;
414
415 private void AddToList()
416 {
417 if (s_OpenDebuggerWindows == null)
418 s_OpenDebuggerWindows = new List<InputDeviceDebuggerWindow>();
419 if (!s_OpenDebuggerWindows.Contains(this))
420 s_OpenDebuggerWindows.Add(this);
421 }
422
423 private void RemoveFromList()
424 {
425 s_OpenDebuggerWindows?.Remove(this);
426 }
427
428 private void NeedControlValueRefresh()
429 {
430 m_NeedControlValueRefresh = true;
431 Repaint();
432 }
433
434 private void OnPlayModeChange(PlayModeStateChange change)
435 {
436 if (change == PlayModeStateChange.EnteredPlayMode || change == PlayModeStateChange.EnteredEditMode)
437 NeedControlValueRefresh();
438 }
439
440 private void OnApplicationFocusChange(bool focus)
441 {
442 NeedControlValueRefresh();
443 }
444
445 private void OnDeviceChange(InputDevice device, InputDeviceChange change)
446 {
447 if (device.deviceId != m_DeviceId)
448 return;
449
450 if (change == InputDeviceChange.Removed)
451 {
452 Close();
453 }
454 else
455 {
456 if (m_DeviceFlags != device.m_DeviceFlags)
457 UpdateDeviceFlags();
458 Repaint();
459 }
460 }
461
462 private void OnDeviceStateChange(InputDevice device, InputEventPtr eventPtr)
463 {
464 if (device == m_Device)
465 NeedControlValueRefresh();
466 }
467
468 private static class Styles
469 {
470 public static string notFoundHelpText = "Device could not be found.";
471
472 public static GUIStyle toolbarTextField;
473 public static GUIStyle toolbarButton;
474
475 static Styles()
476 {
477 toolbarTextField = new GUIStyle(EditorStyles.toolbarTextField);
478 toolbarTextField.alignment = TextAnchor.MiddleRight;
479
480 toolbarButton = new GUIStyle(EditorStyles.toolbarButton);
481 toolbarButton.alignment = TextAnchor.MiddleCenter;
482 }
483 }
484
485 private static class Contents
486 {
487 public static GUIContent clearContent = new GUIContent("Clear");
488 public static GUIContent pauseContent = new GUIContent("Pause");
489 public static GUIContent saveContent = new GUIContent("Save");
490 public static GUIContent loadContent = new GUIContent("Load");
491 public static GUIContent recordFramesContent = new GUIContent("Record Frames");
492 public static GUIContent stateContent = new GUIContent("State");
493 public static GUIContent editorStateContent = new GUIContent("Controls (Editor State)");
494 public static GUIContent playerStateContent = new GUIContent("Controls (Player State)");
495 }
496
497 void ISerializationCallbackReceiver.OnBeforeSerialize()
498 {
499 }
500
501 void ISerializationCallbackReceiver.OnAfterDeserialize()
502 {
503 AddToList();
504 }
505 }
506}
507
508#endif // UNITY_EDITOR