From f3251980f5c21035301d011da54fcc5be39b0c48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=80=9D=E6=B5=B7?= <1464576565@qq.com> Date: Fri, 7 Nov 2025 20:47:57 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UI/GenerateTool/UIGenerateEditorTool.cs | 78 +- .../GenerateTool/UIScriptGeneratorHelper.cs | 300 ++++---- .../UI/GenerateTool/UISettingEditorWindow.cs | 678 ++++++++++++++++-- Editor/UI/IUIGeneratorHelper.cs | 31 + Editor/UI/IUIGeneratorHelper.cs.meta | 3 + .../Inspector/UIHolderObjectBaseInspector.cs | 80 +++ .../UIHolderObjectBaseInspector.cs.meta | 3 + Editor/UI/UIConfig/UIGenerateConfiguration.cs | 118 ++- Runtime/UI/UIBase/UIHolderObjectBase.cs | 7 +- 9 files changed, 1012 insertions(+), 286 deletions(-) create mode 100644 Editor/UI/IUIGeneratorHelper.cs create mode 100644 Editor/UI/IUIGeneratorHelper.cs.meta create mode 100644 Editor/UI/Inspector/UIHolderObjectBaseInspector.cs create mode 100644 Editor/UI/Inspector/UIHolderObjectBaseInspector.cs.meta diff --git a/Editor/UI/GenerateTool/UIGenerateEditorTool.cs b/Editor/UI/GenerateTool/UIGenerateEditorTool.cs index d26790b..9263dad 100644 --- a/Editor/UI/GenerateTool/UIGenerateEditorTool.cs +++ b/Editor/UI/GenerateTool/UIGenerateEditorTool.cs @@ -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("", 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); + } } } } diff --git a/Editor/UI/GenerateTool/UIScriptGeneratorHelper.cs b/Editor/UI/GenerateTool/UIScriptGeneratorHelper.cs index a906449..499033d 100644 --- a/Editor/UI/GenerateTool/UIScriptGeneratorHelper.cs +++ b/Editor/UI/GenerateTool/UIScriptGeneratorHelper.cs @@ -12,15 +12,15 @@ using UnityEditor; using UnityEditor.Callbacks; using UnityEngine.UI; +public enum EBindType +{ + None, + Widget, + ListCom, +} + namespace AlicizaX.UI.Editor { - enum EBindType - { - None, - Widget, - ListCom, - } - [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 + /// + /// 设置自定义命名规则助手[4](@ref) + /// + 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 UIBindDatas = new List(); - private static string GenerateNameSpace = string.Empty; - - private static List ArrayComs = new List(); + private static List _uiBindDatas = new List(); + private static string _generateNameSpace = string.Empty; + private static List _arrayComponents = new List(); private static void GetBindData(Transform root) { @@ -100,34 +123,42 @@ namespace AlicizaX.UI.Editor { Transform child = root.GetChild(i); - bool hasWdiget = child.GetComponent() != null; + bool hasWidget = child.GetComponent() != 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 arrayComs = new List(); + 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 arrayComponents = new List(); 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(); - 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 arrayNode, string nodeName) { - string[] comArray = SplitComName(nodeName); - arrayNode = arrayNode.OrderBy(s => int.Parse(s.name.Split('*').Last())).ToList(); - List tempBindDatas = new List(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 tempBindDatas = new List(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(), EBindType.ListCom)); + string keyName = GetKeyName(com, nodeName, EBindType.ListCom); + if (tempBindDatas.Count - 1 < index) + tempBindDatas.Add(new UIBindData(keyName, new List(), 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 GetRefrenceNameSpace() + private static string GetReferenceNamespace() { - StringBuilder refrenceNameSpaceBuilder = new StringBuilder(); - HashSet nameSpaces = new HashSet(); - nameSpaces.Add("UnityEngine"); - refrenceNameSpaceBuilder.Append($"using UnityEngine;\n"); - foreach (var bindData in UIBindDatas) + StringBuilder referenceNamespaceBuilder = new StringBuilder(); + HashSet namespaces = new HashSet(); + namespaces.Add("UnityEngine"); + referenceNamespaceBuilder.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 uiBindDatas) + private static string GetVariableText(List 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 componentInObjects = UIBindDatas.Find(data => "m" + data.Name == field.Name)?.BindCom; + List 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"); } } } diff --git a/Editor/UI/GenerateTool/UISettingEditorWindow.cs b/Editor/UI/GenerateTool/UISettingEditorWindow.cs index 0b0e937..4e5cbaf 100644 --- a/Editor/UI/GenerateTool/UISettingEditorWindow.cs +++ b/Editor/UI/GenerateTool/UISettingEditorWindow.cs @@ -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().Show(); + var w = GetWindow("UI Setting"); + w.minSize = new Vector2(760, 520); + w.Show(); } - protected override void Initialize() - { - uiGenerateConfiguration = UIGenerateConfiguration.Instance; - UIGenerateCommonData = uiGenerateConfiguration.UIGenerateCommonData; - UIScriptGenerateConfig = uiGenerateConfiguration.UIScriptGenerateConfig; - UIElementRegexConfigs = uiGenerateConfiguration.UIElementRegexConfigs; - RefreshLabel(); - } - - // [Required] [InlineEditor(InlineEditorObjectFieldModes.CompletelyHidden)] [DisableInPlayMode] [HideLabel] private UIGenerateConfiguration uiGenerateConfiguration; + private UIGenerateCommonData UIGenerateCommonData; + private List UIElementRegexConfigs; + private List UIScriptGenerateConfigs; + + private List excludeKeywordsList = new List(); + + + 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; - [Required] [DisableInPlayMode] [HideLabel] [TabGroup("UI基础设置")] [SerializeField] - public UIGenerateCommonData UIGenerateCommonData; - [TabGroup("UI基础设置")] [LabelText("脚本生成预览")] [ShowInInspector] [ReadOnly] [OnValueChanged("RefreshLabel")] private string previewLabel; - - [TabGroup("UI基础设置")] [LabelText("组件生成预览")] [ShowInInspector] [ReadOnly] [OnValueChanged("RefreshLabel")] [SuffixLabel("(下标0开始)")] private string previewCompLabel; - [Required] [DisableInPlayMode] [HideLabel] [TabGroup("UI构建配置")] [SerializeField] - public UIScriptGenerateConfig UIScriptGenerateConfig; + private List m_ScriptGeneratorHelperTypes = new(); + private int m_ScriptGeneratorHelperSelectIndex; - [Required] [DisableInPlayMode] [HideLabel] [TabGroup("UI元素映射")] [SerializeField] [TableList(ShowIndexLabels = false, DrawScrollView = true, AlwaysExpanded = true)] - public List UIElementRegexConfigs; + private void OnEnable() + { + uiGenerateConfiguration = UIGenerateConfiguration.Instance; + if (uiGenerateConfiguration == null) + { + uiGenerateConfiguration = ScriptableObject.CreateInstance(); + } + + UIGenerateCommonData = uiGenerateConfiguration.UIGenerateCommonData ?? new UIGenerateCommonData(); + UIElementRegexConfigs = uiGenerateConfiguration.UIElementRegexConfigs ?? new List(); + UIScriptGenerateConfigs = uiGenerateConfiguration.UIScriptGenerateConfigs ?? new List(); + + 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(); + + 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(); + } + + + float rightX = rect.x + leftWidth + 8; + float rightWidth = rect.width - leftWidth - 8; + Rect btnRect = new Rect(rightX, rect.y, Math.Min(180, rightWidth), 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(); + } + }); + } + }; + + + regexList.DoLayoutList(); + + EditorGUILayout.EndVertical(); + } + + private static List cacheFilterType; + + + private List 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>(txt); + if (list != null) + { + UIElementRegexConfigs = list; + regexList.list = UIElementRegexConfigs; + RefreshLabel(); + } + } + + private void ImportConfig(TextAsset text) + { + try + { + var list = JsonConvert.DeserializeObject>(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>(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>(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 + { + private readonly List allItems; + private List filtered; + private readonly Action 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 items, int currentIndex, Action onSelect) { - base.OnDestroy(); - uiGenerateConfiguration.UIGenerateCommonData = UIGenerateCommonData; - uiGenerateConfiguration.UIScriptGenerateConfig = UIScriptGenerateConfig; - uiGenerateConfiguration.UIElementRegexConfigs = UIElementRegexConfigs; - EditorUtility.SetDirty(uiGenerateConfiguration); - AssetDatabase.SaveAssets(); - UIGenerateConfiguration.Save(); + this.allItems = items ?? new List(); + this.filtered = new List(this.allItems); + this.currentIndex = Mathf.Clamp(currentIndex, -1, this.allItems.Count - 1); + this.onSelect = onSelect; + } + + public static void Show(Rect anchorRect, List items, int currentIndex, Action 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(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 } }; + } } } } diff --git a/Editor/UI/IUIGeneratorHelper.cs b/Editor/UI/IUIGeneratorHelper.cs new file mode 100644 index 0000000..5cca2e0 --- /dev/null +++ b/Editor/UI/IUIGeneratorHelper.cs @@ -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); + } + } +} diff --git a/Editor/UI/IUIGeneratorHelper.cs.meta b/Editor/UI/IUIGeneratorHelper.cs.meta new file mode 100644 index 0000000..277a8db --- /dev/null +++ b/Editor/UI/IUIGeneratorHelper.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2df48f035545426a83a384df3411755a +timeCreated: 1762516569 \ No newline at end of file diff --git a/Editor/UI/Inspector/UIHolderObjectBaseInspector.cs b/Editor/UI/Inspector/UIHolderObjectBaseInspector.cs new file mode 100644 index 0000000..dcd224b --- /dev/null +++ b/Editor/UI/Inspector/UIHolderObjectBaseInspector.cs @@ -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 reorderableDic = new Dictionary(); + + 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); + } +} diff --git a/Editor/UI/Inspector/UIHolderObjectBaseInspector.cs.meta b/Editor/UI/Inspector/UIHolderObjectBaseInspector.cs.meta new file mode 100644 index 0000000..ae529d7 --- /dev/null +++ b/Editor/UI/Inspector/UIHolderObjectBaseInspector.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f03f3b9e96e44b858fa231e9832646ca +timeCreated: 1762514196 \ No newline at end of file diff --git a/Editor/UI/UIConfig/UIGenerateConfiguration.cs b/Editor/UI/UIConfig/UIGenerateConfiguration.cs index 4fc599b..5c7c558 100644 --- a/Editor/UI/UIConfig/UIGenerateConfiguration.cs +++ b/Editor/UI/UIConfig/UIGenerateConfiguration.cs @@ -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 + internal class UIGenerateConfiguration : AlicizaX.Editor.Setting.ScriptableSingleton { - public UIGenerateCommonData UIGenerateCommonData = new UIGenerateCommonData(); - public UIScriptGenerateConfig UIScriptGenerateConfig = new UIScriptGenerateConfig(); - public List UIElementRegexConfigs = new List(); + [Header("通用生成配置")] public UIGenerateCommonData UIGenerateCommonData = new UIGenerateCommonData(); + + [Header("UI生成规则(根据正则匹配)")] public List UIElementRegexConfigs = new List(); + + [Header("UI脚本生成配置(支持多个项目)")] public List UIScriptGenerateConfigs = new List(); + + [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 CombineWords = new Dictionary + [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 CombineWords = new List() { - { "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 cacheFilterType; - - public IEnumerable 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); - } - - return cacheFilterType; - } + [Tooltip("匹配到的UI组件类型")] public string componentType; } - - [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; + } } } diff --git a/Runtime/UI/UIBase/UIHolderObjectBase.cs b/Runtime/UI/UIBase/UIHolderObjectBase.cs index c5e8092..5382511 100644 --- a/Runtime/UI/UIBase/UIHolderObjectBase.cs +++ b/Runtime/UI/UIBase/UIHolderObjectBase.cs @@ -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;