Why does UIElements.DropdownMenu not derive from VisualElement?

I was so thrilled to find what appeared to be a simple, generic DropdownMenu control for UIElements:

But as it turns out, confusingly, this is not a VisualElements-derived class, so I cannot add it to my visual hierarchy. Clearly I am not alone in my confusion, as someone else asked the same question: VisualElement: How to add UnityEngine.UIElements.DropdownMenu to another VisualElement - Questions & Answers - Unity Discussions

How can I add this to my custom editors? And if I can’t, what is the appropriate control to use for a simple dropdown menu?

2 Likes

Hi,
Here is a quick example of how UIElements.DropdownMenu can be used.

using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEditor.UIElements;

class MyButtonWithMenu : TextElement
{
    public DropdownMenu menu { get; private set; }
 
    public MyButtonWithMenu()
    {
        menu = new DropdownMenu();
        this.AddManipulator(new PointerClickable(e => menu.DoDisplayEditorMenu(this.worldBound)));
    }
}

public class MyMenuTest : EditorWindow
{
    [MenuItem("Window/MyMenuTest")]
    public static void ShowExample()
    {
        MyMenuTest wnd = GetWindow<MyMenuTest>();
        wnd.titleContent = new GUIContent("MyMenuTest");
    }

    private MyButtonWithMenu m_ButtonWithMenu;
    private int m_ActiveCount = 0;

    public void OnEnable()
    {
        // Each editor window contains a root VisualElement object
        VisualElement root = rootVisualElement;

        m_ButtonWithMenu = new MyButtonWithMenu();

        m_ButtonWithMenu.menu.AppendAction("Increase Count", a => { m_ActiveCount++; UpdateText(); }, a => DropdownMenuAction.Status.Normal);
        m_ButtonWithMenu.menu.AppendAction("Decrease Count", a => { m_ActiveCount--; UpdateText(); }, a => DropdownMenuAction.Status.Normal);

        m_ButtonWithMenu.style.color = Color.blue;
        m_ButtonWithMenu.style.backgroundColor = Color.red;
        m_ButtonWithMenu.style.paddingTop = 4;
        m_ButtonWithMenu.style.paddingBottom = 4;
        m_ButtonWithMenu.style.paddingLeft = 4;
        m_ButtonWithMenu.style.paddingRight = 4;

        root.Add(m_ButtonWithMenu);
        UpdateText();
    }

    void UpdateText()
    {
        m_ButtonWithMenu.text = "Click Me Please! Count = " + m_ActiveCount;
    }
}
2 Likes

This looks like an excellent solution, I’m sorry I didn’t notice the reply until now!

I don’t seem to see a “PointerClickable” manipulator defined anywhere, though, even though I can see it when searching the Unity reference source.

I also don’t see the DoDisplayEditorMenu extension method, which I can find in the reference source but seems inaccessible from my project. I’m running 2019.3.0b7, which is newer than builds which apparently had this functionality. Am I missing something obvious? I’m including the proper using statements (UnityEditor.UIElements and UnityEngine.UIElements, etc.).

@alexandred_unity Do you know how I can access these seemingly private classes mentioned above?

Here’s an example of a simple right-click menu:

using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;

public class MyEditorWindow : EditorWindow
{
    [MenuItem("Hello/Hello")]
    static void MenuEntry()
    {
        GetWindow<MyEditorWindow>();
    }

    void OnEnable()
    {
        var label = new Label("Hello!");
        label.AddManipulator(new ContextualMenuManipulator(BuildContextualMenu));
        rootVisualElement.Add(label);
    }

    void BuildContextualMenu(ContextualMenuPopulateEvent evt)
    {
        evt.menu.AppendAction("SomeAction", OnMenuAction, DropdownMenuAction.AlwaysEnabled);
    }

    void OnMenuAction(DropdownMenuAction action)
    {
        Debug.Log(action.name);
    }
}

The above code uses the ContextualMenuManipulator which is not a physical element, just a manipulator that goes on top of a element and provides the menu functionality.

By the way, if you just want a dropdown/popup control, have a look at the Choice Fields in the Window > UI > UIElements Samples window.

I can’t get either of these examples to work in my project. @alexandred_unity 's example has the problem that ‘PointerClickable’ does not exist in that namespace.

@uDamian example compiles, but nothing happens as expected. The menu is there, I select it and click on the word Hello! and see no Debug.Log message at all. Plus, where is the dropdown supposed to show up?

