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