Custom UI Builder Attribute Fields

I’ve looked all over the packages and couldn’t find…
I want to use it as reference for a Type field that lets you pick from a Type’s subtypes

EnumField is part of core, but it’s only available in the Editor (not for Runtime). Where are you looking for it? Which packages?

Also, for what you describe, you may need to use PopupField instead if you want to control what the options are. EnumField just takes an Enum and derives the options from the options in the Enum type. You cannot add/remove options.

I am attempting to put a type picker here:


I got so far as making a custom UxmlClassAttributeDescription : TypedUxmlAttributeDescription<Type>
It does use restrictions, but it seems to do nothing.

        UxmlEnumeration enumRestriction = new UxmlEnumeration();

        var values = new List<string>();
        //GetAllClassesExtending exclusing any assemblies containing those strings
        Type[] types = Utils.GetAllClassesExtending<Manipulator>("Unity", "Microsoft", "System", "Mono", "UMotion");
        Array.Sort(types, (T1, T2) => T2.FullName.CompareTo(T1.FullName));
        values = types.Cast(T => T.FullName).ToList();
        enumRestriction.values = values;

        restriction = enumRestriction;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine.UIElements;

public class UxmlClassAttributeDescription : TypedUxmlAttributeDescription<Type> {
    public UxmlClassAttributeDescription() {

        type = "Type";
        typeNamespace = xmlSchemaNamespace;
        defaultValue = typeof(Type);

        UxmlEnumeration enumRestriction = new UxmlEnumeration();

        var values = new List<string>();

        Type[] types = Utils.GetAllClassesExtending<Manipulator>("Unity", "Microsoft", "System", "Mono", "UMotion");
        Array.Sort(types, (T1, T2) => T2.FullName.CompareTo(T1.FullName));
        values = types.Cast(T => T.FullName).ToList();
        enumRestriction.values = values;

        restriction = enumRestriction;
    }

    public override string defaultValueAsString { get { return defaultValue.AssemblyQualifiedName; } }

    public override Type GetValueFromBag(IUxmlAttributes bag, CreationContext cc) {
        return GetValueFromBag(bag, cc, (s, t) => {
            Type tp = Type.GetType(s);
            if (tp != null) {
                return tp;
            }
            return t;
        }, defaultValue);
    }
    public bool TryGetValueFromBag(IUxmlAttributes bag, CreationContext cc, ref Type value) {
        return TryGetValueFromBag(bag, cc, (s, t) => {
            Type tp = Type.GetType(s);
            if (tp != null) {
                return tp;
            }
            return t;
        }, defaultValue, ref value);
    }
}

As a bonus question, can I make extra fields to appear depending on the value of the Manipulator field?
Then I would be able to put per-manipulator parameters

Ah, you’re tying to customize the UI Builder’s Attributes Inspector. This part of the Builder is fairly limited as the focus until now has mostly been to support the default set of UXML attribute types. For example, it does not take into account enumRestriction. It’s something we’ll address in future releases of the Builder but it’s still not our highest priority.

Now, if I were in your position, without the ability to change the code of the Builder, what I’d do is abuse the fact that the UI Builder is just as UI Toolkit as you are. What I mean is, in your custom AddManipulator element, when you get the AttachToPanel event, you can walk up your parents and determine if you are inside the UI Builder’s Canvas. Not only that, but you can then walk back down and find the Builder’s Attributes Inspector and start adding your own custom fields in there. You can use the UI Debugger to get a sense of the Builder’s element hierarchy and what element names to Query for.

Of course, you’ll be on your own and you’ll be responsible for cleaning up any fields you add to the Inspector on de-selection and/or removal of your custom element. But it’s…possible.

Now that’s some crazy stuff, good to know that it’s that unconventionally extensible, but i’d rather wait for a conventional way to manipulate that and just use a monobehaviour to attach manipulators until then.
Unless extending the UI Builder’s Attributes Inspector is scheduled for 2022+, then I guess it will be worth going through the effort to “hack” my way in as you proposed.

After a bit of testing, I figured out that extending the builder will actually be much faster than making the monobehaviours to add the manipulators. So I have to leave a request after reading some of the sources.

