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