Search Unity

Drawing a Field Using Multiple Property Drawers

Discussion in 'Immediate Mode GUI (IMGUI)' started by BinaryCats, Jun 24, 2017.

  1. BinaryCats

    BinaryCats

    Joined:
    Feb 8, 2016
    Posts:
    317
    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.
    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
    Code (csharp):
    1.  
    2. [AttributeUsage(AttributeTargets.Field)]
    3. public abstract class MultiPropertyAttribute : PropertyAttribute
    4. {
    5.     public List<object> stored = new List<object>();
    6.     public virtual GUIContent BuildLabel(GUIContent label)
    7.     {
    8.         return label;
    9.     }
    10.     public abstract void OnGUI(Rect position, SerializedProperty property, GUIContent label);
    11.  
    12.     internal virtual float? GetPropertyHeight( SerializedProperty property, GUIContent label)
    13.     {
    14.         return null;
    15.     }
    16. }
    17.  
    The Drawer which controls the other attributes drawing
    Code (csharp):
    1.  
    2.    [CustomPropertyDrawer(typeof(MultiPropertyAttribute),true)]
    3. public class MultiPropertyDrawer : PropertyDrawer
    4. {
    5.     public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    6.     {
    7.         MultiPropertyAttribute @Attribute = attribute as MultiPropertyAttribute;
    8.         float height = base.GetPropertyHeight(property, label);
    9.         foreach (object atr in @Attribute.stored)//Go through the attributes, and try to get an altered height, if no altered height return default height.
    10.         {
    11.             if (atr as MultiPropertyAttribute != null)
    12.             {
    13.                 //build label here too?
    14.                 var tempheight = ((MultiPropertyAttribute)atr).GetPropertyHeight(property, label);
    15.                 if (tempheight.HasValue)
    16.                 {
    17.                     height = tempheight.Value;
    18.                     break;
    19.                 }
    20.             }
    21.         }
    22.         return height;
    23.     }
    24.     // Draw the property inside the given rect
    25.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    26.     {
    27.         MultiPropertyAttribute @Attribute = attribute as MultiPropertyAttribute;
    28.         // First get the attribute since it contains the range for the slider
    29.         if (@Attribute.stored == null || @Attribute.stored.Count == 0)
    30.         {
    31.             @Attribute.stored = fieldInfo.GetCustomAttributes(typeof(MultiPropertyAttribute), false).OrderBy(s => ((PropertyAttribute)s).order).ToList() ;
    32.         }
    33.         var OrigColor = GUI.color;
    34.         var Label = label;
    35.         foreach (object atr in @Attribute.stored)
    36.         {
    37.             if (atr as MultiPropertyAttribute != null)
    38.             {
    39.                 Label = ((MultiPropertyAttribute)atr).BuildLabel(Label);
    40.                 ((MultiPropertyAttribute)atr).OnGUI(position, property, Label);
    41.             }
    42.         }
    43.         GUI.color = OrigColor;
    44.     }
    45. }
    46.  
    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

    Code (csharp):
    1.  
    2. public class ColorAttribute : MultiPropertyAttribute
    3. {
    4.     Color Color;
    5.     public ColorAttribute(float R, float G, float B)
    6.     {
    7.         Color = new Color(R, G, B);
    8.     }
    9.     // Draw the property inside the given rect
    10.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    11.     {
    12.         GUI.color = Color;
    13.     }
    14. }
    15. public class NewRangeAttribute : MultiPropertyAttribute
    16. {
    17.     float min;
    18.     float max;
    19.     public NewRangeAttribute(float min, float max)
    20.     {
    21.         this.min = min;
    22.         this.max = max;
    23.     }
    24.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    25.     {
    26.         if (property.propertyType == SerializedPropertyType.Float)
    27.             EditorGUI.Slider(position, property, min, max, label);
    28.         else
    29.             EditorGUI.LabelField(position, label.text, "Use Range with float or int.");
    30.     }
    31. }
    32.  
    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?

    Code (csharp):
    1.  
    2.     [Color(1, 0, 0, order = 0)]
    3.     [NewRange(0, 2.0f,order = 1)]
    4.     public float ColorRange = 0.1f;
    5.  
    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.

    Code (csharp):
    1.  
    2. public class Prefab : MultiPropertyAttribute
    3. {
    4.     GUIContent Icon;
    5.     public Prefab()
    6.     {
    7.         Icon = EditorGUIUtility.IconContent("Prefab Icon");
    8.     }
    9.     public override GUIContent BuildLabel(GUIContent label)
    10.     {
    11.             Icon.text = label.text;
    12.       return Icon;
    13.     }
    14.        
    15.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    16.     {
    17.     }
    18. }
    19.  
    And that can be declared like:
    Code (csharp):
    1.  
    2.     [Prefab]
    3.     [Color(0,0,1)]
    4.     [NewRange(0,1,order = 1)]
    5.     public float PrefabColorRange = 0.1f;
    6.  
    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
    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)
    Code (csharp):
    1.  
    2. public class FoldOutAttribute : MultiPropertyAttribute
    3. {
    4.     string[] VarNames;
    5.     bool Foldout = false;
    6.     GUIContent Title;
    7.     public FoldOutAttribute(string title,params string[] varNames)
    8.     {
    9.         VarNames = new string[varNames.Length];
    10.         VarNames = varNames;
    11.         Title = new GUIContent(title);
    12.     }
    13.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    14.     {
    15.         position.height = EditorGUIUtility.singleLineHeight;
    16.         Foldout = EditorGUI.Foldout(position, Foldout, Title);
    17.         if (Foldout)
    18.         {
    19.             ++EditorGUI.indentLevel;
    20.             Rect newpos = position;
    21.             newpos.y += EditorGUIUtility.singleLineHeight + 2;
    22.             EditorGUI.PropertyField(newpos, property);
    23.             for (int i = 0; i < VarNames.Length; i++)
    24.             {
    25.                var a =  property.serializedObject.FindProperty(VarNames[i]);
    26.                 newpos.y += EditorGUIUtility.singleLineHeight +2;
    27.                 newpos.height = EditorGUIUtility.singleLineHeight;
    28.                 EditorGUI.PropertyField(newpos, a);  
    29.             }
    30.         }
    31.     }
    32.    
    33.     internal override float GetPropertyHeight( SerializedProperty property, GUIContent label)
    34.     {
    35.         if (Foldout)
    36.         {
    37.             return (float)((EditorGUIUtility.singleLineHeight + 2) * (VarNames.Count() +2));
    38.         }
    39.         else return float.NegativeInfinity;
    40.          ;
    41.     }
    42. }
    43.  
    Here I alter the drawer height, and what is being drawn when the foldout is open.

    you can define your variable like this:
    Code (csharp):
    1.  
    2.    [Color(0, 1, 0, order = 0)]
    3.     [FoldOut("My Multi Property Drawers", "PrefabColorRange", "ColorRange", order = 1)]
    4.     public float ColorFoldOut;
    5.  
    closed
    upload_2017-6-24_14-35-0.png
    open
    upload_2017-6-24_14-35-21.png


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

    Thanks
     
    Last edited: Jun 26, 2017
  2. shawn

    shawn

    Unity Technologies

    Joined:
    Aug 4, 2007
    Posts:
    552
    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.
     
    Bshsf_9527, Seromu, ModLunar and 6 others like this.
  3. BinaryCats

    BinaryCats

    Joined:
    Feb 8, 2016
    Posts:
    317
    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
    Code (csharp):
    1.  
    2. internal virtual float GetPropertyHeight( SerializedProperty property, GUIContent label){
    3.  
    is now
    Code (csharp):
    1.  
    2. internal virtual float? GetPropertyHeight( SerializedProperty property, GUIContent label){
    3. retun null;//the property value has not been changed
    4. }
    5.  
    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
    Code (csharp):
    1.  
    2. EditorGUI.PropertyField(position, property);
    3. EditorGUI.Toggle(Position, property);
    4.  
    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
     
    Last edited: Jun 26, 2017
    ModLunar and shawn like this.
  4. BinaryCats

    BinaryCats

    Joined:
    Feb 8, 2016
    Posts:
    317
    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.
    Code (csharp):
    1.  
    2. public abstract class MultiPropertyAttribute : PropertyAttribute
    3. {
    4.     public abstract void OnGUI(Rect position, SerializedProperty property, GUIContent label, bool Last);
    5.  
    The draw code now changes to this:
    Code (csharp):
    1.  
    2.     [CustomPropertyDrawer(typeof(MultiPropertyAttribute),true)]
    3. public class MultiPropertyDrawer : PropertyDrawer
    4. {
    5.     // Draw the property inside the given rect
    6.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    7.     {
    8.         MultiPropertyAttribute @Attribute = attribute as MultiPropertyAttribute;
    9.         // First get the attribute since it contains the range for the slider
    10.         if (@Attribute.stored == null || @Attribute.stored.Count == 0)
    11.         {
    12.             @Attribute.stored = fieldInfo.GetCustomAttributes(typeof(MultiPropertyAttribute), false).OrderBy(s => ((PropertyAttribute)s).order).ToList();
    13.         }
    14.         var OrigColor = GUI.color;
    15.         var Label = label;
    16.         for (int i = 0; i < @Attribute.stored.Count; i++)
    17.         {
    18.             var atr = @Attribute.stored[i];
    19.             if (atr as MultiPropertyAttribute != null) // Redundant check because of arg 1 in GetCustomAttributes
    20.             {
    21.                 ((MultiPropertyAttribute)atr).Validation(property);
    22.                 Label = ((MultiPropertyAttribute)atr).BuildLabel(Label);
    23.                 ((MultiPropertyAttribute)atr).OnGUI(position, property, Label, i == @Attribute.stored.Count - 1);
    24.             }
    25.         }
    26.  
    27.         GUI.color = OrigColor;
    28.     }
    29.  
    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,
    Code (csharp):
    1.  
    2.  
    3.     [Color(0,0,1)]
    4.     public float Colorfloat = 0.1f;
    5.  
    With the above change, the I can change the OnGUI for the Color attribute to:


    Code (csharp):
    1.  
    2. public class ColorAttribute : MultiPropertyAttribute
    3. {
    4.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label, bool Last)
    5.     {
    6.         GUI.color = Color;
    7.         if (Last)
    8.         {
    9.             EditorGUI.PropertyField(position, property, label, true);
    10.         }
    11.     }
    12.  
    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.
    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
    Code (csharp):
    1.  
    2.     [CustomPropertyDrawer(typeof(MultiPropertyAttribute),true)]
    3. public class MultiPropertyDrawer : PropertyDrawer
    4. {
    5.     // Draw the property inside the given rect
    6.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    7.     {
    8.         MultiPropertyAttribute @Attribute = attribute as MultiPropertyAttribute;
    9.         // First get the attribute since it contains the range for the slider
    10.         if (@Attribute.stored == null || @Attribute.stored.Count == 0)
    11.         {
    12.             @Attribute.stored = fieldInfo.GetCustomAttributes(typeof(MultiPropertyAttribute), false).OrderBy(s => ((PropertyAttribute)s).order).ToList();
    13.         }
    14.         var OrigColor = GUI.color;
    15.         var Label = label;
    16.         for (int i = 0; i < @Attribute.stored.Count; i++)
    17.         {
    18.             var atr = @Attribute.stored[i];
    19.             if (atr as MultiPropertyAttribute != null) // Redundant check because of arg 1 in GetCustomAttributes
    20.             {
    21.                 ((MultiPropertyAttribute)atr).Validation(property);
    22.                 Label = ((MultiPropertyAttribute)atr).BuildLabel(Label);
    23.                 ((MultiPropertyAttribute)atr).Decorate(position, property, Label);
    24.             }
    25.         }
    26.         ((MultiPropertyAttribute)@Attribute.stored.Last()).OnGUI(position, property, Label);
    27.         GUI.color = OrigColor;
    28.     }
    29.  
    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:
    Code (csharp):
    1.  
    2. public class ColorAttribute : MultiPropertyAttribute
    3. {
    4.     public override void Decorate(Rect position, SerializedProperty property, GUIContent label, bool Last)
    5.         {
    6.         GUI.color = Color;
    7.     }
    8.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label, bool Last)
    9.     {
    10.             EditorGUI.PropertyField(position, property, label, true);
    11.     }
    12.  
    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
     
    Last edited: Jun 26, 2017
  5. bWayneSu

    bWayneSu

    Joined:
    May 22, 2017
    Posts:
    13
    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 :(
     
  6. BinaryCats

    BinaryCats

    Joined:
    Feb 8, 2016
    Posts:
    317
    The drawer's need to be in a (i.e.
    Code (csharp):
    1.  
    2. [LIST=1]
    3. [*][CustomPropertyDrawer(typeof(MultiPropertyAttribute),true)]
    4. [*]public class MultiPropertyDrawer : PropertyDrawer
    5.  
    [/LIST]

    1. needs to be wrapped in #if UNITY_EDITOR so
    Code (csharp):
    1.  
    2. #if UNITY_EDITOR
    3. [CustomPropertyDrawer(typeof(MultiPropertyAttribute),true)]
    4. public class MultiPropertyDrawer : PropertyDrawer
    5. {
    6. ...
    7. }
    8. #endif
    9. [LIST=1]
    10.  
    [/LIST]

    1. The rest of the code should be fine
     
  7. bWayneSu

    bWayneSu

    Joined:
    May 22, 2017
    Posts:
    13
    Wrap in #if UNITY_EDITOR
    Got it! Thanks!
     
  8. PixelLifetime

    PixelLifetime

    Joined:
    Mar 30, 2017
    Posts:
    90
    Hi, @BinaryCats

    Could you tell us how does method
    Code (CSharp):
    1. Validation(SerializedProperty property)
    work, please?
     
  9. BinaryCats

    BinaryCats

    Joined:
    Feb 8, 2016
    Posts:
    317
    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
    Code (csharp):
    1.  
    2.       for (int i = 0; i < @Attribute.stored.Count; i++)
    3.         {
    4.             var atr = @Attribute.stored[i];
    5.             if (atr as MultiPropertyAttribute != null) // Redundant check because of arg 1 in GetCustomAttributes
    6.             {
    7.                 if (((MultiPropertyAttribute)atr).Validation(property))
    8.                 {
    9.                     Label = ((MultiPropertyAttribute)atr).BuildLabel(Label);
    10.                     ((MultiPropertyAttribute)atr).Decorate(position, property, Label);
    11.                 }
    12.                 else
    13.                 {
    14.                     GUI.color = OrigColor;
    15.                     return;
    16.                 }
    17.             }
    18.         }
    19.  
    20.         ((MultiPropertyAttribute)@Attribute.stored.Last()).OnGUI(position, property, Label);
    21.  
    22.    
    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.
     
    Last edited: Oct 30, 2017
    PixelLifetime likes this.
  10. arkano22

    arkano22

    Joined:
    Sep 20, 2012
    Posts:
    1,928
    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.

    Code (CSharp):
    1. [System.AttributeUsage(System.AttributeTargets.Field)]
    2.     public abstract class MultiPropertyAttribute : PropertyAttribute
    3.     {
    4.         public IOrderedEnumerable<object> stored = null;
    5.  
    6.         public virtual void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    7.         {
    8.             EditorGUI.PropertyField(position,property,label);
    9.         }
    10.  
    11.         internal virtual void OnPreGUI(Rect position, SerializedProperty property){}
    12.         internal virtual void OnPostGUI(Rect position, SerializedProperty property){}
    13.  
    14.         internal virtual bool IsVisible(SerializedProperty property){return true;}
    15.         internal virtual float? GetPropertyHeight( SerializedProperty property, GUIContent label){return null;}
    16.     }
    17.  
    18.     [CustomPropertyDrawer(typeof(MultiPropertyAttribute),true)]
    19.     public class MultiPropertyDrawer : PropertyDrawer
    20.     {
    21.         private MultiPropertyAttribute RetrieveAttributes()
    22.         {
    23.             MultiPropertyAttribute mAttribute = attribute as MultiPropertyAttribute;
    24.  
    25.             // Get the attribute list, sorted by "order".
    26.             if (mAttribute.stored == null)
    27.             {
    28.                 mAttribute.stored = fieldInfo.GetCustomAttributes(typeof(MultiPropertyAttribute), false).OrderBy(s => ((PropertyAttribute)s).order);
    29.             }
    30.  
    31.             return mAttribute;
    32.         }
    33.  
    34.         public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    35.         {
    36.             MultiPropertyAttribute mAttribute = RetrieveAttributes();
    37.  
    38.             // If the attribute is invisible, regain the standard vertical spacing.
    39.             foreach (MultiPropertyAttribute attr in mAttribute.stored)
    40.                 if (!attr.IsVisible(property))
    41.                     return -EditorGUIUtility.standardVerticalSpacing;
    42.  
    43.             // In case no attribute returns a modified height, return the property's default one:
    44.             float height = base.GetPropertyHeight(property, label);
    45.  
    46.             // Check if any of the attributes wants to modify height:
    47.             foreach (object atr in mAttribute.stored)
    48.             {
    49.                 if (atr as MultiPropertyAttribute != null)
    50.                 {
    51.                     var tempheight = ((MultiPropertyAttribute)atr).GetPropertyHeight(property, label);
    52.                     if (tempheight.HasValue)
    53.                     {
    54.                         height = tempheight.Value;
    55.                         break;
    56.                     }
    57.                 }
    58.             }
    59.             return height;
    60.         }
    61.  
    62.         public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    63.         {
    64.             MultiPropertyAttribute mAttribute = RetrieveAttributes();
    65.  
    66.             // Calls to IsVisible. If it returns false for any attribute, the property will not be rendered.
    67.             foreach (MultiPropertyAttribute attr in mAttribute.stored)
    68.                 if (!attr.IsVisible(property)) return;
    69.  
    70.             // Calls to OnPreRender before the last attribute draws the UI.
    71.             foreach (MultiPropertyAttribute attr in mAttribute.stored)
    72.                 attr.OnPreGUI(position,property);
    73.  
    74.             // The last attribute is in charge of actually drawing something:
    75.             ((MultiPropertyAttribute)mAttribute.stored.Last()).OnGUI(position,property,label);
    76.  
    77.             // Calls to OnPostRender after the last attribute draws the UI. These are called in reverse order.
    78.             foreach (MultiPropertyAttribute attr in mAttribute.stored.Reverse())
    79.                 attr.OnPostGUI(position,property);
    80.         }
    81.     }
    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:
    Code (CSharp):
    1. [System.AttributeUsage(System.AttributeTargets.Field)]
    2.     public class Indent : MultiPropertyAttribute
    3.     {
    4.         internal override void OnPreGUI(Rect position, SerializedProperty property)
    5.         {
    6.             EditorGUI.indentLevel++;
    7.         }
    8.         internal override void OnPostGUI(Rect position, SerializedProperty property)
    9.         {
    10.             EditorGUI.indentLevel--;
    11.         }
    12.     }
    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:

    Code (CSharp):
    1. [System.AttributeUsage(System.AttributeTargets.Field)]
    2.     public class VisibleIf : MultiPropertyAttribute
    3.     {
    4.         public string MethodName { get; private set; }
    5.         public bool Negate {get; private set;}
    6.  
    7.         private MethodInfo eventMethodInfo = null;
    8.         private FieldInfo fieldInfo = null;
    9.  
    10.         public VisibleIf(string methodName, bool negate = false)
    11.         {
    12.             this.MethodName = methodName;
    13.             this.Negate = negate;
    14.         }
    15.  
    16.         internal override bool IsVisible(SerializedProperty property)
    17.         {
    18.             return Visibility(property) == !Negate;
    19.         }
    20.  
    21.         private bool Visibility(SerializedProperty property)
    22.         {
    23.             System.Type eventOwnerType = property.serializedObject.targetObject.GetType();
    24.             string eventName = MethodName;
    25.  
    26.             // Try finding a method with the name provided:
    27.             if (eventMethodInfo == null)
    28.                 eventMethodInfo = eventOwnerType.GetMethod(eventName, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
    29.  
    30.             // If we could not find a method with that name, look for a field:
    31.             if (eventMethodInfo == null && fieldInfo == null)
    32.                 fieldInfo = eventOwnerType.GetField(eventName, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
    33.  
    34.             if (eventMethodInfo != null)
    35.                 return (bool)eventMethodInfo.Invoke(property.serializedObject.targetObject, null);
    36.             else if (fieldInfo != null)
    37.                 return (bool)fieldInfo.GetValue(property.serializedObject.targetObject);
    38.             else
    39.                 Debug.LogWarning(string.Format("VisibleIf: Unable to find method or field {0} in {1}", eventName, eventOwnerType));
    40.  
    41.             return true;
    42.         }
    43.     }
    You'd use it like this:
    Code (CSharp):
    1.  
    2. class MonkeyTest:MonoBehaviour
    3. {
    4.      public bool hasMonkeys;
    5.  
    6.      [Indent]
    7.      [VisibleIf("hasMonkeys")]
    8.      public int monkeyCount = 5;
    9. }
    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.​
     
    Last edited: Nov 29, 2017
  11. PsyKaw

    PsyKaw

    Joined:
    Aug 16, 2012
    Posts:
    102
    I'm gonna give it a try with a lot of attributes and drawers. Thanks for this great idea!
     
  12. lokinwai

    lokinwai

    Joined:
    Feb 11, 2015
    Posts:
    174
    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:
    Code (CSharp):
    1. using System;
    2. using UnityEditor;
    3.  
    4. namespace UnityEngine
    5. {
    6.     [AttributeUsage(AttributeTargets.Field, Inherited = true, AllowMultiple = true)]
    7.     public abstract class PropertyModifierAttribute : Attribute
    8.     {
    9.         public int order { get; set; }
    10.  
    11.         public virtual float GetHeight(SerializedProperty property, GUIContent label, float height)
    12.         {
    13.             return height;
    14.         }
    15.  
    16.         public virtual bool BeforeGUI(ref Rect position, SerializedProperty property, GUIContent label, bool visible) { return true; }
    17.         public virtual void AfterGUI(Rect position, SerializedProperty property, GUIContent label) { }
    18.     }
    19. }
    Code (CSharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using UnityEditor;
    4. using UnityEngine;
    5.  
    6. [AttributeUsage(AttributeTargets.Field, Inherited = true, AllowMultiple = false)]
    7. public class ModifiablePropertyAttribute : PropertyAttribute
    8. {
    9.     public List<PropertyModifierAttribute> modifiers = null;
    10.  
    11.     public virtual void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    12.     {
    13.         EditorGUI.PropertyField(position, property, label);
    14.     }
    15.  
    16.     public virtual float GetPropertyHeight(SerializedProperty property, GUIContent label)
    17.     {
    18.         return EditorGUI.GetPropertyHeight(property, label);
    19.     }
    20. }
    ModifiablePropertyDrawer draw the property with modifier stack change the GUI state before drawing.
    Code (CSharp):
    1. using System.Linq;
    2. using UnityEditor;
    3. using UnityEngine;
    4.  
    5. [CustomPropertyDrawer(typeof(ModifiablePropertyAttribute), true)]
    6. public class ModifiablePropertyDrawer : PropertyDrawer
    7. {
    8.     public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    9.     {
    10.         var modifiable = (ModifiablePropertyAttribute)attribute;
    11.         if (modifiable.modifiers == null)
    12.             modifiable.modifiers = fieldInfo.GetCustomAttributes(typeof(PropertyModifierAttribute), false)
    13.             .Cast<PropertyModifierAttribute>().OrderBy(s => s.order).ToList();
    14.  
    15.         float height = ((ModifiablePropertyAttribute)attribute).GetPropertyHeight(property, label);
    16.         foreach (var attr in modifiable.modifiers)
    17.             height = attr.GetHeight(property, label, height);
    18.         return height;
    19.     }
    20.  
    21.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    22.     {
    23.         var modifiable = (ModifiablePropertyAttribute)attribute;
    24.  
    25.         bool visible = true;
    26.         foreach (var attr in modifiable.modifiers.AsEnumerable().Reverse())
    27.             visible = attr.BeforeGUI(ref position, property, label, visible);
    28.  
    29.         if (visible)
    30.             modifiable.OnGUI(position, property, label);
    31.  
    32.         foreach (var attr in modifiable.modifiers)
    33.             attr.AfterGUI(position, property, label);
    34.     }
    35. }
    ModifiableRangeAttribute is same as RangeAttribute for ModifiablePropertyDrawer:
    Code (CSharp):
    1. using UnityEditor;
    2. using UnityEngine;
    3.  
    4. public class ModifiableRangeAttribute : ModifiablePropertyAttribute
    5. {
    6.     float min;
    7.     float max;
    8.  
    9.     public ModifiableRangeAttribute(float min, float max)
    10.     {
    11.         this.min = min;
    12.         this.max = max;
    13.     }
    14.  
    15.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    16.     {
    17.         if (property.propertyType == SerializedPropertyType.Float)
    18.             EditorGUI.Slider(position, property, min, max, label);
    19.         else if (property.propertyType == SerializedPropertyType.Integer)
    20.             EditorGUI.IntSlider(position, property, (int)min, (int)max, label);
    21.         else
    22.             EditorGUI.LabelField(position, label.text, "Use Range with float or int.");
    23.     }
    24. }
    Here is the magic, ColorModifier change the GUI color before drawing.
    Code (CSharp):
    1. using UnityEditor;
    2. using UnityEngine;
    3.  
    4. public class ColorModifierAttribute : PropertyModifierAttribute
    5. {
    6.     private Color m_Color;
    7.     private Color m_GUIColor;
    8.  
    9.     public ColorModifierAttribute(float r, float g, float b, float a)
    10.     {
    11.         m_Color = new Color(r, g, b, a);
    12.     }
    13.  
    14.     public override bool BeforeGUI(ref Rect position, SerializedProperty property, GUIContent label, bool visible)
    15.     {
    16.         m_GUIColor = GUI.color;
    17.         if (!visible) return false;
    18.         GUI.color = m_Color;
    19.         return true;
    20.     }
    21.  
    22.     public override void AfterGUI(Rect position, SerializedProperty property, GUIContent label)
    23.     {
    24.         GUI.color = m_GUIColor;
    25.     }
    26. }
    Colored Slider:
    Code (CSharp):
    1.     [ColorModifier(1, 0, 0, 1)]
    2.     [ModifiableRange(0, 100)]
    3.     public int amount = 1;
    4.  
    modifier1.jpg

    BoxModifier drawer a box around the field with padding. It modifies the height and rect of the field:
    Code (CSharp):
    1. using UnityEditor;
    2. using UnityEngine;
    3.  
    4. public class BoxModifierAttribute : PropertyModifierAttribute
    5. {
    6.     public string GUIstyle;
    7.  
    8.     private int m_Left;
    9.     private int m_Right;
    10.     private int m_Top;
    11.     private int m_Bottom;
    12.  
    13.     public BoxModifierAttribute(int left, int right, int top, int bottom)
    14.     {
    15.         m_Left = left;
    16.         m_Right = right;
    17.         m_Top = top;
    18.         m_Bottom = bottom;
    19.     }
    20.  
    21.     public override float GetHeight(SerializedProperty property, GUIContent label, float height)
    22.     {
    23.         return height + m_Top + m_Bottom;
    24.     }
    25.  
    26.     public override bool BeforeGUI(ref Rect position, SerializedProperty property, GUIContent label, bool visible)
    27.     {
    28.         if (!visible) return false;
    29.         if (string.IsNullOrEmpty(GUIstyle))
    30.             GUI.Box(EditorGUI.IndentedRect(position), GUIContent.none);
    31.         else
    32.             GUI.Box(EditorGUI.IndentedRect(position), GUIContent.none, GUIstyle);
    33.         var offset = new RectOffset(m_Left, m_Right, m_Top, m_Bottom);
    34.         position = offset.Remove(position);
    35.         return true;
    36.     }
    37. }
    Code (CSharp):
    1.     [BoxModifier(5, 5, 5, 5, order = 1)]
    2.     [ColorModifier(1, 0, 0, 1)]
    3.     [ModifiableRange(0, 100)]
    4.     public int amount = 1;
    5.  
    The order is stack order, so the color do not change the box.
    modifier2.jpg

    Code (CSharp):
    1.     [ColorModifier(1, 1, 0, 1, order = 2)]
    2.     [BoxModifier(5, 5, 5, 5, order = 1)]
    3.     [ColorModifier(1, 0, 0, 1)]
    4.     [ModifiableRange(0, 100)]
    5.     public int amount = 1;
    6.  
    Add a color above the box.
    modifier3.jpg

    HeaderModifier:
    Code (CSharp):
    1. using UnityEditor;
    2. using UnityEngine;
    3.  
    4. public class HeaderModifierAttribute : PropertyModifierAttribute
    5. {
    6.     public string GUIstyle;
    7.  
    8.     private GUIStyle m_GUIStyle;
    9.     private float m_Height;
    10.     private string m_Header;
    11.  
    12.     public HeaderModifierAttribute(string header)
    13.     {
    14.         m_Header = header;
    15.         m_GUIStyle = string.IsNullOrEmpty(GUIstyle) ? EditorStyles.boldLabel : GUIstyle;
    16.         m_Height = m_GUIStyle.CalcSize(new GUIContent(header)).y;
    17.     }
    18.  
    19.     public override float GetHeight(SerializedProperty property, GUIContent label, float height)
    20.     {
    21.         return height + m_Height;
    22.     }
    23.  
    24.     public override bool BeforeGUI(ref Rect position, SerializedProperty property, GUIContent label, bool visible)
    25.     {
    26.         if (!visible) return false;
    27.         var rect = EditorGUI.IndentedRect(position);
    28.         rect.height = m_Height;
    29.         GUI.Label(rect, m_Header, m_GUIStyle);
    30.         position.yMin += m_Height;
    31.         return true;
    32.     }
    33. }
    Code (CSharp):
    1.     [HeaderModifier("Header", order = 3)]
    2.     [ColorModifier(1, 1, 0, 1, order = 2)]
    3.     [BoxModifier(5, 5, 5, 5, order = 1)]
    4.     [ColorModifier(1, 0, 0, 1)]
    5.     [ModifiableRange(0, 100)]
    6.     public int amount = 1;
    7.  
    modifier4.jpg

    IconModifier:
    Code (CSharp):
    1. using UnityEditor;
    2. using UnityEngine;
    3.  
    4. public class IconModifierAttribute : PropertyModifierAttribute
    5. {
    6.     private static Texture s_Icon;
    7.     private static Texture s_OldIcon;
    8.  
    9.     public IconModifierAttribute(string icon)
    10.     {
    11.         s_Icon = EditorGUIUtility.IconContent(icon).image;
    12.     }
    13.  
    14.     public override bool BeforeGUI(ref Rect position, SerializedProperty property, GUIContent label, bool visible)
    15.     {
    16.         s_OldIcon = label.image;
    17.         if (!visible) return false;
    18.         label.image = s_Icon;
    19.         return true;
    20.     }
    21.  
    22.     public override void AfterGUI(Rect position, SerializedProperty property, GUIContent label)
    23.     {
    24.         label.image = s_OldIcon;
    25.     }
    26. }
    Code (CSharp):
    1.     [HeaderModifier("Header", order = 3)]
    2.     [ColorModifier(1, 1, 0, 1, order = 2)]
    3.     [BoxModifier(5, 5, 5, 5, order = 1)]
    4.     [ColorModifier(1, 0, 0, 1)]
    5.     [IconModifier("Prefab Icon")]
    6.     [ModifiableRange(0, 100)]
    7.     public int amount = 1;
    8.  
    modifier5.jpg
     
    Last edited: Dec 22, 2017
  13. lokinwai

    lokinwai

    Joined:
    Feb 11, 2015
    Posts:
    174
    FoldoutGroupModifier:
    Code (CSharp):
    1. using UnityEditor;
    2. using UnityEngine;
    3.  
    4. public class FoldoutGroupModifierAttribute : PropertyModifierAttribute
    5. {
    6.     public string GUIstyle;
    7.  
    8.     private GUIContent m_Title;
    9.     private string[] m_Names;
    10.     private bool m_Foldout = true;
    11.     private float m_Height;
    12.  
    13.     public FoldoutGroupModifierAttribute(string title, params string[] names)
    14.     {
    15.         m_Title = new GUIContent(title);
    16.         m_Names = names;
    17.     }
    18.  
    19.     public override float GetHeight(SerializedProperty property, GUIContent label, float height)
    20.     {
    21.         m_Height = height;
    22.         height = EditorGUIUtility.singleLineHeight + 2;
    23.         if (m_Foldout)
    24.         {
    25.             height += m_Height;
    26.             foreach (var name in m_Names)
    27.             {
    28.                 var prop = property.serializedObject.FindProperty(name);
    29.                 if (prop == null) continue;
    30.                 height += EditorGUI.GetPropertyHeight(prop) + 2;
    31.             }
    32.         }
    33.         return height;
    34.     }
    35.  
    36.     public override bool BeforeGUI(ref Rect position, SerializedProperty property, GUIContent label, bool visible)
    37.     {
    38.         if (!visible) return false;
    39.  
    40.         var rect = new Rect(position);
    41.         rect.height = EditorGUIUtility.singleLineHeight;
    42.         m_Foldout = EditorGUI.Foldout(rect, m_Foldout, m_Title, true);
    43.         rect.y += rect.height + 2;
    44.  
    45.         rect.height = m_Height;
    46.         position = new Rect(rect);
    47.         rect.y += rect.height + 2;
    48.         if (m_Foldout)
    49.         {
    50.             var label2 = new GUIContent(label);
    51.             EditorGUI.indentLevel++;
    52.             foreach (var name in m_Names)
    53.             {
    54.                 var prop = property.serializedObject.FindProperty(name);
    55.                 if (prop == null) continue;
    56.                 rect.height = EditorGUI.GetPropertyHeight(prop);
    57.                 EditorGUI.PropertyField(rect, prop);
    58.                 rect.y += rect.height + 2;
    59.             }
    60.             label.text = label2.text;
    61.             label.image = label2.image;
    62.             label.tooltip = label2.tooltip;
    63.         }
    64.         return m_Foldout;
    65.     }
    66.  
    67.     public override void AfterGUI(Rect position, SerializedProperty property, GUIContent label)
    68.     {
    69.         if (m_Foldout)
    70.             EditorGUI.indentLevel--;
    71.     }
    72. }
    Code (CSharp):
    1.     [FoldoutGroupModifier("Group", "amount", order = 3)]
    2.     [ModifiableProperty]
    3.     public string title;
    4.     [HeaderModifier("Header", order = 3)]
    5.     [ColorModifier(1, 1, 0, 1, order = 2)]
    6.     [BoxModifier(5, 5, 5, 5, order = 1)]
    7.     [ColorModifier(1, 0, 0, 1)]
    8.     [IconModifier("Prefab Icon")]
    9.     [ModifiableRange(0, 100)]
    10.     [HideInInspector]
    11.     public int amount = 1;
    12.  
    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.
    modifier6.jpg

    Code (CSharp):
    1.     [FoldoutGroupModifier("Group", "amount", order = 3)]
    2.     [ColorModifier(0, 1, 1, 1, order = 2)]
    3.     [BoxModifier(2, 2, 2, 2, order = 1)]
    4.     [ModifiableProperty]
    5.     public string title;
    6.     [HeaderModifier("Header", order = 3)]
    7.     [ColorModifier(1, 1, 0, 1, order = 2)]
    8.     [BoxModifier(5, 5, 5, 5, order = 1)]
    9.     [ColorModifier(1, 0, 0, 1)]
    10.     [IconModifier("Prefab Icon")]
    11.     [ModifiableRange(0, 100)]
    12.     [HideInInspector]
    13.     public int amount = 1;
    14.  
    Add more stack together:
    modifier7.jpg

    Code (CSharp):
    1.     [BoxModifier(0, 4, 4, 4, order = 4)]
    2.     [FoldoutGroupModifier("Group", "amount", order = 3)]
    3.     [ColorModifier(0, 1, 1, 1, order = 2)]
    4.     [BoxModifier(2, 2, 2, 2, order = 1)]
    5.     [ModifiableProperty]
    6.     public string title;
    7.     [HeaderModifier("Header", order = 3)]
    8.     [ColorModifier(1, 1, 0, 1, order = 2)]
    9.     [BoxModifier(5, 5, 5, 5, order = 1)]
    10.     [ColorModifier(1, 0, 0, 1)]
    11.     [IconModifier("Prefab Icon")]
    12.     [ModifiableRange(0, 100)]
    13.     [HideInInspector]
    14.     public int amount = 1;
    15.  
    Add box around the foldout group:
    modifier8.jpg
     
    Last edited: Dec 22, 2017
  14. lokinwai

    lokinwai

    Joined:
    Feb 11, 2015
    Posts:
    174
    Decorate the fields with styles:
    upload_2017-12-23_12-57-3.png

    Code (CSharp):
    1.     [BoxModifier(0, 4, 4, 6, GUIStyle = "ChannelStripBg", order = 5)]
    2.     [FoldoutGroupModifier("Group", "amount2", order = 4)]
    3.     [SpaceModifier(6, order = 3)]
    4.     [ColorModifier(1, 0.8f, 1, 1, order = 2)]
    5.     [BoxModifier(2, 2, 2, 2, GUIStyle = "ShurikenEffectBg", order = 1)]
    6.     [ModifiableProperty]
    7.     public string title2;
    8.     [SpaceModifier(10, GUIStyle = "PR Insertion", order = 4)]
    9.     [ColorModifier(1, 1, 0.8f, 1, order = 3)]
    10.     [HeaderModifier("Header    ", GUIStyle = "flow overlay header lower left", order = 2)]
    11.     [BoxModifier(5, 5, 5, 5, GUIStyle = "flow overlay box", order = 1)]
    12.     [ColorModifier(0.2f, 0.8f, 0.6f, 1)]
    13.     [IconModifier("Prefab Icon")]
    14.     [ModifiableRange(0, 100)]
    15.     [HideInInspector]
    16.     public int amount2 = 1;
    17.  
    Added SpaceModifier:
    Code (CSharp):
    1. using UnityEditor;
    2. using UnityEngine;
    3.  
    4. public class SpaceModifierAttribute : PropertyModifierAttribute
    5. {
    6.     public string GUIStyle;
    7.  
    8.     private int m_Space;
    9.  
    10.     public SpaceModifierAttribute(int space)
    11.     {
    12.         m_Space = space;
    13.     }
    14.  
    15.     public override float GetHeight(SerializedProperty property, GUIContent label, float height)
    16.     {
    17.         return height + m_Space;
    18.     }
    19.  
    20.     public override bool BeforeGUI(ref Rect position, SerializedProperty property, GUIContent label, bool visible)
    21.     {
    22.         if (!visible) return false;
    23.         if (!string.IsNullOrEmpty(GUIStyle))
    24.             GUI.Box(EditorGUI.IndentedRect(position), GUIContent.none, GUIStyle);
    25.         position.yMin += m_Space;
    26.         return true;
    27.     }
    28. }
    Fixed a bug in HeaderModifier, GUIStyle doesn't initialized in constructor.
    Code (CSharp):
    1. using UnityEditor;
    2. using UnityEngine;
    3.  
    4. public class HeaderModifierAttribute : PropertyModifierAttribute
    5. {
    6.     public string GUIStyle;
    7.  
    8.     private GUIStyle m_GUIStyle = null;
    9.     private GUIContent m_Header;
    10.     private Vector2 m_Size;
    11.  
    12.     public HeaderModifierAttribute(string header)
    13.     {
    14.         m_Header = new GUIContent(header);
    15.     }
    16.  
    17.     public override float GetHeight(SerializedProperty property, GUIContent label, float height)
    18.     {
    19.         if (m_GUIStyle == null)
    20.         {
    21.             m_GUIStyle = string.IsNullOrEmpty(GUIStyle) ? EditorStyles.boldLabel : GUIStyle;
    22.             m_Size = m_GUIStyle.CalcSize(m_Header);
    23.         }
    24.         return height + m_Size.y;
    25.     }
    26.  
    27.     public override bool BeforeGUI(ref Rect position, SerializedProperty property, GUIContent label, bool visible)
    28.     {
    29.         if (!visible) return false;
    30.         var rect = EditorGUI.IndentedRect(position);
    31.         rect.size = m_Size;
    32.         GUI.Label(rect, m_Header, m_GUIStyle);
    33.         position.yMin += m_Size.y;
    34.         return true;
    35.     }
    36. }
     
  15. lokinwai

    lokinwai

    Joined:
    Feb 11, 2015
    Posts:
    174
    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.
     
  16. piratekittycat

    piratekittycat

    Joined:
    Dec 29, 2017
    Posts:
    2
    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?
     
  17. BinaryCats

    BinaryCats

    Joined:
    Feb 8, 2016
    Posts:
    317
    hi,

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

    Code (csharp):
    1. #if UNITY_EDITOR
    2. ...your code here
    3. #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
     
  18. piratekittycat

    piratekittycat

    Joined:
    Dec 29, 2017
    Posts:
    2
    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)
     
  19. BinaryCats

    BinaryCats

    Joined:
    Feb 8, 2016
    Posts:
    317
    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:

    Code (csharp):
    1.  
    2.   public class ColorAttribute : MultiPropertyAttribute{
    3.  //Note the constructor and the Color variable are outside the #if
    4. Color Color;
    5.   public ColorAttribute(float R, float G, float B)
    6.   {
    7.       Color = new Color(R, G, B);
    8.   }
    9. #if UNITY_EDITOR
    10. //anything that uses editor stuff needs to be within the #if (i.e. SerializedProperty)
    11.    // Draw the property inside the given rect
    12.    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    13.    {
    14.        GUI.color = Color;
    15.   }
    16. #endif
    17. }
    18.  
    19.  
    in your monobehaviour
    Code (csharp):
    1.  
    2. [Color(1,1,1)]
    3. public float MyField;
    4.  
    This code should compile for builds.

    I hope this makes sense :)
     
  20. phobos2077

    phobos2077

    Joined:
    Feb 10, 2018
    Posts:
    350
    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.
     
    yyylny and BinaryCats like this.
  21. IC_

    IC_

    Joined:
    Jan 27, 2016
    Posts:
    64
    toomasio and ModLunar like this.
  22. RChrispy

    RChrispy

    Joined:
    Dec 18, 2013
    Posts:
    71
    I hope such "stacking" functionality will be included some time in Unity directly!
     
  23. toomasio

    toomasio

    Joined:
    Nov 19, 2013
    Posts:
    198
    Also ran into this issue. Would be nice if we can stack attributes to property fields using built in Unity functions!
     
  24. Long2904

    Long2904

    Joined:
    May 13, 2019
    Posts:
    81
    Is it possible to have a property drawer and a decorator drawer at the same time on a field? For some reason, it wasn't working for me.
     
  25. Geeknerd1337

    Geeknerd1337

    Joined:
    Jun 5, 2013
    Posts:
    52
    Just reporting two years later that this hasn't been implemented. Unity really needs a decent system for this.
     
    daneobyrd and RatherGood like this.