2 * Copyright (C) 2005-2007 Imendio AB
3 * Copyright (C) 2007-2008, 2010 Collabora Ltd.
5 * This program is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU General Public License as
7 * published by the Free Software Foundation; either version 2 of the
8 * License, or (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * General Public License for more details.
15 * You should have received a copy of the GNU General Public
16 * License along with this program; if not, write to the
17 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
18 * Boston, MA 02110-1301 USA
20 * Authors: Mikael Hallendal <micke@imendio.com>
21 * Martyn Russell <martyn@imendio.com>
22 * Xavier Claessens <xclaesse@gmail.com>
23 * Philip Withnall <philip.withnall@collabora.co.uk>
25 * Based off EmpathyContactListView.
32 #include <glib/gi18n-lib.h>
35 #include <telepathy-glib/util.h>
37 #include <folks/folks.h>
38 #include <folks/folks-telepathy.h>
40 #include <libempathy/empathy-individual-manager.h>
41 #include <libempathy/empathy-utils.h>
43 #include "empathy-persona-view.h"
44 #include "empathy-contact-widget.h"
45 #include "empathy-images.h"
46 #include "empathy-cell-renderer-text.h"
47 #include "empathy-cell-renderer-activatable.h"
48 #include "empathy-gtk-enum-types.h"
49 #include "empathy-gtk-marshal.h"
51 #define DEBUG_FLAG EMPATHY_DEBUG_CONTACT
52 #include <libempathy/empathy-debug.h>
55 * SECTION:empathy-persona-view
56 * @title: EmpathyPersonaView
57 * @short_description: A tree view which displays personas from an individual
58 * @include: libempathy-gtk/empathy-persona-view.h
60 * #EmpathyPersonaView is a tree view widget which displays the personas from
61 * a given #EmpathyPersonaStore.
63 * It supports hiding offline personas and highlighting active personas. Active
64 * personas are those which have recently changed state (e.g. online, offline or
65 * from normal to a busy state).
68 #define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyPersonaView)
72 GtkTreeModelFilter *filter;
73 GtkWidget *tooltip_widget;
74 gboolean show_offline;
75 EmpathyPersonaViewFeatureFlags features;
76 } EmpathyPersonaViewPriv;
88 DND_DRAG_TYPE_INDIVIDUAL_ID,
89 DND_DRAG_TYPE_PERSONA_ID,
93 #define DRAG_TYPE(T,I) \
96 static const GtkTargetEntry drag_types_dest[] = {
97 DRAG_TYPE ("text/individual-id", DND_DRAG_TYPE_INDIVIDUAL_ID),
98 DRAG_TYPE ("text/plain", DND_DRAG_TYPE_STRING),
99 DRAG_TYPE ("STRING", DND_DRAG_TYPE_STRING),
102 static const GtkTargetEntry drag_types_source[] = {
103 DRAG_TYPE ("text/persona-id", DND_DRAG_TYPE_PERSONA_ID),
108 static GdkAtom drag_atoms_dest[G_N_ELEMENTS (drag_types_dest)];
109 static GdkAtom drag_atoms_source[G_N_ELEMENTS (drag_types_source)];
113 DRAG_INDIVIDUAL_RECEIVED,
117 static guint signals[LAST_SIGNAL];
119 G_DEFINE_TYPE (EmpathyPersonaView, empathy_persona_view, GTK_TYPE_TREE_VIEW);
122 filter_visible_func (GtkTreeModel *model,
124 EmpathyPersonaView *self)
126 EmpathyPersonaViewPriv *priv = GET_PRIV (self);
129 gtk_tree_model_get (model, iter,
130 EMPATHY_PERSONA_STORE_COL_IS_ONLINE, &is_online,
133 return (priv->show_offline || is_online);
137 set_model (EmpathyPersonaView *self,
140 EmpathyPersonaViewPriv *priv = GET_PRIV (self);
142 tp_clear_object (&priv->filter);
144 priv->filter = GTK_TREE_MODEL_FILTER (gtk_tree_model_filter_new (model,
146 gtk_tree_model_filter_set_visible_func (priv->filter,
147 (GtkTreeModelFilterVisibleFunc) filter_visible_func, self, NULL);
149 gtk_tree_view_set_model (GTK_TREE_VIEW (self), GTK_TREE_MODEL (priv->filter));
153 tooltip_destroy_cb (GtkWidget *widget,
154 EmpathyPersonaView *self)
156 EmpathyPersonaViewPriv *priv = GET_PRIV (self);
158 if (priv->tooltip_widget)
160 DEBUG ("Tooltip destroyed");
161 g_object_unref (priv->tooltip_widget);
162 priv->tooltip_widget = NULL;
167 query_tooltip_cb (EmpathyPersonaView *self,
170 gboolean keyboard_mode,
174 EmpathyPersonaViewPriv *priv = GET_PRIV (self);
175 FolksPersona *persona;
176 EmpathyContact *contact;
180 static gint running = 0;
181 gboolean ret = FALSE;
183 /* Avoid an infinite loop. See GNOME bug #574377 */
188 if (!gtk_tree_view_get_tooltip_context (GTK_TREE_VIEW (self), &x, &y,
189 keyboard_mode, &model, &path, &iter))
194 gtk_tree_view_set_tooltip_row (GTK_TREE_VIEW (self), tooltip, path);
195 gtk_tree_path_free (path);
197 gtk_tree_model_get (model, &iter,
198 EMPATHY_PERSONA_STORE_COL_PERSONA, &persona,
203 contact = empathy_contact_dup_from_tp_contact (tpf_persona_get_contact (
204 TPF_PERSONA (persona)));
206 if (priv->tooltip_widget == NULL)
208 priv->tooltip_widget = empathy_contact_widget_new (contact,
209 EMPATHY_CONTACT_WIDGET_FOR_TOOLTIP |
210 EMPATHY_CONTACT_WIDGET_SHOW_LOCATION);
211 gtk_container_set_border_width (GTK_CONTAINER (priv->tooltip_widget), 8);
212 g_object_ref (priv->tooltip_widget);
213 g_signal_connect (priv->tooltip_widget, "destroy",
214 (GCallback) tooltip_destroy_cb, self);
215 gtk_widget_show (priv->tooltip_widget);
219 empathy_contact_widget_set_contact (priv->tooltip_widget, contact);
222 gtk_tooltip_set_custom (tooltip, priv->tooltip_widget);
225 g_object_unref (contact);
226 g_object_unref (persona);
235 cell_set_background (EmpathyPersonaView *self,
236 GtkCellRenderer *cell,
242 style = gtk_widget_get_style (GTK_WIDGET (self));
246 color = style->bg[GTK_STATE_SELECTED];
248 /* Here we take the current theme colour and add it to
249 * the colour for white and average the two. This
250 * gives a colour which is inline with the theme but
253 color.red = (color.red + (style->white).red) / 2;
254 color.green = (color.green + (style->white).green) / 2;
255 color.blue = (color.blue + (style->white).blue) / 2;
257 g_object_set (cell, "cell-background-gdk", &color, NULL);
261 g_object_set (cell, "cell-background-gdk", NULL, NULL);
266 pixbuf_cell_data_func (GtkTreeViewColumn *tree_column,
267 GtkCellRenderer *cell,
270 EmpathyPersonaView *self)
275 gtk_tree_model_get (model, iter,
276 EMPATHY_PERSONA_STORE_COL_IS_ACTIVE, &is_active,
277 EMPATHY_PERSONA_STORE_COL_ICON_STATUS, &pixbuf,
280 g_object_set (cell, "pixbuf", pixbuf, NULL);
281 tp_clear_object (&pixbuf);
283 cell_set_background (self, cell, is_active);
287 audio_call_cell_data_func (GtkTreeViewColumn *tree_column,
288 GtkCellRenderer *cell,
291 EmpathyPersonaView *self)
294 gboolean can_audio, can_video;
296 gtk_tree_model_get (model, iter,
297 EMPATHY_PERSONA_STORE_COL_IS_ACTIVE, &is_active,
298 EMPATHY_PERSONA_STORE_COL_CAN_AUDIO_CALL, &can_audio,
299 EMPATHY_PERSONA_STORE_COL_CAN_VIDEO_CALL, &can_video,
303 "visible", (can_audio || can_video),
304 "icon-name", can_video? EMPATHY_IMAGE_VIDEO_CALL : EMPATHY_IMAGE_VOIP,
307 cell_set_background (self, cell, is_active);
311 avatar_cell_data_func (GtkTreeViewColumn *tree_column,
312 GtkCellRenderer *cell,
315 EmpathyPersonaView *self)
318 gboolean show_avatar, is_active;
320 gtk_tree_model_get (model, iter,
321 EMPATHY_PERSONA_STORE_COL_PIXBUF_AVATAR, &pixbuf,
322 EMPATHY_PERSONA_STORE_COL_PIXBUF_AVATAR_VISIBLE, &show_avatar,
323 EMPATHY_PERSONA_STORE_COL_IS_ACTIVE, &is_active,
327 "visible", show_avatar,
331 tp_clear_object (&pixbuf);
333 cell_set_background (self, cell, is_active);
337 text_cell_data_func (GtkTreeViewColumn *tree_column,
338 GtkCellRenderer *cell,
341 EmpathyPersonaView *self)
345 gtk_tree_model_get (model, iter,
346 EMPATHY_PERSONA_STORE_COL_IS_ACTIVE, &is_active,
349 cell_set_background (self, cell, is_active);
353 individual_drag_received (EmpathyPersonaView *self,
354 GdkDragContext *context,
355 GtkSelectionData *selection)
357 EmpathyPersonaViewPriv *priv;
358 EmpathyIndividualManager *manager = NULL;
359 FolksIndividual *individual;
360 const gchar *individual_id;
361 gboolean success = FALSE;
363 priv = GET_PRIV (self);
365 individual_id = (const gchar *) gtk_selection_data_get_data (selection);
366 manager = empathy_individual_manager_dup_singleton ();
367 individual = empathy_individual_manager_lookup_member (manager,
370 if (individual == NULL)
372 DEBUG ("Failed to find drag event individual with ID '%s'",
374 g_object_unref (manager);
378 /* Emit a signal notifying of the drag. */
379 g_signal_emit (self, signals[DRAG_INDIVIDUAL_RECEIVED], 0,
380 gdk_drag_context_get_selected_action (context), individual, &success);
382 g_object_unref (manager);
388 drag_data_received (GtkWidget *widget,
389 GdkDragContext *context,
392 GtkSelectionData *selection,
396 EmpathyPersonaView *self = EMPATHY_PERSONA_VIEW (widget);
397 gboolean success = TRUE;
399 if (info == DND_DRAG_TYPE_INDIVIDUAL_ID || info == DND_DRAG_TYPE_STRING)
400 success = individual_drag_received (self, context, selection);
402 gtk_drag_finish (context, success, FALSE, GDK_CURRENT_TIME);
406 drag_motion (GtkWidget *widget,
407 GdkDragContext *context,
412 EmpathyPersonaView *self = EMPATHY_PERSONA_VIEW (widget);
413 EmpathyPersonaViewPriv *priv;
416 priv = GET_PRIV (self);
418 target = gtk_drag_dest_find_target (GTK_WIDGET (self), context, NULL);
420 if (target == drag_atoms_dest[DND_DRAG_TYPE_INDIVIDUAL_ID])
424 /* FIXME: It doesn't make sense for us to highlight a specific row or
425 * position to drop an Individual in, so just highlight the entire
427 * Since I can't find a way to do this, just highlight the first possible
428 * position in the tree. */
429 gdk_drag_status (context, gdk_drag_context_get_suggested_action (context),
432 path = gtk_tree_path_new_first ();
433 gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (self), path,
434 GTK_TREE_VIEW_DROP_BEFORE);
435 gtk_tree_path_free (path);
440 /* Unknown or unhandled drag target */
441 gdk_drag_status (context, GDK_ACTION_DEFAULT, time_);
442 gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (self), NULL, 0);
448 drag_data_get (GtkWidget *widget,
449 GdkDragContext *context,
450 GtkSelectionData *selection,
454 EmpathyPersonaView *self = EMPATHY_PERSONA_VIEW (widget);
455 EmpathyPersonaViewPriv *priv;
456 FolksPersona *persona;
457 const gchar *persona_uid;
459 if (info != DND_DRAG_TYPE_PERSONA_ID)
462 priv = GET_PRIV (self);
464 persona = empathy_persona_view_dup_selected (self);
468 persona_uid = folks_persona_get_uid (persona);
469 gtk_selection_data_set (selection, drag_atoms_source[info], 8,
470 (guchar *) persona_uid, strlen (persona_uid) + 1);
472 g_object_unref (persona);
476 drag_drop (GtkWidget *widget,
477 GdkDragContext *drag_context,
486 set_features (EmpathyPersonaView *self,
487 EmpathyPersonaViewFeatureFlags features)
489 EmpathyPersonaViewPriv *priv = GET_PRIV (self);
491 priv->features = features;
493 /* Setting reorderable is a hack that gets us row previews as drag icons
494 for free. We override all the drag handlers. It's tricky to get the
495 position of the drag icon right in drag_begin. GtkTreeView has special
496 voodoo for it, so we let it do the voodoo that he do (but only if dragging
498 gtk_tree_view_set_reorderable (GTK_TREE_VIEW (self),
499 (features & EMPATHY_PERSONA_VIEW_FEATURE_PERSONA_DRAG));
501 /* Update DnD source/dest */
502 if (features & EMPATHY_PERSONA_VIEW_FEATURE_PERSONA_DRAG)
504 gtk_drag_source_set (GTK_WIDGET (self),
507 G_N_ELEMENTS (drag_types_source),
508 GDK_ACTION_MOVE | GDK_ACTION_COPY);
512 gtk_drag_source_unset (GTK_WIDGET (self));
515 if (features & EMPATHY_PERSONA_VIEW_FEATURE_PERSONA_DROP)
517 gtk_drag_dest_set (GTK_WIDGET (self),
518 GTK_DEST_DEFAULT_ALL,
520 G_N_ELEMENTS (drag_types_dest), GDK_ACTION_MOVE | GDK_ACTION_COPY);
524 gtk_drag_dest_unset (GTK_WIDGET (self));
527 g_object_notify (G_OBJECT (self), "features");
531 empathy_persona_view_init (EmpathyPersonaView *self)
533 EmpathyPersonaViewPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
534 EMPATHY_TYPE_PERSONA_VIEW, EmpathyPersonaViewPriv);
538 /* Connect to tree view signals rather than override. */
539 g_signal_connect (self, "query-tooltip", (GCallback) query_tooltip_cb, NULL);
543 constructed (GObject *object)
545 EmpathyPersonaView *self = EMPATHY_PERSONA_VIEW (object);
546 GtkCellRenderer *cell;
547 GtkTreeViewColumn *col;
552 "headers-visible", FALSE,
553 "show-expanders", FALSE,
556 col = gtk_tree_view_column_new ();
559 cell = gtk_cell_renderer_pixbuf_new ();
560 gtk_tree_view_column_pack_start (col, cell, FALSE);
561 gtk_tree_view_column_set_cell_data_func (col, cell,
562 (GtkTreeCellDataFunc) pixbuf_cell_data_func, self, NULL);
571 cell = empathy_cell_renderer_text_new ();
572 gtk_tree_view_column_pack_start (col, cell, TRUE);
573 gtk_tree_view_column_set_cell_data_func (col, cell,
574 (GtkTreeCellDataFunc) text_cell_data_func, self, NULL);
576 /* We (ab)use the name and status properties here to display display ID and
577 * account name, respectively. Harmless. */
578 gtk_tree_view_column_add_attribute (col, cell,
579 "name", EMPATHY_PERSONA_STORE_COL_DISPLAY_ID);
580 gtk_tree_view_column_add_attribute (col, cell,
581 "text", EMPATHY_PERSONA_STORE_COL_DISPLAY_ID);
582 gtk_tree_view_column_add_attribute (col, cell,
583 "presence-type", EMPATHY_PERSONA_STORE_COL_PRESENCE_TYPE);
584 gtk_tree_view_column_add_attribute (col, cell,
585 "status", EMPATHY_PERSONA_STORE_COL_ACCOUNT_NAME);
587 /* Audio Call Icon */
588 cell = empathy_cell_renderer_activatable_new ();
589 gtk_tree_view_column_pack_start (col, cell, FALSE);
590 gtk_tree_view_column_set_cell_data_func (col, cell,
591 (GtkTreeCellDataFunc) audio_call_cell_data_func, self, NULL);
598 cell = gtk_cell_renderer_pixbuf_new ();
599 gtk_tree_view_column_pack_start (col, cell, FALSE);
600 gtk_tree_view_column_set_cell_data_func (col, cell,
601 (GtkTreeCellDataFunc) avatar_cell_data_func, self, NULL);
611 /* Actually add the column now we have added all cell renderers */
612 gtk_tree_view_append_column (GTK_TREE_VIEW (self), col);
615 for (i = 0; i < G_N_ELEMENTS (drag_types_dest); ++i)
616 drag_atoms_dest[i] = gdk_atom_intern (drag_types_dest[i].target, FALSE);
618 for (i = 0; i < G_N_ELEMENTS (drag_types_source); ++i)
619 drag_atoms_source[i] = gdk_atom_intern (drag_types_source[i].target, FALSE);
623 get_property (GObject *object,
628 EmpathyPersonaViewPriv *priv = GET_PRIV (object);
633 g_value_set_object (value, priv->filter);
635 case PROP_SHOW_OFFLINE:
636 g_value_set_boolean (value, priv->show_offline);
639 g_value_set_flags (value, priv->features);
642 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
648 set_property (GObject *object,
653 EmpathyPersonaView *self = EMPATHY_PERSONA_VIEW (object);
658 set_model (self, g_value_get_object (value));
660 case PROP_SHOW_OFFLINE:
661 empathy_persona_view_set_show_offline (self,
662 g_value_get_boolean (value));
665 set_features (self, g_value_get_flags (value));
668 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
674 dispose (GObject *object)
676 EmpathyPersonaView *self = EMPATHY_PERSONA_VIEW (object);
677 EmpathyPersonaViewPriv *priv = GET_PRIV (self);
679 tp_clear_object (&priv->filter);
681 if (priv->tooltip_widget)
682 gtk_widget_destroy (priv->tooltip_widget);
683 priv->tooltip_widget = NULL;
685 G_OBJECT_CLASS (empathy_persona_view_parent_class)->dispose (object);
689 empathy_persona_view_class_init (EmpathyPersonaViewClass *klass)
691 GObjectClass *object_class = G_OBJECT_CLASS (klass);
692 GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
694 object_class->constructed = constructed;
695 object_class->dispose = dispose;
696 object_class->get_property = get_property;
697 object_class->set_property = set_property;
699 widget_class->drag_data_received = drag_data_received;
700 widget_class->drag_drop = drag_drop;
701 widget_class->drag_data_get = drag_data_get;
702 widget_class->drag_motion = drag_motion;
704 signals[DRAG_INDIVIDUAL_RECEIVED] =
705 g_signal_new ("drag-individual-received",
706 G_OBJECT_CLASS_TYPE (klass),
708 G_STRUCT_OFFSET (EmpathyPersonaViewClass, drag_individual_received),
710 _empathy_gtk_marshal_BOOLEAN__UINT_OBJECT,
711 G_TYPE_BOOLEAN, 2, G_TYPE_UINT, FOLKS_TYPE_INDIVIDUAL);
713 /* We override the "model" property so that we can wrap it in a
714 * GtkTreeModelFilter for showing/hiding offline personas. */
715 g_object_class_override_property (object_class, PROP_MODEL, "model");
718 * EmpathyPersonaStore:show-offline:
720 * Whether to display offline personas.
722 g_object_class_install_property (object_class, PROP_SHOW_OFFLINE,
723 g_param_spec_boolean ("show-offline",
725 "Whether to display offline personas.",
727 G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
730 * EmpathyPersonaStore:features:
732 * Features of the view, such as whether drag and drop is enabled.
734 g_object_class_install_property (object_class, PROP_FEATURES,
735 g_param_spec_flags ("features",
737 "Flags for all enabled features.",
738 EMPATHY_TYPE_PERSONA_VIEW_FEATURE_FLAGS,
739 EMPATHY_PERSONA_VIEW_FEATURE_NONE,
740 G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
742 g_type_class_add_private (object_class, sizeof (EmpathyPersonaViewPriv));
746 * empathy_persona_view_new:
747 * @store: an #EmpathyPersonaStore
748 * @features: a set of flags specifying the view's functionality, or
749 * %EMPATHY_PERSONA_VIEW_FEATURE_NONE
751 * Create a new #EmpathyPersonaView displaying the personas in
752 * #EmpathyPersonaStore.
754 * Return value: a new #EmpathyPersonaView
757 empathy_persona_view_new (EmpathyPersonaStore *store,
758 EmpathyPersonaViewFeatureFlags features)
760 g_return_val_if_fail (EMPATHY_IS_PERSONA_STORE (store), NULL);
762 return g_object_new (EMPATHY_TYPE_PERSONA_VIEW,
764 "features", features,
769 * empathy_persona_view_dup_selected:
770 * @self: an #EmpathyPersonaView
772 * Return the #FolksPersona associated with the currently selected row. The
773 * persona is referenced before being returned. If no row is selected, %NULL is
776 * Return value: the currently selected #FolksPersona, or %NULL; unref with
780 empathy_persona_view_dup_selected (EmpathyPersonaView *self)
782 EmpathyPersonaViewPriv *priv;
783 GtkTreeSelection *selection;
786 FolksPersona *persona;
788 g_return_val_if_fail (EMPATHY_IS_PERSONA_VIEW (self), NULL);
790 priv = GET_PRIV (self);
792 selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (self));
793 if (!gtk_tree_selection_get_selected (selection, &model, &iter))
796 gtk_tree_model_get (model, &iter,
797 EMPATHY_PERSONA_STORE_COL_PERSONA, &persona,
804 * empathy_persona_view_get_show_offline:
805 * @self: an #EmpathyPersonaView
807 * Get the value of the #EmpathyPersonaView:show-offline property.
809 * Return value: %TRUE if offline personas are being shown, %FALSE otherwise
812 empathy_persona_view_get_show_offline (EmpathyPersonaView *self)
814 g_return_val_if_fail (EMPATHY_IS_PERSONA_VIEW (self), FALSE);
816 return GET_PRIV (self)->show_offline;
820 * empathy_persona_view_set_show_offline:
821 * @self: an #EmpathyPersonaView
822 * @show_offline: %TRUE to show personas which are offline, %FALSE otherwise
824 * Set the #EmpathyPersonaView:show-offline property to @show_offline.
827 empathy_persona_view_set_show_offline (EmpathyPersonaView *self,
828 gboolean show_offline)
830 EmpathyPersonaViewPriv *priv;
832 g_return_if_fail (EMPATHY_IS_PERSONA_VIEW (self));
834 priv = GET_PRIV (self);
835 priv->show_offline = show_offline;
837 gtk_tree_model_filter_refilter (priv->filter);
839 g_object_notify (G_OBJECT (self), "show-offline");