// MIT License - Copyright (c) 2025 wallstop
// Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE
namespace WallstopStudios.UnityHelpers.Editor.Settings
{
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
using UnityEngine.Serialization;
using WallstopStudios.UnityHelpers.Core.Attributes;
using WallstopStudios.UnityHelpers.Core.DataStructure.Adapters;
using WallstopStudios.UnityHelpers.Editor.CustomDrawers;
using WallstopStudios.UnityHelpers.Editor.Utils;
using WallstopStudios.UnityHelpers.Editor.Utils.WButton;
using WallstopStudios.UnityHelpers.Editor.Utils.WGroup;
using WallstopStudios.UnityHelpers.Settings;
using WallstopStudios.UnityHelpers.Utils;
///
/// Project-wide configuration surface for Unity Helpers editor tooling.
///
///
/// Currently exposes pagination defaults for and companion editor tooling (SerializableSet, WEnumToggleButtons, WButton trays, duplicate highlighting).
///
[FilePath(
"ProjectSettings/UnityHelpersSettings.asset",
FilePathAttribute.Location.ProjectFolder
)]
public sealed class UnityHelpersSettings : ScriptableSingleton
{
internal static event Action OnSettingsSaved;
public const int MinPageSize = 5;
public const int MaxPageSize = 500;
public const int MaxSerializableDictionaryPageSize = 250;
public const int DefaultStringInListPageSize = 25;
public const int DefaultSerializableSetPageSize = 15;
public const int DefaultSerializableDictionaryPageSize = 15;
public const int DefaultEnumToggleButtonsPageSize = 15;
public const int DefaultWButtonPageSize = 6;
public const int DefaultWButtonHistorySize = 5;
public const int MinWButtonHistorySize = 1;
public const int MaxWButtonHistorySize = 10;
public const string DefaultWButtonColorKey = "Default";
public const string WButtonLightThemeColorKey = "Default-Light";
public const string WButtonDarkThemeColorKey = "Default-Dark";
public const string WButtonLegacyColorKey = "WDefault";
public const string DefaultWEnumToggleButtonsColorKey = "Default";
public const string WEnumToggleButtonsLightThemeColorKey = "Default-Light";
public const string WEnumToggleButtonsDarkThemeColorKey = "Default-Dark";
public const int DefaultWGroupAutoIncludeRowCount = 4;
public const int MinWGroupAutoIncludeRowCount = 0;
public const int MaxWGroupAutoIncludeRowCount = 32;
public const float DefaultDetectAssetChangeLoopWindowSeconds = 15f;
public const float MinDetectAssetChangeLoopWindowSeconds = 1f;
public const float MaxDetectAssetChangeLoopWindowSeconds = 120f;
public const bool DefaultDeferAssetPostprocessorCallbacks = true;
private static readonly Color DefaultLightThemeGroupBackground = new(
0.82f,
0.82f,
0.82f,
1f
);
private static readonly Color DefaultDarkThemeGroupBackground = new(
0.215f,
0.215f,
0.215f,
1f
);
[Obsolete("Use DefaultWButtonColorKey instead.")]
public const string DefaultWButtonPriority = DefaultWButtonColorKey;
public const int DefaultDuplicateTweenCycles = 3;
public const float DefaultFoldoutSpeed = 2f;
public const float MinFoldoutSpeed = 2f;
public const float MaxFoldoutSpeed = 12f;
///
/// Default value for pool max size (0 = unbounded).
///
public const int DefaultPoolMaxSize = 0;
///
/// Default value for pool minimum retain count during purge.
///
public const int DefaultPoolMinRetainCount = 0;
///
/// Default value for pool warm retain count during purge.
/// Active pools keep this many warm to avoid cold-start allocations.
///
public const int DefaultPoolWarmRetainCount = 2;
///
/// Default value for pool idle timeout in seconds (0 = disabled).
///
public const float DefaultPoolIdleTimeoutSeconds = 0f;
///
/// Default value for pool purge interval in seconds.
///
public const float DefaultPoolPurgeIntervalSeconds = 60f;
///
/// Default value for whether intelligent pool purging is enabled.
/// Defaults to true with conservative settings. Use
/// to restore previous behavior.
///
public const bool DefaultPoolIntelligentPurgingEnabled = true;
///
/// Default value for pool idle timeout when intelligent purging is enabled.
/// Set to 5 minutes to be conservative and avoid GC churn.
///
public const float DefaultPoolIntelligentIdleTimeoutSeconds = 300f;
///
/// Default buffer multiplier for comfortable pool size calculation.
/// Comfortable size = max(MinRetainCount, rollingHighWaterMark * BufferMultiplier).
///
public const float DefaultPoolBufferMultiplier = 2.0f;
///
/// Default rolling window duration in seconds for high water mark tracking.
///
public const float DefaultPoolRollingWindowSeconds = 300f;
///
/// Default hysteresis duration in seconds.
/// Purging is suppressed for this duration after a usage spike.
///
public const float DefaultPoolHysteresisSeconds = 120f;
///
/// Default spike threshold multiplier.
/// A spike is detected when concurrent rentals exceed the rolling average by this factor.
///
public const float DefaultPoolSpikeThresholdMultiplier = 2.5f;
///
/// Default value for Failed Tests Exporter enabled state.
///
public const bool DefaultFailedTestsExporterEnabled = false;
///
/// Default relative output directory for failed test results (empty = project root).
///
public const string DefaultFailedTestsOutputDirectory = "";
private static readonly Color DefaultColorKeyButtonColor = new(0.243f, 0.525f, 0.988f, 1f);
private static readonly Color DefaultLightThemeButtonColor = new(0.78f, 0.78f, 0.78f, 1f);
private static readonly Color DefaultDarkThemeButtonColor = new(0.35f, 0.35f, 0.35f, 1f);
private static readonly Color DefaultLightThemeEnumSelectedColor =
DefaultColorKeyButtonColor;
private static readonly Color DefaultLightThemeEnumSelectedTextColor = Color.white;
private static readonly Color DefaultLightThemeEnumInactiveColor =
DefaultLightThemeButtonColor;
private static readonly Color DefaultLightThemeEnumInactiveTextColor = Color.black;
private static readonly Color DefaultDarkThemeEnumSelectedColor =
DefaultColorKeyButtonColor;
private static readonly Color DefaultDarkThemeEnumSelectedTextColor = Color.white;
private static readonly Color DefaultDarkThemeEnumInactiveColor =
DefaultDarkThemeButtonColor;
private static readonly Color DefaultDarkThemeEnumInactiveTextColor = Color.white;
private static readonly Color DefaultCancelButtonColor = new(0.85f, 0.2f, 0.2f, 1f);
private static readonly Color DefaultCancelButtonTextColor = Color.white;
private static readonly Color DefaultClearHistoryButtonColor = new(0.75f, 0.45f, 0.45f, 1f);
private static readonly Color DefaultClearHistoryButtonTextColor = Color.white;
private static readonly Dictionary SettingsGroupFoldoutStates = new();
private static SerializedObject _cachedSettingsSerializedObject;
private const float SettingsLabelWidth = 260f;
private const float SettingsMinFieldWidth = 110f;
private const float CustomColorDrawerMinColorFieldWidth = 42f;
private const float CustomColorDrawerLabelWidthRatio = 0.38f;
private const float CustomColorDrawerMinLabelWidth = 28f;
private const float CustomColorDrawerMaxLabelWidth = 90f;
private const string WaitInstructionBufferFoldoutKey = "Buffers";
private static UnityHelpersBufferSettingsAsset _waitInstructionBufferSettingsAsset;
private static readonly GUIContent StringInListPageSizeContent =
EditorGUIUtility.TrTextContent(
"StringInList",
"Number of options displayed per page in StringInList dropdowns."
);
private static readonly GUIContent SerializableSetPageSizeContent =
EditorGUIUtility.TrTextContent(
"Sets",
"Number of entries displayed per page in SerializableHashSet and SerializableSortedSet inspectors."
);
private static readonly GUIContent SerializableSetStartCollapsedContent =
EditorGUIUtility.TrTextContent(
"Sets Start Collapsed",
"When enabled, SerializableHashSet and SerializableSortedSet inspectors start collapsed unless overridden per field via SerializableCollectionFoldoutAttribute."
);
private static readonly GUIContent SerializableDictionaryPageSizeContent =
EditorGUIUtility.TrTextContent(
"Dictionaries",
"Number of entries displayed per page in SerializableDictionary and SerializableSortedDictionary inspectors."
);
private static readonly GUIContent SerializableDictionaryStartCollapsedContent =
EditorGUIUtility.TrTextContent(
"Dicts Start Collapsed",
"When enabled, SerializableDictionary and SerializableSortedDictionary inspectors start collapsed unless overridden with SerializableCollectionFoldoutAttribute."
);
private static readonly GUIContent EnumToggleButtonsPageSizeContent =
EditorGUIUtility.TrTextContent(
"Enum Toggles",
"Number of toggle buttons displayed per page when WEnumToggleButtons groups exceed the configured threshold."
);
private static readonly GUIContent WButtonPageSizeContent = EditorGUIUtility.TrTextContent(
"Page Size",
"Number of WButton actions displayed per page when grouped by draw order."
);
private static readonly GUIContent WButtonHistorySizeContent =
EditorGUIUtility.TrTextContent(
"History Size",
"Number of recent results remembered per WButton method for each inspected object."
);
private static readonly GUIContent WButtonPlacementContent = EditorGUIUtility.TrTextContent(
"Placement",
"Controls where WButton actions render relative to the inspector content (Top or Bottom of inspector)."
);
private static readonly GUIContent WButtonFoldoutBehaviorContent =
EditorGUIUtility.TrTextContent(
"Foldout Behavior",
"Determines whether WButton action groups are always visible, start expanded, or start collapsed when first drawn."
);
private static readonly GUIContent WButtonFoldoutTweenEnabledContent =
EditorGUIUtility.TrTextContent(
"Animate Foldouts",
"Enable animated transitions when expanding or collapsing WButton action groups."
);
private static readonly GUIContent WButtonFoldoutSpeedContent =
EditorGUIUtility.TrTextContent(
"Foldout Speed",
"Animation speed used when expanding or collapsing WButton action groups."
);
private static readonly GUIContent WButtonCustomColorsContent =
EditorGUIUtility.TrTextContent("WButton Custom Colors");
private static readonly GUIContent WButtonCancelButtonColorContent =
EditorGUIUtility.TrTextContent(
"Cancel Background",
"Background color for the Cancel button that appears during async WButton execution."
);
private static readonly GUIContent WButtonCancelButtonTextColorContent =
EditorGUIUtility.TrTextContent(
"Cancel Text",
"Text color for the Cancel button that appears during async WButton execution."
);
private static readonly GUIContent WButtonClearHistoryButtonColorContent =
EditorGUIUtility.TrTextContent(
"Clear Background",
"Background color for the Clear History button in WButton result history."
);
private static readonly GUIContent WButtonClearHistoryButtonTextColorContent =
EditorGUIUtility.TrTextContent(
"Clear Text",
"Text color for the Clear History button in WButton result history."
);
private static readonly GUIContent WEnumToggleButtonsCustomColorsContent =
EditorGUIUtility.TrTextContent("WEnumToggleButtons Custom Colors");
private static readonly GUIContent InlineEditorFoldoutBehaviorContent =
EditorGUIUtility.TrTextContent(
"Foldout Behavior",
"Default foldout state for inline object editors when a field does not specify a mode."
);
private static readonly GUIContent InlineEditorFoldoutTweenEnabledContent =
EditorGUIUtility.TrTextContent(
"Animate Foldouts",
"Enable animated transitions when expanding or collapsing WInLineEditor foldouts."
);
private static readonly GUIContent InlineEditorFoldoutSpeedContent =
EditorGUIUtility.TrTextContent(
"Foldout Speed",
"Animation speed used when expanding or collapsing WInLineEditor foldouts."
);
private static readonly GUIContent DictionaryFoldoutTweenEnabledContent =
EditorGUIUtility.TrTextContent(
"Animate Dictionary",
"Enable animated transitions when expanding or collapsing SerializableDictionary pending entries."
);
private static readonly GUIContent DictionaryFoldoutSpeedContent =
EditorGUIUtility.TrTextContent(
"Dictionary Speed",
"Animation speed used when expanding or collapsing SerializableDictionary pending entries."
);
private static readonly GUIContent SortedDictionaryFoldoutTweenEnabledContent =
EditorGUIUtility.TrTextContent(
"Animate Sorted Dict",
"Enable animated transitions when expanding or collapsing SerializableSortedDictionary pending entries."
);
private static readonly GUIContent SortedDictionaryFoldoutSpeedContent =
EditorGUIUtility.TrTextContent(
"Sorted Dict Speed",
"Animation speed used when expanding or collapsing SerializableSortedDictionary pending entries."
);
private static readonly GUIContent SetFoldoutTweenEnabledContent =
EditorGUIUtility.TrTextContent(
"Animate Foldouts",
"Enable animated transitions when expanding or collapsing SerializableHashSet manual entry foldouts."
);
private static readonly GUIContent SetFoldoutSpeedContent = EditorGUIUtility.TrTextContent(
"Foldout Speed",
"Animation speed used when expanding or collapsing SerializableHashSet manual entry foldouts."
);
private static readonly GUIContent SortedSetFoldoutTweenEnabledContent =
EditorGUIUtility.TrTextContent(
"Animate Foldouts",
"Enable animated transitions when expanding or collapsing SerializableSortedSet manual entry foldouts."
);
private static readonly GUIContent SortedSetFoldoutSpeedContent =
EditorGUIUtility.TrTextContent(
"Foldout Speed",
"Animation speed used when expanding or collapsing SerializableSortedSet manual entry foldouts."
);
private const string WaitInstructionBufferDefaultsHelpText =
"Configure the global defaults for Buffers.WaitInstruction pooling. These values are applied automatically on domain reload and when the player starts if Auto Apply is enabled.";
private static readonly GUIContent WaitInstructionBufferApplyOnLoadContent =
EditorGUIUtility.TrTextContent(
"Auto Apply",
"When enabled, the configured defaults are applied automatically on domain reload, scene load, and in player builds."
);
private static readonly GUIContent WaitInstructionBufferQuantizationContent =
EditorGUIUtility.TrTextContent(
"Quantization (s)",
"Durations are rounded to this step (in seconds) before being cached. Set to 0 to disable quantization."
);
private static readonly GUIContent WaitInstructionBufferMaxEntriesContent =
EditorGUIUtility.TrTextContent(
"Max Entries",
"Maximum number of cached WaitForSeconds/Realtime durations (0 = unbounded)."
);
private static readonly GUIContent WaitInstructionBufferUseLruContent =
EditorGUIUtility.TrTextContent(
"LRU Eviction",
"When enabled, the cache evicts the least recently used duration instead of refusing new entries once the limit is reached."
);
private static readonly GUIContent WaitInstructionBufferApplyNowButtonContent =
EditorGUIUtility.TrTextContent("Apply Defaults Now");
private static readonly GUIContent WaitInstructionBufferCaptureCurrentButtonContent =
EditorGUIUtility.TrTextContent("Capture Current Values");
private static readonly GUIContent DuplicateAnimationModeContent =
EditorGUIUtility.TrTextContent(
"Animation Mode",
"Controls how duplicate entries are presented in SerializableDictionary inspectors (None, Static, or Tween)."
);
private static readonly GUIContent DuplicateTweenCyclesContent =
EditorGUIUtility.TrTextContent(
"Shake Cycles",
"Number of shake cycles performed when highlighting duplicate entries. Negative values loop indefinitely."
);
private static readonly GUIContent DetectAssetChangeLoopWindowContent =
EditorGUIUtility.TrTextContent(
"Window (s)",
"Time window (in seconds) used to detect repeated DetectAssetChanged callbacks before loop suppression disables them."
);
private static readonly GUIContent DeferAssetPostprocessorCallbacksContent =
EditorGUIUtility.TrTextContent(
"Defer Post-process Callbacks",
"Defer DetectAssetChanged and related asset-processor callbacks out of Unity's import phase to avoid 'SendMessage cannot be called...' warnings. Disable only if you need synchronous callback invocation and have audited your handlers for SendMessage safety."
);
private static readonly GUIContent SerializableSetDuplicateTweenEnabledContent =
EditorGUIUtility.TrTextContent(
"Animate Duplicates",
"Enable lateral shake animations when highlighting duplicate or invalid entries in SerializableHashSet and SerializableSortedSet inspectors."
);
private static readonly GUIContent SerializableSetDuplicateTweenCyclesContent =
EditorGUIUtility.TrTextContent(
"Shake Cycles",
"Number of shake cycles performed for SerializableSet duplicate entries. Negative values loop indefinitely."
);
private static readonly GUIContent WGroupAutoIncludeModeContent =
EditorGUIUtility.TrTextContent(
"Include Mode",
"Default behavior for automatically extending WGroup declarations (None, Finite, or Infinite)."
);
private static readonly GUIContent WGroupAutoIncludeCountContent =
EditorGUIUtility.TrTextContent(
"Include Count",
"Number of additional serialized members appended when auto include mode is Finite."
);
private static readonly GUIContent WGroupStartCollapsedContent =
EditorGUIUtility.TrTextContent(
"Start Collapsed",
"Default foldout state used when collapsible WGroups do not specify startCollapsed explicitly."
);
private static readonly GUIContent WGroupFoldoutTweenEnabledContent =
EditorGUIUtility.TrTextContent(
"Animate Foldouts",
"Enable animated transitions when expanding or collapsing WGroup foldouts."
);
private static readonly GUIContent WGroupFoldoutSpeedContent =
EditorGUIUtility.TrTextContent(
"Foldout Speed",
"Animation speed used when expanding or collapsing WGroup foldouts."
);
private const string PoolPurgingHelpText =
"Configure intelligent pool purging defaults. These settings control how pools automatically trim idle items based on usage patterns.";
private const string FailedTestsExporterHelpText =
"When enabled, the Failed Tests Exporter hooks into the Unity Test Runner to capture test failures and export them to a text file in a configurable directory (defaults to the project root).";
private static readonly GUIContent FailedTestsExporterEnabledContent =
EditorGUIUtility.TrTextContent(
"Enable Failed Tests Exporter",
"When enabled, automatically captures failed test results from the Unity Test Runner and provides menu items to export them."
);
private static readonly GUIContent FailedTestsOutputDirectoryContent =
EditorGUIUtility.TrTextContent(
"Output Directory",
"Relative directory path from the project root where failed test result files are saved. Leave empty to use the project root."
);
private static readonly GUIContent FailedTestsOutputDirectoryBrowseContent =
EditorGUIUtility.TrTextContent(
"Browse\u2026",
"Open a folder picker to select the output directory for failed test result files."
);
private static readonly GUIContent FailedTestsOutputDirectoryClearContent =
EditorGUIUtility.TrTextContent(
"\u00d7",
"Clear the output directory and use the project root instead."
);
private static readonly GUIContent PoolPurgingEnabledContent =
EditorGUIUtility.TrTextContent(
"Enable Global Purging",
"Enable intelligent pool purging globally. When enabled, pools automatically trim idle items based on usage patterns."
);
private static readonly GUIContent PoolIdleTimeoutContent = EditorGUIUtility.TrTextContent(
"Idle Timeout (s)",
"Default idle timeout in seconds. Items idle longer than this are eligible for purging."
);
private static readonly GUIContent PoolMinRetainCountContent =
EditorGUIUtility.TrTextContent(
"Min Retain Count",
"Minimum number of items to always retain in pools during purge operations."
);
private static readonly GUIContent PoolWarmRetainCountContent =
EditorGUIUtility.TrTextContent(
"Warm Retain Count",
"Number of items to keep warm in active pools to avoid cold-start allocations."
);
private static readonly GUIContent PoolMaxSizeContent = EditorGUIUtility.TrTextContent(
"Max Pool Size",
"Maximum pool size (0 = unbounded). Items exceeding this limit will be purged."
);
private static readonly GUIContent PoolBufferMultiplierContent =
EditorGUIUtility.TrTextContent(
"Buffer Multiplier",
"Buffer multiplier for comfortable pool size calculation."
);
private static readonly GUIContent PoolRollingWindowContent =
EditorGUIUtility.TrTextContent(
"Rolling Window (s)",
"Rolling window duration in seconds for high water mark tracking."
);
private static readonly GUIContent PoolHysteresisContent = EditorGUIUtility.TrTextContent(
"Hysteresis (s)",
"Hysteresis duration in seconds. Purging is suppressed for this duration after a usage spike."
);
private static readonly GUIContent PoolSpikeThresholdContent =
EditorGUIUtility.TrTextContent(
"Spike Threshold",
"Spike threshold multiplier. A spike is detected when concurrent rentals exceed the rolling average by this factor."
);
private static readonly GUIContent PoolTypeConfigurationsContent =
EditorGUIUtility.TrTextContent("Per-Type Pool Configurations");
private static readonly GUIContent PoolApplyNowButtonContent =
EditorGUIUtility.TrTextContent("Apply Settings Now");
public enum WButtonActionsPlacement
{
Top = 0,
Bottom = 1,
}
public enum WButtonFoldoutBehavior
{
AlwaysOpen = 0,
StartExpanded = 1,
StartCollapsed = 2,
}
public enum WGroupAutoIncludeMode
{
None = 0,
Finite = 1,
Infinite = 2,
}
public enum InlineEditorFoldoutBehavior
{
AlwaysOpen = 0,
StartExpanded = 1,
StartCollapsed = 2,
}
public readonly struct WGroupAutoIncludeConfiguration
{
public WGroupAutoIncludeConfiguration(WGroupAutoIncludeMode mode, int rowCount)
{
Mode = mode;
RowCount = rowCount < 0 ? 0 : rowCount;
}
public WGroupAutoIncludeMode Mode { get; }
public int RowCount { get; }
}
public readonly struct WButtonPaletteEntry
{
public WButtonPaletteEntry(Color buttonColor, Color textColor)
{
ButtonColor = buttonColor;
TextColor = textColor;
}
public Color ButtonColor { get; }
public Color TextColor { get; }
}
public readonly struct WEnumToggleButtonsPaletteEntry
{
public WEnumToggleButtonsPaletteEntry(
Color selectedBackgroundColor,
Color selectedTextColor,
Color inactiveBackgroundColor,
Color inactiveTextColor
)
{
SelectedBackgroundColor = selectedBackgroundColor;
SelectedTextColor = selectedTextColor;
InactiveBackgroundColor = inactiveBackgroundColor;
InactiveTextColor = inactiveTextColor;
}
public Color SelectedBackgroundColor { get; }
public Color SelectedTextColor { get; }
public Color InactiveBackgroundColor { get; }
public Color InactiveTextColor { get; }
}
public enum DuplicateRowAnimationMode
{
[Obsolete("Disable duplicate duplicate-row animations only when required.", false)]
None = 0,
Static = 1,
Tween = 2,
}
[FormerlySerializedAs("waitInstructionBufferApplyOnLoad")]
[SerializeField]
[Tooltip(
"Whether the configured wait instruction defaults should be applied automatically on domain reload and player start."
)]
[WGroup(
WaitInstructionBufferFoldoutKey,
displayName: "Buffers",
collapsible: true,
startCollapsed: true
)]
private bool _waitInstructionBufferApplyOnLoad = true;
[FormerlySerializedAs("waitInstructionBufferQuantizationStepSeconds")]
[SerializeField]
[Tooltip(
"Rounds requested WaitForSeconds durations to this step size before caching (set to 0 to disable)."
)]
[Min(0f)]
private float _waitInstructionBufferQuantizationStepSeconds;
[FormerlySerializedAs("waitInstructionBufferMaxDistinctEntries")]
[SerializeField]
[Tooltip(
"Maximum number of distinct WaitForSeconds/Realtime entries cached (0 = unlimited)."
)]
[Min(0)]
private int _waitInstructionBufferMaxDistinctEntries =
Buffers.WaitInstructionDefaultMaxDistinctEntries;
[FormerlySerializedAs("waitInstructionBufferUseLruEviction")]
[SerializeField]
[Tooltip(
"Evict the least recently used duration when the cache hits the distinct entry limit."
)]
private bool _waitInstructionBufferUseLruEviction;
[FormerlySerializedAs("waitInstructionBufferDefaultsInitialized")]
[SerializeField]
[HideInInspector]
private bool _waitInstructionBufferDefaultsInitialized;
[FormerlySerializedAs("stringInListPageSize")]
[SerializeField]
[Tooltip("Maximum number of entries shown per page for StringInList dropdowns.")]
[Range(MinPageSize, MaxPageSize)]
[WGroup(
"Pagination",
displayName: "Pagination Defaults",
autoIncludeCount: 5,
collapsible: true
)]
private int _stringInListPageSize = DefaultStringInListPageSize;
[FormerlySerializedAs("serializableSetPageSize")]
[SerializeField]
[Tooltip(
"Maximum number of entries shown per page when drawing SerializableHashSet/SerializableSortedSet inspectors."
)]
[Range(MinPageSize, MaxPageSize)]
private int _serializableSetPageSize = DefaultSerializableSetPageSize;
[FormerlySerializedAs("serializableSetStartCollapsed")]
[SerializeField]
[Tooltip(
"Whether SerializableHashSet and SerializableSortedSet inspectors start collapsed when first rendered."
)]
private bool _serializableSetStartCollapsed = true;
[FormerlySerializedAs("serializableDictionaryPageSize")]
[SerializeField]
[Tooltip(
"Maximum number of entries shown per page when drawing SerializableDictionary/SerializableSortedDictionary inspectors."
)]
[Range(MinPageSize, MaxSerializableDictionaryPageSize)]
private int _serializableDictionaryPageSize = DefaultSerializableDictionaryPageSize;
[FormerlySerializedAs("serializableDictionaryStartCollapsed")]
[SerializeField]
[Tooltip(
"Whether SerializableDictionary and SerializableSortedDictionary inspectors start collapsed when first rendered."
)]
private bool _serializableDictionaryStartCollapsed = true;
[FormerlySerializedAs("enumToggleButtonsPageSize")]
[SerializeField]
[Tooltip(
"Maximum number of toggle buttons shown per page when drawing WEnumToggleButtons groups."
)]
[Range(MinPageSize, MaxPageSize)]
private int _enumToggleButtonsPageSize = DefaultEnumToggleButtonsPageSize;
[FormerlySerializedAs("wbuttonPageSize")]
[SerializeField]
[Tooltip("Maximum number of WButton actions displayed per page in inspector trays.")]
[Range(MinPageSize, MaxPageSize)]
[WGroup(
"WButton Actions",
displayName: "WButton Actions",
autoIncludeCount: 1,
collapsible: true
)]
[WGroupEnd("Pagination")]
private int _wbuttonPageSize = DefaultWButtonPageSize;
[FormerlySerializedAs("wbuttonHistorySize")]
[SerializeField]
[Tooltip("Number of recent invocation results retained per WButton method.")]
[Range(MinWButtonHistorySize, MaxWButtonHistorySize)]
private int _wbuttonHistorySize = DefaultWButtonHistorySize;
[FormerlySerializedAs("wbuttonActionsPlacement")]
[SerializeField]
[Tooltip("Controls where WButton actions are rendered relative to the inspector content.")]
[WGroup(
"WButton Layout",
displayName: "WButton Layout",
autoIncludeCount: 3,
collapsible: true
)]
private WButtonActionsPlacement _wbuttonActionsPlacement = WButtonActionsPlacement.Top;
[FormerlySerializedAs("wbuttonFoldoutBehavior")]
[SerializeField]
[Tooltip(
"Determines whether WButton groups are always shown or foldouts start expanded/collapsed."
)]
private WButtonFoldoutBehavior _wbuttonFoldoutBehavior =
WButtonFoldoutBehavior.StartExpanded;
[FormerlySerializedAs("wbuttonFoldoutTweenEnabled")]
[SerializeField]
[Tooltip("Animate WButton action foldouts when toggled.")]
private bool _wbuttonFoldoutTweenEnabled = true;
[FormerlySerializedAs("wbuttonFoldoutSpeed")]
[SerializeField]
[Tooltip("Animation speed used when toggling WButton action foldouts.")]
[WShowIf(nameof(_wbuttonFoldoutTweenEnabled))]
[Range(MinFoldoutSpeed, MaxFoldoutSpeed)]
private float _wbuttonFoldoutSpeed = DefaultFoldoutSpeed;
[FormerlySerializedAs("wbuttonCancelButtonColor")]
[SerializeField]
[Tooltip(
"Background color for the Cancel button that appears during async WButton execution."
)]
[WGroupEnd("WButton Layout")]
[WGroup(
"WButton Colors",
displayName: "WButton Colors",
autoIncludeCount: 3,
collapsible: true
)]
private Color _wbuttonCancelButtonColor = DefaultCancelButtonColor;
[FormerlySerializedAs("wbuttonCancelButtonTextColor")]
[SerializeField]
[Tooltip("Text color for the Cancel button.")]
private Color _wbuttonCancelButtonTextColor = DefaultCancelButtonTextColor;
[FormerlySerializedAs("wbuttonClearHistoryButtonColor")]
[SerializeField]
[Tooltip("Background color for the Clear History button in WButton result history.")]
private Color _wbuttonClearHistoryButtonColor = DefaultClearHistoryButtonColor;
[FormerlySerializedAs("wbuttonClearHistoryButtonTextColor")]
[SerializeField]
[Tooltip("Text color for the Clear History button.")]
private Color _wbuttonClearHistoryButtonTextColor = DefaultClearHistoryButtonTextColor;
[FormerlySerializedAs("serializableDictionaryFoldoutTweenEnabled")]
[SerializeField]
[Tooltip(
"Animation speed used when toggling SerializableDictionary pending entry foldouts."
)]
[WGroup(
"Dictionary Foldouts",
displayName: "Dictionary Foldouts",
autoIncludeCount: 4,
collapsible: true
)]
private bool _serializableDictionaryFoldoutTweenEnabled = true;
[FormerlySerializedAs("foldoutTweenSettingsInitialized")]
[SerializeField]
[HideInInspector]
private bool _foldoutTweenSettingsInitialized;
[FormerlySerializedAs("serializableDictionaryFoldoutSpeed")]
[SerializeField]
[Tooltip(
"Animation speed used when toggling SerializableDictionary pending entry foldouts."
)]
[WShowIf(nameof(_serializableDictionaryFoldoutTweenEnabled))]
[Range(MinFoldoutSpeed, MaxFoldoutSpeed)]
private float _serializableDictionaryFoldoutSpeed = DefaultFoldoutSpeed;
[FormerlySerializedAs("serializableSortedDictionaryFoldoutTweenEnabled")]
[SerializeField]
[Tooltip(
"Animation speed used when toggling SerializableSortedDictionary pending entry foldouts."
)]
private bool _serializableSortedDictionaryFoldoutTweenEnabled = true;
[FormerlySerializedAs("serializableSortedDictionaryFoldoutSpeed")]
[SerializeField]
[Tooltip(
"Animation speed used when toggling SerializableSortedDictionary pending entry foldouts."
)]
[WShowIf(nameof(_serializableSortedDictionaryFoldoutTweenEnabled))]
[Range(MinFoldoutSpeed, MaxFoldoutSpeed)]
private float _serializableSortedDictionaryFoldoutSpeed = DefaultFoldoutSpeed;
[FormerlySerializedAs("serializableSetFoldoutTweenEnabled")]
[SerializeField]
[Tooltip(
"Enable animated transitions when expanding or collapsing SerializableHashSet manual entry foldouts."
)]
[WGroup("Serializable Sets", displayName: "Serializable Sets", collapsible: true)]
private bool _serializableSetFoldoutTweenEnabled = true;
[FormerlySerializedAs("serializableSetFoldoutSpeed")]
[SerializeField]
[Tooltip("Animation speed used when toggling SerializableHashSet manual entry foldouts.")]
[WShowIf(nameof(_serializableSetFoldoutTweenEnabled))]
[Range(MinFoldoutSpeed, MaxFoldoutSpeed)]
private float _serializableSetFoldoutSpeed = DefaultFoldoutSpeed;
[FormerlySerializedAs("serializableSetDuplicateTweenEnabled")]
[SerializeField]
[Tooltip(
"Enable lateral shake animations when highlighting duplicate or invalid entries in SerializableSet inspectors."
)]
private bool _serializableSetDuplicateTweenEnabled = true;
[FormerlySerializedAs("serializableSetDuplicateTweenCycles")]
[SerializeField]
[Tooltip(
"When enabled, number of shake cycles to play for SerializableSet duplicate entries. Negative values loop indefinitely."
)]
[WShowIf(nameof(_serializableSetDuplicateTweenEnabled))]
private int _serializableSetDuplicateTweenCycles = DefaultDuplicateTweenCycles;
[FormerlySerializedAs("serializableSetDuplicateTweenSettingsInitialized")]
[SerializeField]
[HideInInspector]
private bool _serializableSetDuplicateTweenSettingsInitialized;
[FormerlySerializedAs("serializableSortedSetFoldoutTweenEnabled")]
[SerializeField]
[Tooltip(
"Enable animated transitions when expanding or collapsing SerializableSortedSet manual entry foldouts."
)]
[WGroup("Sorted Set", displayName: "Sorted Sets", autoIncludeCount: 1, collapsible: true)]
private bool _serializableSortedSetFoldoutTweenEnabled = true;
[FormerlySerializedAs("serializableSortedSetFoldoutSpeed")]
[SerializeField]
[Tooltip("Animation speed used when toggling SerializableSortedSet manual entry foldouts.")]
[WShowIf(nameof(_serializableSortedSetFoldoutTweenEnabled))]
[Range(MinFoldoutSpeed, MaxFoldoutSpeed)]
[WGroupEnd("Sorted Set Foldouts")]
private float _serializableSortedSetFoldoutSpeed = DefaultFoldoutSpeed;
[FormerlySerializedAs("duplicateRowAnimationMode")]
[SerializeField]
[Tooltip(
"Controls how duplicate entries are emphasized inside SerializableDictionary inspectors."
)]
[WGroup(
"Duplicate Highlighting",
displayName: "Duplicate Highlighting",
autoIncludeCount: 1,
collapsible: true
)]
private DuplicateRowAnimationMode _duplicateRowAnimationMode =
DuplicateRowAnimationMode.Tween;
[FormerlySerializedAs("duplicateRowTweenCycles")]
[SerializeField]
[Tooltip(
"When using Tween, number of shake cycles to play for duplicate entries. Negative values loop indefinitely."
)]
[WShowIf(
nameof(_duplicateRowAnimationMode),
expectedValues: new object[] { DuplicateRowAnimationMode.Tween }
)]
private int _duplicateRowTweenCycles = DefaultDuplicateTweenCycles;
[FormerlySerializedAs("detectAssetChangeLoopWindowSeconds")]
[SerializeField]
[Tooltip(
"Time window used to detect repeated DetectAssetChanged callbacks before loop suppression engages."
)]
[Min(MinDetectAssetChangeLoopWindowSeconds)]
[WGroup(
"Detect Asset Changes",
displayName: "Detect Asset Changes",
autoIncludeCount: 2,
collapsible: true
)]
private float _detectAssetChangeLoopWindowSeconds =
DefaultDetectAssetChangeLoopWindowSeconds;
[SerializeField]
[Tooltip(
"Defer DetectAssetChanged and related asset-processor callbacks out of Unity's import phase to avoid 'SendMessage cannot be called...' warnings. Disable only if you need synchronous callback invocation and have audited your handlers for SendMessage safety."
)]
private bool _deferAssetPostprocessorCallbacks = DefaultDeferAssetPostprocessorCallbacks;
[FormerlySerializedAs("serializableTypeIgnorePatterns")]
[SerializeField]
[Tooltip(
"Regular expressions evaluated against type names to exclude them from SerializableType pickers."
)]
[WGroup(
"Serializable Types",
displayName: "Serializable Types",
autoIncludeCount: 0,
collapsible: true
)]
private List _serializableTypeIgnorePatterns;
private string[] _serializableTypeIgnorePatternCache = Array.Empty();
private int _serializableTypeIgnorePatternCacheVersion = int.MinValue;
[FormerlySerializedAs("wgroupAutoIncludeMode")]
[SerializeField]
[Tooltip(
"Controls how WGroup automatically includes additional serialized members after a group declaration."
)]
[WGroup(
"WGroup Defaults",
displayName: "WGroup Defaults",
autoIncludeCount: 4,
collapsible: true
)]
private WGroupAutoIncludeMode _wgroupAutoIncludeMode = WGroupAutoIncludeMode.Infinite;
[FormerlySerializedAs("wgroupAutoIncludeRowCount")]
[SerializeField]
[Tooltip(
"Number of additional serialized members captured when the WGroup auto include mode is set to Finite."
)]
[WShowIf(
nameof(_wgroupAutoIncludeMode),
expectedValues: new object[] { WGroupAutoIncludeMode.Finite }
)]
[Range(MinWGroupAutoIncludeRowCount, MaxWGroupAutoIncludeRowCount)]
private int _wgroupAutoIncludeRowCount = DefaultWGroupAutoIncludeRowCount;
[FormerlySerializedAs("wgroupFoldoutsStartCollapsed")]
[SerializeField]
[Tooltip(
"When enabled, collapsible WGroup headers start closed unless the attribute overrides startCollapsed."
)]
private bool _wgroupFoldoutsStartCollapsed = true;
[SerializeField]
[Tooltip("Enable animated transitions when expanding or collapsing WGroup foldouts.")]
private bool _wgroupFoldoutTweenEnabled = true;
[SerializeField]
[Tooltip("Animation speed used when expanding or collapsing WGroup foldouts.")]
[WShowIf(nameof(_wgroupFoldoutTweenEnabled))]
[Range(MinFoldoutSpeed, MaxFoldoutSpeed)]
private float _wgroupFoldoutSpeed = DefaultFoldoutSpeed;
[FormerlySerializedAs("wbuttonCustomColors")]
[SerializeField]
[Tooltip("Named color palette applied to WButton custom color keys.")]
[WGroup(
"Color Palettes",
displayName: "Color Palettes",
autoIncludeCount: 4,
collapsible: true
)]
private WButtonCustomColorDictionary _wbuttonCustomColors = new();
[FormerlySerializedAs("wenumToggleButtonsCustomColors")]
[SerializeField]
[Tooltip("Named color palette applied to WEnumToggleButtons color keys.")]
[WGroupEnd("Color Palettes")]
private WEnumToggleButtonsCustomColorDictionary _wenumToggleButtonsCustomColors = new();
[FormerlySerializedAs("legacyWButtonPriorityColors")]
[SerializeField]
[FormerlySerializedAs("wbuttonPriorityColors")]
#pragma warning disable CS0618 // Type or member is obsolete
[HideInInspector]
private List _legacyWButtonPriorityColors;
#pragma warning restore CS0618 // Type or member is obsolete
[FormerlySerializedAs("serializableTypePatternsInitialized")]
[SerializeField]
[HideInInspector]
private bool _serializableTypePatternsInitialized;
[NonSerialized]
private HashSet _wbuttonCustomColorSkipAutoSuggest;
[FormerlySerializedAs("inlineEditorFoldoutBehavior")]
[SerializeField]
[Tooltip(
"Default foldout behavior used by WInLineEditor when a field does not override the mode."
)]
[WGroup(
"InlineEditors",
displayName: "Inline Editors",
autoIncludeCount: 3,
collapsible: true
)]
private InlineEditorFoldoutBehavior _inlineEditorFoldoutBehavior =
InlineEditorFoldoutBehavior.StartCollapsed;
[SerializeField]
[Tooltip(
"Enable animated transitions when expanding or collapsing WInLineEditor foldouts."
)]
private bool _inlineEditorFoldoutTweenEnabled = true;
[SerializeField]
[Tooltip("Animation speed used when expanding or collapsing WInLineEditor foldouts.")]
[WShowIf(nameof(_inlineEditorFoldoutTweenEnabled))]
[Range(MinFoldoutSpeed, MaxFoldoutSpeed)]
private float _inlineEditorFoldoutSpeed = DefaultFoldoutSpeed;
private const string PoolPurgingFoldoutKey = "PoolPurging";
private const string FailedTestsExporterFoldoutKey = "FailedTestsExporter";
[SerializeField]
[Tooltip(
"Enable intelligent pool purging globally. When enabled, pools automatically trim idle items based on usage patterns."
)]
[WGroup(
PoolPurgingFoldoutKey,
displayName: "Pool Purging",
autoIncludeCount: 6,
collapsible: true,
startCollapsed: true
)]
internal bool _poolPurgingEnabled = DefaultPoolIntelligentPurgingEnabled;
[SerializeField]
[Tooltip(
"Default idle timeout in seconds. Items idle longer than this are eligible for purging."
)]
[Min(0f)]
internal float _poolIdleTimeoutSeconds = DefaultPoolIntelligentIdleTimeoutSeconds;
[SerializeField]
[Tooltip("Minimum number of items to always retain in pools during purge operations.")]
[Min(0)]
internal int _poolMinRetainCount = DefaultPoolMinRetainCount;
[SerializeField]
[Tooltip("Number of items to keep warm in active pools to avoid cold-start allocations.")]
[Min(0)]
internal int _poolWarmRetainCount = DefaultPoolWarmRetainCount;
[SerializeField]
[Tooltip("Maximum pool size (0 = unbounded). Items exceeding this limit will be purged.")]
[Min(0)]
internal int _poolMaxSize = DefaultPoolMaxSize;
[SerializeField]
[Tooltip(
"Buffer multiplier for comfortable pool size calculation. Comfortable size = max(MinRetainCount, rollingHighWaterMark * BufferMultiplier)."
)]
[Min(1f)]
internal float _poolBufferMultiplier = DefaultPoolBufferMultiplier;
[SerializeField]
[Tooltip("Rolling window duration in seconds for high water mark tracking.")]
[Min(1f)]
internal float _poolRollingWindowSeconds = DefaultPoolRollingWindowSeconds;
[SerializeField]
[Tooltip(
"Hysteresis duration in seconds. Purging is suppressed for this duration after a usage spike."
)]
[Min(0f)]
internal float _poolHysteresisSeconds = DefaultPoolHysteresisSeconds;
[SerializeField]
[Tooltip(
"Spike threshold multiplier. A spike is detected when concurrent rentals exceed the rolling average by this factor."
)]
[Min(1f)]
[WGroupEnd(PoolPurgingFoldoutKey)]
internal float _poolSpikeThresholdMultiplier = DefaultPoolSpikeThresholdMultiplier;
[SerializeField]
[Tooltip("Per-type pool purging configurations.")]
[WGroup(
"PoolTypeConfigurations",
displayName: "Per-Type Pool Settings",
collapsible: true,
startCollapsed: true
)]
internal List _poolTypeConfigurations = new();
[SerializeField]
[HideInInspector]
private bool _poolPurgingSettingsInitialized;
[SerializeField]
[Tooltip(
"When enabled, the Failed Tests Exporter automatically captures test failures from the Unity Test Runner."
)]
[WGroup(
FailedTestsExporterFoldoutKey,
displayName: "Failed Tests Exporter",
collapsible: true,
startCollapsed: true
)]
internal bool _failedTestsExporterEnabled = DefaultFailedTestsExporterEnabled;
[SerializeField]
[Tooltip(
"Relative directory path from the project root where failed test result files are saved. Leave empty to use the project root."
)]
[WShowIf(nameof(_failedTestsExporterEnabled))]
[WGroupEnd(FailedTestsExporterFoldoutKey)]
internal string _failedTestsOutputDirectory = DefaultFailedTestsOutputDirectory;
internal HashSet WButtonCustomColorSkipAutoSuggest
{
get => _wbuttonCustomColorSkipAutoSuggest;
set => _wbuttonCustomColorSkipAutoSuggest = value;
}
[Serializable]
internal sealed class SerializableTypeIgnorePattern
{
[FormerlySerializedAs("pattern")]
[SerializeField]
internal string _pattern = string.Empty;
public SerializableTypeIgnorePattern() { }
public SerializableTypeIgnorePattern(string pattern)
{
Pattern = pattern;
}
public string Pattern
{
get => _pattern ?? string.Empty;
set => _pattern = value ?? string.Empty;
}
}
[Serializable]
internal sealed class WButtonCustomColor
{
[FormerlySerializedAs("buttonColor")]
[SerializeField]
internal Color _buttonColor = Color.white;
[FormerlySerializedAs("textColor")]
[SerializeField]
internal Color _textColor = Color.black;
public Color ButtonColor
{
get => _buttonColor;
set => _buttonColor = value;
}
public Color TextColor
{
get => _textColor;
set => _textColor = value;
}
public void EnsureReadableText()
{
if (_textColor.maxColorComponent <= 0f)
{
_textColor = WButtonColorUtility.GetReadableTextColor(_buttonColor);
}
}
}
[Serializable]
private sealed class WButtonCustomColorDictionary
: SerializableDictionary { }
#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(WButtonCustomColor))]
private sealed class WButtonCustomColorDrawer : PropertyDrawer
{
private static readonly GUIContent ButtonLabelContent = EditorGUIUtility.TrTextContent(
"Button"
);
private static readonly GUIContent TextLabelContent = EditorGUIUtility.TrTextContent(
"Text"
);
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
return EditorGUIUtility.singleLineHeight;
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
SerializedProperty buttonColor = property.FindPropertyRelative(
SerializedPropertyNames.WButtonCustomColorButton
);
SerializedProperty textColor = property.FindPropertyRelative(
SerializedPropertyNames.WButtonCustomColorText
);
float spacing = EditorGUIUtility.standardVerticalSpacing;
float availableWidth = Mathf.Max(0f, position.width - spacing);
float halfWidth = availableWidth * 0.5f;
float labelWidth = Mathf.Clamp(
halfWidth * CustomColorDrawerLabelWidthRatio,
CustomColorDrawerMinLabelWidth,
CustomColorDrawerMaxLabelWidth
);
float previousLabelWidth = EditorGUIUtility.labelWidth;
EditorGUIUtility.labelWidth = labelWidth;
try
{
Rect buttonRect = new(position.x, position.y, halfWidth, position.height);
Rect textRect = new(
position.x + halfWidth + spacing,
position.y,
halfWidth,
position.height
);
float minFieldWidth = CustomColorDrawerMinColorFieldWidth + labelWidth;
bool useLabels = halfWidth >= minFieldWidth;
EditorGUI.PropertyField(
buttonRect,
buttonColor,
useLabels ? ButtonLabelContent : GUIContent.none
);
EditorGUI.PropertyField(
textRect,
textColor,
useLabels ? TextLabelContent : GUIContent.none
);
}
finally
{
EditorGUIUtility.labelWidth = previousLabelWidth;
}
}
}
[CustomPropertyDrawer(typeof(WEnumToggleButtonsCustomColor))]
private sealed class WEnumToggleButtonsCustomColorDrawer : PropertyDrawer
{
private static readonly GUIContent SelectedBackgroundLabelContent =
EditorGUIUtility.TrTextContent("Selected BG");
private static readonly GUIContent SelectedTextLabelContent =
EditorGUIUtility.TrTextContent("Selected Text");
private static readonly GUIContent InactiveBackgroundLabelContent =
EditorGUIUtility.TrTextContent("Inactive BG");
private static readonly GUIContent InactiveTextLabelContent =
EditorGUIUtility.TrTextContent("Inactive Text");
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
float lineHeight = EditorGUIUtility.singleLineHeight;
float spacing = EditorGUIUtility.standardVerticalSpacing;
return lineHeight * 2f + spacing;
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
SerializedProperty selectedBackground = property.FindPropertyRelative(
SerializedPropertyNames.WEnumToggleButtonsSelectedBackground
);
SerializedProperty selectedText = property.FindPropertyRelative(
SerializedPropertyNames.WEnumToggleButtonsSelectedText
);
SerializedProperty inactiveBackground = property.FindPropertyRelative(
SerializedPropertyNames.WEnumToggleButtonsInactiveBackground
);
SerializedProperty inactiveText = property.FindPropertyRelative(
SerializedPropertyNames.WEnumToggleButtonsInactiveText
);
float spacing = EditorGUIUtility.standardVerticalSpacing;
float availableWidth = Mathf.Max(0f, position.width - spacing);
float halfWidth = availableWidth * 0.5f;
float lineHeight = EditorGUIUtility.singleLineHeight;
float labelWidth = Mathf.Clamp(
halfWidth * CustomColorDrawerLabelWidthRatio,
CustomColorDrawerMinLabelWidth,
CustomColorDrawerMaxLabelWidth
);
float previousLabelWidth = EditorGUIUtility.labelWidth;
EditorGUIUtility.labelWidth = labelWidth;
try
{
Rect selectedBackgroundRect = new(
position.x,
position.y,
halfWidth,
lineHeight
);
Rect selectedTextRect = new(
position.x + halfWidth + spacing,
position.y,
halfWidth,
lineHeight
);
Rect inactiveBackgroundRect = new(
position.x,
position.y + lineHeight + spacing,
halfWidth,
lineHeight
);
Rect inactiveTextRect = new(
position.x + halfWidth + spacing,
position.y + lineHeight + spacing,
halfWidth,
lineHeight
);
float minFieldWidth = CustomColorDrawerMinColorFieldWidth + labelWidth;
bool useLabels = halfWidth >= minFieldWidth;
EditorGUI.PropertyField(
selectedBackgroundRect,
selectedBackground,
useLabels ? SelectedBackgroundLabelContent : GUIContent.none
);
EditorGUI.PropertyField(
selectedTextRect,
selectedText,
useLabels ? SelectedTextLabelContent : GUIContent.none
);
EditorGUI.PropertyField(
inactiveBackgroundRect,
inactiveBackground,
useLabels ? InactiveBackgroundLabelContent : GUIContent.none
);
EditorGUI.PropertyField(
inactiveTextRect,
inactiveText,
useLabels ? InactiveTextLabelContent : GUIContent.none
);
}
finally
{
EditorGUIUtility.labelWidth = previousLabelWidth;
}
}
}
#endif
[Serializable]
internal sealed class WEnumToggleButtonsCustomColor
{
[FormerlySerializedAs("selectedBackgroundColor")]
[SerializeField]
internal Color _selectedBackgroundColor = DefaultColorKeyButtonColor;
[FormerlySerializedAs("selectedTextColor")]
[SerializeField]
internal Color _selectedTextColor = Color.white;
[FormerlySerializedAs("inactiveBackgroundColor")]
[SerializeField]
internal Color _inactiveBackgroundColor = DefaultLightThemeButtonColor;
[FormerlySerializedAs("inactiveTextColor")]
[SerializeField]
internal Color _inactiveTextColor = Color.black;
public Color SelectedBackgroundColor
{
get => _selectedBackgroundColor;
set => _selectedBackgroundColor = value;
}
public Color SelectedTextColor
{
get => _selectedTextColor;
set => _selectedTextColor = value;
}
public Color InactiveBackgroundColor
{
get => _inactiveBackgroundColor;
set => _inactiveBackgroundColor = value;
}
public Color InactiveTextColor
{
get => _inactiveTextColor;
set => _inactiveTextColor = value;
}
public void EnsureReadableText()
{
if (_selectedTextColor.maxColorComponent <= 0f)
{
_selectedTextColor = WButtonColorUtility.GetReadableTextColor(
_selectedBackgroundColor
);
}
if (_inactiveTextColor.maxColorComponent <= 0f)
{
_inactiveTextColor = WButtonColorUtility.GetReadableTextColor(
_inactiveBackgroundColor
);
}
}
}
[Serializable]
private sealed class WEnumToggleButtonsCustomColorDictionary
: SerializableDictionary { }
///
/// Retrieves the effective page size for StringInList drawers, clamped to safe bounds.
///
public int StringInListPageSize
{
get => Mathf.Clamp(_stringInListPageSize, MinPageSize, MaxPageSize);
set
{
int clamped = Mathf.Clamp(value, MinPageSize, MaxPageSize);
if (clamped == _stringInListPageSize)
{
return;
}
_stringInListPageSize = clamped;
SaveSettings();
}
}
///
/// Retrieves the configured page size for SerializableSet inspectors.
///
public int SerializableSetPageSize
{
get => Mathf.Clamp(_serializableSetPageSize, MinPageSize, MaxPageSize);
set
{
int clamped = Mathf.Clamp(value, MinPageSize, MaxPageSize);
if (clamped == _serializableSetPageSize)
{
return;
}
_serializableSetPageSize = clamped;
SaveSettings();
}
}
///
/// Configures whether SerializableSet inspectors start collapsed by default.
///
public bool SerializableSetStartCollapsed
{
get => _serializableSetStartCollapsed;
set
{
if (_serializableSetStartCollapsed == value)
{
return;
}
_serializableSetStartCollapsed = value;
SaveSettings();
}
}
///
/// Retrieves the configured page size for SerializableDictionary inspectors.
///
public int SerializableDictionaryPageSize
{
get =>
Mathf.Clamp(
_serializableDictionaryPageSize,
MinPageSize,
MaxSerializableDictionaryPageSize
);
set
{
int clamped = Mathf.Clamp(value, MinPageSize, MaxSerializableDictionaryPageSize);
if (clamped == _serializableDictionaryPageSize)
{
return;
}
_serializableDictionaryPageSize = clamped;
SaveSettings();
}
}
///
/// Configures whether SerializableDictionary inspectors start collapsed by default.
///
public bool SerializableDictionaryStartCollapsed
{
get => _serializableDictionaryStartCollapsed;
set
{
if (_serializableDictionaryStartCollapsed == value)
{
return;
}
_serializableDictionaryStartCollapsed = value;
SaveSettings();
}
}
///
/// Configures whether collapsible WGroup headers start closed when their attribute does not specify a preference.
///
public bool WGroupFoldoutsStartCollapsed
{
get => _wgroupFoldoutsStartCollapsed;
set
{
if (_wgroupFoldoutsStartCollapsed == value)
{
return;
}
_wgroupFoldoutsStartCollapsed = value;
WGroupLayoutBuilder.ClearCache();
SaveSettings();
}
}
///
/// Configures whether WGroup foldouts animate when expanding or collapsing.
///
public bool WGroupFoldoutTweenEnabled
{
get => _wgroupFoldoutTweenEnabled;
set
{
if (_wgroupFoldoutTweenEnabled == value)
{
return;
}
_wgroupFoldoutTweenEnabled = value;
SaveSettings();
}
}
///
/// Configures the animation speed for WGroup foldout transitions.
///
public float WGroupFoldoutSpeed
{
get => Mathf.Clamp(_wgroupFoldoutSpeed, MinFoldoutSpeed, MaxFoldoutSpeed);
set
{
float clamped = Mathf.Clamp(value, MinFoldoutSpeed, MaxFoldoutSpeed);
if (Mathf.Approximately(clamped, _wgroupFoldoutSpeed))
{
return;
}
_wgroupFoldoutSpeed = clamped;
SaveSettings();
}
}
///
/// Configures whether WInLineEditor foldouts animate when expanding or collapsing.
///
public bool InlineEditorFoldoutTweenEnabled
{
get => _inlineEditorFoldoutTweenEnabled;
set
{
if (_inlineEditorFoldoutTweenEnabled == value)
{
return;
}
_inlineEditorFoldoutTweenEnabled = value;
SaveSettings();
}
}
///
/// Configures the animation speed for WInLineEditor foldout transitions.
///
public float InlineEditorFoldoutSpeed
{
get => Mathf.Clamp(_inlineEditorFoldoutSpeed, MinFoldoutSpeed, MaxFoldoutSpeed);
set
{
float clamped = Mathf.Clamp(value, MinFoldoutSpeed, MaxFoldoutSpeed);
if (Mathf.Approximately(clamped, _inlineEditorFoldoutSpeed))
{
return;
}
_inlineEditorFoldoutSpeed = clamped;
SaveSettings();
}
}
///
/// Gets the configured page size for WEnumToggleButtons groups.
///
public int EnumToggleButtonsPageSize
{
get => Mathf.Clamp(_enumToggleButtonsPageSize, MinPageSize, MaxPageSize);
set
{
int clamped = Mathf.Clamp(value, MinPageSize, MaxPageSize);
if (clamped == _enumToggleButtonsPageSize)
{
return;
}
_enumToggleButtonsPageSize = clamped;
SaveSettings();
}
}
[Serializable]
[Obsolete("Use WButtonCustomColorDictionary for serialization instead.")]
private sealed class WButtonPriorityColor
{
[FormerlySerializedAs("priority")]
[SerializeField]
internal string _priority = DefaultWButtonColorKey;
[FormerlySerializedAs("buttonColor")]
[SerializeField]
private Color _buttonColor = Color.white;
[FormerlySerializedAs("textColor")]
[SerializeField]
private Color _textColor = Color.black;
public WButtonPriorityColor() { }
public WButtonPriorityColor(string priority, Color buttonColor, Color textColor)
{
Priority = priority;
ButtonColor = buttonColor;
TextColor = textColor;
}
public string Priority
{
get =>
string.IsNullOrWhiteSpace(_priority)
? DefaultWButtonColorKey
: _priority.Trim();
set =>
_priority = string.IsNullOrWhiteSpace(value)
? DefaultWButtonColorKey
: value.Trim();
}
public Color ButtonColor
{
get => _buttonColor;
set => _buttonColor = value;
}
public Color TextColor
{
get => _textColor;
set => _textColor = value;
}
}
///
/// Retrieves the configured page size for WButton groups.
///
public int WButtonPageSize
{
get => Mathf.Clamp(_wbuttonPageSize, MinPageSize, MaxPageSize);
set
{
int clamped = Mathf.Clamp(value, MinPageSize, MaxPageSize);
if (clamped == _wbuttonPageSize)
{
return;
}
_wbuttonPageSize = clamped;
SaveSettings();
}
}
///
/// Retrieves the number of results retained per WButton method.
///
public int WButtonHistorySize
{
get => Mathf.Clamp(_wbuttonHistorySize, MinWButtonHistorySize, MaxWButtonHistorySize);
set
{
int clamped = Mathf.Clamp(value, MinWButtonHistorySize, MaxWButtonHistorySize);
if (clamped == _wbuttonHistorySize)
{
return;
}
_wbuttonHistorySize = clamped;
SaveSettings();
}
}
///
/// Current duplicate-row animation mode used by dictionary inspectors.
///
public DuplicateRowAnimationMode DuplicateRowAnimation
{
get => _duplicateRowAnimationMode;
set
{
if (_duplicateRowAnimationMode == value)
{
return;
}
_duplicateRowAnimationMode = value;
SaveSettings();
}
}
///
/// Number of tween cycles configured for duplicate rows. Negative values loop indefinitely.
///
public int DuplicateRowTweenCycles
{
get => _duplicateRowTweenCycles;
set
{
if (_duplicateRowTweenCycles == value)
{
return;
}
_duplicateRowTweenCycles = value;
SaveSettings();
}
}
///
/// Duration used to detect repeated DetectAssetChanged callbacks before loop suppression activates.
///
public float DetectAssetChangeLoopWindowSeconds
{
get =>
Mathf.Clamp(
_detectAssetChangeLoopWindowSeconds <= 0f
? DefaultDetectAssetChangeLoopWindowSeconds
: _detectAssetChangeLoopWindowSeconds,
MinDetectAssetChangeLoopWindowSeconds,
MaxDetectAssetChangeLoopWindowSeconds
);
set
{
float clamped = Mathf.Clamp(
value <= 0f ? DefaultDetectAssetChangeLoopWindowSeconds : value,
MinDetectAssetChangeLoopWindowSeconds,
MaxDetectAssetChangeLoopWindowSeconds
);
if (Mathf.Approximately(clamped, _detectAssetChangeLoopWindowSeconds))
{
return;
}
_detectAssetChangeLoopWindowSeconds = clamped;
SaveSettings();
}
}
///
/// When true, DetectAssetChanged and related editor asset-processor callbacks are
/// deferred out of Unity's asset-import phase. Prevents spurious
/// "SendMessage cannot be called during Awake, CheckConsistency, or OnValidate"
/// warnings that Unity emits when our processors call
/// AssetDatabase.LoadAllAssetsAtPath / GetComponentsInChildren
/// synchronously from OnPostprocessAllAssets.
///
public bool DeferAssetPostprocessorCallbacks
{
get => _deferAssetPostprocessorCallbacks;
set
{
if (_deferAssetPostprocessorCallbacks == value)
{
return;
}
_deferAssetPostprocessorCallbacks = value;
SaveSettings();
}
}
internal IReadOnlyList GetSerializableTypeIgnorePatterns()
{
if (
_serializableTypeIgnorePatterns == null
|| _serializableTypeIgnorePatterns.Count == 0
)
{
_serializableTypeIgnorePatternCache = Array.Empty();
_serializableTypeIgnorePatternCacheVersion = 0;
return _serializableTypeIgnorePatternCache;
}
int version = ComputeSerializableTypePatternVersion();
if (version == _serializableTypeIgnorePatternCacheVersion)
{
return _serializableTypeIgnorePatternCache;
}
using PooledResource> patternsLease = Buffers.List.Get(
out List patterns
);
using PooledResource> seenLease = Buffers.HashSet.Get(
out HashSet seen
);
foreach (SerializableTypeIgnorePattern entry in _serializableTypeIgnorePatterns)
{
if (entry == null)
{
continue;
}
string pattern = entry.Pattern;
if (string.IsNullOrWhiteSpace(pattern))
{
continue;
}
string trimmed = pattern.Trim();
if (seen.Add(trimmed))
{
patterns.Add(trimmed);
}
}
_serializableTypeIgnorePatternCache =
patterns.Count == 0 ? Array.Empty() : patterns.ToArray();
_serializableTypeIgnorePatternCacheVersion = version;
return _serializableTypeIgnorePatternCache;
}
///
/// Returns the configured page size, falling back to defaults if unset.
///
public static int GetStringInListPageLimit()
{
return instance.StringInListPageSize;
}
public static int GetSerializableSetPageSize()
{
return Mathf.Clamp(instance.SerializableSetPageSize, MinPageSize, MaxPageSize);
}
///
/// Determines whether SerializableSet inspectors should start collapsed by default.
///
public static bool ShouldStartSerializableSetCollapsed()
{
UnityHelpersSettings settings = instance;
return settings == null || settings._serializableSetStartCollapsed;
}
private void InvalidateSerializableTypePatternCache()
{
_serializableTypeIgnorePatternCache = Array.Empty();
_serializableTypeIgnorePatternCacheVersion = int.MinValue;
}
private int ComputeSerializableTypePatternVersion()
{
if (
_serializableTypeIgnorePatterns == null
|| _serializableTypeIgnorePatterns.Count == 0
)
{
return 0;
}
HashCode hash = new HashCode();
hash.Add(_serializableTypeIgnorePatterns.Count);
for (int i = 0; i < _serializableTypeIgnorePatterns.Count; i++)
{
SerializableTypeIgnorePattern entry = _serializableTypeIgnorePatterns[i];
string trimmed = entry?.Pattern?.Trim() ?? string.Empty;
hash.Add(trimmed);
}
return hash.ToHashCode();
}
public static int GetSerializableDictionaryPageSize()
{
return Mathf.Clamp(
instance.SerializableDictionaryPageSize,
MinPageSize,
MaxSerializableDictionaryPageSize
);
}
///
/// Determines whether SerializableDictionary inspectors should start collapsed by default.
///
public static bool ShouldStartSerializableDictionaryCollapsed()
{
UnityHelpersSettings settings = instance;
return settings == null || settings._serializableDictionaryStartCollapsed;
}
///
/// Determines whether collapsible WGroup headers should default to a collapsed state.
///
public static bool ShouldStartWGroupCollapsed()
{
UnityHelpersSettings settings = instance;
return settings == null || settings._wgroupFoldoutsStartCollapsed;
}
///
/// Determines whether WGroup foldouts should animate when expanding or collapsing.
///
public static bool ShouldTweenWGroupFoldouts()
{
return instance._wgroupFoldoutTweenEnabled;
}
internal static void SetWGroupFoldoutTweenEnabled(bool value)
{
instance._wgroupFoldoutTweenEnabled = value;
}
///
/// Gets the animation speed for WGroup foldout transitions.
///
public static float GetWGroupFoldoutSpeed()
{
return Mathf.Clamp(instance._wgroupFoldoutSpeed, MinFoldoutSpeed, MaxFoldoutSpeed);
}
public static int GetEnumToggleButtonsPageSize()
{
return Mathf.Clamp(instance.EnumToggleButtonsPageSize, MinPageSize, MaxPageSize);
}
public static int GetWButtonPageSize()
{
return Mathf.Clamp(instance.WButtonPageSize, MinPageSize, MaxPageSize);
}
public static int GetWButtonHistorySize()
{
return Mathf.Clamp(
instance.WButtonHistorySize,
MinWButtonHistorySize,
MaxWButtonHistorySize
);
}
public static float GetDetectAssetChangeLoopWindowSeconds()
{
UnityHelpersSettings settings = instance;
if (settings == null)
{
return DefaultDetectAssetChangeLoopWindowSeconds;
}
return Mathf.Clamp(
settings.DetectAssetChangeLoopWindowSeconds,
MinDetectAssetChangeLoopWindowSeconds,
MaxDetectAssetChangeLoopWindowSeconds
);
}
public static bool GetDeferAssetPostprocessorCallbacks()
{
UnityHelpersSettings settings = instance;
if (settings == null)
{
return DefaultDeferAssetPostprocessorCallbacks;
}
return settings.DeferAssetPostprocessorCallbacks;
}
public static WGroupAutoIncludeConfiguration GetWGroupAutoIncludeConfiguration()
{
UnityHelpersSettings settings = instance;
int clamped = Mathf.Clamp(
settings._wgroupAutoIncludeRowCount,
MinWGroupAutoIncludeRowCount,
MaxWGroupAutoIncludeRowCount
);
return new WGroupAutoIncludeConfiguration(settings._wgroupAutoIncludeMode, clamped);
}
#if UNITY_EDITOR
internal static void SetWGroupAutoIncludeConfigurationForTests(
WGroupAutoIncludeMode mode,
int rowCount
)
{
UnityHelpersSettings settings = instance;
settings._wgroupAutoIncludeMode = mode;
settings._wgroupAutoIncludeRowCount = Mathf.Clamp(
rowCount,
MinWGroupAutoIncludeRowCount,
MaxWGroupAutoIncludeRowCount
);
settings.SaveSettings();
}
#endif
public static WButtonPaletteEntry ResolveWButtonPalette(string colorKey)
{
return instance.GetWButtonPaletteEntry(colorKey);
}
internal static bool HasWButtonPaletteColorKey(string colorKey)
{
return instance.ContainsColorKey(colorKey);
}
public static WEnumToggleButtonsPaletteEntry ResolveWEnumToggleButtonsPalette(
string colorKey
)
{
return instance.GetWEnumToggleButtonsPaletteEntry(colorKey);
}
internal static bool HasWEnumToggleButtonsPaletteColorKey(string colorKey)
{
return instance.ContainsWEnumToggleButtonsColorKey(colorKey);
}
public static WButtonActionsPlacement GetWButtonActionsPlacement()
{
return instance._wbuttonActionsPlacement;
}
public static WButtonFoldoutBehavior GetWButtonFoldoutBehavior()
{
return instance._wbuttonFoldoutBehavior;
}
public static bool ShouldTweenWButtonFoldouts()
{
return instance._wbuttonFoldoutTweenEnabled;
}
public static float GetWButtonFoldoutSpeed()
{
return Mathf.Clamp(instance._wbuttonFoldoutSpeed, MinFoldoutSpeed, MaxFoldoutSpeed);
}
public static WButtonPaletteEntry GetWButtonCancelButtonColors()
{
UnityHelpersSettings settings = instance;
return new WButtonPaletteEntry(
settings._wbuttonCancelButtonColor,
settings._wbuttonCancelButtonTextColor
);
}
public static WButtonPaletteEntry GetWButtonClearHistoryButtonColors()
{
UnityHelpersSettings settings = instance;
return new WButtonPaletteEntry(
settings._wbuttonClearHistoryButtonColor,
settings._wbuttonClearHistoryButtonTextColor
);
}
public static bool ShouldTweenSerializableDictionaryFoldouts()
{
return instance._serializableDictionaryFoldoutTweenEnabled;
}
internal static void SetSerializableDictionaryFoldoutTweenEnabled(bool value)
{
instance._serializableDictionaryFoldoutTweenEnabled = value;
}
public static float GetSerializableDictionaryFoldoutSpeed()
{
return Mathf.Clamp(
instance._serializableDictionaryFoldoutSpeed,
MinFoldoutSpeed,
MaxFoldoutSpeed
);
}
public static bool ShouldTweenSerializableSortedDictionaryFoldouts()
{
return instance._serializableSortedDictionaryFoldoutTweenEnabled;
}
internal static void SetSerializableSortedDictionaryFoldoutTweenEnabled(bool value)
{
instance._serializableSortedDictionaryFoldoutTweenEnabled = value;
}
public static float GetSerializableSortedDictionaryFoldoutSpeed()
{
return Mathf.Clamp(
instance._serializableSortedDictionaryFoldoutSpeed,
MinFoldoutSpeed,
MaxFoldoutSpeed
);
}
public static bool ShouldTweenSerializableSetFoldouts()
{
return instance._serializableSetFoldoutTweenEnabled;
}
internal static void SetSerializableSetFoldoutTweenEnabled(bool value)
{
instance._serializableSetFoldoutTweenEnabled = value;
}
public static float GetSerializableSetFoldoutSpeed()
{
return Mathf.Clamp(
instance._serializableSetFoldoutSpeed,
MinFoldoutSpeed,
MaxFoldoutSpeed
);
}
public static bool ShouldTweenSerializableSortedSetFoldouts()
{
return instance._serializableSortedSetFoldoutTweenEnabled;
}
internal static void SetSerializableSortedSetFoldoutTweenEnabled(bool value)
{
instance._serializableSortedSetFoldoutTweenEnabled = value;
}
public static float GetSerializableSortedSetFoldoutSpeed()
{
return Mathf.Clamp(
instance._serializableSortedSetFoldoutSpeed,
MinFoldoutSpeed,
MaxFoldoutSpeed
);
}
public static DuplicateRowAnimationMode GetDuplicateRowAnimationMode()
{
return instance._duplicateRowAnimationMode;
}
public static int GetDuplicateRowTweenCycleLimit()
{
return instance._duplicateRowTweenCycles;
}
public static bool ShouldTweenSerializableSetDuplicates()
{
return instance._serializableSetDuplicateTweenEnabled
&& instance._duplicateRowAnimationMode == DuplicateRowAnimationMode.Tween;
}
public static int GetSerializableSetDuplicateTweenCycleLimit()
{
if (!instance._serializableSetDuplicateTweenSettingsInitialized)
{
return instance._duplicateRowTweenCycles;
}
return instance._serializableSetDuplicateTweenCycles;
}
public static InlineEditorFoldoutBehavior GetInlineEditorFoldoutBehavior()
{
return instance._inlineEditorFoldoutBehavior;
}
internal static void SetInlineEditorFoldoutBehavior(InlineEditorFoldoutBehavior value)
{
instance._inlineEditorFoldoutBehavior = value;
}
///
/// Determines whether WInLineEditor foldouts should animate when expanding or collapsing.
///
public static bool ShouldTweenInlineEditorFoldouts()
{
return instance._inlineEditorFoldoutTweenEnabled;
}
///
/// Gets the animation speed for WInLineEditor foldout transitions.
///
public static float GetInlineEditorFoldoutSpeed()
{
return Mathf.Clamp(
instance._inlineEditorFoldoutSpeed,
MinFoldoutSpeed,
MaxFoldoutSpeed
);
}
///
/// Gets whether intelligent pool purging is globally enabled in settings.
///
public static bool GetPoolPurgingEnabled()
{
return instance._poolPurgingEnabled;
}
///
/// Gets whether the Failed Tests Exporter is enabled in settings.
///
public static bool GetFailedTestsExporterEnabled()
{
return instance._failedTestsExporterEnabled;
}
///
/// Gets the validated relative output directory for failed test result files.
/// The returned path uses forward slashes and has no trailing separator.
///
///
/// The validated relative directory path, or
/// (empty string, meaning project root) if the configured path is invalid, does not exist,
/// or escapes the project root.
///
public static string GetFailedTestsOutputDirectory()
{
string directory = instance._failedTestsOutputDirectory;
if (string.IsNullOrWhiteSpace(directory))
{
return DefaultFailedTestsOutputDirectory;
}
// Normalize separators
directory = directory.Replace('\\', '/').TrimEnd('/');
// Reject absolute paths, paths with .., and other invalid patterns
if (Path.IsPathRooted(directory) || directory.Contains(".."))
{
return DefaultFailedTestsOutputDirectory;
}
try
{
string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
string fullPath = Path.GetFullPath(Path.Combine(projectRoot, directory));
// Ensure the resolved path is still within the project root
if (!fullPath.StartsWith(projectRoot, StringComparison.OrdinalIgnoreCase))
{
return DefaultFailedTestsOutputDirectory;
}
if (!Directory.Exists(fullPath))
{
return DefaultFailedTestsOutputDirectory;
}
return directory;
}
catch
{
return DefaultFailedTestsOutputDirectory;
}
}
///
/// Gets the default idle timeout in seconds for pool purging.
///
public static float GetPoolIdleTimeoutSeconds()
{
return Mathf.Max(0f, instance._poolIdleTimeoutSeconds);
}
///
/// Gets the default minimum retain count for pool purging.
///
public static int GetPoolMinRetainCount()
{
return Mathf.Max(0, instance._poolMinRetainCount);
}
///
/// Gets the default warm retain count for pool purging.
/// Active pools keep this many warm to avoid cold-start allocations.
///
public static int GetPoolWarmRetainCount()
{
return Mathf.Max(0, instance._poolWarmRetainCount);
}
///
/// Gets the default maximum pool size.
///
public static int GetPoolMaxSize()
{
return Mathf.Max(0, instance._poolMaxSize);
}
///
/// Gets the default buffer multiplier for comfortable pool size calculation.
///
public static float GetPoolBufferMultiplier()
{
return Mathf.Max(1f, instance._poolBufferMultiplier);
}
///
/// Gets the default rolling window duration in seconds.
///
public static float GetPoolRollingWindowSeconds()
{
return Mathf.Max(1f, instance._poolRollingWindowSeconds);
}
///
/// Gets the default hysteresis duration in seconds.
///
public static float GetPoolHysteresisSeconds()
{
return Mathf.Max(0f, instance._poolHysteresisSeconds);
}
///
/// Gets the default spike threshold multiplier.
///
public static float GetPoolSpikeThresholdMultiplier()
{
return Mathf.Max(1f, instance._poolSpikeThresholdMultiplier);
}
///
/// Gets the per-type pool configurations from settings.
///
public static IReadOnlyList GetPoolTypeConfigurations()
{
UnityHelpersSettings settings = instance;
if (settings._poolTypeConfigurations == null)
{
return Array.Empty();
}
return settings._poolTypeConfigurations;
}
///
/// Applies the current pool purging settings to PoolPurgeSettings.
///
public static void ApplyPoolPurgingSettingsToRuntime()
{
UnityHelpersSettings settings = instance;
PoolPurgeSettings.GlobalEnabled = settings._poolPurgingEnabled;
PoolPurgeSettings.DefaultGlobalIdleTimeoutSeconds = Mathf.Max(
0f,
settings._poolIdleTimeoutSeconds
);
PoolPurgeSettings.DefaultGlobalMinRetainCount = Mathf.Max(
0,
settings._poolMinRetainCount
);
PoolPurgeSettings.DefaultGlobalWarmRetainCount = Mathf.Max(
0,
settings._poolWarmRetainCount
);
PoolPurgeSettings.DefaultGlobalMaxPoolSize = Mathf.Max(0, settings._poolMaxSize);
PoolPurgeSettings.DefaultGlobalBufferMultiplier = Mathf.Max(
1f,
settings._poolBufferMultiplier
);
PoolPurgeSettings.DefaultGlobalRollingWindowSeconds = Mathf.Max(
1f,
settings._poolRollingWindowSeconds
);
PoolPurgeSettings.DefaultGlobalHysteresisSeconds = Mathf.Max(
0f,
settings._poolHysteresisSeconds
);
PoolPurgeSettings.DefaultGlobalSpikeThresholdMultiplier = Mathf.Max(
1f,
settings._poolSpikeThresholdMultiplier
);
ApplyPoolTypeConfigurationsToRuntime(settings._poolTypeConfigurations);
}
private static void ApplyPoolTypeConfigurationsToRuntime(
List configurations
)
{
// Clear previous settings-based configurations before reapplying
PoolPurgeSettings.ClearSettingsTypeConfigurations();
if (configurations == null || configurations.Count == 0)
{
return;
}
foreach (PoolTypeConfiguration config in configurations)
{
if (config == null)
{
continue;
}
Type type = config.ResolveType();
if (type == null)
{
continue;
}
if (!config.Enabled)
{
// Use settings-based disable (lower priority than programmatic Disable)
PoolPurgeSettings.DisableFromSettings(type);
continue;
}
PoolPurgeTypeOptions options = config.ToPoolPurgeTypeOptions();
if (type.IsGenericTypeDefinition)
{
// Use settings-based generic configuration (lower priority than programmatic ConfigureGeneric)
PoolPurgeSettings.ConfigureGenericFromSettings(type, options);
}
else
{
// Use settings-based configuration (lower priority than programmatic Configure)
PoolPurgeSettings.ConfigureFromSettings(type, options);
}
}
}
// Kept for backwards compatibility and possible future use
[System.Diagnostics.CodeAnalysis.SuppressMessage(
"CodeQuality",
"IDE0051:Remove unused private members",
Justification = "Reserved for future use"
)]
private static void ConfigureTypeViaReflection(Type type, PoolPurgeTypeOptions options)
{
System.Reflection.MethodInfo configureMethod = typeof(PoolPurgeSettings)
.GetMethod(
nameof(PoolPurgeSettings.Configure),
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static
)
?.MakeGenericMethod(type);
if (configureMethod == null)
{
return;
}
Action configureAction = existing =>
{
existing.Enabled = options.Enabled;
existing.IdleTimeoutSeconds = options.IdleTimeoutSeconds;
existing.MinRetainCount = options.MinRetainCount;
existing.WarmRetainCount = options.WarmRetainCount;
existing.BufferMultiplier = options.BufferMultiplier;
existing.RollingWindowSeconds = options.RollingWindowSeconds;
existing.HysteresisSeconds = options.HysteresisSeconds;
existing.SpikeThresholdMultiplier = options.SpikeThresholdMultiplier;
};
try
{
configureMethod.Invoke(null, new object[] { configureAction });
}
catch
{
// Ignore configuration errors - they shouldn't crash the editor
}
}
internal static void RegisterPaletteManualEdit(string propertyPath, string key)
{
if (string.IsNullOrWhiteSpace(propertyPath) || string.IsNullOrWhiteSpace(key))
{
return;
}
instance?.RegisterPaletteManualEditInternal(propertyPath, key);
}
internal static class SerializedPropertyNames
{
internal const string SerializableTypeIgnorePatterns = nameof(
_serializableTypeIgnorePatterns
);
internal const string SerializableTypePatternsInitialized = nameof(
_serializableTypePatternsInitialized
);
internal const string SerializableTypePattern = nameof(
SerializableTypeIgnorePattern._pattern
);
internal const string LegacyWButtonPriorityColors = nameof(
_legacyWButtonPriorityColors
);
internal const string WButtonCustomColors = nameof(_wbuttonCustomColors);
internal const string WGroupFoldoutsStartCollapsed = nameof(
_wgroupFoldoutsStartCollapsed
);
internal const string WGroupFoldoutTweenEnabled = nameof(_wgroupFoldoutTweenEnabled);
internal const string WGroupFoldoutSpeed = nameof(_wgroupFoldoutSpeed);
internal const string WEnumToggleButtonsCustomColors = nameof(
_wenumToggleButtonsCustomColors
);
internal const string InlineEditorFoldoutBehavior = nameof(
_inlineEditorFoldoutBehavior
);
internal const string InlineEditorFoldoutTweenEnabled = nameof(
_inlineEditorFoldoutTweenEnabled
);
internal const string InlineEditorFoldoutSpeed = nameof(_inlineEditorFoldoutSpeed);
internal const string WButtonFoldoutTweenEnabled = nameof(_wbuttonFoldoutTweenEnabled);
internal const string SerializableDictionaryFoldoutTweenEnabled = nameof(
_serializableDictionaryFoldoutTweenEnabled
);
internal const string SerializableSortedDictionaryFoldoutTweenEnabled = nameof(
_serializableSortedDictionaryFoldoutTweenEnabled
);
internal const string SerializableSetFoldoutTweenEnabled = nameof(
_serializableSetFoldoutTweenEnabled
);
internal const string SerializableSortedSetFoldoutTweenEnabled = nameof(
_serializableSortedSetFoldoutTweenEnabled
);
internal const string FoldoutTweenSettingsInitialized = nameof(
_foldoutTweenSettingsInitialized
);
internal const string SerializableDictionaryFoldoutSpeed = nameof(
_serializableDictionaryFoldoutSpeed
);
internal const string SerializableSortedDictionaryFoldoutSpeed = nameof(
_serializableSortedDictionaryFoldoutSpeed
);
internal const string SerializableSetFoldoutSpeed = nameof(
_serializableSetFoldoutSpeed
);
internal const string SerializableSortedSetFoldoutSpeed = nameof(
_serializableSortedSetFoldoutSpeed
);
internal const string DetectAssetChangeLoopWindowSeconds = nameof(
_detectAssetChangeLoopWindowSeconds
);
internal const string DeferAssetPostprocessorCallbacks = nameof(
_deferAssetPostprocessorCallbacks
);
#pragma warning disable CS0618 // Type or member is obsolete
internal const string WButtonPriority = nameof(WButtonPriorityColor._priority);
#pragma warning restore CS0618 // Type or member is obsolete
internal const string WButtonCustomColorButton = nameof(
WButtonCustomColor._buttonColor
);
internal const string WButtonCustomColorText = nameof(WButtonCustomColor._textColor);
internal const string WEnumToggleButtonsSelectedBackground = nameof(
WEnumToggleButtonsCustomColor._selectedBackgroundColor
);
internal const string WEnumToggleButtonsSelectedText = nameof(
WEnumToggleButtonsCustomColor._selectedTextColor
);
internal const string WEnumToggleButtonsInactiveBackground = nameof(
WEnumToggleButtonsCustomColor._inactiveBackgroundColor
);
internal const string WEnumToggleButtonsInactiveText = nameof(
WEnumToggleButtonsCustomColor._inactiveTextColor
);
internal const string FailedTestsOutputDirectory = nameof(_failedTestsOutputDirectory);
///
/// Gets the serialized property name value for the given constant name.
/// Exposed for testing to avoid reflection-based field access.
///
internal static string GetPropertyNameValue(string constantName)
{
return constantName switch
{
nameof(SerializableTypeIgnorePatterns) => SerializableTypeIgnorePatterns,
nameof(SerializableTypePatternsInitialized) =>
SerializableTypePatternsInitialized,
nameof(SerializableTypePattern) => SerializableTypePattern,
nameof(LegacyWButtonPriorityColors) => LegacyWButtonPriorityColors,
nameof(WButtonCustomColors) => WButtonCustomColors,
nameof(WGroupFoldoutsStartCollapsed) => WGroupFoldoutsStartCollapsed,
nameof(WGroupFoldoutTweenEnabled) => WGroupFoldoutTweenEnabled,
nameof(WGroupFoldoutSpeed) => WGroupFoldoutSpeed,
nameof(WEnumToggleButtonsCustomColors) => WEnumToggleButtonsCustomColors,
nameof(InlineEditorFoldoutBehavior) => InlineEditorFoldoutBehavior,
nameof(InlineEditorFoldoutTweenEnabled) => InlineEditorFoldoutTweenEnabled,
nameof(InlineEditorFoldoutSpeed) => InlineEditorFoldoutSpeed,
nameof(WButtonFoldoutTweenEnabled) => WButtonFoldoutTweenEnabled,
nameof(SerializableDictionaryFoldoutTweenEnabled) =>
SerializableDictionaryFoldoutTweenEnabled,
nameof(SerializableSortedDictionaryFoldoutTweenEnabled) =>
SerializableSortedDictionaryFoldoutTweenEnabled,
nameof(SerializableSetFoldoutTweenEnabled) =>
SerializableSetFoldoutTweenEnabled,
nameof(SerializableSortedSetFoldoutTweenEnabled) =>
SerializableSortedSetFoldoutTweenEnabled,
nameof(FoldoutTweenSettingsInitialized) => FoldoutTweenSettingsInitialized,
nameof(SerializableDictionaryFoldoutSpeed) =>
SerializableDictionaryFoldoutSpeed,
nameof(SerializableSortedDictionaryFoldoutSpeed) =>
SerializableSortedDictionaryFoldoutSpeed,
nameof(SerializableSetFoldoutSpeed) => SerializableSetFoldoutSpeed,
nameof(SerializableSortedSetFoldoutSpeed) => SerializableSortedSetFoldoutSpeed,
nameof(DetectAssetChangeLoopWindowSeconds) =>
DetectAssetChangeLoopWindowSeconds,
nameof(DeferAssetPostprocessorCallbacks) => DeferAssetPostprocessorCallbacks,
nameof(WButtonPriority) => WButtonPriority,
nameof(WButtonCustomColorButton) => WButtonCustomColorButton,
nameof(WButtonCustomColorText) => WButtonCustomColorText,
nameof(WEnumToggleButtonsSelectedBackground) =>
WEnumToggleButtonsSelectedBackground,
nameof(WEnumToggleButtonsSelectedText) => WEnumToggleButtonsSelectedText,
nameof(WEnumToggleButtonsInactiveBackground) =>
WEnumToggleButtonsInactiveBackground,
nameof(WEnumToggleButtonsInactiveText) => WEnumToggleButtonsInactiveText,
nameof(FailedTestsOutputDirectory) => FailedTestsOutputDirectory,
_ => null,
};
}
}
///
/// Constants for custom color drawer layout calculations, exposed for testing.
///
internal static class CustomColorDrawerLayout
{
///
/// Minimum width for a color field (the picker itself, excluding label).
///
internal const float MinColorFieldWidth = CustomColorDrawerMinColorFieldWidth;
///
/// Ratio of column width to label width (e.g., 0.38 means label is 38% of the column).
///
internal const float LabelWidthRatio = CustomColorDrawerLabelWidthRatio;
///
/// Minimum label width in pixels.
///
internal const float MinLabelWidth = CustomColorDrawerMinLabelWidth;
///
/// Maximum label width in pixels.
///
internal const float MaxLabelWidth = CustomColorDrawerMaxLabelWidth;
///
/// Calculates the label width for a given column width using the same logic as the drawers.
///
internal static float CalculateLabelWidth(float columnWidth)
{
return Mathf.Clamp(columnWidth * LabelWidthRatio, MinLabelWidth, MaxLabelWidth);
}
///
/// Determines whether labels should be displayed for a given column width.
///
internal static bool ShouldShowLabels(float columnWidth)
{
float labelWidth = CalculateLabelWidth(columnWidth);
float minFieldWidth = MinColorFieldWidth + labelWidth;
return columnWidth >= minFieldWidth;
}
}
///
/// Ensures persisted data stays within valid range.
///
internal void OnEnable()
{
_stringInListPageSize = Mathf.Clamp(
_stringInListPageSize <= 0 ? DefaultStringInListPageSize : _stringInListPageSize,
MinPageSize,
MaxPageSize
);
_serializableSetPageSize = Mathf.Clamp(
_serializableSetPageSize <= 0
? DefaultSerializableSetPageSize
: _serializableSetPageSize,
MinPageSize,
MaxPageSize
);
_serializableDictionaryPageSize = Mathf.Clamp(
_serializableDictionaryPageSize <= 0
? DefaultSerializableDictionaryPageSize
: _serializableDictionaryPageSize,
MinPageSize,
MaxSerializableDictionaryPageSize
);
_enumToggleButtonsPageSize = Mathf.Clamp(
_enumToggleButtonsPageSize <= 0
? DefaultEnumToggleButtonsPageSize
: _enumToggleButtonsPageSize,
MinPageSize,
MaxPageSize
);
_wbuttonPageSize = Mathf.Clamp(
_wbuttonPageSize <= 0 ? DefaultWButtonPageSize : _wbuttonPageSize,
MinPageSize,
MaxPageSize
);
_wbuttonHistorySize = Mathf.Clamp(
_wbuttonHistorySize <= 0 ? DefaultWButtonHistorySize : _wbuttonHistorySize,
MinWButtonHistorySize,
MaxWButtonHistorySize
);
if (!Enum.IsDefined(typeof(WButtonActionsPlacement), _wbuttonActionsPlacement))
{
_wbuttonActionsPlacement = WButtonActionsPlacement.Top;
}
if (!Enum.IsDefined(typeof(WButtonFoldoutBehavior), _wbuttonFoldoutBehavior))
{
_wbuttonFoldoutBehavior = WButtonFoldoutBehavior.StartExpanded;
}
if (!Enum.IsDefined(typeof(WGroupAutoIncludeMode), _wgroupAutoIncludeMode))
{
_wgroupAutoIncludeMode = WGroupAutoIncludeMode.Infinite;
}
if (_wgroupAutoIncludeRowCount < MinWGroupAutoIncludeRowCount)
{
_wgroupAutoIncludeRowCount = DefaultWGroupAutoIncludeRowCount;
}
_wgroupAutoIncludeRowCount = Mathf.Clamp(
_wgroupAutoIncludeRowCount,
MinWGroupAutoIncludeRowCount,
MaxWGroupAutoIncludeRowCount
);
_wgroupFoldoutSpeed = Mathf.Clamp(
_wgroupFoldoutSpeed <= 0f ? DefaultFoldoutSpeed : _wgroupFoldoutSpeed,
MinFoldoutSpeed,
MaxFoldoutSpeed
);
_inlineEditorFoldoutSpeed = Mathf.Clamp(
_inlineEditorFoldoutSpeed <= 0f ? DefaultFoldoutSpeed : _inlineEditorFoldoutSpeed,
MinFoldoutSpeed,
MaxFoldoutSpeed
);
_wbuttonFoldoutSpeed = Mathf.Clamp(
_wbuttonFoldoutSpeed <= 0f ? DefaultFoldoutSpeed : _wbuttonFoldoutSpeed,
MinFoldoutSpeed,
MaxFoldoutSpeed
);
_serializableDictionaryFoldoutSpeed = Mathf.Clamp(
_serializableDictionaryFoldoutSpeed <= 0f
? DefaultFoldoutSpeed
: _serializableDictionaryFoldoutSpeed,
MinFoldoutSpeed,
MaxFoldoutSpeed
);
_serializableSortedDictionaryFoldoutSpeed = Mathf.Clamp(
_serializableSortedDictionaryFoldoutSpeed <= 0f
? DefaultFoldoutSpeed
: _serializableSortedDictionaryFoldoutSpeed,
MinFoldoutSpeed,
MaxFoldoutSpeed
);
_serializableSetFoldoutSpeed = Mathf.Clamp(
_serializableSetFoldoutSpeed <= 0f
? DefaultFoldoutSpeed
: _serializableSetFoldoutSpeed,
MinFoldoutSpeed,
MaxFoldoutSpeed
);
_serializableSortedSetFoldoutSpeed = Mathf.Clamp(
_serializableSortedSetFoldoutSpeed <= 0f
? DefaultFoldoutSpeed
: _serializableSortedSetFoldoutSpeed,
MinFoldoutSpeed,
MaxFoldoutSpeed
);
_detectAssetChangeLoopWindowSeconds = Mathf.Clamp(
_detectAssetChangeLoopWindowSeconds <= 0f
? DefaultDetectAssetChangeLoopWindowSeconds
: _detectAssetChangeLoopWindowSeconds,
MinDetectAssetChangeLoopWindowSeconds,
MaxDetectAssetChangeLoopWindowSeconds
);
// Validate the failed tests output directory
if (!string.IsNullOrEmpty(_failedTestsOutputDirectory))
{
string validatedDirectory = GetFailedTestsOutputDirectory();
if (
!string.Equals(
_failedTestsOutputDirectory,
validatedDirectory,
StringComparison.Ordinal
)
)
{
_failedTestsOutputDirectory = validatedDirectory;
SaveSettings();
}
}
if (EnsureFoldoutTweenDefaults())
{
SaveSettings();
}
if (EnsureWButtonCustomColorDefaults())
{
SaveSettings();
}
if (EnsureWEnumToggleButtonsCustomColorDefaults())
{
SaveSettings();
}
bool shouldApplyRuntimeConfig = true;
if (EnsureSerializableTypePatternDefaults())
{
SaveSettings();
shouldApplyRuntimeConfig = false;
}
if (EnsureSerializableSetTweenDefaults())
{
SaveSettings();
shouldApplyRuntimeConfig = false;
}
if (shouldApplyRuntimeConfig)
{
ApplyRuntimeConfiguration();
}
}
///
/// Persists any modifications to disk.
///
public void SaveSettings()
{
EnsureWButtonCustomColorDefaults();
EnsureWEnumToggleButtonsCustomColorDefaults();
ApplyRuntimeConfiguration();
Save(true);
OnSettingsSaved?.Invoke();
}
private void ApplyRuntimeConfiguration()
{
IReadOnlyList patterns = GetSerializableTypeIgnorePatterns();
SerializableTypeCatalog.ConfigureTypeNameIgnorePatterns(patterns);
SerializableTypeCatalog.WarmPatternStats(patterns);
// Apply pool purging settings to runtime
ApplyPoolPurgingSettingsToRuntime();
}
private bool EnsureSerializableTypePatternDefaults()
{
_serializableTypeIgnorePatterns ??= new List();
if (_serializableTypePatternsInitialized)
{
return false;
}
if (_serializableTypeIgnorePatterns.Count == 0)
{
IReadOnlyList defaults = SerializableTypeCatalog.GetDefaultIgnorePatterns();
for (int index = 0; index < defaults.Count; index++)
{
_serializableTypeIgnorePatterns.Add(
new SerializableTypeIgnorePattern(defaults[index])
);
}
InvalidateSerializableTypePatternCache();
}
bool changed = !_serializableTypePatternsInitialized;
_serializableTypePatternsInitialized = true;
return changed;
}
private bool EnsureFoldoutTweenDefaults()
{
if (_foldoutTweenSettingsInitialized)
{
return false;
}
if (!_wbuttonFoldoutTweenEnabled)
{
_wbuttonFoldoutTweenEnabled = true;
}
if (!_serializableDictionaryFoldoutTweenEnabled)
{
_serializableDictionaryFoldoutTweenEnabled = true;
}
if (!_serializableSortedDictionaryFoldoutTweenEnabled)
{
_serializableSortedDictionaryFoldoutTweenEnabled = true;
}
if (!_serializableSetFoldoutTweenEnabled)
{
_serializableSetFoldoutTweenEnabled = true;
}
if (!_serializableSortedSetFoldoutTweenEnabled)
{
_serializableSortedSetFoldoutTweenEnabled = true;
}
_foldoutTweenSettingsInitialized = true;
return true;
}
private bool EnsureSerializableSetTweenDefaults()
{
if (_serializableSetDuplicateTweenSettingsInitialized)
{
return false;
}
_serializableSetDuplicateTweenEnabled =
_duplicateRowAnimationMode == DuplicateRowAnimationMode.Tween;
_serializableSetDuplicateTweenCycles =
_duplicateRowTweenCycles != 0
? _duplicateRowTweenCycles
: DefaultDuplicateTweenCycles;
_serializableSetDuplicateTweenSettingsInitialized = true;
return true;
}
internal bool EnsureWButtonCustomColorDefaults()
{
_wbuttonCustomColors ??= new WButtonCustomColorDictionary();
bool changed = false;
changed |= MigrateLegacyWButtonPalette();
if (
_wbuttonCustomColors.TryGetValue(
DefaultWButtonColorKey,
out WButtonCustomColor legacyDefault
)
)
{
_wbuttonCustomColors.TryAdd(WButtonLegacyColorKey, legacyDefault);
_wbuttonCustomColors.Remove(DefaultWButtonColorKey);
changed = true;
}
if (!_wbuttonCustomColors.ContainsKey(WButtonLegacyColorKey))
{
WButtonCustomColor legacyColor = new()
{
ButtonColor = DefaultColorKeyButtonColor,
TextColor = WButtonColorUtility.GetReadableTextColor(
DefaultColorKeyButtonColor
),
};
_wbuttonCustomColors[WButtonLegacyColorKey] = legacyColor;
changed = true;
}
changed |= EnsureWButtonThemeEntry(
WButtonLightThemeColorKey,
DefaultLightThemeButtonColor,
Color.black
);
changed |= EnsureWButtonThemeEntry(
WButtonDarkThemeColorKey,
DefaultDarkThemeButtonColor,
Color.white
);
int paletteIndex = 0;
foreach (
KeyValuePair entry in _wbuttonCustomColors.ToArray()
)
{
WButtonCustomColor value = entry.Value;
if (value == null)
{
value = new WButtonCustomColor();
_wbuttonCustomColors[entry.Key] = value;
value.ButtonColor = DefaultColorKeyButtonColor;
value.EnsureReadableText();
changed = true;
}
if (IsReservedWButtonColorKey(entry.Key))
{
value.EnsureReadableText();
continue;
}
if (ShouldSkipWButtonAutoSuggest(entry.Key))
{
continue;
}
bool needsSuggestion =
value.ButtonColor.maxColorComponent <= 0f
|| (
ColorsApproximatelyEqual(value.ButtonColor, Color.white)
&& ColorsApproximatelyEqual(value.TextColor, Color.black)
);
if (needsSuggestion)
{
Color suggested = WButtonColorUtility.SuggestPaletteColor(paletteIndex);
value.ButtonColor = suggested;
value.TextColor = WButtonColorUtility.GetReadableTextColor(suggested);
changed = true;
}
else
{
Color previousText = value.TextColor;
value.EnsureReadableText();
if (!ColorsApproximatelyEqual(value.TextColor, previousText))
{
changed = true;
}
}
paletteIndex++;
}
return changed;
}
private bool ShouldSkipWButtonAutoSuggest(string key)
{
return ShouldSkipAutoSuggest(_wbuttonCustomColorSkipAutoSuggest, key);
}
private bool EnsureWButtonThemeEntry(string key, Color buttonColor, Color defaultTextColor)
{
if (
_wbuttonCustomColors.TryGetValue(key, out WButtonCustomColor existing)
&& existing != null
)
{
existing.EnsureReadableText();
return false;
}
Color textColor =
defaultTextColor.maxColorComponent <= 0f
? WButtonColorUtility.GetReadableTextColor(buttonColor)
: defaultTextColor;
WButtonCustomColor themeColor = new()
{
ButtonColor = buttonColor,
TextColor = textColor,
};
themeColor.EnsureReadableText();
_wbuttonCustomColors[key] = themeColor;
return true;
}
private static bool IsReservedWButtonColorKey(string key)
{
if (string.IsNullOrWhiteSpace(key))
{
return true;
}
return string.Equals(key, DefaultWButtonColorKey, StringComparison.OrdinalIgnoreCase)
|| string.Equals(key, WButtonLightThemeColorKey, StringComparison.OrdinalIgnoreCase)
|| string.Equals(key, WButtonDarkThemeColorKey, StringComparison.OrdinalIgnoreCase)
|| string.Equals(key, WButtonLegacyColorKey, StringComparison.OrdinalIgnoreCase);
}
private bool MigrateLegacyWButtonPalette()
{
#pragma warning disable CS0618 // Type or member is obsolete
if (_legacyWButtonPriorityColors == null || _legacyWButtonPriorityColors.Count == 0)
{
return false;
}
bool changed = false;
foreach (WButtonPriorityColor legacy in _legacyWButtonPriorityColors)
{
#pragma warning restore CS0618 // Type or member is obsolete
if (legacy == null)
{
continue;
}
string normalizedKey = NormalizeColorKey(legacy.Priority);
WButtonCustomColor color = new()
{
ButtonColor = legacy.ButtonColor,
TextColor =
legacy.TextColor.maxColorComponent <= 0f
? WButtonColorUtility.GetReadableTextColor(legacy.ButtonColor)
: legacy.TextColor,
};
_wbuttonCustomColors[normalizedKey] = color;
changed = true;
}
_legacyWButtonPriorityColors.Clear();
return changed;
}
private bool EnsureWEnumToggleButtonsCustomColorDefaults()
{
if (_wenumToggleButtonsCustomColors == null)
{
_wenumToggleButtonsCustomColors = new WEnumToggleButtonsCustomColorDictionary();
}
bool changed = false;
if (!_wenumToggleButtonsCustomColors.ContainsKey(DefaultWEnumToggleButtonsColorKey))
{
bool proSkin = EditorGUIUtility.isProSkin;
WEnumToggleButtonsCustomColor defaultColor = new()
{
SelectedBackgroundColor = proSkin
? DefaultDarkThemeEnumSelectedColor
: DefaultLightThemeEnumSelectedColor,
SelectedTextColor = proSkin
? DefaultDarkThemeEnumSelectedTextColor
: DefaultLightThemeEnumSelectedTextColor,
InactiveBackgroundColor = proSkin
? DefaultDarkThemeEnumInactiveColor
: DefaultLightThemeEnumInactiveColor,
InactiveTextColor = proSkin
? DefaultDarkThemeEnumInactiveTextColor
: DefaultLightThemeEnumInactiveTextColor,
};
defaultColor.EnsureReadableText();
_wenumToggleButtonsCustomColors[DefaultWEnumToggleButtonsColorKey] = defaultColor;
changed = true;
}
changed |= EnsureWEnumToggleButtonsThemeEntry(
WEnumToggleButtonsLightThemeColorKey,
DefaultLightThemeEnumSelectedColor,
DefaultLightThemeEnumSelectedTextColor,
DefaultLightThemeEnumInactiveColor,
DefaultLightThemeEnumInactiveTextColor
);
changed |= EnsureWEnumToggleButtonsThemeEntry(
WEnumToggleButtonsDarkThemeColorKey,
DefaultDarkThemeEnumSelectedColor,
DefaultDarkThemeEnumSelectedTextColor,
DefaultDarkThemeEnumInactiveColor,
DefaultDarkThemeEnumInactiveTextColor
);
foreach (
KeyValuePair<
string,
WEnumToggleButtonsCustomColor
> entry in _wenumToggleButtonsCustomColors
)
{
WEnumToggleButtonsCustomColor value = entry.Value;
if (value == null)
{
value = new WEnumToggleButtonsCustomColor();
_wenumToggleButtonsCustomColors[entry.Key] = value;
changed = true;
}
Color previousSelectedText = value.SelectedTextColor;
Color previousInactiveText = value.InactiveTextColor;
value.EnsureReadableText();
if (!ColorsApproximatelyEqual(value.SelectedTextColor, previousSelectedText))
{
changed = true;
}
if (!ColorsApproximatelyEqual(value.InactiveTextColor, previousInactiveText))
{
changed = true;
}
}
return changed;
}
private bool EnsureWEnumToggleButtonsThemeEntry(
string key,
Color selectedBackground,
Color selectedTextDefault,
Color inactiveBackground,
Color inactiveTextDefault
)
{
if (
_wenumToggleButtonsCustomColors.TryGetValue(
key,
out WEnumToggleButtonsCustomColor existing
)
&& existing != null
)
{
existing.EnsureReadableText();
return false;
}
Color resolvedSelectedText =
selectedTextDefault.maxColorComponent <= 0f
? WButtonColorUtility.GetReadableTextColor(selectedBackground)
: selectedTextDefault;
Color resolvedInactiveText =
inactiveTextDefault.maxColorComponent <= 0f
? WButtonColorUtility.GetReadableTextColor(inactiveBackground)
: inactiveTextDefault;
WEnumToggleButtonsCustomColor themeColor = new()
{
SelectedBackgroundColor = selectedBackground,
SelectedTextColor = resolvedSelectedText,
InactiveBackgroundColor = inactiveBackground,
InactiveTextColor = resolvedInactiveText,
};
themeColor.EnsureReadableText();
_wenumToggleButtonsCustomColors[key] = themeColor;
return true;
}
private static bool ShouldSkipAutoSuggest(HashSet skipSet, string key)
{
if (skipSet == null || string.IsNullOrWhiteSpace(key))
{
return false;
}
return skipSet.Contains(key.Trim());
}
private static bool IsReservedWEnumToggleButtonsColorKey(string key)
{
if (string.IsNullOrWhiteSpace(key))
{
return true;
}
return string.Equals(
key,
DefaultWEnumToggleButtonsColorKey,
StringComparison.OrdinalIgnoreCase
)
|| string.Equals(
key,
WEnumToggleButtonsLightThemeColorKey,
StringComparison.OrdinalIgnoreCase
)
|| string.Equals(
key,
WEnumToggleButtonsDarkThemeColorKey,
StringComparison.OrdinalIgnoreCase
);
}
private bool ContainsColorKey(string colorKey)
{
if (string.IsNullOrWhiteSpace(colorKey))
{
return true;
}
if (IsReservedWButtonColorKey(colorKey))
{
return true;
}
if (_wbuttonCustomColors == null || _wbuttonCustomColors.Count == 0)
{
return false;
}
string normalized = string.IsNullOrWhiteSpace(colorKey)
? DefaultWButtonColorKey
: colorKey.Trim();
foreach (string existingKey in _wbuttonCustomColors.Keys)
{
if (string.Equals(existingKey, normalized, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
private bool ContainsWEnumToggleButtonsColorKey(string colorKey)
{
if (string.IsNullOrWhiteSpace(colorKey))
{
return true;
}
if (IsReservedWEnumToggleButtonsColorKey(colorKey))
{
return true;
}
if (
_wenumToggleButtonsCustomColors == null
|| _wenumToggleButtonsCustomColors.Count == 0
)
{
return false;
}
string normalized = colorKey.Trim();
foreach (string existingKey in _wenumToggleButtonsCustomColors.Keys)
{
if (string.Equals(existingKey, normalized, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
private string NormalizeColorKey(string colorKey)
{
if (string.IsNullOrWhiteSpace(colorKey))
{
return DefaultWButtonColorKey;
}
if (IsReservedWButtonColorKey(colorKey))
{
if (
string.Equals(
colorKey,
WButtonLightThemeColorKey,
StringComparison.OrdinalIgnoreCase
)
)
{
return WButtonLightThemeColorKey;
}
if (
string.Equals(
colorKey,
WButtonDarkThemeColorKey,
StringComparison.OrdinalIgnoreCase
)
)
{
return WButtonDarkThemeColorKey;
}
if (
string.Equals(
colorKey,
WButtonLegacyColorKey,
StringComparison.OrdinalIgnoreCase
)
)
{
return WButtonLegacyColorKey;
}
return DefaultWButtonColorKey;
}
string normalized = colorKey.Trim();
if (_wbuttonCustomColors != null)
{
foreach (string existingKey in _wbuttonCustomColors.Keys)
{
if (string.Equals(existingKey, normalized, StringComparison.OrdinalIgnoreCase))
{
return existingKey;
}
}
}
return normalized;
}
private void RegisterPaletteManualEditInternal(string propertyPath, string key)
{
string trimmedKey = key?.Trim();
if (string.IsNullOrWhiteSpace(trimmedKey))
{
return;
}
if (
string.Equals(
propertyPath,
SerializedPropertyNames.WButtonCustomColors,
StringComparison.Ordinal
)
)
{
_wbuttonCustomColorSkipAutoSuggest ??= new HashSet(
StringComparer.OrdinalIgnoreCase
);
_wbuttonCustomColorSkipAutoSuggest.Add(trimmedKey);
}
}
private string NormalizeWEnumToggleButtonsColorKey(string colorKey)
{
if (string.IsNullOrWhiteSpace(colorKey))
{
return DefaultWEnumToggleButtonsColorKey;
}
if (IsReservedWEnumToggleButtonsColorKey(colorKey))
{
if (
string.Equals(
colorKey,
WEnumToggleButtonsLightThemeColorKey,
StringComparison.OrdinalIgnoreCase
)
)
{
return WEnumToggleButtonsLightThemeColorKey;
}
if (
string.Equals(
colorKey,
WEnumToggleButtonsDarkThemeColorKey,
StringComparison.OrdinalIgnoreCase
)
)
{
return WEnumToggleButtonsDarkThemeColorKey;
}
return DefaultWEnumToggleButtonsColorKey;
}
if (_wenumToggleButtonsCustomColors != null)
{
foreach (string existingKey in _wenumToggleButtonsCustomColors.Keys)
{
if (string.Equals(existingKey, colorKey, StringComparison.OrdinalIgnoreCase))
{
return existingKey;
}
}
}
return colorKey.Trim();
}
private WButtonPaletteEntry GetWButtonPaletteEntry(string colorKey)
{
EnsureWButtonCustomColorDefaults();
string normalized = NormalizeColorKey(colorKey);
if (
string.Equals(
normalized,
DefaultWButtonColorKey,
StringComparison.OrdinalIgnoreCase
)
)
{
return GetThemeAwareDefaultWButtonPalette();
}
if (
string.Equals(
normalized,
WButtonLightThemeColorKey,
StringComparison.OrdinalIgnoreCase
)
)
{
return GetWButtonThemePaletteEntry(
WButtonLightThemeColorKey,
DefaultLightThemeButtonColor,
Color.black
);
}
if (
string.Equals(
normalized,
WButtonDarkThemeColorKey,
StringComparison.OrdinalIgnoreCase
)
)
{
return GetWButtonThemePaletteEntry(
WButtonDarkThemeColorKey,
DefaultDarkThemeButtonColor,
Color.white
);
}
if (
string.Equals(normalized, WButtonLegacyColorKey, StringComparison.OrdinalIgnoreCase)
)
{
if (
_wbuttonCustomColors != null
&& _wbuttonCustomColors.TryGetValue(
WButtonLegacyColorKey,
out WButtonCustomColor legacy
)
&& legacy != null
)
{
legacy.EnsureReadableText();
return new WButtonPaletteEntry(legacy.ButtonColor, legacy.TextColor);
}
Color fallbackButton = DefaultColorKeyButtonColor;
Color fallbackText = WButtonColorUtility.GetReadableTextColor(fallbackButton);
return new WButtonPaletteEntry(fallbackButton, fallbackText);
}
if (_wbuttonCustomColors is not { Count: > 0 })
{
return GetThemeAwareDefaultWButtonPalette();
}
if (
_wbuttonCustomColors.TryGetValue(normalized, out WButtonCustomColor directValue)
&& directValue != null
)
{
directValue.EnsureReadableText();
return new WButtonPaletteEntry(directValue.ButtonColor, directValue.TextColor);
}
foreach (KeyValuePair entry in _wbuttonCustomColors)
{
if (
string.Equals(entry.Key, normalized, StringComparison.OrdinalIgnoreCase)
&& entry.Value != null
)
{
entry.Value.EnsureReadableText();
return new WButtonPaletteEntry(entry.Value.ButtonColor, entry.Value.TextColor);
}
}
return GetThemeAwareDefaultWButtonPalette();
}
private WButtonPaletteEntry GetThemeAwareDefaultWButtonPalette()
{
string themeKey = EditorGUIUtility.isProSkin
? WButtonDarkThemeColorKey
: WButtonLightThemeColorKey;
Color fallbackButton = EditorGUIUtility.isProSkin
? DefaultDarkThemeButtonColor
: DefaultLightThemeButtonColor;
Color fallbackText = EditorGUIUtility.isProSkin ? Color.white : Color.black;
return GetWButtonThemePaletteEntry(themeKey, fallbackButton, fallbackText);
}
private WButtonPaletteEntry GetWButtonThemePaletteEntry(
string key,
Color buttonColor,
Color defaultTextColor
)
{
EnsureWButtonCustomColorDefaults();
if (
_wbuttonCustomColors != null
&& _wbuttonCustomColors.TryGetValue(key, out WButtonCustomColor value)
&& value != null
)
{
value.EnsureReadableText();
return new WButtonPaletteEntry(value.ButtonColor, value.TextColor);
}
Color textColor =
defaultTextColor.maxColorComponent <= 0f
? WButtonColorUtility.GetReadableTextColor(buttonColor)
: defaultTextColor;
return new WButtonPaletteEntry(buttonColor, textColor);
}
private WEnumToggleButtonsPaletteEntry GetWEnumToggleButtonsPaletteEntry(string colorKey)
{
EnsureWEnumToggleButtonsCustomColorDefaults();
string normalized = NormalizeWEnumToggleButtonsColorKey(colorKey);
if (
string.Equals(
normalized,
DefaultWEnumToggleButtonsColorKey,
StringComparison.OrdinalIgnoreCase
)
)
{
return GetThemeAwareDefaultWEnumToggleButtonsPalette();
}
if (
string.Equals(
normalized,
WEnumToggleButtonsLightThemeColorKey,
StringComparison.OrdinalIgnoreCase
)
)
{
return GetWEnumToggleButtonsThemePaletteEntry(
WEnumToggleButtonsLightThemeColorKey,
DefaultLightThemeEnumSelectedColor,
DefaultLightThemeEnumSelectedTextColor,
DefaultLightThemeEnumInactiveColor,
DefaultLightThemeEnumInactiveTextColor
);
}
if (
string.Equals(
normalized,
WEnumToggleButtonsDarkThemeColorKey,
StringComparison.OrdinalIgnoreCase
)
)
{
return GetWEnumToggleButtonsThemePaletteEntry(
WEnumToggleButtonsDarkThemeColorKey,
DefaultDarkThemeEnumSelectedColor,
DefaultDarkThemeEnumSelectedTextColor,
DefaultDarkThemeEnumInactiveColor,
DefaultDarkThemeEnumInactiveTextColor
);
}
if (
_wenumToggleButtonsCustomColors != null
&& _wenumToggleButtonsCustomColors.TryGetValue(
normalized,
out WEnumToggleButtonsCustomColor directValue
)
&& directValue != null
)
{
directValue.EnsureReadableText();
return new WEnumToggleButtonsPaletteEntry(
directValue.SelectedBackgroundColor,
directValue.SelectedTextColor,
directValue.InactiveBackgroundColor,
directValue.InactiveTextColor
);
}
if (_wenumToggleButtonsCustomColors != null)
{
foreach (
KeyValuePair<
string,
WEnumToggleButtonsCustomColor
> entry in _wenumToggleButtonsCustomColors
)
{
if (
string.Equals(entry.Key, normalized, StringComparison.OrdinalIgnoreCase)
&& entry.Value != null
)
{
entry.Value.EnsureReadableText();
return new WEnumToggleButtonsPaletteEntry(
entry.Value.SelectedBackgroundColor,
entry.Value.SelectedTextColor,
entry.Value.InactiveBackgroundColor,
entry.Value.InactiveTextColor
);
}
}
}
return GetThemeAwareDefaultWEnumToggleButtonsPalette();
}
private WEnumToggleButtonsPaletteEntry GetThemeAwareDefaultWEnumToggleButtonsPalette()
{
bool proSkin = EditorGUIUtility.isProSkin;
return proSkin
? GetWEnumToggleButtonsThemePaletteEntry(
WEnumToggleButtonsDarkThemeColorKey,
DefaultDarkThemeEnumSelectedColor,
DefaultDarkThemeEnumSelectedTextColor,
DefaultDarkThemeEnumInactiveColor,
DefaultDarkThemeEnumInactiveTextColor
)
: GetWEnumToggleButtonsThemePaletteEntry(
WEnumToggleButtonsLightThemeColorKey,
DefaultLightThemeEnumSelectedColor,
DefaultLightThemeEnumSelectedTextColor,
DefaultLightThemeEnumInactiveColor,
DefaultLightThemeEnumInactiveTextColor
);
}
private WEnumToggleButtonsPaletteEntry GetWEnumToggleButtonsThemePaletteEntry(
string key,
Color selectedBackground,
Color selectedTextDefault,
Color inactiveBackground,
Color inactiveTextDefault
)
{
EnsureWEnumToggleButtonsCustomColorDefaults();
if (
_wenumToggleButtonsCustomColors != null
&& _wenumToggleButtonsCustomColors.TryGetValue(
key,
out WEnumToggleButtonsCustomColor value
)
&& value != null
)
{
value.EnsureReadableText();
return new WEnumToggleButtonsPaletteEntry(
value.SelectedBackgroundColor,
value.SelectedTextColor,
value.InactiveBackgroundColor,
value.InactiveTextColor
);
}
Color resolvedSelectedText =
selectedTextDefault.maxColorComponent <= 0f
? WButtonColorUtility.GetReadableTextColor(selectedBackground)
: selectedTextDefault;
Color resolvedInactiveText =
inactiveTextDefault.maxColorComponent <= 0f
? WButtonColorUtility.GetReadableTextColor(inactiveBackground)
: inactiveTextDefault;
return new WEnumToggleButtonsPaletteEntry(
selectedBackground,
resolvedSelectedText,
inactiveBackground,
resolvedInactiveText
);
}
private static bool DrawSerializableTypeIgnorePatterns(
SerializedProperty patternsProperty,
SerializedProperty initializationFlagProperty
)
{
if (patternsProperty == null)
{
return false;
}
SerializedObject owner = patternsProperty.serializedObject;
bool mutated = false;
EditorGUI.BeginChangeCheck();
GUIContent label = new(
"SerializableType Ignore Regexes",
"Regex patterns evaluated against type names (simple and fully-qualified) to exclude them from the SerializableType picker."
);
patternsProperty.isExpanded = EditorGUILayout.Foldout(
patternsProperty.isExpanded,
label,
true
);
if (!patternsProperty.isExpanded)
{
return false;
}
EditorGUI.indentLevel++;
for (int index = 0; index < patternsProperty.arraySize; index++)
{
SerializedProperty element = patternsProperty.GetArrayElementAtIndex(index);
SerializedProperty patternProperty = element.FindPropertyRelative(
SerializedPropertyNames.SerializableTypePattern
);
using (new EditorGUILayout.HorizontalScope())
{
EditorGUILayout.PropertyField(patternProperty, GUIContent.none);
string patternValue = patternProperty.stringValue ?? string.Empty;
SerializableTypeCatalog.PatternStats stats =
SerializableTypeCatalog.GetPatternStats(patternValue);
string countLabel = stats.IsValid
? $"{stats.MatchCount} match{(stats.MatchCount == 1 ? string.Empty : "es")}"
: "Invalid";
GUIContent countContent = stats.IsValid
? new GUIContent(countLabel)
: new GUIContent(
"Invalid",
stats.ErrorMessage ?? "Pattern is not a valid regular expression."
);
GUILayoutOption width = GUILayout.Width(110f);
EditorGUILayout.LabelField(countContent, GUILayout.ExpandWidth(false), width);
if (GUILayout.Button("Remove", GUILayout.Width(70f)))
{
patternsProperty.DeleteArrayElementAtIndex(index);
if (initializationFlagProperty != null)
{
initializationFlagProperty.boolValue = true;
}
break;
}
}
}
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("Add Regex", GUILayout.Width(110f)))
{
SerializedProperty patternProperty = AppendSerializableTypePatternElement(
patternsProperty
);
if (patternProperty != null)
{
patternProperty.stringValue = string.Empty;
mutated = true;
}
if (initializationFlagProperty != null)
{
initializationFlagProperty.boolValue = true;
}
ApplySerializableTypePatternChanges(owner);
}
if (GUILayout.Button("Reset To Defaults", GUILayout.Width(150f)))
{
patternsProperty.ClearArray();
IReadOnlyList defaults =
SerializableTypeCatalog.GetDefaultIgnorePatterns();
for (int index = 0; index < defaults.Count; index++)
{
SerializedProperty patternProperty = AppendSerializableTypePatternElement(
patternsProperty
);
if (patternProperty != null)
{
patternProperty.stringValue = defaults[index];
}
}
if (initializationFlagProperty != null)
{
initializationFlagProperty.boolValue = true;
}
mutated = true;
ApplySerializableTypePatternChanges(owner);
}
GUILayout.FlexibleSpace();
}
EditorGUI.indentLevel--;
return EditorGUI.EndChangeCheck() || mutated;
}
private static SerializedProperty AppendSerializableTypePatternElement(
SerializedProperty patternsProperty
)
{
if (patternsProperty == null)
{
return null;
}
int newIndex = Mathf.Max(0, patternsProperty.arraySize);
patternsProperty.arraySize = newIndex + 1;
SerializedProperty element = patternsProperty.GetArrayElementAtIndex(newIndex);
if (element == null)
{
return null;
}
return element.FindPropertyRelative(SerializedPropertyNames.SerializableTypePattern);
}
private static void ApplySerializableTypePatternChanges(SerializedObject owner)
{
if (owner == null)
{
return;
}
owner.ApplyModifiedPropertiesWithoutUndo();
owner.Update();
}
private static bool DrawIntSliderField(
GUIContent content,
int currentValue,
int min,
int max,
Action setter
)
{
EditorGUI.BeginChangeCheck();
int newValue = EditorGUILayout.IntSlider(content, currentValue, min, max);
if (EditorGUI.EndChangeCheck())
{
setter(Mathf.Clamp(newValue, min, max));
return true;
}
return false;
}
private static bool DrawIntField(GUIContent content, int currentValue, Action setter)
{
EditorGUI.BeginChangeCheck();
int newValue = EditorGUILayout.IntField(content, currentValue);
if (EditorGUI.EndChangeCheck())
{
setter(newValue);
return true;
}
return false;
}
private static bool DrawFloatField(
GUIContent content,
float currentValue,
Action setter
)
{
EditorGUI.BeginChangeCheck();
float newValue = EditorGUILayout.FloatField(content, currentValue);
if (EditorGUI.EndChangeCheck())
{
setter(newValue);
return true;
}
return false;
}
private static bool DrawFloatSliderField(
GUIContent content,
float currentValue,
float min,
float max,
Action setter,
bool enabled
)
{
using (new EditorGUI.DisabledScope(!enabled))
{
EditorGUI.BeginChangeCheck();
float newValue = EditorGUILayout.Slider(content, currentValue, min, max);
bool changed = EditorGUI.EndChangeCheck();
if (changed && enabled)
{
setter(Mathf.Clamp(newValue, min, max));
return true;
}
}
return false;
}
private static bool DrawToggleField(
GUIContent content,
bool currentValue,
Action setter
)
{
EditorGUI.BeginChangeCheck();
bool newValue = EditorGUILayout.Toggle(content, currentValue);
if (EditorGUI.EndChangeCheck())
{
setter(newValue);
return true;
}
return false;
}
private static bool DrawFailedTestsOutputDirectoryField(UnityHelpersSettings settings)
{
bool changed = false;
string currentDirectory = settings._failedTestsOutputDirectory ?? string.Empty;
EditorGUILayout.BeginHorizontal();
// Show the current directory as a read-only label (or placeholder)
string displayText = string.IsNullOrEmpty(currentDirectory)
? "(Project Root)"
: currentDirectory;
EditorGUILayout.PrefixLabel(FailedTestsOutputDirectoryContent);
using (new EditorGUI.DisabledScope(true))
{
EditorGUILayout.TextField(displayText);
}
// Browse button - opens Unity's folder panel
if (GUILayout.Button(FailedTestsOutputDirectoryBrowseContent, GUILayout.Width(70f)))
{
string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
string startDirectory = string.IsNullOrEmpty(currentDirectory)
? projectRoot
: Path.GetFullPath(Path.Combine(projectRoot, currentDirectory));
if (!Directory.Exists(startDirectory))
{
startDirectory = projectRoot;
}
string selectedPath = EditorUtility.OpenFolderPanel(
"Select Failed Tests Output Directory",
startDirectory,
string.Empty
);
if (!string.IsNullOrEmpty(selectedPath))
{
selectedPath = selectedPath.Replace('\\', '/');
projectRoot = projectRoot.Replace('\\', '/');
if (!projectRoot.EndsWith("/"))
{
projectRoot += "/";
}
if (selectedPath.StartsWith(projectRoot, StringComparison.OrdinalIgnoreCase))
{
string relativePath = selectedPath
.Substring(projectRoot.Length)
.TrimEnd('/');
settings._failedTestsOutputDirectory = relativePath;
changed = true;
}
else if (
string.Equals(
selectedPath.TrimEnd('/'),
projectRoot.TrimEnd('/'),
StringComparison.OrdinalIgnoreCase
)
)
{
settings._failedTestsOutputDirectory = DefaultFailedTestsOutputDirectory;
changed = true;
}
else
{
Debug.LogWarning(
"[UnityHelpersSettings] Selected directory must be within the project root."
);
}
}
}
// Clear button (only shown when a directory is set)
using (new EditorGUI.DisabledScope(string.IsNullOrEmpty(currentDirectory)))
{
if (GUILayout.Button(FailedTestsOutputDirectoryClearContent, GUILayout.Width(24f)))
{
settings._failedTestsOutputDirectory = DefaultFailedTestsOutputDirectory;
changed = true;
}
}
EditorGUILayout.EndHorizontal();
// Show validation warning if path is set but invalid
if (!string.IsNullOrEmpty(currentDirectory))
{
string validated = GetFailedTestsOutputDirectory();
if (string.IsNullOrEmpty(validated))
{
EditorGUILayout.HelpBox(
$"The configured directory \"{currentDirectory}\" does not exist or is invalid. Files will be saved to the project root instead.",
MessageType.Warning
);
}
}
return changed;
}
private static bool DrawEnumPopupField(
GUIContent content,
TEnum currentValue,
Action setter
)
where TEnum : Enum
{
EditorGUI.BeginChangeCheck();
TEnum newValue = (TEnum)EditorGUILayout.EnumPopup(content, currentValue);
if (EditorGUI.EndChangeCheck())
{
setter(newValue);
return true;
}
return false;
}
private static bool DrawColorField(
GUIContent content,
Color currentValue,
Action setter
)
{
EditorGUI.BeginChangeCheck();
Color newValue = EditorGUILayout.ColorField(content, currentValue);
if (EditorGUI.EndChangeCheck())
{
setter(newValue);
return true;
}
return false;
}
private static void DrawWaitInstructionBufferButtons(UnityHelpersSettings settings)
{
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button(WaitInstructionBufferApplyNowButtonContent))
{
settings.ApplyWaitInstructionBufferDefaultsToAsset(applyToRuntime: true);
}
settings.EnsureWaitInstructionBufferDefaultsInitialized();
bool isRuntimeInSync = settings.AreWaitInstructionDefaultsInSyncWithRuntime();
using (new EditorGUI.DisabledScope(isRuntimeInSync))
{
if (GUILayout.Button(WaitInstructionBufferCaptureCurrentButtonContent))
{
settings.CaptureWaitInstructionDefaultsFromRuntime();
}
}
}
}
private readonly struct LabelWidthScope : IDisposable
{
private readonly float _previousWidth;
internal LabelWidthScope(float targetWidth)
{
_previousWidth = EditorGUIUtility.labelWidth;
float currentViewWidth = EditorGUIUtility.currentViewWidth;
float maxLabelWidth = Mathf.Max(0f, currentViewWidth - SettingsMinFieldWidth);
float appliedWidth = Mathf.Clamp(targetWidth, 0f, maxLabelWidth);
EditorGUIUtility.labelWidth = appliedWidth;
}
public void Dispose()
{
EditorGUIUtility.labelWidth = _previousWidth;
}
}
///
/// Returns a cached SerializedObject for the settings instance, creating one if needed.
/// Caching the SerializedObject preserves property expansion states (isExpanded)
/// across frames, preventing foldouts from unexpectedly re-expanding.
///
internal static SerializedObject GetOrCreateCachedSerializedObject(
UnityHelpersSettings settings
)
{
if (settings == null)
{
_cachedSettingsSerializedObject = null;
return null;
}
// Check if we need to create a new SerializedObject:
// - First time (cache is null)
// - Target object changed (shouldn't happen for singleton, but defensive)
// - SerializedObject was disposed or invalidated
if (
_cachedSettingsSerializedObject == null
|| _cachedSettingsSerializedObject.targetObject == null
|| _cachedSettingsSerializedObject.targetObject != settings
)
{
_cachedSettingsSerializedObject?.Dispose();
_cachedSettingsSerializedObject = new SerializedObject(settings);
}
return _cachedSettingsSerializedObject;
}
///
/// Clears the cached SerializedObject for testing purposes.
///
internal static void ClearCachedSerializedObjectForTests()
{
_cachedSettingsSerializedObject?.Dispose();
_cachedSettingsSerializedObject = null;
}
[SettingsProvider]
private static SettingsProvider CreateSettingsProvider()
{
return new SettingsProvider(
"Project/Wallstop Studios/Unity Helpers",
SettingsScope.Project
)
{
label = "Unity Helpers",
guiHandler = _ =>
{
UnityHelpersSettings settings = instance;
settings.EnsureWaitInstructionBufferDefaultsInitialized();
SerializedObject serializedSettings = GetOrCreateCachedSerializedObject(
settings
);
serializedSettings.UpdateIfRequiredOrScript();
bool dataChanged = false;
bool palettePropertyChanged = false;
using (new WGroupHeaderVisualUtility.SettingsContextScope())
using (
PooledResource> waitInstructionPropertiesLease =
SetBuffers
.GetHashSetPool(StringComparer.Ordinal)
.Get(out HashSet waitInstructionPropertiesDrawn)
)
using (new LabelWidthScope(SettingsLabelWidth))
{
SerializedProperty scriptProperty = serializedSettings.FindProperty(
"m_Script"
);
SerializedProperty patternsInitializedProperty =
serializedSettings.FindProperty(
nameof(_serializableTypePatternsInitialized)
);
if (scriptProperty != null)
{
using (new EditorGUI.DisabledScope(true))
{
EditorGUILayout.PropertyField(scriptProperty, true);
}
EditorGUILayout.Space();
}
bool TryDrawSettingsGroupProperty(
SerializedObject owner,
SerializedProperty property
)
{
if (property == null)
{
return false;
}
if (
string.Equals(
property.propertyPath,
nameof(_serializableTypeIgnorePatterns),
StringComparison.Ordinal
)
)
{
bool changed = DrawSerializableTypeIgnorePatterns(
property,
patternsInitializedProperty
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_stringInListPageSize),
StringComparison.Ordinal
)
)
{
bool changed = DrawIntSliderField(
StringInListPageSizeContent,
settings._stringInListPageSize,
MinPageSize,
MaxPageSize,
value => settings._stringInListPageSize = value
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_serializableSetPageSize),
StringComparison.Ordinal
)
)
{
bool changed = DrawIntSliderField(
SerializableSetPageSizeContent,
settings._serializableSetPageSize,
MinPageSize,
MaxPageSize,
value => settings._serializableSetPageSize = value
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_serializableSetStartCollapsed),
StringComparison.Ordinal
)
)
{
bool changed = DrawToggleField(
SerializableSetStartCollapsedContent,
settings._serializableSetStartCollapsed,
value => settings._serializableSetStartCollapsed = value
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_serializableDictionaryPageSize),
StringComparison.Ordinal
)
)
{
bool changed = DrawIntSliderField(
SerializableDictionaryPageSizeContent,
settings._serializableDictionaryPageSize,
MinPageSize,
MaxSerializableDictionaryPageSize,
value => settings._serializableDictionaryPageSize = value
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_serializableDictionaryStartCollapsed),
StringComparison.Ordinal
)
)
{
bool changed = DrawToggleField(
SerializableDictionaryStartCollapsedContent,
settings._serializableDictionaryStartCollapsed,
value => settings._serializableDictionaryStartCollapsed = value
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_enumToggleButtonsPageSize),
StringComparison.Ordinal
)
)
{
bool changed = DrawIntSliderField(
EnumToggleButtonsPageSizeContent,
settings._enumToggleButtonsPageSize,
MinPageSize,
MaxPageSize,
value => settings._enumToggleButtonsPageSize = value
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_wbuttonPageSize),
StringComparison.Ordinal
)
)
{
bool changed = DrawIntSliderField(
WButtonPageSizeContent,
settings._wbuttonPageSize,
MinPageSize,
MaxPageSize,
value => settings._wbuttonPageSize = value
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_wbuttonHistorySize),
StringComparison.Ordinal
)
)
{
bool changed = DrawIntSliderField(
WButtonHistorySizeContent,
settings._wbuttonHistorySize,
MinWButtonHistorySize,
MaxWButtonHistorySize,
value => settings._wbuttonHistorySize = value
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_wbuttonActionsPlacement),
StringComparison.Ordinal
)
)
{
bool changed = DrawEnumPopupField(
WButtonPlacementContent,
settings._wbuttonActionsPlacement,
value => settings._wbuttonActionsPlacement = value
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_wbuttonFoldoutBehavior),
StringComparison.Ordinal
)
)
{
bool changed = DrawEnumPopupField(
WButtonFoldoutBehaviorContent,
settings._wbuttonFoldoutBehavior,
value => settings._wbuttonFoldoutBehavior = value
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_inlineEditorFoldoutBehavior),
StringComparison.Ordinal
)
)
{
bool changed = DrawEnumPopupField(
InlineEditorFoldoutBehaviorContent,
settings._inlineEditorFoldoutBehavior,
value => settings._inlineEditorFoldoutBehavior = value
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_inlineEditorFoldoutTweenEnabled),
StringComparison.Ordinal
)
)
{
bool changed = DrawToggleField(
InlineEditorFoldoutTweenEnabledContent,
settings._inlineEditorFoldoutTweenEnabled,
value => settings._inlineEditorFoldoutTweenEnabled = value
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_inlineEditorFoldoutSpeed),
StringComparison.Ordinal
)
)
{
if (!settings._inlineEditorFoldoutTweenEnabled)
{
return true;
}
bool changed = DrawFloatSliderField(
InlineEditorFoldoutSpeedContent,
settings._inlineEditorFoldoutSpeed,
MinFoldoutSpeed,
MaxFoldoutSpeed,
value => settings._inlineEditorFoldoutSpeed = value,
true
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_wbuttonFoldoutTweenEnabled),
StringComparison.Ordinal
)
)
{
bool changed = DrawToggleField(
WButtonFoldoutTweenEnabledContent,
settings._wbuttonFoldoutTweenEnabled,
value => settings._wbuttonFoldoutTweenEnabled = value
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_wbuttonFoldoutSpeed),
StringComparison.Ordinal
)
)
{
if (!settings._wbuttonFoldoutTweenEnabled)
{
return true;
}
bool changed = DrawFloatSliderField(
WButtonFoldoutSpeedContent,
settings._wbuttonFoldoutSpeed,
MinFoldoutSpeed,
MaxFoldoutSpeed,
value => settings._wbuttonFoldoutSpeed = value,
true
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_wbuttonCancelButtonColor),
StringComparison.Ordinal
)
)
{
bool changed = DrawColorField(
WButtonCancelButtonColorContent,
settings._wbuttonCancelButtonColor,
value => settings._wbuttonCancelButtonColor = value
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_wbuttonCancelButtonTextColor),
StringComparison.Ordinal
)
)
{
bool changed = DrawColorField(
WButtonCancelButtonTextColorContent,
settings._wbuttonCancelButtonTextColor,
value => settings._wbuttonCancelButtonTextColor = value
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_wbuttonClearHistoryButtonColor),
StringComparison.Ordinal
)
)
{
bool changed = DrawColorField(
WButtonClearHistoryButtonColorContent,
settings._wbuttonClearHistoryButtonColor,
value => settings._wbuttonClearHistoryButtonColor = value
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_wbuttonClearHistoryButtonTextColor),
StringComparison.Ordinal
)
)
{
bool changed = DrawColorField(
WButtonClearHistoryButtonTextColorContent,
settings._wbuttonClearHistoryButtonTextColor,
value => settings._wbuttonClearHistoryButtonTextColor = value
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_wbuttonCustomColors),
StringComparison.Ordinal
)
)
{
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(
property,
WButtonCustomColorsContent,
true
);
if (EditorGUI.EndChangeCheck())
{
dataChanged = true;
palettePropertyChanged = true;
}
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_wenumToggleButtonsCustomColors),
StringComparison.Ordinal
)
)
{
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(
property,
WEnumToggleButtonsCustomColorsContent,
true
);
if (EditorGUI.EndChangeCheck())
{
dataChanged = true;
palettePropertyChanged = true;
}
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_waitInstructionBufferApplyOnLoad),
StringComparison.Ordinal
)
)
{
if (!waitInstructionPropertiesDrawn.Add(property.propertyPath))
{
return true;
}
using (new EditorGUI.IndentLevelScope())
{
EditorGUILayout.HelpBox(
WaitInstructionBufferDefaultsHelpText,
MessageType.Info
);
bool changed = DrawToggleField(
WaitInstructionBufferApplyOnLoadContent,
settings._waitInstructionBufferApplyOnLoad,
value => settings._waitInstructionBufferApplyOnLoad = value
);
dataChanged |= changed;
}
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_waitInstructionBufferQuantizationStepSeconds),
StringComparison.Ordinal
)
)
{
if (!waitInstructionPropertiesDrawn.Add(property.propertyPath))
{
return true;
}
using (new EditorGUI.IndentLevelScope())
{
bool changed = DrawFloatField(
WaitInstructionBufferQuantizationContent,
settings._waitInstructionBufferQuantizationStepSeconds,
value =>
settings._waitInstructionBufferQuantizationStepSeconds =
Mathf.Max(0f, value)
);
dataChanged |= changed;
}
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_waitInstructionBufferMaxDistinctEntries),
StringComparison.Ordinal
)
)
{
if (!waitInstructionPropertiesDrawn.Add(property.propertyPath))
{
return true;
}
using (new EditorGUI.IndentLevelScope())
{
bool changed = DrawIntField(
WaitInstructionBufferMaxEntriesContent,
settings._waitInstructionBufferMaxDistinctEntries,
value =>
settings._waitInstructionBufferMaxDistinctEntries =
Mathf.Max(0, value)
);
dataChanged |= changed;
}
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_waitInstructionBufferUseLruEviction),
StringComparison.Ordinal
)
)
{
if (!waitInstructionPropertiesDrawn.Add(property.propertyPath))
{
return true;
}
using (new EditorGUI.IndentLevelScope())
{
bool changed = DrawToggleField(
WaitInstructionBufferUseLruContent,
settings._waitInstructionBufferUseLruEviction,
value =>
settings._waitInstructionBufferUseLruEviction = value
);
dataChanged |= changed;
DrawWaitInstructionBufferButtons(settings);
}
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_serializableDictionaryFoldoutTweenEnabled),
StringComparison.Ordinal
)
)
{
bool changed = DrawToggleField(
DictionaryFoldoutTweenEnabledContent,
settings._serializableDictionaryFoldoutTweenEnabled,
value =>
settings._serializableDictionaryFoldoutTweenEnabled = value
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_serializableDictionaryFoldoutSpeed),
StringComparison.Ordinal
)
)
{
if (!settings._serializableDictionaryFoldoutTweenEnabled)
{
return true;
}
bool changed = DrawFloatSliderField(
DictionaryFoldoutSpeedContent,
settings._serializableDictionaryFoldoutSpeed,
MinFoldoutSpeed,
MaxFoldoutSpeed,
value => settings._serializableDictionaryFoldoutSpeed = value,
true
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_serializableSortedDictionaryFoldoutTweenEnabled),
StringComparison.Ordinal
)
)
{
bool changed = DrawToggleField(
SortedDictionaryFoldoutTweenEnabledContent,
settings._serializableSortedDictionaryFoldoutTweenEnabled,
value =>
settings._serializableSortedDictionaryFoldoutTweenEnabled =
value
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_serializableSortedDictionaryFoldoutSpeed),
StringComparison.Ordinal
)
)
{
if (!settings._serializableSortedDictionaryFoldoutTweenEnabled)
{
return true;
}
bool changed = DrawFloatSliderField(
SortedDictionaryFoldoutSpeedContent,
settings._serializableSortedDictionaryFoldoutSpeed,
MinFoldoutSpeed,
MaxFoldoutSpeed,
value =>
settings._serializableSortedDictionaryFoldoutSpeed = value,
true
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_serializableSetFoldoutTweenEnabled),
StringComparison.Ordinal
)
)
{
bool changed = DrawToggleField(
SetFoldoutTweenEnabledContent,
settings._serializableSetFoldoutTweenEnabled,
value => settings._serializableSetFoldoutTweenEnabled = value
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_serializableSetFoldoutSpeed),
StringComparison.Ordinal
)
)
{
if (!settings._serializableSetFoldoutTweenEnabled)
{
return true;
}
bool changed = DrawFloatSliderField(
SetFoldoutSpeedContent,
settings._serializableSetFoldoutSpeed,
MinFoldoutSpeed,
MaxFoldoutSpeed,
value => settings._serializableSetFoldoutSpeed = value,
true
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_serializableSortedSetFoldoutTweenEnabled),
StringComparison.Ordinal
)
)
{
bool changed = DrawToggleField(
SortedSetFoldoutTweenEnabledContent,
settings._serializableSortedSetFoldoutTweenEnabled,
value =>
settings._serializableSortedSetFoldoutTweenEnabled = value
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_serializableSortedSetFoldoutSpeed),
StringComparison.Ordinal
)
)
{
if (!settings._serializableSortedSetFoldoutTweenEnabled)
{
return true;
}
bool changed = DrawFloatSliderField(
SortedSetFoldoutSpeedContent,
settings._serializableSortedSetFoldoutSpeed,
MinFoldoutSpeed,
MaxFoldoutSpeed,
value => settings._serializableSortedSetFoldoutSpeed = value,
true
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_duplicateRowAnimationMode),
StringComparison.Ordinal
)
)
{
bool changed = DrawEnumPopupField(
DuplicateAnimationModeContent,
settings._duplicateRowAnimationMode,
value => settings._duplicateRowAnimationMode = value
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_duplicateRowTweenCycles),
StringComparison.Ordinal
)
)
{
if (
settings._duplicateRowAnimationMode
!= DuplicateRowAnimationMode.Tween
)
{
return true;
}
bool changed = DrawIntField(
DuplicateTweenCyclesContent,
settings._duplicateRowTweenCycles,
value => settings._duplicateRowTweenCycles = value
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_detectAssetChangeLoopWindowSeconds),
StringComparison.Ordinal
)
)
{
bool changed = DrawFloatSliderField(
DetectAssetChangeLoopWindowContent,
settings.DetectAssetChangeLoopWindowSeconds,
MinDetectAssetChangeLoopWindowSeconds,
MaxDetectAssetChangeLoopWindowSeconds,
value => settings._detectAssetChangeLoopWindowSeconds = value,
true
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_deferAssetPostprocessorCallbacks),
StringComparison.Ordinal
)
)
{
bool changed = DrawToggleField(
DeferAssetPostprocessorCallbacksContent,
settings._deferAssetPostprocessorCallbacks,
value => settings._deferAssetPostprocessorCallbacks = value
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_serializableSetDuplicateTweenEnabled),
StringComparison.Ordinal
)
)
{
bool changed = DrawToggleField(
SerializableSetDuplicateTweenEnabledContent,
settings._serializableSetDuplicateTweenEnabled,
value => settings._serializableSetDuplicateTweenEnabled = value
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_serializableSetDuplicateTweenCycles),
StringComparison.Ordinal
)
)
{
if (!settings._serializableSetDuplicateTweenEnabled)
{
return true;
}
bool changed = DrawIntField(
SerializableSetDuplicateTweenCyclesContent,
settings._serializableSetDuplicateTweenCycles,
value => settings._serializableSetDuplicateTweenCycles = value
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_wgroupAutoIncludeMode),
StringComparison.Ordinal
)
)
{
bool changed = DrawEnumPopupField(
WGroupAutoIncludeModeContent,
settings._wgroupAutoIncludeMode,
value => settings._wgroupAutoIncludeMode = value
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_wgroupAutoIncludeRowCount),
StringComparison.Ordinal
)
)
{
if (settings._wgroupAutoIncludeMode != WGroupAutoIncludeMode.Finite)
{
return true;
}
bool changed = DrawIntSliderField(
WGroupAutoIncludeCountContent,
settings._wgroupAutoIncludeRowCount,
MinWGroupAutoIncludeRowCount,
MaxWGroupAutoIncludeRowCount,
value => settings._wgroupAutoIncludeRowCount = value
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_wgroupFoldoutsStartCollapsed),
StringComparison.Ordinal
)
)
{
bool changed = DrawToggleField(
WGroupStartCollapsedContent,
settings._wgroupFoldoutsStartCollapsed,
value => settings._wgroupFoldoutsStartCollapsed = value
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_wgroupFoldoutTweenEnabled),
StringComparison.Ordinal
)
)
{
bool changed = DrawToggleField(
WGroupFoldoutTweenEnabledContent,
settings._wgroupFoldoutTweenEnabled,
value => settings._wgroupFoldoutTweenEnabled = value
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_wgroupFoldoutSpeed),
StringComparison.Ordinal
)
)
{
if (!settings._wgroupFoldoutTweenEnabled)
{
return true;
}
bool changed = DrawFloatSliderField(
WGroupFoldoutSpeedContent,
settings._wgroupFoldoutSpeed,
MinFoldoutSpeed,
MaxFoldoutSpeed,
value => settings._wgroupFoldoutSpeed = value,
true
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_poolPurgingEnabled),
StringComparison.Ordinal
)
)
{
EditorGUILayout.HelpBox(PoolPurgingHelpText, MessageType.Info);
bool changed = DrawToggleField(
PoolPurgingEnabledContent,
settings._poolPurgingEnabled,
value => settings._poolPurgingEnabled = value
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_failedTestsExporterEnabled),
StringComparison.Ordinal
)
)
{
EditorGUILayout.HelpBox(
FailedTestsExporterHelpText,
MessageType.Info
);
bool changed = DrawToggleField(
FailedTestsExporterEnabledContent,
settings._failedTestsExporterEnabled,
value =>
{
settings._failedTestsExporterEnabled = value;
FailedTestsExporter.Reinitialize();
}
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_failedTestsOutputDirectory),
StringComparison.Ordinal
)
)
{
if (!settings._failedTestsExporterEnabled)
{
return true;
}
bool changed = DrawFailedTestsOutputDirectoryField(settings);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_poolIdleTimeoutSeconds),
StringComparison.Ordinal
)
)
{
bool changed = DrawFloatField(
PoolIdleTimeoutContent,
settings._poolIdleTimeoutSeconds,
value => settings._poolIdleTimeoutSeconds = Mathf.Max(0f, value)
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_poolMinRetainCount),
StringComparison.Ordinal
)
)
{
bool changed = DrawIntField(
PoolMinRetainCountContent,
settings._poolMinRetainCount,
value => settings._poolMinRetainCount = Mathf.Max(0, value)
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_poolWarmRetainCount),
StringComparison.Ordinal
)
)
{
bool changed = DrawIntField(
PoolWarmRetainCountContent,
settings._poolWarmRetainCount,
value => settings._poolWarmRetainCount = Mathf.Max(0, value)
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_poolMaxSize),
StringComparison.Ordinal
)
)
{
bool changed = DrawIntField(
PoolMaxSizeContent,
settings._poolMaxSize,
value => settings._poolMaxSize = Mathf.Max(0, value)
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_poolBufferMultiplier),
StringComparison.Ordinal
)
)
{
bool changed = DrawFloatField(
PoolBufferMultiplierContent,
settings._poolBufferMultiplier,
value => settings._poolBufferMultiplier = Mathf.Max(1f, value)
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_poolRollingWindowSeconds),
StringComparison.Ordinal
)
)
{
bool changed = DrawFloatField(
PoolRollingWindowContent,
settings._poolRollingWindowSeconds,
value =>
settings._poolRollingWindowSeconds = Mathf.Max(1f, value)
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_poolHysteresisSeconds),
StringComparison.Ordinal
)
)
{
bool changed = DrawFloatField(
PoolHysteresisContent,
settings._poolHysteresisSeconds,
value => settings._poolHysteresisSeconds = Mathf.Max(0f, value)
);
dataChanged |= changed;
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_poolSpikeThresholdMultiplier),
StringComparison.Ordinal
)
)
{
bool changed = DrawFloatField(
PoolSpikeThresholdContent,
settings._poolSpikeThresholdMultiplier,
value =>
settings._poolSpikeThresholdMultiplier = Mathf.Max(
1f,
value
)
);
dataChanged |= changed;
// Draw Apply Now button after the last pool purging field
EditorGUILayout.Space(4f);
if (
GUILayout.Button(
PoolApplyNowButtonContent,
GUILayout.Width(150f)
)
)
{
ApplyPoolPurgingSettingsToRuntime();
}
return true;
}
if (
string.Equals(
property.propertyPath,
nameof(_poolTypeConfigurations),
StringComparison.Ordinal
)
)
{
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(
property,
PoolTypeConfigurationsContent,
true
);
if (EditorGUI.EndChangeCheck())
{
dataChanged = true;
}
return true;
}
return false;
}
// Capture color palette state before drawing so we can detect changes
ColorKeyChangeNotifier.CaptureCurrentState(serializedSettings);
EditorGUI.BeginChangeCheck();
string scriptPropertyPath = scriptProperty?.propertyPath;
WGroupLayout layout = WGroupLayoutBuilder.Build(
serializedSettings,
scriptPropertyPath
);
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,
serializedSettings,
SettingsGroupFoldoutStates,
TryDrawSettingsGroupProperty
);
continue;
}
// Skip hidden properties - they should not be rendered
if (operation.IsHiddenInInspector)
{
continue;
}
SerializedProperty property = serializedSettings.FindProperty(
operation.PropertyPath
);
if (property == null)
{
continue;
}
string propertyPath = property.propertyPath;
if (
string.Equals(
propertyPath,
nameof(_foldoutTweenSettingsInitialized),
StringComparison.Ordinal
)
|| string.Equals(
propertyPath,
nameof(_serializableTypePatternsInitialized),
StringComparison.Ordinal
)
|| string.Equals(
propertyPath,
nameof(_legacyWButtonPriorityColors),
StringComparison.Ordinal
)
|| string.Equals(
propertyPath,
nameof(_serializableTypeIgnorePatterns),
StringComparison.Ordinal
)
)
{
continue;
}
EditorGUILayout.PropertyField(property, true);
}
}
bool guiChanged = EditorGUI.EndChangeCheck();
bool applied = serializedSettings.ApplyModifiedPropertiesWithoutUndo();
PaletteSerializationDiagnostics.ReportInspectorApplyResult(
serializedSettings,
palettePropertyChanged,
dataChanged,
guiChanged,
applied
);
// Detect and notify color key changes to trigger inspector repaints
if (palettePropertyChanged && (dataChanged || guiChanged || applied))
{
ColorKeyChangeNotifier.DetectAndNotifyChanges(serializedSettings);
ColorKeyChangeNotifier.RepaintAffectedInspectors();
}
if (!dataChanged && !guiChanged && !applied)
{
return;
}
settings.SaveSettings();
settings.ApplyWaitInstructionBufferDefaultsToAsset(
settings._waitInstructionBufferApplyOnLoad
);
serializedSettings.UpdateIfRequiredOrScript();
},
keywords = new[]
{
"StringInList",
"Pagination",
"SerializableSet",
"WButton",
"Buttons",
"WGroup",
"Groups",
"Placement",
"Foldout",
"UnityHelpers",
"Duplicate",
"SerializableType",
"Regex",
"Speed",
"Animation",
"Pool",
"Purge",
"Purging",
"Buffer",
"Memory",
"Failed Tests",
"Exporter",
"Test Runner",
},
};
}
private void EnsureWaitInstructionBufferDefaultsInitialized()
{
if (_waitInstructionBufferDefaultsInitialized)
{
return;
}
UnityHelpersBufferSettingsAsset asset = EnsureWaitInstructionBufferSettingsAsset();
if (asset == null)
{
return;
}
_waitInstructionBufferApplyOnLoad = asset.ApplyOnLoad;
_waitInstructionBufferQuantizationStepSeconds = asset.QuantizationStepSeconds;
_waitInstructionBufferMaxDistinctEntries = asset.MaxDistinctEntries;
_waitInstructionBufferUseLruEviction = asset.UseLruEviction;
_waitInstructionBufferDefaultsInitialized = true;
}
private void CaptureWaitInstructionDefaultsFromRuntime()
{
_waitInstructionBufferQuantizationStepSeconds = Mathf.Max(
0f,
Buffers.WaitInstructionQuantizationStepSeconds
);
_waitInstructionBufferMaxDistinctEntries = Mathf.Max(
0,
Buffers.WaitInstructionMaxDistinctEntries
);
_waitInstructionBufferUseLruEviction = Buffers.WaitInstructionUseLruEviction;
_waitInstructionBufferDefaultsInitialized = true;
ApplyWaitInstructionBufferDefaultsToAsset(_waitInstructionBufferApplyOnLoad);
}
private bool AreWaitInstructionDefaultsInSyncWithRuntime()
{
float runtimeQuantization = Mathf.Max(
0f,
Buffers.WaitInstructionQuantizationStepSeconds
);
float configuredQuantization = Mathf.Max(
0f,
_waitInstructionBufferQuantizationStepSeconds
);
int runtimeMaxEntries = Mathf.Max(0, Buffers.WaitInstructionMaxDistinctEntries);
int configuredMaxEntries = Mathf.Max(0, _waitInstructionBufferMaxDistinctEntries);
bool runtimeUseLru = Buffers.WaitInstructionUseLruEviction;
return Mathf.Approximately(configuredQuantization, runtimeQuantization)
&& configuredMaxEntries == runtimeMaxEntries
&& _waitInstructionBufferUseLruEviction == runtimeUseLru;
}
private void ApplyWaitInstructionBufferDefaultsToAsset(bool applyToRuntime)
{
UnityHelpersBufferSettingsAsset asset = EnsureWaitInstructionBufferSettingsAsset();
if (asset == null)
{
return;
}
_waitInstructionBufferQuantizationStepSeconds = Mathf.Max(
0f,
_waitInstructionBufferQuantizationStepSeconds
);
_waitInstructionBufferMaxDistinctEntries = Mathf.Max(
0,
_waitInstructionBufferMaxDistinctEntries
);
SerializedObject assetSerialized = new(asset);
SerializedProperty applyOnLoadProperty = assetSerialized.FindProperty(
UnityHelpersBufferSettingsAsset.ApplyOnLoadPropertyName
);
SerializedProperty quantizationProperty = assetSerialized.FindProperty(
UnityHelpersBufferSettingsAsset.QuantizationStepSecondsPropertyName
);
SerializedProperty maxEntriesProperty = assetSerialized.FindProperty(
UnityHelpersBufferSettingsAsset.MaxDistinctEntriesPropertyName
);
SerializedProperty useLruProperty = assetSerialized.FindProperty(
UnityHelpersBufferSettingsAsset.UseLruEvictionPropertyName
);
if (applyOnLoadProperty != null)
{
applyOnLoadProperty.boolValue = _waitInstructionBufferApplyOnLoad;
}
if (quantizationProperty != null)
{
quantizationProperty.floatValue = _waitInstructionBufferQuantizationStepSeconds;
}
if (maxEntriesProperty != null)
{
maxEntriesProperty.intValue = _waitInstructionBufferMaxDistinctEntries;
}
if (useLruProperty != null)
{
useLruProperty.boolValue = _waitInstructionBufferUseLruEviction;
}
bool assetChanged = assetSerialized.ApplyModifiedPropertiesWithoutUndo();
if (assetChanged)
{
EditorUtility.SetDirty(asset);
AssetDatabase.SaveAssets();
}
if (applyToRuntime)
{
asset.ApplyToBuffers();
}
}
private static bool ColorsApproximatelyEqual(Color left, Color right)
{
const float tolerance = 0.01f;
return Mathf.Abs(left.r - right.r) <= tolerance
&& Mathf.Abs(left.g - right.g) <= tolerance
&& Mathf.Abs(left.b - right.b) <= tolerance
&& Mathf.Abs(left.a - right.a) <= tolerance;
}
private static UnityHelpersBufferSettingsAsset EnsureWaitInstructionBufferSettingsAsset()
{
if (_waitInstructionBufferSettingsAsset != null)
{
return _waitInstructionBufferSettingsAsset;
}
_waitInstructionBufferSettingsAsset =
AssetDatabase.LoadAssetAtPath(
UnityHelpersBufferSettingsAsset.AssetPath
);
if (_waitInstructionBufferSettingsAsset != null)
{
return _waitInstructionBufferSettingsAsset;
}
string directory = Path.GetDirectoryName(UnityHelpersBufferSettingsAsset.AssetPath);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
UnityHelpersBufferSettingsAsset created =
CreateInstance();
created.SyncFromRuntime();
AssetDatabase.CreateAsset(created, UnityHelpersBufferSettingsAsset.AssetPath);
AssetDatabase.SaveAssets();
_waitInstructionBufferSettingsAsset = created;
return _waitInstructionBufferSettingsAsset;
}
}
#endif
}