]> git.0d.be Git - empathy.git/blob - src/empathy-call-window.c
dc587e2d7c046dcbdea10981e9aada90178b3163
[empathy.git] / src / empathy-call-window.c
1 /*
2  * empathy-call-window.c - Source for EmpathyCallWindow
3  * Copyright (C) 2008-2011 Collabora Ltd.
4  * @author Sjoerd Simons <sjoerd.simons@collabora.co.uk>
5  *
6  * This library is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU Lesser General Public
8  * License as published by the Free Software Foundation; either
9  * version 2.1 of the License, or (at your option) any later version.
10  *
11  * This library is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * Lesser General Public License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public
17  * License along with this library; if not, write to the Free Software
18  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
19  */
20
21 #include "config.h"
22
23 #include <stdio.h>
24 #include <stdlib.h>
25
26 #include <math.h>
27
28 #include <gdk/gdkkeysyms.h>
29 #include <gst/gst.h>
30 #include <gtk/gtk.h>
31 #include <glib/gi18n.h>
32
33 #include <clutter/clutter.h>
34 #include <clutter-gtk/clutter-gtk.h>
35 #include <clutter-gst/clutter-gst.h>
36
37 #include <telepathy-glib/util.h>
38 #include <telepathy-farstream/telepathy-farstream.h>
39 #include <telepathy-glib/util.h>
40
41 #include <gst/farsight/fs-element-added-notifier.h>
42 #include <gst/farsight/fs-utils.h>
43
44 #include <libempathy/empathy-camera-monitor.h>
45 #include <libempathy/empathy-gsettings.h>
46 #include <libempathy/empathy-tp-contact-factory.h>
47 #include <libempathy/empathy-request-util.h>
48 #include <libempathy/empathy-utils.h>
49
50 #include <libempathy-gtk/empathy-avatar-image.h>
51 #include <libempathy-gtk/empathy-dialpad-widget.h>
52 #include <libempathy-gtk/empathy-ui-utils.h>
53 #include <libempathy-gtk/empathy-sound-manager.h>
54 #include <libempathy-gtk/empathy-geometry.h>
55 #include <libempathy-gtk/empathy-images.h>
56
57 #define DEBUG_FLAG EMPATHY_DEBUG_VOIP
58 #include <libempathy/empathy-debug.h>
59
60 #include "empathy-call-window.h"
61 #include "empathy-call-window-fullscreen.h"
62 #include "empathy-call-factory.h"
63 #include "empathy-video-widget.h"
64 #include "empathy-about-dialog.h"
65 #include "empathy-audio-src.h"
66 #include "empathy-audio-sink.h"
67 #include "empathy-video-src.h"
68 #include "empathy-mic-menu.h"
69 #include "empathy-preferences.h"
70 #include "empathy-rounded-actor.h"
71 #include "empathy-rounded-rectangle.h"
72 #include "empathy-rounded-texture.h"
73 #include "empathy-camera-menu.h"
74
75 #define CONTENT_HBOX_BORDER_WIDTH 6
76 #define CONTENT_HBOX_SPACING 3
77 #define CONTENT_HBOX_CHILDREN_PACKING_PADDING 3
78
79 #define SELF_VIDEO_SECTION_WIDTH 120
80 #define SELF_VIDEO_SECTION_HEIGHT 90
81 #define SELF_VIDEO_SECTION_MARGIN 10
82
83 #define FLOATING_TOOLBAR_OPACITY 192
84 #define FLOATING_TOOLBAR_WIDTH 280
85 #define FLOATING_TOOLBAR_HEIGHT 36
86 #define FLOATING_TOOLBAR_SPACING 20
87
88 /* The avatar's default width and height are set to the same value because we
89    want a square icon. */
90 #define REMOTE_CONTACT_AVATAR_DEFAULT_WIDTH EMPATHY_VIDEO_WIDGET_DEFAULT_HEIGHT
91 #define REMOTE_CONTACT_AVATAR_DEFAULT_HEIGHT \
92   EMPATHY_VIDEO_WIDGET_DEFAULT_HEIGHT
93
94 #define SMALL_TOOLBAR_SIZE 36
95
96 /* If an video input error occurs, the error message will start with "v4l" */
97 #define VIDEO_INPUT_ERROR_PREFIX "v4l"
98
99 /* The time interval in milliseconds between 2 outgoing rings */
100 #define MS_BETWEEN_RING 500
101
102 G_DEFINE_TYPE(EmpathyCallWindow, empathy_call_window, GTK_TYPE_WINDOW)
103
104 enum {
105   PROP_CALL_HANDLER = 1,
106 };
107
108 typedef enum {
109   RINGING,       /* Incoming call */
110   CONNECTING,    /* Outgoing call */
111   CONNECTED,     /* Connected */
112   HELD,          /* Connected, but on hold */
113   DISCONNECTED,  /* Disconnected */
114   REDIALING      /* Redialing (special case of CONNECTING) */
115 } CallState;
116
117 typedef enum {
118   CAMERA_STATE_OFF = 0,
119   CAMERA_STATE_ON,
120 } CameraState;
121
122 typedef enum {
123   PREVIEW_POS_NONE,
124   PREVIEW_POS_TOP_LEFT,
125   PREVIEW_POS_TOP_RIGHT,
126   PREVIEW_POS_BOTTOM_LEFT,
127   PREVIEW_POS_BOTTOM_RIGHT,
128 } PreviewPosition;
129
130 struct _EmpathyCallWindowPriv
131 {
132   gboolean dispose_has_run;
133   EmpathyCallHandler *handler;
134
135   EmpathyContact *contact;
136
137   EmpathyCameraMonitor *camera_monitor;
138
139   guint call_state;
140   gboolean outgoing;
141
142   GtkUIManager *ui_manager;
143   GtkWidget *errors_vbox;
144   /* widget displays the video received from the remote user. This widget is
145    * alive only during call. */
146   ClutterActor *video_output;
147   ClutterActor *video_preview;
148   ClutterActor *drag_preview;
149   ClutterActor *preview_shown_button;
150   ClutterActor *preview_hidden_button;
151   ClutterActor *preview_rectangle1;
152   ClutterActor *preview_rectangle2;
153   ClutterActor *preview_rectangle3;
154   ClutterActor *preview_rectangle4;
155   ClutterActor *preview_rectangle_box1;
156   ClutterActor *preview_rectangle_box2;
157   ClutterActor *preview_rectangle_box3;
158   ClutterActor *preview_rectangle_box4;
159   ClutterActor *preview_spinner_actor;
160   GtkWidget *preview_spinner_widget;
161   GtkWidget *video_container;
162   GtkWidget *remote_user_avatar_widget;
163   GtkWidget *remote_user_avatar_toolbar;
164   GtkWidget *remote_user_name_toolbar;
165   GtkWidget *status_label;
166   GtkWidget *hangup_button;
167   GtkWidget *audio_call_button;
168   GtkWidget *video_call_button;
169   GtkWidget *mic_button;
170   GtkWidget *volume_button;
171   GtkWidget *camera_button;
172   GtkWidget *dialpad_button;
173   GtkWidget *toolbar;
174   GtkWidget *bottom_toolbar;
175   ClutterActor *floating_toolbar;
176   GtkWidget *pane;
177   GtkAction *menu_fullscreen;
178   GtkAction *menu_swap_camera;
179
180   ClutterState *transitions;
181
182   /* The box that contains self and remote avatar and video
183      input/output. When we redial, we destroy and re-create the box */
184   ClutterActor *video_box;
185   ClutterLayoutManager *video_layout;
186
187   /* Coordinates of the preview drag event's start. */
188   PreviewPosition preview_pos;
189
190   /* We keep a reference on the hbox which contains the main content so we can
191      easilly repack everything when toggling fullscreen */
192   GtkWidget *content_hbox;
193
194   /* These are used to accept or reject an incoming call when the status
195      is RINGING. */
196   GtkWidget *incoming_call_dialog;
197   TpyCallChannel *pending_channel;
198   TpChannelDispatchOperation *pending_cdo;
199   TpAddDispatchOperationContext *pending_context;
200
201   gulong video_output_motion_handler_id;
202   guint bus_message_source_id;
203
204   /* String that contains the queued tones to send after the current ones
205      are sent */
206   GString *tones;
207   gboolean sending_tones;
208   GtkWidget *dtmf_panel;
209
210   /* Details vbox */
211   GtkWidget *details_vbox;
212   GtkWidget *vcodec_encoding_label;
213   GtkWidget *acodec_encoding_label;
214   GtkWidget *vcodec_decoding_label;
215   GtkWidget *acodec_decoding_label;
216
217   GtkWidget *audio_remote_candidate_label;
218   GtkWidget *audio_local_candidate_label;
219   GtkWidget *video_remote_candidate_label;
220   GtkWidget *video_local_candidate_label;
221   GtkWidget *video_remote_candidate_info_img;
222   GtkWidget *video_local_candidate_info_img;
223   GtkWidget *audio_remote_candidate_info_img;
224   GtkWidget *audio_local_candidate_info_img;
225
226   GstElement *video_input;
227   GstElement *video_preview_sink;
228   GstElement *video_output_sink;
229   GstElement *audio_input;
230   GstElement *audio_output;
231   GstElement *pipeline;
232   GstElement *video_tee;
233
234   GstElement *funnel;
235
236   GList *notifiers;
237
238   GTimer *timer;
239   guint timer_id;
240
241   GMutex *lock;
242   gboolean call_started;
243   gboolean sending_video;
244   CameraState camera_state;
245
246   EmpathyCallWindowFullscreen *fullscreen;
247   gboolean is_fullscreen;
248
249   gboolean got_video;
250   guint got_video_src;
251
252   guint inactivity_src;
253
254   /* Those fields represent the state of the window before it actually was in
255      fullscreen mode. */
256   gboolean dialpad_was_visible_before_fs;
257   gint original_width_before_fs;
258   gint original_height_before_fs;
259
260   gint x, y, w, h, dialpad_width;
261   gboolean maximized;
262
263   /* TRUE if the call should be started when the pipeline is playing */
264   gboolean start_call_when_playing;
265   /* TRUE if we requested to set the pipeline in the playing state */
266   gboolean pipeline_playing;
267
268   EmpathySoundManager *sound_mgr;
269
270   GSettings *settings;
271   EmpathyMicMenu *mic_menu;
272   EmpathyCameraMenu *camera_menu;
273 };
274
275 #define GET_PRIV(o) (EMPATHY_CALL_WINDOW (o)->priv)
276
277 static void empathy_call_window_realized_cb (GtkWidget *widget,
278   EmpathyCallWindow *window);
279
280 static gboolean empathy_call_window_delete_cb (GtkWidget *widget,
281   GdkEvent *event, EmpathyCallWindow *window);
282
283 static gboolean empathy_call_window_state_event_cb (GtkWidget *widget,
284   GdkEventWindowState *event, EmpathyCallWindow *window);
285
286 static void empathy_call_window_set_send_video (EmpathyCallWindow *window,
287   CameraState state);
288
289 static void empathy_call_window_hangup_cb (gpointer object,
290   EmpathyCallWindow *window);
291
292 static void empathy_call_window_fullscreen_cb (gpointer object,
293   EmpathyCallWindow *window);
294
295 static void empathy_call_window_fullscreen_toggle (EmpathyCallWindow *window);
296
297 static gboolean empathy_call_window_video_button_press_cb (
298   GtkWidget *video_output, GdkEventButton *event, EmpathyCallWindow *window);
299
300 static gboolean empathy_call_window_key_press_cb (GtkWidget *video_output,
301   GdkEventKey *event, EmpathyCallWindow *window);
302
303 static gboolean empathy_call_window_video_output_motion_notify (
304   GtkWidget *widget, GdkEventMotion *event, EmpathyCallWindow *window);
305
306 static void empathy_call_window_video_menu_popup (EmpathyCallWindow *window,
307   guint button);
308
309 static void empathy_call_window_connect_handler (EmpathyCallWindow *self);
310
311 static void empathy_call_window_dialpad_cb (GtkToggleToolButton *button,
312   EmpathyCallWindow *window);
313
314 static void empathy_call_window_restart_call (EmpathyCallWindow *window);
315
316 static void empathy_call_window_status_message (EmpathyCallWindow *window,
317   gchar *message);
318
319 static gboolean empathy_call_window_bus_message (GstBus *bus,
320   GstMessage *message, gpointer user_data);
321
322 static void
323 empathy_call_window_show_hangup_button (EmpathyCallWindow *self,
324     gboolean show)
325 {
326   gtk_widget_set_visible (self->priv->hangup_button, show);
327   gtk_widget_set_visible (self->priv->audio_call_button, !show);
328   gtk_widget_set_visible (self->priv->video_call_button, !show);
329 }
330
331 static void
332 empathy_call_window_audio_call_cb (GtkToggleToolButton *button,
333     EmpathyCallWindow *self)
334 {
335   g_object_set (self->priv->handler, "initial-video", FALSE, NULL);
336   empathy_call_window_restart_call (self);
337 }
338
339 static void
340 empathy_call_window_video_call_cb (GtkToggleToolButton *button,
341     EmpathyCallWindow *self)
342 {
343   empathy_call_window_set_send_video (self, CAMERA_STATE_ON);
344   g_object_set (self->priv->handler, "initial-video", TRUE, NULL);
345   empathy_call_window_restart_call (self);
346 }
347
348 static void
349 empathy_call_window_emit_tones (EmpathyCallWindow *self)
350 {
351   TpChannel *channel;
352
353   if (tp_str_empty (self->priv->tones->str))
354     return;
355
356   g_object_get (self->priv->handler, "call-channel", &channel, NULL);
357
358   DEBUG ("Emitting multiple tones: %s", self->priv->tones->str);
359
360   tp_cli_channel_interface_dtmf_call_multiple_tones (channel, -1,
361       self->priv->tones->str,
362       NULL, NULL, NULL, NULL);
363
364   self->priv->sending_tones = TRUE;
365
366   g_string_set_size (self->priv->tones, 0);
367
368   g_object_unref (channel);
369 }
370
371 static void
372 empathy_call_window_maybe_emit_tones (EmpathyCallWindow *self)
373 {
374   if (self->priv->sending_tones)
375     return;
376
377   empathy_call_window_emit_tones (self);
378 }
379
380 static void
381 empathy_call_window_tones_stopped_cb (TpChannel *proxy,
382     gboolean arg_cancelled,
383     gpointer user_data,
384     GObject *weak_object)
385 {
386   EmpathyCallWindow *self = EMPATHY_CALL_WINDOW (user_data);
387
388   self->priv->sending_tones = FALSE;
389
390   empathy_call_window_emit_tones (self);
391 }
392
393 static void
394 dtmf_start_tone_cb (EmpathyDialpadWidget *dialpad,
395     TpDTMFEvent event,
396     EmpathyCallWindow *self)
397 {
398   EmpathyCallWindowPriv *priv = GET_PRIV (self);
399
400   g_string_append_c (priv->tones, tp_dtmf_event_to_char (event));
401
402   empathy_call_window_maybe_emit_tones (self);
403 }
404
405 static void
406 empathy_call_window_raise_actors (EmpathyCallWindow *self)
407 {
408   clutter_actor_raise_top (self->priv->floating_toolbar);
409
410   clutter_actor_raise_top (self->priv->preview_rectangle_box1);
411   clutter_actor_raise_top (self->priv->preview_rectangle_box2);
412   clutter_actor_raise_top (self->priv->preview_rectangle_box3);
413   clutter_actor_raise_top (self->priv->preview_rectangle_box4);
414 }
415
416 static void
417 empathy_call_window_show_video_output (EmpathyCallWindow *self,
418     gboolean show)
419 {
420   if (self->priv->video_output != NULL)
421     g_object_set (self->priv->video_output, "visible", show, NULL);
422
423   gtk_widget_set_visible (self->priv->remote_user_avatar_widget, !show);
424
425   empathy_call_window_raise_actors (self);
426 }
427
428 static void
429 create_video_output_widget (EmpathyCallWindow *self)
430 {
431   EmpathyCallWindowPriv *priv = GET_PRIV (self);
432
433   g_assert (priv->video_output == NULL);
434   g_assert (priv->pipeline != NULL);
435
436   priv->video_output = clutter_texture_new ();
437
438   clutter_texture_set_keep_aspect_ratio (CLUTTER_TEXTURE (priv->video_output),
439       TRUE);
440
441   priv->video_output_sink = clutter_gst_video_sink_new (
442       CLUTTER_TEXTURE (priv->video_output));
443
444   clutter_container_add_actor (CLUTTER_CONTAINER (priv->video_box),
445       priv->video_output);
446
447   gtk_widget_add_events (priv->video_container,
448       GDK_BUTTON_PRESS_MASK | GDK_POINTER_MOTION_MASK);
449   g_signal_connect (G_OBJECT (priv->video_container), "button-press-event",
450       G_CALLBACK (empathy_call_window_video_button_press_cb), self);
451 }
452
453 static void
454 create_video_input (EmpathyCallWindow *self)
455 {
456   EmpathyCallWindowPriv *priv = GET_PRIV (self);
457
458   g_assert (priv->video_input == NULL);
459   priv->video_input = empathy_video_src_new ();
460   gst_object_ref (priv->video_input);
461   gst_object_sink (priv->video_input);
462 }
463
464 static gboolean
465 audio_control_volume_to_element (GBinding *binding,
466   const GValue *source_value,
467   GValue *target_value,
468   gpointer user_data)
469 {
470   /* AudioControl volume is 0-255, with -1 for unknown */
471   gint hv;
472
473   hv = g_value_get_int (source_value);
474   if (hv < 0)
475     return FALSE;
476
477   hv = MIN (hv, 255);
478   g_value_set_double (target_value, hv/255.0);
479
480   return TRUE;
481 }
482
483 static gboolean
484 element_volume_to_audio_control (GBinding *binding,
485   const GValue *source_value,
486   GValue *target_value,
487   gpointer user_data)
488 {
489   gdouble ev;
490
491   ev = g_value_get_double (source_value);
492   ev = CLAMP (ev, 0.0, 1.0);
493
494   g_value_set_int (target_value, ev * 255);
495   return TRUE;
496 }
497
498 static void
499 create_audio_input (EmpathyCallWindow *self)
500 {
501   EmpathyCallWindowPriv *priv = GET_PRIV (self);
502
503   g_assert (priv->audio_input == NULL);
504   priv->audio_input = empathy_audio_src_new ();
505   gst_object_ref (priv->audio_input);
506   gst_object_sink (priv->audio_input);
507
508   g_object_bind_property (priv->mic_button, "active",
509     priv->audio_input, "mute",
510     G_BINDING_BIDIRECTIONAL |
511       G_BINDING_INVERT_BOOLEAN | G_BINDING_SYNC_CREATE);
512 }
513
514 static void
515 add_video_preview_to_pipeline (EmpathyCallWindow *self)
516 {
517   EmpathyCallWindowPriv *priv = GET_PRIV (self);
518   GstElement *preview;
519
520   g_assert (priv->video_preview != NULL);
521   g_assert (priv->pipeline != NULL);
522   g_assert (priv->video_input != NULL);
523   g_assert (priv->video_tee != NULL);
524
525   preview = priv->video_preview_sink;
526
527   if (!gst_bin_add (GST_BIN (priv->pipeline), priv->video_input))
528     {
529       g_warning ("Could not add video input to pipeline");
530       return;
531     }
532
533   if (!gst_bin_add (GST_BIN (priv->pipeline), preview))
534     {
535       g_warning ("Could not add video preview to pipeline");
536       return;
537     }
538
539   if (!gst_element_link (priv->video_input, priv->video_tee))
540     {
541       g_warning ("Could not link video input to video tee");
542       return;
543     }
544
545   if (!gst_element_link (priv->video_tee, preview))
546     {
547       g_warning ("Could not link video tee to video preview");
548       return;
549     }
550 }
551
552 static void
553 empathy_call_window_disable_camera_cb (GtkAction *action,
554     EmpathyCallWindow *self)
555 {
556   clutter_actor_destroy (self->priv->preview_hidden_button);
557
558   gtk_toggle_tool_button_set_active (
559       GTK_TOGGLE_TOOL_BUTTON (self->priv->camera_button), FALSE);
560 }
561
562 static void
563 empathy_call_window_minimise_camera_cb (GtkAction *action,
564     EmpathyCallWindow *self)
565 {
566   clutter_actor_hide (self->priv->video_preview);
567   clutter_actor_show (self->priv->preview_hidden_button);
568 }
569
570 static void
571 empathy_call_window_maximise_camera_cb (GtkAction *action,
572     EmpathyCallWindow *self)
573 {
574   clutter_actor_show (self->priv->video_preview);
575   clutter_actor_hide (self->priv->preview_hidden_button);
576 }
577
578 static void
579 empathy_call_window_swap_camera_cb (GtkAction *action,
580     EmpathyCallWindow *self)
581 {
582   const GList *cameras, *l;
583   gchar *current_cam;
584
585   DEBUG ("Swapping the camera");
586
587   cameras = empathy_camera_monitor_get_cameras (self->priv->camera_monitor);
588   current_cam = empathy_video_src_dup_device (
589       EMPATHY_GST_VIDEO_SRC (self->priv->video_input));
590
591   for (l = cameras; l != NULL; l = l->next)
592     {
593       EmpathyCamera *camera = l->data;
594
595       if (!tp_strdiff (camera->device, current_cam))
596         {
597           EmpathyCamera *next;
598
599           if (l->next != NULL)
600             next = l->next->data;
601           else
602             next = cameras->data;
603
604           /* EmpathyCameraMenu will update itself and do the actual change
605            * for us */
606           g_settings_set_string (self->priv->settings,
607               EMPATHY_PREFS_CALL_CAMERA_DEVICE,
608               next->device);
609
610           break;
611         }
612     }
613
614   g_free (current_cam);
615 }
616
617 static void
618 empathy_call_window_camera_added_cb (EmpathyCameraMonitor *monitor,
619     EmpathyCamera *camera,
620     EmpathyCallWindow *self)
621 {
622   const GList *cameras = empathy_camera_monitor_get_cameras (monitor);
623
624   gtk_action_set_visible (self->priv->menu_swap_camera,
625       g_list_length ((GList *) cameras) >= 2);
626 }
627
628 static void
629 empathy_call_window_camera_removed_cb (EmpathyCameraMonitor *monitor,
630     EmpathyCamera *camera,
631     EmpathyCallWindow *self)
632 {
633   const GList *cameras = empathy_camera_monitor_get_cameras (monitor);
634
635   gtk_action_set_visible (self->priv->menu_swap_camera,
636       g_list_length ((GList *) cameras) >= 2);
637 }
638
639 static void
640 empathy_call_window_preview_button_clicked_cb (GtkButton *button,
641     EmpathyCallWindow *self)
642 {
643   GtkWidget *menu;
644
645   menu = gtk_ui_manager_get_widget (self->priv->ui_manager,
646       "/preview-menu");
647   gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL,
648       0, gtk_get_current_event_time ());
649   gtk_menu_shell_select_first (GTK_MENU_SHELL (menu), FALSE);
650 }
651
652 static void
653 empathy_call_window_preview_hidden_button_clicked_cb (GtkButton *button,
654     EmpathyCallWindow *self)
655 {
656   GtkWidget *menu;
657
658   menu = gtk_ui_manager_get_widget (self->priv->ui_manager,
659       "/preview-hidden-menu");
660   gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL,
661       0, gtk_get_current_event_time ());
662   gtk_menu_shell_select_first (GTK_MENU_SHELL (menu), FALSE);
663 }
664
665 static ClutterActor *
666 empathy_call_window_create_preview_rectangle (EmpathyCallWindow *self,
667     ClutterActor **box,
668     ClutterBinAlignment x,
669     ClutterBinAlignment y)
670 {
671   ClutterLayoutManager *layout1, *layout2;
672   ClutterActor *rectangle;
673   ClutterActor *box1, *box2;
674
675   layout1 = clutter_bin_layout_new (CLUTTER_BIN_ALIGNMENT_CENTER,
676       CLUTTER_BIN_ALIGNMENT_START);
677
678   box1 = clutter_box_new (layout1);
679
680   *box = box1;
681
682   rectangle = empathy_rounded_rectangle_new (
683       SELF_VIDEO_SECTION_WIDTH + 5,
684       SELF_VIDEO_SECTION_HEIGHT + 5);
685
686   clutter_actor_set_size (box1,
687       SELF_VIDEO_SECTION_WIDTH + 2 * SELF_VIDEO_SECTION_MARGIN,
688       SELF_VIDEO_SECTION_HEIGHT + 2 * SELF_VIDEO_SECTION_MARGIN +
689       FLOATING_TOOLBAR_HEIGHT + FLOATING_TOOLBAR_SPACING);
690
691   layout2 = clutter_bin_layout_new (CLUTTER_BIN_ALIGNMENT_CENTER,
692       CLUTTER_BIN_ALIGNMENT_CENTER);
693
694   /* We have a box with the margins and the video in the middle inside
695    * a bigger box with an extra bottom margin so we're not on top of
696    * the floating toolbar. */
697   box2 = clutter_box_new (layout2);
698
699   clutter_actor_set_size (box2,
700       SELF_VIDEO_SECTION_WIDTH + 2 * SELF_VIDEO_SECTION_MARGIN,
701       SELF_VIDEO_SECTION_HEIGHT + 2 * SELF_VIDEO_SECTION_MARGIN);
702
703   clutter_container_add_actor (CLUTTER_CONTAINER (box1), box2);
704   clutter_container_add_actor (CLUTTER_CONTAINER (box2), rectangle);
705
706   clutter_bin_layout_add (CLUTTER_BIN_LAYOUT (self->priv->video_layout),
707       box1, x, y);
708
709   clutter_actor_hide (rectangle);
710
711   return rectangle;
712 }
713
714 static void
715 empathy_call_window_create_preview_rectangles (EmpathyCallWindow *self)
716 {
717   self->priv->preview_rectangle1 =
718       empathy_call_window_create_preview_rectangle (self,
719           &self->priv->preview_rectangle_box1,
720           CLUTTER_BIN_ALIGNMENT_START, CLUTTER_BIN_ALIGNMENT_START);
721   self->priv->preview_rectangle2 =
722       empathy_call_window_create_preview_rectangle (self,
723           &self->priv->preview_rectangle_box2,
724           CLUTTER_BIN_ALIGNMENT_START, CLUTTER_BIN_ALIGNMENT_END);
725   self->priv->preview_rectangle3 =
726       empathy_call_window_create_preview_rectangle (self,
727           &self->priv->preview_rectangle_box3,
728           CLUTTER_BIN_ALIGNMENT_END, CLUTTER_BIN_ALIGNMENT_START);
729   self->priv->preview_rectangle4 =
730       empathy_call_window_create_preview_rectangle (self,
731           &self->priv->preview_rectangle_box4,
732           CLUTTER_BIN_ALIGNMENT_END, CLUTTER_BIN_ALIGNMENT_END);
733 }
734
735 static void
736 empathy_call_window_show_preview_rectangles (EmpathyCallWindow *self,
737     gboolean show)
738 {
739   g_object_set (self->priv->preview_rectangle1, "visible", show, NULL);
740   g_object_set (self->priv->preview_rectangle2, "visible", show, NULL);
741   g_object_set (self->priv->preview_rectangle3, "visible", show, NULL);
742   g_object_set (self->priv->preview_rectangle4, "visible", show, NULL);
743 }
744
745 static void
746 empathy_call_window_get_preview_coordinates (EmpathyCallWindow *self,
747     PreviewPosition pos,
748     guint *x,
749     guint *y)
750 {
751   guint ret_x = 0, ret_y = 0;
752   ClutterGeometry box;
753
754   if (!clutter_actor_has_allocation (self->priv->video_box))
755     goto out;
756
757   clutter_actor_get_geometry (self->priv->video_box, &box);
758
759   switch (pos)
760     {
761       case PREVIEW_POS_TOP_LEFT:
762         ret_x = ret_y = SELF_VIDEO_SECTION_MARGIN;
763         break;
764       case PREVIEW_POS_TOP_RIGHT:
765         ret_x = box.width - SELF_VIDEO_SECTION_MARGIN
766             - SELF_VIDEO_SECTION_WIDTH;
767         ret_y = SELF_VIDEO_SECTION_MARGIN;
768         break;
769       case PREVIEW_POS_BOTTOM_LEFT:
770         ret_x = SELF_VIDEO_SECTION_MARGIN;
771         ret_y = box.height - SELF_VIDEO_SECTION_MARGIN
772             - SELF_VIDEO_SECTION_HEIGHT
773             - FLOATING_TOOLBAR_HEIGHT - FLOATING_TOOLBAR_SPACING;
774         break;
775       case PREVIEW_POS_BOTTOM_RIGHT:
776         ret_x = box.width - SELF_VIDEO_SECTION_MARGIN
777             - SELF_VIDEO_SECTION_WIDTH;
778         ret_y = box.height - SELF_VIDEO_SECTION_MARGIN
779             - SELF_VIDEO_SECTION_HEIGHT - FLOATING_TOOLBAR_HEIGHT
780             - FLOATING_TOOLBAR_SPACING;
781         break;
782       default:
783         g_warn_if_reached ();
784     }
785
786 out:
787   if (x != NULL)
788     *x = ret_x;
789
790   if (y != NULL)
791     *y = ret_y;
792 }
793
794 static PreviewPosition
795 empathy_call_window_get_preview_position (EmpathyCallWindow *self,
796     gfloat event_x,
797     gfloat event_y)
798 {
799   ClutterGeometry box;
800   PreviewPosition pos = PREVIEW_POS_NONE;
801
802   if (!clutter_actor_has_allocation (self->priv->video_box))
803     return pos;
804
805   clutter_actor_get_geometry (self->priv->video_box, &box);
806
807   if (0 + SELF_VIDEO_SECTION_MARGIN <= event_x &&
808       event_x <= (0 + SELF_VIDEO_SECTION_MARGIN + (gint) SELF_VIDEO_SECTION_WIDTH) &&
809       0 + SELF_VIDEO_SECTION_MARGIN <= event_y &&
810       event_y <= (0 + SELF_VIDEO_SECTION_MARGIN + (gint) SELF_VIDEO_SECTION_HEIGHT))
811     {
812       pos = PREVIEW_POS_TOP_LEFT;
813     }
814   else if (box.width - SELF_VIDEO_SECTION_MARGIN >= event_x &&
815       event_x >= (box.width - SELF_VIDEO_SECTION_MARGIN - (gint) SELF_VIDEO_SECTION_WIDTH) &&
816       0 + SELF_VIDEO_SECTION_MARGIN <= event_y &&
817       event_y <= (0 + SELF_VIDEO_SECTION_MARGIN + (gint) SELF_VIDEO_SECTION_HEIGHT))
818     {
819       pos = PREVIEW_POS_TOP_RIGHT;
820     }
821   else if (0 + SELF_VIDEO_SECTION_MARGIN <= event_x &&
822       event_x <= (0 + SELF_VIDEO_SECTION_MARGIN + (gint) SELF_VIDEO_SECTION_WIDTH) &&
823       box.height - SELF_VIDEO_SECTION_MARGIN - FLOATING_TOOLBAR_HEIGHT - FLOATING_TOOLBAR_SPACING >= event_y &&
824       event_y >= (box.height - SELF_VIDEO_SECTION_MARGIN - FLOATING_TOOLBAR_HEIGHT - FLOATING_TOOLBAR_SPACING - (gint) SELF_VIDEO_SECTION_HEIGHT))
825     {
826       pos = PREVIEW_POS_BOTTOM_LEFT;
827     }
828   else if (box.width - SELF_VIDEO_SECTION_MARGIN >= event_x &&
829       event_x >= (box.width - SELF_VIDEO_SECTION_MARGIN - (gint) SELF_VIDEO_SECTION_WIDTH) &&
830       box.height - SELF_VIDEO_SECTION_MARGIN - SELF_VIDEO_SECTION_MARGIN - FLOATING_TOOLBAR_HEIGHT - FLOATING_TOOLBAR_SPACING >= event_y &&
831       event_y >= (box.height - SELF_VIDEO_SECTION_MARGIN - FLOATING_TOOLBAR_HEIGHT - FLOATING_TOOLBAR_SPACING - (gint) SELF_VIDEO_SECTION_HEIGHT))
832     {
833       pos = PREVIEW_POS_BOTTOM_RIGHT;
834     }
835
836   return pos;
837 }
838
839 static ClutterActor *
840 empathy_call_window_get_preview_rectangle (EmpathyCallWindow *self,
841     PreviewPosition pos)
842 {
843   ClutterActor *rectangle;
844
845   switch (pos)
846     {
847       case PREVIEW_POS_TOP_LEFT:
848         rectangle = self->priv->preview_rectangle1;
849         break;
850       case PREVIEW_POS_TOP_RIGHT:
851         rectangle = self->priv->preview_rectangle3;
852         break;
853       case PREVIEW_POS_BOTTOM_LEFT:
854         rectangle = self->priv->preview_rectangle2;
855         break;
856       case PREVIEW_POS_BOTTOM_RIGHT:
857         rectangle = self->priv->preview_rectangle4;
858         break;
859       default:
860         rectangle = NULL;
861     }
862
863   return rectangle;
864 }
865
866 static void
867 empathy_call_window_move_video_preview (EmpathyCallWindow *self,
868     PreviewPosition pos)
869 {
870   ClutterBinLayout *layout = CLUTTER_BIN_LAYOUT (self->priv->video_layout);
871
872   DEBUG ("moving the video preview to %d", pos);
873
874   self->priv->preview_pos = pos;
875
876   switch (pos)
877     {
878       case PREVIEW_POS_TOP_LEFT:
879         clutter_bin_layout_set_alignment (layout,
880             self->priv->video_preview,
881             CLUTTER_BIN_ALIGNMENT_START,
882             CLUTTER_BIN_ALIGNMENT_START);
883         break;
884       case PREVIEW_POS_TOP_RIGHT:
885         clutter_bin_layout_set_alignment (layout,
886             self->priv->video_preview,
887             CLUTTER_BIN_ALIGNMENT_END,
888             CLUTTER_BIN_ALIGNMENT_START);
889         break;
890       case PREVIEW_POS_BOTTOM_LEFT:
891         clutter_bin_layout_set_alignment (layout,
892             self->priv->video_preview,
893             CLUTTER_BIN_ALIGNMENT_START,
894             CLUTTER_BIN_ALIGNMENT_END);
895         break;
896       case PREVIEW_POS_BOTTOM_RIGHT:
897         clutter_bin_layout_set_alignment (layout,
898             self->priv->video_preview,
899             CLUTTER_BIN_ALIGNMENT_END,
900             CLUTTER_BIN_ALIGNMENT_END);
901         break;
902       default:
903         g_warn_if_reached ();
904     }
905
906   g_settings_set_enum (self->priv->settings, "camera-position", pos);
907 }
908
909 static void
910 empathy_call_window_highlight_preview_rectangle (EmpathyCallWindow *self,
911     PreviewPosition pos)
912 {
913   ClutterActor *rectangle;
914
915   rectangle = empathy_call_window_get_preview_rectangle (self, pos);
916
917   empathy_rounded_rectangle_set_border_width (
918       EMPATHY_ROUNDED_RECTANGLE (rectangle), 5);
919   empathy_rounded_rectangle_set_border_color (
920       EMPATHY_ROUNDED_RECTANGLE (rectangle), CLUTTER_COLOR_Red);
921 }
922
923 static void
924 empathy_call_window_darken_preview_rectangle (EmpathyCallWindow *self,
925     ClutterActor *rectangle)
926 {
927   empathy_rounded_rectangle_set_border_width (
928       EMPATHY_ROUNDED_RECTANGLE (rectangle), 1);
929   empathy_rounded_rectangle_set_border_color (
930       EMPATHY_ROUNDED_RECTANGLE (rectangle), CLUTTER_COLOR_Black);
931 }
932
933 static void
934 empathy_call_window_darken_preview_rectangles (EmpathyCallWindow *self)
935 {
936   ClutterActor *rectangle;
937
938   rectangle = empathy_call_window_get_preview_rectangle (self,
939       self->priv->preview_pos);
940
941   /* We don't want to darken the rectangle where the preview
942    * currently is. */
943
944   if (self->priv->preview_rectangle1 != rectangle)
945     empathy_call_window_darken_preview_rectangle (self,
946         self->priv->preview_rectangle1);
947
948   if (self->priv->preview_rectangle2 != rectangle)
949     empathy_call_window_darken_preview_rectangle (self,
950         self->priv->preview_rectangle2);
951
952   if (self->priv->preview_rectangle3 != rectangle)
953     empathy_call_window_darken_preview_rectangle (self,
954         self->priv->preview_rectangle3);
955
956   if (self->priv->preview_rectangle4 != rectangle)
957     empathy_call_window_darken_preview_rectangle (self,
958         self->priv->preview_rectangle4);
959 }
960
961 static void
962 empathy_call_window_preview_on_drag_begin_cb (ClutterDragAction *action,
963     ClutterActor *actor,
964     gfloat event_x,
965     gfloat event_y,
966     ClutterModifierType modifiers,
967     EmpathyCallWindow *self)
968 {
969   ClutterActor *stage = clutter_actor_get_stage (actor);
970   gfloat rel_x, rel_y;
971
972   self->priv->drag_preview = clutter_clone_new (actor);
973
974   clutter_container_add_actor (CLUTTER_CONTAINER (stage),
975       self->priv->drag_preview);
976
977   clutter_actor_transform_stage_point (actor, event_x, event_y,
978       &rel_x, &rel_y);
979
980   clutter_actor_set_position (self->priv->drag_preview,
981       event_x - rel_x, event_y - rel_y);
982
983   clutter_drag_action_set_drag_handle (action,
984       self->priv->drag_preview);
985
986   clutter_actor_set_opacity (actor, 0);
987   clutter_actor_hide (self->priv->preview_shown_button);
988
989   empathy_call_window_show_preview_rectangles (self, TRUE);
990   empathy_call_window_darken_preview_rectangles (self);
991 }
992
993 static void
994 empathy_call_window_on_animation_completed_cb (ClutterAnimation *animation,
995     ClutterActor *actor)
996 {
997   clutter_actor_set_opacity (actor, 255);
998 }
999
1000 static void
1001 empathy_call_window_preview_on_drag_end_cb (ClutterDragAction *action,
1002     ClutterActor *actor,
1003     gfloat event_x,
1004     gfloat event_y,
1005     ClutterModifierType modifiers,
1006     EmpathyCallWindow *self)
1007 {
1008   PreviewPosition pos;
1009   guint x, y;
1010
1011   /* Get the position before destroying the drag actor, otherwise the
1012    * preview_box allocation won't be valid and we won't be able to
1013    * calculate the position. */
1014   pos = empathy_call_window_get_preview_position (self, event_x, event_y);
1015
1016   empathy_call_window_get_preview_coordinates (self,
1017       pos != PREVIEW_POS_NONE ? pos : self->priv->preview_pos,
1018       &x, &y);
1019
1020   /* Move the preview to the destination and destroy it afterwards */
1021   clutter_actor_animate (self->priv->drag_preview, CLUTTER_LINEAR, 500,
1022       "x", (gfloat) x,
1023       "y", (gfloat) y,
1024       "signal-swapped-after::completed",
1025         clutter_actor_destroy, self->priv->drag_preview,
1026       "signal-swapped-after::completed",
1027         clutter_actor_show, self->priv->preview_shown_button,
1028       "signal::completed",
1029         empathy_call_window_on_animation_completed_cb, actor,
1030       NULL);
1031
1032   self->priv->drag_preview = NULL;
1033
1034   if (pos != PREVIEW_POS_NONE)
1035     empathy_call_window_move_video_preview (self, pos);
1036
1037   empathy_call_window_show_preview_rectangles (self, FALSE);
1038 }
1039
1040 static void
1041 empathy_call_window_preview_on_drag_motion_cb (ClutterDragAction *action,
1042     ClutterActor *actor,
1043     gfloat delta_x,
1044     gfloat delta_y,
1045     EmpathyCallWindow *self)
1046 {
1047   PreviewPosition pos;
1048   gfloat event_x, event_y;
1049
1050   clutter_drag_action_get_motion_coords (action, &event_x, &event_y);
1051
1052   pos = empathy_call_window_get_preview_position (self, event_x, event_y);
1053
1054   if (pos != PREVIEW_POS_NONE)
1055     empathy_call_window_highlight_preview_rectangle (self, pos);
1056   else
1057     empathy_call_window_darken_preview_rectangles (self);
1058 }
1059
1060 static gboolean
1061 empathy_call_window_preview_enter_event_cb (ClutterActor *actor,
1062     ClutterCrossingEvent *event,
1063     EmpathyCallWindow *self)
1064 {
1065   ClutterActor *rectangle;
1066
1067   rectangle = empathy_call_window_get_preview_rectangle (self,
1068       self->priv->preview_pos);
1069
1070   empathy_call_window_highlight_preview_rectangle (self,
1071       self->priv->preview_pos);
1072
1073   clutter_actor_show (rectangle);
1074
1075   return FALSE;
1076 }
1077
1078 static gboolean
1079 empathy_call_window_preview_leave_event_cb (ClutterActor *actor,
1080     ClutterCrossingEvent *event,
1081     EmpathyCallWindow *self)
1082 {
1083   ClutterActor *rectangle;
1084
1085   rectangle = empathy_call_window_get_preview_rectangle (self,
1086       self->priv->preview_pos);
1087
1088   empathy_call_window_darken_preview_rectangle (self, rectangle);
1089
1090   clutter_actor_hide (rectangle);
1091
1092   return FALSE;
1093 }
1094
1095 static void
1096 create_video_preview (EmpathyCallWindow *self)
1097 {
1098   EmpathyCallWindowPriv *priv = GET_PRIV (self);
1099   ClutterLayoutManager *layout, *layout_center, *layout_end;
1100   ClutterActor *preview;
1101   ClutterActor *box;
1102   ClutterActor *b;
1103   ClutterAction *action;
1104   GtkWidget *button;
1105   PreviewPosition pos;
1106   GdkRGBA transparent = { 0., 0., 0., 0. };
1107
1108   g_assert (priv->video_preview == NULL);
1109
1110   pos = g_settings_get_enum (priv->settings, "camera-position");
1111
1112   preview = empathy_rounded_texture_new ();
1113   clutter_actor_set_size (preview,
1114       SELF_VIDEO_SECTION_WIDTH, SELF_VIDEO_SECTION_HEIGHT);
1115   priv->video_preview_sink = clutter_gst_video_sink_new (
1116       CLUTTER_TEXTURE (preview));
1117
1118   /* Add a little offset to the video preview */
1119   layout = clutter_bin_layout_new (CLUTTER_BIN_ALIGNMENT_CENTER,
1120       CLUTTER_BIN_ALIGNMENT_START);
1121   priv->video_preview = clutter_box_new (layout);
1122   clutter_actor_set_size (priv->video_preview,
1123       SELF_VIDEO_SECTION_WIDTH + 2 * SELF_VIDEO_SECTION_MARGIN,
1124       SELF_VIDEO_SECTION_HEIGHT + 2 * SELF_VIDEO_SECTION_MARGIN +
1125       FLOATING_TOOLBAR_HEIGHT + FLOATING_TOOLBAR_SPACING);
1126
1127   /* Spinner for when changing the camera device */
1128   priv->preview_spinner_widget = gtk_spinner_new ();
1129   priv->preview_spinner_actor = empathy_rounded_actor_new ();
1130   empathy_rounded_actor_set_round_factor (
1131       EMPATHY_ROUNDED_ACTOR (priv->preview_spinner_actor), 16);
1132
1133   g_object_set (priv->preview_spinner_widget, "expand", TRUE, NULL);
1134   gtk_widget_override_background_color (
1135       gtk_clutter_actor_get_widget (
1136           GTK_CLUTTER_ACTOR (priv->preview_spinner_actor)),
1137       GTK_STATE_FLAG_NORMAL, &transparent);
1138   gtk_widget_show (priv->preview_spinner_widget);
1139
1140   gtk_container_add (
1141       GTK_CONTAINER (gtk_clutter_actor_get_widget (
1142           GTK_CLUTTER_ACTOR (priv->preview_spinner_actor))),
1143       priv->preview_spinner_widget);
1144   clutter_actor_set_size (priv->preview_spinner_actor,
1145       SELF_VIDEO_SECTION_WIDTH, SELF_VIDEO_SECTION_HEIGHT);
1146   clutter_actor_set_opacity (priv->preview_spinner_actor, 128);
1147   clutter_actor_hide (priv->preview_spinner_actor);
1148
1149   /* We have a box with the margins and the video in the middle inside
1150    * a bigger box with an extra bottom margin so we're not on top of
1151    * the floating toolbar. */
1152   layout_center = clutter_bin_layout_new (CLUTTER_BIN_ALIGNMENT_CENTER,
1153       CLUTTER_BIN_ALIGNMENT_CENTER);
1154   box = clutter_box_new (layout_center);
1155   clutter_actor_set_size (box,
1156       SELF_VIDEO_SECTION_WIDTH + 2 * SELF_VIDEO_SECTION_MARGIN,
1157       SELF_VIDEO_SECTION_HEIGHT + 2 * SELF_VIDEO_SECTION_MARGIN);
1158
1159   clutter_container_add_actor (CLUTTER_CONTAINER (box), preview);
1160   clutter_container_add_actor (CLUTTER_CONTAINER (box),
1161       priv->preview_spinner_actor);
1162   clutter_container_add_actor (CLUTTER_CONTAINER (priv->video_preview), box);
1163
1164   g_object_set (priv->video_preview_sink,
1165       "sync", FALSE,
1166       "async", FALSE,
1167       NULL);
1168
1169   /* Translators: this is an "Info" label. It should be as short
1170    * as possible. */
1171   button = gtk_button_new_with_label (_("i"));
1172   priv->preview_shown_button = b = empathy_rounded_actor_new ();
1173   gtk_container_add (
1174       GTK_CONTAINER (gtk_clutter_actor_get_widget (GTK_CLUTTER_ACTOR (b))),
1175       button);
1176   clutter_actor_set_size (b, 24, 24);
1177
1178   layout_end = clutter_bin_layout_new (CLUTTER_BIN_ALIGNMENT_END,
1179       CLUTTER_BIN_ALIGNMENT_END);
1180   box = clutter_box_new (layout_end);
1181   clutter_actor_set_size (box,
1182       SELF_VIDEO_SECTION_WIDTH,
1183       SELF_VIDEO_SECTION_HEIGHT + SELF_VIDEO_SECTION_MARGIN);
1184
1185   clutter_container_add_actor (CLUTTER_CONTAINER (box), b);
1186   clutter_container_add_actor (CLUTTER_CONTAINER (priv->video_preview), box);
1187
1188   g_signal_connect (button, "clicked",
1189       G_CALLBACK (empathy_call_window_preview_button_clicked_cb),
1190       self);
1191
1192   /* Translators: this is an "Info" label. It should be as short
1193    * as possible. */
1194   button = gtk_button_new_with_label (_("i"));
1195   b = empathy_rounded_actor_new ();
1196   gtk_container_add (
1197       GTK_CONTAINER (gtk_clutter_actor_get_widget (GTK_CLUTTER_ACTOR (b))),
1198       button);
1199   clutter_actor_set_size (b, 24, 24);
1200   priv->preview_hidden_button = b;
1201
1202   clutter_bin_layout_add (CLUTTER_BIN_LAYOUT (priv->video_layout),
1203       priv->preview_hidden_button,
1204       CLUTTER_BIN_ALIGNMENT_START,
1205       CLUTTER_BIN_ALIGNMENT_END);
1206
1207   self->priv->preview_pos = PREVIEW_POS_BOTTOM_LEFT;
1208
1209   clutter_actor_hide (priv->preview_hidden_button);
1210
1211   g_signal_connect (button, "clicked",
1212       G_CALLBACK (empathy_call_window_preview_hidden_button_clicked_cb),
1213       self);
1214
1215   clutter_bin_layout_add (CLUTTER_BIN_LAYOUT (priv->video_layout),
1216       priv->video_preview,
1217       CLUTTER_BIN_ALIGNMENT_START,
1218       CLUTTER_BIN_ALIGNMENT_END);
1219
1220   empathy_call_window_move_video_preview (self, pos);
1221
1222   action = clutter_drag_action_new ();
1223   g_signal_connect (action, "drag-begin",
1224       G_CALLBACK (empathy_call_window_preview_on_drag_begin_cb), self);
1225   g_signal_connect (action, "drag-end",
1226       G_CALLBACK (empathy_call_window_preview_on_drag_end_cb), self);
1227   g_signal_connect (action, "drag-motion",
1228       G_CALLBACK (empathy_call_window_preview_on_drag_motion_cb), self);
1229
1230   g_signal_connect (preview, "enter-event",
1231       G_CALLBACK (empathy_call_window_preview_enter_event_cb), self);
1232   g_signal_connect (preview, "leave-event",
1233       G_CALLBACK (empathy_call_window_preview_leave_event_cb), self);
1234
1235   clutter_actor_add_action (preview, action);
1236   clutter_actor_set_reactive (preview, TRUE);
1237   clutter_actor_set_reactive (priv->preview_shown_button, TRUE);
1238 }
1239
1240 static void
1241 empathy_call_window_start_camera_spinning (EmpathyCallWindow *self)
1242 {
1243   clutter_actor_show (self->priv->preview_spinner_actor);
1244   gtk_spinner_start (GTK_SPINNER (self->priv->preview_spinner_widget));
1245 }
1246
1247 static void
1248 empathy_call_window_stop_camera_spinning (EmpathyCallWindow *self)
1249 {
1250   clutter_actor_hide (self->priv->preview_spinner_actor);
1251   gtk_spinner_stop (GTK_SPINNER (self->priv->preview_spinner_widget));
1252 }
1253
1254 void
1255 empathy_call_window_play_camera (EmpathyCallWindow *self,
1256     gboolean play)
1257 {
1258   EmpathyCallWindowPriv *priv = GET_PRIV (self);
1259   GstElement *preview;
1260   GstState state;
1261
1262   if (priv->video_preview == NULL)
1263     {
1264       create_video_preview (self);
1265       add_video_preview_to_pipeline (self);
1266     }
1267
1268   if (play)
1269     {
1270       state = GST_STATE_PLAYING;
1271     }
1272   else
1273     {
1274       empathy_call_window_start_camera_spinning (self);
1275       state = GST_STATE_NULL;
1276     }
1277
1278   preview = priv->video_preview_sink;
1279
1280   gst_element_set_state (preview, state);
1281   gst_element_set_state (priv->video_tee, state);
1282   gst_element_set_state (priv->video_input, state);
1283 }
1284
1285 static void
1286 display_video_preview (EmpathyCallWindow *self,
1287     gboolean display)
1288 {
1289   EmpathyCallWindowPriv *priv = GET_PRIV (self);
1290
1291   if (priv->video_preview == NULL)
1292     {
1293       create_video_preview (self);
1294       add_video_preview_to_pipeline (self);
1295     }
1296
1297   if (display)
1298     {
1299       /* Display the video preview */
1300       DEBUG ("Show video preview");
1301
1302       empathy_call_window_play_camera (self, TRUE);
1303       clutter_actor_show (priv->video_preview);
1304       clutter_actor_raise_top (priv->floating_toolbar);
1305     }
1306   else
1307     {
1308       /* Hide the video preview */
1309       DEBUG ("Hide video preview");
1310
1311       if (priv->video_preview != NULL)
1312         {
1313           clutter_actor_hide (priv->video_preview);
1314           empathy_call_window_play_camera (self, FALSE);
1315         }
1316     }
1317 }
1318
1319 static void
1320 empathy_call_window_set_state_connecting (EmpathyCallWindow *window)
1321 {
1322   EmpathyCallWindowPriv *priv = GET_PRIV (window);
1323
1324   empathy_call_window_status_message (window, _("Connecting…"));
1325   priv->call_state = CONNECTING;
1326
1327   /* Show the toolbar */
1328   clutter_state_set_state (priv->transitions, "fade-in");
1329
1330   if (priv->outgoing)
1331     empathy_sound_manager_start_playing (priv->sound_mgr, GTK_WIDGET (window),
1332         EMPATHY_SOUND_PHONE_OUTGOING, MS_BETWEEN_RING);
1333 }
1334
1335 static void
1336 disable_camera (EmpathyCallWindow *self)
1337 {
1338   EmpathyCallWindowPriv *priv = GET_PRIV (self);
1339
1340   if (priv->camera_state == CAMERA_STATE_OFF)
1341     return;
1342
1343   DEBUG ("Disable camera");
1344
1345   empathy_call_window_set_send_video (self, CAMERA_STATE_OFF);
1346
1347   priv->camera_state = CAMERA_STATE_OFF;
1348 }
1349
1350 static void
1351 enable_camera (EmpathyCallWindow *self)
1352 {
1353   EmpathyCallWindowPriv *priv = GET_PRIV (self);
1354
1355   if (priv->camera_state == CAMERA_STATE_ON)
1356     return;
1357
1358   if (priv->video_input == NULL)
1359     {
1360       DEBUG ("Can't enable camera, no input");
1361       return;
1362     }
1363
1364   DEBUG ("Enable camera");
1365
1366   empathy_call_window_set_send_video (self, CAMERA_STATE_ON);
1367
1368   priv->camera_state = CAMERA_STATE_ON;
1369 }
1370
1371 static void
1372 empathy_call_window_camera_toggled_cb (GtkToggleToolButton *toggle,
1373   EmpathyCallWindow *self)
1374 {
1375   if (gtk_toggle_tool_button_get_active (toggle))
1376     enable_camera (self);
1377   else
1378     disable_camera (self);
1379 }
1380
1381 static void
1382 create_pipeline (EmpathyCallWindow *self)
1383 {
1384   EmpathyCallWindowPriv *priv = GET_PRIV (self);
1385   GstBus *bus;
1386
1387   g_assert (priv->pipeline == NULL);
1388
1389   priv->pipeline = gst_pipeline_new (NULL);
1390   priv->pipeline_playing = FALSE;
1391
1392   priv->video_tee = gst_element_factory_make ("tee", NULL);
1393   gst_object_ref (priv->video_tee);
1394   gst_object_sink (priv->video_tee);
1395
1396   gst_bin_add (GST_BIN (priv->pipeline), priv->video_tee);
1397
1398   bus = gst_pipeline_get_bus (GST_PIPELINE (priv->pipeline));
1399   priv->bus_message_source_id = gst_bus_add_watch (bus,
1400       empathy_call_window_bus_message, self);
1401
1402   g_object_unref (bus);
1403 }
1404
1405 static void
1406 empathy_call_window_settings_cb (GtkAction *action,
1407     EmpathyCallWindow *self)
1408 {
1409   gchar *args = g_strdup_printf ("-p %s",
1410       empathy_preferences_tab_to_string (EMPATHY_PREFERENCES_TAB_CALLS));
1411
1412   empathy_launch_program (BIN_DIR, "empathy", args);
1413
1414   g_free (args);
1415 }
1416
1417 static void
1418 empathy_call_window_contents_cb (GtkAction *action,
1419     EmpathyCallWindow *self)
1420 {
1421   empathy_url_show (GTK_WIDGET (self), "ghelp:empathy?audio-video");
1422 }
1423
1424 static void
1425 empathy_call_window_debug_cb (GtkAction *action,
1426     EmpathyCallWindow *self)
1427 {
1428   empathy_launch_program (BIN_DIR, "empathy-debugger", "-s Empathy.Call");
1429 }
1430
1431 static void
1432 empathy_call_window_about_cb (GtkAction *action,
1433     EmpathyCallWindow *self)
1434 {
1435   empathy_about_dialog_new (GTK_WINDOW (self));
1436 }
1437
1438 static gboolean
1439 empathy_call_window_toolbar_timeout (gpointer data)
1440 {
1441   EmpathyCallWindow *self = data;
1442
1443   /* We don't want to hide the toolbar if we're not in a call, as
1444    * to show the call status all the time. */
1445   if (self->priv->call_state != CONNECTING &&
1446       self->priv->call_state != DISCONNECTED)
1447     clutter_state_set_state (self->priv->transitions, "fade-out");
1448
1449   return TRUE;
1450 }
1451
1452 static gboolean
1453 empathy_call_window_motion_notify_cb (GtkWidget *widget,
1454     GdkEvent *event,
1455     EmpathyCallWindow *self)
1456 {
1457   clutter_state_set_state (self->priv->transitions, "fade-in");
1458
1459   if (self->priv->inactivity_src > 0)
1460     g_source_remove (self->priv->inactivity_src);
1461
1462   self->priv->inactivity_src = g_timeout_add_seconds (3,
1463       empathy_call_window_toolbar_timeout, self);
1464
1465   return FALSE;
1466 }
1467
1468 static gboolean
1469 empathy_call_window_configure_event_cb (GtkWidget *widget,
1470     GdkEvent  *event,
1471     EmpathyCallWindow *self)
1472 {
1473   GdkWindow *gdk_window;
1474   GdkWindowState window_state;
1475
1476   gtk_window_get_position (GTK_WINDOW (self), &self->priv->x, &self->priv->y);
1477   gtk_window_get_size (GTK_WINDOW (self), &self->priv->w, &self->priv->h);
1478
1479   gtk_widget_get_preferred_width (self->priv->dtmf_panel,
1480       &self->priv->dialpad_width, NULL);
1481
1482   gdk_window = gtk_widget_get_window (widget);
1483   window_state = gdk_window_get_state (gdk_window);
1484   self->priv->maximized = (window_state & GDK_WINDOW_STATE_MAXIMIZED);
1485
1486   return FALSE;
1487 }
1488
1489 static void
1490 empathy_call_window_destroyed_cb (GtkWidget *object,
1491     EmpathyCallWindow *self)
1492 {
1493   if (gtk_widget_get_visible (self->priv->dtmf_panel))
1494     {
1495       /* Save the geometry as if the dialpad was hidden. */
1496       empathy_geometry_save_values (GTK_WINDOW (self),
1497           self->priv->x, self->priv->y,
1498           self->priv->w - self->priv->dialpad_width, self->priv->h,
1499           self->priv->maximized);
1500     }
1501 }
1502
1503 static void
1504 empathy_call_window_stage_allocation_changed_cb (ClutterActor *stage,
1505     GParamSpec *pspec,
1506     ClutterBindConstraint *constraint)
1507 {
1508   ClutterActorBox allocation;
1509
1510   clutter_actor_get_allocation_box (stage, &allocation);
1511
1512   clutter_bind_constraint_set_offset (constraint,
1513       allocation.y2 - allocation.y1 -
1514       FLOATING_TOOLBAR_SPACING - FLOATING_TOOLBAR_HEIGHT);
1515 }
1516
1517 static void
1518 empathy_call_window_incoming_call_response_cb (GtkDialog *dialog,
1519     gint response_id,
1520     EmpathyCallWindow *self)
1521 {
1522   switch (response_id)
1523     {
1524       case GTK_RESPONSE_ACCEPT:
1525         tp_channel_dispatch_operation_handle_with_async (
1526             self->priv->pending_cdo, EMPATHY_CALL_BUS_NAME, NULL, NULL);
1527
1528         tp_clear_object (&self->priv->pending_cdo);
1529         tp_clear_object (&self->priv->pending_channel);
1530         tp_clear_object (&self->priv->pending_context);
1531
1532         break;
1533       case GTK_RESPONSE_CANCEL:
1534         tp_channel_dispatch_operation_close_channels_async (
1535             self->priv->pending_cdo, NULL, NULL);
1536
1537         empathy_call_window_status_message (self, _("Disconnected"));
1538         self->priv->call_state = DISCONNECTED;
1539         break;
1540       default:
1541         g_warn_if_reached ();
1542     }
1543 }
1544
1545 static void
1546 empathy_call_window_set_state_ringing (EmpathyCallWindow *self)
1547 {
1548   gboolean video;
1549
1550   g_assert (self->priv->call_state != CONNECTED);
1551
1552   video = tpy_call_channel_has_initial_video (self->priv->pending_channel);
1553
1554   empathy_call_window_status_message (self, _("Incoming call"));
1555   self->priv->call_state = RINGING;
1556
1557   self->priv->incoming_call_dialog = gtk_message_dialog_new (
1558       GTK_WINDOW (self), GTK_DIALOG_MODAL,
1559       GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE,
1560       video ? _("Incoming video call from %s") : _("Incoming call from %s"),
1561       empathy_contact_get_alias (self->priv->contact));
1562
1563   gtk_dialog_add_buttons (GTK_DIALOG (self->priv->incoming_call_dialog),
1564       _("Reject"), GTK_RESPONSE_CANCEL,
1565       _("Answer"), GTK_RESPONSE_ACCEPT,
1566       NULL);
1567
1568   g_signal_connect (self->priv->incoming_call_dialog, "response",
1569       G_CALLBACK (empathy_call_window_incoming_call_response_cb), self);
1570   gtk_widget_show (self->priv->incoming_call_dialog);
1571 }
1572
1573 static void
1574 empathy_call_window_cdo_invalidated_cb (TpProxy *channel,
1575     guint domain,
1576     gint code,
1577     gchar *message,
1578     EmpathyCallWindow *self)
1579 {
1580   tp_clear_object (&self->priv->pending_cdo);
1581   tp_clear_object (&self->priv->pending_channel);
1582   tp_clear_object (&self->priv->pending_context);
1583
1584   /* We don't know if the incoming call has been accepted or not, so we
1585    * assume it hasn't and if it has, we'll set the proper status when
1586    * we get the new handler. */
1587   empathy_call_window_status_message (self, _("Disconnected"));
1588   self->priv->call_state = DISCONNECTED;
1589
1590   gtk_widget_destroy (self->priv->incoming_call_dialog);
1591   self->priv->incoming_call_dialog = NULL;
1592 }
1593
1594 void
1595 empathy_call_window_start_ringing (EmpathyCallWindow *self,
1596     TpyCallChannel *channel,
1597     TpChannelDispatchOperation *dispatch_operation,
1598     TpAddDispatchOperationContext *context)
1599 {
1600   g_assert (self->priv->pending_channel == NULL);
1601   g_assert (self->priv->pending_context == NULL);
1602   g_assert (self->priv->pending_cdo == NULL);
1603
1604   /* Start ringing and delay until the user answers or hangs. */
1605   self->priv->pending_channel = g_object_ref (channel);
1606   self->priv->pending_context = g_object_ref (context);
1607   self->priv->pending_cdo = g_object_ref (dispatch_operation);
1608
1609   g_signal_connect (self->priv->pending_cdo, "invalidated",
1610       G_CALLBACK (empathy_call_window_cdo_invalidated_cb), self);
1611
1612   empathy_call_window_set_state_ringing (self);
1613   tp_add_dispatch_operation_context_accept (context);
1614 }
1615
1616 static void
1617 empathy_call_window_init (EmpathyCallWindow *self)
1618 {
1619   EmpathyCallWindowPriv *priv;
1620   GtkBuilder *gui;
1621   GtkWidget *top_vbox;
1622   gchar *filename;
1623   ClutterConstraint *constraint;
1624   ClutterActor *remote_avatar;
1625   GtkStyleContext *context;
1626   GdkRGBA rgba;
1627   ClutterColor bg;
1628
1629   priv = self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
1630     EMPATHY_TYPE_CALL_WINDOW, EmpathyCallWindowPriv);
1631
1632   priv->settings = g_settings_new (EMPATHY_PREFS_CALL_SCHEMA);
1633
1634   filename = empathy_file_lookup ("empathy-call-window.ui", "src");
1635   gui = empathy_builder_get_file (filename,
1636     "call_window_vbox", &top_vbox,
1637     "errors_vbox", &priv->errors_vbox,
1638     "pane", &priv->pane,
1639     "remote_user_name_toolbar", &priv->remote_user_name_toolbar,
1640     "remote_user_avatar_toolbar", &priv->remote_user_avatar_toolbar,
1641     "status_label", &priv->status_label,
1642     "audiocall", &priv->audio_call_button,
1643     "videocall", &priv->video_call_button,
1644     "microphone", &priv->mic_button,
1645     "volume", &priv->volume_button,
1646     "camera", &priv->camera_button,
1647     "hangup", &priv->hangup_button,
1648     "dialpad", &priv->dialpad_button,
1649     "toolbar", &priv->toolbar,
1650     "bottom_toolbar", &priv->bottom_toolbar,
1651     "ui_manager", &priv->ui_manager,
1652     "menufullscreen", &priv->menu_fullscreen,
1653     "menupreviewswap", &priv->menu_swap_camera,
1654     "details_vbox",  &priv->details_vbox,
1655     "vcodec_encoding_label", &priv->vcodec_encoding_label,
1656     "acodec_encoding_label", &priv->acodec_encoding_label,
1657     "acodec_decoding_label", &priv->acodec_decoding_label,
1658     "vcodec_decoding_label", &priv->vcodec_decoding_label,
1659     "audio_remote_candidate_label", &priv->audio_remote_candidate_label,
1660     "audio_local_candidate_label", &priv->audio_local_candidate_label,
1661     "video_remote_candidate_label", &priv->video_remote_candidate_label,
1662     "video_local_candidate_label", &priv->video_local_candidate_label,
1663     "video_remote_candidate_info_img", &priv->video_remote_candidate_info_img,
1664     "video_local_candidate_info_img", &priv->video_local_candidate_info_img,
1665     "audio_remote_candidate_info_img", &priv->audio_remote_candidate_info_img,
1666     "audio_local_candidate_info_img", &priv->audio_local_candidate_info_img,
1667     NULL);
1668   g_free (filename);
1669
1670   empathy_builder_connect (gui, self,
1671     "hangup", "clicked", empathy_call_window_hangup_cb,
1672     "audiocall", "clicked", empathy_call_window_audio_call_cb,
1673     "videocall", "clicked", empathy_call_window_video_call_cb,
1674     "camera", "toggled", empathy_call_window_camera_toggled_cb,
1675     "dialpad", "toggled", empathy_call_window_dialpad_cb,
1676     "menufullscreen", "activate", empathy_call_window_fullscreen_cb,
1677     "menusettings", "activate", empathy_call_window_settings_cb,
1678     "menucontents", "activate", empathy_call_window_contents_cb,
1679     "menudebug", "activate", empathy_call_window_debug_cb,
1680     "menuabout", "activate", empathy_call_window_about_cb,
1681     "menupreviewdisable", "activate", empathy_call_window_disable_camera_cb,
1682     "menupreviewminimise", "activate", empathy_call_window_minimise_camera_cb,
1683     "menupreviewmaximise", "activate", empathy_call_window_maximise_camera_cb,
1684     "menupreviewswap", "activate", empathy_call_window_swap_camera_cb,
1685     NULL);
1686
1687   gtk_action_set_sensitive (priv->menu_fullscreen, FALSE);
1688
1689   priv->camera_monitor = empathy_camera_monitor_dup_singleton ();
1690
1691   g_object_bind_property (priv->camera_monitor, "available",
1692       priv->camera_button, "sensitive",
1693       G_BINDING_SYNC_CREATE);
1694
1695   g_signal_connect (priv->camera_monitor, "added",
1696       G_CALLBACK (empathy_call_window_camera_added_cb), self);
1697   g_signal_connect (priv->camera_monitor, "removed",
1698       G_CALLBACK (empathy_call_window_camera_removed_cb), self);
1699
1700   priv->lock = g_mutex_new ();
1701
1702   gtk_container_add (GTK_CONTAINER (self), top_vbox);
1703
1704   priv->content_hbox = gtk_hbox_new (FALSE, CONTENT_HBOX_SPACING);
1705   gtk_container_set_border_width (GTK_CONTAINER (priv->content_hbox),
1706                                   CONTENT_HBOX_BORDER_WIDTH);
1707   gtk_box_pack_start (GTK_BOX (priv->pane), priv->content_hbox,
1708       TRUE, TRUE, 0);
1709
1710   /* avatar/video box */
1711   priv->video_layout = clutter_bin_layout_new (CLUTTER_BIN_ALIGNMENT_CENTER,
1712       CLUTTER_BIN_ALIGNMENT_CENTER);
1713
1714   priv->video_box = clutter_box_new (priv->video_layout);
1715
1716   priv->video_container = gtk_clutter_embed_new ();
1717
1718   /* Set the background color to that of the rest of the window */
1719   context = gtk_widget_get_style_context (priv->content_hbox);
1720   gtk_style_context_get_background_color (context,
1721       GTK_STATE_FLAG_NORMAL, &rgba);
1722   bg.red = CLAMP (rgba.red * 255.0, 0, 255);
1723   bg.green = CLAMP (rgba.green * 255.0, 0, 255);
1724   bg.blue = CLAMP (rgba.blue * 255.0, 0, 255);
1725   bg.alpha = CLAMP (rgba.alpha * 255.0, 0, 255);
1726   clutter_stage_set_color (
1727       CLUTTER_STAGE (gtk_clutter_embed_get_stage (
1728           GTK_CLUTTER_EMBED (priv->video_container))),
1729       &bg);
1730
1731   clutter_container_add (
1732       CLUTTER_CONTAINER (gtk_clutter_embed_get_stage (
1733           GTK_CLUTTER_EMBED (priv->video_container))),
1734       priv->video_box,
1735       NULL);
1736
1737   constraint = clutter_bind_constraint_new (
1738       gtk_clutter_embed_get_stage (GTK_CLUTTER_EMBED (priv->video_container)),
1739       CLUTTER_BIND_SIZE, 0);
1740   clutter_actor_add_constraint (priv->video_box, constraint);
1741
1742   priv->remote_user_avatar_widget = gtk_image_new ();
1743   remote_avatar = gtk_clutter_actor_new_with_contents (
1744       priv->remote_user_avatar_widget);
1745
1746   clutter_container_add_actor (CLUTTER_CONTAINER (priv->video_box),
1747       remote_avatar);
1748
1749   empathy_call_window_create_preview_rectangles (self);
1750
1751   gtk_box_pack_start (GTK_BOX (priv->content_hbox),
1752       priv->video_container, TRUE, TRUE,
1753       CONTENT_HBOX_CHILDREN_PACKING_PADDING);
1754
1755   create_pipeline (self);
1756   create_video_output_widget (self);
1757   create_audio_input (self);
1758   create_video_input (self);
1759
1760   priv->floating_toolbar = empathy_rounded_actor_new ();
1761
1762   gtk_widget_reparent (priv->bottom_toolbar,
1763       gtk_clutter_actor_get_widget (GTK_CLUTTER_ACTOR (priv->floating_toolbar)));
1764
1765   constraint = clutter_bind_constraint_new (
1766       gtk_clutter_embed_get_stage (GTK_CLUTTER_EMBED (priv->video_container)),
1767       CLUTTER_BIND_Y, 0);
1768
1769   clutter_actor_add_constraint (priv->floating_toolbar, constraint);
1770
1771   g_signal_connect (
1772       gtk_clutter_embed_get_stage (GTK_CLUTTER_EMBED (priv->video_container)),
1773       "notify::allocation",
1774       G_CALLBACK (empathy_call_window_stage_allocation_changed_cb),
1775       constraint);
1776
1777   clutter_actor_set_size (priv->floating_toolbar,
1778       FLOATING_TOOLBAR_WIDTH, FLOATING_TOOLBAR_HEIGHT);
1779   clutter_actor_set_opacity (priv->floating_toolbar, FLOATING_TOOLBAR_OPACITY);
1780
1781   clutter_bin_layout_add (CLUTTER_BIN_LAYOUT (priv->video_layout),
1782       priv->floating_toolbar,
1783       CLUTTER_BIN_ALIGNMENT_CENTER,
1784       CLUTTER_BIN_ALIGNMENT_END);
1785
1786   clutter_actor_raise_top (priv->floating_toolbar);
1787
1788   /* Transitions for the floating toolbar */
1789   priv->transitions = clutter_state_new ();
1790
1791   /* all transitions last for 2s */
1792   clutter_state_set_duration (priv->transitions, NULL, NULL, 2000);
1793
1794   /* transition from any state to "fade-out" state */
1795   clutter_state_set (priv->transitions, NULL, "fade-out",
1796       priv->floating_toolbar,
1797       "opacity", CLUTTER_EASE_OUT_QUAD, 0,
1798       NULL);
1799
1800   /* transition from any state to "fade-in" state */
1801   clutter_state_set (priv->transitions, NULL, "fade-in",
1802       priv->floating_toolbar,
1803       "opacity", CLUTTER_EASE_OUT_QUAD, FLOATING_TOOLBAR_OPACITY,
1804       NULL);
1805
1806   /* put the actor into the "fade-in" state with no animation */
1807   clutter_state_warp_to_state (priv->transitions, "fade-in");
1808
1809   /* The call will be started as soon the pipeline is playing */
1810   priv->start_call_when_playing = TRUE;
1811
1812   priv->dtmf_panel = empathy_dialpad_widget_new ();
1813   g_signal_connect (priv->dtmf_panel, "start-tone",
1814       G_CALLBACK (dtmf_start_tone_cb), self);
1815
1816   priv->tones = g_string_new ("");
1817
1818   gtk_box_pack_start (GTK_BOX (priv->pane), priv->dtmf_panel,
1819       FALSE, FALSE, 6);
1820
1821   gtk_box_pack_start (GTK_BOX (priv->pane), priv->details_vbox,
1822       FALSE, FALSE, 0);
1823
1824   gtk_widget_set_sensitive (priv->dtmf_panel, FALSE);
1825
1826   gtk_widget_show_all (top_vbox);
1827
1828   gtk_widget_hide (priv->dtmf_panel);
1829   gtk_widget_hide (priv->details_vbox);
1830
1831   priv->fullscreen = empathy_call_window_fullscreen_new (self);
1832
1833   empathy_call_window_fullscreen_set_video_widget (priv->fullscreen,
1834       priv->video_container);
1835
1836   /* We hide the bottom toolbar after 3s of inactivity and show it
1837    * again on mouse movement */
1838   priv->inactivity_src = g_timeout_add_seconds (3,
1839       empathy_call_window_toolbar_timeout, self);
1840
1841   g_signal_connect (G_OBJECT (priv->fullscreen->leave_fullscreen_button),
1842       "clicked", G_CALLBACK (empathy_call_window_fullscreen_cb), self);
1843
1844   g_signal_connect (G_OBJECT (self), "realize",
1845     G_CALLBACK (empathy_call_window_realized_cb), self);
1846
1847   g_signal_connect (G_OBJECT (self), "delete-event",
1848     G_CALLBACK (empathy_call_window_delete_cb), self);
1849
1850   g_signal_connect (G_OBJECT (self), "window-state-event",
1851     G_CALLBACK (empathy_call_window_state_event_cb), self);
1852
1853   g_signal_connect (G_OBJECT (self), "key-press-event",
1854       G_CALLBACK (empathy_call_window_key_press_cb), self);
1855
1856   g_signal_connect (self, "motion-notify-event",
1857       G_CALLBACK (empathy_call_window_motion_notify_cb), self);
1858
1859   priv->timer = g_timer_new ();
1860
1861   g_object_ref (priv->ui_manager);
1862   g_object_unref (gui);
1863
1864   priv->sound_mgr = empathy_sound_manager_dup_singleton ();
1865   priv->mic_menu = empathy_mic_menu_new (self);
1866   priv->camera_menu = empathy_camera_menu_new (self);
1867
1868   empathy_call_window_show_hangup_button (self, TRUE);
1869
1870   empathy_geometry_bind (GTK_WINDOW (self), "call-window");
1871   /* These signals are used to track the window position and save it
1872    * when the window is destroyed. We need to do this as we don't want
1873    * the window geometry to be saved with the dialpad taken into account. */
1874   g_signal_connect (self, "destroy",
1875       G_CALLBACK (empathy_call_window_destroyed_cb), self);
1876   g_signal_connect (self, "configure-event",
1877       G_CALLBACK (empathy_call_window_configure_event_cb), self);
1878   g_signal_connect (self, "window-state-event",
1879       G_CALLBACK (empathy_call_window_configure_event_cb), self);
1880
1881   /* Don't display labels in both toolbars */
1882   gtk_toolbar_set_style (GTK_TOOLBAR (priv->toolbar), GTK_TOOLBAR_ICONS);
1883 }
1884
1885 /* Instead of specifying a width and a height, we specify only one size. That's
1886    because we want a square avatar icon.  */
1887 static void
1888 init_contact_avatar_with_size (EmpathyContact *contact,
1889     GtkWidget *image_widget,
1890     gint size)
1891 {
1892   GdkPixbuf *pixbuf_avatar = NULL;
1893
1894   if (contact != NULL)
1895     {
1896       pixbuf_avatar = empathy_pixbuf_avatar_from_contact_scaled (contact,
1897         size, size);
1898     }
1899
1900   if (pixbuf_avatar == NULL)
1901     {
1902       pixbuf_avatar = empathy_pixbuf_from_icon_name_sized (
1903           EMPATHY_IMAGE_AVATAR_DEFAULT, size);
1904     }
1905
1906   gtk_image_set_from_pixbuf (GTK_IMAGE (image_widget), pixbuf_avatar);
1907
1908   if (pixbuf_avatar != NULL)
1909     g_object_unref (pixbuf_avatar);
1910 }
1911
1912 static void
1913 set_window_title (EmpathyCallWindow *self)
1914 {
1915   EmpathyCallWindowPriv *priv = GET_PRIV (self);
1916   gchar *tmp;
1917
1918   if (priv->contact != NULL)
1919     {
1920       /* translators: Call is a noun and %s is the contact name. This string
1921        * is used in the window title */
1922       tmp = g_strdup_printf (_("Call with %s"),
1923           empathy_contact_get_alias (priv->contact));
1924       gtk_window_set_title (GTK_WINDOW (self), tmp);
1925       g_free (tmp);
1926     }
1927   else
1928     {
1929       g_warning ("Unknown remote contact!");
1930     }
1931 }
1932
1933 static void
1934 set_remote_user_name (EmpathyCallWindow *self,
1935   EmpathyContact *contact)
1936 {
1937   const gchar *alias = empathy_contact_get_alias (contact);
1938   const gchar *status = empathy_contact_get_status (contact);
1939   gchar *label;
1940
1941   label = g_strdup_printf ("%s\n<small>%s</small>", alias, status);
1942   gtk_label_set_markup (GTK_LABEL (self->priv->remote_user_name_toolbar),
1943       label);
1944   g_free (label);
1945 }
1946
1947 static void
1948 contact_name_changed_cb (EmpathyContact *contact,
1949     GParamSpec *pspec,
1950     EmpathyCallWindow *self)
1951 {
1952   set_window_title (self);
1953   set_remote_user_name (self, contact);
1954 }
1955
1956 static void
1957 contact_presence_changed_cb (EmpathyContact *contact,
1958     GParamSpec *pspec,
1959     EmpathyCallWindow *self)
1960 {
1961   set_remote_user_name (self, contact);
1962 }
1963
1964 static void
1965 contact_avatar_changed_cb (EmpathyContact *contact,
1966     GParamSpec *pspec,
1967     EmpathyCallWindow *self)
1968 {
1969   int size;
1970   GtkAllocation allocation;
1971   GtkWidget *avatar_widget;
1972
1973   avatar_widget = self->priv->remote_user_avatar_widget;
1974
1975   gtk_widget_get_allocation (avatar_widget, &allocation);
1976   size = allocation.height;
1977
1978   if (size == 0)
1979     {
1980       /* the widget is not allocated yet, set a default size */
1981       size = MIN (REMOTE_CONTACT_AVATAR_DEFAULT_HEIGHT,
1982           REMOTE_CONTACT_AVATAR_DEFAULT_WIDTH);
1983     }
1984
1985   init_contact_avatar_with_size (contact, avatar_widget, size);
1986
1987   avatar_widget = self->priv->remote_user_avatar_toolbar;
1988
1989   gtk_widget_get_allocation (avatar_widget, &allocation);
1990   size = allocation.height;
1991
1992   if (size == 0)
1993     {
1994       /* the widget is not allocated yet, set a default size */
1995       size = SMALL_TOOLBAR_SIZE;
1996     }
1997
1998   init_contact_avatar_with_size (contact, avatar_widget, size);
1999 }
2000
2001 static void
2002 empathy_call_window_setup_avatars (EmpathyCallWindow *self,
2003     EmpathyCallHandler *handler)
2004 {
2005   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2006
2007   tp_g_signal_connect_object (priv->contact, "notify::name",
2008       G_CALLBACK (contact_name_changed_cb), self, 0);
2009   tp_g_signal_connect_object (priv->contact, "notify::avatar",
2010     G_CALLBACK (contact_avatar_changed_cb), self, 0);
2011   tp_g_signal_connect_object (priv->contact, "notify::presence",
2012       G_CALLBACK (contact_presence_changed_cb), self, 0);
2013
2014   set_window_title (self);
2015   set_remote_user_name (self, priv->contact);
2016
2017   init_contact_avatar_with_size (priv->contact,
2018       priv->remote_user_avatar_widget,
2019       MIN (REMOTE_CONTACT_AVATAR_DEFAULT_WIDTH,
2020           REMOTE_CONTACT_AVATAR_DEFAULT_HEIGHT));
2021
2022   init_contact_avatar_with_size (priv->contact,
2023       priv->remote_user_avatar_toolbar,
2024       SMALL_TOOLBAR_SIZE);
2025
2026   /* The remote avatar is shown by default and will be hidden when we receive
2027      video from the remote side. */
2028   clutter_actor_hide (priv->video_output);
2029   gtk_widget_show (priv->remote_user_avatar_widget);
2030 }
2031
2032 static void
2033 update_send_codec (EmpathyCallWindow *self,
2034     gboolean audio)
2035 {
2036   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2037   FsCodec *codec;
2038   GtkWidget *widget;
2039   gchar *tmp;
2040
2041   if (audio)
2042     {
2043       codec = empathy_call_handler_get_send_audio_codec (priv->handler);
2044       widget = priv->acodec_encoding_label;
2045     }
2046   else
2047     {
2048       codec = empathy_call_handler_get_send_video_codec (priv->handler);
2049       widget = priv->vcodec_encoding_label;
2050     }
2051
2052   if (codec == NULL)
2053     return;
2054
2055   tmp = g_strdup_printf ("%s/%u", codec->encoding_name, codec->clock_rate);
2056   gtk_label_set_text (GTK_LABEL (widget), tmp);
2057   g_free (tmp);
2058 }
2059
2060 static void
2061 send_audio_codec_notify_cb (GObject *object,
2062     GParamSpec *pspec,
2063     gpointer user_data)
2064 {
2065   EmpathyCallWindow *self = user_data;
2066
2067   update_send_codec (self, TRUE);
2068 }
2069
2070 static void
2071 send_video_codec_notify_cb (GObject *object,
2072     GParamSpec *pspec,
2073     gpointer user_data)
2074 {
2075   EmpathyCallWindow *self = user_data;
2076
2077   update_send_codec (self, FALSE);
2078 }
2079
2080 static void
2081 update_recv_codec (EmpathyCallWindow *self,
2082     gboolean audio)
2083 {
2084   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2085   GList *codecs, *l;
2086   GtkWidget *widget;
2087   GString *str = NULL;
2088
2089   if (audio)
2090     {
2091       codecs = empathy_call_handler_get_recv_audio_codecs (priv->handler);
2092       widget = priv->acodec_decoding_label;
2093     }
2094   else
2095     {
2096       codecs = empathy_call_handler_get_recv_video_codecs (priv->handler);
2097       widget = priv->vcodec_decoding_label;
2098     }
2099
2100   if (codecs == NULL)
2101     return;
2102
2103   for (l = codecs; l != NULL; l = g_list_next (l))
2104     {
2105       FsCodec *codec = l->data;
2106
2107       if (str == NULL)
2108         str = g_string_new (NULL);
2109       else
2110         g_string_append (str, ", ");
2111
2112       g_string_append_printf (str, "%s/%u", codec->encoding_name,
2113           codec->clock_rate);
2114     }
2115
2116   gtk_label_set_text (GTK_LABEL (widget), str->str);
2117   g_string_free (str, TRUE);
2118 }
2119
2120 static void
2121 recv_audio_codecs_notify_cb (GObject *object,
2122     GParamSpec *pspec,
2123     gpointer user_data)
2124 {
2125   EmpathyCallWindow *self = user_data;
2126
2127   update_recv_codec (self, TRUE);
2128 }
2129
2130 static void
2131 recv_video_codecs_notify_cb (GObject *object,
2132     GParamSpec *pspec,
2133     gpointer user_data)
2134 {
2135   EmpathyCallWindow *self = user_data;
2136
2137   update_recv_codec (self, FALSE);
2138 }
2139
2140 static const gchar *
2141 candidate_type_to_str (FsCandidate *candidate)
2142 {
2143   switch (candidate->type)
2144     {
2145       case FS_CANDIDATE_TYPE_HOST:
2146         return "host";
2147       case FS_CANDIDATE_TYPE_SRFLX:
2148         return "server reflexive";
2149       case FS_CANDIDATE_TYPE_PRFLX:
2150         return "peer reflexive";
2151       case FS_CANDIDATE_TYPE_RELAY:
2152         return "relay";
2153       case FS_CANDIDATE_TYPE_MULTICAST:
2154         return "multicast";
2155     }
2156
2157   return NULL;
2158 }
2159
2160 static const gchar *
2161 candidate_type_to_desc (FsCandidate *candidate)
2162 {
2163   switch (candidate->type)
2164     {
2165       case FS_CANDIDATE_TYPE_HOST:
2166         return _("The IP address as seen by the machine");
2167       case FS_CANDIDATE_TYPE_SRFLX:
2168         return _("The IP address as seen by a server on the Internet");
2169       case FS_CANDIDATE_TYPE_PRFLX:
2170         return _("The IP address of the peer as seen by the other side");
2171       case FS_CANDIDATE_TYPE_RELAY:
2172         return _("The IP address of a relay server");
2173       case FS_CANDIDATE_TYPE_MULTICAST:
2174         return _("The IP address of the multicast group");
2175     }
2176
2177   return NULL;
2178 }
2179
2180 static void
2181 update_candidat_widget (EmpathyCallWindow *self,
2182     GtkWidget *label,
2183     GtkWidget *img,
2184     FsCandidate *candidate)
2185 {
2186   gchar *str;
2187
2188   g_assert (candidate != NULL);
2189   str = g_strdup_printf ("%s %u (%s)", candidate->ip,
2190       candidate->port, candidate_type_to_str (candidate));
2191
2192   gtk_label_set_text (GTK_LABEL (label), str);
2193   gtk_widget_set_tooltip_text (img, candidate_type_to_desc (candidate));
2194
2195   g_free (str);
2196 }
2197
2198 static void
2199 candidates_changed_cb (GObject *object,
2200     FsMediaType type,
2201     EmpathyCallWindow *self)
2202 {
2203   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2204   FsCandidate *candidate = NULL;
2205
2206   if (type == FS_MEDIA_TYPE_VIDEO)
2207     {
2208       /* Update remote candidate */
2209       candidate = empathy_call_handler_get_video_remote_candidate (
2210           priv->handler);
2211
2212       update_candidat_widget (self, priv->video_remote_candidate_label,
2213           priv->video_remote_candidate_info_img, candidate);
2214
2215       /* Update local candidate */
2216       candidate = empathy_call_handler_get_video_local_candidate (
2217           priv->handler);
2218
2219       update_candidat_widget (self, priv->video_local_candidate_label,
2220           priv->video_local_candidate_info_img, candidate);
2221     }
2222   else
2223     {
2224       /* Update remote candidate */
2225       candidate = empathy_call_handler_get_audio_remote_candidate (
2226           priv->handler);
2227
2228       update_candidat_widget (self, priv->audio_remote_candidate_label,
2229           priv->audio_remote_candidate_info_img, candidate);
2230
2231       /* Update local candidate */
2232       candidate = empathy_call_handler_get_audio_local_candidate (
2233           priv->handler);
2234
2235       update_candidat_widget (self, priv->audio_local_candidate_label,
2236           priv->audio_local_candidate_info_img, candidate);
2237     }
2238 }
2239
2240 static void
2241 empathy_call_window_constructed (GObject *object)
2242 {
2243   EmpathyCallWindow *self = EMPATHY_CALL_WINDOW (object);
2244   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2245   TpyCallChannel *call;
2246   TpyCallState state;
2247
2248   g_assert (priv->handler != NULL);
2249
2250   g_object_get (priv->handler, "call-channel", &call, NULL);
2251   state = tpy_call_channel_get_state (call, NULL, NULL);
2252   priv->outgoing = (state == TPY_CALL_STATE_PENDING_INITIATOR);
2253   tp_clear_object (&call);
2254
2255   g_object_get (priv->handler, "target-contact", &priv->contact, NULL);
2256   g_assert (priv->contact != NULL);
2257
2258   if (!empathy_contact_can_voip_video (priv->contact))
2259     {
2260       gtk_widget_set_sensitive (priv->video_call_button, FALSE);
2261       gtk_widget_set_sensitive (priv->camera_button, FALSE);
2262     }
2263
2264   empathy_call_window_setup_avatars (self, priv->handler);
2265   empathy_call_window_set_state_connecting (self);
2266
2267   if (!empathy_call_handler_has_initial_video (priv->handler))
2268     {
2269       gtk_toggle_tool_button_set_active (
2270           GTK_TOGGLE_TOOL_BUTTON (priv->camera_button), FALSE);
2271     }
2272   /* If call has InitialVideo, the preview will be started once the call has
2273    * been started (start_call()). */
2274
2275   update_send_codec (self, TRUE);
2276   update_send_codec (self, FALSE);
2277   update_recv_codec (self, TRUE);
2278   update_recv_codec (self, FALSE);
2279
2280   tp_g_signal_connect_object (priv->handler, "notify::send-audio-codec",
2281       G_CALLBACK (send_audio_codec_notify_cb), self, 0);
2282   tp_g_signal_connect_object (priv->handler, "notify::send-video-codec",
2283       G_CALLBACK (send_video_codec_notify_cb), self, 0);
2284   tp_g_signal_connect_object (priv->handler, "notify::recv-audio-codecs",
2285       G_CALLBACK (recv_audio_codecs_notify_cb), self, 0);
2286   tp_g_signal_connect_object (priv->handler, "notify::recv-video-codecs",
2287       G_CALLBACK (recv_video_codecs_notify_cb), self, 0);
2288
2289   tp_g_signal_connect_object (priv->handler, "candidates-changed",
2290       G_CALLBACK (candidates_changed_cb), self, 0);
2291 }
2292
2293 static void empathy_call_window_dispose (GObject *object);
2294 static void empathy_call_window_finalize (GObject *object);
2295
2296 static void
2297 empathy_call_window_set_property (GObject *object,
2298   guint property_id, const GValue *value, GParamSpec *pspec)
2299 {
2300   EmpathyCallWindowPriv *priv = GET_PRIV (object);
2301
2302   switch (property_id)
2303     {
2304       case PROP_CALL_HANDLER:
2305         priv->handler = g_value_dup_object (value);
2306         break;
2307       default:
2308         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
2309     }
2310 }
2311
2312 static void
2313 empathy_call_window_get_property (GObject *object,
2314   guint property_id, GValue *value, GParamSpec *pspec)
2315 {
2316   EmpathyCallWindowPriv *priv = GET_PRIV (object);
2317
2318   switch (property_id)
2319     {
2320       case PROP_CALL_HANDLER:
2321         g_value_set_object (value, priv->handler);
2322         break;
2323       default:
2324         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
2325     }
2326 }
2327
2328 static void
2329 empathy_call_window_class_init (
2330   EmpathyCallWindowClass *empathy_call_window_class)
2331 {
2332   GObjectClass *object_class = G_OBJECT_CLASS (empathy_call_window_class);
2333   GParamSpec *param_spec;
2334
2335   g_type_class_add_private (empathy_call_window_class,
2336     sizeof (EmpathyCallWindowPriv));
2337
2338   object_class->constructed = empathy_call_window_constructed;
2339   object_class->set_property = empathy_call_window_set_property;
2340   object_class->get_property = empathy_call_window_get_property;
2341
2342   object_class->dispose = empathy_call_window_dispose;
2343   object_class->finalize = empathy_call_window_finalize;
2344
2345   param_spec = g_param_spec_object ("handler",
2346     "handler", "The call handler",
2347     EMPATHY_TYPE_CALL_HANDLER,
2348     G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
2349   g_object_class_install_property (object_class,
2350     PROP_CALL_HANDLER, param_spec);
2351 }
2352
2353 void
2354 empathy_call_window_dispose (GObject *object)
2355 {
2356   EmpathyCallWindow *self = EMPATHY_CALL_WINDOW (object);
2357   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2358
2359   if (priv->dispose_has_run)
2360     return;
2361
2362   priv->dispose_has_run = TRUE;
2363
2364   if (priv->handler != NULL)
2365     {
2366       empathy_call_handler_stop_call (priv->handler);
2367       tp_clear_object (&priv->handler);
2368     }
2369
2370   if (priv->bus_message_source_id != 0)
2371     {
2372       g_source_remove (priv->bus_message_source_id);
2373       priv->bus_message_source_id = 0;
2374     }
2375
2376   if (priv->got_video_src > 0)
2377     {
2378       g_source_remove (priv->got_video_src);
2379       priv->got_video_src = 0;
2380     }
2381
2382   if (priv->inactivity_src > 0)
2383     {
2384       g_source_remove (priv->inactivity_src);
2385       priv->inactivity_src = 0;
2386     }
2387
2388   tp_clear_object (&priv->pipeline);
2389   tp_clear_object (&priv->video_input);
2390   tp_clear_object (&priv->audio_input);
2391   tp_clear_object (&priv->video_tee);
2392   tp_clear_object (&priv->ui_manager);
2393   tp_clear_object (&priv->fullscreen);
2394   tp_clear_object (&priv->camera_monitor);
2395   tp_clear_object (&priv->settings);
2396   tp_clear_object (&priv->sound_mgr);
2397   tp_clear_object (&priv->mic_menu);
2398   tp_clear_object (&priv->camera_menu);
2399
2400   g_list_free_full (priv->notifiers, g_object_unref);
2401
2402   if (priv->timer_id != 0)
2403     g_source_remove (priv->timer_id);
2404   priv->timer_id = 0;
2405
2406   if (priv->contact != NULL)
2407     {
2408       g_signal_handlers_disconnect_by_func (priv->contact,
2409           contact_name_changed_cb, self);
2410       priv->contact = NULL;
2411     }
2412
2413   G_OBJECT_CLASS (empathy_call_window_parent_class)->dispose (object);
2414 }
2415
2416 static void
2417 disconnect_video_output_motion_handler (EmpathyCallWindow *self)
2418 {
2419   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2420
2421   if (priv->video_output_motion_handler_id != 0)
2422     {
2423       g_signal_handler_disconnect (G_OBJECT (priv->video_container),
2424           priv->video_output_motion_handler_id);
2425       priv->video_output_motion_handler_id = 0;
2426     }
2427 }
2428
2429 void
2430 empathy_call_window_finalize (GObject *object)
2431 {
2432   EmpathyCallWindow *self = EMPATHY_CALL_WINDOW (object);
2433   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2434
2435   disconnect_video_output_motion_handler (self);
2436
2437   /* free any data held directly by the object here */
2438   g_mutex_free (priv->lock);
2439
2440   g_timer_destroy (priv->timer);
2441
2442   g_string_free (priv->tones, TRUE);
2443
2444   G_OBJECT_CLASS (empathy_call_window_parent_class)->finalize (object);
2445 }
2446
2447
2448 EmpathyCallWindow *
2449 empathy_call_window_new (EmpathyCallHandler *handler)
2450 {
2451   return EMPATHY_CALL_WINDOW (
2452     g_object_new (EMPATHY_TYPE_CALL_WINDOW, "handler", handler, NULL));
2453 }
2454
2455 void
2456 empathy_call_window_present (EmpathyCallWindow *self,
2457     EmpathyCallHandler *handler)
2458 {
2459   g_return_if_fail (EMPATHY_IS_CALL_HANDLER (handler));
2460
2461   tp_clear_object (&self->priv->handler);
2462   self->priv->handler = g_object_ref (handler);
2463   empathy_call_window_connect_handler (self);
2464
2465   empathy_window_present (GTK_WINDOW (self));
2466   empathy_call_window_restart_call (self);
2467 }
2468
2469 static void
2470 empathy_call_window_conference_added_cb (EmpathyCallHandler *handler,
2471   GstElement *conference, gpointer user_data)
2472 {
2473   EmpathyCallWindow *self = EMPATHY_CALL_WINDOW (user_data);
2474   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2475   FsElementAddedNotifier *notifier;
2476   GKeyFile *keyfile;
2477
2478   DEBUG ("Conference added");
2479
2480   /* Add notifier to set the various element properties as needed */
2481   notifier = fs_element_added_notifier_new ();
2482   keyfile = fs_utils_get_default_element_properties (conference);
2483
2484   if (keyfile != NULL)
2485     fs_element_added_notifier_set_properties_from_keyfile (notifier, keyfile);
2486
2487   fs_element_added_notifier_add (notifier, GST_BIN (priv->pipeline));
2488
2489   priv->notifiers = g_list_prepend (priv->notifiers, notifier);
2490
2491   gst_bin_add (GST_BIN (priv->pipeline), conference);
2492   gst_element_set_state (conference, GST_STATE_PLAYING);
2493 }
2494
2495 static void
2496 empathy_call_window_conference_removed_cb (EmpathyCallHandler *handler,
2497   GstElement *conference, gpointer user_data)
2498 {
2499   EmpathyCallWindow *self = EMPATHY_CALL_WINDOW (user_data);
2500   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2501
2502   gst_bin_remove (GST_BIN (priv->pipeline), conference);
2503   gst_element_set_state (conference, GST_STATE_NULL);
2504 }
2505
2506 static gboolean
2507 empathy_call_window_reset_pipeline (EmpathyCallWindow *self)
2508 {
2509   GstStateChangeReturn state_change_return;
2510   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2511
2512   if (priv->pipeline == NULL)
2513     return TRUE;
2514
2515   if (priv->bus_message_source_id != 0)
2516     {
2517       g_source_remove (priv->bus_message_source_id);
2518       priv->bus_message_source_id = 0;
2519     }
2520
2521   state_change_return = gst_element_set_state (priv->pipeline, GST_STATE_NULL);
2522
2523   if (state_change_return == GST_STATE_CHANGE_SUCCESS ||
2524         state_change_return == GST_STATE_CHANGE_NO_PREROLL)
2525     {
2526       if (priv->pipeline != NULL)
2527         g_object_unref (priv->pipeline);
2528       priv->pipeline = NULL;
2529
2530       if (priv->audio_output != NULL)
2531         g_object_unref (priv->audio_output);
2532       priv->audio_output = NULL;
2533
2534       if (priv->video_tee != NULL)
2535         g_object_unref (priv->video_tee);
2536       priv->video_tee = NULL;
2537
2538       if (priv->video_preview != NULL)
2539         clutter_actor_destroy (priv->video_preview);
2540       priv->video_preview = NULL;
2541
2542       /* If we destroy the preview while it's being dragged, we won't
2543        * get the ::drag-end signal, so manually destroy the clone */
2544       if (priv->drag_preview != NULL)
2545         {
2546           clutter_actor_destroy (priv->drag_preview);
2547           empathy_call_window_show_preview_rectangles (self, FALSE);
2548           priv->drag_preview = NULL;
2549         }
2550
2551       priv->funnel = NULL;
2552
2553       create_pipeline (self);
2554       /* Call will be started when user will hit the 'redial' button */
2555       priv->start_call_when_playing = FALSE;
2556       gst_element_set_state (priv->pipeline, GST_STATE_PAUSED);
2557
2558       return TRUE;
2559     }
2560   else
2561     {
2562       g_message ("Error: could not destroy pipeline. Closing call window");
2563       gtk_widget_destroy (GTK_WIDGET (self));
2564
2565       return FALSE;
2566     }
2567 }
2568
2569 static void
2570 reset_details_pane (EmpathyCallWindow *self)
2571 {
2572   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2573
2574   gtk_label_set_text (GTK_LABEL (priv->vcodec_encoding_label), _("Unknown"));
2575   gtk_label_set_text (GTK_LABEL (priv->acodec_encoding_label), _("Unknown"));
2576   gtk_label_set_text (GTK_LABEL (priv->vcodec_decoding_label), _("Unknown"));
2577   gtk_label_set_text (GTK_LABEL (priv->acodec_decoding_label), _("Unknown"));
2578 }
2579
2580 static gboolean
2581 empathy_call_window_disconnected (EmpathyCallWindow *self,
2582     gboolean restart)
2583 {
2584   gboolean could_disconnect = FALSE;
2585   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2586   gboolean could_reset_pipeline;
2587
2588   /* Leave full screen mode if needed */
2589   gtk_window_unfullscreen (GTK_WINDOW (self));
2590
2591   gtk_action_set_sensitive (priv->menu_fullscreen, FALSE);
2592   gtk_widget_set_sensitive (priv->dtmf_panel, FALSE);
2593
2594   priv->sending_tones = FALSE;
2595   g_string_set_size (priv->tones, 0);
2596
2597   could_reset_pipeline = empathy_call_window_reset_pipeline (self);
2598
2599   if (priv->call_state == CONNECTING)
2600       empathy_sound_manager_stop (priv->sound_mgr, EMPATHY_SOUND_PHONE_OUTGOING);
2601
2602   if (priv->call_state != REDIALING)
2603     priv->call_state = DISCONNECTED;
2604
2605   /* Show the toolbar */
2606   clutter_state_set_state (priv->transitions, "fade-in");
2607
2608   if (could_reset_pipeline)
2609     {
2610       g_mutex_lock (priv->lock);
2611
2612       g_timer_stop (priv->timer);
2613
2614       if (priv->timer_id != 0)
2615         g_source_remove (priv->timer_id);
2616       priv->timer_id = 0;
2617
2618       g_mutex_unlock (priv->lock);
2619
2620       if (!restart)
2621         /* We are about to destroy the window, no need to update it or create
2622          * a video preview */
2623         return TRUE;
2624
2625       empathy_call_window_status_message (self, _("Disconnected"));
2626
2627       empathy_call_window_show_hangup_button (self, FALSE);
2628
2629       /* Unsensitive the camera and mic button */
2630       gtk_widget_set_sensitive (priv->camera_button, FALSE);
2631       gtk_widget_set_sensitive (priv->mic_button, FALSE);
2632
2633       /* Be sure that the mic button is enabled */
2634       gtk_toggle_tool_button_set_active (
2635           GTK_TOGGLE_TOOL_BUTTON (priv->mic_button), TRUE);
2636
2637       if (priv->camera_state == CAMERA_STATE_ON)
2638         {
2639           /* Restart the preview with the new pipeline. */
2640           display_video_preview (self, TRUE);
2641         }
2642
2643       /* destroy the video output; it will be recreated when we'll redial */
2644       disconnect_video_output_motion_handler (self);
2645       if (priv->video_output != NULL)
2646         clutter_actor_destroy (priv->video_output);
2647       priv->video_output = NULL;
2648       if (priv->got_video_src > 0)
2649         {
2650           g_source_remove (priv->got_video_src);
2651           priv->got_video_src = 0;
2652         }
2653
2654       gtk_widget_show (priv->remote_user_avatar_widget);
2655
2656       reset_details_pane (self);
2657
2658       priv->sending_video = FALSE;
2659       priv->call_started = FALSE;
2660
2661       could_disconnect = TRUE;
2662
2663       /* TODO: display the self avatar of the preview (depends if the "Always
2664        * Show Video Preview" is enabled or not) */
2665     }
2666
2667   return could_disconnect;
2668 }
2669
2670
2671 static void
2672 empathy_call_window_channel_closed_cb (EmpathyCallHandler *handler,
2673     gpointer user_data)
2674 {
2675   EmpathyCallWindow *self = EMPATHY_CALL_WINDOW (user_data);
2676   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2677
2678   if (empathy_call_window_disconnected (self, TRUE) &&
2679       priv->call_state == REDIALING)
2680       empathy_call_window_restart_call (self);
2681 }
2682
2683 static gboolean
2684 empathy_call_window_content_is_raw (TfContent *content)
2685 {
2686   FsConference *conference;
2687   gboolean israw;
2688
2689   g_object_get (content, "fs-conference", &conference, NULL);
2690   g_assert (conference != NULL);
2691
2692   /* FIXME: Ugly hack, update when moving a packetization property into
2693    * farstream */
2694   israw = g_str_has_prefix (GST_OBJECT_NAME (conference), "fsrawconf");
2695   gst_object_unref (conference);
2696
2697   return israw;
2698 }
2699
2700 static gboolean
2701 empathy_call_window_content_removed_cb (EmpathyCallHandler *handler,
2702     TfContent *content,
2703     EmpathyCallWindow *self)
2704 {
2705   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2706   FsMediaType media_type;
2707
2708   DEBUG ("removing content");
2709
2710   g_object_get (content, "media-type", &media_type, NULL);
2711
2712   /*
2713    * This assumes that there is only one video stream per channel...
2714    */
2715
2716   if ((guint) media_type == FS_MEDIA_TYPE_VIDEO)
2717     {
2718       if (priv->funnel != NULL)
2719         {
2720           GstElement *output;
2721
2722           output = priv->video_output_sink;
2723
2724           gst_element_set_state (output, GST_STATE_NULL);
2725           gst_element_set_state (priv->funnel, GST_STATE_NULL);
2726
2727           gst_bin_remove (GST_BIN (priv->pipeline), output);
2728           gst_bin_remove (GST_BIN (priv->pipeline), priv->funnel);
2729           priv->funnel = NULL;
2730         }
2731     }
2732   else if (media_type == FS_MEDIA_TYPE_AUDIO)
2733     {
2734       if (priv->audio_output != NULL)
2735         {
2736           gst_element_set_state (priv->audio_output, GST_STATE_NULL);
2737
2738           gst_bin_remove (GST_BIN (priv->pipeline), priv->audio_output);
2739           priv->audio_output = NULL;
2740         }
2741     }
2742   else
2743     {
2744       g_assert_not_reached ();
2745     }
2746
2747   return TRUE;
2748 }
2749
2750 static void
2751 empathy_call_window_framerate_changed_cb (EmpathyCallHandler *handler,
2752     guint framerate,
2753     EmpathyCallWindow *self)
2754 {
2755   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2756
2757   DEBUG ("Framerate changed to %u", framerate);
2758
2759   if (priv->video_input != NULL)
2760     empathy_video_src_set_framerate (priv->video_input, framerate);
2761 }
2762
2763 static void
2764 empathy_call_window_resolution_changed_cb (EmpathyCallHandler *handler,
2765     guint width,
2766     guint height,
2767     EmpathyCallWindow *self)
2768 {
2769   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2770
2771   DEBUG ("Resolution changed to %ux%u", width, height);
2772
2773   if (priv->video_input != NULL)
2774     {
2775       empathy_video_src_set_resolution (priv->video_input, width, height);
2776     }
2777 }
2778
2779 /* Called with global lock held */
2780 static GstPad *
2781 empathy_call_window_get_video_sink_pad (EmpathyCallWindow *self)
2782 {
2783   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2784   GstPad *pad;
2785   GstElement *output;
2786
2787   if (priv->funnel == NULL)
2788     {
2789       output = priv->video_output_sink;
2790
2791       priv->funnel = gst_element_factory_make ("fsfunnel", NULL);
2792
2793       if (!priv->funnel)
2794         {
2795           g_warning ("Could not create fsfunnel");
2796           return NULL;
2797         }
2798
2799       if (!gst_bin_add (GST_BIN (priv->pipeline), priv->funnel))
2800         {
2801           gst_object_unref (priv->funnel);
2802           priv->funnel = NULL;
2803           g_warning ("Could  not add funnel to pipeline");
2804           return NULL;
2805         }
2806
2807       if (!gst_bin_add (GST_BIN (priv->pipeline), output))
2808         {
2809           g_warning ("Could not add the video output widget to the pipeline");
2810           goto error;
2811         }
2812
2813       if (!gst_element_link (priv->funnel, output))
2814         {
2815           g_warning ("Could not link output sink to funnel");
2816           goto error_output_added;
2817         }
2818
2819       if (gst_element_set_state (output, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE)
2820         {
2821           g_warning ("Could not start video sink");
2822           goto error_output_added;
2823         }
2824
2825       if (gst_element_set_state (priv->funnel, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE)
2826         {
2827           g_warning ("Could not start funnel");
2828           goto error_output_added;
2829         }
2830     }
2831
2832   pad = gst_element_get_request_pad (priv->funnel, "sink%d");
2833
2834   if (!pad)
2835     g_warning ("Could not get request pad from funnel");
2836
2837   return pad;
2838
2839
2840  error_output_added:
2841
2842   gst_element_set_locked_state (priv->funnel, TRUE);
2843   gst_element_set_locked_state (output, TRUE);
2844
2845   gst_element_set_state (priv->funnel, GST_STATE_NULL);
2846   gst_element_set_state (output, GST_STATE_NULL);
2847
2848   gst_bin_remove (GST_BIN (priv->pipeline), output);
2849   gst_element_set_locked_state (output, FALSE);
2850
2851  error:
2852
2853   gst_bin_remove (GST_BIN (priv->pipeline), priv->funnel);
2854   priv->funnel = NULL;
2855
2856   return NULL;
2857 }
2858
2859 /* Called with global lock held */
2860 static GstPad *
2861 empathy_call_window_get_audio_sink_pad (EmpathyCallWindow *self,
2862   TfContent *content)
2863 {
2864   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2865   GstPad *pad;
2866   GstPadTemplate *template;
2867
2868   if (priv->audio_output == NULL)
2869     {
2870       priv->audio_output = empathy_audio_sink_new ();
2871       g_object_ref_sink (priv->audio_output);
2872
2873       /* volume button to output volume linking */
2874       g_object_bind_property (priv->audio_output, "volume",
2875         priv->volume_button, "value",
2876         G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE);
2877
2878       g_object_bind_property_full (content, "requested-output-volume",
2879         priv->audio_output, "volume",
2880         G_BINDING_DEFAULT,
2881         audio_control_volume_to_element,
2882         element_volume_to_audio_control,
2883         NULL, NULL);
2884
2885       /* Link volumes together, sync the current audio input volume property
2886         * back to farstream first */
2887       g_object_bind_property_full (priv->audio_output, "volume",
2888         content, "reported-output-volume",
2889         G_BINDING_SYNC_CREATE,
2890         element_volume_to_audio_control,
2891         audio_control_volume_to_element,
2892         NULL, NULL);
2893
2894       if (!gst_bin_add (GST_BIN (priv->pipeline), priv->audio_output))
2895         {
2896           g_warning ("Could not add audio sink to pipeline");
2897           g_object_unref (priv->audio_output);
2898           goto error_add_output;
2899         }
2900
2901       if (gst_element_set_state (priv->audio_output, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE)
2902         {
2903           g_warning ("Could not start audio sink");
2904           goto error;
2905         }
2906     }
2907
2908   /* For raw audio conferences assume that the producer of the raw data
2909    * has already processed it, so turn off any echo cancellation and any
2910    * other audio improvements that come with it */
2911   empathy_audio_sink_set_echo_cancel (
2912     EMPATHY_GST_AUDIO_SINK (priv->audio_output),
2913     !empathy_call_window_content_is_raw (content));
2914
2915   template = gst_element_class_get_pad_template (
2916     GST_ELEMENT_GET_CLASS (priv->audio_output), "sink%d");
2917
2918   pad = gst_element_request_pad (priv->audio_output,
2919     template, NULL, NULL);
2920
2921   if (pad == NULL)
2922     {
2923       g_warning ("Could not get sink pad from sink");
2924       return NULL;
2925     }
2926
2927   return pad;
2928
2929 error:
2930   gst_element_set_locked_state (priv->audio_output, TRUE);
2931   gst_element_set_state (priv->audio_output, GST_STATE_NULL);
2932   gst_bin_remove (GST_BIN (priv->pipeline), priv->audio_output);
2933   priv->audio_output = NULL;
2934
2935 error_add_output:
2936
2937   return NULL;
2938 }
2939
2940 static gboolean
2941 empathy_call_window_update_timer (gpointer user_data)
2942 {
2943   EmpathyCallWindow *self = EMPATHY_CALL_WINDOW (user_data);
2944   EmpathyCallWindowPriv *priv = GET_PRIV (self);
2945   const gchar *status;
2946   gchar *str;
2947   gdouble time_;
2948
2949   time_ = g_timer_elapsed (priv->timer, NULL);
2950
2951   if (priv->call_state == HELD)
2952     status = _("On hold");
2953   else if (!gtk_toggle_tool_button_get_active (
2954       GTK_TOGGLE_TOOL_BUTTON (priv->mic_button)))
2955     status = _("Mute");
2956   else
2957     status = _("Duration");
2958
2959   /* Translators: 'status - minutes:seconds' the caller has been connected */
2960   str = g_strdup_printf (_("%s â€” %d:%02dm"),
2961       status,
2962       (int) time_ / 60, (int) time_ % 60);
2963   empathy_call_window_status_message (self, str);
2964   g_free (str);
2965
2966   return TRUE;
2967 }
2968
2969 enum
2970 {
2971   EMP_RESPONSE_BALANCE
2972 };
2973
2974 static void
2975 on_error_infobar_response_cb (GtkInfoBar *info_bar,
2976     gint response_id,
2977     gpointer user_data)
2978 {
2979   switch (response_id)
2980     {
2981       case GTK_RESPONSE_CLOSE:
2982         gtk_widget_destroy (GTK_WIDGET (info_bar));
2983         break;
2984       case EMP_RESPONSE_BALANCE:
2985         empathy_url_show (GTK_WIDGET (info_bar),
2986             g_object_get_data (G_OBJECT (info_bar), "uri"));
2987         break;
2988     }
2989 }
2990
2991 static void
2992 display_error (EmpathyCallWindow *self,
2993     const gchar *img,
2994     const gchar *title,
2995     const gchar *desc,
2996     const gchar *details,
2997     const gchar *button_text,
2998     const gchar *uri,
2999     gint button_response)
3000 {
3001   EmpathyCallWindowPriv *priv = GET_PRIV (self);
3002   GtkWidget *info_bar;
3003   GtkWidget *content_area;
3004   GtkWidget *hbox;
3005   GtkWidget *vbox;
3006   GtkWidget *image;
3007   GtkWidget *label;
3008   gchar *txt;
3009
3010   /* Create info bar */
3011   info_bar = gtk_info_bar_new ();
3012
3013   if (button_text != NULL)
3014     {
3015       gtk_info_bar_add_button (GTK_INFO_BAR (info_bar),
3016           button_text, button_response);
3017       g_object_set_data_full (G_OBJECT (info_bar),
3018           "uri", g_strdup (uri), g_free);
3019     }
3020
3021   gtk_info_bar_add_button (GTK_INFO_BAR (info_bar),
3022       GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE);
3023
3024   gtk_info_bar_set_message_type (GTK_INFO_BAR (info_bar), GTK_MESSAGE_WARNING);
3025
3026   content_area = gtk_info_bar_get_content_area (GTK_INFO_BAR (info_bar));
3027
3028   /* hbox containing the image and the messages vbox */
3029   hbox = gtk_hbox_new (FALSE, 3);
3030   gtk_container_add (GTK_CONTAINER (content_area), hbox);
3031
3032   /* Add image */
3033   image = gtk_image_new_from_icon_name (img, GTK_ICON_SIZE_DIALOG);
3034   gtk_box_pack_start (GTK_BOX (hbox), image, FALSE, FALSE, 0);
3035
3036   /* vbox containing the main message and the details expander */
3037   vbox = gtk_vbox_new (FALSE, 3);
3038   gtk_box_pack_start (GTK_BOX (hbox), vbox, TRUE, TRUE, 0);
3039
3040   /* Add text */
3041   txt = g_strdup_printf ("<b>%s</b>\n%s", title, desc);
3042
3043   label = gtk_label_new (NULL);
3044   gtk_label_set_markup (GTK_LABEL (label), txt);
3045   gtk_label_set_line_wrap (GTK_LABEL (label), TRUE);
3046   gtk_misc_set_alignment (GTK_MISC (label), 0, 0);
3047   g_free (txt);
3048
3049   gtk_box_pack_start (GTK_BOX (vbox), label, TRUE, TRUE, 0);
3050
3051   /* Add details */
3052   if (details != NULL)
3053     {
3054       GtkWidget *expander;
3055
3056       expander = gtk_expander_new (_("Technical Details"));
3057
3058       txt = g_strdup_printf ("<i>%s</i>", details);
3059
3060       label = gtk_label_new (NULL);
3061       gtk_label_set_markup (GTK_LABEL (label), txt);
3062       gtk_label_set_line_wrap (GTK_LABEL (label), TRUE);
3063       gtk_misc_set_alignment (GTK_MISC (label), 0, 0);
3064       g_free (txt);
3065
3066       gtk_container_add (GTK_CONTAINER (expander), label);
3067       gtk_box_pack_start (GTK_BOX (vbox), expander, TRUE, TRUE, 0);
3068     }
3069
3070   g_signal_connect (info_bar, "response",
3071       G_CALLBACK (on_error_infobar_response_cb), NULL);
3072
3073   gtk_box_pack_start (GTK_BOX (priv->errors_vbox), info_bar,
3074       FALSE, FALSE, CONTENT_HBOX_CHILDREN_PACKING_PADDING);
3075   gtk_widget_show_all (info_bar);
3076 }
3077
3078 #if 0
3079 static gchar *
3080 media_stream_error_to_txt (EmpathyCallWindow *self,
3081     TpyCallChannel *call,
3082     gboolean audio,
3083     TpMediaStreamError error)
3084 {
3085   EmpathyCallWindowPriv *priv = GET_PRIV (self);
3086   const gchar *cm = NULL;
3087   gchar *url;
3088   gchar *result;
3089
3090   switch (error)
3091     {
3092       case TP_MEDIA_STREAM_ERROR_CODEC_NEGOTIATION_FAILED:
3093         if (audio)
3094           return g_strdup_printf (
3095               _("%s's software does not understand any of the audio formats "
3096                 "supported by your computer"),
3097             empathy_contact_get_alias (priv->contact));
3098         else
3099           return g_strdup_printf (
3100               _("%s's software does not understand any of the video formats "
3101                 "supported by your computer"),
3102             empathy_contact_get_alias (priv->contact));
3103
3104       case TP_MEDIA_STREAM_ERROR_CONNECTION_FAILED:
3105         return g_strdup_printf (
3106             _("Can't establish a connection to %s. "
3107               "One of you might be on a network that does not allow "
3108               "direct connections."),
3109           empathy_contact_get_alias (priv->contact));
3110
3111       case TP_MEDIA_STREAM_ERROR_NETWORK_ERROR:
3112           return g_strdup (_("There was a failure on the network"));
3113
3114       case TP_MEDIA_STREAM_ERROR_NO_CODECS:
3115         if (audio)
3116           return g_strdup (_("The audio formats necessary for this call "
3117                 "are not installed on your computer"));
3118         else
3119           return g_strdup (_("The video formats necessary for this call "
3120                 "are not installed on your computer"));
3121
3122       case TP_MEDIA_STREAM_ERROR_INVALID_CM_BEHAVIOR:
3123         tp_connection_parse_object_path (
3124             tp_channel_borrow_connection (TP_CHANNEL (call)),
3125             NULL, &cm);
3126
3127         url = g_strdup_printf ("http://bugs.freedesktop.org/enter_bug.cgi?"
3128             "product=Telepathy&amp;component=%s", cm);
3129
3130         result = g_strdup_printf (
3131             _("Something unexpected happened in a Telepathy component. "
3132               "Please <a href=\"%s\">report this bug</a> and attach "
3133               "logs gathered from the 'Debug' window in the Help menu."), url);
3134
3135         g_free (url);
3136         g_free (cm);
3137         return result;
3138
3139       case TP_MEDIA_STREAM_ERROR_MEDIA_ERROR:
3140         return g_strdup (_("There was a failure in the call engine"));
3141
3142       case TP_MEDIA_STREAM_ERROR_EOS:
3143         return g_strdup (_("The end of the stream was reached"));
3144
3145       case TP_MEDIA_STREAM_ERROR_UNKNOWN:
3146       default:
3147         return NULL;
3148     }
3149 }
3150
3151 static void
3152 empathy_call_window_stream_error (EmpathyCallWindow *self,
3153     TpyCallChannel *call,
3154     gboolean audio,
3155     guint code,
3156     const gchar *msg,
3157     const gchar *icon,
3158     const gchar *title)
3159 {
3160   gchar *desc;
3161
3162   desc = media_stream_error_to_txt (self, call, audio, code);
3163   if (desc == NULL)
3164     {
3165       /* No description, use the error message. That's not great as it's not
3166        * localized but it's better than nothing. */
3167       display_error (self, call, icon, title, msg, NULL);
3168     }
3169   else
3170     {
3171       display_error (self, call, icon, title, desc, msg);
3172       g_free (desc);
3173     }
3174 }
3175
3176 static void
3177 empathy_call_window_audio_stream_error (TpyCallChannel *call,
3178     guint code,
3179     const gchar *msg,
3180     EmpathyCallWindow *self)
3181 {
3182   empathy_call_window_stream_error (self, call, TRUE, code, msg,
3183       "gnome-stock-mic", _("Can't establish audio stream"));
3184 }
3185
3186 static void
3187 empathy_call_window_video_stream_error (TpyCallChannel *call,
3188     guint code,
3189     const gchar *msg,
3190     EmpathyCallWindow *self)
3191 {
3192   empathy_call_window_stream_error (self, call, FALSE, code, msg,
3193       "camera-web", _("Can't establish video stream"));
3194 }
3195 #endif
3196
3197 static void
3198 show_balance_error (EmpathyCallWindow *self)
3199 {
3200   TpChannel *call;
3201   TpConnection *conn;
3202   gchar *balance, *tmp;
3203   const gchar *uri, *currency;
3204   gint amount;
3205   guint scale;
3206
3207   g_object_get (self->priv->handler,
3208       "call-channel", &call,
3209       NULL);
3210
3211   conn = tp_channel_borrow_connection (call);
3212   g_object_unref (call);
3213
3214   uri = tp_connection_get_balance_uri (conn);
3215
3216   if (!tp_connection_get_balance (conn, &amount, &scale, &currency))
3217     {
3218       /* unknown balance */
3219       balance = g_strdup ("(--)");
3220     }
3221   else
3222     {
3223       char *money = empathy_format_currency (amount, scale, currency);
3224
3225       balance = g_strdup_printf ("%s %s",
3226           currency, money);
3227       g_free (money);
3228     }
3229
3230   tmp = g_strdup_printf (_("Your current balance is %s."), balance),
3231
3232   display_error (self,
3233       NULL,
3234       _("Sorry, you don’t have enough credit for that call."),
3235       tmp, NULL,
3236       _("Top Up"),
3237       uri,
3238       EMP_RESPONSE_BALANCE);
3239
3240   g_free (tmp);
3241   g_free (balance);
3242 }
3243
3244 static void
3245 empathy_call_window_state_changed_cb (EmpathyCallHandler *handler,
3246     TpyCallState state,
3247     gchar *reason,
3248     EmpathyCallWindow *self)
3249 {
3250   EmpathyCallWindowPriv *priv = GET_PRIV (self);
3251   TpyCallChannel *call;
3252   gboolean can_send_video;
3253
3254   if (state == TPY_CALL_STATE_ENDED &&
3255       !tp_strdiff (reason, TP_ERROR_STR_INSUFFICIENT_BALANCE))
3256     {
3257       show_balance_error (self);
3258       return;
3259     }
3260
3261   if (state != TPY_CALL_STATE_ACCEPTED)
3262     return;
3263
3264   if (priv->call_state == CONNECTED)
3265     return;
3266
3267   g_timer_start (priv->timer);
3268   priv->call_state = CONNECTED;
3269
3270   empathy_sound_manager_stop (priv->sound_mgr, EMPATHY_SOUND_PHONE_OUTGOING);
3271
3272   can_send_video = priv->video_input != NULL &&
3273     empathy_contact_can_voip_video (priv->contact) &&
3274     empathy_camera_monitor_get_available (priv->camera_monitor);
3275
3276   g_object_get (priv->handler, "call-channel", &call, NULL);
3277
3278   if (tpy_call_channel_has_dtmf (call))
3279     gtk_widget_set_sensitive (priv->dtmf_panel, TRUE);
3280
3281   if (priv->video_input == NULL)
3282     empathy_call_window_set_send_video (self, CAMERA_STATE_OFF);
3283
3284   gtk_widget_set_sensitive (priv->camera_button, can_send_video);
3285
3286   empathy_call_window_show_hangup_button (self, TRUE);
3287
3288   gtk_widget_set_sensitive (priv->mic_button, TRUE);
3289
3290   clutter_actor_hide (priv->video_output);
3291   gtk_widget_show (priv->remote_user_avatar_widget);
3292
3293   g_object_unref (call);
3294
3295   g_mutex_lock (priv->lock);
3296
3297   priv->timer_id = g_timeout_add_seconds (1,
3298     empathy_call_window_update_timer, self);
3299
3300   g_mutex_unlock (priv->lock);
3301
3302   empathy_call_window_update_timer (self);
3303
3304   gtk_action_set_sensitive (priv->menu_fullscreen, TRUE);
3305 }
3306
3307 static gboolean
3308 empathy_call_window_show_video_output_cb (gpointer user_data)
3309 {
3310   EmpathyCallWindow *self = EMPATHY_CALL_WINDOW (user_data);
3311
3312   if (self->priv->video_output != NULL)
3313     {
3314       gtk_widget_hide (self->priv->remote_user_avatar_widget);
3315       clutter_actor_show (self->priv->video_output);
3316       empathy_call_window_raise_actors (self);
3317     }
3318
3319   return FALSE;
3320 }
3321
3322 static gboolean
3323 empathy_call_window_check_video_cb (gpointer data)
3324 {
3325   EmpathyCallWindow *self = data;
3326
3327   if (self->priv->got_video)
3328     {
3329       self->priv->got_video = FALSE;
3330       return TRUE;
3331     }
3332
3333   /* No video in the last N seconds, display the remote avatar */
3334   empathy_call_window_show_video_output (self, FALSE);
3335
3336   return TRUE;
3337 }
3338
3339 /* Called from the streaming thread */
3340 static gboolean
3341 empathy_call_window_video_probe_cb (GstPad *pad,
3342     GstMiniObject *mini_obj,
3343     EmpathyCallWindow *self)
3344 {
3345   /* Ignore events */
3346   if (GST_IS_EVENT (mini_obj))
3347     return TRUE;
3348
3349   if (G_UNLIKELY (!self->priv->got_video))
3350     {
3351       /* show the remote video */
3352       g_idle_add_full (G_PRIORITY_DEFAULT_IDLE,
3353           empathy_call_window_show_video_output_cb,
3354           g_object_ref (self), g_object_unref);
3355
3356       self->priv->got_video = TRUE;
3357     }
3358
3359   return TRUE;
3360 }
3361
3362 /* Called from the streaming thread */
3363 static gboolean
3364 empathy_call_window_src_added_cb (EmpathyCallHandler *handler,
3365   TfContent *content, GstPad *src, gpointer user_data)
3366 {
3367   EmpathyCallWindow *self = EMPATHY_CALL_WINDOW (user_data);
3368   EmpathyCallWindowPriv *priv = GET_PRIV (self);
3369   gboolean retval = FALSE;
3370   guint media_type;
3371
3372   GstPad *pad;
3373
3374   g_mutex_lock (priv->lock);
3375
3376   g_object_get (content, "media-type", &media_type, NULL);
3377
3378   switch (media_type)
3379     {
3380       case TP_MEDIA_STREAM_TYPE_AUDIO:
3381         pad = empathy_call_window_get_audio_sink_pad (self, content);
3382         break;
3383       case TP_MEDIA_STREAM_TYPE_VIDEO:
3384         g_idle_add (empathy_call_window_show_video_output_cb, self);
3385         pad = empathy_call_window_get_video_sink_pad (self);
3386
3387         gst_pad_add_data_probe (src,
3388             G_CALLBACK (empathy_call_window_video_probe_cb), self);
3389         if (priv->got_video_src > 0)
3390           g_source_remove (priv->got_video_src);
3391         priv->got_video_src = g_timeout_add_seconds (5,
3392             empathy_call_window_check_video_cb, self);
3393         break;
3394       default:
3395         g_assert_not_reached ();
3396     }
3397
3398   if (pad == NULL)
3399     goto out;
3400
3401   if (GST_PAD_LINK_FAILED (gst_pad_link (src, pad)))
3402       g_warning ("Could not link %s sink pad",
3403           media_type == TP_MEDIA_STREAM_TYPE_AUDIO ? "audio" : "video");
3404   else
3405       retval = TRUE;
3406
3407   gst_object_unref (pad);
3408
3409  out:
3410
3411   /* If no sink could be linked, try to add fakesink to prevent the whole call
3412    * aborting */
3413
3414   if (!retval)
3415     {
3416       GstElement *fakesink = gst_element_factory_make ("fakesink", NULL);
3417
3418       if (gst_bin_add (GST_BIN (priv->pipeline), fakesink))
3419         {
3420           GstPad *sinkpad = gst_element_get_static_pad (fakesink, "sink");
3421           if (gst_element_set_state (fakesink, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE ||
3422               GST_PAD_LINK_FAILED (gst_pad_link (src, sinkpad)))
3423             {
3424               gst_element_set_locked_state (fakesink, TRUE);
3425               gst_element_set_state (fakesink, GST_STATE_NULL);
3426               gst_bin_remove (GST_BIN (priv->pipeline), fakesink);
3427             }
3428           else
3429             {
3430               DEBUG ("Could not link real sink, linked fakesink instead");
3431             }
3432           gst_object_unref (sinkpad);
3433         }
3434       else
3435         {
3436           gst_object_unref (fakesink);
3437         }
3438     }
3439
3440
3441   g_mutex_unlock (priv->lock);
3442
3443   return TRUE;
3444 }
3445
3446 static gboolean
3447 empathy_call_window_content_added_cb (EmpathyCallHandler *handler,
3448   TfContent *content, gpointer user_data)
3449 {
3450   EmpathyCallWindow *self = EMPATHY_CALL_WINDOW (user_data);
3451   EmpathyCallWindowPriv *priv = GET_PRIV (self);
3452   GstPad *sink, *pad;
3453   FsMediaType media_type;
3454   gboolean retval = FALSE;
3455
3456   g_object_get (content, "media-type", &media_type, "sink-pad", &sink, NULL);
3457   g_assert (sink != NULL);
3458
3459   switch (media_type)
3460     {
3461       case FS_MEDIA_TYPE_AUDIO:
3462
3463         /* For raw audio conferences assume that the receiver of the raw data
3464          * wants it unprocessed, so turn off any echo cancellation and any
3465          * other audio improvements that come with it */
3466         empathy_audio_src_set_echo_cancel (
3467           EMPATHY_GST_AUDIO_SRC (priv->audio_input),
3468           !empathy_call_window_content_is_raw (content));
3469
3470         /* Link volumes together, sync the current audio input volume property
3471          * back to farstream first */
3472         g_object_bind_property_full (content, "requested-input-volume",
3473           priv->audio_input, "volume",
3474           G_BINDING_DEFAULT,
3475           audio_control_volume_to_element,
3476           element_volume_to_audio_control,
3477           NULL, NULL);
3478
3479         g_object_bind_property_full (priv->audio_input, "volume",
3480           content, "reported-input-volume",
3481           G_BINDING_SYNC_CREATE,
3482           element_volume_to_audio_control,
3483           audio_control_volume_to_element,
3484           NULL, NULL);
3485
3486         if (!gst_bin_add (GST_BIN (priv->pipeline), priv->audio_input))
3487           {
3488             g_warning ("Could not add audio source to pipeline");
3489             break;
3490           }
3491
3492         pad = gst_element_get_static_pad (priv->audio_input, "src");
3493         if (!pad)
3494           {
3495             gst_bin_remove (GST_BIN (priv->pipeline), priv->audio_input);
3496             g_warning ("Could not get source pad from audio source");
3497             break;
3498           }
3499
3500         if (GST_PAD_LINK_FAILED (gst_pad_link (pad, sink)))
3501           {
3502             gst_bin_remove (GST_BIN (priv->pipeline), priv->audio_input);
3503             g_warning ("Could not link audio source to farsight");
3504             break;
3505           }
3506
3507         if (gst_element_set_state (priv->audio_input, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE)
3508           {
3509             g_warning ("Could not start audio source");
3510             gst_element_set_state (priv->audio_input, GST_STATE_NULL);
3511             gst_bin_remove (GST_BIN (priv->pipeline), priv->audio_input);
3512             break;
3513           }
3514
3515         retval = TRUE;
3516         break;
3517       case FS_MEDIA_TYPE_VIDEO:
3518         if (priv->video_tee != NULL)
3519           {
3520             pad = gst_element_get_request_pad (priv->video_tee, "src%d");
3521             if (GST_PAD_LINK_FAILED (gst_pad_link (pad, sink)))
3522               {
3523                 g_warning ("Could not link video source input pipeline");
3524                 break;
3525               }
3526             gst_object_unref (pad);
3527           }
3528
3529         retval = TRUE;
3530         break;
3531       default:
3532         g_assert_not_reached ();
3533     }
3534
3535   gst_object_unref (sink);
3536   return retval;
3537 }
3538
3539 static void
3540 empathy_call_window_remove_video_input (EmpathyCallWindow *self)
3541 {
3542   EmpathyCallWindowPriv *priv = GET_PRIV (self);
3543   GstElement *preview;
3544
3545   disable_camera (self);
3546
3547   DEBUG ("remove video input");
3548   preview = priv->video_preview_sink;
3549
3550   gst_element_set_state (priv->video_input, GST_STATE_NULL);
3551   gst_element_set_state (priv->video_tee, GST_STATE_NULL);
3552   gst_element_set_state (preview, GST_STATE_NULL);
3553
3554   gst_bin_remove_many (GST_BIN (priv->pipeline), priv->video_input,
3555     preview, NULL);
3556
3557   g_object_unref (priv->video_input);
3558   priv->video_input = NULL;
3559   g_object_unref (priv->video_tee);
3560   priv->video_tee = NULL;
3561   clutter_actor_destroy (priv->video_preview);
3562   priv->video_preview = NULL;
3563
3564   gtk_widget_set_sensitive (priv->camera_button, FALSE);
3565 }
3566
3567 static void
3568 start_call (EmpathyCallWindow *self)
3569 {
3570   EmpathyCallWindowPriv *priv = GET_PRIV (self);
3571
3572   priv->call_started = TRUE;
3573   empathy_call_handler_start_call (priv->handler,
3574       gtk_get_current_event_time ());
3575
3576   if (empathy_call_handler_has_initial_video (priv->handler))
3577     {
3578       TpyCallChannel *call;
3579       TpySendingState s;
3580
3581       g_object_get (priv->handler, "call-channel", &call, NULL);
3582       /* If the call channel isn't set yet we're requesting it, if we're
3583        * requesting it with initial video it should be PENDING_SEND when we get
3584        * it */
3585       if (call == NULL)
3586         s = TPY_SENDING_STATE_PENDING_SEND;
3587       else
3588         s = tpy_call_channel_get_video_state (call);
3589
3590       if (s == TPY_SENDING_STATE_PENDING_SEND ||
3591           s == TPY_SENDING_STATE_SENDING)
3592         {
3593           /* Enable 'send video' buttons and display the preview */
3594           gtk_toggle_tool_button_set_active (
3595             GTK_TOGGLE_TOOL_BUTTON (priv->camera_button), TRUE);
3596         }
3597       else
3598         {
3599           gtk_toggle_tool_button_set_active (
3600             GTK_TOGGLE_TOOL_BUTTON (priv->camera_button), FALSE);
3601
3602           if (priv->video_preview == NULL)
3603             {
3604               create_video_preview (self);
3605               add_video_preview_to_pipeline (self);
3606             }
3607         }
3608
3609       if (call != NULL)
3610         g_object_unref (call);
3611     }
3612 }
3613
3614 static gboolean
3615 empathy_call_window_bus_message (GstBus *bus, GstMessage *message,
3616   gpointer user_data)
3617 {
3618   EmpathyCallWindow *self = EMPATHY_CALL_WINDOW (user_data);
3619   EmpathyCallWindowPriv *priv = GET_PRIV (self);
3620   GstState newstate, pending;
3621
3622   empathy_call_handler_bus_message (priv->handler, bus, message);
3623
3624   switch (GST_MESSAGE_TYPE (message))
3625     {
3626       case GST_MESSAGE_STATE_CHANGED:
3627         if (GST_MESSAGE_SRC (message) == GST_OBJECT (priv->video_input))
3628           {
3629             gst_message_parse_state_changed (message, NULL, &newstate, NULL);
3630           }
3631         if (GST_MESSAGE_SRC (message) == GST_OBJECT (priv->pipeline) &&
3632             !priv->call_started)
3633           {
3634             gst_message_parse_state_changed (message, NULL, &newstate, NULL);
3635             if (newstate == GST_STATE_PAUSED)
3636               {
3637                 gst_element_set_state (priv->pipeline, GST_STATE_PLAYING);
3638                 priv->pipeline_playing = TRUE;
3639
3640                 if (priv->start_call_when_playing)
3641                   start_call (self);
3642               }
3643           }
3644         if (GST_MESSAGE_SRC (message) == GST_OBJECT (priv->video_preview_sink))
3645           {
3646             gst_message_parse_state_changed (message, NULL, &newstate,
3647                 &pending);
3648
3649             if (newstate == GST_STATE_PLAYING &&
3650                 pending == GST_STATE_VOID_PENDING)
3651               empathy_call_window_stop_camera_spinning (self);
3652           }
3653         break;
3654       case GST_MESSAGE_ERROR:
3655         {
3656           GError *error = NULL;
3657           GstElement *gst_error;
3658           gchar *debug;
3659
3660           gst_message_parse_error (message, &error, &debug);
3661           gst_error = GST_ELEMENT (GST_MESSAGE_SRC (message));
3662
3663           g_message ("Element error: %s -- %s\n", error->message, debug);
3664
3665           if (g_str_has_prefix (gst_element_get_name (gst_error),
3666                 VIDEO_INPUT_ERROR_PREFIX))
3667             {
3668               /* Remove the video input and continue */
3669               if (priv->video_input != NULL)
3670                 empathy_call_window_remove_video_input (self);
3671               gst_element_set_state (priv->pipeline, GST_STATE_PLAYING);
3672             }
3673           else
3674             {
3675               empathy_call_window_disconnected (self, TRUE);
3676             }
3677           g_error_free (error);
3678           g_free (debug);
3679         }
3680       case GST_MESSAGE_UNKNOWN:
3681       case GST_MESSAGE_EOS:
3682       case GST_MESSAGE_WARNING:
3683       case GST_MESSAGE_INFO:
3684       case GST_MESSAGE_TAG:
3685       case GST_MESSAGE_BUFFERING:
3686       case GST_MESSAGE_STATE_DIRTY:
3687       case GST_MESSAGE_STEP_DONE:
3688       case GST_MESSAGE_CLOCK_PROVIDE:
3689       case GST_MESSAGE_CLOCK_LOST:
3690       case GST_MESSAGE_NEW_CLOCK:
3691       case GST_MESSAGE_STRUCTURE_CHANGE:
3692       case GST_MESSAGE_STREAM_STATUS:
3693       case GST_MESSAGE_APPLICATION:
3694       case GST_MESSAGE_ELEMENT:
3695       case GST_MESSAGE_SEGMENT_START:
3696       case GST_MESSAGE_SEGMENT_DONE:
3697       case GST_MESSAGE_DURATION:
3698       case GST_MESSAGE_ANY:
3699       default:
3700         break;
3701     }
3702
3703   return TRUE;
3704 }
3705
3706 static void
3707 empathy_call_window_members_changed_cb (TpyCallChannel *call,
3708     GHashTable *members,
3709     EmpathyCallWindow *self)
3710 {
3711   EmpathyCallWindowPriv *priv = GET_PRIV (self);
3712   GHashTableIter iter;
3713   gpointer key, value;
3714   gboolean held = FALSE;
3715
3716   g_hash_table_iter_init (&iter, members);
3717   while (g_hash_table_iter_next (&iter, &key, &value))
3718     {
3719       if (GPOINTER_TO_INT (value) & TPY_CALL_MEMBER_FLAG_HELD)
3720         {
3721           /* This assumes this is a 1-1 call, otherwise one participant
3722            * putting the call on hold wouldn't mean the call is on hold
3723            * for everyone. */
3724           held = TRUE;
3725           break;
3726         }
3727     }
3728
3729   if (held)
3730     priv->call_state = HELD;
3731   else if (priv->call_state == HELD)
3732     priv->call_state = CONNECTED;
3733 }
3734
3735 static void
3736 call_handler_notify_call_cb (EmpathyCallHandler *handler,
3737     GParamSpec *spec,
3738     EmpathyCallWindow *self)
3739 {
3740   EmpathyCallWindowPriv *priv = GET_PRIV (self);
3741   TpyCallChannel *call;
3742
3743   g_object_get (priv->handler, "call-channel", &call, NULL);
3744   if (call == NULL)
3745     return;
3746
3747 /* FIXME
3748   tp_g_signal_connect_object (call, "audio-stream-error",
3749       G_CALLBACK (empathy_call_window_audio_stream_error), self, 0);
3750   tp_g_signal_connect_object (call, "video-stream-error",
3751       G_CALLBACK (empathy_call_window_video_stream_error), self, 0);
3752 */
3753
3754   tp_g_signal_connect_object (call, "members-changed",
3755       G_CALLBACK (empathy_call_window_members_changed_cb), self, 0);
3756
3757   tp_cli_channel_interface_dtmf_connect_to_stopped_tones (TP_CHANNEL (call),
3758       empathy_call_window_tones_stopped_cb, self, NULL,
3759       G_OBJECT (call), NULL);
3760
3761   g_object_unref (call);
3762 }
3763
3764 static void
3765 empathy_call_window_connect_handler (EmpathyCallWindow *self)
3766 {
3767   EmpathyCallWindowPriv *priv = GET_PRIV (self);
3768   TpyCallChannel *call;
3769
3770   g_signal_connect (priv->handler, "state-changed",
3771     G_CALLBACK (empathy_call_window_state_changed_cb), self);
3772   g_signal_connect (priv->handler, "conference-added",
3773     G_CALLBACK (empathy_call_window_conference_added_cb), self);
3774   g_signal_connect (priv->handler, "conference-removed",
3775     G_CALLBACK (empathy_call_window_conference_removed_cb), self);
3776   g_signal_connect (priv->handler, "closed",
3777     G_CALLBACK (empathy_call_window_channel_closed_cb), self);
3778   g_signal_connect (priv->handler, "src-pad-added",
3779     G_CALLBACK (empathy_call_window_src_added_cb), self);
3780   g_signal_connect (priv->handler, "content-added",
3781     G_CALLBACK (empathy_call_window_content_added_cb), self);
3782   g_signal_connect (priv->handler, "content-removed",
3783     G_CALLBACK (empathy_call_window_content_removed_cb), self);
3784
3785   /* We connect to ::call-channel unconditionally since we'll
3786    * get new channels if we hangup and redial or if we reuse the
3787    * call window. */
3788   g_signal_connect (priv->handler, "notify::call-channel",
3789     G_CALLBACK (call_handler_notify_call_cb), self);
3790
3791   g_signal_connect (priv->handler, "framerate-changed",
3792     G_CALLBACK (empathy_call_window_framerate_changed_cb), self);
3793   g_signal_connect (priv->handler, "resolution-changed",
3794     G_CALLBACK (empathy_call_window_resolution_changed_cb), self);
3795
3796   g_object_get (priv->handler, "call-channel", &call, NULL);
3797   if (call != NULL)
3798     {
3799       /* We won't get notify::call-channel for this channel, so
3800        * directly call the callback. */
3801       call_handler_notify_call_cb (priv->handler, NULL, self);
3802       g_object_unref (call);
3803     }
3804 }
3805
3806 static void
3807 empathy_call_window_realized_cb (GtkWidget *widget,
3808     EmpathyCallWindow *self)
3809 {
3810   gint width;
3811
3812   /* Make the hangup button twice as wide */
3813   width = gtk_widget_get_allocated_width (self->priv->hangup_button);
3814   gtk_widget_set_size_request (self->priv->hangup_button, width * 2, -1);
3815
3816   empathy_call_window_connect_handler (self);
3817
3818   gst_element_set_state (self->priv->pipeline, GST_STATE_PAUSED);
3819 }
3820
3821 static gboolean
3822 empathy_call_window_delete_cb (GtkWidget *widget, GdkEvent*event,
3823   EmpathyCallWindow *window)
3824 {
3825   EmpathyCallWindowPriv *priv = GET_PRIV (window);
3826
3827   if (priv->pipeline != NULL)
3828     {
3829       if (priv->bus_message_source_id != 0)
3830         {
3831           g_source_remove (priv->bus_message_source_id);
3832           priv->bus_message_source_id = 0;
3833         }
3834
3835       gst_element_set_state (priv->pipeline, GST_STATE_NULL);
3836     }
3837
3838   if (priv->call_state == CONNECTING)
3839     empathy_sound_manager_stop (priv->sound_mgr, EMPATHY_SOUND_PHONE_OUTGOING);
3840
3841   return FALSE;
3842 }
3843
3844 static void
3845 show_controls (EmpathyCallWindow *window, gboolean set_fullscreen)
3846 {
3847   GtkWidget *menu;
3848   EmpathyCallWindowPriv *priv = GET_PRIV (window);
3849
3850   menu = gtk_ui_manager_get_widget (priv->ui_manager,
3851             "/menubar1");
3852
3853   if (set_fullscreen)
3854     {
3855       gtk_widget_hide (priv->dtmf_panel);
3856       gtk_widget_hide (menu);
3857       gtk_widget_hide (priv->toolbar);
3858     }
3859   else
3860     {
3861       if (priv->dialpad_was_visible_before_fs)
3862         gtk_widget_show (priv->dtmf_panel);
3863
3864       gtk_widget_show (menu);
3865       gtk_widget_show (priv->toolbar);
3866
3867       gtk_window_resize (GTK_WINDOW (window), priv->original_width_before_fs,
3868           priv->original_height_before_fs);
3869     }
3870 }
3871
3872 static void
3873 show_borders (EmpathyCallWindow *window, gboolean set_fullscreen)
3874 {
3875   EmpathyCallWindowPriv *priv = GET_PRIV (window);
3876
3877   gtk_container_set_border_width (GTK_CONTAINER (priv->content_hbox),
3878       set_fullscreen ? 0 : CONTENT_HBOX_BORDER_WIDTH);
3879   gtk_box_set_spacing (GTK_BOX (priv->content_hbox),
3880       set_fullscreen ? 0 : CONTENT_HBOX_SPACING);
3881
3882   if (priv->video_output != NULL)
3883     {
3884 #if 0
3885       gtk_box_set_child_packing (GTK_BOX (priv->content_hbox),
3886           priv->video_output, TRUE, TRUE,
3887           set_fullscreen ? 0 : CONTENT_HBOX_CHILDREN_PACKING_PADDING,
3888           GTK_PACK_START);
3889 #endif
3890     }
3891 }
3892
3893 static gboolean
3894 empathy_call_window_state_event_cb (GtkWidget *widget,
3895   GdkEventWindowState *event, EmpathyCallWindow *window)
3896 {
3897   if (event->changed_mask & GDK_WINDOW_STATE_FULLSCREEN)
3898     {
3899       EmpathyCallWindowPriv *priv = GET_PRIV (window);
3900       gboolean set_fullscreen = event->new_window_state &
3901         GDK_WINDOW_STATE_FULLSCREEN;
3902
3903       if (set_fullscreen)
3904         {
3905           gboolean dialpad_was_visible;
3906           GtkAllocation allocation;
3907           gint original_width, original_height;
3908
3909           gtk_widget_get_allocation (GTK_WIDGET (window), &allocation);
3910           original_width = allocation.width;
3911           original_height = allocation.height;
3912
3913           g_object_get (priv->dtmf_panel,
3914               "visible", &dialpad_was_visible,
3915               NULL);
3916
3917           priv->dialpad_was_visible_before_fs = dialpad_was_visible;
3918           priv->original_width_before_fs = original_width;
3919           priv->original_height_before_fs = original_height;
3920
3921           if (priv->video_output_motion_handler_id == 0 &&
3922                 priv->video_output != NULL)
3923             {
3924               priv->video_output_motion_handler_id = g_signal_connect (
3925                   G_OBJECT (priv->video_container), "motion-notify-event",
3926                   G_CALLBACK (empathy_call_window_video_output_motion_notify),
3927                   window);
3928             }
3929         }
3930       else
3931         {
3932           disconnect_video_output_motion_handler (window);
3933         }
3934
3935       empathy_call_window_fullscreen_set_fullscreen (priv->fullscreen,
3936           set_fullscreen);
3937       show_controls (window, set_fullscreen);
3938       show_borders (window, set_fullscreen);
3939       gtk_action_set_stock_id (priv->menu_fullscreen,
3940           (set_fullscreen ? "gtk-leave-fullscreen" : "gtk-fullscreen"));
3941       priv->is_fullscreen = set_fullscreen;
3942   }
3943
3944   return FALSE;
3945 }
3946
3947 static void
3948 empathy_call_window_show_dialpad (EmpathyCallWindow *window,
3949     gboolean active)
3950 {
3951   EmpathyCallWindowPriv *priv = GET_PRIV (window);
3952   int w, h, dialpad_width;
3953   GtkAllocation allocation;
3954
3955   gtk_widget_get_allocation (GTK_WIDGET (window), &allocation);
3956   w = allocation.width;
3957   h = allocation.height;
3958
3959   gtk_widget_get_preferred_width (priv->dtmf_panel, &dialpad_width, NULL);
3960
3961   if (active)
3962     {
3963       gtk_widget_show (priv->dtmf_panel);
3964       w += dialpad_width;
3965     }
3966   else
3967     {
3968       w -= dialpad_width;
3969       gtk_widget_hide (priv->dtmf_panel);
3970     }
3971
3972   if (w > 0 && h > 0)
3973     gtk_window_resize (GTK_WINDOW (window), w, h);
3974 }
3975
3976 static void
3977 empathy_call_window_set_send_video (EmpathyCallWindow *window,
3978   CameraState state)
3979 {
3980   EmpathyCallWindowPriv *priv = GET_PRIV (window);
3981   TpyCallChannel *call;
3982
3983   priv->sending_video = (state == CAMERA_STATE_ON);
3984
3985   if (state == CAMERA_STATE_ON)
3986     {
3987       /* When we start sending video, we want to show the video preview by
3988          default. */
3989       display_video_preview (window, TRUE);
3990     }
3991   else
3992     {
3993       display_video_preview (window, FALSE);
3994     }
3995
3996   if (priv->call_state != CONNECTED)
3997     return;
3998
3999   g_object_get (priv->handler, "call-channel", &call, NULL);
4000   DEBUG ("%s sending video", priv->sending_video ? "start": "stop");
4001   tpy_call_channel_send_video (call, priv->sending_video);
4002   g_object_unref (call);
4003 }
4004
4005 static void
4006 empathy_call_window_hangup_cb (gpointer object,
4007     EmpathyCallWindow *self)
4008 {
4009   empathy_call_handler_stop_call (self->priv->handler);
4010
4011   empathy_call_window_disconnected (self, TRUE);
4012 }
4013
4014 static void
4015 empathy_call_window_restart_call (EmpathyCallWindow *window)
4016 {
4017   EmpathyCallWindowPriv *priv = GET_PRIV (window);
4018
4019   /* Remove error info bars */
4020   gtk_container_forall (GTK_CONTAINER (priv->errors_vbox),
4021       (GtkCallback) gtk_widget_destroy, NULL);
4022
4023   create_video_output_widget (window);
4024   priv->outgoing = TRUE;
4025   empathy_call_window_set_state_connecting (window);
4026
4027   if (priv->pipeline_playing)
4028     start_call (window);
4029   else
4030     /* call will be started when the pipeline is ready */
4031     priv->start_call_when_playing = TRUE;
4032
4033   empathy_call_window_setup_avatars (window, priv->handler);
4034
4035   empathy_call_window_show_hangup_button (window, TRUE);
4036 }
4037
4038 static void
4039 empathy_call_window_dialpad_cb (GtkToggleToolButton *button,
4040     EmpathyCallWindow *window)
4041 {
4042   gboolean active;
4043
4044   active = gtk_toggle_tool_button_get_active (button);
4045
4046   empathy_call_window_show_dialpad (window, active);
4047 }
4048
4049 static void
4050 empathy_call_window_fullscreen_cb (gpointer object,
4051                                    EmpathyCallWindow *window)
4052 {
4053   empathy_call_window_fullscreen_toggle (window);
4054 }
4055
4056 static void
4057 empathy_call_window_fullscreen_toggle (EmpathyCallWindow *window)
4058 {
4059   EmpathyCallWindowPriv *priv = GET_PRIV (window);
4060
4061   if (priv->is_fullscreen)
4062     gtk_window_unfullscreen (GTK_WINDOW (window));
4063   else
4064     gtk_window_fullscreen (GTK_WINDOW (window));
4065 }
4066
4067 static gboolean
4068 empathy_call_window_video_button_press_cb (GtkWidget *video_preview,
4069   GdkEventButton *event, EmpathyCallWindow *window)
4070 {
4071   if (event->button == 3 && event->type == GDK_BUTTON_PRESS)
4072     {
4073       empathy_call_window_video_menu_popup (window, event->button);
4074       return TRUE;
4075     }
4076
4077   return FALSE;
4078 }
4079
4080 static gboolean
4081 empathy_call_window_key_press_cb (GtkWidget *video_output,
4082   GdkEventKey *event, EmpathyCallWindow *window)
4083 {
4084   EmpathyCallWindowPriv *priv = GET_PRIV (window);
4085
4086   if (priv->is_fullscreen && event->keyval == GDK_KEY_Escape)
4087     {
4088       /* Since we are in fullscreen mode, toggling will bring us back to
4089          normal mode. */
4090       empathy_call_window_fullscreen_toggle (window);
4091       return TRUE;
4092     }
4093
4094   return FALSE;
4095 }
4096
4097 static gboolean
4098 empathy_call_window_video_output_motion_notify (GtkWidget *widget,
4099     GdkEventMotion *event, EmpathyCallWindow *window)
4100 {
4101   EmpathyCallWindowPriv *priv = GET_PRIV (window);
4102
4103   if (priv->is_fullscreen)
4104     {
4105       empathy_call_window_fullscreen_show_popup (priv->fullscreen);
4106
4107       /* Show the bottom toolbar */
4108       empathy_call_window_motion_notify_cb (NULL, NULL, window);
4109       return TRUE;
4110     }
4111   return FALSE;
4112 }
4113
4114 static void
4115 empathy_call_window_video_menu_popup (EmpathyCallWindow *window,
4116   guint button)
4117 {
4118   GtkWidget *menu;
4119   EmpathyCallWindowPriv *priv = GET_PRIV (window);
4120
4121   menu = gtk_ui_manager_get_widget (priv->ui_manager,
4122             "/video-popup");
4123   gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL,
4124       button, gtk_get_current_event_time ());
4125   gtk_menu_shell_select_first (GTK_MENU_SHELL (menu), FALSE);
4126 }
4127
4128 static void
4129 empathy_call_window_status_message (EmpathyCallWindow *self,
4130   gchar *message)
4131 {
4132   gtk_label_set_label (GTK_LABEL (self->priv->status_label), message);
4133 }
4134
4135 GtkUIManager *
4136 empathy_call_window_get_ui_manager (EmpathyCallWindow *window)
4137 {
4138   EmpathyCallWindowPriv *priv = GET_PRIV (window);
4139
4140   return priv->ui_manager;
4141 }
4142
4143 EmpathyGstAudioSrc *
4144 empathy_call_window_get_audio_src (EmpathyCallWindow *window)
4145 {
4146   EmpathyCallWindowPriv *priv = GET_PRIV (window);
4147
4148   return (EmpathyGstAudioSrc *) priv->audio_input;
4149 }
4150
4151 EmpathyGstVideoSrc *
4152 empathy_call_window_get_video_src (EmpathyCallWindow *self)
4153 {
4154   return EMPATHY_GST_VIDEO_SRC (self->priv->video_input);
4155 }