2025-12-09 20:31:44 +08:00
|
|
|
|
using System;
|
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
|
using TMPro;
|
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
using UnityEngine.InputSystem;
|
|
|
|
|
|
using UnityEngine.UI;
|
|
|
|
|
|
|
|
|
|
|
|
public class TestRebindScript : MonoBehaviour
|
|
|
|
|
|
{
|
2025-12-10 17:38:31 +08:00
|
|
|
|
[Header("UI")] public UXButton btn;
|
2025-12-09 20:31:44 +08:00
|
|
|
|
public TextMeshProUGUI bindKeyText;
|
|
|
|
|
|
public Image targetImage;
|
|
|
|
|
|
|
|
|
|
|
|
[Tooltip("如果不使用 actionReference,则用 name 在全局 manager 查找")]
|
|
|
|
|
|
public string actionName = "movement";
|
|
|
|
|
|
|
2025-12-10 17:38:31 +08:00
|
|
|
|
[Header("Optional composite part (WASD style)")] [Tooltip("如果需要绑定 composite 的某一部分(例如 Up/Down/Left/Right),填这个;留空表示绑定非 composite 或整体 binding")]
|
2025-12-09 20:31:44 +08:00
|
|
|
|
public string compositePartName = "";
|
|
|
|
|
|
|
2025-12-10 17:38:31 +08:00
|
|
|
|
[Header("Behavior")] [Tooltip("如果 true,在 Prepare 后自动调用 ConfirmApply() 并保存;否则等待手动 ConfirmPrepared()/CancelPrepared()")]
|
2025-12-09 20:31:44 +08:00
|
|
|
|
public bool autoConfirm = false;
|
|
|
|
|
|
|
|
|
|
|
|
private IDisposable prepareSub;
|
|
|
|
|
|
private IDisposable applySub;
|
|
|
|
|
|
private IDisposable rebindEndSub;
|
2026-03-09 20:38:15 +08:00
|
|
|
|
private IDisposable conflictSub;
|
2025-12-09 20:31:44 +08:00
|
|
|
|
|
|
|
|
|
|
private void Start()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (btn != null) btn.onClick.AddListener(OnBtnClicked);
|
|
|
|
|
|
InputDeviceWatcher.OnDeviceChanged += OnDeviceChanged;
|
|
|
|
|
|
UpdateBindingText();
|
|
|
|
|
|
|
|
|
|
|
|
if (InputBindingManager.Instance != null)
|
|
|
|
|
|
{
|
2026-03-09 20:38:15 +08:00
|
|
|
|
// Subscribe to prepare events - already filtered by IsTargetContext
|
2025-12-09 20:31:44 +08:00
|
|
|
|
prepareSub = InputBindingManager.Instance.OnRebindPrepare.Subscribe(ctx =>
|
|
|
|
|
|
{
|
|
|
|
|
|
if (IsTargetContext(ctx))
|
|
|
|
|
|
{
|
|
|
|
|
|
var disp = ctx.overridePath == InputBindingManager.NULL_BINDING ? "<Cleared>" : ctx.overridePath;
|
|
|
|
|
|
bindKeyText.text = disp;
|
|
|
|
|
|
if (autoConfirm) _ = ConfirmPreparedAsync();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-09 20:38:15 +08:00
|
|
|
|
// Subscribe to apply events - only update if this instance's binding was applied or discarded
|
|
|
|
|
|
applySub = InputBindingManager.Instance.OnApply.Subscribe(evt =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var (success, appliedContexts) = evt;
|
|
|
|
|
|
if (appliedContexts != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Only update if any of the applied/discarded contexts match this instance
|
|
|
|
|
|
foreach (var ctx in appliedContexts)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (IsTargetContext(ctx))
|
|
|
|
|
|
{
|
|
|
|
|
|
UpdateBindingText();
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Subscribe to rebind end events - only update if this instance's binding ended
|
|
|
|
|
|
rebindEndSub = InputBindingManager.Instance.OnRebindEnd.Subscribe(evt =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var (success, context) = evt;
|
|
|
|
|
|
if (IsTargetContext(context))
|
|
|
|
|
|
{
|
|
|
|
|
|
UpdateBindingText();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Subscribe to conflict events - update if this instance is involved in conflict
|
|
|
|
|
|
conflictSub = InputBindingManager.Instance.OnRebindConflict.Subscribe(evt =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var (prepared, conflict) = evt;
|
|
|
|
|
|
// Update if either the prepared or conflict context matches this instance
|
|
|
|
|
|
if (IsTargetContext(prepared) || IsTargetContext(conflict))
|
|
|
|
|
|
{
|
|
|
|
|
|
UpdateBindingText();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-12-09 20:31:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void OnDisable()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (btn != null) btn.onClick.RemoveListener(OnBtnClicked);
|
|
|
|
|
|
InputDeviceWatcher.OnDeviceChanged -= OnDeviceChanged;
|
|
|
|
|
|
prepareSub?.Dispose();
|
|
|
|
|
|
applySub?.Dispose();
|
|
|
|
|
|
rebindEndSub?.Dispose();
|
2026-03-09 20:38:15 +08:00
|
|
|
|
conflictSub?.Dispose();
|
2025-12-09 20:31:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void OnDeviceChanged(InputDeviceWatcher.InputDeviceCategory _)
|
|
|
|
|
|
{
|
|
|
|
|
|
UpdateBindingText();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private InputAction GetAction()
|
|
|
|
|
|
{
|
|
|
|
|
|
return InputBindingManager.Action(actionName);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-09 20:38:15 +08:00
|
|
|
|
private bool IsTargetContext(InputBindingManager.RebindContext ctx)
|
2025-12-09 20:31:44 +08:00
|
|
|
|
{
|
|
|
|
|
|
if (ctx == null || ctx.action == null) return false;
|
|
|
|
|
|
var action = GetAction();
|
|
|
|
|
|
if (action == null) return false;
|
2026-03-09 20:38:15 +08:00
|
|
|
|
|
|
|
|
|
|
// Must match the action
|
|
|
|
|
|
if (ctx.action != action) return false;
|
|
|
|
|
|
|
|
|
|
|
|
// If we have a composite part specified, we need to match the binding index
|
|
|
|
|
|
if (!string.IsNullOrEmpty(compositePartName))
|
|
|
|
|
|
{
|
|
|
|
|
|
// Get the binding at the context's index
|
|
|
|
|
|
if (ctx.bindingIndex < 0 || ctx.bindingIndex >= action.bindings.Count)
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
|
|
var binding = action.bindings[ctx.bindingIndex];
|
|
|
|
|
|
|
|
|
|
|
|
// Check if the binding's name matches our composite part
|
|
|
|
|
|
return string.Equals(binding.name, compositePartName, StringComparison.OrdinalIgnoreCase);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// If no composite part specified, just matching the action is enough
|
|
|
|
|
|
return true;
|
2025-12-09 20:31:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void OnBtnClicked()
|
|
|
|
|
|
{
|
|
|
|
|
|
// Use manager API (we pass part name so manager can pick proper binding if needed)
|
|
|
|
|
|
InputBindingManager.StartRebind(actionName, string.IsNullOrEmpty(compositePartName) ? null : compositePartName);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async void ConfirmPrepared()
|
|
|
|
|
|
{
|
|
|
|
|
|
bool ok = await ConfirmPreparedAsync();
|
|
|
|
|
|
if (!ok) Debug.LogError("ConfirmPrepared: apply failed.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task<bool> ConfirmPreparedAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var task = InputBindingManager.ConfirmApply();
|
|
|
|
|
|
return await task;
|
|
|
|
|
|
}
|
2025-12-10 17:38:31 +08:00
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogError(ex);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2025-12-09 20:31:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void CancelPrepared()
|
|
|
|
|
|
{
|
|
|
|
|
|
InputBindingManager.DiscardPrepared();
|
2026-03-09 20:38:15 +08:00
|
|
|
|
// UpdateBindingText will be called automatically via OnApply event
|
2025-12-09 20:31:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void UpdateBindingText()
|
|
|
|
|
|
{
|
|
|
|
|
|
var action = GetAction();
|
|
|
|
|
|
if (action == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
bindKeyText.text = "<no action>";
|
|
|
|
|
|
if (targetImage != null) targetImage.sprite = null;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-09 20:38:15 +08:00
|
|
|
|
bindKeyText.text = GlyphService.GetDisplayNameFromInputAction(action, compositePartName);
|
2025-12-09 20:31:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var deviceCat = InputDeviceWatcher.CurrentCategory;
|
2026-03-09 20:38:15 +08:00
|
|
|
|
InputActionReference refr=default;
|
|
|
|
|
|
// string controlPath = GlyphService.GetBindingControlPath(action, compositePartName, deviceCat);
|
|
|
|
|
|
if ( GlyphService.TryGetUISpriteForActionPath(action,compositePartName, deviceCat, out Sprite sprite))
|
2025-12-09 20:31:44 +08:00
|
|
|
|
{
|
|
|
|
|
|
if (targetImage != null) targetImage.sprite = sprite;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
if (targetImage != null) targetImage.sprite = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch
|
|
|
|
|
|
{
|
|
|
|
|
|
if (targetImage != null) targetImage.sprite = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|