Freebie: FileDropdown implementation using UnityEditor.IMGUI.Controls.AdvancedDropdown

Today I needed a lightweight and easy way of showing a dropdown with files or folders to pick from the StreamingAssets folder. So I implemented this:

5272050--551835--FileDropdown.jpg

#if UNITY_EDITOR
namespace Nementic
{
    using System;
    using System.IO;
    using UnityEditor.IMGUI.Controls;
    using UnityEngine;

    public class FileDropdown : AdvancedDropdown
    {
        /// <summary>
        /// Specifies the name of the top-level node. If not set, the root directory name will be used.
        /// </summary>
        public string rootName;

        /// <summary>
        /// An optional filter to apply to files, e.g. *.txt to only show text files.
        /// </summary>
        public string fileFilter;

        private readonly DirectoryInfo rootDirectory;
        private readonly Action<CallbackInfo> onFileSelected;
        private readonly Action<CallbackInfo, object> onFileSelectedAdvanced;
        private readonly object userData;

        public FileDropdown(AdvancedDropdownState state, string directoryPath, Action<CallbackInfo> onFileSelected)
            : this(state, directoryPath)
        {
            this.onFileSelected = onFileSelected;
        }

        public FileDropdown(AdvancedDropdownState state, string directoryPath, Action<CallbackInfo, object> onFileSelected, object userData)
            : this(state, directoryPath)
        {
            this.onFileSelectedAdvanced = onFileSelected;
            this.userData = userData;
        }

        private FileDropdown(AdvancedDropdownState state, string directoryPath) : base(state)
        {
            this.minimumSize = new Vector2(200, 300);
            this.rootDirectory = new DirectoryInfo(directoryPath);
        }

        protected override AdvancedDropdownItem BuildRoot()
        {
            if (string.IsNullOrEmpty(rootName))
                rootName = rootDirectory.Name;

            var root = new AdvancedDropdownItem(rootName);
            AddFileSystemEntries(root, rootDirectory);
            return root;
        }

        private void AddFileSystemEntries(AdvancedDropdownItem root, DirectoryInfo directory)
        {
            foreach (DirectoryInfo subDirectory in directory.EnumerateDirectories())
            {
                var folder = new FileDropdownItem(subDirectory.Name, subDirectory.FullName);
                folder.AddChild(new FileDropdownItem("Select Folder", subDirectory.FullName));
                AddFileSystemEntries(folder, subDirectory);
                root.AddChild(folder);
            }

            foreach (FileInfo file in directory.EnumerateFiles(fileFilter, SearchOption.TopDirectoryOnly))
            {
                var child = new FileDropdownItem(file.Name, file.FullName);
                root.AddChild(child);
            }
        }

        protected override void ItemSelected(AdvancedDropdownItem item)
        {
            var fileItem = (FileDropdownItem)item;
            var info = new CallbackInfo(fileItem.name, fileItem.fullName);

            onFileSelected?.Invoke(info);
            onFileSelectedAdvanced?.Invoke(info, userData);
        }

        private class FileDropdownItem : AdvancedDropdownItem
        {
            public readonly string fullName;

            public FileDropdownItem(string name, string fullName) : base(name)
            {
                this.fullName = fullName.Replace(@"\", "/");
            }
        }

        /// <summary>
        /// Provides information about the selected file or directory.
        /// </summary>
        public struct CallbackInfo
        {
            /// <summary>
            /// The name of the file (including its extension) or directory.
            /// </summary>
            public readonly string name;

            /// <summary>
            /// The full path of the file or directory.
            /// </summary>
            public readonly string fullName;

            public CallbackInfo(string name, string fullName)
            {
                this.name = name;
                this.fullName = fullName;
            }
        }
    }
}
#endif

And here is an example of how to use it:

public class MyComponent : UnityEngine.MonoBehaviour
{
    public string levelName;
}


#if UNITY_EDITOR
[UnityEditor.CustomEditor(typeof(MyComponent))]
public class MyComponentEditor : UnityEditor.Editor
{
    private UnityEditor.IMGUI.Controls.AdvancedDropdownState dropdownState;

    public override void OnInspectorGUI()
    {
        Rect controlRect = UnityEditor.EditorGUILayout.GetControlRect();
        Rect buttonRect = controlRect;

        controlRect.width -= 30;
        UnityEditor.EditorGUI.PropertyField(controlRect, serializedObject.FindProperty("levelName"));

        buttonRect.xMin = controlRect.xMax + 4;
        buttonRect.height -= 1;

        if (GUI.Button(buttonRect, new GUIContent(".."), UnityEditor.EditorStyles.miniButton))
        {
            if (dropdownState == null)
                dropdownState = new UnityEditor.IMGUI.Controls.AdvancedDropdownState();

            var dropdown = new FileDropdown(dropdownState, Application.streamingAssetsPath, OnFileSelected)
            {
                rootName = "My Custom Files & Folders",
                fileFilter = "*.txt"
            };

            dropdown.Show(buttonRect);
        }
    }

    private void OnFileSelected(FileDropdown.CallbackInfo info)
    {
        serializedObject.Update();
        serializedObject.FindProperty("levelName").stringValue = info.fullName;
        serializedObject.ApplyModifiedProperties();
    }
}
#endif

The FileDropdown makes it easy to select a number of files from a specific folder or its children or the folders themselves. With this I implemented a level data loading system which can either take a specific file to load, a series of file paths to load randomly or pick a random file from a folder by only setting the folder path. This allowed for a flexible workflow because it was possible to setup folders such as “Easy” and then continue to modify its content without having to fix object references on the components etc.

4 Likes

Thank you for sharing your solution, it helped me a lot! :slight_smile:

2 Likes