/*************************************************************************/ /* 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 "core/object.h" #include "core/os/input.h" #include "core/os/keyboard.h" #include "core/script_language.h" #include "core/variant.h" #include "editor/editor_node.h" #include "editor/editor_resource_preview.h" #include "editor/editor_scale.h" #include "scene/main/viewport.h" #include "visual_script_expression.h" #include "visual_script_flow_control.h" #include "visual_script_func_nodes.h" #include "visual_script_nodes.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() { _change_notify(); emit_signal("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; _change_notify(); } VisualScriptEditorSignalEdit() { undo_redo = NULL; } }; 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() { _change_notify(); emit_signal("changed"); } void _var_value_changed() { _change_notify("value"); //so the whole tree is not redrawn, makes editing smoother in general emit_signal("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); 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); // Setting the default value. Variant::Type type = (Variant::Type)(int)p_value; if (type != Variant::NIL) { Variant default_value; Variant::CallError ce; default_value = Variant::construct(type, NULL, 0, ce); if (ce.error == Variant::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_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; _change_notify(); } VisualScriptEditorVariableEdit() { undo_redo = NULL; } }; 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::REAL: 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::RECT2: color = Color(0.95, 0.57, 0.65); break; case Variant::VECTOR3: 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::QUAT: 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::TRANSFORM: 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::POOL_BYTE_ARRAY: color = Color(0.67, 0.96, 0.78); break; case Variant::POOL_INT_ARRAY: color = Color(0.69, 0.86, 0.96); break; case Variant::POOL_REAL_ARRAY: color = Color(0.59, 0.91, 0.97); break; case Variant::POOL_STRING_ARRAY: color = Color(0.62, 0.77, 0.95); break; case Variant::POOL_VECTOR2_ARRAY: color = Color(0.82, 0.7, 0.96); break; case Variant::POOL_VECTOR3_ARRAY: color = Color(0.87, 0.61, 0.95); break; case Variant::POOL_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::REAL: 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::RECT2: color = Color(0.93, 0.46, 0.56); break; case Variant::VECTOR3: 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::QUAT: 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::TRANSFORM: 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::POOL_BYTE_ARRAY: color = Color(0.38, 0.92, 0.6); break; case Variant::POOL_INT_ARRAY: color = Color(0.38, 0.73, 0.92); break; case Variant::POOL_REAL_ARRAY: color = Color(0.25, 0.83, 0.95); break; case Variant::POOL_STRING_ARRAY: color = Color(0.38, 0.62, 0.92); break; case Variant::POOL_VECTOR2_ARRAY: color = Color(0.62, 0.36, 0.92); break; case Variant::POOL_VECTOR3_ARRAY: color = Color(0.79, 0.35, 0.92); break; case Variant::POOL_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 funcs; script->get_function_list(&funcs); if (funcs.size() <= 0) { updating_graph = false; return; } for (List::Element *F = funcs.front(); F; F = F->next()) { List sequence_conns; script->get_sequence_connection_list(F->get(), &sequence_conns); for (List::Element *E = sequence_conns.front(); E; E = E->next()) { graph->connect_node(itos(E->get().from_node), E->get().from_output, itos(E->get().to_node), 0); } List data_conns; script->get_data_connection_list(F->get(), &data_conns); for (List::Element *E = data_conns.front(); E; E = E->next()) { VisualScript::DataConnection dc = E->get(); Ref from_node = script->get_node(F->get(), E->get().from_node); Ref to_node = script->get_node(F->get(), E->get().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(E->get().from_node), dc.from_port, itos(E->get().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--; } } } List funcs; script->get_function_list(&funcs); if (funcs.size() <= 0) { graph->hide(); select_func_text->show(); updating_graph = false; return; } graph->show(); select_func_text->hide(); Ref type_icons[Variant::VARIANT_MAX] = { Control::get_icon("Variant", "EditorIcons"), Control::get_icon("bool", "EditorIcons"), Control::get_icon("int", "EditorIcons"), Control::get_icon("float", "EditorIcons"), Control::get_icon("String", "EditorIcons"), Control::get_icon("Vector2", "EditorIcons"), Control::get_icon("Rect2", "EditorIcons"), Control::get_icon("Vector3", "EditorIcons"), Control::get_icon("Transform2D", "EditorIcons"), Control::get_icon("Plane", "EditorIcons"), Control::get_icon("Quat", "EditorIcons"), Control::get_icon("AABB", "EditorIcons"), Control::get_icon("Basis", "EditorIcons"), Control::get_icon("Transform", "EditorIcons"), Control::get_icon("Color", "EditorIcons"), Control::get_icon("NodePath", "EditorIcons"), Control::get_icon("RID", "EditorIcons"), Control::get_icon("MiniObject", "EditorIcons"), Control::get_icon("Dictionary", "EditorIcons"), Control::get_icon("Array", "EditorIcons"), Control::get_icon("PoolByteArray", "EditorIcons"), Control::get_icon("PoolIntArray", "EditorIcons"), Control::get_icon("PoolRealArray", "EditorIcons"), Control::get_icon("PoolStringArray", "EditorIcons"), Control::get_icon("PoolVector2Array", "EditorIcons"), Control::get_icon("PoolVector3Array", "EditorIcons"), Control::get_icon("PoolColorArray", "EditorIcons") }; Ref seq_port = Control::get_icon("VisualShaderPort", "EditorIcons"); for (List::Element *F = funcs.front(); F; F = F->next()) { // loop through all the functions List ids; script->get_node_list(F->get(), &ids); StringName editor_icons = "EditorIcons"; for (List::Element *E = ids.front(); E; E = E->next()) { if (p_only_id >= 0 && p_only_id != E->get()) continue; Ref node = script->get_node(F->get(), E->get()); Vector2 pos = script->get_node_position(F->get(), E->get()); GraphNode *gnode = memnew(GraphNode); gnode->set_title(node->get_caption()); gnode->set_offset(pos * EDSCALE); if (error_line == E->get()) { 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->get())); gnode->connect("dragged", this, "_node_moved", varray(E->get())); gnode->connect("close_request", this, "_remove_node", varray(E->get()), CONNECT_DEFERRED); if (E->get() != script->get_function_node_id(F->get())) { //function can't be erased 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", this, "_add_input_port", varray(E->get()), 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", this, "_add_output_port", varray(E->get()), 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(true); line_edit->add_font_override("font", get_font("source", "EditorFonts")); gnode->add_child(line_edit); line_edit->connect("text_changed", this, "_expression_text_changed", varray(E->get())); } else { String text = node->get_text(); if (!text.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", this, "_comment_node_resized", varray(E->get())); } 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("comment", "GraphNode"); Color c = sbf->get_border_color(); c.a = 1; if (EditorSettings::get_singleton()->get("interface/theme/use_graph_node_headers")) { Color mono_color = ((c.r + c.g + c.b) / 3) < 0.7 ? Color(1.0, 1.0, 1.0) : Color(0.0, 0.0, 0.0); mono_color.a = 0.85; c = mono_color; } gnode->add_color_override("title_color", c); c.a = 0.7; gnode->add_color_override("close_color", c); gnode->add_color_override("resizer_color", c); gnode->add_style_override("frame", sbf); } const Color mono_color = get_color("mono_color", "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(true); name_box->connect("resized", this, "_update_node_size", varray(E->get())); name_box->connect("focus_exited", this, "_port_name_focus_out", varray(name_box, E->get(), 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", this, "_change_port_type", varray(E->get(), i, true), CONNECT_DEFERRED); } Button *rmbtn = memnew(Button); rmbtn->set_icon(EditorNode::get_singleton()->get_gui_base()->get_icon("Remove", "EditorIcons")); hbc->add_child(rmbtn); rmbtn->connect("pressed", this, "_remove_input_port", varray(E->get(), i), CONNECT_DEFERRED); } else { hbc->add_child(memnew(Label(left_name))); } if (left_type != Variant::NIL && !script->is_input_value_port_connected(F->get(), E->get(), 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 Variant::CallError ce; const Variant *existingp = &value; value = Variant::construct(left_type, &existingp, 1, ce, false); } if (left_type == Variant::COLOR) { button->set_custom_minimum_size(Size2(30, 0) * EDSCALE); button->connect("draw", this, "_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", this, "_default_value_edited", varray(button, E->get(), 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_icon("Remove", "EditorIcons")); hbc->add_child(rmbtn); rmbtn->connect("pressed", this, "_remove_output_port", varray(E->get(), 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", this, "_change_port_type", varray(E->get(), 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(true); name_box->connect("resized", this, "_update_node_size", varray(E->get())); name_box->connect("focus_exited", this, "_port_name_focus_out", varray(name_box, E->get(), 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_constant("dark_theme", "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("set_scroll_ofs", script->get_function_scroll(default_func) * EDSCALE); updating_graph = false; } void VisualScriptEditor::_change_port_type(int p_select, int p_id, int p_port, bool is_input) { StringName func = _get_function_of_node(p_id); Ref vsn = script->get_node(func, p_id); if (!vsn.is_valid()) return; undo_redo->create_action("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) { StringName func = _get_function_of_node(p_id); Ref vsn = script->get_node(func, 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("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_icon("Override", "EditorIcons"), 1, false, TTR("Override an existing built-in function.")); functions->add_button(0, Control::get_icon("Add", "EditorIcons"), 0, false, TTR("Create a new function.")); functions->set_custom_color(0, Control::get_color("mono_color", "Editor")); List func_names; script->get_function_list(&func_names); for (List::Element *E = func_names.front(); E; E = E->next()) { if (E->get() == default_func) { continue; } TreeItem *ti = members->create_item(functions); ti->set_text(0, E->get()); ti->set_selectable(0, true); ti->set_metadata(0, E->get()); ti->add_button(0, Control::get_icon("Edit", "EditorIcons"), 0); if (selected == E->get()) 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_icon("Add", "EditorIcons"), -1, false, TTR("Create a new variable.")); variables->set_custom_color(0, Control::get_color("mono_color", "Editor")); Ref type_icons[Variant::VARIANT_MAX] = { Control::get_icon("Variant", "EditorIcons"), Control::get_icon("bool", "EditorIcons"), Control::get_icon("int", "EditorIcons"), Control::get_icon("float", "EditorIcons"), Control::get_icon("String", "EditorIcons"), Control::get_icon("Vector2", "EditorIcons"), Control::get_icon("Rect2", "EditorIcons"), Control::get_icon("Vector3", "EditorIcons"), Control::get_icon("Transform2D", "EditorIcons"), Control::get_icon("Plane", "EditorIcons"), Control::get_icon("Quat", "EditorIcons"), Control::get_icon("AABB", "EditorIcons"), Control::get_icon("Basis", "EditorIcons"), Control::get_icon("Transform", "EditorIcons"), Control::get_icon("Color", "EditorIcons"), Control::get_icon("NodePath", "EditorIcons"), Control::get_icon("RID", "EditorIcons"), Control::get_icon("MiniObject", "EditorIcons"), Control::get_icon("Dictionary", "EditorIcons"), Control::get_icon("Array", "EditorIcons"), Control::get_icon("PoolByteArray", "EditorIcons"), Control::get_icon("PoolIntArray", "EditorIcons"), Control::get_icon("PoolRealArray", "EditorIcons"), Control::get_icon("PoolStringArray", "EditorIcons"), Control::get_icon("PoolVector2Array", "EditorIcons"), Control::get_icon("PoolVector3Array", "EditorIcons"), Control::get_icon("PoolColorArray", "EditorIcons") }; List var_names; script->get_variable_list(&var_names); for (List::Element *E = var_names.front(); E; E = E->next()) { TreeItem *ti = members->create_item(variables); ti->set_text(0, E->get()); ti->set_suffix(0, "= " + _sanitized_variant_text(E->get())); ti->set_icon(0, type_icons[script->get_variable_info(E->get()).type]); ti->set_selectable(0, true); ti->set_editable(0, true); ti->set_metadata(0, E->get()); if (selected == E->get()) 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_icon("Add", "EditorIcons"), -1, false, TTR("Create a new signal.")); _signals->set_custom_color(0, Control::get_color("mono_color", "Editor")); List signal_names; script->get_custom_signal_list(&signal_names); for (List::Element *E = signal_names.front(); E; E = E->next()) { TreeItem *ti = members->create_item(_signals); ti->set_text(0, E->get()); ti->set_selectable(0, true); ti->set_editable(0, true); ti->set_metadata(0, E->get()); if (selected == E->get()) ti->select(0); } String base_type = script->get_instance_base_type(); String icon_type = base_type; if (!Control::has_icon(base_type, "EditorIcons")) { icon_type = "Object"; } base_type_select->set_text(base_type); base_type_select->set_icon(Control::get_icon(icon_type, "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) { Variant::CallError ce; const Variant *converted = &var; var = Variant::construct(script->get_variable_info(property_name).type, &converted, 1, ce, false); } 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_children()) { #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_CONTROL); #endif if (held_ctrl) { ERR_FAIL_COND(!script->has_function(selected)); _center_on_node(selected, 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_children()) { selected = new_name; int node_id = script->get_function_node_id(name); Ref func; if (script->has_node(name, node_id)) { func = script->get_node(name, 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 flst; script->get_function_list(&flst); for (List::Element *E = flst.front(); E; E = E->next()) { List lst; script->get_node_list(E->get(), &lst); for (List::Element *F = lst.front(); F; F = F->next()) { Ref fncall = script->get_node(E->get(), F->get()); 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_children()->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, "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_children()->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 ofs = _get_available_pos(); Ref func_node; func_node.instance(); 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); } undo_redo->create_action(TTR("Add Function")); undo_redo->add_do_method(script.ptr(), "add_function", name); undo_redo->add_do_method(script.ptr(), "add_node", name, script->get_available_id(), func_node, ofs); undo_redo->add_undo_method(script.ptr(), "remove_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(); _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", this, "_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_icon("Remove", "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_position_in_parent()); delete_button->connect("pressed", this, "_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_children()) { //add function, this one uses menu if (p_button == 1) { 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 ofs = _get_available_pos(); Ref func_node; func_node.instance(); func_node->set_name(name); undo_redo->create_action(TTR("Add Function")); undo_redo->add_do_method(script.ptr(), "add_function", name); undo_redo->add_do_method(script.ptr(), "add_node", name, script->get_available_id(), func_node, ofs); undo_redo->add_undo_method(script.ptr(), "remove_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(); _update_graph(); } return; //or crash because it will become invalid } if (ti == root->get_children()->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_children()->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_children()) { 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) { StringName func = _get_function_of_node(p_id); Ref vsn = script->get_node(func, 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) { StringName func = _get_function_of_node(p_id); Ref vsn = script->get_node(func, 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) { StringName func = _get_function_of_node(p_id); Ref vsn = script->get_node(func, 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(func, p_id, p_port, &conn_from, &conn_port); if (conn_from != -1) undo_redo->add_do_method(script.ptr(), "data_disconnect", func, 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", func, 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) { StringName func = _get_function_of_node(p_id); Ref vsn = script->get_node(func, 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(func, &data_connections); HashMap> conn_map; for (const List::Element *E = data_connections.front(); E; E = E->next()) { if (E->get().from_node == p_id && E->get().from_port == p_port) { // push into the connections map if (!conn_map.has(E->get().to_node)) conn_map.set(E->get().to_node, Set()); conn_map[E->get().to_node].insert(E->get().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 List::Element *E = keys.front(); E; E = E->next()) { for (const Set::Element *F = conn_map[E->get()].front(); F; F = F->next()) { undo_redo->add_undo_method(script.ptr(), "data_connect", func, p_id, p_port, E->get(), F->get()); } } 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) { StringName func = _get_function_of_node(p_id); Ref vse = script->get_node(func, 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_available_pos(bool centered, Vector2 ofs) const { if (centered) ofs = graph->get_scroll_ofs() + graph->get_size() * 0.5; if (graph->is_using_snap()) { int snap = graph->get_snap(); ofs = ofs.snapped(Vector2(snap, snap)); } ofs /= EDSCALE; while (true) { bool exists = false; List all_fn; script->get_function_list(&all_fn); for (List::Element *F = all_fn.front(); F; F = F->next()) { StringName curr_fn = F->get(); List existing; script->get_node_list(curr_fn, &existing); for (List::Element *E = existing.front(); E; E = E->next()) { Point2 pos = script->get_node_position(curr_fn, E->get()); if (pos.distance_to(ofs) < 50) { ofs += Vector2(graph->get_snap(), graph->get_snap()); exists = true; break; } } } if (exists) continue; break; } return ofs; } 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_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.empty()) return; undo_redo->create_action(TTR("Remove VisualScript Nodes")); for (List::Element *F = to_erase.front(); F; F = F->next()) { int cr_node = F->get(); StringName func = _get_function_of_node(cr_node); undo_redo->add_do_method(script.ptr(), "remove_node", func, cr_node); undo_redo->add_undo_method(script.ptr(), "add_node", func, cr_node, script->get_node(func, cr_node), script->get_node_position(func, cr_node)); List sequence_conns; script->get_sequence_connection_list(func, &sequence_conns); for (List::Element *E = sequence_conns.front(); E; E = E->next()) { if (E->get().from_node == cr_node || E->get().to_node == cr_node) { undo_redo->add_undo_method(script.ptr(), "sequence_connect", func, E->get().from_node, E->get().from_output, E->get().to_node); } } List data_conns; script->get_data_connection_list(func, &data_conns); for (List::Element *E = data_conns.front(); E; E = E->next()) { if (E->get().from_node == F->get() || E->get().to_node == F->get()) { undo_redo->add_undo_method(script.ptr(), "data_connect", func, E->get().from_node, E->get().from_port, 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(); } void VisualScriptEditor::_on_nodes_duplicate() { Set to_duplicate; List funcs; 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); funcs.push_back(_get_function_of_node(id)); } } } if (to_duplicate.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 StringName func = _get_function_of_node(F->get()); Ref node = script->get_node(func, 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", default_func, new_id, dupe, script->get_node_position(func, F->get()) + Vector2(20, 20)); undo_redo->add_undo_method(script.ptr(), "remove_node", default_func, new_id); } for (List::Element *F = funcs.front(); F; F = F->next()) { List seqs; script->get_sequence_connection_list(F->get(), &seqs); for (List::Element *E = seqs.front(); E; E = E->next()) { if (to_duplicate.has(E->get().from_node) && to_duplicate.has(E->get().to_node)) { undo_redo->add_do_method(script.ptr(), "sequence_connect", default_func, remap[E->get().from_node], E->get().from_output, remap[E->get().to_node]); } } List data; script->get_data_connection_list(F->get(), &data); for (List::Element *E = data.front(); E; E = E->next()) { if (to_duplicate.has(E->get().from_node) && to_duplicate.has(E->get().to_node)) { undo_redo->add_do_method(script.ptr(), "data_connect", default_func, 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)); } } if (to_select.size()) { EditorNode::get_singleton()->push_item(script->get_node(default_func, 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) { // 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() == 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_children()) { member_type = MEMBER_FUNCTION; } if (ti->get_parent() == root->get_children()->get_next()) { member_type = MEMBER_VARIABLE; } if (ti->get_parent() == root->get_children()->get_next()->get_next()) { member_type = MEMBER_SIGNAL; } member_name = ti->get_text(0); } if (ED_IS_SHORTCUT("visual_script_editor/delete_selected", 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_doubleclick()) { TreeItem *ti = members->get_selected(); if (ti && ti->get_parent() == members->get_root()->get_children()) // to check if it's a function _center_on_node(ti->get_metadata(0), 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(name, node_id)) { func = script->get_node(name, 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 flst; script->get_function_list(&flst); for (List::Element *E = flst.front(); E; E = E->next()) { List lst; script->get_node_list(E->get(), &lst); for (List::Element *F = lst.front(); F; F = F->next()) { Ref fncall = script->get_node(E->get(), F->get()); 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_scancode() == 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_children()) { dd["type"] = "visual_script_function_drag"; dd["function"] = type; } else if (it->get_parent() == root->get_children()->get_next()) { dd["type"] = "visual_script_variable_drag"; dd["variable"] = type; } else if (it->get_parent() == root->get_children()->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