A game about forced loneliness, made by TACStudios
1#if UNITY_EDITOR 2using System; 3using System.Collections.Generic; 4using System.IO; 5using System.Linq; 6using UnityEngine.InputSystem.LowLevel; 7using UnityEditor; 8using UnityEditorInternal; 9using UnityEditor.IMGUI.Controls; 10using UnityEditor.Networking.PlayerConnection; 11using UnityEngine.InputSystem.Layouts; 12using UnityEngine.InputSystem.Users; 13using UnityEngine.InputSystem.Utilities; 14 15////FIXME: Generate proper IDs for the individual tree view items; the current sequential numbering scheme just causes lots of 16//// weird expansion/collapsing to happen. 17 18////TODO: add way to load and replay event traces 19 20////TODO: refresh metrics on demand 21 22////TODO: when an action is triggered and when a device changes state, make them bold in the list for a brief moment 23 24////TODO: show input users and their actions and devices 25 26////TODO: append " (Disabled) to disabled devices and grey them out 27 28////TODO: split 'Local' and 'Remote' at root rather than inside subnodes 29 30////TODO: refresh when unrecognized device pops up 31 32namespace UnityEngine.InputSystem.Editor 33{ 34 // Allows looking at input activity in the editor. 35 internal class InputDebuggerWindow : EditorWindow, ISerializationCallbackReceiver 36 { 37 private static int s_Disabled; 38 private static InputDebuggerWindow s_Instance; 39 40 [MenuItem("Window/Analysis/Input Debugger", false, 2100)] 41 public static void CreateOrShow() 42 { 43 if (s_Instance == null) 44 { 45 s_Instance = GetWindow<InputDebuggerWindow>(); 46 s_Instance.Show(); 47 s_Instance.titleContent = new GUIContent("Input Debug"); 48 } 49 else 50 { 51 s_Instance.Show(); 52 s_Instance.Focus(); 53 } 54 } 55 56 public static void Enable() 57 { 58 if (s_Disabled == 0) 59 return; 60 61 --s_Disabled; 62 if (s_Disabled == 0 && s_Instance != null) 63 { 64 s_Instance.InstallHooks(); 65 s_Instance.Refresh(); 66 } 67 } 68 69 public static void Disable() 70 { 71 ++s_Disabled; 72 if (s_Disabled == 1 && s_Instance != null) 73 { 74 s_Instance.UninstallHooks(); 75 s_Instance.Refresh(); 76 } 77 } 78 79 private void OnDeviceChange(InputDevice device, InputDeviceChange change) 80 { 81 // Update tree if devices are added or removed. 82 if (change == InputDeviceChange.Added || change == InputDeviceChange.Removed) 83 Refresh(); 84 } 85 86 private void OnLayoutChange(string name, InputControlLayoutChange change) 87 { 88 // Update tree if layout setup has changed. 89 Refresh(); 90 } 91 92 private void OnActionChange(object actionOrMap, InputActionChange change) 93 { 94 switch (change) 95 { 96 // When an action is triggered, we only need a repaint. 97 case InputActionChange.ActionStarted: 98 case InputActionChange.ActionPerformed: 99 case InputActionChange.ActionCanceled: 100 Repaint(); 101 break; 102 103 case InputActionChange.ActionEnabled: 104 case InputActionChange.ActionDisabled: 105 case InputActionChange.ActionMapDisabled: 106 case InputActionChange.ActionMapEnabled: 107 case InputActionChange.BoundControlsChanged: 108 Refresh(); 109 break; 110 } 111 } 112 113 private void OnSettingsChange() 114 { 115 Refresh(); 116 } 117 118 private string OnFindLayout(ref InputDeviceDescription description, string matchedLayout, 119 InputDeviceExecuteCommandDelegate executeCommandDelegate) 120 { 121 // If there's no matched layout, there's a chance this device will go in 122 // the unsupported list. There's no direct notification for that so we 123 // preemptively trigger a refresh. 124 if (string.IsNullOrEmpty(matchedLayout)) 125 Refresh(); 126 127 return null; 128 } 129 130 private void Refresh() 131 { 132 m_NeedReload = true; 133 Repaint(); 134 } 135 136 public void OnDestroy() 137 { 138 UninstallHooks(); 139 } 140 141 private void InstallHooks() 142 { 143 InputSystem.onDeviceChange += OnDeviceChange; 144 InputSystem.onLayoutChange += OnLayoutChange; 145 InputSystem.onFindLayoutForDevice += OnFindLayout; 146 InputSystem.onActionChange += OnActionChange; 147 InputSystem.onSettingsChange += OnSettingsChange; 148 } 149 150 private void UninstallHooks() 151 { 152 InputSystem.onDeviceChange -= OnDeviceChange; 153 InputSystem.onLayoutChange -= OnLayoutChange; 154 InputSystem.onFindLayoutForDevice -= OnFindLayout; 155 InputSystem.onActionChange -= OnActionChange; 156 InputSystem.onSettingsChange -= OnSettingsChange; 157 } 158 159 private void Initialize() 160 { 161 InstallHooks(); 162 163 var newTreeViewState = m_TreeViewState == null; 164 if (newTreeViewState) 165 m_TreeViewState = new TreeViewState(); 166 167 m_TreeView = new InputSystemTreeView(m_TreeViewState); 168 169 // Set default expansion states. 170 if (newTreeViewState) 171 m_TreeView.SetExpanded(m_TreeView.devicesItem.id, true); 172 173 m_Initialized = true; 174 } 175 176 public void OnGUI() 177 { 178 if (s_Disabled > 0) 179 { 180 EditorGUILayout.LabelField("Disabled"); 181 return; 182 } 183 184 // If the new backends aren't enabled, show a warning in the debugger. 185 if (!EditorPlayerSettingHelpers.newSystemBackendsEnabled) 186 { 187 EditorGUILayout.HelpBox( 188 "Platform backends for the new input system are not enabled. " + 189 "No devices and input from hardware will come through in the new input system APIs.\n\n" + 190 "To enable the backends, set 'Active Input Handling' in the player settings to either 'Input System (Preview)' " + 191 "or 'Both' and restart the editor.", MessageType.Warning); 192 } 193 194 // This also brings us back online after a domain reload. 195 if (!m_Initialized) 196 { 197 Initialize(); 198 } 199 else if (m_NeedReload) 200 { 201 m_TreeView.Reload(); 202 m_NeedReload = false; 203 } 204 205 DrawToolbarGUI(); 206 207 var rect = EditorGUILayout.GetControlRect(GUILayout.ExpandHeight(true)); 208 m_TreeView.OnGUI(rect); 209 } 210 211 private static void ResetDevice(InputDevice device, bool hard) 212 { 213 var playerUpdateType = InputDeviceDebuggerWindow.DetermineUpdateTypeToShow(device); 214 var currentUpdateType = InputState.currentUpdateType; 215 InputStateBuffers.SwitchTo(InputSystem.s_Manager.m_StateBuffers, playerUpdateType); 216 InputSystem.ResetDevice(device, alsoResetDontResetControls: hard); 217 InputStateBuffers.SwitchTo(InputSystem.s_Manager.m_StateBuffers, currentUpdateType); 218 } 219 220 private static void ToggleAddDevicesNotSupportedByProject() 221 { 222 InputEditorUserSettings.addDevicesNotSupportedByProject = 223 !InputEditorUserSettings.addDevicesNotSupportedByProject; 224 } 225 226 private void ToggleDiagnosticMode() 227 { 228 if (InputSystem.s_Manager.m_Diagnostics != null) 229 { 230 InputSystem.s_Manager.m_Diagnostics = null; 231 } 232 else 233 { 234 if (m_Diagnostics == null) 235 m_Diagnostics = new InputDiagnostics(); 236 InputSystem.s_Manager.m_Diagnostics = m_Diagnostics; 237 } 238 } 239 240 private static void ToggleTouchSimulation() 241 { 242 InputEditorUserSettings.simulateTouch = !InputEditorUserSettings.simulateTouch; 243 } 244 245 private static void EnableRemoteDevices(bool enable = true) 246 { 247 foreach (var player in EditorConnection.instance.ConnectedPlayers) 248 { 249 EditorConnection.instance.Send(enable ? RemoteInputPlayerConnection.kStartSendingMsg : RemoteInputPlayerConnection.kStopSendingMsg, new byte[0], player.playerId); 250 if (!enable) 251 InputSystem.remoting.RemoveRemoteDevices(player.playerId); 252 } 253 } 254 255 private static void DrawConnectionGUI() 256 { 257 if (GUILayout.Button("Remote Devices…", EditorStyles.toolbarDropDown)) 258 { 259 var menu = new GenericMenu(); 260 var haveRemotes = InputSystem.devices.Any(x => x.remote); 261 if (EditorConnection.instance.ConnectedPlayers.Count > 0) 262 menu.AddItem(new GUIContent("Show remote devices"), haveRemotes, () => 263 { 264 EnableRemoteDevices(!haveRemotes); 265 }); 266 else 267 menu.AddDisabledItem(new GUIContent("Show remote input devices")); 268 269 menu.AddSeparator(""); 270 271 var availableProfilers = ProfilerDriver.GetAvailableProfilers(); 272 foreach (var profiler in availableProfilers) 273 { 274 var enabled = ProfilerDriver.IsIdentifierConnectable(profiler); 275 var profilerName = ProfilerDriver.GetConnectionIdentifier(profiler); 276 var isConnected = ProfilerDriver.connectedProfiler == profiler; 277 if (enabled) 278 menu.AddItem(new GUIContent(profilerName), isConnected, () => { 279 ProfilerDriver.connectedProfiler = profiler; 280 EnableRemoteDevices(); 281 }); 282 else 283 menu.AddDisabledItem(new GUIContent(profilerName)); 284 } 285 286 foreach (var device in UnityEditor.Hardware.DevDeviceList.GetDevices()) 287 { 288 var supportsPlayerConnection = (device.features & UnityEditor.Hardware.DevDeviceFeatures.PlayerConnection) != 0; 289 if (!device.isConnected || !supportsPlayerConnection) 290 continue; 291 292 var url = "device://" + device.id; 293 var isConnected = ProfilerDriver.connectedProfiler == 0xFEEE && ProfilerDriver.directConnectionUrl == url; 294 menu.AddItem(new GUIContent(device.name), isConnected, () => { 295 ProfilerDriver.DirectURLConnect(url); 296 EnableRemoteDevices(); 297 }); 298 } 299 300 menu.ShowAsContext(); 301 } 302 } 303 304 private void DrawToolbarGUI() 305 { 306 EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); 307 308 if (GUILayout.Button(Contents.optionsContent, EditorStyles.toolbarDropDown)) 309 { 310 var menu = new GenericMenu(); 311 312 menu.AddItem(Contents.addDevicesNotSupportedByProjectContent, InputEditorUserSettings.addDevicesNotSupportedByProject, 313 ToggleAddDevicesNotSupportedByProject); 314 menu.AddItem(Contents.diagnosticsModeContent, InputSystem.s_Manager.m_Diagnostics != null, 315 ToggleDiagnosticMode); 316 menu.AddItem(Contents.touchSimulationContent, InputEditorUserSettings.simulateTouch, ToggleTouchSimulation); 317 318 // Add the inverse of "Copy Device Description" which adds a device with the description from 319 // the clipboard to the system. This is most useful for debugging and makes it very easy to 320 // have a first pass at device descriptions supplied by users. 321 try 322 { 323 var copyBuffer = EditorHelpers.GetSystemCopyBufferContents(); 324 if (!string.IsNullOrEmpty(copyBuffer) && 325 copyBuffer.StartsWith("{") && !InputDeviceDescription.FromJson(copyBuffer).empty) 326 { 327 menu.AddItem(Contents.pasteDeviceDescriptionAsDevice, false, () => 328 { 329 var description = InputDeviceDescription.FromJson(copyBuffer); 330 InputSystem.AddDevice(description); 331 }); 332 } 333 } 334 catch (ArgumentException) 335 { 336 // Catch and ignore exception if buffer doesn't actually contain an InputDeviceDescription 337 // in (proper) JSON format. 338 } 339 340 menu.ShowAsContext(); 341 } 342 343 DrawConnectionGUI(); 344 345 GUILayout.FlexibleSpace(); 346 EditorGUILayout.EndHorizontal(); 347 } 348 349 [SerializeField] private TreeViewState m_TreeViewState; 350 351 [NonSerialized] private InputDiagnostics m_Diagnostics; 352 [NonSerialized] private InputSystemTreeView m_TreeView; 353 [NonSerialized] private bool m_Initialized; 354 [NonSerialized] private bool m_NeedReload; 355 356 internal static void ReviveAfterDomainReload() 357 { 358 if (s_Instance != null) 359 { 360 // Trigger initial repaint. Will call Initialize() to install hooks and 361 // refresh tree. 362 s_Instance.Repaint(); 363 } 364 } 365 366 private static class Contents 367 { 368 public static readonly GUIContent optionsContent = new GUIContent("Options"); 369 public static readonly GUIContent touchSimulationContent = new GUIContent("Simulate Touch Input From Mouse or Pen"); 370 public static readonly GUIContent pasteDeviceDescriptionAsDevice = new GUIContent("Paste Device Description as Device"); 371 public static readonly GUIContent addDevicesNotSupportedByProjectContent = new GUIContent("Add Devices Not Listed in 'Supported Devices'"); 372 public static readonly GUIContent diagnosticsModeContent = new GUIContent("Enable Event Diagnostics"); 373 public static readonly GUIContent openDebugView = new GUIContent("Open Device Debug View"); 374 public static readonly GUIContent copyDeviceDescription = new GUIContent("Copy Device Description"); 375 public static readonly GUIContent copyLayoutAsJSON = new GUIContent("Copy Layout as JSON"); 376 public static readonly GUIContent createDeviceFromLayout = new GUIContent("Create Device from Layout"); 377 public static readonly GUIContent generateCodeFromLayout = new GUIContent("Generate Precompiled Layout"); 378 public static readonly GUIContent removeDevice = new GUIContent("Remove Device"); 379 public static readonly GUIContent enableDevice = new GUIContent("Enable Device"); 380 public static readonly GUIContent disableDevice = new GUIContent("Disable Device"); 381 public static readonly GUIContent syncDevice = new GUIContent("Try to Sync Device"); 382 public static readonly GUIContent softResetDevice = new GUIContent("Reset Device (Soft)"); 383 public static readonly GUIContent hardResetDevice = new GUIContent("Reset Device (Hard)"); 384 } 385 386 void ISerializationCallbackReceiver.OnBeforeSerialize() 387 { 388 } 389 390 void ISerializationCallbackReceiver.OnAfterDeserialize() 391 { 392 s_Instance = this; 393 } 394 395 private class InputSystemTreeView : TreeView 396 { 397 public TreeViewItem actionsItem { get; private set; } 398 public TreeViewItem devicesItem { get; private set; } 399 public TreeViewItem layoutsItem { get; private set; } 400 public TreeViewItem settingsItem { get; private set; } 401 public TreeViewItem metricsItem { get; private set; } 402 public TreeViewItem usersItem { get; private set; } 403 404 public InputSystemTreeView(TreeViewState state) 405 : base(state) 406 { 407 Reload(); 408 } 409 410 protected override void ContextClickedItem(int id) 411 { 412 var item = FindItem(id, rootItem); 413 if (item == null) 414 return; 415 416 if (item is DeviceItem deviceItem) 417 { 418 var menu = new GenericMenu(); 419 menu.AddItem(Contents.openDebugView, false, () => InputDeviceDebuggerWindow.CreateOrShowExisting(deviceItem.device)); 420 menu.AddItem(Contents.copyDeviceDescription, false, 421 () => EditorHelpers.SetSystemCopyBufferContents(deviceItem.device.description.ToJson())); 422 menu.AddItem(Contents.removeDevice, false, () => InputSystem.RemoveDevice(deviceItem.device)); 423 if (deviceItem.device.enabled) 424 menu.AddItem(Contents.disableDevice, false, () => InputSystem.DisableDevice(deviceItem.device)); 425 else 426 menu.AddItem(Contents.enableDevice, false, () => InputSystem.EnableDevice(deviceItem.device)); 427 menu.AddItem(Contents.syncDevice, false, () => InputSystem.TrySyncDevice(deviceItem.device)); 428 menu.AddItem(Contents.softResetDevice, false, () => ResetDevice(deviceItem.device, false)); 429 menu.AddItem(Contents.hardResetDevice, false, () => ResetDevice(deviceItem.device, true)); 430 menu.ShowAsContext(); 431 } 432 433 if (item is UnsupportedDeviceItem unsupportedDeviceItem) 434 { 435 var menu = new GenericMenu(); 436 menu.AddItem(Contents.copyDeviceDescription, false, 437 () => EditorHelpers.SetSystemCopyBufferContents(unsupportedDeviceItem.description.ToJson())); 438 menu.ShowAsContext(); 439 } 440 441 if (item is LayoutItem layoutItem) 442 { 443 var layout = EditorInputControlLayoutCache.TryGetLayout(layoutItem.layoutName); 444 if (layout != null) 445 { 446 var menu = new GenericMenu(); 447 menu.AddItem(Contents.copyLayoutAsJSON, false, 448 () => EditorHelpers.SetSystemCopyBufferContents(layout.ToJson())); 449 if (layout.isDeviceLayout) 450 { 451 menu.AddItem(Contents.createDeviceFromLayout, false, 452 () => InputSystem.AddDevice(layout.name)); 453 menu.AddItem(Contents.generateCodeFromLayout, false, () => 454 { 455 var fileName = EditorUtility.SaveFilePanel("Generate InputDevice Code", "", "Fast" + layoutItem.layoutName, "cs"); 456 var isInAssets = fileName.StartsWith(Application.dataPath, StringComparison.OrdinalIgnoreCase); 457 if (isInAssets) 458 fileName = "Assets/" + fileName.Substring(Application.dataPath.Length + 1); 459 if (!string.IsNullOrEmpty(fileName)) 460 { 461 var code = InputLayoutCodeGenerator.GenerateCodeFileForDeviceLayout(layoutItem.layoutName, fileName, prefix: "Fast"); 462 File.WriteAllText(fileName, code); 463 if (isInAssets) 464 AssetDatabase.Refresh(); 465 } 466 }); 467 } 468 menu.ShowAsContext(); 469 } 470 } 471 } 472 473 protected override void DoubleClickedItem(int id) 474 { 475 var item = FindItem(id, rootItem); 476 477 if (item is DeviceItem deviceItem) 478 InputDeviceDebuggerWindow.CreateOrShowExisting(deviceItem.device); 479 } 480 481 protected override TreeViewItem BuildRoot() 482 { 483 var id = 0; 484 485 var root = new TreeViewItem 486 { 487 id = id++, 488 depth = -1 489 }; 490 491 ////TODO: this will need to be improved for multi-user scenarios 492 // Actions. 493 m_EnabledActions.Clear(); 494 InputSystem.ListEnabledActions(m_EnabledActions); 495 if (m_EnabledActions.Count > 0) 496 { 497 actionsItem = AddChild(root, "", ref id); 498 AddEnabledActions(actionsItem, ref id); 499 500 if (!actionsItem.hasChildren) 501 { 502 // We are culling actions that are assigned to users so we may end up with an empty 503 // list even if we have enabled actions. If we do, remove the "Actions" item from the tree. 504 root.children.Remove(actionsItem); 505 } 506 else 507 { 508 // Update title to include action count. 509 actionsItem.displayName = $"Actions ({actionsItem.children.Count})"; 510 } 511 } 512 513 // Users. 514 var userCount = InputUser.all.Count; 515 if (userCount > 0) 516 { 517 usersItem = AddChild(root, $"Users ({userCount})", ref id); 518 foreach (var user in InputUser.all) 519 AddUser(usersItem, user, ref id); 520 } 521 522 // Devices. 523 var devices = InputSystem.devices; 524 devicesItem = AddChild(root, $"Devices ({devices.Count})", ref id); 525 var haveRemotes = devices.Any(x => x.remote); 526 TreeViewItem localDevicesNode = null; 527 if (haveRemotes) 528 { 529 // Split local and remote devices into groups. 530 531 localDevicesNode = AddChild(devicesItem, "Local", ref id); 532 AddDevices(localDevicesNode, devices, ref id); 533 534 var remoteDevicesNode = AddChild(devicesItem, "Remote", ref id); 535 foreach (var player in EditorConnection.instance.ConnectedPlayers) 536 { 537 var playerNode = AddChild(remoteDevicesNode, player.name, ref id); 538 AddDevices(playerNode, devices, ref id, player.playerId); 539 } 540 } 541 else 542 { 543 // We don't have remote devices so don't add an extra group for local devices. 544 // Put them all directly underneath the "Devices" node. 545 AddDevices(devicesItem, devices, ref id); 546 } 547 548 ////TDO: unsupported and disconnected devices should also be shown for remotes 549 550 if (m_UnsupportedDevices == null) 551 m_UnsupportedDevices = new List<InputDeviceDescription>(); 552 m_UnsupportedDevices.Clear(); 553 InputSystem.GetUnsupportedDevices(m_UnsupportedDevices); 554 if (m_UnsupportedDevices.Count > 0) 555 { 556 var parent = haveRemotes ? localDevicesNode : devicesItem; 557 var unsupportedDevicesNode = AddChild(parent, $"Unsupported ({m_UnsupportedDevices.Count})", ref id); 558 foreach (var device in m_UnsupportedDevices) 559 { 560 var item = new UnsupportedDeviceItem 561 { 562 id = id++, 563 depth = unsupportedDevicesNode.depth + 1, 564 displayName = device.ToString(), 565 description = device 566 }; 567 unsupportedDevicesNode.AddChild(item); 568 } 569 unsupportedDevicesNode.children.Sort((a, b) => 570 string.Compare(a.displayName, b.displayName, StringComparison.InvariantCulture)); 571 } 572 573 var disconnectedDevices = InputSystem.disconnectedDevices; 574 if (disconnectedDevices.Count > 0) 575 { 576 var parent = haveRemotes ? localDevicesNode : devicesItem; 577 var disconnectedDevicesNode = AddChild(parent, $"Disconnected ({disconnectedDevices.Count})", ref id); 578 foreach (var device in disconnectedDevices) 579 AddChild(disconnectedDevicesNode, device.ToString(), ref id); 580 disconnectedDevicesNode.children.Sort((a, b) => 581 string.Compare(a.displayName, b.displayName, StringComparison.InvariantCulture)); 582 } 583 584 // Layouts. 585 layoutsItem = AddChild(root, "Layouts", ref id); 586 AddControlLayouts(layoutsItem, ref id); 587 588 ////FIXME: this shows local configuration only 589 // Settings. 590 var settings = InputSystem.settings; 591 var settingsAssetPath = AssetDatabase.GetAssetPath(settings); 592 var settingsLabel = "Settings"; 593 if (!string.IsNullOrEmpty(settingsAssetPath)) 594 settingsLabel = $"Settings ({Path.GetFileName(settingsAssetPath)})"; 595 settingsItem = AddChild(root, settingsLabel, ref id); 596 AddValueItem(settingsItem, "Update Mode", settings.updateMode, ref id); 597 AddValueItem(settingsItem, "Compensate For Screen Orientation", settings.compensateForScreenOrientation, ref id); 598 AddValueItem(settingsItem, "Default Button Press Point", settings.defaultButtonPressPoint, ref id); 599 AddValueItem(settingsItem, "Default Deadzone Min", settings.defaultDeadzoneMin, ref id); 600 AddValueItem(settingsItem, "Default Deadzone Max", settings.defaultDeadzoneMax, ref id); 601 AddValueItem(settingsItem, "Default Tap Time", settings.defaultTapTime, ref id); 602 AddValueItem(settingsItem, "Default Slow Tap Time", settings.defaultSlowTapTime, ref id); 603 AddValueItem(settingsItem, "Default Hold Time", settings.defaultHoldTime, ref id); 604 if (settings.supportedDevices.Count > 0) 605 { 606 var supportedDevices = AddChild(settingsItem, "Supported Devices", ref id); 607 foreach (var item in settings.supportedDevices) 608 { 609 var icon = EditorInputControlLayoutCache.GetIconForLayout(item); 610 AddChild(supportedDevices, item, ref id, icon); 611 } 612 } 613 settingsItem.children.Sort((a, b) => string.Compare(a.displayName, b.displayName, StringComparison.InvariantCultureIgnoreCase)); 614 615 // Metrics. 616 var metrics = InputSystem.metrics; 617 metricsItem = AddChild(root, "Metrics", ref id); 618 AddChild(metricsItem, 619 "Current State Size in Bytes: " + StringHelpers.NicifyMemorySize(metrics.currentStateSizeInBytes), 620 ref id); 621 AddValueItem(metricsItem, "Current Control Count", metrics.currentControlCount, ref id); 622 AddValueItem(metricsItem, "Current Layout Count", metrics.currentLayoutCount, ref id); 623 624 return root; 625 } 626 627 private void AddUser(TreeViewItem parent, InputUser user, ref int id) 628 { 629 ////REVIEW: can we get better identification? allow associating GameObject with user? 630 var userItem = AddChild(parent, "User #" + user.index, ref id); 631 632 // Control scheme. 633 var controlScheme = user.controlScheme; 634 if (controlScheme != null) 635 AddChild(userItem, "Control Scheme: " + controlScheme, ref id); 636 637 // Paired and lost devices. 638 AddDeviceListToUser("Paired Devices", user.pairedDevices, ref id, userItem); 639 AddDeviceListToUser("Lost Devices", user.lostDevices, ref id, userItem); 640 641 // Actions. 642 var actions = user.actions; 643 if (actions != null) 644 { 645 var actionsItem = AddChild(userItem, "Actions", ref id); 646 foreach (var action in actions) 647 AddActionItem(actionsItem, action, ref id); 648 649 parent.children?.Sort((a, b) => string.Compare(a.displayName, b.displayName, StringComparison.CurrentCultureIgnoreCase)); 650 } 651 } 652 653 private void AddDeviceListToUser(string title, ReadOnlyArray<InputDevice> devices, ref int id, TreeViewItem userItem) 654 { 655 if (devices.Count == 0) 656 return; 657 658 var devicesItem = AddChild(userItem, title, ref id); 659 foreach (var device in devices) 660 { 661 Debug.Assert(device != null, title + " has a null item!"); 662 if (device == null) 663 continue; 664 665 var item = new DeviceItem 666 { 667 id = id++, 668 depth = devicesItem.depth + 1, 669 displayName = device.ToString(), 670 device = device, 671 icon = EditorInputControlLayoutCache.GetIconForLayout(device.layout), 672 }; 673 devicesItem.AddChild(item); 674 } 675 } 676 677 private static void AddDevices(TreeViewItem parent, IEnumerable<InputDevice> devices, ref int id, int participantId = InputDevice.kLocalParticipantId) 678 { 679 foreach (var device in devices) 680 { 681 if (device.m_ParticipantId != participantId) 682 continue; 683 684 var displayName = device.name; 685 if (device.usages.Count > 0) 686 displayName += " (" + string.Join(",", device.usages) + ")"; 687 688 var item = new DeviceItem 689 { 690 id = id++, 691 depth = parent.depth + 1, 692 displayName = displayName, 693 device = device, 694 icon = EditorInputControlLayoutCache.GetIconForLayout(device.layout), 695 }; 696 parent.AddChild(item); 697 } 698 699 parent.children?.Sort((a, b) => string.Compare(a.displayName, b.displayName)); 700 } 701 702 private void AddControlLayouts(TreeViewItem parent, ref int id) 703 { 704 // Split root into three different groups: 705 // 1) Control layouts 706 // 2) Device layouts that don't match specific products 707 // 3) Device layouts that match specific products 708 709 var controls = AddChild(parent, "Controls", ref id); 710 var devices = AddChild(parent, "Abstract Devices", ref id); 711 var products = AddChild(parent, "Specific Devices", ref id); 712 713 foreach (var layout in EditorInputControlLayoutCache.allControlLayouts) 714 AddControlLayoutItem(layout, controls, ref id); 715 foreach (var layout in EditorInputControlLayoutCache.allDeviceLayouts) 716 AddControlLayoutItem(layout, devices, ref id); 717 foreach (var layout in EditorInputControlLayoutCache.allProductLayouts) 718 { 719 var rootBaseLayoutName = InputControlLayout.s_Layouts.GetRootLayoutName(layout.name).ToString(); 720 var groupName = string.IsNullOrEmpty(rootBaseLayoutName) ? "Other" : rootBaseLayoutName + "s"; 721 722 var group = products.children?.FirstOrDefault(x => x.displayName == groupName); 723 if (group == null) 724 { 725 group = AddChild(products, groupName, ref id); 726 if (!string.IsNullOrEmpty(rootBaseLayoutName)) 727 group.icon = EditorInputControlLayoutCache.GetIconForLayout(rootBaseLayoutName); 728 } 729 730 AddControlLayoutItem(layout, group, ref id); 731 } 732 733 controls.children?.Sort((a, b) => string.Compare(a.displayName, b.displayName)); 734 devices.children?.Sort((a, b) => string.Compare(a.displayName, b.displayName)); 735 736 if (products.children != null) 737 { 738 products.children.Sort((a, b) => string.Compare(a.displayName, b.displayName)); 739 foreach (var productGroup in products.children) 740 productGroup.children.Sort((a, b) => string.Compare(a.displayName, b.displayName)); 741 } 742 } 743 744 private TreeViewItem AddControlLayoutItem(InputControlLayout layout, TreeViewItem parent, ref int id) 745 { 746 var item = new LayoutItem 747 { 748 parent = parent, 749 depth = parent.depth + 1, 750 id = id++, 751 displayName = layout.displayName ?? layout.name, 752 layoutName = layout.name, 753 }; 754 item.icon = EditorInputControlLayoutCache.GetIconForLayout(layout.name); 755 parent.AddChild(item); 756 757 // Header. 758 AddChild(item, "Type: " + layout.type?.Name, ref id); 759 if (!string.IsNullOrEmpty(layout.m_DisplayName)) 760 AddChild(item, "Display Name: " + layout.m_DisplayName, ref id); 761 if (!string.IsNullOrEmpty(layout.name)) 762 AddChild(item, "Name: " + layout.name, ref id); 763 var baseLayouts = StringHelpers.Join(layout.baseLayouts, ", "); 764 if (!string.IsNullOrEmpty(baseLayouts)) 765 AddChild(item, "Extends: " + baseLayouts, ref id); 766 if (layout.stateFormat != 0) 767 AddChild(item, "Format: " + layout.stateFormat, ref id); 768 if (layout.m_UpdateBeforeRender != null) 769 { 770 var value = layout.m_UpdateBeforeRender.Value ? "Update" : "Disabled"; 771 AddChild(item, "Before Render: " + value, ref id); 772 } 773 if (layout.commonUsages.Count > 0) 774 { 775 AddChild(item, 776 "Common Usages: " + 777 string.Join(", ", layout.commonUsages.Select(x => x.ToString()).ToArray()), 778 ref id); 779 } 780 if (layout.appliedOverrides.Count() > 0) 781 { 782 AddChild(item, 783 "Applied Overrides: " + 784 string.Join(", ", layout.appliedOverrides), 785 ref id); 786 } 787 788 ////TODO: find a more elegant solution than multiple "Matching Devices" parents when having multiple 789 //// matchers 790 // Device matchers. 791 foreach (var matcher in EditorInputControlLayoutCache.GetDeviceMatchers(layout.name)) 792 { 793 var node = AddChild(item, "Matching Devices", ref id); 794 foreach (var pattern in matcher.patterns) 795 AddChild(node, $"{pattern.Key} => \"{pattern.Value}\"", ref id); 796 } 797 798 // Controls. 799 if (layout.controls.Count > 0) 800 { 801 var controls = AddChild(item, "Controls", ref id); 802 foreach (var control in layout.controls) 803 AddControlItem(control, controls, ref id); 804 805 controls.children.Sort((a, b) => string.Compare(a.displayName, b.displayName)); 806 } 807 808 return item; 809 } 810 811 private void AddControlItem(InputControlLayout.ControlItem control, TreeViewItem parent, ref int id) 812 { 813 var item = AddChild(parent, control.variants.IsEmpty() ? control.name : string.Format("{0} ({1})", 814 control.name, control.variants), ref id); 815 816 if (!control.layout.IsEmpty()) 817 item.icon = EditorInputControlLayoutCache.GetIconForLayout(control.layout); 818 819 ////TODO: fully merge TreeViewItems from isModifyingExistingControl control layouts into the control they modify 820 821 ////TODO: allow clicking this field to jump to the layout 822 if (!control.layout.IsEmpty()) 823 AddChild(item, $"Layout: {control.layout}", ref id); 824 if (!control.variants.IsEmpty()) 825 AddChild(item, $"Variant: {control.variants}", ref id); 826 if (!string.IsNullOrEmpty(control.displayName)) 827 AddChild(item, $"Display Name: {control.displayName}", ref id); 828 if (!string.IsNullOrEmpty(control.shortDisplayName)) 829 AddChild(item, $"Short Display Name: {control.shortDisplayName}", ref id); 830 if (control.format != 0) 831 AddChild(item, $"Format: {control.format}", ref id); 832 if (control.offset != InputStateBlock.InvalidOffset) 833 AddChild(item, $"Offset: {control.offset}", ref id); 834 if (control.bit != InputStateBlock.InvalidOffset) 835 AddChild(item, $"Bit: {control.bit}", ref id); 836 if (control.sizeInBits != 0) 837 AddChild(item, $"Size In Bits: {control.sizeInBits}", ref id); 838 if (control.isArray) 839 AddChild(item, $"Array Size: {control.arraySize}", ref id); 840 if (!string.IsNullOrEmpty(control.useStateFrom)) 841 AddChild(item, $"Use State From: {control.useStateFrom}", ref id); 842 if (!control.defaultState.isEmpty) 843 AddChild(item, $"Default State: {control.defaultState.ToString()}", ref id); 844 if (!control.minValue.isEmpty) 845 AddChild(item, $"Min Value: {control.minValue.ToString()}", ref id); 846 if (!control.maxValue.isEmpty) 847 AddChild(item, $"Max Value: {control.maxValue.ToString()}", ref id); 848 849 if (control.usages.Count > 0) 850 AddChild(item, "Usages: " + string.Join(", ", control.usages.Select(x => x.ToString()).ToArray()), ref id); 851 if (control.aliases.Count > 0) 852 AddChild(item, "Aliases: " + string.Join(", ", control.aliases.Select(x => x.ToString()).ToArray()), ref id); 853 854 if (control.isNoisy || control.isSynthetic) 855 { 856 var flags = "Flags: "; 857 if (control.isNoisy) 858 flags += "Noisy"; 859 if (control.isSynthetic) 860 { 861 if (control.isNoisy) 862 flags += ", Synthetic"; 863 else 864 flags += "Synthetic"; 865 } 866 AddChild(item, flags, ref id); 867 } 868 869 if (control.parameters.Count > 0) 870 { 871 var parameters = AddChild(item, "Parameters", ref id); 872 foreach (var parameter in control.parameters) 873 AddChild(parameters, parameter.ToString(), ref id); 874 } 875 876 if (control.processors.Count > 0) 877 { 878 var processors = AddChild(item, "Processors", ref id); 879 foreach (var processor in control.processors) 880 { 881 var processorItem = AddChild(processors, processor.name, ref id); 882 foreach (var parameter in processor.parameters) 883 AddChild(processorItem, parameter.ToString(), ref id); 884 } 885 } 886 } 887 888 private void AddValueItem<TValue>(TreeViewItem parent, string name, TValue value, ref int id) 889 { 890 var item = new ConfigurationItem 891 { 892 id = id++, 893 depth = parent.depth + 1, 894 displayName = $"{name}: {value.ToString()}", 895 name = name 896 }; 897 parent.AddChild(item); 898 } 899 900 private void AddEnabledActions(TreeViewItem parent, ref int id) 901 { 902 foreach (var action in m_EnabledActions) 903 { 904 // If we have users, find out if the action is owned by a user. If so, don't display 905 // it separately. 906 var isOwnedByUser = false; 907 foreach (var user in InputUser.all) 908 { 909 var userActions = user.actions; 910 if (userActions != null && userActions.Contains(action)) 911 { 912 isOwnedByUser = true; 913 break; 914 } 915 } 916 917 if (!isOwnedByUser) 918 AddActionItem(parent, action, ref id); 919 } 920 921 parent.children?.Sort((a, b) => string.Compare(a.displayName, b.displayName, StringComparison.CurrentCultureIgnoreCase)); 922 } 923 924 private unsafe void AddActionItem(TreeViewItem parent, InputAction action, ref int id) 925 { 926 // Add item for action. 927 var name = action.actionMap != null ? $"{action.actionMap.name}/{action.name}" : action.name; 928 if (!action.enabled) 929 name += " (Disabled)"; 930 if (action.actionMap != null && action.actionMap.m_Asset != null) 931 { 932 name += $" ({action.actionMap.m_Asset.name})"; 933 } 934 else 935 { 936 name += " (no asset)"; 937 } 938 939 var item = AddChild(parent, name, ref id); 940 941 // Grab state. 942 var actionMap = action.GetOrCreateActionMap(); 943 actionMap.ResolveBindingsIfNecessary(); 944 var state = actionMap.m_State; 945 946 // Add list of resolved controls. 947 var actionIndex = action.m_ActionIndexInState; 948 var totalBindingCount = state.totalBindingCount; 949 for (var i = 0; i < totalBindingCount; ++i) 950 { 951 ref var bindingState = ref state.bindingStates[i]; 952 if (bindingState.actionIndex != actionIndex) 953 continue; 954 if (bindingState.isComposite) 955 continue; 956 957 var binding = state.GetBinding(i); 958 var controlCount = bindingState.controlCount; 959 var controlStartIndex = bindingState.controlStartIndex; 960 for (var n = 0; n < controlCount; ++n) 961 { 962 var control = state.controls[controlStartIndex + n]; 963 var interactions = 964 StringHelpers.Join(new[] {binding.effectiveInteractions, action.interactions}, ","); 965 966 var text = control.path; 967 if (!string.IsNullOrEmpty(interactions)) 968 { 969 var namesAndParameters = NameAndParameters.ParseMultiple(interactions); 970 text += " ["; 971 text += string.Join(",", namesAndParameters.Select(x => x.name)); 972 text += "]"; 973 } 974 975 AddChild(item, text, ref id); 976 } 977 } 978 } 979 980 private TreeViewItem AddChild(TreeViewItem parent, string displayName, ref int id, Texture2D icon = null) 981 { 982 var item = new TreeViewItem 983 { 984 id = id++, 985 depth = parent.depth + 1, 986 displayName = displayName, 987 icon = icon, 988 }; 989 parent.AddChild(item); 990 return item; 991 } 992 993 private List<InputDeviceDescription> m_UnsupportedDevices; 994 private List<InputAction> m_EnabledActions = new List<InputAction>(); 995 996 private class DeviceItem : TreeViewItem 997 { 998 public InputDevice device; 999 } 1000 1001 private class UnsupportedDeviceItem : TreeViewItem 1002 { 1003 public InputDeviceDescription description; 1004 } 1005 1006 private class ConfigurationItem : TreeViewItem 1007 { 1008 public string name; 1009 } 1010 1011 private class LayoutItem : TreeViewItem 1012 { 1013 public InternedString layoutName; 1014 } 1015 } 1016 } 1017} 1018#endif // UNITY_EDITOR