Răsfoiți Sursa

Support MIME types in file dialog filters on macOS and Linux.

Pāvels Nadtočajevs 11 luni în urmă
părinte
comite
e1f129cb52

+ 7 - 3
doc/classes/DisplayServer.xml

@@ -145,11 +145,11 @@
 			<param index="6" name="callback" type="Callable" />
 			<param index="6" name="callback" type="Callable" />
 			<description>
 			<description>
 				Displays OS native dialog for selecting files or directories in the file system.
 				Displays OS native dialog for selecting files or directories in the file system.
-				Each filter string in the [param filters] array should be formatted like this: [code]*.txt,*.doc;Text Files[/code]. The description text of the filter is optional and can be omitted. See also [member FileDialog.filters].
+				Each filter string in the [param filters] array should be formatted like this: [code]*.png,*.jpg,*.jpeg;Image Files;image/png,image/jpeg[/code]. The description text of the filter is optional and can be omitted. It is recommended to set both file extension and MIME type. See also [member FileDialog.filters].
 				Callbacks have the following arguments: [code]status: bool, selected_paths: PackedStringArray, selected_filter_index: int[/code]. [b]On Android,[/b] callback argument [code]selected_filter_index[/code] is always zero.
 				Callbacks have the following arguments: [code]status: bool, selected_paths: PackedStringArray, selected_filter_index: int[/code]. [b]On Android,[/b] callback argument [code]selected_filter_index[/code] is always zero.
 				[b]Note:[/b] This method is implemented if the display server has the [constant FEATURE_NATIVE_DIALOG_FILE] feature. Supported platforms include Linux (X11/Wayland), Windows, macOS, and Android.
 				[b]Note:[/b] This method is implemented if the display server has the [constant FEATURE_NATIVE_DIALOG_FILE] feature. Supported platforms include Linux (X11/Wayland), Windows, macOS, and Android.
 				[b]Note:[/b] [param current_directory] might be ignored.
 				[b]Note:[/b] [param current_directory] might be ignored.
-				[b]Note:[/b] On Android, the filter strings in the [param filters] array should be specified using MIME types, for example:[code]image/png, image/jpeg"[/code]. Additionally, the [param mode] [constant FILE_DIALOG_MODE_OPEN_ANY] is not supported on Android.
+				[b]Note:[/b] Embedded file dialog and Windows file dialog support only file extensions, while Android, Linux, and macOS file dialogs also support MIME types.
 				[b]Note:[/b] On Android and Linux, [param show_hidden] is ignored.
 				[b]Note:[/b] On Android and Linux, [param show_hidden] is ignored.
 				[b]Note:[/b] On Android and macOS, native file dialogs have no title.
 				[b]Note:[/b] On Android and macOS, native file dialogs have no title.
 				[b]Note:[/b] On macOS, sandboxed apps will save security-scoped bookmarks to retain access to the opened folders across multiple sessions. Use [method OS.get_granted_permissions] to get a list of saved bookmarks.
 				[b]Note:[/b] On macOS, sandboxed apps will save security-scoped bookmarks to retain access to the opened folders across multiple sessions. Use [method OS.get_granted_permissions] to get a list of saved bookmarks.
@@ -168,7 +168,7 @@
 			<param index="8" name="callback" type="Callable" />
 			<param index="8" name="callback" type="Callable" />
 			<description>
 			<description>
 				Displays OS native dialog for selecting files or directories in the file system with additional user selectable options.
 				Displays OS native dialog for selecting files or directories in the file system with additional user selectable options.
-				Each filter string in the [param filters] array should be formatted like this: [code]*.txt,*.doc;Text Files[/code]. The description text of the filter is optional and can be omitted. See also [member FileDialog.filters].
+				Each filter string in the [param filters] array should be formatted like this: [code]*.png,*.jpg,*.jpeg;Image Files;image/png,image/jpeg[/code]. The description text of the filter is optional and can be omitted. It is recommended to set both file extension and MIME type. See also [member FileDialog.filters].
 				[param options] is array of [Dictionary]s with the following keys:
 				[param options] is array of [Dictionary]s with the following keys:
 				- [code]"name"[/code] - option's name [String].
 				- [code]"name"[/code] - option's name [String].
 				- [code]"values"[/code] - [PackedStringArray] of values. If empty, boolean option (check box) is used.
 				- [code]"values"[/code] - [PackedStringArray] of values. If empty, boolean option (check box) is used.
@@ -176,6 +176,7 @@
 				Callbacks have the following arguments: [code]status: bool, selected_paths: PackedStringArray, selected_filter_index: int, selected_option: Dictionary[/code].
 				Callbacks have the following arguments: [code]status: bool, selected_paths: PackedStringArray, selected_filter_index: int, selected_option: Dictionary[/code].
 				[b]Note:[/b] This method is implemented if the display server has the [constant FEATURE_NATIVE_DIALOG_FILE_EXTRA] feature. Supported platforms include Linux (X11/Wayland), Windows, and macOS.
 				[b]Note:[/b] This method is implemented if the display server has the [constant FEATURE_NATIVE_DIALOG_FILE_EXTRA] feature. Supported platforms include Linux (X11/Wayland), Windows, and macOS.
 				[b]Note:[/b] [param current_directory] might be ignored.
 				[b]Note:[/b] [param current_directory] might be ignored.
+				[b]Note:[/b] Embedded file dialog and Windows file dialog support only file extensions, while Android, Linux, and macOS file dialogs also support MIME types.
 				[b]Note:[/b] On Linux (X11), [param show_hidden] is ignored.
 				[b]Note:[/b] On Linux (X11), [param show_hidden] is ignored.
 				[b]Note:[/b] On macOS, native file dialogs have no title.
 				[b]Note:[/b] On macOS, native file dialogs have no title.
 				[b]Note:[/b] On macOS, sandboxed apps will save security-scoped bookmarks to retain access to the opened folders across multiple sessions. Use [method OS.get_granted_permissions] to get a list of saved bookmarks.
 				[b]Note:[/b] On macOS, sandboxed apps will save security-scoped bookmarks to retain access to the opened folders across multiple sessions. Use [method OS.get_granted_permissions] to get a list of saved bookmarks.
