Files
zeling_v2/Assets/_Game/Scripts/Editor/Localization/LocalizationKeyPickerWindow.cs
2026-06-06 09:00:11 +08:00

299 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using BaseGames.Localization;
namespace BaseGames.Editor.Localization
{
/// <summary>
/// 带搜索框的本地化 Key 选择器窗口。
/// 替代 <c>GenericMenu</c>,支持 1000+ 条目的快速模糊搜索key 和译文均可搜索)。
///
/// 键盘操作:
/// ↑ / ↓ — 上下移动高亮行
/// Enter — 确认选择当前高亮行
/// Esc — 关闭窗口(不选中任何 key
///
/// 用法:
/// <code>
/// LocalizationKeyPickerWindow.Show(table, currentKey, selectedKey => {
/// _keyProp.stringValue = selectedKey;
/// serializedObject.ApplyModifiedProperties();
/// });
/// </code>
/// </summary>
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<string> _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;
// ── 公开入口 ──────────────────────────────────────────────────────────
/// <summary>
/// 打开 Key 选择器窗口。
/// </summary>
/// <param name="table">要从哪张表加载 Key先查简体中文找不到查英文。</param>
/// <param name="currentKey">当前已选 Key高亮显示。</param>
/// <param name="onSelected">选中 Key 后的回调。</param>
public static void Show(string table, string currentKey, Action<string> 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<LocalizationKeyPickerWindow>();
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();
}
/// <summary>调整滚动位置,保证 _keyboardIndex 行始终在可见视口内。</summary>
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,
};
}
}
}