Browse Source

Merge pull request #44181 from EricEzaM/PR/INP5-new-input-editor

New Input Map Editor and Editor Settings Shortcut Editor
Rémi Verschelde 4 years ago
parent
commit
61e26d4431

+ 1167 - 0
editor/action_map_editor.cpp

@@ -0,0 +1,1167 @@
+/*************************************************************************/
+/*  action_map_editor.cpp                                                */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+#include "action_map_editor.h"
+#include "core/input/input_map.h"
+#include "core/os/keyboard.h"
+#include "editor/editor_scale.h"
+#include "scene/gui/center_container.h"
+
+/////////////////////////////////////////
+
+// Maps to 2*axis if value is neg, or + 1 if value is pos.
+static const char *_joy_axis_descriptions[JOY_AXIS_MAX * 2] = {
+	TTRC("Left Stick Left, Joystick 0 Left"),
+	TTRC("Left Stick Right, Joystick 0 Right"),
+	TTRC("Left Stick Up, Joystick 0 Up"),
+	TTRC("Left Stick Down, Joystick 0 Down"),
+	TTRC("Right Stick Left, Joystick 1 Left"),
+	TTRC("Right Stick Right, Joystick 1 Right"),
+	TTRC("Right Stick Up, Joystick 1 Up"),
+	TTRC("Right Stick Down, Joystick 1 Down"),
+	TTRC("Joystick 2 Left"),
+	TTRC("Left Trigger, Sony L2, Xbox LT, Joystick 2 Right"),
+	TTRC("Joystick 2 Up"),
+	TTRC("Right Trigger, Sony R2, Xbox RT, Joystick 2 Down"),
+	TTRC("Joystick 3 Left"),
+	TTRC("Joystick 3 Right"),
+	TTRC("Joystick 3 Up"),
+	TTRC("Joystick 3 Down"),
+	TTRC("Joystick 4 Left"),
+	TTRC("Joystick 4 Right"),
+	TTRC("Joystick 4 Up"),
+	TTRC("Joystick 4 Down"),
+};
+
+String InputEventConfigurationDialog::get_event_text(const Ref<InputEvent> &p_event) {
+	ERR_FAIL_COND_V_MSG(p_event.is_null(), String(), "Provided event is not a valid instance of InputEvent");
+
+	// Joypad motion events will display slighlty differently than what the event->as_text() provides. See #43660.
+	Ref<InputEventJoypadMotion> jpmotion = p_event;
+	if (jpmotion.is_valid()) {
+		String desc = TTR("Unknown Joypad Axis");
+		if (jpmotion->get_axis() < JOY_AXIS_MAX) {
+			desc = RTR(_joy_axis_descriptions[2 * jpmotion->get_axis() + (jpmotion->get_axis_value() < 0 ? 0 : 1)]);
+		}
+
+		return vformat("Joypad Axis %s %s (%s)", itos(jpmotion->get_axis()), jpmotion->get_axis_value() < 0 ? "-" : "+", desc);
+	} else {
+		return p_event->as_text();
+	}
+}
+
+void InputEventConfigurationDialog::_set_event(const Ref<InputEvent> &p_event) {
+	if (p_event.is_valid()) {
+		event = p_event;
+
+		// Update Label
+		event_as_text->set_text(get_event_text(event));
+
+		Ref<InputEventKey> k = p_event;
+		Ref<InputEventMouseButton> mb = p_event;
+		Ref<InputEventJoypadButton> joyb = p_event;
+		Ref<InputEventJoypadMotion> joym = p_event;
+		Ref<InputEventWithModifiers> mod = p_event;
+
+		// Update option values and visibility
+		bool show_mods = false;
+		bool show_device = false;
+		bool show_phys_key = false;
+
+		if (mod.is_valid()) {
+			show_mods = true;
+			mod_checkboxes[MOD_ALT]->set_pressed(mod->get_alt());
+			mod_checkboxes[MOD_SHIFT]->set_pressed(mod->get_shift());
+			mod_checkboxes[MOD_COMMAND]->set_pressed(mod->get_command());
+			mod_checkboxes[MOD_CONTROL]->set_pressed(mod->get_control());
+			mod_checkboxes[MOD_META]->set_pressed(mod->get_metakey());
+
+			store_command_checkbox->set_pressed(mod->is_storing_command());
+		}
+
+		if (k.is_valid()) {
+			show_phys_key = true;
+			physical_key_checkbox->set_pressed(k->get_physical_keycode() != 0 && k->get_keycode() == 0);
+
+		} else if (joyb.is_valid() || joym.is_valid() || mb.is_valid()) {
+			show_device = true;
+			_set_current_device(event->get_device());
+		}
+
+		mod_container->set_visible(show_mods);
+		device_container->set_visible(show_device);
+		physical_key_checkbox->set_visible(show_phys_key);
+		additional_options_container->show();
+
+		// Update selected item in input list for keys, joybuttons and joyaxis only (since the mouse cannot be "listened" for).
+		if (k.is_valid() || joyb.is_valid() || joym.is_valid()) {
+			TreeItem *category = input_list_tree->get_root()->get_children();
+			while (category) {
+				TreeItem *input_item = category->get_children();
+
+				// has_type this should be always true, unless the tree structure has been misconfigured.
+				bool has_type = input_item->get_parent()->has_meta("__type");
+				int input_type = input_item->get_parent()->get_meta("__type");
+				if (!has_type) {
+					return;
+				}
+
+				// If event type matches input types of this category.
+				if ((k.is_valid() && input_type == INPUT_KEY) || (joyb.is_valid() && input_type == INPUT_JOY_BUTTON) || (joym.is_valid() && input_type == INPUT_JOY_MOTION)) {
+					// Loop through all items of this category until one matches.
+					while (input_item) {
+						bool key_match = k.is_valid() && (Variant(k->get_keycode()) == input_item->get_meta("__keycode") || Variant(k->get_physical_keycode()) == input_item->get_meta("__keycode"));
+						bool joyb_match = joyb.is_valid() && Variant(joyb->get_button_index()) == input_item->get_meta("__index");
+						bool joym_match = joym.is_valid() && Variant(joym->get_axis()) == input_item->get_meta("__axis") && joym->get_axis_value() == (float)input_item->get_meta("__value");
+						if (key_match || joyb_match || joym_match) {
+							category->set_collapsed(false);
+							input_item->select(0);
+							input_list_tree->ensure_cursor_is_visible();
+							return;
+						}
+						input_item = input_item->get_next();
+					}
+				}
+
+				category->set_collapsed(true); // Event not in this category, so collapse;
+				category = category->get_next();
+			}
+		}
+	} else {
+		// Event is not valid, reset dialog
+		event = p_event;
+		Vector<String> strings;
+
+		// Reset message, promp for input according to which input types are allowed.
+		String text = TTR("Perform an Input (%s).");
+
+		if (allowed_input_types & INPUT_KEY) {
+			strings.append(TTR("Key"));
+		}
+		// We don't check for INPUT_MOUSE_BUTTON since it is ignored in the "Listen Window Input" method.
+
+		if (allowed_input_types & INPUT_JOY_BUTTON) {
+			strings.append(TTR("Joypad Button"));
+		}
+		if (allowed_input_types & INPUT_JOY_MOTION) {
+			strings.append(TTR("Joypad Axis"));
+		}
+
+		if (strings.size() == 0) {
+			text = TTR("Input Event dialog has been misconfigured: No input types are allowed.");
+			event_as_text->set_text(text);
+		} else {
+			String insert_text = String(", ").join(strings);
+			event_as_text->set_text(vformat(text, insert_text));
+		}
+
+		additional_options_container->hide();
+		input_list_tree->deselect_all();
+		_update_input_list();
+	}
+}
+
+void InputEventConfigurationDialog::_tab_selected(int p_tab) {
+	Callable signal_method = callable_mp(this, &InputEventConfigurationDialog::_listen_window_input);
+	if (p_tab == 0) {
+		// Start Listening.
+		if (!is_connected("window_input", signal_method)) {
+			connect("window_input", signal_method);
+		}
+	} else {
+		// Stop Listening.
+		if (is_connected("window_input", signal_method)) {
+			disconnect("window_input", signal_method);
+		}
+		input_list_tree->call_deferred("ensure_cursor_is_visible");
+		if (input_list_tree->get_selected() == nullptr) {
+			// If nothing selected, scroll to top.
+			input_list_tree->scroll_to_item(input_list_tree->get_root());
+		}
+	}
+}
+
+void InputEventConfigurationDialog::_listen_window_input(const Ref<InputEvent> &p_event) {
+	// Ignore if echo or not pressed
+	if (p_event->is_echo() || !p_event->is_pressed()) {
+		return;
+	}
+
+	// Ignore mouse
+	Ref<InputEventMouse> m = p_event;
+	if (m.is_valid()) {
+		return;
+	}
+
+	// Check what the type is and if it is allowed.
+	Ref<InputEventKey> k = p_event;
+	Ref<InputEventJoypadButton> joyb = p_event;
+	Ref<InputEventJoypadMotion> joym = p_event;
+
+	int type = k.is_valid() ? INPUT_KEY : joyb.is_valid() ? INPUT_JOY_BUTTON :
+								  joym.is_valid()		  ? INPUT_JOY_MOTION :
+															  0;
+
+	if (!(allowed_input_types & type)) {
+		return;
+	}
+
+	if (joym.is_valid()) {
+		float axis_value = joym->get_axis_value();
+		if (ABS(axis_value) < 0.9) {
+			// Ignore motion below 0.9 magnitude to avoid accidental touches
+			return;
+		} else {
+			// Always make the value 1 or -1 for display consistency
+			joym->set_axis_value(SGN(axis_value));
+		}
+	}
+
+	if (k.is_valid()) {
+		k->set_pressed(false); // to avoid serialisation of 'pressed' property - doesn't matter for actions anyway.
+		// Maintain physical keycode option state
+		if (physical_key_checkbox->is_pressed()) {
+			k->set_physical_keycode(k->get_keycode());
+			k->set_keycode(0);
+		} else {
+			k->set_keycode(k->get_physical_keycode());
+			k->set_physical_keycode(0);
+		}
+	}
+
+	Ref<InputEventWithModifiers> mod = p_event;
+	if (mod.is_valid()) {
+		// Maintain store command option state
+		mod->set_store_command(store_command_checkbox->is_pressed());
+
+		mod->set_window_id(0);
+	}
+
+	_set_event(p_event);
+	set_input_as_handled();
+}
+
+void InputEventConfigurationDialog::_search_term_updated(const String &) {
+	_update_input_list();
+}
+
+void InputEventConfigurationDialog::_update_input_list() {
+	input_list_tree->clear();
+
+	TreeItem *root = input_list_tree->create_item();
+	String search_term = input_list_search->get_text();
+
+	bool collapse = input_list_search->get_text().is_empty();
+
+	if (allowed_input_types & INPUT_KEY) {
+		TreeItem *kb_root = input_list_tree->create_item(root);
+		kb_root->set_text(0, TTR("Keyboard Keys"));
+		kb_root->set_icon(0, icon_cache.keyboard);
+		kb_root->set_collapsed(collapse);
+		kb_root->set_meta("__type", INPUT_KEY);
+
+		for (int i = 0; i < keycode_get_count(); i++) {
+			String name = keycode_get_name_by_index(i);
+
+			if (!search_term.is_empty() && name.findn(search_term) == -1) {
+				continue;
+			}
+
+			TreeItem *item = input_list_tree->create_item(kb_root);
+			item->set_text(0, name);
+			item->set_meta("__keycode", keycode_get_value_by_index(i));
+		}
+	}
+
+	if (allowed_input_types & INPUT_MOUSE_BUTTON) {
+		TreeItem *mouse_root = input_list_tree->create_item(root);
+		mouse_root->set_text(0, TTR("Mouse Buttons"));
+		mouse_root->set_icon(0, icon_cache.mouse);
+		mouse_root->set_collapsed(collapse);
+		mouse_root->set_meta("__type", INPUT_MOUSE_BUTTON);
+
+		int mouse_buttons[9] = { BUTTON_LEFT, BUTTON_RIGHT, BUTTON_MIDDLE, BUTTON_WHEEL_UP, BUTTON_WHEEL_DOWN, BUTTON_WHEEL_LEFT, BUTTON_WHEEL_RIGHT, BUTTON_XBUTTON1, BUTTON_XBUTTON2 };
+		for (int i = 0; i < 9; i++) {
+			Ref<InputEventMouseButton> mb;
+			mb.instance();
+			mb->set_button_index(mouse_buttons[i]);
+			String desc = get_event_text(mb);
+
+			if (!search_term.is_empty() && desc.findn(search_term) == -1) {
+				continue;
+			}
+
+			TreeItem *item = input_list_tree->create_item(mouse_root);
+			item->set_text(0, desc);
+			item->set_meta("__index", mouse_buttons[i]);
+		}
+	}
+
+	if (allowed_input_types & INPUT_JOY_BUTTON) {
+		TreeItem *joyb_root = input_list_tree->create_item(root);
+		joyb_root->set_text(0, TTR("Joypad Buttons"));
+		joyb_root->set_icon(0, icon_cache.joypad_button);
+		joyb_root->set_collapsed(collapse);
+		joyb_root->set_meta("__type", INPUT_JOY_BUTTON);
+
+		for (int i = 0; i < JOY_BUTTON_MAX; i++) {
+			Ref<InputEventJoypadButton> joyb;
+			joyb.instance();
+			joyb->set_button_index(i);
+			String desc = get_event_text(joyb);
+
+			if (!search_term.is_empty() && desc.findn(search_term) == -1) {
+				continue;
+			}
+
+			TreeItem *item = input_list_tree->create_item(joyb_root);
+			item->set_text(0, desc);
+			item->set_meta("__index", i);
+		}
+	}
+
+	if (allowed_input_types & INPUT_JOY_MOTION) {
+		TreeItem *joya_root = input_list_tree->create_item(root);
+		joya_root->set_text(0, TTR("Joypad Axes"));
+		joya_root->set_icon(0, icon_cache.joypad_axis);
+		joya_root->set_collapsed(collapse);
+		joya_root->set_meta("__type", INPUT_JOY_MOTION);
+
+		for (int i = 0; i < JOY_AXIS_MAX * 2; i++) {
+			int axis = i / 2;
+			int direction = (i & 1) ? 1 : -1;
+			Ref<InputEventJoypadMotion> joym;
+			joym.instance();
+			joym->set_axis(axis);
+			joym->set_axis_value(direction);
+			String desc = get_event_text(joym);
+
+			if (!search_term.is_empty() && desc.findn(search_term) == -1) {
+				continue;
+			}
+
+			TreeItem *item = input_list_tree->create_item(joya_root);
+			item->set_text(0, desc);
+			item->set_meta("__axis", i >> 1);
+			item->set_meta("__value", (i & 1) ? 1 : -1);
+		}
+	}
+}
+
+void InputEventConfigurationDialog::_mod_toggled(bool p_checked, int p_index) {
+	Ref<InputEventWithModifiers> ie = event;
+
+	// Not event with modifiers
+	if (ie.is_null()) {
+		return;
+	}
+
+	if (p_index == 0) {
+		ie->set_alt(p_checked);
+	} else if (p_index == 1) {
+		ie->set_shift(p_checked);
+	} else if (p_index == 2) {
+		ie->set_command(p_checked);
+	} else if (p_index == 3) {
+		ie->set_control(p_checked);
+	} else if (p_index == 4) {
+		ie->set_metakey(p_checked);
+	}
+
+	_set_event(ie);
+}
+
+void InputEventConfigurationDialog::_store_command_toggled(bool p_checked) {
+	Ref<InputEventWithModifiers> ie = event;
+	if (ie.is_valid()) {
+		ie->set_store_command(p_checked);
+		_set_event(ie);
+	}
+
+	if (p_checked) {
+		// If storing Command, show it's checkbox and hide Control (Win/Lin) or Meta (Mac)
+#ifdef APPLE_STYLE_KEYS
+		mod_checkboxes[MOD_META]->hide();
+
+		mod_checkboxes[MOD_COMMAND]->show();
+		mod_checkboxes[MOD_COMMAND]->set_text("Meta (Command)");
+#else
+		mod_checkboxes[MOD_CONTROL]->hide();
+
+		mod_checkboxes[MOD_COMMAND]->show();
+		mod_checkboxes[MOD_COMMAND]->set_text("Control (Command)");
+#endif
+	} else {
+		// If not, hide Command, show Control and Meta.
+		mod_checkboxes[MOD_COMMAND]->hide();
+		mod_checkboxes[MOD_CONTROL]->show();
+		mod_checkboxes[MOD_META]->show();
+	}
+}
+
+void InputEventConfigurationDialog::_physical_keycode_toggled(bool p_checked) {
+	Ref<InputEventKey> k = event;
+
+	if (k.is_null()) {
+		return;
+	}
+
+	if (p_checked) {
+		k->set_physical_keycode(k->get_keycode());
+		k->set_keycode(0);
+	} else {
+		k->set_keycode(k->get_physical_keycode());
+		k->set_physical_keycode(0);
+	}
+
+	_set_event(k);
+}
+
+void InputEventConfigurationDialog::_input_list_item_selected() {
+	TreeItem *selected = input_list_tree->get_selected();
+
+	// Invalid tree selection - type only exists on the "category" items, which are not a valid selection.
+	if (selected->has_meta("__type")) {
+		return;
+	}
+
+	int input_type = selected->get_parent()->get_meta("__type");
+
+	switch (input_type) {
+		case InputEventConfigurationDialog::INPUT_KEY: {
+			int kc = selected->get_meta("__keycode");
+			Ref<InputEventKey> k;
+			k.instance();
+
+			if (physical_key_checkbox->is_pressed()) {
+				k->set_physical_keycode(kc);
+				k->set_keycode(0);
+			} else {
+				k->set_physical_keycode(0);
+				k->set_keycode(kc);
+			}
+
+			// Maintain modifier state from checkboxes
+			k->set_alt(mod_checkboxes[MOD_ALT]->is_pressed());
+			k->set_shift(mod_checkboxes[MOD_SHIFT]->is_pressed());
+			k->set_command(mod_checkboxes[MOD_COMMAND]->is_pressed());
+			k->set_control(mod_checkboxes[MOD_CONTROL]->is_pressed());
+			k->set_metakey(mod_checkboxes[MOD_META]->is_pressed());
+			k->set_store_command(store_command_checkbox->is_pressed());
+
+			_set_event(k);
+		} break;
+		case InputEventConfigurationDialog::INPUT_MOUSE_BUTTON: {
+			int idx = selected->get_meta("__index");
+			Ref<InputEventMouseButton> mb;
+			mb.instance();
+			mb->set_button_index(idx);
+			// Maintain modifier state from checkboxes
+			mb->set_alt(mod_checkboxes[MOD_ALT]->is_pressed());
+			mb->set_shift(mod_checkboxes[MOD_SHIFT]->is_pressed());
+			mb->set_command(mod_checkboxes[MOD_COMMAND]->is_pressed());
+			mb->set_control(mod_checkboxes[MOD_CONTROL]->is_pressed());
+			mb->set_metakey(mod_checkboxes[MOD_META]->is_pressed());
+			mb->set_store_command(store_command_checkbox->is_pressed());
+
+			_set_event(mb);
+		} break;
+		case InputEventConfigurationDialog::INPUT_JOY_BUTTON: {
+			int idx = selected->get_meta("__index");
+			Ref<InputEventJoypadButton> jb = InputEventJoypadButton::create_reference(idx);
+			_set_event(jb);
+		} break;
+		case InputEventConfigurationDialog::INPUT_JOY_MOTION: {
+			int axis = selected->get_meta("__axis");
+			int value = selected->get_meta("__value");
+
+			Ref<InputEventJoypadMotion> jm;
+			jm.instance();
+			jm->set_axis(axis);
+			jm->set_axis_value(value);
+			_set_event(jm);
+		} break;
+		default:
+			break;
+	}
+}
+
+void InputEventConfigurationDialog::_set_current_device(int i_device) {
+	device_id_option->select(i_device + 1);
+}
+
+int InputEventConfigurationDialog::_get_current_device() const {
+	return device_id_option->get_selected() - 1;
+}
+
+String InputEventConfigurationDialog::_get_device_string(int i_device) const {
+	if (i_device == InputMap::ALL_DEVICES) {
+		return TTR("All Devices");
+	}
+	return TTR("Device") + " " + itos(i_device);
+}
+
+void InputEventConfigurationDialog::_notification(int p_what) {
+	switch (p_what) {
+		case NOTIFICATION_ENTER_TREE:
+		case NOTIFICATION_THEME_CHANGED: {
+			input_list_search->set_right_icon(input_list_search->get_theme_icon("Search", "EditorIcons"));
+
+			physical_key_checkbox->set_icon(get_theme_icon("KeyboardPhysical", "EditorIcons"));
+
+			icon_cache.keyboard = get_theme_icon("Keyboard", "EditorIcons");
+			icon_cache.mouse = get_theme_icon("Mouse", "EditorIcons");
+			icon_cache.joypad_button = get_theme_icon("JoyButton", "EditorIcons");
+			icon_cache.joypad_axis = get_theme_icon("JoyAxis", "EditorIcons");
+
+			_update_input_list();
+		} break;
+		default:
+			break;
+	}
+}
+
+void InputEventConfigurationDialog::popup_and_configure(const Ref<InputEvent> &p_event) {
+	if (p_event.is_valid()) {
+		_set_event(p_event);
+	} else {
+		// Clear Event
+		_set_event(p_event);
+
+		// Clear Checkbox Values
+		for (int i = 0; i < MOD_MAX; i++) {
+			mod_checkboxes[i]->set_pressed(false);
+		}
+		physical_key_checkbox->set_pressed(false);
+		store_command_checkbox->set_pressed(true);
+		_set_current_device(0);
+
+		// Switch to "Listen" tab
+		tab_container->set_current_tab(0);
+	}
+
+	popup_centered();
+}
+
+Ref<InputEvent> InputEventConfigurationDialog::get_event() const {
+	return event;
+}
+
+void InputEventConfigurationDialog::set_allowed_input_types(int p_type_masks) {
+	allowed_input_types = p_type_masks;
+}
+
+InputEventConfigurationDialog::InputEventConfigurationDialog() {
+	allowed_input_types = INPUT_KEY | INPUT_MOUSE_BUTTON | INPUT_JOY_BUTTON | INPUT_JOY_MOTION;
+
+	set_title("Event Configuration");
+	set_min_size(Size2i(550 * EDSCALE, 0)); // Min width
+
+	VBoxContainer *main_vbox = memnew(VBoxContainer);
+	add_child(main_vbox);
+
+	tab_container = memnew(TabContainer);
+	tab_container->set_tab_align(TabContainer::TabAlign::ALIGN_LEFT);
+	tab_container->set_use_hidden_tabs_for_min_size(true);
+	tab_container->set_v_size_flags(Control::SIZE_EXPAND_FILL);
+	tab_container->connect("tab_selected", callable_mp(this, &InputEventConfigurationDialog::_tab_selected));
+	main_vbox->add_child(tab_container);
+
+	CenterContainer *cc = memnew(CenterContainer);
+	cc->set_name("Listen for Input");
+	event_as_text = memnew(Label);
+	event_as_text->set_align(Label::ALIGN_CENTER);
+	cc->add_child(event_as_text);
+	tab_container->add_child(cc);
+
+	// List of all input options to manually select from.
+
+	VBoxContainer *manual_vbox = memnew(VBoxContainer);
+	manual_vbox->set_name("Manual Selection");
+	manual_vbox->set_v_size_flags(Control::SIZE_EXPAND_FILL);
+	tab_container->add_child(manual_vbox);
+
+	input_list_search = memnew(LineEdit);
+	input_list_search->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+	input_list_search->set_placeholder(TTR("Filter Inputs"));
+	input_list_search->set_clear_button_enabled(true);
+	input_list_search->connect("text_changed", callable_mp(this, &InputEventConfigurationDialog::_search_term_updated));
+	manual_vbox->add_child(input_list_search);
+
+	input_list_tree = memnew(Tree);
+	input_list_tree->set_custom_minimum_size(Size2(0, 100 * EDSCALE)); // Min height for tree
+	input_list_tree->connect("item_selected", callable_mp(this, &InputEventConfigurationDialog::_input_list_item_selected));
+	input_list_tree->set_v_size_flags(Control::SIZE_EXPAND_FILL);
+	manual_vbox->add_child(input_list_tree);
+
+	input_list_tree->set_hide_root(true);
+	input_list_tree->set_columns(1);
+
+	_update_input_list();
+
+	// Additional Options
+	additional_options_container = memnew(VBoxContainer);
+	additional_options_container->hide();
+
+	Label *opts_label = memnew(Label);
+	opts_label->set_text("Additional Options");
+	additional_options_container->add_child(opts_label);
+
+	// Device Selection
+	device_container = memnew(HBoxContainer);
+	device_container->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+
+	Label *device_label = memnew(Label);
+	device_label->set_text("Device:");
+	device_container->add_child(device_label);
+
+	device_id_option = memnew(OptionButton);
+	device_id_option->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+	device_container->add_child(device_id_option);
+
+	for (int i = -1; i < 8; i++) {
+		device_id_option->add_item(_get_device_string(i));
+	}
+	_set_current_device(0);
+	device_container->hide();
+	additional_options_container->add_child(device_container);
+
+	// Modifier Selection
+	mod_container = memnew(HBoxContainer);
+	for (int i = 0; i < MOD_MAX; i++) {
+		String name = mods[i];
+		mod_checkboxes[i] = memnew(CheckBox);
+		mod_checkboxes[i]->connect("toggled", callable_mp(this, &InputEventConfigurationDialog::_mod_toggled), varray(i));
+		mod_checkboxes[i]->set_text(name);
+		mod_container->add_child(mod_checkboxes[i]);
+	}
+
+	mod_container->add_child(memnew(VSeparator));
+
+	store_command_checkbox = memnew(CheckBox);
+	store_command_checkbox->connect("toggled", callable_mp(this, &InputEventConfigurationDialog::_store_command_toggled));
+	store_command_checkbox->set_pressed(true);
+	store_command_checkbox->set_text(TTR("Store Command"));
+#ifdef APPLE_STYLE_KEYS
+	store_command_checkbox->set_tooltip(TTR("Toggles between serializing 'command' and 'meta'. Used for compatibility with Windows/Linux style keyboard."));
+#else
+	store_command_checkbox->set_tooltip(TTR("Toggles between serializing 'command' and 'control'. Used for compatibility with Apple Style keyboards."));
+#endif
+	mod_container->add_child(store_command_checkbox);
+
+	mod_container->hide();
+	additional_options_container->add_child(mod_container);
+
+	// Physical Key Checkbox
+
+	physical_key_checkbox = memnew(CheckBox);
+	physical_key_checkbox->set_text(TTR("Use Physical Keycode"));
+	physical_key_checkbox->set_tooltip(TTR("Stores the physical position of the key on the keyboard rather than the keys value. Used for compatibility with non-latin layouts."));
+	physical_key_checkbox->connect("toggled", callable_mp(this, &InputEventConfigurationDialog::_physical_keycode_toggled));
+	physical_key_checkbox->hide();
+	additional_options_container->add_child(physical_key_checkbox);
+
+	main_vbox->add_child(additional_options_container);
+
+	// Default to first tab
+	tab_container->set_current_tab(0);
+}
+
+/////////////////////////////////////////
+
+static bool _is_action_name_valid(const String &p_name) {
+	const char32_t *cstr = p_name.get_data();
+	for (int i = 0; cstr[i]; i++) {
+		if (cstr[i] == '/' || cstr[i] == ':' || cstr[i] == '"' ||
+				cstr[i] == '=' || cstr[i] == '\\' || cstr[i] < 32) {
+			return false;
+		}
+	}
+	return true;
+}
+
+void ActionMapEditor::_event_config_confirmed() {
+	Ref<InputEvent> ev = event_config_dialog->get_event();
+
+	Dictionary new_action = current_action.duplicate();
+	Array events = new_action["events"];
+
+	if (current_action_event_index == -1) {
+		// Add new event
+		events.push_back(ev);
+	} else {
+		// Edit existing event
+		events[current_action_event_index] = ev;
+	}
+
+	new_action["events"] = events;
+	emit_signal("action_edited", current_action_name, new_action);
+}
+
+void ActionMapEditor::_add_action_pressed() {
+	_add_action(add_edit->get_text());
+}
+
+void ActionMapEditor::_add_action(const String &p_name) {
+	if (!allow_editing_actions) {
+		return;
+	}
+
+	if (p_name == "" || !_is_action_name_valid(p_name)) {
+		show_message(TTR("Invalid action name. it cannot be.is_empty()() nor contain '/', ':', '=', '\\' or '\"'"));
+		return;
+	}
+
+	add_edit->clear();
+	emit_signal("action_added", p_name);
+}
+
+void ActionMapEditor::_action_edited() {
+	if (!allow_editing_actions) {
+		return;
+	}
+
+	TreeItem *ti = action_tree->get_edited();
+	if (!ti) {
+		return;
+	}
+
+	if (action_tree->get_selected_column() == 0) {
+		// Name Edited
+		String new_name = ti->get_text(0);
+		String old_name = ti->get_meta("__name");
+
+		if (new_name == old_name) {
+			return;
+		}
+
+		if (new_name == "" || !_is_action_name_valid(new_name)) {
+			ti->set_text(0, old_name);
+			show_message(TTR("Invalid action name. it cannot be.is_empty()() nor contain '/', ':', '=', '\\' or '\"'"));
+			return;
+		}
+
+		emit_signal("action_renamed", old_name, new_name);
+	} else if (action_tree->get_selected_column() == 1) {
+		// Deadzone Edited
+		String name = ti->get_meta("__name");
+		Dictionary old_action = ti->get_meta("__action");
+		Dictionary new_action = old_action.duplicate();
+		new_action["deadzone"] = ti->get_range(1);
+
+		// Call deferred so that input can finish propagating through tree, allowing re-making of tree to occur.
+		call_deferred("emit_signal", "action_edited", name, new_action);
+	}
+}
+
+void ActionMapEditor::_tree_button_pressed(Object *p_item, int p_column, int p_id) {
+	ItemButton option = (ItemButton)p_id;
+
+	TreeItem *item = Object::cast_to<TreeItem>(p_item);
+	if (!item) {
+		return;
+	}
+
+	switch (option) {
+		case ActionMapEditor::BUTTON_ADD_EVENT: {
+			current_action = item->get_meta("__action");
+			current_action_name = item->get_meta("__name");
+			current_action_event_index = -1;
+
+			event_config_dialog->popup_and_configure();
+
+		} break;
+		case ActionMapEditor::BUTTON_EDIT_EVENT: {
+			// Action and Action name is located on the parent of the event.
+			current_action = item->get_parent()->get_meta("__action");
+			current_action_name = item->get_parent()->get_meta("__name");
+
+			current_action_event_index = item->get_meta("__index");
+
+			Ref<InputEvent> ie = item->get_meta("__event");
+			if (ie.is_valid()) {
+				event_config_dialog->popup_and_configure(ie);
+			}
+
+		} break;
+		case ActionMapEditor::BUTTON_REMOVE_ACTION: {
+			if (!allow_editing_actions) {
+				break;
+			}
+
+			// Send removed action name
+			String name = item->get_meta("__name");
+			emit_signal("action_removed", name);
+		} break;
+		case ActionMapEditor::BUTTON_REMOVE_EVENT: {
+			// Remove event and send updated action
+			Dictionary action = item->get_parent()->get_meta("__action");
+			String action_name = item->get_parent()->get_meta("__name");
+
+			int event_index = item->get_meta("__index");
+
+			Array events = action["events"];
+			events.remove(event_index);
+			action["events"] = events;
+
+			emit_signal("action_edited", action_name, action);
+		} break;
+		default:
+			break;
+	}
+}
+
+void ActionMapEditor::_tree_item_activated() {
+	TreeItem *item = action_tree->get_selected();
+
+	if (!item || !item->has_meta("__event")) {
+		return;
+	}
+
+	_tree_button_pressed(item, 2, BUTTON_EDIT_EVENT);
+}
+
+void ActionMapEditor::set_show_uneditable(bool p_show) {
+	show_uneditable = p_show;
+	show_uneditable_actions_checkbox->set_pressed(p_show);
+
+	// Prevent unnecessary updates of action list when cache is.is_empty()().
+	if (!actions_cache.is_empty()) {
+		update_action_list();
+	}
+}
+
+void ActionMapEditor::_search_term_updated(const String &) {
+	update_action_list();
+}
+
+Variant ActionMapEditor::get_drag_data_fw(const Point2 &p_point, Control *p_from) {
+	TreeItem *selected = action_tree->get_selected();
+	if (!selected) {
+		return Variant();
+	}
+
+	String name = selected->get_text(0);
+	Label *label = memnew(Label(name));
+	label->set_modulate(Color(1, 1, 1, 1.0f));
+	action_tree->set_drag_preview(label);
+
+	Dictionary drag_data;
+
+	if (selected->has_meta("__action")) {
+		drag_data["input_type"] = "action";
+	}
+
+	if (selected->has_meta("__event")) {
+		drag_data["input_type"] = "event";
+	}
+
+	action_tree->set_drop_mode_flags(Tree::DROP_MODE_INBETWEEN);
+
+	return drag_data;
+}
+
+bool ActionMapEditor::can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const {
+	Dictionary d = p_data;
+	if (!d.has("input_type")) {
+		return false;
+	}
+
+	TreeItem *selected = action_tree->get_selected();
+	TreeItem *item = action_tree->get_item_at_position(p_point);
+	if (!selected || !item || item == selected) {
+		return false;
+	}
+
+	// Don't allow moving an action in-between events.
+	if (d["input_type"] == "action" && item->has_meta("__event")) {
+		return false;
+	}
+
+	// Don't allow moving an event to a different action.
+	if (d["input_type"] == "event" && item->get_parent() != selected->get_parent()) {
+		return false;
+	}
+
+	return true;
+}
+
+void ActionMapEditor::drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) {
+	if (!can_drop_data_fw(p_point, p_data, p_from)) {
+		return;
+	}
+
+	TreeItem *selected = action_tree->get_selected();
+	TreeItem *target = action_tree->get_item_at_position(p_point);
+	bool drop_above = action_tree->get_drop_section_at_position(p_point) == -1;
+
+	if (!target) {
+		return;
+	}
+
+	Dictionary d = p_data;
+	if (d["input_type"] == "action") {
+		// Change action order.
+		String relative_to = target->get_meta("__name");
+		String action_name = selected->get_meta("__name");
+		emit_signal("action_reordered", action_name, relative_to, drop_above);
+
+	} else if (d["input_type"] == "event") {
+		// Change event order
+		int current_index = selected->get_meta("__index");
+		int target_index = target->get_meta("__index");
+
+		// Construct new events array.
+		Dictionary new_action = selected->get_parent()->get_meta("__action");
+
+		Array events = new_action["events"];
+		Array new_events;
+
+		// The following method was used to perform the array changes since `remove` followed by `insert` was not working properly at time of writing.
+		// Loop thought existing events
+		for (int i = 0; i < events.size(); i++) {
+			// If you come across the current index, just skip it, as it has been moved.
+			if (i == current_index) {
+				continue;
+			} else if (i == target_index) {
+				// We are at the target index. If drop above, add selected event there first, then target, so moved event goes on top.
+				if (drop_above) {
+					new_events.push_back(events[current_index]);
+					new_events.push_back(events[target_index]);
+				} else {
+					new_events.push_back(events[target_index]);
+					new_events.push_back(events[current_index]);
+				}
+			} else {
+				new_events.push_back(events[i]);
+			}
+		}
+
+		new_action["events"] = new_events;
+		emit_signal("action_edited", selected->get_parent()->get_meta("__name"), new_action);
+	}
+}
+
+void ActionMapEditor::_notification(int p_what) {
+	switch (p_what) {
+		case NOTIFICATION_ENTER_TREE:
+		case NOTIFICATION_THEME_CHANGED: {
+			action_list_search->set_right_icon(get_theme_icon("Search", "EditorIcons"));
+		} break;
+		default:
+			break;
+	}
+}
+
+void ActionMapEditor::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("get_drag_data_fw"), &ActionMapEditor::get_drag_data_fw);
+	ClassDB::bind_method(D_METHOD("can_drop_data_fw"), &ActionMapEditor::can_drop_data_fw);
+	ClassDB::bind_method(D_METHOD("drop_data_fw"), &ActionMapEditor::drop_data_fw);
+
+	ADD_SIGNAL(MethodInfo("action_added", PropertyInfo(Variant::STRING, "name")));
+	ADD_SIGNAL(MethodInfo("action_edited", PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::DICTIONARY, "new_action")));
+	ADD_SIGNAL(MethodInfo("action_removed", PropertyInfo(Variant::STRING, "name")));
+	ADD_SIGNAL(MethodInfo("action_renamed", PropertyInfo(Variant::STRING, "old_name"), PropertyInfo(Variant::STRING, "new_name")));
+	ADD_SIGNAL(MethodInfo("action_reordered", PropertyInfo(Variant::STRING, "action_name"), PropertyInfo(Variant::STRING, "relative_to"), PropertyInfo(Variant::BOOL, "before")));
+}
+
+LineEdit *ActionMapEditor::get_search_box() const {
+	return action_list_search;
+}
+
+InputEventConfigurationDialog *ActionMapEditor::get_configuration_dialog() {
+	return event_config_dialog;
+}
+
+void ActionMapEditor::update_action_list(const Vector<ActionInfo> &p_action_infos) {
+	if (!p_action_infos.is_empty()) {
+		actions_cache = p_action_infos;
+	}
+
+	action_tree->clear();
+	TreeItem *root = action_tree->create_item();
+
+	int uneditable_count = 0;
+
+	for (int i = 0; i < actions_cache.size(); i++) {
+		ActionInfo action_info = actions_cache[i];
+
+		if (!action_info.editable) {
+			uneditable_count++;
+		}
+
+		String search_term = action_list_search->get_text();
+		if (!search_term.is_empty() && action_info.name.findn(search_term) == -1) {
+			continue;
+		}
+
+		if (!action_info.editable && !show_uneditable) {
+			continue;
+		}
+
+		const Array events = action_info.action["events"];
+		const Variant deadzone = action_info.action["deadzone"];
+
+		// Update Tree...
+
+		TreeItem *action_item = action_tree->create_item(root);
+		action_item->set_meta("__action", action_info.action);
+		action_item->set_meta("__name", action_info.name);
+
+		// First Column - Action Name
+		action_item->set_text(0, action_info.name);
+		action_item->set_editable(0, action_info.editable);
+		action_item->set_icon(0, action_info.icon);
+
+		// Second Column - Deadzone
+		action_item->set_editable(1, true);
+		action_item->set_cell_mode(1, TreeItem::CELL_MODE_RANGE);
+		action_item->set_range_config(1, 0.0, 1.0, 0.01);
+		action_item->set_range(1, deadzone);
+
+		// Third column - buttons
+		action_item->add_button(2, action_tree->get_theme_icon("Add", "EditorIcons"), BUTTON_ADD_EVENT, false, TTR("Add Event"));
+		action_item->add_button(2, action_tree->get_theme_icon("Remove", "EditorIcons"), BUTTON_REMOVE_ACTION, !action_info.editable, action_info.editable ? "Remove Action" : "Cannot Remove Action");
+
+		action_item->set_custom_bg_color(0, action_tree->get_theme_color("prop_subsection", "Editor"));
+		action_item->set_custom_bg_color(1, action_tree->get_theme_color("prop_subsection", "Editor"));
+
+		for (int evnt_idx = 0; evnt_idx < events.size(); evnt_idx++) {
+			Ref<InputEvent> event = events[evnt_idx];
+			if (event.is_null()) {
+				continue;
+			}
+
+			TreeItem *event_item = action_tree->create_item(action_item);
+
+			// First Column - Text
+			event_item->set_text(0, event_config_dialog->get_event_text(event)); // Need to us the special description for JoypadMotion here, so don't use as_text() directly.
+			event_item->set_meta("__event", event);
+			event_item->set_meta("__index", evnt_idx);
+
+			// Third Column - Buttons
+			event_item->add_button(2, action_tree->get_theme_icon("Edit", "EditorIcons"), BUTTON_EDIT_EVENT, false, TTR("Edit Event"));
+			event_item->add_button(2, action_tree->get_theme_icon("Remove", "EditorIcons"), BUTTON_REMOVE_EVENT, false, TTR("Remove Event"));
+			event_item->set_button_color(2, 0, Color(1, 1, 1, 0.75));
+			event_item->set_button_color(2, 1, Color(1, 1, 1, 0.75));
+		}
+	}
+}
+
+void ActionMapEditor::show_message(const String &p_message) {
+	message->set_text(p_message);
+	message->popup_centered(Size2(300, 100) * EDSCALE);
+}
+
+void ActionMapEditor::set_allow_editing_actions(bool p_allow) {
+	allow_editing_actions = p_allow;
+	add_hbox->set_visible(p_allow);
+}
+
+void ActionMapEditor::set_toggle_editable_label(const String &p_label) {
+	show_uneditable_actions_checkbox->set_text(p_label);
+}
+
+void ActionMapEditor::use_external_search_box(LineEdit *p_searchbox) {
+	memdelete(action_list_search);
+	action_list_search = p_searchbox;
+	action_list_search->connect("text_changed", callable_mp(this, &ActionMapEditor::_search_term_updated));
+}
+
+ActionMapEditor::ActionMapEditor() {
+	allow_editing_actions = true;
+	show_uneditable = true;
+
+	// Main Vbox Container
+	VBoxContainer *main_vbox = memnew(VBoxContainer);
+	main_vbox->set_anchors_and_offsets_preset(PRESET_WIDE);
+	add_child(main_vbox);
+
+	HBoxContainer *top_hbox = memnew(HBoxContainer);
+	main_vbox->add_child(top_hbox);
+
+	action_list_search = memnew(LineEdit);
+	action_list_search->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+	action_list_search->set_placeholder(TTR("Filter Actions"));
+	action_list_search->set_clear_button_enabled(true);
+	action_list_search->connect("text_changed", callable_mp(this, &ActionMapEditor::_search_term_updated));
+	top_hbox->add_child(action_list_search);
+
+	show_uneditable_actions_checkbox = memnew(CheckBox);
+	show_uneditable_actions_checkbox->set_pressed(false);
+	show_uneditable_actions_checkbox->set_text(TTR("Show Uneditable Actions"));
+	show_uneditable_actions_checkbox->connect("toggled", callable_mp(this, &ActionMapEditor::set_show_uneditable));
+	top_hbox->add_child(show_uneditable_actions_checkbox);
+
+	// Adding Action line edit + button
+	add_hbox = memnew(HBoxContainer);
+	add_hbox->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+
+	add_edit = memnew(LineEdit);
+	add_edit->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+	add_edit->set_placeholder(TTR("Add New Action"));
+	add_edit->set_clear_button_enabled(true);
+	add_edit->connect("text_entered", callable_mp(this, &ActionMapEditor::_add_action));
+	add_hbox->add_child(add_edit);
+
+	Button *add_button = memnew(Button);
+	add_button->set_text("Add");
+	add_button->connect("pressed", callable_mp(this, &ActionMapEditor::_add_action_pressed));
+	add_hbox->add_child(add_button);
+
+	main_vbox->add_child(add_hbox);
+
+	// Action Editor Tree
+	action_tree = memnew(Tree);
+	action_tree->set_v_size_flags(Control::SIZE_EXPAND_FILL);
+	action_tree->set_columns(3);
+	action_tree->set_hide_root(true);
+	action_tree->set_column_titles_visible(true);
+	action_tree->set_column_title(0, TTR("Action"));
+	action_tree->set_column_title(1, TTR("Deadzone"));
+	action_tree->set_column_expand(1, false);
+	action_tree->set_column_min_width(1, 80 * EDSCALE);
+	action_tree->set_column_expand(2, false);
+	action_tree->set_column_min_width(2, 50 * EDSCALE);
+	action_tree->connect("item_edited", callable_mp(this, &ActionMapEditor::_action_edited));
+	action_tree->connect("item_activated", callable_mp(this, &ActionMapEditor::_tree_item_activated));
+	action_tree->connect("button_pressed", callable_mp(this, &ActionMapEditor::_tree_button_pressed));
+	main_vbox->add_child(action_tree);
+
+	action_tree->set_drag_forwarding(this);
+
+	// Adding event dialog
+	event_config_dialog = memnew(InputEventConfigurationDialog);
+	event_config_dialog->connect("confirmed", callable_mp(this, &ActionMapEditor::_event_config_confirmed));
+	add_child(event_config_dialog);
+
+	message = memnew(AcceptDialog);
+	add_child(message);
+}

