Loading...

UNITY 3D: Extending The Editor part 3 - Reorderable List

Intro


In the previous part, I discussed how to set up a Custom Editor for a ScriptableObject. If you want to check it out, you can do so here. I will be using the data setup that I created in the previous part, but it is not necessary to be able to follow along.

To visualize the data that we keep in that ScriptableObject data asset, we will overwrite the default inspector list view with UnityEditorInternal's Reorderable List.

 

The Reorderable List was added as a feature since Unity 4.5, and you can find it in the UnityEditorInternal namespace. However, this feature is not documented and therefore not used to its full potential, in my opinion. The only downside to this type of list is that it cannot be used in the default Inspector Window and can only be used in Custom Editor's, Custom Windows or Property Drawers. The upside is that its exactly what we're doing here!

 

Fast Travel


If you are only interested in a specific part, you can skip to the section you'd like here. I do however use the same project and files during the entirety of this series. The project's source can be found in the last part of this series.

 

 

End Result


 

 

Implementation


I will continue to work in the same Custom Editor that we used in the previous part, which can be found here. Below you can find a ready to use implementation of the Reorderable List. I will go into more detail about the basics of the Reorderable List after this implementation.

using UnityEditor;
using UnityEditorInternal;
using UnityEngine;

[CustomEditor(typeof(TutorialShortcutData))]
public class TutorialShortcutCustomEditor : Editor
{
    private SerializedProperty m_shortcutData;
    private ReorderableList m_ReorderableList;
    
    private void OnEnable()
    {
        //Find the list in our ScriptableObject script.
        m_shortcutData = serializedObject.FindProperty("shortcuts");
        
        //Create an instance of our reorderable list.
        m_ReorderableList = new ReorderableList(serializedObject: serializedObject, elements: m_shortcutData, draggable: true, displayHeader: true,
            displayAddButton: true, displayRemoveButton: true);

        //Set up the method callback to draw our list header
        m_ReorderableList.drawHeaderCallback = DrawHeaderCallback;

        //Set up the method callback to draw each element in our reorderable list
        m_ReorderableList.drawElementCallback = DrawElementCallback;

        //Set the height of each element.
        m_ReorderableList.elementHeightCallback += ElementHeightCallback;

        //Set up the method callback to define what should happen when we add a new object to our list.
        m_ReorderableList.onAddCallback += OnAddCallback;
    }
    
    /// <summary>
    /// Draws the header for the reorderable list
    /// </summary>
    /// <param name="rect"></param>
    private void DrawHeaderCallback(Rect rect)
    {
        EditorGUI.LabelField(rect, "Shortcuts");
    }
    
    /// <summary>
    /// This methods decides how to draw each element in the list
    /// </summary>
    /// <param name="rect"></param>
    /// <param name="index"></param>
    /// <param name="isactive"></param>
    /// <param name="isfocused"></param>
    private void DrawElementCallback(Rect rect, int index, bool isactive, bool isfocused)
    {
        //Get the element we want to draw from the list.
        SerializedProperty element = m_ReorderableList.serializedProperty.GetArrayElementAtIndex(index);
        rect.y += 2;
    
        //We get the name property of our element so we can display this in our list.
        SerializedProperty elementName = element.FindPropertyRelative("m_Name");
        string elementTitle = string.IsNullOrEmpty(elementName.stringValue)
            ? "New Shortcut"
            : $"Shortcut: {elementName.stringValue}";

        //Draw the list item as a property field, just like Unity does internally.
        EditorGUI.PropertyField(position:
            new Rect(rect.x += 10, rect.y, Screen.width * .8f, height: EditorGUIUtility.singleLineHeight), property:
            element, label: new GUIContent(elementTitle), includeChildren: true);
    }
    
    /// <summary>
    /// Calculates the height of a single element in the list.
    /// This is extremely useful when displaying list-items with nested data. 
    /// </summary>
    /// <param name="index"></param>
    /// <returns></returns>
    private float ElementHeightCallback(int index)
    {
        //Gets the height of the element. This also accounts for properties that can be expanded, like structs.
        float propertyHeight =
            EditorGUI.GetPropertyHeight(m_ReorderableList.serializedProperty.GetArrayElementAtIndex(index), true);
        
        float spacing = EditorGUIUtility.singleLineHeight / 2;
        
        return propertyHeight + spacing;
    }
    
    /// <summary>
    /// Defines how a new list element should be created and added to our list.
    /// </summary>
    /// <param name="list"></param>
    private void OnAddCallback(ReorderableList list)
    {
        var index = list.serializedProperty.arraySize;
        list.serializedProperty.arraySize++;
        list.index = index;
        var element = list.serializedProperty.GetArrayElementAtIndex(index);
    }

    /// <summary>
    /// Draw the Inspector Window
    /// </summary>
    public override void OnInspectorGUI()
    {
        serializedObject.Update();

        m_ReorderableList.DoLayoutList();

        serializedObject.ApplyModifiedProperties();
    }
}

 

Creating the Reorderable List

Creating the list is relatively straightforward. All we need to do is create an instance of the list and pass a couple of parameters in the constructor. The list needs to know for what SerializedObject it needs to draw, the elements in the list we want to display and some options.

The rest of the setup is done by subscribing to a couple of callbacks, which is how the Reorderable List determines how to draw itself. I recommend implementing at least the callbacks used in this example, as they add all the necessary functionality. There are more callbacks to use, but not necessarily essential for the correct functioning of the Reorderable List. I will put a link to them in the Further Readings section.

