This commit is contained in:
2025-01-17 13:10:42 +01:00
commit 4536213c91
15115 changed files with 1442174 additions and 0 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.ToLower().Split(' ');
// We keep two lists. Matches that matches the start of an item always get first priority.
var matchesStart = new List<AdvancedDropdownItem>();
var matchesWithin = new List<AdvancedDropdownItem>();
foreach (var e in m_SearchableElements)
{
var name = e.searchableName.ToLower().Replace(" ", "");
AddMatchItem(e, name, searchWords, matchesStart, matchesWithin);
}
searchTree = new AdvancedDropdownItem(kSearchHeader);
matchesStart.Sort();
foreach (var element in matchesStart)
{
searchTree.AddChild(element);
}
matchesWithin.Sort();
foreach (var element in matchesWithin)
{
searchTree.AddChild(element);
}
}
return searchTree;
}
private void BuildSearchableElements()
{
m_SearchableElements = new List<AdvancedDropdownItem>();
BuildSearchableElements(root);
}
private void BuildSearchableElements(AdvancedDropdownItem item)
{
if (!item.children.Any())
{
if (!item.IsSeparator())
m_SearchableElements.Add(item);
return;
}
foreach (var child in item.children)
{
BuildSearchableElements(child);
}
}
}
}
#endif // UNITY_EDITOR

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,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,3 @@
fileFormatVersion: 2
guid: ed303b5629a047fa8c081261bb1993d1
timeCreated: 1709546908

View File

