1
0
mirror of https://github.com/godotengine/godot.git synced 2025-11-18 14:21:41 +00:00

Allow fixing indirect missing dependencies manually

This commit is contained in:
Haoyu Qiu
2025-06-16 10:56:13 +08:00
parent 07f4c06601
commit f0d5073037
6 changed files with 222 additions and 111 deletions

View File

@@ -43,6 +43,7 @@
#include "editor/editor_node.h"
#include "editor/editor_string_names.h"
#include "editor/editor_undo_redo_manager.h"
#include "editor/file_system/dependency_editor.h"
#include "editor/gui/create_dialog.h"
#include "editor/gui/directory_create_dialog.h"
#include "editor/gui/editor_dir_dialog.h"

View File

@@ -39,14 +39,17 @@
#include "scene/gui/box_container.h"
#include "scene/gui/control.h"
#include "scene/gui/dialogs.h"
#include "scene/gui/item_list.h"
#include "scene/gui/menu_button.h"
#include "scene/gui/split_container.h"
#include "scene/gui/tree.h"
class CreateDialog;
class DependencyEditor;
class DependencyEditorOwners;
class DependencyRemoveDialog;
class EditorDirDialog;
class HBoxContainer;
class ItemList;
class LineEdit;
class ProgressBar;
class SceneCreateDialog;

View File

