IndieGame/client/Packages/com.unity.inputsystem@1.7.0/InputSystem/Editor/Internal/InputEventTreeView.cs

286 lines
9.9 KiB
C#
Raw Normal View History

2024-10-11 10:12:15 +08:00
#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