// 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 System.Collections;
using System.Collections.Generic;
using Sirenix.OdinInspector.Editor;
using UnityEditor;
using UnityEngine;
using WallstopStudios.UnityHelpers.Core.Attributes;
using WallstopStudios.UnityHelpers.Editor.CustomDrawers.Utils;
///
/// Odin Inspector attribute drawer for .
/// Renders integer fields decorated with IntDropDown as dropdown selectors.
///
///
/// This drawer ensures IntDropDown 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 IntDropDownOdinDrawer : OdinAttributeDrawer
{
private static readonly Dictionary DisplayOptionsCache = new();
///
/// Clears all cached state. Called during domain reload via
/// .
///
internal static void ClearCache()
{
DisplayOptionsCache.Clear();
}
///
/// Draws the property as a dropdown selector with the integer options provided by the attribute.
///
/// The label to display for the property.
protected override void DrawPropertyLayout(GUIContent label)
{
IntDropDownAttribute intDropDown = Attribute;
if (intDropDown == null)
{
CallNextDrawer(label);
return;
}
Type valueType = Property.ValueEntry?.TypeOfValue;
if (valueType != typeof(int))
{
EditorGUILayout.HelpBox(
$"[IntDropDown] Type mismatch: field is {valueType?.Name ?? "unknown"}, but IntDropDown requires int.",
MessageType.Error
);
return;
}
object parentValue = Property.Parent?.ValueEntry?.WeakSmartValue;
int[] options = intDropDown.GetOptions(parentValue) ?? Array.Empty();
if (options.Length == 0)
{
EditorGUILayout.HelpBox("No options available for IntDropDown.", MessageType.Info);
return;
}
// Check for mixed values BEFORE any calculations
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;
object currentValue = Property.ValueEntry?.WeakSmartValue;
int currentInt = currentValue is int intValue ? intValue : 0;
int currentIndex = Array.IndexOf(options, currentInt);
string[] displayOptions = GetOrCreateDisplayOptions(options);
Rect controlRect = EditorGUILayout.GetControlRect(
true,
EditorGUIUtility.singleLineHeight
);
try
{
DrawPopupDropDown(
controlRect,
label,
options,
displayOptions,
currentIndex,
currentInt,
hasMultipleDifferentValues
);
}
finally
{
EditorGUI.showMixedValue = previousMixed;
}
}
private void DrawPopupDropDown(
Rect position,
GUIContent label,
int[] options,
string[] displayOptions,
int currentIndex,
int currentInt,
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);
}
// Determine display value without modifying property
string displayValue;
if (hasMultipleDifferentValues)
{
displayValue = "\u2014"; // Em dash for mixed values
}
else if (currentIndex >= 0 && currentIndex < displayOptions.Length)
{
displayValue = displayOptions[currentIndex];
}
else
{
// Invalid value - show it but don't clamp
displayValue = DropDownShared.GetCachedIntString(currentInt) + " (Invalid)";
}
if (GUI.Button(fieldRect, displayValue, EditorStyles.popup))
{
ShowPopupMenu(
fieldRect,
options,
displayOptions,
currentIndex,
hasMultipleDifferentValues
);
}
}
private void ShowPopupMenu(
Rect buttonRect,
int[] options,
string[] displayOptions,
int currentIndex,
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 ApplySelection(int value)
{
// Record Undo for ALL selected objects, filtering out non-Unity targets
IList weakTargets = Property.Tree.WeakTargets;
List validTargets = new(weakTargets.Count);
for (int i = 0; i < weakTargets.Count; i++)
{
if (weakTargets[i] is UnityEngine.Object unityObject && unityObject != null)
{
validTargets.Add(unityObject);
}
}
if (validTargets.Count > 0)
{
Undo.RecordObjects(validTargets.ToArray(), "Change IntDropDown Selection");
}
Property.ValueEntry.WeakSmartValue = value;
}
internal static string[] GetOrCreateDisplayOptions(int[] options)
{
if (options == null || options.Length == 0)
{
return Array.Empty();
}
int hashCode = DropDownShared.ComputeOptionsHash(options);
if (DisplayOptionsCache.TryGetValue(hashCode, out string[] cached))
{
if (cached.Length == options.Length)
{
bool match = true;
for (int i = 0; i < options.Length && match; i++)
{
if (
!string.Equals(
cached[i],
DropDownShared.GetCachedIntString(options[i]),
StringComparison.Ordinal
)
)
{
match = false;
}
}
if (match)
{
return cached;
}
}
}
string[] displayOptions = new string[options.Length];
for (int i = 0; i < options.Length; i++)
{
displayOptions[i] = DropDownShared.GetCachedIntString(options[i]);
}
DisplayOptionsCache[hashCode] = displayOptions;
return displayOptions;
}
}
#endif
}