@@ -1571,13 +1571,9 @@ Error EditorNode::load_resource(const String &p_resource, bool p_ignore_broken_d
}
ERR_FAIL_COND_V(res.is_null(), ERR_CANT_OPEN);
if (!p_ignore_broken_deps && dependency_errors.has(p_resource)) {
Vector<String> errors;
for (const String &E : dependency_errors[p_resource]) {
errors.push_back(E);
}
dependency_error->show(p_resource, errors);
dependency_errors.erase(p_resource);
if (!p_ignore_broken_deps && !dependency_errors.is_empty()) {
dependency_error->show(p_resource, dependency_errors);
dependency_errors.clear();
return ERR_FILE_MISSING_DEPENDENCIES;
}
@@ -4388,10 +4384,6 @@ bool EditorNode::is_multi_window_enabled() const {
return !SceneTree::get_singleton()->get_root()->is_embedding_subwindows() && !EDITOR_GET("interface/editor/single_window_mode") && EDITOR_GET("interface/multi_window/enable");
}
void EditorNode::fix_dependencies(const String &p_for_file) {
dependency_fixer->edit(p_for_file);
}
int EditorNode::new_scene() {
int idx = editor_data.add_edited_scene(-1);
_set_current_scene(idx); // Before trying to remove an empty scene, set the current tab index to the newly added tab index.
@@ -4466,13 +4458,10 @@ Error EditorNode::load_scene(const String &p_scene, bool p_ignore_broken_deps, b
Error err;
Ref<PackedScene> sdata = ResourceLoader::load(lpath, "", ResourceFormatLoader::CACHE_MODE_REPLACE, &err);
if (!p_ignore_broken_deps && dependency_errors.has(lpath)) {
if (!p_ignore_broken_deps && !dependency_errors.is_empty()) {
current_menu_option = -1;
Vector<String> errors;
for (const String &E : dependency_errors[lpath]) {
errors.push_back(E);
}
dependency_error->show(lpath, errors);
dependency_error->show(lpath, dependency_errors);
dependency_errors.clear();
if (prev != -1 && prev != idx) {
_set_current_scene(prev);
@@ -8133,9 +8122,6 @@ EditorNode::EditorNode() {
dependency_error = memnew(DependencyErrorDialog);
gui_base->add_child(dependency_error);
dependency_fixer = memnew(DependencyEditor);
gui_base->add_child(dependency_fixer);
editor_settings_dialog = memnew(EditorSettingsDialog);
gui_base->add_child(editor_settings_dialog);

View File

@@ -65,7 +65,6 @@ class Window;
class AudioStreamImportSettingsDialog;
class AudioStreamPreviewGenerator;
class BackgroundProgress;
class DependencyEditor;
class DependencyErrorDialog;
class DockSplitContainer;
class DynamicFontImportSettingsDialog;
@@ -421,7 +420,6 @@ private:
DependencyErrorDialog *dependency_error = nullptr;
HashMap<String, HashSet<String>> dependency_errors;
DependencyEditor *dependency_fixer = nullptr;
OrphanResourcesDialog *orphan_resources = nullptr;
ConfirmationDialog *open_imported = nullptr;
Button *new_inherited_button = nullptr;
@@ -843,7 +841,6 @@ public:
String get_preview_locale() const;
void set_preview_locale(const String &p_locale);
void fix_dependencies(const String &p_for_file);
int new_scene();
Error load_scene(const String &p_scene, bool p_ignore_broken_deps = false, bool p_set_inherited = false, bool p_force_open_imported = false, bool p_silent_change_tab = false);
Error load_resource(const String &p_resource, bool p_ignore_broken_deps = false);

View File

@@ -37,10 +37,28 @@
#include "editor/editor_string_names.h"
#include "editor/file_system/editor_file_system.h"
#include "editor/gui/editor_file_dialog.h"
#include "editor/gui/editor_quick_open_dialog.h"
#include "editor/settings/editor_settings.h"
#include "editor/themes/editor_scale.h"
#include "scene/gui/box_container.h"
#include "scene/gui/item_list.h"
#include "scene/gui/margin_container.h"
#include "scene/gui/popup_menu.h"
#include "scene/gui/tree.h"
static void _setup_search_file_dialog(EditorFileDialog *p_dialog, const String &p_file, const String &p_type) {
p_dialog->set_title(vformat(TTR("Search Replacement For: %s"), p_file.get_file()));
// Set directory to closest existing directory.
p_dialog->set_current_dir(p_file.get_base_dir());
p_dialog->clear_filters();
List<String> ext;
ResourceLoader::get_recognized_extensions_for_type(p_type, &ext);
for (const String &E : ext) {
p_dialog->add_filter("*." + E);
}
}
void DependencyEditor::_searched(const String &p_path) {
HashMap<String, String> dep_rename;
@@ -59,17 +77,7 @@ void DependencyEditor::_load_pressed(Object *p_item, int p_cell, int p_button, M
TreeItem *ti = Object::cast_to<TreeItem>(p_item);
replacing = ti->get_text(1);
search->set_title(TTR("Search Replacement For:") + " " + replacing.get_file());
// Set directory to closest existing directory.
search->set_current_dir(replacing.get_base_dir());
search->clear_filters();
List<String> ext;
ResourceLoader::get_recognized_extensions_for_type(ti->get_metadata(0), &ext);
for (const String &E : ext) {
search->add_filter("*." + E);
}
_setup_search_file_dialog(search, replacing, ti->get_metadata(0));
search->popup_file_dialog();
}
@@ -171,6 +179,30 @@ void DependencyEditor::_notification(int p_what) {
}
}
static String _get_resolved_dep_path(const String &p_dep) {
if (p_dep.get_slice_count("::") < 3) {
return p_dep.get_slice("::", 0); // No UID, just return the path.
}
const String uid_text = p_dep.get_slice("::", 0);
ResourceUID::ID uid = ResourceUID::get_singleton()->text_to_id(uid_text);
// Dependency is in UID format, obtain proper path.
if (uid != ResourceUID::INVALID_ID && ResourceUID::get_singleton()->has_id(uid)) {
return ResourceUID::get_singleton()->get_id_path(uid);
}
// UID fallback path.
return p_dep.get_slice("::", 2);
}
static String _get_stored_dep_path(const String &p_dep) {
if (p_dep.get_slice_count("::") > 2) {
return p_dep.get_slice("::", 2);
}
return p_dep.get_slice("::", 0);
}
void DependencyEditor::_update_list() {
List<String> deps;
ResourceLoader::get_dependencies(editing, &deps, true);
@@ -184,47 +216,27 @@ void DependencyEditor::_update_list() {
bool broken = false;
for (const String &n : deps) {
for (const String &dep : deps) {
TreeItem *item = tree->create_item(root);
String path;
String type;
if (n.contains("::")) {
path = n.get_slice("::", 0);
type = n.get_slice("::", 1);
const String path = _get_resolved_dep_path(dep);
if (FileAccess::exists(path)) {
item->set_text(0, path.get_file());
item->set_text(1, path);
} else {
path = n;
type = "Resource";
}
ResourceUID::ID uid = ResourceUID::get_singleton()->text_to_id(path);
if (uid != ResourceUID::INVALID_ID) {
// Dependency is in uid format, obtain proper path.
if (ResourceUID::get_singleton()->has_id(uid)) {
path = ResourceUID::get_singleton()->get_id_path(uid);
} else if (n.get_slice_count("::") >= 3) {
// If uid can't be found, try to use fallback path.
path = n.get_slice("::", 2);
} else {
ERR_PRINT("Invalid dependency UID and fallback path.");
continue;
}
}
String name = path.get_file();
Ref<Texture2D> icon = EditorNode::get_singleton()->get_class_icon(type);
item->set_text(0, name);
item->set_icon(0, icon);
item->set_metadata(0, type);
item->set_text(1, path);
if (!FileAccess::exists(path)) {
const String &stored_path = _get_stored_dep_path(dep);
item->set_text(0, stored_path.get_file());
item->set_text(1, stored_path);
item->set_custom_color(1, Color(1, 0.4, 0.3));
missing.push_back(path);
missing.push_back(stored_path);
broken = true;
}
const String type = dep.contains("::") ? dep.get_slice("::", 1) : "Resource";
Ref<Texture2D> icon = EditorNode::get_singleton()->get_class_icon(type);
item->set_icon(0, icon);
item->set_metadata(0, type);
item->add_button(1, folder, 0);
}
@@ -260,10 +272,8 @@ DependencyEditor::DependencyEditor() {
tree->set_column_titles_visible(true);
tree->set_column_title(0, TTR("Resource"));
tree->set_column_clip_content(0, true);
tree->set_column_expand_ratio(0, 2);
tree->set_column_title(1, TTR("Path"));
tree->set_column_clip_content(1, true);
tree->set_column_expand_ratio(1, 1);
tree->set_hide_root(true);
tree->connect("button_clicked", callable_mp(this, &DependencyEditor::_load_pressed));
@@ -293,7 +303,6 @@ DependencyEditor::DependencyEditor() {
search = memnew(EditorFileDialog);
search->connect("file_selected", callable_mp(this, &DependencyEditor::_searched));
search->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILE);
search->set_title(TTR("Search Replacement Resource:"));
add_child(search);
}
@@ -728,61 +737,163 @@ DependencyRemoveDialog::DependencyRemoveDialog() {
}
//////////////
enum {
BUTTON_ID_SEARCH,
BUTTON_ID_OPEN_DEPS_EDITOR,
};
void DependencyErrorDialog::show(const String &p_for_file, const Vector<String> &report) {
void DependencyErrorDialog::show(const String &p_for_file, const HashMap<String, HashSet<String>> &p_report) {
for_file = p_for_file;
set_title(TTR("Error loading:") + " " + p_for_file.get_file());
files->clear();
TreeItem *root = files->create_item(nullptr);
for (int i = 0; i < report.size(); i++) {
String dep;
String type = "Object";
dep = report[i].get_slice("::", 0);
if (report[i].get_slice_count("::") > 0) {
type = report[i].get_slice("::", 1);
// TRANSLATORS: The placeholder is a filename.
set_title(vformat(TTR("Error loading: %s"), p_for_file.get_file()));
HashMap<String, HashSet<String>> missing_to_owners;
for (const KeyValue<String, HashSet<String>> &E : p_report) {
for (const String &missing : E.value) {
missing_to_owners[missing].insert(E.key);
}
Ref<Texture2D> icon = EditorNode::get_singleton()->get_class_icon(type);
TreeItem *ti = files->create_item(root);
ti->set_text(0, dep);
ti->set_icon(0, icon);
}
files->clear();
TreeItem *root = files->create_item(nullptr);
Ref<Texture2D> folder_icon = get_theme_icon(SNAME("folder"), SNAME("FileDialog"));
for (const KeyValue<String, HashSet<String>> &E : missing_to_owners) {
const String &missing_path = E.key.get_slice("::", 0);
const String &missing_type = E.key.get_slice("::", 1);
TreeItem *missing_ti = root->create_child();
missing_ti->set_text(0, missing_path);
missing_ti->set_metadata(0, E.key);
missing_ti->set_auto_translate_mode(0, AUTO_TRANSLATE_MODE_DISABLED);
missing_ti->set_icon(0, EditorNode::get_singleton()->get_class_icon(missing_type));
missing_ti->set_icon(1, get_editor_theme_icon(icon_name_fail));
missing_ti->add_button(1, folder_icon, BUTTON_ID_SEARCH, false, TTRC("Search"));
missing_ti->set_collapsed(true);
for (const String &owner_path : E.value) {
TreeItem *owner_ti = missing_ti->create_child();
// TRANSLATORS: The placeholder is a file path.
owner_ti->set_text(0, vformat(TTR("Referenced by %s"), owner_path));
owner_ti->set_metadata(0, owner_path);
owner_ti->set_auto_translate_mode(0, AUTO_TRANSLATE_MODE_DISABLED);
owner_ti->add_button(1, files->get_editor_theme_icon(SNAME("Edit")), BUTTON_ID_OPEN_DEPS_EDITOR, false, TTRC("Fix Dependencies"));
}
}
set_ok_button_text(TTRC("Open Anyway"));
popup_centered();
}
void DependencyErrorDialog::ok_pressed() {
EditorNode::get_singleton()->load_scene_or_resource(for_file, true);
EditorNode::get_singleton()->load_scene_or_resource(for_file, !errors_fixed);
}
void DependencyErrorDialog::custom_action(const String &) {
EditorNode::get_singleton()->fix_dependencies(for_file);
void DependencyErrorDialog::_on_files_button_clicked(TreeItem *p_item, int p_column, int p_id, MouseButton p_button) {
switch (p_id) {
case BUTTON_ID_SEARCH: {
const String &meta = p_item->get_metadata(0);
const String &missing_path = meta.get_slice("::", 0);
const String &missing_type = meta.get_slice("::", 1);
if (replacement_file_dialog == nullptr) {
replacement_file_dialog = memnew(EditorFileDialog);
replacement_file_dialog->connect("file_selected", callable_mp(this, &DependencyErrorDialog::_on_replacement_file_selected));
replacement_file_dialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILE);
add_child(replacement_file_dialog);
}
replacing_item = p_item;
_setup_search_file_dialog(replacement_file_dialog, missing_path, missing_type);
replacement_file_dialog->popup_file_dialog();
} break;
case BUTTON_ID_OPEN_DEPS_EDITOR: {
const String &owner_path = p_item->get_metadata(0);
if (deps_editor == nullptr) {
deps_editor = memnew(DependencyEditor);
deps_editor->connect(SceneStringName(visibility_changed), callable_mp(this, &DependencyErrorDialog::_check_for_resolved));
add_child(deps_editor);
}
deps_editor->edit(owner_path);
} break;
}
}
void DependencyErrorDialog::_on_replacement_file_selected(const String &p_path) {
const String &missing_path = String(replacing_item->get_metadata(0)).get_slice("::", 0);
for (TreeItem *owner_ti = replacing_item->get_first_child(); owner_ti; owner_ti = owner_ti->get_next()) {
const String &owner_path = owner_ti->get_metadata(0);
ResourceLoader::rename_dependencies(owner_path, { { missing_path, p_path } });
}
_check_for_resolved();
}
void DependencyErrorDialog::_check_for_resolved() {
if (deps_editor && deps_editor->is_visible()) {
return; // Only update when the dialog is closed.
}
errors_fixed = true;
HashMap<String, LocalVector<String>> owner_deps;
TreeItem *root = files->get_root();
for (TreeItem *missing_ti = root->get_first_child(); missing_ti; missing_ti = missing_ti->get_next()) {
bool all_owners_fixed = true;
for (TreeItem *owner_ti = missing_ti->get_first_child(); owner_ti; owner_ti = owner_ti->get_next()) {
const String &owner_path = owner_ti->get_metadata(0);
if (!owner_deps.has(owner_path)) {
List<String> deps;
ResourceLoader::get_dependencies(owner_path, &deps);
LocalVector<String> &stored_paths = owner_deps[owner_path];
for (const String &dep : deps) {
if (!errors_fixed && !FileAccess::exists(_get_resolved_dep_path(dep))) {
errors_fixed = false;
}
stored_paths.push_back(_get_stored_dep_path(dep));
}
}
const LocalVector<String> &stored_paths = owner_deps[owner_path];
const String &missing_path = String(missing_ti->get_metadata(0)).get_slice("::", 0);
if (stored_paths.has(missing_path)) {
all_owners_fixed = false;
break;
}
}
missing_ti->set_icon(1, get_editor_theme_icon(all_owners_fixed ? icon_name_check : icon_name_fail));
}
set_ok_button_text(errors_fixed ? TTRC("Open") : TTRC("Open Anyway"));
}
DependencyErrorDialog::DependencyErrorDialog() {
icon_name_fail = StringName("ImportFail");
icon_name_check = StringName("ImportCheck");
VBoxContainer *vb = memnew(VBoxContainer);
add_child(vb);
files = memnew(Tree);
files->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
files->set_hide_root(true);
vb->add_margin_child(TTR("Load failed due to missing dependencies:"), files, true);
files->set_select_mode(Tree::SELECT_ROW);
files->set_columns(2);
files->set_column_expand(1, false);
files->set_v_size_flags(Control::SIZE_EXPAND_FILL);
files->connect("button_clicked", callable_mp(this, &DependencyErrorDialog::_on_files_button_clicked));
vb->add_margin_child(TTRC("Load failed due to missing dependencies:"), files, true);
set_min_size(Size2(500, 220) * EDSCALE);
set_ok_button_text(TTR("Open Anyway"));
set_cancel_button_text(TTR("Close"));
set_min_size(Size2(500, 320) * EDSCALE);
set_cancel_button_text(TTRC("Close"));
text = memnew(Label);
Label *text = memnew(Label(TTRC("Which action should be taken?")));
text->set_focus_mode(Control::FOCUS_ACCESSIBILITY);
vb->add_child(text);
text->set_text(TTR("Which action should be taken?"));
fdep = add_button(TTR("Fix Dependencies"), true, "fixdeps");
set_title(TTR("Errors loading!"));
}
//////////////////////////////////////////////////////////////////////

View File

@@ -30,13 +30,15 @@
#pragma once
#include "scene/gui/box_container.h"
#include "scene/gui/dialogs.h"
#include "scene/gui/item_list.h"
#include "scene/gui/tree.h"
class EditorFileDialog;
class EditorFileSystemDirectory;
class ItemList;
class PopupMenu;
class Tree;
class TreeItem;
class VBoxContainer;
class DependencyEditor : public AcceptDialog {
GDCLASS(DependencyEditor, AcceptDialog);
@@ -139,17 +141,28 @@ public:
class DependencyErrorDialog : public ConfirmationDialog {
GDCLASS(DependencyErrorDialog, ConfirmationDialog);
private:
StringName icon_name_fail;
StringName icon_name_check;
String for_file;
Mode mode;
Button *fdep = nullptr;
Label *text = nullptr;
TreeItem *replacing_item = nullptr;
bool errors_fixed = false;
Tree *files = nullptr;
EditorFileDialog *replacement_file_dialog = nullptr;
DependencyEditor *deps_editor = nullptr;
void ok_pressed() override;
void custom_action(const String &) override;
void _on_files_button_clicked(TreeItem *p_item, int p_column, int p_id, MouseButton p_button);
void _on_replacement_file_selected(const String &p_path);
void _check_for_resolved();
public:
void show(const String &p_for_file, const Vector<String> &report);
void show(const String &p_for_file, const HashMap<String, HashSet<String>> &p_report);
DependencyErrorDialog();
};