]> git.0d.be Git - empathy.git/blobdiff - src/empathy-call-window.c
CallWindow: correctly detect outgoing calls
[empathy.git] / src / empathy-call-window.c
index c8dc182fb5638191c611309eb0eb9b22de5a2602..05f63e2f6813197bd3fa569e4b3fb96cbf21c03d 100644 (file)
 #include "empathy-mic-menu.h"
 #include "empathy-preferences.h"
 #include "empathy-rounded-actor.h"
+#include "empathy-rounded-rectangle.h"
+#include "empathy-rounded-texture.h"
+#include "empathy-camera-menu.h"
 
 #define CONTENT_HBOX_BORDER_WIDTH 6
 #define CONTENT_HBOX_SPACING 3
 #define CONTENT_HBOX_CHILDREN_PACKING_PADDING 3
 
 #define SELF_VIDEO_SECTION_WIDTH 120
-#define SELF_VIDEO_SECTION_HEIGTH 90
+#define SELF_VIDEO_SECTION_HEIGHT 90
 #define SELF_VIDEO_SECTION_MARGIN 10
 
 #define FLOATING_TOOLBAR_OPACITY 192
@@ -79,8 +82,6 @@
 #define FLOATING_TOOLBAR_HEIGHT 36
 #define FLOATING_TOOLBAR_SPACING 20
 
-#define SELF_VIDEO_MARGIN 10
-
 /* The avatar's default width and height are set to the same value because we
    want a square icon. */
 #define REMOTE_CONTACT_AVATAR_DEFAULT_WIDTH EMPATHY_VIDEO_WIDGET_DEFAULT_HEIGHT
@@ -140,11 +141,17 @@ struct _EmpathyCallWindowPriv
    * alive only during call. */
   ClutterActor *video_output;
   ClutterActor *video_preview;
+  ClutterActor *drag_preview;
+  ClutterActor *preview_shown_button;
   ClutterActor *preview_hidden_button;
   ClutterActor *preview_rectangle1;
   ClutterActor *preview_rectangle2;
   ClutterActor *preview_rectangle3;
   ClutterActor *preview_rectangle4;
+  ClutterActor *preview_rectangle_box1;
+  ClutterActor *preview_rectangle_box2;
+  ClutterActor *preview_rectangle_box3;
+  ClutterActor *preview_rectangle_box4;
   GtkWidget *video_container;
   GtkWidget *remote_user_avatar_widget;
   GtkWidget *remote_user_avatar_toolbar;
@@ -161,6 +168,7 @@ struct _EmpathyCallWindowPriv
   ClutterActor *floating_toolbar;
   GtkWidget *pane;
   GtkAction *menu_fullscreen;
+  GtkAction *menu_swap_camera;
 
   ClutterState *transitions;
 
@@ -170,8 +178,7 @@ struct _EmpathyCallWindowPriv
   ClutterLayoutManager *video_layout;
 
   /* Coordinates of the preview drag event's start. */
-  gfloat event_x;
-  gfloat event_y;
+  PreviewPosition preview_pos;
 
   /* We keep a reference on the hbox which contains the main content so we can
      easilly repack everything when toggling fullscreen */
@@ -246,6 +253,7 @@ struct _EmpathyCallWindowPriv
 
   GSettings *settings;
   EmpathyMicMenu *mic_menu;
+  EmpathyCameraMenu *camera_menu;
 };
 
 #define GET_PRIV(o) (EMPATHY_CALL_WINDOW (o)->priv)
@@ -391,6 +399,17 @@ empathy_call_window_prefs_volume_changed_cb (GSettings *settings,
   empathy_call_window_mic_volume_changed (self);
 }
 
+static void
+empathy_call_window_raise_actors (EmpathyCallWindow *self)
+{
+  clutter_actor_raise_top (self->priv->floating_toolbar);
+
+  clutter_actor_raise_top (self->priv->preview_rectangle_box1);
+  clutter_actor_raise_top (self->priv->preview_rectangle_box2);
+  clutter_actor_raise_top (self->priv->preview_rectangle_box3);
+  clutter_actor_raise_top (self->priv->preview_rectangle_box4);
+}
+
 static void
 empathy_call_window_show_video_output (EmpathyCallWindow *self,
     gboolean show)
@@ -400,7 +419,7 @@ empathy_call_window_show_video_output (EmpathyCallWindow *self,
 
   gtk_widget_set_visible (self->priv->remote_user_avatar_widget, !show);
 
-  clutter_actor_raise_top (self->priv->floating_toolbar);
+  empathy_call_window_raise_actors (self);
 }
 
 static void
@@ -514,6 +533,67 @@ empathy_call_window_maximise_camera_cb (GtkAction *action,
   clutter_actor_hide (self->priv->preview_hidden_button);
 }
 
