286 lines
9.9 KiB
C#
286 lines
9.9 KiB
C#
#if UNITY_EDITOR
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using UnityEditor.IMGUI.Controls;
|
|
using UnityEngine.InputSystem.LowLevel;
|
|
using UnityEditor;
|
|
using UnityEngine.Profiling;
|
|
|
|
////FIXME: this performs horribly; the constant rebuilding on every single event makes the debug view super slow when device is noisy
|
|
|
|
////TODO: add information about which update type + update count an event came through in
|
|
|
|
////TODO: add more information for each event (ideally, dump deltas that highlight control values that have changed)
|
|
|
|
////TODO: add diagnostics to immediately highlight problems with events (e.g. events getting ignored because of incorrect type codes)
|
|
|
|
////TODO: implement support for sorting data by different property columns (we currently always sort events by ID)
|
|
|
|
namespace UnityEngine.InputSystem.Editor
|
|
{
|
|
// Multi-column TreeView that shows the events in a trace.
|
|
internal class InputEventTreeView : TreeView
|
|
{
|
|
private readonly InputEventTrace m_EventTrace;
|
|
private readonly InputControl m_RootControl;
|
|
|
|
private enum ColumnId
|
|
{
|
|
Id,
|
|
Type,
|
|
Device,
|
|
Size,
|
|
Time,
|
|
Details,
|
|
COUNT
|
|
}
|
|
|
|
public static InputEventTreeView Create(InputDevice device, InputEventTrace eventTrace, ref TreeViewState treeState, ref MultiColumnHeaderState headerState)
|
|
{
|
|
if (treeState == null)
|
|
treeState = new TreeViewState();
|
|
|
|
var newHeaderState = CreateHeaderState();
|
|
if (headerState != null)
|
|
MultiColumnHeaderState.OverwriteSerializedFields(headerState, newHeaderState);
|
|
headerState = newHeaderState;
|
|
|
|
var header = new MultiColumnHeader(headerState);
|
|
return new InputEventTreeView(treeState, header, eventTrace, device);
|
|
}
|
|
|
|
private static MultiColumnHeaderState CreateHeaderState()
|
|
{
|
|
var columns = new MultiColumnHeaderState.Column[(int)ColumnId.COUNT];
|
|
|
|
columns[(int)ColumnId.Id] =
|
|
new MultiColumnHeaderState.Column
|
|
{
|
|
width = 80,
|
|
minWidth = 60,
|
|
headerContent = new GUIContent("Id"),
|
|
canSort = false
|
|
};
|
|
columns[(int)ColumnId.Type] =
|
|
new MultiColumnHeaderState.Column
|
|
{
|
|
width = 60,
|
|
minWidth = 60,
|
|
headerContent = new GUIContent("Type"),
|
|
canSort = false
|
|
};
|
|
columns[(int)ColumnId.Device] =
|
|
new MultiColumnHeaderState.Column
|
|
{
|
|
width = 80,
|
|
minWidth = 60,
|
|
headerContent = new GUIContent("Device"),
|
|
canSort = false
|
|
};
|
|
columns[(int)ColumnId.Size] =
|
|
new MultiColumnHeaderState.Column
|
|
{
|
|
width = 50,
|
|
minWidth = 50,
|
|
headerContent = new GUIContent("Size"),
|
|
canSort = false
|
|
};
|
|
columns[(int)ColumnId.Time] =
|
|
new MultiColumnHeaderState.Column
|
|
{
|
|
width = 100,
|
|
minWidth = 80,
|
|
headerContent = new GUIContent("Time"),
|
|
canSort = false
|
|
};
|
|
|
|
columns[(int)ColumnId.Details] =
|
|
new MultiColumnHeaderState.Column
|
|
{
|
|
width = 250,
|
|
minWidth = 100,
|
|
headerContent = new GUIContent("Details"),
|
|
canSort = false
|
|
};
|
|
|
|
return new MultiColumnHeaderState(columns);
|
|
}
|
|
|
|
private InputEventTreeView(TreeViewState state, MultiColumnHeader multiColumnHeader, InputEventTrace eventTrace, InputControl rootControl)
|
|
: base(state, multiColumnHeader)
|
|
{
|
|
m_EventTrace = eventTrace;
|
|
m_RootControl = rootControl;
|
|
Reload();
|
|
}
|
|
|
|
protected override void DoubleClickedItem(int id)
|
|
{
|
|
var item = FindItem(id, rootItem) as EventItem;
|
|
if (item == null)
|
|
return;
|
|
|
|
// We can only inspect state events so ignore double-clicks on other
|
|
// types of events.
|
|
var eventPtr = item.eventPtr;
|
|
if (!eventPtr.IsA<StateEvent>() && !eventPtr.IsA<DeltaStateEvent>())
|
|
return;
|
|
|
|
PopUpStateWindow(eventPtr);
|
|
}
|
|
|
|
////TODO: move inspect and compare from a context menu to the toolbar of the event view
|
|
protected override void ContextClickedItem(int id)
|
|
{
|
|
var item = FindItem(id, rootItem) as EventItem;
|
|
if (item == null)
|
|
return;
|
|
|
|
var menu = new GenericMenu();
|
|
|
|
var selection = GetSelection();
|
|
if (selection.Count == 1)
|
|
{
|
|
menu.AddItem(new GUIContent("Inspect"), false, OnInspectMenuItem, id);
|
|
}
|
|
else if (selection.Count > 1)
|
|
{
|
|
menu.AddItem(new GUIContent("Compare"), false, OnCompareMenuItem, selection);
|
|
}
|
|
|
|
menu.ShowAsContext();
|
|
}
|
|
|
|
private void OnCompareMenuItem(object userData)
|
|
{
|
|
var selection = (IList<int>)userData;
|
|
var window = ScriptableObject.CreateInstance<InputStateWindow>();
|
|
window.InitializeWithEvents(selection.Select(id => ((EventItem)FindItem(id, rootItem)).eventPtr).ToArray(), m_RootControl);
|
|
window.Show();
|
|
}
|
|
|
|
private void OnInspectMenuItem(object userData)
|
|
{
|
|
var itemId = (int)userData;
|
|
var item = FindItem(itemId, rootItem) as EventItem;
|
|
if (item == null)
|
|
return;
|
|
PopUpStateWindow(item.eventPtr);
|
|
}
|
|
|
|
private void PopUpStateWindow(InputEventPtr eventPtr)
|
|
{
|
|
var window = ScriptableObject.CreateInstance<InputStateWindow>();
|
|
window.InitializeWithEvent(eventPtr, m_RootControl);
|
|
window.Show();
|
|
}
|
|
|
|
protected override TreeViewItem BuildRoot()
|
|
{
|
|
Profiler.BeginSample("InputEventTreeView.BuildRoot");
|
|
|
|
var root = new TreeViewItem
|
|
{
|
|
id = 0,
|
|
depth = -1,
|
|
displayName = "Root"
|
|
};
|
|
|
|
var eventCount = m_EventTrace.eventCount;
|
|
if (eventCount == 0)
|
|
{
|
|
// TreeView doesn't allow having empty trees. Put a dummy item in here that we
|
|
// render without contents.
|
|
root.AddChild(new TreeViewItem(1));
|
|
}
|
|
else
|
|
{
|
|
var current = new InputEventPtr();
|
|
// Can't set List to a fixed size and then fill it from the back. So we do it
|
|
// the worse way... fill it in inverse order first, then reverse it :(
|
|
root.children = new List<TreeViewItem>((int)eventCount);
|
|
for (var i = 0; i < eventCount; ++i)
|
|
{
|
|
if (!m_EventTrace.GetNextEvent(ref current))
|
|
break;
|
|
|
|
var item = new EventItem
|
|
{
|
|
id = i + 1,
|
|
depth = 1,
|
|
displayName = current.id.ToString(),
|
|
eventPtr = current
|
|
};
|
|
|
|
root.AddChild(item);
|
|
}
|
|
root.children.Reverse();
|
|
}
|
|
|
|
Profiler.EndSample();
|
|
return root;
|
|
}
|
|
|
|
protected override void RowGUI(RowGUIArgs args)
|
|
{
|
|
// Render nothing if event list is empty.
|
|
if (m_EventTrace.eventCount == 0)
|
|
return;
|
|
|
|
var columnCount = args.GetNumVisibleColumns();
|
|
for (var i = 0; i < columnCount; ++i)
|
|
{
|
|
var item = (EventItem)args.item;
|
|
ColumnGUI(args.GetCellRect(i), item.eventPtr, args.GetColumn(i));
|
|
}
|
|
}
|
|
|
|
private unsafe void ColumnGUI(Rect cellRect, InputEventPtr eventPtr, int column)
|
|
{
|
|
CenterRectUsingSingleLineHeight(ref cellRect);
|
|
|
|
switch (column)
|
|
{
|
|
case (int)ColumnId.Id:
|
|
GUI.Label(cellRect, eventPtr.id.ToString());
|
|
break;
|
|
case (int)ColumnId.Type:
|
|
GUI.Label(cellRect, eventPtr.type.ToString());
|
|
break;
|
|
case (int)ColumnId.Device:
|
|
GUI.Label(cellRect, eventPtr.deviceId.ToString());
|
|
break;
|
|
case (int)ColumnId.Size:
|
|
GUI.Label(cellRect, eventPtr.sizeInBytes.ToString());
|
|
break;
|
|
case (int)ColumnId.Time:
|
|
GUI.Label(cellRect, eventPtr.time.ToString("0.0000s"));
|
|
break;
|
|
case (int)ColumnId.Details:
|
|
if (eventPtr.IsA<DeltaStateEvent>())
|
|
{
|
|
var deltaEventPtr = DeltaStateEvent.From(eventPtr);
|
|
GUI.Label(cellRect, $"Format={deltaEventPtr->stateFormat}, Offset={deltaEventPtr->stateOffset}");
|
|
}
|
|
else if (eventPtr.IsA<StateEvent>())
|
|
{
|
|
var stateEventPtr = StateEvent.From(eventPtr);
|
|
GUI.Label(cellRect, $"Format={stateEventPtr->stateFormat}");
|
|
}
|
|
else if (eventPtr.IsA<TextEvent>())
|
|
{
|
|
var textEventPtr = TextEvent.From(eventPtr);
|
|
GUI.Label(cellRect, $"Character='{(char) textEventPtr->character}'");
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
private class EventItem : TreeViewItem
|
|
{
|
|
public InputEventPtr eventPtr;
|
|
}
|
|
}
|
|
}
|
|
#endif // UNITY_EDITOR
|