Skip to content

Commit 49fcd4c

Browse files
committed
Merge pull request #101546 from bruvzg/portal_color_picker
[Linux] Implement native color picker.
2 parents 9cf741a + 05ca806 commit 49fcd4c

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" />
@@ -1954,6 +1964,9 @@
19541964
<constant name="FEATURE_EMOJI_AND_SYMBOL_PICKER" value="31" enum="Feature">
19551965
Display server supports system emoji and symbol picker. [b]Windows, macOS[/b]
19561966
</constant>
1967+
<constant name="FEATURE_NATIVE_COLOR_PICKER" value="32" enum="Feature">
1968+
Display server supports native color picker. [b]Linux (X11/Wayland)[/b]
1969+
</constant>
19571970
<constant name="MOUSE_MODE_VISIBLE" value="0" enum="MouseMode">
19581971
Makes the mouse cursor visible if it is hidden.
19591972
</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, ReadVariantType p_type, void *r_value) {
5758
DBusMessageIter iter[3];
@@ -340,6 +341,61 @@ void FreeDesktopPortalDesktop::append_dbus_dict_bool(DBusMessageIter *p_iter, co
340341
dbus_message_iter_close_container(p_iter, &dict_iter);
341342
}
342343

344+
bool FreeDesktopPortalDesktop::color_picker_parse_response(DBusMessageIter *p_iter, bool &r_cancel, Color &r_color) {
345+
ERR_FAIL_COND_V(dbus_message_iter_get_arg_type(p_iter) != DBUS_TYPE_UINT32, false);
346+
347+
dbus_uint32_t resp_code;
348+
dbus_message_iter_get_basic(p_iter, &resp_code);
349+
if (resp_code != 0) {
350+
r_cancel = true;
351+
} else {
352+
r_cancel = false;
353+
ERR_FAIL_COND_V(!dbus_message_iter_next(p_iter), false);
354+
ERR_FAIL_COND_V(dbus_message_iter_get_arg_type(p_iter) != DBUS_TYPE_ARRAY, false);
355+
356+
DBusMessageIter dict_iter;
357+
dbus_message_iter_recurse(p_iter, &dict_iter);
358+
while (dbus_message_iter_get_arg_type(&dict_iter) == DBUS_TYPE_DICT_ENTRY) {
359+
DBusMessageIter iter;
360+
dbus_message_iter_recurse(&dict_iter, &iter);
361+
if (dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_STRING) {
362+
const char *key;
363+
dbus_message_iter_get_basic(&iter, &key);
364+
dbus_message_iter_next(&iter);
365+
366+
DBusMessageIter var_iter;
367+
dbus_message_iter_recurse(&iter, &var_iter);
368+
if (strcmp(key, "color") == 0) { // (ddd)
369+
if (dbus_message_iter_get_arg_type(&var_iter) == DBUS_TYPE_STRUCT) {
370+
DBusMessageIter struct_iter;
371+
dbus_message_iter_recurse(&var_iter, &struct_iter);
372+
int idx = 0;
373+
while (dbus_message_iter_get_arg_type(&struct_iter) == DBUS_TYPE_DOUBLE) {
374+
double value = 0.0;
375+
dbus_message_iter_get_basic(&struct_iter, &value);
376+
if (idx == 0) {
377+
r_color.r = value;
378+
} else if (idx == 1) {
379+
r_color.g = value;
380+
} else if (idx == 2) {
381+
r_color.b = value;
382+
}
383+
idx++;
384+
if (!dbus_message_iter_next(&struct_iter)) {
385+
break;
386+
}
387+
}
388+
}
389+
}
390+
}
391+
if (!dbus_message_iter_next(&dict_iter)) {
392+
break;
393+
}
394+
}
395+
}
396+
return true;
397+
}
398+
343399
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) {
344400
ERR_FAIL_COND_V(dbus_message_iter_get_arg_type(p_iter) != DBUS_TYPE_UINT32, false);
345401

@@ -433,6 +489,92 @@ bool FreeDesktopPortalDesktop::file_chooser_parse_response(DBusMessageIter *p_it
433489
return true;
434490
}
435491

492+
bool FreeDesktopPortalDesktop::color_picker(const String &p_xid, const Callable &p_callback) {
493+
if (unsupported) {
494+
return false;
495+
}
496+
497+
DBusError err;
498+
dbus_error_init(&err);
499+
500+
// Open connection and add signal handler.
501+
ColorPickerData cd;
502+
cd.callback = p_callback;
503+
504+
CryptoCore::RandomGenerator rng;
505+
ERR_FAIL_COND_V_MSG(rng.init(), false, "Failed to initialize random number generator.");
506+
uint8_t uuid[64];
507+
Error rng_err = rng.get_random_bytes(uuid, 64);
508+
ERR_FAIL_COND_V_MSG(rng_err, false, "Failed to generate unique token.");
509+
510+
String dbus_unique_name = String::utf8(dbus_bus_get_unique_name(monitor_connection));
511+
String token = String::hex_encode_buffer(uuid, 64);
512+
String path = vformat("/org/freedesktop/portal/desktop/request/%s/%s", dbus_unique_name.replace(".", "_").replace(":", ""), token);
513+
514+
cd.path = path;
515+
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);
516+
dbus_bus_add_match(monitor_connection, cd.filter.utf8().get_data(), &err);
517+
if (dbus_error_is_set(&err)) {
518+
ERR_PRINT(vformat("Failed to add DBus match: %s", err.message));
519+
dbus_error_free(&err);
520+
return false;
521+
}
522+
523+
DBusMessage *message = dbus_message_new_method_call(BUS_OBJECT_NAME, BUS_OBJECT_PATH, BUS_INTERFACE_SCREENSHOT, "PickColor");
524+
{
525+
DBusMessageIter iter;
526+
dbus_message_iter_init_append(message, &iter);
527+
528+
append_dbus_string(&iter, p_xid);
529+
530+
DBusMessageIter arr_iter;
531+
dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &arr_iter);
532+
append_dbus_dict_string(&arr_iter, "handle_token", token);
533+
dbus_message_iter_close_container(&iter, &arr_iter);
534+
}
535+
DBusMessage *reply = dbus_connection_send_with_reply_and_block(monitor_connection, message, DBUS_TIMEOUT_INFINITE, &err);
536+
dbus_message_unref(message);
537+
538+
if (!reply || dbus_error_is_set(&err)) {
539+
ERR_PRINT(vformat("Failed to send DBus message: %s", err.message));
540+
dbus_error_free(&err);
541+
dbus_bus_remove_match(monitor_connection, cd.filter.utf8().get_data(), &err);
542+
return false;
543+
}
544+
545+
// Update signal path.
546+
{
547+
DBusMessageIter iter;
548+
if (dbus_message_iter_init(reply, &iter)) {
549+
if (dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_OBJECT_PATH) {
550+
const char *new_path = nullptr;
551+
dbus_message_iter_get_basic(&iter, &new_path);
552+
if (String::utf8(new_path) != path) {
553+
dbus_bus_remove_match(monitor_connection, cd.filter.utf8().get_data(), &err);
554+
if (dbus_error_is_set(&err)) {
555+
ERR_PRINT(vformat("Failed to remove DBus match: %s", err.message));
556+
dbus_error_free(&err);
557+
return false;
558+
}
559+
cd.filter = String::utf8(new_path);
560+
dbus_bus_add_match(monitor_connection, cd.filter.utf8().get_data(), &err);
561+
if (dbus_error_is_set(&err)) {
562+
ERR_PRINT(vformat("Failed to add DBus match: %s", err.message));
563+
dbus_error_free(&err);
564+
return false;
565+
}
566+
}
567+
}
568+
}
569+
}
570+
dbus_message_unref(reply);
571+
572+
MutexLock lock(color_picker_mutex);
573+
color_pickers.push_back(cd);
574+
575+
return true;
576+
}
577+
436578
bool FreeDesktopPortalDesktop::_is_interface_supported(const char *p_iface) {
437579
bool supported = false;
438580
DBusError err;
@@ -488,6 +630,14 @@ bool FreeDesktopPortalDesktop::is_settings_supported() {
488630
return supported;
489631
}
490632

633+
bool FreeDesktopPortalDesktop::is_screenshot_supported() {
634+
static int supported = -1;
635+
if (supported == -1) {
636+
supported = _is_interface_supported(BUS_INTERFACE_SCREENSHOT);
637+
}
638+
return supported;
639+
}
640+
491641
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) {
492642
if (unsupported) {
493643
return FAILED;
@@ -637,29 +787,47 @@ Error FreeDesktopPortalDesktop::file_dialog_show(DisplayServer::WindowID p_windo
637787
return OK;
638788
}
639789

640-
void FreeDesktopPortalDesktop::process_file_dialog_callbacks() {
641-
MutexLock lock(file_dialog_mutex);
642-
while (!pending_cbs.is_empty()) {
643-
FileDialogCallback cb = pending_cbs.front()->get();
644-
pending_cbs.pop_front();
645-
646-
if (cb.opt_in_cb) {
647-
Variant ret;
648-
Callable::CallError ce;
649-
const Variant *args[4] = { &cb.status, &cb.files, &cb.index, &cb.options };
790+
void FreeDesktopPortalDesktop::process_callbacks() {
791+
{
792+
MutexLock lock(file_dialog_mutex);
793+
while (!pending_file_cbs.is_empty()) {
794+
FileDialogCallback cb = pending_file_cbs.front()->get();
795+
pending_file_cbs.pop_front();
796+
797+
if (cb.opt_in_cb) {
798+
Variant ret;
799+
Callable::CallError ce;
800+
const Variant *args[4] = { &cb.status, &cb.files, &cb.index, &cb.options };
801+
802+
cb.callback.callp(args, 4, ret, ce);
803+
if (ce.error != Callable::CallError::CALL_OK) {
804+
ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(cb.callback, args, 4, ce)));
805+
}
806+
} else {
807+
Variant ret;
808+
Callable::CallError ce;
809+
const Variant *args[3] = { &cb.status, &cb.files, &cb.index };
650810

651-
cb.callback.callp(args, 4, ret, ce);
652-
if (ce.error != Callable::CallError::CALL_OK) {
653-
ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(cb.callback, args, 4, ce)));
811+
cb.callback.callp(args, 3, ret, ce);
812+
if (ce.error != Callable::CallError::CALL_OK) {
813+
ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(cb.callback, args, 3, ce)));
814+
}
654815
}
655-
} else {
816+
}
817+
}
818+
{
819+
MutexLock lock(color_picker_mutex);
820+
while (!pending_color_cbs.is_empty()) {
821+
ColorPickerCallback cb = pending_color_cbs.front()->get();
822+
pending_color_cbs.pop_front();
823+
656824
Variant ret;
657825
Callable::CallError ce;
658-
const Variant *args[3] = { &cb.status, &cb.files, &cb.index };
826+
const Variant *args[2] = { &cb.status, &cb.color };
659827

660-
cb.callback.callp(args, 3, ret, ce);
828+
cb.callback.callp(args, 2, ret, ce);
661829
if (ce.error != Callable::CallError::CALL_OK) {
662-
ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(cb.callback, args, 3, ce)));
830+
ERR_PRINT(vformat("Failed to execute color picker callback: %s.", Variant::get_callable_error_text(cb.callback, args, 2, ce)));
663831
}
664832
}
665833
}
@@ -690,40 +858,72 @@ void FreeDesktopPortalDesktop::_thread_monitor(void *p_ud) {
690858
}
691859
} else if (dbus_message_is_signal(msg, "org.freedesktop.portal.Request", "Response")) {
692860
String path = String::utf8(dbus_message_get_path(msg));
693-
MutexLock lock(portal->file_dialog_mutex);
694-
for (int i = 0; i < portal->file_dialogs.size(); i++) {
695-
FreeDesktopPortalDesktop::FileDialogData &fd = portal->file_dialogs.write[i];
696-
if (fd.path == path) {
697-
DBusMessageIter iter;
698-
if (dbus_message_iter_init(msg, &iter)) {
699-
bool cancel = false;
700-
Vector<String> uris;
701-
Dictionary options;
702-
int index = 0;
703-
file_chooser_parse_response(&iter, fd.filter_names, fd.option_ids, cancel, uris, index, options);
704-
705-
if (fd.callback.is_valid()) {
706-
FileDialogCallback cb;
707-
cb.callback = fd.callback;
708-
cb.status = !cancel;
709-
cb.files = uris;
710-
cb.index = index;
711-
cb.options = options;
712-
cb.opt_in_cb = fd.opt_in_cb;
713-
portal->pending_cbs.push_back(cb);
714-
}
715-
if (fd.prev_focus != DisplayServer::INVALID_WINDOW_ID) {
716-
callable_mp(DisplayServer::get_singleton(), &DisplayServer::window_move_to_foreground).call_deferred(fd.prev_focus);
861+
{
862+
MutexLock lock(portal->file_dialog_mutex);
863+
for (int i = 0; i < portal->file_dialogs.size(); i++) {
864+
FreeDesktopPortalDesktop::FileDialogData &fd = portal->file_dialogs.write[i];
865+
if (fd.path == path) {
866+
DBusMessageIter iter;
867+
if (dbus_message_iter_init(msg, &iter)) {
868+
bool cancel = false;
869+
Vector<String> uris;
870+
Dictionary options;
871+
int index = 0;
872+
file_chooser_parse_response(&iter, fd.filter_names, fd.option_ids, cancel, uris, index, options);
873+
874+
if (fd.callback.is_valid()) {
875+
FileDialogCallback cb;
876+
cb.callback = fd.callback;
877+
cb.status = !cancel;
878+
cb.files = uris;
879+
cb.index = index;
880+
cb.options = options;
881+
cb.opt_in_cb = fd.opt_in_cb;
882+
portal->pending_file_cbs.push_back(cb);
883+
}
884+
if (fd.prev_focus != DisplayServer::INVALID_WINDOW_ID) {
885+
callable_mp(DisplayServer::get_singleton(), &DisplayServer::window_move_to_foreground).call_deferred(fd.prev_focus);
886+
}
717887
}
888+
889+
DBusError err;
890+
dbus_error_init(&err);
891+
dbus_bus_remove_match(portal->monitor_connection, fd.filter.utf8().get_data(), &err);
892+
dbus_error_free(&err);
893+
894+
portal->file_dialogs.remove_at(i);
895+
break;
718896
}
897+
}
898+
}
899+
{
900+
MutexLock lock(portal->color_picker_mutex);
901+
for (int i = 0; i < portal->color_pickers.size(); i++) {
902+
FreeDesktopPortalDesktop::ColorPickerData &cd = portal->color_pickers.write[i];
903+
if (cd.path == path) {
904+
DBusMessageIter iter;
905+
if (dbus_message_iter_init(msg, &iter)) {
906+
bool cancel = false;
907+
Color c;
908+
color_picker_parse_response(&iter, cancel, c);
909+
910+
if (cd.callback.is_valid()) {
911+
ColorPickerCallback cb;
912+
cb.callback = cd.callback;
913+
cb.color = c;
914+
cb.status = !cancel;
915+
portal->pending_color_cbs.push_back(cb);
916+
}
917+
}
719918

720-
DBusError err;
721-
dbus_error_init(&err);
722-
dbus_bus_remove_match(portal->monitor_connection, fd.filter.utf8().get_data(), &err);
723-
dbus_error_free(&err);
919+
DBusError err;
920+
dbus_error_init(&err);
921+
dbus_bus_remove_match(portal->monitor_connection, cd.filter.utf8().get_data(), &err);
922+
dbus_error_free(&err);
724923

725-
portal->file_dialogs.remove_at(i);
726-
break;
924+
portal->color_pickers.remove_at(i);
925+
break;
926+
}
727927
}
728928
}
729929
}

0 commit comments

Comments
 (0)