@@ -0,0 +1,141 @@
#if UNITY_EDITOR
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection;
using UnityEditor;
namespace UnityEngine.InputSystem.Editor
{
internal static class EditorHelpers
{
// Provides an abstraction layer on top of EditorGUIUtility to allow replacing the underlying buffer.
public static Action<string> SetSystemCopyBufferContents = s => EditorGUIUtility.systemCopyBuffer = s;
// Provides an abstraction layer on top of EditorGUIUtility to allow replacing the underlying buffer.
public static Func<string> GetSystemCopyBufferContents = () => EditorGUIUtility.systemCopyBuffer;
// Attempts to retrieve the asset GUID associated with the given asset. If asset is null or the asset
// is not associated with a GUID or the operation fails for any other reason the return value will be null.
public static string GetAssetGUID(Object asset)
{
return !AssetDatabase.TryGetGUIDAndLocalFileIdentifier(asset, out var assetGuid, out long _)
? null : assetGuid;
}
// SerializedProperty.tooltip *should* give us the tooltip as per [Tooltip] attribute. Alas, for some
// reason, it's not happening.
public static string GetTooltip(this SerializedProperty property)
{
if (!string.IsNullOrEmpty(property.tooltip))
return property.tooltip;
var field = property.GetField();
if (field != null)
{
var tooltipAttribute = field.GetCustomAttribute<TooltipAttribute>();
if (tooltipAttribute != null)
return tooltipAttribute.tooltip;
}
return string.Empty;
}
public static string GetHyperlink(string text, string path)
{
return "<a href=\"" + path + $"\">{text}</a>";
}
public static string GetHyperlink(string path)
{
return GetHyperlink(path, path);
}
public static void RestartEditorAndRecompileScripts(bool dryRun = false)
{
// The API here are not public. Use reflection to get to them.
var editorApplicationType = typeof(EditorApplication);
var restartEditorAndRecompileScripts =
editorApplicationType.GetMethod("RestartEditorAndRecompileScripts",
BindingFlags.NonPublic | BindingFlags.Static);
if (!dryRun)
restartEditorAndRecompileScripts.Invoke(null, null);
else if (restartEditorAndRecompileScripts == null)
throw new MissingMethodException(editorApplicationType.FullName, "RestartEditorAndRecompileScripts");
}
// Attempts to make an asset editable in the underlying version control system and returns true if successful.
public static bool CheckOut(string path)
{
if (string.IsNullOrEmpty(path))
throw new ArgumentNullException(nameof(path));
// Make path relative to project folder.
var projectPath = Application.dataPath;
if (path.StartsWith(projectPath) && path.Length > projectPath.Length &&
(path[projectPath.Length] == '/' || path[projectPath.Length] == '\\'))
path = path.Substring(0, projectPath.Length + 1);
return AssetDatabase.MakeEditable(path);
}
/// <summary>
/// Attempts to checkout an asset for editing at the given path and overwrite its file content with
/// the given asset text content.
/// </summary>
/// <param name="assetPath">Path to asset to be checkout out and overwritten.</param>
/// <param name="text">The new file content.</param>
/// <returns>true if the file was successfully checkout for editing and the file was written.
/// This function may return false if unable to checkout the file for editing in the underlying
/// version control system.</returns>
internal static bool WriteAsset(string assetPath, string text)
{
// Attempt to checkout the file path for editing and inform the user if this fails.
if (!CheckOut(assetPath))
return false;
// (Over)write file text content.
File.WriteAllText(GetPhysicalPath(assetPath), text);
// Reimport the asset (indirectly triggers ADB notification callbacks)
AssetDatabase.ImportAsset(assetPath);
return true;
}
/// <summary>
/// Saves an asset to the given <c>assetPath</c> with file content corresponding to <c>text</c>
/// if the current content of the asset given by <c>assetPath</c> is different or the asset do not exist.
/// </summary>
/// <param name="assetPath">Destination asset path.</param>
/// <param name="text">The new desired text content to be written to the asset.</param>
/// <returns><c>true</c> if the asset was successfully modified or created, else <c>false</c>.</returns>
internal static bool SaveAsset(string assetPath, string text)
{
var existingJson = File.Exists(assetPath) ? File.ReadAllText(assetPath) : string.Empty;
// Return immediately if file content has not changed, i.e. touching the file would not yield a difference.
if (text == existingJson)
return false;
// Attempt to write asset to disc (including checkout the file) and inform the user if this fails.
if (WriteAsset(assetPath, text))
return true;
Debug.LogError($"Unable save asset to \"{assetPath}\" since the asset-path could not be checked-out as editable in the underlying version-control system.");
return false;
}
// Maps path into a physical path.
public static string GetPhysicalPath(string path)
{
// Note that we can only get physical path for 2021.2 or newer
#if UNITY_2021_2_OR_NEWER
return FileUtil.GetPhysicalPath(path);
#else
return path;
#endif
}
}
}
#endif // UNITY_EDITOR

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((int)EditorGUIUtility.pixelsPerPoint, 0, 4);
var scalePostFix = scale > 1 ? $"@{scale}x" : "";
if (name.IndexOfAny(Path.GetInvalidFileNameChars()) > -1)
name = string.Join("_", name.Split(Path.GetInvalidFileNameChars()));
var path = Path.Combine(kIconPath, skinPrefix + name + scalePostFix + ".png");
return AssetDatabase.LoadAssetAtPath<Texture2D>(path);
}
public static GUIStyle WithNormalBackground(this GUIStyle style, Texture2D background)
{
style.normal.background = background;
return style;
}
public static GUIStyle WithFontSize(this GUIStyle style, int fontSize)
{
style.fontSize = fontSize;
return style;
}
public static GUIStyle WithFontStyle(this GUIStyle style, FontStyle fontStyle)
{
style.fontStyle = fontStyle;
return style;
}
public static GUIStyle WithAlignment(this GUIStyle style, TextAnchor alignment)
{
style.alignment = alignment;
return style;
}
public static GUIStyle WithMargin(this GUIStyle style, RectOffset margin)
{
style.margin = margin;
return style;
}
public static GUIStyle WithBorder(this GUIStyle style, RectOffset border)
{
style.border = border;
return style;
}
public static GUIStyle WithPadding(this GUIStyle style, RectOffset padding)
{
style.padding = padding;
return style;
}
public static GUIStyle WithFixedWidth(this GUIStyle style, int fixedWidth)
{
style.fixedWidth = fixedWidth;
return style;
}
public static GUIStyle WithFixedHeight(this GUIStyle style, int fixedHeight)
{
style.fixedHeight = fixedHeight;
return style;
}
public static GUIStyle WithRichText(this GUIStyle style, bool richText = true)
{
style.richText = richText;
return style;
}
public static GUIStyle WithFont(this GUIStyle style, Font font)
{
style.font = font;
return style;
}
public static GUIStyle WithContentOffset(this GUIStyle style, Vector2 contentOffset)
{
style.contentOffset = contentOffset;
return style;
}
public static GUIStyle WithNormalTextColor(this GUIStyle style, Color textColor)
{
style.normal.textColor = textColor;
return style;
}
}
}
#endif // UNITY_EDITOR

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,3 @@
fileFormatVersion: 2
guid: 7c93968c9be74a1fa43a3dad5eed4cbd
timeCreated: 1509841821

View File

