Browse Source

Allow shortcuts to have any number of bindings. Updated UI as required.

Eric M 4 years ago
parent
commit
ad30b0a8dd

+ 70 - 14
core/input/shortcut.cpp

@@ -31,14 +31,26 @@
 #include "shortcut.h"
 #include "core/os/keyboard.h"
 
-void Shortcut::set_event(const Ref<InputEvent> &p_event) {
-	ERR_FAIL_COND_MSG(Object::cast_to<InputEventShortcut>(*p_event), "Cannot set a shortcut event to an instance of InputEventShortcut.");
-	event = p_event;
+void Shortcut::set_events(const Array &p_events) {
+	for (int i = 0; i < p_events.size(); i++) {
+		Ref<InputEventShortcut> ies = p_events[i];
+		ERR_FAIL_COND_MSG(ies.is_valid(), "Cannot set a shortcut event to an instance of InputEventShortcut.");
+	}
+
+	events = p_events;
 	emit_changed();
 }
 
-Ref<InputEvent> Shortcut::get_event() const {
-	return event;
+void Shortcut::set_events_list(const List<Ref<InputEvent>> *p_events) {
+	events.clear();
+
+	for (const Ref<InputEvent> &ie : *p_events) {
+		events.push_back(ie);
+	}
+}
+
+Array Shortcut::get_events() const {
+	return events;
 }
 
 bool Shortcut::matches_event(const Ref<InputEvent> &p_event) const {
@@ -48,29 +60,73 @@ bool Shortcut::matches_event(const Ref<InputEvent> &p_event) const {
 			return true;
 		}
 	}
-	return event.is_valid() && event->is_match(p_event, true);
+
+	for (int i = 0; i < events.size(); i++) {
+		Ref<InputEvent> ie = events[i];
+		bool valid = ie.is_valid() && ie->is_match(p_event);
+
+		// Stop on first valid event - don't need to check further.
+		if (valid) {
+			return true;
+		}
+	}
+
+	return false;
 }
 
 String Shortcut::get_as_text() const {
-	if (event.is_valid()) {
-		return event->as_text();
-	} else {
-		return "None";
+	for (int i = 0; i < events.size(); i++) {
+		Ref<InputEvent> ie = events[i];
+		// Return first shortcut which is valid
+		if (ie.is_valid()) {
+			return ie->as_text();
+		}
 	}
+
+	return "None";
 }
 
 bool Shortcut::has_valid_event() const {
-	return event.is_valid();
+	// Tests if there is ANY input event which is valid.
+	for (int i = 0; i < events.size(); i++) {
+		Ref<InputEvent> ie = events[i];
+		if (ie.is_valid()) {
+			return true;
+		}
+	}
+
+	return false;
 }
 
 void Shortcut::_bind_methods() {
-	ClassDB::bind_method(D_METHOD("set_event", "event"), &Shortcut::set_event);
-	ClassDB::bind_method(D_METHOD("get_event"), &Shortcut::get_event);
+	ClassDB::bind_method(D_METHOD("set_events", "events"), &Shortcut::set_events);
+	ClassDB::bind_method(D_METHOD("get_events"), &Shortcut::get_events);
 
 	ClassDB::bind_method(D_METHOD("has_valid_event"), &Shortcut::has_valid_event);
 
 	ClassDB::bind_method(D_METHOD("matches_event", "event"), &Shortcut::matches_event);
 	ClassDB::bind_method(D_METHOD("get_as_text"), &Shortcut::get_as_text);
 
-	ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "event", PROPERTY_HINT_RESOURCE_TYPE, "InputEvent"), "set_event", "get_event");
+	ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "events", PROPERTY_HINT_ARRAY_TYPE, vformat("%s/%s:%s", Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE, "InputEvent")), "set_events", "get_events");
+}
+
+bool Shortcut::is_event_array_equal(const Array &p_event_array1, const Array &p_event_array2) {
+	if (p_event_array1.size() != p_event_array2.size()) {
+		return false;
+	}
+
+	bool is_same = true;
+	for (int i = 0; i < p_event_array1.size(); i++) {
+		Ref<InputEvent> ie_1 = p_event_array1[i];
+		Ref<InputEvent> ie_2 = p_event_array2[i];
+
+		is_same = ie_1->is_match(ie_2);
+
+		// Break on the first that doesn't match - don't need to check further.
+		if (!is_same) {
+			break;
+		}
+	}
+
+	return is_same;
 }

+ 8 - 4
core/input/shortcut.h

@@ -37,18 +37,22 @@
 class Shortcut : public Resource {
 	GDCLASS(Shortcut, Resource);
 
-	Ref<InputEvent> event;
+	Array events;
 
 protected:
 	static void _bind_methods();
 
 public:
-	void set_event(const Ref<InputEvent> &p_shortcut);
-	Ref<InputEvent> get_event() const;
+	void set_events(const Array &p_events);
+	Array get_events() const;
+
+	void set_events_list(const List<Ref<InputEvent>> *p_events);
+
 	bool matches_event(const Ref<InputEvent> &p_event) const;
 	bool has_valid_event() const;
 
 	String get_as_text() const;
-};
 
