Search Unity

Property Drawers

Discussion in 'UIElements' started by MartinIsla, Dec 9, 2018.

  1. MartinIsla

    MartinIsla

    Joined:
    Sep 18, 2013
    Posts:
    64
    Hello!

    I was wondering if there's a (new? better?) way to implement property drawers using UIElements instead of the good ol' IMGUI.

    Thanks!
     
  2. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    221
    Declaring a custom PropertyDrawer in UIElements is very similar to how it worked with IMGUI. Here's an example (requires 2019.1.0a10):

    Code (CSharp):
    1. [Serializable]
    2. public class UIElementsDrawerType
    3. {
    4.     public enum IngredientUnit { Spoon, Cup, Bowl, Piece }
    5.     public string name;
    6.     public int amount = 1;
    7.     public IngredientUnit unit;
    8. }
    9.  
    10. [CustomPropertyDrawer(typeof(UIElementsDrawerType))]
    11. public class UIElementsCustomDrawer : PropertyDrawer
    12. {
    13.     public override VisualElement CreatePropertyGUI(SerializedProperty property)
    14.     {
    15.         var container = new VisualElement();
    16.         UnityEngine.Random.InitState(property.displayName.GetHashCode());
    17.         container.style.backgroundColor = UnityEngine.Random.ColorHSV();
    18.  
    19.         { // Create drawer using C#
    20.             var popup = new PopupWindow();
    21.             container.Add(popup);
    22.             popup.text = property.displayName + " - Using C#";
    23.             popup.Add(new PropertyField(property.FindPropertyRelative("amount")));
    24.             popup.Add(new PropertyField(property.FindPropertyRelative("unit")));
    25.             popup.Add(new PropertyField(property.FindPropertyRelative("name"), "CustomLabel: Name"));
    26.         }
    27.  
    28.         { // Create drawer using UXML
    29.             var vsTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/Bindings/custom-drawer.uxml");
    30.             var drawer = vsTree.CloneTree(property.propertyPath);
    31.             drawer.Q<PopupWindow>().text = property.displayName + " - Using UXML";
    32.             container.Add(drawer);
    33.         }
    34.  
    35.         return container;
    36.     }
    37. }
     
    MartinIsla and mentorgame1 like this.
  3. mentorgame1

    mentorgame1

    Joined:
    Oct 31, 2016
    Posts:
    19
    thank's i will use that on my property drawers.
     
  4. MartinIsla

    MartinIsla

    Joined:
    Sep 18, 2013
    Posts:
    64
    uDamian I'll just say you made my entire tool
     
  5. MartinIsla

    MartinIsla

    Joined:
    Sep 18, 2013
    Posts:
    64
    Is it possible this is broken in 2019.1.0a12?
    When doing this (overriding the CreatePropertyGUI method), I get the "No GUI implemented" message in the place of the property. Added Debug.Logs inside and they don't show up. Overriding the OnGUI method works!


    EDIT: I read the docs and that makes sense in the inspector, presumably because it uses IMGUI. However, in the EditorWindow, fully made with UIElements, it's like it's not there at all!

    EDIT 2: I think it's PropertyFields that are broken. I can't get them to display anything (even integers or Vectors).
     
    Last edited: Dec 24, 2018
  6. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    221
    There is a bit of a catch here, indeed. You need to make sure the Inspector (Editor) for your object is also using UIElements for any UIElements-based custom PropertyDrawers to show up.

    That is, you need to make sure to override CreateInspectorGUI() on the Editor class for your object and then populate manually with PropertyFields. This is a temporary situation until we switch the default inspectors of all objects to UIElements (right now default inspectors still use IMGUI).

    I double checked our UIElements PropertyFields and they appear to work. If they don't work for you, even in a standalone EditorWindow, definitely submit a bug.
     
  7. johnseghersmsft

    johnseghersmsft

    Joined:
    May 18, 2015
    Posts:
    28
    Any idea when UIElements-based Inspectors are going to be the default? It's a pain to have to manually write an editor for every class in which a custom type--with UIElements-based PropertyDrawers--are used.

    Also, in any such class, if you want to use the default (IMGUI-based) implementation for the non-custom stuff, you need to tag any such properties as [HideInInspector] so you don't get the "No GUI implemented" message.
     
  8. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    221
    I don't have an ETA on when the Inspectors will switch to UIElements by default. We stayed with IMGUI for now to reduce the disruption when we switched the core of the InspectorWindow to UIElements. I'll bump this task a bit in our plans if I can. Thanks for the mention.

    For UIElements custom inspector classes, once you declare it, you'll have to build the entire inspector UI yourself. I'm not sure how you're selecting some properties to be automatically generated without an explicit use of PropertyField.

    Also, [HideInInspector] just hides the property from being display in default inspectors. As soon as you define a custom one, that property doesn't do much.

    Can you maybe add a snippet of one of your custom inspectors showing the problem?
     
  9. johnseghersmsft

    johnseghersmsft

    Joined:
    May 18, 2015
    Posts:
    28
    Here's an Editor for a little test class that does this. "EventToCatch" is a custom event name type (I mentioned that type in another thread). It has a property drawer written for UIElements.

    Code (CSharp):
    1.     [CustomEditor(typeof(DestroyOnEvent))]
    2.     public class DestroyOnEventEditor : Editor
    3.     {
    4.         public override VisualElement CreateInspectorGUI()
    5.         {
    6.             var container = new VisualElement();
    7.  
    8.             // Draw the legacy IMGUI base
    9.             var imgui = new IMGUIContainer(OnInspectorGUI);
    10.             container.Add(imgui);
    11.  
    12.             // Create property fields.
    13.             // Add fields to the container.
    14.             container.Add(
    15.                      new PropertyField(serializedObject.FindProperty("EventToCatch")));
    16.             return container;
    17.  
    18.         }
    19.  
    20.         public override void OnInspectorGUI()
    21.         {
    22.             DrawDefaultInspector();
    23.         }
    24.  
    25.     }
    26.  
    The property in the class is:
    Code (CSharp):
    1.         [HideInInspector]
    2.         public UiEventTag EventToCatch;
    3.  
    In this case, the IMGUI portion is really only displaying the script name, but this works to display all non-UIElements PropertyDrawers in the class.

    Unfortunately, if [HideInInspector] is not use, the default Inspector shows "No GUI Implementation".

    I've got a workaround that I'll post as another message with a base class that enumerates hidden properties that have appropriate drawers.
     
  10. johnseghersmsft

    johnseghersmsft

    Joined:
    May 18, 2015
    Posts:
    28
    As a workaround, since I need a custom editor for any class that uses these new PropertyDrawers, I've created a base class from which a custom Editor can be created. I've also added an Attribute to mark compatible PropertyDrawers. One could theoretically probe the drawer class to see if it overrides CreateInspectorGUI() instead.

    Either of these requires that I be able to find the class type of a PropertyDrawer for a given property type. For that, I use reflection to gain access to ScriptAttributeUtility.GetDrawerTypeForType().

    Code (CSharp):
    1. using System;
    2. using System.Reflection;
    3. using UnityEditor;
    4. using UnityEditor.UIElements;
    5. using UnityEngine.UIElements;
    6. using UnityEngine;
    7.  
    8. namespace PluginUtils
    9. {
    10.     [AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)]
    11.     public class VEPropertyDrawerAttribute : Attribute
    12.     {
    13.  
    14.     }
    15.  
    16.     public class VisualElementPropertyDrawerEditorBase : Editor
    17.     {
    18.         public override VisualElement CreateInspectorGUI()
    19.         {
    20.             var container = new VisualElement();
    21.  
    22.             // Draw the legacy IMGUI base
    23.             var imgui = new IMGUIContainer(OnInspectorGUI);
    24.             container.Add(imgui);
    25.  
    26.             // Find all properties that are marked [HideInInspector] that have
    27.             // a PropertyDrawer tagged with the [VEPropertyDrawer] attribute and create
    28.             // PropertyFields for each of them.
    29.             var type = target.GetType();
    30.             // Create property fields.
    31.             // Add fields to the container.
    32.             CreatePropertyFields(container, type);
    33.             return container;
    34.  
    35.         }
    36.  
    37.         protected void CreatePropertyFields(VisualElement container, Type objectType)
    38.         {
    39.             var fields = objectType.GetFields(
    40.                   BindingFlags.GetField | BindingFlags.Instance | BindingFlags.Public);
    41.             foreach (var fieldInfo in fields)
    42.             {
    43.                 var attr = fieldInfo.GetCustomAttribute<HideInInspector>();
    44.                 if (attr == null || !IsPropertyDrawerTagged(fieldInfo.FieldType))
    45.                     continue;
    46.  
    47.                 container.Add(
    48.                     new PropertyField(serializedObject.FindProperty(fieldInfo.Name)));
    49.             }
    50.         }
    51.  
    52.         protected bool IsPropertyDrawerTagged(Type propertyType)
    53.         {
    54.             var drawerType = GetPropertyDrawerType(propertyType);
    55.             if (drawerType == null)
    56.                 return false;
    57.  
    58.             var attrs = drawerType.GetCustomAttributes(
    59.                                 typeof(VEPropertyDrawerAttribute), true);
    60.             return attrs.Length > 0;
    61.         }
    62.  
    63.  
    64.         /// <summary>
    65.         /// Use Reflection to access ScriptAttributeUtility to find the
    66.         /// PropertyDrawer type for a property type
    67.         /// </summary>
    68.         protected Type GetPropertyDrawerType(Type typeToDraw)
    69.         {
    70.             var scriptAttributeUtilityType = GetScriptAttributeUtilityType();
    71.  
    72.             var getDrawerTypeForTypeMethod =
    73.                         scriptAttributeUtilityType.GetMethod(
    74.                             "GetDrawerTypeForType",
    75.                             BindingFlags.Static | BindingFlags.NonPublic, null,
    76.                             new[] { typeof(Type) }, null);
    77.  
    78.             return (Type) getDrawerTypeForTypeMethod.Invoke(null, new[] { typeToDraw });
    79.         }
    80.  
    81.         protected Type GetScriptAttributeUtilityType()
    82.         {
    83.             var asm = Array.Find(AppDomain.CurrentDomain.GetAssemblies(),
    84.                                               (a) => a.GetName().Name == "UnityEditor");
    85.  
    86.             var types = asm.GetTypes();
    87.             var type = Array.Find(types, (t) => t.Name == "ScriptAttributeUtility");
    88.  
    89.             return type;
    90.         }
    91.         public override void OnInspectorGUI()
    92.         {
    93.             DrawDefaultInspector();
    94.         }
    95.  
    96.     }
    97. }
    98.  
    This allows me to create a custom editor like this:
    Code (CSharp):
    1.     [CustomEditor(typeof(DragDetector))]
    2.     public class DragDetectorEditor : VisualElementPropertyDrawerEditorBase
    3.     {
    4.     }
    And the PropertyDrawer is Attribute-tagged as:
    Code (CSharp):
    1.     [VEPropertyDrawer]
    2.     [CustomPropertyDrawer(typeof(Tag), true)]
    3.     public class TagPropertyDrawer : PropertyDrawer
    4.  
     
  11. johnseghersmsft

    johnseghersmsft

    Joined:
    May 18, 2015
    Posts:
    28
    I realized that the better way to detect a VisualElements-based drawer was indeed to look for an implementation of CreatePropertyGUI() declared on the PropertyDrawer (not inherited). So, I replaced these lines in bool IsPropertyDrawerTagged(Type propertyType):
    Code (CSharp):
    1. var attrs = drawerType.GetCustomAttributes(typeof(VEPropertyDrawerAttribute), true);
    2. return attrs.Length > 0;
    with
    Code (CSharp):
    1. var method = drawerType.GetMethod("CreatePropertyGUI",
    2.                 BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance, null,
    3.                 new[] { typeof(SerializedProperty) }, null);
    4. return method != null;
    5.  
    Works like a charm and I no longer need the tagging attribute.
     
  12. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    221
    I agree this is not ideal. I'll try to bump the priorities a bit on the UIElements default inspector. Thanks for the feedback and in-depth workaround!
     
    OliverGrack likes this.
  13. CyRaid

    CyRaid

    Joined:
    Mar 31, 2015
    Posts:
    80
    John, may I use your code for now until default inspector is UI Elements?
     
  14. johnseghersmsft

    johnseghersmsft

    Joined:
    May 18, 2015
    Posts:
    28
    Feel free to use it!