AdvancedDropdown set initial state

Let’s say I’ve a AdvancedDropdown with the weekdays as content like in: (Unity - Scripting API: AdvancedDropdown (unity3d.com))

now, when opened I want to open in the “Weekend” sub-tree. How can I archive that? is it possible via reflections?

my current testings sadly do not work:

                var stateField = typeof(AdvancedDropdown).GetField("m_State", BindingFlags.NonPublic | BindingFlags.Instance);
                var state = stateField.GetValue(this) as AdvancedDropdownState;

                typeof(AdvancedDropdownState).GetMethod("SetSelectedIndex", BindingFlags.NonPublic | BindingFlags.Instance)
    .Invoke(state, new object[] { startItem, startIdx });

                typeof(AdvancedDropdownState).GetMethod("SetSelectionOnItem", BindingFlags.NonPublic | BindingFlags.Instance)
                    .Invoke(state, new object[] { startItem, startIdx });

                typeof(AdvancedDropdownState).GetMethod("MoveDownSelection", BindingFlags.NonPublic | BindingFlags.Instance)
                    .Invoke(state, new object[] { startItem });

So … I guess it’s not possible?

It may be possible by calling SetSelectedIndex(AdvancedDropdownItem item, int index) on AdvancedDropdownState using reflection. item would be Weekend and index would be the child index to select inside weekend.
I haven’t tested it, but I’ve set the state to show an item as selected using this code, which may be useful to keep on the reverse engineering this mess of internal members that could perfectly be public.
I hope it helps.

class GenericDropdown : AdvancedDropdown {
    public string headerTxt;
    public string[] entries;
    public Action<int> onItemSelected;

    static readonly FieldInfo m_DataSource = typeof(AdvancedDropdown).GetField("m_DataSource", BindingFlags.Instance | BindingFlags.NonPublic);
    static readonly Type CallbackDataSource = typeof(AdvancedDropdown).Assembly.GetType("UnityEditor.IMGUI.Controls.CallbackDataSource");
    static readonly Type AdvancedDropdownDataSource = typeof(AdvancedDropdown).Assembly.GetType("UnityEditor.IMGUI.Controls.AdvancedDropdownDataSource");
    static readonly ConstructorInfo CallbackDataSource_Constructor = CallbackDataSource.GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, null, new Type[] { typeof(Func<AdvancedDropdownItem>) }, null);
    static readonly FieldInfo CallbackDataSource_selectedIDs = AdvancedDropdownDataSource.GetField("m_SelectedIDs", BindingFlags.Instance | BindingFlags.NonPublic);

    public GenericDropdown(string headerTxt, string[] entries, Action<int> onItemSelected, int selected = -1) : base(new AdvancedDropdownState()) {
        this.headerTxt = headerTxt;
        this.entries = entries;
        this.onItemSelected = onItemSelected;
        if (selected != -1) {
            var dataSource = CallbackDataSource_Constructor.Invoke(new object[] { (Func<AdvancedDropdownItem>) BuildRoot });
            m_DataSource.SetValue(this, dataSource);
            var selectedIDs = (List<int>) CallbackDataSource_selectedIDs.GetValue(dataSource);
            selectedIDs.Add(selected);
        }

        ((GUIStyle) "DD ItemStyle").richText = true;
    }

    protected override AdvancedDropdownItem BuildRoot() {
        var root = new AdvancedDropdownItem(headerTxt);

        for (int i = 0; i < entries.Length; i++) {
            root.AddChild(new AdvancedDropdownItem(entries[i]) { id = i });
        }

        return root;
    }

    protected override void ItemSelected(AdvancedDropdownItem item) {
        onItemSelected(item.id);
    }
}

Notice that the state variable only gets initialized after calling Show(), that’s why I just set the variable instead of getting it, as it will be null.

1 Like

I looked into this as well. Here’s one that works:

using System.Collections.Generic;
using System.Reflection;
using UnityEditor.IMGUI.Controls;

/// <summary>
/// Inherit from this instead of <see cref="AdvancedDropdown"/>
/// to have the ability to set it to a particular dropdown item.
/// See <see cref="SetSelectedItem"/>.
/// </summary>
public abstract class BetterAdvancedDropdown : AdvancedDropdown
{
   readonly AdvancedDropdownState _state;