@@ -0,0 +1,346 @@
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor.IMGUI.Controls;
using UnityEngine.InputSystem.LowLevel;
using Unity.Profiling;
////TODO: make control values editable (create state events from UI and pump them into the system)
////TODO: show processors attached to controls
////TODO: make controls that have different `value` and `previous` in bold
namespace UnityEngine.InputSystem.Editor
{
// Multi-column TreeView that shows control tree of device.
internal class InputControlTreeView : TreeView
{
// If this is set, the controls won't display their current value but we'll
// show their state data from this buffer instead.
public byte[] stateBuffer;
public byte[][] multipleStateBuffers;
public bool showDifferentOnly;
static readonly ProfilerMarker k_InputBuildControlTreeMarker = new ProfilerMarker("BuildControlTree");
public static InputControlTreeView Create(InputControl rootControl, int numValueColumns, ref TreeViewState treeState, ref MultiColumnHeaderState headerState)
{
if (treeState == null)
treeState = new TreeViewState();
var newHeaderState = CreateHeaderState(numValueColumns);
if (headerState != null)
MultiColumnHeaderState.OverwriteSerializedFields(headerState, newHeaderState);
headerState = newHeaderState;
var header = new MultiColumnHeader(headerState);
return new InputControlTreeView(rootControl, treeState, header);
}
public void RefreshControlValues()
{
foreach (var item in GetRows())
if (item is ControlItem controlItem)
ReadState(controlItem.control, ref controlItem);
}
private const float kRowHeight = 20f;
private enum ColumnId
{
Name,
DisplayName,
Layout,
Type,
Format,
Offset,
Bit,
Size,
Optimized,
Value,
COUNT
}
private InputControl m_RootControl;
private static MultiColumnHeaderState CreateHeaderState(int numValueColumns)
{
var columns = new MultiColumnHeaderState.Column[(int)ColumnId.COUNT + numValueColumns - 1];
columns[(int)ColumnId.Name] =
new MultiColumnHeaderState.Column
{
width = 180,
minWidth = 60,
headerContent = new GUIContent("Name")
};
columns[(int)ColumnId.DisplayName] =
new MultiColumnHeaderState.Column
{
width = 160,
minWidth = 60,
headerContent = new GUIContent("Display Name")
};
columns[(int)ColumnId.Layout] =
new MultiColumnHeaderState.Column
{
width = 100,
minWidth = 60,
headerContent = new GUIContent("Layout")
};
columns[(int)ColumnId.Type] =
new MultiColumnHeaderState.Column
{
width = 100,
minWidth = 60,
headerContent = new GUIContent("Type")
};
columns[(int)ColumnId.Format] =
new MultiColumnHeaderState.Column {headerContent = new GUIContent("Format")};
columns[(int)ColumnId.Offset] =
new MultiColumnHeaderState.Column {headerContent = new GUIContent("Offset")};
columns[(int)ColumnId.Bit] =
new MultiColumnHeaderState.Column {width = 40, headerContent = new GUIContent("Bit")};
columns[(int)ColumnId.Size] =
new MultiColumnHeaderState.Column {headerContent = new GUIContent("Size (Bits)")};
columns[(int)ColumnId.Optimized] =
new MultiColumnHeaderState.Column {headerContent = new GUIContent("Optimized")};
if (numValueColumns == 1)
{
columns[(int)ColumnId.Value] =
new MultiColumnHeaderState.Column {width = 120, headerContent = new GUIContent("Value")};
}
else
{
for (var i = 0; i < numValueColumns; ++i)
columns[(int)ColumnId.Value + i] =
new MultiColumnHeaderState.Column
{
width = 100,
headerContent = new GUIContent("Value " + (char)('A' + i))
};
}
return new MultiColumnHeaderState(columns);
}
private InputControlTreeView(InputControl root, TreeViewState state, MultiColumnHeader header)
: base(state, header)
{
m_RootControl = root;
showBorder = false;
rowHeight = kRowHeight;
}
protected override TreeViewItem BuildRoot()
{
k_InputBuildControlTreeMarker.Begin();
var id = 1;
// Build tree from control down the control hierarchy.
var rootItem = BuildControlTreeRecursive(m_RootControl, 0, ref id);
k_InputBuildControlTreeMarker.End();
// Wrap root control in invisible item required by TreeView.
return new TreeViewItem
{
id = 0,
children = new List<TreeViewItem> {rootItem},
depth = -1
};
}
private ControlItem BuildControlTreeRecursive(InputControl control, int depth, ref int id)
{
// Build children.
List<TreeViewItem> children = null;
var isLeaf = control.children.Count == 0;
if (!isLeaf)
{
children = new List<TreeViewItem>();
foreach (var child in control.children)
{
var childItem = BuildControlTreeRecursive(child, depth + 1, ref id);
if (childItem != null)
children.Add(childItem);
}
// If none of our children returned an item, none of their data is different,
// so if we are supposed to show only controls that differ in value, we're sitting
// on a branch that has no changes. Cull the branch except if we're all the way
// at the root (we want to have at least one item).
if (children.Count == 0 && showDifferentOnly && depth != 0)
return null;
// Sort children by name.
children.Sort((a, b) => string.Compare(a.displayName, b.displayName));
}
// Compute offset. Offsets on the controls are absolute. Make them relative to the
// root control.
var controlOffset = control.stateBlock.byteOffset;
var rootOffset = m_RootControl.stateBlock.byteOffset;
var offset = controlOffset - rootOffset;
// Read state.
var item = new ControlItem
{
id = id++,
control = control,
depth = depth,
children = children
};
////TODO: come up with nice icons depicting different control types
if (!ReadState(control, ref item))
return null;
if (children != null)
{
foreach (var child in children)
child.parent = item;
}
return item;
}
private bool ReadState(InputControl control, ref ControlItem item)
{
// Compute offset. Offsets on the controls are absolute. Make them relative to the
// root control.
var controlOffset = control.stateBlock.byteOffset;
var rootOffset = m_RootControl.stateBlock.byteOffset;
var offset = controlOffset - rootOffset;
item.displayName = control.name;
item.layout = new GUIContent(control.layout);
item.format = new GUIContent(control.stateBlock.format.ToString());
item.offset = new GUIContent(offset.ToString());
item.bit = new GUIContent(control.stateBlock.bitOffset.ToString());
item.sizeInBits = new GUIContent(control.stateBlock.sizeInBits.ToString());
item.type = new GUIContent(control.GetType().Name);
item.optimized = new GUIContent(control.optimizedControlDataType != InputStateBlock.kFormatInvalid ? "+" : "-");
try
{
if (stateBuffer != null)
{
var text = ReadRawValueAsString(control, stateBuffer);
if (text != null)
item.value = new GUIContent(text);
}
else if (multipleStateBuffers != null)
{
var valueStrings = multipleStateBuffers.Select(x => ReadRawValueAsString(control, x));
if (showDifferentOnly && control.children.Count == 0 && valueStrings.Distinct().Count() == 1)
return false;
item.values = valueStrings.Select(x => x != null ? new GUIContent(x) : null).ToArray();
}
else
{
var valueObject = control.ReadValueAsObject();
if (valueObject != null)
item.value = new GUIContent(valueObject.ToString());
}
}
catch (Exception exception)
{
// If we fail to read a value, swallow it so we don't fail completely
// showing anything from the device.
item.value = new GUIContent(exception.ToString());
}
return true;
}
protected override void RowGUI(RowGUIArgs args)
{
var item = (ControlItem)args.item;
var columnCount = args.GetNumVisibleColumns();
for (var i = 0; i < columnCount; ++i)
{
ColumnGUI(args.GetCellRect(i), item, args.GetColumn(i), ref args);
}
}
private void ColumnGUI(Rect cellRect, ControlItem item, int column, ref RowGUIArgs args)
{
CenterRectUsingSingleLineHeight(ref cellRect);
switch (column)
{
case (int)ColumnId.Name:
args.rowRect = cellRect;
base.RowGUI(args);
break;
case (int)ColumnId.DisplayName:
GUI.Label(cellRect, item.control.displayName);
break;
case (int)ColumnId.Layout:
GUI.Label(cellRect, item.layout);
break;
case (int)ColumnId.Format:
GUI.Label(cellRect, item.format);
break;
case (int)ColumnId.Offset:
GUI.Label(cellRect, item.offset);
break;
case (int)ColumnId.Bit:
GUI.Label(cellRect, item.bit);
break;
case (int)ColumnId.Size:
GUI.Label(cellRect, item.sizeInBits);
break;
case (int)ColumnId.Type:
GUI.Label(cellRect, item.type);
break;
case (int)ColumnId.Optimized:
GUI.Label(cellRect, item.optimized);
break;
case (int)ColumnId.Value:
if (item.value != null)
GUI.Label(cellRect, item.value);
else if (item.values != null && item.values[0] != null)
GUI.Label(cellRect, item.values[0]);
break;
default:
var valueIndex = column - (int)ColumnId.Value;
if (item.values != null && item.values[valueIndex] != null)
GUI.Label(cellRect, item.values[valueIndex]);
break;
}
}
private unsafe string ReadRawValueAsString(InputControl control, byte[] state)
{
fixed(byte* statePtr = state)
{
var ptr = statePtr - m_RootControl.m_StateBlock.byteOffset;
return control.ReadValueFromStateAsObject(ptr).ToString();
}
}
private class ControlItem : TreeViewItem
{
public InputControl control;
public GUIContent layout;
public GUIContent format;
public GUIContent offset;
public GUIContent bit;
public GUIContent sizeInBits;
public GUIContent type;
public GUIContent optimized;
public GUIContent value;
public GUIContent[] values;
}
}
}
#endif // UNITY_EDITOR

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: aa58e874cb4143ad884b8da2bb3f0d7a
timeCreated: 1508116067

