[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).
This commit is contained in:
bruvzg 2023-10-13 12:37:46 +03:00
parent 74c32faa78
commit a8f521bcad
No known key found for this signature in database
GPG Key ID: 7960FCF39844EC38
17 changed files with 1175 additions and 198 deletions

View File

@ -119,9 +119,33 @@
<param index="6" name="callback" type="Callable" /> <param index="6" name="callback" type="Callable" />
<description> <description>
Displays OS native dialog for selecting files or directories in the file system. Displays OS native dialog for selecting files or directories in the file system.
Callbacks have the following arguments: [code]bool status, PackedStringArray selected_paths, int selected_filter_index[/code]. 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. [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] This method is implemented on 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] [param current_directory] might be ignored.
[b]Note:[/b] On Linux, [param show_hidden] is 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, native file dialogs have no title.

View File

@ -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)". 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> </description>
</method> </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"> <method name="clear_filters">
<return type="void" /> <return type="void" />
<description> <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. [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> </description>
</method> </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"> <method name="get_vbox">
<return type="VBoxContainer" /> <return type="VBoxContainer" />
<description> <description>
@ -51,6 +87,30 @@
Invalidate and update the current dialog content list. Invalidate and update the current dialog content list.
</description> </description>
</method> </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> </methods>
<members> <members>
<member name="access" type="int" setter="set_access" getter="get_access" enum="FileDialog.Access" default="0"> <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"> <member name="mode_overrides_title" type="bool" setter="set_mode_overrides_title" getter="is_mode_overriding_title" default="true">
If [code]true[/code], changing the [member file_mode] property will set the window title accordingly (e.g. setting [member file_mode] to [constant FILE_MODE_OPEN_FILE] will change the window title to "Open a File"). If [code]true[/code], changing the [member file_mode] property will set the window title accordingly (e.g. setting [member file_mode] to [constant FILE_MODE_OPEN_FILE] will change the window title to "Open a File").
</member> </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;"> <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. 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> </member>

View File

@ -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) { 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 dict_iter;
DBusMessageIter var_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); 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); ERR_FAIL_COND_V(dbus_message_iter_get_arg_type(p_iter) != DBUS_TYPE_UINT32, false);
dbus_uint32_t resp_code; dbus_uint32_t resp_code;
@ -257,6 +305,34 @@ bool FreeDesktopPortalDesktop::file_chooser_parse_response(DBusMessageIter *p_it
String name = String::utf8(value); String name = String::utf8(value);
r_index = p_names.find(name); 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)) { if (!dbus_message_iter_next(&struct_iter)) {
break; break;
} }
@ -285,7 +361,7 @@ bool FreeDesktopPortalDesktop::file_chooser_parse_response(DBusMessageIter *p_it
return true; 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) { if (unsupported) {
return FAILED; return FAILED;
} }
@ -322,6 +398,7 @@ Error FreeDesktopPortalDesktop::file_dialog_show(DisplayServer::WindowID p_windo
fd.callback = p_callback; fd.callback = p_callback;
fd.prev_focus = p_window_id; fd.prev_focus = p_window_id;
fd.filter_names = filter_names; fd.filter_names = filter_names;
fd.opt_in_cb = p_options_in_cb;
CryptoCore::RandomGenerator rng; CryptoCore::RandomGenerator rng;
ERR_FAIL_COND_V_MSG(rng.init(), FAILED, "Failed to initialize random number generator."); 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, "multiple", p_mode == DisplayServer::FILE_DIALOG_MODE_OPEN_FILES);
append_dbus_dict_bool(&arr_iter, "directory", p_mode == DisplayServer::FILE_DIALOG_MODE_OPEN_DIR); append_dbus_dict_bool(&arr_iter, "directory", p_mode == DisplayServer::FILE_DIALOG_MODE_OPEN_DIR);
append_dbus_dict_filters(&arr_iter, filter_names, filter_exts); append_dbus_dict_filters(&arr_iter, filter_names, filter_exts);
append_dbus_dict_options(&arr_iter, p_options);
append_dbus_dict_string(&arr_iter, "current_folder", p_current_directory, true); append_dbus_dict_string(&arr_iter, "current_folder", p_current_directory, true);
if (p_mode == DisplayServer::FILE_DIALOG_MODE_SAVE_FILE) { if (p_mode == DisplayServer::FILE_DIALOG_MODE_SAVE_FILE) {
append_dbus_dict_string(&arr_iter, "current_name", p_filename); append_dbus_dict_string(&arr_iter, "current_name", p_filename);
@ -427,7 +506,17 @@ Error FreeDesktopPortalDesktop::file_dialog_show(DisplayServer::WindowID p_windo
return OK; return OK;
} }
void FreeDesktopPortalDesktop::_file_dialog_callback(const Callable &p_callable, const Variant &p_status, const Variant &p_list, const Variant &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, 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; Variant ret;
Callable::CallError ce; Callable::CallError ce;
const Variant *args[3] = { &p_status, &p_list, &p_index }; const Variant *args[3] = { &p_status, &p_list, &p_index };
@ -437,6 +526,7 @@ void FreeDesktopPortalDesktop::_file_dialog_callback(const Callable &p_callable,
ERR_PRINT(vformat("Failed to execute file dialogs callback: %s.", Variant::get_callable_error_text(p_callable, args, 3, ce))); ERR_PRINT(vformat("Failed to execute file dialogs callback: %s.", Variant::get_callable_error_text(p_callable, args, 3, ce)));
} }
} }
}
void FreeDesktopPortalDesktop::_thread_file_dialog_monitor(void *p_ud) { void FreeDesktopPortalDesktop::_thread_file_dialog_monitor(void *p_ud) {
FreeDesktopPortalDesktop *portal = (FreeDesktopPortalDesktop *)p_ud; FreeDesktopPortalDesktop *portal = (FreeDesktopPortalDesktop *)p_ud;
@ -458,11 +548,12 @@ void FreeDesktopPortalDesktop::_thread_file_dialog_monitor(void *p_ud) {
if (dbus_message_iter_init(msg, &iter)) { if (dbus_message_iter_init(msg, &iter)) {
bool cancel = false; bool cancel = false;
Vector<String> uris; Vector<String> uris;
Dictionary options;
int index = 0; 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()) { 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) { if (fd.prev_focus != DisplayServer::INVALID_WINDOW_ID) {
callable_mp(DisplayServer::get_singleton(), &DisplayServer::window_move_to_foreground).call_deferred(fd.prev_focus); callable_mp(DisplayServer::get_singleton(), &DisplayServer::window_move_to_foreground).call_deferred(fd.prev_focus);

View File

@ -49,12 +49,13 @@ private:
bool read_setting(const char *p_namespace, const char *p_key, int p_type, void *r_value); 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_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);
static void append_dbus_dict_string(DBusMessageIter *p_iter, const String &p_key, const String &p_value, bool p_as_byte_array = false); static void append_dbus_dict_string(DBusMessageIter *p_iter, const String &p_key, const String &p_value, bool p_as_byte_array = false);
static void append_dbus_dict_bool(DBusMessageIter *p_iter, const String &p_key, bool p_value); static void append_dbus_dict_bool(DBusMessageIter *p_iter, const String &p_key, bool p_value);
static bool file_chooser_parse_response(DBusMessageIter *p_iter, const Vector<String> &p_names, 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 { struct FileDialogData {
Vector<String> filter_names; Vector<String> filter_names;
@ -62,6 +63,7 @@ private:
DisplayServer::WindowID prev_focus = DisplayServer::INVALID_WINDOW_ID; DisplayServer::WindowID prev_focus = DisplayServer::INVALID_WINDOW_ID;
Callable callback; Callable callback;
String path; String path;
bool opt_in_cb = false;
}; };
Mutex file_dialog_mutex; Mutex file_dialog_mutex;
@ -77,7 +79,7 @@ public:
bool is_supported() { return !unsupported; } 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. // Retrieve the system's preferred color scheme.
// 0: No preference or unknown. // 0: No preference or unknown.

View File

@ -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); 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 #endif

View File

@ -402,6 +402,7 @@ public:
virtual bool is_dark_mode() const override; 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_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 #endif
virtual void mouse_set_mode(MouseMode p_mode) override; virtual void mouse_set_mode(MouseMode p_mode) override;

View File

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

View File

@ -234,6 +234,8 @@ private:
int _get_system_menu_count(const NSMenu *p_menu) const; 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); 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: public:
NSMenu *get_dock_menu() const; NSMenu *get_dock_menu() const;
void menu_callback(id p_sender); 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 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_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 void mouse_set_mode(MouseMode p_mode) override;
virtual MouseMode mouse_get_mode() const override; virtual MouseMode mouse_get_mode() const override;

View File

@ -34,6 +34,7 @@
#include "godot_content_view.h" #include "godot_content_view.h"
#include "godot_menu_delegate.h" #include "godot_menu_delegate.h"
#include "godot_menu_item.h" #include "godot_menu_item.h"
#include "godot_open_save_delegate.h"
#include "godot_window.h" #include "godot_window.h"
#include "godot_window_delegate.h" #include "godot_window_delegate.h"
#include "key_mapping_macos.h" #include "key_mapping_macos.h"
@ -2079,139 +2080,37 @@ Error DisplayServerMacOS::dialog_show(String p_title, String p_description, Vect
return OK; 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;
}
- (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;
}
}
@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_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 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);
}
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_ _THREAD_SAFE_METHOD_
ERR_FAIL_INDEX_V(int(p_mode), FILE_DIALOG_MODE_SAVE_MAX, FAILED); ERR_FAIL_INDEX_V(int(p_mode), FILE_DIALOG_MODE_SAVE_MAX, FAILED);
NSString *url = [NSString stringWithUTF8String:p_current_directory.utf8().get_data()]; NSString *url = [NSString stringWithUTF8String:p_current_directory.utf8().get_data()];
FileDialogDropdown *handler = nullptr;
WindowID prev_focus = last_focused_window; 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. Callable callback = p_callback; // Make a copy for async completion handler.
if (p_mode == FILE_DIALOG_MODE_SAVE_FILE) { if (p_mode == FILE_DIALOG_MODE_SAVE_FILE) {
NSSavePanel *panel = [NSSavePanel savePanel]; NSSavePanel *panel = [NSSavePanel savePanel];
[panel setDirectoryURL:[NSURL fileURLWithPath:url]]; [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 setExtensionHidden:YES];
[panel setCanSelectHiddenExtension:YES]; [panel setCanSelectHiddenExtension:YES];
[panel setCanCreateDirectories:YES]; [panel setCanCreateDirectories:YES];
[panel setShowsHiddenFiles:p_show_hidden]; [panel setShowsHiddenFiles:p_show_hidden];
[panel setDelegate:panel_delegate];
if (p_filename != "") { if (p_filename != "") {
NSString *fileurl = [NSString stringWithUTF8String:p_filename.utf8().get_data()]; NSString *fileurl = [NSString stringWithUTF8String:p_filename.utf8().get_data()];
[panel setNameFieldStringValue:fileurl]; [panel setNameFieldStringValue:fileurl];
@ -2248,9 +2147,23 @@ Error DisplayServerMacOS::file_dialog_show(const String &p_title, const String &
url.parse_utf8([[[panel URL] path] UTF8String]); url.parse_utf8([[[panel URL] path] UTF8String]);
files.push_back(url); files.push_back(url);
if (!callback.is_null()) { if (!callback.is_null()) {
if (p_options_in_cb) {
Variant v_result = true; Variant v_result = true;
Variant v_files = files; Variant v_files = files;
Variant v_index = [handler getIndex]; 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; Variant ret;
Callable::CallError ce; Callable::CallError ce;
const Variant *args[3] = { &v_result, &v_files, &v_index }; const Variant *args[3] = { &v_result, &v_files, &v_index };
@ -2260,18 +2173,34 @@ Error DisplayServerMacOS::file_dialog_show(const String &p_title, const String &
ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(callback, args, 3, ce))); ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(callback, args, 3, ce)));
} }
} }
}
} else { } else {
if (!callback.is_null()) { if (!callback.is_null()) {
if (p_options_in_cb) {
Variant v_result = false; Variant v_result = false;
Variant v_files = Vector<String>(); Variant v_files = Vector<String>();
Variant v_index = [handler getIndex]; 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; Variant ret;
Callable::CallError ce; Callable::CallError ce;
const Variant *args[3] = { &v_result, &v_files, &v_index }; const Variant *args[3] = { &v_result, &v_files, &v_index };
callback.callp(args, 3, ret, ce); callback.callp(args, 3, ret, ce);
if (ce.error != Callable::CallError::CALL_OK) { 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))); 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]; NSOpenPanel *panel = [NSOpenPanel openPanel];
[panel setDirectoryURL:[NSURL fileURLWithPath:url]]; [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 setExtensionHidden:YES];
[panel setCanSelectHiddenExtension:YES]; [panel setCanSelectHiddenExtension:YES];
[panel setCanCreateDirectories:YES]; [panel setCanCreateDirectories:YES];
[panel setCanChooseFiles:(p_mode != FILE_DIALOG_MODE_OPEN_DIR)]; [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 setCanChooseDirectories:(p_mode == FILE_DIALOG_MODE_OPEN_DIR || p_mode == FILE_DIALOG_MODE_OPEN_ANY)];
[panel setShowsHiddenFiles:p_show_hidden]; [panel setShowsHiddenFiles:p_show_hidden];
[panel setDelegate:panel_delegate];
if (p_filename != "") { if (p_filename != "") {
NSString *fileurl = [NSString stringWithUTF8String:p_filename.utf8().get_data()]; NSString *fileurl = [NSString stringWithUTF8String:p_filename.utf8().get_data()];
[panel setNameFieldStringValue:fileurl]; [panel setNameFieldStringValue:fileurl];
@ -2333,9 +2263,23 @@ Error DisplayServerMacOS::file_dialog_show(const String &p_title, const String &
files.push_back(url); files.push_back(url);
} }
if (!callback.is_null()) { if (!callback.is_null()) {
if (p_options_in_cb) {
Variant v_result = true; Variant v_result = true;
Variant v_files = files; Variant v_files = files;
Variant v_index = [handler getIndex]; 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; Variant ret;
Callable::CallError ce; Callable::CallError ce;
const Variant *args[3] = { &v_result, &v_files, &v_index }; const Variant *args[3] = { &v_result, &v_files, &v_index };
@ -2345,18 +2289,34 @@ Error DisplayServerMacOS::file_dialog_show(const String &p_title, const String &
ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(callback, args, 3, ce))); ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(callback, args, 3, ce)));
} }
} }
}
} else { } else {
if (!callback.is_null()) { if (!callback.is_null()) {
if (p_options_in_cb) {
Variant v_result = false; Variant v_result = false;
Variant v_files = Vector<String>(); Variant v_files = Vector<String>();
Variant v_index = [handler getIndex]; 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; Variant ret;
Callable::CallError ce; Callable::CallError ce;
const Variant *args[3] = { &v_result, &v_files, &v_index }; const Variant *args[3] = { &v_result, &v_files, &v_index };
callback.callp(args, 3, ret, ce); callback.callp(args, 3, ret, ce);
if (ce.error != Callable::CallError::CALL_OK) { 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))); ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(callback, args, 3, ce)));
}
} }
} }
} }

