blob: 525ef027a95012084d5ced5f8dfd0f26df5b1fce [file] [log] [blame]
Stepan Salenikovichc64523b2015-02-27 16:31:00 -05001/*
Stepan Salenikovichbbd6c132015-08-20 15:21:48 -04002 * Copyright (C) 2015 Savoir-faire Linux Inc.
Stepan Salenikovichc64523b2015-02-27 16:31:00 -05003 * Author: Stepan Salenikovich <stepan.salenikovich@savoirfairelinux.com>
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 *
19 * Additional permission under GNU GPL version 3 section 7:
20 *
21 * If you modify this program, or any covered work, by linking or
22 * combining it with the OpenSSL project's OpenSSL library (or a
23 * modified version of that library), containing parts covered by the
Stepan Salenikovichbbd6c132015-08-20 15:21:48 -040024 * terms of the OpenSSL or SSLeay licenses, Savoir-faire Linux Inc.
Stepan Salenikovichc64523b2015-02-27 16:31:00 -050025 * grants you additional permission to convey the resulting work.
26 * Corresponding Source for a non-source form of such a combination
27 * shall include the source code for the parts of OpenSSL used as well
28 * as that of the covered work.
29 */
30
31#include "currentcallview.h"
32
33#include <gtk/gtk.h>
Stepan Salenikovich7e283552015-12-21 16:17:52 -050034#include <glib/gi18n.h>
Stepan Salenikovichc64523b2015-02-27 16:31:00 -050035#include <call.h>
Stepan Salenikovichc64523b2015-02-27 16:31:00 -050036#include <callmodel.h>
Stepan Salenikovich36c025c2015-03-03 19:06:44 -050037#include "utils/drawing.h"
38#include "video/video_widget.h"
Stepan Salenikovich4ac89f12015-03-10 16:48:47 -040039#include <video/previewmanager.h>
Stepan Salenikovich6f687072015-03-26 10:43:37 -040040#include <contactmethod.h>
41#include <person.h>
Stepan Salenikovichbbd6c132015-08-20 15:21:48 -040042#include <globalinstances.h>
43#include "native/pixbufmanipulator.h"
Stepan Salenikovicha448f602015-05-29 13:33:06 -040044#include <media/media.h>
45#include <media/text.h>
46#include <media/textrecording.h>
47#include "models/gtkqtreemodel.h"
Stepan Salenikovich67112d12015-06-16 16:57:06 -040048#include "ringnotify.h"
Stepan Salenikovichf6f42652015-07-15 12:46:14 -040049#include <audio/codecmodel.h>
50#include <account.h>
Stepan Salenikovicha5e8e362015-11-05 16:50:48 -050051#include "utils/files.h"
Stepan Salenikoviche178e632015-11-06 13:31:19 -050052#include <clutter-gtk/clutter-gtk.h>
Stepan Salenikovichc64523b2015-02-27 16:31:00 -050053
Stepan Salenikovich0c7aa2a2015-11-06 17:00:08 -050054static constexpr int CONTROLS_FADE_TIMEOUT = 3000000; /* microseconds */
55static constexpr int FADE_DURATION = 500; /* miliseconds */
56
Stepan Salenikovichc64523b2015-02-27 16:31:00 -050057struct _CurrentCallView
58{
59 GtkBox parent;
60};
61
62struct _CurrentCallViewClass
63{
64 GtkBoxClass parent_class;
65};
66
67typedef struct _CurrentCallViewPrivate CurrentCallViewPrivate;
68
69struct _CurrentCallViewPrivate
70{
Stepan Salenikoviche178e632015-11-06 13:31:19 -050071 GtkWidget *hbox_call_info;
72 GtkWidget *hbox_call_controls;
Stepan Salenikovichc64523b2015-02-27 16:31:00 -050073 GtkWidget *image_peer;
74 GtkWidget *label_identity;
75 GtkWidget *label_status;
76 GtkWidget *label_duration;
Stepan Salenikovich36c025c2015-03-03 19:06:44 -050077 GtkWidget *frame_video;
78 GtkWidget *video_widget;
Stepan Salenikovicha5e8e362015-11-05 16:50:48 -050079 GtkWidget *paned_chat;
Stepan Salenikovicha5129f62015-11-05 15:10:59 -050080 GtkWidget *vbox_chat;
Stepan Salenikovicha448f602015-05-29 13:33:06 -040081 GtkWidget *togglebutton_chat;
82 GtkWidget *textview_chat;
83 GtkWidget *button_chat_input;
84 GtkWidget *entry_chat_input;
85 GtkWidget *scrolledwindow_chat;
Stepan Salenikovich77baa522015-07-07 15:29:14 -040086 GtkWidget *button_hangup;
Stepan Salenikovich7e283552015-12-21 16:17:52 -050087 GtkWidget *scalebutton_quality;
88 GtkWidget *checkbutton_autoquality;
89
90 /* flag used to keep track of the video quality scale pressed state;
91 * we do not want to update the codec bitrate until the user releases the
92 * scale button */
93 gboolean quality_scale_pressed;
Stepan Salenikovicha448f602015-05-29 13:33:06 -040094
95 Call *call;
Stepan Salenikovich36c025c2015-03-03 19:06:44 -050096
Stepan Salenikovichc64523b2015-02-27 16:31:00 -050097 QMetaObject::Connection state_change_connection;
98 QMetaObject::Connection call_details_connection;
Stepan Salenikovichc5f08152015-03-19 00:53:23 -040099 QMetaObject::Connection local_renderer_connection;
100 QMetaObject::Connection remote_renderer_connection;
Stepan Salenikovicha448f602015-05-29 13:33:06 -0400101 QMetaObject::Connection media_added_connection;
102 QMetaObject::Connection new_message_connection;
Stepan Salenikovich7b60b592015-06-16 12:29:07 -0400103 QMetaObject::Connection incoming_msg_connection;
Stepan Salenikovicha5e8e362015-11-05 16:50:48 -0500104
105 GSettings *settings;
Stepan Salenikovich0c7aa2a2015-11-06 17:00:08 -0500106
107 // for clutter animations and to know when to fade in/out the overlays
108 ClutterTransition *fade_info;
109 ClutterTransition *fade_controls;
110 gint64 time_last_mouse_motion;
111 guint timer_fade;
Stepan Salenikovichc64523b2015-02-27 16:31:00 -0500112};
113
114G_DEFINE_TYPE_WITH_PRIVATE(CurrentCallView, current_call_view, GTK_TYPE_BOX);
115
116#define CURRENT_CALL_VIEW_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), CURRENT_CALL_VIEW_TYPE, CurrentCallViewPrivate))
117
Stepan Salenikoviche1b54892015-12-13 22:18:44 -0500118enum {
119 VIDEO_DOUBLE_CLICKED,
120 LAST_SIGNAL
121};
122
123static guint current_call_view_signals[LAST_SIGNAL] = { 0 };
124
Stepan Salenikovichc64523b2015-02-27 16:31:00 -0500125static void
126current_call_view_dispose(GObject *object)
127{
128 CurrentCallView *view;
129 CurrentCallViewPrivate *priv;
130
131 view = CURRENT_CALL_VIEW(object);
132 priv = CURRENT_CALL_VIEW_GET_PRIVATE(view);
133
134 QObject::disconnect(priv->state_change_connection);
135 QObject::disconnect(priv->call_details_connection);
Stepan Salenikovichc5f08152015-03-19 00:53:23 -0400136 QObject::disconnect(priv->local_renderer_connection);
137 QObject::disconnect(priv->remote_renderer_connection);
Stepan Salenikovicha448f602015-05-29 13:33:06 -0400138 QObject::disconnect(priv->media_added_connection);
139 QObject::disconnect(priv->new_message_connection);
Stepan Salenikovich7b60b592015-06-16 12:29:07 -0400140 QObject::disconnect(priv->incoming_msg_connection);
Stepan Salenikovichc5f08152015-03-19 00:53:23 -0400141
Stepan Salenikovicha5e8e362015-11-05 16:50:48 -0500142 g_clear_object(&priv->settings);
143
Stepan Salenikovich0c7aa2a2015-11-06 17:00:08 -0500144 g_source_remove(priv->timer_fade);
145
Stepan Salenikovichc64523b2015-02-27 16:31:00 -0500146 G_OBJECT_CLASS(current_call_view_parent_class)->dispose(object);
147}
148
149static void
Stepan Salenikovicha448f602015-05-29 13:33:06 -0400150chat_toggled(GtkToggleButton *togglebutton, CurrentCallView *self)
151{
152 g_return_if_fail(IS_CURRENT_CALL_VIEW(self));
153 CurrentCallViewPrivate *priv = CURRENT_CALL_VIEW_GET_PRIVATE(self);
154
Stepan Salenikovicha448f602015-05-29 13:33:06 -0400155 if (gtk_toggle_button_get_active(togglebutton)) {
Stepan Salenikovicha5129f62015-11-05 15:10:59 -0500156 gtk_widget_show_all(priv->vbox_chat);
Stepan Salenikovicha448f602015-05-29 13:33:06 -0400157 /* create an outgoing media to bring up chat history, if any */
158 priv->call->addOutgoingMedia<Media::Text>();
Stepan Salenikovichbffaf582015-06-22 14:10:40 -0400159 /* change focus to the chat entry */
160 gtk_widget_grab_focus(priv->entry_chat_input);
Stepan Salenikovicha5129f62015-11-05 15:10:59 -0500161 } else {
162 gtk_widget_hide(priv->vbox_chat);
Stepan Salenikovicha448f602015-05-29 13:33:06 -0400163 }
164}
165
166static void
167send_chat(G_GNUC_UNUSED GtkWidget *widget, CurrentCallView *self)
168{
169 g_return_if_fail(IS_CURRENT_CALL_VIEW(self));
170 CurrentCallViewPrivate *priv = CURRENT_CALL_VIEW_GET_PRIVATE(self);
171
172 /* make sure there is text to send */
173 const gchar *text = gtk_entry_get_text(GTK_ENTRY(priv->entry_chat_input));
174 if (text && strlen(text) > 0) {
Stepan Salenikovich6af88db2015-07-17 12:41:29 -0400175 QMap<QString, QString> messages;
176 messages["text/plain"] = text;
177 priv->call->addOutgoingMedia<Media::Text>()->send(messages);
Stepan Salenikovicha448f602015-05-29 13:33:06 -0400178 /* clear the entry */
179 gtk_entry_set_text(GTK_ENTRY(priv->entry_chat_input), "");
180 }
181}
182
183static void
184scroll_to_bottom(GtkAdjustment *adjustment, G_GNUC_UNUSED gpointer user_data)
185{
186 gtk_adjustment_set_value(adjustment,
187 gtk_adjustment_get_upper(adjustment) - gtk_adjustment_get_page_size(adjustment));
188}
189
Stepan Salenikovicha5e8e362015-11-05 16:50:48 -0500190gboolean
191map_boolean_to_orientation(GValue *value, GVariant *variant, G_GNUC_UNUSED gpointer user_data)
192{
193 if (g_variant_is_of_type(variant, G_VARIANT_TYPE_BOOLEAN)) {
194 if (g_variant_get_boolean(variant)) {
195 // true, chat should be horizontal (to the right)
196 g_value_set_enum(value, GTK_ORIENTATION_HORIZONTAL);
197 } else {
198 // false, chat should be vertical (at the bottom)
199 g_value_set_enum(value, GTK_ORIENTATION_VERTICAL);
200 }
201 return TRUE;
202 }
203 return FALSE;
204}
205
Stepan Salenikovich0c7aa2a2015-11-06 17:00:08 -0500206static gboolean
207timeout_check_last_motion_event(CurrentCallView *self)
208{
209 g_return_val_if_fail(IS_CURRENT_CALL_VIEW(self), G_SOURCE_REMOVE);
210 auto priv = CURRENT_CALL_VIEW_GET_PRIVATE(self);
211
212 auto current_time = g_get_monotonic_time();
213 if (current_time - priv->time_last_mouse_motion >= CONTROLS_FADE_TIMEOUT) {
214 // timeout has passed, hide the controls
215 if (clutter_timeline_get_direction(CLUTTER_TIMELINE(priv->fade_info)) == CLUTTER_TIMELINE_BACKWARD) {
216 clutter_timeline_set_direction(CLUTTER_TIMELINE(priv->fade_info), CLUTTER_TIMELINE_FORWARD);
217 clutter_timeline_set_direction(CLUTTER_TIMELINE(priv->fade_controls), CLUTTER_TIMELINE_FORWARD);
218 if (!clutter_timeline_is_playing(CLUTTER_TIMELINE(priv->fade_info))) {
219 clutter_timeline_rewind(CLUTTER_TIMELINE(priv->fade_info));
220 clutter_timeline_rewind(CLUTTER_TIMELINE(priv->fade_controls));
221 clutter_timeline_start(CLUTTER_TIMELINE(priv->fade_info));
222 clutter_timeline_start(CLUTTER_TIMELINE(priv->fade_controls));
223 }
224 }
225 }
226
227 return G_SOURCE_CONTINUE;
228}
229
230static gboolean
231mouse_moved(CurrentCallView *self)
232{
233 g_return_val_if_fail(IS_CURRENT_CALL_VIEW(self), FALSE);
234 auto priv = CURRENT_CALL_VIEW_GET_PRIVATE(self);
235
236 priv->time_last_mouse_motion = g_get_monotonic_time();
237
238 // since the mouse moved, make sure the controls are shown
239 if (clutter_timeline_get_direction(CLUTTER_TIMELINE(priv->fade_info)) == CLUTTER_TIMELINE_FORWARD) {
240 clutter_timeline_set_direction(CLUTTER_TIMELINE(priv->fade_info), CLUTTER_TIMELINE_BACKWARD);
241 clutter_timeline_set_direction(CLUTTER_TIMELINE(priv->fade_controls), CLUTTER_TIMELINE_BACKWARD);
242 if (!clutter_timeline_is_playing(CLUTTER_TIMELINE(priv->fade_info))) {
243 clutter_timeline_rewind(CLUTTER_TIMELINE(priv->fade_info));
244 clutter_timeline_rewind(CLUTTER_TIMELINE(priv->fade_controls));
245 clutter_timeline_start(CLUTTER_TIMELINE(priv->fade_info));
246 clutter_timeline_start(CLUTTER_TIMELINE(priv->fade_controls));
247 }
248 }
249
250 return FALSE; // propogate event
251}
252
253static ClutterTransition *
254create_fade_out_transition()
255{
256 auto transition = clutter_property_transition_new("opacity");
257 clutter_transition_set_from(transition, G_TYPE_UINT, 255);
258 clutter_transition_set_to(transition, G_TYPE_UINT, 0);
259 clutter_timeline_set_duration(CLUTTER_TIMELINE(transition), FADE_DURATION);
260 clutter_timeline_set_repeat_count(CLUTTER_TIMELINE(transition), 0);
261 clutter_timeline_set_progress_mode(CLUTTER_TIMELINE(transition), CLUTTER_EASE_IN_OUT_CUBIC);
262 return transition;
263}
264
Stepan Salenikovich5ed1b492015-11-13 14:03:31 -0500265static gboolean
266video_widget_focus(GtkWidget *widget, GtkDirectionType direction, CurrentCallView *self)
267{
268 g_return_val_if_fail(IS_CURRENT_CALL_VIEW(self), FALSE);
269 auto priv = CURRENT_CALL_VIEW_GET_PRIVATE(self);
270
271 // if this widget already has focus, we want the focus to move to the next widget, otherwise we
272 // will get stuck in a focus loop on the buttons
273 if (gtk_widget_has_focus(widget))
274 return FALSE;
275
276 // otherwise we want the focus to go to and change between the call control buttons
277 if (gtk_widget_child_focus(GTK_WIDGET(priv->hbox_call_controls), direction)) {
278 // selected a child, make sure call controls are shown
279 mouse_moved(self);
280 return TRUE;
281 }
282
283 // did not select the next child, propogate the event
284 return FALSE;
285}
286
Stepan Salenikovich7e283552015-12-21 16:17:52 -0500287static GtkBox *
288gtk_scale_button_get_box(GtkScaleButton *button)
289{
290 GtkWidget *box = NULL;
291 if (auto dock = gtk_scale_button_get_popup(button)) {
292 // the dock is a popover which contains the box
293 box = gtk_bin_get_child(GTK_BIN(dock));
294 if (box) {
295 if (GTK_IS_FRAME(box)) {
296 // support older versions of gtk; the box used to be in a frame
297 box = gtk_bin_get_child(GTK_BIN(box));
298 }
299 }
300 }
301
302 return GTK_BOX(box);
303}
304
305/**
306 * This gets the GtkScaleButtonScale widget (which is a GtkScale) from the
307 * given GtkScaleButton in order to be able to modify its properties and connect
308 * to its signals
309 */
310static GtkScale *
311gtk_scale_button_get_scale(GtkScaleButton *button)
312{
313 GtkScale *scale = NULL;
314
315 if (auto box = gtk_scale_button_get_box(button)) {
316 GList *children = gtk_container_get_children(GTK_CONTAINER(box));
317 for (GList *c = children; c && !scale; c = c->next) {
318 if (GTK_IS_SCALE(c->data))
319 scale = GTK_SCALE(c->data);
320 }
321 g_list_free(children);
322 }
323
324 return scale;
325}
326
327static void
328set_quality(Call *call, gboolean auto_quality_on, double desired_quality)
329{
330 /* set auto quality true or false, also set the bitrate and quality values;
331 * the slider is from 0 to 100, use the min and max vals to scale each value accordingly */
332 if (const auto& codecModel = call->account()->codecModel()) {
333 const auto& videoCodecs = codecModel->videoCodecs();
334
335 for (int i=0; i < videoCodecs->rowCount();i++) {
336 const auto& idx = videoCodecs->index(i,0);
337
338 if (auto_quality_on) {
339 // g_debug("enable auto quality");
340 videoCodecs->setData(idx, "true", CodecModel::Role::AUTO_QUALITY_ENABLED);
341 } else {
342 auto min_bitrate = idx.data(static_cast<int>(CodecModel::Role::MIN_BITRATE)).toInt();
343 auto max_bitrate = idx.data(static_cast<int>(CodecModel::Role::MAX_BITRATE)).toInt();
344 auto min_quality = idx.data(static_cast<int>(CodecModel::Role::MIN_QUALITY)).toInt();
345 auto max_quality = idx.data(static_cast<int>(CodecModel::Role::MAX_QUALITY)).toInt();
346
347 // g_debug("bitrate min: %d, max: %d, quality min: %d, max: %d", min_bitrate, max_bitrate, min_quality, max_quality);
348
349 double bitrate;
350 bitrate = min_bitrate + (double)(max_bitrate - min_bitrate)*(desired_quality/100.0);
351 if (bitrate < 0) bitrate = 0;
352
353 double quality;
354 // note: a lower value means higher quality
355 quality = (double)min_quality - (min_quality - max_quality)*(desired_quality/100.0);
356 if (quality < 0) quality = 0;
357
358 // g_debug("disable auto quality; %% quality: %d; bitrate: %d; quality: %d", (int)desired_quality, (int)bitrate, (int)quality);
359 videoCodecs->setData(idx, "false", CodecModel::Role::AUTO_QUALITY_ENABLED);
360 videoCodecs->setData(idx, QString::number((int)bitrate), CodecModel::Role::BITRATE);
361 videoCodecs->setData(idx, QString::number((int)quality), CodecModel::Role::QUALITY);
362 }
363 }
364 codecModel << CodecModel::EditAction::SAVE;
365 }
366}
367
368static void
369autoquality_toggled(GtkToggleButton *button, CurrentCallView *self)
370{
371 g_return_if_fail(IS_CURRENT_CALL_VIEW(self));
372 CurrentCallViewPrivate *priv = CURRENT_CALL_VIEW_GET_PRIVATE(self);
373
374 gboolean auto_quality_on = gtk_toggle_button_get_active(button);
375
376 auto scale = gtk_scale_button_get_scale(GTK_SCALE_BUTTON(priv->scalebutton_quality));
377 auto plus_button = gtk_scale_button_get_plus_button(GTK_SCALE_BUTTON(priv->scalebutton_quality));
378 auto minus_button = gtk_scale_button_get_minus_button(GTK_SCALE_BUTTON(priv->scalebutton_quality));
379
380 gtk_widget_set_sensitive(GTK_WIDGET(scale), !auto_quality_on);
381 gtk_widget_set_sensitive(plus_button, !auto_quality_on);
382 gtk_widget_set_sensitive(minus_button, !auto_quality_on);
383
384 double desired_quality = gtk_scale_button_get_value(GTK_SCALE_BUTTON(priv->scalebutton_quality));
385
386 if (priv->call)
387 set_quality(priv->call, auto_quality_on, desired_quality);
388}
389
390static void
391quality_changed(GtkScaleButton *button, G_GNUC_UNUSED gdouble value, CurrentCallView *self)
392{
393 g_return_if_fail(IS_CURRENT_CALL_VIEW(self));
394 CurrentCallViewPrivate *priv = CURRENT_CALL_VIEW_GET_PRIVATE(self);
395
396 /* no need to upate quality if auto quality is enabled */
397 if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(priv->checkbutton_autoquality))) return;
398
399 /* only update if the scale button is released, to reduce the number of updates */
400 if (priv->quality_scale_pressed) return;
401
402 /* we get the value directly from the widget, in case this function is not
403 * called from the event */
404 if (priv->call)
405 set_quality(priv->call, FALSE, gtk_scale_button_get_value(button));
406}
407
408static gboolean
409quality_button_pressed(G_GNUC_UNUSED GtkWidget *widget, G_GNUC_UNUSED GdkEvent *event, CurrentCallView *self)
410{
411 g_return_val_if_fail(IS_CURRENT_CALL_VIEW(self), FALSE);
412 CurrentCallViewPrivate *priv = CURRENT_CALL_VIEW_GET_PRIVATE(self);
413
414 priv->quality_scale_pressed = TRUE;
415
416 return GDK_EVENT_PROPAGATE;
417}
418
419static gboolean
420quality_button_released(G_GNUC_UNUSED GtkWidget *widget, G_GNUC_UNUSED GdkEvent *event, CurrentCallView *self)
421{
422 g_return_val_if_fail(IS_CURRENT_CALL_VIEW(self), FALSE);
423 CurrentCallViewPrivate *priv = CURRENT_CALL_VIEW_GET_PRIVATE(self);
424
425 priv->quality_scale_pressed = FALSE;
426
427 /* now make sure the quality gets updated */
428 quality_changed(GTK_SCALE_BUTTON(priv->scalebutton_quality), 0, self);
429
430 return GDK_EVENT_PROPAGATE;
431}
432
Stepan Salenikovicha448f602015-05-29 13:33:06 -0400433static void
Stepan Salenikovichc64523b2015-02-27 16:31:00 -0500434current_call_view_init(CurrentCallView *view)
435{
436 gtk_widget_init_template(GTK_WIDGET(view));
Stepan Salenikovicha448f602015-05-29 13:33:06 -0400437
438 CurrentCallViewPrivate *priv = CURRENT_CALL_VIEW_GET_PRIVATE(view);
439
Stepan Salenikoviche178e632015-11-06 13:31:19 -0500440 /* create video widget and overlay the call info and controls on it */
441 priv->video_widget = video_widget_new();
442 gtk_container_add(GTK_CONTAINER(priv->frame_video), priv->video_widget);
443 gtk_widget_show_all(priv->frame_video);
444
445 auto stage = gtk_clutter_embed_get_stage(GTK_CLUTTER_EMBED(priv->video_widget));
446 auto actor_info = gtk_clutter_actor_new_with_contents(priv->hbox_call_info);
447 auto actor_controls = gtk_clutter_actor_new_with_contents(priv->hbox_call_controls);
448
449 clutter_actor_add_child(stage, actor_info);
450 clutter_actor_set_x_align(actor_info, CLUTTER_ACTOR_ALIGN_FILL);
451 clutter_actor_set_y_align(actor_info, CLUTTER_ACTOR_ALIGN_START);
452
453 clutter_actor_add_child(stage, actor_controls);
454 clutter_actor_set_x_align(actor_controls, CLUTTER_ACTOR_ALIGN_CENTER);
455 clutter_actor_set_y_align(actor_controls, CLUTTER_ACTOR_ALIGN_END);
456
Stepan Salenikovich0c7aa2a2015-11-06 17:00:08 -0500457 /* add fade in and out states to the info and controls */
458 priv->time_last_mouse_motion = g_get_monotonic_time();
459 priv->fade_info = create_fade_out_transition();
460 priv->fade_controls = create_fade_out_transition();
461 clutter_actor_add_transition(actor_info, "fade_info", priv->fade_info);
462 clutter_actor_add_transition(actor_controls, "fade_controls", priv->fade_controls);
463 clutter_timeline_set_direction(CLUTTER_TIMELINE(priv->fade_info), CLUTTER_TIMELINE_BACKWARD);
464 clutter_timeline_set_direction(CLUTTER_TIMELINE(priv->fade_controls), CLUTTER_TIMELINE_BACKWARD);
465 clutter_timeline_stop(CLUTTER_TIMELINE(priv->fade_info));
466 clutter_timeline_stop(CLUTTER_TIMELINE(priv->fade_controls));
467
468 /* have a timer check every 1 second if the controls should fade out */
469 priv->timer_fade = g_timeout_add(1000, (GSourceFunc)timeout_check_last_motion_event, view);
470
471 /* connect to the mouse motion event to reset the last moved time */
472 g_signal_connect_swapped(priv->video_widget, "motion-notify-event", G_CALLBACK(mouse_moved), view);
473 g_signal_connect_swapped(priv->video_widget, "button-press-event", G_CALLBACK(mouse_moved), view);
474 g_signal_connect_swapped(priv->video_widget, "button-release-event", G_CALLBACK(mouse_moved), view);
475
Stepan Salenikovich5ed1b492015-11-13 14:03:31 -0500476 /* manually handle the focus of the video widget to be able to focus on the call controls */
477 g_signal_connect(priv->video_widget, "focus", G_CALLBACK(video_widget_focus), view);
478
Stepan Salenikovicha448f602015-05-29 13:33:06 -0400479 g_signal_connect(priv->togglebutton_chat, "toggled", G_CALLBACK(chat_toggled), view);
480 g_signal_connect(priv->button_chat_input, "clicked", G_CALLBACK(send_chat), view);
481 g_signal_connect(priv->entry_chat_input, "activate", G_CALLBACK(send_chat), view);
482
483 /* the adjustment params will change only when the model is created and when
484 * new messages are added; in these cases we want to scroll to the bottom of
485 * the chat treeview */
486 GtkAdjustment *adjustment = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(priv->scrolledwindow_chat));
487 g_signal_connect(adjustment, "changed", G_CALLBACK(scroll_to_bottom), NULL);
Stepan Salenikovicha5e8e362015-11-05 16:50:48 -0500488
489 // bind the chat location to the gsetting
490 priv->settings = g_settings_new_full(get_ring_schema(), NULL, NULL);
491 g_settings_bind_with_mapping(priv->settings, "chat-pane-horizontal",
492 priv->paned_chat, "orientation",
493 G_SETTINGS_BIND_GET,
494 map_boolean_to_orientation,
495 nullptr, nullptr, nullptr);
Stepan Salenikovich7e283552015-12-21 16:17:52 -0500496
497 g_signal_connect(priv->scalebutton_quality, "value-changed", G_CALLBACK(quality_changed), view);
498 /* customize the quality button scale */
499 if (auto scale_box = gtk_scale_button_get_box(GTK_SCALE_BUTTON(priv->scalebutton_quality))) {
500 priv->checkbutton_autoquality = gtk_check_button_new_with_label(C_("Enable automatic video quality", "Auto"));
501 gtk_widget_show(priv->checkbutton_autoquality);
502 gtk_box_pack_start(GTK_BOX(scale_box), priv->checkbutton_autoquality, FALSE, TRUE, 0);
503 g_signal_connect(priv->checkbutton_autoquality, "toggled", G_CALLBACK(autoquality_toggled), view);
504 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(priv->checkbutton_autoquality), TRUE);
505 }
506 if (auto scale = gtk_scale_button_get_scale(GTK_SCALE_BUTTON(priv->scalebutton_quality))) {
507 g_signal_connect(scale, "button-press-event", G_CALLBACK(quality_button_pressed), view);
508 g_signal_connect(scale, "button-release-event", G_CALLBACK(quality_button_released), view);
509 }
510
511
Stepan Salenikovichc64523b2015-02-27 16:31:00 -0500512}
513
514static void
515current_call_view_class_init(CurrentCallViewClass *klass)
516{
517 G_OBJECT_CLASS(klass)->dispose = current_call_view_dispose;
518
519 gtk_widget_class_set_template_from_resource(GTK_WIDGET_CLASS (klass),
520 "/cx/ring/RingGnome/currentcallview.ui");
521
Stepan Salenikoviche178e632015-11-06 13:31:19 -0500522 gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, hbox_call_info);
523 gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, hbox_call_controls);
Stepan Salenikovichc64523b2015-02-27 16:31:00 -0500524 gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, image_peer);
525 gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, label_identity);
526 gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, label_status);
527 gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, label_duration);
Stepan Salenikovich36c025c2015-03-03 19:06:44 -0500528 gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, frame_video);
Stepan Salenikovicha5e8e362015-11-05 16:50:48 -0500529 gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, paned_chat);
Stepan Salenikovicha5129f62015-11-05 15:10:59 -0500530 gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, vbox_chat);
Stepan Salenikovicha448f602015-05-29 13:33:06 -0400531 gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, togglebutton_chat);
532 gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, textview_chat);
533 gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, button_chat_input);
534 gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, entry_chat_input);
535 gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, scrolledwindow_chat);
Stepan Salenikovich77baa522015-07-07 15:29:14 -0400536 gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, button_hangup);
Stepan Salenikovich7e283552015-12-21 16:17:52 -0500537 gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), CurrentCallView, scalebutton_quality);
Stepan Salenikoviche1b54892015-12-13 22:18:44 -0500538
539 current_call_view_signals[VIDEO_DOUBLE_CLICKED] = g_signal_new (
540 "video-double-clicked",
541 G_TYPE_FROM_CLASS(klass),
542 (GSignalFlags) (G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION),
543 0,
544 nullptr,
545 nullptr,
546 g_cclosure_marshal_VOID__VOID,
547 G_TYPE_NONE, 0);
Stepan Salenikovichc64523b2015-02-27 16:31:00 -0500548}
549
550GtkWidget *
551current_call_view_new(void)
552{
553 return (GtkWidget *)g_object_new(CURRENT_CALL_VIEW_TYPE, NULL);
554}
555
556static void
557update_state(CurrentCallView *view, Call *call)
558{
559 CurrentCallViewPrivate *priv = CURRENT_CALL_VIEW_GET_PRIVATE(view);
560
Stepan Salenikovich7ec8fe82015-06-02 18:26:39 -0400561 gchar *status = g_strdup_printf("%s", call->toHumanStateName().toUtf8().constData());
Stepan Salenikovichc64523b2015-02-27 16:31:00 -0500562
Stepan Salenikovich7ec8fe82015-06-02 18:26:39 -0400563 gtk_label_set_text(GTK_LABEL(priv->label_status), status);
564
565 g_free(status);
Stepan Salenikovichc64523b2015-02-27 16:31:00 -0500566}
567
568static void
569update_details(CurrentCallView *view, Call *call)
570{
571 CurrentCallViewPrivate *priv = CURRENT_CALL_VIEW_GET_PRIVATE(view);
572
573 /* update call duration */
574 QByteArray ba_length = call->length().toLocal8Bit();
575 gtk_label_set_text(GTK_LABEL(priv->label_duration), ba_length.constData());
576}
577
Stepan Salenikovichc5f08152015-03-19 00:53:23 -0400578static void
Stepan Salenikovicha448f602015-05-29 13:33:06 -0400579print_message_to_buffer(const QModelIndex &idx, GtkTextBuffer *buffer)
580{
581 if (idx.isValid()) {
Stepan Salenikovichdeae6252015-06-22 12:51:52 -0400582 auto message = idx.data().value<QString>().toUtf8();
583 auto sender = idx.data(static_cast<int>(Media::TextRecording::Role::AuthorDisplayname)).value<QString>().toUtf8();
Stepan Salenikovicha448f602015-05-29 13:33:06 -0400584
585 GtkTextIter iter;
Stepan Salenikovicha448f602015-05-29 13:33:06 -0400586
Stepan Salenikovichdeae6252015-06-22 12:51:52 -0400587 /* unless its the very first message, insert a new line */
588 if (idx.row() != 0) {
589 gtk_text_buffer_get_end_iter(buffer, &iter);
590 gtk_text_buffer_insert(buffer, &iter, "\n", -1);
591 }
592
593 auto format_sender = g_strconcat(sender.constData(), ": ", NULL);
594 gtk_text_buffer_get_end_iter(buffer, &iter);
595 gtk_text_buffer_insert_with_tags_by_name(buffer, &iter,
596 format_sender, -1,
597 "bold", NULL);
598 g_free(format_sender);
599
600 /* if the sender name is too long, insert a new line after it */
601 if (sender.length() > 20) {
602 gtk_text_buffer_get_end_iter(buffer, &iter);
603 gtk_text_buffer_insert(buffer, &iter, "\n", -1);
604 }
605
606 gtk_text_buffer_get_end_iter(buffer, &iter);
607 gtk_text_buffer_insert(buffer, &iter, message.constData(), -1);
608
Stepan Salenikovicha448f602015-05-29 13:33:06 -0400609 } else {
610 g_warning("QModelIndex in im model is not valid");
611 }
612}
613
614static void
615parse_chat_model(QAbstractItemModel *model, CurrentCallView *self)
616{
617 g_return_if_fail(IS_CURRENT_CALL_VIEW(self));
618 CurrentCallViewPrivate *priv = CURRENT_CALL_VIEW_GET_PRIVATE(self);
619
620 /* new model, disconnect from the old model updates and clear the text buffer */
621 QObject::disconnect(priv->new_message_connection);
622
623 GtkTextBuffer *new_buffer = gtk_text_buffer_new(NULL);
624 gtk_text_view_set_buffer(GTK_TEXT_VIEW(priv->textview_chat), new_buffer);
Stepan Salenikovichdeae6252015-06-22 12:51:52 -0400625
626 /* add tags to the buffer */
627 gtk_text_buffer_create_tag(new_buffer, "bold", "weight", PANGO_WEIGHT_BOLD, NULL);
628
Stepan Salenikovicha448f602015-05-29 13:33:06 -0400629 g_object_unref(new_buffer);
630
631 /* put all the messages in the im model into the text view */
632 for (int row = 0; row < model->rowCount(); ++row) {
633 QModelIndex idx = model->index(row, 0);
634 print_message_to_buffer(idx, new_buffer);
635 }
636
637 /* append new messages */
638 priv->new_message_connection = QObject::connect(
639 model,
640 &QAbstractItemModel::rowsInserted,
641 [priv, model] (const QModelIndex &parent, int first, int last) {
642 for (int row = first; row <= last; ++row) {
643 QModelIndex idx = model->index(row, 0, parent);
644 print_message_to_buffer(idx, gtk_text_view_get_buffer(GTK_TEXT_VIEW(priv->textview_chat)));
645 }
646 }
647 );
648}
Stepan Salenikovichbfe9ac62015-03-11 12:49:20 -0400649
Stepan Salenikoviche1b54892015-12-13 22:18:44 -0500650static gboolean
651on_button_press_in_video_event(GtkWidget *self, GdkEventButton *event, CurrentCallView *view)
652{
653 g_return_val_if_fail(IS_VIDEO_WIDGET(self), FALSE);
654 g_return_val_if_fail(IS_CURRENT_CALL_VIEW(view), FALSE);
655
656 /* on double click */
657 if (event->type == GDK_2BUTTON_PRESS) {
658 g_debug("double click in video");
659 g_signal_emit(G_OBJECT(view), current_call_view_signals[VIDEO_DOUBLE_CLICKED], 0);
660 }
661
662 return GDK_EVENT_PROPAGATE;
663}
664
665
Stepan Salenikovichc64523b2015-02-27 16:31:00 -0500666void
Stepan Salenikovich7b60b592015-06-16 12:29:07 -0400667monitor_incoming_message(CurrentCallView *self, Media::Text *media)
668{
669 g_return_if_fail(IS_CURRENT_CALL_VIEW(self));
670 CurrentCallViewPrivate *priv = CURRENT_CALL_VIEW_GET_PRIVATE(self);
671
672 /* connect to incoming chat messages to open the chat view */
673 QObject::disconnect(priv->incoming_msg_connection);
674 priv->incoming_msg_connection = QObject::connect(
675 media,
676 &Media::Text::messageReceived,
Stepan Salenikovich6af88db2015-07-17 12:41:29 -0400677 [priv] (G_GNUC_UNUSED const QMap<QString,QString>& m) {
Stepan Salenikovich7b60b592015-06-16 12:29:07 -0400678 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(priv->togglebutton_chat), TRUE);
679 }
680 );
681}
682
683void
Stepan Salenikovichc64523b2015-02-27 16:31:00 -0500684current_call_view_set_call_info(CurrentCallView *view, const QModelIndex& idx) {
685 CurrentCallViewPrivate *priv = CURRENT_CALL_VIEW_GET_PRIVATE(view);
686
Guillaume Roguez5d1514b2015-10-22 15:55:31 -0400687 priv->call = CallModel::instance().getCall(idx);
Stepan Salenikovich6f687072015-03-26 10:43:37 -0400688
689 /* get call image */
Stepan Salenikovichbbd6c132015-08-20 15:21:48 -0400690 QVariant var_i = GlobalInstances::pixmapManipulator().callPhoto(priv->call, QSize(60, 60), false);
Stepan Salenikovich6f687072015-03-26 10:43:37 -0400691 std::shared_ptr<GdkPixbuf> image = var_i.value<std::shared_ptr<GdkPixbuf>>();
692 gtk_image_set_from_pixbuf(GTK_IMAGE(priv->image_peer), image.get());
Stepan Salenikovichc64523b2015-02-27 16:31:00 -0500693
694 /* get name */
Stepan Salenikovich9c1f6682015-03-09 16:21:28 -0400695 QVariant var = idx.model()->data(idx, static_cast<int>(Call::Role::Name));
Stepan Salenikovich6f687072015-03-26 10:43:37 -0400696 QByteArray ba_name = var.toString().toUtf8();
Stepan Salenikovichc64523b2015-02-27 16:31:00 -0500697 gtk_label_set_text(GTK_LABEL(priv->label_identity), ba_name.constData());
698
699 /* change some things depending on call state */
Stepan Salenikovicha448f602015-05-29 13:33:06 -0400700 update_state(view, priv->call);
701 update_details(view, priv->call);
Stepan Salenikovichc64523b2015-02-27 16:31:00 -0500702
703 priv->state_change_connection = QObject::connect(
Stepan Salenikovicha448f602015-05-29 13:33:06 -0400704 priv->call,
Stepan Salenikovichc64523b2015-02-27 16:31:00 -0500705 &Call::stateChanged,
Stepan Salenikovicha448f602015-05-29 13:33:06 -0400706 [view, priv]() { update_state(view, priv->call); }
Stepan Salenikovichc64523b2015-02-27 16:31:00 -0500707 );
708
709 priv->call_details_connection = QObject::connect(
Stepan Salenikovicha448f602015-05-29 13:33:06 -0400710 priv->call,
Stepan Salenikovichc64523b2015-02-27 16:31:00 -0500711 static_cast<void (Call::*)(void)>(&Call::changed),
Stepan Salenikovicha448f602015-05-29 13:33:06 -0400712 [view, priv]() { update_details(view, priv->call); }
Stepan Salenikovichc64523b2015-02-27 16:31:00 -0500713 );
Stepan Salenikovich36c025c2015-03-03 19:06:44 -0500714
Stepan Salenikovich36c025c2015-03-03 19:06:44 -0500715 /* check if we already have a renderer */
Stepan Salenikovich0f693232015-04-22 10:45:08 -0400716 video_widget_push_new_renderer(VIDEO_WIDGET(priv->video_widget),
Stepan Salenikovicha448f602015-05-29 13:33:06 -0400717 priv->call->videoRenderer(),
Stepan Salenikovich0f693232015-04-22 10:45:08 -0400718 VIDEO_RENDERER_REMOTE);
Stepan Salenikovich36c025c2015-03-03 19:06:44 -0500719
Stepan Salenikovich8e5c9d02015-03-11 14:07:10 -0400720 /* callback for remote renderer */
Stepan Salenikovichc5f08152015-03-19 00:53:23 -0400721 priv->remote_renderer_connection = QObject::connect(
Stepan Salenikovicha448f602015-05-29 13:33:06 -0400722 priv->call,
Stepan Salenikovich9c1f6682015-03-09 16:21:28 -0400723 &Call::videoStarted,
Stepan Salenikovicha448f602015-05-29 13:33:06 -0400724 [priv](Video::Renderer *renderer) {
Stepan Salenikovich0f693232015-04-22 10:45:08 -0400725 video_widget_push_new_renderer(VIDEO_WIDGET(priv->video_widget),
726 renderer,
727 VIDEO_RENDERER_REMOTE);
Stepan Salenikovich36c025c2015-03-03 19:06:44 -0500728 }
729 );
Stepan Salenikovich4ac89f12015-03-10 16:48:47 -0400730
Stepan Salenikovich8e5c9d02015-03-11 14:07:10 -0400731 /* local renderer */
Guillaume Roguez5d1514b2015-10-22 15:55:31 -0400732 if (Video::PreviewManager::instance().isPreviewing())
Stepan Salenikovich0f693232015-04-22 10:45:08 -0400733 video_widget_push_new_renderer(VIDEO_WIDGET(priv->video_widget),
Guillaume Roguez5d1514b2015-10-22 15:55:31 -0400734 Video::PreviewManager::instance().previewRenderer(),
Stepan Salenikovich0f693232015-04-22 10:45:08 -0400735 VIDEO_RENDERER_LOCAL);
Stepan Salenikovich8e5c9d02015-03-11 14:07:10 -0400736
737 /* callback for local renderer */
Stepan Salenikovichc5f08152015-03-19 00:53:23 -0400738 priv->local_renderer_connection = QObject::connect(
Guillaume Roguez5d1514b2015-10-22 15:55:31 -0400739 &Video::PreviewManager::instance(),
Stepan Salenikovich8e5c9d02015-03-11 14:07:10 -0400740 &Video::PreviewManager::previewStarted,
Stepan Salenikovicha448f602015-05-29 13:33:06 -0400741 [priv](Video::Renderer *renderer) {
Stepan Salenikovich0f693232015-04-22 10:45:08 -0400742 video_widget_push_new_renderer(VIDEO_WIDGET(priv->video_widget),
743 renderer,
744 VIDEO_RENDERER_LOCAL);
Stepan Salenikovich8e5c9d02015-03-11 14:07:10 -0400745 }
746 );
Stepan Salenikovichbfe9ac62015-03-11 12:49:20 -0400747
748 /* catch double click to make full screen */
749 g_signal_connect(priv->video_widget, "button-press-event",
750 G_CALLBACK(on_button_press_in_video_event),
751 view);
Stepan Salenikovicha448f602015-05-29 13:33:06 -0400752
753 /* check if text media is already present */
754 if (priv->call->hasMedia(Media::Media::Type::TEXT, Media::Media::Direction::IN)) {
755 Media::Text *text = priv->call->firstMedia<Media::Text>(Media::Media::Direction::IN);
756 parse_chat_model(text->recording()->instantMessagingModel(), view);
Stepan Salenikovich7b60b592015-06-16 12:29:07 -0400757 monitor_incoming_message(view, text);
Stepan Salenikovicha448f602015-05-29 13:33:06 -0400758 } else if (priv->call->hasMedia(Media::Media::Type::TEXT, Media::Media::Direction::OUT)) {
759 Media::Text *text = priv->call->firstMedia<Media::Text>(Media::Media::Direction::OUT);
760 parse_chat_model(text->recording()->instantMessagingModel(), view);
Stepan Salenikovich7b60b592015-06-16 12:29:07 -0400761 monitor_incoming_message(view, text);
Stepan Salenikovicha448f602015-05-29 13:33:06 -0400762 } else {
763 /* monitor media for messaging text messaging */
764 priv->media_added_connection = QObject::connect(
765 priv->call,
766 &Call::mediaAdded,
767 [view, priv] (Media::Media* media) {
768 if (media->type() == Media::Media::Type::TEXT) {
769 parse_chat_model(((Media::Text*)media)->recording()->instantMessagingModel(), view);
Stepan Salenikovich7b60b592015-06-16 12:29:07 -0400770 monitor_incoming_message(view, (Media::Text*)media);
Stepan Salenikovicha448f602015-05-29 13:33:06 -0400771 QObject::disconnect(priv->media_added_connection);
772 }
773 }
774 );
775 }
Stepan Salenikovich67112d12015-06-16 16:57:06 -0400776
777 /* check if there were any chat notifications and open the chat view if so */
778 if (ring_notify_close_chat_notification(priv->call))
779 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(priv->togglebutton_chat), TRUE);
Stepan Salenikovich7e283552015-12-21 16:17:52 -0500780
781 /* check if auto quality is enabled or not; */
782 if (const auto& codecModel = priv->call->account()->codecModel()) {
783 const auto& videoCodecs = codecModel->videoCodecs();
784 if (videoCodecs->rowCount() > 0) {
785 /* we only need to check the first codec since by default it is ON for all, and the
786 * gnome client sets its ON or OFF for all codecs as well */
787 const auto& idx = videoCodecs->index(0,0);
788 auto auto_quality_enabled = idx.data(static_cast<int>(CodecModel::Role::AUTO_QUALITY_ENABLED)).toString() == "true";
789 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(priv->checkbutton_autoquality), auto_quality_enabled);
790
791 // TODO: save the manual quality setting in the client and set the slider to that value here;
792 // the daemon resets the bitrate/quality between each call, and the default may be
793 // different for each codec, so there is no reason to check it here
794 }
795 }
Stepan Salenikovichc64523b2015-02-27 16:31:00 -0500796}