Browse Source

[Native File Dialog] Add support for adding custom options to the dialogs.

Add support for adding custom options (checkboxes and optionboxes) to the dialogs (both native and built-in).
bruvzg 1 year ago
parent
commit
a8f521bcad

+ 27 - 3
doc/classes/DisplayServer.xml

@@ -119,9 +119,33 @@
 			<param index="6" name="callback" type="Callable" />
 			<description>
 				Displays OS native dialog for selecting files or directories in the file system.
-				Callbacks have the following arguments: [code]bool status, PackedStringArray selected_paths, int selected_filter_index[/code].
-				[b]Note:[/b] This method is implemented if the display server has the [constant FEATURE_NATIVE_DIALOG] feature.
-				[b]Note:[/b] This method is implemented on Linux, Windows and macOS.
+				Callbacks have the following arguments: [code]status: bool, selected_paths: PackedStringArray, selected_filter_index: int[/code].
+				[b]Note:[/b] This method is implemented if the display server has the [constant FEATURE_NATIVE_DIALOG] feature, i.e. Linux, Windows, and macOS.
+				[b]Note:[/b] [param current_directory] might be ignored.
+				[b]Note:[/b] On Linux, [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.
+			</description>
+		</method>
+		<method name="file_dialog_with_options_show">
+			<return type="int" enum="Error" />
+			<param index="0" name="title" type="String" />
+			<param index="1" name="current_directory" type="String" />
+			<param index="2" name="root" type="String" />
+			<param index="3" name="filename" type="String" />
+			<param index="4" name="show_hidden" type="bool" />
+			<param index="5" name="mode" type="int" enum="DisplayServer.FileDialogMode" />
+			<param index="6" name="filters" type="PackedStringArray" />
+			<param index="7" name="options" type="Dictionary[]" />
+			<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.
+				[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] feature, i.e. Linux, Windows, and macOS.
 				[b]Note:[/b] [param current_directory] might be ignored.
 				[b]Note:[/b] On Linux, [param show_hidden] is ignored.
 				[b]Note:[/b] On macOS, native file dialogs have no title.

+ 63 - 0
doc/classes/FileDialog.xml

@@ -19,6 +19,15 @@
 				For example, a [param filter] of [code]"*.png, *.jpg"[/code] and a [param description] of [code]"Images"[/code] results in filter text "Images (*.png, *.jpg)".
 			</description>
 		</method>
+		<method name="add_option">
+			<return type="void" />
+			<param index="0" name="name" type="String" />
+			<param index="1" name="values" type="PackedStringArray" />
+			<param index="2" name="index" type="int" />
+			<description>
+				Adds an additional [OptionButton] to the file dialog. If [param values] is empty, a [CheckBox] is added instead.
+			</description>
+		</method>
 		<method name="clear_filters">
 			<return type="void" />
 			<description>
@@ -38,6 +47,33 @@
 				[b]Warning:[/b] This is a required internal node, removing and freeing it may cause a crash. If you wish to hide it or any of its children, use their [member CanvasItem.visible] property.
 			</description>
 		</method>
+		<method name="get_option_default" qualifiers="const">
+			<return type="int" />
+			<param index="0" name="option" type="int" />
+			<description>
+				Returns the default value index of the [OptionButton] or [CheckBox] with index [param option].
+			</description>
+		</method>
+		<method name="get_option_name" qualifiers="const">
+			<return type="String" />
+			<param index="0" name="option" type="int" />
+			<description>
+				Returns the name of the [OptionButton] or [CheckBox] with index [param option].
+			</description>
+		</method>
+		<method name="get_option_values" qualifiers="const">
+			<return type="PackedStringArray" />
+			<param index="0" name="option" type="int" />
+			<description>
+				Returns an array of values of the [OptionButton] with index [param option].
+			</description>
+		</method>
+		<method name="get_selected_options" qualifiers="const">
+			<return type="Dictionary" />
+			<description>
+				Returns a [Dictionary] with the selected values of the additional [OptionButton]s and/or [CheckBox]es. [Dictionary] keys are names and values are selected value indices.
+			</description>
+		</method>
 		<method name="get_vbox">
 			<return type="VBoxContainer" />
 			<description>
@@ -51,6 +87,30 @@
 				Invalidate and update the current dialog content list.
 			</description>
 		</method>
+		<method name="set_option_default">
+			<return type="void" />
+			<param index="0" name="option" type="int" />
+			<param index="1" name="index" type="int" />
+			<description>
+				Sets the default value index of the [OptionButton] or [CheckBox] with index [param option].
+			</description>
+		</method>
+		<method name="set_option_name">
+			<return type="void" />
+			<param index="0" name="option" type="int" />
+			<param index="1" name="name" type="String" />
+			<description>
+				Sets the name of the [OptionButton] or [CheckBox] with index [param option].
+			</description>
+		</method>
+		<method name="set_option_values">
+			<return type="void" />
+			<param index="0" name="option" type="int" />
+			<param index="1" name="values" type="PackedStringArray" />
+			<description>
+				Sets the option values of the [OptionButton] with index [param option].
+			</description>
+		</method>
 	</methods>
 	<members>
 		<member name="access" type="int" setter="set_access" getter="get_access" enum="FileDialog.Access" default="0">
@@ -76,6 +136,9 @@
 		<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").
 		</member>
+		<member name="option_count" type="int" setter="set_option_count" getter="get_option_count" default="0">
+			The number of additional [OptionButton]s and [CheckBox]es in the dialog.
+		</member>
 		<member name="root_subfolder" type="String" setter="set_root_subfolder" getter="get_root_subfolder" default="&quot;&quot;">
 			If non-empty, the given sub-folder will be "root" of this [FileDialog], i.e. user won't be able to go to its parent directory.
 		</member>

+ 102 - 11
platform/linuxbsd/freedesktop_portal_desktop.cpp

@@ -142,6 +142,54 @@ void FreeDesktopPortalDesktop::append_dbus_string(DBusMessageIter *p_iter, const
 	}
 }
 
+void FreeDesktopPortalDesktop::append_dbus_dict_options(DBusMessageIter *p_iter, const TypedArray<Dictionary> &p_options) {
+	DBusMessageIter dict_iter;
+	DBusMessageIter var_iter;
+	DBusMessageIter arr_iter;
+	const char *choices_key = "choices";
+
+	dbus_message_iter_open_container(p_iter, DBUS_TYPE_DICT_ENTRY, nullptr, &dict_iter);
+	dbus_message_iter_append_basic(&dict_iter, DBUS_TYPE_STRING, &choices_key);
+	dbus_message_iter_open_container(&dict_iter, DBUS_TYPE_VARIANT, "a(ssa(ss)s)", &var_iter);
+	dbus_message_iter_open_container(&var_iter, DBUS_TYPE_ARRAY, "(ss(ss)s)", &arr_iter);
+
+	for (int i = 0; i < p_options.size(); i++) {
+		const Dictionary &item = p_options[i];
+		if (!item.has("name") || !item.has("values") || !item.has("default")) {
+			continue;
+		}
+		const String &name = item["name"];
+		const Vector<String> &options = item["values"];
+		int default_idx = item["default"];
+
+		DBusMessageIter struct_iter;
+		DBusMessageIter array_iter;
+		DBusMessageIter array_struct_iter;
+		dbus_message_iter_open_container(&arr_iter, DBUS_TYPE_STRUCT, nullptr, &struct_iter);
+		append_dbus_string(&struct_iter, name); // ID.
+		append_dbus_string(&struct_iter, name); // User visible name.
+
+		dbus_message_iter_open_container(&struct_iter, DBUS_TYPE_ARRAY, "(ss)", &array_iter);
+		for (int j = 0; j < options.size(); j++) {
+			dbus_message_iter_open_container(&array_iter, DBUS_TYPE_STRUCT, nullptr, &array_struct_iter);
+			append_dbus_string(&array_struct_iter, itos(j));
+			append_dbus_string(&array_struct_iter, options[j]);
+			dbus_message_iter_close_container(&array_iter, &array_struct_iter);
+		}
+		dbus_message_iter_close_container(&struct_iter, &array_iter);
+		if (options.is_empty()) {
+			append_dbus_string(&struct_iter, (default_idx) ? "true" : "false"); // Default selection.
+		} else {
+			append_dbus_string(&struct_iter, itos(default_idx)); // Default selection.
+		}
+
+		dbus_message_iter_close_container(&arr_iter, &struct_iter);
+	}
+	dbus_message_iter_close_container(&var_iter, &arr_iter);
+	dbus_message_iter_close_container(&dict_iter, &var_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) {
 	DBusMessageIter dict_iter;
 	DBusMessageIter var_iter;
@@ -223,7 +271,7 @@ void FreeDesktopPortalDesktop::append_dbus_dict_bool(DBusMessageIter *p_iter, co
 	dbus_message_iter_close_container(p_iter, &dict_iter);
 }
 
-bool FreeDesktopPortalDesktop::file_chooser_parse_response(DBusMessageIter *p_iter, const Vector<String> &p_names, bool &r_cancel, Vector<String> &r_urls, int &r_index) {
+bool FreeDesktopPortalDesktop::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) {
 	ERR_FAIL_COND_V(dbus_message_iter_get_arg_type(p_iter) != DBUS_TYPE_UINT32, false);
 
 	dbus_uint32_t resp_code;
@@ -257,6 +305,34 @@ bool FreeDesktopPortalDesktop::file_chooser_parse_response(DBusMessageIter *p_it
 							String name = String::utf8(value);
 
 							r_index = p_names.find(name);
+							if (!dbus_message_iter_next(&struct_iter)) {
+								break;
+							}
+						}
+					}
+				} else if (strcmp(key, "choices") == 0) { // a(ss) {
+					if (dbus_message_iter_get_arg_type(&var_iter) == DBUS_TYPE_ARRAY) {
+						DBusMessageIter struct_iter;
+						dbus_message_iter_recurse(&var_iter, &struct_iter);
+						while (dbus_message_iter_get_arg_type(&struct_iter) == DBUS_TYPE_STRUCT) {
+							DBusMessageIter opt_iter;
+							dbus_message_iter_recurse(&struct_iter, &opt_iter);
+							const char *opt_key = nullptr;
+							dbus_message_iter_get_basic(&opt_iter, &opt_key);
+							String opt_skey = String::utf8(opt_key);
+
+							dbus_message_iter_next(&opt_iter);
+							const char *opt_val = nullptr;
+							dbus_message_iter_get_basic(&opt_iter, &opt_val);
+							String opt_sval = String::utf8(opt_val);
+							if (opt_sval == "true") {
+								r_options[opt_skey] = true;
+							} else if (opt_sval == "false") {
+								r_options[opt_skey] = false;
+							} else {
+								r_options[opt_skey] = opt_sval.to_int();
+							}
+
 							if (!dbus_message_iter_next(&struct_iter)) {
 								break;
 							}
@@ -285,7 +361,7 @@ bool FreeDesktopPortalDesktop::file_chooser_parse_response(DBusMessageIter *p_it
 	return true;
 }
 
-Error FreeDesktopPortalDesktop::file_dialog_show(DisplayServer::WindowID p_window_id, const String &p_xid, const String &p_title, const String &p_current_directory, const String &p_filename, DisplayServer::FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback) {
+Error FreeDesktopPortalDesktop::file_dialog_show(DisplayServer::WindowID p_window_id, const String &p_xid, const String &p_title, const String &p_current_directory, const String &p_root, const String &p_filename, DisplayServer::FileDialogMode p_mode, const Vector<String> &p_filters, const TypedArray<Dictionary> &p_options, const Callable &p_callback, bool p_options_in_cb) {
 	if (unsupported) {
 		return FAILED;
 	}
@@ -322,6 +398,7 @@ Error FreeDesktopPortalDesktop::file_dialog_show(DisplayServer::WindowID p_windo
 	fd.callback = p_callback;
 	fd.prev_focus = p_window_id;
 	fd.filter_names = filter_names;
+	fd.opt_in_cb = p_options_in_cb;
 
 	CryptoCore::RandomGenerator rng;
 	ERR_FAIL_COND_V_MSG(rng.init(), FAILED, "Failed to initialize random number generator.");
@@ -373,6 +450,8 @@ Error FreeDesktopPortalDesktop::file_dialog_show(DisplayServer::WindowID p_windo
 		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_options(&arr_iter, p_options);
 		append_dbus_dict_string(&arr_iter, "current_folder", p_current_directory, true);
 		if (p_mode == DisplayServer::FILE_DIALOG_MODE_SAVE_FILE) {
 			append_dbus_dict_string(&arr_iter, "current_name", p_filename);
@@ -427,14 +506,25 @@ Error FreeDesktopPortalDesktop::file_dialog_show(DisplayServer::WindowID p_windo
 	return OK;
 }
 
-void FreeDesktopPortalDesktop::_file_dialog_callback(const Callable &p_callable, const Variant &p_status, const Variant &p_list, const Variant &p_index) {
-	Variant ret;
-	Callable::CallError ce;
-	const Variant *args[3] = { &p_status, &p_list, &p_index };
+void FreeDesktopPortalDesktop::_file_dialog_callback(const Callable &p_callable, const Variant &p_status, const Variant &p_list, const Variant &p_index, const Variant &p_options, bool p_opt_in_cb) {
+	if (p_opt_in_cb) {
+		Variant ret;
+		Callable::CallError ce;
+		const Variant *args[4] = { &p_status, &p_list, &p_index, &p_options };
 
-	p_callable.callp(args, 3, ret, ce);
-	if (ce.error != Callable::CallError::CALL_OK) {
-		ERR_PRINT(vformat("Failed to execute file dialogs callback: %s.", Variant::get_callable_error_text(p_callable, args, 3, ce)));
+		p_callable.callp(args, 4, ret, ce);
+		if (ce.error != Callable::CallError::CALL_OK) {
+			ERR_PRINT(vformat("Failed to execute file dialogs callback: %s.", Variant::get_callable_error_text(p_callable, args, 4, ce)));
+		}
+	} else {
+		Variant ret;
+		Callable::CallError ce;
+		const Variant *args[3] = { &p_status, &p_list, &p_index };
+
+		p_callable.callp(args, 3, ret, ce);
+		if (ce.error != Callable::CallError::CALL_OK) {
+			ERR_PRINT(vformat("Failed to execute file dialogs callback: %s.", Variant::get_callable_error_text(p_callable, args, 3, ce)));
+		}
 	}
 }
 
@@ -458,11 +548,12 @@ void FreeDesktopPortalDesktop::_thread_file_dialog_monitor(void *p_ud) {
 								if (dbus_message_iter_init(msg, &iter)) {
 									bool cancel = false;
 									Vector<String> uris;
+									Dictionary options;
 									int index = 0;
-									file_chooser_parse_response(&iter, fd.filter_names, cancel, uris, index);
+									file_chooser_parse_response(&iter, fd.filter_names, cancel, uris, index, options);
 
 									if (fd.callback.is_valid()) {
-										callable_mp(portal, &FreeDesktopPortalDesktop::_file_dialog_callback).call_deferred(fd.callback, !cancel, uris, index);
+										callable_mp(portal, &FreeDesktopPortalDesktop::_file_dialog_callback).call_deferred(fd.callback, !cancel, uris, index, options, fd.opt_in_cb);
 									}
 									if (fd.prev_focus != DisplayServer::INVALID_WINDOW_ID) {
 										callable_mp(DisplayServer::get_singleton(), &DisplayServer::window_move_to_foreground).call_deferred(fd.prev_focus);

+ 5 - 3
platform/linuxbsd/freedesktop_portal_desktop.h

@@ -49,12 +49,13 @@ private:
 	bool read_setting(const char *p_namespace, const char *p_key, int p_type, void *r_value);
 
 	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_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);
+	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);
 
-	void _file_dialog_callback(const Callable &p_callable, const Variant &p_status, const Variant &p_list, const Variant &p_index);
+	void _file_dialog_callback(const Callable &p_callable, const Variant &p_status, const Variant &p_list, const Variant &p_index, const Variant &p_options, bool p_opt_in_cb);
 
 	struct FileDialogData {
 		Vector<String> filter_names;
@@ -62,6 +63,7 @@ private:
 		DisplayServer::WindowID prev_focus = DisplayServer::INVALID_WINDOW_ID;
 		Callable callback;
 		String path;
+		bool opt_in_cb = false;
 	};
 
 	Mutex file_dialog_mutex;
@@ -77,7 +79,7 @@ public:
 
 	bool is_supported() { return !unsupported; }
 
-	Error file_dialog_show(DisplayServer::WindowID p_window_id, const String &p_xid, const String &p_title, const String &p_current_directory, const String &p_filename, DisplayServer::FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback);
+	Error file_dialog_show(DisplayServer::WindowID p_window_id, const String &p_xid, const String &p_title, const String &p_current_directory, const String &p_root, const String &p_filename, DisplayServer::FileDialogMode p_mode, const Vector<String> &p_filters, const TypedArray<Dictionary> &p_options, const Callable &p_callback, bool p_options_in_cb);
 
 	// Retrieve the system's preferred color scheme.
 	// 0: No preference or unknown.

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

@@ -372,7 +372,18 @@ Error DisplayServerX11::file_dialog_show(const String &p_title, const String &p_
 	}
 
 	String xid = vformat("x11:%x", (uint64_t)windows[window_id].x11_window);
-	return portal_desktop->file_dialog_show(last_focused_window, xid, p_title, p_current_directory, p_filename, p_mode, p_filters, p_callback);
+	return portal_desktop->file_dialog_show(last_focused_window, xid, p_title, p_current_directory, String(), p_filename, p_mode, p_filters, TypedArray<Dictionary>(), p_callback, false);
+}
+
+Error DisplayServerX11::file_dialog_with_options_show(const String &p_title, const String &p_current_directory, const String &p_root, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const TypedArray<Dictionary> &p_options, const Callable &p_callback) {
+	WindowID window_id = last_focused_window;
+
+	if (!windows.has(window_id)) {
+		window_id = MAIN_WINDOW_ID;
+	}
+
+	String xid = vformat("x11:%x", (uint64_t)windows[window_id].x11_window);
+	return portal_desktop->file_dialog_show(last_focused_window, xid, p_title, p_current_directory, p_root, p_filename, p_mode, p_filters, p_options, p_callback, true);
 }
 
 #endif

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

@@ -402,6 +402,7 @@ public:
 	virtual bool is_dark_mode() const override;
 
 	virtual Error file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback) override;
+	virtual Error file_dialog_with_options_show(const String &p_title, const String &p_current_directory, const String &p_root, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const TypedArray<Dictionary> &p_options, const Callable &p_callback) override;
 #endif
 
 	virtual void mouse_set_mode(MouseMode p_mode) override;

+ 1 - 0
platform/macos/SCsub

@@ -20,6 +20,7 @@ files = [
     "godot_main_macos.mm",
     "godot_menu_delegate.mm",
     "godot_menu_item.mm",
+    "godot_open_save_delegate.mm",
     "dir_access_macos.mm",
     "tts_macos.mm",
     "joypad_macos.cpp",

+ 3 - 0
platform/macos/display_server_macos.h

@@ -234,6 +234,8 @@ private:
 	int _get_system_menu_count(const NSMenu *p_menu) const;
 	NSMenuItem *_menu_add_item(const String &p_menu_root, const String &p_label, Key p_accel, int p_index, int *r_out);
 
+	Error _file_dialog_with_options_show(const String &p_title, const String &p_current_directory, const String &p_root, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const TypedArray<Dictionary> &p_options, const Callable &p_callback, bool p_options_in_cb);
+
 public:
 	NSMenu *get_dock_menu() const;
 	void menu_callback(id p_sender);
@@ -345,6 +347,7 @@ public:
 	virtual Error dialog_input_text(String p_title, String p_description, String p_partial, const Callable &p_callback) override;
 
 	virtual Error file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback) override;
+	virtual Error file_dialog_with_options_show(const String &p_title, const String &p_current_directory, const String &p_root, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const TypedArray<Dictionary> &p_options, const Callable &p_callback) override;
 
 	virtual void mouse_set_mode(MouseMode p_mode) override;
 	virtual MouseMode mouse_get_mode() const override;

+ 114 - 154
platform/macos/display_server_macos.mm

@@ -34,6 +34,7 @@
 #include "godot_content_view.h"
 #include "godot_menu_delegate.h"
 #include "godot_menu_item.h"
+#include "godot_open_save_delegate.h"
 #include "godot_window.h"
 #include "godot_window_delegate.h"
 #include "key_mapping_macos.h"
@@ -2079,139 +2080,37 @@ Error DisplayServerMacOS::dialog_show(String p_title, String p_description, Vect
 	return OK;
 }
 
-@interface FileDialogDropdown : NSObject {
-	NSSavePanel *dialog;
-	NSMutableArray *allowed_types;
-	int cur_index;
-}
-
-- (instancetype)initWithDialog:(NSSavePanel *)p_dialog fileTypes:(NSMutableArray *)p_allowed_types;
-- (void)popupAction:(id)sender;
-- (int)getIndex;
-
-@end
-
-@implementation FileDialogDropdown
-
-- (int)getIndex {
-	return cur_index;
-}
-
-- (instancetype)initWithDialog:(NSSavePanel *)p_dialog fileTypes:(NSMutableArray *)p_allowed_types {
-	if ((self = [super init])) {
-		dialog = p_dialog;
-		allowed_types = p_allowed_types;
-		cur_index = 0;
-	}
-	return self;
+Error DisplayServerMacOS::file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback) {
+	return _file_dialog_with_options_show(p_title, p_current_directory, String(), p_filename, p_show_hidden, p_mode, p_filters, TypedArray<Dictionary>(), p_callback, false);
 }
 
-- (void)popupAction:(id)sender {
-	NSUInteger index = [sender indexOfSelectedItem];
-	if (index < [allowed_types count]) {
-		[dialog setAllowedFileTypes:[allowed_types objectAtIndex:index]];
-		cur_index = index;
-	} else {
-		[dialog setAllowedFileTypes:@[]];
-		cur_index = -1;
-	}
+Error DisplayServerMacOS::file_dialog_with_options_show(const String &p_title, const String &p_current_directory, const String &p_root, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const TypedArray<Dictionary> &p_options, const Callable &p_callback) {
+	return _file_dialog_with_options_show(p_title, p_current_directory, p_root, p_filename, p_show_hidden, p_mode, p_filters, p_options, p_callback, true);
 }
 
-@end
-
-FileDialogDropdown *_make_accessory_view(NSSavePanel *p_panel, const Vector<String> &p_filters) {
-	NSView *group = [[NSView alloc] initWithFrame:NSZeroRect];
-	group.translatesAutoresizingMaskIntoConstraints = NO;
-
-	NSTextField *label = [NSTextField labelWithString:[NSString stringWithUTF8String:RTR("Format").utf8().get_data()]];
-	label.translatesAutoresizingMaskIntoConstraints = NO;
-	if (@available(macOS 10.14, *)) {
-		label.textColor = NSColor.secondaryLabelColor;
-	}
-	if (@available(macOS 11.10, *)) {
-		label.font = [NSFont systemFontOfSize:[NSFont smallSystemFontSize]];
-	}
-	[group addSubview:label];
-
-	NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:NSZeroRect pullsDown:NO];
-	popup.translatesAutoresizingMaskIntoConstraints = NO;
-
-	NSMutableArray *allowed_types = [[NSMutableArray alloc] init];
-	bool allow_other = false;
-	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();
-			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.strip_edges() == "*.*" || str.strip_edges() == "*") {
-					allow_other = true;
-				} else if (!str.is_empty()) {
-					[type_filters addObject:[NSString stringWithUTF8String:str.replace("*.", "").strip_edges().utf8().get_data()]];
-				}
-			}
-
-			if ([type_filters count] > 0) {
-				NSString *name_str = [NSString stringWithUTF8String:((tokens.size() == 1) ? tokens[0] : vformat("%s (%s)", tokens[1], tokens[0])).strip_edges().utf8().get_data()];
-				[allowed_types addObject:type_filters];
-				[popup addItemWithTitle:name_str];
-			}
-		}
-	}
-	FileDialogDropdown *handler = [[FileDialogDropdown alloc] initWithDialog:p_panel fileTypes:allowed_types];
-	popup.target = handler;
-	popup.action = @selector(popupAction:);
-
-	[group addSubview:popup];
-
-	NSView *view = [[NSView alloc] initWithFrame:NSZeroRect];
-	view.translatesAutoresizingMaskIntoConstraints = NO;
-	[view addSubview:group];
-
-	NSMutableArray *constraints = [NSMutableArray array];
-	[constraints addObject:[popup.topAnchor constraintEqualToAnchor:group.topAnchor constant:10]];
-	[constraints addObject:[label.leadingAnchor constraintEqualToAnchor:group.leadingAnchor constant:10]];
-	[constraints addObject:[popup.leadingAnchor constraintEqualToAnchor:label.trailingAnchor constant:10]];
-	[constraints addObject:[popup.firstBaselineAnchor constraintEqualToAnchor:label.firstBaselineAnchor]];
-	[constraints addObject:[group.trailingAnchor constraintEqualToAnchor:popup.trailingAnchor constant:10]];
-	[constraints addObject:[group.bottomAnchor constraintEqualToAnchor:popup.bottomAnchor constant:10]];
-	[constraints addObject:[group.topAnchor constraintEqualToAnchor:view.topAnchor]];
-	[constraints addObject:[group.centerXAnchor constraintEqualToAnchor:view.centerXAnchor]];
-	[constraints addObject:[view.bottomAnchor constraintEqualToAnchor:group.bottomAnchor]];
-	[NSLayoutConstraint activateConstraints:constraints];
-
-	[p_panel setAllowsOtherFileTypes:allow_other];
-	if ([allowed_types count] > 0) {
-		[p_panel setAccessoryView:view];
-		[p_panel setAllowedFileTypes:[allowed_types objectAtIndex:0]];
-	}
-
-	return handler;
-}
-
-Error DisplayServerMacOS::file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback) {
+Error DisplayServerMacOS::_file_dialog_with_options_show(const String &p_title, const String &p_current_directory, const String &p_root, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const TypedArray<Dictionary> &p_options, const Callable &p_callback, bool p_options_in_cb) {
 	_THREAD_SAFE_METHOD_
 
 	ERR_FAIL_INDEX_V(int(p_mode), FILE_DIALOG_MODE_SAVE_MAX, FAILED);
 
 	NSString *url = [NSString stringWithUTF8String:p_current_directory.utf8().get_data()];
-	FileDialogDropdown *handler = nullptr;
-
 	WindowID prev_focus = last_focused_window;
 
+	GodotOpenSaveDelegate *panel_delegate = [[GodotOpenSaveDelegate alloc] init];
+	if (p_root.length() > 0) {
+		[panel_delegate setRootPath:p_root];
+	}
 	Callable callback = p_callback; // Make a copy for async completion handler.
 	if (p_mode == FILE_DIALOG_MODE_SAVE_FILE) {
 		NSSavePanel *panel = [NSSavePanel savePanel];
 
 		[panel setDirectoryURL:[NSURL fileURLWithPath:url]];
-		handler = _make_accessory_view(panel, p_filters);
+		[panel_delegate makeAccessoryView:panel filters:p_filters options:p_options];
 		[panel setExtensionHidden:YES];
 		[panel setCanSelectHiddenExtension:YES];
 		[panel setCanCreateDirectories:YES];
 		[panel setShowsHiddenFiles:p_show_hidden];
+		[panel setDelegate:panel_delegate];
 		if (p_filename != "") {
 			NSString *fileurl = [NSString stringWithUTF8String:p_filename.utf8().get_data()];
 			[panel setNameFieldStringValue:fileurl];
@@ -2248,30 +2147,60 @@ Error DisplayServerMacOS::file_dialog_show(const String &p_title, const String &
 							  url.parse_utf8([[[panel URL] path] UTF8String]);
 							  files.push_back(url);
 							  if (!callback.is_null()) {
-								  Variant v_result = true;
-								  Variant v_files = files;
-								  Variant v_index = [handler getIndex];
-								  Variant ret;
-								  Callable::CallError ce;
-								  const Variant *args[3] = { &v_result, &v_files, &v_index };
-
-								  callback.callp(args, 3, ret, ce);
-								  if (ce.error != Callable::CallError::CALL_OK) {
-									  ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(callback, args, 3, ce)));
+								  if (p_options_in_cb) {
+									  Variant v_result = true;
+									  Variant v_files = files;
+									  Variant v_index = [panel_delegate getIndex];
+									  Variant v_opt = [panel_delegate getSelection];
+									  Variant ret;
+									  Callable::CallError ce;
+									  const Variant *args[4] = { &v_result, &v_files, &v_index, &v_opt };
+
+									  callback.callp(args, 4, ret, ce);
+									  if (ce.error != Callable::CallError::CALL_OK) {
+										  ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(callback, args, 4, ce)));
+									  }
+								  } else {
+									  Variant v_result = true;
+									  Variant v_files = files;
+									  Variant v_index = [panel_delegate getIndex];
+									  Variant ret;
+									  Callable::CallError ce;
+									  const Variant *args[3] = { &v_result, &v_files, &v_index };
+
+									  callback.callp(args, 3, ret, ce);
+									  if (ce.error != Callable::CallError::CALL_OK) {
+										  ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(callback, args, 3, ce)));
+									  }
 								  }
 							  }
 						  } else {
 							  if (!callback.is_null()) {
-								  Variant v_result = false;
-								  Variant v_files = Vector<String>();
-								  Variant v_index = [handler getIndex];
-								  Variant ret;
-								  Callable::CallError ce;
-								  const Variant *args[3] = { &v_result, &v_files, &v_index };
-
-								  callback.callp(args, 3, ret, ce);
-								  if (ce.error != Callable::CallError::CALL_OK) {
-									  ERR_PRINT(vformat("Failed to execute file dialogs callback: %s.", Variant::get_callable_error_text(callback, args, 3, ce)));
+								  if (p_options_in_cb) {
+									  Variant v_result = false;
+									  Variant v_files = Vector<String>();
+									  Variant v_index = [panel_delegate getIndex];
+									  Variant v_opt = [panel_delegate getSelection];
+									  Variant ret;
+									  Callable::CallError ce;
+									  const Variant *args[4] = { &v_result, &v_files, &v_index, &v_opt };
+
+									  callback.callp(args, 4, ret, ce);
+									  if (ce.error != Callable::CallError::CALL_OK) {
+										  ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(callback, args, 4, ce)));
+									  }
+								  } else {
+									  Variant v_result = false;
+									  Variant v_files = Vector<String>();
+									  Variant v_index = [panel_delegate getIndex];
+									  Variant ret;
+									  Callable::CallError ce;
+									  const Variant *args[3] = { &v_result, &v_files, &v_index };
+
+									  callback.callp(args, 3, ret, ce);
+									  if (ce.error != Callable::CallError::CALL_OK) {
+										  ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(callback, args, 3, ce)));
+									  }
 								  }
 							  }
 						  }
@@ -2283,13 +2212,14 @@ Error DisplayServerMacOS::file_dialog_show(const String &p_title, const String &
 		NSOpenPanel *panel = [NSOpenPanel openPanel];
 
 		[panel setDirectoryURL:[NSURL fileURLWithPath:url]];
-		handler = _make_accessory_view(panel, p_filters);
+		[panel_delegate makeAccessoryView:panel filters:p_filters options:p_options];
 		[panel setExtensionHidden:YES];
 		[panel setCanSelectHiddenExtension:YES];
 		[panel setCanCreateDirectories:YES];
 		[panel setCanChooseFiles:(p_mode != FILE_DIALOG_MODE_OPEN_DIR)];
 		[panel setCanChooseDirectories:(p_mode == FILE_DIALOG_MODE_OPEN_DIR || p_mode == FILE_DIALOG_MODE_OPEN_ANY)];
 		[panel setShowsHiddenFiles:p_show_hidden];
+		[panel setDelegate:panel_delegate];
 		if (p_filename != "") {
 			NSString *fileurl = [NSString stringWithUTF8String:p_filename.utf8().get_data()];
 			[panel setNameFieldStringValue:fileurl];
@@ -2333,30 +2263,60 @@ Error DisplayServerMacOS::file_dialog_show(const String &p_title, const String &
 								  files.push_back(url);
 							  }
 							  if (!callback.is_null()) {
-								  Variant v_result = true;
-								  Variant v_files = files;
-								  Variant v_index = [handler getIndex];
-								  Variant ret;
-								  Callable::CallError ce;
-								  const Variant *args[3] = { &v_result, &v_files, &v_index };
-
-								  callback.callp(args, 3, ret, ce);
-								  if (ce.error != Callable::CallError::CALL_OK) {
-									  ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(callback, args, 3, ce)));
+								  if (p_options_in_cb) {
+									  Variant v_result = true;
+									  Variant v_files = files;
+									  Variant v_index = [panel_delegate getIndex];
+									  Variant v_opt = [panel_delegate getSelection];
+									  Variant ret;
+									  Callable::CallError ce;
+									  const Variant *args[4] = { &v_result, &v_files, &v_index, &v_opt };
+
+									  callback.callp(args, 4, ret, ce);
+									  if (ce.error != Callable::CallError::CALL_OK) {
+										  ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(callback, args, 4, ce)));
+									  }
+								  } else {
+									  Variant v_result = true;
+									  Variant v_files = files;
+									  Variant v_index = [panel_delegate getIndex];
+									  Variant ret;
+									  Callable::CallError ce;
+									  const Variant *args[3] = { &v_result, &v_files, &v_index };
+
+									  callback.callp(args, 3, ret, ce);
+									  if (ce.error != Callable::CallError::CALL_OK) {
+										  ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(callback, args, 3, ce)));
+									  }
 								  }
 							  }
 						  } else {
 							  if (!callback.is_null()) {
-								  Variant v_result = false;
-								  Variant v_files = Vector<String>();
-								  Variant v_index = [handler getIndex];
-								  Variant ret;
-								  Callable::CallError ce;
-								  const Variant *args[3] = { &v_result, &v_files, &v_index };
-
-								  callback.callp(args, 3, ret, ce);
-								  if (ce.error != Callable::CallError::CALL_OK) {
-									  ERR_PRINT(vformat("Failed to execute file dialogs callback: %s.", Variant::get_callable_error_text(callback, args, 3, ce)));
+								  if (p_options_in_cb) {
+									  Variant v_result = false;
+									  Variant v_files = Vector<String>();
+									  Variant v_index = [panel_delegate getIndex];
+									  Variant v_opt = [panel_delegate getSelection];
+									  Variant ret;
+									  Callable::CallError ce;
+									  const Variant *args[4] = { &v_result, &v_files, &v_index, &v_opt };
+
+									  callback.callp(args, 4, ret, ce);
+									  if (ce.error != Callable::CallError::CALL_OK) {
+										  ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(callback, args, 4, ce)));
+									  }
+								  } else {
+									  Variant v_result = false;
+									  Variant v_files = Vector<String>();
+									  Variant v_index = [panel_delegate getIndex];
+									  Variant ret;
+									  Callable::CallError ce;
+									  const Variant *args[3] = { &v_result, &v_files, &v_index };
+
+									  callback.callp(args, 3, ret, ce);
+									  if (ce.error != Callable::CallError::CALL_OK) {
+										  ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(callback, args, 3, ce)));
+									  }
 								  }
 							  }
 						  }

