Drawing a Field Using Multiple Property Drawers

Hello,

For some reason lots of people favor creating custom editors over property drawers, however these just don't scale, creating lots of custom drawers in a large project is a time sink. Luckily this is where property drawers come in, tagging a field with an attribute (such as [range(..)] ), allows you to draw that field in a custom manner. without having to re-create a custom editor.

The Problem
You can only have one property drawer per variable. Annotating a variable with multiple property Drawers, results in a random property drawer, drawing that variable in the inspector. There are Decorator Drawers, of which you can have multiple per variable. However, with these you do not get access to the property itself, just the location on screen which means it has very limited use.

This means creating something like the following image, is more difficult than it needs to be, and ill show a better way to do this in this thread.
3120484--236131--upload_2017-6-24_14-38-28.png

The Solution
I have created a property drawer which allows multiple other attribute to alter how the variable is drawn. The one caveat is that these other attribute must inherit from my new attribute MultiPropertyAttribute
The Attribute

[AttributeUsage(AttributeTargets.Field)]
public abstract class MultiPropertyAttribute : PropertyAttribute
{
    public List<object> stored = new List<object>();
    public virtual GUIContent BuildLabel(GUIContent label)
    {
        return label;
    }
    public abstract void OnGUI(Rect position, SerializedProperty property, GUIContent label);

    internal virtual float? GetPropertyHeight( SerializedProperty property, GUIContent label)
    {
        return null;
    }
}

The Drawer which controls the other attributes drawing

   [CustomPropertyDrawer(typeof(MultiPropertyAttribute),true)]
public class MultiPropertyDrawer : PropertyDrawer
{
    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        MultiPropertyAttribute @Attribute = attribute as MultiPropertyAttribute;
        float height = base.GetPropertyHeight(property, label);
        foreach (object atr in @Attribute.stored)//Go through the attributes, and try to get an altered height, if no altered height return default height.
        {
            if (atr as MultiPropertyAttribute != null)
            {
                //build label here too?
                var tempheight = ((MultiPropertyAttribute)atr).GetPropertyHeight(property, label);
                if (tempheight.HasValue)
                {
                    height = tempheight.Value;
                    break;
                }
            }
        }
        return height;
    }
    // Draw the property inside the given rect
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        MultiPropertyAttribute @Attribute = attribute as MultiPropertyAttribute;
        // First get the attribute since it contains the range for the slider
        if (@Attribute.stored == null || @Attribute.stored.Count == 0)
        {
            @Attribute.stored = fieldInfo.GetCustomAttributes(typeof(MultiPropertyAttribute), false).OrderBy(s => ((PropertyAttribute)s).order).ToList() ;
        }
        var OrigColor = GUI.color;
        var Label = label;
        foreach (object atr in @Attribute.stored)
        {
            if (atr as MultiPropertyAttribute != null)
            {
                Label = ((MultiPropertyAttribute)atr).BuildLabel(Label);
                ((MultiPropertyAttribute)atr).OnGUI(position, property, Label);
            }
        }
        GUI.color = OrigColor;
    }
}

These two classes are the base of being able to have multiple attributes affect how the variable is drawn.

If for instance, we want to color a field and have this field be a range field we need to implement the two attributes, Color and NewRange

public class ColorAttribute : MultiPropertyAttribute
{
    Color Color;
    public ColorAttribute(float R, float G, float B)
    {
        Color = new Color(R, G, B);
    }
    // Draw the property inside the given rect
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        GUI.color = Color;
    }
}
public class NewRangeAttribute : MultiPropertyAttribute
{
    float min;
    float max;
    public NewRangeAttribute(float min, float max)
    {
        this.min = min;
        this.max = max;
    }
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        if (property.propertyType == SerializedPropertyType.Float)
            EditorGUI.Slider(position, property, min, max, label);
        else
            EditorGUI.LabelField(position, label.text, "Use Range with float or int.");
    }
}

Note: How these classes don't have a drawer class, instead these attributes hold the ONGUI inside them.

So, How do you declare a variable which we want to color red, and be a range slider?

    [Color(1, 0, 0, order = 0)]
    [NewRange(0, 2.0f,order = 1)]
    public float ColorRange = 0.1f;

Note: the order = sent in controls which order the attributes will be drawn in


And now we have two attributes drawing one variable. Something unity does not stock stupport.

You may notice that I have a LabelBuilder function. This is so you can alter how the label looks. For example, you may want an icon in your variables label, the following attribute shows you how this is achieved.

public class Prefab : MultiPropertyAttribute
{
    GUIContent Icon;
    public Prefab()
    {
        Icon = EditorGUIUtility.IconContent("Prefab Icon");
    }
    public override GUIContent BuildLabel(GUIContent label)
    {
            Icon.text = label.text;
      return Icon;
    }

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
    }
}

And that can be declared like:

    [Prefab]
    [Color(0,0,1)]
    [NewRange(0,1,order = 1)]
    public float PrefabColorRange = 0.1f;

