MeowmentDebugTool/Packages/com.bywaystudios.meowmentdebugtool/Runtime/CustomCheckBoxesModule.cs
zhang hongbo cf352f2a22 11
2026-04-06 10:03:28 +08:00

728 lines
26 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
namespace MeowmentDebugTool
{
/// <summary>
/// 自定义复选框模块 - 通过反射加载标记为DebugCheckBox的方法
/// </summary>
public class CustomCheckBoxesModule : IDebugModule
{
#region
private const float SubTabCellWidth = 200f;
private const float SubTabCellHeight = 70f;
private const float SubTabSpacingX = 8f;
private const float SubTabSpacingY = 8f;
private const int SubTabPaddingHorizontal = 20;
private const int SubTabPaddingVertical = 20;
private GameObject customCheckBoxesPage;
private RectTransform checkBoxContainer;
private GameObject checkBoxPrefab;
private ScrollRect checkBoxesScrollRect;
// 保存的SDF字体资源
private TMP_FontAsset savedFontAsset = null;
// 保存每个复选框的状态
private Dictionary<string, bool> checkBoxStates = new Dictionary<string, bool>();
// Tab相关
private Dictionary<string, List<CheckBoxInfo>> tabGroups = new Dictionary<string, List<CheckBoxInfo>>();
private Dictionary<string, GameObject> tabPanels = new Dictionary<string, GameObject>();
private Dictionary<string, Color> tabColors = new Dictionary<string, Color>();
private List<Button> subTabButtons = new List<Button>();
private string currentSubTab = string.Empty;
private bool checkBoxDefinitionsDirty = true;
private bool checkBoxViewInitialized = false;
private HashSet<string> builtSubTabs = new HashSet<string>();
private GameObject subTabBarObject;
private GameObject tabContentRootObject;
#endregion
#region
private class CheckBoxInfo
{
public MethodInfo Method;
public DebugCheckBoxAttribute Attribute;
public string TabName;
}
#endregion
#region
public CustomCheckBoxesModule(GameObject page, RectTransform container, GameObject prefab,
ScrollRect scrollRect)
{
customCheckBoxesPage = page;
checkBoxContainer = container;
checkBoxPrefab = prefab;
checkBoxesScrollRect = scrollRect;
}
#endregion
#region IDebugModule
public void Initialize()
{
Debug.Log("[CustomCheckBoxesModule] 初始化自定义复选框模块...");
checkBoxDefinitionsDirty = true;
checkBoxViewInitialized = false;
}
public GameObject GetPage()
{
return customCheckBoxesPage;
}
public string GetModuleName()
{
return "开关";
}
#endregion
#region
/// <summary>
/// 设置SDF字体
/// </summary>
public void SetSDFFont(TMP_FontAsset fontAsset)
{
savedFontAsset = fontAsset;
ApplyFontToExistingUI();
}
/// <summary>
/// 重新加载自定义复选框
/// </summary>
public void ReloadCustomCheckBoxes()
{
ReloadCheckBoxDefinitions();
InvalidateCheckBoxView();
if (customCheckBoxesPage != null && customCheckBoxesPage.activeInHierarchy)
{
EnsurePageViewInitialized();
}
}
public void EnsurePageViewInitialized()
{
if (checkBoxDefinitionsDirty)
{
ReloadCheckBoxDefinitions();
}
if (!checkBoxViewInitialized)
{
BuildCheckBoxViewSkeleton();
}
if (string.IsNullOrEmpty(currentSubTab) && tabGroups.Count > 0)
{
currentSubTab = tabGroups.OrderBy(kvp => kvp.Key == "默认" ? string.Empty : kvp.Key).First().Key;
}
if (!string.IsNullOrEmpty(currentSubTab))
{
EnsureSubTabContentBuilt(currentSubTab);
}
UpdateSubTabButtonStyles();
}
private void ApplyFontToExistingUI()
{
if (savedFontAsset == null || customCheckBoxesPage == null)
{
return;
}
TMP_Text[] texts = customCheckBoxesPage.GetComponentsInChildren<TMP_Text>(true);
foreach (TMP_Text text in texts)
{
text.font = savedFontAsset;
}
}
#endregion
#region
/// <summary>
/// 加载所有自定义复选框(使用反射)
/// </summary>
private void ReloadCheckBoxDefinitions()
{
if (checkBoxContainer == null || checkBoxPrefab == null)
{
Debug.LogWarning("[CustomCheckBoxesModule] 复选框容器或复选框预制件未设置");
return;
}
tabGroups.Clear();
tabColors.Clear();
// 查找所有标记为DebugCheckBox的方法
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (var assembly in assemblies)
{
try
{
var types = assembly.GetTypes();
foreach (var type in types)
{
var methods = type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
foreach (var method in methods)
{
var attribute = method.GetCustomAttribute<DebugCheckBoxAttribute>();
if (attribute != null)
{
var classAttribute = type.GetCustomAttribute<DebugCheckBoxClassAttribute>(true);
string tabName = ResolveTabName(attribute, classAttribute);
Color tabColor = ResolveTabColor(classAttribute);
RegisterTabColor(tabName, tabColor);
if (!tabGroups.ContainsKey(tabName))
{
tabGroups[tabName] = new List<CheckBoxInfo>();
}
tabGroups[tabName].Add(new CheckBoxInfo
{
Method = method,
Attribute = attribute,
TabName = tabName
});
}
}
}
}
catch (Exception e)
{
// 某些程序集可能无法访问,跳过
Debug.LogWarning($"[CustomCheckBoxesModule] 无法访问程序集 {assembly.FullName}: {e.Message}");
}
}
Debug.Log($"[CustomCheckBoxesModule] 共找到 {tabGroups.Count} 个Tab");
foreach (var tab in tabGroups)
{
Debug.Log($" - Tab: {tab.Key}, 开关数: {tab.Value.Count}");
}
checkBoxDefinitionsDirty = false;
}
private string ResolveTabName(DebugCheckBoxAttribute methodAttribute, DebugCheckBoxClassAttribute classAttribute)
{
if (!string.IsNullOrEmpty(methodAttribute.TabName))
{
return methodAttribute.TabName;
}
if (classAttribute != null && !string.IsNullOrEmpty(classAttribute.TabName))
{
return classAttribute.TabName;
}
return "默认";
}
private Color ResolveTabColor(DebugCheckBoxClassAttribute classAttribute)
{
if (classAttribute != null)
{
return classAttribute.TabColor;
}
return new Color(0.2f, 0.6f, 1f, 1f);
}
private void RegisterTabColor(string tabName, Color tabColor)
{
if (!tabColors.ContainsKey(tabName))
{
tabColors[tabName] = tabColor;
return;
}
Color existing = tabColors[tabName];
if (!Mathf.Approximately(existing.r, tabColor.r) ||
!Mathf.Approximately(existing.g, tabColor.g) ||
!Mathf.Approximately(existing.b, tabColor.b) ||
!Mathf.Approximately(existing.a, tabColor.a))
{
Debug.LogWarning($"[CustomCheckBoxesModule] Tab '{tabName}' 检测到多个颜色定义,已保留第一个颜色配置");
}
}
private void BuildCheckBoxViewSkeleton()
{
InvalidateCheckBoxView();
SetupCheckBoxContainerLayout();
if (tabGroups.Count == 0)
{
GameObject emptyText = new GameObject("EmptyTip");
emptyText.transform.SetParent(checkBoxContainer, false);
TMP_Text text = emptyText.AddComponent<TextMeshProUGUI>();
text.text = "未找到任何 DebugCheckBox";
text.fontSize = 28;
text.fontStyle = FontStyles.Bold;
text.enableAutoSizing = true;
text.fontSizeMin = 12;
text.fontSizeMax = 72;
text.alignment = TextAlignmentOptions.Center;
text.color = new Color(1f, 1f, 1f, 0.8f);
if (savedFontAsset != null)
{
text.font = savedFontAsset;
}
LayoutElement element = emptyText.AddComponent<LayoutElement>();
element.preferredHeight = 80;
checkBoxViewInitialized = true;
return;
}
subTabBarObject = CreateSubTabBar();
tabContentRootObject = new GameObject("SubTabContentRoot");
tabContentRootObject.transform.SetParent(checkBoxContainer, false);
RectTransform contentRootRect = tabContentRootObject.AddComponent<RectTransform>();
contentRootRect.anchorMin = new Vector2(0, 1);
contentRootRect.anchorMax = new Vector2(1, 1);
contentRootRect.pivot = new Vector2(0.5f, 1f);
VerticalLayoutGroup contentRootLayout = tabContentRootObject.AddComponent<VerticalLayoutGroup>();
contentRootLayout.childForceExpandWidth = true;
contentRootLayout.childForceExpandHeight = false;
contentRootLayout.childControlWidth = true;
contentRootLayout.childControlHeight = false;
contentRootLayout.spacing = 0;
contentRootLayout.padding = new RectOffset(0, 0, 0, 0);
ContentSizeFitter contentRootFitter = tabContentRootObject.AddComponent<ContentSizeFitter>();
contentRootFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
contentRootFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained;
LayoutElement contentRootElement = tabContentRootObject.AddComponent<LayoutElement>();
contentRootElement.flexibleWidth = 1;
var sortedTabs = tabGroups.OrderBy(kvp => kvp.Key == "默认" ? string.Empty : kvp.Key);
bool firstTab = true;
foreach (var tab in sortedTabs)
{
string tabName = tab.Key;
CreateSubTabButton(tabName, subTabBarObject.transform);
GameObject tabPanel = CreateTabPanel(tabName, tabContentRootObject.transform);
tabPanels[tabName] = tabPanel;
tabPanel.SetActive(firstTab);
if (firstTab)
{
currentSubTab = tabName;
firstTab = false;
}
}
Canvas.ForceUpdateCanvases();
UpdateSubTabBarHeight(subTabBarObject);
UpdateSubTabButtonStyles();
if (checkBoxesScrollRect != null)
{
Canvas.ForceUpdateCanvases();
checkBoxesScrollRect.verticalNormalizedPosition = 1f;
}
checkBoxViewInitialized = true;
}
private void EnsureSubTabContentBuilt(string tabName)
{
if (string.IsNullOrEmpty(tabName) || builtSubTabs.Contains(tabName))
{
return;
}
if (!tabPanels.TryGetValue(tabName, out GameObject tabPanel) || tabPanel == null)
{
return;
}
if (!tabGroups.TryGetValue(tabName, out List<CheckBoxInfo> checkBoxes))
{
return;
}
foreach (var checkBoxInfo in checkBoxes)
{
CreateCustomCheckBox(checkBoxInfo.Method, checkBoxInfo.Attribute, tabPanel.transform);
}
builtSubTabs.Add(tabName);
Canvas.ForceUpdateCanvases();
}
private void InvalidateCheckBoxView()
{
if (checkBoxContainer != null)
{
foreach (Transform child in checkBoxContainer)
{
UnityEngine.Object.Destroy(child.gameObject);
}
}
tabPanels.Clear();
subTabButtons.Clear();
builtSubTabs.Clear();
currentSubTab = string.Empty;
subTabBarObject = null;
tabContentRootObject = null;
checkBoxViewInitialized = false;
}
private GameObject CreateSubTabBar()
{
GameObject subTabBar = new GameObject("SubTabBar");
subTabBar.transform.SetParent(checkBoxContainer, false);
RectTransform rectTransform = subTabBar.AddComponent<RectTransform>();
rectTransform.anchorMin = new Vector2(0, 1);
rectTransform.anchorMax = new Vector2(1, 1);
rectTransform.pivot = new Vector2(0.5f, 1f);
Image bg = subTabBar.AddComponent<Image>();
bg.color = new Color(0.15f, 0.15f, 0.15f, 0.8f);
GridLayoutGroup layout = subTabBar.AddComponent<GridLayoutGroup>();
layout.cellSize = new Vector2(SubTabCellWidth, SubTabCellHeight);
layout.spacing = new Vector2(SubTabSpacingX, SubTabSpacingY);
layout.padding = new RectOffset(10, 10, 10, 10);
layout.constraint = GridLayoutGroup.Constraint.Flexible;
layout.startAxis = GridLayoutGroup.Axis.Horizontal;
layout.startCorner = GridLayoutGroup.Corner.UpperLeft;
layout.childAlignment = TextAnchor.UpperLeft;
ContentSizeFitter fitter = subTabBar.AddComponent<ContentSizeFitter>();
fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
fitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained;
LayoutElement element = subTabBar.AddComponent<LayoutElement>();
element.minHeight = 80;
element.preferredHeight = 80;
element.flexibleWidth = 1;
return subTabBar;
}
private void CreateSubTabButton(string tabName, Transform parent)
{
GameObject tabButtonObj = new GameObject($"SubTab_{tabName}");
tabButtonObj.transform.SetParent(parent, false);
RectTransform rect = tabButtonObj.AddComponent<RectTransform>();
rect.sizeDelta = new Vector2(200, 56);
Image image = tabButtonObj.AddComponent<Image>();
image.color = GetInactiveTabColor(tabName);
Button button = tabButtonObj.AddComponent<Button>();
GameObject textObj = new GameObject("Text");
textObj.transform.SetParent(tabButtonObj.transform, false);
RectTransform textRect = textObj.AddComponent<RectTransform>();
textRect.anchorMin = Vector2.zero;
textRect.anchorMax = Vector2.one;
textRect.offsetMin = Vector2.zero;
textRect.offsetMax = Vector2.zero;
TMP_Text text = textObj.AddComponent<TextMeshProUGUI>();
text.text = tabName;
text.fontSize = 26;
text.fontStyle = FontStyles.Bold;
text.enableAutoSizing = true;
text.fontSizeMin = 12;
text.fontSizeMax = 72;
text.alignment = TextAlignmentOptions.Center;
text.color = Color.white;
if (savedFontAsset != null)
{
text.font = savedFontAsset;
}
LayoutElement element = tabButtonObj.AddComponent<LayoutElement>();
element.preferredWidth = SubTabCellWidth;
element.minWidth = SubTabCellWidth;
element.preferredHeight = SubTabCellHeight;
button.onClick.AddListener(() => SelectSubTab(tabName));
subTabButtons.Add(button);
}
private void UpdateSubTabBarHeight(GameObject subTabBar)
{
if (subTabBar == null)
{
return;
}
RectTransform subTabRect = subTabBar.GetComponent<RectTransform>();
LayoutElement layoutElement = subTabBar.GetComponent<LayoutElement>();
if (subTabRect == null || layoutElement == null)
{
return;
}
float availableWidth = subTabRect.rect.width;
if (availableWidth <= 0f && checkBoxContainer != null)
{
availableWidth = checkBoxContainer.rect.width;
}
if (availableWidth <= 0f)
{
return;
}
float usableWidth = Mathf.Max(1f, availableWidth - SubTabPaddingHorizontal);
int columnCount = Mathf.Max(1, Mathf.FloorToInt((usableWidth + SubTabSpacingX) / (SubTabCellWidth + SubTabSpacingX)));
int rowCount = Mathf.Max(1, Mathf.CeilToInt((float)subTabButtons.Count / columnCount));
float preferredHeight = SubTabPaddingVertical + rowCount * SubTabCellHeight + Mathf.Max(0, rowCount - 1) * SubTabSpacingY;
layoutElement.minHeight = preferredHeight;
layoutElement.preferredHeight = preferredHeight;
LayoutRebuilder.ForceRebuildLayoutImmediate(subTabRect);
}
private GameObject CreateTabPanel(string tabName, Transform parent)
{
GameObject panel = new GameObject($"Panel_{tabName}");
panel.transform.SetParent(parent, false);
RectTransform rectTransform = panel.AddComponent<RectTransform>();
rectTransform.anchorMin = new Vector2(0, 1);
rectTransform.anchorMax = new Vector2(1, 1);
rectTransform.pivot = new Vector2(0.5f, 1f);
VerticalLayoutGroup layout = panel.AddComponent<VerticalLayoutGroup>();
layout.childForceExpandWidth = true;
layout.childForceExpandHeight = false;
layout.childControlWidth = true;
layout.childControlHeight = false;
layout.spacing = 15;
layout.padding = new RectOffset(0, 0, 10, 10);
ContentSizeFitter fitter = panel.AddComponent<ContentSizeFitter>();
fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
fitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained;
LayoutElement element = panel.AddComponent<LayoutElement>();
element.flexibleWidth = 1;
return panel;
}
private void SelectSubTab(string tabName)
{
if (string.IsNullOrEmpty(tabName))
{
return;
}
EnsureSubTabContentBuilt(tabName);
if (currentSubTab == tabName)
{
return;
}
currentSubTab = tabName;
foreach (var kvp in tabPanels)
{
kvp.Value.SetActive(kvp.Key == tabName);
}
UpdateSubTabButtonStyles();
if (checkBoxesScrollRect != null)
{
Canvas.ForceUpdateCanvases();
checkBoxesScrollRect.verticalNormalizedPosition = 1f;
}
}
private void UpdateSubTabButtonStyles()
{
foreach (var button in subTabButtons)
{
if (button == null)
{
continue;
}
string tabName = button.gameObject.name.Replace("SubTab_", string.Empty);
Image image = button.GetComponent<Image>();
if (image != null)
{
image.color = tabName == currentSubTab
? GetActiveTabColor(tabName)
: GetInactiveTabColor(tabName);
}
}
}
private Color GetActiveTabColor(string tabName)
{
if (tabColors.TryGetValue(tabName, out var color))
{
color.a = 0.95f;
return color;
}
return new Color(0.2f, 0.6f, 1f, 0.95f);
}
private Color GetInactiveTabColor(string tabName)
{
Color activeColor = GetActiveTabColor(tabName);
Color inactive = Color.Lerp(new Color(0.18f, 0.18f, 0.18f, 0.9f), activeColor, 0.35f);
inactive.a = 0.9f;
return inactive;
}
private void SetupCheckBoxContainerLayout()
{
if (checkBoxContainer == null)
{
Debug.LogError("[CustomCheckBoxesModule] checkBoxContainer 为 null无法设置布局");
return;
}
GameObject containerObject = checkBoxContainer.gameObject;
GridLayoutGroup gridLayout = containerObject.GetComponent<GridLayoutGroup>();
if (gridLayout != null)
{
UnityEngine.Object.DestroyImmediate(gridLayout);
}
HorizontalLayoutGroup horizontalLayout = containerObject.GetComponent<HorizontalLayoutGroup>();
if (horizontalLayout != null)
{
UnityEngine.Object.DestroyImmediate(horizontalLayout);
}
VerticalLayoutGroup verticalLayout = containerObject.GetComponent<VerticalLayoutGroup>();
if (verticalLayout == null)
{
verticalLayout = containerObject.AddComponent<VerticalLayoutGroup>();
}
verticalLayout.childForceExpandWidth = true;
verticalLayout.childForceExpandHeight = false;
verticalLayout.childControlWidth = true;
verticalLayout.childControlHeight = false;
verticalLayout.spacing = 15;
verticalLayout.padding = new RectOffset(20, 20, 20, 20);
ContentSizeFitter sizeFitter = containerObject.GetComponent<ContentSizeFitter>();
if (sizeFitter == null)
{
sizeFitter = containerObject.AddComponent<ContentSizeFitter>();
}
sizeFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
sizeFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained;
}
private void CreateCustomCheckBox(MethodInfo method, DebugCheckBoxAttribute attribute, Transform parent)
{
// 验证方法签名:必须接受一个 bool 参数
var parameters = method.GetParameters();
if (parameters.Length != 1 || parameters[0].ParameterType != typeof(bool))
{
Debug.LogWarning($"[CustomCheckBoxesModule] 方法 {method.Name} 必须接受一个 bool 参数");
return;
}
GameObject checkBoxObj = UnityEngine.Object.Instantiate(checkBoxPrefab, parent);
RectTransform checkBoxRect = checkBoxObj.GetComponent<RectTransform>();
if (checkBoxRect != null)
{
LayoutElement layoutElement = checkBoxObj.GetComponent<LayoutElement>();
if (layoutElement == null)
{
layoutElement = checkBoxObj.AddComponent<LayoutElement>();
}
layoutElement.preferredHeight = checkBoxRect.sizeDelta.y > 0 ? checkBoxRect.sizeDelta.y : 80f;
layoutElement.flexibleWidth = 1f;
}
Toggle toggle = checkBoxObj.GetComponent<Toggle>();
TMP_Text labelText = checkBoxObj.GetComponentInChildren<TMP_Text>();
Image backgroundImage = checkBoxObj.GetComponent<Image>();
// 设置复选框文本
if (labelText != null)
{
labelText.text = string.IsNullOrEmpty(attribute.DisplayName) ? method.Name : attribute.DisplayName;
// 应用保存的字体
if (savedFontAsset != null)
{
labelText.font = savedFontAsset;
}
}
// 设置背景颜色
if (backgroundImage != null && attribute.CheckBoxColor != default(Color))
{
backgroundImage.color = attribute.CheckBoxColor;
}
// 获取或初始化状态(默认为 false
string methodKey = $"{method.DeclaringType?.FullName}.{method.Name}";
if (!checkBoxStates.ContainsKey(methodKey))
{
checkBoxStates[methodKey] = false;
}
// 设置初始状态
if (toggle != null)
{
toggle.isOn = checkBoxStates[methodKey];
toggle.onValueChanged.AddListener((bool isOn) =>
{
try
{
// 保存状态
checkBoxStates[methodKey] = isOn;
// 调用方法,传递状态
method.Invoke(null, new object[] { isOn });
Debug.Log($"[CustomCheckBoxesModule] 执行调试方法: {method.Name}, 状态: {isOn}");
}
catch (Exception e)
{
Debug.LogError($"[CustomCheckBoxesModule] 执行调试方法 {method.Name} 时出错: {e.Message}");
}
});
}
}
#endregion
}
}