   protected BetterAdvancedDropdown(AdvancedDropdownState state) : base(state)
   {
      _state = state;
   }

   public void SetSelectedItem(BetterAdvancedDropdownItem itemToSelect, bool invokeItemSelectedCallback = true)
   {
      _state.UpdateSelectionChain(itemToSelect);

      if (invokeItemSelectedCallback)
      {
         ItemSelected(itemToSelect);
      }
   }
}

public static class AdvancedDropdownUtil
{
   /// <summary>
   /// <see cref="AdvancedDropdownState.SetSelectedIndex"/>
   /// </summary>
   static readonly MethodInfo SetSelectedIndexOfItem =
      typeof(AdvancedDropdownState).GetMethod("SetSelectedIndex", BindingFlags.Instance | BindingFlags.NonPublic);

   static readonly object[] SetSelectedIndexOfItemParams = new object[2];

   static readonly FieldInfo ChildrenItems =
      typeof(AdvancedDropdownItem).GetField("m_Children", BindingFlags.Instance | BindingFlags.NonPublic);

   static void CallSetSelectedIndexOfItem(this AdvancedDropdownState state, BetterAdvancedDropdownItem itemToUpdate, int newSelectedIdx)
   {
      SetSelectedIndexOfItemParams[0] = itemToUpdate;
      SetSelectedIndexOfItemParams[1] = newSelectedIdx;
      SetSelectedIndexOfItem.Invoke(state, SetSelectedIndexOfItemParams);
   }

   /// <summary>
   /// Will set the item's parent to select the aforementioned item.
   /// This is extended to the item's entire ancestry (so the grandparent will select the parent, etc.) all the way until the root item.
   /// This will cause the AdvancedDropdown to "jump" to the specified item the next time <see cref="AdvancedDropdown.Show"/> is called.
   /// </summary>
   /// <param name="state"></param>
   /// <param name="itemToSelect"></param>
   public static void UpdateSelectionChain(this AdvancedDropdownState state, BetterAdvancedDropdownItem itemToSelect)
   {
      var item = itemToSelect;
      while (item != null && item.Parent != null)
      {
         var childrenOfParents = (List<AdvancedDropdownItem>) ChildrenItems.GetValue(item.Parent);
         state.CallSetSelectedIndexOfItem(item.Parent, childrenOfParents.IndexOf(item));
         item = item.Parent;
      }
   }
}

/// <summary>
/// Similar to <see cref="AdvancedDropdownItem"/>, except it has a link back to its parent.
/// Note that when you are writing <see cref="AdvancedDropdown.BuildRoot"/>, you have to be
/// the one to assign the <see cref="Parent"/>.
/// </summary>
public class BetterAdvancedDropdownItem : AdvancedDropdownItem
{
   /// <summary>
   /// The item that contains this one.
   /// </summary>
   public readonly BetterAdvancedDropdownItem Parent;

   public BetterAdvancedDropdownItem(string name, BetterAdvancedDropdownItem parent) : base(name)
   {
      Parent = parent;
   }
}

BetterAdvancedDropdown.SetSelectedItem() will do the changing.

So to do all this, the only thing that actually needs to be done is to edit the AdvancedDropdownState. It has a list of the state of each item (AdvancedDropdownState.AdvancedDropdownItemState), the important part of which is
selectedIndex, the field that indicates, for each item, which among its children has been selected.

AdvancedDropdownWindow uses that list of item states to build its ViewStack, which is really just a Stack<AdvancedDropdownItem> that, if you imagine the dropdown items to be folders, the ViewStack is the list of folders it’s currently in (like the current path in a file browser).

As long as the selectedIndex in each item state is assigned (see AdvancedDropdownUtil.UpdateSelectionChain() in that code I posted), AdvancedDropdownWindow will create the correct ViewStack on its own, so it effectively “jumps” the selection to the item you indicated the next time AdvancedDropdown.Show() is called.

The catch is that to do this, each AdvancedDropdownItem now needs to know which its parent is. That’s the purpose of BetterAdvancedDropdownItem. So in your AdvancedDropdown.BuildRoot(), you have to assign the item’s parent.

EDIT: No need to assign the item’s index anymore.