暂无描述
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

InputDebuggerWindow.cs 45KB

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