167 lines
5.4 KiB
C#
167 lines
5.4 KiB
C#
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);
|
||
}
|
||
}
|
||
}
|