// UltEvents // Copyright 2020 Kybernetik //
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
namespace UltEvents.Editor
{
/// [Editor-Only]
/// Manages the construction of menus for selecting methods for s.
///
internal static class MethodSelectionMenu
{
/************************************************************************************************************************/
#region Fields
/************************************************************************************************************************/
///
/// The drawer state from when the menu was opened which needs to be restored when a method is selected because
/// menu items are executed after the frame finishes and the drawer state is cleared.
///
private static readonly DrawerState
CachedState = new DrawerState();
private static readonly StringBuilder
LabelBuilder = new StringBuilder();
// These fields should really be passed around as parameters, but they make all the method signatures annoyingly long.
private static MethodBase _CurrentMethod;
private static BindingFlags _Bindings;
private static GenericMenu _Menu;
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Entry Point
/************************************************************************************************************************/
/// Opens the menu near the specified `area`.
public static void ShowMenu(Rect area)
{
CachedState.CopyFrom(DrawerState.Current);
_CurrentMethod = CachedState.call.GetMethodSafe();
_Bindings = GetBindingFlags();
_Menu = new GenericMenu();
BoolPref.AddDisplayOptions(_Menu);
Object[] targetObjects;
var targets = GetObjectReferences(CachedState.TargetProperty, out targetObjects);
AddCoreItems(targets);
// Populate the main contents of the menu.
{
if (targets == null)
{
var serializedMethodName = CachedState.MethodNameProperty.stringValue;
Type declaringType;
string methodName;
PersistentCall.GetMethodDetails(serializedMethodName, null, out declaringType, out methodName);
// If we have no target, but do have a type, populate the menu with that type's statics.
if (declaringType != null)
{
PopulateMenuWithStatics(targetObjects, declaringType);
goto ShowMenu;
}
else// If we have no type either, pretend the inspected objects are the targets.
{
targets = targetObjects;
}
}
// Ensure that all targets share the same type.
var firstTarget = ValidateTargetsAndGetFirst(targets);
if (firstTarget == null)
{
targets = targetObjects;
firstTarget = targets[0];
}
// Add menu items according to the type of the target.
if (firstTarget is GameObject)
PopulateMenuForGameObject("", false, targets);
else if (firstTarget is Component)
PopulateMenuForComponent(targets);
else
PopulateMenuForObject(targets);
}
ShowMenu:
_Menu.DropDown(area);
GC.Collect();
}
/************************************************************************************************************************/
private static BindingFlags GetBindingFlags()
{
var bindings = BindingFlags.Public | BindingFlags.Instance;
if (BoolPref.ShowNonPublicMethods)
bindings |= BindingFlags.NonPublic;
if (BoolPref.ShowStaticMethods)
bindings |= BindingFlags.Static;
return bindings;
}
/************************************************************************************************************************/
private static void AddCoreItems(Object[] targets)
{
_Menu.AddItem(new GUIContent("Null"), _CurrentMethod == null, () =>
{
DrawerState.Current.CopyFrom(CachedState);
if (targets != null)
{
PersistentCallDrawer.SetMethod(null);
}
else
{
// For a static method, remove the method name but keep the declaring type.
var methodName = CachedState.MethodNameProperty.stringValue;
var lastDot = methodName.LastIndexOf('.');
if (lastDot < 0)
CachedState.MethodNameProperty.stringValue = null;
else
CachedState.MethodNameProperty.stringValue = methodName.Substring(0, lastDot + 1);
CachedState.PersistentArgumentsProperty.arraySize = 0;
CachedState.MethodNameProperty.serializedObject.ApplyModifiedProperties();
}
DrawerState.Current.Clear();
});
var isStatic = _CurrentMethod != null && _CurrentMethod.IsStatic;
if (targets != null && !isStatic)
{
_Menu.AddItem(new GUIContent("Static Method"), isStatic, () =>
{
DrawerState.Current.CopyFrom(CachedState);
PersistentCallDrawer.SetTarget(null);
DrawerState.Current.Clear();
});
}
_Menu.AddSeparator("");
}
/************************************************************************************************************************/
private static Object[] GetObjectReferences(SerializedProperty property, out Object[] targetObjects)
{
targetObjects = property.serializedObject.targetObjects;
if (property.hasMultipleDifferentValues)
{
var references = new Object[targetObjects.Length];
for (int i = 0; i < references.Length; i++)
{
using (var serializedObject = new SerializedObject(targetObjects[i]))
{
references[i] = serializedObject.FindProperty(property.propertyPath).objectReferenceValue;
}
}
return references;
}
else
{
var target = property.objectReferenceValue;
if (target != null)
return new Object[] { target };
else
return null;
}
}
/************************************************************************************************************************/
private static Object ValidateTargetsAndGetFirst(Object[] targets)
{
var firstTarget = targets[0];
if (firstTarget == null)
return null;
var targetType = firstTarget.GetType();
// Make sure all targets have the exact same type.
// Unfortunately supporting inheritance would be more complicated.
var i = 1;
for (; i < targets.Length; i++)
{
var obj = targets[i];
if (obj == null || obj.GetType() != targetType)
{
return null;
}
}
return firstTarget;
}
/************************************************************************************************************************/
private static T[] GetRelatedObjects(Object[] objects, Func getRelatedObject)
{
var relatedObjects = new T[objects.Length];
for (int i = 0; i < relatedObjects.Length; i++)
{
relatedObjects[i] = getRelatedObject(objects[i]);
}
return relatedObjects;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Populate for Objects
/************************************************************************************************************************/
private static void PopulateMenuWithStatics(Object[] targets, Type type)
{
var firstTarget = targets[0];
var component = firstTarget as Component;
if (!ReferenceEquals(component, null))
{
var gameObjects = GetRelatedObjects(targets, (target) => (target as Component).gameObject);
PopulateMenuForGameObject("", true, gameObjects);
}
else
{
PopulateMenuForObject(firstTarget.GetType().GetNameCS(BoolPref.ShowFullTypeNames) + " ->/", targets);
}
_Menu.AddSeparator("");
var bindings = BindingFlags.Static | BindingFlags.Public;
if (BoolPref.ShowNonPublicMethods)
bindings |= BindingFlags.NonPublic;
PopulateMenuWithMembers(type, bindings, "", null);
}
/************************************************************************************************************************/
private static void PopulateMenuForGameObject(string prefix, bool putGameObjectInSubMenu, Object[] targets)
{
var header = new GUIContent(prefix + "Selected GameObject and its Components");
var gameObjectPrefix = prefix;
if (putGameObjectInSubMenu)
{
_Menu.AddDisabledItem(header);
gameObjectPrefix += "GameObject ->/";
}
PopulateMenuForObject(gameObjectPrefix, targets);
if (!putGameObjectInSubMenu)
{
_Menu.AddSeparator(prefix);
_Menu.AddDisabledItem(header);
}
var gameObjects = GetRelatedObjects(targets, (target) => target as GameObject);
PopulateMenuForComponents(prefix, gameObjects);
}
/************************************************************************************************************************/
private static void PopulateMenuForComponents(string prefix, GameObject[] gameObjects)
{
var firstGameObject = gameObjects[0];
var components = firstGameObject.GetComponents();
for (int i = 0; i < components.Length; i++)
{
var component = components[i];
var targets = new Object[gameObjects.Length];
targets[0] = component;
Type type;
var typeIndex = GetComponentTypeIndex(component, components, out type);
int minTypeCount;
Component unused;
GetComponent(firstGameObject, type, typeIndex, out minTypeCount, out unused);
var j = 1;
for (; j < gameObjects.Length; j++)
{
int typeCount;
Component targetComponent;
GetComponent(gameObjects[j], type, typeIndex, out typeCount, out targetComponent);
if (typeCount <= typeIndex)
goto NextComponent;
targets[j] = targetComponent;
if (minTypeCount > typeCount)
minTypeCount = typeCount;
}
var name = type.GetNameCS(BoolPref.ShowFullTypeNames) + " ->/";
if (minTypeCount > 1)
name = UltEventUtils.GetPlacementName(typeIndex) + " " + name;
PopulateMenuForObject(prefix + name, targets);
}
NextComponent:;
}
private static int GetComponentTypeIndex(Component component, Component[] components, out Type type)
{
type = component.GetType();
var count = 0;
for (int i = 0; i < components.Length; i++)
{
var c = components[i];
if (c == component)
break;
else if (c.GetType() == type)
count++;
}
return count;
}
private static void GetComponent(GameObject gameObject, Type type, int targetIndex, out int numberOfComponentsOfType, out Component targetComponent)
{
numberOfComponentsOfType = 0;
targetComponent = null;
var components = gameObject.GetComponents(type);
for (int i = 0; i < components.Length; i++)
{
var component = components[i];
if (component.GetType() == type)
{
if (numberOfComponentsOfType == targetIndex)
targetComponent = component;
numberOfComponentsOfType++;
}
}
}
/************************************************************************************************************************/
private static void PopulateMenuForComponent(Object[] targets)
{
var gameObjects = GetRelatedObjects(targets, (target) => (target as Component).gameObject);
PopulateMenuForGameObject("", true, gameObjects);
_Menu.AddSeparator("");
PopulateMenuForObject(targets);
}
/************************************************************************************************************************/
private static void PopulateMenuForObject(Object[] targets)
{
PopulateMenuForObject("", targets);
}
private static void PopulateMenuForObject(string prefix, Object[] targets)
{
PopulateMenuWithMembers(targets[0].GetType(), _Bindings, prefix, targets);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Populate for Types
/************************************************************************************************************************/
private static void PopulateMenuWithMembers(Type type, BindingFlags bindings, string prefix, Object[] targets)
{
var members = GetSortedMembers(type, bindings);
var previousDeclaringType = type;
var firstSeparator = true;
var firstProperty = true;
var firstMethod = true;
var firstBaseType = true;
var nameMatchesNextMethod = false;
var i = 0;
while (i < members.Count)
{
ParameterInfo[] parameters;
MethodInfo getter;
var member = GetNextSupportedMember(members, ref i, out parameters, out getter);
GotMember:
if (member == null)
return;
i++;
if (BoolPref.SubMenuForEachBaseType)
{
if (firstBaseType && member.DeclaringType != type)
{
if (firstSeparator)
firstSeparator = false;
else
_Menu.AddSeparator(prefix);
var baseTypesOf = "Base Types of " + type.GetNameCS();
if (BoolPref.SubMenuForBaseTypes)
{
prefix += baseTypesOf + " ->/";
}
else
{
_Menu.AddDisabledItem(new GUIContent(prefix + baseTypesOf));
}
firstProperty = false;
firstMethod = false;
firstBaseType = false;
}
if (previousDeclaringType != member.DeclaringType)
{
previousDeclaringType = member.DeclaringType;
firstProperty = true;
firstMethod = true;
firstSeparator = true;
}
}
var property = member as PropertyInfo;
if (property != null)
{
AppendGroupHeader(prefix, "Properties in ", member.DeclaringType, type, ref firstProperty, ref firstSeparator);
AddSelectPropertyItem(prefix, targets, type, property, getter);
continue;
}
var method = member as MethodBase;
if (method != null)
{
AppendGroupHeader(prefix, "Methods in ", member.DeclaringType, type, ref firstMethod, ref firstSeparator);
// Check if the method name matched the previous or next method to group them.
if (BoolPref.GroupMethodOverloads)
{
var nameMatchedPreviousMethod = nameMatchesNextMethod;
ParameterInfo[] nextParameters;
MethodInfo nextGetter;
var nextMember = GetNextSupportedMember(members, ref i, out nextParameters, out nextGetter);
nameMatchesNextMethod = nextMember != null && method.Name == nextMember.Name;
if (nameMatchedPreviousMethod || nameMatchesNextMethod)
{
AddSelectMethodItem(prefix, targets, type, true, method, parameters);
if (i < members.Count)
{
member = nextMember;
parameters = nextParameters;
getter = nextGetter;
goto GotMember;
}
else
{
return;
}
}
}
// Otherwise just build the label normally.
AddSelectMethodItem(prefix, targets, type, false, method, parameters);
}
}
}
/************************************************************************************************************************/
private static void AppendGroupHeader(string prefix, string name, Type declaringType, Type currentType, ref bool firstInGroup, ref bool firstSeparator)
{
if (firstInGroup)
{
LabelBuilder.Length = 0;
LabelBuilder.Append(prefix);
if (BoolPref.SubMenuForEachBaseType && declaringType != currentType)
AppendDeclaringTypeSubMenu(LabelBuilder, declaringType, currentType);
if (firstSeparator)
firstSeparator = false;
else
_Menu.AddSeparator(LabelBuilder.ToString());
LabelBuilder.Append(name);
if (BoolPref.SubMenuForEachBaseType)
LabelBuilder.Append(declaringType.GetNameCS());
else
LabelBuilder.Append(currentType.GetNameCS());
_Menu.AddDisabledItem(new GUIContent(LabelBuilder.ToString()));
firstInGroup = false;
}
}
private static void AppendDeclaringTypeSubMenu(StringBuilder text, Type declaringType, Type currentType)
{
if (BoolPref.SubMenuForEachBaseType)
{
if (BoolPref.SubMenuForRootBaseType || declaringType != currentType)
{
text.Append(declaringType.GetNameCS());
text.Append(" ->/");
}
}
}
/************************************************************************************************************************/
private static void AddSelectPropertyItem(string prefix, Object[] targets, Type currentType, PropertyInfo property, MethodInfo getter)
{
var defaultMethod = getter;
MethodInfo setter = null;
if (IsSupported(property.PropertyType))
{
setter = property.GetSetMethod(true);
if (setter != null)
defaultMethod = setter;
}
LabelBuilder.Length = 0;
LabelBuilder.Append(prefix);
// Declaring Type.
AppendDeclaringTypeSubMenu(LabelBuilder, property.DeclaringType, currentType);
// Non-Public Grouping.
if (BoolPref.GroupNonPublicMethods && !IsPublic(property))
LabelBuilder.Append("Non-Public Properties ->/");
// Property Type and Name.
LabelBuilder.Append(property.PropertyType.GetNameCS(BoolPref.ShowFullTypeNames));
LabelBuilder.Append(' ');
LabelBuilder.Append(property.Name);
// Get and Set.
LabelBuilder.Append(" { ");
if (getter != null) LabelBuilder.Append("get; ");
if (setter != null) LabelBuilder.Append("set; ");
LabelBuilder.Append('}');
var label = LabelBuilder.ToString();
AddSetCallItem(label, defaultMethod, targets);
}
/************************************************************************************************************************/
private static void AddSelectMethodItem(string prefix, Object[] targets, Type currentType, bool methodNameSubMenu,
MethodBase method, ParameterInfo[] parameters)
{
LabelBuilder.Length = 0;
LabelBuilder.Append(prefix);
// Declaring Type.
AppendDeclaringTypeSubMenu(LabelBuilder, method.DeclaringType, currentType);
// Non-Public Grouping.
if (BoolPref.GroupNonPublicMethods && !IsPublic(method))
LabelBuilder.Append("Non-Public Methods ->/");
// Overload Grouping.
if (methodNameSubMenu)
LabelBuilder.Append(method.Name).Append(" ->/");
// Method Signature.
LabelBuilder.Append(GetMethodSignature(method, parameters, true));
var label = LabelBuilder.ToString();
AddSetCallItem(label, method, targets);
}
/************************************************************************************************************************/
private static void AddSetCallItem(string label, MethodBase method, Object[] targets)
{
_Menu.AddItem(
new GUIContent(label),
method == _CurrentMethod,
(userData) =>
{
DrawerState.Current.CopyFrom(CachedState);
var i = 0;
CachedState.CallProperty.ModifyValues((call) =>
{
var target = targets != null ? targets[i % targets.Length] : null;
call.SetMethod(method, target);
i++;
}, "Set Persistent Call");
DrawerState.Current.Clear();
},
null);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Member Gathering
/************************************************************************************************************************/
private static readonly Dictionary>>
MemberCache = new Dictionary>>();
internal static void ClearMemberCache()
{
MemberCache.Clear();
}
/************************************************************************************************************************/
private static List GetSortedMembers(Type type, BindingFlags bindings)
{
// Get the cache for the specified bindings.
Dictionary> memberCache;
if (!MemberCache.TryGetValue(bindings, out memberCache))
{
memberCache = new Dictionary>();
MemberCache.Add(bindings, memberCache);
}
// If the members for the specified type aren't cached for those bindings, gather and sort them.
List members;
if (!memberCache.TryGetValue(type, out members))
{
var properties = type.GetProperties(bindings);
var methods = type.GetMethods(bindings);
// When gathering static members, also include instance constructors.
var constructors = ((bindings & BindingFlags.Static) == BindingFlags.Static) ?
type.GetConstructors((bindings & ~BindingFlags.Static) | BindingFlags.Instance) :
null;
var capacity = properties.Length + methods.Length;
if (constructors != null)
capacity += constructors.Length;
members = new List(capacity);
members.AddRange(properties);
if (constructors != null)
members.AddRange(constructors);
members.AddRange(methods);
// If the bindings include static, add static members from each base type.
if ((bindings & BindingFlags.Static) == BindingFlags.Static && type.BaseType != null)
{
members.AddRange(GetSortedMembers(type.BaseType, bindings & ~BindingFlags.Instance));
}
UltEventUtils.StableInsertionSort(members, CompareMembers);
memberCache.Add(type, members);
}
return members;
}
/************************************************************************************************************************/
private static int CompareMembers(MemberInfo a, MemberInfo b)
{
if (BoolPref.SubMenuForEachBaseType)
{
var result = CompareChildBeforeBase(a.DeclaringType, b.DeclaringType);
if (result != 0)
return result;
}
// Compare types (properties before methods).
if (a is PropertyInfo)
{
if (!(b is PropertyInfo))
return -1;
}
else
{
if (b is PropertyInfo)
return 1;
}
// Non-Public Sub-Menu.
if (BoolPref.GroupNonPublicMethods)
{
if (IsPublic(a))
{
if (!IsPublic(b))
return -1;
}
else
{
if (IsPublic(b))
return 1;
}
}
// Compare names.
return a.Name.CompareTo(b.Name);
}
/************************************************************************************************************************/
private static int CompareChildBeforeBase(Type a, Type b)
{
if (a == b)
return 0;
while (true)
{
a = a.BaseType;
if (a == null)
return 1;
if (a == b)
return -1;
}
}
/************************************************************************************************************************/
private static readonly Dictionary
MemberToIsPublic = new Dictionary();
private static bool IsPublic(MemberInfo member)
{
bool isPublic;
if (!MemberToIsPublic.TryGetValue(member, out isPublic))
{
switch (member.MemberType)
{
case MemberTypes.Constructor:
case MemberTypes.Method:
isPublic = (member as MethodBase).IsPublic;
break;
case MemberTypes.Property:
isPublic =
(member as PropertyInfo).GetGetMethod() != null ||
(member as PropertyInfo).GetSetMethod() != null;
break;
default:
throw new ArgumentException("Unhandled member type", "member");
}
MemberToIsPublic.Add(member, isPublic);
}
return isPublic;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Supported Checks
/************************************************************************************************************************/
private static bool IsSupported(MethodBase method, out ParameterInfo[] parameters)
{
if (method.IsGenericMethod ||
(method.IsSpecialName && (!method.IsConstructor || method.IsStatic)) ||
method.Name.Contains("<") ||
method.IsDefined(typeof(ObsoleteAttribute), true))
{
parameters = null;
return false;
}
// Most UnityEngine.Object types shouldn't be constructed directly.
if (method.IsConstructor)
{
if (typeof(Component).IsAssignableFrom(method.DeclaringType) ||
typeof(ScriptableObject).IsAssignableFrom(method.DeclaringType))
{
parameters = null;
return false;
}
}
parameters = method.GetParameters();
if (!IsSupported(parameters))
return false;
return true;
}
private static bool IsSupported(PropertyInfo property, out MethodInfo getter)
{
if (property.IsSpecialName ||
property.IsDefined(typeof(ObsoleteAttribute), true))// Obsolete.
{
getter = null;
return false;
}
getter = property.GetGetMethod(true);
if (getter == null && !IsSupported(property.PropertyType))
return false;
return true;
}
/************************************************************************************************************************/
///
/// Returns true if the specified `type` can be represented by a .
///
public static bool IsSupported(Type type)
{
if (PersistentCall.IsSupportedNative(type))
{
return true;
}
else
{
int linkIndex;
PersistentArgumentType linkType;
return DrawerState.Current.TryGetLinkable(type, out linkIndex, out linkType);
}
}
///
/// Returns true if the type of each of the `parameters` can be represented by a .
///
public static bool IsSupported(ParameterInfo[] parameters)
{
for (int i = 0; i < parameters.Length; i++)
{
if (!IsSupported(parameters[i].ParameterType))
return false;
}
return true;
}
/************************************************************************************************************************/
private static MemberInfo GetNextSupportedMember(List members, ref int startIndex, out ParameterInfo[] parameters, out MethodInfo getter)
{
while (startIndex < members.Count)
{
var member = members[startIndex];
var property = member as PropertyInfo;
if (property != null && IsSupported(property, out getter))
{
parameters = null;
return member;
}
var method = member as MethodBase;
if (method != null && IsSupported(method, out parameters))
{
getter = null;
return member;
}
startIndex++;
}
parameters = null;
getter = null;
return null;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Method Signatures
/************************************************************************************************************************/
private static readonly Dictionary
MethodSignaturesWithParameters = new Dictionary(),
MethodSignaturesWithoutParameters = new Dictionary();
private static readonly StringBuilder
MethodSignatureBuilder = new StringBuilder();
/************************************************************************************************************************/
public static string GetMethodSignature(MethodBase method, ParameterInfo[] parameters, bool includeParameterNames)
{
if (method == null)
return null;
var signatureCache = includeParameterNames ? MethodSignaturesWithParameters : MethodSignaturesWithoutParameters;
string signature;
if (!signatureCache.TryGetValue(method, out signature))
{
signature = BuildAndCacheSignature(method, parameters, includeParameterNames, signatureCache);
}
return signature;
}
public static string GetMethodSignature(MethodBase method, bool includeParameterNames)
{
if (method == null)
return null;
var signatureCache = includeParameterNames ? MethodSignaturesWithParameters : MethodSignaturesWithoutParameters;
string signature;
if (!signatureCache.TryGetValue(method, out signature))
{
signature = BuildAndCacheSignature(method, method.GetParameters(), includeParameterNames, signatureCache);
}
return signature;
}
/************************************************************************************************************************/
private static string BuildAndCacheSignature(MethodBase method, ParameterInfo[] parameters, bool includeParameterNames,
Dictionary signatureCache)
{
MethodSignatureBuilder.Length = 0;
var returnType = method.GetReturnType();
MethodSignatureBuilder.Append(returnType.GetNameCS(false));
MethodSignatureBuilder.Append(' ');
MethodSignatureBuilder.Append(method.Name);
MethodSignatureBuilder.Append(" (");
for (int i = 0; i < parameters.Length; i++)
{
if (i > 0)
MethodSignatureBuilder.Append(", ");
var parameter = parameters[i];
MethodSignatureBuilder.Append(parameter.ParameterType.GetNameCS(false));
if (includeParameterNames)
{
MethodSignatureBuilder.Append(' ');
MethodSignatureBuilder.Append(parameter.Name);
}
}
MethodSignatureBuilder.Append(')');
var signature = MethodSignatureBuilder.ToString();
MethodSignatureBuilder.Length = 0;
signatureCache.Add(method, signature);
return signature;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
#endif