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);