diff --git a/com.zerogamestudio.zeroengine.data-toolkit/Editor/Core/DataToolkitProjectProfile.cs b/com.zerogamestudio.zeroengine.data-toolkit/Editor/Core/DataToolkitProjectProfile.cs index a195192..c9d7be2 100644 --- a/com.zerogamestudio.zeroengine.data-toolkit/Editor/Core/DataToolkitProjectProfile.cs +++ b/com.zerogamestudio.zeroengine.data-toolkit/Editor/Core/DataToolkitProjectProfile.cs @@ -9,7 +9,8 @@ public sealed class DataToolkitProjectProfile public DataToolkitProjectProfile( DataToolkitProjectSettings settings, IEnumerable toolbarProviders = null, - IEnumerable assetInspectorProviders = null) + IEnumerable assetInspectorProviders = null, + IEnumerable headerActionProviders = null) { Settings = settings ?? throw new ArgumentNullException(nameof(settings)); ToolbarProviders = (toolbarProviders ?? Array.Empty()) @@ -20,10 +21,15 @@ public DataToolkitProjectProfile( .Where(provider => provider != null) .OrderBy(provider => provider.Order) .ToArray(); + HeaderActionProviders = (headerActionProviders ?? Array.Empty()) + .Where(provider => provider != null) + .OrderBy(provider => provider.Order) + .ToArray(); } public DataToolkitProjectSettings Settings { get; } public IReadOnlyList ToolbarProviders { get; } public IReadOnlyList AssetInspectorProviders { get; } + public IReadOnlyList HeaderActionProviders { get; } } } diff --git a/com.zerogamestudio.zeroengine.data-toolkit/Editor/Core/IDataToolkitHeaderActionProvider.cs b/com.zerogamestudio.zeroengine.data-toolkit/Editor/Core/IDataToolkitHeaderActionProvider.cs new file mode 100644 index 0000000..ace16fd --- /dev/null +++ b/com.zerogamestudio.zeroengine.data-toolkit/Editor/Core/IDataToolkitHeaderActionProvider.cs @@ -0,0 +1,9 @@ +namespace ZGS.DataToolkit.Editor +{ + public interface IDataToolkitHeaderActionProvider + { + int Order { get; } + bool IsVisible(DataToolkitContext context); + void DrawHeaderActions(DataToolkitContext context); + } +} diff --git a/com.zerogamestudio.zeroengine.data-toolkit/Editor/Core/IDataToolkitHeaderActionProvider.cs.meta b/com.zerogamestudio.zeroengine.data-toolkit/Editor/Core/IDataToolkitHeaderActionProvider.cs.meta new file mode 100644 index 0000000..7e928b8 --- /dev/null +++ b/com.zerogamestudio.zeroengine.data-toolkit/Editor/Core/IDataToolkitHeaderActionProvider.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8f64b87e007e25c44aac78c77fd0ddfb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.zerogamestudio.zeroengine.data-toolkit/Editor/Inspection/LazyPreviewAssetInspector.cs b/com.zerogamestudio.zeroengine.data-toolkit/Editor/Inspection/LazyPreviewAssetInspector.cs new file mode 100644 index 0000000..cbcb120 --- /dev/null +++ b/com.zerogamestudio.zeroengine.data-toolkit/Editor/Inspection/LazyPreviewAssetInspector.cs @@ -0,0 +1,317 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace ZGS.DataToolkit.Editor +{ + internal sealed class LazyPreviewAssetInspector : IDisposable + { + private const int PageSize = 50; + private const int MaxSummaryDepth = 3; + private static readonly string[] PreferredSummaryNames = + { + "Level", + "RoomType", + "Id", + "ID", + "Key", + "Name", + "DisplayName", + "Title", + "LocalizeKey", + "ClipName", + "RandomClipsName", + "ClipReference" + }; + + private readonly Dictionary foldouts = new(); + private readonly Dictionary pages = new(); + private SerializedObject serializedObject; + private Object target; + + public void SetTarget(Object asset) + { + if (target == asset) + { + return; + } + + Dispose(); + target = asset; + if (asset != null) + { + serializedObject = new SerializedObject(asset); + } + } + + public void Draw() + { + if (serializedObject == null) + { + return; + } + + serializedObject.UpdateIfRequiredOrScript(); + var iterator = serializedObject.GetIterator(); + var enterChildren = true; + + while (iterator.NextVisible(enterChildren)) + { + enterChildren = false; + using (new EditorGUI.DisabledScope(iterator.propertyPath == "m_Script")) + { + if (IsPreviewCollection(iterator)) + { + DrawCollectionPreview(iterator.Copy()); + } + else + { + EditorGUILayout.PropertyField(iterator, BuildLabel(iterator), includeChildren: true); + } + } + } + + serializedObject.ApplyModifiedProperties(); + } + + public void Dispose() + { + serializedObject?.Dispose(); + serializedObject = null; + target = null; + foldouts.Clear(); + pages.Clear(); + } + + private static bool IsPreviewCollection(SerializedProperty property) + { + return property.isArray && property.propertyType != SerializedPropertyType.String; + } + + private void DrawCollectionPreview(SerializedProperty property) + { + var key = property.propertyPath; + var count = property.arraySize; + var expanded = foldouts.TryGetValue(key, out var value) && value; + + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + EditorGUILayout.BeginHorizontal(); + expanded = EditorGUILayout.Foldout(expanded, BuildLabel(property), true); + GUILayout.FlexibleSpace(); + EditorGUILayout.LabelField($"{count} items", GUILayout.Width(90f)); + EditorGUILayout.EndHorizontal(); + foldouts[key] = expanded; + + if (expanded) + { + var pageCount = Mathf.Max(1, Mathf.CeilToInt(count / (float)PageSize)); + var page = Mathf.Clamp(pages.TryGetValue(key, out var storedPage) ? storedPage : 0, 0, pageCount - 1); + DrawPageControls(key, page, pageCount); + page = pages.TryGetValue(key, out storedPage) ? storedPage : page; + + var start = page * PageSize; + var end = Mathf.Min(start + PageSize, count); + for (var i = start; i < end; i++) + { + DrawCollectionElement(property.GetArrayElementAtIndex(i), i); + } + } + + EditorGUILayout.EndVertical(); + } + + private void DrawPageControls(string key, int page, int pageCount) + { + EditorGUILayout.BeginHorizontal(); + using (new EditorGUI.DisabledScope(page <= 0)) + { + if (GUILayout.Button("Prev", GUILayout.Width(64f))) + { + page--; + } + } + + EditorGUILayout.LabelField($"Page {page + 1}/{pageCount}", GUILayout.Width(110f)); + + using (new EditorGUI.DisabledScope(page >= pageCount - 1)) + { + if (GUILayout.Button("Next", GUILayout.Width(64f))) + { + page++; + } + } + + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + pages[key] = Mathf.Clamp(page, 0, pageCount - 1); + } + + private static void DrawCollectionElement(SerializedProperty element, int index) + { + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + EditorGUILayout.PropertyField(element, BuildElementLabel(index, BuildElementSummary(element)), includeChildren: true); + EditorGUILayout.EndVertical(); + } + + private static GUIContent BuildElementLabel(int index, string summary) + { + return string.IsNullOrWhiteSpace(summary) + ? new GUIContent($"Element {index}") + : new GUIContent($"Element {index}: {summary}"); + } + + private static string BuildElementSummary(SerializedProperty element) + { + if (!element.hasVisibleChildren) + { + return ReadLeafValue(element); + } + + var values = new List(); + foreach (var preferredName in PreferredSummaryNames) + { + var child = FindDescendantByNormalizedName(element, preferredName); + if (child == null) + { + continue; + } + + var value = ReadLeafValue(child); + if (!string.IsNullOrWhiteSpace(value)) + { + values.Add($"{preferredName}: {value}"); + } + } + + return values.Count == 0 ? "(expand for details)" : string.Join(" | ", values.Distinct()); + } + + private static SerializedProperty FindDescendantByNormalizedName(SerializedProperty parent, string name) + { + var copy = parent.Copy(); + var end = copy.GetEndProperty(); + var enterChildren = true; + var rootDepth = parent.depth; + var normalizedName = NormalizeName(name); + + while (copy.NextVisible(enterChildren) && !SerializedProperty.EqualContents(copy, end)) + { + var relativeDepth = copy.depth - rootDepth; + if (relativeDepth > MaxSummaryDepth) + { + enterChildren = false; + continue; + } + + enterChildren = relativeDepth < MaxSummaryDepth; + if (NormalizeName(copy.name) == normalizedName) + { + return copy.Copy(); + } + } + + return null; + } + + private static string ReadLeafValue(SerializedProperty property) + { + return property.propertyType switch + { + SerializedPropertyType.String => property.stringValue, + SerializedPropertyType.Boolean => property.boolValue.ToString(), + SerializedPropertyType.Enum => FormatEnumValue(property), + SerializedPropertyType.Integer => property.intValue.ToString(), + SerializedPropertyType.Float => property.floatValue.ToString("0.###"), + SerializedPropertyType.ObjectReference => property.objectReferenceValue == null ? string.Empty : property.objectReferenceValue.name, + _ => ReadKnownNestedValue(property) + }; + } + + private static string FormatEnumValue(SerializedProperty property) + { + return property.enumValueIndex >= 0 && property.enumValueIndex < property.enumDisplayNames.Length + ? property.enumDisplayNames[property.enumValueIndex] + : property.intValue.ToString(); + } + + private static string ReadKnownNestedValue(SerializedProperty property) + { + var guidProperty = property.FindPropertyRelative("m_AssetGUID") + ?? property.FindPropertyRelative("AssetGUID") + ?? property.FindPropertyRelative("assetGUID"); + if (guidProperty != null && guidProperty.propertyType == SerializedPropertyType.String) + { + return guidProperty.stringValue; + } + + if (property.isArray && property.propertyType != SerializedPropertyType.String) + { + var stringPreview = ReadStringArrayPreview(property); + return string.IsNullOrEmpty(stringPreview) ? $"{property.arraySize} items" : stringPreview; + } + + return string.Empty; + } + + private static string ReadStringArrayPreview(SerializedProperty property) + { + if (!property.isArray || property.propertyType == SerializedPropertyType.String) + { + return string.Empty; + } + + var values = new List(); + var count = Mathf.Min(property.arraySize, 3); + for (var i = 0; i < count; i++) + { + var element = property.GetArrayElementAtIndex(i); + if (element.propertyType == SerializedPropertyType.String && !string.IsNullOrWhiteSpace(element.stringValue)) + { + values.Add(element.stringValue); + } + } + + if (values.Count == 0) + { + return string.Empty; + } + + return property.arraySize > values.Count + ? string.Join(", ", values) + ", ..." + : string.Join(", ", values); + } + + private static GUIContent BuildLabel(SerializedProperty property) + { + return new GUIContent(ObjectNames.NicifyVariableName(StripBackingFieldName(property.name))); + } + + private static string NormalizeName(string name) + { + return StripBackingFieldName(name).Replace(" ", string.Empty).Trim(); + } + + private static string StripBackingFieldName(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + return string.Empty; + } + + var trimmed = name.Trim(); + if (trimmed.StartsWith("<", StringComparison.Ordinal)) + { + var suffixIndex = trimmed.IndexOf(">k__BackingField", StringComparison.Ordinal); + if (suffixIndex > 1) + { + return trimmed.Substring(1, suffixIndex - 1); + } + } + + return trimmed; + } + } +} diff --git a/com.zerogamestudio.zeroengine.data-toolkit/Editor/Inspection/LazyPreviewAssetInspector.cs.meta b/com.zerogamestudio.zeroengine.data-toolkit/Editor/Inspection/LazyPreviewAssetInspector.cs.meta new file mode 100644 index 0000000..9b9a5ea --- /dev/null +++ b/com.zerogamestudio.zeroengine.data-toolkit/Editor/Inspection/LazyPreviewAssetInspector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7a0619402dc84ce3a87e793485f8f7d8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.zerogamestudio.zeroengine.data-toolkit/Editor/Inspection/SafeSerializedAssetInspector.cs b/com.zerogamestudio.zeroengine.data-toolkit/Editor/Inspection/SafeSerializedAssetInspector.cs new file mode 100644 index 0000000..9e5ea27 --- /dev/null +++ b/com.zerogamestudio.zeroengine.data-toolkit/Editor/Inspection/SafeSerializedAssetInspector.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace ZGS.DataToolkit.Editor +{ + internal sealed class SafeSerializedAssetInspector : IDisposable + { + private readonly List skippedPropertyNames = new(); + private DataToolkitSafeInspectorRule rule; + private SerializedObject serializedObject; + private Object target; + + public bool CanInspect(Object asset) + { + return asset != null; + } + + public void SetTarget(Object asset, DataToolkitSafeInspectorRule rule) + { + if (target == asset && ReferenceEquals(this.rule, rule)) + { + return; + } + + Dispose(); + target = asset; + this.rule = rule; + if (asset != null && rule != null) + { + serializedObject = new SerializedObject(asset); + } + } + + public void Draw() + { + if (serializedObject == null || rule == null) + { + return; + } + + skippedPropertyNames.Clear(); + serializedObject.UpdateIfRequiredOrScript(); + var iterator = serializedObject.GetIterator(); + var enterChildren = true; + + while (iterator.NextVisible(enterChildren)) + { + enterChildren = false; + if (IsExcludedProperty(iterator)) + { + skippedPropertyNames.Add(GetDisplayName(iterator)); + continue; + } + + using (new EditorGUI.DisabledScope(iterator.propertyPath == "m_Script")) + { + EditorGUILayout.PropertyField(iterator, BuildLabel(iterator), includeChildren: true); + } + } + + serializedObject.ApplyModifiedProperties(); + DrawSkippedPropertySummary(); + } + + public void Dispose() + { + serializedObject?.Dispose(); + serializedObject = null; + target = null; + rule = null; + skippedPropertyNames.Clear(); + } + + private bool IsExcludedProperty(SerializedProperty property) + { + foreach (var excludedPropertyPath in rule.ExcludedPropertyPaths) + { + var normalizedExcludedPath = NormalizePropertyPath(excludedPropertyPath); + if (string.IsNullOrEmpty(normalizedExcludedPath)) + { + continue; + } + + var normalizedPropertyName = NormalizePropertyPath(property.name); + var normalizedPropertyPath = NormalizePropertyPath(property.propertyPath); + if (normalizedPropertyName == normalizedExcludedPath || + normalizedPropertyPath == normalizedExcludedPath || + normalizedPropertyPath.EndsWith("." + normalizedExcludedPath, StringComparison.Ordinal)) + { + return true; + } + } + + return false; + } + + private GUIContent BuildLabel(SerializedProperty property) + { + var displayName = StripBackingFieldName(property.name); + return string.IsNullOrWhiteSpace(displayName) + ? new GUIContent(property.displayName) + : new GUIContent(ObjectNames.NicifyVariableName(displayName)); + } + + private string GetDisplayName(SerializedProperty property) + { + var displayName = StripBackingFieldName(property.name); + return string.IsNullOrWhiteSpace(displayName) + ? property.displayName + : ObjectNames.NicifyVariableName(displayName); + } + + private void DrawSkippedPropertySummary() + { + if (skippedPropertyNames.Count == 0) + { + return; + } + + EditorGUILayout.Space(6f); + EditorGUILayout.HelpBox( + rule.Summary ?? "Some configured heavy fields are hidden to keep this Data Manager view responsive.", + MessageType.Info); + EditorGUILayout.LabelField("Hidden Fields", string.Join(", ", skippedPropertyNames.Distinct(StringComparer.Ordinal))); + } + + private static string NormalizePropertyPath(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return string.Empty; + } + + var normalizedParts = path + .Replace('\\', '.') + .Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries) + .Select(StripBackingFieldName) + .Where(part => !string.IsNullOrWhiteSpace(part)); + + return string.Join(".", normalizedParts); + } + + private static string StripBackingFieldName(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + return string.Empty; + } + + var trimmed = name.Trim(); + if (trimmed.StartsWith("<", StringComparison.Ordinal)) + { + var backingFieldSuffixIndex = trimmed.IndexOf(">k__BackingField", StringComparison.Ordinal); + if (backingFieldSuffixIndex > 1) + { + return trimmed.Substring(1, backingFieldSuffixIndex - 1); + } + } + + return trimmed; + } + } +} diff --git a/com.zerogamestudio.zeroengine.data-toolkit/Editor/Inspection/SafeSerializedAssetInspector.cs.meta b/com.zerogamestudio.zeroengine.data-toolkit/Editor/Inspection/SafeSerializedAssetInspector.cs.meta new file mode 100644 index 0000000..39e3911 --- /dev/null +++ b/com.zerogamestudio.zeroengine.data-toolkit/Editor/Inspection/SafeSerializedAssetInspector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8100cda154994a37ad182b6360126a14 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.zerogamestudio.zeroengine.data-toolkit/Editor/Settings/DataToolkitDefaultInspectorMode.cs b/com.zerogamestudio.zeroengine.data-toolkit/Editor/Settings/DataToolkitDefaultInspectorMode.cs new file mode 100644 index 0000000..3d1d3d1 --- /dev/null +++ b/com.zerogamestudio.zeroengine.data-toolkit/Editor/Settings/DataToolkitDefaultInspectorMode.cs @@ -0,0 +1,9 @@ +namespace ZGS.DataToolkit.Editor +{ + public enum DataToolkitDefaultInspectorMode + { + FullInspector, + SafeSummary, + LazyPreview + } +} diff --git a/com.zerogamestudio.zeroengine.data-toolkit/Editor/Settings/DataToolkitDefaultInspectorMode.cs.meta b/com.zerogamestudio.zeroengine.data-toolkit/Editor/Settings/DataToolkitDefaultInspectorMode.cs.meta new file mode 100644 index 0000000..4dd307f --- /dev/null +++ b/com.zerogamestudio.zeroengine.data-toolkit/Editor/Settings/DataToolkitDefaultInspectorMode.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0d0d3724e1514f2b95d8b1e28d05abb9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.zerogamestudio.zeroengine.data-toolkit/Editor/Settings/DataToolkitProjectSettings.cs b/com.zerogamestudio.zeroengine.data-toolkit/Editor/Settings/DataToolkitProjectSettings.cs index 0a03db5..c2484dd 100644 --- a/com.zerogamestudio.zeroengine.data-toolkit/Editor/Settings/DataToolkitProjectSettings.cs +++ b/com.zerogamestudio.zeroengine.data-toolkit/Editor/Settings/DataToolkitProjectSettings.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; @@ -11,7 +12,9 @@ public DataToolkitProjectSettings( string menuPath, string editorPrefsPrefix, IEnumerable searchRoots, - IEnumerable excludedPaths) + IEnumerable excludedPaths, + DataToolkitDefaultInspectorMode defaultInspectorMode = DataToolkitDefaultInspectorMode.FullInspector, + IEnumerable safeInspectorRules = null) { ProjectId = string.IsNullOrWhiteSpace(projectId) ? "ZGS" : projectId.Trim(); WindowTitle = string.IsNullOrWhiteSpace(windowTitle) ? "Data Manager" : windowTitle.Trim(); @@ -19,6 +22,8 @@ public DataToolkitProjectSettings( EditorPrefsPrefix = string.IsNullOrWhiteSpace(editorPrefsPrefix) ? ProjectId : editorPrefsPrefix.Trim(); SearchRoots = NormalizePaths(searchRoots).ToArray(); ExcludedPaths = NormalizePaths(excludedPaths).ToArray(); + DefaultInspectorMode = defaultInspectorMode; + SafeInspectorRules = NormalizeSafeInspectorRules(safeInspectorRules).ToArray(); } public string ProjectId { get; } @@ -27,6 +32,8 @@ public DataToolkitProjectSettings( public string EditorPrefsPrefix { get; } public IReadOnlyList SearchRoots { get; } public IReadOnlyList ExcludedPaths { get; } + public DataToolkitDefaultInspectorMode DefaultInspectorMode { get; } + public IReadOnlyList SafeInspectorRules { get; } public string PrefKey(string suffix) { @@ -50,5 +57,11 @@ private static IEnumerable NormalizePaths(IEnumerable paths) yield return path.Replace('\\', '/').TrimEnd('/'); } } + + private static IEnumerable NormalizeSafeInspectorRules( + IEnumerable rules) + { + return (rules ?? Array.Empty()).Where(rule => rule != null); + } } } diff --git a/com.zerogamestudio.zeroengine.data-toolkit/Editor/Settings/DataToolkitSafeInspectorRule.cs b/com.zerogamestudio.zeroengine.data-toolkit/Editor/Settings/DataToolkitSafeInspectorRule.cs new file mode 100644 index 0000000..ee668f9 --- /dev/null +++ b/com.zerogamestudio.zeroengine.data-toolkit/Editor/Settings/DataToolkitSafeInspectorRule.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ZGS.DataToolkit.Editor +{ + public sealed class DataToolkitSafeInspectorRule + { + public DataToolkitSafeInspectorRule( + Type assetType, + IEnumerable excludedPropertyPaths, + string summary = null) + { + AssetType = assetType ?? throw new ArgumentNullException(nameof(assetType)); + ExcludedPropertyPaths = (excludedPropertyPaths ?? Array.Empty()) + .Where(path => !string.IsNullOrWhiteSpace(path)) + .Select(path => path.Trim()) + .Distinct(StringComparer.Ordinal) + .ToArray(); + Summary = string.IsNullOrWhiteSpace(summary) ? null : summary.Trim(); + } + + public Type AssetType { get; } + public IReadOnlyList ExcludedPropertyPaths { get; } + public string Summary { get; } + + public bool Matches(Type assetType) + { + return assetType != null && AssetType.IsAssignableFrom(assetType); + } + } +} diff --git a/com.zerogamestudio.zeroengine.data-toolkit/Editor/Settings/DataToolkitSafeInspectorRule.cs.meta b/com.zerogamestudio.zeroengine.data-toolkit/Editor/Settings/DataToolkitSafeInspectorRule.cs.meta new file mode 100644 index 0000000..c7ebf6e --- /dev/null +++ b/com.zerogamestudio.zeroengine.data-toolkit/Editor/Settings/DataToolkitSafeInspectorRule.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1d751a079d3b4af8a49f45c825e04d21 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.zerogamestudio.zeroengine.data-toolkit/Editor/Windows/DataToolkitWindow.cs b/com.zerogamestudio.zeroengine.data-toolkit/Editor/Windows/DataToolkitWindow.cs index 09795b7..558c05b 100644 --- a/com.zerogamestudio.zeroengine.data-toolkit/Editor/Windows/DataToolkitWindow.cs +++ b/com.zerogamestudio.zeroengine.data-toolkit/Editor/Windows/DataToolkitWindow.cs @@ -18,16 +18,20 @@ public sealed class DataToolkitWindow : EditorWindow private const float HeaderRowHeight = 38f; private const float ProjectToolbarRowHeight = 64f; private const float HeaderBodySpacing = 4f; + private const float HeaderActionSpacing = 6f; private const float SplitterWidth = 5f; private const float RowHeight = 24f; private const long LargeAssetInspectorThresholdBytes = 512 * 1024; private readonly CompositeAssetInspector inspector = new(); + private readonly SafeSerializedAssetInspector safeInspector = new(); + private readonly LazyPreviewAssetInspector lazyPreviewInspector = new(); private readonly Dictionary assetCountCache = new(); private readonly Queue pendingCountTypes = new(); private DataToolkitContext context; private IReadOnlyList toolbarProviders = Array.Empty(); + private IReadOnlyList headerActionProviders = Array.Empty(); private Type[] typesToDisplay = Array.Empty(); private Type selectedType; private string selectedAssetPath; @@ -103,6 +107,7 @@ private void Initialize(DataToolkitProjectProfile profile) var settings = profile.Settings; context = new DataToolkitContext(settings); toolbarProviders = profile.ToolbarProviders; + headerActionProviders = profile.HeaderActionProviders; inspector.SetCustomInspectors(context, profile.AssetInspectorProviders); titleContent = new GUIContent(settings.WindowTitle); minSize = new Vector2(980f, 560f); @@ -132,6 +137,8 @@ private void OnDisable() } inspector.Dispose(); + safeInspector.Dispose(); + lazyPreviewInspector.Dispose(); StopAssetCountWarmup(); DataToolkitProjectRegistry.DefaultProfileRegistered -= RestoreDefaultProfileIfUsingGenericFallback; } @@ -145,6 +152,7 @@ private void OnGUI() { EnsureContext(); var visibleToolbarProviders = GetVisibleToolbarProviders(); + var visibleHeaderActionProviders = GetVisibleHeaderActionProviders(); var headerHeight = CalculateHeaderHeight(visibleToolbarProviders.Count); var contentWidth = Mathf.Max(0f, position.width - WindowPadding * 2f); var headerRect = new Rect(WindowPadding, WindowPadding, contentWidth, headerHeight); @@ -154,7 +162,7 @@ private void OnGUI() contentWidth, Mathf.Max(0f, position.height - headerRect.yMax - HeaderBodySpacing - WindowPadding)); - DrawHeaderToolbar(headerRect, visibleToolbarProviders); + DrawHeaderToolbar(headerRect, visibleToolbarProviders, visibleHeaderActionProviders); DrawBodyLayout(bodyRect); } @@ -209,7 +217,37 @@ private IReadOnlyList GetVisibleToolbarProviders() return visibleProviders; } - private void DrawHeaderToolbar(Rect rect, IReadOnlyList visibleToolbarProviders) + private IReadOnlyList GetVisibleHeaderActionProviders() + { + var visibleProviders = new List(); + + foreach (var headerActionProvider in headerActionProviders) + { + if (headerActionProvider == null) + { + continue; + } + + try + { + if (headerActionProvider.IsVisible(context)) + { + visibleProviders.Add(headerActionProvider); + } + } + catch (Exception exception) + { + Debug.LogException(exception); + } + } + + return visibleProviders; + } + + private void DrawHeaderToolbar( + Rect rect, + IReadOnlyList visibleToolbarProviders, + IReadOnlyList visibleHeaderActionProviders) { GUILayout.BeginArea(rect); using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox, GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(true))) @@ -218,6 +256,11 @@ private void DrawHeaderToolbar(Rect rect, IReadOnlyList 0) + { + GUILayout.Space(HeaderActionSpacing); + } if (GUILayout.Button("Refresh", GUILayout.Width(82f), GUILayout.Height(24f))) { @@ -230,6 +273,21 @@ private void DrawHeaderToolbar(Rect rect, IReadOnlyList visibleHeaderActionProviders) + { + foreach (var headerActionProvider in visibleHeaderActionProviders) + { + try + { + headerActionProvider.DrawHeaderActions(context); + } + catch (Exception exception) + { + Debug.LogException(exception); + } + } + } + private void DrawProjectToolbars(IReadOnlyList visibleToolbarProviders) { foreach (var toolbarProvider in visibleToolbarProviders) @@ -345,7 +403,21 @@ private void DrawSelectedAssetInspector(Rect rect) EditorGUILayout.EndHorizontal(); - if (ShouldDeferFullInspector(selectedAsset) && !HasCustomInspectorFor(selectedAsset) && !allowFullInspectorForSelectedAsset) + if (ShouldUseSafeInspector(selectedAsset, out var safeInspectorRule) && + !HasCustomInspectorFor(selectedAsset) && + !allowFullInspectorForSelectedAsset) + { + DrawSafeInspector(safeInspectorRule); + } + else if (ShouldUseLazyPreviewInspector(selectedAsset) && !allowFullInspectorForSelectedAsset) + { + DrawLazyPreviewInspector(); + } + else if (ShouldUseSafeSummaryInspector(selectedAsset) && !allowFullInspectorForSelectedAsset) + { + DrawSafeSummaryInspectorState(); + } + else if (ShouldDeferFullInspector(selectedAsset) && !HasCustomInspectorFor(selectedAsset) && !allowFullInspectorForSelectedAsset) { DrawDeferredInspectorState(); } @@ -368,6 +440,52 @@ private void DrawSelectedAssetInspector(Rect rect) GUILayout.EndArea(); } + private void DrawSafeInspector(DataToolkitSafeInspectorRule rule) + { + safeInspector.SetTarget(selectedAsset, rule); + inspectorScroll = EditorGUILayout.BeginScrollView(inspectorScroll); + EditorGUI.BeginChangeCheck(); + safeInspector.Draw(); + if (EditorGUI.EndChangeCheck()) + { + EditorUtility.SetDirty(selectedAsset); + Repaint(); + } + + EditorGUILayout.Space(8f); + if (GUILayout.Button("Open Full Inspector", GUILayout.Height(28f))) + { + allowFullInspectorForSelectedAsset = true; + inspector.SetTarget(selectedAsset); + Repaint(); + } + + EditorGUILayout.EndScrollView(); + } + + private void DrawLazyPreviewInspector() + { + lazyPreviewInspector.SetTarget(selectedAsset); + inspectorScroll = EditorGUILayout.BeginScrollView(inspectorScroll); + EditorGUI.BeginChangeCheck(); + lazyPreviewInspector.Draw(); + if (EditorGUI.EndChangeCheck()) + { + EditorUtility.SetDirty(selectedAsset); + Repaint(); + } + + EditorGUILayout.Space(8f); + if (GUILayout.Button("Open Full Inspector", GUILayout.Height(28f))) + { + allowFullInspectorForSelectedAsset = true; + inspector.SetTarget(selectedAsset); + Repaint(); + } + + EditorGUILayout.EndScrollView(); + } + private void DrawEmptyInspectorState() { EditorGUILayout.HelpBox("Select a data asset from the middle column.", MessageType.Info); @@ -375,13 +493,24 @@ private void DrawEmptyInspectorState() private void DrawDeferredInspectorState() { - var assetPath = ResolveSelectedAssetPath(); - var sizeInBytes = GetAssetFileSize(assetPath); - - EditorGUILayout.HelpBox( + DrawInspectorSummary( "This asset is large, so the full inspector is deferred to keep Data Manager responsive.", MessageType.Info); + } + + private void DrawSafeSummaryInspectorState() + { + DrawInspectorSummary( + "The full inspector is hidden by default to keep Data Manager responsive.", + MessageType.Info); + } + + private void DrawInspectorSummary(string message, MessageType messageType) + { + var assetPath = ResolveSelectedAssetPath(); + var sizeInBytes = GetAssetFileSize(assetPath); + EditorGUILayout.HelpBox(message, messageType); EditorGUILayout.LabelField("Type", selectedAsset.GetType().Name); EditorGUILayout.LabelField("Path", string.IsNullOrEmpty(assetPath) ? "(unknown)" : assetPath); EditorGUILayout.LabelField("Size", FormatByteSize(sizeInBytes)); @@ -395,6 +524,32 @@ private void DrawDeferredInspectorState() } } + private bool ShouldUseSafeSummaryInspector(UnityEngine.Object asset) + { + return asset != null && + context.Settings.DefaultInspectorMode == DataToolkitDefaultInspectorMode.SafeSummary && + !HasCustomInspectorFor(asset); + } + + private bool ShouldUseLazyPreviewInspector(UnityEngine.Object asset) + { + return asset != null && + context.Settings.DefaultInspectorMode == DataToolkitDefaultInspectorMode.LazyPreview && + !HasCustomInspectorFor(asset); + } + + private bool ShouldUseSafeInspector(UnityEngine.Object asset, out DataToolkitSafeInspectorRule rule) + { + rule = null; + if (asset == null || !safeInspector.CanInspect(asset)) + { + return false; + } + + rule = context.Settings.SafeInspectorRules.FirstOrDefault(candidate => candidate.Matches(asset.GetType())); + return rule != null; + } + private bool ShouldDeferFullInspector(UnityEngine.Object asset) { if (asset == null) @@ -573,6 +728,8 @@ private void SelectAsset(UnityEngine.Object asset) inspectorScroll = Vector2.zero; allowFullInspectorForSelectedAsset = false; inspector.SetTarget(null); + safeInspector.SetTarget(null, null); + lazyPreviewInspector.SetTarget(null); } private void ClearSelectedAsset() @@ -583,6 +740,8 @@ private void ClearSelectedAsset() inspectorScroll = Vector2.zero; allowFullInspectorForSelectedAsset = false; inspector.SetTarget(null); + safeInspector.SetTarget(null, null); + lazyPreviewInspector.SetTarget(null); } private bool IsTypeVisible(Type type)