using System.Collections.Generic; using System.IO; using System.Linq; using AibisDream; using UnityEditor; using UnityEditor.UIElements; using UnityEngine; using UnityEngine.UIElements; namespace AibisDream.SystemEditor { public class ChapterGraphEditorWindow : EditorWindow { private ChapterGraphView _graphView; private ChapterGraphInspector _inspector; private IMGUIContainer _inspectorContainer; private const string RootFolderPrefKey = "AibisDream.ChapterGraph.RootFolder"; private const string CurrentFolderPrefKey = "AibisDream.ChapterGraph.CurrentFolder"; private string _rootFolder; private string _currentFolder; private DropdownField _folderDropdown; private readonly Dictionary _displayToPath = new(); [MenuItem(AibisEditorMenus.ChapterGraphEditor)] public static void ShowWindow() { var window = GetWindow("Chapter Graph Editor"); window.minSize = new Vector2(800, 600); } private void OnEnable() { if (_graphView == null) return; _graphView.LoadGraph(_currentFolder); } private void OnDisable() { if (_graphView != null) { _graphView.OnNodeDeleted -= OnNodeDeleted; } if (_inspector != null) { _inspector.OnColorChanged -= OnNodeColorChanged; _inspector.Destroy(); } } private void CreateGUI() { // 根容器:纵向排列 var root = rootVisualElement; root.style.flexDirection = FlexDirection.Column; // 顶部工具栏 var toolbar = new Toolbar(); toolbar.Add(CreateToolbarButton("Save", OnSaveClicked)); toolbar.Add(CreateToolbarButton("Auto Layout", OnAutoLayoutClicked)); toolbar.Add(CreateToolbarButton("Validate", OnValidateClicked)); _rootFolder = EditorPrefs.GetString(RootFolderPrefKey, "Assets/ScriptableObjects/SceneSO"); _folderDropdown = new DropdownField("路径"); _folderDropdown.style.width = 160; _folderDropdown.style.marginLeft = 8; _folderDropdown.style.marginRight = 4; RefreshFolderChoices(); _folderDropdown.RegisterValueChangedCallback(OnFolderChanged); toolbar.Add(_folderDropdown); var setRootBtn = new Button(OnSetRootFolderClicked) { text = "..." }; setRootBtn.style.width = 28; toolbar.Add(setRootBtn); toolbar.Add(new ToolbarSpacer() { style = { flexGrow = 1 } }); toolbar.Add(CreateToolbarButton("Create Chapter", OnCreateChapterClicked)); root.Add(toolbar); // 主内容区:横向分栏 var mainContainer = new VisualElement(); mainContainer.style.flexGrow = 1; mainContainer.style.flexDirection = FlexDirection.Row; mainContainer.style.overflow = Overflow.Hidden; // 左侧 Inspector 面板 var leftPanel = new VisualElement(); leftPanel.style.width = 320; leftPanel.style.minWidth = 200; leftPanel.style.maxWidth = 500; leftPanel.style.flexShrink = 0; leftPanel.style.flexDirection = FlexDirection.Column; leftPanel.style.borderRightWidth = 1; leftPanel.style.borderRightColor = new StyleColor(new Color(0.15f, 0.15f, 0.15f)); // Inspector 标题 var header = new Label("章节属性") { style = { unityFontStyleAndWeight = FontStyle.Bold, fontSize = 14, paddingTop = 8, paddingBottom = 8, paddingLeft = 12, paddingRight = 12, borderBottomWidth = 1, borderBottomColor = new StyleColor(new Color(0.15f, 0.15f, 0.15f)) } }; leftPanel.Add(header); // IMGUI Inspector _inspector = new ChapterGraphInspector(); _inspectorContainer = new IMGUIContainer(() => _inspector.OnGUI()); _inspectorContainer.style.flexGrow = 1; leftPanel.Add(_inspectorContainer); mainContainer.Add(leftPanel); // 拖拽条 var resizer = new VisualElement(); resizer.style.width = 5; resizer.style.backgroundColor = new StyleColor(new Color(0.15f, 0.15f, 0.15f)); mainContainer.Add(resizer); float startWidth = 0; float startMouseX = 0; resizer.RegisterCallback(evt => { startWidth = leftPanel.resolvedStyle.width; startMouseX = evt.mousePosition.x; resizer.CaptureMouse(); evt.StopPropagation(); }); resizer.RegisterCallback(evt => { if (!resizer.HasMouseCapture()) return; var delta = evt.mousePosition.x - startMouseX; var newWidth = Mathf.Clamp(startWidth + delta, 200, 600); leftPanel.style.width = newWidth; }); resizer.RegisterCallback(evt => { if (resizer.HasMouseCapture()) resizer.ReleaseMouse(); }); // 右侧 GraphView // 套一层容器:GraphView 与左侧 Inspector 并列放在 Flex Row 中时, // 内置 RectangleSelector 绘制的框选矩形会出现坐标偏移(已知 Unity 问题)。 // 将 GraphView 放在独立的 Flex 子容器中可使其局部原点与父容器对齐,规避该偏移。 var graphViewWrapper = new VisualElement(); graphViewWrapper.style.flexGrow = 1; graphViewWrapper.style.flexDirection = FlexDirection.Column; graphViewWrapper.style.overflow = Overflow.Hidden; _graphView = new ChapterGraphView(); _graphView.style.flexGrow = 1; graphViewWrapper.Add(_graphView); mainContainer.Add(graphViewWrapper); root.Add(mainContainer); // 加载图数据 _graphView.LoadGraph(_currentFolder); // 选中节点同步到 Inspector _graphView.OnNodeSelected += OnNodeSelected; _graphView.OnNodeDeleted += OnNodeDeleted; _inspector.OnColorChanged += OnNodeColorChanged; } private void OnNodeDeleted(TalkSceneSO so) { _inspector?.SetTarget(null); _inspectorContainer?.MarkDirtyRepaint(); } private void OnNodeColorChanged(TalkSceneSO so) { _graphView?.UpdateNodeColor(so); } private Button CreateToolbarButton(string text, System.Action onClick) { var btn = new Button(onClick) { text = text }; return btn; } private void OnSaveClicked() { AssetDatabase.SaveAssets(); EditorUtility.DisplayDialog("保存", "所有资源已保存。", "确定"); } private void OnAutoLayoutClicked() { _graphView?.AutoLayout(); } private void OnValidateClicked() { _graphView?.ValidateGraph(); } private void OnCreateChapterClicked() { var center = new Vector2(position.width / 2f, position.height / 2f); var localPos = _graphView.contentViewContainer.WorldToLocal(center); var defaultFolder = _currentFolder ?? EditorPrefs.GetString(ChapterGraphView.DefaultFolderPrefKey, _rootFolder); _graphView?.CreateChapterNodeAt(localPos, defaultFolder); } private void OnNodeSelected(TalkSceneSO so) { _inspector?.SetTarget(so); _inspectorContainer?.MarkDirtyRepaint(); } private void RefreshFolderChoices() { _displayToPath.Clear(); _displayToPath["All"] = null; var subFolders = AssetDatabase.GetSubFolders(_rootFolder); foreach (var folder in subFolders) { var name = Path.GetFileName(folder); _displayToPath[name] = folder; } var rootHasSO = AssetDatabase.FindAssets("t:TalkSceneSO", new[] { _rootFolder }) .Select(g => AssetDatabase.GUIDToAssetPath(g)) .Any(path => Path.GetDirectoryName(path)?.Replace('\\', '/') == _rootFolder); if (rootHasSO) { _displayToPath["(Root)"] = _rootFolder; } _folderDropdown.choices = _displayToPath.Keys.ToList(); var saved = EditorPrefs.GetString(CurrentFolderPrefKey, "All"); _folderDropdown.value = _displayToPath.ContainsKey(saved) ? saved : "All"; _currentFolder = _displayToPath.TryGetValue(_folderDropdown.value, out var path) ? path : null; } private void OnFolderChanged(ChangeEvent evt) { _currentFolder = _displayToPath.TryGetValue(evt.newValue, out var path) ? path : null; EditorPrefs.SetString(CurrentFolderPrefKey, evt.newValue); _graphView?.LoadGraph(_currentFolder); } private void OnSetRootFolderClicked() { var path = EditorUtility.OpenFolderPanel("选择章节根目录", _rootFolder, ""); if (string.IsNullOrEmpty(path)) return; var absoluteRoot = Application.dataPath.Replace('\\', '/'); var selected = path.Replace('\\', '/'); if (!selected.StartsWith(absoluteRoot)) { EditorUtility.DisplayDialog("错误", "请选择项目 Assets 目录内的文件夹。", "确定"); return; } _rootFolder = "Assets" + selected.Substring(absoluteRoot.Length); EditorPrefs.SetString(RootFolderPrefKey, _rootFolder); RefreshFolderChoices(); _graphView?.LoadGraph(_currentFolder); } } }