using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; using BaseGames.Localization; namespace BaseGames.Editor.Localization { /// /// 带搜索框的本地化 Key 选择器窗口。 /// 替代 GenericMenu,支持 1000+ 条目的快速模糊搜索(key 和译文均可搜索)。 /// /// 键盘操作: /// ↑ / ↓ — 上下移动高亮行 /// Enter — 确认选择当前高亮行 /// Esc — 关闭窗口(不选中任何 key) /// /// 用法: /// /// LocalizationKeyPickerWindow.Show(table, currentKey, selectedKey => { /// _keyProp.stringValue = selectedKey; /// serializedObject.ApplyModifiedProperties(); /// }); /// /// public class LocalizationKeyPickerWindow : EditorWindow { // ── 状态 ────────────────────────────────────────────────────────────── private string _searchText = ""; private Vector2 _scroll; private List<(string key, string preview)> _allEntries = new(); private List<(string key, string preview)> _filtered = new(); private Action _onSelected; private string _currentKey; private int _hoveredIndex = -1; private int _keyboardIndex = -1; // 键盘当前高亮行 // 预缓存样式,避免每帧 new GUIStyle private GUIStyle _keyStyle; private GUIStyle _previewStyle; private const float RowHeight = 40f; private const float SearchHeight = 22f; // ── 公开入口 ────────────────────────────────────────────────────────── /// /// 打开 Key 选择器窗口。 /// /// 要从哪张表加载 Key(先查简体中文,找不到查英文)。 /// 当前已选 Key(高亮显示)。 /// 选中 Key 后的回调。 public static void Show(string table, string currentKey, Action onSelected) { var dict = LocalizationManager.GetEditorTable(Language.ChineseSimplified, table) ?? LocalizationManager.GetEditorTable(Language.English, table); if (dict == null || dict.Count == 0) { EditorUtility.DisplayDialog("Key 选择器", $"表「{table}」尚无可用 Key。\n" + $"请先用「BaseGames / Localization / 表格编辑器」或在 {LocalizationPaths.DataRoot}/{{语言}}/{table}.json 中添加条目。", "确定"); return; } var win = CreateInstance(); win.titleContent = new GUIContent($"Key 选择器 — {table}"); win.minSize = new Vector2(440, 520); win._currentKey = currentKey; win._onSelected = onSelected; win._allEntries = dict .Select(kvp => (kvp.Key, kvp.Value)) .OrderBy(t => t.Item1, StringComparer.Ordinal) .ToList(); win.ApplyFilter(); // 把键盘索引初始化到当前 key 所在行 int idx = win._filtered.FindIndex(t => t.key == currentKey); if (idx >= 0) { win._keyboardIndex = idx; win._scroll = new Vector2(0, idx * RowHeight); } win.ShowUtility(); } // ── GUI ─────────────────────────────────────────────────────────────── private void OnGUI() { EnsureStyles(); HandleKeyboardInput(); // 在绘制之前处理按键,当帧即生效 DrawSearchBar(); DrawEntryCount(); DrawList(); } // ── 键盘导航 ────────────────────────────────────────────────────────── private void HandleKeyboardInput() { var e = Event.current; if (e.type != EventType.KeyDown) return; switch (e.keyCode) { case KeyCode.UpArrow: MoveKeyboardSelection(-1); e.Use(); break; case KeyCode.DownArrow: MoveKeyboardSelection(+1); e.Use(); break; case KeyCode.Return: case KeyCode.KeypadEnter: if (_keyboardIndex >= 0 && _keyboardIndex < _filtered.Count) { _onSelected?.Invoke(_filtered[_keyboardIndex].key); e.Use(); Close(); } break; case KeyCode.Escape: e.Use(); Close(); break; } } private void MoveKeyboardSelection(int delta) { if (_filtered.Count == 0) return; _keyboardIndex = _keyboardIndex < 0 ? (delta > 0 ? 0 : _filtered.Count - 1) : Mathf.Clamp(_keyboardIndex + delta, 0, _filtered.Count - 1); ScrollToKeyboardIndex(); Repaint(); } /// 调整滚动位置,保证 _keyboardIndex 行始终在可见视口内。 private void ScrollToKeyboardIndex() { float viewportH = position.height - SearchHeight - 30f; float rowTop = _keyboardIndex * RowHeight; float rowBot = rowTop + RowHeight; if (rowTop < _scroll.y) _scroll.y = rowTop; else if (rowBot > _scroll.y + viewportH) _scroll.y = rowBot - viewportH; } // ── 搜索栏 ──────────────────────────────────────────────────────────── private void DrawSearchBar() { EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); GUI.SetNextControlName("SearchField"); var newSearch = EditorGUILayout.TextField(_searchText, EditorStyles.toolbarSearchField); if (GUILayout.Button("✕", EditorStyles.toolbarButton, GUILayout.Width(22))) newSearch = ""; EditorGUILayout.EndHorizontal(); if (newSearch != _searchText) { _searchText = newSearch; _hoveredIndex = -1; _keyboardIndex = -1; ApplyFilter(); } } private void DrawEntryCount() { EditorGUILayout.LabelField( $"共 {_filtered.Count} 个结果(总 {_allEntries.Count} 条) | ↑↓ 导航 Enter 选中 Esc 关闭", EditorStyles.centeredGreyMiniLabel); EditorGUILayout.Space(2); } // ── 列表(虚拟渲染)────────────────────────────────────────────────── private void DrawList() { float viewportH = position.height - SearchHeight - 30f; _scroll = EditorGUILayout.BeginScrollView(_scroll, GUILayout.Height(viewportH)); int firstVisible = Mathf.Max(0, (int)(_scroll.y / RowHeight) - 1); int lastVisible = Mathf.Min(_filtered.Count - 1, (int)((_scroll.y + viewportH) / RowHeight) + 1); // 顶部占位(未渲染行) if (firstVisible > 0) GUILayout.Space(firstVisible * RowHeight); for (int i = firstVisible; i <= lastVisible && i < _filtered.Count; i++) { var (key, preview) = _filtered[i]; bool isCurrent = string.Equals(key, _currentKey, StringComparison.Ordinal); bool isKeyboard = i == _keyboardIndex; bool isHovered = i == _hoveredIndex; var rowRect = GUILayoutUtility.GetRect(0, RowHeight, GUILayout.ExpandWidth(true)); // 背景优先级:当前选中 > 键盘高亮 > 鼠标悬停 Color bg = isCurrent ? new Color(0.25f, 0.55f, 1f, 0.25f) : isKeyboard ? new Color(0.40f, 0.75f, 0.4f, 0.20f) : isHovered ? new Color(1f, 1f, 1f, 0.05f) : Color.clear; if (bg.a > 0) EditorGUI.DrawRect(rowRect, bg); // 当前 key 左边蓝色竖条 if (isCurrent) EditorGUI.DrawRect(new Rect(rowRect.x, rowRect.y, 3, rowRect.height), new Color(0.3f, 0.7f, 1f, 1f)); // 键盘选中绿色竖条 else if (isKeyboard) EditorGUI.DrawRect(new Rect(rowRect.x, rowRect.y, 3, rowRect.height), new Color(0.4f, 0.9f, 0.4f, 1f)); // Key 文本 EditorGUI.LabelField( new Rect(rowRect.x + 8, rowRect.y + 4, rowRect.width - 12, 18), key, _keyStyle); // 预览文本(绿色) string previewText = preview.Length > 60 ? preview[..60] + "…" : preview; EditorGUI.LabelField( new Rect(rowRect.x + 8, rowRect.y + 22, rowRect.width - 12, 14), previewText, _previewStyle); // 鼠标交互 var ev = Event.current; if (rowRect.Contains(ev.mousePosition)) { if (_hoveredIndex != i) { _hoveredIndex = i; Repaint(); } if (ev.type == EventType.MouseDown && ev.button == 0) { _onSelected?.Invoke(key); ev.Use(); Close(); return; } } } // 底部占位(未渲染行) int remaining = _filtered.Count - lastVisible - 1; if (remaining > 0) GUILayout.Space(remaining * RowHeight); EditorGUILayout.EndScrollView(); } // ── 辅助 ────────────────────────────────────────────────────────────── private void ApplyFilter() { if (string.IsNullOrEmpty(_searchText)) { _filtered = new List<(string, string)>(_allEntries); return; } _filtered = _allEntries .Where(t => t.key.IndexOf(_searchText, StringComparison.OrdinalIgnoreCase) >= 0 || t.preview.IndexOf(_searchText, StringComparison.OrdinalIgnoreCase) >= 0) .ToList(); } private void EnsureStyles() { _keyStyle ??= new GUIStyle(EditorStyles.boldLabel) { fontSize = 11, }; _previewStyle ??= new GUIStyle(EditorStyles.miniLabel) { normal = { textColor = new Color(0.5f, 0.88f, 0.5f) }, fontSize = 10, }; } } }