+ 203 - 0
editor/action_map_editor.h

@@ -0,0 +1,203 @@
+/*************************************************************************/
+/*  action_map_editor.h                                                  */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+#ifndef ACTION_MAP_EDITOR_H
+#define ACTION_MAP_EDITOR_H
+
+#include "editor/editor_data.h"
+
+// Confirmation Dialog used when configuring an input event.
+// Separate from ActionMapEditor for code cleanliness and separation of responsibilities.
+class InputEventConfigurationDialog : public ConfirmationDialog {
+	GDCLASS(InputEventConfigurationDialog, ConfirmationDialog);
+
+public:
+	enum InputType {
+		INPUT_KEY = 1,
+		INPUT_MOUSE_BUTTON = 2,
+		INPUT_JOY_BUTTON = 4,
+		INPUT_JOY_MOTION = 8
+	};
+
+private:
+	struct IconCache {
+		Ref<Texture2D> keyboard;
+		Ref<Texture2D> mouse;
+		Ref<Texture2D> joypad_button;
+		Ref<Texture2D> joypad_axis;
+	} icon_cache;
+
+	Ref<InputEvent> event = Ref<InputEvent>();
+
+	TabContainer *tab_container;
+
+	// Listening for input
+	Label *event_as_text;
+
+	// List of All Key/Mouse/Joypad input options.
+	int allowed_input_types;
+	Tree *input_list_tree;
+	LineEdit *input_list_search;
+
+	// Additional Options, shown depending on event selected
+	VBoxContainer *additional_options_container;
+
+	HBoxContainer *device_container;
+	OptionButton *device_id_option;
+
+	HBoxContainer *mod_container; // Contains the subcontainer and the store command checkbox.
+
+	enum ModCheckbox {
+		MOD_ALT,
+		MOD_SHIFT,
+		MOD_COMMAND,
+		MOD_CONTROL,
+		MOD_META,
+		MOD_MAX
+	};
+	String mods[MOD_MAX] = { "Alt", "Shift", "Command", "Control", "Meta" };
+
+	CheckBox *mod_checkboxes[MOD_MAX];
+	CheckBox *store_command_checkbox;
+
+	CheckBox *physical_key_checkbox;
+
+	void _set_event(const Ref<InputEvent> &p_event);
+
+	void _tab_selected(int p_tab);
+	void _listen_window_input(const Ref<InputEvent> &p_event);
+
+	void _search_term_updated(const String &p_term);
+	void _update_input_list();
+	void _input_list_item_selected();
+
+	void _mod_toggled(bool p_checked, int p_index);
+	void _store_command_toggled(bool p_checked);
+	void _physical_keycode_toggled(bool p_checked);
+
+	void _set_current_device(int i_device);
+	int _get_current_device() const;
+	String _get_device_string(int i_device) const;
+
+protected:
+	void _notification(int p_what);
+
+public:
+	// Pass an existing event to configure it. Alternatively, pass no event to start with a blank configuration.
+	void popup_and_configure(const Ref<InputEvent> &p_event = Ref<InputEvent>());
+	Ref<InputEvent> get_event() const;
+	String get_event_text(const Ref<InputEvent> &p_event);
+
+	void set_allowed_input_types(int p_type_masks);
+
+	InputEventConfigurationDialog();
+};
+
+class ActionMapEditor : public Control {
+	GDCLASS(ActionMapEditor, Control);
+
+public:
+	struct ActionInfo {
+		String name = String();
+		Dictionary action = Dictionary();
+
+		Ref<Texture2D> icon = Ref<Texture2D>();
+		bool editable = true;
+	};
+
+private:
+	enum ItemButton {
+		BUTTON_ADD_EVENT,
+		BUTTON_EDIT_EVENT,
+		BUTTON_REMOVE_ACTION,
+		BUTTON_REMOVE_EVENT,
+	};
+
+	Vector<ActionInfo> actions_cache;
+	Tree *action_tree;
+
+	// Storing which action/event is currently being edited in the InputEventConfigurationDialog.
+
+	Dictionary current_action = Dictionary();
+	String current_action_name = String();
+	int current_action_event_index = -1;
+
+	// Popups
+
+	InputEventConfigurationDialog *event_config_dialog;
+	AcceptDialog *message;
+
+	// Filtering and Adding actions
+
+	bool show_uneditable;
+	CheckBox *show_uneditable_actions_checkbox;
+	LineEdit *action_list_search;
+
+	bool allow_editing_actions;
+	HBoxContainer *add_hbox;
+	LineEdit *add_edit;
+
+	void _event_config_confirmed();
+
+	void _add_action_pressed();
+	void _add_action(const String &p_name);
+	void _action_edited();
+
+	void _tree_button_pressed(Object *p_item, int p_column, int p_id);
+	void _tree_item_activated();
+	void _search_term_updated(const String &p_search_term);
+
+	Variant get_drag_data_fw(const Point2 &p_point, Control *p_from);
+	bool can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const;
+	void drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from);
+
+protected:
+	void _notification(int p_what);
+	static void _bind_methods();
+
+public:
+	LineEdit *get_search_box() const;
+	InputEventConfigurationDialog *get_configuration_dialog();
+
+	// Dictionary represents an Action with "events" (Array) and "deadzone" (float) items. Pass with no param to update list from cached action map.
+	void update_action_list(const Vector<ActionInfo> &p_action_infos = Vector<ActionInfo>());
+	void show_message(const String &p_message);
+
+	void set_show_uneditable(bool p_show);
+	void set_allow_editing_actions(bool p_allow);
+
+	void set_toggle_editable_label(const String &p_label);
+
+	void use_external_search_box(LineEdit *p_searchbox);
+
+	ActionMapEditor();
+};
+
+#endif

