299 lines
12 KiB
C#
299 lines
12 KiB
C#
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,
|
||
};
|
||
}
|
||
}
|
||
}
|