#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选择器,通过类型查找资产,将其名称存储在列表中 public partial class DataEditor : Editor { private string _pickerTargetListName; private Dictionary _assetCache; protected virtual void OnEnable() { // 每次选中新对象时,都清空缓存 _assetCache = new Dictionary(); } protected void DrawCharacterListGUI(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(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(null, false, searchFilter, GUI.skin.GetHashCode()); } EditorGUI.indentLevel--; } } protected T FindObjectData(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(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选择器,通过类型查找资产,将其名称存储在列表中 public partial class DataEditor { /// /// (旧功能) 从 EditorContentCollection 中选择 /// 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 aggregatedItems = new List(); 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}"); } /// /// (新功能) 从当前对象的另一个字段 (List 或 Dictionary) 中选择 /// /// 要添加元素的目标 List /// 数据源字段的变量名 (可以是 List 或 Dictionary) /// 可选标签 /// 可选按钮标签 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().Select(k => k.ToString()).ToList(); } } // 情况 2: 它是 List 或 string[] if (value is IEnumerable list) { return list.ToList(); } } } Debug.LogWarning($"Could not find valid string list or dictionary keys in field '{sourceFieldName}'."); return null; }, $"Select from {sourceFieldName}"); } /// /// 这是一个私有的通用辅助方法,处理按钮绘制和窗口调用 /// private void DrawGenericSelectorButton(string buttonText, SerializedProperty targetListProp, Func> dataProvider, string windowTitle) { EditorGUILayout.BeginHorizontal(); // 仅仅为了排版美观的占位 // EditorGUILayout.LabelField("", GUILayout.Width(0)); if (GUILayout.Button(buttonText, GUILayout.Height(20))) { // 1. 获取数据 (执行传入的逻辑) List 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(); } /// /// 组合绘制:默认列表UI + 本地源选择按钮 /// 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 FindAllModEditReferences() { string[] guids = AssetDatabase.FindAssets("t:ModEditReference"); List results = new List(); foreach (var guid in guids) { var asset = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guid)); if (asset != null) { results.Add(asset); } } return results; } // --- 重构后的通用窗口 --- private class ContentSelectorWindow : EditorWindow { private Action _onSelectCallback; private List _allItems; // 数据直接从外部传入 private List _filteredItems; private string _searchString = ""; private Vector2 _scrollPos; // Show 方法现在的签名变得更通用了,不再绑定 groupName public static void Show(string title, List items, Action onSelect) { var window = GetWindow(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(_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 _typeCache = new Dictionary(); /// /// 绘制一个带 "Select" 按钮的字段,点击后会弹出可搜索的类型选择窗口。 /// /// 存储类名的字符串属性 /// 在Inspector中显示的标签 /// 要查找的基类 /// 【关键】当一个类型被选中时执行的回调 /// 要搜索的命名空间前缀 /// 要从路径中移除的命名空间部分 protected void DrawSearchableTypeSelector( SerializedProperty classNameProp, string label, Type baseType, Action 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(); } /// /// 弹出式搜索窗口(作为私有内部类) /// private class TypeSelectorWindow : EditorWindow { // --- 核心修改 1:新的树状数据结构 --- private class CategoryNode { public string Name; public bool IsExpanded = false; public Dictionary SubCategories = new Dictionary(); public List<(string name, Type type)> Items = new List<(string, Type)>(); } private static Dictionary, (string[] paths, Type[] types)> _staticTypeCache = new Dictionary, (string[], Type[])>(); private CategoryNode _rootNode = new CategoryNode { Name = "Root", IsExpanded = true }; private Action _onSelectCallback; private Type _baseType; private string _namespacePrefix; private string _namespaceToRemove; private string _searchString = ""; private Vector2 _scrollPos; private Dictionary> _groupedFilteredList; private Dictionary _categoryFoldoutStates = new Dictionary(); public static void Show(Type baseType, string namespacePrefix, string namespaceToRemove, Action onSelect) { var window = GetWindow(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(_baseType, _namespacePrefix ?? string.Empty, _namespaceToRemove ?? string.Empty); if (_staticTypeCache.ContainsKey(cacheKey)) return; List<(string path, Type type)> typeList = new List<(string, Type)>(); IEnumerable 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 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(_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(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