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