EDIT: I was working from a laptop and left-click on Hello! does nothing, Right-click I do see “Some Action”! That example seems like it will solve my problem.

Looking in the Choice area, the Enum is close, but I am reading the contents of a folder from disc (.json files) and these will change during runtime as users add new .json files. It would be very easy to use the filenames, as opposed to creating enum types.

What I want to make is a dropdown, where a selection in the Editor fires an event, i.e. read the .json file selected. This is my code, and what I would expect to happen would be to get a dropdown added (like a button ) to my Visual Element.

    public override VisualElement CreateInspectorGUI()
    {
        _RootElement.Clear();
        _ControlElement.Clear();

        presets = Directory.GetFiles(m_savedDataPath);

        DropdownMenu dropdownMenu = new DropdownMenu();

        for (int i = 0; i < presets.Length; i++)
            dropdownMenu.InsertAction(i, presets[i], OnMenuAction);
   
        _ControlElement.Add(dropdownMenu);
        _RootElement.Add(_ControlElement);

        return _RootElement;
    }

    void OnMenuAction(DropdownMenuAction action)
    {
        Debug.Log(action.name);
    }

I understand that DropdownMenu is not VisualElements-derived class and so this won’t work. I don’t really understand why that choice was made and why the workarounds above are so complex.

I’m pretty stuck at this point, can anyone help explain how to get the above examples to work and/or a very simple implementation of a dropdown menu selection?

I have the contextual menu working, but I can’t get it to build using filenames. In the example below, the menu builds correctly from the ‘presets’ array. I can add a new string value to that array, and it builds. It does not build anything from the ‘savedFilenames’ even when the array exists and has values.

If I copy a string from the ‘savedFilenames’ array into ‘presets’, that entry disappears while the other value is still visible. It’s as if I shortened the array without including the new value, or the new value is not a string, but it is!

This seems like a bug to me, I’ve tried moving things around into an Awake function, copying the fileNames array. No menu items ever show up from the savedFilenames array.

    private string m_savedDataPath = "";
    private string[] presets = new string[] { "Hello Again", "And again.." };
    private string[] savedFilenames;

    public void OnEnable()
    {
        m_savedDataPath = Application.persistentDataPath + "/";
        savedFilenames = Directory.GetFiles(m_savedDataPath);

        //The log shows that the string exists, but copying into the array does not result
        //in it being included in the menu.
        Debug.Log(savedFilenames[0]);
        presets[0] = savedFilenames[0];

        //Strangely, doing the exact same thing manually works fine....
        //presets[0] = "New Value!";

        _RootElement = new VisualElement();

        ContextualMenuManipulator m = new ContextualMenuManipulator(BuildContextualMenu);
        m.target = _RootElement;

        var label = new Label("Load a Preset!");
        label.AddManipulator(m);
        _RootElement.Add(label);

    }


    void BuildContextualMenu(ContextualMenuPopulateEvent evt)
    {
        foreach (string s in presets)
            evt.menu.AppendAction(s, OnMenuAction, DropdownMenuAction.AlwaysEnabled);

        //These items never show up, it's as if they don't exist, a debug.log shows they do
        foreach (string s in savedFilenames)
            evt.menu.AppendAction(s, OnMenuAction, DropdownMenuAction.AlwaysEnabled);
    }

    void OnMenuAction(DropdownMenuAction action)
    {
        Debug.Log(action.name);
    }

It doesn’t matter if I’m in Editing or Play mode. Here are the screenshots of what happens:

5516710--566437--Screen Shot 2020-02-24 at 11.03.36 AM.png 5516710--566422--Screen Shot 2020-02-24 at 10.55.16 AM.png 5516710--566425--Screen Shot 2020-02-24 at 10.54.49 AM.png

Solved: I am using System.Linq, there may be a better way. The problem was that I was passing the full path to the menu, which must not be allowed. I only want the filename, Linq lets me do this:

foreach (string s in savedFilenames)
            evt.menu.AppendAction(Path.GetFileName(s), OnMenuAction, DropdownMenuAction.AlwaysEnabled);
1 Like

For my future self stumbling upon this thread in search for “how to do a dropdown with VisualElements”:

PointerClickable is an internal class and can’t be used from other namespaces. Not sure why an example was provided with it. The same goes for DoDisplayEditorMenu which is also in an internal static class “EditorMenuExtensions”.

