Files
Continentis/Assets/Scripts/ScriptExtensions/UModAssistance/Editor/DataEditor.cs
2025-11-10 11:18:19 -05:00

411 lines
18 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
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 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