+ 66 - 0
platform/macos/godot_open_save_delegate.h

@@ -0,0 +1,66 @@
+/**************************************************************************/
+/*  godot_open_save_delegate.h                                            */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#ifndef GODOT_OPEN_SAVE_DELEGATE_H
+#define GODOT_OPEN_SAVE_DELEGATE_H
+
+#import <AppKit/AppKit.h>
+#import <Foundation/Foundation.h>
+
+#include "core/templates/hash_map.h"
+#include "core/variant/typed_array.h"
+#include "core/variant/variant.h"
+
+@interface GodotOpenSaveDelegate : NSObject <NSOpenSavePanelDelegate> {
+	NSSavePanel *dialog;
+	NSMutableArray *allowed_types;
+
+	HashMap<int, String> ctr_ids;
+	Dictionary options;
+	int cur_index;
+	int ctr_id;
+
+	String root;
+}
+
+- (void)makeAccessoryView:(NSSavePanel *)p_panel filters:(const Vector<String> &)p_filters options:(const TypedArray<Dictionary> &)p_options;
+- (void)setFileTypes:(NSMutableArray *)p_allowed_types;
+- (void)popupOptionAction:(id)p_sender;
+- (void)popupCheckAction:(id)p_sender;
+- (void)popupFileAction:(id)p_sender;
+- (int)getIndex;
+- (Dictionary)getSelection;
+- (int)setDefaultInt:(const String &)p_name value:(int)p_value;
+- (int)setDefaultBool:(const String &)p_name value:(bool)p_value;
+- (void)setRootPath:(const String &)p_root_path;
+
+@end
+
+#endif // GODOT_OPEN_SAVE_DELEGATE_H

