]> git.0d.be Git - empathy.git/blobdiff - src/empathy-chat-window.c
Merge branch 'undo-close-tab'
[empathy.git] / src / empathy-chat-window.c
index 4d99afd31b58d62b1ef5bce65ae746b9e686db2e..e18ae2583437869caac8fc82db6ce4dd1bcf381a 100644 (file)
 #include <glib/gi18n.h>
 #include <libnotify/notification.h>
 
-#include <telepathy-glib/account-manager.h>
-#include <telepathy-glib/util.h>
+#include <telepathy-glib/telepathy-glib.h>
 
 #include <libempathy/empathy-contact.h>
 #include <libempathy/empathy-message.h>
-#include <libempathy/empathy-dispatcher.h>
 #include <libempathy/empathy-chatroom-manager.h>
 #include <libempathy/empathy-utils.h>
+#include <libempathy/empathy-tp-contact-factory.h>
+#include <libempathy/empathy-contact-list.h>
 
 #include <libempathy-gtk/empathy-images.h>
 #include <libempathy-gtk/empathy-conf.h>
 #include <libempathy-gtk/empathy-ui-utils.h>
 #include <libempathy-gtk/empathy-notify-manager.h>
 
+#include "empathy-chat-manager.h"
 #include "empathy-chat-window.h"
 #include "empathy-about-dialog.h"
+#include "empathy-invite-participant-dialog.h"
 
 #define DEBUG_FLAG EMPATHY_DEBUG_CHAT
 #include <libempathy/empathy-debug.h>
