UI 자동화를 위해 바인딩 기능 구현
- 유니티 에셋 인증 오류로 meta 재생성
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.ToLowerInvariant().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.ToLowerInvariant().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:
|
||||
@@ -0,0 +1,574 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Callbacks;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
internal class AdvancedDropdownWindow : EditorWindow
|
||||
{
|
||||
private static readonly float kBorderThickness = 1f;
|
||||
private static readonly float kRightMargin = 13f;
|
||||
|
||||
private AdvancedDropdownGUI m_Gui;
|
||||
private AdvancedDropdownDataSource m_DataSource;
|
||||
private AdvancedDropdownState m_State;
|
||||
|
||||
private AdvancedDropdownItem m_CurrentlyRenderedTree;
|
||||
|
||||
protected AdvancedDropdownItem renderedTreeItem => m_CurrentlyRenderedTree;
|
||||
|
||||
private AdvancedDropdownItem m_AnimationTree;
|
||||
private float m_NewAnimTarget;
|
||||
private long m_LastTime;
|
||||
private bool m_ScrollToSelected = true;
|
||||
private float m_InitialSelectionPosition;
|
||||
////FIXME: looks like a bug?
|
||||
#pragma warning disable CS0649
|
||||
private Rect m_ButtonRectScreenPos;
|
||||
private Stack<AdvancedDropdownItem> m_ViewsStack = new Stack<AdvancedDropdownItem>();
|
||||
private bool m_DirtyList = true;
|
||||
|
||||
private string m_Search = "";
|
||||
private bool hasSearch => !string.IsNullOrEmpty(m_Search);
|
||||
|
||||
protected internal string searchString
|
||||
{
|
||||
get => m_Search;
|
||||
set
|
||||
{
|
||||
var isNewSearch = string.IsNullOrEmpty(m_Search) && !string.IsNullOrEmpty(value);
|
||||
m_Search = value;
|
||||
m_DataSource.RebuildSearch(m_Search);
|
||||
m_CurrentlyRenderedTree = m_DataSource.mainTree;
|
||||
if (hasSearch)
|
||||
{
|
||||
m_CurrentlyRenderedTree = m_DataSource.searchTree;
|
||||
if (isNewSearch || state.GetSelectedIndex(m_CurrentlyRenderedTree) < 0)
|
||||
state.SetSelectedIndex(m_CurrentlyRenderedTree, 0);
|
||||
m_ViewsStack.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal bool m_ShowHeader = true;
|
||||
internal bool showHeader
|
||||
{
|
||||
get => m_ShowHeader;
|
||||
set => m_ShowHeader = value;
|
||||
}
|
||||
internal bool m_Searchable = true;
|
||||
internal bool searchable
|
||||
{
|
||||
get => m_Searchable;
|
||||
set => m_Searchable = value;
|
||||
}
|
||||
internal bool m_closeOnSelection = true;
|
||||
internal bool closeOnSelection
|
||||
{
|
||||
get => m_closeOnSelection;
|
||||
set => m_closeOnSelection = value;
|
||||
}
|
||||
|
||||
protected virtual bool isSearchFieldDisabled { get; set; }
|
||||
|
||||
protected bool m_SetInitialSelectionPosition = true;
|
||||
|
||||
public AdvancedDropdownWindow()
|
||||
{
|
||||
m_InitialSelectionPosition = 0f;
|
||||
}
|
||||
|
||||
protected virtual bool setInitialSelectionPosition => m_SetInitialSelectionPosition;
|
||||
|
||||
protected internal AdvancedDropdownState state
|
||||
{
|
||||
get => m_State;
|
||||
set => m_State = value;
|
||||
}
|
||||
|
||||
protected internal AdvancedDropdownGUI gui
|
||||
{
|
||||
get => m_Gui;
|
||||
set => m_Gui = value;
|
||||
}
|
||||
|
||||
protected internal AdvancedDropdownDataSource dataSource
|
||||
{
|
||||
get => m_DataSource;
|
||||
set => m_DataSource = value;
|
||||
}
|
||||
|
||||
public event Action<AdvancedDropdownWindow> windowClosed;
|
||||
public event Action windowDestroyed;
|
||||
public event Action<AdvancedDropdownItem> selectionChanged;
|
||||
|
||||
protected virtual void OnEnable()
|
||||
{
|
||||
m_DirtyList = true;
|
||||
}
|
||||
|
||||
protected virtual void OnDestroy()
|
||||
{
|
||||
// This window sets 'editingTextField = true' continuously, through EditorGUI.FocusTextInControl(),
|
||||
// for the searchfield in its AdvancedDropdownGUI so here we ensure to clean up. This fixes the issue that
|
||||
// EditorGUI.IsEditingTextField() was returning true after e.g the Add Component Menu closes
|
||||
EditorGUIUtility.editingTextField = false;
|
||||
GUIUtility.keyboardControl = 0;
|
||||
windowDestroyed?.Invoke();
|
||||
}
|
||||
|
||||
public static T CreateAndInit<T>(Rect rect, AdvancedDropdownState state) where T : AdvancedDropdownWindow
|
||||
{
|
||||
var instance = CreateInstance<T>();
|
||||
instance.m_State = state;
|
||||
instance.Init(rect);
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void Init(Rect buttonRect)
|
||||
{
|
||||
var screenPoint = GUIUtility.GUIToScreenPoint(new Vector2(buttonRect.x, buttonRect.y));
|
||||
m_ButtonRectScreenPos.x = screenPoint.x;
|
||||
m_ButtonRectScreenPos.y = screenPoint.y;
|
||||
|
||||
if (m_State == null)
|
||||
m_State = new AdvancedDropdownState();
|
||||
if (m_DataSource == null)
|
||||
m_DataSource = new MultiLevelDataSource();
|
||||
if (m_Gui == null)
|
||||
m_Gui = new AdvancedDropdownGUI();
|
||||
m_Gui.state = m_State;
|
||||
m_Gui.Init();
|
||||
|
||||
// Has to be done before calling Show / ShowWithMode
|
||||
screenPoint = GUIUtility.GUIToScreenPoint(new Vector2(buttonRect.x, buttonRect.y));
|
||||
buttonRect.x = screenPoint.x;
|
||||
buttonRect.y = screenPoint.y;
|
||||
|
||||
OnDirtyList();
|
||||
m_CurrentlyRenderedTree = hasSearch ? m_DataSource.searchTree : m_DataSource.mainTree;
|
||||
ShowAsDropDown(buttonRect, CalculateWindowSize(m_ButtonRectScreenPos, out var requiredDropdownSize));
|
||||
|
||||
// If the dropdown is as height as the screen height, give it some margin
|
||||
if (position.height < requiredDropdownSize.y)
|
||||
{
|
||||
var pos = position;
|
||||
pos.y += 5;
|
||||
pos.height -= 10;
|
||||
position = pos;
|
||||
}
|
||||
|
||||
if (setInitialSelectionPosition)
|
||||
{
|
||||
m_InitialSelectionPosition = m_Gui.GetSelectionHeight(m_DataSource, buttonRect);
|
||||
}
|
||||
|
||||
wantsMouseMove = true;
|
||||
SetSelectionFromState();
|
||||
}
|
||||
|
||||
void SetSelectionFromState()
|
||||
{
|
||||
var selectedIndex = m_State.GetSelectedIndex(m_CurrentlyRenderedTree);
|
||||
while (selectedIndex >= 0)
|
||||
{
|
||||
var child = m_State.GetSelectedChild(m_CurrentlyRenderedTree);
|
||||
if (child == null)
|
||||
break;
|
||||
selectedIndex = m_State.GetSelectedIndex(child);
|
||||
if (selectedIndex < 0)
|
||||
break;
|
||||
m_ViewsStack.Push(m_CurrentlyRenderedTree);
|
||||
m_CurrentlyRenderedTree = child;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual Vector2 CalculateWindowSize(Rect buttonRect, out Vector2 requiredDropdownSize)
|
||||
{
|
||||
requiredDropdownSize = m_Gui.CalculateContentSize(m_DataSource);
|
||||
// Add 1 pixel for each border
|
||||
requiredDropdownSize.x += kBorderThickness * 2;
|
||||
requiredDropdownSize.y += kBorderThickness * 2;
|
||||
requiredDropdownSize.x += kRightMargin;
|
||||
|
||||
requiredDropdownSize.y += m_Gui.searchHeight;
|
||||
|
||||
if (showHeader)
|
||||
{
|
||||
requiredDropdownSize.y += m_Gui.headerHeight;
|
||||
}
|
||||
|
||||
requiredDropdownSize.y = Mathf.Clamp(requiredDropdownSize.y, minSize.y, maxSize.y);
|
||||
|
||||
var adjustedButtonRect = buttonRect;
|
||||
adjustedButtonRect.y = 0;
|
||||
adjustedButtonRect.height = requiredDropdownSize.y;
|
||||
|
||||
// Stretch to the width of the button
|
||||
if (requiredDropdownSize.x < buttonRect.width)
|
||||
{
|
||||
requiredDropdownSize.x = buttonRect.width;
|
||||
}
|
||||
// Apply minimum size
|
||||
if (requiredDropdownSize.x < minSize.x)
|
||||
{
|
||||
requiredDropdownSize.x = minSize.x;
|
||||
}
|
||||
if (requiredDropdownSize.y < minSize.y)
|
||||
{
|
||||
requiredDropdownSize.y = minSize.y;
|
||||
}
|
||||
|
||||
return requiredDropdownSize;
|
||||
}
|
||||
|
||||
internal void OnGUI()
|
||||
{
|
||||
m_Gui.BeginDraw(this);
|
||||
|
||||
GUI.Label(new Rect(0, 0, position.width, position.height), GUIContent.none, Styles.background);
|
||||
|
||||
if (m_DirtyList)
|
||||
{
|
||||
OnDirtyList();
|
||||
}
|
||||
|
||||
HandleKeyboard();
|
||||
if (searchable)
|
||||
OnGUISearch();
|
||||
|
||||
if (m_NewAnimTarget != 0 && Event.current.type == EventType.Layout)
|
||||
{
|
||||
var now = DateTime.Now.Ticks;
|
||||
var deltaTime = (now - m_LastTime) / (float)TimeSpan.TicksPerSecond;
|
||||
m_LastTime = now;
|
||||
|
||||
m_NewAnimTarget = Mathf.MoveTowards(m_NewAnimTarget, 0, deltaTime * 4);
|
||||
|
||||
if (m_NewAnimTarget == 0)
|
||||
{
|
||||
m_AnimationTree = null;
|
||||
}
|
||||
Repaint();
|
||||
}
|
||||
|
||||
var anim = m_NewAnimTarget;
|
||||
// Smooth the animation
|
||||
anim = Mathf.Floor(anim) + Mathf.SmoothStep(0, 1, Mathf.Repeat(anim, 1));
|
||||
|
||||
if (anim == 0)
|
||||
{
|
||||
DrawDropdown(0, m_CurrentlyRenderedTree);
|
||||
}
|
||||
else if (anim < 0)
|
||||
{
|
||||
// Go to parent
|
||||
// m_NewAnimTarget goes -1 -> 0
|
||||
DrawDropdown(anim, m_CurrentlyRenderedTree);
|
||||
DrawDropdown(anim + 1, m_AnimationTree);
|
||||
}
|
||||
else // > 0
|
||||
{
|
||||
// Go to child
|
||||
// m_NewAnimTarget 1 -> 0
|
||||
DrawDropdown(anim - 1, m_AnimationTree);
|
||||
DrawDropdown(anim, m_CurrentlyRenderedTree);
|
||||
}
|
||||
|
||||
m_Gui.EndDraw(this);
|
||||
}
|
||||
|
||||
public void ReloadData()
|
||||
{
|
||||
OnDirtyList();
|
||||
}
|
||||
|
||||
private void OnDirtyList()
|
||||
{
|
||||
m_DirtyList = false;
|
||||
m_DataSource.ReloadData();
|
||||
if (hasSearch)
|
||||
{
|
||||
m_DataSource.RebuildSearch(searchString);
|
||||
if (state.GetSelectedIndex(m_CurrentlyRenderedTree) < 0)
|
||||
{
|
||||
state.SetSelectedIndex(m_CurrentlyRenderedTree, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGUISearch()
|
||||
{
|
||||
m_Gui.DrawSearchField(isSearchFieldDisabled, m_Search, (newSearch) =>
|
||||
{
|
||||
searchString = newSearch;
|
||||
});
|
||||
}
|
||||
|
||||
private void HandleKeyboard()
|
||||
{
|
||||
var evt = Event.current;
|
||||
if (evt.type == EventType.KeyDown)
|
||||
{
|
||||
// Special handling when in new script panel
|
||||
if (SpecialKeyboardHandling(evt))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Always do these
|
||||
if (evt.keyCode == KeyCode.DownArrow)
|
||||
{
|
||||
m_State.MoveDownSelection(m_CurrentlyRenderedTree);
|
||||
m_ScrollToSelected = true;
|
||||
evt.Use();
|
||||
}
|
||||
if (evt.keyCode == KeyCode.UpArrow)
|
||||
{
|
||||
m_State.MoveUpSelection(m_CurrentlyRenderedTree);
|
||||
m_ScrollToSelected = true;
|
||||
evt.Use();
|
||||
}
|
||||
if (evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.KeypadEnter)
|
||||
{
|
||||
var selected = m_State.GetSelectedChild(m_CurrentlyRenderedTree);
|
||||
if (selected != null)
|
||||
{
|
||||
if (selected.children.Any())
|
||||
{
|
||||
GoToChild();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (selectionChanged != null)
|
||||
{
|
||||
selectionChanged(m_State.GetSelectedChild(m_CurrentlyRenderedTree));
|
||||
}
|
||||
if (closeOnSelection)
|
||||
{
|
||||
CloseWindow();
|
||||
}
|
||||
}
|
||||
}
|
||||
evt.Use();
|
||||
}
|
||||
|
||||
// Do these if we're not in search mode
|
||||
if (!hasSearch)
|
||||
{
|
||||
if (evt.keyCode == KeyCode.LeftArrow || evt.keyCode == KeyCode.Backspace)
|
||||
{
|
||||
GoToParent();
|
||||
evt.Use();
|
||||
}
|
||||
if (evt.keyCode == KeyCode.RightArrow)
|
||||
{
|
||||
var idx = m_State.GetSelectedIndex(m_CurrentlyRenderedTree);
|
||||
if (idx > -1 && m_CurrentlyRenderedTree.children.ElementAt(idx).children.Any())
|
||||
{
|
||||
GoToChild();
|
||||
}
|
||||
evt.Use();
|
||||
}
|
||||
if (evt.keyCode == KeyCode.Escape)
|
||||
{
|
||||
Close();
|
||||
evt.Use();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseWindow()
|
||||
{
|
||||
windowClosed?.Invoke(this);
|
||||
Close();
|
||||
}
|
||||
|
||||
internal AdvancedDropdownItem GetSelectedItem()
|
||||
{
|
||||
return m_State.GetSelectedChild(m_CurrentlyRenderedTree);
|
||||
}
|
||||
|
||||
protected virtual bool SpecialKeyboardHandling(Event evt)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
private void DrawDropdown(float anim, AdvancedDropdownItem group)
|
||||
{
|
||||
// Start of animated area (the part that moves left and right)
|
||||
var areaPosition = new Rect(0, 0, position.width, position.height);
|
||||
// Adjust to the frame
|
||||
areaPosition.x += kBorderThickness;
|
||||
areaPosition.y += kBorderThickness;
|
||||
areaPosition.height -= kBorderThickness * 2;
|
||||
areaPosition.width -= kBorderThickness * 2;
|
||||
|
||||
GUILayout.BeginArea(m_Gui.GetAnimRect(areaPosition, anim));
|
||||
// Header
|
||||
if (showHeader)
|
||||
m_Gui.DrawHeader(group, GoToParent, m_ViewsStack.Count > 0);
|
||||
|
||||
DrawList(group);
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
|
||||
private void DrawList(AdvancedDropdownItem item)
|
||||
{
|
||||
// Start of scroll view list
|
||||
m_State.SetScrollState(item, GUILayout.BeginScrollView(m_State.GetScrollState(item), GUIStyle.none, GUI.skin.verticalScrollbar));
|
||||
EditorGUIUtility.SetIconSize(m_Gui.iconSize);
|
||||
Rect selectedRect = new Rect();
|
||||
for (var i = 0; i < item.children.Count(); i++)
|
||||
{
|
||||
var child = item.children.ElementAt(i);
|
||||
var selected = m_State.GetSelectedIndex(item) == i;
|
||||
|
||||
if (child.IsSeparator())
|
||||
{
|
||||
GUIHelpers.DrawLineSeparator(child.name);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_Gui.DrawItem(child, child.name, child.icon, child.enabled, child.children.Any(), selected, hasSearch);
|
||||
}
|
||||
|
||||
var r = GUILayoutUtility.GetLastRect();
|
||||
if (selected)
|
||||
selectedRect = r;
|
||||
|
||||
// Skip input handling for the tree used for animation
|
||||
if (item != m_CurrentlyRenderedTree)
|
||||
continue;
|
||||
|
||||
// Select the element the mouse cursor is over.
|
||||
// Only do it on mouse move - keyboard controls are allowed to overwrite this until the next time the mouse moves.
|
||||
if ((Event.current.type == EventType.MouseMove || Event.current.type == EventType.MouseDrag) && child.enabled)
|
||||
{
|
||||
if (!selected && r.Contains(Event.current.mousePosition))
|
||||
{
|
||||
m_State.SetSelectedIndex(item, i);
|
||||
Event.current.Use();
|
||||
}
|
||||
}
|
||||
if (Event.current.type == EventType.MouseUp && r.Contains(Event.current.mousePosition) && child.enabled)
|
||||
{
|
||||
m_State.SetSelectedIndex(item, i);
|
||||
var selectedChild = m_State.GetSelectedChild(item);
|
||||
if (selectedChild.children.Any())
|
||||
{
|
||||
GoToChild();
|
||||
}
|
||||
else if (!selectedChild.IsSeparator())
|
||||
{
|
||||
selectionChanged?.Invoke(selectedChild);
|
||||
if (closeOnSelection)
|
||||
{
|
||||
CloseWindow();
|
||||
GUIUtility.ExitGUI();
|
||||
}
|
||||
}
|
||||
Event.current.Use();
|
||||
}
|
||||
}
|
||||
EditorGUIUtility.SetIconSize(Vector2.zero);
|
||||
GUILayout.EndScrollView();
|
||||
|
||||
// Scroll to selected on windows creation
|
||||
if (m_ScrollToSelected && m_InitialSelectionPosition != 0)
|
||||
{
|
||||
var diffOfPopupAboveTheButton = m_ButtonRectScreenPos.y - position.y;
|
||||
diffOfPopupAboveTheButton -= m_Gui.searchHeight + m_Gui.headerHeight;
|
||||
m_State.SetScrollState(item, new Vector2(0, m_InitialSelectionPosition - diffOfPopupAboveTheButton));
|
||||
m_ScrollToSelected = false;
|
||||
m_InitialSelectionPosition = 0;
|
||||
}
|
||||
// Scroll to show selected
|
||||
else if (m_ScrollToSelected && Event.current.type == EventType.Repaint)
|
||||
{
|
||||
m_ScrollToSelected = false;
|
||||
Rect scrollRect = GUILayoutUtility.GetLastRect();
|
||||
if (selectedRect.yMax - scrollRect.height > m_State.GetScrollState(item).y)
|
||||
{
|
||||
m_State.SetScrollState(item, new Vector2(0, selectedRect.yMax - scrollRect.height));
|
||||
Repaint();
|
||||
}
|
||||
if (selectedRect.y < m_State.GetScrollState(item).y)
|
||||
{
|
||||
m_State.SetScrollState(item, new Vector2(0, selectedRect.y));
|
||||
Repaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void GoToParent()
|
||||
{
|
||||
if (m_ViewsStack.Count == 0)
|
||||
return;
|
||||
m_LastTime = DateTime.Now.Ticks;
|
||||
if (m_NewAnimTarget > 0)
|
||||
m_NewAnimTarget = -1 + m_NewAnimTarget;
|
||||
else
|
||||
m_NewAnimTarget = -1;
|
||||
m_AnimationTree = m_CurrentlyRenderedTree;
|
||||
var parentItem = m_ViewsStack.Pop();
|
||||
|
||||
m_State.ClearSelectionOnItem(m_CurrentlyRenderedTree);
|
||||
|
||||
if (parentItem != null)
|
||||
{
|
||||
var suggestedIndex = parentItem.GetIndexOfChild(m_CurrentlyRenderedTree);
|
||||
m_State.SetSelectionOnItem(parentItem, suggestedIndex);
|
||||
}
|
||||
|
||||
m_CurrentlyRenderedTree = parentItem;
|
||||
}
|
||||
|
||||
private void GoToChild()
|
||||
{
|
||||
m_ViewsStack.Push(m_CurrentlyRenderedTree);
|
||||
m_LastTime = DateTime.Now.Ticks;
|
||||
if (m_NewAnimTarget < 0)
|
||||
m_NewAnimTarget = 1 + m_NewAnimTarget;
|
||||
else
|
||||
m_NewAnimTarget = 1;
|
||||
m_AnimationTree = m_CurrentlyRenderedTree;
|
||||
m_CurrentlyRenderedTree = m_State.GetSelectedChild(m_CurrentlyRenderedTree);
|
||||
}
|
||||
|
||||
[DidReloadScripts]
|
||||
private static void OnScriptReload()
|
||||
{
|
||||
CloseAllOpenWindows<AdvancedDropdownWindow>();
|
||||
}
|
||||
|
||||
protected static void CloseAllOpenWindows<T>()
|
||||
{
|
||||
var windows = Resources.FindObjectsOfTypeAll(typeof(T));
|
||||
foreach (var window in windows)
|
||||
{
|
||||
try
|
||||
{
|
||||
((EditorWindow)window).Close();
|
||||
}
|
||||
catch
|
||||
{
|
||||
DestroyImmediate(window);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class Styles
|
||||
{
|
||||
public static readonly GUIStyle background = "grey_border";
|
||||
public static readonly GUIStyle previewHeader = new GUIStyle(EditorStyles.label).WithPadding(new RectOffset(5, 5, 1, 2));
|
||||
public static readonly GUIStyle previewText = new GUIStyle(EditorStyles.wordWrappedLabel).WithPadding(new RectOffset(3, 5, 4, 4));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // UNITY_EDITOR
|
||||
@@ -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,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ed303b5629a047fa8c081261bb1993d1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,136 @@
|
||||
#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)
|
||||
{
|
||||
return FileUtil.GetPhysicalPath(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
#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(Mathf.CeilToInt(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:
|
||||
@@ -0,0 +1,726 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using UnityEditor;
|
||||
using UnityEngine.InputSystem.Layouts;
|
||||
using UnityEngine.InputSystem.Utilities;
|
||||
|
||||
////TODO: resolving bindings to actions needs to take "{id}" form into account
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
// Helpers for doctoring around in InputActions using SerializedProperties.
|
||||
internal static class InputActionSerializationHelpers
|
||||
{
|
||||
public static string GetName(SerializedProperty element)
|
||||
{
|
||||
using (var nameProperty = element.FindPropertyRelative("m_Name"))
|
||||
{
|
||||
Debug.Assert(nameProperty != null, $"Cannot find m_Name property in {element.propertyPath}");
|
||||
return nameProperty.stringValue;
|
||||
}
|
||||
}
|
||||
|
||||
public static Guid GetId(SerializedProperty element)
|
||||
{
|
||||
using (var idProperty = element.FindPropertyRelative("m_Id"))
|
||||
{
|
||||
Debug.Assert(idProperty != null, $"Cannot find m_Id property in {element.propertyPath}");
|
||||
return new Guid(idProperty.stringValue);
|
||||
}
|
||||
}
|
||||
|
||||
public static int GetIndex(SerializedProperty arrayProperty, Guid id)
|
||||
{
|
||||
Debug.Assert(arrayProperty.isArray, $"Property {arrayProperty.propertyPath} is not an array");
|
||||
for (var i = 0; i < arrayProperty.arraySize; ++i)
|
||||
{
|
||||
using (var element = arrayProperty.GetArrayElementAtIndex(i))
|
||||
if (GetId(element) == id)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static int GetIndex(SerializedProperty arrayProperty, SerializedProperty arrayElement)
|
||||
{
|
||||
return GetIndex(arrayProperty, GetId(arrayElement));
|
||||
}
|
||||
|
||||
public static int GetIndex(SerializedProperty arrayElement)
|
||||
{
|
||||
var arrayProperty = arrayElement.GetArrayPropertyFromElement();
|
||||
return GetIndex(arrayProperty, arrayElement);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starting with the given binding, find the composite that the binding belongs to. The given binding
|
||||
/// must either be the composite or be part of a composite.
|
||||
/// </summary>
|
||||
public static int GetCompositeStartIndex(SerializedProperty bindingArrayProperty, int bindingIndex)
|
||||
{
|
||||
for (var i = bindingIndex; i >= 0; --i)
|
||||
{
|
||||
var bindingProperty = bindingArrayProperty.GetArrayElementAtIndex(i);
|
||||
var bindingFlags = (InputBinding.Flags)bindingProperty.FindPropertyRelative("m_Flags").intValue;
|
||||
if ((bindingFlags & InputBinding.Flags.Composite) != 0)
|
||||
return i;
|
||||
Debug.Assert((bindingFlags & InputBinding.Flags.PartOfComposite) != 0,
|
||||
"Binding is neither a composite nor part of a composite");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static int GetCompositePartCount(SerializedProperty bindingArrayProperty, int bindingIndex)
|
||||
{
|
||||
var compositeStartIndex = GetCompositeStartIndex(bindingArrayProperty, bindingIndex);
|
||||
if (compositeStartIndex == -1)
|
||||
return 0;
|
||||
|
||||
var numParts = 0;
|
||||
for (var i = compositeStartIndex + 1; i < bindingArrayProperty.arraySize; ++i, ++numParts)
|
||||
{
|
||||
var bindingProperty = bindingArrayProperty.GetArrayElementAtIndex(i);
|
||||
var bindingFlags = (InputBinding.Flags)bindingProperty.FindPropertyRelative("m_Flags").intValue;
|
||||
if ((bindingFlags & InputBinding.Flags.PartOfComposite) == 0)
|
||||
break;
|
||||
}
|
||||
|
||||
return numParts;
|
||||
}
|
||||
|
||||
public static int ConvertBindingIndexOnActionToBindingIndexInArray(SerializedProperty bindingArrayProperty, string actionName,
|
||||
int bindingIndexOnAction)
|
||||
{
|
||||
var bindingCount = bindingArrayProperty.arraySize;
|
||||
var indexOnAction = -1;
|
||||
var indexInArray = 0;
|
||||
for (; indexInArray < bindingCount; ++indexInArray)
|
||||
{
|
||||
var bindingActionName = bindingArrayProperty.GetArrayElementAtIndex(indexInArray).FindPropertyRelative("m_Action")
|
||||
.stringValue;
|
||||
if (actionName.Equals(bindingActionName, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
++indexOnAction;
|
||||
if (indexOnAction == bindingIndexOnAction)
|
||||
return indexInArray;
|
||||
}
|
||||
}
|
||||
return indexInArray;
|
||||
}
|
||||
|
||||
public static void AddActionMaps(SerializedObject asset, SerializedObject sourceAsset)
|
||||
{
|
||||
Debug.Assert(asset.targetObject is InputActionAsset);
|
||||
Debug.Assert(sourceAsset.targetObject is InputActionAsset);
|
||||
|
||||
var mapArrayPropertySrc = sourceAsset.FindProperty(nameof(InputActionAsset.m_ActionMaps));
|
||||
var mapArrayPropertyDst = asset.FindProperty(nameof(InputActionAsset.m_ActionMaps));
|
||||
|
||||
// Copy each action map from source and paste at the end of destination
|
||||
var buffer = new StringBuilder();
|
||||
for (var i = 0; i < mapArrayPropertySrc.arraySize; ++i)
|
||||
{
|
||||
buffer.Clear();
|
||||
var mapProperty = mapArrayPropertySrc.GetArrayElementAtIndex(i);
|
||||
CopyPasteHelper.CopyItems(new List<SerializedProperty> {mapProperty}, buffer, typeof(InputActionMap), mapProperty);
|
||||
CopyPasteHelper.PasteItems(buffer.ToString(), new[] { mapArrayPropertyDst.arraySize - 1 }, mapArrayPropertyDst);
|
||||
}
|
||||
}
|
||||
|
||||
public static void AddControlSchemes(SerializedObject asset, SerializedObject sourceAsset)
|
||||
{
|
||||
Debug.Assert((asset.targetObject is InputActionAsset));
|
||||
Debug.Assert((sourceAsset.targetObject is InputActionAsset));
|
||||
|
||||
var src = sourceAsset.FindProperty(nameof(InputActionAsset.m_ControlSchemes));
|
||||
var dst = asset.FindProperty(nameof(InputActionAsset.m_ControlSchemes));
|
||||
|
||||
var buffer = new StringBuilder();
|
||||
src.CopyToJson(buffer, ignoreObjectReferences: true);
|
||||
dst.RestoreFromJson(buffer.ToString());
|
||||
}
|
||||
|
||||
public static SerializedProperty AddActionMap(SerializedObject asset, int index = -1)
|
||||
{
|
||||
if (!(asset.targetObject is InputActionAsset))
|
||||
throw new InvalidOperationException(
|
||||
$"Can only add action maps to InputActionAsset objects (actual object is {asset.targetObject}");
|
||||
|
||||
var mapArrayProperty = asset.FindProperty("m_ActionMaps");
|
||||
var name = FindUniqueName(mapArrayProperty, "New action map");
|
||||
if (index < 0)
|
||||
index = mapArrayProperty.arraySize;
|
||||
|
||||
mapArrayProperty.InsertArrayElementAtIndex(index);
|
||||
var mapProperty = mapArrayProperty.GetArrayElementAtIndex(index);
|
||||
|
||||
mapProperty.FindPropertyRelative("m_Name").stringValue = name;
|
||||
mapProperty.FindPropertyRelative("m_Id").stringValue = Guid.NewGuid().ToString();
|
||||
mapProperty.FindPropertyRelative("m_Actions").ClearArray();
|
||||
mapProperty.FindPropertyRelative("m_Bindings").ClearArray();
|
||||
// NB: This isn't always required: If there's already values in the mapArrayProperty, then inserting a new
|
||||
// element will duplicate the values from the adjacent element to the new element.
|
||||
// However, if the array has been emptied - i.e. if all action maps have been deleted -
|
||||
// then the m_Asset property is null, and needs setting here.
|
||||
if (mapProperty.FindPropertyRelative("m_Asset").objectReferenceValue == null)
|
||||
mapProperty.FindPropertyRelative("m_Asset").objectReferenceValue = asset.targetObject;
|
||||
|
||||
return mapProperty;
|
||||
}
|
||||
|
||||
public static void DeleteActionMap(SerializedObject asset, Guid id)
|
||||
{
|
||||
var mapArrayProperty = asset.FindProperty("m_ActionMaps");
|
||||
var mapIndex = GetIndex(mapArrayProperty, id);
|
||||
if (mapIndex == -1)
|
||||
throw new ArgumentException($"No map with id {id} in {asset}", nameof(id));
|
||||
mapArrayProperty.DeleteArrayElementAtIndex(mapIndex);
|
||||
}
|
||||
|
||||
public static void DeleteAllActionMaps(SerializedObject asset)
|
||||
{
|
||||
Debug.Assert(asset.targetObject is InputActionAsset);
|
||||
|
||||
var mapArrayProperty = asset.FindProperty("m_ActionMaps");
|
||||
while (mapArrayProperty.arraySize > 0)
|
||||
mapArrayProperty.DeleteArrayElementAtIndex(0);
|
||||
}
|
||||
|
||||
public static void MoveActionMap(SerializedObject asset, int fromIndex, int toIndex)
|
||||
{
|
||||
var mapArrayProperty = asset.FindProperty("m_ActionMaps");
|
||||
mapArrayProperty.MoveArrayElement(fromIndex, toIndex);
|
||||
}
|
||||
|
||||
public static void MoveAction(SerializedProperty actionMap, int fromIndex, int toIndex)
|
||||
{
|
||||
var actionArrayProperty = actionMap.FindPropertyRelative(nameof(InputActionMap.m_Actions));
|
||||
actionArrayProperty.MoveArrayElement(fromIndex, toIndex);
|
||||
}
|
||||
|
||||
public static void MoveBinding(SerializedProperty actionMap, int fromIndex, int toIndex)
|
||||
{
|
||||
var arrayProperty = actionMap.FindPropertyRelative(nameof(InputActionMap.m_Bindings));
|
||||
arrayProperty.MoveArrayElement(fromIndex, toIndex);
|
||||
}
|
||||
|
||||
// Append a new action to the end of the set.
|
||||
public static SerializedProperty AddAction(SerializedProperty actionMap, int index = -1)
|
||||
{
|
||||
var actionsArrayProperty = actionMap.FindPropertyRelative("m_Actions");
|
||||
if (index < 0)
|
||||
index = actionsArrayProperty.arraySize;
|
||||
|
||||
var actionName = FindUniqueName(actionsArrayProperty, "New action");
|
||||
|
||||
actionsArrayProperty.InsertArrayElementAtIndex(index);
|
||||
var actionProperty = actionsArrayProperty.GetArrayElementAtIndex(index);
|
||||
|
||||
actionProperty.FindPropertyRelative("m_Name").stringValue = actionName;
|
||||
actionProperty.FindPropertyRelative("m_Type").intValue = (int)InputActionType.Button; // Default to creating button actions.
|
||||
actionProperty.FindPropertyRelative("m_Id").stringValue = Guid.NewGuid().ToString();
|
||||
actionProperty.FindPropertyRelative("m_ExpectedControlType").stringValue = "Button";
|
||||
actionProperty.FindPropertyRelative("m_Flags").intValue = 0;
|
||||
actionProperty.FindPropertyRelative("m_Interactions").stringValue = "";
|
||||
actionProperty.FindPropertyRelative("m_Processors").stringValue = "";
|
||||
|
||||
return actionProperty;
|
||||
}
|
||||
|
||||
public static void DeleteActionAndBindings(SerializedProperty actionMap, Guid actionId)
|
||||
{
|
||||
using (var actionsArrayProperty = actionMap.FindPropertyRelative("m_Actions"))
|
||||
using (var bindingsArrayProperty = actionMap.FindPropertyRelative("m_Bindings"))
|
||||
{
|
||||
// Find index of action.
|
||||
var actionIndex = GetIndex(actionsArrayProperty, actionId);
|
||||
if (actionIndex == -1)
|
||||
throw new ArgumentException($"No action with ID {actionId} in {actionMap.propertyPath}",
|
||||
nameof(actionId));
|
||||
|
||||
using (var actionsProperty = actionsArrayProperty.GetArrayElementAtIndex(actionIndex))
|
||||
{
|
||||
var actionName = GetName(actionsProperty);
|
||||
var actionIdString = actionId.ToString();
|
||||
|
||||
// Delete all bindings that refer to the action by ID or name.
|
||||
for (var i = 0; i < bindingsArrayProperty.arraySize; ++i)
|
||||
{
|
||||
using (var bindingProperty = bindingsArrayProperty.GetArrayElementAtIndex(i))
|
||||
using (var bindingActionProperty = bindingProperty.FindPropertyRelative("m_Action"))
|
||||
{
|
||||
var targetAction = bindingActionProperty.stringValue;
|
||||
if (targetAction.Equals(actionName, StringComparison.InvariantCultureIgnoreCase) ||
|
||||
targetAction == actionIdString)
|
||||
{
|
||||
bindingsArrayProperty.DeleteArrayElementAtIndex(i);
|
||||
--i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
actionsArrayProperty.DeleteArrayElementAtIndex(actionIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// Equivalent to InputAction.AddBinding().
|
||||
public static SerializedProperty AddBinding(SerializedProperty actionProperty,
|
||||
SerializedProperty actionMapProperty = null, SerializedProperty afterBinding = null,
|
||||
string groups = "", string path = "", string name = "",
|
||||
string interactions = "", string processors = "",
|
||||
InputBinding.Flags flags = InputBinding.Flags.None)
|
||||
{
|
||||
var bindingsArrayProperty = actionMapProperty != null
|
||||
? actionMapProperty.FindPropertyRelative("m_Bindings")
|
||||
: actionProperty.FindPropertyRelative("m_SingletonActionBindings");
|
||||
var bindingsCount = bindingsArrayProperty.arraySize;
|
||||
var actionName = actionProperty.FindPropertyRelative("m_Name").stringValue;
|
||||
|
||||
int bindingIndex;
|
||||
if (afterBinding != null)
|
||||
{
|
||||
// If we're supposed to put the binding right after another binding, find the
|
||||
// binding's index. Also, if it's a composite, skip past all its parts.
|
||||
bindingIndex = GetIndex(bindingsArrayProperty, afterBinding);
|
||||
if (IsCompositeBinding(afterBinding))
|
||||
bindingIndex += GetCompositePartCount(bindingsArrayProperty, bindingIndex);
|
||||
++bindingIndex; // Put it *after* the binding.
|
||||
}
|
||||
else
|
||||
{
|
||||
// Find the index of the last binding for the action in the array.
|
||||
var indexOfLastBindingForAction = -1;
|
||||
for (var i = 0; i < bindingsCount; ++i)
|
||||
{
|
||||
var bindingProperty = bindingsArrayProperty.GetArrayElementAtIndex(i);
|
||||
var bindingActionName = bindingProperty.FindPropertyRelative("m_Action").stringValue;
|
||||
if (actionName.Equals(bindingActionName, StringComparison.InvariantCultureIgnoreCase))
|
||||
indexOfLastBindingForAction = i;
|
||||
}
|
||||
|
||||
// Insert after last binding or at end of array.
|
||||
bindingIndex = indexOfLastBindingForAction != -1 ? indexOfLastBindingForAction + 1 : bindingsCount;
|
||||
}
|
||||
|
||||
////TODO: bind using {id} rather than action name
|
||||
return AddBindingToBindingArray(bindingsArrayProperty,
|
||||
bindingIndex: bindingIndex,
|
||||
actionName: actionName,
|
||||
groups: groups,
|
||||
path: path,
|
||||
name: name,
|
||||
interactions: interactions,
|
||||
processors: processors,
|
||||
flags: flags);
|
||||
}
|
||||
|
||||
public static SerializedProperty AddBindingToBindingArray(SerializedProperty bindingsArrayProperty, int bindingIndex = -1,
|
||||
string actionName = "", string groups = "", string path = "", string name = "", string interactions = "", string processors = "",
|
||||
InputBinding.Flags flags = InputBinding.Flags.None)
|
||||
{
|
||||
Debug.Assert(bindingsArrayProperty != null);
|
||||
Debug.Assert(bindingsArrayProperty.isArray, "SerializedProperty is not an array of bindings");
|
||||
Debug.Assert(bindingIndex == -1 || (bindingIndex >= 0 && bindingIndex <= bindingsArrayProperty.arraySize));
|
||||
|
||||
if (bindingIndex == -1)
|
||||
bindingIndex = bindingsArrayProperty.arraySize;
|
||||
|
||||
bindingsArrayProperty.InsertArrayElementAtIndex(bindingIndex);
|
||||
|
||||
var newBindingProperty = bindingsArrayProperty.GetArrayElementAtIndex(bindingIndex);
|
||||
newBindingProperty.FindPropertyRelative("m_Path").stringValue = path;
|
||||
newBindingProperty.FindPropertyRelative("m_Groups").stringValue = groups;
|
||||
newBindingProperty.FindPropertyRelative("m_Interactions").stringValue = interactions;
|
||||
newBindingProperty.FindPropertyRelative("m_Processors").stringValue = processors;
|
||||
newBindingProperty.FindPropertyRelative("m_Flags").intValue = (int)flags;
|
||||
newBindingProperty.FindPropertyRelative("m_Action").stringValue = actionName;
|
||||
newBindingProperty.FindPropertyRelative("m_Name").stringValue = name;
|
||||
newBindingProperty.FindPropertyRelative("m_Id").stringValue = Guid.NewGuid().ToString();
|
||||
|
||||
////FIXME: this likely leaves m_Bindings in the map for singleton actions unsync'd in some cases
|
||||
|
||||
return newBindingProperty;
|
||||
}
|
||||
|
||||
public static void SetBindingPartName(SerializedProperty bindingProperty, string partName)
|
||||
{
|
||||
//expects beautified partName
|
||||
bindingProperty.FindPropertyRelative("m_Name").stringValue = partName;
|
||||
}
|
||||
|
||||
public static void ChangeBinding(SerializedProperty bindingProperty, string path = null, string groups = null,
|
||||
string interactions = null, string processors = null, string action = null)
|
||||
{
|
||||
// Path.
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
var pathProperty = bindingProperty.FindPropertyRelative("m_Path");
|
||||
pathProperty.stringValue = path;
|
||||
}
|
||||
|
||||
// Groups.
|
||||
if (!string.IsNullOrEmpty(groups))
|
||||
{
|
||||
var groupsProperty = bindingProperty.FindPropertyRelative("m_Groups");
|
||||
groupsProperty.stringValue = groups;
|
||||
}
|
||||
|
||||
// Interactions.
|
||||
if (!string.IsNullOrEmpty(interactions))
|
||||
{
|
||||
var interactionsProperty = bindingProperty.FindPropertyRelative("m_Interactions");
|
||||
interactionsProperty.stringValue = interactions;
|
||||
}
|
||||
|
||||
// Processors.
|
||||
if (!string.IsNullOrEmpty(processors))
|
||||
{
|
||||
var processorsProperty = bindingProperty.FindPropertyRelative("m_Processors");
|
||||
processorsProperty.stringValue = processors;
|
||||
}
|
||||
|
||||
// Action.
|
||||
if (!string.IsNullOrEmpty(action))
|
||||
{
|
||||
var actionProperty = bindingProperty.FindPropertyRelative("m_Action");
|
||||
actionProperty.stringValue = action;
|
||||
}
|
||||
}
|
||||
|
||||
public static void DeleteBinding(SerializedProperty binding, SerializedProperty actionMap)
|
||||
{
|
||||
var bindingsProperty = actionMap.FindPropertyRelative("m_Bindings");
|
||||
DeleteBinding(binding, bindingsProperty, binding.GetIndexOfArrayElement());
|
||||
}
|
||||
|
||||
private static void DeleteBinding(SerializedProperty bindingProperty, SerializedProperty bindingArrayProperty, int bindingIndex)
|
||||
{
|
||||
var bindingFlags = (InputBinding.Flags)bindingProperty.FindPropertyRelative("m_Flags").intValue;
|
||||
var isComposite = (bindingFlags & InputBinding.Flags.Composite) != 0;
|
||||
// If it's a composite, delete all its parts first.
|
||||
if (isComposite)
|
||||
{
|
||||
for (var partIndex = bindingIndex + 1; partIndex < bindingArrayProperty.arraySize;)
|
||||
{
|
||||
var part = bindingArrayProperty.GetArrayElementAtIndex(partIndex);
|
||||
var flags = (InputBinding.Flags)part.FindPropertyRelative("m_Flags").intValue;
|
||||
if ((flags & InputBinding.Flags.PartOfComposite) == 0)
|
||||
break;
|
||||
bindingArrayProperty.DeleteArrayElementAtIndex(partIndex);
|
||||
}
|
||||
}
|
||||
|
||||
bindingArrayProperty.DeleteArrayElementAtIndex(bindingIndex);
|
||||
}
|
||||
|
||||
public static void DeleteBinding(SerializedProperty bindingArrayProperty, Guid id)
|
||||
{
|
||||
var bindingIndex = GetIndex(bindingArrayProperty, id);
|
||||
var bindingProperty = bindingArrayProperty.GetArrayElementAtIndex(bindingIndex);
|
||||
DeleteBinding(bindingProperty, bindingArrayProperty, bindingIndex);
|
||||
}
|
||||
|
||||
public static void EnsureUniqueName(SerializedProperty arrayElement)
|
||||
{
|
||||
var arrayProperty = arrayElement.GetArrayPropertyFromElement();
|
||||
var arrayIndexOfElement = arrayElement.GetIndexOfArrayElement();
|
||||
var nameProperty = arrayElement.FindPropertyRelative("m_Name");
|
||||
var baseName = nameProperty.stringValue;
|
||||
nameProperty.stringValue = FindUniqueName(arrayProperty, baseName, ignoreIndex: arrayIndexOfElement);
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Justification = "False positive (possibly caused by lambda expression?).")]
|
||||
public static string FindUniqueName(SerializedProperty arrayProperty, string baseName, int ignoreIndex = -1)
|
||||
{
|
||||
return StringHelpers.MakeUniqueName(baseName,
|
||||
Enumerable.Range(0, arrayProperty.arraySize),
|
||||
index =>
|
||||
{
|
||||
if (index == ignoreIndex)
|
||||
return string.Empty;
|
||||
var elementProperty = arrayProperty.GetArrayElementAtIndex(index);
|
||||
var nameProperty = elementProperty.FindPropertyRelative("m_Name");
|
||||
if (nameProperty == null)
|
||||
throw new ArgumentException($"Cannot find m_Name property in elements of array",
|
||||
nameof(arrayProperty));
|
||||
return nameProperty.stringValue;
|
||||
});
|
||||
}
|
||||
|
||||
public static void AssignUniqueIDs(SerializedProperty element)
|
||||
{
|
||||
AssignUniqueID(element);
|
||||
foreach (var child in element.GetChildren())
|
||||
{
|
||||
if (!child.isArray)
|
||||
continue;
|
||||
|
||||
var fieldType = child.GetFieldType();
|
||||
if (fieldType == typeof(InputBinding[]) || fieldType == typeof(InputAction[]) ||
|
||||
fieldType == typeof(InputActionMap))
|
||||
{
|
||||
for (var i = 0; i < child.arraySize; ++i)
|
||||
using (var childElement = child.GetArrayElementAtIndex(i))
|
||||
AssignUniqueIDs(childElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssignUniqueID(SerializedProperty property)
|
||||
{
|
||||
var idProperty = property.FindPropertyRelative("m_Id");
|
||||
idProperty.stringValue = Guid.NewGuid().ToString();
|
||||
}
|
||||
|
||||
public static void RenameAction(SerializedProperty actionProperty, SerializedProperty actionMapProperty, string newName)
|
||||
{
|
||||
// Make sure name is unique.
|
||||
var actionsArrayProperty = actionMapProperty.FindPropertyRelative("m_Actions");
|
||||
var uniqueName = FindUniqueName(actionsArrayProperty, newName, actionProperty.GetIndexOfArrayElement());
|
||||
|
||||
// Update all bindings that refer to the action.
|
||||
var nameProperty = actionProperty.FindPropertyRelative("m_Name");
|
||||
var oldName = nameProperty.stringValue;
|
||||
var bindingsProperty = actionMapProperty.FindPropertyRelative("m_Bindings");
|
||||
for (var i = 0; i < bindingsProperty.arraySize; i++)
|
||||
{
|
||||
var element = bindingsProperty.GetArrayElementAtIndex(i);
|
||||
var actionNameProperty = element.FindPropertyRelative("m_Action");
|
||||
if (actionNameProperty.stringValue.Equals(oldName, StringComparison.InvariantCultureIgnoreCase))
|
||||
actionNameProperty.stringValue = uniqueName;
|
||||
}
|
||||
|
||||
// Update name.
|
||||
nameProperty.stringValue = uniqueName;
|
||||
}
|
||||
|
||||
public static void RenameActionMap(SerializedProperty actionMapProperty, string newName)
|
||||
{
|
||||
// Make sure name is unique in InputActionAsset.
|
||||
var assetObject = actionMapProperty.serializedObject;
|
||||
var mapsArrayProperty = assetObject.FindProperty("m_ActionMaps");
|
||||
var uniqueName = FindUniqueName(mapsArrayProperty, newName, actionMapProperty.GetIndexOfArrayElement());
|
||||
|
||||
// Assign to map.
|
||||
var nameProperty = actionMapProperty.FindPropertyRelative("m_Name");
|
||||
nameProperty.stringValue = uniqueName;
|
||||
}
|
||||
|
||||
public static void RenameComposite(SerializedProperty compositeGroupProperty, string newName)
|
||||
{
|
||||
var nameProperty = compositeGroupProperty.FindPropertyRelative("m_Name");
|
||||
nameProperty.stringValue = newName;
|
||||
}
|
||||
|
||||
public static SerializedProperty AddCompositeBinding(SerializedProperty actionProperty, SerializedProperty actionMapProperty,
|
||||
string compositeName, Type compositeType = null, string groups = "", bool addPartBindings = true)
|
||||
{
|
||||
var newProperty = AddBinding(actionProperty, actionMapProperty);
|
||||
newProperty.FindPropertyRelative("m_Name").stringValue = ObjectNames.NicifyVariableName(compositeName);
|
||||
newProperty.FindPropertyRelative("m_Path").stringValue = compositeName;
|
||||
newProperty.FindPropertyRelative("m_Flags").intValue = (int)InputBinding.Flags.Composite;
|
||||
|
||||
if (addPartBindings)
|
||||
{
|
||||
var fields = compositeType.GetFields(BindingFlags.GetField | BindingFlags.Public | BindingFlags.Instance);
|
||||
foreach (var field in fields)
|
||||
{
|
||||
// Skip fields that aren't marked with [InputControl] attribute.
|
||||
if (field.GetCustomAttribute<InputControlAttribute>(false) == null)
|
||||
continue;
|
||||
|
||||
var partProperty = AddBinding(actionProperty, actionMapProperty, groups: groups);
|
||||
partProperty.FindPropertyRelative("m_Name").stringValue = field.Name;
|
||||
partProperty.FindPropertyRelative("m_Flags").intValue = (int)InputBinding.Flags.PartOfComposite;
|
||||
}
|
||||
}
|
||||
|
||||
return newProperty;
|
||||
}
|
||||
|
||||
public static bool IsCompositeBinding(SerializedProperty bindingProperty)
|
||||
{
|
||||
using (var flagsProperty = bindingProperty.FindPropertyRelative("m_Flags"))
|
||||
{
|
||||
var flags = (InputBinding.Flags)flagsProperty.intValue;
|
||||
return (flags & InputBinding.Flags.Composite) != 0;
|
||||
}
|
||||
}
|
||||
|
||||
public static SerializedProperty ChangeCompositeBindingType(SerializedProperty bindingProperty,
|
||||
NameAndParameters nameAndParameters)
|
||||
{
|
||||
var bindingsArrayProperty = bindingProperty.GetArrayPropertyFromElement();
|
||||
Debug.Assert(bindingsArrayProperty != null, "SerializedProperty is not an array of bindings");
|
||||
var bindingIndex = bindingProperty.GetIndexOfArrayElement();
|
||||
|
||||
Debug.Assert(IsCompositeBinding(bindingProperty),
|
||||
$"Binding {bindingProperty.propertyPath} is not a composite");
|
||||
|
||||
// If the composite still has the default name, change it to the default
|
||||
// one for the new composite type.
|
||||
var pathProperty = bindingProperty.FindPropertyRelative("m_Path");
|
||||
var nameProperty = bindingProperty.FindPropertyRelative("m_Name");
|
||||
if (nameProperty.stringValue ==
|
||||
ObjectNames.NicifyVariableName(NameAndParameters.Parse(pathProperty.stringValue).name))
|
||||
nameProperty.stringValue = ObjectNames.NicifyVariableName(nameAndParameters.name);
|
||||
|
||||
pathProperty.stringValue = nameAndParameters.ToString();
|
||||
|
||||
// Adjust part bindings if we have information on the registered composite. If we don't have
|
||||
// a type, we don't know about the parts. In that case, leave part bindings untouched.
|
||||
var compositeType = InputBindingComposite.s_Composites.LookupTypeRegistration(nameAndParameters.name);
|
||||
if (compositeType != null)
|
||||
{
|
||||
var actionName = bindingProperty.FindPropertyRelative("m_Action").stringValue;
|
||||
|
||||
// Repurpose existing part bindings for the new composite or add any part bindings that
|
||||
// we're missing.
|
||||
var fields = compositeType.GetFields(BindingFlags.GetField | BindingFlags.Public | BindingFlags.Instance);
|
||||
var partIndex = 0;
|
||||
var partBindingsStartIndex = bindingIndex + 1;
|
||||
foreach (var field in fields)
|
||||
{
|
||||
// Skip fields that aren't marked with [InputControl] attribute.
|
||||
if (field.GetCustomAttribute<InputControlAttribute>(false) == null)
|
||||
continue;
|
||||
|
||||
// See if we can reuse an existing part binding.
|
||||
SerializedProperty partProperty = null;
|
||||
if (partBindingsStartIndex + partIndex < bindingsArrayProperty.arraySize)
|
||||
{
|
||||
////REVIEW: this should probably look up part bindings by name rather than going sequentially
|
||||
var element = bindingsArrayProperty.GetArrayElementAtIndex(partBindingsStartIndex + partIndex);
|
||||
if (((InputBinding.Flags)element.FindPropertyRelative("m_Flags").intValue & InputBinding.Flags.PartOfComposite) != 0)
|
||||
partProperty = element;
|
||||
}
|
||||
|
||||
// If not, insert a new binding.
|
||||
if (partProperty == null)
|
||||
{
|
||||
partProperty = AddBindingToBindingArray(bindingsArrayProperty, partBindingsStartIndex + partIndex,
|
||||
flags: InputBinding.Flags.PartOfComposite);
|
||||
}
|
||||
|
||||
// Initialize.
|
||||
partProperty.FindPropertyRelative("m_Name").stringValue = ObjectNames.NicifyVariableName(field.Name);
|
||||
partProperty.FindPropertyRelative("m_Action").stringValue = actionName;
|
||||
++partIndex;
|
||||
}
|
||||
|
||||
////REVIEW: when we allow adding the same part multiple times, we may want to do something smarter here
|
||||
// Delete extraneous part bindings.
|
||||
while (partBindingsStartIndex + partIndex < bindingsArrayProperty.arraySize)
|
||||
{
|
||||
var element = bindingsArrayProperty.GetArrayElementAtIndex(partBindingsStartIndex + partIndex);
|
||||
if (((InputBinding.Flags)element.FindPropertyRelative("m_Flags").intValue & InputBinding.Flags.PartOfComposite) == 0)
|
||||
break;
|
||||
|
||||
bindingsArrayProperty.DeleteArrayElementAtIndex(partBindingsStartIndex + partIndex);
|
||||
// No incrementing of partIndex.
|
||||
}
|
||||
}
|
||||
|
||||
return bindingProperty;
|
||||
}
|
||||
|
||||
public static void ReplaceBindingGroup(SerializedObject asset, string oldBindingGroup, string newBindingGroup, bool deleteOrphanedBindings = false)
|
||||
{
|
||||
var mapArrayProperty = asset.FindProperty("m_ActionMaps");
|
||||
var mapCount = mapArrayProperty.arraySize;
|
||||
|
||||
for (var k = 0; k < mapCount; ++k)
|
||||
{
|
||||
var actionMapProperty = mapArrayProperty.GetArrayElementAtIndex(k);
|
||||
var bindingsArrayProperty = actionMapProperty.FindPropertyRelative("m_Bindings");
|
||||
var bindingsCount = bindingsArrayProperty.arraySize;
|
||||
|
||||
for (var i = 0; i < bindingsCount; ++i)
|
||||
{
|
||||
var bindingProperty = bindingsArrayProperty.GetArrayElementAtIndex(i);
|
||||
var groupsProperty = bindingProperty.FindPropertyRelative("m_Groups");
|
||||
var groups = groupsProperty.stringValue;
|
||||
|
||||
// Ignore bindings not belonging to any control scheme.
|
||||
if (string.IsNullOrEmpty(groups))
|
||||
continue;
|
||||
|
||||
var groupsArray = groups.Split(InputBinding.Separator);
|
||||
var numGroups = groupsArray.LengthSafe();
|
||||
var didRename = false;
|
||||
for (var n = 0; n < numGroups; ++n)
|
||||
{
|
||||
if (string.Compare(groupsArray[n], oldBindingGroup, StringComparison.InvariantCultureIgnoreCase) != 0)
|
||||
continue;
|
||||
if (string.IsNullOrEmpty(newBindingGroup))
|
||||
{
|
||||
ArrayHelpers.EraseAt(ref groupsArray, n);
|
||||
--n;
|
||||
--numGroups;
|
||||
}
|
||||
else
|
||||
groupsArray[n] = newBindingGroup;
|
||||
didRename = true;
|
||||
}
|
||||
if (!didRename)
|
||||
continue;
|
||||
|
||||
if (groupsArray != null)
|
||||
groupsProperty.stringValue = string.Join(InputBinding.kSeparatorString, groupsArray);
|
||||
else
|
||||
{
|
||||
if (deleteOrphanedBindings)
|
||||
{
|
||||
// Binding no long belongs to any binding group. Delete it.
|
||||
bindingsArrayProperty.DeleteArrayElementAtIndex(i);
|
||||
--i;
|
||||
--bindingsCount;
|
||||
}
|
||||
else
|
||||
{
|
||||
groupsProperty.stringValue = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void RemoveUnusedBindingGroups(SerializedProperty binding, ReadOnlyArray<InputControlScheme> controlSchemes)
|
||||
{
|
||||
var groupsProperty = binding.FindPropertyRelative(nameof(InputBinding.m_Groups));
|
||||
groupsProperty.stringValue = string.Join(InputBinding.kSeparatorString,
|
||||
groupsProperty.stringValue
|
||||
.Split(InputBinding.Separator)
|
||||
.Where(g => controlSchemes.Any(c => c.bindingGroup.Equals(g, StringComparison.InvariantCultureIgnoreCase))));
|
||||
}
|
||||
|
||||
#region Control Schemes
|
||||
|
||||
public static void DeleteAllControlSchemes(SerializedObject asset)
|
||||
{
|
||||
var schemes = GetControlSchemesArray(asset);
|
||||
while (schemes.arraySize > 0)
|
||||
schemes.DeleteArrayElementAtIndex(0);
|
||||
}
|
||||
|
||||
public static int IndexOfControlScheme(SerializedProperty controlSchemeArray, string controlSchemeName)
|
||||
{
|
||||
var serializedControlScheme = controlSchemeArray.FirstOrDefault(sp =>
|
||||
sp.FindPropertyRelative(nameof(InputControlScheme.m_Name)).stringValue == controlSchemeName);
|
||||
return serializedControlScheme?.GetIndexOfArrayElement() ?? -1;
|
||||
}
|
||||
|
||||
public static SerializedProperty GetControlSchemesArray(SerializedObject asset)
|
||||
{
|
||||
return asset.FindProperty(nameof(InputActionAsset.m_ControlSchemes));
|
||||
}
|
||||
|
||||
#endregion // Control Schemes
|
||||
}
|
||||
}
|
||||
#endif // UNITY_EDITOR
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7c93968c9be74a1fa43a3dad5eed4cbd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,352 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor.IMGUI.Controls;
|
||||
using UnityEngine.InputSystem.LowLevel;
|
||||
using Unity.Profiling;
|
||||
|
||||
#if UNITY_6000_2_OR_NEWER
|
||||
using TreeView = UnityEditor.IMGUI.Controls.TreeView<int>;
|
||||
using TreeViewItem = UnityEditor.IMGUI.Controls.TreeViewItem<int>;
|
||||
using TreeViewState = UnityEditor.IMGUI.Controls.TreeViewState<int>;
|
||||
#endif
|
||||
|
||||
////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,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aa58e874cb4143ad884b8da2bb3f0d7a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,292 @@
|
||||
#if UNITY_EDITOR
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor.IMGUI.Controls;
|
||||
using UnityEngine.InputSystem.LowLevel;
|
||||
using UnityEditor;
|
||||
using Unity.Profiling;
|
||||
|
||||
#if UNITY_6000_2_OR_NEWER
|
||||
using TreeView = UnityEditor.IMGUI.Controls.TreeView<int>;
|
||||
using TreeViewItem = UnityEditor.IMGUI.Controls.TreeViewItem<int>;
|
||||
using TreeViewState = UnityEditor.IMGUI.Controls.TreeViewState<int>;
|
||||
#endif
|
||||
|
||||
////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,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a3882f27ee264cbfaa87fbdceacfcea1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,472 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Unity.Collections.LowLevel.Unsafe;
|
||||
using UnityEditor;
|
||||
using UnityEditor.IMGUI.Controls;
|
||||
using UnityEngine.InputSystem.LowLevel;
|
||||
|
||||
#if UNITY_6000_2_OR_NEWER
|
||||
using TreeViewState = UnityEditor.IMGUI.Controls.TreeViewState<int>;
|
||||
#endif
|
||||
|
||||
////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);
|
||||
|
||||
InputSystem.onDeviceChange += OnDeviceChange;
|
||||
}
|
||||
|
||||
private void OnDeviceChange(InputDevice device, InputDeviceChange change)
|
||||
{
|
||||
if (m_Control is null)
|
||||
return;
|
||||
|
||||
if (device.deviceId != m_Control.device.deviceId)
|
||||
return;
|
||||
|
||||
if (change == InputDeviceChange.Removed)
|
||||
Close();
|
||||
}
|
||||
|
||||
internal void OnDestroy()
|
||||
{
|
||||
if (m_Control != null)
|
||||
InputSystem.onDeviceChange -= OnDeviceChange;
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
if (m_StateBuffers is null)
|
||||
return;
|
||||
|
||||
if (m_StateBuffers[m_SelectedStateBuffer] is null)
|
||||
return;
|
||||
|
||||
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,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d0821e88149544e9b53290bf15b8d100
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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,56 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using UnityEditor.IMGUI.Controls;
|
||||
|
||||
#if UNITY_6000_2_OR_NEWER
|
||||
using TreeView = UnityEditor.IMGUI.Controls.TreeView<int>;
|
||||
using TreeViewItem = UnityEditor.IMGUI.Controls.TreeViewItem<int>;
|
||||
#endif
|
||||
|
||||
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