+ 250 - 0
platform/macos/godot_open_save_delegate.mm

@@ -0,0 +1,250 @@
+/**************************************************************************/
+/*  godot_open_save_delegate.mm                                           */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#include "godot_open_save_delegate.h"
+
+@implementation GodotOpenSaveDelegate
+
+- (instancetype)init {
+	self = [super init];
+	if ((self = [super init])) {
+		dialog = nullptr;
+		cur_index = 0;
+		ctr_id = 1;
+		allowed_types = nullptr;
+		root = String();
+	}
+	return self;
+}
+
+- (void)makeAccessoryView:(NSSavePanel *)p_panel filters:(const Vector<String> &)p_filters options:(const TypedArray<Dictionary> &)p_options {
+	dialog = p_panel;
+
+	NSMutableArray *constraints = [NSMutableArray array];
+
+	NSView *base_view = [[NSView alloc] initWithFrame:NSZeroRect];
+	base_view.translatesAutoresizingMaskIntoConstraints = NO;
+
+	NSGridView *view = [NSGridView gridViewWithNumberOfColumns:2 rows:0];
+	view.translatesAutoresizingMaskIntoConstraints = NO;
+	view.columnSpacing = 10;
+	view.rowSpacing = 10;
+	view.rowAlignment = NSGridRowAlignmentLastBaseline;
+
+	int option_count = 0;
+
+	for (int i = 0; i < p_options.size(); i++) {
+		const Dictionary &item = p_options[i];
+		if (!item.has("name") || !item.has("values") || !item.has("default")) {
+			continue;
+		}
+		const String &name = item["name"];
+		const Vector<String> &values = item["values"];
+		int default_idx = item["default"];
+
+		NSTextField *label = [NSTextField labelWithString:[NSString stringWithUTF8String:name.utf8().get_data()]];
+		if (@available(macOS 10.14, *)) {
+			label.textColor = NSColor.secondaryLabelColor;
+		}
+		if (@available(macOS 11.10, *)) {
+			label.font = [NSFont systemFontOfSize:[NSFont smallSystemFontSize]];
+		}
+
+		NSView *popup = nullptr;
+		if (values.is_empty()) {
+			NSButton *popup_check = [NSButton checkboxWithTitle:@"" target:self action:@selector(popupCheckAction:)];
+			int tag = [self setDefaultBool:name value:(bool)default_idx];
+			popup_check.state = (default_idx) ? NSControlStateValueOn : NSControlStateValueOff;
+			popup_check.tag = tag;
+			popup = popup_check;
+		} else {
+			NSPopUpButton *popup_list = [[NSPopUpButton alloc] initWithFrame:NSZeroRect pullsDown:NO];
+			for (int i = 0; i < values.size(); i++) {
+				[popup_list addItemWithTitle:[NSString stringWithUTF8String:values[i].utf8().get_data()]];
+			}
+			int tag = [self setDefaultInt:name value:default_idx];
+			[popup_list selectItemAtIndex:default_idx];
+			popup_list.tag = tag;
+			popup_list.target = self;
+			popup_list.action = @selector(popupOptionAction:);
+			popup = popup_list;
+		}
+
+		[view addRowWithViews:[NSArray arrayWithObjects:label, popup, nil]];
+
+		option_count++;
+	}
+
+	NSMutableArray *new_allowed_types = [[NSMutableArray alloc] init];
+	bool allow_other = false;
+	{
+		NSTextField *label = [NSTextField labelWithString:[NSString stringWithUTF8String:RTR("Format").utf8().get_data()]];
+		if (@available(macOS 10.14, *)) {
+			label.textColor = NSColor.secondaryLabelColor;
+		}
+		if (@available(macOS 11.10, *)) {
+			label.font = [NSFont systemFontOfSize:[NSFont smallSystemFontSize]];
+		}
+
+		NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:NSZeroRect pullsDown:NO];
+		if (p_filters.is_empty()) {
+			[popup addItemWithTitle:@"All Files"];
+		}
+
+		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();
+				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.strip_edges() == "*.*" || str.strip_edges() == "*") {
+						allow_other = true;
+					} else if (!str.is_empty()) {
+						[type_filters addObject:[NSString stringWithUTF8String:str.replace("*.", "").strip_edges().utf8().get_data()]];
+					}
+				}
+
+				if ([type_filters count] > 0) {
+					NSString *name_str = [NSString stringWithUTF8String:((tokens.size() == 1) ? tokens[0] : vformat("%s (%s)", tokens[1], tokens[0])).strip_edges().utf8().get_data()];
+					[new_allowed_types addObject:type_filters];
+					[popup addItemWithTitle:name_str];
+				}
+			}
+		}
+		[self setFileTypes:new_allowed_types];
+		popup.target = self;
+		popup.action = @selector(popupFileAction:);
+
+		[view addRowWithViews:[NSArray arrayWithObjects:label, popup, nil]];
+	}
+
+	[base_view addSubview:view];
+	[constraints addObject:[view.topAnchor constraintEqualToAnchor:base_view.topAnchor constant:10]];
+	[constraints addObject:[base_view.bottomAnchor constraintEqualToAnchor:view.bottomAnchor constant:10]];
+	[constraints addObject:[base_view.centerXAnchor constraintEqualToAnchor:view.centerXAnchor constant:10]];
+	[NSLayoutConstraint activateConstraints:constraints];
+
+	[p_panel setAllowsOtherFileTypes:allow_other];
+	if (option_count > 0 || [new_allowed_types count] > 0) {
+		[p_panel setAccessoryView:base_view];
+	}
+	if ([new_allowed_types count] > 0) {
+		[p_panel setAllowedFileTypes:[new_allowed_types objectAtIndex:0]];
+	}
+}
+
+- (int)getIndex {
+	return cur_index;
+}
+
+- (Dictionary)getSelection {
+	return options;
+}
+
+- (int)setDefaultInt:(const String &)p_name value:(int)p_value {
+	int cid = ctr_id++;
+	options[p_name] = p_value;
+	ctr_ids[cid] = p_name;
+
+	return cid;
+}
+
+- (int)setDefaultBool:(const String &)p_name value:(bool)p_value {
+	int cid = ctr_id++;
+	options[p_name] = p_value;
+	ctr_ids[cid] = p_name;
+
+	return cid;
+}
+
+- (void)setFileTypes:(NSMutableArray *)p_allowed_types {
+	allowed_types = p_allowed_types;
+}
+
+- (instancetype)initWithDialog:(NSSavePanel *)p_dialog {
+	if ((self = [super init])) {
+		dialog = p_dialog;
+		cur_index = 0;
+		ctr_id = 1;
+		allowed_types = nullptr;
+	}
+	return self;
+}
+
+- (void)popupCheckAction:(id)p_sender {
+	NSButton *btn = p_sender;
+	if (btn && ctr_ids.has(btn.tag)) {
+		options[ctr_ids[btn.tag]] = ([btn state] == NSControlStateValueOn);
+	}
+}
+
+- (void)popupOptionAction:(id)p_sender {
+	NSPopUpButton *btn = p_sender;
+	if (btn && ctr_ids.has(btn.tag)) {
+		options[ctr_ids[btn.tag]] = (int)[btn indexOfSelectedItem];
+	}
+}
+
+- (void)popupFileAction:(id)p_sender {
+	NSPopUpButton *btn = p_sender;
+	if (btn) {
+		NSUInteger index = [btn indexOfSelectedItem];
+		if (allowed_types && index < [allowed_types count]) {
+			[dialog setAllowedFileTypes:[allowed_types objectAtIndex:index]];
+			cur_index = index;
+		} else {
+			[dialog setAllowedFileTypes:@[]];
+			cur_index = -1;
+		}
+	}
+}
+
+- (void)setRootPath:(const String &)p_root_path {
+	root = p_root_path;
+}
+
+- (BOOL)panel:(id)sender validateURL:(NSURL *)url error:(NSError *_Nullable *)outError {
+	if (root.is_empty()) {
+		return YES;
+	}
+
+	NSString *ns_path = url.URLByStandardizingPath.URLByResolvingSymlinksInPath.path;
+	String path = String::utf8([ns_path UTF8String]).simplify_path();
+	if (!path.begins_with(root.simplify_path())) {
+		return NO;
+	}
+
+	return YES;
+}
+
+@end