Note:prefab and color don't have an order, by default they will be order = 0, however it doesn't matter which way around these two would get applied
3120484--236128--upload_2017-6-24_14-23-42.png
Here we see the prefab icon has been prefixed to the label, it is color blue and it is a ranged slider.

Lastly, I support Changing the height of the variable in the inspector. The example for this is a foldout, where the height changes depending on if it is collapsed or open.

(quick code, find property will not always work)

public class FoldOutAttribute : MultiPropertyAttribute
{
    string[] VarNames;
    bool Foldout = false;
    GUIContent Title;
    public FoldOutAttribute(string title,params string[] varNames)
    {
        VarNames = new string[varNames.Length];
        VarNames = varNames;
        Title = new GUIContent(title);
    }
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        position.height = EditorGUIUtility.singleLineHeight;
        Foldout = EditorGUI.Foldout(position, Foldout, Title);
        if (Foldout)
        {
            ++EditorGUI.indentLevel;
            Rect newpos = position;
            newpos.y += EditorGUIUtility.singleLineHeight + 2;
            EditorGUI.PropertyField(newpos, property);
            for (int i = 0; i < VarNames.Length; i++)
            {
               var a =  property.serializedObject.FindProperty(VarNames[i]);
                newpos.y += EditorGUIUtility.singleLineHeight +2;
                newpos.height = EditorGUIUtility.singleLineHeight;
                EditorGUI.PropertyField(newpos, a);  
            }
        }
    }

    internal override float GetPropertyHeight( SerializedProperty property, GUIContent label)
    {
        if (Foldout)
        {
            return (float)((EditorGUIUtility.singleLineHeight + 2) * (VarNames.Count() +2));
        }
        else return float.NegativeInfinity;
         ;
    }
}

Here I alter the drawer height, and what is being drawn when the foldout is open.

you can define your variable like this:

   [Color(0, 1, 0, order = 0)]
    [FoldOut("My Multi Property Drawers", "PrefabColorRange", "ColorRange", order = 1)]
    public float ColorFoldOut;

closed
3120484--236129--upload_2017-6-24_14-35-0.png
open
3120484--236131--upload_2017-6-24_14-38-28.png

I hope somebody will find this useful, it has changed the game of how I create properties now.

Thanks

14 Likes

Definitely super interesting and useful. I've shared this internally with our editor teams to hopefully spark a discussion of ways to improve property drawers going forward.

9 Likes


Oh boy, didn't respect to get an official response. Thanks for your words, I hope that I can contribute toUnitys future :).

I made some improvements to the above code, I'll update the above when I get home.
firstly