The right way to do a simple dropdown that is not a context menu seems to be to use a PopupField<Type>.

4 Likes

I wonder, why aren’t the examples provided here in the documentation page of the DropdownMenu if it is actually the only code example of its usage on the Internet?

As you can see, the documentation doesn’t even provide a useful description or any guide on how to use the DropdownMenu or even a mention of why it isn’t a VisualElement. It would also help to add an example to the UIElementsExamples project which is a very helpful resource.

4 Likes

I am working on node-based editor and I struggled with this issue too.

ToolbarMenu class worked for me.

It already has dropdown embedded in it, so declaring additional classes is not required.
This is part of my code, I hope this works for anyone

//this code is part of something much bigger, so I ommited parts unrelated to the topic, please don't paste it as a whole into your project ;)
//mind that I work on graph editor, I'm not sure if it works for your case, but it's worth a shot

public enum ScenarioState
{
   NONE,
   ACTIVE,
   COMPLETED,
   FAILED
}

[System.Serializable]
//GameScenarioNode is my custom parent class, that derives from native class Node used to create graph editors in Unity
public class StageNode : GameScenarioNode
{
    public string StageName;
    public bool EntryPoint = false;
    ScenarioState state = ScenarioState.NONE;
 
    public GameScenarioStage RelatedStage { get; private set; }

    public static StageNode CreateStageNode(GameScenarioGraphView graph, string nodeName, Vector2 position)
    {

        var stageNode = new StageNode()
        {
          //initializing some variables of no importance in the example
        };

       //...

//THIS IS how I added toolbar to the graph node
        var toolbar = new ToolbarMenu();
        toolbar.text = "NONE";                   //this is displayed as a label on top of VisualElement

//these create options in a dropdown menu embedded in the toolbar and assign functionality to them
        toolbar.menu.AppendAction("NONE", new Action<DropdownMenuAction>(x => stageNode.SetStateFromToolbarMenu(toolbar, ScenarioState.NONE)));
        toolbar.menu.AppendAction("ACTIVE", new Action<DropdownMenuAction>(x => stageNode.SetStateFromToolbarMenu(toolbar, ScenarioState.ACTIVE)));
        toolbar.menu.AppendAction("COMPLETED", new Action<DropdownMenuAction>(x => stageNode.SetStateFromToolbarMenu(toolbar, ScenarioState.COMPLETED)));
        toolbar.menu.AppendAction("FAILED", new Action<DropdownMenuAction>(x => stageNode.SetStateFromToolbarMenu(toolbar, ScenarioState.FAILED)));

       //after doing all initialization stuff I finally add toolbar to the content container of my Node
        stageNode.contentContainer.Add(toolbar);

       //...

        return stageNode;
    }

    //a method I call in appended actions
    private void SetStateFromToolbarMenu(ToolbarMenu toolbarMenu, ScenarioState targetState)
    {
        toolbarMenu.text = targetState.ToString(); //this will make label on top of ToolbarMenu change upon switching between the options

        RelatedStage.SetTargetState(this, targetState);//notified the object I found somewhere in not-important-stuff-section in an dropdown-unrelated way, trust me ;)
    }
}
3 Likes

PopupField is currently still undocumented. So here’s a useful tip: whatever class you put in your popup (the Type in above example) … it has to override “ToString()” … otherwise PopupField will print only one entry, and give it the name of your Type (instead of printing all the entries).

It’s a bug (it should print N entries, not 1), but it’s easy to workaround if you realise (guess) the above requirement.

Also I want to draw more attention to this feature that uDamian pointed out, which I hadn’t discovered until now, but is very useful for checking basic usages of core UIToolkit classes:

…NB: by default this is useless you have to manually resize the window (at least in 2019 LTS - it popped up with the window sized to be literally only the width of the left navigation bar (bug?), so there’s no content to see (and nothing to tell you that there’s content being hidden/not rendered). Even on a 4k monitor with a bajillion pixels available.)

I knew uDamian writes useful posts, so I sat there thinking: “Huh? Why did he recommend this? There must be something more here…” before I played with resizing it and discovered the content :).

1 Like

Thank you! I always thought that was useless, just hidden helpfulness! It would be a good thing to fix up in the next version.

hope it will have detailed document

1 Like

So what is the difference usage between this and the ListView?
Unity - Scripting API: ListView (unity3d.com)