Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Tutorial: How to to show specific folder content in the project window via editor scripting

Discussion in 'Immediate Mode GUI (IMGUI)' started by Xarbrough, Dec 10, 2017.

  1. Xarbrough

    Xarbrough

    Joined:
    Dec 11, 2014
    Posts:
    1,188
    I wanted to find a way to show a specific folder and display its content in the Unity project panel from custom editor code. Too bad, there is no official API available for this. This is why I went down the rabbit hole to find out how Unity does it internally and used reflection to achieve what I wanted.

    To make it a little more clear: I basically want to use a similar behavior like

    Code (CSharp):
    1. EditorGUIUtility.PingObject(instanceID);
    but to show the contents of a specific folder asset, whose instance id I already know, instead.

    A simple way of doing this would be to get the folder path and then search the content for an arbitrary asset and then ping it. However, this has two drawbacks: First, it requires the folder to actually have content. You can't open empty folders this way. Secondly, it is not easy to reproduce the project window's sorting and it's generally a little ugly to ping and select an arbitrary asset from a folder when I actually just want to open it like any other browser. This is why I decided to search for the internal functionality of actually displaying the folder contents, which I found in the internal method ShowFolderContents of the ProjectBrowser class.

    I hope this helps someone:

    [Tested with Unity 2017.2.0 and 2020.2.1]

    Code (CSharp):
    1. /// <summary>
    2. /// Selects a folder in the project window and shows its content.
    3. /// Opens a new project window, if none is open yet.
    4. /// </summary>
    5. /// <param name="folderInstanceID">The instance of the folder asset to open.</param>
    6. private static void ShowFolderContents(int folderInstanceID)
    7. {
    8.     // Find the internal ProjectBrowser class in the editor assembly.
    9.     Assembly editorAssembly = typeof(Editor).Assembly;
    10.     System.Type projectBrowserType = editorAssembly.GetType("UnityEditor.ProjectBrowser");
    11.  
    12.     // This is the internal method, which performs the desired action.
    13.     // Should only be called if the project window is in two column mode.
    14.     MethodInfo showFolderContents = projectBrowserType.GetMethod(
    15.         "ShowFolderContents", BindingFlags.Instance | BindingFlags.NonPublic);
    16.  
    17.     // Find any open project browser windows.
    18.     Object[] projectBrowserInstances = Resources.FindObjectsOfTypeAll(projectBrowserType);
    19.  
    20.     if (projectBrowserInstances.Length > 0)
    21.     {
    22.         for (int i = 0; i < projectBrowserInstances.Length; i++)
    23.             ShowFolderContentsInternal(projectBrowserInstances[i], showFolderContents, folderInstanceID);
    24.     }
    25.     else
    26.     {
    27.         EditorWindow projectBrowser = OpenNewProjectBrowser(projectBrowserType);
    28.         ShowFolderContentsInternal(projectBrowser, showFolderContents, folderInstanceID);
    29.     }
    30. }
    31.  
    32. private static void ShowFolderContentsInternal(Object projectBrowser, MethodInfo showFolderContents, int folderInstanceID)
    33. {
    34.     // Sadly, there is no method to check for the view mode.
    35.     // We can use the serialized object to find the private property.
    36.     SerializedObject serializedObject = new SerializedObject(projectBrowser);
    37.     bool inTwoColumnMode = serializedObject.FindProperty("m_ViewMode").enumValueIndex == 1;
    38.  
    39.     if (!inTwoColumnMode)
    40.     {
    41.         // If the browser is not in two column mode, we must set it to show the folder contents.
    42.         MethodInfo setTwoColumns = projectBrowser.GetType().GetMethod(
    43.             "SetTwoColumns", BindingFlags.Instance | BindingFlags.NonPublic);
    44.         setTwoColumns.Invoke(projectBrowser, null);
    45.     }
    46.  
    47.     bool revealAndFrameInFolderTree = true;
    48.     showFolderContents.Invoke(projectBrowser, new object[] { folderInstanceID, revealAndFrameInFolderTree });
    49. }
    50.  
    51. private static EditorWindow OpenNewProjectBrowser(System.Type projectBrowserType)
    52. {
    53.     EditorWindow projectBrowser = EditorWindow.GetWindow(projectBrowserType);
    54.     projectBrowser.Show();
    55.  
    56.     // Unity does some special initialization logic, which we must call,
    57.     // before we can use the ShowFolderContents method (else we get a NullReferenceException).
    58.     MethodInfo init = projectBrowserType.GetMethod("Init", BindingFlags.Instance | BindingFlags.Public);
    59.     init.Invoke(projectBrowser, null);
    60.  
    61.     return projectBrowser;
    62. }
    As you can see, the code used a lot of reflection and can (should) be optimized in production code. Instead of performing all the type and method searching in every call, we can do it once at initialization and store delegates of the reflected methods. Also, I have omitted null checks. You probably want to handle failed reflection attempts.

    Disclaimer: My code assumes internal, undocumented and unsupported functionality, which might change at any point. It might not work in other Unity versions. However, I personally am not afraid of using internal methods, because they still tend to be fairly stable over many years, and I can usually update my own code relatively easily.

    Feature request: I'd like to see an official API to show folder content in the project browser. :)
    Also see: https://feedback.unity3d.com/sugges...er-in-the-project-window-via-editor-scripting
     
    Last edited: Feb 23, 2021
  2. shawn

    shawn

    Unity Technologies

    Joined:
    Aug 4, 2007
    Posts:
    552
    Good stuff! Thanks for sharing your discoveries with the community. :)
     
  3. halley

    halley

    Joined:
    Aug 26, 2013
    Posts:
    2,367
    Sorry for necropost. I'd love to be able to use this, but I can't figure out how to convert a
    string path
    into an
    int folderInstanceID
    . For example, a
    ShowFolderContents("Assets")
    overload would be nice. I may be missing something but the AssetDatabase isn't too helpful in this regard.
     
  4. Madgvox

    Madgvox

    Joined:
    Apr 13, 2014
    Posts:
    1,317
    Code (CSharp):
    1. AssetDatabase.LoadAssetAtPath<Object>( path ).GetInstanceID();
     
    gooby429 and halley like this.
  5. halley

    halley

    Joined:
    Aug 26, 2013
    Posts:
    2,367
    Thanks a lot! I got that to work with a little tweak; I had to specify <UnityEngine.Object> specifically.
     
  6. Xarbrough

    Xarbrough

    Joined:
    Dec 11, 2014
    Posts:
    1,188
    Coming back to this, it's possible to let AssetDatabase convert a path to an instanceID without loading the asset presumably.

    Code (CSharp):
    1. public static void ShowFolderContents(string path)
    2. {
    3.     var getInstanceIDMethod = typeof(AssetDatabase).GetMethod("GetMainAssetInstanceID",
    4.         BindingFlags.Static | BindingFlags.NonPublic);
    5.     int instanceID = (int)getInstanceIDMethod.Invoke(null, new object[] { path });
    6.     ShowFolderContents(instanceID);
    7. }
    This method works in Unity 2020.2, however since we don't really know about its internals its impossible to tell if its actually better, than:

    Code (CSharp):
    1. public static void ShowFolderContents(string guid)
    2. {
    3.     string path = AssetDatabase.GUIDToAssetPath(guid);
    4.     var folder = AssetDatabase.LoadAssetAtPath<DefaultAsset>(path);
    5.  
    6.     if (folder == null)
    7.     {
    8.         // If the resulting instanceID is not a folder, the ProjectBrowser won't complain,
    9.         // but it'll inspect the asset in a weird state (like only show the content of a PSB).
    10.         throw new System.ArgumentException(
    11.             "Must pass a guid of a folder object (DefaultAsset).", nameof(guid));
    12.     }
    13.  
    14.     ShowFolderContents(folder.GetInstanceID());
    15. }
    I'd recommend the GUID approach if its possible to know the GUID, for example if my tool wants users to select prefabs from the "Items" directory, it's easy to copy and paste the GUID from the folders meta file (you can also write a small editor extension for that). This allows additional folders to be inserted above the "Items" folder or allows for the folder to be moved within the project.
     
    VirtualDawn likes this.