+ 0 - 1033
editor/input_map_editor.cpp

@@ -1,1033 +0,0 @@
-/*************************************************************************/
-/*  input_map_editor.cpp                                                 */
-/*************************************************************************/
-/*                       This file is part of:                           */
-/*                           GODOT ENGINE                                */
-/*                      https://godotengine.org                          */
-/*************************************************************************/
-/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur.                 */
-/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md).   */
-/*                                                                       */
-/* Permission is hereby granted, free of charge, to any person obtaining */
-/* a copy of this software and associated documentation files (the       */
-/* "Software"), to deal in the Software without restriction, including   */
-/* without limitation the rights to use, copy, modify, merge, publish,   */
-/* distribute, sublicense, and/or sell copies of the Software, and to    */
-/* permit persons to whom the Software is furnished to do so, subject to */
-/* the following conditions:                                             */
-/*                                                                       */
-/* The above copyright notice and this permission notice shall be        */
-/* included in all copies or substantial portions of the Software.       */
-/*                                                                       */
-/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
-/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
-/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
-/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
-/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
-/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
-/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
-/*************************************************************************/
-
-#include "input_map_editor.h"
-
-#include "core/input/input_map.h"
-#include "core/os/keyboard.h"
-#include "editor/editor_node.h"
-#include "editor/editor_scale.h"
-
-void InputMapEditor::_notification(int p_what) {
-	switch (p_what) {
-		case NOTIFICATION_ENTER_TREE: {
-			action_add_error->add_theme_color_override("font_color", input_editor->get_theme_color("error_color", "Editor"));
-			popup_add->add_icon_item(input_editor->get_theme_icon("Keyboard", "EditorIcons"), TTR("Key"), INPUT_KEY);
-			popup_add->add_icon_item(input_editor->get_theme_icon("KeyboardPhysical", "EditorIcons"), TTR("Physical Key"), INPUT_KEY_PHYSICAL);
-			popup_add->add_icon_item(input_editor->get_theme_icon("JoyButton", "EditorIcons"), TTR("Joy Button"), INPUT_JOY_BUTTON);
-			popup_add->add_icon_item(input_editor->get_theme_icon("JoyAxis", "EditorIcons"), TTR("Joy Axis"), INPUT_JOY_MOTION);
-			popup_add->add_icon_item(input_editor->get_theme_icon("Mouse", "EditorIcons"), TTR("Mouse Button"), INPUT_MOUSE_BUTTON);
-			_update_actions();
-		} break;
-		case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {
-			action_add_error->add_theme_color_override("font_color", input_editor->get_theme_color("error_color", "Editor"));
-			popup_add->set_item_icon(popup_add->get_item_index(INPUT_KEY), input_editor->get_theme_icon("Keyboard", "EditorIcons"));
-			popup_add->set_item_icon(popup_add->get_item_index(INPUT_KEY_PHYSICAL), input_editor->get_theme_icon("KeyboardPhysical", "EditorIcons"));
-			popup_add->set_item_icon(popup_add->get_item_index(INPUT_JOY_BUTTON), input_editor->get_theme_icon("JoyButton", "EditorIcons"));
-			popup_add->set_item_icon(popup_add->get_item_index(INPUT_JOY_MOTION), input_editor->get_theme_icon("JoyAxis", "EditorIcons"));
-			popup_add->set_item_icon(popup_add->get_item_index(INPUT_MOUSE_BUTTON), input_editor->get_theme_icon("Mouse", "EditorIcons"));
-			_update_actions();
-		} break;
-	}
-}
-
-static bool _validate_action_name(const String &p_name) {
-	const char32_t *cstr = p_name.get_data();
-	for (int i = 0; cstr[i]; i++) {
-		if (cstr[i] == '/' || cstr[i] == ':' || cstr[i] == '"' ||
-				cstr[i] == '=' || cstr[i] == '\\' || cstr[i] < 32) {
-			return false;
-		}
-	}
-	return true;
-}
-
-void InputMapEditor::_action_selected() {
-	TreeItem *ti = input_editor->get_selected();
-	if (!ti || !ti->is_editable(0)) {
-		return;
-	}
-
-	add_at = "input/" + ti->get_text(0);
-	edit_idx = -1;
-}
-
-void InputMapEditor::_action_edited() {
-	TreeItem *ti = input_editor->get_selected();
-	if (!ti) {
-		return;
-	}
-
-	if (input_editor->get_selected_column() == 0) {
-		String new_name = ti->get_text(0);
-		String old_name = add_at.substr(add_at.find("/") + 1, add_at.length());
-
-		if (new_name == old_name) {
-			return;
-		}
-
-		if (new_name == "" || !_validate_action_name(new_name)) {
-			ti->set_text(0, old_name);
-			add_at = "input/" + old_name;
-
-			message->set_text(TTR("Invalid action name. it cannot be empty nor contain '/', ':', '=', '\\' or '\"'"));
-			message->popup_centered(Size2(300, 100) * EDSCALE);
-			return;
-		}
-
-		String action_prop = "input/" + new_name;
-
-		if (ProjectSettings::get_singleton()->has_setting(action_prop)) {
-			ti->set_text(0, old_name);
-			add_at = "input/" + old_name;
-
-			message->set_text(vformat(TTR("An action with the name '%s' already exists."), new_name));
-			message->popup_centered(Size2(300, 100) * EDSCALE);
-			return;
-		}
-
-		int order = ProjectSettings::get_singleton()->get_order(add_at);
-		Dictionary action = ProjectSettings::get_singleton()->get(add_at);
-
-		setting = true;
-		undo_redo->create_action(TTR("Rename Input Action Event"));
-		undo_redo->add_do_method(ProjectSettings::get_singleton(), "clear", add_at);
-		undo_redo->add_do_method(ProjectSettings::get_singleton(), "set", action_prop, action);
-		undo_redo->add_do_method(ProjectSettings::get_singleton(), "set_order", action_prop, order);
-		undo_redo->add_undo_method(ProjectSettings::get_singleton(), "clear", action_prop);
-		undo_redo->add_undo_method(ProjectSettings::get_singleton(), "set", add_at, action);
-		undo_redo->add_undo_method(ProjectSettings::get_singleton(), "set_order", add_at, order);
-		undo_redo->add_do_method(this, "_update_actions");
-		undo_redo->add_undo_method(this, "_update_actions");
-		undo_redo->add_do_method(this, "emit_signal", inputmap_changed);
-		undo_redo->add_undo_method(this, "emit_signal", inputmap_changed);
-		undo_redo->commit_action();
-		setting = false;
-
-		add_at = action_prop;
-	} else if (input_editor->get_selected_column() == 1) {
-		String name = "input/" + ti->get_text(0);
-		Dictionary old_action = ProjectSettings::get_singleton()->get(name);
-		Dictionary new_action = old_action.duplicate();
-		new_action["deadzone"] = ti->get_range(1);
-
-		undo_redo->create_action(TTR("Change Action deadzone"));
-		undo_redo->add_do_method(ProjectSettings::get_singleton(), "set", name, new_action);
-		undo_redo->add_do_method(this, "emit_signal", inputmap_changed);
-		undo_redo->add_undo_method(ProjectSettings::get_singleton(), "set", name, old_action);
-		undo_redo->add_undo_method(this, "emit_signal", inputmap_changed);
-		undo_redo->commit_action();
-	}
-}
-
-void InputMapEditor::_device_input_add() {
-	Ref<InputEvent> ie;
-	String name = add_at;
-	int idx = edit_idx;
-	Dictionary old_val = ProjectSettings::get_singleton()->get(name);
-	Dictionary action = old_val.duplicate();
-	Array events = action["events"];
-
-	switch (add_type) {
-		case INPUT_MOUSE_BUTTON: {
-			Ref<InputEventMouseButton> mb;
-			mb.instance();
-			mb->set_button_index(device_index->get_selected() + 1);
-			mb->set_device(_get_current_device());
-
-			for (int i = 0; i < events.size(); i++) {
-				Ref<InputEventMouseButton> aie = events[i];
-				if (aie.is_null()) {
-					continue;
-				}
-				if (aie->get_device() == mb->get_device() && aie->get_button_index() == mb->get_button_index()) {
-					return;
-				}
-			}
-
-			ie = mb;
-
-		} break;
-		case INPUT_JOY_MOTION: {
-			Ref<InputEventJoypadMotion> jm;
-			jm.instance();
-			jm->set_axis(device_index->get_selected() >> 1);
-			jm->set_axis_value((device_index->get_selected() & 1) ? 1 : -1);
-			jm->set_device(_get_current_device());
-
-			for (int i = 0; i < events.size(); i++) {
-				Ref<InputEventJoypadMotion> aie = events[i];
-				if (aie.is_null()) {
-					continue;
-				}
-
-				if (aie->get_device() == jm->get_device() && aie->get_axis() == jm->get_axis() && aie->get_axis_value() == jm->get_axis_value()) {
-					return;
-				}
-			}
-
-			ie = jm;
-
-		} break;
-		case INPUT_JOY_BUTTON: {
-			Ref<InputEventJoypadButton> jb;
-			jb.instance();
-
-			jb->set_button_index(device_index->get_selected());
-			jb->set_device(_get_current_device());
-
-			for (int i = 0; i < events.size(); i++) {
-				Ref<InputEventJoypadButton> aie = events[i];
-				if (aie.is_null()) {
-					continue;
-				}
-				if (aie->get_device() == jb->get_device() && aie->get_button_index() == jb->get_button_index()) {
-					return;
-				}
-			}
-			ie = jb;
-
-		} break;
-		default: {
-		}
-	}
-
-	if (idx < 0 || idx >= events.size()) {
-		events.push_back(ie);
-	} else {
-		events[idx] = ie;
-	}
-	action["events"] = events;
-
-	undo_redo->create_action(TTR("Add Input Action Event"));
-	undo_redo->add_do_method(ProjectSettings::get_singleton(), "set", name, action);
-	undo_redo->add_undo_method(ProjectSettings::get_singleton(), "set", name, old_val);
-	undo_redo->add_do_method(this, "_update_actions");
-	undo_redo->add_undo_method(this, "_update_actions");
-	undo_redo->add_do_method(this, "emit_signal", inputmap_changed);
-	undo_redo->add_undo_method(this, "emit_signal", inputmap_changed);
-	undo_redo->commit_action();
-
-	_show_last_added(ie, name);
-}
-
-void InputMapEditor::_set_current_device(int i_device) {
-	device_id->select(i_device + 1);
-}
-
-int InputMapEditor::_get_current_device() {
-	return device_id->get_selected() - 1;
-}
-
-String InputMapEditor::_get_device_string(int i_device) {
-	if (i_device == InputMap::ALL_DEVICES) {
-		return TTR("All Devices");
-	}
-	return TTR("Device") + " " + itos(i_device);
-}
-
-void InputMapEditor::_press_a_key_confirm() {
-	if (last_wait_for_key.is_null()) {
-		return;
-	}
-
-	Ref<InputEventKey> ie;
-	ie.instance();
-	if (press_a_key_physical) {
-		ie->set_physical_keycode(last_wait_for_key->get_physical_keycode());
-		ie->set_keycode(0);
-	} else {
-		ie->set_physical_keycode(0);
-		ie->set_keycode(last_wait_for_key->get_keycode());
-	}
-	ie->set_shift(last_wait_for_key->get_shift());
-	ie->set_alt(last_wait_for_key->get_alt());
-	ie->set_control(last_wait_for_key->get_control());
-	ie->set_metakey(last_wait_for_key->get_metakey());
-
-	String name = add_at;
-	int idx = edit_idx;
-
-	Dictionary old_val = ProjectSettings::get_singleton()->get(name);
-	Dictionary action = old_val.duplicate();
-	Array events = action["events"];
-
-	for (int i = 0; i < events.size(); i++) {
-		Ref<InputEventKey> aie = events[i];
-		if (aie.is_null()) {
-			continue;
-		}
-		if (!press_a_key_physical) {
-			if (aie->get_keycode_with_modifiers() == ie->get_keycode_with_modifiers()) {
-				return;
-			}
-		} else {
-			if (aie->get_physical_keycode_with_modifiers() == ie->get_physical_keycode_with_modifiers()) {
-				return;
-			}
-		}
-	}
-
-	if (idx < 0 || idx >= events.size()) {
-		events.push_back(ie);
-	} else {
-		events[idx] = ie;
-	}
-	action["events"] = events;
-
-	undo_redo->create_action(TTR("Add Input Action Event"));
-	undo_redo->add_do_method(ProjectSettings::get_singleton(), "set", name, action);
-	undo_redo->add_undo_method(ProjectSettings::get_singleton(), "set", name, old_val);
-	undo_redo->add_do_method(this, "_update_actions");
-	undo_redo->add_undo_method(this, "_update_actions");
-	undo_redo->add_do_method(this, "emit_signal", inputmap_changed);
-	undo_redo->add_undo_method(this, "emit_signal", inputmap_changed);
-	undo_redo->commit_action();
-
-	_show_last_added(ie, name);
-}
-
-void InputMapEditor::_show_last_added(const Ref<InputEvent> &p_event, const String &p_name) {
-	TreeItem *r = input_editor->get_root();
-
-	String name = p_name;
-	name.erase(0, 6);
-	if (!r) {
-		return;
-	}
-	r = r->get_children();
-	if (!r) {
-		return;
-	}
-	bool found = false;
-	while (r) {
-		if (r->get_text(0) != name) {
-			r = r->get_next();
-			continue;
-		}
-		TreeItem *child = r->get_children();
-		while (child) {
-			Variant input = child->get_meta("__input");
-			if (p_event == input) {
-				r->set_collapsed(false);
-				child->select(0);
-				found = true;
-				break;
-			}
-			child = child->get_next();
-		}
-		if (found) {
-			break;
-		}
-		r = r->get_next();
-	}
-
-	if (found) {
-		input_editor->ensure_cursor_is_visible();
-	}
-}
-
-// Maps to 2*axis if value is neg, or + 1 if value is pos.
-static const char *_joy_axis_descriptions[JOY_AXIS_MAX * 2] = {
-	TTRC("Left Stick Left, Joystick 0 Left"),
-	TTRC("Left Stick Right, Joystick 0 Right"),
-	TTRC("Left Stick Up, Joystick 0 Up"),
-	TTRC("Left Stick Down, Joystick 0 Down"),
-	TTRC("Right Stick Left, Joystick 1 Left"),
-	TTRC("Right Stick Right, Joystick 1 Right"),
-	TTRC("Right Stick Up, Joystick 1 Up"),
-	TTRC("Right Stick Down, Joystick 1 Down"),
-	TTRC("Joystick 2 Left"),
-	TTRC("Left Trigger, Sony L2, Xbox LT, Joystick 2 Right"),
-	TTRC("Joystick 2 Up"),
-	TTRC("Right Trigger, Sony R2, Xbox RT, Joystick 2 Down"),
-	TTRC("Joystick 3 Left"),
-	TTRC("Joystick 3 Right"),
-	TTRC("Joystick 3 Up"),
-	TTRC("Joystick 3 Down"),
-	TTRC("Joystick 4 Left"),
-	TTRC("Joystick 4 Right"),
-	TTRC("Joystick 4 Up"),
-	TTRC("Joystick 4 Down"),
-};
-
-// Separate from `InputEvent::as_text()` since the descriptions need to be different for the input map editor. See #43660.
-String InputMapEditor::_get_joypad_motion_event_text(const Ref<InputEventJoypadMotion> &p_event) {
-	ERR_FAIL_COND_V_MSG(p_event.is_null(), String(), "Provided event is not a valid instance of InputEventJoypadMotion");
-
-	String desc = TTR("Unknown Joypad Axis");
-	if (p_event->get_axis() < JOY_AXIS_MAX) {
-		desc = RTR(_joy_axis_descriptions[2 * p_event->get_axis() + (p_event->get_axis_value() < 0 ? 0 : 1)]);
-	}
-
-	return vformat("Joypad Axis %s %s (%s)", itos(p_event->get_axis()), p_event->get_axis_value() < 0 ? "-" : "+", desc);
-}
-
-void InputMapEditor::_wait_for_key(const Ref<InputEvent> &p_event) {
-	Ref<InputEventKey> k = p_event;
-
-	if (k.is_valid() && k->is_pressed() && k->get_keycode() != 0) {
-		last_wait_for_key = p_event;
-		const String str = (press_a_key_physical) ? keycode_get_string(k->get_physical_keycode_with_modifiers()) + TTR(" (Physical)") : keycode_get_string(k->get_keycode_with_modifiers());
-
-		press_a_key_label->set_text(str);
-		press_a_key->get_ok_button()->set_disabled(false);
-		press_a_key->set_input_as_handled();
-	}
-}
-
-void InputMapEditor::_edit_item(Ref<InputEvent> p_exiting_event) {
-	InputType ie_type;
-
-	if ((Ref<InputEventKey>(p_exiting_event)).is_valid()) {
-		if ((Ref<InputEventKey>(p_exiting_event))->get_keycode() != 0) {
-			ie_type = INPUT_KEY;
-		} else {
-			ie_type = INPUT_KEY_PHYSICAL;
-		}
-	} else if ((Ref<InputEventJoypadButton>(p_exiting_event)).is_valid()) {
-		ie_type = INPUT_JOY_BUTTON;
-	} else if ((Ref<InputEventMouseButton>(p_exiting_event)).is_valid()) {
-		ie_type = INPUT_MOUSE_BUTTON;
-	} else if ((Ref<InputEventJoypadMotion>(p_exiting_event)).is_valid()) {
-		ie_type = INPUT_JOY_MOTION;
-	} else {
-		return;
-	}
-
-	_add_item(ie_type, p_exiting_event);
-}
-
-void InputMapEditor::_add_item(int p_item, Ref<InputEvent> p_exiting_event) {
-	add_type = InputType(p_item);
-
-	switch (add_type) {
-		case INPUT_KEY: {
-			press_a_key_physical = false;
-			press_a_key_label->set_text(TTR("Press a Key..."));
-			press_a_key->get_ok_button()->set_disabled(true);
-			last_wait_for_key = Ref<InputEvent>();
-			press_a_key->popup_centered(Size2(250, 80) * EDSCALE);
-			//press_a_key->grab_focus();
-
-		} break;
-		case INPUT_KEY_PHYSICAL: {
-			press_a_key_physical = true;
-			press_a_key_label->set_text(TTR("Press a Key..."));
-
-			last_wait_for_key = Ref<InputEvent>();
-			press_a_key->popup_centered(Size2(250, 80) * EDSCALE);
-			press_a_key->grab_focus();
-
-		} break;
-		case INPUT_MOUSE_BUTTON: {
-			device_index_label->set_text(TTR("Mouse Button Index:"));
-			device_index->clear();
-			device_index->add_item(TTR("Left Button"));
-			device_index->add_item(TTR("Right Button"));
-			device_index->add_item(TTR("Middle Button"));
-			device_index->add_item(TTR("Wheel Up Button"));
-			device_index->add_item(TTR("Wheel Down Button"));
-			device_index->add_item(TTR("Wheel Left Button"));
-			device_index->add_item(TTR("Wheel Right Button"));
-			device_index->add_item(TTR("X Button 1"));
-			device_index->add_item(TTR("X Button 2"));
-			device_input->popup_centered(Size2(350, 95) * EDSCALE);
-
-			Ref<InputEventMouseButton> mb = p_exiting_event;
-			if (mb.is_valid()) {
-				device_index->select(mb->get_button_index() - 1);
-				_set_current_device(mb->get_device());
-				device_input->get_ok_button()->set_text(TTR("Change"));
-			} else {
-				_set_current_device(0);
-				device_input->get_ok_button()->set_text(TTR("Add"));
-			}
-
-		} break;
-		case INPUT_JOY_MOTION: {
-			device_index_label->set_text(TTR("Joypad Axis Index:"));
-			device_index->clear();
-			for (int i = 0; i < JOY_AXIS_MAX * 2; i++) {
-				Ref<InputEventJoypadMotion> jm;
-				jm.instance();
-				jm->set_axis(i / 2);
-				jm->set_axis_value((i & 1) ? 1 : -1);
-				device_index->add_item(_get_joypad_motion_event_text(jm));
-			}
-			device_input->popup_centered(Size2(350, 95) * EDSCALE);
-
-			Ref<InputEventJoypadMotion> jm = p_exiting_event;
-			if (jm.is_valid()) {
-				device_index->select(jm->get_axis() * 2 + (jm->get_axis_value() > 0 ? 1 : 0));
-				_set_current_device(jm->get_device());
-				device_input->get_ok_button()->set_text(TTR("Change"));
-			} else {
-				_set_current_device(0);
-				device_input->get_ok_button()->set_text(TTR("Add"));
-			}
-
-		} break;
-		case INPUT_JOY_BUTTON: {
-			device_index_label->set_text(TTR("Joypad Button Index:"));
-			device_index->clear();
-			for (int i = 0; i < JOY_BUTTON_MAX; i++) {
-				Ref<InputEventJoypadButton> jb;
-				jb.instance();
-				jb->set_button_index(i);
-				device_index->add_item(jb->as_text());
-			}
-			device_input->popup_centered(Size2(350, 95) * EDSCALE);
-
-			Ref<InputEventJoypadButton> jb = p_exiting_event;
-			if (jb.is_valid()) {
-				device_index->select(jb->get_button_index());
-				_set_current_device(jb->get_device());
-				device_input->get_ok_button()->set_text(TTR("Change"));
-			} else {
-				_set_current_device(0);
-				device_input->get_ok_button()->set_text(TTR("Add"));
-			}
-
-		} break;
-		default: {
-		}
-	}
-}
-
-void InputMapEditor::_action_activated() {
-	TreeItem *ti = input_editor->get_selected();
-
-	if (!ti || ti->get_parent() == input_editor->get_root()) {
-		return;
-	}
-
-	String name = "input/" + ti->get_parent()->get_text(0);
-	Dictionary action = ProjectSettings::get_singleton()->get(name);
-	Array events = action["events"];
-	int idx = ti->get_metadata(0);
-
-	ERR_FAIL_INDEX(idx, events.size());
-	Ref<InputEvent> event = events[idx];
-	if (event.is_null()) {
-		return;
-	}
-
-	add_at = name;
-	edit_idx = idx;
-	_edit_item(event);
-}
-
-void InputMapEditor::_action_button_pressed(Object *p_obj, int p_column, int p_id) {
-	TreeItem *ti = Object::cast_to<TreeItem>(p_obj);
-
-	ERR_FAIL_COND(!ti);
-
-	if (p_id == 1) {
-		// Add action event
-		Point2 ofs = input_editor->get_global_position();
-		Rect2 ir = input_editor->get_item_rect(ti);
-		ir.position.y -= input_editor->get_scroll().y;
-		ofs += ir.position + ir.size;
-		ofs.x -= 100;
-		popup_add->set_position(ofs);
-		popup_add->popup();
-		add_at = "input/" + ti->get_text(0);
-		edit_idx = -1;
-
-	} else if (p_id == 2) {
-		// Remove
-
-		if (ti->get_parent() == input_editor->get_root()) {
-			// Remove action
-			String name = "input/" + ti->get_text(0);
-			Dictionary old_val = ProjectSettings::get_singleton()->get(name);
-			int order = ProjectSettings::get_singleton()->get_order(name);
-
-			undo_redo->create_action(TTR("Erase Input Action"));
-			undo_redo->add_do_method(ProjectSettings::get_singleton(), "clear", name);
-			undo_redo->add_undo_method(ProjectSettings::get_singleton(), "set", name, old_val);
-			undo_redo->add_undo_method(ProjectSettings::get_singleton(), "set_order", name, order);
-			undo_redo->add_do_method(this, "_update_actions");
-			undo_redo->add_undo_method(this, "_update_actions");
-			undo_redo->add_do_method(this, "emit_signal", inputmap_changed);
-			undo_redo->add_undo_method(this, "emit_signal", inputmap_changed);
-			undo_redo->commit_action();
-
-		} else {
-			// Remove action event
-			String name = "input/" + ti->get_parent()->get_text(0);
-			Dictionary old_val = ProjectSettings::get_singleton()->get(name);
-			Dictionary action = old_val.duplicate();
-			int idx = ti->get_metadata(0);
-
-			Array events = action["events"];
-			ERR_FAIL_INDEX(idx, events.size());
-			events.remove(idx);
-			action["events"] = events;
-
-			undo_redo->create_action(TTR("Erase Input Action Event"));
-			undo_redo->add_do_method(ProjectSettings::get_singleton(), "set", name, action);
-			undo_redo->add_undo_method(ProjectSettings::get_singleton(), "set", name, old_val);
-			undo_redo->add_do_method(this, "_update_actions");
-			undo_redo->add_undo_method(this, "_update_actions");
-			undo_redo->add_do_method(this, "emit_signal", inputmap_changed);
-			undo_redo->add_undo_method(this, "emit_signal", inputmap_changed);
-			undo_redo->commit_action();
-		}
-	} else if (p_id == 3) {
-		// Edit
-
-		if (ti->get_parent() == input_editor->get_root()) {
-			// Edit action name
-			ti->set_as_cursor(0);
-			input_editor->edit_selected();
-
-		} else {
-			// Edit action event
-			String name = "input/" + ti->get_parent()->get_text(0);
-			int idx = ti->get_metadata(0);
-			Dictionary action = ProjectSettings::get_singleton()->get(name);
-
-			Array events = action["events"];
-			ERR_FAIL_INDEX(idx, events.size());
-
-			Ref<InputEvent> event = events[idx];
-
-			if (event.is_null()) {
-				return;
-			}
-
-			ti->set_as_cursor(0);
-			add_at = name;
-			edit_idx = idx;
-			_edit_item(event);
-		}
-	}
-}
-
-void InputMapEditor::_update_actions() {
-	if (setting) {
-		return;
-	}
-
-	Map<String, bool> collapsed;
-
-	if (input_editor->get_root() && input_editor->get_root()->get_children()) {
-		for (TreeItem *item = input_editor->get_root()->get_children(); item; item = item->get_next()) {
-			collapsed[item->get_text(0)] = item->is_collapsed();
-		}
-	}
-
-	input_editor->clear();
-	TreeItem *root = input_editor->create_item();
-	input_editor->set_hide_root(true);
-
-	List<PropertyInfo> props;
-	ProjectSettings::get_singleton()->get_property_list(&props);
-	for (List<PropertyInfo>::Element *E = props.front(); E; E = E->next()) {
-		const String property_name = E->get().name;
-
-		if (!property_name.begins_with("input/")) {
-			continue;
-		}
-
-		const String name = property_name.get_slice("/", 1);
-
-		TreeItem *item = input_editor->create_item(root);
-		item->set_text(0, name);
-		item->set_custom_bg_color(0, input_editor->get_theme_color("prop_subsection", "Editor"));
-		if (collapsed.has(name)) {
-			item->set_collapsed(collapsed[name]);
-		}
-
-		item->set_editable(1, true);
-		item->set_cell_mode(1, TreeItem::CELL_MODE_RANGE);
-		item->set_range_config(1, 0.0, 1.0, 0.01);
-
-		item->set_custom_bg_color(1, input_editor->get_theme_color("prop_subsection", "Editor"));
-
-		const bool is_builtin_input = ProjectSettings::get_singleton()->get_input_presets().find(property_name) != nullptr;
-		const String tooltip_remove = is_builtin_input ? TTR("Built-in actions can't be removed as they're used for UI navigation.") : TTR("Remove");
-		item->add_button(2, input_editor->get_theme_icon("Add", "EditorIcons"), 1, false, TTR("Add Event"));
-		item->add_button(2, input_editor->get_theme_icon("Remove", "EditorIcons"), 2, false, tooltip_remove);
-
-		if (is_builtin_input) {
-			item->set_button_disabled(2, 1, true);
-		} else {
-			item->set_editable(0, true);
-		}
-
-		Dictionary action = ProjectSettings::get_singleton()->get(property_name);
-		Array events = action["events"];
-		item->set_range(1, action["deadzone"]);
-
-		for (int i = 0; i < events.size(); i++) {
-			Ref<InputEvent> event = events[i];
-			if (event.is_null()) {
-				continue;
-			}
-
-			TreeItem *action2 = input_editor->create_item(item);
-
-			Ref<InputEventKey> k = event;
-			if (k.is_valid()) {
-				if (k->get_keycode() != 0) {
-					action2->set_text(0, keycode_get_string(k->get_keycode_with_modifiers()));
-					action2->set_icon(0, input_editor->get_theme_icon("Keyboard", "EditorIcons"));
-				} else {
-					action2->set_text(0, keycode_get_string(k->get_physical_keycode_with_modifiers()) + TTR(" (Physical)"));
-					action2->set_icon(0, input_editor->get_theme_icon("KeyboardPhysical", "EditorIcons"));
-				}
-			}
-
-			Ref<InputEventJoypadButton> jb = event;
-			if (jb.is_valid()) {
-				action2->set_text(0, jb->as_text());
-				action2->set_icon(0, input_editor->get_theme_icon("JoyButton", "EditorIcons"));
-			}
-
-			Ref<InputEventMouseButton> mb = event;
-			if (mb.is_valid()) {
-				String str = _get_device_string(mb->get_device()) + ", ";
-				switch (mb->get_button_index()) {
-					case BUTTON_LEFT:
-						str += TTR("Left Button");
-						break;
-					case BUTTON_RIGHT:
-						str += TTR("Right Button");
-						break;
-					case BUTTON_MIDDLE:
-						str += TTR("Middle Button");
-						break;
-					case BUTTON_WHEEL_UP:
-						str += TTR("Wheel Up");
-						break;
-					case BUTTON_WHEEL_DOWN:
-						str += TTR("Wheel Down");
-						break;
-					default:
-						str += vformat(TTR("%d Button"), mb->get_button_index());
-				}
-
-				action2->set_text(0, str);
-				action2->set_icon(0, input_editor->get_theme_icon("Mouse", "EditorIcons"));
-			}
-
-			Ref<InputEventJoypadMotion> jm = event;
-			if (jm.is_valid()) {
-				device_index->add_item(_get_joypad_motion_event_text(jm));
-				action2->set_text(0, jm->as_text());
-				action2->set_icon(0, input_editor->get_theme_icon("JoyAxis", "EditorIcons"));
-			}
-			action2->set_metadata(0, i);
-			action2->set_meta("__input", event);
-
-			action2->add_button(2, input_editor->get_theme_icon("Edit", "EditorIcons"), 3, false, TTR("Edit"));
-			action2->add_button(2, input_editor->get_theme_icon("Remove", "EditorIcons"), 2, false, TTR("Remove"));
-			// Fade out the individual event buttons slightly to make the
-			// Add/Remove buttons stand out more.
-			action2->set_button_color(2, 0, Color(1, 1, 1, 0.75));
-			action2->set_button_color(2, 1, Color(1, 1, 1, 0.75));
-		}
-	}
-
-	_action_check(action_name->get_text());
-}
-
-void InputMapEditor::_action_check(String p_action) {
-	if (p_action == "") {
-		action_add->set_disabled(true);
-	} else {
-		if (!_validate_action_name(p_action)) {
-			action_add_error->set_text(TTR("Invalid action name. It cannot be empty nor contain '/', ':', '=', '\\' or '\"'."));
-			action_add_error->show();
-			action_add->set_disabled(true);
-			return;
-		}
-		if (ProjectSettings::get_singleton()->has_setting("input/" + p_action)) {
-			action_add_error->set_text(vformat(TTR("An action with the name '%s' already exists."), p_action));
-			action_add_error->show();
-			action_add->set_disabled(true);
-			return;
-		}
-
-		action_add->set_disabled(false);
-	}
-
-	action_add_error->hide();
-}
-
-void InputMapEditor::_action_adds(String) {
-	if (!action_add->is_disabled()) {
-		_action_add();
-	}
-}
-
-void InputMapEditor::_action_add() {
-	Dictionary action;
-	action["events"] = Array();
-	action["deadzone"] = 0.5f;
-	String name = "input/" + action_name->get_text();
-	undo_redo->create_action(TTR("Add Input Action"));
-	undo_redo->add_do_method(ProjectSettings::get_singleton(), "set", name, action);
-	undo_redo->add_undo_method(ProjectSettings::get_singleton(), "clear", name);
-	undo_redo->add_do_method(this, "_update_actions");
-	undo_redo->add_undo_method(this, "_update_actions");
-	undo_redo->add_do_method(this, "emit_signal", inputmap_changed);
-	undo_redo->add_undo_method(this, "emit_signal", inputmap_changed);
-	undo_redo->commit_action();
-
-	TreeItem *r = input_editor->get_root();
-
-	if (!r) {
-		return;
-	}
-	r = r->get_children();
-	if (!r) {
-		return;
-	}
-	while (r->get_next()) {
-		r = r->get_next();
-	}
-
-	r->select(0);
-	input_editor->ensure_cursor_is_visible();
-	action_add_error->hide();
-	action_name->clear();
-}
-
-Variant InputMapEditor::get_drag_data_fw(const Point2 &p_point, Control *p_from) {
-	TreeItem *selected = input_editor->get_selected();
-	if (!selected || selected->get_parent() != input_editor->get_root()) {
-		return Variant();
-	}
-
-	String name = selected->get_text(0);
-	VBoxContainer *vb = memnew(VBoxContainer);
-	HBoxContainer *hb = memnew(HBoxContainer);
-	Label *label = memnew(Label(name));
-	hb->set_modulate(Color(1, 1, 1, 1.0f));
-	hb->add_child(label);
-	vb->add_child(hb);
-	input_editor->set_drag_preview(vb);
-
-	Dictionary drag_data;
-	drag_data["type"] = "nodes";
-
-	input_editor->set_drop_mode_flags(Tree::DROP_MODE_INBETWEEN);
-
-	return drag_data;
-}
-
-bool InputMapEditor::can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const {
-	Dictionary d = p_data;
-	if (!d.has("type") || d["type"] != "nodes") {
-		return false;
-	}
-
-	TreeItem *selected = input_editor->get_selected();
-	TreeItem *item = input_editor->get_item_at_position(p_point);
-	if (!selected || !item || item == selected || item->get_parent() == selected) {
-		return false;
-	}
-
-	return true;
-}
-
-void InputMapEditor::drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) {
-	if (!can_drop_data_fw(p_point, p_data, p_from)) {
-		return;
-	}
-
-	TreeItem *selected = input_editor->get_selected();
-	TreeItem *item = input_editor->get_item_at_position(p_point);
-	if (!item) {
-		return;
-	}
-	TreeItem *target = item->get_parent() == input_editor->get_root() ? item : item->get_parent();
-
-	String selected_name = "input/" + selected->get_text(0);
-	int old_order = ProjectSettings::get_singleton()->get_order(selected_name);
-	String target_name = "input/" + target->get_text(0);
-	int target_order = ProjectSettings::get_singleton()->get_order(target_name);
-
-	int order = old_order;
-	bool is_below = target_order > old_order;
-	TreeItem *iterator = is_below ? selected->get_next() : selected->get_prev();
-
-	undo_redo->create_action(TTR("Moved Input Action Event"));
-	while (iterator != target) {
-		String iterator_name = "input/" + iterator->get_text(0);
-		int iterator_order = ProjectSettings::get_singleton()->get_order(iterator_name);
-		undo_redo->add_do_method(ProjectSettings::get_singleton(), "set_order", iterator_name, order);
-		undo_redo->add_undo_method(ProjectSettings::get_singleton(), "set_order", iterator_name, iterator_order);
-		order = iterator_order;
-		iterator = is_below ? iterator->get_next() : iterator->get_prev();
-	}
-
-	undo_redo->add_do_method(ProjectSettings::get_singleton(), "set_order", target_name, order);
-	undo_redo->add_do_method(ProjectSettings::get_singleton(), "set_order", selected_name, target_order);
-	undo_redo->add_undo_method(ProjectSettings::get_singleton(), "set_order", target_name, target_order);
-	undo_redo->add_undo_method(ProjectSettings::get_singleton(), "set_order", selected_name, old_order);
-
-	undo_redo->add_do_method(this, "_update_actions");
-	undo_redo->add_undo_method(this, "_update_actions");
-	undo_redo->add_do_method(this, "emit_signal", inputmap_changed);
-	undo_redo->add_undo_method(this, "emit_signal", inputmap_changed);
-	undo_redo->commit_action();
-}
-
-void InputMapEditor::_bind_methods() {
-	ClassDB::bind_method(D_METHOD("_update_actions"), &InputMapEditor::_update_actions);
-
-	ClassDB::bind_method(D_METHOD("get_drag_data_fw"), &InputMapEditor::get_drag_data_fw);
-	ClassDB::bind_method(D_METHOD("can_drop_data_fw"), &InputMapEditor::can_drop_data_fw);
-	ClassDB::bind_method(D_METHOD("drop_data_fw"), &InputMapEditor::drop_data_fw);
-
-	ADD_SIGNAL(MethodInfo("inputmap_changed"));
-}
-
-InputMapEditor::InputMapEditor() {
-	undo_redo = EditorNode::get_undo_redo();
-	press_a_key_physical = false;
-	inputmap_changed = "inputmap_changed";
-
-	VBoxContainer *vbc = memnew(VBoxContainer);
-	vbc->set_anchor_and_offset(SIDE_TOP, Control::ANCHOR_BEGIN, 0);
-	vbc->set_anchor_and_offset(SIDE_BOTTOM, Control::ANCHOR_END, 0);
-	vbc->set_anchor_and_offset(SIDE_LEFT, Control::ANCHOR_BEGIN, 0);
-	vbc->set_anchor_and_offset(SIDE_RIGHT, Control::ANCHOR_END, 0);
-	add_child(vbc);
-
-	HBoxContainer *hbc = memnew(HBoxContainer);
-	vbc->add_child(hbc);
-
-	Label *l = memnew(Label);
-	l->set_text(TTR("Action:"));
-	hbc->add_child(l);
-
-	action_name = memnew(LineEdit);
-	action_name->set_h_size_flags(Control::SIZE_EXPAND_FILL);
-	action_name->connect("text_entered", callable_mp(this, &InputMapEditor::_action_adds));
-	action_name->connect("text_changed", callable_mp(this, &InputMapEditor::_action_check));
-	hbc->add_child(action_name);
-
-	action_add_error = memnew(Label);
-	action_add_error->hide();
-	hbc->add_child(action_add_error);
-
-	Button *add = memnew(Button);
-	add->set_text(TTR("Add"));
-	add->set_disabled(true);
-	add->connect("pressed", callable_mp(this, &InputMapEditor::_action_add));
-	hbc->add_child(add);
-	action_add = add;
-
-	input_editor = memnew(Tree);
-	input_editor->set_v_size_flags(Control::SIZE_EXPAND_FILL);
-	input_editor->set_columns(3);
-	input_editor->set_column_titles_visible(true);
-	input_editor->set_column_title(0, TTR("Action"));
-	input_editor->set_column_title(1, TTR("Deadzone"));
-	input_editor->set_column_expand(1, false);
-	input_editor->set_column_min_width(1, 80 * EDSCALE);
-	input_editor->set_column_expand(2, false);
-	input_editor->set_column_min_width(2, 50 * EDSCALE);
-	input_editor->connect("item_edited", callable_mp(this, &InputMapEditor::_action_edited));
-	input_editor->connect("item_activated", callable_mp(this, &InputMapEditor::_action_activated));
-	input_editor->connect("cell_selected", callable_mp(this, &InputMapEditor::_action_selected));
-	input_editor->connect("button_pressed", callable_mp(this, &InputMapEditor::_action_button_pressed));
-#ifndef _MSC_VER
-#warning need to make drag data forwarding to non controls happen
-#endif
-	//input_editor->set_drag_forwarding(this);
-	vbc->add_child(input_editor);
-
-	// Popups
-
-	popup_add = memnew(PopupMenu);
-	popup_add->connect("id_pressed", callable_mp(this, &InputMapEditor::_add_item), make_binds(Ref<InputEvent>()));
-	add_child(popup_add);
-
-	press_a_key = memnew(ConfirmationDialog);
-	press_a_key->get_ok_button()->set_disabled(true);
-	//press_a_key->set_focus_mode(Control::FOCUS_ALL);
-	press_a_key->connect("window_input", callable_mp(this, &InputMapEditor::_wait_for_key));
-	press_a_key->connect("confirmed", callable_mp(this, &InputMapEditor::_press_a_key_confirm));
-	add_child(press_a_key);
-
-	l = memnew(Label);
-	l->set_text(TTR("Press a Key..."));
-	l->set_anchors_and_offsets_preset(Control::PRESET_WIDE);
-	l->set_align(Label::ALIGN_CENTER);
-	l->set_offset(SIDE_TOP, 20);
-	l->set_anchor_and_offset(SIDE_BOTTOM, Control::ANCHOR_BEGIN, 30);
-	press_a_key->add_child(l);
-	press_a_key_label = l;
-
-	device_input = memnew(ConfirmationDialog);
-	device_input->get_ok_button()->set_text(TTR("Add"));
-	device_input->connect("confirmed", callable_mp(this, &InputMapEditor::_device_input_add));
-	add_child(device_input);
-
-	hbc = memnew(HBoxContainer);
-	device_input->add_child(hbc);
-
-	VBoxContainer *vbc_left = memnew(VBoxContainer);
-	hbc->add_child(vbc_left);
-
-	l = memnew(Label);
-	l->set_text(TTR("Device:"));
-	vbc_left->add_child(l);
-
-	device_id = memnew(OptionButton);
-	for (int i = -1; i < 8; i++) {
-		device_id->add_item(_get_device_string(i));
-	}
-	_set_current_device(0);
-	vbc_left->add_child(device_id);
-
-	VBoxContainer *vbc_right = memnew(VBoxContainer);
-	vbc_right->set_h_size_flags(Control::SIZE_EXPAND_FILL);
-	hbc->add_child(vbc_right);
-
-	l = memnew(Label);
-	l->set_text(TTR("Index:"));
-	vbc_right->add_child(l);
-
-	device_index_label = l;
-	device_index = memnew(OptionButton);
-	device_index->set_clip_text(true);
-	vbc_right->add_child(device_index);
-
-	message = memnew(AcceptDialog);
-	add_child(message);
-}