@@ -1928,6 +1929,9 @@
 		<constant name="FEATURE_WINDOW_EMBEDDING" value="29" enum="Feature">
 		<constant name="FEATURE_WINDOW_EMBEDDING" value="29" enum="Feature">
 			Display server supports embedding a window from another process. [b]Windows, Linux (X11)[/b]
 			Display server supports embedding a window from another process. [b]Windows, Linux (X11)[/b]
 		</constant>
 		</constant>
+		<constant name="FEATURE_NATIVE_DIALOG_FILE_MIME" value="30" enum="Feature">
+			Native file selection dialog supports MIME types as filters.
+		</constant>
 		<constant name="MOUSE_MODE_VISIBLE" value="0" enum="MouseMode">
 		<constant name="MOUSE_MODE_VISIBLE" value="0" enum="MouseMode">
 			Makes the mouse cursor visible if it is hidden.
 			Makes the mouse cursor visible if it is hidden.
 		</constant>
 		</constant>

+ 2 - 2
doc/classes/FileDialog.xml

@@ -145,8 +145,8 @@
 			See also [member filters], which should be used to restrict the file types that can be selected instead of [member filename_filter] which is meant to be set by the user.
 			See also [member filters], which should be used to restrict the file types that can be selected instead of [member filename_filter] which is meant to be set by the user.
 		</member>
 		</member>
 		<member name="filters" type="PackedStringArray" setter="set_filters" getter="get_filters" default="PackedStringArray()">
 		<member name="filters" type="PackedStringArray" setter="set_filters" getter="get_filters" default="PackedStringArray()">