internal virtual float GetPropertyHeight( SerializedProperty property, GUIContent label){

is now

internal virtual float? GetPropertyHeight( SerializedProperty property, GUIContent label){
retun null;//the property value has not been changed
}

and I check if GetPropertyHeight for a property returns null, rather than float.infinity.

additionally, I think I am going to add a Validate method to the base class which would be used to validate the data within the property. e.g. I have a prefab only attribute, which requires the object field to be the parent prefab, I need to validate this data when it is input. I'll come up with a design for this, and update the post

There is a potential issue with two attributes drawing it twice.
i.e.
code results in this

EditorGUI.PropertyField(position, property);
EditorGUI.Toggle(Position, property);

gets called in one attribute, and then again in another. resulting in the field being drawn twice. Either I am going to pass if it is the
last attribute in the list (therefore I can handle the drawing differently). or most likely, Change my OnGUI to return a bool, that bool would be if it should continue drawing or not. again more code updates to come

2 Likes

I have updated the original post code to include the nullable float checks - as it is a much nicer way of doing it.

However I am a little torn between two options for ensuring the variable field cant get drawn twice.

The first way is to include a Last parameter to the OnGUI method in MultiPropertyAttribute class.

public abstract class MultiPropertyAttribute : PropertyAttribute
{
    public abstract void OnGUI(Rect position, SerializedProperty property, GUIContent label, bool Last);

The draw code now changes to this:

    [CustomPropertyDrawer(typeof(MultiPropertyAttribute),true)]
public class MultiPropertyDrawer : PropertyDrawer
{
    // Draw the property inside the given rect
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        MultiPropertyAttribute @Attribute = attribute as MultiPropertyAttribute;
        // First get the attribute since it contains the range for the slider
        if (@Attribute.stored == null || @Attribute.stored.Count == 0)
        {
            @Attribute.stored = fieldInfo.GetCustomAttributes(typeof(MultiPropertyAttribute), false).OrderBy(s => ((PropertyAttribute)s).order).ToList();
        }
        var OrigColor = GUI.color;
        var Label = label;
        for (int i = 0; i < @Attribute.stored.Count; i++)
        {
            var atr = @Attribute.stored[i];
            if (atr as MultiPropertyAttribute != null) // Redundant check because of arg 1 in GetCustomAttributes
            {
                ((MultiPropertyAttribute)atr).Validation(property);
                Label = ((MultiPropertyAttribute)atr).BuildLabel(Label);
                ((MultiPropertyAttribute)atr).OnGUI(position, property, Label, i == @Attribute.stored.Count - 1);
            }
        }

        GUI.color = OrigColor;
    }

Note: I'll provide code snippet tomorrow of why I think Validation(...) is a must have method. Potentially it should return a bool, whether it should continue drawing - See: Streamlined data entry (we use this all the time in work)

Previously, on the original posts example, the Color Attribute would not draw an attribute if it was the only attribute on the field. I,e,

    [Color(0,0,1)]
    public float Colorfloat = 0.1f;

With the above change, the I can change the OnGUI for the Color attribute to:

public class ColorAttribute : MultiPropertyAttribute
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label, bool Last)
    {
        GUI.color = Color;
        if (Last)
        {
            EditorGUI.PropertyField(position, property, label, true);
        }
    }

If the Color attribute is the last attribute to get applied, it will draw the property field. however, if it is not, then it will yield the drawing to the next attribute.
3122777--236427--upload_2017-6-26_20-37-35.png
ColorRange yielded its drawing to RangeSlider.

--

The second method would be to add method to the MultiPropertyAttribute class that would be designed for decoration altering of the field in editor. Change Color, disable editing, changing label etc.

The end result drawing code would look a little like this

    [CustomPropertyDrawer(typeof(MultiPropertyAttribute),true)]
public class MultiPropertyDrawer : PropertyDrawer
{
    // Draw the property inside the given rect
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        MultiPropertyAttribute @Attribute = attribute as MultiPropertyAttribute;
        // First get the attribute since it contains the range for the slider
        if (@Attribute.stored == null || @Attribute.stored.Count == 0)
        {
            @Attribute.stored = fieldInfo.GetCustomAttributes(typeof(MultiPropertyAttribute), false).OrderBy(s => ((PropertyAttribute)s).order).ToList();
        }
        var OrigColor = GUI.color;
        var Label = label;
        for (int i = 0; i < @Attribute.stored.Count; i++)
        {
            var atr = @Attribute.stored[i];
            if (atr as MultiPropertyAttribute != null) // Redundant check because of arg 1 in GetCustomAttributes
            {
                ((MultiPropertyAttribute)atr).Validation(property);
                Label = ((MultiPropertyAttribute)atr).BuildLabel(Label);
                ((MultiPropertyAttribute)atr).Decorate(position, property, Label);
            }
        }
        ((MultiPropertyAttribute)@Attribute.stored.Last()).OnGUI(position, property, Label);
        GUI.color = OrigColor;
    }

Note: the OnGUI no longer needs to know if its the last one to be applied.

The Color Attribute could now look a little something like this:

public class ColorAttribute : MultiPropertyAttribute
{
    public override void Decorate(Rect position, SerializedProperty property, GUIContent label, bool Last)
        {
        GUI.color = Color;
    }
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label, bool Last)
    {
            EditorGUI.PropertyField(position, property, label, true);
    }

This implementation is pretty close to how it is currently implemented into unity, where only one drawer can draw the field. But still gives the attribute access to the property.

It is crunch time at work, so not much time to implement this into a work environment, but i'll pop bits and pieces in and see what see what shakes loose

Hi, @BinaryCats

Thank you for your solution.
But I'm curious about building this Unity project.
Because we use Unity Editor scripts in Attribute class.
Could the project be built successfully?

And sorry for my bad English :(

The drawer's need to be in a (i.e.

[LIST=1]
[*][CustomPropertyDrawer(typeof(MultiPropertyAttribute),true)]
[*]public class MultiPropertyDrawer : PropertyDrawer

[/LIST]

needs to be wrapped in #if UNITY_EDITOR so

#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(MultiPropertyAttribute),true)]
public class MultiPropertyDrawer : PropertyDrawer
{
...
}
#endif
[LIST=1]

[/LIST]

The rest of the code should be fine

Wrap in #if UNITY_EDITOR
Got it! Thanks!

Hi, @BinaryCats

Could you tell us how does method Validation(SerializedProperty property) work, please?


Hi,
Validation(...) may not be essential for your property. This is where I do any validation for the field. I.e. I might clamp an int field, or I might do some checks to see if the object field is a parent prefab.

I hope that clears this up for you.

--edit
However I have been toying around with this idea, which I am quite liking

      for (int i = 0; i < @Attribute.stored.Count; i++)
        {
            var atr = @Attribute.stored[i];
            if (atr as MultiPropertyAttribute != null) // Redundant check because of arg 1 in GetCustomAttributes
            {
                if (((MultiPropertyAttribute)atr).Validation(property))
                {
                    Label = ((MultiPropertyAttribute)atr).BuildLabel(Label);
                    ((MultiPropertyAttribute)atr).Decorate(position, property, Label);
                }
                else
                {
                    GUI.color = OrigColor;
                    return;
                }
            }
        }

        ((MultiPropertyAttribute)@Attribute.stored.Last()).OnGUI(position, property, Label);

here, validation can determine if the field is drawn in editor, by returning true or false. This is useful if the field should only be drawn under certain circumstances.

1 Like

Hi there,

I found this thread while looking for a more flexible alternative to PropertyDrawers, props to BinaryCats for the idea. I'm using a similar approach for one of my assets and it works wonders. Thought I'd contribute back by sharing the modifications I've done to the original scheme.

[System.AttributeUsage(System.AttributeTargets.Field)]
    public abstract class MultiPropertyAttribute : PropertyAttribute
    {
        public IOrderedEnumerable<object> stored = null;

        public virtual void OnGUI(Rect position, SerializedProperty property, GUIContent label)
        {
            EditorGUI.PropertyField(position,property,label);
        }

        internal virtual void OnPreGUI(Rect position, SerializedProperty property){}
        internal virtual void OnPostGUI(Rect position, SerializedProperty property){}

        internal virtual bool IsVisible(SerializedProperty property){return true;}
        internal virtual float? GetPropertyHeight( SerializedProperty property, GUIContent label){return null;}
    }

    [CustomPropertyDrawer(typeof(MultiPropertyAttribute),true)]
    public class MultiPropertyDrawer : PropertyDrawer
    {
        private MultiPropertyAttribute RetrieveAttributes()
        {
            MultiPropertyAttribute mAttribute = attribute as MultiPropertyAttribute;

            // Get the attribute list, sorted by "order".
            if (mAttribute.stored == null)
            {
                mAttribute.stored = fieldInfo.GetCustomAttributes(typeof(MultiPropertyAttribute), false).OrderBy(s => ((PropertyAttribute)s).order);
            }

            return mAttribute;
        }

        public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
        {
            MultiPropertyAttribute mAttribute = RetrieveAttributes();

            // If the attribute is invisible, regain the standard vertical spacing.
            foreach (MultiPropertyAttribute attr in mAttribute.stored)
                if (!attr.IsVisible(property))
                    return -EditorGUIUtility.standardVerticalSpacing;

            // In case no attribute returns a modified height, return the property's default one:
            float height = base.GetPropertyHeight(property, label);

            // Check if any of the attributes wants to modify height:
            foreach (object atr in mAttribute.stored)
            {
                if (atr as MultiPropertyAttribute != null)
                {
                    var tempheight = ((MultiPropertyAttribute)atr).GetPropertyHeight(property, label);
                    if (tempheight.HasValue)
                    {
                        height = tempheight.Value;
                        break;
                    }
                }
            }
            return height;
        }

        public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
        {
            MultiPropertyAttribute mAttribute = RetrieveAttributes();

            // Calls to IsVisible. If it returns false for any attribute, the property will not be rendered.
            foreach (MultiPropertyAttribute attr in mAttribute.stored)
                if (!attr.IsVisible(property)) return;

            // Calls to OnPreRender before the last attribute draws the UI.
            foreach (MultiPropertyAttribute attr in mAttribute.stored)
                attr.OnPreGUI(position,property);

            // The last attribute is in charge of actually drawing something:
            ((MultiPropertyAttribute)mAttribute.stored.Last()).OnGUI(position,property,label);

            // Calls to OnPostRender after the last attribute draws the UI. These are called in reverse order.
            foreach (MultiPropertyAttribute attr in mAttribute.stored.Reverse())
                attr.OnPostGUI(position,property);
        }
    }

These are the most important differences:

  • OnGUI() is virtual instead of abstract, by default it renders a PropertyField. This allows you to have attributes that only decorate/modify the standard UI, without the need to draw the field explicitly yourself.
  • Instead of using Validate() and Decorate() methods, I've split the whole thing in three methods: OnPreGUI, OnGUI, and OnPostGUI.
  • OnPreGUI is called for all attributes in order, before the last one has OnGUI called to render the "main" UI. OnPostGUI is then called for all attributes in reverse order. More on the usefulness of this later.
  • There's an auxiliar method IsVisible() that takes care of skipping rendering altogether if any attribute wants the field to be invisible. It also takes care of returning the correct property height automatically in that case.

Using these methods allows you to do pretty cool stuff. For instance, you can write a "Indent" attribute like this:

[System.AttributeUsage(System.AttributeTargets.Field)]
    public class Indent : MultiPropertyAttribute
    {
        internal override void OnPreGUI(Rect position, SerializedProperty property)
        {
            EditorGUI.indentLevel++;
        }
        internal override void OnPostGUI(Rect position, SerializedProperty property)
        {
            EditorGUI.indentLevel--;
        }
    }

You could also modify the color of the GUI and revert it to the original one afterwards, or enable/disable the GUI, as the calling order of OnPreGUI and OnPostGUI act like a stack. They allow you to inject code before and after all following attributes.

Also creating a conditional visibility attribute is very easy. This will let you specify a bool variable or a bool-returning method in your class that control whether or not the field is rendered in the inspector:

[System.AttributeUsage(System.AttributeTargets.Field)]
    public class VisibleIf : MultiPropertyAttribute
    {
        public string MethodName { get; private set; }
        public bool Negate {get; private set;}

        private MethodInfo eventMethodInfo = null;
        private FieldInfo fieldInfo = null;

        public VisibleIf(string methodName, bool negate = false)
        {
            this.MethodName = methodName;
            this.Negate = negate;
        }

        internal override bool IsVisible(SerializedProperty property)
        {
            return Visibility(property) == !Negate;
        }

        private bool Visibility(SerializedProperty property)
        {
            System.Type eventOwnerType = property.serializedObject.targetObject.GetType();
            string eventName = MethodName;

            // Try finding a method with the name provided:
            if (eventMethodInfo == null)
                eventMethodInfo = eventOwnerType.GetMethod(eventName, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);

            // If we could not find a method with that name, look for a field:
            if (eventMethodInfo == null && fieldInfo == null)
                fieldInfo = eventOwnerType.GetField(eventName, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);

            if (eventMethodInfo != null)
                return (bool)eventMethodInfo.Invoke(property.serializedObject.targetObject, null);
            else if (fieldInfo != null)
                return (bool)fieldInfo.GetValue(property.serializedObject.targetObject);
            else
                Debug.LogWarning(string.Format("VisibleIf: Unable to find method or field {0} in {1}", eventName, eventOwnerType));

            return true;
        }
    }

You'd use it like this:

class MonkeyTest:MonoBehaviour
{
     public bool hasMonkeys;

     [Indent]
     [VisibleIf("hasMonkeys")]
     public int monkeyCount = 5;
}

If you come up with more use cases and modifications please share them :). One idea I have in mind is allowing each attribute to add/remove height to/from the final field height, instead of overriding it. This would allow each attribute to modify the final height of the field on their own, without the need for other attributes to be aware of their presence.

10 Likes

I'm gonna give it a try with a lot of attributes and drawers. Thanks for this great idea!

I have a difference idea, keep the property drawer a single drawer, but add a new modifier stack system. Because we usually would not draw the real property field drawer multiple times. When we want a multiple property drawer on a field, we indeed want make some GUI state change before draw the field. (color, rect, position, etc.)

PropertyModifierAttribute for the modifer and ModifiablePropertyAttribute for the property drawer:

using System;
using UnityEditor;

namespace UnityEngine
{
    [AttributeUsage(AttributeTargets.Field, Inherited = true, AllowMultiple = true)]
    public abstract class PropertyModifierAttribute : Attribute
    {
        public int order { get; set; }

        public virtual float GetHeight(SerializedProperty property, GUIContent label, float height)
        {
            return height;
        }

        public virtual bool BeforeGUI(ref Rect position, SerializedProperty property, GUIContent label, bool visible) { return true; }
        public virtual void AfterGUI(Rect position, SerializedProperty property, GUIContent label) { }
    }
}
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

[AttributeUsage(AttributeTargets.Field, Inherited = true, AllowMultiple = false)]
public class ModifiablePropertyAttribute : PropertyAttribute
{
    public List<PropertyModifierAttribute> modifiers = null;

    public virtual void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        EditorGUI.PropertyField(position, property, label);
    }

    public virtual float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        return EditorGUI.GetPropertyHeight(property, label);
    }
}

