Pārlūkot izejas kodu

Merge pull request #112729 from KoBeWi/multi_group_yoink

Edit groups on multiple nodes
Thaddeus Crews 3 nedēļas atpakaļ
vecāks
revīzija
cb1569345a

+ 83 - 20
editor/docks/groups_editor.cpp

@@ -93,6 +93,41 @@ void GroupsEditor::_set_group_checked(const String &p_name, bool p_checked) {
 	ti->set_checked(0, p_checked);
 }
 
+void GroupsEditor::_add_to_group(const StringName &p_name, bool p_persist, const Array &p_nodes) {
+	for (const Variant &v : p_nodes) {
+		Node *node = Object::cast_to<Node>(v.get_validated_object());
+		if (node) {
+			node->add_to_group(p_name, p_persist);
+		}
+	}
+}
+
+void GroupsEditor::_remove_from_group(const StringName &p_name, const Array &p_nodes) {
+	for (const Variant &v : p_nodes) {
+		Node *node = Object::cast_to<Node>(v.get_validated_object());
+		if (node) {
+			node->remove_from_group(p_name);
+		}
+	}
+}
+
+void GroupsEditor::_get_group_mask(const StringName &p_name, Array &r_nodes, bool p_invert) {
+	for (Node *p_node : selection) {
+		if (p_invert != p_node->is_in_group(p_name)) {
+			r_nodes.push_back(p_node);
+		}
+	}
+}
+
+bool GroupsEditor::_can_edit(const StringName &p_group) {
+	for (Node *p_node : selection) {
+		if (!can_edit(p_node, p_group)) {
+			return false;
+		}
+	}
+	return true;
+}
+
 bool GroupsEditor::_has_group(const String &p_name) {
 	return global_groups.has(p_name) || scene_groups.has(p_name);
 }
@@ -102,7 +137,7 @@ void GroupsEditor::_modify_group(Object *p_item, int p_column, int p_id, MouseBu
 		return;
 	}
 
