Create a PidginAvatar widget.
This does everything the existing code does, but trying to integrate right now
is kind of difficult. The plan is to use this in a new PidginInfoPane I have
started, but that change got very large so I just packed it into the end of
the existing info pane.
The only things that are not implement right now, are making menu items
insensitive and that's because we need to figure out a better want to handle
custom avatars for users.
Testing Done:
Ran locally.
Reviewed at https://reviews.imfreedom.org/r/528/
--- a/doc/reference/pidgin/pidgin-docs.xml Thu Mar 04 22:43:51 2021 -0600
+++ b/doc/reference/pidgin/pidgin-docs.xml Fri Mar 05 03:31:29 2021 -0600
@@ -57,6 +57,7 @@
<xi:include href="xml/pidginactiongroup.xml" />
<xi:include href="xml/pidginapplication.xml" />
<xi:include href="xml/pidginattachment.xml" />
+ <xi:include href="xml/pidginavatar.xml" /> <xi:include href="xml/pidgincellrendererexpander.xml" />
<xi:include href="xml/pidginclosebutton.xml" />
<xi:include href="xml/pidgincontactcompletion.xml" />
--- a/libpurple/buddyicon.c Thu Mar 04 22:43:51 2021 -0600
+++ b/libpurple/buddyicon.c Fri Mar 05 03:31:29 2021 -0600
@@ -477,6 +477,19 @@
+purple_buddy_icon_save_to_filename(PurpleBuddyIcon *icon, + const gchar *filename, GError **error) + data = purple_buddy_icon_get_data(icon, &len); + return g_file_set_contents(filename, data, len, error); purple_buddy_icon_get_account(const PurpleBuddyIcon *icon)
@@ -517,6 +530,18 @@
+purple_buddy_icon_get_stream(PurpleBuddyIcon *icon) { + gconstpointer data = NULL; + g_return_val_if_fail(icon != NULL, NULL); + data = purple_buddy_icon_get_data(icon, &len); + return g_memory_input_stream_new_from_data(data, (gssize)len, NULL); purple_buddy_icon_get_extension(const PurpleBuddyIcon *icon)
--- a/libpurple/buddyicon.h Thu Mar 04 22:43:51 2021 -0600
+++ b/libpurple/buddyicon.h Fri Mar 05 03:31:29 2021 -0600
@@ -25,6 +25,10 @@
#ifndef PURPLE_BUDDYICON_H
#define PURPLE_BUDDYICON_H
* @section_id: libpurple-buddyicon
@@ -169,6 +173,20 @@
size_t len, const char *checksum);
+ * purple_buddy_icon_save_to_filename: + * @icon: The #PurpleBuddyIcon instance. + * @filename: The filename to write. + * @error: (optional): A return address for a #GError. + * Writes the contents of @icon to @filename. + * Returns: %TRUE on success, or %FALSE on error possibly with @error set. +gboolean purple_buddy_icon_save_to_filename(PurpleBuddyIcon *icon, const gchar *filename, GError **error); * purple_buddy_icon_get_account:
@@ -213,6 +231,18 @@
gconstpointer purple_buddy_icon_get_data(const PurpleBuddyIcon *icon, size_t *len);
+ * purple_buddy_icon_get_stream: + * @icon: The #PurpleBuddyIcon instance. + * Gets the data of @icon as a #GInputStream. + * Returns: (transfer full): A new #GInputStream. +GInputStream *purple_buddy_icon_get_stream(PurpleBuddyIcon *icon); * purple_buddy_icon_get_extension:
--- a/pidgin/glade/pidgin3.xml.in Thu Mar 04 22:43:51 2021 -0600
+++ b/pidgin/glade/pidgin3.xml.in Fri Mar 05 03:31:29 2021 -0600
@@ -6,6 +6,7 @@
<glade-widget-class name="PidginAccountStore" generic-name="account_store" title="AccountStore"/>
<glade-widget-class name="PidginAccountFilterConnected" generic-name="account_filter_connected" title="FilterConnected"/>
<glade-widget-class name="PidginAccountFilterProtocol" generic-name="account_filter_protocol" title="FilterProtocol"/>
+ <glade-widget-class name="PidginAvatar" generic-name="avatar" title="Avatar"/> <glade-widget-class name="PidginCloseButton" generic-name="close-button" title="CloseButton"/>
<glade-widget-class name="PidginConversationWindow" generic-name="conversation_window" title="ConversationWindow"/>
<glade-widget-class name="PidginCredentialProviderStore" generic-name="credential_provider_store" title="CredentialProviderStore"/>
@@ -27,6 +28,7 @@
<glade-widget-class-ref name="PidginAccountStore"/>
<glade-widget-class-ref name="PidginAccountFilterConnected"/>
<glade-widget-class-ref name="PidginAccountFilterProtocol"/>
+ <glade-widget-class-ref name="PidginAvatar"/> <glade-widget-class-ref name="PidginCloseButton"/>
<glade-widget-class-ref name="PidginConversationWindow"/>
<glade-widget-class-ref name="PidginCredentialProviderStore"/>
--- a/pidgin/gtkconv.c Thu Mar 04 22:43:51 2021 -0600
+++ b/pidgin/gtkconv.c Fri Mar 05 03:31:29 2021 -0600
@@ -49,6 +49,7 @@
+#include "pidginavatar.h" #include "pidginclosebutton.h"
#include "pidginconversationwindow.h"
@@ -3886,6 +3887,10 @@
gtk_event_box_set_visible_window(GTK_EVENT_BOX(event_box), FALSE);
gtk_widget_show(event_box);
gtkconv->infopane_hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ GtkWidget *avatar = pidgin_avatar_new(); + gtk_box_pack_end(GTK_BOX(gtkconv->infopane_hbox), avatar, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(vbox), event_box, FALSE, FALSE, 0);
gtk_container_add(GTK_CONTAINER(event_box), gtkconv->infopane_hbox);
gtk_widget_show(gtkconv->infopane_hbox);
@@ -3929,6 +3934,10 @@
buddyicon_size = purple_blist_node_get_int((PurpleBlistNode*)contact, "pidgin-infopane-iconsize");
+ pidgin_avatar_set_buddy(avatar, buddy); + pidgin_avatar_set_conversation(avatar, conv); buddyicon_size = CLAMP(buddyicon_size, BUDDYICON_SIZE_MIN, BUDDYICON_SIZE_MAX);
gtk_widget_set_size_request(gtkconv->u.im->icon_container, -1, buddyicon_size);
--- a/pidgin/meson.build Thu Mar 04 22:43:51 2021 -0600
+++ b/pidgin/meson.build Fri Mar 05 03:31:29 2021 -0600
@@ -37,6 +37,7 @@
'pidgincellrendererexpander.c',
'pidgincontactcompletion.c',
@@ -104,6 +105,7 @@
'pidgincellrendererexpander.h',
'pidgincontactcompletion.h',
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/pidgin/pidginavatar.c Fri Mar 05 03:31:29 2021 -0600
@@ -0,0 +1,643 @@
+ * Pidgin - Internet Messenger + * Copyright (C) Pidgin Developers <devel@pidgin.im> + * Pidgin is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <https://www.gnu.org/licenses/>. +#include <glib/gi18n-lib.h> +#include "pidgin/pidginavatar.h" +#define PIDGIN_AVATAR_ACTION_PREFIX "avatar" +/* if you change this value, you _MUST_ update Avatar/menu.ui for the new value +#define PIDGIN_AVATAR_ANIMATE_ACTION "animate" + GdkPixbufAnimation *animation; + PurpleConversation *conversation; +static GParamSpec *properties[N_PROPERTIES] = {NULL, }; +G_DEFINE_TYPE(PidginAvatar, pidgin_avatar, GTK_TYPE_EVENT_BOX) +/****************************************************************************** + *****************************************************************************/ +pidgin_avatar_animate_toggle(GSimpleAction *action, GVariant *value, + PidginAvatar *avatar = PIDGIN_AVATAR(data); + pidgin_avatar_set_animate(avatar, g_variant_get_boolean(value)); + g_simple_action_set_state(action, value); +pidgin_avatar_save_response_cb(GtkNativeDialog *native, gint response, + PidginAvatar *avatar = PIDGIN_AVATAR(data); + PurpleBuddyIcon *icon = NULL; + if(response != GTK_RESPONSE_ACCEPT || !PURPLE_IS_BUDDY(avatar->buddy)) { + gtk_native_dialog_destroy(native); + icon = purple_buddy_get_icon(avatar->buddy); + GtkFileChooser *chooser = GTK_FILE_CHOOSER(native); + gchar *filename = NULL; + filename = gtk_file_chooser_get_filename(chooser); + purple_buddy_icon_save_to_filename(icon, filename, NULL); + gtk_native_dialog_destroy(native); +pidgin_avatar_save_cb(GSimpleAction *action, GVariant *parameter, + PidginAvatar *avatar = PIDGIN_AVATAR(data); + PurpleAccount *account = NULL; + GtkFileChooserNative *native = NULL; + GtkFileChooser *chooser = NULL; + GtkWindow *window = NULL; + const gchar *ext = NULL, *name = NULL; + gchar *filename = NULL; + g_return_if_fail(PURPLE_IS_BUDDY(avatar->buddy)); + ext = purple_buddy_icon_get_extension(purple_buddy_get_icon(avatar->buddy)); + account = purple_buddy_get_account(avatar->buddy); + name = purple_buddy_get_name(avatar->buddy); + filename = g_strdup_printf("%s.%s", purple_normalize(account, name), ext); + window = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(avatar))); + native = gtk_file_chooser_native_new(_("Save Avatar"), + GTK_FILE_CHOOSER_ACTION_SAVE, + g_signal_connect(G_OBJECT(native), "response", + G_CALLBACK(pidgin_avatar_save_response_cb), avatar); + chooser = GTK_FILE_CHOOSER(native); + gtk_file_chooser_set_do_overwrite_confirmation(chooser, TRUE); + gtk_file_chooser_set_filename(chooser, filename); + gtk_native_dialog_show(GTK_NATIVE_DIALOG(native)); +pidgin_avatar_set_custom_response_cb(GtkNativeDialog *native, gint response, + PidginAvatar *avatar = PIDGIN_AVATAR(data); + GtkFileChooser *chooser = GTK_FILE_CHOOSER(native); + gchar *filename = NULL; + if(response != GTK_RESPONSE_ACCEPT || !PURPLE_IS_BUDDY(avatar->buddy)) { + gtk_native_dialog_destroy(native); + filename = gtk_file_chooser_get_filename(chooser); + PurpleContact *contact = purple_buddy_get_contact(avatar->buddy); + PurpleBlistNode *node = PURPLE_BLIST_NODE(contact); + purple_buddy_icons_node_set_custom_icon_from_file(node, filename); + gtk_native_dialog_destroy(native); +pidgin_avatar_set_custom_cb(GSimpleAction *action, GVariant *parameter, + PidginAvatar *avatar = PIDGIN_AVATAR(data); + GtkFileChooserNative *native = NULL; + GtkWindow *window = NULL; + window = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(avatar))); + native = gtk_file_chooser_native_new(_("Set Custom Avatar"), + GTK_FILE_CHOOSER_ACTION_OPEN, + g_signal_connect(G_OBJECT(native), "response", + G_CALLBACK(pidgin_avatar_set_custom_response_cb), avatar); + gtk_native_dialog_show(GTK_NATIVE_DIALOG(native)); +pidgin_avatar_clear_custom_cb(GSimpleAction *action, GVariant *parameter, + PidginAvatar *avatar = PIDGIN_AVATAR(data); + if(PURPLE_IS_BUDDY(avatar->buddy)) { + PurpleContact *contact = purple_buddy_get_contact(avatar->buddy); + PurpleBlistNode *node = PURPLE_BLIST_NODE(contact); + purple_buddy_icons_node_set_custom_icon_from_file(node, NULL); +static GActionEntry actions[] = { + .name = PIDGIN_AVATAR_ANIMATE_ACTION, + .change_state = pidgin_avatar_animate_toggle, + .activate = pidgin_avatar_save_cb, + .name = "set-custom-avatar", + .activate = pidgin_avatar_set_custom_cb, + .name = "clear-custom-avatar", + .activate = pidgin_avatar_clear_custom_cb, +/****************************************************************************** + *****************************************************************************/ +static GdkPixbufAnimation * +pidgin_avatar_find_buddy_icon(PurpleBuddy *buddy, + PurpleIMConversation *conversation) + GdkPixbufAnimation *ret = NULL; + GInputStream *stream = NULL; + PurpleContact *contact = NULL; + g_return_val_if_fail(PURPLE_IS_BUDDY(buddy), NULL); + /* First check if our user has set a custom icon for this buddy. */ + contact = purple_buddy_get_contact(buddy); + if(PURPLE_IS_CONTACT(contact)) { + PurpleBlistNode *node = PURPLE_BLIST_NODE(contact); + PurpleImage *custom_image = NULL; + custom_image = purple_buddy_icons_node_find_custom_icon(node); + if(PURPLE_IS_IMAGE(custom_image)) { + gconstpointer data = purple_image_get_data(custom_image); + gsize length = purple_image_get_data_size(custom_image); + stream = g_memory_input_stream_new_from_data(data, (gssize)length, + /* If there is no custom icon, fall back to checking if the buddy has an + if(!G_IS_INPUT_STREAM(stream)) { + PurpleBuddyIcon *icon = purple_buddy_get_icon(buddy); + stream = purple_buddy_icon_get_stream(icon); + /* Finally if we still don't have icon, we fallback to asking the + * conversation for one. + if(!G_IS_INPUT_STREAM(stream) && PURPLE_IS_IM_CONVERSATION(conversation)) { + PurpleBuddyIcon *icon = purple_im_conversation_get_icon(PURPLE_IM_CONVERSATION(conversation)); + stream = purple_buddy_icon_get_stream(icon); + if(G_IS_INPUT_STREAM(stream)) { + ret = gdk_pixbuf_animation_new_from_stream(stream, NULL, NULL); + g_clear_object(&stream); +pidgin_avatar_update(PidginAvatar *avatar) { + PurpleAccount *account = NULL; + GdkPixbufAnimation *animation = NULL; + if(PURPLE_IS_BUDDY(avatar->buddy)) { + animation = pidgin_avatar_find_buddy_icon(avatar->buddy, NULL); + } else if(PURPLE_IS_IM_CONVERSATION(avatar->conversation)) { + PurpleBuddy *buddy = NULL; + const gchar *name = NULL; + account = purple_conversation_get_account(avatar->conversation); + name = purple_conversation_get_name(avatar->conversation); + buddy = purple_blist_find_buddy(account, name); + if(PURPLE_IS_BUDDY(buddy)) { + animation = pidgin_avatar_find_buddy_icon(buddy, + PURPLE_IM_CONVERSATION(avatar->conversation)); + g_set_object(&avatar->animation, animation); + if(GDK_IS_PIXBUF_ANIMATION(avatar->animation)) { + gtk_image_set_from_animation(GTK_IMAGE(avatar->icon), + GdkPixbuf *frame = NULL; + frame = gdk_pixbuf_animation_get_static_image(avatar->animation); + gtk_image_set_from_pixbuf(GTK_IMAGE(avatar->icon), frame); + gtk_image_clear(GTK_IMAGE(avatar->icon)); + g_clear_object(&animation); +/****************************************************************************** + *****************************************************************************/ +pidgin_avatar_button_press_handler(GtkWidget *widget, GdkEventButton *event, + PidginAvatar *avatar = PIDGIN_AVATAR(data); + GtkBuilder *builder = NULL; + GtkWidget *menu = NULL; + GMenuModel *model = NULL; + if(!gdk_event_triggers_context_menu((GdkEvent *)event)) { + builder = gtk_builder_new_from_resource("/im/pidgin/Pidgin/Avatar/menu.ui"); + model = (GMenuModel *)gtk_builder_get_object(builder, "menu"); + menu = gtk_menu_new_from_model(model); + gtk_menu_attach_to_widget(GTK_MENU(menu), GTK_WIDGET(avatar), NULL); + g_clear_object(&builder); + gtk_widget_show_all(menu); + gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent *)event); + * This function is a callback for when properties change on the buddy we're + * tracking. It should not be reused for the conversation we're tracking + * because we have to disconnect old handlers and reuse of this function will + * cause issues if a buddy is changed but a conversation is not and vice versa. +pidgin_avatar_buddy_icon_updated(GObject *obj, GParamSpec *pspec, gpointer d) { + PidginAvatar *avatar = PIDGIN_AVATAR(d); + pidgin_avatar_update(avatar); + * This function is a callback for when properties change on the conversation + * we're tracking. It should not be reused for the buddy we're tracking + * because we have to disconnect old handlers and reuse of this function will + * cause issues if a buddy is changed but a conversation is not and vice versa. +pidgin_avatar_conversation_updated(GObject *obj, GParamSpec *pspec, gpointer d) + PidginAvatar *avatar = PIDGIN_AVATAR(d); + pidgin_avatar_update(avatar); +pidgin_avatar_enter_notify_handler(GtkWidget *widget, GdkEvent *event, + PidginAvatar *avatar = PIDGIN_AVATAR(widget); + pidgin_avatar_set_animate(avatar, TRUE); +pidgin_avatar_leave_notify_handler(GtkWidget *widget, GdkEvent *event, + PidginAvatar *avatar = PIDGIN_AVATAR(widget); + GActionGroup *group = NULL; + group = gtk_widget_get_action_group(widget, PIDGIN_AVATAR_ACTION_PREFIX); + if(G_IS_SIMPLE_ACTION_GROUP(group)) { + GVariant *state = NULL; + state = g_action_group_get_action_state(group, + PIDGIN_AVATAR_ANIMATE_ACTION); + if(!g_variant_get_boolean(state)) { + pidgin_avatar_set_animate(avatar, FALSE); +/****************************************************************************** + * GObject Implementation + *****************************************************************************/ +pidgin_avatar_get_property(GObject *obj, guint param_id, GValue *value, + PidginAvatar *avatar = PIDGIN_AVATAR(obj); + g_value_set_boolean(value, pidgin_avatar_get_animate(avatar)); + g_value_set_object(value, pidgin_avatar_get_buddy(avatar)); + case PROP_CONVERSATION: + g_value_set_object(value, pidgin_avatar_get_conversation(avatar)); + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec); +pidgin_avatar_set_property(GObject *obj, guint param_id, const GValue *value, + PidginAvatar *avatar = PIDGIN_AVATAR(obj); + pidgin_avatar_set_animate(avatar, g_value_get_boolean(value)); + pidgin_avatar_set_buddy(avatar, g_value_get_object(value)); + case PROP_CONVERSATION: + pidgin_avatar_set_conversation(avatar, g_value_get_object(value)); + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec); +pidgin_avatar_dispose(GObject *obj) { + PidginAvatar *avatar = PIDGIN_AVATAR(obj); + pidgin_avatar_set_buddy(avatar, NULL); + pidgin_avatar_set_conversation(avatar, NULL); + g_clear_object(&avatar->animation); + G_OBJECT_CLASS(pidgin_avatar_parent_class)->dispose(obj); +pidgin_avatar_init(PidginAvatar *avatar) { + GSimpleActionGroup *group = NULL; + gtk_widget_init_template(GTK_WIDGET(avatar)); + /* For development/design purposes, the avatar defaults to the + * "image-missing" icon. However, we don't want to display that to users, + * so we clear it during run time. + gtk_image_clear(GTK_IMAGE(avatar->icon)); + /* Now setup our actions. */ + group = g_simple_action_group_new(); + g_action_map_add_action_entries(G_ACTION_MAP(group), actions, + G_N_ELEMENTS(actions), avatar); + gtk_widget_insert_action_group(GTK_WIDGET(avatar), + PIDGIN_AVATAR_ACTION_PREFIX, + G_ACTION_GROUP(group)); +pidgin_avatar_class_init(PidginAvatarClass *klass) { + GObjectClass *obj_class = G_OBJECT_CLASS(klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass); + obj_class->get_property = pidgin_avatar_get_property; + obj_class->set_property = pidgin_avatar_set_property; + obj_class->dispose = pidgin_avatar_dispose; + * PidginAvatar::animate: + * Whether or not an animated avatar should be animated. + properties[PROP_ANIMATE] = g_param_spec_boolean( + "Whether or not to animate an animated avatar", + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + * The #PurpleBuddy whose avatar will be displayed. + properties[PROP_BUDDY] = g_param_spec_object( + "The buddy whose avatar to display", + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + * PidginAvatar::conversation: + * The #PurpleConversation which will be used to find the correct + properties[PROP_CONVERSATION] = g_param_spec_object( + "conversation", "conversation", + "The conversation used to find the correct buddy.", + PURPLE_TYPE_CONVERSATION, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + g_object_class_install_properties(obj_class, N_PROPERTIES, properties); + gtk_widget_class_set_template_from_resource( + "/im/pidgin/Pidgin/Avatar/avatar.ui" + gtk_widget_class_bind_template_child(widget_class, PidginAvatar, icon); + gtk_widget_class_bind_template_callback(widget_class, + pidgin_avatar_button_press_handler); + gtk_widget_class_bind_template_callback(widget_class, + pidgin_avatar_enter_notify_handler); + gtk_widget_class_bind_template_callback(widget_class, + pidgin_avatar_leave_notify_handler); +/****************************************************************************** + *****************************************************************************/ +pidgin_avatar_new(void) { + return GTK_WIDGET(g_object_new(PIDGIN_TYPE_AVATAR, NULL)); +pidgin_avatar_set_animate(PidginAvatar *avatar, gboolean animate) { + g_return_if_fail(PIDGIN_IS_AVATAR(avatar)); + avatar->animate = animate; + if(GDK_IS_PIXBUF_ANIMATION(avatar->animation)) { + gtk_image_set_from_animation(GTK_IMAGE(avatar->icon), + GdkPixbuf *frame = NULL; + frame = gdk_pixbuf_animation_get_static_image(avatar->animation); + gtk_image_set_from_pixbuf(GTK_IMAGE(avatar->icon), frame); +pidgin_avatar_get_animate(PidginAvatar *avatar) { + g_return_val_if_fail(PIDGIN_IS_AVATAR(avatar), FALSE); + return avatar->animate; +pidgin_avatar_set_buddy(PidginAvatar *avatar, PurpleBuddy *buddy) { + g_return_if_fail(PIDGIN_IS_AVATAR(avatar)); + /* Remove our old signal handler. */ + if(PURPLE_IS_BUDDY(avatar->buddy)) { + g_signal_handlers_disconnect_by_func(avatar->buddy, + pidgin_avatar_buddy_icon_updated, + if(g_set_object(&avatar->buddy, buddy)) { + pidgin_avatar_update(avatar); + g_object_notify_by_pspec(G_OBJECT(avatar), properties[PROP_BUDDY]); + /* Add the notify signal so we can update when the icon changes. */ + if(PURPLE_IS_BUDDY(avatar->buddy)) { + g_signal_connect(G_OBJECT(avatar->buddy), "notify::icon", + G_CALLBACK(pidgin_avatar_buddy_icon_updated), avatar); +pidgin_avatar_get_buddy(PidginAvatar *avatar) { + g_return_val_if_fail(PIDGIN_IS_AVATAR(avatar), NULL); +pidgin_avatar_set_conversation(PidginAvatar *avatar, + PurpleConversation *conversation) + g_return_if_fail(PIDGIN_IS_AVATAR(avatar)); + /* Remove our old signal handler. */ + if(PURPLE_IS_CONVERSATION(avatar->conversation)) { + g_signal_handlers_disconnect_by_func(avatar->conversation, + pidgin_avatar_conversation_updated, + if(g_set_object(&avatar->conversation, conversation)) { + g_object_notify_by_pspec(G_OBJECT(avatar), + properties[PROP_CONVERSATION]); + /* Add the notify signal so we can update when the icon changes. */ + if(PURPLE_IS_CONVERSATION(avatar->conversation)) { + g_signal_connect(G_OBJECT(avatar->conversation), "notify", + G_CALLBACK(pidgin_avatar_conversation_updated), avatar); +pidgin_avatar_get_conversation(PidginAvatar *avatar) { + g_return_val_if_fail(PIDGIN_IS_AVATAR(avatar), NULL); + return avatar->conversation; --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/pidgin/pidginavatar.h Fri Mar 05 03:31:29 2021 -0600
@@ -0,0 +1,151 @@
+ * Pidgin - Internet Messenger + * Copyright (C) Pidgin Developers <devel@pidgin.im> + * Pidgin is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <https://www.gnu.org/licenses/>. +#if !defined(PIDGIN_GLOBAL_HEADER_INSIDE) && !defined(PIDGIN_COMPILATION) +# error "only <pidgin.h> may be included directly" + * @section_id: pidgin-pidginavatar + * @short_description: Avatars + * @title: Avatar Widget + * #PidginAvatar is a widget that displays avatars for contacts or + * The standard _get_type macro for #PidginAvatar. +#define PIDGIN_TYPE_AVATAR (pidgin_avatar_get_type()) +G_DECLARE_FINAL_TYPE(PidginAvatar, pidgin_avatar, + PIDGIN, AVATAR, GtkEventBox) + * #PidginAvatar is an opaque data structure and should only be accessed via + * Creates a new #PidginAvatar instance. + * Returns: (transfer full): The new #PidginAvatar instance. +GtkWidget *pidgin_avatar_new(void); + * pidgin_avatar_set_animate: + * @avatar: The #PidginAvatar instance. + * @animate: Whether or not to animate the avatar. + * Starts or stops animation of @avatar. If avatar is displaying a + * non-animated image, changing this will do nothing unless a new animated +void pidgin_avatar_set_animate(PidginAvatar *avatar, gboolean animate); + * pidgin_avatar_get_animate: + * @avatar: The #PidginAvatar instance. + * Gets whether or not @avatar should be animated. + * Returns: Whether or not @avatar should be animated. +gboolean pidgin_avatar_get_animate(PidginAvatar *avatar); + * pidgin_avatar_set_buddy: + * @avatar: The #PidginAvatar instance. + * @buddy: (nullable): The #PurpleBuddy to set or %NULL to unset. + * Sets or unsets the #PurpleBuddy that @avatar should use for display. +void pidgin_avatar_set_buddy(PidginAvatar *avatar, PurpleBuddy *buddy); + * pidgin_avatar_get_buddy: + * @avatar: The #PidginAvatar instance. + * Gets the #PurpleBuddy that @avatar is using for display. + * Returns: (transfer none): The #PurpleBuddy that @avatar is using for display. +PurpleBuddy *pidgin_avatar_get_buddy(PidginAvatar *avatar); + * pidgin_avatar_set_conversation: + * @avatar: The #PidginAvatar instance. + * @conversation: (nullable): The #PurpleConversation to set or %NULL to unset. + * Sets or unsets the #PurpleConversation that @avatar uses to find the + * #PurpleBuddy whose icon to display. +void pidgin_avatar_set_conversation(PidginAvatar *avatar, PurpleConversation *conversation); + * pidgin_avatar_get_conversation: + * @avatar: The #PidginAvatar instance. + * Gets the #PurpleConversation that @avatar is using for display. + * Returns: (transfer none): The #PurpleConversation that @avatar is using to + * find the #PurpleBuddy whose icon to display. +PurpleConversation *pidgin_avatar_get_conversation(PidginAvatar *avatar); +#endif /* PIDGIN_AVATAR_H */ --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/pidgin/resources/Avatar/avatar.ui Fri Mar 05 03:31:29 2021 -0600
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.38.2 +Pidgin - Internet Messenger +Copyright (C) Pidgin Developers <devel@pidgin.im> +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +You should have received a copy of the GNU General Public License +along with this program; if not, see <https://www.gnu.org/licenses/>. + <requires lib="gtk+" version="3.20"/> + <!-- interface-license-type gplv2 --> + <!-- interface-name Pidgin --> + <!-- interface-description Internet Messenger --> + <!-- interface-copyright Pidgin Developers <devel@pidgin.im> --> + <template class="PidginAvatar" parent="GtkEventBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK | GDK_STRUCTURE_MASK</property> + <signal name="button-press-event" handler="pidgin_avatar_button_press_handler" object="PidginAvatar" swapped="no"/> + <signal name="enter-notify-event" handler="pidgin_avatar_enter_notify_handler" swapped="no"/> + <signal name="leave-notify-event" handler="pidgin_avatar_leave_notify_handler" swapped="no"/> + <object class="GtkImage" id="icon"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="icon-name">image-missing</property> + <property name="icon_size">3</property> --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/pidgin/resources/Avatar/menu.ui Fri Mar 05 03:31:29 2021 -0600
@@ -0,0 +1,23 @@
+ <attribute name="label" translatable="yes">Animate</attribute> + <attribute name="action">avatar.animate</attribute> + <attribute name="label" translatable="yes">Save Avatar As...</attribute> + <attribute name="action">avatar.save-avatar</attribute> + <attribute name="label" translatable="yes">Set Custom Avatar...</attribute> + <attribute name="action">avatar.set-custom-avatar</attribute> + <attribute name="label" translatable="yes">Clear Custom Avatar</attribute> + <attribute name="action">avatar.clear-custom-avatar</attribute> --- a/pidgin/resources/pidgin.gresource.xml Thu Mar 04 22:43:51 2021 -0600
+++ b/pidgin/resources/pidgin.gresource.xml Fri Mar 05 03:31:29 2021 -0600
@@ -9,6 +9,8 @@
<file compressed="true">Accounts/chooser.ui</file>
<file compressed="true">Accounts/entry.css</file>
<file compressed="true">Accounts/menu.ui</file>
+ <file compressed="true">Avatar/avatar.ui</file> + <file compressed="true">Avatar/menu.ui</file> <file compressed="true">BuddyList/window.ui</file>
<file compressed="true">Conversations/invite_dialog.ui</file>
<file compressed="true">Conversations/menu.ui</file>
--- a/po/POTFILES.in Thu Mar 04 22:43:51 2021 -0600
+++ b/po/POTFILES.in Fri Mar 05 03:31:29 2021 -0600
@@ -347,6 +347,7 @@
pidgin/pidginactiongroup.c
pidgin/pidginapplication.c
pidgin/pidginattachment.c
pidgin/pidgincellrendererexpander.c
pidgin/pidginclosebutton.c
@@ -394,6 +395,8 @@
pidgin/resources/Accounts/actionsmenu.ui
pidgin/resources/Accounts/chooser.ui
pidgin/resources/Accounts/menu.ui
+pidgin/resources/Avatar/avatar.ui +pidgin/resources/Avatar/menu.ui pidgin/resources/BuddyList/window.ui
pidgin/resources/closebutton.ui
pidgin/resources/Conversations/invite_dialog.ui