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