@@ -73,7 +75,6 @@ typedef struct {
        GList       *chats_composing;
        gboolean     page_added;
        gboolean     dnd_same_window;
-       guint        save_geometry_id;
        EmpathyChatroomManager *chatroom_manager;
        EmpathyNotifyManager *notify_mgr;
        GtkWidget   *dialog;
@@ -81,6 +82,12 @@ typedef struct {
        NotifyNotification *notification;
        NotificationData *notification_data;
 
+       GtkTargetList *contact_targets;
+       GtkTargetList *file_targets;
+
+       EmpathyChatManager *chat_manager;
+       gulong chat_manager_chats_changed_id;
+
        /* Menu items. */
        GtkUIManager *ui_manager;
        GtkAction   *menu_conv_insert_smiley;
@@ -90,9 +97,11 @@ typedef struct {
        GtkAction   *menu_edit_cut;
        GtkAction   *menu_edit_copy;
        GtkAction   *menu_edit_paste;
+       GtkAction   *menu_edit_find;
 
        GtkAction   *menu_tabs_next;
        GtkAction   *menu_tabs_prev;
+       GtkAction   *menu_tabs_undo_close_tab;
        GtkAction   *menu_tabs_left;
        GtkAction   *menu_tabs_right;
        GtkAction   *menu_tabs_detach;
@@ -107,12 +116,26 @@ static const guint tab_accel_keys[] = {
 
 typedef enum {
        DND_DRAG_TYPE_CONTACT_ID,
+       DND_DRAG_TYPE_URI_LIST,
        DND_DRAG_TYPE_TAB
 } DndDragType;
 
 static const GtkTargetEntry drag_types_dest[] = {
        { "text/contact-id", 0, DND_DRAG_TYPE_CONTACT_ID },
        { "GTK_NOTEBOOK_TAB", GTK_TARGET_SAME_APP, DND_DRAG_TYPE_TAB },
+       { "text/uri-list", 0, DND_DRAG_TYPE_URI_LIST },
+       { "text/path-list", 0, DND_DRAG_TYPE_URI_LIST },
+};
+
+static const GtkTargetEntry drag_types_dest_contact[] = {
+       { "text/contact-id", 0, DND_DRAG_TYPE_CONTACT_ID },
+};
+
+static const GtkTargetEntry drag_types_dest_file[] = {
+       /* must be first to be prioritized, in order to receive the
+        * note's file path from Tomboy instead of an URI */
+       { "text/path-list", 0, DND_DRAG_TYPE_URI_LIST },
+       { "text/uri-list", 0, DND_DRAG_TYPE_URI_LIST },
 };
 
 static void chat_window_update (EmpathyChatWindow *window);
@@ -269,8 +292,8 @@ chat_window_create_label (EmpathyChatWindow *window,
                /* We don't want focus/keynav for the button to avoid clutter, and
                 * Ctrl-W works anyway.
                 */
-               GTK_WIDGET_UNSET_FLAGS (close_button, GTK_CAN_FOCUS);
-               GTK_WIDGET_UNSET_FLAGS (close_button, GTK_CAN_DEFAULT);
+               gtk_widget_set_can_focus (close_button, FALSE);
+               gtk_widget_set_can_default (close_button, FALSE);
 
                /* Set the name to make the special rc style match. */
                gtk_widget_set_name (close_button, "empathy-close-button");
@@ -315,24 +338,53 @@ chat_window_menu_context_update (EmpathyChatWindowPriv *priv,
 {
        gboolean first_page;
        gboolean last_page;
+       gboolean wrap_around;
        gboolean is_connected;
        gint     page_num;
 
        page_num = gtk_notebook_get_current_page (GTK_NOTEBOOK (priv->notebook));
        first_page = (page_num == 0);
        last_page = (page_num == (num_pages - 1));
+       g_object_get (gtk_settings_get_default (), "gtk-keynav-wrap-around",
+                     &wrap_around, NULL);
        is_connected = empathy_chat_get_tp_chat (priv->current_chat) != NULL;
 
-       DEBUG ("Update window : Menu Contexts (Tabs & Conv)");
-
-       gtk_action_set_sensitive (priv->menu_tabs_next, TRUE);
-       gtk_action_set_sensitive (priv->menu_tabs_prev, TRUE);
+       gtk_action_set_sensitive (priv->menu_tabs_next, (!last_page ||
+                                                        wrap_around));
+       gtk_action_set_sensitive (priv->menu_tabs_prev, (!first_page ||
+                                                        wrap_around));
        gtk_action_set_sensitive (priv->menu_tabs_detach, num_pages > 1);
        gtk_action_set_sensitive (priv->menu_tabs_left, !first_page);
        gtk_action_set_sensitive (priv->menu_tabs_right, !last_page);
        gtk_action_set_sensitive (priv->menu_conv_insert_smiley, is_connected);
 }
 
+static void
+chat_window_conversation_menu_update (EmpathyChatWindowPriv *priv,
+                                      EmpathyChatWindow     *self)
+{
+       EmpathyTpChat *tp_chat;
+       TpConnection *connection;
+       GtkAction *action;
+       gboolean sensitive = FALSE;
+
+       g_return_if_fail (priv->current_chat != NULL);
+
+       action = gtk_ui_manager_get_action (priv->ui_manager,
+               "/chats_menubar/menu_conv/menu_conv_invite_participant");
+       tp_chat = empathy_chat_get_tp_chat (priv->current_chat);
+
+       if (tp_chat != NULL) {
+               connection = empathy_tp_chat_get_connection (tp_chat);
+
+               sensitive = empathy_tp_chat_can_add_contact (tp_chat) &&
+                       (tp_connection_get_status (connection, NULL) ==
+                        TP_CONNECTION_STATUS_CONNECTED);
+       }
+
+       gtk_action_set_sensitive (action, sensitive);
+}
+
 static void
 chat_window_contact_menu_update (EmpathyChatWindowPriv *priv,
                                 EmpathyChatWindow     *window)
@@ -343,8 +395,6 @@ chat_window_contact_menu_update (EmpathyChatWindowPriv *priv,
                "/chats_menubar/menu_contact");
        orig_submenu = gtk_menu_item_get_submenu (GTK_MENU_ITEM (menu));
 
-       DEBUG ("Update window : Contact Menu");
-
        if (orig_submenu == NULL || !GTK_WIDGET_VISIBLE (orig_submenu)) {
                submenu = empathy_chat_get_contact_menu (priv->current_chat);
                gtk_menu_item_set_submenu (GTK_MENU_ITEM (menu), submenu);
@@ -357,16 +407,95 @@ chat_window_contact_menu_update (EmpathyChatWindowPriv *priv,
        }
 }
 
-static void
-chat_window_title_update (EmpathyChatWindowPriv *priv)
+static guint
+get_all_unread_messages (EmpathyChatWindowPriv *priv)
+{
+       GList *l;
+       guint nb = 0;
+
+       for (l = priv->chats_new_msg; l != NULL; l = g_list_next (l)) {
+               EmpathyChat *chat = l->data;
+
+               nb += empathy_chat_get_nb_unread_messages (chat);
+       }
+
+       return nb;
+}
+
+static gchar *
+get_window_title_name (EmpathyChatWindowPriv *priv)
 {
-       const gchar *name;
+       const gchar *active_name;
+       guint nb_chats;
+       guint current_unread_msgs;
+
+       nb_chats = g_list_length (priv->chats);
+       g_assert (nb_chats > 0);
+
+       active_name = empathy_chat_get_name (priv->current_chat);
+
+       current_unread_msgs = empathy_chat_get_nb_unread_messages (
+                       priv->current_chat);
+
+       if (nb_chats == 1) {
+               /* only one tab */
+               if (current_unread_msgs == 0)
+                       return g_strdup (active_name);
+               else
+                       return g_strdup_printf (ngettext (
+                               "%s (%d unread)",
+                               "%s (%d unread)", current_unread_msgs),
+                               active_name, current_unread_msgs);
+       } else {
+               guint nb_others = nb_chats - 1;
+               guint all_unread_msgs;
+
+               all_unread_msgs = get_all_unread_messages (priv);
 
-       name = empathy_chat_get_name (priv->current_chat);
+               if (all_unread_msgs == 0) {
+                       /* no unread message */
+                       return g_strdup_printf (ngettext (
+                               "%s (and %u other)",
+                               "%s (and %u others)", nb_others),
+                               active_name, nb_others);
+               }
 
-       DEBUG ("Update window : Title");
+               else if (all_unread_msgs == current_unread_msgs) {
+                       /* unread messages are in the current tab */
+                       return g_strdup_printf (ngettext (
+                               "%s (%d unread)",
+                               "%s (%d unread)", current_unread_msgs),
+                               active_name, current_unread_msgs);
+               }
 
+               else if (current_unread_msgs == 0) {
+                       /* unread messages are in other tabs */
+                       return g_strdup_printf (ngettext (
+                               "%s (%d unread from others)",
+                               "%s (%d unread from others)",
+                               all_unread_msgs),
+                               active_name, all_unread_msgs);
+               }
+
+               else {
+                       /* unread messages are in all the tabs */
+                       return g_strdup_printf (ngettext (
+                               "%s (%d unread from all)",
+                               "%s (%d unread from all)",
+                               all_unread_msgs),
+                               active_name, all_unread_msgs);
+               }
+       }
+}
+
+static void
+chat_window_title_update (EmpathyChatWindowPriv *priv)
+{
+       gchar *name;
+
+       name = get_window_title_name (priv);
        gtk_window_set_title (GTK_WINDOW (priv->dialog), name);
+       g_free (name);
 }
 
 static void
@@ -379,8 +508,6 @@ chat_window_icon_update (EmpathyChatWindowPriv *priv)
 
        n_chats = g_list_length (priv->chats);
 
-       DEBUG ("Update window : Icon");
-
        /* Update window icon */
        if (priv->chats_new_msg) {
                gtk_window_set_icon_name (GTK_WINDOW (priv->dialog),
@@ -412,8 +539,6 @@ chat_window_close_button_update (EmpathyChatWindowPriv *priv,
        GtkWidget *chat_close_button;
        gint       i;
 
-       DEBUG ("Update window : Close Button");
-
        if (num_pages == 1) {
                chat = gtk_notebook_get_nth_page (GTK_NOTEBOOK (priv->notebook), 0);
                chat_close_button = g_object_get_data (G_OBJECT (chat),
@@ -437,12 +562,12 @@ chat_window_update (EmpathyChatWindow *window)
 
        num_pages = gtk_notebook_get_n_pages (GTK_NOTEBOOK (priv->notebook));
 
-       DEBUG ("Update window");
-
        /* Update Tab menu */
        chat_window_menu_context_update (priv,
                                         num_pages);
 
+       chat_window_conversation_menu_update (priv, window);
+
        chat_window_contact_menu_update (priv,
                                         window);
 
@@ -604,8 +729,8 @@ chat_window_chat_notify_cb (EmpathyChat *chat)
                                                              chat);
                }
 
-               g_object_set_data (G_OBJECT (chat), "chat-window-remote-contact",
-                                  remote_contact);
+               g_object_set_data_full (G_OBJECT (chat), "chat-window-remote-contact",
+                                  g_object_ref (remote_contact), (GDestroyNotify) g_object_unref);
        }
 
        chat_window_update_chat_tab (chat);
@@ -725,61 +850,69 @@ chat_window_contacts_toggled_cb (GtkToggleAction   *toggle_action,
        empathy_chat_set_show_contacts (priv->current_chat, active);
 }
 
-static const gchar *
-chat_get_window_id_for_geometry (EmpathyChat *chat)
+static void
+got_contact_cb (EmpathyTpContactFactory *factory,
+                EmpathyContact          *contact,
+                const GError            *error,
+                gpointer                 user_data,
+                GObject                 *object)
 {
-       const gchar *res = NULL;
-       gboolean     separate_windows;
+       EmpathyTpChat *tp_chat = EMPATHY_TP_CHAT (user_data);
 
-       empathy_conf_get_bool (empathy_conf_get (),
-                              EMPATHY_PREFS_UI_SEPARATE_CHAT_WINDOWS,
-                              &separate_windows);
-
-       if (separate_windows) {
-               res = empathy_chat_get_id (chat);
+       if (error != NULL) {
+               DEBUG ("Failed: %s", error->message);
+               return;
+       } else {
+               empathy_contact_list_add (EMPATHY_CONTACT_LIST (tp_chat),
+                               contact, _("Inviting you to this room"));
        }
-
-       return res ? res : "chat-window";
 }
 
-static gboolean
-chat_window_save_geometry_timeout_cb (EmpathyChatWindow *window)
+static void
+chat_window_invite_participant_activate_cb (GtkAction         *action,
+                                           EmpathyChatWindow *window)
 {
        EmpathyChatWindowPriv *priv;
-       gint                  x, y, w, h;
+       GtkWidget             *dialog;
+       EmpathyTpChat         *tp_chat;
+       TpChannel             *channel;
+       int                    response;
+       TpAccount             *account;
 
        priv = GET_PRIV (window);
 
-       gtk_window_get_size (GTK_WINDOW (priv->dialog), &w, &h);
-       gtk_window_get_position (GTK_WINDOW (priv->dialog), &x, &y);
+       g_return_if_fail (priv->current_chat != NULL);
 
-       empathy_geometry_save (chat_get_window_id_for_geometry (priv->current_chat),
-                              x, y, w, h);
+       tp_chat = empathy_chat_get_tp_chat (priv->current_chat);
+       channel = empathy_tp_chat_get_channel (tp_chat);
+       account = empathy_chat_get_account (priv->current_chat);
 
-       priv->save_geometry_id = 0;
+       dialog = empathy_invite_participant_dialog_new (
+                       GTK_WINDOW (priv->dialog), account);
+       gtk_widget_show (dialog);
 
-       return FALSE;
-}
+       response = gtk_dialog_run (GTK_DIALOG (dialog));
 
-static gboolean
-chat_window_configure_event_cb (GtkWidget         *widget,
-                               GdkEventConfigure *event,
-                               EmpathyChatWindow  *window)
-{
-       EmpathyChatWindowPriv *priv;
+       if (response == GTK_RESPONSE_ACCEPT) {
+               TpConnection *connection;
+               EmpathyTpContactFactory *factory;
+               const char *id;
 
-       priv = GET_PRIV (window);
+               id = empathy_contact_selector_dialog_get_selected (
+                               EMPATHY_CONTACT_SELECTOR_DIALOG (dialog), NULL);
+               if (EMP_STR_EMPTY (id)) goto out;
 
-       if (priv->save_geometry_id != 0) {
-               g_source_remove (priv->save_geometry_id);
-       }
+               connection = tp_channel_borrow_connection (channel);
+               factory = empathy_tp_contact_factory_dup_singleton (connection);
 
-       priv->save_geometry_id =
-               g_timeout_add_seconds (1,
-                                      (GSourceFunc) chat_window_save_geometry_timeout_cb,
-                                      window);
+               empathy_tp_contact_factory_get_from_id (factory, id,
+                       got_contact_cb, tp_chat,  NULL, NULL);
 
-       return FALSE;
+               g_object_unref (factory);
+       }
+
+out:
+       gtk_widget_destroy (dialog);
 }
 
 static void
@@ -872,6 +1005,19 @@ chat_window_paste_activate_cb (GtkAction         *action,
        empathy_chat_paste (priv->current_chat);
 }
 
+static void
+chat_window_find_activate_cb (GtkAction         *action,
+                             EmpathyChatWindow *window)
+{
+       EmpathyChatWindowPriv *priv;
+
+       g_return_if_fail (EMPATHY_IS_CHAT_WINDOW (window));
+
+       priv = GET_PRIV (window);
+
+       empathy_chat_find (priv->current_chat);
+}
+
 static void
 chat_window_tabs_next_activate_cb (GtkAction         *action,
                                   EmpathyChatWindow *window)
@@ -879,14 +1025,18 @@ chat_window_tabs_next_activate_cb (GtkAction         *action,
        EmpathyChatWindowPriv *priv;
        EmpathyChat           *chat;
        gint                  index_, numPages;
+       gboolean              wrap_around;
 
        priv = GET_PRIV (window);
 
+       g_object_get (gtk_settings_get_default (), "gtk-keynav-wrap-around",
+                      &wrap_around, NULL);
+
        chat = priv->current_chat;
        index_ = gtk_notebook_get_current_page (GTK_NOTEBOOK (priv->notebook));
        numPages = gtk_notebook_get_n_pages (GTK_NOTEBOOK (priv->notebook));
 
-       if (index_ == (numPages - 1)) {
+       if (index_ == (numPages - 1) && wrap_around) {
                gtk_notebook_set_current_page (GTK_NOTEBOOK (priv->notebook), 0);
                return;
        }
@@ -901,14 +1051,18 @@ chat_window_tabs_previous_activate_cb (GtkAction         *action,
        EmpathyChatWindowPriv *priv;
        EmpathyChat           *chat;
        gint                  index_, numPages;
+       gboolean              wrap_around;
 
        priv = GET_PRIV (window);
 
+       g_object_get (gtk_settings_get_default (), "gtk-keynav-wrap-around",
+                      &wrap_around, NULL);
+
        chat = priv->current_chat;
        index_ = gtk_notebook_get_current_page (GTK_NOTEBOOK (priv->notebook));
        numPages = gtk_notebook_get_n_pages (GTK_NOTEBOOK (priv->notebook));
 
-       if (index_ <= 0) {
+       if (index_ <= 0 && wrap_around) {
                gtk_notebook_set_current_page (GTK_NOTEBOOK (priv->notebook), numPages - 1);
                return;
        }
@@ -916,6 +1070,14 @@ chat_window_tabs_previous_activate_cb (GtkAction         *action,
        gtk_notebook_prev_page (GTK_NOTEBOOK (priv->notebook));
 }
 
+static void
+chat_window_tabs_undo_close_tab_activate_cb (GtkAction         *action,
+                                            EmpathyChatWindow *window)
+{
+       EmpathyChatWindowPriv *priv = GET_PRIV (window);
+       empathy_chat_manager_undo_closed_chat (priv->chat_manager);
+}
+
 static void
 chat_window_tabs_left_activate_cb (GtkAction         *action,
                                   EmpathyChatWindow *window)
@@ -1036,7 +1198,6 @@ chat_window_set_urgency_hint (EmpathyChatWindow *window,
 
        priv = GET_PRIV (window);
 
-       DEBUG ("Turning %s urgency hint", urgent ? "on" : "off");
        gtk_window_set_urgency_hint (GTK_WINDOW (priv->dialog), urgent);
 }
 
@@ -1172,6 +1333,10 @@ chat_window_new_message_cb (EmpathyChat       *chat,
        }
 
        if (has_focus && priv->current_chat == chat) {
+               /* window and tab are focused so consider the message to be read */
+
+               /* FIXME: see Bug#610994 and coments about it in EmpathyChatPriv */
+               empathy_chat_messages_read (chat);
                return;
        }
 
@@ -1202,6 +1367,9 @@ chat_window_new_message_cb (EmpathyChat       *chat,
                    EMPATHY_SOUND_MESSAGE_INCOMING);
                chat_window_show_or_update_notification (window, message, chat);
        }
+
+       /* update the number of unread messages */
+       chat_window_title_update (priv);
 }
 
 static GtkNotebook *
@@ -1258,6 +1426,7 @@ chat_window_page_switched_cb (GtkNotebook      *notebook,
 
        priv->current_chat = chat;
        priv->chats_new_msg = g_list_remove (priv->chats_new_msg, chat);
+       empathy_chat_messages_read (chat);
 
        chat_window_update_chat_tab (chat);
 }
@@ -1349,6 +1518,7 @@ chat_window_page_removed_cb (GtkNotebook      *notebook,
        /* Keep list of chats up to date */
        priv->chats = g_list_remove (priv->chats, chat);
        priv->chats_new_msg = g_list_remove (priv->chats_new_msg, chat);
+       empathy_chat_messages_read (chat);
        priv->chats_composing = g_list_remove (priv->chats_composing, chat);
 
        if (priv->chats == NULL) {
@@ -1365,11 +1535,10 @@ chat_window_focus_in_event_cb (GtkWidget        *widget,
 {
        EmpathyChatWindowPriv *priv;
 
-       DEBUG ("Focus in event, updating title");
-
        priv = GET_PRIV (window);
 
        priv->chats_new_msg = g_list_remove (priv->chats_new_msg, priv->current_chat);
+       empathy_chat_messages_read (priv->current_chat);
 
        chat_window_set_urgency_hint (window, FALSE);
 
@@ -1379,6 +1548,88 @@ chat_window_focus_in_event_cb (GtkWidget        *widget,
        return FALSE;
 }
 
+static gboolean
+chat_window_drag_drop (GtkWidget        *widget,
+                        GdkDragContext   *context,
+                        int               x,
+                        int               y,
+                        guint             time_,
+                        EmpathyChatWindow *window)
+{
+       GdkAtom target;
+       EmpathyChatWindowPriv *priv;
+
+       priv = GET_PRIV (window);
+
+       target = gtk_drag_dest_find_target (widget, context, priv->file_targets);
+       if (target == GDK_NONE)
+               target = gtk_drag_dest_find_target (widget, context, priv->contact_targets);
+
+       if (target != GDK_NONE) {
+               gtk_drag_get_data (widget, context, target, time_);
+               return TRUE;
+       }
+
+       return FALSE;
+}
+
+static gboolean
+chat_window_drag_motion (GtkWidget        *widget,
+                        GdkDragContext   *context,
+                        int               x,
+                        int               y,
+                        guint             time_,
+                        EmpathyChatWindow *window)
+{
+       GdkAtom target;
+       EmpathyChatWindowPriv *priv;
+
+       priv = GET_PRIV (window);
+
+       target = gtk_drag_dest_find_target (widget, context, priv->file_targets);
+       if (target != GDK_NONE) {
+               /* This is a file drag.  Ensure the contact is online and set the
+                  drag type to COPY.  Note that it's possible that the tab will
+                  be switched by GTK+ after a timeout from drag_motion without
+                  getting another drag_motion to disable the drop.  You have
+                  to hold your mouse really still.
+                */
+               EmpathyContact *contact;
+
+               priv = GET_PRIV (window);
+               contact = empathy_chat_get_remote_contact (priv->current_chat);
+               /* contact is NULL for multi-user chats.  We don't do
+                * file transfers to MUCs.  We also don't send files
+                * to offline contacts or contacts that don't support
+                * file transfer.
+                */
+               if ((contact == NULL) || !empathy_contact_is_online (contact)) {
+                       gdk_drag_status (context, 0, time_);
+                       return FALSE;
+               }
+               if (!(empathy_contact_get_capabilities (contact)
+                          & EMPATHY_CAPABILITIES_FT)) {
+                       gdk_drag_status (context, 0, time_);
+                       return FALSE;
+               }
+               gdk_drag_status (context, GDK_ACTION_COPY, time_);
+               return TRUE;
+       }
+
+       target = gtk_drag_dest_find_target (widget, context, priv->contact_targets);
+       if (target != GDK_NONE) {
+               /* This is a drag of a contact from a contact list.  Set to COPY.
+                  FIXME: If this drag is to a MUC window, it invites the user.
+                  Otherwise, it opens a chat.  Should we use a different drag
+                  type for invites?  Should we allow ASK?
+                */
+               gdk_drag_status (context, GDK_ACTION_COPY, time_);
+               return TRUE;
+       }
+
+       return FALSE;
+}
+
 static void
 chat_window_drag_data_received (GtkWidget        *widget,
                                GdkDragContext   *context,
@@ -1461,6 +1712,27 @@ chat_window_drag_data_received (GtkWidget        *widget,
                 */
                gtk_drag_finish (context, TRUE, FALSE, time_);
        }
+       else if (info == DND_DRAG_TYPE_URI_LIST) {
+               EmpathyChatWindowPriv *priv;
+               EmpathyContact *contact;
+               const gchar *data;
+
+               priv = GET_PRIV (window);
+               contact = empathy_chat_get_remote_contact (priv->current_chat);
+
+               /* contact is NULL when current_chat is a multi-user chat.
+                * We don't do file transfers to MUCs, so just cancel the drag.
+                */
+               if (contact == NULL) {
+                       gtk_drag_finish (context, TRUE, FALSE, time_);
+                       return;
+               }
+
+               data = (const gchar *) gtk_selection_data_get_data (selection);
+               empathy_send_file_from_uri_list (contact, data);
+
+               gtk_drag_finish (context, TRUE, FALSE, time_);
+       }
        else if (info == DND_DRAG_TYPE_TAB) {
                EmpathyChat        **chat;
                EmpathyChatWindow   *old_window = NULL;
@@ -1474,29 +1746,27 @@ chat_window_drag_data_received (GtkWidget        *widget,
                        EmpathyChatWindowPriv *priv;
 
                        priv = GET_PRIV (window);
-
-                       if (old_window == window) {
-                               DEBUG ("DND tab (within same window)");
-                               priv->dnd_same_window = TRUE;
-                               gtk_drag_finish (context, TRUE, FALSE, time_);
-                               return;
-                       }
-
-                       priv->dnd_same_window = FALSE;
+                       priv->dnd_same_window = (old_window == window);
+                       DEBUG ("DND tab (within same window: %s)",
+                               priv->dnd_same_window ? "Yes" : "No");
                }
-
-               /* We should return TRUE to remove the data when doing
-                * GDK_ACTION_MOVE, but we don't here otherwise it has
-                * weird consequences, and we handle that internally
-                * anyway with add_chat () and remove_chat ().
-                */
-               gtk_drag_finish (context, TRUE, FALSE, time_);
        } else {
                DEBUG ("DND from unknown source");
                gtk_drag_finish (context, FALSE, FALSE, time_);
        }
 }
 
+static void
+chat_window_chat_manager_chats_changed_cb (EmpathyChatManager *chat_manager,
+                                          guint num_chats_in_manager,
+                                          EmpathyChatWindow *window)
+{
+       EmpathyChatWindowPriv *priv = GET_PRIV (window);
+
+       gtk_action_set_sensitive (priv->menu_tabs_undo_close_tab,
+                                 num_chats_in_manager > 0);
+}
+
 static void
 chat_window_finalize (GObject *object)
 {
@@ -1511,10 +1781,6 @@ chat_window_finalize (GObject *object)
        g_object_unref (priv->ui_manager);
        g_object_unref (priv->chatroom_manager);
        g_object_unref (priv->notify_mgr);
-       if (priv->save_geometry_id != 0) {
-               g_source_remove (priv->save_geometry_id);
-               chat_window_save_geometry_timeout_cb (window);
-       }
 
        if (priv->notification != NULL) {
                notify_notification_close (priv->notification, NULL);
@@ -1527,6 +1793,20 @@ chat_window_finalize (GObject *object)
                        }
        }
 
+       if (priv->contact_targets) {
+               gtk_target_list_unref (priv->contact_targets);
+       }
+       if (priv->file_targets) {
+               gtk_target_list_unref (priv->file_targets);
+       }
+
+       if (priv->chat_manager) {
+               g_signal_handler_disconnect (priv->chat_manager,
+                                            priv->chat_manager_chats_changed_id);
+               g_object_unref (priv->chat_manager);
+               priv->chat_manager = NULL;
+       }
+
        chat_windows = g_list_remove (chat_windows, window);
        gtk_widget_destroy (priv->dialog);
 
@@ -1582,8 +1862,10 @@ empathy_chat_window_init (EmpathyChatWindow *window)
                                       "menu_edit_cut", &priv->menu_edit_cut,
                                       "menu_edit_copy", &priv->menu_edit_copy,
                                       "menu_edit_paste", &priv->menu_edit_paste,
+                                      "menu_edit_find", &priv->menu_edit_find,
                                       "menu_tabs_next", &priv->menu_tabs_next,
                                       "menu_tabs_prev", &priv->menu_tabs_prev,
+                                      "menu_tabs_undo_close_tab", &priv->menu_tabs_undo_close_tab,
                                       "menu_tabs_left", &priv->menu_tabs_left,
                                       "menu_tabs_right", &priv->menu_tabs_right,
                                       "menu_tabs_detach", &priv->menu_tabs_detach,
@@ -1591,18 +1873,20 @@ empathy_chat_window_init (EmpathyChatWindow *window)
        g_free (filename);
 
        empathy_builder_connect (gui, window,
-                             "chat_window", "configure-event", chat_window_configure_event_cb,
                              "menu_conv", "activate", chat_window_conv_activate_cb,
                              "menu_conv_clear", "activate", chat_window_clear_activate_cb,
                              "menu_conv_favorite", "toggled", chat_window_favorite_toggled_cb,
                              "menu_conv_toggle_contacts", "toggled", chat_window_contacts_toggled_cb,
+                             "menu_conv_invite_participant", "activate", chat_window_invite_participant_activate_cb,
                              "menu_conv_close", "activate", chat_window_close_activate_cb,
                              "menu_edit", "activate", chat_window_edit_activate_cb,
                              "menu_edit_cut", "activate", chat_window_cut_activate_cb,
                              "menu_edit_copy", "activate", chat_window_copy_activate_cb,
                              "menu_edit_paste", "activate", chat_window_paste_activate_cb,
+                             "menu_edit_find", "activate", chat_window_find_activate_cb,
                              "menu_tabs_next", "activate", chat_window_tabs_next_activate_cb,
                              "menu_tabs_prev", "activate", chat_window_tabs_previous_activate_cb,
+                             "menu_tabs_undo_close_tab", "activate", chat_window_tabs_undo_close_tab_activate_cb,
                              "menu_tabs_left", "activate", chat_window_tabs_left_activate_cb,
                              "menu_tabs_right", "activate", chat_window_tabs_right_activate_cb,
                              "menu_tabs_detach", "activate", chat_window_detach_activate_cb,
@@ -1639,6 +1923,12 @@ empathy_chat_window_init (EmpathyChatWindow *window)
 
        g_object_unref (accel_group);
 
+       /* Set up drag target lists */
+       priv->contact_targets = gtk_target_list_new (drag_types_dest_contact,
+                                                    G_N_ELEMENTS (drag_types_dest_contact));
+       priv->file_targets = gtk_target_list_new (drag_types_dest_file,
+                                                 G_N_ELEMENTS (drag_types_dest_file));
+
        /* Set up smiley menu */
        smiley_manager = empathy_smiley_manager_dup_singleton ();
        submenu = empathy_smiley_menu_new (smiley_manager,
@@ -1676,15 +1966,24 @@ empathy_chat_window_init (EmpathyChatWindow *window)
 
        /* Set up drag and drop */
        gtk_drag_dest_set (GTK_WIDGET (priv->notebook),
-                          GTK_DEST_DEFAULT_ALL,
+                          GTK_DEST_DEFAULT_HIGHLIGHT,
                           drag_types_dest,
                           G_N_ELEMENTS (drag_types_dest),
-                          GDK_ACTION_MOVE);
+                          GDK_ACTION_MOVE | GDK_ACTION_COPY);
 
+       /* connect_after to allow GtkNotebook's built-in tab switching */
+       g_signal_connect_after (priv->notebook,
+                               "drag-motion",
+                               G_CALLBACK (chat_window_drag_motion),
+                               window);
        g_signal_connect (priv->notebook,
                          "drag-data-received",
                          G_CALLBACK (chat_window_drag_data_received),
                          window);
+       g_signal_connect (priv->notebook,
+                         "drag-drop",
+                         G_CALLBACK (chat_window_drag_drop),
+                         window);
 
        chat_windows = g_list_prepend (chat_windows, window);
 
@@ -1695,6 +1994,16 @@ empathy_chat_window_init (EmpathyChatWindow *window)
        priv->current_chat = NULL;
 
        priv->notify_mgr = empathy_notify_manager_dup_singleton ();
+
+       priv->chat_manager = empathy_chat_manager_dup_singleton ();
+       priv->chat_manager_chats_changed_id =
+               g_signal_connect (priv->chat_manager, "chats-changed",
+                                 G_CALLBACK (chat_window_chat_manager_chats_changed_cb),
+                                 window);
+
+       chat_window_chat_manager_chats_changed_cb (priv->chat_manager,
+                                                  empathy_chat_manager_get_num_chats (priv->chat_manager),
+                                                  window);
 }
 
 EmpathyChatWindow *
@@ -1708,7 +2017,7 @@ empathy_chat_window_new (void)
  * be added.
  */
 EmpathyChatWindow *
-empathy_chat_window_get_default (void)
+empathy_chat_window_get_default (gboolean room)
 {
        GList    *l;
        gboolean  separate_windows = TRUE;
@@ -1723,13 +2032,26 @@ empathy_chat_window_get_default (void)
        }
 
        for (l = chat_windows; l; l = l->next) {
+               EmpathyChatWindowPriv *priv;
                EmpathyChatWindow *chat_window;
                GtkWidget         *dialog;
 
                chat_window = l->data;
+               priv = GET_PRIV (chat_window);
 
                dialog = empathy_chat_window_get_dialog (chat_window);
                if (empathy_window_get_is_visible (GTK_WINDOW (dialog))) {
+                       guint nb_rooms, nb_private;
+                       empathy_chat_window_get_nb_chats (chat_window, &nb_rooms, &nb_private);
+
+                       /* Skip the window if there aren't any rooms in it */
+                       if (room && nb_rooms == 0)
+                               continue;
+
+                       /* Skip the window if there aren't any 1-1 chats in it */
+                       if (!room && nb_private == 0)
+                               continue;
+
                        /* Found a visible window on this desktop */
                        return chat_window;
                }
@@ -1758,7 +2080,7 @@ empathy_chat_window_add_chat (EmpathyChatWindow *window,
        GtkWidget             *label;
        GtkWidget             *popup_label;
        GtkWidget             *child;
-       gint                   x, y, w, h;
+       GValue                value = { 0, };
 
        g_return_if_fail (window != NULL);
        g_return_if_fail (EMPATHY_IS_CHAT (chat));
@@ -1770,21 +2092,21 @@ empathy_chat_window_add_chat (EmpathyChatWindow *window,
 
        /* If this window has just been created, position it */
        if (priv->chats == NULL) {
-               empathy_geometry_load (chat_get_window_id_for_geometry (chat), &x, &y, &w, &h);
+               const gchar *name = "chat-window";
+               gboolean     separate_windows;
 
-               if (x >= 0 && y >= 0) {
-                       /* Let the window manager position it if we don't have
-                        * good x, y coordinates.
-                        */
-                       gtk_window_move (GTK_WINDOW (priv->dialog), x, y);
-               }
+               empathy_conf_get_bool (empathy_conf_get (),
+                                      EMPATHY_PREFS_UI_SEPARATE_CHAT_WINDOWS,
+                                      &separate_windows);
 
-               if (w > 0 && h > 0) {
-                       /* Use the defaults from the ui file if we don't have
-                        * good w, h geometry.
-                        */
-                       gtk_window_resize (GTK_WINDOW (priv->dialog), w, h);
+               if (separate_windows) {
+                       name = empathy_chat_get_id (chat);
+               }
+               else if (empathy_chat_is_room (chat)) {
+                       name = "room-window";
                }
+
+               empathy_geometry_bind (GTK_WINDOW (priv->dialog), name);
        }
 
        child = GTK_WIDGET (chat);
@@ -1806,8 +2128,13 @@ empathy_chat_window_add_chat (EmpathyChatWindow *window,
        gtk_notebook_append_page_menu (GTK_NOTEBOOK (priv->notebook), child, label, popup_label);
        gtk_notebook_set_tab_reorderable (GTK_NOTEBOOK (priv->notebook), child, TRUE);
        gtk_notebook_set_tab_detachable (GTK_NOTEBOOK (priv->notebook), child, TRUE);
-       gtk_notebook_set_tab_label_packing (GTK_NOTEBOOK (priv->notebook), child,
-                                           TRUE, TRUE, GTK_PACK_START);
+       g_value_init (&value, G_TYPE_BOOLEAN);
+       g_value_set_boolean (&value, TRUE);
+       gtk_container_child_set_property (GTK_CONTAINER (priv->notebook),
+                                         child, "tab-expand" , &value);
+       gtk_container_child_set_property (GTK_CONTAINER (priv->notebook),
+                                         child,  "tab-fill" , &value);
+       g_value_unset (&value);
 
        DEBUG ("Chat added (%d references)", G_OBJECT (chat)->ref_count);
 }
@@ -1819,6 +2146,7 @@ empathy_chat_window_remove_chat (EmpathyChatWindow *window,
        EmpathyChatWindowPriv *priv;
        gint                   position;
        EmpathyContact        *remote_contact;
+       EmpathyChatManager    *chat_manager;
 
        g_return_if_fail (window != NULL);
        g_return_if_fail (EMPATHY_IS_CHAT (chat));
@@ -1836,6 +2164,10 @@ empathy_chat_window_remove_chat (EmpathyChatWindow *window,
                                                      chat);
        }
 
+       chat_manager = empathy_chat_manager_dup_singleton ();
+       empathy_chat_manager_closed_chat (chat_manager, chat);
+       g_object_unref (chat_manager);
+
        position = gtk_notebook_page_num (GTK_NOTEBOOK (priv->notebook),
                                          GTK_WIDGET (chat));
        gtk_notebook_remove_page (GTK_NOTEBOOK (priv->notebook), position);
@@ -1950,7 +2282,7 @@ empathy_chat_window_present_chat (EmpathyChat *chat)
 
        /* If the chat has no window, create one */
        if (window == NULL) {
-               window = empathy_chat_window_get_default ();
+               window = empathy_chat_window_get_default (empathy_chat_is_room (chat));
                if (!window) {
                        window = empathy_chat_window_new ();
                }
@@ -1960,8 +2292,29 @@ empathy_chat_window_present_chat (EmpathyChat *chat)
 
        priv = GET_PRIV (window);
        empathy_chat_window_switch_to_chat (window, chat);
-       empathy_window_present (GTK_WINDOW (priv->dialog), TRUE);
+       empathy_window_present (GTK_WINDOW (priv->dialog));
 
        gtk_widget_grab_focus (chat->input_text_view);
 }
 
+void
+empathy_chat_window_get_nb_chats (EmpathyChatWindow *self,
+                              guint *nb_rooms,
+                              guint *nb_private)
+{
+       EmpathyChatWindowPriv *priv = GET_PRIV (self);
+       GList *l;
+       guint _nb_rooms = 0, _nb_private = 0;
+
+       for (l = priv->chats; l != NULL; l = g_list_next (l)) {
+               if (empathy_chat_is_room (EMPATHY_CHAT (l->data)))
+                       _nb_rooms++;
+               else
+                       _nb_private++;
+       }
+
+       if (nb_rooms != NULL)
+               *nb_rooms = _nb_rooms;
+       if (nb_private != NULL)
+               *nb_private = _nb_private;
+}