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