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