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