From 2bf7d146bf91bb71a1c061b00edebb70853fb4d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=80=9D=E6=B5=B7?= <1464576565@qq.com> Date: Mon, 16 Mar 2026 18:33:06 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96UI=E7=94=9F=E6=88=90=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E6=B5=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GenerateWindow/UISettingEditorWindow.cs | 890 +++++++++++------- Editor/UI/Helper/IUIGeneratorRuleHelper.cs | 246 ++--- Editor/UI/Helper/UIGenerationContext.cs | 45 + Editor/UI/Helper/UIGenerationContext.cs.meta | 11 + Editor/UI/Helper/UIGeneratorRuleServices.cs | 338 +++++++ .../UI/Helper/UIGeneratorRuleServices.cs.meta | 11 + Editor/UI/Helper/UIScriptGeneratorHelper.cs | 451 ++++++--- .../Inspector/UIHolderObjectBaseInspector.cs | 4 +- Runtime/UI/Constant/UIHolderFactory.cs | 7 +- Runtime/UI/UIBase/UIBase.cs | 2 +- 10 files changed, 1324 insertions(+), 681 deletions(-) create mode 100644 Editor/UI/Helper/UIGenerationContext.cs create mode 100644 Editor/UI/Helper/UIGenerationContext.cs.meta create mode 100644 Editor/UI/Helper/UIGeneratorRuleServices.cs create mode 100644 Editor/UI/Helper/UIGeneratorRuleServices.cs.meta diff --git a/Editor/UI/GenerateWindow/UISettingEditorWindow.cs b/Editor/UI/GenerateWindow/UISettingEditorWindow.cs index f8a492b..e34655c 100644 --- a/Editor/UI/GenerateWindow/UISettingEditorWindow.cs +++ b/Editor/UI/GenerateWindow/UISettingEditorWindow.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -10,192 +10,296 @@ using UnityEngine; namespace AlicizaX.UI.Editor { - public class UISettingEditorWindow_NoOdin_v2 : EditorWindow + public class UISettingEditorWindow : EditorWindow { + private const float ToolbarButtonWidth = 36f; + + private static List cacheFilterType; + [MenuItem("Tools/AlicizaX/UISetting Window")] private static void OpenWindow() { - var w = GetWindow("UI Setting"); - w.minSize = new Vector2(760, 520); - w.Show(); + var window = GetWindow("UI Setting"); + window.minSize = new Vector2(760, 520); + window.Show(); } + private readonly string[] toolbarTitles = { "General", "Script Generation", "Element Mapping" }; 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 SerializedObject serializedConfig; + private SerializedProperty commonDataProperty; + private SerializedProperty regexConfigsProperty; + private SerializedProperty scriptGenerateConfigsProperty; + private SerializedProperty generatorRuleHelperProperty; + private SerializedProperty excludeKeywordsProperty; private ReorderableList regexList; private ReorderableList projectList; private ReorderableList excludeList; - + private Vector2 scroll; + private int toolbarTab; private TextAsset importText; - - private string previewLabel; private string previewCompLabel; - - private List m_ScriptGeneratorHelperTypes = new(); - private int m_ScriptGeneratorHelperSelectIndex; + private List scriptGeneratorHelperTypes = new(); + private int scriptGeneratorHelperSelectIndex; private void OnEnable() { - uiGenerateConfiguration = UIGenerateConfiguration.Instance; + BindConfiguration(); + SetupLists(); + RefreshScriptGeneratorHelperTypes(); + RefreshPreview(); + } + + private void OnDisable() + { + SaveConfig(false); + } + + private void BindConfiguration() + { + uiGenerateConfiguration = UIGenerateConfiguration.LoadOrCreate(); if (uiGenerateConfiguration == null) { - uiGenerateConfiguration = ScriptableObject.CreateInstance(); + uiGenerateConfiguration = CreateInstance(); } - UIGenerateCommonData = uiGenerateConfiguration.UIGenerateCommonData ?? new UIGenerateCommonData(); - UIElementRegexConfigs = uiGenerateConfiguration.UIElementRegexConfigs ?? new List(); - UIScriptGenerateConfigs = uiGenerateConfiguration.UIScriptGenerateConfigs ?? new List(); + uiGenerateConfiguration.UIGenerateCommonData ??= new UIGenerateCommonData(); + uiGenerateConfiguration.UIElementRegexConfigs ??= new List(); + uiGenerateConfiguration.UIScriptGenerateConfigs ??= new List(); + uiGenerateConfiguration.UIGenerateCommonData.ExcludeKeywords ??= Array.Empty(); - excludeKeywordsList = (UIGenerateCommonData.ExcludeKeywords ?? new string[0]).ToList(); - - SetupLists(); - RefreshLabel(); - RefreshScriptGeneratorHelperTypes(); + serializedConfig = new SerializedObject(uiGenerateConfiguration); + commonDataProperty = serializedConfig.FindProperty(nameof(UIGenerateConfiguration.UIGenerateCommonData)); + regexConfigsProperty = serializedConfig.FindProperty(nameof(UIGenerateConfiguration.UIElementRegexConfigs)); + scriptGenerateConfigsProperty = serializedConfig.FindProperty(nameof(UIGenerateConfiguration.UIScriptGenerateConfigs)); + generatorRuleHelperProperty = serializedConfig.FindProperty(nameof(UIGenerateConfiguration.UIScriptGeneratorRuleHelper)); + excludeKeywordsProperty = commonDataProperty?.FindPropertyRelative(nameof(UIGenerateCommonData.ExcludeKeywords)); } private void SetupLists() { + SetupExcludeList(); + SetupRegexList(); + SetupProjectList(); + } - excludeList = new ReorderableList(excludeKeywordsList, typeof(string), true, true, true, true); - excludeList.drawHeaderCallback = (r) => EditorGUI.LabelField(r, "排除关键字(匹配则不生成)"); + private void SetupExcludeList() + { + excludeList = new ReorderableList(serializedConfig, excludeKeywordsProperty, true, true, true, true); + excludeList.drawHeaderCallback = rect => EditorGUI.LabelField(rect, "Exclude Keywords"); 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]); + if (!IsValidArrayIndex(excludeKeywordsProperty, index)) + { + return; + } + + rect.y += 2f; + var itemProperty = excludeKeywordsProperty.GetArrayElementAtIndex(index); + itemProperty.stringValue = EditorGUI.TextField( + new Rect(rect.x, rect.y, rect.width, EditorGUIUtility.singleLineHeight), + itemProperty.stringValue); }; - excludeList.onAddCallback = (r) => excludeKeywordsList.Add(string.Empty); - excludeList.onRemoveCallback = (r) => + excludeList.onAddCallback = _ => { - if (r.index >= 0) excludeKeywordsList.RemoveAt(r.index); + int index = excludeKeywordsProperty.arraySize; + excludeKeywordsProperty.InsertArrayElementAtIndex(index); + excludeKeywordsProperty.GetArrayElementAtIndex(index).stringValue = string.Empty; + ApplyConfigChanges(); }; + excludeList.onRemoveCallback = list => + { + ReorderableList.defaultBehaviours.DoRemoveButton(list); + ApplyConfigChanges(); + }; + } - - regexList = new ReorderableList(UIElementRegexConfigs, typeof(UIEelementRegexData), true, true, true, true); - regexList.drawHeaderCallback = (r) => EditorGUI.LabelField(r, "UI元素映射 (正则 -> 组件)"); - regexList.elementHeightCallback = (i) => EditorGUIUtility.singleLineHeight * 2 + 8; + private void SetupRegexList() + { + regexList = new ReorderableList(serializedConfig, regexConfigsProperty, true, true, true, true); + regexList.drawHeaderCallback = rect => EditorGUI.LabelField(rect, "UI Element Mapping (Prefix -> Component)"); + regexList.elementHeightCallback = _ => EditorGUIUtility.singleLineHeight + 6f; 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)) + if (!IsValidArrayIndex(regexConfigsProperty, index)) { - 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(); - } - }); + return; } - item.componentType = EditorGUI.TextField(new Rect(rect.x + 260, rect.y + lh + 4, rect.width - 260, lh), item.componentType); + var itemProperty = regexConfigsProperty.GetArrayElementAtIndex(index); + var regexProperty = itemProperty.FindPropertyRelative(nameof(UIEelementRegexData.uiElementRegex)); + var componentTypeProperty = itemProperty.FindPropertyRelative(nameof(UIEelementRegexData.componentType)); + + rect.y += 2f; + float lineHeight = EditorGUIUtility.singleLineHeight; + float leftWidth = rect.width * 0.8f; + Rect textRect = new Rect(rect.x, rect.y, leftWidth - 8f, lineHeight); + Rect buttonRect = new Rect(rect.x + leftWidth + 8f, rect.y, Mathf.Min(180f, rect.width - leftWidth - 8f), lineHeight); + + regexProperty.stringValue = EditorGUI.TextField(textRect, regexProperty.stringValue); + + string buttonLabel = string.IsNullOrEmpty(componentTypeProperty.stringValue) + ? "(Select Type)" + : componentTypeProperty.stringValue; + + if (GUI.Button(buttonRect, buttonLabel, EditorStyles.popup)) + { + var options = CollectComponentTypeNamesFallback(); + Rect anchor = new Rect( + buttonRect.x, + buttonRect.y + buttonRect.height, + Mathf.Min(360f, Mathf.Max(buttonRect.width, 200f)), + buttonRect.height); + + SearchablePopup.Show( + anchor, + options, + Mathf.Max(0, options.IndexOf(componentTypeProperty.stringValue)), + selectedIndex => UpdateRegexComponentType(index, options, selectedIndex)); + } }; - regexList.onAddCallback = (r) => UIElementRegexConfigs.Add(new UIEelementRegexData { uiElementRegex = "", componentType = "" }); - regexList.onRemoveCallback = (r) => + regexList.onAddCallback = _ => { - if (r.index >= 0) UIElementRegexConfigs.RemoveAt(r.index); + int index = regexConfigsProperty.arraySize; + regexConfigsProperty.InsertArrayElementAtIndex(index); + var itemProperty = regexConfigsProperty.GetArrayElementAtIndex(index); + itemProperty.FindPropertyRelative(nameof(UIEelementRegexData.uiElementRegex)).stringValue = string.Empty; + itemProperty.FindPropertyRelative(nameof(UIEelementRegexData.componentType)).stringValue = string.Empty; + ApplyConfigChanges(); }; + regexList.onRemoveCallback = list => + { + ReorderableList.defaultBehaviours.DoRemoveButton(list); + ApplyConfigChanges(); + }; + } - - projectList = new ReorderableList(UIScriptGenerateConfigs, typeof(UIScriptGenerateData), true, true, true, true); - projectList.drawHeaderCallback = (r) => EditorGUI.LabelField(r, "UI脚本生成配置(多个项目)"); - projectList.elementHeightCallback = (i) => EditorGUIUtility.singleLineHeight * 5 + 10; + private void SetupProjectList() + { + projectList = new ReorderableList(serializedConfig, scriptGenerateConfigsProperty, true, true, true, true); + projectList.drawHeaderCallback = rect => EditorGUI.LabelField(rect, "UI Script Generation Config"); + projectList.elementHeightCallback = _ => EditorGUIUtility.singleLineHeight * 5f + 10f; 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/Resources/UI", 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 (!IsValidArrayIndex(scriptGenerateConfigsProperty, index)) { - if (p.StartsWith(Application.dataPath)) - value = "Assets" + p.Substring(Application.dataPath.Length); - else - EditorUtility.DisplayDialog("提示", "请选择 Assets 下的路径", "确定"); + return; } - } - return value; - } + var itemProperty = scriptGenerateConfigsProperty.GetArrayElementAtIndex(index); + var projectNameProperty = itemProperty.FindPropertyRelative(nameof(UIScriptGenerateData.ProjectName)); + var namespaceProperty = itemProperty.FindPropertyRelative(nameof(UIScriptGenerateData.NameSpace)); + var generatePathProperty = itemProperty.FindPropertyRelative(nameof(UIScriptGenerateData.GenerateHolderCodePath)); + var prefabRootProperty = itemProperty.FindPropertyRelative(nameof(UIScriptGenerateData.UIPrefabRootPath)); + var loadTypeProperty = itemProperty.FindPropertyRelative(nameof(UIScriptGenerateData.LoadType)); - private void RefreshScriptGeneratorHelperTypes() - { - m_ScriptGeneratorHelperTypes = new List(); + float lineHeight = EditorGUIUtility.singleLineHeight; + float padding = 2f; - m_ScriptGeneratorHelperTypes.AddRange(AlicizaX.Utility.Assembly.GetRuntimeTypeNames(typeof(IUIGeneratorRuleHelper))); + projectNameProperty.stringValue = EditorGUI.TextField( + new Rect(rect.x, rect.y, rect.width, lineHeight), + "Project Name", + projectNameProperty.stringValue); - m_ScriptGeneratorHelperSelectIndex = m_ScriptGeneratorHelperTypes.IndexOf(UIGenerateConfiguration.Instance.UIScriptGeneratorRuleHelper); - if (m_ScriptGeneratorHelperSelectIndex < 0) + namespaceProperty.stringValue = EditorGUI.TextField( + new Rect(rect.x, rect.y + (lineHeight + padding), rect.width, lineHeight), + "Namespace", + namespaceProperty.stringValue); + + DrawFolderField( + "Holder Code Path", + generatePathProperty, + rect.x, + rect.y + 2f * (lineHeight + padding), + rect.width, + lineHeight); + + DrawFolderField( + "Prefab Root Path", + prefabRootProperty, + rect.x, + rect.y + 3f * (lineHeight + padding), + rect.width, + lineHeight); + + loadTypeProperty.enumValueIndex = (int)(EUIResLoadType)EditorGUI.EnumPopup( + new Rect(rect.x, rect.y + 4f * (lineHeight + padding), rect.width, lineHeight), + "Load Type", + (EUIResLoadType)loadTypeProperty.enumValueIndex); + }; + projectList.onAddCallback = _ => { - m_ScriptGeneratorHelperSelectIndex = 0; - } + int index = scriptGenerateConfigsProperty.arraySize; + scriptGenerateConfigsProperty.InsertArrayElementAtIndex(index); + var itemProperty = scriptGenerateConfigsProperty.GetArrayElementAtIndex(index); + itemProperty.FindPropertyRelative(nameof(UIScriptGenerateData.ProjectName)).stringValue = "NewProject"; + itemProperty.FindPropertyRelative(nameof(UIScriptGenerateData.NameSpace)).stringValue = "Game.UI"; + itemProperty.FindPropertyRelative(nameof(UIScriptGenerateData.GenerateHolderCodePath)).stringValue = "Assets/Scripts/UI/Generated"; + itemProperty.FindPropertyRelative(nameof(UIScriptGenerateData.UIPrefabRootPath)).stringValue = "Assets/Resources/UI"; + itemProperty.FindPropertyRelative(nameof(UIScriptGenerateData.LoadType)).enumValueIndex = (int)EUIResLoadType.Resources; + ApplyConfigChanges(); + }; + projectList.onRemoveCallback = list => + { + ReorderableList.defaultBehaviours.DoRemoveButton(list); + ApplyConfigChanges(); + }; } private void OnGUI() { - GUILayout.Space(6); + if (serializedConfig == null) + { + BindConfiguration(); + SetupLists(); + RefreshScriptGeneratorHelperTypes(); + RefreshPreview(); + } + serializedConfig.Update(); + GUILayout.Space(6f); + DrawToolbar(); + + scroll = EditorGUILayout.BeginScrollView(scroll); + GUILayout.Space(8f); + + switch (toolbarTab) + { + case 0: + DrawCommonPane(); + break; + case 1: + DrawScriptPane(); + break; + case 2: + DrawElementPane(); + break; + } + + EditorGUILayout.EndScrollView(); + GUILayout.Space(8f); + + if (serializedConfig.ApplyModifiedProperties()) + { + EditorUtility.SetDirty(uiGenerateConfiguration); + RefreshPreview(); + } + } + + private void DrawToolbar() + { 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) + bool isActive = toolbarTab == i; + bool toggled = GUILayout.Toggle(isActive, toolbarTitles[i], EditorStyles.toolbarButton, GUILayout.Height(22f)); + if (toggled && toolbarTab != i) { toolbarTab = i; Repaint(); @@ -204,195 +308,200 @@ namespace AlicizaX.UI.Editor 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))) + if (GUILayout.Button(new GUIContent(saveIcon.image, "Save configuration"), EditorStyles.toolbarButton, GUILayout.Width(ToolbarButtonWidth))) { - OnEnable(); - Repaint(); + SaveConfig(true); + } + + if (GUILayout.Button(new GUIContent(refreshIcon.image, "Refresh preview"), EditorStyles.toolbarButton, GUILayout.Width(ToolbarButtonWidth))) + { + RefreshPreview(); + } + + if (GUILayout.Button(new GUIContent(reloadIcon.image, "Reload configuration"), EditorStyles.toolbarButton, GUILayout.Width(ToolbarButtonWidth))) + { + ReloadConfiguration(); } 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.LabelField("General Generation Settings", EditorStyles.boldLabel); + EditorGUILayout.Space(4f); - 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("UI辅助生成", m_ScriptGeneratorHelperSelectIndex, m_ScriptGeneratorHelperTypes.ToArray()); - string selectService = m_ScriptGeneratorHelperTypes[m_ScriptGeneratorHelperSelectIndex]; - if (uiGenerateConfiguration.UIScriptGeneratorRuleHelper != selectService) + EditorGUILayout.PropertyField( + commonDataProperty.FindPropertyRelative(nameof(UIGenerateCommonData.ComCheckSplitName)), + new GUIContent("Component Split", "Example: Button#Close")); + EditorGUILayout.PropertyField( + commonDataProperty.FindPropertyRelative(nameof(UIGenerateCommonData.ComCheckEndName)), + new GUIContent("Component End", "Example: @End")); + EditorGUILayout.PropertyField( + commonDataProperty.FindPropertyRelative(nameof(UIGenerateCommonData.ArrayComSplitName)), + new GUIContent("Array Split", "Example: *Item")); + EditorGUILayout.PropertyField( + commonDataProperty.FindPropertyRelative(nameof(UIGenerateCommonData.GeneratePrefix)), + new GUIContent("Generate Prefix")); + + int nextIndex = EditorGUILayout.Popup( + "Generator Rule Helper", + scriptGeneratorHelperSelectIndex, + scriptGeneratorHelperTypes.ToArray()); + if (nextIndex != scriptGeneratorHelperSelectIndex && + nextIndex >= 0 && + nextIndex < scriptGeneratorHelperTypes.Count) { - UIGenerateConfiguration.Instance.UIScriptGeneratorRuleHelper = selectService; - UIGenerateConfiguration.Save(); + scriptGeneratorHelperSelectIndex = nextIndex; + generatorRuleHelperProperty.stringValue = scriptGeneratorHelperTypes[nextIndex]; } - EditorGUILayout.EndVertical(); - - GUILayout.Space(8); - + GUILayout.Space(8f); excludeList.DoLayoutList(); - EditorGUILayout.Space(8); - - EditorGUILayout.LabelField("脚本生成预览", EditorStyles.boldLabel); - EditorGUILayout.HelpBox(previewLabel ?? "", MessageType.None); - EditorGUILayout.LabelField("组件生成预览 (下标0开始)", EditorStyles.boldLabel); - EditorGUILayout.HelpBox(previewCompLabel ?? "", MessageType.None); - + EditorGUILayout.Space(8f); + EditorGUILayout.LabelField("Script Preview", EditorStyles.boldLabel); + EditorGUILayout.HelpBox(previewLabel ?? string.Empty, MessageType.None); + EditorGUILayout.LabelField("Component Preview", EditorStyles.boldLabel); + EditorGUILayout.HelpBox(previewCompLabel ?? string.Empty, MessageType.None); EditorGUILayout.EndVertical(); - GUILayout.Space(8); + + GUILayout.Space(8f); } private void DrawScriptPane() { EditorGUILayout.BeginVertical("box"); - EditorGUILayout.LabelField("UI脚本生成配置(支持多个项目)", EditorStyles.boldLabel); - GUILayout.Space(6); + EditorGUILayout.LabelField("UI Script Generation Config", EditorStyles.boldLabel); + GUILayout.Space(6f); projectList.DoLayoutList(); EditorGUILayout.EndVertical(); - GUILayout.Space(8); + + GUILayout.Space(8f); } private void DrawElementPane() { EditorGUILayout.BeginVertical("box"); - EditorGUILayout.LabelField("UI元素映射(正则 -> 组件)", EditorStyles.boldLabel); - GUILayout.Space(6); + EditorGUILayout.LabelField("UI Element Mapping", EditorStyles.boldLabel); + GUILayout.Space(6f); GUILayout.BeginHorizontal(EditorStyles.toolbar); + if (GUILayout.Button("Load Default", EditorStyles.toolbarButton, GUILayout.Width(90f))) { - 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(); + LoadDefault(); } + + if (GUILayout.Button("Export", EditorStyles.toolbarButton, GUILayout.Width(70f))) + { + ExportConfig(); + } + + GUILayout.Space(8f); + + importText = (TextAsset)EditorGUILayout.ObjectField( + importText, + typeof(TextAsset), + false, + GUILayout.Height(18f), + GUILayout.MinWidth(200f)); + + GUI.enabled = importText != null; + if (GUILayout.Button("Import", EditorStyles.toolbarButton, GUILayout.Width(84f)) && 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(); - } - }); - } - }; - - + GUILayout.Space(6f); regexList.DoLayoutList(); - EditorGUILayout.EndVertical(); } - private static List cacheFilterType; + private void DrawFolderField(string label, SerializedProperty property, float x, float y, float width, float height) + { + if (property == null) + { + return; + } + Rect textRect = new Rect(x, y, width - 76f, height); + Rect buttonRect = new Rect(x + width - 72f, y, 68f, height); + property.stringValue = EditorGUI.TextField(textRect, label, property.stringValue); + + if (!GUI.Button(buttonRect, "Select")) + { + return; + } + + string selectedPath = EditorUtility.OpenFolderPanel("Select Folder", Application.dataPath, string.Empty); + if (string.IsNullOrEmpty(selectedPath)) + { + return; + } + + if (!selectedPath.StartsWith(Application.dataPath, StringComparison.OrdinalIgnoreCase)) + { + EditorUtility.DisplayDialog("Invalid Folder", "Please select a folder under Assets.", "OK"); + return; + } + + Undo.RecordObject(uiGenerateConfiguration, "Change UI Generation Folder"); + property.stringValue = "Assets" + selectedPath.Substring(Application.dataPath.Length); + ApplyConfigChanges(); + } + + private void RefreshScriptGeneratorHelperTypes() + { + scriptGeneratorHelperTypes = AlicizaX.Utility.Assembly + .GetRuntimeTypeNames(typeof(IUIGeneratorRuleHelper)) + .Distinct(StringComparer.Ordinal) + .OrderBy(typeName => typeName, StringComparer.Ordinal) + .ToList(); + + if (scriptGeneratorHelperTypes.Count == 0) + { + scriptGeneratorHelperTypes.Add(typeof(DefaultUIGeneratorRuleHelper).FullName); + } + + string currentType = generatorRuleHelperProperty?.stringValue; + if (!string.IsNullOrEmpty(currentType) && !scriptGeneratorHelperTypes.Contains(currentType)) + { + scriptGeneratorHelperTypes.Insert(0, currentType); + } + + scriptGeneratorHelperSelectIndex = Mathf.Max(0, scriptGeneratorHelperTypes.IndexOf(currentType)); + } 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(); + .Where(type => !string.IsNullOrEmpty(type.FullName) && !type.FullName.Contains("Editor")) + .Where(type => !type.IsAbstract && !type.IsInterface) + .Where(type => !type.IsGenericTypeDefinition) + .Where(type => !type.IsSubclassOf(typeof(UIHolderObjectBase))) + .Where(type => type.IsSubclassOf(typeof(Component))) + .Where(type => !type.FullName.Contains("YooAsset")) + .Where(type => !type.FullName.Contains("Unity.VisualScripting")) + .Where(type => !type.FullName.Contains("Cysharp.Threading")) + .Where(type => !type.FullName.Contains("UnityEngine.Rendering.UI.Debug")) + .Where(type => !type.FullName.Contains("Unity.PerformanceTesting")) + .Where(type => !type.FullName.Contains("UnityEngine.TestTools")) + .Select(type => type.FullName) + .Distinct(StringComparer.Ordinal) + .OrderBy(typeName => typeName, StringComparer.Ordinal) + .ToList(); cacheFilterType.Add(typeof(GameObject).Name); } @@ -400,33 +509,37 @@ namespace AlicizaX.UI.Editor return cacheFilterType; } - private void LoadDefault() + private void UpdateRegexComponentType(int index, List options, int selectedIndex) { - string defaultPath = null; - try + if (!IsValidArrayIndex(regexConfigsProperty, index) || + selectedIndex < 0 || + selectedIndex >= options.Count) { - 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) + Undo.RecordObject(uiGenerateConfiguration, "Change UI Element Component Type"); + serializedConfig.Update(); + regexConfigsProperty + .GetArrayElementAtIndex(index) + .FindPropertyRelative(nameof(UIEelementRegexData.componentType)) + .stringValue = options[selectedIndex]; + ApplyConfigChanges(); + Repaint(); + } + + private void LoadDefault() + { + string defaultPath = UIGlobalPath.DefaultComPath; + if (!File.Exists(defaultPath)) { - UIElementRegexConfigs = list; - regexList.list = UIElementRegexConfigs; - RefreshLabel(); + EditorUtility.DisplayDialog("Load Default", $"Default file not found: {defaultPath}", "OK"); + return; } + + string text = File.ReadAllText(defaultPath); + var list = JsonConvert.DeserializeObject>(text); + ReplaceRegexConfigs(list, "Load Default UI Element Config"); } private void ImportConfig(TextAsset text) @@ -434,73 +547,123 @@ namespace AlicizaX.UI.Editor try { var list = JsonConvert.DeserializeObject>(text.text); - if (list != null) - { - UIElementRegexConfigs = list; - regexList.list = UIElementRegexConfigs; - RefreshLabel(); - } + ReplaceRegexConfigs(list, "Import UI Element Config"); } - catch (Exception ex) + catch (Exception exception) { - Debug.LogException(ex); - EditorUtility.DisplayDialog("错误", "导入失败,请查看控制台", "OK"); + Debug.LogException(exception); + EditorUtility.DisplayDialog("Import Failed", "Import failed. Check the console for details.", "OK"); } } + private void ReplaceRegexConfigs(IReadOnlyList list, string undoName) + { + if (list == null) + { + return; + } + + Undo.RecordObject(uiGenerateConfiguration, undoName); + serializedConfig.Update(); + regexConfigsProperty.arraySize = list.Count; + for (int i = 0; i < list.Count; i++) + { + var itemProperty = regexConfigsProperty.GetArrayElementAtIndex(i); + itemProperty.FindPropertyRelative(nameof(UIEelementRegexData.uiElementRegex)).stringValue = list[i]?.uiElementRegex ?? string.Empty; + itemProperty.FindPropertyRelative(nameof(UIEelementRegexData.componentType)).stringValue = list[i]?.componentType ?? string.Empty; + } + + ApplyConfigChanges(); + } + private void ExportConfig() { - string json = JsonConvert.SerializeObject(UIElementRegexConfigs, Formatting.Indented); - string path = EditorUtility.SaveFilePanel("导出 UI 元素配置为 JSON", Application.dataPath, "uielementconfig", "txt"); - if (!string.IsNullOrEmpty(path)) + serializedConfig.ApplyModifiedProperties(); + + string json = JsonConvert.SerializeObject(uiGenerateConfiguration.UIElementRegexConfigs, Formatting.Indented); + string path = EditorUtility.SaveFilePanel("Export UI Element Config", Application.dataPath, "uielementconfig", "txt"); + if (string.IsNullOrEmpty(path)) { - File.WriteAllText(path, json); - AssetDatabase.Refresh(); - EditorUtility.DisplayDialog("导出完成", "已导出", "OK"); + return; } + + File.WriteAllText(path, json); + AssetDatabase.Refresh(); + EditorUtility.DisplayDialog("Export Complete", "Config exported.", "OK"); } - private void RefreshLabel() + private void ReloadConfiguration() { - previewLabel = $"{UIGenerateCommonData.GeneratePrefix}_UITestWindow"; - previewCompLabel = $"{UIGenerateCommonData.ArrayComSplitName}Text{UIGenerateCommonData.ComCheckSplitName}Img{UIGenerateCommonData.ComCheckEndName}Test{UIGenerateCommonData.ArrayComSplitName}0"; + BindConfiguration(); + SetupLists(); + RefreshScriptGeneratorHelperTypes(); + RefreshPreview(); Repaint(); } - private void SaveConfig() + private void RefreshPreview() { - UIGenerateCommonData.ExcludeKeywords = excludeKeywordsList.ToArray(); + string prefix = commonDataProperty?.FindPropertyRelative(nameof(UIGenerateCommonData.GeneratePrefix))?.stringValue ?? "ui"; + string arraySplit = commonDataProperty?.FindPropertyRelative(nameof(UIGenerateCommonData.ArrayComSplitName))?.stringValue ?? "*"; + string componentSplit = commonDataProperty?.FindPropertyRelative(nameof(UIGenerateCommonData.ComCheckSplitName))?.stringValue ?? "#"; + string componentEnd = commonDataProperty?.FindPropertyRelative(nameof(UIGenerateCommonData.ComCheckEndName))?.stringValue ?? "@"; - uiGenerateConfiguration.UIGenerateCommonData = UIGenerateCommonData; - uiGenerateConfiguration.UIElementRegexConfigs = UIElementRegexConfigs; - uiGenerateConfiguration.UIScriptGenerateConfigs = UIScriptGenerateConfigs; - UIGenerateConfiguration.Save(); - Debug.Log("UIGenerateConfiguration Saved..."); + previewLabel = $"{prefix}_UITestWindow"; + previewCompLabel = $"{arraySplit}Text{componentSplit}Img{componentEnd}Test{arraySplit}0"; + Repaint(); } - private void OnDisable() => SaveConfig(); + private void ApplyConfigChanges() + { + serializedConfig.ApplyModifiedProperties(); + EditorUtility.SetDirty(uiGenerateConfiguration); + RefreshPreview(); + } + + private void SaveConfig(bool logSave) + { + if (serializedConfig == null || uiGenerateConfiguration == null) + { + return; + } + + serializedConfig.ApplyModifiedProperties(); + EditorUtility.SetDirty(uiGenerateConfiguration); + UIGenerateConfiguration.Save(); + + if (logSave) + { + Debug.Log("UIGenerateConfiguration saved."); + } + } + + private static bool IsValidArrayIndex(SerializedProperty property, int index) + { + return property != null && index >= 0 && index < property.arraySize; + } } 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 const float RowHeight = 20f; private static GUIStyle searchFieldStyle; private static GUIStyle cancelStyle; private static GUIStyle rowStyle; private static GUIStyle selectedRowStyle; - private const float ROW_HEIGHT = 20f; + + private readonly List allItems; + private readonly Action onSelect; + private List filtered; + private int currentIndex; + private string search = string.Empty; + private Vector2 scroll; private SearchablePopup(List items, int currentIndex, Action onSelect) { - this.allItems = items ?? new List(); - this.filtered = new List(this.allItems); - this.currentIndex = Mathf.Clamp(currentIndex, -1, this.allItems.Count - 1); + allItems = items ?? new List(); + filtered = new List(allItems); + this.currentIndex = Mathf.Clamp(currentIndex, -1, allItems.Count - 1); this.onSelect = onSelect; } @@ -509,7 +672,10 @@ namespace AlicizaX.UI.Editor PopupWindow.Show(anchorRect, new SearchablePopup(items, currentIndex, onSelect)); } - public override Vector2 GetWindowSize() => new Vector2(360, 320); + public override Vector2 GetWindowSize() + { + return new Vector2(360f, 320f); + } public override void OnOpen() { @@ -520,35 +686,37 @@ namespace AlicizaX.UI.Editor { InitStyles(); - EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); GUI.SetNextControlName("SearchField"); search = EditorGUILayout.TextField(search, searchFieldStyle, GUILayout.ExpandWidth(true)); - if (GUILayout.Button("", cancelStyle, GUILayout.Width(18))) + if (GUILayout.Button(string.Empty, cancelStyle, GUILayout.Width(18f))) { - search = ""; + search = string.Empty; GUI.FocusControl("SearchField"); } EditorGUILayout.EndHorizontal(); - FilterList(search); - - HandleKeyboard(); - scroll = EditorGUILayout.BeginScrollView(scroll); for (int i = 0; i < filtered.Count; i++) { - bool selected = (i == currentIndex); + 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); + Rect rowRect = GUILayoutUtility.GetRect( + new GUIContent(filtered[i]), + style, + GUILayout.Height(RowHeight), + GUILayout.ExpandWidth(true)); - if (Event.current.type == EventType.MouseDown && r.Contains(Event.current.mousePosition)) + if (Event.current.type == EventType.Repaint) + { + style.Draw(rowRect, filtered[i], false, false, selected, false); + } + + if (Event.current.type == EventType.MouseDown && rowRect.Contains(Event.current.mousePosition)) { Select(filtered[i]); Event.current.Use(); @@ -560,48 +728,62 @@ namespace AlicizaX.UI.Editor private void HandleKeyboard() { - var e = Event.current; - if (e.type != EventType.KeyDown) return; + Event currentEvent = Event.current; + if (currentEvent.type != EventType.KeyDown) + { + return; + } - if (e.keyCode == KeyCode.DownArrow) + if (currentEvent.keyCode == KeyCode.DownArrow) { currentIndex = Mathf.Min(currentIndex + 1, filtered.Count - 1); - e.Use(); + currentEvent.Use(); editorWindow.Repaint(); + return; } - else if (e.keyCode == KeyCode.UpArrow) + + if (currentEvent.keyCode == KeyCode.UpArrow) { currentIndex = Mathf.Max(currentIndex - 1, 0); - e.Use(); + currentEvent.Use(); editorWindow.Repaint(); + return; } - else if (e.keyCode == KeyCode.Return || e.keyCode == KeyCode.KeypadEnter) + + if ((currentEvent.keyCode == KeyCode.Return || currentEvent.keyCode == KeyCode.KeypadEnter) && + filtered.Count > 0 && + currentIndex >= 0 && + currentIndex < filtered.Count) { - if (filtered.Count > 0 && currentIndex >= 0 && currentIndex < filtered.Count) - Select(filtered[currentIndex]); - e.Use(); + Select(filtered[currentIndex]); + currentEvent.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(); + string lowerKeyword = keyword.ToLowerInvariant(); + filtered = allItems + .Where(item => item != null && item.ToLowerInvariant().Contains(lowerKeyword)) + .ToList(); } - if (filtered.Count == 0) currentIndex = -1; - else currentIndex = Mathf.Clamp(currentIndex, 0, filtered.Count - 1); + currentIndex = filtered.Count == 0 ? -1 : 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(); @@ -610,18 +792,28 @@ namespace AlicizaX.UI.Editor 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) }; + 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 } }; + selectedRowStyle = new GUIStyle(rowStyle); + selectedRowStyle.normal.background = Texture2D.grayTexture; } } } diff --git a/Editor/UI/Helper/IUIGeneratorRuleHelper.cs b/Editor/UI/Helper/IUIGeneratorRuleHelper.cs index 81e916c..a7ec723 100644 --- a/Editor/UI/Helper/IUIGeneratorRuleHelper.cs +++ b/Editor/UI/Helper/IUIGeneratorRuleHelper.cs @@ -1,15 +1,26 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Text; using AlicizaX.UI.Runtime; -using UnityEditor; -using UnityEditor.SceneManagement; using UnityEngine; namespace AlicizaX.UI.Editor { + internal interface IContextualUIGeneratorRuleHelper + { + string GetClassGenerateName(UIGenerationContext context); + + string GetUIResourceSavePath(UIGenerationContext context); + + bool CheckCanGenerate(UIGenerationContext context); + + string GetReferenceNamespace(UIGenerationContext context); + + string GetVariableContent(UIGenerationContext context); + + void WriteUIScriptContent(UIGenerationContext context, string scriptContent); + } + public interface IUIGeneratorRuleHelper { string GetPrivateComponentByNameRule(string regexName, string componetName, EBindType bindType); @@ -30,207 +41,74 @@ namespace AlicizaX.UI.Editor } - public class DefaultUIGeneratorRuleHelper : IUIGeneratorRuleHelper + public class DefaultUIGeneratorRuleHelper : IUIGeneratorRuleHelper, IContextualUIGeneratorRuleHelper { - public string GetPrivateComponentByNameRule(string regexName, string componentName, EBindType bindType) - { - var endPrefix = bindType == EBindType.ListCom ? "List" : string.Empty; - var common = UIGenerateConfiguration.Instance.UIGenerateCommonData; - var endNameIndex = componentName.IndexOf(common.ComCheckEndName, StringComparison.Ordinal); + private readonly IUIIdentifierFormatter _identifierFormatter; + private readonly IUIResourcePathResolver _resourcePathResolver; + private readonly IUIScriptCodeEmitter _scriptCodeEmitter; + private readonly IUIScriptFileWriter _scriptFileWriter; - var componentSuffix = endNameIndex >= 0 ? componentName.Substring(endNameIndex + 1) : componentName; - return $"m{regexName}{componentSuffix}{endPrefix}"; + public DefaultUIGeneratorRuleHelper() + : this( + new DefaultUIIdentifierFormatter(), + new DefaultUIResourcePathResolver(), + new DefaultUIScriptCodeEmitter(), + new DefaultUIScriptFileWriter()) + { } + internal DefaultUIGeneratorRuleHelper( + IUIIdentifierFormatter identifierFormatter, + IUIResourcePathResolver resourcePathResolver, + IUIScriptCodeEmitter scriptCodeEmitter, + IUIScriptFileWriter scriptFileWriter) + { + _identifierFormatter = identifierFormatter ?? throw new ArgumentNullException(nameof(identifierFormatter)); + _resourcePathResolver = resourcePathResolver ?? throw new ArgumentNullException(nameof(resourcePathResolver)); + _scriptCodeEmitter = scriptCodeEmitter ?? throw new ArgumentNullException(nameof(scriptCodeEmitter)); + _scriptFileWriter = scriptFileWriter ?? throw new ArgumentNullException(nameof(scriptFileWriter)); + } + + public string GetPrivateComponentByNameRule(string regexName, string componentName, EBindType bindType) + => _identifierFormatter.GetPrivateComponentName(regexName, componentName, bindType); + public string GetPublicComponentByNameRule(string variableName) - { - if (string.IsNullOrEmpty(variableName)) return variableName; - return variableName.Length > 1 ? variableName.Substring(1) : variableName; - } + => _identifierFormatter.GetPublicComponentName(variableName); public string GetClassGenerateName(GameObject targetObject, UIScriptGenerateData scriptGenerateData) - { - var config = UIGenerateConfiguration.Instance.UIGenerateCommonData; - var prefix = config.GeneratePrefix ?? "ui"; - return $"{prefix}_{targetObject.name}"; - } + => _identifierFormatter.GetClassName(targetObject); + + string IContextualUIGeneratorRuleHelper.GetClassGenerateName(UIGenerationContext context) + => _identifierFormatter.GetClassName(context.TargetObject); public string GetUIResourceSavePath(GameObject targetObject, UIScriptGenerateData scriptGenerateData) - { - if (targetObject == null) return $"\"{nameof(targetObject)}\""; + => _resourcePathResolver.GetResourcePath(targetObject, scriptGenerateData); - var defaultPath = targetObject.name; - var assetPath = UIGenerateQuick.GetPrefabAssetPath(targetObject); - - if (string.IsNullOrEmpty(assetPath) || !assetPath.StartsWith("Assets/", StringComparison.Ordinal)) - return defaultPath; - - assetPath = assetPath.Replace('\\', '/'); - return scriptGenerateData.LoadType switch - { - EUIResLoadType.Resources => GetResourcesPath(assetPath, scriptGenerateData, defaultPath), - EUIResLoadType.AssetBundle => GetAssetBundlePath(assetPath, scriptGenerateData, defaultPath), - _ => defaultPath - }; - } - - private static string GetResourcesPath(string assetPath, UIScriptGenerateData scriptGenerateData, string defaultPath) - { - var resourcesRoot = scriptGenerateData.UIPrefabRootPath; - var relPath = GetResourcesRelativePath(assetPath, resourcesRoot); - - if (relPath == null) - { - Debug.LogWarning($"[UI生成] 资源 {assetPath} 不在配置的 Resources 根目录下: {resourcesRoot}"); - return defaultPath; - } - - return relPath; - } - - private static string GetAssetBundlePath(string assetPath, UIScriptGenerateData scriptGenerateData, string defaultPath) - { - try - { - var defaultPackage = YooAsset.Editor.AssetBundleCollectorSettingData.Setting.GetPackage("DefaultPackage"); - if (defaultPackage?.EnableAddressable == true) - return defaultPath; - } - catch - { - // 忽略异常,继续处理 - } - - var bundleRoot = scriptGenerateData.UIPrefabRootPath; - if (!assetPath.StartsWith(bundleRoot, StringComparison.OrdinalIgnoreCase)) - { - Debug.LogWarning($"[UI生成] 资源 {assetPath} 不在配置的 AssetBundle 根目录下: {bundleRoot}"); - return defaultPath; - } - - return Path.ChangeExtension(assetPath, null); - } - - private static string GetResourcesRelativePath(string assetPath, string resourcesRoot) - { - if (string.IsNullOrEmpty(assetPath) || string.IsNullOrEmpty(resourcesRoot)) return null; - - assetPath = assetPath.Replace('\\', '/'); - resourcesRoot = resourcesRoot.Replace('\\', '/'); - - if (!assetPath.StartsWith(resourcesRoot, StringComparison.OrdinalIgnoreCase)) - return null; - - var relPath = assetPath.Substring(resourcesRoot.Length).TrimStart('/'); - return Path.ChangeExtension(relPath, null); - } + string IContextualUIGeneratorRuleHelper.GetUIResourceSavePath(UIGenerationContext context) + => _resourcePathResolver.GetResourcePath(context.TargetObject, context.ScriptGenerateData); public void WriteUIScriptContent(GameObject targetObject, string className, string scriptContent, UIScriptGenerateData scriptGenerateData) - { - if (string.IsNullOrEmpty(className)) throw new ArgumentNullException(nameof(className)); - if (scriptContent == null) throw new ArgumentNullException(nameof(scriptContent)); - if (scriptGenerateData == null) throw new ArgumentNullException(nameof(scriptGenerateData)); + => _scriptFileWriter.Write(targetObject, className, scriptContent, scriptGenerateData); - var scriptFolderPath = scriptGenerateData.GenerateHolderCodePath; - var scriptFilePath = Path.Combine(scriptFolderPath, $"{className}.cs"); - - Directory.CreateDirectory(scriptFolderPath); - - scriptContent = scriptContent.Replace("#Controller#", string.Empty); - - if (File.Exists(scriptFilePath) && IsContentUnchanged(scriptFilePath, scriptContent)) - { - UIScriptGeneratorHelper.BindUIScript(); - return; - } - - File.WriteAllText(scriptFilePath, scriptContent, Encoding.UTF8); - AssetDatabase.Refresh(); - } - - private static bool IsContentUnchanged(string filePath, string newContent) - { - var oldText = File.ReadAllText(filePath, Encoding.UTF8); - return oldText.Equals(newContent, StringComparison.Ordinal); - } + void IContextualUIGeneratorRuleHelper.WriteUIScriptContent(UIGenerationContext context, string scriptContent) + => _scriptFileWriter.Write(context.TargetObject, context.ClassName, scriptContent, context.ScriptGenerateData); public bool CheckCanGenerate(GameObject targetObject, UIScriptGenerateData scriptGenerateData) - { - if (targetObject == null || scriptGenerateData == null) return false; + => _resourcePathResolver.CanGenerate(targetObject, scriptGenerateData); - var assetPath = UIGenerateQuick.GetPrefabAssetPath(targetObject); - if (string.IsNullOrEmpty(assetPath) || !assetPath.StartsWith("Assets/", StringComparison.Ordinal)) - return false; - - assetPath = assetPath.Replace('\\', '/'); - var isValidPath = assetPath.StartsWith(scriptGenerateData.UIPrefabRootPath, StringComparison.OrdinalIgnoreCase); - - if (!isValidPath) - { - Debug.LogWarning($"UI存储位置与配置生成规则不符合 请检查对应配置的UIPrefabRootPath\n[AssetPath]{assetPath}\n[ConfigPath]{scriptGenerateData.UIPrefabRootPath}"); - } - - return isValidPath; - } + bool IContextualUIGeneratorRuleHelper.CheckCanGenerate(UIGenerationContext context) + => _resourcePathResolver.CanGenerate(context.TargetObject, context.ScriptGenerateData); public string GetReferenceNamespace(List uiBindDatas) - { - var namespaceSet = new HashSet(StringComparer.Ordinal) { "UnityEngine" }; + => _scriptCodeEmitter.GetReferenceNamespaces(uiBindDatas); - if (uiBindDatas?.Any(d => d.BindType == EBindType.ListCom) == true) - { - namespaceSet.Add("System.Collections.Generic"); - } - - uiBindDatas? - .Where(bindData => bindData?.Objs?.FirstOrDefault() != null) - .Select(bindData => bindData.GetFirstOrDefaultType().Namespace) - .Where(ns => !string.IsNullOrEmpty(ns)) - .ToList() - .ForEach(ns => namespaceSet.Add(ns)); - - return string.Join(Environment.NewLine, namespaceSet.Select(ns => $"using {ns};")); - } + string IContextualUIGeneratorRuleHelper.GetReferenceNamespace(UIGenerationContext context) + => _scriptCodeEmitter.GetReferenceNamespaces(context.BindDatas?.ToList()); public string GetVariableContent(List uiBindDatas) - { - if (uiBindDatas == null || uiBindDatas.Count == 0) return string.Empty; + => _scriptCodeEmitter.GetVariableContent(uiBindDatas, GetPublicComponentByNameRule); - var variableBuilder = new StringBuilder(); - var variables = uiBindDatas - .Where(b => b != null && !string.IsNullOrEmpty(b.Name)) - .Select(b => GenerateVariableDeclaration(b)) - .Where(declaration => !string.IsNullOrEmpty(declaration)); - - return string.Join("\n\n", variables); - } - - private string GenerateVariableDeclaration(UIBindData bindData) - { - var variableName = bindData.Name; - var publicName = GetPublicComponentByNameRule(variableName); - var firstType = bindData.GetFirstOrDefaultType(); - var typeName = firstType?.Name ?? "Component"; - - var declaration = new StringBuilder(); - declaration.AppendLine("\t\t[SerializeField]"); - - switch (bindData.BindType) - { - case EBindType.None: - case EBindType.Widget: - declaration.AppendLine($"\t\tprivate {typeName} {variableName};"); - declaration.Append($"\t\tpublic {typeName} {publicName} => {variableName};"); - break; - - case EBindType.ListCom: - var count = Math.Max(0, bindData.Objs?.Count ?? 0); - declaration.AppendLine($"\t\tprivate {typeName}[] {variableName} = new {typeName}[{count}];"); - declaration.Append($"\t\tpublic {typeName}[] {publicName} => {variableName};"); - break; - } - - return declaration.ToString(); - } + string IContextualUIGeneratorRuleHelper.GetVariableContent(UIGenerationContext context) + => _scriptCodeEmitter.GetVariableContent(context.BindDatas?.ToList(), GetPublicComponentByNameRule); } } diff --git a/Editor/UI/Helper/UIGenerationContext.cs b/Editor/UI/Helper/UIGenerationContext.cs new file mode 100644 index 0000000..5bc3924 --- /dev/null +++ b/Editor/UI/Helper/UIGenerationContext.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace AlicizaX.UI.Editor +{ + internal sealed class UIGenerationContext + { + public UIGenerationContext(GameObject targetObject, UIScriptGenerateData scriptGenerateData, IReadOnlyList bindDatas) + { + TargetObject = targetObject; + ScriptGenerateData = scriptGenerateData; + BindDatas = bindDatas; + } + + public GameObject TargetObject { get; } + + public UIScriptGenerateData ScriptGenerateData { get; } + + public IReadOnlyList BindDatas { get; } + + public string AssetPath { get; set; } + + public string ClassName { get; set; } + + public string FullTypeName => + string.IsNullOrWhiteSpace(ScriptGenerateData?.NameSpace) ? ClassName : $"{ScriptGenerateData.NameSpace}.{ClassName}"; + } + + internal readonly struct UIGenerationValidationResult + { + private UIGenerationValidationResult(bool isValid, string message) + { + IsValid = isValid; + Message = message; + } + + public bool IsValid { get; } + + public string Message { get; } + + public static UIGenerationValidationResult Success() => new(true, string.Empty); + + public static UIGenerationValidationResult Fail(string message) => new(false, message); + } +} diff --git a/Editor/UI/Helper/UIGenerationContext.cs.meta b/Editor/UI/Helper/UIGenerationContext.cs.meta new file mode 100644 index 0000000..960655f --- /dev/null +++ b/Editor/UI/Helper/UIGenerationContext.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f8b0f70017de25a4b950d96af16e81ca +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/UI/Helper/UIGeneratorRuleServices.cs b/Editor/UI/Helper/UIGeneratorRuleServices.cs new file mode 100644 index 0000000..d1a4c27 --- /dev/null +++ b/Editor/UI/Helper/UIGeneratorRuleServices.cs @@ -0,0 +1,338 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using AlicizaX.UI.Runtime; +using UnityEditor; +using UnityEngine; + +namespace AlicizaX.UI.Editor +{ + internal interface IUIIdentifierFormatter + { + string GetPrivateComponentName(string regexName, string componentName, EBindType bindType); + + string GetPublicComponentName(string variableName); + + string GetClassName(GameObject targetObject); + } + + internal interface IUIResourcePathResolver + { + string GetResourcePath(GameObject targetObject, UIScriptGenerateData scriptGenerateData); + + bool CanGenerate(GameObject targetObject, UIScriptGenerateData scriptGenerateData); + } + + internal interface IUIScriptCodeEmitter + { + string GetReferenceNamespaces(List uiBindDatas); + + string GetVariableContent(List uiBindDatas, Func publicNameFactory); + } + + internal interface IUIScriptFileWriter + { + void Write(GameObject targetObject, string className, string scriptContent, UIScriptGenerateData scriptGenerateData); + } + + internal sealed class DefaultUIIdentifierFormatter : IUIIdentifierFormatter + { + private static readonly HashSet CSharpKeywords = new HashSet(StringComparer.Ordinal) + { + "abstract", "as", "base", "bool", "break", "byte", "case", "catch", "char", "checked", + "class", "const", "continue", "decimal", "default", "delegate", "do", "double", "else", + "enum", "event", "explicit", "extern", "false", "finally", "fixed", "float", "for", + "foreach", "goto", "if", "implicit", "in", "int", "interface", "internal", "is", "lock", + "long", "namespace", "new", "null", "object", "operator", "out", "override", "params", + "private", "protected", "public", "readonly", "ref", "return", "sbyte", "sealed", "short", + "sizeof", "stackalloc", "static", "string", "struct", "switch", "this", "throw", "true", + "try", "typeof", "uint", "ulong", "unchecked", "unsafe", "ushort", "using", "virtual", + "void", "volatile", "while" + }; + + public string GetPrivateComponentName(string regexName, string componentName, EBindType bindType) + { + var endPrefix = bindType == EBindType.ListCom ? "List" : string.Empty; + var common = UIGenerateConfiguration.Instance.UIGenerateCommonData; + var endNameIndex = componentName.IndexOf(common.ComCheckEndName, StringComparison.Ordinal); + var componentSuffix = endNameIndex >= 0 ? componentName.Substring(endNameIndex + 1) : componentName; + var fieldName = $"m{NormalizeIdentifier(regexName)}{NormalizeIdentifier(componentSuffix)}{endPrefix}"; + return MakeSafeIdentifier(string.IsNullOrWhiteSpace(fieldName) ? "mComponent" : fieldName); + } + + public string GetPublicComponentName(string variableName) + { + if (string.IsNullOrEmpty(variableName)) + { + return variableName; + } + + var publicName = variableName.StartsWith("m", StringComparison.Ordinal) && variableName.Length > 1 + ? variableName.Substring(1) + : variableName; + + publicName = NormalizeIdentifier(publicName); + return MakeSafeIdentifier(string.IsNullOrEmpty(publicName) ? "Component" : publicName); + } + + public string GetClassName(GameObject targetObject) + { + var config = UIGenerateConfiguration.Instance.UIGenerateCommonData; + var prefix = NormalizeIdentifier(config.GeneratePrefix ?? "ui"); + var objectName = NormalizeIdentifier(targetObject.name); + var className = $"{prefix}_{objectName}".Trim('_'); + return MakeSafeIdentifier(string.IsNullOrEmpty(className) ? "ui_View" : className); + } + + private static string NormalizeIdentifier(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return string.Empty; + } + + var parts = Regex.Split(value, "[^A-Za-z0-9_]+") + .Where(part => !string.IsNullOrWhiteSpace(part)) + .ToArray(); + + if (parts.Length == 0) + { + return string.Empty; + } + + var builder = new StringBuilder(); + foreach (var part in parts) + { + builder.Append(part[0]); + if (part.Length > 1) + { + builder.Append(part.Substring(1)); + } + } + + return builder.ToString(); + } + + private static string MakeSafeIdentifier(string identifier) + { + if (string.IsNullOrWhiteSpace(identifier)) + { + return "_"; + } + + var safeIdentifier = identifier; + if (char.IsDigit(safeIdentifier[0])) + { + safeIdentifier = "_" + safeIdentifier; + } + + if (CSharpKeywords.Contains(safeIdentifier)) + { + safeIdentifier += "_"; + } + + return safeIdentifier; + } + } + + internal sealed class DefaultUIResourcePathResolver : IUIResourcePathResolver + { + public string GetResourcePath(GameObject targetObject, UIScriptGenerateData scriptGenerateData) + { + if (targetObject == null) + { + return $"\"{nameof(targetObject)}\""; + } + + var defaultPath = targetObject.name; + var assetPath = UIGenerateQuick.GetPrefabAssetPath(targetObject); + if (string.IsNullOrEmpty(assetPath) || !assetPath.StartsWith("Assets/", StringComparison.Ordinal)) + { + return defaultPath; + } + + assetPath = assetPath.Replace('\\', '/'); + return scriptGenerateData.LoadType switch + { + EUIResLoadType.Resources => GetResourcesPath(assetPath, scriptGenerateData, defaultPath), + EUIResLoadType.AssetBundle => GetAssetBundlePath(assetPath, scriptGenerateData, defaultPath), + _ => defaultPath + }; + } + + public bool CanGenerate(GameObject targetObject, UIScriptGenerateData scriptGenerateData) + { + if (targetObject == null || scriptGenerateData == null) + { + return false; + } + + var assetPath = UIGenerateQuick.GetPrefabAssetPath(targetObject); + if (string.IsNullOrEmpty(assetPath) || !assetPath.StartsWith("Assets/", StringComparison.Ordinal)) + { + return false; + } + + assetPath = assetPath.Replace('\\', '/'); + var isValidPath = assetPath.StartsWith(scriptGenerateData.UIPrefabRootPath, StringComparison.OrdinalIgnoreCase); + if (!isValidPath) + { + Debug.LogWarning( + $"UI asset path does not match UIGenerateConfiguration.UIPrefabRootPath.\n[AssetPath]{assetPath}\n[ConfigPath]{scriptGenerateData.UIPrefabRootPath}"); + } + + return isValidPath; + } + + private static string GetResourcesPath(string assetPath, UIScriptGenerateData scriptGenerateData, string defaultPath) + { + var resourcesRoot = scriptGenerateData.UIPrefabRootPath; + var relPath = GetResourcesRelativePath(assetPath, resourcesRoot); + if (relPath == null) + { + Debug.LogWarning($"[UI Generate] Resource {assetPath} is not under configured Resources root: {resourcesRoot}"); + return defaultPath; + } + + return relPath; + } + + private static string GetAssetBundlePath(string assetPath, UIScriptGenerateData scriptGenerateData, string defaultPath) + { + try + { + var defaultPackage = YooAsset.Editor.AssetBundleCollectorSettingData.Setting.GetPackage("DefaultPackage"); + if (defaultPackage?.EnableAddressable == true) + { + return defaultPath; + } + } + catch + { + } + + var bundleRoot = scriptGenerateData.UIPrefabRootPath; + if (!assetPath.StartsWith(bundleRoot, StringComparison.OrdinalIgnoreCase)) + { + Debug.LogWarning($"[UI Generate] Resource {assetPath} is not under configured AssetBundle root: {bundleRoot}"); + return defaultPath; + } + + return Path.ChangeExtension(assetPath, null); + } + + private static string GetResourcesRelativePath(string assetPath, string resourcesRoot) + { + if (string.IsNullOrEmpty(assetPath) || string.IsNullOrEmpty(resourcesRoot)) + { + return null; + } + + assetPath = assetPath.Replace('\\', '/'); + resourcesRoot = resourcesRoot.Replace('\\', '/'); + + if (!assetPath.StartsWith(resourcesRoot, StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + var relPath = assetPath.Substring(resourcesRoot.Length).TrimStart('/'); + return Path.ChangeExtension(relPath, null); + } + } + + internal sealed class DefaultUIScriptCodeEmitter : IUIScriptCodeEmitter + { + public string GetReferenceNamespaces(List uiBindDatas) + { + var namespaceSet = new HashSet(StringComparer.Ordinal) { "UnityEngine" }; + + uiBindDatas? + .Where(bindData => bindData?.GetFirstOrDefaultType() != null) + .Select(bindData => bindData.GetFirstOrDefaultType()?.Namespace) + .Where(ns => !string.IsNullOrEmpty(ns)) + .ToList() + .ForEach(ns => namespaceSet.Add(ns)); + + return string.Join(Environment.NewLine, namespaceSet.OrderBy(ns => ns, StringComparer.Ordinal).Select(ns => $"using {ns};")); + } + + public string GetVariableContent(List uiBindDatas, Func publicNameFactory) + { + if (uiBindDatas == null || uiBindDatas.Count == 0) + { + return string.Empty; + } + + var declarations = uiBindDatas + .Where(bindData => bindData != null && !string.IsNullOrEmpty(bindData.Name)) + .OrderBy(bindData => bindData.Name, StringComparer.Ordinal) + .Select(bindData => GenerateVariableDeclaration(bindData, publicNameFactory)) + .Where(content => !string.IsNullOrEmpty(content)); + + return string.Join(Environment.NewLine + Environment.NewLine, declarations); + } + + private static string GenerateVariableDeclaration(UIBindData bindData, Func publicNameFactory) + { + var variableName = bindData.Name; + var publicName = publicNameFactory(variableName); + var firstType = bindData.GetFirstOrDefaultType(); + var typeName = firstType?.Name ?? "Component"; + + var declaration = new StringBuilder(); + declaration.AppendLine("\t\t[SerializeField]"); + + switch (bindData.BindType) + { + case EBindType.None: + case EBindType.Widget: + declaration.AppendLine($"\t\tprivate {typeName} {variableName};"); + declaration.Append($"\t\tpublic {typeName} {publicName} => {variableName};"); + break; + + case EBindType.ListCom: + var count = Math.Max(0, bindData.Objs?.Count ?? 0); + declaration.AppendLine($"\t\tprivate {typeName}[] {variableName} = new {typeName}[{count}];"); + declaration.Append($"\t\tpublic {typeName}[] {publicName} => {variableName};"); + break; + } + + return declaration.ToString(); + } + } + + internal sealed class DefaultUIScriptFileWriter : IUIScriptFileWriter + { + public void Write(GameObject targetObject, string className, string scriptContent, UIScriptGenerateData scriptGenerateData) + { + if (string.IsNullOrEmpty(className)) throw new ArgumentNullException(nameof(className)); + if (scriptContent == null) throw new ArgumentNullException(nameof(scriptContent)); + if (scriptGenerateData == null) throw new ArgumentNullException(nameof(scriptGenerateData)); + + var scriptFolderPath = scriptGenerateData.GenerateHolderCodePath; + var scriptFilePath = Path.Combine(scriptFolderPath, $"{className}.cs"); + + Directory.CreateDirectory(scriptFolderPath); + scriptContent = scriptContent.Replace("#Controller#", string.Empty); + + if (File.Exists(scriptFilePath) && IsContentUnchanged(scriptFilePath, scriptContent)) + { + UIScriptGeneratorHelper.BindUIScript(); + return; + } + + File.WriteAllText(scriptFilePath, scriptContent, Encoding.UTF8); + AssetDatabase.Refresh(); + } + + private static bool IsContentUnchanged(string filePath, string newContent) + { + var oldText = File.ReadAllText(filePath, Encoding.UTF8); + return oldText.Equals(newContent, StringComparison.Ordinal); + } + } +} diff --git a/Editor/UI/Helper/UIGeneratorRuleServices.cs.meta b/Editor/UI/Helper/UIGeneratorRuleServices.cs.meta new file mode 100644 index 0000000..4c1ff9c --- /dev/null +++ b/Editor/UI/Helper/UIGeneratorRuleServices.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c37ea53228fd9854a9e7a04e26b4b873 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/UI/Helper/UIScriptGeneratorHelper.cs b/Editor/UI/Helper/UIScriptGeneratorHelper.cs index a6fa65f..6449036 100644 --- a/Editor/UI/Helper/UIScriptGeneratorHelper.cs +++ b/Editor/UI/Helper/UIScriptGeneratorHelper.cs @@ -21,59 +21,59 @@ namespace AlicizaX.UI.Editor [Serializable] public class UIBindData { - public string Name { get; } - - public List Objs { get; set; } - public EBindType BindType { get; } - public bool IsGameObject => nameof(GameObject).Equals(TypeName); - - public string TypeName = string.Empty; - - public Type GetFirstOrDefaultType() - { - if (IsGameObject) - { - return typeof(GameObject); - } - - try - { - return Objs.FirstOrDefault()?.GetComponent(TypeName).GetType(); - } - catch (Exception e) - { - Debug.Log(e); - } - - return null; - } - - public UIBindData(string name, List objs, string typeName = "", EBindType bindType = EBindType.None) + public UIBindData(string name, List objs, Type componentType = null, EBindType bindType = EBindType.None) { Name = name; Objs = objs ?? new List(); BindType = bindType; - TypeName = typeName; + ComponentType = componentType; } - public UIBindData(string name, GameObject obj, string typeName = "", EBindType bindType = EBindType.None) - : this(name, new List { obj }, typeName, bindType) + public UIBindData(string name, GameObject obj, Type componentType = null, EBindType bindType = EBindType.None) + : this(name, new List { obj }, componentType, bindType) { } + + public string Name { get; } + + public List Objs { get; set; } + + public EBindType BindType { get; } + + public Type ComponentType { get; private set; } + + public bool IsGameObject => ComponentType == typeof(GameObject); + + public string TypeName => ComponentType?.FullName ?? string.Empty; + + public Type GetFirstOrDefaultType() => ComponentType; + + public void SetComponentType(Type componentType) + { + ComponentType = componentType; + } } public static class UIScriptGeneratorHelper { + private const string GenerateTypeNameKey = "AlicizaX.UI.Generate.TypeName"; + private const string GenerateInstanceIdKey = "AlicizaX.UI.Generate.InstanceId"; + private const string GenerateAssetPathKey = "AlicizaX.UI.Generate.AssetPath"; private static UIGenerateConfiguration _uiGenerateConfiguration; private static IUIGeneratorRuleHelper _uiGeneratorRuleHelper; private static readonly List _uiBindDatas = new List(); private static readonly HashSet _arrayComponents = new HashSet(StringComparer.Ordinal); + private static readonly Dictionary _componentTypeCache = new Dictionary(StringComparer.Ordinal); private static IUIGeneratorRuleHelper UIGeneratorRuleHelper { get { - if (_uiGeneratorRuleHelper == null || (_uiGeneratorRuleHelper != null && _uiGeneratorRuleHelper.GetType().FullName != UIConfiguration.UIScriptGeneratorRuleHelper)) + var configuredHelperTypeName = string.IsNullOrWhiteSpace(UIConfiguration.UIScriptGeneratorRuleHelper) + ? typeof(DefaultUIGeneratorRuleHelper).FullName + : UIConfiguration.UIScriptGeneratorRuleHelper; + + if (_uiGeneratorRuleHelper == null || _uiGeneratorRuleHelper.GetType().FullName != configuredHelperTypeName) { InitializeRuleHelper(); } @@ -82,6 +82,9 @@ namespace AlicizaX.UI.Editor } } + private static IContextualUIGeneratorRuleHelper ContextualUIGeneratorRuleHelper => + UIGeneratorRuleHelper as IContextualUIGeneratorRuleHelper; + private static UIGenerateConfiguration UIConfiguration => _uiGenerateConfiguration ??= UIGenerateConfiguration.Instance; @@ -90,8 +93,7 @@ namespace AlicizaX.UI.Editor var ruleHelperTypeName = UIConfiguration.UIScriptGeneratorRuleHelper; if (string.IsNullOrWhiteSpace(ruleHelperTypeName)) { - Debug.LogError("UIScriptGeneratorHelper: UIScriptGeneratorRuleHelper not configured."); - return null; + ruleHelperTypeName = typeof(DefaultUIGeneratorRuleHelper).FullName; } var ruleHelperType = AlicizaX.Utility.Assembly.GetType(ruleHelperTypeName); @@ -110,14 +112,40 @@ namespace AlicizaX.UI.Editor return _uiGeneratorRuleHelper; } - private static string GetUIElementComponentType(string uiName) + private static Type ResolveUIElementComponentType(string uiName) { - if (string.IsNullOrEmpty(uiName)) return string.Empty; + if (string.IsNullOrEmpty(uiName)) + { + return null; + } - return UIConfiguration.UIElementRegexConfigs + if (_componentTypeCache.TryGetValue(uiName, out var componentType)) + { + return componentType; + } + + var componentTypeName = UIConfiguration.UIElementRegexConfigs ?.Where(pair => !string.IsNullOrEmpty(pair?.uiElementRegex)) .FirstOrDefault(pair => uiName.StartsWith(pair.uiElementRegex, StringComparison.Ordinal)) - ?.componentType ?? string.Empty; + ?.componentType; + + if (string.IsNullOrWhiteSpace(componentTypeName)) + { + return null; + } + + componentType = componentTypeName == nameof(GameObject) + ? typeof(GameObject) + : AlicizaX.Utility.Assembly.GetType(componentTypeName); + + if (componentType == null) + { + Debug.LogError($"UIScriptGeneratorHelper: Could not resolve component type '{componentTypeName}' for '{uiName}'."); + return null; + } + + _componentTypeCache[uiName] = componentType; + return componentType; } private static string[] SplitComponentName(string name) @@ -181,21 +209,22 @@ namespace AlicizaX.UI.Editor private static void ProcessArrayComponent(Transform child, Transform root) { var splitCode = UIConfiguration.UIGenerateCommonData.ArrayComSplitName; - var firstIndex = child.name.IndexOf(splitCode, StringComparison.Ordinal); - var lastIndex = child.name.LastIndexOf(splitCode, StringComparison.Ordinal); + if (!TryGetArrayComponentGroupName(child.name, splitCode, out var groupName)) + { + return; + } - if (firstIndex < 0 || lastIndex <= firstIndex) return; - - var text = child.name.Substring(firstIndex + splitCode.Length, lastIndex - (firstIndex + splitCode.Length)); - if (string.IsNullOrEmpty(text) || _arrayComponents.Contains(text)) return; - - _arrayComponents.Add(text); + var groupKey = $"{root.GetInstanceID()}::{groupName}"; + if (!_arrayComponents.Add(groupKey)) + { + return; + } var arrayComponents = root.Cast() - .Where(sibling => sibling.name.Contains(text, StringComparison.Ordinal)) + .Where(sibling => IsArrayGroupMember(sibling.name, splitCode, groupName)) .ToList(); - CollectArrayComponent(arrayComponents, text); + CollectArrayComponent(arrayComponents, groupName); } private static void CollectComponent(Transform node) @@ -207,19 +236,16 @@ namespace AlicizaX.UI.Editor foreach (var com in componentArray.Where(com => !string.IsNullOrEmpty(com))) { - var typeName = GetUIElementComponentType(com); - if (string.IsNullOrEmpty(typeName)) continue; - - - bool isGameObject = typeName.Equals(nameof(GameObject)); - if (!isGameObject) + var componentType = ResolveUIElementComponentType(com); + if (componentType == null) { - var component = node.GetComponent(typeName); - if (component == null) - { - Debug.LogError($"{node.name} does not have component of type {typeName}"); - continue; - } + continue; + } + + if (componentType != typeof(GameObject) && node.GetComponent(componentType) == null) + { + Debug.LogError($"{node.name} does not have component of type {componentType.FullName}"); + continue; } var keyName = UIGeneratorRuleHelper.GetPrivateComponentByNameRule(com, node.name, EBindType.None); @@ -229,7 +255,7 @@ namespace AlicizaX.UI.Editor continue; } - _uiBindDatas.Add(new UIBindData(keyName, node.gameObject, typeName)); + _uiBindDatas.Add(new UIBindData(keyName, node.gameObject, componentType)); } } @@ -259,7 +285,7 @@ namespace AlicizaX.UI.Editor return; } - _uiBindDatas.Add(new UIBindData(keyName, component.gameObject, component.GetType().FullName, EBindType.Widget)); + _uiBindDatas.Add(new UIBindData(keyName, component.gameObject, component.GetType(), EBindType.Widget)); } private static void CollectArrayComponent(List arrayNode, string nodeName) @@ -292,10 +318,10 @@ namespace AlicizaX.UI.Editor private static List CreateTempBindDatas(string[] componentArray, string nodeName) { - return componentArray.Select((com, index) => + return componentArray.Select(com => { var keyName = UIGeneratorRuleHelper.GetPrivateComponentByNameRule(com, nodeName, EBindType.ListCom); - return new UIBindData(keyName, new List(), com, EBindType.ListCom); + return new UIBindData(keyName, new List(), null, EBindType.ListCom); }).ToList(); } @@ -306,13 +332,13 @@ namespace AlicizaX.UI.Editor var com = componentArray[index]; if (string.IsNullOrEmpty(com)) continue; - var typeName = GetUIElementComponentType(com); - if (string.IsNullOrEmpty(typeName)) continue; - tempBindDatas[index].TypeName = typeName; + var componentType = ResolveUIElementComponentType(com); + if (componentType == null) continue; + tempBindDatas[index].SetComponentType(componentType); foreach (var node in orderedNodes) { - var isGameObject = typeName.Equals(nameof(GameObject)); - var component = isGameObject ? null : node.GetComponent(typeName); + var isGameObject = componentType == typeof(GameObject); + var component = isGameObject ? null : node.GetComponent(componentType); if (component != null || isGameObject) { @@ -320,12 +346,38 @@ namespace AlicizaX.UI.Editor } else { - Debug.LogError($"{node.name} does not have component of type {typeName}"); + Debug.LogError($"{node.name} does not have component of type {componentType.FullName}"); } } } } + private static bool TryGetArrayComponentGroupName(string nodeName, string splitCode, out string groupName) + { + groupName = null; + if (string.IsNullOrEmpty(nodeName) || string.IsNullOrEmpty(splitCode)) + { + return false; + } + + var firstIndex = nodeName.IndexOf(splitCode, StringComparison.Ordinal); + var lastIndex = nodeName.LastIndexOf(splitCode, StringComparison.Ordinal); + if (firstIndex < 0 || lastIndex <= firstIndex) + { + return false; + } + + groupName = nodeName.Substring(firstIndex + splitCode.Length, lastIndex - (firstIndex + splitCode.Length)); + return !string.IsNullOrEmpty(groupName); + } + + private static bool IsArrayGroupMember(string nodeName, string splitCode, string groupName) + { + return TryGetArrayComponentGroupName(nodeName, splitCode, out var candidateGroupName) && + candidateGroupName.Equals(groupName, StringComparison.Ordinal) && + ExtractArrayIndex(nodeName, splitCode).HasValue; + } + private static int? ExtractArrayIndex(string nodeName, string splitCode) { if (string.IsNullOrEmpty(nodeName) || string.IsNullOrEmpty(splitCode)) return null; @@ -344,203 +396,322 @@ namespace AlicizaX.UI.Editor if (!PrefabChecker.IsPrefabAsset(targetObject)) { - Debug.LogWarning("请将UI界面保存为对应的目录Prefab 在进行代码生成"); + Debug.LogWarning("Please save the UI as a prefab asset before generating bindings."); return; } var ruleHelper = UIGeneratorRuleHelper; - if (ruleHelper == null || !ruleHelper.CheckCanGenerate(targetObject, scriptGenerateData)) + if (ruleHelper == null) { return; } InitializeGenerationContext(targetObject); + CollectBindData(targetObject.transform); - var className = ruleHelper.GetClassGenerateName(targetObject, scriptGenerateData); - if (string.IsNullOrEmpty(className)) + var generationContext = new UIGenerationContext(targetObject, scriptGenerateData, _uiBindDatas) { - Debug.LogError("Generated className is empty."); + AssetPath = UIGenerateQuick.GetPrefabAssetPath(targetObject), + ClassName = GetClassGenerateName(ruleHelper, targetObject, scriptGenerateData) + }; + + if (!CheckCanGenerate(ruleHelper, generationContext)) + { + CleanupContext(); return; } - CollectBindData(targetObject.transform); - GenerateScript(targetObject, className, scriptGenerateData, ruleHelper); + var validationResult = ValidateGeneration(generationContext); + if (!validationResult.IsValid) + { + Debug.LogError(validationResult.Message); + CleanupContext(); + return; + } + + GenerateScript(generationContext, ruleHelper); } private static void InitializeGenerationContext(GameObject targetObject) { - EditorPrefs.SetInt("InstanceId", targetObject.GetInstanceID()); + EditorPrefs.SetInt(GenerateInstanceIdKey, targetObject.GetInstanceID()); + var assetPath = UIGenerateQuick.GetPrefabAssetPath(targetObject); + if (!string.IsNullOrEmpty(assetPath)) + { + EditorPrefs.SetString(GenerateAssetPathKey, assetPath); + } + _uiBindDatas.Clear(); _arrayComponents.Clear(); } - private static void GenerateScript(GameObject targetObject, string className, UIScriptGenerateData scriptGenerateData, IUIGeneratorRuleHelper ruleHelper) + private static UIGenerationValidationResult ValidateGeneration(UIGenerationContext context) { - var templateText = File.ReadAllText(UIGlobalPath.TemplatePath); - var processedText = ProcessTemplateText(targetObject, templateText, className, scriptGenerateData, ruleHelper); - EditorPrefs.SetString("Generate", className); - ruleHelper.WriteUIScriptContent(targetObject, className, processedText, scriptGenerateData); + if (context.TargetObject == null) + { + return UIGenerationValidationResult.Fail("UI generation target is null."); + } + + if (context.ScriptGenerateData == null) + { + return UIGenerationValidationResult.Fail("UI generation config is null."); + } + + if (string.IsNullOrWhiteSpace(context.ClassName)) + { + return UIGenerationValidationResult.Fail("Generated class name is empty."); + } + + if (!File.Exists(UIGlobalPath.TemplatePath)) + { + return UIGenerationValidationResult.Fail($"UI template file not found: {UIGlobalPath.TemplatePath}"); + } + + return UIGenerationValidationResult.Success(); } - private static string ProcessTemplateText(GameObject targetObject, string templateText, string className, UIScriptGenerateData scriptGenerateData, IUIGeneratorRuleHelper ruleHelper) + private static void GenerateScript(UIGenerationContext context, IUIGeneratorRuleHelper ruleHelper) { + var templateText = File.ReadAllText(UIGlobalPath.TemplatePath); + var processedText = ProcessTemplateText(context, templateText, ruleHelper); + EditorPrefs.SetString(GenerateTypeNameKey, context.FullTypeName); + if (ContextualUIGeneratorRuleHelper != null) + { + ContextualUIGeneratorRuleHelper.WriteUIScriptContent(context, processedText); + return; + } + + ruleHelper.WriteUIScriptContent(context.TargetObject, context.ClassName, processedText, context.ScriptGenerateData); + } + + private static string ProcessTemplateText(UIGenerationContext context, string templateText, IUIGeneratorRuleHelper ruleHelper) + { + var contextualRuleHelper = ContextualUIGeneratorRuleHelper; return templateText - .Replace("#ReferenceNameSpace#", ruleHelper.GetReferenceNamespace(_uiBindDatas)) - .Replace("#ClassNameSpace#", scriptGenerateData.NameSpace) - .Replace("#ClassName#", className) - .Replace("#TagName#", ruleHelper.GetUIResourceSavePath(targetObject, scriptGenerateData)) - .Replace("#LoadType#", scriptGenerateData.LoadType.ToString()) - .Replace("#Variable#", ruleHelper.GetVariableContent(_uiBindDatas)); + .Replace("#ReferenceNameSpace#", contextualRuleHelper != null ? contextualRuleHelper.GetReferenceNamespace(context) : ruleHelper.GetReferenceNamespace(_uiBindDatas)) + .Replace("#ClassNameSpace#", context.ScriptGenerateData.NameSpace) + .Replace("#ClassName#", context.ClassName) + .Replace("#TagName#", contextualRuleHelper != null ? contextualRuleHelper.GetUIResourceSavePath(context) : ruleHelper.GetUIResourceSavePath(context.TargetObject, context.ScriptGenerateData)) + .Replace("#LoadType#", context.ScriptGenerateData.LoadType.ToString()) + .Replace("#Variable#", contextualRuleHelper != null ? contextualRuleHelper.GetVariableContent(context) : ruleHelper.GetVariableContent(_uiBindDatas)); + } + + private static string GetClassGenerateName(IUIGeneratorRuleHelper ruleHelper, GameObject targetObject, UIScriptGenerateData scriptGenerateData) + { + if (ContextualUIGeneratorRuleHelper != null) + { + return ContextualUIGeneratorRuleHelper.GetClassGenerateName(new UIGenerationContext(targetObject, scriptGenerateData, _uiBindDatas)); + } + + return ruleHelper.GetClassGenerateName(targetObject, scriptGenerateData); + } + + private static bool CheckCanGenerate(IUIGeneratorRuleHelper ruleHelper, UIGenerationContext context) + { + if (ContextualUIGeneratorRuleHelper != null) + { + return ContextualUIGeneratorRuleHelper.CheckCanGenerate(context); + } + + return ruleHelper.CheckCanGenerate(context.TargetObject, context.ScriptGenerateData); } [DidReloadScripts] public static void BindUIScript() { - if (!EditorPrefs.HasKey("Generate")) return; + if (!EditorPrefs.HasKey(GenerateTypeNameKey)) return; - var className = EditorPrefs.GetString("Generate"); - var instanceId = EditorPrefs.GetInt("InstanceId", -1); - var targetObject = EditorUtility.InstanceIDToObject(instanceId) as GameObject; + var scriptTypeName = EditorPrefs.GetString(GenerateTypeNameKey); + var targetObject = ResolveGenerationTarget(); if (targetObject == null) { - Debug.LogWarning("UI script generation attachment object missing!"); + Debug.LogWarning("UI script generation attachment object missing."); + CleanupContext(); return; } _uiBindDatas.Clear(); _arrayComponents.Clear(); + var bindSucceeded = false; CollectBindData(targetObject.transform); try { - BindScriptPropertyField(targetObject, className); + bindSucceeded = BindScriptPropertyField(targetObject, scriptTypeName); } - catch (Exception e) + catch (Exception exception) { + Debug.LogException(exception); } finally { CleanupContext(); } + if (!bindSucceeded) + { + return; + } + EditorUtility.SetDirty(targetObject); - Debug.Log($"Generate {className} Successfully attached to game object"); + Debug.Log($"Generate {scriptTypeName} successfully attached to game object."); + } + + private static GameObject ResolveGenerationTarget() + { + var instanceId = EditorPrefs.GetInt(GenerateInstanceIdKey, -1); + var instanceTarget = EditorUtility.InstanceIDToObject(instanceId) as GameObject; + if (instanceTarget != null) + { + return instanceTarget; + } + + var assetPath = EditorPrefs.GetString(GenerateAssetPathKey, string.Empty); + return string.IsNullOrEmpty(assetPath) ? null : AssetDatabase.LoadAssetAtPath(assetPath); } private static void CleanupContext() { - EditorPrefs.DeleteKey("Generate"); + EditorPrefs.DeleteKey(GenerateTypeNameKey); + EditorPrefs.DeleteKey(GenerateInstanceIdKey); + EditorPrefs.DeleteKey(GenerateAssetPathKey); _uiBindDatas.Clear(); _arrayComponents.Clear(); } - private static void BindScriptPropertyField(GameObject targetObject, string scriptClassName) + private static bool BindScriptPropertyField(GameObject targetObject, string scriptTypeName) { if (targetObject == null) throw new ArgumentNullException(nameof(targetObject)); - if (string.IsNullOrEmpty(scriptClassName)) throw new ArgumentNullException(nameof(scriptClassName)); + if (string.IsNullOrEmpty(scriptTypeName)) throw new ArgumentNullException(nameof(scriptTypeName)); - var scriptType = FindScriptType(scriptClassName); + var scriptType = FindScriptType(scriptTypeName); if (scriptType == null) { - Debug.LogError($"Could not find the class: {scriptClassName}"); - return; + Debug.LogError($"Could not find the class: {scriptTypeName}"); + return false; } var targetHolder = targetObject.GetOrAddComponent(scriptType); - BindFieldsToComponents(targetHolder, scriptType); + return BindFieldsToComponents(targetHolder, scriptType); } - private static Type FindScriptType(string scriptClassName) + private static Type FindScriptType(string scriptTypeName) { + var resolvedType = AlicizaX.Utility.Assembly.GetType(scriptTypeName); + if (resolvedType != null) + { + return resolvedType; + } + return AppDomain.CurrentDomain.GetAssemblies() - .Where(asm => !asm.GetName().Name.EndsWith(".Editor") && - !asm.GetName().Name.Equals("UnityEditor")) - .SelectMany(asm => asm.GetTypes()) + .Where(assembly => !assembly.GetName().Name.EndsWith(".Editor", StringComparison.Ordinal) && + !assembly.GetName().Name.Equals("UnityEditor", StringComparison.Ordinal)) + .SelectMany(assembly => assembly.GetTypes()) .FirstOrDefault(type => type.IsClass && !type.IsAbstract && - type.Name.Equals(scriptClassName, StringComparison.Ordinal)); + type.FullName.Equals(scriptTypeName, StringComparison.Ordinal)); } - private static void BindFieldsToComponents(Component targetHolder, Type scriptType) + private static bool BindFieldsToComponents(Component targetHolder, Type scriptType) { - FieldInfo[] allNonPublicInstanceFields = scriptType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance); - - var fields = allNonPublicInstanceFields.Where(field => + var fields = scriptType.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance).Where(field => field.GetCustomAttributes(typeof(SerializeField), false).Length > 0 ); + var isSuccessful = true; foreach (var field in fields.Where(field => !string.IsNullOrEmpty(field.Name))) { var bindData = _uiBindDatas.Find(data => data.Name == field.Name); - var components = bindData.Objs; - if (components == null) + if (bindData == null) { - Debug.LogError($"Field {field.Name} did not find matching component binding"); + Debug.LogError($"Field {field.Name} did not find matching component binding."); + isSuccessful = false; continue; } - SetFieldValue(field, components, bindData.TypeName, targetHolder); + if (!SetFieldValue(field, bindData, targetHolder)) + { + isSuccessful = false; + } } + + return isSuccessful; } - private static void SetFieldValue(FieldInfo field, IReadOnlyList components, string typeName, Component targetComponent) + private static bool SetFieldValue(FieldInfo field, UIBindData bindData, Component targetComponent) { if (field.FieldType.IsArray) { - SetArrayFieldValue(field, components, typeName, targetComponent); - } - else - { - SetSingleFieldValue(field, components, typeName, targetComponent); + return SetArrayFieldValue(field, bindData, targetComponent); } + + return SetSingleFieldValue(field, bindData, targetComponent); } - private static void SetArrayFieldValue(FieldInfo field, IReadOnlyList components, string typeName, Component targetComponent) + private static bool SetArrayFieldValue(FieldInfo field, UIBindData bindData, Component targetComponent) { var elementType = field.FieldType.GetElementType(); if (elementType == null) { Debug.LogError($"Field {field.Name} has unknown element type."); - return; + return false; } + var components = bindData.Objs; var array = Array.CreateInstance(elementType, components.Count); + var isSuccessful = true; for (var i = 0; i < components.Count; i++) { if (components[i] == null) continue; - var isGameobject = typeName.Equals(nameof(GameObject)); - object ComponentObject = isGameobject ? components[i] : components[i].GetComponent(typeName); + var componentObject = ResolveBoundObject(components[i], bindData.ComponentType); - if (elementType.IsInstanceOfType(ComponentObject)) + if (componentObject != null && elementType.IsInstanceOfType(componentObject)) { - array.SetValue(ComponentObject, i); + array.SetValue(componentObject, i); } else { Debug.LogError($"Element {i} type mismatch for field {field.Name}"); + isSuccessful = false; } } field.SetValue(targetComponent, array); + return isSuccessful; } - private static void SetSingleFieldValue(FieldInfo field, IReadOnlyList components, string typeName, Component targetComponent) + private static bool SetSingleFieldValue(FieldInfo field, UIBindData bindData, Component targetComponent) { - if (components.Count == 0) return; - - var isGameobject = typeName.Equals(nameof(GameObject)); - object firstComponent = isGameobject ? components[0] : components[0].GetComponent(typeName); - if (firstComponent == null) return; - - if (field.FieldType.IsInstanceOfType(firstComponent)) + if (bindData.Objs.Count == 0) { - field.SetValue(targetComponent, firstComponent); + return false; } - else + + var firstComponent = ResolveBoundObject(bindData.Objs[0], bindData.ComponentType); + if (firstComponent == null) + { + return false; + } + + if (!field.FieldType.IsInstanceOfType(firstComponent)) { Debug.LogError($"Field {field.Name} type mismatch"); + return false; } + + field.SetValue(targetComponent, firstComponent); + return true; + } + + private static object ResolveBoundObject(GameObject source, Type componentType) + { + if (source == null || componentType == null) + { + return null; + } + + return componentType == typeof(GameObject) ? source : source.GetComponent(componentType); } public static class PrefabChecker diff --git a/Editor/UI/Inspector/UIHolderObjectBaseInspector.cs b/Editor/UI/Inspector/UIHolderObjectBaseInspector.cs index dcd224b..68b5eb5 100644 --- a/Editor/UI/Inspector/UIHolderObjectBaseInspector.cs +++ b/Editor/UI/Inspector/UIHolderObjectBaseInspector.cs @@ -27,7 +27,9 @@ public class UIHolderObjectBaseEditor : Editor if (prop.isArray) { - string arrayElementTypeName = prop.GetArrayElementAtIndex(0).type; + var fieldType = fields[i].FieldType; + var arrayElementType = fieldType.IsArray ? fieldType.GetElementType() : null; + string arrayElementTypeName = arrayElementType?.Name ?? "Element"; ReorderableList reorderableList = new ReorderableList(serializedObject, prop, true, true, true, true); reorderableList.drawElementCallback = (rect, index, isActive, isFocused) => DrawElementCallback(rect, index, prop, isActive, isFocused); diff --git a/Runtime/UI/Constant/UIHolderFactory.cs b/Runtime/UI/Constant/UIHolderFactory.cs index 823790a..449f007 100644 --- a/Runtime/UI/Constant/UIHolderFactory.cs +++ b/Runtime/UI/Constant/UIHolderFactory.cs @@ -10,12 +10,7 @@ namespace AlicizaX.UI.Runtime { public static class UIHolderFactory { - private static readonly IResourceModule ResourceModule; - - static UIHolderFactory() - { - ResourceModule = ModuleSystem.GetModule(); - } + private static IResourceModule ResourceModule => ModuleSystem.GetModule(); public static async UniTask CreateUIHolderAsync(Transform parent) where T : UIHolderObjectBase { diff --git a/Runtime/UI/UIBase/UIBase.cs b/Runtime/UI/UIBase/UIBase.cs index b8b82da..bf92f4e 100644 --- a/Runtime/UI/UIBase/UIBase.cs +++ b/Runtime/UI/UIBase/UIBase.cs @@ -153,7 +153,7 @@ namespace AlicizaX.UI.Runtime private bool Interactable { - get => _raycaster.enabled && _raycaster != null; + get => _raycaster != null && _raycaster.enabled; set {