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