diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ef8c3bd --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +### 1.1.0 + +Added support for generic arguments. Works like a generic Func, where the last generic argument is the return type of the function. Having only the return value will look for getter properties aswell. Having > 1 generic argument will only look for suitable public methods \ No newline at end of file diff --git a/CHANGELOG.md.meta b/CHANGELOG.md.meta new file mode 100644 index 0000000..8e8e7d5 --- /dev/null +++ b/CHANGELOG.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 4313795d52b054b409f55b7abf4be995 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Property Drawer.meta b/Editor/PropertyDrawer.meta similarity index 77% rename from Editor/Property Drawer.meta rename to Editor/PropertyDrawer.meta index 858c401..b666378 100644 --- a/Editor/Property Drawer.meta +++ b/Editor/PropertyDrawer.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: a98267ecc93a89f4c9b31bad1ccb0bb0 +guid: 0600072b8385f4646a65c03fada4ed7d folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Editor/Property Drawer/SerializableFuncPropertyDrawer.cs b/Editor/PropertyDrawer/SerializableFuncBasePropertyDrawer.cs similarity index 89% rename from Editor/Property Drawer/SerializableFuncPropertyDrawer.cs rename to Editor/PropertyDrawer/SerializableFuncBasePropertyDrawer.cs index c1b846e..5fad306 100644 --- a/Editor/Property Drawer/SerializableFuncPropertyDrawer.cs +++ b/Editor/PropertyDrawer/SerializableFuncBasePropertyDrawer.cs @@ -1,691 +1,718 @@ -#if UNITY_EDITOR - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Text; -using UnityEditor; -using UnityEditor.UIElements; -using UnityEditorInternal; -using UnityEngine; -using UnityEngine.UIElements; -using Utilities.Extensions.SystemExtensions; -using Utilities.Extensions.UIToolkit; -using Utilities.SerializableData.SerializableFunc.UnityEditorUtilities; -using Object = UnityEngine.Object; - -namespace Utilities.SerializableData.SerializableFunc.UnityEditorDrawers -{ - [CustomPropertyDrawer(typeof(SerializableFunc<>))] - public class SerializableFuncPropertyDrawer : PropertyDrawer - { - private const string Target_Function_Label = "Target Function"; - private const string Target_Object_Label = "Target Object"; - private const string No_Function_Label = "No Function"; - - private const string TargetObject_Property_Name = "targetObject"; - private const string MethodName_Property_Name = "methodName"; - - private const string MixedValueContent_Property_Name = "mixedValueContent"; - - #region GUI Drawing - - public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) - { - EditorGUI.BeginProperty(position, GUIContent.none, property); - - if (Event.current.type == EventType.Repaint) - { - ReorderableList.defaultBehaviours.DrawHeaderBackground(position); - } - - Rect headerRect = position; - - headerRect.xMin += 6f; - headerRect.xMax -= 6f; - headerRect.height -= 2f; - headerRect.y += 1f; - - DrawFuncHeader(ref headerRect, property, label.text); - - DrawPropertyView(ref position, ref headerRect, property); - - EditorGUI.EndProperty(); - } - - private void DrawFuncHeader(ref Rect position, SerializedProperty funcProperty, string labelText) - { - position.height = 18f; - string text = GetHeaderText(funcProperty, labelText); - GUI.Label(position, text); - } - - private void DrawPropertyView(ref Rect position, ref Rect headerRect, SerializedProperty funcProperty) - { - Rect listRect = new Rect(position.x, headerRect.y + headerRect.height, position.width, 45f); - - int indentLevel = EditorGUI.indentLevel; - EditorGUI.indentLevel = 0; - if (Event.current.type == EventType.Repaint) - { - ReorderableList.defaultBehaviours.boxBackground.Draw(listRect, isHover: false, isActive: false, on: false, hasKeyboardFocus: false); - } - - listRect.yMin += 1f; - listRect.yMax -= 4f; - listRect.xMin += 1f; - listRect.xMax -= 1f; - - SerializedProperty targetObjectProperty = GetTargetObjectSerializedProperty(funcProperty); - SerializedProperty targetMethodProperty = GetMethodNameSerializedProperty(funcProperty); - EditorGUI.BeginChangeCheck(); - - Rect targetObjectLineRect = GetListViewSingleLineRect(ref position, ref listRect); - Rect[] targetObjectRects = GetTwoRectsForLabelAndProperty(ref targetObjectLineRect); - - EditorGUI.LabelField(targetObjectRects[0], Target_Object_Label); - EditorGUI.PropertyField(targetObjectRects[1], targetObjectProperty, GUIContent.none); - - if (EditorGUI.EndChangeCheck()) - { - targetMethodProperty.stringValue = string.Empty; - targetMethodProperty.serializedObject.ApplyModifiedProperties(); - } - - DrawMethodNameProperty(ref targetObjectLineRect, funcProperty, targetObjectProperty, targetMethodProperty); - - EditorGUI.indentLevel = indentLevel; - } - - private void DrawMethodNameProperty(ref Rect previousRect, - SerializedProperty funcProperty, - SerializedProperty targetObjectSerializedProperty, - SerializedProperty targetMethodSerializedProperty) - { - Rect targetMethodRect = GetNextSingleLineRect(ref previousRect); - Rect[] targetMethodRects = GetTwoRectsForLabelAndProperty(ref targetMethodRect); - - EditorGUI.LabelField(targetMethodRects[0], Target_Function_Label); - - using (new EditorGUI.DisabledScope(targetObjectSerializedProperty.objectReferenceValue == null)) - { - EditorGUI.BeginProperty(targetMethodRect, GUIContent.none, targetMethodSerializedProperty); - - GUIContent content; - if (EditorGUI.showMixedValue) - { - content = GetMixedValueContentGUIContent(); - } - else - { - StringBuilder stringBuilder = new StringBuilder(); - if (targetObjectSerializedProperty.objectReferenceValue == null - || string.IsNullOrEmpty(targetMethodSerializedProperty.stringValue)) - { - stringBuilder.Append(No_Function_Label); - } - else if (!IsPersistantListenerValid(funcProperty, targetObjectSerializedProperty, targetMethodSerializedProperty)) - { - string missingComponentString = GetMissingComponentMethodString(targetObjectSerializedProperty, targetMethodSerializedProperty); - stringBuilder.Append(missingComponentString); - } - else - { - stringBuilder.Append(targetObjectSerializedProperty.objectReferenceValue.GetType().Name); - if (!string.IsNullOrEmpty(targetMethodSerializedProperty.stringValue)) - { - stringBuilder.Append("."); - string nicerName = NicifyGetterPropertyName(targetMethodSerializedProperty.stringValue); - stringBuilder.Append(nicerName); - } - } - - content = new GUIContent(stringBuilder.ToString()); - } - - if (EditorGUI.DropdownButton(targetMethodRects[1], content, FocusType.Passive, EditorStyles.popup)) - { - CachedPropertiesAndObjectsData data = new CachedPropertiesAndObjectsData(funcProperty, targetObjectSerializedProperty, targetMethodSerializedProperty, null, null); - - BuildGenericMenu(data).DropDown(previousRect); - } - - EditorGUI.EndProperty(); - } - } - - private bool IsPersistantListenerValid(SerializedProperty funcProperty, - SerializedProperty targetObjectProperty, - SerializedProperty targetMethodProperty) - { - if (targetObjectProperty.objectReferenceValue == null - || string.IsNullOrWhiteSpace(targetMethodProperty.stringValue)) return false; - - Type returnType = GetFuncReturnTypeFromProperty(funcProperty); - - MethodInfo targetMethod = targetObjectProperty.objectReferenceValue - .GetType() - .GetMethods(BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Instance) - .Where(x => IsSuitableMethodInfo(x, returnType)) - .FirstOrDefault(x => string.Equals(x.Name, targetMethodProperty.stringValue)); - - if (targetMethod != null) return true; - - MethodInfo targetProperty = targetObjectProperty.objectReferenceValue - .GetType() - .GetProperties(BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Instance) - .Where(x => IsSuitableProperty(x, returnType)) - .Select(x => x.GetGetMethod()) - .FirstOrDefault(x => string.Equals(x.Name, targetMethodProperty.stringValue)); - - return targetProperty != null; - } - - private GUIContent GetMixedValueContentGUIContent() - { - MethodInfo info = typeof(EditorGUI) - .GetMethod(MixedValueContent_Property_Name, BindingFlags.FlattenHierarchy | BindingFlags.Static | BindingFlags.Public); - - return info.Invoke(null, null) as GUIContent; - } - - #region Rect Utility - - private Rect GetListViewSingleLineRect(ref Rect position, ref Rect listRect) - { - Rect result = new Rect(position.x, - listRect.y + EditorGUIUtility.standardVerticalSpacing, - listRect.width, - EditorGUIUtility.singleLineHeight); - - result.xMin += 8f; - result.xMax -= 3f; - - return result; - } - - private Rect GetNextSingleLineRect(ref Rect previousRect) - { - return new Rect(previousRect.x, - previousRect.yMax + EditorGUIUtility.standardVerticalSpacing, - previousRect.width, - EditorGUIUtility.singleLineHeight); - } - - private Rect[] GetTwoRectsForLabelAndProperty(ref Rect lineRect) - { - Rect labelRect = new Rect(lineRect.position.x, - lineRect.position.y, - lineRect.width / 3 - 1f, - lineRect.height); - - float propertyRectX = labelRect.xMax + 2f; - float propertyRectWidth = lineRect.xMax - propertyRectX; - - Rect propertyRect = new Rect( - propertyRectX, - labelRect.y, - propertyRectWidth, - lineRect.height); - - return new Rect[] { labelRect, propertyRect }; - } - - #endregion - - #region Property Height Override - - public override float GetPropertyHeight(SerializedProperty property, GUIContent label) - { - return 65f; - } - - #endregion - - #endregion - - #region UI Toolkit Drawing - - public override VisualElement CreatePropertyGUI(SerializedProperty property) - { - VisualElement visualElement = new VisualElement(); - visualElement.AddToClassList("unity-event__container"); - - Label label = new Label(); - label.text = GetHeaderText(property, property.displayName); - label.tooltip = property.tooltip; - label.AddToClassList("unity-list-view__header"); - label.style.overflow = new StyleEnum(Overflow.Hidden); - - VisualElement assignmentVE = CreateFuncAssignmentVisualElement(property); - - visualElement.Add(label); - visualElement.Add(assignmentVE); - - return visualElement; - } - - #region Visible Container Drawing - - private VisualElement CreateFuncAssignmentVisualElement(SerializedProperty funcProperty) - { - VisualElement visualElement = CreateContainerVisualElement(); - - SerializedProperty objectProperty = GetTargetObjectSerializedProperty(funcProperty); - string objectPropertyLabel = ObjectNames.NicifyVariableName(TargetObject_Property_Name); - - ObjectField targetObjectField = GetTargetObjectField(visualElement, objectProperty, objectPropertyLabel); - - SerializedProperty methodNameProperty = GetMethodNameSerializedProperty(funcProperty); - DropdownField functionDropdown = CreateChosenMethodDropdownField(visualElement, methodNameProperty); - - RegisterObjectFieldCallback(targetObjectField, functionDropdown); - - CachedPropertiesAndObjectsData data = new CachedPropertiesAndObjectsData(funcProperty, objectProperty, methodNameProperty, targetObjectField, functionDropdown); - - AssignGenericOSMenuValueToDropdownField(data); - - AssignFormattingCallbackToDropdownField(data); - - return visualElement; - } - - private VisualElement CreateContainerVisualElement() - { - VisualElement visualElement = new VisualElement(); - - visualElement.AddToClassList("unity-scroll-view"); - visualElement.AddToClassList("unity-collection-view__scroll-view"); - visualElement.AddToClassList("unity-collection-view--with-border"); - visualElement.AddToClassList("unity-list-view__scroll-view--with-footer"); - visualElement.AddToClassList("unity-event__list-view-scroll-view"); - - visualElement.style.paddingBottom = 5f; - visualElement.style.paddingLeft = 3f; - visualElement.style.paddingRight = 3f; - visualElement.style.marginBottom = 5f; - - return visualElement; - } - - private ObjectField GetTargetObjectField(VisualElement parentContainer, - SerializedProperty objectProperty, - string objectPropertyLabel) - { - ObjectField targetObjectField = new ObjectField(objectPropertyLabel); - parentContainer.Add(targetObjectField); - targetObjectField.BindProperty(objectProperty); - targetObjectField.style.marginRight = 3f; - targetObjectField.labelElement.style.paddingTop = 0f; - return targetObjectField; - } - - private DropdownField CreateChosenMethodDropdownField(VisualElement parentContainer, - SerializedProperty methodNameProperty) - { - DropdownField functionDropdown = new DropdownField(Target_Function_Label); - parentContainer.Add(functionDropdown); - functionDropdown.BindProperty(methodNameProperty); - functionDropdown.style.marginRight = 3f; - functionDropdown.labelElement.style.paddingTop = 0f; - return functionDropdown; - } - - private void RegisterObjectFieldCallback(ObjectField targetObjectField, DropdownField functionDropdown) - { - targetObjectField.RegisterValueChangedCallback(changeEvent => - { - // hadPreviousValue has to be checked because this event will be raised after you bind a property and the object field is created - - bool hasNewValue = changeEvent.newValue != null; - bool hadPreviousValue = changeEvent.previousValue != null; - if (!hasNewValue - || - (hasNewValue && hadPreviousValue && changeEvent.previousValue != changeEvent.newValue)) - { - functionDropdown.value = null; - } - - functionDropdown.SetEnabled(hasNewValue); - }); - } - - private void AssignGenericOSMenuValueToDropdownField(CachedPropertiesAndObjectsData data) - { - data.DropdownField.AssignGenericMenu(() => - BuildGenericMenu(data)); - } - - private void AssignFormattingCallbackToDropdownField(CachedPropertiesAndObjectsData data) - { - data.DropdownField.AssignFormattingCallback( - _ => FormatFunctionValueSelected(data)); - } - - private string FormatFunctionValueSelected(CachedPropertiesAndObjectsData data) - { - Object obj = data.ObjectField.value; - string methodNameValue = data.TargetMethodProperty.stringValue; - - if (obj == null || string.IsNullOrWhiteSpace(methodNameValue)) return No_Function_Label; - - if (!IsPersistantListenerValid(data.FuncProperty, data.TargetObjectProperty, data.TargetMethodProperty)) - { - return GetMissingComponentMethodString(data.TargetObjectProperty, data.TargetMethodProperty); - } - - methodNameValue = NicifyGetterPropertyName(methodNameValue); - return $"{obj.GetType().Name}.{methodNameValue}"; - } - - #endregion - - #endregion - - #region Common Utility - - private string GetHeaderText(SerializedProperty property, string labelText) - { - return $"{(string.IsNullOrEmpty(labelText) ? "Func" : labelText)} {GetEventParams(property)}"; - } - - private string GetEventParams(SerializedProperty property) - { - string funcReturnValue = GetFuncReturnTypeFromProperty(property).NicifyTypeName(); - return $"({funcReturnValue})"; - } - - private Type GetFuncReturnTypeFromProperty(SerializedProperty funcProperty) - { - return funcProperty.GetBoxedValue().GetType().GetGenericArguments().Last(); - } - - private string GetMissingComponentMethodString(SerializedProperty objectProperty, - SerializedProperty methodProperty) - { - string arg = "UnknownComponent"; - Object objectValue = objectProperty.objectReferenceValue; - if (objectValue != null) - { - arg = objectValue.GetType().Name; - } - - return $""; - } - - #endregion - - #region Generic Menu Building - - private GenericMenu BuildGenericMenu(CachedPropertiesAndObjectsData data) - { - Object target = data.TargetObjectProperty.objectReferenceValue; - - if (target is Component targetComponent) - { - target = targetComponent.gameObject; - } - - SerializedProperty methodNameProperty = data.FuncProperty.FindPropertyRelative(MethodName_Property_Name); - - GenericMenu genericMenu = new GenericMenu(); - - genericMenu.AddItem(new GUIContent(No_Function_Label), string.IsNullOrEmpty(methodNameProperty.stringValue), ClearMethodNameProperty, methodNameProperty); - - if (target == null) return genericMenu; - - genericMenu.AddSeparator(""); - - Type returnType = GetFuncReturnTypeFromProperty(data.FuncProperty); - - Component[] components = ((GameObject)target).GetComponents(); - - DrawMenuForComponent(genericMenu, data, (GameObject)target, returnType); - - foreach (Component component in components) - { - DrawMenuForComponent(genericMenu, data, component, returnType); - } - - return genericMenu; - } - - private void DrawMenuForComponent(GenericMenu genericMenu, - CachedPropertiesAndObjectsData data, - Object component, - Type returnType) - { - Type componentType = component.GetType(); - string componentName = componentType.Name; - - MethodInfo[] suitableProperties = GetSuitablePropertiesFromType(componentType, returnType); - - if (suitableProperties.Length > 0) - { - GUIContent propertiesContent = new GUIContent($"{componentName}/Property Getters"); - genericMenu.AddDisabledItem(propertiesContent); - - foreach (MethodInfo propertyGetter in suitableProperties) - { - string propertyName = NicifyGetterPropertyName(propertyGetter); - GUIContent methodContent = new GUIContent($"{componentName}/{propertyName}"); - - SelectedMethodInfo info = new SelectedMethodInfo(component, propertyGetter); - - bool isOn = IsMethodChosen(data.TargetObjectProperty, data.TargetMethodProperty, component, propertyGetter); - - var menuSelectedCallback = GetMenuSelectedAction(data); - genericMenu.AddItem(methodContent, isOn, menuSelectedCallback, info); - } - - genericMenu.AddSeparator($"{componentName}/"); - } - - MethodInfo[] suitableMethods = GetSuitableMethodsFromType(componentType, returnType); - - if (suitableMethods.Length > 0) - { - GUIContent methodsContent = new GUIContent($"{componentName}/Invokable Methods"); - genericMenu.AddDisabledItem(methodsContent); - - foreach (MethodInfo method in suitableMethods) - { - GUIContent methodContent = new GUIContent($"{componentName}/{method.Name}"); - - SelectedMethodInfo info = new SelectedMethodInfo(component, method); - - bool isOn = IsMethodChosen(data.TargetObjectProperty, data.TargetMethodProperty, component, method); - - var menuSelectedCallback = GetMenuSelectedAction(data); - genericMenu.AddItem(methodContent, isOn, menuSelectedCallback, info); - } - } - } - - private bool IsMethodChosen(SerializedProperty targetObjectProperty, - SerializedProperty targetMethodProperty, - Object component, - MethodInfo method) - { - if (string.IsNullOrWhiteSpace(targetMethodProperty.stringValue)) return false; - - return targetObjectProperty.objectReferenceValue == component - && string.Equals(targetMethodProperty.stringValue, method.Name); - } - - private GenericMenu.MenuFunction2 GetMenuSelectedAction(CachedPropertiesAndObjectsData data) - { - return obj => HandleNewMethodAssigned(obj, data); - } - - #region Method List Getters - - private MethodInfo[] GetSuitableMethodsFromType(Type componentType, Type returnType) - { - IEnumerable suitableMethods = componentType.GetMethods(BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Instance) - .Where(x => IsSuitableMethodInfo(x, returnType)) - .OrderBy(x => x.Name); - - return suitableMethods.ToArray(); - } - - private MethodInfo[] GetSuitablePropertiesFromType(Type componentType, Type returnType) - { - IEnumerable suitableProperties = componentType.GetProperties(BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Instance) - .Where(x => IsSuitableProperty(x, returnType)); - - IEnumerable propertyGetters = suitableProperties - .Select(x => x.GetGetMethod()) - .OrderBy(x => x.Name); - - return propertyGetters.ToArray(); - } - - private bool IsSuitableMethodInfo(MethodInfo info, Type returnType) - { - return !info.IsSpecialName - && info.ReturnType == returnType - && info.GetParameters().Length == 0; - } - - private bool IsSuitableProperty(PropertyInfo propertyInfo, Type returnType) - { - if (propertyInfo.GetCustomAttributes(typeof(ObsoleteAttribute), inherit: true).Length != 0) return false; - - MethodInfo getMethod = propertyInfo.GetGetMethod(); - if (getMethod == null) return false; - - return getMethod.ReturnType == returnType; - } - - #endregion - - #region Event Handlers - - private void ClearMethodNameProperty(object methodNameProperty) - { - SerializedProperty nameProperty = (SerializedProperty)methodNameProperty; - - nameProperty.stringValue = string.Empty; - nameProperty.serializedObject.ApplyModifiedProperties(); - } - - private void HandleNewMethodAssigned(object selectedInfo, CachedPropertiesAndObjectsData data) - { - SelectedMethodInfo info = (SelectedMethodInfo)selectedInfo; - - // If we're drawing with UI Toolkit, the update will be a bit stoopid when you bind a property - // Assigning a value without notify ensures that we don't accidentally set the function dropdown value to a null value - // This has to work this way because when we assign the object to get the method from, we may have to change the object. Problem being, if we try to change the DropdownField's value or property within the object change event, the assert will fail => the SerializedProperty bound to the Dropdown Field will have a different value than the dropdown field itself for some reason - // Unity Event has the same bug when drawn with UI Toolkit, so this is a workaround :> - ObjectField targetObjectField = data.ObjectField; - targetObjectField?.SetValueWithoutNotify(info.TargetObject); - - data.TargetObjectProperty.objectReferenceValue = info.TargetObject; -#if UNITY_2023_1_OR_NEWER - -#else - // Has to happen before we reassign the value, so an additional annoying block of ifs here - bool isSameString = string.Equals(data.TargetMethodProperty.stringValue, info.TargetMethod.Name); -#endif - - data.TargetMethodProperty.stringValue = info.TargetMethod.Name; - -#if UNITY_2023_1_OR_NEWER - -#else - // Older Unity version doesn't automatically use the formatting callback if the old value is the same as the new value - // This is a problem because properties or methods can have the same name on different objects (gameobject.name and transform.name, for example) - // The easiest way to call for formatting to happen is to just reassign the callback - // Only relevant to my fav boi UI toolkit - DropdownField dropdownField = data.DropdownField; - if (isSameString && dropdownField != null) - { - AssignFormattingCallbackToDropdownField(data); - } -#endif - data.TargetMethodProperty.serializedObject.ApplyModifiedProperties(); - } - -#endregion - - #region Utility - - #region Strings - - private string NicifyGetterPropertyName(MethodInfo methodInfo) - { - return NicifyGetterPropertyName(methodInfo.Name); - } - - private string NicifyGetterPropertyName(string methodName) - { - return $"{methodName.Replace("get_", string.Empty)}"; - } - - #endregion - - #endregion - -#endregion - - #region Serialized Property Getters - - private SerializedProperty GetTargetObjectSerializedProperty(SerializedProperty funcProperty) - { - SerializedProperty objectProperty = funcProperty.FindPropertyRelative(TargetObject_Property_Name); - return objectProperty; - } - - private SerializedProperty GetMethodNameSerializedProperty(SerializedProperty funcProperty) - { - SerializedProperty methodNameProperty = funcProperty.FindPropertyRelative(MethodName_Property_Name); - return methodNameProperty; - } - - #endregion - - #region Helper Classes - - private struct SelectedMethodInfo - { - public Object TargetObject; - public MethodInfo TargetMethod; - - public SelectedMethodInfo(Object obj, MethodInfo method) - { - TargetObject = obj; - TargetMethod = method; - } - } - - private struct CachedPropertiesAndObjectsData - { - public SerializedProperty FuncProperty; - public SerializedProperty TargetObjectProperty; - public SerializedProperty TargetMethodProperty; - - public ObjectField ObjectField; - public DropdownField DropdownField; - - public CachedPropertiesAndObjectsData(SerializedProperty funcProperty, - SerializedProperty targetObjectProperty, - SerializedProperty targetMethodProperty, - ObjectField objectField, - DropdownField dropdownField) - { - FuncProperty = funcProperty; - TargetObjectProperty = targetObjectProperty; - TargetMethodProperty = targetMethodProperty; - - ObjectField = objectField; - DropdownField = dropdownField; - } - } - - #endregion - } -} - -#endif +#if UNITY_EDITOR + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using UnityEditor; +using UnityEditor.UIElements; +using UnityEditorInternal; +using UnityEngine; +using UnityEngine.UIElements; +using Utilities.Extensions.SystemExtensions; +using Utilities.Extensions.UIToolkit; +using Utilities.SerializableData.SerializableFunc.UnityEditorUtilities; +using Object = UnityEngine.Object; + +namespace Utilities.SerializableData.SerializableFunc.UnityEditorDrawers +{ + [CustomPropertyDrawer(typeof(SerializableFuncBase<>), true)] + public class SerializableFuncBasePropertyDrawer : PropertyDrawer + { + private const string Target_Function_Label = "Target Function"; + private const string Target_Object_Label = "Target Object"; + private const string No_Function_Label = "No Function"; + + private const string TargetObject_Property_Name = "targetObject"; + private const string MethodName_Property_Name = "methodName"; + + private const string MixedValueContent_Property_Name = "mixedValueContent"; + + private static BindingFlags SuitableMethodsFlags = BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Instance; + + #region GUI Drawing + + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + EditorGUI.BeginProperty(position, GUIContent.none, property); + + if (Event.current.type == EventType.Repaint) + { + ReorderableList.defaultBehaviours.DrawHeaderBackground(position); + } + + Rect headerRect = position; + + headerRect.xMin += 6f; + headerRect.xMax -= 6f; + headerRect.height -= 2f; + headerRect.y += 1f; + + DrawFuncHeader(ref headerRect, property, label.text); + + DrawPropertyView(ref position, ref headerRect, property); + + EditorGUI.EndProperty(); + } + + private void DrawFuncHeader(ref Rect position, SerializedProperty funcProperty, string labelText) + { + position.height = 18f; + string text = GetHeaderText(funcProperty, labelText); + GUI.Label(position, text); + } + + private void DrawPropertyView(ref Rect position, ref Rect headerRect, SerializedProperty funcProperty) + { + Rect listRect = new Rect(position.x, headerRect.y + headerRect.height, position.width, 45f); + + int indentLevel = EditorGUI.indentLevel; + EditorGUI.indentLevel = 0; + if (Event.current.type == EventType.Repaint) + { + ReorderableList.defaultBehaviours.boxBackground.Draw(listRect, isHover: false, isActive: false, on: false, hasKeyboardFocus: false); + } + + listRect.yMin += 1f; + listRect.yMax -= 4f; + listRect.xMin += 1f; + listRect.xMax -= 1f; + + SerializedProperty targetObjectProperty = GetTargetObjectSerializedProperty(funcProperty); + SerializedProperty targetMethodProperty = GetMethodNameSerializedProperty(funcProperty); + EditorGUI.BeginChangeCheck(); + + Rect targetObjectLineRect = GetListViewSingleLineRect(ref position, ref listRect); + Rect[] targetObjectRects = GetTwoRectsForLabelAndProperty(ref targetObjectLineRect); + + EditorGUI.LabelField(targetObjectRects[0], Target_Object_Label); + EditorGUI.PropertyField(targetObjectRects[1], targetObjectProperty, GUIContent.none); + + if (EditorGUI.EndChangeCheck()) + { + targetMethodProperty.stringValue = string.Empty; + targetMethodProperty.serializedObject.ApplyModifiedProperties(); + } + + DrawMethodNameProperty(ref targetObjectLineRect, funcProperty, targetObjectProperty, targetMethodProperty); + + EditorGUI.indentLevel = indentLevel; + } + + private Rect DrawMethodNameProperty(ref Rect previousRect, + SerializedProperty funcProperty, + SerializedProperty targetObjectSerializedProperty, + SerializedProperty targetMethodSerializedProperty) + { + Rect targetMethodRect = GetNextSingleLineRect(ref previousRect); + Rect[] targetMethodRects = GetTwoRectsForLabelAndProperty(ref targetMethodRect); + + EditorGUI.LabelField(targetMethodRects[0], Target_Function_Label); + + using (new EditorGUI.DisabledScope(targetObjectSerializedProperty.objectReferenceValue == null)) + { + EditorGUI.BeginProperty(targetMethodRect, GUIContent.none, targetMethodSerializedProperty); + + GUIContent content; + if (EditorGUI.showMixedValue) + { + content = GetMixedValueContentGUIContent(); + } + else + { + StringBuilder stringBuilder = new StringBuilder(); + + if (targetObjectSerializedProperty.objectReferenceValue == null + || string.IsNullOrEmpty(targetMethodSerializedProperty.stringValue)) + { + stringBuilder.Append(No_Function_Label); + } + else if (!IsPersistantListenerValid(funcProperty, targetObjectSerializedProperty, targetMethodSerializedProperty)) + { + string missingComponentString = GetMissingComponentMethodString(targetObjectSerializedProperty, targetMethodSerializedProperty); + + stringBuilder.Append(missingComponentString); + } + else + { + stringBuilder.Append(targetObjectSerializedProperty.objectReferenceValue.GetType().Name); + if (!string.IsNullOrEmpty(targetMethodSerializedProperty.stringValue)) + { + stringBuilder.Append("."); + string nicerName = NicifyGetterPropertyName(targetMethodSerializedProperty.stringValue); + stringBuilder.Append(nicerName); + } + } + + content = new GUIContent(stringBuilder.ToString()); + } + + if (EditorGUI.DropdownButton(targetMethodRects[1], content, FocusType.Passive, EditorStyles.popup)) + { + CachedPropertiesAndObjectsData data = new CachedPropertiesAndObjectsData(funcProperty, targetObjectSerializedProperty, targetMethodSerializedProperty, null, null); + + BuildGenericMenu(data).DropDown(previousRect); + } + + EditorGUI.EndProperty(); + } + + return targetMethodRect; + } + + private bool IsPersistantListenerValid(SerializedProperty funcProperty, + SerializedProperty targetObjectProperty, + SerializedProperty targetMethodProperty) + { + if (targetObjectProperty.objectReferenceValue == null + || string.IsNullOrWhiteSpace(targetMethodProperty.stringValue)) return false; + + Type[] funcArguments = GetFuncTypeArguments(funcProperty); + Type returnType = funcArguments.Last(); + + MethodInfo targetMethod = targetObjectProperty.objectReferenceValue + .GetType() + .GetMethods(SuitableMethodsFlags) + .Where(x => IsSuitableMethodInfo(x, funcArguments)) + .FirstOrDefault(x => string.Equals(x.Name, targetMethodProperty.stringValue)); + + if (targetMethod != null) return true; + if ((funcArguments.Length - 1) != 0) return false; + + MethodInfo targetProperty = targetObjectProperty.objectReferenceValue + .GetType() + .GetProperties(SuitableMethodsFlags) + .Where(x => IsSuitableProperty(x, returnType)) + .Select(x => x.GetGetMethod()) + .FirstOrDefault(x => string.Equals(x.Name, targetMethodProperty.stringValue)); + + return targetProperty != null; + } + + private GUIContent GetMixedValueContentGUIContent() + { + MethodInfo info = typeof(EditorGUI) + .GetMethod(MixedValueContent_Property_Name, BindingFlags.FlattenHierarchy | BindingFlags.Static | BindingFlags.Public); + + return info.Invoke(null, null) as GUIContent; + } + + #region Rect Utility + + private Rect GetListViewSingleLineRect(ref Rect position, ref Rect listRect) + { + Rect result = new Rect(position.x, + listRect.y + EditorGUIUtility.standardVerticalSpacing, + listRect.width, + EditorGUIUtility.singleLineHeight); + + result.xMin += 8f; + result.xMax -= 3f; + + return result; + } + + private Rect GetNextSingleLineRect(ref Rect previousRect) + { + return new Rect(previousRect.x, + previousRect.yMax + EditorGUIUtility.standardVerticalSpacing, + previousRect.width, + EditorGUIUtility.singleLineHeight); + } + + private Rect[] GetTwoRectsForLabelAndProperty(ref Rect lineRect) + { + Rect labelRect = new Rect(lineRect.position.x, + lineRect.position.y, + lineRect.width / 3 - 1f, + lineRect.height); + + float propertyRectX = labelRect.xMax + 2f; + float propertyRectWidth = lineRect.xMax - propertyRectX; + + Rect propertyRect = new Rect( + propertyRectX, + labelRect.y, + propertyRectWidth, + lineRect.height); + + return new Rect[] { labelRect, propertyRect }; + } + + #endregion + + #endregion + + #region UI Toolkit Drawing + + public override VisualElement CreatePropertyGUI(SerializedProperty property) + { + VisualElement visualElement = new VisualElement(); + visualElement.AddToClassList("unity-event__container"); + + Label label = new Label(); + label.text = GetHeaderText(property, property.displayName); + label.tooltip = property.tooltip; + label.AddToClassList("unity-list-view__header"); + label.style.overflow = new StyleEnum(Overflow.Hidden); + + VisualElement assignmentVE = CreateFuncAssignmentVisualElement(property); + + visualElement.Add(label); + visualElement.Add(assignmentVE); + + return visualElement; + } + + #region Visible Container Drawing + + private VisualElement CreateFuncAssignmentVisualElement(SerializedProperty funcProperty) + { + VisualElement visualElement = CreateContainerVisualElement(); + + SerializedProperty objectProperty = GetTargetObjectSerializedProperty(funcProperty); + string objectPropertyLabel = ObjectNames.NicifyVariableName(TargetObject_Property_Name); + + ObjectField targetObjectField = GetTargetObjectField(visualElement, objectProperty, objectPropertyLabel); + + SerializedProperty methodNameProperty = GetMethodNameSerializedProperty(funcProperty); + DropdownField functionDropdown = CreateChosenMethodDropdownField(visualElement, methodNameProperty); + + RegisterObjectFieldCallback(targetObjectField, functionDropdown); + + CachedPropertiesAndObjectsData data = new CachedPropertiesAndObjectsData(funcProperty, objectProperty, methodNameProperty, targetObjectField, functionDropdown); + + AssignGenericOSMenuValueToDropdownField(data); + + AssignFormattingCallbackToDropdownField(data); + + return visualElement; + } + + private VisualElement CreateContainerVisualElement() + { + VisualElement visualElement = new VisualElement(); + + visualElement.AddToClassList("unity-scroll-view"); + visualElement.AddToClassList("unity-collection-view__scroll-view"); + visualElement.AddToClassList("unity-collection-view--with-border"); + visualElement.AddToClassList("unity-list-view__scroll-view--with-footer"); + visualElement.AddToClassList("unity-event__list-view-scroll-view"); + + visualElement.style.paddingBottom = 5f; + visualElement.style.paddingLeft = 3f; + visualElement.style.paddingRight = 3f; + visualElement.style.marginBottom = 5f; + + return visualElement; + } + + private ObjectField GetTargetObjectField(VisualElement parentContainer, + SerializedProperty objectProperty, + string objectPropertyLabel) + { + ObjectField targetObjectField = new ObjectField(objectPropertyLabel); + parentContainer.Add(targetObjectField); + targetObjectField.BindProperty(objectProperty); + targetObjectField.style.marginRight = 3f; + targetObjectField.labelElement.style.paddingTop = 0f; + return targetObjectField; + } + + private DropdownField CreateChosenMethodDropdownField(VisualElement parentContainer, + SerializedProperty methodNameProperty) + { + DropdownField functionDropdown = new DropdownField(Target_Function_Label); + parentContainer.Add(functionDropdown); + functionDropdown.BindProperty(methodNameProperty); + functionDropdown.style.marginRight = 3f; + functionDropdown.labelElement.style.paddingTop = 0f; + return functionDropdown; + } + + private void RegisterObjectFieldCallback(ObjectField targetObjectField, DropdownField functionDropdown) + { + targetObjectField.RegisterValueChangedCallback(changeEvent => + { + // hadPreviousValue has to be checked because this event will be raised after you bind a property and the object field is created + + bool hasNewValue = changeEvent.newValue != null; + bool hadPreviousValue = changeEvent.previousValue != null; + if (!hasNewValue + || + (hasNewValue && hadPreviousValue && changeEvent.previousValue != changeEvent.newValue)) + { + functionDropdown.value = null; + } + + functionDropdown.SetEnabled(hasNewValue); + }); + } + + private void AssignGenericOSMenuValueToDropdownField(CachedPropertiesAndObjectsData data) + { + data.DropdownField.AssignGenericMenu(() => + BuildGenericMenu(data)); + } + + private void AssignFormattingCallbackToDropdownField(CachedPropertiesAndObjectsData data) + { + data.DropdownField.AssignFormattingCallback( + _ => FormatFunctionValueSelected(data)); + } + + private string FormatFunctionValueSelected(CachedPropertiesAndObjectsData data) + { + Object obj = data.ObjectField.value; + string methodNameValue = data.TargetMethodProperty.stringValue; + + if (obj == null || string.IsNullOrWhiteSpace(methodNameValue)) return No_Function_Label; + + if (!IsPersistantListenerValid(data.FuncProperty, data.TargetObjectProperty, data.TargetMethodProperty)) + { + return GetMissingComponentMethodString(data.TargetObjectProperty, data.TargetMethodProperty); + } + + methodNameValue = NicifyGetterPropertyName(methodNameValue); + return $"{obj.GetType().Name}.{methodNameValue}"; + } + + #endregion + + #endregion + + #region Common Utility + + private string GetHeaderText(SerializedProperty property, string labelText) + { + return $"{(string.IsNullOrEmpty(labelText) ? "Func" : labelText)} {GetEventParamsString(property)}"; + } + + private string GetEventParamsString(SerializedProperty property) + { + IEnumerable typeNames = GetFuncTypeArguments(property) + .Select(x => x.NicifyTypeName()); + + string funcReturnValue = string.Join(", ", typeNames); + return $"({funcReturnValue})"; + } + + private Type[] GetFuncTypeArguments(SerializedProperty funcProperty) + { + return funcProperty.GetBoxedValue(). + GetType(). + GetGenericArguments(); + } + + private string GetMissingComponentMethodString(SerializedProperty objectProperty, + SerializedProperty methodProperty) + { + string arg = "UnknownComponent"; + Object objectValue = objectProperty.objectReferenceValue; + if (objectValue != null) + { + arg = objectValue.GetType().Name; + } + + return $""; + } + + #endregion + + #region Generic Menu Building + + private GenericMenu BuildGenericMenu(CachedPropertiesAndObjectsData data) + { + Object target = data.TargetObjectProperty.objectReferenceValue; + + if (target is Component targetComponent) + { + target = targetComponent.gameObject; + } + + SerializedProperty methodNameProperty = data.FuncProperty.FindPropertyRelative(MethodName_Property_Name); + + GenericMenu genericMenu = new GenericMenu(); + + genericMenu.AddItem(new GUIContent(No_Function_Label), string.IsNullOrEmpty(methodNameProperty.stringValue), ClearMethodNameProperty, methodNameProperty); + + if (target == null) return genericMenu; + + genericMenu.AddSeparator(""); + + Type[] funcParameters = GetFuncTypeArguments(data.FuncProperty); + Component[] components = ((GameObject)target).GetComponents(); + + DrawMenuForComponent(genericMenu, data, (GameObject)target, funcParameters); + + foreach (Component component in components) + { + DrawMenuForComponent(genericMenu, data, component, funcParameters); + } + + return genericMenu; + } + + private void DrawMenuForComponent(GenericMenu genericMenu, + CachedPropertiesAndObjectsData data, + Object component, + Type[] funcParameters) + { + Type componentType = component.GetType(); + string componentName = componentType.Name; + + MethodInfo[] suitableProperties = GetSuitablePropertiesFromType(componentType, funcParameters); + + if (suitableProperties != null && suitableProperties.Any()) + { + GUIContent propertiesContent = new GUIContent($"{componentName}/Property Getters"); + genericMenu.AddDisabledItem(propertiesContent); + + foreach (MethodInfo propertyGetter in suitableProperties) + { + string propertyName = NicifyGetterPropertyName(propertyGetter); + GUIContent methodContent = new GUIContent($"{componentName}/{propertyName}"); + + SelectedMethodInfo info = new SelectedMethodInfo(component, propertyGetter); + + bool isOn = IsMethodChosen(data.TargetObjectProperty, data.TargetMethodProperty, component, propertyGetter); + + var menuSelectedCallback = GetMenuSelectedAction(data); + genericMenu.AddItem(methodContent, isOn, menuSelectedCallback, info); + } + + genericMenu.AddSeparator($"{componentName}/"); + } + + MethodInfo[] suitableMethods = GetSuitableMethodsFromType(componentType, funcParameters); + + if (suitableMethods.Any()) + { + GUIContent methodsContent = new GUIContent($"{componentName}/Invokable Methods"); + genericMenu.AddDisabledItem(methodsContent); + + foreach (MethodInfo method in suitableMethods) + { + GUIContent methodContent = new GUIContent($"{componentName}/{method.Name}"); + + SelectedMethodInfo info = new SelectedMethodInfo(component, method); + + bool isOn = IsMethodChosen(data.TargetObjectProperty, data.TargetMethodProperty, component, method); + + var menuSelectedCallback = GetMenuSelectedAction(data); + genericMenu.AddItem(methodContent, isOn, menuSelectedCallback, info); + } + } + } + + private bool IsMethodChosen(SerializedProperty targetObjectProperty, + SerializedProperty targetMethodProperty, + Object component, + MethodInfo method) + { + if (string.IsNullOrWhiteSpace(targetMethodProperty.stringValue)) return false; + + return targetObjectProperty.objectReferenceValue == component + && string.Equals(targetMethodProperty.stringValue, method.Name); + } + + private GenericMenu.MenuFunction2 GetMenuSelectedAction(CachedPropertiesAndObjectsData data) + { + return obj => HandleNewMethodAssigned(obj, data); + } + + #region Method List Getters + + private MethodInfo[] GetSuitableMethodsFromType(Type componentType, Type[] funcParameters) + { + IEnumerable suitableMethods = componentType + .GetMethods(SuitableMethodsFlags) + .Where(x => IsSuitableMethodInfo(x, funcParameters)) + .OrderBy(x => x.Name); + + return suitableMethods.ToArray(); + } + + private MethodInfo[] GetSuitablePropertiesFromType(Type componentType, Type[] funcParameters) + { + if (funcParameters.Length != 1) return null; + Type returnType = funcParameters.Last(); + + IEnumerable suitableProperties = componentType + .GetProperties(SuitableMethodsFlags) + .Where(x => IsSuitableProperty(x, returnType)); + + IEnumerable propertyGetters = suitableProperties + .Select(x => x.GetGetMethod()) + .OrderBy(x => x.Name); + + return propertyGetters.ToArray(); + } + + private bool IsSuitableMethodInfo(MethodInfo info, Type[] funcParameters) + { + if (info.IsSpecialName) return false; + + Type returnType = funcParameters.Last(); + if (info.ReturnType != returnType) return false; + + ParameterInfo[] parameters = info.GetParameters(); + if (parameters.Length != (funcParameters.Length - 1)) return false; + + for (int i = 0; i < parameters.Length; i++) + { + ParameterInfo parameter = parameters[i]; + Type funcParameterType = funcParameters[i]; + + if (parameter.ParameterType != funcParameterType) return false; + } + + return true; + } + + private bool IsSuitableProperty(PropertyInfo propertyInfo, Type returnType) + { + if (propertyInfo.GetCustomAttributes(typeof(ObsoleteAttribute), inherit: true).Length != 0) return false; + + MethodInfo getMethod = propertyInfo.GetGetMethod(); + if (getMethod == null) return false; + + return getMethod.ReturnType == returnType; + } + + #endregion + + #region Event Handlers + + private void ClearMethodNameProperty(object methodNameProperty) + { + SerializedProperty nameProperty = (SerializedProperty)methodNameProperty; + + nameProperty.stringValue = string.Empty; + nameProperty.serializedObject.ApplyModifiedProperties(); + } + + private void HandleNewMethodAssigned(object selectedInfo, CachedPropertiesAndObjectsData data) + { + SelectedMethodInfo info = (SelectedMethodInfo)selectedInfo; + + // If we're drawing with UI Toolkit, the update will be a bit stoopid when you bind a property + // Assigning a value without notify ensures that we don't accidentally set the function dropdown value to a null value + // This has to work this way because when we assign the object to get the method from, we may have to change the object. Problem being, if we try to change the DropdownField's value or property within the object change event, the assert will fail => the SerializedProperty bound to the Dropdown Field will have a different value than the dropdown field itself for some reason + // Unity Event has the same bug when drawn with UI Toolkit, so this is a workaround :> + ObjectField targetObjectField = data.ObjectField; + targetObjectField?.SetValueWithoutNotify(info.TargetObject); + + data.TargetObjectProperty.objectReferenceValue = info.TargetObject; +#if !UNITY_2023_1_OR_NEWER + // Has to happen before we reassign the value, so an additional annoying block of ifs here + bool isSameString = string.Equals(data.TargetMethodProperty.stringValue, info.TargetMethod.Name); +#endif + + data.TargetMethodProperty.stringValue = info.TargetMethod.Name; + +#if !UNITY_2023_1_OR_NEWER + // Older Unity version doesn't automatically use the formatting callback if the old value is the same as the new value + // This is a problem because properties or methods can have the same name on different objects (gameobject.name and transform.name, for example) + // The easiest way to call for formatting to happen is to just reassign the callback + // Only relevant to my fav boi UI toolkit + DropdownField dropdownField = data.DropdownField; + if (isSameString && dropdownField != null) + { + AssignFormattingCallbackToDropdownField(data); + } +#endif + data.TargetMethodProperty.serializedObject.ApplyModifiedProperties(); + } + + #endregion + + #region Utility + + #region Strings + + private string NicifyGetterPropertyName(MethodInfo methodInfo) + { + return NicifyGetterPropertyName(methodInfo.Name); + } + + private string NicifyGetterPropertyName(string methodName) + { + return $"{methodName.Replace("get_", string.Empty)}"; + } + + #endregion + + #endregion + + #endregion + + #region Serialized Property Getters + + private SerializedProperty GetTargetObjectSerializedProperty(SerializedProperty funcProperty) + { + SerializedProperty objectProperty = funcProperty.FindPropertyRelative(TargetObject_Property_Name); + return objectProperty; + } + + private SerializedProperty GetMethodNameSerializedProperty(SerializedProperty funcProperty) + { + SerializedProperty methodNameProperty = funcProperty.FindPropertyRelative(MethodName_Property_Name); + return methodNameProperty; + } + + #endregion + + #region GUI Overrides + + public override float GetPropertyHeight(SerializedProperty property, GUIContent label) + { + return 65f; + } + + #endregion + + #region Helper Classes + + private struct SelectedMethodInfo + { + public Object TargetObject; + public MethodInfo TargetMethod; + + public SelectedMethodInfo(Object obj, MethodInfo method) + { + TargetObject = obj; + TargetMethod = method; + } + } + + private struct CachedPropertiesAndObjectsData + { + public SerializedProperty FuncProperty; + public SerializedProperty TargetObjectProperty; + public SerializedProperty TargetMethodProperty; + + public ObjectField ObjectField; + public DropdownField DropdownField; + + public CachedPropertiesAndObjectsData(SerializedProperty funcProperty, + SerializedProperty targetObjectProperty, + SerializedProperty targetMethodProperty, + ObjectField objectField, + DropdownField dropdownField) + { + FuncProperty = funcProperty; + TargetObjectProperty = targetObjectProperty; + TargetMethodProperty = targetMethodProperty; + + ObjectField = objectField; + DropdownField = dropdownField; + } + } + + #endregion + } +} + +#endif \ No newline at end of file diff --git a/Editor/Property Drawer/SerializableFuncPropertyDrawer.cs.meta b/Editor/PropertyDrawer/SerializableFuncBasePropertyDrawer.cs.meta similarity index 83% rename from Editor/Property Drawer/SerializableFuncPropertyDrawer.cs.meta rename to Editor/PropertyDrawer/SerializableFuncBasePropertyDrawer.cs.meta index 4b29732..fb8b413 100644 --- a/Editor/Property Drawer/SerializableFuncPropertyDrawer.cs.meta +++ b/Editor/PropertyDrawer/SerializableFuncBasePropertyDrawer.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 33baceefadfdeb945aa7f92e190518f0 +guid: a27853cfcf7edf94fa6821cd44d52513 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/README.md b/README.md index 5ca7fdd..3aee501 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ Allows you to assign Func\ via the Inspector. Looks and acts like a UnityEvent. Supports both GUI and UI Toolkit. Tested with Unity 2020.3, 2021.3, 2022.2, 2023.1. Tested in standalone Windows and Android builds, both with Mono and IL2CPP. +Version 1.1 supports generic parameters. +If there are no generic parameters for the func, you'll also be able to assign getter properties. GUI Representation ![unity_inspector](https://i.imgur.com/pR4uo7H.png) @@ -17,11 +19,15 @@ public class ExampleClass : MonoBehaviour { [Header("My Bool Func")] [SerializeField] private SerializableFunc boolFunc; + [SerializeField] private SerializableFunc stringFunc; private void Start() { bool result = boolFunc.Invoke(); Debug.Log(result); + + string stringResult = stringFunc.Invoke(69); + Debug.Log(stringResult); } } ``` diff --git a/Runtime/SerializableFunc/Base.meta b/Runtime/SerializableFunc/Base.meta new file mode 100644 index 0000000..b0f4a36 --- /dev/null +++ b/Runtime/SerializableFunc/Base.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4bd647a4a04c7f947afa36dc0ab3e8bf +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/SerializableFunc/Base/SerializableFuncBase.cs b/Runtime/SerializableFunc/Base/SerializableFuncBase.cs new file mode 100644 index 0000000..2135e8f --- /dev/null +++ b/Runtime/SerializableFunc/Base/SerializableFuncBase.cs @@ -0,0 +1,83 @@ +using System; +using System.Linq; +using System.Reflection; +using UnityEngine; +using Object = UnityEngine.Object; + +[System.Serializable] +public abstract class SerializableFuncBase + where TFuncType : Delegate +{ + [SerializeField] protected Object targetObject; + [SerializeField] protected string methodName; + + private TFuncType func; + + private static BindingFlags SuitableMethodsFlags = BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Instance; + + protected TFuncType GetReturnedFunc() + { + if (func == null) + { + if (targetObject == null) + { + throw new ArgumentNullException("Target Object is null!"); + } + + if (string.IsNullOrWhiteSpace(methodName)) + { + throw new ArgumentNullException("Target Method is null!"); + } + + Type funcType = typeof(TFuncType); + + MethodInfo info = targetObject + .GetType() + .GetMethods(SuitableMethodsFlags) + .FirstOrDefault(x => IsTargetMethodInfo(x, funcType)); + + if (info == null) + { + throw new MissingMethodException($"Object \"{targetObject.name}\" is missing target method: {methodName}"); + } + + func = (TFuncType)Delegate.CreateDelegate(funcType, targetObject, methodName); + } + + return func; + } + + #region Utility Functions + + private bool IsTargetMethodInfo(MethodInfo methodInfo, Type funcType) + { + if (string.Equals(methodInfo.Name, methodName, StringComparison.InvariantCulture)) return false; + + Type[] typeArguments = funcType.GetGenericArguments(); + + if (methodInfo.ReturnType != typeArguments.Last()) return false; + + ParameterInfo[] parameters = methodInfo.GetParameters(); + + if (parameters.Length != (typeArguments.Length - 1)) return false; + + for (int i = 0; i < parameters.Length; i++) + { + Type argType = typeArguments[i]; + ParameterInfo parameterInfo = parameters[i]; + if (argType != parameterInfo.ParameterType) return false; + } + + return true; + } + + #endregion + + public static implicit operator TFuncType(SerializableFuncBase func) + { + if (func == null) return null; + + TFuncType result = func.GetReturnedFunc(); + return result; + } +} diff --git a/Runtime/SerializableFunc/Base/SerializableFuncBase.cs.meta b/Runtime/SerializableFunc/Base/SerializableFuncBase.cs.meta new file mode 100644 index 0000000..cc23870 --- /dev/null +++ b/Runtime/SerializableFunc/Base/SerializableFuncBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fa96f9624fc8bad459ec0bc927427de8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/SerializableFunc/Implementations.meta b/Runtime/SerializableFunc/Implementations.meta new file mode 100644 index 0000000..0efafdc --- /dev/null +++ b/Runtime/SerializableFunc/Implementations.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a05eff3308be0b2489425421e455c65f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/SerializableFunc/Implementations/SerializableFunc.cs b/Runtime/SerializableFunc/Implementations/SerializableFunc.cs new file mode 100644 index 0000000..6a18cf8 --- /dev/null +++ b/Runtime/SerializableFunc/Implementations/SerializableFunc.cs @@ -0,0 +1,175 @@ +using System; + +[System.Serializable] +public class SerializableFunc : SerializableFuncBase> +{ + public TReturn Invoke() + { + Func func = GetReturnedFunc(); + return func(); + } +} + +[System.Serializable] +public class SerializableFunc : SerializableFuncBase> +{ + public TReturn Invoke(TArg0 arg0) + { + Func func = GetReturnedFunc(); + return func(arg0); + } +} + +[System.Serializable] +public class SerializableFunc : SerializableFuncBase> +{ + public TReturn Invoke(TArg0 arg0, TArg1 arg1) + { + Func func = GetReturnedFunc(); + return func(arg0, arg1); + } +} + +[System.Serializable] +public class SerializableFunc : SerializableFuncBase> +{ + public TReturn Invoke(TArg0 arg0, TArg1 arg1, TArg2 arg2) + { + Func func = GetReturnedFunc(); + return func(arg0, arg1, arg2); + } +} + +[System.Serializable] +public class SerializableFunc : SerializableFuncBase> +{ + public TReturn Invoke(TArg0 arg0, TArg1 arg1, TArg2 arg2, TArg3 arg3) + { + Func func = GetReturnedFunc(); + return func(arg0, arg1, arg2, arg3); + } +} + +[System.Serializable] +public class SerializableFunc : SerializableFuncBase> +{ + public TReturn Invoke(TArg0 arg0, TArg1 arg1, TArg2 arg2, TArg3 arg3, TArg4 arg4) + { + Func func = GetReturnedFunc(); + return func(arg0, arg1, arg2, arg3, arg4); + } +} + +[System.Serializable] +public class SerializableFunc : SerializableFuncBase> +{ + public TReturn Invoke(TArg0 arg0, TArg1 arg1, TArg2 arg2, TArg3 arg3, TArg4 arg4, TArg5 arg5) + { + Func func = GetReturnedFunc(); + return func(arg0, arg1, arg2, arg3, arg4, arg5); + } +} + +[System.Serializable] +public class SerializableFunc : SerializableFuncBase> +{ + public TReturn Invoke(TArg0 arg0, TArg1 arg1, TArg2 arg2, TArg3 arg3, TArg4 arg4, TArg5 arg5, TArg6 arg6) + { + Func func = GetReturnedFunc(); + return func(arg0, arg1, arg2, arg3, arg4, arg5, arg6); + } +} + +[System.Serializable] +public class SerializableFunc : SerializableFuncBase> +{ + public TReturn Invoke(TArg0 arg0, TArg1 arg1, TArg2 arg2, TArg3 arg3, TArg4 arg4, TArg5 arg5, TArg6 arg6, TArg7 arg7) + { + Func func = GetReturnedFunc(); + return func(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7); + } +} + +[System.Serializable] +public class SerializableFunc : SerializableFuncBase> +{ + public TReturn Invoke(TArg0 arg0, TArg1 arg1, TArg2 arg2, TArg3 arg3, TArg4 arg4, TArg5 arg5, TArg6 arg6, TArg7 arg7, TArg8 arg8) + { + Func func = GetReturnedFunc(); + return func(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8); + } +} + +[System.Serializable] +public class SerializableFunc : SerializableFuncBase> +{ + public TReturn Invoke(TArg0 arg0, TArg1 arg1, TArg2 arg2, TArg3 arg3, TArg4 arg4, TArg5 arg5, TArg6 arg6, TArg7 arg7, TArg8 arg8, TArg9 arg9) + { + Func func = GetReturnedFunc(); + return func(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9); + } +} + +[System.Serializable] +public class SerializableFunc : SerializableFuncBase> +{ + public TReturn Invoke(TArg0 arg0, TArg1 arg1, TArg2 arg2, TArg3 arg3, TArg4 arg4, TArg5 arg5, TArg6 arg6, TArg7 arg7, TArg8 arg8, TArg9 arg9, TArg10 arg10) + { + Func func = GetReturnedFunc(); + return func(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10); + } +} + +[System.Serializable] +public class SerializableFunc : SerializableFuncBase> +{ + public TReturn Invoke(TArg0 arg0, TArg1 arg1, TArg2 arg2, TArg3 arg3, TArg4 arg4, TArg5 arg5, TArg6 arg6, TArg7 arg7, TArg8 arg8, TArg9 arg9, TArg10 arg10, TArg11 arg11) + { + Func func = GetReturnedFunc(); + return func(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11); + } +} + +[System.Serializable] +public class SerializableFunc : + SerializableFuncBase> +{ + public TReturn Invoke(TArg0 arg0, TArg1 arg1, TArg2 arg2, TArg3 arg3, TArg4 arg4, TArg5 arg5, TArg6 arg6, TArg7 arg7, TArg8 arg8, TArg9 arg9, TArg10 arg10, TArg11 arg11, TArg12 arg12) + { + Func func = GetReturnedFunc(); + return func(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12); + } +} + +[System.Serializable] +public class SerializableFunc : + SerializableFuncBase> +{ + public TReturn Invoke(TArg0 arg0, TArg1 arg1, TArg2 arg2, TArg3 arg3, TArg4 arg4, TArg5 arg5, TArg6 arg6, TArg7 arg7, TArg8 arg8, TArg9 arg9, TArg10 arg10, TArg11 arg11, TArg12 arg12, TArg13 arg13) + { + Func func = GetReturnedFunc(); + return func(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13); + } +} + +[System.Serializable] +public class SerializableFunc : + SerializableFuncBase> +{ + public TReturn Invoke(TArg0 arg0, TArg1 arg1, TArg2 arg2, TArg3 arg3, TArg4 arg4, TArg5 arg5, TArg6 arg6, TArg7 arg7, TArg8 arg8, TArg9 arg9, TArg10 arg10, TArg11 arg11, TArg12 arg12, TArg13 arg13, TArg14 arg14) + { + Func func = GetReturnedFunc(); + return func(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14); + } +} + +[System.Serializable] +public class SerializableFunc : + SerializableFuncBase> +{ + public TReturn Invoke(TArg0 arg0, TArg1 arg1, TArg2 arg2, TArg3 arg3, TArg4 arg4, TArg5 arg5, TArg6 arg6, TArg7 arg7, TArg8 arg8, TArg9 arg9, TArg10 arg10, TArg11 arg11, TArg12 arg12, TArg13 arg13, TArg14 arg14, TArg15 arg15) + { + Func func = GetReturnedFunc(); + return func(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15); + } +} \ No newline at end of file diff --git a/Runtime/SerializableFunc/SerializableFunc.cs.meta b/Runtime/SerializableFunc/Implementations/SerializableFunc.cs.meta similarity index 83% rename from Runtime/SerializableFunc/SerializableFunc.cs.meta rename to Runtime/SerializableFunc/Implementations/SerializableFunc.cs.meta index ad700e5..a65f48e 100644 --- a/Runtime/SerializableFunc/SerializableFunc.cs.meta +++ b/Runtime/SerializableFunc/Implementations/SerializableFunc.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 638c2cd796656454f8e97e9ef2cc6ae9 +guid: 37cbc0382ca8c094b869e417b3e986fe MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Runtime/SerializableFunc/SerializableFunc.cs b/Runtime/SerializableFunc/SerializableFunc.cs deleted file mode 100644 index c085c05..0000000 --- a/Runtime/SerializableFunc/SerializableFunc.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; -using UnityEngine; -using Object = UnityEngine.Object; - -namespace Utilities.SerializableData.SerializableFunc -{ - [System.Serializable] - public class SerializableFunc - { - [SerializeField] protected Object targetObject; - [SerializeField] protected string methodName; - - private Func func; - - public Func Func { get { return GetReturnedFunc(); } } - - public TReturn Invoke() - { - Func func = GetReturnedFunc(); - - if (func != null) return func(); - return default; - } - - #region Protected Interface - - protected Func GetReturnedFunc() - { - if (func == null) - { - if (targetObject == null) return null; - if (string.IsNullOrWhiteSpace(methodName)) return null; - - MethodInfo info = targetObject - .GetType() - .GetMethods(BindingFlags.FlattenHierarchy | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance) - .FirstOrDefault(x => IsTargetMethodInfo(x)); - - if (info == null) return null; - - func = (Func)Delegate.CreateDelegate(typeof(Func), targetObject, methodName); - } - - return func; - } - - #endregion - - #region Utility Functions - - private bool IsTargetMethodInfo(MethodInfo x) - { - return string.Equals(x.Name, methodName, StringComparison.InvariantCultureIgnoreCase) - && x.ReturnType == typeof(TReturn) - && x.GetParameters().Length == 0; - } - - #endregion - - #region Operators - - public static implicit operator Func(SerializableFunc serializableFunc) - { - if (serializableFunc == null) return null; - - return serializableFunc.GetReturnedFunc(); - } - - #endregion - } -} \ No newline at end of file