Create a “ICustomAttributeField” interface that have a “BindableElement CreateField()” method to it and place a hook for it in BuilderInspectorAttributes.cs right before the last } else { of BuilderStyleRow CreateAttributeRow(UxmlAttributeDescription attribute) (line 167 as of my version) that checks if the attribute have the interface, and invokes the interface method to build the field.
As I understand from the source, that should instantly give us access to custom attribute fields without all of this workaround.
I mean, an ‘unknown’ Uxml***AttributeDescription will default to just 3 simple lines:

                var uiField = new TextField(fieldLabel);
                uiField.RegisterValueChangedCallback(OnAttributeValueChange);
                fieldElement = uiField;

Meanwhile I will fumble about and attempt to do the workaround.

I actually did manage to implement the feature myself:


Of course, since it changed BuilderInspectorAttributes.cs, it will keep being undone constantly. Hopefully there are no issues with simply incorporating this change into the code, or a similar better way to go about it.
Source:

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
public interface ICustomAttributeField {
    BindableElement BuildField(string fieldLabel, EventCallback<ChangeEvent<string>> bind);
}
namespace Unity.UI.Builder
{
    internal class BuilderInspectorAttributes : IBuilderInspectorSection
    {
        BuilderInspector m_Inspector;
        BuilderSelection m_Selection;
        PersistedFoldout m_AttributesSection;

        VisualElement currentVisualElement => m_Inspector.currentVisualElement;

        public VisualElement root => m_AttributesSection;

        public BuilderInspectorAttributes(BuilderInspector inspector)
        {
            m_Inspector = inspector;
            m_Selection = inspector.selection;

            m_AttributesSection = m_Inspector.Q<PersistedFoldout>("inspector-attributes-foldout");
        }

        public void Refresh()
        {
            m_AttributesSection.Clear();

            if (currentVisualElement == null)
                return;

            m_AttributesSection.text = currentVisualElement.typeName;

            if (m_Selection.selectionType != BuilderSelectionType.Element &&
                m_Selection.selectionType != BuilderSelectionType.ElementInTemplateInstance)
                return;

            GenerateAttributeFields();

            // Forward focus to the panel header.
            m_AttributesSection
                .Query()
                .Where(e => e.focusable)
                .ForEach((e) => m_Inspector.AddFocusable(e));
        }

        public void Enable()
        {
            m_AttributesSection.contentContainer.SetEnabled(true);
        }

        public void Disable()
        {
            m_AttributesSection.contentContainer.SetEnabled(false);
        }

        void GenerateAttributeFields()
        {
            var attributeList = currentVisualElement.GetAttributeDescriptions();

            foreach (var attribute in attributeList)
            {
                if (attribute == null || attribute.name == null)
                    continue;

                var styleRow = CreateAttributeRow(attribute);
                m_AttributesSection.Add(styleRow);
            }
        }

        BuilderStyleRow CreateAttributeRow(UxmlAttributeDescription attribute)
        {
            var attributeType = attribute.GetType();

            // Generate field label.
            var fieldLabel = BuilderNameUtilities.ConvertDashToHuman(attribute.name);
            BindableElement fieldElement;
            if (attribute is UxmlStringAttributeDescription)
            {
                var uiField = new TextField(fieldLabel);
                if (attribute.name.Equals("name") || attribute.name.Equals("view-data-key"))
                    uiField.RegisterValueChangedCallback(e =>
                    {
                        OnValidatedAttributeValueChange(e, BuilderNameUtilities.AttributeRegex, BuilderConstants.AttributeValidationSpacialCharacters);
                    });
                else if (attribute.name.Equals("binding-path"))
                    uiField.RegisterValueChangedCallback(e =>
                    {
                        OnValidatedAttributeValueChange(e, BuilderNameUtilities.BindingPathAttributeRegex, BuilderConstants.BindingPathAttributeValidationSpacialCharacters);
                    });
                else
                    uiField.RegisterValueChangedCallback(OnAttributeValueChange);

                if (attribute.name.Equals("text"))
                {
                    uiField.multiline = true;
                    uiField.AddToClassList(BuilderConstants.InspectorMultiLineTextFieldClassName);
                }

                fieldElement = uiField;
            }
            else if (attribute is UxmlFloatAttributeDescription)
            {
                var uiField = new FloatField(fieldLabel);
                uiField.RegisterValueChangedCallback(OnAttributeValueChange);
                fieldElement = uiField;
            }
            else if (attribute is UxmlDoubleAttributeDescription)
            {
                var uiField = new DoubleField(fieldLabel);
                uiField.RegisterValueChangedCallback(OnAttributeValueChange);
                fieldElement = uiField;
            }
            else if (attribute is UxmlIntAttributeDescription)
            {
                var uiField = new IntegerField(fieldLabel);
                uiField.RegisterValueChangedCallback(OnAttributeValueChange);
                fieldElement = uiField;
            }
            else if (attribute is UxmlLongAttributeDescription)
            {
                var uiField = new LongField(fieldLabel);
                uiField.RegisterValueChangedCallback(OnAttributeValueChange);
                fieldElement = uiField;
            }
            else if (attribute is UxmlBoolAttributeDescription)
            {
                var uiField = new Toggle(fieldLabel);
                uiField.RegisterValueChangedCallback(OnAttributeValueChange);
                fieldElement = uiField;
            }
            else if (attribute is UxmlColorAttributeDescription)
            {
                var uiField = new ColorField(fieldLabel);
                uiField.RegisterValueChangedCallback(OnAttributeValueChange);
                fieldElement = uiField;
            }
            else if (attributeType.IsGenericType &&
                !attributeType.GetGenericArguments()[0].IsEnum &&
                attributeType.GetGenericArguments()[0] is Type)
            {
                var uiField = new TextField(fieldLabel);
                uiField.isDelayed = true;
                uiField.RegisterValueChangedCallback(e =>
                {
                    OnValidatedTypeAttributeChange(e, attributeType.GetGenericArguments()[0]);
                });
                fieldElement = uiField;
            }
            else if (attributeType.IsGenericType && attributeType.GetGenericArguments()[0].IsEnum)
            {
                var propInfo = attributeType.GetProperty("defaultValue");
                var enumValue = propInfo.GetValue(attribute, null) as Enum;

                // Create and initialize the EnumField.
                var uiField = new EnumField(fieldLabel);
                uiField.Init(enumValue);

                uiField.RegisterValueChangedCallback(OnAttributeValueChange);
                fieldElement = uiField;
            }
            else if (typeof(ICustomAttributeField).IsAssignableFrom(attribute.GetType())) {
                fieldElement = ((ICustomAttributeField)attribute).BuildField(fieldLabel, OnAttributeValueChange);
            }
            else
            {
                var uiField = new TextField(fieldLabel);
                uiField.RegisterValueChangedCallback(OnAttributeValueChange);
                fieldElement = uiField;
            }

            // Create row.
            var styleRow = new BuilderStyleRow();
            styleRow.Add(fieldElement);

            // Link the field.
            fieldElement.SetProperty(BuilderConstants.InspectorLinkedStyleRowVEPropertyName, styleRow);
            fieldElement.SetProperty(BuilderConstants.InspectorLinkedAttributeDescriptionVEPropertyName, attribute);

            // Set initial value.
            RefreshAttributeField(fieldElement);

            // Setup field binding path.
            fieldElement.bindingPath = attribute.name;

            // Tooltip.
            var label = fieldElement.Q<Label>();
            if (label != null)
                label.tooltip = attribute.name;
            else
                fieldElement.tooltip = attribute.name;

            // Context menu.
            fieldElement.AddManipulator(new ContextualMenuManipulator(BuildAttributeFieldContextualMenu));

            return styleRow;
        }

        object GetCustomValueAbstract(string attributeName)
        {
            if (currentVisualElement is ScrollView)
            {
                var scrollView = currentVisualElement as ScrollView;
                if (attributeName == "mode")
                {
                    if (scrollView.ClassListContains(ScrollView.verticalVariantUssClassName))
                        return ScrollViewMode.Vertical;
                    else if (scrollView.ClassListContains(ScrollView.horizontalVariantUssClassName))
                        return ScrollViewMode.Horizontal;
                    else if (scrollView.ClassListContains(ScrollView.verticalHorizontalVariantUssClassName))
                        return ScrollViewMode.VerticalAndHorizontal;
                }
                else if (attributeName == "show-horizontal-scroller")
                {
                    return scrollView.showHorizontal;
                }
                else if (attributeName == "show-vertical-scroller")
                {
                    return scrollView.showVertical;
                }
            }
            else if (currentVisualElement is ObjectField objectField)
            {
                if (attributeName == "type")
                {
                    return objectField.objectType;
                }
            }

            return null;
        }

        void RefreshAttributeField(BindableElement fieldElement)
        {
            var styleRow = fieldElement.GetProperty(BuilderConstants.InspectorLinkedStyleRowVEPropertyName) as VisualElement;
            var attribute = fieldElement.GetProperty(BuilderConstants.InspectorLinkedAttributeDescriptionVEPropertyName) as UxmlAttributeDescription;

            var veType = currentVisualElement.GetType();
            var camel = BuilderNameUtilities.ConvertDashToCamel(attribute.name);

            var fieldInfo = veType.GetProperty(camel, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.IgnoreCase);

            object veValueAbstract = null;
            if (fieldInfo == null)
            {
                veValueAbstract = GetCustomValueAbstract(attribute.name);
            }
            else
            {
                veValueAbstract = fieldInfo.GetValue(currentVisualElement);
            }
            if (veValueAbstract == null)
                return;

            var attributeType = attribute.GetType();
            var vea = currentVisualElement.GetVisualElementAsset();

            if (attribute is UxmlStringAttributeDescription && fieldElement is TextField)
            {
                (fieldElement as TextField).SetValueWithoutNotify(GetAttributeStringValue(veValueAbstract));
            }
            else if (attribute is UxmlFloatAttributeDescription && fieldElement is FloatField)
            {
                (fieldElement as FloatField).SetValueWithoutNotify((float)veValueAbstract);
            }
            else if (attribute is UxmlDoubleAttributeDescription && fieldElement is DoubleField)
            {
                (fieldElement as DoubleField).SetValueWithoutNotify((double)veValueAbstract);
            }
            else if (attribute is UxmlIntAttributeDescription && fieldElement is IntegerField)
            {
                if (veValueAbstract is int)
                    (fieldElement as IntegerField).SetValueWithoutNotify((int)veValueAbstract);
                else if (veValueAbstract is float)
                    (fieldElement as IntegerField).SetValueWithoutNotify(Convert.ToInt32(veValueAbstract));
            }
            else if (attribute is UxmlLongAttributeDescription && fieldElement is LongField)
            {
                (fieldElement as LongField).SetValueWithoutNotify((long)veValueAbstract);
            }
            else if (attribute is UxmlBoolAttributeDescription && fieldElement is Toggle)
            {
                (fieldElement as Toggle).SetValueWithoutNotify((bool)veValueAbstract);
            }
            else if (attribute is UxmlColorAttributeDescription && fieldElement is ColorField)
            {
                (fieldElement as ColorField).SetValueWithoutNotify((Color)veValueAbstract);
            }
            else if (attributeType.IsGenericType &&
                !attributeType.GetGenericArguments()[0].IsEnum &&
                attributeType.GetGenericArguments()[0] is Type &&
                fieldElement is TextField textField &&
                veValueAbstract is Type veTypeValue)
            {
                var fullTypeName = veTypeValue.AssemblyQualifiedName;
                var fullTypeNameSplit = fullTypeName.Split(',');
                textField.SetValueWithoutNotify($"{fullTypeNameSplit[0]},{fullTypeNameSplit[1]}");
            }
            else if (attributeType.IsGenericType &&
                attributeType.GetGenericArguments()[0].IsEnum &&
                fieldElement is EnumField)
            {
                var propInfo = attributeType.GetProperty("defaultValue");
                var enumValue = propInfo.GetValue(attribute, null) as Enum;

                // Create and initialize the EnumField.
                var uiField = fieldElement as EnumField;

                // Set the value from the UXML attribute.
                var enumAttributeValueStr = vea?.GetAttributeValue(attribute.name);
                if (!string.IsNullOrEmpty(enumAttributeValueStr))
                {
                    var parsedValue = Enum.Parse(enumValue.GetType(), enumAttributeValueStr, true) as Enum;
                    uiField.SetValueWithoutNotify(parsedValue);
                }
            }
            else if (fieldElement is TextField)
            {
                (fieldElement as TextField).SetValueWithoutNotify(veValueAbstract.ToString());
            }

            styleRow.RemoveFromClassList(BuilderConstants.InspectorLocalStyleOverrideClassName);
            if (IsAttributeOverriden(attribute))
                styleRow.AddToClassList(BuilderConstants.InspectorLocalStyleOverrideClassName);
        }

        string GetAttributeStringValue(object attributeValue)
        {
            string value;
            if (attributeValue is Enum @enum)
                value = @enum.ToString();
            else if (attributeValue is IList<string> list)
            {
                value = string.Join(",", list);
            }
            else
            {
                value = attributeValue.ToString();
            }

            return value;
        }

        bool IsAttributeOverriden(UxmlAttributeDescription attribute)
        {
            var vea = currentVisualElement.GetVisualElementAsset();
            if (vea != null && attribute.name == "picking-mode")
            {
                var veaAttributeValue = vea.GetAttributeValue(attribute.name);
                if (veaAttributeValue != null &&
                    veaAttributeValue.ToLower() != attribute.defaultValueAsString.ToLower())
                    return true;
            }
            else if (attribute.name == "name")
            {
                if (!string.IsNullOrEmpty(currentVisualElement.name))
                    return true;
            }
            else if (vea != null && vea.HasAttribute(attribute.name))
                return true;

            return false;
        }

        void ResetAttributeFieldToDefault(BindableElement fieldElement)
        {
            var styleRow = fieldElement.GetProperty(BuilderConstants.InspectorLinkedStyleRowVEPropertyName) as VisualElement;
            var attribute = fieldElement.GetProperty(BuilderConstants.InspectorLinkedAttributeDescriptionVEPropertyName) as UxmlAttributeDescription;

            var attributeType = attribute.GetType();
            var vea = currentVisualElement.GetVisualElementAsset();

            if (attribute is UxmlStringAttributeDescription && fieldElement is TextField)
            {
                var a = attribute as UxmlStringAttributeDescription;
                var f = fieldElement as TextField;
                f.SetValueWithoutNotify(a.defaultValue);
            }
            else if (attribute is UxmlFloatAttributeDescription && fieldElement is FloatField)
            {
                var a = attribute as UxmlFloatAttributeDescription;
                var f = fieldElement as FloatField;
                f.SetValueWithoutNotify(a.defaultValue);
            }
            else if (attribute is UxmlDoubleAttributeDescription && fieldElement is DoubleField)
            {
                var a = attribute as UxmlDoubleAttributeDescription;
                var f = fieldElement as DoubleField;
                f.SetValueWithoutNotify(a.defaultValue);
            }
            else if (attribute is UxmlIntAttributeDescription && fieldElement is IntegerField)
            {
                var a = attribute as UxmlIntAttributeDescription;
                var f = fieldElement as IntegerField;
                f.SetValueWithoutNotify(a.defaultValue);
            }
            else if (attribute is UxmlLongAttributeDescription && fieldElement is LongField)
            {
                var a = attribute as UxmlLongAttributeDescription;
                var f = fieldElement as LongField;
                f.SetValueWithoutNotify(a.defaultValue);
            }
            else if (attribute is UxmlBoolAttributeDescription && fieldElement is Toggle)
            {
                var a = attribute as UxmlBoolAttributeDescription;
                var f = fieldElement as Toggle;
                f.SetValueWithoutNotify(a.defaultValue);
            }
            else if (attribute is UxmlColorAttributeDescription && fieldElement is ColorField)
            {
                var a = attribute as UxmlColorAttributeDescription;
                var f = fieldElement as ColorField;
                f.SetValueWithoutNotify(a.defaultValue);
            }
            else if (attributeType.IsGenericType &&
                !attributeType.GetGenericArguments()[0].IsEnum &&
                attributeType.GetGenericArguments()[0] is Type &&
                fieldElement is TextField)
            {
                var a = attribute as TypedUxmlAttributeDescription<Type>;
                var f = fieldElement as TextField;
                if (a.defaultValue == null)
                    f.SetValueWithoutNotify(string.Empty);
                else
                    f.SetValueWithoutNotify(a.defaultValue.ToString());
            }
            else if (attributeType.IsGenericType &&
                attributeType.GetGenericArguments()[0].IsEnum &&
                fieldElement is EnumField)
            {
                var propInfo = attributeType.GetProperty("defaultValue");
                var enumValue = propInfo.GetValue(attribute, null) as Enum;

                var uiField = fieldElement as EnumField;
                uiField.SetValueWithoutNotify(enumValue);
            }
            else if (fieldElement is TextField)
            {
                (fieldElement as TextField).SetValueWithoutNotify(string.Empty);
            }

            // Clear override.
            styleRow.RemoveFromClassList(BuilderConstants.InspectorLocalStyleOverrideClassName);
            var styleFields = styleRow.Query<BindableElement>().ToList();
            foreach (var styleField in styleFields)
            {
                styleField.RemoveFromClassList(BuilderConstants.InspectorLocalStyleResetClassName);
                styleField.RemoveFromClassList(BuilderConstants.InspectorLocalStyleOverrideClassName);
            }
        }

        void BuildAttributeFieldContextualMenu(ContextualMenuPopulateEvent evt)
        {
            evt.menu.AppendAction(
                BuilderConstants.ContextMenuUnsetMessage,
                UnsetAttributeProperty,
                action =>
                {
                    var fieldElement = action.userData as BindableElement;
                    if (fieldElement == null)
                        return DropdownMenuAction.Status.Disabled;

                    var attributeName = fieldElement.bindingPath;
                    var vea = currentVisualElement.GetVisualElementAsset();
                    return vea.HasAttribute(attributeName)
                        ? DropdownMenuAction.Status.Normal
                        : DropdownMenuAction.Status.Disabled;
                },
                evt.target);

            evt.menu.AppendAction(
                BuilderConstants.ContextMenuUnsetAllMessage,
                UnsetAllAttributes,
                action =>
                {
                    var attributeList = currentVisualElement.GetAttributeDescriptions();
                    foreach (var attribute in attributeList)
                    {
                        if (attribute?.name == null)
                            continue;

                        if(IsAttributeOverriden(attribute))
                            return DropdownMenuAction.Status.Normal;
                    }

                    return DropdownMenuAction.Status.Disabled;
                },
                evt.target);
        }

        void UnsetAllAttributes(DropdownMenuAction action)
        {
            var attributeList = currentVisualElement.GetAttributeDescriptions();

            // Undo/Redo
            Undo.RegisterCompleteObjectUndo(m_Inspector.visualTreeAsset, BuilderConstants.ChangeAttributeValueUndoMessage);

            foreach (var attribute in attributeList)
            {
                if (attribute?.name == null)
                    continue;

                // Unset value in asset.
                var vea = currentVisualElement.GetVisualElementAsset();
                vea.RemoveAttribute(attribute.name);
            }

            var fields = m_AttributesSection.Query<BindableElement>().Where(e => !string.IsNullOrEmpty(e.bindingPath)).ToList();
            foreach (var fieldElement in fields)
            {
                // Reset UI value.
                ResetAttributeFieldToDefault(fieldElement);
            }

            // Call Init();
            CallInitOnElement();

            // Notify of changes.
            m_Selection.NotifyOfHierarchyChange(m_Inspector);
        }

        void UnsetAttributeProperty(DropdownMenuAction action)
        {
            var fieldElement = action.userData as BindableElement;
            var attributeName = fieldElement.bindingPath;


            // Undo/Redo
            Undo.RegisterCompleteObjectUndo(m_Inspector.visualTreeAsset, BuilderConstants.ChangeAttributeValueUndoMessage);

            // Unset value in asset.
            var vea = currentVisualElement.GetVisualElementAsset();
            vea.RemoveAttribute(attributeName);

            // Reset UI value.
            ResetAttributeFieldToDefault(fieldElement);

            // Call Init();
            CallInitOnElement();

            // Notify of changes.
            m_Selection.NotifyOfHierarchyChange(m_Inspector);
        }

        void OnAttributeValueChange(ChangeEvent<string> evt)
        {
            var field = evt.target as TextField;
            PostAttributeValueChange(field, evt.newValue);
        }

        void OnValidatedTypeAttributeChange(ChangeEvent<string> evt, Type desiredType)
        {
            var field = evt.target as TextField;
            var typeName = evt.newValue;
            var fullTypeName = typeName;
            if (!string.IsNullOrEmpty(typeName))
            {
                var type = Type.GetType(fullTypeName, false);

                // Try some auto-fixes.
                if (type == null)
                {
                    fullTypeName = typeName + ", UnityEngine.CoreModule";
                    type = Type.GetType(fullTypeName, false);
                }
                if (type == null)
                {
                    fullTypeName = typeName + ", UnityEditor";
                    type = Type.GetType(fullTypeName, false);
                }
                if (type == null && typeName.Contains("."))
                {
                    var split = typeName.Split('.');
                    fullTypeName = typeName + $", {split[0]}.{split[1]}Module";
                    type = Type.GetType(fullTypeName, false);
                }

                if (type == null)
                {
                    Builder.ShowWarning(string.Format(BuilderConstants.TypeAttributeInvalidTypeMessage, field.label));
                    evt.StopPropagation();
                    return;
                }
                else if (!desiredType.IsAssignableFrom(type))
                {
                    Builder.ShowWarning(string.Format(BuilderConstants.TypeAttributeMustDeriveFromMessage, field.label, desiredType.FullName));
                    evt.StopPropagation();
                    return;
                }
            }

            field.SetValueWithoutNotify(fullTypeName);
            PostAttributeValueChange(field, fullTypeName);
        }

        void OnValidatedAttributeValueChange(ChangeEvent<string> evt, Regex regex, string message)
        {
            var field = evt.target as TextField;
            if (!string.IsNullOrEmpty(evt.newValue) && !regex.IsMatch(evt.newValue))
            {
                Builder.ShowWarning(string.Format(message, field.label));
                field.SetValueWithoutNotify(evt.previousValue);
                evt.StopPropagation();
                return;
            }

            OnAttributeValueChange(evt);
        }

        void OnAttributeValueChange(ChangeEvent<float> evt)
        {
            var field = evt.target as FloatField;
            PostAttributeValueChange(field, evt.newValue.ToString());
        }

        void OnAttributeValueChange(ChangeEvent<double> evt)
        {
            var field = evt.target as DoubleField;
            PostAttributeValueChange(field, evt.newValue.ToString());
        }

        void OnAttributeValueChange(ChangeEvent<int> evt)
        {
            var field = evt.target as IntegerField;
            PostAttributeValueChange(field, evt.newValue.ToString());
        }

        void OnAttributeValueChange(ChangeEvent<long> evt)
        {
            var field = evt.target as LongField;
            PostAttributeValueChange(field, evt.newValue.ToString());
        }

        void OnAttributeValueChange(ChangeEvent<bool> evt)
        {
            var field = evt.target as Toggle;
            PostAttributeValueChange(field, evt.newValue.ToString().ToLower());
        }

        void OnAttributeValueChange(ChangeEvent<Color> evt)
        {
            var field = evt.target as ColorField;
            PostAttributeValueChange(field, "#" + ColorUtility.ToHtmlStringRGBA(evt.newValue));
        }

        void OnAttributeValueChange(ChangeEvent<Enum> evt)
        {
            var field = evt.target as EnumField;
            PostAttributeValueChange(field, evt.newValue.ToString());
        }

        void PostAttributeValueChange(BindableElement field, string value)
        {
            // Undo/Redo
            Undo.RegisterCompleteObjectUndo(m_Inspector.visualTreeAsset, BuilderConstants.ChangeAttributeValueUndoMessage);

            // Set value in asset.
            var vea = currentVisualElement.GetVisualElementAsset();
            vea.SetAttributeValue(field.bindingPath, value);

            // Mark field as overridden.
            var styleRow = field.GetProperty(BuilderConstants.InspectorLinkedStyleRowVEPropertyName) as BuilderStyleRow;
            styleRow.AddToClassList(BuilderConstants.InspectorLocalStyleOverrideClassName);

            var styleFields = styleRow.Query<BindableElement>().ToList();

            foreach (var styleField in styleFields)
            {
                styleField.RemoveFromClassList(BuilderConstants.InspectorLocalStyleResetClassName);
                if (field.bindingPath == styleField.bindingPath)
                {
                    styleField.AddToClassList(BuilderConstants.InspectorLocalStyleOverrideClassName);
                }
                else if (!string.IsNullOrEmpty(styleField.bindingPath) &&
                    field.bindingPath != styleField.bindingPath &&
                    !styleField.ClassListContains(BuilderConstants.InspectorLocalStyleOverrideClassName))
                {
                    styleField.AddToClassList(BuilderConstants.InspectorLocalStyleResetClassName);
                }
            }

            // Call Init();
            CallInitOnElement();

            // Notify of changes.
            m_Selection.NotifyOfHierarchyChange(m_Inspector);
        }

        void CallInitOnElement()
        {
            var fullTypeName = currentVisualElement.GetType().ToString();

            if (VisualElementFactoryRegistry.TryGetValue(fullTypeName, out var factoryList))
            {
                var traits = factoryList[0].GetTraits();

                if (traits == null)
                    return;

                var context = new CreationContext();
                var vea = currentVisualElement.GetVisualElementAsset();

                try
                {
                    traits.Init(currentVisualElement, vea, context);
                }
                catch
                {
                    // HACK: This throws in 2019.3.0a4 because usageHints property throws when set after the element has already been added to the panel.
                }
            }
        }
    }
}

Example usage:

    public BindableElement BuildField(string fieldLabel, EventCallback<ChangeEvent<string>> bind) {
        var values = new List<string>();

        Type[] types = Utils.GetAllClassesExtending<Manipulator>("Unity", "Microsoft", "System", "Mono", "UMotion");
        Array.Sort(types, (T1, T2) => T2.FullName.CompareTo(T1.FullName));
        values = types.Cast(T => T.FullName).ToList();

        PopupField<string> textField = new PopupField<string>(fieldLabel, values, values[0]);

        textField.RegisterValueChangedCallback(bind);
        return textField;
    }

Update:

        void OnAttributeValueChange(ChangeEvent<string> evt)
        {
            var field = evt.target as TextField;
            PostAttributeValueChange(field, evt.newValue);
        }

reliance on “as TextField” causes a null pointer. I will keep messing with this tomorrow. So it’s not a complete solution as of now

Nice work! Maybe you should apply to one of the open positions on the UI Builder team. :slight_smile:

This is definitely not a 2022+ feature. It’s a fairly low hanging fruit so once our main goals are done for the 2021.1 release, this should fit fairly easily.

That said, I’m trying to delay the introduction of a public API for the UI Builder as long as possible. Public APIs tend to be sticky and static for a long time. My thinking on this was feature was to use the existing custom PropertyDrawer framework for extending the UI Builder Inspector for custom UXML attributes. It would work exactly like normal PropertyFields where you just define a new implementation for your specific attribute type. Just needs to bubble up the backlog at this point.

You mean this one? https://careers.unity.com/position/developer-ui-tools-d-veloppeur/2150545
Although I have no near future plans on applying for it, I will keep it in mind.

For now I am much more comfortable simply making what I need and submitting as a contribution, that way I can hopefully get what I need earlier and if it helps the community, all the better.

As I have been looking around, a PropertyDrawer alone is non sufficient, there must be some place where the property value is converted to and from string, since the value needs to be written in UXML and read from it back. It could be generalized for the currently implemented value type fields, but serializing an Type or asset reference can’t be automated and need a custom serialize/deserialize function. For instance, changing
var field = evt.target as TextField -> BindableElement; did fix the null pointer, but the UXML is getting the Type name rather than the AssemblyQualifiedName (which I assume is the only sure fire way to serialize a Type back and forth to string), also, the field itself might say “MyCustomManipulator” but the UxmlClassAttributeDescription value is probably null, as it have not a single place where the string was converted back to Type.

Whenever this actually gets integrated or not is but a convenience for me, but this time a working solution:
OBS: RefreshAttributeField was not used at all, going against the norm.
OBS: There are some allocations I wish I didn’t need to, but I couldn’t figure out how to avoid it while keeping the methods that need implementing minimal
Modified BuilderInspectorAttributes.cs

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
namespace Unity.UI.Builder {
    public abstract class CustomUxmlAttributeDescription<T> : TypedUxmlAttributeDescription<T>, CustomUxmlAttributeDescriptionNonGenericMethods {
        //
        private class BindableElementContainer {
            public BindableElement element;
        }
        public BindableElement BuildField(IUxmlAttributes bag, CreationContext cc, string fieldLabel, EventCallback<BindableElement, string> bind) {
            T CurrentValue = GetValueFromBag(bag, cc);
            BindableElementContainer container = new BindableElementContainer();
            EventCallback<T> callback = (S) => bind(container.element, SerializeToString(S));
            BindableElement element = container.element = BuildField(CurrentValue, fieldLabel, callback);
            return element;
        }
        public abstract BindableElement BuildField(T CurrentValue, string fieldLabel, EventCallback<T> OnChangeCallback);
        public abstract string SerializeToString(T value);
    }
    internal interface CustomUxmlAttributeDescriptionNonGenericMethods {
        BindableElement BuildField(IUxmlAttributes bag, CreationContext cc, string fieldLabel, EventCallback<BindableElement, string> bind);
    }
    internal class BuilderInspectorAttributes : IBuilderInspectorSection {
        BuilderInspector m_Inspector;
        BuilderSelection m_Selection;
        PersistedFoldout m_AttributesSection;

        VisualElement currentVisualElement => m_Inspector.currentVisualElement;

        public VisualElement root => m_AttributesSection;

        public BuilderInspectorAttributes(BuilderInspector inspector) {
            m_Inspector = inspector;
            m_Selection = inspector.selection;

            m_AttributesSection = m_Inspector.Q<PersistedFoldout>("inspector-attributes-foldout");
        }

        public void Refresh() {
            m_AttributesSection.Clear();

            if (currentVisualElement == null)
                return;

            m_AttributesSection.text = currentVisualElement.typeName;

            if (m_Selection.selectionType != BuilderSelectionType.Element &&
                m_Selection.selectionType != BuilderSelectionType.ElementInTemplateInstance)
                return;

            GenerateAttributeFields();

            // Forward focus to the panel header.
            m_AttributesSection
                .Query()
                .Where(e => e.focusable)
                .ForEach((e) => m_Inspector.AddFocusable(e));
        }

        public void Enable() {
            m_AttributesSection.contentContainer.SetEnabled(true);
        }

        public void Disable() {
            m_AttributesSection.contentContainer.SetEnabled(false);
        }

        void GenerateAttributeFields() {
            var attributeList = currentVisualElement.GetAttributeDescriptions();

            foreach (var attribute in attributeList) {
                if (attribute == null || attribute.name == null)
                    continue;

                var styleRow = CreateAttributeRow(attribute);
                m_AttributesSection.Add(styleRow);
            }
        }

        BuilderStyleRow CreateAttributeRow(UxmlAttributeDescription attribute) {
            var attributeType = attribute.GetType();

            // Generate field label.
            var fieldLabel = BuilderNameUtilities.ConvertDashToHuman(attribute.name);
            BindableElement fieldElement;
            if (attribute is UxmlStringAttributeDescription) {
                var uiField = new TextField(fieldLabel);
                if (attribute.name.Equals("name") || attribute.name.Equals("view-data-key"))
                    uiField.RegisterValueChangedCallback(e => {
                        OnValidatedAttributeValueChange(e, BuilderNameUtilities.AttributeRegex, BuilderConstants.AttributeValidationSpacialCharacters);
                    });
                else if (attribute.name.Equals("binding-path"))
                    uiField.RegisterValueChangedCallback(e => {
                        OnValidatedAttributeValueChange(e, BuilderNameUtilities.BindingPathAttributeRegex, BuilderConstants.BindingPathAttributeValidationSpacialCharacters);
                    });
                else
                    uiField.RegisterValueChangedCallback(OnAttributeValueChange);

                if (attribute.name.Equals("text")) {
                    uiField.multiline = true;
                    uiField.AddToClassList(BuilderConstants.InspectorMultiLineTextFieldClassName);
                }

                fieldElement = uiField;
            } else if (attribute is UxmlFloatAttributeDescription) {
                var uiField = new FloatField(fieldLabel);
                uiField.RegisterValueChangedCallback(OnAttributeValueChange);
                fieldElement = uiField;
            } else if (attribute is UxmlDoubleAttributeDescription) {
                var uiField = new DoubleField(fieldLabel);
                uiField.RegisterValueChangedCallback(OnAttributeValueChange);
                fieldElement = uiField;
            } else if (attribute is UxmlIntAttributeDescription) {
                var uiField = new IntegerField(fieldLabel);
                uiField.RegisterValueChangedCallback(OnAttributeValueChange);
                fieldElement = uiField;
            } else if (attribute is UxmlLongAttributeDescription) {
                var uiField = new LongField(fieldLabel);
                uiField.RegisterValueChangedCallback(OnAttributeValueChange);
                fieldElement = uiField;
            } else if (attribute is UxmlBoolAttributeDescription) {
                var uiField = new Toggle(fieldLabel);
                uiField.RegisterValueChangedCallback(OnAttributeValueChange);
                fieldElement = uiField;
            } else if (attribute is UxmlColorAttributeDescription) {
                var uiField = new ColorField(fieldLabel);
                uiField.RegisterValueChangedCallback(OnAttributeValueChange);
                fieldElement = uiField;
            } else if (attributeType.IsGenericType &&
                  !attributeType.GetGenericArguments()[0].IsEnum &&
                  attributeType.GetGenericArguments()[0] is Type) {
                var uiField = new TextField(fieldLabel);
                uiField.isDelayed = true;
                uiField.RegisterValueChangedCallback(e => {
                    OnValidatedTypeAttributeChange(e, attributeType.GetGenericArguments()[0]);
                });
                fieldElement = uiField;
            } else if (attributeType.IsGenericType && attributeType.GetGenericArguments()[0].IsEnum) {
                var propInfo = attributeType.GetProperty("defaultValue");
                var enumValue = propInfo.GetValue(attribute, null) as Enum;

                // Create and initialize the EnumField.
                var uiField = new EnumField(fieldLabel);
                uiField.Init(enumValue);

                uiField.RegisterValueChangedCallback(OnAttributeValueChange);
                fieldElement = uiField;
            } else if (typeof(CustomUxmlAttributeDescriptionNonGenericMethods).IsAssignableFrom(attribute.GetType())) {

                fieldElement = ((CustomUxmlAttributeDescriptionNonGenericMethods)attribute).BuildField(
                    currentVisualElement.GetVisualElementAsset(), new CreationContext(), fieldLabel, OnAttributeValueChange);
            } else {
                var uiField = new TextField(fieldLabel);
                uiField.RegisterValueChangedCallback(OnAttributeValueChange);
                fieldElement = uiField;
            }

            // Create row.
            var styleRow = new BuilderStyleRow();
            styleRow.Add(fieldElement);

            // Link the field.
            fieldElement.SetProperty(BuilderConstants.InspectorLinkedStyleRowVEPropertyName, styleRow);
            fieldElement.SetProperty(BuilderConstants.InspectorLinkedAttributeDescriptionVEPropertyName, attribute);

            // Set initial value.
            RefreshAttributeField(fieldElement);

            // Setup field binding path.
            fieldElement.bindingPath = attribute.name;

            // Tooltip.
            var label = fieldElement.Q<Label>();
            if (label != null)
                label.tooltip = attribute.name;
            else
                fieldElement.tooltip = attribute.name;

            // Context menu.
            fieldElement.AddManipulator(new ContextualMenuManipulator(BuildAttributeFieldContextualMenu));

            return styleRow;
        }

        object GetCustomValueAbstract(string attributeName) {
            if (currentVisualElement is ScrollView) {
                var scrollView = currentVisualElement as ScrollView;
                if (attributeName == "mode") {
                    if (scrollView.ClassListContains(ScrollView.verticalVariantUssClassName))
                        return ScrollViewMode.Vertical;
                    else if (scrollView.ClassListContains(ScrollView.horizontalVariantUssClassName))
                        return ScrollViewMode.Horizontal;
                    else if (scrollView.ClassListContains(ScrollView.verticalHorizontalVariantUssClassName))
                        return ScrollViewMode.VerticalAndHorizontal;
                } else if (attributeName == "show-horizontal-scroller") {
                    return scrollView.showHorizontal;
                } else if (attributeName == "show-vertical-scroller") {
                    return scrollView.showVertical;
                }
            } else if (currentVisualElement is ObjectField objectField) {
                if (attributeName == "type") {
                    return objectField.objectType;
                }
            }

            return null;
        }

        void RefreshAttributeField(BindableElement fieldElement) {
            var styleRow = fieldElement.GetProperty(BuilderConstants.InspectorLinkedStyleRowVEPropertyName) as VisualElement;
            var attribute = fieldElement.GetProperty(BuilderConstants.InspectorLinkedAttributeDescriptionVEPropertyName) as UxmlAttributeDescription;

            var veType = currentVisualElement.GetType();
            var camel = BuilderNameUtilities.ConvertDashToCamel(attribute.name);

            var fieldInfo = veType.GetProperty(camel, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.IgnoreCase);

            object veValueAbstract = null;
            if (fieldInfo == null) {
                veValueAbstract = GetCustomValueAbstract(attribute.name);
            } else {
                veValueAbstract = fieldInfo.GetValue(currentVisualElement);
            }
            if (veValueAbstract == null)
                return;

            var attributeType = attribute.GetType();
            var vea = currentVisualElement.GetVisualElementAsset();

            if (attribute is UxmlStringAttributeDescription && fieldElement is TextField) {
                (fieldElement as TextField).SetValueWithoutNotify(GetAttributeStringValue(veValueAbstract));
            } else if (attribute is UxmlFloatAttributeDescription && fieldElement is FloatField) {
                (fieldElement as FloatField).SetValueWithoutNotify((float)veValueAbstract);
            } else if (attribute is UxmlDoubleAttributeDescription && fieldElement is DoubleField) {
                (fieldElement as DoubleField).SetValueWithoutNotify((double)veValueAbstract);
            } else if (attribute is UxmlIntAttributeDescription && fieldElement is IntegerField) {
                if (veValueAbstract is int)
                    (fieldElement as IntegerField).SetValueWithoutNotify((int)veValueAbstract);
                else if (veValueAbstract is float)
                    (fieldElement as IntegerField).SetValueWithoutNotify(Convert.ToInt32(veValueAbstract));
            } else if (attribute is UxmlLongAttributeDescription && fieldElement is LongField) {
                (fieldElement as LongField).SetValueWithoutNotify((long)veValueAbstract);
            } else if (attribute is UxmlBoolAttributeDescription && fieldElement is Toggle) {
                (fieldElement as Toggle).SetValueWithoutNotify((bool)veValueAbstract);
            } else if (attribute is UxmlColorAttributeDescription && fieldElement is ColorField) {
                (fieldElement as ColorField).SetValueWithoutNotify((Color)veValueAbstract);
            } else if (attributeType.IsGenericType &&
                  !attributeType.GetGenericArguments()[0].IsEnum &&
                  attributeType.GetGenericArguments()[0] is Type &&
                  fieldElement is TextField textField &&
                  veValueAbstract is Type veTypeValue) {
                var fullTypeName = veTypeValue.AssemblyQualifiedName;
                var fullTypeNameSplit = fullTypeName.Split(',');
                textField.SetValueWithoutNotify($"{fullTypeNameSplit[0]},{fullTypeNameSplit[1]}");
            } else if (attributeType.IsGenericType &&
                  attributeType.GetGenericArguments()[0].IsEnum &&
                  fieldElement is EnumField) {
                var propInfo = attributeType.GetProperty("defaultValue");
                var enumValue = propInfo.GetValue(attribute, null) as Enum;

                // Create and initialize the EnumField.
                var uiField = fieldElement as EnumField;

                // Set the value from the UXML attribute.
                var enumAttributeValueStr = vea?.GetAttributeValue(attribute.name);
                if (!string.IsNullOrEmpty(enumAttributeValueStr)) {
                    var parsedValue = Enum.Parse(enumValue.GetType(), enumAttributeValueStr, true) as Enum;
                    uiField.SetValueWithoutNotify(parsedValue);
                }
            } else if (fieldElement is TextField) {
                (fieldElement as TextField).SetValueWithoutNotify(veValueAbstract.ToString());
            }

            styleRow.RemoveFromClassList(BuilderConstants.InspectorLocalStyleOverrideClassName);
            if (IsAttributeOverriden(attribute))
                styleRow.AddToClassList(BuilderConstants.InspectorLocalStyleOverrideClassName);
        }

        string GetAttributeStringValue(object attributeValue) {
            string value;
            if (attributeValue is Enum @enum)
                value = @enum.ToString();
            else if (attributeValue is IList<string> list) {
                value = string.Join(",", list);
            } else {
                value = attributeValue.ToString();
            }

            return value;
        }

        bool IsAttributeOverriden(UxmlAttributeDescription attribute) {
            var vea = currentVisualElement.GetVisualElementAsset();
            if (vea != null && attribute.name == "picking-mode") {
                var veaAttributeValue = vea.GetAttributeValue(attribute.name);
                if (veaAttributeValue != null &&
                    veaAttributeValue.ToLower() != attribute.defaultValueAsString.ToLower())
                    return true;
            } else if (attribute.name == "name") {
                if (!string.IsNullOrEmpty(currentVisualElement.name))
                    return true;
            } else if (vea != null && vea.HasAttribute(attribute.name))
                return true;

            return false;
        }

        void ResetAttributeFieldToDefault(BindableElement fieldElement) {
            var styleRow = fieldElement.GetProperty(BuilderConstants.InspectorLinkedStyleRowVEPropertyName) as VisualElement;
            var attribute = fieldElement.GetProperty(BuilderConstants.InspectorLinkedAttributeDescriptionVEPropertyName) as UxmlAttributeDescription;

            var attributeType = attribute.GetType();
            var vea = currentVisualElement.GetVisualElementAsset();

            if (attribute is UxmlStringAttributeDescription && fieldElement is TextField) {
                var a = attribute as UxmlStringAttributeDescription;
                var f = fieldElement as TextField;
                f.SetValueWithoutNotify(a.defaultValue);
            } else if (attribute is UxmlFloatAttributeDescription && fieldElement is FloatField) {
                var a = attribute as UxmlFloatAttributeDescription;
                var f = fieldElement as FloatField;
                f.SetValueWithoutNotify(a.defaultValue);
            } else if (attribute is UxmlDoubleAttributeDescription && fieldElement is DoubleField) {
                var a = attribute as UxmlDoubleAttributeDescription;
                var f = fieldElement as DoubleField;
                f.SetValueWithoutNotify(a.defaultValue);
            } else if (attribute is UxmlIntAttributeDescription && fieldElement is IntegerField) {
                var a = attribute as UxmlIntAttributeDescription;
                var f = fieldElement as IntegerField;
                f.SetValueWithoutNotify(a.defaultValue);
            } else if (attribute is UxmlLongAttributeDescription && fieldElement is LongField) {
                var a = attribute as UxmlLongAttributeDescription;
                var f = fieldElement as LongField;
                f.SetValueWithoutNotify(a.defaultValue);
            } else if (attribute is UxmlBoolAttributeDescription && fieldElement is Toggle) {
                var a = attribute as UxmlBoolAttributeDescription;
                var f = fieldElement as Toggle;
                f.SetValueWithoutNotify(a.defaultValue);
            } else if (attribute is UxmlColorAttributeDescription && fieldElement is ColorField) {
                var a = attribute as UxmlColorAttributeDescription;
                var f = fieldElement as ColorField;
                f.SetValueWithoutNotify(a.defaultValue);
            } else if (attributeType.IsGenericType &&
                  !attributeType.GetGenericArguments()[0].IsEnum &&
                  attributeType.GetGenericArguments()[0] is Type &&
                  fieldElement is TextField) {
                var a = attribute as TypedUxmlAttributeDescription<Type>;
                var f = fieldElement as TextField;
                if (a.defaultValue == null)
                    f.SetValueWithoutNotify(string.Empty);
                else
                    f.SetValueWithoutNotify(a.defaultValue.ToString());
            } else if (attributeType.IsGenericType &&
                  attributeType.GetGenericArguments()[0].IsEnum &&
                  fieldElement is EnumField) {
                var propInfo = attributeType.GetProperty("defaultValue");
                var enumValue = propInfo.GetValue(attribute, null) as Enum;

                var uiField = fieldElement as EnumField;
                uiField.SetValueWithoutNotify(enumValue);
            } else if (fieldElement is TextField) {
                (fieldElement as TextField).SetValueWithoutNotify(string.Empty);
            }

            // Clear override.
            styleRow.RemoveFromClassList(BuilderConstants.InspectorLocalStyleOverrideClassName);
            var styleFields = styleRow.Query<BindableElement>().ToList();
            foreach (var styleField in styleFields) {
                styleField.RemoveFromClassList(BuilderConstants.InspectorLocalStyleResetClassName);
                styleField.RemoveFromClassList(BuilderConstants.InspectorLocalStyleOverrideClassName);
            }
        }

        void BuildAttributeFieldContextualMenu(ContextualMenuPopulateEvent evt) {
            evt.menu.AppendAction(
                BuilderConstants.ContextMenuUnsetMessage,
                UnsetAttributeProperty,
                action => {
                    var fieldElement = action.userData as BindableElement;
                    if (fieldElement == null)
                        return DropdownMenuAction.Status.Disabled;

                    var attributeName = fieldElement.bindingPath;
                    var vea = currentVisualElement.GetVisualElementAsset();
                    return vea.HasAttribute(attributeName)
                        ? DropdownMenuAction.Status.Normal
                        : DropdownMenuAction.Status.Disabled;
                },
                evt.target);

            evt.menu.AppendAction(
                BuilderConstants.ContextMenuUnsetAllMessage,
                UnsetAllAttributes,
                action => {
                    var attributeList = currentVisualElement.GetAttributeDescriptions();
                    foreach (var attribute in attributeList) {
                        if (attribute?.name == null)
                            continue;

                        if (IsAttributeOverriden(attribute))
                            return DropdownMenuAction.Status.Normal;
                    }

                    return DropdownMenuAction.Status.Disabled;
                },
                evt.target);
        }

        void UnsetAllAttributes(DropdownMenuAction action) {
            var attributeList = currentVisualElement.GetAttributeDescriptions();

            // Undo/Redo
            Undo.RegisterCompleteObjectUndo(m_Inspector.visualTreeAsset, BuilderConstants.ChangeAttributeValueUndoMessage);

            foreach (var attribute in attributeList) {
                if (attribute?.name == null)
                    continue;

                // Unset value in asset.
                var vea = currentVisualElement.GetVisualElementAsset();
                vea.RemoveAttribute(attribute.name);
            }

            var fields = m_AttributesSection.Query<BindableElement>().Where(e => !string.IsNullOrEmpty(e.bindingPath)).ToList();
            foreach (var fieldElement in fields) {
                // Reset UI value.
                ResetAttributeFieldToDefault(fieldElement);
            }

            // Call Init();
            CallInitOnElement();

            // Notify of changes.
            m_Selection.NotifyOfHierarchyChange(m_Inspector);
        }

        void UnsetAttributeProperty(DropdownMenuAction action) {
            var fieldElement = action.userData as BindableElement;
            var attributeName = fieldElement.bindingPath;


            // Undo/Redo
            Undo.RegisterCompleteObjectUndo(m_Inspector.visualTreeAsset, BuilderConstants.ChangeAttributeValueUndoMessage);

            // Unset value in asset.
            var vea = currentVisualElement.GetVisualElementAsset();
            vea.RemoveAttribute(attributeName);

            // Reset UI value.
            ResetAttributeFieldToDefault(fieldElement);

            // Call Init();
            CallInitOnElement();

            // Notify of changes.
            m_Selection.NotifyOfHierarchyChange(m_Inspector);
        }

        void OnAttributeValueChange(ChangeEvent<string> evt) {
            var field = evt.target as BindableElement;
            PostAttributeValueChange(field, evt.newValue);
        }

        void OnAttributeValueChange(BindableElement field, string newValue) {
            PostAttributeValueChange(field, newValue);
        }

        void OnValidatedTypeAttributeChange(ChangeEvent<string> evt, Type desiredType) {
            var field = evt.target as TextField;
            var typeName = evt.newValue;
            var fullTypeName = typeName;
            if (!string.IsNullOrEmpty(typeName)) {
                var type = Type.GetType(fullTypeName, false);

                // Try some auto-fixes.
                if (type == null) {
                    fullTypeName = typeName + ", UnityEngine.CoreModule";
                    type = Type.GetType(fullTypeName, false);
                }
                if (type == null) {
                    fullTypeName = typeName + ", UnityEditor";
                    type = Type.GetType(fullTypeName, false);
                }
                if (type == null && typeName.Contains(".")) {
                    var split = typeName.Split('.');
                    fullTypeName = typeName + $", {split[0]}.{split[1]}Module";
                    type = Type.GetType(fullTypeName, false);
                }

                if (type == null) {
                    Builder.ShowWarning(string.Format(BuilderConstants.TypeAttributeInvalidTypeMessage, field.label));
                    evt.StopPropagation();
                    return;
                } else if (!desiredType.IsAssignableFrom(type)) {
                    Builder.ShowWarning(string.Format(BuilderConstants.TypeAttributeMustDeriveFromMessage, field.label, desiredType.FullName));
                    evt.StopPropagation();
                    return;
                }
            }

            field.SetValueWithoutNotify(fullTypeName);
            PostAttributeValueChange(field, fullTypeName);
        }

        void OnValidatedAttributeValueChange(ChangeEvent<string> evt, Regex regex, string message) {
            var field = evt.target as TextField;
            if (!string.IsNullOrEmpty(evt.newValue) && !regex.IsMatch(evt.newValue)) {
                Builder.ShowWarning(string.Format(message, field.label));
                field.SetValueWithoutNotify(evt.previousValue);
                evt.StopPropagation();
                return;
            }

            OnAttributeValueChange(evt);
        }

        void OnAttributeValueChange(ChangeEvent<float> evt) {
            var field = evt.target as FloatField;
            PostAttributeValueChange(field, evt.newValue.ToString());
        }

        void OnAttributeValueChange(ChangeEvent<double> evt) {
            var field = evt.target as DoubleField;
            PostAttributeValueChange(field, evt.newValue.ToString());
        }

        void OnAttributeValueChange(ChangeEvent<int> evt) {
            var field = evt.target as IntegerField;
            PostAttributeValueChange(field, evt.newValue.ToString());
        }

        void OnAttributeValueChange(ChangeEvent<long> evt) {
            var field = evt.target as LongField;
            PostAttributeValueChange(field, evt.newValue.ToString());
        }

        void OnAttributeValueChange(ChangeEvent<bool> evt) {
            var field = evt.target as Toggle;
            PostAttributeValueChange(field, evt.newValue.ToString().ToLower());
        }

        void OnAttributeValueChange(ChangeEvent<Color> evt) {
            var field = evt.target as ColorField;
            PostAttributeValueChange(field, "#" + ColorUtility.ToHtmlStringRGBA(evt.newValue));
        }

        void OnAttributeValueChange(ChangeEvent<Enum> evt) {
            var field = evt.target as BindableElement;
            PostAttributeValueChange(field, evt.newValue.ToString());
        }

        void PostAttributeValueChange(BindableElement field, string value) {
            // Undo/Redo
            Undo.RegisterCompleteObjectUndo(m_Inspector.visualTreeAsset, BuilderConstants.ChangeAttributeValueUndoMessage);

            // Set value in asset.
            var vea = currentVisualElement.GetVisualElementAsset();
            vea.SetAttributeValue(field.bindingPath, value);

            // Mark field as overridden.
            var styleRow = field.GetProperty(BuilderConstants.InspectorLinkedStyleRowVEPropertyName) as BuilderStyleRow;
            styleRow.AddToClassList(BuilderConstants.InspectorLocalStyleOverrideClassName);

            var styleFields = styleRow.Query<BindableElement>().ToList();

            foreach (var styleField in styleFields) {
                styleField.RemoveFromClassList(BuilderConstants.InspectorLocalStyleResetClassName);
                if (field.bindingPath == styleField.bindingPath) {
                    styleField.AddToClassList(BuilderConstants.InspectorLocalStyleOverrideClassName);
                } else if (!string.IsNullOrEmpty(styleField.bindingPath) &&
                      field.bindingPath != styleField.bindingPath &&
                      !styleField.ClassListContains(BuilderConstants.InspectorLocalStyleOverrideClassName)) {
                    styleField.AddToClassList(BuilderConstants.InspectorLocalStyleResetClassName);
                }
            }

            // Call Init();
            CallInitOnElement();

            // Notify of changes.
            m_Selection.NotifyOfHierarchyChange(m_Inspector);
        }

        void CallInitOnElement() {
            var fullTypeName = currentVisualElement.GetType().ToString();

            if (VisualElementFactoryRegistry.TryGetValue(fullTypeName, out var factoryList)) {
                var traits = factoryList[0].GetTraits();

                if (traits == null)
                    return;

                var context = new CreationContext();
                var vea = currentVisualElement.GetVisualElementAsset();

                try {
                    traits.Init(currentVisualElement, vea, context);
                } catch {
                    // HACK: This throws in 2019.3.0a4 because usageHints property throws when set after the element has already been added to the panel.
                }
            }
        }
    }
}

Example CustomUxmlAttributeDescription (UxmlTypeAttributeDescription.cs)

using System;
using System.Linq;
using Unity.UI.Builder;
using UnityEditor.UIElements;
using UnityEngine.UIElements;

public class UxmlTypeAttributeDescription : CustomUxmlAttributeDescription<Type> {
    public UxmlTypeAttributeDescription() {
        type = "Type";
        typeNamespace = xmlSchemaNamespace;
        defaultValue = typeof(Type);
    }

    public override string defaultValueAsString { get { return defaultValue.AssemblyQualifiedName; } }

    public override BindableElement BuildField(Type CurrentValue, string fieldLabel, EventCallback<Type> OnChangeCallback) {
        Type[] types = Utils.GetAllClassesExtending<Manipulator>("Unity", "Microsoft", "System", "Mono", "UMotion");
        Array.Sort(types, (T1, T2) => T2.FullName.CompareTo(T1.FullName));
        Func<Type, string> func = (T) => (T.ToString());
        PopupField<Type> textField = new PopupField<Type>(fieldLabel, types.ToList(), types[0], func, func);
        if (types.Contains(CurrentValue)) {
            textField.value = CurrentValue;
        }
        textField.RegisterValueChangedCallback((T) => OnChangeCallback(T.newValue));
        return textField;
    }

    public override string SerializeToString(Type value) {
        return value.AssemblyQualifiedName;
    }

    public override Type GetValueFromBag(IUxmlAttributes bag, CreationContext cc) {
        return GetValueFromBag(bag, cc, (s, t) => {
            Type tp = Type.GetType(s);
            if (tp != null) {
                return tp;
            }
            return t;
        }, defaultValue);
    }

    public bool TryGetValueFromBag(IUxmlAttributes bag, CreationContext cc, ref Type value) {
        return TryGetValueFromBag(bag, cc, (s, t) => {
            Type tp = Type.GetType(s);
            if (tp != null) {
                return tp;
            }
            return t;
        }, defaultValue, ref value);
    }
}

Custom Descriptor usage (AddManipulator.cs)

using System;
using System.Collections.Generic;
using UnityEngine.UIElements;

class AddManipulator : VisualElement {
    public Type ManipulatorType { get; set; }

    public new class UxmlFactory : UxmlFactory<AddManipulator, UxmlTraits> { }

    public new class UxmlTraits : VisualElement.UxmlTraits {
        public UxmlTypeAttributeDescription type = new UxmlTypeAttributeDescription { name = "Manipulator-Type", defaultValue = typeof(Type) };
        public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription {
            get { yield break; }
        }

        public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc) {
            base.Init(ve, bag, cc);
            AddManipulator addm = ve as AddManipulator;
            addm.ManipulatorType = type.GetValueFromBag(bag, cc);
        }
    }

}

