From d5e599f77e92adf41db423763aa18b34526ea708 Mon Sep 17 00:00:00 2001
From: bruvzg <7645683+bruvzg@users.noreply.github.com>
Date: Mon, 4 Nov 2024 11:50:42 +0200
Subject: [PATCH] [macOS] Handle bundles as files in the embedded file dialogs.
---
core/io/dir_access.cpp | 2 +
core/io/dir_access.h | 1 +
doc/classes/DirAccess.xml | 8 ++
editor/gui/editor_file_dialog.cpp | 86 +++++++++++++-----
editor/gui/editor_file_dialog.h | 4 +-
platform/macos/dir_access_macos.h | 2 +
platform/macos/dir_access_macos.mm | 10 +++
scene/gui/file_dialog.cpp | 135 ++++++++++++++++++++++++-----
scene/gui/file_dialog.h | 12 +++
9 files changed, 213 insertions(+), 47 deletions(-)
diff --git a/core/io/dir_access.cpp b/core/io/dir_access.cpp
index 14588923cb3..05f49f0483d 100644
--- a/core/io/dir_access.cpp
+++ b/core/io/dir_access.cpp
@@ -588,6 +588,8 @@ void DirAccess::_bind_methods() {
ClassDB::bind_method(D_METHOD("read_link", "path"), &DirAccess::read_link);
ClassDB::bind_method(D_METHOD("create_link", "source", "target"), &DirAccess::create_link);
+ ClassDB::bind_method(D_METHOD("is_bundle", "path"), &DirAccess::is_bundle);
+
ClassDB::bind_method(D_METHOD("set_include_navigational", "enable"), &DirAccess::set_include_navigational);
ClassDB::bind_method(D_METHOD("get_include_navigational"), &DirAccess::get_include_navigational);
ClassDB::bind_method(D_METHOD("set_include_hidden", "enable"), &DirAccess::set_include_hidden);
diff --git a/core/io/dir_access.h b/core/io/dir_access.h
index 2392944f761..1faace1849b 100644
--- a/core/io/dir_access.h
+++ b/core/io/dir_access.h
@@ -160,6 +160,7 @@ public:
bool get_include_hidden() const;
virtual bool is_case_sensitive(const String &p_path) const;
+ virtual bool is_bundle(const String &p_file) const { return false; }
DirAccess() {}
virtual ~DirAccess() {}
diff --git a/doc/classes/DirAccess.xml b/doc/classes/DirAccess.xml
index 0f5844fd635..77bbe511410 100644
--- a/doc/classes/DirAccess.xml
+++ b/doc/classes/DirAccess.xml
@@ -221,6 +221,14 @@
Returns the available space on the current directory's disk, in bytes. Returns [code]0[/code] if the platform-specific method to query the available space fails.
+
+
+
+
+ Returns [code]true[/code] if the directory is a macOS bundle.
+ [b]Note:[/b] This method is implemented on macOS.
+
+
diff --git a/editor/gui/editor_file_dialog.cpp b/editor/gui/editor_file_dialog.cpp
index 0f9f0ea4322..2bac003cf36 100644
--- a/editor/gui/editor_file_dialog.cpp
+++ b/editor/gui/editor_file_dialog.cpp
@@ -185,6 +185,7 @@ void EditorFileDialog::_update_theme_item_cache() {
theme_cache.favorites_up = get_editor_theme_icon(SNAME("MoveUp"));
theme_cache.favorites_down = get_editor_theme_icon(SNAME("MoveDown"));
theme_cache.create_folder = get_editor_theme_icon(SNAME("FolderCreate"));
+ theme_cache.open_folder = get_editor_theme_icon(SNAME("FolderBrowse"));
theme_cache.filter_box = get_editor_theme_icon(SNAME("Search"));
theme_cache.file_sort_button = get_editor_theme_icon(SNAME("Sort"));
@@ -535,7 +536,7 @@ void EditorFileDialog::_action_pressed() {
String file_text = file->get_text();
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)) {
+ if ((mode == FILE_MODE_OPEN_ANY || mode == FILE_MODE_OPEN_FILE) && (dir_access->file_exists(f) || dir_access->is_bundle(f))) {
_save_to_recent();
hide();
emit_signal(SNAME("file_selected"), f);
@@ -787,6 +788,12 @@ void EditorFileDialog::_item_list_item_rmb_clicked(int p_item, const Vector2 &p_
item_menu->add_icon_item(theme_cache.filesystem, item_text, ITEM_MENU_SHOW_IN_EXPLORER);
}
#endif
+ if (single_item_selected) {
+ Dictionary item_meta = item_list->get_item_metadata(p_item);
+ if (item_meta["bundle"]) {
+ item_menu->add_icon_item(theme_cache.open_folder, TTR("Show Package Contents"), ITEM_MENU_SHOW_BUNDLE_CONTENT);
+ }
+ }
if (item_menu->get_item_count() > 0) {
item_menu->set_position(item_list->get_screen_position() + p_pos);
@@ -849,7 +856,7 @@ void EditorFileDialog::_item_menu_id_pressed(int p_option) {
case ITEM_MENU_SHOW_IN_EXPLORER: {
String path;
int idx = item_list->get_current();
- if (idx == -1 || item_list->get_selected_items().size() == 0) {
+ if (idx == -1 || !item_list->is_anything_selected()) {
// Folder background was clicked. Open this folder.
path = ProjectSettings::get_singleton()->globalize_path(dir_access->get_current_dir());
} else {
@@ -859,6 +866,20 @@ void EditorFileDialog::_item_menu_id_pressed(int p_option) {
}
OS::get_singleton()->shell_show_in_file_manager(path, true);
} break;
+
+ case ITEM_MENU_SHOW_BUNDLE_CONTENT: {
+ String path;
+ int idx = item_list->get_current();
+ if (idx == -1 || !item_list->is_anything_selected()) {
+ return;
+ }
+ Dictionary item_meta = item_list->get_item_metadata(idx);
+ dir_access->change_dir(item_meta["path"]);
+ callable_mp(this, &EditorFileDialog::update_file_list).call_deferred();
+ callable_mp(this, &EditorFileDialog::update_dir).call_deferred();
+
+ _push_history();
+ } break;
}
}
@@ -1026,28 +1047,6 @@ void EditorFileDialog::update_file_list() {
}
sort_file_info_list(file_infos, file_sort);
- while (!dirs.is_empty()) {
- const String &dir_name = dirs.front()->get();
-
- item_list->add_item(dir_name);
-
- if (display_mode == DISPLAY_THUMBNAILS) {
- item_list->set_item_icon(-1, folder_thumbnail);
- } else {
- item_list->set_item_icon(-1, theme_cache.folder);
- }
-
- Dictionary d;
- d["name"] = dir_name;
- d["path"] = cdir.path_join(dir_name);
- d["dir"] = true;
-
- item_list->set_item_metadata(-1, d);
- item_list->set_item_icon_modulate(-1, get_dir_icon_color(String(d["path"])));
-
- dirs.pop_front();
- }
-
List patterns;
// build filter
if (filter->get_selected() == filter->get_item_count() - 1) {
@@ -1074,6 +1073,44 @@ void EditorFileDialog::update_file_list() {
}
}
+ while (!dirs.is_empty()) {
+ const String &dir_name = dirs.front()->get();
+
+ bool bundle = dir_access->is_bundle(dir_name);
+ bool found = true;
+ if (bundle) {
+ bool match = patterns.is_empty();
+ for (const String &E : patterns) {
+ if (dir_name.matchn(E)) {
+ match = true;
+ break;
+ }
+ }
+ found = match;
+ }
+
+ if (found) {
+ item_list->add_item(dir_name);
+
+ if (display_mode == DISPLAY_THUMBNAILS) {
+ item_list->set_item_icon(-1, folder_thumbnail);
+ } else {
+ item_list->set_item_icon(-1, theme_cache.folder);
+ }
+
+ Dictionary d;
+ d["name"] = dir_name;
+ d["path"] = cdir.path_join(dir_name);
+ d["dir"] = !bundle;
+ d["bundle"] = bundle;
+
+ item_list->set_item_metadata(-1, d);
+ item_list->set_item_icon_modulate(-1, get_dir_icon_color(String(d["path"])));
+ }
+
+ dirs.pop_front();
+ }
+
while (!file_infos.is_empty()) {
bool match = patterns.is_empty();
@@ -1109,6 +1146,7 @@ void EditorFileDialog::update_file_list() {
Dictionary d;
d["name"] = file_info.name;
d["dir"] = false;
+ d["bundle"] = false;
d["path"] = file_info.path;
item_list->set_item_metadata(-1, d);
diff --git a/editor/gui/editor_file_dialog.h b/editor/gui/editor_file_dialog.h
index 9136a54d576..a151f2b38f7 100644
--- a/editor/gui/editor_file_dialog.h
+++ b/editor/gui/editor_file_dialog.h
@@ -82,7 +82,8 @@ private:
ITEM_MENU_DELETE,
ITEM_MENU_REFRESH,
ITEM_MENU_NEW_FOLDER,
- ITEM_MENU_SHOW_IN_EXPLORER
+ ITEM_MENU_SHOW_IN_EXPLORER,
+ ITEM_MENU_SHOW_BUNDLE_CONTENT,
};
ConfirmationDialog *makedialog = nullptr;
@@ -167,6 +168,7 @@ private:
Ref parent_folder;
Ref forward_folder;
Ref back_folder;
+ Ref open_folder;
Ref reload;
Ref toggle_hidden;
Ref toggle_filename_filter;
diff --git a/platform/macos/dir_access_macos.h b/platform/macos/dir_access_macos.h
index 167c162200d..17a00b7f480 100644
--- a/platform/macos/dir_access_macos.h
+++ b/platform/macos/dir_access_macos.h
@@ -50,6 +50,8 @@ protected:
virtual bool is_hidden(const String &p_name) override;
virtual bool is_case_sensitive(const String &p_path) const override;
+
+ virtual bool is_bundle(const String &p_file) const override;
};
#endif // UNIX ENABLED
diff --git a/platform/macos/dir_access_macos.mm b/platform/macos/dir_access_macos.mm
index 37f717c9de0..8d03dc1c91f 100644
--- a/platform/macos/dir_access_macos.mm
+++ b/platform/macos/dir_access_macos.mm
@@ -96,4 +96,14 @@ bool DirAccessMacOS::is_case_sensitive(const String &p_path) const {
return [cs boolValue];
}
+bool DirAccessMacOS::is_bundle(const String &p_file) const {
+ String f = p_file;
+ if (!f.is_absolute_path()) {
+ f = get_current_dir().path_join(f);
+ }
+ f = fix_path(f);
+
+ return [[NSWorkspace sharedWorkspace] isFilePackageAtPath:[NSString stringWithUTF8String:f.utf8().get_data()]];
+}
+
#endif // UNIX_ENABLED
diff --git a/scene/gui/file_dialog.cpp b/scene/gui/file_dialog.cpp
index d21ede28e06..c58637e0667 100644
--- a/scene/gui/file_dialog.cpp
+++ b/scene/gui/file_dialog.cpp
@@ -463,7 +463,7 @@ void FileDialog::_action_pressed() {
String file_text = file->get_text();
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)) {
+ if ((mode == FILE_MODE_OPEN_ANY || mode == FILE_MODE_OPEN_FILE) && (dir_access->file_exists(f) || dir_access->is_bundle(f))) {
emit_signal(SNAME("file_selected"), f);
hide();
} else if (mode == FILE_MODE_OPEN_ANY || mode == FILE_MODE_OPEN_DIR) {
@@ -535,7 +535,7 @@ void FileDialog::_action_pressed() {
return;
}
- if (dir_access->file_exists(f)) {
+ if (dir_access->file_exists(f) || dir_access->is_bundle(f)) {
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 {
@@ -687,6 +687,74 @@ void FileDialog::update_file_name() {
}
}
+void FileDialog::_item_menu_id_pressed(int p_option) {
+ switch (p_option) {
+ case ITEM_MENU_SHOW_IN_EXPLORER: {
+ TreeItem *ti = tree->get_selected();
+ String path;
+ if (ti) {
+ Dictionary d = ti->get_metadata(0);
+ path = ProjectSettings::get_singleton()->globalize_path(dir_access->get_current_dir().path_join(d["name"]));
+ } else {
+ path = ProjectSettings::get_singleton()->globalize_path(dir_access->get_current_dir());
+ }
+
+ OS::get_singleton()->shell_show_in_file_manager(path, true);
+ } break;
+
+ case ITEM_MENU_SHOW_BUNDLE_CONTENT: {
+ TreeItem *ti = tree->get_selected();
+ if (!ti) {
+ return;
+ }
+ Dictionary d = ti->get_metadata(0);
+ _change_dir(d["name"]);
+ if (mode == FILE_MODE_OPEN_FILE || mode == FILE_MODE_OPEN_FILES || mode == FILE_MODE_OPEN_DIR || mode == FILE_MODE_OPEN_ANY) {
+ file->set_text("");
+ }
+ _push_history();
+ } break;
+ }
+}
+
+void FileDialog::_empty_clicked(const Vector2 &p_pos, MouseButton p_button) {
+ if (p_button == MouseButton::RIGHT) {
+ item_menu->clear();
+#if !defined(ANDROID_ENABLED) && !defined(WEB_ENABLED)
+ // Opening the system file manager is not supported on the Android and web editors.
+ item_menu->add_item(ETR("Open in File Manager"), ITEM_MENU_SHOW_IN_EXPLORER);
+
+ item_menu->set_position(tree->get_screen_position() + p_pos);
+ item_menu->reset_size();
+ item_menu->popup();
+#endif
+ }
+}
+
+void FileDialog::_rmb_select(const Vector2 &p_pos, MouseButton p_button) {
+ if (p_button == MouseButton::RIGHT) {
+ item_menu->clear();
+#if !defined(ANDROID_ENABLED) && !defined(WEB_ENABLED)
+ // Opening the system file manager is not supported on the Android and web editors.
+ TreeItem *ti = tree->get_selected();
+ if (!ti) {
+ return;
+ }
+ Dictionary d = ti->get_metadata(0);
+ if (d["bundle"]) {
+ item_menu->add_item(ETR("Show Package Contents"), ITEM_MENU_SHOW_BUNDLE_CONTENT);
+ }
+ item_menu->add_item(ETR("Open in File Manager"), ITEM_MENU_SHOW_IN_EXPLORER);
+
+ item_menu->set_position(tree->get_screen_position() + p_pos);
+ item_menu->reset_size();
+ item_menu->popup();
+#endif
+ } else {
+ _tree_selected();
+ }
+}
+
void FileDialog::update_file_list() {
tree->clear();
@@ -732,26 +800,6 @@ void FileDialog::update_file_list() {
String filename_filter_lower = file_name_filter.to_lower();
- while (!dirs.is_empty()) {
- const String &dir_name = dirs.front()->get();
-
- if (filename_filter_lower.is_empty() || dir_name.to_lower().contains(filename_filter_lower)) {
- TreeItem *ti = tree->create_item(root);
-
- ti->set_text(0, dir_name);
- ti->set_icon(0, theme_cache.folder);
- ti->set_icon_modulate(0, theme_cache.folder_icon_color);
-
- Dictionary d;
- d["name"] = dir_name;
- d["dir"] = true;
-
- ti->set_metadata(0, d);
- }
-
- dirs.pop_front();
- }
-
List patterns;
// build filter
if (filter->get_selected() == filter->get_item_count() - 1) {
@@ -778,6 +826,40 @@ void FileDialog::update_file_list() {
}
}
+ while (!dirs.is_empty()) {
+ const String &dir_name = dirs.front()->get();
+
+ bool bundle = dir_access->is_bundle(dir_name);
+ bool found = true;
+ if (bundle) {
+ bool match = patterns.is_empty();
+ for (const String &E : patterns) {
+ if (dir_name.matchn(E)) {
+ match = true;
+ break;
+ }
+ }
+ found = match;
+ }
+
+ if (found && (filename_filter_lower.is_empty() || dir_name.to_lower().contains(filename_filter_lower))) {
+ TreeItem *ti = tree->create_item(root);
+
+ ti->set_text(0, dir_name);
+ ti->set_icon(0, theme_cache.folder);
+ ti->set_icon_modulate(0, theme_cache.folder_icon_color);
+
+ Dictionary d;
+ d["name"] = dir_name;
+ d["dir"] = !bundle;
+ d["bundle"] = bundle;
+
+ ti->set_metadata(0, d);
+ }
+
+ dirs.pop_front();
+ }
+
String base_dir = dir_access->get_current_dir();
while (!files.is_empty()) {
@@ -811,6 +893,7 @@ void FileDialog::update_file_list() {
Dictionary d;
d["name"] = files.front()->get();
d["dir"] = false;
+ d["bundle"] = false;
ti->set_metadata(0, d);
if (file->get_text() == files.front()->get() || match_str == files.front()->get()) {
@@ -1653,10 +1736,14 @@ FileDialog::FileDialog() {
_update_drives();
connect(SceneStringName(confirmed), callable_mp(this, &FileDialog::_action_pressed));
+ tree->set_allow_rmb_select(true);
tree->connect("multi_selected", callable_mp(this, &FileDialog::_tree_multi_selected), CONNECT_DEFERRED);
tree->connect("cell_selected", callable_mp(this, &FileDialog::_tree_selected), CONNECT_DEFERRED);
tree->connect("item_activated", callable_mp(this, &FileDialog::_tree_item_activated));
tree->connect("nothing_selected", callable_mp(this, &FileDialog::deselect_all));
+ tree->connect("item_mouse_selected", callable_mp(this, &FileDialog::_rmb_select));
+ tree->connect("empty_clicked", callable_mp(this, &FileDialog::_empty_clicked));
+
dir->connect(SceneStringName(text_submitted), callable_mp(this, &FileDialog::_dir_submitted));
filename_filter->connect(SceneStringName(text_changed), callable_mp(this, &FileDialog::_filename_filter_changed).unbind(1));
filename_filter->connect(SceneStringName(text_submitted), callable_mp(this, &FileDialog::_filename_filter_selected).unbind(1));
@@ -1687,6 +1774,10 @@ FileDialog::FileDialog() {
exterr->set_text(ETR("Invalid extension, or empty filename."));
add_child(exterr, false, INTERNAL_MODE_FRONT);
+ item_menu = memnew(PopupMenu);
+ item_menu->connect(SceneStringName(id_pressed), callable_mp(this, &FileDialog::_item_menu_id_pressed));
+ add_child(item_menu);
+
update_filters();
update_filename_filter_gui();
update_dir();
diff --git a/scene/gui/file_dialog.h b/scene/gui/file_dialog.h
index cb28a52ba68..af8e8950ae9 100644
--- a/scene/gui/file_dialog.h
+++ b/scene/gui/file_dialog.h
@@ -40,6 +40,7 @@
#include "scene/property_list_helper.h"
class GridContainer;
+class PopupMenu;
class FileDialog : public ConfirmationDialog {
GDCLASS(FileDialog, ConfirmationDialog);
@@ -59,6 +60,12 @@ public:
FILE_MODE_SAVE_FILE
};
+ enum ItemMenu {
+ ITEM_MENU_COPY_PATH,
+ ITEM_MENU_SHOW_IN_EXPLORER,
+ ITEM_MENU_SHOW_BUNDLE_CONTENT,
+ };
+
typedef Ref (*GetIconFunc)(const String &);
typedef void (*RegisterFunc)(FileDialog *);
@@ -89,6 +96,7 @@ private:
AcceptDialog *exterr = nullptr;
Ref dir_access;
ConfirmationDialog *confirm_save = nullptr;
+ PopupMenu *item_menu = nullptr;
Label *message = nullptr;
@@ -161,6 +169,10 @@ private:
void update_filename_filter_gui();
void update_filters();
+ void _item_menu_id_pressed(int p_option);
+ void _empty_clicked(const Vector2 &p_pos, MouseButton p_button);
+ void _rmb_select(const Vector2 &p_pos, MouseButton p_button = MouseButton::RIGHT);
+
void _focus_file_text();
void _tree_multi_selected(Object *p_object, int p_cell, bool p_selected);