A game about forced loneliness, made by TACStudios
1#if UNITY_EDITOR
2using System.Collections.Generic;
3using System.Linq;
4using UnityEditor.IMGUI.Controls;
5using UnityEngine.InputSystem.LowLevel;
6using UnityEditor;
7using Unity.Profiling;
8
9////FIXME: this performs horribly; the constant rebuilding on every single event makes the debug view super slow when device is noisy
10
11////TODO: add information about which update type + update count an event came through in
12
13////TODO: add more information for each event (ideally, dump deltas that highlight control values that have changed)
14
15////TODO: add diagnostics to immediately highlight problems with events (e.g. events getting ignored because of incorrect type codes)
16
17////TODO: implement support for sorting data by different property columns (we currently always sort events by ID)
18
19namespace UnityEngine.InputSystem.Editor
20{
21 // Multi-column TreeView that shows the events in a trace.
22 internal class InputEventTreeView : TreeView
23 {
24 private readonly InputEventTrace m_EventTrace;
25 private readonly InputControl m_RootControl;
26 private static readonly ProfilerMarker k_InputEventTreeBuildRootMarker = new ProfilerMarker("InputEventTreeView.BuildRoot");
27
28 private enum ColumnId
29 {
30 Id,
31 Type,
32 Device,
33 Size,
34 Time,
35 Details,
36 COUNT
37 }
38
39 public static InputEventTreeView Create(InputDevice device, InputEventTrace eventTrace, ref TreeViewState treeState, ref MultiColumnHeaderState headerState)
40 {
41 if (treeState == null)
42 treeState = new TreeViewState();
43
44 var newHeaderState = CreateHeaderState();
45 if (headerState != null)
46 MultiColumnHeaderState.OverwriteSerializedFields(headerState, newHeaderState);
47 headerState = newHeaderState;
48
49 var header = new MultiColumnHeader(headerState);
50 return new InputEventTreeView(treeState, header, eventTrace, device);
51 }
52
53 private static MultiColumnHeaderState CreateHeaderState()
54 {
55 var columns = new MultiColumnHeaderState.Column[(int)ColumnId.COUNT];
56
57 columns[(int)ColumnId.Id] =
58 new MultiColumnHeaderState.Column
59 {
60 width = 80,
61 minWidth = 60,
62 headerContent = new GUIContent("Id"),
63 canSort = false
64 };
65 columns[(int)ColumnId.Type] =
66 new MultiColumnHeaderState.Column
67 {
68 width = 60,
69 minWidth = 60,
70 headerContent = new GUIContent("Type"),
71 canSort = false
72 };
73 columns[(int)ColumnId.Device] =
74 new MultiColumnHeaderState.Column
75 {
76 width = 80,
77 minWidth = 60,
78 headerContent = new GUIContent("Device"),
79 canSort = false
80 };
81 columns[(int)ColumnId.Size] =
82 new MultiColumnHeaderState.Column
83 {
84 width = 50,
85 minWidth = 50,
86 headerContent = new GUIContent("Size"),
87 canSort = false
88 };
89 columns[(int)ColumnId.Time] =
90 new MultiColumnHeaderState.Column
91 {
92 width = 100,
93 minWidth = 80,
94 headerContent = new GUIContent("Time"),
95 canSort = false
96 };
97
98 columns[(int)ColumnId.Details] =
99 new MultiColumnHeaderState.Column
100 {
101 width = 250,
102 minWidth = 100,
103 headerContent = new GUIContent("Details"),
104 canSort = false
105 };
106
107 return new MultiColumnHeaderState(columns);
108 }
109
110 private InputEventTreeView(TreeViewState state, MultiColumnHeader multiColumnHeader, InputEventTrace eventTrace, InputControl rootControl)
111 : base(state, multiColumnHeader)
112 {
113 m_EventTrace = eventTrace;
114 m_RootControl = rootControl;
115 Reload();
116 }
117
118 protected override void DoubleClickedItem(int id)
119 {
120 var item = FindItem(id, rootItem) as EventItem;
121 if (item == null)
122 return;
123
124 // We can only inspect state events so ignore double-clicks on other
125 // types of events.
126 var eventPtr = item.eventPtr;
127 if (!eventPtr.IsA<StateEvent>() && !eventPtr.IsA<DeltaStateEvent>())
128 return;
129
130 PopUpStateWindow(eventPtr);
131 }
132
133 ////TODO: move inspect and compare from a context menu to the toolbar of the event view
134 protected override void ContextClickedItem(int id)
135 {
136 var item = FindItem(id, rootItem) as EventItem;
137 if (item == null)
138 return;
139
140 var menu = new GenericMenu();
141
142 var selection = GetSelection();
143 if (selection.Count == 1)
144 {
145 menu.AddItem(new GUIContent("Inspect"), false, OnInspectMenuItem, id);
146 }
147 else if (selection.Count > 1)
148 {
149 menu.AddItem(new GUIContent("Compare"), false, OnCompareMenuItem, selection);
150 }
151
152 menu.ShowAsContext();
153 }
154
155 private void OnCompareMenuItem(object userData)
156 {
157 var selection = (IList<int>)userData;
158 var window = ScriptableObject.CreateInstance<InputStateWindow>();
159 window.InitializeWithEvents(selection.Select(id => ((EventItem)FindItem(id, rootItem)).eventPtr).ToArray(), m_RootControl);
160 window.Show();
161 }
162
163 private void OnInspectMenuItem(object userData)
164 {
165 var itemId = (int)userData;
166 var item = FindItem(itemId, rootItem) as EventItem;
167 if (item == null)
168 return;
169 PopUpStateWindow(item.eventPtr);
170 }
171
172 private void PopUpStateWindow(InputEventPtr eventPtr)
173 {
174 var window = ScriptableObject.CreateInstance<InputStateWindow>();
175 window.InitializeWithEvent(eventPtr, m_RootControl);
176 window.Show();
177 }
178
179 protected override TreeViewItem BuildRoot()
180 {
181 k_InputEventTreeBuildRootMarker.Begin();
182
183 var root = new TreeViewItem
184 {
185 id = 0,
186 depth = -1,
187 displayName = "Root"
188 };
189
190 var eventCount = m_EventTrace.eventCount;
191 if (eventCount == 0)
192 {
193 // TreeView doesn't allow having empty trees. Put a dummy item in here that we
194 // render without contents.
195 root.AddChild(new TreeViewItem(1));
196 }
197 else
198 {
199 var current = new InputEventPtr();
200 // Can't set List to a fixed size and then fill it from the back. So we do it
201 // the worse way... fill it in inverse order first, then reverse it :(
202 root.children = new List<TreeViewItem>((int)eventCount);
203 for (var i = 0; i < eventCount; ++i)
204 {
205 if (!m_EventTrace.GetNextEvent(ref current))
206 break;
207
208 var item = new EventItem
209 {
210 id = i + 1,
211 depth = 1,
212 displayName = current.id.ToString(),
213 eventPtr = current
214 };
215
216 root.AddChild(item);
217 }
218 root.children.Reverse();
219 }
220
221 k_InputEventTreeBuildRootMarker.End();
222 return root;
223 }
224
225 protected override void RowGUI(RowGUIArgs args)
226 {
227 // Render nothing if event list is empty.
228 if (m_EventTrace.eventCount == 0)
229 return;
230
231 var columnCount = args.GetNumVisibleColumns();
232 for (var i = 0; i < columnCount; ++i)
233 {
234 var item = (EventItem)args.item;
235 ColumnGUI(args.GetCellRect(i), item.eventPtr, args.GetColumn(i));
236 }
237 }
238
239 private unsafe void ColumnGUI(Rect cellRect, InputEventPtr eventPtr, int column)
240 {
241 CenterRectUsingSingleLineHeight(ref cellRect);
242
243 switch (column)
244 {
245 case (int)ColumnId.Id:
246 GUI.Label(cellRect, eventPtr.id.ToString());
247 break;
248 case (int)ColumnId.Type:
249 GUI.Label(cellRect, eventPtr.type.ToString());
250 break;
251 case (int)ColumnId.Device:
252 GUI.Label(cellRect, eventPtr.deviceId.ToString());
253 break;
254 case (int)ColumnId.Size:
255 GUI.Label(cellRect, eventPtr.sizeInBytes.ToString());
256 break;
257 case (int)ColumnId.Time:
258 GUI.Label(cellRect, eventPtr.time.ToString("0.0000s"));
259 break;
260 case (int)ColumnId.Details:
261 if (eventPtr.IsA<DeltaStateEvent>())
262 {
263 var deltaEventPtr = DeltaStateEvent.From(eventPtr);
264 GUI.Label(cellRect, $"Format={deltaEventPtr->stateFormat}, Offset={deltaEventPtr->stateOffset}");
265 }
266 else if (eventPtr.IsA<StateEvent>())
267 {
268 var stateEventPtr = StateEvent.From(eventPtr);
269 GUI.Label(cellRect, $"Format={stateEventPtr->stateFormat}");
270 }
271 else if (eventPtr.IsA<TextEvent>())
272 {
273 var textEventPtr = TextEvent.From(eventPtr);
274 GUI.Label(cellRect, $"Character='{(char) textEventPtr->character}'");
275 }
276 break;
277 }
278 }
279
280 private class EventItem : TreeViewItem
281 {
282 public InputEventPtr eventPtr;
283 }
284 }
285}
286#endif // UNITY_EDITOR