Browse Source

Merge pull request #98812 from bruvzg/macos_bundles_as_file

[macOS] Handle bundles as files in the embedded file dialogs.
Thaddeus Crews 7 months ago
parent
commit
44cda51110

+ 2 - 0
core/io/dir_access.cpp

@@ -663,6 +663,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);

+ 1 - 0
core/io/dir_access.h

@@ -168,6 +168,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; }
 
 public:
 	DirAccess() {}

+ 8 - 0
doc/classes/DirAccess.xml

@@ -232,6 +232,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.
 			</description>
 		</method>
+		<method name="is_bundle" qualifiers="const">
+			<return type="bool" />
+			<param index="0" name="path" type="String" />
+			<description>
+				Returns [code]true[/code] if the directory is a macOS bundle.
+				[b]Note:[/b] This method is implemented on macOS.
+			</description>
+		</method>
 		<method name="is_case_sensitive" qualifiers="const">
 			<return type="bool" />
 			<param index="0" name="path" type="String" />

+ 62 - 24
editor/gui/editor_file_dialog.cpp

@@ -191,6 +191,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"));
@@ -541,7 +542,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);
@@ -793,6 +794,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);
@@ -855,7 +862,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 {
@@ -865,6 +872,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;
 	}
 }
 
@@ -1032,28 +1053,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<String> patterns;
 	// build filter
 	if (filter->get_selected() == filter->get_item_count() - 1) {
@@ -1080,6 +1079,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();
 
@@ -1115,6 +1152,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);
 

+ 3 - 1
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<Texture2D> parent_folder;
 		Ref<Texture2D> forward_folder;
 		Ref<Texture2D> back_folder;
+		Ref<Texture2D> open_folder;
 		Ref<Texture2D> reload;
 		Ref<Texture2D> toggle_hidden;
 		Ref<Texture2D> toggle_filename_filter;

+ 2 - 0
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

+ 10 - 0
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

+ 113 - 22
scene/gui/file_dialog.cpp

@@ -469,7 +469,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) {
@@ -541,7 +541,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 {
@@ -693,6 +693,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();
 
@@ -738,26 +806,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<String> patterns;
 	// build filter
 	if (filter->get_selected() == filter->get_item_count() - 1) {
@@ -784,6 +832,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()) {
@@ -817,6 +899,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()) {
@@ -1663,10 +1746,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));
@@ -1697,6 +1784,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();

+ 12 - 0
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<Texture2D> (*GetIconFunc)(const String &);
 	typedef void (*RegisterFunc)(FileDialog *);
 
@@ -89,6 +96,7 @@ private:
 	AcceptDialog *exterr = nullptr;
 	Ref<DirAccess> 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);