+static void
+empathy_call_window_swap_camera_cb (GtkAction *action,
+    EmpathyCallWindow *self)
+{
+  const GList *cameras, *l;
+  gchar *current_cam;
+
+  DEBUG ("Swapping the camera");
+
+  cameras = empathy_camera_monitor_get_cameras (self->priv->camera_monitor);
+  current_cam = empathy_video_src_dup_device (
+      EMPATHY_GST_VIDEO_SRC (self->priv->video_input));
+
+  for (l = cameras; l != NULL; l = l->next)
+    {
+      EmpathyCamera *camera = l->data;
+
+      if (!tp_strdiff (camera->device, current_cam))
+        {
+          EmpathyCamera *next;
+
+          if (l->next != NULL)
+            next = l->next->data;
+          else
+            next = cameras->data;
+
+          /* EmpathyCameraMenu will update itself and do the actual change
+           * for us */
+          g_settings_set_string (self->priv->settings,
+              EMPATHY_PREFS_CALL_CAMERA_DEVICE,
+              next->device);
+
+          break;
+        }
+    }
+
+  g_free (current_cam);
+}
+
+static void
+empathy_call_window_camera_added_cb (EmpathyCameraMonitor *monitor,
+    EmpathyCamera *camera,
+    EmpathyCallWindow *self)
+{
+  const GList *cameras = empathy_camera_monitor_get_cameras (monitor);
+
+  gtk_action_set_visible (self->priv->menu_swap_camera,
+      g_list_length ((GList *) cameras) >= 2);
+}
+
+static void
+empathy_call_window_camera_removed_cb (EmpathyCameraMonitor *monitor,
+    EmpathyCamera *camera,
+    EmpathyCallWindow *self)
+{
+  const GList *cameras = empathy_camera_monitor_get_cameras (monitor);
+
+  gtk_action_set_visible (self->priv->menu_swap_camera,
+      g_list_length ((GList *) cameras) >= 2);
+}
+
 static void
 empathy_call_window_preview_button_clicked_cb (GtkButton *button,
     EmpathyCallWindow *self)
