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 | |
56 | static 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 | |
74 | class ConnectDialogBinds : public Object { |
75 | GDCLASS(ConnectDialogBinds, Object); |
76 | |
77 | public: |
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 | */ |
125 | void 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 | |
155 | void ConnectDialog::_cancel_pressed() { |
156 | hide(); |
157 | } |
158 | |
159 | void ConnectDialog::_item_activated() { |
160 | _ok_pressed(); // From AcceptDialog. |
161 | } |
162 | |
163 | void 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 | */ |
170 | void 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 | |
185 | void ConnectDialog::_focus_currently_connected() { |
186 | tree->set_selected(source); |
187 | } |
188 | |
189 | void 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 | |
203 | void 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 | */ |
211 | void 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 | */ |
225 | void 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 | */ |
239 | StringName 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 | |
276 | void 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 | |
284 | List<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 | |
324 | void 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 | |
402 | void 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 | |
411 | void ConnectDialog::() { |
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 | */ |
421 | void 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 | |
437 | void 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 | |
461 | void ConnectDialog::_bind_methods() { |
462 | ADD_SIGNAL(MethodInfo("connected" )); |
463 | } |
464 | |
465 | Node *ConnectDialog::get_source() const { |
466 | return source; |
467 | } |
468 | |
469 | ConnectDialog::ConnectionData ConnectDialog::get_source_connection_data() const { |
470 | return source_connection_data; |
471 | } |
472 | |
473 | StringName ConnectDialog::get_signal_name() const { |
474 | return signal; |
475 | } |
476 | |
477 | PackedStringArray ConnectDialog::get_signal_args() const { |
478 | return signal_args; |
479 | } |
480 | |
481 | NodePath ConnectDialog::get_dst_path() const { |
482 | return dst_path; |
483 | } |
484 | |
485 | void ConnectDialog::set_dst_node(Node *p_node) { |
486 | tree->set_selected(p_node); |
487 | } |
488 | |
489 | StringName 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 | |
497 | void ConnectDialog::set_dst_method(const StringName &p_method) { |
498 | dst_method->set_text(p_method); |
499 | } |
500 | |
501 | int ConnectDialog::get_unbinds() const { |
502 | return int(unbind_count->get_value()); |
503 | } |
504 | |
505 | Vector<Variant> ConnectDialog::get_binds() const { |
506 | return cdbinds->params; |
507 | } |
508 | |
509 | String 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 | |
537 | bool ConnectDialog::get_deferred() const { |
538 | return deferred->is_pressed(); |
539 | } |
540 | |
541 | bool 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 | */ |
548 | bool 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 | */ |
557 | void 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 | |
594 | void ConnectDialog::(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 | |
611 | void 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 | |
632 | ConnectDialog::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 | |
807 | ConnectDialog::~ConnectDialog() { |
808 | memdelete(cdbinds); |
809 | } |
810 | |
811 | ////////////////////////////////////////// |
812 | |
813 | // Originally copied and adapted from EditorProperty, try to keep style in sync. |
814 | Control *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 | |
844 | struct _ConnectionsDockMethodInfoSort { |
845 | _FORCE_INLINE_ bool operator()(const MethodInfo &a, const MethodInfo &b) const { |
846 | return a.name < b.name; |
847 | } |
848 | }; |
849 | |
850 | void 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 | */ |
858 | void 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 | */ |
921 | void 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 | */ |
945 | void 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 | */ |
966 | void 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 | |
995 | void 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 | |
1012 | void 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 | |
1025 | ConnectionsDock::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 | |
1037 | bool 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 | */ |
1044 | void 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 | */ |
1067 | void 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 | */ |
1090 | void 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 | |
1114 | void 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 | |
1123 | void ConnectionsDock::() { |
1124 | class_menu->set_item_disabled(class_menu->get_item_index(CLASS_MENU_OPEN_DOCS), class_menu_doc_class_name.is_empty()); |
1125 | } |
1126 | |
1127 | void 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 | |
1153 | void ConnectionsDock::() { |
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 | |
1172 | void 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 | |
1193 | void ConnectionsDock::() { |
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 | |
1205 | void 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 | |
1241 | void ConnectionsDock::_close() { |
1242 | hide(); |
1243 | } |
1244 | |
1245 | void 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 | |
1261 | void 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 | |
1285 | void ConnectionsDock::_bind_methods() { |
1286 | ClassDB::bind_method("update_tree" , &ConnectionsDock::update_tree); |
1287 | } |
1288 | |
1289 | void ConnectionsDock::set_node(Node *p_node) { |
1290 | selected_node = p_node; |
1291 | update_tree(); |
1292 | } |
1293 | |
1294 | void 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 | |
1511 | ConnectionsDock::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 | |
1580 | ConnectionsDock::~ConnectionsDock() { |
1581 | } |
1582 | |