-			The available file type filters. Each filter string in the array should be formatted like this: [code]*.txt,*.doc;Text Files[/code]. The description text of the filter is optional and can be omitted.
-			[b]Note:[/b] For android native dialog, MIME types are used like this: [code]image/*, application/pdf[/code].
+			The available file type filters. Each filter string in the array should be formatted like this: [code]*.png,*.jpg,*.jpeg;Image Files;image/png,image/jpeg[/code]. The description text of the filter is optional and can be omitted. Both file extensions and MIME type should be always set.
+			[b]Note:[/b] Embedded file dialog and Windows file dialog support only file extensions, while Android, Linux, and macOS file dialogs also support MIME types.
 		</member>
 		</member>
 		<member name="mode_overrides_title" type="bool" setter="set_mode_overrides_title" getter="is_mode_overriding_title" default="true">
 		<member name="mode_overrides_title" type="bool" setter="set_mode_overrides_title" getter="is_mode_overriding_title" default="true">
 			If [code]true[/code], changing the [member file_mode] property will set the window title accordingly (e.g. setting [member file_mode] to [constant FILE_MODE_OPEN_FILE] will change the window title to "Open a File").
 			If [code]true[/code], changing the [member file_mode] property will set the window title accordingly (e.g. setting [member file_mode] to [constant FILE_MODE_OPEN_FILE] will change the window title to "Open a File").

+ 47 - 14
editor/gui/editor_file_dialog.cpp

@@ -1218,50 +1218,83 @@ void EditorFileDialog::update_filters() {
 
 
 	if (filters.size() > 1) {
 	if (filters.size() > 1) {
 		String all_filters;
 		String all_filters;
+		String all_mime;
 		String all_filters_full;
 		String all_filters_full;
+		String all_mime_full;
 
 
 		const int max_filters = 5;
 		const int max_filters = 5;
 
 
+		// "All Recognized" display name.
 		for (int i = 0; i < MIN(max_filters, filters.size()); i++) {
 		for (int i = 0; i < MIN(max_filters, filters.size()); i++) {
 			String flt = filters[i].get_slicec(';', 0).strip_edges();
 			String flt = filters[i].get_slicec(';', 0).strip_edges();
-			if (i > 0) {
+			if (!all_filters.is_empty() && !flt.is_empty()) {
 				all_filters += ", ";
 				all_filters += ", ";
 			}
 			}
 			all_filters += flt;
 			all_filters += flt;
+
+			String mime = filters[i].get_slicec(';', 2).strip_edges();
+			if (!all_mime.is_empty() && !mime.is_empty()) {
+				all_mime += ", ";
+			}
+			all_mime += mime;
 		}
 		}
+
+		// "All Recognized" filter.
 		for (int i = 0; i < filters.size(); i++) {
 		for (int i = 0; i < filters.size(); i++) {
 			String flt = filters[i].get_slicec(';', 0).strip_edges();
 			String flt = filters[i].get_slicec(';', 0).strip_edges();
-			if (i > 0) {
+			if (!all_filters_full.is_empty() && !flt.is_empty()) {
 				all_filters_full += ",";
 				all_filters_full += ",";
 			}
 			}
 			all_filters_full += flt;
 			all_filters_full += flt;
+
+			String mime = filters[i].get_slicec(';', 2).strip_edges();
+			if (!all_mime_full.is_empty() && !mime.is_empty()) {
+				all_mime_full += ",";
+			}
+			all_mime_full += mime;
+		}
+
+		String native_all_name;
+		if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_NATIVE_DIALOG_FILE_MIME)) {
+			native_all_name += all_filters;
+		}
+		if (!native_all_name.is_empty()) {
+			native_all_name += ", ";
 		}
 		}
+		native_all_name += all_mime;
 
 
 		if (max_filters < filters.size()) {
 		if (max_filters < filters.size()) {
 			all_filters += ", ...";
 			all_filters += ", ...";
+			native_all_name += ", ...";
 		}
 		}
 
 
-		String f = TTR("All Recognized") + " (" + all_filters + ")";
-		filter->add_item(f);
-		processed_filters.push_back(all_filters_full + ";" + f);
+		filter->add_item(atr(ETR("All Recognized")) + " (" + all_filters + ")");
+		processed_filters.push_back(all_filters_full + ";" + atr(ETR("All Recognized")) + " (" + native_all_name + ")" + ";" + all_mime_full);
 	}
 	}
 	for (int i = 0; i < filters.size(); i++) {
 	for (int i = 0; i < filters.size(); i++) {
 		String flt = filters[i].get_slicec(';', 0).strip_edges();
 		String flt = filters[i].get_slicec(';', 0).strip_edges();
-		String desc = filters[i].get_slice(";", 1).strip_edges();
-		if (desc.length()) {
-			String f = desc + " (" + flt + ")";
-			filter->add_item(f);
-			processed_filters.push_back(flt + ";" + f);
+		String desc = filters[i].get_slicec(';', 1).strip_edges();
+		String mime = filters[i].get_slicec(';', 2).strip_edges();
+		String native_name;
+		if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_NATIVE_DIALOG_FILE_MIME)) {
+			native_name += flt;
+		}
+		if (!native_name.is_empty() && !mime.is_empty()) {
+			native_name += ", ";
+		}
+		native_name += mime;
+		if (!desc.is_empty()) {
+			filter->add_item(atr(desc) + " (" + flt + ")");
+			processed_filters.push_back(flt + ";" + atr(desc) + " (" + native_name + ");" + mime);
 		} else {
 		} else {
-			String f = "(" + flt + ")";
-			filter->add_item(f);
-			processed_filters.push_back(flt + ";" + f);
+			filter->add_item("(" + flt + ")");
+			processed_filters.push_back(flt + ";(" + native_name + ");" + mime);
 		}
 		}
 	}
 	}
 
 
 	String f = TTR("All Files") + " (*.*)";
 	String f = TTR("All Files") + " (*.*)";
 	filter->add_item(f);
 	filter->add_item(f);
-	processed_filters.push_back("*.*;" + f);
+	processed_filters.push_back("*.*;" + f + ";application/octet-stream");
 }
 }
 
 
 void EditorFileDialog::clear_filters() {
 void EditorFileDialog::clear_filters() {

+ 1 - 0
platform/android/display_server_android.cpp

@@ -74,6 +74,7 @@ bool DisplayServerAndroid::has_feature(Feature p_feature) const {
 		case FEATURE_NATIVE_DIALOG_INPUT:
 		case FEATURE_NATIVE_DIALOG_INPUT:
 		case FEATURE_NATIVE_DIALOG_FILE:
 		case FEATURE_NATIVE_DIALOG_FILE:
 		//case FEATURE_NATIVE_DIALOG_FILE_EXTRA:
 		//case FEATURE_NATIVE_DIALOG_FILE_EXTRA:
+		case FEATURE_NATIVE_DIALOG_FILE_MIME:
 		//case FEATURE_NATIVE_ICON:
 		//case FEATURE_NATIVE_ICON:
 		//case FEATURE_WINDOW_TRANSPARENCY:
 		//case FEATURE_WINDOW_TRANSPARENCY:
 		case FEATURE_CLIPBOARD:
 		case FEATURE_CLIPBOARD:

+ 8 - 3
platform/android/java_godot_wrapper.cpp

@@ -327,9 +327,14 @@ Error GodotJavaWrapper::show_file_picker(const String &p_current_directory, cons
 		jstring j_current_directory = env->NewStringUTF(p_current_directory.utf8().get_data());
 		jstring j_current_directory = env->NewStringUTF(p_current_directory.utf8().get_data());
 		jstring j_filename = env->NewStringUTF(p_filename.utf8().get_data());
 		jstring j_filename = env->NewStringUTF(p_filename.utf8().get_data());
 		jint j_mode = p_mode;
 		jint j_mode = p_mode;
-		jobjectArray j_filters = env->NewObjectArray(p_filters.size(), env->FindClass("java/lang/String"), nullptr);
-		for (int i = 0; i < p_filters.size(); ++i) {
-			jstring j_filter = env->NewStringUTF(p_filters[i].get_slice(";", 0).utf8().get_data());
+		Vector<String> filters;
+		for (const String &E : p_filters) {
+			filters.append_array(E.get_slicec(';', 0).split(",")); // Add extensions.
+			filters.append_array(E.get_slicec(';', 2).split(",")); // Add MIME types.
+		}
+		jobjectArray j_filters = env->NewObjectArray(filters.size(), env->FindClass("java/lang/String"), nullptr);
+		for (int i = 0; i < filters.size(); ++i) {
+			jstring j_filter = env->NewStringUTF(filters[i].utf8().get_data());
 			env->SetObjectArrayElement(j_filters, i, j_filter);
 			env->SetObjectArrayElement(j_filters, i, j_filter);
 			env->DeleteLocalRef(j_filter);
 			env->DeleteLocalRef(j_filter);
 		}
 		}

+ 1 - 0
platform/ios/display_server_ios.mm

@@ -371,6 +371,7 @@ bool DisplayServerIOS::has_feature(Feature p_feature) const {
 		// case FEATURE_NATIVE_DIALOG_INPUT:
 		// case FEATURE_NATIVE_DIALOG_INPUT:
 		// case FEATURE_NATIVE_DIALOG_FILE:
 		// case FEATURE_NATIVE_DIALOG_FILE:
 		// case FEATURE_NATIVE_DIALOG_FILE_EXTRA:
 		// case FEATURE_NATIVE_DIALOG_FILE_EXTRA:
+		// case FEATURE_NATIVE_DIALOG_FILE_MIME:
 		// case FEATURE_NATIVE_ICON:
 		// case FEATURE_NATIVE_ICON:
 		// case FEATURE_WINDOW_TRANSPARENCY:
 		// case FEATURE_WINDOW_TRANSPARENCY:
 		case FEATURE_CLIPBOARD:
 		case FEATURE_CLIPBOARD:

+ 24 - 6
platform/linuxbsd/freedesktop_portal_desktop.cpp

@@ -192,13 +192,14 @@ void FreeDesktopPortalDesktop::append_dbus_dict_options(DBusMessageIter *p_iter,
 	dbus_message_iter_close_container(p_iter, &dict_iter);
 	dbus_message_iter_close_container(p_iter, &dict_iter);
 }
 }
 
 
-void FreeDesktopPortalDesktop::append_dbus_dict_filters(DBusMessageIter *p_iter, const Vector<String> &p_filter_names, const Vector<String> &p_filter_exts) {
+void FreeDesktopPortalDesktop::append_dbus_dict_filters(DBusMessageIter *p_iter, const Vector<String> &p_filter_names, const Vector<String> &p_filter_exts, const Vector<String> &p_filter_mimes) {
 	DBusMessageIter dict_iter;
 	DBusMessageIter dict_iter;
 	DBusMessageIter var_iter;
 	DBusMessageIter var_iter;
 	DBusMessageIter arr_iter;
 	DBusMessageIter arr_iter;
 	const char *filters_key = "filters";
 	const char *filters_key = "filters";
 
 
 	ERR_FAIL_COND(p_filter_names.size() != p_filter_exts.size());
 	ERR_FAIL_COND(p_filter_names.size() != p_filter_exts.size());
+	ERR_FAIL_COND(p_filter_names.size() != p_filter_mimes.size());
 
 
 	dbus_message_iter_open_container(p_iter, DBUS_TYPE_DICT_ENTRY, nullptr, &dict_iter);
 	dbus_message_iter_open_container(p_iter, DBUS_TYPE_DICT_ENTRY, nullptr, &dict_iter);
 	dbus_message_iter_append_basic(&dict_iter, DBUS_TYPE_STRING, &filters_key);
 	dbus_message_iter_append_basic(&dict_iter, DBUS_TYPE_STRING, &filters_key);
@@ -226,8 +227,20 @@ void FreeDesktopPortalDesktop::append_dbus_dict_filters(DBusMessageIter *p_iter,
 			dbus_message_iter_open_container(&array_iter, DBUS_TYPE_STRUCT, nullptr, &array_struct_iter);
 			dbus_message_iter_open_container(&array_iter, DBUS_TYPE_STRUCT, nullptr, &array_struct_iter);
 			String str = (flt.get_slice(",", j).strip_edges());
 			String str = (flt.get_slice(",", j).strip_edges());
 			{
 			{
-				const unsigned nil = 0;
-				dbus_message_iter_append_basic(&array_struct_iter, DBUS_TYPE_UINT32, &nil);
+				const unsigned flt_type = 0;
+				dbus_message_iter_append_basic(&array_struct_iter, DBUS_TYPE_UINT32, &flt_type);
+			}
+			append_dbus_string(&array_struct_iter, str);
+			dbus_message_iter_close_container(&array_iter, &array_struct_iter);
+		}
+		const String &mime = p_filter_mimes[i];
+		filter_slice_count = mime.get_slice_count(",");
+		for (int j = 0; j < filter_slice_count; j++) {
+			dbus_message_iter_open_container(&array_iter, DBUS_TYPE_STRUCT, nullptr, &array_struct_iter);
+			String str = mime.get_slicec(',', j).strip_edges();
+			{
+				const unsigned flt_type = 1;
+				dbus_message_iter_append_basic(&array_struct_iter, DBUS_TYPE_UINT32, &flt_type);
 			}
 			}
 			append_dbus_string(&array_struct_iter, str);
 			append_dbus_string(&array_struct_iter, str);
 			dbus_message_iter_close_container(&array_iter, &array_struct_iter);
 			dbus_message_iter_close_container(&array_iter, &array_struct_iter);
@@ -384,17 +397,20 @@ Error FreeDesktopPortalDesktop::file_dialog_show(DisplayServer::WindowID p_windo
 
 
 	Vector<String> filter_names;
 	Vector<String> filter_names;
 	Vector<String> filter_exts;
 	Vector<String> filter_exts;
+	Vector<String> filter_mimes;
 	for (int i = 0; i < p_filters.size(); i++) {
 	for (int i = 0; i < p_filters.size(); i++) {
 		Vector<String> tokens = p_filters[i].split(";");
 		Vector<String> tokens = p_filters[i].split(";");
 		if (tokens.size() >= 1) {
 		if (tokens.size() >= 1) {
 			String flt = tokens[0].strip_edges();
 			String flt = tokens[0].strip_edges();
-			if (!flt.is_empty()) {
-				if (tokens.size() == 2) {
+			String mime = (tokens.size() >= 2) ? tokens[2].strip_edges() : String();
+			if (!flt.is_empty() || !mime.is_empty()) {
+				if (tokens.size() >= 2) {
 					if (flt == "*.*") {
 					if (flt == "*.*") {
 						filter_exts.push_back("*");
 						filter_exts.push_back("*");
 					} else {
 					} else {
 						filter_exts.push_back(flt);
 						filter_exts.push_back(flt);
 					}
 					}
+					filter_mimes.push_back(mime);
 					filter_names.push_back(tokens[1]);
 					filter_names.push_back(tokens[1]);
 				} else {
 				} else {
 					if (flt == "*.*") {
 					if (flt == "*.*") {
@@ -404,12 +420,14 @@ Error FreeDesktopPortalDesktop::file_dialog_show(DisplayServer::WindowID p_windo
 						filter_exts.push_back(flt);
 						filter_exts.push_back(flt);
 						filter_names.push_back(flt);
 						filter_names.push_back(flt);
 					}
 					}
+					filter_mimes.push_back(mime);
 				}
 				}
 			}
 			}
 		}
 		}
 	}
 	}
 	if (filter_names.is_empty()) {
 	if (filter_names.is_empty()) {
 		filter_exts.push_back("*");
 		filter_exts.push_back("*");
+		filter_mimes.push_back("");
 		filter_names.push_back(RTR("All Files") + " (*.*)");
 		filter_names.push_back(RTR("All Files") + " (*.*)");
 	}
 	}
 
 
@@ -464,7 +482,7 @@ Error FreeDesktopPortalDesktop::file_dialog_show(DisplayServer::WindowID p_windo
 		append_dbus_dict_string(&arr_iter, "handle_token", token);
 		append_dbus_dict_string(&arr_iter, "handle_token", token);
 		append_dbus_dict_bool(&arr_iter, "multiple", p_mode == DisplayServer::FILE_DIALOG_MODE_OPEN_FILES);
 		append_dbus_dict_bool(&arr_iter, "multiple", p_mode == DisplayServer::FILE_DIALOG_MODE_OPEN_FILES);
 		append_dbus_dict_bool(&arr_iter, "directory", p_mode == DisplayServer::FILE_DIALOG_MODE_OPEN_DIR);
 		append_dbus_dict_bool(&arr_iter, "directory", p_mode == DisplayServer::FILE_DIALOG_MODE_OPEN_DIR);
-		append_dbus_dict_filters(&arr_iter, filter_names, filter_exts);
+		append_dbus_dict_filters(&arr_iter, filter_names, filter_exts, filter_mimes);
 
 
 		append_dbus_dict_options(&arr_iter, p_options, fd.option_ids);
 		append_dbus_dict_options(&arr_iter, p_options, fd.option_ids);
 		append_dbus_dict_string(&arr_iter, "current_folder", p_current_directory, true);
 		append_dbus_dict_string(&arr_iter, "current_folder", p_current_directory, true);

+ 1 - 1
platform/linuxbsd/freedesktop_portal_desktop.h

@@ -51,7 +51,7 @@ private:
 
 
 	static void append_dbus_string(DBusMessageIter *p_iter, const String &p_string);
 	static void append_dbus_string(DBusMessageIter *p_iter, const String &p_string);
 	static void append_dbus_dict_options(DBusMessageIter *p_iter, const TypedArray<Dictionary> &p_options, HashMap<String, String> &r_ids);
 	static void append_dbus_dict_options(DBusMessageIter *p_iter, const TypedArray<Dictionary> &p_options, HashMap<String, String> &r_ids);
-	static void append_dbus_dict_filters(DBusMessageIter *p_iter, const Vector<String> &p_filter_names, const Vector<String> &p_filter_exts);
+	static void append_dbus_dict_filters(DBusMessageIter *p_iter, const Vector<String> &p_filter_names, const Vector<String> &p_filter_exts, const Vector<String> &p_filter_mimes);
 	static void append_dbus_dict_string(DBusMessageIter *p_iter, const String &p_key, const String &p_value, bool p_as_byte_array = false);
 	static void append_dbus_dict_string(DBusMessageIter *p_iter, const String &p_key, const String &p_value, bool p_as_byte_array = false);
 	static void append_dbus_dict_bool(DBusMessageIter *p_iter, const String &p_key, bool p_value);
 	static void append_dbus_dict_bool(DBusMessageIter *p_iter, const String &p_key, bool p_value);
 	static bool file_chooser_parse_response(DBusMessageIter *p_iter, const Vector<String> &p_names, const HashMap<String, String> &p_ids, bool &r_cancel, Vector<String> &r_urls, int &r_index, Dictionary &r_options);
 	static bool file_chooser_parse_response(DBusMessageIter *p_iter, const Vector<String> &p_names, const HashMap<String, String> &p_ids, bool &r_cancel, Vector<String> &r_urls, int &r_index, Dictionary &r_options);

+ 2 - 1
platform/linuxbsd/wayland/display_server_wayland.cpp

@@ -217,7 +217,8 @@ bool DisplayServerWayland::has_feature(Feature p_feature) const {
 		//case FEATURE_NATIVE_DIALOG_INPUT:
 		//case FEATURE_NATIVE_DIALOG_INPUT:
 #ifdef DBUS_ENABLED
 #ifdef DBUS_ENABLED
 		case FEATURE_NATIVE_DIALOG_FILE:
 		case FEATURE_NATIVE_DIALOG_FILE:
-		case FEATURE_NATIVE_DIALOG_FILE_EXTRA: {
+		case FEATURE_NATIVE_DIALOG_FILE_EXTRA:
+		case FEATURE_NATIVE_DIALOG_FILE_MIME: {
 			return true;
 			return true;
 		} break;
 		} break;
 #endif
 #endif

+ 1 - 0
platform/linuxbsd/x11/display_server_x11.cpp

@@ -132,6 +132,7 @@ bool DisplayServerX11::has_feature(Feature p_feature) const {
 #ifdef DBUS_ENABLED
 #ifdef DBUS_ENABLED
 		case FEATURE_NATIVE_DIALOG_FILE:
 		case FEATURE_NATIVE_DIALOG_FILE:
 		case FEATURE_NATIVE_DIALOG_FILE_EXTRA:
 		case FEATURE_NATIVE_DIALOG_FILE_EXTRA:
+		case FEATURE_NATIVE_DIALOG_FILE_MIME:
 #endif
 #endif
 		//case FEATURE_NATIVE_DIALOG:
 		//case FEATURE_NATIVE_DIALOG:
 		//case FEATURE_NATIVE_DIALOG_INPUT:
 		//case FEATURE_NATIVE_DIALOG_INPUT:

+ 2 - 0
platform/macos/detect.py

@@ -217,6 +217,8 @@ def configure(env: "SConsEnvironment"):
             "QuartzCore",
             "QuartzCore",
             "-framework",
             "-framework",
             "Security",
             "Security",
+            "-framework",
+            "UniformTypeIdentifiers",
         ]
         ]
     )
     )
     env.Append(LIBS=["pthread", "z"])
     env.Append(LIBS=["pthread", "z"])

+ 1 - 0
platform/macos/display_server_macos.mm

@@ -774,6 +774,7 @@ bool DisplayServerMacOS::has_feature(Feature p_feature) const {
 		case FEATURE_NATIVE_DIALOG_INPUT:
 		case FEATURE_NATIVE_DIALOG_INPUT:
 		case FEATURE_NATIVE_DIALOG_FILE:
 		case FEATURE_NATIVE_DIALOG_FILE:
 		case FEATURE_NATIVE_DIALOG_FILE_EXTRA:
 		case FEATURE_NATIVE_DIALOG_FILE_EXTRA:
+		case FEATURE_NATIVE_DIALOG_FILE_MIME:
 		case FEATURE_IME:
 		case FEATURE_IME:
 		case FEATURE_WINDOW_TRANSPARENCY:
 		case FEATURE_WINDOW_TRANSPARENCY:
 		case FEATURE_HIDPI:
 		case FEATURE_HIDPI:

+ 1 - 0
platform/macos/godot_open_save_delegate.h

@@ -33,6 +33,7 @@
 
 
 #import <AppKit/AppKit.h>
 #import <AppKit/AppKit.h>
 #import <Foundation/Foundation.h>
 #import <Foundation/Foundation.h>
+#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
 
 
 #include "core/templates/hash_map.h"
 #include "core/templates/hash_map.h"
 #include "core/variant/typed_array.h"
 #include "core/variant/typed_array.h"

+ 93 - 14
platform/macos/godot_open_save_delegate.mm

@@ -119,13 +119,39 @@
 				Vector<String> tokens = p_filters[i].split(";");
 				Vector<String> tokens = p_filters[i].split(";");
 				if (tokens.size() >= 1) {
 				if (tokens.size() >= 1) {
 					String flt = tokens[0].strip_edges();
 					String flt = tokens[0].strip_edges();
+					String mime = (tokens.size() >= 2) ? tokens[2].strip_edges() : String();
 					int filter_slice_count = flt.get_slice_count(",");
 					int filter_slice_count = flt.get_slice_count(",");
 
 
 					NSMutableArray *type_filters = [[NSMutableArray alloc] init];
 					NSMutableArray *type_filters = [[NSMutableArray alloc] init];
 					for (int j = 0; j < filter_slice_count; j++) {
 					for (int j = 0; j < filter_slice_count; j++) {
 						String str = (flt.get_slice(",", j).strip_edges());
 						String str = (flt.get_slice(",", j).strip_edges());
 						if (!str.is_empty()) {
 						if (!str.is_empty()) {
-							[type_filters addObject:[NSString stringWithUTF8String:str.replace("*.", "").strip_edges().utf8().get_data()]];
+							if (@available(macOS 11, *)) {
+								UTType *ut = nullptr;
+								if (str == "*.*") {
+									ut = UTTypeData;
+								} else {
+									ut = [UTType typeWithFilenameExtension:[NSString stringWithUTF8String:str.replace("*.", "").strip_edges().utf8().get_data()]];
+								}
+								if (ut) {
+									[type_filters addObject:ut];
+								}
+							} else {
+								[type_filters addObject:[NSString stringWithUTF8String:str.replace("*.", "").strip_edges().utf8().get_data()]];
+							}
+						}
+					}
+
+					if (@available(macOS 11, *)) {
+						filter_slice_count = mime.get_slice_count(",");
+						for (int j = 0; j < filter_slice_count; j++) {
+							String str = mime.get_slicec(',', j).strip_edges();
+							if (!str.is_empty()) {
+								UTType *ut = [UTType typeWithMIMEType:[NSString stringWithUTF8String:str.strip_edges().utf8().get_data()]];
+								if (ut) {
+									[type_filters addObject:ut];
+								}
+							}
 						}
 						}
 					}
 					}
 
 
@@ -147,13 +173,38 @@
 			Vector<String> tokens = p_filters[0].split(";");
 			Vector<String> tokens = p_filters[0].split(";");
 			if (tokens.size() >= 1) {
 			if (tokens.size() >= 1) {
 				String flt = tokens[0].strip_edges();
 				String flt = tokens[0].strip_edges();
+				String mime = (tokens.size() >= 2) ? tokens[2] : String();
 				int filter_slice_count = flt.get_slice_count(",");
 				int filter_slice_count = flt.get_slice_count(",");
 
 
 				NSMutableArray *type_filters = [[NSMutableArray alloc] init];
 				NSMutableArray *type_filters = [[NSMutableArray alloc] init];
 				for (int j = 0; j < filter_slice_count; j++) {
 				for (int j = 0; j < filter_slice_count; j++) {
 					String str = (flt.get_slice(",", j).strip_edges());
 					String str = (flt.get_slice(",", j).strip_edges());
 					if (!str.is_empty()) {
 					if (!str.is_empty()) {
-						[type_filters addObject:[NSString stringWithUTF8String:str.replace("*.", "").strip_edges().utf8().get_data()]];
+						if (@available(macOS 11, *)) {
+							UTType *ut = nullptr;
+							if (str == "*.*") {
+								ut = UTTypeData;
+							} else {
+								ut = [UTType typeWithFilenameExtension:[NSString stringWithUTF8String:str.replace("*.", "").strip_edges().utf8().get_data()]];
+							}
+							if (ut) {
+								[type_filters addObject:ut];
+							}
+						} else {
+							[type_filters addObject:[NSString stringWithUTF8String:str.replace("*.", "").strip_edges().utf8().get_data()]];
+						}
+					}
+				}
+				if (@available(macOS 11, *)) {
+					filter_slice_count = mime.get_slice_count(",");
+					for (int j = 0; j < filter_slice_count; j++) {
+						String str = mime.get_slicec(',', j).strip_edges();
+						if (!str.is_empty()) {
+							UTType *ut = [UTType typeWithMIMEType:[NSString stringWithUTF8String:str.strip_edges().utf8().get_data()]];
+							if (ut) {
+								[type_filters addObject:ut];
+							}
+						}
 					}
 					}
 				}
 				}
 
 
@@ -176,15 +227,29 @@
 	}
 	}
 	if ([new_allowed_types count] > 0) {
 	if ([new_allowed_types count] > 0) {
 		NSMutableArray *type_filters = [new_allowed_types objectAtIndex:0];
 		NSMutableArray *type_filters = [new_allowed_types objectAtIndex:0];
-		if (type_filters && [type_filters count] == 1 && [[type_filters objectAtIndex:0] isEqualToString:@"*"]) {
-			[p_panel setAllowedFileTypes:nil];
-			[p_panel setAllowsOtherFileTypes:true];
+		if (@available(macOS 11, *)) {
+			if (type_filters && [type_filters count] == 1 && [type_filters objectAtIndex:0] == UTTypeData) {
+				[p_panel setAllowedContentTypes:@[ UTTypeData ]];
+				[p_panel setAllowsOtherFileTypes:true];
+			} else {
+				[p_panel setAllowsOtherFileTypes:false];
+				[p_panel setAllowedContentTypes:type_filters];
+			}
 		} else {
 		} else {
-			[p_panel setAllowsOtherFileTypes:false];
-			[p_panel setAllowedFileTypes:type_filters];
+			if (type_filters && [type_filters count] == 1 && [[type_filters objectAtIndex:0] isEqualToString:@"*"]) {
+				[p_panel setAllowedFileTypes:nil];
+				[p_panel setAllowsOtherFileTypes:true];
+			} else {
+				[p_panel setAllowsOtherFileTypes:false];
+				[p_panel setAllowedFileTypes:type_filters];
+			}
 		}
 		}
 	} else {
 	} else {
-		[p_panel setAllowedFileTypes:nil];
+		if (@available(macOS 11, *)) {
+			[p_panel setAllowedContentTypes:@[ UTTypeData ]];
+		} else {
+			[p_panel setAllowedFileTypes:nil];
+		}
 		[p_panel setAllowsOtherFileTypes:true];
 		[p_panel setAllowsOtherFileTypes:true];
 	}
 	}
 }
 }
@@ -247,16 +312,30 @@
 		NSUInteger index = [btn indexOfSelectedItem];
 		NSUInteger index = [btn indexOfSelectedItem];
 		if (allowed_types && index < [allowed_types count]) {
 		if (allowed_types && index < [allowed_types count]) {
 			NSMutableArray *type_filters = [allowed_types objectAtIndex:index];
 			NSMutableArray *type_filters = [allowed_types objectAtIndex:index];
-			if (type_filters && [type_filters count] == 1 && [[type_filters objectAtIndex:0] isEqualToString:@"*"]) {
-				[dialog setAllowedFileTypes:nil];
-				[dialog setAllowsOtherFileTypes:true];
+			if (@available(macOS 11, *)) {
+				if (type_filters && [type_filters count] == 1 && [type_filters objectAtIndex:0] == UTTypeData) {
+					[dialog setAllowedContentTypes:@[ UTTypeData ]];
+					[dialog setAllowsOtherFileTypes:true];
+				} else {
+					[dialog setAllowsOtherFileTypes:false];
+					[dialog setAllowedContentTypes:type_filters];
+				}
 			} else {
 			} else {
-				[dialog setAllowsOtherFileTypes:false];
-				[dialog setAllowedFileTypes:type_filters];
+				if (type_filters && [type_filters count] == 1 && [[type_filters objectAtIndex:0] isEqualToString:@"*"]) {
+					[dialog setAllowedFileTypes:nil];
+					[dialog setAllowsOtherFileTypes:true];
+				} else {
+					[dialog setAllowsOtherFileTypes:false];
+					[dialog setAllowedFileTypes:type_filters];
+				}
 			}
 			}
 			cur_index = index;
 			cur_index = index;
 		} else {
 		} else {
-			[dialog setAllowedFileTypes:nil];
+			if (@available(macOS 11, *)) {
+				[dialog setAllowedContentTypes:@[ UTTypeData ]];
+			} else {
+				[dialog setAllowedFileTypes:nil];
+			}
 			[dialog setAllowsOtherFileTypes:true];
 			[dialog setAllowsOtherFileTypes:true];
 			cur_index = -1;
 			cur_index = -1;
 		}
 		}

+ 1 - 0
platform/web/display_server_web.cpp

@@ -1134,6 +1134,7 @@ bool DisplayServerWeb::has_feature(Feature p_feature) const {
 		//case FEATURE_NATIVE_DIALOG_INPUT:
 		//case FEATURE_NATIVE_DIALOG_INPUT:
 		//case FEATURE_NATIVE_DIALOG_FILE:
 		//case FEATURE_NATIVE_DIALOG_FILE:
 		//case FEATURE_NATIVE_DIALOG_FILE_EXTRA:
 		//case FEATURE_NATIVE_DIALOG_FILE_EXTRA:
+		//case FEATURE_NATIVE_DIALOG_FILE_MIME:
 		//case FEATURE_NATIVE_ICON:
 		//case FEATURE_NATIVE_ICON:
 		//case FEATURE_WINDOW_TRANSPARENCY:
 		//case FEATURE_WINDOW_TRANSPARENCY:
 		//case FEATURE_KEEP_SCREEN_ON:
 		//case FEATURE_KEEP_SCREEN_ON:

+ 1 - 0
platform/windows/display_server_windows.cpp

@@ -131,6 +131,7 @@ bool DisplayServerWindows::has_feature(Feature p_feature) const {
 		case FEATURE_NATIVE_DIALOG_INPUT:
 		case FEATURE_NATIVE_DIALOG_INPUT:
 		case FEATURE_NATIVE_DIALOG_FILE:
 		case FEATURE_NATIVE_DIALOG_FILE:
 		case FEATURE_NATIVE_DIALOG_FILE_EXTRA:
 		case FEATURE_NATIVE_DIALOG_FILE_EXTRA:
+		//case FEATURE_NATIVE_DIALOG_FILE_MIME:
 		case FEATURE_SWAP_BUFFERS:
 		case FEATURE_SWAP_BUFFERS:
 		case FEATURE_KEEP_SCREEN_ON:
 		case FEATURE_KEEP_SCREEN_ON:
 		case FEATURE_TEXT_TO_SPEECH:
 		case FEATURE_TEXT_TO_SPEECH:

+ 47 - 14
scene/gui/file_dialog.cpp

@@ -958,50 +958,83 @@ void FileDialog::update_filters() {
 
 
 	if (filters.size() > 1) {
 	if (filters.size() > 1) {
 		String all_filters;
 		String all_filters;
+		String all_mime;
 		String all_filters_full;
 		String all_filters_full;
+		String all_mime_full;
 
 
 		const int max_filters = 5;
 		const int max_filters = 5;
 
 
+		// "All Recognized" display name.
 		for (int i = 0; i < MIN(max_filters, filters.size()); i++) {
 		for (int i = 0; i < MIN(max_filters, filters.size()); i++) {
 			String flt = filters[i].get_slicec(';', 0).strip_edges();
 			String flt = filters[i].get_slicec(';', 0).strip_edges();
-			if (i > 0) {
+			if (!all_filters.is_empty() && !flt.is_empty()) {
 				all_filters += ", ";
 				all_filters += ", ";
 			}
 			}
 			all_filters += flt;
 			all_filters += flt;
+
+			String mime = filters[i].get_slicec(';', 2).strip_edges();
+			if (!all_mime.is_empty() && !mime.is_empty()) {
+				all_mime += ", ";
+			}
+			all_mime += mime;
 		}
 		}
+
+		// "All Recognized" filter.
 		for (int i = 0; i < filters.size(); i++) {
 		for (int i = 0; i < filters.size(); i++) {
 			String flt = filters[i].get_slicec(';', 0).strip_edges();
 			String flt = filters[i].get_slicec(';', 0).strip_edges();
-			if (i > 0) {
+			if (!all_filters_full.is_empty() && !flt.is_empty()) {
 				all_filters_full += ",";
 				all_filters_full += ",";
 			}
 			}
 			all_filters_full += flt;
 			all_filters_full += flt;
+
+			String mime = filters[i].get_slicec(';', 2).strip_edges();
+			if (!all_mime_full.is_empty() && !mime.is_empty()) {
+				all_mime_full += ",";
+			}
+			all_mime_full += mime;
 		}
 		}
 
 
+		String native_all_name;
+		if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_NATIVE_DIALOG_FILE_MIME)) {
+			native_all_name += all_filters;
+		}
+		if (!native_all_name.is_empty()) {
+			native_all_name += ", ";
+		}
+		native_all_name += all_mime;
+
 		if (max_filters < filters.size()) {
 		if (max_filters < filters.size()) {
 			all_filters += ", ...";
 			all_filters += ", ...";
+			native_all_name += ", ...";
 		}
 		}
 
 
-		String f = atr(ETR("All Recognized")) + " (" + all_filters + ")";
-		filter->add_item(f);
-		processed_filters.push_back(all_filters_full + ";" + f);
+		filter->add_item(atr(ETR("All Recognized")) + " (" + all_filters + ")");
+		processed_filters.push_back(all_filters_full + ";" + atr(ETR("All Recognized")) + " (" + native_all_name + ")" + ";" + all_mime_full);
 	}
 	}
 	for (int i = 0; i < filters.size(); i++) {
 	for (int i = 0; i < filters.size(); i++) {
 		String flt = filters[i].get_slicec(';', 0).strip_edges();
 		String flt = filters[i].get_slicec(';', 0).strip_edges();
-		String desc = filters[i].get_slice(";", 1).strip_edges();
-		if (desc.length()) {
-			String f = atr(desc) + " (" + flt + ")";
-			filter->add_item(f);
-			processed_filters.push_back(flt + ";" + f);
+		String desc = filters[i].get_slicec(';', 1).strip_edges();
+		String mime = filters[i].get_slicec(';', 2).strip_edges();
+		String native_name;
+		if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_NATIVE_DIALOG_FILE_MIME)) {
+			native_name += flt;
+		}
+		if (!native_name.is_empty() && !mime.is_empty()) {
+			native_name += ", ";
+		}
+		native_name += mime;
+		if (!desc.is_empty()) {
+			filter->add_item(atr(desc) + " (" + flt + ")");
+			processed_filters.push_back(flt + ";" + atr(desc) + " (" + native_name + ");" + mime);
 		} else {
 		} else {
-			String f = "(" + flt + ")";
-			filter->add_item(f);
-			processed_filters.push_back(flt + ";" + f);
+			filter->add_item("(" + flt + ")");
+			processed_filters.push_back(flt + ";(" + native_name + ");" + mime);
 		}
 		}
 	}
 	}
 
 
 	String f = atr(ETR("All Files")) + " (*.*)";
 	String f = atr(ETR("All Files")) + " (*.*)";
 	filter->add_item(f);
 	filter->add_item(f);
-	processed_filters.push_back("*.*;" + f);
+	processed_filters.push_back("*.*;" + f + ";application/octet-stream");
 }
 }
 
 
 void FileDialog::clear_filename_filter() {
 void FileDialog::clear_filename_filter() {

+ 1 - 0
servers/display_server.cpp

@@ -1083,6 +1083,7 @@ void DisplayServer::_bind_methods() {
 	BIND_ENUM_CONSTANT(FEATURE_WINDOW_DRAG);
 	BIND_ENUM_CONSTANT(FEATURE_WINDOW_DRAG);
 	BIND_ENUM_CONSTANT(FEATURE_SCREEN_EXCLUDE_FROM_CAPTURE);
 	BIND_ENUM_CONSTANT(FEATURE_SCREEN_EXCLUDE_FROM_CAPTURE);
 	BIND_ENUM_CONSTANT(FEATURE_WINDOW_EMBEDDING);
 	BIND_ENUM_CONSTANT(FEATURE_WINDOW_EMBEDDING);
+	BIND_ENUM_CONSTANT(FEATURE_NATIVE_DIALOG_FILE_MIME);
 
 
 	BIND_ENUM_CONSTANT(MOUSE_MODE_VISIBLE);
 	BIND_ENUM_CONSTANT(MOUSE_MODE_VISIBLE);
 	BIND_ENUM_CONSTANT(MOUSE_MODE_HIDDEN);
 	BIND_ENUM_CONSTANT(MOUSE_MODE_HIDDEN);

+ 1 - 0
servers/display_server.h

@@ -156,6 +156,7 @@ public:
 		FEATURE_WINDOW_DRAG,
 		FEATURE_WINDOW_DRAG,
 		FEATURE_SCREEN_EXCLUDE_FROM_CAPTURE,
 		FEATURE_SCREEN_EXCLUDE_FROM_CAPTURE,
 		FEATURE_WINDOW_EMBEDDING,
 		FEATURE_WINDOW_EMBEDDING,
+		FEATURE_NATIVE_DIALOG_FILE_MIME,
 	};
 	};
 
 
 	virtual bool has_feature(Feature p_feature) const = 0;
 	virtual bool has_feature(Feature p_feature) const = 0;