+	static bool is_event_array_equal(const Array &p_event_array1, const Array &p_event_array2);
+};
 #endif // SHORTCUT_H

+ 3 - 3
doc/classes/Control.xml

@@ -742,10 +742,10 @@
 		</method>
 		<method name="set_drag_forwarding">
 			<return type="void" />
-			<argument index="0" name="target" type="Control" />
+			<argument index="0" name="target" type="Node" />
 			<description>
-				Forwards the handling of this control's drag and drop to [code]target[/code] control.
-				Forwarding can be implemented in the target control similar to the methods [method _get_drag_data], [method _can_drop_data], and [method _drop_data] but with two differences:
+				Forwards the handling of this control's drag and drop to [code]target[/code] node.
+				Forwarding can be implemented in the target node similar to the methods [method _get_drag_data], [method _can_drop_data], and [method _drop_data] but with two differences:
 				1. The function name must be suffixed with [b]_fw[/b]
 				2. The function must take an extra argument that is the control doing the forwarding
 				[codeblocks]

+ 7 - 7
doc/classes/Shortcut.xml

@@ -4,8 +4,8 @@
 		A shortcut for binding input.
 	</brief_description>
 	<description>
-		A shortcut for binding input.
 		Shortcuts are commonly used for interacting with a [Control] element from an [InputEvent] (also known as hotkeys).
+		One shortcut can contain multiple [InputEvent]'s, allowing the possibility of triggering one action with multiple different inputs.
 	</description>
 	<tutorials>
 	</tutorials>
@@ -13,27 +13,27 @@
 		<method name="get_as_text" qualifiers="const">
 			<return type="String" />
 			<description>
-				Returns the shortcut's [InputEvent] as a [String].
+				Returns the shortcut's first valid [InputEvent] as a [String].
 			</description>
 		</method>
 		<method name="has_valid_event" qualifiers="const">
 			<return type="bool" />
 			<description>
-				Returns whether the shortcut has a valid [member event] assigned to it.
+				Returns whether [member events] contains an [InputEvent] which is valid.
 			</description>
 		</method>
 		<method name="matches_event" qualifiers="const">
 			<return type="bool" />
 			<argument index="0" name="event" type="InputEvent" />
 			<description>
-				Returns whether the shortcut's [member event] matches [code]event[/code].
+				Returns whether any [InputEvent] in [member events] equals [code]event[/code].
 			</description>
 		</method>
 	</methods>
 	<members>
-		<member name="event" type="InputEvent" setter="set_event" getter="get_event">
-			The shortcut's [InputEvent].
-			Generally the [InputEvent] is a keyboard key, though it can be any [InputEvent], including an [InputEventAction].
+		<member name="events" type="Array" setter="set_events" getter="get_events" default="[]">
+			The shortcut's [InputEvent] array.
+			Generally the [InputEvent] used is an [InputEventKey], though it can be any [InputEvent], including an [InputEventAction].
 		</member>
 	</members>
 </class>

+ 108 - 45
editor/editor_settings.cpp

