// 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 }