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