1/**************************************************************************/
2/* filesystem_dock.cpp */
3/**************************************************************************/
4/* This file is part of: */
5/* GODOT ENGINE */
6/* https://godotengine.org */
7/**************************************************************************/
8/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10/* */
11/* Permission is hereby granted, free of charge, to any person obtaining */
12/* a copy of this software and associated documentation files (the */
13/* "Software"), to deal in the Software without restriction, including */
14/* without limitation the rights to use, copy, modify, merge, publish, */
15/* distribute, sublicense, and/or sell copies of the Software, and to */
16/* permit persons to whom the Software is furnished to do so, subject to */
17/* the following conditions: */
18/* */
19/* The above copyright notice and this permission notice shall be */
20/* included in all copies or substantial portions of the Software. */
21/* */
22/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29/**************************************************************************/
30
31#include "filesystem_dock.h"
32
33#include "core/config/project_settings.h"
34#include "core/io/dir_access.h"
35#include "core/io/file_access.h"
36#include "core/io/resource_loader.h"
37#include "core/os/keyboard.h"
38#include "core/os/os.h"
39#include "core/templates/list.h"
40#include "editor/create_dialog.h"
41#include "editor/directory_create_dialog.h"
42#include "editor/editor_feature_profile.h"
43#include "editor/editor_node.h"
44#include "editor/editor_resource_preview.h"
45#include "editor/editor_scale.h"
46#include "editor/editor_settings.h"
47#include "editor/editor_string_names.h"
48#include "editor/gui/editor_dir_dialog.h"
49#include "editor/gui/editor_scene_tabs.h"
50#include "editor/import/resource_importer_scene.h"
51#include "editor/import_dock.h"
52#include "editor/plugins/editor_resource_tooltip_plugins.h"
53#include "editor/scene_create_dialog.h"
54#include "editor/scene_tree_dock.h"
55#include "editor/shader_create_dialog.h"
56#include "scene/gui/item_list.h"
57#include "scene/gui/label.h"
58#include "scene/gui/line_edit.h"
59#include "scene/gui/progress_bar.h"
60#include "scene/gui/texture_rect.h"
61#include "scene/main/window.h"
62#include "scene/resources/packed_scene.h"
63#include "servers/display_server.h"
64
65Control *FileSystemTree::make_custom_tooltip(const String &p_text) const {
66 TreeItem *item = get_item_at_position(get_local_mouse_position());
67 if (!item) {
68 return nullptr;
69 }
70 return FileSystemDock::get_singleton()->create_tooltip_for_path(item->get_metadata(0));
71}
72
73Control *FileSystemList::make_custom_tooltip(const String &p_text) const {
74 int idx = get_item_at_position(get_local_mouse_position());
75 if (idx == -1) {
76 return nullptr;
77 }
78 return FileSystemDock::get_singleton()->create_tooltip_for_path(get_item_metadata(idx));
79}
80
81void FileSystemList::_line_editor_submit(String p_text) {
82 popup_editor->hide();
83
84 emit_signal(SNAME("item_edited"));
85 queue_redraw();
86}
87
88bool FileSystemList::edit_selected() {
89 ERR_FAIL_COND_V_MSG(!is_anything_selected(), false, "No item selected.");
90 int s = get_current();
91 ensure_current_is_visible();
92
93 Rect2 rect;
94 Rect2 popup_rect;
95 Vector2 ofs;
96
97 Vector2 icon_size = get_item_icon(s)->get_size();
98
99 // Handles the different icon modes (TOP/LEFT).
100 switch (get_icon_mode()) {
101 case ItemList::ICON_MODE_LEFT:
102 rect = get_item_rect(s, true);
103 ofs = Vector2(0, Math::floor((MAX(line_editor->get_minimum_size().height, rect.size.height) - rect.size.height) / 2));
104 popup_rect.position = get_screen_position() + rect.position - ofs;
105 popup_rect.size = rect.size;
106
107 // Adjust for icon position and size.
108 popup_rect.size.x -= icon_size.x;
109 popup_rect.position.x += icon_size.x;
110 break;
111 case ItemList::ICON_MODE_TOP:
112 rect = get_item_rect(s, false);
113 popup_rect.position = get_screen_position() + rect.position;
114 popup_rect.size = rect.size;
115
116 // Adjust for icon position and size.
117 popup_rect.size.y -= icon_size.y;
118 popup_rect.position.y += icon_size.y;
119 break;
120 }
121
122 popup_editor->set_position(popup_rect.position);
123 popup_editor->set_size(popup_rect.size);
124
125 String name = get_item_text(s);
126 line_editor->set_text(name);
127 line_editor->select(0, name.rfind("."));
128
129 popup_editor->popup();
130 popup_editor->child_controls_changed();
131 line_editor->grab_focus();
132 return true;
133}
134
135String FileSystemList::get_edit_text() {
136 return line_editor->get_text();
137}
138
139void FileSystemList::_text_editor_popup_modal_close() {
140 if (Input::get_singleton()->is_key_pressed(Key::ESCAPE) ||
141 Input::get_singleton()->is_key_pressed(Key::KP_ENTER) ||
142 Input::get_singleton()->is_key_pressed(Key::ENTER)) {
143 return;
144 }
145
146 _line_editor_submit(line_editor->get_text());
147}
148
149void FileSystemList::_bind_methods() {
150 ADD_SIGNAL(MethodInfo("item_edited"));
151}
152
153FileSystemList::FileSystemList() {
154 popup_editor = memnew(Popup);
155 add_child(popup_editor);
156
157 popup_editor_vb = memnew(VBoxContainer);
158 popup_editor_vb->add_theme_constant_override("separation", 0);
159 popup_editor_vb->set_anchors_and_offsets_preset(PRESET_FULL_RECT);
160 popup_editor->add_child(popup_editor_vb);
161
162 line_editor = memnew(LineEdit);
163 line_editor->set_v_size_flags(SIZE_EXPAND_FILL);
164 popup_editor_vb->add_child(line_editor);
165 line_editor->connect("text_submitted", callable_mp(this, &FileSystemList::_line_editor_submit));
166 popup_editor->connect("popup_hide", callable_mp(this, &FileSystemList::_text_editor_popup_modal_close));
167}
168
169FileSystemDock *FileSystemDock::singleton = nullptr;
170
171Ref<Texture2D> FileSystemDock::_get_tree_item_icon(bool p_is_valid, String p_file_type) {
172 Ref<Texture2D> file_icon;
173 if (!p_is_valid) {
174 file_icon = get_editor_theme_icon(SNAME("ImportFail"));
175 } else {
176 file_icon = (has_theme_icon(p_file_type, EditorStringName(EditorIcons))) ? get_editor_theme_icon(p_file_type) : get_editor_theme_icon(SNAME("File"));
177 }
178 return file_icon;
179}
180
181bool FileSystemDock::_create_tree(TreeItem *p_parent, EditorFileSystemDirectory *p_dir, Vector<String> &uncollapsed_paths, bool p_select_in_favorites, bool p_unfold_path) {
182 bool parent_should_expand = false;
183
184 // Create a tree item for the subdirectory.
185 TreeItem *subdirectory_item = tree->create_item(p_parent);
186 String dname = p_dir->get_name();
187 String lpath = p_dir->get_path();
188
189 if (dname.is_empty()) {
190 dname = "res://";
191 }
192
193 // Set custom folder color (if applicable).
194 bool has_custom_color = assigned_folder_colors.has(lpath);
195 Color custom_color = has_custom_color ? folder_colors[assigned_folder_colors[lpath]] : Color();
196
197 if (has_custom_color) {
198 subdirectory_item->set_icon_modulate(0, editor_is_dark_theme ? custom_color : custom_color * 1.75);
199 subdirectory_item->set_custom_bg_color(0, Color(custom_color, editor_is_dark_theme ? 0.1 : 0.15));
200 } else {
201 TreeItem *parent = subdirectory_item->get_parent();
202 if (parent) {
203 Color parent_bg_color = parent->get_custom_bg_color(0);
204 if (parent_bg_color != Color()) {
205 bool parent_has_custom_color = assigned_folder_colors.has(parent->get_metadata(0));
206 subdirectory_item->set_custom_bg_color(0, parent_has_custom_color ? parent_bg_color.darkened(0.3) : parent_bg_color);
207 subdirectory_item->set_icon_modulate(0, parent->get_icon_modulate(0));
208 } else {
209 subdirectory_item->set_icon_modulate(0, get_theme_color(SNAME("folder_icon_color"), SNAME("FileDialog")));
210 }
211 }
212 }
213
214 subdirectory_item->set_text(0, dname);
215 subdirectory_item->set_structured_text_bidi_override(0, TextServer::STRUCTURED_TEXT_FILE);
216 subdirectory_item->set_icon(0, get_editor_theme_icon(SNAME("Folder")));
217 subdirectory_item->set_selectable(0, true);
218 subdirectory_item->set_metadata(0, lpath);
219 if (!p_select_in_favorites && (current_path == lpath || ((display_mode == DISPLAY_MODE_SPLIT) && current_path.get_base_dir() == lpath))) {
220 subdirectory_item->select(0);
221 // Keep select an item when re-created a tree
222 // To prevent crashing when nothing is selected.
223 subdirectory_item->set_as_cursor(0);
224 }
225
226 if (p_unfold_path && current_path.begins_with(lpath) && current_path != lpath) {
227 subdirectory_item->set_collapsed(false);
228 } else {
229 subdirectory_item->set_collapsed(uncollapsed_paths.find(lpath) < 0);
230 }
231 if (searched_string.length() > 0 && dname.to_lower().find(searched_string) >= 0) {
232 parent_should_expand = true;
233 }
234
235 // Create items for all subdirectories.
236 bool reversed = file_sort == FILE_SORT_NAME_REVERSE;
237 for (int i = reversed ? p_dir->get_subdir_count() - 1 : 0;
238 reversed ? i >= 0 : i < p_dir->get_subdir_count();
239 reversed ? i-- : i++) {
240 parent_should_expand = (_create_tree(subdirectory_item, p_dir->get_subdir(i), uncollapsed_paths, p_select_in_favorites, p_unfold_path) || parent_should_expand);
241 }
242
243 // Create all items for the files in the subdirectory.
244 if (display_mode == DISPLAY_MODE_TREE_ONLY) {
245 String main_scene = GLOBAL_GET("application/run/main_scene");
246
247 // Build the list of the files to display.
248 List<FileInfo> file_list;
249 for (int i = 0; i < p_dir->get_file_count(); i++) {
250 String file_type = p_dir->get_file_type(i);
251 if (file_type != "TextFile" && _is_file_type_disabled_by_feature_profile(file_type)) {
252 // If type is disabled, file won't be displayed.
253 continue;
254 }
255
256 String file_name = p_dir->get_file(i);
257 if (searched_string.length() > 0) {
258 if (file_name.to_lower().find(searched_string) < 0) {
259 // The searched string is not in the file name, we skip it.
260 continue;
261 } else {
262 // We expand all parents.
263 parent_should_expand = true;
264 }
265 }
266
267 FileInfo fi;
268 fi.name = p_dir->get_file(i);
269 fi.type = p_dir->get_file_type(i);
270 fi.import_broken = !p_dir->get_file_import_is_valid(i);
271 fi.modified_time = p_dir->get_file_modified_time(i);
272
273 file_list.push_back(fi);
274 }
275
276 // Sort the file list if needed.
277 _sort_file_info_list(file_list);
278
279 // Build the tree.
280 for (const FileInfo &fi : file_list) {
281 TreeItem *file_item = tree->create_item(subdirectory_item);
282 file_item->set_text(0, fi.name);
283 file_item->set_structured_text_bidi_override(0, TextServer::STRUCTURED_TEXT_FILE);
284 file_item->set_icon(0, _get_tree_item_icon(!fi.import_broken, fi.type));
285 Color parent_bg_color = subdirectory_item->get_custom_bg_color(0);
286 if (has_custom_color) {
287 file_item->set_custom_bg_color(0, parent_bg_color.darkened(0.3));
288 } else if (parent_bg_color != Color()) {
289 file_item->set_custom_bg_color(0, parent_bg_color);
290 }
291 String file_metadata = lpath.path_join(fi.name);
292 file_item->set_metadata(0, file_metadata);
293 if (!p_select_in_favorites && current_path == file_metadata) {
294 file_item->select(0);
295 file_item->set_as_cursor(0);
296 }
297 if (main_scene == file_metadata) {
298 file_item->set_custom_color(0, get_theme_color(SNAME("accent_color"), EditorStringName(Editor)));
299 }
300 Array udata;
301 udata.push_back(tree_update_id);
302 udata.push_back(file_item);
303 EditorResourcePreview::get_singleton()->queue_resource_preview(file_metadata, this, "_tree_thumbnail_done", udata);
304 }
305 } else if (display_mode == DISPLAY_MODE_SPLIT) {
306 if (lpath.get_base_dir() == current_path.get_base_dir()) {
307 subdirectory_item->select(0);
308 subdirectory_item->set_as_cursor(0);
309 }
310 }
311
312 if (searched_string.length() > 0) {
313 if (parent_should_expand) {
314 subdirectory_item->set_collapsed(false);
315 } else if (dname != "res://") {
316 subdirectory_item->get_parent()->remove_child(subdirectory_item);
317 memdelete(subdirectory_item);
318 }
319 }
320
321 return parent_should_expand;
322}
323
324Vector<String> FileSystemDock::get_uncollapsed_paths() const {
325 Vector<String> uncollapsed_paths;
326 TreeItem *root = tree->get_root();
327 if (root) {
328 TreeItem *favorites_item = root->get_first_child();
329 if (!favorites_item->is_collapsed()) {
330 uncollapsed_paths.push_back(favorites_item->get_metadata(0));
331 }
332
333 // BFS to find all uncollapsed paths of the resource directory.
334 TreeItem *res_subtree = root->get_first_child()->get_next();
335 if (res_subtree) {
336 List<TreeItem *> queue;
337 queue.push_back(res_subtree);
338
339 while (!queue.is_empty()) {
340 TreeItem *ti = queue.back()->get();
341 queue.pop_back();
342 if (!ti->is_collapsed() && ti->get_child_count() > 0) {
343 Variant path = ti->get_metadata(0);
344 if (path) {
345 uncollapsed_paths.push_back(path);
346 }
347 }
348 for (int i = 0; i < ti->get_child_count(); i++) {
349 queue.push_back(ti->get_child(i));
350 }
351 }
352 }
353 }
354 return uncollapsed_paths;
355}
356
357void FileSystemDock::_update_tree(const Vector<String> &p_uncollapsed_paths, bool p_uncollapse_root, bool p_select_in_favorites, bool p_unfold_path) {
358 // Recreate the tree.
359 tree->clear();
360 tree_update_id++;
361 updating_tree = true;
362 TreeItem *root = tree->create_item();
363
364 // Handles the favorites.
365 TreeItem *favorites_item = tree->create_item(root);
366 favorites_item->set_icon(0, get_editor_theme_icon(SNAME("Favorites")));
367 favorites_item->set_text(0, TTR("Favorites:"));
368 favorites_item->set_metadata(0, "Favorites");
369 favorites_item->set_collapsed(p_uncollapsed_paths.find("Favorites") < 0);
370
371 Vector<String> favorite_paths = EditorSettings::get_singleton()->get_favorites();
372
373 Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);
374 bool fav_changed = false;
375 for (int i = favorite_paths.size() - 1; i >= 0; i--) {
376 if (da->dir_exists(favorite_paths[i]) || da->file_exists(favorite_paths[i])) {
377 continue;
378 }
379 favorite_paths.remove_at(i);
380 fav_changed = true;
381 }
382 if (fav_changed) {
383 EditorSettings::get_singleton()->set_favorites(favorite_paths);
384 }
385
386 Ref<Texture2D> folder_icon = get_editor_theme_icon(SNAME("Folder"));
387 const Color default_folder_color = get_theme_color(SNAME("folder_icon_color"), SNAME("FileDialog"));
388
389 for (int i = 0; i < favorite_paths.size(); i++) {
390 String favorite = favorite_paths[i];
391 if (!favorite.begins_with("res://")) {
392 continue;
393 }
394
395 String text;
396 Ref<Texture2D> icon;
397 Color color;
398 if (favorite == "res://") {
399 text = "/";
400 icon = folder_icon;
401 color = default_folder_color;
402 } else if (favorite.ends_with("/")) {
403 text = favorite.substr(0, favorite.length() - 1).get_file();
404 icon = folder_icon;
405 color = assigned_folder_colors.has(favorite) ? folder_colors[assigned_folder_colors[favorite]] : default_folder_color;
406 } else {
407 text = favorite.get_file();
408 int index;
409 EditorFileSystemDirectory *dir = EditorFileSystem::get_singleton()->find_file(favorite, &index);
410 if (dir) {
411 icon = _get_tree_item_icon(dir->get_file_import_is_valid(index), dir->get_file_type(index));
412 } else {
413 icon = get_editor_theme_icon(SNAME("File"));
414 }
415 color = Color(1, 1, 1);
416 }
417
418 if (searched_string.length() == 0 || text.to_lower().find(searched_string) >= 0) {
419 TreeItem *ti = tree->create_item(favorites_item);
420 ti->set_text(0, text);
421 ti->set_icon(0, icon);
422 ti->set_icon_modulate(0, color);
423 ti->set_tooltip_text(0, favorite);
424 ti->set_selectable(0, true);
425 ti->set_metadata(0, favorite);
426 if (p_select_in_favorites && favorite == current_path) {
427 ti->select(0);
428 ti->set_as_cursor(0);
429 }
430 if (!favorite.ends_with("/")) {
431 Array udata;
432 udata.push_back(tree_update_id);
433 udata.push_back(ti);
434 EditorResourcePreview::get_singleton()->queue_resource_preview(favorite, this, "_tree_thumbnail_done", udata);
435 }
436 }
437 }
438
439 Vector<String> uncollapsed_paths = p_uncollapsed_paths;
440 if (p_uncollapse_root) {
441 uncollapsed_paths.push_back("res://");
442 }
443
444 // Create the remaining of the tree.
445 _create_tree(root, EditorFileSystem::get_singleton()->get_filesystem(), uncollapsed_paths, p_select_in_favorites, p_unfold_path);
446 tree->ensure_cursor_is_visible();
447 updating_tree = false;
448}
449
450void FileSystemDock::set_display_mode(DisplayMode p_display_mode) {
451 display_mode = p_display_mode;
452 _update_display_mode(false);
453}
454
455void FileSystemDock::_update_display_mode(bool p_force) {
456 // Compute the new display mode.
457 if (p_force || old_display_mode != display_mode) {
458 button_toggle_display_mode->set_pressed(display_mode == DISPLAY_MODE_SPLIT);
459 switch (display_mode) {
460 case DISPLAY_MODE_TREE_ONLY:
461 tree->show();
462 tree->set_v_size_flags(SIZE_EXPAND_FILL);
463 if (display_mode == DISPLAY_MODE_TREE_ONLY) {
464 toolbar2_hbc->show();
465 } else {
466 toolbar2_hbc->hide();
467 }
468
469 _update_tree(get_uncollapsed_paths());
470 file_list_vb->hide();
471 break;
472
473 case DISPLAY_MODE_SPLIT:
474 tree->show();
475 tree->set_v_size_flags(SIZE_EXPAND_FILL);
476 tree->ensure_cursor_is_visible();
477 toolbar2_hbc->hide();
478 _update_tree(get_uncollapsed_paths());
479
480 file_list_vb->show();
481 _update_file_list(true);
482 break;
483 }
484 old_display_mode = display_mode;
485 }
486}
487
488void FileSystemDock::_notification(int p_what) {
489 switch (p_what) {
490 case NOTIFICATION_TRANSLATION_CHANGED:
491 case NOTIFICATION_LAYOUT_DIRECTION_CHANGED:
492 case NOTIFICATION_ENTER_TREE: {
493 if (initialized) {
494 return;
495 }
496 initialized = true;
497 EditorFeatureProfileManager::get_singleton()->connect("current_feature_profile_changed", callable_mp(this, &FileSystemDock::_feature_profile_changed));
498
499 EditorFileSystem::get_singleton()->connect("filesystem_changed", callable_mp(this, &FileSystemDock::_fs_changed));
500 EditorResourcePreview::get_singleton()->connect("preview_invalidated", callable_mp(this, &FileSystemDock::_preview_invalidated));
501
502 button_reload->set_icon(get_editor_theme_icon(SNAME("Reload")));
503 button_toggle_display_mode->set_icon(get_editor_theme_icon(SNAME("Panels2")));
504 button_file_list_display_mode->connect("pressed", callable_mp(this, &FileSystemDock::_toggle_file_display));
505
506 files->connect("item_activated", callable_mp(this, &FileSystemDock::_file_list_activate_file));
507 button_hist_next->connect("pressed", callable_mp(this, &FileSystemDock::_fw_history));
508 button_hist_prev->connect("pressed", callable_mp(this, &FileSystemDock::_bw_history));
509
510 tree_search_box->set_right_icon(get_editor_theme_icon(SNAME("Search")));
511 tree_search_box->set_clear_button_enabled(true);
512 tree_button_sort->set_icon(get_editor_theme_icon(SNAME("Sort")));
513
514 file_list_search_box->set_right_icon(get_editor_theme_icon(SNAME("Search")));
515 file_list_search_box->set_clear_button_enabled(true);
516 file_list_button_sort->set_icon(get_editor_theme_icon(SNAME("Sort")));
517
518 if (is_layout_rtl()) {
519 button_hist_next->set_icon(get_editor_theme_icon(SNAME("Back")));
520 button_hist_prev->set_icon(get_editor_theme_icon(SNAME("Forward")));
521 } else {
522 button_hist_next->set_icon(get_editor_theme_icon(SNAME("Forward")));
523 button_hist_prev->set_icon(get_editor_theme_icon(SNAME("Back")));
524 }
525 file_list_popup->connect("id_pressed", callable_mp(this, &FileSystemDock::_file_list_rmb_option));
526 tree_popup->connect("id_pressed", callable_mp(this, &FileSystemDock::_tree_rmb_option));
527
528 current_path_line_edit->connect("text_submitted", callable_mp(this, &FileSystemDock::_navigate_to_path).bind(false));
529
530 always_show_folders = bool(EDITOR_GET("docks/filesystem/always_show_folders"));
531
532 set_file_list_display_mode(FileSystemDock::FILE_LIST_DISPLAY_LIST);
533
534 _update_display_mode();
535
536 if (EditorFileSystem::get_singleton()->is_scanning()) {
537 _set_scanning_mode();
538 } else {
539 _update_tree(Vector<String>(), true);
540 }
541 } break;
542
543 case NOTIFICATION_PROCESS: {
544 if (EditorFileSystem::get_singleton()->is_scanning()) {
545 scanning_progress->set_value(EditorFileSystem::get_singleton()->get_scanning_progress() * 100);
546 }
547 } break;
548
549 case NOTIFICATION_DRAG_BEGIN: {
550 Dictionary dd = get_viewport()->gui_get_drag_data();
551 if (tree->is_visible_in_tree() && dd.has("type")) {
552 if (dd.has("favorite")) {
553 if ((String(dd["favorite"]) == "all")) {
554 tree->set_drop_mode_flags(Tree::DROP_MODE_INBETWEEN);
555 }
556 } else if ((String(dd["type"]) == "files") || (String(dd["type"]) == "files_and_dirs") || (String(dd["type"]) == "resource")) {
557 tree->set_drop_mode_flags(Tree::DROP_MODE_ON_ITEM | Tree::DROP_MODE_INBETWEEN);
558 } else if ((String(dd["type"]) == "nodes")) {
559 holding_branch = true;
560 TreeItem *item = tree->get_next_selected(tree->get_root());
561 while (item) {
562 tree_items_selected_on_drag_begin.push_back(item);
563 item = tree->get_next_selected(item);
564 }
565 list_items_selected_on_drag_begin = files->get_selected_items();
566 }
567 }
568 } break;
569
570 case NOTIFICATION_DRAG_END: {
571 tree->set_drop_mode_flags(0);
572
573 if (holding_branch) {
574 holding_branch = false;
575 _reselect_items_selected_on_drag_begin(true);
576 }
577 } break;
578
579 case NOTIFICATION_THEME_CHANGED: {
580 overwrite_dialog_scroll->add_theme_style_override("panel", get_theme_stylebox("panel", "Tree"));
581
582 if (is_visible_in_tree()) {
583 _update_display_mode(true);
584 }
585 } break;
586
587 case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {
588 // Update icons.
589 button_reload->set_icon(get_editor_theme_icon(SNAME("Reload")));
590 button_toggle_display_mode->set_icon(get_editor_theme_icon(SNAME("Panels2")));
591 if (is_layout_rtl()) {
592 button_hist_next->set_icon(get_editor_theme_icon(SNAME("Back")));
593 button_hist_prev->set_icon(get_editor_theme_icon(SNAME("Forward")));
594 } else {
595 button_hist_next->set_icon(get_editor_theme_icon(SNAME("Forward")));
596 button_hist_prev->set_icon(get_editor_theme_icon(SNAME("Back")));
597 }
598 if (file_list_display_mode == FILE_LIST_DISPLAY_LIST) {
599 button_file_list_display_mode->set_icon(get_editor_theme_icon(SNAME("FileThumbnail")));
600 } else {
601 button_file_list_display_mode->set_icon(get_editor_theme_icon(SNAME("FileList")));
602 }
603
604 tree_search_box->set_right_icon(get_editor_theme_icon(SNAME("Search")));
605 tree_search_box->set_clear_button_enabled(true);
606 tree_button_sort->set_icon(get_editor_theme_icon(SNAME("Sort")));
607
608 file_list_search_box->set_right_icon(get_editor_theme_icon(SNAME("Search")));
609 file_list_search_box->set_clear_button_enabled(true);
610 file_list_button_sort->set_icon(get_editor_theme_icon(SNAME("Sort")));
611
612 // Update editor dark theme & always show folders states from editor settings, redraw if needed.
613 bool do_redraw = false;
614
615 bool new_editor_is_dark_theme = EditorSettings::get_singleton()->is_dark_theme();
616 if (new_editor_is_dark_theme != editor_is_dark_theme) {
617 editor_is_dark_theme = new_editor_is_dark_theme;
618 do_redraw = true;
619 }
620
621 bool new_always_show_folders = bool(EDITOR_GET("docks/filesystem/always_show_folders"));
622 if (new_always_show_folders != always_show_folders) {
623 always_show_folders = new_always_show_folders;
624 do_redraw = true;
625 }
626
627 if (do_redraw) {
628 _update_file_list(true);
629 _update_tree(get_uncollapsed_paths());
630 }
631
632 // Change full tree mode.
633 _update_display_mode();
634 } break;
635 }
636}
637
638void FileSystemDock::_tree_multi_selected(Object *p_item, int p_column, bool p_selected) {
639 // Update the import dock.
640 import_dock_needs_update = true;
641 call_deferred(SNAME("_update_import_dock"));
642
643 // Return if we don't select something new.
644 if (!p_selected) {
645 return;
646 }
647
648 // Tree item selected.
649 TreeItem *selected = tree->get_selected();
650 if (!selected) {
651 return;
652 }
653
654 TreeItem *favorites_item = tree->get_root()->get_first_child();
655 if (selected->get_parent() == favorites_item && !String(selected->get_metadata(0)).ends_with("/")) {
656 // Go to the favorites if we click in the favorites and the path has changed.
657 current_path = "Favorites";
658 } else {
659 current_path = selected->get_metadata(0);
660 // Note: the "Favorites" item also leads to this path.
661 }
662
663 // Display the current path.
664 _set_current_path_line_edit_text(current_path);
665 _push_to_history();
666
667 // Update the file list.
668 if (!updating_tree && display_mode == DISPLAY_MODE_SPLIT) {
669 _update_file_list(false);
670 }
671}
672
673Vector<String> FileSystemDock::get_selected_paths() const {
674 return _tree_get_selected(false);
675}
676
677String FileSystemDock::get_current_path() const {
678 return current_path;
679}
680
681String FileSystemDock::get_current_directory() const {
682 if (current_path.ends_with("/")) {
683 return current_path;
684 } else {
685 return current_path.get_base_dir();
686 }
687}
688
689void FileSystemDock::_set_current_path_line_edit_text(const String &p_path) {
690 if (p_path == "Favorites") {
691 current_path_line_edit->set_text(TTR("Favorites"));
692 } else {
693 current_path_line_edit->set_text(current_path);
694 }
695}
696
697void FileSystemDock::_navigate_to_path(const String &p_path, bool p_select_in_favorites) {
698 if (p_path == "Favorites") {
699 current_path = p_path;
700 } else {
701 String target_path = p_path;
702 // If the path is a file, do not only go to the directory in the tree, also select the file in the file list.
703 if (target_path.ends_with("/")) {
704 target_path = target_path.substr(0, target_path.length() - 1);
705 }
706 Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);
707 if (da->file_exists(p_path)) {
708 current_path = target_path;
709 } else if (da->dir_exists(p_path)) {
710 current_path = target_path + "/";
711 } else {
712 ERR_FAIL_MSG(vformat("Cannot navigate to '%s' as it has not been found in the file system!", p_path));
713 }
714 }
715
716 _set_current_path_line_edit_text(current_path);
717 _push_to_history();
718
719 _update_tree(get_uncollapsed_paths(), false, p_select_in_favorites, true);
720 if (display_mode == DISPLAY_MODE_SPLIT) {
721 _update_file_list(false);
722 files->get_v_scroll_bar()->set_value(0);
723 }
724
725 String file_name = p_path.get_file();
726 if (!file_name.is_empty()) {
727 for (int i = 0; i < files->get_item_count(); i++) {
728 if (files->get_item_text(i) == file_name) {
729 files->select(i, true);
730 files->ensure_current_is_visible();
731 break;
732 }
733 }
734 }
735}
736
737void FileSystemDock::navigate_to_path(const String &p_path) {
738 file_list_search_box->clear();
739 _navigate_to_path(p_path);
740}
741
742void FileSystemDock::_file_list_thumbnail_done(const String &p_path, const Ref<Texture2D> &p_preview, const Ref<Texture2D> &p_small_preview, const Variant &p_udata) {
743 if ((file_list_vb->is_visible_in_tree() || current_path == p_path.get_base_dir()) && p_preview.is_valid()) {
744 Array uarr = p_udata;
745 int idx = uarr[0];
746 String file = uarr[1];
747 if (idx < files->get_item_count() && files->get_item_text(idx) == file && files->get_item_metadata(idx) == p_path) {
748 if (file_list_display_mode == FILE_LIST_DISPLAY_LIST) {
749 if (p_small_preview.is_valid()) {
750 files->set_item_icon(idx, p_small_preview);
751 }
752 } else {
753 files->set_item_icon(idx, p_preview);
754 }
755 }
756 }
757}
758
759void FileSystemDock::_tree_thumbnail_done(const String &p_path, const Ref<Texture2D> &p_preview, const Ref<Texture2D> &p_small_preview, const Variant &p_udata) {
760 if (p_small_preview.is_valid()) {
761 Array uarr = p_udata;
762 if (tree_update_id == (int)uarr[0]) {
763 TreeItem *file_item = Object::cast_to<TreeItem>(uarr[1]);
764 if (file_item) {
765 file_item->set_icon(0, p_small_preview);
766 }
767 }
768 }
769}
770
771void FileSystemDock::_toggle_file_display() {
772 _set_file_display(file_list_display_mode != FILE_LIST_DISPLAY_LIST);
773 emit_signal(SNAME("display_mode_changed"));
774}
775
776void FileSystemDock::_set_file_display(bool p_active) {
777 if (p_active) {
778 file_list_display_mode = FILE_LIST_DISPLAY_LIST;
779 button_file_list_display_mode->set_icon(get_editor_theme_icon(SNAME("FileThumbnail")));
780 button_file_list_display_mode->set_tooltip_text(TTR("View items as a grid of thumbnails."));
781 } else {
782 file_list_display_mode = FILE_LIST_DISPLAY_THUMBNAILS;
783 button_file_list_display_mode->set_icon(get_editor_theme_icon(SNAME("FileList")));
784 button_file_list_display_mode->set_tooltip_text(TTR("View items as a list."));
785 }
786
787 _update_file_list(true);
788}
789
790bool FileSystemDock::_is_file_type_disabled_by_feature_profile(const StringName &p_class) {
791 Ref<EditorFeatureProfile> profile = EditorFeatureProfileManager::get_singleton()->get_current_profile();
792 if (profile.is_null()) {
793 return false;
794 }
795
796 StringName class_name = p_class;
797
798 while (class_name != StringName()) {
799 if (profile->is_class_disabled(class_name)) {
800 return true;
801 }
802 class_name = ClassDB::get_parent_class(class_name);
803 }
804
805 return false;
806}
807
808void FileSystemDock::_search(EditorFileSystemDirectory *p_path, List<FileInfo> *matches, int p_max_items) {
809 if (matches->size() > p_max_items) {
810 return;
811 }
812
813 for (int i = 0; i < p_path->get_subdir_count(); i++) {
814 _search(p_path->get_subdir(i), matches, p_max_items);
815 }
816
817 for (int i = 0; i < p_path->get_file_count(); i++) {
818 String file = p_path->get_file(i);
819
820 if (file.to_lower().contains(searched_string)) {
821 FileInfo fi;
822 fi.name = file;
823 fi.type = p_path->get_file_type(i);
824 fi.path = p_path->get_file_path(i);
825 fi.import_broken = !p_path->get_file_import_is_valid(i);
826 fi.modified_time = p_path->get_file_modified_time(i);
827
828 if (_is_file_type_disabled_by_feature_profile(fi.type)) {
829 // This type is disabled, will not appear here.
830 continue;
831 }
832
833 matches->push_back(fi);
834 if (matches->size() > p_max_items) {
835 return;
836 }
837 }
838 }
839}
840
841struct FileSystemDock::FileInfoTypeComparator {
842 bool operator()(const FileInfo &p_a, const FileInfo &p_b) const {
843 // Uses the extension, then the icon name to distinguish file types.
844 String icon_path_a = "";
845 String icon_path_b = "";
846 Ref<Texture2D> icon_a = EditorNode::get_singleton()->get_class_icon(p_a.type);
847 if (icon_a.is_valid()) {
848 icon_path_a = icon_a->get_name();
849 }
850 Ref<Texture2D> icon_b = EditorNode::get_singleton()->get_class_icon(p_b.type);
851 if (icon_b.is_valid()) {
852 icon_path_b = icon_b->get_name();
853 }
854 return NaturalNoCaseComparator()(p_a.name.get_extension() + icon_path_a + p_a.name.get_basename(), p_b.name.get_extension() + icon_path_b + p_b.name.get_basename());
855 }
856};
857
858struct FileSystemDock::FileInfoModifiedTimeComparator {
859 bool operator()(const FileInfo &p_a, const FileInfo &p_b) const {
860 return p_a.modified_time > p_b.modified_time;
861 }
862};
863
864void FileSystemDock::_sort_file_info_list(List<FileSystemDock::FileInfo> &r_file_list) {
865 // Sort the file list if needed.
866 switch (file_sort) {
867 case FILE_SORT_TYPE:
868 r_file_list.sort_custom<FileInfoTypeComparator>();
869 break;
870 case FILE_SORT_TYPE_REVERSE:
871 r_file_list.sort_custom<FileInfoTypeComparator>();
872 r_file_list.reverse();
873 break;
874 case FILE_SORT_MODIFIED_TIME:
875 r_file_list.sort_custom<FileInfoModifiedTimeComparator>();
876 break;
877 case FILE_SORT_MODIFIED_TIME_REVERSE:
878 r_file_list.sort_custom<FileInfoModifiedTimeComparator>();
879 r_file_list.reverse();
880 break;
881 case FILE_SORT_NAME_REVERSE:
882 r_file_list.sort();
883 r_file_list.reverse();
884 break;
885 default: // FILE_SORT_NAME
886 r_file_list.sort();
887 break;
888 }
889}
890
891void FileSystemDock::_update_file_list(bool p_keep_selection) {
892 // Register the previously selected items.
893 HashSet<String> cselection;
894 if (p_keep_selection) {
895 for (int i = 0; i < files->get_item_count(); i++) {
896 if (files->is_selected(i)) {
897 cselection.insert(files->get_item_text(i));
898 }
899 }
900 }
901
902 files->clear();
903
904 _set_current_path_line_edit_text(current_path);
905
906 String directory = current_path;
907 String file = "";
908
909 int thumbnail_size = EDITOR_GET("docks/filesystem/thumbnail_size");
910 thumbnail_size *= EDSCALE;
911 Ref<Texture2D> folder_thumbnail;
912 Ref<Texture2D> file_thumbnail;
913 Ref<Texture2D> file_thumbnail_broken;
914
915 bool use_thumbnails = (file_list_display_mode == FILE_LIST_DISPLAY_THUMBNAILS);
916
917 if (use_thumbnails) {
918 // Thumbnails mode.
919 files->set_max_columns(0);
920 files->set_icon_mode(ItemList::ICON_MODE_TOP);
921 files->set_fixed_column_width(thumbnail_size * 3 / 2);
922 files->set_max_text_lines(2);
923 files->set_fixed_icon_size(Size2(thumbnail_size, thumbnail_size));
924
925 if (thumbnail_size < 64) {
926 folder_thumbnail = get_editor_theme_icon(SNAME("FolderMediumThumb"));
927 file_thumbnail = get_editor_theme_icon(SNAME("FileMediumThumb"));
928 file_thumbnail_broken = get_editor_theme_icon(SNAME("FileDeadMediumThumb"));
929 } else {
930 folder_thumbnail = get_editor_theme_icon(SNAME("FolderBigThumb"));
931 file_thumbnail = get_editor_theme_icon(SNAME("FileBigThumb"));
932 file_thumbnail_broken = get_editor_theme_icon(SNAME("FileDeadBigThumb"));
933 }
934 } else {
935 // No thumbnails.
936 files->set_icon_mode(ItemList::ICON_MODE_LEFT);
937 files->set_max_columns(1);
938 files->set_max_text_lines(1);
939 files->set_fixed_column_width(0);
940 files->set_fixed_icon_size(Size2());
941 }
942
943 Ref<Texture2D> folder_icon = (use_thumbnails) ? folder_thumbnail : get_theme_icon(SNAME("folder"), SNAME("FileDialog"));
944 const Color default_folder_color = get_theme_color(SNAME("folder_icon_color"), SNAME("FileDialog"));
945
946 // Build the FileInfo list.
947 List<FileInfo> file_list;
948 if (current_path == "Favorites") {
949 // Display the favorites.
950 Vector<String> favorites_list = EditorSettings::get_singleton()->get_favorites();
951 for (const String &favorite : favorites_list) {
952 String text;
953 Ref<Texture2D> icon;
954 if (favorite == "res://") {
955 text = "/";
956 icon = folder_icon;
957 if (searched_string.length() == 0 || text.to_lower().find(searched_string) >= 0) {
958 files->add_item(text, icon, true);
959 files->set_item_metadata(-1, favorite);
960 }
961 } else if (favorite.ends_with("/")) {
962 text = favorite.substr(0, favorite.length() - 1).get_file();
963 icon = folder_icon;
964 if (searched_string.length() == 0 || text.to_lower().find(searched_string) >= 0) {
965 files->add_item(text, icon, true);
966 files->set_item_metadata(-1, favorite);
967 }
968 } else {
969 int index;
970 EditorFileSystemDirectory *efd = EditorFileSystem::get_singleton()->find_file(favorite, &index);
971
972 FileInfo fi;
973 fi.name = favorite.get_file();
974 fi.path = favorite;
975 if (efd) {
976 fi.type = efd->get_file_type(index);
977 fi.import_broken = !efd->get_file_import_is_valid(index);
978 fi.modified_time = efd->get_file_modified_time(index);
979 } else {
980 fi.type = "";
981 fi.import_broken = true;
982 fi.modified_time = 0;
983 }
984
985 if (searched_string.length() == 0 || fi.name.to_lower().find(searched_string) >= 0) {
986 file_list.push_back(fi);
987 }
988 }
989 }
990 } else {
991 // Get infos on the directory + file.
992 if (directory.ends_with("/") && directory != "res://") {
993 directory = directory.substr(0, directory.length() - 1);
994 }
995 EditorFileSystemDirectory *efd = EditorFileSystem::get_singleton()->get_filesystem_path(directory);
996 if (!efd) {
997 directory = current_path.get_base_dir();
998 file = current_path.get_file();
999 efd = EditorFileSystem::get_singleton()->get_filesystem_path(directory);
1000 }
1001 if (!efd) {
1002 return;
1003 }
1004
1005 if (searched_string.length() > 0) {
1006 // Display the search results.
1007 // Limit the number of results displayed to avoid an infinite loop.
1008 _search(EditorFileSystem::get_singleton()->get_filesystem(), &file_list, 10000);
1009 } else {
1010 if (display_mode == DISPLAY_MODE_TREE_ONLY || always_show_folders) {
1011 // Check for a folder color to inherit (if one is assigned).
1012 Color inherited_folder_color = default_folder_color;
1013 String color_scan_dir = directory;
1014 while (color_scan_dir != "res://" && inherited_folder_color == default_folder_color) {
1015 if (!color_scan_dir.ends_with("/")) {
1016 color_scan_dir += "/";
1017 }
1018
1019 if (assigned_folder_colors.has(color_scan_dir)) {
1020 inherited_folder_color = folder_colors[assigned_folder_colors[color_scan_dir]];
1021 }
1022
1023 color_scan_dir = color_scan_dir.rstrip("/").get_base_dir();
1024 }
1025
1026 // Display folders in the list.
1027 if (directory != "res://") {
1028 files->add_item("..", folder_icon, true);
1029
1030 String bd = directory.get_base_dir();
1031 if (bd != "res://" && !bd.ends_with("/")) {
1032 bd += "/";
1033 }
1034
1035 files->set_item_metadata(-1, bd);
1036 files->set_item_selectable(-1, false);
1037 files->set_item_icon_modulate(-1, editor_is_dark_theme ? inherited_folder_color : inherited_folder_color * 1.75);
1038 }
1039
1040 bool reversed = file_sort == FILE_SORT_NAME_REVERSE;
1041 for (int i = reversed ? efd->get_subdir_count() - 1 : 0;
1042 reversed ? i >= 0 : i < efd->get_subdir_count();
1043 reversed ? i-- : i++) {
1044 String dname = efd->get_subdir(i)->get_name();
1045 String dpath = directory.path_join(dname) + "/";
1046 bool has_custom_color = assigned_folder_colors.has(dpath);
1047
1048 files->add_item(dname, folder_icon, true);
1049 files->set_item_metadata(-1, dpath);
1050 Color this_folder_color = has_custom_color ? folder_colors[assigned_folder_colors[dpath]] : inherited_folder_color;
1051 files->set_item_icon_modulate(-1, editor_is_dark_theme ? this_folder_color : this_folder_color * 1.75);
1052
1053 if (cselection.has(dname)) {
1054 files->select(files->get_item_count() - 1, false);
1055 }
1056 }
1057 }
1058
1059 // Display the folder content.
1060 for (int i = 0; i < efd->get_file_count(); i++) {
1061 FileInfo fi;
1062 fi.name = efd->get_file(i);
1063 fi.path = directory.path_join(fi.name);
1064 fi.type = efd->get_file_type(i);
1065 fi.import_broken = !efd->get_file_import_is_valid(i);
1066 fi.modified_time = efd->get_file_modified_time(i);
1067
1068 file_list.push_back(fi);
1069 }
1070 }
1071 }
1072
1073 // Sort the file list if needed.
1074 _sort_file_info_list(file_list);
1075
1076 // Fills the ItemList control node from the FileInfos.
1077 String main_scene = GLOBAL_GET("application/run/main_scene");
1078 for (FileInfo &E : file_list) {
1079 FileInfo *finfo = &(E);
1080 String fname = finfo->name;
1081 String fpath = finfo->path;
1082 String ftype = finfo->type;
1083
1084 Ref<Texture2D> type_icon;
1085 Ref<Texture2D> big_icon;
1086
1087 String tooltip = fpath;
1088
1089 // Select the icons.
1090 if (!finfo->import_broken) {
1091 type_icon = (has_theme_icon(ftype, EditorStringName(EditorIcons))) ? get_editor_theme_icon(ftype) : get_editor_theme_icon(SNAME("Object"));
1092 big_icon = file_thumbnail;
1093 } else {
1094 type_icon = get_editor_theme_icon(SNAME("ImportFail"));
1095 big_icon = file_thumbnail_broken;
1096 tooltip += "\n" + TTR("Status: Import of file failed. Please fix file and reimport manually.");
1097 }
1098
1099 // Add the item to the ItemList.
1100 int item_index;
1101 if (use_thumbnails) {
1102 files->add_item(fname, big_icon, true);
1103 item_index = files->get_item_count() - 1;
1104 files->set_item_metadata(item_index, fpath);
1105 files->set_item_tag_icon(item_index, type_icon);
1106
1107 } else {
1108 files->add_item(fname, type_icon, true);
1109 item_index = files->get_item_count() - 1;
1110 files->set_item_metadata(item_index, fpath);
1111 }
1112
1113 if (fpath == main_scene) {
1114 files->set_item_custom_fg_color(item_index, get_theme_color(SNAME("accent_color"), EditorStringName(Editor)));
1115 }
1116
1117 // Generate the preview.
1118 if (!finfo->import_broken) {
1119 Array udata;
1120 udata.resize(2);
1121 udata[0] = item_index;
1122 udata[1] = fname;
1123 EditorResourcePreview::get_singleton()->queue_resource_preview(fpath, this, "_file_list_thumbnail_done", udata);
1124 }
1125
1126 // Select the items.
1127 if (cselection.has(fname)) {
1128 files->select(item_index, false);
1129 }
1130
1131 if (!p_keep_selection && !file.is_empty() && fname == file) {
1132 files->select(item_index, true);
1133 files->ensure_current_is_visible();
1134 }
1135
1136 // Tooltip.
1137 if (finfo->sources.size()) {
1138 for (int j = 0; j < finfo->sources.size(); j++) {
1139 tooltip += "\nSource: " + finfo->sources[j];
1140 }
1141 }
1142 files->set_item_tooltip(item_index, tooltip);
1143 }
1144}
1145
1146void FileSystemDock::_select_file(const String &p_path, bool p_select_in_favorites) {
1147 String fpath = p_path;
1148 if (fpath.ends_with("/")) {
1149 if (fpath != "res://") {
1150 fpath = fpath.substr(0, fpath.length() - 1);
1151 }
1152 } else if (fpath != "Favorites") {
1153 if (FileAccess::exists(fpath + ".import")) {
1154 Ref<ConfigFile> config;
1155 config.instantiate();
1156 Error err = config->load(fpath + ".import");
1157 if (err == OK) {
1158 if (config->has_section_key("remap", "importer")) {
1159 String importer = config->get_value("remap", "importer");
1160 if (importer == "keep") {
1161 EditorNode::get_singleton()->show_warning(TTR("Importing has been disabled for this file, so it can't be opened for editing."));
1162 return;
1163 }
1164 }
1165 }
1166 }
1167
1168 String resource_type = ResourceLoader::get_resource_type(fpath);
1169
1170 if (resource_type == "PackedScene") {
1171 bool is_imported = false;
1172
1173 {
1174 List<String> importer_exts;
1175 ResourceImporterScene::get_scene_singleton()->get_recognized_extensions(&importer_exts);
1176 String extension = fpath.get_extension();
1177 for (const String &E : importer_exts) {
1178 if (extension.nocasecmp_to(E) == 0) {
1179 is_imported = true;
1180 break;
1181 }
1182 }
1183 }
1184
1185 if (is_imported) {
1186 ResourceImporterScene::get_scene_singleton()->show_advanced_options(fpath);
1187 } else {
1188 EditorNode::get_singleton()->open_request(fpath);
1189 }
1190 } else if (resource_type == "AnimationLibrary") {
1191 bool is_imported = false;
1192
1193 {
1194 List<String> importer_exts;
1195 ResourceImporterScene::get_animation_singleton()->get_recognized_extensions(&importer_exts);
1196 String extension = fpath.get_extension();
1197 for (const String &E : importer_exts) {
1198 if (extension.nocasecmp_to(E) == 0) {
1199 is_imported = true;
1200 break;
1201 }
1202 }
1203 }
1204
1205 if (is_imported) {
1206 ResourceImporterScene::get_animation_singleton()->show_advanced_options(fpath);
1207 } else {
1208 EditorNode::get_singleton()->open_request(fpath);
1209 }
1210 } else if (ResourceLoader::is_imported(fpath)) {
1211 // If the importer has advanced settings, show them.
1212 int order;
1213 bool can_threads;
1214 String name;
1215 Error err = ResourceFormatImporter::get_singleton()->get_import_order_threads_and_importer(fpath, order, can_threads, name);
1216 bool used_advanced_settings = false;
1217 if (err == OK) {
1218 Ref<ResourceImporter> importer = ResourceFormatImporter::get_singleton()->get_importer_by_name(name);
1219 if (importer.is_valid() && importer->has_advanced_options()) {
1220 importer->show_advanced_options(fpath);
1221 used_advanced_settings = true;
1222 }
1223 }
1224
1225 if (!used_advanced_settings) {
1226 EditorNode::get_singleton()->load_resource(fpath);
1227 }
1228
1229 } else {
1230 EditorNode::get_singleton()->load_resource(fpath);
1231 }
1232 }
1233 _navigate_to_path(fpath, p_select_in_favorites);
1234}
1235
1236void FileSystemDock::_tree_activate_file() {
1237 TreeItem *selected = tree->get_selected();
1238 if (selected) {
1239 String file_path = selected->get_metadata(0);
1240 TreeItem *parent = selected->get_parent();
1241 bool is_favorite = parent != nullptr && parent->get_metadata(0) == "Favorites";
1242
1243 if ((!is_favorite && file_path.ends_with("/")) || file_path == "Favorites") {
1244 bool collapsed = selected->is_collapsed();
1245 selected->set_collapsed(!collapsed);
1246 } else {
1247 _select_file(file_path, is_favorite && !file_path.ends_with("/"));
1248 }
1249 }
1250}
1251
1252void FileSystemDock::_file_list_activate_file(int p_idx) {
1253 _select_file(files->get_item_metadata(p_idx));
1254}
1255
1256void FileSystemDock::_preview_invalidated(const String &p_path) {
1257 if (file_list_display_mode == FILE_LIST_DISPLAY_THUMBNAILS && p_path.get_base_dir() == current_path && searched_string.length() == 0 && file_list_vb->is_visible_in_tree()) {
1258 for (int i = 0; i < files->get_item_count(); i++) {
1259 if (files->get_item_metadata(i) == p_path) {
1260 // Re-request preview.
1261 Array udata;
1262 udata.resize(2);
1263 udata[0] = i;
1264 udata[1] = files->get_item_text(i);
1265 EditorResourcePreview::get_singleton()->queue_resource_preview(p_path, this, "_file_list_thumbnail_done", udata);
1266 break;
1267 }
1268 }
1269 }
1270}
1271
1272void FileSystemDock::_fs_changed() {
1273 button_hist_prev->set_disabled(history_pos == 0);
1274 button_hist_next->set_disabled(history_pos == history.size() - 1);
1275 scanning_vb->hide();
1276 split_box->show();
1277
1278 if (tree->is_visible()) {
1279 _update_tree(get_uncollapsed_paths());
1280 }
1281
1282 if (file_list_vb->is_visible()) {
1283 _update_file_list(true);
1284 }
1285
1286 set_process(false);
1287}
1288
1289void FileSystemDock::_set_scanning_mode() {
1290 button_hist_prev->set_disabled(true);
1291 button_hist_next->set_disabled(true);
1292 split_box->hide();
1293 scanning_vb->show();
1294 set_process(true);
1295 if (EditorFileSystem::get_singleton()->is_scanning()) {
1296 scanning_progress->set_value(EditorFileSystem::get_singleton()->get_scanning_progress() * 100);
1297 } else {
1298 scanning_progress->set_value(0);
1299 }
1300}
1301
1302void FileSystemDock::_fw_history() {
1303 if (history_pos < history.size() - 1) {
1304 history_pos++;
1305 }
1306
1307 _update_history();
1308}
1309
1310void FileSystemDock::_bw_history() {
1311 if (history_pos > 0) {
1312 history_pos--;
1313 }
1314
1315 _update_history();
1316}
1317
1318void FileSystemDock::_update_history() {
1319 current_path = history[history_pos];
1320 _set_current_path_line_edit_text(current_path);
1321
1322 if (tree->is_visible()) {
1323 _update_tree(get_uncollapsed_paths());
1324 tree->grab_focus();
1325 tree->ensure_cursor_is_visible();
1326 }
1327
1328 if (file_list_vb->is_visible()) {
1329 _update_file_list(false);
1330 }
1331
1332 button_hist_prev->set_disabled(history_pos == 0);
1333 button_hist_next->set_disabled(history_pos == history.size() - 1);
1334}
1335
1336void FileSystemDock::_push_to_history() {
1337 if (history[history_pos] != current_path) {
1338 history.resize(history_pos + 1);
1339 history.push_back(current_path);
1340 history_pos++;
1341
1342 if (history.size() > history_max_size) {
1343 history.remove_at(0);
1344 history_pos = history_max_size - 1;
1345 }
1346 }
1347
1348 button_hist_prev->set_disabled(history_pos == 0);
1349 button_hist_next->set_disabled(history_pos == history.size() - 1);
1350}
1351
1352void FileSystemDock::_get_all_items_in_dir(EditorFileSystemDirectory *p_efsd, Vector<String> &r_files, Vector<String> &r_folders) const {
1353 if (p_efsd == nullptr) {
1354 return;
1355 }
1356
1357 for (int i = 0; i < p_efsd->get_subdir_count(); i++) {
1358 r_folders.push_back(p_efsd->get_subdir(i)->get_path());
1359 _get_all_items_in_dir(p_efsd->get_subdir(i), r_files, r_folders);
1360 }
1361 for (int i = 0; i < p_efsd->get_file_count(); i++) {
1362 r_files.push_back(p_efsd->get_file_path(i));
1363 }
1364}
1365
1366void FileSystemDock::_find_remaps(EditorFileSystemDirectory *p_efsd, const Vector<String> &r_renames, Vector<String> &r_to_remaps) const {
1367 for (int i = 0; i < p_efsd->get_subdir_count(); i++) {
1368 _find_remaps(p_efsd->get_subdir(i), r_renames, r_to_remaps);
1369 }
1370 for (int i = 0; i < p_efsd->get_file_count(); i++) {
1371 Vector<String> deps = p_efsd->get_file_deps(i);
1372 for (int j = 0; j < deps.size(); j++) {
1373 if (r_renames.has(deps[j])) {
1374 r_to_remaps.push_back(p_efsd->get_file_path(i));
1375 break;
1376 }
1377 }
1378 }
1379}
1380
1381void FileSystemDock::_try_move_item(const FileOrFolder &p_item, const String &p_new_path,
1382 HashMap<String, String> &p_file_renames, HashMap<String, String> &p_folder_renames) {
1383 // Ensure folder paths end with "/".
1384 String old_path = (p_item.is_file || p_item.path.ends_with("/")) ? p_item.path : (p_item.path + "/");
1385 String new_path = (p_item.is_file || p_new_path.ends_with("/")) ? p_new_path : (p_new_path + "/");
1386
1387 if (new_path == old_path) {
1388 return;
1389 } else if (old_path == "res://") {
1390 EditorNode::get_singleton()->add_io_error(TTR("Cannot move/rename resources root."));
1391 return;
1392 } else if (!p_item.is_file && new_path.begins_with(old_path)) {
1393 // This check doesn't erroneously catch renaming to a longer name as folder paths always end with "/".
1394 EditorNode::get_singleton()->add_io_error(TTR("Cannot move a folder into itself.") + "\n" + old_path + "\n");
1395 return;
1396 }
1397
1398 // Build a list of files which will have new paths as a result of this operation.
1399 Vector<String> file_changed_paths;
1400 Vector<String> folder_changed_paths;
1401 if (p_item.is_file) {
1402 file_changed_paths.push_back(old_path);
1403 } else {
1404 folder_changed_paths.push_back(old_path);
1405 _get_all_items_in_dir(EditorFileSystem::get_singleton()->get_filesystem_path(old_path), file_changed_paths, folder_changed_paths);
1406 }
1407
1408 Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);
1409 print_verbose("Moving " + old_path + " -> " + new_path);
1410 Error err = da->rename(old_path, new_path);
1411 if (err == OK) {
1412 // Move/Rename any corresponding import settings too.
1413 if (p_item.is_file && FileAccess::exists(old_path + ".import")) {
1414 err = da->rename(old_path + ".import", new_path + ".import");
1415 if (err != OK) {
1416 EditorNode::get_singleton()->add_io_error(TTR("Error moving:") + "\n" + old_path + ".import\n");
1417 }
1418 }
1419
1420 // Update scene if it is open.
1421 for (int i = 0; i < file_changed_paths.size(); ++i) {
1422 String new_item_path = p_item.is_file ? new_path : file_changed_paths[i].replace_first(old_path, new_path);
1423 if (ResourceLoader::get_resource_type(new_item_path) == "PackedScene" && EditorNode::get_singleton()->is_scene_open(file_changed_paths[i])) {
1424 EditorData *ed = &EditorNode::get_editor_data();
1425 for (int j = 0; j < ed->get_edited_scene_count(); j++) {
1426 if (ed->get_scene_path(j) == file_changed_paths[i]) {
1427 ed->get_edited_scene_root(j)->set_scene_file_path(new_item_path);
1428 EditorNode::get_singleton()->save_editor_layout_delayed();
1429 break;
1430 }
1431 }
1432 }
1433 }
1434
1435 // Only treat as a changed dependency if it was successfully moved.
1436 for (int i = 0; i < file_changed_paths.size(); ++i) {
1437 p_file_renames[file_changed_paths[i]] = file_changed_paths[i].replace_first(old_path, new_path);
1438 print_verbose(" Remap: " + file_changed_paths[i] + " -> " + p_file_renames[file_changed_paths[i]]);
1439 emit_signal(SNAME("files_moved"), file_changed_paths[i], p_file_renames[file_changed_paths[i]]);
1440 }
1441 for (int i = 0; i < folder_changed_paths.size(); ++i) {
1442 p_folder_renames[folder_changed_paths[i]] = folder_changed_paths[i].replace_first(old_path, new_path);
1443 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));
1444 }
1445 } else {
1446 EditorNode::get_singleton()->add_io_error(TTR("Error moving:") + "\n" + old_path + "\n");
1447 }
1448}
1449
1450void FileSystemDock::_try_duplicate_item(const FileOrFolder &p_item, const String &p_new_path) const {
1451 // Ensure folder paths end with "/".
1452 String old_path = (p_item.is_file || p_item.path.ends_with("/")) ? p_item.path : (p_item.path + "/");
1453 String new_path = (p_item.is_file || p_new_path.ends_with("/")) ? p_new_path : (p_new_path + "/");
1454
1455 if (new_path == old_path) {
1456 return;
1457 } else if (old_path == "res://") {
1458 EditorNode::get_singleton()->add_io_error(TTR("Cannot move/rename resources root."));
1459 return;
1460 } else if (!p_item.is_file && new_path.begins_with(old_path)) {
1461 // This check doesn't erroneously catch renaming to a longer name as folder paths always end with "/".
1462 EditorNode::get_singleton()->add_io_error(TTR("Cannot move a folder into itself.") + "\n" + old_path + "\n");
1463 return;
1464 }
1465 const_cast<FileSystemDock *>(this)->current_path = new_path;
1466
1467 Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);
1468
1469 if (p_item.is_file) {
1470 print_verbose("Duplicating " + old_path + " -> " + new_path);
1471
1472 // Create the directory structure.
1473 da->make_dir_recursive(new_path.get_base_dir());
1474
1475 if (FileAccess::exists(old_path + ".import")) {
1476 Error err = da->copy(old_path, new_path);
1477 if (err != OK) {
1478 EditorNode::get_singleton()->add_io_error(TTR("Error duplicating:") + "\n" + old_path + ": " + error_names[err] + "\n");
1479 return;
1480 }
1481
1482 // Remove uid from .import file to avoid conflict.
1483 Ref<ConfigFile> cfg;
1484 cfg.instantiate();
1485 cfg->load(old_path + ".import");
1486 cfg->erase_section_key("remap", "uid");
1487 err = cfg->save(new_path + ".import");
1488 if (err != OK) {
1489 EditorNode::get_singleton()->add_io_error(TTR("Error duplicating:") + "\n" + old_path + ".import: " + error_names[err] + "\n");
1490 return;
1491 }
1492 } else {
1493 // Files which do not use an uid can just be copied.
1494 if (ResourceLoader::get_resource_uid(old_path) == ResourceUID::INVALID_ID) {
1495 Error err = da->copy(old_path, new_path);
1496 if (err != OK) {
1497 EditorNode::get_singleton()->add_io_error(TTR("Error duplicating:") + "\n" + old_path + ": " + error_names[err] + "\n");
1498 }
1499 return;
1500 }
1501
1502 // Load the resource and save it again in the new location (this generates a new UID).
1503 Error err;
1504 Ref<Resource> res = ResourceLoader::load(old_path, "", ResourceFormatLoader::CACHE_MODE_REUSE, &err);
1505 if (err == OK && res.is_valid()) {
1506 err = ResourceSaver::save(res, new_path, ResourceSaver::FLAG_COMPRESS);
1507 if (err != OK) {
1508 EditorNode::get_singleton()->add_io_error(TTR("Error duplicating:") + " " + vformat(TTR("Failed to save resource at %s: %s"), new_path, error_names[err]));
1509 }
1510 } else if (err != OK) {
1511 // When loading files like text files the error is OK but the resource is still null.
1512 // We can ignore such files.
1513 EditorNode::get_singleton()->add_io_error(TTR("Error duplicating:") + " " + vformat(TTR("Failed to load resource at %s: %s"), new_path, error_names[err]));
1514 }
1515 }
1516 } else {
1517 // Recursively duplicate all files inside the folder.
1518 Ref<DirAccess> old_dir = DirAccess::open(old_path);
1519 Ref<FileAccess> file_access = FileAccess::create(FileAccess::ACCESS_RESOURCES);
1520 old_dir->set_include_navigational(false);
1521 old_dir->list_dir_begin();
1522 for (String f = old_dir->_get_next(); !f.is_empty(); f = old_dir->_get_next()) {
1523 if (f.get_extension() == "import") {
1524 continue;
1525 }
1526 if (file_access->file_exists(old_path + f)) {
1527 _try_duplicate_item(FileOrFolder(old_path + f, true), new_path + f);
1528 } else if (da->dir_exists(old_path + f)) {
1529 _try_duplicate_item(FileOrFolder(old_path + f, false), new_path + f);
1530 }
1531 }
1532 old_dir->list_dir_end();
1533 }
1534}
1535
1536void FileSystemDock::_update_resource_paths_after_move(const HashMap<String, String> &p_renames, const HashMap<String, ResourceUID::ID> &p_uids) const {
1537 // Update the paths in ResourceUID, so that UIDs remain valid.
1538 for (const KeyValue<String, ResourceUID::ID> &pair : p_uids) {
1539 ResourceUID::get_singleton()->set_id(pair.value, p_renames[pair.key]);
1540 }
1541
1542 // Rename all resources loaded, be it subresources or actual resources.
1543 List<Ref<Resource>> cached;
1544 ResourceCache::get_cached_resources(&cached);
1545
1546 for (Ref<Resource> &r : cached) {
1547 String base_path = r->get_path();
1548 String extra_path;
1549 int sep_pos = r->get_path().find("::");
1550 if (sep_pos >= 0) {
1551 extra_path = base_path.substr(sep_pos, base_path.length());
1552 base_path = base_path.substr(0, sep_pos);
1553 }
1554
1555 if (p_renames.has(base_path)) {
1556 base_path = p_renames[base_path];
1557 }
1558
1559 r->set_path(base_path + extra_path);
1560 }
1561}
1562
1563void FileSystemDock::_update_dependencies_after_move(const HashMap<String, String> &p_renames, const Vector<String> &p_remaps) const {
1564 // The following code assumes that the following holds:
1565 // 1) EditorFileSystem contains the old paths/folder structure from before the rename/move.
1566 // 2) ResourceLoader can use the new paths without needing to call rescan.
1567 List<String> scenes_to_reload;
1568 for (int i = 0; i < p_remaps.size(); ++i) {
1569 // Because we haven't called a rescan yet the found remap might still be an old path itself.
1570 String file = p_renames.has(p_remaps[i]) ? p_renames[p_remaps[i]] : p_remaps[i];
1571 print_verbose("Remapping dependencies for: " + file);
1572 Error err = ResourceLoader::rename_dependencies(file, p_renames);
1573 if (err == OK) {
1574 if (ResourceLoader::get_resource_type(file) == "PackedScene") {
1575 scenes_to_reload.push_back(file);
1576 }
1577 } else {
1578 EditorNode::get_singleton()->add_io_error(TTR("Unable to update dependencies:") + "\n" + p_remaps[i] + "\n");
1579 }
1580 }
1581
1582 for (const String &E : scenes_to_reload) {
1583 EditorNode::get_singleton()->reload_scene(E);
1584 }
1585}
1586
1587void FileSystemDock::_update_project_settings_after_move(const HashMap<String, String> &p_renames, const HashMap<String, String> &p_folders_renames) {
1588 // Find all project settings of type FILE and replace them if needed.
1589 const HashMap<StringName, PropertyInfo> prop_info = ProjectSettings::get_singleton()->get_custom_property_info();
1590 for (const KeyValue<StringName, PropertyInfo> &E : prop_info) {
1591 if (E.value.hint == PROPERTY_HINT_FILE) {
1592 String old_path = GLOBAL_GET(E.key);
1593 if (p_renames.has(old_path)) {
1594 ProjectSettings::get_singleton()->set_setting(E.key, p_renames[old_path]);
1595 }
1596 };
1597 }
1598
1599 // Also search for the file in autoload, as they are stored differently from normal files.
1600 List<PropertyInfo> property_list;
1601 ProjectSettings::get_singleton()->get_property_list(&property_list);
1602 for (const PropertyInfo &E : property_list) {
1603 if (E.name.begins_with("autoload/")) {
1604 // If the autoload resource paths has a leading "*", it indicates that it is a Singleton,
1605 // so we have to handle both cases when updating.
1606 String autoload = GLOBAL_GET(E.name);
1607 String autoload_singleton = autoload.substr(1, autoload.length());
1608 if (p_renames.has(autoload)) {
1609 ProjectSettings::get_singleton()->set_setting(E.name, p_renames[autoload]);
1610 } else if (autoload.begins_with("*") && p_renames.has(autoload_singleton)) {
1611 ProjectSettings::get_singleton()->set_setting(E.name, "*" + p_renames[autoload_singleton]);
1612 }
1613 }
1614 }
1615
1616 // Update folder colors.
1617 for (const KeyValue<String, String> &rename : p_folders_renames) {
1618 if (assigned_folder_colors.has(rename.key)) {
1619 assigned_folder_colors[rename.value] = assigned_folder_colors[rename.key];
1620 assigned_folder_colors.erase(rename.key);
1621 }
1622 }
1623 ProjectSettings::get_singleton()->save();
1624}
1625
1626String FileSystemDock::_get_unique_name(const FileOrFolder &p_entry, const String &p_at_path) {
1627 String new_path;
1628 String new_path_base;
1629
1630 if (p_entry.is_file) {
1631 new_path = p_at_path.path_join(p_entry.path.get_file());
1632 new_path_base = new_path.get_basename() + " (%d)." + new_path.get_extension();
1633 } else {
1634 PackedStringArray path_split = p_entry.path.split("/");
1635 new_path = p_at_path.path_join(path_split[path_split.size() - 2]);
1636 new_path_base = new_path + " (%d)";
1637 }
1638
1639 int exist_counter = 1;
1640 Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);
1641 while (da->file_exists(new_path) || da->dir_exists(new_path)) {
1642 exist_counter++;
1643 new_path = vformat(new_path_base, exist_counter);
1644 }
1645
1646 return new_path;
1647}
1648
1649void FileSystemDock::_update_favorites_list_after_move(const HashMap<String, String> &p_files_renames, const HashMap<String, String> &p_folders_renames) const {
1650 Vector<String> favorites_list = EditorSettings::get_singleton()->get_favorites();
1651 Vector<String> new_favorites;
1652
1653 for (const String &old_path : favorites_list) {
1654 if (p_folders_renames.has(old_path)) {
1655 new_favorites.push_back(p_folders_renames[old_path]);
1656 } else if (p_files_renames.has(old_path)) {
1657 new_favorites.push_back(p_files_renames[old_path]);
1658 } else {
1659 new_favorites.push_back(old_path);
1660 }
1661 }
1662
1663 EditorSettings::get_singleton()->set_favorites(new_favorites);
1664}
1665
1666void FileSystemDock::_make_scene_confirm() {
1667 const String scene_path = make_scene_dialog->get_scene_path();
1668
1669 int idx = EditorNode::get_singleton()->new_scene();
1670 EditorNode::get_editor_data().set_scene_path(idx, scene_path);
1671 EditorNode::get_singleton()->set_edited_scene(make_scene_dialog->create_scene_root());
1672 EditorNode::get_singleton()->save_scene_list({ scene_path });
1673}
1674
1675void FileSystemDock::_resource_removed(const Ref<Resource> &p_resource) {
1676 emit_signal(SNAME("resource_removed"), p_resource);
1677}
1678
1679void FileSystemDock::_file_removed(String p_file) {
1680 emit_signal(SNAME("file_removed"), p_file);
1681
1682 // Find the closest parent directory available, in case multiple items were deleted along the same path.
1683 current_path = p_file.get_base_dir();
1684 Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);
1685 while (!da->dir_exists(current_path)) {
1686 current_path = current_path.get_base_dir();
1687 }
1688
1689 current_path_line_edit->set_text(current_path);
1690}
1691
1692void FileSystemDock::_folder_removed(String p_folder) {
1693 emit_signal(SNAME("folder_removed"), p_folder);
1694
1695 // Find the closest parent directory available, in case multiple items were deleted along the same path.
1696 current_path = p_folder.get_base_dir();
1697 Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);
1698 while (!da->dir_exists(current_path)) {
1699 current_path = current_path.get_base_dir();
1700 }
1701
1702 if (assigned_folder_colors.has(p_folder)) {
1703 assigned_folder_colors.erase(p_folder);
1704 _update_folder_colors_setting();
1705 }
1706
1707 current_path_line_edit->set_text(current_path);
1708 EditorFileSystemDirectory *efd = EditorFileSystem::get_singleton()->get_filesystem_path(current_path);
1709 if (efd) {
1710 efd->force_update();
1711 }
1712}
1713
1714void FileSystemDock::_rename_operation_confirm() {
1715 String new_name;
1716 TreeItem *s = tree->get_selected();
1717 int col_index = tree->get_selected_column();
1718
1719 if (tree->has_focus()) {
1720 new_name = s->get_text(col_index).strip_edges();
1721 } else if (files->has_focus()) {
1722 new_name = files->get_edit_text().strip_edges();
1723 }
1724 String old_name = to_rename.is_file ? to_rename.path.get_file() : to_rename.path.left(-1).get_file();
1725
1726 bool rename_error = false;
1727 if (new_name.length() == 0) {
1728 EditorNode::get_singleton()->show_warning(TTR("No name provided."));
1729 rename_error = true;
1730 } else if (new_name.contains("/") || new_name.contains("\\") || new_name.contains(":")) {
1731 EditorNode::get_singleton()->show_warning(TTR("Name contains invalid characters."));
1732 rename_error = true;
1733 } else if (new_name[0] == '.') {
1734 EditorNode::get_singleton()->show_warning(TTR("This filename begins with a dot rendering the file invisible to the editor.\nIf you want to rename it anyway, use your operating system's file manager."));
1735 rename_error = true;
1736 } else if (to_rename.is_file && to_rename.path.get_extension() != new_name.get_extension()) {
1737 if (!EditorFileSystem::get_singleton()->get_valid_extensions().find(new_name.get_extension())) {
1738 EditorNode::get_singleton()->show_warning(TTR("This file extension is not recognized by the editor.\nIf you want to rename it anyway, use your operating system's file manager.\nAfter renaming to an unknown extension, the file won't be shown in the editor anymore."));
1739 rename_error = true;
1740 }
1741 }
1742
1743 // Restore original name.
1744 if (rename_error && tree->has_focus()) {
1745 s->set_text(col_index, old_name);
1746 return;
1747 } else if (rename_error && files->has_focus()) {
1748 return;
1749 }
1750
1751 String old_path = to_rename.path.ends_with("/") ? to_rename.path.left(-1) : to_rename.path;
1752 String new_path = old_path.get_base_dir().path_join(new_name);
1753 if (old_path == new_path) {
1754 return;
1755 }
1756
1757 if (EditorFileSystem::get_singleton()->is_group_file(old_path)) {
1758 EditorFileSystem::get_singleton()->move_group_file(old_path, new_path);
1759 }
1760
1761 // Present a more user friendly warning for name conflict.
1762 Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);
1763#if defined(WINDOWS_ENABLED)
1764 // Workaround case insensitivity on Windows.
1765 if ((da->file_exists(new_path) || da->dir_exists(new_path)) && new_path.to_lower() != old_path.to_lower()) {
1766#else
1767 if (da->file_exists(new_path) || da->dir_exists(new_path)) {
1768#endif
1769 EditorNode::get_singleton()->show_warning(TTR("A file or folder with this name already exists."));
1770 s->set_text(col_index, old_name);
1771 return;
1772 }
1773
1774 Vector<String> old_paths;
1775 HashMap<String, ResourceUID::ID> uids;
1776 Vector<String> remaps;
1777 _before_move(old_paths, uids, remaps);
1778
1779 HashMap<String, String> file_renames;
1780 HashMap<String, String> folder_renames;
1781 _try_move_item(to_rename, new_path, file_renames, folder_renames);
1782
1783 int current_tab = EditorSceneTabs::get_singleton()->get_current_tab();
1784 _update_resource_paths_after_move(file_renames, uids);
1785 _update_dependencies_after_move(file_renames, remaps);
1786 _update_project_settings_after_move(file_renames, folder_renames);
1787 _update_favorites_list_after_move(file_renames, folder_renames);
1788
1789 EditorSceneTabs::get_singleton()->set_current_tab(current_tab);
1790
1791 print_verbose("FileSystem: calling rescan.");
1792 _rescan();
1793
1794 current_path = new_path;
1795 current_path_line_edit->set_text(current_path);
1796}
1797
1798void FileSystemDock::_duplicate_operation_confirm() {
1799 String new_name = duplicate_dialog_text->get_text().strip_edges();
1800 if (new_name.length() == 0) {
1801 EditorNode::get_singleton()->show_warning(TTR("No name provided."));
1802 return;
1803 } else if (new_name.contains("/") || new_name.contains("\\") || new_name.contains(":")) {
1804 EditorNode::get_singleton()->show_warning(TTR("Name contains invalid characters."));
1805 return;
1806 } else if (new_name[0] == '.') {
1807 EditorNode::get_singleton()->show_warning(TTR("Name begins with a dot."));
1808 return;
1809 }
1810
1811 String base_dir = to_duplicate.path.get_base_dir();
1812 // get_base_dir() returns "some/path" if the original path was "some/path/", so work it around.
1813 if (to_duplicate.path.ends_with("/")) {
1814 base_dir = base_dir.get_base_dir();
1815 }
1816
1817 String new_path = base_dir.path_join(new_name);
1818
1819 // Present a more user friendly warning for name conflict
1820 Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);
1821 if (da->file_exists(new_path) || da->dir_exists(new_path)) {
1822 EditorNode::get_singleton()->show_warning(TTR("A file or folder with this name already exists."));
1823 return;
1824 }
1825
1826 _try_duplicate_item(to_duplicate, new_path);
1827
1828 // Rescan everything.
1829 print_verbose("FileSystem: calling rescan.");
1830 _rescan();
1831}
1832
1833void FileSystemDock::_overwrite_dialog_action(bool p_overwrite) {
1834 overwrite_dialog->hide();
1835 _move_operation_confirm(to_move_path, to_move_or_copy, p_overwrite ? OVERWRITE_REPLACE : OVERWRITE_RENAME);
1836}
1837
1838Vector<String> FileSystemDock::_check_existing() {
1839 Vector<String> conflicting_items;
1840 for (const FileOrFolder &item : to_move) {
1841 String old_path = item.path.trim_suffix("/");
1842 String new_path = to_move_path.path_join(old_path.get_file());
1843
1844 if ((item.is_file && FileAccess::exists(new_path)) || (!item.is_file && DirAccess::exists(new_path))) {
1845 conflicting_items.push_back(old_path);
1846 }
1847 }
1848 return conflicting_items;
1849}
1850
1851void FileSystemDock::_move_dialog_confirm(const String &p_path) {
1852 _move_operation_confirm(p_path, move_dialog->is_copy_pressed());
1853}
1854
1855void FileSystemDock::_move_operation_confirm(const String &p_to_path, bool p_copy, Overwrite p_overwrite) {
1856 if (p_overwrite == OVERWRITE_UNDECIDED) {
1857 to_move_path = p_to_path;
1858 to_move_or_copy = p_copy;
1859
1860 Vector<String> conflicting_items = _check_existing();
1861 if (!conflicting_items.is_empty()) {
1862 // Ask to do something.
1863 overwrite_dialog_header->set_text(vformat(
1864 TTR("The following files or folders conflict with items in the target location '%s':"), to_move_path));
1865 overwrite_dialog_file_list->set_text(String("\n").join(conflicting_items));
1866 overwrite_dialog_footer->set_text(
1867 p_copy ? TTR("Do you wish to overwrite them or rename the copied files?")
1868 : TTR("Do you wish to overwrite them or rename the moved files?"));
1869 overwrite_dialog->popup_centered();
1870 return;
1871 }
1872 }
1873
1874 Vector<String> new_paths;
1875 new_paths.resize(to_move.size());
1876 for (int i = 0; i < to_move.size(); i++) {
1877 if (p_overwrite == OVERWRITE_RENAME) {
1878 new_paths.write[i] = _get_unique_name(to_move[i], p_to_path);
1879 } else {
1880 new_paths.write[i] = p_to_path.path_join(to_move[i].path.get_file());
1881 }
1882 }
1883
1884 if (p_copy) {
1885 bool is_copied = false;
1886 for (int i = 0; i < to_move.size(); i++) {
1887 String old_path = to_move[i].path;
1888 String new_path = new_paths[i];
1889
1890 if (!to_move[i].is_file) {
1891 new_path = new_path.path_join(old_path.trim_suffix("/").get_file());
1892 }
1893
1894 if (old_path != new_path) {
1895 _try_duplicate_item(to_move[i], new_path);
1896 is_copied = true;
1897 }
1898 }
1899
1900 if (is_copied) {
1901 _rescan();
1902 }
1903 } else {
1904 // Check groups.
1905 for (int i = 0; i < to_move.size(); i++) {
1906 if (to_move[i].is_file && EditorFileSystem::get_singleton()->is_group_file(to_move[i].path)) {
1907 EditorFileSystem::get_singleton()->move_group_file(to_move[i].path, new_paths[i]);
1908 }
1909 }
1910
1911 Vector<String> old_paths;
1912 HashMap<String, ResourceUID::ID> uids;
1913 Vector<String> remaps;
1914 _before_move(old_paths, uids, remaps);
1915
1916 bool is_moved = false;
1917 HashMap<String, String> file_renames;
1918 HashMap<String, String> folder_renames;
1919
1920 for (int i = 0; i < to_move.size(); i++) {
1921 String old_path = to_move[i].path;
1922 String new_path = new_paths[i];
1923
1924 if (!to_move[i].is_file) {
1925 new_path = new_path.path_join(old_path.trim_suffix("/").get_file());
1926 }
1927
1928 if (old_path != new_path) {
1929 _try_move_item(to_move[i], new_path, file_renames, folder_renames);
1930 is_moved = true;
1931 }
1932 }
1933
1934 if (is_moved) {
1935 int current_tab = EditorSceneTabs::get_singleton()->get_current_tab();
1936 _update_resource_paths_after_move(file_renames, uids);
1937 _update_dependencies_after_move(file_renames, remaps);
1938 _update_project_settings_after_move(file_renames, folder_renames);
1939 _update_favorites_list_after_move(file_renames, folder_renames);
1940
1941 EditorSceneTabs::get_singleton()->set_current_tab(current_tab);
1942
1943 print_verbose("FileSystem: calling rescan.");
1944 _rescan();
1945
1946 current_path = p_to_path;
1947 current_path_line_edit->set_text(current_path);
1948 }
1949 }
1950}
1951
1952void FileSystemDock::_before_move(Vector<String> &r_old_paths, HashMap<String, ResourceUID::ID> &r_uids, Vector<String> &r_remaps) const {
1953 for (int i = 0; i < to_move.size(); i++) {
1954 r_old_paths.push_back(to_move[i].path);
1955 ResourceUID::ID uid = ResourceLoader::get_resource_uid(to_move[i].path);
1956 if (uid != ResourceUID::INVALID_ID) {
1957 r_uids[to_move[i].path] = uid;
1958 }
1959 }
1960
1961 _find_remaps(EditorFileSystem::get_singleton()->get_filesystem(), r_old_paths, r_remaps);
1962
1963 // Open scenes with dependencies on the ones about to be moved will be reloaded,
1964 // so save them first to prevent losing unsaved changes.
1965 EditorNode::get_singleton()->save_scene_list(r_remaps);
1966}
1967
1968Vector<String> FileSystemDock::_tree_get_selected(bool remove_self_inclusion) const {
1969 // Build a list of selected items with the active one at the first position.
1970 Vector<String> selected_strings;
1971
1972 TreeItem *favorites_item = tree->get_root()->get_first_child();
1973 TreeItem *active_selected = tree->get_selected();
1974 if (active_selected && active_selected != favorites_item) {
1975 selected_strings.push_back(active_selected->get_metadata(0));
1976 }
1977
1978 TreeItem *selected = tree->get_root();
1979 selected = tree->get_next_selected(selected);
1980 while (selected) {
1981 if (selected != active_selected && selected != favorites_item) {
1982 selected_strings.push_back(selected->get_metadata(0));
1983 }
1984 selected = tree->get_next_selected(selected);
1985 }
1986
1987 if (remove_self_inclusion) {
1988 selected_strings = _remove_self_included_paths(selected_strings);
1989 }
1990 return selected_strings;
1991}
1992
1993Vector<String> FileSystemDock::_remove_self_included_paths(Vector<String> selected_strings) {
1994 // Remove paths or files that are included into another.
1995 if (selected_strings.size() > 1) {
1996 selected_strings.sort_custom<NaturalNoCaseComparator>();
1997 String last_path = "";
1998 for (int i = 0; i < selected_strings.size(); i++) {
1999 if (!last_path.is_empty() && selected_strings[i].begins_with(last_path)) {
2000 selected_strings.remove_at(i);
2001 i--;
2002 }
2003 if (selected_strings[i].ends_with("/")) {
2004 last_path = selected_strings[i];
2005 }
2006 }
2007 }
2008 return selected_strings;
2009}
2010
2011void FileSystemDock::_tree_rmb_option(int p_option) {
2012 Vector<String> selected_strings = _tree_get_selected(false);
2013
2014 // Execute the current option.
2015 switch (p_option) {
2016 case FOLDER_EXPAND_ALL:
2017 case FOLDER_COLLAPSE_ALL: {
2018 // Expand or collapse the folder
2019 if (selected_strings.size() == 1) {
2020 tree->get_selected()->set_collapsed_recursive(p_option == FOLDER_COLLAPSE_ALL);
2021 }
2022 } break;
2023 default: {
2024 _file_option(p_option, selected_strings);
2025 } break;
2026 }
2027}
2028
2029void FileSystemDock::_file_list_rmb_option(int p_option) {
2030 Vector<int> selected_id = files->get_selected_items();
2031 Vector<String> selected;
2032 for (int i = 0; i < selected_id.size(); i++) {
2033 selected.push_back(files->get_item_metadata(selected_id[i]));
2034 }
2035 _file_option(p_option, selected);
2036}
2037
2038void FileSystemDock::_file_option(int p_option, const Vector<String> &p_selected) {
2039 // The first one should be the active item.
2040
2041 switch (p_option) {
2042 case FILE_SHOW_IN_EXPLORER: {
2043 // Show the file/folder in the OS explorer.
2044 String fpath = current_path;
2045 if (current_path == "Favorites") {
2046 fpath = p_selected[0];
2047 }
2048
2049 String dir = ProjectSettings::get_singleton()->globalize_path(fpath);
2050 OS::get_singleton()->shell_show_in_file_manager(dir, true);
2051 } break;
2052
2053 case FILE_OPEN_EXTERNAL: {
2054 String fpath = current_path;
2055 if (current_path == "Favorites") {
2056 fpath = p_selected[0];
2057 }
2058
2059 String file = ProjectSettings::get_singleton()->globalize_path(fpath);
2060
2061 String resource_type = ResourceLoader::get_resource_type(fpath);
2062 String external_program;
2063
2064 if (resource_type == "CompressedTexture2D" || resource_type == "Image") {
2065 if (file.get_extension() == "svg" || file.get_extension() == "svgz") {
2066 external_program = EDITOR_GET("filesystem/external_programs/vector_image_editor");
2067 } else {
2068 external_program = EDITOR_GET("filesystem/external_programs/raster_image_editor");
2069 }
2070 } else if (ClassDB::is_parent_class(resource_type, "AudioStream")) {
2071 external_program = EDITOR_GET("filesystem/external_programs/audio_editor");
2072 } else if (resource_type == "PackedScene") {
2073 // Ignore non-model scenes.
2074 if (file.get_extension() != "tscn" && file.get_extension() != "scn" && file.get_extension() != "res") {
2075 external_program = EDITOR_GET("filesystem/external_programs/3d_model_editor");
2076 }
2077 } else if (ClassDB::is_parent_class(resource_type, "Script")) {
2078 external_program = EDITOR_GET("text_editor/external/exec_path");
2079 }
2080
2081 if (external_program.is_empty()) {
2082 OS::get_singleton()->shell_open(file);
2083 } else {
2084 List<String> args;
2085 args.push_back(file);
2086 OS::get_singleton()->create_process(external_program, args);
2087 }
2088 } break;
2089
2090 case FILE_OPEN: {
2091 // Open folders.
2092 TreeItem *selected = tree->get_root();
2093 selected = tree->get_next_selected(selected);
2094 while (selected) {
2095 if (p_selected.find(selected->get_metadata(0)) >= 0) {
2096 selected->set_collapsed(false);
2097 }
2098 selected = tree->get_next_selected(selected);
2099 }
2100 // Open the file.
2101 for (int i = 0; i < p_selected.size(); i++) {
2102 _select_file(p_selected[i]);
2103 }
2104 } break;
2105
2106 case FILE_INHERIT: {
2107 // Create a new scene inherited from the selected one.
2108 if (p_selected.size() == 1) {
2109 emit_signal(SNAME("inherit"), p_selected[0]);
2110 }
2111 } break;
2112
2113 case FILE_MAIN_SCENE: {
2114 // Set as main scene with selected scene file.
2115 if (p_selected.size() == 1) {
2116 ProjectSettings::get_singleton()->set("application/run/main_scene", p_selected[0]);
2117 ProjectSettings::get_singleton()->save();
2118 _update_tree(get_uncollapsed_paths());
2119 _update_file_list(true);
2120 }
2121 } break;
2122
2123 case FILE_INSTANTIATE: {
2124 // Instantiate all selected scenes.
2125 Vector<String> paths;
2126 for (int i = 0; i < p_selected.size(); i++) {
2127 String fpath = p_selected[i];
2128 if (EditorFileSystem::get_singleton()->get_file_type(fpath) == "PackedScene") {
2129 paths.push_back(fpath);
2130 }
2131 }
2132 if (!paths.is_empty()) {
2133 emit_signal(SNAME("instantiate"), paths);
2134 }
2135 } break;
2136
2137 case FILE_ADD_FAVORITE: {
2138 // Add the files from favorites.
2139 Vector<String> favorites_list = EditorSettings::get_singleton()->get_favorites();
2140 for (int i = 0; i < p_selected.size(); i++) {
2141 if (!favorites_list.has(p_selected[i])) {
2142 favorites_list.push_back(p_selected[i]);
2143 }
2144 }
2145 EditorSettings::get_singleton()->set_favorites(favorites_list);
2146 _update_tree(get_uncollapsed_paths());
2147 } break;
2148
2149 case FILE_REMOVE_FAVORITE: {
2150 // Remove the files from favorites.
2151 Vector<String> favorites_list = EditorSettings::get_singleton()->get_favorites();
2152 for (int i = 0; i < p_selected.size(); i++) {
2153 favorites_list.erase(p_selected[i]);
2154 }
2155 EditorSettings::get_singleton()->set_favorites(favorites_list);
2156 _update_tree(get_uncollapsed_paths());
2157 if (current_path == "Favorites") {
2158 _update_file_list(true);
2159 }
2160 } break;
2161
2162 case FILE_DEPENDENCIES: {
2163 // Checkout the file dependencies.
2164 if (!p_selected.is_empty()) {
2165 String fpath = p_selected[0];
2166 deps_editor->edit(fpath);
2167 }
2168 } break;
2169
2170 case FILE_OWNERS: {
2171 // Checkout the file owners.
2172 if (!p_selected.is_empty()) {
2173 String fpath = p_selected[0];
2174 owners_editor->show(fpath);
2175 }
2176 } break;
2177
2178 case FILE_MOVE: {
2179 // Move or copy the files to a given location.
2180 to_move.clear();
2181 Vector<String> collapsed_paths = _remove_self_included_paths(p_selected);
2182 for (int i = collapsed_paths.size() - 1; i >= 0; i--) {
2183 String fpath = collapsed_paths[i];
2184 if (fpath != "res://") {
2185 to_move.push_back(FileOrFolder(fpath, !fpath.ends_with("/")));
2186 }
2187 }
2188 if (to_move.size() > 0) {
2189 move_dialog->popup_centered_ratio(0.4);
2190 }
2191 } break;
2192
2193 case FILE_RENAME: {
2194 if (!p_selected.is_empty()) {
2195 // Set to_rename variable for callback execution.
2196 to_rename.path = p_selected[0];
2197 to_rename.is_file = !to_rename.path.ends_with("/");
2198 if (to_rename.path == "res://") {
2199 break;
2200 }
2201
2202 if (tree->has_focus()) {
2203 // Edit node in Tree.
2204 tree->edit_selected(true);
2205
2206 if (to_rename.is_file) {
2207 String name = to_rename.path.get_file();
2208 tree->set_editor_selection(0, name.rfind("."));
2209 } else {
2210 String name = to_rename.path.left(-1).get_file(); // Removes the "/" suffix for folders.
2211 tree->set_editor_selection(0, name.length());
2212 }
2213 } else if (files->has_focus()) {
2214 files->edit_selected();
2215 }
2216 }
2217 } break;
2218
2219 case FILE_REMOVE: {
2220 // Remove the selected files.
2221 Vector<String> remove_files;
2222 Vector<String> remove_folders;
2223 Vector<String> collapsed_paths = _remove_self_included_paths(p_selected);
2224
2225 for (int i = 0; i < collapsed_paths.size(); i++) {
2226 String fpath = collapsed_paths[i];
2227 if (fpath != "res://") {
2228 if (fpath.ends_with("/")) {
2229 remove_folders.push_back(fpath);
2230 } else {
2231 remove_files.push_back(fpath);
2232 }
2233 }
2234 }
2235
2236 if (remove_files.size() + remove_folders.size() > 0) {
2237 remove_dialog->show(remove_folders, remove_files);
2238 }
2239 } break;
2240
2241 case FILE_DUPLICATE: {
2242 // Duplicate the selected files.
2243 for (int i = 0; i < p_selected.size(); i++) {
2244 to_duplicate.path = p_selected[i];
2245 to_duplicate.is_file = !to_duplicate.path.ends_with("/");
2246 if (to_duplicate.is_file) {
2247 String name = to_duplicate.path.get_file();
2248 duplicate_dialog->set_title(TTR("Duplicating file:") + " " + name);
2249 duplicate_dialog_text->set_text(name);
2250 duplicate_dialog_text->select(0, name.rfind("."));
2251 } else {
2252 String name = to_duplicate.path.substr(0, to_duplicate.path.length() - 1).get_file();
2253 duplicate_dialog->set_title(TTR("Duplicating folder:") + " " + name);
2254 duplicate_dialog_text->set_text(name);
2255 duplicate_dialog_text->select(0, name.length());
2256 }
2257 duplicate_dialog->popup_centered(Size2(250, 80) * EDSCALE);
2258 duplicate_dialog_text->grab_focus();
2259 }
2260 } break;
2261
2262 case FILE_INFO: {
2263 } break;
2264
2265 case FILE_REIMPORT: {
2266 ImportDock::get_singleton()->reimport_resources(p_selected);
2267 } break;
2268
2269 case FILE_NEW_FOLDER: {
2270 String directory = current_path;
2271 if (!directory.ends_with("/")) {
2272 directory = directory.get_base_dir();
2273 }
2274 make_dir_dialog->config(directory);
2275 make_dir_dialog->popup_centered();
2276 } break;
2277
2278 case FILE_NEW_SCENE: {
2279 String directory = current_path;
2280 if (!directory.ends_with("/")) {
2281 directory = directory.get_base_dir();
2282 }
2283 make_scene_dialog->config(directory);
2284 make_scene_dialog->popup_centered();
2285 } break;
2286
2287 case FILE_NEW_SCRIPT: {
2288 String fpath = current_path;
2289 if (!fpath.ends_with("/")) {
2290 fpath = fpath.get_base_dir();
2291 }
2292 make_script_dialog->config("Node", fpath.path_join("new_script.gd"), false, false);
2293 make_script_dialog->popup_centered();
2294 } break;
2295
2296 case FILE_COPY_PATH: {
2297 if (!p_selected.is_empty()) {
2298 String fpath = p_selected[0];
2299 DisplayServer::get_singleton()->clipboard_set(fpath);
2300 }
2301 } break;
2302
2303 case FILE_COPY_UID: {
2304 if (!p_selected.is_empty()) {
2305 ResourceUID::ID uid = ResourceLoader::get_resource_uid(p_selected[0]);
2306 if (uid != ResourceUID::INVALID_ID) {
2307 String uid_string = ResourceUID::get_singleton()->id_to_text(uid);
2308 DisplayServer::get_singleton()->clipboard_set(uid_string);
2309 }
2310 }
2311 } break;
2312
2313 case FILE_NEW_RESOURCE: {
2314 new_resource_dialog->popup_create(true);
2315 } break;
2316 case FILE_NEW_TEXTFILE: {
2317 String fpath = current_path;
2318 if (!fpath.ends_with("/")) {
2319 fpath = fpath.get_base_dir();
2320 }
2321 String dir = ProjectSettings::get_singleton()->globalize_path(fpath);
2322 ScriptEditor::get_singleton()->open_text_file_create_dialog(dir);
2323 } break;
2324 }
2325}
2326
2327void FileSystemDock::_resource_created() {
2328 String fpath = current_path;
2329 if (!fpath.ends_with("/")) {
2330 fpath = fpath.get_base_dir();
2331 }
2332
2333 String type_name = new_resource_dialog->get_selected_type();
2334 if (type_name == "Shader") {
2335 make_shader_dialog->config(fpath.path_join("new_shader"), false, false, 0);
2336 make_shader_dialog->popup_centered();
2337 return;
2338 } else if (type_name == "VisualShader") {
2339 make_shader_dialog->config(fpath.path_join("new_shader"), false, false, 1);
2340 make_shader_dialog->popup_centered();
2341 return;
2342 } else if (type_name == "ShaderInclude") {
2343 make_shader_dialog->config(fpath.path_join("new_shader_include"), false, false, 2);
2344 make_shader_dialog->popup_centered();
2345 return;
2346 }
2347
2348 Variant c = new_resource_dialog->instantiate_selected();
2349
2350 ERR_FAIL_COND(!c);
2351 Resource *r = Object::cast_to<Resource>(c);
2352 ERR_FAIL_NULL(r);
2353
2354 PackedScene *scene = Object::cast_to<PackedScene>(r);
2355 if (scene) {
2356 Node *node = memnew(Node);
2357 node->set_name("Node");
2358 scene->pack(node);
2359 memdelete(node);
2360 }
2361
2362 EditorNode::get_singleton()->push_item(r);
2363 EditorNode::get_singleton()->save_resource_as(Ref<Resource>(r), fpath);
2364}
2365
2366void FileSystemDock::_search_changed(const String &p_text, const Control *p_from) {
2367 if (searched_string.length() == 0) {
2368 // Register the uncollapsed paths before they change.
2369 uncollapsed_paths_before_search = get_uncollapsed_paths();
2370 }
2371
2372 searched_string = p_text.to_lower();
2373
2374 if (p_from == tree_search_box) {
2375 file_list_search_box->set_text(searched_string);
2376 } else { // File_list_search_box.
2377 tree_search_box->set_text(searched_string);
2378 }
2379
2380 bool unfold_path = (p_text.is_empty() && !current_path.is_empty());
2381 switch (display_mode) {
2382 case DISPLAY_MODE_TREE_ONLY: {
2383 _update_tree(searched_string.length() == 0 ? uncollapsed_paths_before_search : Vector<String>(), false, false, unfold_path);
2384 } break;
2385 case DISPLAY_MODE_SPLIT: {
2386 _update_file_list(false);
2387 _update_tree(searched_string.length() == 0 ? uncollapsed_paths_before_search : Vector<String>(), false, false, unfold_path);
2388 } break;
2389 }
2390}
2391
2392void FileSystemDock::_rescan() {
2393 _set_scanning_mode();
2394 EditorFileSystem::get_singleton()->scan();
2395}
2396
2397void FileSystemDock::_toggle_split_mode(bool p_active) {
2398 set_display_mode(p_active ? DISPLAY_MODE_SPLIT : DISPLAY_MODE_TREE_ONLY);
2399 emit_signal(SNAME("display_mode_changed"));
2400}
2401
2402void FileSystemDock::fix_dependencies(const String &p_for_file) {
2403 deps_editor->edit(p_for_file);
2404}
2405
2406void FileSystemDock::focus_on_filter() {
2407 LineEdit *current_search_box = nullptr;
2408 if (display_mode == DISPLAY_MODE_TREE_ONLY) {
2409 current_search_box = tree_search_box;
2410 } else if (display_mode == DISPLAY_MODE_SPLIT) {
2411 current_search_box = file_list_search_box;
2412 }
2413
2414 if (current_search_box) {
2415 current_search_box->grab_focus();
2416 current_search_box->select_all();
2417 }
2418}
2419
2420ScriptCreateDialog *FileSystemDock::get_script_create_dialog() const {
2421 return make_script_dialog;
2422}
2423
2424void FileSystemDock::set_file_list_display_mode(FileListDisplayMode p_mode) {
2425 if (p_mode == file_list_display_mode) {
2426 return;
2427 }
2428
2429 _toggle_file_display();
2430}
2431
2432void FileSystemDock::add_resource_tooltip_plugin(const Ref<EditorResourceTooltipPlugin> &p_plugin) {
2433 tooltip_plugins.push_back(p_plugin);
2434}
2435
2436void FileSystemDock::remove_resource_tooltip_plugin(const Ref<EditorResourceTooltipPlugin> &p_plugin) {
2437 int index = tooltip_plugins.find(p_plugin);
2438 ERR_FAIL_COND_MSG(index == -1, "Can't remove plugin that wasn't registered.");
2439 tooltip_plugins.remove_at(index);
2440}
2441
2442Control *FileSystemDock::create_tooltip_for_path(const String &p_path) const {
2443 if (p_path == "Favorites") {
2444 // No tooltip for the "Favorites" group.
2445 return nullptr;
2446 }
2447 if (DirAccess::exists(p_path)) {
2448 // No tooltip for directory.
2449 return nullptr;
2450 }
2451
2452 const String type = ResourceLoader::get_resource_type(p_path);
2453 Control *tooltip = EditorResourceTooltipPlugin::make_default_tooltip(p_path);
2454
2455 for (const Ref<EditorResourceTooltipPlugin> &plugin : tooltip_plugins) {
2456 if (plugin->handles(type)) {
2457 tooltip = plugin->make_tooltip_for_path(p_path, EditorResourcePreview::get_singleton()->get_preview_metadata(p_path), tooltip);
2458 }
2459 }
2460 return tooltip;
2461}
2462
2463Variant FileSystemDock::get_drag_data_fw(const Point2 &p_point, Control *p_from) {
2464 bool all_favorites = true;
2465 bool all_not_favorites = true;
2466
2467 Vector<String> paths;
2468
2469 if (p_from == tree) {
2470 // Check if the first selected is in favorite.
2471 TreeItem *selected = tree->get_next_selected(tree->get_root());
2472 while (selected) {
2473 TreeItem *favorites_item = tree->get_root()->get_first_child();
2474 if (selected == favorites_item) {
2475 // The "Favorites" item is not draggable.
2476 return Variant();
2477 }
2478
2479 bool is_favorite = selected->get_parent() != nullptr && tree->get_root()->get_first_child() == selected->get_parent();
2480 all_favorites &= is_favorite;
2481 all_not_favorites &= !is_favorite;
2482 selected = tree->get_next_selected(selected);
2483 }
2484 if (!all_not_favorites) {
2485 paths = _tree_get_selected(false);
2486 } else {
2487 paths = _tree_get_selected();
2488 }
2489 } else if (p_from == files) {
2490 for (int i = 0; i < files->get_item_count(); i++) {
2491 if (files->is_selected(i)) {
2492 paths.push_back(files->get_item_metadata(i));
2493 }
2494 }
2495 all_favorites = false;
2496 all_not_favorites = true;
2497 }
2498
2499 if (paths.is_empty()) {
2500 return Variant();
2501 }
2502
2503 Dictionary drag_data = EditorNode::get_singleton()->drag_files_and_dirs(paths, p_from);
2504 if (!all_not_favorites) {
2505 drag_data["favorite"] = all_favorites ? "all" : "mixed";
2506 }
2507 return drag_data;
2508}
2509
2510bool FileSystemDock::can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const {
2511 Dictionary drag_data = p_data;
2512
2513 if (drag_data.has("favorite")) {
2514 if (String(drag_data["favorite"]) != "all") {
2515 return false;
2516 }
2517
2518 // Moving favorite around.
2519 TreeItem *ti = tree->get_item_at_position(p_point);
2520 if (!ti) {
2521 return false;
2522 }
2523
2524 int drop_section = tree->get_drop_section_at_position(p_point);
2525 TreeItem *favorites_item = tree->get_root()->get_first_child();
2526
2527 TreeItem *resources_item = favorites_item->get_next();
2528
2529 if (ti == favorites_item) {
2530 return (drop_section == 1); // The parent, first fav.
2531 }
2532 if (ti->get_parent() && favorites_item == ti->get_parent()) {
2533 return true; // A favorite
2534 }
2535 if (ti == resources_item) {
2536 return (drop_section == -1); // The tree, last fav.
2537 }
2538
2539 return false;
2540 }
2541
2542 if (drag_data.has("type") && String(drag_data["type"]) == "resource") {
2543 // Move resources.
2544 String to_dir;
2545 bool favorite;
2546 _get_drag_target_folder(to_dir, favorite, p_point, p_from);
2547 return !to_dir.is_empty();
2548 }
2549
2550 if (drag_data.has("type") && (String(drag_data["type"]) == "files" || String(drag_data["type"]) == "files_and_dirs")) {
2551 // Move files or dir.
2552 String to_dir;
2553 bool favorite;
2554 _get_drag_target_folder(to_dir, favorite, p_point, p_from);
2555
2556 if (favorite) {
2557 return true;
2558 }
2559
2560 if (to_dir.is_empty()) {
2561 return false;
2562 }
2563
2564 // Attempting to move a folder into itself will fail later,
2565 // rather than bring up a message don't try to do it in the first place.
2566 to_dir = to_dir.ends_with("/") ? to_dir : (to_dir + "/");
2567 Vector<String> fnames = drag_data["files"];
2568 for (int i = 0; i < fnames.size(); ++i) {
2569 if (fnames[i].ends_with("/") && to_dir.begins_with(fnames[i])) {
2570 return false;
2571 }
2572 }
2573
2574 return true;
2575 }
2576
2577 if (drag_data.has("type") && String(drag_data["type"]) == "nodes") {
2578 // Save branch as scene.
2579 String to_dir;
2580 bool favorite;
2581 _get_drag_target_folder(to_dir, favorite, p_point, p_from);
2582 return !favorite && Array(drag_data["nodes"]).size() == 1;
2583 }
2584
2585 return false;
2586}
2587
2588void FileSystemDock::drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) {
2589 if (!can_drop_data_fw(p_point, p_data, p_from)) {
2590 return;
2591 }
2592 Dictionary drag_data = p_data;
2593
2594 Vector<String> dirs = EditorSettings::get_singleton()->get_favorites();
2595
2596 if (drag_data.has("favorite")) {
2597 if (String(drag_data["favorite"]) != "all") {
2598 return;
2599 }
2600 // Moving favorite around.
2601 TreeItem *ti = tree->get_item_at_position(p_point);
2602 if (!ti) {
2603 return;
2604 }
2605 int drop_section = tree->get_drop_section_at_position(p_point);
2606
2607 int drop_position;
2608 Vector<String> drag_files = drag_data["files"];
2609 TreeItem *favorites_item = tree->get_root()->get_first_child();
2610 TreeItem *resources_item = favorites_item->get_next();
2611
2612 if (ti == favorites_item) {
2613 // Drop on the favorite folder.
2614 drop_position = 0;
2615 } else if (ti == resources_item) {
2616 // Drop on the resource item.
2617 drop_position = dirs.size();
2618 } else {
2619 // Drop in the list.
2620 drop_position = dirs.find(ti->get_metadata(0));
2621 if (drop_section == 1) {
2622 drop_position++;
2623 }
2624 }
2625
2626 // Remove dragged favorites.
2627 Vector<int> to_remove;
2628 int offset = 0;
2629 for (int i = 0; i < drag_files.size(); i++) {
2630 int to_remove_pos = dirs.find(drag_files[i]);
2631 to_remove.push_back(to_remove_pos);
2632 if (to_remove_pos < drop_position) {
2633 offset++;
2634 }
2635 }
2636 drop_position -= offset;
2637 to_remove.sort();
2638 for (int i = 0; i < to_remove.size(); i++) {
2639 dirs.remove_at(to_remove[i] - i);
2640 }
2641
2642 // Re-add them at the right position.
2643 for (int i = 0; i < drag_files.size(); i++) {
2644 dirs.insert(drop_position, drag_files[i]);
2645 drop_position++;
2646 }
2647
2648 EditorSettings::get_singleton()->set_favorites(dirs);
2649 _update_tree(get_uncollapsed_paths());
2650
2651 if (display_mode == DISPLAY_MODE_SPLIT && current_path == "Favorites") {
2652 _update_file_list(true);
2653 }
2654 return;
2655 }
2656
2657 if (drag_data.has("type") && String(drag_data["type"]) == "resource") {
2658 // Moving resource.
2659 Ref<Resource> res = drag_data["resource"];
2660 String to_dir;
2661 bool favorite;
2662 _get_drag_target_folder(to_dir, favorite, p_point, p_from);
2663 if (res.is_valid() && !to_dir.is_empty()) {
2664 EditorNode::get_singleton()->push_item(res.ptr());
2665 EditorNode::get_singleton()->save_resource_as(res, to_dir);
2666 }
2667 }
2668
2669 if (drag_data.has("type") && (String(drag_data["type"]) == "files" || String(drag_data["type"]) == "files_and_dirs")) {
2670 // Move files or add to favorites.
2671 String to_dir;
2672 bool favorite;
2673 _get_drag_target_folder(to_dir, favorite, p_point, p_from);
2674 if (!to_dir.is_empty()) {
2675 Vector<String> fnames = drag_data["files"];
2676 to_move.clear();
2677 String target_dir = to_dir == "res://" ? to_dir : to_dir.trim_suffix("/");
2678
2679 for (int i = 0; i < fnames.size(); i++) {
2680 if (fnames[i].trim_suffix("/").get_base_dir() != target_dir) {
2681 to_move.push_back(FileOrFolder(fnames[i], !fnames[i].ends_with("/")));
2682 }
2683 }
2684 if (!to_move.is_empty()) {
2685 if (Input::get_singleton()->is_key_pressed(Key::CTRL)) {
2686 _move_operation_confirm(to_dir, true);
2687 } else {
2688 _move_operation_confirm(to_dir);
2689 }
2690 }
2691 } else if (favorite) {
2692 // Add the files from favorites.
2693 Vector<String> fnames = drag_data["files"];
2694 Vector<String> favorites_list = EditorSettings::get_singleton()->get_favorites();
2695 for (int i = 0; i < fnames.size(); i++) {
2696 if (!favorites_list.has(fnames[i])) {
2697 favorites_list.push_back(fnames[i]);
2698 }
2699 }
2700 EditorSettings::get_singleton()->set_favorites(favorites_list);
2701 _update_tree(get_uncollapsed_paths());
2702 }
2703 }
2704
2705 if (drag_data.has("type") && String(drag_data["type"]) == "nodes") {
2706 String to_dir;
2707 bool favorite;
2708 _get_drag_target_folder(to_dir, favorite, p_point, p_from);
2709 SceneTreeDock::get_singleton()->save_branch_to_file(to_dir);
2710 }
2711}
2712
2713void FileSystemDock::_get_drag_target_folder(String &target, bool &target_favorites, const Point2 &p_point, Control *p_from) const {
2714 target = String();
2715 target_favorites = false;
2716
2717 // In the file list.
2718 if (p_from == files) {
2719 int pos = files->get_item_at_position(p_point, true);
2720 if (pos == -1) {
2721 return;
2722 }
2723
2724 String ltarget = files->get_item_metadata(pos);
2725 target = ltarget.ends_with("/") ? ltarget : current_path.get_base_dir();
2726 return;
2727 }
2728
2729 // In the tree.
2730 if (p_from == tree) {
2731 TreeItem *ti = tree->get_item_at_position(p_point);
2732 int section = tree->get_drop_section_at_position(p_point);
2733 if (ti) {
2734 // Check the favorites first.
2735 if (ti == tree->get_root()->get_first_child() && section >= 0) {
2736 target_favorites = true;
2737 return;
2738 } else if (ti->get_parent() == tree->get_root()->get_first_child()) {
2739 target_favorites = true;
2740 return;
2741 } else {
2742 String fpath = ti->get_metadata(0);
2743 if (section == 0) {
2744 if (fpath.ends_with("/")) {
2745 // We drop on a folder.
2746 target = fpath;
2747 return;
2748 } else {
2749 // We drop on the folder that the target file is in.
2750 target = fpath.get_base_dir();
2751 return;
2752 }
2753 } else {
2754 if (ti->get_parent() != tree->get_root()->get_first_child()) {
2755 // Not in the favorite section.
2756 if (fpath != "res://") {
2757 // We drop between two files
2758 if (fpath.ends_with("/")) {
2759 fpath = fpath.substr(0, fpath.length() - 1);
2760 }
2761 target = fpath.get_base_dir();
2762 return;
2763 }
2764 }
2765 }
2766 }
2767 }
2768 }
2769}
2770
2771void FileSystemDock::_update_folder_colors_setting() {
2772 if (!ProjectSettings::get_singleton()->has_setting("file_customization/folder_colors")) {
2773 ProjectSettings::get_singleton()->set_setting("file_customization/folder_colors", assigned_folder_colors);
2774 } else if (assigned_folder_colors.is_empty()) {
2775 ProjectSettings::get_singleton()->set_setting("file_customization/folder_colors", Variant());
2776 }
2777 ProjectSettings::get_singleton()->save();
2778}
2779
2780void FileSystemDock::_folder_color_index_pressed(int p_index, PopupMenu *p_menu) {
2781 Variant chosen_color_name = p_menu->get_item_metadata(p_index);
2782 Vector<String> selected;
2783
2784 // Get all selected folders based on whether the files panel or tree panel is currently focused.
2785 if (files->has_focus()) {
2786 Vector<int> files_selected_ids = files->get_selected_items();
2787 for (int i = 0; i < files_selected_ids.size(); i++) {
2788 selected.push_back(files->get_item_metadata(files_selected_ids[i]));
2789 }
2790 } else {
2791 TreeItem *tree_selected = tree->get_root();
2792 tree_selected = tree->get_next_selected(tree_selected);
2793 while (tree_selected) {
2794 selected.push_back(tree_selected->get_metadata(0));
2795 tree_selected = tree->get_next_selected(tree_selected);
2796 }
2797 }
2798
2799 // Update project settings with new folder colors.
2800 for (int i = 0; i < selected.size(); i++) {
2801 String fpath = selected[i];
2802
2803 if (chosen_color_name) {
2804 assigned_folder_colors[fpath] = chosen_color_name;
2805 } else {
2806 assigned_folder_colors.erase(fpath);
2807 }
2808 }
2809
2810 _update_folder_colors_setting();
2811
2812 _update_tree(get_uncollapsed_paths());
2813 _update_file_list(true);
2814}
2815
2816void FileSystemDock::_file_and_folders_fill_popup(PopupMenu *p_popup, Vector<String> p_paths, bool p_display_path_dependent_options) {
2817 // Add options for files and folders.
2818 ERR_FAIL_COND_MSG(p_paths.is_empty(), "Path cannot be empty.");
2819
2820 Vector<String> filenames;
2821 Vector<String> foldernames;
2822
2823 Vector<String> favorites_list = EditorSettings::get_singleton()->get_favorites();
2824
2825 bool all_files = true;
2826 bool all_files_scenes = true;
2827 bool all_folders = true;
2828 bool all_favorites = true;
2829 bool all_not_favorites = true;
2830
2831 for (int i = 0; i < p_paths.size(); i++) {
2832 String fpath = p_paths[i];
2833 if (fpath.ends_with("/")) {
2834 foldernames.push_back(fpath);
2835 all_files = false;
2836 } else {
2837 filenames.push_back(fpath);
2838 all_folders = false;
2839 all_files_scenes &= (EditorFileSystem::get_singleton()->get_file_type(fpath) == "PackedScene");
2840 }
2841
2842 // Check if in favorites.
2843 bool found = false;
2844 for (int j = 0; j < favorites_list.size(); j++) {
2845 if (favorites_list[j] == fpath) {
2846 found = true;
2847 break;
2848 }
2849 }
2850 if (found) {
2851 all_not_favorites = false;
2852 } else {
2853 all_favorites = false;
2854 }
2855 }
2856
2857 if (all_files) {
2858 if (all_files_scenes) {
2859 if (filenames.size() == 1) {
2860 p_popup->add_icon_item(get_editor_theme_icon(SNAME("Load")), TTR("Open Scene"), FILE_OPEN);
2861 p_popup->add_icon_item(get_editor_theme_icon(SNAME("CreateNewSceneFrom")), TTR("New Inherited Scene"), FILE_INHERIT);
2862 if (GLOBAL_GET("application/run/main_scene") != filenames[0]) {
2863 p_popup->add_icon_item(get_editor_theme_icon(SNAME("PlayScene")), TTR("Set As Main Scene"), FILE_MAIN_SCENE);
2864 }
2865 } else {
2866 p_popup->add_icon_item(get_editor_theme_icon(SNAME("Load")), TTR("Open Scenes"), FILE_OPEN);
2867 }
2868 p_popup->add_icon_item(get_editor_theme_icon(SNAME("Instance")), TTR("Instantiate"), FILE_INSTANTIATE);
2869 p_popup->add_separator();
2870 } else if (filenames.size() == 1) {
2871 p_popup->add_icon_item(get_editor_theme_icon(SNAME("Load")), TTR("Open"), FILE_OPEN);
2872 p_popup->add_separator();
2873 }
2874
2875 if (filenames.size() == 1) {
2876 p_popup->add_item(TTR("Edit Dependencies..."), FILE_DEPENDENCIES);
2877 p_popup->add_item(TTR("View Owners..."), FILE_OWNERS);
2878 p_popup->add_separator();
2879 }
2880 }
2881
2882 if (p_paths.size() == 1 && p_display_path_dependent_options) {
2883 PopupMenu *new_menu = memnew(PopupMenu);
2884 new_menu->set_name("New");
2885 new_menu->connect("id_pressed", callable_mp(this, &FileSystemDock::_tree_rmb_option));
2886
2887 p_popup->add_child(new_menu);
2888 p_popup->add_submenu_item(TTR("Create New"), "New", FILE_NEW);
2889 p_popup->set_item_icon(p_popup->get_item_index(FILE_NEW), get_editor_theme_icon(SNAME("Add")));
2890
2891 new_menu->add_icon_item(get_editor_theme_icon(SNAME("Folder")), TTR("Folder..."), FILE_NEW_FOLDER);
2892 new_menu->add_icon_item(get_editor_theme_icon(SNAME("PackedScene")), TTR("Scene..."), FILE_NEW_SCENE);
2893 new_menu->add_icon_item(get_editor_theme_icon(SNAME("Script")), TTR("Script..."), FILE_NEW_SCRIPT);
2894 new_menu->add_icon_item(get_editor_theme_icon(SNAME("Object")), TTR("Resource..."), FILE_NEW_RESOURCE);
2895 new_menu->add_icon_item(get_editor_theme_icon(SNAME("TextFile")), TTR("TextFile..."), FILE_NEW_TEXTFILE);
2896 p_popup->add_separator();
2897 }
2898
2899 if (all_folders && foldernames.size() > 0) {
2900 p_popup->add_icon_item(get_editor_theme_icon(SNAME("Load")), TTR("Expand Folder"), FILE_OPEN);
2901
2902 if (foldernames.size() == 1) {
2903 p_popup->add_icon_item(get_editor_theme_icon(SNAME("GuiTreeArrowDown")), TTR("Expand Hierarchy"), FOLDER_EXPAND_ALL);
2904 p_popup->add_icon_item(get_editor_theme_icon(SNAME("GuiTreeArrowRight")), TTR("Collapse Hierarchy"), FOLDER_COLLAPSE_ALL);
2905 }
2906
2907 p_popup->add_separator();
2908
2909 if (p_paths[0] != "res://") {
2910 PopupMenu *folder_colors_menu = memnew(PopupMenu);
2911 folder_colors_menu->set_name("FolderColor");
2912 folder_colors_menu->connect("id_pressed", callable_mp(this, &FileSystemDock::_folder_color_index_pressed).bind(folder_colors_menu));
2913
2914 p_popup->add_child(folder_colors_menu);
2915 p_popup->add_submenu_item(TTR("Set Folder Color..."), "FolderColor");
2916 p_popup->set_item_icon(-1, get_editor_theme_icon(SNAME("CanvasItem")));
2917
2918 folder_colors_menu->add_icon_item(get_editor_theme_icon(SNAME("Folder")), TTR("Default (Reset)"));
2919 folder_colors_menu->set_item_icon_modulate(0, get_theme_color(SNAME("folder_icon_color"), SNAME("FileDialog")));
2920 folder_colors_menu->add_separator();
2921
2922 for (const KeyValue<String, Color> &E : folder_colors) {
2923 folder_colors_menu->add_icon_item(get_editor_theme_icon(SNAME("Folder")), TTR(E.key.capitalize()));
2924
2925 folder_colors_menu->set_item_icon_modulate(-1, editor_is_dark_theme ? E.value : E.value * 2);
2926 folder_colors_menu->set_item_metadata(-1, E.key);
2927 }
2928 }
2929 }
2930
2931 if (p_paths.size() == 1) {
2932 p_popup->add_icon_shortcut(get_editor_theme_icon(SNAME("ActionCopy")), ED_GET_SHORTCUT("filesystem_dock/copy_path"), FILE_COPY_PATH);
2933 if (ResourceLoader::get_resource_uid(p_paths[0]) != ResourceUID::INVALID_ID) {
2934 p_popup->add_icon_shortcut(get_editor_theme_icon(SNAME("Instance")), ED_GET_SHORTCUT("filesystem_dock/copy_uid"), FILE_COPY_UID);
2935 }
2936 if (p_paths[0] != "res://") {
2937 p_popup->add_icon_shortcut(get_editor_theme_icon(SNAME("Rename")), ED_GET_SHORTCUT("filesystem_dock/rename"), FILE_RENAME);
2938 p_popup->add_icon_shortcut(get_editor_theme_icon(SNAME("Duplicate")), ED_GET_SHORTCUT("filesystem_dock/duplicate"), FILE_DUPLICATE);
2939 }
2940 }
2941
2942 if (p_paths.size() > 1 || p_paths[0] != "res://") {
2943 p_popup->add_icon_item(get_editor_theme_icon(SNAME("MoveUp")), TTR("Move/Duplicate To..."), FILE_MOVE);
2944 p_popup->add_icon_shortcut(get_editor_theme_icon(SNAME("Remove")), ED_GET_SHORTCUT("filesystem_dock/delete"), FILE_REMOVE);
2945 }
2946
2947 p_popup->add_separator();
2948
2949 if (p_paths.size() >= 1) {
2950 if (!all_favorites) {
2951 p_popup->add_icon_item(get_editor_theme_icon(SNAME("Favorites")), TTR("Add to Favorites"), FILE_ADD_FAVORITE);
2952 }
2953 if (!all_not_favorites) {
2954 p_popup->add_icon_item(get_editor_theme_icon(SNAME("NonFavorite")), TTR("Remove from Favorites"), FILE_REMOVE_FAVORITE);
2955 }
2956
2957 {
2958 List<String> resource_extensions;
2959 ResourceFormatImporter::get_singleton()->get_recognized_extensions_for_type("Resource", &resource_extensions);
2960 HashSet<String> extension_list;
2961 for (const String &extension : resource_extensions) {
2962 extension_list.insert(extension);
2963 }
2964
2965 bool resource_valid = true;
2966 String main_extension;
2967
2968 for (int i = 0; i != p_paths.size(); ++i) {
2969 String extension = p_paths[i].get_extension();
2970 if (extension_list.has(extension)) {
2971 if (main_extension.is_empty()) {
2972 main_extension = extension;
2973 } else if (extension != main_extension) {
2974 resource_valid = false;
2975 break;
2976 }
2977 } else {
2978 resource_valid = false;
2979 break;
2980 }
2981 }
2982
2983 if (resource_valid) {
2984 p_popup->add_icon_item(get_editor_theme_icon(SNAME("Load")), TTR("Reimport"), FILE_REIMPORT);
2985 }
2986 }
2987 }
2988
2989 if (p_paths.size() == 1) {
2990 const String fpath = p_paths[0];
2991
2992#if !defined(ANDROID_ENABLED) && !defined(WEB_ENABLED)
2993 p_popup->add_separator();
2994
2995 // Opening the system file manager is not supported on the Android and web editors.
2996 const bool is_directory = fpath.ends_with("/");
2997 const String item_text = is_directory ? TTR("Open in File Manager") : TTR("Show in File Manager");
2998 p_popup->add_icon_shortcut(get_editor_theme_icon(SNAME("Filesystem")), ED_GET_SHORTCUT("filesystem_dock/show_in_explorer"), FILE_SHOW_IN_EXPLORER);
2999 p_popup->set_item_text(p_popup->get_item_index(FILE_SHOW_IN_EXPLORER), item_text);
3000 if (!is_directory) {
3001 p_popup->add_icon_shortcut(get_editor_theme_icon(SNAME("ExternalLink")), ED_GET_SHORTCUT("filesystem_dock/open_in_external_program"), FILE_OPEN_EXTERNAL);
3002 }
3003#endif
3004
3005 current_path = fpath;
3006 }
3007}
3008
3009void FileSystemDock::_tree_rmb_select(const Vector2 &p_pos, MouseButton p_button) {
3010 if (p_button != MouseButton::RIGHT) {
3011 return;
3012 }
3013 tree->grab_focus();
3014
3015 // Right click is pressed in the tree.
3016 Vector<String> paths = _tree_get_selected(false);
3017
3018 tree_popup->clear();
3019
3020 // Popup.
3021 if (!paths.is_empty()) {
3022 tree_popup->reset_size();
3023 _file_and_folders_fill_popup(tree_popup, paths);
3024 tree_popup->set_position(tree->get_screen_position() + p_pos);
3025 tree_popup->reset_size();
3026 tree_popup->popup();
3027 }
3028}
3029
3030void FileSystemDock::_tree_empty_click(const Vector2 &p_pos, MouseButton p_button) {
3031 if (p_button != MouseButton::RIGHT) {
3032 return;
3033 }
3034 // Right click is pressed in the empty space of the tree.
3035 current_path = "res://";
3036 tree_popup->clear();
3037 tree_popup->reset_size();
3038 tree_popup->add_icon_item(get_editor_theme_icon(SNAME("Folder")), TTR("New Folder..."), FILE_NEW_FOLDER);
3039 tree_popup->add_icon_item(get_editor_theme_icon(SNAME("PackedScene")), TTR("New Scene..."), FILE_NEW_SCENE);
3040 tree_popup->add_icon_item(get_editor_theme_icon(SNAME("Script")), TTR("New Script..."), FILE_NEW_SCRIPT);
3041 tree_popup->add_icon_item(get_editor_theme_icon(SNAME("Object")), TTR("New Resource..."), FILE_NEW_RESOURCE);
3042 tree_popup->add_icon_item(get_editor_theme_icon(SNAME("TextFile")), TTR("New TextFile..."), FILE_NEW_TEXTFILE);
3043#if !defined(ANDROID_ENABLED) && !defined(WEB_ENABLED)
3044 // Opening the system file manager is not supported on the Android and web editors.
3045 tree_popup->add_separator();
3046 tree_popup->add_icon_shortcut(get_editor_theme_icon(SNAME("Filesystem")), ED_GET_SHORTCUT("filesystem_dock/show_in_explorer"), FILE_SHOW_IN_EXPLORER);
3047#endif
3048
3049 tree_popup->set_position(tree->get_screen_position() + p_pos);
3050 tree_popup->reset_size();
3051 tree_popup->popup();
3052}
3053
3054void FileSystemDock::_tree_empty_selected() {
3055 tree->deselect_all();
3056}
3057
3058void FileSystemDock::_file_list_item_clicked(int p_item, const Vector2 &p_pos, MouseButton p_mouse_button_index) {
3059 if (p_mouse_button_index != MouseButton::RIGHT) {
3060 return;
3061 }
3062 files->grab_focus();
3063
3064 // Right click is pressed in the file list.
3065 Vector<String> paths;
3066 for (int i = 0; i < files->get_item_count(); i++) {
3067 if (!files->is_selected(i)) {
3068 continue;
3069 }
3070 if (files->get_item_text(p_item) == "..") {
3071 files->deselect(i);
3072 continue;
3073 }
3074 paths.push_back(files->get_item_metadata(i));
3075 }
3076
3077 // Popup.
3078 if (!paths.is_empty()) {
3079 file_list_popup->clear();
3080 _file_and_folders_fill_popup(file_list_popup, paths, searched_string.length() == 0);
3081 file_list_popup->set_position(files->get_screen_position() + p_pos);
3082 file_list_popup->reset_size();
3083 file_list_popup->popup();
3084 }
3085}
3086
3087void FileSystemDock::_file_list_empty_clicked(const Vector2 &p_pos, MouseButton p_mouse_button_index) {
3088 if (p_mouse_button_index != MouseButton::RIGHT) {
3089 return;
3090 }
3091
3092 // Right click on empty space for file list.
3093 if (searched_string.length() > 0) {
3094 return;
3095 }
3096
3097 current_path = current_path_line_edit->get_text();
3098
3099 file_list_popup->clear();
3100 file_list_popup->reset_size();
3101
3102 file_list_popup->add_icon_item(get_editor_theme_icon(SNAME("Folder")), TTR("New Folder..."), FILE_NEW_FOLDER);
3103 file_list_popup->add_icon_item(get_editor_theme_icon(SNAME("PackedScene")), TTR("New Scene..."), FILE_NEW_SCENE);
3104 file_list_popup->add_icon_item(get_editor_theme_icon(SNAME("Script")), TTR("New Script..."), FILE_NEW_SCRIPT);
3105 file_list_popup->add_icon_item(get_editor_theme_icon(SNAME("Object")), TTR("New Resource..."), FILE_NEW_RESOURCE);
3106 file_list_popup->add_icon_item(get_editor_theme_icon(SNAME("TextFile")), TTR("New TextFile..."), FILE_NEW_TEXTFILE);
3107 file_list_popup->add_separator();
3108 file_list_popup->add_icon_shortcut(get_editor_theme_icon(SNAME("Filesystem")), ED_GET_SHORTCUT("filesystem_dock/show_in_explorer"), FILE_SHOW_IN_EXPLORER);
3109
3110 file_list_popup->set_position(files->get_screen_position() + p_pos);
3111 file_list_popup->reset_size();
3112 file_list_popup->popup();
3113}
3114
3115void FileSystemDock::select_file(const String &p_file) {
3116 _navigate_to_path(p_file);
3117}
3118
3119void FileSystemDock::_file_multi_selected(int p_index, bool p_selected) {
3120 // Set the path to the current focused item.
3121 int current = files->get_current();
3122 if (current == p_index) {
3123 String fpath = files->get_item_metadata(current);
3124 if (!fpath.ends_with("/")) {
3125 current_path = fpath;
3126 if (display_mode == DISPLAY_MODE_SPLIT) {
3127 _update_tree(get_uncollapsed_paths());
3128 }
3129 }
3130 }
3131
3132 // Update the import dock.
3133 import_dock_needs_update = true;
3134 call_deferred(SNAME("_update_import_dock"));
3135}
3136
3137void FileSystemDock::_tree_mouse_exited() {
3138 if (holding_branch) {
3139 _reselect_items_selected_on_drag_begin();
3140 }
3141}
3142
3143void FileSystemDock::_reselect_items_selected_on_drag_begin(bool reset) {
3144 TreeItem *selected_item = tree->get_next_selected(tree->get_root());
3145 if (selected_item) {
3146 selected_item->deselect(0);
3147 }
3148 if (!tree_items_selected_on_drag_begin.is_empty()) {
3149 bool reselected = false;
3150 for (TreeItem *item : tree_items_selected_on_drag_begin) {
3151 if (item->get_tree()) {
3152 item->select(0);
3153 reselected = true;
3154 }
3155 }
3156
3157 if (reset) {
3158 tree_items_selected_on_drag_begin.clear();
3159 }
3160
3161 if (!reselected) {
3162 // If couldn't reselect the items selected on drag begin, select the "res://" item.
3163 tree->get_root()->get_child(1)->select(0);
3164 }
3165 }
3166
3167 files->deselect_all();
3168 if (!list_items_selected_on_drag_begin.is_empty()) {
3169 for (const int idx : list_items_selected_on_drag_begin) {
3170 files->select(idx, false);
3171 }
3172
3173 if (reset) {
3174 list_items_selected_on_drag_begin.clear();
3175 }
3176 }
3177}
3178
3179void FileSystemDock::_tree_gui_input(Ref<InputEvent> p_event) {
3180 Ref<InputEventKey> key = p_event;
3181
3182 Ref<InputEventMouseMotion> mm = p_event;
3183 if (mm.is_valid()) {
3184 TreeItem *item = tree->get_item_at_position(mm->get_position());
3185 if (item && holding_branch) {
3186 String fpath = item->get_metadata(0);
3187 while (!fpath.ends_with("/") && fpath != "res://" && item->get_parent()) { // Find the parent folder tree item.
3188 item = item->get_parent();
3189 fpath = item->get_metadata(0);
3190 }
3191
3192 TreeItem *deselect_item = tree->get_next_selected(tree->get_root());
3193 while (deselect_item) {
3194 deselect_item->deselect(0);
3195 deselect_item = tree->get_next_selected(deselect_item);
3196 }
3197 item->select(0);
3198
3199 if (display_mode == DisplayMode::DISPLAY_MODE_SPLIT) {
3200 files->deselect_all();
3201 // Try to select the corresponding file list item.
3202 const int files_item_idx = files->find_metadata(fpath);
3203 if (files_item_idx != -1) {
3204 files->select(files_item_idx);
3205 }
3206 }
3207 }
3208 }
3209
3210 if (key.is_valid() && key->is_pressed() && !key->is_echo()) {
3211 if (ED_IS_SHORTCUT("filesystem_dock/duplicate", p_event)) {
3212 _tree_rmb_option(FILE_DUPLICATE);
3213 } else if (ED_IS_SHORTCUT("filesystem_dock/copy_path", p_event)) {
3214 _tree_rmb_option(FILE_COPY_PATH);
3215 } else if (ED_IS_SHORTCUT("filesystem_dock/copy_uid", p_event)) {
3216 _tree_rmb_option(FILE_COPY_UID);
3217 } else if (ED_IS_SHORTCUT("filesystem_dock/delete", p_event)) {
3218 _tree_rmb_option(FILE_REMOVE);
3219 } else if (ED_IS_SHORTCUT("filesystem_dock/rename", p_event)) {
3220 _tree_rmb_option(FILE_RENAME);
3221 } else if (ED_IS_SHORTCUT("filesystem_dock/show_in_explorer", p_event)) {
3222 _tree_rmb_option(FILE_SHOW_IN_EXPLORER);
3223 } else if (ED_IS_SHORTCUT("filesystem_dock/open_in_external_program", p_event)) {
3224 _tree_rmb_option(FILE_OPEN_EXTERNAL);
3225 } else if (ED_IS_SHORTCUT("editor/open_search", p_event)) {
3226 focus_on_filter();
3227 } else {
3228 return;
3229 }
3230
3231 accept_event();
3232 }
3233}
3234
3235void FileSystemDock::_file_list_gui_input(Ref<InputEvent> p_event) {
3236 Ref<InputEventMouseMotion> mm = p_event;
3237 if (mm.is_valid() && holding_branch) {
3238 const int item_idx = files->get_item_at_position(mm->get_position());
3239 if (item_idx != -1) {
3240 files->deselect_all();
3241 String fpath = files->get_item_metadata(item_idx);
3242 if (fpath.ends_with("/") || fpath == "res://") {
3243 files->select(item_idx);
3244 }
3245
3246 TreeItem *deselect_item = tree->get_next_selected(tree->get_root());
3247 while (deselect_item) {
3248 deselect_item->deselect(0);
3249 deselect_item = tree->get_next_selected(deselect_item);
3250 }
3251
3252 // Try to select the corresponding tree item.
3253 TreeItem *tree_item = tree->get_item_with_text(files->get_item_text(item_idx));
3254 if (tree_item) {
3255 tree_item->select(0);
3256 } else {
3257 // Find parent folder.
3258 fpath = fpath.substr(0, fpath.rfind("/") + 1);
3259 if (fpath.size() > String("res://").size()) {
3260 fpath = fpath.left(fpath.size() - 2); // Remove last '/'.
3261 const int slash_idx = fpath.rfind("/");
3262 fpath = fpath.substr(slash_idx + 1, fpath.size() - slash_idx - 1);
3263 }
3264
3265 tree_item = tree->get_item_with_text(fpath);
3266 if (tree_item) {
3267 tree_item->select(0);
3268 }
3269 }
3270 }
3271 }
3272
3273 Ref<InputEventKey> key = p_event;
3274 if (key.is_valid() && key->is_pressed() && !key->is_echo()) {
3275 if (ED_IS_SHORTCUT("filesystem_dock/duplicate", p_event)) {
3276 _file_list_rmb_option(FILE_DUPLICATE);
3277 } else if (ED_IS_SHORTCUT("filesystem_dock/copy_path", p_event)) {
3278 _file_list_rmb_option(FILE_COPY_PATH);
3279 } else if (ED_IS_SHORTCUT("filesystem_dock/delete", p_event)) {
3280 _file_list_rmb_option(FILE_REMOVE);
3281 } else if (ED_IS_SHORTCUT("filesystem_dock/rename", p_event)) {
3282 _file_list_rmb_option(FILE_RENAME);
3283 } else if (ED_IS_SHORTCUT("filesystem_dock/show_in_explorer", p_event)) {
3284 _file_list_rmb_option(FILE_SHOW_IN_EXPLORER);
3285 } else if (ED_IS_SHORTCUT("editor/open_search", p_event)) {
3286 focus_on_filter();
3287 } else {
3288 return;
3289 }
3290
3291 accept_event();
3292 }
3293}
3294
3295bool FileSystemDock::_get_imported_files(const String &p_path, String &r_extension, Vector<String> &r_files) const {
3296 if (!p_path.ends_with("/")) {
3297 if (FileAccess::exists(p_path + ".import")) {
3298 if (r_extension.is_empty()) {
3299 r_extension = p_path.get_extension();
3300 } else if (r_extension != p_path.get_extension()) {
3301 r_files.clear();
3302 return false; // File type mismatch, stop search.
3303 }
3304
3305 r_files.push_back(p_path);
3306 }
3307 return true;
3308 }
3309
3310 Ref<DirAccess> da = DirAccess::open(p_path);
3311 da->list_dir_begin();
3312 String n = da->get_next();
3313 while (!n.is_empty()) {
3314 if (n != "." && n != ".." && !n.ends_with(".import")) {
3315 String npath = p_path + n + (da->current_is_dir() ? "/" : "");
3316 if (!_get_imported_files(npath, r_extension, r_files)) {
3317 return false;
3318 }
3319 }
3320 n = da->get_next();
3321 }
3322 da->list_dir_end();
3323 return true;
3324}
3325
3326void FileSystemDock::_update_import_dock() {
3327 if (!import_dock_needs_update) {
3328 return;
3329 }
3330
3331 // List selected.
3332 Vector<String> selected;
3333 if (display_mode == DISPLAY_MODE_TREE_ONLY) {
3334 // Use the tree
3335 selected = _tree_get_selected();
3336
3337 } else {
3338 // Use the file list.
3339 for (int i = 0; i < files->get_item_count(); i++) {
3340 if (!files->is_selected(i)) {
3341 continue;
3342 }
3343
3344 selected.push_back(files->get_item_metadata(i));
3345 }
3346 }
3347
3348 if (!selected.is_empty() && selected[0] == "res://") {
3349 // Scanning res:// is costly and unlikely to yield any useful results.
3350 return;
3351 }
3352
3353 // Expand directory selection.
3354 Vector<String> efiles;
3355 String extension;
3356 for (const String &fpath : selected) {
3357 _get_imported_files(fpath, extension, efiles);
3358 }
3359
3360 // Check import.
3361 Vector<String> imports;
3362 String import_type;
3363 for (int i = 0; i < efiles.size(); i++) {
3364 String fpath = efiles[i];
3365 Ref<ConfigFile> cf;
3366 cf.instantiate();
3367 Error err = cf->load(fpath + ".import");
3368 if (err != OK) {
3369 imports.clear();
3370 break;
3371 }
3372
3373 String type;
3374 if (cf->has_section_key("remap", "type")) {
3375 type = cf->get_value("remap", "type");
3376 }
3377 if (import_type.is_empty()) {
3378 import_type = type;
3379 } else if (import_type != type) {
3380 // All should be the same type.
3381 imports.clear();
3382 break;
3383 }
3384 imports.push_back(fpath);
3385 }
3386
3387 if (imports.size() == 0) {
3388 ImportDock::get_singleton()->clear();
3389 } else if (imports.size() == 1) {
3390 ImportDock::get_singleton()->set_edit_path(imports[0]);
3391 } else {
3392 ImportDock::get_singleton()->set_edit_multiple_paths(imports);
3393 }
3394
3395 import_dock_needs_update = false;
3396}
3397
3398void FileSystemDock::_feature_profile_changed() {
3399 _update_display_mode(true);
3400}
3401
3402void FileSystemDock::set_file_sort(FileSortOption p_file_sort) {
3403 for (int i = 0; i != FILE_SORT_MAX; i++) {
3404 tree_button_sort->get_popup()->set_item_checked(i, (i == (int)p_file_sort));
3405 file_list_button_sort->get_popup()->set_item_checked(i, (i == (int)p_file_sort));
3406 }
3407 file_sort = p_file_sort;
3408
3409 // Update everything needed.
3410 _update_tree(get_uncollapsed_paths());
3411 _update_file_list(true);
3412}
3413
3414void FileSystemDock::_file_sort_popup(int p_id) {
3415 set_file_sort((FileSortOption)p_id);
3416}
3417
3418MenuButton *FileSystemDock::_create_file_menu_button() {
3419 MenuButton *button = memnew(MenuButton);
3420 button->set_flat(true);
3421 button->set_tooltip_text(TTR("Sort Files"));
3422
3423 PopupMenu *p = button->get_popup();
3424 p->connect("id_pressed", callable_mp(this, &FileSystemDock::_file_sort_popup));
3425 p->add_radio_check_item(TTR("Sort by Name (Ascending)"), FILE_SORT_NAME);
3426 p->add_radio_check_item(TTR("Sort by Name (Descending)"), FILE_SORT_NAME_REVERSE);
3427 p->add_radio_check_item(TTR("Sort by Type (Ascending)"), FILE_SORT_TYPE);
3428 p->add_radio_check_item(TTR("Sort by Type (Descending)"), FILE_SORT_TYPE_REVERSE);
3429 p->add_radio_check_item(TTR("Sort by Last Modified"), FILE_SORT_MODIFIED_TIME);
3430 p->add_radio_check_item(TTR("Sort by First Modified"), FILE_SORT_MODIFIED_TIME_REVERSE);
3431 p->set_item_checked(file_sort, true);
3432 return button;
3433}
3434
3435void FileSystemDock::_bind_methods() {
3436 ClassDB::bind_method(D_METHOD("_update_tree"), &FileSystemDock::_update_tree);
3437
3438 ClassDB::bind_method(D_METHOD("_file_list_thumbnail_done"), &FileSystemDock::_file_list_thumbnail_done);
3439 ClassDB::bind_method(D_METHOD("_tree_thumbnail_done"), &FileSystemDock::_tree_thumbnail_done);
3440 ClassDB::bind_method(D_METHOD("_select_file"), &FileSystemDock::_select_file);
3441
3442 ClassDB::bind_method(D_METHOD("navigate_to_path", "path"), &FileSystemDock::navigate_to_path);
3443
3444 ClassDB::bind_method(D_METHOD("_update_import_dock"), &FileSystemDock::_update_import_dock);
3445
3446 ClassDB::bind_method(D_METHOD("add_resource_tooltip_plugin", "plugin"), &FileSystemDock::add_resource_tooltip_plugin);
3447 ClassDB::bind_method(D_METHOD("remove_resource_tooltip_plugin", "plugin"), &FileSystemDock::remove_resource_tooltip_plugin);
3448
3449 ADD_SIGNAL(MethodInfo("inherit", PropertyInfo(Variant::STRING, "file")));
3450 ADD_SIGNAL(MethodInfo("instantiate", PropertyInfo(Variant::PACKED_STRING_ARRAY, "files")));
3451
3452 ADD_SIGNAL(MethodInfo("resource_removed", PropertyInfo(Variant::OBJECT, "resource", PROPERTY_HINT_RESOURCE_TYPE, "Resource")));
3453 ADD_SIGNAL(MethodInfo("file_removed", PropertyInfo(Variant::STRING, "file")));
3454 ADD_SIGNAL(MethodInfo("folder_removed", PropertyInfo(Variant::STRING, "folder")));
3455 ADD_SIGNAL(MethodInfo("files_moved", PropertyInfo(Variant::STRING, "old_file"), PropertyInfo(Variant::STRING, "new_file")));
3456 ADD_SIGNAL(MethodInfo("folder_moved", PropertyInfo(Variant::STRING, "old_folder"), PropertyInfo(Variant::STRING, "new_folder")));
3457
3458 ADD_SIGNAL(MethodInfo("display_mode_changed"));
3459}
3460
3461FileSystemDock::FileSystemDock() {
3462 singleton = this;
3463 set_name("FileSystem");
3464 current_path = "res://";
3465
3466 ProjectSettings::get_singleton()->add_hidden_prefix("file_customization/");
3467
3468 // `KeyModifierMask::CMD_OR_CTRL | Key::C` conflicts with other editor shortcuts.
3469 ED_SHORTCUT("filesystem_dock/copy_path", TTR("Copy Path"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::C);
3470 ED_SHORTCUT("filesystem_dock/copy_uid", TTR("Copy UID"));
3471 ED_SHORTCUT("filesystem_dock/duplicate", TTR("Duplicate..."), KeyModifierMask::CMD_OR_CTRL | Key::D);
3472 ED_SHORTCUT("filesystem_dock/delete", TTR("Delete"), Key::KEY_DELETE);
3473 ED_SHORTCUT("filesystem_dock/rename", TTR("Rename..."), Key::F2);
3474 ED_SHORTCUT_OVERRIDE("filesystem_dock/rename", "macos", Key::ENTER);
3475#if !defined(ANDROID_ENABLED) && !defined(WEB_ENABLED)
3476 // Opening the system file manager or opening in an external program is not supported on the Android and web editors.
3477 ED_SHORTCUT("filesystem_dock/show_in_explorer", TTR("Open in File Manager"));
3478 ED_SHORTCUT("filesystem_dock/open_in_external_program", TTR("Open in External Program"));
3479#endif
3480
3481 folder_colors = HashMap<String, Color>();
3482 folder_colors["red"] = Color(1.0, 0.271, 0.271);
3483 folder_colors["orange"] = Color(1.0, 0.561, 0.271);
3484 folder_colors["yellow"] = Color(1.0, 0.890, 0.271);
3485 folder_colors["green"] = Color(0.502, 1.0, 0.271);
3486 folder_colors["teal"] = Color(0.271, 1.0, 0.635);
3487 folder_colors["blue"] = Color(0.271, 0.843, 1.0);
3488 folder_colors["purple"] = Color(0.502, 0.271, 1.0);
3489 folder_colors["pink"] = Color(1.0, 0.271, 0.588);
3490 folder_colors["gray"] = Color(0.616, 0.616, 0.616);
3491
3492 assigned_folder_colors = ProjectSettings::get_singleton()->get_setting("file_customization/folder_colors");
3493
3494 editor_is_dark_theme = EditorSettings::get_singleton()->is_dark_theme();
3495
3496 VBoxContainer *top_vbc = memnew(VBoxContainer);
3497 add_child(top_vbc);
3498
3499 HBoxContainer *toolbar_hbc = memnew(HBoxContainer);
3500 toolbar_hbc->add_theme_constant_override("separation", 0);
3501 top_vbc->add_child(toolbar_hbc);
3502
3503 button_hist_prev = memnew(Button);
3504 button_hist_prev->set_flat(true);
3505 button_hist_prev->set_disabled(true);
3506 button_hist_prev->set_focus_mode(FOCUS_NONE);
3507 button_hist_prev->set_tooltip_text(TTR("Go to previous selected folder/file."));
3508 toolbar_hbc->add_child(button_hist_prev);
3509
3510 button_hist_next = memnew(Button);
3511 button_hist_next->set_flat(true);
3512 button_hist_next->set_disabled(true);
3513 button_hist_next->set_focus_mode(FOCUS_NONE);
3514 button_hist_next->set_tooltip_text(TTR("Go to next selected folder/file."));
3515 toolbar_hbc->add_child(button_hist_next);
3516
3517 current_path_line_edit = memnew(LineEdit);
3518 current_path_line_edit->set_structured_text_bidi_override(TextServer::STRUCTURED_TEXT_FILE);
3519 current_path_line_edit->set_h_size_flags(SIZE_EXPAND_FILL);
3520 _set_current_path_line_edit_text(current_path);
3521 toolbar_hbc->add_child(current_path_line_edit);
3522
3523 button_reload = memnew(Button);
3524 button_reload->connect("pressed", callable_mp(this, &FileSystemDock::_rescan));
3525 button_reload->set_focus_mode(FOCUS_NONE);
3526 button_reload->set_tooltip_text(TTR("Re-Scan Filesystem"));
3527 button_reload->hide();
3528 toolbar_hbc->add_child(button_reload);
3529
3530 button_toggle_display_mode = memnew(Button);
3531 button_toggle_display_mode->set_toggle_mode(true);
3532 button_toggle_display_mode->connect("toggled", callable_mp(this, &FileSystemDock::_toggle_split_mode));
3533 button_toggle_display_mode->set_focus_mode(FOCUS_NONE);
3534 button_toggle_display_mode->set_tooltip_text(TTR("Toggle Split Mode"));
3535 button_toggle_display_mode->set_flat(true);
3536 toolbar_hbc->add_child(button_toggle_display_mode);
3537
3538 toolbar2_hbc = memnew(HBoxContainer);
3539 toolbar2_hbc->add_theme_constant_override("separation", 0);
3540 top_vbc->add_child(toolbar2_hbc);
3541
3542 tree_search_box = memnew(LineEdit);
3543 tree_search_box->set_h_size_flags(SIZE_EXPAND_FILL);
3544 tree_search_box->set_placeholder(TTR("Filter Files"));
3545 tree_search_box->connect("text_changed", callable_mp(this, &FileSystemDock::_search_changed).bind(tree_search_box));
3546 toolbar2_hbc->add_child(tree_search_box);
3547
3548 tree_button_sort = _create_file_menu_button();
3549 toolbar2_hbc->add_child(tree_button_sort);
3550
3551 file_list_popup = memnew(PopupMenu);
3552
3553 add_child(file_list_popup);
3554
3555 tree_popup = memnew(PopupMenu);
3556
3557 add_child(tree_popup);
3558
3559 split_box = memnew(VSplitContainer);
3560 split_box->set_v_size_flags(SIZE_EXPAND_FILL);
3561 add_child(split_box);
3562
3563 tree = memnew(FileSystemTree);
3564
3565 tree->set_hide_root(true);
3566 SET_DRAG_FORWARDING_GCD(tree, FileSystemDock);
3567 tree->set_allow_rmb_select(true);
3568 tree->set_select_mode(Tree::SELECT_MULTI);
3569 tree->set_custom_minimum_size(Size2(0, 15 * EDSCALE));
3570 tree->set_column_clip_content(0, true);
3571 split_box->add_child(tree);
3572
3573 tree->connect("item_activated", callable_mp(this, &FileSystemDock::_tree_activate_file));
3574 tree->connect("multi_selected", callable_mp(this, &FileSystemDock::_tree_multi_selected));
3575 tree->connect("item_mouse_selected", callable_mp(this, &FileSystemDock::_tree_rmb_select));
3576 tree->connect("empty_clicked", callable_mp(this, &FileSystemDock::_tree_empty_click));
3577 tree->connect("nothing_selected", callable_mp(this, &FileSystemDock::_tree_empty_selected));
3578 tree->connect("gui_input", callable_mp(this, &FileSystemDock::_tree_gui_input));
3579 tree->connect("mouse_exited", callable_mp(this, &FileSystemDock::_tree_mouse_exited));
3580 tree->connect("item_edited", callable_mp(this, &FileSystemDock::_rename_operation_confirm));
3581
3582 file_list_vb = memnew(VBoxContainer);
3583 file_list_vb->set_v_size_flags(SIZE_EXPAND_FILL);
3584 split_box->add_child(file_list_vb);
3585
3586 path_hb = memnew(HBoxContainer);
3587 file_list_vb->add_child(path_hb);
3588
3589 file_list_search_box = memnew(LineEdit);
3590 file_list_search_box->set_h_size_flags(SIZE_EXPAND_FILL);
3591 file_list_search_box->set_placeholder(TTR("Filter Files"));
3592 file_list_search_box->connect("text_changed", callable_mp(this, &FileSystemDock::_search_changed).bind(file_list_search_box));
3593 path_hb->add_child(file_list_search_box);
3594
3595 file_list_button_sort = _create_file_menu_button();
3596 path_hb->add_child(file_list_button_sort);
3597
3598 button_file_list_display_mode = memnew(Button);
3599 button_file_list_display_mode->set_flat(true);
3600 path_hb->add_child(button_file_list_display_mode);
3601
3602 files = memnew(FileSystemList);
3603 files->set_v_size_flags(SIZE_EXPAND_FILL);
3604 files->set_select_mode(ItemList::SELECT_MULTI);
3605 SET_DRAG_FORWARDING_GCD(files, FileSystemDock);
3606 files->connect("item_clicked", callable_mp(this, &FileSystemDock::_file_list_item_clicked));
3607 files->connect("gui_input", callable_mp(this, &FileSystemDock::_file_list_gui_input));
3608 files->connect("multi_selected", callable_mp(this, &FileSystemDock::_file_multi_selected));
3609 files->connect("empty_clicked", callable_mp(this, &FileSystemDock::_file_list_empty_clicked));
3610 files->connect("item_edited", callable_mp(this, &FileSystemDock::_rename_operation_confirm));
3611 files->set_custom_minimum_size(Size2(0, 15 * EDSCALE));
3612 files->set_allow_rmb_select(true);
3613 file_list_vb->add_child(files);
3614
3615 scanning_vb = memnew(VBoxContainer);
3616 scanning_vb->hide();
3617 add_child(scanning_vb);
3618
3619 Label *slabel = memnew(Label);
3620 slabel->set_text(TTR("Scanning Files,\nPlease Wait..."));
3621 slabel->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
3622 scanning_vb->add_child(slabel);
3623
3624 scanning_progress = memnew(ProgressBar);
3625 scanning_vb->add_child(scanning_progress);
3626
3627 deps_editor = memnew(DependencyEditor);
3628 add_child(deps_editor);
3629
3630 owners_editor = memnew(DependencyEditorOwners());
3631 add_child(owners_editor);
3632
3633 remove_dialog = memnew(DependencyRemoveDialog);
3634 remove_dialog->connect("resource_removed", callable_mp(this, &FileSystemDock::_resource_removed));
3635 remove_dialog->connect("file_removed", callable_mp(this, &FileSystemDock::_file_removed));
3636 remove_dialog->connect("folder_removed", callable_mp(this, &FileSystemDock::_folder_removed));
3637 add_child(remove_dialog);
3638
3639 move_dialog = memnew(EditorDirDialog);
3640 add_child(move_dialog);
3641 move_dialog->connect("dir_selected", callable_mp(this, &FileSystemDock::_move_dialog_confirm));
3642
3643 overwrite_dialog = memnew(ConfirmationDialog);
3644 add_child(overwrite_dialog);
3645 overwrite_dialog->set_ok_button_text(TTR("Overwrite"));
3646 overwrite_dialog->add_button(TTR("Keep Both"), true)->connect("pressed", callable_mp(this, &FileSystemDock::_overwrite_dialog_action).bind(false));
3647 overwrite_dialog->connect("confirmed", callable_mp(this, &FileSystemDock::_overwrite_dialog_action).bind(true));
3648
3649 VBoxContainer *overwrite_dialog_vb = memnew(VBoxContainer);
3650 overwrite_dialog->add_child(overwrite_dialog_vb);
3651
3652 overwrite_dialog_header = memnew(Label);
3653 overwrite_dialog_vb->add_child(overwrite_dialog_header);
3654
3655 overwrite_dialog_scroll = memnew(ScrollContainer);
3656 overwrite_dialog_vb->add_child(overwrite_dialog_scroll);
3657 overwrite_dialog_scroll->set_custom_minimum_size(Vector2(400, 600) * EDSCALE);
3658
3659 overwrite_dialog_file_list = memnew(Label);
3660 overwrite_dialog_scroll->add_child(overwrite_dialog_file_list);
3661
3662 overwrite_dialog_footer = memnew(Label);
3663 overwrite_dialog_vb->add_child(overwrite_dialog_footer);
3664
3665 duplicate_dialog = memnew(ConfirmationDialog);
3666 VBoxContainer *duplicate_dialog_vb = memnew(VBoxContainer);
3667 duplicate_dialog->add_child(duplicate_dialog_vb);
3668
3669 duplicate_dialog_text = memnew(LineEdit);
3670 duplicate_dialog_vb->add_margin_child(TTR("Name:"), duplicate_dialog_text);
3671 duplicate_dialog->set_ok_button_text(TTR("Duplicate"));
3672 add_child(duplicate_dialog);
3673 duplicate_dialog->register_text_enter(duplicate_dialog_text);
3674 duplicate_dialog->connect("confirmed", callable_mp(this, &FileSystemDock::_duplicate_operation_confirm));
3675
3676 make_dir_dialog = memnew(DirectoryCreateDialog);
3677 add_child(make_dir_dialog);
3678 make_dir_dialog->connect("dir_created", callable_mp(this, &FileSystemDock::_rescan));
3679
3680 make_scene_dialog = memnew(SceneCreateDialog);
3681 add_child(make_scene_dialog);
3682 make_scene_dialog->connect("confirmed", callable_mp(this, &FileSystemDock::_make_scene_confirm));
3683
3684 make_script_dialog = memnew(ScriptCreateDialog);
3685 make_script_dialog->set_title(TTR("Create Script"));
3686 add_child(make_script_dialog);
3687
3688 make_shader_dialog = memnew(ShaderCreateDialog);
3689 add_child(make_shader_dialog);
3690
3691 new_resource_dialog = memnew(CreateDialog);
3692 add_child(new_resource_dialog);
3693 new_resource_dialog->set_base_type("Resource");
3694 new_resource_dialog->connect("create", callable_mp(this, &FileSystemDock::_resource_created));
3695
3696 searched_string = String();
3697 uncollapsed_paths_before_search = Vector<String>();
3698
3699 tree_update_id = 0;
3700
3701 history_pos = 0;
3702 history_max_size = 20;
3703 history.push_back("res://");
3704
3705 display_mode = DISPLAY_MODE_TREE_ONLY;
3706 old_display_mode = DISPLAY_MODE_TREE_ONLY;
3707 file_list_display_mode = FILE_LIST_DISPLAY_THUMBNAILS;
3708
3709 add_resource_tooltip_plugin(memnew(EditorTextureTooltipPlugin));
3710}
3711
3712FileSystemDock::~FileSystemDock() {
3713 singleton = nullptr;
3714}
3715