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