]> git.0d.be Git - empathy.git/blob - libempathy-gtk/empathy-contact-list-view.c
Improve dispatcher. Fixes bug #465928.
[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., 59 Temple Place - Suite 330,
19  * Boston, MA 02111-1307, 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.h>
31 #include <gtk/gtk.h>
32 #include <glade/glade.h>
33
34 #include <libmissioncontrol/mc-account.h>
35
36 #include <libempathy/empathy-contact-factory.h>
37 #include <libempathy/empathy-contact-list.h>
38 #include <libempathy/empathy-contact-groups.h>
39 #include <libempathy/empathy-dispatcher.h>
40 #include <libempathy/empathy-utils.h>
41
42 #include "empathy-contact-list-view.h"
43 #include "empathy-contact-list-store.h"
44 #include "empathy-images.h"
45 #include "empathy-cell-renderer-expander.h"
46 #include "empathy-cell-renderer-text.h"
47 #include "empathy-cell-renderer-activatable.h"
48 #include "empathy-ui-utils.h"
49 #include "empathy-gtk-enum-types.h"
50 #include "empathy-gtk-marshal.h"
51
52 #define DEBUG_FLAG EMPATHY_DEBUG_CONTACT
53 #include <libempathy/empathy-debug.h>
54
55 /* Flashing delay for icons (milliseconds). */
56 #define FLASH_TIMEOUT 500
57
58 /* Active users are those which have recently changed state
59  * (e.g. online, offline or from normal to a busy state).
60  */
61
62 #define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyContactListView)
63 typedef struct {
64         EmpathyContactListStore        *store;
65         GtkTreeRowReference            *drag_row;
66         EmpathyContactListFeatureFlags  list_features;
67         EmpathyContactFeatureFlags      contact_features;
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_URL,
92         DND_DRAG_TYPE_STRING,
93 };
94
95 static const GtkTargetEntry drag_types_dest[] = {
96         { "text/contact-id", 0, DND_DRAG_TYPE_CONTACT_ID },
97         { "text/uri-list",   0, DND_DRAG_TYPE_URL },
98         { "text/plain",      0, DND_DRAG_TYPE_STRING },
99         { "STRING",          0, DND_DRAG_TYPE_STRING },
100 };
101
102 static const GtkTargetEntry drag_types_source[] = {
103         { "text/contact-id", 0, DND_DRAG_TYPE_CONTACT_ID },
104 };
105
106 static GdkAtom drag_atoms_dest[G_N_ELEMENTS (drag_types_dest)];
107 static GdkAtom drag_atoms_source[G_N_ELEMENTS (drag_types_source)];
108
109 enum {
110         DRAG_CONTACT_RECEIVED,
111         LAST_SIGNAL
112 };
113
114 static guint signals[LAST_SIGNAL];
115
116 G_DEFINE_TYPE (EmpathyContactListView, empathy_contact_list_view, GTK_TYPE_TREE_VIEW);
117
118 static void
119 contact_list_view_drag_data_received (GtkWidget         *widget,
120                                       GdkDragContext    *context,
121                                       gint               x,
122                                       gint               y,
123                                       GtkSelectionData  *selection,
124                                       guint              info,
125                                       guint              time)
126 {
127         EmpathyContactListViewPriv *priv;
128         EmpathyContactList         *list;
129         EmpathyContactFactory      *factory;
130         McAccount                  *account;
131         GtkTreeModel               *model;
132         GtkTreePath                *path;
133         GtkTreeViewDropPosition     position;
134         EmpathyContact             *contact = NULL;
135         const gchar                *id;
136         gchar                     **strv;
137         gchar                      *new_group = NULL;
138         gchar                      *old_group = NULL;
139         gboolean                    is_row;
140
141         priv = GET_PRIV (widget);
142
143         id = (const gchar*) selection->data;
144         DEBUG ("Received %s%s drag & drop contact from roster with id:'%s'",
145                 context->action == GDK_ACTION_MOVE ? "move" : "",
146                 context->action == GDK_ACTION_COPY ? "copy" : "",
147                 id);
148
149         strv = g_strsplit (id, "/", 2);
150         factory = empathy_contact_factory_new ();
151         account = mc_account_lookup (strv[0]);
152         if (account) {
153                 contact = empathy_contact_factory_get_from_id (factory,
154                                                                account,
155                                                                strv[1]);
156                 g_object_unref (account);
157         }
158         g_object_unref (factory);
159         g_strfreev (strv);
160
161         if (!contact) {
162                 DEBUG ("No contact found associated with drag & drop");
163                 return;
164         }
165
166         empathy_contact_run_until_ready (contact,
167                                          EMPATHY_CONTACT_READY_HANDLE,
168                                          NULL);
169
170         model = gtk_tree_view_get_model (GTK_TREE_VIEW (widget));
171
172         /* Get source group information. */
173         if (priv->drag_row) {
174                 path = gtk_tree_row_reference_get_path (priv->drag_row);
175                 if (path) {
176                         old_group = empathy_contact_list_store_get_parent_group (model, path, NULL);
177                         gtk_tree_path_free (path);
178                 }
179         }
180
181         /* Get destination group information. */
182         is_row = gtk_tree_view_get_dest_row_at_pos (GTK_TREE_VIEW (widget),
183                                                     x,
184                                                     y,
185                                                     &path,
186                                                     &position);
187
188         if (is_row) {
189                 new_group = empathy_contact_list_store_get_parent_group (model, path, NULL);
190                 gtk_tree_path_free (path);
191         }
192
193         DEBUG ("contact %s (%d) dragged from '%s' to '%s'",
194                 empathy_contact_get_id (contact),
195                 empathy_contact_get_handle (contact),
196                 old_group, new_group);
197
198         list = empathy_contact_list_store_get_list_iface (priv->store);
199         if (new_group) {
200                 empathy_contact_list_add_to_group (list, contact, new_group);
201         }
202         if (old_group && context->action == GDK_ACTION_MOVE) {  
203                 empathy_contact_list_remove_from_group (list, contact, old_group);
204         }
205
206         g_free (old_group);
207         g_free (new_group);
208
209         gtk_drag_finish (context, TRUE, FALSE, GDK_CURRENT_TIME);
210 }
211
212 static gboolean
213 contact_list_view_drag_motion_cb (DragMotionData *data)
214 {
215         gtk_tree_view_expand_row (GTK_TREE_VIEW (data->view),
216                                   data->path,
217                                   FALSE);
218
219         data->timeout_id = 0;
220
221         return FALSE;
222 }
223
224 static gboolean
225 contact_list_view_drag_motion (GtkWidget      *widget,
226                                GdkDragContext *context,
227                                gint            x,
228                                gint            y,
229                                guint           time)
230 {
231         static DragMotionData *dm = NULL;
232         GtkTreePath           *path;
233         gboolean               is_row;
234         gboolean               is_different = FALSE;
235         gboolean               cleanup = TRUE;
236
237         is_row = gtk_tree_view_get_path_at_pos (GTK_TREE_VIEW (widget),
238                                                 x,
239                                                 y,
240                                                 &path,
241                                                 NULL,
242                                                 NULL,
243                                                 NULL);
244
245         cleanup &= (!dm);
246
247         if (is_row) {
248                 cleanup &= (dm && gtk_tree_path_compare (dm->path, path) != 0);
249                 is_different = (!dm || (dm && gtk_tree_path_compare (dm->path, path) != 0));
250         } else {
251                 cleanup &= FALSE;
252         }
253
254         if (!is_different && !cleanup) {
255                 return TRUE;
256         }
257
258         if (dm) {
259                 gtk_tree_path_free (dm->path);
260                 if (dm->timeout_id) {
261                         g_source_remove (dm->timeout_id);
262                 }
263
264                 g_free (dm);
265
266                 dm = NULL;
267         }
268
269         if (!gtk_tree_view_row_expanded (GTK_TREE_VIEW (widget), path)) {
270                 dm = g_new0 (DragMotionData, 1);
271
272                 dm->view = EMPATHY_CONTACT_LIST_VIEW (widget);
273                 dm->path = gtk_tree_path_copy (path);
274
275                 dm->timeout_id = g_timeout_add_seconds (1,
276                         (GSourceFunc) contact_list_view_drag_motion_cb,
277                         dm);
278         }
279
280         return TRUE;
281 }
282
283 static void
284 contact_list_view_drag_begin (GtkWidget      *widget,
285                               GdkDragContext *context)
286 {
287         EmpathyContactListViewPriv *priv;
288         GtkTreeSelection          *selection;
289         GtkTreeModel              *model;
290         GtkTreePath               *path;
291         GtkTreeIter                iter;
292
293         priv = GET_PRIV (widget);
294
295         GTK_WIDGET_CLASS (empathy_contact_list_view_parent_class)->drag_begin (widget,
296                                                                               context);
297
298         selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (widget));
299         if (!gtk_tree_selection_get_selected (selection, &model, &iter)) {
300                 return;
301         }
302
303         path = gtk_tree_model_get_path (model, &iter);
304         priv->drag_row = gtk_tree_row_reference_new (model, path);
305         gtk_tree_path_free (path);
306 }
307
308 static void
309 contact_list_view_drag_data_get (GtkWidget        *widget,
310                                  GdkDragContext   *context,
311                                  GtkSelectionData *selection,
312                                  guint             info,
313                                  guint             time)
314 {
315         EmpathyContactListViewPriv *priv;
316         GtkTreePath                *src_path;
317         GtkTreeIter                 iter;
318         GtkTreeModel               *model;
319         EmpathyContact             *contact;
320         McAccount                  *account;
321         const gchar                *contact_id;
322         const gchar                *account_id;
323         gchar                      *str;
324
325         priv = GET_PRIV (widget);
326
327         model = gtk_tree_view_get_model (GTK_TREE_VIEW (widget));
328         if (!priv->drag_row) {
329                 return;
330         }
331
332         src_path = gtk_tree_row_reference_get_path (priv->drag_row);
333         if (!src_path) {
334                 return;
335         }
336
337         if (!gtk_tree_model_get_iter (model, &iter, src_path)) {
338                 gtk_tree_path_free (src_path);
339                 return;
340         }
341
342         gtk_tree_path_free (src_path);
343
344         contact = empathy_contact_list_view_get_selected (EMPATHY_CONTACT_LIST_VIEW (widget));
345         if (!contact) {
346                 return;
347         }
348
349         account = empathy_contact_get_account (contact);
350         account_id = mc_account_get_unique_name (account);
351         contact_id = empathy_contact_get_id (contact);
352         g_object_unref (contact);
353         str = g_strconcat (account_id, "/", contact_id, NULL);
354
355         switch (info) {
356         case DND_DRAG_TYPE_CONTACT_ID:
357                 gtk_selection_data_set (selection, drag_atoms_source[info], 8,
358                                         (guchar*)str, strlen (str) + 1);
359                 break;
360         }
361
362         g_free (str);
363 }
364
365 static void
366 contact_list_view_drag_end (GtkWidget      *widget,
367                             GdkDragContext *context)
368 {
369         EmpathyContactListViewPriv *priv;
370
371         priv = GET_PRIV (widget);
372
373         GTK_WIDGET_CLASS (empathy_contact_list_view_parent_class)->drag_end (widget,
374                                                                             context);
375
376         if (priv->drag_row) {
377                 gtk_tree_row_reference_free (priv->drag_row);
378                 priv->drag_row = NULL;
379         }
380 }
381
382 static gboolean
383 contact_list_view_drag_drop (GtkWidget      *widget,
384                              GdkDragContext *drag_context,
385                              gint            x,
386                              gint            y,
387                              guint           time)
388 {
389         return FALSE;
390 }
391
392 typedef struct {
393         EmpathyContactListView *view;
394         guint                   button;
395         guint32                 time;
396 } MenuPopupData;
397
398 static gboolean
399 contact_list_view_popup_menu_idle_cb (gpointer user_data)
400 {
401         MenuPopupData *data = user_data;
402         GtkWidget     *menu;
403
404         menu = empathy_contact_list_view_get_contact_menu (data->view);
405         if (!menu) {
406                 menu = empathy_contact_list_view_get_group_menu (data->view);
407         }
408
409         if (!menu) {
410                 goto OUT;
411         }
412
413         gtk_widget_show (menu);
414
415         gtk_menu_popup (GTK_MENU (menu),
416                         NULL, NULL, NULL, NULL,
417                         data->button, data->time);
418
419 OUT:
420         g_slice_free (MenuPopupData, data);
421
422         return FALSE;
423 }
424
425 static gboolean
426 contact_list_view_button_press_event_cb (EmpathyContactListView *view,
427                                          GdkEventButton         *event,
428                                          gpointer                user_data)
429 {
430         if (event->button == 3) {
431                 MenuPopupData *data;
432
433                 data = g_slice_new (MenuPopupData);
434                 data->view = view;
435                 data->button = event->button;
436                 data->time = event->time;
437                 g_idle_add (contact_list_view_popup_menu_idle_cb, data);
438         }
439
440         return FALSE;
441 }
442
443 static void
444 contact_list_view_row_activated_cb (EmpathyContactListView *view,
445                                     GtkTreePath            *path,
446                                     GtkTreeViewColumn      *col,
447                                     gpointer                user_data)
448 {
449         EmpathyContactListViewPriv *priv = GET_PRIV (view);
450         EmpathyContact             *contact;
451         GtkTreeModel               *model;
452         GtkTreeIter                 iter;
453
454         if (!(priv->contact_features & EMPATHY_CONTACT_FEATURE_CHAT)) {
455                 return;
456         }
457
458         model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
459
460         gtk_tree_model_get_iter (model, &iter, path);
461         gtk_tree_model_get (model, &iter,
462                             EMPATHY_CONTACT_LIST_STORE_COL_CONTACT, &contact,
463                             -1);
464
465         if (contact) {
466                 empathy_dispatcher_chat_with_contact (contact);
467                 g_object_unref (contact);
468         }
469 }
470
471 static void
472 contact_list_view_voip_activated_cb (EmpathyCellRendererActivatable *cell,
473                                      const gchar                    *path_string,
474                                      EmpathyContactListView         *view)
475 {
476         EmpathyContactListViewPriv *priv = GET_PRIV (view);
477         GtkTreeModel               *model;
478         GtkTreeIter                 iter;
479         EmpathyContact             *contact;
480
481         if (!(priv->contact_features & EMPATHY_CONTACT_FEATURE_CALL)) {
482                 return;
483         }
484
485         model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
486         if (!gtk_tree_model_get_iter_from_string (model, &iter, path_string)) {
487                 return;
488         }
489
490         gtk_tree_model_get (model, &iter,
491                             EMPATHY_CONTACT_LIST_STORE_COL_CONTACT, &contact,
492                             -1);
493
494         if (contact) {
495                 empathy_dispatcher_call_with_contact (contact);
496                 g_object_unref (contact);
497         }
498 }
499
500 static void
501 contact_list_view_cell_set_background (EmpathyContactListView *view,
502                                        GtkCellRenderer       *cell,
503                                        gboolean               is_group,
504                                        gboolean               is_active)
505 {
506         GdkColor  color;
507         GtkStyle *style;
508
509         style = gtk_widget_get_style (GTK_WIDGET (view));
510
511         if (!is_group && is_active) {
512                 color = style->bg[GTK_STATE_SELECTED];
513
514                 /* Here we take the current theme colour and add it to
515                  * the colour for white and average the two. This
516                  * gives a colour which is inline with the theme but
517                  * slightly whiter.
518                  */
519                 color.red = (color.red + (style->white).red) / 2;
520                 color.green = (color.green + (style->white).green) / 2;
521                 color.blue = (color.blue + (style->white).blue) / 2;
522
523                 g_object_set (cell,
524                               "cell-background-gdk", &color,
525                               NULL);
526         } else {
527                 g_object_set (cell,
528                               "cell-background-gdk", NULL,
529                               NULL);
530         }
531 }
532
533 static void
534 contact_list_view_pixbuf_cell_data_func (GtkTreeViewColumn     *tree_column,
535                                          GtkCellRenderer       *cell,
536                                          GtkTreeModel          *model,
537                                          GtkTreeIter           *iter,
538                                          EmpathyContactListView *view)
539 {
540         gchar    *icon_name;
541         gboolean  is_group;
542         gboolean  is_active;
543
544         gtk_tree_model_get (model, iter,
545                             EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
546                             EMPATHY_CONTACT_LIST_STORE_COL_IS_ACTIVE, &is_active,
547                             EMPATHY_CONTACT_LIST_STORE_COL_ICON_STATUS, &icon_name,
548                             -1);
549
550         g_object_set (cell,
551                       "visible", !is_group,
552                       "icon-name", icon_name,
553                       NULL);
554
555         g_free (icon_name);
556
557         contact_list_view_cell_set_background (view, cell, is_group, is_active);
558 }
559
560 static void
561 contact_list_view_voip_cell_data_func (GtkTreeViewColumn      *tree_column,
562                                        GtkCellRenderer        *cell,
563                                        GtkTreeModel           *model,
564                                        GtkTreeIter            *iter,
565                                        EmpathyContactListView *view)
566 {
567         gboolean is_group;
568         gboolean is_active;
569         gboolean can_voip;
570
571         gtk_tree_model_get (model, iter,
572                             EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
573                             EMPATHY_CONTACT_LIST_STORE_COL_IS_ACTIVE, &is_active,
574                             EMPATHY_CONTACT_LIST_STORE_COL_CAN_VOIP, &can_voip,
575                             -1);
576
577         g_object_set (cell,
578                       "visible", !is_group && can_voip,
579                       "icon-name", EMPATHY_IMAGE_VOIP,
580                       NULL);
581
582         contact_list_view_cell_set_background (view, cell, is_group, is_active);
583 }
584
585 static void
586 contact_list_view_avatar_cell_data_func (GtkTreeViewColumn     *tree_column,
587                                          GtkCellRenderer       *cell,
588                                          GtkTreeModel          *model,
589                                          GtkTreeIter           *iter,
590                                          EmpathyContactListView *view)
591 {
592         GdkPixbuf *pixbuf;
593         gboolean   show_avatar;
594         gboolean   is_group;
595         gboolean   is_active;
596
597         gtk_tree_model_get (model, iter,
598                             EMPATHY_CONTACT_LIST_STORE_COL_PIXBUF_AVATAR, &pixbuf,
599                             EMPATHY_CONTACT_LIST_STORE_COL_PIXBUF_AVATAR_VISIBLE, &show_avatar,
600                             EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
601                             EMPATHY_CONTACT_LIST_STORE_COL_IS_ACTIVE, &is_active,
602                             -1);
603
604         g_object_set (cell,
605                       "visible", !is_group && show_avatar,
606                       "pixbuf", pixbuf,
607                       NULL);
608
609         if (pixbuf) {
610                 g_object_unref (pixbuf);
611         }
612
613         contact_list_view_cell_set_background (view, cell, is_group, is_active);
614 }
615
616 static void
617 contact_list_view_text_cell_data_func (GtkTreeViewColumn     *tree_column,
618                                        GtkCellRenderer       *cell,
619                                        GtkTreeModel          *model,
620                                        GtkTreeIter           *iter,
621                                        EmpathyContactListView *view)
622 {
623         gboolean is_group;
624         gboolean is_active;
625         gboolean show_status;
626
627         gtk_tree_model_get (model, iter,
628                             EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
629                             EMPATHY_CONTACT_LIST_STORE_COL_IS_ACTIVE, &is_active,
630                             EMPATHY_CONTACT_LIST_STORE_COL_STATUS_VISIBLE, &show_status,
631                             -1);
632
633         g_object_set (cell,
634                       "show-status", show_status,
635                       NULL);
636
637         contact_list_view_cell_set_background (view, cell, is_group, is_active);
638 }
639
640 static void
641 contact_list_view_expander_cell_data_func (GtkTreeViewColumn     *column,
642                                            GtkCellRenderer       *cell,
643                                            GtkTreeModel          *model,
644                                            GtkTreeIter           *iter,
645                                            EmpathyContactListView *view)
646 {
647         gboolean is_group;
648         gboolean is_active;
649
650         gtk_tree_model_get (model, iter,
651                             EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
652                             EMPATHY_CONTACT_LIST_STORE_COL_IS_ACTIVE, &is_active,
653                             -1);
654
655         if (gtk_tree_model_iter_has_child (model, iter)) {
656                 GtkTreePath *path;
657                 gboolean     row_expanded;
658
659                 path = gtk_tree_model_get_path (model, iter);
660                 row_expanded = gtk_tree_view_row_expanded (GTK_TREE_VIEW (column->tree_view), path);
661                 gtk_tree_path_free (path);
662
663                 g_object_set (cell,
664                               "visible", TRUE,
665                               "expander-style", row_expanded ? GTK_EXPANDER_EXPANDED : GTK_EXPANDER_COLLAPSED,
666                               NULL);
667         } else {
668                 g_object_set (cell, "visible", FALSE, NULL);
669         }
670
671         contact_list_view_cell_set_background (view, cell, is_group, is_active);
672 }
673
674 static void
675 contact_list_view_row_expand_or_collapse_cb (EmpathyContactListView *view,
676                                              GtkTreeIter           *iter,
677                                              GtkTreePath           *path,
678                                              gpointer               user_data)
679 {
680         EmpathyContactListViewPriv *priv = GET_PRIV (view);
681         GtkTreeModel               *model;
682         gchar                      *name;
683         gboolean                    expanded;
684
685         if (!(priv->list_features & EMPATHY_CONTACT_LIST_FEATURE_GROUPS_SAVE)) {
686                 return;
687         }
688
689         model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
690
691         gtk_tree_model_get (model, iter,
692                             EMPATHY_CONTACT_LIST_STORE_COL_NAME, &name,
693                             -1);
694
695         expanded = GPOINTER_TO_INT (user_data);
696         empathy_contact_group_set_expanded (name, expanded);
697
698         g_free (name);
699 }
700
701 static void
702 contact_list_view_row_has_child_toggled_cb (GtkTreeModel          *model,
703                                             GtkTreePath           *path,
704                                             GtkTreeIter           *iter,
705                                             EmpathyContactListView *view)
706 {
707         EmpathyContactListViewPriv *priv = GET_PRIV (view);
708         gboolean  is_group = FALSE;
709         gchar    *name = NULL;
710
711         gtk_tree_model_get (model, iter,
712                             EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
713                             EMPATHY_CONTACT_LIST_STORE_COL_NAME, &name,
714                             -1);
715
716         if (!is_group || G_STR_EMPTY (name)) {
717                 g_free (name);
718                 return;
719         }
720
721         if (!(priv->list_features & EMPATHY_CONTACT_LIST_FEATURE_GROUPS_SAVE) ||
722             empathy_contact_group_get_expanded (name)) {
723                 g_signal_handlers_block_by_func (view,
724                                                  contact_list_view_row_expand_or_collapse_cb,
725                                                  GINT_TO_POINTER (TRUE));
726                 gtk_tree_view_expand_row (GTK_TREE_VIEW (view), path, TRUE);
727                 g_signal_handlers_unblock_by_func (view,
728                                                    contact_list_view_row_expand_or_collapse_cb,
729                                                    GINT_TO_POINTER (TRUE));
730         } else {
731                 g_signal_handlers_block_by_func (view,
732                                                  contact_list_view_row_expand_or_collapse_cb,
733                                                  GINT_TO_POINTER (FALSE));
734                 gtk_tree_view_collapse_row (GTK_TREE_VIEW (view), path);
735                 g_signal_handlers_unblock_by_func (view,
736                                                    contact_list_view_row_expand_or_collapse_cb,
737                                                    GINT_TO_POINTER (FALSE));
738         }
739
740         g_free (name);
741 }
742
743 static void
744 contact_list_view_setup (EmpathyContactListView *view)
745 {
746         EmpathyContactListViewPriv *priv;
747         GtkCellRenderer           *cell;
748         GtkTreeViewColumn         *col;
749         gint                       i;
750
751         priv = GET_PRIV (view);
752
753         gtk_tree_view_set_search_equal_func (GTK_TREE_VIEW (view),
754                                              empathy_contact_list_store_search_equal_func,
755                                              NULL, NULL);
756
757         g_signal_connect (priv->store, "row-has-child-toggled",
758                           G_CALLBACK (contact_list_view_row_has_child_toggled_cb),
759                           view);
760         gtk_tree_view_set_model (GTK_TREE_VIEW (view),
761                                  GTK_TREE_MODEL (priv->store));
762
763         /* Setup view */
764         g_object_set (view,
765                       "headers-visible", FALSE,
766                       "reorderable", TRUE,
767                       "show-expanders", FALSE,
768                       NULL);
769
770         col = gtk_tree_view_column_new ();
771
772         /* State */
773         cell = gtk_cell_renderer_pixbuf_new ();
774         gtk_tree_view_column_pack_start (col, cell, FALSE);
775         gtk_tree_view_column_set_cell_data_func (
776                 col, cell,
777                 (GtkTreeCellDataFunc) contact_list_view_pixbuf_cell_data_func,
778                 view, NULL);
779
780         g_object_set (cell,
781                       "xpad", 5,
782                       "ypad", 1,
783                       "visible", FALSE,
784                       NULL);
785
786         /* Name */
787         cell = empathy_cell_renderer_text_new ();
788         gtk_tree_view_column_pack_start (col, cell, TRUE);
789         gtk_tree_view_column_set_cell_data_func (
790                 col, cell,
791                 (GtkTreeCellDataFunc) contact_list_view_text_cell_data_func,
792                 view, NULL);
793
794         gtk_tree_view_column_add_attribute (col, cell,
795                                             "name", EMPATHY_CONTACT_LIST_STORE_COL_NAME);
796         gtk_tree_view_column_add_attribute (col, cell,
797                                             "status", EMPATHY_CONTACT_LIST_STORE_COL_STATUS);
798         gtk_tree_view_column_add_attribute (col, cell,
799                                             "is_group", EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP);
800
801         /* Voip Capability Icon */
802         cell = empathy_cell_renderer_activatable_new ();
803         gtk_tree_view_column_pack_start (col, cell, FALSE);
804         gtk_tree_view_column_set_cell_data_func (
805                 col, cell,
806                 (GtkTreeCellDataFunc) contact_list_view_voip_cell_data_func,
807                 view, NULL);
808
809         g_object_set (cell,
810                       "visible", FALSE,
811                       NULL);
812
813         g_signal_connect (cell, "path-activated",
814                           G_CALLBACK (contact_list_view_voip_activated_cb),
815                           view);
816
817         /* Avatar */
818         cell = gtk_cell_renderer_pixbuf_new ();
819         gtk_tree_view_column_pack_start (col, cell, FALSE);
820         gtk_tree_view_column_set_cell_data_func (
821                 col, cell,
822                 (GtkTreeCellDataFunc) contact_list_view_avatar_cell_data_func,
823                 view, NULL);
824
825         g_object_set (cell,
826                       "xpad", 0,
827                       "ypad", 0,
828                       "visible", FALSE,
829                       "width", 32,
830                       "height", 32,
831                       NULL);
832
833         /* Expander */
834         cell = empathy_cell_renderer_expander_new ();
835         gtk_tree_view_column_pack_end (col, cell, FALSE);
836         gtk_tree_view_column_set_cell_data_func (
837                 col, cell,
838                 (GtkTreeCellDataFunc) contact_list_view_expander_cell_data_func,
839                 view, NULL);
840
841         /* Actually add the column now we have added all cell renderers */
842         gtk_tree_view_append_column (GTK_TREE_VIEW (view), col);
843
844         /* Drag & Drop. */
845         for (i = 0; i < G_N_ELEMENTS (drag_types_dest); ++i) {
846                 drag_atoms_dest[i] = gdk_atom_intern (drag_types_dest[i].target,
847                                                       FALSE);
848         }
849
850         for (i = 0; i < G_N_ELEMENTS (drag_types_source); ++i) {
851                 drag_atoms_source[i] = gdk_atom_intern (drag_types_source[i].target,
852                                                         FALSE);
853         }
854 }
855
856 static void
857 contact_list_view_set_list_features (EmpathyContactListView         *view,
858                                      EmpathyContactListFeatureFlags  features)
859 {
860         EmpathyContactListViewPriv *priv = GET_PRIV (view);
861
862         g_return_if_fail (EMPATHY_IS_CONTACT_LIST_VIEW (view));
863
864         priv->list_features = features;
865
866         /* Update DnD source/dest */
867         if (features & EMPATHY_CONTACT_LIST_FEATURE_CONTACT_DRAG) {
868                 gtk_drag_source_set (GTK_WIDGET (view),
869                                      GDK_BUTTON1_MASK,
870                                      drag_types_source,
871                                      G_N_ELEMENTS (drag_types_source),
872                                      GDK_ACTION_MOVE | GDK_ACTION_COPY);
873         } else {
874                 gtk_drag_source_unset (GTK_WIDGET (view));
875
876         }
877
878         if (features & EMPATHY_CONTACT_LIST_FEATURE_CONTACT_DROP) {
879                 gtk_drag_dest_set (GTK_WIDGET (view),
880                                    GTK_DEST_DEFAULT_ALL,
881                                    drag_types_dest,
882                                    G_N_ELEMENTS (drag_types_dest),
883                                    GDK_ACTION_MOVE | GDK_ACTION_COPY);
884         } else {
885                 /* FIXME: URI could still be  droped depending on FT feature */
886                 gtk_drag_dest_unset (GTK_WIDGET (view));
887         }
888 }
889
890 static void
891 contact_list_view_finalize (GObject *object)
892 {
893         EmpathyContactListViewPriv *priv;
894
895         priv = GET_PRIV (object);
896
897         if (priv->store) {
898                 g_object_unref (priv->store);
899         }
900
901         G_OBJECT_CLASS (empathy_contact_list_view_parent_class)->finalize (object);
902 }
903
904 static void
905 contact_list_view_get_property (GObject    *object,
906                                 guint       param_id,
907                                 GValue     *value,
908                                 GParamSpec *pspec)
909 {
910         EmpathyContactListViewPriv *priv;
911
912         priv = GET_PRIV (object);
913
914         switch (param_id) {
915         case PROP_STORE:
916                 g_value_set_object (value, priv->store);
917                 break;
918         case PROP_LIST_FEATURES:
919                 g_value_set_flags (value, priv->list_features);
920                 break;
921         case PROP_CONTACT_FEATURES:
922                 g_value_set_flags (value, priv->contact_features);
923                 break;
924         default:
925                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
926                 break;
927         };
928 }
929
930 static void
931 contact_list_view_set_property (GObject      *object,
932                                 guint         param_id,
933                                 const GValue *value,
934                                 GParamSpec   *pspec)
935 {
936         EmpathyContactListView     *view = EMPATHY_CONTACT_LIST_VIEW (object);
937         EmpathyContactListViewPriv *priv = GET_PRIV (object);
938
939         switch (param_id) {
940         case PROP_STORE:
941                 priv->store = g_value_dup_object (value);
942                 contact_list_view_setup (view);
943                 break;
944         case PROP_LIST_FEATURES:
945                 contact_list_view_set_list_features (view, g_value_get_flags (value));
946                 break;
947         case PROP_CONTACT_FEATURES:
948                 priv->contact_features = g_value_get_flags (value);
949                 break;
950         default:
951                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
952                 break;
953         };
954 }
955
956 static void
957 empathy_contact_list_view_class_init (EmpathyContactListViewClass *klass)
958 {
959         GObjectClass   *object_class = G_OBJECT_CLASS (klass);
960         GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
961
962         object_class->finalize = contact_list_view_finalize;
963         object_class->get_property = contact_list_view_get_property;
964         object_class->set_property = contact_list_view_set_property;
965
966         widget_class->drag_data_received = contact_list_view_drag_data_received;
967         widget_class->drag_drop          = contact_list_view_drag_drop;
968         widget_class->drag_begin         = contact_list_view_drag_begin;
969         widget_class->drag_data_get      = contact_list_view_drag_data_get;
970         widget_class->drag_end           = contact_list_view_drag_end;
971         /* FIXME: noticed but when you drag the row over the treeview
972          * fast, it seems to stop redrawing itself, if we don't
973          * connect this signal, all is fine.
974          */
975         widget_class->drag_motion        = contact_list_view_drag_motion;
976
977         signals[DRAG_CONTACT_RECEIVED] =
978                 g_signal_new ("drag-contact-received",
979                               G_OBJECT_CLASS_TYPE (klass),
980                               G_SIGNAL_RUN_LAST,
981                               0,
982                               NULL, NULL,
983                               _empathy_gtk_marshal_VOID__OBJECT_STRING_STRING,
984                               G_TYPE_NONE,
985                               3, EMPATHY_TYPE_CONTACT, G_TYPE_STRING, G_TYPE_STRING);
986
987         g_object_class_install_property (object_class,
988                                          PROP_STORE,
989                                          g_param_spec_object ("store",
990                                                              "The store of the view",
991                                                              "The store of the view",
992                                                               EMPATHY_TYPE_CONTACT_LIST_STORE,
993                                                               G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE));
994         g_object_class_install_property (object_class,
995                                          PROP_LIST_FEATURES,
996                                          g_param_spec_flags ("list-features",
997                                                              "Features of the view",
998                                                              "Falgs for all enabled features",
999                                                               EMPATHY_TYPE_CONTACT_LIST_FEATURE_FLAGS,
1000                                                               EMPATHY_CONTACT_LIST_FEATURE_NONE,
1001                                                               G_PARAM_READWRITE));
1002         g_object_class_install_property (object_class,
1003                                          PROP_CONTACT_FEATURES,
1004                                          g_param_spec_flags ("contact-features",
1005                                                              "Features of the contact menu",
1006                                                              "Falgs for all enabled features for the menu",
1007                                                               EMPATHY_TYPE_CONTACT_FEATURE_FLAGS,
1008                                                               EMPATHY_CONTACT_FEATURE_NONE,
1009                                                               G_PARAM_READWRITE));
1010
1011         g_type_class_add_private (object_class, sizeof (EmpathyContactListViewPriv));
1012 }
1013
1014 static void
1015 empathy_contact_list_view_init (EmpathyContactListView *view)
1016 {
1017         EmpathyContactListViewPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (view,
1018                 EMPATHY_TYPE_CONTACT_LIST_VIEW, EmpathyContactListViewPriv);
1019
1020         view->priv = priv;
1021         /* Get saved group states. */
1022         empathy_contact_groups_get_all ();
1023
1024         gtk_tree_view_set_row_separator_func (GTK_TREE_VIEW (view), 
1025                                               empathy_contact_list_store_row_separator_func,
1026                                               NULL, NULL);
1027
1028         /* Connect to tree view signals rather than override. */
1029         g_signal_connect (view,
1030                           "button-press-event",
1031                           G_CALLBACK (contact_list_view_button_press_event_cb),
1032                           NULL);
1033         g_signal_connect (view,
1034                           "row-activated",
1035                           G_CALLBACK (contact_list_view_row_activated_cb),
1036                           NULL);
1037         g_signal_connect (view,
1038                           "row-expanded",
1039                           G_CALLBACK (contact_list_view_row_expand_or_collapse_cb),
1040                           GINT_TO_POINTER (TRUE));
1041         g_signal_connect (view,
1042                           "row-collapsed",
1043                           G_CALLBACK (contact_list_view_row_expand_or_collapse_cb),
1044                           GINT_TO_POINTER (FALSE));
1045 }
1046
1047 EmpathyContactListView *
1048 empathy_contact_list_view_new (EmpathyContactListStore        *store,
1049                                EmpathyContactListFeatureFlags  list_features,
1050                                EmpathyContactFeatureFlags      contact_features)
1051 {
1052         g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST_STORE (store), NULL);
1053         
1054         return g_object_new (EMPATHY_TYPE_CONTACT_LIST_VIEW,
1055                              "store", store,
1056                              "contact-features", contact_features,
1057                              "list-features", list_features,
1058                              NULL);
1059 }
1060
1061 EmpathyContact *
1062 empathy_contact_list_view_get_selected (EmpathyContactListView *view)
1063 {
1064         EmpathyContactListViewPriv *priv;
1065         GtkTreeSelection          *selection;
1066         GtkTreeIter                iter;
1067         GtkTreeModel              *model;
1068         EmpathyContact             *contact;
1069
1070         g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST_VIEW (view), NULL);
1071
1072         priv = GET_PRIV (view);
1073
1074         selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view));
1075         if (!gtk_tree_selection_get_selected (selection, &model, &iter)) {
1076                 return NULL;
1077         }
1078
1079         gtk_tree_model_get (model, &iter,
1080                             EMPATHY_CONTACT_LIST_STORE_COL_CONTACT, &contact,
1081                             -1);
1082
1083         return contact;
1084 }
1085
1086 gchar *
1087 empathy_contact_list_view_get_selected_group (EmpathyContactListView *view)
1088 {
1089         EmpathyContactListViewPriv *priv;
1090         GtkTreeSelection          *selection;
1091         GtkTreeIter                iter;
1092         GtkTreeModel              *model;
1093         gboolean                   is_group;
1094         gchar                     *name;
1095
1096         g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST_VIEW (view), NULL);
1097
1098         priv = GET_PRIV (view);
1099
1100         selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view));
1101         if (!gtk_tree_selection_get_selected (selection, &model, &iter)) {
1102                 return NULL;
1103         }
1104
1105         gtk_tree_model_get (model, &iter,
1106                             EMPATHY_CONTACT_LIST_STORE_COL_IS_GROUP, &is_group,
1107                             EMPATHY_CONTACT_LIST_STORE_COL_NAME, &name,
1108                             -1);
1109
1110         if (!is_group) {
1111                 g_free (name);
1112                 return NULL;
1113         }
1114
1115         return name;
1116 }
1117
1118 static gboolean
1119 contact_list_view_remove_dialog_show (GtkWindow   *parent, 
1120                                       const gchar *window_title, 
1121                                       const gchar *text)
1122 {
1123         GtkWidget *dialog, *label, *image, *hbox;
1124         gboolean res;
1125         
1126         dialog = gtk_dialog_new_with_buttons (window_title, parent,
1127                                               GTK_DIALOG_MODAL,
1128                                               GTK_STOCK_DELETE, GTK_RESPONSE_YES,
1129                                               GTK_STOCK_CANCEL, GTK_RESPONSE_NO,
1130                                               NULL);
1131         gtk_dialog_set_has_separator (GTK_DIALOG(dialog), FALSE);
1132          
1133         label = gtk_label_new (text);
1134         image = gtk_image_new_from_stock (GTK_STOCK_DIALOG_QUESTION, GTK_ICON_SIZE_DIALOG);
1135          
1136         hbox = gtk_hbox_new (FALSE, 5);
1137         gtk_container_set_border_width (GTK_CONTAINER (hbox), 5);
1138         gtk_box_pack_start_defaults (GTK_BOX (hbox), image);
1139         gtk_box_pack_start_defaults (GTK_BOX (hbox), label);     
1140         gtk_box_pack_start_defaults (GTK_BOX(GTK_DIALOG(dialog)->vbox), hbox);
1141
1142         gtk_widget_show (image);
1143         gtk_widget_show (label);
1144         gtk_widget_show (hbox);
1145         gtk_widget_show (dialog);
1146          
1147         res = gtk_dialog_run (GTK_DIALOG (dialog));
1148         gtk_widget_destroy (dialog);
1149
1150         return (res == GTK_RESPONSE_YES);
1151 }
1152
1153 static void
1154 contact_list_view_group_remove_activate_cb (GtkMenuItem            *menuitem,
1155                                             EmpathyContactListView *view)
1156 {
1157         EmpathyContactListViewPriv *priv = GET_PRIV (view);
1158         gchar                      *group;
1159
1160         group = empathy_contact_list_view_get_selected_group (view);
1161         if (group) {
1162                 gchar     *text;
1163                 GtkWindow *parent;
1164
1165                 text = g_strdup_printf (_("Do you really want to remove the group '%s'?"), group);
1166                 parent = empathy_get_toplevel_window (GTK_WIDGET (view));
1167                 if (contact_list_view_remove_dialog_show (parent, _("Removing group"), text)) {
1168                         EmpathyContactList *list;
1169
1170                         list = empathy_contact_list_store_get_list_iface (priv->store);
1171                         empathy_contact_list_remove_group (list, group);
1172                 }
1173
1174                 g_free (text);
1175         }
1176
1177         g_free (group);
1178 }
1179
1180 GtkWidget *
1181 empathy_contact_list_view_get_group_menu (EmpathyContactListView *view)
1182 {
1183         EmpathyContactListViewPriv *priv = GET_PRIV (view);
1184         gchar                      *group;
1185         GtkWidget                  *menu;
1186         GtkWidget                  *item;
1187         GtkWidget                  *image;
1188
1189         g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST_VIEW (view), NULL);
1190
1191         if (!(priv->list_features & (EMPATHY_CONTACT_LIST_FEATURE_GROUPS_RENAME |
1192                                      EMPATHY_CONTACT_LIST_FEATURE_GROUPS_REMOVE))) {
1193                 return NULL;
1194         }
1195
1196         group = empathy_contact_list_view_get_selected_group (view);
1197         if (!group) {
1198                 return NULL;
1199         }
1200
1201         menu = gtk_menu_new ();
1202
1203         /* FIXME: Not implemented yet
1204         if (priv->features & EMPATHY_CONTACT_LIST_FEATURE_GROUPS_RENAME) {
1205                 item = gtk_menu_item_new_with_mnemonic (_("Re_name"));
1206                 gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
1207                 gtk_widget_show (item);
1208                 g_signal_connect (item, "activate",
1209                                   G_CALLBACK (contact_list_view_group_rename_activate_cb),
1210                                   view);
1211         }*/
1212
1213         if (priv->list_features & EMPATHY_CONTACT_LIST_FEATURE_GROUPS_REMOVE) {
1214                 item = gtk_image_menu_item_new_with_mnemonic (_("_Remove"));
1215                 image = gtk_image_new_from_icon_name (GTK_STOCK_REMOVE,
1216                                                       GTK_ICON_SIZE_MENU);
1217                 gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image);
1218                 gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
1219                 gtk_widget_show (item);
1220                 g_signal_connect (item, "activate",
1221                                   G_CALLBACK (contact_list_view_group_remove_activate_cb),
1222                                   view);
1223         }
1224
1225         g_free (group);
1226
1227         return menu;
1228 }
1229
1230 static void
1231 contact_list_view_remove_activate_cb (GtkMenuItem            *menuitem,
1232                                       EmpathyContactListView *view)
1233 {
1234         EmpathyContactListViewPriv *priv = GET_PRIV (view);
1235         EmpathyContact             *contact;
1236                 
1237         contact = empathy_contact_list_view_get_selected (view);
1238
1239         if (contact) {
1240                 gchar     *text; 
1241                 GtkWindow *parent;
1242
1243                 parent = empathy_get_toplevel_window (GTK_WIDGET (view));
1244                 text = g_strdup_printf (_("Do you really want to remove the contact '%s'?"),
1245                                         empathy_contact_get_name (contact));                                            
1246                 if (contact_list_view_remove_dialog_show (parent, _("Removing contact"), text)) {
1247                         EmpathyContactList *list;
1248
1249                         list = empathy_contact_list_store_get_list_iface (priv->store);
1250                         empathy_contact_list_remove (list, contact, 
1251                                 _("Sorry, I don't want you in my contact list anymore."));
1252                 }
1253
1254                 g_free (text);
1255                 g_object_unref (contact);
1256         }
1257 }
1258
1259 GtkWidget *
1260 empathy_contact_list_view_get_contact_menu (EmpathyContactListView *view)
1261 {
1262         EmpathyContactListViewPriv *priv = GET_PRIV (view);
1263         EmpathyContact             *contact;
1264         GtkWidget                  *menu;
1265         GtkWidget                  *item;
1266         GtkWidget                  *image;
1267
1268         g_return_val_if_fail (EMPATHY_IS_CONTACT_LIST_VIEW (view), NULL);
1269
1270         contact = empathy_contact_list_view_get_selected (view);
1271         if (!contact) {
1272                 return NULL;
1273         }
1274
1275         menu = empathy_contact_menu_new (contact, priv->contact_features);
1276
1277         if (!menu &&
1278             !(priv->list_features & EMPATHY_CONTACT_LIST_FEATURE_CONTACT_REMOVE)) {
1279                 g_object_unref (contact);
1280                 return NULL;
1281         }
1282
1283         if (menu) {
1284                 /* Separator */
1285                 item = gtk_separator_menu_item_new ();
1286                 gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
1287                 gtk_widget_show (item);
1288         } else {
1289                 menu = gtk_menu_new ();
1290         }
1291
1292         /* Remove contact */
1293         if (priv->list_features & EMPATHY_CONTACT_LIST_FEATURE_CONTACT_REMOVE) {
1294                 item = gtk_image_menu_item_new_with_mnemonic (_("_Remove"));
1295                 image = gtk_image_new_from_icon_name (GTK_STOCK_REMOVE,
1296                                                       GTK_ICON_SIZE_MENU);
1297                 gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image);
1298                 gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
1299                 gtk_widget_show (item);
1300                 g_signal_connect (item, "activate",
1301                                   G_CALLBACK (contact_list_view_remove_activate_cb),
1302                                   view);
1303         }
1304
1305         g_object_unref (contact);
1306
1307         return menu;
1308 }
1309