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