ModifiablePropertyDrawer draw the property with modifier stack change the GUI state before drawing.

using System.Linq;
using UnityEditor;
using UnityEngine;

[CustomPropertyDrawer(typeof(ModifiablePropertyAttribute), true)]
public class ModifiablePropertyDrawer : PropertyDrawer
{
    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        var modifiable = (ModifiablePropertyAttribute)attribute;
        if (modifiable.modifiers == null)
            modifiable.modifiers = fieldInfo.GetCustomAttributes(typeof(PropertyModifierAttribute), false)
            .Cast<PropertyModifierAttribute>().OrderBy(s => s.order).ToList();

        float height = ((ModifiablePropertyAttribute)attribute).GetPropertyHeight(property, label);
        foreach (var attr in modifiable.modifiers)
            height = attr.GetHeight(property, label, height);
        return height;
    }

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        var modifiable = (ModifiablePropertyAttribute)attribute;

        bool visible = true;
        foreach (var attr in modifiable.modifiers.AsEnumerable().Reverse())
            visible = attr.BeforeGUI(ref position, property, label, visible);

        if (visible)
            modifiable.OnGUI(position, property, label);

        foreach (var attr in modifiable.modifiers)
            attr.AfterGUI(position, property, label);
    }
}

ModifiableRangeAttribute is same as RangeAttribute for ModifiablePropertyDrawer:

