1584 lines
59 KiB
C#
1584 lines
59 KiB
C#
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 = 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 const float ButtonMinWidth = 260f;
|
||
private const float ButtonMaxWidth = 800f;
|
||
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.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;
|
||
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.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 && 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 = true;
|
||
buttonText.overflowMode = TextOverflowModes.Truncate;
|
||
|
||
// 应用保存的字体
|
||
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 = true;
|
||
horizontalLayout.childControlHeight = true;
|
||
horizontalLayout.childForceExpandWidth = true;
|
||
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
|
||
}
|
||
}
|