Kaynağa Gözat

Add file sort to FileDialog

kobewi 4 ay önce
ebeveyn
işleme
33dcd7a6d9

+ 3 - 0
doc/classes/FileDialog.xml

@@ -256,6 +256,9 @@
 		<theme_item name="reload" data_type="icon" type="Texture2D">
 			Custom icon for the reload button.
 		</theme_item>
+		<theme_item name="sort" data_type="icon" type="Texture2D">
+			Custom icon for the sorting options menu.
+		</theme_item>
 		<theme_item name="toggle_filename_filter" data_type="icon" type="Texture2D">
 			Custom icon for the toggle button for the filter for file names.
 		</theme_item>

+ 138 - 44
scene/gui/file_dialog.cpp

@@ -40,7 +40,9 @@
 #include "scene/gui/item_list.h"
 #include "scene/gui/label.h"
 #include "scene/gui/line_edit.h"
+#include "scene/gui/menu_button.h"
 #include "scene/gui/option_button.h"
+#include "scene/gui/separator.h"
 #include "scene/theme/theme_db.h"
 
 void FileDialog::popup_file_dialog() {
@@ -267,6 +269,7 @@ void FileDialog::_notification(int p_what) {
 			_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);
 			invalidate();
 		} break;
 
@@ -779,17 +782,14 @@ void FileDialog::update_file_list() {
 		item = dir_access->get_next();
 	}
 
-	dirs.sort_custom<FileNoCaseComparator>();
-	files.sort_custom<FileNoCaseComparator>();
-
 	String filename_filter_lower = file_name_filter.to_lower();
 
 	List<String> patterns;
