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