messenger-gtk

Gtk+3 graphical user interfaces for GNUnet Messenger
Log | Files | Refs | Submodules | README | LICENSE

commit eb9043d874685dfafae5712f97fb2386041ef263
parent 68dc9951534988d9ac21358f51ee72c0a81e2a9d
Author: Jacki <jacki@thejackimonster.de>
Date:   Fri, 19 Jan 2024 05:10:38 +0100

Add combobox to select between cameras

Signed-off-by: Jacki <jacki@thejackimonster.de>

Diffstat:
Mmeson.build | 1+
Mresources/ui/new_contact.ui | 64+++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Msrc/application.c | 144+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/application.h | 15++++++++++++++-
Msrc/ui/new_contact.c | 120++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Msrc/ui/new_contact.h | 5++++-
6 files changed, 329 insertions(+), 20 deletions(-)

diff --git a/meson.build b/meson.build @@ -44,6 +44,7 @@ messenger_gtk_deps = [ dependency('libportal-gtk3'), dependency('libnotify'), dependency('libqrencode'), + dependency('libpipewire-0.3'), declare_dependency(link_args: '-lunistring'), ] diff --git a/resources/ui/new_contact.ui b/resources/ui/new_contact.ui @@ -22,6 +22,14 @@ Author: Tobias Frisch --> <interface> <requires lib="gtk+" version="3.24"/> + <object class="GtkListStore" id="camera_list_store"> + <columns> + <!-- column-name name --> + <column type="gchararray"/> + <!-- column-name nick --> + <column type="gchararray"/> + </columns> + </object> <object class="GtkDialog" id="new_contact_dialog"> <property name="can-focus">False</property> <property name="title" translatable="yes">New Contact</property> @@ -76,10 +84,42 @@ Author: Tobias Frisch <object class="GtkBox"> <property name="visible">True</property> <property name="can-focus">False</property> - <property name="border-width">8</property> + <property name="margin-start">8</property> + <property name="margin-end">8</property> + <property name="margin-bottom">8</property> <property name="orientation">vertical</property> <property name="spacing">4</property> <child> + <object class="GtkRevealer"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-start">8</property> + <property name="margin-end">8</property> + <property name="margin-bottom">8</property> + <property name="transition-type">none</property> + <property name="reveal-child">True</property> + <child> + <object class="GtkComboBox" id="camera_combo_box"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">center</property> + <property name="model">camera_list_store</property> + <child> + <object class="GtkCellRendererText"/> + <attributes> + <attribute name="text">1</attribute> + </attributes> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> <object class="GtkStack" id="preview_stack"> <property name="visible">True</property> <property name="can-focus">False</property> @@ -141,32 +181,34 @@ Author: Tobias Frisch <packing> <property name="expand">True</property> <property name="fill">True</property> - <property name="position">0</property> + <property name="position">1</property> </packing> </child> <child> - <object class="GtkLabel"> + <object class="GtkEntry" id="id_entry"> + <property name="width-request">250</property> <property name="visible">True</property> - <property name="can-focus">False</property> - <property name="label" translatable="yes">ID:</property> - <property name="xalign">0</property> + <property name="can-focus">True</property> </object> <packing> <property name="expand">False</property> <property name="fill">True</property> - <property name="position">1</property> + <property name="pack-type">end</property> + <property name="position">2</property> </packing> </child> <child> - <object class="GtkEntry" id="id_entry"> - <property name="width-request">250</property> + <object class="GtkLabel"> <property name="visible">True</property> - <property name="can-focus">True</property> + <property name="can-focus">False</property> + <property name="label" translatable="yes">ID:</property> + <property name="xalign">0</property> </object> <packing> <property name="expand">False</property> <property name="fill">True</property> - <property name="position">2</property> + <property name="pack-type">end</property> + <property name="position">3</property> </packing> </child> </object> diff --git a/src/application.c b/src/application.c @@ -31,6 +31,7 @@ #include <libhandy-1/handy.h> #include <libportal-gtk3/portal-gtk3.h> #include <libnotify/notify.h> +#include <pipewire/impl.h> static void _load_ui_stylesheets(MESSENGER_Application *app) @@ -135,6 +136,84 @@ _application_open(GApplication* application, g_application_release(application); } +static void +on_core_done(void *data, + UNUSED uint32_t id, + int seq) +{ + MESSENGER_Application *app = (MESSENGER_Application*) data; + + if (seq == app->pw.pending) + pw_main_loop_quit(app->pw.main_loop); +} + +static void +on_core_error(void *data, + UNUSED uint32_t id, + int seq, + int res, + const char *message) +{ + MESSENGER_Application *app = (MESSENGER_Application*) data; + + g_printerr("ERROR: %s\n", message); + + if (id == PW_ID_CORE && res == -EPIPE) + pw_main_loop_quit(app->pw.main_loop); +} + +static const struct pw_core_events remote_core_events = { + PW_VERSION_CORE_EVENTS, + .done = on_core_done, + .error = on_core_error, +}; + +static void +registry_event_global(void *data, + uint32_t id, + uint32_t permissions, + const char *type, + uint32_t version, + const struct spa_dict *props) +{ + MESSENGER_Application *app = (MESSENGER_Application*) data; + + if (!props) + return; + + struct pw_properties *properties = pw_properties_new_dict(props); + if (!properties) + return; + + size_t size = pw_map_get_size(&(app->pw.globals)); + while (id > size) + pw_map_insert_at(&(app->pw.globals), size++, NULL); + + pw_map_insert_at(&(app->pw.globals), id, properties); + + app->pw.pending = pw_core_sync(app->pw.core, 0, 0); +} + +static void +registry_event_global_remove(void *data, + uint32_t id) +{ + MESSENGER_Application *app = (MESSENGER_Application*) data; + + struct pw_properties *properties = pw_map_lookup(&(app->pw.globals), id); + if (!properties) + return; + + pw_map_insert_at(&(app->pw.globals), id, NULL); + pw_properties_free(properties); +} + +static const struct pw_registry_events registry_events = { + PW_VERSION_REGISTRY_EVENTS, + .global = registry_event_global, + .global_remove = registry_event_global_remove, +}; + void application_init(MESSENGER_Application *app, int argc, @@ -145,6 +224,7 @@ application_init(MESSENGER_Application *app, app->argc = argc; app->argv = argv; + pw_init(&argc, &argv); gst_init(&argc, &argv); gtk_init(&argc, &argv); hdy_init(); @@ -182,6 +262,44 @@ application_init(MESSENGER_Application *app, app->quarks.data = g_quark_from_string("messenger_data"); app->quarks.ui = g_quark_from_string("messenger_ui"); + app->pw.main_loop = pw_main_loop_new(NULL); + app->pw.loop = pw_main_loop_get_loop(app->pw.main_loop); + + app->pw.context = pw_context_new( + app->pw.loop, + pw_properties_new( + PW_KEY_CORE_DAEMON, + NULL, + NULL + ), + 0 + ); + + pw_context_load_module(app->pw.context, "libpipewire-module-link-factory", NULL, NULL); + + app->pw.core = pw_context_connect(app->pw.context, NULL, 0); + app->pw.registry = pw_core_get_registry(app->pw.core, PW_VERSION_REGISTRY, 0); + + pw_map_init(&(app->pw.globals), 64, 16); + + pw_core_add_listener( + app->pw.core, + &(app->pw.core_listener), + &remote_core_events, + app + ); + + pw_registry_add_listener( + app->pw.registry, + &(app->pw.registry_listener), + &registry_events, + app + ); + + app->pw.pending = pw_core_sync(app->pw.core, 0, 0); + + pw_main_loop_run(app->pw.main_loop); + g_application_add_main_option( G_APPLICATION(app->application), "mobile", @@ -468,6 +586,15 @@ application_call_message_event(MESSENGER_Application *app, g_idle_add(G_SOURCE_FUNC(_application_message_event_call), call); } +static int +destroy_global(void *obj, + void *data) +{ + struct pw_properties *properties = (struct pw_properties*) obj; + pw_properties_free(properties); + return 0; +} + void application_exit(MESSENGER_Application *app, MESSENGER_ApplicationSignal signal) @@ -480,6 +607,23 @@ application_exit(MESSENGER_Application *app, g_free(app->portal); app->portal = NULL; + + if (app->pw.core) + { + pw_map_for_each(&(app->pw.globals), destroy_global, NULL); + pw_map_clear(&(app->pw.globals)); + } + + if (app->pw.context) + pw_context_destroy(app->pw.context); + + if (app->pw.main_loop) + { + pw_main_loop_quit(app->pw.main_loop); + pw_main_loop_destroy(app->pw.main_loop); + } + + pw_deinit(); } int diff --git a/src/application.h b/src/application.h @@ -26,8 +26,9 @@ #define APPLICATION_H_ #include <libportal/portal.h> - +#include <pipewire/pipewire.h> #include <pthread.h> + #include <gnunet/gnunet_chat_lib.h> #include "chat/messenger.h" @@ -84,6 +85,18 @@ typedef struct MESSENGER_Application } quarks; struct { + struct pw_main_loop *main_loop; + struct pw_loop *loop; + struct pw_context *context; + struct pw_core *core; + struct pw_registry *registry; + struct spa_hook core_listener; + struct spa_hook registry_listener; + struct pw_map globals; + int pending; + } pw; + + struct { int status; pthread_t tid; char *identity; diff --git a/src/ui/new_contact.c b/src/ui/new_contact.c @@ -1,6 +1,6 @@ /* This file is part of GNUnet. - Copyright (C) 2021--2022 GNUnet e.V. + Copyright (C) 2021--2024 GNUnet e.V. GNUnet is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published @@ -27,6 +27,8 @@ #include "../application.h" #include "../request.h" +#include <pipewire/pipewire.h> + static void handle_cancel_button_click(UNUSED GtkButton *button, gpointer user_data) @@ -74,6 +76,41 @@ handle_dialog_destroy(UNUSED GtkWidget *window, } static void +handle_camera_combo_box_change(GtkComboBox *widget, + gpointer user_data) +{ + UI_NEW_CONTACT_Handle *handle = (UI_NEW_CONTACT_Handle*) user_data; + gchar *name = NULL; + + GtkTreeIter iter; + if (gtk_combo_box_get_active_iter(widget, &iter)) + gtk_tree_model_get( + GTK_TREE_MODEL(handle->camera_list_store), + &iter, + 0, &name, + -1 + ); + + if (!name) + return; + + g_object_set( + G_OBJECT(handle->source), + "target-object", + name, + NULL + ); + + g_free(name); + + if (!handle->pipeline) + return; + + gst_element_set_state(handle->pipeline, GST_STATE_PAUSED); + gst_element_set_state(handle->pipeline, GST_STATE_PLAYING); +} + +static void _disable_video_processing(UI_NEW_CONTACT_Handle *handle, gboolean drop_pipeline) { @@ -252,18 +289,72 @@ _ui_new_contact_video_thread(void *args) return NULL; } +static int +iterate_global(void *obj, + void *data) +{ + UI_NEW_CONTACT_Handle *handle = (UI_NEW_CONTACT_Handle*) data; + struct pw_properties *properties = (struct pw_properties*) obj; + + if (!properties) + return 0; + + struct spa_dict *props = &(properties->dict); + + if ((!props) || (!props->n_items)) + return 0; + + gboolean is_camera = FALSE; + const char *name = NULL; + const char *nick = NULL; + + const struct spa_dict_item *item; + spa_dict_for_each(item, props) + { + if (0 == g_strcmp0(item->key, "node.name")) + name = item->value; + + if (0 == g_strcmp0(item->key, "node.nick")) + nick = item->value; + + if (0 != g_strcmp0(item->key, "media.role")) + continue; + + if (0 != g_strcmp0(item->value, "Camera")) + continue; + + is_camera = TRUE; + } + + if ((!is_camera) || (!name) || (!nick)) + return 0; + + GtkTreeIter iter; + gtk_list_store_append(handle->camera_list_store, &iter); + gtk_list_store_set( + handle->camera_list_store, + &iter, + 0, name, + 1, nick, + -1 + ); + + return 0; +} + static void _init_camera_pipeline(MESSENGER_Application *app, UI_NEW_CONTACT_Handle *handle, gboolean access) { if ((app->portal) && ((access) || xdp_portal_is_camera_present(app->portal))) - g_object_set( - G_OBJECT(handle->source), - "fd", - xdp_portal_open_pipewire_remote_for_camera(app->portal), - NULL - ); + { + pw_map_for_each(&(app->pw.globals), iterate_global, handle); + + GtkTreeIter iter; + if (gtk_tree_model_get_iter_first(GTK_TREE_MODEL(handle->camera_list_store), &iter)) + gtk_combo_box_set_active(handle->camera_combo_box, 0); + } pthread_create( &(handle->video_tid), @@ -334,6 +425,21 @@ ui_new_contact_dialog_init(MESSENGER_Application *app, GTK_WINDOW(app->ui.messenger.main_window) ); + handle->camera_combo_box = GTK_COMBO_BOX( + gtk_builder_get_object(handle->builder, "camera_combo_box") + ); + + handle->camera_list_store = GTK_LIST_STORE( + gtk_builder_get_object(handle->builder, "camera_list_store") + ); + + g_signal_connect( + handle->camera_combo_box, + "changed", + G_CALLBACK(handle_camera_combo_box_change), + handle + ); + handle->preview_stack = GTK_STACK( gtk_builder_get_object(handle->builder, "preview_stack") ); diff --git a/src/ui/new_contact.h b/src/ui/new_contact.h @@ -1,6 +1,6 @@ /* This file is part of GNUnet. - Copyright (C) 2021--2022 GNUnet e.V. + Copyright (C) 2021--2024 GNUnet e.V. GNUnet is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published @@ -42,6 +42,9 @@ typedef struct UI_NEW_CONTACT_Handle GtkBuilder *builder; GtkDialog *dialog; + GtkComboBox *camera_combo_box; + GtkListStore *camera_list_store; + GtkStack *preview_stack; GtkWidget *fail_box;