Skip to content

Commit 05ca806

Browse files
committed
[Linux] Implement native color picker.
1 parent f60f69a commit 05ca806

File tree

11 files changed

+350
-53
lines changed

11 files changed

+350
-53
lines changed

doc/classes/DisplayServer.xml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,16 @@
6464
[b]Note:[/b] This method is only implemented on Linux (X11/Wayland).
6565
</description>
6666
</method>
67+
<method name="color_picker">
68+
<return type="bool" />
69+
<param index="0" name="callback" type="Callable" />
70+
<description>
71+
Displays OS native color picker.
72+
Callbacks have the following arguments: [code]status: bool, color: Color[/code].
73+
[b]Note:[/b] This method is implemented if the display server has the [constant FEATURE_NATIVE_COLOR_PICKER] feature.
74+
[b]Note:[/b] This method is only implemented on Linux (X11/Wayland).
75+
</description>
76+
</method>
6777
<method name="create_status_indicator">
6878
<return type="int" />
6979
<param index="0" name="icon" type="Texture2D" />
@@ -1952,6 +1962,9 @@
19521962
<constant name="FEATURE_EMOJI_AND_SYMBOL_PICKER" value="31" enum="Feature">
19531963
Display server supports system emoji and symbol picker. [b]Windows, macOS[/b]
19541964
</constant>
1965+
<constant name="FEATURE_NATIVE_COLOR_PICKER" value="32" enum="Feature">
1966+
Display server supports native color picker. [b]Linux (X11/Wayland)[/b]
1967+
</constant>
19551968
<constant name="MOUSE_MODE_VISIBLE" value="0" enum="MouseMode">
19561969
Makes the mouse cursor visible if it is hidden.
19571970
</constant>

platform/linuxbsd/freedesktop_portal_desktop.cpp

Lines changed: 247 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
#define BUS_INTERFACE_PROPERTIES "org.freedesktop.DBus.Properties"
5353
#define BUS_INTERFACE_SETTINGS "org.freedesktop.portal.Settings"
5454
#define BUS_INTERFACE_FILE_CHOOSER "org.freedesktop.portal.FileChooser"
55+
#define BUS_INTERFACE_SCREENSHOT "org.freedesktop.portal.Screenshot"
5556