using UnityEditor;
using UnityEngine;

public class ModifiableRangeAttribute : ModifiablePropertyAttribute
{
    float min;
    float max;

    public ModifiableRangeAttribute(float min, float max)
    {
        this.min = min;
        this.max = max;
    }

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        if (property.propertyType == SerializedPropertyType.Float)
            EditorGUI.Slider(position, property, min, max, label);
        else if (property.propertyType == SerializedPropertyType.Integer)
            EditorGUI.IntSlider(position, property, (int)min, (int)max, label);
        else
            EditorGUI.LabelField(position, label.text, "Use Range with float or int.");
    }
}

Here is the magic, ColorModifier change the GUI color before drawing.

using UnityEditor;
using UnityEngine;

public class ColorModifierAttribute : PropertyModifierAttribute
{
    private Color m_Color;
    private Color m_GUIColor;

    public ColorModifierAttribute(float r, float g, float b, float a)
    {
        m_Color = new Color(r, g, b, a);
    }

    public override bool BeforeGUI(ref Rect position, SerializedProperty property, GUIContent label, bool visible)
    {
        m_GUIColor = GUI.color;
        if (!visible) return false;
        GUI.color = m_Color;
        return true;
    }

    public override void AfterGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        GUI.color = m_GUIColor;
    }
}

