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"
50 #include "empathy-ui-utils.h"
52 #define DEBUG_FLAG EMPATHY_DEBUG_CONTACT
53 #include <libempathy/empathy-debug.h>
56 * SECTION:empathy-persona-view
57 * @title: EmpathyPersonaView
58 * @short_description: A tree view which displays personas from an individual
59 * @include: libempathy-gtk/empathy-persona-view.h
61 * #EmpathyPersonaView is a tree view widget which displays the personas from
62 * a given #EmpathyPersonaStore.
64 * It supports hiding offline personas and highlighting active personas. Active
65 * personas are those which have recently changed state (e.g. online, offline or
66 * from normal to a busy state).
69 #define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyPersonaView)
73 GtkTreeModelFilter *filter;
74 GtkWidget *tooltip_widget;
75 gboolean show_offline;
76 EmpathyPersonaViewFeatureFlags features;
77 } EmpathyPersonaViewPriv;
89 DND_DRAG_TYPE_UNKNOWN = -1,
90 DND_DRAG_TYPE_INDIVIDUAL_ID = 0,
91 DND_DRAG_TYPE_PERSONA_ID,
95 #define DRAG_TYPE(T,I) \
98 static const GtkTargetEntry drag_types_dest[] = {
99 DRAG_TYPE ("text/x-individual-id", DND_DRAG_TYPE_INDIVIDUAL_ID),
100 DRAG_TYPE ("text/plain", DND_DRAG_TYPE_STRING),
101 DRAG_TYPE ("STRING", DND_DRAG_TYPE_STRING),
104 static const GtkTargetEntry drag_types_source[] = {
105 DRAG_TYPE ("text/x-persona-id", DND_DRAG_TYPE_PERSONA_ID),
110 static GdkAtom drag_atoms_dest[G_N_ELEMENTS (drag_types_dest)];
114 DRAG_INDIVIDUAL_RECEIVED,
118 static guint signals[LAST_SIGNAL];
120 G_DEFINE_TYPE (EmpathyPersonaView, empathy_persona_view, GTK_TYPE_TREE_VIEW);
123 filter_visible_func (GtkTreeModel *model,
125 EmpathyPersonaView *self)
127 EmpathyPersonaViewPriv *priv = GET_PRIV (self);
130 gtk_tree_model_get (model, iter,
131 EMPATHY_PERSONA_STORE_COL_IS_ONLINE, &is_online,
134 return (priv->show_offline || is_online);
138 set_model (EmpathyPersonaView *self,
141 EmpathyPersonaViewPriv *priv = GET_PRIV (self);
143 tp_clear_object (&priv->filter);
145 priv->filter = GTK_TREE_MODEL_FILTER (gtk_tree_model_filter_new (model,
147 gtk_tree_model_filter_set_visible_func (priv->filter,
148 (GtkTreeModelFilterVisibleFunc) filter_visible_func, self, NULL);
150 gtk_tree_view_set_model (GTK_TREE_VIEW (self), GTK_TREE_MODEL (priv->filter));
154 tooltip_destroy_cb (GtkWidget *widget,
155 EmpathyPersonaView *self)
157 EmpathyPersonaViewPriv *priv = GET_PRIV (self);
159 if (priv->tooltip_widget)
161 DEBUG ("Tooltip destroyed");
162 g_object_unref (priv->tooltip_widget);
163 priv->tooltip_widget = NULL;
168 query_tooltip_cb (EmpathyPersonaView *self,
171 gboolean keyboard_mode,
175 EmpathyPersonaViewPriv *priv = GET_PRIV (self);
176 FolksPersona *persona;
177 TpContact *tp_contact;
178 EmpathyContact *contact;
182 static gint running = 0;
183 gboolean ret = FALSE;
185 /* Avoid an infinite loop. See GNOME bug #574377 */
190 if (!gtk_tree_view_get_tooltip_context (GTK_TREE_VIEW (self), &x, &y,
191 keyboard_mode, &model, &path, &iter))
196 gtk_tree_view_set_tooltip_row (GTK_TREE_VIEW (self), tooltip, path);
197 gtk_tree_path_free (path);
199 gtk_tree_model_get (model, &iter,
200 EMPATHY_PERSONA_STORE_COL_PERSONA, &persona,
205 tp_contact = tpf_persona_get_contact (TPF_PERSONA (persona));
206 if (tp_contact == NULL)
208 g_clear_object (&persona);
212 contact = empathy_contact_dup_from_tp_contact (tp_contact);
214 if (priv->tooltip_widget == NULL)
216 priv->tooltip_widget = empathy_contact_widget_new (contact,
217 EMPATHY_CONTACT_WIDGET_FOR_TOOLTIP |
218 EMPATHY_CONTACT_WIDGET_SHOW_LOCATION);
219 gtk_container_set_border_width (GTK_CONTAINER (priv->tooltip_widget), 8);
220 g_object_ref (priv->tooltip_widget);
221 g_signal_connect (priv->tooltip_widget, "destroy",
222 (GCallback) tooltip_destroy_cb, self);
223 gtk_widget_show (priv->tooltip_widget);
227 empathy_contact_widget_set_contact (priv->tooltip_widget, contact);
230 gtk_tooltip_set_custom (tooltip, priv->tooltip_widget);
233 g_object_unref (contact);
234 g_object_unref (persona);
243 cell_set_background (EmpathyPersonaView *self,
244 GtkCellRenderer *cell,
250 GtkStyleContext *style;
252 style = gtk_widget_get_style_context (GTK_WIDGET (self));
254 gtk_style_context_get_background_color (style, GTK_STATE_FLAG_SELECTED,
257 /* Here we take the current theme colour and add it to
258 * the colour for white and average the two. This
259 * gives a colour which is inline with the theme but
262 empathy_make_color_whiter (&color);
264 g_object_set (cell, "cell-background-rgba", &color, NULL);
268 g_object_set (cell, "cell-background-rgba", NULL, NULL);
273 pixbuf_cell_data_func (GtkTreeViewColumn *tree_column,
274 GtkCellRenderer *cell,
277 EmpathyPersonaView *self)
282 gtk_tree_model_get (model, iter,
283 EMPATHY_PERSONA_STORE_COL_IS_ACTIVE, &is_active,
284 EMPATHY_PERSONA_STORE_COL_ICON_STATUS, &pixbuf,
287 g_object_set (cell, "pixbuf", pixbuf, NULL);
288 tp_clear_object (&pixbuf);
290 cell_set_background (self, cell, is_active);
294 audio_call_cell_data_func (GtkTreeViewColumn *tree_column,
295 GtkCellRenderer *cell,
298 EmpathyPersonaView *self)
301 gboolean can_audio, can_video;
303 gtk_tree_model_get (model, iter,
304 EMPATHY_PERSONA_STORE_COL_IS_ACTIVE, &is_active,
305 EMPATHY_PERSONA_STORE_COL_CAN_AUDIO_CALL, &can_audio,
306 EMPATHY_PERSONA_STORE_COL_CAN_VIDEO_CALL, &can_video,
310 "visible", (can_audio || can_video),
311 "icon-name", can_video? EMPATHY_IMAGE_VIDEO_CALL : EMPATHY_IMAGE_VOIP,
314 cell_set_background (self, cell, is_active);
318 avatar_cell_data_func (GtkTreeViewColumn *tree_column,
319 GtkCellRenderer *cell,
322 EmpathyPersonaView *self)
325 gboolean show_avatar, is_active;
327 gtk_tree_model_get (model, iter,
328 EMPATHY_PERSONA_STORE_COL_PIXBUF_AVATAR, &pixbuf,
329 EMPATHY_PERSONA_STORE_COL_PIXBUF_AVATAR_VISIBLE, &show_avatar,
330 EMPATHY_PERSONA_STORE_COL_IS_ACTIVE, &is_active,
334 "visible", show_avatar,
338 tp_clear_object (&pixbuf);
340 cell_set_background (self, cell, is_active);
344 text_cell_data_func (GtkTreeViewColumn *tree_column,
345 GtkCellRenderer *cell,
348 EmpathyPersonaView *self)
352 gtk_tree_model_get (model, iter,
353 EMPATHY_PERSONA_STORE_COL_IS_ACTIVE, &is_active,
356 cell_set_background (self, cell, is_active);
360 individual_drag_received (EmpathyPersonaView *self,
361 GdkDragContext *context,
362 GtkSelectionData *selection)
364 EmpathyIndividualManager *manager = NULL;
365 FolksIndividual *individual;
366 const gchar *individual_id;
367 gboolean success = FALSE;
369 individual_id = (const gchar *) gtk_selection_data_get_data (selection);
370 manager = empathy_individual_manager_dup_singleton ();
371 individual = empathy_individual_manager_lookup_member (manager,
374 if (individual == NULL)
376 DEBUG ("Failed to find drag event individual with ID '%s'",
378 g_object_unref (manager);
382 /* Emit a signal notifying of the drag. */
383 g_signal_emit (self, signals[DRAG_INDIVIDUAL_RECEIVED], 0,
384 gdk_drag_context_get_selected_action (context), individual, &success);
386 g_object_unref (manager);
392 drag_data_received (GtkWidget *widget,
393 GdkDragContext *context,
396 GtkSelectionData *selection,
400 EmpathyPersonaView *self = EMPATHY_PERSONA_VIEW (widget);
401 gboolean success = TRUE;
403 if (info == DND_DRAG_TYPE_INDIVIDUAL_ID || info == DND_DRAG_TYPE_STRING)
404 success = individual_drag_received (self, context, selection);
406 gtk_drag_finish (context, success, FALSE, GDK_CURRENT_TIME);
410 drag_motion (GtkWidget *widget,
411 GdkDragContext *context,
416 EmpathyPersonaView *self = EMPATHY_PERSONA_VIEW (widget);
419 DndDragType drag_type = DND_DRAG_TYPE_UNKNOWN;
421 target = gtk_drag_dest_find_target (GTK_WIDGET (self), context, NULL);
423 /* Determine the DndDragType of the data */
424 for (i = 0; i < G_N_ELEMENTS (drag_atoms_dest); i++)
426 if (target == drag_atoms_dest[i])
428 drag_type = drag_types_dest[i].info;
433 if (drag_type == DND_DRAG_TYPE_INDIVIDUAL_ID)
437 /* FIXME: It doesn't make sense for us to highlight a specific row or
438 * position to drop an Individual in, so just highlight the entire
440 * Since I can't find a way to do this, just highlight the first possible
441 * position in the tree. */
442 gdk_drag_status (context, gdk_drag_context_get_suggested_action (context),
445 path = gtk_tree_path_new_first ();
446 gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (self), path,
447 GTK_TREE_VIEW_DROP_BEFORE);
448 gtk_tree_path_free (path);
453 /* Unknown or unhandled drag target */
454 gdk_drag_status (context, GDK_ACTION_DEFAULT, time_);
455 gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (self), NULL, 0);
461 drag_data_get (GtkWidget *widget,
462 GdkDragContext *context,
463 GtkSelectionData *selection,
467 EmpathyPersonaView *self = EMPATHY_PERSONA_VIEW (widget);
468 FolksPersona *persona;
469 const gchar *persona_uid;
471 if (info != DND_DRAG_TYPE_PERSONA_ID)
474 persona = empathy_persona_view_dup_selected (self);
478 persona_uid = folks_persona_get_uid (persona);
479 gtk_selection_data_set (selection,
480 gdk_atom_intern ("text/x-persona-id", FALSE), 8,
481 (guchar *) persona_uid, strlen (persona_uid) + 1);
483 g_object_unref (persona);
487 drag_drop (GtkWidget *widget,
488 GdkDragContext *drag_context,
497 set_features (EmpathyPersonaView *self,
498 EmpathyPersonaViewFeatureFlags features)
500 EmpathyPersonaViewPriv *priv = GET_PRIV (self);
502 priv->features = features;
504 /* Setting reorderable is a hack that gets us row previews as drag icons
505 for free. We override all the drag handlers. It's tricky to get the
506 position of the drag icon right in drag_begin. GtkTreeView has special
507 voodoo for it, so we let it do the voodoo that he do (but only if dragging
509 gtk_tree_view_set_reorderable (GTK_TREE_VIEW (self),
510 (features & EMPATHY_PERSONA_VIEW_FEATURE_PERSONA_DRAG));
512 /* Update DnD source/dest */
513 if (features & EMPATHY_PERSONA_VIEW_FEATURE_PERSONA_DRAG)
515 gtk_drag_source_set (GTK_WIDGET (self),
518 G_N_ELEMENTS (drag_types_source),
519 GDK_ACTION_MOVE | GDK_ACTION_COPY);
523 gtk_drag_source_unset (GTK_WIDGET (self));
526 if (features & EMPATHY_PERSONA_VIEW_FEATURE_PERSONA_DROP)
528 gtk_drag_dest_set (GTK_WIDGET (self),
529 GTK_DEST_DEFAULT_ALL,
531 G_N_ELEMENTS (drag_types_dest), GDK_ACTION_MOVE | GDK_ACTION_COPY);
535 gtk_drag_dest_unset (GTK_WIDGET (self));
538 g_object_notify (G_OBJECT (self), "features");
542 empathy_persona_view_init (EmpathyPersonaView *self)
544 EmpathyPersonaViewPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
545 EMPATHY_TYPE_PERSONA_VIEW, EmpathyPersonaViewPriv);
549 /* Connect to tree view signals rather than override. */
550 g_signal_connect (self, "query-tooltip", (GCallback) query_tooltip_cb, NULL);
554 constructed (GObject *object)
556 EmpathyPersonaView *self = EMPATHY_PERSONA_VIEW (object);
557 GtkCellRenderer *cell;
558 GtkTreeViewColumn *col;
563 "headers-visible", FALSE,
564 "show-expanders", FALSE,
567 col = gtk_tree_view_column_new ();
570 cell = gtk_cell_renderer_pixbuf_new ();
571 gtk_tree_view_column_pack_start (col, cell, FALSE);
572 gtk_tree_view_column_set_cell_data_func (col, cell,
573 (GtkTreeCellDataFunc) pixbuf_cell_data_func, self, NULL);
582 cell = empathy_cell_renderer_text_new ();
583 gtk_tree_view_column_pack_start (col, cell, TRUE);
584 gtk_tree_view_column_set_cell_data_func (col, cell,
585 (GtkTreeCellDataFunc) text_cell_data_func, self, NULL);
587 /* We (ab)use the name and status properties here to display display ID and
588 * account name, respectively. Harmless. */
589 gtk_tree_view_column_add_attribute (col, cell,
590 "name", EMPATHY_PERSONA_STORE_COL_DISPLAY_ID);
591 gtk_tree_view_column_add_attribute (col, cell,
592 "text", EMPATHY_PERSONA_STORE_COL_DISPLAY_ID);
593 gtk_tree_view_column_add_attribute (col, cell,
594 "presence-type", EMPATHY_PERSONA_STORE_COL_PRESENCE_TYPE);
595 gtk_tree_view_column_add_attribute (col, cell,
596 "status", EMPATHY_PERSONA_STORE_COL_ACCOUNT_NAME);
598 /* Audio Call Icon */
599 cell = empathy_cell_renderer_activatable_new ();
600 gtk_tree_view_column_pack_start (col, cell, FALSE);
601 gtk_tree_view_column_set_cell_data_func (col, cell,
602 (GtkTreeCellDataFunc) audio_call_cell_data_func, self, NULL);
609 cell = gtk_cell_renderer_pixbuf_new ();
610 gtk_tree_view_column_pack_start (col, cell, FALSE);
611 gtk_tree_view_column_set_cell_data_func (col, cell,
612 (GtkTreeCellDataFunc) avatar_cell_data_func, self, NULL);
622 /* Actually add the column now we have added all cell renderers */
623 gtk_tree_view_append_column (GTK_TREE_VIEW (self), col);
626 for (i = 0; i < G_N_ELEMENTS (drag_types_dest); ++i)
627 drag_atoms_dest[i] = gdk_atom_intern (drag_types_dest[i].target, FALSE);
631 get_property (GObject *object,
636 EmpathyPersonaViewPriv *priv = GET_PRIV (object);
641 g_value_set_object (value, priv->filter);
643 case PROP_SHOW_OFFLINE:
644 g_value_set_boolean (value, priv->show_offline);
647 g_value_set_flags (value, priv->features);
650 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
656 set_property (GObject *object,
661 EmpathyPersonaView *self = EMPATHY_PERSONA_VIEW (object);
666 set_model (self, g_value_get_object (value));
668 case PROP_SHOW_OFFLINE:
669 empathy_persona_view_set_show_offline (self,
670 g_value_get_boolean (value));
673 set_features (self, g_value_get_flags (value));
676 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
682 dispose (GObject *object)
684 EmpathyPersonaView *self = EMPATHY_PERSONA_VIEW (object);
685 EmpathyPersonaViewPriv *priv = GET_PRIV (self);
687 tp_clear_object (&priv->filter);
689 if (priv->tooltip_widget)
690 gtk_widget_destroy (priv->tooltip_widget);
691 priv->tooltip_widget = NULL;
693 G_OBJECT_CLASS (empathy_persona_view_parent_class)->dispose (object);
697 empathy_persona_view_class_init (EmpathyPersonaViewClass *klass)
699 GObjectClass *object_class = G_OBJECT_CLASS (klass);
700 GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
702 object_class->constructed = constructed;
703 object_class->dispose = dispose;
704 object_class->get_property = get_property;
705 object_class->set_property = set_property;
707 widget_class->drag_data_received = drag_data_received;
708 widget_class->drag_drop = drag_drop;
709 widget_class->drag_data_get = drag_data_get;
710 widget_class->drag_motion = drag_motion;
712 signals[DRAG_INDIVIDUAL_RECEIVED] =
713 g_signal_new ("drag-individual-received",
714 G_OBJECT_CLASS_TYPE (klass),
716 G_STRUCT_OFFSET (EmpathyPersonaViewClass, drag_individual_received),
718 _empathy_gtk_marshal_BOOLEAN__UINT_OBJECT,
719 G_TYPE_BOOLEAN, 2, G_TYPE_UINT, FOLKS_TYPE_INDIVIDUAL);
721 /* We override the "model" property so that we can wrap it in a
722 * GtkTreeModelFilter for showing/hiding offline personas. */
723 g_object_class_override_property (object_class, PROP_MODEL, "model");
726 * EmpathyPersonaStore:show-offline:
728 * Whether to display offline personas.
730 g_object_class_install_property (object_class, PROP_SHOW_OFFLINE,
731 g_param_spec_boolean ("show-offline",
733 "Whether to display offline personas.",
735 G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
738 * EmpathyPersonaStore:features:
740 * Features of the view, such as whether drag and drop is enabled.
742 g_object_class_install_property (object_class, PROP_FEATURES,
743 g_param_spec_flags ("features",
745 "Flags for all enabled features.",
746 EMPATHY_TYPE_PERSONA_VIEW_FEATURE_FLAGS,
747 EMPATHY_PERSONA_VIEW_FEATURE_NONE,
748 G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
750 g_type_class_add_private (object_class, sizeof (EmpathyPersonaViewPriv));
754 * empathy_persona_view_new:
755 * @store: an #EmpathyPersonaStore
756 * @features: a set of flags specifying the view's functionality, or
757 * %EMPATHY_PERSONA_VIEW_FEATURE_NONE
759 * Create a new #EmpathyPersonaView displaying the personas in
760 * #EmpathyPersonaStore.
762 * Return value: a new #EmpathyPersonaView
765 empathy_persona_view_new (EmpathyPersonaStore *store,
766 EmpathyPersonaViewFeatureFlags features)
768 g_return_val_if_fail (EMPATHY_IS_PERSONA_STORE (store), NULL);
770 return g_object_new (EMPATHY_TYPE_PERSONA_VIEW,
772 "features", features,
777 * empathy_persona_view_dup_selected:
778 * @self: an #EmpathyPersonaView
780 * Return the #FolksPersona associated with the currently selected row. The
781 * persona is referenced before being returned. If no row is selected, %NULL is
784 * Return value: the currently selected #FolksPersona, or %NULL; unref with
788 empathy_persona_view_dup_selected (EmpathyPersonaView *self)
790 GtkTreeSelection *selection;
793 FolksPersona *persona;
795 g_return_val_if_fail (EMPATHY_IS_PERSONA_VIEW (self), NULL);
797 selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (self));
798 if (!gtk_tree_selection_get_selected (selection, &model, &iter))
801 gtk_tree_model_get (model, &iter,
802 EMPATHY_PERSONA_STORE_COL_PERSONA, &persona,
809 * empathy_persona_view_get_show_offline:
810 * @self: an #EmpathyPersonaView
812 * Get the value of the #EmpathyPersonaView:show-offline property.
814 * Return value: %TRUE if offline personas are being shown, %FALSE otherwise
817 empathy_persona_view_get_show_offline (EmpathyPersonaView *self)
819 g_return_val_if_fail (EMPATHY_IS_PERSONA_VIEW (self), FALSE);
821 return GET_PRIV (self)->show_offline;
825 * empathy_persona_view_set_show_offline:
826 * @self: an #EmpathyPersonaView
827 * @show_offline: %TRUE to show personas which are offline, %FALSE otherwise
829 * Set the #EmpathyPersonaView:show-offline property to @show_offline.
832 empathy_persona_view_set_show_offline (EmpathyPersonaView *self,
833 gboolean show_offline)
835 EmpathyPersonaViewPriv *priv;
837 g_return_if_fail (EMPATHY_IS_PERSONA_VIEW (self));
839 priv = GET_PRIV (self);
840 priv->show_offline = show_offline;
842 gtk_tree_model_filter_refilter (priv->filter);
844 g_object_notify (G_OBJECT (self), "show-offline");