MeowmentDebugTool/Packages/com.bywaystudios.meowmentdebugtool/Runtime/CustomButtonsModule.cs
2026-03-26 14:32:41 +08:00

1576 lines
58 KiB
C#
Raw 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;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
namespace MeowmentDebugTool
{
/// <summary>
/// 自定义按钮模块 - 通过反射加载标记为DebugButton的方法支持分组显示
/// </summary>
public class CustomButtonsModule : IDebugModule
{
#region
private const float SubTabCellWidth = 170f;
private const float SubTabCellHeight = 56f;
private const float SubTabSpacingX = 8f;
private const float SubTabSpacingY = 8f;
private const int SubTabPaddingHorizontal = 20;
private const int SubTabPaddingVertical = 20;
private const float ButtonMinWidth = 260f;
private const float ButtonMaxWidth = 10000f;
private const float ButtonHeight = 90f;
private const float ButtonSpacing = 10f;
private const int ButtonsPerFrame = 12;
private GameObject customButtonsPage;
private RectTransform buttonContainer;
private GameObject buttonPrefab;
private ScrollRect buttonsScrollRect;
// 自定义按钮回调
private Action<Button, TMP_Text> customButtonCallback;
// 保存的SDF字体资源
private TMP_FontAsset savedFontAsset = null;
// 关闭窗口回调
private Action onCloseWindowCallback;
// 分组相关
private Dictionary<string, Dictionary<string, List<ButtonInfo>>> tabGroups = new Dictionary<string, Dictionary<string, List<ButtonInfo>>>();
private Dictionary<string, RuntimeButtonInfo> runtimeButtons = new Dictionary<string, RuntimeButtonInfo>();
private int runtimeButtonBatchUpdateDepth = 0;
private bool runtimeButtonsDirty = false;
private bool buttonDefinitionsDirty = true;
private bool buttonViewInitialized = false;
private HashSet<string> builtSubTabs = new HashSet<string>();
private GameObject subTabBarObject;
private GameObject tabContentRootObject;
private Coroutine subTabBuildCoroutine;
private string buildingSubTab = string.Empty;
private int subTabBuildVersion = 0;
// 子Tab相关
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 Dictionary<Transform, ButtonRowLayoutState> buttonRowStates = new Dictionary<Transform, ButtonRowLayoutState>();
private string currentSubTab = string.Empty;
#endregion
#region
/// <summary>
/// 按钮信息
/// </summary>
private class ButtonInfo
{
public MethodInfo Method;
public Action Callback;
public bool RequiresInput;
public DebugInputButtonAttribute InputAttribute;
public RuntimeDebugInputButtonDefinition RuntimeInputDefinition;
public string DisplayName;
public Color ButtonColor;
public string SourceName;
public string TabName;
public string GroupName;
}
private class RuntimeButtonInfo
{
public string Id;
public Action Callback;
public bool RequiresInput;
public RuntimeDebugInputButtonDefinition RuntimeInputDefinition;
public string TabName;
public string GroupName;
public string DisplayName;
public Color ButtonColor;
public Color TabColor;
}
private class ButtonRowLayoutState
{
public Transform CurrentRow;
public float CurrentRowWidth;
}
#endregion
#region
public CustomButtonsModule(GameObject page, RectTransform container, GameObject prefab,
ScrollRect scrollRect, Action closeWindowCallback)
{
customButtonsPage = page;
buttonContainer = container;
buttonPrefab = prefab;
buttonsScrollRect = scrollRect;
onCloseWindowCallback = closeWindowCallback;
}
#endregion
#region IDebugModule
public void Initialize()
{
Debug.Log("[CustomButtonsModule] 初始化自定义按钮模块...");
if (buttonContainer == null || buttonPrefab == null)
{
Debug.LogError("[CustomButtonsModule] 必要组件缺失,初始化失败");
return;
}
buttonDefinitionsDirty = true;
buttonViewInitialized = false;
}
public GameObject GetPage()
{
return customButtonsPage;
}
public string GetModuleName()
{
return "按钮";
}
#endregion
#region
/// <summary>
/// 设置SDF字体
/// </summary>
public void SetSDFFont(TMP_FontAsset fontAsset)
{
savedFontAsset = fontAsset;
ApplyFontToExistingUI();
}
/// <summary>
/// 设置自定义按钮回调
/// </summary>
public void SetCustomButtonCallback(Action<Button, TMP_Text> callback)
{
customButtonCallback = callback;
}
/// <summary>
/// 重新加载自定义按钮
/// </summary>
public void ReloadCustomButtons()
{
ReloadButtonDefinitions();
InvalidateButtonView();
if (customButtonsPage != null && customButtonsPage.activeInHierarchy)
{
EnsurePageViewInitialized();
}
runtimeButtonsDirty = false;
}
public void EnsurePageViewInitialized()
{
if (buttonDefinitionsDirty)
{
ReloadButtonDefinitions();
}
if (!buttonViewInitialized)
{
BuildButtonViewSkeleton();
}
if (string.IsNullOrEmpty(currentSubTab) && tabGroups.Count > 0)
{
currentSubTab = tabGroups.OrderBy(kvp => kvp.Key == "默认" ? "" : kvp.Key).First().Key;
}
if (!string.IsNullOrEmpty(currentSubTab))
{
EnsureSubTabContentBuilt(currentSubTab);
}
UpdateSubTabButtonStyles();
}
public void BeginRuntimeButtonBatchUpdate()
{
runtimeButtonBatchUpdateDepth++;
}
public void EndRuntimeButtonBatchUpdate(bool reloadIfDirty = true)
{
if (runtimeButtonBatchUpdateDepth <= 0)
{
runtimeButtonBatchUpdateDepth = 0;
if (reloadIfDirty && runtimeButtonsDirty)
{
ReloadRuntimeButtonsIfNeeded();
}
return;
}
runtimeButtonBatchUpdateDepth--;
if (runtimeButtonBatchUpdateDepth == 0 && reloadIfDirty && runtimeButtonsDirty)
{
ReloadRuntimeButtonsIfNeeded();
}
}
public bool RegisterRuntimeButton(string buttonId, Action callback, string tabName = "默认",
string groupName = "默认", string displayName = "", Color? buttonColor = null, Color? tabColor = null,
bool reloadImmediately = true)
{
if (string.IsNullOrEmpty(buttonId))
{
Debug.LogWarning("[CustomButtonsModule] 运行时按钮ID不能为空");
return false;
}
if (callback == null)
{
Debug.LogWarning($"[CustomButtonsModule] 运行时按钮 {buttonId} 的回调不能为空");
return false;
}
runtimeButtons[buttonId] = new RuntimeButtonInfo
{
Id = buttonId,
Callback = callback,
RequiresInput = false,
RuntimeInputDefinition = null,
TabName = string.IsNullOrEmpty(tabName) ? "默认" : tabName,
GroupName = string.IsNullOrEmpty(groupName) ? "默认" : groupName,
DisplayName = string.IsNullOrEmpty(displayName) ? buttonId : displayName,
ButtonColor = buttonColor ?? new Color(0.8f, 0.8f, 0.8f, 1f),
TabColor = tabColor ?? new Color(0.2f, 0.6f, 1f, 1f)
};
RequestRuntimeButtonsReload(reloadImmediately);
return true;
}
public bool RegisterRuntimeInputButton(RuntimeDebugInputButtonDefinition definition, bool reloadImmediately = true)
{
if (definition == null)
{
Debug.LogWarning("[CustomButtonsModule] 运行时输入按钮定义不能为空");
return false;
}
if (string.IsNullOrEmpty(definition.Id))
{
Debug.LogWarning("[CustomButtonsModule] 运行时输入按钮ID不能为空");
return false;
}
if (definition.Callback == null)
{
Debug.LogWarning($"[CustomButtonsModule] 运行时输入按钮 {definition.Id} 的回调不能为空");
return false;
}
if (definition.Parameters == null)
{
definition.Parameters = new List<RuntimeDebugInputParameterDefinition>();
}
runtimeButtons[definition.Id] = new RuntimeButtonInfo
{
Id = definition.Id,
Callback = null,
RequiresInput = true,
RuntimeInputDefinition = definition,
TabName = string.IsNullOrEmpty(definition.TabName) ? "默认" : definition.TabName,
GroupName = string.IsNullOrEmpty(definition.GroupName) ? "默认" : definition.GroupName,
DisplayName = string.IsNullOrEmpty(definition.DisplayName) ? definition.Id : definition.DisplayName,
ButtonColor = definition.ButtonColor,
TabColor = definition.TabColor
};
RequestRuntimeButtonsReload(reloadImmediately);
return true;
}
public bool UnregisterRuntimeButton(string buttonId, bool reloadImmediately = true)
{
if (string.IsNullOrEmpty(buttonId))
{
return false;
}
bool removed = runtimeButtons.Remove(buttonId);
if (removed)
{
RequestRuntimeButtonsReload(reloadImmediately);
}
return removed;
}
public void ClearRuntimeButtons(bool reloadImmediately = true)
{
if (runtimeButtons.Count == 0)
{
return;
}
runtimeButtons.Clear();
RequestRuntimeButtonsReload(reloadImmediately);
}
private void RequestRuntimeButtonsReload(bool reloadImmediately)
{
runtimeButtonsDirty = true;
buttonDefinitionsDirty = true;
if (!reloadImmediately || runtimeButtonBatchUpdateDepth > 0)
{
return;
}
ReloadRuntimeButtonsIfNeeded();
}
private void ReloadRuntimeButtonsIfNeeded()
{
if (!ShouldReloadRuntimeButtonsImmediately())
{
return;
}
ReloadCustomButtons();
}
private bool ShouldReloadRuntimeButtonsImmediately()
{
return buttonViewInitialized || (customButtonsPage != null && customButtonsPage.activeInHierarchy);
}
private void ApplyFontToExistingUI()
{
if (savedFontAsset == null || customButtonsPage == null)
{
return;
}
TMP_Text[] texts = customButtonsPage.GetComponentsInChildren<TMP_Text>(true);
foreach (TMP_Text text in texts)
{
text.font = savedFontAsset;
}
}
#endregion
#region -
/// <summary>
/// 重新扫描所有按钮定义(使用反射)
/// </summary>
private void ReloadButtonDefinitions()
{
if (buttonContainer == null || buttonPrefab == null)
{
Debug.LogWarning("[CustomButtonsModule] 按钮容器或按钮预制件未设置");
return;
}
// 清空现有数据
tabGroups.Clear();
tabColors.Clear();
// 查找所有标记为DebugButton的方法
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<DebugButtonAttribute>();
var inputAttribute = method.GetCustomAttribute<DebugInputButtonAttribute>();
if (attribute != null)
{
// 读取类级别默认配置(可选)
var classAttribute = type.GetCustomAttribute<DebugButtonClassAttribute>(true);
// 先取方法上的配置,再回退到类上的配置
string tabName = ResolveTabName(attribute, classAttribute);
string groupName = ResolveGroupName(attribute, classAttribute);
Color tabColor = ResolveTabColor(classAttribute);
RegisterTabColor(tabName, tabColor);
AddButtonInfoToGroups(new ButtonInfo
{
Method = method,
Callback = null,
RequiresInput = false,
InputAttribute = null,
RuntimeInputDefinition = null,
DisplayName = string.IsNullOrEmpty(attribute.DisplayName) ? method.Name : attribute.DisplayName,
ButtonColor = attribute.ButtonColor,
SourceName = method.Name,
TabName = tabName,
GroupName = groupName
}, tabColor);
}
else if (inputAttribute != null)
{
var classAttribute = type.GetCustomAttribute<DebugButtonClassAttribute>(true);
string tabName = ResolveInputTabName(inputAttribute, classAttribute);
string groupName = ResolveInputGroupName(inputAttribute, classAttribute);
Color tabColor = ResolveTabColor(classAttribute);
AddButtonInfoToGroups(new ButtonInfo
{
Method = method,
Callback = null,
RequiresInput = true,
InputAttribute = inputAttribute,
RuntimeInputDefinition = null,
DisplayName = string.IsNullOrEmpty(inputAttribute.DisplayName) ? method.Name : inputAttribute.DisplayName,
ButtonColor = inputAttribute.ButtonColor,
SourceName = method.Name,
TabName = tabName,
GroupName = groupName
}, tabColor);
}
}
}
}
catch (Exception e)
{
// 某些程序集可能无法访问,跳过
Debug.LogWarning($"[CustomButtonsModule] 无法访问程序集 {assembly.FullName}: {e.Message}");
}
}
foreach (var runtimeButton in runtimeButtons.Values.OrderBy(btn => btn.TabName).ThenBy(btn => btn.GroupName).ThenBy(btn => btn.DisplayName))
{
AddButtonInfoToGroups(new ButtonInfo
{
Method = null,
Callback = runtimeButton.Callback,
RequiresInput = runtimeButton.RequiresInput,
InputAttribute = null,
RuntimeInputDefinition = runtimeButton.RuntimeInputDefinition,
DisplayName = runtimeButton.DisplayName,
ButtonColor = runtimeButton.ButtonColor,
SourceName = runtimeButton.Id,
TabName = runtimeButton.TabName,
GroupName = runtimeButton.GroupName
}, runtimeButton.TabColor);
}
Debug.Log($"[CustomButtonsModule] 共找到 {tabGroups.Count} 个Tab");
foreach (var tab in tabGroups)
{
int buttonCount = tab.Value.Sum(g => g.Value.Count);
Debug.Log($" - Tab: {tab.Key}, 组数: {tab.Value.Count}, 按钮数: {buttonCount}");
}
buttonDefinitionsDirty = false;
}
private void AddButtonInfoToGroups(ButtonInfo buttonInfo, Color tabColor)
{
RegisterTabColor(buttonInfo.TabName, tabColor);
if (!tabGroups.ContainsKey(buttonInfo.TabName))
{
tabGroups[buttonInfo.TabName] = new Dictionary<string, List<ButtonInfo>>();
}
if (!tabGroups[buttonInfo.TabName].ContainsKey(buttonInfo.GroupName))
{
tabGroups[buttonInfo.TabName][buttonInfo.GroupName] = new List<ButtonInfo>();
}
tabGroups[buttonInfo.TabName][buttonInfo.GroupName].Add(buttonInfo);
}
/// <summary>
/// 解析Tab名方法配置优先其次类级别配置最后默认
/// </summary>
private string ResolveTabName(DebugButtonAttribute methodAttribute, DebugButtonClassAttribute classAttribute)
{
if (!string.IsNullOrEmpty(methodAttribute.TabName))
{
return methodAttribute.TabName;
}
if (classAttribute != null && !string.IsNullOrEmpty(classAttribute.TabName))
{
return classAttribute.TabName;
}
return "默认";
}
/// <summary>
/// 解析组名:方法配置优先;当方法组为"默认"时允许被类级别默认组覆盖
/// </summary>
private string ResolveGroupName(DebugButtonAttribute methodAttribute, DebugButtonClassAttribute classAttribute)
{
string methodGroup = string.IsNullOrEmpty(methodAttribute.GroupName) ? "默认" : methodAttribute.GroupName;
if (methodGroup != "默认")
{
return methodGroup;
}
if (classAttribute != null && !string.IsNullOrEmpty(classAttribute.GroupName))
{
return classAttribute.GroupName;
}
return "默认";
}
private string ResolveInputTabName(DebugInputButtonAttribute methodAttribute, DebugButtonClassAttribute classAttribute)
{
if (!string.IsNullOrEmpty(methodAttribute.TabName))
{
return methodAttribute.TabName;
}
if (classAttribute != null && !string.IsNullOrEmpty(classAttribute.TabName))
{
return classAttribute.TabName;
}
return "默认";
}
private string ResolveInputGroupName(DebugInputButtonAttribute methodAttribute, DebugButtonClassAttribute classAttribute)
{
string methodGroup = string.IsNullOrEmpty(methodAttribute.GroupName) ? "默认" : methodAttribute.GroupName;
if (methodGroup != "默认")
{
return methodGroup;
}
if (classAttribute != null && !string.IsNullOrEmpty(classAttribute.GroupName))
{
return classAttribute.GroupName;
}
return "默认";
}
/// <summary>
/// 解析Tab颜色优先类级配置否则使用默认蓝色
/// </summary>
private Color ResolveTabColor(DebugButtonClassAttribute classAttribute)
{
if (classAttribute != null)
{
return classAttribute.TabColor;
}
return new Color(0.2f, 0.6f, 1f, 1f);
}
/// <summary>
/// 注册Tab颜色同名Tab出现多种颜色时保留首个并给出提示
/// </summary>
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($"[CustomButtonsModule] Tab '{tabName}' 检测到多个颜色定义,已保留第一个颜色配置");
}
}
/// <summary>
/// 显示所有Tab和组
/// </summary>
private void BuildButtonViewSkeleton()
{
InvalidateButtonView();
// 确保 buttonContainer 使用垂直布局而不是网格布局
SetupButtonContainerLayout();
if (tabGroups.Count == 0)
{
GameObject emptyText = new GameObject("EmptyTip");
emptyText.transform.SetParent(buttonContainer, false);
TMP_Text text = emptyText.AddComponent<TextMeshProUGUI>();
text.text = "未找到任何 DebugButton";
text.fontSize = 28;
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;
buttonViewInitialized = true;
return;
}
// 1) 创建子Tab栏
subTabBarObject = CreateSubTabBar();
// 2) 创建内容容器
tabContentRootObject = new GameObject("SubTabContentRoot");
tabContentRootObject.transform.SetParent(buttonContainer, 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 = true;
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;
// 按Tab排序"默认"Tab在最前
var sortedTabs = tabGroups.OrderBy(kvp => kvp.Key == "默认" ? "" : kvp.Key);
bool firstTab = true;
foreach (var tab in sortedTabs)
{
string tabName = tab.Key;
var groups = tab.Value;
// 创建Tab按钮
CreateSubTabButton(tabName, subTabBarObject.transform);
// 创建Tab面板此时仅创建空面板内容按需加载
GameObject tabPanel = CreateTabPanel(tabName, tabContentRootObject.transform);
tabPanels[tabName] = tabPanel;
// 默认只显示第一个Tab
tabPanel.SetActive(firstTab);
if (firstTab)
{
currentSubTab = tabName;
firstTab = false;
}
}
Canvas.ForceUpdateCanvases();
UpdateSubTabBarHeight(subTabBarObject);
UpdateSubTabButtonStyles();
// 重置滚动位置到顶部
if (buttonsScrollRect != null)
{
Canvas.ForceUpdateCanvases();
buttonsScrollRect.verticalNormalizedPosition = 1f;
}
buttonViewInitialized = 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 Dictionary<string, List<ButtonInfo>> groups))
{
return;
}
if (subTabBuildCoroutine != null && buildingSubTab == tabName)
{
return;
}
if (UniversalDebugTool.Instance == null || !UniversalDebugTool.Instance.isActiveAndEnabled)
{
BuildSubTabContentImmediately(tabName, tabPanel, groups);
return;
}
StartSubTabContentBuild(tabName, tabPanel, groups);
}
private void StartSubTabContentBuild(string tabName, GameObject tabPanel, Dictionary<string, List<ButtonInfo>> groups)
{
if (tabPanel == null || groups == null)
{
return;
}
CancelSubTabBuildIfNeeded(tabName);
ClearTabPanelContent(tabPanel.transform);
subTabBuildVersion++;
int buildVersion = subTabBuildVersion;
buildingSubTab = tabName;
subTabBuildCoroutine = UniversalDebugTool.Instance.StartCoroutine(BuildSubTabContentCoroutine(tabName, tabPanel, groups, buildVersion));
}
private IEnumerator BuildSubTabContentCoroutine(string tabName, GameObject tabPanel, Dictionary<string, List<ButtonInfo>> groups, int buildVersion)
{
int builtCountInFrame = 0;
try
{
var sortedGroups = groups.OrderBy(kvp => kvp.Key == "默认" ? "" : kvp.Key);
foreach (var group in sortedGroups)
{
if (buildVersion != subTabBuildVersion || tabPanel == null)
{
yield break;
}
string groupName = group.Key;
List<ButtonInfo> buttons = group.Value;
GameObject groupContainer = CreateGroupContainer(groupName, tabPanel.transform);
CreateGroupTitle(groupName, groupContainer.transform);
foreach (var buttonInfo in buttons)
{
if (buildVersion != subTabBuildVersion || tabPanel == null)
{
yield break;
}
CreateCustomButtonInGroup(buttonInfo, groupContainer.transform);
builtCountInFrame++;
if (builtCountInFrame >= ButtonsPerFrame)
{
builtCountInFrame = 0;
Canvas.ForceUpdateCanvases();
yield return null;
}
}
}
builtSubTabs.Add(tabName);
Canvas.ForceUpdateCanvases();
if (buttonsScrollRect != null && currentSubTab == tabName)
{
buttonsScrollRect.verticalNormalizedPosition = 1f;
}
}
finally
{
if (buildVersion == subTabBuildVersion)
{
subTabBuildCoroutine = null;
buildingSubTab = string.Empty;
}
}
}
private void BuildSubTabContentImmediately(string tabName, GameObject tabPanel, Dictionary<string, List<ButtonInfo>> groups)
{
ClearTabPanelContent(tabPanel.transform);
var sortedGroups = groups.OrderBy(kvp => kvp.Key == "默认" ? "" : kvp.Key);
foreach (var group in sortedGroups)
{
string groupName = group.Key;
List<ButtonInfo> buttons = group.Value;
GameObject groupContainer = CreateGroupContainer(groupName, tabPanel.transform);
CreateGroupTitle(groupName, groupContainer.transform);
foreach (var buttonInfo in buttons)
{
CreateCustomButtonInGroup(buttonInfo, groupContainer.transform);
}
}
builtSubTabs.Add(tabName);
Canvas.ForceUpdateCanvases();
}
private void CancelSubTabBuildIfNeeded(string nextTabName = null)
{
if (subTabBuildCoroutine == null)
{
return;
}
if (!string.IsNullOrEmpty(nextTabName) && buildingSubTab == nextTabName)
{
return;
}
if (UniversalDebugTool.Instance != null)
{
UniversalDebugTool.Instance.StopCoroutine(subTabBuildCoroutine);
}
subTabBuildCoroutine = null;
buildingSubTab = string.Empty;
subTabBuildVersion++;
}
private void ClearTabPanelContent(Transform tabPanelTransform)
{
if (tabPanelTransform == null)
{
return;
}
for (int i = tabPanelTransform.childCount - 1; i >= 0; i--)
{
UnityEngine.Object.Destroy(tabPanelTransform.GetChild(i).gameObject);
}
}
private void InvalidateButtonView()
{
CancelSubTabBuildIfNeeded();
if (buttonContainer != null)
{
foreach (Transform child in buttonContainer)
{
UnityEngine.Object.Destroy(child.gameObject);
}
}
tabPanels.Clear();
subTabButtons.Clear();
buttonRowStates.Clear();
builtSubTabs.Clear();
currentSubTab = string.Empty;
subTabBarObject = null;
tabContentRootObject = null;
buttonViewInitialized = false;
}
private GameObject CreateSubTabBar()
{
GameObject subTabBar = new GameObject("SubTabBar");
subTabBar.transform.SetParent(buttonContainer, 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.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 && buttonContainer != null)
{
availableWidth = buttonContainer.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 = true;
layout.spacing = 12;
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;
}
bool isSameTab = currentSubTab == tabName;
currentSubTab = tabName;
foreach (var kvp in tabPanels)
{
kvp.Value.SetActive(kvp.Key == tabName);
}
UpdateSubTabButtonStyles();
EnsureSubTabContentBuilt(tabName);
if (isSameTab && !builtSubTabs.Contains(tabName))
{
Canvas.ForceUpdateCanvases();
}
if (buttonsScrollRect != null)
{
Canvas.ForceUpdateCanvases();
buttonsScrollRect.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;
}
/// <summary>
/// 设置 buttonContainer 的布局
/// </summary>
private void SetupButtonContainerLayout()
{
if (buttonContainer == null)
{
Debug.LogError("[CustomButtonsModule] buttonContainer 为 null无法设置布局");
return;
}
GameObject containerObject = buttonContainer.gameObject;
// 移除可能存在的 GridLayoutGroup
GridLayoutGroup gridLayout = containerObject.GetComponent<GridLayoutGroup>();
if (gridLayout != null)
{
UnityEngine.Object.DestroyImmediate(gridLayout);
}
// 添加或获取 VerticalLayoutGroup
VerticalLayoutGroup verticalLayout = containerObject.GetComponent<VerticalLayoutGroup>();
if (verticalLayout == null)
{
verticalLayout = containerObject.AddComponent<VerticalLayoutGroup>();
if (verticalLayout == null)
{
Debug.LogError("[CustomButtonsModule] 错误:无法添加 VerticalLayoutGroup");
return;
}
}
// 配置垂直布局
verticalLayout.childForceExpandWidth = true;
verticalLayout.childForceExpandHeight = false;
verticalLayout.childControlWidth = true;
verticalLayout.childControlHeight = true;
verticalLayout.spacing = 20;
verticalLayout.padding = new RectOffset(10, 10, 10, 10);
// 确保有 ContentSizeFitter
ContentSizeFitter sizeFitter = containerObject.GetComponent<ContentSizeFitter>();
if (sizeFitter == null)
{
sizeFitter = containerObject.AddComponent<ContentSizeFitter>();
}
sizeFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
sizeFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained;
}
/// <summary>
/// 创建组容器
/// </summary>
private GameObject CreateGroupContainer(string groupName, Transform parent)
{
GameObject groupContainer = new GameObject($"Group_{groupName}");
groupContainer.transform.SetParent(parent, false);
// 添加RectTransform
RectTransform rectTransform = groupContainer.AddComponent<RectTransform>();
// 添加VerticalLayoutGroup让标题和按钮垂直排列
VerticalLayoutGroup verticalLayout = groupContainer.AddComponent<VerticalLayoutGroup>();
verticalLayout.childForceExpandWidth = true;
verticalLayout.childForceExpandHeight = false;
verticalLayout.childControlWidth = true;
verticalLayout.childControlHeight = true;
verticalLayout.spacing = 5;
verticalLayout.padding = new RectOffset(0, 0, 10, 10);
// 添加ContentSizeFitter让容器自动调整大小
ContentSizeFitter sizeFitter = groupContainer.AddComponent<ContentSizeFitter>();
sizeFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
sizeFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained;
// 添加LayoutElement确保占满宽度
LayoutElement layoutElement = groupContainer.AddComponent<LayoutElement>();
layoutElement.flexibleWidth = 1;
return groupContainer;
}
/// <summary>
/// 创建组标题
/// </summary>
private void CreateGroupTitle(string groupName, Transform parent)
{
// 创建标题GameObject
GameObject titleObj = new GameObject($"Title_{groupName}");
titleObj.transform.SetParent(parent, false);
// 添加RectTransform
RectTransform rectTransform = titleObj.AddComponent<RectTransform>();
// 添加TextMeshProUGUI组件
TMP_Text titleText = titleObj.AddComponent<TextMeshProUGUI>();
titleText.text = groupName;
titleText.fontSize = 32;
titleText.fontStyle = FontStyles.Bold;
titleText.alignment = TextAlignmentOptions.Left;
titleText.color = new Color(1f, 1f, 1f, 0.9f);
titleText.margin = new Vector4(10, 5, 0, 5);
// 应用保存的字体
if (savedFontAsset != null)
{
titleText.font = savedFontAsset;
}
// 添加LayoutElement
LayoutElement layoutElement = titleObj.AddComponent<LayoutElement>();
layoutElement.preferredHeight = 50;
layoutElement.flexibleWidth = 1;
}
/// <summary>
/// 在组容器中创建按钮(需要创建一个按钮区域容器)
/// </summary>
private void CreateCustomButtonInGroup(ButtonInfo buttonInfo, Transform groupParent)
{
// 查找或创建按钮区域容器
Transform buttonArea = groupParent.Find("ButtonArea");
if (buttonArea == null)
{
GameObject buttonAreaObj = new GameObject("ButtonArea");
buttonAreaObj.transform.SetParent(groupParent, false);
RectTransform buttonAreaRect = buttonAreaObj.AddComponent<RectTransform>();
buttonAreaRect.anchorMin = new Vector2(0f, 1f);
buttonAreaRect.anchorMax = new Vector2(1f, 1f);
buttonAreaRect.pivot = new Vector2(0.5f, 1f);
buttonAreaRect.offsetMin = new Vector2(0f, buttonAreaRect.offsetMin.y);
buttonAreaRect.offsetMax = new Vector2(0f, buttonAreaRect.offsetMax.y);
VerticalLayoutGroup verticalLayout = buttonAreaObj.AddComponent<VerticalLayoutGroup>();
verticalLayout.spacing = ButtonSpacing;
verticalLayout.padding = new RectOffset(0, 0, 0, 0);
verticalLayout.childControlWidth = true;
verticalLayout.childControlHeight = true;
verticalLayout.childForceExpandWidth = true;
verticalLayout.childForceExpandHeight = false;
// 添加ContentSizeFitter
ContentSizeFitter sizeFitter = buttonAreaObj.AddComponent<ContentSizeFitter>();
sizeFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
sizeFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained;
// 添加LayoutElement
LayoutElement layoutElement = buttonAreaObj.AddComponent<LayoutElement>();
layoutElement.flexibleWidth = 1;
buttonArea = buttonAreaObj.transform;
}
Transform rowTransform = GetOrCreateButtonRow(buttonArea, buttonInfo.DisplayName);
// 在按钮区域中创建按钮
GameObject buttonObj = UnityEngine.Object.Instantiate(buttonPrefab, rowTransform);
Button button = buttonObj.GetComponent<Button>();
TMP_Text buttonText = buttonObj.GetComponentInChildren<TMP_Text>();
Image buttonImage = buttonObj.GetComponent<Image>();
// 设置按钮文本
if (buttonText != null)
{
buttonText.text = buttonInfo.DisplayName;
buttonText.enableWordWrapping = false;
buttonText.overflowMode = TextOverflowModes.Overflow;
// 应用保存的字体
if (savedFontAsset != null)
{
buttonText.font = savedFontAsset;
}
}
ApplyAdaptiveButtonSize(buttonObj, buttonText, buttonInfo.DisplayName);
UpdateButtonRowLayout(buttonArea, rowTransform, buttonObj);
// 设置按钮颜色
if (buttonImage != null && buttonInfo.ButtonColor != default(Color))
{
buttonImage.color = buttonInfo.ButtonColor;
}
// 设置按钮点击事件
button.onClick.AddListener(() =>
{
try
{
if (buttonInfo.RequiresInput)
{
if (buttonInfo.Method != null)
{
UniversalDebugTool.ShowMethodInputDialog(buttonInfo.Method, buttonInfo.InputAttribute, () =>
{
customButtonCallback?.Invoke(button, buttonText);
customButtonCallback = null;
onCloseWindowCallback?.Invoke();
});
return;
}
if (buttonInfo.RuntimeInputDefinition != null)
{
UniversalDebugTool.ShowRuntimeInputDialog(buttonInfo.RuntimeInputDefinition, () =>
{
customButtonCallback?.Invoke(button, buttonText);
customButtonCallback = null;
onCloseWindowCallback?.Invoke();
});
return;
}
return;
}
if (buttonInfo.Method != null)
{
buttonInfo.Method.Invoke(null, null);
Debug.Log($"[CustomButtonsModule] 执行调试方法: {buttonInfo.Method.Name}");
}
else
{
buttonInfo.Callback?.Invoke();
Debug.Log($"[CustomButtonsModule] 执行运行时按钮: {buttonInfo.SourceName}");
}
// 调用回调
customButtonCallback?.Invoke(button, buttonText);
customButtonCallback = null;
// 点击按钮后关闭主窗口
onCloseWindowCallback?.Invoke();
}
catch (Exception e)
{
string sourceName = buttonInfo.Method != null ? buttonInfo.Method.Name : buttonInfo.SourceName;
Debug.LogError($"[CustomButtonsModule] 执行调试按钮 {sourceName} 时出错: {e.Message}");
}
});
}
private void ApplyAdaptiveButtonSize(GameObject buttonObj, TMP_Text buttonText, string displayName)
{
if (buttonObj == null)
{
return;
}
RectTransform rectTransform = buttonObj.GetComponent<RectTransform>();
LayoutElement layoutElement = buttonObj.GetComponent<LayoutElement>();
if (layoutElement == null)
{
layoutElement = buttonObj.AddComponent<LayoutElement>();
}
float preferredWidth = CalculateButtonPreferredWidth(displayName, buttonText);
float availableWidth = GetAvailableButtonWidth(buttonObj.transform.parent as RectTransform);
if (availableWidth > 0f)
{
preferredWidth = Mathf.Min(preferredWidth, availableWidth);
}
if (rectTransform != null)
{
rectTransform.sizeDelta = new Vector2(preferredWidth, ButtonHeight);
}
layoutElement.minWidth = ButtonMinWidth;
layoutElement.preferredWidth = preferredWidth;
layoutElement.minHeight = ButtonHeight;
layoutElement.preferredHeight = ButtonHeight;
}
private Transform GetOrCreateButtonRow(Transform buttonArea, string displayName)
{
if (!buttonRowStates.TryGetValue(buttonArea, out var rowState))
{
rowState = new ButtonRowLayoutState();
buttonRowStates[buttonArea] = rowState;
}
float nextButtonWidth = CalculateButtonPreferredWidth(displayName, null);
float availableWidth = GetAvailableButtonWidth(buttonArea as RectTransform);
if (rowState.CurrentRow == null)
{
rowState.CurrentRow = CreateButtonRow(buttonArea).transform;
rowState.CurrentRowWidth = 0f;
}
if (availableWidth > 0f && rowState.CurrentRowWidth > 0f && rowState.CurrentRowWidth + ButtonSpacing + nextButtonWidth > availableWidth)
{
rowState.CurrentRow = CreateButtonRow(buttonArea).transform;
rowState.CurrentRowWidth = 0f;
}
return rowState.CurrentRow;
}
private void UpdateButtonRowLayout(Transform buttonArea, Transform rowTransform, GameObject buttonObj)
{
if (!buttonRowStates.TryGetValue(buttonArea, out var rowState))
{
return;
}
LayoutElement layoutElement = buttonObj.GetComponent<LayoutElement>();
float buttonWidth = layoutElement != null ? layoutElement.preferredWidth : ButtonMinWidth;
if (rowState.CurrentRow == rowTransform)
{
if (rowState.CurrentRowWidth > 0f)
{
rowState.CurrentRowWidth += ButtonSpacing;
}
rowState.CurrentRowWidth += buttonWidth;
}
}
private GameObject CreateButtonRow(Transform buttonArea)
{
GameObject rowObj = new GameObject("ButtonRow");
rowObj.transform.SetParent(buttonArea, false);
RectTransform rowRect = rowObj.AddComponent<RectTransform>();
rowRect.anchorMin = new Vector2(0f, 1f);
rowRect.anchorMax = new Vector2(1f, 1f);
rowRect.pivot = new Vector2(0.5f, 1f);
rowRect.offsetMin = new Vector2(0f, rowRect.offsetMin.y);
rowRect.offsetMax = new Vector2(0f, rowRect.offsetMax.y);
HorizontalLayoutGroup horizontalLayout = rowObj.AddComponent<HorizontalLayoutGroup>();
horizontalLayout.spacing = ButtonSpacing;
horizontalLayout.padding = new RectOffset(0, 0, 0, 0);
horizontalLayout.childAlignment = TextAnchor.UpperLeft;
horizontalLayout.childControlWidth = false;
horizontalLayout.childControlHeight = true;
horizontalLayout.childForceExpandWidth = false;
horizontalLayout.childForceExpandHeight = false;
ContentSizeFitter sizeFitter = rowObj.AddComponent<ContentSizeFitter>();
sizeFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained;
sizeFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
LayoutElement layoutElement = rowObj.AddComponent<LayoutElement>();
layoutElement.flexibleWidth = 1;
layoutElement.preferredHeight = ButtonHeight;
return rowObj;
}
private float CalculateButtonPreferredWidth(string displayName, TMP_Text buttonText)
{
string content = string.IsNullOrEmpty(displayName) ? "按钮" : displayName;
float estimatedWidth = EstimateButtonWidth(content);
if (buttonText != null)
{
buttonText.ForceMeshUpdate();
Vector2 preferredValues = buttonText.GetPreferredValues(content, 10000f, 1000f);
float preferredWidth = Mathf.Max(preferredValues.x, buttonText.preferredWidth, estimatedWidth - 160f);
return Mathf.Clamp(Mathf.Ceil(preferredWidth + 160f), ButtonMinWidth, ButtonMaxWidth);
}
return Mathf.Clamp(estimatedWidth, ButtonMinWidth, ButtonMaxWidth);
}
private float EstimateButtonWidth(string content)
{
float textWidth = 0f;
foreach (char c in content)
{
if (char.IsWhiteSpace(c))
{
textWidth += 14f;
}
else if (c <= 127)
{
textWidth += 18f;
}
else
{
textWidth += 32f;
}
}
return Mathf.Clamp(textWidth + 100f, ButtonMinWidth, ButtonMaxWidth);
}
private float GetAvailableButtonWidth(RectTransform parentRect)
{
if (buttonContainer != null)
{
float containerWidth = buttonContainer.rect.width;
if (containerWidth > 0f)
{
return Mathf.Max(ButtonMinWidth, containerWidth - 40f);
}
}
if (parentRect == null)
{
return -1f;
}
float width = parentRect.rect.width;
Transform current = parentRect.parent;
while (width <= 0f && current is RectTransform currentRect)
{
width = currentRect.rect.width;
current = current.parent;
}
if (width <= 0f)
{
return -1f;
}
return Mathf.Max(ButtonMinWidth, width - 40f);
}
private void CreateCustomButton(MethodInfo method, DebugButtonAttribute attribute)
{
GameObject buttonObj = UnityEngine.Object.Instantiate(buttonPrefab, buttonContainer);
Button button = buttonObj.GetComponent<Button>();
TMP_Text buttonText = buttonObj.GetComponentInChildren<TMP_Text>();
Image buttonImage = buttonObj.GetComponent<Image>();
// 设置按钮文本
if (buttonText != null)
{
buttonText.text = string.IsNullOrEmpty(attribute.DisplayName) ? method.Name : attribute.DisplayName;
// 应用保存的字体
if (savedFontAsset != null)
{
buttonText.font = savedFontAsset;
}
}
// 设置按钮颜色
if (buttonImage != null && attribute.ButtonColor != default(Color))
{
buttonImage.color = attribute.ButtonColor;
}
// 设置按钮点击事件
button.onClick.AddListener(() =>
{
try
{
method.Invoke(null, null);
Debug.Log($"[CustomButtonsModule] 执行调试方法: {method.Name}");
// 调用回调
customButtonCallback?.Invoke(button, buttonText);
customButtonCallback = null;
// 点击按钮后关闭主窗口
onCloseWindowCallback?.Invoke();
}
catch (Exception e)
{
Debug.LogError($"[CustomButtonsModule] 执行调试方法 {method.Name} 时出错: {e.Message}");
}
});
}
#endregion
}
}