From 0d2a69a05bb8b95fef216c184ed3963eb77e5c42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Blin?= <sebastien.blin@savoirfairelinux.com> Date: Wed, 19 Aug 2020 16:49:57 -0400 Subject: [PATCH] conference: handle conferences informations Remove the change layout button and improve the UX. Add the possibility to hangup a conference or a participant. Gitlab: #1187 Change-Id: Ie7052c75a1dd75e3f96d659c97a3653e6145a882 --- CMakeLists.txt | 2 +- pixmaps/more_vert-24px.svg | 3 + pixmaps/pixmaps.gresource.xml | 1 + src/currentcallview.cpp | 137 +++++++++---- src/mainwindow.cpp | 3 - src/video/video_widget.cpp | 363 ++++++++++++++++++++++++++++++++++ src/video/video_widget.h | 7 + ui/currentcallview.ui | 25 --- 8 files changed, 470 insertions(+), 71 deletions(-) create mode 100644 pixmaps/more_vert-24px.svg diff --git a/CMakeLists.txt b/CMakeLists.txt index b2de4619..92293a06 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,7 +11,7 @@ IF (CMAKE_COMPILER_IS_GNUCC) ENDIF() ENDIF() -set (CMAKE_CXX_STANDARD 14) +set (CMAKE_CXX_STANDARD 17) # set project name and version PROJECT(jami-client-gnome) diff --git a/pixmaps/more_vert-24px.svg b/pixmaps/more_vert-24px.svg new file mode 100644 index 00000000..3a1e4ed2 --- /dev/null +++ b/pixmaps/more_vert-24px.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/> + <path fill="white" d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/> +</svg> \ No newline at end of file diff --git a/pixmaps/pixmaps.gresource.xml b/pixmaps/pixmaps.gresource.xml index c96fdbbb..04aeb59e 100644 --- a/pixmaps/pixmaps.gresource.xml +++ b/pixmaps/pixmaps.gresource.xml @@ -61,5 +61,6 @@ <file alias="retry-white">retry-white.svg</file> <file alias="plugin_white">extension_white_24dp.svg</file> <file alias="view">view.svg</file> + <file alias="more">more_vert-24px.svg</file> </gresource> </gresources> diff --git a/src/currentcallview.cpp b/src/currentcallview.cpp index 2e170163..d808b9a5 100644 --- a/src/currentcallview.cpp +++ b/src/currentcallview.cpp @@ -45,6 +45,7 @@ #include <glib/gi18n.h> #include <QSize> +#include <QJsonObject> #include <set> @@ -92,7 +93,6 @@ struct CurrentCallViewPrivate GtkWidget *video_widget; GtkWidget *frame_chat; GtkWidget *togglebutton_chat; - GtkWidget *togglebutton_view; GtkWidget *togglebutton_muteaudio; GtkWidget *togglebutton_mutevideo; GtkWidget *togglebutton_add_participant; @@ -233,6 +233,7 @@ public: void insertControls(); void checkControlsFading(); void update_view(); + void update_participants_hovers(const QString& callId); CurrentCallView* self = nullptr; // The GTK widget itself CurrentCallViewPrivate* widgets = nullptr; @@ -241,6 +242,7 @@ public: lrc::api::AVModel* avModel_; QMetaObject::Connection state_change_connection; + QMetaObject::Connection layout_change_connection; QMetaObject::Connection update_vcard_connection; QMetaObject::Connection renderer_connection; QMetaObject::Connection smartinfo_refresh_connection; @@ -342,31 +344,6 @@ on_togglebutton_chat_toggled(GtkToggleButton* widget, CurrentCallView* view) } } -static void -on_togglebutton_view_toggled(GtkToggleButton* widget, CurrentCallView* view) -{ - g_return_if_fail(IS_CURRENT_CALL_VIEW(view)); - auto* priv = CURRENT_CALL_VIEW_GET_PRIVATE(view); - - auto confId = priv->cpp->conversation->confId; - if (!confId.isEmpty()) { - auto call = (*priv->cpp->accountInfo)->callModel->getCall(confId); - switch (call.layout) { - case lrc::api::call::Layout::GRID: - (*priv->cpp->accountInfo)->callModel->setConferenceLayout(confId, lrc::api::call::Layout::ONE_WITH_SMALL); - break; - case lrc::api::call::Layout::ONE_WITH_SMALL: - (*priv->cpp->accountInfo)->callModel->setConferenceLayout(confId, lrc::api::call::Layout::ONE); - break; - case lrc::api::call::Layout::ONE: - (*priv->cpp->accountInfo)->callModel->setConferenceLayout(confId, lrc::api::call::Layout::GRID); - break; - } - } - (*priv->cpp->accountInfo)->callModel->setActiveParticipant(confId, ""); - -} - static gboolean on_timer_fade_timeout(CurrentCallView* view) { @@ -390,7 +367,10 @@ on_button_hangup_clicked(CurrentCallView* view) { g_return_if_fail(IS_CURRENT_CALL_VIEW(view)); auto* priv = CURRENT_CALL_VIEW_GET_PRIVATE(view); - (*priv->cpp->accountInfo)->callModel->hangUp(priv->cpp->conversation->callId); + auto callToStop = priv->cpp->conversation->callId; + if (!priv->cpp->conversation->confId.isEmpty()) + callToStop = priv->cpp->conversation->confId; + (*priv->cpp->accountInfo)->callModel->hangUp(callToStop); } static void @@ -460,13 +440,21 @@ on_togglebutton_mutevideo_clicked(CurrentCallView* view) } static gboolean -on_mouse_moved(CurrentCallView* view) +on_mouse_moved(CurrentCallView* view, GdkEvent *event, GtkWidget* widget) { g_return_val_if_fail(IS_CURRENT_CALL_VIEW(view), FALSE); + g_return_val_if_fail(IS_VIDEO_WIDGET(widget), FALSE); auto priv = CURRENT_CALL_VIEW_GET_PRIVATE(view); priv->cpp->time_last_mouse_motion = g_get_monotonic_time(); + // HACK: Cf https://gitlab.gnome.org/GNOME/clutter-gtk/-/issues/11 + // Because the participants_hovers can't have correct mouse events, + // we need to pass the event to the video_widget and check if hovers + // need to be notified + if (event) + video_widget_on_event(VIDEO_WIDGET(priv->video_widget), event); + // since the mouse moved, make sure the controls are shown if (clutter_timeline_get_direction(CLUTTER_TIMELINE(priv->cpp->fade_info)) == CLUTTER_TIMELINE_FORWARD) { clutter_timeline_set_direction(CLUTTER_TIMELINE(priv->cpp->fade_info), CLUTTER_TIMELINE_BACKWARD); @@ -560,7 +548,7 @@ on_video_widget_focus(GtkWidget* widget, GtkDirectionType direction, CurrentCall // otherwise we want the focus to go to and change between the call control buttons if (gtk_widget_child_focus(GTK_WIDGET(priv->hbox_call_controls), direction)) { // selected a child, make sure call controls are shown - on_mouse_moved(view); + on_mouse_moved(view, nullptr, widget); return TRUE; } @@ -962,6 +950,7 @@ CppImpl::CppImpl(CurrentCallView& widget, const lrc::api::Lrc& lrc) CppImpl::~CppImpl() { QObject::disconnect(state_change_connection); + QObject::disconnect(layout_change_connection); QObject::disconnect(update_vcard_connection); QObject::disconnect(renderer_connection); QObject::disconnect(smartinfo_refresh_connection); @@ -1095,7 +1084,6 @@ CppImpl::add_media_handler(lrc::api::plugin::MediaHandlerDetails mediaHandlerDet void CppImpl::updatePluginList() { - auto* priv = CURRENT_CALL_VIEW_GET_PRIVATE(self); auto row = 0; while (GtkWidget* children = GTK_WIDGET(gtk_list_box_get_row_at_index(GTK_LIST_BOX(widgets->list_media_handlers_available), row))) { gtk_container_remove(GTK_CONTAINER(widgets->list_media_handlers_available), children); @@ -1413,7 +1401,9 @@ CppImpl::setCallInfo() try { auto call = (*accountInfo)->callModel->getCall(callToRender); video_widget_set_preview_visible(VIDEO_WIDGET(widgets->video_widget), - (call.status != lrc::api::call::Status::PAUSED) && conversation->confId.isEmpty()); + call.status != lrc::api::call::Status::PAUSED + && call.type != lrc::api::call::Type::CONFERENCE + && call.participantsInfos.empty()); } catch (...) { g_warning("Can't change preview visibility for non existant call"); } @@ -1428,7 +1418,7 @@ CppImpl::setCallInfo() // The renderer doesn't exist for now. Ignore } - + update_participants_hovers(callToRender); // callback for local renderer renderer_connection = QObject::connect( @@ -1445,7 +1435,9 @@ CppImpl::setCallInfo() avModel_, previewRenderer, VIDEO_RENDERER_LOCAL); auto call = (*accountInfo)->callModel->getCall(callToRender); video_widget_set_preview_visible(VIDEO_WIDGET(widgets->video_widget), - (call.status != lrc::api::call::Status::PAUSED)); + call.status != lrc::api::call::Status::PAUSED + && call.type != lrc::api::call::Type::CONFERENCE + && call.participantsInfos.empty()); } } catch (...) { g_warning("Preview renderer is not accessible! This should not happen"); @@ -1473,11 +1465,16 @@ CppImpl::setCallInfo() &*(*accountInfo)->callModel, &lrc::api::NewCallModel::callStatusChanged, [this] (const QString& callId) { + auto callToRender = conversation->callId; + if (!conversation->confId.isEmpty()) + callToRender = conversation->confId; if (callId == conversation->callId) { try { - auto call = (*accountInfo)->callModel->getCall(callId); + auto call = (*accountInfo)->callModel->getCall(callToRender); video_widget_set_preview_visible(VIDEO_WIDGET(widgets->video_widget), - (call.status != lrc::api::call::Status::PAUSED)); + call.status != lrc::api::call::Status::PAUSED + && call.type != lrc::api::call::Type::CONFERENCE + && call.participantsInfos.empty()); } catch (...) { g_warning("Can't set preview visible for inexistant call"); } @@ -1486,6 +1483,13 @@ CppImpl::setCallInfo() } }); + layout_change_connection = QObject::connect( + &*(*accountInfo)->callModel, + &lrc::api::NewCallModel::onParticipantsChanged, + [this] (const QString& callId) { + update_participants_hovers(callId); + }); + update_vcard_connection = QObject::connect( &*(*accountInfo)->contactModel, &lrc::api::ContactModel::contactAdded, @@ -1552,6 +1556,7 @@ CppImpl::insertControls() clutter_actor_add_child(stage, actor_controls); clutter_actor_set_x_align(actor_controls, CLUTTER_ACTOR_ALIGN_CENTER); clutter_actor_set_y_align(actor_controls, CLUTTER_ACTOR_ALIGN_END); + clutter_actor_set_z_position(actor_controls, 4); // Controls should be in front of the hovers clutter_actor_add_child(stage, actor_smartInfo); clutter_actor_set_x_align(actor_smartInfo, CLUTTER_ACTOR_ALIGN_END); @@ -1602,7 +1607,6 @@ CppImpl::insertControls() /* toggle whether or not the chat is displayed */ g_signal_connect(widgets->togglebutton_chat, "toggled", G_CALLBACK(on_togglebutton_chat_toggled), self); - g_signal_connect(widgets->togglebutton_view, "toggled", G_CALLBACK(on_togglebutton_view_toggled), self); /* bind the chat orientation to the gsetting */ widgets->settings = g_settings_new_full(get_settings_schema(), nullptr, nullptr); @@ -1669,6 +1673,61 @@ CppImpl::updateDetails() update_view(); } +void +CppImpl::update_participants_hovers(const QString& callId) +{ + if (callId == conversation->callId or callId == conversation->confId) { + // Remove previouses hovers, because they are now invalid + video_widget_remove_hovers( + VIDEO_WIDGET(widgets->video_widget) + ); + // Update callInfo for the video_widget + video_widget_set_call_info(VIDEO_WIDGET(widgets->video_widget), *accountInfo, callId); + try { + auto call = (*accountInfo)->callModel->getCall(callId); + // Create participant hovers + for (const auto& participant: call.participantsInfos) { + QJsonObject data; + data["x"] = participant["x"].toInt(); + data["y"] = participant["y"].toInt(); + data["w"] = participant["w"].toInt(); + data["h"] = participant["h"].toInt(); + data["active"] = participant["active"] == "true"; + auto bestName = participant["uri"]; + data["isLocal"] = false; + if (bestName == (*accountInfo)->profileInfo.uri) { + bestName = _("me"); + data["isLocal"] = true; + } else { + try { + auto &contact = (*accountInfo)->contactModel->getContact(participant["uri"]); + bestName = contact.profileInfo.alias; + if (bestName.isEmpty()) + bestName = contact.registeredName; + if (bestName.isEmpty()) + bestName = contact.profileInfo.uri; + bestName.remove('\r'); + } catch (...) {} + } + data["bestName"] = bestName; + data["uri"] = participant["uri"]; + video_widget_add_participant_hover( + VIDEO_WIDGET(widgets->video_widget), + data + ); + } + // Update preview visibility, show preview if the call is not hold and + // not a conference host or participant + video_widget_set_preview_visible(VIDEO_WIDGET(widgets->video_widget), + call.status != lrc::api::call::Status::PAUSED + && call.type != lrc::api::call::Type::CONFERENCE + && call.participantsInfos.empty()); + } catch (...) { + g_warning("Can't set preview visible for inexistant call"); + } + } +} + void CppImpl::updateState() { @@ -1720,11 +1779,6 @@ CppImpl::updateState() } catch (std::out_of_range& e) { g_warning("Can't update state for callId=%s", qUtf8Printable(callId)); } - - if (conversation->confId.isEmpty()) - gtk_widget_hide(widgets->togglebutton_view); - else - gtk_widget_show(widgets->togglebutton_view); } void @@ -1957,7 +2011,6 @@ current_call_view_class_init(CurrentCallViewClass *klass) gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, frame_video); gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, frame_chat); gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, togglebutton_chat); - gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, togglebutton_view); gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, togglebutton_add_participant); gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, togglebutton_activate_plugin); gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, togglebutton_transfer); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 8e34e6e7..8cfa2e6e 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -2409,9 +2409,6 @@ CppImpl::slotShowCallView(const std::string& id, lrc::api::conversation::Info or return; } - if (!origin.confId.isEmpty()) - accountInfo_->callModel->setActiveParticipant(origin.confId, origin.callId); - changeView(CURRENT_CALL_VIEW_TYPE, origin); } diff --git a/src/video/video_widget.cpp b/src/video/video_widget.cpp index 884d1731..b8279769 100644 --- a/src/video/video_widget.cpp +++ b/src/video/video_widget.cpp @@ -32,6 +32,10 @@ // LRC #include <api/avmodel.h> +#include <api/call.h> +#include <api/newcallmodel.h> +#include <api/newaccountmodel.h> +#include <api/conversationmodel.h> #include <smartinfohub.h> #include <QSize> @@ -48,6 +52,11 @@ static constexpr const char* JOIN_CALL_KEY = "call_data"; * receive video frames faster than that */ static constexpr int FRAME_RATE_PERIOD = 30; +namespace { namespace details +{ +class CppImpl; +}} + enum SnapshotStatus { NOTHING, HAS_TO_TAKE_ONE, @@ -89,6 +98,9 @@ struct _VideoWidgetPrivate { GtkWidget *popup_menu; lrc::api::AVModel* avModel_; + details::CppImpl* cpp; ///< Non-UI and C++ only code + + GtkWidget *actions_popover; }; struct _VideoWidgetRenderer { @@ -115,6 +127,24 @@ G_DEFINE_TYPE_WITH_PRIVATE(VideoWidget, video_widget, GTK_CLUTTER_TYPE_EMBED); #define VIDEO_WIDGET_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), VIDEO_WIDGET_TYPE, VideoWidgetPrivate)) +namespace { namespace details { + +class CppImpl +{ +public: + explicit CppImpl(VideoWidget& widget) : self(&widget) {} + + std::mutex hoversMtx_ {}; + std::map<std::string, ClutterActor*> hovers_ {}; + std::map<std::string, QJsonObject> hoversInfos_ {}; + VideoWidget* self = nullptr; // The GTK widget itself + AccountInfoPointer const *accountInfo = nullptr; + QString callId {}; +}; + +} +} + /* static prototypes */ static gboolean check_frame_queue (VideoWidget *); static void renderer_stop (VideoWidgetRenderer *); @@ -160,6 +190,9 @@ video_widget_dispose(GObject *object) priv->new_renderer_queue = NULL; } + delete priv->cpp; + priv->cpp = nullptr; + gtk_widget_destroy(priv->popup_menu); G_OBJECT_CLASS(video_widget_parent_class)->dispose(object); @@ -287,6 +320,294 @@ on_drag_end(G_GNUC_UNUSED ClutterDragAction *action, clutter_actor_add_constraint(actor, constraint_y); } +static void +on_hangup(GtkButton *button, VideoWidget *self) +{ + g_return_if_fail(IS_VIDEO_WIDGET(self)); + VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self); + QString uri = QString::fromStdString((gchar*)g_object_get_data(G_OBJECT(button), "uri")); + try { + auto& callModel = (*priv->cpp->accountInfo)->callModel; + auto call = callModel->getCall(priv->cpp->callId); + auto callId = ""; + auto conversations = (*priv->cpp->accountInfo)->conversationModel->allFilteredConversations(); + for (const auto& conversation: conversations) { + if (conversation.participants.empty()) continue; + auto participant = conversation.participants.front(); + if (uri == participant) { + callModel->hangUp(conversation.callId); + return; + } + } + } catch (...) {} +} + +static void +on_maximize(GObject *button, VideoWidget *self) +{ + g_return_if_fail(IS_VIDEO_WIDGET(self)); + VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self); + QString uri = QString::fromStdString((gchar*)g_object_get_data(G_OBJECT(button), "uri")); + bool active = (bool)g_object_get_data(G_OBJECT(button), "active"); + try { + auto& callModel = (*priv->cpp->accountInfo)->callModel; + auto call = callModel->getCall(priv->cpp->callId); + QString callId = ""; + auto conversations = (*priv->cpp->accountInfo)->conversationModel->allFilteredConversations(); + for (const auto& conversation: conversations) { + if (conversation.participants.empty()) continue; + auto participant = conversation.participants.front(); + if (uri == participant) { + callId = conversation.callId; + break; + } + } + switch (call.layout) { + case lrc::api::call::Layout::GRID: + callModel->setActiveParticipant(priv->cpp->callId, callId); + callModel->setConferenceLayout(priv->cpp->callId, lrc::api::call::Layout::ONE_WITH_SMALL); + break; + case lrc::api::call::Layout::ONE_WITH_SMALL: + callModel->setActiveParticipant(priv->cpp->callId, callId); + callModel->setConferenceLayout(priv->cpp->callId, + active? lrc::api::call::Layout::ONE : lrc::api::call::Layout::ONE_WITH_SMALL); + break; + case lrc::api::call::Layout::ONE: + callModel->setActiveParticipant(priv->cpp->callId, callId); + callModel->setConferenceLayout(priv->cpp->callId, lrc::api::call::Layout::GRID); + break; + }; + } catch (...) {} +} + +static void +on_minimize(GtkButton *button, VideoWidget *self) +{ + g_return_if_fail(IS_VIDEO_WIDGET(self)); + VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self); + QString uri = QString::fromStdString((gchar*)g_object_get_data(G_OBJECT(button), "uri")); + try { + auto& callModel = (*priv->cpp->accountInfo)->callModel; + auto call = callModel->getCall(priv->cpp->callId); + switch (call.layout) { + case lrc::api::call::Layout::GRID: + break; + case lrc::api::call::Layout::ONE_WITH_SMALL: + callModel->setConferenceLayout(priv->cpp->callId, lrc::api::call::Layout::GRID); + break; + case lrc::api::call::Layout::ONE: + callModel->setConferenceLayout(priv->cpp->callId, lrc::api::call::Layout::ONE_WITH_SMALL); + break; + }; + } catch (...) {} +} + +static void +on_show_actions_popover(GtkButton *button, VideoWidget *self) +{ + g_return_if_fail(IS_VIDEO_WIDGET(self)); + VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self); + GtkStyleContext* context; + + priv->actions_popover = gtk_popover_new(GTK_WIDGET(self)); + gtk_popover_set_relative_to(GTK_POPOVER(priv->actions_popover), GTK_WIDGET(button)); + + auto* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + bool isLocal = (bool)g_object_get_data(G_OBJECT(button), "isLocal"); + bool active = (bool)g_object_get_data(G_OBJECT(button), "active"); + auto* uri = g_object_get_data(G_OBJECT(button), "uri"); + g_object_set_data(G_OBJECT(priv->actions_popover), "uri", uri); + if (!isLocal) { + auto* hangupBtn = gtk_button_new(); + gtk_button_set_label(GTK_BUTTON(hangupBtn), _("Hangup")); + gtk_box_pack_start(GTK_BOX(box), hangupBtn, TRUE, TRUE, 0); + g_object_set_data(G_OBJECT(hangupBtn), "uri", uri); + context = gtk_widget_get_style_context(hangupBtn); + gtk_style_context_add_class(context, "options-btn"); + auto image = gtk_image_new_from_icon_name("call-stop-symbolic", GTK_ICON_SIZE_BUTTON); + gtk_button_set_relief(GTK_BUTTON(hangupBtn), GTK_RELIEF_NONE); + gtk_button_set_image(GTK_BUTTON(hangupBtn), image); + gtk_button_set_alignment(GTK_BUTTON(hangupBtn), 0, -1); + g_signal_connect(hangupBtn, "clicked", G_CALLBACK(on_hangup), self); + } + try { + auto call = (*priv->cpp->accountInfo)->callModel->getCall(priv->cpp->callId); + if (call.layout != lrc::api::call::Layout::ONE) { + auto* maxBtn = gtk_button_new(); + gtk_button_set_label(GTK_BUTTON(maxBtn), _("Maximize")); + gtk_box_pack_start(GTK_BOX(box), maxBtn, FALSE, TRUE, 0); + g_object_set_data(G_OBJECT(maxBtn), "uri", uri); + g_object_set_data(G_OBJECT(maxBtn), "active", (void*)active); + context = gtk_widget_get_style_context(maxBtn); + gtk_style_context_add_class(context, "options-btn"); + auto image = gtk_image_new_from_icon_name("view-fullscreen-symbolic", GTK_ICON_SIZE_BUTTON); + gtk_button_set_relief(GTK_BUTTON(maxBtn), GTK_RELIEF_NONE); + gtk_button_set_image(GTK_BUTTON(maxBtn), image); + gtk_button_set_alignment(GTK_BUTTON(maxBtn), 0, -1); + g_signal_connect(maxBtn, "clicked", G_CALLBACK(on_maximize), self); + } + if (!(call.layout == lrc::api::call::Layout::GRID + || (call.layout == lrc::api::call::Layout::ONE_WITH_SMALL && !active))) { + auto* minBtn = gtk_button_new(); + gtk_button_set_label(GTK_BUTTON(minBtn), _("Minimize")); + context = gtk_widget_get_style_context(minBtn); + gtk_style_context_add_class(context, "options-btn"); + gtk_box_pack_start(GTK_BOX(box), minBtn, FALSE, TRUE, 0); + g_object_set_data(G_OBJECT(minBtn), "uri", uri); + g_object_set_data(G_OBJECT(minBtn), "active", (void*)active); + auto image = gtk_image_new_from_icon_name("view-restore-symbolic", GTK_ICON_SIZE_BUTTON); + gtk_button_set_relief(GTK_BUTTON(minBtn), GTK_RELIEF_NONE); + gtk_button_set_image(GTK_BUTTON(minBtn), image); + gtk_button_set_alignment(GTK_BUTTON(minBtn), 0, -1); + g_signal_connect(minBtn, "clicked", G_CALLBACK(on_minimize), self); + } + } catch (...) {} + gtk_container_add(GTK_CONTAINER(GTK_POPOVER(priv->actions_popover)), box); + + gtk_widget_set_size_request(priv->actions_popover, -1, -1); +#if GTK_CHECK_VERSION(3,22,0) + gtk_popover_popdown(GTK_POPOVER(priv->actions_popover)); +#else + gtk_widget_show_all(GTK_WIDGET(priv->actions_popover)); +#endif + gtk_widget_show_all(priv->actions_popover); +} + +void +video_widget_add_participant_hover(VideoWidget *self, const QJsonObject& participant) +{ + VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self); + GtkStyleContext* context; + + auto stage = gtk_clutter_embed_get_stage(GTK_CLUTTER_EMBED(self)); + auto* box_participant = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + auto* hover_participant = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); + auto* label_participant = gtk_label_new(participant["bestName"].toString().toLocal8Bit().constData()); + gtk_label_set_xalign(GTK_LABEL(label_participant), 0); + gtk_box_pack_start(GTK_BOX(hover_participant), label_participant, TRUE, TRUE, 0); + gtk_widget_set_visible(GTK_WIDGET(label_participant), TRUE); + + auto call = (*priv->cpp->accountInfo)->callModel->getCall(priv->cpp->callId); + auto isMaster = call.type == lrc::api::call::Type::CONFERENCE; + + if (isMaster) { + auto* options_btn = gtk_button_new(); + + GError *error = nullptr; + auto image = gtk_image_new(); + GdkPixbuf* optionsBuf = gdk_pixbuf_new_from_resource_at_scale("/net/jami/JamiGnome/more", + -1, 12, TRUE, &error); + if (!optionsBuf) { + g_debug("Could not load image: %s", error->message); + g_clear_error(&error); + } else + gtk_image_set_from_pixbuf(GTK_IMAGE(image), optionsBuf); + + + gtk_button_set_relief(GTK_BUTTON(options_btn), GTK_RELIEF_NONE); + gtk_widget_set_tooltip_text(options_btn, _("More options")); + gtk_button_set_image(GTK_BUTTON(options_btn), image); + g_object_set_data(G_OBJECT(options_btn), "uri", (void*)g_strdup(qUtf8Printable(participant["uri"].toString()))); + g_object_set_data(G_OBJECT(options_btn), "isLocal", (void*)participant["isLocal"].toBool()); + g_object_set_data(G_OBJECT(options_btn), "active", (void*)participant["active"].toBool()); + g_signal_connect(options_btn, "clicked", G_CALLBACK(on_show_actions_popover), self); + + gtk_box_pack_start(GTK_BOX(hover_participant), options_btn, FALSE, TRUE, 0); + gtk_widget_set_visible(GTK_WIDGET(options_btn), TRUE); + context = gtk_widget_get_style_context(options_btn); + gtk_style_context_add_class(context, "options-btn"); + } + + gtk_box_pack_end(GTK_BOX(box_participant), hover_participant, FALSE, FALSE, 0); + gtk_widget_set_visible(GTK_WIDGET(hover_participant), TRUE); + gtk_widget_set_size_request(GTK_WIDGET(hover_participant), -1, 32); + + context = gtk_widget_get_style_context(hover_participant); + gtk_style_context_add_class(context, "participant-hover"); + context = gtk_widget_get_style_context(label_participant); + gtk_style_context_add_class(context, "label-hover"); + auto* actor_info = gtk_clutter_actor_new_with_contents(GTK_WIDGET(box_participant)); + + g_object_set_data(G_OBJECT(actor_info), "uri", (void*)g_strdup(qUtf8Printable(participant["uri"].toString()))); + g_object_set_data(G_OBJECT(actor_info), "isLocal", (void*)participant["isLocal"].toBool()); + g_object_set_data(G_OBJECT(actor_info), "active", (void*)participant["active"].toBool()); + + clutter_actor_add_child(stage, actor_info); + clutter_actor_set_y_align(actor_info, CLUTTER_ACTOR_ALIGN_START); + clutter_actor_set_x_align(actor_info, CLUTTER_ACTOR_ALIGN_START); + { + std::lock_guard<std::mutex> lk(priv->cpp->hoversMtx_); + priv->cpp->hoversInfos_[participant["uri"].toString().toStdString()] = participant; + priv->cpp->hovers_[participant["uri"].toString().toStdString()] = actor_info; + } + clutter_actor_hide(actor_info); +} + +void +video_widget_set_call_info(VideoWidget *self, AccountInfoPointer const & accountInfo, const QString& callId) +{ + VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self); + if (not priv or not priv->cpp) + return; + priv->cpp->callId = callId; + priv->cpp->accountInfo = &accountInfo; +} + + +void +video_widget_remove_hovers(VideoWidget *self) +{ + VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self); + std::lock_guard<std::mutex> lk(priv->cpp->hoversMtx_); + priv->cpp->hoversInfos_.clear(); + for (const auto& [uri, actor]: priv->cpp->hovers_) + clutter_actor_destroy(actor); + priv->cpp->hovers_.clear(); +} + +void +video_widget_on_event(VideoWidget *self, GdkEvent* event) +{ + VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self); + g_return_if_fail(priv && event); + + guint button; + // Ignore right click + if (gdk_event_get_button(event, &button) && button == GDK_BUTTON_SECONDARY) + return; + + // HACK-HACK-HACK-HACK-HACK + // https://gitlab.gnome.org/GNOME/clutter-gtk/-/issues/11 + // Retrieve real coordinates in widget and avoid coordinates + // in hovers + GdkDisplay *display = gdk_display_get_default (); + GdkDeviceManager *device_manager = gdk_display_get_device_manager (display); + GdkDevice *device = gdk_device_manager_get_client_pointer (device_manager); + int dx, dy; + gdk_device_get_position (device, NULL, &dx, &dy); + gint wx, wy; + gdk_window_get_origin (gtk_widget_get_window(GTK_WIDGET(self)), &wx, &wy); + int posX = dx - wx; + int posY = dy - wy; + + std::lock_guard<std::mutex> lk(priv->cpp->hoversMtx_); + for (const auto& [uri, actor]: priv->cpp->hovers_) { + if (!CLUTTER_IS_ACTOR(actor)) return; + gfloat x = clutter_actor_get_x(actor), y = clutter_actor_get_y(actor), w, h; + clutter_actor_get_size(actor, &w, &h); + if (posX >= x && posX <= x + w && posY >= y && posY <= y + h) { + // The mouse is in the hover + if (event->type == GDK_BUTTON_PRESS) { + // Let the button clickable without maximizing the participant + if (!(posX >= x + w - 12 && posY >= y + h - 12)) + on_maximize(G_OBJECT(actor), self); + } + clutter_actor_show(actor); + } else { + clutter_actor_hide(actor); + } + } +} /* * video_widget_init() @@ -738,6 +1059,31 @@ check_frame_queue(VideoWidget *self) if (priv->show_preview) clutter_render_image(priv->local); clutter_render_image(priv->remote); + + // HACK: https://gitlab.gnome.org/GNOME/clutter-gtk/-/issues/11 + // Because the CLUTTER_CONTENT_GRAVITY_RESIZE_ASPECT change the ratio of the widget inside the actor + // and we can't get the real dimensions of the rendered renderer, we need to + // re-calculate the real dimensions the actor has + if (priv->remote->actor && priv->remote->v_renderer) { + auto zoomX = priv->remote->v_renderer->size().rwidth() / clutter_actor_get_width(priv->remote->actor); + auto zoomY = priv->remote->v_renderer->size().rheight() / clutter_actor_get_height(priv->remote->actor); + auto zoom = std::max(zoomX, zoomY); + auto real_width = priv->remote->v_renderer->size().rwidth() / zoom; + auto real_height = priv->remote->v_renderer->size().rheight() / zoom; + auto offsetY = (clutter_actor_get_height(priv->remote->actor) - real_height) / 2; + auto offsetX = (clutter_actor_get_width(priv->remote->actor) - real_width) / 2; + + std::lock_guard<std::mutex> lk(priv->cpp->hoversMtx_); + for (const auto& [uri, actor] : priv->cpp->hovers_) { + auto participant = priv->cpp->hoversInfos_[uri]; + + clutter_actor_set_height(actor, participant["h"].toInt() / zoom); + clutter_actor_set_width(actor, participant["w"].toInt() / zoom); + clutter_actor_set_x(actor, offsetX + participant["x"].toInt() / zoom); + clutter_actor_set_y(actor, offsetY + participant["y"].toInt() / zoom); + } + } + if (priv->remote->snapshot_status == HAS_A_NEW_ONE) { priv->remote->snapshot_status = NOTHING; g_signal_emit(G_OBJECT(self), video_widget_signals[SNAPSHOT_SIGNAL], 0); @@ -841,6 +1187,19 @@ GtkWidget* video_widget_new(void) { GtkWidget *self = (GtkWidget *)g_object_new(VIDEO_WIDGET_TYPE, NULL); + auto* priv = VIDEO_WIDGET_GET_PRIVATE(self); + // CSS styles + auto provider = gtk_css_provider_new(); + std::string css = ".participant-hover { background: rgba(0,0,0,0.5); color: white; padding-left: 8; font-size: .8em;}\ + .options-btn:hover {background: rgba(0,0,0,0); border: 0;} \ + .options-btn {background: rgba(0,0,0,0); border: 0;} \ + .label-hover { color: white; }"; + gtk_css_provider_load_from_data(provider, css.c_str(), -1, nullptr); + gtk_style_context_add_provider_for_screen(gdk_display_get_default_screen(gdk_display_get_default()), + GTK_STYLE_PROVIDER(provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + priv->cpp = new details::CppImpl(*VIDEO_WIDGET(self)); + return self; } @@ -906,5 +1265,9 @@ video_widget_set_preview_visible(VideoWidget *self, bool show) VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self); if (priv) { priv->show_preview = show; + if (!show) + clutter_actor_hide(priv->local->actor); + else + clutter_actor_show(priv->local->actor); } } \ No newline at end of file diff --git a/src/video/video_widget.h b/src/video/video_widget.h index 786233b8..e2f87697 100644 --- a/src/video/video_widget.h +++ b/src/video/video_widget.h @@ -23,6 +23,9 @@ #include <gtk/gtk.h> #include <video/renderer.h> #include <api/newvideo.h> +#include <QJsonObject> + +#include "../accountinfopointer.h" namespace lrc { @@ -67,6 +70,10 @@ gboolean video_widget_on_button_press_in_screen_event (VideoWidget *self, void video_widget_take_snapshot (VideoWidget *self); GdkPixbuf* video_widget_get_snapshot (VideoWidget *self); void video_widget_set_preview_visible (VideoWidget *self, bool show); +void video_widget_add_participant_hover(VideoWidget *self, const QJsonObject& participant); +void video_widget_set_call_info(VideoWidget *self, AccountInfoPointer const & accountInfo, const QString& callId); +void video_widget_remove_hovers(VideoWidget *self); +void video_widget_on_event(VideoWidget *self, GdkEvent* event); G_END_DECLS diff --git a/ui/currentcallview.ui b/ui/currentcallview.ui index a2e50ff3..82fd0ef1 100644 --- a/ui/currentcallview.ui +++ b/ui/currentcallview.ui @@ -442,31 +442,6 @@ <property name="fill">True</property> </packing> </child> - <child> - <object class="GtkToggleButton" id="togglebutton_view"> - <style> - <class name="call-button"/> - </style> - <property name="visible">True</property> - <property name="sensitive">True</property> - <property name="can_focus">True</property> - <property name="width-request">48</property> - <property name="height-request">48</property> - <property name="has_tooltip">True</property> - <property name="relief">normal</property> - <property name="tooltip-text" translatable="yes">Toggle view</property> - <property name="image">image_view</property> - <child internal-child="accessible"> - <object class="AtkObject" id="togglebutton_view-atkobject"> - <property name="AtkObject::accessible-name" translatable="yes">Change video layout</property> - </object> - </child> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - </packing> - </child> <child> <object class="GtkToggleButton" id="togglebutton_chat"> <style> -- GitLab