[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:
parent
74c32faa78
commit
a8f521bcad
|
@ -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.
|
||||
|
|
|
@ -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="""">
|
||||
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>
|
||||
|
|
|
@ -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,7 +506,17 @@ 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) {
|
||||
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;
|
||||
Callable::CallError ce;
|
||||
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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FreeDesktopPortalDesktop::_thread_file_dialog_monitor(void *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)) {
|
||||
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);
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
- (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) {
|
||||
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_
|
||||
|
||||
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,9 +2147,23 @@ 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()) {
|
||||
if (p_options_in_cb) {
|
||||
Variant v_result = true;
|
||||
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;
|
||||
Callable::CallError ce;
|
||||
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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!callback.is_null()) {
|
||||
if (p_options_in_cb) {
|
||||
Variant v_result = false;
|
||||
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;
|
||||
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)));
|
||||
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,9 +2263,23 @@ Error DisplayServerMacOS::file_dialog_show(const String &p_title, const String &
|
|||
files.push_back(url);
|
||||
}
|
||||
if (!callback.is_null()) {
|
||||
if (p_options_in_cb) {
|
||||
Variant v_result = true;
|
||||
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;
|
||||
Callable::CallError ce;
|
||||
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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!callback.is_null()) {
|
||||
if (p_options_in_cb) {
|
||||
Variant v_result = false;
|
||||
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;
|
||||
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)));
|
||||
ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(callback, args, 3, ce)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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,6 +511,20 @@ Error DisplayServerWindows::file_dialog_show(const String &p_title, const String
|
|||
}
|
||||
}
|
||||
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_files = file_names;
|
||||
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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
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_files = Vector<String>();
|
||||
Variant v_index = index;
|
||||
|
@ -373,6 +567,7 @@ Error DisplayServerWindows::file_dialog_show(const String &p_title, const String
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pfd->Release();
|
||||
if (prev_focus != INVALID_WINDOW_ID) {
|
||||
callable_mp(DisplayServer::get_singleton(), &DisplayServer::window_move_to_foreground).call_deferred(prev_focus);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue