using System; using System.IO; using System.Collections.Generic; using AlicizaX.Editor; using Sirenix.OdinInspector; using UnityEditor.IMGUI.Controls; using UnityEditor; using UnityEngine; namespace AlicizaX.Localization.Editor { public class LocalizationTableWindow : EditorWindow { private List allTables = new List(); // 存储所有找到的GameLocaizationTable private string[] tableDisplayNames; // 用于下拉框显示的名称数组 private int selectedTableIndex = 0; // 当前选中的索引 private string selectString = string.Empty; private GameLocaizationTable currentTable; // 当前选中的GameLocaizationTable // 原本是 const,现在改成可调的字段 [SerializeField] private float languagesWidth = 200f; [SerializeField] private float tableSheetWidth = 300f; private const float SplitterWidth = 3f; // 拖拽条的宽度 private bool isResizingLeft, isResizingMiddle; private float Spacing => EditorGUIUtility.standardVerticalSpacing * 2; private GUIStyle miniLabelButton => new GUIStyle(EditorStyles.miniButton) { font = EditorStyles.miniBoldLabel.font, fontSize = EditorStyles.miniBoldLabel.fontSize }; public class WindowSelection { public TreeViewItem TreeViewItem; } public sealed class LanguageSelect : WindowSelection { public TempLanguageData Language; } public sealed class SectionSelect : WindowSelection { public SheetSectionTreeView Section; } public sealed class ItemSelect : WindowSelection { public SheetItemTreeView Item; } private LocalizationWindowData windowData; private SearchField searchField; private string searchString; private Vector2 scrollPosition; [SerializeField] private TreeViewState languagesTreeViewState; private LanguagesTreeView languagesTreeView; [SerializeField] private TreeViewState tableSheetTreeViewState; private TableSheetTreeView tableSheetTreeView; private WindowSelection selection = null; private bool globalExpanded = false; private void CreateGUI() { searchField = new SearchField(); } private void OnDestroy() { SaveSelection(); } private void OnEnable() { RefreshTableList(); } private void OnDisable() { SaveSelection(); } private void RefreshTableList() { allTables.Clear(); string[] guids = AssetDatabase.FindAssets("t:GameLocaizationTable"); foreach (string guid in guids) { string assetPath = AssetDatabase.GUIDToAssetPath(guid); GameLocaizationTable table = AssetDatabase.LoadAssetAtPath(assetPath); if (table != null) { allTables.Add(table); } } tableDisplayNames = new string[allTables.Count]; for (int i = 0; i < allTables.Count; i++) { tableDisplayNames[i] = allTables[i].name; } if (allTables.Count > 0 && selectedTableIndex >= allTables.Count) { selectedTableIndex = 0; } var selectIndex = selectedTableIndex; var lastSelect = EditorPrefs.GetString("LastSelectedGameLocaizationTable", string.Empty); if (!string.IsNullOrEmpty(lastSelect)) { for (int i = 0; i < allTables.Count; i++) { var path = AssetDatabase.GetAssetPath(allTables[i]); if (path.Equals(lastSelect)) { selectIndex = i; } } } if (selectIndex <= tableDisplayNames.Length - 1 && selectIndex >= 0) { selectString = tableDisplayNames[selectIndex]; } UpdateCurrentTable(); } private void UpdateCurrentTable() { if (allTables.Count > 0 && selectedTableIndex >= 0 && selectedTableIndex < allTables.Count) { currentTable = allTables[selectedTableIndex]; SaveSelection(); } else { currentTable = null; } InitializeTreeView(); } private void SaveSelection() { if (currentTable != null) { string path = AssetDatabase.GetAssetPath(currentTable); EditorPrefs.SetString("LastSelectedGameLocaizationTable", path); } } private void InitializeTreeView() { if (!currentTable) return; LocalizationWindowUtility.BuildWindowData(currentTable, out windowData); foreach (var section in windowData.TableSheet) { section.IsExpanded = globalExpanded; } languagesTreeViewState = new TreeViewState(); languagesTreeView = new(languagesTreeViewState, windowData, currentTable) { OnLanguageSelect = (s) => selection = s }; tableSheetTreeViewState = new TreeViewState(); tableSheetTreeView = new(tableSheetTreeViewState, windowData) { OnTableSheetSelect = (s) => selection = s }; } private void OnGUI() { // 顶部工具栏 Rect toolbarRect = new(0, 0, position.width, 20f); GUI.Box(toolbarRect, GUIContent.none, EditorStyles.toolbar); float buttonWidth = 100f; float spacing = 5f; Rect leftTitle = new(toolbarRect.xMin, 0, 40, 20f); Rect leftPop = new(leftTitle.xMin + 40, 0, 200, 20f); Rect saveBtn = new(toolbarRect.xMax - buttonWidth - spacing, 0, buttonWidth, 20f); Rect genBtn = new(saveBtn.xMin - buttonWidth - spacing, 0, buttonWidth, 20f); Rect importBtn = new(genBtn.xMin - buttonWidth - spacing, 0, buttonWidth, 20f); Rect exportBtn = new(importBtn.xMin - buttonWidth - spacing, 0, buttonWidth, 20f); EditorGUI.LabelField(leftTitle, "Table", EditorStyles.boldLabel); EditorDrawing.DrawStringSelectPopup(leftPop, tableDisplayNames, selectString, (e) => { selectString = e; selectedTableIndex = allTables.FindIndex(table => table.name == e); UpdateCurrentTable(); }); if (currentTable == null) return; if (GUI.Button(exportBtn, "Export CSV", EditorStyles.toolbarButton)) { string path = EditorUtility.SaveFilePanel("Export CSV", "", "Localization", "csv"); LocalizationExporter.ExportLocalizationToCSV(windowData, path); } if (GUI.Button(importBtn, "Import CSV", EditorStyles.toolbarButton)) { string path = EditorUtility.OpenFilePanel("Export CSV", "", "csv"); LocalizationExporter.ImportLocalizationFromCSV(windowData, path); } if (GUI.Button(genBtn, "Gen Code", EditorStyles.toolbarButton)) { LocalizationWindowUtility.GenerateCode(currentTable); } if (GUI.Button(saveBtn, "Save Asset", EditorStyles.toolbarButton)) { BuildLocalizationTable(); EditorUtility.SetDirty(currentTable); AssetDatabase.SaveAssets(); } // ---------------- 这里开始是 SplitView ---------------- Rect fullRect = new Rect(0, 20f, position.width, position.height - 20f); // 左侧 Languages Rect languagesRect = new Rect(fullRect.x, fullRect.y, languagesWidth, fullRect.height); languagesTreeView?.OnGUI(languagesRect); // 左侧拖拽条 Rect leftSplitter = new Rect(languagesRect.xMax, fullRect.y, SplitterWidth, fullRect.height); EditorGUIUtility.AddCursorRect(leftSplitter, MouseCursor.ResizeHorizontal); HandleResize(ref languagesWidth, leftSplitter, ref isResizingLeft); // 中间 TableSheet Rect tableSheetRect = new Rect(leftSplitter.xMax, fullRect.y, tableSheetWidth, fullRect.height); tableSheetTreeView?.OnGUI(tableSheetRect); // 中间拖拽条 Rect midSplitter = new Rect(tableSheetRect.xMax, fullRect.y, SplitterWidth, fullRect.height); EditorGUIUtility.AddCursorRect(midSplitter, MouseCursor.ResizeHorizontal); HandleResize(ref tableSheetWidth, midSplitter, ref isResizingMiddle); // 右侧 Inspector(剩余空间) float inspectorWidth = fullRect.width - (languagesWidth + tableSheetWidth + SplitterWidth * 2); Rect inspectorRect = new Rect(midSplitter.xMax, fullRect.y, inspectorWidth, fullRect.height); if (selection != null) { GUILayout.BeginArea(inspectorRect); { if (selection is LanguageSelect lang) OnDrawLanguageInspector(lang); else if (selection is SectionSelect sec) OnDrawSectionInspector(sec); else if (selection is ItemSelect item) OnDrawSectionItemInspector(item); } GUILayout.EndArea(); } } private void HandleResize(ref float targetWidth, Rect splitterRect, ref bool isResizing) { Event e = Event.current; switch (e.type) { case EventType.MouseDown: if (splitterRect.Contains(e.mousePosition)) { isResizing = true; e.Use(); } break; case EventType.MouseDrag: if (isResizing) { targetWidth += e.delta.x; targetWidth = Mathf.Max(100f, targetWidth); // 设置最小宽度 e.Use(); Repaint(); } break; case EventType.MouseUp: if (isResizing) { isResizing = false; e.Use(); } break; } } private void OnDrawSectionInspector(SectionSelect section) { // section name change EditorGUI.BeginChangeCheck(); { section.Section.Name = EditorGUILayout.TextField("Name", section.Section.Name); } if (EditorGUI.EndChangeCheck()) { section.TreeViewItem.displayName = section.Section.Name; } using (new EditorGUI.DisabledGroupScope(true)) { int childerCount = section.TreeViewItem.children?.Count ?? 0; EditorGUILayout.IntField(new GUIContent("Keys"), childerCount); } EditorGUILayout.Space(2); EditorDrawing.Separator(); EditorGUILayout.Space(1); using (new EditorGUI.DisabledGroupScope(true)) { EditorGUILayout.LabelField("Id: " + section.Section.Id, EditorStyles.miniBoldLabel); } } private void OnDrawSectionItemInspector(ItemSelect itemSelect) { var item = itemSelect.Item; EditorGUILayout.LabelField("Editing Key Across Languages", EditorStyles.boldLabel); // 显示 Key 和 ID(只读) using (new EditorGUI.DisabledGroupScope(true)) { EditorGUILayout.TextField("Key", item.Key); EditorGUILayout.LabelField("Id: " + item.Id); } EditorGUILayout.Space(4); EditorDrawing.Separator(); EditorGUILayout.Space(4); // 遍历所有语言 foreach (var lang in windowData.Languages) { if (lang.Entry.Asset == null) continue; var languageName = lang.Entry.LanguageName; TempSheetItem targetItem = null; // 在语言的 TableSheet 里找到对应的 Item foreach (var section in lang.TableSheet) { targetItem = section.Items.Find(i => i.Id == item.Id); if (targetItem != null) break; } if (targetItem == null) { EditorGUILayout.HelpBox($"Language [{languageName}] does not contain this key.", MessageType.Info); continue; } // 绘制语言名标题 EditorGUILayout.LabelField(languageName, EditorStyles.miniBoldLabel); // 多行文本框 EditorGUI.BeginChangeCheck(); targetItem.Value = EditorGUILayout.TextArea(targetItem.Value, GUILayout.MinHeight(40)); if (EditorGUI.EndChangeCheck()) { // 标记已修改 EditorUtility.SetDirty(lang.Entry.Asset); } EditorGUILayout.Space(6); } } private void OnDrawLanguageInspector(LanguageSelect selection) { var language = selection.Language; var entry = language.Entry; var treeView = selection.TreeViewItem; using (new EditorGUI.DisabledGroupScope(entry.Asset == null)) { // Draw search field EditorGUILayout.Space(); GUIContent expandText = new GUIContent("Expand"); float expandWidth = miniLabelButton.CalcSize(expandText).x; var searchRect = EditorGUILayout.GetControlRect(); searchRect.xMax -= (expandWidth + 2f); searchString = searchField.OnGUI(searchRect, searchString); Rect expandRect = new Rect(searchRect.xMax + 2f, searchRect.y, expandWidth, searchRect.height); expandRect.y -= 1f; using (new EditorDrawing.BackgroundColorScope("#F7E987")) { if (GUI.Button(expandRect, expandText, miniLabelButton)) { globalExpanded = !globalExpanded; foreach (var section in language.TableSheet) { section.Reference.IsExpanded = globalExpanded; } } } if (entry.Asset != null) { // Draw localization data scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); { foreach (var section in GetSearchResult(language, searchString)) { DrawLocalizationKey(section); } } EditorGUILayout.EndScrollView(); } else { EditorGUILayout.HelpBox("To begin editing localization data, you must first assign a localization asset.", MessageType.Warning); } } } private void DrawLocalizationKey(TempSheetSection section) { if (section.Items == null || section.Items.Count == 0) return; using (new EditorDrawing.BorderBoxScope(false)) { string sectionName = section.Name.Replace(" ", ""); section.Reference.IsExpanded = EditorGUILayout.Foldout(section.Reference.IsExpanded, new GUIContent(sectionName), true, EditorDrawing.Styles.miniBoldLabelFoldout); // Show section keys when expanded if (section.Reference.IsExpanded) { foreach (var item in section.Items) { string keyName = item.Key.Replace(" ", ""); string key = sectionName + "." + keyName; if (IsMultiline(item.Value)) key += " (Multiline)"; using (new EditorGUILayout.VerticalScope(GUI.skin.box)) { // Display the expandable toggle using (new EditorGUILayout.HorizontalScope(GUI.skin.box)) { item.IsExpanded = EditorGUILayout.Foldout(item.IsExpanded, new GUIContent(key), true, EditorDrawing.Styles.miniBoldLabelFoldout); } if (item.IsExpanded) { // Show TextArea when expanded float height = (EditorGUIUtility.standardVerticalSpacing + EditorGUIUtility.singleLineHeight) * 3; height += EditorGUIUtility.standardVerticalSpacing; item.Scroll = EditorGUILayout.BeginScrollView(item.Scroll, GUILayout.Height(height)); item.Value = EditorGUILayout.TextArea(item.Value, GUILayout.ExpandHeight(true)); EditorGUILayout.EndScrollView(); } else { // Show TextField when collapsed item.Value = EditorGUILayout.TextField(item.Value); } } } } } EditorGUILayout.Space(1f); } private IEnumerable GetSearchResult(TempLanguageData languageData, string search) { if (!string.IsNullOrEmpty(search)) { List searchResult = new(); foreach (var section in languageData.TableSheet) { List sectionItems = new(); string sectionName = section.Name.Replace(" ", ""); foreach (var item in section.Items) { string keyName = item.Key.Replace(" ", ""); string key = sectionName + "." + keyName; if (key.Contains(search)) sectionItems.Add(item); } searchResult.Add(new TempSheetSection() { Items = sectionItems, Reference = section.Reference }); } return searchResult; } return languageData.TableSheet; } private bool IsMultiline(string text) { return text.Contains("\n") || text.Contains("\r"); } private void BuildLocalizationTable() { // 1. build table sheet currentTable.TableSheet = new(); foreach (var section in windowData.TableSheet) { GameLocaizationTable.TableData tableData = new GameLocaizationTable.TableData(section.Name, section.Id); foreach (var item in section.Items) { GameLocaizationTable.SheetItem sheetItem = new GameLocaizationTable.SheetItem(item.Key, item.Id, item.isGen); tableData.SectionSheet.Add(sheetItem); } currentTable.TableSheet.Add(tableData); } // 2. build table sheet for each language IList languages = new List(); foreach (var language in windowData.Languages) { if (language.Entry.Asset == null) continue; LocalizationLanguage asset = language.Entry.Asset; IList strings = new List(); foreach (var section in language.TableSheet) { string sectionKey = section.Name.Replace(" ", ""); foreach (var item in section.Items) { string itemKey = item.Key.Replace(" ", ""); string key = sectionKey + "." + itemKey; strings.Add(new() { SectionId = section.Id, EntryId = item.Id, Key = key, Value = item.Value }); } } asset.LanguageName = language.Entry.LanguageName; asset.Strings = new(strings); languages.Add(asset); EditorUtility.SetDirty(asset); } currentTable.Languages = new(languages); } } }