extends ScrollContainer # Settings screen's Controls tab: rebinds every action in InputSettings.ACTIONS # for either device, toggles invert-pitch, and resets to the project.godot # defaults. InputSettings owns the bindings themselves and their persistence; # this script is only the editor for them, and deliberately keeps # settings_menu.gd video-only. # # Rows are built in code rather than laid out in settings.tscn so the list stays # derived from InputSettings.ACTIONS — adding a rebindable action means editing # that one const, not this scene as well. # A joypad axis has to travel this far before a capture accepts it. Resting # stick drift is routinely a few percent off centre and would otherwise bind # itself the instant the player opened a capture. const AXIS_CAPTURE_THRESHOLD := 0.5 @onready var keyboard_button: Button = %KeyboardButton @onready var controller_button: Button = %ControllerButton @onready var status_label: Label = %StatusLabel @onready var binding_list: VBoxContainer = %BindingList @onready var invert_pitch_check: CheckBox = %InvertPitchCheck @onready var reset_button: Button = %ResetButton var _device: String = InputSettings.DEVICE_KEYBOARD # The action currently awaiting an input event, or "" when not capturing. var _capturing: String = "" # action -> the row's Button, so a rebuild-free label refresh is possible and # so capture can restore the right button's text on cancel. var _row_buttons: Dictionary = {} func _ready() -> void: keyboard_button.pressed.connect(_on_device_selected.bind(InputSettings.DEVICE_KEYBOARD)) controller_button.pressed.connect(_on_device_selected.bind(InputSettings.DEVICE_JOYPAD)) invert_pitch_check.toggled.connect(_on_invert_pitch_toggled) reset_button.pressed.connect(_on_reset_pressed) invert_pitch_check.button_pressed = InputSettings.invert_pitch _update_device_buttons() _rebuild_rows() func _on_device_selected(device: String) -> void: _cancel_capture() _device = device _update_device_buttons() _rebuild_rows() func _update_device_buttons() -> void: keyboard_button.button_pressed = _device == InputSettings.DEVICE_KEYBOARD controller_button.button_pressed = _device == InputSettings.DEVICE_JOYPAD func _rebuild_rows() -> void: for child in binding_list.get_children(): child.queue_free() _row_buttons.clear() var last_group := "" for entry in InputSettings.ACTIONS: var group: String = entry["group"] if group != last_group: last_group = group binding_list.add_child(_make_group_header(group)) var row := HBoxContainer.new() row.add_theme_constant_override("separation", 10) var label := Label.new() label.text = entry["label"] label.custom_minimum_size = Vector2(200, 0) row.add_child(label) var button := Button.new() var action: String = entry["action"] button.text = InputSettings.binding_text(action, _device) button.custom_minimum_size = Vector2(0, 36) button.size_flags_horizontal = Control.SIZE_EXPAND_FILL button.clip_text = true button.pressed.connect(_begin_capture.bind(action)) row.add_child(button) _row_buttons[action] = button binding_list.add_child(row) # Re-bound after every rebuild: the rows above are new nodes each time, so # the buttons AudioManager was tracking no longer exist. AudioManager.bind_tree_buttons(self) func _make_group_header(group: String) -> Label: var header := Label.new() header.text = group header.add_theme_font_size_override("font_size", 18) header.modulate = Color(1, 1, 1, 0.7) return header func _begin_capture(action: String) -> void: _cancel_capture() _capturing = action var button: Button = _row_buttons[action] button.text = "Press a key…" if _device == InputSettings.DEVICE_KEYBOARD else "Press a button…" status_label.text = "Listening — press Escape to cancel." func _cancel_capture() -> void: if _capturing == "": return var action := _capturing _capturing = "" if _row_buttons.has(action) and is_instance_valid(_row_buttons[action]): _row_buttons[action].text = InputSettings.binding_text(action, _device) status_label.text = "" # _input rather than _unhandled_input: the row Button has focus while capturing, # and an unhandled-input handler would never see the key that Button consumes as # its own activation. Everything consumed here is marked handled so the pending # event cannot also re-press that button and re-enter capture. func _input(event: InputEvent) -> void: if _capturing == "": return if event.is_action_pressed("ui_cancel"): get_viewport().set_input_as_handled() _cancel_capture() return var captured := _capturable_event(event) if captured == null: return get_viewport().set_input_as_handled() var action := _capturing _capturing = "" var displaced := InputSettings.set_binding(action, captured) _rebuild_rows() if displaced.is_empty(): status_label.text = "" else: status_label.text = "Unbound %s — it was using the same input." % ", ".join(_labels_for(displaced)) # Returns the event to bind, or null if this event is not a legal binding for # the device kind currently being edited. Keeping the check here means a joypad # press can never land in the keyboard column just because that tab was open. func _capturable_event(event: InputEvent) -> InputEvent: if _device == InputSettings.DEVICE_KEYBOARD: if event is InputEventKey and event.pressed and not event.echo: var key := InputEventKey.new() key.physical_keycode = event.physical_keycode return key return null if event is InputEventJoypadButton and event.pressed: var button := InputEventJoypadButton.new() button.button_index = event.button_index return button if event is InputEventJoypadMotion and absf(event.axis_value) >= AXIS_CAPTURE_THRESHOLD: var motion := InputEventJoypadMotion.new() motion.axis = event.axis motion.axis_value = signf(event.axis_value) return motion return null func _labels_for(actions: PackedStringArray) -> PackedStringArray: var out := PackedStringArray() for action in actions: for entry in InputSettings.ACTIONS: if entry["action"] == action: out.append(entry["label"]) break return out func _on_invert_pitch_toggled(pressed: bool) -> void: InputSettings.invert_pitch = pressed func _on_reset_pressed() -> void: _cancel_capture() InputSettings.reset_all() invert_pitch_check.button_pressed = InputSettings.invert_pitch _rebuild_rows() status_label.text = "Bindings reset to defaults."