1/**************************************************************************/
2/* connections_dialog.cpp */
3/**************************************************************************/
4/* This file is part of: */
5/* GODOT ENGINE */
6/* https://godotengine.org */
7/**************************************************************************/
8/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10/* */
11/* Permission is hereby granted, free of charge, to any person obtaining */
12/* a copy of this software and associated documentation files (the */
13/* "Software"), to deal in the Software without restriction, including */
14/* without limitation the rights to use, copy, modify, merge, publish, */
15/* distribute, sublicense, and/or sell copies of the Software, and to */
16/* permit persons to whom the Software is furnished to do so, subject to */
17/* the following conditions: */
18/* */
19/* The above copyright notice and this permission notice shall be */
20/* included in all copies or substantial portions of the Software. */
21/* */
22/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29/**************************************************************************/
30
31#include "connections_dialog.h"
32
33#include "core/config/project_settings.h"
34#include "core/templates/hash_set.h"
35#include "editor/doc_tools.h"
36#include "editor/editor_help.h"
37#include "editor/editor_inspector.h"
38#include "editor/editor_node.h"
39#include "editor/editor_scale.h"
40#include "editor/editor_settings.h"
41#include "editor/editor_string_names.h"
42#include "editor/editor_undo_redo_manager.h"
43#include "editor/gui/scene_tree_editor.h"
44#include "editor/node_dock.h"
45#include "editor/scene_tree_dock.h"
46#include "plugins/script_editor_plugin.h"
47#include "scene/gui/button.h"
48#include "scene/gui/check_box.h"
49#include "scene/gui/label.h"
50#include "scene/gui/line_edit.h"
51#include "scene/gui/option_button.h"
52#include "scene/gui/popup_menu.h"
53#include "scene/gui/spin_box.h"
54#include "scene/resources/packed_scene.h"
55
56static Node *_find_first_script(Node *p_root, Node *p_node) {
57 if (p_node != p_root && p_node->get_owner() != p_root) {
58 return nullptr;
59 }
60 if (!p_node->get_script().is_null()) {
61 return p_node;
62 }
63
64 for (int i = 0; i < p_node->get_child_count(); i++) {
65 Node *ret = _find_first_script(p_root, p_node->get_child(i));
66 if (ret) {
67 return ret;
68 }
69 }
70
71 return nullptr;
72}
73
74class ConnectDialogBinds : public Object {
75 GDCLASS(ConnectDialogBinds, Object);
76
77public:
78 Vector<Variant> params;
79
80 bool _set(const StringName &p_name, const Variant &p_value) {
81 String name = p_name;
82
83 if (name.begins_with("bind/argument_")) {
84 int which = name.get_slice("_", 1).to_int() - 1;
85 ERR_FAIL_INDEX_V(which, params.size(), false);
86 params.write[which] = p_value;
87 } else {
88 return false;
89 }
90
91 return true;
92 }
93
94 bool _get(const StringName &p_name, Variant &r_ret) const {
95 String name = p_name;
96
97 if (name.begins_with("bind/argument_")) {
98 int which = name.get_slice("_", 1).to_int() - 1;
99 ERR_FAIL_INDEX_V(which, params.size(), false);
100 r_ret = params[which];
101 } else {
102 return false;
103 }
104
105 return true;
106 }
107
108 void _get_property_list(List<PropertyInfo> *p_list) const {
109 for (int i = 0; i < params.size(); i++) {
110 p_list->push_back(PropertyInfo(params[i].get_type(), "bind/argument_" + itos(i + 1)));
111 }
112 }
113
114 void notify_changed() {
115 notify_property_list_changed();
116 }
117
118 ConnectDialogBinds() {
119 }
120};
121
122/*
123 * Signal automatically called by parent dialog.
124 */
125void ConnectDialog::ok_pressed() {
126 String method_name = dst_method->get_text();
127
128 if (method_name.is_empty()) {
129 error->set_text(TTR("Method in target node must be specified."));
130 error->popup_centered();
131 return;
132 }
133
134 if (!TS->is_valid_identifier(method_name.strip_edges())) {
135 error->set_text(TTR("Method name must be a valid identifier."));
136 error->popup_centered();
137 return;
138 }
139
140 Node *target = tree->get_selected();
141 if (!target) {
142 return; // Nothing selected in the tree, not an error.
143 }
144 if (target->get_script().is_null()) {
145 if (!target->has_method(method_name)) {
146 error->set_text(TTR("Target method not found. Specify a valid method or attach a script to the target node."));
147 error->popup_centered();
148 return;
149 }
150 }
151 emit_signal(SNAME("connected"));
152 hide();
153}
154
155void ConnectDialog::_cancel_pressed() {
156 hide();
157}
158
159void ConnectDialog::_item_activated() {
160 _ok_pressed(); // From AcceptDialog.
161}
162
163void ConnectDialog::_text_submitted(const String &p_text) {
164 _ok_pressed(); // From AcceptDialog.
165}
166
167/*
168 * Called each time a target node is selected within the target node tree.
169 */
170void ConnectDialog::_tree_node_selected() {
171 Node *current = tree->get_selected();
172
173 if (!current) {
174 return;
175 }
176
177 dst_path = source->get_path_to(current);
178 if (!edit_mode) {
179 set_dst_method(generate_method_callback_name(source, signal, current));
180 }
181 _update_method_tree();
182 _update_ok_enabled();
183}
184
185void ConnectDialog::_focus_currently_connected() {
186 tree->set_selected(source);
187}
188
189void ConnectDialog::_unbind_count_changed(double p_count) {
190 for (Control *control : bind_controls) {
191 BaseButton *b = Object::cast_to<BaseButton>(control);
192 if (b) {
193 b->set_disabled(p_count > 0);
194 }
195
196 EditorInspector *e = Object::cast_to<EditorInspector>(control);
197 if (e) {
198 e->set_read_only(p_count > 0);
199 }
200 }
201}
202
203void ConnectDialog::_method_selected() {
204 TreeItem *selected_item = method_tree->get_selected();
205 dst_method->set_text(selected_item->get_metadata(0));
206}
207
208/*
209 * Adds a new parameter bind to connection.
210 */
211void ConnectDialog::_add_bind() {
212 Variant::Type type = (Variant::Type)type_list->get_item_id(type_list->get_selected());
213
214 Variant value;
215 Callable::CallError err;
216 Variant::construct(type, value, nullptr, 0, err);
217
218 cdbinds->params.push_back(value);
219 cdbinds->notify_changed();
220}
221
222/*
223 * Remove parameter bind from connection.
224 */
225void ConnectDialog::_remove_bind() {
226 String st = bind_editor->get_selected_path();
227 if (st.is_empty()) {
228 return;
229 }
230 int idx = st.get_slice("/", 1).to_int() - 1;
231
232 ERR_FAIL_INDEX(idx, cdbinds->params.size());
233 cdbinds->params.remove_at(idx);
234 cdbinds->notify_changed();
235}
236/*
237 * Automatically generates a name for the callback method.
238 */
239StringName ConnectDialog::generate_method_callback_name(Node *p_source, String p_signal_name, Node *p_target) {
240 String node_name = p_source->get_name();
241 for (int i = 0; i < node_name.length(); i++) { // TODO: Regex filter may be cleaner.
242 char32_t c = node_name[i];
243 if ((i == 0 && !is_unicode_identifier_start(c)) || (i > 0 && !is_unicode_identifier_continue(c))) {
244 if (c == ' ') {
245 // Replace spaces with underlines.
246 c = '_';
247 } else {
248 // Remove any other characters.
249 node_name.remove_at(i);
250 i--;
251 continue;
252 }
253 }
254 node_name[i] = c;
255 }
256
257 Dictionary subst;
258 subst["NodeName"] = node_name.to_pascal_case();
259 subst["nodeName"] = node_name.to_camel_case();
260 subst["node_name"] = node_name.to_snake_case();
261
262 subst["SignalName"] = p_signal_name.to_pascal_case();
263 subst["signalName"] = p_signal_name.to_camel_case();
264 subst["signal_name"] = p_signal_name.to_snake_case();
265
266 String dst_method;
267 if (p_source == p_target) {
268 dst_method = String(GLOBAL_GET("editor/naming/default_signal_callback_to_self_name")).format(subst);
269 } else {
270 dst_method = String(GLOBAL_GET("editor/naming/default_signal_callback_name")).format(subst);
271 }
272
273 return dst_method;
274}
275
276void ConnectDialog::_create_method_tree_items(const List<MethodInfo> &p_methods, TreeItem *p_parent_item) {
277 for (const MethodInfo &mi : p_methods) {
278 TreeItem *method_item = method_tree->create_item(p_parent_item);
279 method_item->set_text(0, get_signature(mi));
280 method_item->set_metadata(0, mi.name);
281 }
282}
283
284List<MethodInfo> ConnectDialog::_filter_method_list(const List<MethodInfo> &p_methods, const MethodInfo &p_signal, const String &p_search_string) const {
285 bool check_signal = compatible_methods_only->is_pressed();
286 List<MethodInfo> ret;
287
288 for (const MethodInfo &mi : p_methods) {
289 if (!p_search_string.is_empty() && !mi.name.contains(p_search_string)) {
290 continue;
291 }
292
293 if (check_signal) {
294 if (mi.arguments.size() != p_signal.arguments.size()) {
295 continue;
296 }
297
298 bool type_mismatch = false;
299 const List<PropertyInfo>::Element *E = p_signal.arguments.front();
300 for (const List<PropertyInfo>::Element *F = mi.arguments.front(); F; F = F->next(), E = E->next()) {
301 Variant::Type stype = E->get().type;
302 Variant::Type mtype = F->get().type;
303
304 if (stype != Variant::NIL && mtype != Variant::NIL && stype != mtype) {
305 type_mismatch = true;
306 break;
307 }
308
309 if (stype == Variant::OBJECT && mtype == Variant::OBJECT && !ClassDB::is_parent_class(E->get().class_name, F->get().class_name)) {
310 type_mismatch = true;
311 break;
312 }
313 }
314
315 if (type_mismatch) {
316 continue;
317 }
318 }
319 ret.push_back(mi);
320 }
321 return ret;
322}
323
324void ConnectDialog::_update_method_tree() {
325 method_tree->clear();
326
327 Color disabled_color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor)) * 0.7;
328 String search_string = method_search->get_text();
329 Node *target = tree->get_selected();
330 if (!target) {
331 return;
332 }
333
334 MethodInfo signal_info;
335 if (compatible_methods_only->is_pressed()) {
336 List<MethodInfo> signals;
337 source->get_signal_list(&signals);
338 for (const MethodInfo &mi : signals) {
339 if (mi.name == signal) {
340 signal_info = mi;
341 break;
342 }
343 }
344 }
345
346 TreeItem *root_item = method_tree->create_item();
347 root_item->set_text(0, TTR("Methods"));
348 root_item->set_selectable(0, false);
349
350 // If a script is attached, get methods from it.
351 ScriptInstance *si = target->get_script_instance();
352 if (si) {
353 if (si->get_script()->is_built_in()) {
354 si->get_script()->reload();
355 }
356 List<MethodInfo> methods;
357 si->get_method_list(&methods);
358 methods = _filter_method_list(methods, signal_info, search_string);
359
360 if (!methods.is_empty()) {
361 TreeItem *si_item = method_tree->create_item(root_item);
362 si_item->set_text(0, TTR("Attached Script"));
363 si_item->set_icon(0, get_editor_theme_icon(SNAME("Script")));
364 si_item->set_selectable(0, false);
365
366 _create_method_tree_items(methods, si_item);
367 }
368 }
369
370 if (script_methods_only->is_pressed()) {
371 empty_tree_label->set_visible(root_item->get_first_child() == nullptr);
372 return;
373 }
374
375 // Get methods from each class in the hierarchy.
376 StringName current_class = target->get_class_name();
377 do {
378 TreeItem *class_item = method_tree->create_item(root_item);
379 class_item->set_text(0, current_class);
380 Ref<Texture2D> icon = get_editor_theme_icon(SNAME("Node"));
381 if (has_theme_icon(current_class, EditorStringName(EditorIcons))) {
382 icon = get_editor_theme_icon(current_class);
383 }
384 class_item->set_icon(0, icon);
385 class_item->set_selectable(0, false);
386
387 List<MethodInfo> methods;
388 ClassDB::get_method_list(current_class, &methods, true);
389 methods = _filter_method_list(methods, signal_info, search_string);
390
391 if (methods.is_empty()) {
392 class_item->set_custom_color(0, disabled_color);
393 } else {
394 _create_method_tree_items(methods, class_item);
395 }
396 current_class = ClassDB::get_parent_class_nocheck(current_class);
397 } while (current_class != StringName());
398
399 empty_tree_label->set_visible(root_item->get_first_child() == nullptr);
400}
401
402void ConnectDialog::_method_check_button_pressed(const CheckButton *p_button) {
403 if (p_button == script_methods_only) {
404 EditorSettings::get_singleton()->set_project_metadata("editor_metadata", "show_script_methods_only", p_button->is_pressed());
405 } else if (p_button == compatible_methods_only) {
406 EditorSettings::get_singleton()->set_project_metadata("editor_metadata", "show_compatible_methods_only", p_button->is_pressed());
407 }
408 _update_method_tree();
409}
410
411void ConnectDialog::_open_method_popup() {
412 method_popup->popup_centered();
413 method_search->clear();
414 method_search->grab_focus();
415}
416
417/*
418 * Enables or disables the connect button. The connect button is enabled if a
419 * node is selected and valid in the selected mode.
420 */
421void ConnectDialog::_update_ok_enabled() {
422 Node *target = tree->get_selected();
423
424 if (target == nullptr) {
425 get_ok_button()->set_disabled(true);
426 return;
427 }
428
429 if (dst_method->get_text().is_empty()) {
430 get_ok_button()->set_disabled(true);
431 return;
432 }
433
434 get_ok_button()->set_disabled(false);
435}
436
437void ConnectDialog::_notification(int p_what) {
438 switch (p_what) {
439 case NOTIFICATION_ENTER_TREE: {
440 bind_editor->edit(cdbinds);
441
442 [[fallthrough]];
443 }
444 case NOTIFICATION_THEME_CHANGED: {
445 for (int i = 0; i < type_list->get_item_count(); i++) {
446 String type_name = Variant::get_type_name((Variant::Type)type_list->get_item_id(i));
447 type_list->set_item_icon(i, get_editor_theme_icon(type_name));
448 }
449
450 Ref<StyleBox> style = get_theme_stylebox("normal", "LineEdit")->duplicate();
451 if (style.is_valid()) {
452 style->set_content_margin(SIDE_TOP, style->get_content_margin(SIDE_TOP) + 1.0);
453 from_signal->add_theme_style_override("normal", style);
454 }
455 method_search->set_right_icon(get_editor_theme_icon("Search"));
456 open_method_tree->set_icon(get_editor_theme_icon("Edit"));
457 } break;
458 }
459}
460
461void ConnectDialog::_bind_methods() {
462 ADD_SIGNAL(MethodInfo("connected"));
463}
464
465Node *ConnectDialog::get_source() const {
466 return source;
467}
468
469ConnectDialog::ConnectionData ConnectDialog::get_source_connection_data() const {
470 return source_connection_data;
471}
472
473StringName ConnectDialog::get_signal_name() const {
474 return signal;
475}
476
477PackedStringArray ConnectDialog::get_signal_args() const {
478 return signal_args;
479}
480
481NodePath ConnectDialog::get_dst_path() const {
482 return dst_path;
483}
484
485void ConnectDialog::set_dst_node(Node *p_node) {
486 tree->set_selected(p_node);
487}
488
489StringName ConnectDialog::get_dst_method_name() const {
490 String txt = dst_method->get_text();
491 if (txt.contains("(")) {
492 txt = txt.left(txt.find("(")).strip_edges();
493 }
494 return txt;
495}
496
497void ConnectDialog::set_dst_method(const StringName &p_method) {
498 dst_method->set_text(p_method);
499}
500
501int ConnectDialog::get_unbinds() const {
502 return int(unbind_count->get_value());
503}
504
505Vector<Variant> ConnectDialog::get_binds() const {
506 return cdbinds->params;
507}
508
509String ConnectDialog::get_signature(const MethodInfo &p_method, PackedStringArray *r_arg_names) {
510 PackedStringArray signature;
511 signature.append(p_method.name);
512 signature.append("(");
513
514 for (int i = 0; i < p_method.arguments.size(); i++) {
515 if (i > 0) {
516 signature.append(", ");
517 }
518
519 const PropertyInfo &pi = p_method.arguments[i];
520 String tname = "var";
521 if (pi.type == Variant::OBJECT && pi.class_name != StringName()) {
522 tname = pi.class_name.operator String();
523 } else if (pi.type != Variant::NIL) {
524 tname = Variant::get_type_name(pi.type);
525 }
526
527 signature.append((pi.name.is_empty() ? String("arg " + itos(i)) : pi.name) + ": " + tname);
528 if (r_arg_names) {
529 r_arg_names->push_back(pi.name + ":" + tname);
530 }
531 }
532
533 signature.append(")");
534 return String().join(signature);
535}
536
537bool ConnectDialog::get_deferred() const {
538 return deferred->is_pressed();
539}
540
541bool ConnectDialog::get_one_shot() const {
542 return one_shot->is_pressed();
543}
544
545/*
546 * Returns true if ConnectDialog is being used to edit an existing connection.
547 */
548bool ConnectDialog::is_editing() const {
549 return edit_mode;
550}
551
552/*
553 * Initialize ConnectDialog and populate fields with expected data.
554 * If creating a connection from scratch, sensible defaults are used.
555 * If editing an existing connection, previous data is retained.
556 */
557void ConnectDialog::init(const ConnectionData &p_cd, const PackedStringArray &p_signal_args, bool p_edit) {
558 set_hide_on_ok(false);
559
560 source = static_cast<Node *>(p_cd.source);
561 signal = p_cd.signal;
562 signal_args = p_signal_args;
563
564 tree->set_selected(nullptr);
565 tree->set_marked(source, true);
566
567 if (p_cd.target) {
568 set_dst_node(static_cast<Node *>(p_cd.target));
569 set_dst_method(p_cd.method);
570 }
571
572 _update_ok_enabled();
573
574 bool b_deferred = (p_cd.flags & CONNECT_DEFERRED) == CONNECT_DEFERRED;
575 bool b_oneshot = (p_cd.flags & CONNECT_ONE_SHOT) == CONNECT_ONE_SHOT;
576
577 deferred->set_pressed(b_deferred);
578 one_shot->set_pressed(b_oneshot);
579
580 unbind_count->set_max(p_signal_args.size());
581
582 unbind_count->set_value(p_cd.unbinds);
583 _unbind_count_changed(p_cd.unbinds);
584
585 cdbinds->params.clear();
586 cdbinds->params = p_cd.binds;
587 cdbinds->notify_changed();
588
589 edit_mode = p_edit;
590
591 source_connection_data = p_cd;
592}
593
594void ConnectDialog::popup_dialog(const String p_for_signal) {
595 from_signal->set_text(p_for_signal);
596 error_label->add_theme_color_override("font_color", error_label->get_theme_color(SNAME("error_color"), EditorStringName(Editor)));
597 filter_nodes->clear();
598
599 if (!advanced->is_pressed()) {
600 error_label->set_visible(!_find_first_script(get_tree()->get_edited_scene_root(), get_tree()->get_edited_scene_root()));
601 }
602
603 if (first_popup) {
604 first_popup = false;
605 _advanced_pressed();
606 }
607
608 popup_centered();
609}
610
611void ConnectDialog::_advanced_pressed() {
612 if (advanced->is_pressed()) {
613 connect_to_label->set_text(TTR("Connect to Node:"));
614 tree->set_connect_to_script_mode(false);
615
616 vbc_right->show();
617 error_label->hide();
618 } else {
619 reset_size();
620 connect_to_label->set_text(TTR("Connect to Script:"));
621 tree->set_connect_to_script_mode(true);
622
623 vbc_right->hide();
624 error_label->set_visible(!_find_first_script(get_tree()->get_edited_scene_root(), get_tree()->get_edited_scene_root()));
625 }
626
627 EditorSettings::get_singleton()->set_project_metadata("editor_metadata", "use_advanced_connections", advanced->is_pressed());
628
629 popup_centered();
630}
631
632ConnectDialog::ConnectDialog() {
633 set_min_size(Size2(0, 500) * EDSCALE);
634
635 HBoxContainer *main_hb = memnew(HBoxContainer);
636 add_child(main_hb);
637
638 VBoxContainer *vbc_left = memnew(VBoxContainer);
639 main_hb->add_child(vbc_left);
640 vbc_left->set_h_size_flags(Control::SIZE_EXPAND_FILL);
641 vbc_left->set_custom_minimum_size(Vector2(400 * EDSCALE, 0));
642
643 from_signal = memnew(LineEdit);
644 vbc_left->add_margin_child(TTR("From Signal:"), from_signal);
645 from_signal->set_editable(false);
646
647 tree = memnew(SceneTreeEditor(false));
648 tree->set_connecting_signal(true);
649 tree->set_show_enabled_subscene(true);
650 tree->set_v_size_flags(Control::SIZE_FILL | Control::SIZE_EXPAND);
651 tree->get_scene_tree()->connect("item_activated", callable_mp(this, &ConnectDialog::_item_activated));
652 tree->connect("node_selected", callable_mp(this, &ConnectDialog::_tree_node_selected));
653 tree->set_connect_to_script_mode(true);
654
655 HBoxContainer *hbc_filter = memnew(HBoxContainer);
656
657 filter_nodes = memnew(LineEdit);
658 hbc_filter->add_child(filter_nodes);
659 filter_nodes->set_h_size_flags(Control::SIZE_FILL | Control::SIZE_EXPAND);
660 filter_nodes->set_placeholder(TTR("Filter Nodes"));
661 filter_nodes->set_clear_button_enabled(true);
662 filter_nodes->connect("text_changed", callable_mp(tree, &SceneTreeEditor::set_filter));
663
664 Button *focus_current = memnew(Button);
665 hbc_filter->add_child(focus_current);
666 focus_current->set_text(TTR("Go to Source"));
667 focus_current->connect("pressed", callable_mp(this, &ConnectDialog::_focus_currently_connected));
668
669 Node *mc = vbc_left->add_margin_child(TTR("Connect to Script:"), hbc_filter, false);
670 connect_to_label = Object::cast_to<Label>(vbc_left->get_child(mc->get_index() - 1));
671 vbc_left->add_child(tree);
672
673 error_label = memnew(Label);
674 error_label->set_text(TTR("Scene does not contain any script."));
675 vbc_left->add_child(error_label);
676 error_label->hide();
677
678 method_popup = memnew(AcceptDialog);
679 method_popup->set_title(TTR("Select Method"));
680 method_popup->set_min_size(Vector2(400, 600) * EDSCALE);
681 add_child(method_popup);
682
683 VBoxContainer *method_vbc = memnew(VBoxContainer);
684 method_popup->add_child(method_vbc);
685
686 method_search = memnew(LineEdit);
687 method_vbc->add_child(method_search);
688 method_search->set_placeholder(TTR("Filter Methods"));
689 method_search->set_clear_button_enabled(true);
690 method_search->connect("text_changed", callable_mp(this, &ConnectDialog::_update_method_tree).unbind(1));
691
692 method_tree = memnew(Tree);
693 method_vbc->add_child(method_tree);
694 method_tree->set_v_size_flags(Control::SIZE_EXPAND_FILL);
695 method_tree->set_hide_root(true);
696 method_tree->connect("item_selected", callable_mp(this, &ConnectDialog::_method_selected));
697 method_tree->connect("item_activated", callable_mp((Window *)method_popup, &Window::hide));
698
699 empty_tree_label = memnew(Label(TTR("No method found matching given filters.")));
700 method_popup->add_child(empty_tree_label);
701 empty_tree_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
702 empty_tree_label->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER);
703 empty_tree_label->set_autowrap_mode(TextServer::AUTOWRAP_WORD);
704
705 script_methods_only = memnew(CheckButton(TTR("Script Methods Only")));
706 method_vbc->add_child(script_methods_only);
707 script_methods_only->set_h_size_flags(Control::SIZE_SHRINK_END);
708 script_methods_only->set_pressed(EditorSettings::get_singleton()->get_project_metadata("editor_metadata", "show_script_methods_only", true));
709 script_methods_only->connect("pressed", callable_mp(this, &ConnectDialog::_method_check_button_pressed).bind(script_methods_only));
710
711 compatible_methods_only = memnew(CheckButton(TTR("Compatible Methods Only")));
712 method_vbc->add_child(compatible_methods_only);
713 compatible_methods_only->set_h_size_flags(Control::SIZE_SHRINK_END);
714 compatible_methods_only->set_pressed(EditorSettings::get_singleton()->get_project_metadata("editor_metadata", "show_compatible_methods_only", true));
715 compatible_methods_only->connect("pressed", callable_mp(this, &ConnectDialog::_method_check_button_pressed).bind(compatible_methods_only));
716
717 vbc_right = memnew(VBoxContainer);
718 main_hb->add_child(vbc_right);
719 vbc_right->set_h_size_flags(Control::SIZE_EXPAND_FILL);
720 vbc_right->set_custom_minimum_size(Vector2(150 * EDSCALE, 0));
721 vbc_right->hide();
722
723 HBoxContainer *add_bind_hb = memnew(HBoxContainer);
724
725 type_list = memnew(OptionButton);
726 type_list->set_h_size_flags(Control::SIZE_EXPAND_FILL);
727 add_bind_hb->add_child(type_list);
728 for (int i = 0; i < Variant::VARIANT_MAX; i++) {
729 if (i == Variant::NIL || i == Variant::OBJECT || i == Variant::CALLABLE || i == Variant::SIGNAL || i == Variant::RID) {
730 // These types can't be constructed or serialized properly, so skip them.
731 continue;
732 }
733
734 type_list->add_item(Variant::get_type_name(Variant::Type(i)), i);
735 }
736 bind_controls.push_back(type_list);
737
738 Button *add_bind = memnew(Button);
739 add_bind->set_text(TTR("Add"));
740 add_bind_hb->add_child(add_bind);
741 add_bind->connect("pressed", callable_mp(this, &ConnectDialog::_add_bind));
742 bind_controls.push_back(add_bind);
743
744 Button *del_bind = memnew(Button);
745 del_bind->set_text(TTR("Remove"));
746 add_bind_hb->add_child(del_bind);
747 del_bind->connect("pressed", callable_mp(this, &ConnectDialog::_remove_bind));
748 bind_controls.push_back(del_bind);
749
750 vbc_right->add_margin_child(TTR("Add Extra Call Argument:"), add_bind_hb);
751
752 bind_editor = memnew(EditorInspector);
753 bind_controls.push_back(bind_editor);
754
755 vbc_right->add_margin_child(TTR("Extra Call Arguments:"), bind_editor, true);
756
757 unbind_count = memnew(SpinBox);
758 unbind_count->set_tooltip_text(TTR("Allows to drop arguments sent by signal emitter."));
759 unbind_count->connect("value_changed", callable_mp(this, &ConnectDialog::_unbind_count_changed));
760
761 vbc_right->add_margin_child(TTR("Unbind Signal Arguments:"), unbind_count);
762
763 HBoxContainer *hbc_method = memnew(HBoxContainer);
764 vbc_left->add_margin_child(TTR("Receiver Method:"), hbc_method);
765
766 dst_method = memnew(LineEdit);
767 dst_method->set_h_size_flags(Control::SIZE_EXPAND_FILL);
768 dst_method->connect("text_changed", callable_mp(method_tree, &Tree::deselect_all).unbind(1));
769 dst_method->connect("text_submitted", callable_mp(this, &ConnectDialog::_text_submitted));
770 hbc_method->add_child(dst_method);
771
772 open_method_tree = memnew(Button);
773 hbc_method->add_child(open_method_tree);
774 open_method_tree->set_text("Pick");
775 open_method_tree->connect("pressed", callable_mp(this, &ConnectDialog::_open_method_popup));
776
777 advanced = memnew(CheckButton(TTR("Advanced")));
778 vbc_left->add_child(advanced);
779 advanced->set_h_size_flags(Control::SIZE_SHRINK_BEGIN | Control::SIZE_EXPAND);
780 advanced->set_pressed(EditorSettings::get_singleton()->get_project_metadata("editor_metadata", "use_advanced_connections", false));
781 advanced->connect("pressed", callable_mp(this, &ConnectDialog::_advanced_pressed));
782
783 HBoxContainer *hbox = memnew(HBoxContainer);
784 vbc_right->add_child(hbox);
785
786 deferred = memnew(CheckBox);
787 deferred->set_h_size_flags(0);
788 deferred->set_text(TTR("Deferred"));
789 deferred->set_tooltip_text(TTR("Defers the signal, storing it in a queue and only firing it at idle time."));
790 hbox->add_child(deferred);
791
792 one_shot = memnew(CheckBox);
793 one_shot->set_h_size_flags(0);
794 one_shot->set_text(TTR("One Shot"));
795 one_shot->set_tooltip_text(TTR("Disconnects the signal after its first emission."));
796 hbox->add_child(one_shot);
797
798 cdbinds = memnew(ConnectDialogBinds);
799
800 error = memnew(AcceptDialog);
801 add_child(error);
802 error->set_title(TTR("Cannot connect signal"));
803 error->set_ok_button_text(TTR("Close"));
804 set_ok_button_text(TTR("Connect"));
805}
806
807ConnectDialog::~ConnectDialog() {
808 memdelete(cdbinds);
809}
810
811//////////////////////////////////////////
812
813// Originally copied and adapted from EditorProperty, try to keep style in sync.
814Control *ConnectionsDockTree::make_custom_tooltip(const String &p_text) const {
815 // `p_text` is expected to be something like this:
816 // - `class|Control||Control brief description.`;
817 // - `signal|gui_input|(event: InputEvent)|gui_input description.`;
818 // - `../../.. :: _on_gui_input()`.
819 // Note that the description can be empty or contain `|`.
820 PackedStringArray slices = p_text.split("|", true, 3);
821 if (slices.size() < 4) {
822 return nullptr; // Use default tooltip instead.
823 }
824
825 String item_type = (slices[0] == "class") ? TTR("Class:") : TTR("Signal:");
826 String item_name = slices[1].strip_edges();
827 String item_params = slices[2].strip_edges();
828 String item_descr = slices[3].strip_edges();
829
830 String text = item_type + " [u][b]" + item_name + "[/b][/u]" + item_params + "\n";
831 if (item_descr.is_empty()) {
832 text += "[i]" + TTR("No description.") + "[/i]";
833 } else {
834 text += item_descr;
835 }
836
837 EditorHelpBit *help_bit = memnew(EditorHelpBit);
838 help_bit->get_rich_text()->set_custom_minimum_size(Size2(360 * EDSCALE, 1));
839 help_bit->set_text(text);
840
841 return help_bit;
842}
843
844struct _ConnectionsDockMethodInfoSort {
845 _FORCE_INLINE_ bool operator()(const MethodInfo &a, const MethodInfo &b) const {
846 return a.name < b.name;
847 }
848};
849
850void ConnectionsDock::_filter_changed(const String &p_text) {
851 update_tree();
852}
853
854/*
855 * Post-ConnectDialog callback for creating/editing connections.
856 * Creates or edits connections based on state of the ConnectDialog when "Connect" is pressed.
857 */
858void ConnectionsDock::_make_or_edit_connection() {
859 NodePath dst_path = connect_dialog->get_dst_path();
860 Node *target = selected_node->get_node(dst_path);
861 ERR_FAIL_NULL(target);
862
863 ConnectDialog::ConnectionData cd;
864 cd.source = connect_dialog->get_source();
865 cd.target = target;
866 cd.signal = connect_dialog->get_signal_name();
867 cd.method = connect_dialog->get_dst_method_name();
868 cd.unbinds = connect_dialog->get_unbinds();
869 if (cd.unbinds == 0) {
870 cd.binds = connect_dialog->get_binds();
871 }
872 bool b_deferred = connect_dialog->get_deferred();
873 bool b_oneshot = connect_dialog->get_one_shot();
874 cd.flags = CONNECT_PERSIST | (b_deferred ? CONNECT_DEFERRED : 0) | (b_oneshot ? CONNECT_ONE_SHOT : 0);
875
876 // Conditions to add function: must have a script and must not have the method already
877 // (in the class, the script itself, or inherited).
878 bool add_script_function = false;
879 Ref<Script> scr = target->get_script();
880 if (!scr.is_null() && !ClassDB::has_method(target->get_class(), cd.method)) {
881 // There is a chance that the method is inherited from another script.
882 bool found_inherited_function = false;
883 Ref<Script> inherited_scr = scr->get_base_script();
884 while (!inherited_scr.is_null()) {
885 int line = inherited_scr->get_language()->find_function(cd.method, inherited_scr->get_source_code());
886 if (line != -1) {
887 found_inherited_function = true;
888 break;
889 }
890
891 inherited_scr = inherited_scr->get_base_script();
892 }
893
894 add_script_function = !found_inherited_function;
895 }
896
897 if (connect_dialog->is_editing()) {
898 _disconnect(connect_dialog->get_source_connection_data());
899 _connect(cd);
900 } else {
901 _connect(cd);
902 }
903
904 if (add_script_function) {
905 PackedStringArray script_function_args = connect_dialog->get_signal_args();
906 script_function_args.resize(script_function_args.size() - cd.unbinds);
907 for (int i = 0; i < cd.binds.size(); i++) {
908 script_function_args.push_back("extra_arg_" + itos(i) + ":" + Variant::get_type_name(cd.binds[i].get_type()));
909 }
910
911 EditorNode::get_singleton()->emit_signal(SNAME("script_add_function_request"), target, cd.method, script_function_args);
912 hide();
913 }
914
915 update_tree();
916}
917
918/*
919 * Creates single connection w/ undo-redo functionality.
920 */
921void ConnectionsDock::_connect(const ConnectDialog::ConnectionData &p_cd) {
922 Node *source = Object::cast_to<Node>(p_cd.source);
923 Node *target = Object::cast_to<Node>(p_cd.target);
924
925 if (!source || !target) {
926 return;
927 }
928
929 Callable callable = p_cd.get_callable();
930 EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
931 undo_redo->create_action(vformat(TTR("Connect '%s' to '%s'"), String(p_cd.signal), String(p_cd.method)));
932 undo_redo->add_do_method(source, "connect", p_cd.signal, callable, p_cd.flags);
933 undo_redo->add_undo_method(source, "disconnect", p_cd.signal, callable);
934 undo_redo->add_do_method(this, "update_tree");
935 undo_redo->add_undo_method(this, "update_tree");
936 undo_redo->add_do_method(SceneTreeDock::get_singleton()->get_tree_editor(), "update_tree"); // To force redraw of scene tree.
937 undo_redo->add_undo_method(SceneTreeDock::get_singleton()->get_tree_editor(), "update_tree");
938
939 undo_redo->commit_action();
940}
941
942/*
943 * Break single connection w/ undo-redo functionality.
944 */
945void ConnectionsDock::_disconnect(const ConnectDialog::ConnectionData &p_cd) {
946 ERR_FAIL_COND(p_cd.source != selected_node); // Shouldn't happen but... Bugcheck.
947
948 EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
949 undo_redo->create_action(vformat(TTR("Disconnect '%s' from '%s'"), p_cd.signal, p_cd.method));
950
951 Callable callable = p_cd.get_callable();
952 undo_redo->add_do_method(selected_node, "disconnect", p_cd.signal, callable);
953 undo_redo->add_undo_method(selected_node, "connect", p_cd.signal, callable, p_cd.flags);
954 undo_redo->add_do_method(this, "update_tree");
955 undo_redo->add_undo_method(this, "update_tree");
956 undo_redo->add_do_method(SceneTreeDock::get_singleton()->get_tree_editor(), "update_tree"); // To force redraw of scene tree.
957 undo_redo->add_undo_method(SceneTreeDock::get_singleton()->get_tree_editor(), "update_tree");
958
959 undo_redo->commit_action();
960}
961
962/*
963 * Break all connections of currently selected signal.
964 * Can undo-redo as a single action.
965 */
966void ConnectionsDock::_disconnect_all() {
967 TreeItem *item = tree->get_selected();
968 if (!item || _get_item_type(*item) != TREE_ITEM_TYPE_SIGNAL) {
969 return;
970 }
971
972 TreeItem *child = item->get_first_child();
973 String signal_name = item->get_metadata(0).operator Dictionary()["name"];
974 EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
975 undo_redo->create_action(vformat(TTR("Disconnect all from signal: '%s'"), signal_name));
976
977 while (child) {
978 Connection connection = child->get_metadata(0);
979 if (!_is_connection_inherited(connection)) {
980 ConnectDialog::ConnectionData cd = connection;
981 undo_redo->add_do_method(selected_node, "disconnect", cd.signal, cd.get_callable());
982 undo_redo->add_undo_method(selected_node, "connect", cd.signal, cd.get_callable(), cd.flags);
983 }
984 child = child->get_next();
985 }
986
987 undo_redo->add_do_method(this, "update_tree");
988 undo_redo->add_undo_method(this, "update_tree");
989 undo_redo->add_do_method(SceneTreeDock::get_singleton()->get_tree_editor(), "update_tree");
990 undo_redo->add_undo_method(SceneTreeDock::get_singleton()->get_tree_editor(), "update_tree");
991
992 undo_redo->commit_action();
993}
994
995void ConnectionsDock::_tree_item_selected() {
996 TreeItem *item = tree->get_selected();
997 if (item && _get_item_type(*item) == TREE_ITEM_TYPE_SIGNAL) {
998 connect_button->set_text(TTR("Connect..."));
999 connect_button->set_icon(get_editor_theme_icon(SNAME("Instance")));
1000 connect_button->set_disabled(false);
1001 } else if (item && _get_item_type(*item) == TREE_ITEM_TYPE_CONNECTION) {
1002 connect_button->set_text(TTR("Disconnect"));
1003 connect_button->set_icon(get_editor_theme_icon(SNAME("Unlinked")));
1004 connect_button->set_disabled(false);
1005 } else {
1006 connect_button->set_text(TTR("Connect..."));
1007 connect_button->set_icon(get_editor_theme_icon(SNAME("Instance")));
1008 connect_button->set_disabled(true);
1009 }
1010}
1011
1012void ConnectionsDock::_tree_item_activated() { // "Activation" on double-click.
1013 TreeItem *item = tree->get_selected();
1014 if (!item) {
1015 return;
1016 }
1017
1018 if (_get_item_type(*item) == TREE_ITEM_TYPE_SIGNAL) {
1019 _open_connection_dialog(*item);
1020 } else if (_get_item_type(*item) == TREE_ITEM_TYPE_CONNECTION) {
1021 _go_to_method(*item);
1022 }
1023}
1024
1025ConnectionsDock::TreeItemType ConnectionsDock::_get_item_type(const TreeItem &p_item) const {
1026 if (&p_item == tree->get_root()) {
1027 return TREE_ITEM_TYPE_ROOT;
1028 } else if (p_item.get_parent() == tree->get_root()) {
1029 return TREE_ITEM_TYPE_CLASS;
1030 } else if (p_item.get_parent()->get_parent() == tree->get_root()) {
1031 return TREE_ITEM_TYPE_SIGNAL;
1032 } else {
1033 return TREE_ITEM_TYPE_CONNECTION;
1034 }
1035}
1036
1037bool ConnectionsDock::_is_connection_inherited(Connection &p_connection) {
1038 return bool(p_connection.flags & CONNECT_INHERITED);
1039}
1040
1041/*
1042 * Open connection dialog with TreeItem data to CREATE a brand-new connection.
1043 */
1044void ConnectionsDock::_open_connection_dialog(TreeItem &p_item) {
1045 Dictionary sinfo = p_item.get_metadata(0);
1046 String signal_name = sinfo["name"];
1047 PackedStringArray signal_args = sinfo["args"];
1048
1049 Node *dst_node = selected_node->get_owner() ? selected_node->get_owner() : selected_node;
1050 if (!dst_node || dst_node->get_script().is_null()) {
1051 dst_node = _find_first_script(get_tree()->get_edited_scene_root(), get_tree()->get_edited_scene_root());
1052 }
1053
1054 ConnectDialog::ConnectionData cd;
1055 cd.source = selected_node;
1056 cd.signal = StringName(signal_name);
1057 cd.target = dst_node;
1058 cd.method = ConnectDialog::generate_method_callback_name(cd.source, signal_name, cd.target);
1059 connect_dialog->popup_dialog(signal_name + "(" + String(", ").join(signal_args) + ")");
1060 connect_dialog->init(cd, signal_args);
1061 connect_dialog->set_title(TTR("Connect a Signal to a Method"));
1062}
1063
1064/*
1065 * Open connection dialog with Connection data to EDIT an existing connection.
1066 */
1067void ConnectionsDock::_open_edit_connection_dialog(TreeItem &p_item) {
1068 TreeItem *signal_item = p_item.get_parent();
1069 ERR_FAIL_NULL(signal_item);
1070
1071 Connection connection = p_item.get_metadata(0);
1072 ConnectDialog::ConnectionData cd = connection;
1073
1074 Node *src = Object::cast_to<Node>(cd.source);
1075 Node *dst = Object::cast_to<Node>(cd.target);
1076
1077 if (src && dst) {
1078 const String &signal_name_ref = cd.signal;
1079 PackedStringArray signal_args = signal_item->get_metadata(0).operator Dictionary()["args"];
1080
1081 connect_dialog->set_title(vformat(TTR("Edit Connection: '%s'"), cd.signal));
1082 connect_dialog->popup_dialog(signal_name_ref);
1083 connect_dialog->init(cd, signal_args, true);
1084 }
1085}
1086
1087/*
1088 * Open slot method location in script editor.
1089 */
1090void ConnectionsDock::_go_to_method(TreeItem &p_item) {
1091 if (_get_item_type(p_item) != TREE_ITEM_TYPE_CONNECTION) {
1092 return;
1093 }
1094
1095 Connection connection = p_item.get_metadata(0);
1096 ConnectDialog::ConnectionData cd = connection;
1097 ERR_FAIL_COND(cd.source != selected_node); // Shouldn't happen but... bugcheck.
1098
1099 if (!cd.target) {
1100 return;
1101 }
1102
1103 Ref<Script> scr = cd.target->get_script();
1104
1105 if (scr.is_null()) {
1106 return;
1107 }
1108
1109 if (scr.is_valid() && ScriptEditor::get_singleton()->script_goto_method(scr, cd.method)) {
1110 EditorNode::get_singleton()->editor_select(EditorNode::EDITOR_SCRIPT);
1111 }
1112}
1113
1114void ConnectionsDock::_handle_class_menu_option(int p_option) {
1115 switch (p_option) {
1116 case CLASS_MENU_OPEN_DOCS:
1117 ScriptEditor::get_singleton()->goto_help("class:" + class_menu_doc_class_name);
1118 EditorNode::get_singleton()->set_visible_editor(EditorNode::EDITOR_SCRIPT);
1119 break;
1120 }
1121}
1122
1123void ConnectionsDock::_class_menu_about_to_popup() {
1124 class_menu->set_item_disabled(class_menu->get_item_index(CLASS_MENU_OPEN_DOCS), class_menu_doc_class_name.is_empty());
1125}
1126
1127void ConnectionsDock::_handle_signal_menu_option(int p_option) {
1128 TreeItem *item = tree->get_selected();
1129 if (!item || _get_item_type(*item) != TREE_ITEM_TYPE_SIGNAL) {
1130 return;
1131 }
1132
1133 Dictionary meta = item->get_metadata(0);
1134
1135 switch (p_option) {
1136 case SIGNAL_MENU_CONNECT: {
1137 _open_connection_dialog(*item);
1138 } break;
1139 case SIGNAL_MENU_DISCONNECT_ALL: {
1140 disconnect_all_dialog->set_text(vformat(TTR("Are you sure you want to remove all connections from the \"%s\" signal?"), meta["name"]));
1141 disconnect_all_dialog->popup_centered();
1142 } break;
1143 case SIGNAL_MENU_COPY_NAME: {
1144 DisplayServer::get_singleton()->clipboard_set(meta["name"]);
1145 } break;
1146 case SIGNAL_MENU_OPEN_DOCS: {
1147 ScriptEditor::get_singleton()->goto_help("class_signal:" + String(meta["class"]) + ":" + String(meta["name"]));
1148 EditorNode::get_singleton()->set_visible_editor(EditorNode::EDITOR_SCRIPT);
1149 } break;
1150 }
1151}
1152
1153void ConnectionsDock::_signal_menu_about_to_popup() {
1154 TreeItem *item = tree->get_selected();
1155 if (!item || _get_item_type(*item) != TREE_ITEM_TYPE_SIGNAL) {
1156 return;
1157 }
1158
1159 Dictionary meta = item->get_metadata(0);
1160
1161 bool disable_disconnect_all = true;
1162 for (int i = 0; i < item->get_child_count(); i++) {
1163 if (!item->get_child(i)->has_meta("_inherited_connection")) {
1164 disable_disconnect_all = false;
1165 }
1166 }
1167
1168 signal_menu->set_item_disabled(signal_menu->get_item_index(SIGNAL_MENU_DISCONNECT_ALL), disable_disconnect_all);
1169 signal_menu->set_item_disabled(signal_menu->get_item_index(SIGNAL_MENU_OPEN_DOCS), String(meta["class"]).is_empty());
1170}
1171
1172void ConnectionsDock::_handle_slot_menu_option(int p_option) {
1173 TreeItem *item = tree->get_selected();
1174 if (!item || _get_item_type(*item) != TREE_ITEM_TYPE_CONNECTION) {
1175 return;
1176 }
1177
1178 switch (p_option) {
1179 case SLOT_MENU_EDIT: {
1180 _open_edit_connection_dialog(*item);
1181 } break;
1182 case SLOT_MENU_GO_TO_METHOD: {
1183 _go_to_method(*item);
1184 } break;
1185 case SLOT_MENU_DISCONNECT: {
1186 Connection connection = item->get_metadata(0);
1187 _disconnect(connection);
1188 update_tree();
1189 } break;
1190 }
1191}
1192
1193void ConnectionsDock::_slot_menu_about_to_popup() {
1194 TreeItem *item = tree->get_selected();
1195 if (!item || _get_item_type(*item) != TREE_ITEM_TYPE_CONNECTION) {
1196 return;
1197 }
1198
1199 bool connection_is_inherited = item->has_meta("_inherited_connection");
1200
1201 slot_menu->set_item_disabled(slot_menu->get_item_index(SLOT_MENU_EDIT), connection_is_inherited);
1202 slot_menu->set_item_disabled(slot_menu->get_item_index(SLOT_MENU_DISCONNECT), connection_is_inherited);
1203}
1204
1205void ConnectionsDock::_rmb_pressed(const Ref<InputEvent> &p_event) {
1206 const Ref<InputEventMouseButton> &mb_event = p_event;
1207 if (mb_event.is_null() || !mb_event->is_pressed() || mb_event->get_button_index() != MouseButton::RIGHT) {
1208 return;
1209 }
1210
1211 TreeItem *item = tree->get_item_at_position(mb_event->get_position());
1212 if (!item) {
1213 return;
1214 }
1215
1216 Vector2 screen_position = tree->get_screen_position() + mb_event->get_position();
1217
1218 switch (_get_item_type(*item)) {
1219 case TREE_ITEM_TYPE_ROOT:
1220 break;
1221 case TREE_ITEM_TYPE_CLASS:
1222 class_menu_doc_class_name = item->get_metadata(0);
1223 class_menu->set_position(screen_position);
1224 class_menu->reset_size();
1225 class_menu->popup();
1226 accept_event(); // Don't collapse item.
1227 break;
1228 case TREE_ITEM_TYPE_SIGNAL:
1229 signal_menu->set_position(screen_position);
1230 signal_menu->reset_size();
1231 signal_menu->popup();
1232 break;
1233 case TREE_ITEM_TYPE_CONNECTION:
1234 slot_menu->set_position(screen_position);
1235 slot_menu->reset_size();
1236 slot_menu->popup();
1237 break;
1238 }
1239}
1240
1241void ConnectionsDock::_close() {
1242 hide();
1243}
1244
1245void ConnectionsDock::_connect_pressed() {
1246 TreeItem *item = tree->get_selected();
1247 if (!item) {
1248 connect_button->set_disabled(true);
1249 return;
1250 }
1251
1252 if (_get_item_type(*item) == TREE_ITEM_TYPE_SIGNAL) {
1253 _open_connection_dialog(*item);
1254 } else if (_get_item_type(*item) == TREE_ITEM_TYPE_CONNECTION) {
1255 Connection connection = item->get_metadata(0);
1256 _disconnect(connection);
1257 update_tree();
1258 }
1259}
1260
1261void ConnectionsDock::_notification(int p_what) {
1262 switch (p_what) {
1263 case NOTIFICATION_ENTER_TREE:
1264 case NOTIFICATION_THEME_CHANGED: {
1265 search_box->set_right_icon(get_editor_theme_icon(SNAME("Search")));
1266
1267 class_menu->set_item_icon(class_menu->get_item_index(CLASS_MENU_OPEN_DOCS), get_editor_theme_icon(SNAME("Help")));
1268
1269 signal_menu->set_item_icon(signal_menu->get_item_index(SIGNAL_MENU_CONNECT), get_editor_theme_icon(SNAME("Instance")));
1270 signal_menu->set_item_icon(signal_menu->get_item_index(SIGNAL_MENU_DISCONNECT_ALL), get_editor_theme_icon(SNAME("Unlinked")));
1271 signal_menu->set_item_icon(signal_menu->get_item_index(SIGNAL_MENU_COPY_NAME), get_editor_theme_icon(SNAME("ActionCopy")));
1272 signal_menu->set_item_icon(signal_menu->get_item_index(SIGNAL_MENU_OPEN_DOCS), get_editor_theme_icon(SNAME("Help")));
1273
1274 slot_menu->set_item_icon(slot_menu->get_item_index(SLOT_MENU_EDIT), get_editor_theme_icon(SNAME("Edit")));
1275 slot_menu->set_item_icon(slot_menu->get_item_index(SLOT_MENU_GO_TO_METHOD), get_editor_theme_icon(SNAME("ArrowRight")));
1276 slot_menu->set_item_icon(slot_menu->get_item_index(SLOT_MENU_DISCONNECT), get_editor_theme_icon(SNAME("Unlinked")));
1277 } break;
1278
1279 case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {
1280 update_tree();
1281 } break;
1282 }
1283}
1284
1285void ConnectionsDock::_bind_methods() {
1286 ClassDB::bind_method("update_tree", &ConnectionsDock::update_tree);
1287}
1288
1289void ConnectionsDock::set_node(Node *p_node) {
1290 selected_node = p_node;
1291 update_tree();
1292}
1293
1294void ConnectionsDock::update_tree() {
1295 String prev_selected;
1296 if (tree->is_anything_selected()) {
1297 prev_selected = tree->get_selected()->get_text(0);
1298 }
1299 tree->clear();
1300
1301 if (!selected_node) {
1302 return;
1303 }
1304
1305 TreeItem *root = tree->create_item();
1306 DocTools *doc_data = EditorHelp::get_doc_data();
1307 EditorData &editor_data = EditorNode::get_editor_data();
1308 StringName native_base = selected_node->get_class();
1309 Ref<Script> script_base = selected_node->get_script();
1310
1311 while (native_base != StringName()) {
1312 String class_name;
1313 String doc_class_name;
1314 String class_brief;
1315 Ref<Texture2D> class_icon;
1316 List<MethodInfo> class_signals;
1317
1318 if (script_base.is_valid()) {
1319 class_name = script_base->get_global_name();
1320 if (class_name.is_empty()) {
1321 class_name = script_base->get_path().get_file();
1322 }
1323
1324 doc_class_name = script_base->get_global_name();
1325 if (doc_class_name.is_empty()) {
1326 doc_class_name = script_base->get_path().trim_prefix("res://").quote();
1327 }
1328
1329 // For a script class, the cache is filled each time.
1330 if (!doc_class_name.is_empty()) {
1331 if (descr_cache.has(doc_class_name)) {
1332 descr_cache[doc_class_name].clear();
1333 }
1334 HashMap<String, DocData::ClassDoc>::ConstIterator F = doc_data->class_list.find(doc_class_name);
1335 if (F) {
1336 class_brief = F->value.brief_description;
1337 for (int i = 0; i < F->value.signals.size(); i++) {
1338 descr_cache[doc_class_name][F->value.signals[i].name] = F->value.signals[i].description;
1339 }
1340 } else {
1341 doc_class_name = String();
1342 }
1343 }
1344
1345 class_icon = editor_data.get_script_icon(script_base);
1346 if (class_icon.is_null() && has_theme_icon(native_base, EditorStringName(EditorIcons))) {
1347 class_icon = get_editor_theme_icon(native_base);
1348 }
1349
1350 script_base->get_script_signal_list(&class_signals);
1351
1352 // TODO: Core: Add optional parameter to ignore base classes (no_inheritance like in ClassDB).
1353 Ref<Script> base = script_base->get_base_script();
1354 if (base.is_valid()) {
1355 List<MethodInfo> base_signals;
1356 base->get_script_signal_list(&base_signals);
1357 HashSet<String> base_signal_names;
1358 for (List<MethodInfo>::Element *F = base_signals.front(); F; F = F->next()) {
1359 base_signal_names.insert(F->get().name);
1360 }
1361 for (List<MethodInfo>::Element *F = class_signals.front(); F; F = F->next()) {
1362 if (base_signal_names.has(F->get().name)) {
1363 class_signals.erase(F);
1364 }
1365 }
1366 }
1367
1368 script_base = base;
1369 } else {
1370 class_name = native_base;
1371 doc_class_name = class_name;
1372
1373 HashMap<String, DocData::ClassDoc>::ConstIterator F = doc_data->class_list.find(doc_class_name);
1374 if (F) {
1375 class_brief = DTR(F->value.brief_description);
1376 // For a native class, the cache is filled once.
1377 if (!descr_cache.has(doc_class_name)) {
1378 for (int i = 0; i < F->value.signals.size(); i++) {
1379 descr_cache[doc_class_name][F->value.signals[i].name] = DTR(F->value.signals[i].description);
1380 }
1381 }
1382 } else {
1383 doc_class_name = String();
1384 }
1385
1386 if (has_theme_icon(native_base, EditorStringName(EditorIcons))) {
1387 class_icon = get_editor_theme_icon(native_base);
1388 }
1389
1390 ClassDB::get_signal_list(native_base, &class_signals, true);
1391
1392 native_base = ClassDB::get_parent_class(native_base);
1393 }
1394
1395 if (class_icon.is_null()) {
1396 class_icon = get_editor_theme_icon(SNAME("Object"));
1397 }
1398
1399 TreeItem *section_item = nullptr;
1400
1401 // Create subsections.
1402 if (!class_signals.is_empty()) {
1403 class_signals.sort();
1404
1405 section_item = tree->create_item(root);
1406 section_item->set_text(0, class_name);
1407 // `|` separators used in `make_custom_tooltip()` for formatting.
1408 section_item->set_tooltip_text(0, "class|" + class_name + "||" + class_brief);
1409 section_item->set_icon(0, class_icon);
1410 section_item->set_selectable(0, false);
1411 section_item->set_editable(0, false);
1412 section_item->set_custom_bg_color(0, get_theme_color(SNAME("prop_subsection"), EditorStringName(Editor)));
1413 section_item->set_metadata(0, doc_class_name);
1414 }
1415
1416 for (MethodInfo &mi : class_signals) {
1417 const StringName &signal_name = mi.name;
1418 if (!search_box->get_text().is_subsequence_ofn(signal_name)) {
1419 continue;
1420 }
1421 PackedStringArray argnames;
1422
1423 // Create the children of the subsection - the actual list of signals.
1424 TreeItem *signal_item = tree->create_item(section_item);
1425 String signame = connect_dialog->get_signature(mi, &argnames);
1426 signal_item->set_text(0, signame);
1427
1428 if (signame == prev_selected) {
1429 signal_item->select(0);
1430 prev_selected = "";
1431 }
1432
1433 Dictionary sinfo;
1434 sinfo["class"] = doc_class_name;
1435 sinfo["name"] = signal_name;
1436 sinfo["args"] = argnames;
1437 signal_item->set_metadata(0, sinfo);
1438 signal_item->set_icon(0, get_editor_theme_icon(SNAME("Signal")));
1439
1440 // Set tooltip with the signal's documentation.
1441 {
1442 String descr;
1443
1444 HashMap<StringName, HashMap<StringName, String>>::ConstIterator G = descr_cache.find(doc_class_name);
1445 if (G) {
1446 HashMap<StringName, String>::ConstIterator F = G->value.find(signal_name);
1447 if (F) {
1448 descr = F->value;
1449 }
1450 }
1451
1452 // `|` separators used in `make_custom_tooltip()` for formatting.
1453 signal_item->set_tooltip_text(0, "signal|" + String(signal_name) + "|" + signame.trim_prefix(mi.name) + "|" + descr);
1454 }
1455
1456 // List existing connections.
1457 List<Object::Connection> existing_connections;
1458 selected_node->get_signal_connection_list(signal_name, &existing_connections);
1459
1460 for (const Object::Connection &F : existing_connections) {
1461 Connection connection = F;
1462 if (!(connection.flags & CONNECT_PERSIST)) {
1463 continue;
1464 }
1465 ConnectDialog::ConnectionData cd = connection;
1466
1467 Node *target = Object::cast_to<Node>(cd.target);
1468 if (!target) {
1469 continue;
1470 }
1471
1472 String path = String(selected_node->get_path_to(target)) + " :: " + cd.method + "()";
1473 if (cd.flags & CONNECT_DEFERRED) {
1474 path += " (deferred)";
1475 }
1476 if (cd.flags & CONNECT_ONE_SHOT) {
1477 path += " (one-shot)";
1478 }
1479 if (cd.unbinds > 0) {
1480 path += " unbinds(" + itos(cd.unbinds) + ")";
1481 } else if (!cd.binds.is_empty()) {
1482 path += " binds(";
1483 for (int i = 0; i < cd.binds.size(); i++) {
1484 if (i > 0) {
1485 path += ", ";
1486 }
1487 path += cd.binds[i].operator String();
1488 }
1489 path += ")";
1490 }
1491
1492 TreeItem *connection_item = tree->create_item(signal_item);
1493 connection_item->set_text(0, path);
1494 connection_item->set_metadata(0, connection);
1495 connection_item->set_icon(0, get_editor_theme_icon(SNAME("Slot")));
1496
1497 if (_is_connection_inherited(connection)) {
1498 // The scene inherits this connection.
1499 connection_item->set_custom_color(0, get_theme_color(SNAME("warning_color"), EditorStringName(Editor)));
1500 connection_item->set_meta("_inherited_connection", true);
1501 }
1502 }
1503 }
1504 }
1505
1506 connect_button->set_text(TTR("Connect..."));
1507 connect_button->set_icon(get_editor_theme_icon(SNAME("Instance")));
1508 connect_button->set_disabled(true);
1509}
1510
1511ConnectionsDock::ConnectionsDock() {
1512 set_name(TTR("Signals"));
1513
1514 VBoxContainer *vbc = this;
1515
1516 search_box = memnew(LineEdit);
1517 search_box->set_h_size_flags(Control::SIZE_EXPAND_FILL);
1518 search_box->set_placeholder(TTR("Filter Signals"));
1519 search_box->set_clear_button_enabled(true);
1520 search_box->connect("text_changed", callable_mp(this, &ConnectionsDock::_filter_changed));
1521 vbc->add_child(search_box);
1522
1523 tree = memnew(ConnectionsDockTree);
1524 tree->set_columns(1);
1525 tree->set_select_mode(Tree::SELECT_ROW);
1526 tree->set_hide_root(true);
1527 tree->set_column_clip_content(0, true);
1528 vbc->add_child(tree);
1529 tree->set_v_size_flags(Control::SIZE_EXPAND_FILL);
1530 tree->set_allow_rmb_select(true);
1531
1532 connect_button = memnew(Button);
1533 HBoxContainer *hb = memnew(HBoxContainer);
1534 vbc->add_child(hb);
1535 hb->add_spacer();
1536 hb->add_child(connect_button);
1537 connect_button->connect("pressed", callable_mp(this, &ConnectionsDock::_connect_pressed));
1538
1539 connect_dialog = memnew(ConnectDialog);
1540 connect_dialog->connect("connected", callable_mp(NodeDock::get_singleton(), &NodeDock::restore_last_valid_node), CONNECT_DEFERRED);
1541 add_child(connect_dialog);
1542
1543 disconnect_all_dialog = memnew(ConfirmationDialog);
1544 add_child(disconnect_all_dialog);
1545 disconnect_all_dialog->connect("confirmed", callable_mp(this, &ConnectionsDock::_disconnect_all));
1546 disconnect_all_dialog->set_text(TTR("Are you sure you want to remove all connections from this signal?"));
1547
1548 class_menu = memnew(PopupMenu);
1549 class_menu->connect("id_pressed", callable_mp(this, &ConnectionsDock::_handle_class_menu_option));
1550 class_menu->connect("about_to_popup", callable_mp(this, &ConnectionsDock::_class_menu_about_to_popup));
1551 class_menu->add_item(TTR("Open Documentation"), CLASS_MENU_OPEN_DOCS);
1552 add_child(class_menu);
1553
1554 signal_menu = memnew(PopupMenu);
1555 signal_menu->connect("id_pressed", callable_mp(this, &ConnectionsDock::_handle_signal_menu_option));
1556 signal_menu->connect("about_to_popup", callable_mp(this, &ConnectionsDock::_signal_menu_about_to_popup));
1557 signal_menu->add_item(TTR("Connect..."), SIGNAL_MENU_CONNECT);
1558 signal_menu->add_item(TTR("Disconnect All"), SIGNAL_MENU_DISCONNECT_ALL);
1559 signal_menu->add_item(TTR("Copy Name"), SIGNAL_MENU_COPY_NAME);
1560 signal_menu->add_separator();
1561 signal_menu->add_item(TTR("Open Documentation"), SIGNAL_MENU_OPEN_DOCS);
1562 add_child(signal_menu);
1563
1564 slot_menu = memnew(PopupMenu);
1565 slot_menu->connect("id_pressed", callable_mp(this, &ConnectionsDock::_handle_slot_menu_option));
1566 slot_menu->connect("about_to_popup", callable_mp(this, &ConnectionsDock::_slot_menu_about_to_popup));
1567 slot_menu->add_item(TTR("Edit..."), SLOT_MENU_EDIT);
1568 slot_menu->add_item(TTR("Go to Method"), SLOT_MENU_GO_TO_METHOD);
1569 slot_menu->add_item(TTR("Disconnect"), SLOT_MENU_DISCONNECT);
1570 add_child(slot_menu);
1571
1572 connect_dialog->connect("connected", callable_mp(this, &ConnectionsDock::_make_or_edit_connection));
1573 tree->connect("item_selected", callable_mp(this, &ConnectionsDock::_tree_item_selected));
1574 tree->connect("item_activated", callable_mp(this, &ConnectionsDock::_tree_item_activated));
1575 tree->connect("gui_input", callable_mp(this, &ConnectionsDock::_rmb_pressed));
1576
1577 add_theme_constant_override("separation", 3 * EDSCALE);
1578}
1579
1580ConnectionsDock::~ConnectionsDock() {
1581}
1582