1/**************************************************************************/
2/* dependency_editor.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 "dependency_editor.h"
32
33#include "core/config/project_settings.h"
34#include "core/io/file_access.h"
35#include "core/io/resource_loader.h"
36#include "editor/editor_file_system.h"
37#include "editor/editor_node.h"
38#include "editor/editor_scale.h"
39#include "editor/editor_settings.h"
40#include "editor/gui/editor_file_dialog.h"
41#include "scene/gui/margin_container.h"
42
43void DependencyEditor::_searched(const String &p_path) {
44 HashMap<String, String> dep_rename;
45 dep_rename[replacing] = p_path;
46
47 ResourceLoader::rename_dependencies(editing, dep_rename);
48
49 _update_list();
50 _update_file();
51}
52
53void DependencyEditor::_load_pressed(Object *p_item, int p_cell, int p_button, MouseButton p_mouse_button) {
54 if (p_mouse_button != MouseButton::LEFT) {
55 return;
56 }
57 TreeItem *ti = Object::cast_to<TreeItem>(p_item);
58 replacing = ti->get_text(1);
59
60 search->set_title(TTR("Search Replacement For:") + " " + replacing.get_file());
61
62 // Set directory to closest existing directory.
63 search->set_current_dir(replacing.get_base_dir());
64
65 search->clear_filters();
66 List<String> ext;
67 ResourceLoader::get_recognized_extensions_for_type(ti->get_metadata(0), &ext);
68 for (const String &E : ext) {
69 search->add_filter("*" + E);
70 }
71 search->popup_file_dialog();
72}
73
74void DependencyEditor::_fix_and_find(EditorFileSystemDirectory *efsd, HashMap<String, HashMap<String, String>> &candidates) {
75 for (int i = 0; i < efsd->get_subdir_count(); i++) {
76 _fix_and_find(efsd->get_subdir(i), candidates);
77 }
78
79 for (int i = 0; i < efsd->get_file_count(); i++) {
80 String file = efsd->get_file(i);
81 if (!candidates.has(file)) {
82 continue;
83 }
84
85 String path = efsd->get_file_path(i);
86
87 for (KeyValue<String, String> &E : candidates[file]) {
88 if (E.value.is_empty()) {
89 E.value = path;
90 continue;
91 }
92
93 //must match the best, using subdirs
94 String existing = E.value.replace_first("res://", "");
95 String current = path.replace_first("res://", "");
96 String lost = E.key.replace_first("res://", "");
97
98 Vector<String> existingv = existing.split("/");
99 existingv.reverse();
100 Vector<String> currentv = current.split("/");
101 currentv.reverse();
102 Vector<String> lostv = lost.split("/");
103 lostv.reverse();
104
105 int existing_score = 0;
106 int current_score = 0;
107
108 for (int j = 0; j < lostv.size(); j++) {
109 if (j < existingv.size() && lostv[j] == existingv[j]) {
110 existing_score++;
111 }
112 if (j < currentv.size() && lostv[j] == currentv[j]) {
113 current_score++;
114 }
115 }
116
117 if (current_score > existing_score) {
118 //if it was the same, could track distance to new path but..
119
120 E.value = path; //replace by more accurate
121 }
122 }
123 }
124}
125
126void DependencyEditor::_fix_all() {
127 if (!EditorFileSystem::get_singleton()->get_filesystem()) {
128 return;
129 }
130
131 HashMap<String, HashMap<String, String>> candidates;
132
133 for (const String &E : missing) {
134 String base = E.get_file();
135 if (!candidates.has(base)) {
136 candidates[base] = HashMap<String, String>();
137 }
138
139 candidates[base][E] = "";
140 }
141
142 _fix_and_find(EditorFileSystem::get_singleton()->get_filesystem(), candidates);
143
144 HashMap<String, String> remaps;
145
146 for (KeyValue<String, HashMap<String, String>> &E : candidates) {
147 for (const KeyValue<String, String> &F : E.value) {
148 if (!F.value.is_empty()) {
149 remaps[F.key] = F.value;
150 }
151 }
152 }
153
154 if (remaps.size()) {
155 ResourceLoader::rename_dependencies(editing, remaps);
156
157 _update_list();
158 _update_file();
159 }
160}
161
162void DependencyEditor::_update_file() {
163 EditorFileSystem::get_singleton()->update_file(editing);
164}
165
166void DependencyEditor::_update_list() {
167 List<String> deps;
168 ResourceLoader::get_dependencies(editing, &deps, true);
169
170 tree->clear();
171 missing.clear();
172
173 TreeItem *root = tree->create_item();
174
175 Ref<Texture2D> folder = tree->get_theme_icon(SNAME("folder"), SNAME("FileDialog"));
176
177 bool broken = false;
178
179 for (const String &n : deps) {
180 TreeItem *item = tree->create_item(root);
181 String path;
182 String type;
183
184 if (n.contains("::")) {
185 path = n.get_slice("::", 0);
186 type = n.get_slice("::", 1);
187 } else {
188 path = n;
189 type = "Resource";
190 }
191
192 ResourceUID::ID uid = ResourceUID::get_singleton()->text_to_id(path);
193 if (uid != ResourceUID::INVALID_ID) {
194 // Dependency is in uid format, obtain proper path.
195 if (ResourceUID::get_singleton()->has_id(uid)) {
196 path = ResourceUID::get_singleton()->get_id_path(uid);
197 } else if (n.get_slice_count("::") >= 3) {
198 // If uid can't be found, try to use fallback path.
199 path = n.get_slice("::", 2);
200 } else {
201 ERR_PRINT("Invalid dependency UID and fallback path.");
202 continue;
203 }
204 }
205
206 String name = path.get_file();
207
208 Ref<Texture2D> icon = EditorNode::get_singleton()->get_class_icon(type);
209 item->set_text(0, name);
210 item->set_icon(0, icon);
211 item->set_metadata(0, type);
212 item->set_text(1, path);
213
214 if (!FileAccess::exists(path)) {
215 item->set_custom_color(1, Color(1, 0.4, 0.3));
216 missing.push_back(path);
217 broken = true;
218 }
219
220 item->add_button(1, folder, 0);
221 }
222
223 fixdeps->set_disabled(!broken);
224}
225
226void DependencyEditor::edit(const String &p_path) {
227 editing = p_path;
228 set_title(TTR("Dependencies For:") + " " + p_path.get_file());
229
230 _update_list();
231 popup_centered_ratio(0.4);
232
233 if (EditorNode::get_singleton()->is_scene_open(p_path)) {
234 EditorNode::get_singleton()->show_warning(vformat(TTR("Scene '%s' is currently being edited.\nChanges will only take effect when reloaded."), p_path.get_file()));
235 } else if (ResourceCache::has(p_path)) {
236 EditorNode::get_singleton()->show_warning(vformat(TTR("Resource '%s' is in use.\nChanges will only take effect when reloaded."), p_path.get_file()));
237 }
238}
239
240void DependencyEditor::_bind_methods() {
241}
242
243DependencyEditor::DependencyEditor() {
244 VBoxContainer *vb = memnew(VBoxContainer);
245 vb->set_name(TTR("Dependencies"));
246 add_child(vb);
247
248 tree = memnew(Tree);
249 tree->set_columns(2);
250 tree->set_column_titles_visible(true);
251 tree->set_column_title(0, TTR("Resource"));
252 tree->set_column_clip_content(0, true);
253 tree->set_column_expand_ratio(0, 2);
254 tree->set_column_title(1, TTR("Path"));
255 tree->set_column_clip_content(1, true);
256 tree->set_column_expand_ratio(1, 1);
257 tree->set_hide_root(true);
258 tree->connect("button_clicked", callable_mp(this, &DependencyEditor::_load_pressed));
259
260 HBoxContainer *hbc = memnew(HBoxContainer);
261 Label *label = memnew(Label(TTR("Dependencies:")));
262 label->set_theme_type_variation("HeaderSmall");
263
264 hbc->add_child(label);
265 hbc->add_spacer();
266 fixdeps = memnew(Button(TTR("Fix Broken")));
267 hbc->add_child(fixdeps);
268 fixdeps->connect("pressed", callable_mp(this, &DependencyEditor::_fix_all));
269
270 vb->add_child(hbc);
271
272 MarginContainer *mc = memnew(MarginContainer);
273 mc->set_v_size_flags(Control::SIZE_EXPAND_FILL);
274
275 mc->add_child(tree);
276 vb->add_child(mc);
277
278 set_title(TTR("Dependency Editor"));
279 search = memnew(EditorFileDialog);
280 search->connect("file_selected", callable_mp(this, &DependencyEditor::_searched));
281 search->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILE);
282 search->set_title(TTR("Search Replacement Resource:"));
283 add_child(search);
284}
285
286/////////////////////////////////////
287void DependencyEditorOwners::_list_rmb_clicked(int p_item, const Vector2 &p_pos, MouseButton p_mouse_button_index) {
288 if (p_mouse_button_index != MouseButton::RIGHT) {
289 return;
290 }
291
292 file_options->clear();
293 file_options->reset_size();
294 if (p_item >= 0) {
295 PackedInt32Array selected_items = owners->get_selected_items();
296 bool only_scenes_selected = true;
297
298 for (int i = 0; i < selected_items.size(); i++) {
299 int item_idx = selected_items[i];
300 if (ResourceLoader::get_resource_type(owners->get_item_text(item_idx)) != "PackedScene") {
301 only_scenes_selected = false;
302 break;
303 }
304 }
305
306 if (only_scenes_selected) {
307 file_options->add_icon_item(get_editor_theme_icon(SNAME("Load")), TTRN("Open Scene", "Open Scenes", selected_items.size()), FILE_OPEN);
308 } else if (selected_items.size() == 1) {
309 file_options->add_icon_item(get_editor_theme_icon(SNAME("Load")), TTR("Open"), FILE_OPEN);
310 } else {
311 return;
312 }
313 }
314
315 file_options->set_position(owners->get_screen_position() + p_pos);
316 file_options->reset_size();
317 file_options->popup();
318}
319
320void DependencyEditorOwners::_select_file(int p_idx) {
321 String fpath = owners->get_item_text(p_idx);
322
323 if (ResourceLoader::get_resource_type(fpath) == "PackedScene") {
324 EditorNode::get_singleton()->open_request(fpath);
325 } else {
326 EditorNode::get_singleton()->load_resource(fpath);
327 }
328 hide();
329 emit_signal(SNAME("confirmed"));
330}
331
332void DependencyEditorOwners::_empty_clicked(const Vector2 &p_pos, MouseButton p_mouse_button_index) {
333 if (p_mouse_button_index != MouseButton::LEFT) {
334 return;
335 }
336
337 owners->deselect_all();
338}
339
340void DependencyEditorOwners::_file_option(int p_option) {
341 switch (p_option) {
342 case FILE_OPEN: {
343 PackedInt32Array selected_items = owners->get_selected_items();
344 for (int i = 0; i < selected_items.size(); i++) {
345 int item_idx = selected_items[i];
346 if (item_idx < 0 || item_idx >= owners->get_item_count()) {
347 break;
348 }
349 _select_file(item_idx);
350 }
351 } break;
352 }
353}
354
355void DependencyEditorOwners::_bind_methods() {
356}
357
358void DependencyEditorOwners::_fill_owners(EditorFileSystemDirectory *efsd) {
359 if (!efsd) {
360 return;
361 }
362
363 for (int i = 0; i < efsd->get_subdir_count(); i++) {
364 _fill_owners(efsd->get_subdir(i));
365 }
366
367 for (int i = 0; i < efsd->get_file_count(); i++) {
368 Vector<String> deps = efsd->get_file_deps(i);
369 bool found = false;
370 for (int j = 0; j < deps.size(); j++) {
371 if (deps[j] == editing) {
372 found = true;
373 break;
374 }
375 }
376 if (!found) {
377 continue;
378 }
379
380 Ref<Texture2D> icon = EditorNode::get_singleton()->get_class_icon(efsd->get_file_type(i));
381
382 owners->add_item(efsd->get_file_path(i), icon);
383 }
384}
385
386void DependencyEditorOwners::show(const String &p_path) {
387 editing = p_path;
388 owners->clear();
389 _fill_owners(EditorFileSystem::get_singleton()->get_filesystem());
390 popup_centered_ratio(0.3);
391
392 set_title(vformat(TTR("Owners of: %s (Total: %d)"), p_path.get_file(), owners->get_item_count()));
393}
394
395DependencyEditorOwners::DependencyEditorOwners() {
396 file_options = memnew(PopupMenu);
397 add_child(file_options);
398 file_options->connect("id_pressed", callable_mp(this, &DependencyEditorOwners::_file_option));
399
400 owners = memnew(ItemList);
401 owners->set_select_mode(ItemList::SELECT_MULTI);
402 owners->connect("item_clicked", callable_mp(this, &DependencyEditorOwners::_list_rmb_clicked));
403 owners->connect("item_activated", callable_mp(this, &DependencyEditorOwners::_select_file));
404 owners->connect("empty_clicked", callable_mp(this, &DependencyEditorOwners::_empty_clicked));
405 owners->set_allow_rmb_select(true);
406 add_child(owners);
407}
408
409///////////////////////
410
411void DependencyRemoveDialog::_find_files_in_removed_folder(EditorFileSystemDirectory *efsd, const String &p_folder) {
412 if (!efsd) {
413 return;
414 }
415
416 for (int i = 0; i < efsd->get_subdir_count(); ++i) {
417 _find_files_in_removed_folder(efsd->get_subdir(i), p_folder);
418 }
419 for (int i = 0; i < efsd->get_file_count(); i++) {
420 String file = efsd->get_file_path(i);
421 ERR_FAIL_COND(all_remove_files.has(file)); //We are deleting a directory which is contained in a directory we are deleting...
422 all_remove_files[file] = p_folder; //Point the file to the ancestor directory we are deleting so we know what to parent it under in the tree.
423 }
424}
425
426void DependencyRemoveDialog::_find_all_removed_dependencies(EditorFileSystemDirectory *efsd, Vector<RemovedDependency> &p_removed) {
427 if (!efsd) {
428 return;
429 }
430
431 for (int i = 0; i < efsd->get_subdir_count(); i++) {
432 _find_all_removed_dependencies(efsd->get_subdir(i), p_removed);
433 }
434
435 for (int i = 0; i < efsd->get_file_count(); i++) {
436 const String path = efsd->get_file_path(i);
437
438 //It doesn't matter if a file we are about to delete will have some of its dependencies removed too
439 if (all_remove_files.has(path)) {
440 continue;
441 }
442
443 Vector<String> all_deps = efsd->get_file_deps(i);
444 for (int j = 0; j < all_deps.size(); ++j) {
445 if (all_remove_files.has(all_deps[j])) {
446 RemovedDependency dep;
447 dep.file = path;
448 dep.file_type = efsd->get_file_type(i);
449 dep.dependency = all_deps[j];
450 dep.dependency_folder = all_remove_files[all_deps[j]];
451 p_removed.push_back(dep);
452 }
453 }
454 }
455}
456
457void DependencyRemoveDialog::_find_localization_remaps_of_removed_files(Vector<RemovedDependency> &p_removed) {
458 for (KeyValue<String, String> &files : all_remove_files) {
459 const String &path = files.key;
460
461 // Look for dependencies in the translation remaps.
462 if (ProjectSettings::get_singleton()->has_setting("internationalization/locale/translation_remaps")) {
463 Dictionary remaps = GLOBAL_GET("internationalization/locale/translation_remaps");
464
465 if (remaps.has(path)) {
466 RemovedDependency dep;
467 dep.file = TTR("Localization remap");
468 dep.file_type = "";
469 dep.dependency = path;
470 dep.dependency_folder = files.value;
471 p_removed.push_back(dep);
472 }
473
474 Array remap_keys = remaps.keys();
475 for (int j = 0; j < remap_keys.size(); j++) {
476 PackedStringArray remapped_files = remaps[remap_keys[j]];
477 for (int k = 0; k < remapped_files.size(); k++) {
478 int splitter_pos = remapped_files[k].rfind(":");
479 String res_path = remapped_files[k].substr(0, splitter_pos);
480 if (res_path == path) {
481 String locale_name = remapped_files[k].substr(splitter_pos + 1);
482
483 RemovedDependency dep;
484 dep.file = vformat(TTR("Localization remap for path '%s' and locale '%s'."), remap_keys[j], locale_name);
485 dep.file_type = "";
486 dep.dependency = path;
487 dep.dependency_folder = files.value;
488 p_removed.push_back(dep);
489 }
490 }
491 }
492 }
493 }
494}
495
496void DependencyRemoveDialog::_build_removed_dependency_tree(const Vector<RemovedDependency> &p_removed) {
497 owners->clear();
498 owners->create_item(); // root
499
500 HashMap<String, TreeItem *> tree_items;
501 for (int i = 0; i < p_removed.size(); i++) {
502 RemovedDependency rd = p_removed[i];
503
504 //Ensure that the dependency is already in the tree
505 if (!tree_items.has(rd.dependency)) {
506 if (rd.dependency_folder.length() > 0) {
507 //Ensure the ancestor folder is already in the tree
508 if (!tree_items.has(rd.dependency_folder)) {
509 TreeItem *folder_item = owners->create_item(owners->get_root());
510 folder_item->set_text(0, rd.dependency_folder);
511 folder_item->set_icon(0, owners->get_editor_theme_icon(SNAME("Folder")));
512 tree_items[rd.dependency_folder] = folder_item;
513 }
514 TreeItem *dependency_item = owners->create_item(tree_items[rd.dependency_folder]);
515 dependency_item->set_text(0, rd.dependency);
516 dependency_item->set_icon(0, owners->get_editor_theme_icon(SNAME("Warning")));
517 tree_items[rd.dependency] = dependency_item;
518 } else {
519 TreeItem *dependency_item = owners->create_item(owners->get_root());
520 dependency_item->set_text(0, rd.dependency);
521 dependency_item->set_icon(0, owners->get_editor_theme_icon(SNAME("Warning")));
522 tree_items[rd.dependency] = dependency_item;
523 }
524 }
525
526 //List this file under this dependency
527 Ref<Texture2D> icon = EditorNode::get_singleton()->get_class_icon(rd.file_type);
528 TreeItem *file_item = owners->create_item(tree_items[rd.dependency]);
529 file_item->set_text(0, rd.file);
530 file_item->set_icon(0, icon);
531 }
532}
533
534void DependencyRemoveDialog::show(const Vector<String> &p_folders, const Vector<String> &p_files) {
535 all_remove_files.clear();
536 dirs_to_delete.clear();
537 files_to_delete.clear();
538 owners->clear();
539
540 for (int i = 0; i < p_folders.size(); ++i) {
541 String folder = p_folders[i].ends_with("/") ? p_folders[i] : (p_folders[i] + "/");
542 _find_files_in_removed_folder(EditorFileSystem::get_singleton()->get_filesystem_path(folder), folder);
543 dirs_to_delete.push_back(folder);
544 }
545 for (int i = 0; i < p_files.size(); ++i) {
546 all_remove_files[p_files[i]] = String();
547 files_to_delete.push_back(p_files[i]);
548 }
549
550 Vector<RemovedDependency> removed_deps;
551 _find_all_removed_dependencies(EditorFileSystem::get_singleton()->get_filesystem(), removed_deps);
552 _find_localization_remaps_of_removed_files(removed_deps);
553 removed_deps.sort();
554 if (removed_deps.is_empty()) {
555 owners->hide();
556 text->set_text(TTR("Remove the selected files from the project? (Cannot be undone.)\nDepending on your filesystem configuration, the files will either be moved to the system trash or deleted permanently."));
557 reset_size();
558 popup_centered();
559 } else {
560 _build_removed_dependency_tree(removed_deps);
561 owners->show();
562 text->set_text(TTR("The files being removed are required by other resources in order for them to work.\nRemove them anyway? (Cannot be undone.)\nDepending on your filesystem configuration, the files will either be moved to the system trash or deleted permanently."));
563 popup_centered(Size2(500, 350));
564 }
565 EditorFileSystem::get_singleton()->scan_changes();
566}
567
568void DependencyRemoveDialog::ok_pressed() {
569 for (const KeyValue<String, String> &E : all_remove_files) {
570 String file = E.key;
571
572 if (ResourceCache::has(file)) {
573 Ref<Resource> res = ResourceCache::get_ref(file);
574 emit_signal(SNAME("resource_removed"), res);
575 res->set_path("");
576 }
577 }
578
579 for (int i = 0; i < files_to_delete.size(); ++i) {
580 // If the file we are deleting for e.g. the main scene, default environment,
581 // or audio bus layout, we must clear its definition in Project Settings.
582 if (files_to_delete[i] == String(GLOBAL_GET("application/config/icon"))) {
583 ProjectSettings::get_singleton()->set("application/config/icon", "");
584 }
585 if (files_to_delete[i] == String(GLOBAL_GET("application/run/main_scene"))) {
586 ProjectSettings::get_singleton()->set("application/run/main_scene", "");
587 }
588 if (files_to_delete[i] == String(GLOBAL_GET("application/boot_splash/image"))) {
589 ProjectSettings::get_singleton()->set("application/boot_splash/image", "");
590 }
591 if (files_to_delete[i] == String(GLOBAL_GET("rendering/environment/defaults/default_environment"))) {
592 ProjectSettings::get_singleton()->set("rendering/environment/defaults/default_environment", "");
593 }
594 if (files_to_delete[i] == String(GLOBAL_GET("display/mouse_cursor/custom_image"))) {
595 ProjectSettings::get_singleton()->set("display/mouse_cursor/custom_image", "");
596 }
597 if (files_to_delete[i] == String(GLOBAL_GET("gui/theme/custom"))) {
598 ProjectSettings::get_singleton()->set("gui/theme/custom", "");
599 }
600 if (files_to_delete[i] == String(GLOBAL_GET("gui/theme/custom_font"))) {
601 ProjectSettings::get_singleton()->set("gui/theme/custom_font", "");
602 }
603 if (files_to_delete[i] == String(GLOBAL_GET("audio/buses/default_bus_layout"))) {
604 ProjectSettings::get_singleton()->set("audio/buses/default_bus_layout", "");
605 }
606
607 String path = OS::get_singleton()->get_resource_dir() + files_to_delete[i].replace_first("res://", "/");
608 print_verbose("Moving to trash: " + path);
609 Error err = OS::get_singleton()->move_to_trash(path);
610 if (err != OK) {
611 EditorNode::get_singleton()->add_io_error(TTR("Cannot remove:") + "\n" + files_to_delete[i] + "\n");
612 } else {
613 emit_signal(SNAME("file_removed"), files_to_delete[i]);
614 }
615 }
616
617 if (dirs_to_delete.size() == 0) {
618 // If we only deleted files we should only need to tell the file system about the files we touched.
619 for (int i = 0; i < files_to_delete.size(); ++i) {
620 EditorFileSystem::get_singleton()->update_file(files_to_delete[i]);
621 }
622 } else {
623 for (int i = 0; i < dirs_to_delete.size(); ++i) {
624 String path = OS::get_singleton()->get_resource_dir() + dirs_to_delete[i].replace_first("res://", "/");
625 print_verbose("Moving to trash: " + path);
626 Error err = OS::get_singleton()->move_to_trash(path);
627 if (err != OK) {
628 EditorNode::get_singleton()->add_io_error(TTR("Cannot remove:") + "\n" + dirs_to_delete[i] + "\n");
629 } else {
630 emit_signal(SNAME("folder_removed"), dirs_to_delete[i]);
631 }
632 }
633
634 EditorFileSystem::get_singleton()->scan_changes();
635 }
636
637 // If some files/dirs would be deleted, favorite dirs need to be updated
638 Vector<String> previous_favorites = EditorSettings::get_singleton()->get_favorites();
639 Vector<String> new_favorites;
640
641 for (int i = 0; i < previous_favorites.size(); ++i) {
642 if (previous_favorites[i].ends_with("/")) {
643 if (dirs_to_delete.find(previous_favorites[i]) < 0) {
644 new_favorites.push_back(previous_favorites[i]);
645 }
646 } else {
647 if (files_to_delete.find(previous_favorites[i]) < 0) {
648 new_favorites.push_back(previous_favorites[i]);
649 }
650 }
651 }
652
653 if (new_favorites.size() < previous_favorites.size()) {
654 EditorSettings::get_singleton()->set_favorites(new_favorites);
655 }
656}
657
658void DependencyRemoveDialog::_bind_methods() {
659 ADD_SIGNAL(MethodInfo("resource_removed", PropertyInfo(Variant::OBJECT, "obj")));
660 ADD_SIGNAL(MethodInfo("file_removed", PropertyInfo(Variant::STRING, "file")));
661 ADD_SIGNAL(MethodInfo("folder_removed", PropertyInfo(Variant::STRING, "folder")));
662}
663
664DependencyRemoveDialog::DependencyRemoveDialog() {
665 set_ok_button_text(TTR("Remove"));
666
667 VBoxContainer *vb = memnew(VBoxContainer);
668 add_child(vb);
669
670 text = memnew(Label);
671 vb->add_child(text);
672
673 owners = memnew(Tree);
674 owners->set_hide_root(true);
675 vb->add_child(owners);
676 owners->set_v_size_flags(Control::SIZE_EXPAND_FILL);
677}
678
679//////////////
680
681void DependencyErrorDialog::show(Mode p_mode, const String &p_for_file, const Vector<String> &report) {
682 mode = p_mode;
683 for_file = p_for_file;
684 set_title(TTR("Error loading:") + " " + p_for_file.get_file());
685 files->clear();
686
687 TreeItem *root = files->create_item(nullptr);
688 for (int i = 0; i < report.size(); i++) {
689 String dep;
690 String type = "Object";
691 dep = report[i].get_slice("::", 0);
692 if (report[i].get_slice_count("::") > 0) {
693 type = report[i].get_slice("::", 1);
694 }
695
696 Ref<Texture2D> icon = EditorNode::get_singleton()->get_class_icon(type);
697
698 TreeItem *ti = files->create_item(root);
699 ti->set_text(0, dep);
700 ti->set_icon(0, icon);
701 }
702
703 popup_centered();
704}
705
706void DependencyErrorDialog::ok_pressed() {
707 switch (mode) {
708 case MODE_SCENE:
709 EditorNode::get_singleton()->load_scene(for_file, true);
710 break;
711 case MODE_RESOURCE:
712 EditorNode::get_singleton()->load_resource(for_file, true);
713 break;
714 }
715}
716
717void DependencyErrorDialog::custom_action(const String &) {
718 EditorNode::get_singleton()->fix_dependencies(for_file);
719}
720
721DependencyErrorDialog::DependencyErrorDialog() {
722 VBoxContainer *vb = memnew(VBoxContainer);
723 add_child(vb);
724
725 files = memnew(Tree);
726 files->set_hide_root(true);
727 vb->add_margin_child(TTR("Load failed due to missing dependencies:"), files, true);
728 files->set_v_size_flags(Control::SIZE_EXPAND_FILL);
729
730 set_min_size(Size2(500, 220) * EDSCALE);
731 set_ok_button_text(TTR("Open Anyway"));
732 set_cancel_button_text(TTR("Close"));
733
734 text = memnew(Label);
735 vb->add_child(text);
736 text->set_text(TTR("Which action should be taken?"));
737
738 mode = Mode::MODE_RESOURCE;
739
740 fdep = add_button(TTR("Fix Dependencies"), true, "fixdeps");
741
742 set_title(TTR("Errors loading!"));
743}
744
745//////////////////////////////////////////////////////////////////////
746
747void OrphanResourcesDialog::ok_pressed() {
748 paths.clear();
749
750 _find_to_delete(files->get_root(), paths);
751 if (paths.is_empty()) {
752 return;
753 }
754
755 delete_confirm->set_text(vformat(TTR("Permanently delete %d item(s)? (No undo!)"), paths.size()));
756 delete_confirm->popup_centered();
757}
758
759bool OrphanResourcesDialog::_fill_owners(EditorFileSystemDirectory *efsd, HashMap<String, int> &refs, TreeItem *p_parent) {
760 if (!efsd) {
761 return false;
762 }
763
764 bool has_children = false;
765
766 for (int i = 0; i < efsd->get_subdir_count(); i++) {
767 TreeItem *dir_item = nullptr;
768 if (p_parent) {
769 dir_item = files->create_item(p_parent);
770 dir_item->set_text(0, efsd->get_subdir(i)->get_name());
771 dir_item->set_icon(0, files->get_theme_icon(SNAME("folder"), SNAME("FileDialog")));
772 }
773 bool children = _fill_owners(efsd->get_subdir(i), refs, dir_item);
774
775 if (p_parent) {
776 if (!children) {
777 memdelete(dir_item);
778 } else {
779 has_children = true;
780 }
781 }
782 }
783
784 for (int i = 0; i < efsd->get_file_count(); i++) {
785 if (!p_parent) {
786 Vector<String> deps = efsd->get_file_deps(i);
787 for (int j = 0; j < deps.size(); j++) {
788 if (!refs.has(deps[j])) {
789 refs[deps[j]] = 1;
790 }
791 }
792 } else {
793 String path = efsd->get_file_path(i);
794 if (!refs.has(path)) {
795 TreeItem *ti = files->create_item(p_parent);
796 ti->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);
797 ti->set_text(0, efsd->get_file(i));
798 ti->set_editable(0, true);
799
800 String type = efsd->get_file_type(i);
801
802 Ref<Texture2D> icon = EditorNode::get_singleton()->get_class_icon(type);
803 ti->set_icon(0, icon);
804 int ds = efsd->get_file_deps(i).size();
805 ti->set_text(1, itos(ds));
806 if (ds) {
807 ti->add_button(1, files->get_editor_theme_icon(SNAME("GuiVisibilityVisible")), -1, false, TTR("Show Dependencies"));
808 }
809 ti->set_metadata(0, path);
810 has_children = true;
811 }
812 }
813 }
814
815 return has_children;
816}
817
818void OrphanResourcesDialog::refresh() {
819 HashMap<String, int> refs;
820 _fill_owners(EditorFileSystem::get_singleton()->get_filesystem(), refs, nullptr);
821 files->clear();
822 TreeItem *root = files->create_item();
823 _fill_owners(EditorFileSystem::get_singleton()->get_filesystem(), refs, root);
824}
825
826void OrphanResourcesDialog::show() {
827 refresh();
828 popup_centered_ratio(0.4);
829}
830
831void OrphanResourcesDialog::_find_to_delete(TreeItem *p_item, List<String> &r_paths) {
832 while (p_item) {
833 if (p_item->get_cell_mode(0) == TreeItem::CELL_MODE_CHECK && p_item->is_checked(0)) {
834 r_paths.push_back(p_item->get_metadata(0));
835 }
836
837 if (p_item->get_first_child()) {
838 _find_to_delete(p_item->get_first_child(), r_paths);
839 }
840
841 p_item = p_item->get_next();
842 }
843}
844
845void OrphanResourcesDialog::_delete_confirm() {
846 Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);
847 for (const String &E : paths) {
848 da->remove(E);
849 EditorFileSystem::get_singleton()->update_file(E);
850 }
851 refresh();
852}
853
854void OrphanResourcesDialog::_button_pressed(Object *p_item, int p_column, int p_id, MouseButton p_button) {
855 if (p_button != MouseButton::LEFT) {
856 return;
857 }
858 TreeItem *ti = Object::cast_to<TreeItem>(p_item);
859
860 String path = ti->get_metadata(0);
861 dep_edit->edit(path);
862}
863
864void OrphanResourcesDialog::_bind_methods() {
865}
866
867OrphanResourcesDialog::OrphanResourcesDialog() {
868 set_title(TTR("Orphan Resource Explorer"));
869 delete_confirm = memnew(ConfirmationDialog);
870 set_ok_button_text(TTR("Delete"));
871 add_child(delete_confirm);
872 dep_edit = memnew(DependencyEditor);
873 add_child(dep_edit);
874 delete_confirm->connect("confirmed", callable_mp(this, &OrphanResourcesDialog::_delete_confirm));
875 set_hide_on_ok(false);
876
877 VBoxContainer *vbc = memnew(VBoxContainer);
878 add_child(vbc);
879
880 files = memnew(Tree);
881 files->set_columns(2);
882 files->set_column_titles_visible(true);
883 files->set_column_custom_minimum_width(1, 100 * EDSCALE);
884 files->set_column_expand(0, true);
885 files->set_column_clip_content(0, true);
886 files->set_column_expand(1, false);
887 files->set_column_clip_content(1, true);
888 files->set_column_title(0, TTR("Resource"));
889 files->set_column_title(1, TTR("Owns"));
890 files->set_hide_root(true);
891 vbc->add_margin_child(TTR("Resources Without Explicit Ownership:"), files, true);
892 files->connect("button_clicked", callable_mp(this, &OrphanResourcesDialog::_button_pressed));
893}
894