View File

@ -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

View File

@ -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

View File

@ -219,7 +219,137 @@ void DisplayServerWindows::tts_stop() {
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) { 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_ _THREAD_SAFE_METHOD_
ERR_FAIL_INDEX_V(int(p_mode), FILE_DIALOG_MODE_SAVE_MAX, FAILED); 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); hr = CoCreateInstance(CLSID_FileOpenDialog, nullptr, CLSCTX_INPROC_SERVER, IID_IFileOpenDialog, (void **)&pfd);
} }
if (SUCCEEDED(hr)) { 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; DWORD flags;
pfd->GetOptions(&flags); pfd->GetOptions(&flags);
if (p_mode == FILE_DIALOG_MODE_OPEN_FILES) { 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); hr = pfd->Show(windows[window_id].hWnd);
pfd->Unadvise(cookie);
Dictionary options = event_handler->get_selected();
pfde->Release();
event_handler->Release();
UINT index = 0; UINT index = 0;
pfd->GetFileTypeIndex(&index); pfd->GetFileTypeIndex(&index);
if (index > 0) {
index = index - 1;
}
if (SUCCEEDED(hr)) { if (SUCCEEDED(hr)) {
Vector<String> file_names; Vector<String> file_names;
@ -346,6 +511,20 @@ Error DisplayServerWindows::file_dialog_show(const String &p_title, const String
} }
} }
if (!p_callback.is_null()) { if (!p_callback.is_null()) {
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_result = true;
Variant v_files = file_names; Variant v_files = file_names;
Variant v_index = index; Variant v_index = index;
@ -358,8 +537,23 @@ Error DisplayServerWindows::file_dialog_show(const String &p_title, const String
ERR_PRINT(vformat("Failed to execute file dialogs callback: %s.", Variant::get_callable_error_text(p_callback, args, 3, ce))); ERR_PRINT(vformat("Failed to execute file dialogs callback: %s.", Variant::get_callable_error_text(p_callback, args, 3, ce)));
} }
} }
}
} else { } else {
if (!p_callback.is_null()) { if (!p_callback.is_null()) {
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_result = false;
Variant v_files = Vector<String>(); Variant v_files = Vector<String>();
Variant v_index = index; Variant v_index = index;
@ -373,6 +567,7 @@ Error DisplayServerWindows::file_dialog_show(const String &p_title, const String
} }
} }
} }
}
pfd->Release(); pfd->Release();
if (prev_focus != INVALID_WINDOW_ID) { if (prev_focus != INVALID_WINDOW_ID) {
callable_mp(DisplayServer::get_singleton(), &DisplayServer::window_move_to_foreground).call_deferred(prev_focus); callable_mp(DisplayServer::get_singleton(), &DisplayServer::window_move_to_foreground).call_deferred(prev_focus);

View File

@ -497,6 +497,8 @@ class DisplayServerWindows : public DisplayServer {
LRESULT _handle_early_window_message(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); LRESULT _handle_early_window_message(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
Point2i _get_screens_origin() const; 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: public:
LRESULT WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); LRESULT WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
LRESULT MouseProc(int code, 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 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_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 void mouse_set_mode(MouseMode p_mode) override;
virtual MouseMode mouse_get_mode() const override; virtual MouseMode mouse_get_mode() const override;

View File

@ -30,9 +30,13 @@
#include "file_dialog.h" #include "file_dialog.h"
#include "core/config/project_settings.h"
#include "core/os/keyboard.h" #include "core/os/keyboard.h"
#include "core/string/print_string.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/label.h"
#include "scene/gui/option_button.h"
#include "scene/theme/theme_db.h" #include "scene/theme/theme_db.h"
FileDialog::GetIconFunc FileDialog::get_icon_func = nullptr; FileDialog::GetIconFunc FileDialog::get_icon_func = nullptr;
@ -56,20 +60,30 @@ void FileDialog::_focus_file_text() {
} }
void FileDialog::popup(const Rect2i &p_rect) { void FileDialog::popup(const Rect2i &p_rect) {
_update_option_controls();
#ifdef TOOLS_ENABLED #ifdef TOOLS_ENABLED
if (is_part_of_edited_scene()) { if (is_part_of_edited_scene()) {
ConfirmationDialog::popup(p_rect); ConfirmationDialog::popup(p_rect);
} }
#endif #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())) {
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 { } else {
ConfirmationDialog::popup(p_rect); ConfirmationDialog::popup(p_rect);
} }
} }
void FileDialog::set_visible(bool p_visible) { void FileDialog::set_visible(bool p_visible) {
_update_option_controls();
#ifdef TOOLS_ENABLED #ifdef TOOLS_ENABLED
if (is_part_of_edited_scene()) { if (is_part_of_edited_scene()) {
ConfirmationDialog::set_visible(p_visible); ConfirmationDialog::set_visible(p_visible);
@ -77,23 +91,52 @@ void FileDialog::set_visible(bool p_visible) {
} }
#endif #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) { 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 { } else {
ConfirmationDialog::set_visible(p_visible); 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_ok) {
if (p_files.size() > 0) { 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) { if (mode == FILE_MODE_OPEN_FILES) {
emit_signal(SNAME("files_selected"), p_files); emit_signal(SNAME("files_selected"), files);
} else { } else {
if (mode == FILE_MODE_SAVE_FILE) { 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); emit_signal(SNAME("file_selected"), f);
} else if ((mode == FILE_MODE_OPEN_ANY || mode == FILE_MODE_OPEN_FILE) && dir_access->file_exists(f)) { } else if ((mode == FILE_MODE_OPEN_ANY || mode == FILE_MODE_OPEN_FILE) && dir_access->file_exists(f)) {
emit_signal(SNAME("file_selected"), 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); file->set_text(f);
dir->set_text(f.get_base_dir()); dir->set_text(f.get_base_dir());
_filter_selected(p_filter); selected_options = p_selected_options;
filter->select(p_filter);
} }
} else { } else {
file->set_text(""); file->set_text("");
@ -1007,6 +1051,212 @@ void FileDialog::_update_drives(bool p_select) {
bool FileDialog::default_show_hidden_files = false; 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() { void FileDialog::_bind_methods() {
ClassDB::bind_method(D_METHOD("_cancel_pressed"), &FileDialog::_cancel_pressed); 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("add_filter", "filter", "description"), &FileDialog::add_filter, DEFVAL(""));
ClassDB::bind_method(D_METHOD("set_filters", "filters"), &FileDialog::set_filters); 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_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_dir"), &FileDialog::get_current_dir);
ClassDB::bind_method(D_METHOD("get_current_file"), &FileDialog::get_current_file); ClassDB::bind_method(D_METHOD("get_current_file"), &FileDialog::get_current_file);
ClassDB::bind_method(D_METHOD("get_current_path"), &FileDialog::get_current_path); 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::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::STRING, "root_subfolder"), "set_root_subfolder", "get_root_subfolder");
ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "filters"), "set_filters", "get_filters"); 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, "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::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"); 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); file_box->add_child(filter);
vbox->add_child(file_box); 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); dir_access = DirAccess::create(DirAccess::ACCESS_RESOURCES);
_update_drives(); _update_drives();