+ 0 - 109
editor/input_map_editor.h

@@ -1,109 +0,0 @@
-/*************************************************************************/
-/*  input_map_editor.h                                                   */
-/*************************************************************************/
-/*                       This file is part of:                           */
-/*                           GODOT ENGINE                                */
-/*                      https://godotengine.org                          */
-/*************************************************************************/
-/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur.                 */
-/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md).   */
-/*                                                                       */
-/* Permission is hereby granted, free of charge, to any person obtaining */
-/* a copy of this software and associated documentation files (the       */
-/* "Software"), to deal in the Software without restriction, including   */
-/* without limitation the rights to use, copy, modify, merge, publish,   */
-/* distribute, sublicense, and/or sell copies of the Software, and to    */
-/* permit persons to whom the Software is furnished to do so, subject to */
-/* the following conditions:                                             */
-/*                                                                       */
-/* The above copyright notice and this permission notice shall be        */
-/* included in all copies or substantial portions of the Software.       */
-/*                                                                       */
-/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
-/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
-/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
-/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
-/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
-/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
-/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
-/*************************************************************************/
-
-#ifndef INPUT_MAP_EDITOR_H
-#define INPUT_MAP_EDITOR_H
-
-#include "core/object/undo_redo.h"
-#include "editor/editor_data.h"
-
-class InputMapEditor : public Control {
-	GDCLASS(InputMapEditor, Control);
-
-	enum InputType {
-		INPUT_KEY,
-		INPUT_KEY_PHYSICAL,
-		INPUT_JOY_BUTTON,
-		INPUT_JOY_MOTION,
-		INPUT_MOUSE_BUTTON
-	};
-
-	Tree *input_editor;
-	LineEdit *action_name;
-	Button *action_add;
-	Label *action_add_error;
-
-	InputType add_type;
-	String add_at;
-	int edit_idx;
-
-	PopupMenu *popup_add;
-	ConfirmationDialog *press_a_key;
-	bool press_a_key_physical;
-	Label *press_a_key_label;
-	ConfirmationDialog *device_input;
-	OptionButton *device_id;
-	OptionButton *device_index;
-	Label *device_index_label;
-	MenuButton *popup_copy_to_feature;
-
-	Ref<InputEventKey> last_wait_for_key;
-
-	AcceptDialog *message;
-	UndoRedo *undo_redo;
-	String inputmap_changed;
-	bool setting = false;
-
-	void _update_actions();
-	void _add_item(int p_item, Ref<InputEvent> p_exiting_event = Ref<InputEvent>());
-	void _edit_item(Ref<InputEvent> p_exiting_event);
-
-	void _action_check(String p_action);
-	void _action_adds(String);
-	void _action_add();
-	void _device_input_add();
-
-	void _action_selected();
-	void _action_edited();
-	void _action_activated();
-	void _action_button_pressed(Object *p_obj, int p_column, int p_id);
-	void _wait_for_key(const Ref<InputEvent> &p_event);
-	void _press_a_key_confirm();
-	void _show_last_added(const Ref<InputEvent> &p_event, const String &p_name);
-
-	String _get_joypad_motion_event_text(const Ref<InputEventJoypadMotion> &p_event);
-
-	Variant get_drag_data_fw(const Point2 &p_point, Control *p_from);
-	bool can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const;
-	void drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from);
-
-protected:
-	int _get_current_device();
-	void _set_current_device(int i_device);
-	String _get_device_string(int i_device);
-
-	void _notification(int p_what);
-	static void _bind_methods();
-
-public:
-	InputMapEditor();
-};
-
-#endif // INPUT_MAP_EDITOR_H

