DataEditor & StorySystem Graph
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using UnityEditor;
|
||||
@@ -135,92 +136,251 @@ namespace SLSFramework.UModAssistance
|
||||
|
||||
#endregion
|
||||
|
||||
#region Type选择器,将获取的类型名存入一个string中
|
||||
#region Searchable Type选择器,将获取的类型名存入一个string中
|
||||
|
||||
public partial class DataEditor
|
||||
{
|
||||
private static Dictionary<Tuple<Type, string>, (string[] paths, Type[] types)> _typeCache =
|
||||
new Dictionary<Tuple<Type, string>, (string[], Type[])>();
|
||||
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">要查找的基类 (例如 typeof(CardLogicBase))</param>
|
||||
/// <param name="namespaceToRemove">可选参数,用于从路径中移除特定的命名空间部分 (例如 ".Cards")</param>
|
||||
/// <returns>如果值被用户改变,则返回true</returns>
|
||||
protected bool DrawTypeSelectorGUI(SerializedProperty classNameProp, string label, Type baseType,
|
||||
out Type returnedType, string namespacePrefix = null, string namespaceToRemove = null)
|
||||
/// <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)
|
||||
{
|
||||
// --- 核心修改 2:使用包含 namespaceToRemove 的复合键 ---
|
||||
var cacheKey = new Tuple<Type, string>(baseType, namespaceToRemove ?? string.Empty);
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
EditorGUILayout.TextField(label, classNameProp.stringValue);
|
||||
|
||||
if (!_typeCache.ContainsKey(cacheKey))
|
||||
if (GUILayout.Button("Select", GUILayout.Width(60)))
|
||||
{
|
||||
CacheDerivedTypes(baseType, namespacePrefix, namespaceToRemove, cacheKey);
|
||||
}
|
||||
|
||||
var (paths, types) = _typeCache[cacheKey];
|
||||
|
||||
int currentIndex = -1;
|
||||
for (int i = 0; i < types.Length; i++)
|
||||
{
|
||||
if (types[i].Name == classNameProp.stringValue)
|
||||
TypeSelectorWindow.Show(baseType, namespacePrefix, namespaceToRemove, (selectedType) =>
|
||||
{
|
||||
currentIndex = i;
|
||||
break;
|
||||
}
|
||||
classNameProp.stringValue = selectedType.Name;
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
onTypeSelected?.Invoke(selectedType);
|
||||
});
|
||||
}
|
||||
|
||||
int newIndex = EditorGUILayout.Popup(label, currentIndex, paths);
|
||||
|
||||
if (newIndex != currentIndex)
|
||||
{
|
||||
classNameProp.stringValue = (newIndex >= 0 && newIndex < types.Length)
|
||||
? types[newIndex].Name
|
||||
: string.Empty;
|
||||
|
||||
returnedType = types[newIndex];
|
||||
return true;
|
||||
}
|
||||
|
||||
returnedType = null;
|
||||
return false;
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
private void CacheDerivedTypes(Type baseType, string namespacePrefix, string namespaceToRemove, Tuple<Type, string> cacheKey)
|
||||
|
||||
/// <summary>
|
||||
/// 弹出式搜索窗口(作为私有内部类)
|
||||
/// </summary>
|
||||
private class TypeSelectorWindow : EditorWindow
|
||||
{
|
||||
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)
|
||||
// --- 核心修改 1:新的树状数据结构 ---
|
||||
private class CategoryNode
|
||||
{
|
||||
string path = "Uncategorized/" + type.Name;
|
||||
if (type.Namespace != null && type.Namespace.StartsWith(namespacePrefix))
|
||||
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())
|
||||
{
|
||||
string formattedNamespace = type.Namespace.Substring(namespacePrefix.Length);
|
||||
|
||||
// --- 核心修改 3:使用传入的参数来替换硬编码 ---
|
||||
if (!string.IsNullOrEmpty(namespaceToRemove))
|
||||
{
|
||||
formattedNamespace = formattedNamespace.Replace("." + namespaceToRemove, "");
|
||||
}
|
||||
|
||||
formattedNamespace = formattedNamespace.Replace('.', '/');
|
||||
if (formattedNamespace.StartsWith("/")) formattedNamespace = formattedNamespace.Substring(1);
|
||||
path = formattedNamespace + "/" + type.Name;
|
||||
FilterList();
|
||||
}
|
||||
|
||||
typeList.Add((path, type));
|
||||
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();
|
||||
}
|
||||
|
||||
typeList.Sort((a, b) => a.path.CompareTo(b.path));
|
||||
// --- 核心修改 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)>();
|
||||
|
||||
_typeCache[cacheKey] = (typeList.Select(t => t.path).ToArray(), typeList.Select(t => t.type).ToArray());
|
||||
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(); // 消耗掉这个点击事件
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user