Skip to content

Instantly share code, notes, and snippets.

@wappenull
Last active March 12, 2024 03:11
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save wappenull/2391b3c23dd20ede74483d0da4cab3f1 to your computer and use it in GitHub Desktop.
Save wappenull/2391b3c23dd20ede74483d0da4cab3f1 to your computer and use it in GitHub Desktop.
Find PropertyDrawer with attribute [CustomPropertyDrawer( typeof( T ) )] in all assemblies
// Revision history
// Rev 1 16/MAY/2021 initial
// Rev 2 23/AUG/2021 add support for array property path
// Rev 3 23/AUG/2021 cache using type+path (s_PathHashVsType)
// Rev 4 23/AUG/2021 properly handling array and list by stealing code from Unity CS reference
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEditor;
namespace dninosores.UnityEditorAttributes
{
/// <summary>
/// Finds custom property drawer for a given type.
/// From https://forum.unity.com/threads/solved-custompropertydrawer-not-being-using-in-editorgui-propertyfield.534968/
/// </summary>
internal static class PropertyDrawerFinder
{
struct TypeAndFieldInfo
{
internal Type type;
internal FieldInfo fi;
}
// Rev 3, be more evil with more cache!
private static readonly Dictionary<int, TypeAndFieldInfo> s_PathHashVsType = new Dictionary<int, TypeAndFieldInfo>();
private static readonly Dictionary<Type, PropertyDrawer> s_TypeVsDrawerCache = new Dictionary<Type, PropertyDrawer>();
/// <summary>
/// Searches for custom property drawer for given property, or returns null if no custom property drawer was found.
/// </summary>
public static PropertyDrawer FindDrawerForProperty( SerializedProperty property )
{
PropertyDrawer drawer;
TypeAndFieldInfo tfi;
int pathHash = _GetUniquePropertyPathHash( property );
if( !s_PathHashVsType.TryGetValue( pathHash, out tfi ) )
{
tfi.type = _GetPropertyType( property, out tfi.fi );
s_PathHashVsType[pathHash] = tfi;
}
if( tfi.type == null )
return null;
if( !s_TypeVsDrawerCache.TryGetValue( tfi.type, out drawer ) )
{
drawer = FindDrawerForType( tfi.type );
s_TypeVsDrawerCache.Add( tfi.type, drawer );
}
if( drawer != null )
{
// Drawer created by custom way like this will not have "fieldInfo" field installed
// It is an optional, but some user code in advanced drawer might use it.
// To install it, we must use reflection again, the backing field name is "internal FieldInfo m_FieldInfo"
// See ref file in UnityCsReference (2019) project. Note that name could changed in future update.
// unitycsreference\Editor\Mono\ScriptAttributeGUI\PropertyDrawer.cs
var fieldInfoBacking = typeof(PropertyDrawer).GetField( "m_FieldInfo", BindingFlags.NonPublic | BindingFlags.Instance );
if( fieldInfoBacking != null )
fieldInfoBacking.SetValue( drawer, tfi.fi );
}
return drawer;
}
/// <summary>
/// Gets type of a serialized property.
/// </summary>
private static Type _GetPropertyType( SerializedProperty property, out FieldInfo fi )
{
// To see real property type, must dig into object that hosts it.
_GetPropertyFieldInfo( property, out Type resolvedType, out fi );
return resolvedType;
}
/// <summary>
/// For caching.
/// </summary>
private static int _GetUniquePropertyPathHash( SerializedProperty property )
{
int hash = property.serializedObject.targetObject.GetType( ).GetHashCode( );
hash += property.propertyPath.GetHashCode( );
return hash;
}
private static void _GetPropertyFieldInfo( SerializedProperty property, out Type resolvedType, out FieldInfo fi )
{
string[] fullPath = property.propertyPath.Split('.');
// fi is FieldInfo in perspective of parentType (property.serializedObject.targetObject)
// NonPublic to support [SerializeField] vars
Type parentType = property.serializedObject.targetObject.GetType();
fi = parentType.GetField( fullPath[0], BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance );
resolvedType = fi.FieldType;
for( int i = 1; i < fullPath.Length; i++ )
{
// To properly handle array and list
// This has deeper rabbit hole, see
// unitycsreference\Editor\Mono\ScriptAttributeGUI\ScriptAttributeUtility.cs GetFieldInfoFromPropertyPath
// here we will simplify it for now (could break)
// If we are at 'Array' section like in `tiles.Array.data[0].tilemodId`
if( _IsArrayPropertyPath( fullPath, i ) )
{
if( fi.FieldType.IsArray )
resolvedType = fi.FieldType.GetElementType( );
else if( _IsListType( fi.FieldType, out Type underlying ) )
resolvedType = underlying;
i++; // skip also the 'data[x]' part
// In this case, fi is not updated, FieldInfo stay the same pointing to 'tiles' part
}
else
{
fi = resolvedType.GetField( fullPath[i], BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance );
resolvedType = fi.FieldType;
}
}
}
static bool _IsArrayPropertyPath( string[] fullPath, int i )
{
// Also search for array pattern, thanks user https://gist.github.com/kkolyan
// like `tiles.Array.data[0].tilemodId`
// This is just a quick check, actual check in Unity uses RegEx
if( fullPath[i] == "Array" && i+1 < fullPath.Length && fullPath[i+1].StartsWith( "data" ) )
return true;
return false;
}
/// <summary>
/// Stolen from unitycsreference\Editor\Mono\ScriptAttributeGUI\ScriptAttributeUtility.cs
/// </summary>
static bool _IsListType( Type t, out Type containedType )
{
if( t.IsGenericType && t.GetGenericTypeDefinition( ) == typeof( List<> ) )
{
containedType = t.GetGenericArguments( )[0];
return true;
}
containedType = null;
return false;
}
/// <summary>
/// Returns custom property drawer for type if one could be found, or null if
/// no custom property drawer could be found. Does not use cached values, so it's resource intensive.
/// </summary>
public static PropertyDrawer FindDrawerForType( Type propertyType )
{
var cpdType = typeof(CustomPropertyDrawer);
FieldInfo typeField = cpdType.GetField("m_Type", BindingFlags.NonPublic | BindingFlags.Instance);
FieldInfo childField = cpdType.GetField("m_UseForChildren", BindingFlags.NonPublic | BindingFlags.Instance);
// Optimization note:
// For benchmark (on DungeonLooter 0.8.4)
// - Original, search all assemblies and classes: 250 msec
// - Wappen optimized, search only specific name assembly and classes: 5 msec
foreach( Assembly assem in AppDomain.CurrentDomain.GetAssemblies( ) )
{
// Wappen optimization: filter only "*Editor" assembly
if( !assem.FullName.Contains( "Editor" ) )
continue;
foreach( Type candidate in assem.GetTypes( ) )
{
// Wappen optimization: filter only "*Drawer" class name, like "SomeTypeDrawer"
if( !candidate.Name.Contains( "Drawer" ) )
continue;
// See if this is a class that has [CustomPropertyDrawer( typeof( T ) )]
foreach( Attribute a in candidate.GetCustomAttributes( typeof( CustomPropertyDrawer ) ) )
{
if( a.GetType( ).IsSubclassOf( typeof( CustomPropertyDrawer ) ) || a.GetType( ) == typeof( CustomPropertyDrawer ) )
{
CustomPropertyDrawer drawerAttribute = (CustomPropertyDrawer)a;
Type drawerType = (Type) typeField.GetValue(drawerAttribute);
if( drawerType == propertyType ||
((bool)childField.GetValue( drawerAttribute ) && propertyType.IsSubclassOf( drawerType )) ||
((bool)childField.GetValue( drawerAttribute ) && IsGenericSubclass( drawerType, propertyType )) )
{
if( candidate.IsSubclassOf( typeof( PropertyDrawer ) ) )
{
// Technical note: PropertyDrawer.fieldInfo will not available via this drawer
// It has to be manually setup by caller.
var drawer = (PropertyDrawer)Activator.CreateInstance( candidate );
return drawer;
}
}
}
}
}
}
return null;
}
/// <summary>
/// Returns true if the parent type is generic and the child type implements it.
/// </summary>
private static bool IsGenericSubclass( Type parent, Type child )
{
if( !parent.IsGenericType )
{
return false;
}
Type currentType = child;
bool isAccessor = false;
while( !isAccessor && currentType != null )
{
if( currentType.IsGenericType && currentType.GetGenericTypeDefinition( ) == parent.GetGenericTypeDefinition( ) )
{
isAccessor = true;
break;
}
currentType = currentType.BaseType;
}
return isAccessor;
}
}
}
@kkolyan
Copy link

