// MIT License - Copyright (c) 2025 wallstop
// Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE
namespace WallstopStudios.UnityHelpers.Editor.CustomEditors
{
#if UNITY_EDITOR && ODIN_INSPECTOR
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEditor;
using WallstopStudios.UnityHelpers.Core.Attributes;
using WallstopStudios.UnityHelpers.Editor.CustomDrawers;
using WallstopStudios.UnityHelpers.Editor.Extensions;
using WallstopStudios.UnityHelpers.Editor.Internal;
using WallstopStudios.UnityHelpers.Editor.Settings;
using WallstopStudios.UnityHelpers.Editor.Utils.WButton;
using WallstopStudios.UnityHelpers.Editor.Utils.WGroup;
using WallstopStudios.UnityHelpers.Utils;
///
/// Shared logic for Odin-based WButton inspectors.
///
///
/// This helper consolidates duplicate code between
/// and to follow DRY principles.
///
internal static class WButtonOdinInspectorHelper
{
private const string ScriptPropertyPath = "m_Script";
internal static readonly WallstopGenericPool<
Dictionary
> PropertyLookupPool = new(
() => new Dictionary(16, StringComparer.Ordinal),
onRelease: d => d.Clear()
);
///
/// Draws the complete inspector GUI for an Odin-based object with WButton and WGroup support.
///
/// The editor instance.
/// Per-editor pagination state for button groups.
/// Per-editor foldout state for button groups.
/// Per-editor foldout state for WGroups.
internal static void DrawInspectorGUI(
Editor editor,
Dictionary paginationStates,
Dictionary foldoutStates,
Dictionary groupFoldoutStates
)
{
// Check editor.target first using Unity's == operator to detect destroyed objects
// before accessing serializedObject, which would log an error if target is destroyed
if (editor == null || editor.target == null)
{
return;
}
SerializedObject serializedObject = editor.serializedObject;
if (serializedObject == null || serializedObject.targetObject == null)
{
return;
}
using PooledResource> triggeredContextsLease =
Buffers.GetList(
4,
out List triggeredContexts
);
editor.serializedObject.UpdateIfRequiredOrScript();
using PooledResource> propertyLookupLease =
PropertyLookupPool.Get(out Dictionary propertyLookup);
SerializedProperty scriptProperty = BuildPropertyLookup(
editor.serializedObject,
propertyLookup
);
bool drawScriptField = scriptProperty != null && !InlineInspectorContext.IsActive;
if (drawScriptField)
{
using (new EditorGUI.DisabledScope(true))
{
EditorGUILayout.PropertyField(scriptProperty, true);
}
EditorGUILayout.Space();
}
UnityHelpersSettings.WButtonActionsPlacement placement =
UnityHelpersSettings.GetWButtonActionsPlacement();
UnityHelpersSettings.WButtonFoldoutBehavior foldoutBehavior =
UnityHelpersSettings.GetWButtonFoldoutBehavior();
bool globalPlacementIsTop =
placement == UnityHelpersSettings.WButtonActionsPlacement.Top;
if (
WButtonGUI.DrawButtons(
editor,
WButtonPlacement.Top,
paginationStates,
foldoutStates,
foldoutBehavior,
triggeredContexts,
globalPlacementIsTop
)
)
{
EditorGUILayout.Space();
}
string scriptPathOrNull = scriptProperty != null ? scriptProperty.propertyPath : null;
WGroupLayout layout = WGroupLayoutBuilder.Build(
editor.serializedObject,
scriptPathOrNull
);
IReadOnlyList operations = layout.Operations;
for (int index = 0; index < operations.Count; index++)
{
WGroupDrawOperation operation = operations[index];
if (operation.Type == WGroupDrawOperationType.Group)
{
WGroupDefinition definition = operation.Group;
if (definition == null)
{
continue;
}
WGroupGUI.DrawGroup(
definition,
editor.serializedObject,
groupFoldoutStates,
propertyLookup
);
continue;
}
if (operation.IsHiddenInInspector)
{
continue;
}
if (
!propertyLookup.TryGetValue(
operation.PropertyPath,
out SerializedProperty property
)
)
{
continue;
}
if (!WShowIfPropertyDrawer.ShouldShowProperty(property))
{
continue;
}
DrawValidationHelpBoxIfNeeded(property);
EditorGUILayout.PropertyField(property, true);
}
editor.serializedObject.ApplyModifiedProperties();
if (
WButtonGUI.DrawButtons(
editor,
WButtonPlacement.Bottom,
paginationStates,
foldoutStates,
foldoutBehavior,
triggeredContexts,
globalPlacementIsTop
)
)
{
EditorGUILayout.Space();
}
if (triggeredContexts.Count > 0)
{
WButtonInvocationController.ProcessTriggeredMethods(triggeredContexts);
}
}
private static SerializedProperty BuildPropertyLookup(
SerializedObject serializedObject,
Dictionary propertyLookup
)
{
propertyLookup.Clear();
SerializedProperty scriptProperty = null;
SerializedProperty iterator = serializedObject.GetIterator();
bool enterChildren = true;
while (iterator.NextVisible(enterChildren))
{
enterChildren = false;
string path = iterator.propertyPath;
if (string.Equals(path, ScriptPropertyPath, StringComparison.Ordinal))
{
scriptProperty = iterator.Copy();
continue;
}
propertyLookup[path] = iterator.Copy();
}
return scriptProperty;
}
private static void DrawValidationHelpBoxIfNeeded(SerializedProperty property)
{
if (!property.isArray || property.propertyType == SerializedPropertyType.String)
{
return;
}
property.GetEnclosingObject(out FieldInfo fieldInfo);
if (fieldInfo == null)
{
return;
}
ValidateAssignmentAttribute validateAttribute =
fieldInfo.GetCustomAttribute();
if (validateAttribute != null)
{
ValidateAssignmentPropertyDrawer.DrawValidationHelpBoxIfNeeded(
property,
validateAttribute
);
return;
}
WNotNullAttribute notNullAttribute = fieldInfo.GetCustomAttribute();
if (notNullAttribute != null)
{
WNotNullPropertyDrawer.DrawValidationHelpBoxIfNeeded(property, notNullAttribute);
}
}
}
#endif
}