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