diff --git a/doc/classes/FileDialog.xml b/doc/classes/FileDialog.xml
index aced51b906c..159f196eaa0 100644
--- a/doc/classes/FileDialog.xml
+++ b/doc/classes/FileDialog.xml
@@ -256,6 +256,9 @@
Custom icon for the reload button.
+
+ Custom icon for the sorting options menu.
+
Custom icon for the toggle button for the filter for file names.
diff --git a/scene/gui/file_dialog.cpp b/scene/gui/file_dialog.cpp
index 276af1ca459..18ddf9ede71 100644
--- a/scene/gui/file_dialog.cpp
+++ b/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();
- files.sort_custom();
-
String filename_filter_lower = file_name_filter.to_lower();
List 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 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();
+ } else {
+ filtered_dirs.sort_custom();
+ }
+
+ 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 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 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);
+ FileInfo fi;
+ fi.name = filename;
+ fi.match_string = match_str;
- if (mode == FILE_MODE_OPEN_DIR) {
- file_list->set_item_disabled(-1, true);
+ // 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));
}
- Dictionary d;
- d["name"] = filename;
- d["dir"] = false;
- d["bundle"] = false;
- file_list->set_item_metadata(-1, d);
+ filtered_files.push_back(fi);
+ }
+ }
- if (filename_edit->get_text() == filename || match_str == filename) {
- file_list->select(file_list->get_item_count() - 1);
- }
+ switch (file_sort) {
+ case FileSortOption::NAME:
+ case FileSortOption::NAME_REVERSE:
+ filtered_files.sort_custom();
+ break;
+ case FileSortOption::TYPE:
+ case FileSortOption::TYPE_REVERSE:
+ filtered_files.sort_custom();
+ break;
+ case FileSortOption::MODIFIED_TIME:
+ case FileSortOption::MODIFIED_TIME_REVERSE:
+ filtered_files.sort_custom();
+ 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 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 FileDialog::_get_options() const {
TypedArray 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,23 +1775,7 @@ FileDialog::FileDialog() {
top_toolbar->add_child(refresh_button);
refresh_button->connect(SceneStringName(pressed), callable_mp(this, &FileDialog::update_file_list));
- 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);
- show_hidden->connect(SceneStringName(toggled), callable_mp(this, &FileDialog::set_show_hidden_files));
-
- 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);
- show_filename_filter_button->connect(SceneStringName(toggled), callable_mp(this, &FileDialog::set_show_filename_filter));
+ top_toolbar->add_child(memnew(VSeparator));
make_dir_button = memnew(Button);
make_dir_button->set_theme_type_variation(SceneStringName(FlatButton));
@@ -1731,12 +1784,53 @@ FileDialog::FileDialog() {
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");
- main_vbox->add_child(label);
+ 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."));
+ 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."));
+ lower_toolbar->add_child(show_filename_filter_button);
+ show_filename_filter_button->connect(SceneStringName(toggled), callable_mp(this, &FileDialog::set_show_filename_filter));
+
+ 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);
file_list->set_v_size_flags(Control::SIZE_EXPAND_FILL);
diff --git a/scene/gui/file_dialog.h b/scene/gui/file_dialog.h
index c9fc4cd6401..9374cb4f00e 100644
--- a/scene/gui/file_dialog.h
+++ b/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 dir_access;
Vector 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 folder;
Ref file;
Ref create_folder;
+ Ref 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 &p_icon);
diff --git a/scene/theme/default_theme.cpp b/scene/theme/default_theme.cpp
index c5fdb491281..17499750854 100644
--- a/scene/theme/default_theme.cpp
+++ b/scene/theme/default_theme.cpp
@@ -695,6 +695,8 @@ void fill_default_theme(Ref &theme, const Ref &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));
diff --git a/scene/theme/icons/sort.svg b/scene/theme/icons/sort.svg
new file mode 100644
index 00000000000..12446def277
--- /dev/null
+++ b/scene/theme/icons/sort.svg
@@ -0,0 +1 @@
+