A game about forced loneliness, made by TACStudios
at master 21 kB view raw
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