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