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