]> git.0d.be Git - empathy.git/blob - libempathy-gtk/empathy-contact-list-view.c
Merge branch 'sasl'
[empathy.git] / libempathy-gtk / empathy-contact-list-view.c
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3  * Copyright (C) 2005-2007 Imendio AB
4  * Copyright (C) 2007-2008 Collabora Ltd.
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU General Public License as
8  * published by the Free Software Foundation; either version 2 of the
9  * License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public
17  * License along with this program; if not, write to the
18  * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
19  * Boston, MA  02110-1301  USA
20  *
21  * Authors: Mikael Hallendal <micke@imendio.com>
22  *          Martyn Russell <martyn@imendio.com>
23  *          Xavier Claessens <xclaesse@gmail.com>
24  */
25
26 #include "config.h"
27
28 #include <string.h>
29
30 #include <glib/gi18n-lib.h>
31 #include <gdk/gdkkeysyms.h>
32 #include <gtk/gtk.h>
33
34 #include <telepathy-glib/account-manager.h>
35 #include <telepathy-glib/util.h>
36
37 #include <libempathy/empathy-call-factory.h>
38 #include <libempathy/empathy-tp-contact-factory.h>
39 #include <libempathy/empathy-contact-list.h>
40 #include <libempathy/empathy-contact-groups.h>
41 #include <libempathy/empathy-dispatcher.h>
42 #include <libempathy/empathy-utils.h>
43
44 #include "empathy-contact-list-view.h"
45 #include "empathy-contact-list-store.h"
46 #include "empathy-images.h"
47 #include "empathy-cell-renderer-expander.h"
48 #include "empathy-cell-renderer-text.h"
49 #include "empathy-cell-renderer-activatable.h"
50 #include "empathy-ui-utils.h"
51 #include "empathy-gtk-enum-types.h"
52 #include "empathy-gtk-marshal.h"
53
54 #define DEBUG_FLAG EMPATHY_DEBUG_CONTACT
55 #include <libempathy/empathy-debug.h>
56
57 /* Active users are those which have recently changed state
58  * (e.g. online, offline or from normal to a busy state).
59  */
60
61 #define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyContactListView)
62 typedef struct {
63         EmpathyContactListStore        *store;
64         GtkTreeRowReference            *drag_row;
65         EmpathyContactListFeatureFlags  list_features;
66         EmpathyContactFeatureFlags      contact_features;
67         GtkWidget                      *tooltip_widget;
68         GtkTargetList                  *file_targets;
69
70         GtkTreeModelFilter             *filter;
71         GtkWidget                      *search_widget;
72 } EmpathyContactListViewPriv;
73
74 typedef struct {
75         EmpathyContactListView *view;
76         GtkTreePath           *path;
77         guint                  timeout_id;
78 } DragMotionData;
79
80 typedef struct {
81         EmpathyContactListView *view;
82         EmpathyContact         *contact;
83         gboolean               remove;
84 } ShowActiveData;
85
86 enum {
87         PROP_0,
88         PROP_STORE,
89         PROP_LIST_FEATURES,
90         PROP_CONTACT_FEATURES,
91 };
92
93 enum DndDragType {
94         DND_DRAG_TYPE_CONTACT_ID,
95         DND_DRAG_TYPE_URI_LIST,
96         DND_DRAG_TYPE_STRING,
97 };
98
99 static const GtkTargetEntry drag_types_dest[] = {
100         { "text/path-list",  0, DND_DRAG_TYPE_URI_LIST },
101         { "text/uri-list",   0, DND_DRAG_TYPE_URI_LIST },
102         { "text/contact-id", 0, DND_DRAG_TYPE_CONTACT_ID },
103         { "text/plain",      0, DND_DRAG_TYPE_STRING },
104         { "STRING",          0, DND_DRAG_TYPE_STRING },
105 };
106
107 static const GtkTargetEntry drag_types_dest_file[] = {
108         { "text/path-list",  0, DND_DRAG_TYPE_URI_LIST },
109         { "text/uri-list",   0, DND_DRAG_TYPE_URI_LIST },
110 };
111
112 static const GtkTargetEntry drag_types_source[] = {
113         { "text/contact-id", 0, DND_DRAG_TYPE_CONTACT_ID },
114 };
115
116 static GdkAtom drag_atoms_dest[G_N_ELEMENTS (drag_types_dest)];
117 static GdkAtom drag_atoms_source[G_N_ELEMENTS (drag_types_source)];
118
119 enum {
120         DRAG_CONTACT_RECEIVED,
121         LAST_SIGNAL
122 };
123
124 static guint signals[LAST_SIGNAL];
125
126 G_DEFINE_TYPE (EmpathyContactListView, empathy_contact_list_view, GTK_TYPE_TREE_VIEW);
127
128 static void
129 contact_list_view_tooltip_destroy_cb (GtkWidget              *widget,
130                                       EmpathyContactListView *view)
131 {
132         EmpathyContactListViewPriv *priv = GET_PRIV (view);
133
134         if (priv->tooltip_widget) {
135                 DEBUG ("Tooltip destroyed");
136                 g_object_unref (priv->tooltip_widget);
137                 priv->tooltip_widget = NULL;
138         }
139 }
140
141 static gboolean
142 contact_list_view_is_visible_contact (EmpathyContactListView *self,
143                                       EmpathyContact *contact)
144 {
145         EmpathyContactListViewPriv *priv = GET_PRIV (self);
146         EmpathyLiveSearch *live = EMPATHY_LIVE_SEARCH (priv->search_widget);
147         const gchar *str;
148         const gchar *p;
149         gchar *dup_str = NULL;
150         gboolean visible;
151
152         g_assert (live != NULL);
153
154         /* check alias name */
155         str = empathy_contact_get_alias (contact);
156         if (empathy_live_search_match (live, str))
157                 return TRUE;
158
159         /* check contact id, remove the @server.com part */
160         str = empathy_contact_get_id (contact);
161         p = strstr (str, "@");
162         if (p != NULL)
163                 str = dup_str = g_strndup (str, p - str);
164
165         visible = empathy_live_search_match (live, str);
166         g_free (dup_str);
167         if (visible)
168                 return TRUE;
169
170         /* FIXME: Add more rules here, we could check phone numbers in
171          * contact's vCard for example. */
172
173         return FALSE;
174 }
175
176 static gboolean
177 contact_list_view_filter_visible_func (GtkTreeModel *model,
178                                        GtkTreeIter  *iter,
179                                        gpointer      user_data)
180 {
181         EmpathyContactListView     *self = EMPATHY_CONTACT_LIST_VIEW (user_data);
182         EmpathyContactListViewPriv *priv = GET_PRIV (self);
183         EmpathyContact             *contact = NULL;
184         gboolean                    is_group, is_separator, valid;
185         GtkTreeIter                 child_iter;
186         gboolean                    visible;
187
188         if (priv->search_widget == NULL ||
189             !gtk_widget_get_visible (priv->search_widget))
190                 return TRUE;
191
192         gtk_tree_model_get (model, iter,
193                 EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
194                 EMPATHY_CONTACT_LIST_STORE_COL_IS_SEPARATOR, &is_separator,
195                 EMPATHY_CONTACT_LIST_STORE_COL_CONTACT, &contact,
196                 -1);
197
198         if (contact != NULL) {
199                 visible = contact_list_view_is_visible_contact (self, contact);
200                 g_object_unref (contact);
201                 return visible;
202         }
203
204         if (is_separator) {
205                 return TRUE;
206         }
207
208         /* Not a contact, not a separator, must be a group */
209         g_return_val_if_fail (is_group, FALSE);
210
211         /* only show groups which are not empty */
212         for (valid = gtk_tree_model_iter_children (model, &child_iter, iter);
213              valid; valid = gtk_tree_model_iter_next (model, &child_iter)) {
214                 gtk_tree_model_get (model, &child_iter,
215                         EMPATHY_CONTACT_LIST_STORE_COL_CONTACT, &contact,
216                         -1);
217
218                 if (contact == NULL)
219                         continue;
220
221                 visible = contact_list_view_is_visible_contact (self, contact);
222                 g_object_unref (contact);
223
224                 /* show group if it has at least one visible contact in it */
225                 if (visible)
226                         return TRUE;
227         }
228
229         return FALSE;
230 }
231
232 static gboolean
233 contact_list_view_query_tooltip_cb (EmpathyContactListView *view,
234                                     gint                    x,
235                                     gint                    y,
236                                     gboolean                keyboard_mode,
237                                     GtkTooltip             *tooltip,
238                                     gpointer                user_data)
239 {
240         EmpathyContactListViewPriv *priv = GET_PRIV (view);
241         EmpathyContact             *contact;
242         GtkTreeModel               *model;
243         GtkTreeIter                 iter;
244         GtkTreePath                *path;
245         static gint                 running = 0;
246         gboolean                    ret = FALSE;
247
248         /* Avoid an infinite loop. See GNOME bug #574377 */
249         if (running > 0) {
250                 return FALSE;
251         }
252         running++;
253
254         /* Don't show the tooltip if there's already a popup menu */
255         if (gtk_menu_get_for_attach_widget (GTK_WIDGET (view)) != NULL) {
256                 goto OUT;
257         }
258
259         if (!gtk_tree_view_get_tooltip_context (GTK_TREE_VIEW (view), &x, &y,
260                                                 keyboard_mode,
261                                                 &model, &path, &iter)) {
262                 goto OUT;
263         }
264
265         gtk_tree_view_set_tooltip_row (GTK_TREE_VIEW (view), tooltip, path);
266         gtk_tree_path_free (path);
267
268         gtk_tree_model_get (model, &iter,
269                             EMPATHY_CONTACT_LIST_STORE_COL_CONTACT, &contact,
270                             -1);
271         if (!contact) {
272                 goto OUT;
273         }
274
275         if (!priv->tooltip_widget) {
276                 priv->tooltip_widget = empathy_contact_widget_new (contact,
277                         EMPATHY_CONTACT_WIDGET_FOR_TOOLTIP |
278                         EMPATHY_CONTACT_WIDGET_SHOW_LOCATION);
279                 gtk_container_set_border_width (
280                         GTK_CONTAINER (priv->tooltip_widget), 8);
281                 g_object_ref (priv->tooltip_widget);
282                 g_signal_connect (priv->tooltip_widget, "destroy",
283                                   G_CALLBACK (contact_list_view_tooltip_destroy_cb),
284                                   view);
285                 gtk_widget_show (priv->tooltip_widget);
286         } else {
287                 empathy_contact_widget_set_contact (priv->tooltip_widget,
288                                                     contact);
289         }
290
291         gtk_tooltip_set_custom (tooltip, priv->tooltip_widget);
292         ret = TRUE;
293
294         g_object_unref (contact);
295 OUT:
296         running--;
297
298         return ret;
299 }
300
301 typedef struct {
302         gchar *new_group;
303         gchar *old_group;
304         GdkDragAction action;
305 } DndGetContactData;
306
307 static void
308 contact_list_view_dnd_get_contact_free (DndGetContactData *data)
309 {
310         g_free (data->new_group);
311         g_free (data->old_group);
312         g_slice_free (DndGetContactData, data);
313 }
314
315 static void
316 contact_list_view_drag_got_contact (TpConnection            *connection,
317                                     EmpathyContact          *contact,
318                                     const GError            *error,
319                                     gpointer                 user_data,
320                                     GObject                 *view)
321 {
322         EmpathyContactListViewPriv *priv = GET_PRIV (view);
323         DndGetContactData          *data = user_data;
324         EmpathyContactList         *list;
325
326         if (error != NULL) {
327                 DEBUG ("Error: %s", error->message);
328                 return;
329         }
330
331         DEBUG ("contact %s (%d) dragged from '%s' to '%s'",
332                 empathy_contact_get_id (contact),
333                 empathy_contact_get_handle (contact),
334                 data->old_group, data->new_group);
335
336         list = empathy_contact_list_store_get_list_iface (priv->store);
337
338         if (!tp_strdiff (data->new_group, EMPATHY_CONTACT_LIST_STORE_FAVORITE)) {
339                 /* Mark contact as favourite */
340                 empathy_contact_list_add_to_favourites (list, contact);
341                 return;
342         }
343
344         if (!tp_strdiff (data->old_group, EMPATHY_CONTACT_LIST_STORE_FAVORITE)) {
345                 /* Remove contact as favourite */
346                 empathy_contact_list_remove_from_favourites (list, contact);
347                 /* Don't try to remove it */
348                 g_free (data->old_group);
349                 data->old_group = NULL;
350         }
351
352         if (data->new_group) {
353                 empathy_contact_list_add_to_group (list, contact, data->new_group);
354         }
355         if (data->old_group && data->action == GDK_ACTION_MOVE) {
356                 empathy_contact_list_remove_from_group (list, contact, data->old_group);
357         }
358 }
359
360 static gboolean
361 group_can_be_modified (const gchar *name,
362                        gboolean     is_fake_group,
363                        gboolean     adding)
364 {
365         /* Real groups can always be modified */
366         if (!is_fake_group)
367                 return TRUE;
368
369         /* The favorite fake group can be modified so users can
370          * add/remove favorites using DnD */
371         if (!tp_strdiff (name, EMPATHY_CONTACT_LIST_STORE_FAVORITE))
372                 return TRUE;
373
374         /* We can remove contacts from the 'ungrouped' fake group */
375         if (!adding && !tp_strdiff (name, EMPATHY_CONTACT_LIST_STORE_UNGROUPED))
376                 return TRUE;
377
378         return FALSE;
379 }
380
381 static gboolean
382 contact_list_view_contact_drag_received (GtkWidget         *view,
383                                          GdkDragContext    *context,
384                                          GtkTreeModel      *model,
385                                          GtkTreePath       *path,
386                                          GtkSelectionData  *selection)
387 {
388         EmpathyContactListViewPriv *priv;
389         TpAccountManager           *account_manager;
390         TpConnection               *connection;
391         TpAccount                  *account;
392         DndGetContactData          *data;
393         GtkTreePath                *source_path;
394         const gchar   *sel_data;
395         gchar        **strv = NULL;
396         const gchar   *account_id = NULL;
397         const gchar   *contact_id = NULL;
398         gchar         *new_group = NULL;
399         gchar         *old_group = NULL;
400         gboolean       success = TRUE;
401         gboolean       new_group_is_fake, old_group_is_fake = TRUE;
402
403         priv = GET_PRIV (view);
404
405         sel_data = (const gchar *) gtk_selection_data_get_data (selection);
406         new_group = empathy_contact_list_store_get_parent_group (model,
407                                                                  path, NULL, &new_group_is_fake);
408
409         if (!group_can_be_modified (new_group, new_group_is_fake, TRUE))
410                 return FALSE;
411
412         /* Get source group information. */
413         if (priv->drag_row) {
414                 source_path = gtk_tree_row_reference_get_path (priv->drag_row);
415                 if (source_path) {
416                         old_group = empathy_contact_list_store_get_parent_group (
417                                                                                  model, source_path, NULL, &old_group_is_fake);
418                         gtk_tree_path_free (source_path);
419                 }
420         }
421
422         if (!group_can_be_modified (old_group, old_group_is_fake, FALSE))
423                 return FALSE;
424
425         if (!tp_strdiff (old_group, new_group)) {
426                 g_free (new_group);
427                 g_free (old_group);
428                 return FALSE;
429         }
430
431         account_manager = tp_account_manager_dup ();
432         strv = g_strsplit (sel_data, ":", 2);
433         if (g_strv_length (strv) == 2) {
434                 account_id = strv[0];
435                 contact_id = strv[1];
436                 account = tp_account_manager_ensure_account (account_manager, account_id);
437         }
438         if (account) {
439                 connection = tp_account_get_connection (account);
440         }
441
442         if (!connection) {
443                 DEBUG ("Failed to get connection for account '%s'", account_id);
444                 success = FALSE;
445                 g_free (new_group);
446                 g_free (old_group);
447                 g_object_unref (account_manager);
448                 return FALSE;
449         }
450
451         data = g_slice_new0 (DndGetContactData);
452         data->new_group = new_group;
453         data->old_group = old_group;
454         data->action = gdk_drag_context_get_selected_action (context);
455
456         /* FIXME: We should probably wait for the cb before calling
457          * gtk_drag_finish */
458         empathy_tp_contact_factory_get_from_id (connection, contact_id,
459                                                 contact_list_view_drag_got_contact,
460                                                 data, (GDestroyNotify) contact_list_view_dnd_get_contact_free,
461                                                 G_OBJECT (view));
462         g_strfreev (strv);
463         g_object_unref (account_manager);
464
465         return TRUE;
466 }
467
468 static gboolean
469 contact_list_view_file_drag_received (GtkWidget         *view,
470                                       GdkDragContext    *context,
471                                       GtkTreeModel      *model,
472                                       GtkTreePath       *path,
473                                       GtkSelectionData  *selection)
474 {
475         GtkTreeIter     iter;
476         const gchar    *sel_data;
477         EmpathyContact *contact;
478
479         sel_data = (const gchar *) gtk_selection_data_get_data (selection);
480
481         gtk_tree_model_get_iter (model, &iter, path);
482         gtk_tree_model_get (model, &iter,
483                             EMPATHY_CONTACT_LIST_STORE_COL_CONTACT, &contact,
484                             -1);
485         if (!contact) {
486                 return FALSE;
487         }
488
489         empathy_send_file_from_uri_list (contact, sel_data);
490
491         g_object_unref (contact);
492
493         return TRUE;
494 }
495
496 static void
497 contact_list_view_drag_data_received (GtkWidget         *view,
498                                       GdkDragContext    *context,
499                                       gint               x,
500                                       gint               y,
501                                       GtkSelectionData  *selection,
502                                       guint              info,
503                                       guint              time_)
504 {
505         GtkTreeModel               *model;
506         gboolean                    is_row;
507         GtkTreeViewDropPosition     position;
508         GtkTreePath                *path;
509         gboolean                    success = TRUE;
510
511         model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
512
513         /* Get destination group information. */
514         is_row = gtk_tree_view_get_dest_row_at_pos (GTK_TREE_VIEW (view),
515                                                     x,
516                                                     y,
517                                                     &path,
518                                                     &position);
519         if (!is_row) {
520                 success = FALSE;
521         }
522         else if (info == DND_DRAG_TYPE_CONTACT_ID || info == DND_DRAG_TYPE_STRING) {
523                 success = contact_list_view_contact_drag_received (view,
524                                                                    context,
525                                                                    model,
526                                                                    path,
527                                                                    selection);
528         }
529         else if (info == DND_DRAG_TYPE_URI_LIST) {
530                 success = contact_list_view_file_drag_received (view,
531                                                                 context,
532                                                                 model,
533                                                                 path,
534                                                                 selection);
535         }
536
537         gtk_tree_path_free (path);
538         gtk_drag_finish (context, success, FALSE, GDK_CURRENT_TIME);
539 }
540
541 static gboolean
542 contact_list_view_drag_motion_cb (DragMotionData *data)
543 {
544         gtk_tree_view_expand_row (GTK_TREE_VIEW (data->view),
545                                   data->path,
546                                   FALSE);
547
548         data->timeout_id = 0;
549
550         return FALSE;
551 }
552
553 static gboolean
554 contact_list_view_drag_motion (GtkWidget      *widget,
555                                GdkDragContext *context,
556                                gint            x,
557                                gint            y,
558                                guint           time_)
559 {
560         EmpathyContactListViewPriv *priv;
561         GtkTreeModel               *model;
562         GdkAtom                target;
563         GtkTreeIter            iter;
564         static DragMotionData *dm = NULL;
565         GtkTreePath           *path;
566         gboolean               is_row;
567         gboolean               is_different = FALSE;
568         gboolean               cleanup = TRUE;
569         gboolean               retval = TRUE;
570
571         priv = GET_PRIV (EMPATHY_CONTACT_LIST_VIEW (widget));
572         model = gtk_tree_view_get_model (GTK_TREE_VIEW (widget));
573
574         is_row = gtk_tree_view_get_path_at_pos (GTK_TREE_VIEW (widget),
575                                                 x,
576                                                 y,
577                                                 &path,
578                                                 NULL,
579                                                 NULL,
580                                                 NULL);
581
582         cleanup &= (!dm);
583
584         if (is_row) {
585                 cleanup &= (dm && gtk_tree_path_compare (dm->path, path) != 0);
586                 is_different = (!dm || (dm && gtk_tree_path_compare (dm->path, path) != 0));
587         } else {
588                 cleanup &= FALSE;
589         }
590
591         if (path == NULL) {
592                 /* Coordinates don't point to an actual row, so make sure the pointer
593                    and highlighting don't indicate that a drag is possible.
594                  */
595                 gdk_drag_status (context, GDK_ACTION_DEFAULT, time_);
596                 gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (widget), NULL, 0);
597                 return FALSE;
598         }
599         target = gtk_drag_dest_find_target (widget, context, priv->file_targets);
600         gtk_tree_model_get_iter (model, &iter, path);
601
602         if (target == GDK_NONE) {
603                 /* If target == GDK_NONE, then we don't have a target that can be
604                    dropped on a contact.  This means a contact drag.  If we're
605                    pointing to a group, highlight it.  Otherwise, if the contact
606                    we're pointing to is in a group, highlight that.  Otherwise,
607                    set the drag position to before the first row for a drag into
608                    the "non-group" at the top.
609                  */
610                 GtkTreeIter  group_iter;
611                 gboolean     is_group;
612                 GtkTreePath *group_path;
613                 gtk_tree_model_get (model, &iter,
614                                     EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
615                                     -1);
616                 if (is_group) {
617                         group_iter = iter;
618                 }
619                 else {
620                         if (gtk_tree_model_iter_parent (model, &group_iter, &iter))
621                                 gtk_tree_model_get (model, &group_iter,
622                                                     EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
623                                                     -1);
624                 }
625                 if (is_group) {
626                         gdk_drag_status (context, GDK_ACTION_MOVE, time_);
627                         group_path = gtk_tree_model_get_path (model, &group_iter);
628                         gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (widget),
629                                                          group_path,
630                                                          GTK_TREE_VIEW_DROP_INTO_OR_BEFORE);
631                         gtk_tree_path_free (group_path);
632                 }
633                 else {
634                         group_path = gtk_tree_path_new_first ();
635                         gdk_drag_status (context, GDK_ACTION_MOVE, time_);
636                         gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (widget),
637                                                          group_path,
638                                                          GTK_TREE_VIEW_DROP_BEFORE);
639                 }
640         }
641         else {
642                 /* This is a file drag, and it can only be dropped on contacts,
643                    not groups.
644                  */
645                 EmpathyContact *contact;
646                 gtk_tree_model_get (model, &iter,
647                                     EMPATHY_CONTACT_LIST_STORE_COL_CONTACT, &contact,
648                                     -1);
649                 if (contact != NULL &&
650                     empathy_contact_is_online (contact) &&
651                     (empathy_contact_get_capabilities (contact) & EMPATHY_CAPABILITIES_FT)) {
652                         gdk_drag_status (context, GDK_ACTION_COPY, time_);
653                         gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (widget),
654                                                          path,
655                                                          GTK_TREE_VIEW_DROP_INTO_OR_BEFORE);
656                         g_object_unref (contact);
657                 }
658                 else {
659                         gdk_drag_status (context, 0, time_);
660                         gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (widget), NULL, 0);
661                         retval = FALSE;
662                 }
663         }
664
665         if (!is_different && !cleanup) {
666                 return retval;
667         }
668
669         if (dm) {
670                 gtk_tree_path_free (dm->path);
671                 if (dm->timeout_id) {
672                         g_source_remove (dm->timeout_id);
673                 }
674
675                 g_free (dm);
676
677                 dm = NULL;
678         }
679
680         if (!gtk_tree_view_row_expanded (GTK_TREE_VIEW (widget), path)) {
681                 dm = g_new0 (DragMotionData, 1);
682
683                 dm->view = EMPATHY_CONTACT_LIST_VIEW (widget);
684                 dm->path = gtk_tree_path_copy (path);
685
686                 dm->timeout_id = g_timeout_add_seconds (1,
687                         (GSourceFunc) contact_list_view_drag_motion_cb,
688                         dm);
689         }
690
691         return retval;
692 }
693
694 static void
695 contact_list_view_drag_begin (GtkWidget      *widget,
696                               GdkDragContext *context)
697 {
698         EmpathyContactListViewPriv *priv;
699         GtkTreeSelection          *selection;
700         GtkTreeModel              *model;
701         GtkTreePath               *path;
702         GtkTreeIter                iter;
703
704         priv = GET_PRIV (widget);
705
706         GTK_WIDGET_CLASS (empathy_contact_list_view_parent_class)->drag_begin (widget,
707                                                                               context);
708
709         selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (widget));
710         if (!gtk_tree_selection_get_selected (selection, &model, &iter)) {
711                 return;
712         }
713
714         path = gtk_tree_model_get_path (model, &iter);
715         priv->drag_row = gtk_tree_row_reference_new (model, path);
716         gtk_tree_path_free (path);
717 }
718
719 static void
720 contact_list_view_drag_data_get (GtkWidget        *widget,
721                                  GdkDragContext   *context,
722                                  GtkSelectionData *selection,
723                                  guint             info,
724                                  guint             time_)
725 {
726         EmpathyContactListViewPriv *priv;
727         GtkTreePath                *src_path;
728         GtkTreeIter                 iter;
729         GtkTreeModel               *model;
730         EmpathyContact             *contact;
731         TpAccount                  *account;
732         const gchar                *contact_id;
733         const gchar                *account_id;
734         gchar                      *str;
735
736         priv = GET_PRIV (widget);
737
738         model = gtk_tree_view_get_model (GTK_TREE_VIEW (widget));
739         if (!priv->drag_row) {
740                 return;
741         }
742
743         src_path = gtk_tree_row_reference_get_path (priv->drag_row);
744         if (!src_path) {
745                 return;
746         }
747
748         if (!gtk_tree_model_get_iter (model, &iter, src_path)) {
749                 gtk_tree_path_free (src_path);
750                 return;
751         }
752
753         gtk_tree_path_free (src_path);
754
755         contact = empathy_contact_list_view_dup_selected (EMPATHY_CONTACT_LIST_VIEW (widget));
756         if (!contact) {
757                 return;
758         }
759
760         account = empathy_contact_get_account (contact);
761         account_id = tp_proxy_get_object_path (account);
762         contact_id = empathy_contact_get_id (contact);
763         g_object_unref (contact);
764         str = g_strconcat (account_id, ":", contact_id, NULL);
765
766         if (info == DND_DRAG_TYPE_CONTACT_ID) {
767                 gtk_selection_data_set (selection, drag_atoms_source[info], 8,
768                                         (guchar *) str, strlen (str) + 1);
769         }
770
771         g_free (str);
772 }
773
774 static void
775 contact_list_view_drag_end (GtkWidget      *widget,
776                             GdkDragContext *context)
777 {
778         EmpathyContactListViewPriv *priv;
779
780         priv = GET_PRIV (widget);
781
782         GTK_WIDGET_CLASS (empathy_contact_list_view_parent_class)->drag_end (widget,
783                                                                             context);
784
785         if (priv->drag_row) {
786                 gtk_tree_row_reference_free (priv->drag_row);
787                 priv->drag_row = NULL;
788         }
789 }
790
791 static gboolean
792 contact_list_view_drag_drop (GtkWidget      *widget,
793                              GdkDragContext *drag_context,
794                              gint            x,
795                              gint            y,
796                              guint           time_)
797 {
798         return FALSE;
799 }
800
801 typedef struct {
802         EmpathyContactListView *view;
803         guint                   button;
804         guint32                 time;
805 } MenuPopupData;
806
807 static gboolean
808 contact_list_view_popup_menu_idle_cb (gpointer user_data)
809 {
810         MenuPopupData *data = user_data;
811         GtkWidget     *menu;
812
813         menu = empathy_contact_list_view_get_contact_menu (data->view);
814         if (!menu) {
815                 menu = empathy_contact_list_view_get_group_menu (data->view);
816         }
817
818         if (menu) {
819                 gtk_menu_attach_to_widget (GTK_MENU (menu),
820                                            GTK_WIDGET (data->view), NULL);
821                 gtk_widget_show (menu);
822                 gtk_menu_popup (GTK_MENU (menu),
823                                 NULL, NULL, NULL, NULL,
824                                 data->button, data->time);
825                 g_object_ref_sink (menu);
826                 g_object_unref (menu);
827         }
828
829         g_slice_free (MenuPopupData, data);
830
831         return FALSE;
832 }
833
834 static gboolean
835 contact_list_view_button_press_event_cb (EmpathyContactListView *view,
836                                          GdkEventButton         *event,
837                                          gpointer                user_data)
838 {
839         if (event->button == 3) {
840                 MenuPopupData *data;
841
842                 data = g_slice_new (MenuPopupData);
843                 data->view = view;
844                 data->button = event->button;
845                 data->time = event->time;
846                 g_idle_add (contact_list_view_popup_menu_idle_cb, data);
847         }
848
849         return FALSE;
850 }
851
852 static gboolean
853 contact_list_view_key_press_event_cb (EmpathyContactListView *view,
854                                       GdkEventKey            *event,
855                                       gpointer                user_data)
856 {
857         if (event->keyval == GDK_KEY_Menu) {
858                 MenuPopupData *data;
859
860                 data = g_slice_new (MenuPopupData);
861                 data->view = view;
862                 data->button = 0;
863                 data->time = event->time;
864                 g_idle_add (contact_list_view_popup_menu_idle_cb, data);
865         }
866
867         return FALSE;
868 }
869
870 static void
871 contact_list_view_row_activated (GtkTreeView       *view,
872                                  GtkTreePath       *path,
873                                  GtkTreeViewColumn *column)
874 {
875         EmpathyContactListViewPriv *priv = GET_PRIV (view);
876         EmpathyContact             *contact;
877         GtkTreeModel               *model;
878         GtkTreeIter                 iter;
879
880         if (!(priv->contact_features & EMPATHY_CONTACT_FEATURE_CHAT)) {
881                 return;
882         }
883
884         model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
885         gtk_tree_model_get_iter (model, &iter, path);
886         gtk_tree_model_get (model, &iter,
887                             EMPATHY_CONTACT_LIST_STORE_COL_CONTACT, &contact,
888                             -1);
889
890         if (contact) {
891                 DEBUG ("Starting a chat");
892                 empathy_dispatcher_chat_with_contact (contact,
893                         gtk_get_current_event_time ());
894                 g_object_unref (contact);
895         }
896 }
897
898 static void
899 contact_list_view_call_activated_cb (
900     EmpathyCellRendererActivatable *cell,
901     const gchar                    *path_string,
902     EmpathyContactListView         *view)
903 {
904         GtkWidget *menu;
905         GtkTreeModel *model;
906         GtkTreeIter iter;
907         EmpathyContact *contact;
908         GdkEventButton *event;
909         GtkMenuShell *shell;
910         GtkWidget *item;
911
912         model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
913         if (!gtk_tree_model_get_iter_from_string (model, &iter, path_string))
914                 return;
915
916         gtk_tree_model_get (model, &iter,
917                             EMPATHY_CONTACT_LIST_STORE_COL_CONTACT, &contact,
918                             -1);
919         if (contact == NULL)
920                 return;
921
922         event = (GdkEventButton *) gtk_get_current_event ();
923
924         menu = gtk_menu_new ();
925         shell = GTK_MENU_SHELL (menu);
926
927         /* audio */
928         item = empathy_contact_audio_call_menu_item_new (contact);
929         gtk_menu_shell_append (shell, item);
930         gtk_widget_show (item);
931
932         /* video */
933         item = empathy_contact_video_call_menu_item_new (contact);
934         gtk_menu_shell_append (shell, item);
935         gtk_widget_show (item);
936
937         gtk_menu_attach_to_widget (GTK_MENU (menu),
938                                    GTK_WIDGET (view), NULL);
939         gtk_widget_show (menu);
940         gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL,
941                         event->button, event->time);
942         g_object_ref_sink (menu);
943         g_object_unref (menu);
944
945         g_object_unref (contact);
946 }
947
948 static void
949 contact_list_view_cell_set_background (EmpathyContactListView *view,
950                                        GtkCellRenderer        *cell,
951                                        gboolean                is_group,
952                                        gboolean                is_active)
953 {
954         GdkColor  color;
955         GtkStyle *style;
956
957         style = gtk_widget_get_style (GTK_WIDGET (view));
958
959         if (!is_group && is_active) {
960                 color = style->bg[GTK_STATE_SELECTED];
961
962                 /* Here we take the current theme colour and add it to
963                  * the colour for white and average the two. This
964                  * gives a colour which is inline with the theme but
965                  * slightly whiter.
966                  */
967                 color.red = (color.red + (style->white).red) / 2;
968                 color.green = (color.green + (style->white).green) / 2;
969                 color.blue = (color.blue + (style->white).blue) / 2;
970
971                 g_object_set (cell,
972                               "cell-background-gdk", &color,
973                               NULL);
974         } else {
975                 g_object_set (cell,
976                               "cell-background-gdk", NULL,
977                               NULL);
978         }
979 }
980
981 static void
982 contact_list_view_pixbuf_cell_data_func (GtkTreeViewColumn      *tree_column,
983                                          GtkCellRenderer        *cell,
984                                          GtkTreeModel           *model,
985                                          GtkTreeIter            *iter,
986                                          EmpathyContactListView *view)
987 {
988         GdkPixbuf *pixbuf;
989         gboolean   is_group;
990         gboolean   is_active;
991
992         gtk_tree_model_get (model, iter,
993                             EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
994                             EMPATHY_CONTACT_LIST_STORE_COL_IS_ACTIVE, &is_active,
995                             EMPATHY_CONTACT_LIST_STORE_COL_ICON_STATUS, &pixbuf,
996                             -1);
997
998         g_object_set (cell,
999                       "visible", !is_group,
1000                       "pixbuf", pixbuf,
1001                       NULL);
1002
1003         if (pixbuf != NULL) {
1004                 g_object_unref (pixbuf);
1005         }
1006
1007         contact_list_view_cell_set_background (view, cell, is_group, is_active);
1008 }
1009
1010 static void
1011 contact_list_view_group_icon_cell_data_func (GtkTreeViewColumn     *tree_column,
1012                                              GtkCellRenderer       *cell,
1013                                              GtkTreeModel          *model,
1014                                              GtkTreeIter           *iter,
1015                                              EmpathyContactListView *view)
1016 {
1017         GdkPixbuf *pixbuf = NULL;
1018         gboolean is_group;
1019         gchar *name;
1020
1021         gtk_tree_model_get (model, iter,
1022                             EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
1023                             EMPATHY_CONTACT_LIST_STORE_COL_NAME, &name,
1024                             -1);
1025
1026         if (!is_group)
1027                 goto out;
1028
1029         if (!tp_strdiff (name, EMPATHY_CONTACT_LIST_STORE_FAVORITE)) {
1030                 pixbuf = empathy_pixbuf_from_icon_name ("emblem-favorite",
1031                         GTK_ICON_SIZE_MENU);
1032         }
1033         else if (!tp_strdiff (name, EMPATHY_CONTACT_LIST_STORE_PEOPLE_NEARBY)) {
1034                 pixbuf = empathy_pixbuf_from_icon_name ("im-local-xmpp",
1035                         GTK_ICON_SIZE_MENU);
1036         }
1037
1038 out:
1039         g_object_set (cell,
1040                       "visible", pixbuf != NULL,
1041                       "pixbuf", pixbuf,
1042                       NULL);
1043
1044         if (pixbuf != NULL)
1045                 g_object_unref (pixbuf);
1046
1047         g_free (name);
1048 }
1049
1050 static void
1051 contact_list_view_audio_call_cell_data_func (
1052                                        GtkTreeViewColumn      *tree_column,
1053                                        GtkCellRenderer        *cell,
1054                                        GtkTreeModel           *model,
1055                                        GtkTreeIter            *iter,
1056                                        EmpathyContactListView *view)
1057 {
1058         gboolean is_group;
1059         gboolean is_active;
1060         gboolean can_audio, can_video;
1061
1062         gtk_tree_model_get (model, iter,
1063                             EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
1064                             EMPATHY_CONTACT_LIST_STORE_COL_IS_ACTIVE, &is_active,
1065                             EMPATHY_CONTACT_LIST_STORE_COL_CAN_AUDIO_CALL, &can_audio,
1066                             EMPATHY_CONTACT_LIST_STORE_COL_CAN_VIDEO_CALL, &can_video,
1067                             -1);
1068
1069         g_object_set (cell,
1070                       "visible", !is_group && (can_audio || can_video),
1071                       "icon-name", can_video? EMPATHY_IMAGE_VIDEO_CALL : EMPATHY_IMAGE_VOIP,
1072                       NULL);
1073
1074         contact_list_view_cell_set_background (view, cell, is_group, is_active);
1075 }
1076
1077 static void
1078 contact_list_view_avatar_cell_data_func (GtkTreeViewColumn      *tree_column,
1079                                          GtkCellRenderer        *cell,
1080                                          GtkTreeModel           *model,
1081                                          GtkTreeIter            *iter,
1082                                          EmpathyContactListView *view)
1083 {
1084         GdkPixbuf *pixbuf;
1085         gboolean   show_avatar;
1086         gboolean   is_group;
1087         gboolean   is_active;
1088
1089         gtk_tree_model_get (model, iter,
1090                             EMPATHY_CONTACT_LIST_STORE_COL_PIXBUF_AVATAR, &pixbuf,
1091                             EMPATHY_CONTACT_LIST_STORE_COL_PIXBUF_AVATAR_VISIBLE, &show_avatar,
1092                             EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
1093                             EMPATHY_CONTACT_LIST_STORE_COL_IS_ACTIVE, &is_active,
1094                             -1);
1095
1096         g_object_set (cell,
1097                       "visible", !is_group && show_avatar,
1098                       "pixbuf", pixbuf,
1099                       NULL);
1100
1101         if (pixbuf) {
1102                 g_object_unref (pixbuf);
1103         }
1104
1105         contact_list_view_cell_set_background (view, cell, is_group, is_active);
1106 }
1107
1108 static void
1109 contact_list_view_text_cell_data_func (GtkTreeViewColumn      *tree_column,
1110                                        GtkCellRenderer        *cell,
1111                                        GtkTreeModel           *model,
1112                                        GtkTreeIter            *iter,
1113                                        EmpathyContactListView *view)
1114 {
1115         gboolean is_group;
1116         gboolean is_active;
1117
1118         gtk_tree_model_get (model, iter,
1119                             EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
1120                             EMPATHY_CONTACT_LIST_STORE_COL_IS_ACTIVE, &is_active,
1121                             -1);
1122
1123         contact_list_view_cell_set_background (view, cell, is_group, is_active);
1124 }
1125
1126 static void
1127 contact_list_view_expander_cell_data_func (GtkTreeViewColumn      *column,
1128                                            GtkCellRenderer        *cell,
1129                                            GtkTreeModel           *model,
1130                                            GtkTreeIter            *iter,
1131                                            EmpathyContactListView *view)
1132 {
1133         gboolean is_group;
1134         gboolean is_active;
1135
1136         gtk_tree_model_get (model, iter,
1137                             EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
1138                             EMPATHY_CONTACT_LIST_STORE_COL_IS_ACTIVE, &is_active,
1139                             -1);
1140
1141         if (gtk_tree_model_iter_has_child (model, iter)) {
1142                 GtkTreePath *path;
1143                 gboolean     row_expanded;
1144
1145                 path = gtk_tree_model_get_path (model, iter);
1146                 row_expanded = gtk_tree_view_row_expanded (GTK_TREE_VIEW (gtk_tree_view_column_get_tree_view (column)), path);
1147                 gtk_tree_path_free (path);
1148
1149                 g_object_set (cell,
1150                               "visible", TRUE,
1151                               "expander-style", row_expanded ? GTK_EXPANDER_EXPANDED : GTK_EXPANDER_COLLAPSED,
1152                               NULL);
1153         } else {
1154                 g_object_set (cell, "visible", FALSE, NULL);
1155         }
1156
1157         contact_list_view_cell_set_background (view, cell, is_group, is_active);
1158 }
1159
1160 static void
1161 contact_list_view_row_expand_or_collapse_cb (EmpathyContactListView *view,
1162                                              GtkTreeIter            *iter,
1163                                              GtkTreePath            *path,
1164                                              gpointer                user_data)
1165 {
1166         EmpathyContactListViewPriv *priv = GET_PRIV (view);
1167         GtkTreeModel               *model;
1168         gchar                      *name;
1169         gboolean                    expanded;
1170
1171         if (!(priv->list_features & EMPATHY_CONTACT_LIST_FEATURE_GROUPS_SAVE)) {
1172                 return;
1173         }
1174
1175         model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
1176
1177         gtk_tree_model_get (model, iter,
1178                             EMPATHY_CONTACT_LIST_STORE_COL_NAME, &name,
1179                             -1);
1180
1181         expanded = GPOINTER_TO_INT (user_data);
1182         empathy_contact_group_set_expanded (name, expanded);
1183
1184         g_free (name);
1185 }
1186
1187 static gboolean
1188 contact_list_view_start_search_cb (EmpathyContactListView *view,
1189                                    gpointer                data)
1190 {
1191         EmpathyContactListViewPriv *priv = GET_PRIV (view);
1192
1193         if (priv->search_widget == NULL)
1194                 return FALSE;
1195
1196         if (gtk_widget_get_visible (GTK_WIDGET (priv->search_widget)))
1197                 gtk_widget_grab_focus (GTK_WIDGET (priv->search_widget));
1198         else
1199                 gtk_widget_show (GTK_WIDGET (priv->search_widget));
1200
1201         return TRUE;
1202 }
1203
1204 static void
1205 contact_list_view_search_text_notify_cb (EmpathyLiveSearch      *search,
1206                                          GParamSpec             *pspec,
1207                                          EmpathyContactListView *view)
1208 {
1209         EmpathyContactListViewPriv *priv = GET_PRIV (view);
1210         GtkTreePath *path;
1211         GtkTreeViewColumn *focus_column;
1212         GtkTreeModel *model;
1213         GtkTreeIter iter;
1214         gboolean set_cursor = FALSE;
1215
1216         gtk_tree_model_filter_refilter (priv->filter);
1217
1218         /* Set cursor on the first contact. If it is already set on a group,
1219          * set it on its first child contact. Note that first child of a group
1220          * is its separator, that's why we actually set to the 2nd
1221          */
1222
1223         model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
1224         gtk_tree_view_get_cursor (GTK_TREE_VIEW (view), &path, &focus_column);
1225
1226         if (path == NULL) {
1227                 path = gtk_tree_path_new_from_string ("0:1");
1228                 set_cursor = TRUE;
1229         } else if (gtk_tree_path_get_depth (path) < 2) {
1230                 gboolean is_group;
1231
1232                 gtk_tree_model_get_iter (model, &iter, path);
1233                 gtk_tree_model_get (model, &iter,
1234                         EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
1235                         -1);
1236
1237                 if (is_group) {
1238                         gtk_tree_path_down (path);
1239                         gtk_tree_path_next (path);
1240                         set_cursor = TRUE;
1241                 }
1242         }
1243
1244         if (set_cursor) {
1245                 /* FIXME: Workaround for GTK bug #621651, we have to make sure
1246                  * the path is valid. */
1247                 if (gtk_tree_model_get_iter (model, &iter, path)) {
1248                         gtk_tree_view_set_cursor (GTK_TREE_VIEW (view), path,
1249                                 focus_column, FALSE);
1250                 }
1251         }
1252
1253         gtk_tree_path_free (path);
1254 }
1255
1256 static void
1257 contact_list_view_search_activate_cb (GtkWidget *search,
1258                                       EmpathyContactListView *view)
1259 {
1260         GtkTreePath *path;
1261         GtkTreeViewColumn *focus_column;
1262
1263         gtk_tree_view_get_cursor (GTK_TREE_VIEW (view), &path, &focus_column);
1264         if (path != NULL) {
1265                 gtk_tree_view_row_activated (GTK_TREE_VIEW (view), path,
1266                         focus_column);
1267                 gtk_tree_path_free (path);
1268
1269                 gtk_widget_hide (search);
1270         }
1271 }
1272
1273 static gboolean
1274 contact_list_view_search_key_navigation_cb (GtkWidget *search,
1275                                             GdkEvent *event,
1276                                             EmpathyContactListView *view)
1277 {
1278         GdkEventKey *eventkey = ((GdkEventKey *) event);
1279         gboolean ret = FALSE;
1280
1281         if (eventkey->keyval == GDK_KEY_Up || eventkey->keyval == GDK_KEY_Down) {
1282                 GdkEvent *new_event;
1283
1284                 new_event = gdk_event_copy (event);
1285                 gtk_widget_grab_focus (GTK_WIDGET (view));
1286                 ret = gtk_widget_event (GTK_WIDGET (view), new_event);
1287                 gtk_widget_grab_focus (search);
1288
1289                 gdk_event_free (new_event);
1290         }
1291
1292         return ret;
1293 }
1294
1295 static void
1296 contact_list_view_search_hide_cb (EmpathyLiveSearch      *search,
1297                                   EmpathyContactListView *view)
1298 {
1299         EmpathyContactListViewPriv *priv = GET_PRIV (view);
1300         GtkTreeModel               *model;
1301         GtkTreeIter                 iter;
1302         gboolean                    valid = FALSE;
1303
1304         /* block expand or collapse handlers, they would write the
1305          * expand or collapsed setting to file otherwise */
1306         g_signal_handlers_block_by_func (view,
1307                 contact_list_view_row_expand_or_collapse_cb,
1308                 GINT_TO_POINTER (TRUE));
1309         g_signal_handlers_block_by_func (view,
1310                 contact_list_view_row_expand_or_collapse_cb,
1311                 GINT_TO_POINTER (FALSE));
1312
1313         /* restore which groups are expanded and which are not */
1314         model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
1315         for (valid = gtk_tree_model_get_iter_first (model, &iter);
1316              valid; valid = gtk_tree_model_iter_next (model, &iter)) {
1317                 gboolean      is_group;
1318                 gchar        *name = NULL;
1319                 GtkTreePath  *path;
1320
1321                 gtk_tree_model_get (model, &iter,
1322                         EMPATHY_CONTACT_LIST_STORE_COL_NAME, &name,
1323                         EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
1324                         -1);
1325
1326                 if (!is_group) {
1327                         g_free (name);
1328                         continue;
1329                 }
1330
1331                 path = gtk_tree_model_get_path (model, &iter);
1332                 if ((priv->list_features & EMPATHY_CONTACT_LIST_FEATURE_GROUPS_SAVE) == 0 ||
1333                     empathy_contact_group_get_expanded (name)) {
1334                         gtk_tree_view_expand_row (GTK_TREE_VIEW (view), path,
1335                                 TRUE);
1336                 } else {
1337                         gtk_tree_view_collapse_row (GTK_TREE_VIEW (view), path);
1338                 }
1339
1340                 gtk_tree_path_free (path);
1341                 g_free (name);
1342         }
1343
1344         /* unblock expand or collapse handlers */
1345         g_signal_handlers_unblock_by_func (view,
1346                 contact_list_view_row_expand_or_collapse_cb,
1347                 GINT_TO_POINTER (TRUE));
1348         g_signal_handlers_unblock_by_func (view,
1349                 contact_list_view_row_expand_or_collapse_cb,
1350                 GINT_TO_POINTER (FALSE));
1351 }
1352
1353 static void
1354 contact_list_view_search_show_cb (EmpathyLiveSearch      *search,
1355                                   EmpathyContactListView *view)
1356 {
1357         /* block expand or collapse handlers during expand all, they would
1358          * write the expand or collapsed setting to file otherwise */
1359         g_signal_handlers_block_by_func (view,
1360                 contact_list_view_row_expand_or_collapse_cb,
1361                 GINT_TO_POINTER (TRUE));
1362
1363         gtk_tree_view_expand_all (GTK_TREE_VIEW (view));
1364
1365         g_signal_handlers_unblock_by_func (view,
1366                 contact_list_view_row_expand_or_collapse_cb,
1367                 GINT_TO_POINTER (TRUE));
1368 }
1369
1370 typedef struct {
1371         EmpathyContactListView *view;
1372         GtkTreeRowReference *row_ref;
1373         gboolean expand;
1374 } ExpandData;
1375
1376 static gboolean
1377 contact_list_view_expand_idle_cb (gpointer user_data)
1378 {
1379         ExpandData *data = user_data;
1380         GtkTreePath *path;
1381
1382         path = gtk_tree_row_reference_get_path (data->row_ref);
1383         if (path == NULL)
1384                 goto done;
1385
1386         g_signal_handlers_block_by_func (data->view,
1387                 contact_list_view_row_expand_or_collapse_cb,
1388                 GINT_TO_POINTER (data->expand));
1389
1390         if (data->expand) {
1391                 gtk_tree_view_expand_row (GTK_TREE_VIEW (data->view), path,
1392                     TRUE);
1393         } else {
1394                 gtk_tree_view_collapse_row (GTK_TREE_VIEW (data->view), path);
1395         }
1396         gtk_tree_path_free (path);
1397
1398         g_signal_handlers_unblock_by_func (data->view,
1399                 contact_list_view_row_expand_or_collapse_cb,
1400                 GINT_TO_POINTER (data->expand));
1401
1402 done:
1403         g_object_unref (data->view);
1404         gtk_tree_row_reference_free (data->row_ref);
1405         g_slice_free (ExpandData, data);
1406
1407         return FALSE;
1408 }
1409
1410 static void
1411 contact_list_view_row_has_child_toggled_cb (GtkTreeModel           *model,
1412                                             GtkTreePath            *path,
1413                                             GtkTreeIter            *iter,
1414                                             EmpathyContactListView *view)
1415 {
1416         EmpathyContactListViewPriv *priv = GET_PRIV (view);
1417         gboolean  is_group = FALSE;
1418         gchar    *name = NULL;
1419         ExpandData *data;
1420
1421         gtk_tree_model_get (model, iter,
1422                             EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
1423                             EMPATHY_CONTACT_LIST_STORE_COL_NAME, &name,
1424                             -1);
1425
1426         if (!is_group || EMP_STR_EMPTY (name)) {
1427                 g_free (name);
1428                 return;
1429         }
1430
1431         data = g_slice_new0 (ExpandData);
1432         data->view = g_object_ref (view);
1433         data->row_ref = gtk_tree_row_reference_new (model, path);
1434         data->expand =
1435                 (priv->list_features & EMPATHY_CONTACT_LIST_FEATURE_GROUPS_SAVE) == 0 ||
1436                 (priv->search_widget != NULL && gtk_widget_get_visible (priv->search_widget)) ||
1437                 empathy_contact_group_get_expanded (name);
1438
1439         /* FIXME: It doesn't work to call gtk_tree_view_expand_row () from within
1440          * gtk_tree_model_filter_refilter () */
1441         g_idle_add (contact_list_view_expand_idle_cb, data);
1442
1443         g_free (name);
1444 }
1445
1446 static void
1447 contact_list_view_verify_group_visibility (EmpathyContactListView *view,
1448                                            GtkTreePath            *path)
1449 {
1450         EmpathyContactListViewPriv *priv = GET_PRIV (view);
1451         GtkTreeModel *model;
1452         GtkTreePath *parent_path;
1453         GtkTreeIter parent_iter;
1454
1455         if (gtk_tree_path_get_depth (path) < 2)
1456                 return;
1457
1458         /* A group row is visible if and only if at least one if its child is
1459          * visible. So when a row is inserted/deleted/changed in the base model,
1460          * that could modify the visibility of its parent in the filter model.
1461          */
1462
1463         model = GTK_TREE_MODEL (priv->store);
1464         parent_path = gtk_tree_path_copy (path);
1465         gtk_tree_path_up (parent_path);
1466         if (gtk_tree_model_get_iter (model, &parent_iter, parent_path)) {
1467                 /* This tells the filter to verify the visibility of that row,
1468                  * and show/hide it if necessary */
1469                 gtk_tree_model_row_changed (GTK_TREE_MODEL (priv->store),
1470                         parent_path, &parent_iter);
1471         }
1472         gtk_tree_path_free (parent_path);
1473 }
1474
1475 static void
1476 contact_list_view_store_row_changed_cb (GtkTreeModel           *model,
1477                                         GtkTreePath            *path,
1478                                         GtkTreeIter            *iter,
1479                                         EmpathyContactListView *view)
1480 {
1481         contact_list_view_verify_group_visibility (view, path);
1482 }
1483
1484 static void
1485 contact_list_view_store_row_deleted_cb (GtkTreeModel           *model,
1486                                         GtkTreePath            *path,
1487                                         EmpathyContactListView *view)
1488 {
1489         contact_list_view_verify_group_visibility (view, path);
1490 }
1491
1492 static void
1493 contact_list_view_constructed (GObject *object)
1494 {
1495         EmpathyContactListView     *view = EMPATHY_CONTACT_LIST_VIEW (object);
1496         EmpathyContactListViewPriv *priv = GET_PRIV (view);
1497         GtkCellRenderer            *cell;
1498         GtkTreeViewColumn          *col;
1499         guint                       i;
1500
1501         priv->filter = GTK_TREE_MODEL_FILTER (gtk_tree_model_filter_new (
1502                         GTK_TREE_MODEL (priv->store), NULL));
1503         gtk_tree_model_filter_set_visible_func (priv->filter,
1504                         contact_list_view_filter_visible_func,
1505                         view, NULL);
1506
1507         g_signal_connect (priv->filter, "row-has-child-toggled",
1508                           G_CALLBACK (contact_list_view_row_has_child_toggled_cb),
1509                           view);
1510
1511         gtk_tree_view_set_model (GTK_TREE_VIEW (view),
1512                                  GTK_TREE_MODEL (priv->filter));
1513
1514         tp_g_signal_connect_object (priv->store, "row-changed",
1515                 G_CALLBACK (contact_list_view_store_row_changed_cb),
1516                 view, 0);
1517         tp_g_signal_connect_object (priv->store, "row-inserted",
1518                 G_CALLBACK (contact_list_view_store_row_changed_cb),
1519                 view, 0);
1520         tp_g_signal_connect_object (priv->store, "row-deleted",
1521                 G_CALLBACK (contact_list_view_store_row_deleted_cb),
1522                 view, 0);
1523
1524         /* Setup view */
1525         /* Setting reorderable is a hack that gets us row previews as drag icons
1526            for free.  We override all the drag handlers.  It's tricky to get the
1527            position of the drag icon right in drag_begin.  GtkTreeView has special
1528            voodoo for it, so we let it do the voodoo that he do.
1529          */
1530         g_object_set (view,
1531                       "headers-visible", FALSE,
1532                       "reorderable", TRUE,
1533                       "show-expanders", FALSE,
1534                       NULL);
1535
1536         col = gtk_tree_view_column_new ();
1537
1538         /* State */
1539         cell = gtk_cell_renderer_pixbuf_new ();
1540         gtk_tree_view_column_pack_start (col, cell, FALSE);
1541         gtk_tree_view_column_set_cell_data_func (
1542                 col, cell,
1543                 (GtkTreeCellDataFunc) contact_list_view_pixbuf_cell_data_func,
1544                 view, NULL);
1545
1546         g_object_set (cell,
1547                       "xpad", 5,
1548                       "ypad", 1,
1549                       "visible", FALSE,
1550                       NULL);
1551
1552         /* Group icon */
1553         cell = gtk_cell_renderer_pixbuf_new ();
1554         gtk_tree_view_column_pack_start (col, cell, FALSE);
1555         gtk_tree_view_column_set_cell_data_func (
1556                 col, cell,
1557                 (GtkTreeCellDataFunc) contact_list_view_group_icon_cell_data_func,
1558                 view, NULL);
1559
1560         g_object_set (cell,
1561                       "xpad", 0,
1562                       "ypad", 0,
1563                       "visible", FALSE,
1564                       "width", 16,
1565                       "height", 16,
1566                       NULL);
1567
1568         /* Name */
1569         cell = empathy_cell_renderer_text_new ();
1570         gtk_tree_view_column_pack_start (col, cell, TRUE);
1571         gtk_tree_view_column_set_cell_data_func (
1572                 col, cell,
1573                 (GtkTreeCellDataFunc) contact_list_view_text_cell_data_func,
1574                 view, NULL);
1575
1576         gtk_tree_view_column_add_attribute (col, cell,
1577                                             "name", EMPATHY_CONTACT_LIST_STORE_COL_NAME);
1578         gtk_tree_view_column_add_attribute (col, cell,
1579                                             "text", EMPATHY_CONTACT_LIST_STORE_COL_NAME);
1580         gtk_tree_view_column_add_attribute (col, cell,
1581                                             "presence-type", EMPATHY_CONTACT_LIST_STORE_COL_PRESENCE_TYPE);
1582         gtk_tree_view_column_add_attribute (col, cell,
1583                                             "status", EMPATHY_CONTACT_LIST_STORE_COL_STATUS);
1584         gtk_tree_view_column_add_attribute (col, cell,
1585                                             "is_group", EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP);
1586         gtk_tree_view_column_add_attribute (col, cell,
1587                                             "compact", EMPATHY_CONTACT_LIST_STORE_COL_COMPACT);
1588
1589         /* Audio Call Icon */
1590         cell = empathy_cell_renderer_activatable_new ();
1591         gtk_tree_view_column_pack_start (col, cell, FALSE);
1592         gtk_tree_view_column_set_cell_data_func (
1593                 col, cell,
1594                 (GtkTreeCellDataFunc) contact_list_view_audio_call_cell_data_func,
1595                 view, NULL);
1596
1597         g_object_set (cell,
1598                       "visible", FALSE,
1599                       NULL);
1600
1601         g_signal_connect (cell, "path-activated",
1602                           G_CALLBACK (contact_list_view_call_activated_cb),
1603                           view);
1604
1605         /* Avatar */
1606         cell = gtk_cell_renderer_pixbuf_new ();
1607         gtk_tree_view_column_pack_start (col, cell, FALSE);
1608         gtk_tree_view_column_set_cell_data_func (
1609                 col, cell,
1610                 (GtkTreeCellDataFunc) contact_list_view_avatar_cell_data_func,
1611                 view, NULL);
1612
1613         g_object_set (cell,
1614                       "xpad", 0,
1615                       "ypad", 0,
1616                       "visible", FALSE,
1617                       "width", 32,
1618                       "height", 32,
1619                       NULL);
1620
1621         /* Expander */
1622         cell = empathy_cell_renderer_expander_new ();
1623         gtk_tree_view_column_pack_end (col, cell, FALSE);
1624         gtk_tree_view_column_set_cell_data_func (
1625                 col, cell,
1626                 (GtkTreeCellDataFunc) contact_list_view_expander_cell_data_func,
1627                 view, NULL);
1628
1629         /* Actually add the column now we have added all cell renderers */
1630         gtk_tree_view_append_column (GTK_TREE_VIEW (view), col);
1631
1632         /* Drag & Drop. */
1633         for (i = 0; i < G_N_ELEMENTS (drag_types_dest); ++i) {
1634                 drag_atoms_dest[i] = gdk_atom_intern (drag_types_dest[i].target,
1635                                                       FALSE);
1636         }
1637
1638         for (i = 0; i < G_N_ELEMENTS (drag_types_source); ++i) {
1639                 drag_atoms_source[i] = gdk_atom_intern (drag_types_source[i].target,
1640                                                         FALSE);
1641         }
1642 }
1643
1644 static void
1645 contact_list_view_set_list_features (EmpathyContactListView         *view,
1646                                      EmpathyContactListFeatureFlags  features)
1647 {
1648         EmpathyContactListViewPriv *priv = GET_PRIV (view);
1649         gboolean                    has_tooltip;
1650
1651         g_return_if_fail (EMPATHY_IS_CONTACT_LIST_VIEW (view));
1652
1653         priv->list_features = features;
1654
1655         /* Update DnD source/dest */
1656         if (features & EMPATHY_CONTACT_LIST_FEATURE_CONTACT_DRAG) {
1657                 gtk_drag_source_set (GTK_WIDGET (view),
1658                                      GDK_BUTTON1_MASK,
1659                                      drag_types_source,
1660                                      G_N_ELEMENTS (drag_types_source),
1661                                      GDK_ACTION_MOVE | GDK_ACTION_COPY);
1662         } else {
1663                 gtk_drag_source_unset (GTK_WIDGET (view));
1664
1665         }
1666
1667         if (features & EMPATHY_CONTACT_LIST_FEATURE_CONTACT_DROP) {
1668                 gtk_drag_dest_set (GTK_WIDGET (view),
1669                                    GTK_DEST_DEFAULT_ALL,
1670                                    drag_types_dest,
1671                                    G_N_ELEMENTS (drag_types_dest),
1672                                    GDK_ACTION_MOVE | GDK_ACTION_COPY);
1673         } else {
1674                 /* FIXME: URI could still be droped depending on FT feature */
1675                 gtk_drag_dest_unset (GTK_WIDGET (view));
1676         }
1677
1678         /* Update has-tooltip */
1679         has_tooltip = (features & EMPATHY_CONTACT_LIST_FEATURE_CONTACT_TOOLTIP) != 0;
1680         gtk_widget_set_has_tooltip (GTK_WIDGET (view), has_tooltip);
1681 }
1682
1683 static void
1684 contact_list_view_dispose (GObject *object)
1685 {
1686         EmpathyContactListView *view = EMPATHY_CONTACT_LIST_VIEW (object);
1687         EmpathyContactListViewPriv *priv = GET_PRIV (view);
1688
1689         if (priv->store) {
1690                 g_object_unref (priv->store);
1691                 priv->store = NULL;
1692         }
1693         if (priv->filter) {
1694                 g_object_unref (priv->filter);
1695                 priv->filter = NULL;
1696         }
1697         if (priv->tooltip_widget) {
1698                 gtk_widget_destroy (priv->tooltip_widget);
1699                 priv->tooltip_widget = NULL;
1700         }
1701         if (priv->file_targets) {
1702                 gtk_target_list_unref (priv->file_targets);
1703                 priv->file_targets = NULL;
1704         }
1705
1706         empathy_contact_list_view_set_live_search (view, NULL);
1707
1708         G_OBJECT_CLASS (empathy_contact_list_view_parent_class)->dispose (object);
1709 }
1710
1711 static void
1712 contact_list_view_get_property (GObject    *object,
1713                                 guint       param_id,
1714                                 GValue     *value,
1715                                 GParamSpec *pspec)
1716 {
1717         EmpathyContactListViewPriv *priv;
1718
1719         priv = GET_PRIV (object);
1720
1721         switch (param_id) {
1722         case PROP_STORE:
1723                 g_value_set_object (value, priv->store);
1724                 break;
1725         case PROP_LIST_FEATURES:
1726                 g_value_set_flags (value, priv->list_features);
1727                 break;
1728         case PROP_CONTACT_FEATURES:
1729                 g_value_set_flags (value, priv->contact_features);
1730                 break;
1731         default:
1732                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1733                 break;
1734         };
1735 }
1736
1737 static void
1738 contact_list_view_set_property (GObject      *object,
1739                                 guint         param_id,
1740                                 const GValue *value,
1741                                 GParamSpec   *pspec)
1742 {
1743         EmpathyContactListView     *view = EMPATHY_CONTACT_LIST_VIEW (object);
1744         EmpathyContactListViewPriv *priv = GET_PRIV (object);
1745
1746         switch (param_id) {
1747         case PROP_STORE:
1748                 priv->store = g_value_dup_object (value);
1749                 break;
1750         case PROP_LIST_FEATURES:
1751                 contact_list_view_set_list_features (view, g_value_get_flags (value));
1752                 break;
1753         case PROP_CONTACT_FEATURES:
1754                 priv->contact_features = g_value_get_flags (value);
1755                 break;
1756         default:
1757                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1758                 break;
1759         };
1760 }
1761
1762 static void
1763 empathy_contact_list_view_class_init (EmpathyContactListViewClass *klass)
1764 {
1765         GObjectClass     *object_class = G_OBJECT_CLASS (klass);
1766         GtkWidgetClass   *widget_class = GTK_WIDGET_CLASS (klass);
1767         GtkTreeViewClass *tree_view_class = GTK_TREE_VIEW_CLASS (klass);
1768
1769         object_class->constructed        = contact_list_view_constructed;
1770         object_class->dispose            = contact_list_view_dispose;
1771         object_class->get_property       = contact_list_view_get_property;
1772         object_class->set_property       = contact_list_view_set_property;
1773
1774         widget_class->drag_data_received = contact_list_view_drag_data_received;
1775         widget_class->drag_drop          = contact_list_view_drag_drop;
1776         widget_class->drag_begin         = contact_list_view_drag_begin;
1777         widget_class->drag_data_get      = contact_list_view_drag_data_get;
1778         widget_class->drag_end           = contact_list_view_drag_end;
1779         widget_class->drag_motion        = contact_list_view_drag_motion;
1780
1781         /* We use the class method to let user of this widget to connect to
1782          * the signal and stop emission of the signal so the default handler
1783          * won't be called. */
1784         tree_view_class->row_activated = contact_list_view_row_activated;
1785
1786         signals[DRAG_CONTACT_RECEIVED] =
1787                 g_signal_new ("drag-contact-received",
1788                               G_OBJECT_CLASS_TYPE (klass),
1789                               G_SIGNAL_RUN_LAST,
1790                               0,
1791                               NULL, NULL,
1792                               _empathy_gtk_marshal_VOID__OBJECT_STRING_STRING,
1793                               G_TYPE_NONE,
1794                               3, EMPATHY_TYPE_CONTACT, G_TYPE_STRING, G_TYPE_STRING);
1795
1796         g_object_class_install_property (object_class,
1797                                          PROP_STORE,
1798                                          g_param_spec_object ("store",
1799                                                              "The store of the view",
1800                                                              "The store of the view",
1801                                                               EMPATHY_TYPE_CONTACT_LIST_STORE,
1802                                                               G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE));
1803         g_object_class_install_property (object_class,
1804                                          PROP_LIST_FEATURES,
1805                                          g_param_spec_flags ("list-features",
1806                                                              "Features of the view",
1807                                                              "Flags for all enabled features",
1808                                                               EMPATHY_TYPE_CONTACT_LIST_FEATURE_FLAGS,
1809                                                               EMPATHY_CONTACT_LIST_FEATURE_NONE,
1810                                                               G_PARAM_READWRITE));
1811         g_object_class_install_property (object_class,
1812                                          PROP_CONTACT_FEATURES,
1813                                          g_param_spec_flags ("contact-features",
1814                                                              "Features of the contact menu",
1815                                                              "Flags for all enabled features for the menu",
1816                                                               EMPATHY_TYPE_CONTACT_FEATURE_FLAGS,
1817                                                               EMPATHY_CONTACT_FEATURE_NONE,
1818                                                               G_PARAM_READWRITE));
1819
1820         g_type_class_add_private (object_class, sizeof (EmpathyContactListViewPriv));
1821 }
1822
1823 static void
1824 empathy_contact_list_view_init (EmpathyContactListView *view)
1825 {
1826         EmpathyContactListViewPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (view,
1827                 EMPATHY_TYPE_CONTACT_LIST_VIEW, EmpathyContactListViewPriv);
1828
1829         view->priv = priv;
1830
1831         /* Get saved group states. */
1832         empathy_contact_groups_get_all ();
1833
1834         gtk_tree_view_set_row_separator_func (GTK_TREE_VIEW (view),
1835                                               empathy_contact_list_store_row_separator_func,
1836                                               NULL, NULL);
1837
1838         /* Set up drag target lists. */
1839         priv->file_targets = gtk_target_list_new (drag_types_dest_file,
1840                                                   G_N_ELEMENTS (drag_types_dest_file));
1841
1842         /* Connect to tree view signals rather than override. */
1843         g_signal_connect (view, "button-press-event",
1844                           G_CALLBACK (contact_list_view_button_press_event_cb),
1845                           NULL);
1846         g_signal_connect (view, "key-press-event",
1847                           G_CALLBACK (contact_list_view_key_press_event_cb),
1848                           NULL);
1849         g_signal_connect (view, "row-expanded",
1850                           G_CALLBACK (contact_list_view_row_expand_or_collapse_cb),
1851                           GINT_TO_POINTER (TRUE));
1852         g_signal_connect (view, "row-collapsed",
1853                           G_CALLBACK (contact_list_view_row_expand_or_collapse_cb),
1854                           GINT_TO_POINTER (FALSE));
1855         g_signal_connect (view, "query-tooltip",
1856                           G_CALLBACK (contact_list_view_query_tooltip_cb),
1857                           NULL);
1858 }
1859
1860 EmpathyContactListView *
1861 empathy_contact_list_view_new (EmpathyContactListStore        *store,
1862                                EmpathyContactListFeatureFlags  list_features,
1863                                EmpathyContactFeatureFlags      contact_features)
1864 {
1865         g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST_STORE (store), NULL);
1866
1867         return g_object_new (EMPATHY_TYPE_CONTACT_LIST_VIEW,
1868                              "store", store,
1869                              "contact-features", contact_features,
1870                              "list-features", list_features,
1871                              NULL);
1872 }
1873
1874 EmpathyContact *
1875 empathy_contact_list_view_dup_selected (EmpathyContactListView *view)
1876 {
1877         EmpathyContactListViewPriv *priv;
1878         GtkTreeSelection          *selection;
1879         GtkTreeIter                iter;
1880         GtkTreeModel              *model;
1881         EmpathyContact             *contact;
1882
1883         g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST_VIEW (view), NULL);
1884
1885         priv = GET_PRIV (view);
1886
1887         selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view));
1888         if (!gtk_tree_selection_get_selected (selection, &model, &iter)) {
1889                 return NULL;
1890         }
1891
1892         gtk_tree_model_get (model, &iter,
1893                             EMPATHY_CONTACT_LIST_STORE_COL_CONTACT, &contact,
1894                             -1);
1895
1896         return contact;
1897 }
1898
1899 EmpathyContactListFlags
1900 empathy_contact_list_view_get_flags (EmpathyContactListView *view)
1901 {
1902         EmpathyContactListViewPriv *priv;
1903         GtkTreeSelection          *selection;
1904         GtkTreeIter                iter;
1905         GtkTreeModel              *model;
1906         EmpathyContactListFlags    flags;
1907
1908         g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST_VIEW (view), 0);
1909
1910         priv = GET_PRIV (view);
1911
1912         selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view));
1913         if (!gtk_tree_selection_get_selected (selection, &model, &iter)) {
1914                 return 0;
1915         }
1916
1917         gtk_tree_model_get (model, &iter,
1918                             EMPATHY_CONTACT_LIST_STORE_COL_FLAGS, &flags,
1919                             -1);
1920
1921         return flags;
1922 }
1923
1924 gchar *
1925 empathy_contact_list_view_get_selected_group (EmpathyContactListView *view,
1926                                               gboolean *is_fake_group)
1927 {
1928         EmpathyContactListViewPriv *priv;
1929         GtkTreeSelection          *selection;
1930         GtkTreeIter                iter;
1931         GtkTreeModel              *model;
1932         gboolean                   is_group;
1933         gchar                     *name;
1934         gboolean                   fake;
1935
1936         g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST_VIEW (view), NULL);
1937
1938         priv = GET_PRIV (view);
1939
1940         selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view));
1941         if (!gtk_tree_selection_get_selected (selection, &model, &iter)) {
1942                 return NULL;
1943         }
1944
1945         gtk_tree_model_get (model, &iter,
1946                             EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
1947                             EMPATHY_CONTACT_LIST_STORE_COL_NAME, &name,
1948                             EMPATHY_CONTACT_LIST_STORE_COL_IS_FAKE_GROUP, &fake,
1949                             -1);
1950
1951         if (!is_group) {
1952                 g_free (name);
1953                 return NULL;
1954         }
1955
1956         if (is_fake_group != NULL)
1957                 *is_fake_group = fake;
1958
1959         return name;
1960 }
1961
1962 static gboolean
1963 contact_list_view_remove_dialog_show (GtkWindow   *parent,
1964                                       const gchar *message,
1965                                       const gchar *secondary_text)
1966 {
1967         GtkWidget *dialog;
1968         gboolean res;
1969
1970         dialog = gtk_message_dialog_new (parent, GTK_DIALOG_MODAL,
1971                                          GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE,
1972                                          "%s", message);
1973         gtk_dialog_add_buttons (GTK_DIALOG (dialog),
1974                                 GTK_STOCK_CANCEL, GTK_RESPONSE_NO,
1975                                 GTK_STOCK_DELETE, GTK_RESPONSE_YES,
1976                                 NULL);
1977         gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
1978                                                   "%s", secondary_text);
1979
1980         gtk_widget_show (dialog);
1981
1982         res = gtk_dialog_run (GTK_DIALOG (dialog));
1983         gtk_widget_destroy (dialog);
1984
1985         return (res == GTK_RESPONSE_YES);
1986 }
1987
1988 static void
1989 contact_list_view_group_remove_activate_cb (GtkMenuItem            *menuitem,
1990                                             EmpathyContactListView *view)
1991 {
1992         EmpathyContactListViewPriv *priv = GET_PRIV (view);
1993         gchar                      *group;
1994
1995         group = empathy_contact_list_view_get_selected_group (view, NULL);
1996         if (group) {
1997                 gchar     *text;
1998                 GtkWindow *parent;
1999
2000                 text = g_strdup_printf (_("Do you really want to remove the group '%s'?"), group);
2001                 parent = empathy_get_toplevel_window (GTK_WIDGET (view));
2002                 if (contact_list_view_remove_dialog_show (parent, _("Removing group"), text)) {
2003                         EmpathyContactList *list;
2004
2005                         list = empathy_contact_list_store_get_list_iface (priv->store);
2006                         empathy_contact_list_remove_group (list, group);
2007                 }
2008
2009                 g_free (text);
2010         }
2011
2012         g_free (group);
2013 }
2014
2015 GtkWidget *
2016 empathy_contact_list_view_get_group_menu (EmpathyContactListView *view)
2017 {
2018         EmpathyContactListViewPriv *priv = GET_PRIV (view);
2019         gchar                      *group;
2020         GtkWidget                  *menu;
2021         GtkWidget                  *item;
2022         GtkWidget                  *image;
2023         gboolean                   is_fake_group;
2024
2025         g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST_VIEW (view), NULL);
2026
2027         if (!(priv->list_features & (EMPATHY_CONTACT_LIST_FEATURE_GROUPS_RENAME |
2028                                      EMPATHY_CONTACT_LIST_FEATURE_GROUPS_REMOVE))) {
2029                 return NULL;
2030         }
2031
2032         group = empathy_contact_list_view_get_selected_group (view, &is_fake_group);
2033         if (!group || is_fake_group) {
2034                 /* We can't alter fake groups */
2035                 return NULL;
2036         }
2037
2038         menu = gtk_menu_new ();
2039
2040         /* FIXME: Not implemented yet
2041         if (priv->features & EMPATHY_CONTACT_LIST_FEATURE_GROUPS_RENAME) {
2042                 item = gtk_menu_item_new_with_mnemonic (_("Re_name"));
2043                 gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
2044                 gtk_widget_show (item);
2045                 g_signal_connect (item, "activate",
2046                                   G_CALLBACK (contact_list_view_group_rename_activate_cb),
2047                                   view);
2048         }*/
2049
2050         if (priv->list_features & EMPATHY_CONTACT_LIST_FEATURE_GROUPS_REMOVE) {
2051                 item = gtk_image_menu_item_new_with_mnemonic (_("_Remove"));
2052                 image = gtk_image_new_from_icon_name (GTK_STOCK_REMOVE,
2053                                                       GTK_ICON_SIZE_MENU);
2054                 gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image);
2055                 gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
2056                 gtk_widget_show (item);
2057                 g_signal_connect (item, "activate",
2058                                   G_CALLBACK (contact_list_view_group_remove_activate_cb),
2059                                   view);
2060         }
2061
2062         g_free (group);
2063
2064         return menu;
2065 }
2066
2067 static void
2068 contact_list_view_remove_activate_cb (GtkMenuItem            *menuitem,
2069                                       EmpathyContactListView *view)
2070 {
2071         EmpathyContactListViewPriv *priv = GET_PRIV (view);
2072         EmpathyContact             *contact;
2073
2074         contact = empathy_contact_list_view_dup_selected (view);
2075
2076         if (contact) {
2077                 gchar     *text;
2078                 GtkWindow *parent;
2079
2080                 parent = empathy_get_toplevel_window (GTK_WIDGET (view));
2081                 text = g_strdup_printf (_("Do you really want to remove the contact '%s'?"),
2082                                         empathy_contact_get_alias (contact));
2083                 if (contact_list_view_remove_dialog_show (parent, _("Removing contact"), text)) {
2084                         EmpathyContactList *list;
2085
2086                         list = empathy_contact_list_store_get_list_iface (priv->store);
2087                         empathy_contact_list_remove (list, contact, "");
2088                 }
2089
2090                 g_free (text);
2091                 g_object_unref (contact);
2092         }
2093 }
2094
2095 GtkWidget *
2096 empathy_contact_list_view_get_contact_menu (EmpathyContactListView *view)
2097 {
2098         EmpathyContactListViewPriv *priv = GET_PRIV (view);
2099         EmpathyContact             *contact;
2100         GtkWidget                  *menu;
2101         GtkWidget                  *item;
2102         GtkWidget                  *image;
2103         EmpathyContactListFlags     flags;
2104
2105         g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST_VIEW (view), NULL);
2106
2107         contact = empathy_contact_list_view_dup_selected (view);
2108         if (!contact) {
2109                 return NULL;
2110         }
2111         flags = empathy_contact_list_view_get_flags (view);
2112
2113         menu = empathy_contact_menu_new (contact, priv->contact_features);
2114
2115         /* Remove contact */
2116         if (priv->list_features & EMPATHY_CONTACT_LIST_FEATURE_CONTACT_REMOVE &&
2117             flags & EMPATHY_CONTACT_LIST_CAN_REMOVE) {
2118                 /* create the menu if required, or just add a separator */
2119                 if (!menu) {
2120                         menu = gtk_menu_new ();
2121                 } else {
2122                         item = gtk_separator_menu_item_new ();
2123                         gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
2124                         gtk_widget_show (item);
2125                 }
2126
2127                 /* Remove */
2128                 item = gtk_image_menu_item_new_with_mnemonic (_("_Remove"));
2129                 image = gtk_image_new_from_icon_name (GTK_STOCK_REMOVE,
2130                                                       GTK_ICON_SIZE_MENU);
2131                 gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image);
2132                 gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
2133                 gtk_widget_show (item);
2134                 g_signal_connect (item, "activate",
2135                                   G_CALLBACK (contact_list_view_remove_activate_cb),
2136                                   view);
2137         }
2138
2139         g_object_unref (contact);
2140
2141         return menu;
2142 }
2143
2144 void
2145 empathy_contact_list_view_set_live_search (EmpathyContactListView *view,
2146                                            EmpathyLiveSearch      *search)
2147 {
2148         EmpathyContactListViewPriv *priv = GET_PRIV (view);
2149
2150         /* remove old handlers if old search was not null */
2151         if (priv->search_widget != NULL) {
2152                 g_signal_handlers_disconnect_by_func (view,
2153                         contact_list_view_start_search_cb,
2154                         NULL);
2155
2156                 g_signal_handlers_disconnect_by_func (priv->search_widget,
2157                         contact_list_view_search_text_notify_cb,
2158                         view);
2159                 g_signal_handlers_disconnect_by_func (priv->search_widget,
2160                         contact_list_view_search_activate_cb,
2161                         view);
2162                 g_signal_handlers_disconnect_by_func (priv->search_widget,
2163                         contact_list_view_search_key_navigation_cb,
2164                         view);
2165                 g_signal_handlers_disconnect_by_func (priv->search_widget,
2166                         contact_list_view_search_hide_cb,
2167                         view);
2168                 g_signal_handlers_disconnect_by_func (priv->search_widget,
2169                         contact_list_view_search_show_cb,
2170                         view);
2171                 g_object_unref (priv->search_widget);
2172                 priv->search_widget = NULL;
2173         }
2174
2175         /* connect handlers if new search is not null */
2176         if (search != NULL) {
2177                 priv->search_widget = g_object_ref (search);
2178
2179                 g_signal_connect (view, "start-interactive-search",
2180                                   G_CALLBACK (contact_list_view_start_search_cb),
2181                                   NULL);
2182
2183                 g_signal_connect (priv->search_widget, "notify::text",
2184                         G_CALLBACK (contact_list_view_search_text_notify_cb),
2185                         view);
2186                 g_signal_connect (priv->search_widget, "activate",
2187                         G_CALLBACK (contact_list_view_search_activate_cb),
2188                         view);
2189                 g_signal_connect (priv->search_widget, "key-navigation",
2190                         G_CALLBACK (contact_list_view_search_key_navigation_cb),
2191                         view);
2192                 g_signal_connect (priv->search_widget, "hide",
2193                         G_CALLBACK (contact_list_view_search_hide_cb),
2194                         view);
2195                 g_signal_connect (priv->search_widget, "show",
2196                         G_CALLBACK (contact_list_view_search_show_cb),
2197                         view);
2198         }
2199 }