@@ -542,35 +622,47 @@ empathy_call_window_preview_hidden_button_clicked_cb (GtkButton *button,
 
 static ClutterActor *
 empathy_call_window_create_preview_rectangle (EmpathyCallWindow *self,
+    ClutterActor **box,
     ClutterBinAlignment x,
     ClutterBinAlignment y)
 {
-  ClutterLayoutManager *layout;
+  ClutterLayoutManager *layout1, *layout2;
   ClutterActor *rectangle;
-  ClutterActor *box;
+  ClutterActor *box1, *box2;
 
-  layout = clutter_bin_layout_new (CLUTTER_BIN_ALIGNMENT_CENTER,
-      CLUTTER_BIN_ALIGNMENT_CENTER);
+  layout1 = clutter_bin_layout_new (CLUTTER_BIN_ALIGNMENT_CENTER,
+      CLUTTER_BIN_ALIGNMENT_START);
 
-  box = clutter_box_new (layout);
+  box1 = clutter_box_new (layout1);
 
-  rectangle = clutter_rectangle_new_with_color (
-      CLUTTER_COLOR_Transparent);
+  *box = box1;
 
-  clutter_rectangle_set_border_width (CLUTTER_RECTANGLE (rectangle),
-      1);
+  rectangle = empathy_rounded_rectangle_new (
+      SELF_VIDEO_SECTION_WIDTH + 5,
+      SELF_VIDEO_SECTION_HEIGHT + 5);
 
-  clutter_actor_set_size (box,
-      SELF_VIDEO_SECTION_WIDTH + 2 * SELF_VIDEO_MARGIN,
-      SELF_VIDEO_SECTION_HEIGTH + 2 * SELF_VIDEO_MARGIN);
+  clutter_actor_set_size (box1,
+      SELF_VIDEO_SECTION_WIDTH + 2 * SELF_VIDEO_SECTION_MARGIN,
+      SELF_VIDEO_SECTION_HEIGHT + 2 * SELF_VIDEO_SECTION_MARGIN +
+      FLOATING_TOOLBAR_HEIGHT + FLOATING_TOOLBAR_SPACING);
+
+  layout2 = clutter_bin_layout_new (CLUTTER_BIN_ALIGNMENT_CENTER,
+      CLUTTER_BIN_ALIGNMENT_CENTER);
 
-  clutter_actor_set_size (rectangle,
-      SELF_VIDEO_SECTION_WIDTH, SELF_VIDEO_SECTION_HEIGTH);
+  /* We have a box with the margins and the video in the middle inside
+   * a bigger box with an extra bottom margin so we're not on top of
+   * the floating toolbar. */
+  box2 = clutter_box_new (layout2);
 
-  clutter_container_add_actor (CLUTTER_CONTAINER (box), rectangle);
+  clutter_actor_set_size (box2,
+      SELF_VIDEO_SECTION_WIDTH + 2 * SELF_VIDEO_SECTION_MARGIN,
+      SELF_VIDEO_SECTION_HEIGHT + 2 * SELF_VIDEO_SECTION_MARGIN);
+
+  clutter_container_add_actor (CLUTTER_CONTAINER (box1), box2);
+  clutter_container_add_actor (CLUTTER_CONTAINER (box2), rectangle);
 
   clutter_bin_layout_add (CLUTTER_BIN_LAYOUT (self->priv->video_layout),
-      box, x, y);
+      box1, x, y);
 
   clutter_actor_hide (rectangle);
 
@@ -582,15 +674,19 @@ empathy_call_window_create_preview_rectangles (EmpathyCallWindow *self)
 {
   self->priv->preview_rectangle1 =
       empathy_call_window_create_preview_rectangle (self,
+          &self->priv->preview_rectangle_box1,
           CLUTTER_BIN_ALIGNMENT_START, CLUTTER_BIN_ALIGNMENT_START);
   self->priv->preview_rectangle2 =
       empathy_call_window_create_preview_rectangle (self,
+          &self->priv->preview_rectangle_box2,
           CLUTTER_BIN_ALIGNMENT_START, CLUTTER_BIN_ALIGNMENT_END);
   self->priv->preview_rectangle3 =
       empathy_call_window_create_preview_rectangle (self,
+          &self->priv->preview_rectangle_box3,
           CLUTTER_BIN_ALIGNMENT_END, CLUTTER_BIN_ALIGNMENT_START);
   self->priv->preview_rectangle4 =
       empathy_call_window_create_preview_rectangle (self,
+          &self->priv->preview_rectangle_box4,
           CLUTTER_BIN_ALIGNMENT_END, CLUTTER_BIN_ALIGNMENT_END);
 }
 
@@ -612,33 +708,36 @@ empathy_call_window_get_preview_position (EmpathyCallWindow *self,
   ClutterGeometry box;
   PreviewPosition pos = PREVIEW_POS_NONE;
 
+  if (!clutter_actor_has_allocation (self->priv->video_box))
+    return pos;
+
   clutter_actor_get_geometry (self->priv->video_box, &box);
 
-  if (0 + SELF_VIDEO_MARGIN <= event_x &&
-      event_x <= (0 + SELF_VIDEO_MARGIN + (gint) SELF_VIDEO_SECTION_WIDTH) &&
-      0 + SELF_VIDEO_MARGIN <= event_y &&
-      event_y <= (0 + SELF_VIDEO_MARGIN + (gint) SELF_VIDEO_SECTION_HEIGTH))
+  if (0 + SELF_VIDEO_SECTION_MARGIN <= event_x &&
+      event_x <= (0 + SELF_VIDEO_SECTION_MARGIN + (gint) SELF_VIDEO_SECTION_WIDTH) &&
+      0 + SELF_VIDEO_SECTION_MARGIN <= event_y &&
+      event_y <= (0 + SELF_VIDEO_SECTION_MARGIN + (gint) SELF_VIDEO_SECTION_HEIGHT))
     {
       pos = PREVIEW_POS_TOP_LEFT;
     }
-  else if (box.width - SELF_VIDEO_MARGIN >= event_x &&
-      event_x >= (box.width - SELF_VIDEO_MARGIN - (gint) SELF_VIDEO_SECTION_WIDTH) &&
-      0 + SELF_VIDEO_MARGIN <= event_y &&
-      event_y <= (0 + SELF_VIDEO_MARGIN + (gint) SELF_VIDEO_SECTION_HEIGTH))
+  else if (box.width - SELF_VIDEO_SECTION_MARGIN >= event_x &&
+      event_x >= (box.width - SELF_VIDEO_SECTION_MARGIN - (gint) SELF_VIDEO_SECTION_WIDTH) &&
+      0 + SELF_VIDEO_SECTION_MARGIN <= event_y &&
+      event_y <= (0 + SELF_VIDEO_SECTION_MARGIN + (gint) SELF_VIDEO_SECTION_HEIGHT))
     {
       pos = PREVIEW_POS_TOP_RIGHT;
     }
-  else if (0 + SELF_VIDEO_MARGIN <= event_x &&
-      event_x <= (0 + SELF_VIDEO_MARGIN + (gint) SELF_VIDEO_SECTION_WIDTH) &&
-      box.height - SELF_VIDEO_MARGIN >= event_y &&
-      event_y >= (box.height - SELF_VIDEO_MARGIN - (gint) SELF_VIDEO_SECTION_HEIGTH))
+  else if (0 + SELF_VIDEO_SECTION_MARGIN <= event_x &&
+      event_x <= (0 + SELF_VIDEO_SECTION_MARGIN + (gint) SELF_VIDEO_SECTION_WIDTH) &&
+      box.height - SELF_VIDEO_SECTION_MARGIN - FLOATING_TOOLBAR_HEIGHT - FLOATING_TOOLBAR_SPACING >= event_y &&
+      event_y >= (box.height - SELF_VIDEO_SECTION_MARGIN - FLOATING_TOOLBAR_HEIGHT - FLOATING_TOOLBAR_SPACING - (gint) SELF_VIDEO_SECTION_HEIGHT))
     {
       pos = PREVIEW_POS_BOTTOM_LEFT;
     }
-  else if (box.width - SELF_VIDEO_MARGIN >= event_x &&
-      event_x >= (box.width - SELF_VIDEO_MARGIN - (gint) SELF_VIDEO_SECTION_WIDTH) &&
-      box.height - SELF_VIDEO_MARGIN >= event_y &&
-      event_y >= (box.height - SELF_VIDEO_MARGIN - (gint) SELF_VIDEO_SECTION_HEIGTH))
+  else if (box.width - SELF_VIDEO_SECTION_MARGIN >= event_x &&
+      event_x >= (box.width - SELF_VIDEO_SECTION_MARGIN - (gint) SELF_VIDEO_SECTION_WIDTH) &&
+      box.height - SELF_VIDEO_SECTION_MARGIN - SELF_VIDEO_SECTION_MARGIN - FLOATING_TOOLBAR_HEIGHT - FLOATING_TOOLBAR_SPACING >= event_y &&
+      event_y >= (box.height - SELF_VIDEO_SECTION_MARGIN - FLOATING_TOOLBAR_HEIGHT - FLOATING_TOOLBAR_SPACING - (gint) SELF_VIDEO_SECTION_HEIGHT))
     {
       pos = PREVIEW_POS_BOTTOM_RIGHT;
     }
@@ -681,6 +780,8 @@ empathy_call_window_move_video_preview (EmpathyCallWindow *self,
 
   DEBUG ("moving the video preview to %d", pos);
 
+  self->priv->preview_pos = pos;
+
   switch (pos)
     {
       case PREVIEW_POS_TOP_LEFT:
@@ -710,6 +811,8 @@ empathy_call_window_move_video_preview (EmpathyCallWindow *self,
       default:
         g_warn_if_reached ();
     }
+
+  g_settings_set_enum (self->priv->settings, "camera-position", pos);
 }
 
 static void
@@ -720,37 +823,48 @@ empathy_call_window_highlight_preview_rectangle (EmpathyCallWindow *self,
 
   rectangle = empathy_call_window_get_preview_rectangle (self, pos);
 
-  clutter_rectangle_set_border_width (CLUTTER_RECTANGLE (rectangle), 3);
-  clutter_rectangle_set_border_color (CLUTTER_RECTANGLE (rectangle),
-      CLUTTER_COLOR_Red);
+  empathy_rounded_rectangle_set_border_width (
+      EMPATHY_ROUNDED_RECTANGLE (rectangle), 5);
+  empathy_rounded_rectangle_set_border_color (
+      EMPATHY_ROUNDED_RECTANGLE (rectangle), CLUTTER_COLOR_Red);
+}
+
+static void
+empathy_call_window_darken_preview_rectangle (EmpathyCallWindow *self,
+    ClutterActor *rectangle)
+{
+  empathy_rounded_rectangle_set_border_width (
+      EMPATHY_ROUNDED_RECTANGLE (rectangle), 1);
+  empathy_rounded_rectangle_set_border_color (
+      EMPATHY_ROUNDED_RECTANGLE (rectangle), CLUTTER_COLOR_Black);
 }
 
 static void
 empathy_call_window_darken_preview_rectangles (EmpathyCallWindow *self)
 {
-  clutter_rectangle_set_border_width (
-      CLUTTER_RECTANGLE (self->priv->preview_rectangle1), 1);
-  clutter_rectangle_set_border_color (
-      CLUTTER_RECTANGLE (self->priv->preview_rectangle1),
-      CLUTTER_COLOR_Black);
+  ClutterActor *rectangle;
+
+  rectangle = empathy_call_window_get_preview_rectangle (self,
+      self->priv->preview_pos);
+
+  /* We don't want to darken the rectangle where the preview
+   * currently is. */
 
-  clutter_rectangle_set_border_width (
-      CLUTTER_RECTANGLE (self->priv->preview_rectangle2), 1);
-  clutter_rectangle_set_border_color (
-      CLUTTER_RECTANGLE (self->priv->preview_rectangle2),
-      CLUTTER_COLOR_Black);
+  if (self->priv->preview_rectangle1 != rectangle)
+    empathy_call_window_darken_preview_rectangle (self,
+        self->priv->preview_rectangle1);
 
-  clutter_rectangle_set_border_width (
-      CLUTTER_RECTANGLE (self->priv->preview_rectangle3), 1);
-  clutter_rectangle_set_border_color (
-      CLUTTER_RECTANGLE (self->priv->preview_rectangle3),
-      CLUTTER_COLOR_Black);
+  if (self->priv->preview_rectangle2 != rectangle)
+    empathy_call_window_darken_preview_rectangle (self,
+        self->priv->preview_rectangle2);
 
-  clutter_rectangle_set_border_width (
-      CLUTTER_RECTANGLE (self->priv->preview_rectangle4), 1);
-  clutter_rectangle_set_border_color (
-      CLUTTER_RECTANGLE (self->priv->preview_rectangle4),
-      CLUTTER_COLOR_Black);
+  if (self->priv->preview_rectangle3 != rectangle)
+    empathy_call_window_darken_preview_rectangle (self,
+        self->priv->preview_rectangle3);
+
+  if (self->priv->preview_rectangle4 != rectangle)
+    empathy_call_window_darken_preview_rectangle (self,
+        self->priv->preview_rectangle4);
 }
 
 static void
@@ -761,10 +875,28 @@ empathy_call_window_preview_on_drag_begin_cb (ClutterDragAction *action,
     ClutterModifierType modifiers,
     EmpathyCallWindow *self)
 {
-  empathy_call_window_show_preview_rectangles (self, TRUE);
+  ClutterActor *stage = clutter_actor_get_stage (actor);
+  gfloat rel_x, rel_y;
+
+  self->priv->drag_preview = clutter_clone_new (actor);
+
+  clutter_container_add_actor (CLUTTER_CONTAINER (stage),
+      self->priv->drag_preview);
+
+  clutter_actor_transform_stage_point (actor, event_x, event_y,
+      &rel_x, &rel_y);
+
+  clutter_actor_set_position (self->priv->drag_preview,
+      event_x - rel_x, event_y - rel_y);
+
+  clutter_drag_action_set_drag_handle (action,
+      self->priv->drag_preview);
+
+  clutter_actor_set_opacity (actor, 0);
+  clutter_actor_hide (self->priv->preview_shown_button);
 
-  self->priv->event_x = event_x;
-  self->priv->event_y = event_y;
+  empathy_call_window_show_preview_rectangles (self, TRUE);
+  empathy_call_window_darken_preview_rectangles (self);
 }
 
 static void
@@ -777,8 +909,18 @@ empathy_call_window_preview_on_drag_end_cb (ClutterDragAction *action,
 {
   PreviewPosition pos;
 
+  /* Get the position before destroying the drag actor, otherwise the
+   * preview_box allocation won't be valid and we won't be able to
+   * calculate the position. */
   pos = empathy_call_window_get_preview_position (self, event_x, event_y);
 
+  /* Destroy the video preview copy that we were dragging */
+  clutter_actor_destroy (self->priv->drag_preview);
+  self->priv->drag_preview = NULL;
+
+  clutter_actor_set_opacity (actor, 255);
+  clutter_actor_show (self->priv->preview_shown_button);
+
   if (pos != PREVIEW_POS_NONE)
     empathy_call_window_move_video_preview (self, pos);
 
@@ -793,9 +935,11 @@ empathy_call_window_preview_on_drag_motion_cb (ClutterDragAction *action,
     EmpathyCallWindow *self)
 {
   PreviewPosition pos;
+  gfloat event_x, event_y;
 
-  pos = empathy_call_window_get_preview_position (self,
-      self->priv->event_x - delta_x, self->priv->event_y + delta_y);
+  clutter_drag_action_get_motion_coords (action, &event_x, &event_y);
+
+  pos = empathy_call_window_get_preview_position (self, event_x, event_y);
 
   if (pos != PREVIEW_POS_NONE)
     empathy_call_window_highlight_preview_rectangle (self, pos);
@@ -803,47 +947,81 @@ empathy_call_window_preview_on_drag_motion_cb (ClutterDragAction *action,
     empathy_call_window_darken_preview_rectangles (self);
 }
 
+static gboolean
+empathy_call_window_preview_enter_event_cb (ClutterActor *actor,
+    ClutterCrossingEvent *event,
+    EmpathyCallWindow *self)
+{
+  ClutterActor *rectangle;
+
+  rectangle = empathy_call_window_get_preview_rectangle (self,
+      self->priv->preview_pos);
+
+  empathy_call_window_highlight_preview_rectangle (self,
+      self->priv->preview_pos);
+
+  clutter_actor_show (rectangle);
+
+  return FALSE;
+}
+
+static gboolean
+empathy_call_window_preview_leave_event_cb (ClutterActor *actor,
+    ClutterCrossingEvent *event,
+    EmpathyCallWindow *self)
+{
+  ClutterActor *rectangle;
+
+  rectangle = empathy_call_window_get_preview_rectangle (self,
+      self->priv->preview_pos);
+
+  empathy_call_window_darken_preview_rectangle (self, rectangle);
+
+  clutter_actor_hide (rectangle);
+
+  return FALSE;
+}
+
 static void
 create_video_preview (EmpathyCallWindow *self)
 {
   EmpathyCallWindowPriv *priv = GET_PRIV (self);
-  ClutterLayoutManager *layout, *layout_center;
+  ClutterLayoutManager *layout, *layout_center, *layout_end;
   ClutterActor *preview;
   ClutterActor *box;
   ClutterActor *b;
   ClutterAction *action;
   GtkWidget *button;
+  PreviewPosition pos;
 
   g_assert (priv->video_preview == NULL);
 
-  preview = clutter_texture_new ();
+  pos = g_settings_get_enum (priv->settings, "camera-position");
+
+  preview = empathy_rounded_texture_new ();
   clutter_actor_set_size (preview,
-      SELF_VIDEO_SECTION_WIDTH, SELF_VIDEO_SECTION_HEIGTH);
+      SELF_VIDEO_SECTION_WIDTH, SELF_VIDEO_SECTION_HEIGHT);
   priv->video_preview_sink = clutter_gst_video_sink_new (
       CLUTTER_TEXTURE (preview));
 
-  /* Flip the video preview */
-  clutter_actor_set_rotation (preview,
-      CLUTTER_Y_AXIS,
-      180,
-      SELF_VIDEO_SECTION_WIDTH * 0.5,
-      0.0,
-      0.0);
-
   /* Add a little offset to the video preview */
   layout = clutter_bin_layout_new (CLUTTER_BIN_ALIGNMENT_CENTER,
-      CLUTTER_BIN_ALIGNMENT_CENTER);
+      CLUTTER_BIN_ALIGNMENT_START);
   priv->video_preview = clutter_box_new (layout);
   clutter_actor_set_size (priv->video_preview,
       SELF_VIDEO_SECTION_WIDTH + 2 * SELF_VIDEO_SECTION_MARGIN,
-      SELF_VIDEO_SECTION_HEIGTH + 2 * SELF_VIDEO_SECTION_MARGIN +
+      SELF_VIDEO_SECTION_HEIGHT + 2 * SELF_VIDEO_SECTION_MARGIN +
       FLOATING_TOOLBAR_HEIGHT + FLOATING_TOOLBAR_SPACING);
 
+  /* We have a box with the margins and the video in the middle inside
+   * a bigger box with an extra bottom margin so we're not on top of
+   * the floating toolbar. */
   layout_center = clutter_bin_layout_new (CLUTTER_BIN_ALIGNMENT_CENTER,
       CLUTTER_BIN_ALIGNMENT_CENTER);
   box = clutter_box_new (layout_center);
   clutter_actor_set_size (box,
-      SELF_VIDEO_SECTION_WIDTH, SELF_VIDEO_SECTION_HEIGTH);
+      SELF_VIDEO_SECTION_WIDTH + 2 * SELF_VIDEO_SECTION_MARGIN,
+      SELF_VIDEO_SECTION_HEIGHT + 2 * SELF_VIDEO_SECTION_MARGIN);
 
   clutter_container_add_actor (CLUTTER_CONTAINER (box), preview);
   clutter_container_add_actor (CLUTTER_CONTAINER (priv->video_preview), box);
@@ -856,12 +1034,21 @@ create_video_preview (EmpathyCallWindow *self)
   /* Translators: this is an "Info" label. It should be as short
    * as possible. */
   button = gtk_button_new_with_label (_("i"));
-  b = gtk_clutter_actor_new_with_contents (button);
+  priv->preview_shown_button = b = empathy_rounded_actor_new ();
+  gtk_container_add (
+      GTK_CONTAINER (gtk_clutter_actor_get_widget (GTK_CLUTTER_ACTOR (b))),
+      button);
+  clutter_actor_set_size (b, 24, 24);
 
-  clutter_bin_layout_add (CLUTTER_BIN_LAYOUT (layout_center),
-      b,
-      CLUTTER_BIN_ALIGNMENT_END,
+  layout_end = clutter_bin_layout_new (CLUTTER_BIN_ALIGNMENT_END,
       CLUTTER_BIN_ALIGNMENT_END);
+  box = clutter_box_new (layout_end);
+  clutter_actor_set_size (box,
+      SELF_VIDEO_SECTION_WIDTH,
+      SELF_VIDEO_SECTION_HEIGHT + SELF_VIDEO_SECTION_MARGIN);
+
+  clutter_container_add_actor (CLUTTER_CONTAINER (box), b);
+  clutter_container_add_actor (CLUTTER_CONTAINER (priv->video_preview), box);
 
   g_signal_connect (button, "clicked",
       G_CALLBACK (empathy_call_window_preview_button_clicked_cb),
@@ -870,14 +1057,20 @@ create_video_preview (EmpathyCallWindow *self)
   /* Translators: this is an "Info" label. It should be as short
    * as possible. */
   button = gtk_button_new_with_label (_("i"));
-  priv->preview_hidden_button =
-      gtk_clutter_actor_new_with_contents (button);
+  b = empathy_rounded_actor_new ();
+  gtk_container_add (
+      GTK_CONTAINER (gtk_clutter_actor_get_widget (GTK_CLUTTER_ACTOR (b))),
+      button);
+  clutter_actor_set_size (b, 24, 24);
+  priv->preview_hidden_button = b;
 
   clutter_bin_layout_add (CLUTTER_BIN_LAYOUT (priv->video_layout),
       priv->preview_hidden_button,
       CLUTTER_BIN_ALIGNMENT_START,
       CLUTTER_BIN_ALIGNMENT_END);
 
+  self->priv->preview_pos = PREVIEW_POS_BOTTOM_LEFT;
+
   clutter_actor_hide (priv->preview_hidden_button);
 
   g_signal_connect (button, "clicked",
@@ -889,6 +1082,8 @@ create_video_preview (EmpathyCallWindow *self)
       CLUTTER_BIN_ALIGNMENT_START,
       CLUTTER_BIN_ALIGNMENT_END);
 
+  empathy_call_window_move_video_preview (self, pos);
+
   action = clutter_drag_action_new ();
   g_signal_connect (action, "drag-begin",
       G_CALLBACK (empathy_call_window_preview_on_drag_begin_cb), self);
@@ -897,12 +1092,18 @@ create_video_preview (EmpathyCallWindow *self)
   g_signal_connect (action, "drag-motion",
       G_CALLBACK (empathy_call_window_preview_on_drag_motion_cb), self);
 
+  g_signal_connect (preview, "enter-event",
+      G_CALLBACK (empathy_call_window_preview_enter_event_cb), self);
+  g_signal_connect (preview, "leave-event",
+      G_CALLBACK (empathy_call_window_preview_leave_event_cb), self);
+
   clutter_actor_add_action (preview, action);
   clutter_actor_set_reactive (preview, TRUE);
+  clutter_actor_set_reactive (priv->preview_shown_button, TRUE);
 }
 
-static void
-play_camera (EmpathyCallWindow *window,
+void
+empathy_call_window_play_camera (EmpathyCallWindow *window,
     gboolean play)
 {
   EmpathyCallWindowPriv *priv = GET_PRIV (window);
@@ -938,7 +1139,7 @@ display_video_preview (EmpathyCallWindow *self,
       /* Display the video preview */
       DEBUG ("Show video preview");
 
-      play_camera (self, TRUE);
+      empathy_call_window_play_camera (self, TRUE);
       clutter_actor_show (priv->video_preview);
       clutter_actor_raise_top (priv->floating_toolbar);
     }
@@ -950,7 +1151,7 @@ display_video_preview (EmpathyCallWindow *self,
       if (priv->video_preview != NULL)
         {
           clutter_actor_hide (priv->video_preview);
-          play_camera (self, FALSE);
+          empathy_call_window_play_camera (self, FALSE);
         }
     }
 }
@@ -963,6 +1164,9 @@ empathy_call_window_set_state_connecting (EmpathyCallWindow *window)
   empathy_call_window_status_message (window, _("Connecting…"));
   priv->call_state = CONNECTING;
 
+  /* Show the toolbar */
+  clutter_state_set_state (priv->transitions, "fade-in");
+
   if (priv->outgoing)
     empathy_sound_manager_start_playing (priv->sound_mgr, GTK_WIDGET (window),
         EMPATHY_SOUND_PHONE_OUTGOING, MS_BETWEEN_RING);
@@ -1079,7 +1283,11 @@ empathy_call_window_toolbar_timeout (gpointer data)
 {
   EmpathyCallWindow *self = data;
 
-  clutter_state_set_state (self->priv->transitions, "fade-out");
+  /* We don't want to hide the toolbar if we're not in a call, as
+   * to show the call status all the time. */
+  if (self->priv->call_state != CONNECTING &&
+      self->priv->call_state != DISCONNECTED)
+    clutter_state_set_state (self->priv->transitions, "fade-out");
 
   return TRUE;
 }
@@ -1183,6 +1391,7 @@ empathy_call_window_init (EmpathyCallWindow *self)
     "bottom_toolbar", &priv->bottom_toolbar,
     "ui_manager", &priv->ui_manager,
     "menufullscreen", &priv->menu_fullscreen,
+    "menupreviewswap", &priv->menu_swap_camera,
     "details_vbox",  &priv->details_vbox,
     "vcodec_encoding_label", &priv->vcodec_encoding_label,
     "acodec_encoding_label", &priv->acodec_encoding_label,
@@ -1215,6 +1424,7 @@ empathy_call_window_init (EmpathyCallWindow *self)
     "menupreviewdisable", "activate", empathy_call_window_disable_camera_cb,
     "menupreviewminimise", "activate", empathy_call_window_minimise_camera_cb,
     "menupreviewmaximise", "activate", empathy_call_window_maximise_camera_cb,
+    "menupreviewswap", "activate", empathy_call_window_swap_camera_cb,
     NULL);
 
   gtk_action_set_sensitive (priv->menu_fullscreen, FALSE);
@@ -1225,6 +1435,11 @@ empathy_call_window_init (EmpathyCallWindow *self)
       priv->camera_button, "sensitive",
       G_BINDING_SYNC_CREATE);
 
+  g_signal_connect (priv->camera_monitor, "added",
+      G_CALLBACK (empathy_call_window_camera_added_cb), self);
+  g_signal_connect (priv->camera_monitor, "removed",
+      G_CALLBACK (empathy_call_window_camera_removed_cb), self);
+
   priv->lock = g_mutex_new ();
 
   gtk_container_add (GTK_CONTAINER (self), top_vbox);
@@ -1241,8 +1456,6 @@ empathy_call_window_init (EmpathyCallWindow *self)
 
   priv->video_box = clutter_box_new (priv->video_layout);
 
-  empathy_call_window_create_preview_rectangles (self);
-
   priv->video_container = gtk_clutter_embed_new ();
 
   /* Set the background color to that of the rest of the window */
@@ -1276,6 +1489,8 @@ empathy_call_window_init (EmpathyCallWindow *self)
   clutter_container_add_actor (CLUTTER_CONTAINER (priv->video_box),
       remote_avatar);
 
+  empathy_call_window_create_preview_rectangles (self);
+
   gtk_box_pack_start (GTK_BOX (priv->content_hbox),
       priv->video_container, TRUE, TRUE,
       CONTENT_HBOX_CHILDREN_PACKING_PADDING);
@@ -1342,7 +1557,7 @@ empathy_call_window_init (EmpathyCallWindow *self)
       G_CALLBACK (dtmf_button_released_cb));
 
   gtk_box_pack_start (GTK_BOX (priv->pane), priv->dtmf_panel,
-      FALSE, FALSE, 0);
+      FALSE, FALSE, 6);
 
   gtk_box_pack_start (GTK_BOX (priv->pane), priv->details_vbox,
       FALSE, FALSE, 0);
@@ -1389,6 +1604,7 @@ empathy_call_window_init (EmpathyCallWindow *self)
 
   priv->sound_mgr = empathy_sound_manager_dup_singleton ();
   priv->mic_menu = empathy_mic_menu_new (self);
+  priv->camera_menu = empathy_camera_menu_new (self);
 
   empathy_call_window_show_hangup_button (self, TRUE);
 
@@ -1777,17 +1993,24 @@ empathy_call_window_constructed (GObject *object)
   EmpathyCallWindow *self = EMPATHY_CALL_WINDOW (object);
   EmpathyCallWindowPriv *priv = GET_PRIV (self);
   TpyCallChannel *call;
+  TpyCallState state;
 
   g_assert (priv->handler != NULL);
 
   g_object_get (priv->handler, "call-channel", &call, NULL);
-  priv->outgoing = (call == NULL);
-  if (call != NULL)
-    g_object_unref (call);
+  state = tpy_call_channel_get_state (call, NULL, NULL);
+  priv->outgoing = (state == TPY_CALL_STATE_PENDING_INITIATOR);
+  tp_clear_object (&call);
 
   g_object_get (priv->handler, "target-contact", &priv->contact, NULL);
   g_assert (priv->contact != NULL);
 
+  if (!empathy_contact_can_voip_video (priv->contact))
+    {
+      gtk_widget_set_sensitive (priv->video_call_button, FALSE);
+      gtk_widget_set_sensitive (priv->camera_button, FALSE);
+    }
+
   empathy_call_window_setup_avatars (self, priv->handler);
   empathy_call_window_set_state_connecting (self);
 
@@ -1920,7 +2143,9 @@ empathy_call_window_dispose (GObject *object)
   tp_clear_object (&priv->fullscreen);
   tp_clear_object (&priv->camera_monitor);
   tp_clear_object (&priv->settings);
-  tp_clear_object (&priv->transitions);
+  tp_clear_object (&priv->sound_mgr);
+  tp_clear_object (&priv->mic_menu);
+  tp_clear_object (&priv->camera_menu);
 
   g_list_free_full (priv->notifiers, g_object_unref);
 
@@ -1935,11 +2160,6 @@ empathy_call_window_dispose (GObject *object)
       priv->contact = NULL;
     }
 
-
-  tp_clear_object (&priv->sound_mgr);
-
-  tp_clear_object (&priv->mic_menu);
-
   G_OBJECT_CLASS (empathy_call_window_parent_class)->dispose (object);
 }
 
