test
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5cdea6ae28e4a4b6ca0bb7506f88764c
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -0,0 +1,78 @@
|
||||
#if UNITY_EDITOR
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
internal abstract class AdvancedDropdown
|
||||
{
|
||||
protected Vector2 minimumSize { get; set; }
|
||||
protected Vector2 maximumSize { get; set; }
|
||||
|
||||
internal AdvancedDropdownWindow m_WindowInstance;
|
||||
internal AdvancedDropdownState m_State;
|
||||
internal AdvancedDropdownDataSource m_DataSource;
|
||||
internal AdvancedDropdownGUI m_Gui;
|
||||
|
||||
public AdvancedDropdown(AdvancedDropdownState state)
|
||||
{
|
||||
m_State = state;
|
||||
}
|
||||
|
||||
public void Show(Rect rect)
|
||||
{
|
||||
if (m_WindowInstance != null)
|
||||
{
|
||||
m_WindowInstance.Close();
|
||||
m_WindowInstance = null;
|
||||
}
|
||||
if (m_DataSource == null)
|
||||
{
|
||||
m_DataSource = new CallbackDataSource(BuildRoot, BuildCustomSearch);
|
||||
}
|
||||
if (m_Gui == null)
|
||||
{
|
||||
m_Gui = new AdvancedDropdownGUI();
|
||||
}
|
||||
|
||||
m_WindowInstance = ScriptableObject.CreateInstance<AdvancedDropdownWindow>();
|
||||
if (minimumSize != Vector2.zero)
|
||||
m_WindowInstance.minSize = minimumSize;
|
||||
if (maximumSize != Vector2.zero)
|
||||
m_WindowInstance.maxSize = maximumSize;
|
||||
m_WindowInstance.state = m_State;
|
||||
m_WindowInstance.dataSource = m_DataSource;
|
||||
m_WindowInstance.gui = m_Gui;
|
||||
m_WindowInstance.windowClosed +=
|
||||
w => { ItemSelected(w.GetSelectedItem()); };
|
||||
m_WindowInstance.windowDestroyed += OnDestroy;
|
||||
m_WindowInstance.Init(rect);
|
||||
}
|
||||
|
||||
public void Reload()
|
||||
{
|
||||
m_WindowInstance?.ReloadData();
|
||||
}
|
||||
|
||||
public void Repaint()
|
||||
{
|
||||
m_WindowInstance?.Repaint();
|
||||
}
|
||||
|
||||
protected abstract AdvancedDropdownItem BuildRoot();
|
||||
|
||||
protected virtual AdvancedDropdownItem BuildCustomSearch(string searchString,
|
||||
IEnumerable<AdvancedDropdownItem> elements)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
protected virtual void ItemSelected(AdvancedDropdownItem item)
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void OnDestroy()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4b0617d4946754ab980342582b1f560c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -0,0 +1,133 @@
|
||||
#if UNITY_EDITOR
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
internal abstract class AdvancedDropdownDataSource
|
||||
{
|
||||
private static readonly string kSearchHeader = L10n.Tr("Search");
|
||||
|
||||
public AdvancedDropdownItem mainTree { get; private set; }
|
||||
public AdvancedDropdownItem searchTree { get; private set; }
|
||||
public List<int> selectedIDs { get; } = new List<int>();
|
||||
|
||||
protected AdvancedDropdownItem root => mainTree;
|
||||
protected List<AdvancedDropdownItem> m_SearchableElements;
|
||||
|
||||
public void ReloadData()
|
||||
{
|
||||
mainTree = FetchData();
|
||||
}
|
||||
|
||||
protected abstract AdvancedDropdownItem FetchData();
|
||||
|
||||
public void RebuildSearch(string search)
|
||||
{
|
||||
searchTree = Search(search);
|
||||
}
|
||||
|
||||
protected bool AddMatchItem(AdvancedDropdownItem e, string name, string[] searchWords, List<AdvancedDropdownItem> matchesStart, List<AdvancedDropdownItem> matchesWithin)
|
||||
{
|
||||
var didMatchAll = true;
|
||||
var didMatchStart = false;
|
||||
|
||||
// See if we match ALL the search words.
|
||||
for (var w = 0; w < searchWords.Length; w++)
|
||||
{
|
||||
var search = searchWords[w];
|
||||
if (name.Contains(search))
|
||||
{
|
||||
// If the start of the item matches the first search word, make a note of that.
|
||||
if (w == 0 && name.StartsWith(search))
|
||||
didMatchStart = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// As soon as any word is not matched, we disregard this item.
|
||||
didMatchAll = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// We always need to match all search words.
|
||||
// If we ALSO matched the start, this item gets priority.
|
||||
if (didMatchAll)
|
||||
{
|
||||
if (didMatchStart)
|
||||
matchesStart.Add(e);
|
||||
else
|
||||
matchesWithin.Add(e);
|
||||
}
|
||||
return didMatchAll;
|
||||
}
|
||||
|
||||
protected virtual AdvancedDropdownItem PerformCustomSearch(string searchString)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
protected virtual AdvancedDropdownItem Search(string searchString)
|
||||
{
|
||||
if (m_SearchableElements == null)
|
||||
{
|
||||
BuildSearchableElements();
|
||||
}
|
||||
if (string.IsNullOrEmpty(searchString))
|
||||
return null;
|
||||
|
||||
var searchTree = PerformCustomSearch(searchString);
|
||||
if (searchTree == null)
|
||||
{
|
||||
// Support multiple search words separated by spaces.
|
||||
var searchWords = searchString.ToLower().Split(' ');
|
||||
|
||||
// We keep two lists. Matches that matches the start of an item always get first priority.
|
||||
var matchesStart = new List<AdvancedDropdownItem>();
|
||||
var matchesWithin = new List<AdvancedDropdownItem>();
|
||||
|
||||
foreach (var e in m_SearchableElements)
|
||||
{
|
||||
var name = e.searchableName.ToLower().Replace(" ", "");
|
||||
AddMatchItem(e, name, searchWords, matchesStart, matchesWithin);
|
||||
}
|
||||
|
||||
searchTree = new AdvancedDropdownItem(kSearchHeader);
|
||||
matchesStart.Sort();
|
||||
foreach (var element in matchesStart)
|
||||
{
|
||||
searchTree.AddChild(element);
|
||||
}
|
||||
matchesWithin.Sort();
|
||||
foreach (var element in matchesWithin)
|
||||
{
|
||||
searchTree.AddChild(element);
|
||||
}
|
||||
}
|
||||
|
||||
return searchTree;
|
||||
}
|
||||
|
||||
private void BuildSearchableElements()
|
||||
{
|
||||
m_SearchableElements = new List<AdvancedDropdownItem>();
|
||||
BuildSearchableElements(root);
|
||||
}
|
||||
|
||||
private void BuildSearchableElements(AdvancedDropdownItem item)
|
||||
{
|
||||
if (!item.children.Any())
|
||||
{
|
||||
if (!item.IsSeparator())
|
||||
m_SearchableElements.Add(item);
|
||||
return;
|
||||
}
|
||||
foreach (var child in item.children)
|
||||
{
|
||||
BuildSearchableElements(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // UNITY_EDITOR
|
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0fb454a124e8649bb9326261b1739032
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -0,0 +1,259 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.IMGUI.Controls;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
internal class AdvancedDropdownGUI
|
||||
{
|
||||
private static class Styles
|
||||
{
|
||||
public static readonly GUIStyle toolbarSearchField = EditorStyles.toolbarSearchField;
|
||||
public static readonly GUIStyle itemStyle = new GUIStyle("PR Label")
|
||||
.WithAlignment(TextAnchor.MiddleLeft)
|
||||
.WithPadding(new RectOffset())
|
||||
.WithMargin(new RectOffset())
|
||||
.WithFixedHeight(17);
|
||||
public static readonly GUIStyle richTextItemStyle = new GUIStyle("PR Label")
|
||||
.WithAlignment(TextAnchor.MiddleLeft)
|
||||
.WithPadding(new RectOffset())
|
||||
.WithMargin(new RectOffset())
|
||||
.WithFixedHeight(17)
|
||||
.WithRichText();
|
||||
public static readonly GUIStyle header = new GUIStyle("In BigTitle")
|
||||
.WithFont(EditorStyles.boldLabel.font)
|
||||
.WithMargin(new RectOffset())
|
||||
.WithBorder(new RectOffset(0, 0, 3, 3))
|
||||
.WithPadding(new RectOffset(6, 6, 6, 6))
|
||||
.WithContentOffset(Vector2.zero);
|
||||
public static readonly GUIStyle headerArrow = new GUIStyle()
|
||||
.WithAlignment(TextAnchor.MiddleCenter)
|
||||
.WithFontSize(20)
|
||||
.WithNormalTextColor(Color.gray);
|
||||
public static readonly GUIStyle checkMark = new GUIStyle("PR Label")
|
||||
.WithAlignment(TextAnchor.MiddleCenter)
|
||||
.WithPadding(new RectOffset())
|
||||
.WithMargin(new RectOffset())
|
||||
.WithFixedHeight(17);
|
||||
public static readonly GUIContent arrowRightContent = new GUIContent("▸");
|
||||
public static readonly GUIContent arrowLeftContent = new GUIContent("◂");
|
||||
}
|
||||
|
||||
//This should ideally match line height
|
||||
private static readonly Vector2 s_IconSize = new Vector2(13, 13);
|
||||
|
||||
internal Rect m_SearchRect;
|
||||
internal Rect m_HeaderRect;
|
||||
private bool m_FocusSet;
|
||||
|
||||
internal virtual float searchHeight => m_SearchRect.height;
|
||||
|
||||
internal virtual float headerHeight => m_HeaderRect.height;
|
||||
|
||||
internal virtual GUIStyle lineStyle => Styles.itemStyle;
|
||||
internal virtual GUIStyle richTextLineStyle => Styles.richTextItemStyle;
|
||||
internal GUIStyle headerStyle => Styles.header;
|
||||
|
||||
internal virtual Vector2 iconSize => s_IconSize;
|
||||
|
||||
internal AdvancedDropdownState state { get; set; }
|
||||
|
||||
private readonly SearchField m_SearchField = new SearchField();
|
||||
|
||||
public void Init()
|
||||
{
|
||||
m_FocusSet = false;
|
||||
}
|
||||
|
||||
private const float k_IndentPerLevel = 20f;
|
||||
|
||||
internal virtual void BeginDraw(EditorWindow window)
|
||||
{
|
||||
}
|
||||
|
||||
internal virtual void EndDraw(EditorWindow window)
|
||||
{
|
||||
}
|
||||
|
||||
internal virtual void DrawItem(AdvancedDropdownItem item, string name, Texture2D icon, bool enabled,
|
||||
bool drawArrow, bool selected, bool hasSearch, bool richText = false)
|
||||
{
|
||||
var content = new GUIContent(name, icon);
|
||||
var imgTemp = content.image;
|
||||
//we need to pretend we have an icon to calculate proper width in case
|
||||
if (content.image == null)
|
||||
content.image = Texture2D.whiteTexture;
|
||||
var style = richText ? richTextLineStyle : lineStyle;
|
||||
var rect = GUILayoutUtility.GetRect(content, style, GUILayout.ExpandWidth(true));
|
||||
content.image = imgTemp;
|
||||
|
||||
if (Event.current.type != EventType.Repaint)
|
||||
return;
|
||||
|
||||
style.Draw(rect, GUIContent.none, false, false, selected, selected);
|
||||
if (!hasSearch)
|
||||
{
|
||||
rect.x += item.indent * k_IndentPerLevel;
|
||||
rect.width -= item.indent * k_IndentPerLevel;
|
||||
}
|
||||
|
||||
var imageTemp = content.image;
|
||||
if (content.image == null)
|
||||
{
|
||||
style.Draw(rect, GUIContent.none, false, false, selected, selected);
|
||||
rect.x += iconSize.x + 1;
|
||||
rect.width -= iconSize.x + 1;
|
||||
}
|
||||
rect.x += EditorGUIUtility.standardVerticalSpacing;
|
||||
rect.width -= EditorGUIUtility.standardVerticalSpacing;
|
||||
EditorGUI.BeginDisabledGroup(!enabled);
|
||||
style.Draw(rect, content, false, false, selected, selected);
|
||||
content.image = imageTemp;
|
||||
if (drawArrow)
|
||||
{
|
||||
var size = style.lineHeight;
|
||||
var arrowRect = new Rect(rect.x + rect.width - size, rect.y, size, size);
|
||||
style.Draw(arrowRect, Styles.arrowRightContent, false, false, false, false);
|
||||
}
|
||||
EditorGUI.EndDisabledGroup();
|
||||
}
|
||||
|
||||
internal virtual void DrawHeader(AdvancedDropdownItem group, Action backButtonPressed, bool hasParent)
|
||||
{
|
||||
var content = new GUIContent(group.name, group.icon);
|
||||
m_HeaderRect = GUILayoutUtility.GetRect(content, Styles.header, GUILayout.ExpandWidth(true));
|
||||
|
||||
if (Event.current.type == EventType.Repaint)
|
||||
Styles.header.Draw(m_HeaderRect, content, false, false, false, false);
|
||||
|
||||
// Back button
|
||||
if (hasParent)
|
||||
{
|
||||
var arrowWidth = 13;
|
||||
var arrowRect = new Rect(m_HeaderRect.x, m_HeaderRect.y, arrowWidth, m_HeaderRect.height);
|
||||
if (Event.current.type == EventType.Repaint)
|
||||
Styles.headerArrow.Draw(arrowRect, Styles.arrowLeftContent, false, false, false, false);
|
||||
if (Event.current.type == EventType.MouseDown && m_HeaderRect.Contains(Event.current.mousePosition))
|
||||
{
|
||||
backButtonPressed();
|
||||
Event.current.Use();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal virtual void DrawFooter(AdvancedDropdownItem selectedItem)
|
||||
{
|
||||
}
|
||||
|
||||
internal void DrawSearchField(bool isSearchFieldDisabled, string searchString, Action<string> searchChanged)
|
||||
{
|
||||
if (!isSearchFieldDisabled && !m_FocusSet)
|
||||
{
|
||||
m_FocusSet = true;
|
||||
m_SearchField.SetFocus();
|
||||
}
|
||||
|
||||
using (new EditorGUI.DisabledScope(isSearchFieldDisabled))
|
||||
{
|
||||
var newSearch = DrawSearchFieldControl(searchString);
|
||||
|
||||
if (newSearch != searchString)
|
||||
{
|
||||
searchChanged(newSearch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal virtual string DrawSearchFieldControl(string searchString)
|
||||
{
|
||||
var paddingX = 8f;
|
||||
var paddingY = 2f;
|
||||
var rect = GUILayoutUtility.GetRect(0, 0, Styles.toolbarSearchField);
|
||||
//rect.x += paddingX;
|
||||
rect.y += paddingY + 1; // Add one for the border
|
||||
rect.height += Styles.toolbarSearchField.fixedHeight + paddingY * 3;
|
||||
rect.width -= paddingX;// * 2;
|
||||
m_SearchRect = rect;
|
||||
searchString = m_SearchField.OnToolbarGUI(m_SearchRect, searchString);
|
||||
return searchString;
|
||||
}
|
||||
|
||||
internal Rect GetAnimRect(Rect position, float anim)
|
||||
{
|
||||
// Calculate rect for animated area
|
||||
var rect = new Rect(position);
|
||||
rect.x = position.x + position.width * anim;
|
||||
rect.y += searchHeight;
|
||||
rect.height -= searchHeight;
|
||||
return rect;
|
||||
}
|
||||
|
||||
internal Vector2 CalculateContentSize(AdvancedDropdownDataSource dataSource)
|
||||
{
|
||||
var maxWidth = 0f;
|
||||
var maxHeight = 0f;
|
||||
var includeArrow = false;
|
||||
var arrowWidth = 0f;
|
||||
|
||||
foreach (var child in dataSource.mainTree.children)
|
||||
{
|
||||
var content = new GUIContent(child.name, child.icon);
|
||||
var a = lineStyle.CalcSize(content);
|
||||
a.x += iconSize.x + 1;
|
||||
|
||||
if (maxWidth < a.x)
|
||||
{
|
||||
maxWidth = a.x + 1;
|
||||
includeArrow |= child.children.Any();
|
||||
}
|
||||
if (child.IsSeparator())
|
||||
{
|
||||
maxHeight += GUIHelpers.Styles.lineSeparator.CalcHeight(content, maxWidth) + GUIHelpers.Styles.lineSeparator.margin.vertical;
|
||||
}
|
||||
else
|
||||
{
|
||||
maxHeight += lineStyle.CalcHeight(content, maxWidth);
|
||||
}
|
||||
if (arrowWidth == 0)
|
||||
{
|
||||
lineStyle.CalcMinMaxWidth(Styles.arrowRightContent, out arrowWidth, out arrowWidth);
|
||||
}
|
||||
}
|
||||
if (includeArrow)
|
||||
{
|
||||
maxWidth += arrowWidth;
|
||||
}
|
||||
return new Vector2(maxWidth, maxHeight);
|
||||
}
|
||||
|
||||
internal float GetSelectionHeight(AdvancedDropdownDataSource dataSource, Rect buttonRect)
|
||||
{
|
||||
if (state.GetSelectedIndex(dataSource.mainTree) == -1)
|
||||
return 0;
|
||||
var height = 0f;
|
||||
for (var i = 0; i < dataSource.mainTree.children.Count(); i++)
|
||||
{
|
||||
var child = dataSource.mainTree.children.ElementAt(i);
|
||||
var content = new GUIContent(child.name, child.icon);
|
||||
if (state.GetSelectedIndex(dataSource.mainTree) == i)
|
||||
{
|
||||
var diff = (lineStyle.CalcHeight(content, 0) - buttonRect.height) / 2f;
|
||||
return height + diff;
|
||||
}
|
||||
if (child.IsSeparator())
|
||||
{
|
||||
height += GUIHelpers.Styles.lineSeparator.CalcHeight(content, 0) + GUIHelpers.Styles.lineSeparator.margin.vertical;
|
||||
}
|
||||
else
|
||||
{
|
||||
height += lineStyle.CalcHeight(content, 0);
|
||||
}
|
||||
}
|
||||
return height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // UNITY_EDITOR
|
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6109a857ac56b4017b44b06a9de0f827
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -0,0 +1,75 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
internal class AdvancedDropdownItem : IComparable
|
||||
{
|
||||
internal readonly List<AdvancedDropdownItem> m_Children = new List<AdvancedDropdownItem>();
|
||||
|
||||
public string name { get; set; }
|
||||
public Texture2D icon { get; set; }
|
||||
public int id { get; set; }
|
||||
public bool enabled { get; set; } = true;
|
||||
public int indent { get; set; }
|
||||
|
||||
internal int elementIndex { get; set; } = -1;
|
||||
|
||||
public IEnumerable<AdvancedDropdownItem> children => m_Children;
|
||||
|
||||
protected string m_SearchableName;
|
||||
public virtual string searchableName => string.IsNullOrEmpty(m_SearchableName) ? name : m_SearchableName;
|
||||
|
||||
public void AddChild(AdvancedDropdownItem child)
|
||||
{
|
||||
m_Children.Add(child);
|
||||
}
|
||||
|
||||
public int GetIndexOfChild(AdvancedDropdownItem child)
|
||||
{
|
||||
return m_Children.IndexOf(child);
|
||||
}
|
||||
|
||||
static readonly AdvancedDropdownItem k_SeparatorItem = new SeparatorDropdownItem();
|
||||
|
||||
public AdvancedDropdownItem(string name)
|
||||
{
|
||||
this.name = name;
|
||||
id = name.GetHashCode();
|
||||
}
|
||||
|
||||
public virtual int CompareTo(object o)
|
||||
{
|
||||
return name.CompareTo((o as AdvancedDropdownItem).name);
|
||||
}
|
||||
|
||||
public void AddSeparator(string label = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(label))
|
||||
AddChild(k_SeparatorItem);
|
||||
else
|
||||
AddChild(new SeparatorDropdownItem(label));
|
||||
}
|
||||
|
||||
internal bool IsSeparator()
|
||||
{
|
||||
return this is SeparatorDropdownItem;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
private class SeparatorDropdownItem : AdvancedDropdownItem
|
||||
{
|
||||
public SeparatorDropdownItem(string label = "")
|
||||
: base(label)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // UNITY_EDITOR
|
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 42a7c5e6c9bc849a3a9163e719422c50
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -0,0 +1,132 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
[Serializable]
|
||||
internal class AdvancedDropdownState
|
||||
{
|
||||
[Serializable]
|
||||
private class AdvancedDropdownItemState
|
||||
{
|
||||
public AdvancedDropdownItemState(AdvancedDropdownItem item)
|
||||
{
|
||||
itemId = item.id;
|
||||
}
|
||||
|
||||
public int itemId;
|
||||
public int selectedIndex = -1;
|
||||
public Vector2 scroll;
|
||||
}
|
||||
|
||||
[SerializeField]
|
||||
private AdvancedDropdownItemState[] states = new AdvancedDropdownItemState[0];
|
||||
private AdvancedDropdownItemState m_LastSelectedState;
|
||||
|
||||
private AdvancedDropdownItemState GetStateForItem(AdvancedDropdownItem item)
|
||||
{
|
||||
if (m_LastSelectedState != null && m_LastSelectedState.itemId == item.id)
|
||||
return m_LastSelectedState;
|
||||
for (int i = 0; i < states.Length; i++)
|
||||
{
|
||||
if (states[i].itemId == item.id)
|
||||
{
|
||||
m_LastSelectedState = states[i];
|
||||
return m_LastSelectedState;
|
||||
}
|
||||
}
|
||||
Array.Resize(ref states, states.Length + 1);
|
||||
states[states.Length - 1] = new AdvancedDropdownItemState(item);
|
||||
m_LastSelectedState = states[states.Length - 1];
|
||||
return states[states.Length - 1];
|
||||
}
|
||||
|
||||
internal void MoveDownSelection(AdvancedDropdownItem item)
|
||||
{
|
||||
var state = GetStateForItem(item);
|
||||
var selectedIndex = state.selectedIndex;
|
||||
do
|
||||
{
|
||||
++selectedIndex;
|
||||
}
|
||||
while (selectedIndex < item.children.Count() && item.children.ElementAt(selectedIndex).IsSeparator());
|
||||
|
||||
if (selectedIndex >= item.children.Count())
|
||||
selectedIndex = 0;
|
||||
|
||||
if (selectedIndex < item.children.Count())
|
||||
SetSelectionOnItem(item, selectedIndex);
|
||||
}
|
||||
|
||||
internal void MoveUpSelection(AdvancedDropdownItem item)
|
||||
{
|
||||
var state = GetStateForItem(item);
|
||||
var selectedIndex = state.selectedIndex;
|
||||
do
|
||||
{
|
||||
--selectedIndex;
|
||||
}
|
||||
while (selectedIndex >= 0 && item.children.ElementAt(selectedIndex).IsSeparator());
|
||||
|
||||
if (selectedIndex < 0)
|
||||
selectedIndex = item.children.Count() - 1;
|
||||
|
||||
if (selectedIndex >= 0)
|
||||
SetSelectionOnItem(item, selectedIndex);
|
||||
}
|
||||
|
||||
internal void SetSelectionOnItem(AdvancedDropdownItem item, int selectedIndex)
|
||||
{
|
||||
var state = GetStateForItem(item);
|
||||
|
||||
if (selectedIndex < 0)
|
||||
{
|
||||
state.selectedIndex = 0;
|
||||
}
|
||||
else if (selectedIndex >= item.children.Count())
|
||||
{
|
||||
state.selectedIndex = item.children.Count() - 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
state.selectedIndex = selectedIndex;
|
||||
}
|
||||
}
|
||||
|
||||
internal void ClearSelectionOnItem(AdvancedDropdownItem item)
|
||||
{
|
||||
GetStateForItem(item).selectedIndex = -1;
|
||||
}
|
||||
|
||||
internal int GetSelectedIndex(AdvancedDropdownItem item)
|
||||
{
|
||||
return GetStateForItem(item).selectedIndex;
|
||||
}
|
||||
|
||||
internal void SetSelectedIndex(AdvancedDropdownItem item, int index)
|
||||
{
|
||||
GetStateForItem(item).selectedIndex = index;
|
||||
}
|
||||
|
||||
internal AdvancedDropdownItem GetSelectedChild(AdvancedDropdownItem item)
|
||||
{
|
||||
var index = GetSelectedIndex(item);
|
||||
if (!item.children.Any() || index < 0 || index >= item.children.Count())
|
||||
return null;
|
||||
return item.children.ElementAt(index);
|
||||
}
|
||||
|
||||
internal Vector2 GetScrollState(AdvancedDropdownItem item)
|
||||
{
|
||||
return GetStateForItem(item).scroll;
|
||||
}
|
||||
|
||||
internal void SetScrollState(AdvancedDropdownItem item, Vector2 scrollState)
|
||||
{
|
||||
GetStateForItem(item).scroll = scrollState;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // UNITY_EDITOR
|
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f61b0e35037644f37b6ac81513577c50
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c46ed0dd23ba548fd87436e8c89334e9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -0,0 +1,32 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
internal class CallbackDataSource : AdvancedDropdownDataSource
|
||||
{
|
||||
private readonly Func<AdvancedDropdownItem> m_BuildCallback;
|
||||
private readonly Func<string, IEnumerable<AdvancedDropdownItem>, AdvancedDropdownItem>
|
||||
m_SearchCallback;
|
||||
|
||||
internal CallbackDataSource(Func<AdvancedDropdownItem> buildCallback,
|
||||
Func<string, IEnumerable<AdvancedDropdownItem>, AdvancedDropdownItem> searchCallback = null)
|
||||
{
|
||||
m_BuildCallback = buildCallback;
|
||||
m_SearchCallback = searchCallback;
|
||||
}
|
||||
|
||||
protected override AdvancedDropdownItem FetchData()
|
||||
{
|
||||
return m_BuildCallback();
|
||||
}
|
||||
|
||||
protected override AdvancedDropdownItem PerformCustomSearch(string searchString)
|
||||
{
|
||||
return m_SearchCallback?.Invoke(searchString, m_SearchableElements);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // UNITY_EDITOR
|
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2fe2cf22711ac4db3b094a955603f8bd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -0,0 +1,86 @@
|
||||
#if UNITY_EDITOR
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
internal class MultiLevelDataSource : AdvancedDropdownDataSource
|
||||
{
|
||||
private string[] m_DisplayedOptions;
|
||||
internal string[] displayedOptions
|
||||
{
|
||||
set { m_DisplayedOptions = value; }
|
||||
}
|
||||
|
||||
private string m_Label = "";
|
||||
internal string label
|
||||
{
|
||||
set { m_Label = value; }
|
||||
}
|
||||
|
||||
internal MultiLevelDataSource()
|
||||
{
|
||||
}
|
||||
|
||||
public MultiLevelDataSource(string[] displayOptions)
|
||||
{
|
||||
m_DisplayedOptions = displayOptions;
|
||||
}
|
||||
|
||||
protected override AdvancedDropdownItem FetchData()
|
||||
{
|
||||
var rootGroup = new AdvancedDropdownItem(m_Label);
|
||||
m_SearchableElements = new List<AdvancedDropdownItem>();
|
||||
|
||||
for (int i = 0; i < m_DisplayedOptions.Length; i++)
|
||||
{
|
||||
var menuPath = m_DisplayedOptions[i];
|
||||
var paths = menuPath.Split('/');
|
||||
|
||||
AdvancedDropdownItem parent = rootGroup;
|
||||
for (var j = 0; j < paths.Length; j++)
|
||||
{
|
||||
var path = paths[j];
|
||||
if (j == paths.Length - 1)
|
||||
{
|
||||
var element = new MultiLevelItem(path, menuPath);
|
||||
element.elementIndex = i;
|
||||
parent.AddChild(element);
|
||||
m_SearchableElements.Add(element);
|
||||
continue;
|
||||
}
|
||||
|
||||
var groupPathId = paths[0];
|
||||
for (int k = 1; k <= j; k++)
|
||||
groupPathId += "/" + paths[k];
|
||||
|
||||
var group = parent.children.SingleOrDefault(c => ((MultiLevelItem)c).stringId == groupPathId);
|
||||
if (group == null)
|
||||
{
|
||||
group = new MultiLevelItem(path, groupPathId);
|
||||
parent.AddChild(group);
|
||||
}
|
||||
parent = group;
|
||||
}
|
||||
}
|
||||
return rootGroup;
|
||||
}
|
||||
|
||||
class MultiLevelItem : AdvancedDropdownItem
|
||||
{
|
||||
internal string stringId;
|
||||
public MultiLevelItem(string path, string menuPath) : base(path)
|
||||
{
|
||||
stringId = menuPath;
|
||||
id = menuPath.GetHashCode();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return stringId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // UNITY_EDITOR
|
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: caeab4c8826564dfda5e44a7e01a8de1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -0,0 +1,62 @@
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEngine.InputSystem.Utilities;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
internal static class BuildProviderHelpers
|
||||
{
|
||||
// Adds the given object to the list of preloaded asset if not already present and
|
||||
// returns the argument given if the object was added to the list or null if already present.
|
||||
public static Object PreProcessSinglePreloadedAsset(Object assetToPreload)
|
||||
{
|
||||
// Avoid including any null asset
|
||||
if (assetToPreload == null)
|
||||
return null;
|
||||
|
||||
// If we operate on temporary object instead of a properly persisted asset, adding that temporary asset
|
||||
// would result in preloadedAssets containing null object "{fileID: 0}". Hence we ignore these.
|
||||
if (EditorUtility.IsPersistent(assetToPreload))
|
||||
{
|
||||
// Add asset object, if it's not in there already.
|
||||
var preloadedAssets = PlayerSettings.GetPreloadedAssets();
|
||||
if (preloadedAssets != null && preloadedAssets.IndexOf(assetToPreload) == -1)
|
||||
{
|
||||
ArrayHelpers.Append(ref preloadedAssets, assetToPreload);
|
||||
PlayerSettings.SetPreloadedAssets(preloadedAssets);
|
||||
return assetToPreload;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Removes the given object from preloaded assets if present.
|
||||
// The object passed as argument if set to null by this function regardless if existing in preloaded
|
||||
// assets or not.
|
||||
public static void PostProcessSinglePreloadedAsset(ref Object assetAddedByThisProvider)
|
||||
{
|
||||
if (assetAddedByThisProvider == null)
|
||||
return;
|
||||
|
||||
// Revert back to original state by removing all object(s) from preloaded assets that was added by this processor.
|
||||
var preloadedAssets = PlayerSettings.GetPreloadedAssets();
|
||||
while (preloadedAssets != null && preloadedAssets.Length > 0)
|
||||
{
|
||||
var index = Array.IndexOf(preloadedAssets, assetAddedByThisProvider);
|
||||
if (index != -1)
|
||||
{
|
||||
ArrayHelpers.EraseAt(ref preloadedAssets, index);
|
||||
PlayerSettings.SetPreloadedAssets(preloadedAssets);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
assetAddedByThisProvider = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // UNITY_EDITOR
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ed303b5629a047fa8c081261bb1993d1
|
||||
timeCreated: 1709546908
|
@@ -0,0 +1,141 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using UnityEditor;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
internal static class EditorHelpers
|
||||
{
|
||||
// Provides an abstraction layer on top of EditorGUIUtility to allow replacing the underlying buffer.
|
||||
public static Action<string> SetSystemCopyBufferContents = s => EditorGUIUtility.systemCopyBuffer = s;
|
||||
|
||||
// Provides an abstraction layer on top of EditorGUIUtility to allow replacing the underlying buffer.
|
||||
public static Func<string> GetSystemCopyBufferContents = () => EditorGUIUtility.systemCopyBuffer;
|
||||
|
||||
// Attempts to retrieve the asset GUID associated with the given asset. If asset is null or the asset
|
||||
// is not associated with a GUID or the operation fails for any other reason the return value will be null.
|
||||
public static string GetAssetGUID(Object asset)
|
||||
{
|
||||
return !AssetDatabase.TryGetGUIDAndLocalFileIdentifier(asset, out var assetGuid, out long _)
|
||||
? null : assetGuid;
|
||||
}
|
||||
|
||||
// SerializedProperty.tooltip *should* give us the tooltip as per [Tooltip] attribute. Alas, for some
|
||||
// reason, it's not happening.
|
||||
public static string GetTooltip(this SerializedProperty property)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(property.tooltip))
|
||||
return property.tooltip;
|
||||
|
||||
var field = property.GetField();
|
||||
if (field != null)
|
||||
{
|
||||
var tooltipAttribute = field.GetCustomAttribute<TooltipAttribute>();
|
||||
if (tooltipAttribute != null)
|
||||
return tooltipAttribute.tooltip;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public static string GetHyperlink(string text, string path)
|
||||
{
|
||||
return "<a href=\"" + path + $"\">{text}</a>";
|
||||
}
|
||||
|
||||
public static string GetHyperlink(string path)
|
||||
{
|
||||
return GetHyperlink(path, path);
|
||||
}
|
||||
|
||||
public static void RestartEditorAndRecompileScripts(bool dryRun = false)
|
||||
{
|
||||
// The API here are not public. Use reflection to get to them.
|
||||
var editorApplicationType = typeof(EditorApplication);
|
||||
var restartEditorAndRecompileScripts =
|
||||
editorApplicationType.GetMethod("RestartEditorAndRecompileScripts",
|
||||
BindingFlags.NonPublic | BindingFlags.Static);
|
||||
if (!dryRun)
|
||||
restartEditorAndRecompileScripts.Invoke(null, null);
|
||||
else if (restartEditorAndRecompileScripts == null)
|
||||
throw new MissingMethodException(editorApplicationType.FullName, "RestartEditorAndRecompileScripts");
|
||||
}
|
||||
|
||||
// Attempts to make an asset editable in the underlying version control system and returns true if successful.
|
||||
public static bool CheckOut(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
|
||||
// Make path relative to project folder.
|
||||
var projectPath = Application.dataPath;
|
||||
if (path.StartsWith(projectPath) && path.Length > projectPath.Length &&
|
||||
(path[projectPath.Length] == '/' || path[projectPath.Length] == '\\'))
|
||||
path = path.Substring(0, projectPath.Length + 1);
|
||||
|
||||
return AssetDatabase.MakeEditable(path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to checkout an asset for editing at the given path and overwrite its file content with
|
||||
/// the given asset text content.
|
||||
/// </summary>
|
||||
/// <param name="assetPath">Path to asset to be checkout out and overwritten.</param>
|
||||
/// <param name="text">The new file content.</param>
|
||||
/// <returns>true if the file was successfully checkout for editing and the file was written.
|
||||
/// This function may return false if unable to checkout the file for editing in the underlying
|
||||
/// version control system.</returns>
|
||||
internal static bool WriteAsset(string assetPath, string text)
|
||||
{
|
||||
// Attempt to checkout the file path for editing and inform the user if this fails.
|
||||
if (!CheckOut(assetPath))
|
||||
return false;
|
||||
|
||||
// (Over)write file text content.
|
||||
File.WriteAllText(GetPhysicalPath(assetPath), text);
|
||||
|
||||
// Reimport the asset (indirectly triggers ADB notification callbacks)
|
||||
AssetDatabase.ImportAsset(assetPath);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves an asset to the given <c>assetPath</c> with file content corresponding to <c>text</c>
|
||||
/// if the current content of the asset given by <c>assetPath</c> is different or the asset do not exist.
|
||||
/// </summary>
|
||||
/// <param name="assetPath">Destination asset path.</param>
|
||||
/// <param name="text">The new desired text content to be written to the asset.</param>
|
||||
/// <returns><c>true</c> if the asset was successfully modified or created, else <c>false</c>.</returns>
|
||||
internal static bool SaveAsset(string assetPath, string text)
|
||||
{
|
||||
var existingJson = File.Exists(assetPath) ? File.ReadAllText(assetPath) : string.Empty;
|
||||
|
||||
// Return immediately if file content has not changed, i.e. touching the file would not yield a difference.
|
||||
if (text == existingJson)
|
||||
return false;
|
||||
|
||||
// Attempt to write asset to disc (including checkout the file) and inform the user if this fails.
|
||||
if (WriteAsset(assetPath, text))
|
||||
return true;
|
||||
|
||||
Debug.LogError($"Unable save asset to \"{assetPath}\" since the asset-path could not be checked-out as editable in the underlying version-control system.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Maps path into a physical path.
|
||||
public static string GetPhysicalPath(string path)
|
||||
{
|
||||
// Note that we can only get physical path for 2021.2 or newer
|
||||
#if UNITY_2021_2_OR_NEWER
|
||||
return FileUtil.GetPhysicalPath(path);
|
||||
#else
|
||||
return path;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // UNITY_EDITOR
|
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6d671947d51444fb9b8a4bf96c16bfed
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -0,0 +1,133 @@
|
||||
#if UNITY_EDITOR
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
internal static class GUIHelpers
|
||||
{
|
||||
public static class Styles
|
||||
{
|
||||
public static readonly GUIStyle lineSeparator = new GUIStyle().WithFixedHeight(1).WithMargin(new RectOffset(0, 0, 2, 2));
|
||||
}
|
||||
|
||||
private const string kIconPath = "Packages/com.unity.inputsystem/InputSystem/Editor/Icons/";
|
||||
|
||||
public static void DrawLineSeparator(string label = null)
|
||||
{
|
||||
var hasLabel = !string.IsNullOrEmpty(label);
|
||||
EditorGUILayout.BeginVertical();
|
||||
var rect = GUILayoutUtility.GetRect(GUIContent.none, Styles.lineSeparator, GUILayout.ExpandWidth(true));
|
||||
var labelRect = new Rect();
|
||||
GUIContent labelContent = null;
|
||||
if (hasLabel)
|
||||
{
|
||||
labelContent = new GUIContent(label);
|
||||
labelRect = GUILayoutUtility.GetRect(labelContent, EditorStyles.miniLabel, GUILayout.ExpandWidth(true));
|
||||
}
|
||||
EditorGUILayout.EndVertical();
|
||||
|
||||
if (Event.current.type != EventType.Repaint)
|
||||
return;
|
||||
|
||||
var orgColor = GUI.color;
|
||||
var tintColor = EditorGUIUtility.isProSkin ? new Color(0.12f, 0.12f, 0.12f, 1.333f) : new Color(0.6f, 0.6f, 0.6f, 1.333f);
|
||||
GUI.color = GUI.color * tintColor;
|
||||
GUI.DrawTexture(rect, EditorGUIUtility.whiteTexture);
|
||||
GUI.color = orgColor;
|
||||
|
||||
if (hasLabel)
|
||||
EditorGUI.LabelField(labelRect, labelContent, EditorStyles.miniLabel);
|
||||
}
|
||||
|
||||
public static Texture2D LoadIcon(string name)
|
||||
{
|
||||
var skinPrefix = EditorGUIUtility.isProSkin ? "d_" : "";
|
||||
var scale = Mathf.Clamp((int)EditorGUIUtility.pixelsPerPoint, 0, 4);
|
||||
var scalePostFix = scale > 1 ? $"@{scale}x" : "";
|
||||
if (name.IndexOfAny(Path.GetInvalidFileNameChars()) > -1)
|
||||
name = string.Join("_", name.Split(Path.GetInvalidFileNameChars()));
|
||||
var path = Path.Combine(kIconPath, skinPrefix + name + scalePostFix + ".png");
|
||||
return AssetDatabase.LoadAssetAtPath<Texture2D>(path);
|
||||
}
|
||||
|
||||
public static GUIStyle WithNormalBackground(this GUIStyle style, Texture2D background)
|
||||
{
|
||||
style.normal.background = background;
|
||||
return style;
|
||||
}
|
||||
|
||||
public static GUIStyle WithFontSize(this GUIStyle style, int fontSize)
|
||||
{
|
||||
style.fontSize = fontSize;
|
||||
return style;
|
||||
}
|
||||
|
||||
public static GUIStyle WithFontStyle(this GUIStyle style, FontStyle fontStyle)
|
||||
{
|
||||
style.fontStyle = fontStyle;
|
||||
return style;
|
||||
}
|
||||
|
||||
public static GUIStyle WithAlignment(this GUIStyle style, TextAnchor alignment)
|
||||
{
|
||||
style.alignment = alignment;
|
||||
return style;
|
||||
}
|
||||
|
||||
public static GUIStyle WithMargin(this GUIStyle style, RectOffset margin)
|
||||
{
|
||||
style.margin = margin;
|
||||
return style;
|
||||
}
|
||||
|
||||
public static GUIStyle WithBorder(this GUIStyle style, RectOffset border)
|
||||
{
|
||||
style.border = border;
|
||||
return style;
|
||||
}
|
||||
|
||||
public static GUIStyle WithPadding(this GUIStyle style, RectOffset padding)
|
||||
{
|
||||
style.padding = padding;
|
||||
return style;
|
||||
}
|
||||
|
||||
public static GUIStyle WithFixedWidth(this GUIStyle style, int fixedWidth)
|
||||
{
|
||||
style.fixedWidth = fixedWidth;
|
||||
return style;
|
||||
}
|
||||
|
||||
public static GUIStyle WithFixedHeight(this GUIStyle style, int fixedHeight)
|
||||
{
|
||||
style.fixedHeight = fixedHeight;
|
||||
return style;
|
||||
}
|
||||
|
||||
public static GUIStyle WithRichText(this GUIStyle style, bool richText = true)
|
||||
{
|
||||
style.richText = richText;
|
||||
return style;
|
||||
}
|
||||
|
||||
public static GUIStyle WithFont(this GUIStyle style, Font font)
|
||||
{
|
||||
style.font = font;
|
||||
return style;
|
||||
}
|
||||
|
||||
public static GUIStyle WithContentOffset(this GUIStyle style, Vector2 contentOffset)
|
||||
{
|
||||
style.contentOffset = contentOffset;
|
||||
return style;
|
||||
}
|
||||
|
||||
public static GUIStyle WithNormalTextColor(this GUIStyle style, Color textColor)
|
||||
{
|
||||
style.normal.textColor = textColor;
|
||||
return style;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // UNITY_EDITOR
|
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 54ee1a1058dfd4e089fe94a8e880938e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7c93968c9be74a1fa43a3dad5eed4cbd
|
||||
timeCreated: 1509841821
|
@@ -0,0 +1,346 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor.IMGUI.Controls;
|
||||
using UnityEngine.InputSystem.LowLevel;
|
||||
using Unity.Profiling;
|
||||
|
||||
////TODO: make control values editable (create state events from UI and pump them into the system)
|
||||
|
||||
////TODO: show processors attached to controls
|
||||
|
||||
////TODO: make controls that have different `value` and `previous` in bold
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
// Multi-column TreeView that shows control tree of device.
|
||||
internal class InputControlTreeView : TreeView
|
||||
{
|
||||
// If this is set, the controls won't display their current value but we'll
|
||||
// show their state data from this buffer instead.
|
||||
public byte[] stateBuffer;
|
||||
public byte[][] multipleStateBuffers;
|
||||
public bool showDifferentOnly;
|
||||
|
||||
static readonly ProfilerMarker k_InputBuildControlTreeMarker = new ProfilerMarker("BuildControlTree");
|
||||
|
||||
public static InputControlTreeView Create(InputControl rootControl, int numValueColumns, ref TreeViewState treeState, ref MultiColumnHeaderState headerState)
|
||||
{
|
||||
if (treeState == null)
|
||||
treeState = new TreeViewState();
|
||||
|
||||
var newHeaderState = CreateHeaderState(numValueColumns);
|
||||
if (headerState != null)
|
||||
MultiColumnHeaderState.OverwriteSerializedFields(headerState, newHeaderState);
|
||||
headerState = newHeaderState;
|
||||
|
||||
var header = new MultiColumnHeader(headerState);
|
||||
return new InputControlTreeView(rootControl, treeState, header);
|
||||
}
|
||||
|
||||
public void RefreshControlValues()
|
||||
{
|
||||
foreach (var item in GetRows())
|
||||
if (item is ControlItem controlItem)
|
||||
ReadState(controlItem.control, ref controlItem);
|
||||
}
|
||||
|
||||
private const float kRowHeight = 20f;
|
||||
|
||||
private enum ColumnId
|
||||
{
|
||||
Name,
|
||||
DisplayName,
|
||||
Layout,
|
||||
Type,
|
||||
Format,
|
||||
Offset,
|
||||
Bit,
|
||||
Size,
|
||||
Optimized,
|
||||
Value,
|
||||
|
||||
COUNT
|
||||
}
|
||||
|
||||
private InputControl m_RootControl;
|
||||
|
||||
private static MultiColumnHeaderState CreateHeaderState(int numValueColumns)
|
||||
{
|
||||
var columns = new MultiColumnHeaderState.Column[(int)ColumnId.COUNT + numValueColumns - 1];
|
||||
|
||||
columns[(int)ColumnId.Name] =
|
||||
new MultiColumnHeaderState.Column
|
||||
{
|
||||
width = 180,
|
||||
minWidth = 60,
|
||||
headerContent = new GUIContent("Name")
|
||||
};
|
||||
columns[(int)ColumnId.DisplayName] =
|
||||
new MultiColumnHeaderState.Column
|
||||
{
|
||||
width = 160,
|
||||
minWidth = 60,
|
||||
headerContent = new GUIContent("Display Name")
|
||||
};
|
||||
columns[(int)ColumnId.Layout] =
|
||||
new MultiColumnHeaderState.Column
|
||||
{
|
||||
width = 100,
|
||||
minWidth = 60,
|
||||
headerContent = new GUIContent("Layout")
|
||||
};
|
||||
columns[(int)ColumnId.Type] =
|
||||
new MultiColumnHeaderState.Column
|
||||
{
|
||||
width = 100,
|
||||
minWidth = 60,
|
||||
headerContent = new GUIContent("Type")
|
||||
};
|
||||
columns[(int)ColumnId.Format] =
|
||||
new MultiColumnHeaderState.Column {headerContent = new GUIContent("Format")};
|
||||
columns[(int)ColumnId.Offset] =
|
||||
new MultiColumnHeaderState.Column {headerContent = new GUIContent("Offset")};
|
||||
columns[(int)ColumnId.Bit] =
|
||||
new MultiColumnHeaderState.Column {width = 40, headerContent = new GUIContent("Bit")};
|
||||
columns[(int)ColumnId.Size] =
|
||||
new MultiColumnHeaderState.Column {headerContent = new GUIContent("Size (Bits)")};
|
||||
columns[(int)ColumnId.Optimized] =
|
||||
new MultiColumnHeaderState.Column {headerContent = new GUIContent("Optimized")};
|
||||
|
||||
if (numValueColumns == 1)
|
||||
{
|
||||
columns[(int)ColumnId.Value] =
|
||||
new MultiColumnHeaderState.Column {width = 120, headerContent = new GUIContent("Value")};
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var i = 0; i < numValueColumns; ++i)
|
||||
columns[(int)ColumnId.Value + i] =
|
||||
new MultiColumnHeaderState.Column
|
||||
{
|
||||
width = 100,
|
||||
headerContent = new GUIContent("Value " + (char)('A' + i))
|
||||
};
|
||||
}
|
||||
|
||||
return new MultiColumnHeaderState(columns);
|
||||
}
|
||||
|
||||
private InputControlTreeView(InputControl root, TreeViewState state, MultiColumnHeader header)
|
||||
: base(state, header)
|
||||
{
|
||||
m_RootControl = root;
|
||||
showBorder = false;
|
||||
rowHeight = kRowHeight;
|
||||
}
|
||||
|
||||
protected override TreeViewItem BuildRoot()
|
||||
{
|
||||
k_InputBuildControlTreeMarker.Begin();
|
||||
|
||||
var id = 1;
|
||||
|
||||
// Build tree from control down the control hierarchy.
|
||||
var rootItem = BuildControlTreeRecursive(m_RootControl, 0, ref id);
|
||||
|
||||
k_InputBuildControlTreeMarker.End();
|
||||
|
||||
// Wrap root control in invisible item required by TreeView.
|
||||
return new TreeViewItem
|
||||
{
|
||||
id = 0,
|
||||
children = new List<TreeViewItem> {rootItem},
|
||||
depth = -1
|
||||
};
|
||||
}
|
||||
|
||||
private ControlItem BuildControlTreeRecursive(InputControl control, int depth, ref int id)
|
||||
{
|
||||
// Build children.
|
||||
List<TreeViewItem> children = null;
|
||||
var isLeaf = control.children.Count == 0;
|
||||
if (!isLeaf)
|
||||
{
|
||||
children = new List<TreeViewItem>();
|
||||
|
||||
foreach (var child in control.children)
|
||||
{
|
||||
var childItem = BuildControlTreeRecursive(child, depth + 1, ref id);
|
||||
if (childItem != null)
|
||||
children.Add(childItem);
|
||||
}
|
||||
|
||||
// If none of our children returned an item, none of their data is different,
|
||||
// so if we are supposed to show only controls that differ in value, we're sitting
|
||||
// on a branch that has no changes. Cull the branch except if we're all the way
|
||||
// at the root (we want to have at least one item).
|
||||
if (children.Count == 0 && showDifferentOnly && depth != 0)
|
||||
return null;
|
||||
|
||||
// Sort children by name.
|
||||
children.Sort((a, b) => string.Compare(a.displayName, b.displayName));
|
||||
}
|
||||
|
||||
// Compute offset. Offsets on the controls are absolute. Make them relative to the
|
||||
// root control.
|
||||
var controlOffset = control.stateBlock.byteOffset;
|
||||
var rootOffset = m_RootControl.stateBlock.byteOffset;
|
||||
var offset = controlOffset - rootOffset;
|
||||
|
||||
// Read state.
|
||||
var item = new ControlItem
|
||||
{
|
||||
id = id++,
|
||||
control = control,
|
||||
depth = depth,
|
||||
children = children
|
||||
};
|
||||
|
||||
////TODO: come up with nice icons depicting different control types
|
||||
if (!ReadState(control, ref item))
|
||||
return null;
|
||||
|
||||
if (children != null)
|
||||
{
|
||||
foreach (var child in children)
|
||||
child.parent = item;
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
private bool ReadState(InputControl control, ref ControlItem item)
|
||||
{
|
||||
// Compute offset. Offsets on the controls are absolute. Make them relative to the
|
||||
// root control.
|
||||
var controlOffset = control.stateBlock.byteOffset;
|
||||
var rootOffset = m_RootControl.stateBlock.byteOffset;
|
||||
var offset = controlOffset - rootOffset;
|
||||
|
||||
item.displayName = control.name;
|
||||
item.layout = new GUIContent(control.layout);
|
||||
item.format = new GUIContent(control.stateBlock.format.ToString());
|
||||
item.offset = new GUIContent(offset.ToString());
|
||||
item.bit = new GUIContent(control.stateBlock.bitOffset.ToString());
|
||||
item.sizeInBits = new GUIContent(control.stateBlock.sizeInBits.ToString());
|
||||
item.type = new GUIContent(control.GetType().Name);
|
||||
item.optimized = new GUIContent(control.optimizedControlDataType != InputStateBlock.kFormatInvalid ? "+" : "-");
|
||||
|
||||
try
|
||||
{
|
||||
if (stateBuffer != null)
|
||||
{
|
||||
var text = ReadRawValueAsString(control, stateBuffer);
|
||||
if (text != null)
|
||||
item.value = new GUIContent(text);
|
||||
}
|
||||
else if (multipleStateBuffers != null)
|
||||
{
|
||||
var valueStrings = multipleStateBuffers.Select(x => ReadRawValueAsString(control, x));
|
||||
if (showDifferentOnly && control.children.Count == 0 && valueStrings.Distinct().Count() == 1)
|
||||
return false;
|
||||
item.values = valueStrings.Select(x => x != null ? new GUIContent(x) : null).ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
var valueObject = control.ReadValueAsObject();
|
||||
if (valueObject != null)
|
||||
item.value = new GUIContent(valueObject.ToString());
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
// If we fail to read a value, swallow it so we don't fail completely
|
||||
// showing anything from the device.
|
||||
item.value = new GUIContent(exception.ToString());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void RowGUI(RowGUIArgs args)
|
||||
{
|
||||
var item = (ControlItem)args.item;
|
||||
|
||||
var columnCount = args.GetNumVisibleColumns();
|
||||
for (var i = 0; i < columnCount; ++i)
|
||||
{
|
||||
ColumnGUI(args.GetCellRect(i), item, args.GetColumn(i), ref args);
|
||||
}
|
||||
}
|
||||
|
||||
private void ColumnGUI(Rect cellRect, ControlItem item, int column, ref RowGUIArgs args)
|
||||
{
|
||||
CenterRectUsingSingleLineHeight(ref cellRect);
|
||||
|
||||
switch (column)
|
||||
{
|
||||
case (int)ColumnId.Name:
|
||||
args.rowRect = cellRect;
|
||||
base.RowGUI(args);
|
||||
break;
|
||||
case (int)ColumnId.DisplayName:
|
||||
GUI.Label(cellRect, item.control.displayName);
|
||||
break;
|
||||
case (int)ColumnId.Layout:
|
||||
GUI.Label(cellRect, item.layout);
|
||||
break;
|
||||
case (int)ColumnId.Format:
|
||||
GUI.Label(cellRect, item.format);
|
||||
break;
|
||||
case (int)ColumnId.Offset:
|
||||
GUI.Label(cellRect, item.offset);
|
||||
break;
|
||||
case (int)ColumnId.Bit:
|
||||
GUI.Label(cellRect, item.bit);
|
||||
break;
|
||||
case (int)ColumnId.Size:
|
||||
GUI.Label(cellRect, item.sizeInBits);
|
||||
break;
|
||||
case (int)ColumnId.Type:
|
||||
GUI.Label(cellRect, item.type);
|
||||
break;
|
||||
case (int)ColumnId.Optimized:
|
||||
GUI.Label(cellRect, item.optimized);
|
||||
break;
|
||||
case (int)ColumnId.Value:
|
||||
if (item.value != null)
|
||||
GUI.Label(cellRect, item.value);
|
||||
else if (item.values != null && item.values[0] != null)
|
||||
GUI.Label(cellRect, item.values[0]);
|
||||
break;
|
||||
default:
|
||||
var valueIndex = column - (int)ColumnId.Value;
|
||||
if (item.values != null && item.values[valueIndex] != null)
|
||||
GUI.Label(cellRect, item.values[valueIndex]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe string ReadRawValueAsString(InputControl control, byte[] state)
|
||||
{
|
||||
fixed(byte* statePtr = state)
|
||||
{
|
||||
var ptr = statePtr - m_RootControl.m_StateBlock.byteOffset;
|
||||
return control.ReadValueFromStateAsObject(ptr).ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private class ControlItem : TreeViewItem
|
||||
{
|
||||
public InputControl control;
|
||||
public GUIContent layout;
|
||||
public GUIContent format;
|
||||
public GUIContent offset;
|
||||
public GUIContent bit;
|
||||
public GUIContent sizeInBits;
|
||||
public GUIContent type;
|
||||
public GUIContent optimized;
|
||||
public GUIContent value;
|
||||
public GUIContent[] values;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // UNITY_EDITOR
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aa58e874cb4143ad884b8da2bb3f0d7a
|
||||
timeCreated: 1508116067
|
@@ -0,0 +1,286 @@
|
||||
#if UNITY_EDITOR
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor.IMGUI.Controls;
|
||||
using UnityEngine.InputSystem.LowLevel;
|
||||
using UnityEditor;
|
||||
using Unity.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 static readonly ProfilerMarker k_InputEventTreeBuildRootMarker = new ProfilerMarker("InputEventTreeView.BuildRoot");
|
||||
|
||||
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()
|
||||
{
|
||||
k_InputEventTreeBuildRootMarker.Begin();
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
k_InputEventTreeBuildRootMarker.End();
|
||||
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
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a3882f27ee264cbfaa87fbdceacfcea1
|
||||
timeCreated: 1508116097
|
@@ -0,0 +1,442 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Unity.Collections.LowLevel.Unsafe;
|
||||
using UnityEditor;
|
||||
using UnityEditor.IMGUI.Controls;
|
||||
using UnityEngine.InputSystem.LowLevel;
|
||||
|
||||
////TODO: add ability to single-step through events
|
||||
|
||||
////TODO: annotate raw memory view with control offset and ranges (probably easiest to put the control tree and raw memory view side by side)
|
||||
|
||||
////TODO: find way to automatically dock the state windows next to their InputDeviceDebuggerWindows
|
||||
//// (probably needs an extension to the editor UI APIs as the only programmatic docking controls
|
||||
//// seem to be through GetWindow)
|
||||
|
||||
////TODO: allow setting a C# struct type that we can use to display the layout of the data
|
||||
|
||||
////TODO: for delta state events, highlight the controls included in the event (or show only those)
|
||||
|
||||
////FIXME: need to prevent extra controls appended at end from reading beyond the state buffer
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
// Additional window that we can pop open to inspect raw state (either on events or on controls/devices).
|
||||
internal class InputStateWindow : EditorWindow
|
||||
{
|
||||
private const int kBytesPerHexGroup = 1;
|
||||
private const int kHexGroupsPerLine = 8;
|
||||
private const int kHexDumpLineHeight = 25;
|
||||
private const int kOffsetLabelWidth = 30;
|
||||
private const int kHexGroupWidth = 25;
|
||||
private const int kBitGroupWidth = 75;
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (m_PollControlState && m_Control != null)
|
||||
{
|
||||
PollBuffersFromControl(m_Control);
|
||||
Repaint();
|
||||
}
|
||||
}
|
||||
|
||||
public void InitializeWithEvent(InputEventPtr eventPtr, InputControl control)
|
||||
{
|
||||
m_Control = control;
|
||||
m_PollControlState = false;
|
||||
m_StateBuffers = new byte[1][];
|
||||
m_StateBuffers[0] = GetEventStateBuffer(eventPtr, control);
|
||||
m_SelectedStateBuffer = 0;
|
||||
|
||||
titleContent = new GUIContent(control.displayName);
|
||||
}
|
||||
|
||||
public void InitializeWithEvents(InputEventPtr[] eventPtrs, InputControl control)
|
||||
{
|
||||
var numEvents = eventPtrs.Length;
|
||||
|
||||
m_Control = control;
|
||||
m_PollControlState = false;
|
||||
m_StateBuffers = new byte[numEvents][];
|
||||
for (var i = 0; i < numEvents; ++i)
|
||||
m_StateBuffers[i] = GetEventStateBuffer(eventPtrs[i], control);
|
||||
m_CompareStateBuffers = true;
|
||||
m_ShowDifferentOnly = true;
|
||||
|
||||
titleContent = new GUIContent(control.displayName);
|
||||
}
|
||||
|
||||
private unsafe byte[] GetEventStateBuffer(InputEventPtr eventPtr, InputControl control)
|
||||
{
|
||||
// Must be an event carrying state.
|
||||
if (!eventPtr.IsA<StateEvent>() && !eventPtr.IsA<DeltaStateEvent>())
|
||||
throw new ArgumentException("Event must be state or delta event", nameof(eventPtr));
|
||||
|
||||
// Get state data.
|
||||
void* dataPtr;
|
||||
uint dataSize;
|
||||
uint stateSize;
|
||||
uint stateOffset = 0;
|
||||
|
||||
if (eventPtr.IsA<DeltaStateEvent>())
|
||||
{
|
||||
var deltaEventPtr = DeltaStateEvent.From(eventPtr);
|
||||
stateSize = control.stateBlock.alignedSizeInBytes;
|
||||
stateOffset = deltaEventPtr->stateOffset;
|
||||
dataPtr = deltaEventPtr->deltaState;
|
||||
dataSize = deltaEventPtr->deltaStateSizeInBytes;
|
||||
}
|
||||
else
|
||||
{
|
||||
var stateEventPtr = StateEvent.From(eventPtr);
|
||||
dataSize = stateSize = stateEventPtr->stateSizeInBytes;
|
||||
dataPtr = stateEventPtr->state;
|
||||
}
|
||||
|
||||
// Copy event data.
|
||||
var buffer = new byte[stateSize];
|
||||
fixed(byte* bufferPtr = buffer)
|
||||
{
|
||||
UnsafeUtility.MemCpy(bufferPtr + stateOffset, dataPtr, dataSize);
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public unsafe void InitializeWithControl(InputControl control)
|
||||
{
|
||||
m_Control = control;
|
||||
m_PollControlState = true;
|
||||
m_SelectedStateBuffer = (int)BufferSelector.Default;
|
||||
|
||||
PollBuffersFromControl(control, selectBuffer: true);
|
||||
|
||||
titleContent = new GUIContent(control.displayName);
|
||||
}
|
||||
|
||||
private unsafe void PollBuffersFromControl(InputControl control, bool selectBuffer = false)
|
||||
{
|
||||
var bufferChoices = new List<GUIContent>();
|
||||
var bufferChoiceValues = new List<int>();
|
||||
|
||||
// Copy front and back buffer state for each update that has valid buffers.
|
||||
var device = control.device;
|
||||
var stateSize = control.m_StateBlock.alignedSizeInBytes;
|
||||
var stateOffset = control.m_StateBlock.byteOffset;
|
||||
m_StateBuffers = new byte[(int)BufferSelector.COUNT][];
|
||||
for (var i = 0; i < (int)BufferSelector.COUNT; ++i)
|
||||
{
|
||||
var selector = (BufferSelector)i;
|
||||
var deviceState = TryGetDeviceState(device, selector);
|
||||
if (deviceState == null)
|
||||
continue;
|
||||
|
||||
var buffer = new byte[stateSize];
|
||||
fixed(byte* stateDataPtr = buffer)
|
||||
{
|
||||
UnsafeUtility.MemCpy(stateDataPtr, (byte*)deviceState + (int)stateOffset, stateSize);
|
||||
}
|
||||
m_StateBuffers[i] = buffer;
|
||||
|
||||
if (selectBuffer && m_StateBuffers[m_SelectedStateBuffer] == null)
|
||||
m_SelectedStateBuffer = (int)selector;
|
||||
|
||||
bufferChoices.Add(Contents.bufferChoices[i]);
|
||||
bufferChoiceValues.Add(i);
|
||||
}
|
||||
|
||||
m_BufferChoices = bufferChoices.ToArray();
|
||||
m_BufferChoiceValues = bufferChoiceValues.ToArray();
|
||||
}
|
||||
|
||||
private static unsafe void* TryGetDeviceState(InputDevice device, BufferSelector selector)
|
||||
{
|
||||
var manager = InputSystem.s_Manager;
|
||||
var deviceIndex = device.m_DeviceIndex;
|
||||
|
||||
switch (selector)
|
||||
{
|
||||
case BufferSelector.PlayerUpdateFrontBuffer:
|
||||
if (manager.m_StateBuffers.m_PlayerStateBuffers.valid)
|
||||
return manager.m_StateBuffers.m_PlayerStateBuffers.GetFrontBuffer(deviceIndex);
|
||||
break;
|
||||
case BufferSelector.PlayerUpdateBackBuffer:
|
||||
if (manager.m_StateBuffers.m_PlayerStateBuffers.valid)
|
||||
return manager.m_StateBuffers.m_PlayerStateBuffers.GetBackBuffer(deviceIndex);
|
||||
break;
|
||||
case BufferSelector.EditorUpdateFrontBuffer:
|
||||
if (manager.m_StateBuffers.m_EditorStateBuffers.valid)
|
||||
return manager.m_StateBuffers.m_EditorStateBuffers.GetFrontBuffer(deviceIndex);
|
||||
break;
|
||||
case BufferSelector.EditorUpdateBackBuffer:
|
||||
if (manager.m_StateBuffers.m_EditorStateBuffers.valid)
|
||||
return manager.m_StateBuffers.m_EditorStateBuffers.GetBackBuffer(deviceIndex);
|
||||
break;
|
||||
case BufferSelector.NoiseMaskBuffer:
|
||||
return manager.m_StateBuffers.noiseMaskBuffer;
|
||||
case BufferSelector.ResetMaskBuffer:
|
||||
return manager.m_StateBuffers.resetMaskBuffer;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void OnGUI()
|
||||
{
|
||||
if (m_Control == null)
|
||||
m_ShowRawBytes = true;
|
||||
|
||||
// If our state is no longer valid, just close the window.
|
||||
if (m_StateBuffers == null)
|
||||
{
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
|
||||
GUILayout.BeginHorizontal(EditorStyles.toolbar);
|
||||
m_PollControlState = GUILayout.Toggle(m_PollControlState, Contents.live, EditorStyles.toolbarButton);
|
||||
|
||||
m_ShowRawBytes = GUILayout.Toggle(m_ShowRawBytes, Contents.showRawMemory, EditorStyles.toolbarButton,
|
||||
GUILayout.Width(150));
|
||||
|
||||
m_ShowAsBits = GUILayout.Toggle(m_ShowAsBits, Contents.showBits, EditorStyles.toolbarButton);
|
||||
|
||||
if (m_CompareStateBuffers)
|
||||
{
|
||||
var showDifferentOnly = GUILayout.Toggle(m_ShowDifferentOnly, Contents.showDifferentOnly,
|
||||
EditorStyles.toolbarButton, GUILayout.Width(150));
|
||||
if (showDifferentOnly != m_ShowDifferentOnly && m_ControlTree != null)
|
||||
{
|
||||
m_ControlTree.showDifferentOnly = showDifferentOnly;
|
||||
m_ControlTree.Reload();
|
||||
}
|
||||
|
||||
m_ShowDifferentOnly = showDifferentOnly;
|
||||
}
|
||||
|
||||
// If we have multiple state buffers to choose from and we're not comparing them to each other,
|
||||
// add dropdown that allows selecting which buffer to display.
|
||||
if (m_StateBuffers.Length > 1 && !m_CompareStateBuffers)
|
||||
{
|
||||
var selectedBuffer = EditorGUILayout.IntPopup(m_SelectedStateBuffer, m_BufferChoices,
|
||||
m_BufferChoiceValues, EditorStyles.toolbarPopup);
|
||||
if (selectedBuffer != m_SelectedStateBuffer)
|
||||
{
|
||||
m_SelectedStateBuffer = selectedBuffer;
|
||||
m_ControlTree = null;
|
||||
}
|
||||
}
|
||||
|
||||
GUILayout.FlexibleSpace();
|
||||
GUILayout.EndHorizontal();
|
||||
|
||||
if (m_ShowRawBytes)
|
||||
{
|
||||
DrawHexDump();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (m_ControlTree == null)
|
||||
{
|
||||
if (m_CompareStateBuffers)
|
||||
{
|
||||
m_ControlTree = InputControlTreeView.Create(m_Control, m_StateBuffers.Length, ref m_ControlTreeState, ref m_ControlTreeHeaderState);
|
||||
m_ControlTree.multipleStateBuffers = m_StateBuffers;
|
||||
m_ControlTree.showDifferentOnly = m_ShowDifferentOnly;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_ControlTree = InputControlTreeView.Create(m_Control, 1, ref m_ControlTreeState, ref m_ControlTreeHeaderState);
|
||||
m_ControlTree.stateBuffer = m_StateBuffers[m_SelectedStateBuffer];
|
||||
}
|
||||
m_ControlTree.Reload();
|
||||
m_ControlTree.ExpandAll();
|
||||
}
|
||||
|
||||
var rect = EditorGUILayout.GetControlRect(GUILayout.ExpandHeight(true));
|
||||
m_ControlTree.OnGUI(rect);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] TryGetBackBufferForCurrentlySelected()
|
||||
{
|
||||
if (m_StateBuffers.Length != (int)BufferSelector.COUNT)
|
||||
return null;
|
||||
|
||||
switch ((BufferSelector)m_SelectedStateBuffer)
|
||||
{
|
||||
case BufferSelector.PlayerUpdateFrontBuffer:
|
||||
return m_StateBuffers[(int)BufferSelector.PlayerUpdateBackBuffer];
|
||||
case BufferSelector.EditorUpdateFrontBuffer:
|
||||
return m_StateBuffers[(int)BufferSelector.EditorUpdateBackBuffer];
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private string FormatByte(byte value)
|
||||
{
|
||||
if (m_ShowAsBits)
|
||||
return Convert.ToString(value, 2).PadLeft(8, '0');
|
||||
else
|
||||
return value.ToString("X2");
|
||||
}
|
||||
|
||||
////TODO: support dumping multiple state side-by-side when comparing
|
||||
private void DrawHexDump()
|
||||
{
|
||||
m_HexDumpScrollPosition = EditorGUILayout.BeginScrollView(m_HexDumpScrollPosition);
|
||||
|
||||
var stateBuffer = m_StateBuffers[m_SelectedStateBuffer];
|
||||
var prevStateBuffer = TryGetBackBufferForCurrentlySelected();
|
||||
if (prevStateBuffer != null && prevStateBuffer.Length != stateBuffer.Length) // we assume they're same length, otherwise ignore prev buffer
|
||||
prevStateBuffer = null;
|
||||
var numBytes = stateBuffer.Length;
|
||||
var numHexGroups = numBytes / kBytesPerHexGroup + (numBytes % kBytesPerHexGroup > 0 ? 1 : 0);
|
||||
var numLines = numHexGroups / kHexGroupsPerLine + (numHexGroups % kHexGroupsPerLine > 0 ? 1 : 0);
|
||||
var currentOffset = 0;
|
||||
var currentLineRect = EditorGUILayout.GetControlRect(GUILayout.ExpandWidth(true));
|
||||
currentLineRect.height = kHexDumpLineHeight;
|
||||
var currentHexGroup = 0;
|
||||
var currentByte = 0;
|
||||
|
||||
////REVIEW: what would be totally awesome is if this not just displayed a hex dump but also the correlation to current
|
||||
//// control offset assignments
|
||||
|
||||
for (var line = 0; line < numLines; ++line)
|
||||
{
|
||||
// Draw offset.
|
||||
var offsetLabelRect = currentLineRect;
|
||||
offsetLabelRect.width = kOffsetLabelWidth;
|
||||
GUI.Label(offsetLabelRect, currentOffset.ToString(), Styles.offsetLabel);
|
||||
currentOffset += kBytesPerHexGroup * kHexGroupsPerLine;
|
||||
|
||||
// Draw hex groups.
|
||||
var hexGroupRect = offsetLabelRect;
|
||||
hexGroupRect.x += kOffsetLabelWidth + 10;
|
||||
hexGroupRect.width = m_ShowAsBits ? kBitGroupWidth : kHexGroupWidth;
|
||||
for (var group = 0;
|
||||
group < kHexGroupsPerLine && currentHexGroup < numHexGroups;
|
||||
++group, ++currentHexGroup)
|
||||
{
|
||||
// Convert bytes to hex.
|
||||
var hex = string.Empty;
|
||||
|
||||
for (var i = 0; i < kBytesPerHexGroup; ++i, ++currentByte)
|
||||
{
|
||||
if (currentByte >= numBytes)
|
||||
{
|
||||
hex += " ";
|
||||
continue;
|
||||
}
|
||||
|
||||
var current = FormatByte(stateBuffer[currentByte]);
|
||||
if (prevStateBuffer == null)
|
||||
{
|
||||
hex += current;
|
||||
continue;
|
||||
}
|
||||
|
||||
var prev = FormatByte(prevStateBuffer[currentByte]);
|
||||
if (prev.Length != current.Length)
|
||||
{
|
||||
hex += current;
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var j = 0; j < current.Length; ++j)
|
||||
{
|
||||
if (current[j] != prev[j])
|
||||
hex += $"<color=#C84B31FF>{current[j]}</color>";
|
||||
else
|
||||
hex += current[j];
|
||||
}
|
||||
}
|
||||
|
||||
////TODO: draw alternating backgrounds for the hex groups
|
||||
|
||||
GUI.Label(hexGroupRect, hex, style: Styles.hexLabel);
|
||||
hexGroupRect.x += m_ShowAsBits ? kBitGroupWidth : kHexGroupWidth;
|
||||
}
|
||||
|
||||
currentLineRect.y += kHexDumpLineHeight;
|
||||
}
|
||||
|
||||
EditorGUILayout.EndScrollView();
|
||||
}
|
||||
|
||||
// We copy the state we're inspecting to a buffer we own so that we're safe
|
||||
// against any mutations.
|
||||
// When inspecting controls (as opposed to events), we copy all their various
|
||||
// state buffers and allow switching between them.
|
||||
[SerializeField] private byte[][] m_StateBuffers;
|
||||
[SerializeField] private int m_SelectedStateBuffer;
|
||||
[SerializeField] private bool m_CompareStateBuffers;
|
||||
[SerializeField] private bool m_ShowDifferentOnly;
|
||||
[SerializeField] private bool m_ShowRawBytes;
|
||||
[SerializeField] private bool m_ShowAsBits;
|
||||
[SerializeField] private bool m_PollControlState;
|
||||
[SerializeField] private TreeViewState m_ControlTreeState;
|
||||
[SerializeField] private MultiColumnHeaderState m_ControlTreeHeaderState;
|
||||
[SerializeField] private Vector2 m_HexDumpScrollPosition;
|
||||
|
||||
[NonSerialized] private InputControlTreeView m_ControlTree;
|
||||
[NonSerialized] private GUIContent[] m_BufferChoices;
|
||||
[NonSerialized] private int[] m_BufferChoiceValues;
|
||||
|
||||
////FIXME: we lose this on domain reload; how should we recover?
|
||||
[NonSerialized] private InputControl m_Control;
|
||||
|
||||
private enum BufferSelector
|
||||
{
|
||||
PlayerUpdateFrontBuffer,
|
||||
PlayerUpdateBackBuffer,
|
||||
EditorUpdateFrontBuffer,
|
||||
EditorUpdateBackBuffer,
|
||||
NoiseMaskBuffer,
|
||||
ResetMaskBuffer,
|
||||
COUNT,
|
||||
Default = PlayerUpdateFrontBuffer
|
||||
}
|
||||
|
||||
private static class Styles
|
||||
{
|
||||
public static GUIStyle offsetLabel = new GUIStyle
|
||||
{
|
||||
alignment = TextAnchor.UpperRight,
|
||||
fontStyle = FontStyle.BoldAndItalic,
|
||||
font = EditorStyles.boldFont,
|
||||
fontSize = EditorStyles.boldFont.fontSize - 2,
|
||||
normal = new GUIStyleState { textColor = Color.black }
|
||||
};
|
||||
|
||||
public static GUIStyle hexLabel = new GUIStyle
|
||||
{
|
||||
fontStyle = FontStyle.Normal,
|
||||
font = EditorGUIUtility.Load("Fonts/RobotoMono/RobotoMono-Regular.ttf") as Font,
|
||||
fontSize = EditorStyles.label.fontSize + 2,
|
||||
normal = new GUIStyleState { textColor = Color.white },
|
||||
richText = true
|
||||
};
|
||||
}
|
||||
|
||||
private static class Contents
|
||||
{
|
||||
public static GUIContent live = new GUIContent("Live");
|
||||
public static GUIContent showRawMemory = new GUIContent("Display Raw Memory");
|
||||
public static GUIContent showBits = new GUIContent("Bits/Hex");
|
||||
public static GUIContent showDifferentOnly = new GUIContent("Show Only Differences");
|
||||
public static GUIContent[] bufferChoices =
|
||||
{
|
||||
new GUIContent("Player (Current)"),
|
||||
new GUIContent("Player (Previous)"),
|
||||
new GUIContent("Editor (Current)"),
|
||||
new GUIContent("Editor (Previous)"),
|
||||
new GUIContent("Noise Mask"),
|
||||
new GUIContent("Reset Mask")
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // UNITY_EDITOR
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d0821e88149544e9b53290bf15b8d100
|
||||
timeCreated: 1508117269
|
@@ -0,0 +1,393 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using UnityEditor;
|
||||
using UnityEngine.InputSystem.Utilities;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Helpers for working with <see cref="SerializedProperty"/> in the editor.
|
||||
/// </summary>
|
||||
internal static class SerializedPropertyHelpers
|
||||
{
|
||||
// Show a PropertyField with a greyed-out default text if the field is empty and not being edited.
|
||||
// This is meant to communicate the fact that filling these properties is optional and that Unity will
|
||||
// use reasonable defaults if left empty.
|
||||
public static void PropertyFieldWithDefaultText(this SerializedProperty prop, GUIContent label, string defaultText)
|
||||
{
|
||||
GUI.SetNextControlName(label.text);
|
||||
var rt = GUILayoutUtility.GetRect(label, GUI.skin.textField);
|
||||
|
||||
EditorGUI.PropertyField(rt, prop, label);
|
||||
if (string.IsNullOrEmpty(prop.stringValue) && GUI.GetNameOfFocusedControl() != label.text && Event.current.type == EventType.Repaint)
|
||||
{
|
||||
using (new EditorGUI.DisabledScope(true))
|
||||
{
|
||||
rt.xMin += EditorGUIUtility.labelWidth;
|
||||
GUI.skin.textField.Draw(rt, new GUIContent(defaultText), false, false, false, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static SerializedProperty GetParentProperty(this SerializedProperty property)
|
||||
{
|
||||
var path = property.propertyPath;
|
||||
var lastDot = path.LastIndexOf('.');
|
||||
if (lastDot == -1)
|
||||
return null;
|
||||
var parentPath = path.Substring(0, lastDot);
|
||||
return property.serializedObject.FindProperty(parentPath);
|
||||
}
|
||||
|
||||
public static SerializedProperty GetArrayPropertyFromElement(this SerializedProperty property)
|
||||
{
|
||||
// Arrays have a structure of 'arrayName.Array.data[index]'.
|
||||
// Given property should be element and thus 'data[index]'.
|
||||
var arrayProperty = property.GetParentProperty();
|
||||
Debug.Assert(arrayProperty.name == "Array", "Expecting 'Array' property");
|
||||
return arrayProperty.GetParentProperty();
|
||||
}
|
||||
|
||||
public static int GetIndexOfArrayElement(this SerializedProperty property)
|
||||
{
|
||||
if (property == null)
|
||||
return -1;
|
||||
var propertyPath = property.propertyPath;
|
||||
if (propertyPath[propertyPath.Length - 1] != ']')
|
||||
return -1;
|
||||
var lastIndexOfLeftBracket = propertyPath.LastIndexOf('[');
|
||||
if (int.TryParse(
|
||||
propertyPath.Substring(lastIndexOfLeftBracket + 1, propertyPath.Length - lastIndexOfLeftBracket - 2),
|
||||
out var index))
|
||||
return index;
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static Type GetArrayElementType(this SerializedProperty property)
|
||||
{
|
||||
Debug.Assert(property.isArray, $"Property {property.propertyPath} is not an array");
|
||||
|
||||
var fieldType = property.GetFieldType();
|
||||
if (fieldType == null)
|
||||
throw new ArgumentException($"Cannot determine managed field type of {property.propertyPath}",
|
||||
nameof(property));
|
||||
|
||||
return fieldType.GetElementType();
|
||||
}
|
||||
|
||||
public static void ResetValuesToDefault(this SerializedProperty property)
|
||||
{
|
||||
var isString = property.propertyType == SerializedPropertyType.String;
|
||||
|
||||
if (property.isArray && !isString)
|
||||
{
|
||||
property.ClearArray();
|
||||
}
|
||||
else if (property.hasChildren && !isString)
|
||||
{
|
||||
foreach (var child in property.GetChildren())
|
||||
ResetValuesToDefault(child);
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (property.propertyType)
|
||||
{
|
||||
case SerializedPropertyType.Float:
|
||||
property.floatValue = default(float);
|
||||
break;
|
||||
|
||||
case SerializedPropertyType.Boolean:
|
||||
property.boolValue = default(bool);
|
||||
break;
|
||||
|
||||
case SerializedPropertyType.Enum:
|
||||
case SerializedPropertyType.Integer:
|
||||
property.intValue = default(int);
|
||||
break;
|
||||
|
||||
case SerializedPropertyType.String:
|
||||
property.stringValue = string.Empty;
|
||||
break;
|
||||
|
||||
case SerializedPropertyType.ObjectReference:
|
||||
property.objectReferenceValue = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static string ToJson(this SerializedObject serializedObject)
|
||||
{
|
||||
return JsonUtility.ToJson(serializedObject, prettyPrint: true);
|
||||
}
|
||||
|
||||
// The following is functionality that allows turning Unity data into text and text
|
||||
// back into Unity data. Given that this is essential functionality for any kind of
|
||||
// copypaste support, I'm not sure why the Unity editor API isn't providing this out
|
||||
// of the box. Internally, we do have support for this on a whole-object kind of level
|
||||
// but not for parts of serialized objects.
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="property"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// Converting entire objects to JSON is easy using Unity's serialization system but we cannot
|
||||
/// easily convert just a part of the serialized graph to JSON (or any text format for that matter)
|
||||
/// and then recreate the same data from text through SerializedProperties. This method helps by manually
|
||||
/// turning an arbitrary part of a graph into JSON which can then be used with <see cref="RestoreFromJson"/>
|
||||
/// to write the data back into an existing property.
|
||||
///
|
||||
/// The primary use for this is copy-paste where serialized data needs to be stored in
|
||||
/// <see cref="EditorGUIUtility.systemCopyBuffer"/>.
|
||||
/// </remarks>
|
||||
public static string CopyToJson(this SerializedProperty property, bool ignoreObjectReferences = false)
|
||||
{
|
||||
var buffer = new StringBuilder();
|
||||
CopyToJson(property, buffer, ignoreObjectReferences);
|
||||
return buffer.ToString();
|
||||
}
|
||||
|
||||
public static void CopyToJson(this SerializedProperty property, StringBuilder buffer, bool ignoreObjectReferences = false)
|
||||
{
|
||||
CopyToJson(property, buffer, noPropertyName: true, ignoreObjectReferences: ignoreObjectReferences);
|
||||
}
|
||||
|
||||
private static void CopyToJson(this SerializedProperty property, StringBuilder buffer, bool noPropertyName, bool ignoreObjectReferences)
|
||||
{
|
||||
var propertyType = property.propertyType;
|
||||
if (ignoreObjectReferences && propertyType == SerializedPropertyType.ObjectReference)
|
||||
return;
|
||||
|
||||
// Property name.
|
||||
if (!noPropertyName)
|
||||
{
|
||||
buffer.Append('"');
|
||||
buffer.Append(property.name);
|
||||
buffer.Append('"');
|
||||
buffer.Append(':');
|
||||
}
|
||||
|
||||
// Strings are classified as arrays and have children.
|
||||
var isString = propertyType == SerializedPropertyType.String;
|
||||
|
||||
// Property value.
|
||||
if (property.isArray && !isString)
|
||||
{
|
||||
buffer.Append('[');
|
||||
var arraySize = property.arraySize;
|
||||
var isFirst = true;
|
||||
for (var i = 0; i < arraySize; ++i)
|
||||
{
|
||||
var element = property.GetArrayElementAtIndex(i);
|
||||
if (ignoreObjectReferences && element.propertyType == SerializedPropertyType.ObjectReference)
|
||||
continue;
|
||||
if (!isFirst)
|
||||
buffer.Append(',');
|
||||
CopyToJson(element, buffer, true, ignoreObjectReferences);
|
||||
isFirst = false;
|
||||
}
|
||||
buffer.Append(']');
|
||||
}
|
||||
else if (property.hasChildren && !isString)
|
||||
{
|
||||
// Any structured data we represent as a JSON object.
|
||||
|
||||
buffer.Append('{');
|
||||
var isFirst = true;
|
||||
foreach (var child in property.GetChildren())
|
||||
{
|
||||
if (ignoreObjectReferences && child.propertyType == SerializedPropertyType.ObjectReference)
|
||||
continue;
|
||||
if (!isFirst)
|
||||
buffer.Append(',');
|
||||
CopyToJson(child, buffer, false, ignoreObjectReferences);
|
||||
isFirst = false;
|
||||
}
|
||||
buffer.Append('}');
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (propertyType)
|
||||
{
|
||||
case SerializedPropertyType.Enum:
|
||||
case SerializedPropertyType.Integer:
|
||||
buffer.Append(property.intValue);
|
||||
break;
|
||||
|
||||
case SerializedPropertyType.Float:
|
||||
buffer.Append(property.floatValue);
|
||||
break;
|
||||
|
||||
case SerializedPropertyType.String:
|
||||
buffer.Append('"');
|
||||
buffer.Append(property.stringValue.Escape());
|
||||
buffer.Append('"');
|
||||
break;
|
||||
|
||||
case SerializedPropertyType.Boolean:
|
||||
if (property.boolValue)
|
||||
buffer.Append("true");
|
||||
else
|
||||
buffer.Append("false");
|
||||
break;
|
||||
|
||||
////TODO: other property types
|
||||
default:
|
||||
throw new NotImplementedException($"Support for {property.propertyType} property type");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void RestoreFromJson(this SerializedProperty property, string json)
|
||||
{
|
||||
var parser = new JsonParser(json);
|
||||
RestoreFromJson(property, ref parser);
|
||||
}
|
||||
|
||||
public static void RestoreFromJson(this SerializedProperty property, ref JsonParser parser)
|
||||
{
|
||||
var isString = property.propertyType == SerializedPropertyType.String;
|
||||
|
||||
if (property.isArray && !isString)
|
||||
{
|
||||
property.ClearArray();
|
||||
parser.ParseToken('[');
|
||||
while (!parser.ParseToken(']') && !parser.isAtEnd)
|
||||
{
|
||||
var index = property.arraySize;
|
||||
property.InsertArrayElementAtIndex(index);
|
||||
var elementProperty = property.GetArrayElementAtIndex(index);
|
||||
RestoreFromJson(elementProperty, ref parser);
|
||||
parser.ParseToken(',');
|
||||
}
|
||||
}
|
||||
else if (property.hasChildren && !isString)
|
||||
{
|
||||
parser.ParseToken('{');
|
||||
while (!parser.ParseToken('}') && !parser.isAtEnd)
|
||||
{
|
||||
parser.ParseStringValue(out var propertyName);
|
||||
parser.ParseToken(':');
|
||||
|
||||
var childProperty = property.FindPropertyRelative(propertyName.ToString());
|
||||
if (childProperty == null)
|
||||
throw new ArgumentException($"Cannot find property '{propertyName}' in {property}", nameof(property));
|
||||
|
||||
RestoreFromJson(childProperty, ref parser);
|
||||
parser.ParseToken(',');
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (property.propertyType)
|
||||
{
|
||||
case SerializedPropertyType.Float:
|
||||
{
|
||||
parser.ParseNumber(out var num);
|
||||
property.floatValue = (float)num.ToDouble();
|
||||
break;
|
||||
}
|
||||
|
||||
case SerializedPropertyType.String:
|
||||
{
|
||||
parser.ParseStringValue(out var str);
|
||||
property.stringValue = str.ToString();
|
||||
break;
|
||||
}
|
||||
|
||||
case SerializedPropertyType.Boolean:
|
||||
{
|
||||
parser.ParseBooleanValue(out var b);
|
||||
property.boolValue = b.ToBoolean();
|
||||
break;
|
||||
}
|
||||
|
||||
case SerializedPropertyType.Enum:
|
||||
case SerializedPropertyType.Integer:
|
||||
{
|
||||
parser.ParseNumber(out var num);
|
||||
property.intValue = (int)num.ToInteger();
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new NotImplementedException(
|
||||
$"Restoring property value of type {property.propertyType} (property: {property})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<SerializedProperty> GetChildren(this SerializedProperty property)
|
||||
{
|
||||
if (!property.hasChildren)
|
||||
yield break;
|
||||
|
||||
using (var iter = property.Copy())
|
||||
{
|
||||
var end = iter.GetEndProperty(true);
|
||||
|
||||
// Go to first child.
|
||||
if (!iter.Next(true))
|
||||
yield break; // Shouldn't happen; we've already established we have children.
|
||||
|
||||
// Iterate over children.
|
||||
while (!SerializedProperty.EqualContents(iter, end))
|
||||
{
|
||||
yield return iter;
|
||||
if (!iter.Next(false))
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static FieldInfo GetField(this SerializedProperty property)
|
||||
{
|
||||
var objectType = property.serializedObject.targetObject.GetType();
|
||||
var currentSerializableType = objectType;
|
||||
var pathComponents = property.propertyPath.Split('.');
|
||||
|
||||
FieldInfo result = null;
|
||||
foreach (var component in pathComponents)
|
||||
{
|
||||
// Handle arrays. They are followed by "Array" and "data[N]" elements.
|
||||
if (result != null && currentSerializableType.IsArray)
|
||||
{
|
||||
if (component == "Array")
|
||||
continue;
|
||||
|
||||
if (component.StartsWith("data["))
|
||||
{
|
||||
currentSerializableType = currentSerializableType.GetElementType();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
result = currentSerializableType.GetField(component,
|
||||
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy);
|
||||
if (result == null)
|
||||
return null;
|
||||
currentSerializableType = result.FieldType;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static Type GetFieldType(this SerializedProperty property)
|
||||
{
|
||||
return GetField(property)?.FieldType;
|
||||
}
|
||||
|
||||
public static void SetStringValue(this SerializedProperty property, string propertyName, string value)
|
||||
{
|
||||
var propertyRelative = property?.FindPropertyRelative(propertyName);
|
||||
if (propertyRelative != null)
|
||||
propertyRelative.stringValue = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // UNITY_EDITOR
|
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e22caa7f1befe4d4db80921ba3cdabdb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -0,0 +1,146 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
internal static class SerializedPropertyLinqExtensions
|
||||
{
|
||||
public static IEnumerable<T> Select<T>(this SerializedProperty property, Func<SerializedProperty, T> selector)
|
||||
{
|
||||
if (property == null)
|
||||
throw new ArgumentNullException(nameof(property));
|
||||
|
||||
if (selector == null)
|
||||
throw new ArgumentNullException(nameof(selector));
|
||||
|
||||
if (property.isArray == false)
|
||||
yield break;
|
||||
|
||||
for (var i = 0; i < property.arraySize; i++)
|
||||
{
|
||||
yield return selector(property.GetArrayElementAtIndex(i));
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<SerializedProperty> Where(this SerializedProperty property,
|
||||
Func<SerializedProperty, bool> predicate)
|
||||
{
|
||||
if (property == null)
|
||||
throw new ArgumentNullException(nameof(property));
|
||||
|
||||
if (predicate == null)
|
||||
throw new ArgumentNullException(nameof(predicate));
|
||||
|
||||
if (property.isArray == false)
|
||||
yield break;
|
||||
|
||||
for (var i = 0; i < property.arraySize; i++)
|
||||
{
|
||||
var element = property.GetArrayElementAtIndex(i);
|
||||
if (predicate(element))
|
||||
yield return element;
|
||||
}
|
||||
}
|
||||
|
||||
public static SerializedProperty FindLast(this SerializedProperty property, Func<SerializedProperty, bool> predicate)
|
||||
{
|
||||
Debug.Assert(predicate != null, "Missing predicate for FindLast function.");
|
||||
Debug.Assert(property != null, "SerializedProperty missing for FindLast function.");
|
||||
|
||||
if (property.isArray == false)
|
||||
return null;
|
||||
|
||||
for (int i = property.arraySize - 1; i >= 0; i--)
|
||||
{
|
||||
var element = property.GetArrayElementAtIndex(i);
|
||||
if (predicate(element))
|
||||
return element;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static SerializedProperty FirstOrDefault(this SerializedProperty property)
|
||||
{
|
||||
if (property == null)
|
||||
throw new ArgumentNullException(nameof(property));
|
||||
|
||||
if (property.isArray == false || property.arraySize == 0)
|
||||
return null;
|
||||
|
||||
return property.GetArrayElementAtIndex(0);
|
||||
}
|
||||
|
||||
public static SerializedProperty FirstOrDefault(this SerializedProperty property,
|
||||
Func<SerializedProperty, bool> predicate)
|
||||
{
|
||||
if (property == null)
|
||||
throw new ArgumentNullException(nameof(property));
|
||||
|
||||
if (predicate == null)
|
||||
throw new ArgumentNullException(nameof(predicate));
|
||||
|
||||
if (property.isArray == false)
|
||||
return null;
|
||||
|
||||
for (var i = 0; i < property.arraySize; i++)
|
||||
{
|
||||
var arrayElementAtIndex = property.GetArrayElementAtIndex(i);
|
||||
if (predicate(arrayElementAtIndex) == false)
|
||||
continue;
|
||||
|
||||
return arrayElementAtIndex;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static IEnumerable<SerializedProperty> Skip(this SerializedProperty property, int count)
|
||||
{
|
||||
if (property == null)
|
||||
throw new ArgumentNullException(nameof(property));
|
||||
|
||||
if (count < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(count));
|
||||
|
||||
return SkipIterator(property, count);
|
||||
}
|
||||
|
||||
public static IEnumerable<SerializedProperty> Take(this SerializedProperty property, int count)
|
||||
{
|
||||
if (property == null)
|
||||
throw new ArgumentNullException(nameof(property));
|
||||
|
||||
if (count < 0 || count > property.arraySize)
|
||||
throw new ArgumentOutOfRangeException(nameof(count));
|
||||
|
||||
return TakeIterator(property, count);
|
||||
}
|
||||
|
||||
private static IEnumerable<SerializedProperty> SkipIterator(SerializedProperty source, int count)
|
||||
{
|
||||
var enumerator = source.GetEnumerator();
|
||||
while (count > 0 && enumerator.MoveNext()) count--;
|
||||
if (count <= 0)
|
||||
{
|
||||
while (enumerator.MoveNext())
|
||||
yield return (SerializedProperty)enumerator.Current;
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<SerializedProperty> TakeIterator(SerializedProperty source, int count)
|
||||
{
|
||||
if (count > 0)
|
||||
{
|
||||
foreach (SerializedProperty element in source)
|
||||
{
|
||||
yield return element;
|
||||
if (--count == 0) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c018e4b76d0cd35459e780d36eb69c45
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -0,0 +1,51 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using UnityEditor.IMGUI.Controls;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for working with tree views.
|
||||
/// </summary>
|
||||
/// <seealso cref="TreeView"/>
|
||||
internal static class TreeViewHelpers
|
||||
{
|
||||
public static TItem TryFindItemInHierarchy<TItem>(this TreeViewItem item)
|
||||
where TItem : TreeViewItem
|
||||
{
|
||||
while (item != null)
|
||||
{
|
||||
if (item is TItem result)
|
||||
return result;
|
||||
item = item.parent;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static bool IsParentOf(this TreeViewItem parent, TreeViewItem child)
|
||||
{
|
||||
if (parent == null)
|
||||
throw new ArgumentNullException(nameof(parent));
|
||||
if (child == null)
|
||||
throw new ArgumentNullException(nameof(child));
|
||||
|
||||
do
|
||||
{
|
||||
child = child.parent;
|
||||
}
|
||||
while (child != null && child != parent);
|
||||
return child != null;
|
||||
}
|
||||
|
||||
public static void ExpandChildren(this TreeView treeView, TreeViewItem item)
|
||||
{
|
||||
if (!item.hasChildren)
|
||||
return;
|
||||
|
||||
foreach (var child in item.children)
|
||||
treeView.SetExpanded(child.id, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // UNITY_EDITOR
|
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 60f36b88774fb4ed685661de123d7449
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
Reference in New Issue
Block a user