Key rebinding

Click a key, press another one, and the action moves to it. Stealing a key from another action leaves that one unbound, which is the case a rebind screen has to handle.

The editor above is running this project. Change a line and it reloads.

Godot classes used

InputMapInputEventKeyButtonGridContainer

The code

scripts/main.gd
extends Node

const ACTIONS: Array[Dictionary] = [
	{action = &'rb_left', label = 'Move left', key = KEY_A},
	{action = &'rb_right', label = 'Move right', key = KEY_D},
	{action = &'rb_jump', label = 'Jump', key = KEY_SPACE},
	{action = &'rb_fire', label = 'Fire', key = KEY_J},
]

var buttons: Array[Button] = []
var lights: Array[StyleBoxFlat] = []
var status: Label
var listening: int = -1


func _ready() -> void:
	var column: VBoxContainer = _root()

	var title: Label = Label.new()
	title.text = 'Key rebinding'
	title.add_theme_font_size_override('font_size', 28)
	column.add_child(title)

	var hint: Label = Label.new()
	hint.text = 'click a key to rebind it, then press the new one, escape cancels'
	hint.add_theme_color_override('font_color', Color('#9aa3b5'))
	column.add_child(hint)

	var grid: GridContainer = GridContainer.new()
	grid.columns = 3
	grid.add_theme_constant_override('h_separation', 18)
	grid.add_theme_constant_override('v_separation', 12)
	column.add_child(grid)

	for i in ACTIONS.size():
		_register(ACTIONS[i].action, ACTIONS[i].key)

		var label: Label = Label.new()
		label.text = ACTIONS[i].label
		label.custom_minimum_size = Vector2(160, 0)
		grid.add_child(label)

		var button: Button = Button.new()
		button.custom_minimum_size = Vector2(190, 44)
		button.pressed.connect(_listen.bind(i))
		grid.add_child(button)
		buttons.append(button)

		grid.add_child(_light())
		_refresh(i)

	status = Label.new()
	status.text = 'holding a bound key lights its dot'
	status.add_theme_color_override('font_color', Color('#8eef97'))
	column.add_child(status)

	print('click a key button and press another key, InputMap keeps the change while the scene runs')


func _process(_delta: float) -> void:
	for i in ACTIONS.size():
		var held: bool = listening < 0 and Input.is_action_pressed(ACTIONS[i].action)
		lights[i].bg_color = Color('#8eef97') if held else Color('#2a2f3d')


func _input(event: InputEvent) -> void:
	if listening < 0 or not (event is InputEventKey):
		return

	var key: InputEventKey = event

	if not key.pressed or key.echo:
		return

	get_viewport().set_input_as_handled()

	if key.keycode == KEY_ESCAPE:
		status.text = 'rebind cancelled'
	else:
		_bind(listening, key.keycode)

	_refresh(listening)
	listening = -1


func _listen(index: int) -> void:
	if listening >= 0:
		_refresh(listening)

	listening = index
	buttons[index].text = 'press a key'
	buttons[index].release_focus()
	status.text = 'waiting for a key for %s' % ACTIONS[index].label


func _bind(index: int, keycode: Key) -> void:
	var taken: int = _owner_of(keycode)
	var note: String = ''

	if taken >= 0 and taken != index:
		InputMap.action_erase_events(ACTIONS[taken].action)
		_refresh(taken)
		note = ', %s is unbound now' % ACTIONS[taken].label

	InputMap.action_erase_events(ACTIONS[index].action)

	var event: InputEventKey = InputEventKey.new()
	event.keycode = keycode
	InputMap.action_add_event(ACTIONS[index].action, event)

	status.text = '%s is %s%s' % [ACTIONS[index].label, OS.get_keycode_string(keycode), note]


func _owner_of(keycode: Key) -> int:
	for i in ACTIONS.size():
		for event in InputMap.action_get_events(ACTIONS[i].action):
			if event is InputEventKey and (event as InputEventKey).keycode == keycode:
				return i

	return -1


func _register(action: StringName, keycode: Key) -> void:
	if InputMap.has_action(action):
		InputMap.erase_action(action)

	InputMap.add_action(action)

	var event: InputEventKey = InputEventKey.new()
	event.keycode = keycode
	InputMap.action_add_event(action, event)


func _refresh(index: int) -> void:
	buttons[index].text = _key_text(ACTIONS[index].action)


func _key_text(action: StringName) -> String:
	for event in InputMap.action_get_events(action):
		if event is InputEventKey:
			return OS.get_keycode_string((event as InputEventKey).keycode)

	return 'unbound'


func _light() -> Panel:
	var style: StyleBoxFlat = StyleBoxFlat.new()
	style.bg_color = Color('#2a2f3d')
	style.set_corner_radius_all(11)
	lights.append(style)

	var dot: Panel = Panel.new()
	dot.custom_minimum_size = Vector2(22, 22)
	dot.size_flags_vertical = Control.SIZE_SHRINK_CENTER
	dot.add_theme_stylebox_override('panel', style)
	dot.mouse_filter = Control.MOUSE_FILTER_IGNORE

	return dot


func _root() -> VBoxContainer:
	var background: ColorRect = ColorRect.new()
	background.color = Color('#12141a')
	add_child(background)
	background.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)

	var center: CenterContainer = CenterContainer.new()
	add_child(center)
	center.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)

	var column: VBoxContainer = VBoxContainer.new()
	column.add_theme_constant_override('separation', 20)
	center.add_child(column)

	return column

More Input examples