5657
bool FreeDesktopPortalDesktop::try_parse_variant(DBusMessage *p_reply_message, int p_type, void *r_value) {
5758
DBusMessageIter iter[3];
@@ -295,6 +296,61 @@ void FreeDesktopPortalDesktop::append_dbus_dict_bool(DBusMessageIter *p_iter, co
295296
dbus_message_iter_close_container(p_iter, &dict_iter);
296297
}
297298

299+
bool FreeDesktopPortalDesktop::color_picker_parse_response(DBusMessageIter *p_iter, bool &r_cancel, Color &r_color) {
300+
ERR_FAIL_COND_V(dbus_message_iter_get_arg_type(p_iter) != DBUS_TYPE_UINT32, false);
301+
302+
dbus_uint32_t resp_code;
303+
dbus_message_iter_get_basic(p_iter, &resp_code);
304+
if (resp_code != 0) {
305+
r_cancel = true;
306+
} else {
307+
r_cancel = false;
308+
ERR_FAIL_COND_V(!dbus_message_iter_next(p_iter), false);
309+
ERR_FAIL_COND_V(dbus_message_iter_get_arg_type(p_iter) != DBUS_TYPE_ARRAY, false);
310+
311+
DBusMessageIter dict_iter;
312+
dbus_message_iter_recurse(p_iter, &dict_iter);
313+
while (dbus_message_iter_get_arg_type(&dict_iter) == DBUS_TYPE_DICT_ENTRY) {
314+
DBusMessageIter iter;
315+
dbus_message_iter_recurse(&dict_iter, &iter);
316+
if (dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_STRING) {
317+
const char *key;
318+
dbus_message_iter_get_basic(&iter, &key);
319+
dbus_message_iter_next(&iter);
320+
321+
DBusMessageIter var_iter;
322+
dbus_message_iter_recurse(&iter, &var_iter);
323+
if (strcmp(key, "color") == 0) { // (ddd)
324+
if (dbus_message_iter_get_arg_type(&var_iter) == DBUS_TYPE_STRUCT) {
325+
DBusMessageIter struct_iter;
326+
dbus_message_iter_recurse(&var_iter, &struct_iter);
327+
int idx = 0;
328+
while (dbus_message_iter_get_arg_type(&struct_iter) == DBUS_TYPE_DOUBLE) {
329+
double value = 0.0;
330+
dbus_message_iter_get_basic(&struct_iter, &value);
331+
if (idx == 0) {
332+
r_color.r = value;
333+
} else if (idx == 1) {
334+
r_color.g = value;
335+
} else if (idx == 2) {
336+
r_color.b = value;
337+
}
338+
idx++;
339+
if (!dbus_message_iter_next(&struct_iter)) {
340+
break;
341+
}
342+
}
343+
}
344+
}
345+
}
346+
if (!dbus_message_iter_next(&dict_iter)) {
347+
break;
348+
}
349+
}
350+
}
351+
return true;
352+
}
353+
298354
bool 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) {
299355
ERR_FAIL_COND_V(dbus_message_iter_get_arg_type(p_iter) != DBUS_TYPE_UINT32, false);
300356

@@ -388,6 +444,92 @@ bool FreeDesktopPortalDesktop::file_chooser_parse_response(DBusMessageIter *p_it
388444
return true;
389445
}
390446

447+
bool FreeDesktopPortalDesktop::color_picker(const String &p_xid, const Callable &p_callback) {
448+
if (unsupported) {
449+
return false;
450+
}
451+
452+
DBusError err;
453+
dbus_error_init(&err);
454+
455+
// Open connection and add signal handler.
456+
ColorPickerData cd;
457+
cd.callback = p_callback;
458+
459+
CryptoCore::RandomGenerator rng;
460+
ERR_FAIL_COND_V_MSG(rng.init(), false, "Failed to initialize random number generator.");
461+
uint8_t uuid[64];
462+
Error rng_err = rng.get_random_bytes(uuid, 64);
463+
ERR_FAIL_COND_V_MSG(rng_err, false, "Failed to generate unique token.");
464+
465+
String dbus_unique_name = String::utf8(dbus_bus_get_unique_name(monitor_connection));
466+
String token = String::hex_encode_buffer(uuid, 64);
467+
String path = vformat("/org/freedesktop/portal/desktop/request/%s/%s", dbus_unique_name.replace(".", "_").replace(":", ""), token);
468+
469+
cd.path = path;
470+
cd.filter = vformat("type='signal',sender='org.freedesktop.portal.Desktop',path='%s',interface='org.freedesktop.portal.Request',member='Response',destination='%s'", path, dbus_unique_name);
471+
dbus_bus_add_match(monitor_connection, cd.filter.utf8().get_data(), &err);
472+
if (dbus_error_is_set(&err)) {
473+
ERR_PRINT(vformat("Failed to add DBus match: %s", err.message));
474+
dbus_error_free(&err);
475+
return false;
476+
}
477+
478+
DBusMessage *message = dbus_message_new_method_call(BUS_OBJECT_NAME, BUS_OBJECT_PATH, BUS_INTERFACE_SCREENSHOT, "PickColor");
479+
{
480+
DBusMessageIter iter;
481+
dbus_message_iter_init_append(message, &iter);
482+
483+
append_dbus_string(&iter, p_xid);
484+
485+
DBusMessageIter arr_iter;
486+
dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &arr_iter);
487+
append_dbus_dict_string(&arr_iter, "handle_token", token);
488+
dbus_message_iter_close_container(&iter, &arr_iter);
489+
}
490+
DBusMessage *reply = dbus_connection_send_with_reply_and_block(monitor_connection, message, DBUS_TIMEOUT_INFINITE, &err);
491+
dbus_message_unref(message);
492+
493+
if (!reply || dbus_error_is_set(&err)) {
494+
ERR_PRINT(vformat("Failed to send DBus message: %s", err.message));
495+
dbus_error_free(&err);
496+
dbus_bus_remove_match(monitor_connection, cd.filter.utf8().get_data(), &err);
497+
return false;
498+
}
499+
500+
// Update signal path.
501+
{
502+
DBusMessageIter iter;
503+
if (dbus_message_iter_init(reply, &iter)) {
504+
if (dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_OBJECT_PATH) {
505+
const char *new_path = nullptr;
506+
dbus_message_iter_get_basic(&iter, &new_path);
507+
if (String::utf8(new_path) != path) {
508+
dbus_bus_remove_match(monitor_connection, cd.filter.utf8().get_data(), &err);
509+
if (dbus_error_is_set(&err)) {
510+
ERR_PRINT(vformat("Failed to remove DBus match: %s", err.message));
511+
dbus_error_free(&err);
512+
return false;
513+
}
514+
cd.filter = String::utf8(new_path);
515+
dbus_bus_add_match(monitor_connection, cd.filter.utf8().get_data(), &err);
516+
if (dbus_error_is_set(&err)) {
517+
ERR_PRINT(vformat("Failed to add DBus match: %s", err.message));
518+
dbus_error_free(&err);
519+
return false;
520+
}
521+
}
522+
}
523+
}
524+
}
525+
dbus_message_unref(reply);
526+
527+
MutexLock lock(color_picker_mutex);
528+
color_pickers.push_back(cd);
529+
530+
return true;
531+
}
532+
391533
bool FreeDesktopPortalDesktop::_is_interface_supported(const char *p_iface) {
392534
bool supported = false;
393535
DBusError err;
@@ -443,6 +585,14 @@ bool FreeDesktopPortalDesktop::is_settings_supported() {
443585
return supported;
444586
}
445587

588+
bool FreeDesktopPortalDesktop::is_screenshot_supported() {
589+
static int supported = -1;
590+
if (supported == -1) {
591+
supported = _is_interface_supported(BUS_INTERFACE_SCREENSHOT);
592+
}
593+
return supported;
594+
}
595+
446596
Error FreeDesktopPortalDesktop::file_dialog_show(DisplayServer::WindowID p_window_id, const String &p_xid, const String &p_title, const String &p_current_directory, const String &p_root, const String &p_filename, DisplayServer::FileDialogMode p_mode, const Vector<String> &p_filters, const TypedArray<Dictionary> &p_options, const Callable &p_callback, bool p_options_in_cb) {
447597
if (unsupported) {
448598
return FAILED;
@@ -592,29 +742,47 @@ Error FreeDesktopPortalDesktop::file_dialog_show(DisplayServer::WindowID p_windo
592742
return OK;
593743
}
594744

595-
void FreeDesktopPortalDesktop::process_file_dialog_callbacks() {
596-
MutexLock lock(file_dialog_mutex);
597-
while (!pending_cbs.is_empty()) {
598-
FileDialogCallback cb = pending_cbs.front()->get();
599-
pending_cbs.pop_front();
600-
601-
if (cb.opt_in_cb) {
602-
Variant ret;
603-
Callable::CallError ce;
604-
const Variant *args[4] = { &cb.status, &cb.files, &cb.index, &cb.options };
745+
void FreeDesktopPortalDesktop::process_callbacks() {
746+
{
747+
MutexLock lock(file_dialog_mutex);
748+
while (!pending_file_cbs.is_empty()) {
749+
FileDialogCallback cb = pending_file_cbs.front()->get();
750+
pending_file_cbs.pop_front();
751+
752+
if (cb.opt_in_cb) {
753+
Variant ret;
754+
Callable::CallError ce;
755+
const Variant *args[4] = { &cb.status, &cb.files, &cb.index, &cb.options };
756+
757+
cb.callback.callp(args, 4, ret, ce);
758+
if (ce.error != Callable::CallError::CALL_OK) {
759+
ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(cb.callback, args, 4, ce)));
760+
}
761+
} else {
762+
Variant ret;
763+
Callable::CallError ce;
764+
const Variant *args[3] = { &cb.status, &cb.files, &cb.index };
605765

606-
cb.callback.callp(args, 4, ret, ce);
607-
if (ce.error != Callable::CallError::CALL_OK) {
608-
ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(cb.callback, args, 4, ce)));
766+
cb.callback.callp(args, 3, ret, ce);
767+
if (ce.error != Callable::CallError::CALL_OK) {
768+
ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(cb.callback, args, 3, ce)));
769+
}
609770
}
610-
} else {
771+
}
772+
}
773+
{
774+
MutexLock lock(color_picker_mutex);
775+
while (!pending_color_cbs.is_empty()) {
776+
ColorPickerCallback cb = pending_color_cbs.front()->get();
777+
pending_color_cbs.pop_front();
778+
611779
Variant ret;
612780
Callable::CallError ce;
613-
const Variant *args[3] = { &cb.status, &cb.files, &cb.index };
781+
const Variant *args[2] = { &cb.status, &cb.color };
614782

615-
cb.callback.callp(args, 3, ret, ce);
783+
cb.callback.callp(args, 2, ret, ce);
616784
if (ce.error != Callable::CallError::CALL_OK) {
617-
ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(cb.callback, args, 3, ce)));
785+
ERR_PRINT(vformat("Failed to execute color picker callback: %s.", Variant::get_callable_error_text(cb.callback, args, 2, ce)));
618786
}
619787
}
620788
}
@@ -645,40 +813,72 @@ void FreeDesktopPortalDesktop::_thread_monitor(void *p_ud) {
645813
}
646814
} else if (dbus_message_is_signal(msg, "org.freedesktop.portal.Request", "Response")) {
647815
String path = String::utf8(dbus_message_get_path(msg));
648-
MutexLock lock(portal->file_dialog_mutex);
649-
for (int i = 0; i < portal->file_dialogs.size(); i++) {
650-
FreeDesktopPortalDesktop::FileDialogData &fd = portal->file_dialogs.write[i];
651-
if (fd.path == path) {
652-
DBusMessageIter iter;
653-
if (dbus_message_iter_init(msg, &iter)) {
654-
bool cancel = false;
655-
Vector<String> uris;
656-
Dictionary options;
657-
int index = 0;
658-
file_chooser_parse_response(&iter, fd.filter_names, fd.option_ids, cancel, uris, index, options);
659-
660-
if (fd.callback.is_valid()) {
661-
FileDialogCallback cb;
662-
cb.callback = fd.callback;
663-
cb.status = !cancel;
664-
cb.files = uris;
665-
cb.index = index;
666-
cb.options = options;
667-
cb.opt_in_cb = fd.opt_in_cb;
668-
portal->pending_cbs.push_back(cb);
669-
}
670-
if (fd.prev_focus != DisplayServer::INVALID_WINDOW_ID) {
671-
callable_mp(DisplayServer::get_singleton(), &DisplayServer::window_move_to_foreground).call_deferred(fd.prev_focus);
816+
{
817+
MutexLock lock(portal->file_dialog_mutex);
818+
for (int i = 0; i < portal->file_dialogs.size(); i++) {
819+
FreeDesktopPortalDesktop::FileDialogData &fd = portal->file_dialogs.write[i];
820+
if (fd.path == path) {
821+
DBusMessageIter iter;
822+
if (dbus_message_iter_init(msg, &iter)) {
823+
bool cancel = false;
824+
Vector<String> uris;
825+
Dictionary options;
826+
int index = 0;
827+
file_chooser_parse_response(&iter, fd.filter_names, fd.option_ids, cancel, uris, index, options);
828+
829+
if (fd.callback.is_valid()) {
830+
FileDialogCallback cb;
831+
cb.callback = fd.callback;
832+
cb.status = !cancel;
833+
cb.files = uris;
834+
cb.index = index;
835+
cb.options = options;
836+
cb.opt_in_cb = fd.opt_in_cb;
837+
portal->pending_file_cbs.push_back(cb);
838+
}
839+
if (fd.prev_focus != DisplayServer::INVALID_WINDOW_ID) {
840+
callable_mp(DisplayServer::get_singleton(), &DisplayServer::window_move_to_foreground).call_deferred(fd.prev_focus);
841+
}
672842
}
843+
844+
DBusError err;
845+
dbus_error_init(&err);
846+
dbus_bus_remove_match(portal->monitor_connection, fd.filter.utf8().get_data(), &err);
847+
dbus_error_free(&err);
848+
849+
portal->file_dialogs.remove_at(i);
850+
break;
673851
}
852+
}
853+
}
854+
{
855+
MutexLock lock(portal->color_picker_mutex);
856+
for (int i = 0; i < portal->color_pickers.size(); i++) {
857+
FreeDesktopPortalDesktop::ColorPickerData &cd = portal->color_pickers.write[i];
858+
if (cd.path == path) {
859+
DBusMessageIter iter;
860+
if (dbus_message_iter_init(msg, &iter)) {
861+
bool cancel = false;
862+
Color c;
863+
color_picker_parse_response(&iter, cancel, c);
864+
865+
if (cd.callback.is_valid()) {
866+
ColorPickerCallback cb;
867+
cb.callback = cd.callback;
868+
cb.color = c;
869+
cb.status = !cancel;
870+
portal->pending_color_cbs.push_back(cb);
871+
}
872+
}
674873

675-
DBusError err;
676-
dbus_error_init(&err);
677-
dbus_bus_remove_match(portal->monitor_connection, fd.filter.utf8().get_data(), &err);
678-
dbus_error_free(&err);
874+
DBusError err;
875+
dbus_error_init(&err);
876+
dbus_bus_remove_match(portal->monitor_connection, cd.filter.utf8().get_data(), &err);
877+
dbus_error_free(&err);
679878

680-
portal->file_dialogs.remove_at(i);
681-
break;
879+
portal->color_pickers.remove_at(i);
880+
break;
881+
}
682882
}
683883
}
684884
}

0 commit comments

Comments
 (0)