X-Git-Url: https://git.0d.be/?p=empathy.git;a=blobdiff_plain;f=src%2Fempathy-call-window.c;h=2a92e947168b3d395cd17c253ed1de264d8d5039;hp=afd02522ba9241ece04f199d8c9e852a938e55ab;hb=7b6b8da406493311445f6c2470a005a542972693;hpb=a36214f589404b37c6125b0a3faa756f9e001cd6 diff --git a/src/empathy-call-window.c b/src/empathy-call-window.c index afd02522..2a92e947 100644 --- a/src/empathy-call-window.c +++ b/src/empathy-call-window.c @@ -41,8 +41,11 @@ #include #include +#include #include +#include #include + #include #include #include @@ -56,18 +59,29 @@ #include "empathy-call-window-fullscreen.h" #include "empathy-call-factory.h" #include "empathy-video-widget.h" +#include "empathy-about-dialog.h" #include "empathy-audio-src.h" #include "empathy-audio-sink.h" #include "empathy-video-src.h" -#include "ev-sidebar.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. */ @@ -90,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 { @@ -102,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; @@ -120,12 +143,23 @@ 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; GtkWidget *remote_user_name_toolbar; - GtkWidget *sidebar; GtkWidget *status_label; GtkWidget *hangup_button; GtkWidget *audio_call_button; @@ -135,27 +169,41 @@ struct _EmpathyCallWindowPriv GtkWidget *dialpad_button; GtkWidget *toolbar; GtkWidget *bottom_toolbar; + ClutterActor *floating_toolbar; GtkWidget *pane; - GtkAction *menu_sidebar; 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; - GtkWidget *volume_scale; - GtkWidget *volume_progress_bar; - GtkAdjustment *audio_input_adj; + /* String that contains the queued tones to send after the current ones + are sent */ + GString *tones; + gboolean sending_tones; GtkWidget *dtmf_panel; /* Details vbox */ @@ -189,10 +237,6 @@ struct _EmpathyCallWindowPriv GTimer *timer; guint timer_id; - GtkWidget *video_contrast; - GtkWidget *video_brightness; - GtkWidget *video_gamma; - GMutex *lock; gboolean call_started; gboolean sending_video; @@ -204,13 +248,15 @@ 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 sidebar_was_visible_before_fs; + gboolean dialpad_was_visible_before_fs; gint original_width_before_fs; gint original_height_before_fs; - gint x, y, w, h, sidebar_width; + gint x, y, w, h, dialpad_width; gboolean maximized; /* TRUE if the call should be started when the pipeline is playing */ @@ -220,7 +266,9 @@ struct _EmpathyCallWindowPriv EmpathySoundManager *sound_mgr; + GSettings *settings; EmpathyMicMenu *mic_menu; + EmpathyCameraMenu *camera_menu; }; #define GET_PRIV(o) (EMPATHY_CALL_WINDOW (o)->priv) @@ -240,19 +288,6 @@ static void empathy_call_window_set_send_video (EmpathyCallWindow *window, static void empathy_call_window_mic_toggled_cb ( GtkToggleToolButton *toggle, EmpathyCallWindow *window); -static void empathy_call_window_sidebar_cb (GtkToggleAction *menu, - EmpathyCallWindow *self); - -static void empathy_call_window_sidebar_hidden_cb (EvSidebar *sidebar, - EmpathyCallWindow *window); - -static void empathy_call_window_sidebar_shown_cb (EvSidebar *sidebar, - EmpathyCallWindow *window); - -static void empathy_call_window_sidebar_changed_cb (EvSidebar *sidebar, - const gchar *page, - EmpathyCallWindow *window); - static void empathy_call_window_hangup_cb (gpointer object, EmpathyCallWindow *window); @@ -273,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); @@ -315,168 +352,82 @@ empathy_call_window_video_call_cb (GtkToggleToolButton *button, } static void -dtmf_button_pressed_cb (GtkButton *button, EmpathyCallWindow *window) -{ - EmpathyCallWindowPriv *priv = GET_PRIV (window); - TpyCallChannel *call; - 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_object_unref (call); -} - -static void -dtmf_button_released_cb (GtkButton *button, EmpathyCallWindow *window) +empathy_call_window_emit_tones (EmpathyCallWindow *self) { - EmpathyCallWindowPriv *priv = GET_PRIV (window); - TpyCallChannel *call; - - g_object_get (priv->handler, "call-channel", &call, NULL); + TpChannel *channel; - tpy_call_channel_dtmf_stop_tone (call); + if (tp_str_empty (self->priv->tones->str)) + return; - g_object_unref (call); -} + g_object_get (self->priv->handler, "call-channel", &channel, NULL); -static GtkWidget * -empathy_call_window_create_video_input_add_slider (EmpathyCallWindow *self, - gchar *label_text, GtkWidget *bin) -{ - GtkWidget *vbox = gtk_vbox_new (FALSE, 2); - GtkWidget *scale = gtk_vscale_new_with_range (0, 100, 10); - GtkWidget *label = gtk_label_new (label_text); + DEBUG ("Emitting multiple tones: %s", self->priv->tones->str); - gtk_widget_set_sensitive (scale, FALSE); + tp_cli_channel_interface_dtmf_call_multiple_tones (channel, -1, + self->priv->tones->str, + NULL, NULL, NULL, NULL); - gtk_container_add (GTK_CONTAINER (bin), vbox); + self->priv->sending_tones = TRUE; - gtk_range_set_inverted (GTK_RANGE (scale), TRUE); - gtk_box_pack_start (GTK_BOX (vbox), scale, TRUE, TRUE, 0); - gtk_box_pack_start (GTK_BOX (vbox), label, FALSE, FALSE, 0); + g_string_set_size (self->priv->tones, 0); - return scale; + g_object_unref (channel); } static void -empathy_call_window_video_contrast_changed_cb (GtkAdjustment *adj, - EmpathyCallWindow *self) - +empathy_call_window_maybe_emit_tones (EmpathyCallWindow *self) { - EmpathyCallWindowPriv *priv = GET_PRIV (self); + if (self->priv->sending_tones) + return; - empathy_video_src_set_channel (priv->video_input, - EMPATHY_GST_VIDEO_SRC_CHANNEL_CONTRAST, gtk_adjustment_get_value (adj)); + empathy_call_window_emit_tones (self); } static void -empathy_call_window_video_brightness_changed_cb (GtkAdjustment *adj, - EmpathyCallWindow *self) - +empathy_call_window_tones_stopped_cb (TpChannel *proxy, + gboolean arg_cancelled, + gpointer user_data, + GObject *weak_object) { - EmpathyCallWindowPriv *priv = GET_PRIV (self); - - empathy_video_src_set_channel (priv->video_input, - EMPATHY_GST_VIDEO_SRC_CHANNEL_BRIGHTNESS, gtk_adjustment_get_value (adj)); -} - -static void -empathy_call_window_video_gamma_changed_cb (GtkAdjustment *adj, - EmpathyCallWindow *self) + EmpathyCallWindow *self = EMPATHY_CALL_WINDOW (user_data); -{ - EmpathyCallWindowPriv *priv = GET_PRIV (self); + self->priv->sending_tones = FALSE; - empathy_video_src_set_channel (priv->video_input, - EMPATHY_GST_VIDEO_SRC_CHANNEL_GAMMA, gtk_adjustment_get_value (adj)); + empathy_call_window_emit_tones (self); } - -static GtkWidget * -empathy_call_window_create_video_input (EmpathyCallWindow *self) +static void +dtmf_button_pressed_cb (GtkButton *button, + EmpathyCallWindow *self) { EmpathyCallWindowPriv *priv = GET_PRIV (self); - GtkWidget *hbox; - - hbox = gtk_hbox_new (TRUE, 3); - - priv->video_contrast = empathy_call_window_create_video_input_add_slider ( - self, _("Contrast"), hbox); + GQuark button_quark; + TpDTMFEvent event; - priv->video_brightness = empathy_call_window_create_video_input_add_slider ( - self, _("Brightness"), hbox); + button_quark = g_quark_from_static_string (EMPATHY_DTMF_BUTTON_ID); + event = GPOINTER_TO_UINT (g_object_get_qdata (G_OBJECT (button), + button_quark)); - priv->video_gamma = empathy_call_window_create_video_input_add_slider ( - self, _("Gamma"), hbox); + g_string_append_c (priv->tones, tp_dtmf_event_to_char (event)); - return hbox; + empathy_call_window_maybe_emit_tones (self); } +/* empathy_create_dtmf_dialpad() requires a callback, even if empty */ static void -empathy_call_window_setup_video_input (EmpathyCallWindow *self) +dtmf_button_released_cb (GtkButton *button, + EmpathyCallWindow *self) { - EmpathyCallWindowPriv *priv = GET_PRIV (self); - guint supported; - GtkAdjustment *adj; - - supported = empathy_video_src_get_supported_channels (priv->video_input); - - if (supported & EMPATHY_GST_VIDEO_SRC_SUPPORTS_CONTRAST) - { - adj = gtk_range_get_adjustment (GTK_RANGE (priv->video_contrast)); - - gtk_adjustment_set_value (adj, - empathy_video_src_get_channel (priv->video_input, - EMPATHY_GST_VIDEO_SRC_CHANNEL_CONTRAST)); - - g_signal_connect (G_OBJECT (adj), "value-changed", - G_CALLBACK (empathy_call_window_video_contrast_changed_cb), self); - - gtk_widget_set_sensitive (priv->video_contrast, TRUE); - } - - if (supported & EMPATHY_GST_VIDEO_SRC_SUPPORTS_BRIGHTNESS) - { - adj = gtk_range_get_adjustment (GTK_RANGE (priv->video_brightness)); - - gtk_adjustment_set_value (adj, - empathy_video_src_get_channel (priv->video_input, - EMPATHY_GST_VIDEO_SRC_CHANNEL_BRIGHTNESS)); - - g_signal_connect (G_OBJECT (adj), "value-changed", - G_CALLBACK (empathy_call_window_video_brightness_changed_cb), self); - gtk_widget_set_sensitive (priv->video_brightness, TRUE); - } - - if (supported & EMPATHY_GST_VIDEO_SRC_SUPPORTS_GAMMA) - { - adj = gtk_range_get_adjustment (GTK_RANGE (priv->video_gamma)); - - gtk_adjustment_set_value (adj, - empathy_video_src_get_channel (priv->video_input, - EMPATHY_GST_VIDEO_SRC_CHANNEL_GAMMA)); - - g_signal_connect (G_OBJECT (adj), "value-changed", - G_CALLBACK (empathy_call_window_video_gamma_changed_cb), self); - gtk_widget_set_sensitive (priv->video_gamma, TRUE); - } } static void -empathy_call_window_mic_volume_changed_cb (GtkAdjustment *adj, - EmpathyCallWindow *self) +empathy_call_window_mic_volume_changed (EmpathyCallWindow *self) { EmpathyCallWindowPriv *priv = GET_PRIV (self); gdouble volume; - volume = gtk_adjustment_get_value (adj)/100.0; + volume = g_settings_get_double (priv->settings, + EMPATHY_PREFS_CALL_SOUND_VOLUME) / 100.0; /* Don't store the volume because of muting */ if (volume > 0 || gtk_toggle_tool_button_get_active ( @@ -495,56 +446,22 @@ empathy_call_window_mic_volume_changed_cb (GtkAdjustment *adj, } static void -empathy_call_window_audio_input_level_changed_cb (EmpathyGstAudioSrc *src, - gdouble level, EmpathyCallWindow *window) +empathy_call_window_prefs_volume_changed_cb (GSettings *settings, + gchar *key, + EmpathyCallWindow *self) { - gdouble value; - EmpathyCallWindowPriv *priv = GET_PRIV (window); - - value = CLAMP (pow (10, level / 20), 0.0, 1.0); - gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR (priv->volume_progress_bar), - value); + empathy_call_window_mic_volume_changed (self); } -static GtkWidget * -empathy_call_window_create_audio_input (EmpathyCallWindow *self) +static void +empathy_call_window_raise_actors (EmpathyCallWindow *self) { - EmpathyCallWindowPriv *priv = GET_PRIV (self); - GtkWidget *hbox, *vbox, *label; - - hbox = gtk_hbox_new (TRUE, 3); + clutter_actor_raise_top (self->priv->floating_toolbar); - vbox = gtk_vbox_new (FALSE, 3); - gtk_box_pack_start (GTK_BOX (hbox), vbox, FALSE, FALSE, 3); - - priv->volume_scale = gtk_vscale_new_with_range (0, 150, 100); - gtk_range_set_inverted (GTK_RANGE (priv->volume_scale), TRUE); - label = gtk_label_new (_("Volume")); - - priv->audio_input_adj = gtk_range_get_adjustment ( - GTK_RANGE (priv->volume_scale)); - priv->volume = empathy_audio_src_get_volume (EMPATHY_GST_AUDIO_SRC - (priv->audio_input)); - gtk_adjustment_set_value (priv->audio_input_adj, priv->volume * 100); - - g_signal_connect (G_OBJECT (priv->audio_input_adj), "value-changed", - G_CALLBACK (empathy_call_window_mic_volume_changed_cb), self); - - gtk_box_pack_start (GTK_BOX (vbox), priv->volume_scale, TRUE, TRUE, 3); - gtk_box_pack_start (GTK_BOX (vbox), label, FALSE, FALSE, 3); - - priv->volume_progress_bar = gtk_progress_bar_new (); - - gtk_orientable_set_orientation (GTK_ORIENTABLE (priv->volume_progress_bar), - GTK_ORIENTATION_VERTICAL); - - gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR (priv->volume_progress_bar), - 0); - - gtk_box_pack_start (GTK_BOX (hbox), priv->volume_progress_bar, FALSE, FALSE, - 3); - - return hbox; + 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 @@ -555,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 @@ -602,10 +521,6 @@ create_audio_input (EmpathyCallWindow *self) priv->audio_input = empathy_audio_src_new (); gst_object_ref (priv->audio_input); gst_object_sink (priv->audio_input); - - tp_g_signal_connect_object (priv->audio_input, "peak-level-changed", - G_CALLBACK (empathy_call_window_audio_input_level_changed_cb), - self, 0); } static void @@ -672,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) @@ -685,59 +661,516 @@ empathy_call_window_preview_button_clicked_cb (GtkButton *button, gtk_menu_shell_select_first (GTK_MENU_SHELL (menu), FALSE); } -static void -empathy_call_window_preview_hidden_button_clicked_cb (GtkButton *button, +static void +empathy_call_window_preview_hidden_button_clicked_cb (GtkButton *button, + EmpathyCallWindow *self) +{ + GtkWidget *menu; + + menu = gtk_ui_manager_get_widget (self->priv->ui_manager, + "/preview-hidden-menu"); + gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL, + 0, gtk_get_current_event_time ()); + 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) { - GtkWidget *menu; + ClutterActor *rectangle; - menu = gtk_ui_manager_get_widget (self->priv->ui_manager, - "/preview-hidden-menu"); - gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL, - 0, gtk_get_current_event_time ()); - gtk_menu_shell_select_first (GTK_MENU_SHELL (menu), FALSE); + 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, @@ -748,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), @@ -762,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", @@ -780,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; @@ -819,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 { @@ -830,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); } } } @@ -843,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); @@ -921,6 +1411,69 @@ 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) +{ + 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) +{ + 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, @@ -932,8 +1485,8 @@ empathy_call_window_configure_event_cb (GtkWidget *widget, gtk_window_get_position (GTK_WINDOW (self), &self->priv->x, &self->priv->y); gtk_window_get_size (GTK_WINDOW (self), &self->priv->w, &self->priv->h); - gtk_widget_get_preferred_width (self->priv->sidebar, - &self->priv->sidebar_width, NULL); + gtk_widget_get_preferred_width (self->priv->dtmf_panel, + &self->priv->dialpad_width, NULL); gdk_window = gtk_widget_get_window (widget); window_state = gdk_window_get_state (gdk_window); @@ -946,26 +1499,137 @@ static void empathy_call_window_destroyed_cb (GtkWidget *object, EmpathyCallWindow *self) { - if (gtk_widget_get_visible (self->priv->sidebar)) + if (gtk_widget_get_visible (self->priv->dtmf_panel)) { - /* Save the geometry as if the sidebar was hidden. */ + /* Save the geometry as if the dialpad was hidden. */ empathy_geometry_save_values (GTK_WINDOW (self), self->priv->x, self->priv->y, - self->priv->w - self->priv->sidebar_width, self->priv->h, + self->priv->w - self->priv->dialpad_width, self->priv->h, self->priv->maximized); } } +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) { EmpathyCallWindowPriv *priv; GtkBuilder *gui; GtkWidget *top_vbox; - GtkWidget *page; gchar *filename; - GtkWidget *scroll; - ClutterConstraint *size_constraint; + ClutterConstraint *constraint; ClutterActor *remote_avatar; GtkStyleContext *context; GdkRGBA rgba; @@ -974,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, @@ -990,9 +1656,9 @@ empathy_call_window_init (EmpathyCallWindow *self) "dialpad", &priv->dialpad_button, "toolbar", &priv->toolbar, "bottom_toolbar", &priv->bottom_toolbar, - "menusidebar", &priv->menu_sidebar, "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, @@ -1010,19 +1676,22 @@ empathy_call_window_init (EmpathyCallWindow *self) g_free (filename); empathy_builder_connect (gui, self, - "menuhangup", "activate", empathy_call_window_hangup_cb, "hangup", "clicked", empathy_call_window_hangup_cb, "audiocall", "clicked", empathy_call_window_audio_call_cb, "videocall", "clicked", empathy_call_window_video_call_cb, - "menusidebar", "toggled", empathy_call_window_sidebar_cb, "volume", "value-changed", empathy_call_window_volume_changed_cb, "microphone", "toggled", empathy_call_window_mic_toggled_cb, "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); @@ -1033,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); @@ -1040,7 +1714,8 @@ empathy_call_window_init (EmpathyCallWindow *self) priv->content_hbox = gtk_hbox_new (FALSE, CONTENT_HBOX_SPACING); gtk_container_set_border_width (GTK_CONTAINER (priv->content_hbox), CONTENT_HBOX_BORDER_WIDTH); - gtk_paned_pack1 (GTK_PANED (priv->pane), priv->content_hbox, TRUE, FALSE); + gtk_box_pack_start (GTK_BOX (priv->pane), priv->content_hbox, + TRUE, TRUE, 0); /* avatar/video box */ priv->video_layout = clutter_bin_layout_new (CLUTTER_BIN_ALIGNMENT_CENTER, @@ -1069,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 ( @@ -1081,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); @@ -1090,52 +1767,87 @@ empathy_call_window_init (EmpathyCallWindow *self) create_audio_input (self); create_video_input (self); - /* The call will be started as soon the pipeline is playing */ - priv->start_call_when_playing = TRUE; + priv->floating_toolbar = empathy_rounded_actor_new (); - priv->sidebar = ev_sidebar_new (); - g_signal_connect (G_OBJECT (priv->sidebar), - "hide", G_CALLBACK (empathy_call_window_sidebar_hidden_cb), self); - g_signal_connect (G_OBJECT (priv->sidebar), - "show", G_CALLBACK (empathy_call_window_sidebar_shown_cb), self); - g_signal_connect (G_OBJECT (priv->sidebar), "changed", - G_CALLBACK (empathy_call_window_sidebar_changed_cb), self); - gtk_paned_pack2 (GTK_PANED (priv->pane), priv->sidebar, FALSE, FALSE); + gtk_widget_reparent (priv->bottom_toolbar, + gtk_clutter_actor_get_widget (GTK_CLUTTER_ACTOR (priv->floating_toolbar))); - page = empathy_call_window_create_audio_input (self); - ev_sidebar_add_page (EV_SIDEBAR (priv->sidebar), "audio-input", - _("Audio input"), page); + 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"); - page = empathy_call_window_create_video_input (self); - ev_sidebar_add_page (EV_SIDEBAR (priv->sidebar), "video-input", - _("Video input"), page); + /* The call will be started as soon the pipeline is playing */ + priv->start_call_when_playing = TRUE; priv->dtmf_panel = empathy_create_dtmf_dialpad (G_OBJECT (self), G_CALLBACK (dtmf_button_pressed_cb), G_CALLBACK (dtmf_button_released_cb)); - ev_sidebar_add_page (EV_SIDEBAR (priv->sidebar), "dialpad", - _("Dialpad"), priv->dtmf_panel); - gtk_widget_set_sensitive (priv->dtmf_panel, FALSE); + priv->tones = g_string_new (""); + + gtk_box_pack_start (GTK_BOX (priv->pane), priv->dtmf_panel, + FALSE, FALSE, 6); - /* Put the details vbox in a scroll window as it can require a lot of - * horizontal space. */ - scroll = gtk_scrolled_window_new (NULL, NULL); - gtk_scrolled_window_add_with_viewport (GTK_SCROLLED_WINDOW (scroll), - priv->details_vbox); + gtk_box_pack_start (GTK_BOX (priv->pane), priv->details_vbox, + FALSE, FALSE, 0); - ev_sidebar_add_page (EV_SIDEBAR (priv->sidebar), "details", _("Details"), - scroll); + gtk_widget_set_sensitive (priv->dtmf_panel, FALSE); gtk_widget_show_all (top_vbox); - gtk_widget_hide (priv->sidebar); + gtk_widget_hide (priv->dtmf_panel); + gtk_widget_hide (priv->details_vbox); priv->fullscreen = empathy_call_window_fullscreen_new (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); @@ -1151,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); @@ -1158,19 +1873,30 @@ 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); + /* 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); + empathy_geometry_bind (GTK_WINDOW (self), "call-window"); /* These signals are used to track the window position and save it * when the window is destroyed. We need to do this as we don't want - * the window geometry to be saved with the sidebar taken into account. */ + * the window geometry to be saved with the dialpad taken into account. */ g_signal_connect (self, "destroy", G_CALLBACK (empathy_call_window_destroyed_cb), self); g_signal_connect (self, "configure-event", G_CALLBACK (empathy_call_window_configure_event_cb), self); g_signal_connect (self, "window-state-event", G_CALLBACK (empathy_call_window_configure_event_cb), self); + + /* Don't display labels in both toolbars */ + gtk_toolbar_set_style (GTK_TOOLBAR (priv->toolbar), GTK_TOOLBAR_ICONS); } /* Instead of specifying a width and a height, we specify only one size. That's @@ -1217,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!"); } } @@ -1295,13 +2021,12 @@ empathy_call_window_setup_avatars (EmpathyCallWindow *self, { EmpathyCallWindowPriv *priv = GET_PRIV (self); - g_signal_connect (priv->contact, "notify::name", - G_CALLBACK (contact_name_changed_cb), self); - g_signal_connect (priv->contact, "notify::avatar", - G_CALLBACK (contact_avatar_changed_cb), self); - /* FIXME: There's no EmpathyContact::presence yet */ - g_signal_connect (priv->contact, "notify::presence", - G_CALLBACK (contact_presence_changed_cb), self); + tp_g_signal_connect_object (priv->contact, "notify::name", + G_CALLBACK (contact_name_changed_cb), self, 0); + tp_g_signal_connect_object (priv->contact, "notify::avatar", + G_CALLBACK (contact_avatar_changed_cb), self, 0); + tp_g_signal_connect_object (priv->contact, "notify::presence", + G_CALLBACK (contact_presence_changed_cb), self, 0); set_window_title (self); set_remote_user_name (self, priv->contact); @@ -1535,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); @@ -1664,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); @@ -1671,6 +2409,10 @@ empathy_call_window_dispose (GObject *object) tp_clear_object (&priv->ui_manager); 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); @@ -1685,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); } @@ -1719,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); } @@ -1730,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) @@ -1791,9 +2544,6 @@ empathy_call_window_reset_pipeline (EmpathyCallWindow *self) g_object_unref (priv->pipeline); priv->pipeline = NULL; - g_signal_handlers_disconnect_by_func (priv->audio_input_adj, - empathy_call_window_mic_volume_changed_cb, self); - if (priv->audio_output != NULL) g_object_unref (priv->audio_output); priv->audio_output = NULL; @@ -1806,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); @@ -1849,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) @@ -1857,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); @@ -1892,9 +2657,6 @@ empathy_call_window_disconnected (EmpathyCallWindow *self, display_video_preview (self, TRUE); } - gtk_progress_bar_set_fraction ( - GTK_PROGRESS_BAR (priv->volume_progress_bar), 0); - /* destroy the video output; it will be recreated when we'll redial */ disconnect_video_output_motion_handler (self); if (priv->video_output != NULL) @@ -2118,14 +2880,23 @@ empathy_call_window_update_timer (gpointer user_data) { EmpathyCallWindow *self = EMPATHY_CALL_WINDOW (user_data); EmpathyCallWindowPriv *priv = GET_PRIV (self); + const gchar *status; gchar *str; gdouble time_; time_ = g_timer_elapsed (priv->timer, NULL); + if (priv->call_state == HELD) + status = _("On hold"); + else if (!gtk_toggle_tool_button_get_active ( + GTK_TOGGLE_TOOL_BUTTON (priv->mic_button))) + status = _("Mute"); + else + status = _("Duration"); + /* Translators: 'status - minutes:seconds' the caller has been connected */ str = g_strdup_printf (_("%s — %d:%02dm"), - priv->call_state == HELD ? _("On hold") : _("Connected"), + status, (int) time_ / 60, (int) time_ % 60); empathy_call_window_status_message (self, str); g_free (str); @@ -2391,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; @@ -2655,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); @@ -2665,8 +3437,6 @@ empathy_call_window_bus_message (GstBus *bus, GstMessage *message, if (GST_MESSAGE_SRC (message) == GST_OBJECT (priv->video_input)) { gst_message_parse_state_changed (message, NULL, &newstate, NULL); - if (newstate == GST_STATE_PAUSED) - empathy_call_window_setup_video_input (self); } if (GST_MESSAGE_SRC (message) == GST_OBJECT (priv->pipeline) && !priv->call_started) @@ -2681,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: { @@ -2785,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; + + /* 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 (priv->pipeline, GST_STATE_PAUSED); + gst_element_set_state (self->priv->pipeline, GST_STATE_PAUSED); } static gboolean @@ -2865,14 +3657,14 @@ show_controls (EmpathyCallWindow *window, gboolean set_fullscreen) if (set_fullscreen) { - gtk_widget_hide (priv->sidebar); + gtk_widget_hide (priv->dtmf_panel); gtk_widget_hide (menu); gtk_widget_hide (priv->toolbar); } else { - if (priv->sidebar_was_visible_before_fs) - gtk_widget_show (priv->sidebar); + if (priv->dialpad_was_visible_before_fs) + gtk_widget_show (priv->dtmf_panel); gtk_widget_show (menu); gtk_widget_show (priv->toolbar); @@ -2915,7 +3707,7 @@ empathy_call_window_state_event_cb (GtkWidget *widget, if (set_fullscreen) { - gboolean sidebar_was_visible; + gboolean dialpad_was_visible; GtkAllocation allocation; gint original_width, original_height; @@ -2923,9 +3715,11 @@ empathy_call_window_state_event_cb (GtkWidget *widget, original_width = allocation.width; original_height = allocation.height; - g_object_get (priv->sidebar, "visible", &sidebar_was_visible, NULL); + g_object_get (priv->dtmf_panel, + "visible", &dialpad_was_visible, + NULL); - priv->sidebar_was_visible_before_fs = sidebar_was_visible; + priv->dialpad_was_visible_before_fs = dialpad_was_visible; priv->original_width_before_fs = original_width; priv->original_height_before_fs = original_height; @@ -2956,68 +3750,32 @@ empathy_call_window_state_event_cb (GtkWidget *widget, } static void -empathy_call_window_update_sidebar_buttons (EmpathyCallWindow *window, - gboolean toggled) -{ - EmpathyCallWindowPriv *priv = GET_PRIV (window); - - /* Update dialpad button */ - g_signal_handlers_block_by_func (priv->dialpad_button, - empathy_call_window_dialpad_cb, window); - gtk_toggle_tool_button_set_active ( - GTK_TOGGLE_TOOL_BUTTON (priv->dialpad_button), - toggled); - g_signal_handlers_unblock_by_func (priv->dialpad_button, - empathy_call_window_dialpad_cb, window); - - /* Update sidebar menu */ - g_signal_handlers_block_by_func (priv->menu_sidebar, - empathy_call_window_sidebar_cb, window); - gtk_toggle_action_set_active ( - GTK_TOGGLE_ACTION (priv->menu_sidebar), - gtk_widget_get_visible (priv->sidebar)); - g_signal_handlers_unblock_by_func (priv->menu_sidebar, - empathy_call_window_sidebar_cb, window); -} - -static void -empathy_call_window_show_sidebar (EmpathyCallWindow *window, +empathy_call_window_show_dialpad (EmpathyCallWindow *window, gboolean active) { EmpathyCallWindowPriv *priv = GET_PRIV (window); - int w, h, sidebar_width, handle_size; + int w, h, dialpad_width; GtkAllocation allocation; - gchar *page; - gboolean dialpad_shown; gtk_widget_get_allocation (GTK_WIDGET (window), &allocation); w = allocation.width; h = allocation.height; - gtk_widget_style_get (priv->pane, "handle_size", &handle_size, NULL); - - gtk_widget_get_preferred_width (priv->sidebar, &sidebar_width, NULL); + gtk_widget_get_preferred_width (priv->dtmf_panel, &dialpad_width, NULL); if (active) { - gtk_widget_show (priv->sidebar); - w += sidebar_width + handle_size; + gtk_widget_show (priv->dtmf_panel); + w += dialpad_width; } else { - w -= sidebar_width + handle_size; - gtk_widget_hide (priv->sidebar); + w -= dialpad_width; + gtk_widget_hide (priv->dtmf_panel); } if (w > 0 && h > 0) gtk_window_resize (GTK_WINDOW (window), w, h); - - /* Update the 'Dialpad' menu */ - page = ev_sidebar_get_current_page (EV_SIDEBAR (priv->sidebar)); - dialpad_shown = active && !tp_strdiff (page, "dialpad"); - g_free (page); - - empathy_call_window_update_sidebar_buttons (window, dialpad_shown); } static void @@ -3051,18 +3809,22 @@ 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) { - empathy_audio_src_set_volume (EMPATHY_GST_AUDIO_SRC (priv->audio_input), - priv->volume); - gtk_adjustment_set_value (priv->audio_input_adj, priv->volume * 100); + g_settings_set_double (priv->settings, EMPATHY_PREFS_CALL_SOUND_VOLUME, + priv->volume * 100); } else { @@ -3071,45 +3833,21 @@ empathy_call_window_mic_toggled_cb (GtkToggleToolButton *toggle, * sides mute at the same time on certain CMs AFAIK. Need to revisit this * in the future. GNOME #574574 */ - empathy_audio_src_set_volume (EMPATHY_GST_AUDIO_SRC (priv->audio_input), - 0); - gtk_adjustment_set_value (priv->audio_input_adj, 0); + g_settings_set_double (priv->settings, EMPATHY_PREFS_CALL_SOUND_VOLUME, + 0); } -} - -static void -empathy_call_window_sidebar_hidden_cb (EvSidebar *sidebar, - EmpathyCallWindow *window) -{ - empathy_call_window_show_sidebar (window, FALSE); -} -static void -empathy_call_window_sidebar_shown_cb (EvSidebar *sidebar, - EmpathyCallWindow *window) -{ - empathy_call_window_show_sidebar (window, TRUE); -} - -static void -empathy_call_window_sidebar_changed_cb (EvSidebar *sidebar, - const gchar *page, - EmpathyCallWindow *window) -{ - empathy_call_window_update_sidebar_buttons (window, - !tp_strdiff (page, "dialpad")); + g_signal_handlers_unblock_by_func (priv->settings, + empathy_call_window_prefs_volume_changed_cb, self); } static void empathy_call_window_hangup_cb (gpointer object, - EmpathyCallWindow *window) + EmpathyCallWindow *self) { - EmpathyCallWindowPriv *priv = GET_PRIV (window); + empathy_call_handler_stop_call (self->priv->handler); - empathy_call_handler_stop_call (priv->handler); - - if (empathy_call_window_disconnected (window, FALSE)) - gtk_widget_destroy (GTK_WIDGET (window)); + empathy_call_window_disconnected (self, TRUE); } static void @@ -3123,13 +3861,10 @@ empathy_call_window_restart_call (EmpathyCallWindow *window) create_video_output_widget (window); - g_signal_connect (G_OBJECT (priv->audio_input_adj), "value-changed", - G_CALLBACK (empathy_call_window_mic_volume_changed_cb), window); - /* While the call was disconnected, the input volume might have changed. * However, since the audio_input source was destroyed, its volume has not * been updated during that time. That's why we manually update it here */ - empathy_call_window_mic_volume_changed_cb (priv->audio_input_adj, window); + empathy_call_window_mic_volume_changed (window); priv->outgoing = TRUE; empathy_call_window_set_state_connecting (window); @@ -3149,23 +3884,11 @@ static void empathy_call_window_dialpad_cb (GtkToggleToolButton *button, EmpathyCallWindow *window) { - EmpathyCallWindowPriv *priv = GET_PRIV (window); gboolean active; active = gtk_toggle_tool_button_get_active (button); - if (active) - ev_sidebar_set_current_page (EV_SIDEBAR (priv->sidebar), "dialpad"); - - empathy_call_window_show_sidebar (window, active); -} - -static void -empathy_call_window_sidebar_cb (GtkToggleAction *menu, - EmpathyCallWindow *self) -{ - empathy_call_window_show_sidebar (self, - gtk_toggle_action_get_active (menu)); + empathy_call_window_show_dialpad (window, active); } static void @@ -3225,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; @@ -3279,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); +}