Path: blob/master/editor/file_system/dependency_editor.cpp
20871 views
/**************************************************************************/1/* dependency_editor.cpp */2/**************************************************************************/3/* This file is part of: */4/* GODOT ENGINE */5/* https://godotengine.org */6/**************************************************************************/7/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */8/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */9/* */10/* Permission is hereby granted, free of charge, to any person obtaining */11/* a copy of this software and associated documentation files (the */12/* "Software"), to deal in the Software without restriction, including */13/* without limitation the rights to use, copy, modify, merge, publish, */14/* distribute, sublicense, and/or sell copies of the Software, and to */15/* permit persons to whom the Software is furnished to do so, subject to */16/* the following conditions: */17/* */18/* The above copyright notice and this permission notice shall be */19/* included in all copies or substantial portions of the Software. */20/* */21/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */22/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */23/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */24/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */25/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */26/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */27/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */28/**************************************************************************/2930#include "dependency_editor.h"3132#include "core/config/project_settings.h"33#include "core/io/file_access.h"34#include "core/io/resource_loader.h"35#include "editor/editor_node.h"36#include "editor/editor_string_names.h"37#include "editor/file_system/editor_file_system.h"38#include "editor/gui/editor_file_dialog.h"39#include "editor/gui/editor_quick_open_dialog.h"40#include "editor/settings/editor_settings.h"41#include "editor/themes/editor_scale.h"42#include "scene/gui/box_container.h"43#include "scene/gui/item_list.h"44#include "scene/gui/line_edit.h"45#include "scene/gui/margin_container.h"46#include "scene/gui/menu_button.h"47#include "scene/gui/popup_menu.h"48#include "scene/gui/tree.h"4950static void _setup_search_file_dialog(EditorFileDialog *p_dialog, const String &p_file, const String &p_type) {51p_dialog->set_title(vformat(TTR("Search Replacement For: %s"), p_file.get_file()));5253// Set directory to closest existing directory.54p_dialog->set_current_dir(p_file.get_base_dir());5556p_dialog->clear_filters();57List<String> ext;58ResourceLoader::get_recognized_extensions_for_type(p_type, &ext);59for (const String &E : ext) {60p_dialog->add_filter("*." + E);61}62}6364struct DependencyEditorSortByType {65bool operator()(const String &p_a, const String &p_b) const {66const String a_type = p_a.contains("::") ? p_a.get_slice("::", 1) : "Resource";67const String b_type = p_b.contains("::") ? p_b.get_slice("::", 1) : "Resource";68const String a_path = p_a.contains("::") ? p_a.get_slice("::", 2) : p_a;69const String b_path = p_b.contains("::") ? p_b.get_slice("::", 2) : p_b;70return a_type == b_type ? a_path < b_path : a_type < b_type;71}72};7374struct DependencyEditorSortByPath {75bool operator()(const String &p_a, const String &p_b) const {76const String a_path = p_a.contains("::") ? p_a.get_slice("::", 2) : p_a;77const String b_path = p_b.contains("::") ? p_b.get_slice("::", 2) : p_b;78return a_path < b_path;79}80};8182struct DependencyEditorSortByFile {83bool operator()(const String &p_a, const String &p_b) const {84const String a_path = p_a.contains("::") ? p_a.get_slice("::", 2) : p_a;85const String b_path = p_b.contains("::") ? p_b.get_slice("::", 2) : p_b;86const String a_file = a_path.get_file();87const String b_file = b_path.get_file();88return a_file == b_file ? a_path < b_path : a_file < b_file;89}90};9192void DependencyEditor::_searched(const String &p_path) {93HashMap<String, String> dep_rename;94dep_rename[replacing] = p_path;9596ResourceLoader::rename_dependencies(editing, dep_rename);9798_update_list();99_update_file();100}101102void DependencyEditor::_load_pressed(Object *p_item, int p_cell, int p_button, MouseButton p_mouse_button) {103if (p_mouse_button != MouseButton::LEFT) {104return;105}106TreeItem *ti = Object::cast_to<TreeItem>(p_item);107replacing = ti->get_text((int)Column::PATH);108109_setup_search_file_dialog(search, replacing, ti->get_metadata(0));110search->popup_file_dialog();111}112113void DependencyEditor::_fix_and_find(EditorFileSystemDirectory *efsd, HashMap<String, HashMap<String, String>> &candidates) {114for (int i = 0; i < efsd->get_subdir_count(); i++) {115_fix_and_find(efsd->get_subdir(i), candidates);116}117118for (int i = 0; i < efsd->get_file_count(); i++) {119String file = efsd->get_file(i);120if (!candidates.has(file)) {121continue;122}123124String path = efsd->get_file_path(i);125126for (KeyValue<String, String> &E : candidates[file]) {127if (E.value.is_empty()) {128E.value = path;129continue;130}131132//must match the best, using subdirs133String existing = E.value.replace_first("res://", "");134String current = path.replace_first("res://", "");135String lost = E.key.replace_first("res://", "");136137Vector<String> existingv = existing.split("/");138existingv.reverse();139Vector<String> currentv = current.split("/");140currentv.reverse();141Vector<String> lostv = lost.split("/");142lostv.reverse();143144int existing_score = 0;145int current_score = 0;146147for (int j = 0; j < lostv.size(); j++) {148if (j < existingv.size() && lostv[j] == existingv[j]) {149existing_score++;150}151if (j < currentv.size() && lostv[j] == currentv[j]) {152current_score++;153}154}155156if (current_score > existing_score) {157//if it was the same, could track distance to new path but..158159E.value = path; //replace by more accurate160}161}162}163}164165void DependencyEditor::_fix_all() {166if (!EditorFileSystem::get_singleton()->get_filesystem()) {167return;168}169170HashMap<String, HashMap<String, String>> candidates;171172for (const String &E : missing) {173String base = E.get_file();174if (!candidates.has(base)) {175candidates[base] = HashMap<String, String>();176}177178candidates[base][E] = "";179}180181_fix_and_find(EditorFileSystem::get_singleton()->get_filesystem(), candidates);182183HashMap<String, String> remaps;184185for (KeyValue<String, HashMap<String, String>> &E : candidates) {186for (const KeyValue<String, String> &F : E.value) {187if (!F.value.is_empty()) {188remaps[F.key] = F.value;189}190}191}192193if (remaps.size()) {194ResourceLoader::rename_dependencies(editing, remaps);195196_update_list();197_update_file();198}199}200201void DependencyEditor::_update_file() {202EditorFileSystem::get_singleton()->update_file(editing);203}204205void DependencyEditor::_notification(int p_what) {206if (p_what == NOTIFICATION_THEME_CHANGED) {207warning_label->add_theme_color_override(SceneStringName(font_color), get_theme_color("warning_color", EditorStringName(Editor)));208filter->set_right_icon(get_editor_theme_icon(SNAME("Search")));209menu_sort->set_button_icon(get_editor_theme_icon(SNAME("Sort")));210}211}212213static String _get_resolved_dep_path(const String &p_dep) {214if (p_dep.get_slice_count("::") < 3) {215return p_dep.get_slice("::", 0); // No UID, just return the path.216}217218const String uid_text = p_dep.get_slice("::", 0);219ResourceUID::ID uid = ResourceUID::get_singleton()->text_to_id(uid_text);220221// Dependency is in UID format, obtain proper path.222if (uid != ResourceUID::INVALID_ID && ResourceUID::get_singleton()->has_id(uid)) {223return ResourceUID::get_singleton()->get_id_path(uid);224}225226// UID fallback path.227return p_dep.get_slice("::", 2);228}229230static String _get_stored_dep_path(const String &p_dep) {231if (p_dep.get_slice_count("::") > 2) {232return p_dep.get_slice("::", 2);233}234return p_dep.get_slice("::", 0);235}236237List<String> DependencyEditor::_filter_deps(const List<String> &p_deps) {238const String filter_text = filter->get_text();239240if (filter_text.is_empty()) {241return p_deps;242}243244List<String> filtered;245246for (const String &item : p_deps) {247const String path = item.contains("::") ? item.get_slice("::", 2) : item;248249if (path.containsn(filter_text)) {250filtered.push_back(item);251}252}253254return filtered;255}256257void DependencyEditor::_update_list() {258List<String> deps;259ResourceLoader::get_dependencies(editing, &deps, true);260deps = _filter_deps(deps);261262switch (sort_by) {263case DependencyEditorSortBy::TYPE:264deps.sort_custom<DependencyEditorSortByType>();265break;266case DependencyEditorSortBy::TYPE_REVERSE:267deps.sort_custom<DependencyEditorSortByType>();268deps.reverse();269break;270case DependencyEditorSortBy::PATH:271deps.sort_custom<DependencyEditorSortByPath>();272break;273case DependencyEditorSortBy::PATH_REVERSE:274deps.sort_custom<DependencyEditorSortByPath>();275deps.reverse();276break;277case DependencyEditorSortBy::NAME:278deps.sort_custom<DependencyEditorSortByFile>();279break;280case DependencyEditorSortBy::NAME_REVERSE:281deps.sort_custom<DependencyEditorSortByFile>();282deps.reverse();283break;284default:285break;286}287288tree->clear();289missing.clear();290291TreeItem *root = tree->create_item();292293Ref<Texture2D> folder = tree->get_theme_icon(SNAME("folder"), SNAME("FileDialog"));294295bool broken = false;296297for (const String &dep : deps) {298TreeItem *item = tree->create_item(root);299300const String path = _get_resolved_dep_path(dep);301if (FileAccess::exists(path)) {302item->set_text((int)Column::NAME, path.get_file());303item->set_text((int)Column::PATH, path);304} else {305const String &stored_path = _get_stored_dep_path(dep);306item->set_text((int)Column::NAME, stored_path.get_file());307item->set_text((int)Column::PATH, stored_path);308item->set_custom_color((int)Column::PATH, Color(1, 0.4, 0.3));309missing.push_back(stored_path);310broken = true;311}312313const String type = dep.contains("::") ? dep.get_slice("::", 1) : "Resource";314String name = path.get_file();315Ref<Texture2D> icon = EditorNode::get_singleton()->get_class_icon(type);316item->set_icon((int)Column::TYPE, icon);317item->set_metadata(0, type);318item->set_text((int)Column::TYPE, type);319item->set_text((int)Column::NAME, name);320item->set_text((int)Column::PATH, path);321item->add_button((int)Column::PATH, folder, 0);322}323324fixdeps->set_disabled(!broken);325}326327void DependencyEditor::edit(const String &p_path) {328editing = p_path;329set_title(TTR("Dependencies For:") + " " + p_path.get_file());330331filter->set_text("");332333_update_menu_sort();334_update_list();335336if (EditorNode::get_singleton()->is_scene_open(p_path)) {337warning_label->show();338warning_label->set_text(vformat(TTR("Scene \"%s\" is currently being edited. Changes will only take effect when reloaded."), p_path.get_file()));339} else if (ResourceCache::has(p_path)) {340warning_label->show();341warning_label->set_text(vformat(TTR("Resource \"%s\" is in use. Changes will only take effect when reloaded."), p_path.get_file()));342} else {343warning_label->hide();344}345popup_centered_ratio(0.4);346}347348void DependencyEditor::_sort_option_selected(int p_id) {349sort_by = (DependencyEditorSortBy)p_id;350_update_menu_sort();351_update_list();352}353354void DependencyEditor::_update_menu_sort() {355for (int i = 0; i != (int)DependencyEditorSortBy::MAX; i++) {356menu_sort->get_popup()->set_item_checked(i, (i == (int)sort_by));357}358}359360DependencyEditor::DependencyEditor() {361VBoxContainer *vb = memnew(VBoxContainer);362add_child(vb);363364tree = memnew(Tree);365tree->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);366tree->set_theme_type_variation("TreeTable");367tree->set_hide_folding(true);368tree->set_columns((int)Column::MAX);369tree->set_column_titles_visible(true);370tree->set_column_title((int)Column::TYPE, TTRC("Type"));371tree->set_column_clip_content((int)Column::TYPE, true);372tree->set_column_expand_ratio((int)Column::TYPE, 2);373tree->set_column_title((int)Column::NAME, TTRC("Name"));374tree->set_column_clip_content((int)Column::NAME, true);375tree->set_column_expand_ratio((int)Column::NAME, 3);376tree->set_column_title((int)Column::PATH, TTRC("Path"));377tree->set_column_clip_content((int)Column::PATH, true);378tree->set_column_expand_ratio((int)Column::PATH, 5);379tree->set_hide_root(true);380tree->connect("button_clicked", callable_mp(this, &DependencyEditor::_load_pressed));381382HBoxContainer *hbc = memnew(HBoxContainer);383Label *label = memnew(Label(TTR("Dependencies:")));384label->set_theme_type_variation("HeaderSmall");385386hbc->add_child(label);387hbc->add_spacer();388fixdeps = memnew(Button(TTR("Fix Broken")));389hbc->add_child(fixdeps);390fixdeps->connect(SceneStringName(pressed), callable_mp(this, &DependencyEditor::_fix_all));391392vb->add_child(hbc);393394HBoxContainer *hbc_filter = memnew(HBoxContainer);395vb->add_child(hbc_filter);396filter = memnew(LineEdit);397filter->set_accessibility_name(TTRC("Filter Dependencies"));398filter->set_placeholder(TTRC("Filter Dependencies"));399filter->set_clear_button_enabled(true);400filter->set_h_size_flags(Control::SIZE_EXPAND_FILL);401filter->connect(SceneStringName(text_changed), callable_mp(this, &DependencyEditor::_update_list).unbind(1));402hbc_filter->add_child(filter);403404menu_sort = memnew(MenuButton);405menu_sort->set_flat(false);406menu_sort->set_theme_type_variation("FlatMenuButton");407menu_sort->set_tooltip_text(TTRC("Sort Dependencies"));408menu_sort->set_accessibility_name(TTRC("Sort Dependencies"));409410PopupMenu *popup_sort = menu_sort->get_popup();411popup_sort->connect(SceneStringName(id_pressed), callable_mp(this, &DependencyEditor::_sort_option_selected));412popup_sort->add_radio_check_item(TTRC("Sort by Type (Ascending)"), (int)DependencyEditorSortBy::TYPE);413popup_sort->add_radio_check_item(TTRC("Sort by Type (Descending)"), (int)DependencyEditorSortBy::TYPE_REVERSE);414popup_sort->add_radio_check_item(TTRC("Sort by Name (Ascending)"), (int)DependencyEditorSortBy::NAME);415popup_sort->add_radio_check_item(TTRC("Sort by Name (Descending)"), (int)DependencyEditorSortBy::NAME_REVERSE);416popup_sort->add_radio_check_item(TTRC("Sort by Path (Ascending)"), (int)DependencyEditorSortBy::PATH);417popup_sort->add_radio_check_item(TTRC("Sort by Path (Descending)"), (int)DependencyEditorSortBy::PATH_REVERSE);418popup_sort->set_item_checked((int)sort_by, true);419420hbc_filter->add_child(menu_sort);421422MarginContainer *mc = memnew(MarginContainer);423mc->set_v_size_flags(Control::SIZE_EXPAND_FILL);424425mc->add_child(tree);426vb->add_child(mc);427428warning_label = memnew(Label);429warning_label->set_autowrap_mode(TextServer::AUTOWRAP_WORD);430vb->add_child(warning_label);431432set_title(TTR("Dependency Editor"));433search = memnew(EditorFileDialog);434search->connect("file_selected", callable_mp(this, &DependencyEditor::_searched));435search->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILE);436add_child(search);437}438439/////////////////////////////////////440void DependencyEditorOwners::_list_rmb_clicked(int p_item, const Vector2 &p_pos, MouseButton p_mouse_button_index) {441if (p_mouse_button_index != MouseButton::RIGHT) {442return;443}444445file_options->clear();446file_options->reset_size();447if (p_item >= 0) {448PackedInt32Array selected_items = owners->get_selected_items();449bool only_scenes_selected = true;450451for (int i = 0; i < selected_items.size(); i++) {452int item_idx = selected_items[i];453if (ResourceLoader::get_resource_type(owners->get_item_text(item_idx)) != "PackedScene") {454only_scenes_selected = false;455break;456}457}458459if (only_scenes_selected) {460file_options->add_icon_item(get_editor_theme_icon(SNAME("Load")), TTRN("Open Scene", "Open Scenes", selected_items.size()), FILE_MENU_OPEN);461} else if (selected_items.size() == 1) {462file_options->add_icon_item(get_editor_theme_icon(SNAME("Load")), TTR("Open"), FILE_MENU_OPEN);463} else {464return;465}466}467468file_options->set_position(owners->get_screen_position() + p_pos);469file_options->reset_size();470file_options->popup();471}472473void DependencyEditorOwners::_select_file(int p_idx) {474String fpath = owners->get_item_text(p_idx);475EditorNode::get_singleton()->load_scene_or_resource(fpath);476477hide();478emit_signal(SceneStringName(confirmed));479}480481void DependencyEditorOwners::_empty_clicked(const Vector2 &p_pos, MouseButton p_mouse_button_index) {482if (p_mouse_button_index != MouseButton::LEFT) {483return;484}485486owners->deselect_all();487}488489void DependencyEditorOwners::_file_option(int p_option) {490switch (p_option) {491case FILE_MENU_OPEN: {492PackedInt32Array selected_items = owners->get_selected_items();493for (int i = 0; i < selected_items.size(); i++) {494int item_idx = selected_items[i];495if (item_idx < 0 || item_idx >= owners->get_item_count()) {496break;497}498_select_file(item_idx);499}500} break;501}502}503504void DependencyEditorOwners::_fill_owners(EditorFileSystemDirectory *efsd) {505if (!efsd) {506return;507}508509for (int i = 0; i < efsd->get_subdir_count(); i++) {510_fill_owners(efsd->get_subdir(i));511}512513for (int i = 0; i < efsd->get_file_count(); i++) {514Vector<String> deps = efsd->get_file_deps(i);515bool found = false;516for (int j = 0; j < deps.size(); j++) {517if (deps[j] == editing) {518found = true;519break;520}521}522if (!found) {523continue;524}525526Ref<Texture2D> icon = EditorNode::get_singleton()->get_class_icon(efsd->get_file_type(i));527528owners->add_item(efsd->get_file_path(i), icon);529}530}531532void DependencyEditorOwners::show(const String &p_path) {533editing = p_path;534owners->clear();535_fill_owners(EditorFileSystem::get_singleton()->get_filesystem());536537int count = owners->get_item_count();538if (count > 0) {539empty->hide();540owners_count->set_text(vformat(TTR("Owners of: %s (Total: %d)"), p_path.get_file(), count));541owners_count->show();542owners_mc->show();543} else {544owners_count->hide();545owners_mc->hide();546empty->set_text(vformat(TTR("No owners found for: %s"), p_path.get_file()));547empty->show();548}549550popup_centered_ratio(0.3);551}552553DependencyEditorOwners::DependencyEditorOwners() {554file_options = memnew(PopupMenu);555add_child(file_options);556file_options->connect(SceneStringName(id_pressed), callable_mp(this, &DependencyEditorOwners::_file_option));557558VBoxContainer *vbox = memnew(VBoxContainer);559add_child(vbox);560561owners_count = memnew(Label);562owners_count->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);563owners_count->set_autowrap_mode(TextServer::AUTOWRAP_WORD_SMART);564owners_count->set_custom_minimum_size(Size2(200 * EDSCALE, 0));565vbox->add_child(owners_count);566567empty = memnew(Label);568empty->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);569empty->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);570empty->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER);571empty->set_autowrap_mode(TextServer::AUTOWRAP_WORD_SMART);572empty->set_custom_minimum_size(Size2(200 * EDSCALE, 0));573empty->set_v_size_flags(Control::SIZE_EXPAND_FILL);574empty->hide();575vbox->add_child(empty);576577owners_mc = memnew(MarginContainer);578owners_mc->set_v_size_flags(Control::SIZE_EXPAND_FILL);579owners_mc->set_theme_type_variation("NoBorderHorizontalWindow");580vbox->add_child(owners_mc);581582owners = memnew(ItemList);583owners->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);584owners->set_select_mode(ItemList::SELECT_MULTI);585owners->set_scroll_hint_mode(ItemList::SCROLL_HINT_MODE_BOTH);586owners->connect("item_clicked", callable_mp(this, &DependencyEditorOwners::_list_rmb_clicked));587owners->connect("item_activated", callable_mp(this, &DependencyEditorOwners::_select_file));588owners->connect("empty_clicked", callable_mp(this, &DependencyEditorOwners::_empty_clicked));589owners->set_allow_rmb_select(true);590owners_mc->add_child(owners);591592set_title(TTRC("Owners List"));593}594595///////////////////////596597void DependencyRemoveDialog::_find_files_in_removed_folder(EditorFileSystemDirectory *efsd, const String &p_folder) {598if (!efsd) {599return;600}601602for (int i = 0; i < efsd->get_subdir_count(); ++i) {603_find_files_in_removed_folder(efsd->get_subdir(i), p_folder);604}605for (int i = 0; i < efsd->get_file_count(); i++) {606String file = efsd->get_file_path(i);607ERR_FAIL_COND(all_remove_files.has(file)); //We are deleting a directory which is contained in a directory we are deleting...608all_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.609}610}611612void DependencyRemoveDialog::_find_all_removed_dependencies(EditorFileSystemDirectory *efsd, Vector<RemovedDependency> &p_removed) {613if (!efsd) {614return;615}616617for (int i = 0; i < efsd->get_subdir_count(); i++) {618_find_all_removed_dependencies(efsd->get_subdir(i), p_removed);619}620621for (int i = 0; i < efsd->get_file_count(); i++) {622const String path = efsd->get_file_path(i);623624//It doesn't matter if a file we are about to delete will have some of its dependencies removed too625if (all_remove_files.has(path)) {626continue;627}628629Vector<String> all_deps = efsd->get_file_deps(i);630for (int j = 0; j < all_deps.size(); ++j) {631if (all_remove_files.has(all_deps[j])) {632RemovedDependency dep;633dep.file = path;634dep.file_type = efsd->get_file_type(i);635dep.dependency = all_deps[j];636dep.dependency_folder = all_remove_files[all_deps[j]];637p_removed.push_back(dep);638}639}640}641}642643void DependencyRemoveDialog::_find_localization_remaps_of_removed_files(Vector<RemovedDependency> &p_removed) {644for (KeyValue<String, String> &files : all_remove_files) {645const String &path = files.key;646647// Look for dependencies in the translation remaps.648if (ProjectSettings::get_singleton()->has_setting("internationalization/locale/translation_remaps")) {649Dictionary remaps = GLOBAL_GET("internationalization/locale/translation_remaps");650651if (remaps.has(path)) {652RemovedDependency dep;653dep.file = TTR("Localization remap");654dep.file_type = "";655dep.dependency = path;656dep.dependency_folder = files.value;657p_removed.push_back(dep);658}659660for (const KeyValue<Variant, Variant> &remap_kv : remaps) {661PackedStringArray remapped_files = remap_kv.value;662for (const String &remapped_file : remapped_files) {663int splitter_pos = remapped_file.rfind_char(':');664String res_path = remapped_file.substr(0, splitter_pos);665if (res_path == path) {666String locale_name = remapped_file.substr(splitter_pos + 1);667668RemovedDependency dep;669dep.file = vformat(TTR("Localization remap for path '%s' and locale '%s'."), remap_kv.key, locale_name);670dep.file_type = "";671dep.dependency = path;672dep.dependency_folder = files.value;673p_removed.push_back(dep);674}675}676}677}678}679}680681void DependencyRemoveDialog::_build_removed_dependency_tree(const Vector<RemovedDependency> &p_removed) {682owners->clear();683owners->create_item(); // root684685HashMap<String, TreeItem *> tree_items;686for (int i = 0; i < p_removed.size(); i++) {687RemovedDependency rd = p_removed[i];688689//Ensure that the dependency is already in the tree690if (!tree_items.has(rd.dependency)) {691if (rd.dependency_folder.length() > 0) {692//Ensure the ancestor folder is already in the tree693if (!tree_items.has(rd.dependency_folder)) {694TreeItem *folder_item = owners->create_item(owners->get_root());695folder_item->set_text(0, rd.dependency_folder);696folder_item->set_icon(0, owners->get_editor_theme_icon(SNAME("Folder")));697tree_items[rd.dependency_folder] = folder_item;698}699TreeItem *dependency_item = owners->create_item(tree_items[rd.dependency_folder]);700dependency_item->set_text(0, rd.dependency);701dependency_item->set_icon(0, owners->get_editor_theme_icon(SNAME("Warning")));702tree_items[rd.dependency] = dependency_item;703} else {704TreeItem *dependency_item = owners->create_item(owners->get_root());705dependency_item->set_text(0, rd.dependency);706dependency_item->set_icon(0, owners->get_editor_theme_icon(SNAME("Warning")));707tree_items[rd.dependency] = dependency_item;708}709}710711//List this file under this dependency712Ref<Texture2D> icon = EditorNode::get_singleton()->get_class_icon(rd.file_type);713TreeItem *file_item = owners->create_item(tree_items[rd.dependency]);714file_item->set_text(0, rd.file);715file_item->set_icon(0, icon);716}717}718719void DependencyRemoveDialog::_show_files_to_delete_list() {720files_to_delete_list->clear();721722for (const String &s : dirs_to_delete) {723String t = s.trim_prefix("res://");724files_to_delete_list->add_item(t, Ref<Texture2D>(), false);725}726727for (const String &s : files_to_delete) {728String t = s.trim_prefix("res://");729files_to_delete_list->add_item(t, Ref<Texture2D>(), false);730}731}732733void DependencyRemoveDialog::show(const Vector<String> &p_folders, const Vector<String> &p_files) {734all_remove_files.clear();735dirs_to_delete.clear();736files_to_delete.clear();737owners->clear();738739for (int i = 0; i < p_folders.size(); ++i) {740String folder = p_folders[i].ends_with("/") ? p_folders[i] : (p_folders[i] + "/");741_find_files_in_removed_folder(EditorFileSystem::get_singleton()->get_filesystem_path(folder), folder);742dirs_to_delete.push_back(folder);743}744for (int i = 0; i < p_files.size(); ++i) {745all_remove_files[p_files[i]] = String();746files_to_delete.push_back(p_files[i]);747}748749_show_files_to_delete_list();750751Vector<RemovedDependency> removed_deps;752_find_all_removed_dependencies(EditorFileSystem::get_singleton()->get_filesystem(), removed_deps);753_find_localization_remaps_of_removed_files(removed_deps);754removed_deps.sort();755if (removed_deps.is_empty()) {756vb_owners->hide();757text->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."));758reset_size();759popup_centered();760} else {761_build_removed_dependency_tree(removed_deps);762vb_owners->show();763text->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."));764popup_centered(Size2(500, 350));765}766767EditorFileSystem::get_singleton()->scan_changes();768}769770void DependencyRemoveDialog::ok_pressed() {771HashMap<String, StringName> setting_path_map;772for (const StringName &setting : path_project_settings) {773const String path = ResourceUID::ensure_path(GLOBAL_GET(setting));774setting_path_map[path] = setting;775}776777bool project_settings_modified = false;778779for (const KeyValue<String, String> &E : all_remove_files) {780String file = E.key;781782if (ResourceCache::has(file)) {783Ref<Resource> res = ResourceCache::get_ref(file);784emit_signal(SNAME("resource_removed"), res);785res->set_path("");786}787788// If the file we are deleting for e.g. the main scene, default environment,789// or audio bus layout, we must clear its definition in Project Settings.790const StringName *setting_name = setting_path_map.getptr(file);791if (setting_name) {792ProjectSettings::get_singleton()->set(*setting_name, "");793project_settings_modified = true;794}795}796797for (const String &file : files_to_delete) {798const String path = OS::get_singleton()->get_resource_dir() + file.replace_first("res://", "/");799print_verbose("Moving to trash: " + path);800Error err = OS::get_singleton()->move_to_trash(path);801if (err != OK) {802EditorNode::get_singleton()->add_io_error(TTR("Cannot remove:") + "\n" + file + "\n");803} else {804emit_signal(SNAME("file_removed"), file);805}806}807if (project_settings_modified) {808ProjectSettings::get_singleton()->save();809}810811if (dirs_to_delete.is_empty()) {812// If we only deleted files we should only need to tell the file system about the files we touched.813for (int i = 0; i < files_to_delete.size(); ++i) {814EditorFileSystem::get_singleton()->update_file(files_to_delete[i]);815}816} else {817for (int i = 0; i < dirs_to_delete.size(); ++i) {818String path = OS::get_singleton()->get_resource_dir() + dirs_to_delete[i].replace_first("res://", "/");819print_verbose("Moving to trash: " + path);820Error err = OS::get_singleton()->move_to_trash(path);821if (err != OK) {822EditorNode::get_singleton()->add_io_error(TTR("Cannot remove:") + "\n" + dirs_to_delete[i] + "\n");823} else {824emit_signal(SNAME("folder_removed"), dirs_to_delete[i]);825}826}827828EditorFileSystem::get_singleton()->scan_changes();829}830831// If some files/dirs would be deleted, favorite dirs need to be updated832Vector<String> previous_favorites = EditorSettings::get_singleton()->get_favorites();833Vector<String> new_favorites;834835for (int i = 0; i < previous_favorites.size(); ++i) {836if (previous_favorites[i].ends_with("/")) {837if (!dirs_to_delete.has(previous_favorites[i])) {838new_favorites.push_back(previous_favorites[i]);839}840} else {841if (!files_to_delete.has(previous_favorites[i])) {842new_favorites.push_back(previous_favorites[i]);843}844}845}846847if (new_favorites.size() < previous_favorites.size()) {848EditorSettings::get_singleton()->set_favorites(new_favorites);849}850}851852void DependencyRemoveDialog::_bind_methods() {853ADD_SIGNAL(MethodInfo("resource_removed", PropertyInfo(Variant::OBJECT, "obj")));854ADD_SIGNAL(MethodInfo("file_removed", PropertyInfo(Variant::STRING, "file")));855ADD_SIGNAL(MethodInfo("folder_removed", PropertyInfo(Variant::STRING, "folder")));856}857858DependencyRemoveDialog::DependencyRemoveDialog() {859set_ok_button_text(TTR("Remove"));860861VBoxContainer *vb = memnew(VBoxContainer);862vb->set_h_size_flags(Control::SIZE_EXPAND_FILL);863add_child(vb);864865text = memnew(Label);866text->set_focus_mode(Control::FOCUS_ACCESSIBILITY);867vb->add_child(text);868869Label *files_to_delete_label = memnew(Label);870files_to_delete_label->set_theme_type_variation("HeaderSmall");871files_to_delete_label->set_text(TTR("Files to be deleted:"));872vb->add_child(files_to_delete_label);873874MarginContainer *mc = memnew(MarginContainer);875mc->set_theme_type_variation("NoBorderHorizontalWindow");876mc->set_v_size_flags(Control::SIZE_EXPAND_FILL);877vb->add_child(mc);878879files_to_delete_list = memnew(ItemList);880files_to_delete_list->set_scroll_hint_mode(ItemList::SCROLL_HINT_MODE_BOTH);881files_to_delete_list->set_custom_minimum_size(Size2(0, 94) * EDSCALE);882files_to_delete_list->set_accessibility_name(TTRC("Files to be deleted:"));883mc->add_child(files_to_delete_list);884885vb_owners = memnew(VBoxContainer);886vb_owners->set_h_size_flags(Control::SIZE_EXPAND_FILL);887vb_owners->set_v_size_flags(Control::SIZE_EXPAND_FILL);888vb->add_child(vb_owners);889890Label *owners_label = memnew(Label);891owners_label->set_theme_type_variation("HeaderSmall");892owners_label->set_text(TTR("Dependencies of files to be deleted:"));893vb_owners->add_child(owners_label);894895mc = memnew(MarginContainer);896mc->set_theme_type_variation("NoBorderHorizontalWindow");897mc->set_v_size_flags(Control::SIZE_EXPAND_FILL);898vb_owners->add_child(mc);899900owners = memnew(Tree);901owners->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);902owners->set_scroll_hint_mode(Tree::SCROLL_HINT_MODE_BOTH);903owners->set_hide_root(true);904owners->set_custom_minimum_size(Size2(0, 94) * EDSCALE);905owners->set_accessibility_name(TTRC("Dependencies"));906mc->add_child(owners);907owners->set_v_size_flags(Control::SIZE_EXPAND_FILL);908909List<PropertyInfo> property_list;910ProjectSettings::get_singleton()->get_property_list(&property_list);911for (const PropertyInfo &pi : property_list) {912if (pi.type == Variant::STRING && pi.hint == PROPERTY_HINT_FILE) {913path_project_settings.push_back(pi.name);914}915}916}917918//////////////919enum {920BUTTON_ID_SEARCH,921BUTTON_ID_OPEN_DEPS_EDITOR,922};923924void DependencyErrorDialog::show(const String &p_for_file, const HashMap<String, HashSet<String>> &p_report) {925for_file = p_for_file;926927// TRANSLATORS: The placeholder is a filename.928set_title(vformat(TTR("Error loading: %s"), p_for_file.get_file()));929930HashMap<String, HashSet<String>> missing_to_owners;931for (const KeyValue<String, HashSet<String>> &E : p_report) {932for (const String &missing : E.value) {933missing_to_owners[missing].insert(E.key);934}935}936937files->clear();938TreeItem *root = files->create_item(nullptr);939Ref<Texture2D> folder_icon = get_theme_icon(SNAME("folder"), SNAME("FileDialog"));940941for (const KeyValue<String, HashSet<String>> &E : missing_to_owners) {942const String &missing_path = E.key.get_slice("::", 0);943const String &missing_type = E.key.get_slice("::", 1);944945TreeItem *missing_ti = root->create_child();946missing_ti->set_text(0, missing_path);947missing_ti->set_metadata(0, E.key);948missing_ti->set_auto_translate_mode(0, AUTO_TRANSLATE_MODE_DISABLED);949missing_ti->set_icon(0, EditorNode::get_singleton()->get_class_icon(missing_type));950missing_ti->set_icon(1, get_editor_theme_icon(icon_name_fail));951missing_ti->add_button(1, folder_icon, BUTTON_ID_SEARCH, false, TTRC("Search"));952missing_ti->set_collapsed(true);953954for (const String &owner_path : E.value) {955TreeItem *owner_ti = missing_ti->create_child();956// TRANSLATORS: The placeholder is a file path.957owner_ti->set_text(0, vformat(TTR("Referenced by %s"), owner_path));958owner_ti->set_metadata(0, owner_path);959owner_ti->set_auto_translate_mode(0, AUTO_TRANSLATE_MODE_DISABLED);960owner_ti->add_button(1, files->get_editor_theme_icon(SNAME("Edit")), BUTTON_ID_OPEN_DEPS_EDITOR, false, TTRC("Fix Dependencies"));961}962}963964set_ok_button_text(TTRC("Open Anyway"));965popup_centered();966}967968void DependencyErrorDialog::ok_pressed() {969EditorNode::get_singleton()->load_scene_or_resource(for_file, !errors_fixed);970}971972void DependencyErrorDialog::_on_files_button_clicked(TreeItem *p_item, int p_column, int p_id, MouseButton p_button) {973switch (p_id) {974case BUTTON_ID_SEARCH: {975const String &meta = p_item->get_metadata(0);976const String &missing_path = meta.get_slice("::", 0);977const String &missing_type = meta.get_slice("::", 1);978if (replacement_file_dialog == nullptr) {979replacement_file_dialog = memnew(EditorFileDialog);980replacement_file_dialog->connect("file_selected", callable_mp(this, &DependencyErrorDialog::_on_replacement_file_selected));981replacement_file_dialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILE);982add_child(replacement_file_dialog);983}984replacing_item = p_item;985_setup_search_file_dialog(replacement_file_dialog, missing_path, missing_type);986replacement_file_dialog->popup_file_dialog();987} break;988989case BUTTON_ID_OPEN_DEPS_EDITOR: {990const String &owner_path = p_item->get_metadata(0);991if (deps_editor == nullptr) {992deps_editor = memnew(DependencyEditor);993deps_editor->connect(SceneStringName(visibility_changed), callable_mp(this, &DependencyErrorDialog::_check_for_resolved));994add_child(deps_editor);995}996deps_editor->edit(owner_path);997} break;998}999}10001001void DependencyErrorDialog::_on_replacement_file_selected(const String &p_path) {1002const String &missing_path = String(replacing_item->get_metadata(0)).get_slice("::", 0);10031004for (TreeItem *owner_ti = replacing_item->get_first_child(); owner_ti; owner_ti = owner_ti->get_next()) {1005const String &owner_path = owner_ti->get_metadata(0);1006ResourceLoader::rename_dependencies(owner_path, { { missing_path, p_path } });1007}10081009_check_for_resolved();1010}10111012void DependencyErrorDialog::_check_for_resolved() {1013if (deps_editor && deps_editor->is_visible()) {1014return; // Only update when the dialog is closed.1015}10161017errors_fixed = true;1018HashMap<String, LocalVector<String>> owner_deps;10191020TreeItem *root = files->get_root();1021for (TreeItem *missing_ti = root->get_first_child(); missing_ti; missing_ti = missing_ti->get_next()) {1022bool all_owners_fixed = true;10231024for (TreeItem *owner_ti = missing_ti->get_first_child(); owner_ti; owner_ti = owner_ti->get_next()) {1025const String &owner_path = owner_ti->get_metadata(0);10261027if (!owner_deps.has(owner_path)) {1028List<String> deps;1029ResourceLoader::get_dependencies(owner_path, &deps);10301031LocalVector<String> &stored_paths = owner_deps[owner_path];1032for (const String &dep : deps) {1033if (!errors_fixed && !FileAccess::exists(_get_resolved_dep_path(dep))) {1034errors_fixed = false;1035}1036stored_paths.push_back(_get_stored_dep_path(dep));1037}1038}1039const LocalVector<String> &stored_paths = owner_deps[owner_path];1040const String &missing_path = String(missing_ti->get_metadata(0)).get_slice("::", 0);10411042if (stored_paths.has(missing_path)) {1043all_owners_fixed = false;1044break;1045}1046}10471048missing_ti->set_icon(1, get_editor_theme_icon(all_owners_fixed ? icon_name_check : icon_name_fail));1049}10501051set_ok_button_text(errors_fixed ? TTRC("Open") : TTRC("Open Anyway"));1052}10531054DependencyErrorDialog::DependencyErrorDialog() {1055icon_name_fail = StringName("ImportFail");1056icon_name_check = StringName("ImportCheck");10571058VBoxContainer *vb = memnew(VBoxContainer);1059add_child(vb);10601061files = memnew(Tree);1062files->set_hide_root(true);1063files->set_select_mode(Tree::SELECT_ROW);1064files->set_columns(2);1065files->set_column_expand(1, false);1066files->set_v_size_flags(Control::SIZE_EXPAND_FILL);1067files->connect("button_clicked", callable_mp(this, &DependencyErrorDialog::_on_files_button_clicked));1068vb->add_margin_child(TTRC("Load failed due to missing dependencies:"), files, true);10691070set_min_size(Size2(500, 320) * EDSCALE);1071set_cancel_button_text(TTRC("Close"));10721073Label *text = memnew(Label(TTRC("Which action should be taken?")));1074text->set_focus_mode(Control::FOCUS_ACCESSIBILITY);1075vb->add_child(text);1076}10771078//////////////////////////////////////////////////////////////////////10791080void OrphanResourcesDialog::ok_pressed() {1081paths.clear();10821083_find_to_delete(files->get_root(), paths);1084if (paths.is_empty()) {1085return;1086}10871088delete_confirm->set_text(vformat(TTR("Permanently delete %d item(s)? (No undo!)"), paths.size()));1089delete_confirm->popup_centered();1090}10911092bool OrphanResourcesDialog::_fill_owners(EditorFileSystemDirectory *efsd, HashMap<String, int> &refs, TreeItem *p_parent) {1093if (!efsd) {1094return false;1095}10961097bool has_children = false;10981099for (int i = 0; i < efsd->get_subdir_count(); i++) {1100TreeItem *dir_item = nullptr;1101if (p_parent) {1102dir_item = files->create_item(p_parent);1103dir_item->set_text(0, efsd->get_subdir(i)->get_name());1104dir_item->set_icon(0, files->get_theme_icon(SNAME("folder"), SNAME("FileDialog")));1105}1106bool children = _fill_owners(efsd->get_subdir(i), refs, dir_item);11071108if (p_parent) {1109if (!children) {1110memdelete(dir_item);1111} else {1112has_children = true;1113}1114}1115}11161117for (int i = 0; i < efsd->get_file_count(); i++) {1118if (!p_parent) {1119Vector<String> deps = efsd->get_file_deps(i);1120for (int j = 0; j < deps.size(); j++) {1121if (!refs.has(deps[j])) {1122refs[deps[j]] = 1;1123}1124}1125} else {1126String path = efsd->get_file_path(i);1127if (!refs.has(path)) {1128TreeItem *ti = files->create_item(p_parent);1129ti->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);1130ti->set_text(0, efsd->get_file(i));1131ti->set_editable(0, true);11321133String type = efsd->get_file_type(i);11341135Ref<Texture2D> icon = EditorNode::get_singleton()->get_class_icon(type);1136ti->set_icon(0, icon);1137int ds = efsd->get_file_deps(i).size();1138ti->set_text(1, itos(ds));1139if (ds) {1140ti->add_button(1, files->get_editor_theme_icon(SNAME("GuiVisibilityVisible")), -1, false, TTR("Show Dependencies"));1141}1142ti->set_metadata(0, path);1143has_children = true;1144}1145}1146}11471148return has_children;1149}11501151void OrphanResourcesDialog::refresh() {1152HashMap<String, int> refs;1153_fill_owners(EditorFileSystem::get_singleton()->get_filesystem(), refs, nullptr);1154files->clear();1155TreeItem *root = files->create_item();1156_fill_owners(EditorFileSystem::get_singleton()->get_filesystem(), refs, root);1157}11581159void OrphanResourcesDialog::show() {1160refresh();1161popup_centered_ratio(0.4);1162}11631164void OrphanResourcesDialog::_find_to_delete(TreeItem *p_item, List<String> &r_paths) {1165while (p_item) {1166if (p_item->get_cell_mode(0) == TreeItem::CELL_MODE_CHECK && p_item->is_checked(0)) {1167r_paths.push_back(p_item->get_metadata(0));1168}11691170if (p_item->get_first_child()) {1171_find_to_delete(p_item->get_first_child(), r_paths);1172}11731174p_item = p_item->get_next();1175}1176}11771178void OrphanResourcesDialog::_delete_confirm() {1179Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);1180for (const String &E : paths) {1181da->remove(E);1182EditorFileSystem::get_singleton()->update_file(E);1183}1184refresh();1185}11861187void OrphanResourcesDialog::_button_pressed(Object *p_item, int p_column, int p_id, MouseButton p_button) {1188if (p_button != MouseButton::LEFT) {1189return;1190}1191TreeItem *ti = Object::cast_to<TreeItem>(p_item);11921193String path = ti->get_metadata(0);1194dep_edit->edit(path);1195}11961197OrphanResourcesDialog::OrphanResourcesDialog() {1198set_title(TTR("Orphan Resource Explorer"));1199delete_confirm = memnew(ConfirmationDialog);1200set_ok_button_text(TTR("Delete"));1201add_child(delete_confirm);1202dep_edit = memnew(DependencyEditor);1203add_child(dep_edit);1204delete_confirm->connect(SceneStringName(confirmed), callable_mp(this, &OrphanResourcesDialog::_delete_confirm));1205set_hide_on_ok(false);12061207VBoxContainer *vbc = memnew(VBoxContainer);1208add_child(vbc);12091210files = memnew(Tree);1211files->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);1212files->set_theme_type_variation("TreeTable");1213files->set_columns(2);1214files->set_column_titles_visible(true);1215files->set_column_custom_minimum_width(1, 100 * EDSCALE);1216files->set_column_expand(0, true);1217files->set_column_clip_content(0, true);1218files->set_column_expand(1, false);1219files->set_column_clip_content(1, true);1220files->set_column_title(0, TTR("Resource"));1221files->set_column_title(1, TTR("Owns"));1222files->set_hide_root(true);1223files->set_scroll_hint_mode(Tree::SCROLL_HINT_MODE_BOTTOM);1224files->connect("button_clicked", callable_mp(this, &OrphanResourcesDialog::_button_pressed));12251226MarginContainer *mc = vbc->add_margin_child(TTRC("Resources Without Explicit Ownership:"), files, true);1227mc->set_theme_type_variation("NoBorderHorizontalWindow");1228mc->set_v_size_flags(Control::SIZE_EXPAND_FILL);1229}123012311232