Next I will make a version where you get the whole UIElement parent as a way to enable a custom field take into consideration other fields to decide it’s value. So I can make a sub inspector for the manipulator selected.

:slight_smile:

I haven’t spent too much time looking into the specifics but I agree it won’t be trivial. The initial idea was actually to just use the Editor class and CreateInspectorGUI() for your entire custom VisualElement, not per-field. You would then do all the string conversions yourself for any custom UXML attributes in your Editor class impl. There’s also the fact that VisualElements are not GameObject (or ScriptableObjects of any kind) which means we’d need to create a fake one for the Editor to attach itself to. So for now, your solution seems more reasonable for your needs.

Thanks for posting your solution. I’m sure it will be useful for other users in a similar bind. Not sure how much, if any, we’ll use in the final implementation in the Builder but it serves as a good bump in our relative priorities.

1 Like

Is there still no integrated way of doing this?

2 Likes

we are waiting for this feature, any good news for this?

Yes this is a problem, making my transition from uGUi to Ui toolkit impossible. I need to add serializeField or attributes to my VisualElements accessible in the inspector.

See

We are well aware of this problem, but it requires major changes in the UXML serialization and UI Builder implementation. The good news is that we are actually working on these changes, but it won’t reach a Unity version before a while (the earliest estimate would be 2023 LTS).

