com.alicizax.unity.framework/Editor/GameObjectPool/PoolConfigEditorWindow.cs

597 lines
20 KiB
C#
Raw Permalink Normal View History

2026-04-17 21:01:20 +08:00
using System.Collections.Generic;
using AlicizaX.Editor;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
namespace AlicizaX
{
public sealed class PoolConfigEditorWindow : EditorWindow
{
private const float MinLeftWidth = 240f;
private const float InitialLeftWidth = 300f;
private const int ListItemHeight = 46;
private const string WindowTitle = "Pool Config Editor";
private static readonly Color LeftPanelColor = new Color(0.22f, 0.22f, 0.22f, 1f);
private static readonly Color RightPanelColor = new Color(0.16f, 0.16f, 0.16f, 1f);
private PoolConfigScriptableObject _asset;
private SerializedObject _serializedObject;
private SerializedProperty _entriesProperty;
private readonly List<int> _entryIndices = new List<int>();
private int _selectedIndex;
private bool _hasUnsavedChanges;
private ToolbarButton _saveButton;
private Label _titleLabel;
private VisualElement _leftPane;
private ListView _listView;
private ScrollView _detailScrollView;
private Label _detailTitleLabel;
private VisualElement _detailFieldsContainer;
private VisualElement _emptyContainer;
[MenuItem("AlicizaX/GameObjectPool/Open PoolConfig Editor")]
public static void OpenWindow()
{
Open(Selection.activeObject as PoolConfigScriptableObject);
}
public static void Open(PoolConfigScriptableObject asset)
{
PoolConfigEditorWindow window = GetWindow<PoolConfigEditorWindow>(false, WindowTitle, true);
window.minSize = new Vector2(920f, 560f);
window.SetAsset(asset);
window.Show();
}
[OnOpenAsset(0)]
private static bool OnOpenAsset(int instanceId, int line)
{
Object obj = EditorUtility.InstanceIDToObject(instanceId);
if (obj is not PoolConfigScriptableObject asset)
{
return false;
}
Open(asset);
return true;
}
private void CreateGUI()
{
titleContent = new GUIContent(WindowTitle, EditorGUIUtility.IconContent("ScriptableObject Icon").image);
BuildUi();
if (_asset == null && Selection.activeObject is PoolConfigScriptableObject selectedAsset)
{
SetAsset(selectedAsset);
}
else
{
RefreshUi();
}
}
private void OnEnable()
{
titleContent = new GUIContent(WindowTitle, EditorGUIUtility.IconContent("ScriptableObject Icon").image);
if (rootVisualElement.childCount == 0)
{
BuildUi();
}
if (_asset == null && Selection.activeObject is PoolConfigScriptableObject selectedAsset)
{
SetAsset(selectedAsset);
}
else
{
RefreshUi();
}
}
private void OnSelectionChange()
{
if (Selection.activeObject is PoolConfigScriptableObject selectedAsset && selectedAsset != _asset)
{
SetAsset(selectedAsset);
}
}
private void BuildUi()
{
rootVisualElement.Clear();
rootVisualElement.style.flexDirection = FlexDirection.Column;
Toolbar toolbar = new Toolbar();
toolbar.style.flexShrink = 0f;
_saveButton = new ToolbarButton(SaveAsset)
{
tooltip = "Save PoolConfig"
};
_saveButton.Add(new Image
{
image = EditorGUIUtility.IconContent("SaveActive").image,
scaleMode = ScaleMode.ScaleToFit
});
_titleLabel = new Label();
_titleLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
_titleLabel.style.flexGrow = 1f;
_titleLabel.style.marginLeft = 6f;
_titleLabel.style.unityTextAlign = TextAnchor.MiddleLeft;
toolbar.Add(_saveButton);
toolbar.Add(_titleLabel);
rootVisualElement.Add(toolbar);
TwoPaneSplitView splitView = new TwoPaneSplitView(0, InitialLeftWidth, TwoPaneSplitViewOrientation.Horizontal);
splitView.style.flexGrow = 1f;
rootVisualElement.Add(splitView);
_leftPane = new VisualElement();
_leftPane.style.flexGrow = 1f;
_leftPane.style.minWidth = MinLeftWidth;
_leftPane.style.backgroundColor = LeftPanelColor;
_leftPane.style.paddingLeft = 4f;
_leftPane.style.paddingRight = 4f;
_leftPane.style.paddingTop = 4f;
_leftPane.style.paddingBottom = 4f;
VisualElement rightPane = new VisualElement();
rightPane.style.flexGrow = 1f;
rightPane.style.backgroundColor = RightPanelColor;
rightPane.style.paddingLeft = 10f;
rightPane.style.paddingRight = 10f;
rightPane.style.paddingTop = 8f;
rightPane.style.paddingBottom = 8f;
splitView.Add(_leftPane);
splitView.Add(rightPane);
BuildLeftPane();
BuildRightPane(rightPane);
}
private void BuildLeftPane()
{
_listView = new ListView
{
selectionType = SelectionType.Single,
virtualizationMethod = CollectionVirtualizationMethod.FixedHeight,
reorderable = false,
showBorder = false,
showAlternatingRowBackgrounds = AlternatingRowBackground.None,
style =
{
flexGrow = 1f,
marginBottom = 4f
},
fixedItemHeight = ListItemHeight
};
_listView.makeItem = MakeListItem;
_listView.bindItem = BindListItem;
_listView.selectionChanged += _ => OnListSelectionChanged();
_leftPane.Add(_listView);
VisualElement footer = new VisualElement();
footer.style.flexDirection = FlexDirection.Row;
footer.style.justifyContent = Justify.Center;
footer.style.alignItems = Align.Center;
footer.style.flexShrink = 0f;
footer.style.height = 28f;
Button addButton = new Button(AddEntry)
{
tooltip = "Add Item"
};
addButton.style.width = 24f;
addButton.style.height = 20f;
addButton.style.marginRight = 4f;
addButton.Add(new Image
{
image = EditorUtils.Styles.PlusIcon.image,
scaleMode = ScaleMode.ScaleToFit
});
Button removeButton = new Button(RemoveEntry)
{
tooltip = "Remove Item",
name = "remove-button"
};
removeButton.style.width = 24f;
removeButton.style.height = 20f;
removeButton.Add(new Image
{
image = EditorUtils.Styles.MinusIcon.image,
scaleMode = ScaleMode.ScaleToFit
});
removeButton.SetEnabled(false);
footer.Add(addButton);
footer.Add(removeButton);
_leftPane.Add(footer);
}
private void BuildRightPane(VisualElement rightPane)
{
_emptyContainer = new VisualElement
{
style =
{
flexGrow = 1f,
justifyContent = Justify.Center
}
};
_emptyContainer.Add(new HelpBox("No entry selected.", HelpBoxMessageType.Info));
_detailScrollView = new ScrollView
{
style =
{
flexGrow = 1f
}
};
_detailTitleLabel = new Label();
_detailTitleLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
_detailTitleLabel.style.marginBottom = 6f;
_detailTitleLabel.style.unityTextAlign = TextAnchor.MiddleLeft;
_detailFieldsContainer = new VisualElement();
_detailFieldsContainer.style.flexDirection = FlexDirection.Column;
_detailFieldsContainer.style.flexGrow = 1f;
_detailScrollView.Add(_detailTitleLabel);
_detailScrollView.Add(_detailFieldsContainer);
rightPane.Add(_emptyContainer);
rightPane.Add(_detailScrollView);
}
private VisualElement MakeListItem()
{
VisualElement root = new VisualElement();
root.style.height = ListItemHeight;
root.style.flexDirection = FlexDirection.Column;
root.style.justifyContent = Justify.Center;
root.style.paddingLeft = 8f;
root.style.paddingRight = 8f;
root.style.paddingTop = 6f;
root.style.paddingBottom = 6f;
root.style.marginBottom = 2f;
Label primary = new Label { name = "primary" };
primary.style.unityTextAlign = TextAnchor.MiddleLeft;
Label secondary = new Label { name = "secondary" };
secondary.style.unityTextAlign = TextAnchor.MiddleLeft;
secondary.style.fontSize = 11f;
secondary.style.color = new Color(0.72f, 0.72f, 0.72f, 1f);
root.Add(primary);
root.Add(secondary);
return root;
}
private void BindListItem(VisualElement element, int index)
{
SerializedProperty entry = GetEntryAt(index);
element.Q<Label>("primary").text = GetPrimaryLabel(entry);
element.Q<Label>("secondary").text = GetSecondaryLabel(entry);
}
private void SetAsset(PoolConfigScriptableObject asset)
{
_asset = asset;
_selectedIndex = 0;
_hasUnsavedChanges = false;
if (_asset == null)
{
_serializedObject = null;
_entriesProperty = null;
}
else
{
_asset.Normalize();
_serializedObject = new SerializedObject(_asset);
_entriesProperty = _serializedObject.FindProperty("entries");
RebuildEntryIndices();
ClampSelection();
}
RefreshUi();
}
private void RefreshUi()
{
if (_titleLabel == null)
{
return;
}
RefreshTitle();
_saveButton?.SetEnabled(_asset != null);
if (_asset == null || _serializedObject == null)
{
_entryIndices.Clear();
_listView.itemsSource = _entryIndices;
_listView.Rebuild();
_emptyContainer.Clear();
_emptyContainer.Add(new HelpBox("Select or double-click a PoolConfig asset to edit it in this window.", HelpBoxMessageType.Info));
_emptyContainer.style.display = DisplayStyle.Flex;
_detailScrollView.style.display = DisplayStyle.None;
UpdateRemoveButtonState();
return;
}
RebuildEntryIndices();
ClampSelection();
_listView.itemsSource = _entryIndices;
_listView.Rebuild();
if (_entryIndices.Count > 0)
{
_listView.SetSelectionWithoutNotify(new[] { _selectedIndex });
_emptyContainer.style.display = DisplayStyle.None;
_detailScrollView.style.display = DisplayStyle.Flex;
RebuildDetailFields();
}
else
{
_emptyContainer.Clear();
_emptyContainer.Add(new HelpBox("No entry selected.", HelpBoxMessageType.Info));
_emptyContainer.style.display = DisplayStyle.Flex;
_detailScrollView.style.display = DisplayStyle.None;
}
UpdateRemoveButtonState();
}
private void RebuildEntryIndices()
{
_entryIndices.Clear();
int count = _entriesProperty?.arraySize ?? 0;
for (int i = 0; i < count; i++)
{
_entryIndices.Add(i);
}
}
private void UpdateRemoveButtonState()
{
Button removeButton = _leftPane?.Q<Button>("remove-button");
removeButton?.SetEnabled(_entryIndices.Count > 0);
}
private void OnListSelectionChanged()
{
if (_listView.selectedIndex < 0 || _listView.selectedIndex >= _entryIndices.Count)
{
return;
}
_selectedIndex = _listView.selectedIndex;
UpdateRemoveButtonState();
RebuildDetailFields();
}
private void RebuildDetailFields()
{
if (_asset == null || _serializedObject == null)
{
return;
}
SerializedProperty selectedProperty = GetSelectedProperty();
if (selectedProperty == null)
{
return;
}
_serializedObject.Update();
_detailTitleLabel.text = GetPrimaryLabel(selectedProperty);
_detailFieldsContainer.Unbind();
_detailFieldsContainer.Clear();
SerializedProperty iterator = selectedProperty.Copy();
SerializedProperty end = iterator.GetEndProperty();
bool enterChildren = true;
while (iterator.NextVisible(enterChildren) && !SerializedProperty.EqualContents(iterator, end))
{
PropertyField field = new PropertyField(iterator.Copy());
field.RegisterCallback<SerializedPropertyChangeEvent>(OnDetailPropertyChanged);
_detailFieldsContainer.Add(field);
enterChildren = false;
}
_detailFieldsContainer.Bind(_serializedObject);
}
private void OnDetailPropertyChanged(SerializedPropertyChangeEvent evt)
{
if (_asset == null || _serializedObject == null)
{
return;
}
_serializedObject.ApplyModifiedPropertiesWithoutUndo();
_asset.Normalize();
_hasUnsavedChanges = true;
_listView?.RefreshItems();
RefreshTitle();
_detailTitleLabel.text = GetPrimaryLabel(GetSelectedProperty());
}
private void AddEntry()
{
if (_entriesProperty == null)
{
return;
}
_serializedObject.Update();
int index = _entriesProperty.arraySize;
_entriesProperty.InsertArrayElementAtIndex(index);
SerializedProperty property = _entriesProperty.GetArrayElementAtIndex(index);
InitializeNewEntry(property, index);
_serializedObject.ApplyModifiedPropertiesWithoutUndo();
_asset.Normalize();
RebuildEntryIndices();
_selectedIndex = index;
_hasUnsavedChanges = true;
RefreshUi();
_listView.SetSelectionWithoutNotify(new[] { _selectedIndex });
}
private void RemoveEntry()
{
if (_entriesProperty == null || _entryIndices.Count == 0)
{
return;
}
_serializedObject.Update();
_entriesProperty.DeleteArrayElementAtIndex(_selectedIndex);
_serializedObject.ApplyModifiedPropertiesWithoutUndo();
_asset.Normalize();
RebuildEntryIndices();
if (_selectedIndex >= _entryIndices.Count)
{
_selectedIndex = Mathf.Max(0, _entryIndices.Count - 1);
}
_hasUnsavedChanges = true;
RefreshUi();
if (_entryIndices.Count > 0)
{
_listView.SetSelectionWithoutNotify(new[] { _selectedIndex });
}
}
private void InitializeNewEntry(SerializedProperty property, int index)
{
property.FindPropertyRelative("entryName").stringValue = $"PoolEntry{index + 1}";
property.FindPropertyRelative("group").stringValue = PoolEntry.DefaultGroup;
property.FindPropertyRelative("assetPath").stringValue = string.Empty;
property.FindPropertyRelative("matchMode").enumValueIndex = (int)PoolMatchMode.Exact;
property.FindPropertyRelative("loaderType").enumValueIndex = (int)PoolResourceLoaderType.AssetBundle;
property.FindPropertyRelative("overflowPolicy").enumValueIndex = (int)PoolOverflowPolicy.FailFast;
property.FindPropertyRelative("trimPolicy").enumValueIndex = (int)PoolTrimPolicy.IdleOnly;
property.FindPropertyRelative("activationMode").enumValueIndex = (int)PoolActivationMode.SetActive;
property.FindPropertyRelative("resetMode").enumValueIndex = (int)PoolResetMode.PoolableCallbacks;
property.FindPropertyRelative("minRetained").intValue = 0;
property.FindPropertyRelative("softCapacity").intValue = 8;
property.FindPropertyRelative("hardCapacity").intValue = 8;
property.FindPropertyRelative("idleTrimDelay").floatValue = 30f;
property.FindPropertyRelative("prefabUnloadDelay").floatValue = 60f;
property.FindPropertyRelative("autoRecycleDelay").floatValue = 0f;
property.FindPropertyRelative("trimBatchPerTick").intValue = 2;
property.FindPropertyRelative("warmupBatchPerFrame").intValue = 2;
property.FindPropertyRelative("warmupFrameBudgetMs").floatValue = 1f;
property.FindPropertyRelative("allowRuntimeExpand").boolValue = false;
property.FindPropertyRelative("keepPrefabResident").boolValue = false;
property.FindPropertyRelative("aggressiveTrimOnLowMemory").boolValue = false;
property.FindPropertyRelative("priority").intValue = index;
}
private void SaveAsset()
{
if (_asset == null || _serializedObject == null)
{
return;
}
_serializedObject.ApplyModifiedProperties();
_asset.Normalize();
EditorUtility.SetDirty(_asset);
AssetDatabase.SaveAssets();
_serializedObject.Update();
_hasUnsavedChanges = false;
RefreshUi();
}
private void RefreshTitle()
{
string assetLabel = _asset == null ? "No PoolConfig Selected" : _asset.name;
if (_hasUnsavedChanges)
{
assetLabel += " *";
}
_titleLabel.text = assetLabel;
}
private SerializedProperty GetEntryAt(int index)
{
return _entriesProperty == null || index < 0 || index >= _entriesProperty.arraySize
? null
: _entriesProperty.GetArrayElementAtIndex(index);
}
private SerializedProperty GetSelectedProperty()
{
if (_entryIndices.Count == 0)
{
return null;
}
ClampSelection();
return GetEntryAt(_selectedIndex);
}
private string GetPrimaryLabel(SerializedProperty property)
{
if (property == null)
{
return "<Missing>";
}
string entryName = property.FindPropertyRelative("entryName").stringValue;
string assetPath = property.FindPropertyRelative("assetPath").stringValue;
if (!string.IsNullOrWhiteSpace(entryName))
{
return entryName;
}
return string.IsNullOrWhiteSpace(assetPath) ? "<Unnamed Entry>" : assetPath;
}
private string GetSecondaryLabel(SerializedProperty property)
{
if (property == null)
{
return string.Empty;
}
string group = property.FindPropertyRelative("group").stringValue;
string assetPath = property.FindPropertyRelative("assetPath").stringValue;
return string.IsNullOrWhiteSpace(assetPath) ? group : $"{group} | {assetPath}";
}
private void ClampSelection()
{
if (_entryIndices.Count == 0)
{
_selectedIndex = 0;
return;
}
_selectedIndex = Mathf.Clamp(_selectedIndex, 0, _entryIndices.Count - 1);
}
}
}