How to edit array/list property with custom PropertyDrawer?
This is not about custom inspectors for arrays/lists in general (I assume/hope that I already know how to do that). I am having a specifically weird behaviour only when using arrays/lists as nested properties.
In brief: Using EditorGUI.PropertyField on a SerializedProperty (which is actually a string[]) won't display a proper array/list inspector element. Instead, I am only seeing a dull "foldout icon".
And In details...
I am using a custom PropertyDrawer (see source code below) for handling a simple custom data class instance inside the Inspector.
My approach seems to be very simple, mainly follwowing the Unity docs at: https://docs.unity3d.com/ScriptReference/PropertyDrawer.html
Here's my"data item class":
[Serializable]
public class ThesaurusItem
{
public string word;
public string[] syllables;
}
The issue goes with its "syllables" property (string[]). The data class is used as a plain and straight property inside the surrounding MonoBehaviour class:
public class Thesaurus : MonoBehaviour
{
public ThesaurusItem testItem;
}
Here's my issue: Any array or list type properties like "syllables" are only displayed as a "foldout" icon - and otherwise emtpy. In particular, there's no "size" input field at all.
This is what I'd expect to see in the Inspector (which is what I get for a simple public string[] class property):
And here's what I actually get:
As soon as my string[] property is handled as part of the data item class by the custom drawer, it is nothing more than a "foldout" icon - with nothing else otherwise. I can click it (i.e. it turns into a downward arrow), but it doesn't expand anything.
Of course my array property is still empty, so I don't expect to see any content. But I would expect to see at least the "size" field.
Finally, here's my PropertyDrawer:
// Note that this is more or less identical to Unity's example code here: https://docs.unity3d.com/ScriptReference/PropertyDrawer.html
[CustomPropertyDrawer(typeof(ThesaurusItem))]
public class ThesaurusItemDrawer : PropertyDrawer
{
// Draw the property inside the given rect
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
// Using BeginProperty / EndProperty on the parent property means that
// prefab override logic works on the entire property.
EditorGUI.BeginProperty(position, label, property);
// Draw label
position = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label);
// Don't make child fields be indented
var indent = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
// Calculate rects
Rect wordRect = new Rectposition.x, position.y, 90, position.height);
Rect syllablesRect = new Rect(wordRect.position.x + wordRect.width + 20, position.y, 50, position.height);
// Draw fields - pass GUIContent.none to each so they are drawn without labels
EditorGUI.PropertyField(wordRect, property.FindPropertyRelative("word"), GUIContent.none);
EditorGUI.PropertyField(syllablesRect, property.FindPropertyRelative("syllables"), GUIContent.none, true);
// Set indent back to what it was
EditorGUI.indentLevel = indent;
EditorGUI.EndProperty();
}
}
Any ideas what's going wrong here?
Answer by coffiarts · Jan 04, 2019 at 08:38 AM
I've found a workaround for my own (admittedly quite lenghty and complicated) question, so I'd like to share it myself.
First of all: Unity doesn't seem to support custom drawers for arrays and lists themselves. See this older post from a Unity staff member Literally:
You can't make a PropertyDrawer for arrays or generic lists themselves. [...] On the plus side, elements inside arrays and lists do work with PropertyDrawers.
The solution sounds simple: Wrapping arrays/lists in a parent class and creating a drawer for the parent.
This works fine (in terms that the drawer will actually be applied to the array/list). The downside is that the individual elements of the array/list won't be automatically rendered, neither the "size" field. You still have to implement this manually (by looping etc.). Especially the handling of the array size looks a bit nasty, as you need to track the size in a separate int property (see "arrSize" below) and listen to changes to it.
My solution looks like this now:
Custom Wrapper class plus Drawer for a plain string[] property:
// Encapsulation of a plain string[] inside a Property, so that it can be used together with a custom PropertyDrawer (StringArrayAttributeDrawer)
[Serializable]
public class StringArrayEditorAttribute : PropertyAttribute
{
public static readonly int INITIAL_SIZE = 1;
public string[] values;
public StringArrayEditorAttribute()
{
values = new string[INITIAL_SIZE];
for (int i = 0; i < INITIAL_SIZE; i++)
{
values[i] = "";
}
}
}
[CustomPropertyDrawer(typeof(StringArrayEditorAttribute))]
public class StringArrayEditorAttributeDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
SerializedProperty arrayProp = property.FindPropertyRelative("values");
for (int i = 0; i < arrayProp.arraySize; i++)
{
// This will display an Inspector Field for each array item (layout this as desired)
SerializedProperty value = arrayProp.GetArrayElementAtIndex(i);
EditorGUI.PropertyField(position, value, GUIContent.none);
}
}
}
Use this property inside your surrounding class instead of a string[]:
[Serializable]
public class MyListItem : PropertyAttribute
{
public StringArrayEditorAttribute stringArrProp = new StringArrayEditorAttribute();
public int arrSize; // necessary for handling changes to the array's size!
}
Inside the Drawer for the surrounding class, it's sufficient to pass the property into a regular PropertyField
[CustomPropertyDrawer(typeof(ThesaurusItem))]
public class ThesaurusItemDrawer : PropertyDrawer
{
[...]
EditorGUI.LabelField(someRect, new GUIContent("size"));
[...]
EditorGUI.PropertyField(someRect, property.FindPropertyRelative("arrSize"), GUIContent.none);
[...]
EditorGUI.PropertyField(someRect, property.FindPropertyRelative("stringArrProp"), GUIContent.none);
}
Now for the ugly part: Keeping track of array size changes...
I found no better way for this than using OnValidate() in the surrounding (i.e. the inspected) MonoBehaviour component (credits to user Edy for his solution proposed here): whenever anything changes in the inspector, we need to check any of the array items for a change in arrSize, and then adjust the array length accordingly (very nasty, but it works!):
public class MyComponent : MonoBehaviour
{
OnValidate()
{
foreach (MyListItem item in someParentList)
{
// Do we need to adjust the array to a new size?
if (item.arrSize != item.stringArrProp.values.Length)
{
string[] newArray = new string[item.arrSize];
for (int i = 0; i < newArray.Length; i++)
{
if (item.stringArrProp.values.Length > i)
{
newArray[i] = item.stringArrProp.values[i];
}
else
{
newArray[i] = "";
}
}
item.stringArrProp.values = newArray;
}
}
}
}
Answer by ShawnFeatherly · Aug 15, 2019 at 12:26 AM
Similar to @coffiarts 's answer, here's a pretty clean example of wrapping an array type, such as string[], in another class. https://github.com/cfoulston/Unity-Reorderable-List/blob/master/Example/Example.cs From what I can tell, he found a way around the array size check requirement being in OnValidate().
Answer by tomekkie · Aug 04, 2021 at 06:17 AM
Looks like this is not a problem in the recent versions of Unity any more. I have run into this issue after importing the CustomPropertyDrawer from 2020.2.2f1 - where it is working properly on lists or arrays without any custom wrapper - to 2020.1.0f1 - where it is not working.
Your answer
Follow this Question
Related Questions
Popup to choose which child class I want in Custom Editor Inspector 0 Answers
Property Drawer ArgumentException 1 Answer
Is there a way to live-update script-controlled UI formatting in the editor? 0 Answers
Displaying recursive types in inspector (Custom Editor) 0 Answers
Custom Inspector for Particle System 2 Answers