Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions doc/classes/DisplayServer.xml
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,11 @@
<param index="6" name="callback" type="Callable" />
<description>
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. Both file extensions and MIME type should be always set. See also [member FileDialog.filters].
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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. Both file extensions and MIME type should be always set. 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, Android, Linux and macOS file dialogs also support MIME types.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
[b]Note:[/b] Embedded file dialog, and Windows file dialog support only file extensions, Android, Linux and macOS file dialogs also support MIME types.
[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.
Expand All @@ -161,14 +161,15 @@
<param index="8" name="callback" type="Callable" />
<description>
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. Both file extensions and MIME type should be always set. See also [member FileDialog.filters].
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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. Both file extensions and MIME type should be always set. 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.
- [code]"default"[/code] - default selected option index ([int]) or default boolean value ([bool]).
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, Android, Linux and macOS file dialogs also support MIME types.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
[b]Note:[/b] Embedded file dialog, and Windows file dialog support only file extensions, Android, Linux and macOS file dialogs also support MIME types.
[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.
Expand Down Expand Up @@ -1895,6 +1896,9 @@
<constant name="FEATURE_NATIVE_DIALOG_FILE_EXTRA" value="26" enum="Feature">
The display server supports all features of [constant FEATURE_NATIVE_DIALOG_FILE], with the added functionality of Options and native dialog file access to [code]res://[/code] and [code]user://[/code] paths. See [method file_dialog_show] and [method file_dialog_with_options_show]. [b]Windows, macOS, Linux (X11/Wayland)[/b]
</constant>
<constant name="FEATURE_NATIVE_DIALOG_FILE_MIME" value="27" enum="Feature">
Native file selection dialog supports MIME types as filters.
</constant>
<constant name="MOUSE_MODE_VISIBLE" value="0" enum="MouseMode">
Makes the mouse cursor visible if it is hidden.
</constant>
Expand Down
4 changes: 2 additions & 2 deletions doc/classes/FileDialog.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
</member>
<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, Android, Linux and macOS file dialogs also support MIME types.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
[b]Note:[/b] Embedded file dialog, and Windows file dialog support only file extensions, Android, Linux and macOS file dialogs also support MIME types.
[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 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").
Expand Down
61 changes: 47 additions & 14 deletions editor/gui/editor_file_dialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1152,50 +1152,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 mime = filters[i].get_slice(";", 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 (*)");
String f = atr(ETR("All Files")) + " (*)";
filter->add_item(f);
processed_filters.push_back("*.*;" + f);
processed_filters.push_back("*.*;" + f + ";application/octet-stream");
}

void EditorFileDialog::clear_filters() {
Expand Down
1 change: 1 addition & 0 deletions platform/android/display_server_android.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
11 changes: 8 additions & 3 deletions platform/android/java_godot_wrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -312,9 +312,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].utf8().get_data());
Vector<String> filters;
for (const String &E : p_filters) {
filters.append_array(E.get_slice(";", 0).split(",")); // Add extensions.
filters.append_array(E.get_slice(";", 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);
}
Expand Down
1 change: 1 addition & 0 deletions platform/ios/display_server_ios.mm
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,7 @@
// 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:
Expand Down
30 changes: 24 additions & 6 deletions platform/linuxbsd/freedesktop_portal_desktop.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -190,13 +190,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<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 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);
Expand Down Expand Up @@ -224,8 +225,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_slice(",", 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);
Expand Down Expand Up @@ -379,17 +392,20 @@ Error FreeDesktopPortalDesktop::file_dialog_show(DisplayServer::WindowID p_windo

Vector<String> filter_names;
Vector<String> filter_exts;
Vector<String> filter_mimes;
for (int i = 0; i < p_filters.size(); i++) {
Vector<String> 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 == "*.*") {
Expand All @@ -399,12 +415,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") + " (*)");
}

Expand Down Expand Up @@ -459,7 +477,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);
append_dbus_dict_string(&arr_iter, "current_folder", p_current_directory, true);
Expand Down
2 changes: 1 addition & 1 deletion platform/linuxbsd/freedesktop_portal_desktop.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class FreeDesktopPortalDesktop : public Object {

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);
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_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, bool &r_cancel, Vector<String> &r_urls, int &r_index, Dictionary &r_options);
Expand Down
3 changes: 2 additions & 1 deletion platform/linuxbsd/wayland/display_server_wayland.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions platform/linuxbsd/x11/display_server_x11.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,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:
Expand Down
2 changes: 2 additions & 0 deletions platform/macos/detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,8 @@ def configure(env: "SConsEnvironment"):
"QuartzCore",
"-framework",
"Security",
"-framework",
"UniformTypeIdentifiers",
]
)
env.Append(LIBS=["pthread", "z"])
Expand Down
1 change: 1 addition & 0 deletions platform/macos/display_server_macos.mm
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,7 @@
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:
Expand Down
1 change: 1 addition & 0 deletions platform/macos/godot_open_save_delegate.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@

#import <AppKit/AppKit.h>
#import <Foundation/Foundation.h>
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>

#include "core/templates/hash_map.h"
#include "core/variant/typed_array.h"
Expand Down
Loading