278 lines
10 KiB
C#
278 lines
10 KiB
C#
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<string, string> _displayToPath = new();
|
|
|
|
[MenuItem(AibisEditorMenus.ChapterGraphEditor)]
|
|
public static void ShowWindow()
|
|
{
|
|
var window = GetWindow<ChapterGraphEditorWindow>("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<MouseDownEvent>(evt =>
|
|
{
|
|
startWidth = leftPanel.resolvedStyle.width;
|
|
startMouseX = evt.mousePosition.x;
|
|
resizer.CaptureMouse();
|
|
evt.StopPropagation();
|
|
});
|
|
resizer.RegisterCallback<MouseMoveEvent>(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<MouseUpEvent>(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<string> 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);
|
|
}
|
|
}
|
|
}
|