/*************************************************************************/ /* editor_build_profile.cpp */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ /* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ /* Copyright (c) 2014-2022 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 "editor_build_profile.h" #include "core/io/dir_access.h" #include "core/io/json.h" #include "editor/editor_file_dialog.h" #include "editor/editor_file_system.h" #include "editor/editor_node.h" #include "editor/editor_property_name_processor.h" #include "editor/editor_scale.h" #include "editor/editor_settings.h" const char *EditorBuildProfile::build_option_identifiers[BUILD_OPTION_MAX] = { // This maps to SCons build options. "disable_3d", "disable_2d_physics", "disable_3d_physics", "disable_navigation", "openxr", "opengl3", "vulkan", }; const bool EditorBuildProfile::build_option_disable_values[BUILD_OPTION_MAX] = { // This maps to SCons build options. true, true, true, true, false, false, false }; void EditorBuildProfile::set_disable_class(const StringName &p_class, bool p_disabled) { if (p_disabled) { disabled_classes.insert(p_class); } else { disabled_classes.erase(p_class); } } bool EditorBuildProfile::is_class_disabled(const StringName &p_class) const { if (p_class == StringName()) { return false; } return disabled_classes.has(p_class) || is_class_disabled(ClassDB::get_parent_class_nocheck(p_class)); } void EditorBuildProfile::set_item_collapsed(const StringName &p_class, bool p_collapsed) { if (p_collapsed) { collapsed_classes.insert(p_class); } else { collapsed_classes.erase(p_class); } } bool EditorBuildProfile::is_item_collapsed(const StringName &p_class) const { return collapsed_classes.has(p_class); } void EditorBuildProfile::set_disable_build_option(BuildOption p_build_option, bool p_disable) { ERR_FAIL_INDEX(p_build_option, BUILD_OPTION_MAX); build_options_disabled[p_build_option] = p_disable; } void EditorBuildProfile::clear_disabled_classes() { disabled_classes.clear(); collapsed_classes.clear(); } bool EditorBuildProfile::is_build_option_disabled(BuildOption p_build_option) const { ERR_FAIL_INDEX_V(p_build_option, BUILD_OPTION_MAX, false); return build_options_disabled[p_build_option]; } bool EditorBuildProfile::get_build_option_disable_value(BuildOption p_build_option) { ERR_FAIL_INDEX_V(p_build_option, BUILD_OPTION_MAX, false); return build_option_disable_values[p_build_option]; } void EditorBuildProfile::set_force_detect_classes(const String &p_classes) { force_detect_classes = p_classes; } String EditorBuildProfile::get_force_detect_classes() const { return force_detect_classes; } String EditorBuildProfile::get_build_option_name(BuildOption p_build_option) { ERR_FAIL_INDEX_V(p_build_option, BUILD_OPTION_MAX, String()); const char *build_option_names[BUILD_OPTION_MAX] = { TTRC("3D Engine"), TTRC("2D Physics"), TTRC("3D Physics"), TTRC("Navigation"), TTRC("XR"), TTRC("RenderingDevice"), TTRC("OpenGL"), TTRC("Vulkan"), }; return TTRGET(build_option_names[p_build_option]); } String EditorBuildProfile::get_build_option_description(BuildOption p_build_option) { ERR_FAIL_INDEX_V(p_build_option, BUILD_OPTION_MAX, String()); const char *build_option_descriptions[BUILD_OPTION_MAX] = { TTRC("3D Nodes as well as RenderingServer access to 3D features."), TTRC("2D Physics nodes and PhysicsServer2D."), TTRC("3D Physics nodes and PhysicsServer3D."), TTRC("Navigation, both 2D and 3D."), TTRC("XR (AR and VR)."), TTRC("RenderingDevice based rendering (if disabled, the OpenGL back-end is required)."), TTRC("OpenGL back-end (if disabled, the RenderingDevice back-end is required)."), TTRC("Vulkan back-end of RenderingDevice."), }; return TTRGET(build_option_descriptions[p_build_option]); } Error EditorBuildProfile::save_to_file(const String &p_path) { Dictionary data; data["type"] = "build_profile"; Array dis_classes; for (const StringName &E : disabled_classes) { dis_classes.push_back(String(E)); } dis_classes.sort(); data["disabled_classes"] = dis_classes; Dictionary dis_build_options; for (int i = 0; i < BUILD_OPTION_MAX; i++) { if (build_options_disabled[i]) { dis_build_options[build_option_identifiers[i]] = build_option_disable_values[i]; } } data["disabled_build_options"] = dis_build_options; if (!force_detect_classes.is_empty()) { data["force_detect_classes"] = force_detect_classes; } Ref f = FileAccess::open(p_path, FileAccess::WRITE); ERR_FAIL_COND_V_MSG(f.is_null(), ERR_CANT_CREATE, "Cannot create file '" + p_path + "'."); JSON json; String text = json.stringify(data, "\t"); f->store_string(text); return OK; } Error EditorBuildProfile::load_from_file(const String &p_path) { Error err; String text = FileAccess::get_file_as_string(p_path, &err); if (err != OK) { return err; } JSON json; err = json.parse(text); if (err != OK) { ERR_PRINT("Error parsing '" + p_path + "' on line " + itos(json.get_error_line()) + ": " + json.get_error_message()); return ERR_PARSE_ERROR; } Dictionary data = json.get_data(); if (!data.has("type") || String(data["type"]) != "build_profile") { ERR_PRINT("Error parsing '" + p_path + "', it's not a build profile."); return ERR_PARSE_ERROR; } disabled_classes.clear(); if (data.has("disabled_classes")) { Array disabled_classes_arr = data["disabled_classes"]; for (int i = 0; i < disabled_classes_arr.size(); i++) { disabled_classes.insert(disabled_classes_arr[i]); } } for (int i = 0; i < BUILD_OPTION_MAX; i++) { build_options_disabled[i] = false; } if (data.has("disabled_build_options")) { Dictionary disabled_build_options_arr = data["disabled_build_options"]; List keys; disabled_build_options_arr.get_key_list(&keys); for (const Variant &K : keys) { String key = K; for (int i = 0; i < BUILD_OPTION_MAX; i++) { String f = build_option_identifiers[i]; if (f == key) { build_options_disabled[i] = true; break; } } } } if (data.has("force_detect_classes")) { force_detect_classes = data["force_detect_classes"]; } return OK; } void EditorBuildProfile::_bind_methods() { ClassDB::bind_method(D_METHOD("set_disable_class", "class_name", "disable"), &EditorBuildProfile::set_disable_class); ClassDB::bind_method(D_METHOD("is_class_disabled", "class_name"), &EditorBuildProfile::is_class_disabled); ClassDB::bind_method(D_METHOD("set_disable_build_option", "build_option", "disable"), &EditorBuildProfile::set_disable_build_option); ClassDB::bind_method(D_METHOD("is_build_option_disabled", "build_option"), &EditorBuildProfile::is_build_option_disabled); ClassDB::bind_method(D_METHOD("get_build_option_name", "build_option"), &EditorBuildProfile::_get_build_option_name); ClassDB::bind_method(D_METHOD("save_to_file", "path"), &EditorBuildProfile::save_to_file); ClassDB::bind_method(D_METHOD("load_from_file", "path"), &EditorBuildProfile::load_from_file); BIND_ENUM_CONSTANT(BUILD_OPTION_3D); BIND_ENUM_CONSTANT(BUILD_OPTION_PHYSICS_2D); BIND_ENUM_CONSTANT(BUILD_OPTION_PHYSICS_3D); BIND_ENUM_CONSTANT(BUILD_OPTION_NAVIGATION); BIND_ENUM_CONSTANT(BUILD_OPTION_XR); BIND_ENUM_CONSTANT(BUILD_OPTION_RENDERING_DEVICE); BIND_ENUM_CONSTANT(BUILD_OPTION_OPENGL); BIND_ENUM_CONSTANT(BUILD_OPTION_VULKAN); BIND_ENUM_CONSTANT(BUILD_OPTION_MAX); } EditorBuildProfile::EditorBuildProfile() {} ////////////////////////// void EditorBuildProfileManager::_notification(int p_what) { switch (p_what) { case NOTIFICATION_READY: { String last_file = EditorSettings::get_singleton()->get_project_metadata("build_profile", "last_file_path", ""); if (!last_file.is_empty()) { _import_profile(last_file); } if (edited.is_null()) { edited.instantiate(); _update_edited_profile(); } } break; } } void EditorBuildProfileManager::_profile_action(int p_action) { last_action = Action(p_action); switch (p_action) { case ACTION_RESET: { confirm_dialog->set_text("Reset the edited profile?"); confirm_dialog->popup_centered(); } break; case ACTION_LOAD: { import_profile->popup_file_dialog(); } break; case ACTION_SAVE: { if (!profile_path->get_text().is_empty()) { Error err = edited->save_to_file(profile_path->get_text()); if (err != OK) { EditorNode::get_singleton()->show_warning(TTR("File saving failed.")); } break; } [[fallthrough]]; } case ACTION_SAVE_AS: { export_profile->popup_file_dialog(); export_profile->set_current_file(profile_path->get_text()); } break; case ACTION_NEW: { confirm_dialog->set_text("Create a new profile?"); confirm_dialog->popup_centered(); } break; case ACTION_DETECT: { confirm_dialog->set_text("This will scan all files in the current project to detect used classes."); confirm_dialog->popup_centered(); } break; case ACTION_MAX: { } break; } } void EditorBuildProfileManager::_find_files(EditorFileSystemDirectory *p_dir, const HashMap &p_cache, HashMap &r_detected) { if (p_dir == nullptr) { return; } for (int i = 0; i < p_dir->get_file_count(); i++) { String p = p_dir->get_file_path(i); uint64_t timestamp = 0; String md5; if (p_cache.has(p)) { const DetectedFile &cache = p_cache[p]; // Check if timestamp and MD5 match. timestamp = FileAccess::get_modified_time(p); bool cache_valid = true; if (cache.timestamp != timestamp) { md5 = FileAccess::get_md5(p); if (md5 != cache.md5) { cache_valid = false; } } if (cache_valid) { r_detected.insert(p, cache); continue; } } // Not cached, or cache invalid. DetectedFile cache; HashSet classes; ResourceLoader::get_classes_used(p, &classes); for (const StringName &E : classes) { cache.classes.push_back(E); } if (md5.is_empty()) { cache.timestamp = FileAccess::get_modified_time(p); cache.md5 = FileAccess::get_md5(p); } else { cache.timestamp = timestamp; cache.md5 = md5; } r_detected.insert(p, cache); } for (int i = 0; i < p_dir->get_subdir_count(); i++) { _find_files(p_dir->get_subdir(i), p_cache, r_detected); } } void EditorBuildProfileManager::_detect_classes() { HashMap previous_file_cache; Ref f = FileAccess::open("res://.godot/editor/used_class_cache", FileAccess::READ); if (f.is_valid()) { while (!f->eof_reached()) { String l = f->get_line(); Vector fields = l.split("::"); if (fields.size() == 4) { String path = fields[0]; DetectedFile df; df.timestamp = fields[1].to_int(); df.md5 = fields[2]; df.classes = fields[3].split(","); previous_file_cache.insert(path, df); } } f.unref(); } HashMap updated_file_cache; _find_files(EditorFileSystem::get_singleton()->get_filesystem(), previous_file_cache, updated_file_cache); HashSet used_classes; // Find classes and update the disk cache in the process. f = FileAccess::open("res://.godot/editor/used_class_cache", FileAccess::WRITE); for (const KeyValue &E : updated_file_cache) { String l = E.key + "::" + itos(E.value.timestamp) + "::" + E.value.md5 + "::"; for (int i = 0; i < E.value.classes.size(); i++) { String c = E.value.classes[i]; if (i > 0) { l += ","; } l += c; used_classes.insert(c); } f->store_line(l); } f.unref(); // Add forced ones. Vector force_detect = edited->get_force_detect_classes().split(","); for (int i = 0; i < force_detect.size(); i++) { String c = force_detect[i].strip_edges(); if (c.is_empty()) { continue; } used_classes.insert(c); } // Filter all classes to discard inherited ones. HashSet all_used_classes; for (const StringName &E : used_classes) { StringName c = E; if (!ClassDB::class_exists(c)) { // Maybe this is an old class that got replaced? try getting compat class. c = ClassDB::get_compatibility_class(c); if (!c) { // No luck, skip. continue; } } while (c) { all_used_classes.insert(c); c = ClassDB::get_parent_class(c); } } edited->clear_disabled_classes(); List all_classes; ClassDB::get_class_list(&all_classes); for (const StringName &E : all_classes) { if (all_used_classes.has(E)) { // This class is valid, do nothing. continue; } StringName p = ClassDB::get_parent_class(E); if (!p || all_used_classes.has(p)) { // If no parent, or if the parent is enabled, then add to disabled classes. // This way we avoid disabling redundant classes. edited->set_disable_class(E, true); } } } void EditorBuildProfileManager::_action_confirm() { switch (last_action) { case ACTION_RESET: { edited.instantiate(); _update_edited_profile(); } break; case ACTION_LOAD: { } break; case ACTION_SAVE: { } break; case ACTION_SAVE_AS: { } break; case ACTION_NEW: { profile_path->set_text(""); edited.instantiate(); _update_edited_profile(); } break; case ACTION_DETECT: { _detect_classes(); _update_edited_profile(); } break; case ACTION_MAX: { } break; } } void EditorBuildProfileManager::_fill_classes_from(TreeItem *p_parent, const String &p_class, const String &p_selected) { TreeItem *class_item = class_list->create_item(p_parent); class_item->set_cell_mode(0, TreeItem::CELL_MODE_CHECK); class_item->set_icon(0, EditorNode::get_singleton()->get_class_icon(p_class, "Node")); String text = p_class; bool disabled = edited->is_class_disabled(p_class); if (disabled) { class_item->set_custom_color(0, class_list->get_theme_color(SNAME("disabled_font_color"), SNAME("Editor"))); } class_item->set_text(0, text); class_item->set_editable(0, true); class_item->set_selectable(0, true); class_item->set_metadata(0, p_class); bool collapsed = edited->is_item_collapsed(p_class); class_item->set_collapsed(collapsed); if (p_class == p_selected) { class_item->select(0); } if (disabled) { // Class disabled, do nothing else (do not show further). return; } class_item->set_checked(0, true); // If it's not disabled, its checked. List child_classes; ClassDB::get_direct_inheriters_from_class(p_class, &child_classes); child_classes.sort_custom(); for (const StringName &name : child_classes) { if (String(name).begins_with("Editor") || ClassDB::get_api_type(name) != ClassDB::API_CORE) { continue; } _fill_classes_from(class_item, name, p_selected); } } void EditorBuildProfileManager::_class_list_item_selected() { if (updating_build_options) { return; } TreeItem *item = class_list->get_selected(); if (!item) { return; } Variant md = item->get_metadata(0); if (md.get_type() == Variant::STRING || md.get_type() == Variant::STRING_NAME) { String class_name = md; String class_description; DocTools *dd = EditorHelp::get_doc_data(); HashMap::Iterator E = dd->class_list.find(class_name); if (E) { class_description = DTR(E->value.brief_description); } description_bit->set_text(class_description); } else if (md.get_type() == Variant::INT) { int build_option_id = md; String build_option_description = EditorBuildProfile::get_build_option_description(EditorBuildProfile::BuildOption(build_option_id)); description_bit->set_text(TTRGET(build_option_description)); return; } else { return; } } void EditorBuildProfileManager::_class_list_item_edited() { if (updating_build_options) { return; } TreeItem *item = class_list->get_edited(); if (!item) { return; } bool checked = item->is_checked(0); Variant md = item->get_metadata(0); if (md.get_type() == Variant::STRING || md.get_type() == Variant::STRING_NAME) { String class_selected = md; edited->set_disable_class(class_selected, !checked); _update_edited_profile(); } else if (md.get_type() == Variant::INT) { int build_option_selected = md; edited->set_disable_build_option(EditorBuildProfile::BuildOption(build_option_selected), !checked); } } void EditorBuildProfileManager::_class_list_item_collapsed(Object *p_item) { if (updating_build_options) { return; } TreeItem *item = Object::cast_to(p_item); if (!item) { return; } Variant md = item->get_metadata(0); if (md.get_type() != Variant::STRING && md.get_type() != Variant::STRING_NAME) { return; } String class_name = md; bool collapsed = item->is_collapsed(); edited->set_item_collapsed(class_name, collapsed); } void EditorBuildProfileManager::_update_edited_profile() { String class_selected; int build_option_selected = -1; if (class_list->get_selected()) { Variant md = class_list->get_selected()->get_metadata(0); if (md.get_type() == Variant::STRING || md.get_type() == Variant::STRING_NAME) { class_selected = md; } else if (md.get_type() == Variant::INT) { build_option_selected = md; } } class_list->clear(); updating_build_options = true; TreeItem *root = class_list->create_item(); TreeItem *build_options = class_list->create_item(root); build_options->set_text(0, TTR("General Features:")); for (int i = 0; i < EditorBuildProfile::BUILD_OPTION_MAX; i++) { TreeItem *build_option; build_option = class_list->create_item(build_options); build_option->set_cell_mode(0, TreeItem::CELL_MODE_CHECK); build_option->set_text(0, EditorBuildProfile::get_build_option_name(EditorBuildProfile::BuildOption(i))); build_option->set_selectable(0, true); build_option->set_editable(0, true); build_option->set_metadata(0, i); if (!edited->is_build_option_disabled(EditorBuildProfile::BuildOption(i))) { build_option->set_checked(0, true); } if (i == build_option_selected) { build_option->select(0); } } TreeItem *classes = class_list->create_item(root); classes->set_text(0, TTR("Nodes and Classes:")); _fill_classes_from(classes, "Node", class_selected); _fill_classes_from(classes, "Resource", class_selected); force_detect_classes->set_text(edited->get_force_detect_classes()); updating_build_options = false; _class_list_item_selected(); } void EditorBuildProfileManager::_force_detect_classes_changed(const String &p_text) { if (updating_build_options) { return; } edited->set_force_detect_classes(force_detect_classes->get_text()); } void EditorBuildProfileManager::_import_profile(const String &p_path) { Ref profile; profile.instantiate(); Error err = profile->load_from_file(p_path); String basefile = p_path.get_file(); if (err != OK) { EditorNode::get_singleton()->show_warning(vformat(TTR("File '%s' format is invalid, import aborted."), basefile)); return; } profile_path->set_text(p_path); EditorSettings::get_singleton()->set_project_metadata("build_profile", "last_file_path", p_path); edited = profile; _update_edited_profile(); } void EditorBuildProfileManager::_export_profile(const String &p_path) { ERR_FAIL_COND(edited.is_null()); Error err = edited->save_to_file(p_path); if (err != OK) { EditorNode::get_singleton()->show_warning(vformat(TTR("Error saving profile to path: '%s'."), p_path)); } else { profile_path->set_text(p_path); EditorSettings::get_singleton()->set_project_metadata("build_profile", "last_file_path", p_path); } } Ref EditorBuildProfileManager::get_current_profile() { return edited; } EditorBuildProfileManager *EditorBuildProfileManager::singleton = nullptr; void EditorBuildProfileManager::_bind_methods() { ClassDB::bind_method("_update_selected_profile", &EditorBuildProfileManager::_update_edited_profile); } EditorBuildProfileManager::EditorBuildProfileManager() { VBoxContainer *main_vbc = memnew(VBoxContainer); add_child(main_vbc); HBoxContainer *path_hbc = memnew(HBoxContainer); profile_path = memnew(LineEdit); path_hbc->add_child(profile_path); profile_path->set_editable(true); profile_path->set_h_size_flags(Control::SIZE_EXPAND_FILL); profile_actions[ACTION_NEW] = memnew(Button(TTR("New"))); path_hbc->add_child(profile_actions[ACTION_NEW]); profile_actions[ACTION_NEW]->connect("pressed", callable_mp(this, &EditorBuildProfileManager::_profile_action).bind(ACTION_NEW)); profile_actions[ACTION_LOAD] = memnew(Button(TTR("Load"))); path_hbc->add_child(profile_actions[ACTION_LOAD]); profile_actions[ACTION_LOAD]->connect("pressed", callable_mp(this, &EditorBuildProfileManager::_profile_action).bind(ACTION_LOAD)); profile_actions[ACTION_SAVE] = memnew(Button(TTR("Save"))); path_hbc->add_child(profile_actions[ACTION_SAVE]); profile_actions[ACTION_SAVE]->connect("pressed", callable_mp(this, &EditorBuildProfileManager::_profile_action).bind(ACTION_SAVE)); profile_actions[ACTION_SAVE_AS] = memnew(Button(TTR("Save As"))); path_hbc->add_child(profile_actions[ACTION_SAVE_AS]); profile_actions[ACTION_SAVE_AS]->connect("pressed", callable_mp(this, &EditorBuildProfileManager::_profile_action).bind(ACTION_SAVE_AS)); main_vbc->add_margin_child(TTR("Profile:"), path_hbc); main_vbc->add_child(memnew(HSeparator)); HBoxContainer *profiles_hbc = memnew(HBoxContainer); profile_actions[ACTION_RESET] = memnew(Button(TTR("Reset to Defaults"))); profiles_hbc->add_child(profile_actions[ACTION_RESET]); profile_actions[ACTION_RESET]->connect("pressed", callable_mp(this, &EditorBuildProfileManager::_profile_action).bind(ACTION_RESET)); profile_actions[ACTION_DETECT] = memnew(Button(TTR("Detect from Project"))); profiles_hbc->add_child(profile_actions[ACTION_DETECT]); profile_actions[ACTION_DETECT]->connect("pressed", callable_mp(this, &EditorBuildProfileManager::_profile_action).bind(ACTION_DETECT)); main_vbc->add_margin_child(TTR("Actions:"), profiles_hbc); class_list = memnew(Tree); class_list->set_hide_root(true); class_list->set_edit_checkbox_cell_only_when_checkbox_is_pressed(true); class_list->connect("cell_selected", callable_mp(this, &EditorBuildProfileManager::_class_list_item_selected)); class_list->connect("item_edited", callable_mp(this, &EditorBuildProfileManager::_class_list_item_edited), CONNECT_DEFERRED); class_list->connect("item_collapsed", callable_mp(this, &EditorBuildProfileManager::_class_list_item_collapsed)); // It will be displayed once the user creates or chooses a profile. main_vbc->add_margin_child(TTR("Configure Engine Build Profile:"), class_list, true); description_bit = memnew(EditorHelpBit); description_bit->set_custom_minimum_size(Size2(0, 80) * EDSCALE); main_vbc->add_margin_child(TTR("Description:"), description_bit, false); confirm_dialog = memnew(ConfirmationDialog); add_child(confirm_dialog); confirm_dialog->set_title(TTR("Please Confirm:")); confirm_dialog->connect("confirmed", callable_mp(this, &EditorBuildProfileManager::_action_confirm)); import_profile = memnew(EditorFileDialog); add_child(import_profile); import_profile->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILE); import_profile->add_filter("*.build", TTR("Egine Build Profile")); import_profile->connect("files_selected", callable_mp(this, &EditorBuildProfileManager::_import_profile)); import_profile->set_title(TTR("Load Profile")); import_profile->set_access(EditorFileDialog::ACCESS_FILESYSTEM); export_profile = memnew(EditorFileDialog); add_child(export_profile); export_profile->set_file_mode(EditorFileDialog::FILE_MODE_SAVE_FILE); export_profile->add_filter("*.build", TTR("Egine Build Profile")); export_profile->connect("file_selected", callable_mp(this, &EditorBuildProfileManager::_export_profile)); export_profile->set_title(TTR("Export Profile")); export_profile->set_access(EditorFileDialog::ACCESS_FILESYSTEM); force_detect_classes = memnew(LineEdit); main_vbc->add_margin_child(TTR("Forced classes on detect:"), force_detect_classes); force_detect_classes->connect("text_changed", callable_mp(this, &EditorBuildProfileManager::_force_detect_classes_changed)); set_title(TTR("Edit Build Configuration Profile")); singleton = this; }