Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
godotengine
GitHub Repository: godotengine/godot
Path: blob/master/editor/docks/filesystem_dock.cpp
20843 views
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/input/input.h"
35
#include "core/io/dir_access.h"
36
#include "core/io/file_access.h"
37
#include "core/io/resource_loader.h"
38
#include "core/os/keyboard.h"
39
#include "core/os/os.h"
40
#include "core/templates/list.h"
41
#include "editor/docks/editor_dock_manager.h"
42
#include "editor/docks/import_dock.h"
43
#include "editor/docks/scene_tree_dock.h"
44
#include "editor/editor_node.h"
45
#include "editor/editor_string_names.h"
46
#include "editor/editor_undo_redo_manager.h"
47
#include "editor/file_system/dependency_editor.h"
48
#include "editor/gui/create_dialog.h"
49
#include "editor/gui/directory_create_dialog.h"
50
#include "editor/gui/editor_dir_dialog.h"
51
#include "editor/import/3d/scene_import_settings.h"
52
#include "editor/inspector/editor_context_menu_plugin.h"
53
#include "editor/inspector/editor_resource_preview.h"
54
#include "editor/inspector/editor_resource_tooltip_plugins.h"
55
#include "editor/plugins/editor_resource_conversion_plugin.h"
56
#include "editor/scene/editor_scene_tabs.h"
57
#include "editor/scene/scene_create_dialog.h"
58
#include "editor/settings/editor_command_palette.h"
59
#include "editor/settings/editor_feature_profile.h"
60
#include "editor/settings/editor_settings.h"
61
#include "editor/shader/shader_create_dialog.h"
62
#include "editor/themes/editor_scale.h"
63
#include "editor/themes/editor_theme_manager.h"
64
#include "scene/gui/box_container.h"
65
#include "scene/gui/item_list.h"
66
#include "scene/gui/label.h"
67
#include "scene/gui/line_edit.h"
68
#include "scene/gui/progress_bar.h"
69
#include "scene/resources/packed_scene.h"
70
#include "servers/display/display_server.h"
71
72
Control *FileSystemTree::make_custom_tooltip(const String &p_text) const {
73
TreeItem *item = get_item_at_position(get_local_mouse_position());
74
if (!item) {
75
return nullptr;
76
}
77
return FileSystemDock::get_singleton()->create_tooltip_for_path(item->get_metadata(0));
78
}
79
80
Control *FileSystemList::make_custom_tooltip(const String &p_text) const {
81
int idx = get_item_at_position(get_local_mouse_position(), true);
82
if (idx == -1) {
83
return nullptr;
84
}
85
return FileSystemDock::get_singleton()->create_tooltip_for_path(get_item_metadata(idx));
86
}
87
88
void FileSystemList::_line_editor_submit(const String &p_text) {
89
if (popup_edit_committed) {
90
return; // Already processed by _text_editor_popup_modal_close
91
}
92
93
if (popup_editor->get_hide_reason() == Popup::HIDE_REASON_CANCELED) {
94
return; // ESC pressed, app focus lost, or forced close from code.
95
}
96
97
popup_edit_committed = true; // End edit popup processing.
98
popup_editor->hide();
99
100
emit_signal(SNAME("item_edited"));
101
queue_redraw();
102
}
103
104
bool FileSystemList::edit_selected() {
105
ERR_FAIL_COND_V_MSG(!is_anything_selected(), false, "No item selected.");
106
int s = get_current();
107
ERR_FAIL_COND_V_MSG(s < 0, false, "No current item selected.");
108
ensure_current_is_visible();
109
110
Rect2 rect;
111
Rect2 popup_rect;
112
Vector2 ofs;
113
114
Vector2 icon_size = get_fixed_icon_size() * get_icon_scale();
115
116
// Handles the different icon modes (TOP/LEFT).
117
switch (get_icon_mode()) {
118
case ItemList::ICON_MODE_LEFT:
119
rect = get_item_rect(s, true);
120
if (get_v_scroll_bar()->is_visible()) {
121
rect.position.y -= get_v_scroll_bar()->get_value();
122
}
123
if (get_h_scroll_bar()->is_visible()) {
124
rect.position.x -= get_h_scroll_bar()->get_value();
125
}
126
ofs = Vector2(0, Math::floor((MAX(line_editor->get_minimum_size().height, rect.size.height) - rect.size.height) / 2));
127
popup_rect.position = rect.position - ofs;
128
popup_rect.size = rect.size;
129
130
// Adjust for icon position and size.
131
popup_rect.size.x -= MAX(theme_cache.h_separation, 0) / 2 + icon_size.x;
132
popup_rect.position.x += MAX(theme_cache.h_separation, 0) / 2 + icon_size.x;
133
break;
134
case ItemList::ICON_MODE_TOP:
135
rect = get_item_rect(s, false);
136
if (get_v_scroll_bar()->is_visible()) {
137
rect.position.y -= get_v_scroll_bar()->get_value();
138
}
139
if (get_h_scroll_bar()->is_visible()) {
140
rect.position.x -= get_h_scroll_bar()->get_value();
141
}
142
popup_rect.position = rect.position;
143
popup_rect.size = rect.size;
144
145
// Adjust for icon position and size.
146
popup_rect.size.y -= MAX(theme_cache.v_separation, 0) / 2 + theme_cache.icon_margin + icon_size.y;
147
popup_rect.position.y += MAX(theme_cache.v_separation, 0) / 2 + theme_cache.icon_margin + icon_size.y;
148
break;
149
}
150
if (is_layout_rtl()) {
151
popup_rect.position.x = get_size().width - popup_rect.position.x - popup_rect.size.x;
152
}
153
popup_rect.position += get_screen_position();
154
155
popup_editor->set_position(popup_rect.position);
156
popup_editor->set_size(popup_rect.size);
157
158
String name = get_item_text(s);
159
line_editor->set_text(name);
160
line_editor->select(0, name.rfind_char('.'));
161
162
popup_edit_committed = false; // Start edit popup processing.
163
popup_editor->popup();
164
popup_editor->child_controls_changed();
165
line_editor->grab_focus();
166
return true;
167
}
168
169
String FileSystemList::get_edit_text() {
170
return line_editor->get_text();
171
}
172
173
void FileSystemList::_text_editor_popup_modal_close() {
174
if (popup_edit_committed) {
175
return; // Already processed by _text_editor_popup_modal_close
176
}
177
178
if (popup_editor->get_hide_reason() == Popup::HIDE_REASON_CANCELED) {
179
return; // ESC pressed, app focus lost, or forced close from code.
180
}
181
182
_line_editor_submit(line_editor->get_text());
183
}
184
185
void FileSystemList::_bind_methods() {
186
ADD_SIGNAL(MethodInfo("item_edited"));
187
}
188
189
FileSystemList::FileSystemList() {
190
set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
191
192
popup_editor = memnew(Popup);
193
add_child(popup_editor);
194
195
popup_editor_vb = memnew(VBoxContainer);
196
popup_editor_vb->add_theme_constant_override("separation", 0);
197
popup_editor_vb->set_anchors_and_offsets_preset(PRESET_FULL_RECT);
198
popup_editor->add_child(popup_editor_vb);
199
200
line_editor = memnew(LineEdit);
201
line_editor->set_v_size_flags(SIZE_EXPAND_FILL);
202
popup_editor_vb->add_child(line_editor);
203
line_editor->connect(SceneStringName(text_submitted), callable_mp(this, &FileSystemList::_line_editor_submit));
204
popup_editor->connect("popup_hide", callable_mp(this, &FileSystemList::_text_editor_popup_modal_close));
205
}
206
207
Ref<Texture2D> FileSystemDock::_get_tree_item_icon(bool p_is_valid, const String &p_file_type, const String &p_icon_path) {
208
if (!p_icon_path.is_empty()) {
209
Ref<Texture2D> icon = ResourceLoader::load(p_icon_path);
210
if (icon.is_valid()) {
211
return icon;
212
}
213
}
214
215
if (!p_is_valid) {
216
return get_editor_theme_icon(SNAME("ImportFail"));
217
} else if (has_theme_icon(p_file_type, EditorStringName(EditorIcons))) {
218
return get_editor_theme_icon(p_file_type);
219
} else {
220
return get_editor_theme_icon(SNAME("File"));
221
}
222
}
223
224
void FileSystemDock::_create_tree(TreeItem *p_parent, EditorFileSystemDirectory *p_dir, Vector<String> &uncollapsed_paths, bool p_select_in_favorites, bool p_unfold_path) {
225
// Create a tree item for the subdirectory.
226
TreeItem *subdirectory_item = tree->create_item(p_parent);
227
String dname = p_dir->get_name();
228
String lpath = p_dir->get_path();
229
230
if (dname.is_empty()) {
231
dname = "res://";
232
resources_item = subdirectory_item;
233
}
234
235
// Set custom folder color (if applicable).
236
bool has_custom_color = assigned_folder_colors.has(lpath);
237
Color custom_color = has_custom_color ? folder_colors[assigned_folder_colors[lpath]] : Color();
238
Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);
239
240
if (has_custom_color) {
241
subdirectory_item->set_icon_modulate(0, editor_is_dark_icon_and_font ? custom_color : custom_color * ITEM_COLOR_SCALE);
242
subdirectory_item->set_custom_bg_color(0, Color(custom_color, editor_is_dark_icon_and_font ? ITEM_ALPHA_MIN : ITEM_ALPHA_MAX));
243
} else {
244
TreeItem *parent = subdirectory_item->get_parent();
245
if (parent) {
246
Color parent_bg_color = parent->get_custom_bg_color(0);
247
if (parent_bg_color != Color()) {
248
bool parent_has_custom_color = assigned_folder_colors.has(parent->get_metadata(0));
249
subdirectory_item->set_custom_bg_color(0, parent_has_custom_color ? parent_bg_color.darkened(ITEM_BG_DARK_SCALE) : parent_bg_color);
250
subdirectory_item->set_icon_modulate(0, parent->get_icon_modulate(0));
251
} else {
252
subdirectory_item->set_icon_modulate(0, get_theme_color(SNAME("folder_icon_color"), SNAME("FileDialog")));
253
}
254
}
255
}
256
257
subdirectory_item->set_text(0, dname);
258
subdirectory_item->set_structured_text_bidi_override(0, TextServer::STRUCTURED_TEXT_FILE);
259
subdirectory_item->set_icon(0, get_editor_theme_icon(SNAME("Folder")));
260
if (da->is_link(lpath)) {
261
subdirectory_item->set_icon_overlay(0, get_editor_theme_icon(SNAME("LinkOverlay")));
262
subdirectory_item->set_tooltip_text(0, vformat(TTR("Link to: %s"), da->read_link(lpath)));
263
}
264
subdirectory_item->set_selectable(0, true);
265
subdirectory_item->set_metadata(0, lpath);
266
folder_map[lpath] = subdirectory_item;
267
268
if (!p_select_in_favorites && (current_path == lpath || ((display_mode != DISPLAY_MODE_TREE_ONLY) && current_path.get_base_dir() == lpath))) {
269
subdirectory_item->select(0);
270
// Keep select an item when re-created a tree
271
// To prevent crashing when nothing is selected.
272
subdirectory_item->set_as_cursor(0);
273
}
274
275
if (p_unfold_path && current_path.begins_with(lpath) && current_path != lpath) {
276
subdirectory_item->set_collapsed(false);
277
} else {
278
subdirectory_item->set_collapsed(!uncollapsed_paths.has(lpath));
279
}
280
281
// Create items for all subdirectories.
282
bool reversed = file_sort == FileSortOption::FILE_SORT_NAME_REVERSE;
283
for (int i = reversed ? p_dir->get_subdir_count() - 1 : 0;
284
reversed ? i >= 0 : i < p_dir->get_subdir_count();
285
reversed ? i-- : i++) {
286
_create_tree(subdirectory_item, p_dir->get_subdir(i), uncollapsed_paths, p_select_in_favorites, p_unfold_path);
287
}
288
289
// Create all items for the files in the subdirectory.
290
if (display_mode == DISPLAY_MODE_TREE_ONLY) {
291
// Build the list of the files to display.
292
List<FileInfo> file_list;
293
for (int i = 0; i < p_dir->get_file_count(); i++) {
294
String file_type = p_dir->get_file_type(i);
295
if (_is_file_type_disabled_by_feature_profile(file_type)) {
296
// If type is disabled, file won't be displayed.
297
continue;
298
}
299
300
FileInfo file_info;
301
file_info.name = p_dir->get_file(i);
302
file_info.type = p_dir->get_file_type(i);
303
file_info.icon_path = p_dir->get_file_icon_path(i);
304
file_info.import_broken = !p_dir->get_file_import_is_valid(i);
305
file_info.modified_time = p_dir->get_file_modified_time(i);
306
307
file_list.push_back(file_info);
308
}
309
310
// Sort the file list if needed.
311
sort_file_info_list(file_list, file_sort);
312
313
// Build the tree.
314
const int icon_size = get_theme_constant(SNAME("class_icon_size"), EditorStringName(Editor));
315
316
for (const FileInfo &file_info : file_list) {
317
TreeItem *file_item = tree->create_item(subdirectory_item);
318
const String file_metadata = lpath.path_join(file_info.name);
319
file_item->set_text(0, file_info.name);
320
file_item->set_structured_text_bidi_override(0, TextServer::STRUCTURED_TEXT_FILE);
321
file_item->set_icon(0, _get_tree_item_icon(!file_info.import_broken, file_info.type, file_info.icon_path));
322
if (da->is_link(file_metadata)) {
323
file_item->set_icon_overlay(0, get_editor_theme_icon(SNAME("LinkOverlay")));
324
// TRANSLATORS: This is a tooltip for a file that is a symbolic link to another file.
325
file_item->set_tooltip_text(0, vformat(TTR("Link to: %s"), da->read_link(file_metadata)));
326
}
327
file_item->set_icon_max_width(0, icon_size);
328
Color parent_bg_color = subdirectory_item->get_custom_bg_color(0);
329
if (has_custom_color) {
330
file_item->set_custom_bg_color(0, parent_bg_color.darkened(ITEM_BG_DARK_SCALE));
331
} else if (parent_bg_color != Color()) {
332
file_item->set_custom_bg_color(0, parent_bg_color);
333
}
334
file_item->set_metadata(0, file_metadata);
335
if (!p_select_in_favorites && current_path == file_metadata) {
336
file_item->select(0);
337
file_item->set_as_cursor(0);
338
}
339
if (main_scene_path == file_metadata) {
340
file_item->set_custom_color(0, get_theme_color(SNAME("accent_color"), EditorStringName(Editor)));
341
}
342
EditorResourcePreview::get_singleton()->queue_resource_preview(file_metadata, callable_mp(this, &FileSystemDock::_tree_thumbnail_done).bind(tree_update_id, file_item->get_instance_id()));
343
}
344
} else {
345
if (lpath.get_base_dir() == current_path.get_base_dir()) {
346
subdirectory_item->select(0);
347
subdirectory_item->set_as_cursor(0);
348
}
349
}
350
}
351
352
Vector<String> FileSystemDock::get_uncollapsed_paths() const {
353
Vector<String> uncollapsed_paths;
354
TreeItem *root = tree->get_root();
355
if (root) {
356
if (!favorites_item->is_collapsed()) {
357
uncollapsed_paths.push_back(favorites_item->get_metadata(0));
358
}
359
360
// BFS to find all uncollapsed paths of the resource directory.
361
TreeItem *res_subtree = root->get_first_child()->get_next();
362
if (res_subtree) {
363
List<TreeItem *> queue;
364
queue.push_back(res_subtree);
365
366
while (!queue.is_empty()) {
367
TreeItem *ti = queue.back()->get();
368
queue.pop_back();
369
if (!ti->is_collapsed() && ti->get_child_count() > 0) {
370
Variant path = ti->get_metadata(0);
371
if (path) {
372
uncollapsed_paths.push_back(path);
373
}
374
}
375
for (int i = 0; i < ti->get_child_count(); i++) {
376
queue.push_back(ti->get_child(i));
377
}
378
}
379
}
380
}
381
return uncollapsed_paths;
382
}
383
384
void FileSystemDock::_update_tree(const Vector<String> &p_uncollapsed_paths, bool p_uncollapse_root, bool p_scroll_to_selected) {
385
// Recreate the tree.
386
tree->clear();
387
tree_update_id++;
388
updating_tree = true;
389
TreeItem *root = tree->create_item();
390
folder_map.clear();
391
392
// Handles the favorites.
393
favorites_item = tree->create_item(root);
394
favorites_item->set_icon(0, get_editor_theme_icon(SNAME("Favorites")));
395
favorites_item->set_text(0, TTRC("Favorites:"));
396
favorites_item->set_auto_translate_mode(0, AUTO_TRANSLATE_MODE_ALWAYS);
397
favorites_item->set_metadata(0, "Favorites");
398
favorites_item->set_collapsed(!p_uncollapsed_paths.has("Favorites"));
399
400
Vector<String> favorite_paths = EditorSettings::get_singleton()->get_favorites();
401
402
Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);
403
bool fav_changed = false;
404
for (int i = favorite_paths.size() - 1; i >= 0; i--) {
405
if (da->dir_exists(favorite_paths[i]) || da->file_exists(favorite_paths[i])) {
406
continue;
407
}
408
favorite_paths.remove_at(i);
409
fav_changed = true;
410
}
411
if (fav_changed) {
412
EditorSettings::get_singleton()->set_favorites(favorite_paths);
413
// Setting favorites causes the tree to update, so continuing is redundant.
414
return;
415
}
416
417
Ref<Texture2D> folder_icon = get_editor_theme_icon(SNAME("Folder"));
418
const Color default_folder_color = get_theme_color(SNAME("folder_icon_color"), SNAME("FileDialog"));
419
420
const int icon_size = get_theme_constant(SNAME("class_icon_size"), EditorStringName(Editor));
421
for (const String &favorite : favorite_paths) {
422
if (!favorite.begins_with("res://")) {
423
continue;
424
}
425
426
String text;
427
Ref<Texture2D> icon;
428
Color color;
429
if (favorite == "res://") {
430
text = "/";
431
icon = folder_icon;
432
color = default_folder_color;
433
} else if (favorite.ends_with("/")) {
434
text = favorite.substr(0, favorite.length() - 1).get_file();
435
icon = folder_icon;
436
color = FileSystemDock::get_dir_icon_color(favorite, default_folder_color);
437
} else {
438
text = favorite.get_file();
439
int index;
440
EditorFileSystemDirectory *dir = EditorFileSystem::get_singleton()->find_file(favorite, &index);
441
if (dir) {
442
icon = _get_tree_item_icon(dir->get_file_import_is_valid(index), dir->get_file_type(index), dir->get_file_icon_path(index));
443
} else {
444
icon = get_editor_theme_icon(SNAME("File"));
445
}
446
color = Color(1, 1, 1);
447
}
448
449
TreeItem *ti = tree->create_item(favorites_item);
450
ti->set_text(0, text);
451
ti->set_icon(0, icon);
452
ti->set_icon_modulate(0, color);
453
ti->set_icon_max_width(0, icon_size);
454
ti->set_tooltip_text(0, favorite);
455
ti->set_selectable(0, true);
456
ti->set_metadata(0, favorite);
457
458
if (!favorite.ends_with("/")) {
459
EditorResourcePreview::get_singleton()->queue_resource_preview(favorite, callable_mp(this, &FileSystemDock::_tree_thumbnail_done).bind(tree_update_id, ti->get_instance_id()));
460
}
461
}
462
463
Vector<String> uncollapsed_paths = p_uncollapsed_paths;
464
if (p_uncollapse_root) {
465
uncollapsed_paths.push_back("res://");
466
}
467
468
// Create the remaining of the tree.
469
_create_tree(root, EditorFileSystem::get_singleton()->get_filesystem(), uncollapsed_paths, false);
470
if (!searched_tokens.is_empty()) {
471
_update_filtered_items();
472
}
473
474
if (p_scroll_to_selected) {
475
tree->ensure_cursor_is_visible();
476
}
477
478
updating_tree = false;
479
}
480
481
void FileSystemDock::set_display_mode(DisplayMode p_display_mode) {
482
display_mode = p_display_mode;
483
_update_display_mode(false);
484
}
485
486
void FileSystemDock::_update_display_mode(bool p_force) {
487
// Compute the new display mode.
488
if (p_force || old_display_mode != display_mode) {
489
switch (display_mode) {
490
case DISPLAY_MODE_TREE_ONLY: {
491
button_toggle_display_mode->set_button_icon(get_editor_theme_icon(SNAME("Panels1")));
492
tree->show();
493
tree->set_v_size_flags(SIZE_EXPAND_FILL);
494
tree->set_theme_type_variation("");
495
if (horizontal) {
496
toolbar2_hbc->hide();
497
498
tree->set_scroll_hint_mode(Tree::SCROLL_HINT_MODE_BOTH);
499
tree_mc->set_theme_type_variation("NoBorderBottomPanel");
500
} else {
501
toolbar2_hbc->show();
502
503
tree->set_scroll_hint_mode(Tree::SCROLL_HINT_MODE_TOP);
504
tree_mc->set_theme_type_variation("NoBorderHorizontalBottom");
505
}
506
button_file_list_display_mode->hide();
507
508
_update_tree(get_uncollapsed_paths());
509
file_list_vb->hide();
510
} break;
511
512
case DISPLAY_MODE_HSPLIT:
513
case DISPLAY_MODE_VSPLIT: {
514
const bool is_vertical = display_mode == DISPLAY_MODE_VSPLIT;
515
split_box->set_vertical(is_vertical);
516
517
const int actual_offset = is_vertical ? split_box_offset_v : split_box_offset_h;
518
split_box->set_split_offset(actual_offset);
519
const StringName icon = is_vertical ? SNAME("Panels2") : SNAME("Panels2Alt");
520
button_toggle_display_mode->set_button_icon(get_editor_theme_icon(icon));
521
522
tree->show();
523
tree->set_v_size_flags(SIZE_EXPAND_FILL);
524
if (is_vertical) {
525
tree->set_theme_type_variation("");
526
tree->set_scroll_hint_mode(Tree::SCROLL_HINT_MODE_BOTH);
527
tree_mc->set_theme_type_variation(horizontal ? "NoBorderBottomPanel" : "NoBorderHorizontal");
528
529
files->set_theme_type_variation(horizontal ? "ItemListSecondary" : "");
530
files->set_scroll_hint_mode(horizontal ? ItemList::SCROLL_HINT_MODE_DISABLED : ItemList::SCROLL_HINT_MODE_TOP);
531
files_mc->set_theme_type_variation(horizontal ? "" : "NoBorderHorizontalBottom");
532
} else {
533
tree->set_theme_type_variation("TreeSecondary");
534
tree->set_scroll_hint_mode(Tree::SCROLL_HINT_MODE_DISABLED);
535
tree_mc->set_theme_type_variation("");
536
537
files->set_theme_type_variation("ItemListSecondary");
538
files->set_scroll_hint_mode(ItemList::SCROLL_HINT_MODE_DISABLED);
539
files_mc->set_theme_type_variation("");
540
}
541
tree->ensure_cursor_is_visible();
542
543
toolbar2_hbc->hide();
544
button_file_list_display_mode->show();
545
_update_tree(get_uncollapsed_paths());
546
547
file_list_vb->show();
548
_update_file_list(true);
549
} break;
550
}
551
552
old_display_mode = display_mode;
553
}
554
}
555
556
void FileSystemDock::_notification(int p_what) {
557
switch (p_what) {
558
case NOTIFICATION_READY: {
559
EditorFeatureProfileManager::get_singleton()->connect("current_feature_profile_changed", callable_mp(this, &FileSystemDock::_feature_profile_changed));
560
EditorFileSystem::get_singleton()->connect("filesystem_changed", callable_mp(this, &FileSystemDock::_fs_changed));
561
EditorResourcePreview::get_singleton()->connect("preview_invalidated", callable_mp(this, &FileSystemDock::_preview_invalidated));
562
563
button_file_list_display_mode->connect(SceneStringName(pressed), callable_mp(this, &FileSystemDock::_toggle_file_display));
564
files->connect("item_activated", callable_mp(this, &FileSystemDock::_file_list_activate_file));
565
button_hist_next->connect(SceneStringName(pressed), callable_mp(this, &FileSystemDock::_fw_history));
566
button_hist_prev->connect(SceneStringName(pressed), callable_mp(this, &FileSystemDock::_bw_history));
567
file_list_popup->connect(SceneStringName(id_pressed), callable_mp(this, &FileSystemDock::_file_list_rmb_option));
568
tree_popup->connect(SceneStringName(id_pressed), callable_mp(this, &FileSystemDock::_tree_rmb_option));
569
current_path_line_edit->connect(SceneStringName(text_submitted), callable_mp(this, &FileSystemDock::_navigate_to_path).bind(false, true));
570
571
always_show_folders = bool(EDITOR_GET("docks/filesystem/always_show_folders"));
572
thumbnail_size_setting = EDITOR_GET("docks/filesystem/thumbnail_size");
573
574
set_file_list_display_mode(FileSystemDock::FILE_LIST_DISPLAY_LIST);
575
576
_update_display_mode();
577
578
if (EditorFileSystem::get_singleton()->is_scanning()) {
579
_set_scanning_mode();
580
} else {
581
_update_tree(Vector<String>(), true);
582
}
583
} break;
584
585
case NOTIFICATION_PROCESS: {
586
if (EditorFileSystem::get_singleton()->is_scanning()) {
587
scanning_progress->set_value(EditorFileSystem::get_singleton()->get_scanning_progress() * 100.0f);
588
}
589
} break;
590
591
case NOTIFICATION_DRAG_BEGIN: {
592
Dictionary dd = get_viewport()->gui_get_drag_data();
593
if (tree->is_visible_in_tree() && dd.has("type")) {
594
if (dd.has("favorite")) {
595
if ((String(dd["favorite"]) == "all")) {
596
tree->set_drop_mode_flags(Tree::DROP_MODE_INBETWEEN);
597
}
598
} else if ((String(dd["type"]) == "files") || (String(dd["type"]) == "files_and_dirs")) {
599
tree->set_drop_mode_flags(Tree::DROP_MODE_ON_ITEM | Tree::DROP_MODE_INBETWEEN);
600
} else if ((String(dd["type"]) == "nodes") || (String(dd["type"]) == "resource")) {
601
holding_branch = true;
602
TreeItem *item = tree->get_next_selected(tree->get_root());
603
while (item) {
604
tree_items_selected_on_drag_begin.push_back(item);
605
item = tree->get_next_selected(item);
606
}
607
list_items_selected_on_drag_begin = files->get_selected_items();
608
}
609
}
610
} break;
611
612
case NOTIFICATION_DRAG_END: {
613
tree->set_drop_mode_flags(0);
614
615
if (holding_branch) {
616
holding_branch = false;
617
_reselect_items_selected_on_drag_begin(true);
618
}
619
} break;
620
621
case NOTIFICATION_TRANSLATION_CHANGED:
622
case NOTIFICATION_LAYOUT_DIRECTION_CHANGED:
623
case NOTIFICATION_THEME_CHANGED: {
624
_update_display_mode(true);
625
626
StringName mode_icon = "Panels1";
627
if (display_mode == DISPLAY_MODE_VSPLIT) {
628
mode_icon = "Panels2";
629
} else if (display_mode == DISPLAY_MODE_HSPLIT) {
630
mode_icon = "Panels2Alt";
631
}
632
button_toggle_display_mode->set_button_icon(get_editor_theme_icon(mode_icon));
633
634
if (file_list_display_mode == FILE_LIST_DISPLAY_LIST) {
635
button_file_list_display_mode->set_button_icon(get_editor_theme_icon(SNAME("FileThumbnail")));
636
} else {
637
button_file_list_display_mode->set_button_icon(get_editor_theme_icon(SNAME("FileList")));
638
}
639
640
tree_search_box->set_right_icon(get_editor_theme_icon(SNAME("Search")));
641
tree_button_sort->set_button_icon(get_editor_theme_icon(SNAME("Sort")));
642
643
file_list_search_box->set_right_icon(get_editor_theme_icon(SNAME("Search")));
644
file_list_button_sort->set_button_icon(get_editor_theme_icon(SNAME("Sort")));
645
646
if (is_layout_rtl()) {
647
button_hist_next->set_button_icon(get_editor_theme_icon(SNAME("Back")));
648
button_hist_prev->set_button_icon(get_editor_theme_icon(SNAME("Forward")));
649
} else {
650
button_hist_next->set_button_icon(get_editor_theme_icon(SNAME("Forward")));
651
button_hist_prev->set_button_icon(get_editor_theme_icon(SNAME("Back")));
652
}
653
654
overwrite_dialog_scroll->add_theme_style_override(SceneStringName(panel), get_theme_stylebox(SceneStringName(panel), "Tree"));
655
} break;
656
657
case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {
658
// Update editor dark theme & always show folders states from editor settings, redraw if needed.
659
bool do_redraw = false;
660
661
bool new_editor_is_dark_icon_and_font = EditorThemeManager::is_dark_icon_and_font();
662
if (new_editor_is_dark_icon_and_font != editor_is_dark_icon_and_font) {
663
editor_is_dark_icon_and_font = new_editor_is_dark_icon_and_font;
664
do_redraw = true;
665
}
666
667
bool new_always_show_folders = bool(EDITOR_GET("docks/filesystem/always_show_folders"));
668
if (new_always_show_folders != always_show_folders) {
669
always_show_folders = new_always_show_folders;
670
do_redraw = true;
671
}
672
673
int new_thumbnail_size_setting = EDITOR_GET("docks/filesystem/thumbnail_size");
674
if (new_thumbnail_size_setting != thumbnail_size_setting) {
675
thumbnail_size_setting = new_thumbnail_size_setting;
676
do_redraw = true;
677
}
678
679
if (do_redraw) {
680
update_all();
681
}
682
683
if (EditorThemeManager::is_generated_theme_outdated()) {
684
// Change full tree mode.
685
_update_display_mode();
686
}
687
} break;
688
}
689
}
690
691
void FileSystemDock::_tree_multi_selected(Object *p_item, int p_column, bool p_selected) {
692
// Update the import dock.
693
import_dock_needs_update = true;
694
callable_mp(this, &FileSystemDock::_update_import_dock).call_deferred();
695
696
// Return if we don't select something new.
697
if (!p_selected) {
698
return;
699
}
700
701
// Tree item selected.
702
TreeItem *selected = tree->get_selected();
703
if (!selected) {
704
return;
705
}
706
707
if (selected->get_parent() == favorites_item && !String(selected->get_metadata(0)).ends_with("/")) {
708
// Go to the favorites if we click in the favorites and the path has changed.
709
current_path = "Favorites";
710
} else {
711
current_path = selected->get_metadata(0);
712
// Note: the "Favorites" item also leads to this path.
713
}
714
715
// Display the current path.
716
_set_current_path_line_edit_text(current_path);
717
_push_to_history();
718
719
// Update the file list.
720
if (!updating_tree && display_mode != DISPLAY_MODE_TREE_ONLY) {
721
_update_file_list(false);
722
}
723
}
724
725
Vector<String> FileSystemDock::get_selected_paths() const {
726
if (display_mode == DISPLAY_MODE_TREE_ONLY) {
727
return _tree_get_selected(false);
728
} else {
729
Vector<String> selected = _file_list_get_selected();
730
if (selected.is_empty()) {
731
selected.push_back(get_current_directory());
732
}
733
return selected;
734
}
735
}
736
737
String FileSystemDock::get_current_path() const {
738
return current_path;
739
}
740
741
String FileSystemDock::get_current_directory() const {
742
if (current_path.ends_with("/")) {
743
return current_path;
744
} else {
745
return current_path.get_base_dir();
746
}
747
}
748
749
void FileSystemDock::_set_current_path_line_edit_text(const String &p_path) {
750
if (p_path == "Favorites") {
751
current_path_line_edit->set_text(TTR("Favorites"));
752
} else {
753
current_path_line_edit->set_text(current_path);
754
}
755
}
756
757
void FileSystemDock::_navigate_to_path(const String &p_path, bool p_select_in_favorites, bool p_grab_focus) {
758
String target_path = p_path;
759
bool is_directory = false;
760
761
if (p_path.is_empty()) {
762
target_path = "res://";
763
is_directory = true;
764
} else if (p_path != "Favorites") {
765
Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);
766
if (da->dir_exists(p_path)) {
767
is_directory = true;
768
if (!p_path.ends_with("/")) {
769
target_path += "/";
770
}
771
} else if (!da->file_exists(p_path)) {
772
ERR_FAIL_MSG(vformat("Cannot navigate to '%s' as it has not been found in the file system!", p_path));
773
}
774
}
775
776
current_path = target_path;
777
_set_current_path_line_edit_text(current_path);
778
_push_to_history();
779
780
String base_dir_path = target_path.get_base_dir();
781
if (base_dir_path != "res://") {
782
base_dir_path += "/";
783
}
784
785
TreeItem **directory_ptr = folder_map.getptr(base_dir_path);
786
if (!directory_ptr) {
787
return;
788
}
789
790
// Unfold all folders along the path.
791
TreeItem *ti = *directory_ptr;
792
while (ti) {
793
ti->set_collapsed(false);
794
ti = ti->get_parent();
795
}
796
797
// Select the file or directory in the tree.
798
tree->deselect_all();
799
if (display_mode == DISPLAY_MODE_TREE_ONLY) {
800
// Either search for 'folder/' or '/file.ext'.
801
const String file_name = is_directory ? target_path.trim_suffix("/").get_file() + "/" : "/" + target_path.get_file();
802
TreeItem *item = is_directory ? *directory_ptr : (*directory_ptr)->get_first_child();
803
while (item) {
804
if (item->get_metadata(0).operator String().ends_with(file_name)) {
805
item->select(0);
806
break;
807
}
808
item = item->get_next();
809
}
810
if (p_grab_focus) {
811
tree->grab_focus(true);
812
}
813
} else {
814
(*directory_ptr)->select(0);
815
_update_file_list(false);
816
if (p_grab_focus) {
817
files->grab_focus(true);
818
}
819
}
820
tree->ensure_cursor_is_visible();
821
}
822
823
bool FileSystemDock::_update_filtered_items(TreeItem *p_tree_item) {
824
TreeItem *item = p_tree_item;
825
if (!item) {
826
item = tree->get_root();
827
}
828
ERR_FAIL_NULL_V(item, false);
829
830
bool keep_visible = false;
831
for (TreeItem *child = item->get_first_child(); child; child = child->get_next()) {
832
keep_visible = _update_filtered_items(child) || keep_visible;
833
}
834
835
if (searched_tokens.is_empty()) {
836
item->set_visible(true);
837
// Always uncollapse root (the hidden item above res:// and favorites).
838
item->set_collapsed(item != tree->get_root() && !uncollapsed_paths_before_search.has(item->get_metadata(0)));
839
return true;
840
}
841
842
if (keep_visible) {
843
item->set_collapsed(false);
844
} else {
845
// res:// and favorites are always visible.
846
keep_visible = item == resources_item || item == favorites_item;
847
keep_visible = keep_visible || _matches_all_search_tokens(item->get_text(0));
848
}
849
item->set_visible(keep_visible);
850
return keep_visible;
851
}
852
853
void FileSystemDock::navigate_to_path(const String &p_path) {
854
file_list_search_box->clear();
855
// Try to set the FileSystem dock visible.
856
EditorDockManager::get_singleton()->focus_dock(this);
857
_navigate_to_path(p_path, false, is_visible_in_tree());
858
859
import_dock_needs_update = true;
860
_update_import_dock();
861
}
862
863
void FileSystemDock::_file_list_thumbnail_done(const String &p_path, const Ref<Texture2D> &p_preview, const Ref<Texture2D> &p_small_preview, int p_index, const String &p_filename) {
864
if (p_preview.is_valid()) {
865
if (p_index < files->get_item_count() && files->get_item_text(p_index) == p_filename && files->get_item_metadata(p_index) == p_path) {
866
Ref<Texture2D> thumbnail;
867
868
if (file_list_display_mode == FILE_LIST_DISPLAY_LIST) {
869
thumbnail = p_small_preview;
870
} else {
871
thumbnail = p_preview;
872
}
873
874
if (thumbnail.is_valid()) {
875
files->set_item_icon(p_index, _apply_thumbnail_filter(thumbnail, p_path));
876
}
877
}
878
}
879
}
880
881
void FileSystemDock::_tree_thumbnail_done(const String &p_path, const Ref<Texture2D> &p_preview, const Ref<Texture2D> &p_small_preview, int p_update_id, ObjectID p_item) {
882
TreeItem *item = ObjectDB::get_instance<TreeItem>(p_item);
883
if (item && tree_update_id == p_update_id && p_small_preview.is_valid()) {
884
item->set_icon(0, _apply_thumbnail_filter(p_small_preview, p_path));
885
}
886
}
887
888
Ref<Texture2D> FileSystemDock::_apply_thumbnail_filter(const Ref<Texture2D> &p_thumbnail, const String &p_file_path) const {
889
if (!p_file_path.is_empty()) {
890
int index;
891
EditorFileSystemDirectory *dir = EditorFileSystem::get_singleton()->find_file(p_file_path, &index);
892
893
if (dir) {
894
if (dir->get_file_import_is_valid(index)) {
895
const StringName &file_type = dir->get_file_type(index);
896
897
if (file_type == SNAME("CompressedTexture2D") || file_type == SNAME("Image")) {
898
const String extension = p_file_path.get_extension();
899
900
if (extension != "svg" && extension != "svgz") {
901
Ref<CanvasTexture> thumbnail_wrapped;
902
thumbnail_wrapped.instantiate();
903
thumbnail_wrapped->set_diffuse_texture(p_thumbnail);
904
thumbnail_wrapped->set_texture_filter(CanvasItem::TextureFilter::TEXTURE_FILTER_NEAREST_WITH_MIPMAPS);
905
return thumbnail_wrapped;
906
}
907
}
908
}
909
}
910
}
911
912
return p_thumbnail;
913
}
914
915
void FileSystemDock::_toggle_file_display() {
916
_set_file_display(file_list_display_mode != FILE_LIST_DISPLAY_LIST);
917
emit_signal(SNAME("display_mode_changed"));
918
}
919
920
void FileSystemDock::_set_file_display(bool p_active) {
921
if (p_active) {
922
file_list_display_mode = FILE_LIST_DISPLAY_LIST;
923
button_file_list_display_mode->set_button_icon(get_editor_theme_icon(SNAME("FileThumbnail")));
924
button_file_list_display_mode->set_tooltip_text(TTRC("View items as a grid of thumbnails."));
925
} else {
926
file_list_display_mode = FILE_LIST_DISPLAY_THUMBNAILS;
927
button_file_list_display_mode->set_button_icon(get_editor_theme_icon(SNAME("FileList")));
928
button_file_list_display_mode->set_tooltip_text(TTRC("View items as a list."));
929
}
930
931
_update_file_list(true);
932
}
933
934
bool FileSystemDock::_is_file_type_disabled_by_feature_profile(const StringName &p_class) {
935
Ref<EditorFeatureProfile> profile = EditorFeatureProfileManager::get_singleton()->get_current_profile();
936
if (profile.is_null() || !ClassDB::class_exists(p_class)) {
937
return false;
938
}
939
940
StringName class_name = p_class;
941
942
while (class_name != StringName()) {
943
if (profile->is_class_disabled(class_name)) {
944
return true;
945
}
946
class_name = ClassDB::get_parent_class(class_name);
947
}
948
949
return false;
950
}
951
952
void FileSystemDock::_search(EditorFileSystemDirectory *p_path, List<FileInfo> *matches, int p_max_items) {
953
if (matches->size() > p_max_items) {
954
return;
955
}
956
957
for (int i = 0; i < p_path->get_subdir_count(); i++) {
958
_search(p_path->get_subdir(i), matches, p_max_items);
959
}
960
961
for (int i = 0; i < p_path->get_file_count(); i++) {
962
String file = p_path->get_file(i);
963
964
if (_matches_all_search_tokens(file)) {
965
FileInfo file_info;
966
file_info.name = file;
967
file_info.type = p_path->get_file_type(i);
968
file_info.path = p_path->get_file_path(i);
969
file_info.import_broken = !p_path->get_file_import_is_valid(i);
970
file_info.modified_time = p_path->get_file_modified_time(i);
971
972
if (_is_file_type_disabled_by_feature_profile(file_info.type)) {
973
// This type is disabled, will not appear here.
974
continue;
975
}
976
977
matches->push_back(file_info);
978
if (matches->size() > p_max_items) {
979
return;
980
}
981
}
982
}
983
}
984
985
void FileSystemDock::_update_file_list(bool p_keep_selection) {
986
// Register the previously current and selected items.
987
HashSet<String> previous_selection;
988
HashSet<int> valid_selection;
989
if (p_keep_selection) {
990
for (int i = 0; i < files->get_item_count(); i++) {
991
if (files->is_selected(i)) {
992
previous_selection.insert(files->get_item_text(i));
993
}
994
}
995
}
996
997
files->clear();
998
999
_set_current_path_line_edit_text(current_path);
1000
1001
String directory = current_path;
1002
String file = "";
1003
1004
int thumbnail_size = thumbnail_size_setting * EDSCALE;
1005
Ref<Texture2D> folder_thumbnail;
1006
Ref<Texture2D> file_thumbnail;
1007
Ref<Texture2D> file_thumbnail_broken;
1008
1009
bool use_thumbnails = (file_list_display_mode == FILE_LIST_DISPLAY_THUMBNAILS);
1010
1011
if (use_thumbnails) {
1012
// Thumbnails mode.
1013
files->set_max_columns(0);
1014
files->set_icon_mode(ItemList::ICON_MODE_TOP);
1015
files->set_fixed_column_width(thumbnail_size * 3 / 2);
1016
files->set_max_text_lines(2);
1017
files->set_fixed_icon_size(Size2(thumbnail_size, thumbnail_size));
1018
1019
const int icon_size = get_theme_constant(SNAME("class_icon_size"), EditorStringName(Editor));
1020
files->set_fixed_tag_icon_size(Size2(icon_size, icon_size));
1021
1022
if (thumbnail_size < 64) {
1023
folder_thumbnail = get_editor_theme_icon(SNAME("FolderMediumThumb"));
1024
file_thumbnail = get_editor_theme_icon(SNAME("FileMediumThumb"));
1025
file_thumbnail_broken = get_editor_theme_icon(SNAME("FileDeadMediumThumb"));
1026
} else {
1027
folder_thumbnail = get_editor_theme_icon(SNAME("FolderBigThumb"));
1028
file_thumbnail = get_editor_theme_icon(SNAME("FileBigThumb"));
1029
file_thumbnail_broken = get_editor_theme_icon(SNAME("FileDeadBigThumb"));
1030
}
1031
} else {
1032
// No thumbnails.
1033
files->set_icon_mode(ItemList::ICON_MODE_LEFT);
1034
files->set_max_columns(1);
1035
files->set_max_text_lines(1);
1036
files->set_fixed_column_width(0);
1037
const int icon_size = get_theme_constant(SNAME("class_icon_size"), EditorStringName(Editor));
1038
files->set_fixed_icon_size(Size2(icon_size, icon_size));
1039
}
1040
1041
Ref<Texture2D> folder_icon = (use_thumbnails) ? folder_thumbnail : get_theme_icon(SNAME("folder"), SNAME("FileDialog"));
1042
const Color default_folder_color = get_theme_color(SNAME("folder_icon_color"), SNAME("FileDialog"));
1043
1044
// Build the FileInfo list.
1045
List<FileInfo> file_list;
1046
if (current_path == "Favorites") {
1047
// Display the favorites.
1048
Vector<String> favorites_list = EditorSettings::get_singleton()->get_favorites();
1049
for (const String &favorite : favorites_list) {
1050
if (!favorite.begins_with("res://")) {
1051
continue;
1052
}
1053
String text;
1054
Ref<Texture2D> icon;
1055
if (favorite == "res://") {
1056
text = "/";
1057
icon = folder_icon;
1058
if (searched_tokens.is_empty() || _matches_all_search_tokens(text)) {
1059
files->add_item(text, icon, true);
1060
files->set_item_metadata(-1, favorite);
1061
}
1062
} else if (favorite.ends_with("/")) {
1063
text = favorite.substr(0, favorite.length() - 1).get_file();
1064
icon = folder_icon;
1065
if (searched_tokens.is_empty() || _matches_all_search_tokens(text)) {
1066
files->add_item(text, icon, true);
1067
files->set_item_metadata(-1, favorite);
1068
}
1069
} else {
1070
int index;
1071
EditorFileSystemDirectory *efd = EditorFileSystem::get_singleton()->find_file(favorite, &index);
1072
1073
FileInfo file_info;
1074
file_info.name = favorite.get_file();
1075
file_info.path = favorite;
1076
if (efd) {
1077
file_info.type = efd->get_file_type(index);
1078
file_info.icon_path = efd->get_file_icon_path(index);
1079
file_info.import_broken = !efd->get_file_import_is_valid(index);
1080
file_info.modified_time = efd->get_file_modified_time(index);
1081
} else {
1082
file_info.type = "";
1083
file_info.import_broken = true;
1084
file_info.modified_time = 0;
1085
}
1086
1087
if (searched_tokens.is_empty() || _matches_all_search_tokens(file_info.name)) {
1088
file_list.push_back(file_info);
1089
}
1090
}
1091
}
1092
} else {
1093
if (!directory.begins_with("res://")) {
1094
directory = "res://" + directory;
1095
}
1096
// Get infos on the directory + file.
1097
if (directory.ends_with("/") && directory != "res://") {
1098
directory = directory.substr(0, directory.length() - 1);
1099
}
1100
EditorFileSystemDirectory *efd = EditorFileSystem::get_singleton()->get_filesystem_path(directory);
1101
if (!efd) {
1102
directory = current_path.get_base_dir();
1103
file = current_path.get_file();
1104
efd = EditorFileSystem::get_singleton()->get_filesystem_path(directory);
1105
}
1106
if (!efd) {
1107
return;
1108
}
1109
1110
if (!searched_tokens.is_empty()) {
1111
// Display the search results.
1112
// Limit the number of results displayed to avoid an infinite loop.
1113
_search(EditorFileSystem::get_singleton()->get_filesystem(), &file_list, 10000);
1114
} else {
1115
if (display_mode == DISPLAY_MODE_TREE_ONLY || always_show_folders) {
1116
// Check for a folder color to inherit (if one is assigned).
1117
Color inherited_folder_color = default_folder_color;
1118
String color_scan_dir = directory;
1119
while (color_scan_dir != "res://" && inherited_folder_color == default_folder_color) {
1120
if (!color_scan_dir.ends_with("/")) {
1121
color_scan_dir += "/";
1122
}
1123
1124
if (assigned_folder_colors.has(color_scan_dir)) {
1125
inherited_folder_color = folder_colors[assigned_folder_colors[color_scan_dir]];
1126
}
1127
1128
color_scan_dir = color_scan_dir.rstrip("/").get_base_dir();
1129
}
1130
1131
// Display folders in the list.
1132
if (directory != "res://") {
1133
files->add_item("..", folder_icon, true);
1134
1135
String bd = directory.get_base_dir();
1136
if (bd != "res://" && !bd.ends_with("/")) {
1137
bd += "/";
1138
}
1139
1140
files->set_item_metadata(-1, bd);
1141
files->set_item_selectable(-1, false);
1142
if (!editor_is_dark_icon_and_font && inherited_folder_color != default_folder_color) {
1143
files->set_item_icon_modulate(-1, inherited_folder_color * ITEM_COLOR_SCALE);
1144
} else {
1145
files->set_item_icon_modulate(-1, inherited_folder_color);
1146
}
1147
}
1148
1149
bool reversed = file_sort == FileSortOption::FILE_SORT_NAME_REVERSE;
1150
for (int i = reversed ? efd->get_subdir_count() - 1 : 0;
1151
reversed ? i >= 0 : i < efd->get_subdir_count();
1152
reversed ? i-- : i++) {
1153
String dname = efd->get_subdir(i)->get_name();
1154
String dpath = directory.path_join(dname) + "/";
1155
bool has_custom_color = assigned_folder_colors.has(dpath);
1156
1157
files->add_item(dname, folder_icon, true);
1158
files->set_item_metadata(-1, dpath);
1159
Color this_folder_color = has_custom_color ? folder_colors[assigned_folder_colors[dpath]] : inherited_folder_color;
1160
if (!editor_is_dark_icon_and_font && this_folder_color != default_folder_color) {
1161
this_folder_color *= ITEM_COLOR_SCALE;
1162
}
1163
files->set_item_icon_modulate(-1, this_folder_color);
1164
1165
if (previous_selection.has(dname)) {
1166
files->select(files->get_item_count() - 1, false);
1167
valid_selection.insert(files->get_item_count() - 1);
1168
}
1169
}
1170
}
1171
1172
// Display the folder content.
1173
for (int i = 0; i < efd->get_file_count(); i++) {
1174
FileInfo file_info;
1175
file_info.name = efd->get_file(i);
1176
file_info.path = directory.path_join(file_info.name);
1177
file_info.type = efd->get_file_type(i);
1178
file_info.icon_path = efd->get_file_icon_path(i);
1179
file_info.import_broken = !efd->get_file_import_is_valid(i);
1180
file_info.modified_time = efd->get_file_modified_time(i);
1181
1182
file_list.push_back(file_info);
1183
}
1184
}
1185
}
1186
1187
// Sort the file list if needed.
1188
sort_file_info_list(file_list, file_sort);
1189
1190
// Fills the ItemList control node from the FileInfos.
1191
for (FileInfo &E : file_list) {
1192
FileInfo *finfo = &(E);
1193
String fname = finfo->name;
1194
String fpath = finfo->path;
1195
1196
Ref<Texture2D> type_icon;
1197
Ref<Texture2D> big_icon;
1198
1199
String tooltip = fpath;
1200
1201
// Select the icons.
1202
type_icon = _get_tree_item_icon(!finfo->import_broken, finfo->type, finfo->icon_path);
1203
if (!finfo->import_broken) {
1204
big_icon = file_thumbnail;
1205
} else {
1206
big_icon = file_thumbnail_broken;
1207
tooltip += "\n" + TTR("Status: Import of file failed. Please fix file and reimport manually.");
1208
}
1209
1210
// Add the item to the ItemList.
1211
int item_index;
1212
if (use_thumbnails) {
1213
files->add_item(fname, big_icon, true);
1214
item_index = files->get_item_count() - 1;
1215
files->set_item_metadata(item_index, fpath);
1216
files->set_item_tag_icon(item_index, type_icon);
1217
1218
} else {
1219
files->add_item(fname, type_icon, true);
1220
item_index = files->get_item_count() - 1;
1221
files->set_item_metadata(item_index, fpath);
1222
}
1223
1224
if (fpath == main_scene_path) {
1225
files->set_item_custom_fg_color(item_index, get_theme_color(SNAME("accent_color"), EditorStringName(Editor)));
1226
}
1227
1228
// Generate the preview.
1229
if (!finfo->import_broken) {
1230
EditorResourcePreview::get_singleton()->queue_resource_preview(fpath, callable_mp(this, &FileSystemDock::_file_list_thumbnail_done).bind(item_index, fname));
1231
}
1232
1233
// Select the items.
1234
if (previous_selection.has(fname)) {
1235
files->select(item_index, false);
1236
valid_selection.insert(item_index);
1237
}
1238
1239
if (!p_keep_selection && !file.is_empty() && fname == file) {
1240
files->select(item_index, true);
1241
files->ensure_current_is_visible();
1242
}
1243
1244
// Tooltip.
1245
if (finfo->sources.size()) {
1246
for (int j = 0; j < finfo->sources.size(); j++) {
1247
tooltip += "\nSource: " + finfo->sources[j];
1248
}
1249
}
1250
files->set_item_tooltip(item_index, tooltip);
1251
}
1252
1253
// If we only have any selected items retained, we need to update the current idx.
1254
if (!valid_selection.is_empty()) {
1255
files->set_current(*valid_selection.begin());
1256
}
1257
}
1258
1259
HashSet<String> FileSystemDock::_get_valid_conversions_for_file_paths(const Vector<String> &p_paths) {
1260
HashSet<String> all_valid_conversion_to_targets;
1261
for (const String &fpath : p_paths) {
1262
if (fpath.is_empty() || fpath == "res://" || !FileAccess::exists(fpath) || FileAccess::exists(fpath + ".import")) {
1263
return HashSet<String>();
1264
}
1265
1266
Vector<Ref<EditorResourceConversionPlugin>> conversions = EditorNode::get_singleton()->find_resource_conversion_plugin_for_type_name(EditorFileSystem::get_singleton()->get_file_type(fpath));
1267
1268
if (conversions.is_empty()) {
1269
// This resource can't convert to anything, so return an empty list.
1270
return HashSet<String>();
1271
}
1272
1273
// Get a list of all potential conversion-to targets.
1274
HashSet<String> current_valid_conversion_to_targets;
1275
for (const Ref<EditorResourceConversionPlugin> &E : conversions) {
1276
const String what = E->converts_to();
1277
current_valid_conversion_to_targets.insert(what);
1278
}
1279
1280
if (all_valid_conversion_to_targets.is_empty()) {
1281
// If we have no existing valid conversions, this is the first one, so copy them directly.
1282
all_valid_conversion_to_targets = current_valid_conversion_to_targets;
1283
} else {
1284
// Check existing conversion targets and remove any which are not in the current list.
1285
for (const String &S : all_valid_conversion_to_targets) {
1286
if (!current_valid_conversion_to_targets.has(S)) {
1287
all_valid_conversion_to_targets.erase(S);
1288
}
1289
}
1290
// We have no more remaining valid conversions, so break the loop.
1291
if (all_valid_conversion_to_targets.is_empty()) {
1292
break;
1293
}
1294
}
1295
}
1296
1297
return all_valid_conversion_to_targets;
1298
}
1299
1300
void FileSystemDock::_select_file(const String &p_path, bool p_select_in_favorites, bool p_navigate) {
1301
String fpath = p_path;
1302
if (fpath.ends_with("/")) {
1303
// Ignore a directory.
1304
} else if (fpath != "Favorites") {
1305
if (FileAccess::exists(fpath + ".import")) {
1306
Ref<ConfigFile> config;
1307
config.instantiate();
1308
Error err = config->load(fpath + ".import");
1309
if (err == OK) {
1310
if (config->has_section_key("remap", "importer")) {
1311
String importer = config->get_value("remap", "importer");
1312
if (importer == "keep" || importer == "skip") {
1313
EditorNode::get_singleton()->show_warning(TTRC("Importing has been disabled for this file, so it can't be opened for editing."));
1314
return;
1315
}
1316
}
1317
}
1318
}
1319
1320
String resource_type = ResourceLoader::get_resource_type(fpath);
1321
if (resource_type == "PackedScene" || resource_type == "AnimationLibrary") {
1322
bool is_imported = false;
1323
{
1324
List<String> importer_exts;
1325
ResourceImporterScene::get_scene_importer_extensions(&importer_exts);
1326
String extension = fpath.get_extension();
1327
for (const String &E : importer_exts) {
1328
if (extension.nocasecmp_to(E) == 0) {
1329
is_imported = true;
1330
break;
1331
}
1332
}
1333
}
1334
1335
if (is_imported) {
1336
SceneImportSettingsDialog::get_singleton()->open_settings(p_path, resource_type);
1337
} else {
1338
EditorNode::get_singleton()->load_scene_or_resource(fpath);
1339
}
1340
} else if (ResourceLoader::is_imported(fpath)) {
1341
// If the importer has advanced settings, show them.
1342
int order;
1343
bool can_threads;
1344
String name;
1345
Error err = ResourceFormatImporter::get_singleton()->get_import_order_threads_and_importer(fpath, order, can_threads, name);
1346
bool used_advanced_settings = false;
1347
if (err == OK) {
1348
Ref<ResourceImporter> importer = ResourceFormatImporter::get_singleton()->get_importer_by_name(name);
1349
if (importer.is_valid() && importer->has_advanced_options()) {
1350
importer->show_advanced_options(fpath);
1351
used_advanced_settings = true;
1352
}
1353
}
1354
1355
if (!used_advanced_settings) {
1356
EditorNode::get_singleton()->load_resource(fpath);
1357
}
1358
} else {
1359
EditorNode::get_singleton()->load_resource(fpath);
1360
}
1361
}
1362
if (p_navigate) {
1363
_navigate_to_path(fpath, p_select_in_favorites);
1364
}
1365
}
1366
1367
void FileSystemDock::_tree_activate_file() {
1368
TreeItem *selected = tree->get_selected();
1369
if (selected) {
1370
String file_path = selected->get_metadata(0);
1371
TreeItem *parent = selected->get_parent();
1372
bool is_favorite = parent != nullptr && parent->get_metadata(0) == "Favorites";
1373
bool is_folder = file_path.ends_with("/");
1374
1375
if ((!is_favorite && is_folder) || file_path == "Favorites") {
1376
bool collapsed = selected->is_collapsed();
1377
selected->set_collapsed(!collapsed);
1378
} else {
1379
_select_file(file_path, is_favorite && !is_folder, is_favorite && is_folder);
1380
}
1381
}
1382
}
1383
1384
void FileSystemDock::_file_list_activate_file(int p_idx) {
1385
_select_file(files->get_item_metadata(p_idx));
1386
}
1387
1388
void FileSystemDock::_preview_invalidated(const String &p_path) {
1389
if (file_list_display_mode == FILE_LIST_DISPLAY_THUMBNAILS && p_path.get_base_dir() == current_path && searched_tokens.is_empty() && file_list_vb->is_visible_in_tree()) {
1390
for (int i = 0; i < files->get_item_count(); i++) {
1391
if (files->get_item_metadata(i) == p_path) {
1392
// Re-request preview.
1393
EditorResourcePreview::get_singleton()->queue_resource_preview(p_path, callable_mp(this, &FileSystemDock::_file_list_thumbnail_done).bind(i, files->get_item_text(i)));
1394
break;
1395
}
1396
}
1397
}
1398
}
1399
1400
void FileSystemDock::_fs_changed() {
1401
button_hist_prev->set_disabled(history_pos == 0);
1402
button_hist_next->set_disabled(history_pos == history.size() - 1);
1403
scanning_vb->hide();
1404
split_box->show();
1405
1406
update_all();
1407
1408
if (!select_after_scan.is_empty()) {
1409
_navigate_to_path(select_after_scan);
1410
select_after_scan.clear();
1411
import_dock_needs_update = true;
1412
_update_import_dock();
1413
}
1414
1415
set_process(false);
1416
if (had_focus) {
1417
had_focus->grab_focus();
1418
had_focus = nullptr;
1419
}
1420
}
1421
1422
void FileSystemDock::_set_scanning_mode() {
1423
button_hist_prev->set_disabled(true);
1424
button_hist_next->set_disabled(true);
1425
split_box->hide();
1426
scanning_vb->show();
1427
set_process(true);
1428
if (EditorFileSystem::get_singleton()->is_scanning()) {
1429
scanning_progress->set_value(EditorFileSystem::get_singleton()->get_scanning_progress() * 100);
1430
} else {
1431
scanning_progress->set_value(0);
1432
}
1433
}
1434
1435
void FileSystemDock::_fw_history() {
1436
if (history_pos < history.size() - 1) {
1437
history_pos++;
1438
}
1439
1440
_update_history();
1441
}
1442
1443
void FileSystemDock::_bw_history() {
1444
if (history_pos > 0) {
1445
history_pos--;
1446
}
1447
1448
_update_history();
1449
}
1450
1451
void FileSystemDock::_update_history() {
1452
current_path = history[history_pos];
1453
_set_current_path_line_edit_text(current_path);
1454
1455
if (tree->is_visible()) {
1456
_update_tree(get_uncollapsed_paths());
1457
tree->grab_focus(true);
1458
}
1459
1460
if (file_list_vb->is_visible()) {
1461
_update_file_list(false);
1462
}
1463
1464
button_hist_prev->set_disabled(history_pos == 0);
1465
button_hist_next->set_disabled(history_pos == history.size() - 1);
1466
}
1467
1468
void FileSystemDock::_push_to_history() {
1469
if (history[history_pos] != current_path) {
1470
history.resize(history_pos + 1);
1471
history.push_back(current_path);
1472
history_pos++;
1473
1474
if (history.size() > history_max_size) {
1475
history.remove_at(0);
1476
history_pos = history_max_size - 1;
1477
}
1478
}
1479
1480
button_hist_prev->set_disabled(history_pos == 0);
1481
button_hist_next->set_disabled(history_pos == history.size() - 1);
1482
}
1483
1484
void FileSystemDock::_get_all_items_in_dir(EditorFileSystemDirectory *p_efsd, Vector<String> &r_files, Vector<String> &r_folders) const {
1485
if (p_efsd == nullptr) {
1486
return;
1487
}
1488
1489
for (int i = 0; i < p_efsd->get_subdir_count(); i++) {
1490
r_folders.push_back(p_efsd->get_subdir(i)->get_path());
1491
_get_all_items_in_dir(p_efsd->get_subdir(i), r_files, r_folders);
1492
}
1493
for (int i = 0; i < p_efsd->get_file_count(); i++) {
1494
r_files.push_back(p_efsd->get_file_path(i));
1495
}
1496
}
1497
1498
void FileSystemDock::_find_file_owners(EditorFileSystemDirectory *p_efsd, const HashSet<String> &p_renames, HashSet<String> &r_file_owners) const {
1499
for (int i = 0; i < p_efsd->get_subdir_count(); i++) {
1500
_find_file_owners(p_efsd->get_subdir(i), p_renames, r_file_owners);
1501
}
1502
for (int i = 0; i < p_efsd->get_file_count(); i++) {
1503
Vector<String> deps = p_efsd->get_file_deps(i);
1504
for (int j = 0; j < deps.size(); j++) {
1505
if (p_renames.has(deps[j])) {
1506
r_file_owners.insert(p_efsd->get_file_path(i));
1507
break;
1508
}
1509
}
1510
}
1511
}
1512
1513
void FileSystemDock::_try_move_item(const FileOrFolder &p_item, const String &p_new_path,
1514
HashMap<String, String> &p_file_renames, HashMap<String, String> &p_folder_renames) {
1515
// Ensure folder paths end with "/".
1516
String old_path = (p_item.is_file || p_item.path.ends_with("/")) ? p_item.path : (p_item.path + "/");
1517
String new_path = (p_item.is_file || p_new_path.ends_with("/")) ? p_new_path : (p_new_path + "/");
1518
1519
if (new_path == old_path) {
1520
return;
1521
} else if (old_path == "res://") {
1522
EditorNode::get_singleton()->add_io_error(TTR("Cannot move/rename resources root."));
1523
return;
1524
} else if (!p_item.is_file && new_path.begins_with(old_path)) {
1525
// This check doesn't erroneously catch renaming to a longer name as folder paths always end with "/".
1526
EditorNode::get_singleton()->add_io_error(TTR("Cannot move a folder into itself.") + "\n" + old_path + "\n");
1527
return;
1528
}
1529
1530
// Build a list of files which will have new paths as a result of this operation.
1531
Vector<String> file_changed_paths;
1532
Vector<String> folder_changed_paths;
1533
if (p_item.is_file) {
1534
file_changed_paths.push_back(old_path);
1535
} else {
1536
folder_changed_paths.push_back(old_path);
1537
_get_all_items_in_dir(EditorFileSystem::get_singleton()->get_filesystem_path(old_path), file_changed_paths, folder_changed_paths);
1538
}
1539
1540
Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);
1541
print_verbose("Moving " + old_path + " -> " + new_path);
1542
Error err = da->rename(old_path, new_path);
1543
if (err == OK) {
1544
// Move/Rename any corresponding import settings too.
1545
if (p_item.is_file && FileAccess::exists(old_path + ".import")) {
1546
err = da->rename(old_path + ".import", new_path + ".import");
1547
if (err != OK) {
1548
EditorNode::get_singleton()->add_io_error(TTR("Error moving:") + "\n" + old_path + ".import\n");
1549
}
1550
}
1551
1552
if (p_item.is_file && FileAccess::exists(old_path + ".uid")) {
1553
err = da->rename(old_path + ".uid", new_path + ".uid");
1554
if (err != OK) {
1555
EditorNode::get_singleton()->add_io_error(TTR("Error moving:") + "\n" + old_path + ".uid\n");
1556
}
1557
}
1558
1559
// Update scene if it is open.
1560
for (int i = 0; i < file_changed_paths.size(); ++i) {
1561
String new_item_path = p_item.is_file ? new_path : file_changed_paths[i].replace_first(old_path, new_path);
1562
if (ResourceLoader::get_resource_type(new_item_path) == "PackedScene" && EditorNode::get_singleton()->is_scene_open(file_changed_paths[i])) {
1563
EditorData *ed = &EditorNode::get_editor_data();
1564
for (int j = 0; j < ed->get_edited_scene_count(); j++) {
1565
if (ed->get_scene_path(j) == file_changed_paths[i]) {
1566
ed->get_edited_scene_root(j)->set_scene_file_path(new_item_path);
1567
EditorNode::get_singleton()->save_editor_layout_delayed();
1568
break;
1569
}
1570
}
1571
}
1572
}
1573
1574
// Only treat as a changed dependency if it was successfully moved.
1575
for (int i = 0; i < file_changed_paths.size(); ++i) {
1576
p_file_renames[file_changed_paths[i]] = file_changed_paths[i].replace_first(old_path, new_path);
1577
print_verbose(" Remap: " + file_changed_paths[i] + " -> " + p_file_renames[file_changed_paths[i]]);
1578
emit_signal(SNAME("files_moved"), file_changed_paths[i], p_file_renames[file_changed_paths[i]]);
1579
}
1580
for (int i = 0; i < folder_changed_paths.size(); ++i) {
1581
p_folder_renames[folder_changed_paths[i]] = folder_changed_paths[i].replace_first(old_path, new_path);
1582
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));
1583
}
1584
} else {
1585
EditorNode::get_singleton()->add_io_error(TTR("Error moving:") + "\n" + old_path + "\n");
1586
}
1587
}
1588
1589
void FileSystemDock::_try_duplicate_item(const FileOrFolder &p_item, const String &p_new_path) const {
1590
// Ensure folder paths end with "/".
1591
String old_path = (p_item.is_file || p_item.path.ends_with("/")) ? p_item.path : (p_item.path + "/");
1592
String new_path = (p_item.is_file || p_new_path.ends_with("/")) ? p_new_path : (p_new_path + "/");
1593
1594
if (new_path == old_path) {
1595
return;
1596
} else if (old_path == "res://") {
1597
EditorNode::get_singleton()->add_io_error(TTR("Cannot move/rename resources root."));
1598
return;
1599
} else if (!p_item.is_file && new_path.begins_with(old_path)) {
1600
// This check doesn't erroneously catch renaming to a longer name as folder paths always end with "/".
1601
EditorNode::get_singleton()->add_io_error(TTR("Cannot move a folder into itself.") + "\n" + old_path + "\n");
1602
return;
1603
}
1604
1605
if (p_item.is_file) {
1606
print_verbose("Duplicating " + old_path + " -> " + new_path);
1607
1608
// Create the directory structure.
1609
EditorFileSystem::get_singleton()->make_dir_recursive(p_new_path.get_base_dir());
1610
1611
Error err = EditorFileSystem::get_singleton()->copy_file(old_path, new_path);
1612
if (err != OK) {
1613
EditorNode::get_singleton()->add_io_error(TTR("Error duplicating:") + "\n" + old_path + U" → " + new_path + ": " + TTR(error_names[err]) + "\n");
1614
}
1615
} else {
1616
Error err = EditorFileSystem::get_singleton()->copy_directory(old_path, new_path);
1617
if (err != OK) {
1618
EditorNode::get_singleton()->add_io_error(TTR("Error duplicating directory:") + "\n" + old_path + U" → " + new_path + ": " + TTR(error_names[err]) + "\n");
1619
}
1620
}
1621
}
1622
1623
void FileSystemDock::_update_resource_paths_after_move(const HashMap<String, String> &p_renames, const HashMap<String, ResourceUID::ID> &p_uids) const {
1624
for (const KeyValue<String, String> &pair : p_renames) {
1625
// Update UID path.
1626
const String &old_path = pair.key;
1627
const String &new_path = pair.value;
1628
1629
const HashMap<String, ResourceUID::ID>::ConstIterator I = p_uids.find(old_path);
1630
if (I) {
1631
ResourceUID::get_singleton()->set_id(I->value, new_path);
1632
}
1633
EditorFileSystem::get_singleton()->register_global_class_script(old_path, new_path);
1634
}
1635
1636
// Rename all resources loaded, be it subresources or actual resources.
1637
List<Ref<Resource>> cached;
1638
ResourceCache::get_cached_resources(&cached);
1639
1640
for (Ref<Resource> &r : cached) {
1641
String base_path = r->get_path();
1642
String extra_path;
1643
int sep_pos = r->get_path().find("::");
1644
if (sep_pos >= 0) {
1645
extra_path = base_path.substr(sep_pos);
1646
base_path = base_path.substr(0, sep_pos);
1647
}
1648
1649
if (p_renames.has(base_path)) {
1650
base_path = p_renames[base_path];
1651
r->set_path(base_path + extra_path);
1652
}
1653
}
1654
1655
EditorNode::get_editor_data().script_class_save_global_classes();
1656
EditorFileSystem::get_singleton()->emit_signal(SNAME("script_classes_updated"));
1657
}
1658
1659
void FileSystemDock::_update_dependencies_after_move(const HashMap<String, String> &p_renames, const HashSet<String> &p_file_owners) const {
1660
// The following code assumes that the following holds:
1661
// 1) EditorFileSystem contains the old paths/folder structure from before the rename/move.
1662
// 2) ResourceLoader can use the new paths without needing to call rescan.
1663
1664
// The currently edited scene should be reloaded first, so get it's path (GH-82652).
1665
const String &edited_scene_path = EditorNode::get_editor_data().get_scene_path(EditorNode::get_editor_data().get_edited_scene());
1666
List<String> scenes_to_reload;
1667
for (const String &E : p_file_owners) {
1668
// Because we haven't called a rescan yet the found remap might still be an old path itself.
1669
const HashMap<String, String>::ConstIterator I = p_renames.find(E);
1670
const String file = I ? I->value : E;
1671
print_verbose("Remapping dependencies for: " + file);
1672
const Error err = ResourceLoader::rename_dependencies(file, p_renames);
1673
if (err == OK) {
1674
if (ResourceLoader::get_resource_type(file) == "PackedScene") {
1675
if (file == edited_scene_path) {
1676
scenes_to_reload.push_front(file);
1677
} else {
1678
scenes_to_reload.push_back(file);
1679
}
1680
}
1681
} else {
1682
EditorNode::get_singleton()->add_io_error(TTR("Unable to update dependencies for:") + "\n" + E + "\n");
1683
}
1684
}
1685
1686
for (const String &E : scenes_to_reload) {
1687
EditorNode::get_singleton()->reload_scene(E);
1688
}
1689
}
1690
1691
void FileSystemDock::_update_project_settings_after_move(const HashMap<String, String> &p_renames, const HashMap<String, String> &p_folders_renames) {
1692
// Find all project settings of type FILE and replace them if needed.
1693
const HashMap<StringName, PropertyInfo> prop_info(ProjectSettings::get_singleton()->get_custom_property_info());
1694
for (const KeyValue<StringName, PropertyInfo> &E : prop_info) {
1695
if (E.value.hint == PROPERTY_HINT_FILE || E.value.hint == PROPERTY_HINT_FILE_PATH) {
1696
String old_path = GLOBAL_GET(E.key);
1697
if (p_renames.has(old_path)) {
1698
ProjectSettings::get_singleton()->set_setting(E.key, p_renames[old_path]);
1699
}
1700
};
1701
}
1702
1703
// Also search for the file in autoload, as they are stored differently from normal files.
1704
List<PropertyInfo> property_list;
1705
ProjectSettings::get_singleton()->get_property_list(&property_list);
1706
for (const PropertyInfo &E : property_list) {
1707
if (E.name.begins_with("autoload/")) {
1708
// If the autoload resource paths has a leading "*", it indicates that it is a Singleton,
1709
// so we have to handle both cases when updating.
1710
String autoload = GLOBAL_GET(E.name);
1711
String autoload_singleton = autoload.substr(1);
1712
if (p_renames.has(autoload)) {
1713
ProjectSettings::get_singleton()->set_setting(E.name, p_renames[autoload]);
1714
} else if (autoload.begins_with("*") && p_renames.has(autoload_singleton)) {
1715
ProjectSettings::get_singleton()->set_setting(E.name, "*" + p_renames[autoload_singleton]);
1716
}
1717
}
1718
}
1719
1720
// Update folder colors.
1721
for (const KeyValue<String, String> &rename : p_folders_renames) {
1722
if (assigned_folder_colors.has(rename.key)) {
1723
assigned_folder_colors[rename.value] = assigned_folder_colors[rename.key];
1724
assigned_folder_colors.erase(rename.key);
1725
}
1726
}
1727
ProjectSettings::get_singleton()->save();
1728
}
1729
1730
String FileSystemDock::_get_unique_name(const FileOrFolder &p_entry, const String &p_at_path) {
1731
String new_path;
1732
String new_path_base;
1733
1734
if (p_entry.is_file) {
1735
new_path = p_at_path.path_join(p_entry.path.get_file());
1736
new_path_base = new_path.get_basename() + " (%d)." + new_path.get_extension();
1737
} else {
1738
PackedStringArray path_split = p_entry.path.split("/");
1739
new_path = p_at_path.path_join(path_split[path_split.size() - 2]);
1740
new_path_base = new_path + " (%d)";
1741
}
1742
1743
int exist_counter = 1;
1744
Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);
1745
while (da->file_exists(new_path) || da->dir_exists(new_path)) {
1746
exist_counter++;
1747
new_path = vformat(new_path_base, exist_counter);
1748
}
1749
1750
return new_path;
1751
}
1752
1753
void FileSystemDock::_update_favorites_after_move(const HashMap<String, String> &p_files_renames, const HashMap<String, String> &p_folders_renames) const {
1754
Vector<String> favorite_files = EditorSettings::get_singleton()->get_favorites();
1755
Vector<String> new_favorite_files;
1756
for (const String &old_path : favorite_files) {
1757
if (p_folders_renames.has(old_path)) {
1758
new_favorite_files.push_back(p_folders_renames[old_path]);
1759
} else if (p_files_renames.has(old_path)) {
1760
new_favorite_files.push_back(p_files_renames[old_path]);
1761
} else {
1762
new_favorite_files.push_back(old_path);
1763
}
1764
}
1765
EditorSettings::get_singleton()->set_favorites(new_favorite_files);
1766
1767
HashMap<String, PackedStringArray> favorite_properties = EditorSettings::get_singleton()->get_favorite_properties();
1768
for (const KeyValue<String, String> &KV : p_files_renames) {
1769
if (favorite_properties.has(KV.key)) {
1770
favorite_properties.replace_key(KV.key, KV.value);
1771
}
1772
}
1773
EditorSettings::get_singleton()->set_favorite_properties(favorite_properties);
1774
}
1775
1776
void FileSystemDock::_make_scene_confirm() {
1777
const String scene_path = make_scene_dialog->get_scene_path();
1778
1779
int idx = EditorNode::get_singleton()->new_scene();
1780
EditorNode::get_editor_data().set_scene_path(idx, scene_path);
1781
EditorNode::get_singleton()->set_edited_scene(make_scene_dialog->create_scene_root());
1782
EditorNode::get_singleton()->save_scene_if_open(scene_path);
1783
}
1784
1785
void FileSystemDock::_resource_removed(const Ref<Resource> &p_resource) {
1786
const Ref<Script> &scr = p_resource;
1787
if (scr.is_valid()) {
1788
ScriptServer::remove_global_class_by_path(scr->get_path());
1789
EditorNode::get_editor_data().script_class_save_global_classes();
1790
EditorFileSystem::get_singleton()->emit_signal(SNAME("script_classes_updated"));
1791
}
1792
emit_signal(SNAME("resource_removed"), p_resource);
1793
}
1794
1795
void FileSystemDock::_file_removed(const String &p_file) {
1796
emit_signal(SNAME("file_removed"), p_file);
1797
1798
// Find the closest parent directory available, in case multiple items were deleted along the same path.
1799
current_path = p_file.get_base_dir();
1800
Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);
1801
while (!da->dir_exists(current_path)) {
1802
current_path = current_path.get_base_dir();
1803
}
1804
1805
current_path_line_edit->set_text(current_path);
1806
}
1807
1808
void FileSystemDock::_folder_removed(const String &p_folder) {
1809
emit_signal(SNAME("folder_removed"), p_folder);
1810
1811
// Find the closest parent directory available, in case multiple items were deleted along the same path.
1812
current_path = p_folder.get_base_dir();
1813
Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);
1814
while (!da->dir_exists(current_path)) {
1815
current_path = current_path.get_base_dir();
1816
}
1817
1818
// Remove assigned folder color for all subfolders.
1819
bool folder_colors_updated = false;
1820
for (const Variant &E : assigned_folder_colors.get_key_list()) {
1821
const String &path = E;
1822
// These folder paths are guaranteed to end with a "/".
1823
if (path.begins_with(p_folder)) {
1824
assigned_folder_colors.erase(path);
1825
folder_colors_updated = true;
1826
}
1827
}
1828
if (folder_colors_updated) {
1829
_update_folder_colors_setting();
1830
}
1831
1832
current_path_line_edit->set_text(current_path);
1833
EditorFileSystemDirectory *efd = EditorFileSystem::get_singleton()->get_filesystem_path(current_path);
1834
if (efd) {
1835
efd->force_update();
1836
}
1837
}
1838
1839
void FileSystemDock::_rename_operation_confirm() {
1840
String new_name;
1841
TreeItem *ti = tree->get_edited();
1842
int col_index = tree->get_edited_column();
1843
1844
if (ti) {
1845
new_name = ti->get_text(col_index).strip_edges();
1846
} else {
1847
new_name = files->get_edit_text().strip_edges();
1848
}
1849
String old_name = to_rename.is_file ? to_rename.path.get_file() : to_rename.path.left(-1).get_file();
1850
1851
bool rename_error = false;
1852
if (new_name.length() == 0) {
1853
EditorNode::get_singleton()->show_warning(TTRC("No name provided."));
1854
rename_error = true;
1855
} else if (new_name.contains_char('/') || new_name.contains_char('\\') || new_name.contains_char(':')) {
1856
EditorNode::get_singleton()->show_warning(TTRC("Name contains invalid characters."));
1857
rename_error = true;
1858
} else if (new_name[0] == '.') {
1859
EditorNode::get_singleton()->show_warning(TTRC("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."));
1860
rename_error = true;
1861
} else if (to_rename.is_file && to_rename.path.get_extension() != new_name.get_extension()) {
1862
if (!EditorFileSystem::get_singleton()->get_valid_extensions().find(new_name.get_extension())) {
1863
EditorNode::get_singleton()->show_warning(TTRC("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."));
1864
rename_error = true;
1865
}
1866
}
1867
1868
// Restore original name.
1869
if (rename_error) {
1870
if (ti) {
1871
ti->set_text(col_index, old_name);
1872
}
1873
return;
1874
}
1875
1876
String old_path = to_rename.path.ends_with("/") ? to_rename.path.left(-1) : to_rename.path;
1877
String new_path = old_path.get_base_dir().path_join(new_name);
1878
if (old_path == new_path) {
1879
return;
1880
}
1881
1882
if (EditorFileSystem::get_singleton()->is_group_file(old_path)) {
1883
EditorFileSystem::get_singleton()->move_group_file(old_path, new_path);
1884
}
1885
1886
// Present a more user friendly warning for name conflict.
1887
Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES);
1888
1889
bool new_exist = (da->file_exists(new_path) || da->dir_exists(new_path));
1890
if (!da->is_case_sensitive(new_path.get_base_dir())) {
1891
new_exist = new_exist && (new_path.to_lower() != old_path.to_lower());
1892
}
1893
if (new_exist) {
1894
EditorNode::get_singleton()->show_warning(TTRC("A file or folder with this name already exists."));
1895
if (ti) {
1896
ti->set_text(col_index, old_name);
1897
}
1898
return;
1899
}
1900
1901
HashMap<String, ResourceUID::ID> uids;
1902
HashSet<String> file_owners; // The files that use these moved/renamed resource files.
1903
_before_move(uids, file_owners);
1904
1905
HashMap<String, String> file_renames;
1906
HashMap<String, String> folder_renames;
1907
_try_move_item(to_rename, new_path, file_renames, folder_renames);
1908
1909
int current_tab = EditorSceneTabs::get_singleton()->get_current_tab();
1910
_update_resource_paths_after_move(file_renames, uids);
1911
_update_dependencies_after_move(file_renames, file_owners);
1912
_update_project_settings_after_move(file_renames, folder_renames);
1913
_update_favorites_after_move(file_renames, folder_renames);
1914
1915
EditorSceneTabs::get_singleton()->set_current_tab(current_tab);
1916
1917
if (ti) {
1918
current_path = new_path;
1919
current_path_line_edit->set_text(current_path);
1920
}
1921
1922
print_verbose("FileSystem: calling rescan.");
1923
_rescan();
1924
}
1925
1926
void FileSystemDock::_duplicate_operation_confirm(const String &p_path) {
1927
const String base_dir = p_path.trim_suffix("/").get_base_dir();
1928
if (!DirAccess::dir_exists_absolute(base_dir)) {
1929
Error err = EditorFileSystem::get_singleton()->make_dir_recursive(base_dir);
1930
if (err != OK) {
1931
EditorNode::get_singleton()->show_warning(vformat(TTR("Could not create base directory: %s"), TTR(error_names[err])));
1932
return;
1933
}
1934
}
1935
_try_duplicate_item(to_duplicate, p_path);
1936
}
1937
1938
void FileSystemDock::_move_confirm() {
1939
_move_operation_confirm(confirm_move_to_dir, confirm_to_copy);
1940
}
1941
1942
void FileSystemDock::_overwrite_dialog_action(bool p_overwrite) {
1943
overwrite_dialog->hide();
1944
_move_operation_confirm(to_move_path, to_move_or_copy, p_overwrite ? OVERWRITE_REPLACE : OVERWRITE_RENAME);
1945
}
1946
1947
void FileSystemDock::_convert_dialog_action() {
1948
Vector<Ref<Resource>> selected_resources;
1949
for (const String &S : to_convert) {
1950
Ref<Resource> res = ResourceLoader::load(S);
1951
ERR_FAIL_COND(res.is_null());
1952
selected_resources.push_back(res);
1953
}
1954
1955
Vector<Ref<Resource>> converted_resources;
1956
HashSet<Ref<Resource>> resources_to_erase_history_for;
1957
for (Ref<Resource> res : selected_resources) {
1958
Vector<Ref<EditorResourceConversionPlugin>> conversions = EditorNode::get_singleton()->find_resource_conversion_plugin_for_resource(res);
1959
for (const Ref<EditorResourceConversionPlugin> &conversion : conversions) {
1960
int conversion_id = 0;
1961
for (const String &target : cached_valid_conversion_targets) {
1962
if (conversion_id == selected_conversion_id && conversion->converts_to() == target) {
1963
Ref<Resource> converted_res = conversion->convert(res);
1964
ERR_FAIL_COND(converted_res.is_null());
1965
ERR_FAIL_COND(res.is_null());
1966
converted_resources.push_back(converted_res);
1967
resources_to_erase_history_for.insert(res);
1968
break;
1969
}
1970
conversion_id++;
1971
}
1972
}
1973
}
1974
1975
// Clear history for the objects being replaced.
1976
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
1977
for (Ref<Resource> res : resources_to_erase_history_for) {
1978
undo_redo->clear_history(true, undo_redo->get_history_id_for_object(res.ptr()));
1979
}
1980
1981
// Updates all the resources existing as node properties.
1982
EditorNode::get_singleton()->replace_resources_in_scenes(selected_resources, converted_resources);
1983
1984
// Overwrite the old resources.
1985
for (int i = 0; i < converted_resources.size(); i++) {
1986
Ref<Resource> original_resource = selected_resources.get(i);
1987
Ref<Resource> new_resource = converted_resources.get(i);
1988
1989
// Notify plugins that the original resource is removed.
1990
emit_signal(SNAME("file_removed"), original_resource->get_path());
1991
// Overwrite the path.
1992
new_resource->set_path(original_resource->get_path(), true);
1993
1994
ResourceSaver::save(new_resource);
1995
}
1996
}
1997
1998
Vector<String> FileSystemDock::_check_existing() {
1999
Vector<String> conflicting_items;
2000
for (const FileOrFolder &item : to_move) {
2001
String old_path = item.path.trim_suffix("/");
2002
String new_path = to_move_path.path_join(old_path.get_file());
2003
2004
if ((item.is_file && FileAccess::exists(new_path)) || (!item.is_file && DirAccess::exists(new_path))) {
2005
conflicting_items.push_back(old_path);
2006
}
2007
}
2008
return conflicting_items;
2009
}
2010
2011
void FileSystemDock::_move_operation_confirm(const String &p_to_path, bool p_copy, Overwrite p_overwrite) {
2012
if (p_overwrite == OVERWRITE_UNDECIDED) {
2013
to_move_path = p_to_path;
2014
to_move_or_copy = p_copy;
2015
2016
Vector<String> conflicting_items = _check_existing();
2017
if (!conflicting_items.is_empty()) {
2018
// Ask to do something.
2019
overwrite_dialog_header->set_text(vformat(
2020
TTR("The following files or folders conflict with items in the target location '%s':"), to_move_path));
2021
overwrite_dialog_file_list->set_text(String("\n").join(conflicting_items));
2022
overwrite_dialog_footer->set_text(
2023
p_copy ? TTRC("Do you wish to overwrite them or rename the copied files?")
2024
: TTRC("Do you wish to overwrite them or rename the moved files?"));
2025
overwrite_dialog->popup_centered();
2026
return;
2027
}
2028
}
2029
2030
Vector<String> new_paths;
2031
new_paths.resize(to_move.size());
2032
for (int i = 0; i < to_move.size(); i++) {
2033
if (p_overwrite == OVERWRITE_RENAME) {
2034
new_paths.write[i] = _get_unique_name(to_move[i], p_to_path);
2035
} else {
2036
new_paths.write[i] = p_to_path.path_join(to_move[i].path.trim_suffix("/").get_file());
2037
}
2038
}
2039
2040
if (p_copy) {
2041
for (int i = 0; i < to_move.size(); i++) {
2042
if (to_move[i].path != new_paths[i]) {
2043
_try_duplicate_item(to_move[i], new_paths[i]);
2044
select_after_scan = new_paths[i];
2045
}
2046
}
2047
} else {
2048
// Check groups.
2049
for (int i = 0; i < to_move.size(); i++) {
2050
if (to_move[i].is_file && EditorFileSystem::get_singleton()->is_group_file(to_move[i].path)) {
2051
EditorFileSystem::get_singleton()->move_group_file(to_move[i].path, new_paths[i]);
2052
}
2053
}
2054
2055
HashMap<String, ResourceUID::ID> uids;
2056
HashSet<String> file_owners; // The files that use these moved/renamed resource files.
2057
_before_move(uids, file_owners);
2058
2059
bool is_moved = false;
2060
HashMap<String, String> file_renames;
2061
HashMap<String, String> folder_renames;
2062
2063
for (int i = 0; i < to_move.size(); i++) {
2064
if (to_move[i].path != new_paths[i]) {
2065
_try_move_item(to_move[i], new_paths[i], file_renames, folder_renames);
2066
is_moved = true;
2067
}
2068
}
2069
2070
if (is_moved) {
2071
Vector<String> files_to_update;
2072
for (KeyValue<String, String> &E : file_renames) {
2073
if (!files_to_update.has(E.key)) {
2074
files_to_update.push_back(E.key);
2075
}
2076
if (!files_to_update.has(E.value)) {
2077
files_to_update.push_back(E.value);
2078
}
2079
}
2080
print_verbose("FileSystem: updating file infos.");
2081
EditorFileSystem::get_singleton()->update_files(files_to_update);
2082
2083
int current_tab = EditorSceneTabs::get_singleton()->get_current_tab();
2084
_update_resource_paths_after_move(file_renames, uids);
2085
_update_dependencies_after_move(file_renames, file_owners);
2086
_update_project_settings_after_move(file_renames, folder_renames);
2087
_update_favorites_after_move(file_renames, folder_renames);
2088
2089
EditorSceneTabs::get_singleton()->set_current_tab(current_tab);
2090
2091
print_verbose("FileSystem: calling rescan.");
2092
_rescan();
2093
2094
current_path = p_to_path;
2095
current_path_line_edit->set_text(current_path);
2096
}
2097
}
2098
}
2099
2100
void FileSystemDock::_before_move(HashMap<String, ResourceUID::ID> &r_uids, HashSet<String> &r_file_owners) const {
2101
HashSet<String> renamed_files;
2102
for (int i = 0; i < to_move.size(); i++) {
2103
if (to_move[i].is_file) {
2104
renamed_files.insert(to_move[i].path);
2105
ResourceUID::ID uid = ResourceLoader::get_resource_uid(to_move[i].path);
2106
if (uid != ResourceUID::INVALID_ID) {
2107
r_uids[to_move[i].path] = uid;
2108
}
2109
} else {
2110
EditorFileSystemDirectory *current_folder = EditorFileSystem::get_singleton()->get_filesystem_path(to_move[i].path);
2111
ERR_CONTINUE(current_folder == nullptr);
2112
List<EditorFileSystemDirectory *> folders;
2113
folders.push_back(current_folder);
2114
while (folders.front()) {
2115
current_folder = folders.front()->get();
2116
for (int j = 0; j < current_folder->get_file_count(); j++) {
2117
const String file_path = current_folder->get_file_path(j);
2118
renamed_files.insert(file_path);
2119
ResourceUID::ID uid = ResourceLoader::get_resource_uid(file_path);
2120
if (uid != ResourceUID::INVALID_ID) {
2121
r_uids[file_path] = uid;
2122
}
2123
}
2124
for (int j = 0; j < current_folder->get_subdir_count(); j++) {
2125
folders.push_back(current_folder->get_subdir(j));
2126
}
2127
folders.pop_front();
2128
}
2129
}
2130
}
2131
2132
// Look for files that use these moved/renamed resource files.
2133
_find_file_owners(EditorFileSystem::get_singleton()->get_filesystem(), renamed_files, r_file_owners);
2134
2135
// Open scenes with dependencies on the ones about to be moved will be reloaded,
2136
// so save them first to prevent losing unsaved changes.
2137
EditorNode::get_singleton()->save_scene_list(r_file_owners);
2138
}
2139
2140
Vector<String> FileSystemDock::_tree_get_selected(bool remove_self_inclusion, bool p_include_unselected_cursor) const {
2141
// Build a list of selected items with the active one at the first position.
2142
Vector<String> selected_strings;
2143
2144
TreeItem *cursor_item = tree->get_selected();
2145
if (cursor_item && (p_include_unselected_cursor || cursor_item->is_selected(0)) && cursor_item != favorites_item) {
2146
selected_strings.push_back(cursor_item->get_metadata(0));
2147
}
2148
2149
TreeItem *selected = tree->get_root();
2150
selected = tree->get_next_selected(selected);
2151
while (selected) {
2152
if (selected != cursor_item && selected != favorites_item && selected->is_visible_in_tree()) {
2153
selected_strings.push_back(selected->get_metadata(0));
2154
}
2155
selected = tree->get_next_selected(selected);
2156
}
2157
2158
if (remove_self_inclusion) {
2159
selected_strings = _remove_self_included_paths(selected_strings);
2160
}
2161
return selected_strings;
2162
}
2163
2164
Vector<String> FileSystemDock::_file_list_get_selected() const {
2165
Vector<int> selected_ids = files->get_selected_items();
2166
Vector<String> selected;
2167
selected.resize(selected_ids.size());
2168
2169
String *selected_write = selected.ptrw();
2170
int i = 0;
2171
for (const int id : selected_ids) {
2172
selected_write[i] = files->get_item_metadata(id);
2173
i++;
2174
}
2175
return selected;
2176
}
2177
2178
Vector<String> FileSystemDock::_remove_self_included_paths(Vector<String> selected_strings) {
2179
// Remove paths or files that are included into another.
2180
if (selected_strings.size() > 1) {
2181
selected_strings.sort_custom<FileNoCaseComparator>();
2182
String last_path = "";
2183
for (int i = 0; i < selected_strings.size(); i++) {
2184
if (!last_path.is_empty() && selected_strings[i].begins_with(last_path)) {
2185
selected_strings.remove_at(i);
2186
i--;
2187
}
2188
if (selected_strings[i].ends_with("/")) {
2189
last_path = selected_strings[i];
2190
}
2191
}
2192
}
2193
return selected_strings;
2194
}
2195
2196
void FileSystemDock::_tree_rmb_option(int p_option) {
2197
if (p_option > FILE_MENU_MAX && p_option < CONVERT_BASE_ID) {
2198
// Extra options don't need paths.
2199
_file_option(p_option, {});
2200
return;
2201
}
2202
2203
Vector<String> selected_strings = _tree_get_selected(false);
2204
2205
// Execute the current option.
2206
switch (p_option) {
2207
case FILE_MENU_EXPAND_ALL:
2208
case FILE_MENU_COLLAPSE_ALL: {
2209
// Expand or collapse the folder
2210
if (selected_strings.size() == 1) {
2211
tree->get_selected()->set_collapsed_recursive(p_option == FILE_MENU_COLLAPSE_ALL);
2212
}
2213
} break;
2214
case FILE_MENU_RENAME: {
2215
selected_strings = _tree_get_selected(false, true);
2216
[[fallthrough]];
2217
}
2218
default: {
2219
_file_option(p_option, selected_strings);
2220
} break;
2221
}
2222
}
2223
2224
void FileSystemDock::_file_list_rmb_option(int p_option) {
2225
if (p_option > FILE_MENU_MAX && p_option < CONVERT_BASE_ID) {
2226
// Extra options don't need paths.
2227
_file_option(p_option, {});
2228
return;
2229
}
2230
_file_option(p_option, _file_list_get_selected());
2231
}
2232
2233
void FileSystemDock::_generic_rmb_option_selected(int p_option) {
2234
// Used for submenu commands where we don't know whether we're
2235
// calling from the file_list_rmb menu or the _tree_rmb option.
2236
if (files->has_focus()) {
2237
_file_list_rmb_option(p_option);
2238
} else {
2239
_tree_rmb_option(p_option);
2240
}
2241
}
2242
2243
void FileSystemDock::_file_option(int p_option, const Vector<String> &p_selected) {
2244
// The first one should be the active item.
2245
2246
switch (p_option) {
2247
case FILE_MENU_SHOW_IN_EXPLORER: {
2248
// Show the file/folder in the OS explorer.
2249
String fpath = current_path;
2250
if (current_path == "Favorites") {
2251
if (p_selected.is_empty()) {
2252
return;
2253
}
2254
fpath = p_selected[0];
2255
}
2256
2257
String dir = ProjectSettings::get_singleton()->globalize_path(fpath);
2258
OS::get_singleton()->shell_show_in_file_manager(dir, true);
2259
} break;
2260
2261
case FILE_MENU_OPEN_EXTERNAL: {
2262
String fpath = current_path;
2263
if (current_path == "Favorites") {
2264
if (p_selected.is_empty()) {
2265
return;
2266
}
2267
fpath = p_selected[0];
2268
}
2269
2270
const String file = ProjectSettings::get_singleton()->globalize_path(fpath);
2271
const String extension = file.get_extension();
2272
2273
const String resource_type = ResourceLoader::get_resource_type(fpath);
2274
String external_program;
2275
2276
if (ClassDB::is_parent_class(resource_type, "Script") || extension == "tres" || extension == "tscn") {
2277
external_program = EDITOR_GET("text_editor/external/exec_path");
2278
} else if (extension == "res" || extension == "scn") {
2279
// Binary resources have no meaningful editor outside Godot, so just fallback to something default.
2280
} else if (resource_type == "CompressedTexture2D" || resource_type == "Image") {
2281
if (extension == "svg" || extension == "svgz") {
2282
external_program = EDITOR_GET("filesystem/external_programs/vector_image_editor");
2283
} else {
2284
external_program = EDITOR_GET("filesystem/external_programs/raster_image_editor");
2285
}
2286
} else if (ClassDB::is_parent_class(resource_type, "AudioStream")) {
2287
external_program = EDITOR_GET("filesystem/external_programs/audio_editor");
2288
} else if (resource_type == "PackedScene") {
2289
external_program = EDITOR_GET("filesystem/external_programs/3d_model_editor");
2290
}
2291
2292
if (external_program.is_empty()) {
2293
OS::get_singleton()->shell_open(file);
2294
} else {
2295
List<String> paths;
2296
paths.push_back(file);
2297
OS::get_singleton()->open_with_program(external_program, paths);
2298
}
2299
} break;
2300
2301
case FILE_MENU_OPEN_IN_TERMINAL: {
2302
String fpath = current_path;
2303
if (current_path == "Favorites") {
2304
if (p_selected.is_empty()) {
2305
return;
2306
}
2307
fpath = p_selected[0];
2308
}
2309
2310
Vector<String> terminal_emulators;
2311
const String terminal_emulator_setting = EDITOR_GET("filesystem/external_programs/terminal_emulator");
2312
if (terminal_emulator_setting.is_empty()) {
2313
// Figure out a default terminal emulator to use.
2314
#if defined(WINDOWS_ENABLED)
2315
// Default to PowerShell as done by Windows 10 and later.
2316
terminal_emulators.push_back("powershell");
2317
#elif defined(MACOS_ENABLED)
2318
// NOTE: To avoid duplicating the Terminal icon on the Dock, we will use the `open` command
2319
// rather than directly launching the Terminal.app bundle.
2320
terminal_emulators.push_back("open");
2321
#elif defined(LINUXBSD_ENABLED)
2322
// Try terminal emulators that ship with common Linux distributions first.
2323
terminal_emulators.push_back("gnome-terminal");
2324
terminal_emulators.push_back("konsole");
2325
terminal_emulators.push_back("xfce4-terminal");
2326
terminal_emulators.push_back("lxterminal");
2327
terminal_emulators.push_back("kitty");
2328
terminal_emulators.push_back("alacritty");
2329
terminal_emulators.push_back("urxvt");
2330
terminal_emulators.push_back("xterm");
2331
#endif
2332
} else {
2333
// Use the user-specified terminal.
2334
terminal_emulators.push_back(terminal_emulator_setting);
2335
}
2336
2337
String flags = EDITOR_GET("filesystem/external_programs/terminal_emulator_flags");
2338
String arguments = flags;
2339
if (arguments.is_empty()) {
2340
// NOTE: This default value is ignored further below if the terminal executable is `powershell` or `cmd`,
2341
// due to these terminals requiring nonstandard syntax to start in a specified folder.
2342
arguments = "{directory}";
2343
}
2344
2345
#ifdef LINUXBSD_ENABLED
2346
String chosen_terminal_emulator;
2347
for (const String &terminal_emulator : terminal_emulators) {
2348
String pipe;
2349
List<String> test_args; // Required for `execute()`, as it doesn't accept `Vector<String>`.
2350
test_args.push_back("-cr");
2351
test_args.push_back("command -v " + terminal_emulator);
2352
const Error err = OS::get_singleton()->execute("bash", test_args, &pipe);
2353
// Check if a path to the terminal executable exists.
2354
if (err == OK && pipe.contains_char('/')) {
2355
chosen_terminal_emulator = terminal_emulator;
2356
break;
2357
} else if (err == ERR_CANT_FORK) {
2358
ERR_PRINT_ED(vformat(TTR("Couldn't run external program to check for terminal emulator presence: command -v %s"), terminal_emulator));
2359
}
2360
}
2361
#else
2362
// On Windows and macOS, the first (and only) terminal emulator in the list is always available.
2363
String chosen_terminal_emulator = terminal_emulators[0];
2364
#endif
2365
2366
List<String> terminal_emulator_args; // Required for `execute()`, as it doesn't accept `Vector<String>`.
2367
bool append_default_args = true;
2368
2369
#ifdef LINUXBSD_ENABLED
2370
// Prepend default arguments based on the terminal emulator name.
2371
// Use `String.ends_with()` so that installations in non-default paths
2372
// or `/usr/local/bin` are detected correctly.
2373
if (flags.is_empty()) {
2374
if (chosen_terminal_emulator.ends_with("konsole")) {
2375
terminal_emulator_args.push_back("--workdir");
2376
} else if (chosen_terminal_emulator.ends_with("gnome-terminal")) {
2377
terminal_emulator_args.push_back("--working-directory");
2378
} else if (chosen_terminal_emulator.ends_with("urxvt")) {
2379
terminal_emulator_args.push_back("-cd");
2380
} else if (chosen_terminal_emulator.ends_with("xfce4-terminal")) {
2381
terminal_emulator_args.push_back("--working-directory");
2382
} else if (chosen_terminal_emulator.ends_with("lxterminal")) {
2383
terminal_emulator_args.push_back("--working-directory={directory}");
2384
append_default_args = false;
2385
} else if (chosen_terminal_emulator.ends_with("kitty")) {
2386
terminal_emulator_args.push_back("--directory");
2387
} else if (chosen_terminal_emulator.ends_with("alacritty")) {
2388
terminal_emulator_args.push_back("--working-directory");
2389
} else if (chosen_terminal_emulator.ends_with("xterm")) {
2390
terminal_emulator_args.push_back("-e");
2391
terminal_emulator_args.push_back("cd '{directory}' && exec $SHELL");
2392
append_default_args = false;
2393
}
2394
}
2395
#endif
2396
2397
#ifdef MACOS_ENABLED
2398
if (terminal_emulator_setting.is_empty()) {
2399
terminal_emulator_args.push_back("-b");
2400
terminal_emulator_args.push_back("com.apple.terminal");
2401
}
2402
#endif
2403
2404
#ifdef WINDOWS_ENABLED
2405
// Prepend default arguments based on the terminal emulator name.
2406
// Use `String.get_basename().to_lower()` to handle Windows' case-insensitive paths
2407
// with optional file extensions for executables in `PATH`.
2408
if (chosen_terminal_emulator.get_basename().to_lower() == "powershell") {
2409
terminal_emulator_args.push_back("-noexit");
2410
terminal_emulator_args.push_back("-command");
2411
terminal_emulator_args.push_back("cd '{directory}'");
2412
append_default_args = false;
2413
} else if (chosen_terminal_emulator.get_basename().to_lower() == "cmd") {
2414
terminal_emulator_args.push_back("/K");
2415
terminal_emulator_args.push_back("cd /d {directory}");
2416
append_default_args = false;
2417
}
2418
#endif
2419
2420
Vector<String> arguments_array = arguments.split(" ");
2421
for (const String &argument : arguments_array) {
2422
if (!append_default_args && argument == "{directory}") {
2423
// Prevent appending a `{directory}` placeholder twice when using powershell or cmd.
2424
// This allows users to enter the path to cmd or PowerShell in the custom terminal emulator path,
2425
// and make it work without having to enter custom arguments.
2426
continue;
2427
}
2428
terminal_emulator_args.push_back(argument);
2429
}
2430
2431
const bool is_directory = fpath.ends_with("/");
2432
for (String &terminal_emulator_arg : terminal_emulator_args) {
2433
if (is_directory) {
2434
terminal_emulator_arg = terminal_emulator_arg.replace("{directory}", ProjectSettings::get_singleton()->globalize_path(fpath));
2435
} else {
2436
terminal_emulator_arg = terminal_emulator_arg.replace("{directory}", ProjectSettings::get_singleton()->globalize_path(fpath).get_base_dir());
2437
}
2438
}
2439
2440
if (OS::get_singleton()->is_stdout_verbose()) {
2441
// Print full command line to help with troubleshooting.
2442
String command_string = chosen_terminal_emulator;
2443
for (const String &arg : terminal_emulator_args) {
2444
command_string += " " + arg;
2445
}
2446
print_line("Opening terminal emulator:", command_string);
2447
}
2448
2449
const Error err = OS::get_singleton()->create_process(chosen_terminal_emulator, terminal_emulator_args, nullptr, true);
2450
if (err != OK) {
2451
String args_string;
2452
for (const String &terminal_emulator_arg : terminal_emulator_args) {
2453
args_string += terminal_emulator_arg;
2454
}
2455
ERR_PRINT_ED(vformat(TTR("Couldn't run external terminal program (error code %d): %s %s\nCheck `filesystem/external_programs/terminal_emulator` and `filesystem/external_programs/terminal_emulator_flags` in the Editor Settings."), err, chosen_terminal_emulator, args_string));
2456
}
2457
} break;
2458
2459
case FILE_MENU_OPEN: {
2460
// Open folders.
2461
TreeItem *selected = tree->get_root();
2462
selected = tree->get_next_selected(selected);
2463
while (selected) {
2464
if (p_selected.has(selected->get_metadata(0))) {
2465
selected->set_collapsed(false);
2466
}
2467
selected = tree->get_next_selected(selected);
2468
}
2469
// Open the file.
2470
for (int i = 0; i < p_selected.size(); i++) {
2471
_select_file(p_selected[i]);
2472
}
2473
} break;
2474
2475
case FILE_MENU_INHERIT: {
2476
// Create a new scene inherited from the selected one.
2477
if (p_selected.size() == 1) {
2478
emit_signal(SNAME("inherit"), p_selected[0]);
2479
}
2480
} break;
2481
2482
case FILE_MENU_MAIN_SCENE: {
2483
// Set as main scene with selected scene file.
2484
if (p_selected.size() == 1) {
2485
main_scene_path = ResourceUID::path_to_uid(p_selected[0]);
2486
ProjectSettings::get_singleton()->set("application/run/main_scene", main_scene_path);
2487
ProjectSettings::get_singleton()->save();
2488
_update_tree(get_uncollapsed_paths());
2489
_update_file_list(true);
2490
}
2491
} break;
2492
2493
case FILE_MENU_INSTANTIATE: {
2494
// Instantiate all selected scenes.
2495
Vector<String> paths;
2496
for (int i = 0; i < p_selected.size(); i++) {
2497
const String &fpath = p_selected[i];
2498
if (EditorFileSystem::get_singleton()->get_file_type(fpath) == "PackedScene") {
2499
paths.push_back(fpath);
2500
}
2501
}
2502
if (!paths.is_empty()) {
2503
emit_signal(SNAME("instantiate"), paths);
2504
}
2505
} break;
2506
2507
case FILE_MENU_ADD_FAVORITE: {
2508
// Add the files from favorites.
2509
Vector<String> favorites_list = EditorSettings::get_singleton()->get_favorites();
2510
for (int i = 0; i < p_selected.size(); i++) {
2511
if (!favorites_list.has(p_selected[i])) {
2512
favorites_list.push_back(p_selected[i]);
2513
}
2514
}
2515
EditorSettings::get_singleton()->set_favorites(favorites_list);
2516
} break;
2517
2518
case FILE_MENU_REMOVE_FAVORITE: {
2519
// Remove the files from favorites.
2520
Vector<String> favorites_list = EditorSettings::get_singleton()->get_favorites();
2521
for (int i = 0; i < p_selected.size(); i++) {
2522
favorites_list.erase(p_selected[i]);
2523
}
2524
EditorSettings::get_singleton()->set_favorites(favorites_list);
2525
} break;
2526
2527
case FILE_MENU_SHOW_IN_FILESYSTEM: {
2528
if (!p_selected.is_empty()) {
2529
navigate_to_path(p_selected[0]);
2530
}
2531
} break;
2532
2533
case FILE_MENU_DEPENDENCIES: {
2534
// Checkout the file dependencies.
2535
if (!p_selected.is_empty()) {
2536
const String &fpath = p_selected[0];
2537
deps_editor->edit(fpath);
2538
}
2539
} break;
2540
2541
case FILE_MENU_OWNERS: {
2542
// Checkout the file owners.
2543
if (!p_selected.is_empty()) {
2544
const String &fpath = p_selected[0];
2545
owners_editor->show(fpath);
2546
}
2547
} break;
2548
2549
case FILE_MENU_MOVE: {
2550
// Move or copy the files to a given location.
2551
to_move.clear();
2552
Vector<String> collapsed_paths = _remove_self_included_paths(p_selected);
2553
for (int i = collapsed_paths.size() - 1; i >= 0; i--) {
2554
const String &fpath = collapsed_paths[i];
2555
if (fpath != "res://") {
2556
to_move.push_back(FileOrFolder(fpath, !fpath.ends_with("/")));
2557
}
2558
}
2559
if (to_move.size() > 0) {
2560
move_dialog->config(p_selected);
2561
move_dialog->popup_centered_ratio(0.4);
2562
}
2563
} break;
2564
2565
case FILE_MENU_RENAME: {
2566
if (!p_selected.is_empty()) {
2567
// Set to_rename variable for callback execution.
2568
to_rename.path = p_selected[0];
2569
to_rename.is_file = !to_rename.path.ends_with("/");
2570
if (to_rename.path == "res://") {
2571
break;
2572
}
2573
2574
// Rename has same logic as move for resource files.
2575
to_move.clear();
2576
to_move.push_back(to_rename);
2577
2578
if (tree->has_focus()) {
2579
tree->grab_focus(!tree->has_focus(true));
2580
// Edit node in Tree.
2581
tree->edit_selected(true);
2582
2583
if (to_rename.is_file) {
2584
String name = to_rename.path.get_file();
2585
tree->set_editor_selection(0, name.rfind_char('.'));
2586
} else {
2587
String name = to_rename.path.left(-1).get_file(); // Removes the "/" suffix for folders.
2588
tree->set_editor_selection(0, name.length());
2589
}
2590
} else if (files->has_focus()) {
2591
files->edit_selected();
2592
}
2593
}
2594
} break;
2595
2596
case FILE_MENU_REMOVE: {
2597
// Remove the selected files.
2598
Vector<String> remove_files;
2599
Vector<String> remove_folders;
2600
Vector<String> collapsed_paths = _remove_self_included_paths(p_selected);
2601
2602
for (int i = 0; i < collapsed_paths.size(); i++) {
2603
const String &fpath = collapsed_paths[i];
2604
if (fpath != "res://") {
2605
if (fpath.ends_with("/")) {
2606
remove_folders.push_back(fpath);
2607
} else {
2608
remove_files.push_back(fpath);
2609
}
2610
}
2611
}
2612
2613
if (remove_files.size() + remove_folders.size() > 0) {
2614
remove_dialog->show(remove_folders, remove_files);
2615
}
2616
} break;
2617
2618
case FILE_MENU_DUPLICATE: {
2619
if (p_selected.size() != 1) {
2620
return;
2621
}
2622
2623
to_duplicate.path = p_selected[0];
2624
to_duplicate.is_file = !to_duplicate.path.ends_with("/");
2625
if (to_duplicate.is_file) {
2626
String name = to_duplicate.path.get_file();
2627
make_dir_dialog->config(to_duplicate.path.get_base_dir(), callable_mp(this, &FileSystemDock::_duplicate_operation_confirm),
2628
DirectoryCreateDialog::MODE_FILE, TTR("Duplicating file:") + " " + name, name);
2629
} else {
2630
String name = to_duplicate.path.trim_suffix("/").get_file();
2631
make_dir_dialog->config(to_duplicate.path.trim_suffix("/").get_base_dir(), callable_mp(this, &FileSystemDock::_duplicate_operation_confirm),
2632
DirectoryCreateDialog::MODE_DIRECTORY, TTR("Duplicating folder:") + " " + name, name);
2633
}
2634
make_dir_dialog->popup_centered();
2635
} break;
2636
2637
case FILE_MENU_REIMPORT: {
2638
ImportDock::get_singleton()->reimport_resources(p_selected);
2639
} break;
2640
2641
case FILE_MENU_NEW_FOLDER: {
2642
String directory = current_path;
2643
if (!directory.ends_with("/")) {
2644
directory = directory.get_base_dir();
2645
}
2646
make_dir_dialog->config(directory, callable_mp(this, &FileSystemDock::create_directory).bind(directory),
2647
DirectoryCreateDialog::MODE_DIRECTORY, TTR("Create Folder"), "new folder");
2648
make_dir_dialog->popup_centered();
2649
} break;
2650
2651
case FILE_MENU_NEW_SCENE: {
2652
String directory = current_path;
2653
if (!directory.ends_with("/")) {
2654
directory = directory.get_base_dir();
2655
}
2656
make_scene_dialog->config(directory);
2657
make_scene_dialog->popup_centered();
2658
} break;
2659
2660
case FILE_MENU_NEW_SCRIPT: {
2661
String fpath = current_path;
2662
if (!fpath.ends_with("/")) {
2663
fpath = fpath.get_base_dir();
2664
}
2665
make_script_dialog->config("Node", fpath.path_join("new_script.gd"), false, false);
2666
make_script_dialog->popup_centered();
2667
} break;
2668
2669
case FILE_MENU_COPY_PATH: {
2670
if (!p_selected.is_empty()) {
2671
const String &fpath = p_selected[0];
2672
DisplayServer::get_singleton()->clipboard_set(fpath);
2673
}
2674
} break;
2675
2676
case FILE_MENU_COPY_ABSOLUTE_PATH: {
2677
if (!p_selected.is_empty()) {
2678
const String &fpath = p_selected[0];
2679
const String absolute_path = ProjectSettings::get_singleton()->globalize_path(fpath);
2680
DisplayServer::get_singleton()->clipboard_set(absolute_path);
2681
}
2682
} break;
2683
2684
case FILE_MENU_COPY_UID: {
2685
if (!p_selected.is_empty()) {
2686
ResourceUID::ID uid = ResourceLoader::get_resource_uid(p_selected[0]);
2687
if (uid != ResourceUID::INVALID_ID) {
2688
String uid_string = ResourceUID::get_singleton()->id_to_text(uid);
2689
DisplayServer::get_singleton()->clipboard_set(uid_string);
2690
}
2691
}
2692
} break;
2693
2694
case FILE_MENU_NEW_RESOURCE: {
2695
new_resource_dialog->popup_create(true);
2696
} break;
2697
case FILE_MENU_NEW_TEXTFILE: {
2698
String fpath = current_path;
2699
if (!fpath.ends_with("/")) {
2700
fpath = fpath.get_base_dir();
2701
}
2702
String dir = ProjectSettings::get_singleton()->globalize_path(fpath);
2703
ScriptEditor::get_singleton()->open_text_file_create_dialog(dir);
2704
} break;
2705
case FILE_MENU_RUN_SCRIPT: {
2706
if (p_selected.size() == 1) {
2707
const String &fpath = p_selected[0];
2708
Ref<Script> scr = ResourceLoader::load(fpath);
2709
ERR_FAIL_COND(scr.is_null());
2710
EditorNode::get_singleton()->run_editor_script(scr);
2711
}
2712
} break;
2713
2714
case EXTRA_FOCUS_PATH: {
2715
focus_on_filter();
2716
} break;
2717
case EXTRA_FOCUS_FILTER: {
2718
focus_on_filter();
2719
} break;
2720
2721
default: {
2722
if (p_option >= EditorContextMenuPlugin::BASE_ID) {
2723
if (!EditorContextMenuPluginManager::get_singleton()->activate_custom_option(EditorContextMenuPlugin::CONTEXT_SLOT_FILESYSTEM, p_option, p_selected)) {
2724
// For create new file option, pass the path location of mouse click position instead, to plugin callback.
2725
String fpath = current_path;
2726
if (!fpath.ends_with("/")) {
2727
fpath = fpath.get_base_dir();
2728
}
2729
EditorContextMenuPluginManager::get_singleton()->activate_custom_option(EditorContextMenuPlugin::CONTEXT_SLOT_FILESYSTEM_CREATE, p_option, { fpath });
2730
}
2731
} else if (p_option >= CONVERT_BASE_ID) {
2732
selected_conversion_id = p_option - CONVERT_BASE_ID;
2733
ERR_FAIL_INDEX(selected_conversion_id, (int)cached_valid_conversion_targets.size());
2734
2735
to_convert.clear();
2736
for (const String &S : p_selected) {
2737
to_convert.push_back(S);
2738
}
2739
2740
int conversion_id = 0;
2741
for (const String &E : cached_valid_conversion_targets) {
2742
if (conversion_id == selected_conversion_id) {
2743
conversion_dialog->set_text(vformat(TTR("Do you wish to convert these files to %s? (This operation cannot be undone!)"), E));
2744
conversion_dialog->popup_centered();
2745
break;
2746
}
2747
conversion_id++;
2748
}
2749
}
2750
break;
2751
}
2752
}
2753
}
2754
2755
int FileSystemDock::_get_menu_option_from_key(const Ref<InputEventKey> &p_key) {
2756
if (ED_IS_SHORTCUT("filesystem_dock/duplicate", p_key)) {
2757
return FILE_MENU_DUPLICATE;
2758
} else if (ED_IS_SHORTCUT("filesystem_dock/copy_path", p_key)) {
2759
return FILE_MENU_COPY_PATH;
2760
} else if (ED_IS_SHORTCUT("filesystem_dock/copy_absolute_path", p_key)) {
2761
return FILE_MENU_COPY_ABSOLUTE_PATH;
2762
} else if (ED_IS_SHORTCUT("filesystem_dock/copy_uid", p_key)) {
2763
return FILE_MENU_COPY_UID;
2764
} else if (ED_IS_SHORTCUT("filesystem_dock/delete", p_key)) {
2765
return FILE_MENU_REMOVE;
2766
} else if (ED_IS_SHORTCUT("filesystem_dock/new_folder", p_key)) {
2767
return FILE_MENU_NEW_FOLDER;
2768
} else if (ED_IS_SHORTCUT("filesystem_dock/new_scene", p_key)) {
2769
return FILE_MENU_NEW_SCENE;
2770
} else if (ED_IS_SHORTCUT("filesystem_dock/new_script", p_key)) {
2771
return FILE_MENU_NEW_SCRIPT;
2772
} else if (ED_IS_SHORTCUT("filesystem_dock/new_resource", p_key)) {
2773
return FILE_MENU_NEW_RESOURCE;
2774
} else if (ED_IS_SHORTCUT("filesystem_dock/new_textfile", p_key)) {
2775
return FILE_MENU_NEW_TEXTFILE;
2776
} else if (ED_IS_SHORTCUT("filesystem_dock/rename", p_key)) {
2777
return FILE_MENU_RENAME;
2778
} else if (ED_IS_SHORTCUT("filesystem_dock/show_in_explorer", p_key)) {
2779
return FILE_MENU_SHOW_IN_EXPLORER;
2780
} else if (ED_IS_SHORTCUT("filesystem_dock/open_in_external_program", p_key)) {
2781
return FILE_MENU_OPEN_EXTERNAL;
2782
} else if (ED_IS_SHORTCUT("filesystem_dock/open_in_terminal", p_key)) {
2783
return FILE_MENU_OPEN_IN_TERMINAL;
2784
} else if (ED_IS_SHORTCUT("filesystem_dock/focus_path", p_key)) {
2785
return EXTRA_FOCUS_PATH;
2786
} else if (ED_IS_SHORTCUT("editor/open_search", p_key)) {
2787
return EXTRA_FOCUS_FILTER;
2788
}
2789
return -1;
2790
}
2791
2792
void FileSystemDock::_resource_created() {
2793
String fpath = current_path;
2794
if (!fpath.ends_with("/")) {
2795
fpath = fpath.get_base_dir();
2796
}
2797
2798
const String type_name = new_resource_dialog->get_selected_type();
2799
if (type_name == "Shader") {
2800
make_shader_dialog->config(fpath.path_join("new_shader"), false, false, type_name);
2801
make_shader_dialog->popup_centered();
2802
return;
2803
} else if (type_name == "ShaderInclude") {
2804
make_shader_dialog->config(fpath.path_join("new_shader_include"), false, false, type_name);
2805
make_shader_dialog->popup_centered();
2806
return;
2807
} else if (type_name == "VisualShader") {
2808
make_shader_dialog->config(fpath.path_join("new_shader"), false, false, type_name);
2809
make_shader_dialog->popup_centered();
2810
return;
2811
} else if (ClassDB::is_parent_class(type_name, "Script")) {
2812
make_script_dialog->config("Node", fpath.path_join("new_script"), false, false);
2813
make_script_dialog->popup_centered();
2814
return;
2815
}
2816
2817
Variant c = new_resource_dialog->instantiate_selected();
2818
2819
ERR_FAIL_COND(!c);
2820
Resource *r = Object::cast_to<Resource>(c);
2821
ERR_FAIL_NULL(r);
2822
2823
PackedScene *scene = Object::cast_to<PackedScene>(r);
2824
if (scene) {
2825
Node *node = memnew(Node);
2826
node->set_name("Node");
2827
scene->pack(node);
2828
memdelete(node);
2829
}
2830
2831
EditorNode::get_singleton()->push_item(r);
2832
EditorNode::get_singleton()->save_resource_as(Ref<Resource>(r), fpath);
2833
}
2834
2835
void FileSystemDock::_script_or_shader_created(const Ref<Resource> &p_resource) {
2836
if (Object::cast_to<Script>(p_resource.ptr()) && !EDITOR_GET("docks/filesystem/automatically_open_created_scripts").operator bool()) {
2837
return;
2838
}
2839
EditorNode::get_singleton()->push_item(p_resource.ptr());
2840
}
2841
2842
void FileSystemDock::_search_changed(const String &p_text, const Control *p_from) {
2843
if (searched_tokens.is_empty()) {
2844
// Register the uncollapsed paths before they change.
2845
uncollapsed_paths_before_search = get_uncollapsed_paths();
2846
}
2847
2848
const String searched_string = p_text.to_lower();
2849
if (searched_string.begins_with("uid://")) {
2850
ResourceUID::ID id = ResourceUID::get_singleton()->text_to_id(searched_string);
2851
if (id != ResourceUID::INVALID_ID && ResourceUID::get_singleton()->has_id(id)) {
2852
navigate_to_path(ResourceUID::get_singleton()->get_id_path(id));
2853
return;
2854
}
2855
}
2856
2857
searched_tokens = searched_string.split(" ", false);
2858
2859
if (p_from == tree_search_box) {
2860
file_list_search_box->set_text(searched_string);
2861
} else { // File_list_search_box.
2862
tree_search_box->set_text(searched_string);
2863
}
2864
2865
_update_filtered_items();
2866
if (display_mode == DISPLAY_MODE_HSPLIT || display_mode == DISPLAY_MODE_VSPLIT) {
2867
_update_file_list(false);
2868
}
2869
if (searched_tokens.is_empty()) {
2870
_navigate_to_path(current_path);
2871
}
2872
}
2873
2874
bool FileSystemDock::_matches_all_search_tokens(const String &p_text) {
2875
if (searched_tokens.is_empty()) {
2876
return false;
2877
}
2878
const String s = p_text.to_lower();
2879
for (const String &t : searched_tokens) {
2880
if (!s.contains(t)) {
2881
return false;
2882
}
2883
}
2884
return true;
2885
}
2886
2887
void FileSystemDock::_rescan() {
2888
if (tree->has_focus()) {
2889
had_focus = tree;
2890
} else if (files->has_focus()) {
2891
had_focus = files;
2892
}
2893
2894
_set_scanning_mode();
2895
EditorFileSystem::get_singleton()->scan();
2896
}
2897
2898
void FileSystemDock::_change_split_mode() {
2899
DisplayMode next_mode = DISPLAY_MODE_TREE_ONLY;
2900
if (display_mode == DISPLAY_MODE_VSPLIT) {
2901
next_mode = DISPLAY_MODE_HSPLIT;
2902
} else if (display_mode == DISPLAY_MODE_TREE_ONLY) {
2903
next_mode = DISPLAY_MODE_VSPLIT;
2904
}
2905
2906
set_display_mode(next_mode);
2907
emit_signal(SNAME("display_mode_changed"));
2908
}
2909
2910
void FileSystemDock::_split_dragged(int p_offset) {
2911
if (split_box->is_vertical()) {
2912
split_box_offset_v = p_offset;
2913
} else {
2914
split_box_offset_h = p_offset;
2915
}
2916
}
2917
2918
void FileSystemDock::fix_dependencies(const String &p_for_file) {
2919
deps_editor->edit(p_for_file);
2920
}
2921
2922
void FileSystemDock::update_all() {
2923
if (tree->is_visible()) {
2924
_update_tree(get_uncollapsed_paths(), false, false);
2925
}
2926
2927
if (file_list_vb->is_visible()) {
2928
_update_file_list(true);
2929
}
2930
}
2931
2932
void FileSystemDock::focus_on_path() {
2933
current_path_line_edit->grab_focus();
2934
current_path_line_edit->select_all();
2935
}
2936
2937
void FileSystemDock::focus_on_filter() {
2938
LineEdit *current_search_box = nullptr;
2939
if (display_mode == DISPLAY_MODE_TREE_ONLY) {
2940
current_search_box = tree_search_box;
2941
} else {
2942
current_search_box = file_list_search_box;
2943
}
2944
2945
if (current_search_box) {
2946
current_search_box->grab_focus();
2947
current_search_box->select_all();
2948
}
2949
}
2950
2951
void FileSystemDock::create_directory(const String &p_path, const String &p_base_dir) {
2952
String trimmed_path = p_path;
2953
if (!p_base_dir.is_empty()) {
2954
// Trims off the joining '/' if the base didn't end with one. If the base did have it
2955
// and there's two slashes, the empty directory is safe to trim off anyways.
2956
trimmed_path = trimmed_path.trim_prefix(p_base_dir).trim_prefix("/");
2957
}
2958
Error err = EditorFileSystem::get_singleton()->make_dir_recursive(trimmed_path, p_base_dir);
2959
if (err != OK) {
2960
EditorNode::get_singleton()->show_warning(vformat(TTR("Could not create folder: %s"), TTR(error_names[err])));
2961
}
2962
}
2963
2964
ScriptCreateDialog *FileSystemDock::get_script_create_dialog() const {
2965
return make_script_dialog;
2966
}
2967
2968
void FileSystemDock::set_file_list_display_mode(FileListDisplayMode p_mode) {
2969
if (p_mode == file_list_display_mode) {
2970
return;
2971
}
2972
2973
_toggle_file_display();
2974
}
2975
2976
void FileSystemDock::add_resource_tooltip_plugin(const Ref<EditorResourceTooltipPlugin> &p_plugin) {
2977
tooltip_plugins.push_back(p_plugin);
2978
}
2979
2980
void FileSystemDock::remove_resource_tooltip_plugin(const Ref<EditorResourceTooltipPlugin> &p_plugin) {
2981
int index = tooltip_plugins.find(p_plugin);
2982
ERR_FAIL_COND_MSG(index == -1, "Can't remove plugin that wasn't registered.");
2983
tooltip_plugins.remove_at(index);
2984
}
2985
2986
String FileSystemDock::get_folder_path_at_mouse_position() const {
2987
TreeItem *item = tree->get_item_at_position(tree->get_local_mouse_position());
2988
if (!item) {
2989
return String();
2990
}
2991
String fpath = item->get_metadata(0);
2992
return fpath.get_base_dir();
2993
}
2994
2995
Control *FileSystemDock::create_tooltip_for_path(const String &p_path) const {
2996
if (p_path == "Favorites") {
2997
// No tooltip for the "Favorites" group.
2998
return nullptr;
2999
}
3000
if (DirAccess::exists(p_path)) {
3001
// No tooltip for directory.
3002
return nullptr;
3003
}
3004
ERR_FAIL_COND_V(!FileAccess::exists(p_path), nullptr);
3005
3006
const String type = ResourceLoader::get_resource_type(p_path);
3007
Control *tooltip = EditorResourceTooltipPlugin::make_default_tooltip(p_path);
3008
3009
for (const Ref<EditorResourceTooltipPlugin> &plugin : tooltip_plugins) {
3010
if (plugin->handles(type)) {
3011
tooltip = plugin->make_tooltip_for_path(p_path, EditorResourcePreview::get_singleton()->get_preview_metadata(p_path), tooltip);
3012
}
3013
}
3014
return tooltip;
3015
}
3016
3017
Variant FileSystemDock::get_drag_data_fw(const Point2 &p_point, Control *p_from) {
3018
bool all_favorites = true;
3019
bool all_not_favorites = true;
3020
3021
Vector<String> paths;
3022
3023
if (p_from == tree) {
3024
// Check if the first selected is in favorite.
3025
TreeItem *selected = tree->get_next_selected(tree->get_root());
3026
while (selected) {
3027
if (selected == favorites_item) {
3028
// The "Favorites" item is not draggable.
3029
return Variant();
3030
}
3031
3032
bool is_favorite = selected->get_parent() != nullptr && tree->get_root()->get_first_child() == selected->get_parent();
3033
all_favorites &= is_favorite;
3034
all_not_favorites &= !is_favorite;
3035
selected = tree->get_next_selected(selected);
3036
}
3037
if (!all_not_favorites) {
3038
paths = _tree_get_selected(false);
3039
} else {
3040
paths = _tree_get_selected();
3041
}
3042
} else if (p_from == files) {
3043
for (int i = 0; i < files->get_item_count(); i++) {
3044
if (files->is_selected(i)) {
3045
paths.push_back(files->get_item_metadata(i));
3046
}
3047
}
3048
all_favorites = false;
3049
all_not_favorites = true;
3050
}
3051
3052
if (paths.is_empty()) {
3053
return Variant();
3054
}
3055
3056
Dictionary drag_data = EditorNode::get_singleton()->drag_files_and_dirs(paths, p_from);
3057
if (!all_not_favorites) {
3058
drag_data["favorite"] = all_favorites ? "all" : "mixed";
3059
}
3060
return drag_data;
3061
}
3062
3063
bool FileSystemDock::can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const {
3064
Dictionary drag_data = p_data;
3065
3066
if (drag_data.has("favorite")) {
3067
if (String(drag_data["favorite"]) != "all") {
3068
return false;
3069
}
3070
3071
// Moving favorite around.
3072
TreeItem *ti = (p_point == Vector2(Math::INF, Math::INF)) ? tree->get_selected() : tree->get_item_at_position(p_point);
3073
if (!ti) {
3074
return false;
3075
}
3076
3077
int drop_section = (p_point == Vector2(Math::INF, Math::INF)) ? tree->get_drop_section_at_position(tree->get_item_rect(ti).position) : tree->get_drop_section_at_position(p_point);
3078
if (ti == favorites_item) {
3079
return (drop_section == 1); // The parent, first fav.
3080
}
3081
if (ti->get_parent() && favorites_item == ti->get_parent()) {
3082
return true; // A favorite
3083
}
3084
if (ti == resources_item) {
3085
return (drop_section == -1); // The tree, last fav.
3086
}
3087
3088
return false;
3089
}
3090
3091
if (drag_data.has("type") && String(drag_data["type"]) == "resource") {
3092
// Move resources.
3093
String to_dir;
3094
bool favorite;
3095
_get_drag_target_folder(to_dir, favorite, p_point, p_from);
3096
return !favorite;
3097
}
3098
3099
if (drag_data.has("type") && (String(drag_data["type"]) == "files" || String(drag_data["type"]) == "files_and_dirs")) {
3100
// Move files or dir.
3101
String to_dir;
3102
bool favorite;
3103
_get_drag_target_folder(to_dir, favorite, p_point, p_from);
3104
3105
if (favorite) {
3106
return true;
3107
}
3108
3109
if (to_dir.is_empty()) {
3110
return false;
3111
}
3112
3113
// Attempting to move a folder into itself will fail later,
3114
// rather than bring up a message don't try to do it in the first place.
3115
to_dir = to_dir.ends_with("/") ? to_dir : (to_dir + "/");
3116
Vector<String> fnames = drag_data["files"];
3117
for (int i = 0; i < fnames.size(); ++i) {
3118
if (fnames[i].ends_with("/") && to_dir.begins_with(fnames[i])) {
3119
return false;
3120
}
3121
}
3122
3123
return true;
3124
}
3125
3126
if (drag_data.has("type") && String(drag_data["type"]) == "nodes") {
3127
// Save branch as scene.
3128
String to_dir;
3129
bool favorite;
3130
_get_drag_target_folder(to_dir, favorite, p_point, p_from);
3131
return !favorite && Array(drag_data["nodes"]).size() == 1;
3132
}
3133
3134
return false;
3135
}
3136
3137
void FileSystemDock::drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) {
3138
if (!can_drop_data_fw(p_point, p_data, p_from)) {
3139
return;
3140
}
3141
Dictionary drag_data = p_data;
3142
3143
Vector<String> dirs = EditorSettings::get_singleton()->get_favorites();
3144
3145
if (drag_data.has("favorite")) {
3146
if (String(drag_data["favorite"]) != "all") {
3147
return;
3148
}
3149
// Moving favorite around.
3150
TreeItem *ti = (p_point == Vector2(Math::INF, Math::INF)) ? tree->get_selected() : tree->get_item_at_position(p_point);
3151
if (!ti) {
3152
return;
3153
}
3154
int drop_section = (p_point == Vector2(Math::INF, Math::INF)) ? tree->get_drop_section_at_position(tree->get_item_rect(ti).position) : tree->get_drop_section_at_position(p_point);
3155
3156
int drop_position;
3157
Vector<String> drag_files = drag_data["files"];
3158
if (ti == favorites_item) {
3159
// Drop on the favorite folder.
3160
drop_position = 0;
3161
} else if (ti == resources_item) {
3162
// Drop on the resource item.
3163
drop_position = dirs.size();
3164
} else {
3165
// Drop in the list.
3166
drop_position = dirs.find(ti->get_metadata(0));
3167
if (drop_section == 1) {
3168
drop_position++;
3169
}
3170
}
3171
3172
// Remove dragged favorites.
3173
Vector<int> to_remove;
3174
int offset = 0;
3175
for (int i = 0; i < drag_files.size(); i++) {
3176
int to_remove_pos = dirs.find(drag_files[i]);
3177
to_remove.push_back(to_remove_pos);
3178
if (to_remove_pos < drop_position) {
3179
offset++;
3180
}
3181
}
3182
drop_position -= offset;
3183
to_remove.sort();
3184
for (int i = 0; i < to_remove.size(); i++) {
3185
dirs.remove_at(to_remove[i] - i);
3186
}
3187
3188
// Re-add them at the right position.
3189
for (int i = 0; i < drag_files.size(); i++) {
3190
dirs.insert(drop_position, drag_files[i]);
3191
drop_position++;
3192
}
3193
3194
EditorSettings::get_singleton()->set_favorites(dirs);
3195
_update_tree(get_uncollapsed_paths());
3196
3197
if (display_mode != DISPLAY_MODE_TREE_ONLY && current_path == "Favorites") {
3198
_update_file_list(true);
3199
}
3200
return;
3201
}
3202
3203
if (drag_data.has("type") && String(drag_data["type"]) == "resource") {
3204
// Moving resource.
3205
Ref<Resource> res = drag_data["resource"];
3206
String to_dir;
3207
bool favorite;
3208
tree->set_drop_mode_flags(Tree::DROP_MODE_ON_ITEM);
3209
_get_drag_target_folder(to_dir, favorite, p_point, p_from);
3210
if (to_dir.is_empty()) {
3211
to_dir = get_current_directory();
3212
}
3213
3214
if (res.is_valid() && !to_dir.is_empty()) {
3215
EditorNode::get_singleton()->push_item(res.ptr());
3216
EditorNode::get_singleton()->save_resource_as(res, to_dir);
3217
}
3218
}
3219
3220
if (drag_data.has("type") && (String(drag_data["type"]) == "files" || String(drag_data["type"]) == "files_and_dirs")) {
3221
// Move files or add to favorites.
3222
String to_dir;
3223
bool favorite;
3224
_get_drag_target_folder(to_dir, favorite, p_point, p_from);
3225
if (!to_dir.is_empty()) {
3226
Vector<String> fnames = drag_data["files"];
3227
to_move.clear();
3228
String target_dir = to_dir == "res://" ? to_dir : to_dir.trim_suffix("/");
3229
3230
for (int i = 0; i < fnames.size(); i++) {
3231
if (fnames[i].trim_suffix("/").get_base_dir() != target_dir) {
3232
to_move.push_back(FileOrFolder(fnames[i], !fnames[i].ends_with("/")));
3233
}
3234
}
3235
if (!to_move.is_empty()) {
3236
String move_confirm_text;
3237
confirm_move_to_dir = to_dir;
3238
3239
if (Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL)) {
3240
move_confirm_text = vformat(TTRN("Copy %d selected item to \"%s\"?", "Copy %d selected items to \"%s\"?", to_move.size()), to_move.size(), target_dir);
3241
confirm_to_copy = true;
3242
} else {
3243
move_confirm_text = vformat(TTRN("Move %d selected item to \"%s\"?", "Move %d selected items to \"%s\"?", to_move.size()), to_move.size(), target_dir);
3244
confirm_to_copy = false;
3245
}
3246
3247
move_confirm_dialog->set_text(move_confirm_text);
3248
move_confirm_dialog->popup_centered();
3249
}
3250
} else if (favorite) {
3251
// Add the files from favorites.
3252
Vector<String> fnames = drag_data["files"];
3253
Vector<String> favorites_list = EditorSettings::get_singleton()->get_favorites();
3254
for (int i = 0; i < fnames.size(); i++) {
3255
if (!favorites_list.has(fnames[i])) {
3256
favorites_list.push_back(fnames[i]);
3257
}
3258
}
3259
EditorSettings::get_singleton()->set_favorites(favorites_list);
3260
_update_tree(get_uncollapsed_paths());
3261
}
3262
}
3263
3264
if (drag_data.has("type") && String(drag_data["type"]) == "nodes") {
3265
String to_dir;
3266
bool favorite;
3267
tree->set_drop_mode_flags(Tree::DROP_MODE_ON_ITEM);
3268
_get_drag_target_folder(to_dir, favorite, p_point, p_from);
3269
if (to_dir.is_empty()) {
3270
to_dir = get_current_directory();
3271
}
3272
SceneTreeDock::get_singleton()->save_branch_to_file(to_dir);
3273
}
3274
}
3275
3276
void FileSystemDock::_get_drag_target_folder(String &target, bool &target_favorites, const Point2 &p_point, Control *p_from) const {
3277
target = String();
3278
target_favorites = false;
3279
3280
// In the file list.
3281
if (p_from == files) {
3282
int pos = (p_point == Vector2(Math::INF, Math::INF)) ? -1 : files->get_item_at_position(p_point, true);
3283
if (pos == -1) {
3284
target = get_current_directory();
3285
return;
3286
}
3287
3288
String ltarget = files->get_item_metadata(pos);
3289
target = ltarget.ends_with("/") ? ltarget : current_path.get_base_dir();
3290
return;
3291
}
3292
3293
// In the tree.
3294
if (p_from == tree) {
3295
TreeItem *ti = (p_point == Vector2(Math::INF, Math::INF)) ? tree->get_selected() : tree->get_item_at_position(p_point);
3296
int section = (p_point == Vector2(Math::INF, Math::INF)) ? tree->get_drop_section_at_position(tree->get_item_rect(ti).position) : tree->get_drop_section_at_position(p_point);
3297
if (ti) {
3298
// Check the favorites first.
3299
if (ti == tree->get_root()->get_first_child() && section >= 0) {
3300
target_favorites = true;
3301
return;
3302
} else if (ti->get_parent() == tree->get_root()->get_first_child()) {
3303
target_favorites = true;
3304
return;
3305
} else {
3306
String fpath = ti->get_metadata(0);
3307
if (section == 0) {
3308
if (fpath.ends_with("/")) {
3309
// We drop on a folder.
3310
target = fpath;
3311
return;
3312
} else {
3313
// We drop on the folder that the target file is in.
3314
target = fpath.get_base_dir();
3315
return;
3316
}
3317
} else {
3318
if (ti->get_parent() != tree->get_root()->get_first_child()) {
3319
// Not in the favorite section.
3320
if (fpath != "res://") {
3321
// We drop between two files
3322
if (fpath.ends_with("/")) {
3323
fpath = fpath.substr(0, fpath.length() - 1);
3324
}
3325
target = fpath.get_base_dir();
3326
return;
3327
}
3328
}
3329
}
3330
}
3331
}
3332
}
3333
}
3334
3335
void FileSystemDock::_update_folder_colors_setting() {
3336
if (!ProjectSettings::get_singleton()->has_setting("file_customization/folder_colors")) {
3337
ProjectSettings::get_singleton()->set_setting("file_customization/folder_colors", assigned_folder_colors);
3338
} else if (assigned_folder_colors.is_empty()) {
3339
ProjectSettings::get_singleton()->set_setting("file_customization/folder_colors", Variant());
3340
}
3341
ProjectSettings::get_singleton()->save();
3342
}
3343
3344
void FileSystemDock::_folder_color_index_pressed(int p_index, PopupMenu *p_menu) {
3345
Variant chosen_color_name = p_menu->get_item_metadata(p_index);
3346
Vector<String> selected;
3347
3348
// Get all selected folders based on whether the files panel or tree panel is currently focused.
3349
if (files->has_focus()) {
3350
Vector<int> files_selected_ids = files->get_selected_items();
3351
for (int i = 0; i < files_selected_ids.size(); i++) {
3352
selected.push_back(files->get_item_metadata(files_selected_ids[i]));
3353
}
3354
} else {
3355
TreeItem *tree_selected = tree->get_root();
3356
tree_selected = tree->get_next_selected(tree_selected);
3357
while (tree_selected) {
3358
selected.push_back(tree_selected->get_metadata(0));
3359
tree_selected = tree->get_next_selected(tree_selected);
3360
}
3361
}
3362
3363
// Update project settings with new folder colors.
3364
for (int i = 0; i < selected.size(); i++) {
3365
const String &fpath = selected[i];
3366
3367
if (chosen_color_name) {
3368
assigned_folder_colors[fpath] = chosen_color_name;
3369
} else {
3370
assigned_folder_colors.erase(fpath);
3371
}
3372
}
3373
3374
_update_folder_colors_setting();
3375
update_all();
3376
3377
emit_signal(SNAME("folder_color_changed"));
3378
}
3379
3380
void FileSystemDock::_file_and_folders_fill_popup(PopupMenu *p_popup, const Vector<String> &p_paths, bool p_display_path_dependent_options) {
3381
// Add options for files and folders.
3382
ERR_FAIL_COND_MSG(p_paths.is_empty(), "Path cannot be empty.");
3383
3384
Vector<String> filenames;
3385
Vector<String> foldernames;
3386
3387
Vector<String> favorites_list = EditorSettings::get_singleton()->get_favorites();
3388
3389
bool all_files = true;
3390
bool all_files_scenes = true;
3391
bool all_folders = true;
3392
bool all_favorites = true;
3393
bool all_not_favorites = true;
3394
3395
for (int i = 0; i < p_paths.size(); i++) {
3396
const String &fpath = p_paths[i];
3397
if (fpath.ends_with("/")) {
3398
foldernames.push_back(fpath);
3399
all_files = false;
3400
} else {
3401
filenames.push_back(fpath);
3402
all_folders = false;
3403
all_files_scenes &= (EditorFileSystem::get_singleton()->get_file_type(fpath) == "PackedScene");
3404
}
3405
3406
// Check if in favorites.
3407
bool found = false;
3408
for (int j = 0; j < favorites_list.size(); j++) {
3409
if (favorites_list[j] == fpath) {
3410
found = true;
3411
break;
3412
}
3413
}
3414
if (found) {
3415
all_not_favorites = false;
3416
} else {
3417
all_favorites = false;
3418
}
3419
}
3420
3421
if (all_files) {
3422
if (all_files_scenes) {
3423
if (filenames.size() == 1) {
3424
p_popup->add_icon_item(get_editor_theme_icon(SNAME("Load")), TTRC("Open Scene"), FILE_MENU_OPEN);
3425
p_popup->add_icon_item(get_editor_theme_icon(SNAME("CreateNewSceneFrom")), TTRC("New Inherited Scene"), FILE_MENU_INHERIT);
3426
if (main_scene_path != filenames[0]) {
3427
p_popup->add_icon_item(get_editor_theme_icon(SNAME("PlayScene")), TTRC("Set as Main Scene"), FILE_MENU_MAIN_SCENE);
3428
}
3429
} else {
3430
p_popup->add_icon_item(get_editor_theme_icon(SNAME("Load")), TTRC("Open Scenes"), FILE_MENU_OPEN);
3431
}
3432
p_popup->add_icon_item(get_editor_theme_icon(SNAME("Instance")), TTRC("Instantiate"), FILE_MENU_INSTANTIATE);
3433
p_popup->add_separator();
3434
} else if (filenames.size() == 1) {
3435
p_popup->add_icon_item(get_editor_theme_icon(SNAME("Load")), TTRC("Open"), FILE_MENU_OPEN);
3436
3437
String type = EditorFileSystem::get_singleton()->get_file_type(filenames[0]);
3438
if (ClassDB::is_parent_class(type, "Script")) {
3439
Ref<Script> scr = ResourceLoader::load(filenames[0]);
3440
if (scr.is_valid()) {
3441
if (ClassDB::is_parent_class(scr->get_instance_base_type(), "EditorScript")) {
3442
p_popup->add_icon_item(get_editor_theme_icon(SNAME("MainPlay")), TTRC("Run"), FILE_MENU_RUN_SCRIPT);
3443
}
3444
}
3445
}
3446
p_popup->add_separator();
3447
}
3448
3449
if (filenames.size() == 1) {
3450
p_popup->add_item(TTRC("Edit Dependencies..."), FILE_MENU_DEPENDENCIES);
3451
p_popup->add_item(TTRC("View Owners..."), FILE_MENU_OWNERS);
3452
p_popup->add_separator();
3453
}
3454
}
3455
3456
if (p_paths.size() == 1 && p_display_path_dependent_options) {
3457
PopupMenu *new_menu = memnew(PopupMenu);
3458
new_menu->connect(SceneStringName(id_pressed), callable_mp(this, &FileSystemDock::_generic_rmb_option_selected));
3459
3460
p_popup->add_submenu_node_item(TTRC("Create New"), new_menu, FILE_MENU_NEW);
3461
p_popup->set_item_icon(p_popup->get_item_index(FILE_MENU_NEW), get_editor_theme_icon(SNAME("Add")));
3462
3463
new_menu->add_icon_item(get_editor_theme_icon(SNAME("Folder")), TTRC("Folder..."), FILE_MENU_NEW_FOLDER);
3464
new_menu->set_item_shortcut(-1, ED_GET_SHORTCUT("filesystem_dock/new_folder"));
3465
new_menu->add_icon_item(get_editor_theme_icon(SNAME("PackedScene")), TTRC("Scene..."), FILE_MENU_NEW_SCENE);
3466
new_menu->set_item_shortcut(-1, ED_GET_SHORTCUT("filesystem_dock/new_scene"));
3467
new_menu->add_icon_item(get_editor_theme_icon(SNAME("Script")), TTRC("Script..."), FILE_MENU_NEW_SCRIPT);
3468
new_menu->set_item_shortcut(-1, ED_GET_SHORTCUT("filesystem_dock/new_script"));
3469
new_menu->add_icon_item(get_editor_theme_icon(SNAME("Object")), TTRC("Resource..."), FILE_MENU_NEW_RESOURCE);
3470
new_menu->set_item_shortcut(-1, ED_GET_SHORTCUT("filesystem_dock/new_resource"));
3471
new_menu->add_icon_item(get_editor_theme_icon(SNAME("TextFile")), TTRC("TextFile..."), FILE_MENU_NEW_TEXTFILE);
3472
new_menu->set_item_shortcut(-1, ED_GET_SHORTCUT("filesystem_dock/new_textfile"));
3473
3474
const PackedStringArray folder_path = { p_paths[0].get_base_dir() };
3475
// Options for CONTEXT_SLOT_FILESYSTEM_CREATE are added with an offset, to avoid conflicts in case plugins add options for both FileSystem slots.
3476
EditorContextMenuPluginManager::get_singleton()->add_options_from_plugins(new_menu, EditorContextMenuPlugin::CONTEXT_SLOT_FILESYSTEM_CREATE, folder_path, 500);
3477
p_popup->add_separator();
3478
}
3479
3480
// Check if the root path is selected, we must check p_paths[1] because the first string in
3481
// the list of paths obtained by _tree_get_selected(...) is not always the root path.
3482
bool root_path_not_selected = p_paths[0] != "res://" && (p_paths.size() <= 1 || p_paths[1] != "res://");
3483
3484
if (all_folders && foldernames.size() > 0) {
3485
p_popup->add_icon_item(get_editor_theme_icon(SNAME("Load")), TTRC("Expand Folder"), FILE_MENU_OPEN);
3486
3487
if (foldernames.size() == 1) {
3488
p_popup->add_icon_item(get_editor_theme_icon(SNAME("GuiTreeArrowDown")), TTRC("Expand Hierarchy"), FILE_MENU_EXPAND_ALL);
3489
p_popup->add_icon_item(get_editor_theme_icon(SNAME("GuiTreeArrowRight")), TTRC("Collapse Hierarchy"), FILE_MENU_COLLAPSE_ALL);
3490
}
3491
3492
p_popup->add_separator();
3493
3494
// Only add the 'Set Folder Color...' option if the root path is not selected.
3495
if (root_path_not_selected) {
3496
PopupMenu *folder_colors_menu = memnew(PopupMenu);
3497
folder_colors_menu->connect(SceneStringName(id_pressed), callable_mp(this, &FileSystemDock::_folder_color_index_pressed).bind(folder_colors_menu));
3498
3499
p_popup->add_submenu_node_item(TTRC("Set Folder Color..."), folder_colors_menu);
3500
p_popup->set_item_icon(-1, get_editor_theme_icon(SNAME("Paint")));
3501
3502
folder_colors_menu->add_icon_item(get_editor_theme_icon(SNAME("Folder")), TTRC("Default (Reset)"));
3503
folder_colors_menu->set_item_icon_modulate(0, get_theme_color(SNAME("folder_icon_color"), SNAME("FileDialog")));
3504
folder_colors_menu->add_separator();
3505
3506
for (const KeyValue<String, Color> &E : folder_colors) {
3507
folder_colors_menu->add_icon_item(get_editor_theme_icon(SNAME("Folder")), E.key.capitalize());
3508
3509
folder_colors_menu->set_item_icon_modulate(-1, editor_is_dark_icon_and_font ? E.value : E.value * 2);
3510
folder_colors_menu->set_item_metadata(-1, E.key);
3511
}
3512
}
3513
}
3514
3515
// Add the options that are only available when a single item is selected.
3516
if (p_paths.size() == 1) {
3517
p_popup->add_icon_shortcut(get_editor_theme_icon(SNAME("ActionCopy")), ED_GET_SHORTCUT("filesystem_dock/copy_path"), FILE_MENU_COPY_PATH);
3518
p_popup->add_shortcut(ED_GET_SHORTCUT("filesystem_dock/copy_absolute_path"), FILE_MENU_COPY_ABSOLUTE_PATH);
3519
if (ResourceLoader::get_resource_uid(p_paths[0]) != ResourceUID::INVALID_ID) {
3520
p_popup->add_icon_shortcut(get_editor_theme_icon(SNAME("Instance")), ED_GET_SHORTCUT("filesystem_dock/copy_uid"), FILE_MENU_COPY_UID);
3521
}
3522
if (root_path_not_selected) {
3523
p_popup->add_icon_shortcut(get_editor_theme_icon(SNAME("Rename")), ED_GET_SHORTCUT("filesystem_dock/rename"), FILE_MENU_RENAME);
3524
p_popup->add_icon_shortcut(get_editor_theme_icon(SNAME("Duplicate")), ED_GET_SHORTCUT("filesystem_dock/duplicate"), FILE_MENU_DUPLICATE);
3525
}
3526
}
3527
3528
// Add the options that are only available when the root path is not selected.
3529
if (root_path_not_selected) {
3530
p_popup->add_icon_item(get_editor_theme_icon(SNAME("MoveUp")), TTRC("Move/Duplicate To..."), FILE_MENU_MOVE);
3531
p_popup->add_icon_shortcut(get_editor_theme_icon(SNAME("Remove")), ED_GET_SHORTCUT("filesystem_dock/delete"), FILE_MENU_REMOVE);
3532
}
3533
3534
// Only add a separator if we have actually placed any options in the menu since the last separator.
3535
if (p_paths.size() == 1 || root_path_not_selected) {
3536
p_popup->add_separator();
3537
}
3538
3539
// Add the options that are available when one or more items are selected.
3540
if (p_paths.size() >= 1) {
3541
if (!all_favorites) {
3542
p_popup->add_icon_item(get_editor_theme_icon(SNAME("Favorites")), TTRC("Add to Favorites"), FILE_MENU_ADD_FAVORITE);
3543
}
3544
if (!all_not_favorites) {
3545
p_popup->add_icon_item(get_editor_theme_icon(SNAME("NonFavorite")), TTRC("Remove from Favorites"), FILE_MENU_REMOVE_FAVORITE);
3546
}
3547
3548
if (root_path_not_selected) {
3549
cached_valid_conversion_targets = _get_valid_conversions_for_file_paths(p_paths);
3550
3551
int relative_id = 0;
3552
if (!cached_valid_conversion_targets.is_empty()) {
3553
p_popup->add_separator();
3554
3555
// If we have more than one type we can convert into, collapse it into a submenu.
3556
const int CONVERSION_SUBMENU_THRESHOLD = 1;
3557
3558
PopupMenu *container_menu = p_popup;
3559
String conversion_string_template = "Convert to %s";
3560
3561
if (cached_valid_conversion_targets.size() > CONVERSION_SUBMENU_THRESHOLD) {
3562
container_menu = memnew(PopupMenu);
3563
container_menu->connect(SceneStringName(id_pressed), callable_mp(this, &FileSystemDock::_generic_rmb_option_selected));
3564
3565
p_popup->add_submenu_node_item(TTRC("Convert to..."), container_menu, FILE_MENU_NEW);
3566
conversion_string_template = "%s";
3567
}
3568
3569
for (const String &E : cached_valid_conversion_targets) {
3570
Ref<Texture2D> icon;
3571
if (has_theme_icon(E, SNAME("EditorIcons"))) {
3572
icon = get_editor_theme_icon(E);
3573
} else {
3574
icon = get_editor_theme_icon(SNAME("Object"));
3575
}
3576
3577
container_menu->add_icon_item(icon, vformat(TTR(conversion_string_template), E), CONVERT_BASE_ID + relative_id);
3578
relative_id++;
3579
}
3580
}
3581
}
3582
3583
{
3584
List<String> resource_extensions;
3585
ResourceFormatImporter::get_singleton()->get_recognized_extensions_for_type("Resource", &resource_extensions);
3586
HashSet<String> extension_list;
3587
for (const String &extension : resource_extensions) {
3588
extension_list.insert(extension);
3589
}
3590
3591
bool resource_valid = true;
3592
String main_extension;
3593
3594
for (int i = 0; i != p_paths.size(); ++i) {
3595
String extension = p_paths[i].get_extension();
3596
if (extension_list.has(extension)) {
3597
if (main_extension.is_empty()) {
3598
main_extension = extension;
3599
} else if (extension != main_extension) {
3600
resource_valid = false;
3601
break;
3602
}
3603
} else {
3604
resource_valid = false;
3605
break;
3606
}
3607
}
3608
3609
if (resource_valid) {
3610
p_popup->add_icon_item(get_editor_theme_icon(SNAME("Load")), TTRC("Reimport"), FILE_MENU_REIMPORT);
3611
}
3612
}
3613
}
3614
3615
if (p_paths.size() == 1) {
3616
const String &fpath = p_paths[0];
3617
3618
[[maybe_unused]] bool added_separator = false;
3619
3620
if (favorites_list.has(fpath)) {
3621
TreeItem *cursor_item = tree->get_selected();
3622
bool is_item_in_favorites = false;
3623
while (cursor_item != nullptr) {
3624
if (cursor_item == favorites_item) {
3625
is_item_in_favorites = true;
3626
break;
3627
}
3628
3629
cursor_item = cursor_item->get_parent();
3630
}
3631
3632
if (is_item_in_favorites) {
3633
p_popup->add_separator();
3634
added_separator = true;
3635
p_popup->add_icon_item(get_editor_theme_icon(SNAME("ShowInFileSystem")), TTRC("Show in FileSystem"), FILE_MENU_SHOW_IN_FILESYSTEM);
3636
}
3637
}
3638
3639
#if !defined(ANDROID_ENABLED) && !defined(WEB_ENABLED)
3640
if (!added_separator) {
3641
p_popup->add_separator();
3642
added_separator = true;
3643
}
3644
3645
// Opening the system file manager is not supported on the Android and web editors.
3646
const bool is_directory = fpath.ends_with("/");
3647
3648
p_popup->add_icon_shortcut(get_editor_theme_icon(SNAME("Terminal")), ED_GET_SHORTCUT("filesystem_dock/open_in_terminal"), FILE_MENU_OPEN_IN_TERMINAL);
3649
p_popup->set_item_text(p_popup->get_item_index(FILE_MENU_OPEN_IN_TERMINAL), is_directory ? TTRC("Open in Terminal") : TTRC("Open Folder in Terminal"));
3650
3651
if (!is_directory) {
3652
p_popup->add_icon_shortcut(get_editor_theme_icon(SNAME("ExternalLink")), ED_GET_SHORTCUT("filesystem_dock/open_in_external_program"), FILE_MENU_OPEN_EXTERNAL);
3653
}
3654
3655
p_popup->add_icon_shortcut(get_editor_theme_icon(SNAME("Filesystem")), ED_GET_SHORTCUT("filesystem_dock/show_in_explorer"), FILE_MENU_SHOW_IN_EXPLORER);
3656
p_popup->set_item_text(p_popup->get_item_index(FILE_MENU_SHOW_IN_EXPLORER), is_directory ? TTRC("Open in File Manager") : TTRC("Show in File Manager"));
3657
#endif
3658
3659
current_path = fpath;
3660
}
3661
EditorContextMenuPluginManager::get_singleton()->add_options_from_plugins(p_popup, EditorContextMenuPlugin::CONTEXT_SLOT_FILESYSTEM, p_paths);
3662
}
3663
3664
void FileSystemDock::_tree_rmb_select(const Vector2 &p_pos, MouseButton p_button) {
3665
if (p_button != MouseButton::RIGHT) {
3666
return;
3667
}
3668
tree->grab_focus(true);
3669
3670
// Right click is pressed in the tree.
3671
Vector<String> paths = _tree_get_selected(false);
3672
3673
tree_popup->clear();
3674
3675
// Popup.
3676
if (!paths.is_empty()) {
3677
tree_popup->reset_size();
3678
_file_and_folders_fill_popup(tree_popup, paths);
3679
tree_popup->set_position(tree->get_screen_position() + p_pos);
3680
tree_popup->reset_size();
3681
tree_popup->popup();
3682
}
3683
}
3684
3685
void FileSystemDock::_tree_empty_click(const Vector2 &p_pos, MouseButton p_button) {
3686
if (p_button != MouseButton::RIGHT) {
3687
return;
3688
}
3689
// Right click is pressed in the empty space of the tree.
3690
current_path = "res://";
3691
tree_popup->clear();
3692
tree_popup->reset_size();
3693
tree_popup->add_icon_item(get_editor_theme_icon(SNAME("Folder")), TTRC("New Folder..."), FILE_MENU_NEW_FOLDER);
3694
tree_popup->set_item_shortcut(-1, ED_GET_SHORTCUT("filesystem_dock/new_folder"));
3695
tree_popup->add_icon_item(get_editor_theme_icon(SNAME("PackedScene")), TTRC("New Scene..."), FILE_MENU_NEW_SCENE);
3696
tree_popup->set_item_shortcut(-1, ED_GET_SHORTCUT("filesystem_dock/new_scene"));
3697
tree_popup->add_icon_item(get_editor_theme_icon(SNAME("Script")), TTRC("New Script..."), FILE_MENU_NEW_SCRIPT);
3698
tree_popup->set_item_shortcut(-1, ED_GET_SHORTCUT("filesystem_dock/new_script"));
3699
tree_popup->add_icon_item(get_editor_theme_icon(SNAME("Object")), TTRC("New Resource..."), FILE_MENU_NEW_RESOURCE);
3700
tree_popup->set_item_shortcut(-1, ED_GET_SHORTCUT("filesystem_dock/new_resource"));
3701
tree_popup->add_icon_item(get_editor_theme_icon(SNAME("TextFile")), TTRC("New TextFile..."), FILE_MENU_NEW_TEXTFILE);
3702
tree_popup->set_item_shortcut(-1, ED_GET_SHORTCUT("filesystem_dock/new_textfile"));
3703
3704
// To keep consistency with options added to "Create New..." menu (for plugin which has slot as CONTEXT_SLOT_FILESYSTEM_CREATE).
3705
EditorContextMenuPluginManager::get_singleton()->add_options_from_plugins(tree_popup, EditorContextMenuPlugin::CONTEXT_SLOT_FILESYSTEM_CREATE, Vector<String>(), 500);
3706
#if !defined(ANDROID_ENABLED) && !defined(WEB_ENABLED)
3707
// Opening the system file manager is not supported on the Android and web editors.
3708
tree_popup->add_separator();
3709
tree_popup->add_icon_shortcut(get_editor_theme_icon(SNAME("Terminal")), ED_GET_SHORTCUT("filesystem_dock/open_in_terminal"), FILE_MENU_OPEN_IN_TERMINAL);
3710
tree_popup->add_icon_shortcut(get_editor_theme_icon(SNAME("Filesystem")), ED_GET_SHORTCUT("filesystem_dock/show_in_explorer"), FILE_MENU_SHOW_IN_EXPLORER);
3711
#endif
3712
3713
tree_popup->set_position(tree->get_screen_position() + p_pos);
3714
tree_popup->reset_size();
3715
tree_popup->popup();
3716
}
3717
3718
void FileSystemDock::_tree_empty_selected() {
3719
tree->deselect_all();
3720
current_path = "";
3721
current_path_line_edit->set_text(current_path);
3722
if (file_list_vb->is_visible()) {
3723
_update_file_list(false);
3724
}
3725
_update_selection_changed();
3726
}
3727
3728
void FileSystemDock::_file_list_item_clicked(int p_item, const Vector2 &p_pos, MouseButton p_mouse_button_index) {
3729
if (p_mouse_button_index != MouseButton::RIGHT) {
3730
return;
3731
}
3732
files->grab_focus(true);
3733
3734
// Right click is pressed in the file list.
3735
Vector<String> paths;
3736
for (int i = 0; i < files->get_item_count(); i++) {
3737
if (!files->is_selected(i)) {
3738
continue;
3739
}
3740
if (files->get_item_text(p_item) == "..") {
3741
files->deselect(i);
3742
continue;
3743
}
3744
paths.push_back(files->get_item_metadata(i));
3745
}
3746
3747
// Popup.
3748
if (!paths.is_empty()) {
3749
file_list_popup->clear();
3750
_file_and_folders_fill_popup(file_list_popup, paths, searched_tokens.is_empty());
3751
file_list_popup->set_position(files->get_screen_position() + p_pos);
3752
file_list_popup->reset_size();
3753
file_list_popup->popup();
3754
}
3755
}
3756
3757
void FileSystemDock::_file_list_empty_clicked(const Vector2 &p_pos, MouseButton p_mouse_button_index) {
3758
if (p_mouse_button_index != MouseButton::RIGHT) {
3759
return;
3760
}
3761
3762
// Right click on empty space for file list.
3763
if (!searched_tokens.is_empty()) {
3764
return;
3765
}
3766
3767
current_path = current_path_line_edit->get_text();
3768
3769
// Favorites isn't a directory so don't show menu.
3770
if (current_path == "Favorites") {
3771
return;
3772
}
3773
3774
file_list_popup->clear();
3775
file_list_popup->reset_size();
3776
3777
file_list_popup->add_icon_item(get_editor_theme_icon(SNAME("Folder")), TTRC("New Folder..."), FILE_MENU_NEW_FOLDER);
3778
file_list_popup->set_item_shortcut(-1, ED_GET_SHORTCUT("filesystem_dock/new_folder"));
3779
file_list_popup->add_icon_item(get_editor_theme_icon(SNAME("PackedScene")), TTRC("New Scene..."), FILE_MENU_NEW_SCENE);
3780
file_list_popup->set_item_shortcut(-1, ED_GET_SHORTCUT("filesystem_dock/new_scene"));
3781
file_list_popup->add_icon_item(get_editor_theme_icon(SNAME("Script")), TTRC("New Script..."), FILE_MENU_NEW_SCRIPT);
3782
file_list_popup->set_item_shortcut(-1, ED_GET_SHORTCUT("filesystem_dock/new_script"));
3783
file_list_popup->add_icon_item(get_editor_theme_icon(SNAME("Object")), TTRC("New Resource..."), FILE_MENU_NEW_RESOURCE);
3784
file_list_popup->set_item_shortcut(-1, ED_GET_SHORTCUT("filesystem_dock/new_resource"));
3785
file_list_popup->add_icon_item(get_editor_theme_icon(SNAME("TextFile")), TTRC("New TextFile..."), FILE_MENU_NEW_TEXTFILE);
3786
file_list_popup->set_item_shortcut(-1, ED_GET_SHORTCUT("filesystem_dock/new_textfile"));
3787
3788
// To keep consistency with options added to "Create New..." menu (for plugin which has slot as CONTEXT_SLOT_FILESYSTEM_CREATE).
3789
EditorContextMenuPluginManager::get_singleton()->add_options_from_plugins(file_list_popup, EditorContextMenuPlugin::CONTEXT_SLOT_FILESYSTEM_CREATE, Vector<String>(), 500);
3790
file_list_popup->add_separator();
3791
file_list_popup->add_icon_shortcut(get_editor_theme_icon(SNAME("Terminal")), ED_GET_SHORTCUT("filesystem_dock/open_in_terminal"), FILE_MENU_OPEN_IN_TERMINAL);
3792
file_list_popup->add_icon_shortcut(get_editor_theme_icon(SNAME("Filesystem")), ED_GET_SHORTCUT("filesystem_dock/show_in_explorer"), FILE_MENU_SHOW_IN_EXPLORER);
3793
3794
file_list_popup->set_position(files->get_screen_position() + p_pos);
3795
file_list_popup->reset_size();
3796
file_list_popup->popup();
3797
}
3798
3799
void FileSystemDock::select_file(const String &p_file) {
3800
_navigate_to_path(p_file);
3801
}
3802
3803
void FileSystemDock::_file_multi_selected(int p_index, bool p_selected) {
3804
// Set the path to the current focused item.
3805
int current = files->get_current();
3806
if (current == p_index) {
3807
String fpath = files->get_item_metadata(current);
3808
if (!fpath.ends_with("/")) {
3809
current_path = fpath;
3810
}
3811
}
3812
3813
// Update the import dock.
3814
import_dock_needs_update = true;
3815
callable_mp(this, &FileSystemDock::_update_import_dock).call_deferred();
3816
}
3817
3818
void FileSystemDock::_update_selection_changed() {
3819
Vector<String> selection;
3820
selection.append_array(_tree_get_selected());
3821
selection.append_array(_file_list_get_selected());
3822
if (prev_selection != selection) {
3823
prev_selection = selection;
3824
emit_signal(SNAME("selection_changed"));
3825
}
3826
}
3827
3828
void FileSystemDock::_tree_mouse_exited() {
3829
if (holding_branch) {
3830
_reselect_items_selected_on_drag_begin();
3831
}
3832
}
3833
3834
void FileSystemDock::_reselect_items_selected_on_drag_begin(bool reset) {
3835
TreeItem *selected_item = tree->get_next_selected(tree->get_root());
3836
if (selected_item) {
3837
selected_item->deselect(0);
3838
}
3839
if (!tree_items_selected_on_drag_begin.is_empty()) {
3840
bool reselected = false;
3841
for (TreeItem *item : tree_items_selected_on_drag_begin) {
3842
if (item->get_tree()) {
3843
item->select(0);
3844
reselected = true;
3845
}
3846
}
3847
3848
if (reset) {
3849
tree_items_selected_on_drag_begin.clear();
3850
}
3851
3852
if (!reselected) {
3853
// If couldn't reselect the items selected on drag begin, select the "res://" item.
3854
tree->get_root()->get_child(1)->select(0);
3855
}
3856
}
3857
3858
files->deselect_all();
3859
if (!list_items_selected_on_drag_begin.is_empty()) {
3860
for (const int idx : list_items_selected_on_drag_begin) {
3861
files->select(idx, false);
3862
}
3863
3864
if (reset) {
3865
list_items_selected_on_drag_begin.clear();
3866
}
3867
}
3868
}
3869
3870
void FileSystemDock::_tree_gui_input(Ref<InputEvent> p_event) {
3871
Ref<InputEventMouseMotion> mm = p_event;
3872
if (mm.is_valid()) {
3873
TreeItem *item = tree->get_item_at_position(mm->get_position());
3874
if (item && holding_branch) {
3875
String fpath = item->get_metadata(0);
3876
while (!fpath.ends_with("/") && fpath != "res://" && item->get_parent()) { // Find the parent folder tree item.
3877
item = item->get_parent();
3878
fpath = item->get_metadata(0);
3879
}
3880
3881
TreeItem *deselect_item = tree->get_next_selected(tree->get_root());
3882
while (deselect_item) {
3883
deselect_item->deselect(0);
3884
deselect_item = tree->get_next_selected(deselect_item);
3885
}
3886
item->select(0);
3887
3888
if (display_mode != DisplayMode::DISPLAY_MODE_TREE_ONLY) {
3889
files->deselect_all();
3890
// Try to select the corresponding file list item.
3891
const int files_item_idx = files->find_metadata(fpath);
3892
if (files_item_idx != -1) {
3893
files->select(files_item_idx);
3894
}
3895
}
3896
}
3897
}
3898
3899
Ref<InputEventKey> key = p_event;
3900
if (key.is_valid() && key->is_pressed() && !key->is_echo()) {
3901
int option_id = _get_menu_option_from_key(key);
3902
if (option_id > -1) {
3903
_tree_rmb_option(option_id);
3904
} else {
3905
bool create = false;
3906
Callable custom_callback = EditorContextMenuPluginManager::get_singleton()->match_custom_shortcut(EditorContextMenuPlugin::CONTEXT_SLOT_FILESYSTEM, p_event);
3907
if (!custom_callback.is_valid()) {
3908
create = true;
3909
custom_callback = EditorContextMenuPluginManager::get_singleton()->match_custom_shortcut(EditorContextMenuPlugin::CONTEXT_SLOT_FILESYSTEM_CREATE, p_event);
3910
}
3911
3912
if (custom_callback.is_valid()) {
3913
PackedStringArray selected = _tree_get_selected(false);
3914
if (create) {
3915
if (selected.is_empty()) {
3916
selected.append("res://");
3917
} else if (selected.size() == 1) {
3918
selected.write[0] = selected[0].get_base_dir();
3919
} else {
3920
return;
3921
}
3922
}
3923
EditorContextMenuPluginManager::get_singleton()->invoke_callback(custom_callback, selected);
3924
} else {
3925
return;
3926
}
3927
}
3928
3929
accept_event();
3930
}
3931
}
3932
3933
void FileSystemDock::_file_list_gui_input(Ref<InputEvent> p_event) {
3934
Ref<InputEventMouseMotion> mm = p_event;
3935
if (mm.is_valid() && holding_branch) {
3936
const int item_idx = files->get_item_at_position(mm->get_position(), true);
3937
files->deselect_all();
3938
String fpath;
3939
if (item_idx != -1) {
3940
fpath = files->get_item_metadata(item_idx);
3941
if (fpath.ends_with("/") || fpath == "res://") {
3942
files->select(item_idx);
3943
}
3944
} else {
3945
fpath = get_current_directory();
3946
}
3947
3948
TreeItem *deselect_item = tree->get_next_selected(tree->get_root());
3949
while (deselect_item) {
3950
deselect_item->deselect(0);
3951
deselect_item = tree->get_next_selected(deselect_item);
3952
}
3953
3954
// Try to select the corresponding tree item.
3955
TreeItem *tree_item = (item_idx != -1) ? tree->get_item_with_text(files->get_item_text(item_idx)) : nullptr;
3956
3957
if (tree_item) {
3958
tree_item->select(0);
3959
} else {
3960
// Find parent folder.
3961
fpath = fpath.substr(0, fpath.rfind_char('/') + 1);
3962
if (fpath.size() > String("res://").size()) {
3963
fpath = fpath.left(fpath.size() - 2); // Remove last '/'.
3964
const int slash_idx = fpath.rfind_char('/');
3965
fpath = fpath.substr(slash_idx + 1);
3966
}
3967
3968
tree_item = tree->get_item_with_text(fpath);
3969
if (tree_item) {
3970
tree_item->select(0);
3971
}
3972
}
3973
}
3974
3975
Ref<InputEventKey> key = p_event;
3976
if (key.is_valid() && key->is_pressed() && !key->is_echo()) {
3977
int option_id = _get_menu_option_from_key(key);
3978
if (option_id > -1) {
3979
_file_list_rmb_option(option_id);
3980
} else {
3981
Callable custom_callback = EditorContextMenuPluginManager::get_singleton()->match_custom_shortcut(EditorContextMenuPlugin::CONTEXT_SLOT_FILESYSTEM, p_event);
3982
if (!custom_callback.is_valid()) {
3983
custom_callback = EditorContextMenuPluginManager::get_singleton()->match_custom_shortcut(EditorContextMenuPlugin::CONTEXT_SLOT_FILESYSTEM_CREATE, p_event);
3984
}
3985
3986
if (custom_callback.is_valid()) {
3987
EditorContextMenuPluginManager::get_singleton()->invoke_callback(custom_callback, _file_list_get_selected());
3988
} else {
3989
return;
3990
}
3991
}
3992
3993
accept_event();
3994
}
3995
}
3996
3997
bool FileSystemDock::_get_imported_files(const String &p_path, String &r_extension, Vector<String> &r_files) const {
3998
if (!p_path.ends_with("/")) {
3999
if (FileAccess::exists(p_path + ".import")) {
4000
if (r_extension.is_empty()) {
4001
r_extension = p_path.get_extension();
4002
} else if (r_extension != p_path.get_extension()) {
4003
r_files.clear();
4004
return false; // File type mismatch, stop search.
4005
}
4006
4007
r_files.push_back(p_path);
4008
}
4009
return true;
4010
}
4011
4012
Ref<DirAccess> da = DirAccess::open(p_path);
4013
ERR_FAIL_COND_V(da.is_null(), false);
4014
4015
da->list_dir_begin();
4016
String n = da->get_next();
4017
while (!n.is_empty()) {
4018
if (n != "." && n != ".." && !n.ends_with(".import")) {
4019
String npath = p_path + n + (da->current_is_dir() ? "/" : "");
4020
if (!_get_imported_files(npath, r_extension, r_files)) {
4021
return false;
4022
}
4023
}
4024
n = da->get_next();
4025
}
4026
da->list_dir_end();
4027
return true;
4028
}
4029
4030
void FileSystemDock::_update_import_dock() {
4031
if (!import_dock_needs_update) {
4032
return;
4033
}
4034
4035
_update_selection_changed();
4036
4037
// List selected.
4038
Vector<String> selected;
4039
if (display_mode == DISPLAY_MODE_TREE_ONLY) {
4040
// Use the tree
4041
selected = _tree_get_selected();
4042
4043
} else {
4044
// Use the file list.
4045
for (int i = 0; i < files->get_item_count(); i++) {
4046
if (!files->is_selected(i)) {
4047
continue;
4048
}
4049
4050
selected.push_back(files->get_item_metadata(i));
4051
}
4052
}
4053
4054
if (!selected.is_empty() && selected[0] == "res://") {
4055
// Scanning res:// is costly and unlikely to yield any useful results.
4056
return;
4057
}
4058
4059
// Expand directory selection.
4060
Vector<String> efiles;
4061
String extension;
4062
for (const String &fpath : selected) {
4063
_get_imported_files(fpath, extension, efiles);
4064
}
4065
4066
// Check import.
4067
Vector<String> imports;
4068
String import_type;
4069
for (int i = 0; i < efiles.size(); i++) {
4070
const String &fpath = efiles[i];
4071
Ref<ConfigFile> cf;
4072
cf.instantiate();
4073
Error err = cf->load(fpath + ".import");
4074
if (err != OK) {
4075
imports.clear();
4076
break;
4077
}
4078
4079
String type;
4080
if (cf->has_section_key("remap", "type")) {
4081
type = cf->get_value("remap", "type");
4082
}
4083
if (import_type.is_empty()) {
4084
import_type = type;
4085
} else if (import_type != type) {
4086
// All should be the same type.
4087
imports.clear();
4088
break;
4089
}
4090
imports.push_back(fpath);
4091
}
4092
4093
if (imports.is_empty()) {
4094
ImportDock::get_singleton()->clear();
4095
} else if (imports.size() == 1) {
4096
ImportDock::get_singleton()->set_edit_path(imports[0]);
4097
} else {
4098
ImportDock::get_singleton()->set_edit_multiple_paths(imports);
4099
}
4100
4101
import_dock_needs_update = false;
4102
}
4103
4104
void FileSystemDock::_feature_profile_changed() {
4105
_update_display_mode(true);
4106
}
4107
4108
void FileSystemDock::_project_settings_changed() {
4109
assigned_folder_colors = ProjectSettings::get_singleton()->get_setting("file_customization/folder_colors");
4110
4111
const String &current_main_scene_path = ResourceUID::ensure_path(GLOBAL_GET("application/run/main_scene"));
4112
if (main_scene_path != current_main_scene_path) {
4113
main_scene_path = current_main_scene_path;
4114
update_all();
4115
}
4116
}
4117
4118
void FileSystemDock::set_file_sort(FileSortOption p_file_sort) {
4119
for (int i = 0; i != (int)FileSortOption::FILE_SORT_MAX; i++) {
4120
tree_button_sort->get_popup()->set_item_checked(i, (i == (int)p_file_sort));
4121
file_list_button_sort->get_popup()->set_item_checked(i, (i == (int)p_file_sort));
4122
}
4123
file_sort = p_file_sort;
4124
4125
// Update everything needed.
4126
update_all();
4127
}
4128
4129
void FileSystemDock::_file_sort_popup(int p_id) {
4130
set_file_sort((FileSortOption)p_id);
4131
}
4132
4133
// TODO: Could use a unit test.
4134
Color FileSystemDock::get_dir_icon_color(const String &p_dir_path, const Color &p_default) {
4135
if (!singleton) { // This method can be called from the project manager.
4136
return p_default;
4137
}
4138
Color folder_icon_color = p_default;
4139
4140
// Check for a folder color to inherit (if one is assigned).
4141
String parent_dir = ProjectSettings::get_singleton()->localize_path(p_dir_path);
4142
while (!parent_dir.is_empty() && parent_dir != "res://") {
4143
if (!parent_dir.ends_with("/")) {
4144
parent_dir += "/";
4145
}
4146
4147
const String color_name = singleton->assigned_folder_colors.get(parent_dir, String());
4148
if (!color_name.is_empty()) {
4149
folder_icon_color = singleton->folder_colors[color_name];
4150
break;
4151
}
4152
parent_dir = parent_dir.trim_suffix("/").get_base_dir();
4153
}
4154
return folder_icon_color;
4155
}
4156
4157
const HashMap<String, Color> &FileSystemDock::get_folder_colors() const {
4158
return folder_colors;
4159
}
4160
4161
Dictionary FileSystemDock::get_assigned_folder_colors() const {
4162
return assigned_folder_colors;
4163
}
4164
4165
MenuButton *FileSystemDock::_create_file_menu_button() {
4166
MenuButton *button = memnew(MenuButton);
4167
button->set_flat(false);
4168
button->set_theme_type_variation("FlatMenuButton");
4169
button->set_tooltip_text(TTRC("Sort Files"));
4170
button->set_accessibility_name(TTRC("Sort Files"));
4171
4172
PopupMenu *p = button->get_popup();
4173
p->connect(SceneStringName(id_pressed), callable_mp(this, &FileSystemDock::_file_sort_popup));
4174
p->add_radio_check_item(TTRC("Sort by Name (Ascending)"), (int)FileSortOption::FILE_SORT_NAME);
4175
p->add_radio_check_item(TTRC("Sort by Name (Descending)"), (int)FileSortOption::FILE_SORT_NAME_REVERSE);
4176
p->add_radio_check_item(TTRC("Sort by Type (Ascending)"), (int)FileSortOption::FILE_SORT_TYPE);
4177
p->add_radio_check_item(TTRC("Sort by Type (Descending)"), (int)FileSortOption::FILE_SORT_TYPE_REVERSE);
4178
p->add_radio_check_item(TTRC("Sort by Last Modified"), (int)FileSortOption::FILE_SORT_MODIFIED_TIME);
4179
p->add_radio_check_item(TTRC("Sort by First Modified"), (int)FileSortOption::FILE_SORT_MODIFIED_TIME_REVERSE);
4180
p->set_item_checked((int)file_sort, true);
4181
return button;
4182
}
4183
4184
void FileSystemDock::update_layout(EditorDock::DockLayout p_layout) {
4185
bool new_horizontal = (p_layout == EditorDock::DOCK_LAYOUT_HORIZONTAL);
4186
if (horizontal == new_horizontal) {
4187
return;
4188
}
4189
horizontal = new_horizontal;
4190
4191
if (horizontal) {
4192
path_hb->reparent(toolbar_hbc);
4193
toolbar_hbc->move_child(path_hb, 2);
4194
set_meta("_dock_display_mode", get_display_mode());
4195
set_meta("_dock_file_display_mode", get_file_list_display_mode());
4196
4197
FileSystemDock::DisplayMode new_display_mode = FileSystemDock::DisplayMode(int(get_meta("_bottom_display_mode", int(FileSystemDock::DISPLAY_MODE_HSPLIT))));
4198
FileSystemDock::FileListDisplayMode new_file_display_mode = FileSystemDock::FileListDisplayMode(int(get_meta("_bottom_file_display_mode", int(FileSystemDock::FILE_LIST_DISPLAY_THUMBNAILS))));
4199
4200
set_display_mode(new_display_mode);
4201
set_file_list_display_mode(new_file_display_mode);
4202
set_custom_minimum_size(Size2(0, 200) * EDSCALE);
4203
} else {
4204
path_hb->reparent(file_list_vb);
4205
file_list_vb->move_child(path_hb, 0);
4206
set_meta("_bottom_display_mode", get_display_mode());
4207
set_meta("_bottom_file_display_mode", get_file_list_display_mode());
4208
4209
FileSystemDock::DisplayMode new_display_mode = FileSystemDock::DISPLAY_MODE_TREE_ONLY;
4210
FileSystemDock::FileListDisplayMode new_file_display_mode = FileSystemDock::FILE_LIST_DISPLAY_LIST;
4211
4212
new_display_mode = FileSystemDock::DisplayMode(int(get_meta("_dock_display_mode", new_display_mode)));
4213
new_file_display_mode = FileSystemDock::FileListDisplayMode(int(get_meta("_dock_file_display_mode", new_file_display_mode)));
4214
4215
set_display_mode(new_display_mode);
4216
set_file_list_display_mode(new_file_display_mode);
4217
set_custom_minimum_size(Size2(0, 0));
4218
}
4219
}
4220
4221
void FileSystemDock::save_layout_to_config(Ref<ConfigFile> &p_layout, const String &p_section) const {
4222
p_layout->set_value(p_section, "h_split_offset", get_h_split_offset());
4223
p_layout->set_value(p_section, "v_split_offset", get_v_split_offset());
4224
p_layout->set_value(p_section, "display_mode", get_display_mode());
4225
p_layout->set_value(p_section, "file_sort", (int)get_file_sort());
4226
p_layout->set_value(p_section, "file_list_display_mode", get_file_list_display_mode());
4227
PackedStringArray selected_files = get_selected_paths();
4228
p_layout->set_value(p_section, "selected_paths", selected_files);
4229
Vector<String> uncollapsed_paths = get_uncollapsed_paths();
4230
p_layout->set_value(p_section, "uncollapsed_paths", searched_tokens.is_empty() ? uncollapsed_paths : uncollapsed_paths_before_search);
4231
}
4232
4233
void FileSystemDock::load_layout_from_config(const Ref<ConfigFile> &p_layout, const String &p_section) {
4234
if (p_layout->has_section_key(p_section, "h_split_offset")) {
4235
int fs_h_split_ofs = p_layout->get_value(p_section, "h_split_offset");
4236
set_h_split_offset(fs_h_split_ofs);
4237
}
4238
4239
if (p_layout->has_section_key(p_section, "v_split_offset")) {
4240
int fs_v_split_ofs = p_layout->get_value(p_section, "v_split_offset");
4241
set_v_split_offset(fs_v_split_ofs);
4242
}
4243
4244
if (p_layout->has_section_key(p_section, "display_mode")) {
4245
DisplayMode dock_filesystem_display_mode = DisplayMode(int(p_layout->get_value(p_section, "display_mode")));
4246
set_display_mode(dock_filesystem_display_mode);
4247
}
4248
4249
if (p_layout->has_section_key(p_section, "file_sort")) {
4250
FileSortOption dock_filesystem_file_sort = FileSortOption(int(p_layout->get_value(p_section, "file_sort")));
4251
set_file_sort(dock_filesystem_file_sort);
4252
}
4253
4254
if (p_layout->has_section_key(p_section, "file_list_display_mode")) {
4255
FileListDisplayMode dock_filesystem_file_list_display_mode = FileListDisplayMode(int(p_layout->get_value(p_section, "file_list_display_mode")));
4256
set_file_list_display_mode(dock_filesystem_file_list_display_mode);
4257
}
4258
4259
if (p_layout->has_section_key(p_section, "selected_paths")) {
4260
PackedStringArray dock_filesystem_selected_paths = p_layout->get_value(p_section, "selected_paths");
4261
for (int i = 0; i < dock_filesystem_selected_paths.size(); i++) {
4262
select_file(dock_filesystem_selected_paths[i]);
4263
}
4264
}
4265
4266
// Restore collapsed state.
4267
PackedStringArray uncollapsed_tis;
4268
if (p_layout->has_section_key(p_section, "uncollapsed_paths")) {
4269
uncollapsed_tis = p_layout->get_value(p_section, "uncollapsed_paths");
4270
} else {
4271
uncollapsed_tis = { "res://" };
4272
}
4273
4274
if (!uncollapsed_tis.is_empty()) {
4275
for (int i = 0; i < uncollapsed_tis.size(); i++) {
4276
TreeItem *uncollapsed_ti = get_tree_control()->get_item_with_metadata(uncollapsed_tis[i], 0);
4277
if (uncollapsed_ti) {
4278
uncollapsed_ti->set_collapsed(false);
4279
}
4280
}
4281
get_tree_control()->queue_redraw();
4282
}
4283
}
4284
4285
void FileSystemDock::_bind_methods() {
4286
ClassDB::bind_method(D_METHOD("navigate_to_path", "path"), &FileSystemDock::navigate_to_path);
4287
4288
ClassDB::bind_method(D_METHOD("add_resource_tooltip_plugin", "plugin"), &FileSystemDock::add_resource_tooltip_plugin);
4289
ClassDB::bind_method(D_METHOD("remove_resource_tooltip_plugin", "plugin"), &FileSystemDock::remove_resource_tooltip_plugin);
4290
4291
ADD_SIGNAL(MethodInfo("inherit", PropertyInfo(Variant::STRING, "file")));
4292
ADD_SIGNAL(MethodInfo("instantiate", PropertyInfo(Variant::PACKED_STRING_ARRAY, "files")));
4293
4294
ADD_SIGNAL(MethodInfo("resource_removed", PropertyInfo(Variant::OBJECT, "resource", PROPERTY_HINT_RESOURCE_TYPE, Resource::get_class_static())));
4295
ADD_SIGNAL(MethodInfo("file_removed", PropertyInfo(Variant::STRING, "file")));
4296
ADD_SIGNAL(MethodInfo("folder_removed", PropertyInfo(Variant::STRING, "folder")));
4297
ADD_SIGNAL(MethodInfo("files_moved", PropertyInfo(Variant::STRING, "old_file"), PropertyInfo(Variant::STRING, "new_file")));
4298
ADD_SIGNAL(MethodInfo("folder_moved", PropertyInfo(Variant::STRING, "old_folder"), PropertyInfo(Variant::STRING, "new_folder")));
4299
ADD_SIGNAL(MethodInfo("folder_color_changed"));
4300
ADD_SIGNAL(MethodInfo("selection_changed"));
4301
4302
ADD_SIGNAL(MethodInfo("display_mode_changed"));
4303
}
4304
4305
FileSystemDock::FileSystemDock() {
4306
singleton = this;
4307
set_name(TTRC("FileSystem"));
4308
set_icon_name("Folder");
4309
set_dock_shortcut(ED_SHORTCUT_AND_COMMAND("docks/open_filesystem", TTRC("Open FileSystem Dock"), KeyModifierMask::ALT | Key::F));
4310
set_default_slot(EditorDock::DOCK_SLOT_LEFT_BR);
4311
set_available_layouts(DOCK_LAYOUT_ALL);
4312
4313
ProjectSettings::get_singleton()->add_hidden_prefix("file_customization/");
4314
4315
// `KeyModifierMask::CMD_OR_CTRL | Key::C` conflicts with other editor shortcuts.
4316
ED_SHORTCUT("filesystem_dock/copy_path", TTRC("Copy Path"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::C);
4317
ED_SHORTCUT("filesystem_dock/copy_absolute_path", TTRC("Copy Absolute Path"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::ALT | Key::C);
4318
ED_SHORTCUT("filesystem_dock/copy_uid", TTRC("Copy UID"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::ALT | KeyModifierMask::SHIFT | Key::C);
4319
ED_SHORTCUT("filesystem_dock/duplicate", TTRC("Duplicate..."), KeyModifierMask::CMD_OR_CTRL | Key::D);
4320
ED_SHORTCUT("filesystem_dock/delete", TTRC("Delete"), Key::KEY_DELETE);
4321
ED_SHORTCUT("filesystem_dock/new_folder", TTRC("New Folder..."), Key::NONE);
4322
ED_SHORTCUT("filesystem_dock/new_scene", TTRC("New Scene..."), Key::NONE);
4323
ED_SHORTCUT("filesystem_dock/new_script", TTRC("New Script..."), Key::NONE);
4324
ED_SHORTCUT("filesystem_dock/new_resource", TTRC("New Resource..."), Key::NONE);
4325
ED_SHORTCUT("filesystem_dock/new_textfile", TTRC("New TextFile..."), Key::NONE);
4326
ED_SHORTCUT("filesystem_dock/rename", TTRC("Rename..."), Key::F2);
4327
ED_SHORTCUT_OVERRIDE("filesystem_dock/rename", "macos", Key::ENTER);
4328
#if !defined(ANDROID_ENABLED) && !defined(WEB_ENABLED)
4329
// Opening the system file manager or opening in an external program is not supported on the Android and web editors.
4330
ED_SHORTCUT("filesystem_dock/show_in_explorer", TTRC("Open in File Manager"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::ALT | Key::R);
4331
ED_SHORTCUT("filesystem_dock/open_in_external_program", TTRC("Open in External Program"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::ALT | Key::E);
4332
ED_SHORTCUT("filesystem_dock/open_in_terminal", TTRC("Open in Terminal"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::ALT | Key::T);
4333
#endif
4334
4335
ED_SHORTCUT("filesystem_dock/focus_path", TTRC("Focus Path"), KeyModifierMask::CMD_OR_CTRL | Key::L);
4336
// Allow both Cmd + L and Cmd + Shift + G to match Safari's and Finder's shortcuts respectively.
4337
ED_SHORTCUT_OVERRIDE_ARRAY("filesystem_dock/focus_path", "macos",
4338
{ int32_t(KeyModifierMask::META | Key::L), int32_t(KeyModifierMask::META | KeyModifierMask::SHIFT | Key::G) });
4339
4340
// Properly translating color names would require a separate HashMap, so for simplicity they are provided as comments.
4341
folder_colors["red"] = Color(1.0, 0.271, 0.271); // TTR("Red")
4342
folder_colors["orange"] = Color(1.0, 0.561, 0.271); // TTR("Orange")
4343
folder_colors["yellow"] = Color(1.0, 0.890, 0.271); // TTR("Yellow")
4344
folder_colors["green"] = Color(0.502, 1.0, 0.271); // TTR("Green")
4345
folder_colors["teal"] = Color(0.271, 1.0, 0.635); // TTR("Teal")
4346
folder_colors["blue"] = Color(0.271, 0.843, 1.0); // TTR("Blue")
4347
folder_colors["purple"] = Color(0.502, 0.271, 1.0); // TTR("Purple")
4348
folder_colors["pink"] = Color(1.0, 0.271, 0.588); // TTR("Pink")
4349
folder_colors["gray"] = Color(0.616, 0.616, 0.616); // TTR("Gray")
4350
4351
assigned_folder_colors = ProjectSettings::get_singleton()->get_setting("file_customization/folder_colors");
4352
4353
editor_is_dark_icon_and_font = EditorThemeManager::is_dark_icon_and_font();
4354
4355
VBoxContainer *main_vb = memnew(VBoxContainer);
4356
add_child(main_vb);
4357
4358
VBoxContainer *top_vbc = memnew(VBoxContainer);
4359
main_vb->add_child(top_vbc);
4360
4361
toolbar_hbc = memnew(HBoxContainer);
4362
top_vbc->add_child(toolbar_hbc);
4363
4364
HBoxContainer *nav_hbc = memnew(HBoxContainer);
4365
nav_hbc->add_theme_constant_override("separation", 0);
4366
toolbar_hbc->add_child(nav_hbc);
4367
4368
button_hist_prev = memnew(Button);
4369
button_hist_prev->set_theme_type_variation(SceneStringName(FlatButton));
4370
button_hist_prev->set_disabled(true);
4371
button_hist_prev->set_focus_mode(FOCUS_ACCESSIBILITY);
4372
button_hist_prev->set_tooltip_text(TTRC("Go to previous selected folder/file."));
4373
nav_hbc->add_child(button_hist_prev);
4374
4375
button_hist_next = memnew(Button);
4376
button_hist_next->set_theme_type_variation(SceneStringName(FlatButton));
4377
button_hist_next->set_disabled(true);
4378
button_hist_next->set_focus_mode(FOCUS_ACCESSIBILITY);
4379
button_hist_next->set_tooltip_text(TTRC("Go to next selected folder/file."));
4380
nav_hbc->add_child(button_hist_next);
4381
4382
current_path_line_edit = memnew(LineEdit);
4383
current_path_line_edit->set_structured_text_bidi_override(TextServer::STRUCTURED_TEXT_FILE);
4384
current_path_line_edit->set_accessibility_name(TTRC("Path"));
4385
current_path_line_edit->set_h_size_flags(SIZE_EXPAND_FILL);
4386
_set_current_path_line_edit_text(current_path);
4387
toolbar_hbc->add_child(current_path_line_edit);
4388
4389
button_toggle_display_mode = memnew(Button);
4390
button_toggle_display_mode->connect(SceneStringName(pressed), callable_mp(this, &FileSystemDock::_change_split_mode));
4391
button_toggle_display_mode->set_focus_mode(FOCUS_ACCESSIBILITY);
4392
button_toggle_display_mode->set_tooltip_text(TTRC("Change Split Mode"));
4393
button_toggle_display_mode->set_theme_type_variation("FlatMenuButton");
4394
toolbar_hbc->add_child(button_toggle_display_mode);
4395
4396
toolbar2_hbc = memnew(HBoxContainer);
4397
top_vbc->add_child(toolbar2_hbc);
4398
4399
tree_search_box = memnew(LineEdit);
4400
tree_search_box->set_h_size_flags(SIZE_EXPAND_FILL);
4401
tree_search_box->set_placeholder(TTRC("Filter Files"));
4402
tree_search_box->set_clear_button_enabled(true);
4403
tree_search_box->connect(SceneStringName(text_changed), callable_mp(this, &FileSystemDock::_search_changed).bind(tree_search_box));
4404
toolbar2_hbc->add_child(tree_search_box);
4405
4406
tree_button_sort = _create_file_menu_button();
4407
toolbar2_hbc->add_child(tree_button_sort);
4408
4409
file_list_popup = memnew(PopupMenu);
4410
4411
add_child(file_list_popup);
4412
4413
tree_popup = memnew(PopupMenu);
4414
4415
add_child(tree_popup);
4416
4417
split_box = memnew(SplitContainer);
4418
split_box->set_v_size_flags(SIZE_EXPAND_FILL);
4419
split_box->connect("dragged", callable_mp(this, &FileSystemDock::_split_dragged));
4420
split_box_offset_h = 240 * EDSCALE;
4421
main_vb->add_child(split_box);
4422
4423
tree_mc = memnew(MarginContainer);
4424
split_box->add_child(tree_mc);
4425
tree_mc->set_theme_type_variation("NoBorderHorizontalBottom");
4426
tree_mc->set_v_size_flags(Control::SIZE_EXPAND_FILL);
4427
4428
tree = memnew(FileSystemTree);
4429
tree->set_accessibility_name(TTRC("Directories"));
4430
tree->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
4431
tree->set_hide_root(true);
4432
tree->set_scroll_hint_mode(Tree::SCROLL_HINT_MODE_TOP);
4433
SET_DRAG_FORWARDING_GCD(tree, FileSystemDock);
4434
tree->set_allow_rmb_select(true);
4435
tree->set_select_mode(Tree::SELECT_MULTI);
4436
tree->set_custom_minimum_size(Size2(40 * EDSCALE, 15 * EDSCALE));
4437
tree->set_column_clip_content(0, true);
4438
tree_mc->add_child(tree);
4439
4440
tree->connect("item_activated", callable_mp(this, &FileSystemDock::_tree_activate_file));
4441
tree->connect("multi_selected", callable_mp(this, &FileSystemDock::_tree_multi_selected));
4442
tree->connect("item_mouse_selected", callable_mp(this, &FileSystemDock::_tree_rmb_select));
4443
tree->connect("empty_clicked", callable_mp(this, &FileSystemDock::_tree_empty_click));
4444
tree->connect("nothing_selected", callable_mp(this, &FileSystemDock::_tree_empty_selected));
4445
tree->connect(SceneStringName(gui_input), callable_mp(this, &FileSystemDock::_tree_gui_input));
4446
tree->connect(SceneStringName(mouse_exited), callable_mp(this, &FileSystemDock::_tree_mouse_exited));
4447
tree->connect("item_edited", callable_mp(this, &FileSystemDock::_rename_operation_confirm));
4448
4449
file_list_vb = memnew(VBoxContainer);
4450
file_list_vb->set_v_size_flags(SIZE_EXPAND_FILL);
4451
split_box->add_child(file_list_vb);
4452
4453
path_hb = memnew(HBoxContainer);
4454
path_hb->set_h_size_flags(SIZE_EXPAND_FILL);
4455
file_list_vb->add_child(path_hb);
4456
4457
file_list_search_box = memnew(LineEdit);
4458
file_list_search_box->set_h_size_flags(SIZE_EXPAND_FILL);
4459
file_list_search_box->set_placeholder(TTRC("Filter Files"));
4460
file_list_search_box->set_accessibility_name(TTRC("Filter Files"));
4461
file_list_search_box->set_clear_button_enabled(true);
4462
file_list_search_box->connect(SceneStringName(text_changed), callable_mp(this, &FileSystemDock::_search_changed).bind(file_list_search_box));
4463
path_hb->add_child(file_list_search_box);
4464
4465
file_list_button_sort = _create_file_menu_button();
4466
path_hb->add_child(file_list_button_sort);
4467
4468
button_file_list_display_mode = memnew(Button);
4469
button_file_list_display_mode->set_accessibility_name(TTRC("Display Mode"));
4470
button_file_list_display_mode->set_theme_type_variation("FlatMenuButton");
4471
path_hb->add_child(button_file_list_display_mode);
4472
4473
files_mc = memnew(MarginContainer);
4474
file_list_vb->add_child(files_mc);
4475
files_mc->set_theme_type_variation("NoBorderHorizontalBottom");
4476
files_mc->set_v_size_flags(Control::SIZE_EXPAND_FILL);
4477
4478
files = memnew(FileSystemList);
4479
files->set_accessibility_name(TTRC("Files"));
4480
files->set_select_mode(ItemList::SELECT_MULTI);
4481
files->set_scroll_hint_mode(ItemList::SCROLL_HINT_MODE_TOP);
4482
SET_DRAG_FORWARDING_GCD(files, FileSystemDock);
4483
files->connect("item_clicked", callable_mp(this, &FileSystemDock::_file_list_item_clicked));
4484
files->connect(SceneStringName(gui_input), callable_mp(this, &FileSystemDock::_file_list_gui_input));
4485
files->connect("multi_selected", callable_mp(this, &FileSystemDock::_file_multi_selected));
4486
files->connect("empty_clicked", callable_mp(this, &FileSystemDock::_file_list_empty_clicked));
4487
files->connect("item_edited", callable_mp(this, &FileSystemDock::_rename_operation_confirm));
4488
files->set_custom_minimum_size(Size2(0, 15 * EDSCALE));
4489
files->set_allow_rmb_select(true);
4490
files_mc->add_child(files);
4491
4492
scanning_vb = memnew(VBoxContainer);
4493
scanning_vb->hide();
4494
main_vb->add_child(scanning_vb);
4495
4496
Label *slabel = memnew(Label);
4497
slabel->set_text(TTRC("Scanning Files,\nPlease Wait..."));
4498
slabel->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
4499
scanning_vb->add_child(slabel);
4500
4501
scanning_progress = memnew(ProgressBar);
4502
scanning_progress->set_accessibility_name(TTRC("Filesystem Scan"));
4503
scanning_vb->add_child(scanning_progress);
4504
4505
deps_editor = memnew(DependencyEditor);
4506
add_child(deps_editor);
4507
4508
owners_editor = memnew(DependencyEditorOwners());
4509
add_child(owners_editor);
4510
4511
remove_dialog = memnew(DependencyRemoveDialog);
4512
remove_dialog->connect("resource_removed", callable_mp(this, &FileSystemDock::_resource_removed));
4513
remove_dialog->connect("file_removed", callable_mp(this, &FileSystemDock::_file_removed));
4514
remove_dialog->connect("folder_removed", callable_mp(this, &FileSystemDock::_folder_removed));
4515
add_child(remove_dialog);
4516
4517
move_dialog = memnew(EditorDirDialog);
4518
add_child(move_dialog);
4519
move_dialog->connect("move_pressed", callable_mp(this, &FileSystemDock::_move_operation_confirm).bind(false, OVERWRITE_UNDECIDED));
4520
move_dialog->connect("copy_pressed", callable_mp(this, &FileSystemDock::_move_operation_confirm).bind(true, OVERWRITE_UNDECIDED));
4521
4522
overwrite_dialog = memnew(ConfirmationDialog);
4523
add_child(overwrite_dialog);
4524
overwrite_dialog->set_ok_button_text(TTRC("Overwrite"));
4525
overwrite_dialog->add_button(TTRC("Keep Both"), true)->connect(SceneStringName(pressed), callable_mp(this, &FileSystemDock::_overwrite_dialog_action).bind(false));
4526
overwrite_dialog->connect(SceneStringName(confirmed), callable_mp(this, &FileSystemDock::_overwrite_dialog_action).bind(true));
4527
4528
VBoxContainer *overwrite_dialog_vb = memnew(VBoxContainer);
4529
overwrite_dialog->add_child(overwrite_dialog_vb);
4530
4531
overwrite_dialog_header = memnew(Label);
4532
overwrite_dialog_vb->add_child(overwrite_dialog_header);
4533
4534
overwrite_dialog_scroll = memnew(ScrollContainer);
4535
overwrite_dialog_vb->add_child(overwrite_dialog_scroll);
4536
overwrite_dialog_scroll->set_custom_minimum_size(Vector2(400, 600) * EDSCALE);
4537
4538
overwrite_dialog_file_list = memnew(Label);
4539
overwrite_dialog_scroll->add_child(overwrite_dialog_file_list);
4540
4541
overwrite_dialog_footer = memnew(Label);
4542
overwrite_dialog_vb->add_child(overwrite_dialog_footer);
4543
4544
make_dir_dialog = memnew(DirectoryCreateDialog);
4545
add_child(make_dir_dialog);
4546
4547
make_scene_dialog = memnew(SceneCreateDialog);
4548
add_child(make_scene_dialog);
4549
make_scene_dialog->connect(SceneStringName(confirmed), callable_mp(this, &FileSystemDock::_make_scene_confirm));
4550
4551
make_script_dialog = memnew(ScriptCreateDialog);
4552
make_script_dialog->set_title(TTRC("Create Script"));
4553
add_child(make_script_dialog);
4554
make_script_dialog->connect("script_created", callable_mp(this, &FileSystemDock::_script_or_shader_created));
4555
4556
make_shader_dialog = memnew(ShaderCreateDialog);
4557
add_child(make_shader_dialog);
4558
make_shader_dialog->connect("shader_created", callable_mp(this, &FileSystemDock::_script_or_shader_created));
4559
make_shader_dialog->connect("shader_include_created", callable_mp(this, &FileSystemDock::_script_or_shader_created));
4560
4561
new_resource_dialog = memnew(CreateDialog);
4562
add_child(new_resource_dialog);
4563
new_resource_dialog->set_base_type("Resource");
4564
new_resource_dialog->connect("create", callable_mp(this, &FileSystemDock::_resource_created));
4565
4566
conversion_dialog = memnew(ConfirmationDialog);
4567
add_child(conversion_dialog);
4568
conversion_dialog->set_ok_button_text(TTRC("Convert"));
4569
conversion_dialog->connect(SceneStringName(confirmed), callable_mp(this, &FileSystemDock::_convert_dialog_action));
4570
4571
move_confirm_dialog = memnew(ConfirmationDialog);
4572
add_child(move_confirm_dialog);
4573
move_confirm_dialog->connect(SceneStringName(confirmed), callable_mp(this, &FileSystemDock::_move_confirm));
4574
4575
uncollapsed_paths_before_search = Vector<String>();
4576
4577
tree_update_id = 0;
4578
4579
history_pos = 0;
4580
history_max_size = 20;
4581
history.push_back("res://");
4582
4583
display_mode = DISPLAY_MODE_TREE_ONLY;
4584
old_display_mode = DISPLAY_MODE_TREE_ONLY;
4585
file_list_display_mode = FILE_LIST_DISPLAY_THUMBNAILS;
4586
4587
ProjectSettings::get_singleton()->connect("settings_changed", callable_mp(this, &FileSystemDock::_project_settings_changed));
4588
EditorSettings::get_singleton()->connect("_favorites_changed", callable_mp(this, &FileSystemDock::update_all));
4589
main_scene_path = ResourceUID::ensure_path(GLOBAL_GET("application/run/main_scene"));
4590
4591
add_resource_tooltip_plugin(memnew(EditorTextureTooltipPlugin));
4592
add_resource_tooltip_plugin(memnew(EditorAudioStreamTooltipPlugin));
4593
}
4594
4595
FileSystemDock::~FileSystemDock() {
4596
singleton = nullptr;
4597
}
4598
4599