@@ -73,14 +73,15 @@ bool EditorSettings::_set_only(const StringName &p_name, const Variant &p_value)
 
 	if (p_name == "shortcuts") {
 		Array arr = p_value;
-		ERR_FAIL_COND_V(arr.size() && arr.size() & 1, true);
-		for (int i = 0; i < arr.size(); i += 2) {
-			String name = arr[i];
-			Ref<InputEvent> shortcut = arr[i + 1];
+		for (int i = 0; i < arr.size(); i++) {
+			Dictionary dict = arr[i];
+			String name = dict["name"];
+
+			Array shortcut_events = dict["shortcuts"];
 
 			Ref<Shortcut> sc;
 			sc.instantiate();
-			sc->set_event(shortcut);
+			sc->set_events(shortcut_events);
 			add_shortcut(name, sc);
 		}
 
@@ -138,11 +139,11 @@ bool EditorSettings::_get(const StringName &p_name, Variant &r_ret) const {
 	_THREAD_SAFE_METHOD_
 
 	if (p_name == "shortcuts") {
-		Array arr;
-		for (const KeyValue<String, Ref<Shortcut>> &E : shortcuts) {
-			Ref<Shortcut> sc = E.value;
+		Array save_array;
+		for (const KeyValue<String, Ref<Shortcut>> &shortcut_definition : shortcuts) {
+			Ref<Shortcut> sc = shortcut_definition.value;
 
-			if (builtin_action_overrides.has(E.key)) {
+			if (builtin_action_overrides.has(shortcut_definition.key)) {
 				// This shortcut was auto-generated from built in actions: don't save.
 				continue;
 			}
@@ -151,34 +152,57 @@ bool EditorSettings::_get(const StringName &p_name, Variant &r_ret) const {
 				if (!sc->has_meta("original")) {
 					continue; //this came from settings but is not any longer used
 				}
+			}
 
-				Ref<InputEvent> original = sc->get_meta("original");
-				if (sc->matches_event(original) || (original.is_null() && sc->get_event().is_null())) {
-					continue; //not changed from default, don't save
-				}
+			Array original_events = sc->get_meta("original");
+			Array shortcut_events = sc->get_events();
+
+			bool is_same = Shortcut::is_event_array_equal(original_events, shortcut_events);
+			if (is_same) {
+				continue; // Not changed from default; don't save.
 			}
 
-			arr.push_back(E.key);
-			arr.push_back(sc->get_event());
+			Dictionary dict;
+			dict["name"] = shortcut_definition.key;
+			dict["shortcuts"] = shortcut_events;
+
+			save_array.push_back(dict);
 		}
-		r_ret = arr;
+		r_ret = save_array;
 		return true;
 	} else if (p_name == "builtin_action_overrides") {
 		Array actions_arr;
-		for (const KeyValue<String, List<Ref<InputEvent>>> &E : builtin_action_overrides) {
-			List<Ref<InputEvent>> events = E.value;
+		for (const KeyValue<String, List<Ref<InputEvent>>> &action_override : builtin_action_overrides) {
+			List<Ref<InputEvent>> events = action_override.value;
 
-			// TODO: skip actions which are the same as the builtin.
 			Dictionary action_dict;
-			action_dict["name"] = E.key;
+			action_dict["name"] = action_override.key;
 
+			// Convert the list to an array, and only keep key events as this is for the editor.
 			Array events_arr;
-			for (List<Ref<InputEvent>>::Element *I = events.front(); I; I = I->next()) {
-				events_arr.push_back(I->get());
+			for (const Ref<InputEvent> &ie : events) {
+				Ref<InputEventKey> iek = ie;
+				if (iek.is_valid()) {
+					events_arr.append(iek);
+				}
 			}
 
-			action_dict["events"] = events_arr;
+			Array defaults_arr;
+			List<Ref<InputEvent>> defaults = InputMap::get_singleton()->get_builtins()[action_override.key];
+			for (const Ref<InputEvent> &default_input_event : defaults) {
+				if (default_input_event.is_valid()) {
+					defaults_arr.append(default_input_event);
+				}
+			}
+
+			bool same = Shortcut::is_event_array_equal(events_arr, defaults_arr);
 
+			// Don't save if same as default.
+			if (same) {
+				continue;
+			}
+
+			action_dict["events"] = events_arr;
 			actions_arr.push_back(action_dict);
 		}
 
@@ -1405,7 +1429,7 @@ Ref<Shortcut> EditorSettings::get_shortcut(const String &p_name) const {
 	const Map<String, List<Ref<InputEvent>>>::Element *builtin_override = builtin_action_overrides.find(p_name);
 	if (builtin_override) {
 		sc.instantiate();
-		sc->set_event(builtin_override->get().front()->get());
+		sc->set_events_list(&builtin_override->get());
 		sc->set_name(InputMap::get_singleton()->get_builtin_display_name(p_name));
 	}
 
@@ -1414,7 +1438,7 @@ Ref<Shortcut> EditorSettings::get_shortcut(const String &p_name) const {
 		const OrderedHashMap<String, List<Ref<InputEvent>>>::ConstElement builtin_default = InputMap::get_singleton()->get_builtins_with_feature_overrides_applied().find(p_name);
 		if (builtin_default) {
 			sc.instantiate();
-			sc->set_event(builtin_default.get().front()->get());
+			sc->set_events_list(&builtin_default.get());
 			sc->set_name(InputMap::get_singleton()->get_builtin_display_name(p_name));
 		}
 	}
@@ -1450,52 +1474,91 @@ void ED_SHORTCUT_OVERRIDE(const String &p_path, const String &p_feature, Key p_k
 	Ref<Shortcut> sc = EditorSettings::get_singleton()->get_shortcut(p_path);
 	ERR_FAIL_COND_MSG(!sc.is_valid(), "Used ED_SHORTCUT_OVERRIDE with invalid shortcut: " + p_path + ".");
 
+	PackedInt32Array arr;
+	arr.push_back(p_keycode);
+
+	ED_SHORTCUT_OVERRIDE_ARRAY(p_path, p_feature, arr);
+}
+
+void ED_SHORTCUT_OVERRIDE_ARRAY(const String &p_path, const String &p_feature, const PackedInt32Array &p_keycodes) {
+	Ref<Shortcut> sc = EditorSettings::get_singleton()->get_shortcut(p_path);
+	ERR_FAIL_COND_MSG(!sc.is_valid(), "Used ED_SHORTCUT_OVERRIDE_ARRAY with invalid shortcut: " + p_path + ".");
+
 	// Only add the override if the OS supports the provided feature.
-	if (OS::get_singleton()->has_feature(p_feature)) {
-		Ref<InputEventKey> ie;
-		if (p_keycode) {
-			ie = InputEventKey::create_reference(p_keycode);
+	if (!OS::get_singleton()->has_feature(p_feature)) {
+		return;
+	}
+
+	Array events;
+
+	for (int i = 0; i < p_keycodes.size(); i++) {
+		Key keycode = (Key)p_keycodes[i];
+
+#ifdef OSX_ENABLED
+		// Use Cmd+Backspace as a general replacement for Delete shortcuts on macOS
+		if (keycode == KEY_DELETE) {
+			keycode = KEY_MASK_CMD | KEY_BACKSPACE;
 		}
+#endif
 
-		// Directly override the existing shortcut.
-		sc->set_event(ie);
-		sc->set_meta("original", ie);
+		Ref<InputEventKey> ie;
+		if (keycode) {
+			ie = InputEventKey::create_reference(keycode);
+			events.push_back(ie);
+		}
 	}
+
+	// Directly override the existing shortcut.
+	sc->set_events(events);
+	sc->set_meta("original", events);
 }
 
 Ref<Shortcut> ED_SHORTCUT(const String &p_path, const String &p_name, Key p_keycode) {
+	PackedInt32Array arr;
+	arr.push_back(p_keycode);
+	return ED_SHORTCUT_ARRAY(p_path, p_name, arr);
+}
+
+Ref<Shortcut> ED_SHORTCUT_ARRAY(const String &p_path, const String &p_name, const PackedInt32Array &p_keycodes) {
+	Array events;
+
+	for (int i = 0; i < p_keycodes.size(); i++) {
+		Key keycode = (Key)p_keycodes[i];
+
 #ifdef OSX_ENABLED
-	// Use Cmd+Backspace as a general replacement for Delete shortcuts on macOS
-	if (p_keycode == KEY_DELETE) {
-		p_keycode = KEY_MASK_CMD | KEY_BACKSPACE;
-	}
+		// Use Cmd+Backspace as a general replacement for Delete shortcuts on macOS
+		if (keycode == KEY_DELETE) {
+			keycode = KEY_MASK_CMD | KEY_BACKSPACE;
+		}
 #endif
 
-	Ref<InputEventKey> ie;
-	if (p_keycode) {
-		ie = InputEventKey::create_reference(p_keycode);
+		Ref<InputEventKey> ie;
+		if (keycode) {
+			ie = InputEventKey::create_reference(keycode);
+			events.push_back(ie);
+		}
 	}
 
 	if (!EditorSettings::get_singleton()) {
 		Ref<Shortcut> sc;
 		sc.instantiate();
 		sc->set_name(p_name);
-		sc->set_event(ie);
-		sc->set_meta("original", ie);
+		sc->set_events(events);
+		sc->set_meta("original", events);
 		return sc;
 	}
 
 	Ref<Shortcut> sc = EditorSettings::get_singleton()->get_shortcut(p_path);
 	if (sc.is_valid()) {
 		sc->set_name(p_name); //keep name (the ones that come from disk have no name)
-		sc->set_meta("original", ie); //to compare against changes
+		sc->set_meta("original", events); //to compare against changes
 		return sc;
 	}
 
 	sc.instantiate();
 	sc->set_name(p_name);
-	sc->set_event(ie);
-	sc->set_meta("original", ie); //to compare against changes
+	sc->set_events(events);
+	sc->set_meta("original", events); //to compare against changes
 	EditorSettings::get_singleton()->add_shortcut(p_path, sc);
 
 	return sc;
@@ -1550,7 +1613,7 @@ void EditorSettings::set_builtin_action_override(const String &p_name, const Arr
 
 	// Update the shortcut (if it is used somewhere in the editor) to be the first event of the new list.
 	if (shortcuts.has(p_name)) {
-		shortcuts[p_name]->set_event(event_list.front()->get());
+		shortcuts[p_name]->set_events_list(&event_list);
 	}
 }
 

+ 2 - 0
editor/editor_settings.h

@@ -201,7 +201,9 @@ Variant _EDITOR_GET(const String &p_setting);
 
 #define ED_IS_SHORTCUT(p_name, p_ev) (EditorSettings::get_singleton()->is_shortcut(p_name, p_ev))
 Ref<Shortcut> ED_SHORTCUT(const String &p_path, const String &p_name, Key p_keycode = KEY_NONE);
+Ref<Shortcut> ED_SHORTCUT_ARRAY(const String &p_path, const String &p_name, const PackedInt32Array &p_keycodes);
 void ED_SHORTCUT_OVERRIDE(const String &p_path, const String &p_feature, Key p_keycode = KEY_NONE);
+void ED_SHORTCUT_OVERRIDE_ARRAY(const String &p_path, const String &p_feature, const PackedInt32Array &p_keycodes);
 Ref<Shortcut> ED_GET_SHORTCUT(const String &p_path);
 
 #endif // EDITOR_SETTINGS_H

+ 8 - 2
editor/plugins/node_3d_editor_plugin.cpp

@@ -2483,8 +2483,14 @@ static bool is_shortcut_pressed(const String &p_path) {
 	if (shortcut.is_null()) {
 		return false;
 	}
-	InputEventKey *k = Object::cast_to<InputEventKey>(shortcut->get_event().ptr());
-	if (k == nullptr) {
+
+	const Array shortcuts = shortcut->get_events();
+	Ref<InputEventKey> k;
+	if (shortcuts.size() > 0) {
+		k = shortcuts.front();
+	}
+
+	if (k.is_null()) {
 		return false;
 	}
 	const Input &input = *Input::get_singleton();

+ 253 - 185
editor/settings_config_dialog.cpp

@@ -193,35 +193,25 @@ void EditorSettingsDialog::_event_config_confirmed() {
 		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;
-		}
+	if (current_event_index == -1) {
+		// Add new event
+		current_events.push_back(k);
+	} else {
+		// Edit existing event
+		current_events[current_event_index] = k;
+	}
 
-		_update_builtin_action(current_action, current_action_events);
+	if (is_editing_action) {
+		_update_builtin_action(current_edited_identifier, current_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_event", k);
-		undo_redo->add_undo_method(current_sc.ptr(), "set_event", current_sc->get_event());
-		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();
+		_update_shortcut_events(current_edited_identifier, current_events);
 	}
 }
 
 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);
+	Array old_input_array = EditorSettings::get_singleton()->get_builtin_action_overrides(p_name);
 
-	undo_redo->create_action(TTR("Edit Built-in Action"));
+	undo_redo->create_action(TTR("Edit Built-in Action") + " '" + p_name + "'");
 	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");
@@ -231,15 +221,125 @@ void EditorSettingsDialog::_update_builtin_action(const String &p_name, const Ar
 	_update_shortcuts();
 }
 
+void EditorSettingsDialog::_update_shortcut_events(const String &p_path, const Array &p_events) {
+	Ref<Shortcut> current_sc = EditorSettings::get_singleton()->get_shortcut(p_path);
+
+	undo_redo->create_action(TTR("Edit Shortcut") + " '" + p_path + "'");
+	undo_redo->add_do_method(current_sc.ptr(), "set_events", p_events);
+	undo_redo->add_undo_method(current_sc.ptr(), "set_events", current_sc->get_events());
+	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();
+}
+
+Array EditorSettingsDialog::_event_list_to_array_helper(List<Ref<InputEvent>> &p_events) {
+	Array events;
+
+	// Convert the list to an array, and only keep key events as this is for the editor.
+	for (List<Ref<InputEvent>>::Element *E = p_events.front(); E; E = E->next()) {
+		Ref<InputEventKey> k = E->get();
+		if (k.is_valid()) {
+			events.append(E->get());
+		}
+	}
+
+	return events;
+}
+
+void EditorSettingsDialog::_create_shortcut_treeitem(TreeItem *p_parent, const String &p_shortcut_identifier, const String &p_display, Array &p_events, bool p_allow_revert, bool p_is_action, bool p_is_collapsed) {
+	TreeItem *shortcut_item = shortcuts->create_item(p_parent);
+	shortcut_item->set_collapsed(p_is_collapsed);
+	shortcut_item->set_text(0, p_display);
+
+	Ref<InputEvent> primary = p_events.size() > 0 ? Ref<InputEvent>(p_events[0]) : Ref<InputEvent>();
+	Ref<InputEvent> secondary = p_events.size() > 1 ? Ref<InputEvent>(p_events[1]) : Ref<InputEvent>();
+
+	String sc_text = "None";
+	if (primary.is_valid()) {
+		sc_text = primary->as_text();
+
+		if (secondary.is_valid()) {
+			sc_text += ", " + secondary->as_text();
+
+			if (p_events.size() > 2) {
+				sc_text += " (+" + itos(p_events.size() - 2) + ")";
+			}
+		}
+	}
+
+	shortcut_item->set_text(1, sc_text);
+	if (sc_text == "None") {
+		// Fade out unassigned shortcut labels for easier visual grepping.
+		shortcut_item->set_custom_color(1, shortcuts->get_theme_color("font_color", "Label") * Color(1, 1, 1, 0.5));
+	}
+
+	if (p_allow_revert) {
+		shortcut_item->add_button(1, shortcuts->get_theme_icon("Reload", "EditorIcons"), SHORTCUT_REVERT);
+	}
+
+	shortcut_item->add_button(1, shortcuts->get_theme_icon("Add", "EditorIcons"), SHORTCUT_ADD);
+	shortcut_item->add_button(1, shortcuts->get_theme_icon("Close", "EditorIcons"), SHORTCUT_ERASE);
+
+	shortcut_item->set_meta("is_action", p_is_action);
+	shortcut_item->set_meta("type", "shortcut");
+	shortcut_item->set_meta("shortcut_identifier", p_shortcut_identifier);
+	shortcut_item->set_meta("events", p_events);
+
+	// Shortcut Input Events
+	for (int i = 0; i < p_events.size(); i++) {
+		Ref<InputEvent> ie = p_events[i];
+		if (ie.is_null()) {
+			continue;
+		}
+
+		TreeItem *event_item = shortcuts->create_item(shortcut_item);
+
+		event_item->set_text(0, shortcut_item->get_child_count() == 1 ? "Primary" : "");
+		event_item->set_text(1, ie->as_text());
+
+		event_item->add_button(1, shortcuts->get_theme_icon("Edit", "EditorIcons"), SHORTCUT_EDIT);
+		event_item->add_button(1, shortcuts->get_theme_icon("Close", "EditorIcons"), SHORTCUT_ERASE);
+
+		event_item->set_custom_bg_color(0, shortcuts->get_theme_color("dark_color_3", "Editor"));
+		event_item->set_custom_bg_color(1, shortcuts->get_theme_color("dark_color_3", "Editor"));
+
+		event_item->set_meta("is_action", p_is_action);
+		event_item->set_meta("type", "event");
+		event_item->set_meta("event_index", i);
+	}
+}
+
 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_first_child()) {
-		for (TreeItem *item = shortcuts->get_root()->get_first_child(); item; item = item->get_next()) {
-			collapsed[item->get_text(0)] = item->is_collapsed();
+		TreeItem *ti = shortcuts->get_root()->get_first_child();
+		while (ti) {
+			// Not all items have valid or unique text in the first column - so if it has an identifier, use that, as it should be unique.
+			if (ti->get_first_child() && ti->has_meta("shortcut_identifier")) {
+				collapsed[ti->get_meta("shortcut_identifier")] = ti->is_collapsed();
+			} else {
+				collapsed[ti->get_text(0)] = ti->is_collapsed();
+			}
+
+			// Try go down tree
+			TreeItem *ti_next = ti->get_first_child();
+			// Try go across tree
+			if (!ti_next) {
+				ti_next = ti->get_next();
+			}
+			// Try go up tree, to next node
+			if (!ti_next) {
+				ti_next = ti->get_parent()->get_next();
+			}
+
+			ti = ti_next;
 		}
 	}
+
 	shortcuts->clear();
 
 	TreeItem *root = shortcuts->create_item();
@@ -247,7 +347,6 @@ void EditorSettingsDialog::_update_shortcuts() {
 
 	// 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"));
 	common_section->set_selectable(0, false);
@@ -262,7 +361,6 @@ void EditorSettingsDialog::_update_shortcuts() {
 	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();
-
 		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.
@@ -278,21 +376,6 @@ void EditorSettingsDialog::_update_shortcuts() {
 			}
 		}
 
-		bool same_as_defaults = key_default_events.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 && !key_default_events[count]->is_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);
 
@@ -300,25 +383,12 @@ void EditorSettingsDialog::_update_shortcuts() {
 			continue;
 		}
 
-		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(SNAME("Reload"), SNAME("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(SNAME("font_color"), SNAME("Label")) * Color(1, 1, 1, 0.5));
-		}
+		Array action_events = _event_list_to_array_helper(action.inputs);
+		Array default_events = _event_list_to_array_helper(all_default_events);
+		bool same_as_defaults = Shortcut::is_event_array_equal(default_events, action_events);
+		bool collapse = !collapsed.has(action_name) || (collapsed.has(action_name) && collapsed[action_name]);
 
-		item->add_button(1, shortcuts->get_theme_icon(SNAME("Edit"), SNAME("EditorIcons")), 0);
-		item->add_button(1, shortcuts->get_theme_icon(SNAME("Close"), SNAME("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);
+		_create_shortcut_treeitem(common_section, action_name, action_name, action_events, !same_as_defaults, true, collapse);
 	}
 
 	// Editor Shortcuts
@@ -332,11 +402,10 @@ void EditorSettingsDialog::_update_shortcuts() {
 			continue;
 		}
 
-		Ref<InputEvent> original = sc->get_meta("original");
-
-		String section_name = E.get_slice("/", 0);
+		// Shortcut Section
 
 		TreeItem *section;
+		String section_name = E.get_slice("/", 0);
 
 		if (sections.has(section_name)) {
 			section = sections[section_name];
@@ -357,28 +426,18 @@ void EditorSettingsDialog::_update_shortcuts() {
 			sections[section_name] = section;
 		}
 
-		// Don't match unassigned shortcuts when searching for assigned keys in search results.
-		// This prevents all unassigned shortcuts from appearing when searching a string like "no".
-		if (shortcut_filter.is_subsequence_ofi(sc->get_name()) || (sc->get_as_text() != "None" && shortcut_filter.is_subsequence_ofi(sc->get_as_text()))) {
-			TreeItem *item = shortcuts->create_item(section);
-
-			item->set_text(0, sc->get_name());
-			item->set_text(1, sc->get_as_text());
+		// Shortcut Item
 
-			if (!sc->matches_event(original) && !(sc->get_event().is_null() && original.is_null())) {
-				item->add_button(1, shortcuts->get_theme_icon(SNAME("Reload"), SNAME("EditorIcons")), 2);
-			}
+		if (!shortcut_filter.is_subsequence_ofi(sc->get_name())) {
+			continue;
+		}
 
-			if (sc->get_as_text() == "None") {
-				// Fade out unassigned shortcut labels for easier visual grepping.
-				item->set_custom_color(1, shortcuts->get_theme_color(SNAME("font_color"), SNAME("Label")) * Color(1, 1, 1, 0.5));
-			}
+		Array original = sc->get_meta("original");
+		Array shortcuts_array = sc->get_events();
+		bool same_as_defaults = Shortcut::is_event_array_equal(original, shortcuts_array);
+		bool collapse = !collapsed.has(E) || (collapsed.has(E) && collapsed[E]);
 
-			item->add_button(1, shortcuts->get_theme_icon(SNAME("Edit"), SNAME("EditorIcons")), 0);
-			item->add_button(1, shortcuts->get_theme_icon(SNAME("Close"), SNAME("EditorIcons")), 1);
-			item->set_tooltip(0, E);
-			item->set_metadata(0, E);
-		}
+		_create_shortcut_treeitem(section, E, sc->get_name(), shortcuts_array, !same_as_defaults, false, collapse);
 	}
 
 	// remove sections with no shortcuts
@@ -392,123 +451,130 @@ void EditorSettingsDialog::_update_shortcuts() {
 
 void EditorSettingsDialog::_shortcut_button_pressed(Object *p_item, int p_column, int p_idx) {
 	TreeItem *ti = Object::cast_to<TreeItem>(p_item);
-	ERR_FAIL_COND(!ti);
-
-	button_idx = p_idx;
+	ERR_FAIL_COND_MSG(!ti, "Object passed is not a TreeItem");
 
-	if (ti->get_metadata(0) == "Common") {
-		// Editing a Built-in action, which can have multiple bindings.
-		editing_action = true;
-		current_action = ti->get_text(0);
+	ShortcutButton button_idx = (ShortcutButton)p_idx;
 
-		switch (button_idx) {
-			case SHORTCUT_REVERT: {
-				Array events;
-				List<Ref<InputEvent>> defaults = InputMap::get_singleton()->get_builtins_with_feature_overrides_applied()[current_action];
+	is_editing_action = ti->get_meta("is_action");
 
-				// Convert the list to an array, and only keep key events as this is for the editor.
-				for (const Ref<InputEvent> &k : defaults) {
-					if (k.is_valid()) {
-						events.append(k);
-					}
-				}
+	String type = ti->get_meta("type");
 
-				_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(SNAME("Add"), SNAME("EditorIcons")), TTR("Add"));
-				}
+	if (type == "event") {
+		current_edited_identifier = ti->get_parent()->get_meta("shortcut_identifier");
+		current_events = ti->get_parent()->get_meta("events");
+		current_event_index = ti->get_meta("event_index");
+	} else { // Type is "shortcut"
+		current_edited_identifier = ti->get_meta("shortcut_identifier");
+		current_events = ti->get_meta("events");
+		current_event_index = -1;
+	}
 
-				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;
-		}
-	} 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_event());
-				shortcut_being_edited = item;
-				break;
-			case EditorSettingsDialog::SHORTCUT_ERASE: {
-				if (!sc.is_valid()) {
-					return; //pointless, there is nothing
+	switch (button_idx) {
+		case EditorSettingsDialog::SHORTCUT_ADD: {
+			// Only for "shortcut" types
+			shortcut_editor->popup_and_configure();
+		} break;
+		case EditorSettingsDialog::SHORTCUT_EDIT: {
+			// Only for "event" types
+			shortcut_editor->popup_and_configure(current_events[current_event_index]);
+		} break;
+		case EditorSettingsDialog::SHORTCUT_ERASE: {
+			if (type == "shortcut") {
+				if (is_editing_action) {
+					_update_builtin_action(current_edited_identifier, Array());
+				} else {
+					_update_shortcut_events(current_edited_identifier, Array());
 				}
+			} else if (type == "event") {
+				current_events.remove(current_event_index);
 
-				undo_redo->create_action(TTR("Erase Shortcut"));
-				undo_redo->add_do_method(sc.ptr(), "set_event", Ref<InputEvent>());
-				undo_redo->add_undo_method(sc.ptr(), "set_event", sc->get_event());
-				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
+				if (is_editing_action) {
+					_update_builtin_action(current_edited_identifier, current_events);
+				} else {
+					_update_shortcut_events(current_edited_identifier, current_events);
 				}
+			}
+		} break;
+		case EditorSettingsDialog::SHORTCUT_REVERT: {
+			// Only for "shortcut" types
+			if (is_editing_action) {
+				List<Ref<InputEvent>> defaults = InputMap::get_singleton()->get_builtins_with_feature_overrides_applied()[current_edited_identifier];
+				Array events = _event_list_to_array_helper(defaults);
 
-				Ref<InputEvent> original = sc->get_meta("original");
-
-				undo_redo->create_action(TTR("Restore Shortcut"));
-				undo_redo->add_do_method(sc.ptr(), "set_event", original);
-				undo_redo->add_undo_method(sc.ptr(), "set_event", sc->get_event());
-				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;
-		}
-	}
-}
-
-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();
+				_update_builtin_action(current_edited_identifier, events);
 			} else {
-				// Configure existing
-				current_action_event_index = p_index;
-				shortcut_editor->popup_and_configure(action_popup->get_item_metadata(p_index));
+				Ref<Shortcut> sc = EditorSettings::get_singleton()->get_shortcut(current_edited_identifier);
+				Array original = sc->get_meta("original");
+				_update_shortcut_events(current_edited_identifier, original);
 			}
 		} break;
-		case SHORTCUT_ERASE: {
-			current_action_events.remove(p_index);
-			_update_builtin_action(current_action, current_action_events);
-		} break;
 		default:
 			break;
 	}
 }
 
+Variant EditorSettingsDialog::get_drag_data_fw(const Point2 &p_point, Control *p_from) {
+	TreeItem *selected = shortcuts->get_selected();
+
+	// Only allow drag for events
+	if (!selected || !selected->has_meta("type") || selected->get_meta("type") != "event") {
+		return Variant();
+	}
+
+	String label_text = "Event " + itos(selected->get_meta("event_index"));
+	Label *label = memnew(Label(label_text));
+	label->set_modulate(Color(1, 1, 1, 1.0f));
+	shortcuts->set_drag_preview(label);
+
+	shortcuts->set_drop_mode_flags(Tree::DROP_MODE_INBETWEEN);
+
+	return Dictionary(); // No data required
+}
+
+bool EditorSettingsDialog::can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const {
+	TreeItem *selected = shortcuts->get_selected();
+	TreeItem *item = shortcuts->get_item_at_position(p_point);
+	if (!selected || !item || item == selected || !item->has_meta("type") || item->get_meta("type") != "event") {
+		return false;
+	}
+
+	// Don't allow moving an events in-between shortcuts.
+	if (selected->get_parent()->get_meta("shortcut_identifier") != item->get_parent()->get_meta("shortcut_identifier")) {
+		return false;
+	}
+
+	return true;
+}
+
+void EditorSettingsDialog::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 = shortcuts->get_selected();
+	TreeItem *target = shortcuts->get_item_at_position(p_point);
+
+	if (!target) {
+		return;
+	}
+
+	int target_event_index = target->get_meta("event_index");
+	int index_moving_from = selected->get_meta("event_index");
+
+	Array events = selected->get_parent()->get_meta("events");
+
+	Variant event_moved = events[index_moving_from];
+	events.remove(index_moving_from);
+	events.insert(target_event_index, event_moved);
+
+	String ident = selected->get_parent()->get_meta("shortcut_identifier");
+	if (selected->get_meta("is_action")) {
+		_update_builtin_action(ident, events);
+	} else {
+		_update_shortcut_events(ident, events);
+	}
+}
+
 void EditorSettingsDialog::_tabs_tab_changed(int p_tab) {
 	_focus_current_search_box();
 }
@@ -544,13 +610,13 @@ void EditorSettingsDialog::_editor_restart_close() {
 void EditorSettingsDialog::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("_update_shortcuts"), &EditorSettingsDialog::_update_shortcuts);
 	ClassDB::bind_method(D_METHOD("_settings_changed"), &EditorSettingsDialog::_settings_changed);
+
+	ClassDB::bind_method(D_METHOD("_get_drag_data_fw"), &EditorSettingsDialog::get_drag_data_fw);
+	ClassDB::bind_method(D_METHOD("_can_drop_data_fw"), &EditorSettingsDialog::can_drop_data_fw);
+	ClassDB::bind_method(D_METHOD("_drop_data_fw"), &EditorSettingsDialog::drop_data_fw);
 }
 
 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);
@@ -628,6 +694,8 @@ EditorSettingsDialog::EditorSettingsDialog() {
 	shortcuts->connect("button_pressed", callable_mp(this, &EditorSettingsDialog::_shortcut_button_pressed));
 	tab_shortcuts->add_child(shortcuts);
 
+	shortcuts->set_drag_forwarding(this);
+
 	// Adding event dialog
 	shortcut_editor = memnew(InputEventConfigurationDialog);
 	shortcut_editor->connect("confirmed", callable_mp(this, &EditorSettingsDialog::_event_config_confirmed));

+ 18 - 12
editor/settings_config_dialog.h

@@ -53,29 +53,28 @@ class EditorSettingsDialog : public AcceptDialog {
 	LineEdit *shortcut_search_box;
 	SectionedInspector *inspector;
 
+	// Shortcuts
 	enum ShortcutButton {
+		SHORTCUT_ADD,
 		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;
+	Tree *shortcuts;
+	String shortcut_filter;
+
+	InputEventConfigurationDialog *shortcut_editor;
+
+	bool is_editing_action = false;
+	String current_edited_identifier;
+	Array current_events;
+	int current_event_index = -1;
 
 	Timer *timer;
 
 	UndoRedo *undo_redo;
 
-	// Shortcuts
-	String shortcut_filter;
-	Tree *shortcuts;
-	InputEventConfigurationDialog *shortcut_editor;
-	String shortcut_being_edited;
-
 	virtual void cancel_pressed() override;
 	virtual void ok_pressed() override;
 
@@ -89,7 +88,14 @@ class EditorSettingsDialog : public AcceptDialog {
 
 	void _event_config_confirmed();
 
+	void _create_shortcut_treeitem(TreeItem *p_parent, const String &p_shortcut_identifier, const String &p_display, Array &p_events, bool p_allow_revert, bool p_is_common, bool p_is_collapsed);
+	Array _event_list_to_array_helper(List<Ref<InputEvent>> &p_events);
 	void _update_builtin_action(const String &p_name, const Array &p_events);
+	void _update_shortcut_events(const String &p_path, const Array &p_events);
+
+	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);
 
 	void _tabs_tab_changed(int p_tab);
 	void _focus_current_search_box();