├── .github └── FUNDING.yml ├── CHANGELOG.md ├── CHANGELOG.md.meta ├── Editor.meta ├── Editor ├── Addressables.meta ├── Addressables │ ├── AddressableEntryFinder.cs │ ├── AddressableEntryFinder.cs.meta │ ├── AddressableUtils.cs │ └── AddressableUtils.cs.meta ├── ObjectFinder.meta ├── ObjectFinder │ ├── AssetFileResult.cs │ ├── AssetFileResult.cs.meta │ ├── AssetRenameDetector.cs │ ├── AssetRenameDetector.cs.meta │ ├── ConfigFinder.cs │ ├── ConfigFinder.cs.meta │ ├── DropdownOption.cs │ ├── DropdownOption.cs.meta │ ├── FolderObjectFinder.cs │ ├── FolderObjectFinder.cs.meta │ ├── ObjectFinder.cs │ ├── ObjectFinder.cs.meta │ ├── ObjectFinderFactory.cs │ ├── ObjectFinderFactory.cs.meta │ ├── ScriptableContainerFinder.cs │ ├── ScriptableContainerFinder.cs.meta │ ├── ScriptableGroupFinder.cs │ └── ScriptableGroupFinder.cs.meta ├── PhEngine.QuickDropdown.Editor.asmdef ├── PhEngine.QuickDropdown.Editor.asmdef.meta ├── PropertyDrawer.meta ├── PropertyDrawer │ ├── DropdownFieldDrawer.cs │ ├── DropdownFieldDrawer.cs.meta │ ├── FieldUtils.cs │ └── FieldUtils.cs.meta ├── Utility.meta └── Utility │ ├── AssetUtils.cs │ ├── AssetUtils.cs.meta │ ├── IconUtils.cs │ └── IconUtils.cs.meta ├── LICENSE ├── LICENSE.meta ├── PhEngine.QuickDropdown.asmdef ├── PhEngine.QuickDropdown.asmdef.meta ├── README.md ├── README.md.meta ├── Runtime.meta ├── Runtime ├── Addressables.meta ├── Addressables │ ├── FromAddressable.cs │ └── FromAddressable.cs.meta ├── DropdownField.cs ├── DropdownField.cs.meta ├── FromConfig.cs ├── FromConfig.cs.meta ├── FromFolder.cs ├── FromFolder.cs.meta ├── FromGroup.cs ├── FromGroup.cs.meta ├── ScriptableContainer.cs ├── ScriptableContainer.cs.meta ├── ScriptableGroup.cs └── ScriptableGroup.cs.meta ├── Samples.meta ├── Samples ├── AddressableDropdownExample.cs ├── AddressableDropdownExample.cs.meta ├── ElementConfig.cs ├── ElementConfig.cs.meta ├── QuickDropdownExample.cs ├── QuickDropdownExample.cs.meta ├── SampleConfig.cs └── SampleConfig.cs.meta ├── package.json └── package.json.meta /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | buy_me_a_coffee: phanphantz 2 | github: phanphantz 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | This is the history log file for the Unity Quick Dropdown package. 2 | 3 | # 0.2.3 4 | 5 | **Fixed Bugs** 6 | - Fixed FromAddressable cannot find asset if the object name does not match the address. 7 | - Fixed wrong parameter in the example script. 8 | 9 | # 0.2.2 10 | 11 | **New Features** 12 | - Directly supported List & Array 13 | 14 | **Improvements** 15 | - UX Change: Creating new ScriptableObject from a '+' button will open it as floating window instead of jumping to it. 16 | 17 | **Fixed Bugs** 18 | - Fixed bug where FromFolder attribute will not find the folder if the path ends with '/' 19 | - Fix button and dropdown positioning calculation. 20 | 21 | # 0.2.1 22 | 23 | **Fixed Bugs** 24 | - Fixed Ambiguous function call error on Unity 2022 with Addressables package. 25 | 26 | # 0.2.0 27 | 28 | **New Features** 29 | - Supported dropdown display for nested elements inside **List & Array** 30 | - Added [FromAddressable] attribute. 31 | - Added [FromConfig] attribute. 32 | - Added a **Fix** button to create a new source if does not exist or recheck the source again after asset modifications. 33 | - Warn about renamed sources in the console. 34 | 35 | **Improvements** 36 | - Huge performance optimization by reducing the frequency of asset lookup. 37 | - Clicking on [FromFolder]'s source info will open the enclosing folder instead of just selecting it from outside. 38 | - Draw the property as a normal field if the source does not exist. 39 | - Undo support for ScriptableObject creation. 40 | - Edit error status color. Use orange instead of red. 41 | 42 | **Fixed Bugs** 43 | - Prevent cyclic referencing which leads to stack overflow 44 | - Prevent type mismatch error when using Dropdown fields with unsupported types 45 | 46 | # 0.1.0 47 | - Initial release -------------------------------------------------------------------------------- /CHANGELOG.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 59ce958d640047a282845f8be156b043 3 | timeCreated: 1742159446 -------------------------------------------------------------------------------- /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fb8809596206450c93e95a86d3b7b970 3 | timeCreated: 1740933594 -------------------------------------------------------------------------------- /Editor/Addressables.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c88209f8b8ac4fe9b60a572793c89adf 3 | timeCreated: 1741728699 -------------------------------------------------------------------------------- /Editor/Addressables/AddressableEntryFinder.cs: -------------------------------------------------------------------------------- 1 | #if ADDRESSABLES_DROPDOWN 2 | using System; 3 | using System.IO; 4 | using System.Linq; 5 | using UnityEditor; 6 | using UnityEditor.AddressableAssets.Settings; 7 | using UnityEngine; 8 | using Object = UnityEngine.Object; 9 | 10 | namespace PhEngine.QuickDropdown.Editor.Addressables 11 | { 12 | public class AddressableEntryFinder : ObjectFinder 13 | { 14 | AddressableAssetGroup Group => CachedSource as AddressableAssetGroup; 15 | static Texture unsafeAddressableIcon; 16 | public AddressableEntryFinder(DropdownField field, Type type) : base(field, type) 17 | { 18 | } 19 | 20 | public override bool IsTypeSupported(Type type) 21 | { 22 | return base.IsTypeSupported(type) || FieldUtils.IsTypeOrCollectionOfType(type); 23 | } 24 | 25 | public override string[] SearchForItems() 26 | { 27 | return FieldUtils.IsTypeOrCollectionOfType(Type) ? 28 | Group.entries.Select(e => e.address).ToArray() : 29 | Group.entries.Select(e => e.MainAsset.name).ToArray(); 30 | } 31 | 32 | public override object GetResultAtIndex(int index) 33 | { 34 | var entry = Group.entries.ElementAt(index); 35 | if (FieldUtils.IsTypeOrCollectionOfType(Type)) 36 | return entry.address; 37 | 38 | var path = AssetDatabase.GUIDToAssetPath(entry.guid); 39 | return AssetDatabase.LoadAssetAtPath(path); 40 | } 41 | 42 | public override void SelectAndPingSource() 43 | { 44 | Selection.activeObject = Group; 45 | EditorGUIUtility.PingObject(Group); 46 | } 47 | 48 | public override void CreateNewScriptableObject() 49 | { 50 | var folderPath = "Assets/Resources/QuickDropdown/AddressableAssets/" + Type.Name; 51 | if (!Directory.Exists(folderPath)) 52 | { 53 | Directory.CreateDirectory(folderPath); 54 | AssetDatabase.Refresh(); 55 | } 56 | 57 | Undo.IncrementCurrentGroup(); 58 | var id = Undo.GetCurrentGroup(); 59 | var createdItem = AssetUtils.CreateScriptableObjectAndOpen(Field.DefaultNewItemName, Type, folderPath); 60 | var actualPath = AssetDatabase.GetAssetPath(createdItem); 61 | AddressableUtils.AddToAddressableGroup(AssetDatabase.AssetPathToGUID(actualPath), ObjectPath, createdItem.name); 62 | Undo.CollapseUndoOperations(id); 63 | } 64 | 65 | public override Texture GetSourceIcon() 66 | { 67 | if (unsafeAddressableIcon == null) 68 | unsafeAddressableIcon = EditorGUIUtility.IconContent("d_UnityLogo").image; 69 | 70 | return unsafeAddressableIcon; 71 | } 72 | 73 | public override bool IsBelongToSource(object currentObject) 74 | { 75 | var targetObject = currentObject as Object; 76 | if (currentObject is string address) 77 | { 78 | return Group.entries.Any(e => e.address == address); 79 | } 80 | 81 | if (targetObject == null) 82 | return false; 83 | 84 | return AssetDatabase.TryGetGUIDAndLocalFileIdentifier(targetObject, out var guid, out long _) 85 | && Group.entries.Any(e => e.guid == guid); 86 | } 87 | 88 | protected override Object SearchForSource() 89 | { 90 | return AddressableUtils.GetFirstFoundGroup(ObjectPath); 91 | } 92 | 93 | protected override Object CreateNewSource() 94 | { 95 | return AddressableUtils.GetOrCreateGroup(ObjectPath); 96 | } 97 | } 98 | } 99 | #endif -------------------------------------------------------------------------------- /Editor/Addressables/AddressableEntryFinder.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 734287c7904141318865e7de67636e15 3 | timeCreated: 1741891421 -------------------------------------------------------------------------------- /Editor/Addressables/AddressableUtils.cs: -------------------------------------------------------------------------------- 1 | #if ADDRESSABLES_DROPDOWN 2 | using System; 3 | using System.Collections.Generic; 4 | using UnityEditor; 5 | using UnityEditor.AddressableAssets; 6 | using UnityEditor.AddressableAssets.Settings; 7 | using UnityEditor.AddressableAssets.Settings.GroupSchemas; 8 | using UnityEngine; 9 | 10 | namespace PhEngine.QuickDropdown.Editor.Addressables 11 | { 12 | public static class AddressableUtils 13 | { 14 | static AddressableAssetSettings GetDefaultAddressableAssetSettings() 15 | { 16 | if (unsafeSettings == null) 17 | unsafeSettings = AddressableAssetSettingsDefaultObject.GetSettings(true); 18 | 19 | return unsafeSettings; 20 | } 21 | 22 | static AddressableAssetSettings unsafeSettings; 23 | 24 | public static AddressableAssetGroup GetFirstFoundGroup(string groupName) 25 | { 26 | return GetDefaultAddressableAssetSettings().FindGroup(groupName); 27 | } 28 | 29 | public static AddressableAssetGroup GetOrCreateGroup(string groupName) 30 | { 31 | var existingGroup = GetFirstFoundGroup(groupName); 32 | if (existingGroup != null) 33 | return existingGroup; 34 | 35 | var settings = GetDefaultAddressableAssetSettings(); 36 | Undo.IncrementCurrentGroup(); 37 | var id = Undo.GetCurrentGroup(); 38 | Undo.RegisterCompleteObjectUndo(settings, "Create " + groupName); 39 | var targetGroup = settings.CreateGroup(groupName, false, false, true, new List()); 40 | Debug.Log($"Created new addressable group: {targetGroup.name}"); 41 | Undo.RegisterCreatedObjectUndo(targetGroup, "Create " + groupName); 42 | 43 | targetGroup.AddSchema(); 44 | targetGroup.AddSchema(); 45 | EditorUtility.SetDirty(targetGroup); 46 | EditorUtility.SetDirty(settings); 47 | 48 | Undo.CollapseUndoOperations(id); 49 | return targetGroup; 50 | } 51 | 52 | public static void AddToAddressableGroup(string guid, string groupName, string address) 53 | { 54 | var targetGroup = GetOrCreateGroup(groupName); 55 | var settings = GetDefaultAddressableAssetSettings(); 56 | Undo.RegisterCompleteObjectUndo(settings, "Create Entry"); 57 | settings.CreateOrMoveEntry(guid, targetGroup, false, false); 58 | if (targetGroup == null) 59 | throw new InvalidOperationException("Unable to get or create the group: " + groupName); 60 | 61 | targetGroup.GetAssetEntry(guid)?.SetAddress(address); 62 | } 63 | } 64 | } 65 | #endif -------------------------------------------------------------------------------- /Editor/Addressables/AddressableUtils.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ff1ba1bd12e64cb6b9f22af45e145e76 3 | timeCreated: 1741728713 -------------------------------------------------------------------------------- /Editor/ObjectFinder.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a057a36cf0974db8b02522e6b360d85d 3 | timeCreated: 1742032060 -------------------------------------------------------------------------------- /Editor/ObjectFinder/AssetFileResult.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace PhEngine.QuickDropdown.Editor 4 | { 5 | public class AssetFileResult 6 | { 7 | public string name; 8 | public string assetPath; 9 | public AssetFileResult(string assetPath) 10 | { 11 | name = Path.GetFileNameWithoutExtension(assetPath); 12 | this.assetPath = assetPath; 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /Editor/ObjectFinder/AssetFileResult.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5c11c3070ea849789b057afda810fcb5 3 | timeCreated: 1740946272 -------------------------------------------------------------------------------- /Editor/ObjectFinder/AssetRenameDetector.cs: -------------------------------------------------------------------------------- 1 | namespace PhEngine.QuickDropdown.Editor 2 | { 3 | public class AssetRenameDetector : UnityEditor.AssetModificationProcessor 4 | { 5 | static string[] OnWillSaveAssets(string[] paths) 6 | { 7 | ObjectFinderFactory.Dispose(); 8 | return paths; 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /Editor/ObjectFinder/AssetRenameDetector.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b512822d2ffe4d3faae6377bbc92c5e3 3 | timeCreated: 1742072232 -------------------------------------------------------------------------------- /Editor/ObjectFinder/ConfigFinder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using UnityEditor; 4 | using UnityEngine; 5 | using Object = UnityEngine.Object; 6 | 7 | namespace PhEngine.QuickDropdown.Editor 8 | { 9 | public class ConfigFinder : ScriptableContainerFinder 10 | { 11 | FromConfig FromConfig => Field as FromConfig; 12 | public ConfigFinder(DropdownField field, Type type) : base(field, type) 13 | { 14 | } 15 | 16 | protected override Object SearchForSource() 17 | { 18 | var possibleItems = AssetDatabase 19 | .FindAssets("t:" + FromConfig.ConfigType.Name) 20 | .Select(guid => AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guid))) 21 | .ToArray(); 22 | 23 | var result = possibleItems.FirstOrDefault(g => g); 24 | if (possibleItems.Length > 1 && result != null) 25 | Debug.LogWarning($"There are more than one Config Group of type: {FromConfig.ConfigType.Name} in the project. The first match '{result.name}' will be used. Please make sure there is only one instance of this type."); 26 | 27 | return result; 28 | } 29 | 30 | protected override ScriptableContainer CreateNewContainer(string groupPath) 31 | { 32 | return AssetUtils.CreateScriptableObject(FromConfig.ConfigType, groupPath) as ScriptableContainer; 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /Editor/ObjectFinder/ConfigFinder.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: aefbf68e4c8e4e13b10c4934e9401d0b 3 | timeCreated: 1741553766 -------------------------------------------------------------------------------- /Editor/ObjectFinder/DropdownOption.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using UnityEngine.Serialization; 5 | 6 | namespace PhEngine.QuickDropdown.Editor 7 | { 8 | [Serializable] 9 | public class DropdownOption 10 | { 11 | public string text; 12 | public List options = new List(); 13 | public DropdownOption(string text) 14 | { 15 | this.text = text; 16 | } 17 | 18 | public bool IsGroup => options.Count > 0; 19 | 20 | public void AddPath(IEnumerable pathSegments) 21 | { 22 | var currentLevel = this; 23 | foreach (var segment in pathSegments) 24 | { 25 | var existingOption = currentLevel.options.FirstOrDefault(o => o.text == segment); 26 | if (existingOption == null) 27 | { 28 | existingOption = new DropdownOption(segment); 29 | currentLevel.options.Add(existingOption); 30 | } 31 | currentLevel = existingOption; 32 | } 33 | } 34 | 35 | public static DropdownOption[] FromFlatPaths(string[] flatPaths) 36 | { 37 | var root = new DropdownOption(string.Empty); 38 | foreach (var path in flatPaths) 39 | { 40 | var segments = path.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries); 41 | root.AddPath(segments); 42 | } 43 | return root.options.ToArray(); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /Editor/ObjectFinder/DropdownOption.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7e8995bb30e440b6bad2bcfba2993f3f 3 | timeCreated: 1742114649 -------------------------------------------------------------------------------- /Editor/ObjectFinder/FolderObjectFinder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using UnityEditor; 5 | using UnityEngine; 6 | using Object = UnityEngine.Object; 7 | 8 | namespace PhEngine.QuickDropdown.Editor 9 | { 10 | public class FolderObjectFinder : ObjectFinder 11 | { 12 | AssetFileResult[] pathResults; 13 | string AssetPath { get; } 14 | 15 | public FolderObjectFinder(DropdownField field, Type type) : base(field, type) 16 | { 17 | var assetPath = ObjectPath.StartsWith("Assets/") ? ObjectPath : "Assets/" + ObjectPath; 18 | if (assetPath.EndsWith('/')) 19 | assetPath = assetPath.Substring(0, assetPath.Length - 1); 20 | 21 | AssetPath = assetPath; 22 | } 23 | 24 | protected override bool IsPathMatched() 25 | { 26 | return CachedSource.name == AssetPath.Split('/').LastOrDefault(); 27 | } 28 | 29 | public override string[] SearchForItems() 30 | { 31 | pathResults = FindInFolder(Type.Name, AssetPath); 32 | return pathResults.Select(r => 33 | { 34 | var path = r.assetPath.Replace(AssetPath, "").Split('.')[0]; 35 | if (path.StartsWith("/")) 36 | path = path.Substring(1); 37 | return path; 38 | }).ToArray(); 39 | } 40 | 41 | public override object GetResultAtIndex(int index) 42 | { 43 | return AssetUtils.LoadAssetAtPath(pathResults[index].assetPath, Type); 44 | } 45 | 46 | public override void SelectAndPingSource() 47 | { 48 | Selection.activeObject = CachedSource; 49 | EditorApplication.delayCall += () => { EditorApplication.ExecuteMenuItem("Assets/Open"); }; 50 | } 51 | 52 | public override void CreateNewScriptableObject() 53 | { 54 | if (string.IsNullOrEmpty(AssetPath)) 55 | throw new InvalidOperationException("Directory path is empty."); 56 | 57 | if (CachedSource == null) 58 | PrepareSource(); 59 | 60 | AssetUtils.CreateScriptableObjectAndOpen(Field.DefaultNewItemName, Type, AssetPath); 61 | } 62 | 63 | public override Texture GetSourceIcon() 64 | { 65 | return IconUtils.GetFolderIcon(); 66 | } 67 | 68 | public override bool IsBelongToSource(object currentObject) 69 | { 70 | var path = AssetUtils.GetAssetPath(currentObject as Object); 71 | if (pathResults == null) 72 | SearchForItems(); 73 | 74 | return path != null && pathResults != null && pathResults.Any(r => r.assetPath == path); 75 | } 76 | 77 | protected override Object SearchForSource() 78 | { 79 | if (!Directory.Exists(AssetPath)) 80 | return null; 81 | 82 | return AssetUtils.LoadAssetAtPath(AssetPath, typeof(Object)); 83 | } 84 | 85 | protected override Object CreateNewSource() 86 | { 87 | Directory.CreateDirectory(AssetPath); 88 | AssetDatabase.Refresh(); 89 | return SearchForSource(); 90 | } 91 | 92 | static AssetFileResult[] FindInFolder(string typeName, string folderPath) 93 | { 94 | if (!Directory.Exists(folderPath)) 95 | return new AssetFileResult[] { }; 96 | 97 | return AssetDatabase.FindAssets("t:" + typeName, new[] {folderPath}) 98 | .Select(guid => new AssetFileResult(AssetDatabase.GUIDToAssetPath(guid))) 99 | .ToArray(); 100 | } 101 | } 102 | } -------------------------------------------------------------------------------- /Editor/ObjectFinder/FolderObjectFinder.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a00a804071d34bbd9ba2b29de89a580e 3 | timeCreated: 1741019577 -------------------------------------------------------------------------------- /Editor/ObjectFinder/ObjectFinder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using Object = UnityEngine.Object; 5 | 6 | namespace PhEngine.QuickDropdown.Editor 7 | { 8 | public abstract class ObjectFinder 9 | { 10 | protected string ObjectPath { get; } 11 | protected DropdownField Field { get; } 12 | protected Type Type { get; } 13 | 14 | protected Object CachedSource { get; private set; } 15 | 16 | bool wasSearchPerformed; 17 | 18 | protected ObjectFinder(DropdownField field, Type type) 19 | { 20 | ObjectPath = field.Path; 21 | Field = field; 22 | Type = type; 23 | } 24 | 25 | public virtual bool IsTypeSupported(Type type) 26 | { 27 | return FieldUtils.IsTypeOrCollectionOfType(type); 28 | } 29 | 30 | public abstract string[] SearchForItems(); 31 | public abstract object GetResultAtIndex(int index); 32 | public abstract void SelectAndPingSource(); 33 | public abstract void CreateNewScriptableObject(); 34 | public abstract Texture GetSourceIcon(); 35 | public abstract bool IsBelongToSource(object currentObject); 36 | 37 | public bool CheckAndPrepareSource() 38 | { 39 | if (CachedSource) 40 | { 41 | if (IsPathMatched()) 42 | return true; 43 | 44 | Debug.LogWarning($"The old source '{ObjectPath}' was renamed. Either Rename it back, Change the Path in code, or Click 'Fix' to create new source with the correct name."); 45 | CachedSource = null; 46 | return false; 47 | } 48 | 49 | if (wasSearchPerformed) 50 | return false; 51 | 52 | SearchAndCacheSource(); 53 | wasSearchPerformed = true; 54 | return CachedSource; 55 | } 56 | 57 | protected virtual bool IsPathMatched() 58 | { 59 | return CachedSource.name == ObjectPath; 60 | } 61 | 62 | public void SearchAndCacheSource() 63 | { 64 | #if QDD_DEDUG 65 | Debug.Log(GetType().Name + " Perform Searching: " + ObjectPath); 66 | #endif 67 | CachedSource = SearchForSource(); 68 | } 69 | 70 | protected abstract Object SearchForSource(); 71 | public void CreateOrGetSourceFromInspector() 72 | { 73 | SearchAndCacheSource(); 74 | if (CachedSource) 75 | { 76 | Debug.Log($"[{GetType().Name}] Found an existing source with name '{CachedSource.name}'"); 77 | return; 78 | } 79 | 80 | PrepareSource(); 81 | Debug.Log($"[{GetType().Name}] Created a new source with name '{CachedSource.name}'"); 82 | } 83 | 84 | protected void PrepareSource() 85 | { 86 | CachedSource = CreateNewSource(); 87 | } 88 | 89 | protected abstract Object CreateNewSource(); 90 | public virtual string GetIdentityName(Object currentObject) 91 | { 92 | return currentObject.name; 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /Editor/ObjectFinder/ObjectFinder.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ab6e955c1d3d47238e9cafe3b8a621f5 3 | timeCreated: 1741008203 -------------------------------------------------------------------------------- /Editor/ObjectFinder/ObjectFinderFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | 5 | namespace PhEngine.QuickDropdown.Editor 6 | { 7 | public static class ObjectFinderFactory 8 | { 9 | static Dictionary> CachedFinders = new Dictionary>(); 10 | public static ObjectFinder GetFinder(DropdownField field, Type type) 11 | { 12 | if (!CachedFinders.TryGetValue(type, out var finderDict)) 13 | { 14 | var newFinder = CreateNewFinder(field, type); 15 | 16 | finderDict = new Dictionary { { field.Path, newFinder } }; 17 | CachedFinders.TryAdd(type, finderDict); 18 | 19 | return newFinder; 20 | } 21 | 22 | if (!finderDict.TryGetValue(field.Path, out var finder)) 23 | { 24 | finder = CreateNewFinder(field, type); 25 | finderDict.TryAdd(field.Path, finder); 26 | } 27 | return finder; 28 | } 29 | 30 | static ObjectFinder CreateNewFinder(DropdownField field, Type type) 31 | { 32 | #if QDD_DEDUG 33 | Debug.Log("Create new Finder: " + field.GetType().Name + " for type: " + type.Name); 34 | #endif 35 | switch (field) 36 | { 37 | case FromConfig: 38 | return new ConfigFinder(field, type); 39 | case FromFolder: 40 | return new FolderObjectFinder(field, type); 41 | case FromGroup: 42 | return new ScriptableGroupFinder(field, type); 43 | #if ADDRESSABLES_DROPDOWN 44 | case PhEngine.QuickDropdown.Addressables.FromAddressable: 45 | return new Addressables.AddressableEntryFinder(field, type); 46 | #endif 47 | default: 48 | throw new NotImplementedException($"Don't know how to get finder for {type}"); 49 | } 50 | } 51 | 52 | public static void Dispose() 53 | { 54 | #if QDD_DEDUG 55 | Debug.Log("Disposed all cached finders"); 56 | #endif 57 | CachedFinders = new Dictionary>(); 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /Editor/ObjectFinder/ObjectFinderFactory.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a765ca211169457b9b8253a6783ca5ba 3 | timeCreated: 1742031669 -------------------------------------------------------------------------------- /Editor/ObjectFinder/ScriptableContainerFinder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using UnityEditor; 4 | using UnityEngine; 5 | using Object = UnityEngine.Object; 6 | 7 | namespace PhEngine.QuickDropdown.Editor 8 | { 9 | public abstract class ScriptableContainerFinder : ObjectFinder 10 | { 11 | ScriptableContainer Container => CachedSource as ScriptableContainer; 12 | protected ScriptableContainerFinder(DropdownField field, Type type) : base(field, type) 13 | { 14 | } 15 | 16 | public override string[] SearchForItems() 17 | { 18 | return Container ? Container.GetStringOptions(Type) : new string[] { }; 19 | } 20 | 21 | public override object GetResultAtIndex(int index) 22 | { 23 | return Container.GetObjectFromFlatTree(Type, index); 24 | } 25 | 26 | public override void SelectAndPingSource() 27 | { 28 | AssetUtils.SelectAndPingInProjectTab(Container); 29 | } 30 | 31 | public override void CreateNewScriptableObject() 32 | { 33 | Undo.IncrementCurrentGroup(); 34 | var undoId = Undo.GetCurrentGroup(); 35 | if (Container == null) 36 | PrepareSource(); 37 | var groupPath = AssetUtils.GetAssetPath(Container); 38 | var newInstance = AssetUtils.CreateScriptableObjectAndOpen(Field.DefaultNewItemName, Type, Path.GetDirectoryName(groupPath)); 39 | Undo.RegisterCompleteObjectUndo(Container, "Create new ScriptableObject"); 40 | Container.AddObject(newInstance); 41 | EditorUtility.SetDirty(Container); 42 | Undo.CollapseUndoOperations(undoId); 43 | } 44 | 45 | public override Texture GetSourceIcon() 46 | { 47 | return IconUtils.GetScriptableObjectIcon(); 48 | } 49 | 50 | public override bool IsBelongToSource(object currentObject) 51 | { 52 | return Container.ContainsObject(currentObject as Object); 53 | } 54 | 55 | protected override Object CreateNewSource() 56 | { 57 | var groupPath = AssetUtils.GetDefaultAssetPath(ObjectPath); 58 | var directory = Path.GetDirectoryName(groupPath); 59 | if (string.IsNullOrEmpty(directory)) 60 | throw new InvalidOperationException("Directory path is empty."); 61 | 62 | if (!Directory.Exists(directory)) 63 | Directory.CreateDirectory(directory); 64 | 65 | return CreateNewContainer(groupPath); 66 | } 67 | 68 | protected abstract ScriptableContainer CreateNewContainer(string groupPath); 69 | } 70 | } -------------------------------------------------------------------------------- /Editor/ObjectFinder/ScriptableContainerFinder.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c9b1f2c8078342dc972423e608a8c842 3 | timeCreated: 1741019588 -------------------------------------------------------------------------------- /Editor/ObjectFinder/ScriptableGroupFinder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using UnityEditor; 4 | using Object = UnityEngine.Object; 5 | 6 | namespace PhEngine.QuickDropdown.Editor 7 | { 8 | public class ScriptableGroupFinder : ScriptableContainerFinder 9 | { 10 | public ScriptableGroupFinder(DropdownField field, Type type) : base(field, type) 11 | { 12 | } 13 | 14 | protected override Object SearchForSource() 15 | { 16 | var guids = AssetDatabase.FindAssets("t:" + nameof(ScriptableGroup) + " " + ObjectPath); 17 | foreach (var guid in guids) 18 | { 19 | var path = AssetDatabase.GUIDToAssetPath(guid); 20 | if (Path.GetFileNameWithoutExtension(path) != ObjectPath) 21 | continue; 22 | 23 | var obj = AssetDatabase.LoadAssetAtPath(path); 24 | if (obj != null) 25 | return obj; 26 | } 27 | return null; 28 | } 29 | 30 | protected override ScriptableContainer CreateNewContainer(string groupPath) 31 | { 32 | return AssetUtils.CreateScriptableObject(typeof(ScriptableGroup), groupPath) as ScriptableGroup; 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /Editor/ObjectFinder/ScriptableGroupFinder.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: eec0cee857d443f9945f8cf00340c94a 3 | timeCreated: 1741558235 -------------------------------------------------------------------------------- /Editor/PhEngine.QuickDropdown.Editor.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PhEngine.QuickDropdown.Editor", 3 | "rootNamespace": "PhEngine", 4 | "references": [ 5 | "PhEngine.QuickDropdown", 6 | "Unity.Addressables.Editor", 7 | "Unity.Addressables" 8 | ], 9 | "includePlatforms": [ 10 | "Editor" 11 | ], 12 | "excludePlatforms": [], 13 | "allowUnsafeCode": false, 14 | "overrideReferences": false, 15 | "precompiledReferences": [], 16 | "autoReferenced": true, 17 | "defineConstraints": [], 18 | "versionDefines": [ 19 | { 20 | "name": "com.unity.addressables", 21 | "expression": "0.0", 22 | "define": "ADDRESSABLES_DROPDOWN" 23 | } 24 | ], 25 | "noEngineReferences": false 26 | } -------------------------------------------------------------------------------- /Editor/PhEngine.QuickDropdown.Editor.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9e4af0773b2248de8f589bbb7b534d4f 3 | timeCreated: 1741893467 -------------------------------------------------------------------------------- /Editor/PropertyDrawer.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d46d5839a7754bcea35bca88bbbbf22d 3 | timeCreated: 1742032083 -------------------------------------------------------------------------------- /Editor/PropertyDrawer/DropdownFieldDrawer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using UnityEditor; 4 | using UnityEngine; 5 | using Object = UnityEngine.Object; 6 | 7 | namespace PhEngine.QuickDropdown.Editor 8 | { 9 | [CustomPropertyDrawer(typeof(DropdownField), true)] 10 | public class DropdownFieldDrawer : PropertyDrawer 11 | { 12 | static readonly Color LinkColor = new Color(0, 0.6f, 0.8f); 13 | static readonly Color ErrorColor = new Color(0.9f, 0.3f, 0); 14 | 15 | DropdownField Field { get; set; } 16 | Rect Position { get; set; } 17 | Rect DetailRect { get; set; } 18 | SerializedProperty Property { get; set; } 19 | GUIContent Label { get; set; } 20 | Type Type { get; set; } 21 | ObjectFinder Finder { get; set; } 22 | 23 | bool IsUnityObject { get; set; } 24 | bool IsSourceValid { get; set; } 25 | 26 | string Path => Field.Path; 27 | float SingleLineHeight => EditorGUIUtility.singleLineHeight; 28 | 29 | GUIContent[] options; 30 | string[] objectNames; 31 | 32 | public override float GetPropertyHeight(SerializedProperty property, GUIContent label) 33 | { 34 | return base.GetPropertyHeight(property, label) 35 | + (!((DropdownField)attribute).IsHideInfo ? EditorGUIUtility.singleLineHeight + 3f : 0); 36 | } 37 | 38 | public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) 39 | { 40 | Position = position; 41 | Property = property; 42 | Label = label; 43 | 44 | DetailRect = new Rect(Position.x, Position.y + SingleLineHeight, Position.width, SingleLineHeight); 45 | 46 | //Get Type information 47 | Type = FieldUtils.GetFlatFieldType(property); 48 | if (Type == null) 49 | { 50 | DrawDefaultField(); 51 | return; 52 | } 53 | 54 | IsUnityObject = Type.IsSubclassOf(typeof(Object)); 55 | if (Type.IsSubclassOf(typeof(MonoBehaviour))) 56 | Type = typeof(GameObject); 57 | 58 | //Get DropdownField 59 | Field = (DropdownField)attribute; 60 | if (Field == null) 61 | { 62 | DrawFallbackField("Attribute is invalid"); 63 | return; 64 | } 65 | 66 | if (Field.CheckInvalid(out var error)) 67 | { 68 | DrawFallbackField(error.Message); 69 | return; 70 | } 71 | 72 | //Get ObjectFinder 73 | Finder = ObjectFinderFactory.GetFinder(Field, Type); 74 | if (!Finder.IsTypeSupported(Type)) 75 | { 76 | DrawFallbackField("This type is not supported by the attribute " + Field.GetType().Name); 77 | return; 78 | } 79 | 80 | IsSourceValid = Finder.CheckAndPrepareSource(); 81 | if (!IsSourceValid) 82 | { 83 | DrawDefaultField(); 84 | } 85 | else 86 | { 87 | if (!TryDrawContent()) 88 | return; 89 | } 90 | 91 | if (!Field.IsHideInfo) 92 | DrawGroupInfo(); 93 | } 94 | 95 | void DrawDefaultField() 96 | { 97 | var rect = Position; 98 | rect.height = EditorGUIUtility.singleLineHeight; 99 | EditorGUI.PropertyField(rect, Property, Label); 100 | #if QDD_DEDUG 101 | Debug.Log("Draw Default field"); 102 | #endif 103 | } 104 | 105 | void DrawFallbackField(string reason) 106 | { 107 | var oldColor = GUI.color; 108 | GUI.color = ErrorColor; 109 | DrawDefaultField(); 110 | if (Field is { IsHideInfo: false }) 111 | { 112 | GUI.Label(DetailRect, reason, EditorStyles.miniLabel); 113 | } 114 | 115 | GUI.color = oldColor; 116 | } 117 | 118 | bool TryDrawContent() 119 | { 120 | var isShouldDrawCreateButton = Type.IsSubclassOf(typeof(ScriptableObject)) && !Field.IsHideCreateSOButton; 121 | var isShouldDrawInspectButton = !Field.IsHideInspectButton && IsUnityObject; 122 | 123 | var buttonWidth = 25f; 124 | var allButtonWidth = 0f; 125 | if (isShouldDrawCreateButton) 126 | allButtonWidth += buttonWidth; 127 | if (isShouldDrawInspectButton) 128 | allButtonWidth += buttonWidth; 129 | 130 | Rect remainingRect = EditorGUI.PrefixLabel(Position, Label); 131 | remainingRect.width -= allButtonWidth; 132 | var isFocused = remainingRect.Contains(Event.current.mousePosition); 133 | if (isFocused || options == null) 134 | PrepareSearchResults(); 135 | 136 | var oldColor = GUI.color; 137 | var dimmedColor = oldColor; 138 | dimmedColor.a = 0.8f; 139 | GUI.color = isFocused ? oldColor : dimmedColor; 140 | 141 | remainingRect.height = SingleLineHeight; 142 | if (!TryDrawDropdown(remainingRect)) 143 | { 144 | GUI.color = oldColor; 145 | return false; 146 | } 147 | GUI.color = oldColor; 148 | 149 | if (isShouldDrawInspectButton) 150 | DrawInspectButton(allButtonWidth, buttonWidth); 151 | 152 | if (isShouldDrawCreateButton) 153 | DrawCreateButton(allButtonWidth, buttonWidth); 154 | 155 | return true; 156 | } 157 | 158 | void PrepareSearchResults() 159 | { 160 | #if QDD_DEDUG 161 | Debug.Log($"[{Field.GetType().Name}] Build Options for Dropdown"); 162 | #endif 163 | var results = Finder.SearchForItems(); 164 | objectNames = results 165 | .Select(result => result.Split('/').LastOrDefault()) 166 | .ToArray(); 167 | 168 | var baseOptions = new[] { GetNullItemContent() }; 169 | var icon = IconUtils.GetIconForType(Type); 170 | options = baseOptions 171 | .Concat(results.Select(s => new GUIContent(s, icon))) 172 | .ToArray(); 173 | } 174 | 175 | static GUIContent GetNullItemContent() 176 | { 177 | return new GUIContent("NULL", IconUtils.GetWarningIcon()); 178 | } 179 | 180 | bool TryDrawDropdown(Rect rect) 181 | { 182 | Object currentObject = null; 183 | string rawAddress = ""; 184 | if (IsUnityObject) 185 | { 186 | currentObject = Property.objectReferenceValue; 187 | } 188 | else if (Type == typeof(string)) 189 | { 190 | rawAddress = Property.stringValue; 191 | } 192 | 193 | //Recheck if the object reference really belong to source 194 | if ((IsUnityObject ? currentObject : !string.IsNullOrEmpty(rawAddress)) && !Finder.IsBelongToSource(IsUnityObject ? currentObject : rawAddress)) 195 | { 196 | DrawFallbackField($"The Object does not belong to path: {Path}"); 197 | return false; 198 | } 199 | 200 | var currentIndex = FindCurrentIndex(); 201 | var selectedIndex = EditorGUI.Popup(rect, currentIndex, options); 202 | if (selectedIndex == currentIndex) 203 | return true; 204 | 205 | if (selectedIndex != 0) 206 | { 207 | var targetObject = Finder.GetResultAtIndex(selectedIndex - 1); 208 | ApplyChangeToProperty(targetObject); 209 | } 210 | else 211 | { 212 | ApplyChangeToProperty(null); 213 | } 214 | 215 | return true; 216 | } 217 | 218 | int FindCurrentIndex() 219 | { 220 | var index = -1; 221 | Object currentObject = null; 222 | string rawAddress = ""; 223 | if (IsUnityObject) 224 | { 225 | currentObject = Property.objectReferenceValue; 226 | } 227 | else if (Type == typeof(string)) 228 | { 229 | rawAddress = Property.stringValue; 230 | } 231 | 232 | if (currentObject || !string.IsNullOrEmpty(rawAddress)) 233 | { 234 | index = Array.IndexOf(objectNames, IsUnityObject && currentObject ? Finder.GetIdentityName(currentObject) : rawAddress); 235 | } 236 | 237 | //Index 0 is NULL option 238 | index++; 239 | return index; 240 | } 241 | 242 | void ApplyChangeToProperty(object targetObject) 243 | { 244 | if (IsUnityObject) 245 | Property.objectReferenceValue = targetObject as Object; 246 | else if (Type == typeof(string)) 247 | Property.stringValue = targetObject as string; 248 | Property.serializedObject.ApplyModifiedProperties(); 249 | } 250 | 251 | void DrawCreateButton(float allButtonWidth, float buttonWidth) 252 | { 253 | var createButtonRect = new Rect( 254 | Position.x + Position.width - allButtonWidth + (Field.IsHideInspectButton ? 0 : buttonWidth), 255 | Position.y, buttonWidth, SingleLineHeight); 256 | 257 | if (GUI.Button(createButtonRect, "+")) 258 | Finder.CreateNewScriptableObject(); 259 | } 260 | 261 | void DrawInspectButton(float allButtonWidth, float buttonWidth) 262 | { 263 | var inspectButtonRect = new Rect(Position.x + Position.width - allButtonWidth, Position.y, 264 | buttonWidth, 265 | SingleLineHeight); 266 | DrawObjectInspectButton(Property, inspectButtonRect, Field); 267 | } 268 | 269 | void DrawGroupInfo() 270 | { 271 | var image = Finder.GetSourceIcon(); 272 | 273 | Rect iconRect = new Rect(DetailRect.x + 10, DetailRect.y + 3, 12, 12); 274 | var pathContent = new GUIContent(Path, IsSourceValid ? "Click to search for the source again" : "Click to Jump to the source"); 275 | var pathWidth = EditorStyles.miniLabel.CalcSize(pathContent).x; 276 | Rect buttonRect = new Rect(DetailRect.x + 30, DetailRect.y, pathWidth, DetailRect.height); 277 | GUI.DrawTexture(iconRect, image, ScaleMode.ScaleToFit); 278 | var oldColor = GUI.color; 279 | GUI.color = IsSourceValid ? LinkColor : Color.yellow; 280 | if (GUI.Button(buttonRect, pathContent, EditorStyles.miniLabel)) 281 | { 282 | if (IsSourceValid) 283 | Finder.SelectAndPingSource(); 284 | else 285 | Finder.SearchAndCacheSource(); 286 | } 287 | 288 | if (!IsSourceValid) 289 | { 290 | buttonRect.x += buttonRect.width; 291 | buttonRect.width = 30f; 292 | GUI.color = oldColor; 293 | if (GUI.Button(buttonRect, new GUIContent("Fix", "Search for a source with the specified name or Create it if not found."), EditorStyles.miniButton)) 294 | Finder.CreateOrGetSourceFromInspector(); 295 | } 296 | 297 | GUI.color = oldColor; 298 | } 299 | 300 | static void DrawObjectInspectButton(SerializedProperty property, Rect inspectButtonRect, DropdownField field) 301 | { 302 | EditorGUI.BeginDisabledGroup(property.objectReferenceValue == null); 303 | if (GUI.Button(inspectButtonRect, new GUIContent(EditorGUIUtility.IconContent("d_Search Icon")))) 304 | { 305 | switch (field.InspectMode) 306 | { 307 | case InspectMode.OpenPropertyWindow: 308 | EditorGUIUtility.PingObject(property.objectReferenceValue); 309 | EditorUtility.OpenPropertyEditor(property.objectReferenceValue); 310 | break; 311 | case InspectMode.Select: 312 | AssetUtils.SelectAndPingInProjectTab(property.objectReferenceValue); 313 | break; 314 | default: 315 | throw new ArgumentOutOfRangeException(); 316 | } 317 | } 318 | 319 | EditorGUI.EndDisabledGroup(); 320 | } 321 | } 322 | } -------------------------------------------------------------------------------- /Editor/PropertyDrawer/DropdownFieldDrawer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5ce0e5f0b26949ce9e3a1917b91eedfa 3 | timeCreated: 1740933604 -------------------------------------------------------------------------------- /Editor/PropertyDrawer/FieldUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | using UnityEditor; 5 | 6 | namespace PhEngine.QuickDropdown.Editor 7 | { 8 | public static class FieldUtils 9 | { 10 | public static Type GetFlatFieldType(SerializedProperty property) 11 | { 12 | var targetObject = property.serializedObject.targetObject; 13 | var targetType = targetObject.GetType(); 14 | return ToFlatType(GetFieldViaPath(targetType, property.propertyPath)?.FieldType); 15 | } 16 | 17 | public static bool IsTypeOrCollectionOfType(Type type) 18 | { 19 | return type == typeof(T) || type.IsSubclassOf(typeof(T)) || IsArrayOrListOf(type); 20 | } 21 | 22 | static Type ToFlatType(Type type) 23 | { 24 | if (type.IsArray) 25 | return type.GetElementType(); 26 | 27 | if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)) 28 | return type.GetGenericArguments()[0]; 29 | 30 | return type; 31 | } 32 | 33 | static bool IsArrayOrListOf(Type type) 34 | { 35 | if (type.IsArray && type.GetElementType()!.IsSubclassOf(typeof(T))) 36 | return true; 37 | 38 | if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)) 39 | { 40 | var elementType = type.GetGenericArguments()[0]; 41 | return elementType.IsSubclassOf(typeof(T)); 42 | } 43 | 44 | return false; 45 | } 46 | 47 | /// 48 | /// Taken from: https://discussions.unity.com/t/a-smarter-way-to-get-the-type-of-serializedproperty/186674/6 49 | /// 50 | /// 51 | /// 52 | /// 53 | static FieldInfo GetFieldViaPath(Type type, string path) 54 | { 55 | var flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; 56 | var parent = type; 57 | var fi = parent.GetField(path, flags); 58 | var paths = path.Split('.'); 59 | for (int i = 0; i < paths.Length; i++) 60 | { 61 | fi = parent?.GetField(paths[i], flags); 62 | if (fi != null) 63 | { 64 | // there are only two container field type that can be serialized: 65 | // Array and List 66 | if (fi.FieldType.IsArray) 67 | { 68 | parent = fi.FieldType.GetElementType(); 69 | i += 2; 70 | continue; 71 | } 72 | 73 | if (fi.FieldType.IsGenericType) 74 | { 75 | parent = fi.FieldType.GetGenericArguments()[0]; 76 | i += 2; 77 | continue; 78 | } 79 | 80 | parent = fi.FieldType; 81 | } 82 | else 83 | { 84 | break; 85 | } 86 | } 87 | 88 | if (fi == null) 89 | return type.BaseType != null ? GetFieldViaPath(type.BaseType, path) : null; 90 | 91 | return fi; 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /Editor/PropertyDrawer/FieldUtils.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 46d2538279094f26af7a470fff154a06 3 | timeCreated: 1742031227 -------------------------------------------------------------------------------- /Editor/Utility.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e820195a814d4bd6bb0bc022b6850f0b 3 | timeCreated: 1742032261 -------------------------------------------------------------------------------- /Editor/Utility/AssetUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using UnityEditor; 4 | using UnityEngine; 5 | using Object = UnityEngine.Object; 6 | 7 | namespace PhEngine.QuickDropdown.Editor 8 | { 9 | public static class AssetUtils 10 | { 11 | public static Object LoadAssetAtPath(string path, Type type) 12 | { 13 | return AssetDatabase.LoadAssetAtPath(path, type); 14 | } 15 | 16 | public static string GetAssetPath(Object obj) 17 | { 18 | return AssetDatabase.GetAssetPath(obj); 19 | } 20 | 21 | public static string GetDefaultAssetPath(string assetName) 22 | { 23 | return $"Assets/Resources/QuickDropdown/{assetName}.asset"; 24 | } 25 | 26 | public static void SelectAndPingInProjectTab(Object obj) 27 | { 28 | Selection.activeObject = obj; 29 | EditorGUIUtility.PingObject(obj); 30 | } 31 | 32 | public static ScriptableObject CreateScriptableObjectAndOpen(string name, Type type, string folderPath) 33 | { 34 | name = string.IsNullOrEmpty(name) ? type.Name : name; 35 | var assetPath = GetUniqueAssetFilePath(name, folderPath); 36 | var loadedInstance = CreateScriptableObject(type, assetPath); 37 | EditorGUIUtility.PingObject(loadedInstance); 38 | EditorUtility.OpenPropertyEditor(loadedInstance); 39 | return loadedInstance; 40 | } 41 | 42 | public static ScriptableObject CreateScriptableObject(Type type, string assetPath) 43 | { 44 | AssetDatabase.CreateAsset(ScriptableObject.CreateInstance(type), assetPath); 45 | AssetDatabase.SaveAssets(); 46 | AssetDatabase.Refresh(); 47 | 48 | var loadedInstance = AssetDatabase.LoadAssetAtPath(assetPath); 49 | Undo.RegisterCreatedObjectUndo(loadedInstance, "Create ScriptableObject"); 50 | return loadedInstance; 51 | } 52 | 53 | static string GetUniqueAssetFilePath(string name, string path) 54 | { 55 | string assetPath = $"{path}/{name}.asset"; 56 | var index = 0; 57 | while (File.Exists(assetPath)) 58 | { 59 | assetPath = $"{path}/{name}_{index}.asset"; 60 | index++; 61 | } 62 | 63 | return assetPath; 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /Editor/Utility/AssetUtils.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9aa9d0fab2f14ca1aaa535aa7f64d148 3 | timeCreated: 1742032220 -------------------------------------------------------------------------------- /Editor/Utility/IconUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using UnityEditor; 4 | using UnityEngine; 5 | 6 | namespace PhEngine.QuickDropdown.Editor 7 | { 8 | public static class IconUtils 9 | { 10 | static Texture GetObjectIcon() 11 | { 12 | if (unsafeObjectIcon) 13 | return unsafeObjectIcon; 14 | 15 | unsafeObjectIcon = EditorGUIUtility.IconContent("d_Prefab On Icon").image; 16 | return unsafeObjectIcon; 17 | } 18 | 19 | static Texture GetPrefabIcon() 20 | { 21 | if (unsafePrefabIcon) 22 | return unsafePrefabIcon; 23 | 24 | unsafePrefabIcon = EditorGUIUtility.IconContent("d_Prefab Icon").image; 25 | return unsafePrefabIcon; 26 | } 27 | 28 | public static Texture GetScriptableObjectIcon() 29 | { 30 | if (unsafeScriptableObjectIcon) 31 | return unsafeScriptableObjectIcon; 32 | 33 | unsafeScriptableObjectIcon = EditorGUIUtility.IconContent("ScriptableObject Icon").image; 34 | return unsafeScriptableObjectIcon; 35 | } 36 | 37 | public static Texture GetWarningIcon() 38 | { 39 | if (unsafeWarningIcon) 40 | return unsafeWarningIcon; 41 | 42 | unsafeWarningIcon = EditorGUIUtility.IconContent("console.warnicon.inactive.sml@2x").image; 43 | return unsafeWarningIcon; 44 | } 45 | 46 | public static Texture GetFolderIcon() 47 | { 48 | if (unsafeFolderIcon) 49 | return unsafeFolderIcon; 50 | 51 | unsafeFolderIcon = EditorGUIUtility.IconContent("d_FolderOpened Icon").image; 52 | return unsafeFolderIcon; 53 | } 54 | 55 | static Texture unsafeScriptableObjectIcon; 56 | static Texture unsafeWarningIcon; 57 | static Texture unsafeFolderIcon; 58 | static Texture unsafePrefabIcon; 59 | static Texture unsafeObjectIcon; 60 | 61 | public static Texture GetIconForType(Type type) 62 | { 63 | if (type == typeof(string)) 64 | return null; 65 | if (type.IsSubclassOf(typeof(ScriptableObject))) 66 | return GetScriptableObjectIcon(); 67 | if (type.IsSubclassOf(typeof(GameObject)) || type.IsSubclassOf(typeof(Component)) || 68 | type.IsSubclassOf(typeof(MonoBehaviour))) 69 | return GetPrefabIcon(); 70 | 71 | var typeName = type.Name; 72 | var commonAssetTypes = new string[] 73 | { 74 | "Sprite", "AudioClip", "Texture", "Texture2D", "Material", "TextAsset", "VideoClip", "AnimationClip", 75 | "Mesh", "Animator", "AnimatorController", "AnimatorOverrideController", "Avatar" 76 | }; 77 | var mightBeCommonAssetType = commonAssetTypes.Contains(typeName) 78 | ? EditorGUIUtility.IconContent(typeName + " Icon").image 79 | : null; 80 | return mightBeCommonAssetType != null ? mightBeCommonAssetType : GetObjectIcon(); 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /Editor/Utility/IconUtils.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 456f264fc75d49518ac5e2732c055317 3 | timeCreated: 1740935338 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Phun Peeticharoenthum 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: befcd927d5014792b619d7d3e3a7c989 3 | timeCreated: 1741203928 -------------------------------------------------------------------------------- /PhEngine.QuickDropdown.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PhEngine.QuickDropdown", 3 | "rootNamespace": "PhEngine", 4 | "references": [ 5 | "Unity.Addressables" 6 | ], 7 | "includePlatforms": [], 8 | "excludePlatforms": [], 9 | "allowUnsafeCode": false, 10 | "overrideReferences": false, 11 | "precompiledReferences": [], 12 | "autoReferenced": true, 13 | "defineConstraints": [], 14 | "versionDefines": [ 15 | { 16 | "name": "com.unity.addressables", 17 | "expression": "0.0", 18 | "define": "ADDRESSABLES_DROPDOWN" 19 | } 20 | ], 21 | "noEngineReferences": false 22 | } -------------------------------------------------------------------------------- /PhEngine.QuickDropdown.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5e2df7d594b84fc2bd4ff8633277f4b0 3 | timeCreated: 1741893443 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⚡Unity Quick Dropdown 2 | 3 | C# Attributes that allow you to quickly assign Unity assets from a Dropdown on the Inspector. Support any `UnityEngine.Object` types. Help save time and reduce human errors by letting you pick an object from your desired location. 4 | 5 | 6 | 7 | > [!NOTE] 8 | > This Library is not an Official Library from Unity. 9 | 10 | # Overview 11 | 12 | - **[FromFolder]** - Display a Dropdown of Unity Assets from a specific **Folder**. 13 | - **[FromGroup]** - Display a Dropdown of Unity Assets from the `ScriptableGroup` with a matching name. 14 | - Using `ScriptableGroups` allows you to move the assets in the project around without losing their group organization. 15 | - You can nest ScriptableGroup inside each other. Cyclic references are also prevented and filtered out. 16 | - **[FromConfig]** - Display a Dropdown of Unity Assets from a first found `ScriptableContainer` with a specified type. 17 | - This is useful for looking up objects from a ScriptableObject that is meant to be a singular "Config" or "Setting" (a Singleton if you will) 18 | - **[FromAddressable]** - Display a Dropdown of Addressable Assets from a specific Addressable Group. 19 | - Supported **List & Array** (Both direct usage & nested elements) 20 | - **QoL Features**: 21 | - Select & Jump to the assigned asset or its enclosing location. 22 | - You get a **Fix** button for creating a new source if it does not exist. 23 | - An **Inspect** button allows you to quickly open a floating property window of the assigned asset. 24 | - Warn user if the assigned object does not belong to the specified location. 25 | - Warn about invalid locations. 26 | - Supports nested dropdown. 27 | - Easily customize how the Dropdown look using attribute parameters. 28 | - Supports Multi-Edit. 29 | - Undo-Friendly. 30 | 31 | - For **ScriptableObjects**: 32 | - Quickly create new instances of `ScriptableObject` and add them into the specified location from a `+` button. When creating a new asset this way, the enclosing Folder / ScriptableGroup are also **created automatically** if they didn't exist. 33 | 34 | ## **Installation** 35 | There are 2 options to install the package: 36 | - A) Download source code and put them into the Unity's Assets folder 37 | - B) Install from the **Package Manager** using this git URL: https://github.com/phanphantz/Unity-Quick-Dropdown.git 38 | 39 | # Quick Example 40 | 41 | ```csharp 42 | using PhEngine.QuickDropdown; 43 | using UnityEngine; 44 | 45 | public class QuickDropdownExample : MonoBehaviour 46 | { 47 | public float health; 48 | 49 | //Let user pick 'ElementConfig' asset from the ScriptableGroup named 'TestGroup' in the asset folder. 50 | //By default, This also display Inspect button, Create button (Only for ScriptableObjects), and a mini button to jump to the enclosing group. 51 | [FromGroup("TestGroup"), SerializeField] 52 | ElementConfig element; 53 | 54 | //Let user pick 'ElementConfig' from a first found ScriptableObject with the type of 'SampleConfig' 55 | [FromConfig(typeof(SampleConfig)), SerializeField] 56 | ElementConfig sampleConfigItem; 57 | 58 | public float attack; 59 | public float stamina; 60 | 61 | //Let user pick Sprite from the folder 'Assets/Sprites' and all the subfolders below. 62 | //The folder information is hidden from 'isHideInfo' flag 63 | //These path variations also work: 'Assets/Sprites', 'Assets/Sprites/', 'Sprites/' 64 | [FromFolder("Sprites", isHideInfo: true), SerializeField] 65 | Sprite sprite; 66 | 67 | //By default, Inspect button will open the assigned asset as a floating window. 68 | //You can change the button behaviour using different InpsectModes 69 | [FromFolder("Prefabs", inspectMode: InspectMode.Select), SerializeField] 70 | GameObject prefab; 71 | } 72 | ``` 73 | ### Result: 74 | 75 | 76 | # List & Array Support 77 | ```csharp 78 | //Quick Dropdown now directly supports List & Array 79 | [FromGroup("TestGroup"), SerializeField] 80 | List directList = new List(); 81 | 82 | [FromGroup("TestGroup"), SerializeField] 83 | ElementConfig[] directArray = new ElementConfig[] {}; 84 | 85 | //Nested List & Array also works 86 | [SerializeField] List nestedDropdownList = new List(); 87 | [SerializeField] ElementConfigData[] nestedDropdownArray = new ElementConfigData[] {}; 88 | ``` 89 | 90 | # Addressables Support 91 | If you have the **Addressables** package installed in the project, you can use **[FromAddressable]** attribute on **Unity Object fields** and **string fields** to display a dropdown of Addressable assets from a desired group. 92 | - When you specify an Addressable group name that does not exist. You also get the **Fix** button on the inspector to quickly create it. 93 | - Creating **ScriptableObjects** using **Create** button from the inspector will also add the created asset to the target Addressable Group. 94 | 95 | ```csharp 96 | using PhEngine.QuickDropdown.Addressables; 97 | using UnityEngine; 98 | 99 | public class AddressableDropdownExample : MonoBehaviour 100 | { 101 | [FromAddressable("PackedAddressableGroup"), SerializeField] 102 | ElementConfig addressableConfig; 103 | 104 | [FromAddressable("PackedAddressableGroup"), SerializeField] 105 | string addressableAddress; 106 | } 107 | ``` 108 | > [!NOTE] 109 | > Unfortunately **[FromAddressable]** does not work with **AssetReference** at the moment. Since it is drawn by its own property drawer. 110 | 111 | # Future Plans 112 | - **[FromScene]** attribute. 113 | - **[FromStringList]** attribute. 114 | - A way to bind Create functions for creating non-ScriptableObject assets. 115 | - A Popup UGUI to specify the name of the asset upon creation. 116 | 117 | Please feel free to Contribute and send me Pull requests. 118 | You can also [**Buy me a coffee!**](https://buymeacoffee.com/phanphantz)☕ 119 | 120 | **Phun,**\ 121 | phun.peeticharoenthum@gmail.com 122 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 874241f5d0d947ceb3472981523ffe14 3 | timeCreated: 1741199414 -------------------------------------------------------------------------------- /Runtime.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f1b11d70b0524ab6887b3c7f997f6b24 3 | timeCreated: 1741120379 -------------------------------------------------------------------------------- /Runtime/Addressables.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ef367a6cb49543e38214b57c1f7149e2 3 | timeCreated: 1741729146 -------------------------------------------------------------------------------- /Runtime/Addressables/FromAddressable.cs: -------------------------------------------------------------------------------- 1 | #if ADDRESSABLES_DROPDOWN 2 | namespace PhEngine.QuickDropdown.Addressables 3 | { 4 | public class FromAddressable : DropdownField 5 | { 6 | public FromAddressable(string path, InspectMode inspectMode = InspectMode.OpenPropertyWindow, string defaultNewItemName = null, bool isHideInspectButton = false, bool isHideInfo = false, bool isHideCreateSOButton = false) : base(path, inspectMode, defaultNewItemName, isHideInspectButton, isHideInfo, isHideCreateSOButton) 7 | { 8 | } 9 | } 10 | } 11 | #endif -------------------------------------------------------------------------------- /Runtime/Addressables/FromAddressable.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8d44ed489feb4bb0aec18aea13fa95f0 3 | timeCreated: 1741729188 -------------------------------------------------------------------------------- /Runtime/DropdownField.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | 4 | namespace PhEngine.QuickDropdown 5 | { 6 | public abstract class DropdownField : PropertyAttribute 7 | { 8 | public string Path { get; } 9 | public InspectMode InspectMode { get; } 10 | public string DefaultNewItemName { get; } 11 | public bool IsHideInspectButton { get; } 12 | public bool IsHideInfo { get; } 13 | public bool IsHideCreateSOButton { get; } 14 | 15 | protected DropdownField(string path, InspectMode inspectMode = InspectMode.OpenPropertyWindow, string defaultNewItemName = null, bool isHideInspectButton = false, bool isHideInfo = false, bool isHideCreateSOButton = false) 16 | { 17 | Path = path; 18 | InspectMode = inspectMode; 19 | DefaultNewItemName = defaultNewItemName; 20 | IsHideInspectButton = isHideInspectButton; 21 | IsHideInfo = isHideInfo; 22 | IsHideCreateSOButton = isHideCreateSOButton; 23 | } 24 | 25 | public virtual bool CheckInvalid(out Exception exception) 26 | { 27 | exception = null; 28 | if (!string.IsNullOrEmpty(Path)) 29 | return false; 30 | 31 | exception = new Exception("Path is null or empty."); 32 | return true; 33 | } 34 | } 35 | 36 | public enum InspectMode 37 | { 38 | OpenPropertyWindow, Select 39 | } 40 | } -------------------------------------------------------------------------------- /Runtime/DropdownField.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 80dcd054197f43798f91c4078b8a7907 3 | timeCreated: 1740860851 -------------------------------------------------------------------------------- /Runtime/FromConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PhEngine.QuickDropdown 4 | { 5 | public class FromConfig : DropdownField 6 | { 7 | public Type ConfigType { get; } 8 | public FromConfig(Type configType, InspectMode inspectMode = InspectMode.OpenPropertyWindow, string defaultNewItemName = null, bool isHideInspectButton = false, bool isHideInfo = false, bool isHideCreateSOButton = false) : base(configType.FullName, inspectMode, defaultNewItemName, isHideInspectButton, isHideInfo, isHideCreateSOButton) 9 | { 10 | ConfigType = configType; 11 | } 12 | 13 | public override bool CheckInvalid(out Exception exception) 14 | { 15 | exception = null; 16 | if (ConfigType == typeof(ScriptableGroup)) 17 | { 18 | exception = new Exception("Config cannot be a type of ScriptableGroup"); 19 | return true; 20 | } 21 | if (ConfigType.IsSubclassOf(typeof(ScriptableContainer))) 22 | return base.CheckInvalid(out exception); 23 | 24 | exception = new Exception("The type must be a subclass of ScriptableContainer."); 25 | return true; 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /Runtime/FromConfig.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ac15127a88e24141844ce59d761098d6 3 | timeCreated: 1741554998 -------------------------------------------------------------------------------- /Runtime/FromFolder.cs: -------------------------------------------------------------------------------- 1 | namespace PhEngine.QuickDropdown 2 | { 3 | public class FromFolder : DropdownField 4 | { 5 | public FromFolder(string path, InspectMode inspectMode = InspectMode.OpenPropertyWindow, string defaultNewItemName = null, bool isHideInspectButton = false, bool isHideInfo = false, bool isHideCreateSOButton = false) : base(path, inspectMode, defaultNewItemName, isHideInspectButton, isHideInfo, isHideCreateSOButton) 6 | { 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /Runtime/FromFolder.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f3e73e0cebe34627ac965de34569326c 3 | timeCreated: 1741009806 -------------------------------------------------------------------------------- /Runtime/FromGroup.cs: -------------------------------------------------------------------------------- 1 | namespace PhEngine.QuickDropdown 2 | { 3 | public class FromGroup : DropdownField 4 | { 5 | public FromGroup(string path, InspectMode inspectMode = InspectMode.OpenPropertyWindow, string defaultNewItemName = null, bool isHideInspectButton = false, bool isHideInfo = false, bool isHideCreateSOButton = false) : base(path, inspectMode, defaultNewItemName, isHideInspectButton, isHideInfo, isHideCreateSOButton) 6 | { 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /Runtime/FromGroup.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3a9cdc9239e74328b9a1cd1ed24b7bdd 3 | timeCreated: 1741009796 -------------------------------------------------------------------------------- /Runtime/ScriptableContainer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using UnityEngine; 5 | using Object = UnityEngine.Object; 6 | 7 | namespace PhEngine.QuickDropdown 8 | { 9 | public abstract class ScriptableContainer : ScriptableObject 10 | { 11 | public abstract string[] GetStringOptions(Type type); 12 | public abstract bool ContainsObject(Object targetObject); 13 | public abstract void AddObject(Object obj); 14 | public abstract Object GetObjectFromFlatTree(Type type, int targetIndex); 15 | } 16 | 17 | public abstract class ScriptableContainer : ScriptableContainer where T : Object 18 | { 19 | public IReadOnlyList ElementList => elementList.AsReadOnly(); 20 | [SerializeField] List elementList = new List(); 21 | 22 | public override void AddObject(Object element) 23 | { 24 | elementList.Add(element as T); 25 | } 26 | 27 | public override string[] GetStringOptions(Type type) 28 | { 29 | return ElementList 30 | .Where(so => so && so.GetType() == type) 31 | .Select(so => so.name) 32 | .ToArray(); 33 | } 34 | 35 | public override Object GetObjectFromFlatTree(Type type, int targetIndex) 36 | { 37 | var currentIndex = 0; 38 | foreach (var element in ElementList) 39 | { 40 | var isTypeMatched = element && element.GetType() == type; 41 | if (currentIndex == targetIndex && isTypeMatched) 42 | return element; 43 | 44 | if (isTypeMatched) 45 | currentIndex++; 46 | } 47 | return null; 48 | } 49 | 50 | public override bool ContainsObject(Object targetObject) 51 | { 52 | return ElementList.Contains(targetObject); 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /Runtime/ScriptableContainer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 084d862a4da3422cbcde0c286c8ad155 3 | timeCreated: 1741557578 -------------------------------------------------------------------------------- /Runtime/ScriptableGroup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Object = UnityEngine.Object; 5 | 6 | namespace PhEngine.QuickDropdown 7 | { 8 | public sealed class ScriptableGroup : ScriptableContainer 9 | { 10 | public override string[] GetStringOptions(Type type) 11 | { 12 | return GetOptions(type, new List(){this}); 13 | } 14 | 15 | string[] GetOptions(Type type, List parentList) 16 | { 17 | return ElementList 18 | .SelectMany(so=> 19 | { 20 | if (so is ScriptableGroup nestedGroup) 21 | { 22 | if (parentList.Contains(nestedGroup)) 23 | return new string[] { }; 24 | 25 | var newParentList = new List(parentList); 26 | newParentList.Add(nestedGroup); 27 | return nestedGroup.GetOptions(type, newParentList).ToArray(); 28 | } 29 | return so && so.GetType() == type ? new [] {GetOptionString(so)} : new string[]{}; 30 | }) 31 | .ToArray(); 32 | 33 | string GetOptionString(Object obj) 34 | { 35 | return parentList.Count <= 1 ? 36 | obj.name : 37 | string.Join("/", parentList.Skip(1).Select(p => p.name)) + "/" + obj.name; 38 | } 39 | } 40 | 41 | public override Object GetObjectFromFlatTree(Type type, int targetIndex) 42 | { 43 | var currentIndex = 0; 44 | return FlattenAndReturn(new List(){this}, type, ref currentIndex, targetIndex); 45 | } 46 | 47 | Object FlattenAndReturn(List parentList, Type type, ref int currentIndex, int targetIndex) 48 | { 49 | foreach (var element in ElementList) 50 | { 51 | if (element is ScriptableGroup nestedGroup) 52 | { 53 | if (parentList.Contains(nestedGroup)) 54 | continue; 55 | 56 | var newParentList = new List(parentList); 57 | newParentList.Add(nestedGroup); 58 | var result = nestedGroup.FlattenAndReturn(newParentList, type, ref currentIndex, targetIndex); 59 | if (result != null) 60 | return result; 61 | } 62 | 63 | var isTypeMatched = element && element.GetType() == type; 64 | if (currentIndex == targetIndex && isTypeMatched) 65 | return element; 66 | 67 | if (isTypeMatched) 68 | currentIndex++; 69 | } 70 | 71 | return null; 72 | } 73 | 74 | public override bool ContainsObject(Object targetObject) 75 | { 76 | return targetObject != null && FlattenAndCheckContains(targetObject, new List(){this}); 77 | } 78 | 79 | bool FlattenAndCheckContains(Object targetToCheck, List parentList) 80 | { 81 | foreach (var element in ElementList) 82 | { 83 | if (element is ScriptableGroup nestedGroup) 84 | { 85 | if (parentList.Contains(nestedGroup)) 86 | continue; 87 | 88 | var newParentList = new List(parentList); 89 | newParentList.Add(nestedGroup); 90 | if (nestedGroup.FlattenAndCheckContains(targetToCheck, newParentList)) 91 | return true; 92 | } 93 | if (element == targetToCheck) 94 | return true; 95 | } 96 | return false; 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /Runtime/ScriptableGroup.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c8e08058296243f2b7913a0cdec872bf 3 | timeCreated: 1740860948 -------------------------------------------------------------------------------- /Samples.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2808f8d149d1485eacb0f9b7d39a27d9 3 | timeCreated: 1741121225 -------------------------------------------------------------------------------- /Samples/AddressableDropdownExample.cs: -------------------------------------------------------------------------------- 1 | #if ADDRESSABLES_DROPDOWN 2 | using System.Collections.Generic; 3 | using PhEngine.QuickDropdown.Addressables; 4 | using UnityEngine; 5 | 6 | public class AddressableDropdownExample : MonoBehaviour 7 | { 8 | [FromAddressable("PackedAddressableGroup"), SerializeField] 9 | ElementConfig addressableConfig; 10 | 11 | [FromAddressable("PackedAddressableGroup"), SerializeField] 12 | string addressableAddress; 13 | 14 | [FromAddressable("PackedAddressableGroup"), SerializeField] 15 | List addressList = new List(); 16 | } 17 | #endif -------------------------------------------------------------------------------- /Samples/AddressableDropdownExample.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6db60443181448afafc2365dff766c30 3 | timeCreated: 1741896920 -------------------------------------------------------------------------------- /Samples/ElementConfig.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | public class ElementConfig : ScriptableObject 4 | { 5 | public string id; 6 | public Sprite icon; 7 | public string displayName; 8 | [TextArea] public string description; 9 | public string[] tags; 10 | } -------------------------------------------------------------------------------- /Samples/ElementConfig.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e159014ec4ca4764a0dc4d4a0735162b 3 | timeCreated: 1740936272 -------------------------------------------------------------------------------- /Samples/QuickDropdownExample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using PhEngine.QuickDropdown; 4 | using UnityEngine; 5 | 6 | public class QuickDropdownExample : MonoBehaviour 7 | { 8 | public float health; 9 | 10 | //Let user pick 'ElementConfig' asset from the ScriptableGroup named 'TestGroup' in the asset folder. 11 | //By default, This also display Inspect button, Create button (Only for ScriptableObjects), and a mini button to jump to the enclosing group. 12 | [FromGroup("TestGroup"), SerializeField] 13 | ElementConfig element; 14 | 15 | //Let user pick 'ElementConfig' from a first found ScriptableObject with the type of 'SampleConfig' 16 | [FromConfig(typeof(SampleConfig)), SerializeField] 17 | ElementConfig sampleConfigItem; 18 | 19 | public float attack; 20 | public float stamina; 21 | 22 | //Let user pick Sprite from the folder 'Assets/Sprites' and all the subfolders below. 23 | //The folder information is hidden from 'isHideInfo' flag 24 | //These path variations also work: 'Assets/Sprites', 'Assets/Sprites/', 'Sprites/' 25 | [FromFolder("Sprites", isHideInfo: true), SerializeField] 26 | Sprite sprite; 27 | 28 | //By default, Inspect button will open the assigned asset as a floating window. 29 | //You can change the button behaviour using different InpsectModes 30 | [FromFolder("Prefabs", inspectMode: InspectMode.Select), SerializeField] 31 | GameObject prefab; 32 | 33 | //Quick Dropdown now directly supports List & Array 34 | [FromGroup("TestGroup"), SerializeField] 35 | List directList = new List(); 36 | 37 | [FromGroup("TestGroup"), SerializeField] 38 | ElementConfig[] directArray = new ElementConfig[] {}; 39 | 40 | //Nested List & Array also works 41 | [SerializeField] List nestedDropdownList = new List(); 42 | [SerializeField] ElementConfigData[] nestedDropdownArray = new ElementConfigData[] {}; 43 | 44 | [Serializable] 45 | public class ElementConfigData 46 | { 47 | [FromGroup("TestGroup", isHideInfo: true)] 48 | public ElementConfig config; 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /Samples/QuickDropdownExample.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9543b14dee29424f8353f75f157a30a9 3 | timeCreated: 1740933823 -------------------------------------------------------------------------------- /Samples/SampleConfig.cs: -------------------------------------------------------------------------------- 1 | using PhEngine.QuickDropdown; 2 | 3 | public class SampleConfig : ScriptableContainer 4 | { 5 | } -------------------------------------------------------------------------------- /Samples/SampleConfig.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3d6112df5ae34ec7ac755d431d5b1205 3 | timeCreated: 1741560840 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.phengine.quickdropdown", 3 | "displayName": "Unity Quick Dropdown", 4 | "version": "0.2.3", 5 | "description": "C# Attributes that allow you to quickly assign Unity assets from a Dropdown on the Inspector.", 6 | "keywords": [], 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/phanphantz/Unity-Quick-Dropdown" 10 | }, 11 | "license": "MIT", 12 | "author": "Phun Peeticharoenthum", 13 | "documentationUrl": "https://github.com/phanphantz/Unity-Quick-Dropdown/blob/master/README.md" 14 | } 15 | -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e4bf25b8d9a2474aa4ce37fa43cc568f 3 | timeCreated: 1741199397 --------------------------------------------------------------------------------