Search Unity

Editor GUI Foldout styling does not apply to foldout headers

Discussion in 'Immediate Mode GUI (IMGUI)' started by WallaceT_MFM, Jan 21, 2019.

  1. WallaceT_MFM

    WallaceT_MFM

    Joined:
    Sep 25, 2017
    Posts:
    264
    Hi, I'm trying to make an attribute that I can apply to public variables in scripts to make their inspector labels change color. I have a solution that works for everything except for variables that are drawn as foldouts, such as arrays and custom classes. Here's my code so far
    Code (CSharp):
    1. public class LabelColorAttribute: PropertyAttribute
    2. {
    3.     public float R { get; private set; }
    4.     public float G { get; private set; }
    5.     public float B { get; private set; }
    6.  
    7.     public LabelColorAttribute(float r = 0.5f, float g = 0.5f, float b = 0.5f)
    8.     { R = r; G = g; B = b; }
    9. }
    Code (CSharp):
    1. [CustomPropertyDrawer(typeof(LabelColorAttribute))]
    2. public class LabelColorDrawer : PropertyDrawer
    3. {
    4.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    5.     {
    6.         var att = ((LabelColorAttribute)this.attribute);
    7.  
    8.         Color original = EditorStyles.label.normal.textColor;
    9.         Color newColor = new Color(att.R, att.G, att.B);
    10.  
    11.         SetColor(newColor);
    12.         EditorGUI.PropertyField(position, property, label, true);
    13.         SetColor(original);
    14.     }
    15.  
    16.     private void SetColor(Color color)
    17.     {
    18.         SetStyleColor(EditorStyles.label, color);
    19.         SetStyleColor(EditorStyles.foldout, color);
    20.         SetStyleColor(EditorStyles.foldoutPreDrop, color);
    21.     }
    22.  
    23.     private void SetStyleColor(GUIStyle style, Color color)
    24.     {
    25.         style.normal.textColor =
    26.             style.onNormal.textColor =
    27.             style.active.textColor =
    28.             style.onActive.textColor =
    29.             style.focused.textColor =
    30.             style.onFocused.textColor =
    31.             style.hover.textColor =
    32.             style.onHover.textColor = color;
    33.     }
    34. }
    Code (csharp):
    1. // Usage examples
    2. [LabelColor(1,0,0)] public string dialogKey = "Some string";
    3. [LabelColor(0.5f,0.5f,1.0f)] public string[] randomKeys;
    The problem is with the header lines for the foldout, as seen here:

    Any ideas on how to make the header lines accept the style?
    ========================================================================
    ========================================================================
    EDIT: I'm documenting my research here, in case it proves useful to someone in the future. Right now, I still haven't found a solution.
    After some experimentation doing things I know I shouldn't be doing, I've changed my drawer to this
    Code (CSharp):
    1. public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    2.     {
    3.         var att = ((LabelColorAttribute)this.attribute);
    4.  
    5.         Color original = EditorStyles.label.normal.textColor;
    6.         Color newColor = new Color(att.R, att.G, att.B);
    7.  
    8.         SetColor(newColor);
    9.         EditorGUI.PropertyField(position, property, label, true);
    10.         //SetColor(original);
    11.     }
    12.  
    13.     private void SetColor(Color color)
    14.     {
    15.         SetStyleColor(EditorStyles.label, color);
    16.         SetStyleColor(EditorStyles.foldout, Color.green);
    17.         SetStyleColor(EditorStyles.foldoutPreDrop, Color.yellow);
    18.     }
    19.  
    20. // Usage
    21. [LabelColor(1,0,0)] public string dialogKey = "Some string";
    22. [LabelColor(0,0,1)] public string[] randomKeys; // Changed this to a pure blue
    Note the manual override for the colors in SetColor. SetStyleColor is the same as before. Also note that the color resetting code in OnGUI is commented out.
    After restarting Unity (so that previous style changes are flushed), the result looks like this


    So, there looks to be some caching going on. Even though the EditorStyles.label style is being changed before EditorGUI.PropertyField is called, the Size label is still using the color from a different instance of the property. More to follow soon.
    =======================================================================
    =======================================================================
    My quest has taken me to reading Unity's reference source code, specifically EditorGUI.DefaultPropertyField() (ref:https://github.com/Unity-Technologies/UnityCsReference/blob/master/Editor/Mono/EditorGUI.cs). I found this section of code
    Code (CSharp):
    1. // Handle the actual foldout first, since that's the one that supports keyboard control.
    2.                 // This makes it work more consistent with PrefixLabel.
    3.                 childrenAreExpanded = property.isExpanded;
    4.  
    5.                 bool newChildrenAreExpanded = childrenAreExpanded;
    6.                 using (new DisabledScope(!property.editable))
    7.                 {
    8.                     GUIStyle foldoutStyle = (DragAndDrop.activeControlID == -10) ? EditorStyles.foldoutPreDrop : EditorStyles.foldout;
    9.                     newChildrenAreExpanded = Foldout(position, childrenAreExpanded, s_PropertyFieldTempContent, true, foldoutStyle);
    10.                 }
    11.  
    12.  
    13.                 if (childrenAreExpanded && property.isArray && property.arraySize > property.serializedObject.maxArraySizeForMultiEditing && property.serializedObject.isEditingMultipleObjects)
    14.                 {
    15.                     Rect boxRect = position;
    16.                     boxRect.xMin += EditorGUIUtility.labelWidth - indent;
    17.  
    18.                     s_ArrayMultiInfoContent.text = s_ArrayMultiInfoContent.tooltip = string.Format(s_ArrayMultiInfoFormatString, property.serializedObject.maxArraySizeForMultiEditing);
    19.                     LabelField(boxRect, GUIContent.none, s_ArrayMultiInfoContent, EditorStyles.helpBox);
    20.                 }
    I haven't figured out why this wouldn't work the way I'm expecting. You can see that EditorStyles.foldoutPreDrop, EditorStyles.foldout, and EditorStyles.helpBox. I tried switching EditorStyles.helpBox's text color, but no luck. I followed the code to EditorStyles.FoldoutInternal() and found this section of code:
    Code (CSharp):
    1. internal static bool FoldoutInternal(Rect position, bool foldout, GUIContent content, bool toggleOnLabelClick, GUIStyle style)
    2.         {
    3.             Rect origPosition = position;
    4. // ... ...
    5.                 case EventType.Repaint:
    6.                     EditorStyles.foldoutSelected.Draw(position, GUIContent.none, id, s_DragUpdatedOverID == id);
    7.  
    8.                     Rect drawRect = new Rect(position.x + indent, position.y, EditorGUIUtility.labelWidth - indent, position.height);
    9.  
    10.                     // If mixed values, indicate it in the collapsed foldout field so it's easy to see at a glance if anything
    11.                     // in the Inspector has different values. Don't show it when expanded, since the difference will be visible further down.
    12.                     if (showMixedValue && !foldout)
    13.                     {
    14.                         style.Draw(drawRect, content, id, false);
    15.  
    16.                         BeginHandleMixedValueContentColor();
    17.                         Rect fieldPosition = origPosition;
    18.                         fieldPosition.xMin += EditorGUIUtility.labelWidth;
    19.                         EditorStyles.label.Draw(fieldPosition, s_MixedValueContent, id, false);
    20.                         EndHandleMixedValueContentColor();
    21.                     }
    22.                     else
    23.                     {
    24.                         style.Draw(drawRect, content, id, foldout);
    25.                     }
    26.                     break;
    From here, I noticed the expected use of EditorStyles.label and the passed foldout style (usually EditorStyles.foldout). However, there's also the unexpected EditorStyles.foldoutSelected, which seems to be an internal style that can't be changed (except through reflection). Curious, this lead me to look into the EditorStyles class (ref: https://github.com/Unity-Technologies/UnityCsReference/blob/master/Editor/Mono/GUI/EditorStyles.cs). And what have we here?
    Code (CSharp):
    1. // the editor styles currently in use
    2.         internal static EditorStyles s_Current;
    3.  
    4.         // the list of editor styles to use
    5.         private static EditorStyles[] s_CachedStyles = { null, null };
    6.  
    7.         internal static void UpdateSkinCache()
    8.         {
    9.             UpdateSkinCache(EditorGUIUtility.skinIndex);
    10.         }
    11.  
    12.         internal static void UpdateSkinCache(int skinIndex)
    13.         {
    14.             // Don't cache the Game GUISkin styles
    15.             if (GUIUtility.s_SkinMode == 0)
    16.                 return;
    17.  
    18.             if (s_CachedStyles[skinIndex] == null)
    19.             {
    20.                 s_CachedStyles[skinIndex] = new EditorStyles();
    21.                 s_CachedStyles[skinIndex].InitSharedStyles();
    22.             }
    23.  
    24.             s_Current = s_CachedStyles[skinIndex];
    25.             EditorGUIUtility.s_FontIsBold = -1;
    26.             EditorGUIUtility.SetBoldDefaultFont(false);
    27.         }
    So there is style caching going on. As of this writing, I haven't figured out where exactly the issue is coming from. My investigation continues...
    =======================================================================
    =======================================================================
    A breakthrough of sorts! I've discovered that a custom property drawer is executed once per element of an array, not on the array itself, which is what's causing the problem. This can be seen by drawing a single label in the property drawer, and then noticing that each element in the array displays the label instead of writing over the whole array with one label. Other people seem to have found this (ref:https://forum.unity.com/threads/custom-attribute-an-array-size-limiter.379452/), but I didn't know to look for it until I found it.
    From https://unity3d.com/unity/whats-new/unity-4.3

    "Editor: PropertyDrawer attributes on members that are arrays are now applied to each element in the array rather than the array as a whole. This was always the intention since there is no other way to apply attributes to array elements, but it didn't work correctly before. Apologies for the inconvenience to anyone who relied on the unintended behavior for custom drawing of arrays."

    So, it looks like there is no simple solution, by design. I'm going to investigate overriding the default drawers as see if I can do everything the same, except for array handling. I found this article about the topic, which may be useful https://medium.com/developers-writi...-and-lists-in-unity3d-by-default-e4fba13d1b50
    The article points to this repo, which has code for overriding the default array inspector with reorderable arrays: https://gist.github.com/t0chas/34afd1e4c9bc28649311
     
    Last edited: Jan 22, 2019