From 4790da7900bac721cb6650dfa873800d53a67739 Mon Sep 17 00:00:00 2001 From: bruvzg <7645683+bruvzg@users.noreply.github.com> Date: Tue, 30 Mar 2021 15:42:50 +0300 Subject: [PATCH] [macOS] Implement optional native file selection dialog support for sandboxed apps. --- core/core_bind.cpp | 10 ++ core/core_bind.h | 2 + core/os/os.cpp | 4 + core/os/os.h | 3 + doc/classes/DisplayServer.xml | 33 ++++ doc/classes/FileDialog.xml | 4 + doc/classes/OS.xml | 17 +- platform/macos/display_server_macos.h | 2 + platform/macos/display_server_macos.mm | 170 ++++++++++++++++++ .../doc_classes/EditorExportPlatformMacOS.xml | 3 + platform/macos/export/export_plugin.cpp | 9 + platform/macos/os_macos.h | 4 + platform/macos/os_macos.mm | 59 ++++++ scene/gui/file_dialog.cpp | 43 +++++ scene/gui/file_dialog.h | 8 + scene/gui/popup_menu.cpp | 2 +- scene/gui/popup_menu.h | 2 +- scene/main/window.h | 2 +- servers/display_server.cpp | 13 ++ servers/display_server.h | 10 ++ 20 files changed, 395 insertions(+), 5 deletions(-) diff --git a/core/core_bind.cpp b/core/core_bind.cpp index 2d0d24406c92..a73b198be2c1 100644 --- a/core/core_bind.cpp +++ b/core/core_bind.cpp @@ -442,6 +442,10 @@ bool OS::has_feature(const String &p_feature) const { } } +bool OS::is_sandboxed() const { + return ::OS::get_singleton()->is_sandboxed(); +} + uint64_t OS::get_static_memory_usage() const { return ::OS::get_singleton()->get_static_memory_usage(); } @@ -545,6 +549,10 @@ Vector OS::get_granted_permissions() const { return ::OS::get_singleton()->get_granted_permissions(); } +void OS::revoke_granted_permissions() { + ::OS::get_singleton()->revoke_granted_permissions(); +} + String OS::get_unique_id() const { return ::OS::get_singleton()->get_unique_id(); } @@ -636,10 +644,12 @@ void OS::_bind_methods() { ClassDB::bind_method(D_METHOD("get_main_thread_id"), &OS::get_main_thread_id); ClassDB::bind_method(D_METHOD("has_feature", "tag_name"), &OS::has_feature); + ClassDB::bind_method(D_METHOD("is_sandboxed"), &OS::is_sandboxed); ClassDB::bind_method(D_METHOD("request_permission", "name"), &OS::request_permission); ClassDB::bind_method(D_METHOD("request_permissions"), &OS::request_permissions); ClassDB::bind_method(D_METHOD("get_granted_permissions"), &OS::get_granted_permissions); + ClassDB::bind_method(D_METHOD("revoke_granted_permissions"), &OS::revoke_granted_permissions); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "low_processor_usage_mode"), "set_low_processor_usage_mode", "is_in_low_processor_usage_mode"); ADD_PROPERTY(PropertyInfo(Variant::INT, "low_processor_usage_mode_sleep_usec"), "set_low_processor_usage_mode_sleep_usec", "get_low_processor_usage_mode_sleep_usec"); diff --git a/core/core_bind.h b/core/core_bind.h index 6b25510b1431..077bb19c806b 100644 --- a/core/core_bind.h +++ b/core/core_bind.h @@ -234,10 +234,12 @@ class OS : public Object { Thread::ID get_main_thread_id() const; bool has_feature(const String &p_feature) const; + bool is_sandboxed() const; bool request_permission(const String &p_name); bool request_permissions(); Vector get_granted_permissions() const; + void revoke_granted_permissions(); static OS *get_singleton() { return singleton; } diff --git a/core/os/os.cpp b/core/os/os.cpp index 67423128a3f4..38ea4a0fddd4 100644 --- a/core/os/os.cpp +++ b/core/os/os.cpp @@ -505,6 +505,10 @@ bool OS::has_feature(const String &p_feature) { return false; } +bool OS::is_sandboxed() const { + return false; +} + void OS::set_restart_on_exit(bool p_restart, const List &p_restart_arguments) { restart_on_exit = p_restart; restart_commandline = p_restart_arguments; diff --git a/core/os/os.h b/core/os/os.h index f2787d638160..965dc1f91250 100644 --- a/core/os/os.h +++ b/core/os/os.h @@ -295,6 +295,8 @@ class OS { bool has_feature(const String &p_feature); + virtual bool is_sandboxed() const; + void set_has_server_feature_callback(HasServerFeatureCallback p_callback); void set_restart_on_exit(bool p_restart, const List &p_restart_arguments); @@ -304,6 +306,7 @@ class OS { virtual bool request_permission(const String &p_name) { return true; } virtual bool request_permissions() { return true; } virtual Vector get_granted_permissions() const { return Vector(); } + virtual void revoke_granted_permissions() {} // For recording / measuring benchmark data. Only enabled with tools void set_use_benchmark(bool p_use_benchmark); diff --git a/doc/classes/DisplayServer.xml b/doc/classes/DisplayServer.xml index b09c81d806e2..920fe901a822 100644 --- a/doc/classes/DisplayServer.xml +++ b/doc/classes/DisplayServer.xml @@ -96,6 +96,24 @@ [b]Note:[/b] This method is implemented only on Windows. + + + + + + + + + + + Displays OS native dialog for selecting files or directories in the file system. + Callbacks have the following arguments: [code]bool status, PackedStringArray selected_paths[/code]. + [b]Note:[/b] This method is implemented if the display server has the [code]FEATURE_NATIVE_DIALOG[/code] feature. + [b]Note:[/b] This method is implemented on macOS. + [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. + + @@ -1729,6 +1747,21 @@ Represents the size of the [enum CursorShape] enum. + + The native file dialog allows selecting one, and only one file. + + + The native file dialog allows selecting multiple files. + + + The native file dialog only allows selecting a directory, disallowing the selection of any file. + + + The native file dialog allows selecting one file or directory. + + + The native file dialog will warn when a file exists. + Windowed mode, i.e. [Window] doesn't occupy the whole screen (unless set to the size of the screen). diff --git a/doc/classes/FileDialog.xml b/doc/classes/FileDialog.xml index 1020d2dfbcfb..75856e91d847 100644 --- a/doc/classes/FileDialog.xml +++ b/doc/classes/FileDialog.xml @@ -83,6 +83,10 @@ If [code]true[/code], the dialog will show hidden files. + + If [code]true[/code], [member access] is set to [constant ACCESS_FILESYSTEM], and it is supported by the current [DisplayServer], OS native dialog will be used instead of custom one. + [b]Note:[/b] On macOS, sandboxed apps always use native dialogs to access host filesystem. + diff --git a/doc/classes/OS.xml b/doc/classes/OS.xml index d8d0078b7794..03169d390a0b 100644 --- a/doc/classes/OS.xml +++ b/doc/classes/OS.xml @@ -240,8 +240,8 @@ - With this function, you can get the list of dangerous permissions that have been granted to the Android application. - [b]Note:[/b] This method is implemented only on Android. + On Android devices: With this function, you can get the list of dangerous permissions that have been granted. + On macOS (sandboxed applications only): This function returns the list of user selected folders accessible to the application. Use native file dialog to request folder access permission. @@ -534,6 +534,13 @@ Returns [code]true[/code] if the project will automatically restart when it exits for any reason, [code]false[/code] otherwise. See also [method set_restart_on_exit] and [method get_restart_on_exit_arguments]. + + + + Returns [code]true[/code] if application is running in the sandbox. + [b]Note:[/b] This method is implemented on macOS. + + @@ -602,6 +609,12 @@ [b]Note:[/b] This method is implemented only on Android. + + + + On macOS (sandboxed applications only), this function clears list of user selected folders accessible to the application. + + diff --git a/platform/macos/display_server_macos.h b/platform/macos/display_server_macos.h index 2a7b2ce2a9bd..aef45e534f17 100644 --- a/platform/macos/display_server_macos.h +++ b/platform/macos/display_server_macos.h @@ -315,6 +315,8 @@ class DisplayServerMacOS : public DisplayServer { virtual Error dialog_show(String p_title, String p_description, Vector p_buttons, const Callable &p_callback) override; virtual Error dialog_input_text(String p_title, String p_description, String p_partial, const Callable &p_callback) override; + virtual Error file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector &p_filters, const Callable &p_callback) override; + virtual void mouse_set_mode(MouseMode p_mode) override; virtual MouseMode mouse_get_mode() const override; diff --git a/platform/macos/display_server_macos.mm b/platform/macos/display_server_macos.mm index a4db78b69731..9be0c8ec74f6 100644 --- a/platform/macos/display_server_macos.mm +++ b/platform/macos/display_server_macos.mm @@ -1847,6 +1847,176 @@ return OK; } +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 &p_filters, const Callable &p_callback) { + _THREAD_SAFE_METHOD_ + + NSString *url = [NSString stringWithUTF8String:p_current_directory.utf8().get_data()]; + NSMutableArray *allowed_types = [[NSMutableArray alloc] init]; + bool allow_other = false; + for (int i = 0; i < p_filters.size(); i++) { + Vector tokens = p_filters[i].split(";"); + if (tokens.size() > 0) { + if (tokens[0].strip_edges() == "*.*") { + allow_other = true; + } else { + [allowed_types addObject:[NSString stringWithUTF8String:tokens[0].replace("*.", "").strip_edges().utf8().get_data()]]; + } + } + } + + Callable callback = p_callback; // Make a copy for async completion handler. + switch (p_mode) { + case FILE_DIALOG_MODE_SAVE_FILE: { + NSSavePanel *panel = [NSSavePanel savePanel]; + + [panel setDirectoryURL:[NSURL fileURLWithPath:url]]; + if ([allowed_types count]) { + [panel setAllowedFileTypes:allowed_types]; + } + [panel setAllowsOtherFileTypes:allow_other]; + [panel setExtensionHidden:YES]; + [panel setCanSelectHiddenExtension:YES]; + [panel setCanCreateDirectories:YES]; + [panel setShowsHiddenFiles:p_show_hidden]; + if (p_filename != "") { + NSString *fileurl = [NSString stringWithUTF8String:p_filename.utf8().get_data()]; + [panel setNameFieldStringValue:fileurl]; + } + + [panel beginSheetModalForWindow:[[NSApplication sharedApplication] mainWindow] + completionHandler:^(NSInteger ret) { + if (ret == NSModalResponseOK) { + // Save bookmark for folder. + if (OS::get_singleton()->is_sandboxed()) { + NSArray *bookmarks = [[NSUserDefaults standardUserDefaults] arrayForKey:@"sec_bookmarks"]; + bool skip = false; + for (id bookmark in bookmarks) { + NSError *error = nil; + BOOL isStale = NO; + NSURL *exurl = [NSURL URLByResolvingBookmarkData:bookmark options:NSURLBookmarkResolutionWithSecurityScope relativeToURL:nil bookmarkDataIsStale:&isStale error:&error]; + if (!error && !isStale && ([[exurl path] compare:[[panel directoryURL] path]] == NSOrderedSame)) { + skip = true; + break; + } + } + if (!skip) { + NSError *error = nil; + NSData *bookmark = [[panel directoryURL] bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:nil error:&error]; + if (!error) { + NSArray *new_bookmarks = [bookmarks arrayByAddingObject:bookmark]; + [[NSUserDefaults standardUserDefaults] setObject:new_bookmarks forKey:@"sec_bookmarks"]; + } + } + } + // Callback. + Vector files; + String url; + url.parse_utf8([[[panel URL] path] UTF8String]); + files.push_back(url); + if (!callback.is_null()) { + Variant v_status = true; + Variant v_files = files; + Variant *v_args[2] = { &v_status, &v_files }; + Variant ret; + Callable::CallError ce; + callback.callp((const Variant **)&v_args, 2, ret, ce); + } + } else { + if (!callback.is_null()) { + Variant v_status = false; + Variant v_files = Vector(); + Variant *v_args[2] = { &v_status, &v_files }; + Variant ret; + Callable::CallError ce; + callback.callp((const Variant **)&v_args, 2, ret, ce); + } + } + }]; + } break; + case FILE_DIALOG_MODE_OPEN_ANY: + case FILE_DIALOG_MODE_OPEN_FILE: + case FILE_DIALOG_MODE_OPEN_FILES: + case FILE_DIALOG_MODE_OPEN_DIR: { + NSOpenPanel *panel = [NSOpenPanel openPanel]; + + [panel setDirectoryURL:[NSURL fileURLWithPath:url]]; + if ([allowed_types count]) { + [panel setAllowedFileTypes:allowed_types]; + } + [panel setAllowsOtherFileTypes:allow_other]; + [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]; + if (p_filename != "") { + NSString *fileurl = [NSString stringWithUTF8String:p_filename.utf8().get_data()]; + [panel setNameFieldStringValue:fileurl]; + } + [panel setAllowsMultipleSelection:(p_mode == FILE_DIALOG_MODE_OPEN_FILES)]; + + [panel beginSheetModalForWindow:[[NSApplication sharedApplication] mainWindow] + completionHandler:^(NSInteger ret) { + if (ret == NSModalResponseOK) { + // Save bookmark for folder. + NSArray *urls = [(NSOpenPanel *)panel URLs]; + if (OS::get_singleton()->is_sandboxed()) { + NSArray *bookmarks = [[NSUserDefaults standardUserDefaults] arrayForKey:@"sec_bookmarks"]; + NSMutableArray *new_bookmarks = [bookmarks mutableCopy]; + for (NSUInteger i = 0; i != [urls count]; ++i) { + bool skip = false; + for (id bookmark in bookmarks) { + NSError *error = nil; + BOOL isStale = NO; + NSURL *exurl = [NSURL URLByResolvingBookmarkData:bookmark options:NSURLBookmarkResolutionWithSecurityScope relativeToURL:nil bookmarkDataIsStale:&isStale error:&error]; + if (!error && !isStale && ([[exurl path] compare:[[urls objectAtIndex:i] path]] == NSOrderedSame)) { + skip = true; + break; + } + } + if (!skip) { + NSError *error = nil; + NSData *bookmark = [[urls objectAtIndex:i] bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:nil error:&error]; + if (!error) { + [new_bookmarks addObject:bookmark]; + } + } + } + [[NSUserDefaults standardUserDefaults] setObject:new_bookmarks forKey:@"sec_bookmarks"]; + } + // Callback. + Vector files; + for (NSUInteger i = 0; i != [urls count]; ++i) { + String url; + url.parse_utf8([[[urls objectAtIndex:i] path] UTF8String]); + files.push_back(url); + } + if (!callback.is_null()) { + Variant v_status = true; + Variant v_files = files; + Variant *v_args[2] = { &v_status, &v_files }; + Variant ret; + Callable::CallError ce; + callback.callp((const Variant **)&v_args, 2, ret, ce); + } + } else { + if (!callback.is_null()) { + Variant v_status = false; + Variant v_files = Vector(); + Variant *v_args[2] = { &v_status, &v_files }; + Variant ret; + Callable::CallError ce; + callback.callp((const Variant **)&v_args, 2, ret, ce); + } + } + }]; + } break; + } + + return OK; +} + Error DisplayServerMacOS::dialog_input_text(String p_title, String p_description, String p_partial, const Callable &p_callback) { _THREAD_SAFE_METHOD_ diff --git a/platform/macos/doc_classes/EditorExportPlatformMacOS.xml b/platform/macos/doc_classes/EditorExportPlatformMacOS.xml index 9199701eb3be..6af816989d96 100644 --- a/platform/macos/doc_classes/EditorExportPlatformMacOS.xml +++ b/platform/macos/doc_classes/EditorExportPlatformMacOS.xml @@ -96,6 +96,9 @@ Allows read or write access to the user's "Pictures" folder. See [url=https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_assets_pictures_read-write]com.apple.security.files.pictures.read-write[/url]. + + Allows read or write access to the locations the user has selected using a native file dialog. See [url=https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_files_user-selected_read-write]com.apple.security.files.user-selected.read-write[/url]. + List of helper executables to embedded to the app bundle. Sandboxed app are limited to execute only these executable. See [url=https://developer.apple.com/documentation/xcode/embedding-a-helper-tool-in-a-sandboxed-app]Embedding a command-line tool in a sandboxed app[/url]. diff --git a/platform/macos/export/export_plugin.cpp b/platform/macos/export/export_plugin.cpp index 0dc6d0dcee96..a3ba31a5d980 100644 --- a/platform/macos/export/export_plugin.cpp +++ b/platform/macos/export/export_plugin.cpp @@ -425,6 +425,7 @@ void EditorExportPlatformMacOS::get_export_options(List *r_options r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/entitlements/app_sandbox/files_pictures", PROPERTY_HINT_ENUM, "No,Read-only,Read-write"), 0)); r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/entitlements/app_sandbox/files_music", PROPERTY_HINT_ENUM, "No,Read-only,Read-write"), 0)); r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/entitlements/app_sandbox/files_movies", PROPERTY_HINT_ENUM, "No,Read-only,Read-write"), 0)); + r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/entitlements/app_sandbox/files_user_selected", PROPERTY_HINT_ENUM, "No,Read-only,Read-write"), 0)); r_options->push_back(ExportOption(PropertyInfo(Variant::ARRAY, "codesign/entitlements/app_sandbox/helper_executables", PROPERTY_HINT_ARRAY_TYPE, itos(Variant::STRING) + "/" + itos(PROPERTY_HINT_GLOBAL_FILE) + ":"), Array())); r_options->push_back(ExportOption(PropertyInfo(Variant::PACKED_STRING_ARRAY, "codesign/custom_options"), PackedStringArray())); @@ -1922,6 +1923,14 @@ Error EditorExportPlatformMacOS::export_project(const Ref &p ent_f->store_line("com.apple.security.files.movies.read-write"); ent_f->store_line(""); } + if ((int)p_preset->get("codesign/entitlements/app_sandbox/files_user_selected") == 1) { + ent_f->store_line("com.apple.security.files.user-selected.read-only"); + ent_f->store_line(""); + } + if ((int)p_preset->get("codesign/entitlements/app_sandbox/files_user_selected") == 2) { + ent_f->store_line("com.apple.security.files.user-selected.read-write"); + ent_f->store_line(""); + } } ent_f->store_line(""); diff --git a/platform/macos/os_macos.h b/platform/macos/os_macos.h index ab61649d19d2..ae94b6296d54 100644 --- a/platform/macos/os_macos.h +++ b/platform/macos/os_macos.h @@ -113,6 +113,10 @@ class OS_MacOS : public OS_Unix { virtual String get_unique_id() const override; virtual String get_processor_name() const override; + virtual bool is_sandboxed() const override; + virtual Vector get_granted_permissions() const override; + virtual void revoke_granted_permissions() override; + virtual bool _check_internal_feature_support(const String &p_feature) override; virtual void disable_crash_handler() override; diff --git a/platform/macos/os_macos.mm b/platform/macos/os_macos.mm index 69062c5920d0..c17ea95f4f75 100644 --- a/platform/macos/os_macos.mm +++ b/platform/macos/os_macos.mm @@ -76,6 +76,36 @@ ERR_FAIL_V_MSG("", String("Couldn't get the CPU model name. Returning an empty string.")); } +bool OS_MacOS::is_sandboxed() const { + return has_environment("APP_SANDBOX_CONTAINER_ID"); +} + +Vector OS_MacOS::get_granted_permissions() const { + Vector ret; + + if (is_sandboxed()) { + NSArray *bookmarks = [[NSUserDefaults standardUserDefaults] arrayForKey:@"sec_bookmarks"]; + for (id bookmark in bookmarks) { + NSError *error = nil; + BOOL isStale = NO; + NSURL *url = [NSURL URLByResolvingBookmarkData:bookmark options:NSURLBookmarkResolutionWithSecurityScope relativeToURL:nil bookmarkDataIsStale:&isStale error:&error]; + if (!error && !isStale) { + String url_string; + url_string.parse_utf8([[url path] UTF8String]); + ret.push_back(url_string); + } + } + } + + return ret; +} + +void OS_MacOS::revoke_granted_permissions() { + if (is_sandboxed()) { + [[NSUserDefaults standardUserDefaults] setObject:nil forKey:@"sec_bookmarks"]; + } +} + void OS_MacOS::initialize_core() { OS_Unix::initialize_core(); @@ -85,6 +115,18 @@ } void OS_MacOS::finalize() { + if (is_sandboxed()) { + NSArray *bookmarks = [[NSUserDefaults standardUserDefaults] arrayForKey:@"sec_bookmarks"]; + for (id bookmark in bookmarks) { + NSError *error = nil; + BOOL isStale = NO; + NSURL *url = [NSURL URLByResolvingBookmarkData:bookmark options:NSURLBookmarkResolutionWithSecurityScope relativeToURL:nil bookmarkDataIsStale:&isStale error:&error]; + if (!error && !isStale) { + [url stopAccessingSecurityScopedResource]; + } + } + } + #ifdef COREMIDI_ENABLED midi_driver.close(); #endif @@ -733,6 +775,23 @@ } OS_MacOS::OS_MacOS() { + if (is_sandboxed()) { + // Load security-scoped bookmarks, request access, remove stale or invalid bookmarks. + NSArray *bookmarks = [[NSUserDefaults standardUserDefaults] arrayForKey:@"sec_bookmarks"]; + NSMutableArray *new_bookmarks = [[NSMutableArray alloc] init]; + for (id bookmark in bookmarks) { + NSError *error = nil; + BOOL isStale = NO; + NSURL *url = [NSURL URLByResolvingBookmarkData:bookmark options:NSURLBookmarkResolutionWithSecurityScope relativeToURL:nil bookmarkDataIsStale:&isStale error:&error]; + if (!error && !isStale) { + if ([url startAccessingSecurityScopedResource]) { + [new_bookmarks addObject:bookmark]; + } + } + } + [[NSUserDefaults standardUserDefaults] setObject:new_bookmarks forKey:@"sec_bookmarks"]; + } + main_loop = nullptr; Vector loggers; diff --git a/scene/gui/file_dialog.cpp b/scene/gui/file_dialog.cpp index f7507640c825..fe32e406e8a8 100644 --- a/scene/gui/file_dialog.cpp +++ b/scene/gui/file_dialog.cpp @@ -54,6 +54,38 @@ void FileDialog::_focus_file_text() { } } +void FileDialog::popup(const Rect2i &p_rect) { + 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)); + } else { + ConfirmationDialog::popup(p_rect); + } +} + +void FileDialog::_native_dialog_cb(bool p_ok, const Vector &p_files) { + if (p_ok) { + if (p_files.size() > 0) { + String f = p_files[0]; + if (mode == FILE_MODE_OPEN_FILES) { + emit_signal("files_selected", p_files); + } else { + if (mode == FILE_MODE_SAVE_FILE) { + emit_signal("file_selected", f); + } else if ((mode == FILE_MODE_OPEN_ANY || mode == FILE_MODE_OPEN_FILE) && dir_access->file_exists(f)) { + emit_signal("file_selected", f); + } else if (mode == FILE_MODE_OPEN_ANY || mode == FILE_MODE_OPEN_DIR) { + emit_signal("dir_selected", f); + } + } + file->set_text(f); + dir->set_text(f.get_base_dir()); + } + } else { + file->set_text(""); + emit_signal("cancelled"); + } +} + VBoxContainer *FileDialog::get_vbox() { return vbox; } @@ -980,6 +1012,8 @@ void FileDialog::_bind_methods() { ClassDB::bind_method(D_METHOD("get_root_subfolder"), &FileDialog::get_root_subfolder); ClassDB::bind_method(D_METHOD("set_show_hidden_files", "show"), &FileDialog::set_show_hidden_files); ClassDB::bind_method(D_METHOD("is_showing_hidden_files"), &FileDialog::is_showing_hidden_files); + ClassDB::bind_method(D_METHOD("set_use_native_dialog", "native"), &FileDialog::set_use_native_dialog); + ClassDB::bind_method(D_METHOD("get_use_native_dialog"), &FileDialog::get_use_native_dialog); ClassDB::bind_method(D_METHOD("deselect_all"), &FileDialog::deselect_all); ClassDB::bind_method(D_METHOD("invalidate"), &FileDialog::invalidate); @@ -990,6 +1024,7 @@ void FileDialog::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::STRING, "root_subfolder"), "set_root_subfolder", "get_root_subfolder"); ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "filters"), "set_filters", "get_filters"); ADD_PROPERTY(PropertyInfo(Variant::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"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "current_file", PROPERTY_HINT_FILE, "*", PROPERTY_USAGE_NONE), "set_current_file", "get_current_file"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "current_path", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_current_path", "get_current_path"); @@ -1025,6 +1060,14 @@ void FileDialog::set_default_show_hidden_files(bool p_show) { default_show_hidden_files = p_show; } +void FileDialog::set_use_native_dialog(bool p_native) { + use_native_dialog = p_native; +} + +bool FileDialog::get_use_native_dialog() const { + return use_native_dialog; +} + FileDialog::FileDialog() { show_hidden_files = default_show_hidden_files; diff --git a/scene/gui/file_dialog.h b/scene/gui/file_dialog.h index 87c41be98b51..dece241ca9af 100644 --- a/scene/gui/file_dialog.h +++ b/scene/gui/file_dialog.h @@ -105,6 +105,7 @@ class FileDialog : public ConfirmationDialog { static bool default_show_hidden_files; bool show_hidden_files = false; + bool use_native_dialog = false; bool is_invalidating = false; @@ -158,6 +159,8 @@ class FileDialog : public ConfirmationDialog { virtual void shortcut_input(const Ref &p_event) override; + void _native_dialog_cb(bool p_ok, const Vector &p_files); + bool _is_open_should_be_disabled(); virtual void _post_popup() override; @@ -169,6 +172,8 @@ class FileDialog : public ConfirmationDialog { static void _bind_methods(); //bind helpers public: + virtual void popup(const Rect2i &p_rect = Rect2i()) override; + void popup_file_dialog(); void clear_filters(); void add_filter(const String &p_filter, const String &p_description = ""); @@ -191,6 +196,9 @@ class FileDialog : public ConfirmationDialog { void set_mode_overrides_title(bool p_override); bool is_mode_overriding_title() const; + void set_use_native_dialog(bool p_native); + bool get_use_native_dialog() const; + void set_file_mode(FileMode p_mode); FileMode get_file_mode() const; diff --git a/scene/gui/popup_menu.cpp b/scene/gui/popup_menu.cpp index e2324f3fe5ea..c3c18ddadeb3 100644 --- a/scene/gui/popup_menu.cpp +++ b/scene/gui/popup_menu.cpp @@ -2308,7 +2308,7 @@ void PopupMenu::_bind_methods() { ADD_SIGNAL(MethodInfo("menu_changed")); } -void PopupMenu::popup(const Rect2 &p_bounds) { +void PopupMenu::popup(const Rect2i &p_bounds) { moved = Vector2(); popup_time_msec = OS::get_singleton()->get_ticks_msec(); Popup::popup(p_bounds); diff --git a/scene/gui/popup_menu.h b/scene/gui/popup_menu.h index b4655f13ae29..5ad9cd430306 100644 --- a/scene/gui/popup_menu.h +++ b/scene/gui/popup_menu.h @@ -312,7 +312,7 @@ class PopupMenu : public Popup { void set_allow_search(bool p_allow); bool get_allow_search() const; - virtual void popup(const Rect2 &p_bounds = Rect2()); + virtual void popup(const Rect2i &p_bounds = Rect2i()) override; void take_mouse_focus(); diff --git a/scene/main/window.h b/scene/main/window.h index b98b888380be..7a10499d9b7f 100644 --- a/scene/main/window.h +++ b/scene/main/window.h @@ -315,7 +315,7 @@ class Window : public Viewport { Window *get_parent_visible_window() const; Viewport *get_parent_viewport() const; - void popup(const Rect2i &p_screen_rect = Rect2i()); + virtual void popup(const Rect2i &p_screen_rect = Rect2i()); void popup_on_parent(const Rect2i &p_parent_rect); void popup_centered(const Size2i &p_minsize = Size2i()); void popup_centered_ratio(float p_ratio = 0.8); diff --git a/servers/display_server.cpp b/servers/display_server.cpp index 313e7218ed6d..5822a630db2c 100644 --- a/servers/display_server.cpp +++ b/servers/display_server.cpp @@ -497,6 +497,11 @@ Error DisplayServer::dialog_input_text(String p_title, String p_description, Str return OK; } +Error DisplayServer::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 &p_filters, 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; } @@ -755,6 +760,8 @@ void DisplayServer::_bind_methods() { ClassDB::bind_method(D_METHOD("dialog_show", "title", "description", "buttons", "callback"), &DisplayServer::dialog_show); 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("keyboard_get_layout_count"), &DisplayServer::keyboard_get_layout_count); ClassDB::bind_method(D_METHOD("keyboard_get_current_layout"), &DisplayServer::keyboard_get_current_layout); ClassDB::bind_method(D_METHOD("keyboard_set_current_layout", "index"), &DisplayServer::keyboard_set_current_layout); @@ -846,6 +853,12 @@ void DisplayServer::_bind_methods() { BIND_ENUM_CONSTANT(CURSOR_HELP); BIND_ENUM_CONSTANT(CURSOR_MAX); + BIND_ENUM_CONSTANT(FILE_DIALOG_MODE_OPEN_FILE); + BIND_ENUM_CONSTANT(FILE_DIALOG_MODE_OPEN_FILES); + BIND_ENUM_CONSTANT(FILE_DIALOG_MODE_OPEN_DIR); + BIND_ENUM_CONSTANT(FILE_DIALOG_MODE_OPEN_ANY); + BIND_ENUM_CONSTANT(FILE_DIALOG_MODE_SAVE_FILE); + BIND_ENUM_CONSTANT(WINDOW_MODE_WINDOWED); BIND_ENUM_CONSTANT(WINDOW_MODE_MINIMIZED); BIND_ENUM_CONSTANT(WINDOW_MODE_MAXIMIZED); diff --git a/servers/display_server.h b/servers/display_server.h index e6d9c51a679d..5db9b3231f42 100644 --- a/servers/display_server.h +++ b/servers/display_server.h @@ -493,6 +493,15 @@ class DisplayServer : public Object { virtual Error dialog_show(String p_title, String p_description, Vector p_buttons, const Callable &p_callback); virtual Error dialog_input_text(String p_title, String p_description, String p_partial, const Callable &p_callback); + enum FileDialogMode { + FILE_DIALOG_MODE_OPEN_FILE, + FILE_DIALOG_MODE_OPEN_FILES, + FILE_DIALOG_MODE_OPEN_DIR, + FILE_DIALOG_MODE_OPEN_ANY, + FILE_DIALOG_MODE_SAVE_FILE + }; + 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 &p_filters, const Callable &p_callback); + virtual int keyboard_get_layout_count() const; virtual int keyboard_get_current_layout() const; virtual void keyboard_set_current_layout(int p_index); @@ -546,5 +555,6 @@ VARIANT_ENUM_CAST(DisplayServer::VirtualKeyboardType); VARIANT_ENUM_CAST(DisplayServer::CursorShape) VARIANT_ENUM_CAST(DisplayServer::VSyncMode) VARIANT_ENUM_CAST(DisplayServer::TTSUtteranceEvent) +VARIANT_ENUM_CAST(DisplayServer::FileDialogMode) #endif // DISPLAY_SERVER_H