/*************************************************************************/ /* visual_script_editor.cpp */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ /* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ /* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* 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 "visual_script_editor.h" #include "../visual_script_expression.h" #include "../visual_script_flow_control.h" #include "../visual_script_func_nodes.h" #include "../visual_script_nodes.h" #include "core/input/input.h" #include "core/object/class_db.h" #include "core/object/script_language.h" #include "core/os/keyboard.h" #include "core/variant/variant.h" #include "editor/editor_node.h" #include "editor/editor_resource_preview.h" #include "editor/editor_scale.h" #include "scene/main/window.h" #ifdef TOOLS_ENABLED class VisualScriptEditorSignalEdit : public Object { GDCLASS(VisualScriptEditorSignalEdit, Object); StringName sig; public: UndoRedo *undo_redo; Ref script; protected: static void _bind_methods() { ClassDB::bind_method("_sig_changed", &VisualScriptEditorSignalEdit::_sig_changed); ADD_SIGNAL(MethodInfo("changed")); } void _sig_changed() { notify_property_list_changed(); emit_signal(SNAME("changed")); } bool _set(const StringName &p_name, const Variant &p_value) { if (sig == StringName()) { return false; } if (p_name == "argument_count") { int new_argc = p_value; int argc = script->custom_signal_get_argument_count(sig); if (argc == new_argc) { return true; } undo_redo->create_action(TTR("Change Signal Arguments")); if (new_argc < argc) { for (int i = new_argc; i < argc; i++) { undo_redo->add_do_method(script.ptr(), "custom_signal_remove_argument", sig, new_argc); undo_redo->add_undo_method(script.ptr(), "custom_signal_add_argument", sig, script->custom_signal_get_argument_name(sig, i), script->custom_signal_get_argument_type(sig, i), -1); } } else if (new_argc > argc) { for (int i = argc; i < new_argc; i++) { undo_redo->add_do_method(script.ptr(), "custom_signal_add_argument", sig, Variant::NIL, "arg" + itos(i + 1), -1); undo_redo->add_undo_method(script.ptr(), "custom_signal_remove_argument", sig, argc); } } undo_redo->add_do_method(this, "_sig_changed"); undo_redo->add_undo_method(this, "_sig_changed"); undo_redo->commit_action(); return true; } if (String(p_name).begins_with("argument/")) { int idx = String(p_name).get_slice("/", 1).to_int() - 1; ERR_FAIL_INDEX_V(idx, script->custom_signal_get_argument_count(sig), false); String what = String(p_name).get_slice("/", 2); if (what == "type") { int old_type = script->custom_signal_get_argument_type(sig, idx); int new_type = p_value; undo_redo->create_action(TTR("Change Argument Type")); undo_redo->add_do_method(script.ptr(), "custom_signal_set_argument_type", sig, idx, new_type); undo_redo->add_undo_method(script.ptr(), "custom_signal_set_argument_type", sig, idx, old_type); undo_redo->commit_action(); return true; } if (what == "name") { String old_name = script->custom_signal_get_argument_name(sig, idx); String new_name = p_value; undo_redo->create_action(TTR("Change Argument name")); undo_redo->add_do_method(script.ptr(), "custom_signal_set_argument_name", sig, idx, new_name); undo_redo->add_undo_method(script.ptr(), "custom_signal_set_argument_name", sig, idx, old_name); undo_redo->commit_action(); return true; } } return false; } bool _get(const StringName &p_name, Variant &r_ret) const { if (sig == StringName()) { return false; } if (p_name == "argument_count") { r_ret = script->custom_signal_get_argument_count(sig); return true; } if (String(p_name).begins_with("argument/")) { int idx = String(p_name).get_slice("/", 1).to_int() - 1; ERR_FAIL_INDEX_V(idx, script->custom_signal_get_argument_count(sig), false); String what = String(p_name).get_slice("/", 2); if (what == "type") { r_ret = script->custom_signal_get_argument_type(sig, idx); return true; } if (what == "name") { r_ret = script->custom_signal_get_argument_name(sig, idx); return true; } } return false; } void _get_property_list(List *p_list) const { if (sig == StringName()) { return; } p_list->push_back(PropertyInfo(Variant::INT, "argument_count", PROPERTY_HINT_RANGE, "0,256")); String argt = "Variant"; for (int i = 1; i < Variant::VARIANT_MAX; i++) { argt += "," + Variant::get_type_name(Variant::Type(i)); } for (int i = 0; i < script->custom_signal_get_argument_count(sig); i++) { p_list->push_back(PropertyInfo(Variant::INT, "argument/" + itos(i + 1) + "/type", PROPERTY_HINT_ENUM, argt)); p_list->push_back(PropertyInfo(Variant::STRING, "argument/" + itos(i + 1) + "/name")); } } public: void edit(const StringName &p_sig) { sig = p_sig; notify_property_list_changed(); } VisualScriptEditorSignalEdit() { undo_redo = nullptr; } }; class VisualScriptEditorVariableEdit : public Object { GDCLASS(VisualScriptEditorVariableEdit, Object); StringName var; public: UndoRedo *undo_redo; Ref script; protected: static void _bind_methods() { ClassDB::bind_method("_var_changed", &VisualScriptEditorVariableEdit::_var_changed); ClassDB::bind_method("_var_value_changed", &VisualScriptEditorVariableEdit::_var_value_changed); ADD_SIGNAL(MethodInfo("changed")); } void _var_changed() { notify_property_list_changed(); emit_signal(SNAME("changed")); } void _var_value_changed() { emit_signal(SNAME("changed")); } bool _set(const StringName &p_name, const Variant &p_value) { if (var == StringName()) { return false; } if (String(p_name) == "value") { undo_redo->create_action(TTR("Set Variable Default Value")); Variant current = script->get_variable_default_value(var); undo_redo->add_do_method(script.ptr(), "set_variable_default_value", var, p_value); undo_redo->add_undo_method(script.ptr(), "set_variable_default_value", var, current); undo_redo->add_do_method(this, "_var_value_changed"); undo_redo->add_undo_method(this, "_var_value_changed"); undo_redo->commit_action(); return true; } Dictionary d = script->call("get_variable_info", var); if (String(p_name) == "type") { Dictionary dc = d.duplicate(); dc["type"] = p_value; undo_redo->create_action(TTR("Set Variable Type")); undo_redo->add_do_method(script.ptr(), "set_variable_info", var, dc); undo_redo->add_undo_method(script.ptr(), "set_variable_info", var, d); // Setting the default value. Variant::Type type = (Variant::Type)(int)p_value; if (type != Variant::NIL) { Variant default_value; Callable::CallError ce; Variant::construct(type, default_value, nullptr, 0, ce); if (ce.error == Callable::CallError::CALL_OK) { undo_redo->add_do_method(script.ptr(), "set_variable_default_value", var, default_value); undo_redo->add_undo_method(script.ptr(), "set_variable_default_value", var, dc["value"]); } } undo_redo->add_do_method(this, "_var_changed"); undo_redo->add_undo_method(this, "_var_changed"); undo_redo->commit_action(); return true; } if (String(p_name) == "hint") { Dictionary dc = d.duplicate(); dc["hint"] = p_value; undo_redo->create_action(TTR("Set Variable Type")); undo_redo->add_do_method(script.ptr(), "set_variable_info", var, dc); undo_redo->add_undo_method(script.ptr(), "set_variable_info", var, d); undo_redo->add_do_method(this, "_var_changed"); undo_redo->add_undo_method(this, "_var_changed"); undo_redo->commit_action(); return true; } if (String(p_name) == "hint_string") { Dictionary dc = d.duplicate(); dc["hint_string"] = p_value; undo_redo->create_action(TTR("Set Variable Type")); undo_redo->add_do_method(script.ptr(), "set_variable_info", var, dc); undo_redo->add_undo_method(script.ptr(), "set_variable_info", var, d); undo_redo->add_do_method(this, "_var_changed"); undo_redo->add_undo_method(this, "_var_changed"); undo_redo->commit_action(); return true; } if (String(p_name) == "export") { script->set_variable_export(var, p_value); EditorNode::get_singleton()->get_inspector()->update_tree(); return true; } return false; } bool _get(const StringName &p_name, Variant &r_ret) const { if (var == StringName()) { return false; } if (String(p_name) == "value") { r_ret = script->get_variable_default_value(var); return true; } PropertyInfo pinfo = script->get_variable_info(var); if (String(p_name) == "type") { r_ret = pinfo.type; return true; } if (String(p_name) == "hint") { r_ret = pinfo.hint; return true; } if (String(p_name) == "hint_string") { r_ret = pinfo.hint_string; return true; } if (String(p_name) == "export") { r_ret = script->get_variable_export(var); return true; } return false; } void _get_property_list(List *p_list) const { if (var == StringName()) { return; } String argt = "Variant"; for (int i = 1; i < Variant::VARIANT_MAX; i++) { argt += "," + Variant::get_type_name(Variant::Type(i)); } p_list->push_back(PropertyInfo(Variant::INT, "type", PROPERTY_HINT_ENUM, argt)); p_list->push_back(PropertyInfo(script->get_variable_info(var).type, "value", script->get_variable_info(var).hint, script->get_variable_info(var).hint_string, PROPERTY_USAGE_DEFAULT)); // Update this when PropertyHint changes. p_list->push_back(PropertyInfo(Variant::INT, "hint", PROPERTY_HINT_ENUM, "None,Range,ExpRange,Enum,ExpEasing,Length,SpriteFrame,KeyAccel,Flags,Layers2dRender,Layers2dPhysics,Layer3dRender,Layer3dPhysics,File,Dir,GlobalFile,GlobalDir,ResourceType,MultilineText,PlaceholderText,ColorNoAlpha,ImageCompressLossy,ImageCompressLossLess,ObjectId,String,NodePathToEditedNode,MethodOfVariantType,MethodOfBaseType,MethodOfInstance,MethodOfScript,PropertyOfVariantType,PropertyOfBaseType,PropertyOfInstance,PropertyOfScript,ObjectTooBig,NodePathValidTypes")); p_list->push_back(PropertyInfo(Variant::STRING, "hint_string")); p_list->push_back(PropertyInfo(Variant::BOOL, "export")); } public: void edit(const StringName &p_var) { var = p_var; notify_property_list_changed(); } VisualScriptEditorVariableEdit() { undo_redo = nullptr; } }; static Color _color_from_type(Variant::Type p_type, bool dark_theme = true) { Color color; if (dark_theme) { switch (p_type) { case Variant::NIL: color = Color(0.41, 0.93, 0.74); break; case Variant::BOOL: color = Color(0.55, 0.65, 0.94); break; case Variant::INT: color = Color(0.49, 0.78, 0.94); break; case Variant::FLOAT: color = Color(0.38, 0.85, 0.96); break; case Variant::STRING: color = Color(0.42, 0.65, 0.93); break; case Variant::VECTOR2: color = Color(0.74, 0.57, 0.95); break; case Variant::VECTOR2I: color = Color(0.74, 0.57, 0.95); break; case Variant::RECT2: color = Color(0.95, 0.57, 0.65); break; case Variant::RECT2I: color = Color(0.95, 0.57, 0.65); break; case Variant::VECTOR3: color = Color(0.84, 0.49, 0.93); break; case Variant::VECTOR3I: color = Color(0.84, 0.49, 0.93); break; case Variant::TRANSFORM2D: color = Color(0.77, 0.93, 0.41); break; case Variant::PLANE: color = Color(0.97, 0.44, 0.44); break; case Variant::QUATERNION: color = Color(0.93, 0.41, 0.64); break; case Variant::AABB: color = Color(0.93, 0.47, 0.57); break; case Variant::BASIS: color = Color(0.89, 0.93, 0.41); break; case Variant::TRANSFORM3D: color = Color(0.96, 0.66, 0.43); break; case Variant::COLOR: color = Color(0.62, 1.0, 0.44); break; case Variant::NODE_PATH: color = Color(0.41, 0.58, 0.93); break; case Variant::RID: color = Color(0.41, 0.93, 0.6); break; case Variant::OBJECT: color = Color(0.47, 0.95, 0.91); break; case Variant::DICTIONARY: color = Color(0.47, 0.93, 0.69); break; case Variant::ARRAY: color = Color(0.88, 0.88, 0.88); break; case Variant::PACKED_BYTE_ARRAY: color = Color(0.67, 0.96, 0.78); break; case Variant::PACKED_INT32_ARRAY: color = Color(0.69, 0.86, 0.96); break; case Variant::PACKED_FLOAT32_ARRAY: color = Color(0.59, 0.91, 0.97); break; case Variant::PACKED_INT64_ARRAY: color = Color(0.69, 0.86, 0.96); break; case Variant::PACKED_FLOAT64_ARRAY: color = Color(0.59, 0.91, 0.97); break; case Variant::PACKED_STRING_ARRAY: color = Color(0.62, 0.77, 0.95); break; case Variant::PACKED_VECTOR2_ARRAY: color = Color(0.82, 0.7, 0.96); break; case Variant::PACKED_VECTOR3_ARRAY: color = Color(0.87, 0.61, 0.95); break; case Variant::PACKED_COLOR_ARRAY: color = Color(0.91, 1.0, 0.59); break; default: color.set_hsv(p_type / float(Variant::VARIANT_MAX), 0.7, 0.7); } } else { switch (p_type) { case Variant::NIL: color = Color(0.15, 0.89, 0.63); break; case Variant::BOOL: color = Color(0.43, 0.56, 0.92); break; case Variant::INT: color = Color(0.31, 0.7, 0.91); break; case Variant::FLOAT: color = Color(0.15, 0.8, 0.94); break; case Variant::STRING: color = Color(0.27, 0.56, 0.91); break; case Variant::VECTOR2: color = Color(0.68, 0.46, 0.93); break; case Variant::VECTOR2I: color = Color(0.68, 0.46, 0.93); break; case Variant::RECT2: color = Color(0.93, 0.46, 0.56); break; case Variant::RECT2I: color = Color(0.93, 0.46, 0.56); break; case Variant::VECTOR3: color = Color(0.86, 0.42, 0.93); break; case Variant::VECTOR3I: color = Color(0.86, 0.42, 0.93); break; case Variant::TRANSFORM2D: color = Color(0.59, 0.81, 0.1); break; case Variant::PLANE: color = Color(0.97, 0.44, 0.44); break; case Variant::QUATERNION: color = Color(0.93, 0.41, 0.64); break; case Variant::AABB: color = Color(0.93, 0.47, 0.57); break; case Variant::BASIS: color = Color(0.7, 0.73, 0.1); break; case Variant::TRANSFORM3D: color = Color(0.96, 0.56, 0.28); break; case Variant::COLOR: color = Color(0.24, 0.75, 0.0); break; case Variant::NODE_PATH: color = Color(0.41, 0.58, 0.93); break; case Variant::RID: color = Color(0.17, 0.9, 0.45); break; case Variant::OBJECT: color = Color(0.07, 0.84, 0.76); break; case Variant::DICTIONARY: color = Color(0.34, 0.91, 0.62); break; case Variant::ARRAY: color = Color(0.45, 0.45, 0.45); break; case Variant::PACKED_BYTE_ARRAY: color = Color(0.38, 0.92, 0.6); break; case Variant::PACKED_INT32_ARRAY: color = Color(0.38, 0.73, 0.92); break; case Variant::PACKED_FLOAT32_ARRAY: color = Color(0.25, 0.83, 0.95); break; case Variant::PACKED_INT64_ARRAY: color = Color(0.38, 0.73, 0.92); break; case Variant::PACKED_FLOAT64_ARRAY: color = Color(0.25, 0.83, 0.95); break; case Variant::PACKED_STRING_ARRAY: color = Color(0.38, 0.62, 0.92); break; case Variant::PACKED_VECTOR2_ARRAY: color = Color(0.62, 0.36, 0.92); break; case Variant::PACKED_VECTOR3_ARRAY: color = Color(0.79, 0.35, 0.92); break; case Variant::PACKED_COLOR_ARRAY: color = Color(0.57, 0.73, 0.0); break; default: color.set_hsv(p_type / float(Variant::VARIANT_MAX), 0.3, 0.3); } } return color; } void VisualScriptEditor::_update_graph_connections() { graph->clear_connections(); List sequence_conns; script->get_sequence_connection_list(&sequence_conns); for (const VisualScript::SequenceConnection &E : sequence_conns) { graph->connect_node(itos(E.from_node), E.from_output, itos(E.to_node), 0); } List data_conns; script->get_data_connection_list(&data_conns); for (VisualScript::DataConnection &dc : data_conns) { Ref from_node = script->get_node(dc.from_node); Ref to_node = script->get_node(dc.to_node); if (to_node->has_input_sequence_port()) { dc.to_port++; } dc.from_port += from_node->get_output_sequence_port_count(); graph->connect_node(itos(dc.from_node), dc.from_port, itos(dc.to_node), dc.to_port); } } void VisualScriptEditor::_update_graph(int p_only_id) { if (updating_graph) { return; } updating_graph = true; //byebye all nodes if (p_only_id >= 0) { if (graph->has_node(itos(p_only_id))) { Node *gid = graph->get_node(itos(p_only_id)); if (gid) { memdelete(gid); } } } else { for (int i = 0; i < graph->get_child_count(); i++) { if (Object::cast_to(graph->get_child(i))) { memdelete(graph->get_child(i)); i--; } } } graph->show(); select_func_text->hide(); Ref type_icons[Variant::VARIANT_MAX] = { Control::get_theme_icon(SNAME("Variant"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("bool"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("int"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("float"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("String"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("Vector2"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("Vector2i"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("Rect2"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("Rect2i"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("Vector3"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("Vector3i"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("Transform2D"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("Plane"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("Quaternion"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("AABB"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("Basis"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("Transform3D"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("Color"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("StringName"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("NodePath"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("RID"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("MiniObject"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("Callable"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("Signal"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("Dictionary"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("Array"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("PackedByteArray"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("PackedInt32Array"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("PackedInt64Array"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("PackedFloat32Array"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("PackedFloat64Array"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("PackedStringArray"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("PackedVector2Array"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("PackedVector3Array"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("PackedColorArray"), SNAME("EditorIcons")) }; Ref seq_port = Control::get_theme_icon(SNAME("VisualShaderPort"), SNAME("EditorIcons")); List node_ids; script->get_node_list(&node_ids); List ids; script->get_node_list(&ids); StringName editor_icons = "EditorIcons"; for (int &E : ids) { if (p_only_id >= 0 && p_only_id != E) { continue; } Ref node = script->get_node(E); Vector2 pos = script->get_node_position(E); GraphNode *gnode = memnew(GraphNode); gnode->set_title(node->get_caption()); gnode->set_position_offset(pos * EDSCALE); if (error_line == E) { gnode->set_overlay(GraphNode::OVERLAY_POSITION); } else if (node->is_breakpoint()) { gnode->set_overlay(GraphNode::OVERLAY_BREAKPOINT); } gnode->set_meta("__vnode", node); gnode->set_name(itos(E)); gnode->connect("dragged", callable_mp(this, &VisualScriptEditor::_node_moved), varray(E)); gnode->connect("close_request", callable_mp(this, &VisualScriptEditor::_remove_node), varray(E), CONNECT_DEFERRED); { Ref v = node; if (!v.is_valid()) { gnode->set_show_close_button(true); } } bool has_gnode_text = false; Ref nd_list = node; bool is_vslist = nd_list.is_valid(); if (is_vslist) { HBoxContainer *hbnc = memnew(HBoxContainer); if (nd_list->is_input_port_editable()) { has_gnode_text = true; Button *btn = memnew(Button); btn->set_text(TTR("Add Input Port")); hbnc->add_child(btn); btn->connect("pressed", callable_mp(this, &VisualScriptEditor::_add_input_port), varray(E), CONNECT_DEFERRED); } if (nd_list->is_output_port_editable()) { if (nd_list->is_input_port_editable()) { hbnc->add_spacer(); } has_gnode_text = true; Button *btn = memnew(Button); btn->set_text(TTR("Add Output Port")); hbnc->add_child(btn); btn->connect("pressed", callable_mp(this, &VisualScriptEditor::_add_output_port), varray(E), CONNECT_DEFERRED); } gnode->add_child(hbnc); } else if (Object::cast_to(node.ptr())) { has_gnode_text = true; LineEdit *line_edit = memnew(LineEdit); line_edit->set_text(node->get_text()); line_edit->set_expand_to_text_length_enabled(true); line_edit->add_theme_font_override("font", get_theme_font(SNAME("source"), SNAME("EditorFonts"))); gnode->add_child(line_edit); line_edit->connect("text_changed", callable_mp(this, &VisualScriptEditor::_expression_text_changed), varray(E)); } else { String text = node->get_text(); if (!text.is_empty()) { has_gnode_text = true; Label *label = memnew(Label); label->set_text(text); gnode->add_child(label); } } if (Object::cast_to(node.ptr())) { Ref vsc = node; gnode->set_comment(true); gnode->set_resizable(true); gnode->set_custom_minimum_size(vsc->get_size() * EDSCALE); gnode->connect("resize_request", callable_mp(this, &VisualScriptEditor::_comment_node_resized), varray(E)); } if (node_styles.has(node->get_category())) { Ref sbf = node_styles[node->get_category()]; if (gnode->is_comment()) { sbf = EditorNode::get_singleton()->get_theme_base()->get_theme()->get_stylebox(SNAME("comment"), SNAME("GraphNode")); } Color c = sbf->get_border_color(); c = ((c.r + c.g + c.b) / 3) < 0.7 ? Color(1.0, 1.0, 1.0, 0.85) : Color(0.0, 0.0, 0.0, 0.85); Color ic = c; gnode->add_theme_color_override("title_color", c); c.a = 1; gnode->add_theme_color_override("close_color", c); gnode->add_theme_color_override("resizer_color", ic); gnode->add_theme_style_override("frame", sbf); } const Color mono_color = get_theme_color(SNAME("mono_color"), SNAME("Editor")); int slot_idx = 0; bool single_seq_output = node->get_output_sequence_port_count() == 1 && node->get_output_sequence_port_text(0) == String(); if ((node->has_input_sequence_port() || single_seq_output) || has_gnode_text) { // IF has_gnode_text is true BUT we have no sequence ports to draw (in here), // we still draw the disabled default ones to shift up the slots by one, // so the slots DON'T start with the content text. // IF has_gnode_text is false, but we DO want to draw default sequence ports, // we draw a dummy text to take up the position of the sequence nodes, so all the other ports are still aligned correctly. if (!has_gnode_text) { Label *dummy = memnew(Label); dummy->set_text(" "); gnode->add_child(dummy); } gnode->set_slot(0, node->has_input_sequence_port(), TYPE_SEQUENCE, mono_color, single_seq_output, TYPE_SEQUENCE, mono_color, seq_port, seq_port); slot_idx++; } int mixed_seq_ports = 0; if (!single_seq_output) { if (node->has_mixed_input_and_sequence_ports()) { mixed_seq_ports = node->get_output_sequence_port_count(); } else { for (int i = 0; i < node->get_output_sequence_port_count(); i++) { Label *text2 = memnew(Label); text2->set_text(node->get_output_sequence_port_text(i)); text2->set_align(Label::ALIGN_RIGHT); gnode->add_child(text2); gnode->set_slot(slot_idx, false, 0, Color(), true, TYPE_SEQUENCE, mono_color, seq_port, seq_port); slot_idx++; } } } for (int i = 0; i < MAX(node->get_output_value_port_count(), MAX(mixed_seq_ports, node->get_input_value_port_count())); i++) { bool left_ok = false; Variant::Type left_type = Variant::NIL; String left_name; if (i < node->get_input_value_port_count()) { PropertyInfo pi = node->get_input_value_port_info(i); left_ok = true; left_type = pi.type; left_name = pi.name; } bool right_ok = false; Variant::Type right_type = Variant::NIL; String right_name; if (i >= mixed_seq_ports && i < node->get_output_value_port_count() + mixed_seq_ports) { PropertyInfo pi = node->get_output_value_port_info(i - mixed_seq_ports); right_ok = true; right_type = pi.type; right_name = pi.name; } VBoxContainer *vbc = memnew(VBoxContainer); HBoxContainer *hbc = memnew(HBoxContainer); HBoxContainer *hbc2 = memnew(HBoxContainer); vbc->add_child(hbc); vbc->add_child(hbc2); if (left_ok) { Ref t; if (left_type >= 0 && left_type < Variant::VARIANT_MAX) { t = type_icons[left_type]; } if (t.is_valid()) { TextureRect *tf = memnew(TextureRect); tf->set_texture(t); tf->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED); hbc->add_child(tf); } if (is_vslist) { if (nd_list->is_input_port_name_editable()) { LineEdit *name_box = memnew(LineEdit); hbc->add_child(name_box); name_box->set_custom_minimum_size(Size2(60 * EDSCALE, 0)); name_box->set_text(left_name); name_box->set_expand_to_text_length_enabled(true); name_box->connect("resized", callable_mp(this, &VisualScriptEditor::_update_node_size), varray(E)); name_box->connect("focus_exited", callable_mp(this, &VisualScriptEditor::_port_name_focus_out), varray(name_box, E, i, true)); } else { hbc->add_child(memnew(Label(left_name))); } if (nd_list->is_input_port_type_editable()) { OptionButton *opbtn = memnew(OptionButton); for (int j = Variant::NIL; j < Variant::VARIANT_MAX; j++) { opbtn->add_item(Variant::get_type_name(Variant::Type(j))); } opbtn->select(left_type); opbtn->set_custom_minimum_size(Size2(100 * EDSCALE, 0)); hbc->add_child(opbtn); opbtn->connect("item_selected", callable_mp(this, &VisualScriptEditor::_change_port_type), varray(E, i, true), CONNECT_DEFERRED); } Button *rmbtn = memnew(Button); rmbtn->set_icon(EditorNode::get_singleton()->get_gui_base()->get_theme_icon(SNAME("Remove"), SNAME("EditorIcons"))); hbc->add_child(rmbtn); rmbtn->connect("pressed", callable_mp(this, &VisualScriptEditor::_remove_input_port), varray(E, i), CONNECT_DEFERRED); } else { hbc->add_child(memnew(Label(left_name))); } if (left_type != Variant::NIL && !script->is_input_value_port_connected(E, i)) { PropertyInfo pi = node->get_input_value_port_info(i); Button *button = memnew(Button); Variant value = node->get_default_input_value(i); if (value.get_type() != left_type) { //different type? for now convert //not the same, reconvert Callable::CallError ce; const Variant *existingp = &value; Variant::construct(left_type, value, &existingp, 1, ce); } if (left_type == Variant::COLOR) { button->set_custom_minimum_size(Size2(30, 0) * EDSCALE); button->connect("draw", callable_mp(this, &VisualScriptEditor::_draw_color_over_button), varray(button, value)); } else if (left_type == Variant::OBJECT && Ref(value).is_valid()) { Ref res = value; Array arr; arr.push_back(button->get_instance_id()); arr.push_back(String(value)); EditorResourcePreview::get_singleton()->queue_edited_resource_preview(res, this, "_button_resource_previewed", arr); } else if (pi.type == Variant::INT && pi.hint == PROPERTY_HINT_ENUM) { button->set_text(pi.hint_string.get_slice(",", value)); } else { button->set_text(value); } button->connect("pressed", callable_mp(this, &VisualScriptEditor::_default_value_edited), varray(button, E, i)); hbc2->add_child(button); } } else { Control *c = memnew(Control); c->set_custom_minimum_size(Size2(10, 0) * EDSCALE); hbc->add_child(c); } hbc->add_spacer(); hbc2->add_spacer(); if (i < mixed_seq_ports) { Label *text2 = memnew(Label); text2->set_text(node->get_output_sequence_port_text(i)); text2->set_align(Label::ALIGN_RIGHT); hbc->add_child(text2); } if (right_ok) { if (is_vslist) { Button *rmbtn = memnew(Button); rmbtn->set_icon(EditorNode::get_singleton()->get_gui_base()->get_theme_icon(SNAME("Remove"), SNAME("EditorIcons"))); hbc->add_child(rmbtn); rmbtn->connect("pressed", callable_mp(this, &VisualScriptEditor::_remove_output_port), varray(E, i), CONNECT_DEFERRED); if (nd_list->is_output_port_type_editable()) { OptionButton *opbtn = memnew(OptionButton); for (int j = Variant::NIL; j < Variant::VARIANT_MAX; j++) { opbtn->add_item(Variant::get_type_name(Variant::Type(j))); } opbtn->select(right_type); opbtn->set_custom_minimum_size(Size2(100 * EDSCALE, 0)); hbc->add_child(opbtn); opbtn->connect("item_selected", callable_mp(this, &VisualScriptEditor::_change_port_type), varray(E, i, false), CONNECT_DEFERRED); } if (nd_list->is_output_port_name_editable()) { LineEdit *name_box = memnew(LineEdit); hbc->add_child(name_box); name_box->set_custom_minimum_size(Size2(60 * EDSCALE, 0)); name_box->set_text(right_name); name_box->set_expand_to_text_length_enabled(true); name_box->connect("resized", callable_mp(this, &VisualScriptEditor::_update_node_size), varray(E)); name_box->connect("focus_exited", callable_mp(this, &VisualScriptEditor::_port_name_focus_out), varray(name_box, E, i, false)); } else { hbc->add_child(memnew(Label(right_name))); } } else { hbc->add_child(memnew(Label(right_name))); } Ref t; if (right_type >= 0 && right_type < Variant::VARIANT_MAX) { t = type_icons[right_type]; } if (t.is_valid()) { TextureRect *tf = memnew(TextureRect); tf->set_texture(t); tf->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED); hbc->add_child(tf); } } gnode->add_child(vbc); bool dark_theme = get_theme_constant(SNAME("dark_theme"), SNAME("Editor")); if (i < mixed_seq_ports) { gnode->set_slot(slot_idx, left_ok, left_type, _color_from_type(left_type, dark_theme), true, TYPE_SEQUENCE, mono_color, Ref(), seq_port); } else { gnode->set_slot(slot_idx, left_ok, left_type, _color_from_type(left_type, dark_theme), right_ok, right_type, _color_from_type(right_type, dark_theme)); } slot_idx++; } graph->add_child(gnode); if (gnode->is_comment()) { graph->move_child(gnode, 0); } } _update_graph_connections(); float graph_minimap_opacity = EditorSettings::get_singleton()->get("editors/visual_editors/minimap_opacity"); graph->set_minimap_opacity(graph_minimap_opacity); // Use default_func instead of default_func for now I think that should be good stop gap solution to ensure not breaking anything. graph->call_deferred(SNAME("set_scroll_ofs"), script->get_scroll() * EDSCALE); updating_graph = false; } void VisualScriptEditor::_change_port_type(int p_select, int p_id, int p_port, bool is_input) { Ref vsn = script->get_node(p_id); if (!vsn.is_valid()) { return; } undo_redo->create_action(TTR("Change Port Type")); if (is_input) { undo_redo->add_do_method(vsn.ptr(), "set_input_data_port_type", p_port, Variant::Type(p_select)); undo_redo->add_undo_method(vsn.ptr(), "set_input_data_port_type", p_port, vsn->get_input_value_port_info(p_port).type); } else { undo_redo->add_do_method(vsn.ptr(), "set_output_data_port_type", p_port, Variant::Type(p_select)); undo_redo->add_undo_method(vsn.ptr(), "set_output_data_port_type", p_port, vsn->get_output_value_port_info(p_port).type); } undo_redo->commit_action(); } void VisualScriptEditor::_update_node_size(int p_id) { Node *node = graph->get_node(itos(p_id)); if (Object::cast_to(node)) { Object::cast_to(node)->set_size(Vector2(1, 1)); // Shrink if text is smaller. } } void VisualScriptEditor::_port_name_focus_out(const Node *p_name_box, int p_id, int p_port, bool is_input) { Ref vsn = script->get_node(p_id); if (!vsn.is_valid()) { return; } String text; if (Object::cast_to(p_name_box)) { text = Object::cast_to(p_name_box)->get_text(); } else { return; } undo_redo->create_action(TTR("Change Port Name")); if (is_input) { undo_redo->add_do_method(vsn.ptr(), "set_input_data_port_name", p_port, text); undo_redo->add_undo_method(vsn.ptr(), "set_input_data_port_name", p_port, vsn->get_input_value_port_info(p_port).name); } else { undo_redo->add_do_method(vsn.ptr(), "set_output_data_port_name", p_port, text); undo_redo->add_undo_method(vsn.ptr(), "set_output_data_port_name", p_port, vsn->get_output_value_port_info(p_port).name); } undo_redo->commit_action(); } void VisualScriptEditor::_update_members() { ERR_FAIL_COND(!script.is_valid()); updating_members = true; members->clear(); TreeItem *root = members->create_item(); TreeItem *functions = members->create_item(root); functions->set_selectable(0, false); functions->set_text(0, TTR("Functions:")); functions->add_button(0, Control::get_theme_icon(SNAME("Override"), SNAME("EditorIcons")), 1, false, TTR("Override an existing built-in function.")); functions->add_button(0, Control::get_theme_icon(SNAME("Add"), SNAME("EditorIcons")), 0, false, TTR("Create a new function.")); functions->set_custom_color(0, Control::get_theme_color(SNAME("mono_color"), SNAME("Editor"))); List func_names; script->get_function_list(&func_names); func_names.sort_custom(); for (const StringName &E : func_names) { TreeItem *ti = members->create_item(functions); ti->set_text(0, E); ti->set_selectable(0, true); ti->set_metadata(0, E); ti->add_button(0, Control::get_theme_icon(SNAME("Edit"), SNAME("EditorIcons")), 0); if (selected == E) { ti->select(0); } } TreeItem *variables = members->create_item(root); variables->set_selectable(0, false); variables->set_text(0, TTR("Variables:")); variables->add_button(0, Control::get_theme_icon(SNAME("Add"), SNAME("EditorIcons")), -1, false, TTR("Create a new variable.")); variables->set_custom_color(0, Control::get_theme_color(SNAME("mono_color"), SNAME("Editor"))); Ref type_icons[Variant::VARIANT_MAX] = { Control::get_theme_icon(SNAME("Variant"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("bool"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("int"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("float"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("String"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("Vector2"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("Vector2i"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("Rect2"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("Rect2i"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("Vector3"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("Vector3i"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("Transform2D"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("Plane"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("Quaternion"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("AABB"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("Basis"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("Transform3D"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("Color"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("NodePath"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("RID"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("MiniObject"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("Callable"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("Signal"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("Dictionary"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("Array"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("PackedByteArray"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("PackedInt32Array"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("PackedFloat32Array"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("PackedStringArray"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("PackedVector2Array"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("PackedVector3Array"), SNAME("EditorIcons")), Control::get_theme_icon(SNAME("PackedColorArray"), SNAME("EditorIcons")) }; List var_names; script->get_variable_list(&var_names); for (const StringName &E : var_names) { TreeItem *ti = members->create_item(variables); ti->set_text(0, E); ti->set_suffix(0, "= " + _sanitized_variant_text(E)); ti->set_icon(0, type_icons[script->get_variable_info(E).type]); ti->set_selectable(0, true); ti->set_editable(0, true); ti->set_metadata(0, E); if (selected == E) { ti->select(0); } } TreeItem *_signals = members->create_item(root); _signals->set_selectable(0, false); _signals->set_text(0, TTR("Signals:")); _signals->add_button(0, Control::get_theme_icon(SNAME("Add"), SNAME("EditorIcons")), -1, false, TTR("Create a new signal.")); _signals->set_custom_color(0, Control::get_theme_color(SNAME("mono_color"), SNAME("Editor"))); List signal_names; script->get_custom_signal_list(&signal_names); for (const StringName &E : signal_names) { TreeItem *ti = members->create_item(_signals); ti->set_text(0, E); ti->set_selectable(0, true); ti->set_editable(0, true); ti->set_metadata(0, E); if (selected == E) { ti->select(0); } } String base_type = script->get_instance_base_type(); String icon_type = base_type; if (!Control::has_theme_icon(base_type, SNAME("EditorIcons"))) { icon_type = "Object"; } base_type_select->set_text(base_type); base_type_select->set_icon(Control::get_theme_icon(icon_type, SNAME("EditorIcons"))); updating_members = false; } String VisualScriptEditor::_sanitized_variant_text(const StringName &property_name) { Variant var = script->get_variable_default_value(property_name); if (script->get_variable_info(property_name).type != Variant::NIL) { Callable::CallError ce; const Variant *converted = &var; Variant n; Variant::construct(script->get_variable_info(property_name).type, n, &converted, 1, ce); var = n; } return String(var); } void VisualScriptEditor::_member_selected() { if (updating_members) { return; } TreeItem *ti = members->get_selected(); ERR_FAIL_COND(!ti); selected = ti->get_metadata(0); if (ti->get_parent() == members->get_root()->get_first_child()) { #ifdef OSX_ENABLED bool held_ctrl = Input::get_singleton()->is_key_pressed(KEY_META); #else bool held_ctrl = Input::get_singleton()->is_key_pressed(KEY_CTRL); #endif if (held_ctrl) { ERR_FAIL_COND(!script->has_function(selected)); _center_on_node(script->get_function_node_id(selected)); } } } void VisualScriptEditor::_member_edited() { if (updating_members) { return; } TreeItem *ti = members->get_edited(); ERR_FAIL_COND(!ti); String name = ti->get_metadata(0); String new_name = ti->get_text(0); if (name == new_name) { return; } if (!new_name.is_valid_identifier()) { EditorNode::get_singleton()->show_warning(TTR("Name is not a valid identifier:") + " " + new_name); updating_members = true; ti->set_text(0, name); updating_members = false; return; } if (script->has_function(new_name) || script->has_variable(new_name) || script->has_custom_signal(new_name)) { EditorNode::get_singleton()->show_warning(TTR("Name already in use by another func/var/signal:") + " " + new_name); updating_members = true; ti->set_text(0, name); updating_members = false; return; } TreeItem *root = members->get_root(); if (ti->get_parent() == root->get_first_child()) { selected = new_name; int node_id = script->get_function_node_id(name); Ref func; if (script->has_node(node_id)) { func = script->get_node(node_id); } undo_redo->create_action(TTR("Rename Function")); undo_redo->add_do_method(script.ptr(), "rename_function", name, new_name); undo_redo->add_undo_method(script.ptr(), "rename_function", new_name, name); if (func.is_valid()) { undo_redo->add_do_method(func.ptr(), "set_name", new_name); undo_redo->add_undo_method(func.ptr(), "set_name", name); } // Also fix all function calls. List lst; script->get_node_list(&lst); for (int &F : lst) { Ref fncall = script->get_node(F); if (!fncall.is_valid()) { continue; } if (fncall->get_function() == name) { undo_redo->add_do_method(fncall.ptr(), "set_function", new_name); undo_redo->add_undo_method(fncall.ptr(), "set_function", name); } } undo_redo->add_do_method(this, "_update_members"); undo_redo->add_undo_method(this, "_update_members"); undo_redo->add_do_method(this, "_update_graph"); undo_redo->add_undo_method(this, "_update_graph"); undo_redo->add_do_method(this, "emit_signal", "edited_script_changed"); undo_redo->add_undo_method(this, "emit_signal", "edited_script_changed"); undo_redo->commit_action(); return; // Or crash because it will become invalid. } if (ti->get_parent() == root->get_first_child()->get_next()) { selected = new_name; undo_redo->create_action(TTR("Rename Variable")); undo_redo->add_do_method(script.ptr(), "rename_variable", name, new_name); undo_redo->add_undo_method(script.ptr(), "rename_variable", new_name, name); undo_redo->add_do_method(this, "_update_members"); undo_redo->add_undo_method(this, "_update_members"); undo_redo->add_do_method(this, "_update_graph"); undo_redo->add_undo_method(this, "_update_graph"); undo_redo->add_do_method(this, "emit_signal", "edited_script_changed"); undo_redo->add_undo_method(this, "emit_signal", "edited_script_changed"); undo_redo->commit_action(); return; // Or crash because it will become invalid. } if (ti->get_parent() == root->get_first_child()->get_next()->get_next()) { selected = new_name; undo_redo->create_action(TTR("Rename Signal")); undo_redo->add_do_method(script.ptr(), "rename_custom_signal", name, new_name); undo_redo->add_undo_method(script.ptr(), "rename_custom_signal", new_name, name); undo_redo->add_do_method(this, "_update_members"); undo_redo->add_undo_method(this, "_update_members"); undo_redo->add_do_method(this, "emit_signal", "edited_script_changed"); undo_redo->add_undo_method(this, "emit_signal", "edited_script_changed"); undo_redo->commit_action(); return; // Or crash because it will become invalid. } } void VisualScriptEditor::_create_function_dialog() { function_create_dialog->popup_centered(); func_name_box->set_text(""); func_name_box->grab_focus(); for (int i = 0; i < func_input_vbox->get_child_count(); i++) { Node *nd = func_input_vbox->get_child(i); nd->queue_delete(); } } void VisualScriptEditor::_create_function() { String name = _validate_name((func_name_box->get_text() == "") ? "new_func" : func_name_box->get_text()); selected = name; Vector2 pos = _get_available_pos(); Ref func_node; func_node.instantiate(); func_node->set_name(name); for (int i = 0; i < func_input_vbox->get_child_count(); i++) { OptionButton *opbtn = Object::cast_to(func_input_vbox->get_child(i)->get_child(3)); LineEdit *lne = Object::cast_to(func_input_vbox->get_child(i)->get_child(1)); if (!opbtn || !lne) { continue; } Variant::Type arg_type = Variant::Type(opbtn->get_selected()); String arg_name = lne->get_text(); func_node->add_argument(arg_type, arg_name); } int func_node_id = script->get_available_id(); undo_redo->create_action(TTR("Add Function")); undo_redo->add_do_method(script.ptr(), "add_function", name, func_node_id); undo_redo->add_undo_method(script.ptr(), "remove_function", name); undo_redo->add_do_method(script.ptr(), "add_node", func_node_id, func_node, pos); undo_redo->add_undo_method(script.ptr(), "remove_node", func_node_id); undo_redo->add_do_method(this, "_update_members"); undo_redo->add_undo_method(this, "_update_members"); undo_redo->add_do_method(this, "_update_graph"); undo_redo->add_undo_method(this, "_update_graph"); undo_redo->add_do_method(this, "emit_signal", "edited_script_changed"); undo_redo->add_undo_method(this, "emit_signal", "edited_script_changed"); undo_redo->commit_action(); _update_graph(); } void VisualScriptEditor::_add_node_dialog() { _generic_search(script->get_instance_base_type(), graph->get_global_position() + Vector2(55, 80), true); } void VisualScriptEditor::_add_func_input() { HBoxContainer *hbox = memnew(HBoxContainer); hbox->set_h_size_flags(SIZE_EXPAND_FILL); Label *name_label = memnew(Label); name_label->set_text(TTR("Name:")); hbox->add_child(name_label); LineEdit *name_box = memnew(LineEdit); name_box->set_h_size_flags(SIZE_EXPAND_FILL); name_box->set_text("input"); name_box->connect("focus_entered", callable_mp(this, &VisualScriptEditor::_deselect_input_names)); hbox->add_child(name_box); Label *type_label = memnew(Label); type_label->set_text(TTR("Type:")); hbox->add_child(type_label); OptionButton *type_box = memnew(OptionButton); type_box->set_custom_minimum_size(Size2(120 * EDSCALE, 0)); for (int i = Variant::NIL; i < Variant::VARIANT_MAX; i++) { type_box->add_item(Variant::get_type_name(Variant::Type(i))); } type_box->select(1); hbox->add_child(type_box); Button *delete_button = memnew(Button); delete_button->set_icon(EditorNode::get_singleton()->get_gui_base()->get_theme_icon(SNAME("Remove"), SNAME("EditorIcons"))); delete_button->set_tooltip(vformat(TTR("Delete input port"))); hbox->add_child(delete_button); for (int i = 0; i < func_input_vbox->get_child_count(); i++) { LineEdit *line_edit = (LineEdit *)func_input_vbox->get_child(i)->get_child(1); line_edit->deselect(); } func_input_vbox->add_child(hbox); hbox->set_meta("id", hbox->get_index()); delete_button->connect("pressed", callable_mp(this, &VisualScriptEditor::_remove_func_input), varray(hbox)); name_box->select_all(); name_box->grab_focus(); } void VisualScriptEditor::_remove_func_input(Node *p_node) { func_input_vbox->remove_child(p_node); p_node->queue_delete(); } void VisualScriptEditor::_deselect_input_names() { int cn = func_input_vbox->get_child_count(); for (int i = 0; i < cn; i++) { LineEdit *lne = Object::cast_to(func_input_vbox->get_child(i)->get_child(1)); if (lne) { lne->deselect(); } } } void VisualScriptEditor::_member_button(Object *p_item, int p_column, int p_button) { TreeItem *ti = Object::cast_to(p_item); TreeItem *root = members->get_root(); if (ti->get_parent() == root) { //main buttons if (ti == root->get_first_child()) { // Add function, this one uses menu. if (p_button == 1) { // Ensure script base exists otherwise use custom base type. ERR_FAIL_COND(script.is_null()); new_virtual_method_select->select_method_from_base_type(script->get_instance_base_type(), String(), true); return; } else if (p_button == 0) { String name = _validate_name("new_function"); selected = name; Vector2 pos = _get_available_pos(); Ref func_node; func_node.instantiate(); func_node->set_name(name); int fn_id = script->get_available_id(); undo_redo->create_action(TTR("Add Function")); undo_redo->add_do_method(script.ptr(), "add_function", name, fn_id); undo_redo->add_do_method(script.ptr(), "add_node", fn_id, func_node, pos); undo_redo->add_undo_method(script.ptr(), "remove_function", name); undo_redo->add_undo_method(script.ptr(), "remove_node", fn_id); undo_redo->add_do_method(this, "_update_members"); undo_redo->add_undo_method(this, "_update_members"); undo_redo->add_do_method(this, "_update_graph"); undo_redo->add_undo_method(this, "_update_graph"); undo_redo->add_do_method(this, "emit_signal", "edited_script_changed"); undo_redo->add_undo_method(this, "emit_signal", "edited_script_changed"); undo_redo->commit_action(); _update_graph(); } return; // Or crash because it will become invalid. } if (ti == root->get_first_child()->get_next()) { // Add variable. String name = _validate_name("new_variable"); selected = name; undo_redo->create_action(TTR("Add Variable")); undo_redo->add_do_method(script.ptr(), "add_variable", name); undo_redo->add_undo_method(script.ptr(), "remove_variable", name); undo_redo->add_do_method(this, "_update_members"); undo_redo->add_undo_method(this, "_update_members"); undo_redo->add_do_method(this, "emit_signal", "edited_script_changed"); undo_redo->add_undo_method(this, "emit_signal", "edited_script_changed"); undo_redo->commit_action(); return; // Or crash because it will become invalid. } if (ti == root->get_first_child()->get_next()->get_next()) { // Add variable. String name = _validate_name("new_signal"); selected = name; undo_redo->create_action(TTR("Add Signal")); undo_redo->add_do_method(script.ptr(), "add_custom_signal", name); undo_redo->add_undo_method(script.ptr(), "remove_custom_signal", name); undo_redo->add_do_method(this, "_update_members"); undo_redo->add_undo_method(this, "_update_members"); undo_redo->add_do_method(this, "emit_signal", "edited_script_changed"); undo_redo->add_undo_method(this, "emit_signal", "edited_script_changed"); undo_redo->commit_action(); return; // Or crash because it will become invalid. } } else if (ti->get_parent() == root->get_first_child()) { selected = ti->get_text(0); function_name_edit->set_position(Input::get_singleton()->get_mouse_position() - Vector2(60, -10)); function_name_edit->popup(); function_name_box->set_text(selected); function_name_box->select_all(); } } void VisualScriptEditor::_add_input_port(int p_id) { Ref vsn = script->get_node(p_id); if (!vsn.is_valid()) { return; } updating_graph = true; undo_redo->create_action(TTR("Add Input Port"), UndoRedo::MERGE_ENDS); undo_redo->add_do_method(vsn.ptr(), "add_input_data_port", Variant::NIL, "arg", -1); undo_redo->add_do_method(this, "_update_graph", p_id); undo_redo->add_undo_method(vsn.ptr(), "remove_input_data_port", vsn->get_input_value_port_count()); undo_redo->add_undo_method(this, "_update_graph", p_id); updating_graph = false; undo_redo->commit_action(); } void VisualScriptEditor::_add_output_port(int p_id) { Ref vsn = script->get_node(p_id); if (!vsn.is_valid()) { return; } updating_graph = true; undo_redo->create_action(TTR("Add Output Port"), UndoRedo::MERGE_ENDS); undo_redo->add_do_method(vsn.ptr(), "add_output_data_port", Variant::NIL, "arg", -1); undo_redo->add_do_method(this, "_update_graph", p_id); undo_redo->add_undo_method(vsn.ptr(), "remove_output_data_port", vsn->get_output_value_port_count()); undo_redo->add_undo_method(this, "_update_graph", p_id); updating_graph = false; undo_redo->commit_action(); } void VisualScriptEditor::_remove_input_port(int p_id, int p_port) { Ref vsn = script->get_node(p_id); if (!vsn.is_valid()) { return; } updating_graph = true; undo_redo->create_action(TTR("Remove Input Port"), UndoRedo::MERGE_ENDS); int conn_from = -1, conn_port = -1; script->get_input_value_port_connection_source(p_id, p_port, &conn_from, &conn_port); if (conn_from != -1) { undo_redo->add_do_method(script.ptr(), "data_disconnect", conn_from, conn_port, p_id, p_port); } undo_redo->add_do_method(vsn.ptr(), "remove_input_data_port", p_port); undo_redo->add_do_method(this, "_update_graph", p_id); if (conn_from != -1) { undo_redo->add_undo_method(script.ptr(), "data_connect", conn_from, conn_port, p_id, p_port); } undo_redo->add_undo_method(vsn.ptr(), "add_input_data_port", vsn->get_input_value_port_info(p_port).type, vsn->get_input_value_port_info(p_port).name, p_port); undo_redo->add_undo_method(this, "_update_graph", p_id); updating_graph = false; undo_redo->commit_action(); } void VisualScriptEditor::_remove_output_port(int p_id, int p_port) { Ref vsn = script->get_node(p_id); if (!vsn.is_valid()) { return; } updating_graph = true; undo_redo->create_action(TTR("Remove Output Port"), UndoRedo::MERGE_ENDS); List data_connections; script->get_data_connection_list(&data_connections); HashMap> conn_map; for (const VisualScript::DataConnection &E : data_connections) { if (E.from_node == p_id && E.from_port == p_port) { // Push into the connections map. if (!conn_map.has(E.to_node)) { conn_map.set(E.to_node, Set()); } conn_map[E.to_node].insert(E.to_port); } } undo_redo->add_do_method(vsn.ptr(), "remove_output_data_port", p_port); undo_redo->add_do_method(this, "_update_graph", p_id); List keys; conn_map.get_key_list(&keys); for (const int &E : keys) { for (const Set::Element *F = conn_map[E].front(); F; F = F->next()) { undo_redo->add_undo_method(script.ptr(), "data_connect", p_id, p_port, E, F); } } undo_redo->add_undo_method(vsn.ptr(), "add_output_data_port", vsn->get_output_value_port_info(p_port).type, vsn->get_output_value_port_info(p_port).name, p_port); undo_redo->add_undo_method(this, "_update_graph", p_id); updating_graph = false; undo_redo->commit_action(); } void VisualScriptEditor::_expression_text_changed(const String &p_text, int p_id) { Ref vse = script->get_node(p_id); if (!vse.is_valid()) { return; } updating_graph = true; undo_redo->create_action(TTR("Change Expression"), UndoRedo::MERGE_ENDS); undo_redo->add_do_property(vse.ptr(), "expression", p_text); undo_redo->add_undo_property(vse.ptr(), "expression", vse->get("expression")); undo_redo->add_do_method(this, "_update_graph", p_id); undo_redo->add_undo_method(this, "_update_graph", p_id); undo_redo->commit_action(); Node *node = graph->get_node(itos(p_id)); if (Object::cast_to(node)) { Object::cast_to(node)->set_size(Vector2(1, 1)); // Shrink if text is smaller. } updating_graph = false; } Vector2 VisualScriptEditor::_get_pos_in_graph(Vector2 p_point) const { Vector2 pos = (graph->get_scroll_ofs() + p_point) / (graph->get_zoom() * EDSCALE); if (graph->is_using_snap()) { int snap = graph->get_snap(); pos = pos.snapped(Vector2(snap, snap)); } return pos; } Vector2 VisualScriptEditor::_get_available_pos(bool p_centered, Vector2 p_pos) const { if (p_centered) { p_pos = _get_pos_in_graph(graph->get_size() * 0.5); } while (true) { bool exists = false; List existing; script->get_node_list(&existing); for (int &E : existing) { Point2 pos = script->get_node_position(E); if (pos.distance_to(p_pos) < 50) { p_pos += Vector2(graph->get_snap(), graph->get_snap()); exists = true; break; } } if (exists) { continue; } break; } return p_pos; } String VisualScriptEditor::_validate_name(const String &p_name) const { String valid = p_name; int counter = 1; while (true) { bool exists = script->has_function(valid) || script->has_variable(valid) || script->has_custom_signal(valid); if (exists) { counter++; valid = p_name + "_" + itos(counter); continue; } break; } return valid; } void VisualScriptEditor::_on_nodes_copy() { clipboard->nodes.clear(); clipboard->data_connections.clear(); clipboard->sequence_connections.clear(); for (int i = 0; i < graph->get_child_count(); i++) { GraphNode *gn = Object::cast_to(graph->get_child(i)); if (gn) { if (gn->is_selected()) { int id = gn->get_name().operator String().to_int(); Ref node = script->get_node(id); if (Object::cast_to(*node)) { EditorNode::get_singleton()->show_warning(TTR("Can't copy the function node.")); return; } if (node.is_valid()) { clipboard->nodes[id] = node->duplicate(true); clipboard->nodes_positions[id] = script->get_node_position(id); } } } } if (clipboard->nodes.is_empty()) { return; } List sequence_connections; script->get_sequence_connection_list(&sequence_connections); for (const VisualScript::SequenceConnection &E : sequence_connections) { if (clipboard->nodes.has(E.from_node) && clipboard->nodes.has(E.to_node)) { clipboard->sequence_connections.insert(E); } } List data_connections; script->get_data_connection_list(&data_connections); for (const VisualScript::DataConnection &E : data_connections) { if (clipboard->nodes.has(E.from_node) && clipboard->nodes.has(E.to_node)) { clipboard->data_connections.insert(E); } } } void VisualScriptEditor::_on_nodes_paste() { if (clipboard->nodes.is_empty()) { EditorNode::get_singleton()->show_warning(TTR("Clipboard is empty!")); return; } Map remap; undo_redo->create_action(TTR("Paste VisualScript Nodes")); int idc = script->get_available_id() + 1; Set to_select; Set existing_positions; { List nodes; script->get_node_list(&nodes); for (int &E : nodes) { Vector2 pos = script->get_node_position(E).snapped(Vector2(2, 2)); existing_positions.insert(pos); } } bool first_paste = true; Vector2 position_offset = Vector2(0, 0); for (KeyValue> &E : clipboard->nodes) { Ref node = E.value->duplicate(); int new_id = idc++; to_select.insert(new_id); remap[E.key] = new_id; Vector2 paste_pos = clipboard->nodes_positions[E.key]; if (first_paste) { position_offset = _get_pos_in_graph(mouse_up_position - graph->get_global_position()) - paste_pos; first_paste = false; } paste_pos += position_offset; while (existing_positions.has(paste_pos.snapped(Vector2(2, 2)))) { paste_pos += Vector2(20, 20) * EDSCALE; } undo_redo->add_do_method(script.ptr(), "add_node", new_id, node, paste_pos); undo_redo->add_undo_method(script.ptr(), "remove_node", new_id); } for (Set::Element *E = clipboard->sequence_connections.front(); E; E = E->next()) { undo_redo->add_do_method(script.ptr(), "sequence_connect", remap[E->get().from_node], E->get().from_output, remap[E->get().to_node]); undo_redo->add_undo_method(script.ptr(), "sequence_disconnect", remap[E->get().from_node], E->get().from_output, remap[E->get().to_node]); } for (Set::Element *E = clipboard->data_connections.front(); E; E = E->next()) { undo_redo->add_do_method(script.ptr(), "data_connect", remap[E->get().from_node], E->get().from_port, remap[E->get().to_node], E->get().to_port); undo_redo->add_undo_method(script.ptr(), "data_disconnect", remap[E->get().from_node], E->get().from_port, remap[E->get().to_node], E->get().to_port); } undo_redo->add_do_method(this, "_update_graph"); undo_redo->add_undo_method(this, "_update_graph"); undo_redo->commit_action(); for (int i = 0; i < graph->get_child_count(); i++) { GraphNode *gn = Object::cast_to(graph->get_child(i)); if (gn) { int id = gn->get_name().operator String().to_int(); gn->set_selected(to_select.has(id)); } } } void VisualScriptEditor::_on_nodes_delete() { // Delete all the selected nodes. List to_erase; for (int i = 0; i < graph->get_child_count(); i++) { GraphNode *gn = Object::cast_to(graph->get_child(i)); if (gn) { if (gn->is_selected() && gn->is_close_button_visible()) { to_erase.push_back(gn->get_name().operator String().to_int()); } } } if (to_erase.is_empty()) { return; } undo_redo->create_action(TTR("Remove VisualScript Nodes")); for (int &F : to_erase) { int cr_node = F; undo_redo->add_do_method(script.ptr(), "remove_node", cr_node); undo_redo->add_undo_method(script.ptr(), "add_node", cr_node, script->get_node(cr_node), script->get_node_position(cr_node)); List sequence_conns; script->get_sequence_connection_list(&sequence_conns); for (const VisualScript::SequenceConnection &E : sequence_conns) { if (E.from_node == cr_node || E.to_node == cr_node) { undo_redo->add_undo_method(script.ptr(), "sequence_connect", E.from_node, E.from_output, E.to_node); } } List data_conns; script->get_data_connection_list(&data_conns); for (const VisualScript::DataConnection &E : data_conns) { if (E.from_node == F || E.to_node == F) { undo_redo->add_undo_method(script.ptr(), "data_connect", E.from_node, E.from_port, E.to_node, E.to_port); } } } undo_redo->add_do_method(this, "_update_graph"); undo_redo->add_undo_method(this, "_update_graph"); undo_redo->commit_action(); } void VisualScriptEditor::_on_nodes_duplicate() { Set to_duplicate; for (int i = 0; i < graph->get_child_count(); i++) { GraphNode *gn = Object::cast_to(graph->get_child(i)); if (gn) { if (gn->is_selected() && gn->is_close_button_visible()) { int id = gn->get_name().operator String().to_int(); to_duplicate.insert(id); } } } if (to_duplicate.is_empty()) { return; } undo_redo->create_action(TTR("Duplicate VisualScript Nodes")); int idc = script->get_available_id() + 1; Set to_select; HashMap remap; for (Set::Element *F = to_duplicate.front(); F; F = F->next()) { // Duplicate from the specific function but place it into the default func as it would lack the connections. Ref node = script->get_node(F->get()); Ref dupe = node->duplicate(true); int new_id = idc++; remap.set(F->get(), new_id); to_select.insert(new_id); undo_redo->add_do_method(script.ptr(), "add_node", new_id, dupe, script->get_node_position(F->get()) + Vector2(20, 20)); undo_redo->add_undo_method(script.ptr(), "remove_node", new_id); } List seqs; script->get_sequence_connection_list(&seqs); for (const VisualScript::SequenceConnection &E : seqs) { if (to_duplicate.has(E.from_node) && to_duplicate.has(E.to_node)) { undo_redo->add_do_method(script.ptr(), "sequence_connect", remap[E.from_node], E.from_output, remap[E.to_node]); } } List data; script->get_data_connection_list(&data); for (const VisualScript::DataConnection &E : data) { if (to_duplicate.has(E.from_node) && to_duplicate.has(E.to_node)) { undo_redo->add_do_method(script.ptr(), "data_connect", remap[E.from_node], E.from_port, remap[E.to_node], E.to_port); } } undo_redo->add_do_method(this, "_update_graph"); undo_redo->add_undo_method(this, "_update_graph"); undo_redo->commit_action(); for (int i = 0; i < graph->get_child_count(); i++) { GraphNode *gn = Object::cast_to(graph->get_child(i)); if (gn) { int id = gn->get_name().operator String().to_int(); gn->set_selected(to_select.has(id)); } } if (to_select.size()) { EditorNode::get_singleton()->push_item(script->get_node(to_select.front()->get()).ptr()); } } void VisualScriptEditor::_generic_search(String p_base_type, Vector2 pos, bool node_centered) { if (node_centered) { port_action_pos = graph->get_size() / 2.0f; } else { port_action_pos = graph->get_viewport()->get_mouse_position() - graph->get_global_position(); } new_connect_node_select->select_from_visual_script(p_base_type, false, false); // neither connecting nor reset text // Ensure that the dialog fits inside the graph. Size2 bounds = graph->get_global_position() + graph->get_size() - new_connect_node_select->get_size(); pos.x = pos.x > bounds.x ? bounds.x : pos.x; pos.y = pos.y > bounds.y ? bounds.y : pos.y; if (pos != Vector2()) { new_connect_node_select->set_position(pos); } } void VisualScriptEditor::input(const Ref &p_event) { ERR_FAIL_COND(p_event.is_null()); // GUI input for VS Editor Plugin Ref key = p_event; if (key.is_valid() && !key->is_pressed()) { mouse_up_position = Input::get_singleton()->get_mouse_position(); } } void VisualScriptEditor::_graph_gui_input(const Ref &p_event) { Ref key = p_event; if (key.is_valid() && key->is_pressed() && key->get_button_mask() == MOUSE_BUTTON_RIGHT) { saved_position = graph->get_local_mouse_position(); Point2 gpos = Input::get_singleton()->get_mouse_position(); _generic_search(script->get_instance_base_type(), gpos); } } void VisualScriptEditor::_members_gui_input(const Ref &p_event) { Ref key = p_event; if (key.is_valid() && key->is_pressed() && !key->is_echo()) { if (members->has_focus()) { TreeItem *ti = members->get_selected(); if (ti) { TreeItem *root = members->get_root(); if (ti->get_parent() == root->get_first_child()) { member_type = MEMBER_FUNCTION; } if (ti->get_parent() == root->get_first_child()->get_next()) { member_type = MEMBER_VARIABLE; } if (ti->get_parent() == root->get_first_child()->get_next()->get_next()) { member_type = MEMBER_SIGNAL; } member_name = ti->get_text(0); } if (ED_IS_SHORTCUT("ui_graph_delete", p_event)) { _member_option(MEMBER_REMOVE); } if (ED_IS_SHORTCUT("visual_script_editor/edit_member", p_event)) { _member_option(MEMBER_EDIT); } } } Ref btn = p_event; if (btn.is_valid() && btn->is_double_click()) { TreeItem *ti = members->get_selected(); if (ti && ti->get_parent() == members->get_root()->get_first_child()) { // to check if it's a function _center_on_node(script->get_function_node_id(ti->get_metadata(0))); } } } void VisualScriptEditor::_rename_function(const String &name, const String &new_name) { if (!new_name.is_valid_identifier()) { EditorNode::get_singleton()->show_warning(TTR("Name is not a valid identifier:") + " " + new_name); return; } if (script->has_function(new_name) || script->has_variable(new_name) || script->has_custom_signal(new_name)) { EditorNode::get_singleton()->show_warning(TTR("Name already in use by another func/var/signal:") + " " + new_name); return; } int node_id = script->get_function_node_id(name); Ref func; if (script->has_node(node_id)) { func = script->get_node(node_id); } undo_redo->create_action(TTR("Rename Function")); undo_redo->add_do_method(script.ptr(), "rename_function", name, new_name); undo_redo->add_undo_method(script.ptr(), "rename_function", new_name, name); if (func.is_valid()) { undo_redo->add_do_method(func.ptr(), "set_name", new_name); undo_redo->add_undo_method(func.ptr(), "set_name", name); } // Also fix all function calls. List lst; script->get_node_list(&lst); for (int &F : lst) { Ref fncall = script->get_node(F); if (!fncall.is_valid()) { continue; } if (fncall->get_function() == name) { undo_redo->add_do_method(fncall.ptr(), "set_function", new_name); undo_redo->add_undo_method(fncall.ptr(), "set_function", name); } } undo_redo->add_do_method(this, "_update_members"); undo_redo->add_undo_method(this, "_update_members"); undo_redo->add_do_method(this, "_update_graph"); undo_redo->add_undo_method(this, "_update_graph"); undo_redo->add_do_method(this, "emit_signal", "edited_script_changed"); undo_redo->add_undo_method(this, "emit_signal", "edited_script_changed"); undo_redo->commit_action(); } void VisualScriptEditor::_fn_name_box_input(const Ref &p_event) { if (!function_name_edit->is_visible()) { return; } Ref key = p_event; if (key.is_valid() && key->is_pressed() && key->get_keycode() == KEY_ENTER) { function_name_edit->hide(); _rename_function(selected, function_name_box->get_text()); function_name_box->clear(); } } Variant VisualScriptEditor::get_drag_data_fw(const Point2 &p_point, Control *p_from) { if (p_from == members) { TreeItem *it = members->get_item_at_position(p_point); if (!it) { return Variant(); } String type = it->get_metadata(0); if (type == String()) { return Variant(); } Dictionary dd; TreeItem *root = members->get_root(); if (it->get_parent() == root->get_first_child()) { dd["type"] = "visual_script_function_drag"; dd["function"] = type; } else if (it->get_parent() == root->get_first_child()->get_next()) { dd["type"] = "visual_script_variable_drag"; dd["variable"] = type; } else if (it->get_parent() == root->get_first_child()->get_next()->get_next()) { dd["type"] = "visual_script_signal_drag"; dd["signal"] = type; } else { return Variant(); } Label *label = memnew(Label); label->set_text(it->get_text(0)); set_drag_preview(label); return dd; } return Variant(); } bool VisualScriptEditor::can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const { if (p_from == graph) { Dictionary d = p_data; if (d.has("type") && (String(d["type"]) == "visual_script_node_drag" || String(d["type"]) == "visual_script_function_drag" || String(d["type"]) == "visual_script_variable_drag" || String(d["type"]) == "visual_script_signal_drag" || String(d["type"]) == "obj_property" || String(d["type"]) == "resource" || String(d["type"]) == "files" || String(d["type"]) == "nodes")) { if (String(d["type"]) == "obj_property") { #ifdef OSX_ENABLED const_cast(this)->_show_hint(vformat(TTR("Hold %s to drop a Getter. Hold Shift to drop a generic signature."), find_keycode_name(KEY_META))); #else const_cast(this)->_show_hint(TTR("Hold Ctrl to drop a Getter. Hold Shift to drop a generic signature.")); #endif } if (String(d["type"]) == "nodes") { #ifdef OSX_ENABLED const_cast(this)->_show_hint(vformat(TTR("Hold %s to drop a simple reference to the node."), find_keycode_name(KEY_META))); #else const_cast(this)->_show_hint(TTR("Hold Ctrl to drop a simple reference to the node.")); #endif } if (String(d["type"]) == "visual_script_variable_drag") { #ifdef OSX_ENABLED const_cast(this)->_show_hint(vformat(TTR("Hold %s to drop a Variable Setter."), find_keycode_name(KEY_META))); #else const_cast(this)->_show_hint(TTR("Hold Ctrl to drop a Variable Setter.")); #endif } return true; } } return false; } static Node *_find_script_node(Node *p_edited_scene, Node *p_current_node, const Ref