| Assets/Editor/UIComponent/TextLanguageAdapterEditor.cs | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| Assets/Editor/UIComponent/TextLanguageAdapterHelper.cs | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| Assets/Editor/UIComponent/TextLanguageAdapterHelper.cs.meta | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| Assets/Editor/UIComponent/TextLanguageAdapterScanTool.cs | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| Assets/Editor/UIComponent/TextLanguageAdapterScanTool.cs.meta | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
Assets/Editor/UIComponent/TextLanguageAdapterEditor.cs
@@ -1,298 +1,200 @@ using UnityEngine; using UnityEditor; using System.Collections.Generic; using UnityEditor; using UnityEngine; /// <summary> /// TextLanguageAdapter 自定义编辑器 /// 提供可视化的多语言排版配置界面 /// </summary> [CustomEditor(typeof(TextLanguageAdapter), true)] [CanEditMultipleObjects] public class TextLanguageAdapterEditor : Editor { private Dictionary<string, bool> m_FoldoutStates = new Dictionary<string, bool>(); private Dictionary<string, string> m_LanguageShowDict; private static readonly string[] m_PresetLanguageIds = { TextLanguageAdapter.DefaultLangId, "zh", "ft", "en"}; private static readonly string[] m_PresetLanguageNames = { "默认", "简体", "繁体", "English" }; private string m_CopySourceLanguageId = ""; protected virtual void OnEnable() private void OnEnable() { InitLanguageShowDict(); TextLanguageAdapterHelper.Initialize(); if (target is TextLanguageAdapter adapter) { // 确保 default 配置存在 if (!adapter.HasConfig(TextLanguageAdapter.DefaultLangId)) { adapter.ReadCurrentToConfig(TextLanguageAdapter.DefaultLangId); EditorUtility.SetDirty(adapter); } foreach (var langId in adapter.GetConfiguredLanguages()) { m_FoldoutStates.TryAdd(langId, false); } foreach (var langId in adapter.GetConfiguredLanguages()) m_FoldoutStates.TryAdd(langId, false); } } public override void OnInspectorGUI() { TextLanguageAdapter adapter = target as TextLanguageAdapter; var adapter = target as TextLanguageAdapter; DrawBasicInfo(adapter); EditorGUILayout.Space(); DrawLanguageConfigs(adapter); if (GUI.changed) { EditorUtility.SetDirty(target); } } private void InitLanguageShowDict() { if (m_LanguageShowDict != null) return; m_LanguageShowDict = new Dictionary<string, string>(); for (int i = 0; i < m_PresetLanguageIds.Length; i++) { m_LanguageShowDict[m_PresetLanguageIds[i]] = m_PresetLanguageNames[i]; } } private string GetLanguageDisplayName(string languageId) { if (m_LanguageShowDict != null && m_LanguageShowDict.TryGetValue(languageId, out string name)) { return name; } return TextLanguageAdapter.GetLanguageShowName(languageId); if (GUI.changed) EditorUtility.SetDirty(target); } private void DrawBasicInfo(TextLanguageAdapter adapter) { EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.LabelField("基本信息", EditorStyles.boldLabel); EditorGUI.BeginDisabledGroup(true); // 使用新开放的 TargetTextComponent 属性,彻底消除反射 EditorGUILayout.ObjectField("目标组件", adapter.LanguageConfigs.keys.Count > 0 ? adapter.TargetTextComponent : null, typeof(Component), true); EditorGUILayout.EnumPopup("组件类型", adapter.TargetTextType); EditorGUI.EndDisabledGroup(); if (GUILayout.Button("刷新组件检测", GUILayout.Width(120))) using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) { adapter.Editor_ForceRefreshDetection(); } EditorGUILayout.LabelField("基本信息", EditorStyles.boldLabel); using (new EditorGUI.DisabledScope(true)) { EditorGUILayout.ObjectField("目标组件", adapter.LanguageConfigs.keys.Count > 0 ? adapter.TargetTextComponent : null, typeof(Component), true); EditorGUILayout.EnumPopup("组件类型", adapter.TargetTextType); } EditorGUILayout.EndVertical(); if (GUILayout.Button("刷新组件检测", GUILayout.Width(120))) adapter.Editor_ForceRefreshDetection(); EditorGUILayout.HelpBox("组件或类型变更后需执行刷新组件检测", MessageType.Info); } } private void DrawLanguageConfigs(TextLanguageAdapter adapter) { EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.LabelField($"语言排版配置 (必须包含 {TextLanguageAdapter.DefaultLangId} 默认配置)", EditorStyles.boldLabel); List<string> configuredLanguages = adapter.GetConfiguredLanguages(); foreach (var langId in configuredLanguages) using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) { DrawLanguageConfigItem(adapter, langId); } EditorGUILayout.LabelField("语言排版配置", EditorStyles.boldLabel); EditorGUILayout.HelpBox("default仅用于编辑时恢复初始状态,无对应语言配置时用组件当前状态", MessageType.Info); foreach (var langId in adapter.GetConfiguredLanguages()) DrawLanguageConfigItem(adapter, langId); EditorGUILayout.Space(); DrawAddLanguageConfig(adapter); EditorGUILayout.EndVertical(); EditorGUILayout.Space(); DrawAddLanguageConfig(adapter); } } private void DrawLanguageConfigItem(TextLanguageAdapter adapter, string languageId) private void DrawLanguageConfigItem(TextLanguageAdapter adapter, string langId) { m_FoldoutStates.TryAdd(languageId, false); m_FoldoutStates.TryAdd(langId, false); string displayName = GetLanguageDisplayName(languageId); string foldoutLabel = string.IsNullOrEmpty(displayName) ? languageId : $"{languageId} ({displayName})"; int presetIndex = System.Array.IndexOf(TextLanguageAdapterHelper.PresetLanguageIds, langId); string name = presetIndex >= 0 ? TextLanguageAdapterHelper.PresetLanguageNames[presetIndex] : TextLanguageAdapter.GetLanguageShowName(langId); string label = string.IsNullOrEmpty(name) ? langId : $"{langId} ({name})"; EditorGUI.indentLevel++; // 高亮默认配置 if (languageId == TextLanguageAdapter.DefaultLangId) { GUI.backgroundColor = new Color(1f, 0.9f, 0.7f); } m_FoldoutStates[languageId] = EditorGUILayout.Foldout(m_FoldoutStates[languageId], foldoutLabel, true); if (langId == TextLanguageAdapter.DefaultLangId) GUI.backgroundColor = new Color(1f, 0.9f, 0.7f); m_FoldoutStates[langId] = EditorGUILayout.Foldout(m_FoldoutStates[langId], label, true); GUI.backgroundColor = Color.white; if (m_FoldoutStates[languageId]) if (m_FoldoutStates[langId]) { EditorGUI.indentLevel++; LanguageConfigItem config = adapter.LanguageConfigs.Get(languageId); var config = adapter.LanguageConfigs.Get(langId); if (config != null) { DrawConfigItem(config); EditorGUILayout.Space(); DrawConfigItemActions(adapter, languageId); DrawConfigItemActions(adapter, langId); } EditorGUI.indentLevel--; } EditorGUI.indentLevel--; } /// <summary> /// 绘制 RectTransform 和指定的文本适配属性 /// </summary> private void DrawConfigItem(LanguageConfigItem config) private void DrawConfigItem(LanguageConfigItem cfg) { // 1. RectTransform 配置 EditorGUILayout.LabelField("RectTransform 配置", EditorStyles.miniBoldLabel); EditorGUI.BeginChangeCheck(); config.anchoredPosition = EditorGUILayout.Vector2Field("Anchored Position", config.anchoredPosition); config.sizeDelta = EditorGUILayout.Vector2Field("Size Delta", config.sizeDelta); config.anchorMin = EditorGUILayout.Vector2Field("Anchor Min", config.anchorMin); config.anchorMax = EditorGUILayout.Vector2Field("Anchor Max", config.anchorMax); config.pivot = EditorGUILayout.Vector2Field("Pivot", config.pivot); config.localScale = EditorGUILayout.Vector3Field("Local Scale", config.localScale); config.localRotation = EditorGUILayout.Vector3Field("Local Rotation", config.localRotation); cfg.anchoredPosition = EditorGUILayout.Vector2Field("Anchored Position", cfg.anchoredPosition); cfg.sizeDelta = EditorGUILayout.Vector2Field("Size Delta", cfg.sizeDelta); cfg.anchorMin = EditorGUILayout.Vector2Field("Anchor Min", cfg.anchorMin); cfg.anchorMax = EditorGUILayout.Vector2Field("Anchor Max", cfg.anchorMax); cfg.pivot = EditorGUILayout.Vector2Field("Pivot", cfg.pivot); cfg.localScale = EditorGUILayout.Vector3Field("Local Scale", cfg.localScale); cfg.localRotation = EditorGUILayout.Vector3Field("Local Rotation", cfg.localRotation); EditorGUILayout.Space(); // 2. 文本排版与适配配置 EditorGUILayout.LabelField("文本排版与适配配置", EditorStyles.miniBoldLabel); config.font = (Font)EditorGUILayout.ObjectField("Font", config.font, typeof(Font), false); config.fontStyle = (FontStyle)EditorGUILayout.EnumPopup("Font Style", config.fontStyle); config.fontSize = EditorGUILayout.IntField("Font Size", config.fontSize); config.lineSpacing = EditorGUILayout.FloatField("Line Spacing", config.lineSpacing); config.alignment = (TextAnchor)EditorGUILayout.EnumPopup("Alignment", config.alignment); config.alignByGeometry = EditorGUILayout.Toggle("Align By Geometry", config.alignByGeometry); config.horizontalOverflow = (HorizontalWrapMode)EditorGUILayout.EnumPopup("Horizontal Overflow", config.horizontalOverflow); config.verticalOverflow = (VerticalWrapMode)EditorGUILayout.EnumPopup("Vertical Overflow", config.verticalOverflow); config.resizeTextForBestFit = EditorGUILayout.Toggle("Best Fit", config.resizeTextForBestFit); cfg.font = (Font)EditorGUILayout.ObjectField("Font", cfg.font, typeof(Font), false); cfg.fontStyle = (FontStyle)EditorGUILayout.EnumPopup("Font Style", cfg.fontStyle); cfg.fontSize = EditorGUILayout.IntField("Font Size", cfg.fontSize); cfg.lineSpacing = EditorGUILayout.FloatField("Line Spacing", cfg.lineSpacing); cfg.alignment = (TextAnchor)EditorGUILayout.EnumPopup("Alignment", cfg.alignment); cfg.alignByGeometry = EditorGUILayout.Toggle("Align By Geometry", cfg.alignByGeometry); cfg.horizontalOverflow = (HorizontalWrapMode)EditorGUILayout.EnumPopup("Horizontal Overflow", cfg.horizontalOverflow); cfg.verticalOverflow = (VerticalWrapMode)EditorGUILayout.EnumPopup("Vertical Overflow", cfg.verticalOverflow); cfg.resizeTextForBestFit = EditorGUILayout.Toggle("Best Fit", cfg.resizeTextForBestFit); if (config.resizeTextForBestFit) if (cfg.resizeTextForBestFit) { EditorGUI.indentLevel++; config.resizeTextMinSize = EditorGUILayout.IntField("Min Size", config.resizeTextMinSize); config.resizeTextMaxSize = EditorGUILayout.IntField("Max Size", config.resizeTextMaxSize); cfg.resizeTextMinSize = EditorGUILayout.IntField("Min Size", cfg.resizeTextMinSize); cfg.resizeTextMaxSize = EditorGUILayout.IntField("Max Size", cfg.resizeTextMaxSize); EditorGUI.indentLevel--; } if (EditorGUI.EndChangeCheck()) { EditorUtility.SetDirty(target); } if (EditorGUI.EndChangeCheck()) EditorUtility.SetDirty(target); } private void DrawConfigItemActions(TextLanguageAdapter adapter, string languageId) private void DrawConfigItemActions(TextLanguageAdapter adapter, string langId) { EditorGUILayout.BeginHorizontal(); GUILayout.Space(EditorGUI.indentLevel * 15f); // 补偿 GUILayout 忽略 indentLevel 的问题 if (GUILayout.Button("从当前读取", GUILayout.Width(80))) using (new EditorGUILayout.HorizontalScope()) { Undo.RecordObject(adapter, "Read Current Config"); adapter.ReadCurrentToConfig(languageId); } GUILayout.Space(EditorGUI.indentLevel * 15f); if (GUILayout.Button("应用", GUILayout.Width(60))) { Undo.RecordObject(adapter, "Apply Config"); adapter.ApplyConfig(languageId); } bool isDefault = languageId == TextLanguageAdapter.DefaultLangId; if (!isDefault) { if (GUILayout.Button("复制到默认", GUILayout.Width(80))) if (GUILayout.Button("从当前读取", GUILayout.Width(80))) { Undo.RecordObject(adapter, "Copy to Default"); adapter.SetConfig(TextLanguageAdapter.DefaultLangId, adapter.LanguageConfigs.Get(languageId)?.Clone()); Undo.RecordObject(adapter, "Read Current Config"); adapter.ReadCurrentToConfig(langId); } if (GUILayout.Button("应用", GUILayout.Width(60))) { Undo.RecordObject(adapter, "Apply Config"); adapter.ApplyConfig(langId); } using (new EditorGUI.DisabledScope(langId == TextLanguageAdapter.DefaultLangId)) { if (GUILayout.Button("删除", GUILayout.Width(50))) { Undo.RecordObject(adapter, "Remove Config"); adapter.RemoveConfig(langId); m_FoldoutStates.Remove(langId); } } } // Default 不可删除 GUI.enabled = !isDefault; if (GUILayout.Button("删除", GUILayout.Width(50))) { Undo.RecordObject(adapter, "Remove Config"); adapter.RemoveConfig(languageId); m_FoldoutStates.Remove(languageId); } GUI.enabled = true; EditorGUILayout.EndHorizontal(); } private void DrawAddLanguageConfig(TextLanguageAdapter adapter) { // === 添加预设语言 === EditorGUILayout.BeginHorizontal(); GUILayout.Space(15f); EditorGUILayout.LabelField("添加预设语言:", GUILayout.Width(100)); // Inspector 面板的可用总宽度 (减去右侧滚动条的大致宽度) float viewWidth = EditorGUIUtility.currentViewWidth - 30f; float currentWidth = 115f; // 起始宽度:15(Space) + 100(LabelWidth) float buttonWidth = 64f; // 按钮宽度 60 + 默认边距大约 4 float currentWidth = 115f; float buttonWidth = 64f; for (int i = 0; i < m_PresetLanguageIds.Length; i++) for (int i = 0; i < TextLanguageAdapterHelper.PresetLanguageIds.Length; i++) { string langId = m_PresetLanguageIds[i]; string langId = TextLanguageAdapterHelper.PresetLanguageIds[i]; if (!adapter.HasConfig(langId)) { // 检测当前行是否放得下下一个按钮 if (currentWidth + buttonWidth > viewWidth) { // 放不下,结束当前行,开启新的一行 EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); // 新行为了美观,缩进对齐上面的按钮 GUILayout.Space(115f); currentWidth = 115f; } if (GUILayout.Button(m_PresetLanguageNames[i], GUILayout.Width(60))) if (GUILayout.Button(TextLanguageAdapterHelper.PresetLanguageNames[i], GUILayout.Width(60))) { CreateNewLanguageConfig(adapter, langId); Undo.RecordObject(adapter, "Add Language Config"); var newConfig = adapter.HasConfig(TextLanguageAdapter.DefaultLangId) ? adapter.LanguageConfigs.Get(TextLanguageAdapter.DefaultLangId).Clone() : new LanguageConfigItem(); adapter.SetConfig(langId, newConfig); m_FoldoutStates[langId] = true; } // 累加当前行的宽度 currentWidth += buttonWidth; } } EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(2f); // === 添加自定义语言 === EditorGUILayout.BeginHorizontal(); GUILayout.Space(15f); EditorGUILayout.LabelField("自定义语言ID:", GUILayout.Width(100)); m_CopySourceLanguageId = EditorGUILayout.TextField(m_CopySourceLanguageId, GUILayout.Width(80)); if (GUILayout.Button("添加", GUILayout.Width(50))) { string newLangId = m_CopySourceLanguageId.Trim(); if (!string.IsNullOrEmpty(newLangId) && !adapter.HasConfig(newLangId)) { CreateNewLanguageConfig(adapter, newLangId); m_CopySourceLanguageId = ""; GUI.FocusControl(null); } } EditorGUILayout.EndHorizontal(); } /// <summary> 辅助方法:封装新建语言配置的复用逻辑 </summary> private void CreateNewLanguageConfig(TextLanguageAdapter adapter, string langId) { Undo.RecordObject(adapter, "Add Language Config"); LanguageConfigItem newConfig = adapter.HasConfig(TextLanguageAdapter.DefaultLangId) ? adapter.LanguageConfigs.Get(TextLanguageAdapter.DefaultLangId).Clone() : new LanguageConfigItem(); adapter.SetConfig(langId, newConfig); m_FoldoutStates[langId] = true; // 自动展开新添加的项 } } Assets/Editor/UIComponent/TextLanguageAdapterHelper.cs
New file @@ -0,0 +1,67 @@ using System.Collections.Generic; using System.IO; using LitJson; using UnityEngine; /// <summary> /// 编辑器语言配置读取助手 /// 负责统一解析 InitialFunction.txt /// </summary> public static class TextLanguageAdapterHelper { public static string[] PresetLanguageIds { get; private set; } public static string[] PresetLanguageNames { get; private set; } private static bool s_Initialized; public static void Initialize() { if (s_Initialized) return; var idList = new List<string> { TextLanguageAdapter.DefaultLangId }; var nameList = new List<string> { "默认" }; string configPath = Path.Combine(Application.dataPath, "ResourcesOut/Config/InitialFunction.txt"); if (File.Exists(configPath)) { try { string[] lines = File.ReadAllLines(configPath); for (int i = 3; i < lines.Length; i++) { string line = lines[i]; if (string.IsNullOrWhiteSpace(line)) continue; int index = line.IndexOf('\t'); if (index == -1) continue; string key = line.Substring(0, index); if (key == "LanguageEx") { string[] fields = line.Split('\t'); if (fields.Length > 1 && !string.IsNullOrEmpty(fields[1])) { var dict = JsonMapper.ToObject<Dictionary<string, string>>(fields[1]); if (dict != null) { foreach (var kvp in dict) { idList.Add(kvp.Key); nameList.Add(kvp.Value); } } } break; } } } catch (System.Exception ex) { Debug.LogWarning($"[EditorLanguageHelper] 读取配置失败: {ex.Message}"); } } PresetLanguageIds = idList.ToArray(); PresetLanguageNames = nameList.ToArray(); s_Initialized = true; } } Assets/Editor/UIComponent/TextLanguageAdapterHelper.cs.meta
New file @@ -0,0 +1,11 @@ fileFormatVersion: 2 guid: 0c45ec45897ae19418b885190f6e94d6 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: Assets/Editor/UIComponent/TextLanguageAdapterScanTool.cs
New file @@ -0,0 +1,505 @@ using System.Collections.Generic; using System.IO; using UnityEditor; using UnityEditor.IMGUI.Controls; using UnityEngine; // ======================== 扫描数据模型 ======================== public class ScanResultItem { public string PrefabPath { get; } public string GameObjectPath { get; } public List<string> MissingLanguages { get; set; } = new List<string>(); public TextComponentType ComponentType { get; } public string PrefabGUID { get; } public ScanResultItem(string path, string goPath, TextComponentType type, string guid) { PrefabPath = path; GameObjectPath = goPath; ComponentType = type; PrefabGUID = guid; } public string GetDisplayName() => $"{GameObjectPath} (缺少: {string.Join(", ", MissingLanguages)}) [{ComponentType}]"; } public class PrefabScanResult { public string PrefabPath { get; } public string PrefabGUID { get; } public List<ScanResultItem> Items { get; } = new List<ScanResultItem>(); public PrefabScanResult(string path, string guid) { PrefabPath = path; PrefabGUID = guid; } } public class ScanResultSummary { public string ScanDirectory { get; } public int TotalPrefabsScanned { get; set; } public int TotalAdaptersFound { get; private set; } public int AdaptersWithMissingConfig { get; private set; } public List<PrefabScanResult> PrefabResults { get; } = new List<PrefabScanResult>(); public ScanResultSummary(string dir) => ScanDirectory = dir; public void AddResult(ScanResultItem item) { var prefabResult = PrefabResults.Find(p => p.PrefabPath == item.PrefabPath); if (prefabResult == null) { prefabResult = new PrefabScanResult(item.PrefabPath, item.PrefabGUID); PrefabResults.Add(prefabResult); } prefabResult.Items.Add(item); TotalAdaptersFound++; AdaptersWithMissingConfig++; } } // ======================== TreeView 实现 ======================== public class MetadataTreeViewItem : TreeViewItem { public object Metadata { get; } public MetadataTreeViewItem(int id, string name, object meta) : base(id, 0, name) => Metadata = meta; } public class ScanResultTreeView : TreeView { private ScanResultSummary m_Summary; public ScanResultTreeView(TreeViewState state, ScanResultSummary summary) : base(state) { m_Summary = summary; Reload(); ExpandAll(); } protected override TreeViewItem BuildRoot() { var root = new TreeViewItem { id = 0, depth = -1, displayName = "Root" }; // 【关键修复 1】:必须初始化 children 列表。 // 否则当扫描结果完美(0个错误)时,root.children 为 null 会导致 Unity 报错 root.children = new List<TreeViewItem>(); if (m_Summary == null || m_Summary.PrefabResults.Count == 0) return root; int itemId = 1; foreach (var prefabResult in m_Summary.PrefabResults) { string prefabName = Path.GetFileNameWithoutExtension(prefabResult.PrefabPath); var prefabItem = new MetadataTreeViewItem(itemId++, $"{prefabName} ({prefabResult.Items.Count}个问题)", prefabResult); foreach (var adapterItem in prefabResult.Items) { var adapterTreeItem = new MetadataTreeViewItem(itemId++, adapterItem.GetDisplayName(), adapterItem); prefabItem.AddChild(adapterTreeItem); } root.AddChild(prefabItem); } // 【关键修复 2】:Unity 官方规范要求,手动使用 AddChild 构建树之后,必须调用此方法刷新深度和层级关系 SetupDepthsFromParentsAndChildren(root); return root; } protected override void RowGUI(RowGUIArgs args) { var item = args.item as MetadataTreeViewItem; if (item != null && item.Metadata is PrefabScanResult) GUI.Label(args.rowRect, item.displayName, EditorStyles.boldLabel); else if (item != null && item.Metadata is ScanResultItem adapterItem) GUI.Label(args.rowRect, $"{adapterItem.GameObjectPath} (缺少: {string.Join(", ", adapterItem.MissingLanguages)})"); else base.RowGUI(args); } protected override void DoubleClickedItem(int id) { var item = FindItem(id, rootItem) as MetadataTreeViewItem; if (item == null) return; if (item.Metadata is ScanResultItem adapterItem) PingGameObject(adapterItem.PrefabPath, adapterItem.GameObjectPath); else if (item.Metadata is PrefabScanResult prefabResult) { var obj = AssetDatabase.LoadAssetAtPath<GameObject>(prefabResult.PrefabPath); if (obj != null) { Selection.activeObject = obj; EditorGUIUtility.PingObject(obj); } } } private void PingGameObject(string prefabPath, string gameObjectPath) { var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath); if (prefab == null) return; Transform target = prefab.transform.Find(gameObjectPath); if (target != null) { Selection.activeObject = target.gameObject; EditorGUIUtility.PingObject(target.gameObject); } else { Selection.activeObject = prefab; EditorGUIUtility.PingObject(prefab); Debug.LogWarning($"[ScanTool] 找不到路径 '{gameObjectPath}',已选中整个预制体"); } } } // ======================== 扫描工具主窗口 ======================== public class TextLanguageAdapterScanTool : EditorWindow { private string m_ScanDirectory = "Assets"; private Vector2 m_ScrollPosition; private ScanResultSummary m_ScanResult; private bool m_IsScanning; private float m_ScanProgress; private string m_ScanStatus; private ScanResultTreeView m_TreeView; private TreeViewState m_TreeViewState; // 批量操作的UI状态 private int m_SourceLangIndex = 0; private int m_TargetLangIndex = 0; private bool m_OverwriteExisting = false; [MenuItem("程序/TextLanguageAdapter扫描与管理工具")] public static void ShowWindow() { var window = GetWindow<TextLanguageAdapterScanTool>("语言适配器扫描"); window.minSize = new Vector2(600f, 500f); } private void OnEnable() => TextLanguageAdapterHelper.Initialize(); private void OnGUI() { DrawHeader(); EditorGUILayout.Space(5f); DrawScanSettings(); EditorGUILayout.Space(5f); DrawScanButton(); EditorGUILayout.Space(5f); DrawResults(); EditorGUILayout.Space(5f); DrawBatchOperations(); } private void DrawHeader() { using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) { EditorGUILayout.LabelField("TextLanguageAdapter 配置缺失扫描与批量操作工具", EditorStyles.boldLabel); EditorGUILayout.HelpBox("扫描指定目录下所有预制体,检测组件的语言配置是否完整,或执行批量语言配置复制。", MessageType.Info); } } private void DrawScanSettings() { using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) { EditorGUILayout.LabelField("扫描设置", EditorStyles.boldLabel); using (new EditorGUILayout.HorizontalScope()) { EditorGUILayout.LabelField("目标目录:", GUILayout.Width(80f)); m_ScanDirectory = EditorGUILayout.TextField(m_ScanDirectory); if (GUILayout.Button("选择...", GUILayout.Width(70f))) { string path = EditorUtility.OpenFolderPanel("选择扫描目录", Application.dataPath, ""); if (!string.IsNullOrEmpty(path)) m_ScanDirectory = path.StartsWith(Application.dataPath) ? "Assets" + path.Substring(Application.dataPath.Length) : path; } } EditorGUILayout.Space(5f); EditorGUILayout.LabelField($"预设语言:"); EditorGUILayout.BeginHorizontal(); GUILayout.Space(15f); int displayCount = 0; foreach (var langId in TextLanguageAdapterHelper.PresetLanguageIds) { if (langId == TextLanguageAdapter.DefaultLangId) continue; EditorGUILayout.LabelField(langId, GUILayout.Width(50f)); if (++displayCount % 8 == 0) { EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); GUILayout.Space(15f); } } EditorGUILayout.EndHorizontal(); } } private void DrawScanButton() { using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) { using (new EditorGUILayout.HorizontalScope()) { using (new EditorGUI.DisabledScope(m_IsScanning || string.IsNullOrEmpty(m_ScanDirectory))) { if (GUILayout.Button("开始扫描", GUILayout.Height(30f))) StartScan(); } if (m_IsScanning) { EditorGUILayout.LabelField("扫描中...", GUILayout.Width(100f)); m_ScanProgress = EditorGUILayout.Slider(m_ScanProgress, 0f, 1f); Repaint(); } } if (!string.IsNullOrEmpty(m_ScanStatus)) EditorGUILayout.LabelField(m_ScanStatus); } } private void DrawResults() { using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) { EditorGUILayout.LabelField("扫描结果", EditorStyles.boldLabel); if (m_ScanResult != null) { EditorGUILayout.LabelField($"预制体总数: {m_ScanResult.TotalPrefabsScanned} | Adapter总数: {m_ScanResult.TotalAdaptersFound} | 缺失配置: {m_ScanResult.AdaptersWithMissingConfig}"); EditorGUILayout.Space(5f); if (m_TreeView != null) { m_ScrollPosition = EditorGUILayout.BeginScrollView(m_ScrollPosition, GUILayout.MinHeight(150f)); var rect = EditorGUILayout.GetControlRect(false, m_TreeView.totalHeight); m_TreeView.OnGUI(rect); EditorGUILayout.EndScrollView(); } EditorGUILayout.Space(5f); using (new EditorGUILayout.HorizontalScope()) { if (GUILayout.Button("展开全部")) m_TreeView?.ExpandAll(); if (GUILayout.Button("折叠全部")) m_TreeView?.CollapseAll(); } } else { EditorGUILayout.HelpBox("点击「开始扫描」按钮进行扫描", MessageType.None); } } } // ======================== 新增:批量操作功能 ======================== private void DrawBatchOperations() { using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) { EditorGUILayout.LabelField("批量操作 (针对目标目录下的所有预制体上的TextLanguageAdapter组件)", EditorStyles.boldLabel); if (TextLanguageAdapterHelper.PresetLanguageIds == null || TextLanguageAdapterHelper.PresetLanguageIds.Length == 0) return; using (new EditorGUILayout.HorizontalScope()) { EditorGUILayout.LabelField("旧语言:", GUILayout.Width(60f)); m_SourceLangIndex = EditorGUILayout.Popup(m_SourceLangIndex, TextLanguageAdapterHelper.PresetLanguageNames); GUILayout.Space(20f); EditorGUILayout.LabelField("新语言:", GUILayout.Width(60f)); m_TargetLangIndex = EditorGUILayout.Popup(m_TargetLangIndex, TextLanguageAdapterHelper.PresetLanguageNames); } m_OverwriteExisting = EditorGUILayout.Toggle("覆盖已存在的目标配置", m_OverwriteExisting); EditorGUILayout.Space(5f); bool isSameLanguage = m_SourceLangIndex == m_TargetLangIndex; using (new EditorGUI.DisabledScope(isSameLanguage || m_IsScanning || string.IsNullOrEmpty(m_ScanDirectory))) { if (GUILayout.Button("批量复制配置", GUILayout.Height(30f))) { string sourceLang = TextLanguageAdapterHelper.PresetLanguageIds[m_SourceLangIndex]; string targetLang = TextLanguageAdapterHelper.PresetLanguageIds[m_TargetLangIndex]; if (EditorUtility.DisplayDialog("高危操作确认", $"此操作将遍历【{m_ScanDirectory}】下所有预制体。\n\n" + $"把它们的 [{sourceLang}] 配置复制并应用到 [{targetLang}] 配置上。\n\n" + $"此操作不可撤销!建议提前使用 Git/SVN 提交代码。\n确定要继续吗?", "确定执行", "取消")) { ExecuteBatchCopy(sourceLang, targetLang); } } } if (isSameLanguage) { EditorGUILayout.HelpBox("旧语言与新语言不能相同", MessageType.Warning); } } } private void ExecuteBatchCopy(string sourceLang, string targetLang) { string[] prefabGuids = AssetDatabase.FindAssets("t:Prefab", new[] { m_ScanDirectory }); if (prefabGuids.Length == 0) return; int modifiedPrefabCount = 0; int modifiedAdapterCount = 0; try { for (int i = 0; i < prefabGuids.Length; i++) { string path = AssetDatabase.GUIDToAssetPath(prefabGuids[i]); EditorUtility.DisplayProgressBar("批量复制配置", $"处理中 ({i + 1}/{prefabGuids.Length}): {path}", (float)i / prefabGuids.Length); // 【核心修改】:直接加载资产内存,不使用 LoadPrefabContents 进行实例化 GameObject prefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>(path); bool isModified = false; foreach (var adapter in prefabAsset.GetComponentsInChildren<TextLanguageAdapter>(true)) { if (adapter.HasConfig(sourceLang)) { if (m_OverwriteExisting || !adapter.HasConfig(targetLang)) { // 必须标记对象为脏,否则 Unity 不会把资产的修改存盘 Undo.RecordObject(adapter, "Batch Copy Language Config"); var clonedConfig = adapter.GetConfig(sourceLang).Clone(); adapter.SetConfig(targetLang, clonedConfig); EditorUtility.SetDirty(adapter); isModified = true; modifiedAdapterCount++; } } } if (isModified) { modifiedPrefabCount++; } } } finally { EditorUtility.ClearProgressBar(); // 统一保存所有标记为 Dirty 的资产修改 AssetDatabase.SaveAssets(); PerformScan(); EditorUtility.DisplayDialog("批量操作完成", $"批量复制结束!\n\n修改的预制体数量: {modifiedPrefabCount} 个\n更新的适配器配置数量: {modifiedAdapterCount} 个", "确认"); } } // ======================== 核心扫描逻辑 ======================== private void StartScan() { m_IsScanning = true; m_ScanProgress = 0f; m_ScanStatus = "准备扫描..."; EditorApplication.CallbackFunction updateCallback = null; updateCallback = () => { if (!m_IsScanning) { EditorApplication.update -= updateCallback; return; } PerformScan(); m_IsScanning = false; EditorApplication.update -= updateCallback; }; EditorApplication.update += updateCallback; } private void PerformScan() { m_ScanResult = new ScanResultSummary(m_ScanDirectory); string[] prefabGuids = AssetDatabase.FindAssets("t:Prefab", new[] { m_ScanDirectory }); m_ScanResult.TotalPrefabsScanned = prefabGuids.Length; if (prefabGuids.Length == 0) { m_ScanStatus = "未找到任何预制体"; return; } for (int i = 0; i < prefabGuids.Length; i++) { string path = AssetDatabase.GUIDToAssetPath(prefabGuids[i]); ScanPrefab(path, prefabGuids[i]); m_ScanProgress = (float)(i + 1) / prefabGuids.Length; m_ScanStatus = $"正在扫描: {Path.GetFileName(path)} ({i + 1}/{prefabGuids.Length})"; if (i % 10 == 0) Repaint(); } m_TreeViewState ??= new TreeViewState(); m_TreeView = new ScanResultTreeView(m_TreeViewState, m_ScanResult); m_ScanStatus = $"扫描完成! 发现 {m_ScanResult.AdaptersWithMissingConfig} 个缺失配置"; Repaint(); } private void ScanPrefab(string path, string guid) { GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path); if (prefab == null) return; foreach (var adapter in prefab.GetComponentsInChildren<TextLanguageAdapter>(true)) { List<string> missing = new List<string>(); foreach (var langId in TextLanguageAdapterHelper.PresetLanguageIds) { if (langId == TextLanguageAdapter.DefaultLangId) continue; if (!adapter.HasConfig(langId)) missing.Add(langId); } if (missing.Count > 0) { var item = new ScanResultItem(path, GetGameObjectPath(adapter.gameObject, prefab), adapter.TargetTextType, guid) { MissingLanguages = missing }; m_ScanResult.AddResult(item); } } } private string GetGameObjectPath(GameObject go, GameObject root) { var parts = new List<string>(); Transform curr = go.transform; while (curr != null && curr != root.transform) { parts.Insert(0, curr.name); curr = curr.parent; } return string.Join("/", parts); } } Assets/Editor/UIComponent/TextLanguageAdapterScanTool.cs.meta
New file @@ -0,0 +1,11 @@ fileFormatVersion: 2 guid: bed187951fdc81f4583b3ff993b28eef MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: