Variable property drawer height effects all in List

Hi all, this has been bugging me for a little while now, and can’t seem to figure out a decent solution.

I’m writing a custom property drawer for an action class, which contains several foldouts. I’ve set it up so that each block of the foldout uses a float property for height that checks whether the bool for the foldout is toggled, and calculates it into the overall property height and positions of the inspector fields. This works fine when I have a single instance of the action class, but causes problems when they’re put into a list, where hitting the toggle activates the same foldout in each element in the list.

Is this a problem with using Lists with Property Drawers? Or is it my use of properties outside the OnGUI method? Or something else entirely? And is there a way to resolve it?

Thanks so much for any help or insight you might offer.

Here’s the code in question below:

[CustomPropertyDrawer(typeof(CoreCharacterAction))]
public class CoreCharacterActionDrawer : PropertyDrawer
{
	bool 
		showContent = false,
		showCosts = false,
		showDuration = false,
		showMovementSpeed = false,
		showTurningSpeed = false;

	float CostsHeight {
		get { 
			if (showCosts)
				return 45;
			else
				return 15;
		}
	}
	float DurationsHeight {
		get { 
			if (showDuration)
				return 75;
			else
				return 15;
		}
	}
	float MovementsHeight {
		get { 
			if (showMovementSpeed)
				return 75;
			else
				return 15;
		}
	}
	float TurningHeight {
		get { 
			if (showTurningSpeed)
				return 75;
			else
				return 15;
		}
	}

	public override float GetPropertyHeight (SerializedProperty property, GUIContent label) {
		float tempHeight = 15;
		if (showContent) {
			tempHeight += CostsHeight + DurationsHeight + MovementsHeight + TurningHeight;
		}
		Debug.LogWarning (tempHeight);
		return tempHeight;
	}

