pidgin/pidgin

Create a PidginAvatar widget.

2021-03-05, Gary Kramlich
28d50eece92d
Parents 691bd780ab06
Children 44c107f92a65
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 @@
g_object_unref(old_img);
}
+gboolean
+purple_buddy_icon_save_to_filename(PurpleBuddyIcon *icon,
+ const gchar *filename, GError **error)
+{
+ gconstpointer data;
+ size_t len;
+
+ data = purple_buddy_icon_get_data(icon, &len);
+
+ return g_file_set_contents(filename, data, len, error);
+}
+
+
PurpleAccount *
purple_buddy_icon_get_account(const PurpleBuddyIcon *icon)
{
@@ -517,6 +530,18 @@
return NULL;
}
+GInputStream *
+purple_buddy_icon_get_stream(PurpleBuddyIcon *icon) {
+ gconstpointer data = NULL;
+ size_t len = 0;
+
+ 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);
+}
+
const char *
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
+
+#include <glib.h>
+#include <gio/gio.h>
+
/**
* SECTION:buddyicon
* @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.
+ *
+ * Since: 3.0.0
+ */
+gboolean purple_buddy_icon_save_to_filename(PurpleBuddyIcon *icon, const gchar *filename, GError **error);
+
+/**
* purple_buddy_icon_get_account:
* @icon: The buddy icon.
*
@@ -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.
+ *
+ * Since: 3.0.0
+ */
+GInputStream *purple_buddy_icon_get_stream(PurpleBuddyIcon *icon);
+
+/**
* purple_buddy_icon_get_extension:
* @icon: The buddy icon.
*
--- 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 "gtkprefs.h"
#include "gtkprivacy.h"
#include "gtkutils.h"
+#include "pidginavatar.h"
#include "pidginclosebutton.h"
#include "pidginconversationwindow.h"
#include "pidgincore.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 @@
if (contact) {
buddyicon_size = purple_blist_node_get_int((PurpleBlistNode*)contact, "pidgin-infopane-iconsize");
}
+
+ pidgin_avatar_set_buddy(avatar, buddy);
+ } else {
+ 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 @@
'pidginactiongroup.c',
'pidginapplication.c',
'pidginattachment.c',
+ 'pidginavatar.c',
'pidgincellrendererexpander.c',
'pidginclosebutton.c',
'pidgincontactcompletion.c',
@@ -104,6 +105,7 @@
'pidginactiongroup.h',
'pidginapplication.h',
'pidginattachment.h',
+ 'pidginavatar.h',
'pidgincellrendererexpander.h',
'pidginclosebutton.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
+ * source distribution.
+ *
+ * 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
+ * as well.
+ */
+#define PIDGIN_AVATAR_ANIMATE_ACTION "animate"
+
+struct _PidginAvatar {
+ GtkEventBox parent;
+
+ GtkWidget *icon;
+
+ GdkPixbufAnimation *animation;
+ gboolean animate;
+
+ PurpleBuddy *buddy;
+ PurpleConversation *conversation;
+};
+
+enum {
+ PROP_0,
+ PROP_ANIMATE,
+ PROP_BUDDY,
+ PROP_CONVERSATION,
+ N_PROPERTIES,
+};
+static GParamSpec *properties[N_PROPERTIES] = {NULL, };
+
+G_DEFINE_TYPE(PidginAvatar, pidgin_avatar, GTK_TYPE_EVENT_BOX)
+
+/******************************************************************************
+ * Actions
+ *****************************************************************************/
+static void
+pidgin_avatar_animate_toggle(GSimpleAction *action, GVariant *value,
+ gpointer data)
+{
+ PidginAvatar *avatar = PIDGIN_AVATAR(data);
+
+ pidgin_avatar_set_animate(avatar, g_variant_get_boolean(value));
+ g_simple_action_set_state(action, value);
+}
+
+static void
+pidgin_avatar_save_response_cb(GtkNativeDialog *native, gint response,
+ gpointer data)
+{
+ PidginAvatar *avatar = PIDGIN_AVATAR(data);
+ PurpleBuddyIcon *icon = NULL;
+
+ if(response != GTK_RESPONSE_ACCEPT || !PURPLE_IS_BUDDY(avatar->buddy)) {
+ gtk_native_dialog_destroy(native);
+
+ return;
+ }
+
+ icon = purple_buddy_get_icon(avatar->buddy);
+
+ if(icon != NULL) {
+ 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);
+
+ g_free(filename);
+ }
+
+ gtk_native_dialog_destroy(native);
+}
+
+static void
+pidgin_avatar_save_cb(GSimpleAction *action, GVariant *parameter,
+ gpointer data)
+{
+ 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"),
+ window,
+ GTK_FILE_CHOOSER_ACTION_SAVE,
+ _("_Save"),
+ _("_Cancel"));
+ 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);
+ g_free(filename);
+
+ gtk_native_dialog_show(GTK_NATIVE_DIALOG(native));
+}
+
+static void
+pidgin_avatar_set_custom_response_cb(GtkNativeDialog *native, gint response,
+ gpointer data)
+{
+ 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);
+
+ return;
+ }
+
+ filename = gtk_file_chooser_get_filename(chooser);
+ if(filename != NULL) {
+ 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);
+}
+
+static void
+pidgin_avatar_set_custom_cb(GSimpleAction *action, GVariant *parameter,
+ gpointer data)
+{
+ 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"),
+ window,
+ GTK_FILE_CHOOSER_ACTION_OPEN,
+ _("_Set Custom"),
+ _("_Cancel"));
+
+ g_signal_connect(G_OBJECT(native), "response",
+ G_CALLBACK(pidgin_avatar_set_custom_response_cb), avatar);
+
+ gtk_native_dialog_show(GTK_NATIVE_DIALOG(native));
+}
+
+static void
+pidgin_avatar_clear_custom_cb(GSimpleAction *action, GVariant *parameter,
+ gpointer data)
+{
+ 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,
+ .state = "false",
+ .change_state = pidgin_avatar_animate_toggle,
+ }, {
+ .name = "save-avatar",
+ .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,
+ },
+};
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+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,
+ NULL);
+ }
+ }
+
+ /* If there is no custom icon, fall back to checking if the buddy has an
+ * icon set.
+ */
+ if(!G_IS_INPUT_STREAM(stream)) {
+ PurpleBuddyIcon *icon = purple_buddy_get_icon(buddy);
+
+ if(icon != NULL) {
+ 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));
+
+ if(icon != NULL) {
+ 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);
+ }
+
+ return ret;
+}
+
+static void
+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)) {
+ if(avatar->animate) {
+ gtk_image_set_from_animation(GTK_IMAGE(avatar->icon),
+ avatar->animation);
+ } else {
+ GdkPixbuf *frame = NULL;
+
+ frame = gdk_pixbuf_animation_get_static_image(avatar->animation);
+
+ gtk_image_set_from_pixbuf(GTK_IMAGE(avatar->icon), frame);
+ }
+ } else {
+ gtk_image_clear(GTK_IMAGE(avatar->icon));
+ }
+
+ g_clear_object(&animation);
+}
+
+/******************************************************************************
+ * Callbacks
+ *****************************************************************************/
+static gboolean
+pidgin_avatar_button_press_handler(GtkWidget *widget, GdkEventButton *event,
+ gpointer data)
+{
+ PidginAvatar *avatar = PIDGIN_AVATAR(data);
+ GtkBuilder *builder = NULL;
+ GtkWidget *menu = NULL;
+ GMenuModel *model = NULL;
+
+ if(!gdk_event_triggers_context_menu((GdkEvent *)event)) {
+ return FALSE;
+ }
+
+ 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);
+
+ return TRUE;
+}
+
+/*
+ * 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.
+ */
+static void
+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.
+ */
+static void
+pidgin_avatar_conversation_updated(GObject *obj, GParamSpec *pspec, gpointer d)
+{
+ PidginAvatar *avatar = PIDGIN_AVATAR(d);
+
+ pidgin_avatar_update(avatar);
+}
+
+static gboolean
+pidgin_avatar_enter_notify_handler(GtkWidget *widget, GdkEvent *event,
+ gpointer data)
+{
+ PidginAvatar *avatar = PIDGIN_AVATAR(widget);
+
+ pidgin_avatar_set_animate(avatar, TRUE);
+
+ return FALSE;
+}
+
+static gboolean
+pidgin_avatar_leave_notify_handler(GtkWidget *widget, GdkEvent *event,
+ gpointer user_data)
+{
+ 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);
+ }
+ }
+
+ return FALSE;
+}
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+static void
+pidgin_avatar_get_property(GObject *obj, guint param_id, GValue *value,
+ GParamSpec *pspec)
+{
+ PidginAvatar *avatar = PIDGIN_AVATAR(obj);
+
+ switch(param_id) {
+ case PROP_ANIMATE:
+ g_value_set_boolean(value, pidgin_avatar_get_animate(avatar));
+ break;
+ case PROP_BUDDY:
+ g_value_set_object(value, pidgin_avatar_get_buddy(avatar));
+ break;
+ case PROP_CONVERSATION:
+ g_value_set_object(value, pidgin_avatar_get_conversation(avatar));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+ break;
+ }
+}
+
+static void
+pidgin_avatar_set_property(GObject *obj, guint param_id, const GValue *value,
+ GParamSpec *pspec)
+{
+ PidginAvatar *avatar = PIDGIN_AVATAR(obj);
+
+ switch(param_id) {
+ case PROP_ANIMATE:
+ pidgin_avatar_set_animate(avatar, g_value_get_boolean(value));
+ break;
+ case PROP_BUDDY:
+ pidgin_avatar_set_buddy(avatar, g_value_get_object(value));
+ break;
+ case PROP_CONVERSATION:
+ pidgin_avatar_set_conversation(avatar, g_value_get_object(value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+ break;
+ }
+}
+
+static void
+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);
+}
+
+static void
+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));
+}
+
+static void
+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(
+ "animate", "animate",
+ "Whether or not to animate an animated avatar",
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * PidginAvatar::buddy:
+ *
+ * The #PurpleBuddy whose avatar will be displayed.
+ */
+ properties[PROP_BUDDY] = g_param_spec_object(
+ "buddy", "buddy",
+ "The buddy whose avatar to display",
+ PURPLE_TYPE_BUDDY,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * PidginAvatar::conversation:
+ *
+ * The #PurpleConversation which will be used to find the correct
+ * #PurpleBuddy.
+ */
+ 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(
+ widget_class,
+ "/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);
+}
+
+/******************************************************************************
+ * API
+ *****************************************************************************/
+GtkWidget *
+pidgin_avatar_new(void) {
+ return GTK_WIDGET(g_object_new(PIDGIN_TYPE_AVATAR, NULL));
+}
+
+void
+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)) {
+ if(avatar->animate) {
+ gtk_image_set_from_animation(GTK_IMAGE(avatar->icon),
+ avatar->animation);
+ } else {
+ GdkPixbuf *frame = NULL;
+
+ frame = gdk_pixbuf_animation_get_static_image(avatar->animation);
+
+ gtk_image_set_from_pixbuf(GTK_IMAGE(avatar->icon), frame);
+ }
+ }
+}
+
+gboolean
+pidgin_avatar_get_animate(PidginAvatar *avatar) {
+ g_return_val_if_fail(PIDGIN_IS_AVATAR(avatar), FALSE);
+
+ return avatar->animate;
+}
+
+void
+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,
+ avatar);
+ }
+
+ 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);
+ }
+}
+
+PurpleBuddy *
+pidgin_avatar_get_buddy(PidginAvatar *avatar) {
+ g_return_val_if_fail(PIDGIN_IS_AVATAR(avatar), NULL);
+
+ return avatar->buddy;
+}
+
+void
+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,
+ avatar);
+ }
+
+ 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);
+ }
+}
+
+PurpleConversation *
+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
+ * source distribution.
+ *
+ * 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"
+#endif
+
+#ifndef PIDGIN_AVATAR_H
+#define PIDGIN_AVATAR_H
+
+/**
+ * SECTION:pidginavatar
+ * @section_id: pidgin-pidginavatar
+ * @short_description: Avatars
+ * @title: Avatar Widget
+ *
+ * #PidginAvatar is a widget that displays avatars for contacts or
+ * conversations.
+ */
+
+#include <glib.h>
+
+#include <gtk/gtk.h>
+
+#include <purple.h>
+
+G_BEGIN_DECLS
+
+/**
+ * PIDGIN_TYPE_AVATAR:
+ *
+ * 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:
+ *
+ * #PidginAvatar is an opaque data structure and should only be accessed via
+ * its API.
+ */
+
+/**
+ * pidgin_avatar_new:
+ *
+ * Creates a new #PidginAvatar instance.
+ *
+ * Returns: (transfer full): The new #PidginAvatar instance.
+ *
+ * Since: 3.0.0
+ */
+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
+ * image is set.
+ *
+ * Since: 3.0.0
+ */
+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.
+ *
+ * Since: 3.0.0
+ */
+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.
+ *
+ * Since: 3.0.0
+ */
+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.
+ *
+ * Since: 3.0.0
+ */
+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.
+ *
+ * Since: 3.0.0
+ */
+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.
+ *
+ * Since: 3.0.0
+ */
+PurpleConversation *pidgin_avatar_get_conversation(PidginAvatar *avatar);
+
+G_END_DECLS
+
+#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/>.
+
+-->
+<interface>
+ <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"/>
+ <child>
+ <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>
+ </object>
+ </child>
+ </template>
+</interface>
--- /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 @@
+<?xml version="1.0"?>
+<interface>
+ <menu id="menu">
+ <section>
+ <item>
+ <attribute name="label" translatable="yes">Animate</attribute>
+ <attribute name="action">avatar.animate</attribute>
+ </item>
+ <item>
+ <attribute name="label" translatable="yes">Save Avatar As...</attribute>
+ <attribute name="action">avatar.save-avatar</attribute>
+ </item>
+ <item>
+ <attribute name="label" translatable="yes">Set Custom Avatar...</attribute>
+ <attribute name="action">avatar.set-custom-avatar</attribute>
+ </item>
+ <item>
+ <attribute name="label" translatable="yes">Clear Custom Avatar</attribute>
+ <attribute name="action">avatar.clear-custom-avatar</attribute>
+ </item>
+ </section>
+ </menu>
+</interface>
--- 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/pidginavatar.c
pidgin/pidgin.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