[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" />
<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.

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

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

View file

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

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);
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

View file

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

View file

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

View file

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

View file

@ -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,30 +2147,60 @@ Error DisplayServerMacOS::file_dialog_show(const String &p_title, const String &
url.parse_utf8([[[panel URL] path] UTF8String]);
files.push_back(url);
if (!callback.is_null()) {
Variant v_result = true;
Variant v_files = files;
Variant v_index = [handler getIndex];
Variant ret;
Callable::CallError ce;
const Variant *args[3] = { &v_result, &v_files, &v_index };
if (p_options_in_cb) {
Variant v_result = true;
Variant v_files = files;
Variant v_index = [panel_delegate getIndex];
Variant v_opt = [panel_delegate getSelection];
Variant ret;
Callable::CallError ce;
const Variant *args[4] = { &v_result, &v_files, &v_index, &v_opt };
callback.callp(args, 3, ret, ce);
if (ce.error != Callable::CallError::CALL_OK) {
ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(callback, args, 3, ce)));
callback.callp(args, 4, ret, ce);
if (ce.error != Callable::CallError::CALL_OK) {
ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(callback, args, 4, ce)));
}
} else {
Variant v_result = true;
Variant v_files = files;
Variant v_index = [panel_delegate getIndex];
Variant ret;
Callable::CallError ce;
const Variant *args[3] = { &v_result, &v_files, &v_index };
callback.callp(args, 3, ret, ce);
if (ce.error != Callable::CallError::CALL_OK) {
ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(callback, args, 3, ce)));
}
}
}
} else {
if (!callback.is_null()) {
Variant v_result = false;
Variant v_files = Vector<String>();
Variant v_index = [handler getIndex];
Variant ret;
Callable::CallError ce;
const Variant *args[3] = { &v_result, &v_files, &v_index };
if (p_options_in_cb) {
Variant v_result = false;
Variant v_files = Vector<String>();
Variant v_index = [panel_delegate getIndex];
Variant v_opt = [panel_delegate getSelection];
Variant ret;
Callable::CallError ce;
const Variant *args[4] = { &v_result, &v_files, &v_index, &v_opt };
callback.callp(args, 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)));
callback.callp(args, 4, ret, ce);
if (ce.error != Callable::CallError::CALL_OK) {
ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(callback, args, 4, ce)));
}
} else {
Variant v_result = false;
Variant v_files = Vector<String>();
Variant v_index = [panel_delegate getIndex];
Variant ret;
Callable::CallError ce;
const Variant *args[3] = { &v_result, &v_files, &v_index };
callback.callp(args, 3, ret, ce);
if (ce.error != Callable::CallError::CALL_OK) {
ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(callback, args, 3, ce)));
}
}
}
}
@ -2283,13 +2212,14 @@ Error DisplayServerMacOS::file_dialog_show(const String &p_title, const String &
NSOpenPanel *panel = [NSOpenPanel openPanel];
[panel setDirectoryURL:[NSURL fileURLWithPath:url]];
handler = _make_accessory_view(panel, p_filters);
[panel_delegate makeAccessoryView:panel filters:p_filters options:p_options];
[panel setExtensionHidden:YES];
[panel setCanSelectHiddenExtension:YES];
[panel setCanCreateDirectories:YES];
[panel setCanChooseFiles:(p_mode != FILE_DIALOG_MODE_OPEN_DIR)];
[panel setCanChooseDirectories:(p_mode == FILE_DIALOG_MODE_OPEN_DIR || p_mode == FILE_DIALOG_MODE_OPEN_ANY)];
[panel setShowsHiddenFiles:p_show_hidden];
[panel setDelegate:panel_delegate];
if (p_filename != "") {
NSString *fileurl = [NSString stringWithUTF8String:p_filename.utf8().get_data()];
[panel setNameFieldStringValue:fileurl];
@ -2333,30 +2263,60 @@ Error DisplayServerMacOS::file_dialog_show(const String &p_title, const String &
files.push_back(url);
}
if (!callback.is_null()) {
Variant v_result = true;
Variant v_files = files;
Variant v_index = [handler getIndex];
Variant ret;
Callable::CallError ce;
const Variant *args[3] = { &v_result, &v_files, &v_index };
if (p_options_in_cb) {
Variant v_result = true;
Variant v_files = files;
Variant v_index = [panel_delegate getIndex];
Variant v_opt = [panel_delegate getSelection];
Variant ret;
Callable::CallError ce;
const Variant *args[4] = { &v_result, &v_files, &v_index, &v_opt };
callback.callp(args, 3, ret, ce);
if (ce.error != Callable::CallError::CALL_OK) {
ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(callback, args, 3, ce)));
callback.callp(args, 4, ret, ce);
if (ce.error != Callable::CallError::CALL_OK) {
ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(callback, args, 4, ce)));
}
} else {
Variant v_result = true;
Variant v_files = files;
Variant v_index = [panel_delegate getIndex];
Variant ret;
Callable::CallError ce;
const Variant *args[3] = { &v_result, &v_files, &v_index };
callback.callp(args, 3, ret, ce);
if (ce.error != Callable::CallError::CALL_OK) {
ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(callback, args, 3, ce)));
}
}
}
} else {
if (!callback.is_null()) {
Variant v_result = false;
Variant v_files = Vector<String>();
Variant v_index = [handler getIndex];
Variant ret;
Callable::CallError ce;
const Variant *args[3] = { &v_result, &v_files, &v_index };
if (p_options_in_cb) {
Variant v_result = false;
Variant v_files = Vector<String>();
Variant v_index = [panel_delegate getIndex];
Variant v_opt = [panel_delegate getSelection];
Variant ret;
Callable::CallError ce;
const Variant *args[4] = { &v_result, &v_files, &v_index, &v_opt };
callback.callp(args, 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)));
callback.callp(args, 4, ret, ce);
if (ce.error != Callable::CallError::CALL_OK) {
ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(callback, args, 4, ce)));
}
} else {
Variant v_result = false;
Variant v_files = Vector<String>();
Variant v_index = [panel_delegate getIndex];
Variant ret;
Callable::CallError ce;
const Variant *args[3] = { &v_result, &v_files, &v_index };
callback.callp(args, 3, ret, ce);
if (ce.error != Callable::CallError::CALL_OK) {
ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(callback, args, 3, ce)));
}
}
}
}

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

View file

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

View file

@ -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();

View file

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

View file

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

View file

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