using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using Ashwild.Player; namespace Ashwild.Settings { /// /// Settings sub-panel that lists every rebindable binding from the shared InputConfig as rows and /// drives interactive rebinds. It is the orchestrator: the InputManager performs the rebind on the /// asset, the SettingsManager persists it, and this panel refreshes the rows. It also owns the /// modal rebind prompt (a "press a key" overlay that becomes a "X already uses this key → /// Replace / Cancel" conflict prompt) — the prompt objects live under the SettingsPanel so they /// render in front of everything. Rows show layout-aware glyphs, so AZERTY reads correctly. /// public class KeybindingsSettingsSubPanel : SettingsSubPanel { #region Serialized Fields [Header("Row List")] [SerializeField] private Transform rowContainer; [SerializeField] private GameObject rowPrefab; [SerializeField] private Button resetAllButton; [Header("Rebind Prompt")] [Tooltip("Standalone prompt overlay — its own object under the SettingsPanel, not a child here.")] [SerializeField] private RebindPromptUI prompt; #endregion #region State private readonly List rows = new List(); private bool built; /// /// The shared input config, pulled from the SettingsManager (assigned there, not here). /// private InputConfig config; /// /// The conflict awaiting the player's Replace/Cancel choice, kept so the buttons can resolve it. /// private InputManager.RebindOutcome pendingOutcome; private bool hasPendingConflict; private KeybindRowUI activeRow; #endregion #region Unity Lifecycle /// /// Wires the reset-all button and the prompt's Replace/Cancel buttons. /// private void Awake() { if (resetAllButton != null) resetAllButton.onClick.AddListener(ResetAll); } /// /// Aborts any rebind/conflict in flight when the panel is hidden (category switch / closed). /// private void OnDisable() { if (InputManager.Instance != null) InputManager.Instance.CancelOngoingRebind(); if (hasPendingConflict) OnConflictCancel(); HidePrompt(); } #endregion #region Public API /// /// Builds the rows once, then repaints every current key (so reopening shows live bindings). /// public override void Refresh() { config = SettingsManager.Instance != null ? SettingsManager.Instance.InputConfig : null; if (config == null) { Debug.LogError("[KeybindingsSettingsSubPanel] No InputConfig available from SettingsManager.", this); return; } HidePrompt(); if (!built) BuildRows(); RefreshAll(); } #endregion #region Row Building /// /// Instantiates one row per config binding and binds its callbacks. /// private void BuildRows() { if (rowPrefab == null || rowContainer == null) { Debug.LogError($"[KeybindingsSettingsSubPanel] Row prefab/container missing on '{name}'.", this); return; } foreach (RebindableEntry entry in config.Bindings) { GameObject go = Instantiate(rowPrefab, rowContainer); KeybindRowUI row = go.GetComponent(); if (row == null) { Debug.LogError("[KeybindingsSettingsSubPanel] Row prefab has no KeybindRowUI.", this); Destroy(go); continue; } row.Initialize(entry, OnRowRebind, OnRowReset); rows.Add(row); } built = true; } #endregion #region Row Callbacks /// /// Opens the prompt and starts capturing a new key for the clicked row. /// private void OnRowRebind(KeybindRowUI row) { if (InputManager.Instance == null) return; activeRow = row; ShowListening(); InputManager.Instance.StartInteractiveRebind(row.ActionName, row.BindingIndex, OnRebindOutcome); } /// /// Handles the capture result: persist + refresh on success, raise the conflict prompt on a /// clash, or just close on cancel. /// private void OnRebindOutcome(InputManager.RebindOutcome outcome) { switch (outcome.Result) { case InputManager.RebindResult.Completed: HidePrompt(); Persist(); RefreshAll(); break; case InputManager.RebindResult.Conflict: pendingOutcome = outcome; hasPendingConflict = true; ShowConflict(LabelFor(outcome.ConflictActionName)); break; default: HidePrompt(); if (activeRow != null) RefreshRow(activeRow); break; } } /// /// Resolves a conflict by swapping the two keys (the other action takes this one's old key). /// private void OnConflictReplace() { if (!hasPendingConflict) return; if (InputManager.Instance != null) InputManager.Instance.ApplySwap(pendingOutcome); ClearConflict(); HidePrompt(); Persist(); RefreshAll(); } /// /// Resolves a conflict by undoing the new binding (the other action keeps its key). /// private void OnConflictCancel() { if (!hasPendingConflict) return; if (InputManager.Instance != null) InputManager.Instance.RevertRebind(pendingOutcome); ClearConflict(); HidePrompt(); RefreshAll(); } /// /// Resets the clicked row's binding to default and persists. /// private void OnRowReset(KeybindRowUI row) { if (InputManager.Instance == null) return; InputManager.Instance.ResetBinding(row.ActionName, row.BindingIndex); Persist(); RefreshRow(row); } /// /// Resets every binding to default and persists. /// private void ResetAll() { if (InputManager.Instance == null) return; InputManager.Instance.ResetAllBindings(); Persist(); RefreshAll(); } #endregion #region Prompt /// /// Shows the prompt in "press a key" mode. /// private void ShowListening() { if (prompt != null) prompt.ShowListening(); } /// /// Switches the open prompt to the conflict state for the named action, wiring its buttons. /// private void ShowConflict(string conflictLabel) { if (prompt != null) prompt.ShowConflict(conflictLabel, OnConflictReplace, OnConflictCancel); } /// /// Hides the prompt entirely. /// private void HidePrompt() { if (prompt != null) prompt.Hide(); } /// /// Clears the pending-conflict state. /// private void ClearConflict() { hasPendingConflict = false; pendingOutcome = default; } #endregion #region Refresh /// /// Persists the current bindings through the SettingsManager. /// private void Persist() { if (SettingsManager.Instance != null) SettingsManager.Instance.SaveKeybinds(); } /// /// Repaints the current key on every row. /// private void RefreshAll() { foreach (KeybindRowUI row in rows) RefreshRow(row); } /// /// Repaints one row's current key, choosing a glyph from the config when available. /// private void RefreshRow(KeybindRowUI row) { if (InputManager.Instance == null) return; string display = InputManager.Instance.GetBindingDisplayString(row.ActionName, row.BindingIndex); Sprite icon = config != null ? config.GetIcon(display) : null; if (icon == null && config != null) { string controlPath = InputManager.Instance.GetBindingControlPath(row.ActionName, row.BindingIndex); icon = config.GetIcon(controlPath); } row.RefreshDisplay(display, icon); } /// /// The display label authored for an action (first matching binding), or the raw name. /// private string LabelFor(string actionName) { if (config != null) foreach (RebindableEntry entry in config.Bindings) if (entry.ActionName == actionName) return entry.DisplayLabel; return actionName; } #endregion } }