/**************************************************************************/ /* filesystem_dock.cpp */ /**************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ /* https://godotengine.org */ /**************************************************************************/ /* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ /* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ /* "Software"), to deal in the Software without restriction, including */ /* without limitation the rights to use, copy, modify, merge, publish, */ /* distribute, sublicense, and/or sell copies of the Software, and to */ /* permit persons to whom the Software is furnished to do so, subject to */ /* the following conditions: */ /* */ /* The above copyright notice and this permission notice shall be */ /* included in all copies or substantial portions of the Software. */ /* */ /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ /* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ /* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ /* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ /* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /**************************************************************************/ #include "filesystem_dock.h" #include "core/config/project_settings.h" #include "core/io/dir_access.h" #include "core/io/file_access.h" #include "core/io/resource_loader.h" #include "core/os/keyboard.h" #include "core/os/os.h" #include "core/templates/list.h" #include "editor/create_dialog.h" #include "editor/directory_create_dialog.h" #include "editor/editor_dock_manager.h" #include "editor/editor_feature_profile.h" #include "editor/editor_node.h" #include "editor/editor_resource_preview.h" #include "editor/editor_settings.h" #include "editor/editor_string_names.h" #include "editor/editor_undo_redo_manager.h" #include "editor/gui/editor_dir_dialog.h" #include "editor/gui/editor_scene_tabs.h" #include "editor/import/3d/scene_import_settings.h" #include "editor/import_dock.h" #include "editor/plugins/editor_context_menu_plugin.h" #include "editor/plugins/editor_resource_conversion_plugin.h" #include "editor/plugins/editor_resource_tooltip_plugins.h" #include "editor/scene_create_dialog.h" #include "editor/scene_tree_dock.h" #include "editor/shader_create_dialog.h" #include "editor/themes/editor_scale.h" #include "editor/themes/editor_theme_manager.h" #include "scene/gui/item_list.h" #include "scene/gui/label.h" #include "scene/gui/line_edit.h" #include "scene/gui/progress_bar.h" #include "scene/resources/packed_scene.h" #include "servers/display_server.h" Control *FileSystemTree::make_custom_tooltip(const String &p_text) const { TreeItem *item = get_item_at_position(get_local_mouse_position()); if (!item) { return nullptr; } return FileSystemDock::get_singleton()->create_tooltip_for_path(item->get_metadata(0)); } Control *FileSystemList::make_custom_tooltip(const String &p_text) const { int idx = get_item_at_position(get_local_mouse_position()); if (idx == -1) { return nullptr; } return FileSystemDock::get_singleton()->create_tooltip_for_path(get_item_metadata(idx)); } void FileSystemList::_line_editor_submit(const String &p_text) { if (popup_edit_commited) { return; // Already processed by _text_editor_popup_modal_close } if (popup_editor->get_hide_reason() == Popup::HIDE_REASON_CANCELED) { return; // ESC pressed, app focus lost, or forced close from code. } popup_edit_commited = true; // End edit popup processing. popup_editor->hide(); emit_signal(SNAME("item_edited")); queue_redraw(); } bool FileSystemList::edit_selected() { ERR_FAIL_COND_V_MSG(!is_anything_selected(), false, "No item selected."); int s = get_current(); ERR_FAIL_COND_V_MSG(s < 0, false, "No current item selected."); ensure_current_is_visible(); Rect2 rect; Rect2 popup_rect; Vector2 ofs; Vector2 icon_size = get_item_icon(s)->get_size(); // Handles the different icon modes (TOP/LEFT). switch (get_icon_mode()) { case ItemList::ICON_MODE_LEFT: rect = get_item_rect(s, true); ofs = Vector2(0, Math::floor((MAX(line_editor->get_minimum_size().height, rect.size.height) - rect.size.height) / 2)); popup_rect.position = get_screen_position() + rect.position - ofs; popup_rect.size = rect.size; // Adjust for icon position and size. popup_rect.size.x -= icon_size.x; popup_rect.position.x += icon_size.x; break; case ItemList::ICON_MODE_TOP: rect = get_item_rect(s, false); popup_rect.position = get_screen_position() + rect.position; popup_rect.size = rect.size; // Adjust for icon position and size. popup_rect.size.y -= icon_size.y; popup_rect.position.y += icon_size.y; break; } popup_editor->set_position(popup_rect.position); popup_editor->set_size(popup_rect.size); String name = get_item_text(s); line_editor->set_text(name); line_editor->select(0, name.rfind(".")); popup_edit_commited = false; // Start edit popup processing. popup_editor->popup(); popup_editor->child_controls_changed(); line_editor->grab_focus(); return true; } String FileSystemList::get_edit_text() { return line_editor->get_text(); } void FileSystemList::_text_editor_popup_modal_close() { if (popup_edit_commited) { return; // Already processed by _text_editor_popup_modal_close } if (popup_editor->get_hide_reason() == Popup::HIDE_REASON_CANCELED) { return; // ESC pressed, app focus lost, or forced close from code. } _line_editor_submit(line_editor->get_text()); } void FileSystemList::_bind_methods() { ADD_SIGNAL(MethodInfo("item_edited")); } FileSystemList::FileSystemList() { set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED); popup_editor = memnew(Popup); add_child(popup_editor); popup_editor_vb = memnew(VBoxContainer); popup_editor_vb->add_theme_constant_override("separation", 0); popup_editor_vb->set_anchors_and_offsets_preset(PRESET_FULL_RECT); popup_editor->add_child(popup_editor_vb); line_editor = memnew(LineEdit); line_editor->set_v_size_flags(SIZE_EXPAND_FILL); popup_editor_vb->add_child(line_editor); line_editor->connect("text_submitted", callable_mp(this, &FileSystemList::_line_editor_submit)); popup_editor->connect("popup_hide", callable_mp(this, &FileSystemList::_text_editor_popup_modal_close)); } FileSystemDock *FileSystemDock::singleton = nullptr; Ref FileSystemDock::_get_tree_item_icon(bool p_is_valid, const String &p_file_type, const String &p_icon_path) { if (!p_icon_path.is_empty()) { Ref icon = ResourceLoader::load(p_icon_path); if (icon.is_valid()) { return icon; } } if (!p_is_valid) { return get_editor_theme_icon(SNAME("ImportFail")); } else if (has_theme_icon(p_file_type, EditorStringName(EditorIcons))) { return get_editor_theme_icon(p_file_type); } else { return get_editor_theme_icon(SNAME("File")); } } bool FileSystemDock::_create_tree(TreeItem *p_parent, EditorFileSystemDirectory *p_dir, Vector &uncollapsed_paths, bool p_select_in_favorites, bool p_unfold_path) { bool parent_should_expand = false; // Create a tree item for the subdirectory. TreeItem *subdirectory_item = tree->create_item(p_parent); String dname = p_dir->get_name(); String lpath = p_dir->get_path(); if (dname.is_empty()) { dname = "res://"; } // Set custom folder color (if applicable). bool has_custom_color = assigned_folder_colors.has(lpath); Color custom_color = has_custom_color ? folder_colors[assigned_folder_colors[lpath]] : Color(); Ref da = DirAccess::create(DirAccess::ACCESS_RESOURCES); if (has_custom_color) { subdirectory_item->set_icon_modulate(0, editor_is_dark_theme ? custom_color : custom_color * ITEM_COLOR_SCALE); subdirectory_item->set_custom_bg_color(0, Color(custom_color, editor_is_dark_theme ? ITEM_ALPHA_MIN : ITEM_ALPHA_MAX)); } else { TreeItem *parent = subdirectory_item->get_parent(); if (parent) { Color parent_bg_color = parent->get_custom_bg_color(0); if (parent_bg_color != Color()) { bool parent_has_custom_color = assigned_folder_colors.has(parent->get_metadata(0)); subdirectory_item->set_custom_bg_color(0, parent_has_custom_color ? parent_bg_color.darkened(ITEM_BG_DARK_SCALE) : parent_bg_color); subdirectory_item->set_icon_modulate(0, parent->get_icon_modulate(0)); } else { subdirectory_item->set_icon_modulate(0, get_theme_color(SNAME("folder_icon_color"), SNAME("FileDialog"))); } } } subdirectory_item->set_text(0, dname); subdirectory_item->set_structured_text_bidi_override(0, TextServer::STRUCTURED_TEXT_FILE); subdirectory_item->set_icon(0, get_editor_theme_icon(SNAME("Folder"))); if (da->is_link(lpath)) { subdirectory_item->set_icon_overlay(0, get_editor_theme_icon(SNAME("LinkOverlay"))); subdirectory_item->set_tooltip_text(0, vformat(TTR("Link to: %s"), da->read_link(lpath))); } subdirectory_item->set_selectable(0, true); subdirectory_item->set_metadata(0, lpath); if (!p_select_in_favorites && (current_path == lpath || ((display_mode != DISPLAY_MODE_TREE_ONLY) && current_path.get_base_dir() == lpath))) { subdirectory_item->select(0); // Keep select an item when re-created a tree // To prevent crashing when nothing is selected. subdirectory_item->set_as_cursor(0); } if (p_unfold_path && current_path.begins_with(lpath) && current_path != lpath) { subdirectory_item->set_collapsed(false); } else { subdirectory_item->set_collapsed(!uncollapsed_paths.has(lpath)); } if (!searched_tokens.is_empty() && _matches_all_search_tokens(dname)) { parent_should_expand = true; } // Create items for all subdirectories. bool reversed = file_sort == FILE_SORT_NAME_REVERSE; for (int i = reversed ? p_dir->get_subdir_count() - 1 : 0; reversed ? i >= 0 : i < p_dir->get_subdir_count(); reversed ? i-- : i++) { parent_should_expand = (_create_tree(subdirectory_item, p_dir->get_subdir(i), uncollapsed_paths, p_select_in_favorites, p_unfold_path) || parent_should_expand); } // Create all items for the files in the subdirectory. if (display_mode == DISPLAY_MODE_TREE_ONLY) { String main_scene = GLOBAL_GET("application/run/main_scene"); // Build the list of the files to display. List file_list; for (int i = 0; i < p_dir->get_file_count(); i++) { String file_type = p_dir->get_file_type(i); if (file_type != "TextFile" && file_type != "OtherFile" && _is_file_type_disabled_by_feature_profile(file_type)) { // If type is disabled, file won't be displayed. continue; } String file_name = p_dir->get_file(i); if (!searched_tokens.is_empty()) { if (!_matches_all_search_tokens(file_name)) { // The searched string is not in the file name, we skip it. continue; } else { // We expand all parents. parent_should_expand = true; } } FileInfo fi; fi.name = p_dir->get_file(i); fi.type = p_dir->get_file_type(i); fi.icon_path = p_dir->get_file_icon_path(i); fi.import_broken = !p_dir->get_file_import_is_valid(i); fi.modified_time = p_dir->get_file_modified_time(i); file_list.push_back(fi); } // Sort the file list if needed. _sort_file_info_list(file_list); // Build the tree. const int icon_size = get_theme_constant(SNAME("class_icon_size"), EditorStringName(Editor)); for (const FileInfo &fi : file_list) { TreeItem *file_item = tree->create_item(subdirectory_item); const String file_metadata = lpath.path_join(fi.name); file_item->set_text(0, fi.name); file_item->set_structured_text_bidi_override(0, TextServer::STRUCTURED_TEXT_FILE); file_item->set_icon(0, _get_tree_item_icon(!fi.import_broken, fi.type, fi.icon_path)); if (da->is_link(file_metadata)) { file_item->set_icon_overlay(0, get_editor_theme_icon(SNAME("LinkOverlay"))); file_item->set_tooltip_text(0, vformat(TTR("Link to: %s"), da->read_link(file_metadata))); } file_item->set_icon_max_width(0, icon_size); Color parent_bg_color = subdirectory_item->get_custom_bg_color(0); if (has_custom_color) { file_item->set_custom_bg_color(0, parent_bg_color.darkened(ITEM_BG_DARK_SCALE)); } else if (parent_bg_color != Color()) { file_item->set_custom_bg_color(0, parent_bg_color); } file_item->set_metadata(0, file_metadata); if (!p_select_in_favorites && current_path == file_metadata) { file_item->select(0); file_item->set_as_cursor(0); } if (main_scene == file_metadata) { file_item->set_custom_color(0, get_theme_color(SNAME("accent_color"), EditorStringName(Editor))); } Array udata; udata.push_back(tree_update_id); udata.push_back(file_item); EditorResourcePreview::get_singleton()->queue_resource_preview(file_metadata, this, "_tree_thumbnail_done", udata); } } else { if (lpath.get_base_dir() == current_path.get_base_dir()) { subdirectory_item->select(0); subdirectory_item->set_as_cursor(0); } } if (!searched_tokens.is_empty()) { if (parent_should_expand) { subdirectory_item->set_collapsed(false); } else if (dname != "res://") { subdirectory_item->get_parent()->remove_child(subdirectory_item); memdelete(subdirectory_item); } } return parent_should_expand; } Vector FileSystemDock::get_uncollapsed_paths() const { Vector uncollapsed_paths; TreeItem *root = tree->get_root(); if (root) { TreeItem *favorites_item = root->get_first_child(); if (!favorites_item->is_collapsed()) { uncollapsed_paths.push_back(favorites_item->get_metadata(0)); } // BFS to find all uncollapsed paths of the resource directory. TreeItem *res_subtree = root->get_first_child()->get_next(); if (res_subtree) { List queue; queue.push_back(res_subtree); while (!queue.is_empty()) { TreeItem *ti = queue.back()->get(); queue.pop_back(); if (!ti->is_collapsed() && ti->get_child_count() > 0) { Variant path = ti->get_metadata(0); if (path) { uncollapsed_paths.push_back(path); } } for (int i = 0; i < ti->get_child_count(); i++) { queue.push_back(ti->get_child(i)); } } } } return uncollapsed_paths; } void FileSystemDock::_update_tree(const Vector &p_uncollapsed_paths, bool p_uncollapse_root, bool p_select_in_favorites, bool p_unfold_path) { // Recreate the tree. tree->clear(); tree_update_id++; updating_tree = true; TreeItem *root = tree->create_item(); // Handles the favorites. TreeItem *favorites_item = tree->create_item(root); favorites_item->set_icon(0, get_editor_theme_icon(SNAME("Favorites"))); favorites_item->set_text(0, TTR("Favorites:")); favorites_item->set_metadata(0, "Favorites"); favorites_item->set_collapsed(!p_uncollapsed_paths.has("Favorites")); Vector favorite_paths = EditorSettings::get_singleton()->get_favorites(); Ref da = DirAccess::create(DirAccess::ACCESS_RESOURCES); bool fav_changed = false; for (int i = favorite_paths.size() - 1; i >= 0; i--) { if (da->dir_exists(favorite_paths[i]) || da->file_exists(favorite_paths[i])) { continue; } favorite_paths.remove_at(i); fav_changed = true; } if (fav_changed) { EditorSettings::get_singleton()->set_favorites(favorite_paths); } Ref folder_icon = get_editor_theme_icon(SNAME("Folder")); const Color default_folder_color = get_theme_color(SNAME("folder_icon_color"), SNAME("FileDialog")); for (int i = 0; i < favorite_paths.size(); i++) { const String &favorite = favorite_paths[i]; if (!favorite.begins_with("res://")) { continue; } String text; Ref icon; Color color; if (favorite == "res://") { text = "/"; icon = folder_icon; color = default_folder_color; } else if (favorite.ends_with("/")) { text = favorite.substr(0, favorite.length() - 1).get_file(); icon = folder_icon; color = assigned_folder_colors.has(favorite) ? folder_colors[assigned_folder_colors[favorite]] : default_folder_color; } else { text = favorite.get_file(); int index; EditorFileSystemDirectory *dir = EditorFileSystem::get_singleton()->find_file(favorite, &index); if (dir) { icon = _get_tree_item_icon(dir->get_file_import_is_valid(index), dir->get_file_type(index), dir->get_file_icon_path(index)); } else { icon = get_editor_theme_icon(SNAME("File")); } color = Color(1, 1, 1); } if (searched_tokens.is_empty() || _matches_all_search_tokens(text)) { TreeItem *ti = tree->create_item(favorites_item); ti->set_text(0, text); ti->set_icon(0, icon); ti->set_icon_modulate(0, color); ti->set_tooltip_text(0, favorite); ti->set_selectable(0, true); ti->set_metadata(0, favorite); if (p_select_in_favorites && favorite == current_path) { ti->select(0); ti->set_as_cursor(0); } if (!favorite.ends_with("/")) { Array udata; udata.push_back(tree_update_id); udata.push_back(ti); EditorResourcePreview::get_singleton()->queue_resource_preview(favorite, this, "_tree_thumbnail_done", udata); } } } Vector uncollapsed_paths = p_uncollapsed_paths; if (p_uncollapse_root) { uncollapsed_paths.push_back("res://"); } // Create the remaining of the tree. _create_tree(root, EditorFileSystem::get_singleton()->get_filesystem(), uncollapsed_paths, p_select_in_favorites, p_unfold_path); tree->ensure_cursor_is_visible(); updating_tree = false; } void FileSystemDock::set_display_mode(DisplayMode p_display_mode) { display_mode = p_display_mode; _update_display_mode(false); } void FileSystemDock::_update_display_mode(bool p_force) { // Compute the new display mode. if (p_force || old_display_mode != display_mode) { switch (display_mode) { case DISPLAY_MODE_TREE_ONLY: button_toggle_display_mode->set_icon(get_editor_theme_icon(SNAME("Panels1"))); tree->show(); tree->set_v_size_flags(SIZE_EXPAND_FILL); toolbar2_hbc->show(); _update_tree(get_uncollapsed_paths()); file_list_vb->hide(); break; case DISPLAY_MODE_HSPLIT: case DISPLAY_MODE_VSPLIT: const bool is_vertical = display_mode == DISPLAY_MODE_VSPLIT; split_box->set_vertical(is_vertical); const int actual_offset = is_vertical ? split_box_offset_v : split_box_offset_h; split_box->set_split_offset(actual_offset); const StringName icon = is_vertical ? SNAME("Panels2") : SNAME("Panels2Alt"); button_toggle_display_mode->set_icon(get_editor_theme_icon(icon)); tree->show(); tree->set_v_size_flags(SIZE_EXPAND_FILL); tree->ensure_cursor_is_visible(); toolbar2_hbc->hide(); _update_tree(get_uncollapsed_paths()); file_list_vb->show(); _update_file_list(true); break; } old_display_mode = display_mode; } } void FileSystemDock::_notification(int p_what) { switch (p_what) { case NOTIFICATION_READY: { EditorFeatureProfileManager::get_singleton()->connect("current_feature_profile_changed", callable_mp(this, &FileSystemDock::_feature_profile_changed)); EditorFileSystem::get_singleton()->connect("filesystem_changed", callable_mp(this, &FileSystemDock::_fs_changed)); EditorResourcePreview::get_singleton()->connect("preview_invalidated", callable_mp(this, &FileSystemDock::_preview_invalidated)); button_file_list_display_mode->connect(SceneStringName(pressed), callable_mp(this, &FileSystemDock::_toggle_file_display)); files->connect("item_activated", callable_mp(this, &FileSystemDock::_file_list_activate_file)); button_hist_next->connect(SceneStringName(pressed), callable_mp(this, &FileSystemDock::_fw_history)); button_hist_prev->connect(SceneStringName(pressed), callable_mp(this, &FileSystemDock::_bw_history)); file_list_popup->connect(SceneStringName(id_pressed), callable_mp(this, &FileSystemDock::_file_list_rmb_option)); tree_popup->connect(SceneStringName(id_pressed), callable_mp(this, &FileSystemDock::_tree_rmb_option)); current_path_line_edit->connect("text_submitted", callable_mp(this, &FileSystemDock::_navigate_to_path).bind(false)); always_show_folders = bool(EDITOR_GET("docks/filesystem/always_show_folders")); set_file_list_display_mode(FileSystemDock::FILE_LIST_DISPLAY_LIST); _update_display_mode(); if (EditorFileSystem::get_singleton()->is_scanning()) { _set_scanning_mode(); } else { _update_tree(Vector(), true); } } break; case NOTIFICATION_PROCESS: { if (EditorFileSystem::get_singleton()->is_scanning()) { scanning_progress->set_value(EditorFileSystem::get_singleton()->get_scanning_progress() * 100.0f); } } break; case NOTIFICATION_DRAG_BEGIN: { Dictionary dd = get_viewport()->gui_get_drag_data(); if (tree->is_visible_in_tree() && dd.has("type")) { if (dd.has("favorite")) { if ((String(dd["favorite"]) == "all")) { tree->set_drop_mode_flags(Tree::DROP_MODE_INBETWEEN); } } else if ((String(dd["type"]) == "files") || (String(dd["type"]) == "files_and_dirs")) { tree->set_drop_mode_flags(Tree::DROP_MODE_ON_ITEM | Tree::DROP_MODE_INBETWEEN); } else if ((String(dd["type"]) == "nodes") || (String(dd["type"]) == "resource")) { holding_branch = true; TreeItem *item = tree->get_next_selected(tree->get_root()); while (item) { tree_items_selected_on_drag_begin.push_back(item); item = tree->get_next_selected(item); } list_items_selected_on_drag_begin = files->get_selected_items(); } } } break; case NOTIFICATION_DRAG_END: { tree->set_drop_mode_flags(0); if (holding_branch) { holding_branch = false; _reselect_items_selected_on_drag_begin(true); } } break; case NOTIFICATION_TRANSLATION_CHANGED: case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: case NOTIFICATION_THEME_CHANGED: { _update_display_mode(true); button_reload->set_icon(get_editor_theme_icon(SNAME("Reload"))); StringName mode_icon = "Panels1"; if (display_mode == DISPLAY_MODE_VSPLIT) { mode_icon = "Panels2"; } else if (display_mode == DISPLAY_MODE_HSPLIT) { mode_icon = "Panels2Alt"; } button_toggle_display_mode->set_icon(get_editor_theme_icon(mode_icon)); if (file_list_display_mode == FILE_LIST_DISPLAY_LIST) { button_file_list_display_mode->set_icon(get_editor_theme_icon(SNAME("FileThumbnail"))); } else { button_file_list_display_mode->set_icon(get_editor_theme_icon(SNAME("FileList"))); } tree_search_box->set_right_icon(get_editor_theme_icon(SNAME("Search"))); tree_button_sort->set_icon(get_editor_theme_icon(SNAME("Sort"))); file_list_search_box->set_right_icon(get_editor_theme_icon(SNAME("Search"))); file_list_button_sort->set_icon(get_editor_theme_icon(SNAME("Sort"))); button_dock_placement->set_icon(get_editor_theme_icon(SNAME("GuiTabMenuHl"))); if (is_layout_rtl()) { button_hist_next->set_icon(get_editor_theme_icon(SNAME("Back"))); button_hist_prev->set_icon(get_editor_theme_icon(SNAME("Forward"))); } else { button_hist_next->set_icon(get_editor_theme_icon(SNAME("Forward"))); button_hist_prev->set_icon(get_editor_theme_icon(SNAME("Back"))); } overwrite_dialog_scroll->add_theme_style_override(SceneStringName(panel), get_theme_stylebox(SceneStringName(panel), "Tree")); } break; case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: { // Update editor dark theme & always show folders states from editor settings, redraw if needed. bool do_redraw = false; bool new_editor_is_dark_theme = EditorThemeManager::is_dark_theme(); if (new_editor_is_dark_theme != editor_is_dark_theme) { editor_is_dark_theme = new_editor_is_dark_theme; do_redraw = true; } bool new_always_show_folders = bool(EDITOR_GET("docks/filesystem/always_show_folders")); if (new_always_show_folders != always_show_folders) { always_show_folders = new_always_show_folders; do_redraw = true; } if (do_redraw) { _update_file_list(true); _update_tree(get_uncollapsed_paths()); } if (EditorThemeManager::is_generated_theme_outdated()) { // Change full tree mode. _update_display_mode(); } } break; } } void FileSystemDock::_tree_multi_selected(Object *p_item, int p_column, bool p_selected) { // Update the import dock. import_dock_needs_update = true; callable_mp(this, &FileSystemDock::_update_import_dock).call_deferred(); // Return if we don't select something new. if (!p_selected) { return; } // Tree item selected. TreeItem *selected = tree->get_selected(); if (!selected) { return; } TreeItem *favorites_item = tree->get_root()->get_first_child(); if (selected->get_parent() == favorites_item && !String(selected->get_metadata(0)).ends_with("/")) { // Go to the favorites if we click in the favorites and the path has changed. current_path = "Favorites"; } else { current_path = selected->get_metadata(0); // Note: the "Favorites" item also leads to this path. } // Display the current path. _set_current_path_line_edit_text(current_path); _push_to_history(); // Update the file list. if (!updating_tree && display_mode != DISPLAY_MODE_TREE_ONLY) { _update_file_list(false); } } Vector FileSystemDock::get_selected_paths() const { if (display_mode == DISPLAY_MODE_TREE_ONLY) { return _tree_get_selected(false); } else { Vector selected = _file_list_get_selected(); if (selected.is_empty()) { selected.push_back(get_current_directory()); } return selected; } } String FileSystemDock::get_current_path() const { return current_path; } String FileSystemDock::get_current_directory() const { if (current_path.ends_with("/")) { return current_path; } else { return current_path.get_base_dir(); } } void FileSystemDock::_set_current_path_line_edit_text(const String &p_path) { if (p_path == "Favorites") { current_path_line_edit->set_text(TTR("Favorites")); } else { current_path_line_edit->set_text(current_path); } } void FileSystemDock::_navigate_to_path(const String &p_path, bool p_select_in_favorites) { if (p_path == "Favorites") { current_path = p_path; } else { String target_path = p_path; // If the path is a file, do not only go to the directory in the tree, also select the file in the file list. if (target_path.ends_with("/")) { target_path = target_path.substr(0, target_path.length() - 1); } Ref da = DirAccess::create(DirAccess::ACCESS_RESOURCES); if (da->file_exists(p_path)) { current_path = target_path; } else if (da->dir_exists(p_path)) { current_path = target_path + "/"; } else { ERR_FAIL_MSG(vformat("Cannot navigate to '%s' as it has not been found in the file system!", p_path)); } } _set_current_path_line_edit_text(current_path); _push_to_history(); _update_tree(get_uncollapsed_paths(), false, p_select_in_favorites, true); if (display_mode != DISPLAY_MODE_TREE_ONLY) { _update_file_list(false); // Reset the scroll for a directory. if (p_path.ends_with("/")) { files->get_v_scroll_bar()->set_value(0); } } String file_name = p_path.get_file(); if (!file_name.is_empty()) { for (int i = 0; i < files->get_item_count(); i++) { if (files->get_item_text(i) == file_name) { files->select(i, true); files->ensure_current_is_visible(); break; } } } } void FileSystemDock::navigate_to_path(const String &p_path) { file_list_search_box->clear(); _navigate_to_path(p_path); // Ensure that the FileSystem dock is visible. EditorDockManager::get_singleton()->focus_dock(this); import_dock_needs_update = true; _update_import_dock(); } void FileSystemDock::_file_list_thumbnail_done(const String &p_path, const Ref &p_preview, const Ref &p_small_preview, const Variant &p_udata) { if (p_preview.is_valid()) { Array uarr = p_udata; int idx = uarr[0]; String file = uarr[1]; if (idx < files->get_item_count() && files->get_item_text(idx) == file && files->get_item_metadata(idx) == p_path) { if (file_list_display_mode == FILE_LIST_DISPLAY_LIST) { if (p_small_preview.is_valid()) { files->set_item_icon(idx, p_small_preview); } } else { files->set_item_icon(idx, p_preview); } } } } void FileSystemDock::_tree_thumbnail_done(const String &p_path, const Ref &p_preview, const Ref &p_small_preview, const Variant &p_udata) { if (p_small_preview.is_valid()) { Array uarr = p_udata; if (tree_update_id == (int)uarr[0]) { TreeItem *file_item = Object::cast_to(uarr[1]); if (file_item) { file_item->set_icon(0, p_small_preview); } } } } void FileSystemDock::_toggle_file_display() { _set_file_display(file_list_display_mode != FILE_LIST_DISPLAY_LIST); emit_signal(SNAME("display_mode_changed")); } void FileSystemDock::_set_file_display(bool p_active) { if (p_active) { file_list_display_mode = FILE_LIST_DISPLAY_LIST; button_file_list_display_mode->set_icon(get_editor_theme_icon(SNAME("FileThumbnail"))); button_file_list_display_mode->set_tooltip_text(TTR("View items as a grid of thumbnails.")); } else { file_list_display_mode = FILE_LIST_DISPLAY_THUMBNAILS; button_file_list_display_mode->set_icon(get_editor_theme_icon(SNAME("FileList"))); button_file_list_display_mode->set_tooltip_text(TTR("View items as a list.")); } _update_file_list(true); } bool FileSystemDock::_is_file_type_disabled_by_feature_profile(const StringName &p_class) { Ref profile = EditorFeatureProfileManager::get_singleton()->get_current_profile(); if (profile.is_null()) { return false; } StringName class_name = p_class; while (class_name != StringName()) { if (profile->is_class_disabled(class_name)) { return true; } class_name = ClassDB::get_parent_class(class_name); } return false; } void FileSystemDock::_search(EditorFileSystemDirectory *p_path, List *matches, int p_max_items) { if (matches->size() > p_max_items) { return; } for (int i = 0; i < p_path->get_subdir_count(); i++) { _search(p_path->get_subdir(i), matches, p_max_items); } for (int i = 0; i < p_path->get_file_count(); i++) { String file = p_path->get_file(i); if (_matches_all_search_tokens(file)) { FileInfo fi; fi.name = file; fi.type = p_path->get_file_type(i); fi.path = p_path->get_file_path(i); fi.import_broken = !p_path->get_file_import_is_valid(i); fi.modified_time = p_path->get_file_modified_time(i); if (_is_file_type_disabled_by_feature_profile(fi.type)) { // This type is disabled, will not appear here. continue; } matches->push_back(fi); if (matches->size() > p_max_items) { return; } } } } struct FileSystemDock::FileInfoTypeComparator { bool operator()(const FileInfo &p_a, const FileInfo &p_b) const { return FileNoCaseComparator()(p_a.name.get_extension() + p_a.type + p_a.name.get_basename(), p_b.name.get_extension() + p_b.type + p_b.name.get_basename()); } }; struct FileSystemDock::FileInfoModifiedTimeComparator { bool operator()(const FileInfo &p_a, const FileInfo &p_b) const { return p_a.modified_time > p_b.modified_time; } }; void FileSystemDock::_sort_file_info_list(List &r_file_list) { // Sort the file list if needed. switch (file_sort) { case FILE_SORT_TYPE: r_file_list.sort_custom(); break; case FILE_SORT_TYPE_REVERSE: r_file_list.sort_custom(); r_file_list.reverse(); break; case FILE_SORT_MODIFIED_TIME: r_file_list.sort_custom(); break; case FILE_SORT_MODIFIED_TIME_REVERSE: r_file_list.sort_custom(); r_file_list.reverse(); break; case FILE_SORT_NAME_REVERSE: r_file_list.sort(); r_file_list.reverse(); break; default: // FILE_SORT_NAME r_file_list.sort(); break; } } void FileSystemDock::_update_file_list(bool p_keep_selection) { // Register the previously current and selected items. HashSet previous_selection; HashSet valid_selection; if (p_keep_selection) { for (int i = 0; i < files->get_item_count(); i++) { if (files->is_selected(i)) { previous_selection.insert(files->get_item_text(i)); } } } files->clear(); _set_current_path_line_edit_text(current_path); String directory = current_path; String file = ""; int thumbnail_size = EDITOR_GET("docks/filesystem/thumbnail_size"); thumbnail_size *= EDSCALE; Ref folder_thumbnail; Ref file_thumbnail; Ref file_thumbnail_broken; bool use_thumbnails = (file_list_display_mode == FILE_LIST_DISPLAY_THUMBNAILS); if (use_thumbnails) { // Thumbnails mode. files->set_max_columns(0); files->set_icon_mode(ItemList::ICON_MODE_TOP); files->set_fixed_column_width(thumbnail_size * 3 / 2); files->set_max_text_lines(2); files->set_fixed_icon_size(Size2(thumbnail_size, thumbnail_size)); const int icon_size = get_theme_constant(SNAME("class_icon_size"), EditorStringName(Editor)); files->set_fixed_tag_icon_size(Size2(icon_size, icon_size)); if (thumbnail_size < 64) { folder_thumbnail = get_editor_theme_icon(SNAME("FolderMediumThumb")); file_thumbnail = get_editor_theme_icon(SNAME("FileMediumThumb")); file_thumbnail_broken = get_editor_theme_icon(SNAME("FileDeadMediumThumb")); } else { folder_thumbnail = get_editor_theme_icon(SNAME("FolderBigThumb")); file_thumbnail = get_editor_theme_icon(SNAME("FileBigThumb")); file_thumbnail_broken = get_editor_theme_icon(SNAME("FileDeadBigThumb")); } } else { // No thumbnails. files->set_icon_mode(ItemList::ICON_MODE_LEFT); files->set_max_columns(1); files->set_max_text_lines(1); files->set_fixed_column_width(0); const int icon_size = get_theme_constant(SNAME("class_icon_size"), EditorStringName(Editor)); files->set_fixed_icon_size(Size2(icon_size, icon_size)); } Ref folder_icon = (use_thumbnails) ? folder_thumbnail : get_theme_icon(SNAME("folder"), SNAME("FileDialog")); const Color default_folder_color = get_theme_color(SNAME("folder_icon_color"), SNAME("FileDialog")); // Build the FileInfo list. List file_list; if (current_path == "Favorites") { // Display the favorites. Vector favorites_list = EditorSettings::get_singleton()->get_favorites(); for (const String &favorite : favorites_list) { String text; Ref icon; if (favorite == "res://") { text = "/"; icon = folder_icon; if (searched_tokens.is_empty() || _matches_all_search_tokens(text)) { files->add_item(text, icon, true); files->set_item_metadata(-1, favorite); } } else if (favorite.ends_with("/")) { text = favorite.substr(0, favorite.length() - 1).get_file(); icon = folder_icon; if (searched_tokens.is_empty() || _matches_all_search_tokens(text)) { files->add_item(text, icon, true); files->set_item_metadata(-1, favorite); } } else { int index; EditorFileSystemDirectory *efd = EditorFileSystem::get_singleton()->find_file(favorite, &index); FileInfo fi; fi.name = favorite.get_file(); fi.path = favorite; if (efd) { fi.type = efd->get_file_type(index); fi.icon_path = efd->get_file_icon_path(index); fi.import_broken = !efd->get_file_import_is_valid(index); fi.modified_time = efd->get_file_modified_time(index); } else { fi.type = ""; fi.import_broken = true; fi.modified_time = 0; } if (searched_tokens.is_empty() || _matches_all_search_tokens(fi.name)) { file_list.push_back(fi); } } } } else { if (!directory.begins_with("res://")) { directory = "res://" + directory; } // Get infos on the directory + file. if (directory.ends_with("/") && directory != "res://") { directory = directory.substr(0, directory.length() - 1); } EditorFileSystemDirectory *efd = EditorFileSystem::get_singleton()->get_filesystem_path(directory); if (!efd) { directory = current_path.get_base_dir(); file = current_path.get_file(); efd = EditorFileSystem::get_singleton()->get_filesystem_path(directory); } if (!efd) { return; } if (!searched_tokens.is_empty()) { // Display the search results. // Limit the number of results displayed to avoid an infinite loop. _search(EditorFileSystem::get_singleton()->get_filesystem(), &file_list, 10000); } else { if (display_mode == DISPLAY_MODE_TREE_ONLY || always_show_folders) { // Check for a folder color to inherit (if one is assigned). Color inherited_folder_color = default_folder_color; String color_scan_dir = directory; while (color_scan_dir != "res://" && inherited_folder_color == default_folder_color) { if (!color_scan_dir.ends_with("/")) { color_scan_dir += "/"; } if (assigned_folder_colors.has(color_scan_dir)) { inherited_folder_color = folder_colors[assigned_folder_colors[color_scan_dir]]; } color_scan_dir = color_scan_dir.rstrip("/").get_base_dir(); } // Display folders in the list. if (directory != "res://") { files->add_item("..", folder_icon, true); String bd = directory.get_base_dir(); if (bd != "res://" && !bd.ends_with("/")) { bd += "/"; } files->set_item_metadata(-1, bd); files->set_item_selectable(-1, false); files->set_item_icon_modulate(-1, editor_is_dark_theme ? inherited_folder_color : inherited_folder_color * ITEM_COLOR_SCALE); } bool reversed = file_sort == FILE_SORT_NAME_REVERSE; for (int i = reversed ? efd->get_subdir_count() - 1 : 0; reversed ? i >= 0 : i < efd->get_subdir_count(); reversed ? i-- : i++) { String dname = efd->get_subdir(i)->get_name(); String dpath = directory.path_join(dname) + "/"; bool has_custom_color = assigned_folder_colors.has(dpath); files->add_item(dname, folder_icon, true); files->set_item_metadata(-1, dpath); Color this_folder_color = has_custom_color ? folder_colors[assigned_folder_colors[dpath]] : inherited_folder_color; files->set_item_icon_modulate(-1, editor_is_dark_theme ? this_folder_color : this_folder_color * ITEM_COLOR_SCALE); if (previous_selection.has(dname)) { files->select(files->get_item_count() - 1, false); valid_selection.insert(files->get_item_count() - 1); } } } // Display the folder content. for (int i = 0; i < efd->get_file_count(); i++) { FileInfo fi; fi.name = efd->get_file(i); fi.path = directory.path_join(fi.name); fi.type = efd->get_file_type(i); fi.icon_path = efd->get_file_icon_path(i); fi.import_broken = !efd->get_file_import_is_valid(i); fi.modified_time = efd->get_file_modified_time(i); file_list.push_back(fi); } } } // Sort the file list if needed. _sort_file_info_list(file_list); // Fills the ItemList control node from the FileInfos. String main_scene = GLOBAL_GET("application/run/main_scene"); for (FileInfo &E : file_list) { FileInfo *finfo = &(E); String fname = finfo->name; String fpath = finfo->path; Ref type_icon; Ref big_icon; String tooltip = fpath; // Select the icons. type_icon = _get_tree_item_icon(!finfo->import_broken, finfo->type, finfo->icon_path); if (!finfo->import_broken) { big_icon = file_thumbnail; } else { big_icon = file_thumbnail_broken; tooltip += "\n" + TTR("Status: Import of file failed. Please fix file and reimport manually."); } // Add the item to the ItemList. int item_index; if (use_thumbnails) { files->add_item(fname, big_icon, true); item_index = files->get_item_count() - 1; files->set_item_metadata(item_index, fpath); files->set_item_tag_icon(item_index, type_icon); } else { files->add_item(fname, type_icon, true); item_index = files->get_item_count() - 1; files->set_item_metadata(item_index, fpath); } if (fpath == main_scene) { files->set_item_custom_fg_color(item_index, get_theme_color(SNAME("accent_color"), EditorStringName(Editor))); } // Generate the preview. if (!finfo->import_broken) { Array udata; udata.resize(2); udata[0] = item_index; udata[1] = fname; EditorResourcePreview::get_singleton()->queue_resource_preview(fpath, this, "_file_list_thumbnail_done", udata); } // Select the items. if (previous_selection.has(fname)) { files->select(item_index, false); valid_selection.insert(item_index); } if (!p_keep_selection && !file.is_empty() && fname == file) { files->select(item_index, true); files->ensure_current_is_visible(); } // Tooltip. if (finfo->sources.size()) { for (int j = 0; j < finfo->sources.size(); j++) { tooltip += "\nSource: " + finfo->sources[j]; } } files->set_item_tooltip(item_index, tooltip); } // If we only have any selected items retained, we need to update the current idx. if (!valid_selection.is_empty()) { files->set_current(*valid_selection.begin()); } } HashSet FileSystemDock::_get_valid_conversions_for_file_paths(const Vector &p_paths) { HashSet all_valid_conversion_to_targets; for (const String &fpath : p_paths) { if (fpath.is_empty() || fpath == "res://" || !FileAccess::exists(fpath) || FileAccess::exists(fpath + ".import")) { return HashSet(); } Vector> conversions = EditorNode::get_singleton()->find_resource_conversion_plugin_for_type_name(EditorFileSystem::get_singleton()->get_file_type(fpath)); if (conversions.is_empty()) { // This resource can't convert to anything, so return an empty list. return HashSet(); } // Get a list of all potentional conversion-to targets. HashSet current_valid_conversion_to_targets; for (const Ref &E : conversions) { const String what = E->converts_to(); current_valid_conversion_to_targets.insert(what); } if (all_valid_conversion_to_targets.is_empty()) { // If we have no existing valid conversions, this is the first one, so copy them directly. all_valid_conversion_to_targets = current_valid_conversion_to_targets; } else { // Check existing conversion targets and remove any which are not in the current list. for (const String &S : all_valid_conversion_to_targets) { if (!current_valid_conversion_to_targets.has(S)) { all_valid_conversion_to_targets.erase(S); } } // We have no more remaining valid conversions, so break the loop. if (all_valid_conversion_to_targets.is_empty()) { break; } } } return all_valid_conversion_to_targets; } void FileSystemDock::_select_file(const String &p_path, bool p_select_in_favorites) { String fpath = p_path; if (fpath.ends_with("/")) { // Ignore a directory. } else if (fpath != "Favorites") { if (FileAccess::exists(fpath + ".import")) { Ref config; config.instantiate(); Error err = config->load(fpath + ".import"); if (err == OK) { if (config->has_section_key("remap", "importer")) { String importer = config->get_value("remap", "importer"); if (importer == "keep" || importer == "skip") { EditorNode::get_singleton()->show_warning(TTR("Importing has been disabled for this file, so it can't be opened for editing.")); return; } } } } String resource_type = ResourceLoader::get_resource_type(fpath); if (resource_type == "PackedScene" || resource_type == "AnimationLibrary") { bool is_imported = false; { List importer_exts; ResourceImporterScene::get_scene_importer_extensions(&importer_exts); String extension = fpath.get_extension(); for (const String &E : importer_exts) { if (extension.nocasecmp_to(E) == 0) { is_imported = true; break; } } } if (is_imported) { SceneImportSettingsDialog::get_singleton()->open_settings(p_path, resource_type); } else if (resource_type == "PackedScene") { EditorNode::get_singleton()->open_request(fpath); } else { EditorNode::get_singleton()->load_resource(fpath); } } else if (ResourceLoader::is_imported(fpath)) { // If the importer has advanced settings, show them. int order; bool can_threads; String name; Error err = ResourceFormatImporter::get_singleton()->get_import_order_threads_and_importer(fpath, order, can_threads, name); bool used_advanced_settings = false; if (err == OK) { Ref importer = ResourceFormatImporter::get_singleton()->get_importer_by_name(name); if (importer.is_valid() && importer->has_advanced_options()) { importer->show_advanced_options(fpath); used_advanced_settings = true; } } if (!used_advanced_settings) { EditorNode::get_singleton()->load_resource(fpath); } } else { EditorNode::get_singleton()->load_resource(fpath); } } _navigate_to_path(fpath, p_select_in_favorites); } void FileSystemDock::_tree_activate_file() { TreeItem *selected = tree->get_selected(); if (selected) { String file_path = selected->get_metadata(0); TreeItem *parent = selected->get_parent(); bool is_favorite = parent != nullptr && parent->get_metadata(0) == "Favorites"; if ((!is_favorite && file_path.ends_with("/")) || file_path == "Favorites") { bool collapsed = selected->is_collapsed(); selected->set_collapsed(!collapsed); } else { _select_file(file_path, is_favorite && !file_path.ends_with("/")); } } } void FileSystemDock::_file_list_activate_file(int p_idx) { _select_file(files->get_item_metadata(p_idx)); } void FileSystemDock::_preview_invalidated(const String &p_path) { if (file_list_display_mode == FILE_LIST_DISPLAY_THUMBNAILS && p_path.get_base_dir() == current_path && searched_tokens.is_empty() && file_list_vb->is_visible_in_tree()) { for (int i = 0; i < files->get_item_count(); i++) { if (files->get_item_metadata(i) == p_path) { // Re-request preview. Array udata; udata.resize(2); udata[0] = i; udata[1] = files->get_item_text(i); EditorResourcePreview::get_singleton()->queue_resource_preview(p_path, this, "_file_list_thumbnail_done", udata); break; } } } } void FileSystemDock::_fs_changed() { button_hist_prev->set_disabled(history_pos == 0); button_hist_next->set_disabled(history_pos == history.size() - 1); scanning_vb->hide(); split_box->show(); if (tree->is_visible()) { _update_tree(get_uncollapsed_paths()); } if (file_list_vb->is_visible()) { _update_file_list(true); } if (!select_after_scan.is_empty()) { _navigate_to_path(select_after_scan); select_after_scan.clear(); import_dock_needs_update = true; _update_import_dock(); } set_process(false); } void FileSystemDock::_directory_created(const String &p_path) { if (!DirAccess::exists(p_path)) { return; } EditorFileSystem::get_singleton()->add_new_directory(p_path); _update_tree(get_uncollapsed_paths()); _update_file_list(true); } void FileSystemDock::_set_scanning_mode() { button_hist_prev->set_disabled(true); button_hist_next->set_disabled(true); split_box->hide(); scanning_vb->show(); set_process(true); if (EditorFileSystem::get_singleton()->is_scanning()) { scanning_progress->set_value(EditorFileSystem::get_singleton()->get_scanning_progress() * 100); } else { scanning_progress->set_value(0); } } void FileSystemDock::_fw_history() { if (history_pos < history.size() - 1) { history_pos++; } _update_history(); } void FileSystemDock::_bw_history() { if (history_pos > 0) { history_pos--; } _update_history(); } void FileSystemDock::_update_history() { current_path = history[history_pos]; _set_current_path_line_edit_text(current_path); if (tree->is_visible()) { _update_tree(get_uncollapsed_paths()); tree->grab_focus(); tree->ensure_cursor_is_visible(); } if (file_list_vb->is_visible()) { _update_file_list(false); } button_hist_prev->set_disabled(history_pos == 0); button_hist_next->set_disabled(history_pos == history.size() - 1); } void FileSystemDock::_push_to_history() { if (history[history_pos] != current_path) { history.resize(history_pos + 1); history.push_back(current_path); history_pos++; if (history.size() > history_max_size) { history.remove_at(0); history_pos = history_max_size - 1; } } button_hist_prev->set_disabled(history_pos == 0); button_hist_next->set_disabled(history_pos == history.size() - 1); } void FileSystemDock::_get_all_items_in_dir(EditorFileSystemDirectory *p_efsd, Vector &r_files, Vector &r_folders) const { if (p_efsd == nullptr) { return; } for (int i = 0; i < p_efsd->get_subdir_count(); i++) { r_folders.push_back(p_efsd->get_subdir(i)->get_path()); _get_all_items_in_dir(p_efsd->get_subdir(i), r_files, r_folders); } for (int i = 0; i < p_efsd->get_file_count(); i++) { r_files.push_back(p_efsd->get_file_path(i)); } } void FileSystemDock::_find_file_owners(EditorFileSystemDirectory *p_efsd, const HashSet &p_renames, HashSet &r_file_owners) const { for (int i = 0; i < p_efsd->get_subdir_count(); i++) { _find_file_owners(p_efsd->get_subdir(i), p_renames, r_file_owners); } for (int i = 0; i < p_efsd->get_file_count(); i++) { Vector deps = p_efsd->get_file_deps(i); for (int j = 0; j < deps.size(); j++) { if (p_renames.has(deps[j])) { r_file_owners.insert(p_efsd->get_file_path(i)); break; } } } } void FileSystemDock::_try_move_item(const FileOrFolder &p_item, const String &p_new_path, HashMap &p_file_renames, HashMap &p_folder_renames) { // Ensure folder paths end with "/". String old_path = (p_item.is_file || p_item.path.ends_with("/")) ? p_item.path : (p_item.path + "/"); String new_path = (p_item.is_file || p_new_path.ends_with("/")) ? p_new_path : (p_new_path + "/"); if (new_path == old_path) { return; } else if (old_path == "res://") { EditorNode::get_singleton()->add_io_error(TTR("Cannot move/rename resources root.")); return; } else if (!p_item.is_file && new_path.begins_with(old_path)) { // This check doesn't erroneously catch renaming to a longer name as folder paths always end with "/". EditorNode::get_singleton()->add_io_error(TTR("Cannot move a folder into itself.") + "\n" + old_path + "\n"); return; } // Build a list of files which will have new paths as a result of this operation. Vector file_changed_paths; Vector folder_changed_paths; if (p_item.is_file) { file_changed_paths.push_back(old_path); } else { folder_changed_paths.push_back(old_path); _get_all_items_in_dir(EditorFileSystem::get_singleton()->get_filesystem_path(old_path), file_changed_paths, folder_changed_paths); } Ref da = DirAccess::create(DirAccess::ACCESS_RESOURCES); print_verbose("Moving " + old_path + " -> " + new_path); Error err = da->rename(old_path, new_path); if (err == OK) { // Move/Rename any corresponding import settings too. if (p_item.is_file && FileAccess::exists(old_path + ".import")) { err = da->rename(old_path + ".import", new_path + ".import"); if (err != OK) { EditorNode::get_singleton()->add_io_error(TTR("Error moving:") + "\n" + old_path + ".import\n"); } } // Update scene if it is open. for (int i = 0; i < file_changed_paths.size(); ++i) { String new_item_path = p_item.is_file ? new_path : file_changed_paths[i].replace_first(old_path, new_path); if (ResourceLoader::get_resource_type(new_item_path) == "PackedScene" && EditorNode::get_singleton()->is_scene_open(file_changed_paths[i])) { EditorData *ed = &EditorNode::get_editor_data(); for (int j = 0; j < ed->get_edited_scene_count(); j++) { if (ed->get_scene_path(j) == file_changed_paths[i]) { ed->get_edited_scene_root(j)->set_scene_file_path(new_item_path); EditorNode::get_singleton()->save_editor_layout_delayed(); break; } } } } // Only treat as a changed dependency if it was successfully moved. for (int i = 0; i < file_changed_paths.size(); ++i) { p_file_renames[file_changed_paths[i]] = file_changed_paths[i].replace_first(old_path, new_path); print_verbose(" Remap: " + file_changed_paths[i] + " -> " + p_file_renames[file_changed_paths[i]]); emit_signal(SNAME("files_moved"), file_changed_paths[i], p_file_renames[file_changed_paths[i]]); } for (int i = 0; i < folder_changed_paths.size(); ++i) { p_folder_renames[folder_changed_paths[i]] = folder_changed_paths[i].replace_first(old_path, new_path); emit_signal(SNAME("folder_moved"), folder_changed_paths[i], p_folder_renames[folder_changed_paths[i]].substr(0, p_folder_renames[folder_changed_paths[i]].length() - 1)); } } else { EditorNode::get_singleton()->add_io_error(TTR("Error moving:") + "\n" + old_path + "\n"); } } void FileSystemDock::_try_duplicate_item(const FileOrFolder &p_item, const String &p_new_path) const { // Ensure folder paths end with "/". String old_path = (p_item.is_file || p_item.path.ends_with("/")) ? p_item.path : (p_item.path + "/"); String new_path = (p_item.is_file || p_new_path.ends_with("/")) ? p_new_path : (p_new_path + "/"); if (new_path == old_path) { return; } else if (old_path == "res://") { EditorNode::get_singleton()->add_io_error(TTR("Cannot move/rename resources root.")); return; } else if (!p_item.is_file && new_path.begins_with(old_path)) { // This check doesn't erroneously catch renaming to a longer name as folder paths always end with "/". EditorNode::get_singleton()->add_io_error(TTR("Cannot move a folder into itself.") + "\n" + old_path + "\n"); return; } Ref da = DirAccess::create(DirAccess::ACCESS_RESOURCES); if (p_item.is_file) { print_verbose("Duplicating " + old_path + " -> " + new_path); // Create the directory structure. da->make_dir_recursive(new_path.get_base_dir()); if (FileAccess::exists(old_path + ".import")) { Error err = da->copy(old_path, new_path); if (err != OK) { EditorNode::get_singleton()->add_io_error(TTR("Error duplicating:") + "\n" + old_path + ": " + error_names[err] + "\n"); return; } // Remove uid from .import file to avoid conflict. Ref cfg; cfg.instantiate(); cfg->load(old_path + ".import"); cfg->erase_section_key("remap", "uid"); err = cfg->save(new_path + ".import"); if (err != OK) { EditorNode::get_singleton()->add_io_error(TTR("Error duplicating:") + "\n" + old_path + ".import: " + error_names[err] + "\n"); return; } } else { // Files which do not use an uid can just be copied. if (ResourceLoader::get_resource_uid(old_path) == ResourceUID::INVALID_ID) { Error err = da->copy(old_path, new_path); if (err != OK) { EditorNode::get_singleton()->add_io_error(TTR("Error duplicating:") + "\n" + old_path + ": " + error_names[err] + "\n"); } return; } // Load the resource and save it again in the new location (this generates a new UID). Error err; Ref res = ResourceLoader::load(old_path, "", ResourceFormatLoader::CACHE_MODE_REUSE, &err); if (err == OK && res.is_valid()) { err = ResourceSaver::save(res, new_path, ResourceSaver::FLAG_COMPRESS); if (err != OK) { EditorNode::get_singleton()->add_io_error(TTR("Error duplicating:") + " " + vformat(TTR("Failed to save resource at %s: %s"), new_path, error_names[err])); } } else if (err != OK) { // When loading files like text files the error is OK but the resource is still null. // We can ignore such files. EditorNode::get_singleton()->add_io_error(TTR("Error duplicating:") + " " + vformat(TTR("Failed to load resource at %s: %s"), new_path, error_names[err])); } } } else { da->make_dir(new_path); // Recursively duplicate all files inside the folder. Ref old_dir = DirAccess::open(old_path); ERR_FAIL_COND(old_dir.is_null()); Ref file_access = FileAccess::create(FileAccess::ACCESS_RESOURCES); old_dir->set_include_navigational(false); old_dir->list_dir_begin(); for (String f = old_dir->_get_next(); !f.is_empty(); f = old_dir->_get_next()) { if (f.get_extension() == "import") { continue; } if (file_access->file_exists(old_path + f)) { _try_duplicate_item(FileOrFolder(old_path + f, true), new_path + f); } else if (da->dir_exists(old_path + f)) { _try_duplicate_item(FileOrFolder(old_path + f, false), new_path + f); } } old_dir->list_dir_end(); } } void FileSystemDock::_update_resource_paths_after_move(const HashMap &p_renames, const HashMap &p_uids) const { for (const KeyValue &pair : p_renames) { // Update UID path. const String &old_path = pair.key; const String &new_path = pair.value; const HashMap::ConstIterator I = p_uids.find(old_path); if (I) { ResourceUID::get_singleton()->set_id(I->value, new_path); } EditorFileSystem::get_singleton()->register_global_class_script(old_path, new_path); } // Rename all resources loaded, be it subresources or actual resources. List> cached; ResourceCache::get_cached_resources(&cached); for (Ref &r : cached) { String base_path = r->get_path(); String extra_path; int sep_pos = r->get_path().find("::"); if (sep_pos >= 0) { extra_path = base_path.substr(sep_pos, base_path.length()); base_path = base_path.substr(0, sep_pos); } if (p_renames.has(base_path)) { base_path = p_renames[base_path]; r->set_path(base_path + extra_path); } } ScriptServer::save_global_classes(); EditorNode::get_editor_data().script_class_save_icon_paths(); EditorFileSystem::get_singleton()->emit_signal(SNAME("script_classes_updated")); } void FileSystemDock::_update_dependencies_after_move(const HashMap &p_renames, const HashSet &p_file_owners) const { // The following code assumes that the following holds: // 1) EditorFileSystem contains the old paths/folder structure from before the rename/move. // 2) ResourceLoader can use the new paths without needing to call rescan. // The currently edited scene should be reloaded first, so get it's path (GH-82652). const String &edited_scene_path = EditorNode::get_editor_data().get_scene_path(EditorNode::get_editor_data().get_edited_scene()); List scenes_to_reload; for (const String &E : p_file_owners) { // Because we haven't called a rescan yet the found remap might still be an old path itself. const HashMap::ConstIterator I = p_renames.find(E); const String file = I ? I->value : E; print_verbose("Remapping dependencies for: " + file); const Error err = ResourceLoader::rename_dependencies(file, p_renames); if (err == OK) { if (ResourceLoader::get_resource_type(file) == "PackedScene") { if (file == edited_scene_path) { scenes_to_reload.push_front(file); } else { scenes_to_reload.push_back(file); } } } else { EditorNode::get_singleton()->add_io_error(TTR("Unable to update dependencies for:") + "\n" + E + "\n"); } } for (const String &E : scenes_to_reload) { EditorNode::get_singleton()->reload_scene(E); } } void FileSystemDock::_update_project_settings_after_move(const HashMap &p_renames, const HashMap &p_folders_renames) { // Find all project settings of type FILE and replace them if needed. const HashMap prop_info = ProjectSettings::get_singleton()->get_custom_property_info(); for (const KeyValue &E : prop_info) { if (E.value.hint == PROPERTY_HINT_FILE) { String old_path = GLOBAL_GET(E.key); if (p_renames.has(old_path)) { ProjectSettings::get_singleton()->set_setting(E.key, p_renames[old_path]); } }; } // Also search for the file in autoload, as they are stored differently from normal files. List property_list; ProjectSettings::get_singleton()->get_property_list(&property_list); for (const PropertyInfo &E : property_list) { if (E.name.begins_with("autoload/")) { // If the autoload resource paths has a leading "*", it indicates that it is a Singleton, // so we have to handle both cases when updating. String autoload = GLOBAL_GET(E.name); String autoload_singleton = autoload.substr(1, autoload.length()); if (p_renames.has(autoload)) { ProjectSettings::get_singleton()->set_setting(E.name, p_renames[autoload]); } else if (autoload.begins_with("*") && p_renames.has(autoload_singleton)) { ProjectSettings::get_singleton()->set_setting(E.name, "*" + p_renames[autoload_singleton]); } } } // Update folder colors. for (const KeyValue &rename : p_folders_renames) { if (assigned_folder_colors.has(rename.key)) { assigned_folder_colors[rename.value] = assigned_folder_colors[rename.key]; assigned_folder_colors.erase(rename.key); } } ProjectSettings::get_singleton()->save(); } String FileSystemDock::_get_unique_name(const FileOrFolder &p_entry, const String &p_at_path) { String new_path; String new_path_base; if (p_entry.is_file) { new_path = p_at_path.path_join(p_entry.path.get_file()); new_path_base = new_path.get_basename() + " (%d)." + new_path.get_extension(); } else { PackedStringArray path_split = p_entry.path.split("/"); new_path = p_at_path.path_join(path_split[path_split.size() - 2]); new_path_base = new_path + " (%d)"; } int exist_counter = 1; Ref da = DirAccess::create(DirAccess::ACCESS_RESOURCES); while (da->file_exists(new_path) || da->dir_exists(new_path)) { exist_counter++; new_path = vformat(new_path_base, exist_counter); } return new_path; } void FileSystemDock::_update_favorites_list_after_move(const HashMap &p_files_renames, const HashMap &p_folders_renames) const { Vector favorites_list = EditorSettings::get_singleton()->get_favorites(); Vector new_favorites; for (const String &old_path : favorites_list) { if (p_folders_renames.has(old_path)) { new_favorites.push_back(p_folders_renames[old_path]); } else if (p_files_renames.has(old_path)) { new_favorites.push_back(p_files_renames[old_path]); } else { new_favorites.push_back(old_path); } } EditorSettings::get_singleton()->set_favorites(new_favorites); } void FileSystemDock::_make_scene_confirm() { const String scene_path = make_scene_dialog->get_scene_path(); int idx = EditorNode::get_singleton()->new_scene(); EditorNode::get_editor_data().set_scene_path(idx, scene_path); EditorNode::get_singleton()->set_edited_scene(make_scene_dialog->create_scene_root()); EditorNode::get_singleton()->save_scene_if_open(scene_path); } void FileSystemDock::_resource_removed(const Ref &p_resource) { const Ref