-	if (!node) {
+	if (selection.is_empty()) {
 		return;
 	}
 
@@ -177,7 +212,7 @@ void GroupsEditor::_update_tree() {
 		return;
 	}
 
-	if (!node) {
+	if (selection.is_empty()) {
 		return;
 	}
 
@@ -190,7 +225,9 @@ void GroupsEditor::_update_tree() {
 	tree->clear();
 
 	List<Node::GroupInfo> groups;
-	node->get_groups(&groups);
+	for (Node *p_node : selection) {
+		p_node->get_groups(&groups);
+	}
 	groups.sort_custom<_GroupInfoComparator>();
 
 	List<StringName> current_groups;
@@ -220,7 +257,7 @@ void GroupsEditor::_update_tree() {
 
 		TreeItem *item = tree->create_item(local_root);
 		item->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);
-		item->set_editable(0, can_edit(node, E));
+		item->set_editable(0, _can_edit(E));
 		item->set_checked(0, current_groups.find(E) != nullptr);
 		item->set_text(0, E);
 		item->set_meta("__local", true);
@@ -252,7 +289,7 @@ void GroupsEditor::_update_tree() {
 
 		TreeItem *item = tree->create_item(global_root);
 		item->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);
-		item->set_editable(0, can_edit(node, E));
+		item->set_editable(0, _can_edit(E));
 		item->set_checked(0, current_groups.find(E) != nullptr);
 		item->set_text(0, E);
 		item->set_meta("__local", false);
@@ -307,15 +344,18 @@ void GroupsEditor::_cache_scene_groups(const ObjectID &p_id) {
 	}
 }
 
-void GroupsEditor::set_current(Node *p_node) {
-	if (node == p_node) {
+void GroupsEditor::set_selection(const Vector<Node *> &p_nodes) {
+	if (p_nodes.is_empty()) {
+		holder->hide();
+		select_a_node->show();
+		selection.clear();
 		return;
 	}
-	node = p_node;
 
-	if (!node) {
-		return;
-	}
+	selection = p_nodes;
+
+	holder->show();
+	select_a_node->hide();
 
 	if (scene_tree->get_edited_scene_root() != scene_root_node) {
 		scene_root_node = scene_tree->get_edited_scene_root();
@@ -338,8 +378,10 @@ void GroupsEditor::_item_edited() {
 	if (ti->is_checked(0)) {
 		undo_redo->create_action(TTR("Add to Group"));
 
-		undo_redo->add_do_method(node, "add_to_group", name, true);
-		undo_redo->add_undo_method(node, "remove_from_group", name);
+		Array nodes;
+		_get_group_mask(name, nodes, true);
+		undo_redo->add_do_method(this, "_add_to_group", name, true, nodes);
+		undo_redo->add_undo_method(this, "_remove_from_group", name, nodes);
 
 		undo_redo->add_do_method(this, "_set_group_checked", name, true);
 		undo_redo->add_undo_method(this, "_set_group_checked", name, false);
@@ -353,8 +395,10 @@ void GroupsEditor::_item_edited() {
 	} else {
 		undo_redo->create_action(TTR("Remove from Group"));
 
-		undo_redo->add_do_method(node, "remove_from_group", name);
-		undo_redo->add_undo_method(node, "add_to_group", name, true);
+		Array nodes;
+		_get_group_mask(name, nodes, false);
+		undo_redo->add_do_method(this, "_remove_from_group", name, nodes);
+		undo_redo->add_undo_method(this, "_add_to_group", name, true, nodes);
 
 		undo_redo->add_do_method(this, "_set_group_checked", name, false);
 		undo_redo->add_undo_method(this, "_set_group_checked", name, true);
@@ -489,8 +533,10 @@ void GroupsEditor::_confirm_add() {
 	EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
 	undo_redo->create_action(TTR("Add to Group"));
 
-	undo_redo->add_do_method(node, "add_to_group", name, true);
-	undo_redo->add_undo_method(node, "remove_from_group", name);
+	Array nodes;
+	_get_group_mask(name, nodes, true);
+	undo_redo->add_do_method(this, "_add_to_group", name, true, nodes);
+	undo_redo->add_undo_method(this, "_remove_from_group", name, nodes);
 
 	bool is_local = !global_group_button->is_pressed();
 	if (is_local) {
@@ -819,6 +865,9 @@ void GroupsEditor::_bind_methods() {
 	ClassDB::bind_method("_rename_scene_group", &GroupsEditor::_rename_scene_group);
 	ClassDB::bind_method("_remove_scene_group", &GroupsEditor::_remove_scene_group);
 	ClassDB::bind_method("_set_group_checked", &GroupsEditor::_set_group_checked);
+
+	ClassDB::bind_method("_add_to_group", &GroupsEditor::_add_to_group);
+	ClassDB::bind_method("_remove_from_group", &GroupsEditor::_remove_from_group);
 }
 
 void GroupsEditor::_node_removed(Node *p_node) {
@@ -834,15 +883,19 @@ void GroupsEditor::_node_removed(Node *p_node) {
 }
 
 GroupsEditor::GroupsEditor() {
-	node = nullptr;
 	scene_tree = SceneTree::get_singleton();
 
 	ED_SHORTCUT("groups_editor/delete", TTRC("Delete"), Key::KEY_DELETE);
 	ED_SHORTCUT("groups_editor/rename", TTRC("Rename"), Key::F2);
 	ED_SHORTCUT_OVERRIDE("groups_editor/rename", "macos", Key::ENTER);
 
+	holder = memnew(VBoxContainer);
+	holder->set_v_size_flags(SIZE_EXPAND_FILL);
+	holder->hide();
+	add_child(holder);
+
 	HBoxContainer *hbc = memnew(HBoxContainer);
-	add_child(hbc);
+	holder->add_child(hbc);
 
 	add = memnew(Button);
 	add->set_theme_type_variation("FlatMenuButton");
@@ -867,11 +920,21 @@ GroupsEditor::GroupsEditor() {
 	tree->connect("button_clicked", callable_mp(this, &GroupsEditor::_modify_group));
 	tree->connect("item_mouse_selected", callable_mp(this, &GroupsEditor::_item_mouse_selected));
 	tree->connect(SceneStringName(gui_input), callable_mp(this, &GroupsEditor::_groups_gui_input));
-	add_child(tree);
+	holder->add_child(tree);
 
 	menu = memnew(PopupMenu);
 	menu->connect(SceneStringName(id_pressed), callable_mp(this, &GroupsEditor::_menu_id_pressed));
 	tree->add_child(menu);
 
+	select_a_node = memnew(Label);
+	select_a_node->set_focus_mode(FOCUS_ACCESSIBILITY);
+	select_a_node->set_text(TTRC("Select one or more nodes to edit their groups."));
+	select_a_node->set_custom_minimum_size(Size2(100 * EDSCALE, 0));
+	select_a_node->set_v_size_flags(SIZE_EXPAND_FILL);
+	select_a_node->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER);
+	select_a_node->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
+	select_a_node->set_autowrap_mode(TextServer::AUTOWRAP_WORD_SMART);
+	add_child(select_a_node);
+
 	ProjectSettingsEditor::get_singleton()->get_group_settings()->connect("group_changed", callable_mp(this, &GroupsEditor::_update_groups_and_tree));
 }

+ 9 - 2
editor/docks/groups_editor.h

@@ -52,7 +52,7 @@ class GroupsEditor : public VBoxContainer {
 	bool groups_dirty = false;
 	bool update_groups_and_tree_queued = false;
 
-	Node *node = nullptr;
+	LocalVector<Node *> selection;
 	Node *scene_root_node = nullptr;
 	SceneTree *scene_tree = nullptr;
 
@@ -73,9 +73,11 @@ class GroupsEditor : public VBoxContainer {
 
 	PopupMenu *menu = nullptr;
 
+	VBoxContainer *holder = nullptr;
 	LineEdit *filter = nullptr;
 	Button *add = nullptr;
 	Tree *tree = nullptr;
+	Label *select_a_node = nullptr;
 
 	HashMap<ObjectID, HashMap<StringName, bool>> scene_groups_cache;
 	HashMap<StringName, bool> scene_groups_for_caching;
@@ -122,6 +124,11 @@ class GroupsEditor : public VBoxContainer {
 
 	void _node_removed(Node *p_node);
 
+	void _add_to_group(const StringName &p_name, bool p_persist, const Array &p_nodes);
+	void _remove_from_group(const StringName &p_name, const Array &p_nodes);
+	void _get_group_mask(const StringName &p_name, Array &r_nodes, bool p_invert);
+	bool _can_edit(const StringName &p_group);
+
 protected:
 	void _notification(int p_what);
 	static void _bind_methods();
@@ -134,7 +141,7 @@ public:
 		CONVERT_GROUP,
 	};
 
-	void set_current(Node *p_node);
+	void set_selection(const Vector<Node *> &p_nodes);
 
 	GroupsEditor();
 };

+ 10 - 44
editor/docks/node_dock.cpp

@@ -55,15 +55,7 @@ void NodeDock::save_layout_to_config(Ref<ConfigFile> &p_layout, const String &p_
 
 void NodeDock::load_layout_from_config(const Ref<ConfigFile> &p_layout, const String &p_section) {
 	const int current_tab = p_layout->get_value(p_section, "current_tab", 0);
-	if (select_a_node->is_visible()) {
-		if (current_tab == 0) {
-			groups_button->set_pressed_no_signal(false);
-			connections_button->set_pressed_no_signal(true);
-		} else if (current_tab == 1) {
-			groups_button->set_pressed_no_signal(true);
-			connections_button->set_pressed_no_signal(false);
-		}
-	} else if (current_tab == 0) {
+	if (current_tab == 0) {
 		show_connections();
 	} else if (current_tab == 1) {
 		show_groups();
@@ -83,32 +75,17 @@ void NodeDock::update_lists() {
 	connections->update_tree();
 }
 
-void NodeDock::set_object(Object *p_object) {
-	connections->set_object(p_object);
-	groups->set_current(Object::cast_to<Node>(p_object));
-
-	if (p_object) {
-		if (connections_button->is_pressed()) {
-			connections->show();
-		} else {
-			groups->show();
-		}
+void NodeDock::set_selection(const Vector<Object *> &p_objects) {
+	connections->set_selection(p_objects);
 
-		if (Object::cast_to<Resource>(p_object)) {
-			show_connections();
-			groups_button->set_disabled(true);
-		} else {
-			groups_button->set_disabled(false);
+	Vector<Node *> nodes;
+	for (Object *obj : p_objects) {
+		Node *n = Object::cast_to<Node>(obj);
+		if (n) {
+			nodes.append(n);
 		}
-
-		mode_hb->show();
-		select_a_node->hide();
-	} else {
-		connections->hide();
-		groups->hide();
-		mode_hb->hide();
-		select_a_node->show();
 	}
+	groups->set_selection(nodes);
 }
 
 NodeDock::NodeDock() {
@@ -123,13 +100,11 @@ NodeDock::NodeDock() {
 
 	mode_hb = memnew(HBoxContainer);
 	main_vb->add_child(mode_hb);
-	mode_hb->hide();
 
 	connections_button = memnew(Button);
 	connections_button->set_theme_type_variation(SceneStringName(FlatButton));
 	connections_button->set_text(TTRC("Signals"));
 	connections_button->set_toggle_mode(true);
-	connections_button->set_pressed(true);
 	connections_button->set_h_size_flags(SIZE_EXPAND_FILL);
 	connections_button->set_clip_text(true);
 	mode_hb->add_child(connections_button);
@@ -139,7 +114,6 @@ NodeDock::NodeDock() {
 	groups_button->set_theme_type_variation(SceneStringName(FlatButton));
 	groups_button->set_text(TTRC("Groups"));
 	groups_button->set_toggle_mode(true);
-	groups_button->set_pressed(false);
 	groups_button->set_h_size_flags(SIZE_EXPAND_FILL);
 	groups_button->set_clip_text(true);
 	mode_hb->add_child(groups_button);
@@ -155,15 +129,7 @@ NodeDock::NodeDock() {
 	groups->set_v_size_flags(SIZE_EXPAND_FILL);
 	groups->hide();
 
-	select_a_node = memnew(Label);
-	select_a_node->set_focus_mode(FOCUS_ACCESSIBILITY);
-	select_a_node->set_text(TTRC("Select a single node to edit its signals and groups, or select an independent resource to view its signals."));
-	select_a_node->set_custom_minimum_size(Size2(100 * EDSCALE, 0));
-	select_a_node->set_v_size_flags(SIZE_EXPAND_FILL);
-	select_a_node->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER);
-	select_a_node->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
-	select_a_node->set_autowrap_mode(TextServer::AUTOWRAP_WORD_SMART);
-	main_vb->add_child(select_a_node);
+	show_connections();
 }
 
 NodeDock::~NodeDock() {

+ 1 - 3
editor/docks/node_dock.h

@@ -47,8 +47,6 @@ class NodeDock : public EditorDock {
 
 	HBoxContainer *mode_hb = nullptr;
 
-	Label *select_a_node = nullptr;
-
 private:
 	inline static NodeDock *singleton = nullptr;
 
@@ -62,7 +60,7 @@ protected:
 	virtual void load_layout_from_config(const Ref<ConfigFile> &p_layout, const String &p_section) override;
 
 public:
-	void set_object(Object *p_object);
+	void set_selection(const Vector<Object *> &p_objects);
 
 	void show_groups();
 	void show_connections();

+ 2 - 2
editor/docks/scene_tree_dock.cpp

@@ -1351,7 +1351,7 @@ void SceneTreeDock::_tool_selected(int p_tool, bool p_confirm_override) {
 					undo_redo->add_undo_method(node, "set_scene_file_path", node->get_scene_file_path());
 					_node_replace_owner(node, node, root);
 					_node_strip_signal_inheritance(node);
-					NodeDock::get_singleton()->set_object(node); // Refresh.
+					NodeDock::get_singleton()->set_selection(Vector<Object *>{ node }); // Refresh.
 					undo_redo->add_do_method(scene_tree, "update_tree");
 					undo_redo->add_undo_method(scene_tree, "update_tree");
 					undo_redo->commit_action();
@@ -2855,7 +2855,7 @@ void SceneTreeDock::_delete_confirm(bool p_cut) {
 	editor_history->cleanup_history();
 	InspectorDock::get_singleton()->call("_prepare_history");
 	InspectorDock::get_singleton()->update(nullptr);
-	NodeDock::get_singleton()->set_object(nullptr);
+	NodeDock::get_singleton()->set_selection(Vector<Object *>{});
 }
 
 void SceneTreeDock::_update_script_button() {

+ 13 - 6
editor/editor_node.cpp

@@ -2888,7 +2888,7 @@ void EditorNode::push_node_item(Node *p_node) {
 void EditorNode::push_item(Object *p_object, const String &p_property, bool p_inspector_only) {
 	if (!p_object) {
 		InspectorDock::get_inspector_singleton()->edit(nullptr);
-		NodeDock::get_singleton()->set_object(nullptr);
+		NodeDock::get_singleton()->set_selection(Vector<Object *>());
 		SceneTreeDock::get_singleton()->set_selected(nullptr);
 		InspectorDock::get_singleton()->update(nullptr);
 		hide_unused_editors();
@@ -2999,7 +2999,7 @@ void EditorNode::_edit_current(bool p_skip_foreign, bool p_skip_inspector_update
 	if (!current_obj) {
 		SceneTreeDock::get_singleton()->set_selected(nullptr);
 		InspectorDock::get_inspector_singleton()->edit(nullptr);
-		NodeDock::get_singleton()->set_object(nullptr);
+		NodeDock::get_singleton()->set_selection(Vector<Object *>());
 		InspectorDock::get_singleton()->update(nullptr);
 		EditorDebuggerNode::get_singleton()->clear_remote_tree_selection();
 		hide_unused_editors();
@@ -3033,7 +3033,7 @@ void EditorNode::_edit_current(bool p_skip_foreign, bool p_skip_inspector_update
 		if (!p_skip_inspector_update) {
 			InspectorDock::get_inspector_singleton()->edit(current_res);
 			SceneTreeDock::get_singleton()->set_selected(nullptr);
-			NodeDock::get_singleton()->set_object(current_res);
+			NodeDock::get_singleton()->set_selection(Vector<Object *>{ current_res });
 			InspectorDock::get_singleton()->update(nullptr);
 			EditorDebuggerNode::get_singleton()->clear_remote_tree_selection();
 			ImportDock::get_singleton()->set_edit_path(current_res->get_path());
@@ -3063,7 +3063,7 @@ void EditorNode::_edit_current(bool p_skip_foreign, bool p_skip_inspector_update
 
 		InspectorDock::get_inspector_singleton()->edit(current_node);
 		if (current_node->is_inside_tree()) {
-			NodeDock::get_singleton()->set_object(current_node);
+			NodeDock::get_singleton()->set_selection(Vector<Object *>{ current_node });
 			SceneTreeDock::get_singleton()->set_selected(current_node);
 			SceneTreeDock::get_singleton()->set_selection({ current_node });
 			InspectorDock::get_singleton()->update(current_node);
@@ -3075,7 +3075,7 @@ void EditorNode::_edit_current(bool p_skip_foreign, bool p_skip_inspector_update
 				}
 			}
 		} else {
-			NodeDock::get_singleton()->set_object(nullptr);
+			NodeDock::get_singleton()->set_selection(Vector<Object *>());
 			SceneTreeDock::get_singleton()->set_selected(nullptr);
 			InspectorDock::get_singleton()->update(nullptr);
 		}
@@ -3122,8 +3122,15 @@ void EditorNode::_edit_current(bool p_skip_foreign, bool p_skip_inspector_update
 			EditorDebuggerNode::get_singleton()->clear_remote_tree_selection();
 		}
 
+		// TODO: This can be replaced by some casting operator.
+		Vector<Object *> nodes_as_objects;
+		nodes_as_objects.reserve_exact(multi_nodes.size());
+		for (Node *n : multi_nodes) {
+			nodes_as_objects.append(n);
+		}
+
 		InspectorDock::get_inspector_singleton()->edit(current_obj);
-		NodeDock::get_singleton()->set_object(nullptr);
+		NodeDock::get_singleton()->set_selection(nodes_as_objects);
 		SceneTreeDock::get_singleton()->set_selected(selected_node);
 		SceneTreeDock::get_singleton()->set_selection(multi_nodes);
 		InspectorDock::get_singleton()->update(nullptr);

+ 32 - 11
editor/scene/connections_dialog.cpp

@@ -1503,8 +1503,16 @@ void ConnectionsDock::_bind_methods() {
 	ClassDB::bind_method("update_tree", &ConnectionsDock::update_tree);
 }
 
-void ConnectionsDock::set_object(Object *p_obj) {
-	selected_object = p_obj;
+void ConnectionsDock::set_selection(const Vector<Object *> &p_objects) {
+	if (p_objects.size() != 1) {
+		select_a_node->show();
+		holder->hide();
+		selected_object = nullptr;
+	} else {
+		select_a_node->hide();
+		holder->show();
+		selected_object = p_objects[0];
+	}
 	is_editing_resource = (Object::cast_to<Resource>(selected_object) != nullptr);
 	update_tree();
 }
@@ -1699,7 +1707,10 @@ void ConnectionsDock::update_tree() {
 ConnectionsDock::ConnectionsDock() {
 	set_name(TTR("Signals"));
 
-	VBoxContainer *vbc = this;
+	holder = memnew(VBoxContainer);
+	holder->set_v_size_flags(SIZE_EXPAND_FILL);
+	holder->hide();
+	add_child(holder);
 
 	search_box = memnew(LineEdit);
 	search_box->set_h_size_flags(Control::SIZE_EXPAND_FILL);
@@ -1707,7 +1718,7 @@ ConnectionsDock::ConnectionsDock() {
 	search_box->set_accessibility_name(TTRC("Filter Signals"));
 	search_box->set_clear_button_enabled(true);
 	search_box->connect(SceneStringName(text_changed), callable_mp(this, &ConnectionsDock::_filter_changed));
-	vbc->add_child(search_box);
+	holder->add_child(search_box);
 
 	tree = memnew(ConnectionsDockTree);
 	tree->set_accessibility_name(TTRC("Connections"));
@@ -1716,24 +1727,24 @@ ConnectionsDock::ConnectionsDock() {
 	tree->set_select_mode(Tree::SELECT_ROW);
 	tree->set_hide_root(true);
 	tree->set_column_clip_content(0, true);
-	vbc->add_child(tree);
+	holder->add_child(tree);
 	tree->set_v_size_flags(Control::SIZE_EXPAND_FILL);
 	tree->set_allow_rmb_select(true);
 
 	connect_button = memnew(Button);
 	connect_button->set_accessibility_name(TTRC("Connect"));
 	HBoxContainer *hb = memnew(HBoxContainer);
-	vbc->add_child(hb);
+	holder->add_child(hb);
 	hb->add_spacer();
 	hb->add_child(connect_button);
 	connect_button->connect(SceneStringName(pressed), callable_mp(this, &ConnectionsDock::_connect_pressed));
 
 	connect_dialog = memnew(ConnectDialog);
 	connect_dialog->set_process_shortcut_input(true);
-	add_child(connect_dialog);
+	holder->add_child(connect_dialog);
 
 	disconnect_all_dialog = memnew(ConfirmationDialog);
-	add_child(disconnect_all_dialog);
+	holder->add_child(disconnect_all_dialog);
 	disconnect_all_dialog->connect(SceneStringName(confirmed), callable_mp(this, &ConnectionsDock::_disconnect_all));
 	disconnect_all_dialog->set_text(TTR("Are you sure you want to remove all connections from this signal?"));
 
@@ -1741,7 +1752,7 @@ ConnectionsDock::ConnectionsDock() {
 	class_menu->connect(SceneStringName(id_pressed), callable_mp(this, &ConnectionsDock::_handle_class_menu_option));
 	class_menu->connect("about_to_popup", callable_mp(this, &ConnectionsDock::_class_menu_about_to_popup));
 	class_menu->add_item(TTR("Open Documentation"), CLASS_MENU_OPEN_DOCS);
-	add_child(class_menu);
+	holder->add_child(class_menu);
 
 	signal_menu = memnew(PopupMenu);
 	signal_menu->connect(SceneStringName(id_pressed), callable_mp(this, &ConnectionsDock::_handle_signal_menu_option));
@@ -1751,7 +1762,7 @@ ConnectionsDock::ConnectionsDock() {
 	signal_menu->add_item(TTR("Copy Name"), SIGNAL_MENU_COPY_NAME);
 	signal_menu->add_separator();
 	signal_menu->add_item(TTR("Open Documentation"), SIGNAL_MENU_OPEN_DOCS);
-	add_child(signal_menu);
+	holder->add_child(signal_menu);
 
 	slot_menu = memnew(PopupMenu);
 	slot_menu->connect(SceneStringName(id_pressed), callable_mp(this, &ConnectionsDock::_handle_slot_menu_option));
@@ -1759,7 +1770,7 @@ ConnectionsDock::ConnectionsDock() {
 	slot_menu->add_item(TTR("Edit..."), SLOT_MENU_EDIT);
 	slot_menu->add_item(TTR("Go to Method"), SLOT_MENU_GO_TO_METHOD);
 	slot_menu->add_shortcut(ED_SHORTCUT("connections_editor/disconnect", TTRC("Disconnect"), Key::KEY_DELETE), SLOT_MENU_DISCONNECT);
-	add_child(slot_menu);
+	holder->add_child(slot_menu);
 
 	connect_dialog->connect("connected", callable_mp(this, &ConnectionsDock::_make_or_edit_connection));
 	tree->connect(SceneStringName(item_selected), callable_mp(this, &ConnectionsDock::_tree_item_selected));
@@ -1767,4 +1778,14 @@ ConnectionsDock::ConnectionsDock() {
 	tree->connect(SceneStringName(gui_input), callable_mp(this, &ConnectionsDock::_tree_gui_input));
 
 	add_theme_constant_override("separation", 3 * EDSCALE);
+
+	select_a_node = memnew(Label);
+	select_a_node->set_focus_mode(FOCUS_ACCESSIBILITY);
+	select_a_node->set_text(TTRC("Select a single node or resource to edit its signals."));
+	select_a_node->set_custom_minimum_size(Size2(100 * EDSCALE, 0));
+	select_a_node->set_v_size_flags(SIZE_EXPAND_FILL);
+	select_a_node->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER);
+	select_a_node->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
+	select_a_node->set_autowrap_mode(TextServer::AUTOWRAP_WORD_SMART);
+	add_child(select_a_node);
 }

+ 4 - 1
editor/scene/connections_dialog.h

@@ -237,6 +237,9 @@ class ConnectionsDock : public VBoxContainer {
 		SLOT_MENU_DISCONNECT,
 	};
 
+	VBoxContainer *holder = nullptr;
+	Label *select_a_node = nullptr;
+
 	Object *selected_object = nullptr;
 	ConnectionsDockTree *tree = nullptr;
 
@@ -282,7 +285,7 @@ protected:
 	static void _bind_methods();
 
 public:
-	void set_object(Object *p_obj);
+	void set_selection(const Vector<Object *> &p_objects);
 	void update_tree();
 
 	ConnectionsDock();