Passion & UI
This commit is contained in:
166
Assets/Scripts/Settings/UI/SettingsEntryKeyBinding.cs
Normal file
166
Assets/Scripts/Settings/UI/SettingsEntryKeyBinding.cs
Normal file
@@ -0,0 +1,166 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user