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

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

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]

/// <summary>
/// Selects a folder in the project window and shows its content.
/// Opens a new project window, if none is open yet.
/// </summary>
/// <param name="folderInstanceID">The instance of the folder asset to open.</param>
private static void ShowFolderContents(int folderInstanceID)
{
    // Find the internal ProjectBrowser class in the editor assembly.
    Assembly editorAssembly = typeof(Editor).Assembly;
    System.Type projectBrowserType = editorAssembly.GetType("UnityEditor.ProjectBrowser");

    // This is the internal method, which performs the desired action.
    // Should only be called if the project window is in two column mode.
    MethodInfo showFolderContents = projectBrowserType.GetMethod(
        "ShowFolderContents", BindingFlags.Instance | BindingFlags.NonPublic);

    // Find any open project browser windows.
    Object[] projectBrowserInstances = Resources.FindObjectsOfTypeAll(projectBrowserType);

    if (projectBrowserInstances.Length > 0)
    {
        for (int i = 0; i < projectBrowserInstances.Length; i++)
            ShowFolderContentsInternal(projectBrowserInstances[i], showFolderContents, folderInstanceID);
    }
    else
    {
        EditorWindow projectBrowser = OpenNewProjectBrowser(projectBrowserType);
        ShowFolderContentsInternal(projectBrowser, showFolderContents, folderInstanceID);
    }
}

private static void ShowFolderContentsInternal(Object projectBrowser, MethodInfo showFolderContents, int folderInstanceID)
{
    // Sadly, there is no method to check for the view mode.
    // We can use the serialized object to find the private property.
    SerializedObject serializedObject = new SerializedObject(projectBrowser);
    bool inTwoColumnMode = serializedObject.FindProperty("m_ViewMode").enumValueIndex == 1;

    if (!inTwoColumnMode)
    {
        // If the browser is not in two column mode, we must set it to show the folder contents.
        MethodInfo setTwoColumns = projectBrowser.GetType().GetMethod(
            "SetTwoColumns", BindingFlags.Instance | BindingFlags.NonPublic);
        setTwoColumns.Invoke(projectBrowser, null);
    }

    bool revealAndFrameInFolderTree = true;
    showFolderContents.Invoke(projectBrowser, new object[] { folderInstanceID, revealAndFrameInFolderTree });
}

private static EditorWindow OpenNewProjectBrowser(System.Type projectBrowserType)
{
    EditorWindow projectBrowser = EditorWindow.GetWindow(projectBrowserType);
    projectBrowser.Show();

    // Unity does some special initialization logic, which we must call,
    // before we can use the ShowFolderContents method (else we get a NullReferenceException).
    MethodInfo init = projectBrowserType.GetMethod("Init", BindingFlags.Instance | BindingFlags.Public);
    init.Invoke(projectBrowser, null);

    return projectBrowser;
}

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. :slight_smile:
Also see: https://feedback.unity3d.com/suggestions/navigate-to-a-specific-folder-in-the-project-window-via-editor-scripting

14 Likes

Good stuff! Thanks for sharing your discoveries with the community. :slight_smile:

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.

AssetDatabase.LoadAssetAtPath<Object>( path ).GetInstanceID();
3 Likes

Thanks a lot! I got that to work with a little tweak; I had to specify <UnityEngine.Object> specifically.

Coming back to this, it’s possible to let AssetDatabase convert a path to an instanceID without loading the asset presumably.

public static void ShowFolderContents(string path)
{
    var getInstanceIDMethod = typeof(AssetDatabase).GetMethod("GetMainAssetInstanceID",
        BindingFlags.Static | BindingFlags.NonPublic);
    int instanceID = (int)getInstanceIDMethod.Invoke(null, new object[] { path });
    ShowFolderContents(instanceID);
}

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:

public static void ShowFolderContents(string guid)
{
    string path = AssetDatabase.GUIDToAssetPath(guid);
    var folder = AssetDatabase.LoadAssetAtPath<DefaultAsset>(path);

    if (folder == null)
    {
        // If the resulting instanceID is not a folder, the ProjectBrowser won't complain,
        // but it'll inspect the asset in a weird state (like only show the content of a PSB).
        throw new System.ArgumentException(
            "Must pass a guid of a folder object (DefaultAsset).", nameof(guid));
    }

    ShowFolderContents(folder.GetInstanceID());
}

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.

2 Likes

This thread was very helpful. I noticed, though, that this method bypasses ProjectBrowser’s lock state, so it’ll switch to a new view even if the view is currently locked. I was able to fix that by reflecting into the ProjectBrowser instance to get the isLocked property:

public static bool IsProjectWindowLocked(Object projectBrowserInstance, Type projectBrowserType ) {
    PropertyInfo isLockedProperty = projectBrowserType.GetProperty("isLocked", BindingFlags.Instance | BindingFlags.NonPublic);
    bool isLocked = (bool)isLockedProperty.GetValue(projectBrowserInstance, null);
    return isLocked;
}
1 Like

For those coming here an wondering. I noticed sometimes this does not work. The mehods are all there (reflections work) and are called but the project view simply does not show the folder contents in the split view. I assume a timing issue.

This version tries to select the first asset in the folder first and then (with a delay ‘EditorApplication.delayCall’) attempts to select the folder. The delay makes it work more often. Also added the isLocked check mentioned above.

using UnityEditor;
using UnityEngine;
using System.Reflection;