private void OnEnable()
{
    //Find the list in our ScriptableObject script.
    m_shortcutData = serializedObject.FindProperty("shortcuts");
    
    //Create an instance of our reorderable list.
    m_ReorderableList = new ReorderableList(serializedObject: serializedObject, elements: m_shortcutData, draggable: true, displayHeader: true,
        displayAddButton: true, displayRemoveButton: true);
​
    //Set up the method callback to draw our list header
    m_ReorderableList.drawHeaderCallback = DrawHeaderCallback;
​
    //Set up the method callback to draw each element in our reorderable list
    m_ReorderableList.drawElementCallback = DrawElementCallback;
​
    //Set the height of each element.
    m_ReorderableList.elementHeightCallback += ElementHeightCallback;
​
    //Set up the method callback to define what should happen when we add a new object to our list.
    m_ReorderableList.onAddCallback += OnAddCallback;
}

 

Drawing each list element

This callback gives you control over how a list item is drawn to the inspector. All GUI(Layout) and EditorGUI(Layout) classes can be used to draw controls, just like we're used to in standard editor scripting. I do however recommend that you use the non-layout versions because you are given a Rect to work with. Choosing layout over non-layouted editor GUI will result in weird positioning in the inspector window.

private void DrawElementCallback(Rect rect, int index, bool isactive, bool isfocused)
{
    //Get the element we want to draw from the list.
    SerializedProperty element = m_ReorderableList.serializedProperty.GetArrayElementAtIndex(index);
    rect.y += 2;
    
    //We get the name property of our element so we can display this in our list.
    SerializedProperty elementName = element.FindPropertyRelative("m_Name");
    string elementTitle = string.IsNullOrEmpty(elementName.stringValue)
        ? "New Shortcut"
        : $"Shortcut: {elementName.stringValue}";
​
    //Draw the list item as a property field, just like Unity does internally.
    EditorGUI.PropertyField(position:
            new Rect(rect.x += 10, rect.y, Screen.width * .8f, height: EditorGUIUtility.singleLineHeight), property:
            element, label: new GUIContent(elementTitle), includeChildren: true);
}

Because we use a SerializedProperty to hold our list data, we draw a EditorGUI.PropertyField to display the entire property. The cool part about this is that Unity's internal serialization system handles serialized properties. Therefore dirtying, saving, and undo/redo events are automatically being handled for us.

 

Adding an element to the list

When clicking the "Add" (or "+") button, the onAddCallback gets invoked. In this callback, we need to define how a new element should be added to the list.

private void OnAddCallback(ReorderableList list)
{
    //Insert an extra item add the end of our list.
    var index = list.serializedProperty.arraySize;
    list.serializedProperty.arraySize++;
    list.index = index;

    //If we want to do anything with the item we just added, 
    //We can create reference by using this method
    var element = list.serializedProperty.GetArrayElementAtIndex(index);
}

 

Calculating heights

Another important callback is the ElementHeightCallback(int index). This method calculates the height of an element. If you are using list items that only consist of single line variables, you're probably best to not use this callback because the Reorderable List draws each item with a singleLineHeight by default.

If you are using structs or multiline data/variables, you need to calculate the line height of each element by yourself. Otherwise, you get results like this:

Which is not what we want.

To calculate the correct height, we use a method called EditorGUI.GetPropertyHeight(), which gets the height of a given property. I added a single line half height as extra spacing to give it a more clean look.

private float ElementHeightCallback(int index)
{
    //Gets the height of the element. This also accounts for properties that can be expanded, like structs.
    float propertyHeight = EditorGUI.GetPropertyHeight(m_ReorderableList.serializedProperty.GetArrayElementAtIndex(index), true);
    
    float spacing = EditorGUIUtility.singleLineHeight / 2;
    
    return propertyHeight + spacing;
}

After calculating, it now looks like this:

 

 

Additional callbacks

The Reorderable List gives us several other callbacks that can be useful on certain occasions. Although I will not be using them in this tutorial, it is worth listing them here.

 

onReorderCallbackWithDetails

When reordering elements in the list, this event is invoked. This event has some extra parameters which contain the list itself, the old index of the element dragged, and the new index of the element dragged.

 

onSelectCallback

This event is invoked whenever a list item gets selected.

 

onRemoveCallback

This event is invoked when an element gets removed from the list. If you want to override the default behavior when removing items, you can do so here.

 

onCanAddCallback

This event gets invoked just before the onAddCallback. Here you can decide whether it is allowed to add a specific element to the list. Return true if allowed, otherwise false.

 

onCanRemoveCallback

Basically the same as the onCanAddCallback, but instead controls whether an element can be removed from the list.

 

onChangedCallback

Whenever something changes in the list (element added, removed or reordered), this callback gets invoked.

 

Conclusion


Now that you know how to create a Reorderable List, managing your list data will be much easier. In the next part, I will explain how to create a custom property drawer and a custom control to go with that drawer. Drawers are handy if you're looking to customize the looks of a single field in the inspector. It can, however, also be used in combination with custom editors.

If you have any questions or feedback, please feel free to email me or post a comment in the comment section down below.

How to set up a custom property drawer and custom IMGUI control

`

Further readings and references


Be the first to comment

Post Comment

This website uses cookies to ensure you get the best experience on my website