View File

@ -38,6 +38,8 @@
#include "scene/gui/option_button.h" #include "scene/gui/option_button.h"
#include "scene/gui/tree.h" #include "scene/gui/tree.h"
class GridContainer;
class FileDialog : public ConfirmationDialog { class FileDialog : public ConfirmationDialog {
GDCLASS(FileDialog, ConfirmationDialog); GDCLASS(FileDialog, ConfirmationDialog);
@ -70,6 +72,7 @@ private:
Button *makedir = nullptr; Button *makedir = nullptr;
Access access = ACCESS_RESOURCES; Access access = ACCESS_RESOURCES;
VBoxContainer *vbox = nullptr; VBoxContainer *vbox = nullptr;
GridContainer *grid_options = nullptr;
FileMode mode; FileMode mode;
LineEdit *dir = nullptr; LineEdit *dir = nullptr;
HBoxContainer *drives_container = nullptr; HBoxContainer *drives_container = nullptr;
@ -128,6 +131,15 @@ private:
Color icon_pressed_color; Color icon_pressed_color;
} theme_cache; } 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_dir();
void update_file_name(); void update_file_name();
void update_file_list(); void update_file_list();
@ -159,15 +171,23 @@ private:
virtual void shortcut_input(const Ref<InputEvent> &p_event) override; 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(); 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; virtual void _post_popup() override;
protected: protected:
void _validate_property(PropertyInfo &p_property) const; void _validate_property(PropertyInfo &p_property) const;
void _notification(int p_what); 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(); static void _bind_methods();
public: public:
@ -190,6 +210,20 @@ public:
void set_current_file(const String &p_file); void set_current_file(const String &p_file);
void set_current_path(const String &p_path); 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); void set_root_subfolder(const String &p_root);
String get_root_subfolder() const; String get_root_subfolder() const;

View File

@ -532,6 +532,11 @@ Error DisplayServer::file_dialog_show(const String &p_title, const String &p_cur
return OK; 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 { int DisplayServer::keyboard_get_layout_count() const {
return 0; 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("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_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_layout_count"), &DisplayServer::keyboard_get_layout_count);
ClassDB::bind_method(D_METHOD("keyboard_get_current_layout"), &DisplayServer::keyboard_get_current_layout); ClassDB::bind_method(D_METHOD("keyboard_get_current_layout"), &DisplayServer::keyboard_get_current_layout);

View File

@ -514,6 +514,7 @@ public:
FILE_DIALOG_MODE_SAVE_MAX 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_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_layout_count() const;
virtual int keyboard_get_current_layout() const; virtual int keyboard_get_current_layout() const;