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