// 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 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; [CustomEditor(typeof(UnityEngine.Object), editorForChildClasses: true)] [CanEditMultipleObjects] public sealed class WButtonInspector : Editor { private const string ScriptPropertyPath = "m_Script"; private static readonly WallstopGenericPool< Dictionary > PropertyLookupPool = new( () => new Dictionary(16, StringComparer.Ordinal), onRelease: d => d.Clear() ); private readonly Dictionary _paginationStates = new(); private readonly Dictionary _foldoutStates = new(); private readonly Dictionary _groupFoldoutStates = new(); public override void OnInspectorGUI() { if (serializedObject == null || serializedObject.targetObject == null) { return; } using PooledResource> triggeredContextsLease = Buffers.GetList( 4, out List triggeredContexts ); serializedObject.UpdateIfRequiredOrScript(); using PooledResource> propertyLookupLease = PropertyLookupPool.Get(out Dictionary propertyLookup); SerializedProperty scriptProperty = BuildPropertyLookup( 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; // When global placement is Top: render buttons above properties // When global placement is Bottom: render buttons below properties // // Groups with explicit placement override render in their specified location: // - GroupPlacement.Top: always render at top (before properties) // - GroupPlacement.Bottom: always render at bottom (after properties) // - GroupPlacement.UseGlobalSetting: follow the global placement setting // // We call DrawButtons twice per location: // 1. WButtonPlacement.Top pass: draws groups that should appear at top // 2. WButtonPlacement.Bottom pass: draws groups that should appear at bottom // // At top of inspector (before properties): // - Groups with placement == Top // - Groups with UseGlobalSetting when global is Top if ( WButtonGUI.DrawButtons( this, WButtonPlacement.Top, _paginationStates, _foldoutStates, foldoutBehavior, triggeredContexts, globalPlacementIsTop ) ) { EditorGUILayout.Space(); } string scriptPathOrNull = scriptProperty != null ? scriptProperty.propertyPath : null; WGroupLayout layout = WGroupLayoutBuilder.Build(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, serializedObject, _groupFoldoutStates, propertyLookup ); continue; } // Skip hidden properties - they should not be rendered if (operation.IsHiddenInInspector) { continue; } if ( !propertyLookup.TryGetValue( operation.PropertyPath, out SerializedProperty property ) ) { continue; } // Check WShowIf condition - this handles conditional visibility for all properties // including arrays/lists which need editor-level handling since PropertyDrawers // for attributes on arrays only affect elements, not the array itself if (!WShowIfPropertyDrawer.ShouldShowProperty(property)) { continue; } // Draw validation HelpBox for arrays/lists with validation attributes // PropertyDrawers for attributes on arrays only affect elements, so we handle // array-level validation warnings/errors here in the custom editor DrawValidationHelpBoxIfNeeded(property); EditorGUILayout.PropertyField(property, true); } serializedObject.ApplyModifiedProperties(); // At bottom of inspector (after properties): // - Groups with placement == Bottom // - Groups with UseGlobalSetting when global is Bottom if ( WButtonGUI.DrawButtons( this, 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; } /// /// Draws validation HelpBox for arrays/lists with ValidateAssignment or WNotNull attributes. /// PropertyDrawers for attributes on arrays only affect elements, not the array itself, /// so we handle array-level validation here in the custom editor. /// private static void DrawValidationHelpBoxIfNeeded(SerializedProperty property) { // Only handle array/list properties - non-array properties are handled by PropertyDrawers 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 }