How does one access the Label argument of a PropertyField within a PropretyDrawer using UIToolkit/UIElements?

Title. I want to be able to create my own label for a PropertyField, but when using a custom PropertyDrawer the “label” argument of the PropertyField constructor seems to be entirely ignored. Further I can’t seem to find any “label” or “text” fields or the like.

Hey @craftsmanbeck,

It does appear that the label argument to the PropertyField constructor is ignored during CreateInspectorGUI when using a custom property drawer with UIElements. However, I have found a bit of a hack workaround using EditorCoroutineUtility.StartCoroutine. You can install the EditorCoroutine package via Window → Package Manager. Be sure to check Advanced → Show Preview Packages.

Below is an example that demonstrates the problem of not being able to set the labels as well as a solution. It uses the Recipe/Ingredient data structure from Unity - Scripting API: PropertyDrawer. Here’s a screenshot of the resulting UI (note the custom labels):

160480-screenshot-4.png


Assets/Recipe.cs

// From the example at https://docs.unity3d.com/ScriptReference/PropertyDrawer.html
using System;
using UnityEngine;

public enum IngredientUnit { Spoon, Cup, Bowl, Piece }

// Custom serializable class
[Serializable]
public class Ingredient
{
    public string name;
    public int amount = 1;
    public IngredientUnit unit;
}

public class Recipe : MonoBehaviour
{
    public Ingredient potionResult;
    public Ingredient[] potionIngredients;
}

Assets/Editor/IngredientDrawer.cs

using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine.UIElements;

[CustomPropertyDrawer(typeof(Ingredient))]
public class IngredientDrawer : PropertyDrawer
{
    public override VisualElement CreatePropertyGUI(SerializedProperty property)
    {
        // NOTE: This gets called when you call `ui.Bind()` in `RecipeEditor.cs`,
        //       but also gets called again automatically afterwards, which seems
        //       to be what's blowing away the changes to the labels.
        //Debug.Log($"[IngredientDrawer.CreatePropertyGUI] - {property.displayName}"); // For tracing

        var container = new VisualElement();

        // NOTE: These 3 `label` arguments work fine
        var amountField = new PropertyField(property.FindPropertyRelative("amount"), "Amount (arg)");
        var unitField = new PropertyField(property.FindPropertyRelative("unit"), "Unit (arg)");
        var nameField = new PropertyField(property.FindPropertyRelative("name"), "Name (arg)");

        container.Add(amountField);
        container.Add(unitField);
        container.Add(nameField);

        return container;
    }
}

Assets/Editor/IngredientDrawer.cs

using System.Collections;
using Unity.EditorCoroutines.Editor;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;

[CustomEditor(typeof(Recipe))]
public class RecipeEditor : Editor
{
    PropertyField resultField;
    PropertyField ingredientsField;

    public override VisualElement CreateInspectorGUI()
    {
        //Debug.Log($"[RecipeEditor.CreateInspectorGUI] - BEGIN"); // For tracing

        var ui = new VisualElement();

        // NOTE: The `label` arguments, "Result (arg)" and "Ingredients (arg)", seem to be ignored.
        //resultField = new PropertyField(serializedObject.FindProperty("potionResult"), "Result (arg)");
        //ingredientsField = new PropertyField(serializedObject.FindProperty("potionIngredients"), "Ingredients (arg)");

        // NOTE: Setting the `label` property after creating the fields also doesn't work.
        //resultField.label = "Result (set prop)";
        //ingredientsField.label = "Test Ingredients (set prop)";

        resultField = new PropertyField(serializedObject.FindProperty("potionResult"));
        ingredientsField = new PropertyField(serializedObject.FindProperty("potionIngredients"));
        ui.Add(resultField);
        ui.Add(ingredientsField);

        // Calling `Bind` actually fleshes out the PropertyFields above, but it doesn't matter,
        // since the fields get overwritten later, so setting the labels after this still won't work.
        ////Debug.Log(ingredientsField.childCount); // 0
        //ui.Bind(serializedObject);
        ////Debug.Log(ingredientsField.childCount); // 1 -- doesn't matter


        // HOWEVER!! Setting the labels in a coroutine later does work!!
        EditorCoroutineUtility.StartCoroutine(InitializeUICoroutine(), this);
        EditorCoroutineUtility.StartCoroutine(RefreshUICoroutine(), this);

        //Debug.Log($"[RecipeEditor.CreateInspectorGUI] - END"); // For tracing

        return ui;
    }

    void InitializeUI()
    {
        // Static labels (not dynamic list/array)
        // OPTION: Create and insert a new label
        resultField.Insert(0, new Label("Result (coroutine)"));
    }

    void RefreshUI()
    {
        //Debug.Log($"[RecipeEditor.UpdateLabels]"); // For tracing

        // OPTION: Find and edit an existing label
        var foldout = ingredientsField.Q<Foldout>("unity-foldout-potionIngredients");
        foldout.text = "Potion Ingredients (coroutine)";

        // Alternative quick-n-dirty way to do the above since we know it's the first label
        //ingredientsField.Q<Label>().text = "Potion Ingredients (coroutine2)";

        // Another example of finding and editing an existing label
        var sizeField = ingredientsField.Q<IntegerField>("unity-input-potionIngredients.Array.size");
        sizeField.label = "Size (coroutine)";

        // Really hacky way to set a label's text, if you know the exact child layout (FRAGILE!)
        //((Label)ingredientsField[0][0][0][0]).text = "Size (coroutine2)";

        // New size field needs a new event handler
        sizeField.RegisterCallback<BlurEvent>(HandleSizeFieldBlur);
    }

    IEnumerator InitializeUICoroutine()
    {
        yield return new WaitForSecondsRealtime(0.067f);
        InitializeUI();
    }

    IEnumerator RefreshUICoroutine()
    {
        yield return new WaitForSecondsRealtime(0.067f);
        RefreshUI();
    }

    void HandleSizeFieldBlur(BlurEvent evt)
    {
        EditorCoroutineUtility.StartCoroutine(RefreshUICoroutine(), this);
    }

}

And here’s a screenshot of when I was using the UIElements debugger to find the structure of the UI:

Note: tested with Unity version 2019.3.13f1 and 2020.1.0b3


Hope this helps!