Browse Source

Add favorites and recent directories to FileDialog

kobewi 4 months ago
parent
commit
f75c564d58

+ 9 - 0
doc/classes/FileDialog.xml

@@ -240,6 +240,15 @@
 		<theme_item name="create_folder" data_type="icon" type="Texture2D">
 			Custom icon for the create folder button.
 		</theme_item>
+		<theme_item name="favorite" data_type="icon" type="Texture2D">
+			Custom icon for favorite folder button.
+		</theme_item>
+		<theme_item name="favorite_down" data_type="icon" type="Texture2D">
+			Custom icon for button to move down a favorite entry.
+		</theme_item>
+		<theme_item name="favorite_up" data_type="icon" type="Texture2D">
+			Custom icon for button to move up a favorite entry.
+		</theme_item>
 		<theme_item name="file" data_type="icon" type="Texture2D">
 			Custom icon for files.
 		</theme_item>

+ 321 - 7
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<String> 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<String> favorited_paths;
+	Vector<String> 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<String> recent_dir_paths;
+	Vector<String> 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<Dictionary> FileDialog::_get_options() const {
 	TypedArray<Dictionary> 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:")));

+ 26 - 0
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<String> values;
@@ -142,6 +144,9 @@ private:
 	bool show_hidden_files = false;
 	bool use_native_dialog = false;
 
+	inline static LocalVector<String> global_favorites;
+	inline static LocalVector<String> 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<Texture2D> file;
 		Ref<Texture2D> create_folder;
 		Ref<Texture2D> sort;
+		Ref<Texture2D> favorite;
+		Ref<Texture2D> favorite_up;
+		Ref<Texture2D> 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<Texture2D> &p_icon);
 

+ 3 - 0
scene/theme/default_theme.cpp

@@ -690,12 +690,15 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &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));

+ 1 - 0
scene/theme/icons/favorite.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#e0e0e0" d="M8 1.7 5.626 5.797 1 6.895l3.236 3.408-.359 4.673 4.14-1.977 4.157 1.942-.396-4.653L15 6.895l-4.626-1.098z"/></svg>

+ 1 - 0
scene/theme/icons/move_down.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="none" stroke="#e0e0e0" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 2H6m2 4v8M4 9l4 5 4-5"/></svg>

+ 1 - 0
scene/theme/icons/move_up.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="none" stroke="#e0e0e0" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 14h4m-2-4V2m4 5L8 2 4 7"/></svg>