Search Unity

Why does UIElements.DropdownMenu not derive from VisualElement?

Discussion in 'UI Toolkit' started by mikevargas, Nov 6, 2019.

  1. mikevargas

    mikevargas

    Joined:
    Aug 17, 2019
    Posts:
    22
    I was so thrilled to find what appeared to be a simple, generic DropdownMenu control for UIElements:
    https://docs.unity3d.com/2019.3/Documentation/ScriptReference/UIElements.DropdownMenu.html

    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: https://answers.unity.com/questions/1650583/visualelement-how-to-add-unityengineuielementsdrop.html

    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?
     
    Voronoi likes this.
  2. alexandred_unity

    alexandred_unity

    Unity Technologies

    Joined:
    Dec 12, 2018
    Posts:
    43
    Hi,
    Here is a quick example of how UIElements.DropdownMenu can be used.


    Code (CSharp):
    1. using UnityEditor;
    2. using UnityEngine;
    3. using UnityEngine.UIElements;
    4. using UnityEditor.UIElements;
    5.  
    6. class MyButtonWithMenu : TextElement
    7. {
    8.     public DropdownMenu menu { get; private set; }
    9.  
    10.     public MyButtonWithMenu()
    11.     {
    12.         menu = new DropdownMenu();
    13.         this.AddManipulator(new PointerClickable(e => menu.DoDisplayEditorMenu(this.worldBound)));
    14.     }
    15. }
    16.  
    17. public class MyMenuTest : EditorWindow
    18. {
    19.     [MenuItem("Window/MyMenuTest")]
    20.     public static void ShowExample()
    21.     {
    22.         MyMenuTest wnd = GetWindow<MyMenuTest>();
    23.         wnd.titleContent = new GUIContent("MyMenuTest");
    24.     }
    25.  
    26.     private MyButtonWithMenu m_ButtonWithMenu;
    27.     private int m_ActiveCount = 0;
    28.  
    29.     public void OnEnable()
    30.     {
    31.         // Each editor window contains a root VisualElement object
    32.         VisualElement root = rootVisualElement;
    33.  
    34.         m_ButtonWithMenu = new MyButtonWithMenu();
    35.  
    36.         m_ButtonWithMenu.menu.AppendAction("Increase Count", a => { m_ActiveCount++; UpdateText(); }, a => DropdownMenuAction.Status.Normal);
    37.         m_ButtonWithMenu.menu.AppendAction("Decrease Count", a => { m_ActiveCount--; UpdateText(); }, a => DropdownMenuAction.Status.Normal);
    38.  
    39.         m_ButtonWithMenu.style.color = Color.blue;
    40.         m_ButtonWithMenu.style.backgroundColor = Color.red;
    41.         m_ButtonWithMenu.style.paddingTop = 4;
    42.         m_ButtonWithMenu.style.paddingBottom = 4;
    43.         m_ButtonWithMenu.style.paddingLeft = 4;
    44.         m_ButtonWithMenu.style.paddingRight = 4;
    45.  
    46.         root.Add(m_ButtonWithMenu);
    47.         UpdateText();
    48.     }
    49.  
    50.     void UpdateText()
    51.     {
    52.         m_ButtonWithMenu.text = "Click Me Please! Count = " + m_ActiveCount;
    53.     }
    54. }
    55.  
     
    Last edited: Nov 8, 2019
    mikevargas and mbussidk like this.
  3. mikevargas

    mikevargas

    Joined:
    Aug 17, 2019
    Posts:
    22
    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.).
     
    Last edited: Dec 3, 2019
  4. mikevargas

    mikevargas

    Joined:
    Aug 17, 2019
    Posts:
    22
    @alexandred_unity Do you know how I can access these seemingly private classes mentioned above?
     
  5. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    1,231
    Here's an example of a simple right-click menu:
    Code (CSharp):
    1. using UnityEditor;
    2. using UnityEditor.UIElements;
    3. using UnityEngine;
    4. using UnityEngine.UIElements;
    5.  
    6. public class MyEditorWindow : EditorWindow
    7. {
    8.     [MenuItem("Hello/Hello")]
    9.     static void MenuEntry()
    10.     {
    11.         GetWindow<MyEditorWindow>();
    12.     }
    13.  
    14.     void OnEnable()
    15.     {
    16.         var label = new Label("Hello!");
    17.         label.AddManipulator(new ContextualMenuManipulator(BuildContextualMenu));
    18.         rootVisualElement.Add(label);
    19.     }
    20.  
    21.     void BuildContextualMenu(ContextualMenuPopulateEvent evt)
    22.     {
    23.         evt.menu.AppendAction("SomeAction", OnMenuAction, DropdownMenuAction.AlwaysEnabled);
    24.     }
    25.  
    26.     void OnMenuAction(DropdownMenuAction action)
    27.     {
    28.         Debug.Log(action.name);
    29.     }
    30. }
    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.
     
  6. Voronoi

    Voronoi

    Joined:
    Jul 2, 2012
    Posts:
    590
    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.

    Code (CSharp):
    1.  
    2.     public override VisualElement CreateInspectorGUI()
    3.     {
    4.         _RootElement.Clear();
    5.         _ControlElement.Clear();
    6.  
    7.         presets = Directory.GetFiles(m_savedDataPath);
    8.  
    9.         DropdownMenu dropdownMenu = new DropdownMenu();
    10.  
    11.         for (int i = 0; i < presets.Length; i++)
    12.             dropdownMenu.InsertAction(i, presets[i], OnMenuAction);
    13.    
    14.         _ControlElement.Add(dropdownMenu);
    15.         _RootElement.Add(_ControlElement);
    16.  
    17.         return _RootElement;
    18.     }
    19.  
    20.     void OnMenuAction(DropdownMenuAction action)
    21.     {
    22.         Debug.Log(action.name);
    23.     }
    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?
     
    Last edited: Feb 24, 2020
  7. Voronoi

    Voronoi

    Joined:
    Jul 2, 2012
    Posts:
    590
    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.

    Code (CSharp):
    1.  
    2.     private string m_savedDataPath = "";
    3.     private string[] presets = new string[] { "Hello Again", "And again.." };
    4.     private string[] savedFilenames;
    5.  
    6.     public void OnEnable()
    7.     {
    8.         m_savedDataPath = Application.persistentDataPath + "/";
    9.         savedFilenames = Directory.GetFiles(m_savedDataPath);
    10.  
    11.         //The log shows that the string exists, but copying into the array does not result
    12.         //in it being included in the menu.
    13.         Debug.Log(savedFilenames[0]);
    14.         presets[0] = savedFilenames[0];
    15.  
    16.         //Strangely, doing the exact same thing manually works fine....
    17.         //presets[0] = "New Value!";
    18.  
    19.         _RootElement = new VisualElement();
    20.  
    21.         ContextualMenuManipulator m = new ContextualMenuManipulator(BuildContextualMenu);
    22.         m.target = _RootElement;
    23.  
    24.         var label = new Label("Load a Preset!");
    25.         label.AddManipulator(m);
    26.         _RootElement.Add(label);
    27.  
    28.     }
    29.  
    30.  
    31.     void BuildContextualMenu(ContextualMenuPopulateEvent evt)
    32.     {
    33.         foreach (string s in presets)
    34.             evt.menu.AppendAction(s, OnMenuAction, DropdownMenuAction.AlwaysEnabled);
    35.  
    36.         //These items never show up, it's as if they don't exist, a debug.log shows they do
    37.         foreach (string s in savedFilenames)
    38.             evt.menu.AppendAction(s, OnMenuAction, DropdownMenuAction.AlwaysEnabled);
    39.     }
    40.  
    41.     void OnMenuAction(DropdownMenuAction action)
    42.     {
    43.         Debug.Log(action.name);
    44.     }
    45.  
    46.  
    It doesn't matter if I'm in Editing or Play mode. Here are the screenshots of what happens:

    Screen Shot 2020-02-24 at 11.03.36 AM.png Screen Shot 2020-02-24 at 10.55.16 AM.png Screen Shot 2020-02-24 at 10.54.49 AM.png
     
    Last edited: Feb 24, 2020
  8. Voronoi

    Voronoi

    Joined:
    Jul 2, 2012
    Posts:
    590
    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:

    Code (CSharp):
    1.  
    2. foreach (string s in savedFilenames)
    3.             evt.menu.AppendAction(Path.GetFileName(s), OnMenuAction, DropdownMenuAction.AlwaysEnabled);
     
    Last edited: Feb 25, 2020
    Catsoft-Studios likes this.
  9. fherbst

    fherbst

    Joined:
    Jun 24, 2012
    Posts:
    802
    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
    Code (CSharp):
    1. PopupField<Type>
    .
     
    EyasSH, mrmcduck and mikevargas like this.
  10. Alex-CG

    Alex-CG

    Joined:
    May 10, 2015
    Posts:
    11
    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.
     
    Last edited: Oct 21, 2020
  11. BloodHound_PL

    BloodHound_PL

    Joined:
    May 12, 2016
    Posts:
    4
    I am working on node-based editor and I struggled with this issue too.

    ToolbarMenu class worked for me.
    https://docs.unity3d.com/ScriptReference/UIElements.ToolbarMenu.html

    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

    Code (CSharp):
    1.  
    2.  
    3. //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 ;)
    4. //mind that I work on graph editor, I'm not sure if it works for your case, but it's worth a shot
    5.  
    6. public enum ScenarioState
    7. {
    8.    NONE,
    9.    ACTIVE,
    10.    COMPLETED,
    11.    FAILED
    12. }
    13.  
    14. [System.Serializable]
    15. //GameScenarioNode is my custom parent class, that derives from native class Node used to create graph editors in Unity
    16. public class StageNode : GameScenarioNode
    17. {
    18.     public string StageName;
    19.     public bool EntryPoint = false;
    20.     ScenarioState state = ScenarioState.NONE;
    21.  
    22.     public GameScenarioStage RelatedStage { get; private set; }
    23.  
    24.     public static StageNode CreateStageNode(GameScenarioGraphView graph, string nodeName, Vector2 position)
    25.     {
    26.  
    27.         var stageNode = new StageNode()
    28.         {
    29.           //initializing some variables of no importance in the example
    30.         };
    31.  
    32.        //...
    33.  
    34. //THIS IS how I added toolbar to the graph node
    35.         var toolbar = new ToolbarMenu();
    36.         toolbar.text = "NONE";                   //this is displayed as a label on top of VisualElement
    37.  
    38. //these create options in a dropdown menu embedded in the toolbar and assign functionality to them
    39.         toolbar.menu.AppendAction("NONE", new Action<DropdownMenuAction>(x => stageNode.SetStateFromToolbarMenu(toolbar, ScenarioState.NONE)));
    40.         toolbar.menu.AppendAction("ACTIVE", new Action<DropdownMenuAction>(x => stageNode.SetStateFromToolbarMenu(toolbar, ScenarioState.ACTIVE)));
    41.         toolbar.menu.AppendAction("COMPLETED", new Action<DropdownMenuAction>(x => stageNode.SetStateFromToolbarMenu(toolbar, ScenarioState.COMPLETED)));
    42.         toolbar.menu.AppendAction("FAILED", new Action<DropdownMenuAction>(x => stageNode.SetStateFromToolbarMenu(toolbar, ScenarioState.FAILED)));
    43.  
    44.        //after doing all initialization stuff I finally add toolbar to the content container of my Node
    45.         stageNode.contentContainer.Add(toolbar);
    46.  
    47.        //...
    48.  
    49.         return stageNode;
    50.     }
    51.  
    52.     //a method I call in appended actions
    53.     private void SetStateFromToolbarMenu(ToolbarMenu toolbarMenu, ScenarioState targetState)
    54.     {
    55.         toolbarMenu.text = targetState.ToString(); //this will make label on top of ToolbarMenu change upon switching between the options
    56.  
    57.         RelatedStage.SetTargetState(this, targetState);//notified the object I found somewhere in not-important-stuff-section in an dropdown-unrelated way, trust me ;)
    58.     }
    59. }
     
    Last edited: Nov 17, 2020
  12. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,933
    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.
     
  13. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,933
    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 :).
     
    PrimalCoder likes this.
  14. skowroshima

    skowroshima

    Joined:
    Oct 14, 2018
    Posts:
    75
    Thank you! I always thought that was useless, just hidden helpfulness! It would be a good thing to fix up in the next version.
     
  15. gyx920820

    gyx920820

    Joined:
    Oct 8, 2015
    Posts:
    35
    hope it will have detailed document
     
    mikevargas likes this.
  16. randomdragon

    randomdragon

    Joined:
    Feb 11, 2020
    Posts:
    31