Colored Slider:

    [ColorModifier(1, 0, 0, 1)]
    [ModifiableRange(0, 100)]
    public int amount = 1;

3331025--259634--modifier1.jpg

BoxModifier drawer a box around the field with padding. It modifies the height and rect of the field:

using UnityEditor;
using UnityEngine;

public class BoxModifierAttribute : PropertyModifierAttribute
{
    public string GUIstyle;

    private int m_Left;
    private int m_Right;
    private int m_Top;
    private int m_Bottom;

    public BoxModifierAttribute(int left, int right, int top, int bottom)
    {
        m_Left = left;
        m_Right = right;
        m_Top = top;
        m_Bottom = bottom;
    }

    public override float GetHeight(SerializedProperty property, GUIContent label, float height)
    {
        return height + m_Top + m_Bottom;
    }

    public override bool BeforeGUI(ref Rect position, SerializedProperty property, GUIContent label, bool visible)
    {
        if (!visible) return false;
        if (string.IsNullOrEmpty(GUIstyle))
            GUI.Box(EditorGUI.IndentedRect(position), GUIContent.none);
        else
            GUI.Box(EditorGUI.IndentedRect(position), GUIContent.none, GUIstyle);
        var offset = new RectOffset(m_Left, m_Right, m_Top, m_Bottom);
        position = offset.Remove(position);
        return true;
    }
}
    [BoxModifier(5, 5, 5, 5, order = 1)]
    [ColorModifier(1, 0, 0, 1)]
    [ModifiableRange(0, 100)]
    public int amount = 1;

The order is stack order, so the color do not change the box.
3331025--259635--modifier2.jpg

    [ColorModifier(1, 1, 0, 1, order = 2)]
    [BoxModifier(5, 5, 5, 5, order = 1)]
    [ColorModifier(1, 0, 0, 1)]
    [ModifiableRange(0, 100)]
    public int amount = 1;

Add a color above the box.
3331025--259636--modifier3.jpg

HeaderModifier:

using UnityEditor;
using UnityEngine;

public class HeaderModifierAttribute : PropertyModifierAttribute
{
    public string GUIstyle;

    private GUIStyle m_GUIStyle;
    private float m_Height;
    private string m_Header;

    public HeaderModifierAttribute(string header)
    {
        m_Header = header;
        m_GUIStyle = string.IsNullOrEmpty(GUIstyle) ? EditorStyles.boldLabel : GUIstyle;
        m_Height = m_GUIStyle.CalcSize(new GUIContent(header)).y;
    }

    public override float GetHeight(SerializedProperty property, GUIContent label, float height)
    {
        return height + m_Height;
    }

    public override bool BeforeGUI(ref Rect position, SerializedProperty property, GUIContent label, bool visible)
    {
        if (!visible) return false;
        var rect = EditorGUI.IndentedRect(position);
        rect.height = m_Height;
        GUI.Label(rect, m_Header, m_GUIStyle);
        position.yMin += m_Height;
        return true;
    }
}
    [HeaderModifier("Header", order = 3)]
    [ColorModifier(1, 1, 0, 1, order = 2)]
    [BoxModifier(5, 5, 5, 5, order = 1)]
    [ColorModifier(1, 0, 0, 1)]
    [ModifiableRange(0, 100)]
    public int amount = 1;

3331025--259637--modifier4.jpg

IconModifier:

using UnityEditor;
using UnityEngine;

public class IconModifierAttribute : PropertyModifierAttribute
{
    private static Texture s_Icon;
    private static Texture s_OldIcon;

    public IconModifierAttribute(string icon)
    {
        s_Icon = EditorGUIUtility.IconContent(icon).image;
    }

    public override bool BeforeGUI(ref Rect position, SerializedProperty property, GUIContent label, bool visible)
    {
        s_OldIcon = label.image;
        if (!visible) return false;
        label.image = s_Icon;
        return true;
    }

    public override void AfterGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        label.image = s_OldIcon;
    }
}
    [HeaderModifier("Header", order = 3)]
    [ColorModifier(1, 1, 0, 1, order = 2)]
    [BoxModifier(5, 5, 5, 5, order = 1)]
    [ColorModifier(1, 0, 0, 1)]
    [IconModifier("Prefab Icon")]
    [ModifiableRange(0, 100)]
    public int amount = 1;