Good to hear, thank you!

Hello. Have any news for this feature? Extentions for UIBuilder like Editor Extentions it’s very important feature for using it in production.

I really want something similar to PropertyDrawer and EditorWindow.ShowAsDropDown.
UxmlAssetAttributeDescription already there.

All we can say for now is that the team is actively working on exactly this right now. Aiming for 2023 LTS, but that’s not a guarantee at this point.

Any news on this feature? We also would really appreciate that.

2 Likes

We ended up releasing a feature called Uxml Serialization, which allows users to get rid of the factory and traits classes for their custom element. Using this, you could take an element that looks like this:

public class MyElement : VisualElement
{
    new class UXMLFactory : UXMLFactory<MyElement, UXMLTraits>
    {
    }

    new class UXMLTraits : VisualElement.UXMLTraits
    {
        private UXMLFloatAttributeDescription m_Value = new UXMLFloatAttributeDescription {name = "value"};

        public override void Init(VisualElement ve, IUXMLAttributes bag, CreationContext cc)
        {
            base.Init(ve, bag, cc);
            var baseField = (MyElement) ve;
            baseField.value = m_Value.GetValueFromBag(bag, cc);
        }
    }

    public float value { get; set; }
}

And transform it into this:

[UXMLElement]
public partial class MyElement : VisualElement
{
    [UXMLAttribute]
    public float value { get; set; }
}

The main advantages of this new approach are:

  • Much reduced boilerplate code required in order to use an element in Uxml.
  • Instead of parsing the text of the attribute at runtime to convert it into the correct type, we can do this at import time.
  • To display these fields in the UI Builder inspector, we are now using a PropertyField, meaning that you can define property drawers for the fields of your element.

We’ll make an announcement about this soon to provide more details.
Hope this helps!

8 Likes

Nice, but how does this handle custom classes? Can we then somehow create an editor for it?