How to make PropertyField interactable like normal property in custom editor script

Hi,

I made a custom editor script with UI Toolkit. It could assign property value by a dropdown field. But not like the normal property, I cannot right click to copy value or apply/revert prefab value. And it doesn’t show hint when prefab value is dirty.

9831057--1413939--螢幕擷取畫面 2024-05-13 145737.png

9831057--1413942--螢幕擷取畫面 2024-05-13 145801.png

What should I do to make the propery to provide prefab dirty hint and be interactable like normal propery?

Thank you

Sincerely,
Sherlore

Hi,
Can you share the code you are using to create the field?

Hi, thank you for reply.
Bellow are my codes:

  • UXML
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi="http://www.w3.org/2001/XMLSchema-instance" engine="UnityEngine.UIElements" editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="True">
    <ui:smile:ropdownField label="Color Key" index="-1" choices="System.Collections.Generic.List`1[System.String]" name="keyDropdown" />
    <uie:ColorField label="Color" value="#000000FF" show-eye-dropper="false" show-alpha="false" hdr="false" name="keyColor" focusable="true" style="margin-top: 2px;" />
</ui:UXML>

9835911--1414974--螢幕擷取畫面 2024-05-15 152627.png

  • Custom Inspector script
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEditor.UIElements;
using UnityEngine.UIElements;
using UnityEditor.AssetImporters;

[CustomEditor(typeof(GraphicColorBinder))]
public class GraphicColorBinderUIE : Editor
{
    public VisualTreeAsset m_UXML;
    private ColorField colorField;
   
    public override VisualElement CreateInspectorGUI()
    {
        var root = new VisualElement();
        InspectorElement.FillDefaultInspector(root, serializedObject, this);
       
        // Create property container element.
        m_UXML.CloneTree(root);
               
        var colorBinder = target as BaseColorBinder;
               
        int index = ThemeConfig.instance.colorOptions.IndexOf(colorBinder.key);
       
        if(index < 0)
        {
            index = 0;
           
            var csharpLabel = new Label( System.String.Format("Key({0}) doesn't exist in current theme. Use Default", colorBinder.key) );
            root.Add(csharpLabel);
        }
       
        var dropdownField = root.Q<DropdownField>("keyDropdown");
        dropdownField.choices = ThemeConfig.instance.colorOptions;
        dropdownField.value = ThemeConfig.instance.colorOptions[index];
               
        colorField = root.Q<ColorField>("keyColor");
        colorField.value = colorBinder.GetColor();
        colorField.SetEnabled(false);
       
        dropdownField.RegisterCallback<ChangeEvent<string>>((evt) =>
        {
            Undo.RecordObject(colorBinder, "Change color key");

            colorBinder.key = evt.newValue;
            colorField.value = colorBinder.GetColor();
        });

        return root;
    }
}

9835911--1414977--螢幕擷取畫面 2024-05-15 152644.png

  • Component
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

[AddComponentMenu("Theme/GraphicColorBinder")]
public class GraphicColorBinder : BaseColorBinder
{
    [SerializeField] Graphic graphic;

    protected override void Awake()
    {
        if (graphic == null) graphic = GetComponent<Graphic>();
       
        base.Awake();
    }
   
    public override void SetColor(Color color)
    {
        if (graphic == null)
        {
            return;
        }
        graphic.color = color;
    }
}
  • Inherited class of Component
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[ExecuteAlways, ExecuteInEditMode]
public abstract class BaseColorBinder : MonoBehaviour
{   
    [HideInInspector]
    public string m_key = "primary";
    public string key
    {
        get
        {
            return m_key;
        }
        set
        {
            m_key = value;
           
            UpdateColor();
        }
    }
   
    protected virtual void Awake()
    {
        Debug.Log("BaseColorBinder/Awake");
        UpdateColor();
       
        #if !UNITY_EDITOR
        if(!ThemeConfig.instance.useDynamicTheme)
        {
            return;
        }
        #endif
       
        if(!ThemeConfig.instance.IsInColorBinder(this))
        {
            ThemeConfig.instance.AddColorBinder(this);
        }
    }
   
    protected virtual void OnDestroy()
    {
        #if !UNITY_EDITOR
        if(!ThemeConfig.instance.useDynamicTheme)
        {
            return;
        }
        #endif
       
        if(ThemeConfig.instance.IsInColorBinder(this))
        {
            ThemeConfig.instance.RemoveColorBinder(this);
        }
    }
   
    public virtual void UpdateColor()
    {
        SetColor( GetColor() );
    }
   
    public Color GetColor()
    {
        if(ThemeConfig.instance.colorDic.ContainsKey(key))
        {
            return ThemeConfig.instance.colorDic[key];
        }
        else
        {
            Debug.LogWarning( System.String.Format("Object({0}) with key({1}) cannot found color in theme", this.name, key) );
            return Color.white;
        }
    }
   
    public abstract void SetColor(Color color);
   
    public virtual void OnValidate()
    {
        UpdateColor();
       
        #if !UNITY_EDITOR
        if(!ThemeConfig.instance.useDynamicTheme)
        {
            return;
        }
        #endif
       
        if(!ThemeConfig.instance.IsInColorBinder(this))
        {
            ThemeConfig.instance.AddColorBinder(this);
        }
    }
}

Thank you.

Sincerely,
Sherlore

The context menu is set by the PropertyField. You could add it manually but the API is internal so you would need to recreate it. For your example, I would recommend changing from using a custom Editor to a PropertyField with a custom property drawer.
You can decorate the field with an attribute to invoke the drawer just for the field you care about, the PropertyField will set up the context menu on the element for you.

1 Like

Thank you for information. But my property is actually a “string” value, and I am trying to let my user can set this string value only from a dropdown menu which composed by options from a dynamic string array (not Enum). That’s why I hide my string field in inspector and created a dropdwon to set string value with UI Toolkit.

Is that possible to make my string property a ProperyField that set by a dropdown with custom options?
Or is possible to make the dropdown menu has context menu and prefab diry hint like PropertyField?

Thank you very much

Sincerely,
Sherlore

You can do that with a property drawer, yes

2 Likes

Oh I think I finally got it. Thank you for help!

1 Like

Hi,
Thanks for your help. I am able to progress a little. But I ran into new confused.

9836994--1415166--螢幕擷取畫面 2024-05-16 001003.png

1.As the “Key” field, I am able to make my string property set by a custom dropdown. And the property has context menu function and would be marked dirty when prefab value was changed.

But the position of the mark is weird. The mark is not at the general position. How do I fix it?

My codes were showed as below:

  • I made a “ColorKeyAttribute”
public class ColorKeyAttribute : PropertyAttribute
{
}
  • And a PropertyDrawer of ColorKeyAttribute
[CustomPropertyDrawer(typeof(ColorKeyAttribute))]
public class ColorKeyDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        EditorGUI.BeginProperty(position, label, property);

        Rect popPosition = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label);

        int selectedValue = ThemeConfig.instance.colorOptions.IndexOf(property.stringValue);
        string[] displayOptions = ThemeConfig.instance.colorOptions.ToArray();

        EditorGUI.BeginChangeCheck();
        selectedValue = EditorGUI.Popup(popPosition, selectedValue, displayOptions);

        if (EditorGUI.EndChangeCheck())
        {
            property.stringValue = displayOptions[selectedValue];
        }

        EditorGUI.EndProperty();
    }
}
  • Made my string value using ColorKeyAttribute
[ColorKeyAttribute]
public string m_key = "primary";

2.Right now, I can only achieve this with IMGUI. Is it possible to achieve same functionality with UI Toolkit?

Yes you should use UI Toolkit, not IMGUI.
Override CreatePropertyGUI instead.
You may need to add this class to your field to get it aligned https://docs.unity3d.com/6000.0/Documentation/ScriptReference/UIElements.BaseField_1-alignedFieldUssClassName.html

Sorry, I have no idea how to achieve it with UI Toolkit. Can you give me some hint?

I can create a PropertyField with code:

[CustomPropertyDrawer(typeof(ColorKeyAttribute))]
public class ColorKeyDrawer : PropertyDrawer
{
    public override VisualElement CreatePropertyGUI(SerializedProperty property)
    {
        var container = new VisualElement();

        var keyField = new PropertyField(property);
        container.Add(keyField);

        return container;
    }
}

In this way, PropertyField works as general PropertyField, But string value is set by a input field in inspector.

I can create a Dropdown with code:

[CustomPropertyDrawer(typeof(ColorKeyAttribute))]
public class ColorKeyDrawer : PropertyDrawer
{
    public override VisualElement CreatePropertyGUI(SerializedProperty property)
    {
        var container = new VisualElement();
      
        string colorKey = property.stringValue;

        int index = ThemeConfig.instance.colorOptions.IndexOf(colorKey);

        if (index < 0)
        {
            index = 0;

            var csharpLabel = new Label(System.String.Format("Key({0}) doesn't exist in current theme. Use Default", colorKey));
            container.Add(csharpLabel);
        }

        var dropdownField = new DropdownField("ColorKey2", ThemeConfig.instance.colorOptions, ThemeConfig.instance.colorOptions[index]);
        //dropdownField.AddToClassList("some-styled-field");

        dropdownField.RegisterCallback<ChangeEvent<string>>((evt) =>
        {
            property.stringValue = evt.newValue;
        });

        container.Add(dropdownField);

        return container;
    }
}

But in this case, set property.stringValue with dropdownField.RegisterCallback not work.
And DropdownField doesn’t not has the functionality of context menu or prefab value dirty.

How do I make it work correct?

Thanks in advance.

You need to bind the field to the property. This will give it the context menu.

dropdownField.BindProperty(property)

You may also need to do this to get the label to align correctly.

dropdownField.AddToClassList(BaseField<string>.alignedFieldUssClassName);

Oh my god! Thank you! It works perfect now!. Exactly what I want to achieve. You are so amazing and professional :slight_smile:

Thanks for help. I have learned a lot from you.

1 Like