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