-	// build filter
+	// Build filter.
 	if (filter->get_selected() == filter->get_item_count() - 1) {
-		// match all
+		// Match all.
 	} else if (filters.size() > 1 && filter->get_selected() == 0) {
-		// match all filters
+		// Match all filters.
 		for (int i = 0; i < filters.size(); i++) {
 			String f = filters[i].get_slicec(';', 0);
 			for (int j = 0; j < f.get_slice_count(","); j++) {
@@ -810,6 +810,10 @@ void FileDialog::update_file_list() {
 		}
 	}
 
+	LocalVector<DirInfo> filtered_dirs;
+	filtered_dirs.reserve(dirs.size());
+	const String base_dir = dir_access->get_current_dir();
+
 	for (const String &dir_name : dirs) {
 		bool bundle = dir_access->is_bundle(dir_name);
 		bool found = true;
@@ -825,18 +829,39 @@ void FileDialog::update_file_list() {
 		}
 
 		if (found && (filename_filter_lower.is_empty() || dir_name.to_lower().contains(filename_filter_lower))) {
-			file_list->add_item(dir_name, theme_cache.folder);
-			file_list->set_item_icon_modulate(-1, theme_cache.folder_icon_color);
-
-			Dictionary d;
-			d["name"] = dir_name;
-			d["dir"] = !bundle;
-			d["bundle"] = bundle;
-			file_list->set_item_metadata(-1, d);
+			DirInfo di;
+			di.name = dir_name;
+			di.bundle = bundle;
+			if (file_sort == FileSortOption::MODIFIED_TIME || file_sort == FileSortOption::MODIFIED_TIME_REVERSE) {
+				di.modified_time = FileAccess::get_modified_time(base_dir.path_join(dir_name));
+			}
+			filtered_dirs.push_back(di);
 		}
 	}
 
-	String base_dir = dir_access->get_current_dir();
+	if (file_sort == FileSortOption::MODIFIED_TIME || file_sort == FileSortOption::MODIFIED_TIME_REVERSE) {
+		filtered_dirs.sort_custom<DirInfo::TimeComparator>();
+	} else {
+		filtered_dirs.sort_custom<DirInfo::NameComparator>();
+	}
+
+	if (file_sort == FileSortOption::NAME_REVERSE || file_sort == FileSortOption::MODIFIED_TIME_REVERSE) {
+		filtered_dirs.reverse();
+	}
+
+	for (const DirInfo &info : filtered_dirs) {
+		file_list->add_item(info.name, theme_cache.folder);
+		file_list->set_item_icon_modulate(-1, theme_cache.folder_icon_color);
+
+		Dictionary d;
+		d["name"] = info.name;
+		d["dir"] = !info.bundle;
+		d["bundle"] = info.bundle;
+		file_list->set_item_metadata(-1, d);
+	}
+
+	LocalVector<FileInfo> filtered_files;
+	filtered_files.reserve(files.size());
 
 	for (const String &filename : files) {
 		bool match = patterns.is_empty();
@@ -851,22 +876,57 @@ void FileDialog::update_file_list() {
 		}
 
 		if (match && (filename_filter_lower.is_empty() || filename.to_lower().contains(filename_filter_lower))) {
-			const Ref<Texture2D> icon = get_icon_func ? get_icon_func(base_dir.path_join(filename)) : theme_cache.file;
-			file_list->add_item(filename, icon);
-			file_list->set_item_icon_modulate(-1, theme_cache.file_icon_color);
-
-			if (mode == FILE_MODE_OPEN_DIR) {
-				file_list->set_item_disabled(-1, true);
-			}
-			Dictionary d;
-			d["name"] = filename;
-			d["dir"] = false;
-			d["bundle"] = false;
-			file_list->set_item_metadata(-1, d);
-
-			if (filename_edit->get_text() == filename || match_str == filename) {
-				file_list->select(file_list->get_item_count() - 1);
+			FileInfo fi;
+			fi.name = filename;
+			fi.match_string = match_str;
+
+			// Only assign sorting fields when needed.
+			if (file_sort == FileSortOption::TYPE || file_sort == FileSortOption::TYPE_REVERSE) {
+				fi.type_sort = filename.get_extension() + filename.get_basename();
+			} else if (file_sort == FileSortOption::MODIFIED_TIME || file_sort == FileSortOption::MODIFIED_TIME_REVERSE) {
+				fi.modified_time = FileAccess::get_modified_time(base_dir.path_join(filename));
 			}
+			filtered_files.push_back(fi);
+		}
+	}
+
+	switch (file_sort) {
+		case FileSortOption::NAME:
+		case FileSortOption::NAME_REVERSE:
+			filtered_files.sort_custom<FileInfo::NameComparator>();
+			break;
+		case FileSortOption::TYPE:
+		case FileSortOption::TYPE_REVERSE:
+			filtered_files.sort_custom<FileInfo::TypeComparator>();
+			break;
+		case FileSortOption::MODIFIED_TIME:
+		case FileSortOption::MODIFIED_TIME_REVERSE:
+			filtered_files.sort_custom<FileInfo::TimeComparator>();
+			break;
+		default:
+			ERR_PRINT(vformat("Invalid FileDialog sort option: %d", int(file_sort)));
+	}
+
+	if (file_sort == FileSortOption::NAME_REVERSE || file_sort == FileSortOption::TYPE_REVERSE || file_sort == FileSortOption::MODIFIED_TIME_REVERSE) {
+		filtered_files.reverse();
+	}
+
+	for (const FileInfo &info : filtered_files) {
+		const Ref<Texture2D> icon = get_icon_func ? get_icon_func(base_dir.path_join(info.name)) : theme_cache.file;
+		file_list->add_item(info.name, icon);
+		file_list->set_item_icon_modulate(-1, theme_cache.file_icon_color);
+
+		if (mode == FILE_MODE_OPEN_DIR) {
+			file_list->set_item_disabled(-1, true);
+		}
+		Dictionary d;
+		d["name"] = info.name;
+		d["dir"] = false;
+		d["bundle"] = false;
+		file_list->set_item_metadata(-1, d);
+
+		if (filename_edit->get_text() == info.name || info.match_string == info.name) {
+			file_list->select(file_list->get_item_count() - 1);
 		}
 	}
 
@@ -1314,6 +1374,14 @@ void FileDialog::_update_drives(bool p_select) {
 	}
 }
 
+void FileDialog::_sort_option_selected(int p_option) {
+	for (int i = 0; i < int(FileSortOption::MAX); i++) {
+		file_sort_button->get_popup()->set_item_checked(i, (i == p_option));
+	}
+	file_sort = FileSortOption(p_option);
+	invalidate();
+}
+
 TypedArray<Dictionary> FileDialog::_get_options() const {
 	TypedArray<Dictionary> out;
 	for (const FileDialog::Option &opt : options) {
@@ -1563,6 +1631,7 @@ void FileDialog::_bind_methods() {
 	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_COLOR, FileDialog, folder_icon_color);
 	BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, FileDialog, file_icon_color);
@@ -1706,36 +1775,61 @@ FileDialog::FileDialog() {
 	top_toolbar->add_child(refresh_button);
 	refresh_button->connect(SceneStringName(pressed), callable_mp(this, &FileDialog::update_file_list));
 
+	top_toolbar->add_child(memnew(VSeparator));
+
+	make_dir_button = memnew(Button);
+	make_dir_button->set_theme_type_variation(SceneStringName(FlatButton));
+	make_dir_button->set_accessibility_name(ETR("Create New Folder"));
+	make_dir_button->set_tooltip_text(ETR("Create a new folder."));
+	top_toolbar->add_child(make_dir_button);
+	make_dir_button->connect(SceneStringName(pressed), callable_mp(this, &FileDialog::_make_dir));
+
+	HBoxContainer *lower_toolbar = memnew(HBoxContainer);
+	main_vbox->add_child(lower_toolbar);
+
+	{
+		Label *label = memnew(Label(ETR("Directories & Files:")));
+		label->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+		label->set_theme_type_variation("HeaderSmall");
+		lower_toolbar->add_child(label);
+	}
+
 	show_hidden = memnew(Button);
 	show_hidden->set_theme_type_variation(SceneStringName(FlatButton));
 	show_hidden->set_toggle_mode(true);
 	show_hidden->set_pressed(is_showing_hidden_files());
 	show_hidden->set_accessibility_name(ETR("Show Hidden Files"));
 	show_hidden->set_tooltip_text(ETR("Toggle the visibility of hidden files."));
-	top_toolbar->add_child(show_hidden);
+	lower_toolbar->add_child(show_hidden);
 	show_hidden->connect(SceneStringName(toggled), callable_mp(this, &FileDialog::set_show_hidden_files));
 
+	lower_toolbar->add_child(memnew(VSeparator));
+
 	show_filename_filter_button = memnew(Button);
 	show_filename_filter_button->set_theme_type_variation(SceneStringName(FlatButton));
 	show_filename_filter_button->set_toggle_mode(true);
 	show_filename_filter_button->set_pressed(false);
 	show_filename_filter_button->set_accessibility_name(ETR("Filter File Names"));
 	show_filename_filter_button->set_tooltip_text(ETR("Toggle the visibility of the filter for file names."));
-	top_toolbar->add_child(show_filename_filter_button);
+	lower_toolbar->add_child(show_filename_filter_button);
 	show_filename_filter_button->connect(SceneStringName(toggled), callable_mp(this, &FileDialog::set_show_filename_filter));
 
-	make_dir_button = memnew(Button);
-	make_dir_button->set_theme_type_variation(SceneStringName(FlatButton));
-	make_dir_button->set_accessibility_name(ETR("Create New Folder"));
-	make_dir_button->set_tooltip_text(ETR("Create a new folder."));
-	top_toolbar->add_child(make_dir_button);
-	make_dir_button->connect(SceneStringName(pressed), callable_mp(this, &FileDialog::_make_dir));
-
-	{
-		Label *label = memnew(Label(ETR("Directories & Files:")));
-		label->set_theme_type_variation("HeaderSmall");
-		main_vbox->add_child(label);
-	}
+	file_sort_button = memnew(MenuButton);
+	file_sort_button->set_flat(false);
+	file_sort_button->set_theme_type_variation("FlatMenuButton");
+	file_sort_button->set_tooltip_text(ETR("Sort files"));
+	file_sort_button->set_accessibility_name(ETR("Sort Files"));
+
+	PopupMenu *sort_menu = file_sort_button->get_popup();
+	sort_menu->add_radio_check_item(ETR("Sort by Name (Ascending)"), int(FileSortOption::NAME));
+	sort_menu->add_radio_check_item(ETR("Sort by Name (Descending)"), int(FileSortOption::NAME_REVERSE));
+	sort_menu->add_radio_check_item(ETR("Sort by Type (Ascending)"), int(FileSortOption::TYPE));
+	sort_menu->add_radio_check_item(ETR("Sort by Type (Descending)"), int(FileSortOption::TYPE_REVERSE));
+	sort_menu->add_radio_check_item(ETR("Sort by Modified Time (Newest First)"), int(FileSortOption::MODIFIED_TIME));
+	sort_menu->add_radio_check_item(ETR("Sort by Modified Time (Oldest First)"), int(FileSortOption::MODIFIED_TIME_REVERSE));
+	sort_menu->set_item_checked(0, true);
+	lower_toolbar->add_child(file_sort_button);
+	sort_menu->connect(SceneStringName(id_pressed), callable_mp(this, &FileDialog::_sort_option_selected));
 
 	file_list = memnew(ItemList);
 	file_list->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);

+ 59 - 1
scene/gui/file_dialog.h

@@ -39,6 +39,7 @@ class GridContainer;
 class HBoxContainer;
 class ItemList;
 class LineEdit;
+class MenuButton;
 class OptionButton;
 class PopupMenu;
 class VBoxContainer;
@@ -52,6 +53,59 @@ class FileDialog : public ConfirmationDialog {
 		int default_idx = 0;
 	};
 
+	struct DirInfo {
+		String name;
+		uint64_t modified_time = 0;
+		bool bundle = false;
+
+		struct NameComparator {
+			bool operator()(const DirInfo &p_a, const DirInfo &p_b) const {
+				return FileNoCaseComparator()(p_a.name, p_b.name);
+			}
+		};
+
+		struct TimeComparator {
+			bool operator()(const DirInfo &p_a, const DirInfo &p_b) const {
+				return p_a.modified_time > p_b.modified_time;
+			}
+		};
+	};
+
+	struct FileInfo {
+		String name;
+		String match_string;
+		String type_sort;
+		uint64_t modified_time = 0;
+
+		struct NameComparator {
+			bool operator()(const FileInfo &p_a, const FileInfo &p_b) const {
+				return FileNoCaseComparator()(p_a.name, p_b.name);
+			}
+		};
+
+		struct TypeComparator {
+			bool operator()(const FileInfo &p_a, const FileInfo &p_b) const {
+				return FileNoCaseComparator()(p_a.type_sort, p_b.type_sort);
+			}
+		};
+
+		struct TimeComparator {
+			bool operator()(const FileInfo &p_a, const FileInfo &p_b) const {
+				return p_a.modified_time > p_b.modified_time;
+			}
+		};
+	};
+
+	enum class FileSortOption {
+		NAME,
+		NAME_REVERSE,
+		TYPE,
+		TYPE_REVERSE,
+		MODIFIED_TIME,
+		MODIFIED_TIME_REVERSE,
+		MAX
+	};
+
 public:
 	enum Access {
 		ACCESS_RESOURCES,
@@ -90,6 +144,7 @@ private:
 
 	Access access = ACCESS_RESOURCES;
 	FileMode mode = FILE_MODE_SAVE_FILE;
+	FileSortOption file_sort = FileSortOption::NAME;
 	Ref<DirAccess> dir_access;
 
 	Vector<String> filters;
@@ -124,9 +179,10 @@ private:
 	HBoxContainer *shortcuts_container = nullptr;
 
 	Button *refresh_button = nullptr;
+	Button *make_dir_button = nullptr;
 	Button *show_hidden = nullptr;
 	Button *show_filename_filter_button = nullptr;
-	Button *make_dir_button = nullptr;
+	MenuButton *file_sort_button = nullptr;
 
 	ItemList *file_list = nullptr;
 	Label *message = nullptr;
@@ -158,6 +214,7 @@ private:
 		Ref<Texture2D> folder;
 		Ref<Texture2D> file;
 		Ref<Texture2D> create_folder;
+		Ref<Texture2D> sort;
 
 		Color folder_icon_color;
 		Color file_icon_color;
@@ -206,6 +263,7 @@ private:
 
 	void _change_dir(const String &p_new_dir);
 	void _update_drives(bool p_select = true);
+	void _sort_option_selected(int p_option);
 
 	void _invalidate();
 	void _setup_button(Button *p_button, const Ref<Texture2D> &p_icon);

+ 2 - 0
scene/theme/default_theme.cpp

@@ -695,6 +695,8 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &default_font, const
 	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_color("folder_icon_color", "FileDialog", Color(1, 1, 1));
 	theme->set_color("file_icon_color", "FileDialog", Color(1, 1, 1));
 	theme->set_color("file_disabled_color", "FileDialog", Color(1, 1, 1, 0.25));

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

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#e0e0e0" d="M9 1v2h6V1zM4 1a1 1 0 0 0-.691.291l-2 2a1 1 0 0 0 1.414 1.414l.293-.293v7.172l-.293-.293a1 1 0 0 0-1.414 1.414l2 2a1 1 0 0 0 1.414 0l2-2a1 1 0 0 0-1.414-1.414l-.293.293V4.412l.293.293a1 1 0 0 0 1.414-1.414l-2-2A1 1 0 0 0 4 1zm5 6v2h4V7zm0 6v2h2v-2z"/></svg>