View File

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

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a3882f27ee264cbfaa87fbdceacfcea1
timeCreated: 1508116097

View File

@@ -0,0 +1,442 @@
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using Unity.Collections.LowLevel.Unsafe;
using UnityEditor;
using UnityEditor.IMGUI.Controls;
using UnityEngine.InputSystem.LowLevel;
////TODO: add ability to single-step through events
////TODO: annotate raw memory view with control offset and ranges (probably easiest to put the control tree and raw memory view side by side)
////TODO: find way to automatically dock the state windows next to their InputDeviceDebuggerWindows
//// (probably needs an extension to the editor UI APIs as the only programmatic docking controls
//// seem to be through GetWindow)
////TODO: allow setting a C# struct type that we can use to display the layout of the data
////TODO: for delta state events, highlight the controls included in the event (or show only those)
////FIXME: need to prevent extra controls appended at end from reading beyond the state buffer
namespace UnityEngine.InputSystem.Editor
{
// Additional window that we can pop open to inspect raw state (either on events or on controls/devices).
internal class InputStateWindow : EditorWindow
{
private const int kBytesPerHexGroup = 1;
private const int kHexGroupsPerLine = 8;
private const int kHexDumpLineHeight = 25;
private const int kOffsetLabelWidth = 30;
private const int kHexGroupWidth = 25;
private const int kBitGroupWidth = 75;
void Update()
{
if (m_PollControlState && m_Control != null)
{
PollBuffersFromControl(m_Control);
Repaint();
}
}
public void InitializeWithEvent(InputEventPtr eventPtr, InputControl control)
{
m_Control = control;
m_PollControlState = false;
m_StateBuffers = new byte[1][];
m_StateBuffers[0] = GetEventStateBuffer(eventPtr, control);
m_SelectedStateBuffer = 0;
titleContent = new GUIContent(control.displayName);
}
public void InitializeWithEvents(InputEventPtr[] eventPtrs, InputControl control)
{
var numEvents = eventPtrs.Length;
m_Control = control;
m_PollControlState = false;
m_StateBuffers = new byte[numEvents][];
for (var i = 0; i < numEvents; ++i)
m_StateBuffers[i] = GetEventStateBuffer(eventPtrs[i], control);
m_CompareStateBuffers = true;
m_ShowDifferentOnly = true;
titleContent = new GUIContent(control.displayName);
}
private unsafe byte[] GetEventStateBuffer(InputEventPtr eventPtr, InputControl control)
{
// Must be an event carrying state.
if (!eventPtr.IsA<StateEvent>() && !eventPtr.IsA<DeltaStateEvent>())
throw new ArgumentException("Event must be state or delta event", nameof(eventPtr));
// Get state data.
void* dataPtr;
uint dataSize;
uint stateSize;
uint stateOffset = 0;
if (eventPtr.IsA<DeltaStateEvent>())
{
var deltaEventPtr = DeltaStateEvent.From(eventPtr);
stateSize = control.stateBlock.alignedSizeInBytes;
stateOffset = deltaEventPtr->stateOffset;
dataPtr = deltaEventPtr->deltaState;
dataSize = deltaEventPtr->deltaStateSizeInBytes;
}
else
{
var stateEventPtr = StateEvent.From(eventPtr);
dataSize = stateSize = stateEventPtr->stateSizeInBytes;
dataPtr = stateEventPtr->state;
}
// Copy event data.
var buffer = new byte[stateSize];
fixed(byte* bufferPtr = buffer)
{
UnsafeUtility.MemCpy(bufferPtr + stateOffset, dataPtr, dataSize);
}
return buffer;
}
public unsafe void InitializeWithControl(InputControl control)
{
m_Control = control;
m_PollControlState = true;
m_SelectedStateBuffer = (int)BufferSelector.Default;
PollBuffersFromControl(control, selectBuffer: true);
titleContent = new GUIContent(control.displayName);
}
private unsafe void PollBuffersFromControl(InputControl control, bool selectBuffer = false)
{
var bufferChoices = new List<GUIContent>();
var bufferChoiceValues = new List<int>();
// Copy front and back buffer state for each update that has valid buffers.
var device = control.device;
var stateSize = control.m_StateBlock.alignedSizeInBytes;
var stateOffset = control.m_StateBlock.byteOffset;
m_StateBuffers = new byte[(int)BufferSelector.COUNT][];
for (var i = 0; i < (int)BufferSelector.COUNT; ++i)
{
var selector = (BufferSelector)i;
var deviceState = TryGetDeviceState(device, selector);
if (deviceState == null)
continue;
var buffer = new byte[stateSize];
fixed(byte* stateDataPtr = buffer)
{
UnsafeUtility.MemCpy(stateDataPtr, (byte*)deviceState + (int)stateOffset, stateSize);
}
m_StateBuffers[i] = buffer;
if (selectBuffer && m_StateBuffers[m_SelectedStateBuffer] == null)
m_SelectedStateBuffer = (int)selector;
bufferChoices.Add(Contents.bufferChoices[i]);
bufferChoiceValues.Add(i);
}
m_BufferChoices = bufferChoices.ToArray();
m_BufferChoiceValues = bufferChoiceValues.ToArray();
}
private static unsafe void* TryGetDeviceState(InputDevice device, BufferSelector selector)
{
var manager = InputSystem.s_Manager;
var deviceIndex = device.m_DeviceIndex;
switch (selector)
{
case BufferSelector.PlayerUpdateFrontBuffer:
if (manager.m_StateBuffers.m_PlayerStateBuffers.valid)
return manager.m_StateBuffers.m_PlayerStateBuffers.GetFrontBuffer(deviceIndex);
break;
case BufferSelector.PlayerUpdateBackBuffer:
if (manager.m_StateBuffers.m_PlayerStateBuffers.valid)
return manager.m_StateBuffers.m_PlayerStateBuffers.GetBackBuffer(deviceIndex);
break;
case BufferSelector.EditorUpdateFrontBuffer:
if (manager.m_StateBuffers.m_EditorStateBuffers.valid)
return manager.m_StateBuffers.m_EditorStateBuffers.GetFrontBuffer(deviceIndex);
break;
case BufferSelector.EditorUpdateBackBuffer:
if (manager.m_StateBuffers.m_EditorStateBuffers.valid)
return manager.m_StateBuffers.m_EditorStateBuffers.GetBackBuffer(deviceIndex);
break;
case BufferSelector.NoiseMaskBuffer:
return manager.m_StateBuffers.noiseMaskBuffer;
case BufferSelector.ResetMaskBuffer:
return manager.m_StateBuffers.resetMaskBuffer;
}
return null;
}
public void OnGUI()
{
if (m_Control == null)
m_ShowRawBytes = true;
// If our state is no longer valid, just close the window.
if (m_StateBuffers == null)
{
Close();
return;
}
GUILayout.BeginHorizontal(EditorStyles.toolbar);
m_PollControlState = GUILayout.Toggle(m_PollControlState, Contents.live, EditorStyles.toolbarButton);
m_ShowRawBytes = GUILayout.Toggle(m_ShowRawBytes, Contents.showRawMemory, EditorStyles.toolbarButton,
GUILayout.Width(150));
m_ShowAsBits = GUILayout.Toggle(m_ShowAsBits, Contents.showBits, EditorStyles.toolbarButton);
if (m_CompareStateBuffers)
{
var showDifferentOnly = GUILayout.Toggle(m_ShowDifferentOnly, Contents.showDifferentOnly,
EditorStyles.toolbarButton, GUILayout.Width(150));
if (showDifferentOnly != m_ShowDifferentOnly && m_ControlTree != null)
{
m_ControlTree.showDifferentOnly = showDifferentOnly;
m_ControlTree.Reload();
}
m_ShowDifferentOnly = showDifferentOnly;
}
// If we have multiple state buffers to choose from and we're not comparing them to each other,
// add dropdown that allows selecting which buffer to display.
if (m_StateBuffers.Length > 1 && !m_CompareStateBuffers)
{
var selectedBuffer = EditorGUILayout.IntPopup(m_SelectedStateBuffer, m_BufferChoices,
m_BufferChoiceValues, EditorStyles.toolbarPopup);
if (selectedBuffer != m_SelectedStateBuffer)
{
m_SelectedStateBuffer = selectedBuffer;
m_ControlTree = null;
}
}
GUILayout.FlexibleSpace();
GUILayout.EndHorizontal();
if (m_ShowRawBytes)
{
DrawHexDump();
}
else
{
if (m_ControlTree == null)
{
if (m_CompareStateBuffers)
{
m_ControlTree = InputControlTreeView.Create(m_Control, m_StateBuffers.Length, ref m_ControlTreeState, ref m_ControlTreeHeaderState);
m_ControlTree.multipleStateBuffers = m_StateBuffers;
m_ControlTree.showDifferentOnly = m_ShowDifferentOnly;
}
else
{
m_ControlTree = InputControlTreeView.Create(m_Control, 1, ref m_ControlTreeState, ref m_ControlTreeHeaderState);
m_ControlTree.stateBuffer = m_StateBuffers[m_SelectedStateBuffer];
}
m_ControlTree.Reload();
m_ControlTree.ExpandAll();
}
var rect = EditorGUILayout.GetControlRect(GUILayout.ExpandHeight(true));
m_ControlTree.OnGUI(rect);
}
}
private byte[] TryGetBackBufferForCurrentlySelected()
{
if (m_StateBuffers.Length != (int)BufferSelector.COUNT)
return null;
switch ((BufferSelector)m_SelectedStateBuffer)
{
case BufferSelector.PlayerUpdateFrontBuffer:
return m_StateBuffers[(int)BufferSelector.PlayerUpdateBackBuffer];
case BufferSelector.EditorUpdateFrontBuffer:
return m_StateBuffers[(int)BufferSelector.EditorUpdateBackBuffer];
default:
return null;
}
}
private string FormatByte(byte value)
{
if (m_ShowAsBits)
return Convert.ToString(value, 2).PadLeft(8, '0');
else
return value.ToString("X2");
}
////TODO: support dumping multiple state side-by-side when comparing
private void DrawHexDump()
{
m_HexDumpScrollPosition = EditorGUILayout.BeginScrollView(m_HexDumpScrollPosition);
var stateBuffer = m_StateBuffers[m_SelectedStateBuffer];
var prevStateBuffer = TryGetBackBufferForCurrentlySelected();
if (prevStateBuffer != null && prevStateBuffer.Length != stateBuffer.Length) // we assume they're same length, otherwise ignore prev buffer
prevStateBuffer = null;
var numBytes = stateBuffer.Length;
var numHexGroups = numBytes / kBytesPerHexGroup + (numBytes % kBytesPerHexGroup > 0 ? 1 : 0);
var numLines = numHexGroups / kHexGroupsPerLine + (numHexGroups % kHexGroupsPerLine > 0 ? 1 : 0);
var currentOffset = 0;
var currentLineRect = EditorGUILayout.GetControlRect(GUILayout.ExpandWidth(true));
currentLineRect.height = kHexDumpLineHeight;
var currentHexGroup = 0;
var currentByte = 0;
////REVIEW: what would be totally awesome is if this not just displayed a hex dump but also the correlation to current
//// control offset assignments
for (var line = 0; line < numLines; ++line)
{
// Draw offset.
var offsetLabelRect = currentLineRect;
offsetLabelRect.width = kOffsetLabelWidth;
GUI.Label(offsetLabelRect, currentOffset.ToString(), Styles.offsetLabel);
currentOffset += kBytesPerHexGroup * kHexGroupsPerLine;
// Draw hex groups.
var hexGroupRect = offsetLabelRect;
hexGroupRect.x += kOffsetLabelWidth + 10;
hexGroupRect.width = m_ShowAsBits ? kBitGroupWidth : kHexGroupWidth;
for (var group = 0;
group < kHexGroupsPerLine && currentHexGroup < numHexGroups;
++group, ++currentHexGroup)
{
// Convert bytes to hex.
var hex = string.Empty;
for (var i = 0; i < kBytesPerHexGroup; ++i, ++currentByte)
{
if (currentByte >= numBytes)
{
hex += " ";
continue;
}
var current = FormatByte(stateBuffer[currentByte]);
if (prevStateBuffer == null)
{
hex += current;
continue;
}
var prev = FormatByte(prevStateBuffer[currentByte]);
if (prev.Length != current.Length)
{
hex += current;
continue;
}
for (var j = 0; j < current.Length; ++j)
{
if (current[j] != prev[j])
hex += $"<color=#C84B31FF>{current[j]}</color>";
else
hex += current[j];
}
}
////TODO: draw alternating backgrounds for the hex groups
GUI.Label(hexGroupRect, hex, style: Styles.hexLabel);
hexGroupRect.x += m_ShowAsBits ? kBitGroupWidth : kHexGroupWidth;
}
currentLineRect.y += kHexDumpLineHeight;
}
EditorGUILayout.EndScrollView();
}
// We copy the state we're inspecting to a buffer we own so that we're safe
// against any mutations.
// When inspecting controls (as opposed to events), we copy all their various
// state buffers and allow switching between them.
[SerializeField] private byte[][] m_StateBuffers;
[SerializeField] private int m_SelectedStateBuffer;
[SerializeField] private bool m_CompareStateBuffers;
[SerializeField] private bool m_ShowDifferentOnly;
[SerializeField] private bool m_ShowRawBytes;
[SerializeField] private bool m_ShowAsBits;
[SerializeField] private bool m_PollControlState;
[SerializeField] private TreeViewState m_ControlTreeState;
[SerializeField] private MultiColumnHeaderState m_ControlTreeHeaderState;
[SerializeField] private Vector2 m_HexDumpScrollPosition;
[NonSerialized] private InputControlTreeView m_ControlTree;
[NonSerialized] private GUIContent[] m_BufferChoices;
[NonSerialized] private int[] m_BufferChoiceValues;
////FIXME: we lose this on domain reload; how should we recover?
[NonSerialized] private InputControl m_Control;
private enum BufferSelector
{
PlayerUpdateFrontBuffer,
PlayerUpdateBackBuffer,
EditorUpdateFrontBuffer,
EditorUpdateBackBuffer,
NoiseMaskBuffer,
ResetMaskBuffer,
COUNT,
Default = PlayerUpdateFrontBuffer
}
private static class Styles
{
public static GUIStyle offsetLabel = new GUIStyle
{
alignment = TextAnchor.UpperRight,
fontStyle = FontStyle.BoldAndItalic,
font = EditorStyles.boldFont,
fontSize = EditorStyles.boldFont.fontSize - 2,
normal = new GUIStyleState { textColor = Color.black }
};
public static GUIStyle hexLabel = new GUIStyle
{
fontStyle = FontStyle.Normal,
font = EditorGUIUtility.Load("Fonts/RobotoMono/RobotoMono-Regular.ttf") as Font,
fontSize = EditorStyles.label.fontSize + 2,
normal = new GUIStyleState { textColor = Color.white },
richText = true
};
}
private static class Contents
{
public static GUIContent live = new GUIContent("Live");
public static GUIContent showRawMemory = new GUIContent("Display Raw Memory");
public static GUIContent showBits = new GUIContent("Bits/Hex");
public static GUIContent showDifferentOnly = new GUIContent("Show Only Differences");
public static GUIContent[] bufferChoices =
{
new GUIContent("Player (Current)"),
new GUIContent("Player (Previous)"),
new GUIContent("Editor (Current)"),
new GUIContent("Editor (Previous)"),
new GUIContent("Noise Mask"),
new GUIContent("Reset Mask")
};
}
}
}
#endif // UNITY_EDITOR

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d0821e88149544e9b53290bf15b8d100
timeCreated: 1508117269

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,51 @@
#if UNITY_EDITOR
using System;
using UnityEditor.IMGUI.Controls;
namespace UnityEngine.InputSystem.Editor
{
/// <summary>
/// Extension methods for working with tree views.
/// </summary>
/// <seealso cref="TreeView"/>
internal static class TreeViewHelpers
{
public static TItem TryFindItemInHierarchy<TItem>(this TreeViewItem item)
where TItem : TreeViewItem
{
while (item != null)
{
if (item is TItem result)
return result;
item = item.parent;
}
return null;
}
public static bool IsParentOf(this TreeViewItem parent, TreeViewItem child)
{
if (parent == null)
throw new ArgumentNullException(nameof(parent));
if (child == null)
throw new ArgumentNullException(nameof(child));
do
{
child = child.parent;
}
while (child != null && child != parent);
return child != null;
}
public static void ExpandChildren(this TreeView treeView, TreeViewItem item)
{
if (!item.hasChildren)
return;
foreach (var child in item.children)
treeView.SetExpanded(child.id, true);
}
}
}
#endif // UNITY_EDITOR

View File

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