Files
Continentis/Assets/Scripts/ScriptExtensions/UModAssistance/Editor/DataEditor.cs
SoulliesOfficial 27af2b7eb2 小修
2025-11-30 21:22:39 -05:00

649 lines
28 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 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