]> git.0d.be Git - empathy.git/blob - libempathy-gtk/empathy-individual-linker.c
factor out start_gnome_contacts()
[empathy.git] / libempathy-gtk / empathy-individual-linker.c
1 /*
2  * Copyright (C) 2010 Collabora Ltd.
3  *
4  * This program is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU General Public License as
6  * published by the Free Software Foundation; either version 2 of the
7  * License, or (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public
15  * License along with this program; if not, write to the
16  * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
17  * Boston, MA  02110-1301  USA
18  *
19  * Authors: Philip Withnall <philip.withnall@collabora.co.uk>
20  */
21
22 #include "config.h"
23
24 #include <string.h>
25
26 #include <glib/gi18n-lib.h>
27 #include <gtk/gtk.h>
28
29 #include <telepathy-glib/util.h>
30
31 #include <folks/folks.h>
32
33 #include <libempathy/empathy-individual-manager.h>
34 #include <libempathy/empathy-utils.h>
35
36 #include "empathy-individual-linker.h"
37 #include "empathy-individual-store.h"
38 #include "empathy-individual-store-manager.h"
39 #include "empathy-individual-view.h"
40 #include "empathy-individual-widget.h"
41 #include "empathy-persona-store.h"
42 #include "empathy-persona-view.h"
43
44 /**
45  * SECTION:empathy-individual-linker
46  * @title:EmpathyIndividualLinker
47  * @short_description: A widget used to link together #FolksIndividual<!-- -->s
48  * @include: libempathy-gtk/empathy-individual-linker.h
49  *
50  * #EmpathyIndividualLinker is a widget which allows selection of several
51  * #FolksIndividual<!-- -->s to link together to form a single new individual.
52  * The widget provides a preview of the linked individual.
53  */
54
55 /**
56  * EmpathyIndividualLinker:
57  * @parent: parent object
58  *
59  * Widget which extends #GtkBin to provide a list of #FolksIndividual<!-- -->s
60  * to link together.
61  */
62
63 #define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyIndividualLinker)
64
65 typedef struct {
66   EmpathyIndividualStore *individual_store; /* owned */
67   EmpathyIndividualView *individual_view; /* child widget */
68   GtkWidget *preview_widget; /* child widget */
69   EmpathyPersonaStore *persona_store; /* owned */
70   GtkTreeViewColumn *toggle_column; /* child widget */
71   GtkCellRenderer *toggle_renderer; /* child widget */
72   GtkWidget *search_widget; /* child widget */
73
74   FolksIndividual *start_individual; /* owned, allow-none */
75   FolksIndividual *new_individual; /* owned, allow-none */
76
77   /* Stores the Individuals whose Personas have been added to the
78    * new_individual */
79   /* unowned Individual (borrowed from EmpathyIndividualStore) -> bool */
80   GHashTable *changed_individuals;
81 } EmpathyIndividualLinkerPriv;
82
83 enum {
84   PROP_START_INDIVIDUAL = 1,
85   PROP_HAS_CHANGED,
86 };
87
88 G_DEFINE_TYPE (EmpathyIndividualLinker, empathy_individual_linker,
89     GTK_TYPE_BOX);
90
91 static void
92 contact_toggle_cell_data_func (GtkTreeViewColumn *tree_column,
93     GtkCellRenderer *cell,
94     GtkTreeModel *tree_model,
95     GtkTreeIter *iter,
96     EmpathyIndividualLinker *self)
97 {
98   EmpathyIndividualLinkerPriv *priv;
99   FolksIndividual *individual;
100   gboolean is_group, individual_added;
101
102   priv = GET_PRIV (self);
103
104   gtk_tree_model_get (tree_model, iter,
105       EMPATHY_INDIVIDUAL_STORE_COL_IS_GROUP, &is_group,
106       EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual,
107       -1);
108
109   individual_added = GPOINTER_TO_UINT (g_hash_table_lookup (
110       priv->changed_individuals, individual));
111
112   /* We don't want to show checkboxes next to the group rows.
113    * All checkboxes should be sensitive except the checkbox for the start
114    * individual, which should be permanently active and insensitive */
115   g_object_set (cell,
116       "visible", !is_group,
117       "sensitive", individual != priv->start_individual,
118       "activatable", individual != priv->start_individual,
119       "active", individual_added || individual == priv->start_individual,
120       NULL);
121
122   tp_clear_object (&individual);
123 }
124
125 static void
126 update_toggle_renderers (EmpathyIndividualLinker *self)
127 {
128   EmpathyIndividualLinkerPriv *priv = GET_PRIV (self);
129
130   /* Re-setting the cell data func to the same function causes a refresh of the
131    * entire column, ensuring that each toggle button is correctly active or
132    * inactive. This is necessary because one Individual might appear multiple
133    * times in the list (in different groups), so toggling one instance of the
134    * Individual should toggle all of them. */
135   gtk_tree_view_column_set_cell_data_func (priv->toggle_column,
136       priv->toggle_renderer,
137       (GtkTreeCellDataFunc) contact_toggle_cell_data_func, self, NULL);
138 }
139
140 static void
141 link_individual (EmpathyIndividualLinker *self,
142     FolksIndividual *individual)
143 {
144   EmpathyIndividualLinkerPriv *priv = GET_PRIV (self);
145   GeeSet *old_personas, *new_personas;
146   GeeHashSet *final_personas;
147   gboolean personas_changed;
148
149   /* Add the individual to the link */
150   g_hash_table_insert (priv->changed_individuals, individual,
151       GUINT_TO_POINTER (TRUE));
152
153   /* Add personas which are in @individual to priv->new_individual, adding them
154    * to the set of personas. */
155   old_personas = folks_individual_get_personas (individual);
156   new_personas = folks_individual_get_personas (priv->new_individual);
157   final_personas = gee_hash_set_new (FOLKS_TYPE_PERSONA, g_object_ref,
158       g_object_unref, g_direct_hash, g_direct_equal);
159   gee_collection_add_all (GEE_COLLECTION (final_personas),
160       GEE_COLLECTION (old_personas));
161   personas_changed = gee_collection_add_all (GEE_COLLECTION (final_personas),
162       GEE_COLLECTION (new_personas));
163
164   /* avoid updating all values in the Individual if the set of personas doesn't
165    * actually change */
166   if (personas_changed)
167     {
168       folks_individual_set_personas (priv->new_individual,
169           GEE_SET (final_personas));
170     }
171
172   g_clear_object (&final_personas);
173
174   /* Update the toggle renderers, so that if this Individual is listed in
175    * another group in the EmpathyIndividualView, the toggle button for that
176    * group is updated. */
177   update_toggle_renderers (self);
178
179   g_object_notify (G_OBJECT (self), "has-changed");
180 }
181
182 static void
183 unlink_individual (EmpathyIndividualLinker *self,
184     FolksIndividual *individual)
185 {
186   EmpathyIndividualLinkerPriv *priv = GET_PRIV (self);
187   GeeSet *removed_personas, *old_personas;
188   GeeHashSet *final_personas;
189   gboolean personas_changed;
190
191   /* Remove the individual from the link */
192   g_hash_table_remove (priv->changed_individuals, individual);
193
194   /* Remove personas which are in @individual from priv->new_individual. */
195   old_personas = folks_individual_get_personas (priv->new_individual);
196   removed_personas = folks_individual_get_personas (individual);
197
198   final_personas = gee_hash_set_new (FOLKS_TYPE_PERSONA, g_object_ref,
199       g_object_unref, g_direct_hash, g_direct_equal);
200   gee_collection_add_all (GEE_COLLECTION (final_personas),
201       GEE_COLLECTION (old_personas));
202   personas_changed = gee_collection_remove_all (GEE_COLLECTION (final_personas),
203       GEE_COLLECTION (removed_personas));
204
205   if (personas_changed)
206     {
207       folks_individual_set_personas (priv->new_individual,
208           GEE_SET (final_personas));
209     }
210
211   g_clear_object (&final_personas);
212
213   /* Update the toggle renderers, so that if this Individual is listed in
214    * another group in the EmpathyIndividualView, the toggle button for that
215    * group is updated. */
216   update_toggle_renderers (self);
217
218   g_object_notify (G_OBJECT (self), "has-changed");
219 }
220
221 static void
222 toggle_individual_row (EmpathyIndividualLinker *self,
223     GtkTreePath *path)
224 {
225   EmpathyIndividualLinkerPriv *priv = GET_PRIV (self);
226   FolksIndividual *individual;
227   GtkTreeIter iter;
228   GtkTreeModel *tree_model;
229   gboolean individual_added;
230
231   tree_model = gtk_tree_view_get_model (GTK_TREE_VIEW (priv->individual_view));
232
233   gtk_tree_model_get_iter (tree_model, &iter, path);
234   gtk_tree_model_get (tree_model, &iter,
235       EMPATHY_INDIVIDUAL_STORE_COL_INDIVIDUAL, &individual,
236       -1);
237
238   if (individual == NULL)
239     return;
240
241   individual_added = GPOINTER_TO_UINT (g_hash_table_lookup (
242       priv->changed_individuals, individual));
243
244   /* Toggle the Individual's linked status */
245   if (individual_added)
246     unlink_individual (self, individual);
247   else
248     link_individual (self, individual);
249
250   g_object_unref (individual);
251 }
252
253 static void
254 row_activated_cb (EmpathyIndividualView *view,
255     GtkTreePath *path,
256     GtkTreeViewColumn *column,
257     EmpathyIndividualLinker *self)
258 {
259   toggle_individual_row (self, path);
260 }
261
262 static void
263 row_toggled_cb (GtkCellRendererToggle *cell_renderer,
264     const gchar *path,
265     EmpathyIndividualLinker *self)
266 {
267   GtkTreePath *tree_path = gtk_tree_path_new_from_string (path);
268   toggle_individual_row (self, tree_path);
269   gtk_tree_path_free (tree_path);
270 }
271
272 static gboolean
273 individual_view_drag_motion_cb (GtkWidget *widget,
274     GdkDragContext *context,
275     gint x,
276     gint y,
277     guint time_)
278 {
279   EmpathyIndividualView *view = EMPATHY_INDIVIDUAL_VIEW (widget);
280   GdkAtom target;
281
282   target = gtk_drag_dest_find_target (GTK_WIDGET (view), context, NULL);
283
284   if (target == gdk_atom_intern_static_string ("text/x-persona-id"))
285     {
286       GtkTreePath *path;
287
288       /* FIXME: It doesn't make sense for us to highlight a specific row or
289        * position to drop a Persona in, so just highlight the entire widget.
290        * Since I can't find a way to do this, just highlight the first possible
291        * position in the tree. */
292       gdk_drag_status (context, gdk_drag_context_get_suggested_action (context),
293           time_);
294
295       path = gtk_tree_path_new_first ();
296       gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (view), path,
297           GTK_TREE_VIEW_DROP_BEFORE);
298       gtk_tree_path_free (path);
299
300       return TRUE;
301     }
302
303   /* Unknown or unhandled drag target */
304   gdk_drag_status (context, GDK_ACTION_DEFAULT, time_);
305   gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (view), NULL, 0);
306
307   return FALSE;
308 }
309
310 static gboolean
311 individual_view_drag_persona_received_cb (EmpathyIndividualView *view,
312     GdkDragAction action,
313     FolksPersona *persona,
314     FolksIndividual *individual,
315     EmpathyIndividualLinker *self)
316 {
317   EmpathyIndividualLinkerPriv *priv = GET_PRIV (self);
318
319   /* A Persona has been dragged onto the EmpathyIndividualView (from the
320    * EmpathyPersonaView), so we try to remove the Individual which contains
321    * the Persona from the link. */
322   if (individual != priv->start_individual)
323     {
324       unlink_individual (self, individual);
325       return TRUE;
326     }
327
328   return FALSE;
329 }
330
331 static gboolean
332 persona_view_drag_individual_received_cb (EmpathyPersonaView *view,
333     GdkDragAction action,
334     FolksIndividual *individual,
335     EmpathyIndividualLinker *self)
336 {
337   /* An Individual has been dragged onto the EmpathyPersonaView (from the
338    * EmpathyIndividualView), so we try to add the Individual to the link. */
339   link_individual (self, individual);
340
341   return TRUE;
342 }
343
344 static void
345 set_up (EmpathyIndividualLinker *self)
346 {
347   EmpathyIndividualLinkerPriv *priv;
348   EmpathyIndividualManager *individual_manager;
349   GtkWidget *top_vbox;
350   GtkPaned *paned;
351   GtkWidget *label, *scrolled_window;
352   GtkBox *vbox;
353   EmpathyPersonaView *persona_view;
354   gchar *tmp;
355   GtkWidget *alignment;
356
357   priv = GET_PRIV (self);
358
359   top_vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
360
361   /* Layout panes */
362
363   paned = GTK_PANED (gtk_paned_new (GTK_ORIENTATION_HORIZONTAL));
364
365   /* Left column heading */
366   alignment = gtk_alignment_new (0.5, 0.5, 1, 1);
367   gtk_alignment_set_padding (GTK_ALIGNMENT (alignment), 0, 0, 0, 6);
368   gtk_widget_show (alignment);
369
370   vbox = GTK_BOX (gtk_box_new (GTK_ORIENTATION_VERTICAL, 6));
371   label = gtk_label_new (NULL);
372   tmp = g_strdup_printf ("<b>%s</b>", _("Select contacts to link"));
373   gtk_label_set_markup (GTK_LABEL (label), tmp);
374   g_free (tmp);
375   gtk_box_pack_start (vbox, label, FALSE, TRUE, 0);
376   gtk_widget_show (label);
377
378   /* Individual selector */
379   individual_manager = empathy_individual_manager_dup_singleton ();
380   priv->individual_store = EMPATHY_INDIVIDUAL_STORE (
381       empathy_individual_store_manager_new (individual_manager));
382   g_object_unref (individual_manager);
383
384   empathy_individual_store_set_show_protocols (priv->individual_store, FALSE);
385
386   priv->individual_view = empathy_individual_view_new (priv->individual_store,
387       EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_DRAG |
388       EMPATHY_INDIVIDUAL_VIEW_FEATURE_INDIVIDUAL_DROP |
389       EMPATHY_INDIVIDUAL_VIEW_FEATURE_PERSONA_DROP,
390       EMPATHY_INDIVIDUAL_FEATURE_NONE);
391   empathy_individual_view_set_show_offline (priv->individual_view, TRUE);
392   empathy_individual_view_set_show_untrusted (priv->individual_view, FALSE);
393
394   g_signal_connect (priv->individual_view, "row-activated",
395       (GCallback) row_activated_cb, self);
396   g_signal_connect (priv->individual_view, "drag-motion",
397       (GCallback) individual_view_drag_motion_cb, self);
398   g_signal_connect (priv->individual_view, "drag-persona-received",
399       (GCallback) individual_view_drag_persona_received_cb, self);
400
401   /* Add a checkbox column to the selector */
402   priv->toggle_renderer = gtk_cell_renderer_toggle_new ();
403   g_signal_connect (priv->toggle_renderer, "toggled",
404       (GCallback) row_toggled_cb, self);
405
406   priv->toggle_column = gtk_tree_view_column_new ();
407   gtk_tree_view_column_pack_start (priv->toggle_column, priv->toggle_renderer,
408       FALSE);
409   gtk_tree_view_column_set_cell_data_func (priv->toggle_column,
410       priv->toggle_renderer,
411       (GtkTreeCellDataFunc) contact_toggle_cell_data_func, self, NULL);
412
413   gtk_tree_view_insert_column (GTK_TREE_VIEW (priv->individual_view),
414       priv->toggle_column, 0);
415
416   scrolled_window = gtk_scrolled_window_new (NULL, NULL);
417   gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
418       GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
419   gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scrolled_window),
420       GTK_SHADOW_IN);
421   gtk_container_add (GTK_CONTAINER (scrolled_window),
422       GTK_WIDGET (priv->individual_view));
423   gtk_widget_show (GTK_WIDGET (priv->individual_view));
424
425   gtk_box_pack_start (vbox, scrolled_window, TRUE, TRUE, 0);
426   gtk_widget_show (scrolled_window);
427
428   /* Live search */
429   priv->search_widget = empathy_live_search_new (
430       GTK_WIDGET (priv->individual_view));
431   empathy_individual_view_set_live_search (priv->individual_view,
432       EMPATHY_LIVE_SEARCH (priv->search_widget));
433
434   gtk_box_pack_end (vbox, priv->search_widget, FALSE, TRUE, 0);
435
436   gtk_container_add (GTK_CONTAINER (alignment), GTK_WIDGET (vbox));
437   gtk_paned_pack1 (paned, alignment, TRUE, FALSE);
438   gtk_widget_show (GTK_WIDGET (vbox));
439
440   /* Right column heading */
441   alignment = gtk_alignment_new (0.5, 0.5, 1, 1);
442   gtk_alignment_set_padding (GTK_ALIGNMENT (alignment), 0, 0, 6, 0);
443   gtk_widget_show (alignment);
444
445   vbox = GTK_BOX (gtk_box_new (GTK_ORIENTATION_VERTICAL, 6));
446   label = gtk_label_new (NULL);
447   tmp = g_strdup_printf ("<b>%s</b>", _("New contact preview"));
448   gtk_label_set_markup (GTK_LABEL (label), tmp);
449   g_free (tmp);
450   gtk_box_pack_start (vbox, label, FALSE, TRUE, 0);
451   gtk_widget_show (label);
452
453   /* New individual preview */
454   priv->preview_widget = empathy_individual_widget_new (priv->new_individual,
455       EMPATHY_INDIVIDUAL_WIDGET_SHOW_DETAILS);
456   gtk_box_pack_start (vbox, priv->preview_widget, FALSE, TRUE, 0);
457   gtk_widget_show (priv->preview_widget);
458
459   /* Persona list */
460   scrolled_window = gtk_scrolled_window_new (NULL, NULL);
461   gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
462       GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
463   gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scrolled_window),
464       GTK_SHADOW_IN);
465
466   priv->persona_store = empathy_persona_store_new (priv->new_individual);
467   empathy_persona_store_set_show_protocols (priv->persona_store, TRUE);
468   persona_view = empathy_persona_view_new (priv->persona_store,
469       EMPATHY_PERSONA_VIEW_FEATURE_ALL);
470   empathy_persona_view_set_show_offline (persona_view, TRUE);
471
472   g_signal_connect (persona_view, "drag-individual-received",
473       (GCallback) persona_view_drag_individual_received_cb, self);
474
475   gtk_container_add (GTK_CONTAINER (scrolled_window),
476       GTK_WIDGET (persona_view));
477   gtk_widget_show (GTK_WIDGET (persona_view));
478
479   gtk_box_pack_start (vbox, scrolled_window, TRUE, TRUE, 0);
480   gtk_widget_show (scrolled_window);
481
482   gtk_container_add (GTK_CONTAINER (alignment), GTK_WIDGET (vbox));
483   gtk_paned_pack2 (paned, alignment, TRUE, FALSE);
484   gtk_widget_show (GTK_WIDGET (vbox));
485
486   gtk_widget_show (GTK_WIDGET (paned));
487
488   /* Footer label */
489   label = gtk_label_new (NULL);
490   tmp = g_strdup_printf ("<i>%s</i>",
491       _("Contacts selected in the list on the left will be linked together."));
492   gtk_label_set_markup (GTK_LABEL (label), tmp);
493   g_free (tmp);
494   gtk_widget_show (label);
495
496   gtk_box_pack_start (GTK_BOX (top_vbox), GTK_WIDGET (paned), TRUE, TRUE, 0);
497   gtk_box_pack_start (GTK_BOX (top_vbox), label, FALSE, TRUE, 0);
498
499   /* Add the main vbox to the bin */
500   gtk_box_pack_start (GTK_BOX (self), GTK_WIDGET (top_vbox), TRUE, TRUE, 0);
501   gtk_widget_show (GTK_WIDGET (top_vbox));
502 }
503
504 static void
505 empathy_individual_linker_init (EmpathyIndividualLinker *self)
506 {
507   EmpathyIndividualLinkerPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
508       EMPATHY_TYPE_INDIVIDUAL_LINKER, EmpathyIndividualLinkerPriv);
509
510   self->priv = priv;
511
512   priv->changed_individuals = g_hash_table_new (NULL, NULL);
513
514   set_up (self);
515 }
516
517 static void
518 get_property (GObject *object,
519     guint param_id,
520     GValue *value,
521     GParamSpec *pspec)
522 {
523   EmpathyIndividualLinkerPriv *priv;
524
525   priv = GET_PRIV (object);
526
527   switch (param_id)
528     {
529       case PROP_START_INDIVIDUAL:
530         g_value_set_object (value, priv->start_individual);
531         break;
532       case PROP_HAS_CHANGED:
533         g_value_set_boolean (value, empathy_individual_linker_get_has_changed (
534             EMPATHY_INDIVIDUAL_LINKER (object)));
535         break;
536       default:
537         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
538         break;
539     }
540 }
541
542 static void
543 set_property (GObject *object,
544     guint param_id,
545     const GValue *value,
546     GParamSpec *pspec)
547 {
548   switch (param_id)
549     {
550       case PROP_START_INDIVIDUAL:
551         empathy_individual_linker_set_start_individual (
552             EMPATHY_INDIVIDUAL_LINKER (object), g_value_get_object (value));
553         break;
554       default:
555         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
556         break;
557     }
558 }
559
560 static void
561 dispose (GObject *object)
562 {
563   EmpathyIndividualLinkerPriv *priv = GET_PRIV (object);
564
565   tp_clear_object (&priv->individual_store);
566   tp_clear_object (&priv->persona_store);
567   tp_clear_object (&priv->start_individual);
568   tp_clear_object (&priv->new_individual);
569
570   G_OBJECT_CLASS (empathy_individual_linker_parent_class)->dispose (object);
571 }
572
573 static void
574 finalize (GObject *object)
575 {
576   EmpathyIndividualLinkerPriv *priv = GET_PRIV (object);
577
578   g_hash_table_unref (priv->changed_individuals);
579
580   G_OBJECT_CLASS (empathy_individual_linker_parent_class)->finalize (object);
581 }
582
583 static void
584 empathy_individual_linker_class_init (EmpathyIndividualLinkerClass *klass)
585 {
586   GObjectClass *object_class = G_OBJECT_CLASS (klass);
587
588   object_class->get_property = get_property;
589   object_class->set_property = set_property;
590   object_class->dispose = dispose;
591   object_class->finalize = finalize;
592
593   /**
594    * EmpathyIndividualLinker:start-individual:
595    *
596    * The #FolksIndividual to link other individuals to. This individual is
597    * selected by default in the list of individuals, and cannot be unselected.
598    * This ensures that empathy_individual_linker_get_linked_personas() will
599    * always return at least one persona to link.
600    */
601   g_object_class_install_property (object_class, PROP_START_INDIVIDUAL,
602       g_param_spec_object ("start-individual",
603           "Start Individual",
604           "The #FolksIndividual to link other individuals to.",
605           FOLKS_TYPE_INDIVIDUAL,
606           G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
607
608   /**
609    * EmpathyIndividualLinker:has-changed:
610    *
611    * Whether #FolksIndividual<!-- -->s have been added to or removed from
612    * the linked individual currently displayed in the widget.
613    *
614    * This will be %FALSE after the widget is initialised, and set to %TRUE when
615    * an individual is checked in the individual view on the left of the widget.
616    * If the individual is later unchecked, this will be reset to %FALSE, etc.
617    */
618   g_object_class_install_property (object_class, PROP_HAS_CHANGED,
619       g_param_spec_boolean ("has-changed",
620           "Changed?",
621           "Whether individuals have been added to or removed from the linked "
622           "individual currently displayed in the widget.",
623           FALSE,
624           G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
625
626   g_type_class_add_private (object_class, sizeof (EmpathyIndividualLinkerPriv));
627 }
628
629 /**
630  * empathy_individual_linker_new:
631  * @start_individual: (allow-none): the #FolksIndividual to link to, or %NULL
632  *
633  * Creates a new #EmpathyIndividualLinker.
634  *
635  * Return value: a new #EmpathyIndividualLinker
636  */
637 GtkWidget *
638 empathy_individual_linker_new (FolksIndividual *start_individual)
639 {
640   g_return_val_if_fail (start_individual == NULL ||
641       FOLKS_IS_INDIVIDUAL (start_individual), NULL);
642
643   return g_object_new (EMPATHY_TYPE_INDIVIDUAL_LINKER,
644       "start-individual", start_individual,
645       NULL);
646 }
647
648 /**
649  * empathy_individual_linker_get_start_individual:
650  * @self: an #EmpathyIndividualLinker
651  *
652  * Get the value of #EmpathyIndividualLinker:start-individual.
653  *
654  * Return value: (transfer none): the start individual for linking, or %NULL
655  */
656 FolksIndividual *
657 empathy_individual_linker_get_start_individual (EmpathyIndividualLinker *self)
658 {
659   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_LINKER (self), NULL);
660
661   return GET_PRIV (self)->start_individual;
662 }
663
664 /**
665  * empathy_individual_linker_set_start_individual:
666  * @self: an #EmpathyIndividualLinker
667  * @individual: (allow-none): the start individual, or %NULL
668  *
669  * Set the value of #EmpathyIndividualLinker:start-individual to @individual.
670  */
671 void
672 empathy_individual_linker_set_start_individual (EmpathyIndividualLinker *self,
673     FolksIndividual *individual)
674 {
675   EmpathyIndividualLinkerPriv *priv;
676
677   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_LINKER (self));
678   g_return_if_fail (individual == NULL || FOLKS_IS_INDIVIDUAL (individual));
679
680   priv = GET_PRIV (self);
681
682   tp_clear_object (&priv->start_individual);
683   tp_clear_object (&priv->new_individual);
684   g_hash_table_remove_all (priv->changed_individuals);
685
686   if (individual != NULL)
687     {
688       priv->start_individual = g_object_ref (individual);
689       priv->new_individual = folks_individual_new (
690           folks_individual_get_personas (individual));
691       empathy_individual_view_set_store (priv->individual_view,
692           priv->individual_store);
693     }
694   else
695     {
696       priv->start_individual = NULL;
697       priv->new_individual = NULL;
698
699       /* We only display Individuals in the individual view if we have a
700        * new_individual to link them into */
701       empathy_individual_view_set_store (priv->individual_view, NULL);
702     }
703
704   empathy_individual_widget_set_individual (
705       EMPATHY_INDIVIDUAL_WIDGET (priv->preview_widget), priv->new_individual);
706   empathy_persona_store_set_individual (priv->persona_store,
707       priv->new_individual);
708
709   g_object_freeze_notify (G_OBJECT (self));
710   g_object_notify (G_OBJECT (self), "start-individual");
711   g_object_notify (G_OBJECT (self), "has-changed");
712   g_object_thaw_notify (G_OBJECT (self));
713 }
714
715 /**
716  * empathy_individual_linker_get_linked_personas:
717  * @self: an #EmpathyIndividualLinker
718  *
719  * Return a list of the #FolksPersona<!-- -->s which comprise the linked
720  * individual currently displayed in the widget.
721  *
722  * The return value is guaranteed to contain at least one element.
723  *
724  * Return value: (transfer none) (element-type Folks.Persona): a set of
725  * #FolksPersona<!-- -->s to link together
726  */
727 GeeSet *
728 empathy_individual_linker_get_linked_personas (EmpathyIndividualLinker *self)
729 {
730   EmpathyIndividualLinkerPriv *priv;
731   GeeSet *personas;
732
733   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_LINKER (self), NULL);
734
735   priv = GET_PRIV (self);
736
737   if (priv->new_individual == NULL)
738     return NULL;
739
740   personas = folks_individual_get_personas (priv->new_individual);
741   g_assert (personas != NULL);
742   return personas;
743 }
744
745 /**
746  * empathy_individual_linker_get_has_changed:
747  * @self: an #EmpathyIndividualLinker
748  *
749  * Return whether #FolksIndividual<!-- -->s have been added to or removed from
750  * the linked individual currently displayed in the widget.
751  *
752  * This will be %FALSE after the widget is initialised, and set to %TRUE when
753  * an individual is checked in the individual view on the left of the widget.
754  * If the individual is later unchecked, this will be reset to %FALSE, etc.
755  *
756  * Return value: %TRUE if the linked individual has been changed, %FALSE
757  * otherwise
758  */
759 gboolean
760 empathy_individual_linker_get_has_changed (EmpathyIndividualLinker *self)
761 {
762   EmpathyIndividualLinkerPriv *priv;
763
764   g_return_val_if_fail (EMPATHY_IS_INDIVIDUAL_LINKER (self), FALSE);
765
766   priv = GET_PRIV (self);
767
768   return (g_hash_table_size (priv->changed_individuals) > 0) ? TRUE : FALSE;
769 }
770
771 void
772 empathy_individual_linker_set_search_text (EmpathyIndividualLinker *self,
773     const gchar *search_text)
774 {
775   g_return_if_fail (EMPATHY_IS_INDIVIDUAL_LINKER (self));
776
777   empathy_live_search_set_text (
778       EMPATHY_LIVE_SEARCH (GET_PRIV (self)->search_widget), search_text);
779 }