Populating the drop-down add menu with types

Rotorz ReorderableList

Populating the drop-down add menu with types

Utility functionality is provided to assist when building menus that contain a number of addable element types that implement some interface or base type; this is referred to as the element contract type.

Let's begin by defining the base class that each of our elements must be derived from:

// ExampleNode.cs
using UnityEngine;

public abstract class ExampleNode : ScriptableObject {

    [SerializeField]
    private string _displayName;

    public string DisplayName {
        get { return _displayName; }
        set { _displayName = value; }
    }

}
// ExampleNode.js
class ExampleNode extends ScriptableObject {

    @SerializeField
    private var _displayName:String;

    function get DisplayName():String { return _displayName; }
    function set DisplayName(value:String) { _displayName = value; }

}

We then need some sort of container wherein our node instances will be stored. In the case of this example this will be another custom ScriptableObject implementation; although the same principle can also be applied to a collection type.

// ExampleGraph.cs
using System.Collections.Generic;
using UnityEngine;

public abstract class ExampleGraph : ScriptableObject {

    [SerializeField]
    private List<ExampleNode> _nodes = new List<ExampleNode>();

    public void AddNode(ExampleNode node) {
        _nodes.Add(node);
    }

}
// ExampleGraph.js
import System.Collections.Generic;

class ExampleGraph extends ScriptableObject {

    @SerializeField
    private var _nodes:List.<ExampleNode> = new List.<ExampleNode>();

    function AddNode(node:ExampleNode) {
        _nodes.Add(node);
    }

}

We will also need to implement at least one type of node so that the add menu will contain at least one type to select from!

// NodeTypeA.cs
public class NodeTypeA : ExampleNode {
}

// NodeTypeB.cs
public class NodeTypeB : ExampleNode {
}
// NodeTypeA.js
class NodeTypeA extends ExampleNode {
}

// NodeTypeB.js
class NodeTypeB extends ExampleNode {
}

An IElementAdderTContext implementation is also needed since this defines how nodes are to be created and how they are to be associated with their context object. In the case of this example the context object will be an instance of ExampleGraph.

using Rotorz.ReorderableList;
using System;
using UnityEngine;

public class ExampleNodeElementAdder : IElementAdder<ExampleGraph> {

    public ExampleNodeElementAdder(ExampleGraph graph) {
        Object = graph;
    }

    public ExampleGraph Object { get; private set; }

    public bool CanAddElement(Type type) {
        return true;
    }

    public object AddElement(Type type) {
        var node = (ExampleNode)ScriptableObject.CreateInstance(type);
        Object.AddNode(node);
        return node;
    }

}
import Rotorz.ReorderableList;
import System;

class ExampleNodeElementAdder implements IElementAdder.<ExampleGraph> {

    public ExampleNodeElementAdder(graph:ExampleGraph) {
        _object = graph;
    }

    private var _object:ExampleGraph;
    public function get Object():ExampleGraph { return _object; }

    function CanAddElement(type:Type):boolean {
        return true;
    }

    function AddElement(type:Type):object {
        var node = ScriptableObject.CreateInstance(type) as ExampleNode;
        Object.AddNode(node);
        return node;
    }

}

The drop-down add menu can then be defined like shown below where we make use of an adder element menu builder to populate the menu with the relevant element types:

using Rotorz.ReorderableList;
using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(ExampleGraph))]
public class ExampleGraphEditor : Editor {

    private ReorderableListControl _listControl;
    private IReorderableListAdaptor _listAdaptor;

    private void OnEnable() {
        // Create list control and pass flag into constructor so that the
        // regular add button is not displayed.
        _listControl = new ReorderableListControl(ReorderableListFlags.HideAddButton);

        // Subscribe to event for when add menu button is clicked which will
        // also indicate that the add menu button is to be presented.
        _listControl.AddMenuClicked += OnAddMenuClicked;

        // Create adaptor for example list.
        var nodesProperty = serializedObject.FindProperty("_nodes");
        _listAdaptor = new SerializedPropertyAdaptor(nodesProperty);
    }

    private void OnDisable() {
        // Unsubscribe from event, good practice.
        if (_listControl != null)
            _listControl.AddMenuClicked -= OnAddMenuClicked;
    }

    private void OnAddMenuClicked(object sender, AddMenuClickedEventArgs args) {
        var graph = target as ExampleGraph;
        var elementAdder = new ExampleNodeElementAdder(graph);

        var builder = ElementAdderMenuBuilder.For<ExampleGraph>(typeof(ExampleNode));
        builder.SetElementAdder(elementAdder);

        var menu = builder.GetMenu();
        menu.DropDown(args.ButtonPosition);
    }

    private void OnGUI() {
        // Draw layout version of reorderable list control.
        _listControl.Draw(_listAdaptor);
    }

}
#pragma strict
import Rotorz.ReorderableList;

@CustomEditor(ExampleGraph)
class ExampleGraphEditor extends Editor {

    private var _listControl:ReorderableListControl;
    private var _listAdaptor:IReorderableListAdaptor;

    function OnEnable() {
        // Create list control and pass flag into constructor so that the
        // regular add button is not displayed.
        _listControl = new ReorderableListControl(ReorderableListFlags.HideAddButton);

        // Subscribe to event for when add menu button is clicked which will
        // also indicate that the add menu button is to be presented.
        _listControl.AddMenuClicked += OnAddMenuClicked;

        // Create adaptor for example list.
        var nodesProperty = serializedObject.FindProperty('_nodes');
        _listAdaptor = new SerializedPropertyAdaptor(nodesProperty);
    }

    function OnDisable() {
        // Unsubscribe from event, good practice.
        if (_listControl != null)
            _listControl.AddMenuClicked -= OnAddMenuClicked;
    }

    function OnAddMenuClicked(sender:object, args:AddMenuClickedEventArgs) {
        var graph = target as ExampleGraph;
        var elementAdder = new ExampleNodeElementAdder(graph);

        var builder = ElementAdderMenuBuilder.For.<ExampleGraph>(ExampleNode);
        builder.SetElementAdder(elementAdder);

        var menu = builder.GetMenu();
        menu.DropDown(args.ButtonPosition);
    }

    function OnGUI() {
        // Draw layout version of reorderable list control.
        _listControl.Draw(_listAdaptor);
    }

}

With this approach custom commands can also be included by adding them directly using the menu builder.

Adder menus can also be extended with custom commands without needing to directly interact with the menu builder. This can be achieved by annotating custom command implementations with an attribute which defines the context in which the command will be included:

using Rotorz.ReorderableList;
using UnityEngine;

[ElementAdderMenuCommand(typeof(ExampleNode))]
public class SpecialCommand : IElementAdderMenuCommand<ExampleGraph> {

    public SpecialCommand() {
        Content = new GUIContent("Special Command");
    }

    public GUIContent Content { get; private set; }

    public bool CanExecute(IElementAdder<ExampleGraph> elementAdder) {
        return true;
    }

    public void Execute(IElementAdder<ExampleGraph> elementAdder) {
        // Execute some custom command here!
        // Such as bulk adding nodes!
    }

}
#pragma strict
import Rotorz.ReorderableList;

@ElementAdderMenuCommand(ExampleNode)
class SpecialCommand implements IElementAdderMenuCommand.<ExampleGraph> {

    private var _content:GUIContent = new GUIContent('Special Command');
    public function get Content():GUIContent { return _content; }

    function CanExecute(elementAdder:IElementAdder.<ExampleGraph>):boolean {
        return true;
    }

    function Execute(elementAdder:IElementAdder.<ExampleGraph>) {
        // Execute some custom command here!
        // Such as bulk adding nodes!
    }

}