+ 214 - 4
editor/project_settings_editor.cpp

@@ -269,6 +269,206 @@ void ProjectSettingsEditor::_editor_restart_close() {
 	restart_container->hide();
 }
 
+void ProjectSettingsEditor::_action_added(const String &p_name) {
+	String name = "input/" + p_name;
+
+	if (ProjectSettings::get_singleton()->has_setting(name)) {
+		action_map->show_message(vformat(TTR("An action with the name '%s' already exists."), name));
+		return;
+	}
+
+	Dictionary action;
+	action["events"] = Array();
+	action["deadzone"] = 0.5f;
+
+	undo_redo->create_action(TTR("Add Input Action"));
+	undo_redo->add_do_method(ProjectSettings::get_singleton(), "set", name, action);
+	undo_redo->add_undo_method(ProjectSettings::get_singleton(), "clear", name);
+
+	undo_redo->add_do_method(this, "_update_action_map_editor");
+	undo_redo->add_undo_method(this, "_update_action_map_editor");
+	undo_redo->add_do_method(this, "queue_save");
+	undo_redo->add_undo_method(this, "queue_save");
+	undo_redo->commit_action();
+}
+
+void ProjectSettingsEditor::_action_edited(const String &p_name, const Dictionary &p_action) {
+	const String property_name = "input/" + p_name;
+	Dictionary old_val = ProjectSettings::get_singleton()->get(property_name);
+
+	if (old_val["deadzone"] != p_action["deadzone"]) {
+		// Deadzone Changed
+		undo_redo->create_action(TTR("Change Action deadzone"));
+		undo_redo->add_do_method(ProjectSettings::get_singleton(), "set", property_name, p_action);
+		undo_redo->add_undo_method(ProjectSettings::get_singleton(), "set", property_name, old_val);
+
+	} else {
+		// Events changed
+		int event_count = ((Array)p_action["events"]).size();
+		int old_event_count = ((Array)old_val["events"]).size();
+
+		if (event_count == old_event_count) {
+			undo_redo->create_action(TTR("Edit Input Action Event"));
+		} else if (event_count > old_event_count) {
+			undo_redo->create_action(TTR("Add Input Action Event"));
+		} else if (event_count < old_event_count) {
+			undo_redo->create_action(TTR("Remove Input Action Event"));
+		}
+
+		undo_redo->add_do_method(ProjectSettings::get_singleton(), "set", property_name, p_action);
+		undo_redo->add_undo_method(ProjectSettings::get_singleton(), "set", property_name, old_val);
+	}
+
+	undo_redo->add_do_method(this, "_update_action_map_editor");
+	undo_redo->add_undo_method(this, "_update_action_map_editor");
+	undo_redo->add_do_method(this, "queue_save");
+	undo_redo->add_undo_method(this, "queue_save");
+	undo_redo->commit_action();
+}
+
+void ProjectSettingsEditor::_action_removed(const String &p_name) {
+	const String property_name = "input/" + p_name;
+
+	Dictionary old_val = ProjectSettings::get_singleton()->get(property_name);
+	int order = ProjectSettings::get_singleton()->get_order(property_name);
+
+	undo_redo->create_action(TTR("Erase Input Action"));
+	undo_redo->add_do_method(ProjectSettings::get_singleton(), "clear", property_name);
+	undo_redo->add_undo_method(ProjectSettings::get_singleton(), "set", property_name, old_val);
+	undo_redo->add_undo_method(ProjectSettings::get_singleton(), "set_order", property_name, order);
+
+	undo_redo->add_do_method(this, "_update_action_map_editor");
+	undo_redo->add_undo_method(this, "_update_action_map_editor");
+	undo_redo->add_do_method(this, "queue_save");
+	undo_redo->add_undo_method(this, "queue_save");
+	undo_redo->commit_action();
+}
+
+void ProjectSettingsEditor::_action_renamed(const String &p_old_name, const String &p_new_name) {
+	const String old_property_name = "input/" + p_old_name;
+	const String new_property_name = "input/" + p_new_name;
+
+	if (ProjectSettings::get_singleton()->has_setting(new_property_name)) {
+		action_map->show_message(vformat(TTR("An action with the name '%s' already exists."), new_property_name));
+		return;
+	}
+
+	int order = ProjectSettings::get_singleton()->get_order(old_property_name);
+	Dictionary action = ProjectSettings::get_singleton()->get(old_property_name);
+
+	undo_redo->create_action(TTR("Rename Input Action Event"));
+	// Do: clear old, set new
+	undo_redo->add_do_method(ProjectSettings::get_singleton(), "clear", old_property_name);
+	undo_redo->add_do_method(ProjectSettings::get_singleton(), "set", new_property_name, action);
+	undo_redo->add_do_method(ProjectSettings::get_singleton(), "set_order", new_property_name, order);
+	// Undo: clear new, set old
+	undo_redo->add_undo_method(ProjectSettings::get_singleton(), "clear", new_property_name);
+	undo_redo->add_undo_method(ProjectSettings::get_singleton(), "set", old_property_name, action);
+	undo_redo->add_undo_method(ProjectSettings::get_singleton(), "set_order", old_property_name, order);
+
+	undo_redo->add_do_method(this, "_update_action_map_editor");
+	undo_redo->add_undo_method(this, "_update_action_map_editor");
+	undo_redo->add_do_method(this, "queue_save");
+	undo_redo->add_undo_method(this, "queue_save");
+	undo_redo->commit_action();
+}
+
+void ProjectSettingsEditor::_action_reordered(const String &p_action_name, const String &p_relative_to, bool p_before) {
+	const String action_name = "input/" + p_action_name;
+	const String target_name = "input/" + p_relative_to;
+
+	// It is much easier to rebuild the custom "input" properties rather than messing around with the "order" values of them.
+	Variant action_value = ps->get(action_name);
+	Variant target_value = ps->get(target_name);
+
+	List<PropertyInfo> props;
+	OrderedHashMap<String, Variant> action_values;
+	ProjectSettings::get_singleton()->get_property_list(&props);
+
+	undo_redo->create_action(TTR("Update Input Action Order"));
+
+	for (List<PropertyInfo>::Element *E = props.front(); E; E = E->next()) {
+		PropertyInfo prop = E->get();
+		// Skip builtins and non-inputs
+		if (ProjectSettings::get_singleton()->is_builtin_setting(prop.name) || !prop.name.begins_with("input/")) {
+			continue;
+		}
+
+		action_values.insert(prop.name, ps->get(prop.name));
+
+		undo_redo->add_do_method(ProjectSettings::get_singleton(), "clear", prop.name);
+		undo_redo->add_undo_method(ProjectSettings::get_singleton(), "clear", prop.name);
+	}
+
+	for (OrderedHashMap<String, Variant>::Element E = action_values.front(); E; E = E.next()) {
+		String name = E.key();
+		Variant value = E.get();
+
+		if (name == target_name) {
+			if (p_before) {
+				// Insert before target
+				undo_redo->add_do_method(ProjectSettings::get_singleton(), "set", action_name, action_value);
+				undo_redo->add_do_method(ProjectSettings::get_singleton(), "set", target_name, target_value);
+
+				undo_redo->add_undo_method(ProjectSettings::get_singleton(), "set", target_name, target_value);
+				undo_redo->add_undo_method(ProjectSettings::get_singleton(), "set", action_name, action_value);
+			} else {
+				// Insert after target
+				undo_redo->add_do_method(ProjectSettings::get_singleton(), "set", target_name, target_value);
+				undo_redo->add_do_method(ProjectSettings::get_singleton(), "set", action_name, action_value);
+
+				undo_redo->add_undo_method(ProjectSettings::get_singleton(), "set", action_name, action_value);
+				undo_redo->add_undo_method(ProjectSettings::get_singleton(), "set", target_name, target_value);
+			}
+
+		} else if (name != action_name) {
+			undo_redo->add_do_method(ProjectSettings::get_singleton(), "set", name, value);
+			undo_redo->add_undo_method(ProjectSettings::get_singleton(), "set", name, value);
+		}
+	}
+
+	undo_redo->add_do_method(this, "_update_action_map_editor");
+	undo_redo->add_undo_method(this, "_update_action_map_editor");
+	undo_redo->add_do_method(this, "queue_save");
+	undo_redo->add_undo_method(this, "queue_save");
+	undo_redo->commit_action();
+}
+
+void ProjectSettingsEditor::_update_action_map_editor() {
+	Vector<ActionMapEditor::ActionInfo> actions;
+
+	List<PropertyInfo> props;
+	ProjectSettings::get_singleton()->get_property_list(&props);
+
+	const Ref<Texture2D> builtin_icon = get_theme_icon("PinPressed", "EditorIcons");
+	for (List<PropertyInfo>::Element *E = props.front(); E; E = E->next()) {
+		const String property_name = E->get().name;
+
+		if (!property_name.begins_with("input/")) {
+			continue;
+		}
+
+		// Strip the "input/" from the left.
+		String display_name = property_name.substr(String("input/").size() - 1);
+		Dictionary action = ProjectSettings::get_singleton()->get(property_name);
+
+		ActionMapEditor::ActionInfo action_info;
+		action_info.action = action;
+		action_info.editable = true;
+		action_info.name = display_name;
+
+		const bool is_builtin_input = ProjectSettings::get_singleton()->get_input_presets().find(property_name) != nullptr;
+		if (is_builtin_input) {
+			action_info.editable = false;
+			action_info.icon = builtin_icon;
+		}
+
+		actions.push_back(action_info);
+	}
+
+	action_map->update_action_list(actions);
+}
+
 void ProjectSettingsEditor::_notification(int p_what) {
 	switch (p_what) {
 		case NOTIFICATION_VISIBILITY_CHANGED: {
@@ -289,6 +489,8 @@ void ProjectSettingsEditor::_notification(int p_what) {
 			restart_container->add_theme_style_override("panel", get_theme_stylebox("bg", "Tree"));
 			restart_icon->set_texture(get_theme_icon("StatusWarning", "EditorIcons"));
 			restart_label->add_theme_color_override("font_color", get_theme_color("warning_color", "Editor"));
+
+			_update_action_map_editor();
 		} break;
 		case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {
 			search_box->set_right_icon(get_theme_icon("Search", "EditorIcons"));
@@ -299,6 +501,8 @@ void ProjectSettingsEditor::_notification(int p_what) {
 
 void ProjectSettingsEditor::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("queue_save"), &ProjectSettingsEditor::queue_save);
+
+	ClassDB::bind_method(D_METHOD("_update_action_map_editor"), &ProjectSettingsEditor::_update_action_map_editor);
 }
 
 ProjectSettingsEditor::ProjectSettingsEditor(EditorData *p_data) {
@@ -437,10 +641,16 @@ ProjectSettingsEditor::ProjectSettingsEditor(EditorData *p_data) {
 	restart_close_button->connect("pressed", callable_mp(this, &ProjectSettingsEditor::_editor_restart_close));
 	restart_hb->add_child(restart_close_button);
 
-	inputmap_editor = memnew(InputMapEditor);
-	inputmap_editor->set_name(TTR("Input Map"));
-	inputmap_editor->connect("inputmap_changed", callable_mp(this, &ProjectSettingsEditor::queue_save));
-	tab_container->add_child(inputmap_editor);
+	action_map = memnew(ActionMapEditor);
+	action_map->set_name(TTR("Input Map"));
+	action_map->connect("action_added", callable_mp(this, &ProjectSettingsEditor::_action_added));
+	action_map->connect("action_edited", callable_mp(this, &ProjectSettingsEditor::_action_edited));
+	action_map->connect("action_removed", callable_mp(this, &ProjectSettingsEditor::_action_removed));
+	action_map->connect("action_renamed", callable_mp(this, &ProjectSettingsEditor::_action_renamed));
+	action_map->connect("action_reordered", callable_mp(this, &ProjectSettingsEditor::_action_reordered));
+	action_map->set_toggle_editable_label(TTR("Show built-in Actions"));
+	action_map->set_show_uneditable(false);
+	tab_container->add_child(action_map);
 
 	localization_editor = memnew(LocalizationEditor);
 	localization_editor->set_name(TTR("Localization"));

+ 10 - 10
editor/project_settings_editor.h

@@ -32,10 +32,10 @@
 #define PROJECT_SETTINGS_EDITOR_H
 
 #include "core/object/undo_redo.h"
+#include "editor/action_map_editor.h"
 #include "editor/editor_data.h"
 #include "editor/editor_plugin_settings.h"
 #include "editor/editor_sectioned_inspector.h"
-#include "editor/input_map_editor.h"
 #include "editor/localization_editor.h"
 #include "editor/shader_globals_editor.h"
 #include "editor_autoload_settings.h"
@@ -44,26 +44,18 @@
 class ProjectSettingsEditor : public AcceptDialog {
 	GDCLASS(ProjectSettingsEditor, AcceptDialog);
 
-	enum InputType {
-		INPUT_KEY,
-		INPUT_KEY_PHYSICAL,
-		INPUT_JOY_BUTTON,
-		INPUT_JOY_MOTION,
-		INPUT_MOUSE_BUTTON
-	};
-
 	static ProjectSettingsEditor *singleton;
 	ProjectSettings *ps;
 	Timer *timer;
 
 	TabContainer *tab_container;
 	SectionedInspector *inspector;
-	InputMapEditor *inputmap_editor;
 	LocalizationEditor *localization_editor;
 	EditorAutoloadSettings *autoload_settings;
 	ShaderGlobalsEditor *shaders_global_variables_editor;
 	EditorPluginSettings *plugin_settings;
 
+	ActionMapEditor *action_map;
 	HBoxContainer *search_bar;
 	LineEdit *search_box;
 	CheckButton *advanced;
@@ -102,6 +94,14 @@ class ProjectSettingsEditor : public AcceptDialog {
 	void _editor_restart_close();
 
 	void _add_feature_overrides();
+
+	void _action_added(const String &p_name);
+	void _action_edited(const String &p_name, const Dictionary &p_action);
+	void _action_removed(const String &p_name);
+	void _action_renamed(const String &p_old_name, const String &p_new_name);
+	void _action_reordered(const String &p_action_name, const String &p_relative_to, bool p_before);
+	void _update_action_map_editor();
+
 	ProjectSettingsEditor();
 
 protected:

+ 248 - 96
editor/settings_config_dialog.cpp

@@ -31,6 +31,7 @@
 #include "settings_config_dialog.h"
 
 #include "core/config/project_settings.h"
+#include "core/input/input_map.h"
 #include "core/os/keyboard.h"
 #include "editor/debugger/editor_debugger_node.h"
 #include "editor_file_system.h"
@@ -184,7 +185,52 @@ void EditorSettingsDialog::_update_icons() {
 	restart_label->add_theme_color_override("font_color", shortcuts->get_theme_color("warning_color", "Editor"));
 }
 
+void EditorSettingsDialog::_event_config_confirmed() {
+	Ref<InputEventKey> k = shortcut_editor->get_event();
+	if (k.is_null()) {
+		return;
+	}
+
+	if (editing_action) {
+		if (current_action_event_index == -1) {
+			// Add new event
+			current_action_events.push_back(k);
+		} else {
+			// Edit existing event
+			current_action_events[current_action_event_index] = k;
+		}
+
+		_update_builtin_action(current_action, current_action_events);
+	} else {
+		k = k->duplicate();
+		Ref<Shortcut> current_sc = EditorSettings::get_singleton()->get_shortcut(shortcut_being_edited);
+
+		undo_redo->create_action(TTR("Change Shortcut") + " '" + shortcut_being_edited + "'");
+		undo_redo->add_do_method(current_sc.ptr(), "set_shortcut", k);
+		undo_redo->add_undo_method(current_sc.ptr(), "set_shortcut", current_sc->get_shortcut());
+		undo_redo->add_do_method(this, "_update_shortcuts");
+		undo_redo->add_undo_method(this, "_update_shortcuts");
+		undo_redo->add_do_method(this, "_settings_changed");
+		undo_redo->add_undo_method(this, "_settings_changed");
+		undo_redo->commit_action();
+	}
+}
+
+void EditorSettingsDialog::_update_builtin_action(const String &p_name, const Array &p_events) {
+	Array old_input_array = EditorSettings::get_singleton()->get_builtin_action_overrides(current_action);
+
+	undo_redo->create_action(TTR("Edit Built-in Action"));
+	undo_redo->add_do_method(EditorSettings::get_singleton(), "set_builtin_action_override", p_name, p_events);
+	undo_redo->add_undo_method(EditorSettings::get_singleton(), "set_builtin_action_override", p_name, old_input_array);
+	undo_redo->add_do_method(this, "_settings_changed");
+	undo_redo->add_undo_method(this, "_settings_changed");
+	undo_redo->commit_action();
+
+	_update_shortcuts();
+}
+
 void EditorSettingsDialog::_update_shortcuts() {
+	// Before clearing the tree, take note of which categories are collapsed so that this state can be maintained when the tree is repopulated.
 	Map<String, bool> collapsed;
 
 	if (shortcuts->get_root() && shortcuts->get_root()->get_children()) {
@@ -192,15 +238,93 @@ void EditorSettingsDialog::_update_shortcuts() {
 			collapsed[item->get_text(0)] = item->is_collapsed();
 		}
 	}
-
 	shortcuts->clear();
 
-	List<String> slist;
-	EditorSettings::get_singleton()->get_shortcut_list(&slist);
 	TreeItem *root = shortcuts->create_item();
-
 	Map<String, TreeItem *> sections;
 
+	// Set up section for Common/Built-in actions
+	TreeItem *common_section = shortcuts->create_item(root);
+
+	sections["Common"] = common_section;
+	common_section->set_text(0, TTR("Common"));
+	if (collapsed.has("Common")) {
+		common_section->set_collapsed(collapsed["Common"]);
+	}
+	common_section->set_custom_bg_color(0, shortcuts->get_theme_color("prop_subsection", "Editor"));
+	common_section->set_custom_bg_color(1, shortcuts->get_theme_color("prop_subsection", "Editor"));
+
+	// Get the action map for the editor, and add each item to the "Common" section.
+	OrderedHashMap<StringName, InputMap::Action> action_map = InputMap::get_singleton()->get_action_map();
+	for (OrderedHashMap<StringName, InputMap::Action>::Element E = action_map.front(); E; E = E.next()) {
+		String action_name = E.key();
+
+		if (!shortcut_filter.is_subsequence_ofi(action_name)) {
+			continue;
+		}
+
+		InputMap::Action action = E.get();
+
+		Array events; // Need to get the list of events into an array so it can be set as metadata on the item.
+		Vector<String> event_strings;
+
+		List<Ref<InputEvent>> defaults = InputMap::get_singleton()->get_builtins().find(action_name).value();
+		// Remove all non-key events from the defaults.
+		for (List<Ref<InputEvent>>::Element *I = defaults.front(); I; I = I->next()) {
+			Ref<InputEventKey> k = I->get();
+			if (k.is_null()) {
+				I->erase();
+			}
+		}
+
+		bool same_as_defaults = defaults.size() == action.inputs.size(); // Initially this is set to just whether the arrays are equal. Later we check the events if needed.
+
+		int count = 0;
+		for (List<Ref<InputEvent>>::Element *I = action.inputs.front(); I; I = I->next()) {
+			// Add event and event text to respective arrays.
+			events.push_back(I->get());
+			event_strings.push_back(I->get()->as_text());
+
+			// Only check if the events have been the same so far - once one fails, we don't need to check any more.
+			if (same_as_defaults) {
+				Ref<InputEventKey> k = defaults[count];
+				// Only check keys, since we are in the editor.
+				if (k.is_valid() && !defaults[count]->shortcut_match(I->get())) {
+					same_as_defaults = false;
+				}
+			}
+			count++;
+		}
+
+		// Join the text of the events with a delimiter so they can all be displayed in one cell.
+		String events_display_string = event_strings.is_empty() ? "None" : String("; ").join(event_strings);
+
+		TreeItem *item = shortcuts->create_item(common_section);
+		item->set_text(0, action_name);
+		item->set_text(1, events_display_string);
+
+		if (!same_as_defaults) {
+			item->add_button(1, shortcuts->get_theme_icon("Reload", "EditorIcons"), 2);
+		}
+
+		if (events_display_string == "None") {
+			// Fade out unassigned shortcut labels for easier visual grepping.
+			item->set_custom_color(1, shortcuts->get_theme_color("font_color", "Label") * Color(1, 1, 1, 0.5));
+		}
+
+		item->add_button(1, shortcuts->get_theme_icon("Edit", "EditorIcons"), 0);
+		item->add_button(1, shortcuts->get_theme_icon("Close", "EditorIcons"), 1);
+		item->set_tooltip(0, action_name);
+		item->set_tooltip(1, events_display_string);
+		item->set_metadata(0, "Common");
+		item->set_metadata(1, events);
+	}
+
+	// Editor Shortcuts
+
+	List<String> slist;
+	EditorSettings::get_singleton()->get_shortcut_list(&slist);
+
 	for (List<String>::Element *E = slist.front(); E; E = E->next()) {
 		Ref<Shortcut> sc = EditorSettings::get_singleton()->get_shortcut(E->get());
 		if (!sc->has_meta("original")) {
@@ -267,84 +391,119 @@ void EditorSettingsDialog::_shortcut_button_pressed(Object *p_item, int p_column
 	TreeItem *ti = Object::cast_to<TreeItem>(p_item);
 	ERR_FAIL_COND(!ti);
 
-	String item = ti->get_metadata(0);
-	Ref<Shortcut> sc = EditorSettings::get_singleton()->get_shortcut(item);
-
-	if (p_idx == 0) {
-		press_a_key_label->set_text(TTR("Press a Key..."));
-		last_wait_for_key = Ref<InputEventKey>();
-		press_a_key->popup_centered(Size2(250, 80) * EDSCALE);
-		//press_a_key->grab_focus();
-		press_a_key->get_ok_button()->set_focus_mode(Control::FOCUS_NONE);
-		press_a_key->get_cancel_button()->set_focus_mode(Control::FOCUS_NONE);
-		shortcut_configured = item;
-
-	} else if (p_idx == 1) { //erase
-		if (!sc.is_valid()) {
-			return; //pointless, there is nothing
+	if (ti->get_metadata(0) == "Common") {
+		// Editing a Built-in action, which can have multiple bindings.
+		button_idx = p_idx;
+		editing_action = true;
+		current_action = ti->get_text(0);
+
+		switch (button_idx) {
+			case SHORTCUT_REVERT: {
+				Array events;
+				List<Ref<InputEvent>> defaults = InputMap::get_singleton()->get_builtins()[current_action];
+
+				// Convert the list to an array, and only keep key events as this is for the editor.
+				for (List<Ref<InputEvent>>::Element *E = defaults.front(); E; E = E->next()) {
+					Ref<InputEventKey> k = E->get();
+					if (k.is_valid()) {
+						events.append(E->get());
+					}
+				}
+
+				_update_builtin_action(current_action, events);
+			} break;
+			case SHORTCUT_EDIT:
+			case SHORTCUT_ERASE: {
+				// For Edit end Delete, we will show a popup which displays each event so the user can select which one to edit/delete.
+				current_action_events = ti->get_metadata(1);
+				action_popup->clear();
+
+				for (int i = 0; i < current_action_events.size(); i++) {
+					Ref<InputEvent> ie = current_action_events[i];
+					action_popup->add_item(ie->as_text());
+					action_popup->set_item_metadata(i, ie);
+				}
+
+				if (button_idx == SHORTCUT_EDIT) {
+					// If editing, add a button which can be used to add an additional event.
+					action_popup->add_icon_item(get_theme_icon("Add", "EditorIcons"), TTR("Add"));
+				}
+
+				action_popup->set_position(get_position() + get_mouse_position());
+				action_popup->take_mouse_focus();
+				action_popup->popup();
+				action_popup->set_as_minsize();
+			} break;
+			default:
+				break;
 		}
-
-		undo_redo->create_action(TTR("Erase Shortcut"));
-		undo_redo->add_do_method(sc.ptr(), "set_shortcut", Ref<InputEvent>());
-		undo_redo->add_undo_method(sc.ptr(), "set_shortcut", sc->get_shortcut());
-		undo_redo->add_do_method(this, "_update_shortcuts");
-		undo_redo->add_undo_method(this, "_update_shortcuts");
-		undo_redo->add_do_method(this, "_settings_changed");
-		undo_redo->add_undo_method(this, "_settings_changed");
-		undo_redo->commit_action();
-	} else if (p_idx == 2) { //revert to original
-		if (!sc.is_valid()) {
-			return; //pointless, there is nothing
+	} else {
+		// Editing an Editor Shortcut, which can only have 1 binding.
+		String item = ti->get_metadata(0);
+		Ref<Shortcut> sc = EditorSettings::get_singleton()->get_shortcut(item);
+		editing_action = false;
+
+		switch (button_idx) {
+			case EditorSettingsDialog::SHORTCUT_EDIT:
+				shortcut_editor->popup_and_configure(sc->get_shortcut());
+				shortcut_being_edited = item;
+				break;
+			case EditorSettingsDialog::SHORTCUT_ERASE: {
+				if (!sc.is_valid()) {
+					return; //pointless, there is nothing
+				}
+
+				undo_redo->create_action(TTR("Erase Shortcut"));
+				undo_redo->add_do_method(sc.ptr(), "set_shortcut", Ref<InputEvent>());
+				undo_redo->add_undo_method(sc.ptr(), "set_shortcut", sc->get_shortcut());
+				undo_redo->add_do_method(this, "_update_shortcuts");
+				undo_redo->add_undo_method(this, "_update_shortcuts");
+				undo_redo->add_do_method(this, "_settings_changed");
+				undo_redo->add_undo_method(this, "_settings_changed");
+				undo_redo->commit_action();
+			} break;
+			case EditorSettingsDialog::SHORTCUT_REVERT: {
+				if (!sc.is_valid()) {
+					return; //pointless, there is nothing
+				}
+
+				Ref<InputEvent> original = sc->get_meta("original");
+
+				undo_redo->create_action(TTR("Restore Shortcut"));
+				undo_redo->add_do_method(sc.ptr(), "set_shortcut", original);
+				undo_redo->add_undo_method(sc.ptr(), "set_shortcut", sc->get_shortcut());
+				undo_redo->add_do_method(this, "_update_shortcuts");
+				undo_redo->add_undo_method(this, "_update_shortcuts");
+				undo_redo->add_do_method(this, "_settings_changed");
+				undo_redo->add_undo_method(this, "_settings_changed");
+				undo_redo->commit_action();
+			} break;
+			default:
+				break;
 		}
-
-		Ref<InputEvent> original = sc->get_meta("original");
-
-		undo_redo->create_action(TTR("Restore Shortcut"));
-		undo_redo->add_do_method(sc.ptr(), "set_shortcut", original);
-		undo_redo->add_undo_method(sc.ptr(), "set_shortcut", sc->get_shortcut());
-		undo_redo->add_do_method(this, "_update_shortcuts");
-		undo_redo->add_undo_method(this, "_update_shortcuts");
-		undo_redo->add_do_method(this, "_settings_changed");
-		undo_redo->add_undo_method(this, "_settings_changed");
-		undo_redo->commit_action();
 	}
 }
 
-void EditorSettingsDialog::_wait_for_key(const Ref<InputEvent> &p_event) {
-	Ref<InputEventKey> k = p_event;
-
-	if (k.is_valid() && k->is_pressed() && k->get_keycode() != 0) {
-		last_wait_for_key = k;
-		const String str = keycode_get_string(k->get_keycode_with_modifiers());
-
-		press_a_key_label->set_text(str);
-		press_a_key->set_input_as_handled();
-	}
-}
-
-void EditorSettingsDialog::_press_a_key_confirm() {
-	if (last_wait_for_key.is_null()) {
-		return;
+void EditorSettingsDialog::_builtin_action_popup_index_pressed(int p_index) {
+	switch (button_idx) {
+		case SHORTCUT_EDIT: {
+			if (p_index == action_popup->get_item_count() - 1) {
+				// Selected last item in list (Add button), therefore add new
+				current_action_event_index = -1;
+				shortcut_editor->popup_and_configure();
+			} else {
+				// Configure existing
+				current_action_event_index = p_index;
+				shortcut_editor->popup_and_configure(action_popup->get_item_metadata(p_index));
+			}
+		} break;
+		case SHORTCUT_ERASE: {
+			current_action_events.remove(p_index);
+			_update_builtin_action(current_action, current_action_events);
+		} break;
+		default:
+			break;
 	}
-
-	Ref<InputEventKey> ie;
-	ie.instance();
-	ie->set_keycode(last_wait_for_key->get_keycode());
-	ie->set_shift(last_wait_for_key->get_shift());
-	ie->set_control(last_wait_for_key->get_control());
-	ie->set_alt(last_wait_for_key->get_alt());
-	ie->set_metakey(last_wait_for_key->get_metakey());
-
-	Ref<Shortcut> sc = EditorSettings::get_singleton()->get_shortcut(shortcut_configured);
-
-	undo_redo->create_action(TTR("Change Shortcut") + " '" + shortcut_configured + "'");
-	undo_redo->add_do_method(sc.ptr(), "set_shortcut", ie);
-	undo_redo->add_undo_method(sc.ptr(), "set_shortcut", sc->get_shortcut());
-	undo_redo->add_do_method(this, "_update_shortcuts");
-	undo_redo->add_undo_method(this, "_update_shortcuts");
-	undo_redo->add_do_method(this, "_settings_changed");
-	undo_redo->add_undo_method(this, "_settings_changed");
-	undo_redo->commit_action();
 }
 
 void EditorSettingsDialog::_tabs_tab_changed(int p_tab) {
@@ -382,9 +541,14 @@ void EditorSettingsDialog::_editor_restart_close() {
 void EditorSettingsDialog::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("_unhandled_input"), &EditorSettingsDialog::_unhandled_input);
 	ClassDB::bind_method(D_METHOD("_update_shortcuts"), &EditorSettingsDialog::_update_shortcuts);
+	ClassDB::bind_method(D_METHOD("_settings_changed"), &EditorSettingsDialog::_settings_changed);
 }
 
 EditorSettingsDialog::EditorSettingsDialog() {
+	action_popup = memnew(PopupMenu);
+	action_popup->connect("index_pressed", callable_mp(this, &EditorSettingsDialog::_builtin_action_popup_index_pressed));
+	add_child(action_popup);
+
 	set_title(TTR("Editor Settings"));
 
 	undo_redo = memnew(UndoRedo);
@@ -442,21 +606,17 @@ EditorSettingsDialog::EditorSettingsDialog() {
 	// Shortcuts Tab
 
 	tab_shortcuts = memnew(VBoxContainer);
+
 	tabs->add_child(tab_shortcuts);
 	tab_shortcuts->set_name(TTR("Shortcuts"));
 
-	hbc = memnew(HBoxContainer);
-	hbc->set_h_size_flags(Control::SIZE_EXPAND_FILL);
-	tab_shortcuts->add_child(hbc);
-
 	shortcut_search_box = memnew(LineEdit);
 	shortcut_search_box->set_placeholder(TTR("Search"));
 	shortcut_search_box->set_h_size_flags(Control::SIZE_EXPAND_FILL);
-	hbc->add_child(shortcut_search_box);
+	tab_shortcuts->add_child(shortcut_search_box);
 	shortcut_search_box->connect("text_changed", callable_mp(this, &EditorSettingsDialog::_filter_shortcuts));
 
 	shortcuts = memnew(Tree);
-	tab_shortcuts->add_child(shortcuts, true);
 	shortcuts->set_v_size_flags(Control::SIZE_EXPAND_FILL);
 	shortcuts->set_columns(2);
 	shortcuts->set_hide_root(true);
@@ -464,21 +624,13 @@ EditorSettingsDialog::EditorSettingsDialog() {
 	shortcuts->set_column_title(0, TTR("Name"));
 	shortcuts->set_column_title(1, TTR("Binding"));
 	shortcuts->connect("button_pressed", callable_mp(this, &EditorSettingsDialog::_shortcut_button_pressed));
+	tab_shortcuts->add_child(shortcuts);
 
-	press_a_key = memnew(ConfirmationDialog);
-	//press_a_key->set_focus_mode(Control::FOCUS_ALL);
-	add_child(press_a_key);
-
-	Label *l = memnew(Label);
-	l->set_text(TTR("Press a Key..."));
-	l->set_anchors_and_offsets_preset(Control::PRESET_WIDE);
-	l->set_align(Label::ALIGN_CENTER);
-	l->set_offset(SIDE_TOP, 20);
-	l->set_anchor_and_offset(SIDE_BOTTOM, Control::ANCHOR_BEGIN, 30);
-	press_a_key_label = l;
-	press_a_key->add_child(l);
-	press_a_key->connect("window_input", callable_mp(this, &EditorSettingsDialog::_wait_for_key));
-	press_a_key->connect("confirmed", callable_mp(this, &EditorSettingsDialog::_press_a_key_confirm));
+	// Adding event dialog
+	shortcut_editor = memnew(InputEventConfigurationDialog);
+	shortcut_editor->connect("confirmed", callable_mp(this, &EditorSettingsDialog::_event_config_confirmed));
+	shortcut_editor->set_allowed_input_types(InputEventConfigurationDialog::InputType::INPUT_KEY);
+	add_child(shortcut_editor);
 
 	set_hide_on_ok(true);
 

+ 23 - 10
editor/settings_config_dialog.h

@@ -31,6 +31,7 @@
 #ifndef SETTINGS_CONFIG_DIALOG_H
 #define SETTINGS_CONFIG_DIALOG_H
 
+#include "editor/action_map_editor.h"
 #include "editor/editor_sectioned_inspector.h"
 #include "editor_inspector.h"
 #include "scene/gui/dialogs.h"
@@ -52,16 +53,28 @@ class EditorSettingsDialog : public AcceptDialog {
 	LineEdit *shortcut_search_box;
 	SectionedInspector *inspector;
 
+	enum ShortcutButton {
+		SHORTCUT_EDIT,
+		SHORTCUT_ERASE,
+		SHORTCUT_REVERT
+	};
+
+	int button_idx;
+	int current_action_event_index = -1;
+	bool editing_action = false;
+	String current_action;
+	Array current_action_events;
+	PopupMenu *action_popup;
+
 	Timer *timer;
 
 	UndoRedo *undo_redo;
-	Tree *shortcuts;
 
-	ConfirmationDialog *press_a_key;
-	Label *press_a_key_label;
-	Ref<InputEventKey> last_wait_for_key;
-	String shortcut_configured;
+	// Shortcuts
 	String shortcut_filter;
+	Tree *shortcuts;
+	InputEventConfigurationDialog *shortcut_editor;
+	String shortcut_being_edited;
 
 	virtual void cancel_pressed() override;
 	virtual void ok_pressed() override;
@@ -74,20 +87,20 @@ class EditorSettingsDialog : public AcceptDialog {
 	void _notification(int p_what);
 	void _update_icons();
 
-	void _press_a_key_confirm();
-	void _wait_for_key(const Ref<InputEvent> &p_event);
+	void _event_config_confirmed();
+
+	void _update_builtin_action(const String &p_name, const Array &p_events);
 
 	void _tabs_tab_changed(int p_tab);
 	void _focus_current_search_box();
 
-	void _clear_shortcut_search_box();
-	void _clear_search_box();
-
 	void _filter_shortcuts(const String &p_filter);
 
 	void _update_shortcuts();
 	void _shortcut_button_pressed(Object *p_item, int p_column, int p_idx);
 
+	void _builtin_action_popup_index_pressed(int p_index);
+
 	static void _undo_redo_callback(void *p_self, const String &p_name);
 
 	Label *restart_label;

+ 0 - 4
scene/main/window.cpp

@@ -881,10 +881,6 @@ bool Window::_can_consume_input_events() const {
 }
 
 void Window::_window_input(const Ref<InputEvent> &p_ev) {
-	if (Engine::get_singleton()->is_editor_hint() && (Object::cast_to<InputEventJoypadButton>(p_ev.ptr()) || Object::cast_to<InputEventJoypadMotion>(*p_ev))) {
-		return; //avoid joy input on editor
-	}
-
 	if (EngineDebugger::is_active()) {
 		//quit from game window using F8
 		Ref<InputEventKey> k = p_ev;