This commit is contained in:
陈思海 2025-11-07 20:47:57 +08:00
parent 3a72214da2
commit f3251980f5
9 changed files with 1012 additions and 286 deletions

View File

@ -2,22 +2,72 @@
using UnityEditor;
using UnityEngine;
namespace AlicizaX.UI.Editor {
public static class UIGenerateEditorTool {
[MenuItem("GameObject/UI生成绑定/热更工程UI代码", priority = 10)]
public static void GenerateHotfixUIScript() {
UIScriptGeneratorHelper.GenerateAndAttachScript(
Selection.gameObjects.FirstOrDefault(),
UIGenerateConfiguration.Instance.UIScriptGenerateConfig.HotFixProjectUIScriptGenerateData
);
namespace AlicizaX.UI.Editor
{
public class UIGenerateEditorWindow : EditorWindow
{
private GameObject selectedObject;
private Vector2 windowPosition;
private float windowWidth;
private float windowHeight;
private string[] menuItems;
[MenuItem("GameObject/UI生成绑定", priority = 10)]
public static void ShowWindow()
{
GameObject selectedObject = Selection.gameObjects.FirstOrDefault();
if (selectedObject == null) return;
var uiScriptConfigs = UIGenerateConfiguration.Instance.UIScriptGenerateConfigs;
if (uiScriptConfigs == null || uiScriptConfigs.Count == 0) return;
var window = GetWindow<UIGenerateEditorWindow>("", true);
window.selectedObject = selectedObject;
window.menuItems = uiScriptConfigs.Select(config => $"{config.ProjectName}").ToArray();
var windowWidth = 300;
var windowHeight = (window.menuItems.Length * 35f);
Vector3 objectWorldPosition = selectedObject.transform.position;
Vector3 screenPosition = HandleUtility.WorldToGUIPoint(objectWorldPosition);
var windowPosition = new Vector2(screenPosition.x, screenPosition.y - windowHeight - 5f);
window.minSize = new Vector2(windowWidth, windowHeight);
window.maxSize = new Vector2(windowWidth, windowHeight);
window.position = new Rect(windowPosition, new Vector2(windowWidth, windowHeight));
window.Show();
}
[MenuItem("GameObject/UI生成绑定/主工程UI代码", priority = 21)]
public static void GenerateMainUIScript() {
UIScriptGeneratorHelper.GenerateAndAttachScript(
Selection.gameObjects.FirstOrDefault(),
UIGenerateConfiguration.Instance.UIScriptGenerateConfig.MainProjectUIScriptGenerateData
);
private void OnGUI()
{
GUILayout.Space(5);
foreach (var item in menuItems)
{
if (GUILayout.Button(item, EditorStyles.toolbarButton))
{
GenerateScriptForConfig(selectedObject, item);
Close();
}
GUILayout.Space(10);
}
}
private void GenerateScriptForConfig(GameObject selectedObject, string itemName)
{
var uiScriptConfigs = UIGenerateConfiguration.Instance.UIScriptGenerateConfigs;
var config = uiScriptConfigs.FirstOrDefault(c => $"{c.ProjectName}" == itemName);
if (config != null)
{
UIScriptGeneratorHelper.GenerateAndAttachScript(selectedObject, config);
}
else
{
Debug.LogWarning("Configuration not found for item: " + itemName);
}
}
}
}

View File

@ -12,15 +12,15 @@ using UnityEditor;
using UnityEditor.Callbacks;
using UnityEngine.UI;
namespace AlicizaX.UI.Editor
{
enum EBindType
public enum EBindType
{
None,
Widget,
ListCom,
}
namespace AlicizaX.UI.Editor
{
[Serializable]
class UIBindData
{
@ -47,8 +47,32 @@ namespace AlicizaX.UI.Editor
internal static class UIScriptGeneratorHelper
{
private static UIGenerateConfiguration _uiGenerateConfiguration;
private static IUIGeneratorHelper _nameRuleHelper;
static UIGenerateConfiguration UIGenerateConfiguration
/// <summary>
/// 设置自定义命名规则助手[4](@ref)
/// </summary>
public static IUIGeneratorHelper UIGeneratorRuleHelper
{
get
{
if (_nameRuleHelper == null || (_nameRuleHelper != null && !UIConfiguration.UIScriptGeneratorHelper.Equals(_nameRuleHelper.GetType().FullName)))
{
Type ruleHelperType = Type.GetType(UIConfiguration.UIScriptGeneratorHelper);
if (ruleHelperType == null)
{
Debug.LogError($"UIScriptGeneratorHelper: Could not load UI ScriptGeneratorHelper {UIConfiguration.UIScriptGeneratorHelper}");
return null;
}
_nameRuleHelper = Activator.CreateInstance(ruleHelperType) as IUIGeneratorHelper;
}
return _nameRuleHelper;
}
}
private static UIGenerateConfiguration UIConfiguration
{
get
{
@ -61,11 +85,11 @@ namespace AlicizaX.UI.Editor
}
}
private static string GetVerType(string uiName)
private static string GetVersionType(string uiName)
{
foreach (var pair in UIGenerateConfiguration.UIElementRegexConfigs)
foreach (var pair in UIConfiguration.UIElementRegexConfigs)
{
if (uiName.StartsWith(pair.uiElementRegex))
if (uiName.StartsWith(pair.uiElementRegex, StringComparison.Ordinal))
{
return pair.componentType;
}
@ -74,25 +98,24 @@ namespace AlicizaX.UI.Editor
return string.Empty;
}
private static string[] SplitComName(string name)
private static string[] SplitComponentName(string name)
{
bool hasCom = name.Contains(UIGenerateConfiguration.UIGenerateCommonData.ComCheckEndName);
bool hasCom = name.Contains(UIConfiguration.UIGenerateCommonData.ComCheckEndName);
if (!hasCom) return null;
string comStr = name.Substring(0, name.IndexOf(UIGenerateConfiguration.UIGenerateCommonData.ComCheckEndName));
return comStr.Split(UIGenerateConfiguration.UIGenerateCommonData.ComCheckSplitName);
string comStr = name.Substring(0,
name.IndexOf(UIConfiguration.UIGenerateCommonData.ComCheckEndName, StringComparison.Ordinal));
return comStr.Split(UIConfiguration.UIGenerateCommonData.ComCheckSplitName);
}
private static string GetKeyName(string key, string componentName)
private static string GetKeyName(string key, string componentName, EBindType bindType)
{
return $"{key}{componentName.Substring(componentName.IndexOf(UIGenerateConfiguration.UIGenerateCommonData.ComCheckEndName) + 1)}";
return UIGeneratorRuleHelper.GetPrivateComponentByNameRule(key, componentName, bindType);
}
private static List<UIBindData> UIBindDatas = new List<UIBindData>();
private static string GenerateNameSpace = string.Empty;
private static List<string> ArrayComs = new List<string>();
private static List<UIBindData> _uiBindDatas = new List<UIBindData>();
private static string _generateNameSpace = string.Empty;
private static List<string> _arrayComponents = new List<string>();
private static void GetBindData(Transform root)
{
@ -100,34 +123,42 @@ namespace AlicizaX.UI.Editor
{
Transform child = root.GetChild(i);
bool hasWdiget = child.GetComponent<UIHolderObjectBase>() != null;
bool hasWidget = child.GetComponent<UIHolderObjectBase>() != null;
if (UIGenerateConfiguration.UIGenerateCommonData.ExcludeKeywords.Any(k => child.name.IndexOf(k, StringComparison.OrdinalIgnoreCase) >= 0)) continue;
if (UIConfiguration.UIGenerateCommonData.ExcludeKeywords.Any(k =>
child.name.IndexOf(k, StringComparison.OrdinalIgnoreCase) >= 0))
continue;
bool isArrayComs = child.name.StartsWith(UIGenerateConfiguration.UIGenerateCommonData.ArrayComSplitName);
if (hasWdiget)
bool isArrayComponent = child.name.StartsWith(
UIConfiguration.UIGenerateCommonData.ArrayComSplitName, StringComparison.Ordinal);
if (hasWidget)
{
CollectWidget(child);
}
else if (isArrayComs)
else if (isArrayComponent)
{
string splitCode = UIGenerateConfiguration.UIGenerateCommonData.ArrayComSplitName;
int lastIndex = child.name.LastIndexOf(splitCode);
string text = child.name.Substring(child.name.IndexOf(splitCode) + 1, lastIndex - 1);
if (ArrayComs.Contains(text)) continue;
ArrayComs.Add(text);
List<Transform> arrayComs = new List<Transform>();
string splitCode = UIConfiguration.UIGenerateCommonData.ArrayComSplitName;
int lastIndex = child.name.LastIndexOf(splitCode, StringComparison.Ordinal);
string text = child.name.Substring(
child.name.IndexOf(splitCode, StringComparison.Ordinal) + 1,
lastIndex - 1);
if (_arrayComponents.Contains(text)) continue;
_arrayComponents.Add(text);
List<Transform> arrayComponents = new List<Transform>();
for (int j = 0; j < root.childCount; j++)
{
if (root.GetChild(j).name.Contains(text))
if (root.GetChild(j).name.Contains(text, StringComparison.Ordinal))
{
arrayComs.Add(root.GetChild(j));
arrayComponents.Add(root.GetChild(j));
}
}
CollectArrayComponent(arrayComs, text);
CollectArrayComponent(arrayComponents, text);
}
else if (!isArrayComs && !hasWdiget)
else if (!isArrayComponent && !hasWidget)
{
CollectComponent(child);
GetBindData(child);
@ -137,25 +168,25 @@ namespace AlicizaX.UI.Editor
private static void CollectComponent(Transform node)
{
string[] comArray = SplitComName(node.name);
if (comArray != null)
string[] componentArray = SplitComponentName(node.name);
if (componentArray != null)
{
foreach (var com in comArray)
foreach (var com in componentArray)
{
string typeName = GetVerType(com);
string typeName = GetVersionType(com);
if (string.IsNullOrEmpty(typeName)) continue;
Component component = node.GetComponent(typeName);
if (component != null)
{
string keyName = GetKeyName(com, node.name);
if (UIBindDatas.Exists(a => a.Name == keyName))
string keyName = GetKeyName(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));
_uiBindDatas.Add(new UIBindData(keyName, component));
}
else
{
@ -167,44 +198,50 @@ namespace AlicizaX.UI.Editor
private static void CollectWidget(Transform node)
{
if (node.name.IndexOf(UIGenerateConfiguration.UIGenerateCommonData.ComCheckEndName) != -1 && node.name.IndexOf(UIGenerateConfiguration.UIGenerateCommonData.ComCheckSplitName) != -1)
if (node.name.IndexOf(UIConfiguration.UIGenerateCommonData.ComCheckEndName, StringComparison.Ordinal) != -1 &&
node.name.IndexOf(UIConfiguration.UIGenerateCommonData.ComCheckSplitName, StringComparison.Ordinal) != -1)
{
Debug.LogWarning($"{node.name} 子组件不能包含规则定义符号!");
Debug.LogWarning($"{node.name} child component cannot contain rule definition symbols!");
return;
}
UIHolderObjectBase component = node.GetComponent<UIHolderObjectBase>();
string keyName = node.name;
if (UIBindDatas.Exists(a => a.Name == keyName))
string keyName = GetKeyName(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));
_uiBindDatas.Add(new UIBindData(keyName, component, EBindType.Widget));
}
private static void CollectArrayComponent(List<Transform> arrayNode, string nodeName)
{
string[] comArray = SplitComName(nodeName);
arrayNode = arrayNode.OrderBy(s => int.Parse(s.name.Split('*').Last())).ToList();
List<UIBindData> tempBindDatas = new List<UIBindData>(comArray.Length);
string[] componentArray = SplitComponentName(nodeName);
arrayNode = arrayNode.OrderBy(s => int.Parse(s.name.Split('*').Last(),
System.Globalization.CultureInfo.InvariantCulture)).ToList();
if (comArray != null)
List<UIBindData> tempBindDatas = new List<UIBindData>(componentArray.Length);
if (componentArray != null)
{
int index = 0;
foreach (var com in comArray)
foreach (var com in componentArray)
{
foreach (var node in arrayNode)
{
string typeName = GetVerType(com);
string typeName = GetVersionType(com);
if (string.IsNullOrEmpty(typeName)) continue;
Component component = node.GetComponent(typeName);
if (component != null)
{
string keyName = GetKeyName(com, nodeName) + "List";
if (tempBindDatas.Count - 1 < index) tempBindDatas.Add(new UIBindData(keyName, new List<Component>(), EBindType.ListCom));
string keyName = GetKeyName(com, nodeName, EBindType.ListCom);
if (tempBindDatas.Count - 1 < index)
tempBindDatas.Add(new UIBindData(keyName, new List<Component>(), EBindType.ListCom));
tempBindDatas[index].BindCom.Add(component);
}
else
@ -217,84 +254,91 @@ namespace AlicizaX.UI.Editor
}
}
UIBindDatas.AddRange(tempBindDatas.ToArray());
_uiBindDatas.AddRange(tempBindDatas.ToArray());
}
private static string GetReferenceNamespace()
{
StringBuilder referenceNamespaceBuilder = new StringBuilder();
HashSet<string> namespaces = new HashSet<string>();
namespaces.Add("UnityEngine");
referenceNamespaceBuilder.Append("using UnityEngine;\n");
private static string GetRefrenceNameSpace()
foreach (var bindData in _uiBindDatas)
{
StringBuilder refrenceNameSpaceBuilder = new StringBuilder();
HashSet<string> nameSpaces = new HashSet<string>();
nameSpaces.Add("UnityEngine");
refrenceNameSpaceBuilder.Append($"using UnityEngine;\n");
foreach (var bindData in UIBindDatas)
{
string nameSpace = bindData.BindCom.FirstOrDefault().GetType().Namespace;
string nameSpace = bindData.BindCom.FirstOrDefault()?.GetType().Namespace;
if (bindData.BindType == EBindType.ListCom)
{
if (!nameSpaces.Contains("using System.Collections.Generic;"))
if (!namespaces.Contains("System.Collections.Generic"))
{
refrenceNameSpaceBuilder.Append("using System.Collections.Generic;\n");
referenceNamespaceBuilder.Append("using System.Collections.Generic;\n");
namespaces.Add("System.Collections.Generic");
}
}
if (!nameSpaces.Contains(nameSpace) && !string.IsNullOrEmpty(nameSpace))
if (!string.IsNullOrEmpty(nameSpace) && !namespaces.Contains(nameSpace))
{
nameSpaces.Add(nameSpace);
refrenceNameSpaceBuilder.Append($"using {nameSpace};\n");
namespaces.Add(nameSpace);
referenceNamespaceBuilder.Append($"using {nameSpace};\n");
}
}
return refrenceNameSpaceBuilder.ToString();
return referenceNamespaceBuilder.ToString();
}
private static string GetVarText(List<UIBindData> uiBindDatas)
private static string GetVariableText(List<UIBindData> uiBindDatas)
{
StringBuilder varTextBuilder = new StringBuilder();
StringBuilder variableTextBuilder = new StringBuilder();
foreach (var bindData in uiBindDatas)
{
var varName = bindData.Name;
varTextBuilder.Append("\t\t[SerializeField]\n");
varTextBuilder.Append("\t\t[ReadOnly]\n");
varTextBuilder.Append("\t\t[HideLabel]\n");
string variableName = bindData.Name;
string publicName = UIGeneratorRuleHelper.GetPublicComponentByNameRule(variableName);
variableTextBuilder.Append("\t\t[SerializeField]\n");
if (bindData.BindType == EBindType.None)
{
varTextBuilder.Append($"\t\tprivate {bindData.BindCom.FirstOrDefault().GetType().Name} m{varName};\n");
varTextBuilder.Append($"\t\tpublic {bindData.BindCom.FirstOrDefault().GetType().Name} {varName} => m{varName};\n\n");
variableTextBuilder.Append(
$"\t\tprivate {bindData.BindCom.FirstOrDefault()?.GetType().Name} {variableName};\n");
variableTextBuilder.Append(
$"\t\tpublic {bindData.BindCom.FirstOrDefault()?.GetType().Name} {publicName} => {variableName};\n\n");
}
else if (bindData.BindType == EBindType.ListCom)
{
varTextBuilder.Append($"\t\tprivate {bindData.BindCom.FirstOrDefault().GetType().Name} [] m{varName} = new {bindData.BindCom.FirstOrDefault().GetType().Name}[{bindData.BindCom.Count}];\n");
varTextBuilder.Append($"\t\tpublic {bindData.BindCom.FirstOrDefault().GetType().Name} [] {varName} => m{varName};\n\n");
variableTextBuilder.Append(
$"\t\tprivate {bindData.BindCom.FirstOrDefault()?.GetType().Name}[] {variableName} = " +
$"new {bindData.BindCom.FirstOrDefault()?.GetType().Name}[{bindData.BindCom.Count}];\n");
variableTextBuilder.Append(
$"\t\tpublic {bindData.BindCom.FirstOrDefault()?.GetType().Name}[] {publicName} => {variableName};\n\n");
}
else if (bindData.BindType == EBindType.Widget)
{
varTextBuilder.Append($"\t\tprivate {bindData.BindCom.FirstOrDefault().GetType().Name} m{varName};\n");
varTextBuilder.Append($"\t\tpublic {bindData.BindCom.FirstOrDefault().GetType().Name} {varName} => m{varName};\n\n");
variableTextBuilder.Append(
$"\t\tprivate {bindData.BindCom.FirstOrDefault()?.GetType().Name} {variableName};\n");
variableTextBuilder.Append(
$"\t\tpublic {bindData.BindCom.FirstOrDefault()?.GetType().Name} {publicName} => {variableName};\n\n");
}
}
return varTextBuilder.ToString();
return variableTextBuilder.ToString();
}
private static string GenerateScript(string className)
{
StringBuilder scriptBuilder = new StringBuilder();
scriptBuilder.Append(GetRefrenceNameSpace());
scriptBuilder.Append(GetReferenceNamespace());
scriptBuilder.Append("using Sirenix.OdinInspector;\n");
scriptBuilder.Append("using AlicizaX.UI.Runtime;\n");
scriptBuilder.Append($"namespace {GenerateNameSpace}\n");
scriptBuilder.Append($"namespace {_generateNameSpace}\n");
scriptBuilder.Append("{\n");
scriptBuilder.Append($"\t#Attribute#\n");
scriptBuilder.Append("\t#Attribute#\n");
scriptBuilder.Append($"\tpublic class {className} : UIHolderObjectBase\n");
scriptBuilder.Append("\t{\n");
scriptBuilder.Append($"\t\tpublic const string ResTag = #Tag#;\n");
scriptBuilder.Append("\t\tpublic const string ResTag = #Tag#;\n");
scriptBuilder.Append("\t\t#region Generated by Script Tool\n\n");
scriptBuilder.Append(GetVarText(UIBindDatas));
scriptBuilder.Append(GetVariableText(_uiBindDatas));
scriptBuilder.Append("\n\t\t#endregion\n");
scriptBuilder.Append("\t}\n");
@ -306,51 +350,50 @@ namespace AlicizaX.UI.Editor
public static void GenerateAndAttachScript(GameObject targetObject, UIScriptGenerateData scriptGenerateData)
{
EditorPrefs.SetInt("InstanceId", targetObject.GetInstanceID());
UIBindDatas.Clear();
GenerateNameSpace = scriptGenerateData.NameSpace;
ArrayComs.Clear();
string className = $"{UIGenerateConfiguration.UIGenerateCommonData.GeneratePrefix}_{targetObject.name}";
_uiBindDatas.Clear();
_generateNameSpace = scriptGenerateData.NameSpace;
_arrayComponents.Clear();
string className = $"{UIConfiguration.UIGenerateCommonData.GeneratePrefix}_{targetObject.name}";
string scriptSavePath = Path.Combine(scriptGenerateData.GenerateHolderCodePath, className + ".cs");
GetBindData(targetObject.transform);
string scriptContent = GenerateScript(className);
string TagName = $"\"{targetObject.name}\"";
string tagName = $"\"{targetObject.name}\"";
if (scriptGenerateData.LoadType == EUIResLoadType.Resources)
{
string matchWords = string.Empty;
UIGenerateConfiguration.UIGenerateCommonData.CombineWords.Any(t =>
UIConfiguration.UIGenerateCommonData.CombineWords.Any(t =>
{
if (targetObject.name.IndexOf(t.Key) >= 0)
if (targetObject.name.IndexOf(t.Key, StringComparison.Ordinal) >= 0)
{
matchWords = t.Value;
}
return targetObject.name.IndexOf(t.Key) >= 0;
return targetObject.name.IndexOf(t.Key, StringComparison.Ordinal) >= 0;
});
string finalPath = Path.Combine(scriptGenerateData.UIPrefabRootPath.Replace("Assets/", "").Replace("Resources/", ""), matchWords);
string didc = Path.Combine(scriptGenerateData.UIPrefabRootPath, matchWords);
if (!Directory.Exists(didc))
string finalPath = Path.Combine(
scriptGenerateData.UIPrefabRootPath.Replace("Assets/", "").Replace("Resources/", ""),
matchWords);
string directory = Path.Combine(scriptGenerateData.UIPrefabRootPath, matchWords);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(didc);
Directory.CreateDirectory(directory);
}
finalPath = Utility.Path.GetRegularPath(finalPath);
TagName = $"\"{finalPath}/{targetObject.name}\"";
tagName = $"\"{finalPath}/{targetObject.name}\"";
}
scriptContent = scriptContent.Replace("#Tag#", TagName);
//#Attribute#
string uiAttribute = $"[UIRes({className}.ResTag, EUIResLoadType.{scriptGenerateData.LoadType.ToString()})]";
scriptContent = scriptContent.Replace("#Tag#", tagName);
string uiAttribute = $"[UIRes({className}.ResTag, EUIResLoadType.{scriptGenerateData.LoadType})]";
scriptContent = scriptContent.Replace("#Attribute#", uiAttribute);
if (File.Exists(scriptSavePath))
{
string oldText = File.ReadAllText(scriptSavePath);
@ -362,34 +405,32 @@ namespace AlicizaX.UI.Editor
}
}
File.WriteAllText(scriptSavePath, scriptContent);
File.WriteAllText(scriptSavePath, scriptContent, Encoding.UTF8);
EditorPrefs.SetString("Generate", className);
AssetDatabase.Refresh();
}
[DidReloadScripts]
private static void CheckHasAttach()
{
bool has = EditorPrefs.HasKey("Generate");
if (has)
{
UIBindDatas.Clear();
ArrayComs.Clear();
var className = EditorPrefs.GetString("Generate");
// var targetObject = UIGenerateWindow.GetTargetObject();
_uiBindDatas.Clear();
_arrayComponents.Clear();
string className = EditorPrefs.GetString("Generate");
int instanceId = EditorPrefs.GetInt("InstanceId", -1);
if (instanceId == -1)
{
return;
}
var targetObject = (GameObject)EditorUtility.InstanceIDToObject(instanceId);
GameObject targetObject = (GameObject)EditorUtility.InstanceIDToObject(instanceId);
if (!targetObject)
{
Debug.Log("UI脚本生成附加物体丢失!");
Debug.Log("UI script generation attachment object missing!");
}
EditorPrefs.DeleteKey("Generate");
@ -400,17 +441,16 @@ namespace AlicizaX.UI.Editor
}
}
private static void AttachScriptToGameObject(GameObject targetObject, string scriptClassName)
{
Type scriptType = null;
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
if (assembly.FullName.Contains("Editor")) continue;
var types = assembly.GetTypes();
Type[] types = assembly.GetTypes();
foreach (var type in types)
{
if (type.IsClass && !type.IsAbstract && type.Name.Contains(scriptClassName))
if (type.IsClass && !type.IsAbstract && type.Name.Contains(scriptClassName, StringComparison.Ordinal))
{
scriptType = type;
}
@ -424,28 +464,24 @@ namespace AlicizaX.UI.Editor
foreach (FieldInfo field in fields)
{
List<Component> componentInObjects = UIBindDatas.Find(data => "m" + data.Name == field.Name)?.BindCom;
List<Component> componentInObjects = _uiBindDatas.Find(data => data.Name == field.Name)?.BindCom;
if (componentInObjects != null)
{
if (field.FieldType.IsArray)
{
// 获取数组元素类型
Type elementType = field.FieldType.GetElementType();
// 创建对应类型的数组
Array array = Array.CreateInstance(elementType, componentInObjects.Count);
for (int i = 0; i < componentInObjects.Count; i++)
{
Component comp = componentInObjects[i];
// 检查元素类型是否匹配
if (elementType.IsInstanceOfType(comp))
{
array.SetValue(comp, i);
}
else
{
Debug.LogError($"元素 {i} 类型不匹配,期望 {elementType.Name},实际为 {comp.GetType().Name}");
// 处理错误,如跳过或终止赋值
Debug.LogError($"Element {i} type mismatch, expected {elementType.Name}, actual {comp.GetType().Name}");
}
}
@ -453,24 +489,22 @@ namespace AlicizaX.UI.Editor
}
else
{
// 非数组字段取第一个元素
if (componentInObjects.Count > 0)
{
// 同样检查类型兼容性
if (field.FieldType.IsInstanceOfType(componentInObjects[0]))
{
field.SetValue(component, componentInObjects[0]);
}
else
{
Debug.LogError($"字段 {field.Name} 类型不匹配,无法赋值");
Debug.LogError($"Field {field.Name} type mismatch, cannot assign value");
}
}
}
}
else
{
Debug.LogError($"字段 {field.Name} 未找到匹配的组件绑定");
Debug.LogError($"Field {field.Name} did not find matching component binding");
}
}
}

View File

@ -1,104 +1,648 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using AlicizaX.UI.Runtime;
using Newtonsoft.Json;
using Sirenix.OdinInspector;
using Sirenix.OdinInspector.Editor;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
using UnityEngine.Windows;
namespace AlicizaX.UI.Editor
{
internal class UISettingEditorWindow : OdinEditorWindow
public class UISettingEditorWindow_NoOdin_v2 : EditorWindow
{
[MenuItem("Tools/AlicizaX/UI/Setting Window")]
[MenuItem("Tools/AlicizaX/UISetting Window")]
private static void OpenWindow()
{
GetWindow<UISettingEditorWindow>().Show();
var w = GetWindow<UISettingEditorWindow_NoOdin_v2>("UI Setting");
w.minSize = new Vector2(760, 520);
w.Show();
}
protected override void Initialize()
private UIGenerateConfiguration uiGenerateConfiguration;
private UIGenerateCommonData UIGenerateCommonData;
private List<UIEelementRegexData> UIElementRegexConfigs;
private List<UIScriptGenerateData> UIScriptGenerateConfigs;
private List<string> excludeKeywordsList = new List<string>();
private Vector2 scroll;
private int toolbarTab;
private readonly string[] toolbarTitles = { "UI基础设置", "UI构建配置", "UI元素映射" };
private ReorderableList combineList;
private ReorderableList regexList;
private ReorderableList projectList;
private ReorderableList excludeList;
private TextAsset importText;
private string previewLabel;
private string previewCompLabel;
private List<string> m_ScriptGeneratorHelperTypes = new();
private int m_ScriptGeneratorHelperSelectIndex;
private void OnEnable()
{
uiGenerateConfiguration = UIGenerateConfiguration.Instance;
UIGenerateCommonData = uiGenerateConfiguration.UIGenerateCommonData;
UIScriptGenerateConfig = uiGenerateConfiguration.UIScriptGenerateConfig;
UIElementRegexConfigs = uiGenerateConfiguration.UIElementRegexConfigs;
if (uiGenerateConfiguration == null)
{
uiGenerateConfiguration = ScriptableObject.CreateInstance<UIGenerateConfiguration>();
}
UIGenerateCommonData = uiGenerateConfiguration.UIGenerateCommonData ?? new UIGenerateCommonData();
UIElementRegexConfigs = uiGenerateConfiguration.UIElementRegexConfigs ?? new List<UIEelementRegexData>();
UIScriptGenerateConfigs = uiGenerateConfiguration.UIScriptGenerateConfigs ?? new List<UIScriptGenerateData>();
excludeKeywordsList = (UIGenerateCommonData.ExcludeKeywords ?? new string[0]).ToList();
SetupLists();
RefreshLabel();
RefreshScriptGeneratorHelperTypes();
}
private void SetupLists()
{
combineList = new ReorderableList(UIGenerateCommonData.CombineWords, typeof(StringPair), true, true, true, true);
combineList.drawHeaderCallback = (r) => EditorGUI.LabelField(r, "路径拼接映射 (Key -> Value)");
combineList.drawElementCallback = (rect, index, active, focused) =>
{
var p = UIGenerateCommonData.CombineWords[index];
rect.y += 2;
float half = rect.width / 2 - 8;
p.Key = EditorGUI.TextField(new Rect(rect.x, rect.y, half, EditorGUIUtility.singleLineHeight), p.Key);
p.Value = EditorGUI.TextField(new Rect(rect.x + half + 16, rect.y, half, EditorGUIUtility.singleLineHeight), p.Value);
};
combineList.onAddCallback = (r) => UIGenerateCommonData.CombineWords.Add(new StringPair("Key", "Value"));
combineList.onRemoveCallback = (r) =>
{
if (r.index >= 0) UIGenerateCommonData.CombineWords.RemoveAt(r.index);
};
excludeList = new ReorderableList(excludeKeywordsList, typeof(string), true, true, true, true);
excludeList.drawHeaderCallback = (r) => EditorGUI.LabelField(r, "排除关键字(匹配则不生成)");
excludeList.drawElementCallback = (rect, index, active, focused) =>
{
rect.y += 2;
excludeKeywordsList[index] = EditorGUI.TextField(new Rect(rect.x, rect.y, rect.width, EditorGUIUtility.singleLineHeight), excludeKeywordsList[index]);
};
excludeList.onAddCallback = (r) => excludeKeywordsList.Add(string.Empty);
excludeList.onRemoveCallback = (r) =>
{
if (r.index >= 0) excludeKeywordsList.RemoveAt(r.index);
};
regexList = new ReorderableList(UIElementRegexConfigs, typeof(UIEelementRegexData), true, true, true, true);
regexList.drawHeaderCallback = (r) => EditorGUI.LabelField(r, "UI元素映射 (正则 -> 组件)");
regexList.elementHeightCallback = (i) => EditorGUIUtility.singleLineHeight * 2 + 8;
regexList.drawElementCallback = (rect, index, active, focused) =>
{
var item = UIElementRegexConfigs[index];
rect.y += 2;
float lh = EditorGUIUtility.singleLineHeight;
EditorGUI.BeginChangeCheck();
item.uiElementRegex = EditorGUI.TextField(new Rect(rect.x + 70, rect.y, rect.width - 70, lh), item.uiElementRegex);
if (EditorGUI.EndChangeCheck()) RefreshLabel();
EditorGUI.LabelField(new Rect(rect.x, rect.y + lh + 4, 60, lh), "Component");
var btnRect = new Rect(rect.x + 70, rect.y + lh + 4, 180, lh);
string btnLabel = string.IsNullOrEmpty(item.componentType) ? "(选择类型)" : item.componentType;
if (GUI.Button(btnRect, btnLabel, EditorStyles.popup))
{
var opts = CollectComponentTypeNamesFallback();
Rect anchor = new Rect(btnRect.x, btnRect.y + btnRect.height, Math.Min(360f, Mathf.Max(btnRect.width, 200f)), btnRect.height);
SearchablePopup.Show(anchor, opts, Math.Max(0, opts.IndexOf(item.componentType)), (selIndex) =>
{
if (selIndex >= 0 && selIndex < opts.Count)
{
item.componentType = opts[selIndex];
Repaint();
}
});
}
item.componentType = EditorGUI.TextField(new Rect(rect.x + 260, rect.y + lh + 4, rect.width - 260, lh), item.componentType);
};
regexList.onAddCallback = (r) => UIElementRegexConfigs.Add(new UIEelementRegexData { uiElementRegex = "", componentType = "" });
regexList.onRemoveCallback = (r) =>
{
if (r.index >= 0) UIElementRegexConfigs.RemoveAt(r.index);
};
projectList = new ReorderableList(UIScriptGenerateConfigs, typeof(UIScriptGenerateData), true, true, true, true);
projectList.drawHeaderCallback = (r) => EditorGUI.LabelField(r, "UI脚本生成配置多个项目");
projectList.elementHeightCallback = (i) => EditorGUIUtility.singleLineHeight * 5 + 10;
projectList.drawElementCallback = (rect, index, active, focused) =>
{
var d = UIScriptGenerateConfigs[index];
float lh = EditorGUIUtility.singleLineHeight;
float pad = 2;
d.ProjectName = EditorGUI.TextField(new Rect(rect.x, rect.y, rect.width, lh), "项目名", d.ProjectName);
d.NameSpace = EditorGUI.TextField(new Rect(rect.x, rect.y + (lh + pad), rect.width, lh), "命名空间", d.NameSpace);
d.GenerateHolderCodePath = DrawFolderField("生成脚本路径", d.GenerateHolderCodePath, rect.x, rect.y + 2 * (lh + pad), rect.width, lh);
d.UIPrefabRootPath = DrawFolderField("Prefab根目录", d.UIPrefabRootPath, rect.x, rect.y + 3 * (lh + pad), rect.width, lh);
d.LoadType = (EUIResLoadType)EditorGUI.EnumPopup(new Rect(rect.x, rect.y + 4 * (lh + pad), rect.width, lh), "加载类型", d.LoadType);
};
projectList.onAddCallback = (r) => UIScriptGenerateConfigs.Add(new UIScriptGenerateData { ProjectName = "NewProject", NameSpace = "Game.UI", GenerateHolderCodePath = "Assets/Scripts/UI/Generated", UIPrefabRootPath = "Assets/Art/UI/Prefabs", LoadType = EUIResLoadType.Resources });
projectList.onRemoveCallback = (r) =>
{
if (r.index >= 0) UIScriptGenerateConfigs.RemoveAt(r.index);
};
}
private string DrawFolderField(string label, string value, float x, float y, float width, float h)
{
var txtRect = new Rect(x, y, width - 76, h);
var btnRect = new Rect(x + width - 72, y, 68, h);
value = EditorGUI.TextField(txtRect, label, value);
if (GUI.Button(btnRect, "选择"))
{
string p = EditorUtility.OpenFolderPanel("选择路径", Application.dataPath, "");
if (!string.IsNullOrEmpty(p))
{
if (p.StartsWith(Application.dataPath))
value = "Assets" + p.Substring(Application.dataPath.Length);
else
EditorUtility.DisplayDialog("提示", "请选择 Assets 下的路径", "确定");
}
}
return value;
}
private void RefreshScriptGeneratorHelperTypes()
{
m_ScriptGeneratorHelperTypes = new List<string>();
m_ScriptGeneratorHelperTypes.AddRange(AlicizaX.Utility.Assembly.GetRuntimeTypeNames(typeof(IUIGeneratorHelper)));
m_ScriptGeneratorHelperSelectIndex = m_ScriptGeneratorHelperTypes.IndexOf(UIGenerateConfiguration.Instance.UIScriptGeneratorHelper);
if (m_ScriptGeneratorHelperSelectIndex < 0)
{
m_ScriptGeneratorHelperSelectIndex = 0;
}
}
private void OnGUI()
{
GUILayout.Space(6);
GUILayout.BeginHorizontal(EditorStyles.toolbar);
for (int i = 0; i < toolbarTitles.Length; i++)
{
bool isActive = (toolbarTab == i);
bool result = GUILayout.Toggle(isActive, toolbarTitles[i], EditorStyles.toolbarButton, GUILayout.Height(22));
if (result && toolbarTab != i)
{
toolbarTab = i;
Repaint();
}
}
GUILayout.FlexibleSpace();
var saveIcon = EditorGUIUtility.IconContent("SaveActive");
var refreshIcon = EditorGUIUtility.IconContent("Refresh");
var reloadIcon = EditorGUIUtility.IconContent("RotateTool");
GUIContent saveBtn = new GUIContent(saveIcon.image, "保存 (Save Now)");
GUIContent refreshBtn = new GUIContent(refreshIcon.image, "刷新预览");
GUIContent reloadBtn = new GUIContent(reloadIcon.image, "重载配置");
if (GUILayout.Button(saveBtn, EditorStyles.toolbarButton, GUILayout.Width(36)))
SaveConfig();
if (GUILayout.Button(refreshBtn, EditorStyles.toolbarButton, GUILayout.Width(36)))
RefreshLabel();
if (GUILayout.Button(reloadBtn, EditorStyles.toolbarButton, GUILayout.Width(36)))
{
OnEnable();
Repaint();
}
GUILayout.EndHorizontal();
scroll = EditorGUILayout.BeginScrollView(scroll);
GUILayout.Space(8);
switch (toolbarTab)
{
case 0: DrawCommonPane(); break;
case 1: DrawScriptPane(); break;
case 2: DrawElementPane(); break;
}
EditorGUILayout.EndScrollView();
GUILayout.Space(8);
}
private void DrawCommonPane()
{
EditorGUILayout.BeginVertical("box");
EditorGUILayout.LabelField("通用生成配置", EditorStyles.boldLabel);
EditorGUILayout.Space(4);
EditorGUILayout.BeginVertical();
EditorGUI.BeginChangeCheck();
UIGenerateCommonData.ComCheckSplitName = EditorGUILayout.TextField(new GUIContent("组件检查分隔符", "例如 Button#Close"), UIGenerateCommonData.ComCheckSplitName);
UIGenerateCommonData.ComCheckEndName = EditorGUILayout.TextField(new GUIContent("组件结尾分隔符", "例如 @End"), UIGenerateCommonData.ComCheckEndName);
UIGenerateCommonData.ArrayComSplitName = EditorGUILayout.TextField(new GUIContent("数组组件分隔符", "例如 *Item"), UIGenerateCommonData.ArrayComSplitName);
UIGenerateCommonData.GeneratePrefix = EditorGUILayout.TextField(new GUIContent("生成脚本前缀"), UIGenerateCommonData.GeneratePrefix);
m_ScriptGeneratorHelperSelectIndex = EditorGUILayout.Popup("解密服务", m_ScriptGeneratorHelperSelectIndex, m_ScriptGeneratorHelperTypes.ToArray());
string selectService = m_ScriptGeneratorHelperTypes[m_ScriptGeneratorHelperSelectIndex];
if (uiGenerateConfiguration.UIScriptGeneratorHelper != selectService)
{
UIGenerateConfiguration.Instance.UIScriptGeneratorHelper = selectService;
UIGenerateConfiguration.Save();
}
EditorGUILayout.EndVertical();
GUILayout.Space(8);
excludeList.DoLayoutList();
GUILayout.Space(8);
combineList.DoLayoutList();
EditorGUILayout.Space(8);
EditorGUILayout.LabelField("脚本生成预览", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(previewLabel ?? "", MessageType.None);
EditorGUILayout.LabelField("组件生成预览 (下标0开始)", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(previewCompLabel ?? "", MessageType.None);
EditorGUILayout.EndVertical();
GUILayout.Space(8);
}
private void DrawScriptPane()
{
EditorGUILayout.BeginVertical("box");
EditorGUILayout.LabelField("UI脚本生成配置支持多个项目", EditorStyles.boldLabel);
GUILayout.Space(6);
projectList.DoLayoutList();
EditorGUILayout.EndVertical();
GUILayout.Space(8);
}
private void DrawElementPane()
{
EditorGUILayout.BeginVertical("box");
EditorGUILayout.LabelField("UI元素映射正则 -> 组件)", EditorStyles.boldLabel);
GUILayout.Space(6);
GUILayout.BeginHorizontal(EditorStyles.toolbar);
{
if (GUILayout.Button("加载默认", EditorStyles.toolbarButton, GUILayout.Width(90)))
LoadDefault();
if (GUILayout.Button("导出", EditorStyles.toolbarButton, GUILayout.Width(70)))
ExportConfig();
GUILayout.Space(8);
importText = (TextAsset)EditorGUILayout.ObjectField(importText, typeof(TextAsset), false, GUILayout.Height(18), GUILayout.MinWidth(200));
GUI.enabled = importText != null;
if (GUILayout.Button("执行导入", EditorStyles.toolbarButton, GUILayout.Width(84)))
{
if (importText != null)
{
ImportConfig(importText);
importText = null;
}
}
GUI.enabled = true;
GUILayout.FlexibleSpace();
}
GUILayout.EndHorizontal();
GUILayout.Space(6);
regexList.elementHeightCallback = (i) => EditorGUIUtility.singleLineHeight + 6;
regexList.drawElementCallback = (rect, index, active, focused) =>
{
if (index < 0 || index >= UIElementRegexConfigs.Count) return;
var item = UIElementRegexConfigs[index];
rect.y += 2;
float lh = EditorGUIUtility.singleLineHeight;
float padding = 6f;
float leftWidth = rect.width * 0.80f;
Rect regexRect = new Rect(rect.x, rect.y, leftWidth - 8, lh);
EditorGUI.BeginChangeCheck();
string newRegex = EditorGUI.TextField(regexRect, item.uiElementRegex);
if (EditorGUI.EndChangeCheck())
{
item.uiElementRegex = newRegex;
RefreshLabel();
}
// [Required] [InlineEditor(InlineEditorObjectFieldModes.CompletelyHidden)] [DisableInPlayMode] [HideLabel]
private UIGenerateConfiguration uiGenerateConfiguration;
[Required] [DisableInPlayMode] [HideLabel] [TabGroup("UI基础设置")] [SerializeField]
public UIGenerateCommonData UIGenerateCommonData;
float rightX = rect.x + leftWidth + 8;
float rightWidth = rect.width - leftWidth - 8;
Rect btnRect = new Rect(rightX, rect.y, Math.Min(180, rightWidth), lh);
[TabGroup("UI基础设置")] [LabelText("脚本生成预览")] [ShowInInspector] [ReadOnly] [OnValueChanged("RefreshLabel")]
private string previewLabel;
string btnLabel = string.IsNullOrEmpty(item.componentType) ? "(选择类型)" : item.componentType;
if (GUI.Button(btnRect, btnLabel, EditorStyles.popup))
{
var opts = CollectComponentTypeNamesFallback();
Rect anchor = new Rect(btnRect.x, btnRect.y + btnRect.height, Math.Min(360f, Mathf.Max(btnRect.width, 200f)), btnRect.height);
[TabGroup("UI基础设置")] [LabelText("组件生成预览")] [ShowInInspector] [ReadOnly] [OnValueChanged("RefreshLabel")] [SuffixLabel("(下标0开始)")]
private string previewCompLabel;
SearchablePopup.Show(anchor, opts, Math.Max(0, opts.IndexOf(item.componentType)), (selIndex) =>
{
if (selIndex >= 0 && selIndex < opts.Count)
{
item.componentType = opts[selIndex];
Repaint();
}
});
}
};
[Required] [DisableInPlayMode] [HideLabel] [TabGroup("UI构建配置")] [SerializeField]
public UIScriptGenerateConfig UIScriptGenerateConfig;
[Required] [DisableInPlayMode] [HideLabel] [TabGroup("UI元素映射")] [SerializeField] [TableList(ShowIndexLabels = false, DrawScrollView = true, AlwaysExpanded = true)]
public List<UIEelementRegexData> UIElementRegexConfigs;
regexList.DoLayoutList();
EditorGUILayout.EndVertical();
}
private static List<string> cacheFilterType;
private List<string> CollectComponentTypeNamesFallback()
{
if (cacheFilterType == null)
{
cacheFilterType = AlicizaX.Utility.Assembly.GetTypes()
.Where(m => !m.FullName.Contains("Editor"))
.Where(x => !x.IsAbstract || x.IsInterface)
.Where(x => !x.IsGenericTypeDefinition)
.Where(x => !x.IsSubclassOf(typeof(UIHolderObjectBase)))
.Where(x => x.IsSubclassOf(typeof(Component)))
.Where(x => !x.FullName.Contains("YooAsset"))
.Where(x => !x.FullName.Contains(("Unity.VisualScripting")))
.Where(x => !x.FullName.Contains(("Cysharp.Threading")))
.Where(x => !x.FullName.Contains(("UnityEngine.Rendering.UI.Debug")))
.Where(x => !x.FullName.Contains(("Unity.PerformanceTesting")))
.Where(x => !x.FullName.Contains(("UnityEngine.TestTools")))
.Select(x => x.Name).ToList();
cacheFilterType.Add(typeof(GameObject).Name);
}
return cacheFilterType;
}
private void LoadDefault()
{
string defaultPath = null;
try
{
var f = typeof(UIGlobalPath).GetField("DefaultComPath", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic);
if (f != null) defaultPath = f.GetValue(null) as string;
}
catch
{
}
if (string.IsNullOrEmpty(defaultPath)) defaultPath = "Assets/uielementconfig.txt";
if (!File.Exists(defaultPath))
{
EditorUtility.DisplayDialog("加载默认", $"未找到默认文件:{defaultPath}", "OK");
return;
}
string txt = File.ReadAllText(defaultPath);
var list = JsonConvert.DeserializeObject<List<UIEelementRegexData>>(txt);
if (list != null)
{
UIElementRegexConfigs = list;
regexList.list = UIElementRegexConfigs;
RefreshLabel();
}
}
private void ImportConfig(TextAsset text)
{
try
{
var list = JsonConvert.DeserializeObject<List<UIEelementRegexData>>(text.text);
if (list != null)
{
UIElementRegexConfigs = list;
regexList.list = UIElementRegexConfigs;
RefreshLabel();
}
}
catch (Exception ex)
{
Debug.LogException(ex);
EditorUtility.DisplayDialog("错误", "导入失败,请查看控制台", "OK");
}
}
private void ExportConfig()
{
string json = JsonConvert.SerializeObject(UIElementRegexConfigs, Formatting.Indented);
string path = EditorUtility.SaveFilePanel("导出 UI 元素配置为 JSON", Application.dataPath, "uielementconfig", "txt");
if (!string.IsNullOrEmpty(path))
{
File.WriteAllText(path, json);
AssetDatabase.Refresh();
EditorUtility.DisplayDialog("导出完成", "已导出", "OK");
}
}
private void RefreshLabel()
{
previewLabel = $"{UIGenerateCommonData.GeneratePrefix}_UITestWindow";
previewCompLabel = $"{UIGenerateCommonData.ArrayComSplitName}Text{UIGenerateCommonData.ComCheckSplitName}Img" +
$"{UIGenerateCommonData.ComCheckEndName}Test{UIGenerateCommonData.ArrayComSplitName}0";
previewCompLabel = $"{UIGenerateCommonData.ArrayComSplitName}Text{UIGenerateCommonData.ComCheckSplitName}Img{UIGenerateCommonData.ComCheckEndName}Test{UIGenerateCommonData.ArrayComSplitName}0";
Repaint();
}
[TabGroup("UI元素映射")]
[Sirenix.OdinInspector.Button("加载默认")]
private void LoadDefaultConfig()
private void SaveConfig()
{
const string Path = UIGlobalPath.DefaultComPath;
string text = System.IO.File.ReadAllText(Path);
UIElementRegexConfigs = JsonConvert.DeserializeObject<List<UIEelementRegexData>>(text);
}
UIGenerateCommonData.ExcludeKeywords = excludeKeywordsList.ToArray();
[TabGroup("UI元素映射")]
[Sirenix.OdinInspector.Button("导出")]
private void ExportConfig()
{
var json = JsonConvert.SerializeObject(UIElementRegexConfigs);
System.IO.File.WriteAllText("Assets/uielementconfig.txt", json);
AssetDatabase.Refresh();
Debug.Log("Export UIElements Finished");
}
[TabGroup("UI元素映射")]
[Sirenix.OdinInspector.Button("导入")]
private void ImportConfig(TextAsset text)
{
UIElementRegexConfigs = JsonConvert.DeserializeObject<List<UIEelementRegexData>>(text.text);
Debug.Log("Import UIElements Finished");
}
protected override void OnDisable()
{
base.OnDisable();
uiGenerateConfiguration.UIGenerateCommonData = UIGenerateCommonData;
uiGenerateConfiguration.UIScriptGenerateConfig = UIScriptGenerateConfig;
uiGenerateConfiguration.UIElementRegexConfigs = UIElementRegexConfigs;
EditorUtility.SetDirty(uiGenerateConfiguration);
AssetDatabase.SaveAssets();
uiGenerateConfiguration.UIScriptGenerateConfigs = UIScriptGenerateConfigs;
UIGenerateConfiguration.Save();
}
protected override void OnDestroy()
private void OnDisable() => SaveConfig();
}
internal class SearchablePopup : PopupWindowContent
{
base.OnDestroy();
uiGenerateConfiguration.UIGenerateCommonData = UIGenerateCommonData;
uiGenerateConfiguration.UIScriptGenerateConfig = UIScriptGenerateConfig;
uiGenerateConfiguration.UIElementRegexConfigs = UIElementRegexConfigs;
EditorUtility.SetDirty(uiGenerateConfiguration);
AssetDatabase.SaveAssets();
UIGenerateConfiguration.Save();
private readonly List<string> allItems;
private List<string> filtered;
private readonly Action<int> onSelect;
private int currentIndex;
private string search = "";
private Vector2 scroll;
private static GUIStyle searchFieldStyle;
private static GUIStyle cancelStyle;
private static GUIStyle rowStyle;
private static GUIStyle selectedRowStyle;
private const float ROW_HEIGHT = 20f;
private SearchablePopup(List<string> items, int currentIndex, Action<int> onSelect)
{
this.allItems = items ?? new List<string>();
this.filtered = new List<string>(this.allItems);
this.currentIndex = Mathf.Clamp(currentIndex, -1, this.allItems.Count - 1);
this.onSelect = onSelect;
}
public static void Show(Rect anchorRect, List<string> items, int currentIndex, Action<int> onSelect)
{
PopupWindow.Show(anchorRect, new SearchablePopup(items, currentIndex, onSelect));
}
public override Vector2 GetWindowSize() => new Vector2(360, 320);
public override void OnOpen()
{
EditorApplication.delayCall += () => EditorGUI.FocusTextInControl("SearchField");
}
public override void OnGUI(Rect rect)
{
InitStyles();
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
GUI.SetNextControlName("SearchField");
search = EditorGUILayout.TextField(search, searchFieldStyle, GUILayout.ExpandWidth(true));
if (GUILayout.Button("", cancelStyle, GUILayout.Width(18)))
{
search = "";
GUI.FocusControl("SearchField");
}
EditorGUILayout.EndHorizontal();
FilterList(search);
HandleKeyboard();
scroll = EditorGUILayout.BeginScrollView(scroll);
for (int i = 0; i < filtered.Count; i++)
{
bool selected = (i == currentIndex);
var style = selected ? selectedRowStyle : rowStyle;
Rect r = GUILayoutUtility.GetRect(new GUIContent(filtered[i]), style, GUILayout.Height(ROW_HEIGHT), GUILayout.ExpandWidth(true));
if (Event.current.type == EventType.Repaint)
style.Draw(r, filtered[i], false, false, selected, false);
if (Event.current.type == EventType.MouseDown && r.Contains(Event.current.mousePosition))
{
Select(filtered[i]);
Event.current.Use();
}
}
EditorGUILayout.EndScrollView();
}
private void HandleKeyboard()
{
var e = Event.current;
if (e.type != EventType.KeyDown) return;
if (e.keyCode == KeyCode.DownArrow)
{
currentIndex = Mathf.Min(currentIndex + 1, filtered.Count - 1);
e.Use();
editorWindow.Repaint();
}
else if (e.keyCode == KeyCode.UpArrow)
{
currentIndex = Mathf.Max(currentIndex - 1, 0);
e.Use();
editorWindow.Repaint();
}
else if (e.keyCode == KeyCode.Return || e.keyCode == KeyCode.KeypadEnter)
{
if (filtered.Count > 0 && currentIndex >= 0 && currentIndex < filtered.Count)
Select(filtered[currentIndex]);
e.Use();
}
}
private void FilterList(string keyword)
{
if (string.IsNullOrEmpty(keyword))
filtered = new List<string>(allItems);
else
{
string lower = keyword.ToLowerInvariant();
filtered = allItems.Where(i => i != null && i.ToLowerInvariant().Contains(lower)).ToList();
}
if (filtered.Count == 0) currentIndex = -1;
else currentIndex = Mathf.Clamp(currentIndex, 0, filtered.Count - 1);
}
private void Select(string item)
{
int originalIndex = allItems.IndexOf(item);
if (originalIndex >= 0)
onSelect?.Invoke(originalIndex);
editorWindow.Close();
GUIUtility.ExitGUI();
}
private void InitStyles()
{
if (searchFieldStyle == null)
searchFieldStyle = GUI.skin.FindStyle("ToolbarSeachTextField") ?? EditorStyles.toolbarSearchField;
if (cancelStyle == null)
cancelStyle = GUI.skin.FindStyle("ToolbarSeachCancelButton") ?? EditorStyles.toolbarButton;
if (rowStyle == null)
{
rowStyle = new GUIStyle("PR Label") { alignment = TextAnchor.MiddleLeft, padding = new RectOffset(6, 6, 2, 2) };
}
if (selectedRowStyle == null)
{
selectedRowStyle = new GUIStyle(rowStyle) { normal = { background = Texture2D.grayTexture } };
}
}
}
}

View File

@ -0,0 +1,31 @@
using System;
namespace AlicizaX.UI.Editor
{
public interface IUIGeneratorHelper
{
string GetPrivateComponentByNameRule(string regexName, string componetName, EBindType bindType);
string GetPublicComponentByNameRule(string variableName);
}
public class DefaultUIGeneratorHelper : IUIGeneratorHelper
{
public string GetPrivateComponentByNameRule(string regexName, string componentName, EBindType bindType)
{
string endPrefix = bindType == EBindType.ListCom ? "List" : string.Empty;
int endNameIndex = componentName.IndexOf(
UIGenerateConfiguration.Instance.UIGenerateCommonData.ComCheckEndName,
StringComparison.Ordinal);
string componentSuffix = endNameIndex >= 0 ? componentName.Substring(endNameIndex + 1) : componentName;
return $"m{regexName}{componentSuffix}{endPrefix}";
}
public string GetPublicComponentByNameRule(string variableName)
{
return variableName.Substring(1);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2df48f035545426a83a384df3411755a
timeCreated: 1762516569

View File

@ -0,0 +1,80 @@
using UnityEngine;
using UnityEditor;
using UnityEditorInternal;
using System;
using System.Collections.Generic;
using System.Reflection;
using AlicizaX.UI.Runtime;
[CustomEditor(typeof(UIHolderObjectBase), true)]
public class UIHolderObjectBaseEditor : Editor
{
private SerializedProperty[] serializedProperties;
private Dictionary<string, ReorderableList> reorderableDic = new Dictionary<string, ReorderableList>();
private void OnEnable()
{
var fields = target.GetType().GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
serializedProperties = new SerializedProperty[fields.Length];
for (int i = 0; i < fields.Length; i++)
{
SerializedProperty prop = serializedObject.FindProperty(fields[i].Name);
if (prop != null)
{
serializedProperties[i] = prop;
if (prop.isArray)
{
string arrayElementTypeName = prop.GetArrayElementAtIndex(0).type;
ReorderableList reorderableList = new ReorderableList(serializedObject, prop, true, true, true, true);
reorderableList.drawElementCallback = (rect, index, isActive, isFocused) => DrawElementCallback(rect, index, prop, isActive, isFocused);
reorderableList.drawHeaderCallback = (rect) => DrawHeaderCallback(rect, arrayElementTypeName);
reorderableDic.Add(prop.propertyPath, reorderableList);
}
}
}
}
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUI.BeginDisabledGroup(true);
for (int i = 0; i < serializedProperties.Length; i++)
{
var property = serializedProperties[i];
if (property != null)
{
if (property.isArray && reorderableDic.TryGetValue(property.propertyPath, out var reorderableList))
{
reorderableList.DoLayoutList();
}
else
{
EditorGUILayout.PropertyField(property, GUIContent.none, true);
}
}
}
EditorGUI.EndDisabledGroup();
serializedObject.ApplyModifiedProperties();
}
private void DrawElementCallback(Rect rect, int index, SerializedProperty arrayProperty, bool isActive, bool isFocused)
{
var element = arrayProperty.GetArrayElementAtIndex(index);
EditorGUI.PropertyField(new Rect(rect.x, rect.y, rect.width, EditorGUIUtility.singleLineHeight), element, GUIContent.none);
}
private void DrawHeaderCallback(Rect rect, string arrayElementTypeName)
{
string arrayTypeName = $"{arrayElementTypeName}[]";
EditorGUI.LabelField(rect, arrayTypeName);
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f03f3b9e96e44b858fa231e9832646ca
timeCreated: 1762514196

View File

@ -2,98 +2,84 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using AlicizaX.Editor.Setting;
using AlicizaX;
using AlicizaX.Editor.Setting;
using AlicizaX.UI.Runtime;
using Newtonsoft.Json;
using Sirenix.OdinInspector;
using UnityEngine;
using UnityEngine.Serialization;
using UnityEditor;
namespace AlicizaX.UI.Editor
{
[AlicizaX.Editor.Setting.FilePath("ProjectSettings/UIGenerateConfiguration.asset")]
internal class UIGenerateConfiguration : ScriptableSingleton<UIGenerateConfiguration>
internal class UIGenerateConfiguration : AlicizaX.Editor.Setting.ScriptableSingleton<UIGenerateConfiguration>
{
public UIGenerateCommonData UIGenerateCommonData = new UIGenerateCommonData();
public UIScriptGenerateConfig UIScriptGenerateConfig = new UIScriptGenerateConfig();
public List<UIEelementRegexData> UIElementRegexConfigs = new List<UIEelementRegexData>();
[Header("通用生成配置")] public UIGenerateCommonData UIGenerateCommonData = new UIGenerateCommonData();
[Header("UI生成规则根据正则匹配")] public List<UIEelementRegexData> UIElementRegexConfigs = new List<UIEelementRegexData>();
[Header("UI脚本生成配置支持多个项目")] public List<UIScriptGenerateData> UIScriptGenerateConfigs = new List<UIScriptGenerateData>();
[Header("UI脚本生成辅助类")] public string UIScriptGeneratorHelper;
}
[System.Serializable]
[Serializable]
public class UIGenerateCommonData
{
[LabelText("组件检查分隔符")] public string ComCheckSplitName = "#";
[LabelText("组件结尾分隔符")] public string ComCheckEndName = "@";
[LabelText("数组组件检查分隔符")] public string ArrayComSplitName = "*";
[LabelText("生成脚本前缀")] public string GeneratePrefix = "ui";
[LabelText("排除表")] public string[] ExcludeKeywords = { "ViewHolder" };
[Header("命名规则")] [Tooltip("组件检查分隔符例如Button#Close")]
public string ComCheckSplitName = "#";
[ShowInInspector] [LabelText("生成路径拼接")]
public Dictionary<string, string> CombineWords = new Dictionary<string, string>
[Tooltip("组件结尾分隔符,例如:@End")] public string ComCheckEndName = "@";
[Tooltip("数组组件检查分隔符,例如:*Item")] public string ArrayComSplitName = "*";
[Tooltip("生成脚本前缀")] public string GeneratePrefix = "ui";
[Tooltip("排除的关键字(匹配则不生成)")] public string[] ExcludeKeywords = { "ViewHolder" };
[Tooltip("路径拼接映射Window -> Window 文件夹")]
public List<StringPair> CombineWords = new List<StringPair>()
{
{ "Window", "Window" },
{ "ViewHolder", "ViewHolder" },
{ "Widget", "Widget" },
new StringPair("Window", "Window"),
new StringPair("ViewHolder", "ViewHolder"),
new StringPair("Widget", "Widget"),
};
}
[System.Serializable]
[Serializable]
public class UIEelementRegexData
{
public string uiElementRegex;
[Tooltip("匹配UI元素名称的正则表达式")] public string uiElementRegex;
[ShowInInspector] [ValueDropdown("GetFilteredTypeList", ExpandAllMenuItems = false)]
public string componentType;
private static List<string> cacheFilterType;
public IEnumerable<string> GetFilteredTypeList()
{
if (cacheFilterType == null)
{
cacheFilterType = AlicizaX.Utility.Assembly.GetTypes()
.Where(m => !m.FullName.Contains("Editor"))
.Where(x => !x.IsAbstract || x.IsInterface)
.Where(x => !x.IsGenericTypeDefinition)
.Where(x => !x.IsSubclassOf(typeof(UIHolderObjectBase)))
.Where(x => x.IsSubclassOf(typeof(Component)))
.Where(x => !x.FullName.Contains("YooAsset"))
.Where(x => !x.FullName.Contains(("Unity.VisualScripting")))
.Where(x => !x.FullName.Contains(("Cysharp.Threading")))
.Where(x => !x.FullName.Contains(("UnityEngine.Rendering.UI.Debug")))
.Where(x => !x.FullName.Contains(("Unity.PerformanceTesting")))
.Where(x => !x.FullName.Contains(("UnityEngine.TestTools")))
.Select(x => x.Name).ToList();
cacheFilterType.Add(typeof(GameObject).Name);
[Tooltip("匹配到的UI组件类型")] public string componentType;
}
return cacheFilterType;
}
}
[System.Serializable]
public class UIScriptGenerateConfig
{
[BoxGroup("主工程")] public UIScriptGenerateData MainProjectUIScriptGenerateData = new UIScriptGenerateData();
[BoxGroup("热更工程")] public UIScriptGenerateData HotFixProjectUIScriptGenerateData = new UIScriptGenerateData();
}
[System.Serializable]
[Serializable]
public class UIScriptGenerateData
{
public string NameSpace;
[Header("项目识别信息")] [Tooltip("该UI工程的名称例如MainProject, HotFix, EditorUI")]
public string ProjectName = "MainProject";
[Sirenix.OdinInspector.FolderPath(RequireExistingPath = true, AbsolutePath = false)]
public string GenerateHolderCodePath;
[Tooltip("该UI工程所属命名空间")] public string NameSpace = "Game.UI";
[Sirenix.OdinInspector.FolderPath(RequireExistingPath = true, AbsolutePath = false)]
public string UIPrefabRootPath;
[Header("路径设置")] [Tooltip("生成的UI脚本路径相对Assets")]
public string GenerateHolderCodePath = "Assets/Scripts/UI/Generated";
public EUIResLoadType LoadType;
[Tooltip("UI Prefab根目录")] public string UIPrefabRootPath = "Assets/Art/UI/Prefabs";
[Header("加载类型")] [Tooltip("UI资源加载方式本地 / YooAsset / Addressable等")]
public EUIResLoadType LoadType = EUIResLoadType.Resources;
}
[Serializable]
public class StringPair
{
public string Key;
public string Value;
public StringPair(string key, string value)
{
Key = key;
Value = value;
}
}
}

View File

@ -1,15 +1,10 @@
using Sirenix.OdinInspector;
using System;
using System.Collections.Generic;
using System;
using Cysharp.Threading.Tasks;
using Sirenix.Utilities;
using UnityEngine;
using UnityEngine.UI;
namespace AlicizaX.UI.Runtime
{
[DisallowMultipleComponent]
[HideMonoScript]
public abstract class UIHolderObjectBase : UnityEngine.MonoBehaviour
{
public Action OnWindowInitEvent;