Browse Source

Merge pull request #80104 from bruvzg/freedesktop_native_file_dialog

[Linux/Freedesktop] Implement native file selection dialog support.
Rémi Verschelde 2 years ago
parent
commit
c72b851dfb

+ 3 - 1
doc/classes/DisplayServer.xml

@@ -121,7 +121,9 @@
 				Displays OS native dialog for selecting files or directories in the file system.
 				Displays OS native dialog for selecting files or directories in the file system.
 				Callbacks have the following arguments: [code]bool status, PackedStringArray selected_paths[/code].
 				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 if the display server has the [code]FEATURE_NATIVE_DIALOG[/code] feature.
-				[b]Note:[/b] This method is implemented on Windows and macOS.
+				[b]Note:[/b] This method is implemented on Linux, Windows and macOS.
+				[b]Note:[/b] [param current_directory] might be ignored.
+				[b]Note:[/b] On Linux, [param show_hidden] is ignored.
 				[b]Note:[/b] On macOS, native file dialogs have no title.
 				[b]Note:[/b] On macOS, 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.
 				[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>
 			</description>

+ 1 - 1
doc/classes/OS.xml

@@ -538,7 +538,7 @@
 			<return type="bool" />
 			<return type="bool" />
 			<description>
 			<description>
 				Returns [code]true[/code] if application is running in the sandbox.
 				Returns [code]true[/code] if application is running in the sandbox.
-				[b]Note:[/b] This method is implemented on macOS.
+				[b]Note:[/b] This method is implemented on macOS and Linux.
 			</description>
 			</description>
 		</method>
 		</method>
 		<method name="is_stdout_verbose" qualifiers="const">
 		<method name="is_stdout_verbose" qualifiers="const">

+ 343 - 7
platform/linuxbsd/freedesktop_portal_desktop.cpp

@@ -32,6 +32,7 @@
 
 
 #ifdef DBUS_ENABLED
 #ifdef DBUS_ENABLED
 
 
+#include "core/crypto/crypto_core.h"
 #include "core/error/error_macros.h"
 #include "core/error/error_macros.h"
 #include "core/os/os.h"
 #include "core/os/os.h"
 #include "core/string/ustring.h"
 #include "core/string/ustring.h"
@@ -43,12 +44,15 @@
 #include <dbus/dbus.h>
 #include <dbus/dbus.h>
 #endif
 #endif
 
 
+#include <unistd.h>
+
 #define BUS_OBJECT_NAME "org.freedesktop.portal.Desktop"
 #define BUS_OBJECT_NAME "org.freedesktop.portal.Desktop"
 #define BUS_OBJECT_PATH "/org/freedesktop/portal/desktop"
 #define BUS_OBJECT_PATH "/org/freedesktop/portal/desktop"
 
 
 #define BUS_INTERFACE_SETTINGS "org.freedesktop.portal.Settings"
 #define BUS_INTERFACE_SETTINGS "org.freedesktop.portal.Settings"
+#define BUS_INTERFACE_FILE_CHOOSER "org.freedesktop.portal.FileChooser"
 
 
-static bool try_parse_variant(DBusMessage *p_reply_message, int p_type, void *r_value) {
+bool FreeDesktopPortalDesktop::try_parse_variant(DBusMessage *p_reply_message, int p_type, void *r_value) {
 	DBusMessageIter iter[3];
 	DBusMessageIter iter[3];
 
 
 	dbus_message_iter_init(p_reply_message, &iter[0]);
 	dbus_message_iter_init(p_reply_message, &iter[0]);
@@ -80,11 +84,11 @@ bool FreeDesktopPortalDesktop::read_setting(const char *p_namespace, const char
 
 
 	DBusConnection *bus = dbus_bus_get(DBUS_BUS_SESSION, &error);
 	DBusConnection *bus = dbus_bus_get(DBUS_BUS_SESSION, &error);
 	if (dbus_error_is_set(&error)) {
 	if (dbus_error_is_set(&error)) {
-		dbus_error_free(&error);
-		unsupported = true;
 		if (OS::get_singleton()->is_stdout_verbose()) {
 		if (OS::get_singleton()->is_stdout_verbose()) {
-			ERR_PRINT(String() + "Error opening D-Bus connection: " + error.message);
+			ERR_PRINT(vformat("Error opening D-Bus connection: %s", error.message));
 		}
 		}
+		dbus_error_free(&error);
+		unsupported = true;
 		return false;
 		return false;
 	}
 	}
 
 
@@ -100,11 +104,11 @@ bool FreeDesktopPortalDesktop::read_setting(const char *p_namespace, const char
 	DBusMessage *reply = dbus_connection_send_with_reply_and_block(bus, message, 50, &error);
 	DBusMessage *reply = dbus_connection_send_with_reply_and_block(bus, message, 50, &error);
 	dbus_message_unref(message);
 	dbus_message_unref(message);
 	if (dbus_error_is_set(&error)) {
 	if (dbus_error_is_set(&error)) {
-		dbus_error_free(&error);
-		dbus_connection_unref(bus);
 		if (OS::get_singleton()->is_stdout_verbose()) {
 		if (OS::get_singleton()->is_stdout_verbose()) {
-			ERR_PRINT(String() + "Error on D-Bus communication: " + error.message);
+			ERR_PRINT(vformat("Error on D-Bus communication: %s", error.message));
 		}
 		}
+		dbus_error_free(&error);
+		dbus_connection_unref(bus);
 		return false;
 		return false;
 	}
 	}
 
 
@@ -126,6 +130,317 @@ uint32_t FreeDesktopPortalDesktop::get_appearance_color_scheme() {
 	return value;
 	return value;
 }
 }
 
 
+static const char *cs_empty = "";
+
+void FreeDesktopPortalDesktop::append_dbus_string(DBusMessageIter *p_iter, const String &p_string) {
+	CharString cs = p_string.utf8();
+	const char *cs_ptr = cs.ptr();
+	if (cs_ptr) {
+		dbus_message_iter_append_basic(p_iter, DBUS_TYPE_STRING, &cs_ptr);
+	} else {
+		dbus_message_iter_append_basic(p_iter, DBUS_TYPE_STRING, &cs_empty);
+	}
+}
+
+void FreeDesktopPortalDesktop::append_dbus_dict_filters(DBusMessageIter *p_iter, const Vector<String> &p_filters) {
+	DBusMessageIter dict_iter;
+	DBusMessageIter var_iter;
+	DBusMessageIter arr_iter;
+	const char *filters_key = "filters";
+
+	dbus_message_iter_open_container(p_iter, DBUS_TYPE_DICT_ENTRY, nullptr, &dict_iter);
+	dbus_message_iter_append_basic(&dict_iter, DBUS_TYPE_STRING, &filters_key);
+	dbus_message_iter_open_container(&dict_iter, DBUS_TYPE_VARIANT, "a(sa(us))", &var_iter);
+	dbus_message_iter_open_container(&var_iter, DBUS_TYPE_ARRAY, "(sa(us))", &arr_iter);
+	for (int i = 0; i < p_filters.size(); i++) {
+		Vector<String> tokens = p_filters[i].split(";");
+		if (tokens.size() == 2) {
+			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, tokens[0]);
+
+			dbus_message_iter_open_container(&struct_iter, DBUS_TYPE_ARRAY, "(us)", &array_iter);
+			dbus_message_iter_open_container(&array_iter, DBUS_TYPE_STRUCT, nullptr, &array_struct_iter);
+			{
+				const unsigned nil = 0;
+				dbus_message_iter_append_basic(&array_struct_iter, DBUS_TYPE_UINT32, &nil);
+			}
+			append_dbus_string(&array_struct_iter, tokens[1]);
+			dbus_message_iter_close_container(&array_iter, &array_struct_iter);
+			dbus_message_iter_close_container(&struct_iter, &array_iter);
+			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_string(DBusMessageIter *p_iter, const String &p_key, const String &p_value, bool p_as_byte_array) {
+	DBusMessageIter dict_iter;
+	DBusMessageIter var_iter;
+	dbus_message_iter_open_container(p_iter, DBUS_TYPE_DICT_ENTRY, nullptr, &dict_iter);
+	append_dbus_string(&dict_iter, p_key);
+
+	if (p_as_byte_array) {
+		DBusMessageIter arr_iter;
+		dbus_message_iter_open_container(&dict_iter, DBUS_TYPE_VARIANT, "ay", &var_iter);
+		dbus_message_iter_open_container(&var_iter, DBUS_TYPE_ARRAY, "y", &arr_iter);
+		CharString cs = p_value.utf8();
+		const char *cs_ptr = cs.get_data();
+		do {
+			dbus_message_iter_append_basic(&arr_iter, DBUS_TYPE_BYTE, cs_ptr);
+		} while (*cs_ptr++);
+		dbus_message_iter_close_container(&var_iter, &arr_iter);
+	} else {
+		dbus_message_iter_open_container(&dict_iter, DBUS_TYPE_VARIANT, "s", &var_iter);
+		append_dbus_string(&var_iter, p_value);
+	}
+
+	dbus_message_iter_close_container(&dict_iter, &var_iter);
+	dbus_message_iter_close_container(p_iter, &dict_iter);
+}
+
+void FreeDesktopPortalDesktop::append_dbus_dict_bool(DBusMessageIter *p_iter, const String &p_key, bool p_value) {
+	DBusMessageIter dict_iter;
+	DBusMessageIter var_iter;
+	dbus_message_iter_open_container(p_iter, DBUS_TYPE_DICT_ENTRY, nullptr, &dict_iter);
+	append_dbus_string(&dict_iter, p_key);
+
+	dbus_message_iter_open_container(&dict_iter, DBUS_TYPE_VARIANT, "b", &var_iter);
+	{
+		int val = p_value;
+		dbus_message_iter_append_basic(&var_iter, DBUS_TYPE_BOOLEAN, &val);
+	}
+
+	dbus_message_iter_close_container(&dict_iter, &var_iter);
+	dbus_message_iter_close_container(p_iter, &dict_iter);
+}
+
+bool FreeDesktopPortalDesktop::file_chooser_parse_response(DBusMessageIter *p_iter, bool &r_cancel, Vector<String> &r_urls) {
+	ERR_FAIL_COND_V(dbus_message_iter_get_arg_type(p_iter) != DBUS_TYPE_UINT32, false);
+
+	dbus_uint32_t resp_code;
+	dbus_message_iter_get_basic(p_iter, &resp_code);
+	if (resp_code != 0) {
+		r_cancel = true;
+	} else {
+		r_cancel = false;
+		ERR_FAIL_COND_V(!dbus_message_iter_next(p_iter), false);
+		ERR_FAIL_COND_V(dbus_message_iter_get_arg_type(p_iter) != DBUS_TYPE_ARRAY, false);
+
+		DBusMessageIter dict_iter;
+		dbus_message_iter_recurse(p_iter, &dict_iter);
+		while (dbus_message_iter_get_arg_type(&dict_iter) == DBUS_TYPE_DICT_ENTRY) {
+			DBusMessageIter iter;
+			dbus_message_iter_recurse(&dict_iter, &iter);
+			if (dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_STRING) {
+				const char *key;
+				dbus_message_iter_get_basic(&iter, &key);
+				dbus_message_iter_next(&iter);
+
+				DBusMessageIter var_iter;
+				dbus_message_iter_recurse(&iter, &var_iter);
+				if (strcmp(key, "uris") == 0) {
+					if (dbus_message_iter_get_arg_type(&var_iter) == DBUS_TYPE_ARRAY) {
+						DBusMessageIter uri_iter;
+						dbus_message_iter_recurse(&var_iter, &uri_iter);
+						while (dbus_message_iter_get_arg_type(&uri_iter) == DBUS_TYPE_STRING) {
+							const char *value;
+							dbus_message_iter_get_basic(&uri_iter, &value);
+							r_urls.push_back(String::utf8(value).trim_prefix("file://").uri_decode());
+							if (!dbus_message_iter_next(&uri_iter)) {
+								break;
+							}
+						}
+					}
+				}
+			}
+			if (!dbus_message_iter_next(&dict_iter)) {
+				break;
+			}
+		}
+	}
+	return true;
+}
+
+Error FreeDesktopPortalDesktop::file_dialog_show(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) {
+	if (unsupported) {
+		return FAILED;
+	}
+
+	DBusError err;
+	dbus_error_init(&err);
+
+	// Open connection and add signal handler.
+	FileDialogData fd;
+	fd.callback = p_callback;
+
+	CryptoCore::RandomGenerator rng;
+	ERR_FAIL_COND_V_MSG(rng.init(), FAILED, "Failed to initialize random number generator.");
+	uint8_t uuid[64];
+	Error rng_err = rng.get_random_bytes(uuid, 64);
+	ERR_FAIL_COND_V_MSG(rng_err, rng_err, "Failed to generate unique token.");
+
+	fd.connection = dbus_bus_get(DBUS_BUS_SESSION, &err);
+	if (dbus_error_is_set(&err)) {
+		ERR_PRINT(vformat("Failed to open DBus connection: %s", err.message));
+		dbus_error_free(&err);
+		unsupported = true;
+		return FAILED;
+	}
+
+	String dbus_unique_name = String::utf8(dbus_bus_get_unique_name(fd.connection));
+	String token = String::hex_encode_buffer(uuid, 64);
+	String path = vformat("/org/freedesktop/portal/desktop/request/%s/%s", dbus_unique_name.replace(".", "_").replace(":", ""), token);
+
+	fd.path = vformat("type='signal',sender='org.freedesktop.portal.Desktop',path='%s',interface='org.freedesktop.portal.Request',member='Response',destination='%s'", path, dbus_unique_name);
+	dbus_bus_add_match(fd.connection, fd.path.utf8().get_data(), &err);
+	if (dbus_error_is_set(&err)) {
+		ERR_PRINT(vformat("Failed to add DBus match: %s", err.message));
+		dbus_error_free(&err);
+		dbus_connection_unref(fd.connection);
+		return FAILED;
+	}
+
+	// Generate FileChooser message.
+	const char *method = nullptr;
+	switch (p_mode) {
+		case DisplayServer::FILE_DIALOG_MODE_SAVE_FILE: {
+			method = "SaveFile";
+		} break;
+		case DisplayServer::FILE_DIALOG_MODE_OPEN_ANY:
+		case DisplayServer::FILE_DIALOG_MODE_OPEN_FILE:
+		case DisplayServer::FILE_DIALOG_MODE_OPEN_DIR:
+		case DisplayServer::FILE_DIALOG_MODE_OPEN_FILES: {
+			method = "OpenFile";
+		} break;
+	}
+
+	DBusMessage *message = dbus_message_new_method_call(BUS_OBJECT_NAME, BUS_OBJECT_PATH, BUS_INTERFACE_FILE_CHOOSER, method);
+	{
+		DBusMessageIter iter;
+		dbus_message_iter_init_append(message, &iter);
+
+		append_dbus_string(&iter, p_xid);
+		append_dbus_string(&iter, p_title);
+
+		DBusMessageIter arr_iter;
+		dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &arr_iter);
+
+		append_dbus_dict_string(&arr_iter, "handle_token", token);
+		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, p_filters);
+		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);
+		}
+
+		dbus_message_iter_close_container(&iter, &arr_iter);
+	}
+
+	DBusMessage *reply = dbus_connection_send_with_reply_and_block(fd.connection, message, DBUS_TIMEOUT_INFINITE, &err);
+	dbus_message_unref(message);
+
+	if (!reply || dbus_error_is_set(&err)) {
+		ERR_PRINT(vformat("Failed to send DBus message: %s", err.message));
+		dbus_error_free(&err);
+		dbus_bus_remove_match(fd.connection, fd.path.utf8().get_data(), &err);
+		dbus_connection_unref(fd.connection);
+		return FAILED;
+	}
+
+	// Update signal path.
+	{
+		DBusMessageIter iter;
+		if (dbus_message_iter_init(reply, &iter)) {
+			if (dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_OBJECT_PATH) {
+				const char *new_path = nullptr;
+				dbus_message_iter_get_basic(&iter, &new_path);
+				if (String::utf8(new_path) != path) {
+					dbus_bus_remove_match(fd.connection, fd.path.utf8().get_data(), &err);
+					if (dbus_error_is_set(&err)) {
+						ERR_PRINT(vformat("Failed to remove DBus match: %s", err.message));
+						dbus_error_free(&err);
+						dbus_connection_unref(fd.connection);
+						return FAILED;
+					}
+					fd.path = String::utf8(new_path);
+					dbus_bus_add_match(fd.connection, fd.path.utf8().get_data(), &err);
+					if (dbus_error_is_set(&err)) {
+						ERR_PRINT(vformat("Failed to add DBus match: %s", err.message));
+						dbus_error_free(&err);
+						dbus_connection_unref(fd.connection);
+						return FAILED;
+					}
+				}
+			}
+		}
+	}
+	dbus_message_unref(reply);
+
+	MutexLock lock(file_dialog_mutex);
+	file_dialogs.push_back(fd);
+
+	return OK;
+}
+
+void FreeDesktopPortalDesktop::_thread_file_dialog_monitor(void *p_ud) {
+	FreeDesktopPortalDesktop *portal = (FreeDesktopPortalDesktop *)p_ud;
+
+	while (!portal->file_dialog_thread_abort.is_set()) {
+		{
+			MutexLock lock(portal->file_dialog_mutex);
+			for (int i = portal->file_dialogs.size() - 1; i >= 0; i--) {
+				bool remove = false;
+				{
+					FreeDesktopPortalDesktop::FileDialogData &fd = portal->file_dialogs.write[i];
+					if (fd.connection) {
+						while (true) {
+							DBusMessage *msg = dbus_connection_pop_message(fd.connection);
+							if (!msg) {
+								break;
+							} else if (dbus_message_is_signal(msg, "org.freedesktop.portal.Request", "Response")) {
+								DBusMessageIter iter;
+								if (dbus_message_iter_init(msg, &iter)) {
+									bool cancel = false;
+									Vector<String> uris;
+									file_chooser_parse_response(&iter, cancel, uris);
+
+									if (fd.callback.is_valid()) {
+										Variant v_status = !cancel;
+										Variant v_files = uris;
+										Variant *v_args[2] = { &v_status, &v_files };
+										fd.callback.call_deferredp((const Variant **)&v_args, 2);
+									}
+								}
+								dbus_message_unref(msg);
+
+								DBusError err;
+								dbus_error_init(&err);
+								dbus_bus_remove_match(fd.connection, fd.path.utf8().get_data(), &err);
+								dbus_error_free(&err);
+								dbus_connection_unref(fd.connection);
+								remove = true;
+								break;
+							}
+							dbus_message_unref(msg);
+						}
+						dbus_connection_read_write(fd.connection, 0);
+					}
+				}
+				if (remove) {
+					portal->file_dialogs.remove_at(i);
+				}
+			}
+		}
+		usleep(50000);
+	}
+}
+
 FreeDesktopPortalDesktop::FreeDesktopPortalDesktop() {
 FreeDesktopPortalDesktop::FreeDesktopPortalDesktop() {
 #ifdef SOWRAP_ENABLED
 #ifdef SOWRAP_ENABLED
 #ifdef DEBUG_ENABLED
 #ifdef DEBUG_ENABLED
@@ -153,6 +468,27 @@ FreeDesktopPortalDesktop::FreeDesktopPortalDesktop() {
 		print_verbose("PortalDesktop: Unsupported DBus library version!");
 		print_verbose("PortalDesktop: Unsupported DBus library version!");
 		unsupported = true;
 		unsupported = true;
 	}
 	}
+
+	if (!unsupported) {
+		file_dialog_thread_abort.clear();
+		file_dialog_thread.start(FreeDesktopPortalDesktop::_thread_file_dialog_monitor, this);
+	}
+}
+
+FreeDesktopPortalDesktop::~FreeDesktopPortalDesktop() {
+	file_dialog_thread_abort.set();
+	if (file_dialog_thread.is_started()) {
+		file_dialog_thread.wait_to_finish();
+	}
+	for (FreeDesktopPortalDesktop::FileDialogData &fd : file_dialogs) {
+		if (fd.connection) {
+			DBusError err;
+			dbus_error_init(&err);
+			dbus_bus_remove_match(fd.connection, fd.path.utf8().get_data(), &err);
+			dbus_error_free(&err);
+			dbus_connection_unref(fd.connection);
+		}
+	}
 }
 }
 
 
 #endif // DBUS_ENABLED
 #endif // DBUS_ENABLED

+ 29 - 1
platform/linuxbsd/freedesktop_portal_desktop.h

@@ -33,20 +33,48 @@
 
 
 #ifdef DBUS_ENABLED
 #ifdef DBUS_ENABLED
 
 
-#include <stdint.h>
+#include "core/os/thread.h"
+#include "servers/display_server.h"
+
+struct DBusMessage;
+struct DBusConnection;
+struct DBusMessageIter;
 
 
 class FreeDesktopPortalDesktop {
 class FreeDesktopPortalDesktop {
 private:
 private:
 	bool unsupported = false;
 	bool unsupported = false;
 
 
+	static bool try_parse_variant(DBusMessage *p_reply_message, int p_type, void *r_value);
 	// Read a setting from org.freekdesktop.portal.Settings
 	// Read a setting from org.freekdesktop.portal.Settings
 	bool read_setting(const char *p_namespace, const char *p_key, int p_type, void *r_value);
 	bool read_setting(const char *p_namespace, const char *p_key, int p_type, void *r_value);
 
 
+	static void append_dbus_string(DBusMessageIter *p_iter, const String &p_string);
+	static void append_dbus_dict_filters(DBusMessageIter *p_iter, const Vector<String> &p_filters);
+	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, bool &r_cancel, Vector<String> &r_urls);
+
+	struct FileDialogData {
+		DBusConnection *connection = nullptr;
+		Callable callback;
+		String path;
+	};
+
+	Mutex file_dialog_mutex;
+	Vector<FileDialogData> file_dialogs;
+	Thread file_dialog_thread;
+	SafeFlag file_dialog_thread_abort;
+
+	static void _thread_file_dialog_monitor(void *p_ud);
+
 public:
 public:
 	FreeDesktopPortalDesktop();
 	FreeDesktopPortalDesktop();
+	~FreeDesktopPortalDesktop();
 
 
 	bool is_supported() { return !unsupported; }
 	bool is_supported() { return !unsupported; }
 
 
+	Error file_dialog_show(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);
+
 	// Retrieve the system's preferred color scheme.
 	// Retrieve the system's preferred color scheme.
 	// 0: No preference or unknown.
 	// 0: No preference or unknown.
 	// 1: Prefer dark appearance.
 	// 1: Prefer dark appearance.

+ 1 - 23
platform/linuxbsd/joypad_linux.cpp

@@ -82,31 +82,9 @@ void JoypadLinux::Joypad::reset() {
 	events.clear();
 	events.clear();
 }
 }
 
 
-#ifdef UDEV_ENABLED
-// This function is derived from SDL:
-// https://github.com/libsdl-org/SDL/blob/main/src/core/linux/SDL_sandbox.c#L28-L45
-static bool detect_sandbox() {
-	if (access("/.flatpak-info", F_OK) == 0) {
-		return true;
-	}
-
-	// For Snap, we check multiple variables because they might be set for
-	// unrelated reasons. This is the same thing WebKitGTK does.
-	if (OS::get_singleton()->has_environment("SNAP") && OS::get_singleton()->has_environment("SNAP_NAME") && OS::get_singleton()->has_environment("SNAP_REVISION")) {
-		return true;
-	}
-
-	if (access("/run/host/container-manager", F_OK) == 0) {
-		return true;
-	}
-
-	return false;
-}
-#endif // UDEV_ENABLED
-
 JoypadLinux::JoypadLinux(Input *in) {
 JoypadLinux::JoypadLinux(Input *in) {
 #ifdef UDEV_ENABLED
 #ifdef UDEV_ENABLED
-	if (detect_sandbox()) {
+	if (OS::get_singleton()->is_sandboxed()) {
 		// Linux binaries in sandboxes / containers need special handling because
 		// Linux binaries in sandboxes / containers need special handling because
 		// libudev doesn't work there. So we need to fallback to manual parsing
 		// libudev doesn't work there. So we need to fallback to manual parsing
 		// of /dev/input in such case.
 		// of /dev/input in such case.

+ 21 - 0
platform/linuxbsd/os_linuxbsd.cpp

@@ -164,6 +164,27 @@ String OS_LinuxBSD::get_processor_name() const {
 	ERR_FAIL_V_MSG("", String("Couldn't get the CPU model name from `/proc/cpuinfo`. Returning an empty string."));
 	ERR_FAIL_V_MSG("", String("Couldn't get the CPU model name from `/proc/cpuinfo`. Returning an empty string."));
 }
 }
 
 
+bool OS_LinuxBSD::is_sandboxed() const {
+	// This function is derived from SDL:
+	// https://github.com/libsdl-org/SDL/blob/main/src/core/linux/SDL_sandbox.c#L28-L45
+
+	if (access("/.flatpak-info", F_OK) == 0) {
+		return true;
+	}
+
+	// For Snap, we check multiple variables because they might be set for
+	// unrelated reasons. This is the same thing WebKitGTK does.
+	if (has_environment("SNAP") && has_environment("SNAP_NAME") && has_environment("SNAP_REVISION")) {
+		return true;
+	}
+
+	if (access("/run/host/container-manager", F_OK) == 0) {
+		return true;
+	}
+
+	return false;
+}
+
 void OS_LinuxBSD::finalize() {
 void OS_LinuxBSD::finalize() {
 	if (main_loop) {
 	if (main_loop) {
 		memdelete(main_loop);
 		memdelete(main_loop);

+ 2 - 0
platform/linuxbsd/os_linuxbsd.h

@@ -123,6 +123,8 @@ public:
 	virtual String get_unique_id() const override;
 	virtual String get_unique_id() const override;
 	virtual String get_processor_name() const override;
 	virtual String get_processor_name() const override;
 
 
+	virtual bool is_sandboxed() const override;
+
 	virtual void alert(const String &p_alert, const String &p_title = "ALERT!") override;
 	virtual void alert(const String &p_alert, const String &p_title = "ALERT!") override;
 
 
 	virtual bool _check_internal_feature_support(const String &p_feature) override;
 	virtual bool _check_internal_feature_support(const String &p_feature) override;

+ 14 - 0
platform/linuxbsd/x11/display_server_x11.cpp

@@ -122,6 +122,9 @@ bool DisplayServerX11::has_feature(Feature p_feature) const {
 		case FEATURE_WINDOW_TRANSPARENCY:
 		case FEATURE_WINDOW_TRANSPARENCY:
 		//case FEATURE_HIDPI:
 		//case FEATURE_HIDPI:
 		case FEATURE_ICON:
 		case FEATURE_ICON:
+#ifdef DBUS_ENABLED
+		case FEATURE_NATIVE_DIALOG:
+#endif
 		//case FEATURE_NATIVE_ICON:
 		//case FEATURE_NATIVE_ICON:
 		case FEATURE_SWAP_BUFFERS:
 		case FEATURE_SWAP_BUFFERS:
 #ifdef DBUS_ENABLED
 #ifdef DBUS_ENABLED
@@ -360,6 +363,17 @@ bool DisplayServerX11::is_dark_mode() const {
 	}
 	}
 }
 }
 
 
+Error DisplayServerX11::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) {
+	WindowID window_id = _get_focused_window_or_popup();
+
+	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(xid, p_title, p_current_directory, p_filename, p_mode, p_filters, p_callback);
+}
+
 #endif
 #endif
 
 
 void DisplayServerX11::mouse_set_mode(MouseMode p_mode) {
 void DisplayServerX11::mouse_set_mode(MouseMode p_mode) {

+ 2 - 0
platform/linuxbsd/x11/display_server_x11.h

@@ -393,6 +393,8 @@ public:
 #if defined(DBUS_ENABLED)
 #if defined(DBUS_ENABLED)
 	virtual bool is_dark_mode_supported() const override;
 	virtual bool is_dark_mode_supported() const override;
 	virtual bool is_dark_mode() const override;
 	virtual bool is_dark_mode() const override;
+
+	virtual Error file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback) override;
 #endif
 #endif
 
 
 	virtual void mouse_set_mode(MouseMode p_mode) override;
 	virtual void mouse_set_mode(MouseMode p_mode) override;