]> git.0d.be Git - empathy.git/blob - libempathy-gtk/empathy-contact-list-view.c
5aab817d759158c3e0cde8e47bcada022918a639
[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         gboolean show_status;
1034         gchar *name;
1035
1036         gtk_tree_model_get (model, iter,
1037                             EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
1038                             EMPATHY_CONTACT_LIST_STORE_COL_IS_ACTIVE, &is_active,
1039                             EMPATHY_CONTACT_LIST_STORE_COL_STATUS_VISIBLE, &show_status,
1040                             EMPATHY_CONTACT_LIST_STORE_COL_NAME, &name,
1041                             -1);
1042
1043         g_object_set (cell,
1044                       "show-status", show_status,
1045                       "text", name,
1046                       NULL);
1047         g_free (name);
1048
1049         contact_list_view_cell_set_background (view, cell, is_group, is_active);
1050 }
1051
1052 static void
1053 contact_list_view_expander_cell_data_func (GtkTreeViewColumn     *column,
1054                                            GtkCellRenderer       *cell,
1055                                            GtkTreeModel          *model,
1056                                            GtkTreeIter           *iter,
1057                                            EmpathyContactListView *view)
1058 {
1059         gboolean is_group;
1060         gboolean is_active;
1061
1062         gtk_tree_model_get (model, iter,
1063                             EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
1064                             EMPATHY_CONTACT_LIST_STORE_COL_IS_ACTIVE, &is_active,
1065                             -1);
1066
1067         if (gtk_tree_model_iter_has_child (model, iter)) {
1068                 GtkTreePath *path;
1069                 gboolean     row_expanded;
1070
1071                 path = gtk_tree_model_get_path (model, iter);
1072                 row_expanded = gtk_tree_view_row_expanded (GTK_TREE_VIEW (gtk_tree_view_column_get_tree_view (column)), path);
1073                 gtk_tree_path_free (path);
1074
1075                 g_object_set (cell,
1076                               "visible", TRUE,
1077                               "expander-style", row_expanded ? GTK_EXPANDER_EXPANDED : GTK_EXPANDER_COLLAPSED,
1078                               NULL);
1079         } else {
1080                 g_object_set (cell, "visible", FALSE, NULL);
1081         }
1082
1083         contact_list_view_cell_set_background (view, cell, is_group, is_active);
1084 }
1085
1086 static void
1087 contact_list_view_row_expand_or_collapse_cb (EmpathyContactListView *view,
1088                                              GtkTreeIter           *iter,
1089                                              GtkTreePath           *path,
1090                                              gpointer               user_data)
1091 {
1092         EmpathyContactListViewPriv *priv = GET_PRIV (view);
1093         GtkTreeModel               *model;
1094         gchar                      *name;
1095         gboolean                    expanded;
1096
1097         if (!(priv->list_features & EMPATHY_CONTACT_LIST_FEATURE_GROUPS_SAVE)) {
1098                 return;
1099         }
1100
1101         model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
1102
1103         gtk_tree_model_get (model, iter,
1104                             EMPATHY_CONTACT_LIST_STORE_COL_NAME, &name,
1105                             -1);
1106
1107         expanded = GPOINTER_TO_INT (user_data);
1108         empathy_contact_group_set_expanded (name, expanded);
1109
1110         g_free (name);
1111 }
1112
1113 static void
1114 contact_list_view_row_has_child_toggled_cb (GtkTreeModel          *model,
1115                                             GtkTreePath           *path,
1116                                             GtkTreeIter           *iter,
1117                                             EmpathyContactListView *view)
1118 {
1119         EmpathyContactListViewPriv *priv = GET_PRIV (view);
1120         gboolean  is_group = FALSE;
1121         gchar    *name = NULL;
1122
1123         gtk_tree_model_get (model, iter,
1124                             EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
1125                             EMPATHY_CONTACT_LIST_STORE_COL_NAME, &name,
1126                             -1);
1127
1128         if (!is_group || EMP_STR_EMPTY (name)) {
1129                 g_free (name);
1130                 return;
1131         }
1132
1133         if (!(priv->list_features & EMPATHY_CONTACT_LIST_FEATURE_GROUPS_SAVE) ||
1134             empathy_contact_group_get_expanded (name)) {
1135                 g_signal_handlers_block_by_func (view,
1136                                                  contact_list_view_row_expand_or_collapse_cb,
1137                                                  GINT_TO_POINTER (TRUE));
1138                 gtk_tree_view_expand_row (GTK_TREE_VIEW (view), path, TRUE);
1139                 g_signal_handlers_unblock_by_func (view,
1140                                                    contact_list_view_row_expand_or_collapse_cb,
1141                                                    GINT_TO_POINTER (TRUE));
1142         } else {
1143                 g_signal_handlers_block_by_func (view,
1144                                                  contact_list_view_row_expand_or_collapse_cb,
1145                                                  GINT_TO_POINTER (FALSE));
1146                 gtk_tree_view_collapse_row (GTK_TREE_VIEW (view), path);
1147                 g_signal_handlers_unblock_by_func (view,
1148                                                    contact_list_view_row_expand_or_collapse_cb,
1149                                                    GINT_TO_POINTER (FALSE));
1150         }
1151
1152         g_free (name);
1153 }
1154
1155 static void
1156 contact_list_view_setup (EmpathyContactListView *view)
1157 {
1158         EmpathyContactListViewPriv *priv;
1159         GtkCellRenderer           *cell;
1160         GtkTreeViewColumn         *col;
1161         guint                      i;
1162
1163         priv = GET_PRIV (view);
1164
1165         gtk_tree_view_set_search_equal_func (GTK_TREE_VIEW (view),
1166                                              empathy_contact_list_store_search_equal_func,
1167                                              NULL, NULL);
1168
1169         g_signal_connect (priv->store, "row-has-child-toggled",
1170                           G_CALLBACK (contact_list_view_row_has_child_toggled_cb),
1171                           view);
1172         gtk_tree_view_set_model (GTK_TREE_VIEW (view),
1173                                  GTK_TREE_MODEL (priv->store));
1174
1175         /* Setup view */
1176         /* Setting reorderable is a hack that gets us row previews as drag icons
1177            for free.  We override all the drag handlers.  It's tricky to get the
1178            position of the drag icon right in drag_begin.  GtkTreeView has special
1179            voodoo for it, so we let it do the voodoo that he do.
1180          */
1181         g_object_set (view,
1182                       "headers-visible", FALSE,
1183                       "reorderable", TRUE,
1184                       "show-expanders", FALSE,
1185                       NULL);
1186
1187         col = gtk_tree_view_column_new ();
1188
1189         /* State */
1190         cell = gtk_cell_renderer_pixbuf_new ();
1191         gtk_tree_view_column_pack_start (col, cell, FALSE);
1192         gtk_tree_view_column_set_cell_data_func (
1193                 col, cell,
1194                 (GtkTreeCellDataFunc) contact_list_view_pixbuf_cell_data_func,
1195                 view, NULL);
1196
1197         g_object_set (cell,
1198                       "xpad", 5,
1199                       "ypad", 1,
1200                       "visible", FALSE,
1201                       NULL);
1202
1203         /* Group icon */
1204         cell = gtk_cell_renderer_pixbuf_new ();
1205         gtk_tree_view_column_pack_start (col, cell, FALSE);
1206         gtk_tree_view_column_set_cell_data_func (
1207                 col, cell,
1208                 (GtkTreeCellDataFunc) contact_list_view_group_icon_cell_data_func,
1209                 view, NULL);
1210
1211         g_object_set (cell,
1212                       "xpad", 0,
1213                       "ypad", 0,
1214                       "visible", FALSE,
1215                       "width", 16,
1216                       "height", 16,
1217                       NULL);
1218
1219         /* Name */
1220         cell = empathy_cell_renderer_text_new ();
1221         gtk_tree_view_column_pack_start (col, cell, TRUE);
1222         gtk_tree_view_column_set_cell_data_func (
1223                 col, cell,
1224                 (GtkTreeCellDataFunc) contact_list_view_text_cell_data_func,
1225                 view, NULL);
1226
1227         gtk_tree_view_column_add_attribute (col, cell,
1228                                             "name", EMPATHY_CONTACT_LIST_STORE_COL_NAME);
1229         gtk_tree_view_column_add_attribute (col, cell,
1230                                             "status", EMPATHY_CONTACT_LIST_STORE_COL_STATUS);
1231         gtk_tree_view_column_add_attribute (col, cell,
1232                                             "is_group", EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP);
1233
1234         /* Audio Call Icon */
1235         cell = empathy_cell_renderer_activatable_new ();
1236         gtk_tree_view_column_pack_start (col, cell, FALSE);
1237         gtk_tree_view_column_set_cell_data_func (
1238                 col, cell,
1239                 (GtkTreeCellDataFunc) contact_list_view_audio_call_cell_data_func,
1240                 view, NULL);
1241
1242         g_object_set (cell,
1243                       "visible", FALSE,
1244                       NULL);
1245
1246         g_signal_connect (cell, "path-activated",
1247                           G_CALLBACK (contact_list_view_call_activated_cb),
1248                           view);
1249
1250         /* Avatar */
1251         cell = gtk_cell_renderer_pixbuf_new ();
1252         gtk_tree_view_column_pack_start (col, cell, FALSE);
1253         gtk_tree_view_column_set_cell_data_func (
1254                 col, cell,
1255                 (GtkTreeCellDataFunc) contact_list_view_avatar_cell_data_func,
1256                 view, NULL);
1257
1258         g_object_set (cell,
1259                       "xpad", 0,
1260                       "ypad", 0,
1261                       "visible", FALSE,
1262                       "width", 32,
1263                       "height", 32,
1264                       NULL);
1265
1266         /* Expander */
1267         cell = empathy_cell_renderer_expander_new ();
1268         gtk_tree_view_column_pack_end (col, cell, FALSE);
1269         gtk_tree_view_column_set_cell_data_func (
1270                 col, cell,
1271                 (GtkTreeCellDataFunc) contact_list_view_expander_cell_data_func,
1272                 view, NULL);
1273
1274         /* Actually add the column now we have added all cell renderers */
1275         gtk_tree_view_append_column (GTK_TREE_VIEW (view), col);
1276
1277         /* Drag & Drop. */
1278         for (i = 0; i < G_N_ELEMENTS (drag_types_dest); ++i) {
1279                 drag_atoms_dest[i] = gdk_atom_intern (drag_types_dest[i].target,
1280                                                       FALSE);
1281         }
1282
1283         for (i = 0; i < G_N_ELEMENTS (drag_types_source); ++i) {
1284                 drag_atoms_source[i] = gdk_atom_intern (drag_types_source[i].target,
1285                                                         FALSE);
1286         }
1287 }
1288
1289 static void
1290 contact_list_view_set_list_features (EmpathyContactListView         *view,
1291                                      EmpathyContactListFeatureFlags  features)
1292 {
1293         EmpathyContactListViewPriv *priv = GET_PRIV (view);
1294         gboolean                    has_tooltip;
1295
1296         g_return_if_fail (EMPATHY_IS_CONTACT_LIST_VIEW (view));
1297
1298         priv->list_features = features;
1299
1300         /* Update DnD source/dest */
1301         if (features & EMPATHY_CONTACT_LIST_FEATURE_CONTACT_DRAG) {
1302                 gtk_drag_source_set (GTK_WIDGET (view),
1303                                      GDK_BUTTON1_MASK,
1304                                      drag_types_source,
1305                                      G_N_ELEMENTS (drag_types_source),
1306                                      GDK_ACTION_MOVE | GDK_ACTION_COPY);
1307         } else {
1308                 gtk_drag_source_unset (GTK_WIDGET (view));
1309
1310         }
1311
1312         if (features & EMPATHY_CONTACT_LIST_FEATURE_CONTACT_DROP) {
1313                 gtk_drag_dest_set (GTK_WIDGET (view),
1314                                    GTK_DEST_DEFAULT_ALL,
1315                                    drag_types_dest,
1316                                    G_N_ELEMENTS (drag_types_dest),
1317                                    GDK_ACTION_MOVE | GDK_ACTION_COPY);
1318         } else {
1319                 /* FIXME: URI could still be droped depending on FT feature */
1320                 gtk_drag_dest_unset (GTK_WIDGET (view));
1321         }
1322
1323         /* Update has-tooltip */
1324         has_tooltip = (features & EMPATHY_CONTACT_LIST_FEATURE_CONTACT_TOOLTIP) != 0;
1325         gtk_widget_set_has_tooltip (GTK_WIDGET (view), has_tooltip);
1326 }
1327
1328 static void
1329 contact_list_view_finalize (GObject *object)
1330 {
1331         EmpathyContactListViewPriv *priv;
1332
1333         priv = GET_PRIV (object);
1334
1335         if (priv->store) {
1336                 g_object_unref (priv->store);
1337         }
1338         if (priv->tooltip_widget) {
1339                 gtk_widget_destroy (priv->tooltip_widget);
1340         }
1341         if (priv->file_targets) {
1342                 gtk_target_list_unref (priv->file_targets);
1343         }
1344
1345         G_OBJECT_CLASS (empathy_contact_list_view_parent_class)->finalize (object);
1346 }
1347
1348 static void
1349 contact_list_view_get_property (GObject    *object,
1350                                 guint       param_id,
1351                                 GValue     *value,
1352                                 GParamSpec *pspec)
1353 {
1354         EmpathyContactListViewPriv *priv;
1355
1356         priv = GET_PRIV (object);
1357
1358         switch (param_id) {
1359         case PROP_STORE:
1360                 g_value_set_object (value, priv->store);
1361                 break;
1362         case PROP_LIST_FEATURES:
1363                 g_value_set_flags (value, priv->list_features);
1364                 break;
1365         case PROP_CONTACT_FEATURES:
1366                 g_value_set_flags (value, priv->contact_features);
1367                 break;
1368         default:
1369                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1370                 break;
1371         };
1372 }
1373
1374 static void
1375 contact_list_view_set_property (GObject      *object,
1376                                 guint         param_id,
1377                                 const GValue *value,
1378                                 GParamSpec   *pspec)
1379 {
1380         EmpathyContactListView     *view = EMPATHY_CONTACT_LIST_VIEW (object);
1381         EmpathyContactListViewPriv *priv = GET_PRIV (object);
1382
1383         switch (param_id) {
1384         case PROP_STORE:
1385                 priv->store = g_value_dup_object (value);
1386                 contact_list_view_setup (view);
1387                 break;
1388         case PROP_LIST_FEATURES:
1389                 contact_list_view_set_list_features (view, g_value_get_flags (value));
1390                 break;
1391         case PROP_CONTACT_FEATURES:
1392                 priv->contact_features = g_value_get_flags (value);
1393                 break;
1394         default:
1395                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1396                 break;
1397         };
1398 }
1399
1400 static void
1401 empathy_contact_list_view_class_init (EmpathyContactListViewClass *klass)
1402 {
1403         GObjectClass     *object_class = G_OBJECT_CLASS (klass);
1404         GtkWidgetClass   *widget_class = GTK_WIDGET_CLASS (klass);
1405         GtkTreeViewClass *tree_view_class = GTK_TREE_VIEW_CLASS (klass);
1406
1407         object_class->finalize = contact_list_view_finalize;
1408         object_class->get_property = contact_list_view_get_property;
1409         object_class->set_property = contact_list_view_set_property;
1410
1411         widget_class->drag_data_received = contact_list_view_drag_data_received;
1412         widget_class->drag_drop          = contact_list_view_drag_drop;
1413         widget_class->drag_begin         = contact_list_view_drag_begin;
1414         widget_class->drag_data_get      = contact_list_view_drag_data_get;
1415         widget_class->drag_end           = contact_list_view_drag_end;
1416         widget_class->drag_motion        = contact_list_view_drag_motion;
1417
1418         /* We use the class method to let user of this widget to connect to
1419          * the signal and stop emission of the signal so the default handler
1420          * won't be called. */
1421         tree_view_class->row_activated = contact_list_view_row_activated;
1422
1423         signals[DRAG_CONTACT_RECEIVED] =
1424                 g_signal_new ("drag-contact-received",
1425                               G_OBJECT_CLASS_TYPE (klass),
1426                               G_SIGNAL_RUN_LAST,
1427                               0,
1428                               NULL, NULL,
1429                               _empathy_gtk_marshal_VOID__OBJECT_STRING_STRING,
1430                               G_TYPE_NONE,
1431                               3, EMPATHY_TYPE_CONTACT, G_TYPE_STRING, G_TYPE_STRING);
1432
1433         g_object_class_install_property (object_class,
1434                                          PROP_STORE,
1435                                          g_param_spec_object ("store",
1436                                                              "The store of the view",
1437                                                              "The store of the view",
1438                                                               EMPATHY_TYPE_CONTACT_LIST_STORE,
1439                                                               G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE));
1440         g_object_class_install_property (object_class,
1441                                          PROP_LIST_FEATURES,
1442                                          g_param_spec_flags ("list-features",
1443                                                              "Features of the view",
1444                                                              "Falgs for all enabled features",
1445                                                               EMPATHY_TYPE_CONTACT_LIST_FEATURE_FLAGS,
1446                                                               EMPATHY_CONTACT_LIST_FEATURE_NONE,
1447                                                               G_PARAM_READWRITE));
1448         g_object_class_install_property (object_class,
1449                                          PROP_CONTACT_FEATURES,
1450                                          g_param_spec_flags ("contact-features",
1451                                                              "Features of the contact menu",
1452                                                              "Falgs for all enabled features for the menu",
1453                                                               EMPATHY_TYPE_CONTACT_FEATURE_FLAGS,
1454                                                               EMPATHY_CONTACT_FEATURE_NONE,
1455                                                               G_PARAM_READWRITE));
1456
1457         g_type_class_add_private (object_class, sizeof (EmpathyContactListViewPriv));
1458 }
1459
1460 static void
1461 empathy_contact_list_view_init (EmpathyContactListView *view)
1462 {
1463         EmpathyContactListViewPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (view,
1464                 EMPATHY_TYPE_CONTACT_LIST_VIEW, EmpathyContactListViewPriv);
1465
1466         view->priv = priv;
1467         /* Get saved group states. */
1468         empathy_contact_groups_get_all ();
1469
1470         gtk_tree_view_set_row_separator_func (GTK_TREE_VIEW (view),
1471                                               empathy_contact_list_store_row_separator_func,
1472                                               NULL, NULL);
1473
1474         /* Set up drag target lists. */
1475         priv->file_targets = gtk_target_list_new (drag_types_dest_file,
1476                                                   G_N_ELEMENTS (drag_types_dest_file));
1477
1478         /* Connect to tree view signals rather than override. */
1479         g_signal_connect (view, "button-press-event",
1480                           G_CALLBACK (contact_list_view_button_press_event_cb),
1481                           NULL);
1482         g_signal_connect (view, "key-press-event",
1483                           G_CALLBACK (contact_list_view_key_press_event_cb),
1484                           NULL);
1485         g_signal_connect (view, "row-expanded",
1486                           G_CALLBACK (contact_list_view_row_expand_or_collapse_cb),
1487                           GINT_TO_POINTER (TRUE));
1488         g_signal_connect (view, "row-collapsed",
1489                           G_CALLBACK (contact_list_view_row_expand_or_collapse_cb),
1490                           GINT_TO_POINTER (FALSE));
1491         g_signal_connect (view, "query-tooltip",
1492                           G_CALLBACK (contact_list_view_query_tooltip_cb),
1493                           NULL);
1494 }
1495
1496 EmpathyContactListView *
1497 empathy_contact_list_view_new (EmpathyContactListStore        *store,
1498                                EmpathyContactListFeatureFlags  list_features,
1499                                EmpathyContactFeatureFlags      contact_features)
1500 {
1501         g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST_STORE (store), NULL);
1502
1503         return g_object_new (EMPATHY_TYPE_CONTACT_LIST_VIEW,
1504                              "store", store,
1505                              "contact-features", contact_features,
1506                              "list-features", list_features,
1507                              NULL);
1508 }
1509
1510 EmpathyContact *
1511 empathy_contact_list_view_dup_selected (EmpathyContactListView *view)
1512 {
1513         EmpathyContactListViewPriv *priv;
1514         GtkTreeSelection          *selection;
1515         GtkTreeIter                iter;
1516         GtkTreeModel              *model;
1517         EmpathyContact             *contact;
1518
1519         g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST_VIEW (view), NULL);
1520
1521         priv = GET_PRIV (view);
1522
1523         selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view));
1524         if (!gtk_tree_selection_get_selected (selection, &model, &iter)) {
1525                 return NULL;
1526         }
1527
1528         gtk_tree_model_get (model, &iter,
1529                             EMPATHY_CONTACT_LIST_STORE_COL_CONTACT, &contact,
1530                             -1);
1531
1532         return contact;
1533 }
1534
1535 EmpathyContactListFlags
1536 empathy_contact_list_view_get_flags (EmpathyContactListView *view)
1537 {
1538         EmpathyContactListViewPriv *priv;
1539         GtkTreeSelection          *selection;
1540         GtkTreeIter                iter;
1541         GtkTreeModel              *model;
1542         EmpathyContactListFlags    flags;
1543
1544         g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST_VIEW (view), 0);
1545
1546         priv = GET_PRIV (view);
1547
1548         selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view));
1549         if (!gtk_tree_selection_get_selected (selection, &model, &iter)) {
1550                 return 0;
1551         }
1552
1553         gtk_tree_model_get (model, &iter,
1554                             EMPATHY_CONTACT_LIST_STORE_COL_FLAGS, &flags,
1555                             -1);
1556
1557         return flags;
1558 }
1559
1560 gchar *
1561 empathy_contact_list_view_get_selected_group (EmpathyContactListView *view,
1562                                               gboolean *is_fake_group)
1563 {
1564         EmpathyContactListViewPriv *priv;
1565         GtkTreeSelection          *selection;
1566         GtkTreeIter                iter;
1567         GtkTreeModel              *model;
1568         gboolean                   is_group;
1569         gchar                     *name;
1570         gboolean                   fake;
1571
1572         g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST_VIEW (view), NULL);
1573
1574         priv = GET_PRIV (view);
1575
1576         selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view));
1577         if (!gtk_tree_selection_get_selected (selection, &model, &iter)) {
1578                 return NULL;
1579         }
1580
1581         gtk_tree_model_get (model, &iter,
1582                             EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
1583                             EMPATHY_CONTACT_LIST_STORE_COL_NAME, &name,
1584                             EMPATHY_CONTACT_LIST_STORE_COL_IS_FAKE_GROUP, &fake,
1585                             -1);
1586
1587         if (!is_group) {
1588                 g_free (name);
1589                 return NULL;
1590         }
1591
1592         if (is_fake_group != NULL)
1593                 *is_fake_group = fake;
1594
1595         return name;
1596 }
1597
1598 static gboolean
1599 contact_list_view_remove_dialog_show (GtkWindow   *parent,
1600                                       const gchar *message,
1601                                       const gchar *secondary_text)
1602 {
1603         GtkWidget *dialog;
1604         gboolean res;
1605
1606         dialog = gtk_message_dialog_new (parent, GTK_DIALOG_MODAL,
1607                                          GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE,
1608                                          "%s", message);
1609         gtk_dialog_add_buttons (GTK_DIALOG (dialog),
1610                                 GTK_STOCK_CANCEL, GTK_RESPONSE_NO,
1611                                 GTK_STOCK_DELETE, GTK_RESPONSE_YES,
1612                                 NULL);
1613         gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
1614                                                   "%s", secondary_text);
1615
1616         gtk_widget_show (dialog);
1617
1618         res = gtk_dialog_run (GTK_DIALOG (dialog));
1619         gtk_widget_destroy (dialog);
1620
1621         return (res == GTK_RESPONSE_YES);
1622 }
1623
1624 static void
1625 contact_list_view_group_remove_activate_cb (GtkMenuItem            *menuitem,
1626                                             EmpathyContactListView *view)
1627 {
1628         EmpathyContactListViewPriv *priv = GET_PRIV (view);
1629         gchar                      *group;
1630
1631         group = empathy_contact_list_view_get_selected_group (view, NULL);
1632         if (group) {
1633                 gchar     *text;
1634                 GtkWindow *parent;
1635
1636                 text = g_strdup_printf (_("Do you really want to remove the group '%s'?"), group);
1637                 parent = empathy_get_toplevel_window (GTK_WIDGET (view));
1638                 if (contact_list_view_remove_dialog_show (parent, _("Removing group"), text)) {
1639                         EmpathyContactList *list;
1640
1641                         list = empathy_contact_list_store_get_list_iface (priv->store);
1642                         empathy_contact_list_remove_group (list, group);
1643                 }
1644
1645                 g_free (text);
1646         }
1647
1648         g_free (group);
1649 }
1650
1651 GtkWidget *
1652 empathy_contact_list_view_get_group_menu (EmpathyContactListView *view)
1653 {
1654         EmpathyContactListViewPriv *priv = GET_PRIV (view);
1655         gchar                      *group;
1656         GtkWidget                  *menu;
1657         GtkWidget                  *item;
1658         GtkWidget                  *image;
1659         gboolean                   is_fake_group;
1660
1661         g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST_VIEW (view), NULL);
1662
1663         if (!(priv->list_features & (EMPATHY_CONTACT_LIST_FEATURE_GROUPS_RENAME |
1664                                      EMPATHY_CONTACT_LIST_FEATURE_GROUPS_REMOVE))) {
1665                 return NULL;
1666         }
1667
1668         group = empathy_contact_list_view_get_selected_group (view, &is_fake_group);
1669         if (!group || is_fake_group) {
1670                 /* We can't alter fake groups */
1671                 return NULL;
1672         }
1673
1674         menu = gtk_menu_new ();
1675
1676         /* FIXME: Not implemented yet
1677         if (priv->features & EMPATHY_CONTACT_LIST_FEATURE_GROUPS_RENAME) {
1678                 item = gtk_menu_item_new_with_mnemonic (_("Re_name"));
1679                 gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
1680                 gtk_widget_show (item);
1681                 g_signal_connect (item, "activate",
1682                                   G_CALLBACK (contact_list_view_group_rename_activate_cb),
1683                                   view);
1684         }*/
1685
1686         if (priv->list_features & EMPATHY_CONTACT_LIST_FEATURE_GROUPS_REMOVE) {
1687                 item = gtk_image_menu_item_new_with_mnemonic (_("_Remove"));
1688                 image = gtk_image_new_from_icon_name (GTK_STOCK_REMOVE,
1689                                                       GTK_ICON_SIZE_MENU);
1690                 gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image);
1691                 gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
1692                 gtk_widget_show (item);
1693                 g_signal_connect (item, "activate",
1694                                   G_CALLBACK (contact_list_view_group_remove_activate_cb),
1695                                   view);
1696         }
1697
1698         g_free (group);
1699
1700         return menu;
1701 }
1702
1703 static void
1704 contact_list_view_remove_activate_cb (GtkMenuItem            *menuitem,
1705                                       EmpathyContactListView *view)
1706 {
1707         EmpathyContactListViewPriv *priv = GET_PRIV (view);
1708         EmpathyContact             *contact;
1709
1710         contact = empathy_contact_list_view_dup_selected (view);
1711
1712         if (contact) {
1713                 gchar     *text;
1714                 GtkWindow *parent;
1715
1716                 parent = empathy_get_toplevel_window (GTK_WIDGET (view));
1717                 text = g_strdup_printf (_("Do you really want to remove the contact '%s'?"),
1718                                         empathy_contact_get_name (contact));
1719                 if (contact_list_view_remove_dialog_show (parent, _("Removing contact"), text)) {
1720                         EmpathyContactList *list;
1721
1722                         list = empathy_contact_list_store_get_list_iface (priv->store);
1723                         empathy_contact_list_remove (list, contact, "");
1724                 }
1725
1726                 g_free (text);
1727                 g_object_unref (contact);
1728         }
1729 }
1730
1731 GtkWidget *
1732 empathy_contact_list_view_get_contact_menu (EmpathyContactListView *view)
1733 {
1734         EmpathyContactListViewPriv *priv = GET_PRIV (view);
1735         EmpathyContact             *contact;
1736         GtkWidget                  *menu;
1737         GtkWidget                  *item;
1738         GtkWidget                  *image;
1739         EmpathyContactListFlags     flags;
1740
1741         g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST_VIEW (view), NULL);
1742
1743         contact = empathy_contact_list_view_dup_selected (view);
1744         if (!contact) {
1745                 return NULL;
1746         }
1747         flags = empathy_contact_list_view_get_flags (view);
1748
1749         menu = empathy_contact_menu_new (contact, priv->contact_features);
1750
1751         /* Remove contact */
1752         if (priv->list_features & EMPATHY_CONTACT_LIST_FEATURE_CONTACT_REMOVE &&
1753             flags & EMPATHY_CONTACT_LIST_CAN_REMOVE) {
1754                 /* create the menu if required, or just add a separator */
1755                 if (!menu) {
1756                         menu = gtk_menu_new ();
1757                 } else {
1758                         item = gtk_separator_menu_item_new ();
1759                         gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
1760                         gtk_widget_show (item);
1761                 }
1762
1763                 /* Remove */
1764                 item = gtk_image_menu_item_new_with_mnemonic (_("_Remove"));
1765                 image = gtk_image_new_from_icon_name (GTK_STOCK_REMOVE,
1766                                                       GTK_ICON_SIZE_MENU);
1767                 gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image);
1768                 gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
1769                 gtk_widget_show (item);
1770                 g_signal_connect (item, "activate",
1771                                   G_CALLBACK (contact_list_view_remove_activate_cb),
1772                                   view);
1773         }
1774
1775         g_object_unref (contact);
1776
1777         return menu;
1778 }
1779