X-Git-Url: https://git.0d.be/?p=empathy.git;a=blobdiff_plain;f=src%2Fempathy-call-window.c;h=2a92e947168b3d395cd17c253ed1de264d8d5039;hp=bcbadc7e0b29de73f1bc1ebd027376daa978c7e4;hb=7b6b8da406493311445f6c2470a005a542972693;hpb=d89a90c582166d20b9a5c165d5784e434fc1de7f diff --git a/src/empathy-call-window.c b/src/empathy-call-window.c index bcbadc7e..2a92e947 100644 --- a/src/empathy-call-window.c +++ b/src/empathy-call-window.c @@ -43,6 +43,7 @@ #include #include #include +#include #include #include @@ -63,13 +64,24 @@ #include "empathy-audio-sink.h" #include "empathy-video-src.h" #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 +#define FLOATING_TOOLBAR_WIDTH 280 +#define FLOATING_TOOLBAR_HEIGHT 36 +#define FLOATING_TOOLBAR_SPACING 20 /* The avatar's default width and height are set to the same value because we want a square icon. */ @@ -92,11 +104,12 @@ enum { }; typedef enum { - CONNECTING, - CONNECTED, - HELD, - DISCONNECTED, - REDIALING + RINGING, /* Incoming call */ + CONNECTING, /* Outgoing call */ + CONNECTED, /* Connected */ + HELD, /* Connected, but on hold */ + DISCONNECTED, /* Disconnected */ + REDIALING /* Redialing (special case of CONNECTING) */ } CallState; typedef enum { @@ -104,6 +117,14 @@ typedef enum { CAMERA_STATE_ON, } CameraState; +typedef enum { + PREVIEW_POS_NONE, + PREVIEW_POS_TOP_LEFT, + PREVIEW_POS_TOP_RIGHT, + PREVIEW_POS_BOTTOM_LEFT, + PREVIEW_POS_BOTTOM_RIGHT, +} PreviewPosition; + struct _EmpathyCallWindowPriv { gboolean dispose_has_run; @@ -122,7 +143,19 @@ 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; + ClutterActor *preview_spinner_actor; + GtkWidget *preview_spinner_widget; GtkWidget *video_container; GtkWidget *remote_user_avatar_widget; GtkWidget *remote_user_avatar_toolbar; @@ -136,23 +169,41 @@ struct _EmpathyCallWindowPriv GtkWidget *dialpad_button; GtkWidget *toolbar; GtkWidget *bottom_toolbar; + ClutterActor *floating_toolbar; GtkWidget *pane; GtkAction *menu_fullscreen; + GtkAction *menu_swap_camera; + + ClutterState *transitions; /* The box that contains self and remote avatar and video input/output. When we redial, we destroy and re-create the box */ ClutterActor *video_box; ClutterLayoutManager *video_layout; + /* Coordinates of the preview drag event's start. */ + PreviewPosition preview_pos; + /* We keep a reference on the hbox which contains the main content so we can easilly repack everything when toggling fullscreen */ GtkWidget *content_hbox; + /* These are used to accept or reject an incoming call when the status + is RINGING. */ + GtkWidget *incoming_call_dialog; + TpyCallChannel *pending_channel; + TpChannelDispatchOperation *pending_cdo; + TpAddDispatchOperationContext *pending_context; + gulong video_output_motion_handler_id; guint bus_message_source_id; gdouble volume; + /* String that contains the queued tones to send after the current ones + are sent */ + GString *tones; + gboolean sending_tones; GtkWidget *dtmf_panel; /* Details vbox */ @@ -197,6 +248,8 @@ struct _EmpathyCallWindowPriv gboolean got_video; guint got_video_src; + guint inactivity_src; + /* Those fields represent the state of the window before it actually was in fullscreen mode. */ gboolean dialpad_was_visible_before_fs; @@ -215,6 +268,7 @@ struct _EmpathyCallWindowPriv GSettings *settings; EmpathyMicMenu *mic_menu; + EmpathyCameraMenu *camera_menu; }; #define GET_PRIV(o) (EMPATHY_CALL_WINDOW (o)->priv) @@ -254,6 +308,8 @@ static gboolean empathy_call_window_video_output_motion_notify ( static void empathy_call_window_video_menu_popup (EmpathyCallWindow *window, guint button); +static void empathy_call_window_connect_handler (EmpathyCallWindow *self); + static void empathy_call_window_dialpad_cb (GtkToggleToolButton *button, EmpathyCallWindow *window); @@ -296,35 +352,72 @@ empathy_call_window_video_call_cb (GtkToggleToolButton *button, } static void -dtmf_button_pressed_cb (GtkButton *button, EmpathyCallWindow *window) +empathy_call_window_emit_tones (EmpathyCallWindow *self) { - EmpathyCallWindowPriv *priv = GET_PRIV (window); - TpyCallChannel *call; + TpChannel *channel; + + if (tp_str_empty (self->priv->tones->str)) + return; + + g_object_get (self->priv->handler, "call-channel", &channel, NULL); + + DEBUG ("Emitting multiple tones: %s", self->priv->tones->str); + + tp_cli_channel_interface_dtmf_call_multiple_tones (channel, -1, + self->priv->tones->str, + NULL, NULL, NULL, NULL); + + self->priv->sending_tones = TRUE; + + g_string_set_size (self->priv->tones, 0); + + g_object_unref (channel); +} + +static void +empathy_call_window_maybe_emit_tones (EmpathyCallWindow *self) +{ + if (self->priv->sending_tones) + return; + + empathy_call_window_emit_tones (self); +} + +static void +empathy_call_window_tones_stopped_cb (TpChannel *proxy, + gboolean arg_cancelled, + gpointer user_data, + GObject *weak_object) +{ + EmpathyCallWindow *self = EMPATHY_CALL_WINDOW (user_data); + + self->priv->sending_tones = FALSE; + + empathy_call_window_emit_tones (self); +} + +static void +dtmf_button_pressed_cb (GtkButton *button, + EmpathyCallWindow *self) +{ + EmpathyCallWindowPriv *priv = GET_PRIV (self); GQuark button_quark; TpDTMFEvent event; - g_object_get (priv->handler, "call-channel", &call, NULL); - button_quark = g_quark_from_static_string (EMPATHY_DTMF_BUTTON_ID); event = GPOINTER_TO_UINT (g_object_get_qdata (G_OBJECT (button), button_quark)); - tpy_call_channel_dtmf_start_tone (call, event); + g_string_append_c (priv->tones, tp_dtmf_event_to_char (event)); - g_object_unref (call); + empathy_call_window_maybe_emit_tones (self); } +/* empathy_create_dtmf_dialpad() requires a callback, even if empty */ static void -dtmf_button_released_cb (GtkButton *button, EmpathyCallWindow *window) +dtmf_button_released_cb (GtkButton *button, + EmpathyCallWindow *self) { - EmpathyCallWindowPriv *priv = GET_PRIV (window); - TpyCallChannel *call; - - g_object_get (priv->handler, "call-channel", &call, NULL); - - tpy_call_channel_dtmf_stop_tone (call); - - g_object_unref (call); } static void @@ -360,6 +453,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) @@ -368,6 +472,8 @@ empathy_call_window_show_video_output (EmpathyCallWindow *self, g_object_set (self->priv->video_output, "visible", show, NULL); gtk_widget_set_visible (self->priv->remote_user_avatar_widget, !show); + + empathy_call_window_raise_actors (self); } static void @@ -481,6 +587,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) @@ -507,46 +674,503 @@ empathy_call_window_preview_hidden_button_clicked_cb (GtkButton *button, gtk_menu_shell_select_first (GTK_MENU_SHELL (menu), FALSE); } +static ClutterActor * +empathy_call_window_create_preview_rectangle (EmpathyCallWindow *self, + ClutterActor **box, + ClutterBinAlignment x, + ClutterBinAlignment y) +{ + ClutterLayoutManager *layout1, *layout2; + ClutterActor *rectangle; + ClutterActor *box1, *box2; + + layout1 = clutter_bin_layout_new (CLUTTER_BIN_ALIGNMENT_CENTER, + CLUTTER_BIN_ALIGNMENT_START); + + box1 = clutter_box_new (layout1); + + *box = box1; + + rectangle = empathy_rounded_rectangle_new ( + SELF_VIDEO_SECTION_WIDTH + 5, + SELF_VIDEO_SECTION_HEIGHT + 5); + + 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); + + /* 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_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), + box1, x, y); + + clutter_actor_hide (rectangle); + + return rectangle; +} + +static void +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); +} + +static void +empathy_call_window_show_preview_rectangles (EmpathyCallWindow *self, + gboolean show) +{ + g_object_set (self->priv->preview_rectangle1, "visible", show, NULL); + g_object_set (self->priv->preview_rectangle2, "visible", show, NULL); + g_object_set (self->priv->preview_rectangle3, "visible", show, NULL); + g_object_set (self->priv->preview_rectangle4, "visible", show, NULL); +} + +static void +empathy_call_window_get_preview_coordinates (EmpathyCallWindow *self, + PreviewPosition pos, + guint *x, + guint *y) +{ + guint ret_x = 0, ret_y = 0; + ClutterGeometry box; + + if (!clutter_actor_has_allocation (self->priv->video_box)) + goto out; + + clutter_actor_get_geometry (self->priv->video_box, &box); + + switch (pos) + { + case PREVIEW_POS_TOP_LEFT: + ret_x = ret_y = SELF_VIDEO_SECTION_MARGIN; + break; + case PREVIEW_POS_TOP_RIGHT: + ret_x = box.width - SELF_VIDEO_SECTION_MARGIN + - SELF_VIDEO_SECTION_WIDTH; + ret_y = SELF_VIDEO_SECTION_MARGIN; + break; + case PREVIEW_POS_BOTTOM_LEFT: + ret_x = SELF_VIDEO_SECTION_MARGIN; + ret_y = box.height - SELF_VIDEO_SECTION_MARGIN + - SELF_VIDEO_SECTION_HEIGHT + - FLOATING_TOOLBAR_HEIGHT - FLOATING_TOOLBAR_SPACING; + break; + case PREVIEW_POS_BOTTOM_RIGHT: + ret_x = box.width - SELF_VIDEO_SECTION_MARGIN + - SELF_VIDEO_SECTION_WIDTH; + ret_y = box.height - SELF_VIDEO_SECTION_MARGIN + - SELF_VIDEO_SECTION_HEIGHT - FLOATING_TOOLBAR_HEIGHT + - FLOATING_TOOLBAR_SPACING; + break; + default: + g_warn_if_reached (); + } + +out: + if (x != NULL) + *x = ret_x; + + if (y != NULL) + *y = ret_y; +} + +static PreviewPosition +empathy_call_window_get_preview_position (EmpathyCallWindow *self, + gfloat event_x, + gfloat event_y) +{ + 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_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_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_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_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; + } + + return pos; +} + +static ClutterActor * +empathy_call_window_get_preview_rectangle (EmpathyCallWindow *self, + PreviewPosition pos) +{ + ClutterActor *rectangle; + + switch (pos) + { + case PREVIEW_POS_TOP_LEFT: + rectangle = self->priv->preview_rectangle1; + break; + case PREVIEW_POS_TOP_RIGHT: + rectangle = self->priv->preview_rectangle3; + break; + case PREVIEW_POS_BOTTOM_LEFT: + rectangle = self->priv->preview_rectangle2; + break; + case PREVIEW_POS_BOTTOM_RIGHT: + rectangle = self->priv->preview_rectangle4; + break; + default: + rectangle = NULL; + } + + return rectangle; +} + +static void +empathy_call_window_move_video_preview (EmpathyCallWindow *self, + PreviewPosition pos) +{ + ClutterBinLayout *layout = CLUTTER_BIN_LAYOUT (self->priv->video_layout); + + DEBUG ("moving the video preview to %d", pos); + + self->priv->preview_pos = pos; + + switch (pos) + { + case PREVIEW_POS_TOP_LEFT: + clutter_bin_layout_set_alignment (layout, + self->priv->video_preview, + CLUTTER_BIN_ALIGNMENT_START, + CLUTTER_BIN_ALIGNMENT_START); + break; + case PREVIEW_POS_TOP_RIGHT: + clutter_bin_layout_set_alignment (layout, + self->priv->video_preview, + CLUTTER_BIN_ALIGNMENT_END, + CLUTTER_BIN_ALIGNMENT_START); + break; + case PREVIEW_POS_BOTTOM_LEFT: + clutter_bin_layout_set_alignment (layout, + self->priv->video_preview, + CLUTTER_BIN_ALIGNMENT_START, + CLUTTER_BIN_ALIGNMENT_END); + break; + case PREVIEW_POS_BOTTOM_RIGHT: + clutter_bin_layout_set_alignment (layout, + self->priv->video_preview, + CLUTTER_BIN_ALIGNMENT_END, + CLUTTER_BIN_ALIGNMENT_END); + break; + default: + g_warn_if_reached (); + } + + g_settings_set_enum (self->priv->settings, "camera-position", pos); +} + +static void +empathy_call_window_highlight_preview_rectangle (EmpathyCallWindow *self, + PreviewPosition pos) +{ + ClutterActor *rectangle; + + rectangle = empathy_call_window_get_preview_rectangle (self, pos); + + 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) +{ + 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. */ + + if (self->priv->preview_rectangle1 != rectangle) + empathy_call_window_darken_preview_rectangle (self, + self->priv->preview_rectangle1); + + if (self->priv->preview_rectangle2 != rectangle) + empathy_call_window_darken_preview_rectangle (self, + self->priv->preview_rectangle2); + + 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 +empathy_call_window_preview_on_drag_begin_cb (ClutterDragAction *action, + ClutterActor *actor, + gfloat event_x, + gfloat event_y, + ClutterModifierType modifiers, + EmpathyCallWindow *self) +{ + 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); + + empathy_call_window_show_preview_rectangles (self, TRUE); + empathy_call_window_darken_preview_rectangles (self); +} + +static void +empathy_call_window_on_animation_completed_cb (ClutterAnimation *animation, + ClutterActor *actor) +{ + clutter_actor_set_opacity (actor, 255); +} + +static void +empathy_call_window_preview_on_drag_end_cb (ClutterDragAction *action, + ClutterActor *actor, + gfloat event_x, + gfloat event_y, + ClutterModifierType modifiers, + EmpathyCallWindow *self) +{ + PreviewPosition pos; + guint x, y; + + /* 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); + + empathy_call_window_get_preview_coordinates (self, + pos != PREVIEW_POS_NONE ? pos : self->priv->preview_pos, + &x, &y); + + /* Move the preview to the destination and destroy it afterwards */ + clutter_actor_animate (self->priv->drag_preview, CLUTTER_LINEAR, 500, + "x", (gfloat) x, + "y", (gfloat) y, + "signal-swapped-after::completed", + clutter_actor_destroy, self->priv->drag_preview, + "signal-swapped-after::completed", + clutter_actor_show, self->priv->preview_shown_button, + "signal::completed", + empathy_call_window_on_animation_completed_cb, actor, + NULL); + + self->priv->drag_preview = NULL; + + if (pos != PREVIEW_POS_NONE) + empathy_call_window_move_video_preview (self, pos); + + empathy_call_window_show_preview_rectangles (self, FALSE); +} + +static void +empathy_call_window_preview_on_drag_motion_cb (ClutterDragAction *action, + ClutterActor *actor, + gfloat delta_x, + gfloat delta_y, + EmpathyCallWindow *self) +{ + PreviewPosition pos; + gfloat event_x, event_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); + else + 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; + GdkRGBA transparent = { 0., 0., 0., 0. }; 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_END, + layout = clutter_bin_layout_new (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 + 10, SELF_VIDEO_SECTION_HEIGTH + 10); - + SELF_VIDEO_SECTION_WIDTH + 2 * SELF_VIDEO_SECTION_MARGIN, + SELF_VIDEO_SECTION_HEIGHT + 2 * SELF_VIDEO_SECTION_MARGIN + + FLOATING_TOOLBAR_HEIGHT + FLOATING_TOOLBAR_SPACING); + + /* Spinner for when changing the camera device */ + priv->preview_spinner_widget = gtk_spinner_new (); + priv->preview_spinner_actor = empathy_rounded_actor_new (); + empathy_rounded_actor_set_round_factor ( + EMPATHY_ROUNDED_ACTOR (priv->preview_spinner_actor), 16); + + g_object_set (priv->preview_spinner_widget, "expand", TRUE, NULL); + gtk_widget_override_background_color ( + gtk_clutter_actor_get_widget ( + GTK_CLUTTER_ACTOR (priv->preview_spinner_actor)), + GTK_STATE_FLAG_NORMAL, &transparent); + gtk_widget_show (priv->preview_spinner_widget); + + gtk_container_add ( + GTK_CONTAINER (gtk_clutter_actor_get_widget ( + GTK_CLUTTER_ACTOR (priv->preview_spinner_actor))), + priv->preview_spinner_widget); + clutter_actor_set_size (priv->preview_spinner_actor, + SELF_VIDEO_SECTION_WIDTH, SELF_VIDEO_SECTION_HEIGHT); + clutter_actor_set_opacity (priv->preview_spinner_actor, 128); + clutter_actor_hide (priv->preview_spinner_actor); + + /* 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 (box), + priv->preview_spinner_actor); clutter_container_add_actor (CLUTTER_CONTAINER (priv->video_preview), box); g_object_set (priv->video_preview_sink, @@ -557,12 +1181,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), @@ -571,14 +1204,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", @@ -589,26 +1228,64 @@ create_video_preview (EmpathyCallWindow *self) priv->video_preview, 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); + g_signal_connect (action, "drag-end", + G_CALLBACK (empathy_call_window_preview_on_drag_end_cb), 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, +empathy_call_window_start_camera_spinning (EmpathyCallWindow *self) +{ + clutter_actor_show (self->priv->preview_spinner_actor); + gtk_spinner_start (GTK_SPINNER (self->priv->preview_spinner_widget)); +} + +static void +empathy_call_window_stop_camera_spinning (EmpathyCallWindow *self) +{ + clutter_actor_hide (self->priv->preview_spinner_actor); + gtk_spinner_stop (GTK_SPINNER (self->priv->preview_spinner_widget)); +} + +void +empathy_call_window_play_camera (EmpathyCallWindow *self, gboolean play) { - EmpathyCallWindowPriv *priv = GET_PRIV (window); + EmpathyCallWindowPriv *priv = GET_PRIV (self); GstElement *preview; GstState state; if (priv->video_preview == NULL) { - create_video_preview (window); - add_video_preview_to_pipeline (window); + create_video_preview (self); + add_video_preview_to_pipeline (self); } if (play) - state = GST_STATE_PLAYING; + { + state = GST_STATE_PLAYING; + } else - state = GST_STATE_NULL; + { + empathy_call_window_start_camera_spinning (self); + state = GST_STATE_NULL; + } preview = priv->video_preview_sink; @@ -628,8 +1305,9 @@ 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); } else { @@ -639,7 +1317,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); } } } @@ -652,6 +1330,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); @@ -730,6 +1411,18 @@ create_pipeline (EmpathyCallWindow *self) g_object_unref (bus); } +static void +empathy_call_window_settings_cb (GtkAction *action, + EmpathyCallWindow *self) +{ + gchar *args = g_strdup_printf ("-p %s", + empathy_preferences_tab_to_string (EMPATHY_PREFERENCES_TAB_CALLS)); + + empathy_launch_program (BIN_DIR, "empathy", args); + + g_free (args); +} + static void empathy_call_window_contents_cb (GtkAction *action, EmpathyCallWindow *self) @@ -737,6 +1430,13 @@ empathy_call_window_contents_cb (GtkAction *action, empathy_url_show (GTK_WIDGET (self), "ghelp:empathy?audio-video"); } +static void +empathy_call_window_debug_cb (GtkAction *action, + EmpathyCallWindow *self) +{ + empathy_launch_program (BIN_DIR, "empathy-debugger", "-s Empathy.Call"); +} + static void empathy_call_window_about_cb (GtkAction *action, EmpathyCallWindow *self) @@ -744,6 +1444,36 @@ empathy_call_window_about_cb (GtkAction *action, empathy_about_dialog_new (GTK_WINDOW (self)); } +static gboolean +empathy_call_window_toolbar_timeout (gpointer data) +{ + EmpathyCallWindow *self = data; + + /* 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; +} + +static gboolean +empathy_call_window_motion_notify_cb (GtkWidget *widget, + GdkEvent *event, + EmpathyCallWindow *self) +{ + clutter_state_set_state (self->priv->transitions, "fade-in"); + + if (self->priv->inactivity_src > 0) + g_source_remove (self->priv->inactivity_src); + + self->priv->inactivity_src = g_timeout_add_seconds (3, + empathy_call_window_toolbar_timeout, self); + + return FALSE; +} + static gboolean empathy_call_window_configure_event_cb (GtkWidget *widget, GdkEvent *event, @@ -779,6 +1509,119 @@ empathy_call_window_destroyed_cb (GtkWidget *object, } } +static void +empathy_call_window_stage_allocation_changed_cb (ClutterActor *stage, + GParamSpec *pspec, + ClutterBindConstraint *constraint) +{ + ClutterActorBox allocation; + + clutter_actor_get_allocation_box (stage, &allocation); + + clutter_bind_constraint_set_offset (constraint, + allocation.y2 - allocation.y1 - + FLOATING_TOOLBAR_SPACING - FLOATING_TOOLBAR_HEIGHT); +} + +static void +empathy_call_window_incoming_call_response_cb (GtkDialog *dialog, + gint response_id, + EmpathyCallWindow *self) +{ + switch (response_id) + { + case GTK_RESPONSE_ACCEPT: + tp_channel_dispatch_operation_handle_with_async ( + self->priv->pending_cdo, EMPATHY_CALL_BUS_NAME, NULL, NULL); + + tp_clear_object (&self->priv->pending_cdo); + tp_clear_object (&self->priv->pending_channel); + tp_clear_object (&self->priv->pending_context); + + break; + case GTK_RESPONSE_CANCEL: + tp_channel_dispatch_operation_close_channels_async ( + self->priv->pending_cdo, NULL, NULL); + + empathy_call_window_status_message (self, _("Disconnected")); + self->priv->call_state = DISCONNECTED; + break; + default: + g_warn_if_reached (); + } +} + +static void +empathy_call_window_set_state_ringing (EmpathyCallWindow *self) +{ + gboolean video; + + g_assert (self->priv->call_state != CONNECTED); + + video = tpy_call_channel_has_initial_video (self->priv->pending_channel); + + empathy_call_window_status_message (self, _("Incoming call")); + self->priv->call_state = RINGING; + + self->priv->incoming_call_dialog = gtk_message_dialog_new ( + GTK_WINDOW (self), GTK_DIALOG_MODAL, + GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE, + video ? _("Incoming video call from %s") : _("Incoming call from %s"), + empathy_contact_get_alias (self->priv->contact)); + + gtk_dialog_add_buttons (GTK_DIALOG (self->priv->incoming_call_dialog), + _("Reject"), GTK_RESPONSE_CANCEL, + _("Answer"), GTK_RESPONSE_ACCEPT, + NULL); + + g_signal_connect (self->priv->incoming_call_dialog, "response", + G_CALLBACK (empathy_call_window_incoming_call_response_cb), self); + gtk_widget_show (self->priv->incoming_call_dialog); +} + +static void +empathy_call_window_cdo_invalidated_cb (TpProxy *channel, + guint domain, + gint code, + gchar *message, + EmpathyCallWindow *self) +{ + tp_clear_object (&self->priv->pending_cdo); + tp_clear_object (&self->priv->pending_channel); + tp_clear_object (&self->priv->pending_context); + + /* We don't know if the incoming call has been accepted or not, so we + * assume it hasn't and if it has, we'll set the proper status when + * we get the new handler. */ + empathy_call_window_status_message (self, _("Disconnected")); + self->priv->call_state = DISCONNECTED; + + gtk_widget_destroy (self->priv->incoming_call_dialog); + self->priv->incoming_call_dialog = NULL; +} + +void +empathy_call_window_start_ringing (EmpathyCallWindow *self, + TpyCallChannel *channel, + TpChannelDispatchOperation *dispatch_operation, + TpAddDispatchOperationContext *context) +{ + g_assert (self->priv->pending_channel == NULL); + g_assert (self->priv->pending_context == NULL); + g_assert (self->priv->pending_cdo == NULL); + + /* Start ringing and delay until the user answers or hangs. */ + self->priv->pending_channel = g_object_ref (channel); + self->priv->pending_context = g_object_ref (context); + self->priv->pending_cdo = g_object_ref (dispatch_operation); + + g_signal_connect (self->priv->pending_cdo, "invalidated", + G_CALLBACK (empathy_call_window_cdo_invalidated_cb), self); + + empathy_call_window_set_state_ringing (self); + tp_add_dispatch_operation_context_accept (context); +} + static void empathy_call_window_init (EmpathyCallWindow *self) { @@ -786,7 +1629,7 @@ empathy_call_window_init (EmpathyCallWindow *self) GtkBuilder *gui; GtkWidget *top_vbox; gchar *filename; - ClutterConstraint *size_constraint; + ClutterConstraint *constraint; ClutterActor *remote_avatar; GtkStyleContext *context; GdkRGBA rgba; @@ -795,6 +1638,8 @@ empathy_call_window_init (EmpathyCallWindow *self) priv = self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, EMPATHY_TYPE_CALL_WINDOW, EmpathyCallWindowPriv); + priv->settings = g_settings_new (EMPATHY_PREFS_CALL_SCHEMA); + filename = empathy_file_lookup ("empathy-call-window.ui", "src"); gui = empathy_builder_get_file (filename, "call_window_vbox", &top_vbox, @@ -813,6 +1658,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, @@ -838,11 +1684,14 @@ empathy_call_window_init (EmpathyCallWindow *self) "camera", "toggled", empathy_call_window_camera_toggled_cb, "dialpad", "toggled", empathy_call_window_dialpad_cb, "menufullscreen", "activate", empathy_call_window_fullscreen_cb, + "menusettings", "activate", empathy_call_window_settings_cb, "menucontents", "activate", empathy_call_window_contents_cb, + "menudebug", "activate", empathy_call_window_debug_cb, "menuabout", "activate", empathy_call_window_about_cb, "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); @@ -853,6 +1702,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); @@ -890,10 +1744,10 @@ empathy_call_window_init (EmpathyCallWindow *self) priv->video_box, NULL); - size_constraint = clutter_bind_constraint_new ( + constraint = clutter_bind_constraint_new ( gtk_clutter_embed_get_stage (GTK_CLUTTER_EMBED (priv->video_container)), CLUTTER_BIND_SIZE, 0); - clutter_actor_add_constraint (priv->video_box, size_constraint); + clutter_actor_add_constraint (priv->video_box, constraint); priv->remote_user_avatar_widget = gtk_image_new (); remote_avatar = gtk_clutter_actor_new_with_contents ( @@ -902,6 +1756,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); @@ -911,6 +1767,55 @@ empathy_call_window_init (EmpathyCallWindow *self) create_audio_input (self); create_video_input (self); + priv->floating_toolbar = empathy_rounded_actor_new (); + + gtk_widget_reparent (priv->bottom_toolbar, + gtk_clutter_actor_get_widget (GTK_CLUTTER_ACTOR (priv->floating_toolbar))); + + constraint = clutter_bind_constraint_new ( + gtk_clutter_embed_get_stage (GTK_CLUTTER_EMBED (priv->video_container)), + CLUTTER_BIND_Y, 0); + + clutter_actor_add_constraint (priv->floating_toolbar, constraint); + + g_signal_connect ( + gtk_clutter_embed_get_stage (GTK_CLUTTER_EMBED (priv->video_container)), + "notify::allocation", + G_CALLBACK (empathy_call_window_stage_allocation_changed_cb), + constraint); + + clutter_actor_set_size (priv->floating_toolbar, + FLOATING_TOOLBAR_WIDTH, FLOATING_TOOLBAR_HEIGHT); + clutter_actor_set_opacity (priv->floating_toolbar, FLOATING_TOOLBAR_OPACITY); + + clutter_bin_layout_add (CLUTTER_BIN_LAYOUT (priv->video_layout), + priv->floating_toolbar, + CLUTTER_BIN_ALIGNMENT_CENTER, + CLUTTER_BIN_ALIGNMENT_END); + + clutter_actor_raise_top (priv->floating_toolbar); + + /* Transitions for the floating toolbar */ + priv->transitions = clutter_state_new (); + + /* all transitions last for 2s */ + clutter_state_set_duration (priv->transitions, NULL, NULL, 2000); + + /* transition from any state to "fade-out" state */ + clutter_state_set (priv->transitions, NULL, "fade-out", + priv->floating_toolbar, + "opacity", CLUTTER_EASE_OUT_QUAD, 0, + NULL); + + /* transition from any state to "fade-in" state */ + clutter_state_set (priv->transitions, NULL, "fade-in", + priv->floating_toolbar, + "opacity", CLUTTER_EASE_OUT_QUAD, FLOATING_TOOLBAR_OPACITY, + NULL); + + /* put the actor into the "fade-in" state with no animation */ + clutter_state_warp_to_state (priv->transitions, "fade-in"); + /* The call will be started as soon the pipeline is playing */ priv->start_call_when_playing = TRUE; @@ -918,8 +1823,10 @@ empathy_call_window_init (EmpathyCallWindow *self) G_CALLBACK (dtmf_button_pressed_cb), G_CALLBACK (dtmf_button_released_cb)); + priv->tones = g_string_new (""); + 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); @@ -936,6 +1843,11 @@ empathy_call_window_init (EmpathyCallWindow *self) empathy_call_window_fullscreen_set_video_widget (priv->fullscreen, priv->video_container); + /* We hide the bottom toolbar after 3s of inactivity and show it + * again on mouse movement */ + priv->inactivity_src = g_timeout_add_seconds (3, + empathy_call_window_toolbar_timeout, self); + g_signal_connect (G_OBJECT (priv->fullscreen->leave_fullscreen_button), "clicked", G_CALLBACK (empathy_call_window_fullscreen_cb), self); @@ -951,6 +1863,9 @@ empathy_call_window_init (EmpathyCallWindow *self) g_signal_connect (G_OBJECT (self), "key-press-event", G_CALLBACK (empathy_call_window_key_press_cb), self); + g_signal_connect (self, "motion-notify-event", + G_CALLBACK (empathy_call_window_motion_notify_cb), self); + priv->timer = g_timer_new (); g_object_ref (priv->ui_manager); @@ -958,10 +1873,14 @@ 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); - priv->settings = g_settings_new (EMPATHY_PREFS_CALL_SCHEMA); + /* Retrieve initial volume */ + priv->volume = g_settings_get_double (priv->settings, + EMPATHY_PREFS_CALL_SOUND_VOLUME) / 100.0; + g_signal_connect (priv->settings, "changed::"EMPATHY_PREFS_CALL_SOUND_VOLUME, G_CALLBACK (empathy_call_window_prefs_volume_changed_cb), self); @@ -978,7 +1897,6 @@ empathy_call_window_init (EmpathyCallWindow *self) /* Don't display labels in both toolbars */ gtk_toolbar_set_style (GTK_TOOLBAR (priv->toolbar), GTK_TOOLBAR_ICONS); - gtk_toolbar_set_style (GTK_TOOLBAR (priv->bottom_toolbar), GTK_TOOLBAR_ICONS); } /* Instead of specifying a width and a height, we specify only one size. That's @@ -1025,7 +1943,7 @@ set_window_title (EmpathyCallWindow *self) } else { - gtk_window_set_title (GTK_WINDOW (self), _("Call with %d participants")); + g_warning ("Unknown remote contact!"); } } @@ -1342,17 +2260,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); @@ -1471,6 +2396,12 @@ empathy_call_window_dispose (GObject *object) priv->got_video_src = 0; } + if (priv->inactivity_src > 0) + { + g_source_remove (priv->inactivity_src); + priv->inactivity_src = 0; + } + tp_clear_object (&priv->pipeline); tp_clear_object (&priv->video_input); tp_clear_object (&priv->audio_input); @@ -1479,6 +2410,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->sound_mgr); + tp_clear_object (&priv->mic_menu); + tp_clear_object (&priv->camera_menu); g_list_free_full (priv->notifiers, g_object_unref); @@ -1493,11 +2427,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); } @@ -1527,6 +2456,8 @@ empathy_call_window_finalize (GObject *object) g_timer_destroy (priv->timer); + g_string_free (priv->tones, TRUE); + G_OBJECT_CLASS (empathy_call_window_parent_class)->finalize (object); } @@ -1538,6 +2469,20 @@ empathy_call_window_new (EmpathyCallHandler *handler) g_object_new (EMPATHY_TYPE_CALL_WINDOW, "handler", handler, NULL)); } +void +empathy_call_window_present (EmpathyCallWindow *self, + EmpathyCallHandler *handler) +{ + g_return_if_fail (EMPATHY_IS_CALL_HANDLER (handler)); + + tp_clear_object (&self->priv->handler); + self->priv->handler = g_object_ref (handler); + empathy_call_window_connect_handler (self); + + empathy_window_present (GTK_WINDOW (self)); + empathy_call_window_restart_call (self); +} + static void empathy_call_window_conference_added_cb (EmpathyCallHandler *handler, GstElement *conference, gpointer user_data) @@ -1611,6 +2556,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); @@ -1654,6 +2608,9 @@ empathy_call_window_disconnected (EmpathyCallWindow *self, gtk_action_set_sensitive (priv->menu_fullscreen, FALSE); gtk_widget_set_sensitive (priv->dtmf_panel, FALSE); + priv->sending_tones = FALSE; + g_string_set_size (priv->tones, 0); + could_reset_pipeline = empathy_call_window_reset_pipeline (self); if (priv->call_state == CONNECTING) @@ -1662,6 +2619,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); @@ -2202,6 +3162,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); + empathy_call_window_raise_actors (self); } return FALSE; @@ -2466,7 +3427,7 @@ empathy_call_window_bus_message (GstBus *bus, GstMessage *message, { EmpathyCallWindow *self = EMPATHY_CALL_WINDOW (user_data); EmpathyCallWindowPriv *priv = GET_PRIV (self); - GstState newstate; + GstState newstate, pending; empathy_call_handler_bus_message (priv->handler, bus, message); @@ -2490,6 +3451,15 @@ empathy_call_window_bus_message (GstBus *bus, GstMessage *message, start_call (self); } } + if (GST_MESSAGE_SRC (message) == GST_OBJECT (priv->video_preview_sink)) + { + gst_message_parse_state_changed (message, NULL, &newstate, + &pending); + + if (newstate == GST_STATE_PLAYING && + pending == GST_STATE_VOID_PENDING) + empathy_call_window_stop_camera_spinning (self); + } break; case GST_MESSAGE_ERROR: { @@ -2594,50 +3564,63 @@ call_handler_notify_call_cb (EmpathyCallHandler *handler, tp_g_signal_connect_object (call, "members-changed", G_CALLBACK (empathy_call_window_members_changed_cb), self, 0); + tp_cli_channel_interface_dtmf_connect_to_stopped_tones (TP_CHANNEL (call), + empathy_call_window_tones_stopped_cb, self, NULL, + G_OBJECT (call), NULL); + g_object_unref (call); } static void -empathy_call_window_realized_cb (GtkWidget *widget, EmpathyCallWindow *window) +empathy_call_window_connect_handler (EmpathyCallWindow *self) { - EmpathyCallWindowPriv *priv = GET_PRIV (window); + EmpathyCallWindowPriv *priv = GET_PRIV (self); TpyCallChannel *call; - gint width; - - /* Make the hangup button twice as wide */ - width = gtk_widget_get_allocated_width (priv->hangup_button); - gtk_widget_set_size_request (priv->hangup_button, width * 2, -1); g_signal_connect (priv->handler, "state-changed", - G_CALLBACK (empathy_call_window_state_changed_cb), window); + G_CALLBACK (empathy_call_window_state_changed_cb), self); g_signal_connect (priv->handler, "conference-added", - G_CALLBACK (empathy_call_window_conference_added_cb), window); + G_CALLBACK (empathy_call_window_conference_added_cb), self); g_signal_connect (priv->handler, "conference-removed", - G_CALLBACK (empathy_call_window_conference_removed_cb), window); + G_CALLBACK (empathy_call_window_conference_removed_cb), self); g_signal_connect (priv->handler, "closed", - G_CALLBACK (empathy_call_window_channel_closed_cb), window); + G_CALLBACK (empathy_call_window_channel_closed_cb), self); g_signal_connect (priv->handler, "src-pad-added", - G_CALLBACK (empathy_call_window_src_added_cb), window); + G_CALLBACK (empathy_call_window_src_added_cb), self); g_signal_connect (priv->handler, "sink-pad-added", - G_CALLBACK (empathy_call_window_sink_added_cb), window); + G_CALLBACK (empathy_call_window_sink_added_cb), self); g_signal_connect (priv->handler, "sink-pad-removed", - G_CALLBACK (empathy_call_window_sink_removed_cb), window); + G_CALLBACK (empathy_call_window_sink_removed_cb), self); + + /* We connect to ::call-channel unconditionally since we'll + * get new channels if we hangup and redial or if we reuse the + * call window. */ + g_signal_connect (priv->handler, "notify::call-channel", + G_CALLBACK (call_handler_notify_call_cb), self); g_object_get (priv->handler, "call-channel", &call, NULL); if (call != NULL) { - call_handler_notify_call_cb (priv->handler, NULL, window); + /* We won't get notify::call-channel for this channel, so + * directly call the callback. */ + call_handler_notify_call_cb (priv->handler, NULL, self); g_object_unref (call); } - else - { - /* call-channel doesn't exist yet, we'll connect signals once it has been - * set */ - g_signal_connect (priv->handler, "notify::call-channel", - G_CALLBACK (call_handler_notify_call_cb), window); - } +} + +static void +empathy_call_window_realized_cb (GtkWidget *widget, + EmpathyCallWindow *self) +{ + gint width; - gst_element_set_state (priv->pipeline, GST_STATE_PAUSED); + /* Make the hangup button twice as wide */ + width = gtk_widget_get_allocated_width (self->priv->hangup_button); + gtk_widget_set_size_request (self->priv->hangup_button, width * 2, -1); + + empathy_call_window_connect_handler (self); + + gst_element_set_state (self->priv->pipeline, GST_STATE_PAUSED); } static gboolean @@ -2826,13 +3809,18 @@ empathy_call_window_set_send_video (EmpathyCallWindow *window, static void empathy_call_window_mic_toggled_cb (GtkToggleToolButton *toggle, - EmpathyCallWindow *window) + EmpathyCallWindow *self) { - EmpathyCallWindowPriv *priv = GET_PRIV (window); + EmpathyCallWindowPriv *priv = GET_PRIV (self); gboolean active; active = (gtk_toggle_tool_button_get_active (toggle)); + /* We don't want the settings callback to react to this change to avoid + * a loop. */ + g_signal_handlers_block_by_func (priv->settings, + empathy_call_window_prefs_volume_changed_cb, self); + if (active) { g_settings_set_double (priv->settings, EMPATHY_PREFS_CALL_SOUND_VOLUME, @@ -2848,6 +3836,9 @@ empathy_call_window_mic_toggled_cb (GtkToggleToolButton *toggle, g_settings_set_double (priv->settings, EMPATHY_PREFS_CALL_SOUND_VOLUME, 0); } + + g_signal_handlers_unblock_by_func (priv->settings, + empathy_call_window_prefs_volume_changed_cb, self); } static void @@ -2957,6 +3948,9 @@ empathy_call_window_video_output_motion_notify (GtkWidget *widget, if (priv->is_fullscreen) { empathy_call_window_fullscreen_show_popup (priv->fullscreen); + + /* Show the bottom toolbar */ + empathy_call_window_motion_notify_cb (NULL, NULL, window); return TRUE; } return FALSE; @@ -3011,3 +4005,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); +}