]> git.0d.be Git - empathy.git/blob - libempathy-gtk/empathy-individual-view.c
Bug 627219 — Link dialog search doesn't work
[empathy.git] / libempathy-gtk / empathy-individual-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-2010 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  *          Travis Reitter <travis.reitter@collabora.co.uk>
25  */
26
27 #include "config.h"
28
29 #include <string.h>
30
31 #include <glib/gi18n-lib.h>
32 #include <gdk/gdkkeysyms.h>
33 #include <gtk/gtk.h>
34
35 #include <telepathy-glib/account-manager.h>
36 #include <telepathy-glib/util.h>
37
38 #include <folks/folks.h>
39 #include <folks/folks-telepathy.h>
40
41 #include <libempathy/empathy-call-factory.h>
42 #include <libempathy/empathy-individual-manager.h>
43 #include <libempathy/empathy-contact-groups.h>
44 #include <libempathy/empathy-dispatcher.h>
45 #include <libempathy/empathy-utils.h>
46
47 #include "empathy-individual-view.h"
48 #include "empathy-individual-menu.h"
49 #include "empathy-individual-store.h"
50 #include "empathy-images.h"
51 #include "empathy-cell-renderer-expander.h"
52 #include "empathy-cell-renderer-text.h"
53 #include "empathy-cell-renderer-activatable.h"
54 #include "empathy-ui-utils.h"
55 #include "empathy-gtk-enum-types.h"
56 #include "empathy-gtk-marshal.h"
57
58 #define DEBUG_FLAG EMPATHY_DEBUG_CONTACT
59 #include <libempathy/empathy-debug.h>
60
61 /* Active users are those which have recently changed state
62  * (e.g. online, offline or from normal to a busy state).
63  */
64
65 #define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyIndividualView)
66 typedef struct
67 {
68   EmpathyIndividualStore *store;
69   GtkTreeRowReference *drag_row;
70   EmpathyIndividualViewFeatureFlags view_features;
71   EmpathyContactFeatureFlags individual_features;
72   GtkWidget *tooltip_widget;
73   GtkTargetList *file_targets;
74
75   gboolean show_offline;
76
77   GtkTreeModelFilter *filter;
78   GtkWidget *search_widget;
79
80   guint expand_groups_idle_handler;
81   /* owned string (group name) -> bool (whether to expand/contract) */
82   GHashTable *expand_groups;
83 } EmpathyIndividualViewPriv;
84
85 typedef struct
86 {
87   EmpathyIndividualView *view;
88   GtkTreePath *path;
89   guint timeout_id;
90 } DragMotionData;
91
92 typedef struct
93 {
94   EmpathyIndividualView *view;
95   FolksIndividual *individual;
96   gboolean remove;
97 } ShowActiveData;
98
99 enum
100 {
101   PROP_0,
102   PROP_STORE,
103   PROP_VIEW_FEATURES,
104   PROP_INDIVIDUAL_FEATURES,
105   PROP_SHOW_OFFLINE,
106 };
107
108 /* TODO: re-add DRAG_TYPE_CONTACT_ID, for the case that we're dragging around
109  * specific EmpathyContacts (between/in/out of Individuals) */
110 enum DndDragType
111 {
112   DND_DRAG_TYPE_INDIVIDUAL_ID,
113   DND_DRAG_TYPE_URI_LIST,
114   DND_DRAG_TYPE_STRING,
115 };
116
117 #define DRAG_TYPE(T,I) \
118   { (gchar *) T, 0, I }
119
120 static const GtkTargetEntry drag_types_dest[] = {
121   DRAG_TYPE ("text/path-list", DND_DRAG_TYPE_URI_LIST),
122   DRAG_TYPE ("text/uri-list", DND_DRAG_TYPE_URI_LIST),
123   DRAG_TYPE ("text/contact-id", DND_DRAG_TYPE_INDIVIDUAL_ID),
124   DRAG_TYPE ("text/plain", DND_DRAG_TYPE_STRING),
125   DRAG_TYPE ("STRING", DND_DRAG_TYPE_STRING),
126 };
127
128 static const GtkTargetEntry drag_types_dest_file[] = {
129   DRAG_TYPE ("text/path-list", DND_DRAG_TYPE_URI_LIST),
130   DRAG_TYPE ("text/uri-list", DND_DRAG_TYPE_URI_LIST),
131 };
132
133 static const GtkTargetEntry drag_types_source[] = {
134   DRAG_TYPE ("text/contact-id", DND_DRAG_TYPE_INDIVIDUAL_ID),
135 };
136
137 #undef DRAG_TYPE
138
139 static GdkAtom drag_atoms_dest[G_N_ELEMENTS (drag_types_dest)];
140 static GdkAtom drag_atoms_source[G_N_ELEMENTS (drag_types_source)];
141
142 enum
143 {
144   DRAG_CONTACT_RECEIVED,
145   LAST_SIGNAL
146 };
147
148 static guint signals[LAST_SIGNAL];
149
150 G_DEFINE_TYPE (EmpathyIndividualView, empathy_individual_view,
151     GTK_TYPE_TREE_VIEW);
152
153 static void
154 individual_view_tooltip_destroy_cb (GtkWidget *widget,
155     EmpathyIndividualView *view)
156 {
157   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
158
159   if (priv->tooltip_widget != NULL)
160     {
161       DEBUG ("Tooltip destroyed");
162       tp_clear_object (&priv->tooltip_widget);
163     }
164 }
165
166 static gboolean
167 individual_view_query_tooltip_cb (EmpathyIndividualView *view,
168     gint x,
169     gint y,
170     gboolean keyboard_mode,
171     GtkTooltip *tooltip,
172     gpointer user_data)
173 {
174   EmpathyIndividualViewPriv *priv;
175   FolksIndividual *individual;
176   GtkTreeModel *model;
177   GtkTreeIter iter;
178   GtkTreePath *path;
179   static gint running = 0;
180   gboolean ret = FALSE;
181
182   priv = GET_PRIV (view);
183
184   /* Avoid an infinite loop. See GNOME bug #574377 */
185   if (running > 0)
186     return FALSE;
187
188   running++;
189
190   /* Don't show the tooltip if there's already a popup menu */
191   if (gtk_menu_get_for_attach_widget (GTK_WIDGET (view)) != NULL)
192     goto OUT;
193
194   if (!gtk_tree_view_get_tooltip_context (GTK_TREE_VIEW (view), &x, &y,
195           keyboard_mode, &model, &path, &iter))
196     goto OUT;
197
198   gtk_tree_view_set_tooltip_row (GTK_TREE_VIEW (view), tooltip, path);
199   gtk_tree_path_free (path);
200
201   gtk_tree_model_get (model, &iter,
202       EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual,
203       -1);
204   if (individual == NULL)
205     goto OUT;
206
207   if (priv->tooltip_widget == NULL)
208     {
209       priv->tooltip_widget = empathy_individual_widget_new (individual,
210           EMPATHY_INDIVIDUAL_WIDGET_FOR_TOOLTIP |
211           EMPATHY_INDIVIDUAL_WIDGET_SHOW_LOCATION);
212       gtk_container_set_border_width (GTK_CONTAINER (priv->tooltip_widget), 8);
213       g_object_ref (priv->tooltip_widget);
214       g_signal_connect (priv->tooltip_widget, "destroy",
215           G_CALLBACK (individual_view_tooltip_destroy_cb), view);
216       gtk_widget_show (priv->tooltip_widget);
217     }
218   else
219     {
220       empathy_individual_widget_set_individual (
221         EMPATHY_INDIVIDUAL_WIDGET (priv->tooltip_widget), individual);
222     }
223
224   gtk_tooltip_set_custom (tooltip, priv->tooltip_widget);
225   ret = TRUE;
226
227   g_object_unref (individual);
228 OUT:
229   running--;
230
231   return ret;
232 }
233
234 static void
235 groups_change_group_cb (GObject *source,
236     GAsyncResult *result,
237     gpointer user_data)
238 {
239   FolksGroups *groups = FOLKS_GROUPS (source);
240   GError *error = NULL;
241
242   folks_groups_change_group_finish (groups, result, &error);
243   if (error != NULL)
244     {
245       g_warning ("failed to change group: %s", error->message);
246       g_clear_error (&error);
247     }
248 }
249
250 static void
251 individual_view_handle_drag (EmpathyIndividualView *self,
252     FolksIndividual *individual,
253     const gchar *old_group,
254     const gchar *new_group,
255     GdkDragAction action)
256 {
257   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self));
258   g_return_if_fail (FOLKS_IS_INDIVIDUAL (individual));
259
260   DEBUG ("individual %s dragged from '%s' to '%s'",
261       folks_individual_get_id (individual), old_group, new_group);
262
263   if (!tp_strdiff (new_group, EMPATHY_INDIVIDUAL_STORE_FAVORITE))
264     {
265       /* Mark contact as favourite */
266       folks_favourite_set_is_favourite (FOLKS_FAVOURITE (individual), TRUE);
267       return;
268     }
269
270   if (!tp_strdiff (old_group, EMPATHY_INDIVIDUAL_STORE_FAVORITE))
271     {
272       /* Remove contact as favourite */
273       folks_favourite_set_is_favourite (FOLKS_FAVOURITE (individual), FALSE);
274
275       /* Don't try to remove it */
276       old_group = NULL;
277     }
278
279   if (new_group != NULL)
280     folks_groups_change_group (FOLKS_GROUPS (individual), new_group, TRUE,
281         groups_change_group_cb, NULL);
282
283   if (old_group != NULL && action == GDK_ACTION_MOVE)
284     folks_groups_change_group (FOLKS_GROUPS (individual), old_group, FALSE,
285         groups_change_group_cb, NULL);
286 }
287
288 static gboolean
289 group_can_be_modified (const gchar *name,
290     gboolean is_fake_group,
291     gboolean adding)
292 {
293   /* Real groups can always be modified */
294   if (!is_fake_group)
295     return TRUE;
296
297   /* The favorite fake group can be modified so users can
298    * add/remove favorites using DnD */
299   if (!tp_strdiff (name, EMPATHY_INDIVIDUAL_STORE_FAVORITE))
300     return TRUE;
301
302   /* We can remove contacts from the 'ungrouped' fake group */
303   if (!adding && !tp_strdiff (name, EMPATHY_INDIVIDUAL_STORE_UNGROUPED))
304     return TRUE;
305
306   return FALSE;
307 }
308
309 static gboolean
310 individual_view_contact_drag_received (GtkWidget *self,
311     GdkDragContext *context,
312     GtkTreeModel *model,
313     GtkTreePath *path,
314     GtkSelectionData *selection)
315 {
316   EmpathyIndividualViewPriv *priv;
317   EmpathyIndividualManager *manager;
318   FolksIndividual *individual;
319   GtkTreePath *source_path;
320   const gchar *sel_data;
321   gchar *new_group = NULL;
322   gchar *old_group = NULL;
323   gboolean new_group_is_fake, old_group_is_fake = TRUE;
324
325   priv = GET_PRIV (self);
326
327   sel_data = (const gchar *) gtk_selection_data_get_data (selection);
328   new_group = empathy_individual_store_get_parent_group (model, path,
329       NULL, &new_group_is_fake);
330
331   if (!group_can_be_modified (new_group, new_group_is_fake, TRUE))
332     return FALSE;
333
334   /* Get source group information. */
335   if (priv->drag_row)
336     {
337       source_path = gtk_tree_row_reference_get_path (priv->drag_row);
338       if (source_path)
339         {
340           old_group =
341               empathy_individual_store_get_parent_group (model, source_path,
342               NULL, &old_group_is_fake);
343           gtk_tree_path_free (source_path);
344         }
345     }
346
347   if (!group_can_be_modified (old_group, old_group_is_fake, FALSE))
348     return FALSE;
349
350   if (!tp_strdiff (old_group, new_group))
351     {
352       g_free (new_group);
353       g_free (old_group);
354       return FALSE;
355     }
356
357   /* XXX: for contacts, we used to ensure the account, create the contact
358    * factory, and then wait on the contacts. But they should already be
359    * created by this point */
360
361   manager = empathy_individual_manager_dup_singleton ();
362   individual = empathy_individual_manager_lookup_member (manager, sel_data);
363
364   if (individual == NULL)
365     {
366       DEBUG ("failed to find drag event individual with ID '%s'", sel_data);
367
368       g_object_unref (manager);
369
370       return FALSE;
371     }
372
373   /* FIXME: We should probably wait for the cb before calling
374    * gtk_drag_finish */
375
376   individual_view_handle_drag (EMPATHY_INDIVIDUAL_VIEW (self), individual,
377       old_group, new_group, gdk_drag_context_get_selected_action (context));
378
379   g_object_unref (G_OBJECT (manager));
380   g_free (old_group);
381   g_free (new_group);
382
383   return TRUE;
384 }
385
386 static gboolean
387 individual_view_file_drag_received (GtkWidget *view,
388     GdkDragContext *context,
389     GtkTreeModel *model,
390     GtkTreePath *path,
391     GtkSelectionData *selection)
392 {
393   GtkTreeIter iter;
394   const gchar *sel_data;
395   FolksIndividual *individual;
396   EmpathyContact *contact;
397
398   sel_data = (const gchar *) gtk_selection_data_get_data (selection);
399
400   gtk_tree_model_get_iter (model, &iter, path);
401   gtk_tree_model_get (model, &iter,
402       EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual, -1);
403   if (individual == NULL)
404     return FALSE;
405
406   contact = empathy_contact_dup_from_folks_individual (individual);
407   empathy_send_file_from_uri_list (contact, sel_data);
408
409   g_object_unref (individual);
410   tp_clear_object (&contact);
411
412   return TRUE;
413 }
414
415 static void
416 individual_view_drag_data_received (GtkWidget *view,
417     GdkDragContext *context,
418     gint x,
419     gint y,
420     GtkSelectionData *selection,
421     guint info,
422     guint time_)
423 {
424   GtkTreeModel *model;
425   gboolean is_row;
426   GtkTreeViewDropPosition position;
427   GtkTreePath *path;
428   gboolean success = TRUE;
429
430   model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
431
432   /* Get destination group information. */
433   is_row = gtk_tree_view_get_dest_row_at_pos (GTK_TREE_VIEW (view),
434       x, y, &path, &position);
435   if (!is_row)
436     {
437       success = FALSE;
438     }
439   else if (info == DND_DRAG_TYPE_INDIVIDUAL_ID
440       || info == DND_DRAG_TYPE_STRING)
441     {
442       success = individual_view_contact_drag_received (view,
443           context, model, path, selection);
444     }
445   else if (info == DND_DRAG_TYPE_URI_LIST)
446     {
447       success = individual_view_file_drag_received (view,
448           context, model, path, selection);
449     }
450
451   gtk_tree_path_free (path);
452   gtk_drag_finish (context, success, FALSE, GDK_CURRENT_TIME);
453 }
454
455 static gboolean
456 individual_view_drag_motion_cb (DragMotionData *data)
457 {
458   gtk_tree_view_expand_row (GTK_TREE_VIEW (data->view), data->path, FALSE);
459
460   data->timeout_id = 0;
461
462   return FALSE;
463 }
464
465 static gboolean
466 individual_view_drag_motion (GtkWidget *widget,
467     GdkDragContext *context,
468     gint x,
469     gint y,
470     guint time_)
471 {
472   EmpathyIndividualViewPriv *priv;
473   GtkTreeModel *model;
474   GdkAtom target;
475   GtkTreeIter iter;
476   static DragMotionData *dm = NULL;
477   GtkTreePath *path;
478   gboolean is_row;
479   gboolean is_different = FALSE;
480   gboolean cleanup = TRUE;
481   gboolean retval = TRUE;
482
483   priv = GET_PRIV (EMPATHY_INDIVIDUAL_VIEW (widget));
484   model = gtk_tree_view_get_model (GTK_TREE_VIEW (widget));
485
486   is_row = gtk_tree_view_get_path_at_pos (GTK_TREE_VIEW (widget),
487       x, y, &path, NULL, NULL, NULL);
488
489   cleanup &= (dm == NULL);
490
491   if (is_row)
492     {
493       cleanup &= (dm && gtk_tree_path_compare (dm->path, path) != 0);
494       is_different = ((dm == NULL) || ((dm != NULL)
495               && gtk_tree_path_compare (dm->path, path) != 0));
496     }
497   else
498     cleanup &= FALSE;
499
500   if (path == NULL)
501     {
502       /* Coordinates don't point to an actual row, so make sure the pointer
503          and highlighting don't indicate that a drag is possible.
504        */
505       gdk_drag_status (context, GDK_ACTION_DEFAULT, time_);
506       gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (widget), NULL, 0);
507       return FALSE;
508     }
509   target = gtk_drag_dest_find_target (widget, context, priv->file_targets);
510   gtk_tree_model_get_iter (model, &iter, path);
511
512   if (target == GDK_NONE)
513     {
514       /* If target == GDK_NONE, then we don't have a target that can be
515          dropped on a contact.  This means a contact drag.  If we're
516          pointing to a group, highlight it.  Otherwise, if the contact
517          we're pointing to is in a group, highlight that.  Otherwise,
518          set the drag position to before the first row for a drag into
519          the "non-group" at the top.
520        */
521       GtkTreeIter group_iter;
522       gboolean is_group;
523       GtkTreePath *group_path;
524       gtk_tree_model_get (model, &iter,
525           EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group, -1);
526       if (is_group)
527         {
528           group_iter = iter;
529         }
530       else
531         {
532           if (gtk_tree_model_iter_parent (model, &group_iter, &iter))
533             gtk_tree_model_get (model, &group_iter,
534                 EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group, -1);
535         }
536       if (is_group)
537         {
538           gdk_drag_status (context, GDK_ACTION_MOVE, time_);
539           group_path = gtk_tree_model_get_path (model, &group_iter);
540           gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (widget),
541               group_path, GTK_TREE_VIEW_DROP_INTO_OR_BEFORE);
542           gtk_tree_path_free (group_path);
543         }
544       else
545         {
546           group_path = gtk_tree_path_new_first ();
547           gdk_drag_status (context, GDK_ACTION_MOVE, time_);
548           gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (widget),
549               group_path, GTK_TREE_VIEW_DROP_BEFORE);
550         }
551     }
552   else
553     {
554       /* This is a file drag, and it can only be dropped on contacts,
555          not groups.
556        */
557       FolksIndividual *individual;
558       EmpathyCapabilities caps = EMPATHY_CAPABILITIES_NONE;
559
560       gtk_tree_model_get (model, &iter,
561           EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual, -1);
562       if (individual != NULL)
563         {
564           EmpathyContact *contact = NULL;
565
566           contact = empathy_contact_dup_from_folks_individual (individual);
567           caps = empathy_contact_get_capabilities (contact);
568
569           tp_clear_object (&contact);
570         }
571
572       if (individual != NULL &&
573           folks_individual_is_online (individual) &&
574           (caps & EMPATHY_CAPABILITIES_FT))
575         {
576           gdk_drag_status (context, GDK_ACTION_COPY, time_);
577           gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (widget),
578               path, GTK_TREE_VIEW_DROP_INTO_OR_BEFORE);
579         }
580       else
581         {
582           gdk_drag_status (context, 0, time_);
583           gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (widget), NULL, 0);
584           retval = FALSE;
585         }
586
587       if (individual != NULL)
588         g_object_unref (individual);
589     }
590
591   if (!is_different && !cleanup)
592     return retval;
593
594   if (dm)
595     {
596       gtk_tree_path_free (dm->path);
597       if (dm->timeout_id)
598         {
599           g_source_remove (dm->timeout_id);
600         }
601
602       g_free (dm);
603
604       dm = NULL;
605     }
606
607   if (!gtk_tree_view_row_expanded (GTK_TREE_VIEW (widget), path))
608     {
609       dm = g_new0 (DragMotionData, 1);
610
611       dm->view = EMPATHY_INDIVIDUAL_VIEW (widget);
612       dm->path = gtk_tree_path_copy (path);
613
614       dm->timeout_id = g_timeout_add_seconds (1,
615           (GSourceFunc) individual_view_drag_motion_cb, dm);
616     }
617
618   return retval;
619 }
620
621 static void
622 individual_view_drag_begin (GtkWidget *widget,
623     GdkDragContext *context)
624 {
625   EmpathyIndividualViewPriv *priv;
626   GtkTreeSelection *selection;
627   GtkTreeModel *model;
628   GtkTreePath *path;
629   GtkTreeIter iter;
630
631   priv = GET_PRIV (widget);
632
633   GTK_WIDGET_CLASS (empathy_individual_view_parent_class)->drag_begin (widget,
634       context);
635
636   selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (widget));
637   if (!gtk_tree_selection_get_selected (selection, &model, &iter))
638     return;
639
640   path = gtk_tree_model_get_path (model, &iter);
641   priv->drag_row = gtk_tree_row_reference_new (model, path);
642   gtk_tree_path_free (path);
643 }
644
645 static void
646 individual_view_drag_data_get (GtkWidget *widget,
647     GdkDragContext *context,
648     GtkSelectionData *selection,
649     guint info,
650     guint time_)
651 {
652   EmpathyIndividualViewPriv *priv;
653   GtkTreePath *src_path;
654   GtkTreeIter iter;
655   GtkTreeModel *model;
656   FolksIndividual *individual;
657   const gchar *individual_id;
658
659   priv = GET_PRIV (widget);
660
661   model = gtk_tree_view_get_model (GTK_TREE_VIEW (widget));
662   if (priv->drag_row == NULL)
663     return;
664
665   src_path = gtk_tree_row_reference_get_path (priv->drag_row);
666   if (src_path == NULL)
667     return;
668
669   if (!gtk_tree_model_get_iter (model, &iter, src_path))
670     {
671       gtk_tree_path_free (src_path);
672       return;
673     }
674
675   gtk_tree_path_free (src_path);
676
677   individual =
678       empathy_individual_view_dup_selected (EMPATHY_INDIVIDUAL_VIEW (widget));
679   if (individual == NULL)
680     return;
681
682   individual_id = folks_individual_get_id (individual);
683
684   if (info == DND_DRAG_TYPE_INDIVIDUAL_ID)
685     {
686       gtk_selection_data_set (selection, drag_atoms_source[info], 8,
687           (guchar *) individual_id, strlen (individual_id) + 1);
688     }
689
690   g_object_unref (individual);
691 }
692
693 static void
694 individual_view_drag_end (GtkWidget *widget,
695     GdkDragContext *context)
696 {
697   EmpathyIndividualViewPriv *priv;
698
699   priv = GET_PRIV (widget);
700
701   GTK_WIDGET_CLASS (empathy_individual_view_parent_class)->drag_end (widget,
702       context);
703
704   if (priv->drag_row)
705     {
706       gtk_tree_row_reference_free (priv->drag_row);
707       priv->drag_row = NULL;
708     }
709 }
710
711 static gboolean
712 individual_view_drag_drop (GtkWidget *widget,
713     GdkDragContext *drag_context,
714     gint x,
715     gint y,
716     guint time_)
717 {
718   return FALSE;
719 }
720
721 typedef struct
722 {
723   EmpathyIndividualView *view;
724   guint button;
725   guint32 time;
726 } MenuPopupData;
727
728 static gboolean
729 individual_view_popup_menu_idle_cb (gpointer user_data)
730 {
731   MenuPopupData *data = user_data;
732   GtkWidget *menu;
733
734   menu = empathy_individual_view_get_individual_menu (data->view);
735   if (menu == NULL)
736     menu = empathy_individual_view_get_group_menu (data->view);
737
738   if (menu != NULL)
739     {
740       g_signal_connect (menu, "deactivate", G_CALLBACK (gtk_menu_detach), NULL);
741       gtk_menu_attach_to_widget (GTK_MENU (menu), GTK_WIDGET (data->view),
742           NULL);
743       gtk_widget_show (menu);
744       gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL, data->button,
745           data->time);
746       g_object_ref_sink (menu);
747       g_object_unref (menu);
748     }
749
750   g_slice_free (MenuPopupData, data);
751
752   return FALSE;
753 }
754
755 static gboolean
756 individual_view_button_press_event_cb (EmpathyIndividualView *view,
757     GdkEventButton *event,
758     gpointer user_data)
759 {
760   if (event->button == 3)
761     {
762       MenuPopupData *data;
763
764       data = g_slice_new (MenuPopupData);
765       data->view = view;
766       data->button = event->button;
767       data->time = event->time;
768       g_idle_add (individual_view_popup_menu_idle_cb, data);
769     }
770
771   return FALSE;
772 }
773
774 static gboolean
775 individual_view_key_press_event_cb (EmpathyIndividualView *view,
776     GdkEventKey *event,
777     gpointer user_data)
778 {
779   if (event->keyval == GDK_Menu)
780     {
781       MenuPopupData *data;
782
783       data = g_slice_new (MenuPopupData);
784       data->view = view;
785       data->button = 0;
786       data->time = event->time;
787       g_idle_add (individual_view_popup_menu_idle_cb, data);
788     }
789
790   return FALSE;
791 }
792
793 static void
794 individual_view_row_activated (GtkTreeView *view,
795     GtkTreePath *path,
796     GtkTreeViewColumn *column)
797 {
798   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
799   FolksIndividual *individual;
800   EmpathyContact *contact = NULL;
801   GtkTreeModel *model;
802   GtkTreeIter iter;
803
804   if (!(priv->individual_features & EMPATHY_CONTACT_FEATURE_CHAT))
805     return;
806
807   model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
808   gtk_tree_model_get_iter (model, &iter, path);
809   gtk_tree_model_get (model, &iter,
810       EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual, -1);
811
812   if (individual == NULL)
813     return;
814
815   contact = empathy_contact_dup_from_folks_individual (individual);
816   if (contact != NULL)
817     {
818       DEBUG ("Starting a chat");
819
820       empathy_dispatcher_chat_with_contact (contact,
821           gtk_get_current_event_time ());
822     }
823
824   g_object_unref (individual);
825   tp_clear_object (&contact);
826 }
827
828 static void
829 individual_view_call_activated_cb (EmpathyCellRendererActivatable *cell,
830     const gchar *path_string,
831     EmpathyIndividualView *view)
832 {
833   GtkWidget *menu;
834   GtkTreeModel *model;
835   GtkTreeIter iter;
836   FolksIndividual *individual;
837   GdkEventButton *event;
838   GtkMenuShell *shell;
839   GtkWidget *item;
840
841   model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
842   if (!gtk_tree_model_get_iter_from_string (model, &iter, path_string))
843     return;
844
845   gtk_tree_model_get (model, &iter,
846       EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual, -1);
847   if (individual == NULL)
848     return;
849
850   event = (GdkEventButton *) gtk_get_current_event ();
851
852   menu = gtk_menu_new ();
853   shell = GTK_MENU_SHELL (menu);
854
855   /* audio */
856   item = empathy_individual_audio_call_menu_item_new (individual, NULL);
857   gtk_menu_shell_append (shell, item);
858   gtk_widget_show (item);
859
860   /* video */
861   item = empathy_individual_video_call_menu_item_new (individual, NULL);
862   gtk_menu_shell_append (shell, item);
863   gtk_widget_show (item);
864
865   g_signal_connect (menu, "deactivate", G_CALLBACK (gtk_menu_detach), NULL);
866   gtk_menu_attach_to_widget (GTK_MENU (menu), GTK_WIDGET (view), NULL);
867   gtk_widget_show (menu);
868   gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL,
869       event->button, event->time);
870   g_object_ref_sink (menu);
871   g_object_unref (menu);
872
873   g_object_unref (individual);
874 }
875
876 static void
877 individual_view_cell_set_background (EmpathyIndividualView *view,
878     GtkCellRenderer *cell,
879     gboolean is_group,
880     gboolean is_active)
881 {
882   GdkColor color;
883   GtkStyle *style;
884
885   style = gtk_widget_get_style (GTK_WIDGET (view));
886
887   if (!is_group && is_active)
888     {
889       color = style->bg[GTK_STATE_SELECTED];
890
891       /* Here we take the current theme colour and add it to
892        * the colour for white and average the two. This
893        * gives a colour which is inline with the theme but
894        * slightly whiter.
895        */
896       color.red = (color.red + (style->white).red) / 2;
897       color.green = (color.green + (style->white).green) / 2;
898       color.blue = (color.blue + (style->white).blue) / 2;
899
900       g_object_set (cell, "cell-background-gdk", &color, NULL);
901     }
902   else
903     g_object_set (cell, "cell-background-gdk", NULL, NULL);
904 }
905
906 static void
907 individual_view_pixbuf_cell_data_func (GtkTreeViewColumn *tree_column,
908     GtkCellRenderer *cell,
909     GtkTreeModel *model,
910     GtkTreeIter *iter,
911     EmpathyIndividualView *view)
912 {
913   GdkPixbuf *pixbuf;
914   gboolean is_group;
915   gboolean is_active;
916
917   gtk_tree_model_get (model, iter,
918       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
919       EMPATHY_INDIVIDUAL_STORE_COL_IS_ACTIVE, &is_active,
920       EMPATHY_INDIVIDUAL_STORE_COL_ICON_STATUS, &pixbuf, -1);
921
922   g_object_set (cell,
923       "visible", !is_group,
924       "pixbuf", pixbuf,
925       NULL);
926
927   tp_clear_object (&pixbuf);
928
929   individual_view_cell_set_background (view, cell, is_group, is_active);
930 }
931
932 static void
933 individual_view_group_icon_cell_data_func (GtkTreeViewColumn *tree_column,
934     GtkCellRenderer *cell,
935     GtkTreeModel *model,
936     GtkTreeIter *iter,
937     EmpathyIndividualView *view)
938 {
939   GdkPixbuf *pixbuf = NULL;
940   gboolean is_group;
941   gchar *name;
942
943   gtk_tree_model_get (model, iter,
944       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
945       EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name, -1);
946
947   if (!is_group)
948     goto out;
949
950   if (!tp_strdiff (name, EMPATHY_INDIVIDUAL_STORE_FAVORITE))
951     {
952       pixbuf = empathy_pixbuf_from_icon_name ("emblem-favorite",
953           GTK_ICON_SIZE_MENU);
954     }
955   else if (!tp_strdiff (name, EMPATHY_INDIVIDUAL_STORE_PEOPLE_NEARBY))
956     {
957       pixbuf = empathy_pixbuf_from_icon_name ("im-local-xmpp",
958           GTK_ICON_SIZE_MENU);
959     }
960
961 out:
962   g_object_set (cell,
963       "visible", pixbuf != NULL,
964       "pixbuf", pixbuf,
965       NULL);
966
967   tp_clear_object (&pixbuf);
968
969   g_free (name);
970 }
971
972 static void
973 individual_view_audio_call_cell_data_func (GtkTreeViewColumn *tree_column,
974     GtkCellRenderer *cell,
975     GtkTreeModel *model,
976     GtkTreeIter *iter,
977     EmpathyIndividualView *view)
978 {
979   gboolean is_group;
980   gboolean is_active;
981   gboolean can_audio, can_video;
982
983   gtk_tree_model_get (model, iter,
984       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
985       EMPATHY_INDIVIDUAL_STORE_COL_IS_ACTIVE, &is_active,
986       EMPATHY_INDIVIDUAL_STORE_COL_CAN_AUDIO_CALL, &can_audio,
987       EMPATHY_INDIVIDUAL_STORE_COL_CAN_VIDEO_CALL, &can_video, -1);
988
989   g_object_set (cell,
990       "visible", !is_group && (can_audio || can_video),
991       "icon-name", can_video ? EMPATHY_IMAGE_VIDEO_CALL : EMPATHY_IMAGE_VOIP,
992       NULL);
993
994   individual_view_cell_set_background (view, cell, is_group, is_active);
995 }
996
997 static void
998 individual_view_avatar_cell_data_func (GtkTreeViewColumn *tree_column,
999     GtkCellRenderer *cell,
1000     GtkTreeModel *model,
1001     GtkTreeIter *iter,
1002     EmpathyIndividualView *view)
1003 {
1004   GdkPixbuf *pixbuf;
1005   gboolean show_avatar;
1006   gboolean is_group;
1007   gboolean is_active;
1008
1009   gtk_tree_model_get (model, iter,
1010       EMPATHY_INDIVIDUAL_STORE_COL_PIXBUF_AVATAR, &pixbuf,
1011       EMPATHY_INDIVIDUAL_STORE_COL_PIXBUF_AVATAR_VISIBLE, &show_avatar,
1012       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
1013       EMPATHY_INDIVIDUAL_STORE_COL_IS_ACTIVE, &is_active, -1);
1014
1015   g_object_set (cell,
1016       "visible", !is_group && show_avatar,
1017       "pixbuf", pixbuf,
1018       NULL);
1019
1020   tp_clear_object (&pixbuf);
1021
1022   individual_view_cell_set_background (view, cell, is_group, is_active);
1023 }
1024
1025 static void
1026 individual_view_text_cell_data_func (GtkTreeViewColumn *tree_column,
1027     GtkCellRenderer *cell,
1028     GtkTreeModel *model,
1029     GtkTreeIter *iter,
1030     EmpathyIndividualView *view)
1031 {
1032   gboolean is_group;
1033   gboolean is_active;
1034
1035   gtk_tree_model_get (model, iter,
1036       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
1037       EMPATHY_INDIVIDUAL_STORE_COL_IS_ACTIVE, &is_active, -1);
1038
1039   individual_view_cell_set_background (view, cell, is_group, is_active);
1040 }
1041
1042 static void
1043 individual_view_expander_cell_data_func (GtkTreeViewColumn *column,
1044     GtkCellRenderer *cell,
1045     GtkTreeModel *model,
1046     GtkTreeIter *iter,
1047     EmpathyIndividualView *view)
1048 {
1049   gboolean is_group;
1050   gboolean is_active;
1051
1052   gtk_tree_model_get (model, iter,
1053       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
1054       EMPATHY_INDIVIDUAL_STORE_COL_IS_ACTIVE, &is_active, -1);
1055
1056   if (gtk_tree_model_iter_has_child (model, iter))
1057     {
1058       GtkTreePath *path;
1059       gboolean row_expanded;
1060
1061       path = gtk_tree_model_get_path (model, iter);
1062       row_expanded =
1063           gtk_tree_view_row_expanded (GTK_TREE_VIEW
1064           (gtk_tree_view_column_get_tree_view (column)), path);
1065       gtk_tree_path_free (path);
1066
1067       g_object_set (cell,
1068           "visible", TRUE,
1069           "expander-style",
1070           row_expanded ? GTK_EXPANDER_EXPANDED : GTK_EXPANDER_COLLAPSED,
1071           NULL);
1072     }
1073   else
1074     g_object_set (cell, "visible", FALSE, NULL);
1075
1076   individual_view_cell_set_background (view, cell, is_group, is_active);
1077 }
1078
1079 static void
1080 individual_view_row_expand_or_collapse_cb (EmpathyIndividualView *view,
1081     GtkTreeIter *iter,
1082     GtkTreePath *path,
1083     gpointer user_data)
1084 {
1085   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
1086   GtkTreeModel *model;
1087   gchar *name;
1088   gboolean expanded;
1089
1090   if (!(priv->view_features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_SAVE))
1091     return;
1092
1093   model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
1094
1095   gtk_tree_model_get (model, iter,
1096       EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name, -1);
1097
1098   expanded = GPOINTER_TO_INT (user_data);
1099   empathy_contact_group_set_expanded (name, expanded);
1100
1101   g_free (name);
1102 }
1103
1104 static gboolean
1105 individual_view_start_search_cb (EmpathyIndividualView *view,
1106     gpointer data)
1107 {
1108   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
1109
1110   if (priv->search_widget == NULL)
1111     return FALSE;
1112
1113   if (gtk_widget_get_visible (GTK_WIDGET (priv->search_widget)))
1114     gtk_widget_grab_focus (GTK_WIDGET (priv->search_widget));
1115   else
1116     gtk_widget_show (GTK_WIDGET (priv->search_widget));
1117
1118   return TRUE;
1119 }
1120
1121 static void
1122 individual_view_search_text_notify_cb (EmpathyLiveSearch *search,
1123     GParamSpec *pspec,
1124     EmpathyIndividualView *view)
1125 {
1126   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
1127   GtkTreePath *path;
1128   GtkTreeViewColumn *focus_column;
1129   GtkTreeModel *model;
1130   GtkTreeIter iter;
1131   gboolean set_cursor = FALSE;
1132
1133   gtk_tree_model_filter_refilter (priv->filter);
1134
1135   /* Set cursor on the first contact. If it is already set on a group,
1136    * set it on its first child contact. Note that first child of a group
1137    * is its separator, that's why we actually set to the 2nd
1138    */
1139
1140   model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
1141   gtk_tree_view_get_cursor (GTK_TREE_VIEW (view), &path, &focus_column);
1142
1143   if (path == NULL)
1144     {
1145       path = gtk_tree_path_new_from_string ("0:1");
1146       set_cursor = TRUE;
1147     }
1148   else if (gtk_tree_path_get_depth (path) < 2)
1149     {
1150       gboolean is_group;
1151
1152       gtk_tree_model_get_iter (model, &iter, path);
1153       gtk_tree_model_get (model, &iter,
1154           EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
1155           -1);
1156
1157       if (is_group)
1158         {
1159           gtk_tree_path_down (path);
1160           gtk_tree_path_next (path);
1161           set_cursor = TRUE;
1162         }
1163     }
1164
1165   if (set_cursor)
1166     {
1167       /* FIXME: Workaround for GTK bug #621651, we have to make sure the path is
1168        * valid. */
1169       if (gtk_tree_model_get_iter (model, &iter, path))
1170         {
1171           gtk_tree_view_set_cursor (GTK_TREE_VIEW (view), path, focus_column,
1172               FALSE);
1173         }
1174     }
1175
1176   gtk_tree_path_free (path);
1177 }
1178
1179 static void
1180 individual_view_search_activate_cb (GtkWidget *search,
1181   EmpathyIndividualView *view)
1182 {
1183   GtkTreePath *path;
1184   GtkTreeViewColumn *focus_column;
1185
1186   gtk_tree_view_get_cursor (GTK_TREE_VIEW (view), &path, &focus_column);
1187   if (path != NULL)
1188     {
1189       gtk_tree_view_row_activated (GTK_TREE_VIEW (view), path, focus_column);
1190       gtk_tree_path_free (path);
1191
1192       gtk_widget_hide (search);
1193     }
1194 }
1195
1196 static gboolean
1197 individual_view_search_key_navigation_cb (GtkWidget *search,
1198   GdkEvent *event,
1199   EmpathyIndividualView *view)
1200 {
1201   GdkEventKey *eventkey = ((GdkEventKey *) event);
1202   gboolean ret = FALSE;
1203
1204   if (eventkey->keyval == GDK_Up || eventkey->keyval == GDK_Down)
1205     {
1206       GdkEvent *new_event;
1207
1208       new_event = gdk_event_copy (event);
1209       gtk_widget_grab_focus (GTK_WIDGET (view));
1210       ret = gtk_widget_event (GTK_WIDGET (view), new_event);
1211       gtk_widget_grab_focus (search);
1212
1213       gdk_event_free (new_event);
1214     }
1215
1216   return ret;
1217 }
1218
1219 static void
1220 individual_view_search_hide_cb (EmpathyLiveSearch *search,
1221     EmpathyIndividualView *view)
1222 {
1223   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
1224   GtkTreeModel *model;
1225   GtkTreePath *cursor_path;
1226   GtkTreeIter iter;
1227   gboolean valid = FALSE;
1228
1229   /* block expand or collapse handlers, they would write the
1230    * expand or collapsed setting to file otherwise */
1231   g_signal_handlers_block_by_func (view,
1232       individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (TRUE));
1233   g_signal_handlers_block_by_func (view,
1234     individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (FALSE));
1235
1236   /* restore which groups are expanded and which are not */
1237   model = gtk_tree_view_get_model (GTK_TREE_VIEW (view));
1238   for (valid = gtk_tree_model_get_iter_first (model, &iter);
1239        valid; valid = gtk_tree_model_iter_next (model, &iter))
1240     {
1241       gboolean is_group;
1242       gchar *name = NULL;
1243       GtkTreePath *path;
1244
1245       gtk_tree_model_get (model, &iter,
1246           EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name,
1247           EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
1248           -1);
1249
1250       if (!is_group)
1251         {
1252           g_free (name);
1253           continue;
1254         }
1255
1256       path = gtk_tree_model_get_path (model, &iter);
1257       if ((priv->view_features &
1258             EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_SAVE) == 0 ||
1259           empathy_contact_group_get_expanded (name))
1260         {
1261           gtk_tree_view_expand_row (GTK_TREE_VIEW (view), path, TRUE);
1262         }
1263       else
1264         {
1265           gtk_tree_view_collapse_row (GTK_TREE_VIEW (view), path);
1266         }
1267
1268       gtk_tree_path_free (path);
1269       g_free (name);
1270     }
1271
1272   /* unblock expand or collapse handlers */
1273   g_signal_handlers_unblock_by_func (view,
1274       individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (TRUE));
1275   g_signal_handlers_unblock_by_func (view,
1276       individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (FALSE));
1277
1278   /* keep the selected contact visible */
1279   gtk_tree_view_get_cursor (GTK_TREE_VIEW (view), &cursor_path, NULL);
1280
1281   if (cursor_path != NULL)
1282     gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (view), cursor_path, NULL,
1283         FALSE, 0, 0);
1284
1285   gtk_tree_path_free (cursor_path);
1286 }
1287
1288 static void
1289 individual_view_search_show_cb (EmpathyLiveSearch *search,
1290     EmpathyIndividualView *view)
1291 {
1292   /* block expand or collapse handlers during expand all, they would
1293    * write the expand or collapsed setting to file otherwise */
1294   g_signal_handlers_block_by_func (view,
1295       individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (TRUE));
1296
1297   gtk_tree_view_expand_all (GTK_TREE_VIEW (view));
1298
1299   g_signal_handlers_unblock_by_func (view,
1300       individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (TRUE));
1301 }
1302
1303 static gboolean
1304 expand_idle_foreach_cb (GtkTreeModel *model,
1305     GtkTreePath *path,
1306     GtkTreeIter *iter,
1307     EmpathyIndividualView *self)
1308 {
1309   EmpathyIndividualViewPriv *priv;
1310   gboolean is_group;
1311   gpointer should_expand;
1312   gchar *name;
1313
1314   /* We only want groups */
1315   if (gtk_tree_path_get_depth (path) > 1)
1316     return FALSE;
1317
1318   gtk_tree_model_get (model, iter,
1319       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
1320       EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name,
1321       -1);
1322
1323   if (is_group == FALSE)
1324     {
1325       g_free (name);
1326       return FALSE;
1327     }
1328
1329   priv = GET_PRIV (self);
1330
1331   if (g_hash_table_lookup_extended (priv->expand_groups, name, NULL,
1332       &should_expand) == TRUE)
1333     {
1334       if (GPOINTER_TO_INT (should_expand) == TRUE)
1335         gtk_tree_view_expand_row (GTK_TREE_VIEW (self), path, FALSE);
1336       else
1337         gtk_tree_view_collapse_row (GTK_TREE_VIEW (self), path);
1338
1339       g_hash_table_remove (priv->expand_groups, name);
1340     }
1341
1342   g_free (name);
1343
1344   return FALSE;
1345 }
1346
1347 static gboolean
1348 individual_view_expand_idle_cb (EmpathyIndividualView *self)
1349 {
1350   EmpathyIndividualViewPriv *priv = GET_PRIV (self);
1351
1352   DEBUG ("individual_view_expand_idle_cb");
1353
1354   g_signal_handlers_block_by_func (self,
1355     individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (TRUE));
1356   g_signal_handlers_block_by_func (self,
1357     individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (FALSE));
1358
1359   gtk_tree_model_foreach (GTK_TREE_MODEL (priv->filter),
1360       (GtkTreeModelForeachFunc) expand_idle_foreach_cb, self);
1361
1362   g_signal_handlers_unblock_by_func (self,
1363       individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (FALSE));
1364   g_signal_handlers_unblock_by_func (self,
1365       individual_view_row_expand_or_collapse_cb, GINT_TO_POINTER (TRUE));
1366
1367   g_object_unref (self);
1368   priv->expand_groups_idle_handler = 0;
1369
1370   return FALSE;
1371 }
1372
1373 static void
1374 individual_view_row_has_child_toggled_cb (GtkTreeModel *model,
1375     GtkTreePath *path,
1376     GtkTreeIter *iter,
1377     EmpathyIndividualView *view)
1378 {
1379   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
1380   gboolean should_expand, is_group = FALSE;
1381   gchar *name = NULL;
1382   gpointer will_expand;
1383
1384   gtk_tree_model_get (model, iter,
1385       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
1386       EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name,
1387       -1);
1388
1389   if (!is_group || EMP_STR_EMPTY (name))
1390     {
1391       g_free (name);
1392       return;
1393     }
1394
1395   should_expand = (priv->view_features &
1396           EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_SAVE) == 0 ||
1397       (priv->search_widget != NULL &&
1398           gtk_widget_get_visible (priv->search_widget)) ||
1399       empathy_contact_group_get_expanded (name);
1400
1401   /* FIXME: It doesn't work to call gtk_tree_view_expand_row () from within
1402    * gtk_tree_model_filter_refilter (). We add the rows to expand/contract to
1403    * a hash table, and expand or contract them as appropriate all at once in
1404    * an idle handler which iterates over all the group rows. */
1405   if (g_hash_table_lookup_extended (priv->expand_groups, name, NULL,
1406       &will_expand) == FALSE &&
1407       GPOINTER_TO_INT (will_expand) != should_expand)
1408     {
1409       g_hash_table_insert (priv->expand_groups, g_strdup (name),
1410           GINT_TO_POINTER (should_expand));
1411
1412       if (priv->expand_groups_idle_handler == 0)
1413         {
1414           priv->expand_groups_idle_handler =
1415               g_idle_add ((GSourceFunc) individual_view_expand_idle_cb,
1416                   g_object_ref (view));
1417         }
1418     }
1419
1420   g_free (name);
1421 }
1422
1423 /* FIXME: This is a workaround for bgo#621076 */
1424 static void
1425 individual_view_verify_group_visibility (EmpathyIndividualView *view,
1426     GtkTreePath *path)
1427 {
1428   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
1429   GtkTreeModel *model;
1430   GtkTreePath *parent_path;
1431   GtkTreeIter parent_iter;
1432
1433   if (gtk_tree_path_get_depth (path) < 2)
1434     return;
1435
1436   /* A group row is visible if and only if at least one if its child is visible.
1437    * So when a row is inserted/deleted/changed in the base model, that could
1438    * modify the visibility of its parent in the filter model.
1439   */
1440
1441   model = GTK_TREE_MODEL (priv->store);
1442   parent_path = gtk_tree_path_copy (path);
1443   gtk_tree_path_up (parent_path);
1444   if (gtk_tree_model_get_iter (model, &parent_iter, parent_path))
1445     {
1446       /* This tells the filter to verify the visibility of that row, and
1447        * show/hide it if necessary */
1448       gtk_tree_model_row_changed (GTK_TREE_MODEL (priv->store),
1449               parent_path, &parent_iter);
1450     }
1451   gtk_tree_path_free (parent_path);
1452 }
1453
1454 static void
1455 individual_view_store_row_changed_cb (GtkTreeModel *model,
1456   GtkTreePath *path,
1457   GtkTreeIter *iter,
1458   EmpathyIndividualView *view)
1459 {
1460   individual_view_verify_group_visibility (view, path);
1461 }
1462
1463 static void
1464 individual_view_store_row_deleted_cb (GtkTreeModel *model,
1465   GtkTreePath *path,
1466   EmpathyIndividualView *view)
1467 {
1468   individual_view_verify_group_visibility (view, path);
1469 }
1470
1471 static gboolean
1472 individual_view_is_visible_individual (EmpathyIndividualView *self,
1473     FolksIndividual *individual)
1474 {
1475   EmpathyIndividualViewPriv *priv = GET_PRIV (self);
1476   EmpathyLiveSearch *live = EMPATHY_LIVE_SEARCH (priv->search_widget);
1477   const gchar *str;
1478   GList *personas, *l;
1479
1480   /* We're only giving the visibility wrt filtering here, not things like
1481    * presence. */
1482   if (live == NULL || gtk_widget_get_visible (GTK_WIDGET (live)) == FALSE)
1483     return TRUE;
1484
1485   /* check alias name */
1486   str = folks_individual_get_alias (individual);
1487
1488   if (empathy_live_search_match (live, str))
1489     return TRUE;
1490
1491   /* check contact id, remove the @server.com part */
1492   personas = folks_individual_get_personas (individual);
1493   for (l = personas; l; l = l->next)
1494     {
1495       const gchar *p;
1496       gchar *dup_str = NULL;
1497       gboolean visible;
1498
1499       if (!TPF_IS_PERSONA (l->data))
1500         continue;
1501
1502       str = folks_persona_get_display_id (l->data);
1503       p = strstr (str, "@");
1504       if (p != NULL)
1505         str = dup_str = g_strndup (str, p - str);
1506
1507       visible = empathy_live_search_match (live, str);
1508       g_free (dup_str);
1509       if (visible)
1510         return TRUE;
1511     }
1512
1513   /* FIXME: Add more rules here, we could check phone numbers in
1514    * contact's vCard for example. */
1515
1516   return FALSE;
1517 }
1518
1519 static gboolean
1520 individual_view_filter_visible_func (GtkTreeModel *model,
1521     GtkTreeIter *iter,
1522     gpointer user_data)
1523 {
1524   EmpathyIndividualView *self = EMPATHY_INDIVIDUAL_VIEW (user_data);
1525   EmpathyIndividualViewPriv *priv = GET_PRIV (self);
1526   FolksIndividual *individual = NULL;
1527   gboolean is_group, is_separator, valid;
1528   GtkTreeIter child_iter;
1529   gboolean visible, is_online;
1530   gboolean is_searching = TRUE;
1531
1532   if (priv->search_widget == NULL ||
1533       !gtk_widget_get_visible (priv->search_widget))
1534      is_searching = FALSE;
1535
1536   gtk_tree_model_get (model, iter,
1537       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
1538       EMPATHY_INDIVIDUAL_STORE_COL_IS_SEPARATOR, &is_separator,
1539       EMPATHY_INDIVIDUAL_STORE_COL_IS_ONLINE, &is_online,
1540       EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual,
1541       -1);
1542
1543   if (individual != NULL)
1544     {
1545       if (is_searching == TRUE)
1546         visible = individual_view_is_visible_individual (self, individual);
1547       else
1548         visible = (priv->show_offline || is_online);
1549
1550       g_object_unref (individual);
1551
1552       /* FIXME: Work around bgo#626552/bgo#621076 */
1553       if (visible == TRUE)
1554         {
1555           GtkTreePath *path = gtk_tree_model_get_path (model, iter);
1556           individual_view_verify_group_visibility (self, path);
1557           gtk_tree_path_free (path);
1558         }
1559
1560       return visible;
1561     }
1562
1563   if (is_separator)
1564     return TRUE;
1565
1566   /* Not a contact, not a separator, must be a group */
1567   g_return_val_if_fail (is_group, FALSE);
1568
1569   /* only show groups which are not empty */
1570   for (valid = gtk_tree_model_iter_children (model, &child_iter, iter);
1571        valid; valid = gtk_tree_model_iter_next (model, &child_iter))
1572     {
1573       gtk_tree_model_get (model, &child_iter,
1574         EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual,
1575         EMPATHY_INDIVIDUAL_STORE_COL_IS_ONLINE, &is_online,
1576         -1);
1577
1578       if (individual == NULL)
1579         continue;
1580
1581       visible = individual_view_is_visible_individual (self, individual);
1582       g_object_unref (individual);
1583
1584       /* show group if it has at least one visible contact in it */
1585       if ((is_searching && visible) ||
1586           (!is_searching && (priv->show_offline || is_online)))
1587         return TRUE;
1588     }
1589
1590   return FALSE;
1591 }
1592
1593 static void
1594 individual_view_constructed (GObject *object)
1595 {
1596   EmpathyIndividualView *view = EMPATHY_INDIVIDUAL_VIEW (object);
1597   GtkCellRenderer *cell;
1598   GtkTreeViewColumn *col;
1599   guint i;
1600
1601   /* Setup view */
1602   g_object_set (view,
1603       "headers-visible", FALSE,
1604       "show-expanders", FALSE,
1605       NULL);
1606
1607   col = gtk_tree_view_column_new ();
1608
1609   /* State */
1610   cell = gtk_cell_renderer_pixbuf_new ();
1611   gtk_tree_view_column_pack_start (col, cell, FALSE);
1612   gtk_tree_view_column_set_cell_data_func (col, cell,
1613       (GtkTreeCellDataFunc) individual_view_pixbuf_cell_data_func,
1614       view, NULL);
1615
1616   g_object_set (cell,
1617       "xpad", 5,
1618       "ypad", 1,
1619       "visible", FALSE,
1620       NULL);
1621
1622   /* Group icon */
1623   cell = gtk_cell_renderer_pixbuf_new ();
1624   gtk_tree_view_column_pack_start (col, cell, FALSE);
1625   gtk_tree_view_column_set_cell_data_func (col, cell,
1626       (GtkTreeCellDataFunc) individual_view_group_icon_cell_data_func,
1627       view, NULL);
1628
1629   g_object_set (cell,
1630       "xpad", 0,
1631       "ypad", 0,
1632       "visible", FALSE,
1633       "width", 16,
1634       "height", 16,
1635       NULL);
1636
1637   /* Name */
1638   cell = empathy_cell_renderer_text_new ();
1639   gtk_tree_view_column_pack_start (col, cell, TRUE);
1640   gtk_tree_view_column_set_cell_data_func (col, cell,
1641       (GtkTreeCellDataFunc) individual_view_text_cell_data_func, view, NULL);
1642
1643   gtk_tree_view_column_add_attribute (col, cell,
1644       "name", EMPATHY_INDIVIDUAL_STORE_COL_NAME);
1645   gtk_tree_view_column_add_attribute (col, cell,
1646       "text", EMPATHY_INDIVIDUAL_STORE_COL_NAME);
1647   gtk_tree_view_column_add_attribute (col, cell,
1648       "presence-type", EMPATHY_INDIVIDUAL_STORE_COL_PRESENCE_TYPE);
1649   gtk_tree_view_column_add_attribute (col, cell,
1650       "status", EMPATHY_INDIVIDUAL_STORE_COL_STATUS);
1651   gtk_tree_view_column_add_attribute (col, cell,
1652       "is_group", EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP);
1653   gtk_tree_view_column_add_attribute (col, cell,
1654       "compact", EMPATHY_INDIVIDUAL_STORE_COL_COMPACT);
1655
1656   /* Audio Call Icon */
1657   cell = empathy_cell_renderer_activatable_new ();
1658   gtk_tree_view_column_pack_start (col, cell, FALSE);
1659   gtk_tree_view_column_set_cell_data_func (col, cell,
1660       (GtkTreeCellDataFunc) individual_view_audio_call_cell_data_func,
1661       view, NULL);
1662
1663   g_object_set (cell, "visible", FALSE, NULL);
1664
1665   g_signal_connect (cell, "path-activated",
1666       G_CALLBACK (individual_view_call_activated_cb), view);
1667
1668   /* Avatar */
1669   cell = gtk_cell_renderer_pixbuf_new ();
1670   gtk_tree_view_column_pack_start (col, cell, FALSE);
1671   gtk_tree_view_column_set_cell_data_func (col, cell,
1672       (GtkTreeCellDataFunc) individual_view_avatar_cell_data_func,
1673       view, NULL);
1674
1675   g_object_set (cell,
1676       "xpad", 0,
1677       "ypad", 0,
1678       "visible", FALSE,
1679       "width", 32,
1680       "height", 32,
1681       NULL);
1682
1683   /* Expander */
1684   cell = empathy_cell_renderer_expander_new ();
1685   gtk_tree_view_column_pack_end (col, cell, FALSE);
1686   gtk_tree_view_column_set_cell_data_func (col, cell,
1687       (GtkTreeCellDataFunc) individual_view_expander_cell_data_func,
1688       view, NULL);
1689
1690   /* Actually add the column now we have added all cell renderers */
1691   gtk_tree_view_append_column (GTK_TREE_VIEW (view), col);
1692
1693   /* Drag & Drop. */
1694   for (i = 0; i < G_N_ELEMENTS (drag_types_dest); ++i)
1695     {
1696       drag_atoms_dest[i] = gdk_atom_intern (drag_types_dest[i].target, FALSE);
1697     }
1698
1699   for (i = 0; i < G_N_ELEMENTS (drag_types_source); ++i)
1700     {
1701       drag_atoms_source[i] = gdk_atom_intern (drag_types_source[i].target,
1702           FALSE);
1703     }
1704 }
1705
1706 static void
1707 individual_view_set_view_features (EmpathyIndividualView *view,
1708     EmpathyIndividualFeatureFlags features)
1709 {
1710   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
1711   gboolean has_tooltip;
1712
1713   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view));
1714
1715   priv->view_features = features;
1716
1717   /* Setting reorderable is a hack that gets us row previews as drag icons
1718      for free.  We override all the drag handlers.  It's tricky to get the
1719      position of the drag icon right in drag_begin.  GtkTreeView has special
1720      voodoo for it, so we let it do the voodoo that he do (but only if dragging
1721      is enabled).
1722    */
1723   gtk_tree_view_set_reorderable (GTK_TREE_VIEW (view),
1724       (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_CONTACT_DRAG));
1725
1726   /* Update DnD source/dest */
1727   if (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_CONTACT_DRAG)
1728     {
1729       gtk_drag_source_set (GTK_WIDGET (view),
1730           GDK_BUTTON1_MASK,
1731           drag_types_source,
1732           G_N_ELEMENTS (drag_types_source),
1733           GDK_ACTION_MOVE | GDK_ACTION_COPY);
1734     }
1735   else
1736     {
1737       gtk_drag_source_unset (GTK_WIDGET (view));
1738
1739     }
1740
1741   if (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_CONTACT_DROP)
1742     {
1743       gtk_drag_dest_set (GTK_WIDGET (view),
1744           GTK_DEST_DEFAULT_ALL,
1745           drag_types_dest,
1746           G_N_ELEMENTS (drag_types_dest), GDK_ACTION_MOVE | GDK_ACTION_COPY);
1747     }
1748   else
1749     {
1750       /* FIXME: URI could still be droped depending on FT feature */
1751       gtk_drag_dest_unset (GTK_WIDGET (view));
1752     }
1753
1754   /* Update has-tooltip */
1755   has_tooltip =
1756       (features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_CONTACT_TOOLTIP) != 0;
1757   gtk_widget_set_has_tooltip (GTK_WIDGET (view), has_tooltip);
1758 }
1759
1760 static void
1761 individual_view_dispose (GObject *object)
1762 {
1763   EmpathyIndividualView *view = EMPATHY_INDIVIDUAL_VIEW (object);
1764   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
1765
1766   tp_clear_object (&priv->store);
1767   tp_clear_object (&priv->filter);
1768   tp_clear_pointer (&priv->tooltip_widget, gtk_widget_destroy);
1769   tp_clear_pointer (&priv->file_targets, gtk_target_list_unref);
1770
1771   empathy_individual_view_set_live_search (view, NULL);
1772
1773   G_OBJECT_CLASS (empathy_individual_view_parent_class)->dispose (object);
1774 }
1775
1776 static void
1777 individual_view_finalize (GObject *object)
1778 {
1779   EmpathyIndividualViewPriv *priv = GET_PRIV (object);
1780
1781   g_hash_table_destroy (priv->expand_groups);
1782
1783   G_OBJECT_CLASS (empathy_individual_view_parent_class)->finalize (object);
1784 }
1785
1786 static void
1787 individual_view_get_property (GObject *object,
1788     guint param_id,
1789     GValue *value,
1790     GParamSpec *pspec)
1791 {
1792   EmpathyIndividualViewPriv *priv;
1793
1794   priv = GET_PRIV (object);
1795
1796   switch (param_id)
1797     {
1798     case PROP_STORE:
1799       g_value_set_object (value, priv->store);
1800       break;
1801     case PROP_VIEW_FEATURES:
1802       g_value_set_flags (value, priv->view_features);
1803       break;
1804     case PROP_INDIVIDUAL_FEATURES:
1805       g_value_set_flags (value, priv->individual_features);
1806       break;
1807     case PROP_SHOW_OFFLINE:
1808       g_value_set_boolean (value, priv->show_offline);
1809       break;
1810     default:
1811       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1812       break;
1813     };
1814 }
1815
1816 static void
1817 individual_view_set_property (GObject *object,
1818     guint param_id,
1819     const GValue *value,
1820     GParamSpec *pspec)
1821 {
1822   EmpathyIndividualView *view = EMPATHY_INDIVIDUAL_VIEW (object);
1823   EmpathyIndividualViewPriv *priv = GET_PRIV (object);
1824
1825   switch (param_id)
1826     {
1827     case PROP_STORE:
1828       empathy_individual_view_set_store (view, g_value_get_object (value));
1829       break;
1830     case PROP_VIEW_FEATURES:
1831       individual_view_set_view_features (view, g_value_get_flags (value));
1832       break;
1833     case PROP_INDIVIDUAL_FEATURES:
1834       priv->individual_features = g_value_get_flags (value);
1835       break;
1836     case PROP_SHOW_OFFLINE:
1837       empathy_individual_view_set_show_offline (view,
1838           g_value_get_boolean (value));
1839       break;
1840     default:
1841       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1842       break;
1843     };
1844 }
1845
1846 static void
1847 empathy_individual_view_class_init (EmpathyIndividualViewClass *klass)
1848 {
1849   GObjectClass *object_class = G_OBJECT_CLASS (klass);
1850   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
1851   GtkTreeViewClass *tree_view_class = GTK_TREE_VIEW_CLASS (klass);
1852
1853   object_class->constructed = individual_view_constructed;
1854   object_class->dispose = individual_view_dispose;
1855   object_class->finalize = individual_view_finalize;
1856   object_class->get_property = individual_view_get_property;
1857   object_class->set_property = individual_view_set_property;
1858
1859   widget_class->drag_data_received = individual_view_drag_data_received;
1860   widget_class->drag_drop = individual_view_drag_drop;
1861   widget_class->drag_begin = individual_view_drag_begin;
1862   widget_class->drag_data_get = individual_view_drag_data_get;
1863   widget_class->drag_end = individual_view_drag_end;
1864   widget_class->drag_motion = individual_view_drag_motion;
1865
1866   /* We use the class method to let user of this widget to connect to
1867    * the signal and stop emission of the signal so the default handler
1868    * won't be called. */
1869   tree_view_class->row_activated = individual_view_row_activated;
1870
1871   signals[DRAG_CONTACT_RECEIVED] =
1872       g_signal_new ("drag-contact-received",
1873       G_OBJECT_CLASS_TYPE (klass),
1874       G_SIGNAL_RUN_LAST,
1875       0,
1876       NULL, NULL,
1877       _empathy_gtk_marshal_VOID__OBJECT_STRING_STRING,
1878       G_TYPE_NONE, 3, EMPATHY_TYPE_CONTACT, G_TYPE_STRING, G_TYPE_STRING);
1879
1880   g_object_class_install_property (object_class,
1881       PROP_STORE,
1882       g_param_spec_object ("store",
1883           "The store of the view",
1884           "The store of the view",
1885           EMPATHY_TYPE_INDIVIDUAL_STORE,
1886           G_PARAM_READWRITE));
1887   g_object_class_install_property (object_class,
1888       PROP_VIEW_FEATURES,
1889       g_param_spec_flags ("view-features",
1890           "Features of the view",
1891           "Flags for all enabled features",
1892           EMPATHY_TYPE_INDIVIDUAL_VIEW_FEATURE_FLAGS,
1893           EMPATHY_INDIVIDUAL_VIEW_FEATURE_NONE, G_PARAM_READWRITE));
1894   g_object_class_install_property (object_class,
1895       PROP_INDIVIDUAL_FEATURES,
1896       g_param_spec_flags ("individual-features",
1897           "Features of the contact menu",
1898           "Flags for all enabled features for the menu",
1899           EMPATHY_TYPE_INDIVIDUAL_FEATURE_FLAGS,
1900           EMPATHY_CONTACT_FEATURE_NONE, G_PARAM_READWRITE));
1901   g_object_class_install_property (object_class,
1902       PROP_SHOW_OFFLINE,
1903       g_param_spec_boolean ("show-offline",
1904           "Show Offline",
1905           "Whether contact list should display "
1906           "offline contacts", FALSE, G_PARAM_READWRITE));
1907
1908   g_type_class_add_private (object_class, sizeof (EmpathyIndividualViewPriv));
1909 }
1910
1911 static void
1912 empathy_individual_view_init (EmpathyIndividualView *view)
1913 {
1914   EmpathyIndividualViewPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (view,
1915       EMPATHY_TYPE_INDIVIDUAL_VIEW, EmpathyIndividualViewPriv);
1916
1917   view->priv = priv;
1918   /* Get saved group states. */
1919   empathy_contact_groups_get_all ();
1920
1921   priv->expand_groups = g_hash_table_new_full (g_str_hash, g_str_equal,
1922       (GDestroyNotify) g_free, NULL);
1923
1924   gtk_tree_view_set_row_separator_func (GTK_TREE_VIEW (view),
1925       empathy_individual_store_row_separator_func, NULL, NULL);
1926
1927   /* Set up drag target lists. */
1928   priv->file_targets = gtk_target_list_new (drag_types_dest_file,
1929       G_N_ELEMENTS (drag_types_dest_file));
1930
1931   /* Connect to tree view signals rather than override. */
1932   g_signal_connect (view, "button-press-event",
1933       G_CALLBACK (individual_view_button_press_event_cb), NULL);
1934   g_signal_connect (view, "key-press-event",
1935       G_CALLBACK (individual_view_key_press_event_cb), NULL);
1936   g_signal_connect (view, "row-expanded",
1937       G_CALLBACK (individual_view_row_expand_or_collapse_cb),
1938       GINT_TO_POINTER (TRUE));
1939   g_signal_connect (view, "row-collapsed",
1940       G_CALLBACK (individual_view_row_expand_or_collapse_cb),
1941       GINT_TO_POINTER (FALSE));
1942   g_signal_connect (view, "query-tooltip",
1943       G_CALLBACK (individual_view_query_tooltip_cb), NULL);
1944 }
1945
1946 EmpathyIndividualView *
1947 empathy_individual_view_new (EmpathyIndividualStore *store,
1948     EmpathyIndividualViewFeatureFlags view_features,
1949     EmpathyIndividualFeatureFlags individual_features)
1950 {
1951   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_STORE (store), NULL);
1952
1953   return g_object_new (EMPATHY_TYPE_INDIVIDUAL_VIEW,
1954       "store", store,
1955       "individual-features", individual_features,
1956       "view-features", view_features, NULL);
1957 }
1958
1959 FolksIndividual *
1960 empathy_individual_view_dup_selected (EmpathyIndividualView *view)
1961 {
1962   EmpathyIndividualViewPriv *priv;
1963   GtkTreeSelection *selection;
1964   GtkTreeIter iter;
1965   GtkTreeModel *model;
1966   FolksIndividual *individual;
1967
1968   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), NULL);
1969
1970   priv = GET_PRIV (view);
1971
1972   selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view));
1973   if (!gtk_tree_selection_get_selected (selection, &model, &iter))
1974     return NULL;
1975
1976   gtk_tree_model_get (model, &iter,
1977       EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual, -1);
1978
1979   return individual;
1980 }
1981
1982 EmpathyIndividualManagerFlags
1983 empathy_individual_view_get_flags (EmpathyIndividualView *view)
1984 {
1985   EmpathyIndividualViewPriv *priv;
1986   GtkTreeSelection *selection;
1987   GtkTreeIter iter;
1988   GtkTreeModel *model;
1989   EmpathyIndividualFeatureFlags flags;
1990
1991   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), 0);
1992
1993   priv = GET_PRIV (view);
1994
1995   selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view));
1996   if (!gtk_tree_selection_get_selected (selection, &model, &iter))
1997     return 0;
1998
1999   gtk_tree_model_get (model, &iter,
2000       EMPATHY_INDIVIDUAL_STORE_COL_FLAGS, &flags, -1);
2001
2002   return flags;
2003 }
2004
2005 gchar *
2006 empathy_individual_view_get_selected_group (EmpathyIndividualView *view,
2007     gboolean *is_fake_group)
2008 {
2009   EmpathyIndividualViewPriv *priv;
2010   GtkTreeSelection *selection;
2011   GtkTreeIter iter;
2012   GtkTreeModel *model;
2013   gboolean is_group;
2014   gchar *name;
2015   gboolean fake;
2016
2017   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), NULL);
2018
2019   priv = GET_PRIV (view);
2020
2021   selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (view));
2022   if (!gtk_tree_selection_get_selected (selection, &model, &iter))
2023     return NULL;
2024
2025   gtk_tree_model_get (model, &iter,
2026       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
2027       EMPATHY_INDIVIDUAL_STORE_COL_NAME, &name,
2028       EMPATHY_INDIVIDUAL_STORE_COL_IS_FAKE_GROUP, &fake, -1);
2029
2030   if (!is_group)
2031     {
2032       g_free (name);
2033       return NULL;
2034     }
2035
2036   if (is_fake_group != NULL)
2037     *is_fake_group = fake;
2038
2039   return name;
2040 }
2041
2042 static gboolean
2043 individual_view_remove_dialog_show (GtkWindow *parent,
2044     const gchar *message,
2045     const gchar *secondary_text)
2046 {
2047   GtkWidget *dialog;
2048   gboolean res;
2049
2050   dialog = gtk_message_dialog_new (parent, GTK_DIALOG_MODAL,
2051       GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE, "%s", message);
2052   gtk_dialog_add_buttons (GTK_DIALOG (dialog),
2053       GTK_STOCK_CANCEL, GTK_RESPONSE_NO,
2054       GTK_STOCK_DELETE, GTK_RESPONSE_YES, NULL);
2055   gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
2056       "%s", secondary_text);
2057
2058   gtk_widget_show (dialog);
2059
2060   res = gtk_dialog_run (GTK_DIALOG (dialog));
2061   gtk_widget_destroy (dialog);
2062
2063   return (res == GTK_RESPONSE_YES);
2064 }
2065
2066 static void
2067 individual_view_group_remove_activate_cb (GtkMenuItem *menuitem,
2068     EmpathyIndividualView *view)
2069 {
2070   gchar *group;
2071
2072   group = empathy_individual_view_get_selected_group (view, NULL);
2073   if (group != NULL)
2074     {
2075       gchar *text;
2076       GtkWindow *parent;
2077
2078       text =
2079           g_strdup_printf (_("Do you really want to remove the group '%s'?"),
2080           group);
2081       parent = empathy_get_toplevel_window (GTK_WIDGET (view));
2082       if (individual_view_remove_dialog_show (parent, _("Removing group"),
2083               text))
2084         {
2085           EmpathyIndividualManager *manager =
2086               empathy_individual_manager_dup_singleton ();
2087           empathy_individual_manager_remove_group (manager, group);
2088           g_object_unref (G_OBJECT (manager));
2089         }
2090
2091       g_free (text);
2092     }
2093
2094   g_free (group);
2095 }
2096
2097 GtkWidget *
2098 empathy_individual_view_get_group_menu (EmpathyIndividualView *view)
2099 {
2100   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
2101   gchar *group;
2102   GtkWidget *menu;
2103   GtkWidget *item;
2104   GtkWidget *image;
2105   gboolean is_fake_group;
2106
2107   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), NULL);
2108
2109   if (!(priv->view_features & (EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_RENAME |
2110               EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_REMOVE)))
2111     return NULL;
2112
2113   group = empathy_individual_view_get_selected_group (view, &is_fake_group);
2114   if (!group || is_fake_group)
2115     {
2116       /* We can't alter fake groups */
2117       return NULL;
2118     }
2119
2120   menu = gtk_menu_new ();
2121
2122   /* TODO: implement
2123      if (priv->view_features &
2124      EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_RENAME) {
2125      item = gtk_menu_item_new_with_mnemonic (_("Re_name"));
2126      gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
2127      gtk_widget_show (item);
2128      g_signal_connect (item, "activate",
2129      G_CALLBACK (individual_view_group_rename_activate_cb),
2130      view);
2131      }
2132    */
2133
2134   if (priv->view_features & EMPATHY_INDIVIDUAL_VIEW_FEATURE_GROUPS_REMOVE)
2135     {
2136       item = gtk_image_menu_item_new_with_mnemonic (_("_Remove"));
2137       image = gtk_image_new_from_icon_name (GTK_STOCK_REMOVE,
2138           GTK_ICON_SIZE_MENU);
2139       gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image);
2140       gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
2141       gtk_widget_show (item);
2142       g_signal_connect (item, "activate",
2143           G_CALLBACK (individual_view_group_remove_activate_cb), view);
2144     }
2145
2146   g_free (group);
2147
2148   return menu;
2149 }
2150
2151 static void
2152 individual_view_remove_activate_cb (GtkMenuItem *menuitem,
2153     EmpathyIndividualView *view)
2154 {
2155   FolksIndividual *individual;
2156
2157   individual = empathy_individual_view_dup_selected (view);
2158
2159   if (individual != NULL)
2160     {
2161       gchar *text;
2162       GtkWindow *parent;
2163
2164       parent = empathy_get_toplevel_window (GTK_WIDGET (view));
2165       text =
2166           g_strdup_printf (_
2167           ("Do you really want to remove the contact '%s'?"),
2168           folks_individual_get_alias (individual));
2169       if (individual_view_remove_dialog_show (parent, _("Removing contact"),
2170               text))
2171         {
2172           EmpathyIndividualManager *manager;
2173
2174           manager = empathy_individual_manager_dup_singleton ();
2175           empathy_individual_manager_remove (manager, individual, "");
2176           g_object_unref (G_OBJECT (manager));
2177         }
2178
2179       g_free (text);
2180       g_object_unref (individual);
2181     }
2182 }
2183
2184 GtkWidget *
2185 empathy_individual_view_get_individual_menu (EmpathyIndividualView *view)
2186 {
2187   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
2188   FolksIndividual *individual;
2189   GtkWidget *menu = NULL;
2190   GtkWidget *item;
2191   GtkWidget *image;
2192   EmpathyIndividualManagerFlags flags;
2193
2194   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (view), NULL);
2195
2196   individual = empathy_individual_view_dup_selected (view);
2197   if (individual == NULL)
2198     return NULL;
2199
2200   flags = empathy_individual_view_get_flags (view);
2201
2202   menu = empathy_individual_menu_new (individual, priv->individual_features);
2203
2204   /* Remove contact */
2205   if (priv->view_features &
2206       EMPATHY_INDIVIDUAL_VIEW_FEATURE_CONTACT_REMOVE &&
2207       flags & EMPATHY_INDIVIDUAL_MANAGER_CAN_REMOVE)
2208     {
2209
2210       /* create the menu if required, or just add a separator */
2211       if (menu == NULL)
2212         menu = gtk_menu_new ();
2213       else
2214         {
2215           item = gtk_separator_menu_item_new ();
2216           gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
2217           gtk_widget_show (item);
2218         }
2219
2220       /* Remove */
2221       item = gtk_image_menu_item_new_with_mnemonic (_("_Remove"));
2222       image = gtk_image_new_from_icon_name (GTK_STOCK_REMOVE,
2223           GTK_ICON_SIZE_MENU);
2224       gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image);
2225       gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
2226       gtk_widget_show (item);
2227       g_signal_connect (item, "activate",
2228           G_CALLBACK (individual_view_remove_activate_cb), view);
2229     }
2230
2231   g_object_unref (individual);
2232
2233   return menu;
2234 }
2235
2236 void
2237 empathy_individual_view_set_live_search (EmpathyIndividualView *view,
2238     EmpathyLiveSearch *search)
2239 {
2240   EmpathyIndividualViewPriv *priv = GET_PRIV (view);
2241
2242   /* remove old handlers if old search was not null */
2243   if (priv->search_widget != NULL)
2244     {
2245       g_signal_handlers_disconnect_by_func (view,
2246           individual_view_start_search_cb, NULL);
2247
2248       g_signal_handlers_disconnect_by_func (priv->search_widget,
2249           individual_view_search_text_notify_cb, view);
2250       g_signal_handlers_disconnect_by_func (priv->search_widget,
2251           individual_view_search_activate_cb, view);
2252       g_signal_handlers_disconnect_by_func (priv->search_widget,
2253           individual_view_search_key_navigation_cb, view);
2254       g_signal_handlers_disconnect_by_func (priv->search_widget,
2255           individual_view_search_hide_cb, view);
2256       g_signal_handlers_disconnect_by_func (priv->search_widget,
2257           individual_view_search_show_cb, view);
2258       g_object_unref (priv->search_widget);
2259       priv->search_widget = NULL;
2260     }
2261
2262   /* connect handlers if new search is not null */
2263   if (search != NULL)
2264     {
2265       priv->search_widget = g_object_ref (search);
2266
2267       g_signal_connect (view, "start-interactive-search",
2268           G_CALLBACK (individual_view_start_search_cb), NULL);
2269
2270       g_signal_connect (priv->search_widget, "notify::text",
2271           G_CALLBACK (individual_view_search_text_notify_cb), view);
2272       g_signal_connect (priv->search_widget, "activate",
2273           G_CALLBACK (individual_view_search_activate_cb), view);
2274       g_signal_connect (priv->search_widget, "key-navigation",
2275           G_CALLBACK (individual_view_search_key_navigation_cb), view);
2276       g_signal_connect (priv->search_widget, "hide",
2277           G_CALLBACK (individual_view_search_hide_cb), view);
2278       g_signal_connect (priv->search_widget, "show",
2279           G_CALLBACK (individual_view_search_show_cb), view);
2280     }
2281 }
2282
2283 gboolean
2284 empathy_individual_view_is_searching (EmpathyIndividualView *self)
2285 {
2286   EmpathyIndividualViewPriv *priv;
2287
2288   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self), FALSE);
2289
2290   priv = GET_PRIV (self);
2291
2292   return (priv->search_widget != NULL &&
2293           gtk_widget_get_visible (priv->search_widget));
2294 }
2295
2296 gboolean
2297 empathy_individual_view_get_show_offline (EmpathyIndividualView *self)
2298 {
2299   EmpathyIndividualViewPriv *priv;
2300
2301   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self), FALSE);
2302
2303   priv = GET_PRIV (self);
2304
2305   return priv->show_offline;
2306 }
2307
2308 void
2309 empathy_individual_view_set_show_offline (EmpathyIndividualView *self,
2310     gboolean show_offline)
2311 {
2312   EmpathyIndividualViewPriv *priv;
2313
2314   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self));
2315
2316   priv = GET_PRIV (self);
2317
2318   priv->show_offline = show_offline;
2319
2320   g_object_notify (G_OBJECT (self), "show-offline");
2321   gtk_tree_model_filter_refilter (priv->filter);
2322 }
2323
2324 EmpathyIndividualStore *
2325 empathy_individual_view_get_store (EmpathyIndividualView *self)
2326 {
2327   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self), NULL);
2328
2329   return GET_PRIV (self)->store;
2330 }
2331
2332 void
2333 empathy_individual_view_set_store (EmpathyIndividualView *self,
2334     EmpathyIndividualStore *store)
2335 {
2336   EmpathyIndividualViewPriv *priv;
2337
2338   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_VIEW (self));
2339   g_return_if_fail (store == NULL || EMPATHY_IS_INDIVIDUAL_STORE (store));
2340
2341   priv = GET_PRIV (self);
2342
2343   /* Destroy the old filter and remove the old store */
2344   if (priv->store != NULL)
2345     {
2346       g_signal_handlers_disconnect_by_func (priv->store,
2347           individual_view_store_row_changed_cb, self);
2348       g_signal_handlers_disconnect_by_func (priv->store,
2349           individual_view_store_row_deleted_cb, self);
2350
2351       g_signal_handlers_disconnect_by_func (priv->filter,
2352           individual_view_row_has_child_toggled_cb, self);
2353
2354       gtk_tree_view_set_model (GTK_TREE_VIEW (self), NULL);
2355     }
2356
2357   tp_clear_object (&priv->filter);
2358   tp_clear_object (&priv->store);
2359
2360   /* Set the new store */
2361   priv->store = store;
2362
2363   if (store != NULL)
2364     {
2365       g_object_ref (store);
2366
2367       /* Create a new filter */
2368       priv->filter = GTK_TREE_MODEL_FILTER (gtk_tree_model_filter_new (
2369           GTK_TREE_MODEL (priv->store), NULL));
2370       gtk_tree_model_filter_set_visible_func (priv->filter,
2371           individual_view_filter_visible_func, self, NULL);
2372
2373       g_signal_connect (priv->filter, "row-has-child-toggled",
2374           G_CALLBACK (individual_view_row_has_child_toggled_cb), self);
2375       gtk_tree_view_set_model (GTK_TREE_VIEW (self),
2376           GTK_TREE_MODEL (priv->filter));
2377
2378       tp_g_signal_connect_object (priv->store, "row-changed",
2379           G_CALLBACK (individual_view_store_row_changed_cb), self, 0);
2380       tp_g_signal_connect_object (priv->store, "row-inserted",
2381           G_CALLBACK (individual_view_store_row_changed_cb), self, 0);
2382       tp_g_signal_connect_object (priv->store, "row-deleted",
2383           G_CALLBACK (individual_view_store_row_deleted_cb), self, 0);
2384     }
2385 }