Search Unity

Question AdvancedDropdown set initial state

Discussion in 'Immediate Mode GUI (IMGUI)' started by OBiwer, Apr 4, 2023.

  1. OBiwer

    OBiwer

    Joined:
    Aug 2, 2022
    Posts:
    61
    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:
    Code (csharp):
    1.  
    2.                 var stateField = typeof(AdvancedDropdown).GetField("m_State", BindingFlags.NonPublic | BindingFlags.Instance);
    3.                 var state = stateField.GetValue(this) as AdvancedDropdownState;
    4.  
    5.                 typeof(AdvancedDropdownState).GetMethod("SetSelectedIndex", BindingFlags.NonPublic | BindingFlags.Instance)
    6.     .Invoke(state, new object[] { startItem, startIdx });
    7.  
    8.                 typeof(AdvancedDropdownState).GetMethod("SetSelectionOnItem", BindingFlags.NonPublic | BindingFlags.Instance)
    9.                     .Invoke(state, new object[] { startItem, startIdx });
    10.  
    11.                 typeof(AdvancedDropdownState).GetMethod("MoveDownSelection", BindingFlags.NonPublic | BindingFlags.Instance)
    12.                     .Invoke(state, new object[] { startItem });
    13.  
     
  2. OBiwer

    OBiwer

    Joined:
    Aug 2, 2022
    Posts:
    61
    So ... I guess it's not possible?
     
  3. forestrf

    forestrf

    Joined:
    Aug 28, 2010
    Posts:
    231
    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.

    Code (CSharp):
    1.  
    2. class GenericDropdown : AdvancedDropdown {
    3.     public string headerTxt;
    4.     public string[] entries;
    5.     public Action<int> onItemSelected;
    6.  
    7.     static readonly FieldInfo m_DataSource = typeof(AdvancedDropdown).GetField("m_DataSource", BindingFlags.Instance | BindingFlags.NonPublic);
    8.     static readonly Type CallbackDataSource = typeof(AdvancedDropdown).Assembly.GetType("UnityEditor.IMGUI.Controls.CallbackDataSource");
    9.     static readonly Type AdvancedDropdownDataSource = typeof(AdvancedDropdown).Assembly.GetType("UnityEditor.IMGUI.Controls.AdvancedDropdownDataSource");
    10.     static readonly ConstructorInfo CallbackDataSource_Constructor = CallbackDataSource.GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, null, new Type[] { typeof(Func<AdvancedDropdownItem>) }, null);
    11.     static readonly FieldInfo CallbackDataSource_selectedIDs = AdvancedDropdownDataSource.GetField("m_SelectedIDs", BindingFlags.Instance | BindingFlags.NonPublic);
    12.  
    13.     public GenericDropdown(string headerTxt, string[] entries, Action<int> onItemSelected, int selected = -1) : base(new AdvancedDropdownState()) {
    14.         this.headerTxt = headerTxt;
    15.         this.entries = entries;
    16.         this.onItemSelected = onItemSelected;
    17.         if (selected != -1) {
    18.             var dataSource = CallbackDataSource_Constructor.Invoke(new object[] { (Func<AdvancedDropdownItem>) BuildRoot });
    19.             m_DataSource.SetValue(this, dataSource);
    20.             var selectedIDs = (List<int>) CallbackDataSource_selectedIDs.GetValue(dataSource);
    21.             selectedIDs.Add(selected);
    22.         }
    23.  
    24.         ((GUIStyle) "DD ItemStyle").richText = true;
    25.     }
    26.  
    27.     protected override AdvancedDropdownItem BuildRoot() {
    28.         var root = new AdvancedDropdownItem(headerTxt);
    29.  
    30.         for (int i = 0; i < entries.Length; i++) {
    31.             root.AddChild(new AdvancedDropdownItem(entries[i]) { id = i });
    32.         }
    33.  
    34.         return root;
    35.     }
    36.  
    37.     protected override void ItemSelected(AdvancedDropdownItem item) {
    38.         onItemSelected(item.id);
    39.     }
    40. }
    41.  
    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.
     
    Last edited: Jul 3, 2023
    OBiwer likes this.
  4. AnomalusUndrdog

    AnomalusUndrdog

    Joined:
    Jul 3, 2009
    Posts:
    1,553
    I looked into this as well. Here's one that works:

    Code (CSharp):
    1.  
    2. using System.Collections.Generic;
    3. using System.Reflection;
    4. using UnityEditor.IMGUI.Controls;
    5.  
    6. /// <summary>
    7. /// Inherit from this instead of <see cref="AdvancedDropdown"/>
    8. /// to have the ability to set it to a particular dropdown item.
    9. /// See <see cref="SetSelectedItem"/>.
    10. /// </summary>
    11. public abstract class BetterAdvancedDropdown : AdvancedDropdown
    12. {
    13.    readonly AdvancedDropdownState _state;
    14.  
    15.    protected BetterAdvancedDropdown(AdvancedDropdownState state) : base(state)
    16.    {
    17.       _state = state;
    18.    }
    19.  
    20.    public void SetSelectedItem(BetterAdvancedDropdownItem itemToSelect, bool invokeItemSelectedCallback = true)
    21.    {
    22.       _state.UpdateSelectionChain(itemToSelect);
    23.  
    24.       if (invokeItemSelectedCallback)
    25.       {
    26.          ItemSelected(itemToSelect);
    27.       }
    28.    }
    29. }
    30.  
    31. public static class AdvancedDropdownUtil
    32. {
    33.    /// <summary>
    34.    /// <see cref="AdvancedDropdownState.SetSelectedIndex"/>
    35.    /// </summary>
    36.    static readonly MethodInfo SetSelectedIndexOfItem =
    37.       typeof(AdvancedDropdownState).GetMethod("SetSelectedIndex", BindingFlags.Instance | BindingFlags.NonPublic);
    38.  
    39.    static readonly object[] SetSelectedIndexOfItemParams = new object[2];
    40.  
    41.    static readonly FieldInfo ChildrenItems =
    42.       typeof(AdvancedDropdownItem).GetField("m_Children", BindingFlags.Instance | BindingFlags.NonPublic);
    43.  
    44.    static void CallSetSelectedIndexOfItem(this AdvancedDropdownState state, BetterAdvancedDropdownItem itemToUpdate, int newSelectedIdx)
    45.    {
    46.       SetSelectedIndexOfItemParams[0] = itemToUpdate;
    47.       SetSelectedIndexOfItemParams[1] = newSelectedIdx;
    48.       SetSelectedIndexOfItem.Invoke(state, SetSelectedIndexOfItemParams);
    49.    }
    50.  
    51.    /// <summary>
    52.    /// Will set the item's parent to select the aforementioned item.
    53.    /// This is extended to the item's entire ancestry (so the grandparent will select the parent, etc.) all the way until the root item.
    54.    /// This will cause the AdvancedDropdown to "jump" to the specified item the next time <see cref="AdvancedDropdown.Show"/> is called.
    55.    /// </summary>
    56.    /// <param name="state"></param>
    57.    /// <param name="itemToSelect"></param>
    58.    public static void UpdateSelectionChain(this AdvancedDropdownState state, BetterAdvancedDropdownItem itemToSelect)
    59.    {
    60.       var item = itemToSelect;
    61.       while (item != null && item.Parent != null)
    62.       {
    63.          var childrenOfParents = (List<AdvancedDropdownItem>) ChildrenItems.GetValue(item.Parent);
    64.          state.CallSetSelectedIndexOfItem(item.Parent, childrenOfParents.IndexOf(item));
    65.          item = item.Parent;
    66.       }
    67.    }
    68. }
    69.  
    70. /// <summary>
    71. /// Similar to <see cref="AdvancedDropdownItem"/>, except it has a link back to its parent.
    72. /// Note that when you are writing <see cref="AdvancedDropdown.BuildRoot"/>, you have to be
    73. /// the one to assign the <see cref="Parent"/>.
    74. /// </summary>
    75. public class BetterAdvancedDropdownItem : AdvancedDropdownItem
    76. {
    77.    /// <summary>
    78.    /// The item that contains this one.
    79.    /// </summary>
    80.    public readonly BetterAdvancedDropdownItem Parent;
    81.  
    82.    public BetterAdvancedDropdownItem(string name, BetterAdvancedDropdownItem parent) : base(name)
    83.    {
    84.       Parent = parent;
    85.    }
    86. }
    87.  
    88.  
    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.
     
    Last edited: Oct 15, 2023