3331025--259638--modifier5.jpg

3 Likes

FoldoutGroupModifier:

using UnityEditor;
using UnityEngine;

public class FoldoutGroupModifierAttribute : PropertyModifierAttribute
{
    public string GUIstyle;

    private GUIContent m_Title;
    private string[] m_Names;
    private bool m_Foldout = true;
    private float m_Height;

    public FoldoutGroupModifierAttribute(string title, params string[] names)
    {
        m_Title = new GUIContent(title);
        m_Names = names;
    }

    public override float GetHeight(SerializedProperty property, GUIContent label, float height)
    {
        m_Height = height;
        height = EditorGUIUtility.singleLineHeight + 2;
        if (m_Foldout)
        {
            height += m_Height;
            foreach (var name in m_Names)
            {
                var prop = property.serializedObject.FindProperty(name);
                if (prop == null) continue;
                height += EditorGUI.GetPropertyHeight(prop) + 2;
            }
        }
        return height;
    }

    public override bool BeforeGUI(ref Rect position, SerializedProperty property, GUIContent label, bool visible)
    {
        if (!visible) return false;

        var rect = new Rect(position);
        rect.height = EditorGUIUtility.singleLineHeight;
        m_Foldout = EditorGUI.Foldout(rect, m_Foldout, m_Title, true);
        rect.y += rect.height + 2;

        rect.height = m_Height;
        position = new Rect(rect);
        rect.y += rect.height + 2;
        if (m_Foldout)
        {
            var label2 = new GUIContent(label);
            EditorGUI.indentLevel++;
            foreach (var name in m_Names)
            {
                var prop = property.serializedObject.FindProperty(name);
                if (prop == null) continue;
                rect.height = EditorGUI.GetPropertyHeight(prop);
                EditorGUI.PropertyField(rect, prop);
                rect.y += rect.height + 2;
            }
            label.text = label2.text;
            label.image = label2.image;
            label.tooltip = label2.tooltip;
        }
        return m_Foldout;
    }

    public override void AfterGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        if (m_Foldout)
            EditorGUI.indentLevel--;
    }
}
    [FoldoutGroupModifier("Group", "amount", order = 3)]
    [ModifiableProperty]
    public string title;
    [HeaderModifier("Header", order = 3)]
    [ColorModifier(1, 1, 0, 1, order = 2)]
    [BoxModifier(5, 5, 5, 5, order = 1)]
    [ColorModifier(1, 0, 0, 1)]
    [IconModifier("Prefab Icon")]
    [ModifiableRange(0, 100)]
    [HideInInspector]
    public int amount = 1;

I used property.serializedObject.FindProperty that only find property from root.
To use with array or serializable object, need change the code.
"amount" field need [HideInInspector] as it drawer in "title" field.
3331029--259640--modifier6.jpg

    [FoldoutGroupModifier("Group", "amount", order = 3)]
    [ColorModifier(0, 1, 1, 1, order = 2)]
    [BoxModifier(2, 2, 2, 2, order = 1)]
    [ModifiableProperty]
    public string title;
    [HeaderModifier("Header", order = 3)]
    [ColorModifier(1, 1, 0, 1, order = 2)]
    [BoxModifier(5, 5, 5, 5, order = 1)]
    [ColorModifier(1, 0, 0, 1)]
    [IconModifier("Prefab Icon")]
    [ModifiableRange(0, 100)]
    [HideInInspector]
    public int amount = 1;

Add more stack together:
3331029--259642--modifier7.jpg

    [BoxModifier(0, 4, 4, 4, order = 4)]
    [FoldoutGroupModifier("Group", "amount", order = 3)]
    [ColorModifier(0, 1, 1, 1, order = 2)]
    [BoxModifier(2, 2, 2, 2, order = 1)]
    [ModifiableProperty]
    public string title;
    [HeaderModifier("Header", order = 3)]
    [ColorModifier(1, 1, 0, 1, order = 2)]
    [BoxModifier(5, 5, 5, 5, order = 1)]
    [ColorModifier(1, 0, 0, 1)]
    [IconModifier("Prefab Icon")]
    [ModifiableRange(0, 100)]
    [HideInInspector]
    public int amount = 1;

Add box around the foldout group:
3331029--259643--modifier8.jpg

Decorate the fields with styles:
3331826--259762--upload_2017-12-23_12-57-3.png

    [BoxModifier(0, 4, 4, 6, GUIStyle = "ChannelStripBg", order = 5)]
    [FoldoutGroupModifier("Group", "amount2", order = 4)]
    [SpaceModifier(6, order = 3)]
    [ColorModifier(1, 0.8f, 1, 1, order = 2)]
    [BoxModifier(2, 2, 2, 2, GUIStyle = "ShurikenEffectBg", order = 1)]
    [ModifiableProperty]
    public string title2;
    [SpaceModifier(10, GUIStyle = "PR Insertion", order = 4)]
    [ColorModifier(1, 1, 0.8f, 1, order = 3)]
    [HeaderModifier("Header    ", GUIStyle = "flow overlay header lower left", order = 2)]
    [BoxModifier(5, 5, 5, 5, GUIStyle = "flow overlay box", order = 1)]
    [ColorModifier(0.2f, 0.8f, 0.6f, 1)]
    [IconModifier("Prefab Icon")]
    [ModifiableRange(0, 100)]
    [HideInInspector]
    public int amount2 = 1;

