649 lines
28 KiB
C#
649 lines
28 KiB
C#
#if UNITY_EDITOR
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Reflection;
|
||
using Continentis.Mods;
|
||
using UnityEditor;
|
||
using UnityEngine;
|
||
using UnityEngine.Events;
|
||
using Object = UnityEngine.Object;
|
||
|
||
namespace SLSFramework.UModAssistance
|
||
{
|
||
#region List<string>选择器,通过类型查找资产,将其名称存储在列表中
|
||
|
||
public partial class DataEditor : Editor
|
||
{
|
||
private string _pickerTargetListName;
|
||
|
||
private Dictionary<string, Object> _assetCache;
|
||
|
||
protected virtual void OnEnable()
|
||
{
|
||
// 每次选中新对象时,都清空缓存
|
||
_assetCache = new Dictionary<string, Object>();
|
||
}
|
||
|
||
|
||
protected void DrawCharacterListGUI<T>(SerializedProperty listProperty, string searchFilter = "") where T : Object
|
||
{
|
||
if (string.IsNullOrEmpty(searchFilter))
|
||
{
|
||
searchFilter = $"t:{typeof(T).Name}";
|
||
}
|
||
|
||
listProperty.isExpanded = EditorGUILayout.Foldout(listProperty.isExpanded, listProperty.displayName, false);
|
||
|
||
if (listProperty.isExpanded)
|
||
{
|
||
EditorGUI.indentLevel++;
|
||
EditorGUI.indentLevel++;
|
||
|
||
for (int i = 0; i < listProperty.arraySize; i++)
|
||
{
|
||
SerializedProperty elementNameProp = listProperty.GetArrayElementAtIndex(i);
|
||
EditorGUILayout.BeginHorizontal();
|
||
|
||
EditorGUI.BeginChangeCheck();
|
||
EditorGUILayout.PropertyField(elementNameProp, GUIContent.none);
|
||
bool valueChanged = EditorGUI.EndChangeCheck();
|
||
string assetName = elementNameProp.stringValue;
|
||
|
||
if (valueChanged || !_assetCache.TryGetValue(assetName, out Object foundAsset))
|
||
{
|
||
foundAsset = FindObjectData<T>(assetName, searchFilter);
|
||
_assetCache[assetName] = foundAsset; // 将搜索结果(即使是null)存入缓存
|
||
}
|
||
|
||
if (foundAsset != null)
|
||
{
|
||
EditorGUI.BeginDisabledGroup(true);
|
||
EditorGUILayout.ObjectField(foundAsset, typeof(T), false, GUILayout.Width(150));
|
||
EditorGUI.EndDisabledGroup();
|
||
}
|
||
else
|
||
{
|
||
EditorGUILayout.LabelField(new GUIContent(EditorGUIUtility.IconContent("console.warnicon").image, "资产未找到或名称不匹配"),
|
||
GUILayout.Width(20));
|
||
}
|
||
|
||
if (GUILayout.Button("-", GUILayout.Width(20)))
|
||
{
|
||
_assetCache.Remove(elementNameProp.stringValue);
|
||
listProperty.DeleteArrayElementAtIndex(i);
|
||
i--;
|
||
}
|
||
|
||
EditorGUILayout.EndHorizontal();
|
||
}
|
||
|
||
if (GUILayout.Button("Add by Search..."))
|
||
{
|
||
_pickerTargetListName = listProperty.propertyPath;
|
||
EditorGUIUtility.ShowObjectPicker<T>(null, false, searchFilter, GUI.skin.GetHashCode());
|
||
}
|
||
|
||
EditorGUI.indentLevel--;
|
||
}
|
||
}
|
||
|
||
protected T FindObjectData<T>(string assetName, string searchFilter) where T : Object
|
||
{
|
||
if (string.IsNullOrEmpty(assetName)) return null;
|
||
|
||
string[] guids = AssetDatabase.FindAssets($"{assetName} {searchFilter}");
|
||
if (guids.Length > 0)
|
||
{
|
||
return AssetDatabase.LoadAssetAtPath<T>(AssetDatabase.GUIDToAssetPath(guids[0]));
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
protected void HandleObjectPicker()
|
||
{
|
||
if (Event.current.commandName == "ObjectSelectorUpdated" && EditorGUIUtility.GetObjectPickerControlID() == GUI.skin.GetHashCode())
|
||
{
|
||
Object pickedObject = EditorGUIUtility.GetObjectPickerObject();
|
||
if (pickedObject != null)
|
||
{
|
||
SerializedProperty targetListProp = serializedObject.FindProperty(_pickerTargetListName);
|
||
if (targetListProp != null)
|
||
{
|
||
bool exists = false;
|
||
for (int i = 0; i < targetListProp.arraySize; i++)
|
||
{
|
||
if (targetListProp.GetArrayElementAtIndex(i).stringValue == pickedObject.name)
|
||
{
|
||
exists = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!exists)
|
||
{
|
||
targetListProp.InsertArrayElementAtIndex(targetListProp.arraySize);
|
||
targetListProp.GetArrayElementAtIndex(targetListProp.arraySize - 1).stringValue = pickedObject.name;
|
||
}
|
||
}
|
||
|
||
_pickerTargetListName = null;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region List<string>选择器,通过类型查找资产,将其名称存储在列表中
|
||
public partial class DataEditor
|
||
{
|
||
/// <summary>
|
||
/// (旧功能) 从 EditorContentCollection 中选择
|
||
/// </summary>
|
||
private void DrawContentSelector(SerializedProperty targetListProp, string groupName, string label = null)
|
||
{
|
||
if (!targetListProp.isExpanded) return;
|
||
|
||
DrawGenericSelectorButton($"Add {groupName}...", targetListProp, () =>
|
||
{
|
||
// 1. 查找所有配置文件
|
||
var collections = FindAllModEditReferences();
|
||
|
||
if (collections == null || collections.Count == 0)
|
||
{
|
||
Debug.LogError("No 'EditorContentCollection' assets found in the project.");
|
||
return null;
|
||
}
|
||
|
||
// 2. 整合所有文件中的同名 Group 数据
|
||
List<string> aggregatedItems = new List<string>();
|
||
bool groupFound = false;
|
||
|
||
foreach (var collection in collections)
|
||
{
|
||
var items = collection.GetItems(groupName);
|
||
if (items != null && items.Count > 0)
|
||
{
|
||
aggregatedItems.AddRange(items);
|
||
groupFound = true;
|
||
}
|
||
}
|
||
|
||
if (!groupFound)
|
||
{
|
||
Debug.LogWarning($"Group '{groupName}' not found in any EditorContentCollection.");
|
||
return null;
|
||
}
|
||
|
||
// 3. 去重并返回 (使用 Linq 的 Distinct)
|
||
return aggregatedItems.Distinct().ToList();
|
||
}, $"Select {groupName}");
|
||
}
|
||
|
||
/// <summary>
|
||
/// (新功能) 从当前对象的另一个字段 (List<string> 或 Dictionary) 中选择
|
||
/// </summary>
|
||
/// <param name="targetListProp">要添加元素的目标 List</param>
|
||
/// <param name="sourceFieldName">数据源字段的变量名 (可以是 List<string> 或 Dictionary<string, T>)</param>
|
||
/// <param name="label">可选标签</param>
|
||
/// <param name="buttonLabel">可选按钮标签</param>
|
||
private void DrawLocalContentSelector(SerializedProperty targetListProp, string sourceFieldName, string label = null, string buttonLabel = null)
|
||
{
|
||
if (string.IsNullOrEmpty(buttonLabel)) buttonLabel = $"Add from {sourceFieldName}...";
|
||
|
||
DrawGenericSelectorButton(buttonLabel, targetListProp, () =>
|
||
{
|
||
// 数据获取逻辑:利用反射从当前对象内部查找
|
||
var targetObj = serializedObject.targetObject;
|
||
// 支持私有和公有字段
|
||
var field = targetObj.GetType().GetField(sourceFieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||
|
||
if (field != null)
|
||
{
|
||
var value = field.GetValue(targetObj);
|
||
if (value != null)
|
||
{
|
||
// 情况 1: 它是 Dictionary (SerializableDictionary 或 普通 Dictionary)
|
||
// 我们通过反射查找 "Keys" 属性,这样兼容性最好
|
||
var keysProp = value.GetType().GetProperty("Keys");
|
||
if (keysProp != null)
|
||
{
|
||
var keys = keysProp.GetValue(value) as System.Collections.IEnumerable;
|
||
if (keys != null)
|
||
{
|
||
// 将所有 Key 转为 string 返回
|
||
return keys.Cast<object>().Select(k => k.ToString()).ToList();
|
||
}
|
||
}
|
||
|
||
// 情况 2: 它是 List<string> 或 string[]
|
||
if (value is IEnumerable<string> list)
|
||
{
|
||
return list.ToList();
|
||
}
|
||
}
|
||
}
|
||
|
||
Debug.LogWarning($"Could not find valid string list or dictionary keys in field '{sourceFieldName}'.");
|
||
return null;
|
||
}, $"Select from {sourceFieldName}");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 这是一个私有的通用辅助方法,处理按钮绘制和窗口调用
|
||
/// </summary>
|
||
private void DrawGenericSelectorButton(string buttonText, SerializedProperty targetListProp, Func<List<string>> dataProvider, string windowTitle)
|
||
{
|
||
EditorGUILayout.BeginHorizontal();
|
||
// 仅仅为了排版美观的占位
|
||
// EditorGUILayout.LabelField("", GUILayout.Width(0));
|
||
|
||
if (GUILayout.Button(buttonText, GUILayout.Height(20)))
|
||
{
|
||
// 1. 获取数据 (执行传入的逻辑)
|
||
List<string> items = dataProvider?.Invoke();
|
||
|
||
if (items != null && items.Count > 0)
|
||
{
|
||
// 2. 打开通用窗口
|
||
ContentSelectorWindow.Show(windowTitle, items, (selectedItem) =>
|
||
{
|
||
// 3. 添加到列表的通用逻辑
|
||
bool alreadyExists = false;
|
||
for (int i = 0; i < targetListProp.arraySize; i++)
|
||
{
|
||
if (targetListProp.GetArrayElementAtIndex(i).stringValue == selectedItem)
|
||
{
|
||
alreadyExists = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!alreadyExists)
|
||
{
|
||
targetListProp.InsertArrayElementAtIndex(targetListProp.arraySize);
|
||
targetListProp.GetArrayElementAtIndex(targetListProp.arraySize - 1).stringValue = selectedItem;
|
||
serializedObject.ApplyModifiedProperties();
|
||
}
|
||
else
|
||
{
|
||
Debug.LogWarning($"Item '{selectedItem}' already exists.");
|
||
}
|
||
});
|
||
}
|
||
}
|
||
EditorGUILayout.EndHorizontal();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 组合绘制:默认列表UI + 本地源选择按钮
|
||
/// </summary>
|
||
protected void DrawListWithLocalSelector(SerializedProperty listProp, string sourceFieldName, string label = null)
|
||
{
|
||
if (label != null) EditorGUILayout.PropertyField(listProp, new GUIContent(label), true);
|
||
else EditorGUILayout.PropertyField(listProp, true);
|
||
|
||
if (listProp.isExpanded)
|
||
{
|
||
DrawLocalContentSelector(listProp, sourceFieldName);
|
||
}
|
||
//EditorGUILayout.Space();
|
||
}
|
||
|
||
// (DrawListWithContentSelector 保持不变,可以继续使用)
|
||
protected void DrawListWithEditRefSelector(SerializedProperty listProp, string groupName, string label = null)
|
||
{
|
||
if (label != null) EditorGUILayout.PropertyField(listProp, new GUIContent(label), true);
|
||
else EditorGUILayout.PropertyField(listProp, true);
|
||
|
||
if (listProp.isExpanded)
|
||
{
|
||
DrawContentSelector(listProp, groupName);
|
||
}
|
||
//EditorGUILayout.Space();
|
||
}
|
||
|
||
private static List<ModEditReference> FindAllModEditReferences()
|
||
{
|
||
string[] guids = AssetDatabase.FindAssets("t:ModEditReference");
|
||
List<ModEditReference> results = new List<ModEditReference>();
|
||
|
||
foreach (var guid in guids)
|
||
{
|
||
var asset = AssetDatabase.LoadAssetAtPath<ModEditReference>(AssetDatabase.GUIDToAssetPath(guid));
|
||
if (asset != null)
|
||
{
|
||
results.Add(asset);
|
||
}
|
||
}
|
||
return results;
|
||
}
|
||
|
||
// --- 重构后的通用窗口 ---
|
||
private class ContentSelectorWindow : EditorWindow
|
||
{
|
||
private Action<string> _onSelectCallback;
|
||
private List<string> _allItems; // 数据直接从外部传入
|
||
private List<string> _filteredItems;
|
||
private string _searchString = "";
|
||
private Vector2 _scrollPos;
|
||
|
||
// Show 方法现在的签名变得更通用了,不再绑定 groupName
|
||
public static void Show(string title, List<string> items, Action<string> onSelect)
|
||
{
|
||
var window = GetWindow<ContentSelectorWindow>(true, title, true);
|
||
window.minSize = new Vector2(250, 300);
|
||
window._onSelectCallback = onSelect;
|
||
window._allItems = items; // 直接接收数据
|
||
window.FilterList();
|
||
}
|
||
|
||
private void OnGUI()
|
||
{
|
||
GUILayout.BeginHorizontal(EditorStyles.toolbar);
|
||
EditorGUI.BeginChangeCheck();
|
||
_searchString = GUILayout.TextField(_searchString, GUI.skin.FindStyle("ToolbarSearchTextField"));
|
||
if (GUILayout.Button("", GUI.skin.FindStyle("ToolbarSearchCancelButton"))) _searchString = "";
|
||
if (EditorGUI.EndChangeCheck()) FilterList();
|
||
GUILayout.EndHorizontal();
|
||
|
||
_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos);
|
||
if (_filteredItems != null)
|
||
{
|
||
foreach (var item in _filteredItems)
|
||
{
|
||
if (GUILayout.Button(item, EditorStyles.label))
|
||
{
|
||
_onSelectCallback?.Invoke(item);
|
||
this.Close(); // 可选:点击后关闭
|
||
}
|
||
}
|
||
}
|
||
EditorGUILayout.EndScrollView();
|
||
}
|
||
|
||
private void FilterList()
|
||
{
|
||
if (string.IsNullOrEmpty(_searchString)) _filteredItems = new List<string>(_allItems);
|
||
else _filteredItems = _allItems.Where(i => i.IndexOf(_searchString, StringComparison.OrdinalIgnoreCase) >= 0).ToList();
|
||
}
|
||
}
|
||
}
|
||
#endregion
|
||
|
||
#region Searchable Type选择器,将获取的类型名存入一个string中
|
||
|
||
public partial class DataEditor
|
||
{
|
||
private static Dictionary<Type, (string[] paths, Type[] types)> _typeCache =
|
||
new Dictionary<Type, (string[], Type[])>();
|
||
|
||
/// <summary>
|
||
/// 绘制一个带 "Select" 按钮的字段,点击后会弹出可搜索的类型选择窗口。
|
||
/// </summary>
|
||
/// <param name="classNameProp">存储类名的字符串属性</param>
|
||
/// <param name="label">在Inspector中显示的标签</param>
|
||
/// <param name="baseType">要查找的基类</param>
|
||
/// <param name="onTypeSelected">【关键】当一个类型被选中时执行的回调</param>
|
||
/// <param name="namespacePrefix">要搜索的命名空间前缀</param>
|
||
/// <param name="namespaceToRemove">要从路径中移除的命名空间部分</param>
|
||
protected void DrawSearchableTypeSelector(
|
||
SerializedProperty classNameProp,
|
||
string label,
|
||
Type baseType,
|
||
Action<Type> onTypeSelected,
|
||
string namespacePrefix = null,
|
||
string namespaceToRemove = null)
|
||
{
|
||
EditorGUILayout.BeginHorizontal();
|
||
EditorGUILayout.TextField(label, classNameProp.stringValue);
|
||
|
||
if (GUILayout.Button("Select", GUILayout.Width(60)))
|
||
{
|
||
TypeSelectorWindow.Show(baseType, namespacePrefix, namespaceToRemove, (selectedType) =>
|
||
{
|
||
classNameProp.stringValue = selectedType.Name;
|
||
serializedObject.ApplyModifiedProperties();
|
||
onTypeSelected?.Invoke(selectedType);
|
||
});
|
||
}
|
||
EditorGUILayout.EndHorizontal();
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 弹出式搜索窗口(作为私有内部类)
|
||
/// </summary>
|
||
private class TypeSelectorWindow : EditorWindow
|
||
{
|
||
// --- 核心修改 1:新的树状数据结构 ---
|
||
private class CategoryNode
|
||
{
|
||
public string Name;
|
||
public bool IsExpanded = false;
|
||
public Dictionary<string, CategoryNode> SubCategories = new Dictionary<string, CategoryNode>();
|
||
public List<(string name, Type type)> Items = new List<(string, Type)>();
|
||
}
|
||
|
||
private static Dictionary<Tuple<Type, string, string>, (string[] paths, Type[] types)> _staticTypeCache =
|
||
new Dictionary<Tuple<Type, string, string>, (string[], Type[])>();
|
||
|
||
private CategoryNode _rootNode = new CategoryNode { Name = "Root", IsExpanded = true };
|
||
private Action<Type> _onSelectCallback;
|
||
private Type _baseType;
|
||
private string _namespacePrefix;
|
||
private string _namespaceToRemove;
|
||
|
||
private string _searchString = "";
|
||
private Vector2 _scrollPos;
|
||
|
||
private Dictionary<string, List<(string path, Type type)>> _groupedFilteredList;
|
||
private Dictionary<string, bool> _categoryFoldoutStates = new Dictionary<string, bool>();
|
||
|
||
public static void Show(Type baseType, string namespacePrefix, string namespaceToRemove, Action<Type> onSelect)
|
||
{
|
||
var window = GetWindow<TypeSelectorWindow>(true, "Select Type", true);
|
||
window.minSize = new Vector2(300, 400);
|
||
window._baseType = baseType;
|
||
window._namespacePrefix = namespacePrefix;
|
||
window._namespaceToRemove = namespaceToRemove;
|
||
window._onSelectCallback = onSelect;
|
||
window.FilterList();
|
||
}
|
||
|
||
private void OnGUI()
|
||
{
|
||
GUILayout.BeginHorizontal(EditorStyles.toolbar);
|
||
EditorGUI.BeginChangeCheck();
|
||
_searchString = GUILayout.TextField(_searchString, GUI.skin.FindStyle("ToolbarSearchTextField"));
|
||
if (GUILayout.Button("", GUI.skin.FindStyle("ToolbarSearchCancelButton"))) _searchString = "";
|
||
if (EditorGUI.EndChangeCheck())
|
||
{
|
||
FilterList();
|
||
}
|
||
GUILayout.EndHorizontal();
|
||
|
||
_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos);
|
||
if (_rootNode.SubCategories.Count == 0 && _rootNode.Items.Count == 0)
|
||
{
|
||
EditorGUILayout.LabelField("No matching types found.");
|
||
}
|
||
else
|
||
{
|
||
DrawNodeGUI(_rootNode, 0);
|
||
}
|
||
EditorGUILayout.EndScrollView();
|
||
}
|
||
|
||
// --- 核心修改 1:重写 CacheDerivedTypes,使其更健壮 ---
|
||
private void CacheDerivedTypes()
|
||
{
|
||
var cacheKey = new Tuple<Type, string, string>(_baseType, _namespacePrefix ?? string.Empty, _namespaceToRemove ?? string.Empty);
|
||
if (_staticTypeCache.ContainsKey(cacheKey)) return;
|
||
|
||
List<(string path, Type type)> typeList = new List<(string, Type)>();
|
||
|
||
IEnumerable<Type> types = AppDomain.CurrentDomain.GetAssemblies()
|
||
.SelectMany(assembly => assembly.GetTypes())
|
||
.Where(t => _baseType.IsAssignableFrom(t) && !t.IsAbstract && !t.IsInterface && t != _baseType);
|
||
|
||
foreach (var type in types)
|
||
{
|
||
string path;
|
||
if (!string.IsNullOrEmpty(_namespacePrefix) && type.Namespace != null && type.Namespace.StartsWith(_namespacePrefix))
|
||
{
|
||
// 1. 获取前缀后的命名空间
|
||
// e.g., ".Basic.Cards.General.Skills"
|
||
string formattedNamespace = type.Namespace.Substring(_namespacePrefix.Length);
|
||
|
||
// 2. 按 '.' 拆分为段落,并移除空条目 (比如开头的那个)
|
||
List<string> segments = formattedNamespace.Split('.').Where(s => !string.IsNullOrEmpty(s)).ToList();
|
||
|
||
// 3. 安全地移除指定的 'namespaceToRemove' 段落
|
||
if (!string.IsNullOrEmpty(_namespaceToRemove))
|
||
{
|
||
segments.Remove(_namespaceToRemove);
|
||
}
|
||
|
||
// 4. 用 '/' 重新组合路径
|
||
if (segments.Count > 0)
|
||
{
|
||
path = string.Join("/", segments) + "/" + type.Name;
|
||
}
|
||
else
|
||
{
|
||
path = type.Name; // 如果没有剩余段落,则直接使用类名
|
||
}
|
||
}
|
||
else
|
||
{
|
||
path = "Uncategorized/" + type.Name;
|
||
}
|
||
typeList.Add((path, type));
|
||
}
|
||
typeList.Sort((a, b) => a.path.CompareTo(b.path));
|
||
_staticTypeCache[cacheKey] = (typeList.Select(t => t.path).ToArray(), typeList.Select(t => t.type).ToArray());
|
||
}
|
||
|
||
private void FilterList()
|
||
{
|
||
var cacheKey = new Tuple<Type, string, string>(_baseType, _namespacePrefix ?? string.Empty, _namespaceToRemove ?? string.Empty);
|
||
if (!_staticTypeCache.ContainsKey(cacheKey))
|
||
{
|
||
CacheDerivedTypes();
|
||
}
|
||
|
||
var (allPaths, allTypes) = _staticTypeCache[cacheKey];
|
||
|
||
// 1. 重置根节点
|
||
_rootNode = new CategoryNode { Name = "Root", IsExpanded = true };
|
||
|
||
for (int i = 0; i < allPaths.Length; i++)
|
||
{
|
||
string fullPath = allPaths[i];
|
||
Type type = allTypes[i];
|
||
|
||
// 2. 检查是否匹配搜索词
|
||
if (string.IsNullOrEmpty(_searchString) ||
|
||
fullPath.IndexOf(_searchString, StringComparison.OrdinalIgnoreCase) >= 0)
|
||
{
|
||
var segments = fullPath.Split('/');
|
||
CategoryNode currentNode = _rootNode;
|
||
|
||
// 3. 遍历所有“目录”部分,构建树
|
||
for (int j = 0; j < segments.Length - 1; j++)
|
||
{
|
||
string categoryName = segments[j];
|
||
if (!currentNode.SubCategories.ContainsKey(categoryName))
|
||
{
|
||
var newNode = new CategoryNode { Name = categoryName };
|
||
// 如果在搜索,自动展开所有匹配路径上的节点
|
||
if (!string.IsNullOrEmpty(_searchString))
|
||
{
|
||
newNode.IsExpanded = true;
|
||
}
|
||
currentNode.SubCategories[categoryName] = newNode;
|
||
}
|
||
currentNode = currentNode.SubCategories[categoryName];
|
||
}
|
||
|
||
// 4. 将“文件”部分(即类名)添加到它所属的节点
|
||
string itemName = segments.Last();
|
||
currentNode.Items.Add((itemName, type));
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- 核心修改 4:全新的递归绘制方法 ---
|
||
private void DrawNodeGUI(CategoryNode node, int indentLevel)
|
||
{
|
||
// 1. 绘制所有子类别(作为可折叠的Foldouts)
|
||
foreach (var subCategory in node.SubCategories.Values.OrderBy(c => c.Name))
|
||
{
|
||
// 设置当前层级的缩进
|
||
EditorGUI.indentLevel = indentLevel;
|
||
|
||
// 使用我们自定义的、带三角箭头的粗体样式
|
||
subCategory.IsExpanded = EditorGUILayout.Foldout(subCategory.IsExpanded, subCategory.Name, true);
|
||
|
||
if (subCategory.IsExpanded)
|
||
{
|
||
// 递归调用,绘制子节点的内容,缩进+1
|
||
DrawNodeGUI(subCategory, indentLevel + 1);
|
||
}
|
||
}
|
||
|
||
EditorGUI.indentLevel = indentLevel;
|
||
foreach (var (name, type) in node.Items.OrderBy(i => i.name))
|
||
{
|
||
// B. 从布局系统中获取一个**已经正确缩进**的矩形区域
|
||
Rect controlRect = EditorGUILayout.GetControlRect();
|
||
|
||
// C. 在这个矩形区域内绘制标签,它会自动使用我们设置的缩进
|
||
// 我们使用 _clickableLabelStyle 来实现悬停变色效果
|
||
EditorGUI.LabelField(controlRect, name);
|
||
|
||
// D. 为这个矩形区域添加一个可点击的光标提示
|
||
EditorGUIUtility.AddCursorRect(controlRect, MouseCursor.Link);
|
||
|
||
// E. 手动检查在这个矩形区域内的点击事件
|
||
if (Event.current.type == EventType.MouseDown && Event.current.button == 0 &&
|
||
controlRect.Contains(Event.current.mousePosition))
|
||
{
|
||
_onSelectCallback?.Invoke(type);
|
||
this.Close();
|
||
Event.current.Use(); // 消耗掉这个点击事件
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 绘制一个按钮,点击后调用指定方法
|
||
public partial class DataEditor
|
||
{
|
||
protected void DrawMethodButton<T>(string buttonLabel, string functionName) where T : Object
|
||
{
|
||
if (GUILayout.Button(buttonLabel))
|
||
{
|
||
T invoker = target as T;
|
||
MethodInfo method = typeof(T).GetMethod(functionName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||
if (method != null)
|
||
{
|
||
method.Invoke(invoker, null);
|
||
}
|
||
else
|
||
{
|
||
Debug.LogWarning($"Method '{functionName}' not found in type '{typeof(T).Name}'.");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
#endregion
|
||
}
|
||
#endif |