	public override void OnGUI (Rect position, SerializedProperty property, GUIContent label) {
		EditorGUIUtility.LookLikeControls();

		SerializedProperty 
			name = property.FindPropertyRelative ("name"),

			stateEffect = property.FindPropertyRelative ("stateEffect"),
			primaryVitalType = property.FindPropertyRelative ("primaryVitalType"),
			secondaryVitalType = property.FindPropertyRelative ("secondaryVitalType"),
			primaryCost = property.FindPropertyRelative ("primaryCost"),
			secondaryCost = property.FindPropertyRelative ("secondaryCost"),

			activationType = property.FindPropertyRelative ("activationType"),
			enterDuration = property.FindPropertyRelative ("enterDuration"),
			activeDuration = property.FindPropertyRelative ("activeDuration"),
			activeInputName = property.FindPropertyRelative ("activeInputName"),
			exitDuration = property.FindPropertyRelative ("exitDuration"),

			moveType = property.FindPropertyRelative ("moveType"),
			enterMoveSpeed = property.FindPropertyRelative ("enterMoveSpeed"),
			activeMoveSpeed = property.FindPropertyRelative ("activeMoveSpeed"),
			exitMoveSpeed = property.FindPropertyRelative ("exitMoveSpeed"),

			lookType = property.FindPropertyRelative ("lookType"),
			enterLookSpeed = property.FindPropertyRelative ("enterTurnSpeed"),
			activeLookSpeed = property.FindPropertyRelative ("activeTurnSpeed"),
			exitLookSpeed = property.FindPropertyRelative ("exitTurnSpeed");

		showContent = EditorGUI.Foldout (new Rect (position.x, 
		                                           position.y,
		                                           position.width,
		                                           15),
		                                 showContent, name.stringValue);


		if (showContent) {
			name.stringValue = EditorGUI.LabelField (EditorGUI.IndentedRect (new Rect (position.x, 
			                                                                          position.y + 15,
			                                                                          position.width,
			                                                                          15)), name.stringValue);


			showCosts = EditorGUI.Foldout (EditorGUI.IndentedRect(new Rect (position.x, 
			                                         position.y+30,
			                                         position.width,
			                                         15)),
			                               showCosts, "Resource Costs");

			if (showCosts == true) {
				EditorGUI.indentLevel = 2;
				primaryVitalType.enumValueIndex = EditorGUI.Popup (EditorGUI.IndentedRect(new Rect (position.x, 
				                                                             position.y + 30,
				                                                             position.width / 3,
				                                                             15)), 
				                                                   primaryVitalType.enumValueIndex, primaryVitalType.enumNames);

				if (primaryVitalType.enumValueIndex != 0)
					primaryCost.floatValue = EditorGUI.Slider (EditorGUI.IndentedRect(new Rect (position.x + position.width / 3, 
					                                                     position.y + 30,
					                                                     position.width * 2 / 3,
					                                                     15)), 
					                                           primaryCost.floatValue, 0, 50);
	
				secondaryVitalType.enumValueIndex = EditorGUI.Popup (EditorGUI.IndentedRect(new Rect (position.x, 
				                                                               position.y + 45,
				                                                               position.width / 3,
				                                                               15)), 
				                                                     secondaryVitalType.enumValueIndex, secondaryVitalType.enumNames);

				if (secondaryVitalType.enumValueIndex != 0)
					secondaryCost.floatValue = EditorGUI.Slider (EditorGUI.IndentedRect(new Rect (position.x + position.width / 3, 
					                                                       position.y + 45,
					                                                       position.width * 2 / 3,
					                                                       15)),
					                                             secondaryCost.floatValue, 0, 50);

				EditorGUI.indentLevel = 1;

			}

			showDuration = EditorGUI.Foldout (EditorGUI.IndentedRect(new Rect (position.x, 
			                                            position.y + CostsHeight + 15,
			                                            position.width / 3,
			                                            15)),
			                                  showDuration, "Effect Durations");
			if (showDuration == true) {
				EditorGUI.indentLevel = 2;
				activationType.enumValueIndex = EditorGUI.Popup (EditorGUI.IndentedRect (new Rect (position.x, 
				                                                 position.y + CostsHeight + 30,
				                                                 position.width,
				                                                 15)), 
				                                                 activationType.enumValueIndex, activationType.enumNames);
				enterDuration.floatValue = EditorGUI.Slider (EditorGUI.IndentedRect(new Rect (position.x, 
				                                                       position.y + CostsHeight + 45,
				                                                       position.width,
				                                                       15)),
				                                             "startup duration", enterDuration.floatValue, 0, 5);
				if (activationType.enumValueIndex == 0)
					activeDuration.floatValue = EditorGUI.Slider (EditorGUI.IndentedRect(new Rect (position.x, 
					                                                              position.y + CostsHeight + 60,
					                                                              position.width,
					                                                              15)),
					                                                    "active duration", activeDuration.floatValue, 0, 5);
				else
					activeInputName.enumValueIndex = EditorGUI.Popup (EditorGUI.IndentedRect(new Rect (position.x, 
					                                                                  position.y + CostsHeight + 60,
					                                                                  position.width,
					                                                                  15)),
					                                                        "active input", activeInputName.enumValueIndex, activeInputName.enumNames);

				exitDuration.floatValue = EditorGUI.Slider (EditorGUI.IndentedRect(new Rect (position.x, 
				                                                            position.y + CostsHeight + 75,
				                                                            position.width,
				                                                            15)),
				                                                  "cooldown duration", exitDuration.floatValue, 0, 5);
				EditorGUI.indentLevel = 1;
			}

			showMovementSpeed = EditorGUI.Foldout (EditorGUI.IndentedRect(new Rect (position.x, 
			                                                                        position.y + CostsHeight + DurationsHeight + 15,
			                                                                        position.width,
			                                                                        15)),
			                                       showMovementSpeed, "Movement Speeds");
			if (showMovementSpeed) { 
				EditorGUI.indentLevel = 2;
				moveType.enumValueIndex = EditorGUI.Popup (EditorGUI.IndentedRect (new Rect (position.x, 
				                                                                             position.y + CostsHeight + DurationsHeight + 30,
				                                                                             position.width,
				                                                                             15)),
				                                           moveType.enumValueIndex, moveType.enumNames);

				enterMoveSpeed.floatValue = EditorGUI.Slider (EditorGUI.IndentedRect (new Rect (position.x, 
				                                                                                      position.y + CostsHeight + DurationsHeight + 45,
				                                                                                      position.width,
				                                                                                      15)),
				                                                    enterMoveSpeed.floatValue, 0, 25);

				activeMoveSpeed.floatValue = EditorGUI.Slider (EditorGUI.IndentedRect (new Rect (position.x, 
				                                                                                       position.y + CostsHeight + DurationsHeight + 60,
				                                                                                       position.width,
				                                                                                       15)),
				                                                     activeMoveSpeed.floatValue, 0, 25);

				exitMoveSpeed.floatValue = EditorGUI.Slider (EditorGUI.IndentedRect (new Rect (position.x, 
				                                                                                     position.y + CostsHeight + DurationsHeight + 75,
				                                                                                     position.width,
				                                                                                     15)),
				                                                   exitMoveSpeed.floatValue, 0, 25);
				EditorGUI.indentLevel = 1;
			}
			showTurningSpeed = EditorGUI.Foldout (EditorGUI.IndentedRect(new Rect (position.x, 
			                                                                       position.y + CostsHeight + DurationsHeight + MovementsHeight + 15,
			                                                                       position.width,
			                                                                       15)),
			                                      showTurningSpeed, "Turn Speeds");
			if (showTurningSpeed) {
				EditorGUI.indentLevel = 2;
				lookType.enumValueIndex = EditorGUI.Popup (EditorGUI.IndentedRect(new Rect (position.x, 
				                                                                            position.y + CostsHeight + DurationsHeight + MovementsHeight + 30,
				                                                                            position.width,
				                                                                            15)),
				                                           lookType.enumValueIndex, lookType.enumNames);
				enterLookSpeed.floatValue = EditorGUI.Slider (EditorGUI.IndentedRect(new Rect (position.x, 
				                                                                               position.y + CostsHeight + DurationsHeight + MovementsHeight + 45,
				                                                                               position.width,
				                                                                               15)),
				                                              enterLookSpeed.floatValue, 0, 25);
				activeLookSpeed.floatValue = EditorGUI.Slider (EditorGUI.IndentedRect(new Rect (position.x, 
				                                                                                position.y + CostsHeight + DurationsHeight + MovementsHeight + 60,
				                                                                                position.width,
				                                                                                15)),
				                                               activeLookSpeed.floatValue, 0, 25);
				exitLookSpeed.floatValue = EditorGUI.Slider (EditorGUI.IndentedRect(new Rect (position.x, 
				                                                                              position.y + CostsHeight + DurationsHeight + MovementsHeight + 75,
				                                                                              position.width,
				                                                                              15)),
				                                             exitLookSpeed.floatValue, 0, 25);
				EditorGUI.indentLevel = 1;
			}
		}
	}
}

