Skip to content

Commit

Permalink
Merge pull request #47499 from bruvzg/mac_native_fd
Browse files Browse the repository at this point in the history
[macOS, sandbox] Implement optional native file selection dialog support for sandboxed apps.
  • Loading branch information
YuriSizov committed Jul 14, 2023
2 parents 60f3b79 + 4790da7 commit c2d0c52
Show file tree
Hide file tree
Showing 20 changed files with 395 additions and 5 deletions.
10 changes: 10 additions & 0 deletions core/core_bind.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down Expand Up @@ -545,6 +549,10 @@ Vector<String> 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();
}
Expand Down Expand Up @@ -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");
Expand Down
2 changes: 2 additions & 0 deletions core/core_bind.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> get_granted_permissions() const;
void revoke_granted_permissions();

static OS *get_singleton() { return singleton; }

Expand Down
4 changes: 4 additions & 0 deletions core/os/os.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> &p_restart_arguments) {
restart_on_exit = p_restart;
restart_commandline = p_restart_arguments;
Expand Down
3 changes: 3 additions & 0 deletions core/os/os.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> &p_restart_arguments);
Expand All @@ -304,6 +306,7 @@ class OS {
virtual bool request_permission(const String &p_name) { return true; }
virtual bool request_permissions() { return true; }
virtual Vector<String> get_granted_permissions() const { return Vector<String>(); }
virtual void revoke_granted_permissions() {}

// For recording / measuring benchmark data. Only enabled with tools
void set_use_benchmark(bool p_use_benchmark);
Expand Down
33 changes: 33 additions & 0 deletions doc/classes/DisplayServer.xml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,24 @@
[b]Note:[/b] This method is implemented only on Windows.
</description>
</method>
<method name="file_dialog_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="filename" type="String" />
<param index="3" name="show_hidden" type="bool" />
<param index="4" name="mode" type="int" enum="DisplayServer.FileDialogMode" />
<param index="5" name="filters" type="PackedStringArray" />
<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[/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.
</description>
</method>
<method name="force_process_and_drop_events">
<return type="void" />
<description>
Expand Down Expand Up @@ -1729,6 +1747,21 @@
<constant name="CURSOR_MAX" value="17" enum="CursorShape">
Represents the size of the [enum CursorShape] enum.
</constant>
<constant name="FILE_DIALOG_MODE_OPEN_FILE" value="0" enum="FileDialogMode">
The native file dialog allows selecting one, and only one file.
</constant>
<constant name="FILE_DIALOG_MODE_OPEN_FILES" value="1" enum="FileDialogMode">
The native file dialog allows selecting multiple files.
</constant>
<constant name="FILE_DIALOG_MODE_OPEN_DIR" value="2" enum="FileDialogMode">
The native file dialog only allows selecting a directory, disallowing the selection of any file.
</constant>
<constant name="FILE_DIALOG_MODE_OPEN_ANY" value="3" enum="FileDialogMode">
The native file dialog allows selecting one file or directory.
</constant>
<constant name="FILE_DIALOG_MODE_SAVE_FILE" value="4" enum="FileDialogMode">
The native file dialog will warn when a file exists.
</constant>
<constant name="WINDOW_MODE_WINDOWED" value="0" enum="WindowMode">
Windowed mode, i.e. [Window] doesn't occupy the whole screen (unless set to the size of the screen).
</constant>
Expand Down
4 changes: 4 additions & 0 deletions doc/classes/FileDialog.xml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@
If [code]true[/code], the dialog will show hidden files.
</member>
<member name="title" type="String" setter="set_title" getter="get_title" overrides="Window" default="&quot;Save a File&quot;" />
<member name="use_native_dialog" type="bool" setter="set_use_native_dialog" getter="get_use_native_dialog" default="false">
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.
</member>
</members>
<signals>
<signal name="dir_selected">
Expand Down
17 changes: 15 additions & 2 deletions doc/classes/OS.xml
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,8 @@
<method name="get_granted_permissions" qualifiers="const">
<return type="PackedStringArray" />
<description>
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.
</description>
</method>
<method name="get_keycode_string" qualifiers="const">
Expand Down Expand Up @@ -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].
</description>
</method>
<method name="is_sandboxed" qualifiers="const">
<return type="bool" />
<description>
Returns [code]true[/code] if application is running in the sandbox.
[b]Note:[/b] This method is implemented on macOS.
</description>
</method>
<method name="is_stdout_verbose" qualifiers="const">
<return type="bool" />
<description>
Expand Down Expand Up @@ -602,6 +609,12 @@
[b]Note:[/b] This method is implemented only on Android.
</description>
</method>
<method name="revoke_granted_permissions">
<return type="void" />
<description>
On macOS (sandboxed applications only), this function clears list of user selected folders accessible to the application.
</description>
</method>
<method name="set_environment" qualifiers="const">
<return type="void" />
<param index="0" name="variable" type="String" />
Expand Down
2 changes: 2 additions & 0 deletions platform/macos/display_server_macos.h
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,8 @@ class DisplayServerMacOS : public DisplayServer {
virtual Error dialog_show(String p_title, String p_description, Vector<String> 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<String> &p_filters, const Callable &p_callback) override;

virtual void mouse_set_mode(MouseMode p_mode) override;
virtual MouseMode mouse_get_mode() const override;

Expand Down
170 changes: 170 additions & 0 deletions platform/macos/display_server_macos.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> &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<String> 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<String> 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<String>();
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<String> 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<String>();
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_

Expand Down
3 changes: 3 additions & 0 deletions platform/macos/doc_classes/EditorExportPlatformMacOS.xml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@
<member name="codesign/entitlements/app_sandbox/files_pictures" type="int" setter="" getter="">
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].
</member>
<member name="codesign/entitlements/app_sandbox/files_user_selected" type="int" setter="" getter="">
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].
</member>
<member name="codesign/entitlements/app_sandbox/helper_executables" type="Array" setter="" getter="">
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].
</member>
Expand Down
9 changes: 9 additions & 0 deletions platform/macos/export/export_plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ void EditorExportPlatformMacOS::get_export_options(List<ExportOption> *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()));

Expand Down Expand Up @@ -1922,6 +1923,14 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p
ent_f->store_line("<key>com.apple.security.files.movies.read-write</key>");
ent_f->store_line("<true/>");
}
if ((int)p_preset->get("codesign/entitlements/app_sandbox/files_user_selected") == 1) {
ent_f->store_line("<key>com.apple.security.files.user-selected.read-only</key>");
ent_f->store_line("<true/>");
}
if ((int)p_preset->get("codesign/entitlements/app_sandbox/files_user_selected") == 2) {
ent_f->store_line("<key>com.apple.security.files.user-selected.read-write</key>");
ent_f->store_line("<true/>");
}
}

ent_f->store_line("</dict>");
Expand Down
Loading

0 comments on commit c2d0c52

Please sign in to comment.