DCS/ruiyiweiUX/Assets/Scripts/UI/Dialogs/CustomKeyboard.cs

507 lines
18 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 TMPro;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
public enum KeyboardLayout
{
Default,
Numeric,
Hex,
Pinyin // 拼音键盘(支持中文输入)
}
/// <summary>
/// 动态生成自定义键盘并将按键事件转发给 InputDialog
/// 用法:把该脚本挂在 CustomKeyboardPanel 上,面板内部应包含一个用于放按键的容器(可不设置则使用自身)。
/// </summary>
public class CustomKeyboard : MonoBehaviour
{
public Transform KeysContainer; // 按键放置容器
[Tooltip("如果设置了 KeyPrefab则在运行时使用该 prefab 来生成按键;如果为空,脚本会创建简单的 GameObject仅在运行时。")]
public GameObject KeyPrefab;
public Vector2 KeySize = new Vector2(60, 60);
public Vector2 KeySpacing = new Vector2(6, 6);
private object _target; // 可以是 InputDialog 或 KeyboardInputAdapter
private KeyboardLayout _layout = KeyboardLayout.Default;
private bool _isPasswordMode = false;
private bool _isUpperCase = false; // 拼音键盘大小写切换
private GridLayoutGroup _grid;
// 拼音输入相关
private string _pinyinBuffer = ""; // 拼音缓冲区
private Transform _candidateContainer; // 候选字容器
public Action OnKeyboardSizeChanged; // 键盘大小变化回调
// 翻页相关
private int _currentPage = 0; // 当前页码0基索引
private const int _candidatesPerPage = 7; // 每页显示的候选字数量
private string[] _allCandidates = null; // 当前拼音的所有候选字
void Awake()
{
// 如果 KeysContainer 未设置,尝试查找;否则 InputDialog.CreateDynamicKeyboard() 已设置
if (KeysContainer == null)
{
var child = this.transform.Find("KeysContainer");
if (child != null)
KeysContainer = child;
else
KeysContainer = this.transform;
}
_grid = KeysContainer.GetComponent<GridLayoutGroup>();
// 如果容器本身没有 GridLayoutGroup 则添加一个(仅在这里需要,动态创建时已添加)
if (_grid == null)
{
_grid = KeysContainer.gameObject.AddComponent<GridLayoutGroup>();
_grid.cellSize = KeySize;
_grid.spacing = KeySpacing;
}
}
public void SetTarget(object target, KeyboardLayout layout, bool isPassword)
{
_target = target;
_layout = layout;
_isPasswordMode = isPassword;
_isUpperCase = false;
_pinyinBuffer = "";
RebuildKeys();
}
public void RebuildKeys()
{
// 注意: 修改 Prefab Asset例如在 Project 视图中打开的 Prefab
// 直接对其 Transform 做 SetParent/Destroy 会导致 Unity 抛出错误:
// "Setting the parent of a transform which resides in a Prefab Asset is disabled to prevent data corruption"
// 因此此方法只会在运行时Play mode修改场景实例的子对象。
if (!Application.isPlaying)
{
Debug.Log("RebuildKeys skipped: only runs in Play mode to avoid editing prefab assets. For editing prefab assets use the Editor -> Prefab editing workflow.");
return;
}
// 清除已有按键(运行时安全)
for (int i = KeysContainer.childCount - 1; i >= 0; --i)
{
var child = KeysContainer.GetChild(i);
if (Application.isPlaying) Destroy(child.gameObject);
else DestroyImmediate(child.gameObject);
}
switch (_layout)
{
case KeyboardLayout.Numeric:
BuildNumeric();
break;
case KeyboardLayout.Hex:
BuildHex();
break;
case KeyboardLayout.Pinyin:
BuildPinyin();
break;
default:
BuildDefault();
break;
}
// add control row: Clear Backspace Confirm
BuildControlRow();
}
private void BuildKey(string label, Action onClick)
{
// 优先使用 KeyPrefab需在 Inspector 中赋值),在运行时用 Instantiate 创建实例并设置父物体——这是安全的。
GameObject go = null;
if (KeyPrefab != null)
{
go = Instantiate(KeyPrefab, KeysContainer, false);
go.name = "Key_" + label;
// 如果 prefab 没有自动设置文本,需要查找并设置
var tmp = go.GetComponentInChildren<TextMeshProUGUI>();
if (tmp != null) tmp.text = label;
var btn = go.GetComponent<Button>();
if (btn != null)
{
btn.onClick.RemoveAllListeners();
btn.onClick.AddListener(() => onClick?.Invoke());
}
}
else
{
// 只有在运行时才创建临时 GameObject编辑模式下请勿调用此逻辑以避免修改 Prefab Asset
if (!Application.isPlaying)
{
Debug.LogWarning("Cannot create key GameObject in edit mode without KeyPrefab. Assign KeyPrefab or run in Play mode.");
return;
}
go = new GameObject("Key_" + label, typeof(RectTransform), typeof(Image), typeof(Button));
go.transform.SetParent(KeysContainer, false);
var img = go.GetComponent<Image>();
img.color = new Color(0.15f, 0.17f, 0.2f, 1f);
var btn = go.GetComponent<Button>();
btn.onClick.AddListener(() => onClick?.Invoke());
var txtGO = new GameObject("Label", typeof(RectTransform));
txtGO.transform.SetParent(go.transform, false);
var txt = txtGO.AddComponent<TextMeshProUGUI>();
txt.text = label;
txt.alignment = TextAlignmentOptions.Center;
txt.color = Color.white;
txt.fontSize = 24;
var rt = txt.GetComponent<RectTransform>();
rt.anchorMin = Vector2.zero;
rt.anchorMax = Vector2.one;
rt.offsetMin = Vector2.zero;
rt.offsetMax = Vector2.zero;
}
}
private void BuildNumeric()
{
string[] keys = new string[] { "1","2","3","4","5","6","7","8","9","0",".","+" };
foreach (var k in keys)
{
BuildKey(k, () => CallOnKeyPressed(k));
}
}
private void BuildPinyin()
{
// 拼音键盘 - 26个字母用于拼音输入
string[] letters = new string[] { "q","w","e","r","t","y","u","i","o","p","a","s","d","f","g","h","j","k","l","z","x","c","v","b","n","m" };
foreach (var letter in letters)
{
BuildKey(letter, () => OnPinyinLetterPressed(letter));
}
// 添加空格键(选择第一个候选字)
BuildKey("空格", () => SelectCandidate(0));
}
/// <summary>
/// 拼音字母按下
/// </summary>
private void OnPinyinLetterPressed(string letter)
{
_pinyinBuffer += letter;
UpdatePinyinCandidates();
}
/// <summary>
/// 更新拼音候选字
/// </summary>
private void UpdatePinyinCandidates()
{
// 清除旧的候选字
if (_candidateContainer != null)
{
for (int i = _candidateContainer.childCount - 1; i >= 0; --i)
{
if (Application.isPlaying)
Destroy(_candidateContainer.GetChild(i).gameObject);
else
DestroyImmediate(_candidateContainer.GetChild(i).gameObject);
}
}
else
{
// 创建候选字容器(在控制按钮上方)
var candidateGO = new GameObject("CandidateContainer", typeof(RectTransform));
candidateGO.transform.SetParent(KeysContainer.parent, false);
_candidateContainer = candidateGO.transform;
var candidateRT = candidateGO.GetComponent<RectTransform>();
candidateRT.anchorMin = new Vector2(0, 1);
candidateRT.anchorMax = new Vector2(1, 1);
candidateRT.pivot = new Vector2(0.5f, 1);
candidateRT.anchoredPosition = new Vector2(0, 50);
candidateRT.sizeDelta = new Vector2(0, 60);
// 添加背景
var bg = candidateGO.AddComponent<Image>();
bg.color = new Color(0.12f, 0.14f, 0.16f, 1f);
// 添加水平布局
var layout = candidateGO.AddComponent<HorizontalLayoutGroup>();
layout.spacing = 5;
layout.padding = new RectOffset(10, 10, 5, 5);
layout.childControlWidth = false;
layout.childControlHeight = true;
layout.childForceExpandWidth = false;
layout.childForceExpandHeight = true;
}
// 显示拼音缓冲区
if (!string.IsNullOrEmpty(_pinyinBuffer))
{
// 获取候选汉字
_allCandidates = PinyinInputManager.GetCandidates(_pinyinBuffer);
if (_allCandidates != null && _allCandidates.Length > 0)
{
// 计算总页数
int totalPages = Mathf.CeilToInt((float)_allCandidates.Length / _candidatesPerPage);
// 确保页码在有效范围内
_currentPage = Mathf.Clamp(_currentPage, 0, totalPages - 1);
CreateCandidateText($"拼音: {_pinyinBuffer} ({_currentPage + 1}/{totalPages})", () => {});
// 添加上一页按钮
// if (_currentPage > 0)
// {
CreateCandidateButton("← 上页", () => {
if (_currentPage > 0)
{
_currentPage--;
UpdatePinyinCandidates();
}
});
// }
// 计算当前页的候选字范围
int startIndex = _currentPage * _candidatesPerPage;
int endIndex = Mathf.Min(startIndex + _candidatesPerPage, _allCandidates.Length);
// 显示当前页的候选字
for (int i = startIndex; i < endIndex; i++)
{
int globalIndex = i; // 全局索引
int displayNumber = (i - startIndex) + 1; // 显示编号1-8
string candidate = _allCandidates[i];
CreateCandidateButton($"{displayNumber}.{candidate}", () => SelectCandidateByGlobalIndex(globalIndex));
}
// 添加下一页按钮
if (_currentPage < totalPages - 1)
{
CreateCandidateButton("下页 →", () => {
_currentPage++;
UpdatePinyinCandidates();
});
}
}else{
// 显示拼音
CreateCandidateText($"拼音: {_pinyinBuffer} 暂无数据", () => {});
}
}
else
{
// 清空候选字和重置页码
_allCandidates = null;
_currentPage = 0;
}
}
/// <summary>
/// 创建候选字文本
/// </summary>
private void CreateCandidateText(string text, Action onClick)
{
var textGO = new GameObject("CandidateText", typeof(RectTransform));
textGO.transform.SetParent(_candidateContainer, false);
var rt = textGO.GetComponent<RectTransform>();
rt.sizeDelta = new Vector2(150, 50);
var tmp = textGO.AddComponent<TextMeshProUGUI>();
tmp.text = text;
tmp.fontSize = 20;
// 尝试加载中文字体,如果失败则使用默认字体
var chineseFont = Resources.Load<TMP_FontAsset>("Fonts/SIMLI SDF");
if (chineseFont != null)
tmp.font = chineseFont;
tmp.color = Color.cyan;
tmp.alignment = TextAlignmentOptions.MidlineLeft;
}
/// <summary>
/// 创建候选字按钮
/// </summary>
private void CreateCandidateButton(string text, Action onClick)
{
var btnGO = new GameObject("CandidateBtn", typeof(RectTransform), typeof(Image), typeof(Button));
btnGO.transform.SetParent(_candidateContainer, false);
var rt = btnGO.GetComponent<RectTransform>();
// 根据文本长度调整宽度(翻页按钮需要更宽)
float width = text.Contains("上页") || text.Contains("下页") ? 80 : 70;
rt.sizeDelta = new Vector2(width, 50);
var img = btnGO.GetComponent<Image>();
img.color = new Color(0.2f, 0.25f, 0.3f, 1f);
var btn = btnGO.GetComponent<Button>();
btn.onClick.AddListener(() => onClick?.Invoke());
var txtGO = new GameObject("Label", typeof(RectTransform));
txtGO.transform.SetParent(btnGO.transform, false);
var tmp = txtGO.AddComponent<TextMeshProUGUI>();
tmp.text = text;
// 尝试加载中文字体
var chineseFont = Resources.Load<TMP_FontAsset>("Fonts/SIMLI SDF");
if (chineseFont != null)
tmp.font = chineseFont;
tmp.alignment = TextAlignmentOptions.Center;
tmp.color = Color.white;
tmp.fontSize = 22;
var txtRT = tmp.GetComponent<RectTransform>();
txtRT.anchorMin = Vector2.zero;
txtRT.anchorMax = Vector2.one;
txtRT.offsetMin = Vector2.zero;
txtRT.offsetMax = Vector2.zero;
}
/// <summary>
/// 选择候选字(通过全局索引)
/// </summary>
private void SelectCandidateByGlobalIndex(int globalIndex)
{
if (_allCandidates != null && globalIndex >= 0 && globalIndex < _allCandidates.Length)
{
// 输入选中的汉字
CallOnKeyPressed(_allCandidates[globalIndex]);
// 清空拼音缓冲区和重置页码
_pinyinBuffer = "";
_allCandidates = null;
_currentPage = 0;
UpdatePinyinCandidates();
}
}
/// <summary>
/// 选择候选字(用于空格键,选择第一个)
/// </summary>
private void SelectCandidate(int relativeIndex)
{
if (string.IsNullOrEmpty(_pinyinBuffer))
{
// 如果没有拼音缓冲,空格键输入空格
CallOnKeyPressed(" ");
return;
}
// 计算全局索引(当前页的第一个 + 相对索引)
int globalIndex = _currentPage * _candidatesPerPage + relativeIndex;
SelectCandidateByGlobalIndex(globalIndex);
}
private void BuildHex()
{
string[] keys = new string[] { "0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F" };
foreach (var k in keys) BuildKey(k, () => CallOnKeyPressed(k));
}
private void BuildDefault()
{
// 简易默认键盘包含数字和常用符号
string[] keys = new string[] {
"1","2","3","4","5","6","7","8","9","0",
"q","w","e","r","t","y","u","i","o","p",
"a","s","d","f","g","h","j","k","l",
"z","x","c","v","b","n","m",
"-","_","@" };
foreach (var k in keys) BuildKey(k, () => CallOnKeyPressed(k));
}
private void BuildControlRow()
{
// Add Clear
BuildKey("Clear", () => {
if (_layout == KeyboardLayout.Pinyin)
{
_pinyinBuffer = "";
UpdatePinyinCandidates();
}
CallOnClearPressed();
});
// Backspace
BuildKey("←", () => {
if (_layout == KeyboardLayout.Pinyin && !string.IsNullOrEmpty(_pinyinBuffer))
{
_pinyinBuffer = _pinyinBuffer.Substring(0, _pinyinBuffer.Length - 1);
UpdatePinyinCandidates();
}
else
{
CallOnBackspacePressed();
}
});
// Confirm -> trigger confirm button and close keyboard
BuildKey("OK", () => {
CallConfirmButton();
});
}
// Helper methods to call target methods safely using reflection or direct cast
private void CallOnKeyPressed(string key)
{
if (_target == null) return;
var method = _target.GetType().GetMethod("OnKeyPressed");
if (method != null)
{
method.Invoke(_target, new object[] { key });
}
}
private void CallOnBackspacePressed()
{
if (_target == null) return;
var method = _target.GetType().GetMethod("OnBackspacePressed");
if (method != null)
{
method.Invoke(_target, null);
}
}
private void CallOnClearPressed()
{
if (_target == null) return;
var method = _target.GetType().GetMethod("OnClearPressed");
if (method != null)
{
method.Invoke(_target, null);
}
}
private void CallConfirmButton()
{
if (_target == null) return;
var confirmButtonProp = _target.GetType().GetProperty("ConfirmButton");
if (confirmButtonProp != null)
{
var btn = confirmButtonProp.GetValue(_target) as Button;
if (btn != null)
{
btn.onClick.Invoke();
return;
}
}
// 如果没有 ConfirmButton 属性,尝试直接调用管理器的 ConfirmInput
if (CustomKeyboardManager.Instance != null)
{
CustomKeyboardManager.Instance.ConfirmInput();
}
}
}