UI 자동화를 위해 바인딩 기능 구현

- 유니티 에셋 인증 오류로 meta 재생성
This commit is contained in:
2026-01-25 01:31:34 +09:00
parent 2ceb28f55d
commit ce83f21c93
1861 changed files with 377882 additions and 211 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5cdea6ae28e4a4b6ca0bb7506f88764c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4b0617d4946754ab980342582b1f560c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0fb454a124e8649bb9326261b1739032
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6109a857ac56b4017b44b06a9de0f827
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 42a7c5e6c9bc849a3a9163e719422c50
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f61b0e35037644f37b6ac81513577c50
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c46ed0dd23ba548fd87436e8c89334e9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2fe2cf22711ac4db3b094a955603f8bd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: caeab4c8826564dfda5e44a7e01a8de1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ed303b5629a047fa8c081261bb1993d1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6d671947d51444fb9b8a4bf96c16bfed
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 54ee1a1058dfd4e089fe94a8e880938e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7c93968c9be74a1fa43a3dad5eed4cbd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: aa58e874cb4143ad884b8da2bb3f0d7a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a3882f27ee264cbfaa87fbdceacfcea1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d0821e88149544e9b53290bf15b8d100
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e22caa7f1befe4d4db80921ba3cdabdb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c018e4b76d0cd35459e780d36eb69c45
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 60f36b88774fb4ed685661de123d7449
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: