TylerTemp / SaintsField

A Unity Inspector extension tool focusing on script fields inspector enhancement
MIT License
148 stars 9 forks source link

UI Toolkit prevent CustomPropertyDrawer of generic subclass from being used #23

Closed ACCIAI0 closed 3 months ago

ACCIAI0 commented 4 months ago

If I use either ShowIf or HideIf attribute on a field or property with a Type with a custom property drawer, that field is rendered in the inspector with a default PropertyField instead of using the custom drawer. For Example:

immagine

This is how it looks like with the HideIf attribute,

immagine

This is how it should be using the custom drawer (it only shows the internal list of entries)

TylerTemp commented 4 months ago

Hi,

according to the screenshot, you seems are using IMGUI instead of UI Toolkit.

For SaintsField, only UI Toolkit can fallback to a CustomPropertyDrawer of a data type.

IMGUI has some limitation. I tried once but failed. If you have some good suggestion about IMGUI please share.

So, as an example, this is the code:

Source.cs

[Serializable]
public struct Source
{
    [SerializeField] private string[] serializedEntries;
}

SourceDrawer.cs

using UnityEditor;
using UnityEngine;
#if UNITY_2021_3_OR_NEWER
using UnityEngine.UIElements;
using UnityEditor.UIElements;
#endif

[CustomPropertyDrawer(typeof(Source))]
public class SourceDrawer : PropertyDrawer
{
    #region IMGUI

    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        SerializedProperty arrProperty = property.FindPropertyRelative("serializedEntries");
        return EditorGUI.GetPropertyHeight(arrProperty, label, true);
    }

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        SerializedProperty arrProperty = property.FindPropertyRelative("serializedEntries");
        EditorGUI.PropertyField(position, arrProperty, label, true);
    }
    #endregion

#if UNITY_2021_3_OR_NEWER
    public override VisualElement CreatePropertyGUI(SerializedProperty property)
    {
        SerializedProperty arrProperty = property.FindPropertyRelative("serializedEntries");
        return new PropertyField(arrProperty, property.displayName);
    }
#endif
}

SourceExample.cs

public class SourceExample : MonoBehaviour
{
    public Source normal;
    [ShowIf, InfoBox("Type CustomDrawer fallback only works on UI Toolkit", above: true)] public Source withIf;
}

Then, in IMGUI it won't use the custom drawer:

image

But UI Toolkit works fine:

image


In short:

  1. Custom drawer fallback only works for UI Toolkit. I can not figure out how to make it work in IMGUI.
  2. If you really want to use this feature, please consider switch to UI Toolkit, which works in Unity 2021.3+. If you've already using 2021.3+, ensure you didn't accidentally disabled it by using NaughtyAttributes or a very old version of Odin.
  3. If you can only use IMGUI, other workaround is to change your custom drawer from data type to a decorator type, which you need to define a PropertyAttribute, the write drawer for that attribute

Edit:

I did write it in change log but forget in document... will update the document soon.

2.4.2

  1. Since this version, UI Toolkit now can property fallback to CustomPropertyDrawer of a custom type (previously it only supports to fallback to custom PropertyAttribute drawer).

    Note: this feature is only for UI Toolkit. IMGUI does not support this feature.

    Note: combining with RichLabel, UI Toolkit will find the first unity-label class label which in some case might not be correct. This feature can not be turned off yet. Please report issue if you face any problem.

TylerTemp commented 3 months ago

... Wait... I sort of having some idea...

ACCIAI0 commented 3 months ago

@TylerTemp The fact is I AM using UIElements. the only difference between my code and your example is that the custom editor is technically assigned to the abstract parent type of the one I serialize in my screenshot, with editorOfChildClasses = true

TylerTemp commented 3 months ago

@ACCIAI0 Do you mean:

[Serializable]
public abstract class SuperSource
{

}

[Serializable]
public class Source: SuperSource
{
    [SerializeField] private string[] serializedEntries;
}

// public Source normal;
[HideIf(nameof(toggle)), InfoBox("Type CustomDrawer fallback", above: true)] public Source withIf;

with drawer:

[CustomPropertyDrawer(typeof(ImGuiFallback.SuperSource))]  // super class of the data
public class SourceDrawer : PropertyDrawer
{
    #region IMGUI

    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        SerializedProperty arrProperty = property.FindPropertyRelative("serializedEntries");
        return EditorGUI.GetPropertyHeight(arrProperty, label, true);
    }

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        SerializedProperty arrProperty = property.FindPropertyRelative("serializedEntries");
        EditorGUI.PropertyField(position, arrProperty, label, true);
    }
    #endregion

#if UNITY_2021_3_OR_NEWER
    public override VisualElement CreatePropertyGUI(SerializedProperty property)
    {
        SerializedProperty arrProperty = property.FindPropertyRelative("serializedEntries");
        return new PropertyField(arrProperty, property.displayName);
    }
#endif
}

I still can not re-produce it on Unity 2022.2.0f1, 2021.3.0f1 under UI Toolkit. Please share more information, like your Unity version, and a simple drawer that has this issue

ACCIAI0 commented 3 months ago

I'm using it in Unity 2022.3.20f1, but I'm unsure if it makes any difference. The bare-bones skeleton of my code is as follows:

[Serializable]
public class Container<T1, T2>
{
    [Serializable]
    protected sealed class Entry
    {
        public T1 Name;
        public T2 Object;
    }

    [SerializeField]
    protected List<Entry> Entries;
}

[Serializable]
public sealed class ContainerChild<T> : Container<string, T>
{
    public T GetByName(string name)
    {
        var entry = Entries.Find(e => e.Name == name);
        return entry == default ? default : entry.Object;
    }
}

while the property drawer is as follows:

[CustomPropertyDrawer(typeof(Container<,>), true)]
public sealed class ContainerDrawer : PropertyDrawer
{
    public override VisualElement CreatePropertyGUI(SerializedProperty property)
    {
        var entries = property.FindPropertyRelative("Entries");
        var listView = new ListView
        {
            headerTitle = property.displayName,
            showBorder = true,
            showFoldoutHeader = true,
            showAddRemoveFooter = true,
            showAlternatingRowBackgrounds = AlternatingRowBackground.All,
            virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight,
            showBoundCollectionSize = true,
            makeItem = MakeItem,
            bindItem = BindItem
        };
        listView.BindProperty(entries);

        return listView;

        VisualElement MakeItem()
        {
            var element = new VisualElement();

            var name = new TextField("Name") { name = "Name" };
            var obj = new PropertyField { label = "Object", name = "Object" };

            name.AddToClassList(BaseField<object>.alignedFieldUssClassName);

            element.Add(name);
            element.Add(obj);

            return element;
        }

        void BindItem(VisualElement element, int index)
        {
            var name = element.Q<TextField>("Name");
            var obj = element.Q<PropertyField>("Object");

            var item = entries.GetArrayElementAtIndex(index);
            name.BindProperty(item.FindPropertyRelative("Name"));
            obj.BindProperty(item.FindPropertyRelative("Object"));
        }
    }
}

Using this exact code it works, but the original code affected by this bug still doesn't. I'll investigate further and let you know if I find something.

NOTE: The property drawer is technically wrong, as it works only for that single subclass I made, but for the sake of the example it doesn't matter

TylerTemp commented 3 months ago

Bug confirmed

TylerTemp commented 3 months ago

Hi @ACCIAI0

Please check if it's fixed in 3.0.6

given the code you provide with:

[Serializable]
public  class GameObjectChild : ContainerChild<GameObject>
{
}

public bool toggle;

public GameObjectChild normal;

[HideIf(nameof(toggle)), InfoBox("Inherent Fallback", above: true)] public GameObjectChild inherent;

[HideIf(nameof(toggle)), InfoBox("Direct Fallback", above: true)] public ContainerChild<GameObject> direct;

gives:

image

TylerTemp commented 3 months ago

This issue is closed cuz it's fixed.

If you still have a problem with this issue, please comment here and I'll open this issue again.

ACCIAI0 commented 3 months ago

Sorry, I had a lot of stuff to do and this slipped my mind. I tried it out and can confirm it works like a charm. Nice job, your speed at addressing issues make this library very valuable to me!