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