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