From e1f129cb527712df5b9aac75a6a0d30cdce33bd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pa=CC=84vels=20Nadtoc=CC=8Cajevs?= <7645683+bruvzg@users.noreply.github.com> Date: Sun, 17 Nov 2024 12:25:23 +0200 Subject: [PATCH] Support MIME types in file dialog filters on macOS and Linux. --- doc/classes/DisplayServer.xml | 10 +- doc/classes/FileDialog.xml | 4 +- editor/gui/editor_file_dialog.cpp | 61 +++++++--- platform/android/display_server_android.cpp | 1 + platform/android/java_godot_wrapper.cpp | 11 +- platform/ios/display_server_ios.mm | 1 + .../linuxbsd/freedesktop_portal_desktop.cpp | 30 ++++- .../linuxbsd/freedesktop_portal_desktop.h | 2 +- .../wayland/display_server_wayland.cpp | 3 +- platform/linuxbsd/x11/display_server_x11.cpp | 1 + platform/macos/detect.py | 2 + platform/macos/display_server_macos.mm | 1 + platform/macos/godot_open_save_delegate.h | 1 + platform/macos/godot_open_save_delegate.mm | 107 +++++++++++++++--- platform/web/display_server_web.cpp | 1 + platform/windows/display_server_windows.cpp | 1 + scene/gui/file_dialog.cpp | 61 +++++++--- servers/display_server.cpp | 1 + servers/display_server.h | 1 + 19 files changed, 242 insertions(+), 58 deletions(-) diff --git a/doc/classes/DisplayServer.xml b/doc/classes/DisplayServer.xml index 1f495b87117..cdfd336e2f0 100644 --- a/doc/classes/DisplayServer.xml +++ b/doc/classes/DisplayServer.xml @@ -145,11 +145,11 @@ 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. [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] 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 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. @@ -168,7 +168,7 @@ 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: - [code]"name"[/code] - option's name [String]. - [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]. [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] 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 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. @@ -1928,6 +1929,9 @@ Display server supports embedding a window from another process. [b]Windows, Linux (X11)[/b] + + Native file selection dialog supports MIME types as filters. + Makes the mouse cursor visible if it is hidden. diff --git a/doc/classes/FileDialog.xml b/doc/classes/FileDialog.xml index 64369bec30e..921fb0b7146 100644 --- a/doc/classes/FileDialog.xml +++ b/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. - 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. 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"). diff --git a/editor/gui/editor_file_dialog.cpp b/editor/gui/editor_file_dialog.cpp index daca6c6aae1..0f4d1161d74 100644 --- a/editor/gui/editor_file_dialog.cpp +++ b/editor/gui/editor_file_dialog.cpp @@ -1218,50 +1218,83 @@ void EditorFileDialog::update_filters() { if (filters.size() > 1) { String all_filters; + String all_mime; String all_filters_full; + String all_mime_full; const int max_filters = 5; + // "All Recognized" display name. for (int i = 0; i < MIN(max_filters, filters.size()); i++) { String flt = filters[i].get_slicec(';', 0).strip_edges(); - if (i > 0) { + if (!all_filters.is_empty() && !flt.is_empty()) { all_filters += ", "; } 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++) { 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 += 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()) { 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++) { 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 { - 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") + " (*.*)"; filter->add_item(f); - processed_filters.push_back("*.*;" + f); + processed_filters.push_back("*.*;" + f + ";application/octet-stream"); } void EditorFileDialog::clear_filters() { diff --git a/platform/android/display_server_android.cpp b/platform/android/display_server_android.cpp index d624bbbeb2c..77b879133ed 100644 --- a/platform/android/display_server_android.cpp +++ b/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_FILE: //case FEATURE_NATIVE_DIALOG_FILE_EXTRA: + case FEATURE_NATIVE_DIALOG_FILE_MIME: //case FEATURE_NATIVE_ICON: //case FEATURE_WINDOW_TRANSPARENCY: case FEATURE_CLIPBOARD: diff --git a/platform/android/java_godot_wrapper.cpp b/platform/android/java_godot_wrapper.cpp index 69d7ac37ea6..42249b9f588 100644 --- a/platform/android/java_godot_wrapper.cpp +++ b/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_filename = env->NewStringUTF(p_filename.utf8().get_data()); 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 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->DeleteLocalRef(j_filter); } diff --git a/platform/ios/display_server_ios.mm b/platform/ios/display_server_ios.mm index 4bb290b5d5b..09acc199bf9 100644 --- a/platform/ios/display_server_ios.mm +++ b/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_FILE: // case FEATURE_NATIVE_DIALOG_FILE_EXTRA: + // case FEATURE_NATIVE_DIALOG_FILE_MIME: // case FEATURE_NATIVE_ICON: // case FEATURE_WINDOW_TRANSPARENCY: case FEATURE_CLIPBOARD: diff --git a/platform/linuxbsd/freedesktop_portal_desktop.cpp b/platform/linuxbsd/freedesktop_portal_desktop.cpp index ae7a17a1da2..513499c1759 100644 --- a/platform/linuxbsd/freedesktop_portal_desktop.cpp +++ b/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); } -void FreeDesktopPortalDesktop::append_dbus_dict_filters(DBusMessageIter *p_iter, const Vector &p_filter_names, const Vector &p_filter_exts) { +void FreeDesktopPortalDesktop::append_dbus_dict_filters(DBusMessageIter *p_iter, const Vector &p_filter_names, const Vector &p_filter_exts, const Vector &p_filter_mimes) { DBusMessageIter dict_iter; DBusMessageIter var_iter; DBusMessageIter arr_iter; 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_mimes.size()); 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); @@ -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); 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); dbus_message_iter_close_container(&array_iter, &array_struct_iter); @@ -384,17 +397,20 @@ Error FreeDesktopPortalDesktop::file_dialog_show(DisplayServer::WindowID p_windo Vector filter_names; Vector filter_exts; + Vector filter_mimes; for (int i = 0; i < p_filters.size(); i++) { Vector tokens = p_filters[i].split(";"); if (tokens.size() >= 1) { 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 == "*.*") { filter_exts.push_back("*"); } else { filter_exts.push_back(flt); } + filter_mimes.push_back(mime); filter_names.push_back(tokens[1]); } else { if (flt == "*.*") { @@ -404,12 +420,14 @@ Error FreeDesktopPortalDesktop::file_dialog_show(DisplayServer::WindowID p_windo filter_exts.push_back(flt); filter_names.push_back(flt); } + filter_mimes.push_back(mime); } } } } if (filter_names.is_empty()) { filter_exts.push_back("*"); + filter_mimes.push_back(""); 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_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_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_string(&arr_iter, "current_folder", p_current_directory, true); diff --git a/platform/linuxbsd/freedesktop_portal_desktop.h b/platform/linuxbsd/freedesktop_portal_desktop.h index 413c31dd746..330004758af 100644 --- a/platform/linuxbsd/freedesktop_portal_desktop.h +++ b/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_dict_options(DBusMessageIter *p_iter, const TypedArray &p_options, HashMap &r_ids); - static void append_dbus_dict_filters(DBusMessageIter *p_iter, const Vector &p_filter_names, const Vector &p_filter_exts); + static void append_dbus_dict_filters(DBusMessageIter *p_iter, const Vector &p_filter_names, const Vector &p_filter_exts, const Vector &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_bool(DBusMessageIter *p_iter, const String &p_key, bool p_value); static bool file_chooser_parse_response(DBusMessageIter *p_iter, const Vector &p_names, const HashMap &p_ids, bool &r_cancel, Vector &r_urls, int &r_index, Dictionary &r_options); diff --git a/platform/linuxbsd/wayland/display_server_wayland.cpp b/platform/linuxbsd/wayland/display_server_wayland.cpp index 27e936124e4..320f4109028 100644 --- a/platform/linuxbsd/wayland/display_server_wayland.cpp +++ b/platform/linuxbsd/wayland/display_server_wayland.cpp @@ -217,7 +217,8 @@ bool DisplayServerWayland::has_feature(Feature p_feature) const { //case FEATURE_NATIVE_DIALOG_INPUT: #ifdef DBUS_ENABLED 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; } break; #endif diff --git a/platform/linuxbsd/x11/display_server_x11.cpp b/platform/linuxbsd/x11/display_server_x11.cpp index 540726fa6dc..a795c6a9d57 100644 --- a/platform/linuxbsd/x11/display_server_x11.cpp +++ b/platform/linuxbsd/x11/display_server_x11.cpp @@ -132,6 +132,7 @@ bool DisplayServerX11::has_feature(Feature p_feature) const { #ifdef DBUS_ENABLED case FEATURE_NATIVE_DIALOG_FILE: case FEATURE_NATIVE_DIALOG_FILE_EXTRA: + case FEATURE_NATIVE_DIALOG_FILE_MIME: #endif //case FEATURE_NATIVE_DIALOG: //case FEATURE_NATIVE_DIALOG_INPUT: diff --git a/platform/macos/detect.py b/platform/macos/detect.py index 61a9f0d8c85..510671db6a5 100644 --- a/platform/macos/detect.py +++ b/platform/macos/detect.py @@ -217,6 +217,8 @@ def configure(env: "SConsEnvironment"): "QuartzCore", "-framework", "Security", + "-framework", + "UniformTypeIdentifiers", ] ) env.Append(LIBS=["pthread", "z"]) diff --git a/platform/macos/display_server_macos.mm b/platform/macos/display_server_macos.mm index 1986169442f..253b4b0a9fe 100644 --- a/platform/macos/display_server_macos.mm +++ b/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_FILE: case FEATURE_NATIVE_DIALOG_FILE_EXTRA: + case FEATURE_NATIVE_DIALOG_FILE_MIME: case FEATURE_IME: case FEATURE_WINDOW_TRANSPARENCY: case FEATURE_HIDPI: diff --git a/platform/macos/godot_open_save_delegate.h b/platform/macos/godot_open_save_delegate.h index 8857ef1fa95..12349ef9901 100644 --- a/platform/macos/godot_open_save_delegate.h +++ b/platform/macos/godot_open_save_delegate.h @@ -33,6 +33,7 @@ #import #import +#import #include "core/templates/hash_map.h" #include "core/variant/typed_array.h" diff --git a/platform/macos/godot_open_save_delegate.mm b/platform/macos/godot_open_save_delegate.mm index 0d6bfa0c531..fa986c02cec 100644 --- a/platform/macos/godot_open_save_delegate.mm +++ b/platform/macos/godot_open_save_delegate.mm @@ -119,13 +119,39 @@ Vector tokens = p_filters[i].split(";"); if (tokens.size() >= 1) { String flt = tokens[0].strip_edges(); + String mime = (tokens.size() >= 2) ? tokens[2].strip_edges() : String(); int filter_slice_count = flt.get_slice_count(","); NSMutableArray *type_filters = [[NSMutableArray alloc] init]; for (int j = 0; j < filter_slice_count; j++) { String str = (flt.get_slice(",", j).strip_edges()); 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 tokens = p_filters[0].split(";"); if (tokens.size() >= 1) { String flt = tokens[0].strip_edges(); + String mime = (tokens.size() >= 2) ? tokens[2] : String(); int filter_slice_count = flt.get_slice_count(","); NSMutableArray *type_filters = [[NSMutableArray alloc] init]; for (int j = 0; j < filter_slice_count; j++) { String str = (flt.get_slice(",", j).strip_edges()); 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) { 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 { - [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 { - [p_panel setAllowedFileTypes:nil]; + if (@available(macOS 11, *)) { + [p_panel setAllowedContentTypes:@[ UTTypeData ]]; + } else { + [p_panel setAllowedFileTypes:nil]; + } [p_panel setAllowsOtherFileTypes:true]; } } @@ -247,16 +312,30 @@ NSUInteger index = [btn indexOfSelectedItem]; if (allowed_types && index < [allowed_types count]) { 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 { - [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; } else { - [dialog setAllowedFileTypes:nil]; + if (@available(macOS 11, *)) { + [dialog setAllowedContentTypes:@[ UTTypeData ]]; + } else { + [dialog setAllowedFileTypes:nil]; + } [dialog setAllowsOtherFileTypes:true]; cur_index = -1; } diff --git a/platform/web/display_server_web.cpp b/platform/web/display_server_web.cpp index 6671eb028ea..8b4a733a088 100644 --- a/platform/web/display_server_web.cpp +++ b/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_FILE: //case FEATURE_NATIVE_DIALOG_FILE_EXTRA: + //case FEATURE_NATIVE_DIALOG_FILE_MIME: //case FEATURE_NATIVE_ICON: //case FEATURE_WINDOW_TRANSPARENCY: //case FEATURE_KEEP_SCREEN_ON: diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp index 3c1a3915217..46846a45f7f 100644 --- a/platform/windows/display_server_windows.cpp +++ b/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_FILE: case FEATURE_NATIVE_DIALOG_FILE_EXTRA: + //case FEATURE_NATIVE_DIALOG_FILE_MIME: case FEATURE_SWAP_BUFFERS: case FEATURE_KEEP_SCREEN_ON: case FEATURE_TEXT_TO_SPEECH: diff --git a/scene/gui/file_dialog.cpp b/scene/gui/file_dialog.cpp index de1ec3c6164..d685da9a52c 100644 --- a/scene/gui/file_dialog.cpp +++ b/scene/gui/file_dialog.cpp @@ -958,50 +958,83 @@ void FileDialog::update_filters() { if (filters.size() > 1) { String all_filters; + String all_mime; String all_filters_full; + String all_mime_full; const int max_filters = 5; + // "All Recognized" display name. for (int i = 0; i < MIN(max_filters, filters.size()); i++) { String flt = filters[i].get_slicec(';', 0).strip_edges(); - if (i > 0) { + if (!all_filters.is_empty() && !flt.is_empty()) { all_filters += ", "; } 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++) { 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 += 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()) { 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++) { 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 { - 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")) + " (*.*)"; filter->add_item(f); - processed_filters.push_back("*.*;" + f); + processed_filters.push_back("*.*;" + f + ";application/octet-stream"); } void FileDialog::clear_filename_filter() { diff --git a/servers/display_server.cpp b/servers/display_server.cpp index b31c4d6b1d3..c105a530174 100644 --- a/servers/display_server.cpp +++ b/servers/display_server.cpp @@ -1083,6 +1083,7 @@ void DisplayServer::_bind_methods() { BIND_ENUM_CONSTANT(FEATURE_WINDOW_DRAG); BIND_ENUM_CONSTANT(FEATURE_SCREEN_EXCLUDE_FROM_CAPTURE); 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_HIDDEN); diff --git a/servers/display_server.h b/servers/display_server.h index a49e31a474c..f804c22d088 100644 --- a/servers/display_server.h +++ b/servers/display_server.h @@ -156,6 +156,7 @@ public: FEATURE_WINDOW_DRAG, FEATURE_SCREEN_EXCLUDE_FROM_CAPTURE, FEATURE_WINDOW_EMBEDDING, + FEATURE_NATIVE_DIALOG_FILE_MIME, }; virtual bool has_feature(Feature p_feature) const = 0;