Files
aibis-dream/Assets/Editor/DeveloperMode/DeveloperModeTestSavePanelBuilder.cs
T
2026-07-24 13:16:03 +08:00

461 lines
20 KiB
C#

#if UNITY_EDITOR
using System;
using System.Linq;
using AibisDream.UI;
using TMPro;
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;
namespace AibisDream.DeveloperMode.Editor
{
/// <summary>只重建 DeveloperModePanel 的测试存档页,避免覆盖其他页的人工调整。</summary>
public static class DeveloperModeTestSavePanelBuilder
{
private const string PrefabPath =
"Assets/GameContent/Feature_MainUI/Prefabs/DeveloperModePanel.prefab";
private const string FontAssetPath =
"Assets/Font/Assets/WenQuanYi Bitmap Song 14px SDF.asset";
private static readonly Color Surface = new(0.055f, 0.085f, 0.11f, 0.96f);
private static readonly Color Accent = new(0.16f, 0.75f, 0.88f, 1f);
private static readonly Color TextColor = new(0.88f, 0.96f, 1f, 1f);
[MenuItem("Tools/AIBIS/Developer Mode/Rebuild Test Save Page")]
public static void BuildFromMenu()
{
Build();
Selection.activeObject = AssetDatabase.LoadAssetAtPath<GameObject>(PrefabPath);
}
public static void BuildFromCommandLine()
{
try
{
Build();
Debug.Log("[DeveloperModeTestSavePanelBuilder] Build completed.");
}
catch (Exception ex)
{
Debug.LogException(ex);
EditorApplication.Exit(1);
}
}
private static void Build()
{
var root = PrefabUtility.LoadPrefabContents(PrefabPath);
try
{
var panel = root.GetComponent<DeveloperModePanel>()
?? throw new InvalidOperationException("DeveloperModePanel component not found.");
var serialized = new SerializedObject(panel);
var pages = serialized.FindProperty("tabPages")
?? throw new MissingFieldException(nameof(DeveloperModePanel), "tabPages");
if (pages.arraySize < 4)
{
throw new InvalidOperationException("DeveloperModePanel requires five tab pages.");
}
var savePage = pages.GetArrayElementAtIndex(3).objectReferenceValue as GameObject
?? throw new InvalidOperationException("Save page reference is missing.");
ClearChildren(savePage.transform);
ConfigurePageLayout(savePage);
BuildSavePage(savePage.transform, serialized);
RenameSaveTab(serialized);
var font = AssetDatabase.LoadAssetAtPath<TMP_FontAsset>(FontAssetPath)
?? throw new InvalidOperationException($"Font not found: {FontAssetPath}");
foreach (var text in root.GetComponentsInChildren<TMP_Text>(true))
{
text.font = font;
}
serialized.ApplyModifiedPropertiesWithoutUndo();
PrefabUtility.SaveAsPrefabAsset(root, PrefabPath);
}
finally
{
PrefabUtility.UnloadPrefabContents(root);
}
AssetDatabase.SaveAssets();
}
private static void BuildSavePage(Transform page, SerializedObject serialized)
{
var recording = CreateHorizontal("Recording Toolbar", page, 6f);
AddLayout(recording, 38f);
var recordingToggle = CreateToggle("Record Test Saves", recording.transform, "录制测试存档", 145f);
var recordingState = CreateText("Recording State", recording.transform, "○ OFF", 14f, FontStyles.Bold);
AddLayout(recordingState.gameObject, 34f);
recordingState.rectTransform.sizeDelta = new Vector2(105f, 0f);
var search = CreateInput("Test Save Search", recording.transform, "搜索章节、节点或场景...", 250f);
var reload = CreateButton("Reload Test Saves", recording.transform, "重新扫描", 92f, 34f);
var open = CreateButton("Open Test Saves", recording.transform, "打开目录", 92f, 34f);
var columns = CreateHorizontal("Library Columns", page, 7f);
var columnsLayout = columns.AddComponent<LayoutElement>();
columnsLayout.flexibleHeight = 1f;
columnsLayout.minHeight = 420f;
var chapters = CreateVertical("Chapter Column", columns.transform, 4f);
var chapterLayout = chapters.AddComponent<LayoutElement>();
chapterLayout.preferredWidth = 245f;
chapterLayout.flexibleWidth = 0f;
chapterLayout.flexibleHeight = 1f;
var chapterTitle = CreateText("Title", chapters.transform, "章节 / 覆盖率", 16f, FontStyles.Bold);
AddLayout(chapterTitle.gameObject, 28f);
CreateList(
"Chapter List",
chapters.transform,
out var chapterContent,
out var chapterTemplate);
var entries = CreateVertical("Entry Column", columns.transform, 4f);
var entryLayout = entries.AddComponent<LayoutElement>();
entryLayout.flexibleWidth = 1f;
entryLayout.flexibleHeight = 1f;
var entryTitle = CreateText(
"Title",
entries.transform,
"存档点(● 已录制 / ○ 缺失 / ✕ 无效)",
16f,
FontStyles.Bold);
AddLayout(entryTitle.gameObject, 28f);
CreateList(
"Entry List",
entries.transform,
out var entryContent,
out var entryTemplate);
var actions = CreateHorizontal("Actions", page, 6f);
AddLayout(actions, 36f);
var jump = CreateButton("Jump", actions.transform, "跳转到选中", 118f, 32f);
var deleteSelected = CreateButton("Delete Selected", actions.transform, "删除选中", 105f, 32f);
var deleteInvalid = CreateButton("Delete Invalid", actions.transform, "清理无效", 105f, 32f);
var clear = CreateButton("Clear Library", actions.transform, "清空存档库", 118f, 32f);
var status = CreateText(
"Save Status",
page,
"测试存档录制默认关闭;开启后仅复制正式节点自动档。",
14f);
AddLayout(status.gameObject, 46f);
status.color = new Color(0.45f, 1f, 0.65f);
status.alignment = TextAlignmentOptions.TopLeft;
Set(serialized, "testSaveRecordingToggle", recordingToggle);
Set(serialized, "testSaveRecordingStateText", recordingState);
Set(serialized, "testSaveSearchInput", search);
Set(serialized, "reloadSavesButton", reload.GetComponent<Button>());
Set(serialized, "openTestSaveFolderButton", open.GetComponent<Button>());
Set(serialized, "jumpToTestSaveButton", jump.GetComponent<Button>());
Set(serialized, "deleteSelectedTestSaveButton", deleteSelected.GetComponent<Button>());
Set(serialized, "deleteInvalidTestSavesButton", deleteInvalid.GetComponent<Button>());
Set(serialized, "clearTestSaveLibraryButton", clear.GetComponent<Button>());
Set(serialized, "saveStatusText", status);
Set(serialized, "testSaveChapterContent", chapterContent);
Set(serialized, "testSaveChapterRowTemplate", chapterTemplate);
Set(serialized, "testSaveEntryContent", entryContent);
Set(serialized, "testSaveEntryRowTemplate", entryTemplate);
}
private static void RenameSaveTab(SerializedObject serialized)
{
var buttons = serialized.FindProperty("tabButtons");
if (buttons == null || buttons.arraySize < 4) return;
var button = buttons.GetArrayElementAtIndex(3).objectReferenceValue as Button;
var label = button?.GetComponentInChildren<TMP_Text>(true);
if (label != null) label.text = "测试存档";
}
private static void ConfigurePageLayout(GameObject page)
{
var layout = page.GetComponent<VerticalLayoutGroup>() ?? page.AddComponent<VerticalLayoutGroup>();
layout.padding = new RectOffset(8, 8, 8, 8);
layout.spacing = 6f;
layout.childAlignment = TextAnchor.UpperLeft;
layout.childControlWidth = true;
layout.childForceExpandWidth = true;
layout.childControlHeight = true;
layout.childForceExpandHeight = false;
}
private static void ClearChildren(Transform parent)
{
for (var index = parent.childCount - 1; index >= 0; index--)
{
UnityEngine.Object.DestroyImmediate(parent.GetChild(index).gameObject);
}
}
private static GameObject CreateUiObject(string name, params Type[] components)
{
var types = components.Contains(typeof(RectTransform))
? components
: new[] { typeof(RectTransform) }.Concat(components).ToArray();
var result = new GameObject(name, types);
result.layer = LayerMask.NameToLayer("UI");
return result;
}
private static GameObject CreatePanel(string name, Transform parent, Color color)
{
var result = CreateUiObject(name, typeof(CanvasRenderer), typeof(Image));
result.transform.SetParent(parent, false);
result.GetComponent<Image>().color = color;
return result;
}
private static GameObject CreateVertical(string name, Transform parent, float spacing)
{
var result = CreateUiObject(name, typeof(VerticalLayoutGroup));
result.transform.SetParent(parent, false);
var layout = result.GetComponent<VerticalLayoutGroup>();
layout.spacing = spacing;
layout.childAlignment = TextAnchor.UpperLeft;
layout.childControlWidth = true;
layout.childForceExpandWidth = true;
layout.childControlHeight = true;
layout.childForceExpandHeight = false;
return result;
}
private static GameObject CreateHorizontal(string name, Transform parent, float spacing)
{
var result = CreateUiObject(name, typeof(HorizontalLayoutGroup));
result.transform.SetParent(parent, false);
var layout = result.GetComponent<HorizontalLayoutGroup>();
layout.spacing = spacing;
layout.childAlignment = TextAnchor.MiddleLeft;
layout.childControlWidth = true;
layout.childControlHeight = true;
layout.childForceExpandWidth = false;
layout.childForceExpandHeight = true;
return result;
}
private static TMP_Text CreateText(
string name,
Transform parent,
string value,
float size,
FontStyles style = FontStyles.Normal)
{
var result = CreateUiObject(name, typeof(CanvasRenderer), typeof(TextMeshProUGUI));
result.transform.SetParent(parent, false);
var text = result.GetComponent<TextMeshProUGUI>();
text.font = AssetDatabase.LoadAssetAtPath<TMP_FontAsset>(FontAssetPath);
text.text = value;
text.fontSize = size;
text.fontStyle = style;
text.color = TextColor;
text.enableWordWrapping = true;
text.overflowMode = TextOverflowModes.Ellipsis;
text.raycastTarget = false;
return text;
}
private static GameObject CreateButton(
string name,
Transform parent,
string label,
float width,
float height)
{
var result = CreateUiObject(
name,
typeof(CanvasRenderer),
typeof(Image),
typeof(Button),
typeof(LayoutElement));
result.transform.SetParent(parent, false);
var image = result.GetComponent<Image>();
image.color = new Color(0.09f, 0.18f, 0.23f, 1f);
var button = result.GetComponent<Button>();
button.targetGraphic = image;
var layout = result.GetComponent<LayoutElement>();
layout.preferredWidth = width;
layout.preferredHeight = height;
var text = CreateText("Label", result.transform, label, 14f, FontStyles.Bold);
Stretch(text.rectTransform, new Vector2(5f, 2f), new Vector2(-5f, -2f));
text.alignment = TextAlignmentOptions.Center;
return result;
}
private static TMP_InputField CreateInput(
string name,
Transform parent,
string placeholder,
float width)
{
var root = CreateUiObject(
name,
typeof(CanvasRenderer),
typeof(Image),
typeof(TMP_InputField),
typeof(LayoutElement));
root.transform.SetParent(parent, false);
root.GetComponent<Image>().color = new Color(0.035f, 0.065f, 0.085f, 1f);
var layout = root.GetComponent<LayoutElement>();
layout.preferredWidth = width;
layout.preferredHeight = 34f;
var viewport = CreateUiObject("Text Area", typeof(RectMask2D));
viewport.transform.SetParent(root.transform, false);
Stretch(viewport.GetComponent<RectTransform>(), new Vector2(8f, 3f), new Vector2(-8f, -3f));
var placeholderText = CreateText("Placeholder", viewport.transform, placeholder, 14f);
Stretch(placeholderText.rectTransform);
placeholderText.color = new Color(0.45f, 0.58f, 0.63f, 1f);
placeholderText.fontStyle = FontStyles.Italic;
placeholderText.alignment = TextAlignmentOptions.MidlineLeft;
var inputText = CreateText("Text", viewport.transform, string.Empty, 14f);
Stretch(inputText.rectTransform);
inputText.alignment = TextAlignmentOptions.MidlineLeft;
var input = root.GetComponent<TMP_InputField>();
input.textViewport = viewport.GetComponent<RectTransform>();
input.textComponent = inputText;
input.placeholder = placeholderText;
input.lineType = TMP_InputField.LineType.SingleLine;
return input;
}
private static Toggle CreateToggle(string name, Transform parent, string label, float width)
{
var root = CreateUiObject(name, typeof(Toggle), typeof(LayoutElement));
root.transform.SetParent(parent, false);
var layout = root.GetComponent<LayoutElement>();
layout.preferredWidth = width;
layout.preferredHeight = 34f;
var box = CreatePanel("Background", root.transform, new Color(0.08f, 0.15f, 0.19f, 1f));
var boxRect = box.GetComponent<RectTransform>();
boxRect.anchorMin = boxRect.anchorMax = new Vector2(0f, 0.5f);
boxRect.pivot = new Vector2(0f, 0.5f);
boxRect.sizeDelta = new Vector2(24f, 24f);
var check = CreatePanel("Checkmark", box.transform, Accent);
Stretch(check.GetComponent<RectTransform>(), new Vector2(5f, 5f), new Vector2(-5f, -5f));
var text = CreateText("Label", root.transform, label, 15f);
SetAnchors(text.rectTransform, Vector2.zero, Vector2.one, new Vector2(34f, 0f), Vector2.zero);
text.alignment = TextAlignmentOptions.MidlineLeft;
var toggle = root.GetComponent<Toggle>();
toggle.targetGraphic = box.GetComponent<Image>();
toggle.graphic = check.GetComponent<Image>();
toggle.isOn = false;
return toggle;
}
private static void CreateList(
string name,
Transform parent,
out RectTransform content,
out DeveloperModeListRow template)
{
var root = CreatePanel(name, parent, new Color(0.02f, 0.035f, 0.05f, 1f));
var rootLayout = root.AddComponent<LayoutElement>();
rootLayout.flexibleHeight = 1f;
rootLayout.minHeight = 300f;
var scroll = root.AddComponent<ScrollRect>();
scroll.horizontal = false;
scroll.vertical = true;
scroll.movementType = ScrollRect.MovementType.Clamped;
scroll.scrollSensitivity = 24f;
var viewport = CreateUiObject("Viewport", typeof(RectMask2D));
viewport.transform.SetParent(root.transform, false);
Stretch(viewport.GetComponent<RectTransform>(), new Vector2(3f, 3f), new Vector2(-3f, -3f));
var contentObject = CreateUiObject(
"Content",
typeof(VerticalLayoutGroup),
typeof(ContentSizeFitter));
contentObject.transform.SetParent(viewport.transform, false);
content = contentObject.GetComponent<RectTransform>();
content.anchorMin = new Vector2(0f, 1f);
content.anchorMax = new Vector2(1f, 1f);
content.pivot = new Vector2(0.5f, 1f);
content.sizeDelta = Vector2.zero;
var vertical = contentObject.GetComponent<VerticalLayoutGroup>();
vertical.spacing = 2f;
vertical.childControlWidth = true;
vertical.childForceExpandWidth = true;
vertical.childControlHeight = true;
vertical.childForceExpandHeight = false;
contentObject.GetComponent<ContentSizeFitter>().verticalFit =
ContentSizeFitter.FitMode.PreferredSize;
var row = CreateUiObject(
"Row Template",
typeof(CanvasRenderer),
typeof(Image),
typeof(Button),
typeof(LayoutElement),
typeof(DeveloperModeListRow));
row.transform.SetParent(content, false);
var rowImage = row.GetComponent<Image>();
rowImage.color = Surface;
var rowButton = row.GetComponent<Button>();
rowButton.targetGraphic = rowImage;
var rowLayout = row.GetComponent<LayoutElement>();
rowLayout.preferredHeight = 30f;
rowLayout.minHeight = 30f;
var rowText = CreateText("Label", row.transform, "Template", 13f);
Stretch(rowText.rectTransform, new Vector2(7f, 2f), new Vector2(-7f, -2f));
rowText.alignment = TextAlignmentOptions.MidlineLeft;
rowText.enableWordWrapping = false;
var serialized = new SerializedObject(row.GetComponent<DeveloperModeListRow>());
Set(serialized, "button", rowButton);
Set(serialized, "label", rowText);
Set(serialized, "background", rowImage);
serialized.ApplyModifiedPropertiesWithoutUndo();
row.SetActive(false);
template = row.GetComponent<DeveloperModeListRow>();
scroll.viewport = viewport.GetComponent<RectTransform>();
scroll.content = content;
}
private static void SetAnchors(
RectTransform rect,
Vector2 min,
Vector2 max,
Vector2 offsetMin,
Vector2 offsetMax)
{
rect.anchorMin = min;
rect.anchorMax = max;
rect.offsetMin = offsetMin;
rect.offsetMax = offsetMax;
}
private static void Stretch(
RectTransform rect,
Vector2 offsetMin,
Vector2 offsetMax)
{
rect.anchorMin = Vector2.zero;
rect.anchorMax = Vector2.one;
rect.offsetMin = offsetMin;
rect.offsetMax = offsetMax;
}
private static void Stretch(RectTransform rect)
{
Stretch(rect, Vector2.zero, Vector2.zero);
}
private static void AddLayout(GameObject target, float height)
{
var layout = target.GetComponent<LayoutElement>() ?? target.AddComponent<LayoutElement>();
layout.preferredHeight = height;
}
private static void Set(SerializedObject serialized, string propertyName, UnityEngine.Object value)
{
var property = serialized.FindProperty(propertyName)
?? throw new MissingFieldException(
serialized.targetObject.GetType().Name,
propertyName);
property.objectReferenceValue = value;
}
}
}
#endif