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 | |
44 | const 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 | |
62 | const 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 | |
80 | const 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 | |
98 | const 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 | |
115 | void 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 | |
123 | bool 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 | |
130 | void 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 | |
138 | bool EditorBuildProfile::is_item_collapsed(const StringName &p_class) const { |
139 | return collapsed_classes.has(p_class); |
140 | } |
141 | |
142 | void 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 | |
147 | void EditorBuildProfile::clear_disabled_classes() { |
148 | disabled_classes.clear(); |
149 | collapsed_classes.clear(); |
150 | } |
151 | |
152 | bool 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 | |
157 | bool 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 | |
162 | void EditorBuildProfile::set_force_detect_classes(const String &p_classes) { |
163 | force_detect_classes = p_classes; |
164 | } |
165 | |
166 | String EditorBuildProfile::get_force_detect_classes() const { |
167 | return force_detect_classes; |
168 | } |
169 | |
170 | String 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 | |
191 | String 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 | |
214 | EditorBuildProfile::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 | |
219 | String 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 | |
230 | Error 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 | |
265 | Error 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 | |
324 | void 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 | |
357 | EditorBuildProfile::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 | |
365 | void 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 | |
381 | void 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 | |
419 | void 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 | |
475 | void 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 | |
570 | void 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 | |
596 | void 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 | |
637 | void 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 | |
670 | void 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 | |
693 | void 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 | |
713 | void 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 | |
772 | void 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 | |
779 | void 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 | |
796 | void 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 | |
807 | Ref<EditorBuildProfile> EditorBuildProfileManager::get_current_profile() { |
808 | return edited; |
809 | } |
810 | |
811 | EditorBuildProfileManager *EditorBuildProfileManager::singleton = nullptr; |
812 | |
813 | void EditorBuildProfileManager::_bind_methods() { |
814 | ClassDB::bind_method("_update_selected_profile" , &EditorBuildProfileManager::_update_edited_profile); |
815 | } |
816 | |
817 | EditorBuildProfileManager::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 | |