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