/**************************************************************************/ /* connections_dialog.cpp */ /**************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ /* https://godotengine.org */ /**************************************************************************/ /* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ /* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ /* "Software"), to deal in the Software without restriction, including */ /* without limitation the rights to use, copy, modify, merge, publish, */ /* distribute, sublicense, and/or sell copies of the Software, and to */ /* permit persons to whom the Software is furnished to do so, subject to */ /* the following conditions: */ /* */ /* The above copyright notice and this permission notice shall be */ /* included in all copies or substantial portions of the Software. */ /* */ /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ /* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ /* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ /* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ /* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /**************************************************************************/ #include "connections_dialog.h" #include "core/config/project_settings.h" #include "core/templates/hash_set.h" #include "editor/editor_help.h" #include "editor/editor_inspector.h" #include "editor/editor_node.h" #include "editor/editor_settings.h" #include "editor/editor_string_names.h" #include "editor/editor_undo_redo_manager.h" #include "editor/gui/scene_tree_editor.h" #include "editor/node_dock.h" #include "editor/scene_tree_dock.h" #include "editor/themes/editor_scale.h" #include "plugins/script_editor_plugin.h" #include "scene/gui/button.h" #include "scene/gui/check_box.h" #include "scene/gui/label.h" #include "scene/gui/line_edit.h" #include "scene/gui/option_button.h" #include "scene/gui/popup_menu.h" #include "scene/gui/spin_box.h" #include "scene/resources/packed_scene.h" static Node *_find_first_script(Node *p_root, Node *p_node) { if (p_node != p_root && p_node->get_owner() != p_root) { return nullptr; } if (!p_node->get_script().is_null()) { return p_node; } for (int i = 0; i < p_node->get_child_count(); i++) { Node *ret = _find_first_script(p_root, p_node->get_child(i)); if (ret) { return ret; } } return nullptr; } class ConnectDialogBinds : public Object { GDCLASS(ConnectDialogBinds, Object); public: Vector params; bool _set(const StringName &p_name, const Variant &p_value) { String name = p_name; if (name.begins_with("bind/argument_")) { int which = name.get_slice("_", 1).to_int() - 1; ERR_FAIL_INDEX_V(which, params.size(), false); params.write[which] = p_value; } else { return false; } return true; } bool _get(const StringName &p_name, Variant &r_ret) const { String name = p_name; if (name.begins_with("bind/argument_")) { int which = name.get_slice("_", 1).to_int() - 1; ERR_FAIL_INDEX_V(which, params.size(), false); r_ret = params[which]; } else { return false; } return true; } void _get_property_list(List *p_list) const { for (int i = 0; i < params.size(); i++) { p_list->push_back(PropertyInfo(params[i].get_type(), "bind/argument_" + itos(i + 1))); } } void notify_changed() { notify_property_list_changed(); } ConnectDialogBinds() { } }; /* * Signal automatically called by parent dialog. */ void ConnectDialog::ok_pressed() { String method_name = dst_method->get_text(); if (method_name.is_empty()) { error->set_text(TTR("Method in target node must be specified.")); error->popup_centered(); return; } if (!TS->is_valid_identifier(method_name.strip_edges())) { error->set_text(TTR("Method name must be a valid identifier.")); error->popup_centered(); return; } Node *target = tree->get_selected(); if (!target) { return; // Nothing selected in the tree, not an error. } if (target->get_script().is_null()) { if (!target->has_method(method_name)) { error->set_text(TTR("Target method not found. Specify a valid method or attach a script to the target node.")); error->popup_centered(); return; } } emit_signal(SNAME("connected")); hide(); } void ConnectDialog::_cancel_pressed() { hide(); } void ConnectDialog::_item_activated() { _ok_pressed(); // From AcceptDialog. } void ConnectDialog::_text_submitted(const String &p_text) { _ok_pressed(); // From AcceptDialog. } /* * Called each time a target node is selected within the target node tree. */ void ConnectDialog::_tree_node_selected() { Node *current = tree->get_selected(); if (!current) { return; } dst_path = source->get_path_to(current); if (!edit_mode) { set_dst_method(generate_method_callback_name(source, signal, current)); } _update_method_tree(); _update_ok_enabled(); } void ConnectDialog::_focus_currently_connected() { tree->set_selected(source); } void ConnectDialog::_unbind_count_changed(double p_count) { for (Control *control : bind_controls) { BaseButton *b = Object::cast_to(control); if (b) { b->set_disabled(p_count > 0); } EditorInspector *e = Object::cast_to(control); if (e) { e->set_read_only(p_count > 0); } } } void ConnectDialog::_method_selected() { TreeItem *selected_item = method_tree->get_selected(); dst_method->set_text(selected_item->get_metadata(0)); } /* * Adds a new parameter bind to connection. */ void ConnectDialog::_add_bind() { Variant::Type type = (Variant::Type)type_list->get_item_id(type_list->get_selected()); Variant value; Callable::CallError err; Variant::construct(type, value, nullptr, 0, err); cdbinds->params.push_back(value); cdbinds->notify_changed(); } /* * Remove parameter bind from connection. */ void ConnectDialog::_remove_bind() { String st = bind_editor->get_selected_path(); if (st.is_empty()) { return; } int idx = st.get_slice("/", 1).to_int() - 1; ERR_FAIL_INDEX(idx, cdbinds->params.size()); cdbinds->params.remove_at(idx); cdbinds->notify_changed(); } /* * Automatically generates a name for the callback method. */ StringName ConnectDialog::generate_method_callback_name(Node *p_source, String p_signal_name, Node *p_target) { String node_name = p_source->get_name(); for (int i = 0; i < node_name.length(); i++) { // TODO: Regex filter may be cleaner. char32_t c = node_name[i]; if ((i == 0 && !is_unicode_identifier_start(c)) || (i > 0 && !is_unicode_identifier_continue(c))) { if (c == ' ') { // Replace spaces with underlines. c = '_'; } else { // Remove any other characters. node_name.remove_at(i); i--; continue; } } node_name[i] = c; } Dictionary subst; subst["NodeName"] = node_name.to_pascal_case(); subst["nodeName"] = node_name.to_camel_case(); subst["node_name"] = node_name.to_snake_case(); subst["SignalName"] = p_signal_name.to_pascal_case(); subst["signalName"] = p_signal_name.to_camel_case(); subst["signal_name"] = p_signal_name.to_snake_case(); String dst_method; if (p_source == p_target) { dst_method = String(GLOBAL_GET("editor/naming/default_signal_callback_to_self_name")).format(subst); } else { dst_method = String(GLOBAL_GET("editor/naming/default_signal_callback_name")).format(subst); } return dst_method; } void ConnectDialog::_create_method_tree_items(const List &p_methods, TreeItem *p_parent_item) { for (const MethodInfo &mi : p_methods) { TreeItem *method_item = method_tree->create_item(p_parent_item); method_item->set_text(0, get_signature(mi)); method_item->set_metadata(0, mi.name); } } List ConnectDialog::_filter_method_list(const List &p_methods, const MethodInfo &p_signal, const String &p_search_string) const { bool check_signal = compatible_methods_only->is_pressed(); List ret; for (const MethodInfo &mi : p_methods) { if (!p_search_string.is_empty() && !mi.name.contains(p_search_string)) { continue; } if (check_signal) { if (mi.arguments.size() != p_signal.arguments.size()) { continue; } bool type_mismatch = false; const List::Element *E = p_signal.arguments.front(); for (const List::Element *F = mi.arguments.front(); F; F = F->next(), E = E->next()) { Variant::Type stype = E->get().type; Variant::Type mtype = F->get().type; if (stype != Variant::NIL && mtype != Variant::NIL && stype != mtype) { type_mismatch = true; break; } if (stype == Variant::OBJECT && mtype == Variant::OBJECT && !ClassDB::is_parent_class(E->get().class_name, F->get().class_name)) { type_mismatch = true; break; } } if (type_mismatch) { continue; } } ret.push_back(mi); } return ret; } void ConnectDialog::_update_method_tree() { method_tree->clear(); Color disabled_color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor)) * 0.7; String search_string = method_search->get_text(); Node *target = tree->get_selected(); if (!target) { return; } MethodInfo signal_info; if (compatible_methods_only->is_pressed()) { List signals; source->get_signal_list(&signals); for (const MethodInfo &mi : signals) { if (mi.name == signal) { signal_info = mi; break; } } } TreeItem *root_item = method_tree->create_item(); root_item->set_text(0, TTR("Methods")); root_item->set_selectable(0, false); // If a script is attached, get methods from it. ScriptInstance *si = target->get_script_instance(); if (si) { if (si->get_script()->is_built_in()) { si->get_script()->reload(); } List methods; si->get_method_list(&methods); methods = _filter_method_list(methods, signal_info, search_string); if (!methods.is_empty()) { TreeItem *si_item = method_tree->create_item(root_item); si_item->set_text(0, TTR("Attached Script")); si_item->set_icon(0, get_editor_theme_icon(SNAME("Script"))); si_item->set_selectable(0, false); _create_method_tree_items(methods, si_item); } } if (script_methods_only->is_pressed()) { empty_tree_label->set_visible(root_item->get_first_child() == nullptr); return; } // Get methods from each class in the hierarchy. StringName current_class = target->get_class_name(); do { TreeItem *class_item = method_tree->create_item(root_item); class_item->set_text(0, current_class); Ref icon = get_editor_theme_icon(SNAME("Node")); if (has_theme_icon(current_class, EditorStringName(EditorIcons))) { icon = get_editor_theme_icon(current_class); } class_item->set_icon(0, icon); class_item->set_selectable(0, false); List methods; ClassDB::get_method_list(current_class, &methods, true); methods = _filter_method_list(methods, signal_info, search_string); if (methods.is_empty()) { class_item->set_custom_color(0, disabled_color); } else { _create_method_tree_items(methods, class_item); } current_class = ClassDB::get_parent_class_nocheck(current_class); } while (current_class != StringName()); empty_tree_label->set_visible(root_item->get_first_child() == nullptr); } void ConnectDialog::_method_check_button_pressed(const CheckButton *p_button) { if (p_button == script_methods_only) { EditorSettings::get_singleton()->set_project_metadata("editor_metadata", "show_script_methods_only", p_button->is_pressed()); } else if (p_button == compatible_methods_only) { EditorSettings::get_singleton()->set_project_metadata("editor_metadata", "show_compatible_methods_only", p_button->is_pressed()); } _update_method_tree(); } void ConnectDialog::_open_method_popup() { method_popup->popup_centered(); method_search->clear(); method_search->grab_focus(); } /* * Enables or disables the connect button. The connect button is enabled if a * node is selected and valid in the selected mode. */ void ConnectDialog::_update_ok_enabled() { Node *target = tree->get_selected(); if (target == nullptr) { get_ok_button()->set_disabled(true); return; } if (dst_method->get_text().is_empty()) { get_ok_button()->set_disabled(true); return; } get_ok_button()->set_disabled(false); } void ConnectDialog::_notification(int p_what) { switch (p_what) { case NOTIFICATION_ENTER_TREE: { bind_editor->edit(cdbinds); [[fallthrough]]; } case NOTIFICATION_THEME_CHANGED: { for (int i = 0; i < type_list->get_item_count(); i++) { String type_name = Variant::get_type_name((Variant::Type)type_list->get_item_id(i)); type_list->set_item_icon(i, get_editor_theme_icon(type_name)); } Ref style = get_theme_stylebox("normal", "LineEdit")->duplicate(); if (style.is_valid()) { style->set_content_margin(SIDE_TOP, style->get_content_margin(SIDE_TOP) + 1.0); from_signal->add_theme_style_override("normal", style); } method_search->set_right_icon(get_editor_theme_icon("Search")); open_method_tree->set_icon(get_editor_theme_icon("Edit")); } break; } } void ConnectDialog::_bind_methods() { ADD_SIGNAL(MethodInfo("connected")); } Node *ConnectDialog::get_source() const { return source; } ConnectDialog::ConnectionData ConnectDialog::get_source_connection_data() const { return source_connection_data; } StringName ConnectDialog::get_signal_name() const { return signal; } PackedStringArray ConnectDialog::get_signal_args() const { return signal_args; } NodePath ConnectDialog::get_dst_path() const { return dst_path; } void ConnectDialog::set_dst_node(Node *p_node) { tree->set_selected(p_node); } StringName ConnectDialog::get_dst_method_name() const { String txt = dst_method->get_text(); if (txt.contains("(")) { txt = txt.left(txt.find("(")).strip_edges(); } return txt; } void ConnectDialog::set_dst_method(const StringName &p_method) { dst_method->set_text(p_method); } int ConnectDialog::get_unbinds() const { return int(unbind_count->get_value()); } Vector ConnectDialog::get_binds() const { return cdbinds->params; } String ConnectDialog::get_signature(const MethodInfo &p_method, PackedStringArray *r_arg_names) { PackedStringArray signature; signature.append(p_method.name); signature.append("("); for (int i = 0; i < p_method.arguments.size(); i++) { if (i > 0) { signature.append(", "); } const PropertyInfo &pi = p_method.arguments[i]; String type_name; switch (pi.type) { case Variant::NIL: type_name = "Variant"; break; case Variant::INT: if ((pi.usage & PROPERTY_USAGE_CLASS_IS_ENUM) && pi.class_name != StringName() && !String(pi.class_name).begins_with("res://")) { type_name = pi.class_name; } else { type_name = "int"; } break; case Variant::ARRAY: if (pi.hint == PROPERTY_HINT_ARRAY_TYPE && !pi.hint_string.is_empty() && !pi.hint_string.begins_with("res://")) { type_name = "Array[" + pi.hint_string + "]"; } else { type_name = "Array"; } break; case Variant::OBJECT: if (pi.class_name != StringName()) { type_name = pi.class_name; } else { type_name = "Object"; } break; default: type_name = Variant::get_type_name(pi.type); break; } String arg_name = pi.name.is_empty() ? "arg" + itos(i) : pi.name; signature.append(arg_name + ": " + type_name); if (r_arg_names) { r_arg_names->push_back(arg_name + ":" + type_name); } } signature.append(")"); return String().join(signature); } bool ConnectDialog::get_deferred() const { return deferred->is_pressed(); } bool ConnectDialog::get_one_shot() const { return one_shot->is_pressed(); } /* * Returns true if ConnectDialog is being used to edit an existing connection. */ bool ConnectDialog::is_editing() const { return edit_mode; } /* * Initialize ConnectDialog and populate fields with expected data. * If creating a connection from scratch, sensible defaults are used. * If editing an existing connection, previous data is retained. */ void ConnectDialog::init(const ConnectionData &p_cd, const PackedStringArray &p_signal_args, bool p_edit) { set_hide_on_ok(false); source = static_cast(p_cd.source); signal = p_cd.signal; signal_args = p_signal_args; tree->set_selected(nullptr); tree->set_marked(source, true); if (p_cd.target) { set_dst_node(static_cast(p_cd.target)); set_dst_method(p_cd.method); } _update_ok_enabled(); bool b_deferred = (p_cd.flags & CONNECT_DEFERRED) == CONNECT_DEFERRED; bool b_oneshot = (p_cd.flags & CONNECT_ONE_SHOT) == CONNECT_ONE_SHOT; deferred->set_pressed(b_deferred); one_shot->set_pressed(b_oneshot); unbind_count->set_max(p_signal_args.size()); unbind_count->set_value(p_cd.unbinds); _unbind_count_changed(p_cd.unbinds); cdbinds->params.clear(); cdbinds->params = p_cd.binds; cdbinds->notify_changed(); edit_mode = p_edit; source_connection_data = p_cd; } void ConnectDialog::popup_dialog(const String p_for_signal) { from_signal->set_text(p_for_signal); error_label->add_theme_color_override("font_color", error_label->get_theme_color(SNAME("error_color"), EditorStringName(Editor))); filter_nodes->clear(); if (!advanced->is_pressed()) { error_label->set_visible(!_find_first_script(get_tree()->get_edited_scene_root(), get_tree()->get_edited_scene_root())); } if (first_popup) { first_popup = false; _advanced_pressed(); } popup_centered(); } void ConnectDialog::_advanced_pressed() { if (advanced->is_pressed()) { connect_to_label->set_text(TTR("Connect to Node:")); tree->set_connect_to_script_mode(false); vbc_right->show(); error_label->hide(); } else { reset_size(); connect_to_label->set_text(TTR("Connect to Script:")); tree->set_connect_to_script_mode(true); vbc_right->hide(); error_label->set_visible(!_find_first_script(get_tree()->get_edited_scene_root(), get_tree()->get_edited_scene_root())); } EditorSettings::get_singleton()->set_project_metadata("editor_metadata", "use_advanced_connections", advanced->is_pressed()); popup_centered(); } ConnectDialog::ConnectDialog() { set_min_size(Size2(0, 500) * EDSCALE); HBoxContainer *main_hb = memnew(HBoxContainer); add_child(main_hb); VBoxContainer *vbc_left = memnew(VBoxContainer); main_hb->add_child(vbc_left); vbc_left->set_h_size_flags(Control::SIZE_EXPAND_FILL); vbc_left->set_custom_minimum_size(Vector2(400 * EDSCALE, 0)); from_signal = memnew(LineEdit); vbc_left->add_margin_child(TTR("From Signal:"), from_signal); from_signal->set_editable(false); tree = memnew(SceneTreeEditor(false)); tree->set_connecting_signal(true); tree->set_show_enabled_subscene(true); tree->set_v_size_flags(Control::SIZE_FILL | Control::SIZE_EXPAND); tree->get_scene_tree()->connect("item_activated", callable_mp(this, &ConnectDialog::_item_activated)); tree->connect("node_selected", callable_mp(this, &ConnectDialog::_tree_node_selected)); tree->set_connect_to_script_mode(true); HBoxContainer *hbc_filter = memnew(HBoxContainer); filter_nodes = memnew(LineEdit); hbc_filter->add_child(filter_nodes); filter_nodes->set_h_size_flags(Control::SIZE_FILL | Control::SIZE_EXPAND); filter_nodes->set_placeholder(TTR("Filter Nodes")); filter_nodes->set_clear_button_enabled(true); filter_nodes->connect("text_changed", callable_mp(tree, &SceneTreeEditor::set_filter)); Button *focus_current = memnew(Button); hbc_filter->add_child(focus_current); focus_current->set_text(TTR("Go to Source")); focus_current->connect("pressed", callable_mp(this, &ConnectDialog::_focus_currently_connected)); Node *mc = vbc_left->add_margin_child(TTR("Connect to Script:"), hbc_filter, false); connect_to_label = Object::cast_to