feat: input remapping screen with conflict detection and config persistence
This commit is contained in:
@@ -1,17 +1,23 @@
|
|||||||
[gd_scene load_steps=3 format=3 uid="uid://cvfsbiy5ggrpg"]
|
[gd_scene load_steps=4 format=3 uid="uid://cvfsbiy5ggrpg"]
|
||||||
|
|
||||||
[ext_resource type="PackedScene" uid="uid://bxpr4m7lq7clh" path="res://objects/ui/input_button.tscn" id="1_h8s4o"]
|
[ext_resource type="PackedScene" uid="uid://bxpr4m7lq7clh" path="res://objects/ui/input_button.tscn" id="1_h8s4o"]
|
||||||
|
[ext_resource type="Script" uid="uid://input_settings_script" path="res://scripts/UI/InputSettings.cs" id="2_inputs"]
|
||||||
|
|
||||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_se25o"]
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_se25o"]
|
||||||
bg_color = Color(0, 0, 0, 1)
|
bg_color = Color(0, 0, 0, 1)
|
||||||
|
|
||||||
[node name="Input Settings" type="Control"]
|
[node name="Input Settings" type="Control" node_paths=PackedStringArray("ActionsContainer", "ResetButton", "InputSettingsControl")]
|
||||||
layout_mode = 3
|
layout_mode = 3
|
||||||
anchors_preset = 15
|
anchors_preset = 15
|
||||||
anchor_right = 1.0
|
anchor_right = 1.0
|
||||||
anchor_bottom = 1.0
|
anchor_bottom = 1.0
|
||||||
grow_horizontal = 2
|
grow_horizontal = 2
|
||||||
grow_vertical = 2
|
grow_vertical = 2
|
||||||
|
script = ExtResource("2_inputs")
|
||||||
|
ActionsContainer = NodePath("PanelContainer/MarginContainer/VBoxContainer/ScrollContainer/Actions")
|
||||||
|
ResetButton = NodePath("PanelContainer/MarginContainer/VBoxContainer/Reset to default Button")
|
||||||
|
InputSettingsControl = NodePath(".")
|
||||||
|
InputButtonScene = ExtResource("1_h8s4o")
|
||||||
|
|
||||||
[node name="PanelContainer" type="PanelContainer" parent="."]
|
[node name="PanelContainer" type="PanelContainer" parent="."]
|
||||||
layout_mode = 1
|
layout_mode = 1
|
||||||
@@ -48,24 +54,6 @@ layout_mode = 2
|
|||||||
size_flags_horizontal = 3
|
size_flags_horizontal = 3
|
||||||
theme_override_constants/separation = 8
|
theme_override_constants/separation = 8
|
||||||
|
|
||||||
[node name="Input button" parent="PanelContainer/MarginContainer/VBoxContainer/ScrollContainer/Actions" instance=ExtResource("1_h8s4o")]
|
|
||||||
layout_mode = 2
|
|
||||||
|
|
||||||
[node name="Input button2" parent="PanelContainer/MarginContainer/VBoxContainer/ScrollContainer/Actions" instance=ExtResource("1_h8s4o")]
|
|
||||||
layout_mode = 2
|
|
||||||
|
|
||||||
[node name="Input button3" parent="PanelContainer/MarginContainer/VBoxContainer/ScrollContainer/Actions" instance=ExtResource("1_h8s4o")]
|
|
||||||
layout_mode = 2
|
|
||||||
|
|
||||||
[node name="Input button4" parent="PanelContainer/MarginContainer/VBoxContainer/ScrollContainer/Actions" instance=ExtResource("1_h8s4o")]
|
|
||||||
layout_mode = 2
|
|
||||||
|
|
||||||
[node name="Input button5" parent="PanelContainer/MarginContainer/VBoxContainer/ScrollContainer/Actions" instance=ExtResource("1_h8s4o")]
|
|
||||||
layout_mode = 2
|
|
||||||
|
|
||||||
[node name="Input button6" parent="PanelContainer/MarginContainer/VBoxContainer/ScrollContainer/Actions" instance=ExtResource("1_h8s4o")]
|
|
||||||
layout_mode = 2
|
|
||||||
|
|
||||||
[node name="Reset to default Button" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer"]
|
[node name="Reset to default Button" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer"]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
text = "RESET_TO_DEFAULT_BUTTON"
|
text = "RESET_TO_DEFAULT_BUTTON"
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ size_flags_vertical = 3
|
|||||||
|
|
||||||
[node name="Input Settings Button" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer"]
|
[node name="Input Settings Button" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer"]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
disabled = true
|
|
||||||
text = "INPUT_BUTTON"
|
text = "INPUT_BUTTON"
|
||||||
flat = true
|
flat = true
|
||||||
|
|
||||||
|
|||||||
205
scripts/UI/InputSettings.cs
Normal file
205
scripts/UI/InputSettings.cs
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
using Godot;
|
||||||
|
using Mr.BrickAdventures.Autoloads;
|
||||||
|
|
||||||
|
namespace Mr.BrickAdventures.scripts.UI;
|
||||||
|
|
||||||
|
public partial class InputSettings : Control
|
||||||
|
{
|
||||||
|
[Export] public VBoxContainer ActionsContainer { get; set; }
|
||||||
|
[Export] public Button ResetButton { get; set; }
|
||||||
|
[Export] public Control InputSettingsControl { get; set; }
|
||||||
|
[Export] public PackedScene InputButtonScene { get; set; }
|
||||||
|
|
||||||
|
private static readonly string[] RemappableActions =
|
||||||
|
["left", "right", "jump", "attack", "pause", "show_marketplace"];
|
||||||
|
|
||||||
|
private UIManager UIManager => UIManager.Instance;
|
||||||
|
private ConfigFileHandler ConfigFileHandler => ConfigFileHandler.Instance;
|
||||||
|
|
||||||
|
private string _rebindingAction;
|
||||||
|
private Button _rebindingButton;
|
||||||
|
private Label _rebindingLabel;
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
PopulateActions();
|
||||||
|
ResetButton.FocusMode = FocusModeEnum.All;
|
||||||
|
ResetButton.Pressed += OnResetPressed;
|
||||||
|
|
||||||
|
if (InputDeviceManager.Instance != null)
|
||||||
|
InputDeviceManager.Instance.DeviceChanged += OnDeviceChanged;
|
||||||
|
InputSettingsControl.VisibilityChanged += OnVisibilityChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _ExitTree()
|
||||||
|
{
|
||||||
|
if (InputDeviceManager.Instance != null)
|
||||||
|
InputDeviceManager.Instance.DeviceChanged -= OnDeviceChanged;
|
||||||
|
InputSettingsControl.VisibilityChanged -= OnVisibilityChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _UnhandledInput(InputEvent @event)
|
||||||
|
{
|
||||||
|
if (_rebindingAction != null)
|
||||||
|
{
|
||||||
|
if (@event is InputEventKey keyEvent && keyEvent.Pressed && !keyEvent.Echo)
|
||||||
|
{
|
||||||
|
GetViewport().SetInputAsHandled();
|
||||||
|
TryBind(_rebindingAction, _rebindingButton, _rebindingLabel, keyEvent);
|
||||||
|
}
|
||||||
|
else if (@event.IsActionReleased("ui_cancel"))
|
||||||
|
{
|
||||||
|
GetViewport().SetInputAsHandled();
|
||||||
|
CancelRebind();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!@event.IsActionReleased("ui_cancel")) return;
|
||||||
|
if (!UIManager.IsScreenOnTop(InputSettingsControl)) return;
|
||||||
|
UIManager.PopScreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PopulateActions()
|
||||||
|
{
|
||||||
|
foreach (Node child in ActionsContainer.GetChildren())
|
||||||
|
child.QueueFree();
|
||||||
|
|
||||||
|
bool first = true;
|
||||||
|
foreach (var action in RemappableActions)
|
||||||
|
{
|
||||||
|
var row = InputButtonScene.Instantiate<Button>();
|
||||||
|
ActionsContainer.AddChild(row);
|
||||||
|
|
||||||
|
var labelAction = row.GetNode<Label>("MarginContainer/HBoxContainer/LabelAction");
|
||||||
|
var labelInput = row.GetNode<Label>("MarginContainer/HBoxContainer/LabelInput");
|
||||||
|
|
||||||
|
labelAction.Text = HumanizeAction(action);
|
||||||
|
labelInput.Text = GetKeyName(action);
|
||||||
|
|
||||||
|
row.FocusMode = FocusModeEnum.All;
|
||||||
|
|
||||||
|
var capturedAction = action;
|
||||||
|
var capturedLabel = labelInput;
|
||||||
|
var capturedRow = row;
|
||||||
|
row.Pressed += () => OnRowPressed(capturedAction, capturedRow, capturedLabel);
|
||||||
|
|
||||||
|
if (first)
|
||||||
|
{
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnRowPressed(string action, Button row, Label labelInput)
|
||||||
|
{
|
||||||
|
if (_rebindingAction != null)
|
||||||
|
CancelRebind();
|
||||||
|
|
||||||
|
_rebindingAction = action;
|
||||||
|
_rebindingButton = row;
|
||||||
|
_rebindingLabel = labelInput;
|
||||||
|
labelInput.Text = "Press a key...";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TryBind(string action, Button row, Label labelInput, InputEventKey keyEvent)
|
||||||
|
{
|
||||||
|
var newKey = keyEvent.Keycode != Key.None ? keyEvent.Keycode : keyEvent.PhysicalKeycode;
|
||||||
|
|
||||||
|
// Conflict check
|
||||||
|
foreach (var other in RemappableActions)
|
||||||
|
{
|
||||||
|
if (other == action) continue;
|
||||||
|
var existing = GetCurrentKey(other);
|
||||||
|
if (existing == newKey)
|
||||||
|
{
|
||||||
|
labelInput.Text = $"Already used by {HumanizeAction(other)}!";
|
||||||
|
_rebindingAction = null;
|
||||||
|
_rebindingButton = null;
|
||||||
|
_rebindingLabel = null;
|
||||||
|
var capturedAction = action;
|
||||||
|
var capturedLabel = labelInput;
|
||||||
|
GetTree().CreateTimer(2.0).Timeout += () =>
|
||||||
|
{
|
||||||
|
if (IsInstanceValid(capturedLabel))
|
||||||
|
capturedLabel.Text = GetKeyName(capturedAction);
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply
|
||||||
|
foreach (var ev in InputMap.ActionGetEvents(action))
|
||||||
|
{
|
||||||
|
if (ev is InputEventKey)
|
||||||
|
InputMap.ActionEraseEvent(action, ev);
|
||||||
|
}
|
||||||
|
InputMap.ActionAddEvent(action, keyEvent);
|
||||||
|
|
||||||
|
// Persist
|
||||||
|
ConfigFileHandler.SettingsConfig.SetValue("input_settings", action, (long)newKey);
|
||||||
|
ConfigFileHandler.SettingsConfig.Save(ConfigFileHandler.SettingsPath);
|
||||||
|
|
||||||
|
labelInput.Text = OS.GetKeycodeString(newKey);
|
||||||
|
|
||||||
|
_rebindingAction = null;
|
||||||
|
_rebindingButton = null;
|
||||||
|
_rebindingLabel = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CancelRebind()
|
||||||
|
{
|
||||||
|
if (_rebindingLabel != null)
|
||||||
|
_rebindingLabel.Text = GetKeyName(_rebindingAction);
|
||||||
|
_rebindingAction = null;
|
||||||
|
_rebindingButton = null;
|
||||||
|
_rebindingLabel = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnResetPressed()
|
||||||
|
{
|
||||||
|
InputMap.LoadFromProjectSettings();
|
||||||
|
ConfigFileHandler.SettingsConfig.EraseSection("input_settings");
|
||||||
|
ConfigFileHandler.SettingsConfig.Save(ConfigFileHandler.SettingsPath);
|
||||||
|
PopulateActions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnVisibilityChanged()
|
||||||
|
{
|
||||||
|
if (InputSettingsControl.IsVisibleInTree() && InputDeviceManager.Instance?.IsPointerless == true)
|
||||||
|
GrabFirstFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDeviceChanged(int device)
|
||||||
|
{
|
||||||
|
var d = (InputDeviceManager.InputDevice)device;
|
||||||
|
if (d != InputDeviceManager.InputDevice.Mouse && InputSettingsControl.IsVisibleInTree())
|
||||||
|
GrabFirstFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GrabFirstFocus()
|
||||||
|
{
|
||||||
|
var children = ActionsContainer.GetChildren();
|
||||||
|
if (children.Count > 0 && children[0] is Button btn)
|
||||||
|
btn.GrabFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Key GetCurrentKey(string action)
|
||||||
|
{
|
||||||
|
foreach (var ev in InputMap.ActionGetEvents(action))
|
||||||
|
{
|
||||||
|
if (ev is InputEventKey key)
|
||||||
|
return key.Keycode != Key.None ? key.Keycode : key.PhysicalKeycode;
|
||||||
|
}
|
||||||
|
return Key.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetKeyName(string action)
|
||||||
|
{
|
||||||
|
var key = GetCurrentKey(action);
|
||||||
|
return key == Key.None ? "---" : OS.GetKeycodeString(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string HumanizeAction(string action)
|
||||||
|
=> action.Replace("_", " ").ToUpper();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user