1/**************************************************************************/
2/* editor_build_profile.cpp */
3/**************************************************************************/
4/* This file is part of: */
5/* GODOT ENGINE */
6/* https://godotengine.org */
7/**************************************************************************/
8/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10/* */
11/* Permission is hereby granted, free of charge, to any person obtaining */
12/* a copy of this software and associated documentation files (the */
13/* "Software"), to deal in the Software without restriction, including */
14/* without limitation the rights to use, copy, modify, merge, publish, */
15/* distribute, sublicense, and/or sell copies of the Software, and to */
16/* permit persons to whom the Software is furnished to do so, subject to */
17/* the following conditions: */
18/* */
19/* The above copyright notice and this permission notice shall be */
20/* included in all copies or substantial portions of the Software. */
21/* */
22/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29/**************************************************************************/
30
31#include "editor_build_profile.h"
32
33#include "core/io/dir_access.h"
34#include "core/io/json.h"
35#include "editor/editor_file_system.h"
36#include "editor/editor_node.h"
37#include "editor/editor_paths.h"
38#include "editor/editor_property_name_processor.h"
39#include "editor/editor_scale.h"
40#include "editor/editor_settings.h"
41#include "editor/editor_string_names.h"
42#include "editor/gui/editor_file_dialog.h"
43
44const char *EditorBuildProfile::build_option_identifiers[BUILD_OPTION_MAX] = {
45 // This maps to SCons build options.
46 "disable_3d",
47 "disable_2d_physics",
48 "disable_3d_physics",
49 "disable_navigation",
50 "openxr",
51 "rendering_device", // FIXME: there's no scons option to disable rendering device
52 "opengl3",
53 "vulkan",
54 "module_text_server_fb_enabled",
55 "module_text_server_adv_enabled",
56 "module_freetype_enabled",
57 "brotli",
58 "graphite",
59 "module_msdfgen_enabled"
60};
61
62const bool EditorBuildProfile::build_option_disabled_by_default[BUILD_OPTION_MAX] = {
63 // This maps to SCons build options.
64 false, // 3D
65 false, // PHYSICS_2D
66 false, // PHYSICS_3D
67 false, // NAVIGATION
68 false, // XR
69 false, // RENDERING_DEVICE
70 false, // OPENGL
71 false, // VULKAN
72 true, // TEXT_SERVER_FALLBACK
73 false, // TEXT_SERVER_COMPLEX
74 false, // DYNAMIC_FONTS
75 false, // WOFF2_FONTS
76 false, // GRPAHITE_FONTS
77 false, // MSDFGEN
78};
79
80const bool EditorBuildProfile::build_option_disable_values[BUILD_OPTION_MAX] = {
81 // This maps to SCons build options.
82 true, // 3D
83 true, // PHYSICS_2D
84 true, // PHYSICS_3D
85 true, // NAVIGATION
86 false, // XR
87 false, // RENDERING_DEVICE
88 false, // OPENGL
89 false, // VULKAN
90 false, // TEXT_SERVER_FALLBACK
91 false, // TEXT_SERVER_COMPLEX
92 false, // DYNAMIC_FONTS
93 false, // WOFF2_FONTS
94 false, // GRPAHITE_FONTS
95 false, // MSDFGEN
96};
97
98const EditorBuildProfile::BuildOptionCategory EditorBuildProfile::build_option_category[BUILD_OPTION_MAX] = {
99 BUILD_OPTION_CATEGORY_GENERAL, // 3D
100 BUILD_OPTION_CATEGORY_GENERAL, // PHYSICS_2D
101 BUILD_OPTION_CATEGORY_GENERAL, // PHYSICS_3D
102 BUILD_OPTION_CATEGORY_GENERAL, // NAVIGATION
103 BUILD_OPTION_CATEGORY_GENERAL, // XR
104 BUILD_OPTION_CATEGORY_GENERAL, // RENDERING_DEVICE
105 BUILD_OPTION_CATEGORY_GENERAL, // OPENGL
106 BUILD_OPTION_CATEGORY_GENERAL, // VULKAN
107 BUILD_OPTION_CATEGORY_TEXT_SERVER, // TEXT_SERVER_FALLBACK
108 BUILD_OPTION_CATEGORY_TEXT_SERVER, // TEXT_SERVER_COMPLEX
109 BUILD_OPTION_CATEGORY_TEXT_SERVER, // DYNAMIC_FONTS
110 BUILD_OPTION_CATEGORY_TEXT_SERVER, // WOFF2_FONTS
111 BUILD_OPTION_CATEGORY_TEXT_SERVER, // GRPAHITE_FONTS
112 BUILD_OPTION_CATEGORY_TEXT_SERVER, // MSDFGEN
113};
114
115void EditorBuildProfile::set_disable_class(const StringName &p_class, bool p_disabled) {
116 if (p_disabled) {
117 disabled_classes.insert(p_class);
118 } else {
119 disabled_classes.erase(p_class);
120 }
121}
122
123bool EditorBuildProfile::is_class_disabled(const StringName &p_class) const {
124 if (p_class == StringName()) {
125 return false;
126 }
127 return disabled_classes.has(p_class) || is_class_disabled(ClassDB::get_parent_class_nocheck(p_class));
128}
129
130void EditorBuildProfile::set_item_collapsed(const StringName &p_class, bool p_collapsed) {
131 if (p_collapsed) {
132 collapsed_classes.insert(p_class);
133 } else {
134 collapsed_classes.erase(p_class);
135 }
136}
137
138bool EditorBuildProfile::is_item_collapsed(const StringName &p_class) const {
139 return collapsed_classes.has(p_class);
140}
141
142void EditorBuildProfile::set_disable_build_option(BuildOption p_build_option, bool p_disable) {
143 ERR_FAIL_INDEX(p_build_option, BUILD_OPTION_MAX);
144 build_options_disabled[p_build_option] = p_disable;
145}
146
147void EditorBuildProfile::clear_disabled_classes() {
148 disabled_classes.clear();
149 collapsed_classes.clear();
150}
151
152bool EditorBuildProfile::is_build_option_disabled(BuildOption p_build_option) const {
153 ERR_FAIL_INDEX_V(p_build_option, BUILD_OPTION_MAX, false);
154 return build_options_disabled[p_build_option];
155}
156
157bool EditorBuildProfile::get_build_option_disable_value(BuildOption p_build_option) {
158 ERR_FAIL_INDEX_V(p_build_option, BUILD_OPTION_MAX, false);
159 return build_option_disable_values[p_build_option];
160}
161
162void EditorBuildProfile::set_force_detect_classes(const String &p_classes) {
163 force_detect_classes = p_classes;
164}
165
166String EditorBuildProfile::get_force_detect_classes() const {
167 return force_detect_classes;
168}
169
170String EditorBuildProfile::get_build_option_name(BuildOption p_build_option) {
171 ERR_FAIL_INDEX_V(p_build_option, BUILD_OPTION_MAX, String());
172 const char *build_option_names[BUILD_OPTION_MAX] = {
173 TTRC("3D Engine"),
174 TTRC("2D Physics"),
175 TTRC("3D Physics"),
176 TTRC("Navigation"),
177 TTRC("XR"),
178 TTRC("RenderingDevice"),
179 TTRC("OpenGL"),
180 TTRC("Vulkan"),
181 TTRC("Text Server: Fallback"),
182 TTRC("Text Server: Advanced"),
183 TTRC("TTF, OTF, Type 1, WOFF1 Fonts"),
184 TTRC("WOFF2 Fonts"),
185 TTRC("SIL Graphite Fonts"),
186 TTRC("Multi-channel Signed Distance Field Font Rendering"),
187 };
188 return TTRGET(build_option_names[p_build_option]);
189}
190
191String EditorBuildProfile::get_build_option_description(BuildOption p_build_option) {
192 ERR_FAIL_INDEX_V(p_build_option, BUILD_OPTION_MAX, String());
193
194 const char *build_option_descriptions[BUILD_OPTION_MAX] = {
195 TTRC("3D Nodes as well as RenderingServer access to 3D features."),
196 TTRC("2D Physics nodes and PhysicsServer2D."),
197 TTRC("3D Physics nodes and PhysicsServer3D."),
198 TTRC("Navigation, both 2D and 3D."),
199 TTRC("XR (AR and VR)."),
200 TTRC("RenderingDevice based rendering (if disabled, the OpenGL back-end is required)."),
201 TTRC("OpenGL back-end (if disabled, the RenderingDevice back-end is required)."),
202 TTRC("Vulkan back-end of RenderingDevice."),
203 TTRC("Fallback implementation of Text Server\nSupports basic text layouts."),
204 TTRC("Text Server implementation powered by ICU and HarfBuzz libraries.\nSupports complex text layouts, BiDi, and contextual OpenType font features."),
205 TTRC("TrueType, OpenType, Type 1, and WOFF1 font format support using FreeType library (if disabled, WOFF2 support is also disabled)."),
206 TTRC("WOFF2 font format support using FreeType and Brotli libraries."),
207 TTRC("SIL Graphite smart font technology support (supported by Advanced Text Server only)."),
208 TTRC("Multi-channel signed distance field font rendering support using msdfgen library (pre-rendered MSDF fonts can be used even if this option disabled)."),
209 };
210
211 return TTRGET(build_option_descriptions[p_build_option]);
212}
213
214EditorBuildProfile::BuildOptionCategory EditorBuildProfile::get_build_option_category(BuildOption p_build_option) {
215 ERR_FAIL_INDEX_V(p_build_option, BUILD_OPTION_MAX, BUILD_OPTION_CATEGORY_GENERAL);
216 return build_option_category[p_build_option];
217}
218
219String EditorBuildProfile::get_build_option_category_name(BuildOptionCategory p_build_option_category) {
220 ERR_FAIL_INDEX_V(p_build_option_category, BUILD_OPTION_CATEGORY_MAX, String());
221
222 const char *build_option_subcategories[BUILD_OPTION_CATEGORY_MAX]{
223 TTRC("General Features:"),
224 TTRC("Text Rendering and Font Options:"),
225 };
226
227 return TTRGET(build_option_subcategories[p_build_option_category]);
228}
229
230Error EditorBuildProfile::save_to_file(const String &p_path) {
231 Dictionary data;
232 data["type"] = "build_profile";
233 Array dis_classes;
234 for (const StringName &E : disabled_classes) {
235 dis_classes.push_back(String(E));
236 }
237 dis_classes.sort();
238 data["disabled_classes"] = dis_classes;
239
240 Dictionary dis_build_options;
241 for (int i = 0; i < BUILD_OPTION_MAX; i++) {
242 if (build_options_disabled[i] != build_option_disabled_by_default[i]) {
243 if (build_options_disabled[i]) {
244 dis_build_options[build_option_identifiers[i]] = build_option_disable_values[i];
245 } else {
246 dis_build_options[build_option_identifiers[i]] = !build_option_disable_values[i];
247 }
248 }
249 }
250
251 data["disabled_build_options"] = dis_build_options;
252
253 if (!force_detect_classes.is_empty()) {
254 data["force_detect_classes"] = force_detect_classes;
255 }
256
257 Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::WRITE);
258 ERR_FAIL_COND_V_MSG(f.is_null(), ERR_CANT_CREATE, "Cannot create file '" + p_path + "'.");
259
260 String text = JSON::stringify(data, "\t");
261 f->store_string(text);
262 return OK;
263}
264
265Error EditorBuildProfile::load_from_file(const String &p_path) {
266 Error err;
267 String text = FileAccess::get_file_as_string(p_path, &err);
268 if (err != OK) {
269 return err;
270 }
271
272 JSON json;
273 err = json.parse(text);
274 if (err != OK) {
275 ERR_PRINT("Error parsing '" + p_path + "' on line " + itos(json.get_error_line()) + ": " + json.get_error_message());
276 return ERR_PARSE_ERROR;
277 }
278
279 Dictionary data = json.get_data();
280
281 if (!data.has("type") || String(data["type"]) != "build_profile") {
282 ERR_PRINT("Error parsing '" + p_path + "', it's not a build profile.");
283 return ERR_PARSE_ERROR;
284 }
285
286 disabled_classes.clear();
287
288 if (data.has("disabled_classes")) {
289 Array disabled_classes_arr = data["disabled_classes"];
290 for (int i = 0; i < disabled_classes_arr.size(); i++) {
291 disabled_classes.insert(disabled_classes_arr[i]);
292 }
293 }
294
295 for (int i = 0; i < BUILD_OPTION_MAX; i++) {
296 build_options_disabled[i] = build_option_disabled_by_default[i];
297 }
298
299 if (data.has("disabled_build_options")) {
300 Dictionary disabled_build_options_arr = data["disabled_build_options"];
301 List<Variant> keys;
302 disabled_build_options_arr.get_key_list(&keys);
303
304 for (const Variant &K : keys) {
305 String key = K;
306
307 for (int i = 0; i < BUILD_OPTION_MAX; i++) {
308 String f = build_option_identifiers[i];
309 if (f == key) {
310 build_options_disabled[i] = true;
311 break;
312 }
313 }
314 }
315 }
316
317 if (data.has("force_detect_classes")) {
318 force_detect_classes = data["force_detect_classes"];
319 }
320
321 return OK;
322}
323
324void EditorBuildProfile::_bind_methods() {
325 ClassDB::bind_method(D_METHOD("set_disable_class", "class_name", "disable"), &EditorBuildProfile::set_disable_class);
326 ClassDB::bind_method(D_METHOD("is_class_disabled", "class_name"), &EditorBuildProfile::is_class_disabled);
327
328 ClassDB::bind_method(D_METHOD("set_disable_build_option", "build_option", "disable"), &EditorBuildProfile::set_disable_build_option);
329 ClassDB::bind_method(D_METHOD("is_build_option_disabled", "build_option"), &EditorBuildProfile::is_build_option_disabled);
330
331 ClassDB::bind_method(D_METHOD("get_build_option_name", "build_option"), &EditorBuildProfile::_get_build_option_name);
332
333 ClassDB::bind_method(D_METHOD("save_to_file", "path"), &EditorBuildProfile::save_to_file);
334 ClassDB::bind_method(D_METHOD("load_from_file", "path"), &EditorBuildProfile::load_from_file);
335
336 BIND_ENUM_CONSTANT(BUILD_OPTION_3D);
337 BIND_ENUM_CONSTANT(BUILD_OPTION_PHYSICS_2D);
338 BIND_ENUM_CONSTANT(BUILD_OPTION_PHYSICS_3D);
339 BIND_ENUM_CONSTANT(BUILD_OPTION_NAVIGATION);
340 BIND_ENUM_CONSTANT(BUILD_OPTION_XR);
341 BIND_ENUM_CONSTANT(BUILD_OPTION_RENDERING_DEVICE);
342 BIND_ENUM_CONSTANT(BUILD_OPTION_OPENGL);
343 BIND_ENUM_CONSTANT(BUILD_OPTION_VULKAN);
344 BIND_ENUM_CONSTANT(BUILD_OPTION_TEXT_SERVER_FALLBACK);
345 BIND_ENUM_CONSTANT(BUILD_OPTION_TEXT_SERVER_ADVANCED);
346 BIND_ENUM_CONSTANT(BUILD_OPTION_DYNAMIC_FONTS);
347 BIND_ENUM_CONSTANT(BUILD_OPTION_WOFF2_FONTS);
348 BIND_ENUM_CONSTANT(BUILD_OPTION_GRPAHITE_FONTS);
349 BIND_ENUM_CONSTANT(BUILD_OPTION_MSDFGEN);
350 BIND_ENUM_CONSTANT(BUILD_OPTION_MAX);
351
352 BIND_ENUM_CONSTANT(BUILD_OPTION_CATEGORY_GENERAL);
353 BIND_ENUM_CONSTANT(BUILD_OPTION_CATEGORY_TEXT_SERVER);
354 BIND_ENUM_CONSTANT(BUILD_OPTION_CATEGORY_MAX);
355}
356
357EditorBuildProfile::EditorBuildProfile() {
358 for (int i = 0; i < EditorBuildProfile::BUILD_OPTION_MAX; i++) {
359 build_options_disabled[i] = build_option_disabled_by_default[i];
360 }
361}
362
363//////////////////////////
364
365void EditorBuildProfileManager::_notification(int p_what) {
366 switch (p_what) {
367 case NOTIFICATION_READY: {
368 String last_file = EditorSettings::get_singleton()->get_project_metadata("build_profile", "last_file_path", "");
369 if (!last_file.is_empty()) {
370 _import_profile(last_file);
371 }
372 if (edited.is_null()) {
373 edited.instantiate();
374 _update_edited_profile();
375 }
376
377 } break;
378 }
379}
380
381void EditorBuildProfileManager::_profile_action(int p_action) {
382 last_action = Action(p_action);
383
384 switch (p_action) {
385 case ACTION_RESET: {
386 confirm_dialog->set_text("Reset the edited profile?");
387 confirm_dialog->popup_centered();
388 } break;
389 case ACTION_LOAD: {
390 import_profile->popup_file_dialog();
391 } break;
392 case ACTION_SAVE: {
393 if (!profile_path->get_text().is_empty()) {
394 Error err = edited->save_to_file(profile_path->get_text());
395 if (err != OK) {
396 EditorNode::get_singleton()->show_warning(TTR("File saving failed."));
397 }
398 break;
399 }
400 [[fallthrough]];
401 }
402 case ACTION_SAVE_AS: {
403 export_profile->popup_file_dialog();
404 export_profile->set_current_file(profile_path->get_text());
405 } break;
406 case ACTION_NEW: {
407 confirm_dialog->set_text("Create a new profile?");
408 confirm_dialog->popup_centered();
409 } break;
410 case ACTION_DETECT: {
411 confirm_dialog->set_text("This will scan all files in the current project to detect used classes.");
412 confirm_dialog->popup_centered();
413 } break;
414 case ACTION_MAX: {
415 } break;
416 }
417}
418
419void EditorBuildProfileManager::_find_files(EditorFileSystemDirectory *p_dir, const HashMap<String, DetectedFile> &p_cache, HashMap<String, DetectedFile> &r_detected) {
420 if (p_dir == nullptr) {
421 return;
422 }
423
424 for (int i = 0; i < p_dir->get_file_count(); i++) {
425 String p = p_dir->get_file_path(i);
426
427 uint64_t timestamp = 0;
428 String md5;
429
430 if (p_cache.has(p)) {
431 const DetectedFile &cache = p_cache[p];
432 // Check if timestamp and MD5 match.
433 timestamp = FileAccess::get_modified_time(p);
434 bool cache_valid = true;
435 if (cache.timestamp != timestamp) {
436 md5 = FileAccess::get_md5(p);
437 if (md5 != cache.md5) {
438 cache_valid = false;
439 }
440 }
441
442 if (cache_valid) {
443 r_detected.insert(p, cache);
444 continue;
445 }
446 }
447
448 // Not cached, or cache invalid.
449
450 DetectedFile cache;
451
452 HashSet<StringName> classes;
453 ResourceLoader::get_classes_used(p, &classes);
454
455 for (const StringName &E : classes) {
456 cache.classes.push_back(E);
457 }
458
459 if (md5.is_empty()) {
460 cache.timestamp = FileAccess::get_modified_time(p);
461 cache.md5 = FileAccess::get_md5(p);
462 } else {
463 cache.timestamp = timestamp;
464 cache.md5 = md5;
465 }
466
467 r_detected.insert(p, cache);
468 }
469
470 for (int i = 0; i < p_dir->get_subdir_count(); i++) {
471 _find_files(p_dir->get_subdir(i), p_cache, r_detected);
472 }
473}
474
475void EditorBuildProfileManager::_detect_classes() {
476 HashMap<String, DetectedFile> previous_file_cache;
477
478 Ref<FileAccess> f = FileAccess::open(EditorPaths::get_singleton()->get_project_settings_dir().path_join("used_class_cache"), FileAccess::READ);
479 if (f.is_valid()) {
480 while (!f->eof_reached()) {
481 String l = f->get_line();
482 Vector<String> fields = l.split("::");
483 if (fields.size() == 4) {
484 String path = fields[0];
485 DetectedFile df;
486 df.timestamp = fields[1].to_int();
487 df.md5 = fields[2];
488 df.classes = fields[3].split(",");
489 previous_file_cache.insert(path, df);
490 }
491 }
492 f.unref();
493 }
494
495 HashMap<String, DetectedFile> updated_file_cache;
496
497 _find_files(EditorFileSystem::get_singleton()->get_filesystem(), previous_file_cache, updated_file_cache);
498
499 HashSet<StringName> used_classes;
500
501 // Find classes and update the disk cache in the process.
502 f = FileAccess::open(EditorPaths::get_singleton()->get_project_settings_dir().path_join("used_class_cache"), FileAccess::WRITE);
503
504 for (const KeyValue<String, DetectedFile> &E : updated_file_cache) {
505 String l = E.key + "::" + itos(E.value.timestamp) + "::" + E.value.md5 + "::";
506 for (int i = 0; i < E.value.classes.size(); i++) {
507 String c = E.value.classes[i];
508 if (i > 0) {
509 l += ",";
510 }
511 l += c;
512 used_classes.insert(c);
513 }
514 f->store_line(l);
515 }
516
517 f.unref();
518
519 // Add forced ones.
520
521 Vector<String> force_detect = edited->get_force_detect_classes().split(",");
522 for (int i = 0; i < force_detect.size(); i++) {
523 String c = force_detect[i].strip_edges();
524 if (c.is_empty()) {
525 continue;
526 }
527 used_classes.insert(c);
528 }
529
530 // Filter all classes to discard inherited ones.
531
532 HashSet<StringName> all_used_classes;
533
534 for (const StringName &E : used_classes) {
535 StringName c = E;
536 if (!ClassDB::class_exists(c)) {
537 // Maybe this is an old class that got replaced? try getting compat class.
538 c = ClassDB::get_compatibility_class(c);
539 if (!c) {
540 // No luck, skip.
541 continue;
542 }
543 }
544 while (c) {
545 all_used_classes.insert(c);
546 c = ClassDB::get_parent_class(c);
547 }
548 }
549
550 edited->clear_disabled_classes();
551
552 List<StringName> all_classes;
553 ClassDB::get_class_list(&all_classes);
554
555 for (const StringName &E : all_classes) {
556 if (all_used_classes.has(E)) {
557 // This class is valid, do nothing.
558 continue;
559 }
560
561 StringName p = ClassDB::get_parent_class(E);
562 if (!p || all_used_classes.has(p)) {
563 // If no parent, or if the parent is enabled, then add to disabled classes.
564 // This way we avoid disabling redundant classes.
565 edited->set_disable_class(E, true);
566 }
567 }
568}
569
570void EditorBuildProfileManager::_action_confirm() {
571 switch (last_action) {
572 case ACTION_RESET: {
573 edited.instantiate();
574 _update_edited_profile();
575 } break;
576 case ACTION_LOAD: {
577 } break;
578 case ACTION_SAVE: {
579 } break;
580 case ACTION_SAVE_AS: {
581 } break;
582 case ACTION_NEW: {
583 profile_path->set_text("");
584 edited.instantiate();
585 _update_edited_profile();
586 } break;
587 case ACTION_DETECT: {
588 _detect_classes();
589 _update_edited_profile();
590 } break;
591 case ACTION_MAX: {
592 } break;
593 }
594}
595
596void EditorBuildProfileManager::_fill_classes_from(TreeItem *p_parent, const String &p_class, const String &p_selected) {
597 TreeItem *class_item = class_list->create_item(p_parent);
598 class_item->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);
599 class_item->set_icon(0, EditorNode::get_singleton()->get_class_icon(p_class, "Node"));
600 String text = p_class;
601
602 bool disabled = edited->is_class_disabled(p_class);
603 if (disabled) {
604 class_item->set_custom_color(0, class_list->get_theme_color(SNAME("disabled_font_color"), EditorStringName(Editor)));
605 }
606
607 class_item->set_text(0, text);
608 class_item->set_editable(0, true);
609 class_item->set_selectable(0, true);
610 class_item->set_metadata(0, p_class);
611
612 bool collapsed = edited->is_item_collapsed(p_class);
613 class_item->set_collapsed(collapsed);
614
615 if (p_class == p_selected) {
616 class_item->select(0);
617 }
618 if (disabled) {
619 // Class disabled, do nothing else (do not show further).
620 return;
621 }
622
623 class_item->set_checked(0, true); // If it's not disabled, its checked.
624
625 List<StringName> child_classes;
626 ClassDB::get_direct_inheriters_from_class(p_class, &child_classes);
627 child_classes.sort_custom<StringName::AlphCompare>();
628
629 for (const StringName &name : child_classes) {
630 if (String(name).begins_with("Editor") || ClassDB::get_api_type(name) != ClassDB::API_CORE) {
631 continue;
632 }
633 _fill_classes_from(class_item, name, p_selected);
634 }
635}
636
637void EditorBuildProfileManager::_class_list_item_selected() {
638 if (updating_build_options) {
639 return;
640 }
641
642 TreeItem *item = class_list->get_selected();
643 if (!item) {
644 return;
645 }
646
647 Variant md = item->get_metadata(0);
648 if (md.get_type() == Variant::STRING || md.get_type() == Variant::STRING_NAME) {
649 String class_name = md;
650 String class_description;
651
652 DocTools *dd = EditorHelp::get_doc_data();
653 HashMap<String, DocData::ClassDoc>::Iterator E = dd->class_list.find(class_name);
654 if (E) {
655 class_description = DTR(E->value.brief_description);
656 }
657
658 description_bit->set_text(class_description);
659 } else if (md.get_type() == Variant::INT) {
660 int build_option_id = md;
661 String build_option_description = EditorBuildProfile::get_build_option_description(EditorBuildProfile::BuildOption(build_option_id));
662
663 description_bit->set_text(TTRGET(build_option_description));
664 return;
665 } else {
666 return;
667 }
668}
669
670void EditorBuildProfileManager::_class_list_item_edited() {
671 if (updating_build_options) {
672 return;
673 }
674
675 TreeItem *item = class_list->get_edited();
676 if (!item) {
677 return;
678 }
679
680 bool checked = item->is_checked(0);
681
682 Variant md = item->get_metadata(0);
683 if (md.get_type() == Variant::STRING || md.get_type() == Variant::STRING_NAME) {
684 String class_selected = md;
685 edited->set_disable_class(class_selected, !checked);
686 _update_edited_profile();
687 } else if (md.get_type() == Variant::INT) {
688 int build_option_selected = md;
689 edited->set_disable_build_option(EditorBuildProfile::BuildOption(build_option_selected), !checked);
690 }
691}
692
693void EditorBuildProfileManager::_class_list_item_collapsed(Object *p_item) {
694 if (updating_build_options) {
695 return;
696 }
697
698 TreeItem *item = Object::cast_to<TreeItem>(p_item);
699 if (!item) {
700 return;
701 }
702
703 Variant md = item->get_metadata(0);
704 if (md.get_type() != Variant::STRING && md.get_type() != Variant::STRING_NAME) {
705 return;
706 }
707
708 String class_name = md;
709 bool collapsed = item->is_collapsed();
710 edited->set_item_collapsed(class_name, collapsed);
711}
712
713void EditorBuildProfileManager::_update_edited_profile() {
714 String class_selected;
715 int build_option_selected = -1;
716
717 if (class_list->get_selected()) {
718 Variant md = class_list->get_selected()->get_metadata(0);
719 if (md.get_type() == Variant::STRING || md.get_type() == Variant::STRING_NAME) {
720 class_selected = md;
721 } else if (md.get_type() == Variant::INT) {
722 build_option_selected = md;
723 }
724 }
725
726 class_list->clear();
727
728 updating_build_options = true;
729
730 TreeItem *root = class_list->create_item();
731
732 HashMap<EditorBuildProfile::BuildOptionCategory, TreeItem *> subcats;
733 for (int i = 0; i < EditorBuildProfile::BUILD_OPTION_CATEGORY_MAX; i++) {
734 TreeItem *build_cat;
735 build_cat = class_list->create_item(root);
736
737 build_cat->set_text(0, EditorBuildProfile::get_build_option_category_name(EditorBuildProfile::BuildOptionCategory(i)));
738 subcats[EditorBuildProfile::BuildOptionCategory(i)] = build_cat;
739 }
740
741 for (int i = 0; i < EditorBuildProfile::BUILD_OPTION_MAX; i++) {
742 TreeItem *build_option;
743 build_option = class_list->create_item(subcats[EditorBuildProfile::get_build_option_category(EditorBuildProfile::BuildOption(i))]);
744
745 build_option->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);
746 build_option->set_text(0, EditorBuildProfile::get_build_option_name(EditorBuildProfile::BuildOption(i)));
747 build_option->set_selectable(0, true);
748 build_option->set_editable(0, true);
749 build_option->set_metadata(0, i);
750 if (!edited->is_build_option_disabled(EditorBuildProfile::BuildOption(i))) {
751 build_option->set_checked(0, true);
752 }
753
754 if (i == build_option_selected) {
755 build_option->select(0);
756 }
757 }
758
759 TreeItem *classes = class_list->create_item(root);
760 classes->set_text(0, TTR("Nodes and Classes:"));
761
762 _fill_classes_from(classes, "Node", class_selected);
763 _fill_classes_from(classes, "Resource", class_selected);
764
765 force_detect_classes->set_text(edited->get_force_detect_classes());
766
767 updating_build_options = false;
768
769 _class_list_item_selected();
770}
771
772void EditorBuildProfileManager::_force_detect_classes_changed(const String &p_text) {
773 if (updating_build_options) {
774 return;
775 }
776 edited->set_force_detect_classes(force_detect_classes->get_text());
777}
778
779void EditorBuildProfileManager::_import_profile(const String &p_path) {
780 Ref<EditorBuildProfile> profile;
781 profile.instantiate();
782 Error err = profile->load_from_file(p_path);
783 String basefile = p_path.get_file();
784 if (err != OK) {
785 EditorNode::get_singleton()->show_warning(vformat(TTR("File '%s' format is invalid, import aborted."), basefile));
786 return;
787 }
788
789 profile_path->set_text(p_path);
790 EditorSettings::get_singleton()->set_project_metadata("build_profile", "last_file_path", p_path);
791
792 edited = profile;
793 _update_edited_profile();
794}
795
796void EditorBuildProfileManager::_export_profile(const String &p_path) {
797 ERR_FAIL_COND(edited.is_null());
798 Error err = edited->save_to_file(p_path);
799 if (err != OK) {
800 EditorNode::get_singleton()->show_warning(vformat(TTR("Error saving profile to path: '%s'."), p_path));
801 } else {
802 profile_path->set_text(p_path);
803 EditorSettings::get_singleton()->set_project_metadata("build_profile", "last_file_path", p_path);
804 }
805}
806
807Ref<EditorBuildProfile> EditorBuildProfileManager::get_current_profile() {
808 return edited;
809}
810
811EditorBuildProfileManager *EditorBuildProfileManager::singleton = nullptr;
812
813void EditorBuildProfileManager::_bind_methods() {
814 ClassDB::bind_method("_update_selected_profile", &EditorBuildProfileManager::_update_edited_profile);
815}
816
817EditorBuildProfileManager::EditorBuildProfileManager() {
818 VBoxContainer *main_vbc = memnew(VBoxContainer);
819 add_child(main_vbc);
820
821 HBoxContainer *path_hbc = memnew(HBoxContainer);
822 profile_path = memnew(LineEdit);
823 path_hbc->add_child(profile_path);
824 profile_path->set_editable(true);
825 profile_path->set_h_size_flags(Control::SIZE_EXPAND_FILL);
826
827 profile_actions[ACTION_NEW] = memnew(Button(TTR("New")));
828 path_hbc->add_child(profile_actions[ACTION_NEW]);
829 profile_actions[ACTION_NEW]->connect("pressed", callable_mp(this, &EditorBuildProfileManager::_profile_action).bind(ACTION_NEW));
830
831 profile_actions[ACTION_LOAD] = memnew(Button(TTR("Load")));
832 path_hbc->add_child(profile_actions[ACTION_LOAD]);
833 profile_actions[ACTION_LOAD]->connect("pressed", callable_mp(this, &EditorBuildProfileManager::_profile_action).bind(ACTION_LOAD));
834
835 profile_actions[ACTION_SAVE] = memnew(Button(TTR("Save")));
836 path_hbc->add_child(profile_actions[ACTION_SAVE]);
837 profile_actions[ACTION_SAVE]->connect("pressed", callable_mp(this, &EditorBuildProfileManager::_profile_action).bind(ACTION_SAVE));
838
839 profile_actions[ACTION_SAVE_AS] = memnew(Button(TTR("Save As")));
840 path_hbc->add_child(profile_actions[ACTION_SAVE_AS]);
841 profile_actions[ACTION_SAVE_AS]->connect("pressed", callable_mp(this, &EditorBuildProfileManager::_profile_action).bind(ACTION_SAVE_AS));
842
843 main_vbc->add_margin_child(TTR("Profile:"), path_hbc);
844
845 main_vbc->add_child(memnew(HSeparator));
846
847 HBoxContainer *profiles_hbc = memnew(HBoxContainer);
848
849 profile_actions[ACTION_RESET] = memnew(Button(TTR("Reset to Defaults")));
850 profiles_hbc->add_child(profile_actions[ACTION_RESET]);
851 profile_actions[ACTION_RESET]->connect("pressed", callable_mp(this, &EditorBuildProfileManager::_profile_action).bind(ACTION_RESET));
852
853 profile_actions[ACTION_DETECT] = memnew(Button(TTR("Detect from Project")));
854 profiles_hbc->add_child(profile_actions[ACTION_DETECT]);
855 profile_actions[ACTION_DETECT]->connect("pressed", callable_mp(this, &EditorBuildProfileManager::_profile_action).bind(ACTION_DETECT));
856
857 main_vbc->add_margin_child(TTR("Actions:"), profiles_hbc);
858
859 class_list = memnew(Tree);
860 class_list->set_hide_root(true);
861 class_list->set_edit_checkbox_cell_only_when_checkbox_is_pressed(true);
862 class_list->connect("cell_selected", callable_mp(this, &EditorBuildProfileManager::_class_list_item_selected));
863 class_list->connect("item_edited", callable_mp(this, &EditorBuildProfileManager::_class_list_item_edited), CONNECT_DEFERRED);
864 class_list->connect("item_collapsed", callable_mp(this, &EditorBuildProfileManager::_class_list_item_collapsed));
865 // It will be displayed once the user creates or chooses a profile.
866 main_vbc->add_margin_child(TTR("Configure Engine Build Profile:"), class_list, true);
867
868 description_bit = memnew(EditorHelpBit);
869 description_bit->set_custom_minimum_size(Size2(0, 80) * EDSCALE);
870 main_vbc->add_margin_child(TTR("Description:"), description_bit, false);
871
872 confirm_dialog = memnew(ConfirmationDialog);
873 add_child(confirm_dialog);
874 confirm_dialog->set_title(TTR("Please Confirm:"));
875 confirm_dialog->connect("confirmed", callable_mp(this, &EditorBuildProfileManager::_action_confirm));
876
877 import_profile = memnew(EditorFileDialog);
878 add_child(import_profile);
879 import_profile->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILE);
880 import_profile->add_filter("*.build", TTR("Engine Build Profile"));
881 import_profile->connect("files_selected", callable_mp(this, &EditorBuildProfileManager::_import_profile));
882 import_profile->set_title(TTR("Load Profile"));
883 import_profile->set_access(EditorFileDialog::ACCESS_FILESYSTEM);
884
885 export_profile = memnew(EditorFileDialog);
886 add_child(export_profile);
887 export_profile->set_file_mode(EditorFileDialog::FILE_MODE_SAVE_FILE);
888 export_profile->add_filter("*.build", TTR("Engine Build Profile"));
889 export_profile->connect("file_selected", callable_mp(this, &EditorBuildProfileManager::_export_profile));
890 export_profile->set_title(TTR("Export Profile"));
891 export_profile->set_access(EditorFileDialog::ACCESS_FILESYSTEM);
892
893 force_detect_classes = memnew(LineEdit);
894 main_vbc->add_margin_child(TTR("Forced classes on detect:"), force_detect_classes);
895 force_detect_classes->connect("text_changed", callable_mp(this, &EditorBuildProfileManager::_force_detect_classes_changed));
896
897 set_title(TTR("Edit Build Configuration Profile"));
898
899 singleton = this;
900}
901