b82aa93a11e7a97acc234423c4fff3f27e1ba74f
[empathy.git] / libempathy-gtk / empathy-contact-chooser.c
1 /*
2  * empathy-contact-chooser.c
3  *
4  * EmpathyContactChooser
5  *
6  * (c) 2009, Collabora Ltd.
7  *
8  * Authors:
9  *    Danielle Madeley <danielle.madeley@collabora.co.uk>
10  */
11
12 #include "config.h"
13 #include "empathy-contact-chooser.h"
14
15 #include "empathy-client-factory.h"
16 #include "empathy-individual-store-manager.h"
17 #include "empathy-individual-view.h"
18 #include "empathy-ui-utils.h"
19 #include "empathy-utils.h"
20
21 G_DEFINE_TYPE (EmpathyContactChooser,
22     empathy_contact_chooser, GTK_TYPE_BOX);
23
24 enum {
25   SIG_SELECTION_CHANGED,
26   SIG_ACTIVATE,
27   LAST_SIGNAL
28 };
29
30 static guint signals[LAST_SIGNAL];
31
32 typedef struct _AddTemporaryIndividualCtx AddTemporaryIndividualCtx;
33
34 struct _EmpathyContactChooserPrivate
35 {
36   TpAccountManager *account_mgr;
37
38   EmpathyIndividualStore *store;
39   EmpathyIndividualView *view;
40   GtkWidget *search_entry;
41   GtkWidget *scroll_view;
42
43   GPtrArray *search_words;
44   gchar *search_str;
45
46   /* Context representing the FolksIndividual which are added because of the
47    * current search from the user. */
48   AddTemporaryIndividualCtx *add_temp_ctx;
49
50   EmpathyContactChooserFilterFunc filter_func;
51   gpointer filter_data;
52
53   /* list of reffed TpContact */
54   GList *tp_contacts;
55 };
56
57 struct _AddTemporaryIndividualCtx
58 {
59   EmpathyContactChooser *self;
60   /* List of owned FolksIndividual */
61   GList *individuals;
62 };
63
64 static AddTemporaryIndividualCtx *
65 add_temporary_individual_ctx_new (EmpathyContactChooser *self)
66 {
67   AddTemporaryIndividualCtx *ctx = g_slice_new0 (AddTemporaryIndividualCtx);
68
69   ctx->self = self;
70   return ctx;
71 }
72
73 static void
74 add_temporary_individual_ctx_free (AddTemporaryIndividualCtx *ctx)
75 {
76   GList *l;
77
78   /* Remove all the individuals from the model */
79   for (l = ctx->individuals; l != NULL; l = g_list_next (l))
80     {
81       FolksIndividual *individual = l->data;
82
83       individual_store_remove_individual_and_disconnect (ctx->self->priv->store,
84           individual);
85
86       g_object_unref (individual);
87     }
88
89   g_list_free (ctx->individuals);
90   g_slice_free (AddTemporaryIndividualCtx, ctx);
91 }
92
93 static void
94 contact_chooser_dispose (GObject *object)
95 {
96   EmpathyContactChooser *self = (EmpathyContactChooser *)
97     object;
98
99   tp_clear_pointer (&self->priv->add_temp_ctx,
100       add_temporary_individual_ctx_free);
101
102   tp_clear_object (&self->priv->store);
103   tp_clear_pointer (&self->priv->search_words, g_ptr_array_unref);
104   tp_clear_pointer (&self->priv->search_str, g_free);
105
106   tp_clear_object (&self->priv->account_mgr);
107
108   g_list_free_full (self->priv->tp_contacts, g_object_unref);
109   self->priv->tp_contacts = NULL;
110
111   G_OBJECT_CLASS (empathy_contact_chooser_parent_class)->dispose (
112       object);
113 }
114
115 static void
116 empathy_contact_chooser_class_init (
117     EmpathyContactChooserClass *klass)
118 {
119   GObjectClass *object_class = G_OBJECT_CLASS (klass);
120
121   object_class->dispose = contact_chooser_dispose;
122
123   g_type_class_add_private (object_class,
124       sizeof (EmpathyContactChooserPrivate));
125
126   signals[SIG_SELECTION_CHANGED] = g_signal_new ("selection-changed",
127             G_TYPE_FROM_CLASS (klass),
128             G_SIGNAL_RUN_LAST,
129             0,
130             NULL, NULL,
131             g_cclosure_marshal_generic,
132             G_TYPE_NONE,
133             1, FOLKS_TYPE_INDIVIDUAL);
134
135   signals[SIG_ACTIVATE] = g_signal_new ("activate",
136             G_TYPE_FROM_CLASS (klass),
137             G_SIGNAL_RUN_LAST,
138             0,
139             NULL, NULL,
140             g_cclosure_marshal_generic,
141             G_TYPE_NONE,
142             0);
143 }
144
145 static void
146 view_selection_changed_cb (GtkWidget *treeview,
147     EmpathyContactChooser *self)
148 {
149   FolksIndividual *individual;
150
151   individual = empathy_individual_view_dup_selected (self->priv->view);
152
153   g_signal_emit (self, signals[SIG_SELECTION_CHANGED], 0, individual);
154
155   tp_clear_object (&individual);
156 }
157
158 static gboolean
159 filter_func (GtkTreeModel *model,
160     GtkTreeIter *iter,
161     gpointer user_data)
162 {
163   EmpathyContactChooser *self = user_data;
164   FolksIndividual *individual;
165   gboolean is_online;
166   gboolean display = FALSE;
167   gboolean searching = FALSE;
168
169   gtk_tree_model_get (model, iter,
170       EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual,
171       EMPATHY_INDIVIDUAL_STORE_COL_IS_ONLINE, &is_online,
172       -1);
173
174   if (individual == NULL)
175     goto out;
176
177   if (self->priv->search_words != NULL)
178     {
179       searching = TRUE;
180
181       /* Filter out the contact if we are searching and it doesn't match */
182       if (!empathy_individual_match_string (individual,
183             self->priv->search_str, self->priv->search_words))
184         goto out;
185     }
186
187   if (self->priv->filter_func == NULL)
188     display = TRUE;
189   else
190     display = self->priv->filter_func (self, individual, is_online, searching,
191       self->priv->filter_data);
192 out:
193   tp_clear_object (&individual);
194   return display;
195 }
196
197 static void
198 contact_capabilities_changed (TpContact *contact,
199     GParamSpec *pspec,
200     EmpathyContactChooser *self)
201 {
202   empathy_individual_view_refilter (self->priv->view);
203 }
204
205 static void
206 get_contacts_cb (GObject *source,
207     GAsyncResult *result,
208     gpointer user_data)
209 {
210   TpWeakRef *wr = user_data;
211   AddTemporaryIndividualCtx *ctx;
212   EmpathyContactChooser *self;
213   GError *error = NULL;
214   FolksIndividual *individual;
215   TpContact *contact;
216   EmpathyContact *emp_contact;
217
218   self = tp_weak_ref_dup_object (wr);
219   if (self == NULL)
220     goto out;
221
222   ctx = tp_weak_ref_get_user_data (wr);
223
224   emp_contact = empathy_client_factory_dup_contact_by_id_finish (
225         EMPATHY_CLIENT_FACTORY (source), result, &error);
226   if (emp_contact == NULL)
227     goto out;
228
229   contact = empathy_contact_get_tp_contact (emp_contact);
230
231   if (self->priv->add_temp_ctx != ctx)
232     /* another request has been started */
233     goto out;
234
235   individual =  empathy_ensure_individual_from_tp_contact (contact);
236   if (individual == NULL)
237     goto out;
238
239   /* tp-glib will unref the TpContact once we return from this callback
240    * but folks expect us to keep a reference on the TpContact.
241    * Ideally folks shouldn't force us to do that: bgo #666580 */
242   self->priv->tp_contacts = g_list_prepend (self->priv->tp_contacts,
243       g_object_ref (contact));
244
245   /* listen for updates to the capabilities */
246   tp_g_signal_connect_object (contact, "notify::capabilities",
247       G_CALLBACK (contact_capabilities_changed), self, 0);
248
249   /* Pass ownership to the list */
250   ctx->individuals = g_list_prepend (ctx->individuals, individual);
251
252   individual_store_add_individual_and_connect (self->priv->store, individual);
253
254   /* if nothing is selected, select the first matching node */
255   if (!gtk_tree_selection_get_selected (
256         gtk_tree_view_get_selection (GTK_TREE_VIEW (self->priv->view)),
257         NULL, NULL))
258     empathy_individual_view_select_first (self->priv->view);
259
260 out:
261   g_clear_object (&emp_contact);
262   g_clear_object (&self);
263   tp_weak_ref_destroy (wr);
264 }
265
266 static void
267 add_temporary_individuals (EmpathyContactChooser *self,
268     const gchar *id)
269 {
270   GList *accounts, *l;
271
272   tp_clear_pointer (&self->priv->add_temp_ctx,
273       add_temporary_individual_ctx_free);
274
275   if (tp_str_empty (id))
276     return;
277
278   self->priv->add_temp_ctx = add_temporary_individual_ctx_new (self);
279
280   /* Try to add an individual for each connected account */
281   accounts = tp_account_manager_dup_valid_accounts (self->priv->account_mgr);
282   for (l = accounts; l != NULL; l = g_list_next (l))
283     {
284       TpAccount *account = l->data;
285       TpConnection *conn;
286       EmpathyClientFactory *factory;
287
288       conn = tp_account_get_connection (account);
289       if (conn == NULL)
290         continue;
291
292       factory = empathy_client_factory_dup ();
293
294       empathy_client_factory_dup_contact_by_id_async (factory, conn, id,
295           get_contacts_cb,
296           tp_weak_ref_new (self, self->priv->add_temp_ctx, NULL));
297
298       g_object_unref (factory);
299     }
300
301   g_list_free_full (accounts, g_object_unref);
302 }
303
304 static void
305 search_text_changed (GtkEntry *entry,
306     EmpathyContactChooser *self)
307 {
308   const gchar *id;
309
310   tp_clear_pointer (&self->priv->search_words, g_ptr_array_unref);
311   tp_clear_pointer (&self->priv->search_str, g_free);
312
313   id = gtk_entry_get_text (entry);
314
315   self->priv->search_words = tpaw_live_search_strip_utf8_string (id);
316   self->priv->search_str = g_strdup (id);
317
318   add_temporary_individuals (self, id);
319
320   empathy_individual_view_refilter (self->priv->view);
321 }
322
323 static void
324 search_activate_cb (GtkEntry *entry,
325     EmpathyContactChooser *self)
326 {
327   g_signal_emit (self, signals[SIG_ACTIVATE], 0);
328 }
329
330 static void
331 view_activate_cb (GtkTreeView *view,
332     GtkTreePath *path,
333     GtkTreeViewColumn *column,
334     EmpathyContactChooser *self)
335 {
336   g_signal_emit (self, signals[SIG_ACTIVATE], 0);
337 }
338
339 static gboolean
340 search_key_press_cb (GtkEntry *entry,
341     GdkEventKey *event,
342     EmpathyContactChooser *self)
343 {
344   GtkTreeSelection *selection;
345   GtkTreeModel *model;
346   GtkTreeIter iter;
347
348   if (event->state != 0)
349     return FALSE;
350
351   switch (event->keyval)
352     {
353       case GDK_KEY_Down:
354       case GDK_KEY_KP_Down:
355       case GDK_KEY_Up:
356       case GDK_KEY_KP_Up:
357         break;
358
359       default:
360         return FALSE;
361     }
362
363   selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (self->priv->view));
364
365   if (!gtk_tree_selection_get_selected (selection, &model, &iter))
366     return TRUE;
367
368   switch (event->keyval)
369     {
370       case GDK_KEY_Down:
371       case GDK_KEY_KP_Down:
372         if (!gtk_tree_model_iter_next (model, &iter))
373           return TRUE;
374
375         break;
376
377       case GDK_KEY_Up:
378       case GDK_KEY_KP_Up:
379         if (!gtk_tree_model_iter_previous (model, &iter))
380           return TRUE;
381
382         break;
383
384       default:
385         g_assert_not_reached ();
386     }
387
388   gtk_tree_selection_select_iter (selection, &iter);
389
390   return TRUE;
391 }
392
393 static void
394 empathy_contact_chooser_init (EmpathyContactChooser *self)
395 {
396   EmpathyIndividualManager *mgr;
397   GtkTreeSelection *selection;
398   GQuark features[] = { TP_ACCOUNT_MANAGER_FEATURE_CORE, 0 };
399
400   self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, EMPATHY_TYPE_CONTACT_CHOOSER,
401       EmpathyContactChooserPrivate);
402
403   self->priv->account_mgr = tp_account_manager_dup ();
404
405   /* We don't wait for the CORE feature to be prepared, which is fine as we
406    * won't use the account manager until user starts searching. Furthermore,
407    * the AM has probably already been prepared by another Empathy
408    * component. */
409   tp_proxy_prepare_async (self->priv->account_mgr, features, NULL, NULL);
410
411   /* Search entry */
412   self->priv->search_entry = gtk_entry_new ();
413   gtk_box_pack_start (GTK_BOX (self), self->priv->search_entry, FALSE, TRUE, 6);
414   gtk_widget_show (self->priv->search_entry);
415
416   g_signal_connect (self->priv->search_entry, "changed",
417       G_CALLBACK (search_text_changed), self);
418   g_signal_connect (self->priv->search_entry, "activate",
419       G_CALLBACK (search_activate_cb), self);
420   g_signal_connect (self->priv->search_entry, "key-press-event",
421       G_CALLBACK (search_key_press_cb), self);
422
423   /* Add the treeview */
424   mgr = empathy_individual_manager_dup_singleton ();
425   self->priv->store = EMPATHY_INDIVIDUAL_STORE (
426       empathy_individual_store_manager_new (mgr));
427   g_object_unref (mgr);
428
429   empathy_individual_store_set_show_groups (self->priv->store, FALSE);
430
431   self->priv->view = empathy_individual_view_new (self->priv->store,
432       EMPATHY_INDIVIDUAL_VIEW_FEATURE_NONE, EMPATHY_INDIVIDUAL_FEATURE_NONE);
433
434   empathy_individual_view_set_custom_filter (self->priv->view,
435       filter_func, self);
436
437   selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (self->priv->view));
438
439   g_signal_connect (selection, "changed",
440       G_CALLBACK (view_selection_changed_cb), self);
441   g_signal_connect (self->priv->view, "row-activated",
442       G_CALLBACK (view_activate_cb), self);
443
444   self->priv->scroll_view = gtk_scrolled_window_new (NULL, NULL);
445
446   gtk_container_add (GTK_CONTAINER (self->priv->scroll_view),
447       GTK_WIDGET (self->priv->view));
448
449   gtk_box_pack_start (GTK_BOX (self), self->priv->scroll_view, TRUE, TRUE, 6);
450   gtk_widget_show (GTK_WIDGET (self->priv->view));
451   gtk_widget_show (self->priv->scroll_view);
452 }
453
454 GtkWidget *
455 empathy_contact_chooser_new (void)
456 {
457   return g_object_new (EMPATHY_TYPE_CONTACT_CHOOSER,
458       "orientation", GTK_ORIENTATION_VERTICAL,
459       NULL);
460 }
461
462 /* Note that because of bgo #666580 the returned invidivdual is valid until
463  * @self is destroyed. To keep it around after that, the TpContact associated
464  * with the individual needs to be manually reffed. */
465 FolksIndividual *
466 empathy_contact_chooser_dup_selected (EmpathyContactChooser *self)
467 {
468   return empathy_individual_view_dup_selected (self->priv->view);
469 }
470
471 void
472 empathy_contact_chooser_set_filter_func (EmpathyContactChooser *self,
473     EmpathyContactChooserFilterFunc func,
474     gpointer user_data)
475 {
476   g_assert (self->priv->filter_func == NULL);
477
478   self->priv->filter_func = func;
479   self->priv->filter_data = user_data;
480 }
481
482 void
483 empathy_contact_chooser_show_search_entry (EmpathyContactChooser *self,
484     gboolean show)
485 {
486   gtk_widget_set_visible (self->priv->search_entry, show);
487 }
488
489 void
490 empathy_contact_chooser_show_tree_view (EmpathyContactChooser *self,
491     gboolean show)
492 {
493   gtk_widget_set_visible (GTK_WIDGET (self->priv->scroll_view), show);
494 }