com.alicizax.unity.framework/Editor/UI/Helper/UIScriptGeneratorHelper.cs

739 lines
28 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using AlicizaX.UI.Runtime;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEditor.SceneManagement;
using UnityEngine;
namespace AlicizaX.UI.Editor
{
public enum EBindType
{
None,
Widget,
ListCom
}
[Serializable]
public class UIBindData
{
public UIBindData(string name, List<GameObject> objs, Type componentType = null, EBindType bindType = EBindType.None)
{
Name = name;
Objs = objs ?? new List<GameObject>();
BindType = bindType;
ComponentType = componentType;
}
public UIBindData(string name, GameObject obj, Type componentType = null, EBindType bindType = EBindType.None)
: this(name, new List<GameObject> { obj }, componentType, bindType)
{
}
public string Name { get; }
public List<GameObject> Objs { get; set; }
public EBindType BindType { get; }
public Type ComponentType { get; private set; }
public bool IsGameObject => ComponentType == typeof(GameObject);
public string TypeName => ComponentType?.FullName ?? string.Empty;
public Type GetFirstOrDefaultType() => ComponentType;
public void SetComponentType(Type componentType)
{
ComponentType = componentType;
}
}
public static class UIScriptGeneratorHelper
{
private const string GenerateTypeNameKey = "AlicizaX.UI.Generate.TypeName";
private const string GenerateInstanceIdKey = "AlicizaX.UI.Generate.InstanceId";
private const string GenerateAssetPathKey = "AlicizaX.UI.Generate.AssetPath";
private static UIGenerateConfiguration _uiGenerateConfiguration;
private static IUIGeneratorRuleHelper _uiGeneratorRuleHelper;
private static readonly List<UIBindData> _uiBindDatas = new List<UIBindData>();
private static readonly HashSet<string> _arrayComponents = new HashSet<string>(StringComparer.Ordinal);
private static readonly Dictionary<string, Type> _componentTypeCache = new Dictionary<string, Type>(StringComparer.Ordinal);
private static IUIGeneratorRuleHelper UIGeneratorRuleHelper
{
get
{
var configuredHelperTypeName = string.IsNullOrWhiteSpace(UIConfiguration.UIScriptGeneratorRuleHelper)
? typeof(DefaultUIGeneratorRuleHelper).FullName
: UIConfiguration.UIScriptGeneratorRuleHelper;
if (_uiGeneratorRuleHelper == null || _uiGeneratorRuleHelper.GetType().FullName != configuredHelperTypeName)
{
InitializeRuleHelper();
}
return _uiGeneratorRuleHelper;
}
}
private static IContextualUIGeneratorRuleHelper ContextualUIGeneratorRuleHelper =>
UIGeneratorRuleHelper as IContextualUIGeneratorRuleHelper;
private static UIGenerateConfiguration UIConfiguration =>
_uiGenerateConfiguration ??= UIGenerateConfiguration.Instance;
private static IUIGeneratorRuleHelper InitializeRuleHelper()
{
var ruleHelperTypeName = UIConfiguration.UIScriptGeneratorRuleHelper;
if (string.IsNullOrWhiteSpace(ruleHelperTypeName))
{
ruleHelperTypeName = typeof(DefaultUIGeneratorRuleHelper).FullName;
}
var ruleHelperType = AlicizaX.Utility.Assembly.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 Type ResolveUIElementComponentType(string uiName)
{
if (string.IsNullOrEmpty(uiName))
{
return null;
}
if (_componentTypeCache.TryGetValue(uiName, out var componentType))
{
return componentType;
}
var componentTypeName = UIConfiguration.UIElementRegexConfigs
?.Where(pair => !string.IsNullOrEmpty(pair?.uiElementRegex))
.FirstOrDefault(pair => uiName.StartsWith(pair.uiElementRegex, StringComparison.Ordinal))
?.componentType;
if (string.IsNullOrWhiteSpace(componentTypeName))
{
return null;
}
componentType = componentTypeName == nameof(GameObject)
? typeof(GameObject)
: AlicizaX.Utility.Assembly.GetType(componentTypeName);
if (componentType == null)
{
Debug.LogError($"UIScriptGeneratorHelper: Could not resolve component type '{componentTypeName}' for '{uiName}'.");
return null;
}
_componentTypeCache[uiName] = componentType;
return componentType;
}
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;
var endIndex = name.IndexOf(common.ComCheckEndName, StringComparison.Ordinal);
if (endIndex <= 0) return null;
var comStr = name.Substring(0, endIndex);
var split = common.ComCheckSplitName ?? "#";
return comStr.Split(new[] { split }, StringSplitOptions.RemoveEmptyEntries);
}
private static void CollectBindData(Transform root)
{
if (root == null) return;
foreach (Transform child in root.Cast<Transform>().Where(child => child != null))
{
if (ShouldSkipChild(child)) continue;
var hasWidget = child.GetComponent<UIHolderObjectBase>() != null;
var isArrayComponent = IsArrayComponent(child.name);
if (hasWidget)
{
CollectWidget(child);
}
else if (isArrayComponent)
{
ProcessArrayComponent(child, root);
}
else
{
CollectComponent(child);
CollectBindData(child);
}
}
}
private static bool ShouldSkipChild(Transform child)
{
var keywords = UIConfiguration.UIGenerateCommonData.ExcludeKeywords;
return keywords?.Any(k =>
!string.IsNullOrEmpty(k) &&
child.name.IndexOf(k, StringComparison.OrdinalIgnoreCase) >= 0) == true;
}
private static bool IsArrayComponent(string componentName)
{
var splitName = UIConfiguration.UIGenerateCommonData.ArrayComSplitName;
return !string.IsNullOrEmpty(splitName) &&
componentName.StartsWith(splitName, StringComparison.Ordinal);
}
private static void ProcessArrayComponent(Transform child, Transform root)
{
var splitCode = UIConfiguration.UIGenerateCommonData.ArrayComSplitName;
if (!TryGetArrayComponentGroupName(child.name, splitCode, out var groupName))
{
return;
}
var groupKey = $"{root.GetInstanceID()}::{groupName}";
if (!_arrayComponents.Add(groupKey))
{
return;
}
var arrayComponents = root.Cast<Transform>()
.Where(sibling => IsArrayGroupMember(sibling.name, splitCode, groupName))
.ToList();
CollectArrayComponent(arrayComponents, groupName);
}
private static void CollectComponent(Transform node)
{
if (node == null) return;
var componentArray = SplitComponentName(node.name);
if (componentArray == null || componentArray.Length == 0) return;
foreach (var com in componentArray.Where(com => !string.IsNullOrEmpty(com)))
{
var componentType = ResolveUIElementComponentType(com);
if (componentType == null)
{
continue;
}
if (componentType != typeof(GameObject) && node.GetComponent(componentType) == null)
{
Debug.LogError($"{node.name} does not have component of type {componentType.FullName}");
continue;
}
var keyName = UIGeneratorRuleHelper.GetPrivateComponentByNameRule(com, node.name, EBindType.None);
if (_uiBindDatas.Exists(data => data.Name == keyName))
{
Debug.LogError($"Duplicate key found: {keyName}");
continue;
}
_uiBindDatas.Add(new UIBindData(keyName, node.gameObject, componentType));
}
}
private static void CollectWidget(Transform node)
{
if (node == null) return;
var common = UIConfiguration.UIGenerateCommonData;
if (node.name.Contains(common.ComCheckEndName, StringComparison.Ordinal) &&
node.name.Contains(common.ComCheckSplitName, StringComparison.Ordinal))
{
Debug.LogWarning($"{node.name} child component cannot contain rule definition symbols!");
return;
}
var component = node.GetComponent<UIHolderObjectBase>();
if (component == null)
{
Debug.LogError($"{node.name} expected to be a widget but does not have UIHolderObjectBase.");
return;
}
var keyName = UIGeneratorRuleHelper.GetPrivateComponentByNameRule(string.Empty, node.name, EBindType.Widget);
if (_uiBindDatas.Exists(data => data.Name == keyName))
{
Debug.LogError($"Duplicate key found: {keyName}");
return;
}
_uiBindDatas.Add(new UIBindData(keyName, component.gameObject, component.GetType(), EBindType.Widget));
}
private static void CollectArrayComponent(List<Transform> arrayNode, string nodeName)
{
if (arrayNode == null || !arrayNode.Any()) return;
var componentArray = SplitComponentName(nodeName);
if (componentArray == null || componentArray.Length == 0)
{
Debug.LogWarning($"CollectArrayComponent: {nodeName} has no component definitions.");
return;
}
var orderedNodes = OrderArrayNodes(arrayNode);
var tempBindDatas = CreateTempBindDatas(componentArray, nodeName);
PopulateArrayComponents(componentArray, orderedNodes, tempBindDatas);
_uiBindDatas.AddRange(tempBindDatas);
}
private static List<Transform> OrderArrayNodes(List<Transform> arrayNode)
{
var splitCode = UIConfiguration.UIGenerateCommonData.ArrayComSplitName;
return arrayNode
.Select(node => new { Node = node, Index = ExtractArrayIndex(node.name, splitCode) })
.OrderBy(x => x.Index ?? int.MaxValue)
.Select(x => x.Node)
.ToList();
}
private static List<UIBindData> CreateTempBindDatas(string[] componentArray, string nodeName)
{
return componentArray.Select(com =>
{
var keyName = UIGeneratorRuleHelper.GetPrivateComponentByNameRule(com, nodeName, EBindType.ListCom);
return new UIBindData(keyName, new List<GameObject>(), null, EBindType.ListCom);
}).ToList();
}
private static void PopulateArrayComponents(string[] componentArray, List<Transform> orderedNodes, List<UIBindData> tempBindDatas)
{
for (var index = 0; index < componentArray.Length; index++)
{
var com = componentArray[index];
if (string.IsNullOrEmpty(com)) continue;
var componentType = ResolveUIElementComponentType(com);
if (componentType == null) continue;
tempBindDatas[index].SetComponentType(componentType);
foreach (var node in orderedNodes)
{
var isGameObject = componentType == typeof(GameObject);
var component = isGameObject ? null : node.GetComponent(componentType);
if (component != null || isGameObject)
{
tempBindDatas[index].Objs.Add(node.gameObject);
}
else
{
Debug.LogError($"{node.name} does not have component of type {componentType.FullName}");
}
}
}
}
private static bool TryGetArrayComponentGroupName(string nodeName, string splitCode, out string groupName)
{
groupName = null;
if (string.IsNullOrEmpty(nodeName) || string.IsNullOrEmpty(splitCode))
{
return false;
}
var firstIndex = nodeName.IndexOf(splitCode, StringComparison.Ordinal);
var lastIndex = nodeName.LastIndexOf(splitCode, StringComparison.Ordinal);
if (firstIndex < 0 || lastIndex <= firstIndex)
{
return false;
}
groupName = nodeName.Substring(firstIndex + splitCode.Length, lastIndex - (firstIndex + splitCode.Length));
return !string.IsNullOrEmpty(groupName);
}
private static bool IsArrayGroupMember(string nodeName, string splitCode, string groupName)
{
return TryGetArrayComponentGroupName(nodeName, splitCode, out var candidateGroupName) &&
candidateGroupName.Equals(groupName, StringComparison.Ordinal) &&
ExtractArrayIndex(nodeName, splitCode).HasValue;
}
private static int? ExtractArrayIndex(string nodeName, string splitCode)
{
if (string.IsNullOrEmpty(nodeName) || string.IsNullOrEmpty(splitCode)) return null;
var lastIndex = nodeName.LastIndexOf(splitCode, StringComparison.Ordinal);
if (lastIndex < 0) return null;
var suffix = nodeName.Substring(lastIndex + splitCode.Length);
return int.TryParse(suffix, out var idx) ? idx : (int?)null;
}
public static void GenerateUIBindScript(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("Please save the UI as a prefab asset before generating bindings.");
return;
}
var ruleHelper = UIGeneratorRuleHelper;
if (ruleHelper == null)
{
return;
}
InitializeGenerationContext(targetObject);
CollectBindData(targetObject.transform);
var generationContext = new UIGenerationContext(targetObject, scriptGenerateData, _uiBindDatas)
{
AssetPath = UIGenerateQuick.GetPrefabAssetPath(targetObject),
ClassName = GetClassGenerateName(ruleHelper, targetObject, scriptGenerateData)
};
if (!CheckCanGenerate(ruleHelper, generationContext))
{
CleanupContext();
return;
}
var validationResult = ValidateGeneration(generationContext);
if (!validationResult.IsValid)
{
Debug.LogError(validationResult.Message);
CleanupContext();
return;
}
GenerateScript(generationContext, ruleHelper);
}
private static void InitializeGenerationContext(GameObject targetObject)
{
EditorPrefs.SetInt(GenerateInstanceIdKey, targetObject.GetInstanceID());
var assetPath = UIGenerateQuick.GetPrefabAssetPath(targetObject);
if (!string.IsNullOrEmpty(assetPath))
{
EditorPrefs.SetString(GenerateAssetPathKey, assetPath);
}
_uiBindDatas.Clear();
_arrayComponents.Clear();
}
private static UIGenerationValidationResult ValidateGeneration(UIGenerationContext context)
{
if (context.TargetObject == null)
{
return UIGenerationValidationResult.Fail("UI generation target is null.");
}
if (context.ScriptGenerateData == null)
{
return UIGenerationValidationResult.Fail("UI generation config is null.");
}
if (string.IsNullOrWhiteSpace(context.ClassName))
{
return UIGenerationValidationResult.Fail("Generated class name is empty.");
}
if (!File.Exists(UIGlobalPath.TemplatePath))
{
return UIGenerationValidationResult.Fail($"UI template file not found: {UIGlobalPath.TemplatePath}");
}
return UIGenerationValidationResult.Success();
}
private static void GenerateScript(UIGenerationContext context, IUIGeneratorRuleHelper ruleHelper)
{
var templateText = File.ReadAllText(UIGlobalPath.TemplatePath);
var processedText = ProcessTemplateText(context, templateText, ruleHelper);
EditorPrefs.SetString(GenerateTypeNameKey, context.FullTypeName);
if (ContextualUIGeneratorRuleHelper != null)
{
ContextualUIGeneratorRuleHelper.WriteUIScriptContent(context, processedText);
return;
}
ruleHelper.WriteUIScriptContent(context.TargetObject, context.ClassName, processedText, context.ScriptGenerateData);
}
private static string ProcessTemplateText(UIGenerationContext context, string templateText, IUIGeneratorRuleHelper ruleHelper)
{
var contextualRuleHelper = ContextualUIGeneratorRuleHelper;
return templateText
.Replace("#ReferenceNameSpace#", contextualRuleHelper != null ? contextualRuleHelper.GetReferenceNamespace(context) : ruleHelper.GetReferenceNamespace(_uiBindDatas))
.Replace("#ClassNameSpace#", context.ScriptGenerateData.NameSpace)
.Replace("#ClassName#", context.ClassName)
.Replace("#TagName#", contextualRuleHelper != null ? contextualRuleHelper.GetUIResourceSavePath(context) : ruleHelper.GetUIResourceSavePath(context.TargetObject, context.ScriptGenerateData))
.Replace("#LoadType#", context.ScriptGenerateData.LoadType.ToString())
.Replace("#Variable#", contextualRuleHelper != null ? contextualRuleHelper.GetVariableContent(context) : ruleHelper.GetVariableContent(_uiBindDatas));
}
private static string GetClassGenerateName(IUIGeneratorRuleHelper ruleHelper, GameObject targetObject, UIScriptGenerateData scriptGenerateData)
{
if (ContextualUIGeneratorRuleHelper != null)
{
return ContextualUIGeneratorRuleHelper.GetClassGenerateName(new UIGenerationContext(targetObject, scriptGenerateData, _uiBindDatas));
}
return ruleHelper.GetClassGenerateName(targetObject, scriptGenerateData);
}
private static bool CheckCanGenerate(IUIGeneratorRuleHelper ruleHelper, UIGenerationContext context)
{
if (ContextualUIGeneratorRuleHelper != null)
{
return ContextualUIGeneratorRuleHelper.CheckCanGenerate(context);
}
return ruleHelper.CheckCanGenerate(context.TargetObject, context.ScriptGenerateData);
}
[DidReloadScripts]
public static void BindUIScript()
{
if (!EditorPrefs.HasKey(GenerateTypeNameKey)) return;
var scriptTypeName = EditorPrefs.GetString(GenerateTypeNameKey);
var targetObject = ResolveGenerationTarget();
if (targetObject == null)
{
Debug.LogWarning("UI script generation attachment object missing.");
CleanupContext();
return;
}
_uiBindDatas.Clear();
_arrayComponents.Clear();
var bindSucceeded = false;
CollectBindData(targetObject.transform);
try
{
bindSucceeded = BindScriptPropertyField(targetObject, scriptTypeName);
}
catch (Exception exception)
{
Debug.LogException(exception);
}
finally
{
CleanupContext();
}
if (!bindSucceeded)
{
return;
}
EditorUtility.SetDirty(targetObject);
Debug.Log($"Generate {scriptTypeName} successfully attached to game object.");
}
private static GameObject ResolveGenerationTarget()
{
var instanceId = EditorPrefs.GetInt(GenerateInstanceIdKey, -1);
var instanceTarget = EditorUtility.InstanceIDToObject(instanceId) as GameObject;
if (instanceTarget != null)
{
return instanceTarget;
}
var assetPath = EditorPrefs.GetString(GenerateAssetPathKey, string.Empty);
return string.IsNullOrEmpty(assetPath) ? null : AssetDatabase.LoadAssetAtPath<GameObject>(assetPath);
}
private static void CleanupContext()
{
EditorPrefs.DeleteKey(GenerateTypeNameKey);
EditorPrefs.DeleteKey(GenerateInstanceIdKey);
EditorPrefs.DeleteKey(GenerateAssetPathKey);
_uiBindDatas.Clear();
_arrayComponents.Clear();
}
private static bool BindScriptPropertyField(GameObject targetObject, string scriptTypeName)
{
if (targetObject == null) throw new ArgumentNullException(nameof(targetObject));
if (string.IsNullOrEmpty(scriptTypeName)) throw new ArgumentNullException(nameof(scriptTypeName));
var scriptType = FindScriptType(scriptTypeName);
if (scriptType == null)
{
Debug.LogError($"Could not find the class: {scriptTypeName}");
return false;
}
var targetHolder = targetObject.GetOrAddComponent(scriptType);
return BindFieldsToComponents(targetHolder, scriptType);
}
private static Type FindScriptType(string scriptTypeName)
{
var resolvedType = AlicizaX.Utility.Assembly.GetType(scriptTypeName);
if (resolvedType != null)
{
return resolvedType;
}
return AppDomain.CurrentDomain.GetAssemblies()
.Where(assembly => !assembly.GetName().Name.EndsWith(".Editor", StringComparison.Ordinal) &&
!assembly.GetName().Name.Equals("UnityEditor", StringComparison.Ordinal))
.SelectMany(assembly => assembly.GetTypes())
.FirstOrDefault(type => type.IsClass && !type.IsAbstract &&
type.FullName.Equals(scriptTypeName, StringComparison.Ordinal));
}
private static bool BindFieldsToComponents(Component targetHolder, Type scriptType)
{
var fields = scriptType.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance).Where(field =>
field.GetCustomAttributes(typeof(SerializeField), false).Length > 0
);
var isSuccessful = true;
foreach (var field in fields.Where(field => !string.IsNullOrEmpty(field.Name)))
{
var bindData = _uiBindDatas.Find(data => data.Name == field.Name);
if (bindData == null)
{
Debug.LogError($"Field {field.Name} did not find matching component binding.");
isSuccessful = false;
continue;
}
if (!SetFieldValue(field, bindData, targetHolder))
{
isSuccessful = false;
}
}
return isSuccessful;
}
private static bool SetFieldValue(FieldInfo field, UIBindData bindData, Component targetComponent)
{
if (field.FieldType.IsArray)
{
return SetArrayFieldValue(field, bindData, targetComponent);
}
return SetSingleFieldValue(field, bindData, targetComponent);
}
private static bool SetArrayFieldValue(FieldInfo field, UIBindData bindData, Component targetComponent)
{
var elementType = field.FieldType.GetElementType();
if (elementType == null)
{
Debug.LogError($"Field {field.Name} has unknown element type.");
return false;
}
var components = bindData.Objs;
var array = Array.CreateInstance(elementType, components.Count);
var isSuccessful = true;
for (var i = 0; i < components.Count; i++)
{
if (components[i] == null) continue;
var componentObject = ResolveBoundObject(components[i], bindData.ComponentType);
if (componentObject != null && elementType.IsInstanceOfType(componentObject))
{
array.SetValue(componentObject, i);
}
else
{
Debug.LogError($"Element {i} type mismatch for field {field.Name}");
isSuccessful = false;
}
}
field.SetValue(targetComponent, array);
return isSuccessful;
}
private static bool SetSingleFieldValue(FieldInfo field, UIBindData bindData, Component targetComponent)
{
if (bindData.Objs.Count == 0)
{
return false;
}
var firstComponent = ResolveBoundObject(bindData.Objs[0], bindData.ComponentType);
if (firstComponent == null)
{
return false;
}
if (!field.FieldType.IsInstanceOfType(firstComponent))
{
Debug.LogError($"Field {field.Name} type mismatch");
return false;
}
field.SetValue(targetComponent, firstComponent);
return true;
}
private static object ResolveBoundObject(GameObject source, Type componentType)
{
if (source == null || componentType == null)
{
return null;
}
return componentType == typeof(GameObject) ? source : source.GetComponent(componentType);
}
public static class PrefabChecker
{
public static bool IsEditingPrefabAsset(GameObject go)
{
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
return prefabStage?.IsPartOfPrefabContents(go) == true;
}
public static bool IsPrefabAsset(GameObject go)
{
if (go == null) return false;
var assetType = PrefabUtility.GetPrefabAssetType(go);
var isRegularPrefab = assetType == PrefabAssetType.Regular ||
assetType == PrefabAssetType.Variant ||
assetType == PrefabAssetType.Model;
return isRegularPrefab || IsEditingPrefabAsset(go);
}
}
}
}