Added SpaceModifier:

using UnityEditor;
using UnityEngine;

public class SpaceModifierAttribute : PropertyModifierAttribute
{
    public string GUIStyle;

    private int m_Space;

    public SpaceModifierAttribute(int space)
    {
        m_Space = space;
    }

    public override float GetHeight(SerializedProperty property, GUIContent label, float height)
    {
        return height + m_Space;
    }

    public override bool BeforeGUI(ref Rect position, SerializedProperty property, GUIContent label, bool visible)
    {
        if (!visible) return false;
        if (!string.IsNullOrEmpty(GUIStyle))
            GUI.Box(EditorGUI.IndentedRect(position), GUIContent.none, GUIStyle);
        position.yMin += m_Space;
        return true;
    }
}

Fixed a bug in HeaderModifier, GUIStyle doesn't initialized in constructor.

using UnityEditor;
using UnityEngine;

public class HeaderModifierAttribute : PropertyModifierAttribute
{
    public string GUIStyle;

    private GUIStyle m_GUIStyle = null;
    private GUIContent m_Header;
    private Vector2 m_Size;

    public HeaderModifierAttribute(string header)
    {
        m_Header = new GUIContent(header);
    }

    public override float GetHeight(SerializedProperty property, GUIContent label, float height)
    {
        if (m_GUIStyle == null)
        {
            m_GUIStyle = string.IsNullOrEmpty(GUIStyle) ? EditorStyles.boldLabel : GUIStyle;
            m_Size = m_GUIStyle.CalcSize(m_Header);
        }
        return height + m_Size.y;
    }

    public override bool BeforeGUI(ref Rect position, SerializedProperty property, GUIContent label, bool visible)
    {
        if (!visible) return false;
        var rect = EditorGUI.IndentedRect(position);
        rect.size = m_Size;
        GUI.Label(rect, m_Header, m_GUIStyle);
        position.yMin += m_Size.y;
        return true;
    }
}

Hello, I find it interesting and will continue to make it a complete system. I have posted a new thread in Works In Progress group.

Sorry if this is basic, but I see a lot of the above code uses SerializedProperty in the MultiPropertyAttribute class.

SerializedProperty is from the UnityEditor namespace, which means the MultiPropertyAttribute won't compile in an actual build. I know about putting property drawers in an 'Editor' folder or wrapping it in a conditional compilation statement; however, if MultiPropertyAttribute itself is dependent on the UnityEditor namespace I don't see how this code could actually build and run outside of Unity.

What am I missing?

hi,

you need to wrap the ongui (and functions that use the SerializedProperty type) in define tags

#if UNITY_EDITOR
...your code here
#endif

This means that the code between those tags will not get built into builds. seen as the ongui only happen when inspecting the object in editor, it will not (try) to get called in build, and will not fail

Thanks, I appreciate your response. That makes sense about OnGUI, but this also means that you would have to wrap all your Attributes that used the MultiPropertyAttribute in conditional compilation as well, right?

I like this approach (versus having to redo custom GUIs), but I don't like that I would have to add #if every time I wanted to use a custom attribute. (Please let me know if I am misunderstanding anything)

You do not need to wrap the attribute on the field in #ifs. So long as you wrap the inside of the YourAttribute in #ifs - not including the constructor.

so:

  public class ColorAttribute : MultiPropertyAttribute{
 //Note the constructor and the Color variable are outside the #if
Color Color;
  public ColorAttribute(float R, float G, float B)
  {
      Color = new Color(R, G, B);
  }
#if UNITY_EDITOR
//anything that uses editor stuff needs to be within the #if (i.e. SerializedProperty)
   // Draw the property inside the given rect
   public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
   {
       GUI.color = Color;
  }
#endif
}

in your monobehaviour

[Color(1,1,1)]
public float MyField;

This code should compile for builds.

I hope this makes sense :slight_smile:

I've stumbled upon this limitation today when trying to implement "ShowIf" attribute. Found this thread and solutions provided very helpful. But I've decided to stay away from such complicated solutions (for now), as they are not very portable between projects (require to rewrite all your drawers to take full advantage). Maybe they are more useful for stand-alone editor tools...

It seems there is another simpler (but more limited) approach to my problem. If I simply put the value I need a custom drawer for (e.g. Tag selector) in a struct, then I can just add my "ShowIf" attribute to a struct field. This way the limitation is avoided as there are no 2 drawers for the same SerializedProperty (one for the struct field and another for the field inside of it). The downside is of course the wasted line in inspector, but I can live with that.

2 Likes