592 lines
24 KiB
C#
592 lines
24 KiB
C#
using System;
|
||
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Reflection;
|
||
using System.Text;
|
||
using AlicizaX.UI.Editor;
|
||
using AlicizaX.UI.Runtime;
|
||
using UnityEngine;
|
||
using UnityEditor;
|
||
using UnityEditor.Callbacks;
|
||
using UnityEditor.SceneManagement;
|
||
|
||
namespace AlicizaX.UI.Editor
|
||
{
|
||
public enum EBindType
|
||
{
|
||
None,
|
||
Widget,
|
||
ListCom,
|
||
}
|
||
|
||
|
||
[Serializable]
|
||
internal class UIBindData
|
||
{
|
||
public string Name;
|
||
public List<Component> BindCom;
|
||
public EBindType BindType;
|
||
|
||
public UIBindData(string name, List<Component> bindCom, EBindType bindType = EBindType.None)
|
||
{
|
||
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||
BindCom = bindCom ?? new List<Component>();
|
||
BindType = bindType;
|
||
}
|
||
|
||
public UIBindData(string name, Component bindCom, EBindType bindType = EBindType.None)
|
||
: this(name, new List<Component> { bindCom }, bindType)
|
||
{
|
||
}
|
||
}
|
||
|
||
internal static class UIScriptGeneratorHelper
|
||
{
|
||
private static UIGenerateConfiguration _uiGenerateConfiguration;
|
||
private static IUIGeneratorRuleHelper _uiGeneratorRuleHelper;
|
||
|
||
private static List<UIBindData> _uiBindDatas = new List<UIBindData>();
|
||
private static HashSet<string> _arrayComponents = new HashSet<string>(StringComparer.Ordinal);
|
||
|
||
public static IUIGeneratorRuleHelper UIGeneratorRuleHelper
|
||
{
|
||
get
|
||
{
|
||
if (_uiGeneratorRuleHelper == null ||
|
||
(_uiGeneratorRuleHelper != null && !UIConfiguration.UIScriptGeneratorRuleHelper.Equals(_uiGeneratorRuleHelper.GetType().FullName, StringComparison.Ordinal)))
|
||
{
|
||
var ruleHelperTypeName = UIConfiguration.UIScriptGeneratorRuleHelper;
|
||
if (string.IsNullOrWhiteSpace(ruleHelperTypeName))
|
||
{
|
||
Debug.LogError("UIScriptGeneratorHelper: UIScriptGeneratorRuleHelper not configured.");
|
||
return null;
|
||
}
|
||
|
||
Type ruleHelperType = Type.GetType(ruleHelperTypeName);
|
||
if (ruleHelperType == null)
|
||
{
|
||
Debug.LogError($"UIScriptGeneratorHelper: Could not load UI ScriptGeneratorHelper {ruleHelperTypeName}");
|
||
return null;
|
||
}
|
||
|
||
_uiGeneratorRuleHelper = Activator.CreateInstance(ruleHelperType) as IUIGeneratorRuleHelper;
|
||
if (_uiGeneratorRuleHelper == null)
|
||
{
|
||
Debug.LogError($"UIScriptGeneratorHelper: Failed to instantiate {ruleHelperTypeName} as IUIGeneratorRuleHelper.");
|
||
}
|
||
}
|
||
|
||
return _uiGeneratorRuleHelper;
|
||
}
|
||
}
|
||
|
||
private static UIGenerateConfiguration UIConfiguration
|
||
{
|
||
get
|
||
{
|
||
if (_uiGenerateConfiguration == null)
|
||
{
|
||
_uiGenerateConfiguration = UIGenerateConfiguration.Instance;
|
||
}
|
||
|
||
return _uiGenerateConfiguration;
|
||
}
|
||
}
|
||
|
||
private static string GetUIElementComponentType(string uiName)
|
||
{
|
||
if (string.IsNullOrEmpty(uiName)) return string.Empty;
|
||
foreach (var pair in UIConfiguration.UIElementRegexConfigs ?? Enumerable.Empty<UIEelementRegexData>())
|
||
{
|
||
if (string.IsNullOrEmpty(pair?.uiElementRegex)) continue;
|
||
if (uiName.StartsWith(pair.uiElementRegex, StringComparison.Ordinal))
|
||
{
|
||
return pair.componentType ?? string.Empty;
|
||
}
|
||
}
|
||
|
||
return string.Empty;
|
||
}
|
||
|
||
private static string[] SplitComponentName(string name)
|
||
{
|
||
if (string.IsNullOrEmpty(name)) return null;
|
||
|
||
var common = UIConfiguration.UIGenerateCommonData;
|
||
if (string.IsNullOrEmpty(common?.ComCheckEndName) || !name.Contains(common.ComCheckEndName))
|
||
return null;
|
||
|
||
int endIndex = name.IndexOf(common.ComCheckEndName, StringComparison.Ordinal);
|
||
if (endIndex <= 0) return null;
|
||
|
||
string comStr = name.Substring(0, endIndex);
|
||
string split = common.ComCheckSplitName ?? "#";
|
||
// 使用 string[] 重载并移除空项,防止错误 overload
|
||
return comStr.Split(new[] { split }, StringSplitOptions.RemoveEmptyEntries);
|
||
}
|
||
|
||
private static string GetVariableName(string key, string componentName, EBindType bindType)
|
||
{
|
||
var helper = UIGeneratorRuleHelper;
|
||
if (helper == null)
|
||
throw new InvalidOperationException("UIGeneratorRuleHelper is not configured.");
|
||
return helper.GetPrivateComponentByNameRule(key, componentName, bindType);
|
||
}
|
||
|
||
private static void GetBindData(Transform root)
|
||
{
|
||
if (root == null) return;
|
||
|
||
for (int i = 0; i < root.childCount; ++i)
|
||
{
|
||
Transform child = root.GetChild(i);
|
||
if (child == null) continue;
|
||
|
||
// 排除关键字
|
||
if (UIConfiguration.UIGenerateCommonData.ExcludeKeywords != null &&
|
||
UIConfiguration.UIGenerateCommonData.ExcludeKeywords.Any(k =>
|
||
!string.IsNullOrEmpty(k) && child.name.IndexOf(k, StringComparison.OrdinalIgnoreCase) >= 0))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
bool hasWidget = child.GetComponent<UIHolderObjectBase>() != null;
|
||
bool isArrayComponent = !string.IsNullOrEmpty(UIConfiguration.UIGenerateCommonData.ArrayComSplitName) &&
|
||
child.name.StartsWith(UIConfiguration.UIGenerateCommonData.ArrayComSplitName, StringComparison.Ordinal);
|
||
|
||
if (hasWidget)
|
||
{
|
||
CollectWidget(child);
|
||
}
|
||
else if (isArrayComponent)
|
||
{
|
||
// 提取 array 标识文本(例如 "*Item*0" -> "Item")
|
||
string splitCode = UIConfiguration.UIGenerateCommonData.ArrayComSplitName;
|
||
int firstIndex = child.name.IndexOf(splitCode, StringComparison.Ordinal);
|
||
int lastIndex = child.name.LastIndexOf(splitCode, StringComparison.Ordinal);
|
||
if (firstIndex < 0 || lastIndex <= firstIndex) continue;
|
||
|
||
// 中间文本
|
||
string text = child.name.Substring(firstIndex + splitCode.Length, lastIndex - (firstIndex + splitCode.Length));
|
||
if (string.IsNullOrEmpty(text)) continue;
|
||
|
||
if (_arrayComponents.Contains(text)) continue;
|
||
_arrayComponents.Add(text);
|
||
|
||
// 在同一个父节点下收集包含该 text 的同级节点
|
||
List<Transform> arrayComponents = new List<Transform>();
|
||
for (int j = 0; j < root.childCount; j++)
|
||
{
|
||
Transform sibling = root.GetChild(j);
|
||
if (sibling != null && sibling.name.Contains(text, StringComparison.Ordinal))
|
||
{
|
||
arrayComponents.Add(sibling);
|
||
}
|
||
}
|
||
|
||
CollectArrayComponent(arrayComponents, text);
|
||
}
|
||
else // 普通组件/进一步递归
|
||
{
|
||
CollectComponent(child);
|
||
GetBindData(child);
|
||
}
|
||
}
|
||
}
|
||
|
||
private static void CollectComponent(Transform node)
|
||
{
|
||
if (node == null) return;
|
||
|
||
string[] componentArray = SplitComponentName(node.name);
|
||
if (componentArray == null || componentArray.Length == 0) return;
|
||
|
||
foreach (var com in componentArray)
|
||
{
|
||
if (string.IsNullOrEmpty(com)) continue;
|
||
string typeName = GetUIElementComponentType(com);
|
||
if (string.IsNullOrEmpty(typeName)) continue;
|
||
|
||
Component component = node.GetComponent(typeName);
|
||
if (component != null)
|
||
{
|
||
string keyName = GetVariableName(com, node.name, EBindType.None);
|
||
if (_uiBindDatas.Exists(a => a.Name == keyName))
|
||
{
|
||
Debug.LogError($"Duplicate key found: {keyName}");
|
||
continue;
|
||
}
|
||
|
||
_uiBindDatas.Add(new UIBindData(keyName, component));
|
||
}
|
||
else
|
||
{
|
||
Debug.LogError($"{node.name} does not have component of type {typeName}");
|
||
}
|
||
}
|
||
}
|
||
|
||
private static void CollectWidget(Transform node)
|
||
{
|
||
if (node == null) return;
|
||
|
||
var common = UIConfiguration.UIGenerateCommonData;
|
||
if (node.name.IndexOf(common.ComCheckEndName, StringComparison.Ordinal) != -1 &&
|
||
node.name.IndexOf(common.ComCheckSplitName, StringComparison.Ordinal) != -1)
|
||
{
|
||
Debug.LogWarning($"{node.name} child component cannot contain rule definition symbols!");
|
||
return;
|
||
}
|
||
|
||
UIHolderObjectBase component = node.GetComponent<UIHolderObjectBase>();
|
||
if (component == null)
|
||
{
|
||
Debug.LogError($"{node.name} expected to be a widget but does not have UIHolderObjectBase.");
|
||
return;
|
||
}
|
||
|
||
string keyName = GetVariableName(string.Empty, node.name, EBindType.Widget);
|
||
if (_uiBindDatas.Exists(a => a.Name == keyName))
|
||
{
|
||
Debug.LogError($"Duplicate key found: {keyName}");
|
||
return;
|
||
}
|
||
|
||
_uiBindDatas.Add(new UIBindData(keyName, component, EBindType.Widget));
|
||
}
|
||
|
||
private static void CollectArrayComponent(List<Transform> arrayNode, string nodeName)
|
||
{
|
||
if (arrayNode == null || arrayNode.Count == 0) return;
|
||
|
||
// 从 nodeName(例如 "*Item*0")取出组件描述部分(即名字中 @End 之前那部分)
|
||
string[] componentArray = SplitComponentName(nodeName);
|
||
|
||
// 对 arrayNode 做基于后缀的安全排序:提取 last segment 作为索引(int.TryParse)
|
||
string splitCode = UIConfiguration.UIGenerateCommonData.ArrayComSplitName;
|
||
var orderedNodes = arrayNode
|
||
.Select(n => new { Node = n, RawIndex = ExtractArrayIndex(n.name, splitCode) })
|
||
.OrderBy(x => x.RawIndex.HasValue ? x.RawIndex.Value : int.MaxValue)
|
||
.Select(x => x.Node)
|
||
.ToList();
|
||
|
||
if (componentArray == null || componentArray.Length == 0)
|
||
{
|
||
Debug.LogWarning($"CollectArrayComponent: {nodeName} has no component definitions.");
|
||
return;
|
||
}
|
||
|
||
// 准备临时 bind 列表,每个 componentArray 项对应一个 UIBindData
|
||
List<UIBindData> tempBindDatas = new List<UIBindData>(componentArray.Length);
|
||
for (int i = 0; i < componentArray.Length; i++)
|
||
{
|
||
string keyNamePreview = GetVariableName(componentArray[i], nodeName, EBindType.ListCom);
|
||
tempBindDatas.Add(new UIBindData(keyNamePreview, new List<Component>(), EBindType.ListCom));
|
||
}
|
||
|
||
// 遍历元素并填充
|
||
for (int index = 0; index < componentArray.Length; index++)
|
||
{
|
||
string com = componentArray[index];
|
||
if (string.IsNullOrEmpty(com)) continue;
|
||
|
||
string typeName = GetUIElementComponentType(com);
|
||
if (string.IsNullOrEmpty(typeName)) continue;
|
||
|
||
foreach (var node in orderedNodes)
|
||
{
|
||
Component component = node.GetComponent(typeName);
|
||
if (component != null)
|
||
{
|
||
tempBindDatas[index].BindCom.Add(component);
|
||
}
|
||
else
|
||
{
|
||
Debug.LogError($"{node.name} does not have component of type {typeName}");
|
||
}
|
||
}
|
||
}
|
||
|
||
// 将结果合并到全局绑定数据
|
||
_uiBindDatas.AddRange(tempBindDatas);
|
||
}
|
||
|
||
private static int? ExtractArrayIndex(string nodeName, string splitCode)
|
||
{
|
||
if (string.IsNullOrEmpty(nodeName) || string.IsNullOrEmpty(splitCode)) return null;
|
||
int last = nodeName.LastIndexOf(splitCode, StringComparison.Ordinal);
|
||
if (last < 0) return null;
|
||
string suffix = nodeName.Substring(last + splitCode.Length);
|
||
if (int.TryParse(suffix, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out int idx))
|
||
return idx;
|
||
return null;
|
||
}
|
||
|
||
private static string GetReferenceNamespace()
|
||
{
|
||
HashSet<string> namespaceSet = new HashSet<string>(StringComparer.Ordinal);
|
||
|
||
namespaceSet.Add("UnityEngine");
|
||
if (_uiBindDatas.Any(d => d.BindType == EBindType.ListCom))
|
||
namespaceSet.Add("System.Collections.Generic");
|
||
|
||
foreach (var bindData in _uiBindDatas)
|
||
{
|
||
var ns = bindData.BindCom?.FirstOrDefault()?.GetType().Namespace;
|
||
if (!string.IsNullOrEmpty(ns) && !namespaceSet.Contains(ns))
|
||
{
|
||
namespaceSet.Add(ns);
|
||
}
|
||
}
|
||
|
||
return new StringBuilder().Append(string.Join(Environment.NewLine, namespaceSet.Select(n => $"using {n};"))).ToString();
|
||
}
|
||
|
||
private static string GetVariableText(List<UIBindData> uiBindDatas)
|
||
{
|
||
if (uiBindDatas == null || uiBindDatas.Count == 0) return string.Empty;
|
||
|
||
StringBuilder variableTextBuilder = new StringBuilder();
|
||
var helper = UIGeneratorRuleHelper;
|
||
if (helper == null) throw new InvalidOperationException("UIGeneratorRuleHelper is not configured.");
|
||
|
||
var result = string.Join("\n\n", uiBindDatas
|
||
.Where(b => b != null && !string.IsNullOrEmpty(b.Name))
|
||
.Select(b =>
|
||
{
|
||
string variableName = b.Name;
|
||
string publicName = helper.GetPublicComponentByNameRule(variableName);
|
||
var firstType = b.BindCom?.FirstOrDefault()?.GetType();
|
||
string typeName = firstType?.Name ?? "Component";
|
||
|
||
var sb = new StringBuilder();
|
||
sb.AppendLine("\t\t[SerializeField]");
|
||
|
||
if (b.BindType == EBindType.None || b.BindType == EBindType.Widget)
|
||
{
|
||
sb.AppendLine($"\t\tprivate {typeName} {variableName};");
|
||
sb.Append($"\t\tpublic {typeName} {publicName} => {variableName};");
|
||
}
|
||
else if (b.BindType == EBindType.ListCom)
|
||
{
|
||
int count = Math.Max(0, b.BindCom?.Count ?? 0);
|
||
sb.AppendLine($"\t\tprivate {typeName}[] {variableName} = new {typeName}[{count}];");
|
||
sb.Append($"\t\tpublic {typeName}[] {publicName} => {variableName};");
|
||
}
|
||
|
||
return sb.ToString();
|
||
}));
|
||
|
||
return variableTextBuilder.Append(result).ToString();
|
||
}
|
||
|
||
|
||
public static void GenerateAndAttachScript(GameObject targetObject, UIScriptGenerateData scriptGenerateData)
|
||
{
|
||
if (targetObject == null) throw new ArgumentNullException(nameof(targetObject));
|
||
if (scriptGenerateData == null) throw new ArgumentNullException(nameof(scriptGenerateData));
|
||
|
||
if (!PrefabChecker.IsPrefabAsset(targetObject))
|
||
{
|
||
Debug.LogWarning("请将UI界面保存为对应的目录Prefab 在进行代码生成");
|
||
return;
|
||
}
|
||
|
||
var ruleHelper = UIGeneratorRuleHelper;
|
||
if (ruleHelper == null)
|
||
{
|
||
Debug.LogError("UIGeneratorRuleHelper not available, abort.");
|
||
return;
|
||
}
|
||
|
||
if (!ruleHelper.CheckCanGenerate(targetObject, scriptGenerateData))
|
||
{
|
||
return;
|
||
}
|
||
|
||
EditorPrefs.SetInt("InstanceId", targetObject.GetInstanceID());
|
||
_uiBindDatas.Clear();
|
||
_arrayComponents.Clear();
|
||
|
||
string className = ruleHelper.GetClassGenerateName(targetObject, scriptGenerateData);
|
||
if (string.IsNullOrEmpty(className))
|
||
{
|
||
Debug.LogError("Generated className is empty.");
|
||
return;
|
||
}
|
||
|
||
GetBindData(targetObject.transform);
|
||
|
||
string uiTemplateText = File.ReadAllText(UIGlobalPath.TemplatePath);
|
||
uiTemplateText = uiTemplateText.Replace("#ReferenceNameSpace#", GetReferenceNamespace());
|
||
uiTemplateText = uiTemplateText.Replace("#ClassNameSpace#", scriptGenerateData.NameSpace);
|
||
uiTemplateText = uiTemplateText.Replace("#ClassName#", className);
|
||
uiTemplateText = uiTemplateText.Replace("#TagName#", ruleHelper.GetUIResourceSavePath(targetObject, scriptGenerateData));
|
||
uiTemplateText = uiTemplateText.Replace("#LoadType#", scriptGenerateData.LoadType.ToString());
|
||
uiTemplateText = uiTemplateText.Replace("#Variable#", GetVariableText(_uiBindDatas));
|
||
|
||
ruleHelper.WriteUIScriptContent(className, uiTemplateText, scriptGenerateData);
|
||
}
|
||
|
||
[DidReloadScripts]
|
||
public static void CheckHasAttach()
|
||
{
|
||
if (!EditorPrefs.HasKey("Generate"))
|
||
return;
|
||
EditorPrefs.DeleteKey("Generate");
|
||
_uiBindDatas.Clear();
|
||
_arrayComponents.Clear();
|
||
|
||
string className = EditorPrefs.GetString("Generate");
|
||
int instanceId = EditorPrefs.GetInt("InstanceId", -1);
|
||
|
||
if (instanceId == -1)
|
||
{
|
||
Debug.LogWarning("CheckHasAttach: InstanceId missing.");
|
||
|
||
return;
|
||
}
|
||
|
||
GameObject targetObject = EditorUtility.InstanceIDToObject(instanceId) as GameObject;
|
||
|
||
if (targetObject == null)
|
||
{
|
||
Debug.LogWarning("UI script generation attachment object missing!");
|
||
return;
|
||
}
|
||
|
||
|
||
GetBindData(targetObject.transform);
|
||
|
||
BindScriptPropertyField(targetObject, className);
|
||
Debug.Log($"Generate {className} Successfully attached to game object");
|
||
|
||
}
|
||
|
||
private static void BindScriptPropertyField(GameObject targetObject, string scriptClassName)
|
||
{
|
||
if (targetObject == null) throw new ArgumentNullException(nameof(targetObject));
|
||
if (string.IsNullOrEmpty(scriptClassName)) throw new ArgumentNullException(nameof(scriptClassName));
|
||
|
||
Type scriptType = null;
|
||
|
||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||
{
|
||
// 跳过典型的编辑器专用程序集(但只在程序集名字结束或包含 ".Editor" 时跳过)
|
||
var asmName = assembly.GetName().Name ?? string.Empty;
|
||
if (asmName.EndsWith(".Editor", StringComparison.OrdinalIgnoreCase) ||
|
||
asmName.Equals("UnityEditor", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
Type[] types= assembly.GetTypes();
|
||
|
||
foreach (var type in types)
|
||
{
|
||
if (!type.IsClass || type.IsAbstract) continue;
|
||
if (type.Name.Equals(scriptClassName, StringComparison.Ordinal) ||
|
||
type.Name.Contains(scriptClassName, StringComparison.Ordinal))
|
||
{
|
||
scriptType = type;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (scriptType != null) break;
|
||
}
|
||
|
||
if (scriptType == null)
|
||
{
|
||
Debug.LogError($"Could not find the class: {scriptClassName}");
|
||
return;
|
||
}
|
||
|
||
Component component = targetObject.GetOrAddComponent(scriptType);
|
||
|
||
FieldInfo[] fields = scriptType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance);
|
||
foreach (FieldInfo field in fields)
|
||
{
|
||
if (string.IsNullOrEmpty(field.Name)) continue;
|
||
var componentInObjects = _uiBindDatas.Find(data => data.Name == field.Name)?.BindCom;
|
||
if (componentInObjects == null)
|
||
{
|
||
Debug.LogError($"Field {field.Name} did not find matching component binding");
|
||
continue;
|
||
}
|
||
|
||
if (field.FieldType.IsArray)
|
||
{
|
||
Type elementType = field.FieldType.GetElementType();
|
||
if (elementType == null)
|
||
{
|
||
Debug.LogError($"Field {field.Name} has unknown element type.");
|
||
continue;
|
||
}
|
||
|
||
Array array = Array.CreateInstance(elementType, componentInObjects.Count);
|
||
for (int i = 0; i < componentInObjects.Count; i++)
|
||
{
|
||
Component comp = componentInObjects[i];
|
||
if (comp == null) continue;
|
||
|
||
if (elementType.IsInstanceOfType(comp))
|
||
{
|
||
array.SetValue(comp, i);
|
||
}
|
||
else
|
||
{
|
||
Debug.LogError($"Element {i} type mismatch for field {field.Name}, expected {elementType.Name}, actual {comp.GetType().Name}");
|
||
}
|
||
}
|
||
|
||
field.SetValue(component, array);
|
||
}
|
||
else
|
||
{
|
||
if (componentInObjects.Count > 0)
|
||
{
|
||
var first = componentInObjects[0];
|
||
if (first == null) continue;
|
||
|
||
if (field.FieldType.IsInstanceOfType(first))
|
||
{
|
||
field.SetValue(component, first);
|
||
}
|
||
else
|
||
{
|
||
Debug.LogError($"Field {field.Name} type mismatch, cannot assign value. Field expects {field.FieldType.Name}, actual {first.GetType().Name}");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
public static class PrefabChecker
|
||
{
|
||
public static bool IsEditingPrefabAsset(GameObject go)
|
||
{
|
||
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
|
||
if (prefabStage == null)
|
||
return false;
|
||
return prefabStage.IsPartOfPrefabContents(go);
|
||
}
|
||
|
||
public static bool IsPrefabAsset(GameObject go)
|
||
{
|
||
if (go == null) return false;
|
||
|
||
var assetType = PrefabUtility.GetPrefabAssetType(go);
|
||
if (assetType == PrefabAssetType.Regular ||
|
||
assetType == PrefabAssetType.Variant ||
|
||
assetType == PrefabAssetType.Model)
|
||
return true;
|
||
|
||
return IsEditingPrefabAsset(go);
|
||
}
|
||
}
|
||
}
|
||
}
|