Initial Commit
This commit is contained in:
8
Editor.meta
Normal file
8
Editor.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 83554dd98535b594a8eb0598859038de
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Editor/Extensions.meta
Normal file
8
Editor/Extensions.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bf6137b31e1ad2747b16d1fd7eb292a5
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Editor/Extensions/Dropdown Field.meta
Normal file
8
Editor/Extensions/Dropdown Field.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 49fcc97bf68f1be43af4476942bfc60e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
130
Editor/Extensions/Dropdown Field/DropdownFieldExtensions.cs
Normal file
130
Editor/Extensions/Dropdown Field/DropdownFieldExtensions.cs
Normal file
@@ -0,0 +1,130 @@
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using UnityEditor;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace Utilities.Extensions.UIToolkit
|
||||
{
|
||||
public static class DropdownFieldExtensions
|
||||
{
|
||||
private static Type genericOSMenuType;
|
||||
private static Type iGenericMenuInterfaceType;
|
||||
|
||||
private const string CreateMenuCallback_Property_Name = "createMenuCallback";
|
||||
private const string GenericOsMenu_Type_Name = "UnityEditor.UIElements.GenericOSMenu";
|
||||
private const string IGenericMenu_Type_Name = "IGenericMenu";
|
||||
|
||||
private const string FormatSelectedValueCallback_Property_Name = "formatSelectedValueCallback";
|
||||
|
||||
#region Generic Menu Assigning
|
||||
|
||||
public static void AssignGenericMenu(this DropdownField dropdownField,
|
||||
Func<GenericMenu> genericMenuBuildingFunc)
|
||||
{
|
||||
Func<object> genericOSMenuFunc = () =>
|
||||
{
|
||||
GenericMenu menu = genericMenuBuildingFunc();
|
||||
return GetGenericOSMenu(menu);
|
||||
};
|
||||
|
||||
object boxedfunc = ConvertFuncToDesiredType(genericOSMenuFunc);
|
||||
|
||||
FieldInfo createMenuCallbackFieldInfo = typeof(DropdownField)
|
||||
.GetField(CreateMenuCallback_Property_Name, BindingFlags.FlattenHierarchy | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
|
||||
|
||||
createMenuCallbackFieldInfo.SetValue(dropdownField, boxedfunc);
|
||||
}
|
||||
|
||||
#region Generic OS Menu Creation
|
||||
|
||||
private static object GetGenericOSMenu(GenericMenu menu)
|
||||
{
|
||||
Type type = GetGenericOSMenuType();
|
||||
object[] args = new object[] { menu };
|
||||
return Activator.CreateInstance(type, args);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Type helpers
|
||||
|
||||
private static Type GetGenericOSMenuType()
|
||||
{
|
||||
if (genericOSMenuType == null)
|
||||
{
|
||||
genericOSMenuType = typeof(UnityEditor.UIElements.ColorField).Assembly.GetType(GenericOsMenu_Type_Name, false, true);
|
||||
}
|
||||
|
||||
return genericOSMenuType;
|
||||
}
|
||||
|
||||
private static Type GetIGenericMenuInterfaceType()
|
||||
{
|
||||
if (iGenericMenuInterfaceType == null)
|
||||
{
|
||||
Type genericOSType = GetGenericOSMenuType();
|
||||
iGenericMenuInterfaceType = genericOSType.GetInterface(IGenericMenu_Type_Name);
|
||||
}
|
||||
|
||||
return iGenericMenuInterfaceType;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Func Conversion
|
||||
|
||||
private static Delegate ConvertFuncToDesiredType(Func<object> func)
|
||||
{
|
||||
Type interfaceType = GetIGenericMenuInterfaceType();
|
||||
Type resultType = typeof(Func<>).MakeGenericType(interfaceType);
|
||||
|
||||
Expression<Func<object>> expressionFunc = FuncToExpression(func);
|
||||
|
||||
InvocationExpression invokedExpression = Expression.Invoke(expressionFunc);
|
||||
UnaryExpression convertedReturnValue = Expression.Convert(invokedExpression, interfaceType);
|
||||
|
||||
LambdaExpression lambda = Expression.Lambda(delegateType: resultType, body: convertedReturnValue);
|
||||
|
||||
return lambda.Compile();
|
||||
}
|
||||
|
||||
private static Expression<Func<T>> FuncToExpression<T>(Func<T> f)
|
||||
{
|
||||
return () => f();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
|
||||
#region Formatting Callback Assigning
|
||||
|
||||
public static void AssignFormattingCallback(this DropdownField dropdownField,
|
||||
Func<string, string> formattingCallback)
|
||||
{
|
||||
#if UNITY_2022_2_OR_NEWER
|
||||
dropdownField.formatSelectedValueCallback = formattingCallback;
|
||||
#else
|
||||
AssignFormattingCallbackViaReflection(dropdownField, formattingCallback);
|
||||
#endif
|
||||
}
|
||||
|
||||
private static void AssignFormattingCallbackViaReflection(this DropdownField functionDropdown,
|
||||
Func<string, string> formattingCallback)
|
||||
{
|
||||
PropertyInfo propertyInfo = typeof(DropdownField)
|
||||
.GetProperty(FormatSelectedValueCallback_Property_Name, BindingFlags.FlattenHierarchy | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
|
||||
|
||||
object[] args = new object[] { formattingCallback };
|
||||
|
||||
propertyInfo.SetMethod.Invoke(functionDropdown, args);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f923456164a309c4d81abab46ab3f8ca
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Editor/Extensions/SerializedProperty.meta
Normal file
8
Editor/Extensions/SerializedProperty.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4950e11fcaa85be40ba5df654ee71efb
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,147 @@
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System.Collections;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using UnityEditor;
|
||||
|
||||
namespace Utilities.SerializableData.SerializableFunc.UnityEditorUtilities
|
||||
{
|
||||
public static class SerializedPropertyExtensions
|
||||
{
|
||||
static readonly Regex rgx = new Regex(@"\[\d+\]", RegexOptions.Compiled);
|
||||
|
||||
public static object GetBoxedValue(this SerializedProperty property)
|
||||
{
|
||||
property.serializedObject.ApplyModifiedProperties();
|
||||
#if UNITY_2023_2_OR_NEWER
|
||||
return property.boxedValue;
|
||||
#else
|
||||
return GetValue<object>(property);
|
||||
#endif
|
||||
}
|
||||
|
||||
public static void SetBoxedValue(this SerializedProperty property, object value)
|
||||
{
|
||||
property.serializedObject.Update();
|
||||
#if UNITY_2023_2_OR_NEWER
|
||||
property.boxedValue = value;
|
||||
#else
|
||||
SetValue<object>(property, value);
|
||||
#endif
|
||||
property.serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
public static T GetValue<T>(this SerializedProperty property) where T : class
|
||||
{
|
||||
object obj = property.serializedObject.targetObject;
|
||||
string path = property.propertyPath.Replace(".Array.data", string.Empty);
|
||||
string[] fieldStructure = path.Split('.');
|
||||
for (int i = 0; i < fieldStructure.Length; i++)
|
||||
{
|
||||
if (fieldStructure[i].Contains("["))
|
||||
{
|
||||
int index = System.Convert.ToInt32(new string(fieldStructure[i].Where(c => char.IsDigit(c)).ToArray()));
|
||||
obj = GetFieldValueWithIndex(rgx.Replace(fieldStructure[i], string.Empty), obj, index);
|
||||
}
|
||||
else
|
||||
{
|
||||
obj = GetFieldValue(fieldStructure[i], obj);
|
||||
}
|
||||
}
|
||||
return (T)obj;
|
||||
}
|
||||
|
||||
public static bool SetValue<T>(this SerializedProperty property, T value) where T : class
|
||||
{
|
||||
object obj = property.serializedObject.targetObject;
|
||||
string path = property.propertyPath.Replace(".Array.data", "");
|
||||
string[] fieldStructure = path.Split('.');
|
||||
for (int i = 0; i < fieldStructure.Length - 1; i++)
|
||||
{
|
||||
if (fieldStructure[i].Contains("["))
|
||||
{
|
||||
int index = System.Convert.ToInt32(new string(fieldStructure[i].Where(c => char.IsDigit(c)).ToArray()));
|
||||
obj = GetFieldValueWithIndex(rgx.Replace(fieldStructure[i], ""), obj, index);
|
||||
}
|
||||
else
|
||||
{
|
||||
obj = GetFieldValue(fieldStructure[i], obj);
|
||||
}
|
||||
}
|
||||
|
||||
string fieldName = fieldStructure.Last();
|
||||
if (fieldName.Contains("["))
|
||||
{
|
||||
int index = System.Convert.ToInt32(new string(fieldName.Where(c => char.IsDigit(c)).ToArray()));
|
||||
return SetFieldValueWithIndex(rgx.Replace(fieldName, ""), obj, index, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
return SetFieldValue(fieldName, obj, value);
|
||||
}
|
||||
}
|
||||
|
||||
private static object GetFieldValue(string fieldName, object obj, BindingFlags bindings = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
|
||||
{
|
||||
FieldInfo field = obj.GetType().GetField(fieldName, bindings);
|
||||
if (field != null)
|
||||
{
|
||||
return field.GetValue(obj);
|
||||
}
|
||||
return default(object);
|
||||
}
|
||||
|
||||
private static object GetFieldValueWithIndex(string fieldName, object obj, int index, BindingFlags bindings = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
|
||||
{
|
||||
FieldInfo field = obj.GetType().GetField(fieldName, bindings);
|
||||
if (field != null)
|
||||
{
|
||||
object list = field.GetValue(obj);
|
||||
if (list.GetType().IsArray)
|
||||
{
|
||||
return ((object[])list)[index];
|
||||
}
|
||||
else if (list is IEnumerable)
|
||||
{
|
||||
return ((IList)list)[index];
|
||||
}
|
||||
}
|
||||
return default(object);
|
||||
}
|
||||
|
||||
public static bool SetFieldValue(string fieldName, object obj, object value, bool includeAllBases = false, BindingFlags bindings = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
|
||||
{
|
||||
FieldInfo field = obj.GetType().GetField(fieldName, bindings);
|
||||
if (field != null)
|
||||
{
|
||||
field.SetValue(obj, value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool SetFieldValueWithIndex(string fieldName, object obj, int index, object value, bool includeAllBases = false, BindingFlags bindings = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
|
||||
{
|
||||
FieldInfo field = obj.GetType().GetField(fieldName, bindings);
|
||||
if (field != null)
|
||||
{
|
||||
object list = field.GetValue(obj);
|
||||
if (list.GetType().IsArray)
|
||||
{
|
||||
((object[])list)[index] = value;
|
||||
return true;
|
||||
}
|
||||
else if (list is IEnumerable)
|
||||
{
|
||||
((IList)list)[index] = value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9092eebd24b0d8d4985c409afe2768d4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Editor/Extensions/Type Extensions.meta
Normal file
8
Editor/Extensions/Type Extensions.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8bb44bb4f5e72344b87845ac17958288
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
60
Editor/Extensions/Type Extensions/TypeExtensions.cs
Normal file
60
Editor/Extensions/Type Extensions/TypeExtensions.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Utilities.Extensions.SystemExtensions
|
||||
{
|
||||
public static class TypeExtensions
|
||||
{
|
||||
private static Dictionary<Type, string> PrimitiveTypesLookup = new Dictionary<Type, string>
|
||||
{
|
||||
{ typeof(bool), "Bool" },
|
||||
{ typeof(byte), "Byte" },
|
||||
{ typeof(char), "Char" },
|
||||
{ typeof(decimal), "Decimal" },
|
||||
{ typeof(double), "Double" },
|
||||
{ typeof(float), "Float" },
|
||||
{ typeof(int), "Int" },
|
||||
{ typeof(long), "Long" },
|
||||
{ typeof(sbyte), "Sbyte" },
|
||||
{ typeof(short), "Short" },
|
||||
{ typeof(string), "String" },
|
||||
{ typeof(uint), "Uint" },
|
||||
{ typeof(ulong), "Ulong" },
|
||||
{ typeof(ushort), "Ushort" },
|
||||
};
|
||||
|
||||
public static string NicifyTypeName(this Type type)
|
||||
{
|
||||
if (type.IsArray)
|
||||
{
|
||||
Type elementType = type.GetElementType();
|
||||
string elementTypeString = NicifyTypeName(elementType);
|
||||
return $"{elementTypeString}[]";
|
||||
}
|
||||
|
||||
if (!type.IsGenericType)
|
||||
{
|
||||
return GetSingleTypeName(type);
|
||||
}
|
||||
|
||||
if (type.IsNested && type.DeclaringType.IsGenericType) return type.Name;
|
||||
|
||||
string genericTypeString = $"{type.Name[..type.Name.IndexOf('`')]}";
|
||||
|
||||
IEnumerable<string> genericArgumentNames = type.GetGenericArguments()
|
||||
.Select(x => NicifyTypeName(x));
|
||||
|
||||
string genericArgumentsString = string.Join(", ", genericArgumentNames);
|
||||
|
||||
return $"{genericTypeString}<{genericArgumentsString}>";
|
||||
}
|
||||
|
||||
private static string GetSingleTypeName(Type type)
|
||||
{
|
||||
if (PrimitiveTypesLookup.TryGetValue(type, out string result)) return result;
|
||||
|
||||
return type.Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Editor/Extensions/Type Extensions/TypeExtensions.cs.meta
Normal file
11
Editor/Extensions/Type Extensions/TypeExtensions.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 46fcb395397539e4f99e257852dcaaa7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Editor/Property Drawer.meta
Normal file
8
Editor/Property Drawer.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a98267ecc93a89f4c9b31bad1ccb0bb0
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
682
Editor/Property Drawer/SerializableFuncPropertyDrawer.cs
Normal file
682
Editor/Property Drawer/SerializableFuncPropertyDrawer.cs
Normal file
@@ -0,0 +1,682 @@
|
||||
#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
|
||||
|
||||
#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>(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 $"<Missing {arg}.{methodProperty.stringValue}>";
|
||||
}
|
||||
|
||||
#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<Component>();
|
||||
|
||||
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<MethodInfo> 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<PropertyInfo> suitableProperties = componentType.GetProperties(BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(x => IsSuitableProperty(x, returnType));
|
||||
|
||||
IEnumerable<MethodInfo> 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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 33baceefadfdeb945aa7f92e190518f0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
18
Editor/SerializableFunc.Editor.asmdef
Normal file
18
Editor/SerializableFunc.Editor.asmdef
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "SerializableFunc.Editor",
|
||||
"rootNamespace": "",
|
||||
"references": [
|
||||
"GUID:e72a8e3728c787f4fafc146b590e6be5"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
7
Editor/SerializableFunc.Editor.asmdef.meta
Normal file
7
Editor/SerializableFunc.Editor.asmdef.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 75a87e1cf9f3f7a459b64a70045728b6
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
21
LICENSE.md
Normal file
21
LICENSE.md
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Anton Zhernosek
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
7
LICENSE.md.meta
Normal file
7
LICENSE.md.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8308ca570cebab84d9cf8419d00c1c08
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
29
README.md
Normal file
29
README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
### Serializable Func
|
||||
A UnityEvent for function calls with a return value.
|
||||
Allows you to assign Func<T> 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.
|
||||
|
||||
GUI Representation
|
||||

|
||||
|
||||
UI Toolkit Representation
|
||||

|
||||
|
||||
### Example Usage
|
||||
```csharp
|
||||
public class ExampleClass : MonoBehaviour
|
||||
{
|
||||
[Header("My Bool Func")]
|
||||
[SerializeField] private SerializableFunc<bool> boolFunc;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
bool result = boolFunc.Invoke();
|
||||
Debug.Log(result);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
7
README.md.meta
Normal file
7
README.md.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: af10af29744df484b8201ea05b67fb1a
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Runtime.meta
Normal file
8
Runtime.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2c15dee222c1fc946830340e04e998df
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
14
Runtime/SerializableFunc.Runtime.asmdef
Normal file
14
Runtime/SerializableFunc.Runtime.asmdef
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "SerializableFunc.Runtime",
|
||||
"rootNamespace": "",
|
||||
"references": [],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
7
Runtime/SerializableFunc.Runtime.asmdef.meta
Normal file
7
Runtime/SerializableFunc.Runtime.asmdef.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e72a8e3728c787f4fafc146b590e6be5
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Runtime/SerializableFunc.meta
Normal file
8
Runtime/SerializableFunc.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fe4625ec820a55848834d700960d1a9f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
73
Runtime/SerializableFunc/SerializableFunc.cs
Normal file
73
Runtime/SerializableFunc/SerializableFunc.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace Utilities.SerializableData.SerializableFunc
|
||||
{
|
||||
[System.Serializable]
|
||||
public class SerializableFunc<TReturn>
|
||||
{
|
||||
[SerializeField] protected Object targetObject;
|
||||
[SerializeField] protected string methodName;
|
||||
|
||||
private Func<TReturn> func;
|
||||
|
||||
public Func<TReturn> Func { get { return GetReturnedFunc(); } }
|
||||
|
||||
public TReturn Invoke()
|
||||
{
|
||||
Func<TReturn> func = GetReturnedFunc();
|
||||
|
||||
if (func != null) return func();
|
||||
return default;
|
||||
}
|
||||
|
||||
#region Protected Interface
|
||||
|
||||
protected Func<TReturn> 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<TReturn>)Delegate.CreateDelegate(typeof(Func<TReturn>), 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<TReturn>(SerializableFunc<TReturn> serializableFunc)
|
||||
{
|
||||
if (serializableFunc == null) return null;
|
||||
|
||||
return serializableFunc.GetReturnedFunc();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
11
Runtime/SerializableFunc/SerializableFunc.cs.meta
Normal file
11
Runtime/SerializableFunc/SerializableFunc.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 638c2cd796656454f8e97e9ef2cc6ae9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9fc0d4010bbf28b4594072e72b8655ab
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 69ff54f7aedeaa94b859181718444cfd
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,175 @@
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using Utilities.SerializableData.SerializableFunc;
|
||||
|
||||
public class SerializableFuncDebugger : MonoBehaviour
|
||||
{
|
||||
[Header("UI References")]
|
||||
[SerializeField] private TextMeshProUGUI displayedText;
|
||||
|
||||
[Header("Debug Controls")]
|
||||
[SerializeField] private KeyCode boolFuncInvocationKeyCode = KeyCode.Q;
|
||||
[SerializeField] private KeyCode stringFuncInvocationKeyCode = KeyCode.W;
|
||||
[SerializeField] private KeyCode vector3FuncFuncInvocationKeyCode = KeyCode.E;
|
||||
[SerializeField] private KeyCode quaternionFuncFuncInvocationKeyCode = KeyCode.R;
|
||||
[SerializeField] private KeyCode floatFuncInvocationKeyCode = KeyCode.T;
|
||||
[SerializeField] private KeyCode listFuncInvocationKeyCode = KeyCode.Y;
|
||||
|
||||
[Header("Test Funcs")]
|
||||
[SerializeField] private SerializableFunc<bool> boolFunc;
|
||||
[SerializeField] private SerializableFunc<string> stringFunc;
|
||||
[SerializeField] private SerializableFunc<Vector3> vector3Func;
|
||||
[SerializeField] private SerializableFunc<Quaternion> quaternionFunc;
|
||||
[SerializeField] private SerializableFunc<float> floatFunc;
|
||||
[SerializeField] private SerializableFunc<List<string>> listFunc;
|
||||
[SerializeField] private SerializableFunc<float[]> arrayFunc;
|
||||
[SerializeField] private SerializableFunc<float[][]> array2DFunc;
|
||||
[SerializeField] private SerializableFunc<InternalClass> internalClassFunc;
|
||||
[SerializeField] private SerializableFunc<GenericInternalClass<bool>> genericInternalClassFunc;
|
||||
[SerializeField] private SerializableFunc<GenericInternalClass<Dictionary<string, byte>>> genericInternalClassFunc2;
|
||||
|
||||
[Header("Test Objects")]
|
||||
[SerializeField] private List<string> stringsList;
|
||||
|
||||
private void Update()
|
||||
{
|
||||
TryInvokeBoolFunc();
|
||||
TryInvokeStringFunc();
|
||||
TryInvokeVector3Func();
|
||||
TryInvokeQuaternionFunc();
|
||||
TryInvokeFloatFunc();
|
||||
TryInvokeListFunc();
|
||||
}
|
||||
|
||||
#region Public Calls
|
||||
|
||||
public void CallBoolFunc()
|
||||
{
|
||||
bool result = boolFunc.Invoke();
|
||||
DisplayCalledFuncResult(result);
|
||||
}
|
||||
|
||||
public void CallStringFunc()
|
||||
{
|
||||
string result = stringFunc.Invoke();
|
||||
DisplayCalledFuncResult(result);
|
||||
}
|
||||
|
||||
public void CallVector3Func()
|
||||
{
|
||||
Vector3 result = vector3Func.Invoke();
|
||||
DisplayCalledFuncResult(result);
|
||||
}
|
||||
|
||||
public void CallQuaternionFunc()
|
||||
{
|
||||
Quaternion result = quaternionFunc.Invoke();
|
||||
DisplayCalledFuncResult(result);
|
||||
}
|
||||
|
||||
public void CallFloatFunc()
|
||||
{
|
||||
float result = floatFunc.Invoke();
|
||||
DisplayCalledFuncResult(result);
|
||||
}
|
||||
|
||||
public void CallListFunc()
|
||||
{
|
||||
List<string> result = listFunc.Invoke();
|
||||
PrintListDataToConsole(result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Data Returns
|
||||
|
||||
public List<string> GetSampleList()
|
||||
{
|
||||
return stringsList;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Calls
|
||||
|
||||
private void TryInvokeBoolFunc()
|
||||
{
|
||||
if (!Input.GetKeyDown(boolFuncInvocationKeyCode)) return;
|
||||
CallBoolFunc();
|
||||
}
|
||||
|
||||
private void TryInvokeStringFunc()
|
||||
{
|
||||
if (!Input.GetKeyDown(stringFuncInvocationKeyCode)) return;
|
||||
CallStringFunc();
|
||||
}
|
||||
|
||||
private void TryInvokeVector3Func()
|
||||
{
|
||||
if (!Input.GetKeyDown(vector3FuncFuncInvocationKeyCode)) return;
|
||||
CallVector3Func();
|
||||
}
|
||||
|
||||
private void TryInvokeQuaternionFunc()
|
||||
{
|
||||
if (!Input.GetKeyDown(quaternionFuncFuncInvocationKeyCode)) return;
|
||||
CallQuaternionFunc();
|
||||
}
|
||||
|
||||
private void TryInvokeFloatFunc()
|
||||
{
|
||||
if (!Input.GetKeyDown(floatFuncInvocationKeyCode)) return;
|
||||
CallFloatFunc();
|
||||
}
|
||||
|
||||
private void TryInvokeListFunc()
|
||||
{
|
||||
if (!Input.GetKeyDown(listFuncInvocationKeyCode)) return;
|
||||
CallListFunc();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void DisplayCalledFuncResult(object result)
|
||||
{
|
||||
string resultString = $"Res: {result}";
|
||||
|
||||
if (displayedText != null)
|
||||
{
|
||||
displayedText.text = resultString;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log(resultString);
|
||||
}
|
||||
}
|
||||
|
||||
private void PrintListDataToConsole(List<string> list)
|
||||
{
|
||||
if (list == null)
|
||||
{
|
||||
Debug.Log("The list is null");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"The list has {list.Count} elements");
|
||||
foreach (string item in list)
|
||||
{
|
||||
Debug.Log(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class InternalClass
|
||||
{
|
||||
}
|
||||
|
||||
private class GenericInternalClass<T>
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f6e31a116de69b146878917a8db62209
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
24
package.json
Normal file
24
package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "com.antonzhernosek.serializablefunc",
|
||||
"displayName": "SerializableFunc",
|
||||
"version": "1.0.0",
|
||||
"unity": "2020.3",
|
||||
"keywords": [
|
||||
"unity",
|
||||
"editor",
|
||||
"serializable",
|
||||
"func"
|
||||
],
|
||||
"description": "Unity Event and Func<T> had another baby. This one looks a bit nicer and doesn't like arguments",
|
||||
"author": {
|
||||
"name": "Anton Zhernosek",
|
||||
"email": "AntonZhernosekWork@gmail.com"
|
||||
},
|
||||
"samples": [
|
||||
{
|
||||
"displayName": "Serializable Func Sample Scene",
|
||||
"description": "A simple test scene with a simple debugger to showcase how the script works",
|
||||
"path": "Samples~/SerializableFuncSampleScene"
|
||||
}
|
||||
]
|
||||
}
|
||||
7
package.json.meta
Normal file
7
package.json.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5d803fc359c587b409b27fc9cfff1648
|
||||
PackageManifestImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user