@@ -2053,6 +2273,15 @@ empathy_call_window_reset_pipeline (EmpathyCallWindow *self)
         clutter_actor_destroy (priv->video_preview);
       priv->video_preview = NULL;
 
+      /* If we destroy the preview while it's being dragged, we won't
+       * get the ::drag-end signal, so manually destroy the clone */
+      if (priv->drag_preview != NULL)
+        {
+          clutter_actor_destroy (priv->drag_preview);
+          empathy_call_window_show_preview_rectangles (self, FALSE);
+          priv->drag_preview = NULL;
+        }
+
       priv->funnel = NULL;
 
       create_pipeline (self);
@@ -2104,6 +2333,9 @@ empathy_call_window_disconnected (EmpathyCallWindow *self,
   if (priv->call_state != REDIALING)
     priv->call_state = DISCONNECTED;
 
+  /* Show the toolbar */
+  clutter_state_set_state (priv->transitions, "fade-in");
+
   if (could_reset_pipeline)
     {
       g_mutex_lock (priv->lock);
@@ -2644,7 +2876,7 @@ empathy_call_window_show_video_output_cb (gpointer user_data)
     {
       gtk_widget_hide (self->priv->remote_user_avatar_widget);
       clutter_actor_show (self->priv->video_output);
-      clutter_actor_raise_top (self->priv->floating_toolbar);
+      empathy_call_window_raise_actors (self);
     }
 
   return FALSE;
@@ -3465,3 +3697,9 @@ empathy_call_window_get_audio_src (EmpathyCallWindow *window)
 
   return (EmpathyGstAudioSrc *) priv->audio_input;
 }
+
+EmpathyGstVideoSrc *
+empathy_call_window_get_video_src (EmpathyCallWindow *self)
+{
+  return EMPATHY_GST_VIDEO_SRC (self->priv->video_input);
+}