// MIT License - Copyright (c) 2025 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE namespace WallstopStudios.UnityHelpers.Editor.CustomDrawers { #if UNITY_EDITOR && ODIN_INSPECTOR using System; using Sirenix.OdinInspector.Editor; using UnityEditor; using UnityEngine; using WallstopStudios.UnityHelpers.Core.Attributes; using WallstopStudios.UnityHelpers.Core.DataStructure.Adapters; using WallstopStudios.UnityHelpers.Editor.CustomDrawers.Utils; /// /// Odin Inspector attribute drawer for . /// Renders string fields decorated with StringInList as dropdown selectors. /// /// /// This drawer ensures StringInList works correctly when Odin Inspector is installed /// and classes derive from SerializedMonoBehaviour or SerializedScriptableObject, /// where Unity's standard PropertyDrawer system is bypassed. /// public sealed class StringInListOdinDrawer : OdinAttributeDrawer { /// /// Draws the property as a dropdown selector with the string options provided by the attribute. /// /// The label to display for the property. protected override void DrawPropertyLayout(GUIContent label) { StringInListAttribute stringInList = Attribute; if (stringInList == null) { CallNextDrawer(label); return; } object parentValue = Property.Parent?.ValueEntry?.WeakSmartValue; string[] options = stringInList.GetOptions(parentValue) ?? Array.Empty(); if (options.Length == 0) { EditorGUILayout.HelpBox("No options available for StringInList.", MessageType.Info); return; } Type valueType = Property.ValueEntry?.TypeOfValue; if (valueType == null) { CallNextDrawer(label); return; } // Check for mixed values bool hasMultipleDifferentValues = false; if (Property.ValueEntry.ValueCount > 1) { object firstValue = Property.ValueEntry.WeakValues[0]; for (int i = 1; i < Property.ValueEntry.ValueCount; i++) { if (!Equals(firstValue, Property.ValueEntry.WeakValues[i])) { hasMultipleDifferentValues = true; break; } } } // Set showMixedValue FIRST, before any index calculations bool previousMixed = EditorGUI.showMixedValue; EditorGUI.showMixedValue = hasMultipleDifferentValues; try { // Handle string field if (valueType == typeof(string)) { DrawStringDropDown(label, options, hasMultipleDifferentValues); return; } // Handle int field (index-based) if (valueType == typeof(int)) { DrawIntIndexDropDown(label, options, hasMultipleDifferentValues); return; } // Handle SerializableType if (valueType == typeof(SerializableType)) { DrawSerializableTypeDropDown( label, options, stringInList, hasMultipleDifferentValues ); return; } // Unsupported type EditorGUILayout.HelpBox( $"[StringInList] Type mismatch: field is {valueType.Name}, but StringInList requires string, int, or SerializableType.", MessageType.Error ); } finally { EditorGUI.showMixedValue = previousMixed; } } private void DrawStringDropDown( GUIContent label, string[] options, bool hasMultipleDifferentValues ) { object currentValue = Property.ValueEntry?.WeakSmartValue; string currentString = currentValue as string ?? string.Empty; int currentIndex = Array.IndexOf(options, currentString); string[] displayOptions = GetDisplayOptions(options, Attribute); Rect controlRect = EditorGUILayout.GetControlRect( true, EditorGUIUtility.singleLineHeight ); DrawPopupDropDown( controlRect, label, options, displayOptions, currentIndex, currentString, ApplyStringSelection, hasMultipleDifferentValues ); } private void DrawIntIndexDropDown( GUIContent label, string[] options, bool hasMultipleDifferentValues ) { object currentValue = Property.ValueEntry?.WeakSmartValue; int currentIndex = currentValue is int intValue ? intValue : -1; // Clamp index to valid range if (currentIndex < 0 || currentIndex >= options.Length) { currentIndex = -1; } string[] displayOptions = GetDisplayOptions(options, Attribute); Rect controlRect = EditorGUILayout.GetControlRect( true, EditorGUIUtility.singleLineHeight ); string currentDisplay = currentIndex >= 0 && currentIndex < displayOptions.Length ? displayOptions[currentIndex] : DropDownShared.GetCachedIntString(currentValue is int idx ? idx : -1); DrawPopupDropDown( controlRect, label, options, displayOptions, currentIndex, currentDisplay, ApplyIntIndexSelection, hasMultipleDifferentValues ); } private void DrawSerializableTypeDropDown( GUIContent label, string[] options, StringInListAttribute attribute, bool hasMultipleDifferentValues ) { object currentValue = Property.ValueEntry?.WeakSmartValue; string currentAssemblyQualifiedName = string.Empty; if (currentValue is SerializableType serializableType) { currentAssemblyQualifiedName = serializableType.AssemblyQualifiedName ?? string.Empty; } int currentIndex = Array.IndexOf(options, currentAssemblyQualifiedName); string[] displayOptions = GetDisplayOptions(options, attribute); Rect controlRect = EditorGUILayout.GetControlRect( true, EditorGUIUtility.singleLineHeight ); string currentDisplay = currentIndex >= 0 && currentIndex < displayOptions.Length ? displayOptions[currentIndex] : "(None)"; DrawPopupDropDown( controlRect, label, options, displayOptions, currentIndex, currentDisplay, ApplySerializableTypeSelection, hasMultipleDifferentValues ); } private void DrawPopupDropDown( Rect position, GUIContent label, string[] options, string[] displayOptions, int currentIndex, string currentDisplay, Action applySelection, bool hasMultipleDifferentValues ) { Rect labelRect = new( position.x, position.y, EditorGUIUtility.labelWidth, position.height ); Rect fieldRect = new( position.x + EditorGUIUtility.labelWidth + 2f, position.y, position.width - EditorGUIUtility.labelWidth - 2f, position.height ); if (label != null && label != GUIContent.none) { EditorGUI.LabelField(labelRect, label); } string buttonText = hasMultipleDifferentValues ? "\u2014" : currentDisplay; if (GUI.Button(fieldRect, buttonText, EditorStyles.popup)) { ShowPopupMenu( fieldRect, options, displayOptions, currentIndex, applySelection, hasMultipleDifferentValues ); } } private void ShowPopupMenu( Rect buttonRect, string[] options, string[] displayOptions, int currentIndex, Action applySelection, bool hasMultipleDifferentValues ) { GenericMenu menu = new(); for (int i = 0; i < options.Length; i++) { int capturedIndex = i; bool isSelected = i == currentIndex && !hasMultipleDifferentValues; menu.AddItem( new GUIContent(displayOptions[i]), isSelected, () => applySelection(options[capturedIndex]) ); } menu.DropDown(buttonRect); } private void ApplyStringSelection(string value) { Property.ValueEntry.WeakSmartValue = value; } private void ApplyIntIndexSelection(string value) { object parentValue = Property.Parent?.ValueEntry?.WeakSmartValue; string[] options = Attribute.GetOptions(parentValue) ?? Array.Empty(); int index = Array.IndexOf(options, value); if (index >= 0) { Property.ValueEntry.WeakSmartValue = index; } } private void ApplySerializableTypeSelection(string assemblyQualifiedName) { Type resolvedType = null; if (!string.IsNullOrEmpty(assemblyQualifiedName)) { resolvedType = Type.GetType(assemblyQualifiedName, throwOnError: false); } Property.ValueEntry.WeakSmartValue = new SerializableType(resolvedType); } private static string[] GetDisplayOptions(string[] options, StringInListAttribute attribute) { if (IsSerializableTypeProvider(attribute)) { string[] names = SerializableTypeCatalog.GetDisplayNames(); if (names != null && names.Length == options.Length) { return names; } } return options; } private static bool IsSerializableTypeProvider(StringInListAttribute attribute) { return attribute?.ProviderType == typeof(SerializableTypeCatalog) && string.Equals( attribute.ProviderMethodName, nameof(SerializableTypeCatalog.GetAssemblyQualifiedNames), StringComparison.Ordinal ); } } #endif }