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