1 /*
2  * Album (Buddy Icon Archiver)
3  *
4  * Copyright (C) 2005-2008, Sadrul Habib Chowdhury <imadil@gmail.com>
5  * Copyright (C) 2005-2008, Richard Laager <rlaager@pidgin.im>
6  * Copyright (C) 2006, Jérôme Poulin (TiCPU) <jeromepoulin@gmail.com>
7  *
8  * This program is free software; you can redistribute it and/or
9  * modify it under the terms of the GNU General Public License as
10  * published by the Free Software Foundation; either version 2 of the
11  * License, or (at your option) any later version.
12  *
13  * This program is distributed in the hope that it will be useful, but
14  * WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16  * General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License
19  * along with this program; if not, write to the Free Software
20  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
21  * 02111-1301, USA.
22  */
23 
24 #include "album-ui.h"
25 
26 /* We want to use the gstdio functions when possible so that non-ASCII
27  * filenames are handled properly on Windows. */
28 #include <glib/gstdio.h>
29 
30 #include <gtk/gtk.h>
31 #include <gtk/gtkwidget.h>
32 
33 #include <sys/stat.h>
34 #include <string.h>
35 
36 #include <account.h>
37 #include <debug.h>
38 #include <gtkblist.h>
39 #include <pidgin.h>
40 #include <plugin.h>
41 #include <request.h>
42 #include <util.h>
43 
44 /* XXX: For DATADIR... There must be a better way. */
45 #ifdef _WIN32
46 #include <win32/win32dep.h>
47 #endif
48 
49 /* The increment between sizes. */
50 #ifndef ICON_SIZE_MULTIPLIER
51 #define ICON_SIZE_MULTIPLIER 32
52 #endif
53 
54 /* The size of the icon in the icon viewer's label. */
55 #ifndef LABEL_ICON_SIZE
56 #define LABEL_ICON_SIZE 24
57 #endif
58 
59 /* How much to pad between two rows. */
60 #ifndef ROW_PADDING
61 #define ROW_PADDING 0
62 #endif
63 
64 /* Padding around the vbox containing the image and text. */
65 #ifndef VBOX_BORDER
66 #define VBOX_BORDER 10
67 #endif
68 
69 /* Spacing between children in the vbox containing the image and text. */
70 #ifndef VBOX_SPACING
71 #define VBOX_SPACING 5
72 #endif
73 
74 /* Padding around the text label for an icon. */
75 #ifndef LABEL_PADDING
76 #define LABEL_PADDING 3
77 #endif
78 
79 /* Padding around the image. */
80 #ifndef IMAGE_PADDING
81 #define IMAGE_PADDING 3
82 #endif
83 
84 typedef struct _BuddyIcon BuddyIcon;
85 typedef struct _BuddyWindow BuddyWindow;
86 typedef struct _icon_viewer_key icon_viewer_key;
87 
88 struct _BuddyIcon
89 {
90 	char *full_path;
91 	time_t timestamp;
92 
93 	/* For suggesting a filename on image save. */
94 	char *buddy_name;
95 };
96 
97 struct _BuddyWindow
98 {
99 	GtkWidget *window;
100 	GtkWidget *vbox;
101 	GtkWidget *iconview;
102 	GtkTextBuffer *text_buffer;
103 	int text_height;
104 	int text_width;
105 	GtkRequisition requisition;
106 };
107 
108 /* contact or ((account and screenname) and possibly buddy) will be set.*/
109 struct _icon_viewer_key
110 {
111 	PurpleContact *contact;
112 
113 	PurpleBuddy *buddy;
114 
115 	PurpleAccount *account;
116 	char *screenname;
117 	GList *list;
118 };
119 
120 static void update_icon_view(icon_viewer_key *key);
121 static void show_buddy_icon_window(icon_viewer_key *key, const char *name);
122 
icon_viewer_key_free(void * data)123 void icon_viewer_key_free(void *data)
124 {
125 	icon_viewer_key *key = (icon_viewer_key *)data;
126 	g_free(key->screenname);
127 	g_free(key);
128 }
129 
icon_viewer_hash(gconstpointer data)130 guint icon_viewer_hash(gconstpointer data)
131 {
132 	const icon_viewer_key *key = data;
133 
134 	if (key->contact != NULL)
135 		return g_direct_hash(key->contact);
136 
137 	return g_str_hash(key->screenname) +
138 		g_str_hash(purple_account_get_username(key->account));
139 }
140 
icon_viewer_equal(gconstpointer y,gconstpointer z)141 gboolean icon_viewer_equal(gconstpointer y, gconstpointer z)
142 {
143 	const icon_viewer_key *a, *b;
144 
145 	a = y;
146 	b = z;
147 
148 	if (a->contact != NULL)
149 	{
150 		if (b->contact != NULL)
151 			return (a->contact == b->contact);
152 		else
153 			return FALSE;
154 	}
155 	else if (b->contact != NULL)
156 		return FALSE;
157 
158 	if (a->account == b->account)
159 	{
160 		char *normal = g_strdup(purple_normalize(a->account, a->screenname));
161 		if (!strcmp(normal, purple_normalize(b->account, b->screenname)))
162 		{
163 			g_free(normal);
164 			return TRUE;
165 		}
166 		g_free(normal);
167 	}
168 
169 	return FALSE;
170 }
171 
set_window_geometry(BuddyWindow * bw,int buddy_icon_size)172 static void set_window_geometry(BuddyWindow *bw, int buddy_icon_size)
173 {
174 	GdkGeometry geom;
175 
176 	g_return_if_fail(bw != NULL);
177 
178 	/* Set the window geometry.  This controls window resizing. */
179 	geom.base_width  = bw->requisition.width  + 40; /* XXX: Where is the hardcoded value coming from? */
180 	geom.base_height = bw->requisition.height + 18; /* XXX: Where is the hardcoded value coming from? */
181 	geom.width_inc   = MAX(buddy_icon_size, bw->text_width) + 2 * VBOX_BORDER;
182 	geom.height_inc  = ROW_PADDING + 2 * VBOX_BORDER + buddy_icon_size + 2 * IMAGE_PADDING + VBOX_SPACING + bw->text_height + 2 * LABEL_PADDING;
183 	geom.min_width   = geom.base_width  + 3 * geom.width_inc;  /* Minimum size: 3 wide */
184 	geom.min_height  = geom.base_height +     geom.height_inc; /* Minimum size: 1 high */
185 	gtk_window_set_geometry_hints(GTK_WINDOW(bw->window), bw->vbox, &geom,
186 	                              GDK_HINT_MIN_SIZE | GDK_HINT_RESIZE_INC | GDK_HINT_BASE_SIZE);
187 }
188 
resize_icons(GtkWidget * combo,icon_viewer_key * key)189 static gboolean resize_icons(GtkWidget *combo, icon_viewer_key *key)
190 {
191 	int sel = gtk_combo_box_get_active(GTK_COMBO_BOX(combo));
192 	BuddyWindow *bw;
193 
194 	switch(sel)
195 	{
196 		case 0: /* Small */
197 		case 1: /* Medium */
198 		case 2: /* Large */
199 			purple_prefs_set_int(PREF_ICON_SIZE, sel);
200 			break;
201 		default:
202 			g_return_val_if_reached(FALSE);
203 	}
204 	update_icon_view(key);
205 
206 	bw = g_hash_table_lookup(buddy_windows, key);
207 	g_return_val_if_fail(bw != NULL, FALSE);
208 	set_window_geometry(bw, ICON_SIZE_MULTIPLIER * (sel + 1));
209 
210 	return FALSE;
211 }
212 
213 /* Save the size of the window. */
update_size(GtkWidget * win,GdkEventConfigure * event,gpointer data)214 static gboolean update_size(GtkWidget *win, GdkEventConfigure *event, gpointer data)
215 {
216 	int w;
217 	int h;
218 
219 	gtk_window_get_size(GTK_WINDOW(win), &w, &h);
220 
221 	purple_prefs_set_int(PREF_WINDOW_WIDTH, w);
222 	purple_prefs_set_int(PREF_WINDOW_HEIGHT, h);
223 
224 	/* We want the normal handling to continue. */
225 	return FALSE;
226 }
227 
window_close(GtkWidget * win,gint resp,icon_viewer_key * key)228 static gboolean window_close(GtkWidget *win, gint resp, icon_viewer_key *key)
229 {
230 	g_hash_table_remove(buddy_windows, key);
231 	gtk_widget_destroy(win);
232 	return TRUE;
233 }
234 
235 /* Returns a scroller window which contains widget. */
wrap_in_scroller(GtkWidget * widget)236 static GtkWidget *wrap_in_scroller(GtkWidget *widget)
237 {
238 	GtkWidget *scroller = gtk_scrolled_window_new(NULL, NULL);
239 
240 	gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroller),
241 	                               GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
242 
243 	gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scroller),
244 	                                    GTK_SHADOW_IN);
245 
246 	gtk_container_add(GTK_CONTAINER(scroller), widget);
247 
248 	return scroller;
249 }
250 
251 /* Based on Purple's gtkimhtml.c, image_save_yes_cb(). */
convert_image(GtkImage * image,const char * filename)252 static void convert_image(GtkImage *image, const char *filename)
253 {
254 	gchar *type = NULL;
255 	GError *error = NULL;
256 	GSList *formats = gdk_pixbuf_get_formats();
257 
258 	while (formats)
259 	{
260 		GdkPixbufFormat *format = formats->data;
261 		gchar **extensions = gdk_pixbuf_format_get_extensions(format);
262 		gpointer p = extensions;
263 
264 		while (gdk_pixbuf_format_is_writable(format) && extensions && extensions[0])
265 		{
266 			gchar *fmt_ext = extensions[0];
267 			const gchar *file_ext = filename + strlen(filename) - strlen(fmt_ext);
268 
269 			if (!strcmp(fmt_ext, file_ext))
270 			{
271 				type = gdk_pixbuf_format_get_name(format);
272 				break;
273 			}
274 
275 			extensions++;
276 		}
277 
278 		g_strfreev(p);
279 
280 		if (type)
281 			break;
282 
283 		formats = formats->next;
284 	}
285 
286 	g_slist_free(formats);
287 
288 	if (!type)
289 	{
290 		GtkWidget *dialog;
291 
292 		/* Present an error dialog. */
293 		dialog = gtk_message_dialog_new_with_markup(NULL, 0, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE,
294 		                                            _("<span size='larger' weight='bold'>Unrecognized file type</span>\n\nDefaulting to PNG."));
295 		g_signal_connect_swapped(dialog, "response", G_CALLBACK (gtk_widget_destroy), dialog);
296 		gtk_widget_show(dialog);
297 
298 		/* Assume the user wants a PNG. */
299 		type = g_strdup("png");
300 	}
301 
302 	gdk_pixbuf_save(gtk_image_get_pixbuf(image), filename, type, &error, NULL);
303 
304 	if (error)
305 	{
306 		GtkWidget *dialog;
307 
308 		/* Present an error dialog. */
309 		dialog = gtk_message_dialog_new_with_markup(NULL, 0, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK,
310 		                                            _("<span size='larger' weight='bold'>Error saving image</span>\n\n%s"),
311 		                                            error->message);
312 		g_signal_connect_swapped(dialog, "response", G_CALLBACK (gtk_widget_destroy), dialog);
313 		gtk_widget_show(dialog);
314 
315 		g_error_free(error);
316 	}
317 
318 	g_free(type);
319 }
320 
321 static void
image_save_cb(GtkWidget * widget,gint response,GtkImage * image)322 image_save_cb(GtkWidget *widget, gint response, GtkImage *image)
323 {
324 	char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(widget));
325 	char *image_name = g_object_get_data(G_OBJECT(image), "filename");
326 
327 	gtk_widget_destroy(widget);
328 
329 	if (response != GTK_RESPONSE_ACCEPT)
330 		return;
331 
332 	purple_debug_misc(PLUGIN_STATIC_NAME, "Saving image %s as: %s\n", image_name, filename);
333 
334 	/* Keeping this crud out of this function is a Good Thing (TM). */
335 	convert_image(image, filename);
336 
337 	g_free(filename);
338 }
339 
save_dialog(GtkWidget * widget,GtkImage * image)340 static void save_dialog(GtkWidget *widget, GtkImage *image)
341 {
342 	GtkWidget *dialog;
343 	const char *ext;
344 	char *filename;
345 
346 	dialog = gtk_file_chooser_dialog_new(_("Save Image"),
347 	                                     NULL,
348 	                                     GTK_FILE_CHOOSER_ACTION_SAVE,
349 	                                     GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
350 	                                     GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT,
351 	                                     NULL);
352 	gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT);
353 
354 	/* Determine the extension. */
355 	ext = g_object_get_data(G_OBJECT(image), "filename");
356 	if (ext)
357 		ext = strrchr(ext, '.');
358 	if (ext == NULL)
359 		ext = "";
360 
361 	filename = g_strdup_printf("%s%s", purple_escape_filename(g_object_get_data(G_OBJECT(image), "buddy_name")), ext);
362 	gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog), filename);
363 	g_free(filename);
364 
365 	g_signal_connect(G_OBJECT(GTK_FILE_CHOOSER(dialog)), "response",
366 					 G_CALLBACK(image_save_cb), image);
367 
368 	gtk_widget_show(dialog);
369 }
370 
save_menu(GtkWidget * event_box,GdkEventButton * event,GtkImage * image)371 static gboolean save_menu(GtkWidget *event_box, GdkEventButton *event, GtkImage *image)
372 {
373 	GtkWidget *menu = gtk_menu_new();
374 	GtkWidget *item = gtk_image_menu_item_new_with_mnemonic("_Save Icon");
375 
376 	gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(item),
377 	                              gtk_image_new_from_stock(GTK_STOCK_SAVE, GTK_ICON_SIZE_MENU));
378 
379 	gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
380 	g_signal_connect(G_OBJECT(item), "activate", G_CALLBACK(save_dialog), image);
381 
382 	gtk_widget_show_all(menu);
383 
384 	gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, event_box, 3, event->time);
385 
386 	return FALSE;
387 }
388 
buddy_icon_compare(BuddyIcon * b1,BuddyIcon * b2)389 static gint buddy_icon_compare(BuddyIcon *b1, BuddyIcon *b2)
390 {
391 	gint ret;
392 
393 	/* NOTE: This sorts by timestamp DESCENDING. */
394 	if ((ret = b2->timestamp - b1->timestamp) != 0)
395 		return ret;
396 
397 	/* This isn't strictly necessary, but it
398 	 * ensures the icon order is stable. */
399 	return strcmp(b1->full_path, b2->full_path);
400 }
401 
402 /* Returns an unsorted list of available icons for buddy (as BuddyIcon *).
403  * The items need to be freed.
404  */
retrieve_icons(PurpleAccount * account,const char * name)405 static GList *retrieve_icons(PurpleAccount *account, const char *name)
406 {
407 	char *path;
408 	GDir *dir;
409 	const char *filename;
410 	GList *list = NULL;
411 
412 	path = album_buddy_icon_get_dir(account, name);
413 	if (path == NULL)
414 	{
415 		purple_debug_warning(PLUGIN_STATIC_NAME, "Path for buddy %s not found.\n", name);
416 		return NULL;
417 	}
418 
419 	if (!(dir = g_dir_open(path, 0, NULL)))
420 	{
421 		purple_debug_warning(PLUGIN_STATIC_NAME, "Could not open path: %s\n", path);
422 		g_free(path);
423 		return NULL;
424 	}
425 
426 	while ((filename = g_dir_read_name(dir)))
427 	{
428 		char *full_path = g_build_filename(path, filename, NULL);
429 		struct stat st;
430 		BuddyIcon *icon;
431 
432 		/* We need the file's timestamp. */
433 		if (stat(full_path, &st) != 0)
434 		{
435 			g_free(full_path);
436 			continue;
437 		}
438 
439 		icon = g_new0(BuddyIcon, 1);
440 		icon->full_path = full_path;
441 		icon->timestamp = st.st_mtime;
442 		icon->buddy_name = g_strdup(name);
443 
444 		list = g_list_prepend(list, icon);
445 	}
446 
447 	g_dir_close(dir);
448 
449 	g_free(path);
450 	return list;
451 }
452 
add_icon_from_list_cb(gpointer data)453 static gboolean add_icon_from_list_cb(gpointer data)
454 {
455 	GtkTextIter text_iter;
456 	int buddy_icon_pref = purple_prefs_get_int(PREF_ICON_SIZE);
457 	int buddy_icon_size;
458 	BuddyWindow *bw;
459 	GtkTextBuffer *text_buffer;
460 	GtkTextIter start, end;
461 	GtkWidget *iconview;
462 	icon_viewer_key *key = data;
463 	BuddyIcon *icon;
464 	GdkPixbuf *pixbuf;
465 	int width;
466 	int height;
467 	int xpad = 0;
468 	int ypad = 0;
469 	GdkPixbuf *scaled;
470 	GtkWidget *image;
471 	GtkWidget *event_box;
472 	GtkWidget *alignment;
473 	GtkWidget *widget;
474 	GtkWidget *label;
475 	const char *timestamp;
476 	GtkTextChildAnchor *anchor;
477 	BuddyIcon *prev_icon;
478 	char *prev_icon_filename;
479 	GList *l;
480 
481 
482 	/* If we're out of icons, kill this idle callback. */
483 	if (key->list == NULL)
484 		return FALSE;
485 
486 	bw = g_hash_table_lookup(buddy_windows, key);
487 	g_return_val_if_fail(bw != NULL, FALSE);
488 
489 	text_buffer = bw->text_buffer;
490 	iconview = bw->iconview;
491 
492 	/* Clamp the pref value to the allowable range. */
493 	buddy_icon_pref = CLAMP(buddy_icon_pref, 0, 2);
494 
495 	buddy_icon_size = ICON_SIZE_MULTIPLIER * (buddy_icon_pref + 1);
496 
497 	gtk_text_buffer_get_end_iter(text_buffer, &text_iter);
498 
499 
500 	/* Duplicate Removal */
501 
502 	prev_icon = key->list->data;
503 	/* Technically, this will yield "/filename.ext", but the
504 	 * code below will get the same thing, so it'll compare fine. */
505 	prev_icon_filename = strrchr(prev_icon->full_path, '/');
506 	if (prev_icon_filename == NULL)
507 	{
508 		/* This should never happen. */
509 		prev_icon_filename = prev_icon->full_path;
510 	}
511 
512 	for (l = key->list->next ; l != NULL ; l = l->next)
513 	{
514 		BuddyIcon *this_icon = l->data;
515 		char *this_icon_filename = strrchr(this_icon->full_path, '/');
516 
517 		if (this_icon_filename == NULL)
518 		{
519 			/* This should never happen. */
520 			this_icon_filename = this_icon->full_path;
521 		}
522 
523 		/* The files are named by hash, so if they have the
524 		 * same basename, we can assume they have the same
525 		 * contents.  This happens when someone uses the
526 		 * same icon on multiple accounts.  We only want to
527 		 * show each icon once.
528 		 */
529 		if (!strcmp(this_icon_filename, prev_icon_filename))
530 		{
531 			key->list = g_list_delete_link(key->list, l);
532 		}
533 	}
534 
535 
536 	/* Pop off one icon to add. */
537 	icon = key->list->data;
538 	key->list = g_list_delete_link(key->list, key->list);
539 
540 	pixbuf = gdk_pixbuf_new_from_file(icon->full_path, NULL);
541 	if (pixbuf == NULL)
542 	{
543 		purple_debug_warning(PLUGIN_STATIC_NAME, "Invalid image file: %s\n", icon->full_path);
544 		g_free(icon->full_path);
545 		g_free(icon->buddy_name);
546 		g_free(icon);
547 		return TRUE;
548 	}
549 
550 	width = gdk_pixbuf_get_width(pixbuf);
551 	height = gdk_pixbuf_get_height(pixbuf);
552 
553 	/* Never scale the image up. */
554 	if (height > buddy_icon_size || width > buddy_icon_size)
555 	{
556 		/* Scale the image proportionally to fit with a square of size buddy_icon_size. */
557 		if (width > height)
558 		{
559 				int new_height = (int)(buddy_icon_size / (double)width * height);
560 				ypad = buddy_icon_size - new_height;
561 				scaled = gdk_pixbuf_scale_simple(pixbuf, buddy_icon_size, new_height, GDK_INTERP_BILINEAR);
562 		}
563 		else
564 		{
565 				int new_width = (int)(buddy_icon_size / (double)height * width);
566 				xpad = buddy_icon_size - new_width;
567 				scaled = gdk_pixbuf_scale_simple(pixbuf, new_width, buddy_icon_size, GDK_INTERP_BILINEAR);
568 		}
569 		g_object_unref(G_OBJECT(pixbuf));
570 	}
571 	else
572 	{
573 		ypad = buddy_icon_size - height;
574 		xpad = buddy_icon_size - width;
575 		scaled = pixbuf;
576 	}
577 
578 
579 	/* Now we're ready to add the icon. */
580 
581 	/* Create the image and an event box to which to attach the right-click menu. */
582 	image = gtk_image_new_from_pixbuf(scaled);
583 	g_object_unref(G_OBJECT(scaled));
584 	event_box = gtk_event_box_new();
585 	gtk_event_box_set_visible_window(GTK_EVENT_BOX(event_box), FALSE);
586 	gtk_container_add(GTK_CONTAINER(event_box), image);
587 
588 	/* Save the filename and create a right-click handler. */
589 	g_object_set_data_full(G_OBJECT(image), "buddy_name", icon->buddy_name, g_free);
590 	g_object_set_data_full(G_OBJECT(image), "filename", icon->full_path, g_free);
591 	g_signal_connect(G_OBJECT(event_box), "button-press-event",
592 					 G_CALLBACK(save_menu), image);
593 
594 	/* Add padding as required. */
595 	alignment = gtk_alignment_new(0.5, 0.5, 0, 0);
596 	/* The + 1 is in case the padding is odd. It ensures that one side will get the extra one pixel so that
597 	 * the padding is always correct. Without it, we'd be one pixel short whenever xpad or ypad was odd. */
598 	gtk_alignment_set_padding(GTK_ALIGNMENT(alignment), ypad / 2, (ypad + 1) / 2, xpad / 2, (xpad + 1) / 2);
599 	gtk_container_add(GTK_CONTAINER(alignment), event_box);
600 
601 	widget = gtk_vbox_new(FALSE, VBOX_SPACING);
602 	gtk_container_set_border_width(GTK_CONTAINER(widget), VBOX_BORDER);
603 	gtk_box_pack_start(GTK_BOX(widget), alignment, FALSE, FALSE, IMAGE_PADDING);
604 
605 	/* Label */
606 	timestamp = purple_utf8_strftime(_("%x\n%X"),  localtime(&icon->timestamp));
607 	label = gtk_label_new(NULL);
608 	gtk_label_set_text(GTK_LABEL(label), timestamp);
609 	gtk_label_set_justify(GTK_LABEL(label), GTK_JUSTIFY_CENTER);
610 	gtk_box_pack_start(GTK_BOX(widget), label, TRUE, TRUE, LABEL_PADDING);
611 
612 	anchor = gtk_text_buffer_create_child_anchor(text_buffer, &text_iter);
613 	gtk_text_view_add_child_at_anchor(GTK_TEXT_VIEW(iconview), widget, anchor);
614 
615 	gtk_widget_show_all(widget);
616 	gtk_text_buffer_get_bounds(text_buffer, &start, &end);
617 	gtk_text_buffer_apply_tag_by_name(text_buffer, "word_wrap", &start, &end);
618 
619 	g_free(icon);
620 	return TRUE;
621 }
622 
update_icon_view(icon_viewer_key * key)623 static void update_icon_view(icon_viewer_key *key)
624 {
625 	GtkTextIter start;
626 	GtkTextIter end;
627 	GList *list = NULL;
628 	BuddyWindow *bw;
629 	GtkWidget *iconview;
630 	GtkTextBuffer *text_buffer;
631 	gboolean success = FALSE;
632 
633 	bw = g_hash_table_lookup(buddy_windows, key);
634 	g_return_if_fail(bw != NULL);
635 	iconview = bw->iconview;
636 	text_buffer = bw->text_buffer;
637 
638 	gtk_text_buffer_get_bounds(text_buffer, &start, &end);
639 	gtk_text_buffer_delete(text_buffer, &start, &end);
640 
641 	if (key->contact != NULL)
642 	{
643 		PurpleBlistNode *bnode;
644 
645 		/* Get the list of all the accounts of the contact and sort it. */
646 		for (bnode = ((PurpleBlistNode *)key->contact)->child; bnode ; bnode = bnode->next)
647 		{
648 			list = g_list_concat(retrieve_icons(purple_buddy_get_account((PurpleBuddy *)bnode),
649 			                                    purple_buddy_get_name((PurpleBuddy *)bnode)), list);
650 		}
651 	}
652 	else if (key->buddy != NULL)
653 	{
654 		list = retrieve_icons(purple_buddy_get_account(key->buddy),
655 		                      purple_buddy_get_name(key->buddy));
656 	}
657 	else
658 	{
659 		list = retrieve_icons(key->account, key->screenname);
660 	}
661 
662 	/* Show icons for all the accounts of the contact. */
663 	if (list != NULL)
664 	{
665 		int id;
666 
667 		list = g_list_sort(list, (GCompareFunc)buddy_icon_compare);
668 		success = (list != NULL);
669 		key->list = list;
670 
671 		/* It's possible we already have one of these loops running, but
672 		 * having two won't harm anything, so we don't do anything about it. */
673 		id = g_idle_add(add_icon_from_list_cb, key);
674 		g_object_set_data_full(G_OBJECT(iconview), "update-idle-callback",
675 					GINT_TO_POINTER(id), (GDestroyNotify)g_source_remove);
676 	}
677 
678 	if (!success)
679 	{
680 		/* No icons were found.  Display an appropriate message. */
681 		GtkWidget *hbox;
682 		char *filename;
683 		GdkPixbuf *pixbuf;
684 		GdkPixbuf *scaled;
685 		GtkWidget *image;
686 		char *str;
687 		GtkWidget *label;
688 		GtkTextIter text_iter;
689 		GtkTextChildAnchor *anchor;
690 
691 		/* Hbox */
692 		/* Reuse spacing information that's used for the vbox which contains the image and text for each icon. */
693 		hbox = gtk_hbox_new(FALSE, VBOX_SPACING);
694 		gtk_container_set_border_width(GTK_CONTAINER(hbox), VBOX_BORDER);
695 
696 		/* Image */
697 #ifndef _WIN32
698 		filename = g_build_filename(PIXMAPSDIR, "dialogs", "purple_info.png", NULL);
699 #else
700 		filename = g_build_filename(wpurple_install_dir(), "pixmaps", "pidgin", "dialogs", "purple_info.png", NULL);
701 #endif
702 
703 		pixbuf = gdk_pixbuf_new_from_file(filename, NULL);
704 		g_free(filename);
705 
706 		scaled = gdk_pixbuf_scale_simple(pixbuf, 48, 48, GDK_INTERP_BILINEAR);
707 		g_object_unref(G_OBJECT(pixbuf));
708 
709 		image = gtk_image_new_from_pixbuf(scaled);
710 		g_object_unref(G_OBJECT(scaled));
711 		gtk_box_pack_start(GTK_BOX(hbox), image, FALSE, FALSE, 0);
712 
713 
714 		/* Label */
715 		str = g_strdup_printf("<span size='larger' weight='bold'>%s</span>", _("No icons were found."));
716 		label = gtk_label_new(NULL);
717 		gtk_label_set_markup(GTK_LABEL(label), str);
718 		g_free(str);
719 
720 		gtk_misc_set_alignment(GTK_MISC(label), 0, 0.5);
721 		gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0);
722 
723 
724 		/* Add the hbox to the text view. */
725 		gtk_text_buffer_get_iter_at_offset (text_buffer, &text_iter, 0);
726 		anchor = gtk_text_buffer_create_child_anchor(text_buffer, &text_iter);
727 		gtk_text_view_add_child_at_anchor(GTK_TEXT_VIEW(iconview), hbox, anchor);
728 	}
729 
730 	gtk_widget_show_all(iconview);
731 	gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(iconview), FALSE);
732 }
733 
update_runtime(icon_viewer_key * key,gpointer value,PurpleBuddy * buddy)734 static void update_runtime(icon_viewer_key *key, gpointer value, PurpleBuddy *buddy)
735 {
736 	PurpleAccount *account = purple_buddy_get_account(buddy);
737 
738 	if (key->contact != NULL)
739 	{
740 		char *name = g_strdup(purple_normalize(account, purple_buddy_get_name(buddy)));
741 		PurpleBlistNode *node;
742 
743 		for (node = ((PurpleBlistNode *)key->contact)->child; node; node = node->next)
744 		{
745 			if (account == purple_buddy_get_account((PurpleBuddy *)node) &&
746 			    !strcmp(name, purple_normalize(account, purple_buddy_get_name((PurpleBuddy *)node))))
747 			{
748 				g_free(name);
749 				update_icon_view(key);
750 				return;
751 			}
752 		}
753 		g_free(name);
754 	}
755 	else if (account == key->account &&
756 	         !strcmp(key->screenname, purple_normalize(account, purple_buddy_get_name(buddy))))
757 	{
758 		update_icon_view(key);
759 	}
760 }
761 
album_update_runtime(PurpleBuddy * buddy,gpointer data)762 void album_update_runtime(PurpleBuddy *buddy, gpointer data)
763 {
764 	g_hash_table_foreach(buddy_windows, (GHFunc)update_runtime, buddy);
765 }
766 
get_viewer_icon()767 static GtkWidget *get_viewer_icon()
768 {
769 	char *filename = NULL;
770 	GdkPixbuf *pixbuf;
771 	int width;
772 	int height;
773 	GdkPixbuf *scaled;
774 	GtkWidget *image;
775 
776 	if (filename == NULL)
777 #ifndef _WIN32
778 		filename = g_build_filename(PIXMAPSDIR, "icons", "online.png", NULL);
779 #else
780 		filename = g_build_filename(wpurple_install_dir(), "pixmaps", "pidgin", "icons", "online.png", NULL);
781 #endif
782 
783 	pixbuf = gdk_pixbuf_new_from_file(filename, NULL);
784 	g_free(filename);
785 
786 	width = gdk_pixbuf_get_width(pixbuf);
787 	height = gdk_pixbuf_get_height(pixbuf);
788 
789 	/* Never scale the image up. */
790 	if (height > LABEL_ICON_SIZE || width > LABEL_ICON_SIZE)
791 	{
792 		/* Scale the image proportionally to fit with a square of size buddy_icon_size. */
793 		if (width > height)
794 		{
795 				int new_height = (int)(LABEL_ICON_SIZE / (double)width * height);
796 				scaled = gdk_pixbuf_scale_simple(pixbuf, LABEL_ICON_SIZE, new_height, GDK_INTERP_BILINEAR);
797 		}
798 		else
799 		{
800 				int new_width = (int)(LABEL_ICON_SIZE / (double)height * width);
801 				scaled = gdk_pixbuf_scale_simple(pixbuf, new_width, LABEL_ICON_SIZE, GDK_INTERP_BILINEAR);
802 		}
803 		g_object_unref(G_OBJECT(pixbuf));
804 	}
805 	else
806 	{
807 		scaled = pixbuf;
808 	}
809 
810 	image = gtk_image_new_from_pixbuf(scaled);
811 	g_object_unref(G_OBJECT(scaled));
812 	return image;
813 }
814 
view_buddy_icons_cb(PurpleBlistNode * node,gpointer data)815 static void view_buddy_icons_cb(PurpleBlistNode *node, gpointer data)
816 {
817 	gboolean contact_expanded;
818 	icon_viewer_key *key = g_new0(icon_viewer_key, 1);
819 	const char *name;
820 
821 	g_return_if_fail(node != NULL);
822 
823 	if(PURPLE_BLIST_NODE_HAS_FLAG(node, PURPLE_BLIST_NODE_FLAG_NO_SAVE))
824 		return;
825 
826 	contact_expanded = pidgin_blist_node_is_contact_expanded(node);
827 
828 	if (PURPLE_BLIST_NODE_IS_BUDDY(node))
829 	{
830 		if (!contact_expanded)
831 		{
832 			/* Work on the contact, since it's collapsed. */
833 
834 			name = purple_contact_get_alias((PurpleContact*)node->parent);
835 			if (name == NULL)
836 				name = purple_buddy_get_name(((PurpleContact *)node->parent)->priority);
837 
838 			if (node->next != NULL)
839 			{
840 				/* The contact has at least two buddies.  */
841 				key->contact = (PurpleContact *)node->parent;
842 			}
843 			else
844 			{
845 				/* Treat one-buddy contacts similar to buddies. */
846 				key->account = purple_buddy_get_account((PurpleBuddy *)node);
847 				key->screenname = g_strdup(purple_normalize(key->account, purple_buddy_get_name((PurpleBuddy *)node)));
848 				key->buddy = (PurpleBuddy *)node;
849 			}
850 		}
851 		else
852 		{
853 			/* The contact is expanded and the user has chosen a buddy. */
854 
855 			key->account = purple_buddy_get_account((PurpleBuddy *)node);
856 			key->screenname = g_strdup(purple_normalize(key->account, purple_buddy_get_name((PurpleBuddy *)node)));
857 			key->buddy = (PurpleBuddy *)node;
858 
859 			name = purple_buddy_get_alias_only((PurpleBuddy *)node);
860 			if (name == NULL)
861 			{
862 				name = purple_buddy_get_name((PurpleBuddy *)node);
863 			}
864 		}
865 	}
866 	else if (PURPLE_BLIST_NODE_IS_CONTACT(node))
867 	{
868 		/* The contact is expanded and the user has chosen the contact. */
869 
870 		if (node->child != NULL && node->child->next != NULL)
871 		{
872 			/* The contact has at least two buddies.  */
873 			key->contact = (PurpleContact *)node;
874 		}
875 		else
876 		{
877 			/* Treat one-buddy contacts similar to buddies. */
878 			key->account = purple_buddy_get_account((PurpleBuddy *)node->child);
879 			key->screenname = g_strdup(purple_normalize(key->account, purple_buddy_get_name((PurpleBuddy *)node->child)));
880 			key->buddy = (PurpleBuddy *)node->child;
881 		}
882 
883 		name = purple_contact_get_alias((PurpleContact*)node);
884 		if (name == NULL)
885 			name = purple_buddy_get_name(((PurpleContact *)node)->priority);
886 	}
887 	else
888 		g_return_if_reached();
889 
890 	show_buddy_icon_window(key, name);
891 }
892 
compare_buddy_keys(icon_viewer_key * key1,BuddyWindow * bw,icon_viewer_key * key2)893 static gboolean compare_buddy_keys(icon_viewer_key *key1, BuddyWindow *bw, icon_viewer_key *key2)
894 {
895 	g_return_val_if_fail(key2->contact == NULL, FALSE);
896 
897 	if (key1->contact == NULL)
898 	{
899 		if (key1->account == key2->account)
900 		{
901 			char *normal = g_strdup(purple_normalize(key1->account, key1->screenname));
902 			if (!strcmp(normal, purple_normalize(key2->account, key2->screenname)))
903 			{
904 				g_free(normal);
905 				return TRUE;
906 			}
907 			g_free(normal);
908 		}
909 	}
910 
911 	return FALSE;
912 }
913 
show_buddy_icon_window(icon_viewer_key * key,const char * name)914 static void show_buddy_icon_window(icon_viewer_key *key, const char *name)
915 {
916 	char *title;
917 	GtkWidget *win;
918 	GtkWidget *vbox;
919 	char *str;
920 	GtkTextIter start;
921 	GtkTextIter end;
922 	GtkWidget *title_box;
923 	GtkWidget *label;
924 	GtkWidget *combo;
925 	GtkWidget *iconview;
926 	GtkTextBuffer *text_buffer;
927 	time_t now;
928 	const char *timestamp;
929 	PangoLayout *layout;
930 	int text_width;
931 	int text_height;
932 	int buddy_icon_pref = purple_prefs_get_int(PREF_ICON_SIZE);
933 	int buddy_icon_size;
934 	BuddyWindow *bw;
935 
936 	/* Return if a window is already opened for the buddy. */
937 	if ((bw = g_hash_table_lookup(buddy_windows, key)) != NULL)
938 	{
939 		icon_viewer_key_free(key);
940 		gtk_window_present(GTK_WINDOW(bw->window));
941 		return;
942 	}
943 
944 	/* If it was a contact, it would've matched above.
945 	 * Compare screennames...
946 	 */
947 	if (key->contact == NULL &&
948 	    (bw = g_hash_table_find(buddy_windows, (GHRFunc)compare_buddy_keys, key)) != NULL)
949 	{
950 		icon_viewer_key_free(key);
951 		gtk_window_present(GTK_WINDOW(bw->window));
952 		return;
953 	}
954 
955 	/* Clamp the pref value to the allowable range. */
956 	buddy_icon_pref = MAX(0, buddy_icon_pref);
957 	buddy_icon_pref = MIN(2, buddy_icon_pref);
958 
959 	buddy_icon_size = ICON_SIZE_MULTIPLIER * (buddy_icon_pref + 1);
960 
961 	title = g_strdup_printf(_("Buddy Icons used by %s"), name);
962 
963 	win = gtk_dialog_new_with_buttons(title, NULL, 0,
964 	                                  GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE, NULL);
965 	gtk_window_set_role(GTK_WINDOW(win), "buddy_icon_viewer");
966 	gtk_container_set_border_width(GTK_CONTAINER(win), 12);
967 
968 	vbox = gtk_vbox_new(FALSE, 5);
969 	gtk_box_pack_start(GTK_BOX(GTK_DIALOG(win)->vbox), vbox, TRUE, TRUE, 0);
970 
971 	iconview = gtk_text_view_new();
972 	text_buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(iconview));
973 	gtk_text_view_set_editable(GTK_TEXT_VIEW(iconview), FALSE);
974 #if ROW_PADDING != 0
975 	gtk_text_view_set_pixels_inside_wrap(GTK_TEXT_VIEW(iconview), ROW_PADDING);
976 #endif
977 	gtk_text_buffer_create_tag (text_buffer, "word_wrap",
978 	                            "wrap_mode", GTK_WRAP_WORD, NULL);
979 	gtk_text_buffer_get_bounds(text_buffer, &start, &end);
980 	gtk_text_buffer_apply_tag_by_name(text_buffer, "word_wrap", &start, &end);
981 
982 	/* Get the size of the text of a sample timestamp. */
983 	now = time(NULL);
984 	timestamp = purple_utf8_strftime("%x\n%X",  localtime(&now));
985 	layout = gtk_widget_create_pango_layout(iconview, timestamp);
986 	pango_layout_get_pixel_size(layout, &text_width, &text_height);
987 
988 
989 	/* Title Box */
990 	title_box = gtk_hbox_new(FALSE, 6);
991 	gtk_container_set_border_width(GTK_CONTAINER(title_box), 6);
992 	gtk_box_pack_start(GTK_BOX(vbox), title_box, FALSE, FALSE, 0);
993 
994 
995 	/* Icon */
996 	gtk_box_pack_start(GTK_BOX(title_box), get_viewer_icon(), FALSE, FALSE, 0);
997 
998 
999 	/* Label */
1000 	str = g_strdup_printf("<span size='larger' weight='bold'>%s</span>", title);
1001 	g_free(title);
1002 
1003 	label = gtk_label_new(NULL);
1004 	gtk_label_set_markup(GTK_LABEL(label), str);
1005 	g_free(str);
1006 
1007 	gtk_misc_set_alignment(GTK_MISC(label), 0, 0);
1008 	gtk_box_pack_start(GTK_BOX(title_box), label, FALSE, FALSE, 0);
1009 
1010 
1011 	/* Icon View */
1012 	gtk_box_pack_start(GTK_BOX(vbox), wrap_in_scroller(iconview), TRUE, TRUE, 0);
1013 
1014 
1015 	/* Size Selector */
1016 	combo = gtk_combo_box_new_text();
1017 
1018 	str = g_strdup_printf(_("Small (%1$ux%1$u)"), ICON_SIZE_MULTIPLIER);
1019 	gtk_combo_box_append_text(GTK_COMBO_BOX(combo), str);
1020 	g_free(str);
1021 
1022 	str = g_strdup_printf(_("Medium (%1$ux%1$u)"), ICON_SIZE_MULTIPLIER * 2);
1023 	gtk_combo_box_append_text(GTK_COMBO_BOX(combo), str);
1024 	g_free(str);
1025 
1026 	str = g_strdup_printf(_("Large (%1$ux%1$u)"), ICON_SIZE_MULTIPLIER * 3);
1027 	gtk_combo_box_append_text(GTK_COMBO_BOX(combo), str);
1028 	g_free(str);
1029 
1030 	gtk_combo_box_set_active(GTK_COMBO_BOX(combo), buddy_icon_pref);
1031 
1032 	gtk_widget_show_all(combo);
1033 	gtk_signal_connect(GTK_OBJECT(combo), "changed", GTK_SIGNAL_FUNC(resize_icons), key);
1034 
1035 	gtk_box_pack_start(GTK_BOX(GTK_DIALOG(win)->action_area), combo, FALSE, FALSE, 0);
1036 	gtk_box_reorder_child(GTK_BOX(GTK_DIALOG(win)->action_area), combo, 0);
1037 
1038 	/* Add the stuff in the hashtable. */
1039 	bw = g_new0(BuddyWindow, 1);
1040 	bw->window = win;
1041 	bw->vbox = vbox;
1042 	bw->iconview = iconview;
1043 	bw->text_buffer = text_buffer;
1044 	bw->text_height = text_height;
1045 	bw->text_width = text_width;
1046 	g_hash_table_insert(buddy_windows, key, bw);
1047 
1048 	update_icon_view(key);
1049 
1050 	gtk_widget_size_request(bw->iconview, &bw->requisition);
1051 	set_window_geometry(bw, buddy_icon_size);
1052 
1053 	gtk_window_set_default_size(GTK_WINDOW(win),
1054 	                            purple_prefs_get_int(PREF_WINDOW_WIDTH),
1055 	                            purple_prefs_get_int(PREF_WINDOW_HEIGHT));
1056 
1057 	gtk_window_set_policy(GTK_WINDOW(win), FALSE, TRUE, FALSE);
1058 	gtk_widget_show_all(win);
1059 
1060 #if 0
1061 	/* TODO: We need a way to disconnect this signal handler when the window is closed.
1062 	 * TODO: Otherwise, it segfaults in update_runtime. */
1063 
1064 	/* Register a signal handler to update the viewer if a new buddy icon is cached. */
1065 	purple_signal_connect_priority(purple_buddy_icons_get_handle(), "buddy-icon-cached",
1066 	                             purple_plugins_find_with_id(PLUGIN_ID), PURPLE_CALLBACK(update_runtime),
1067 	                             key, PURPLE_SIGNAL_PRIORITY_DEFAULT + 1);
1068 #endif
1069 
1070 	gtk_signal_connect(GTK_OBJECT(win), "configure_event",
1071 	                   GTK_SIGNAL_FUNC(update_size), NULL);
1072 	g_signal_connect(G_OBJECT(win), "response",
1073 	                 G_CALLBACK(window_close), key);
1074 }
1075 
has_stored_icons(PurpleBuddy * buddy)1076 static gboolean has_stored_icons(PurpleBuddy *buddy)
1077 {
1078 	char *path = album_buddy_icon_get_dir(purple_buddy_get_account(buddy),
1079 	                                      purple_buddy_get_name(buddy));
1080 	GDir *dir = g_dir_open(path, 0, NULL);
1081 
1082 	g_free(path);
1083 
1084 	if (dir)
1085 	{
1086 		if (g_dir_read_name(dir))
1087 		{
1088 			g_dir_close(dir);
1089 			return TRUE;
1090 		}
1091 		g_dir_close(dir);
1092 	}
1093 
1094 	return FALSE;
1095 }
1096 
1097 static void
album_select_dialog_cb(gpointer data,PurpleRequestFields * fields)1098 album_select_dialog_cb(gpointer data, PurpleRequestFields *fields)
1099 {
1100 	char *username;
1101 	PurpleAccount *account;
1102 
1103 	account  = purple_request_fields_get_account(fields, "account");
1104 
1105 	username = g_strdup(purple_normalize(account,
1106 		purple_request_fields_get_string(fields,  "screenname")));
1107 
1108 	if (username != NULL && *username != '\0' && account != NULL )
1109 	{
1110 		icon_viewer_key *key = g_new0(icon_viewer_key, 1);
1111 		key->account = account;
1112 		key->screenname = username;
1113 
1114 		show_buddy_icon_window(key, username);
1115 	}
1116 }
1117 
1118 /* Based on Pidgin's gtkdialogs.c, pidgindialogs_log(). */
album_select_dialog(PurplePluginAction * action)1119 static void album_select_dialog(PurplePluginAction *action)
1120 {
1121 	PurpleRequestFields *fields;
1122 	PurpleRequestFieldGroup *group;
1123 	PurpleRequestField *field;
1124 
1125 	fields = purple_request_fields_new();
1126 
1127 	group = purple_request_field_group_new(NULL);
1128 	purple_request_fields_add_group(fields, group);
1129 
1130 	field = purple_request_field_string_new("screenname", _("_Name"), NULL, FALSE);
1131 	purple_request_field_set_type_hint(field, "screenname-all");
1132 	purple_request_field_set_required(field, TRUE);
1133 	purple_request_field_group_add_field(group, field);
1134 
1135 	field = purple_request_field_account_new("account", _("_Account"), NULL);
1136 	purple_request_field_set_type_hint(field, "account");
1137 	purple_request_field_account_set_show_all(field, TRUE);
1138 	purple_request_field_set_visible(field, (purple_accounts_get_all() != NULL &&
1139 	                                       purple_accounts_get_all()->next != NULL));
1140 	purple_request_field_set_required(field, TRUE);
1141 	purple_request_field_group_add_field(group, field);
1142 
1143 	purple_request_fields(purple_get_blist(), _("View Buddy Icons..."),
1144 	                    NULL,
1145 	                    _("Please enter the screen name or alias of the person whose icon album you want to view."),
1146 	                    fields,
1147 	                    _("OK"), G_CALLBACK(album_select_dialog_cb),
1148 	                    _("Cancel"), NULL,
1149 	                    NULL, NULL, NULL,
1150 	                    NULL);
1151 }
1152 
album_get_plugin_actions(PurplePlugin * plugin,gpointer data)1153 GList *album_get_plugin_actions(PurplePlugin *plugin, gpointer data)
1154 {
1155 	GList *actions = NULL;
1156 
1157 	actions = g_list_append(actions, purple_plugin_action_new(_("View Buddy Icons"), album_select_dialog));
1158 
1159 	return actions;
1160 }
1161 
album_blist_node_menu_cb(PurpleBlistNode * node,GList ** menu)1162 void album_blist_node_menu_cb(PurpleBlistNode *node, GList **menu)
1163 {
1164 	gboolean contact_expanded;
1165 	PurpleMenuAction *action;
1166 	void (*callback)() = view_buddy_icons_cb;
1167 
1168 	if (!(PURPLE_BLIST_NODE_IS_CONTACT(node) || PURPLE_BLIST_NODE_IS_BUDDY(node)))
1169 		return;
1170 
1171 	contact_expanded = pidgin_blist_node_is_contact_expanded(node);
1172 
1173 	if (PURPLE_BLIST_NODE_IS_BUDDY(node))
1174 	{
1175 		if (contact_expanded)
1176 		{
1177 			if (!has_stored_icons((PurpleBuddy *)node))
1178 			{
1179 				callback = NULL;
1180 			}
1181 		}
1182 		else if (PURPLE_BLIST_NODE_IS_CONTACT(node))
1183 		{
1184 			/* We don't want to show this option in buddy submenus. */
1185 			if ((PurpleBlistNode *)((PurpleContact *)node->parent)->priority != node)
1186 				return;
1187 
1188 			/* Find the contact and fall through to the contact handling code. */
1189 			node = node->parent;
1190 		}
1191 	}
1192 
1193 	if (PURPLE_BLIST_NODE_IS_CONTACT(node))
1194 	{
1195 		PurpleBlistNode *bnode;
1196 
1197 		for (bnode = node->child; bnode; bnode = bnode->next)
1198 		{
1199 			if (has_stored_icons((PurpleBuddy *)bnode))
1200 				break;
1201 		}
1202 
1203 		/* bnode == NULL when the for loop made it all the way through. */
1204 		if (bnode == NULL)
1205 		{
1206 			/* No icons found. */
1207 			callback = NULL;
1208 		}
1209 	}
1210 
1211 	/* Separator */
1212 	(*menu) = g_list_append(*menu, NULL);
1213 
1214 	action = purple_menu_action_new(_("_View Buddy Icons"), PURPLE_CALLBACK(callback), NULL, NULL);
1215 	(*menu) = g_list_append(*menu, action);
1216 }
1217