Search Unity

[Solved] CustomPropertyDrawer not being using in EditorGUI.PropertyField()

Discussion in 'Scripting' started by DARKHAWX, Jun 7, 2018.

  1. DARKHAWX

    DARKHAWX

    Joined:
    Apr 15, 2017
    Posts:
    15
    So I've got a ReorderableList of objects and one of the objects includes a custom class that I've created a custom property drawer for. I use EditorGUI.PropertyField() to get it to draw the custom property field, however this doesn't work. It draws the default inspector version of the object (as a foldout). Another problem is that if I have more than one object in the list all the object foldouts will open and close at the same time.
    Here's the following code for the object that will be displayed. MeshPattern and MeshShapePart will have their own custom property drawers as well. MeshGroupSerObj exists so that I can create a SerializedObject of this and then call FindProperty to get a serialized property of the MeshGroup object.

    Code (CSharp):
    1.     [System.Serializable]
    2.     public class MeshGroup {
    3.         public int id;
    4.         public List<int> children = new List<int>();
    5.         public MeshPattern meshPattern = new MeshPattern();
    6.         public List<MeshShapePart> shapeParts = new List<MeshShapePart> ();
    7.  
    8.         public MeshGroup(int id) {
    9.             this.id = id;
    10.         }
    11.     }
    12.  
    13.     [System.Serializable]
    14.     public class MeshGroupSerObj : ScriptableObject {
    15.         [SerializeField]
    16.         private MeshGroup mGroup;
    17.  
    18.         public void SetGroup(MeshGroup mGroup) {
    19.             this.mGroup = mGroup;
    20.         }
    21.     }
    The following is the drawElementCallback for the ReorderableList. The important line is
    Code (CSharp):
    1. EditorGUI.PropertyField(rect, mGroupSerObj.FindProperty("mGroup"), new GUIContent("Group"), true);
    This should make the inspector draw the custom property drawer but it doesn't.
    Code (CSharp):
    1.     baseGroupList.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) => {
    2.         float height = 0f;
    3.         SerializedProperty prop = baseGroupList.serializedProperty.GetArrayElementAtIndex(index);
    4.         MeshGroupSerObj mGroup = manager.groupList.MakeMeshGroupSerObj(prop.intValue);
    5.         SerializedObject mGroupSerObj = new SerializedObject(mGroup);
    6.         rect.y += 2;
    7.         EditorGUI.PropertyField(rect, mGroupSerObj.FindProperty("mGroup"), new GUIContent("Group"), true);
    8.         height = EditorGUI.GetPropertyHeight(mGroupSerObj.FindProperty("mGroup"), true);
    9.         try {
    10.             heights[index] = height;
    11.         } catch (ArgumentOutOfRangeException e) {
    12.             Debug.LogWarning(e.Message);
    13.         } finally {
    14.             float[] floats = heights.ToArray();
    15.             Array.Resize (ref floats, serObj.FindProperty("groupList").FindPropertyRelative("baseGroups").arraySize);
    16.             heights = floats.ToList();
    17.         }
    18.     };
    Lastly here is the custom property drawer. It should just display a label field with "Test" in it, but like I said previously, it displays the default inspector and so I see all the other fields.
    Code (CSharp):
    1.     [CustomPropertyDrawer(typeof(MeshGroup), true)]
    2.     public class MeshGroupDrawer : PropertyDrawer {
    3.    
    4.         public override void OnGUI (Rect initPos, SerializedProperty prop, GUIContent label)
    5.         {
    6.             int oldIndent = EditorGUI.indentLevel;
    7.             float totalWidth = EditorGUIUtility.currentViewWidth - (oldIndent + 1) * 12 - initPos.x + 4;
    8.    
    9.             initPos.height = 16f;
    10.             EditorGUI.LabelField (initPos, "Test");
    11.         }
    12.     }
    Does anyone know what the problem is? I can't seem to work it out.

    TooLongDidn'tRead Using EditorGUI.PropertyField() doesn't draw associated CustomPropertyDrawer. I would love to know why.
     
  2. DARKHAWX

    DARKHAWX

    Joined:
    Apr 15, 2017
    Posts:
    15
    Solved!

    Turns out your objects need to be in their own file with matching file name and class name. I'm not sure if this is required for normal classes, but Scriptable Objects do require this.
     
  3. Deleted User

    Deleted User

    Guest

    Fixing the class names didn't work for me, so I wrote this script as a workaround. It finds the custom property drawer for a SerializedProperty manually, so if EditorGUI.PropertyField() doesn't work you can get the property drawer for the SerializedProperty and call its OnGUI() and GetPropertyHeight() methods yourself. Instead of calling EditorGUI.PropertyField(), you would call PropertyDrawerFinder.Find(<SerializedProperty>).OnGUI() ).

    This method only searches for custom property drawers though, so make sure you call EditorGUI.PropertyField as a fallback if PropertyDrawerFinder.Find() returns null.

    Code (CSharp):
    1.  
    2. using System;
    3. using System.Collections.Generic;
    4. using System.Reflection;
    5. using UnityEditor;
    6.  
    7. namespace dninosores.UnityEditorAttributes
    8. {
    9.     /// <summary>
    10.     /// Finds custom property drawer for a given type.
    11.     /// </summary>
    12.     public static class PropertyDrawerFinder
    13.     {
    14.         private static Dictionary<Type, PropertyDrawer> customDrawers = new Dictionary<Type, PropertyDrawer>();
    15.  
    16.         /// <summary>
    17.         /// Searches for custom property drawer for given property, or returns null if no custom property drawer was found.
    18.         /// </summary>
    19.         public static PropertyDrawer Find(SerializedProperty property)
    20.         {
    21.             Type pType = PropertyDrawerFinder.GetPropertyType(property);
    22.             if (!customDrawers.ContainsKey(pType))
    23.             {
    24.                 customDrawers.Add(pType, PropertyDrawerFinder.Find(pType));
    25.             }
    26.  
    27.  
    28.             return customDrawers[pType];
    29.         }
    30.  
    31.  
    32.         /// <summary>
    33.         /// Gets type of a serialized property.
    34.         /// </summary>
    35.         public static Type GetPropertyType(SerializedProperty property)
    36.         {
    37.             Type parentType = property.serializedObject.targetObject.GetType();
    38.             string[] fullPath = property.propertyPath.Split('.');
    39.             FieldInfo fi = parentType.GetField(fullPath[0]);
    40.             for (int i = 1; i < fullPath.Length; i++)
    41.             {
    42.                 fi = fi.FieldType.GetField(fullPath[i]);
    43.             }
    44.             return fi.FieldType;
    45.         }
    46.  
    47.  
    48.         /// <summary>
    49.         /// Returns custom property drawer for type if one could be found, or null if
    50.         /// no custom property drawer could be found. Does not use cached values, so it's resource intensive.
    51.         /// </summary>
    52.         public static PropertyDrawer Find(Type propertyType)
    53.         {
    54.             foreach (Assembly assem in AppDomain.CurrentDomain.GetAssemblies())
    55.             {
    56.                 foreach (Type candidate in assem.GetTypes())
    57.                 {
    58.                     FieldInfo typeField = typeof(CustomPropertyDrawer).GetField("m_Type", BindingFlags.NonPublic | BindingFlags.Instance);
    59.                     FieldInfo childField = typeof(CustomPropertyDrawer).GetField("m_UseForChildren", BindingFlags.NonPublic | BindingFlags.Instance);
    60.                     foreach (Attribute a in candidate.GetCustomAttributes(typeof(CustomPropertyDrawer)))
    61.                     {
    62.                         if (a.GetType().IsSubclassOf(typeof(CustomPropertyDrawer)) || a.GetType() == typeof(CustomPropertyDrawer))
    63.                         {
    64.                             CustomPropertyDrawer drawerAttribute = (CustomPropertyDrawer)a;
    65.                             Type drawerType =  (Type) typeField.GetValue(drawerAttribute);
    66.                             if (drawerType == propertyType ||
    67.                                 ((bool)childField.GetValue(drawerAttribute) && propertyType.IsSubclassOf(drawerType)) ||
    68.                                 ((bool)childField.GetValue(drawerAttribute) && IsGenericSubclass(drawerType, propertyType)))
    69.                             {
    70.                                 if (candidate.IsSubclassOf(typeof(PropertyDrawer))){
    71.                                     return (PropertyDrawer)Activator.CreateInstance(candidate);
    72.                                 }
    73.                             }
    74.                            
    75.                         }
    76.                     }
    77.  
    78.  
    79.                 }
    80.             }
    81.  
    82.  
    83.  
    84.             return null;
    85.         }
    86.  
    87.         /// <summary>
    88.         /// Returns true if the parent type is generic and the child type implements it.
    89.         /// </summary>
    90.         private static bool IsGenericSubclass(Type parent, Type child)
    91.         {
    92.             if (!parent.IsGenericType)
    93.             {
    94.                 return false;
    95.             }
    96.  
    97.             Type currentType = child;
    98.             bool isAccessor = false;
    99.             while (!isAccessor && currentType != null)
    100.             {
    101.                 if (currentType.IsGenericType && currentType.GetGenericTypeDefinition() == parent.GetGenericTypeDefinition())
    102.                 {
    103.                     isAccessor = true;
    104.                     break;
    105.                 }
    106.                 currentType = currentType.BaseType;
    107.             }
    108.             return isAccessor;
    109.         }
    110.  
    111.     }
    112. }
    113.  
     
    Wappenull and avrahamyor like this.
  4. Wappenull

    Wappenull

    Joined:
    Oct 29, 2013
    Posts:
    51
    Code from "Deleted User" above actually does thing I wanted. So I will explain more here.

    What's the problem?
    This problem is a little bit different and deeper than OP's.

    Let's say for the:
    public MyStruct data;

    If you happen to be in another OnGUI of some CustomPropertyDrawer. Even while you have
    [CustomPropertyDrawer( typeof( MyStruct ) )]
    class MyStructPropertyDrawer : PropertyDrawer


    Calling the
    EditorGUI.PropertyField
    will not use MyStructPropertyDrawer because Unity safeguard the recursion. Unity will only pick one PropertyDrawer for type T. If there is multiple PropertyDrawer(s) that match that candidate. It will not picked again.

    What do I want?
    While on another CustomPropertyDrawer. I want to trigger MyStructPropertyDrawer OnGUI. Calling EditorGUI.PropertyField will only call the default one. (Due to aforementioned safeguard)

    What does that code do?
    It search for all classes in all assemblies the class that has attribute
    [CustomPropertyDrawer( typeof( MyStruct ) )]
    , cache it, and return for you to call OnGUI.

    Some modification I made
    I have made some optimization/modification to his code.
    The new restriction to speed up the search is that it only search for assemblies containing name "Editor" and Classes name with "Drawer" in it. This result in 100x times faster.

    "Deleted User" I don't know who you are, but you are awesome!
     
    Xriuk likes this.
  5. Xriuk

    Xriuk

    Joined:
    Aug 10, 2018
    Posts:
    21
    Wonderful code, but I had to remove the part where you exclude non-Editor assemblies from the search because some Drawers in my project are not under the Editor assembly so they weren't found.

    Inside
    public static PropertyDrawer FindDrawerForType( Type propertyType )
    I had to remove
    Code (CSharp):
    1. // Wappen optimization: filter only "*Editor" assembly
    2. if( !assem.FullName.Contains( "Editor" ) )
    3.    continue;