Files
Cielonos/Assets/Scripts/Settings/UI/SettingsEntryKeyBinding.cs

167 lines
5.4 KiB
C#
Raw Normal View History

2026-06-12 17:11:39 -04:00
using System;
using TMPro;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
namespace Cielonos.Settings.UI
{
/// <summary>
/// 键位绑定条目,用于 <see cref="KeyBindingPage"/> 中的每一行。
/// <para>
/// 显示:[Action 名称] [当前按键文本] [重新绑定按钮]
/// 点击重新绑定按钮后进入监听模式,捕获下一个按键输入并更新绑定。
/// </para>
/// </summary>
public class SettingsEntryKeyBinding : MonoBehaviour
{
[SerializeField] private TMP_Text actionNameText;
[SerializeField] private TMP_Text currentKeyText;
[SerializeField] private Button rebindButton;
[SerializeField] private TMP_Text rebindButtonText;
[Header("Visuals")]
[SerializeField] private string waitingText = "...";
[SerializeField] private GameObject waitingOverlay;
private InputAction inputAction;
private int bindingIndex;
private InputActionRebindingExtensions.RebindingOperation rebindOperation;
/// <summary>绑定完成时触发。</summary>
public event Action OnBindingChanged;
/// <summary>
/// 初始化键位绑定条目。
/// </summary>
/// <param name="action">要绑定的 InputAction。</param>
/// <param name="targetBindingIndex">
/// 要重新绑定的 binding 索引。
/// 对于非 composite 的 action通常为 0。
/// </param>
/// <param name="displayName">条目显示名称(可为 null则使用 action 名)。</param>
public void Initialize(InputAction action, int targetBindingIndex, string displayName = null)
{
inputAction = action;
bindingIndex = targetBindingIndex;
if (actionNameText != null)
actionNameText.text = displayName ?? FormatActionName(action.name);
if (rebindButton != null)
rebindButton.onClick.AddListener(StartRebind);
RefreshDisplay();
}
/// <summary>
/// 刷新当前按键的显示文本。
/// </summary>
public void RefreshDisplay()
{
if (inputAction == null || currentKeyText == null) return;
string displayString = inputAction.bindings[bindingIndex].ToDisplayString(
InputBinding.DisplayStringOptions.DontUseShortDisplayNames);
currentKeyText.text = displayString;
}
/// <summary>
/// 开始交互式重新绑定。
/// </summary>
private void StartRebind()
{
if (inputAction == null) return;
// 显示等待提示
SetWaitingState(true);
// 先禁用 action 以允许重新绑定
inputAction.Disable();
rebindOperation = inputAction.PerformInteractiveRebinding(bindingIndex)
.WithControlsExcluding("<Mouse>/position")
.WithControlsExcluding("<Mouse>/delta")
.WithControlsExcluding("<Pointer>/position")
.WithControlsExcluding("<Pointer>/delta")
.WithCancelingThrough("<Keyboard>/escape")
.OnMatchWaitForAnother(0.1f)
.OnComplete(operation => FinishRebind(true))
.OnCancel(operation => FinishRebind(false))
.Start();
}
/// <summary>
/// 重新绑定完成或取消后的处理。
/// </summary>
private void FinishRebind(bool completed)
{
rebindOperation?.Dispose();
rebindOperation = null;
inputAction.Enable();
SetWaitingState(false);
RefreshDisplay();
if (completed)
{
OnBindingChanged?.Invoke();
}
}
/// <summary>
/// 重置此条目的绑定到默认值。
/// </summary>
public void ResetToDefault()
{
if (inputAction == null) return;
inputAction.RemoveBindingOverride(bindingIndex);
RefreshDisplay();
OnBindingChanged?.Invoke();
}
private void SetWaitingState(bool isWaiting)
{
if (currentKeyText != null)
currentKeyText.text = isWaiting ? waitingText : "";
if (rebindButton != null)
rebindButton.interactable = !isWaiting;
if (waitingOverlay != null)
waitingOverlay.SetActive(isWaiting);
}
/// <summary>
/// 将 ActionName (camelCase) 格式化为可读文本。
/// </summary>
private static string FormatActionName(string actionName)
{
if (string.IsNullOrEmpty(actionName)) return actionName;
var sb = new System.Text.StringBuilder(actionName.Length + 4);
sb.Append(char.ToUpper(actionName[0]));
for (int i = 1; i < actionName.Length; i++)
{
char c = actionName[i];
if (char.IsUpper(c) && i > 0 && char.IsLower(actionName[i - 1]))
sb.Append(' ');
sb.Append(c);
}
return sb.ToString();
}
private void OnDestroy()
{
rebindOperation?.Dispose();
if (rebindButton != null)
rebindButton.onClick.RemoveListener(StartRebind);
}
}
}