(Feat) Add Remmaping
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
using System;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using Ashwild.Player;
|
||||
|
||||
namespace Ashwild.Settings
|
||||
{
|
||||
/// <summary>
|
||||
/// One rebindable-key row: an action label, the current key (shown as a glyph when an icon is
|
||||
/// available, otherwise as text), a Rebind button and a Reset button. Pure UI — it holds the
|
||||
/// config entry's action name + binding index and raises callbacks; it knows nothing about the
|
||||
/// Input System. Mirrors SlotUI/RecipeButtonUI: the panel calls Initialize once, then RefreshDisplay.
|
||||
/// </summary>
|
||||
public class KeybindRowUI : MonoBehaviour
|
||||
{
|
||||
#region Serialized Fields
|
||||
|
||||
[Header("References")]
|
||||
[SerializeField] private TMP_Text labelText;
|
||||
[SerializeField] private TMP_Text keyText;
|
||||
[SerializeField] private Image keyIcon;
|
||||
[SerializeField] private Button rebindButton;
|
||||
[SerializeField] private Button resetButton;
|
||||
|
||||
#endregion
|
||||
|
||||
#region State
|
||||
|
||||
private string actionName;
|
||||
private int bindingIndex;
|
||||
private Action<KeybindRowUI> onRebind;
|
||||
private Action<KeybindRowUI> onReset;
|
||||
|
||||
public string ActionName => actionName;
|
||||
public int BindingIndex => bindingIndex;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public API
|
||||
|
||||
/// <summary>
|
||||
/// Binds this row to its config entry and the panel callbacks. Call once after Instantiate.
|
||||
/// </summary>
|
||||
public void Initialize(RebindableEntry entry, Action<KeybindRowUI> onRebind, Action<KeybindRowUI> onReset)
|
||||
{
|
||||
actionName = entry.ActionName;
|
||||
bindingIndex = entry.BindingIndex;
|
||||
this.onRebind = onRebind;
|
||||
this.onReset = onReset;
|
||||
|
||||
if (labelText != null) labelText.text = entry.DisplayLabel;
|
||||
|
||||
if (rebindButton != null)
|
||||
{
|
||||
rebindButton.onClick.RemoveAllListeners();
|
||||
rebindButton.onClick.AddListener(() => this.onRebind?.Invoke(this));
|
||||
}
|
||||
if (resetButton != null)
|
||||
{
|
||||
resetButton.onClick.RemoveAllListeners();
|
||||
resetButton.onClick.AddListener(() => this.onReset?.Invoke(this));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the current key as a glyph when an icon is supplied, otherwise as text.
|
||||
/// </summary>
|
||||
public void RefreshDisplay(string keyDisplay, Sprite icon)
|
||||
{
|
||||
bool hasIcon = icon != null;
|
||||
|
||||
if (keyIcon != null)
|
||||
{
|
||||
keyIcon.enabled = hasIcon;
|
||||
if (hasIcon) keyIcon.sprite = icon;
|
||||
}
|
||||
if (keyText != null)
|
||||
{
|
||||
keyText.gameObject.SetActive(!hasIcon);
|
||||
keyText.text = keyDisplay;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1cf0849fb8494a24ca8a7342bdbfcf84
|
||||
@@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Ashwild.Settings
|
||||
{
|
||||
/// <summary>
|
||||
/// Standalone modal overlay for rebinding, living as its own object under the SettingsPanel (a
|
||||
/// sibling of the category sub-panels) so it renders in front of everything. One message label
|
||||
/// plus two buttons: while capturing it reads "press a key" with the buttons hidden; on a clash
|
||||
/// it shows "X already uses this key" with Replace / Cancel. Pure UI driven by the keybindings
|
||||
/// sub-panel, which holds a reference and calls these methods. Buttons are wired per-conflict.
|
||||
/// </summary>
|
||||
public class RebindPromptUI : MonoBehaviour
|
||||
{
|
||||
#region Serialized Fields
|
||||
|
||||
[Header("Root")]
|
||||
[Tooltip("The overlay object toggled on/off. Leave empty to use this GameObject.")]
|
||||
[SerializeField] private GameObject root;
|
||||
|
||||
[Header("References")]
|
||||
[SerializeField] private TMP_Text messageText;
|
||||
[SerializeField] private Button replaceButton;
|
||||
[SerializeField] private Button cancelButton;
|
||||
|
||||
[Header("Text")]
|
||||
[SerializeField] private string listeningText = "Appuyez sur une touche…";
|
||||
[Tooltip("{0} = the action that already uses the key.")]
|
||||
[SerializeField] private string conflictFormat = "« {0} » utilise déjà cette touche.";
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public API
|
||||
|
||||
/// <summary>
|
||||
/// Shows the overlay in "press a key" mode (buttons hidden).
|
||||
/// </summary>
|
||||
public void ShowListening()
|
||||
{
|
||||
Target.SetActive(true);
|
||||
if (messageText != null) messageText.text = listeningText;
|
||||
SetButtonsActive(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Switches the open overlay to the conflict prompt, showing and wiring the two buttons.
|
||||
/// </summary>
|
||||
public void ShowConflict(string conflictLabel, Action onReplace, Action onCancel)
|
||||
{
|
||||
Target.SetActive(true);
|
||||
if (messageText != null) messageText.text = string.Format(conflictFormat, conflictLabel);
|
||||
SetButtonsActive(true);
|
||||
|
||||
if (replaceButton != null)
|
||||
{
|
||||
replaceButton.onClick.RemoveAllListeners();
|
||||
replaceButton.onClick.AddListener(() => onReplace?.Invoke());
|
||||
}
|
||||
if (cancelButton != null)
|
||||
{
|
||||
cancelButton.onClick.RemoveAllListeners();
|
||||
cancelButton.onClick.AddListener(() => onCancel?.Invoke());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hides the overlay.
|
||||
/// </summary>
|
||||
public void Hide() => Target.SetActive(false);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Internal Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Shows/hides the Replace and Cancel buttons (only relevant in the conflict state).
|
||||
/// </summary>
|
||||
private void SetButtonsActive(bool active)
|
||||
{
|
||||
if (replaceButton != null) replaceButton.gameObject.SetActive(active);
|
||||
if (cancelButton != null) cancelButton.gameObject.SetActive(active);
|
||||
}
|
||||
|
||||
private GameObject Target => root != null ? root : gameObject;
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ce5206c9bb735e247ae12cd858bd0d7b
|
||||
@@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ashwild.Settings
|
||||
{
|
||||
/// <summary>
|
||||
/// A reusable dropdown. Pure UI: it knows nothing about which setting it drives. The panel
|
||||
/// calls <see cref="Init"/> once to register the selection action, supplies the option list via
|
||||
/// <see cref="SetOptions"/> (resolutions, monitors… are all built by the panel) and selects the
|
||||
/// current entry through <see cref="SetValue"/>.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public class SettingDropdownWidget : MonoBehaviour
|
||||
{
|
||||
#region Serialized Fields
|
||||
|
||||
[Header("References")]
|
||||
[SerializeField] private TMP_Dropdown dropdown;
|
||||
|
||||
#endregion
|
||||
|
||||
#region State
|
||||
|
||||
private Action<int> onChanged;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public API
|
||||
|
||||
/// <summary>
|
||||
/// Registers the action to run when the user picks an option. Call once.
|
||||
/// </summary>
|
||||
public void Init(Action<int> onChanged)
|
||||
{
|
||||
if (dropdown == null)
|
||||
{
|
||||
Debug.LogError($"[SettingDropdownWidget] '{name}' has no Dropdown assigned.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
this.onChanged = onChanged;
|
||||
dropdown.onValueChanged.RemoveListener(HandleChanged);
|
||||
dropdown.onValueChanged.AddListener(HandleChanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the dropdown's options. Call before <see cref="SetValue"/> on refresh.
|
||||
/// </summary>
|
||||
public void SetOptions(IEnumerable<string> options)
|
||||
{
|
||||
if (dropdown == null) return;
|
||||
dropdown.ClearOptions();
|
||||
dropdown.AddOptions(new List<string>(options));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects the given index (clamped to the current options) without invoking the action.
|
||||
/// </summary>
|
||||
public void SetValue(int index)
|
||||
{
|
||||
if (dropdown == null) return;
|
||||
dropdown.SetValueWithoutNotify(Mathf.Clamp(index, 0, dropdown.options.Count - 1));
|
||||
dropdown.RefreshShownValue();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
/// <summary>
|
||||
/// Runs the apply action with the chosen index.
|
||||
/// </summary>
|
||||
private void HandleChanged(int index) => onChanged?.Invoke(index);
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bfc37cc18ac6d884a9382a7ffc312d40
|
||||
@@ -0,0 +1,95 @@
|
||||
using System;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Ashwild.Settings
|
||||
{
|
||||
/// <summary>
|
||||
/// A reusable slider + value label. It is pure UI: it knows nothing about which setting it
|
||||
/// drives. The hosting panel calls <see cref="Init"/> once to hand it its range, label format
|
||||
/// and the action to run when the user moves it; afterwards the panel pushes the current value
|
||||
/// through <see cref="SetValue"/>. The range is applied to the slider in <see cref="Init"/>, so
|
||||
/// a control can never be stuck at the slider's default 0–1 range.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public class SettingSliderWidget : MonoBehaviour
|
||||
{
|
||||
#region Serialized Fields
|
||||
|
||||
[Header("References")]
|
||||
[SerializeField] private Slider slider;
|
||||
[SerializeField] private TMP_Text valueLabel;
|
||||
|
||||
#endregion
|
||||
|
||||
#region State
|
||||
|
||||
private Action<float> onChanged;
|
||||
private float labelScale = 1f;
|
||||
private string labelFormat = "{0:F0}";
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public API
|
||||
|
||||
/// <summary>
|
||||
/// Configures the slider with its range and label format, and registers the action to run
|
||||
/// when the user drags it. Call once (the apply action is not invoked by <see cref="SetValue"/>).
|
||||
/// </summary>
|
||||
public void Init(float min, float max, float labelScale, string labelFormat, Action<float> onChanged)
|
||||
{
|
||||
if (slider == null)
|
||||
{
|
||||
Debug.LogError($"[SettingSliderWidget] '{name}' has no Slider assigned.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
this.labelScale = labelScale;
|
||||
this.labelFormat = labelFormat;
|
||||
this.onChanged = onChanged;
|
||||
|
||||
slider.minValue = min;
|
||||
slider.maxValue = max;
|
||||
slider.onValueChanged.RemoveListener(HandleSliderChanged);
|
||||
slider.onValueChanged.AddListener(HandleSliderChanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Displays the given value without invoking the apply action.
|
||||
/// </summary>
|
||||
public void SetValue(float value)
|
||||
{
|
||||
if (slider == null) return;
|
||||
slider.SetValueWithoutNotify(value);
|
||||
UpdateLabel(value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
/// <summary>
|
||||
/// Updates the label and runs the apply action with the user-driven value.
|
||||
/// </summary>
|
||||
private void HandleSliderChanged(float value)
|
||||
{
|
||||
UpdateLabel(value);
|
||||
onChanged?.Invoke(value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Internal Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Formats and shows the scaled value, when a label is wired.
|
||||
/// </summary>
|
||||
private void UpdateLabel(float value)
|
||||
{
|
||||
if (valueLabel != null) valueLabel.text = string.Format(labelFormat, value * labelScale);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: edafafa6f8d058845ba74c0ad09e2981
|
||||
@@ -0,0 +1,136 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Ashwild.Settings
|
||||
{
|
||||
/// <summary>
|
||||
/// A "◄ value ►" control that cycles a list of preset values. Pure UI: it knows nothing about
|
||||
/// which setting it drives. The hosting panel calls <see cref="Init"/> once with the presets
|
||||
/// (each a value plus its display label) and the action to run when the user steps; afterwards
|
||||
/// the panel pushes the current value through <see cref="SetValue"/>. Because each preset
|
||||
/// carries its own label, a value like -1 can read as "Unlimited" rather than a number.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public class SettingStepperWidget : MonoBehaviour
|
||||
{
|
||||
#region Serialized Fields
|
||||
|
||||
[Header("References")]
|
||||
[SerializeField] private Button previousButton;
|
||||
[SerializeField] private Button nextButton;
|
||||
[SerializeField] private TMP_Text valueLabel;
|
||||
|
||||
#endregion
|
||||
|
||||
#region State
|
||||
|
||||
private IReadOnlyList<(int value, string label)> presets;
|
||||
private Action<int> onChanged;
|
||||
private bool wrap;
|
||||
private int currentIndex;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public API
|
||||
|
||||
/// <summary>
|
||||
/// Configures the stepper with its presets and wrap behaviour, and registers the action to
|
||||
/// run when the user steps. Call once (the action is not invoked by <see cref="SetValue"/>).
|
||||
/// </summary>
|
||||
public void Init(IReadOnlyList<(int value, string label)> presets, bool wrap, Action<int> onChanged)
|
||||
{
|
||||
if (presets == null || presets.Count == 0)
|
||||
{
|
||||
Debug.LogError($"[SettingStepperWidget] '{name}' was initialized with no presets.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
this.presets = presets;
|
||||
this.wrap = wrap;
|
||||
this.onChanged = onChanged;
|
||||
|
||||
if (previousButton != null)
|
||||
{
|
||||
previousButton.onClick.RemoveListener(StepDown);
|
||||
previousButton.onClick.AddListener(StepDown);
|
||||
}
|
||||
if (nextButton != null)
|
||||
{
|
||||
nextButton.onClick.RemoveListener(StepUp);
|
||||
nextButton.onClick.AddListener(StepUp);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects the preset matching the given value (falling back to the first) and shows its
|
||||
/// label, without invoking the apply action.
|
||||
/// </summary>
|
||||
public void SetValue(int value)
|
||||
{
|
||||
if (presets == null) return;
|
||||
currentIndex = IndexOfValue(value);
|
||||
UpdateLabel();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
private void StepDown() => Step(-1);
|
||||
private void StepUp() => Step(+1);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Internal Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Moves the selection by one step (clamped or wrapped), shows it and runs the apply action.
|
||||
/// </summary>
|
||||
private void Step(int direction)
|
||||
{
|
||||
if (presets == null || presets.Count == 0) return;
|
||||
|
||||
int count = presets.Count;
|
||||
int next = currentIndex + direction;
|
||||
if (wrap)
|
||||
next = (next % count + count) % count;
|
||||
else
|
||||
next = Mathf.Clamp(next, 0, count - 1);
|
||||
|
||||
if (next == currentIndex) return;
|
||||
|
||||
currentIndex = next;
|
||||
UpdateLabel();
|
||||
onChanged?.Invoke(presets[currentIndex].value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the preset index whose value equals the given one, or 0 when none matches.
|
||||
/// </summary>
|
||||
private int IndexOfValue(int value)
|
||||
{
|
||||
for (int i = 0; i < presets.Count; i++)
|
||||
if (presets[i].value == value) return i;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the current preset's label and disables arrows that would do nothing.
|
||||
/// </summary>
|
||||
private void UpdateLabel()
|
||||
{
|
||||
if (valueLabel != null) valueLabel.text = presets[currentIndex].label;
|
||||
|
||||
if (!wrap)
|
||||
{
|
||||
if (previousButton != null) previousButton.interactable = currentIndex > 0;
|
||||
if (nextButton != null) nextButton.interactable = currentIndex < presets.Count - 1;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ed871655db50611488015c55020523db
|
||||
@@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Ashwild.Settings
|
||||
{
|
||||
/// <summary>
|
||||
/// A reusable toggle. Pure UI: it knows nothing about which bool setting it drives. The panel
|
||||
/// calls <see cref="Init"/> once to register the action to run when the user flips it, then
|
||||
/// reflects state through <see cref="SetValue"/>.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public class SettingToggleWidget : MonoBehaviour
|
||||
{
|
||||
#region Serialized Fields
|
||||
|
||||
[Header("References")]
|
||||
[SerializeField] private Toggle toggle;
|
||||
|
||||
#endregion
|
||||
|
||||
#region State
|
||||
|
||||
private Action<bool> onChanged;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public API
|
||||
|
||||
/// <summary>
|
||||
/// Registers the action to run when the user flips the toggle. Call once.
|
||||
/// </summary>
|
||||
public void Init(Action<bool> onChanged)
|
||||
{
|
||||
if (toggle == null)
|
||||
{
|
||||
Debug.LogError($"[SettingToggleWidget] '{name}' has no Toggle assigned.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
this.onChanged = onChanged;
|
||||
toggle.onValueChanged.RemoveListener(HandleToggleChanged);
|
||||
toggle.onValueChanged.AddListener(HandleToggleChanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Displays the given state without invoking the apply action.
|
||||
/// </summary>
|
||||
public void SetValue(bool value)
|
||||
{
|
||||
if (toggle != null) toggle.SetIsOnWithoutNotify(value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
/// <summary>
|
||||
/// Runs the apply action with the user-driven state.
|
||||
/// </summary>
|
||||
private void HandleToggleChanged(bool value) => onChanged?.Invoke(value);
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 856178189b5f23e438619599ec0d1a6c
|
||||
Reference in New Issue
Block a user