Path: blob/master/platform/linuxbsd/freedesktop_portal_desktop.cpp
21209 views
/**************************************************************************/1/* freedesktop_portal_desktop.cpp */2/**************************************************************************/3/* This file is part of: */4/* GODOT ENGINE */5/* https://godotengine.org */6/**************************************************************************/7/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */8/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */9/* */10/* Permission is hereby granted, free of charge, to any person obtaining */11/* a copy of this software and associated documentation files (the */12/* "Software"), to deal in the Software without restriction, including */13/* without limitation the rights to use, copy, modify, merge, publish, */14/* distribute, sublicense, and/or sell copies of the Software, and to */15/* permit persons to whom the Software is furnished to do so, subject to */16/* the following conditions: */17/* */18/* The above copyright notice and this permission notice shall be */19/* included in all copies or substantial portions of the Software. */20/* */21/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */22/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */23/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */24/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */25/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */26/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */27/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */28/**************************************************************************/2930#include "freedesktop_portal_desktop.h"3132#ifdef DBUS_ENABLED3334#include "core/crypto/crypto_core.h"35#include "core/error/error_macros.h"36#include "core/os/os.h"37#include "core/string/ustring.h"38#include "core/variant/variant.h"3940#ifdef SOWRAP_ENABLED41#include "dbus-so_wrap.h"42#else43#include <dbus/dbus.h>44#endif4546#include <unistd.h>4748#define BUS_OBJECT_NAME "org.freedesktop.portal.Desktop"49#define BUS_OBJECT_PATH "/org/freedesktop/portal/desktop"5051#define BUS_INTERFACE_PROPERTIES "org.freedesktop.DBus.Properties"52#define BUS_INTERFACE_SETTINGS "org.freedesktop.portal.Settings"53#define BUS_INTERFACE_FILE_CHOOSER "org.freedesktop.portal.FileChooser"54#define BUS_INTERFACE_SCREENSHOT "org.freedesktop.portal.Screenshot"55#define BUS_INTERFACE_INHIBIT "org.freedesktop.portal.Inhibit"56#define BUS_INTERFACE_REQUEST "org.freedesktop.portal.Request"5758#define INHIBIT_FLAG_IDLE 85960bool FreeDesktopPortalDesktop::try_parse_variant(DBusMessage *p_reply_message, ReadVariantType p_type, void *r_value) {61DBusMessageIter iter[3];6263dbus_message_iter_init(p_reply_message, &iter[0]);64if (dbus_message_iter_get_arg_type(&iter[0]) != DBUS_TYPE_VARIANT) {65return false;66}6768dbus_message_iter_recurse(&iter[0], &iter[1]);69if (dbus_message_iter_get_arg_type(&iter[1]) != DBUS_TYPE_VARIANT) {70return false;71}7273dbus_message_iter_recurse(&iter[1], &iter[2]);74if (p_type == VAR_TYPE_COLOR) {75if (dbus_message_iter_get_arg_type(&iter[2]) != DBUS_TYPE_STRUCT) {76return false;77}78DBusMessageIter struct_iter;79dbus_message_iter_recurse(&iter[2], &struct_iter);80int idx = 0;81while (dbus_message_iter_get_arg_type(&struct_iter) == DBUS_TYPE_DOUBLE) {82double value = 0.0;83dbus_message_iter_get_basic(&struct_iter, &value);84if (value < 0.0 || value > 1.0) {85return false;86}87if (idx == 0) {88static_cast<Color *>(r_value)->r = value;89} else if (idx == 1) {90static_cast<Color *>(r_value)->g = value;91} else if (idx == 2) {92static_cast<Color *>(r_value)->b = value;93}94idx++;95if (!dbus_message_iter_next(&struct_iter)) {96break;97}98}99if (idx != 3) {100return false;101}102} else if (p_type == VAR_TYPE_UINT32) {103if (dbus_message_iter_get_arg_type(&iter[2]) != DBUS_TYPE_UINT32) {104return false;105}106dbus_message_iter_get_basic(&iter[2], r_value);107} else if (p_type == VAR_TYPE_BOOL) {108if (dbus_message_iter_get_arg_type(&iter[2]) != DBUS_TYPE_BOOLEAN) {109return false;110}111dbus_message_iter_get_basic(&iter[2], r_value);112}113return true;114}115116bool FreeDesktopPortalDesktop::read_setting(const char *p_namespace, const char *p_key, ReadVariantType p_type, void *r_value) {117if (unsupported) {118return false;119}120121DBusError error;122dbus_error_init(&error);123124DBusConnection *bus = dbus_bus_get(DBUS_BUS_SESSION, &error);125if (dbus_error_is_set(&error)) {126if (OS::get_singleton()->is_stdout_verbose()) {127ERR_PRINT(vformat("Error opening D-Bus connection: %s", String::utf8(error.message)));128}129dbus_error_free(&error);130unsupported = true;131return false;132}133134DBusMessage *message = dbus_message_new_method_call(135BUS_OBJECT_NAME, BUS_OBJECT_PATH, BUS_INTERFACE_SETTINGS,136"Read");137dbus_message_append_args(138message,139DBUS_TYPE_STRING, &p_namespace,140DBUS_TYPE_STRING, &p_key,141DBUS_TYPE_INVALID);142143DBusMessage *reply = dbus_connection_send_with_reply_and_block(bus, message, 50, &error);144dbus_message_unref(message);145if (dbus_error_is_set(&error)) {146if (OS::get_singleton()->is_stdout_verbose()) {147ERR_PRINT(vformat("Failed to read setting %s %s: %s", p_namespace, p_key, String::utf8(error.message)));148}149dbus_error_free(&error);150dbus_connection_unref(bus);151return false;152}153154bool success = try_parse_variant(reply, p_type, r_value);155156dbus_message_unref(reply);157dbus_connection_unref(bus);158159return success;160}161162uint32_t FreeDesktopPortalDesktop::get_appearance_color_scheme() {163if (unsupported) {164return 0;165}166167uint32_t value = 0;168if (read_setting("org.freedesktop.appearance", "color-scheme", VAR_TYPE_UINT32, &value)) {169return value;170} else {171return 0;172}173}174175Color FreeDesktopPortalDesktop::get_appearance_accent_color() {176if (unsupported) {177return Color(0, 0, 0, 0);178}179180Color value;181if (read_setting("org.freedesktop.appearance", "accent-color", VAR_TYPE_COLOR, &value)) {182return value;183} else {184return Color(0, 0, 0, 0);185}186}187188uint32_t FreeDesktopPortalDesktop::get_high_contrast() {189if (unsupported) {190return -1;191}192193dbus_bool_t value = false;194if (read_setting("org.gnome.desktop.a11y.interface", "high-contrast", VAR_TYPE_BOOL, &value)) {195return value;196}197return -1;198}199200static const char *cs_empty = "";201202void FreeDesktopPortalDesktop::append_dbus_string(DBusMessageIter *p_iter, const String &p_string) {203CharString cs = p_string.utf8();204const char *cs_ptr = cs.ptr();205if (cs_ptr) {206dbus_message_iter_append_basic(p_iter, DBUS_TYPE_STRING, &cs_ptr);207} else {208dbus_message_iter_append_basic(p_iter, DBUS_TYPE_STRING, &cs_empty);209}210}211212void FreeDesktopPortalDesktop::append_dbus_dict_options(DBusMessageIter *p_iter, const TypedArray<Dictionary> &p_options, HashMap<String, String> &r_ids) {213DBusMessageIter dict_iter;214DBusMessageIter var_iter;215DBusMessageIter arr_iter;216const char *choices_key = "choices";217218dbus_message_iter_open_container(p_iter, DBUS_TYPE_DICT_ENTRY, nullptr, &dict_iter);219dbus_message_iter_append_basic(&dict_iter, DBUS_TYPE_STRING, &choices_key);220dbus_message_iter_open_container(&dict_iter, DBUS_TYPE_VARIANT, "a(ssa(ss)s)", &var_iter);221dbus_message_iter_open_container(&var_iter, DBUS_TYPE_ARRAY, "(ss(ss)s)", &arr_iter);222223r_ids.clear();224for (int i = 0; i < p_options.size(); i++) {225const Dictionary &item = p_options[i];226if (!item.has("name") || !item.has("values") || !item.has("default")) {227continue;228}229const String &name = item["name"];230const Vector<String> &options = item["values"];231int default_idx = item["default"];232233DBusMessageIter struct_iter;234DBusMessageIter array_iter;235DBusMessageIter array_struct_iter;236dbus_message_iter_open_container(&arr_iter, DBUS_TYPE_STRUCT, nullptr, &struct_iter);237append_dbus_string(&struct_iter, "option_" + itos(i)); // ID.238append_dbus_string(&struct_iter, name); // User visible name.239r_ids["option_" + itos(i)] = name;240241dbus_message_iter_open_container(&struct_iter, DBUS_TYPE_ARRAY, "(ss)", &array_iter);242for (int j = 0; j < options.size(); j++) {243dbus_message_iter_open_container(&array_iter, DBUS_TYPE_STRUCT, nullptr, &array_struct_iter);244append_dbus_string(&array_struct_iter, itos(j));245append_dbus_string(&array_struct_iter, options[j]);246dbus_message_iter_close_container(&array_iter, &array_struct_iter);247}248dbus_message_iter_close_container(&struct_iter, &array_iter);249if (options.is_empty()) {250append_dbus_string(&struct_iter, (default_idx) ? "true" : "false"); // Default selection.251} else {252append_dbus_string(&struct_iter, itos(default_idx)); // Default selection.253}254255dbus_message_iter_close_container(&arr_iter, &struct_iter);256}257dbus_message_iter_close_container(&var_iter, &arr_iter);258dbus_message_iter_close_container(&dict_iter, &var_iter);259dbus_message_iter_close_container(p_iter, &dict_iter);260}261262void FreeDesktopPortalDesktop::append_dbus_dict_filters(DBusMessageIter *p_iter, const Vector<String> &p_filter_names, const Vector<String> &p_filter_exts, const Vector<String> &p_filter_mimes) {263DBusMessageIter dict_iter;264DBusMessageIter var_iter;265DBusMessageIter arr_iter;266const char *filters_key = "filters";267268ERR_FAIL_COND(p_filter_names.size() != p_filter_exts.size());269ERR_FAIL_COND(p_filter_names.size() != p_filter_mimes.size());270271dbus_message_iter_open_container(p_iter, DBUS_TYPE_DICT_ENTRY, nullptr, &dict_iter);272dbus_message_iter_append_basic(&dict_iter, DBUS_TYPE_STRING, &filters_key);273dbus_message_iter_open_container(&dict_iter, DBUS_TYPE_VARIANT, "a(sa(us))", &var_iter);274dbus_message_iter_open_container(&var_iter, DBUS_TYPE_ARRAY, "(sa(us))", &arr_iter);275for (int i = 0; i < p_filter_names.size(); i++) {276DBusMessageIter struct_iter;277DBusMessageIter array_iter;278DBusMessageIter array_struct_iter;279dbus_message_iter_open_container(&arr_iter, DBUS_TYPE_STRUCT, nullptr, &struct_iter);280append_dbus_string(&struct_iter, p_filter_names[i]);281282dbus_message_iter_open_container(&struct_iter, DBUS_TYPE_ARRAY, "(us)", &array_iter);283const String &flt_orig = p_filter_exts[i];284String flt;285for (int j = 0; j < flt_orig.length(); j++) {286if (is_unicode_letter(flt_orig[j])) {287flt += vformat("[%c%c]", String::char_lowercase(flt_orig[j]), String::char_uppercase(flt_orig[j]));288} else {289flt += flt_orig[j];290}291}292int filter_slice_count = flt.get_slice_count(",");293for (int j = 0; j < filter_slice_count; j++) {294dbus_message_iter_open_container(&array_iter, DBUS_TYPE_STRUCT, nullptr, &array_struct_iter);295String str = (flt.get_slicec(',', j).strip_edges());296{297const unsigned flt_type = 0;298dbus_message_iter_append_basic(&array_struct_iter, DBUS_TYPE_UINT32, &flt_type);299}300append_dbus_string(&array_struct_iter, str);301dbus_message_iter_close_container(&array_iter, &array_struct_iter);302}303const String &mime = p_filter_mimes[i];304filter_slice_count = mime.get_slice_count(",");305for (int j = 0; j < filter_slice_count; j++) {306dbus_message_iter_open_container(&array_iter, DBUS_TYPE_STRUCT, nullptr, &array_struct_iter);307String str = mime.get_slicec(',', j).strip_edges();308{309const unsigned flt_type = 1;310dbus_message_iter_append_basic(&array_struct_iter, DBUS_TYPE_UINT32, &flt_type);311}312append_dbus_string(&array_struct_iter, str);313dbus_message_iter_close_container(&array_iter, &array_struct_iter);314}315dbus_message_iter_close_container(&struct_iter, &array_iter);316dbus_message_iter_close_container(&arr_iter, &struct_iter);317}318dbus_message_iter_close_container(&var_iter, &arr_iter);319dbus_message_iter_close_container(&dict_iter, &var_iter);320dbus_message_iter_close_container(p_iter, &dict_iter);321}322323void FreeDesktopPortalDesktop::append_dbus_dict_string(DBusMessageIter *p_iter, const String &p_key, const String &p_value, bool p_as_byte_array) {324DBusMessageIter dict_iter;325DBusMessageIter var_iter;326dbus_message_iter_open_container(p_iter, DBUS_TYPE_DICT_ENTRY, nullptr, &dict_iter);327append_dbus_string(&dict_iter, p_key);328329if (p_as_byte_array) {330DBusMessageIter arr_iter;331dbus_message_iter_open_container(&dict_iter, DBUS_TYPE_VARIANT, "ay", &var_iter);332dbus_message_iter_open_container(&var_iter, DBUS_TYPE_ARRAY, "y", &arr_iter);333CharString cs = p_value.utf8();334const char *cs_ptr = cs.get_data();335do {336dbus_message_iter_append_basic(&arr_iter, DBUS_TYPE_BYTE, cs_ptr);337} while (*cs_ptr++);338dbus_message_iter_close_container(&var_iter, &arr_iter);339} else {340dbus_message_iter_open_container(&dict_iter, DBUS_TYPE_VARIANT, "s", &var_iter);341append_dbus_string(&var_iter, p_value);342}343344dbus_message_iter_close_container(&dict_iter, &var_iter);345dbus_message_iter_close_container(p_iter, &dict_iter);346}347348void FreeDesktopPortalDesktop::append_dbus_dict_bool(DBusMessageIter *p_iter, const String &p_key, bool p_value) {349DBusMessageIter dict_iter;350DBusMessageIter var_iter;351dbus_message_iter_open_container(p_iter, DBUS_TYPE_DICT_ENTRY, nullptr, &dict_iter);352append_dbus_string(&dict_iter, p_key);353354dbus_message_iter_open_container(&dict_iter, DBUS_TYPE_VARIANT, "b", &var_iter);355{356int val = p_value;357dbus_message_iter_append_basic(&var_iter, DBUS_TYPE_BOOLEAN, &val);358}359360dbus_message_iter_close_container(&dict_iter, &var_iter);361dbus_message_iter_close_container(p_iter, &dict_iter);362}363364bool FreeDesktopPortalDesktop::color_picker_parse_response(DBusMessageIter *p_iter, bool &r_cancel, Color &r_color) {365ERR_FAIL_COND_V(dbus_message_iter_get_arg_type(p_iter) != DBUS_TYPE_UINT32, false);366367dbus_uint32_t resp_code;368dbus_message_iter_get_basic(p_iter, &resp_code);369if (resp_code != 0) {370r_cancel = true;371} else {372r_cancel = false;373ERR_FAIL_COND_V(!dbus_message_iter_next(p_iter), false);374ERR_FAIL_COND_V(dbus_message_iter_get_arg_type(p_iter) != DBUS_TYPE_ARRAY, false);375376DBusMessageIter dict_iter;377dbus_message_iter_recurse(p_iter, &dict_iter);378while (dbus_message_iter_get_arg_type(&dict_iter) == DBUS_TYPE_DICT_ENTRY) {379DBusMessageIter iter;380dbus_message_iter_recurse(&dict_iter, &iter);381if (dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_STRING) {382const char *key;383dbus_message_iter_get_basic(&iter, &key);384dbus_message_iter_next(&iter);385386DBusMessageIter var_iter;387dbus_message_iter_recurse(&iter, &var_iter);388if (strcmp(key, "color") == 0) { // (ddd)389if (dbus_message_iter_get_arg_type(&var_iter) == DBUS_TYPE_STRUCT) {390DBusMessageIter struct_iter;391dbus_message_iter_recurse(&var_iter, &struct_iter);392int idx = 0;393while (dbus_message_iter_get_arg_type(&struct_iter) == DBUS_TYPE_DOUBLE) {394double value = 0.0;395dbus_message_iter_get_basic(&struct_iter, &value);396if (idx == 0) {397r_color.r = value;398} else if (idx == 1) {399r_color.g = value;400} else if (idx == 2) {401r_color.b = value;402}403idx++;404if (!dbus_message_iter_next(&struct_iter)) {405break;406}407}408}409}410}411if (!dbus_message_iter_next(&dict_iter)) {412break;413}414}415}416return true;417}418419bool FreeDesktopPortalDesktop::file_chooser_parse_response(DBusMessageIter *p_iter, const Vector<String> &p_names, const HashMap<String, String> &p_ids, bool &r_cancel, Vector<String> &r_urls, int &r_index, Dictionary &r_options) {420ERR_FAIL_COND_V(dbus_message_iter_get_arg_type(p_iter) != DBUS_TYPE_UINT32, false);421422dbus_uint32_t resp_code;423dbus_message_iter_get_basic(p_iter, &resp_code);424if (resp_code != 0) {425r_cancel = true;426} else {427r_cancel = false;428ERR_FAIL_COND_V(!dbus_message_iter_next(p_iter), false);429ERR_FAIL_COND_V(dbus_message_iter_get_arg_type(p_iter) != DBUS_TYPE_ARRAY, false);430431DBusMessageIter dict_iter;432dbus_message_iter_recurse(p_iter, &dict_iter);433while (dbus_message_iter_get_arg_type(&dict_iter) == DBUS_TYPE_DICT_ENTRY) {434DBusMessageIter iter;435dbus_message_iter_recurse(&dict_iter, &iter);436if (dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_STRING) {437const char *key;438dbus_message_iter_get_basic(&iter, &key);439dbus_message_iter_next(&iter);440441DBusMessageIter var_iter;442dbus_message_iter_recurse(&iter, &var_iter);443if (strcmp(key, "current_filter") == 0) { // (sa(us))444if (dbus_message_iter_get_arg_type(&var_iter) == DBUS_TYPE_STRUCT) {445DBusMessageIter struct_iter;446dbus_message_iter_recurse(&var_iter, &struct_iter);447while (dbus_message_iter_get_arg_type(&struct_iter) == DBUS_TYPE_STRING) {448const char *value;449dbus_message_iter_get_basic(&struct_iter, &value);450String name = String::utf8(value);451452r_index = p_names.find(name);453if (!dbus_message_iter_next(&struct_iter)) {454break;455}456}457}458} else if (strcmp(key, "choices") == 0) { // a(ss) {459if (dbus_message_iter_get_arg_type(&var_iter) == DBUS_TYPE_ARRAY) {460DBusMessageIter struct_iter;461dbus_message_iter_recurse(&var_iter, &struct_iter);462while (dbus_message_iter_get_arg_type(&struct_iter) == DBUS_TYPE_STRUCT) {463DBusMessageIter opt_iter;464dbus_message_iter_recurse(&struct_iter, &opt_iter);465const char *opt_key = nullptr;466dbus_message_iter_get_basic(&opt_iter, &opt_key);467String opt_skey = String::utf8(opt_key);468dbus_message_iter_next(&opt_iter);469const char *opt_val = nullptr;470dbus_message_iter_get_basic(&opt_iter, &opt_val);471String opt_sval = String::utf8(opt_val);472473if (p_ids.has(opt_skey)) {474opt_skey = p_ids[opt_skey];475if (opt_sval == "true") {476r_options[opt_skey] = true;477} else if (opt_sval == "false") {478r_options[opt_skey] = false;479} else {480r_options[opt_skey] = opt_sval.to_int();481}482}483484if (!dbus_message_iter_next(&struct_iter)) {485break;486}487}488}489} else if (strcmp(key, "uris") == 0) { // as490if (dbus_message_iter_get_arg_type(&var_iter) == DBUS_TYPE_ARRAY) {491DBusMessageIter uri_iter;492dbus_message_iter_recurse(&var_iter, &uri_iter);493while (dbus_message_iter_get_arg_type(&uri_iter) == DBUS_TYPE_STRING) {494const char *value;495dbus_message_iter_get_basic(&uri_iter, &value);496r_urls.push_back(String::utf8(value).trim_prefix("file://").uri_file_decode());497if (!dbus_message_iter_next(&uri_iter)) {498break;499}500}501}502}503}504if (!dbus_message_iter_next(&dict_iter)) {505break;506}507}508}509return true;510}511512bool FreeDesktopPortalDesktop::color_picker(const String &p_xid, const Callable &p_callback) {513if (unsupported) {514return false;515}516517// Open connection and add signal handler.518ColorPickerData cd;519cd.callback = p_callback;520521String token;522if (make_request_token(token) != OK) {523return false;524}525526DBusMessage *message = dbus_message_new_method_call(BUS_OBJECT_NAME, BUS_OBJECT_PATH, BUS_INTERFACE_SCREENSHOT, "PickColor");527{528DBusMessageIter iter;529dbus_message_iter_init_append(message, &iter);530531append_dbus_string(&iter, p_xid);532533DBusMessageIter arr_iter;534dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &arr_iter);535append_dbus_dict_string(&arr_iter, "handle_token", token);536dbus_message_iter_close_container(&iter, &arr_iter);537}538539if (!send_request(message, token, cd.path, cd.filter)) {540return false;541}542543MutexLock lock(color_picker_mutex);544color_pickers.push_back(cd);545546return true;547}548549bool FreeDesktopPortalDesktop::_is_interface_supported(const char *p_iface, uint32_t p_minimum_version) {550bool supported = false;551DBusError err;552dbus_error_init(&err);553DBusConnection *bus = dbus_bus_get(DBUS_BUS_SESSION, &err);554if (dbus_error_is_set(&err)) {555dbus_error_free(&err);556} else {557DBusMessage *message = dbus_message_new_method_call(BUS_OBJECT_NAME, BUS_OBJECT_PATH, BUS_INTERFACE_PROPERTIES, "Get");558if (message) {559const char *name_space = p_iface;560const char *key = "version";561dbus_message_append_args(562message,563DBUS_TYPE_STRING, &name_space,564DBUS_TYPE_STRING, &key,565DBUS_TYPE_INVALID);566DBusMessage *reply = dbus_connection_send_with_reply_and_block(bus, message, 250, &err);567if (dbus_error_is_set(&err)) {568dbus_error_free(&err);569} else if (reply) {570DBusMessageIter iter;571if (dbus_message_iter_init(reply, &iter)) {572DBusMessageIter iter_ver;573dbus_message_iter_recurse(&iter, &iter_ver);574dbus_uint32_t ver_code;575dbus_message_iter_get_basic(&iter_ver, &ver_code);576print_verbose(vformat("PortalDesktop: %s version %d detected, version %d required.", p_iface, ver_code, p_minimum_version));577supported = ver_code >= p_minimum_version;578}579dbus_message_unref(reply);580}581dbus_message_unref(message);582}583dbus_connection_unref(bus);584}585return supported;586}587588bool FreeDesktopPortalDesktop::is_file_chooser_supported() {589static int supported = -1;590if (supported == -1) {591supported = _is_interface_supported(BUS_INTERFACE_FILE_CHOOSER, 3);592}593return supported;594}595596bool FreeDesktopPortalDesktop::is_settings_supported() {597static int supported = -1;598if (supported == -1) {599supported = _is_interface_supported(BUS_INTERFACE_SETTINGS, 1);600}601return supported;602}603604bool FreeDesktopPortalDesktop::is_screenshot_supported() {605static int supported = -1;606if (supported == -1) {607supported = _is_interface_supported(BUS_INTERFACE_SCREENSHOT, 1);608}609return supported;610}611612bool FreeDesktopPortalDesktop::is_inhibit_supported() {613static int supported = -1;614if (supported == -1) {615// If not sandboxed, prefer to use org.freedesktop.ScreenSaver616supported = OS::get_singleton()->is_sandboxed() && _is_interface_supported(BUS_INTERFACE_INHIBIT, 1);617}618return supported;619}620621Error FreeDesktopPortalDesktop::make_request_token(String &r_token) {622CryptoCore::RandomGenerator rng;623ERR_FAIL_COND_V_MSG(rng.init(), FAILED, "Failed to initialize random number generator.");624uint8_t uuid[64];625Error rng_err = rng.get_random_bytes(uuid, 64);626ERR_FAIL_COND_V_MSG(rng_err, rng_err, "Failed to generate unique token.");627628r_token = String::hex_encode_buffer(uuid, 64);629return OK;630}631632bool FreeDesktopPortalDesktop::send_request(DBusMessage *p_message, const String &r_token, String &r_response_path, String &r_response_filter) {633String dbus_unique_name = String::utf8(dbus_bus_get_unique_name(monitor_connection));634635r_response_path = vformat("/org/freedesktop/portal/desktop/request/%s/%s", dbus_unique_name.replace_char('.', '_').remove_char(':'), r_token);636r_response_filter = vformat("type='signal',sender='org.freedesktop.portal.Desktop',path='%s',interface='org.freedesktop.portal.Request',member='Response',destination='%s'", r_response_path, dbus_unique_name);637638DBusError err;639dbus_error_init(&err);640641dbus_bus_add_match(monitor_connection, r_response_filter.utf8().get_data(), &err);642if (dbus_error_is_set(&err)) {643ERR_PRINT(vformat("Failed to add DBus match: %s.", String::utf8(err.message)));644dbus_error_free(&err);645return false;646}647648DBusMessage *reply = dbus_connection_send_with_reply_and_block(monitor_connection, p_message, DBUS_TIMEOUT_INFINITE, &err);649dbus_message_unref(p_message);650651if (!reply || dbus_error_is_set(&err)) {652ERR_PRINT(vformat("Failed to send DBus message: %s.", String::utf8(err.message)));653dbus_error_free(&err);654dbus_bus_remove_match(monitor_connection, r_response_filter.utf8().get_data(), &err);655return false;656}657658// Check request path matches our expectation659{660DBusMessageIter iter;661if (dbus_message_iter_init(reply, &iter)) {662if (dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_OBJECT_PATH) {663const char *new_path = nullptr;664dbus_message_iter_get_basic(&iter, &new_path);665if (String::utf8(new_path) != r_response_path) {666ERR_PRINT(vformat("Expected request path %s but actual path was %s.", r_response_path, new_path));667dbus_bus_remove_match(monitor_connection, r_response_filter.utf8().get_data(), &err);668if (dbus_error_is_set(&err)) {669ERR_PRINT(vformat("Failed to remove DBus match: %s.", String::utf8(err.message)));670dbus_error_free(&err);671}672return false;673}674}675}676}677dbus_message_unref(reply);678return true;679}680681Error 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) {682if (unsupported) {683return FAILED;684}685686ERR_FAIL_INDEX_V(int(p_mode), DisplayServer::FILE_DIALOG_MODE_SAVE_MAX, FAILED);687ERR_FAIL_NULL_V(monitor_connection, FAILED);688689Vector<String> filter_names;690Vector<String> filter_exts;691Vector<String> filter_mimes;692for (int i = 0; i < p_filters.size(); i++) {693Vector<String> tokens = p_filters[i].split(";");694if (tokens.size() >= 1) {695String flt = tokens[0].strip_edges();696String mime = (tokens.size() >= 3) ? tokens[2].strip_edges() : String();697if (!flt.is_empty() || !mime.is_empty()) {698if (tokens.size() >= 2) {699if (flt == "*.*") {700filter_exts.push_back("*");701} else {702filter_exts.push_back(flt);703}704filter_mimes.push_back(mime);705filter_names.push_back(tokens[1]);706} else {707if (flt == "*.*") {708filter_exts.push_back("*");709filter_names.push_back(RTR("All Files") + " (*.*)");710} else {711filter_exts.push_back(flt);712filter_names.push_back(flt);713}714filter_mimes.push_back(mime);715}716}717}718}719if (filter_names.is_empty()) {720filter_exts.push_back("*");721filter_mimes.push_back("");722filter_names.push_back(RTR("All Files") + " (*.*)");723}724725// Open connection and add signal handler.726FileDialogData fd;727fd.callback = p_callback;728fd.prev_focus = p_window_id;729fd.filter_names = filter_names;730fd.opt_in_cb = p_options_in_cb;731732String token;733Error err = make_request_token(token);734if (err != OK) {735return err;736}737738// Generate FileChooser message.739const char *method = nullptr;740if (p_mode == DisplayServer::FILE_DIALOG_MODE_SAVE_FILE) {741method = "SaveFile";742} else {743method = "OpenFile";744}745746DBusMessage *message = dbus_message_new_method_call(BUS_OBJECT_NAME, BUS_OBJECT_PATH, BUS_INTERFACE_FILE_CHOOSER, method);747{748DBusMessageIter iter;749dbus_message_iter_init_append(message, &iter);750751append_dbus_string(&iter, p_xid);752append_dbus_string(&iter, p_title);753754DBusMessageIter arr_iter;755dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &arr_iter);756757append_dbus_dict_string(&arr_iter, "handle_token", token);758append_dbus_dict_bool(&arr_iter, "multiple", p_mode == DisplayServer::FILE_DIALOG_MODE_OPEN_FILES);759append_dbus_dict_bool(&arr_iter, "directory", p_mode == DisplayServer::FILE_DIALOG_MODE_OPEN_DIR);760append_dbus_dict_filters(&arr_iter, filter_names, filter_exts, filter_mimes);761762append_dbus_dict_options(&arr_iter, p_options, fd.option_ids);763append_dbus_dict_string(&arr_iter, "current_folder", p_current_directory, true);764if (p_mode == DisplayServer::FILE_DIALOG_MODE_SAVE_FILE) {765append_dbus_dict_string(&arr_iter, "current_name", p_filename);766}767768dbus_message_iter_close_container(&iter, &arr_iter);769}770771if (!send_request(message, token, fd.path, fd.filter)) {772return FAILED;773}774775MutexLock lock(file_dialog_mutex);776file_dialogs.push_back(fd);777778return OK;779}780781bool FreeDesktopPortalDesktop::inhibit(const String &p_xid) {782if (unsupported) {783return false;784}785786MutexLock lock(inhibit_mutex);787ERR_FAIL_COND_V_MSG(!inhibit_path.is_empty(), false, "Another inhibit request is already open.");788789String token;790if (make_request_token(token) != OK) {791return false;792}793794DBusMessage *message = dbus_message_new_method_call(BUS_OBJECT_NAME, BUS_OBJECT_PATH, BUS_INTERFACE_INHIBIT, "Inhibit");795{796DBusMessageIter iter;797dbus_message_iter_init_append(message, &iter);798799append_dbus_string(&iter, p_xid);800801dbus_uint32_t flags = INHIBIT_FLAG_IDLE;802dbus_message_iter_append_basic(&iter, DBUS_TYPE_UINT32, &flags);803804{805DBusMessageIter arr_iter;806dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &arr_iter);807808append_dbus_dict_string(&arr_iter, "handle_token", token);809810const char *reason = "Running Godot Engine Project";811append_dbus_dict_string(&arr_iter, "reason", reason);812813dbus_message_iter_close_container(&iter, &arr_iter);814}815}816817if (!send_request(message, token, inhibit_path, inhibit_filter)) {818return false;819}820821return true;822}823824void FreeDesktopPortalDesktop::uninhibit() {825if (unsupported) {826return;827}828829MutexLock lock(inhibit_mutex);830ERR_FAIL_COND_MSG(inhibit_path.is_empty(), "No inhibit request is active.");831832DBusError error;833dbus_error_init(&error);834835DBusMessage *message = dbus_message_new_method_call(BUS_OBJECT_NAME, inhibit_path.utf8().get_data(), BUS_INTERFACE_REQUEST, "Close");836DBusMessage *reply = dbus_connection_send_with_reply_and_block(monitor_connection, message, DBUS_TIMEOUT_USE_DEFAULT, &error);837dbus_message_unref(message);838if (dbus_error_is_set(&error)) {839ERR_PRINT(vformat("Failed to uninhibit: %s.", String::utf8(error.message)));840dbus_error_free(&error);841} else if (reply) {842dbus_message_unref(reply);843}844845dbus_bus_remove_match(monitor_connection, inhibit_filter.utf8().get_data(), &error);846if (dbus_error_is_set(&error)) {847ERR_PRINT(vformat("Failed to remove match: %s.", String::utf8(error.message)));848dbus_error_free(&error);849}850851inhibit_path.clear();852inhibit_filter.clear();853}854855void FreeDesktopPortalDesktop::process_callbacks() {856{857MutexLock lock(file_dialog_mutex);858while (!pending_file_cbs.is_empty()) {859FileDialogCallback cb = pending_file_cbs.front()->get();860pending_file_cbs.pop_front();861862if (cb.opt_in_cb) {863Variant ret;864Callable::CallError ce;865const Variant *args[4] = { &cb.status, &cb.files, &cb.index, &cb.options };866867cb.callback.callp(args, 4, ret, ce);868if (ce.error != Callable::CallError::CALL_OK) {869ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(cb.callback, args, 4, ce)));870}871} else {872Variant ret;873Callable::CallError ce;874const Variant *args[3] = { &cb.status, &cb.files, &cb.index };875876cb.callback.callp(args, 3, ret, ce);877if (ce.error != Callable::CallError::CALL_OK) {878ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(cb.callback, args, 3, ce)));879}880}881}882}883{884MutexLock lock(color_picker_mutex);885while (!pending_color_cbs.is_empty()) {886ColorPickerCallback cb = pending_color_cbs.front()->get();887pending_color_cbs.pop_front();888889Variant ret;890Callable::CallError ce;891const Variant *args[2] = { &cb.status, &cb.color };892893cb.callback.callp(args, 2, ret, ce);894if (ce.error != Callable::CallError::CALL_OK) {895ERR_PRINT(vformat("Failed to execute color picker callback: %s.", Variant::get_callable_error_text(cb.callback, args, 2, ce)));896}897}898}899}900901void FreeDesktopPortalDesktop::_thread_monitor(void *p_ud) {902FreeDesktopPortalDesktop *portal = (FreeDesktopPortalDesktop *)p_ud;903904while (!portal->monitor_thread_abort.is_set()) {905if (portal->monitor_connection) {906while (true) {907DBusMessage *msg = dbus_connection_pop_message(portal->monitor_connection);908if (!msg) {909break;910} else if (dbus_message_is_signal(msg, "org.freedesktop.portal.Settings", "SettingChanged")) {911DBusMessageIter iter;912if (dbus_message_iter_init(msg, &iter)) {913const char *value;914dbus_message_iter_get_basic(&iter, &value);915String name_space = String::utf8(value);916dbus_message_iter_next(&iter);917dbus_message_iter_get_basic(&iter, &value);918String key = String::utf8(value);919920if (name_space == "org.freedesktop.appearance" && (key == "color-scheme" || key == "accent-color")) {921callable_mp(portal, &FreeDesktopPortalDesktop::_system_theme_changed_callback).call_deferred();922}923}924} else if (dbus_message_is_signal(msg, "org.freedesktop.portal.Request", "Response")) {925String path = String::utf8(dbus_message_get_path(msg));926{927MutexLock lock(portal->file_dialog_mutex);928for (int i = 0; i < portal->file_dialogs.size(); i++) {929FreeDesktopPortalDesktop::FileDialogData &fd = portal->file_dialogs.write[i];930if (fd.path == path) {931DBusMessageIter iter;932if (dbus_message_iter_init(msg, &iter)) {933bool cancel = false;934Vector<String> uris;935Dictionary options;936int index = 0;937file_chooser_parse_response(&iter, fd.filter_names, fd.option_ids, cancel, uris, index, options);938939if (fd.callback.is_valid()) {940FileDialogCallback cb;941cb.callback = fd.callback;942cb.status = !cancel;943cb.files = uris;944cb.index = index;945cb.options = options;946cb.opt_in_cb = fd.opt_in_cb;947portal->pending_file_cbs.push_back(cb);948}949if (fd.prev_focus != DisplayServer::INVALID_WINDOW_ID) {950callable_mp(DisplayServer::get_singleton(), &DisplayServer::window_move_to_foreground).call_deferred(fd.prev_focus);951}952}953954DBusError err;955dbus_error_init(&err);956dbus_bus_remove_match(portal->monitor_connection, fd.filter.utf8().get_data(), &err);957dbus_error_free(&err);958959portal->file_dialogs.remove_at(i);960break;961}962}963}964{965MutexLock lock(portal->color_picker_mutex);966for (int i = 0; i < portal->color_pickers.size(); i++) {967FreeDesktopPortalDesktop::ColorPickerData &cd = portal->color_pickers.write[i];968if (cd.path == path) {969DBusMessageIter iter;970if (dbus_message_iter_init(msg, &iter)) {971bool cancel = false;972Color c;973color_picker_parse_response(&iter, cancel, c);974975if (cd.callback.is_valid()) {976ColorPickerCallback cb;977cb.callback = cd.callback;978cb.color = c;979cb.status = !cancel;980portal->pending_color_cbs.push_back(cb);981}982}983984DBusError err;985dbus_error_init(&err);986dbus_bus_remove_match(portal->monitor_connection, cd.filter.utf8().get_data(), &err);987dbus_error_free(&err);988989portal->color_pickers.remove_at(i);990break;991}992}993}994{995MutexLock lock(portal->inhibit_mutex);996if (portal->inhibit_path == path) {997DBusMessageIter iter;998if (dbus_message_iter_init(msg, &iter) && dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_UINT32) {999dbus_uint32_t resp_code;1000dbus_message_iter_get_basic(&iter, &resp_code);1001if (resp_code != 0) {1002// The protocol does not give any further details1003ERR_PRINT(vformat("Inhibit portal request failed with reason %u.", resp_code));1004}1005}1006}1007}1008}1009dbus_message_unref(msg);1010}1011dbus_connection_read_write(portal->monitor_connection, 0);1012}10131014OS::get_singleton()->delay_usec(50'000);1015}1016}10171018void FreeDesktopPortalDesktop::_system_theme_changed_callback() {1019if (system_theme_changed.is_valid()) {1020Variant ret;1021Callable::CallError ce;1022system_theme_changed.callp(nullptr, 0, ret, ce);1023if (ce.error != Callable::CallError::CALL_OK) {1024ERR_PRINT(vformat("Failed to execute system theme changed callback: %s.", Variant::get_callable_error_text(system_theme_changed, nullptr, 0, ce)));1025}1026}1027}10281029FreeDesktopPortalDesktop::FreeDesktopPortalDesktop() {1030DBusError err;1031dbus_error_init(&err);1032monitor_connection = dbus_bus_get(DBUS_BUS_SESSION, &err);1033if (dbus_error_is_set(&err)) {1034dbus_error_free(&err);1035} else {1036theme_path = "type='signal',sender='org.freedesktop.portal.Desktop',interface='org.freedesktop.portal.Settings',member='SettingChanged'";1037dbus_bus_add_match(monitor_connection, theme_path.utf8().get_data(), &err);1038if (dbus_error_is_set(&err)) {1039dbus_error_free(&err);1040dbus_connection_unref(monitor_connection);1041monitor_connection = nullptr;1042}1043dbus_connection_read_write(monitor_connection, 0);1044}10451046if (!unsupported) {1047monitor_thread_abort.clear();1048monitor_thread.start(FreeDesktopPortalDesktop::_thread_monitor, this);1049}1050}10511052FreeDesktopPortalDesktop::~FreeDesktopPortalDesktop() {1053monitor_thread_abort.set();1054if (monitor_thread.is_started()) {1055monitor_thread.wait_to_finish();1056}10571058if (monitor_connection) {1059DBusError err;1060for (FreeDesktopPortalDesktop::FileDialogData &fd : file_dialogs) {1061dbus_error_init(&err);1062dbus_bus_remove_match(monitor_connection, fd.filter.utf8().get_data(), &err);1063dbus_error_free(&err);1064}1065dbus_error_init(&err);1066dbus_bus_remove_match(monitor_connection, theme_path.utf8().get_data(), &err);1067dbus_error_free(&err);1068dbus_connection_unref(monitor_connection);1069}1070}10711072#endif // DBUS_ENABLED107310741075