kkolyan commented Aug 22, 2021

enhanced GetPropertyFieldInfo to handle arrays correctly:

        public static FieldInfo GetPropertyFieldInfo(SerializedProperty property)
        {
            Type parentType = property.serializedObject.targetObject.GetType();
            string[] fullPath = property.propertyPath.Split('.');
            FieldInfo fi = parentType.GetField(fullPath[0],
                BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); // NonPublic to support [SerializeField] vars
            for (int i = 1; i < fullPath.Length; i++)
            {
                Type fieldType;
                //like `tiles.Array.data[0].tilemodId`
                if (fullPath[i] == "Array")
                {
                    fieldType = fi.FieldType.GetElementType();
                    i += 2;
                }
                else
                {
                    fieldType = fi.FieldType;
                }

                fi = fieldType.GetField(fullPath[i]);
            }

            return fi;
        }

@wappenull
Copy link
Author

enhanced GetPropertyFieldInfo to handle arrays correctly:

@kkolyan Thanks for pointing out. I also include your change into the gist.

@alpalas
Copy link

alpalas commented Jan 4, 2024

I came upon an issue when the property in FindDrawerForProperty method was a [SerializeField] private field

The fix was to traverse the hierarchy in search for private field

    private static void _GetPropertyFieldInfo( SerializedProperty property, out Type resolvedType, out FieldInfo fi )
        {
            string[] fullPath = property.propertyPath.Split('.');

            // fi is FieldInfo in perspective of parentType (property.serializedObject.targetObject)
            // NonPublic to support [SerializeField] vars
            Type parentType = property.serializedObject.targetObject.GetType();

            //traverse the hierarchy in case the field is private [SerializeField]
            FieldInfo foundFieldInfo = null;
            while (foundFieldInfo == null && parentType != null)
            {
                foundFieldInfo = parentType.GetField( fullPath[0], BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance );
                parentType = parentType.BaseType;
            }

            fi = foundFieldInfo;
            resolvedType = fi.FieldType;

            for( int i = 1; i < fullPath.Length; i++ )
            {
                // To properly handle array and list
                // This has deeper rabbit hole, see
                // unitycsreference\Editor\Mono\ScriptAttributeGUI\ScriptAttributeUtility.cs GetFieldInfoFromPropertyPath
                // here we will simplify it for now (could break)

                // If we are at 'Array' section like in `tiles.Array.data[0].tilemodId`
                if( _IsArrayPropertyPath( fullPath, i ) )
                {
                    if( fi.FieldType.IsArray )
                        resolvedType = fi.FieldType.GetElementType( );
                    else if( _IsListType( fi.FieldType, out Type underlying ) )
                        resolvedType = underlying;

                    i++; // skip also the 'data[x]' part
                    // In this case, fi is not updated, FieldInfo stay the same pointing to 'tiles' part
                }
                else
                {
                    fi = resolvedType.GetField( fullPath[i] );
                    resolvedType = fi.FieldType;
                }
            }
        }

@WolvenBard
Copy link

WolvenBard commented Mar 8, 2024

Hey @wappenull,

Just a small suggestion to the snippet introduced by @kkolyan to the GetPropertyFieldInfo method:

The second call to GetField within the for loop should search for non public fields to support private [SerializeField] methods (just as the first call to GetField does at the beginning of that method).

See the change here in my fork of your gist (Github doesn't have PRs for gists, sadly):
https://gist.github.com/WolvenBard/bf004bd2c9131c4fa2048ab9edbcd2f0#file-propertydrawerfinder-cs-L120

@wappenull
Copy link
Author

@WolvenBard I have included your edit. (1 line) TBH I already forgot what my piece of code is for XD. But I still have this piece of code in my main project.

For @alpalas edit above, also thank you for addition, I guess we are, in some sense, in the rabbit hole if we had reached this script (lol)
But I did not include your edit in mine yet, so if anyone need it for their specific case, please do a self service for your own.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment