diff --git a/doc/classes/FileDialog.xml b/doc/classes/FileDialog.xml index 4f2c61a2a1d..7989b357067 100644 --- a/doc/classes/FileDialog.xml +++ b/doc/classes/FileDialog.xml @@ -240,6 +240,15 @@ Custom icon for the create folder button. + + Custom icon for favorite folder button. + + + Custom icon for button to move down a favorite entry. + + + Custom icon for button to move up a favorite entry. + Custom icon for files. diff --git a/scene/gui/file_dialog.cpp b/scene/gui/file_dialog.cpp index b887a0b0ff6..e0c28206dc8 100644 --- a/scene/gui/file_dialog.cpp +++ b/scene/gui/file_dialog.cpp @@ -43,6 +43,7 @@ #include "scene/gui/menu_button.h" #include "scene/gui/option_button.h" #include "scene/gui/separator.h" +#include "scene/gui/split_container.h" #include "scene/theme/theme_db.h" void FileDialog::popup_file_dialog() { @@ -249,11 +250,13 @@ void FileDialog::_notification(int p_what) { } break; case NOTIFICATION_VISIBILITY_CHANGED: { - if (!is_visible()) { + if (is_visible()) { + _update_favorite_list(); + _update_recent_list(); + invalidate(); // Put it here to preview in the editor. + } else { set_process_shortcut_input(false); } - - invalidate(); // Put it here to preview in the editor. } break; case NOTIFICATION_THEME_CHANGED: { @@ -266,10 +269,13 @@ void FileDialog::_notification(int p_what) { } _setup_button(dir_up, theme_cache.parent_folder); _setup_button(refresh_button, theme_cache.reload); + _setup_button(favorite_button, theme_cache.favorite); _setup_button(show_hidden, theme_cache.toggle_hidden); _setup_button(make_dir_button, theme_cache.create_folder); _setup_button(show_filename_filter_button, theme_cache.toggle_filename_filter); _setup_button(file_sort_button, theme_cache.sort); + _setup_button(fav_up_button, theme_cache.favorite_up); + _setup_button(fav_down_button, theme_cache.favorite_down); invalidate(); } break; @@ -400,6 +406,8 @@ void FileDialog::_file_submitted(const String &p_file) { } void FileDialog::_save_confirm_pressed() { + _save_to_recent(); + String f = dir_access->get_current_dir().path_join(filename_edit->get_text()); emit_signal(SNAME("file_selected"), f); hide(); @@ -443,6 +451,7 @@ void FileDialog::_action_pressed() { if (mode == FILE_MODE_OPEN_FILES) { const Vector files = get_selected_files(); if (!files.is_empty()) { + _save_to_recent(); emit_signal(SNAME("files_selected"), files); hide(); } @@ -453,6 +462,7 @@ void FileDialog::_action_pressed() { String f = file_text.is_absolute_path() ? file_text : dir_access->get_current_dir().path_join(file_text); if ((mode == FILE_MODE_OPEN_ANY || mode == FILE_MODE_OPEN_FILE) && (dir_access->file_exists(f) || dir_access->is_bundle(f))) { + _save_to_recent(); emit_signal(SNAME("file_selected"), f); hide(); } else if (mode == FILE_MODE_OPEN_ANY || mode == FILE_MODE_OPEN_DIR) { @@ -467,6 +477,7 @@ void FileDialog::_action_pressed() { } } + _save_to_recent(); emit_signal(SNAME("dir_selected"), path); hide(); } @@ -528,6 +539,7 @@ void FileDialog::_action_pressed() { confirm_save->set_text(vformat(atr(ETR("File \"%s\" already exists.\nDo you want to overwrite it?")), f)); confirm_save->popup_centered(Size2(250, 80)); } else { + _save_to_recent(); emit_signal(SNAME("file_selected"), f); hide(); } @@ -937,6 +949,21 @@ void FileDialog::update_file_list() { _file_list_select_first(); } } + + favorite_list->deselect_all(); + favorite_button->set_pressed(false); + + const int fav_count = favorite_list->get_item_count(); + for (int i = 0; i < fav_count; i++) { + const String fav_dir = favorite_list->get_item_metadata(i); + if (fav_dir != base_dir && fav_dir != base_dir + "/") { + continue; + } + favorite_list->select(i); + favorite_button->set_pressed(true); + break; + } + _update_fav_buttons(); } void FileDialog::_filter_selected(int) { @@ -1274,6 +1301,8 @@ void FileDialog::set_access(Access p_access) { invalidate(); update_filters(); update_dir(); + _update_favorite_list(); + _update_recent_list(); } void FileDialog::invalidate() { @@ -1382,6 +1411,223 @@ void FileDialog::_sort_option_selected(int p_option) { invalidate(); } +void FileDialog::_favorite_selected(int p_item) { + ERR_FAIL_UNSIGNED_INDEX((uint32_t)p_item, global_favorites.size()); + _change_dir(favorite_list->get_item_metadata(p_item)); + _push_history(); +} + +void FileDialog::_favorite_pressed() { + String directory = get_current_dir(); + if (!directory.ends_with("/")) { + directory += "/"; + } + + bool found = false; + for (const String &name : global_favorites) { + if (!_path_matches_access(name)) { + continue; + } + + if (name == directory) { + found = true; + break; + } + } + + if (found) { + global_favorites.erase(directory); + } else { + global_favorites.push_back(directory); + } + _update_favorite_list(); +} + +void FileDialog::_favorite_move_up() { + int current = favorite_list->get_current(); + if (current <= 0) { + return; + } + + int a_idx = global_favorites.find(favorite_list->get_item_metadata(current - 1)); + int b_idx = global_favorites.find(favorite_list->get_item_metadata(current)); + + if (a_idx == -1 || b_idx == -1) { + return; + } + SWAP(global_favorites[a_idx], global_favorites[b_idx]); + _update_favorite_list(); +} + +void FileDialog::_favorite_move_down() { + int current = favorite_list->get_current(); + if (current == -1 || current >= favorite_list->get_item_count() - 1) { + return; + } + + int a_idx = global_favorites.find(favorite_list->get_item_metadata(current)); + int b_idx = global_favorites.find(favorite_list->get_item_metadata(current + 1)); + + if (a_idx == -1 || b_idx == -1) { + return; + } + SWAP(global_favorites[a_idx], global_favorites[b_idx]); + _update_favorite_list(); +} + +void FileDialog::_update_favorite_list() { + const String current = get_current_dir(); + + favorite_list->clear(); + favorite_button->set_pressed(false); + + Vector favorited_paths; + Vector favorited_names; + + int current_favorite = -1; + for (uint32_t i = 0; i < global_favorites.size(); i++) { + String name = global_favorites[i]; + if (!_path_matches_access(name)) { + continue; + } + + if (!name.ends_with("/") || !name.begins_with(root_prefix)) { + continue; + } + + if (!dir_access->dir_exists(name)) { + // Remove invalid directory from the list of favorited directories. + global_favorites.remove_at(i); + i--; + continue; + } + + if (name == current) { + current_favorite = favorited_names.size(); + } + favorited_paths.append(name); + + // Compute favorite display text. + if (name == "res://" || name == "user://") { + name = "/"; + } else { + if (current_favorite == -1 && name == current + "/") { + current_favorite = favorited_names.size(); + } + name = name.trim_suffix("/"); + name = name.get_file(); + } + favorited_names.append(name); + } + + // EditorNode::disambiguate_filenames(favorited_paths, favorited_names); // TODO Needs a non-editor method. + + const int favorites_size = favorited_paths.size(); + for (int i = 0; i < favorites_size; i++) { + favorite_list->add_item(favorited_names[i], theme_cache.folder); + favorite_list->set_item_tooltip(-1, favorited_paths[i]); + favorite_list->set_item_metadata(-1, favorited_paths[i]); + + if (i == current_favorite) { + favorite_button->set_pressed(true); + favorite_list->set_current(favorite_list->get_item_count() - 1); + recent_list->deselect_all(); + } + } + _update_fav_buttons(); +} + +void FileDialog::_update_fav_buttons() { + const int current = favorite_list->get_current(); + fav_up_button->set_disabled(current < 1); + fav_down_button->set_disabled(current == -1 || current >= favorite_list->get_item_count() - 1); +} + +void FileDialog::_recent_selected(int p_item) { + ERR_FAIL_UNSIGNED_INDEX((uint32_t)p_item, global_recents.size()); + _change_dir(recent_list->get_item_metadata(p_item)); + _push_history(); +} + +void FileDialog::_save_to_recent() { + String directory = get_current_dir(); + if (!directory.ends_with("/")) { + directory += "/"; + } + + int count = 0; + for (uint32_t i = 0; i < global_recents.size(); i++) { + const String &dir = global_recents[i]; + if (!_path_matches_access(dir)) { + continue; + } + + if (dir == directory || count > MAX_RECENTS) { + global_recents.remove_at(i); + i--; + } else { + count++; + } + } + global_recents.insert(0, directory); + + _update_recent_list(); +} + +void FileDialog::_update_recent_list() { + recent_list->clear(); + + Vector recent_dir_paths; + Vector recent_dir_names; + + for (uint32_t i = 0; i < global_recents.size(); i++) { + String name = global_recents[i]; + if (!_path_matches_access(name)) { + continue; + } + + if (!name.begins_with(root_prefix)) { + continue; + } + + if (!dir_access->dir_exists(name)) { + // Remove invalid directory from the list of recent directories. + global_recents.remove_at(i); + i--; + continue; + } + recent_dir_paths.append(name); + + // Compute recent directory display text. + if (name == "res://" || name == "user://") { + name = "/"; + } else { + name = name.trim_suffix("/").get_file(); + } + recent_dir_names.append(name); + } + + // EditorNode::disambiguate_filenames(recent_dir_paths, recent_dir_names); // TODO Needs a non-editor method. + + const int recent_size = recent_dir_paths.size(); + for (int i = 0; i < recent_size; i++) { + recent_list->add_item(recent_dir_names[i], theme_cache.folder); + recent_list->set_item_tooltip(-1, recent_dir_paths[i]); + recent_list->set_item_metadata(-1, recent_dir_paths[i]); + } +} + +bool FileDialog::_path_matches_access(const String &p_path) const { + bool is_res = p_path.begins_with("res://"); + bool is_user = p_path.begins_with("user://"); + if (access == ACCESS_RESOURCES) { + return is_res; + } else if (access == ACCESS_USERDATA) { + return is_user; + } + return !is_res && !is_user; +} + TypedArray FileDialog::_get_options() const { TypedArray out; for (const FileDialog::Option &opt : options) { @@ -1626,12 +1872,15 @@ void FileDialog::_bind_methods() { BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, FileDialog, forward_folder); BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, FileDialog, back_folder); BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, FileDialog, reload); + BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, FileDialog, favorite); BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, FileDialog, toggle_hidden); BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, FileDialog, folder); BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, FileDialog, toggle_filename_filter); BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, FileDialog, file); BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, FileDialog, create_folder); BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, FileDialog, sort); + BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, FileDialog, favorite_up); + BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, FileDialog, favorite_down); BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, FileDialog, folder_icon_color); BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, FileDialog, file_icon_color); @@ -1775,6 +2024,13 @@ FileDialog::FileDialog() { top_toolbar->add_child(refresh_button); refresh_button->connect(SceneStringName(pressed), callable_mp(this, &FileDialog::update_file_list)); + favorite_button = memnew(Button); + favorite_button->set_theme_type_variation(SceneStringName(FlatButton)); + favorite_button->set_accessibility_name(TTRC("(Un)favorite Folder")); + favorite_button->set_tooltip_text(TTRC("(Un)favorite current folder.")); + top_toolbar->add_child(favorite_button); + favorite_button->connect(SceneStringName(pressed), callable_mp(this, &FileDialog::_favorite_pressed)); + top_toolbar->add_child(memnew(VSeparator)); make_dir_button = memnew(Button); @@ -1784,8 +2040,66 @@ FileDialog::FileDialog() { top_toolbar->add_child(make_dir_button); make_dir_button->connect(SceneStringName(pressed), callable_mp(this, &FileDialog::_make_dir)); + HSplitContainer *main_split = memnew(HSplitContainer); + main_split->set_v_size_flags(Control::SIZE_EXPAND_FILL); + main_vbox->add_child(main_split); + + { + VSplitContainer *fav_split = memnew(VSplitContainer); + main_split->add_child(fav_split); + + VBoxContainer *fav_vbox = memnew(VBoxContainer); + fav_vbox->set_v_size_flags(Control::SIZE_EXPAND_FILL); + fav_split->add_child(fav_vbox); + + HBoxContainer *fav_hbox = memnew(HBoxContainer); + fav_vbox->add_child(fav_hbox); + + { + Label *label = memnew(Label(ETR("Favorites:"))); + label->set_h_size_flags(Control::SIZE_EXPAND_FILL); + fav_hbox->add_child(label); + } + + fav_up_button = memnew(Button); + fav_up_button->set_theme_type_variation(SceneStringName(FlatButton)); + fav_hbox->add_child(fav_up_button); + fav_up_button->connect(SceneStringName(pressed), callable_mp(this, &FileDialog::_favorite_move_up)); + + fav_down_button = memnew(Button); + fav_down_button->set_theme_type_variation(SceneStringName(FlatButton)); + fav_hbox->add_child(fav_down_button); + fav_down_button->connect(SceneStringName(pressed), callable_mp(this, &FileDialog::_favorite_move_down)); + + favorite_list = memnew(ItemList); + favorite_list->set_v_size_flags(Control::SIZE_EXPAND_FILL); + favorite_list->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED); + favorite_list->set_accessibility_name(ETR("Favorites")); + fav_vbox->add_child(favorite_list); + favorite_list->connect(SceneStringName(item_selected), callable_mp(this, &FileDialog::_favorite_selected)); + + VBoxContainer *recent_vbox = memnew(VBoxContainer); + recent_vbox->set_v_size_flags(Control::SIZE_EXPAND_FILL); + fav_split->add_child(recent_vbox); + + { + Label *label = memnew(Label(ETR("Recent:"))); + recent_vbox->add_child(label); + } + + recent_list = memnew(ItemList); + recent_list->set_v_size_flags(Control::SIZE_EXPAND_FILL); + recent_list->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED); + recent_list->set_accessibility_name(ETR("Recent")); + recent_vbox->add_child(recent_list); + recent_list->connect(SceneStringName(item_selected), callable_mp(this, &FileDialog::_recent_selected)); + } + + VBoxContainer *file_vbox = memnew(VBoxContainer); + main_split->add_child(file_vbox); + HBoxContainer *lower_toolbar = memnew(HBoxContainer); - main_vbox->add_child(lower_toolbar); + file_vbox->add_child(lower_toolbar); { Label *label = memnew(Label(ETR("Directories & Files:"))); @@ -1836,7 +2150,7 @@ FileDialog::FileDialog() { file_list->set_v_size_flags(Control::SIZE_EXPAND_FILL); file_list->set_accessibility_name(ETR("Directories and Files")); file_list->set_allow_rmb_select(true); - main_vbox->add_child(file_list); + file_vbox->add_child(file_list); file_list->connect("multi_selected", callable_mp(this, &FileDialog::_file_list_multi_selected)); file_list->connect("item_selected", callable_mp(this, &FileDialog::_file_list_selected)); file_list->connect("item_activated", callable_mp(this, &FileDialog::_file_list_item_activated)); @@ -1853,7 +2167,7 @@ FileDialog::FileDialog() { filename_filter_box = memnew(HBoxContainer); filename_filter_box->set_visible(false); - main_vbox->add_child(filename_filter_box); + file_vbox->add_child(filename_filter_box); { Label *label = memnew(Label(ETR("Filter:"))); @@ -1871,7 +2185,7 @@ FileDialog::FileDialog() { filename_filter->connect(SceneStringName(text_submitted), callable_mp(this, &FileDialog::_filename_filter_selected).unbind(1)); file_box = memnew(HBoxContainer); - main_vbox->add_child(file_box); + file_vbox->add_child(file_box); { Label *label = memnew(Label(ETR("File:"))); diff --git a/scene/gui/file_dialog.h b/scene/gui/file_dialog.h index 9374cb4f00e..4b6e64c086d 100644 --- a/scene/gui/file_dialog.h +++ b/scene/gui/file_dialog.h @@ -47,6 +47,8 @@ class VBoxContainer; class FileDialog : public ConfirmationDialog { GDCLASS(FileDialog, ConfirmationDialog); + inline static constexpr int MAX_RECENTS = 20; + struct Option { String name; Vector values; @@ -142,6 +144,9 @@ private: bool show_hidden_files = false; bool use_native_dialog = false; + inline static LocalVector global_favorites; + inline static LocalVector global_recents; + Access access = ACCESS_RESOURCES; FileMode mode = FILE_MODE_SAVE_FILE; FileSortOption file_sort = FileSortOption::NAME; @@ -179,11 +184,17 @@ private: HBoxContainer *shortcuts_container = nullptr; Button *refresh_button = nullptr; + Button *favorite_button = nullptr; Button *make_dir_button = nullptr; Button *show_hidden = nullptr; Button *show_filename_filter_button = nullptr; MenuButton *file_sort_button = nullptr; + Button *fav_up_button = nullptr; + Button *fav_down_button = nullptr; + ItemList *favorite_list = nullptr; + ItemList *recent_list = nullptr; + ItemList *file_list = nullptr; Label *message = nullptr; PopupMenu *item_menu = nullptr; @@ -215,6 +226,9 @@ private: Ref file; Ref create_folder; Ref sort; + Ref favorite; + Ref favorite_up; + Ref favorite_down; Color folder_icon_color; Color file_icon_color; @@ -265,6 +279,18 @@ private: void _update_drives(bool p_select = true); void _sort_option_selected(int p_option); + void _favorite_selected(int p_item); + void _favorite_pressed(); + void _favorite_move_up(); + void _favorite_move_down(); + void _update_favorite_list(); + void _update_fav_buttons(); + + void _recent_selected(int p_item); + void _save_to_recent(); + void _update_recent_list(); + bool _path_matches_access(const String &p_path) const; + void _invalidate(); void _setup_button(Button *p_button, const Ref &p_icon); diff --git a/scene/theme/default_theme.cpp b/scene/theme/default_theme.cpp index b82f2721e7b..641853cf6e4 100644 --- a/scene/theme/default_theme.cpp +++ b/scene/theme/default_theme.cpp @@ -690,12 +690,15 @@ void fill_default_theme(Ref &theme, const Ref &default_font, const theme->set_icon("back_folder", "FileDialog", icons["arrow_left"]); theme->set_icon("forward_folder", "FileDialog", icons["arrow_right"]); theme->set_icon("reload", "FileDialog", icons["reload"]); + theme->set_icon("favorite", "FileDialog", icons["favorite"]); theme->set_icon("toggle_hidden", "FileDialog", icons["visibility_visible"]); theme->set_icon("toggle_filename_filter", "FileDialog", icons["toggle_filename_filter"]); theme->set_icon("folder", "FileDialog", icons["folder"]); theme->set_icon("file", "FileDialog", icons["file"]); theme->set_icon("create_folder", "FileDialog", icons["folder_create"]); theme->set_icon("sort", "FileDialog", icons["sort"]); + theme->set_icon("favorite_up", "FileDialog", icons["move_up"]); + theme->set_icon("favorite_down", "FileDialog", icons["move_down"]); theme->set_color("folder_icon_color", "FileDialog", Color(1, 1, 1)); theme->set_color("file_icon_color", "FileDialog", Color(1, 1, 1)); diff --git a/scene/theme/icons/favorite.svg b/scene/theme/icons/favorite.svg new file mode 100644 index 00000000000..3595265d966 --- /dev/null +++ b/scene/theme/icons/favorite.svg @@ -0,0 +1 @@ + diff --git a/scene/theme/icons/move_down.svg b/scene/theme/icons/move_down.svg new file mode 100644 index 00000000000..34591074769 --- /dev/null +++ b/scene/theme/icons/move_down.svg @@ -0,0 +1 @@ + diff --git a/scene/theme/icons/move_up.svg b/scene/theme/icons/move_up.svg new file mode 100644 index 00000000000..e98d5832f3e --- /dev/null +++ b/scene/theme/icons/move_up.svg @@ -0,0 +1 @@ +