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