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