namespace YourNameSpace
{
    /// <summary>
    /// Thanks to: https://discussions.unity.com/t/tutorial-how-to-to-show-specific-folder-content-in-the-project-window-via-editor-scripting/685248
    /// With some additions to make it work more often in Unity 6+.
    /// </summary>
    public static class ProjectViewUtils
    {
        public static void OpenFolderInProjectView(string folderPath)
        {
            // Load the folder as an object
            Object folder = AssetDatabase.LoadAssetAtPath<Object>(folderPath);
            OpenFolderInProjectView(folder);
        }

        public static void OpenFolderInProjectView(UnityEngine.Object folder)
        {
            // Load the folder as an object
            if (folder == null)
                return;

            // First try via assets inside.
            SelectFirstAssetInFolder(folder); // May or may not be needed.
            
            // Then try the folder directly. The delay makes it work more often
            EditorApplication.delayCall += () =>
            {
                ShowFolderContents(folder.GetInstanceID());
                Selection.activeObject = null;
            };
        }
        
        /// <summary>
        /// Selects the first asset inside the given folder.
        /// </summary>
        /// <param name="folder">The folder object (must be a folder in the Project view)</param>
        public static void SelectFirstAssetInFolder(Object folder)
        {
            string folderPath = AssetDatabase.GetAssetPath(folder);
            
            string[] guids = AssetDatabase.FindAssets("t:Object", new[] { folderPath });
            if (guids.Length == 0)
                return;

            string firstAssetPath = AssetDatabase.GUIDToAssetPath(guids[0]);
            Object firstAsset = AssetDatabase.LoadAssetAtPath<Object>(firstAssetPath);
            
            // Select
            if (firstAsset != null)
            {
                Selection.activeObject = firstAsset;
            }
        }
        
        /// <summary>
        /// Selects a folder in the project window and shows its content.
        /// Opens a new project window, if none is open yet.
        /// </summary>
        /// <param name="folderInstanceID">The instance of the folder asset to open.</param>
        private static void ShowFolderContents(int folderInstanceID)
        {
            // Find the internal ProjectBrowser class in the editor assembly.
            Assembly editorAssembly = typeof(Editor).Assembly;
            System.Type projectBrowserType = editorAssembly.GetType("UnityEditor.ProjectBrowser");
            
            // Abort if reflection failed.
            if (projectBrowserType == null)
                return;

            // This is the internal method, which performs the desired action.
            // Should only be called if the project window is in two column mode.
            MethodInfo showFolderContents = projectBrowserType.GetMethod(
                "ShowFolderContents", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);

            // Abort if reflection failed.
            if (showFolderContents == null)
                return;

            // Find any open project browser windows.
            Object[] projectBrowserInstances = Resources.FindObjectsOfTypeAll(projectBrowserType);

            if (projectBrowserInstances.Length > 0)
            {
                for (int i = 0; i < projectBrowserInstances.Length; i++)
                {
                    var window = projectBrowserInstances[i];
                    
                    // Skip if locked.
                    if (IsProjectWindowLocked(window, projectBrowserType, true))
                        continue;
                    
                    ShowFolderContentsInternal(window, showFolderContents, folderInstanceID);
                }
            }
            else
            {
                EditorWindow projectBrowser = OpenNewProjectBrowser(projectBrowserType);
                ShowFolderContentsInternal(projectBrowser, showFolderContents, folderInstanceID);
            }
        }
        
        public static bool IsProjectWindowLocked(Object projectBrowserInstance, System.Type projectBrowserType, bool defaultValue = true)
        {
            PropertyInfo isLockedProperty = projectBrowserType.GetProperty("isLocked", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
            
            // Assume default if reflection failed.
            if (isLockedProperty == null)
                return defaultValue;

            bool isLocked = (bool)isLockedProperty.GetValue(projectBrowserInstance, null);
            return isLocked;
        }

        private static void ShowFolderContentsInternal(Object projectBrowser, MethodInfo showFolderContents, int folderInstanceID)
        {
            // Sadly, there is no method to check for the view mode.
            // We can use the serialized object to find the private property.
            SerializedObject serializedObject = new SerializedObject(projectBrowser);
            bool inTwoColumnMode = serializedObject.FindProperty("m_ViewMode").enumValueIndex == 1;

            if (!inTwoColumnMode)
            {
                // If the browser is not in two column mode, we must set it to show the folder contents.
                MethodInfo setTwoColumns = projectBrowser.GetType().GetMethod(
                    "SetTwoColumns", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
                setTwoColumns.Invoke(projectBrowser, null);
            }

            bool revealAndFrameInFolderTree = true;
            showFolderContents.Invoke(projectBrowser, new object[] { folderInstanceID, revealAndFrameInFolderTree });
        }

        private static EditorWindow OpenNewProjectBrowser(System.Type projectBrowserType)
        {
            EditorWindow projectBrowser = EditorWindow.GetWindow(projectBrowserType);
            projectBrowser.Show();

            // Unity does some special initialization logic, which we must call,
            // before we can use the ShowFolderContents method (else we get a NullReferenceException).
            MethodInfo init = projectBrowserType.GetMethod("Init", BindingFlags.Instance | BindingFlags.Public);
            init.Invoke(projectBrowser, null);

            return projectBrowser;
        }
    }
}

Also the reflection results could be cached but I don’t use it that often.

Note, in one of the Unity 6 versions, they started using EntityId instead of int ids, and the functions have a different signature.
You have to replace

showFolderContents.Invoke(projectBrowser, new object[] { folderInstanceID, revealAndFrameInFolderTree });

with

EntityId folderEntityId = folderInstanceID;
showFolderContents.Invoke(projectBrowser, new object[] { folderEntityId, revealAndFrameInFolderTree });