I’d recommend you pull your SerializedProperties into class scope and initialize them in the GetPropertyHeight method. Then you’d have access to their corresponding isExpanded property which you’d use instead of manually managing your show_ booleans. You could pick a single property in your desired blocks to keep track of the foldout states.

I’m pretty sure something like this should work:

[CustomPropertyDrawer(typeof(CoreCharacterAction))]
public class CoreCharacterActionDrawer : PropertyDrawer
{
    private SerializedProperty
        name,
        primaryVitalType,
        secondaryVitalType,
        primaryCost,
        secondaryCost;

    float CostsHeight
    {
        get
        {
            if(primaryVitalType.isExpanded)
                return 45;
            else
                return 15;
        }
    }

    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        name = property.FindPropertyRelative("name");

        primaryVitalType = property.FindPropertyRelative("primaryVitalType");
        secondaryVitalType = property.FindPropertyRelative("secondaryVitalType");
        primaryCost = property.FindPropertyRelative("primaryCost");
        secondaryCost = property.FindPropertyRelative("secondaryCost");

        float tempHeight = 15;
        if(name.isExpanded)
        {
            tempHeight += CostsHeight /* + ... + ... */;
        }
        Debug.LogWarning(tempHeight);
        return tempHeight;
    }

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        EditorGUIUtility.LookLikeControls();

        name.isExpanded = EditorGUI.Foldout(new Rect(position.x, position.y, position.width, 15), name.isExpanded, name.stringValue);

        if(name.isExpanded)
        {
            //...

            primaryVitalType.isExpanded = EditorGUI.Foldout(EditorGUI.IndentedRect(new Rect(position.x, position.y + 30, position.width, 15)), primaryVitalType.isExpanded, "Resource Costs");

            if(primaryVitalType.isExpanded == true)
            {
                //...
            }
        }
    }
}

Failing that, I usually add the booleans to the actual object - encapsulating them in #if UNITY_EDITOR … #endif directives.

This may have been added to Unity recently (This post is over two years old by this point!), but it took me a while to discover it buried in the documentation - posting it here for posterity;

EditorGUI.GetPropertyHeight(SerializedProperty property, UIContent label = null, bool includeChildren = true);

This function will return the height of a SerializedProperty object, as used by Editor GUI functions. It also takes into account whether or not a list/array is expanded.

All you need to really do is use the “isExpanded” field on the SerializedProperty:

for example as such:

public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
    property.isExpanded = EditorGUI.Foldout(currentRect, property.isExpanded, "Sound");

    if (property.isExpanded) {
       ...
    }
}

I believe the problem is that all instances of CoreCharacterAction, including each one in the list, are drawn with the same instance of the CoreCharacterActionDrawer – so any member variables of the property drawer (showContent, etc.) are shared across the drawing of all the properties.

What you could do is keep track of the member variables for each property instance being drawn, so replace this:

bool 
       showContent = false,
       showCosts = false,
       showDuration = false,
       showMovementSpeed = false,
       showTurningSpeed = false;

with this:

private static class ShowInfo
{
    public bool 
       showContent = false,
       showCosts = false,
       showDuration = false,
       showMovementSpeed = false,
       showTurningSpeed = false;
}

private Dictionary<SerializedProperty,ShowInfo> InfoPerProperty = new Dictionary<SerializedProperty,ShowInfo>();

Then in the OnGUI method …

ShowInfo showInfo;
if (!InfoPerProperty.TryGetValue(property, out showInfo))
{
    showInfo = new ShowInfo();
    InfoPerProperty.Add(property, showInfo);
}

...

if (showInfo.showContent)
{
    ....
}

Basically this maintains a dictionary of info to show for each SerializedProperty that is edited by the property drawer. In the OnGUI method it looks up the info for the current property. If not found it creates a new info instance.

Hope that helps.