+ 215 - 20
platform/windows/display_server_windows.cpp

@@ -219,7 +219,137 @@ void DisplayServerWindows::tts_stop() {
 	tts->stop();
 }
 
+// Silence warning due to a COM API weirdness.
+#if defined(__GNUC__) && !defined(__clang__)
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wnon-virtual-dtor"
+#endif
+
+class FileDialogEventHandler : public IFileDialogEvents, public IFileDialogControlEvents {
+	LONG ref_count = 1;
+	int ctl_id = 1;
+
+	HashMap<int, String> ctls;
+	Dictionary selected;
+	String root;
+
+public:
+	// IUnknown methods
+	HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppv) {
+		static const QITAB qit[] = {
+			QITABENT(FileDialogEventHandler, IFileDialogEvents),
+			QITABENT(FileDialogEventHandler, IFileDialogControlEvents),
+			{ 0, 0 },
+		};
+		return QISearch(this, qit, riid, ppv);
+	}
+
+	ULONG STDMETHODCALLTYPE AddRef() {
+		return InterlockedIncrement(&ref_count);
+	}
+
+	ULONG STDMETHODCALLTYPE Release() {
+		long ref = InterlockedDecrement(&ref_count);
+		if (!ref) {
+			delete this;
+		}
+		return ref;
+	}
+
+	// IFileDialogEvents methods
+	HRESULT STDMETHODCALLTYPE OnFileOk(IFileDialog *) { return S_OK; };
+	HRESULT STDMETHODCALLTYPE OnFolderChange(IFileDialog *) { return S_OK; };
+
+	HRESULT STDMETHODCALLTYPE OnFolderChanging(IFileDialog *p_pfd, IShellItem *p_item) {
+		if (root.is_empty()) {
+			return S_OK;
+		}
+
+		LPWSTR lpw_path = nullptr;
+		p_item->GetDisplayName(SIGDN_FILESYSPATH, &lpw_path);
+		if (!lpw_path) {
+			return S_FALSE;
+		}
+		String path = String::utf16((const char16_t *)lpw_path).simplify_path();
+		if (!path.begins_with(root.simplify_path())) {
+			return S_FALSE;
+		}
+		return S_OK;
+	}
+
+	HRESULT STDMETHODCALLTYPE OnHelp(IFileDialog *) { return S_OK; };
+	HRESULT STDMETHODCALLTYPE OnSelectionChange(IFileDialog *) { return S_OK; };
+	HRESULT STDMETHODCALLTYPE OnShareViolation(IFileDialog *, IShellItem *, FDE_SHAREVIOLATION_RESPONSE *) { return S_OK; };
+	HRESULT STDMETHODCALLTYPE OnTypeChange(IFileDialog *pfd) { return S_OK; };
+	HRESULT STDMETHODCALLTYPE OnOverwrite(IFileDialog *, IShellItem *, FDE_OVERWRITE_RESPONSE *) { return S_OK; };
+
+	// IFileDialogControlEvents methods
+	HRESULT STDMETHODCALLTYPE OnItemSelected(IFileDialogCustomize *p_pfdc, DWORD p_ctl_id, DWORD p_item_idx) {
+		if (ctls.has(p_ctl_id)) {
+			selected[ctls[p_ctl_id]] = (int)p_item_idx;
+		}
+		return S_OK;
+	}
+
+	HRESULT STDMETHODCALLTYPE OnButtonClicked(IFileDialogCustomize *, DWORD) { return S_OK; };
+	HRESULT STDMETHODCALLTYPE OnCheckButtonToggled(IFileDialogCustomize *p_pfdc, DWORD p_ctl_id, BOOL p_checked) {
+		if (ctls.has(p_ctl_id)) {
+			selected[ctls[p_ctl_id]] = (bool)p_checked;
+		}
+		return S_OK;
+	}
+	HRESULT STDMETHODCALLTYPE OnControlActivating(IFileDialogCustomize *, DWORD) { return S_OK; };
+
+	Dictionary get_selected() {
+		return selected;
+	}
+
+	void set_root(const String &p_root) {
+		root = p_root;
+	}
+
+	void add_option(IFileDialogCustomize *p_pfdc, const String &p_name, const Vector<String> &p_options, int p_default) {
+		int gid = ctl_id++;
+		int cid = ctl_id++;
+
+		if (p_options.size() == 0) {
+			// Add check box.
+			p_pfdc->StartVisualGroup(gid, L"");
+			p_pfdc->AddCheckButton(cid, (LPCWSTR)p_name.utf16().get_data(), p_default);
+			p_pfdc->SetControlState(cid, CDCS_VISIBLE | CDCS_ENABLED);
+			p_pfdc->EndVisualGroup();
+			selected[p_name] = (bool)p_default;
+		} else {
+			// Add combo box.
+			p_pfdc->StartVisualGroup(gid, (LPCWSTR)p_name.utf16().get_data());
+			p_pfdc->AddComboBox(cid);
+			p_pfdc->SetControlState(cid, CDCS_VISIBLE | CDCS_ENABLED);
+			for (int i = 0; i < p_options.size(); i++) {
+				p_pfdc->AddControlItem(cid, i, (LPCWSTR)p_options[i].utf16().get_data());
+			}
+			p_pfdc->SetSelectedControlItem(cid, p_default);
+			p_pfdc->EndVisualGroup();
+			selected[p_name] = p_default;
+		}
+		ctls[cid] = p_name;
+	}
+
+	virtual ~FileDialogEventHandler(){};
+};
+
+#if defined(__GNUC__) && !defined(__clang__)
+#pragma GCC diagnostic pop
+#endif
+
 Error DisplayServerWindows::file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback) {
+	return _file_dialog_with_options_show(p_title, p_current_directory, String(), p_filename, p_show_hidden, p_mode, p_filters, TypedArray<Dictionary>(), p_callback, false);
+}
+
+Error DisplayServerWindows::file_dialog_with_options_show(const String &p_title, const String &p_current_directory, const String &p_root, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const TypedArray<Dictionary> &p_options, const Callable &p_callback) {
+	return _file_dialog_with_options_show(p_title, p_current_directory, p_root, p_filename, p_show_hidden, p_mode, p_filters, p_options, p_callback, true);
+}
+
+Error DisplayServerWindows::_file_dialog_with_options_show(const String &p_title, const String &p_current_directory, const String &p_root, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const TypedArray<Dictionary> &p_options, const Callable &p_callback, bool p_options_in_cb) {
 	_THREAD_SAFE_METHOD_
 
 	ERR_FAIL_INDEX_V(int(p_mode), FILE_DIALOG_MODE_SAVE_MAX, FAILED);
@@ -269,6 +399,31 @@ Error DisplayServerWindows::file_dialog_show(const String &p_title, const String
 		hr = CoCreateInstance(CLSID_FileOpenDialog, nullptr, CLSCTX_INPROC_SERVER, IID_IFileOpenDialog, (void **)&pfd);
 	}
 	if (SUCCEEDED(hr)) {
+		IFileDialogEvents *pfde = nullptr;
+		FileDialogEventHandler *event_handler = new FileDialogEventHandler();
+		hr = event_handler->QueryInterface(IID_PPV_ARGS(&pfde));
+
+		DWORD cookie = 0;
+		hr = pfd->Advise(pfde, &cookie);
+
+		IFileDialogCustomize *pfdc = nullptr;
+		hr = pfd->QueryInterface(IID_PPV_ARGS(&pfdc));
+
+		for (int i = 0; i < p_options.size(); i++) {
+			const Dictionary &item = p_options[i];
+			if (!item.has("name") || !item.has("values") || !item.has("default")) {
+				continue;
+			}
+			const String &name = item["name"];
+			const Vector<String> &options = item["values"];
+			int default_idx = item["default"];
+
+			event_handler->add_option(pfdc, name, options, default_idx);
+		}
+		event_handler->set_root(p_root);
+
+		pfdc->Release();
+
 		DWORD flags;
 		pfd->GetOptions(&flags);
 		if (p_mode == FILE_DIALOG_MODE_OPEN_FILES) {
@@ -306,8 +461,18 @@ Error DisplayServerWindows::file_dialog_show(const String &p_title, const String
 		}
 
 		hr = pfd->Show(windows[window_id].hWnd);
+		pfd->Unadvise(cookie);
+
+		Dictionary options = event_handler->get_selected();
+
+		pfde->Release();
+		event_handler->Release();
+
 		UINT index = 0;
 		pfd->GetFileTypeIndex(&index);
+		if (index > 0) {
+			index = index - 1;
+		}
 
 		if (SUCCEEDED(hr)) {
 			Vector<String> file_names;
@@ -346,30 +511,60 @@ Error DisplayServerWindows::file_dialog_show(const String &p_title, const String
 				}
 			}
 			if (!p_callback.is_null()) {
-				Variant v_result = true;
-				Variant v_files = file_names;
-				Variant v_index = index;
-				Variant ret;
-				Callable::CallError ce;
-				const Variant *args[3] = { &v_result, &v_files, &v_index };
-
-				p_callback.callp(args, 3, ret, ce);
-				if (ce.error != Callable::CallError::CALL_OK) {
-					ERR_PRINT(vformat("Failed to execute file dialogs callback: %s.", Variant::get_callable_error_text(p_callback, args, 3, ce)));
+				if (p_options_in_cb) {
+					Variant v_result = true;
+					Variant v_files = file_names;
+					Variant v_index = index;
+					Variant v_opt = options;
+					Variant ret;
+					Callable::CallError ce;
+					const Variant *args[4] = { &v_result, &v_files, &v_index, &v_opt };
+
+					p_callback.callp(args, 4, ret, ce);
+					if (ce.error != Callable::CallError::CALL_OK) {
+						ERR_PRINT(vformat("Failed to execute file dialogs callback: %s.", Variant::get_callable_error_text(p_callback, args, 4, ce)));
+					}
+				} else {
+					Variant v_result = true;
+					Variant v_files = file_names;
+					Variant v_index = index;
+					Variant ret;
+					Callable::CallError ce;
+					const Variant *args[3] = { &v_result, &v_files, &v_index };
+
+					p_callback.callp(args, 3, ret, ce);
+					if (ce.error != Callable::CallError::CALL_OK) {
+						ERR_PRINT(vformat("Failed to execute file dialogs callback: %s.", Variant::get_callable_error_text(p_callback, args, 3, ce)));
+					}
 				}
 			}
 		} else {
 			if (!p_callback.is_null()) {
-				Variant v_result = false;
-				Variant v_files = Vector<String>();
-				Variant v_index = index;
-				Variant ret;
-				Callable::CallError ce;
-				const Variant *args[3] = { &v_result, &v_files, &v_index };
-
-				p_callback.callp(args, 3, ret, ce);
-				if (ce.error != Callable::CallError::CALL_OK) {
-					ERR_PRINT(vformat("Failed to execute file dialogs callback: %s.", Variant::get_callable_error_text(p_callback, args, 3, ce)));
+				if (p_options_in_cb) {
+					Variant v_result = false;
+					Variant v_files = Vector<String>();
+					Variant v_index = index;
+					Variant v_opt = options;
+					Variant ret;
+					Callable::CallError ce;
+					const Variant *args[4] = { &v_result, &v_files, &v_index, &v_opt };
+
+					p_callback.callp(args, 4, ret, ce);
+					if (ce.error != Callable::CallError::CALL_OK) {
+						ERR_PRINT(vformat("Failed to execute file dialogs callback: %s.", Variant::get_callable_error_text(p_callback, args, 4, ce)));
+					}
+				} else {
+					Variant v_result = false;
+					Variant v_files = Vector<String>();
+					Variant v_index = index;
+					Variant ret;
+					Callable::CallError ce;
+					const Variant *args[3] = { &v_result, &v_files, &v_index };
+
+					p_callback.callp(args, 3, ret, ce);
+					if (ce.error != Callable::CallError::CALL_OK) {
+						ERR_PRINT(vformat("Failed to execute file dialogs callback: %s.", Variant::get_callable_error_text(p_callback, args, 3, ce)));
+					}
 				}
 			}
 		}

+ 3 - 0
platform/windows/display_server_windows.h

@@ -497,6 +497,8 @@ class DisplayServerWindows : public DisplayServer {
 	LRESULT _handle_early_window_message(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
 	Point2i _get_screens_origin() const;
 
+	Error _file_dialog_with_options_show(const String &p_title, const String &p_current_directory, const String &p_root, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const TypedArray<Dictionary> &p_options, const Callable &p_callback, bool p_options_in_cb);
+
 public:
 	LRESULT WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
 	LRESULT MouseProc(int code, WPARAM wParam, LPARAM lParam);
@@ -521,6 +523,7 @@ public:
 	virtual Color get_accent_color() const override;
 
 	virtual Error file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback) override;
+	virtual Error file_dialog_with_options_show(const String &p_title, const String &p_current_directory, const String &p_root, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const TypedArray<Dictionary> &p_options, const Callable &p_callback) override;
 
 	virtual void mouse_set_mode(MouseMode p_mode) override;
 	virtual MouseMode mouse_get_mode() const override;

+ 274 - 8
scene/gui/file_dialog.cpp

@@ -30,9 +30,13 @@
 
 #include "file_dialog.h"
 
+#include "core/config/project_settings.h"
 #include "core/os/keyboard.h"
 #include "core/string/print_string.h"
+#include "scene/gui/check_box.h"
+#include "scene/gui/grid_container.h"
 #include "scene/gui/label.h"
+#include "scene/gui/option_button.h"
 #include "scene/theme/theme_db.h"
 
 FileDialog::GetIconFunc FileDialog::get_icon_func = nullptr;
@@ -56,20 +60,30 @@ void FileDialog::_focus_file_text() {
 }
 
 void FileDialog::popup(const Rect2i &p_rect) {
+	_update_option_controls();
+
 #ifdef TOOLS_ENABLED
 	if (is_part_of_edited_scene()) {
 		ConfirmationDialog::popup(p_rect);
 	}
 #endif
 
-	if (access == ACCESS_FILESYSTEM && DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_NATIVE_DIALOG) && (use_native_dialog || OS::get_singleton()->is_sandboxed())) {
-		DisplayServer::get_singleton()->file_dialog_show(get_title(), dir->get_text(), file->get_text().get_file(), show_hidden_files, DisplayServer::FileDialogMode(mode), filters, callable_mp(this, &FileDialog::_native_dialog_cb));
+	if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_NATIVE_DIALOG) && (use_native_dialog || OS::get_singleton()->is_sandboxed())) {
+		String root;
+		if (access == ACCESS_RESOURCES) {
+			root = ProjectSettings::get_singleton()->get_resource_path();
+		} else if (access == ACCESS_USERDATA) {
+			root = OS::get_singleton()->get_user_data_dir();
+		}
+		DisplayServer::get_singleton()->file_dialog_with_options_show(get_title(), ProjectSettings::get_singleton()->globalize_path(dir->get_text()), root, file->get_text().get_file(), show_hidden_files, DisplayServer::FileDialogMode(mode), filters, _get_options(), callable_mp(this, &FileDialog::_native_dialog_cb));
 	} else {
 		ConfirmationDialog::popup(p_rect);
 	}
 }
 
 void FileDialog::set_visible(bool p_visible) {
+	_update_option_controls();
+
 #ifdef TOOLS_ENABLED
 	if (is_part_of_edited_scene()) {
 		ConfirmationDialog::set_visible(p_visible);
@@ -77,23 +91,52 @@ void FileDialog::set_visible(bool p_visible) {
 	}
 #endif
 
-	if (access == ACCESS_FILESYSTEM && DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_NATIVE_DIALOG) && (use_native_dialog || OS::get_singleton()->is_sandboxed())) {
+	if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_NATIVE_DIALOG) && (use_native_dialog || OS::get_singleton()->is_sandboxed())) {
 		if (p_visible) {
-			DisplayServer::get_singleton()->file_dialog_show(get_title(), dir->get_text(), file->get_text().get_file(), show_hidden_files, DisplayServer::FileDialogMode(mode), filters, callable_mp(this, &FileDialog::_native_dialog_cb));
+			String root;
+			if (access == ACCESS_RESOURCES) {
+				root = ProjectSettings::get_singleton()->get_resource_path();
+			} else if (access == ACCESS_USERDATA) {
+				root = OS::get_singleton()->get_user_data_dir();
+			}
+			DisplayServer::get_singleton()->file_dialog_with_options_show(get_title(), ProjectSettings::get_singleton()->globalize_path(dir->get_text()), root, file->get_text().get_file(), show_hidden_files, DisplayServer::FileDialogMode(mode), filters, _get_options(), callable_mp(this, &FileDialog::_native_dialog_cb));
 		}
 	} else {
 		ConfirmationDialog::set_visible(p_visible);
 	}
 }
 
-void FileDialog::_native_dialog_cb(bool p_ok, const Vector<String> &p_files, int p_filter) {
+void FileDialog::_native_dialog_cb(bool p_ok, const Vector<String> &p_files, int p_filter, const Dictionary &p_selected_options) {
 	if (p_ok) {
 		if (p_files.size() > 0) {
-			const String &f = p_files[0];
+			Vector<String> files = p_files;
+			if (access != ACCESS_FILESYSTEM) {
+				for (String &file_name : files) {
+					file_name = ProjectSettings::get_singleton()->localize_path(file_name);
+				}
+			}
+			String f = files[0];
 			if (mode == FILE_MODE_OPEN_FILES) {
-				emit_signal(SNAME("files_selected"), p_files);
+				emit_signal(SNAME("files_selected"), files);
 			} else {
 				if (mode == FILE_MODE_SAVE_FILE) {
+					if (p_filter >= 0 && p_filter < filters.size()) {
+						bool valid = false;
+						String flt = filters[p_filter].get_slice(";", 0);
+						int filter_slice_count = flt.get_slice_count(",");
+						for (int j = 0; j < filter_slice_count; j++) {
+							String str = (flt.get_slice(",", j).strip_edges());
+							if (f.match(str)) {
+								valid = true;
+								break;
+							}
+						}
+
+						if (!valid && filter_slice_count > 0) {
+							String str = (flt.get_slice(",", 0).strip_edges());
+							f += str.substr(1, str.length() - 1);
+						}
+					}
 					emit_signal(SNAME("file_selected"), f);
 				} else if ((mode == FILE_MODE_OPEN_ANY || mode == FILE_MODE_OPEN_FILE) && dir_access->file_exists(f)) {
 					emit_signal(SNAME("file_selected"), f);
@@ -103,7 +146,8 @@ void FileDialog::_native_dialog_cb(bool p_ok, const Vector<String> &p_files, int
 			}
 			file->set_text(f);
 			dir->set_text(f.get_base_dir());
-			_filter_selected(p_filter);
+			selected_options = p_selected_options;
+			filter->select(p_filter);
 		}
 	} else {
 		file->set_text("");
@@ -1007,6 +1051,212 @@ void FileDialog::_update_drives(bool p_select) {
 
 bool FileDialog::default_show_hidden_files = false;
 
+TypedArray<Dictionary> FileDialog::_get_options() const {
+	TypedArray<Dictionary> out;
+	for (const FileDialog::Option &opt : options) {
+		Dictionary dict;
+		dict["name"] = opt.name;
+		dict["values"] = opt.values;
+		dict["default"] = (int)selected_options.get(opt.name, opt.default_idx);
+		out.push_back(dict);
+	}
+	return out;
+}
+
+void FileDialog::_option_changed_checkbox_toggled(bool p_pressed, const String &p_name) {
+	if (selected_options.has(p_name)) {
+		selected_options[p_name] = p_pressed;
+	}
+}
+
+void FileDialog::_option_changed_item_selected(int p_idx, const String &p_name) {
+	if (selected_options.has(p_name)) {
+		selected_options[p_name] = p_idx;
+	}
+}
+
+void FileDialog::_update_option_controls() {
+	if (!options_dirty) {
+		return;
+	}
+	options_dirty = false;
+
+	while (grid_options->get_child_count(false) > 0) {
+		Node *child = grid_options->get_child(0);
+		grid_options->remove_child(child);
+		child->queue_free();
+	}
+	selected_options.clear();
+
+	for (const FileDialog::Option &opt : options) {
+		Label *lbl = memnew(Label);
+		lbl->set_text(opt.name);
+		grid_options->add_child(lbl);
+		if (opt.values.is_empty()) {
+			CheckBox *cb = memnew(CheckBox);
+			cb->set_pressed(opt.default_idx);
+			grid_options->add_child(cb);
+			cb->connect("toggled", callable_mp(this, &FileDialog::_option_changed_checkbox_toggled).bind(opt.name));
+			selected_options[opt.name] = (bool)opt.default_idx;
+		} else {
+			OptionButton *ob = memnew(OptionButton);
+			for (const String &val : opt.values) {
+				ob->add_item(val);
+			}
+			ob->select(opt.default_idx);
+			grid_options->add_child(ob);
+			ob->connect("item_selected", callable_mp(this, &FileDialog::_option_changed_item_selected).bind(opt.name));
+			selected_options[opt.name] = opt.default_idx;
+		}
+	}
+}
+
+Dictionary FileDialog::get_selected_options() const {
+	return selected_options;
+}
+
+String FileDialog::get_option_name(int p_option) const {
+	ERR_FAIL_INDEX_V(p_option, options.size(), String());
+	return options[p_option].name;
+}
+
+Vector<String> FileDialog::get_option_values(int p_option) const {
+	ERR_FAIL_INDEX_V(p_option, options.size(), Vector<String>());
+	return options[p_option].values;
+}
+
+int FileDialog::get_option_default(int p_option) const {
+	ERR_FAIL_INDEX_V(p_option, options.size(), -1);
+	return options[p_option].default_idx;
+}
+
+void FileDialog::set_option_name(int p_option, const String &p_name) {
+	if (p_option < 0) {
+		p_option += get_option_count();
+	}
+	ERR_FAIL_INDEX(p_option, options.size());
+	options.write[p_option].name = p_name;
+	options_dirty = true;
+	if (is_visible()) {
+		_update_option_controls();
+	}
+}
+
+void FileDialog::set_option_values(int p_option, const Vector<String> &p_values) {
+	if (p_option < 0) {
+		p_option += get_option_count();
+	}
+	ERR_FAIL_INDEX(p_option, options.size());
+	options.write[p_option].values = p_values;
+	if (p_values.is_empty()) {
+		options.write[p_option].default_idx = CLAMP(options[p_option].default_idx, 0, 1);
+	} else {
+		options.write[p_option].default_idx = CLAMP(options[p_option].default_idx, 0, options[p_option].values.size() - 1);
+	}
+	options_dirty = true;
+	if (is_visible()) {
+		_update_option_controls();
+	}
+}
+
+void FileDialog::set_option_default(int p_option, int p_index) {
+	if (p_option < 0) {
+		p_option += get_option_count();
+	}
+	ERR_FAIL_INDEX(p_option, options.size());
+	if (options[p_option].values.is_empty()) {
+		options.write[p_option].default_idx = CLAMP(p_index, 0, 1);
+	} else {
+		options.write[p_option].default_idx = CLAMP(p_index, 0, options[p_option].values.size() - 1);
+	}
+	options_dirty = true;
+	if (is_visible()) {
+		_update_option_controls();
+	}
+}
+
+void FileDialog::add_option(const String &p_name, const Vector<String> &p_values, int p_index) {
+	Option opt;
+	opt.name = p_name;
+	opt.values = p_values;
+	if (opt.values.is_empty()) {
+		opt.default_idx = CLAMP(p_index, 0, 1);
+	} else {
+		opt.default_idx = CLAMP(p_index, 0, opt.values.size() - 1);
+	}
+	options.push_back(opt);
+	options_dirty = true;
+	if (is_visible()) {
+		_update_option_controls();
+	}
+}
+
+void FileDialog::set_option_count(int p_count) {
+	ERR_FAIL_COND(p_count < 0);
+	int prev_size = options.size();
+
+	if (prev_size == p_count) {
+		return;
+	}
+	options.resize(p_count);
+
+	options_dirty = true;
+	notify_property_list_changed();
+	if (is_visible()) {
+		_update_option_controls();
+	}
+}
+
+int FileDialog::get_option_count() const {
+	return options.size();
+}
+
+bool FileDialog::_set(const StringName &p_name, const Variant &p_value) {
+	Vector<String> components = String(p_name).split("/", true, 2);
+	if (components.size() >= 2 && components[0].begins_with("option_") && components[0].trim_prefix("option_").is_valid_int()) {
+		int item_index = components[0].trim_prefix("option_").to_int();
+		String property = components[1];
+		if (property == "name") {
+			set_option_name(item_index, p_value);
+			return true;
+		} else if (property == "values") {
+			set_option_values(item_index, p_value);
+			return true;
+		} else if (property == "default") {
+			set_option_default(item_index, p_value);
+			return true;
+		}
+	}
+	return false;
+}
+
+bool FileDialog::_get(const StringName &p_name, Variant &r_ret) const {
+	Vector<String> components = String(p_name).split("/", true, 2);
+	if (components.size() >= 2 && components[0].begins_with("option_") && components[0].trim_prefix("option_").is_valid_int()) {
+		int item_index = components[0].trim_prefix("option_").to_int();
+		String property = components[1];
+		if (property == "name") {
+			r_ret = get_option_name(item_index);
+			return true;
+		} else if (property == "values") {
+			r_ret = get_option_values(item_index);
+			return true;
+		} else if (property == "default") {
+			r_ret = get_option_default(item_index);
+			return true;
+		}
+	}
+	return false;
+}
+
+void FileDialog::_get_property_list(List<PropertyInfo> *p_list) const {
+	for (int i = 0; i < options.size(); i++) {
+		p_list->push_back(PropertyInfo(Variant::STRING, vformat("option_%d/name", i)));
+		p_list->push_back(PropertyInfo(Variant::PACKED_STRING_ARRAY, vformat("option_%d/values", i)));
+		p_list->push_back(PropertyInfo(Variant::INT, vformat("option_%d/default", i)));
+	}
+}
+
 void FileDialog::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("_cancel_pressed"), &FileDialog::_cancel_pressed);
 
@@ -1014,6 +1264,16 @@ void FileDialog::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("add_filter", "filter", "description"), &FileDialog::add_filter, DEFVAL(""));
 	ClassDB::bind_method(D_METHOD("set_filters", "filters"), &FileDialog::set_filters);
 	ClassDB::bind_method(D_METHOD("get_filters"), &FileDialog::get_filters);
+	ClassDB::bind_method(D_METHOD("get_option_name", "option"), &FileDialog::get_option_name);
+	ClassDB::bind_method(D_METHOD("get_option_values", "option"), &FileDialog::get_option_values);
+	ClassDB::bind_method(D_METHOD("get_option_default", "option"), &FileDialog::get_option_default);
+	ClassDB::bind_method(D_METHOD("set_option_name", "option", "name"), &FileDialog::set_option_name);
+	ClassDB::bind_method(D_METHOD("set_option_values", "option", "values"), &FileDialog::set_option_values);
+	ClassDB::bind_method(D_METHOD("set_option_default", "option", "index"), &FileDialog::set_option_default);
+	ClassDB::bind_method(D_METHOD("set_option_count", "count"), &FileDialog::set_option_count);
+	ClassDB::bind_method(D_METHOD("get_option_count"), &FileDialog::get_option_count);
+	ClassDB::bind_method(D_METHOD("add_option", "name", "values", "index"), &FileDialog::add_option);
+	ClassDB::bind_method(D_METHOD("get_selected_options"), &FileDialog::get_selected_options);
 	ClassDB::bind_method(D_METHOD("get_current_dir"), &FileDialog::get_current_dir);
 	ClassDB::bind_method(D_METHOD("get_current_file"), &FileDialog::get_current_file);
 	ClassDB::bind_method(D_METHOD("get_current_path"), &FileDialog::get_current_path);
@@ -1043,6 +1303,7 @@ void FileDialog::_bind_methods() {
 	ADD_PROPERTY(PropertyInfo(Variant::INT, "access", PROPERTY_HINT_ENUM, "Resources,User Data,File System"), "set_access", "get_access");
 	ADD_PROPERTY(PropertyInfo(Variant::STRING, "root_subfolder"), "set_root_subfolder", "get_root_subfolder");
 	ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "filters"), "set_filters", "get_filters");
+	ADD_ARRAY_COUNT("Options", "option_count", "set_option_count", "get_option_count", "option_");
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "show_hidden_files"), "set_show_hidden_files", "is_showing_hidden_files");
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "use_native_dialog"), "set_use_native_dialog", "get_use_native_dialog");
 	ADD_PROPERTY(PropertyInfo(Variant::STRING, "current_dir", PROPERTY_HINT_DIR, "", PROPERTY_USAGE_NONE), "set_current_dir", "get_current_dir");
@@ -1195,6 +1456,11 @@ FileDialog::FileDialog() {
 	file_box->add_child(filter);
 	vbox->add_child(file_box);
 
+	grid_options = memnew(GridContainer);
+	grid_options->set_h_size_flags(Control::SIZE_SHRINK_CENTER);
+	grid_options->set_columns(2);
+	vbox->add_child(grid_options);
+
 	dir_access = DirAccess::create(DirAccess::ACCESS_RESOURCES);
 	_update_drives();
 

+ 35 - 1
scene/gui/file_dialog.h

@@ -38,6 +38,8 @@
 #include "scene/gui/option_button.h"
 #include "scene/gui/tree.h"
 
+class GridContainer;
+
 class FileDialog : public ConfirmationDialog {
 	GDCLASS(FileDialog, ConfirmationDialog);
 
@@ -70,6 +72,7 @@ private:
 	Button *makedir = nullptr;
 	Access access = ACCESS_RESOURCES;
 	VBoxContainer *vbox = nullptr;
+	GridContainer *grid_options = nullptr;
 	FileMode mode;
 	LineEdit *dir = nullptr;
 	HBoxContainer *drives_container = nullptr;
@@ -128,6 +131,15 @@ private:
 		Color icon_pressed_color;
 	} theme_cache;
 
+	struct Option {
+		String name;
+		Vector<String> values;
+		int default_idx = 0;
+	};
+	Vector<Option> options;
+	Dictionary selected_options;
+	bool options_dirty = false;
+
 	void update_dir();
 	void update_file_name();
 	void update_file_list();
@@ -159,15 +171,23 @@ private:
 
 	virtual void shortcut_input(const Ref<InputEvent> &p_event) override;
 
-	void _native_dialog_cb(bool p_ok, const Vector<String> &p_files, int p_filter);
+	void _native_dialog_cb(bool p_ok, const Vector<String> &p_files, int p_filter, const Dictionary &p_selected_options);
 
 	bool _is_open_should_be_disabled();
 
+	TypedArray<Dictionary> _get_options() const;
+	void _update_option_controls();
+	void _option_changed_checkbox_toggled(bool p_pressed, const String &p_name);
+	void _option_changed_item_selected(int p_idx, const String &p_name);
+
 	virtual void _post_popup() override;
 
 protected:
 	void _validate_property(PropertyInfo &p_property) const;
 	void _notification(int p_what);
+	bool _set(const StringName &p_name, const Variant &p_value);
+	bool _get(const StringName &p_name, Variant &r_ret) const;
+	void _get_property_list(List<PropertyInfo> *p_list) const;
 	static void _bind_methods();
 
 public:
@@ -190,6 +210,20 @@ public:
 	void set_current_file(const String &p_file);
 	void set_current_path(const String &p_path);
 
+	String get_option_name(int p_option) const;
+	Vector<String> get_option_values(int p_option) const;
+	int get_option_default(int p_option) const;
+	void set_option_name(int p_option, const String &p_name);
+	void set_option_values(int p_option, const Vector<String> &p_values);
+	void set_option_default(int p_option, int p_index);
+
+	void add_option(const String &p_name, const Vector<String> &p_values, int p_index);
+
+	void set_option_count(int p_count);
+	int get_option_count() const;
+
+	Dictionary get_selected_options() const;
+
 	void set_root_subfolder(const String &p_root);
 	String get_root_subfolder() const;
 

+ 6 - 0
servers/display_server.cpp

@@ -532,6 +532,11 @@ Error DisplayServer::file_dialog_show(const String &p_title, const String &p_cur
 	return OK;
 }
 
+Error DisplayServer::file_dialog_with_options_show(const String &p_title, const String &p_current_directory, const String &p_root, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const TypedArray<Dictionary> &p_options, const Callable &p_callback) {
+	WARN_PRINT("Native dialogs not supported by this display server.");
+	return OK;
+}
+
 int DisplayServer::keyboard_get_layout_count() const {
 	return 0;
 }
@@ -804,6 +809,7 @@ void DisplayServer::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("dialog_input_text", "title", "description", "existing_text", "callback"), &DisplayServer::dialog_input_text);
 
 	ClassDB::bind_method(D_METHOD("file_dialog_show", "title", "current_directory", "filename", "show_hidden", "mode", "filters", "callback"), &DisplayServer::file_dialog_show);
+	ClassDB::bind_method(D_METHOD("file_dialog_with_options_show", "title", "current_directory", "root", "filename", "show_hidden", "mode", "filters", "options", "callback"), &DisplayServer::file_dialog_with_options_show);
 
 	ClassDB::bind_method(D_METHOD("keyboard_get_layout_count"), &DisplayServer::keyboard_get_layout_count);
 	ClassDB::bind_method(D_METHOD("keyboard_get_current_layout"), &DisplayServer::keyboard_get_current_layout);

+ 1 - 0
servers/display_server.h

@@ -514,6 +514,7 @@ public:
 		FILE_DIALOG_MODE_SAVE_MAX
 	};
 	virtual Error file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback);
+	virtual Error file_dialog_with_options_show(const String &p_title, const String &p_current_directory, const String &p_root, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const TypedArray<Dictionary> &p_options, const Callable &p_callback);
 
 	virtual int keyboard_get_layout_count() const;
 	virtual int keyboard_get_current_layout() const;