I guess I'll release this later today. Is that ok with everyone?
[pidgin-git.git] / pidgin / gtkconv.c
blob4f0121d93136d158437b13410c2951f181459ab5
1 /**
2 * @file gtkconv.c GTK+ Conversation API
3 * @ingroup pidgin
4 */
6 /* pidgin
8 * Pidgin is the legal property of its developers, whose names are too numerous
9 * to list here. Please refer to the COPYRIGHT file distributed with this
10 * source distribution.
12 * This program is free software; you can redistribute it and/or modify
13 * it under the terms of the GNU General Public License as published by
14 * the Free Software Foundation; either version 2 of the License, or
15 * (at your option) any later version.
17 * This program is distributed in the hope that it will be useful,
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 * GNU General Public License for more details.
22 * You should have received a copy of the GNU General Public License
23 * along with this program; if not, write to the Free Software
24 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
27 #define _PIDGIN_GTKCONV_C_
29 #include "internal.h"
30 #include "pidgin.h"
32 #ifndef _WIN32
33 # include <X11/Xlib.h>
34 #endif
36 #ifdef USE_GTKSPELL
37 # include <gtkspell/gtkspell.h>
38 # ifdef _WIN32
39 # include "wspell.h"
40 # endif
41 #endif
43 #include <gdk/gdkkeysyms.h>
45 #include "account.h"
46 #include "cmds.h"
47 #include "core.h"
48 #include "debug.h"
49 #include "idle.h"
50 #include "imgstore.h"
51 #include "log.h"
52 #include "notify.h"
53 #include "prpl.h"
54 #include "request.h"
55 #include "util.h"
56 #include "version.h"
58 #include "gtkdnd-hints.h"
59 #include "gtkblist.h"
60 #include "gtkconv.h"
61 #include "gtkconvwin.h"
62 #include "gtkdialogs.h"
63 #include "gtkimhtml.h"
64 #include "gtkimhtmltoolbar.h"
65 #include "gtklog.h"
66 #include "gtkmenutray.h"
67 #include "gtkpounce.h"
68 #include "gtkprefs.h"
69 #include "gtkprivacy.h"
70 #include "gtkthemes.h"
71 #include "gtkutils.h"
72 #include "pidginstock.h"
73 #include "pidgintooltip.h"
75 #include "gtknickcolors.h"
77 #define CLOSE_CONV_TIMEOUT_SECS (10 * 60)
79 #define AUTO_RESPONSE "&lt;AUTO-REPLY&gt; : "
81 typedef enum
83 PIDGIN_CONV_SET_TITLE = 1 << 0,
84 PIDGIN_CONV_BUDDY_ICON = 1 << 1,
85 PIDGIN_CONV_MENU = 1 << 2,
86 PIDGIN_CONV_TAB_ICON = 1 << 3,
87 PIDGIN_CONV_TOPIC = 1 << 4,
88 PIDGIN_CONV_SMILEY_THEME = 1 << 5,
89 PIDGIN_CONV_COLORIZE_TITLE = 1 << 6
90 }PidginConvFields;
92 enum {
93 CONV_ICON_COLUMN,
94 CONV_TEXT_COLUMN,
95 CONV_EMBLEM_COLUMN,
96 CONV_PROTOCOL_ICON_COLUMN,
97 CONV_NUM_COLUMNS
98 } PidginInfopaneColumns;
100 #define PIDGIN_CONV_ALL ((1 << 7) - 1)
102 /* XXX: These color defines shouldn't really be here. But the nick-color
103 * generation algorithm uses them, so keeping these around until we fix that. */
104 #define DEFAULT_SEND_COLOR "#204a87"
105 #define DEFAULT_HIGHLIGHT_COLOR "#AF7F00"
107 #define BUDDYICON_SIZE_MIN 32
108 #define BUDDYICON_SIZE_MAX 96
110 /* Undef this to turn off "custom-smiley" debug messages */
111 #define DEBUG_CUSTOM_SMILEY
113 #define LUMINANCE(c) (float)((0.3*(c.red))+(0.59*(c.green))+(0.11*(c.blue)))
115 /* From http://www.w3.org/TR/AERT#color-contrast */
116 #define MIN_BRIGHTNESS_CONTRAST 75
117 #define MIN_COLOR_CONTRAST 200
119 #define NUM_NICK_COLORS 220
120 static GdkColor *nick_colors = NULL;
121 static guint nbr_nick_colors;
123 typedef struct {
124 GtkWidget *window;
126 GtkWidget *entry;
127 GtkWidget *message;
129 PurpleConversation *conv;
131 } InviteBuddyInfo;
133 static GtkWidget *invite_dialog = NULL;
134 static GtkWidget *warn_close_dialog = NULL;
136 static PidginWindow *hidden_convwin = NULL;
137 static GList *window_list = NULL;
139 /* Lists of status icons at all available sizes for use as window icons */
140 static GList *available_list = NULL;
141 static GList *away_list = NULL;
142 static GList *busy_list = NULL;
143 static GList *xa_list = NULL;
144 static GList *offline_list = NULL;
145 static GHashTable *prpl_lists = NULL;
147 static gboolean update_send_to_selection(PidginWindow *win);
148 static void generate_send_to_items(PidginWindow *win);
150 /* Prototypes. <-- because Paco-Paco hates this comment. */
151 static gboolean infopane_entry_activate(PidginConversation *gtkconv);
152 static void got_typing_keypress(PidginConversation *gtkconv, gboolean first);
153 static void gray_stuff_out(PidginConversation *gtkconv);
154 static void add_chat_buddy_common(PurpleConversation *conv, PurpleConvChatBuddy *cb, const char *old_name);
155 static gboolean tab_complete(PurpleConversation *conv);
156 static void pidgin_conv_updated(PurpleConversation *conv, PurpleConvUpdateType type);
157 static void conv_set_unseen(PurpleConversation *gtkconv, PidginUnseenState state);
158 static void gtkconv_set_unseen(PidginConversation *gtkconv, PidginUnseenState state);
159 static void update_typing_icon(PidginConversation *gtkconv);
160 static void update_typing_message(PidginConversation *gtkconv, const char *message);
161 static const char *item_factory_translate_func (const char *path, gpointer func_data);
162 gboolean pidgin_conv_has_focus(PurpleConversation *conv);
163 static GdkColor* generate_nick_colors(guint *numcolors, GdkColor background);
164 static gboolean color_is_visible(GdkColor foreground, GdkColor background, int color_contrast, int brightness_contrast);
165 static GtkTextTag *get_buddy_tag(PurpleConversation *conv, const char *who, PurpleMessageFlags flag, gboolean create);
166 static void pidgin_conv_update_fields(PurpleConversation *conv, PidginConvFields fields);
167 static void focus_out_from_menubar(GtkWidget *wid, PidginWindow *win);
168 static void pidgin_conv_tab_pack(PidginWindow *win, PidginConversation *gtkconv);
169 static gboolean infopane_press_cb(GtkWidget *widget, GdkEventButton *e, PidginConversation *conv);
170 static void hide_conv(PidginConversation *gtkconv, gboolean closetimer);
172 static void pidgin_conv_set_position_size(PidginWindow *win, int x, int y,
173 int width, int height);
174 static gboolean pidgin_conv_xy_to_right_infopane(PidginWindow *win, int x, int y);
176 static const GdkColor *get_nick_color(PidginConversation *gtkconv, const char *name)
178 static GdkColor col;
179 GtkStyle *style = gtk_widget_get_style(gtkconv->imhtml);
180 float scale;
182 col = nick_colors[g_str_hash(name) % nbr_nick_colors];
183 scale = ((1-(LUMINANCE(style->base[GTK_STATE_NORMAL]) / LUMINANCE(style->white))) *
184 (LUMINANCE(style->white)/MAX(MAX(col.red, col.blue), col.green)));
186 /* The colors are chosen to look fine on white; we should never have to darken */
187 if (scale > 1) {
188 col.red *= scale;
189 col.green *= scale;
190 col.blue *= scale;
193 return &col;
196 static PurpleBlistNode *
197 get_conversation_blist_node(PurpleConversation *conv)
199 PurpleBlistNode *node = NULL;
201 switch (purple_conversation_get_type(conv)) {
202 case PURPLE_CONV_TYPE_IM:
203 node = PURPLE_BLIST_NODE(purple_find_buddy(conv->account, conv->name));
204 node = node ? node->parent : NULL;
205 break;
206 case PURPLE_CONV_TYPE_CHAT:
207 node = PURPLE_BLIST_NODE(purple_blist_find_chat(conv->account, conv->name));
208 break;
209 default:
210 break;
212 return node;
215 /**************************************************************************
216 * Callbacks
217 **************************************************************************/
219 static gboolean
220 close_this_sucker(gpointer data)
222 PidginConversation *gtkconv = data;
223 GList *list = g_list_copy(gtkconv->convs);
224 g_list_foreach(list, (GFunc)purple_conversation_destroy, NULL);
225 g_list_free(list);
226 return FALSE;
229 static gboolean
230 close_conv_cb(GtkButton *button, PidginConversation *gtkconv)
232 /* We are going to destroy the conversations immediately only if the 'close immediately'
233 * preference is selected. Otherwise, close the conversation after a reasonable timeout
234 * (I am going to consider 10 minutes as a 'reasonable timeout' here.
235 * For chats, close immediately if the chat is not in the buddylist, or if the chat is
236 * not marked 'Persistent' */
237 PurpleConversation *conv = gtkconv->active_conv;
238 PurpleAccount *account = purple_conversation_get_account(conv);
239 const char *name = purple_conversation_get_name(conv);
241 switch (purple_conversation_get_type(conv)) {
242 case PURPLE_CONV_TYPE_IM:
244 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/close_immediately"))
245 close_this_sucker(gtkconv);
246 else
247 hide_conv(gtkconv, TRUE);
248 break;
250 case PURPLE_CONV_TYPE_CHAT:
252 PurpleChat *chat = purple_blist_find_chat(account, name);
253 if (!chat ||
254 !purple_blist_node_get_bool(&chat->node, "gtk-persistent"))
255 close_this_sucker(gtkconv);
256 else
257 hide_conv(gtkconv, FALSE);
258 break;
260 default:
264 return TRUE;
267 static gboolean
268 lbox_size_allocate_cb(GtkWidget *w, GtkAllocation *allocation, gpointer data)
270 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/chat/userlist_width", allocation->width == 1 ? 0 : allocation->width);
272 return FALSE;
275 static void
276 default_formatize(PidginConversation *c)
278 PurpleConversation *conv = c->active_conv;
279 gtk_imhtml_setup_entry(GTK_IMHTML(c->entry), conv->features);
282 static void
283 conversation_entry_clear(PidginConversation *gtkconv)
285 GtkIMHtml *imhtml = GTK_IMHTML(gtkconv->entry);
286 gtk_source_undo_manager_begin_not_undoable_action(imhtml->undo_manager);
287 gtk_imhtml_clear(imhtml);
288 gtk_source_undo_manager_end_not_undoable_action(imhtml->undo_manager);
291 static void
292 clear_formatting_cb(GtkIMHtml *imhtml, PidginConversation *gtkconv)
294 default_formatize(gtkconv);
297 static const char *
298 pidgin_get_cmd_prefix(void)
300 return "/";
303 static PurpleCmdRet
304 say_command_cb(PurpleConversation *conv,
305 const char *cmd, char **args, char **error, void *data)
307 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM)
308 purple_conv_im_send(PURPLE_CONV_IM(conv), args[0]);
309 else if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT)
310 purple_conv_chat_send(PURPLE_CONV_CHAT(conv), args[0]);
312 return PURPLE_CMD_RET_OK;
315 static PurpleCmdRet
316 me_command_cb(PurpleConversation *conv,
317 const char *cmd, char **args, char **error, void *data)
319 char *tmp;
321 tmp = g_strdup_printf("/me %s", args[0]);
323 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM)
324 purple_conv_im_send(PURPLE_CONV_IM(conv), tmp);
325 else if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT)
326 purple_conv_chat_send(PURPLE_CONV_CHAT(conv), tmp);
328 g_free(tmp);
329 return PURPLE_CMD_RET_OK;
332 static PurpleCmdRet
333 debug_command_cb(PurpleConversation *conv,
334 const char *cmd, char **args, char **error, void *data)
336 char *tmp, *markup;
338 if (!g_ascii_strcasecmp(args[0], "version")) {
339 tmp = g_strdup_printf("Using Pidgin v%s with libpurple v%s.",
340 DISPLAY_VERSION, purple_core_get_version());
341 } else if (!g_ascii_strcasecmp(args[0], "plugins")) {
342 /* Show all the loaded plugins, including the protocol plugins and plugin loaders.
343 * This is intentional, since third party prpls are often sources of bugs, and some
344 * plugin loaders (e.g. mono) can also be buggy.
346 GString *str = g_string_new("Loaded Plugins: ");
347 const GList *plugins = purple_plugins_get_loaded();
348 if (plugins) {
349 for (; plugins; plugins = plugins->next) {
350 str = g_string_append(str, purple_plugin_get_name(plugins->data));
351 if (plugins->next)
352 str = g_string_append(str, ", ");
354 } else {
355 str = g_string_append(str, "(none)");
358 tmp = g_string_free(str, FALSE);
359 } else {
360 purple_conversation_write(conv, NULL, _("Supported debug options are: plugins version"),
361 PURPLE_MESSAGE_NO_LOG|PURPLE_MESSAGE_ERROR, time(NULL));
362 return PURPLE_CMD_RET_OK;
365 markup = g_markup_escape_text(tmp, -1);
366 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM)
367 purple_conv_im_send(PURPLE_CONV_IM(conv), markup);
368 else if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT)
369 purple_conv_chat_send(PURPLE_CONV_CHAT(conv), markup);
371 g_free(tmp);
372 g_free(markup);
373 return PURPLE_CMD_RET_OK;
376 static void clear_conversation_scrollback_cb(PurpleConversation *conv,
377 void *data)
379 PidginConversation *gtkconv = NULL;
381 gtkconv = PIDGIN_CONVERSATION(conv);
382 if (gtkconv)
383 gtk_imhtml_clear(GTK_IMHTML(gtkconv->imhtml));
386 static PurpleCmdRet
387 clear_command_cb(PurpleConversation *conv,
388 const char *cmd, char **args, char **error, void *data)
390 purple_conversation_clear_message_history(conv);
391 return PURPLE_CMD_RET_OK;
394 static PurpleCmdRet
395 clearall_command_cb(PurpleConversation *conv,
396 const char *cmd, char **args, char **error, void *data)
398 purple_conversation_foreach(purple_conversation_clear_message_history);
399 return PURPLE_CMD_RET_OK;
402 static PurpleCmdRet
403 help_command_cb(PurpleConversation *conv,
404 const char *cmd, char **args, char **error, void *data)
406 GList *l, *text;
407 GString *s;
409 if (args[0] != NULL) {
410 s = g_string_new("");
411 text = purple_cmd_help(conv, args[0]);
413 if (text) {
414 for (l = text; l; l = l->next)
415 if (l->next)
416 g_string_append_printf(s, "%s\n", (char *)l->data);
417 else
418 g_string_append_printf(s, "%s", (char *)l->data);
419 } else {
420 g_string_append(s, _("No such command (in this context)."));
422 } else {
423 s = g_string_new(_("Use \"/help &lt;command&gt;\" for help on a specific command.\n"
424 "The following commands are available in this context:\n"));
426 text = purple_cmd_list(conv);
427 for (l = text; l; l = l->next)
428 if (l->next)
429 g_string_append_printf(s, "%s, ", (char *)l->data);
430 else
431 g_string_append_printf(s, "%s.", (char *)l->data);
432 g_list_free(text);
435 purple_conversation_write(conv, NULL, s->str, PURPLE_MESSAGE_NO_LOG, time(NULL));
436 g_string_free(s, TRUE);
438 return PURPLE_CMD_RET_OK;
441 static void
442 send_history_add(PidginConversation *gtkconv, const char *message)
444 GList *first;
446 first = g_list_first(gtkconv->send_history);
447 g_free(first->data);
448 first->data = g_strdup(message);
449 gtkconv->send_history = g_list_prepend(first, NULL);
452 static gboolean
453 check_for_and_do_command(PurpleConversation *conv)
455 PidginConversation *gtkconv;
456 char *cmd;
457 const char *prefix;
458 GtkTextIter start;
459 gboolean retval = FALSE;
461 gtkconv = PIDGIN_CONVERSATION(conv);
462 prefix = pidgin_get_cmd_prefix();
464 cmd = gtk_imhtml_get_text(GTK_IMHTML(gtkconv->entry), NULL, NULL);
465 gtk_text_buffer_get_start_iter(GTK_IMHTML(gtkconv->entry)->text_buffer, &start);
467 if (cmd && (strncmp(cmd, prefix, strlen(prefix)) == 0)
468 && !gtk_text_iter_get_child_anchor(&start)) {
469 PurpleCmdStatus status;
470 char *error, *cmdline, *markup, *send_history;
471 GtkTextIter end;
473 send_history = gtk_imhtml_get_markup(GTK_IMHTML(gtkconv->entry));
474 send_history_add(gtkconv, send_history);
475 g_free(send_history);
477 cmdline = cmd + strlen(prefix);
479 if (strcmp(cmdline, "xyzzy") == 0) {
480 purple_conversation_write(conv, "", "Nothing happens",
481 PURPLE_MESSAGE_NO_LOG, time(NULL));
482 g_free(cmd);
483 return TRUE;
486 gtk_text_iter_forward_chars(&start, g_utf8_strlen(prefix, -1));
487 gtk_text_buffer_get_end_iter(GTK_IMHTML(gtkconv->entry)->text_buffer, &end);
488 markup = gtk_imhtml_get_markup_range(GTK_IMHTML(gtkconv->entry), &start, &end);
489 status = purple_cmd_do_command(conv, cmdline, markup, &error);
490 g_free(markup);
492 switch (status) {
493 case PURPLE_CMD_STATUS_OK:
494 retval = TRUE;
495 break;
496 case PURPLE_CMD_STATUS_NOT_FOUND:
498 PurplePluginProtocolInfo *prpl_info = NULL;
499 PurpleConnection *gc;
501 if ((gc = purple_conversation_get_gc(conv)))
502 prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(gc->prpl);
504 if ((prpl_info != NULL) && (prpl_info->options & OPT_PROTO_SLASH_COMMANDS_NATIVE)) {
505 char *spaceslash;
507 /* If the first word in the entered text has a '/' in it, then the user
508 * probably didn't mean it as a command. So send the text as message. */
509 spaceslash = cmdline;
510 while (*spaceslash && *spaceslash != ' ' && *spaceslash != '/')
511 spaceslash++;
513 if (*spaceslash != '/') {
514 purple_conversation_write(conv, "", _("Unknown command."), PURPLE_MESSAGE_NO_LOG, time(NULL));
515 retval = TRUE;
518 break;
520 case PURPLE_CMD_STATUS_WRONG_ARGS:
521 purple_conversation_write(conv, "", _("Syntax Error: You typed the wrong number of arguments "
522 "to that command."),
523 PURPLE_MESSAGE_NO_LOG, time(NULL));
524 retval = TRUE;
525 break;
526 case PURPLE_CMD_STATUS_FAILED:
527 purple_conversation_write(conv, "", error ? error : _("Your command failed for an unknown reason."),
528 PURPLE_MESSAGE_NO_LOG, time(NULL));
529 g_free(error);
530 retval = TRUE;
531 break;
532 case PURPLE_CMD_STATUS_WRONG_TYPE:
533 if(purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM)
534 purple_conversation_write(conv, "", _("That command only works in chats, not IMs."),
535 PURPLE_MESSAGE_NO_LOG, time(NULL));
536 else
537 purple_conversation_write(conv, "", _("That command only works in IMs, not chats."),
538 PURPLE_MESSAGE_NO_LOG, time(NULL));
539 retval = TRUE;
540 break;
541 case PURPLE_CMD_STATUS_WRONG_PRPL:
542 purple_conversation_write(conv, "", _("That command doesn't work on this protocol."),
543 PURPLE_MESSAGE_NO_LOG, time(NULL));
544 retval = TRUE;
545 break;
549 g_free(cmd);
550 return retval;
553 static void
554 send_cb(GtkWidget *widget, PidginConversation *gtkconv)
556 PurpleConversation *conv = gtkconv->active_conv;
557 PurpleAccount *account;
558 PurpleConnection *gc;
559 PurpleMessageFlags flags = 0;
560 char *buf, *clean;
562 account = purple_conversation_get_account(conv);
564 if (check_for_and_do_command(conv)) {
565 conversation_entry_clear(gtkconv);
566 return;
569 if ((purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) &&
570 purple_conv_chat_has_left(PURPLE_CONV_CHAT(conv)))
571 return;
573 if (!purple_account_is_connected(account))
574 return;
576 buf = gtk_imhtml_get_markup(GTK_IMHTML(gtkconv->entry));
577 clean = gtk_imhtml_get_text(GTK_IMHTML(gtkconv->entry), NULL, NULL);
579 gtk_widget_grab_focus(gtkconv->entry);
581 if (strlen(clean) == 0) {
582 g_free(buf);
583 g_free(clean);
584 return;
587 purple_idle_touch();
589 /* XXX: is there a better way to tell if the message has images? */
590 if (GTK_IMHTML(gtkconv->entry)->im_images != NULL)
591 flags |= PURPLE_MESSAGE_IMAGES;
593 gc = purple_account_get_connection(account);
594 if (gc && (conv->features & PURPLE_CONNECTION_NO_NEWLINES)) {
595 char **bufs;
596 int i;
598 bufs = gtk_imhtml_get_markup_lines(GTK_IMHTML(gtkconv->entry));
599 for (i = 0; bufs[i]; i++) {
600 send_history_add(gtkconv, bufs[i]);
601 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM)
602 purple_conv_im_send_with_flags(PURPLE_CONV_IM(conv), bufs[i], flags);
603 else if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT)
604 purple_conv_chat_send_with_flags(PURPLE_CONV_CHAT(conv), bufs[i], flags);
607 g_strfreev(bufs);
609 } else {
610 send_history_add(gtkconv, buf);
611 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM)
612 purple_conv_im_send_with_flags(PURPLE_CONV_IM(conv), buf, flags);
613 else if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT)
614 purple_conv_chat_send_with_flags(PURPLE_CONV_CHAT(conv), buf, flags);
617 g_free(clean);
618 g_free(buf);
620 conversation_entry_clear(gtkconv);
621 gtkconv_set_unseen(gtkconv, PIDGIN_UNSEEN_NONE);
624 static void
625 add_remove_cb(GtkWidget *widget, PidginConversation *gtkconv)
627 PurpleAccount *account;
628 const char *name;
629 PurpleConversation *conv = gtkconv->active_conv;
631 account = purple_conversation_get_account(conv);
632 name = purple_conversation_get_name(conv);
634 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) {
635 PurpleBuddy *b;
637 b = purple_find_buddy(account, name);
638 if (b != NULL)
639 pidgin_dialogs_remove_buddy(b);
640 else if (account != NULL && purple_account_is_connected(account))
641 purple_blist_request_add_buddy(account, (char *)name, NULL, NULL);
642 } else if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) {
643 PurpleChat *c;
645 c = purple_blist_find_chat(account, name);
646 if (c != NULL)
647 pidgin_dialogs_remove_chat(c);
648 else if (account != NULL && purple_account_is_connected(account))
649 purple_blist_request_add_chat(account, NULL, NULL, name);
652 gtk_widget_grab_focus(PIDGIN_CONVERSATION(conv)->entry);
655 static void chat_do_info(PidginConversation *gtkconv, const char *who)
657 PurpleConversation *conv = gtkconv->active_conv;
658 PurpleConnection *gc;
660 if ((gc = purple_conversation_get_gc(conv))) {
661 pidgin_retrieve_user_info_in_chat(gc, who, purple_conv_chat_get_id(PURPLE_CONV_CHAT(conv)));
666 static void
667 info_cb(GtkWidget *widget, PidginConversation *gtkconv)
669 PurpleConversation *conv = gtkconv->active_conv;
671 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) {
672 pidgin_retrieve_user_info(purple_conversation_get_gc(conv),
673 purple_conversation_get_name(conv));
674 gtk_widget_grab_focus(gtkconv->entry);
675 } else if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) {
676 /* Get info of the person currently selected in the GtkTreeView */
677 PidginChatPane *gtkchat;
678 GtkTreeIter iter;
679 GtkTreeModel *model;
680 GtkTreeSelection *sel;
681 char *name;
683 gtkchat = gtkconv->u.chat;
685 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
686 sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(gtkchat->list));
688 if (gtk_tree_selection_get_selected(sel, NULL, &iter))
689 gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, CHAT_USERS_NAME_COLUMN, &name, -1);
690 else
691 return;
693 chat_do_info(gtkconv, name);
694 g_free(name);
698 static void
699 block_cb(GtkWidget *widget, PidginConversation *gtkconv)
701 PurpleConversation *conv = gtkconv->active_conv;
702 PurpleAccount *account;
704 account = purple_conversation_get_account(conv);
706 if (account != NULL && purple_account_is_connected(account))
707 pidgin_request_add_block(account, purple_conversation_get_name(conv));
709 gtk_widget_grab_focus(PIDGIN_CONVERSATION(conv)->entry);
712 static void
713 unblock_cb(GtkWidget *widget, PidginConversation *gtkconv)
715 PurpleConversation *conv = gtkconv->active_conv;
716 PurpleAccount *account;
718 account = purple_conversation_get_account(conv);
720 if (account != NULL && purple_account_is_connected(account))
721 pidgin_request_add_permit(account, purple_conversation_get_name(conv));
723 gtk_widget_grab_focus(PIDGIN_CONVERSATION(conv)->entry);
726 static gboolean
727 chat_invite_filter(const PidginBuddyCompletionEntry *entry, gpointer data)
729 PurpleAccount *filter_account = data;
730 PurpleAccount *account = NULL;
732 if (entry->is_buddy) {
733 if (PURPLE_BUDDY_IS_ONLINE(entry->entry.buddy))
734 account = purple_buddy_get_account(entry->entry.buddy);
735 else
736 return FALSE;
737 } else {
738 account = entry->entry.logged_buddy->account;
740 if (account == filter_account)
741 return TRUE;
742 return FALSE;
745 static void
746 do_invite(GtkWidget *w, int resp, InviteBuddyInfo *info)
748 const char *buddy, *message;
749 PurpleConversation *conv;
751 conv = info->conv;
753 if (resp == GTK_RESPONSE_OK) {
754 buddy = gtk_entry_get_text(GTK_ENTRY(info->entry));
755 message = gtk_entry_get_text(GTK_ENTRY(info->message));
757 if (!g_ascii_strcasecmp(buddy, ""))
758 return;
760 serv_chat_invite(purple_conversation_get_gc(conv),
761 purple_conv_chat_get_id(PURPLE_CONV_CHAT(conv)),
762 message, buddy);
765 gtk_widget_destroy(invite_dialog);
766 invite_dialog = NULL;
768 g_free(info);
771 static void
772 invite_dnd_recv(GtkWidget *widget, GdkDragContext *dc, gint x, gint y,
773 GtkSelectionData *sd, guint inf, guint t, gpointer data)
775 InviteBuddyInfo *info = (InviteBuddyInfo *)data;
776 const char *convprotocol;
777 gboolean success = TRUE;
779 convprotocol = purple_account_get_protocol_id(purple_conversation_get_account(info->conv));
781 if (sd->target == gdk_atom_intern("PURPLE_BLIST_NODE", FALSE))
783 PurpleBlistNode *node = NULL;
784 PurpleBuddy *buddy;
786 memcpy(&node, sd->data, sizeof(node));
788 if (PURPLE_BLIST_NODE_IS_CONTACT(node))
789 buddy = purple_contact_get_priority_buddy((PurpleContact *)node);
790 else if (PURPLE_BLIST_NODE_IS_BUDDY(node))
791 buddy = (PurpleBuddy *)node;
792 else
793 return;
795 if (strcmp(convprotocol, purple_account_get_protocol_id(buddy->account)))
797 purple_notify_error(PIDGIN_CONVERSATION(info->conv), NULL,
798 _("That buddy is not on the same protocol as this "
799 "chat."), NULL);
800 success = FALSE;
802 else
803 gtk_entry_set_text(GTK_ENTRY(info->entry), purple_buddy_get_name(buddy));
805 gtk_drag_finish(dc, success, (dc->action == GDK_ACTION_MOVE), t);
807 else if (sd->target == gdk_atom_intern("application/x-im-contact", FALSE))
809 char *protocol = NULL;
810 char *username = NULL;
811 PurpleAccount *account;
813 if (pidgin_parse_x_im_contact((const char *)sd->data, FALSE, &account,
814 &protocol, &username, NULL))
816 if (account == NULL)
818 purple_notify_error(PIDGIN_CONVERSATION(info->conv), NULL,
819 _("You are not currently signed on with an account that "
820 "can invite that buddy."), NULL);
822 else if (strcmp(convprotocol, purple_account_get_protocol_id(account)))
824 purple_notify_error(PIDGIN_CONVERSATION(info->conv), NULL,
825 _("That buddy is not on the same protocol as this "
826 "chat."), NULL);
827 success = FALSE;
829 else
831 gtk_entry_set_text(GTK_ENTRY(info->entry), username);
835 g_free(username);
836 g_free(protocol);
838 gtk_drag_finish(dc, success, (dc->action == GDK_ACTION_MOVE), t);
842 static const GtkTargetEntry dnd_targets[] =
844 {"PURPLE_BLIST_NODE", GTK_TARGET_SAME_APP, 0},
845 {"application/x-im-contact", 0, 1}
848 static void
849 invite_cb(GtkWidget *widget, PidginConversation *gtkconv)
851 PurpleConversation *conv = gtkconv->active_conv;
852 InviteBuddyInfo *info = NULL;
854 if (invite_dialog == NULL) {
855 PidginWindow *gtkwin;
856 GtkWidget *label;
857 GtkWidget *vbox, *hbox;
858 GtkWidget *table;
859 GtkWidget *img;
861 img = gtk_image_new_from_stock(PIDGIN_STOCK_DIALOG_QUESTION,
862 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_HUGE));
864 info = g_new0(InviteBuddyInfo, 1);
865 info->conv = conv;
867 gtkwin = pidgin_conv_get_window(gtkconv);
869 /* Create the new dialog. */
870 invite_dialog = gtk_dialog_new_with_buttons(
871 _("Invite Buddy Into Chat Room"),
872 GTK_WINDOW(gtkwin->window), 0,
873 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
874 PIDGIN_STOCK_INVITE, GTK_RESPONSE_OK, NULL);
876 gtk_dialog_set_default_response(GTK_DIALOG(invite_dialog),
877 GTK_RESPONSE_OK);
878 gtk_container_set_border_width(GTK_CONTAINER(invite_dialog), PIDGIN_HIG_BOX_SPACE);
879 gtk_window_set_resizable(GTK_WINDOW(invite_dialog), FALSE);
880 gtk_dialog_set_has_separator(GTK_DIALOG(invite_dialog), FALSE);
882 info->window = GTK_WIDGET(invite_dialog);
884 /* Setup the outside spacing. */
885 vbox = GTK_DIALOG(invite_dialog)->vbox;
887 gtk_box_set_spacing(GTK_BOX(vbox), PIDGIN_HIG_BORDER);
888 gtk_container_set_border_width(GTK_CONTAINER(vbox), PIDGIN_HIG_BOX_SPACE);
890 /* Setup the inner hbox and put the dialog's icon in it. */
891 hbox = gtk_hbox_new(FALSE, PIDGIN_HIG_BORDER);
892 gtk_container_add(GTK_CONTAINER(vbox), hbox);
893 gtk_box_pack_start(GTK_BOX(hbox), img, FALSE, FALSE, 0);
894 gtk_misc_set_alignment(GTK_MISC(img), 0, 0);
896 /* Setup the right vbox. */
897 vbox = gtk_vbox_new(FALSE, 0);
898 gtk_container_add(GTK_CONTAINER(hbox), vbox);
900 /* Put our happy label in it. */
901 label = gtk_label_new(_("Please enter the name of the user you wish "
902 "to invite, along with an optional invite "
903 "message."));
904 gtk_widget_set_size_request(label, 350, -1);
905 gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
906 gtk_misc_set_alignment(GTK_MISC(label), 0, 0);
907 gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 0);
909 /* hbox for the table, and to give it some spacing on the left. */
910 hbox = gtk_hbox_new(FALSE, PIDGIN_HIG_BOX_SPACE);
911 gtk_container_add(GTK_CONTAINER(vbox), hbox);
913 /* Setup the table we're going to use to lay stuff out. */
914 table = gtk_table_new(2, 2, FALSE);
915 gtk_table_set_row_spacings(GTK_TABLE(table), PIDGIN_HIG_BOX_SPACE);
916 gtk_table_set_col_spacings(GTK_TABLE(table), PIDGIN_HIG_BOX_SPACE);
917 gtk_container_set_border_width(GTK_CONTAINER(table), PIDGIN_HIG_BORDER);
918 gtk_box_pack_start(GTK_BOX(vbox), table, FALSE, FALSE, 0);
920 /* Now the Buddy label */
921 label = gtk_label_new(NULL);
922 gtk_label_set_markup_with_mnemonic(GTK_LABEL(label), _("_Buddy:"));
923 gtk_misc_set_alignment(GTK_MISC(label), 0, 0);
924 gtk_table_attach_defaults(GTK_TABLE(table), label, 0, 1, 0, 1);
926 /* Now the Buddy drop-down entry field. */
927 info->entry = gtk_entry_new();
928 pidgin_setup_screenname_autocomplete_with_filter(info->entry, NULL, chat_invite_filter,
929 purple_conversation_get_account(conv));
930 gtk_table_attach_defaults(GTK_TABLE(table), info->entry, 1, 2, 0, 1);
931 gtk_label_set_mnemonic_widget(GTK_LABEL(label), info->entry);
933 /* Now the label for "Message" */
934 label = gtk_label_new(NULL);
935 gtk_label_set_markup_with_mnemonic(GTK_LABEL(label), _("_Message:"));
936 gtk_misc_set_alignment(GTK_MISC(label), 0, 0);
937 gtk_table_attach_defaults(GTK_TABLE(table), label, 0, 1, 1, 2);
940 /* And finally, the Message entry field. */
941 info->message = gtk_entry_new();
942 gtk_entry_set_activates_default(GTK_ENTRY(info->message), TRUE);
944 gtk_table_attach_defaults(GTK_TABLE(table), info->message, 1, 2, 1, 2);
945 gtk_label_set_mnemonic_widget(GTK_LABEL(label), info->message);
947 /* Connect the signals. */
948 g_signal_connect(G_OBJECT(invite_dialog), "response",
949 G_CALLBACK(do_invite), info);
950 /* Setup drag-and-drop */
951 gtk_drag_dest_set(info->window,
952 GTK_DEST_DEFAULT_MOTION |
953 GTK_DEST_DEFAULT_DROP,
954 dnd_targets,
955 sizeof(dnd_targets) / sizeof(GtkTargetEntry),
956 GDK_ACTION_COPY);
957 gtk_drag_dest_set(info->entry,
958 GTK_DEST_DEFAULT_MOTION |
959 GTK_DEST_DEFAULT_DROP,
960 dnd_targets,
961 sizeof(dnd_targets) / sizeof(GtkTargetEntry),
962 GDK_ACTION_COPY);
964 g_signal_connect(G_OBJECT(info->window), "drag_data_received",
965 G_CALLBACK(invite_dnd_recv), info);
966 g_signal_connect(G_OBJECT(info->entry), "drag_data_received",
967 G_CALLBACK(invite_dnd_recv), info);
970 gtk_widget_show_all(invite_dialog);
972 if (info != NULL)
973 gtk_widget_grab_focus(info->entry);
976 static void
977 menu_new_conv_cb(gpointer data, guint action, GtkWidget *widget)
979 pidgin_dialogs_im();
982 static void
983 menu_join_chat_cb(gpointer data, guint action, GtkWidget *widget)
985 pidgin_blist_joinchat_show();
988 static void
989 savelog_writefile_cb(void *user_data, const char *filename)
991 PurpleConversation *conv = (PurpleConversation *)user_data;
992 FILE *fp;
993 const char *name;
994 char **lines;
995 gchar *text;
997 if ((fp = g_fopen(filename, "w+")) == NULL) {
998 purple_notify_error(PIDGIN_CONVERSATION(conv), NULL, _("Unable to open file."), NULL);
999 return;
1002 name = purple_conversation_get_name(conv);
1003 fprintf(fp, "<html>\n<head>\n");
1004 fprintf(fp, "<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">\n");
1005 fprintf(fp, "<title>%s</title>\n</head>\n<body>\n", name);
1006 fprintf(fp, _("<h1>Conversation with %s</h1>\n"), name);
1008 lines = gtk_imhtml_get_markup_lines(
1009 GTK_IMHTML(PIDGIN_CONVERSATION(conv)->imhtml));
1010 text = g_strjoinv("<br>\n", lines);
1011 fprintf(fp, "%s", text);
1012 g_free(text);
1013 g_strfreev(lines);
1015 fprintf(fp, "\n</body>\n</html>\n");
1016 fclose(fp);
1020 * It would be kinda cool if this gave the option of saving a
1021 * plaintext v. HTML file.
1023 static void
1024 menu_save_as_cb(gpointer data, guint action, GtkWidget *widget)
1026 PidginWindow *win = data;
1027 PurpleConversation *conv = pidgin_conv_window_get_active_conversation(win);
1028 PurpleBuddy *buddy = purple_find_buddy(conv->account, conv->name);
1029 const char *name;
1030 gchar *buf;
1031 gchar *c;
1033 if (buddy != NULL)
1034 name = purple_buddy_get_contact_alias(buddy);
1035 else
1036 name = purple_normalize(conv->account, conv->name);
1038 buf = g_strdup_printf("%s.html", name);
1039 for (c = buf ; *c ; c++)
1041 if (*c == '/' || *c == '\\')
1042 *c = ' ';
1044 purple_request_file(PIDGIN_CONVERSATION(conv), _("Save Conversation"),
1045 buf,
1046 TRUE, G_CALLBACK(savelog_writefile_cb), NULL,
1047 NULL, NULL, conv,
1048 conv);
1050 g_free(buf);
1053 static void
1054 menu_view_log_cb(gpointer data, guint action, GtkWidget *widget)
1056 PidginWindow *win = data;
1057 PurpleConversation *conv;
1058 PurpleLogType type;
1059 PidginBuddyList *gtkblist;
1060 GdkCursor *cursor;
1061 const char *name;
1062 PurpleAccount *account;
1063 GSList *buddies;
1064 GSList *cur;
1066 conv = pidgin_conv_window_get_active_conversation(win);
1068 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM)
1069 type = PURPLE_LOG_IM;
1070 else if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT)
1071 type = PURPLE_LOG_CHAT;
1072 else
1073 return;
1075 gtkblist = pidgin_blist_get_default_gtk_blist();
1077 cursor = gdk_cursor_new(GDK_WATCH);
1078 gdk_window_set_cursor(gtkblist->window->window, cursor);
1079 gdk_window_set_cursor(win->window->window, cursor);
1080 gdk_cursor_unref(cursor);
1081 gdk_display_flush(gdk_drawable_get_display(GDK_DRAWABLE(widget->window)));
1083 name = purple_conversation_get_name(conv);
1084 account = purple_conversation_get_account(conv);
1086 buddies = purple_find_buddies(account, name);
1087 for (cur = buddies; cur != NULL; cur = cur->next)
1089 PurpleBlistNode *node = cur->data;
1090 if ((node != NULL) && ((node->prev != NULL) || (node->next != NULL)))
1092 pidgin_log_show_contact((PurpleContact *)node->parent);
1093 g_slist_free(buddies);
1094 gdk_window_set_cursor(gtkblist->window->window, NULL);
1095 gdk_window_set_cursor(win->window->window, NULL);
1096 return;
1099 g_slist_free(buddies);
1101 pidgin_log_show(type, name, account);
1103 gdk_window_set_cursor(gtkblist->window->window, NULL);
1104 gdk_window_set_cursor(win->window->window, NULL);
1107 static void
1108 menu_clear_cb(gpointer data, guint action, GtkWidget *widget)
1110 PidginWindow *win = data;
1111 PurpleConversation *conv;
1113 conv = pidgin_conv_window_get_active_conversation(win);
1114 purple_conversation_clear_message_history(conv);
1117 static void
1118 menu_find_cb(gpointer data, guint action, GtkWidget *widget)
1120 PidginWindow *gtkwin = data;
1121 PidginConversation *gtkconv = pidgin_conv_window_get_active_gtkconv(gtkwin);
1122 gtk_widget_show_all(gtkconv->quickfind.container);
1123 gtk_widget_grab_focus(gtkconv->quickfind.entry);
1126 #ifdef USE_VV
1127 static void
1128 menu_initiate_media_call_cb(gpointer data, guint action, GtkWidget *widget)
1130 PidginWindow *win = (PidginWindow *)data;
1131 PurpleConversation *conv = pidgin_conv_window_get_active_conversation(win);
1132 PurpleAccount *account = purple_conversation_get_account(conv);
1134 purple_prpl_initiate_media(account,
1135 purple_conversation_get_name(conv),
1136 action == 0 ? PURPLE_MEDIA_AUDIO :
1137 action == 1 ? PURPLE_MEDIA_VIDEO :
1138 action == 2 ? PURPLE_MEDIA_AUDIO |
1139 PURPLE_MEDIA_VIDEO : PURPLE_MEDIA_NONE);
1141 #endif
1143 static void
1144 menu_send_file_cb(gpointer data, guint action, GtkWidget *widget)
1146 PidginWindow *win = data;
1147 PurpleConversation *conv = pidgin_conv_window_get_active_conversation(win);
1149 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) {
1150 serv_send_file(purple_conversation_get_gc(conv), purple_conversation_get_name(conv), NULL);
1155 static void
1156 menu_get_attention_cb(gpointer data, guint action, GtkWidget *widget)
1158 PidginWindow *win = data;
1159 PurpleConversation *conv = pidgin_conv_window_get_active_conversation(win);
1161 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) {
1162 purple_prpl_send_attention(purple_conversation_get_gc(conv),
1163 purple_conversation_get_name(conv), 0);
1167 static void
1168 menu_add_pounce_cb(gpointer data, guint action, GtkWidget *widget)
1170 PidginWindow *win = data;
1171 PurpleConversation *conv;
1173 conv = pidgin_conv_window_get_active_gtkconv(win)->active_conv;
1175 pidgin_pounce_editor_show(purple_conversation_get_account(conv),
1176 purple_conversation_get_name(conv), NULL);
1179 static void
1180 menu_insert_link_cb(gpointer data, guint action, GtkWidget *widget)
1182 PidginWindow *win = data;
1183 PidginConversation *gtkconv;
1184 GtkIMHtmlToolbar *toolbar;
1186 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
1187 toolbar = GTK_IMHTMLTOOLBAR(gtkconv->toolbar);
1189 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(toolbar->link),
1190 !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toolbar->link)));
1193 static void
1194 menu_insert_image_cb(gpointer data, guint action, GtkWidget *widget)
1196 PidginWindow *win = data;
1197 PidginConversation *gtkconv;
1198 GtkIMHtmlToolbar *toolbar;
1200 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
1201 toolbar = GTK_IMHTMLTOOLBAR(gtkconv->toolbar);
1203 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(toolbar->image),
1204 !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toolbar->image)));
1208 static void
1209 menu_alias_cb(gpointer data, guint action, GtkWidget *widget)
1211 PidginWindow *win = data;
1212 PurpleConversation *conv;
1213 PurpleAccount *account;
1214 const char *name;
1216 conv = pidgin_conv_window_get_active_conversation(win);
1217 account = purple_conversation_get_account(conv);
1218 name = purple_conversation_get_name(conv);
1220 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) {
1221 PurpleBuddy *b;
1223 b = purple_find_buddy(account, name);
1224 if (b != NULL)
1225 pidgin_dialogs_alias_buddy(b);
1226 } else if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) {
1227 PurpleChat *c;
1229 c = purple_blist_find_chat(account, name);
1230 if (c != NULL)
1231 pidgin_dialogs_alias_chat(c);
1235 static void
1236 menu_get_info_cb(gpointer data, guint action, GtkWidget *widget)
1238 PidginWindow *win = data;
1239 PurpleConversation *conv;
1241 conv = pidgin_conv_window_get_active_conversation(win);
1243 info_cb(NULL, PIDGIN_CONVERSATION(conv));
1246 static void
1247 menu_invite_cb(gpointer data, guint action, GtkWidget *widget)
1249 PidginWindow *win = data;
1250 PurpleConversation *conv;
1252 conv = pidgin_conv_window_get_active_conversation(win);
1254 invite_cb(NULL, PIDGIN_CONVERSATION(conv));
1257 static void
1258 menu_block_cb(gpointer data, guint action, GtkWidget *widget)
1260 PidginWindow *win = data;
1261 PurpleConversation *conv;
1263 conv = pidgin_conv_window_get_active_conversation(win);
1265 block_cb(NULL, PIDGIN_CONVERSATION(conv));
1268 static void
1269 menu_unblock_cb(gpointer data, guint action, GtkWidget *widget)
1271 PidginWindow *win = data;
1272 PurpleConversation *conv;
1274 conv = pidgin_conv_window_get_active_conversation(win);
1276 unblock_cb(NULL, PIDGIN_CONVERSATION(conv));
1279 static void
1280 menu_add_remove_cb(gpointer data, guint action, GtkWidget *widget)
1282 PidginWindow *win = data;
1283 PurpleConversation *conv;
1285 conv = pidgin_conv_window_get_active_conversation(win);
1287 add_remove_cb(NULL, PIDGIN_CONVERSATION(conv));
1290 static gboolean
1291 close_already(gpointer data)
1293 purple_conversation_destroy(data);
1294 return FALSE;
1297 static void
1298 hide_conv(PidginConversation *gtkconv, gboolean closetimer)
1300 GList *list;
1302 purple_signal_emit(pidgin_conversations_get_handle(),
1303 "conversation-hiding", gtkconv);
1305 for (list = g_list_copy(gtkconv->convs); list; list = g_list_delete_link(list, list)) {
1306 PurpleConversation *conv = list->data;
1307 if (closetimer) {
1308 guint timer = GPOINTER_TO_INT(purple_conversation_get_data(conv, "close-timer"));
1309 if (timer)
1310 purple_timeout_remove(timer);
1311 timer = purple_timeout_add_seconds(CLOSE_CONV_TIMEOUT_SECS, close_already, conv);
1312 purple_conversation_set_data(conv, "close-timer", GINT_TO_POINTER(timer));
1314 #if 0
1315 /* I will miss you */
1316 purple_conversation_set_ui_ops(conv, NULL);
1317 #else
1318 pidgin_conv_window_remove_gtkconv(gtkconv->win, gtkconv);
1319 pidgin_conv_window_add_gtkconv(hidden_convwin, gtkconv);
1320 #endif
1324 static void
1325 menu_close_conv_cb(gpointer data, guint action, GtkWidget *widget)
1327 PidginWindow *win = data;
1329 close_conv_cb(NULL, PIDGIN_CONVERSATION(pidgin_conv_window_get_active_conversation(win)));
1332 static void
1333 menu_logging_cb(gpointer data, guint action, GtkWidget *widget)
1335 PidginWindow *win = data;
1336 PurpleConversation *conv;
1337 gboolean logging;
1338 PurpleBlistNode *node;
1340 conv = pidgin_conv_window_get_active_conversation(win);
1342 if (conv == NULL)
1343 return;
1345 logging = gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(widget));
1347 if (logging == purple_conversation_is_logging(conv))
1348 return;
1350 node = get_conversation_blist_node(conv);
1352 if (logging)
1354 /* Enable logging first so the message below can be logged. */
1355 purple_conversation_set_logging(conv, TRUE);
1357 purple_conversation_write(conv, NULL,
1358 _("Logging started. Future messages in this conversation will be logged."),
1359 conv->logs ? (PURPLE_MESSAGE_SYSTEM) :
1360 (PURPLE_MESSAGE_SYSTEM | PURPLE_MESSAGE_NO_LOG),
1361 time(NULL));
1363 else
1365 purple_conversation_write(conv, NULL,
1366 _("Logging stopped. Future messages in this conversation will not be logged."),
1367 conv->logs ? (PURPLE_MESSAGE_SYSTEM) :
1368 (PURPLE_MESSAGE_SYSTEM | PURPLE_MESSAGE_NO_LOG),
1369 time(NULL));
1371 /* Disable the logging second, so that the above message can be logged. */
1372 purple_conversation_set_logging(conv, FALSE);
1375 /* Save the setting IFF it's different than the pref. */
1376 switch (conv->type)
1378 case PURPLE_CONV_TYPE_IM:
1379 if (logging == purple_prefs_get_bool("/purple/logging/log_ims"))
1380 purple_blist_node_remove_setting(node, "enable-logging");
1381 else
1382 purple_blist_node_set_bool(node, "enable-logging", logging);
1383 break;
1385 case PURPLE_CONV_TYPE_CHAT:
1386 if (logging == purple_prefs_get_bool("/purple/logging/log_chats"))
1387 purple_blist_node_remove_setting(node, "enable-logging");
1388 else
1389 purple_blist_node_set_bool(node, "enable-logging", logging);
1390 break;
1392 default:
1393 break;
1397 static void
1398 menu_toolbar_cb(gpointer data, guint action, GtkWidget *widget)
1400 purple_prefs_set_bool(PIDGIN_PREFS_ROOT "/conversations/show_formatting_toolbar",
1401 gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(widget)));
1404 static void
1405 menu_sounds_cb(gpointer data, guint action, GtkWidget *widget)
1407 PidginWindow *win = data;
1408 PurpleConversation *conv;
1409 PidginConversation *gtkconv;
1410 PurpleBlistNode *node;
1412 conv = pidgin_conv_window_get_active_conversation(win);
1414 if (!conv)
1415 return;
1417 gtkconv = PIDGIN_CONVERSATION(conv);
1419 gtkconv->make_sound =
1420 gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(widget));
1421 node = get_conversation_blist_node(conv);
1422 if (node)
1423 purple_blist_node_set_bool(node, "gtk-mute-sound", !gtkconv->make_sound);
1426 static void
1427 menu_timestamps_cb(gpointer data, guint action, GtkWidget *widget)
1429 purple_prefs_set_bool(PIDGIN_PREFS_ROOT "/conversations/show_timestamps",
1430 gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(widget)));
1433 static void
1434 chat_do_im(PidginConversation *gtkconv, const char *who)
1436 PurpleConversation *conv = gtkconv->active_conv;
1437 PurpleAccount *account;
1438 PurpleConnection *gc;
1439 PurplePluginProtocolInfo *prpl_info = NULL;
1440 gchar *real_who = NULL;
1442 account = purple_conversation_get_account(conv);
1443 g_return_if_fail(account != NULL);
1445 gc = purple_account_get_connection(account);
1446 g_return_if_fail(gc != NULL);
1448 prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(gc->prpl);
1450 if (prpl_info && prpl_info->get_cb_real_name)
1451 real_who = prpl_info->get_cb_real_name(gc,
1452 purple_conv_chat_get_id(PURPLE_CONV_CHAT(conv)), who);
1454 if(!who && !real_who)
1455 return;
1457 pidgin_dialogs_im_with_user(account, real_who ? real_who : who);
1459 g_free(real_who);
1462 static void pidgin_conv_chat_update_user(PurpleConversation *conv, const char *user);
1464 static void
1465 ignore_cb(GtkWidget *w, PidginConversation *gtkconv)
1467 PurpleConversation *conv = gtkconv->active_conv;
1468 PurpleConvChat *chat;
1469 const char *name;
1471 chat = PURPLE_CONV_CHAT(conv);
1472 name = g_object_get_data(G_OBJECT(w), "user_data");
1474 if (name == NULL)
1475 return;
1477 if (purple_conv_chat_is_user_ignored(chat, name))
1478 purple_conv_chat_unignore(chat, name);
1479 else
1480 purple_conv_chat_ignore(chat, name);
1482 pidgin_conv_chat_update_user(conv, name);
1485 static void
1486 menu_chat_im_cb(GtkWidget *w, PidginConversation *gtkconv)
1488 const char *who = g_object_get_data(G_OBJECT(w), "user_data");
1490 chat_do_im(gtkconv, who);
1493 static void
1494 menu_chat_send_file_cb(GtkWidget *w, PidginConversation *gtkconv)
1496 PurplePluginProtocolInfo *prpl_info;
1497 PurpleConversation *conv = gtkconv->active_conv;
1498 const char *who = g_object_get_data(G_OBJECT(w), "user_data");
1499 PurpleConnection *gc = purple_conversation_get_gc(conv);
1500 gchar *real_who = NULL;
1502 g_return_if_fail(gc != NULL);
1504 prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(gc->prpl);
1506 if (prpl_info && prpl_info->get_cb_real_name)
1507 real_who = prpl_info->get_cb_real_name(gc,
1508 purple_conv_chat_get_id(PURPLE_CONV_CHAT(conv)), who);
1510 serv_send_file(gc, real_who ? real_who : who, NULL);
1511 g_free(real_who);
1514 static void
1515 menu_chat_info_cb(GtkWidget *w, PidginConversation *gtkconv)
1517 char *who;
1519 who = g_object_get_data(G_OBJECT(w), "user_data");
1521 chat_do_info(gtkconv, who);
1524 static void
1525 menu_chat_get_away_cb(GtkWidget *w, PidginConversation *gtkconv)
1527 PurpleConversation *conv = gtkconv->active_conv;
1528 PurplePluginProtocolInfo *prpl_info = NULL;
1529 PurpleConnection *gc;
1530 char *who;
1532 gc = purple_conversation_get_gc(conv);
1533 who = g_object_get_data(G_OBJECT(w), "user_data");
1535 if (gc != NULL) {
1536 prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(gc->prpl);
1539 * May want to expand this to work similarly to menu_info_cb?
1542 if (prpl_info->get_cb_away != NULL)
1544 prpl_info->get_cb_away(gc,
1545 purple_conv_chat_get_id(PURPLE_CONV_CHAT(conv)), who);
1550 static void
1551 menu_chat_add_remove_cb(GtkWidget *w, PidginConversation *gtkconv)
1553 PurpleConversation *conv = gtkconv->active_conv;
1554 PurpleAccount *account;
1555 PurpleBuddy *b;
1556 char *name;
1558 account = purple_conversation_get_account(conv);
1559 name = g_object_get_data(G_OBJECT(w), "user_data");
1560 b = purple_find_buddy(account, name);
1562 if (b != NULL)
1563 pidgin_dialogs_remove_buddy(b);
1564 else if (account != NULL && purple_account_is_connected(account))
1565 purple_blist_request_add_buddy(account, name, NULL, NULL);
1567 gtk_widget_grab_focus(PIDGIN_CONVERSATION(conv)->entry);
1570 static GtkTextMark *
1571 get_mark_for_user(PidginConversation *gtkconv, const char *who)
1573 GtkTextBuffer *buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(gtkconv->imhtml));
1574 char *tmp = g_strconcat("user:", who, NULL);
1575 GtkTextMark *mark = gtk_text_buffer_get_mark(buf, tmp);
1577 g_free(tmp);
1578 return mark;
1581 static void
1582 menu_last_said_cb(GtkWidget *w, PidginConversation *gtkconv)
1584 GtkTextMark *mark;
1585 const char *who;
1587 who = g_object_get_data(G_OBJECT(w), "user_data");
1588 mark = get_mark_for_user(gtkconv, who);
1590 if (mark != NULL)
1591 gtk_text_view_scroll_to_mark(GTK_TEXT_VIEW(gtkconv->imhtml), mark, 0.1, FALSE, 0, 0);
1592 else
1593 g_return_if_reached();
1596 static GtkWidget *
1597 create_chat_menu(PurpleConversation *conv, const char *who, PurpleConnection *gc)
1599 static GtkWidget *menu = NULL;
1600 PurplePluginProtocolInfo *prpl_info = NULL;
1601 PurpleConvChat *chat = PURPLE_CONV_CHAT(conv);
1602 gboolean is_me = FALSE;
1603 GtkWidget *button;
1604 PurpleBuddy *buddy = NULL;
1606 if (gc != NULL)
1607 prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(gc->prpl);
1610 * If a menu already exists, destroy it before creating a new one,
1611 * thus freeing-up the memory it occupied.
1613 if (menu)
1614 gtk_widget_destroy(menu);
1616 if (!strcmp(chat->nick, purple_normalize(conv->account, who)))
1617 is_me = TRUE;
1619 menu = gtk_menu_new();
1621 if (!is_me) {
1622 button = pidgin_new_item_from_stock(menu, _("IM"), PIDGIN_STOCK_TOOLBAR_MESSAGE_NEW,
1623 G_CALLBACK(menu_chat_im_cb), PIDGIN_CONVERSATION(conv), 0, 0, NULL);
1625 if (gc == NULL)
1626 gtk_widget_set_sensitive(button, FALSE);
1627 else
1628 g_object_set_data_full(G_OBJECT(button), "user_data", g_strdup(who), g_free);
1631 if (prpl_info && prpl_info->send_file)
1633 gboolean can_receive_file = TRUE;
1635 button = pidgin_new_item_from_stock(menu, _("Send File"),
1636 PIDGIN_STOCK_TOOLBAR_SEND_FILE, G_CALLBACK(menu_chat_send_file_cb),
1637 PIDGIN_CONVERSATION(conv), 0, 0, NULL);
1639 if (gc == NULL || prpl_info == NULL)
1640 can_receive_file = FALSE;
1641 else {
1642 gchar *real_who = NULL;
1643 if (prpl_info->get_cb_real_name)
1644 real_who = prpl_info->get_cb_real_name(gc,
1645 purple_conv_chat_get_id(PURPLE_CONV_CHAT(conv)), who);
1646 if (!(!prpl_info->can_receive_file || prpl_info->can_receive_file(gc, real_who ? real_who : who)))
1647 can_receive_file = FALSE;
1648 g_free(real_who);
1651 if (!can_receive_file)
1652 gtk_widget_set_sensitive(button, FALSE);
1653 else
1654 g_object_set_data_full(G_OBJECT(button), "user_data", g_strdup(who), g_free);
1658 if (purple_conv_chat_is_user_ignored(PURPLE_CONV_CHAT(conv), who))
1659 button = pidgin_new_item_from_stock(menu, _("Un-Ignore"), PIDGIN_STOCK_IGNORE,
1660 G_CALLBACK(ignore_cb), PIDGIN_CONVERSATION(conv), 0, 0, NULL);
1661 else
1662 button = pidgin_new_item_from_stock(menu, _("Ignore"), PIDGIN_STOCK_IGNORE,
1663 G_CALLBACK(ignore_cb), PIDGIN_CONVERSATION(conv), 0, 0, NULL);
1665 if (gc == NULL)
1666 gtk_widget_set_sensitive(button, FALSE);
1667 else
1668 g_object_set_data_full(G_OBJECT(button), "user_data", g_strdup(who), g_free);
1671 if (prpl_info && (prpl_info->get_info || prpl_info->get_cb_info)) {
1672 button = pidgin_new_item_from_stock(menu, _("Info"), PIDGIN_STOCK_TOOLBAR_USER_INFO,
1673 G_CALLBACK(menu_chat_info_cb), PIDGIN_CONVERSATION(conv), 0, 0, NULL);
1675 if (gc == NULL)
1676 gtk_widget_set_sensitive(button, FALSE);
1677 else
1678 g_object_set_data_full(G_OBJECT(button), "user_data", g_strdup(who), g_free);
1681 if (prpl_info && prpl_info->get_cb_away) {
1682 button = pidgin_new_item_from_stock(menu, _("Get Away Message"), PIDGIN_STOCK_AWAY,
1683 G_CALLBACK(menu_chat_get_away_cb), PIDGIN_CONVERSATION(conv), 0, 0, NULL);
1685 if (gc == NULL)
1686 gtk_widget_set_sensitive(button, FALSE);
1687 else
1688 g_object_set_data_full(G_OBJECT(button), "user_data", g_strdup(who), g_free);
1691 if (!is_me && prpl_info && !(prpl_info->options & OPT_PROTO_UNIQUE_CHATNAME)) {
1692 if ((buddy = purple_find_buddy(conv->account, who)) != NULL)
1693 button = pidgin_new_item_from_stock(menu, _("Remove"), GTK_STOCK_REMOVE,
1694 G_CALLBACK(menu_chat_add_remove_cb), PIDGIN_CONVERSATION(conv), 0, 0, NULL);
1695 else
1696 button = pidgin_new_item_from_stock(menu, _("Add"), GTK_STOCK_ADD,
1697 G_CALLBACK(menu_chat_add_remove_cb), PIDGIN_CONVERSATION(conv), 0, 0, NULL);
1699 if (gc == NULL)
1700 gtk_widget_set_sensitive(button, FALSE);
1701 else
1702 g_object_set_data_full(G_OBJECT(button), "user_data", g_strdup(who), g_free);
1705 button = pidgin_new_item_from_stock(menu, _("Last Said"), GTK_STOCK_INDEX,
1706 G_CALLBACK(menu_last_said_cb), PIDGIN_CONVERSATION(conv), 0, 0, NULL);
1707 g_object_set_data_full(G_OBJECT(button), "user_data", g_strdup(who), g_free);
1708 if (!get_mark_for_user(PIDGIN_CONVERSATION(conv), who))
1709 gtk_widget_set_sensitive(button, FALSE);
1711 if (buddy != NULL)
1713 if (purple_account_is_connected(conv->account))
1714 pidgin_append_blist_node_proto_menu(menu, conv->account->gc,
1715 (PurpleBlistNode *)buddy);
1716 pidgin_append_blist_node_extended_menu(menu, (PurpleBlistNode *)buddy);
1717 gtk_widget_show_all(menu);
1720 return menu;
1724 static gint
1725 gtkconv_chat_popup_menu_cb(GtkWidget *widget, PidginConversation *gtkconv)
1727 PurpleConversation *conv = gtkconv->active_conv;
1728 PidginChatPane *gtkchat;
1729 PurpleConnection *gc;
1730 PurpleAccount *account;
1731 GtkTreeSelection *sel;
1732 GtkTreeIter iter;
1733 GtkTreeModel *model;
1734 GtkWidget *menu;
1735 gchar *who;
1737 gtkconv = PIDGIN_CONVERSATION(conv);
1738 gtkchat = gtkconv->u.chat;
1739 account = purple_conversation_get_account(conv);
1740 gc = account->gc;
1742 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
1744 sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(gtkchat->list));
1745 if(!gtk_tree_selection_get_selected(sel, NULL, &iter))
1746 return FALSE;
1748 gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, CHAT_USERS_NAME_COLUMN, &who, -1);
1749 menu = create_chat_menu (conv, who, gc);
1750 gtk_menu_popup(GTK_MENU(menu), NULL, NULL,
1751 pidgin_treeview_popup_menu_position_func, widget,
1752 0, GDK_CURRENT_TIME);
1753 g_free(who);
1755 return TRUE;
1759 static gint
1760 right_click_chat_cb(GtkWidget *widget, GdkEventButton *event,
1761 PidginConversation *gtkconv)
1763 PurpleConversation *conv = gtkconv->active_conv;
1764 PidginChatPane *gtkchat;
1765 PurpleConnection *gc;
1766 PurpleAccount *account;
1767 GtkTreePath *path;
1768 GtkTreeIter iter;
1769 GtkTreeModel *model;
1770 GtkTreeViewColumn *column;
1771 gchar *who;
1772 int x, y;
1774 gtkchat = gtkconv->u.chat;
1775 account = purple_conversation_get_account(conv);
1776 gc = account->gc;
1778 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
1780 gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(gtkchat->list),
1781 event->x, event->y, &path, &column, &x, &y);
1783 if (path == NULL)
1784 return FALSE;
1786 gtk_tree_selection_select_path(GTK_TREE_SELECTION(
1787 gtk_tree_view_get_selection(GTK_TREE_VIEW(gtkchat->list))), path);
1788 gtk_tree_view_set_cursor(GTK_TREE_VIEW(gtkchat->list),
1789 path, NULL, FALSE);
1790 gtk_widget_grab_focus(GTK_WIDGET(gtkchat->list));
1792 gtk_tree_model_get_iter(GTK_TREE_MODEL(model), &iter, path);
1793 gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, CHAT_USERS_NAME_COLUMN, &who, -1);
1795 /* emit chat-nick-clicked signal */
1796 if (event->type == GDK_BUTTON_PRESS) {
1797 gint plugin_return = GPOINTER_TO_INT(purple_signal_emit_return_1(
1798 pidgin_conversations_get_handle(), "chat-nick-clicked",
1799 conv, who, event->button));
1800 if (plugin_return)
1801 goto handled;
1804 if (event->button == 1 && event->type == GDK_2BUTTON_PRESS) {
1805 chat_do_im(gtkconv, who);
1806 } else if (event->button == 2 && event->type == GDK_BUTTON_PRESS) {
1807 /* Move to user's anchor */
1808 GtkTextMark *mark = get_mark_for_user(gtkconv, who);
1810 if(mark != NULL)
1811 gtk_text_view_scroll_to_mark(GTK_TEXT_VIEW(gtkconv->imhtml), mark, 0.1, FALSE, 0, 0);
1812 } else if (event->button == 3 && event->type == GDK_BUTTON_PRESS) {
1813 GtkWidget *menu = create_chat_menu (conv, who, gc);
1814 gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL,
1815 event->button, event->time);
1818 handled:
1819 g_free(who);
1820 gtk_tree_path_free(path);
1822 return TRUE;
1825 static void
1826 activate_list_cb(GtkTreeView *list, GtkTreePath *path, GtkTreeViewColumn *column, PidginConversation *gtkconv)
1828 GtkTreeIter iter;
1829 GtkTreeModel *model;
1830 gchar *who;
1832 model = gtk_tree_view_get_model(GTK_TREE_VIEW(list));
1834 gtk_tree_model_get_iter(GTK_TREE_MODEL(model), &iter, path);
1835 gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, CHAT_USERS_NAME_COLUMN, &who, -1);
1836 chat_do_im(gtkconv, who);
1838 g_free(who);
1841 static void
1842 move_to_next_unread_tab(PidginConversation *gtkconv, gboolean forward)
1844 PidginConversation *next_gtkconv = NULL, *most_active = NULL;
1845 PidginUnseenState unseen_state = PIDGIN_UNSEEN_NONE;
1846 PidginWindow *win;
1847 int initial, i, total, diff;
1849 win = gtkconv->win;
1850 initial = gtk_notebook_page_num(GTK_NOTEBOOK(win->notebook),
1851 gtkconv->tab_cont);
1852 total = pidgin_conv_window_get_gtkconv_count(win);
1853 /* By adding total here, the moduli calculated later will always have two
1854 * positive arguments. x % y where x < 0 is not guaranteed to return a
1855 * positive number.
1857 diff = (forward ? 1 : -1) + total;
1859 for (i = (initial + diff) % total; i != initial; i = (i + diff) % total) {
1860 next_gtkconv = pidgin_conv_window_get_gtkconv_at_index(win, i);
1861 if (next_gtkconv->unseen_state > unseen_state) {
1862 most_active = next_gtkconv;
1863 unseen_state = most_active->unseen_state;
1864 if(PIDGIN_UNSEEN_NICK == unseen_state) /* highest possible state */
1865 break;
1869 if (most_active == NULL) { /* no new messages */
1870 i = (i + diff) % total;
1871 most_active = pidgin_conv_window_get_gtkconv_at_index(win, i);
1874 if (most_active != NULL && most_active != gtkconv)
1875 pidgin_conv_window_switch_gtkconv(win, most_active);
1878 static gboolean
1879 gtkconv_cycle_focus(PidginConversation *gtkconv, GtkDirectionType dir)
1881 PurpleConversation *conv = gtkconv->active_conv;
1882 gboolean chat = purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT;
1883 GtkWidget *next = NULL;
1884 struct {
1885 GtkWidget *from;
1886 GtkWidget *to;
1887 } transitions[] = {
1888 {gtkconv->entry, gtkconv->imhtml},
1889 {gtkconv->imhtml, chat ? gtkconv->u.chat->list : gtkconv->entry},
1890 {chat ? gtkconv->u.chat->list : NULL, gtkconv->entry},
1891 {NULL, NULL}
1892 }, *ptr;
1894 for (ptr = transitions; !next && ptr->from; ptr++) {
1895 GtkWidget *from, *to;
1896 if (dir == GTK_DIR_TAB_FORWARD) {
1897 from = ptr->from;
1898 to = ptr->to;
1899 } else {
1900 from = ptr->to;
1901 to = ptr->from;
1903 if (gtk_widget_is_focus(from))
1904 next = to;
1907 if (next)
1908 gtk_widget_grab_focus(next);
1909 return !!next;
1912 static gboolean
1913 conv_keypress_common(PidginConversation *gtkconv, GdkEventKey *event)
1915 PidginWindow *win;
1916 int curconv;
1918 win = gtkconv->win;
1919 curconv = gtk_notebook_get_current_page(GTK_NOTEBOOK(win->notebook));
1921 /* clear any tooltips */
1922 pidgin_tooltip_destroy();
1924 /* If CTRL was held down... */
1925 if (event->state & GDK_CONTROL_MASK) {
1926 switch (event->keyval) {
1927 case GDK_Page_Down:
1928 case GDK_KP_Page_Down:
1929 case ']':
1930 if (!pidgin_conv_window_get_gtkconv_at_index(win, curconv + 1))
1931 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), 0);
1932 else
1933 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), curconv + 1);
1934 return TRUE;
1935 break;
1937 case GDK_Page_Up:
1938 case GDK_KP_Page_Up:
1939 case '[':
1940 if (!pidgin_conv_window_get_gtkconv_at_index(win, curconv - 1))
1941 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), -1);
1942 else
1943 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), curconv - 1);
1944 return TRUE;
1945 break;
1947 case GDK_Tab:
1948 case GDK_KP_Tab:
1949 case GDK_ISO_Left_Tab:
1950 if (event->state & GDK_SHIFT_MASK) {
1951 move_to_next_unread_tab(gtkconv, FALSE);
1952 } else {
1953 move_to_next_unread_tab(gtkconv, TRUE);
1956 return TRUE;
1957 break;
1959 case GDK_comma:
1960 gtk_notebook_reorder_child(GTK_NOTEBOOK(win->notebook),
1961 gtk_notebook_get_nth_page(GTK_NOTEBOOK(win->notebook), curconv),
1962 curconv - 1);
1963 return TRUE;
1964 break;
1966 case GDK_period:
1967 gtk_notebook_reorder_child(GTK_NOTEBOOK(win->notebook),
1968 gtk_notebook_get_nth_page(GTK_NOTEBOOK(win->notebook), curconv),
1969 (curconv + 1) % gtk_notebook_get_n_pages(GTK_NOTEBOOK(win->notebook)));
1970 return TRUE;
1971 break;
1972 case GDK_F6:
1973 if (gtkconv_cycle_focus(gtkconv, event->state & GDK_SHIFT_MASK ? GTK_DIR_TAB_BACKWARD : GTK_DIR_TAB_FORWARD))
1974 return TRUE;
1975 break;
1976 } /* End of switch */
1979 /* If ALT (or whatever) was held down... */
1980 else if (event->state & GDK_MOD1_MASK)
1982 if (event->keyval > '0' && event->keyval <= '9')
1984 guint switchto = event->keyval - '1';
1985 if (switchto < pidgin_conv_window_get_gtkconv_count(win))
1986 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), switchto);
1988 return TRUE;
1992 /* If neither CTRL nor ALT were held down... */
1993 else
1995 switch (event->keyval) {
1996 case GDK_F2:
1997 if (gtk_widget_is_focus(GTK_WIDGET(win->notebook))) {
1998 infopane_entry_activate(gtkconv);
1999 return TRUE;
2001 break;
2002 case GDK_F6:
2003 if (gtkconv_cycle_focus(gtkconv, event->state & GDK_SHIFT_MASK ? GTK_DIR_TAB_BACKWARD : GTK_DIR_TAB_FORWARD))
2004 return TRUE;
2005 break;
2008 return FALSE;
2011 static gboolean
2012 entry_key_press_cb(GtkWidget *entry, GdkEventKey *event, gpointer data)
2014 PurpleConversation *conv;
2015 PidginConversation *gtkconv;
2017 gtkconv = (PidginConversation *)data;
2018 conv = gtkconv->active_conv;
2020 if (conv_keypress_common(gtkconv, event))
2021 return TRUE;
2023 /* If CTRL was held down... */
2024 if (event->state & GDK_CONTROL_MASK) {
2025 switch (event->keyval) {
2026 case GDK_Up:
2027 if (!gtkconv->send_history)
2028 break;
2030 if (gtkconv->entry != entry)
2031 break;
2033 if (!gtkconv->send_history->prev) {
2034 GtkTextIter start, end;
2036 g_free(gtkconv->send_history->data);
2038 gtk_text_buffer_get_start_iter(gtkconv->entry_buffer,
2039 &start);
2040 gtk_text_buffer_get_end_iter(gtkconv->entry_buffer, &end);
2042 gtkconv->send_history->data =
2043 gtk_imhtml_get_markup(GTK_IMHTML(gtkconv->entry));
2046 if (gtkconv->send_history->next && gtkconv->send_history->next->data) {
2047 GObject *object;
2048 GtkTextIter iter;
2049 GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(gtkconv->entry));
2051 gtkconv->send_history = gtkconv->send_history->next;
2053 /* Block the signal to prevent application of default formatting. */
2054 object = g_object_ref(G_OBJECT(gtkconv->entry));
2055 g_signal_handlers_block_matched(object, G_SIGNAL_MATCH_DATA, 0, 0, NULL,
2056 NULL, gtkconv);
2057 /* Clear the formatting. */
2058 gtk_imhtml_clear_formatting(GTK_IMHTML(gtkconv->entry));
2059 /* Unblock the signal. */
2060 g_signal_handlers_unblock_matched(object, G_SIGNAL_MATCH_DATA, 0, 0, NULL,
2061 NULL, gtkconv);
2062 g_object_unref(object);
2064 gtk_imhtml_clear(GTK_IMHTML(gtkconv->entry));
2065 gtk_imhtml_append_text_with_images(
2066 GTK_IMHTML(gtkconv->entry), gtkconv->send_history->data,
2067 0, NULL);
2068 /* this is mainly just a hack so the formatting at the
2069 * cursor gets picked up. */
2070 gtk_text_buffer_get_end_iter(buffer, &iter);
2071 gtk_text_buffer_move_mark_by_name(buffer, "insert", &iter);
2074 return TRUE;
2075 break;
2077 case GDK_Down:
2078 if (!gtkconv->send_history)
2079 break;
2081 if (gtkconv->entry != entry)
2082 break;
2084 if (gtkconv->send_history->prev && gtkconv->send_history->prev->data) {
2085 GObject *object;
2086 GtkTextIter iter;
2087 GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(gtkconv->entry));
2089 gtkconv->send_history = gtkconv->send_history->prev;
2091 /* Block the signal to prevent application of default formatting. */
2092 object = g_object_ref(G_OBJECT(gtkconv->entry));
2093 g_signal_handlers_block_matched(object, G_SIGNAL_MATCH_DATA, 0, 0, NULL,
2094 NULL, gtkconv);
2095 /* Clear the formatting. */
2096 gtk_imhtml_clear_formatting(GTK_IMHTML(gtkconv->entry));
2097 /* Unblock the signal. */
2098 g_signal_handlers_unblock_matched(object, G_SIGNAL_MATCH_DATA, 0, 0, NULL,
2099 NULL, gtkconv);
2100 g_object_unref(object);
2102 gtk_imhtml_clear(GTK_IMHTML(gtkconv->entry));
2103 gtk_imhtml_append_text_with_images(
2104 GTK_IMHTML(gtkconv->entry), gtkconv->send_history->data,
2105 0, NULL);
2106 /* this is mainly just a hack so the formatting at the
2107 * cursor gets picked up. */
2108 if (*(char *)gtkconv->send_history->data) {
2109 gtk_text_buffer_get_end_iter(buffer, &iter);
2110 gtk_text_buffer_move_mark_by_name(buffer, "insert", &iter);
2111 } else {
2112 /* Restore the default formatting */
2113 default_formatize(gtkconv);
2117 return TRUE;
2118 break;
2119 } /* End of switch */
2122 /* If ALT (or whatever) was held down... */
2123 else if (event->state & GDK_MOD1_MASK) {
2127 /* If neither CTRL nor ALT were held down... */
2128 else {
2129 switch (event->keyval) {
2130 case GDK_Tab:
2131 case GDK_KP_Tab:
2132 case GDK_ISO_Left_Tab:
2133 if (gtkconv->entry != entry)
2134 break;
2136 gint plugin_return;
2137 plugin_return = GPOINTER_TO_INT(purple_signal_emit_return_1(
2138 pidgin_conversations_get_handle(), "chat-nick-autocomplete",
2139 conv, event->state & GDK_SHIFT_MASK));
2140 return plugin_return ? TRUE : tab_complete(conv);
2142 break;
2144 case GDK_Page_Up:
2145 case GDK_KP_Page_Up:
2146 gtk_imhtml_page_up(GTK_IMHTML(gtkconv->imhtml));
2147 return TRUE;
2148 break;
2150 case GDK_Page_Down:
2151 case GDK_KP_Page_Down:
2152 gtk_imhtml_page_down(GTK_IMHTML(gtkconv->imhtml));
2153 return TRUE;
2154 break;
2158 return FALSE;
2162 * NOTE:
2163 * This guy just kills a single right click from being propagated any
2164 * further. I have no idea *why* we need this, but we do ... It
2165 * prevents right clicks on the GtkTextView in a convo dialog from
2166 * going all the way down to the notebook. I suspect a bug in
2167 * GtkTextView, but I'm not ready to point any fingers yet.
2169 static gboolean
2170 entry_stop_rclick_cb(GtkWidget *widget, GdkEventButton *event, gpointer data)
2172 if (event->button == 3 && event->type == GDK_BUTTON_PRESS) {
2173 /* Right single click */
2174 g_signal_stop_emission_by_name(G_OBJECT(widget), "button_press_event");
2176 return TRUE;
2179 return FALSE;
2183 * If someone tries to type into the conversation backlog of a
2184 * conversation window then we yank focus from the conversation backlog
2185 * and give it to the text entry box so that people can type
2186 * all the live long day and it will get entered into the entry box.
2188 static gboolean
2189 refocus_entry_cb(GtkWidget *widget, GdkEventKey *event, gpointer data)
2191 PidginConversation *gtkconv = data;
2193 /* If we have a valid key for the conversation display, then exit */
2194 if ((event->state & GDK_CONTROL_MASK) ||
2195 (event->keyval == GDK_F6) ||
2196 (event->keyval == GDK_F10) ||
2197 (event->keyval == GDK_Shift_L) ||
2198 (event->keyval == GDK_Shift_R) ||
2199 (event->keyval == GDK_Control_L) ||
2200 (event->keyval == GDK_Control_R) ||
2201 (event->keyval == GDK_Escape) ||
2202 (event->keyval == GDK_Up) ||
2203 (event->keyval == GDK_Down) ||
2204 (event->keyval == GDK_Left) ||
2205 (event->keyval == GDK_Right) ||
2206 (event->keyval == GDK_Page_Up) ||
2207 (event->keyval == GDK_KP_Page_Up) ||
2208 (event->keyval == GDK_Page_Down) ||
2209 (event->keyval == GDK_KP_Page_Down) ||
2210 (event->keyval == GDK_Home) ||
2211 (event->keyval == GDK_End) ||
2212 (event->keyval == GDK_Tab) ||
2213 (event->keyval == GDK_KP_Tab) ||
2214 (event->keyval == GDK_ISO_Left_Tab))
2216 if (event->type == GDK_KEY_PRESS)
2217 return conv_keypress_common(gtkconv, event);
2218 return FALSE;
2221 if (event->type == GDK_KEY_RELEASE)
2222 gtk_widget_grab_focus(gtkconv->entry);
2224 gtk_widget_event(gtkconv->entry, (GdkEvent *)event);
2226 return TRUE;
2229 static void
2230 regenerate_options_items(PidginWindow *win);
2232 void
2233 pidgin_conv_switch_active_conversation(PurpleConversation *conv)
2235 PidginConversation *gtkconv;
2236 PurpleConversation *old_conv;
2237 GtkIMHtml *entry;
2238 const char *protocol_name;
2240 g_return_if_fail(conv != NULL);
2242 gtkconv = PIDGIN_CONVERSATION(conv);
2243 old_conv = gtkconv->active_conv;
2245 purple_debug_info("gtkconv", "setting active conversation on toolbar %p\n",
2246 conv);
2247 gtk_imhtmltoolbar_switch_active_conversation(GTK_IMHTMLTOOLBAR(gtkconv->toolbar),
2248 conv);
2250 if (old_conv == conv)
2251 return;
2253 purple_conversation_close_logs(old_conv);
2254 gtkconv->active_conv = conv;
2256 purple_conversation_set_logging(conv,
2257 gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(gtkconv->win->menu.logging)));
2259 entry = GTK_IMHTML(gtkconv->entry);
2260 protocol_name = purple_account_get_protocol_name(conv->account);
2261 gtk_imhtml_set_protocol_name(entry, protocol_name);
2262 gtk_imhtml_set_protocol_name(GTK_IMHTML(gtkconv->imhtml), protocol_name);
2264 if (!(conv->features & PURPLE_CONNECTION_HTML))
2265 gtk_imhtml_clear_formatting(GTK_IMHTML(gtkconv->entry));
2266 else if (conv->features & PURPLE_CONNECTION_FORMATTING_WBFO &&
2267 !(old_conv->features & PURPLE_CONNECTION_FORMATTING_WBFO))
2269 /* The old conversation allowed formatting on parts of the
2270 * buffer, but the new one only allows it on the whole
2271 * buffer. This code saves the formatting from the current
2272 * position of the cursor, clears the formatting, then
2273 * applies the saved formatting to the entire buffer. */
2275 gboolean bold;
2276 gboolean italic;
2277 gboolean underline;
2278 char *fontface = gtk_imhtml_get_current_fontface(entry);
2279 char *forecolor = gtk_imhtml_get_current_forecolor(entry);
2280 char *backcolor = gtk_imhtml_get_current_backcolor(entry);
2281 char *background = gtk_imhtml_get_current_background(entry);
2282 gint fontsize = gtk_imhtml_get_current_fontsize(entry);
2283 gboolean bold2;
2284 gboolean italic2;
2285 gboolean underline2;
2287 gtk_imhtml_get_current_format(entry, &bold, &italic, &underline);
2289 /* Clear existing formatting */
2290 gtk_imhtml_clear_formatting(entry);
2292 /* Apply saved formatting to the whole buffer. */
2294 gtk_imhtml_get_current_format(entry, &bold2, &italic2, &underline2);
2296 if (bold != bold2)
2297 gtk_imhtml_toggle_bold(entry);
2299 if (italic != italic2)
2300 gtk_imhtml_toggle_italic(entry);
2302 if (underline != underline2)
2303 gtk_imhtml_toggle_underline(entry);
2305 gtk_imhtml_toggle_fontface(entry, fontface);
2307 if (!(conv->features & PURPLE_CONNECTION_NO_FONTSIZE))
2308 gtk_imhtml_font_set_size(entry, fontsize);
2310 gtk_imhtml_toggle_forecolor(entry, forecolor);
2312 if (!(conv->features & PURPLE_CONNECTION_NO_BGCOLOR))
2314 gtk_imhtml_toggle_backcolor(entry, backcolor);
2315 gtk_imhtml_toggle_background(entry, background);
2318 g_free(fontface);
2319 g_free(forecolor);
2320 g_free(backcolor);
2321 g_free(background);
2323 else
2325 /* This is done in default_formatize, which is called from clear_formatting_cb,
2326 * which is (obviously) a clear_formatting signal handler. However, if we're
2327 * here, we didn't call gtk_imhtml_clear_formatting() (because we want to
2328 * preserve the formatting exactly as it is), so we have to do this now. */
2329 gtk_imhtml_set_whole_buffer_formatting_only(entry,
2330 (conv->features & PURPLE_CONNECTION_FORMATTING_WBFO));
2333 purple_signal_emit(pidgin_conversations_get_handle(), "conversation-switched", conv);
2335 gray_stuff_out(gtkconv);
2336 update_typing_icon(gtkconv);
2337 g_object_set_data(G_OBJECT(entry), "transient_buddy", NULL);
2338 regenerate_options_items(gtkconv->win);
2340 gtk_window_set_title(GTK_WINDOW(gtkconv->win->window),
2341 gtk_label_get_text(GTK_LABEL(gtkconv->tab_label)));
2344 static void
2345 menu_conv_sel_send_cb(GObject *m, gpointer data)
2347 PurpleAccount *account = g_object_get_data(m, "purple_account");
2348 gchar *name = g_object_get_data(m, "purple_buddy_name");
2349 PurpleConversation *conv;
2351 if (gtk_check_menu_item_get_active((GtkCheckMenuItem*) m) == FALSE)
2352 return;
2354 conv = purple_conversation_new(PURPLE_CONV_TYPE_IM, account, name);
2355 pidgin_conv_switch_active_conversation(conv);
2358 static void
2359 insert_text_cb(GtkTextBuffer *textbuffer, GtkTextIter *position,
2360 gchar *new_text, gint new_text_length, gpointer user_data)
2362 PidginConversation *gtkconv = (PidginConversation *)user_data;
2364 g_return_if_fail(gtkconv != NULL);
2366 if (!purple_prefs_get_bool("/purple/conversations/im/send_typing"))
2367 return;
2369 got_typing_keypress(gtkconv, (gtk_text_iter_is_start(position) &&
2370 gtk_text_iter_is_end(position)));
2373 static void
2374 delete_text_cb(GtkTextBuffer *textbuffer, GtkTextIter *start_pos,
2375 GtkTextIter *end_pos, gpointer user_data)
2377 PidginConversation *gtkconv = (PidginConversation *)user_data;
2378 PurpleConversation *conv;
2379 PurpleConvIm *im;
2381 g_return_if_fail(gtkconv != NULL);
2383 conv = gtkconv->active_conv;
2385 if (!purple_prefs_get_bool("/purple/conversations/im/send_typing"))
2386 return;
2388 im = PURPLE_CONV_IM(conv);
2390 if (gtk_text_iter_is_start(start_pos) && gtk_text_iter_is_end(end_pos)) {
2392 /* We deleted all the text, so turn off typing. */
2393 purple_conv_im_stop_send_typed_timeout(im);
2395 serv_send_typing(purple_conversation_get_gc(conv),
2396 purple_conversation_get_name(conv),
2397 PURPLE_NOT_TYPING);
2399 else {
2400 /* We're deleting, but not all of it, so it counts as typing. */
2401 got_typing_keypress(gtkconv, FALSE);
2405 /**************************************************************************
2406 * A bunch of buddy icon functions
2407 **************************************************************************/
2409 static GList *get_prpl_icon_list(PurpleAccount *account)
2411 GList *l = NULL;
2412 PurplePlugin *prpl = purple_find_prpl(purple_account_get_protocol_id(account));
2413 PurplePluginProtocolInfo *prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(prpl);
2414 const char *prplname = prpl_info->list_icon(account, NULL);
2415 l = g_hash_table_lookup(prpl_lists, prplname);
2416 if (l)
2417 return l;
2419 l = g_list_prepend(l, pidgin_create_prpl_icon(account, PIDGIN_PRPL_ICON_LARGE));
2420 l = g_list_prepend(l, pidgin_create_prpl_icon(account, PIDGIN_PRPL_ICON_MEDIUM));
2421 l = g_list_prepend(l, pidgin_create_prpl_icon(account, PIDGIN_PRPL_ICON_SMALL));
2423 g_hash_table_insert(prpl_lists, g_strdup(prplname), l);
2424 return l;
2427 static GList *
2428 pidgin_conv_get_tab_icons(PurpleConversation *conv)
2430 PurpleAccount *account = NULL;
2431 const char *name = NULL;
2433 g_return_val_if_fail(conv != NULL, NULL);
2435 account = purple_conversation_get_account(conv);
2436 name = purple_conversation_get_name(conv);
2438 g_return_val_if_fail(account != NULL, NULL);
2439 g_return_val_if_fail(name != NULL, NULL);
2441 /* Use the buddy icon, if possible */
2442 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) {
2443 PurpleBuddy *b = purple_find_buddy(account, name);
2444 if (b != NULL) {
2445 PurplePresence *p;
2446 p = purple_buddy_get_presence(b);
2447 if (purple_presence_is_status_primitive_active(p, PURPLE_STATUS_AWAY))
2448 return away_list;
2449 if (purple_presence_is_status_primitive_active(p, PURPLE_STATUS_UNAVAILABLE))
2450 return busy_list;
2451 if (purple_presence_is_status_primitive_active(p, PURPLE_STATUS_EXTENDED_AWAY))
2452 return xa_list;
2453 if (purple_presence_is_status_primitive_active(p, PURPLE_STATUS_OFFLINE))
2454 return offline_list;
2455 else
2456 return available_list;
2460 return get_prpl_icon_list(account);
2463 static const char *
2464 pidgin_conv_get_icon_stock(PurpleConversation *conv)
2466 PurpleAccount *account = NULL;
2467 const char *stock = NULL;
2469 g_return_val_if_fail(conv != NULL, NULL);
2471 account = purple_conversation_get_account(conv);
2472 g_return_val_if_fail(account != NULL, NULL);
2474 /* Use the buddy icon, if possible */
2475 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) {
2476 const char *name = NULL;
2477 PurpleBuddy *b;
2478 name = purple_conversation_get_name(conv);
2479 b = purple_find_buddy(account, name);
2480 if (b != NULL) {
2481 PurplePresence *p = purple_buddy_get_presence(b);
2482 PurpleStatus *active = purple_presence_get_active_status(p);
2483 PurpleStatusType *type = purple_status_get_type(active);
2484 PurpleStatusPrimitive prim = purple_status_type_get_primitive(type);
2485 stock = pidgin_stock_id_from_status_primitive(prim);
2486 } else {
2487 stock = PIDGIN_STOCK_STATUS_PERSON;
2489 } else {
2490 stock = PIDGIN_STOCK_STATUS_CHAT;
2493 return stock;
2496 static GdkPixbuf *
2497 pidgin_conv_get_icon(PurpleConversation *conv, GtkWidget *parent, const char *icon_size)
2499 PurpleAccount *account = NULL;
2500 const char *name = NULL;
2501 const char *stock = NULL;
2502 GdkPixbuf *status = NULL;
2503 PurpleBlistUiOps *ops = purple_blist_get_ui_ops();
2504 GtkIconSize size;
2506 g_return_val_if_fail(conv != NULL, NULL);
2508 account = purple_conversation_get_account(conv);
2509 name = purple_conversation_get_name(conv);
2511 g_return_val_if_fail(account != NULL, NULL);
2512 g_return_val_if_fail(name != NULL, NULL);
2514 /* Use the buddy icon, if possible */
2515 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) {
2516 PurpleBuddy *b = purple_find_buddy(account, name);
2517 if (b != NULL) {
2518 /* I hate this hack. It fixes a bug where the pending message icon
2519 * displays in the conv tab even though it shouldn't.
2520 * A better solution would be great. */
2521 if (ops && ops->update)
2522 ops->update(NULL, (PurpleBlistNode*)b);
2526 stock = pidgin_conv_get_icon_stock(conv);
2527 size = gtk_icon_size_from_name(icon_size);
2528 status = gtk_widget_render_icon (parent, stock, size, "GtkWidget");
2529 return status;
2532 GdkPixbuf *
2533 pidgin_conv_get_tab_icon(PurpleConversation *conv, gboolean small_icon)
2535 const char *icon_size = small_icon ? PIDGIN_ICON_SIZE_TANGO_MICROSCOPIC : PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL;
2536 return pidgin_conv_get_icon(conv, PIDGIN_CONVERSATION(conv)->icon, icon_size);
2540 static void
2541 update_tab_icon(PurpleConversation *conv)
2543 PidginConversation *gtkconv;
2544 PidginWindow *win;
2545 GList *l;
2546 GdkPixbuf *emblem = NULL;
2547 const char *status = NULL;
2548 const char *infopane_status = NULL;
2550 g_return_if_fail(conv != NULL);
2552 gtkconv = PIDGIN_CONVERSATION(conv);
2553 win = gtkconv->win;
2554 if (conv != gtkconv->active_conv)
2555 return;
2557 status = infopane_status = pidgin_conv_get_icon_stock(conv);
2559 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) {
2560 PurpleBuddy *b = purple_find_buddy(conv->account, conv->name);
2561 if (b)
2562 emblem = pidgin_blist_get_emblem((PurpleBlistNode*)b);
2565 g_return_if_fail(status != NULL);
2567 g_object_set(G_OBJECT(gtkconv->icon), "stock", status, NULL);
2568 g_object_set(G_OBJECT(gtkconv->menu_icon), "stock", status, NULL);
2570 gtk_list_store_set(GTK_LIST_STORE(gtkconv->infopane_model),
2571 &(gtkconv->infopane_iter),
2572 CONV_ICON_COLUMN, infopane_status, -1);
2574 gtk_list_store_set(GTK_LIST_STORE(gtkconv->infopane_model),
2575 &(gtkconv->infopane_iter),
2576 CONV_EMBLEM_COLUMN, emblem, -1);
2577 if (emblem)
2578 g_object_unref(emblem);
2580 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/blist/show_protocol_icons")) {
2581 emblem = pidgin_create_prpl_icon(gtkconv->active_conv->account, PIDGIN_PRPL_ICON_SMALL);
2582 } else {
2583 emblem = NULL;
2586 gtk_list_store_set(GTK_LIST_STORE(gtkconv->infopane_model),
2587 &(gtkconv->infopane_iter),
2588 CONV_PROTOCOL_ICON_COLUMN, emblem, -1);
2589 if (emblem)
2590 g_object_unref(emblem);
2592 /* XXX seanegan Why do I have to do this? */
2593 gtk_widget_queue_resize(gtkconv->infopane);
2594 gtk_widget_queue_draw(gtkconv->infopane);
2596 if (pidgin_conv_window_is_active_conversation(conv) &&
2597 (purple_conversation_get_type(conv) != PURPLE_CONV_TYPE_IM ||
2598 gtkconv->u.im->anim == NULL))
2600 l = pidgin_conv_get_tab_icons(conv);
2602 gtk_window_set_icon_list(GTK_WINDOW(win->window), l);
2606 #if 0
2607 /* This gets added as an idle handler when doing something that
2608 * redraws the icon. It sets the auto_resize gboolean to TRUE.
2609 * This way, when the size_allocate callback gets triggered, it notices
2610 * that this is an autoresize, and after the main loop iterates, it
2611 * gets set back to FALSE
2613 static gboolean reset_auto_resize_cb(gpointer data)
2615 PidginConversation *gtkconv = (PidginConversation *)data;
2616 gtkconv->auto_resize = FALSE;
2617 return FALSE;
2619 #endif
2621 static gboolean
2622 redraw_icon(gpointer data)
2624 PidginConversation *gtkconv = (PidginConversation *)data;
2625 PurpleConversation *conv = gtkconv->active_conv;
2626 PurpleAccount *account;
2628 GdkPixbuf *buf;
2629 GdkPixbuf *scale;
2630 gint delay;
2631 int scale_width, scale_height;
2632 int size;
2634 gtkconv = PIDGIN_CONVERSATION(conv);
2635 account = purple_conversation_get_account(conv);
2637 if (!(account && account->gc)) {
2638 gtkconv->u.im->icon_timer = 0;
2639 return FALSE;
2642 gdk_pixbuf_animation_iter_advance(gtkconv->u.im->iter, NULL);
2643 buf = gdk_pixbuf_animation_iter_get_pixbuf(gtkconv->u.im->iter);
2645 scale_width = gdk_pixbuf_get_width(buf);
2646 scale_height = gdk_pixbuf_get_height(buf);
2648 gtk_widget_get_size_request(gtkconv->u.im->icon_container, NULL, &size);
2649 size = MIN(size, MIN(scale_width, scale_height));
2650 size = CLAMP(size, BUDDYICON_SIZE_MIN, BUDDYICON_SIZE_MAX);
2652 if (scale_width == scale_height) {
2653 scale_width = scale_height = size;
2654 } else if (scale_height > scale_width) {
2655 scale_width = size * scale_width / scale_height;
2656 scale_height = size;
2657 } else {
2658 scale_height = size * scale_height / scale_width;
2659 scale_width = size;
2662 scale = gdk_pixbuf_scale_simple(buf, scale_width, scale_height,
2663 GDK_INTERP_BILINEAR);
2664 if (pidgin_gdk_pixbuf_is_opaque(scale))
2665 pidgin_gdk_pixbuf_make_round(scale);
2667 gtk_image_set_from_pixbuf(GTK_IMAGE(gtkconv->u.im->icon), scale);
2668 g_object_unref(G_OBJECT(scale));
2669 gtk_widget_queue_draw(gtkconv->u.im->icon);
2671 delay = gdk_pixbuf_animation_iter_get_delay_time(gtkconv->u.im->iter);
2673 if (delay < 100)
2674 delay = 100;
2676 gtkconv->u.im->icon_timer = g_timeout_add(delay, redraw_icon, gtkconv);
2678 return FALSE;
2681 static void
2682 start_anim(GtkObject *obj, PidginConversation *gtkconv)
2684 int delay;
2686 if (gtkconv->u.im->anim == NULL)
2687 return;
2689 if (gtkconv->u.im->icon_timer != 0)
2690 return;
2692 if (gdk_pixbuf_animation_is_static_image(gtkconv->u.im->anim))
2693 return;
2695 delay = gdk_pixbuf_animation_iter_get_delay_time(gtkconv->u.im->iter);
2697 if (delay < 100)
2698 delay = 100;
2700 gtkconv->u.im->icon_timer = g_timeout_add(delay, redraw_icon, gtkconv);
2703 static void
2704 remove_icon(GtkWidget *widget, PidginConversation *gtkconv)
2706 GList *children;
2707 GtkWidget *event;
2708 PurpleConversation *conv = gtkconv->active_conv;
2710 g_return_if_fail(conv != NULL);
2712 gtk_widget_set_size_request(gtkconv->u.im->icon_container, -1, BUDDYICON_SIZE_MIN);
2713 children = gtk_container_get_children(GTK_CONTAINER(gtkconv->u.im->icon_container));
2714 if (children) {
2715 /* We know there's only one child here. It'd be nice to shortcut to the
2716 event box, but we can't change the PidginConversation until 3.0 */
2717 event = (GtkWidget *)children->data;
2718 gtk_container_remove(GTK_CONTAINER(gtkconv->u.im->icon_container), event);
2719 g_list_free(children);
2722 if (gtkconv->u.im->anim != NULL)
2723 g_object_unref(G_OBJECT(gtkconv->u.im->anim));
2725 if (gtkconv->u.im->icon_timer != 0)
2726 g_source_remove(gtkconv->u.im->icon_timer);
2728 if (gtkconv->u.im->iter != NULL)
2729 g_object_unref(G_OBJECT(gtkconv->u.im->iter));
2731 gtkconv->u.im->icon_timer = 0;
2732 gtkconv->u.im->icon = NULL;
2733 gtkconv->u.im->anim = NULL;
2734 gtkconv->u.im->iter = NULL;
2735 gtkconv->u.im->show_icon = FALSE;
2738 static void
2739 saveicon_writefile_cb(void *user_data, const char *filename)
2741 PidginConversation *gtkconv = (PidginConversation *)user_data;
2742 PurpleConversation *conv = gtkconv->active_conv;
2743 PurpleBuddyIcon *icon;
2744 const void *data;
2745 size_t len;
2747 icon = purple_conv_im_get_icon(PURPLE_CONV_IM(conv));
2748 data = purple_buddy_icon_get_data(icon, &len);
2750 if ((len <= 0) || (data == NULL) || !purple_util_write_data_to_file_absolute(filename, data, len)) {
2751 purple_notify_error(gtkconv, NULL, _("Unable to save icon file to disk."), NULL);
2755 static void
2756 custom_icon_sel_cb(const char *filename, gpointer data)
2758 if (filename) {
2759 const gchar *name;
2760 PurpleBuddy *buddy;
2761 PurpleContact *contact;
2762 PidginConversation *gtkconv = data;
2763 PurpleConversation *conv = gtkconv->active_conv;
2764 PurpleAccount *account = purple_conversation_get_account(conv);
2766 name = purple_conversation_get_name(conv);
2767 buddy = purple_find_buddy(account, name);
2768 if (!buddy) {
2769 purple_debug_info("custom-icon", "You can only set custom icons for people on your buddylist.\n");
2770 return;
2772 contact = purple_buddy_get_contact(buddy);
2774 purple_buddy_icons_node_set_custom_icon_from_file((PurpleBlistNode*)contact, filename);
2778 static void
2779 set_custom_icon_cb(GtkWidget *widget, PidginConversation *gtkconv)
2781 GtkWidget *win = pidgin_buddy_icon_chooser_new(GTK_WINDOW(gtkconv->win->window),
2782 custom_icon_sel_cb, gtkconv);
2783 gtk_widget_show_all(win);
2786 static void
2787 change_size_cb(GtkWidget *widget, PidginConversation *gtkconv)
2789 int size = 0;
2790 PurpleConversation *conv = gtkconv->active_conv;
2791 GSList *buddies;
2793 gtk_widget_get_size_request(gtkconv->u.im->icon_container, NULL, &size);
2795 if (size == BUDDYICON_SIZE_MAX) {
2796 size = BUDDYICON_SIZE_MIN;
2797 } else {
2798 size = BUDDYICON_SIZE_MAX;
2801 gtk_widget_set_size_request(gtkconv->u.im->icon_container, -1, size);
2802 pidgin_conv_update_buddy_icon(conv);
2804 buddies = purple_find_buddies(purple_conversation_get_account(conv),
2805 purple_conversation_get_name(conv));
2806 for (; buddies; buddies = g_slist_delete_link(buddies, buddies)) {
2807 PurpleBuddy *buddy = buddies->data;
2808 PurpleContact *contact = purple_buddy_get_contact(buddy);
2809 purple_blist_node_set_int((PurpleBlistNode*)contact, "pidgin-infopane-iconsize", size);
2813 static void
2814 remove_custom_icon_cb(GtkWidget *widget, PidginConversation *gtkconv)
2816 const gchar *name;
2817 PurpleBuddy *buddy;
2818 PurpleAccount *account;
2819 PurpleContact *contact;
2820 PurpleConversation *conv = gtkconv->active_conv;
2822 account = purple_conversation_get_account(conv);
2823 name = purple_conversation_get_name(conv);
2824 buddy = purple_find_buddy(account, name);
2825 if (!buddy) {
2826 return;
2828 contact = purple_buddy_get_contact(buddy);
2830 purple_buddy_icons_node_set_custom_icon_from_file((PurpleBlistNode*)contact, NULL);
2833 static void
2834 icon_menu_save_cb(GtkWidget *widget, PidginConversation *gtkconv)
2836 PurpleConversation *conv = gtkconv->active_conv;
2837 const gchar *ext;
2838 gchar *buf;
2840 g_return_if_fail(conv != NULL);
2842 ext = purple_buddy_icon_get_extension(purple_conv_im_get_icon(PURPLE_CONV_IM(conv)));
2844 buf = g_strdup_printf("%s.%s", purple_normalize(conv->account, conv->name), ext);
2846 purple_request_file(gtkconv, _("Save Icon"), buf, TRUE,
2847 G_CALLBACK(saveicon_writefile_cb), NULL,
2848 conv->account, NULL, conv,
2849 gtkconv);
2851 g_free(buf);
2854 static void
2855 stop_anim(GtkObject *obj, PidginConversation *gtkconv)
2857 if (gtkconv->u.im->icon_timer != 0)
2858 g_source_remove(gtkconv->u.im->icon_timer);
2860 gtkconv->u.im->icon_timer = 0;
2864 static void
2865 toggle_icon_animate_cb(GtkWidget *w, PidginConversation *gtkconv)
2867 gtkconv->u.im->animate =
2868 gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(w));
2870 if (gtkconv->u.im->animate)
2871 start_anim(NULL, gtkconv);
2872 else
2873 stop_anim(NULL, gtkconv);
2876 static gboolean
2877 icon_menu(GtkObject *obj, GdkEventButton *e, PidginConversation *gtkconv)
2879 static GtkWidget *menu = NULL;
2880 PurpleConversation *conv;
2881 PurpleBuddy *buddy;
2883 if (e->button == 1 && e->type == GDK_BUTTON_PRESS) {
2884 change_size_cb(NULL, gtkconv);
2885 return TRUE;
2888 if (e->button != 3 || e->type != GDK_BUTTON_PRESS) {
2889 return FALSE;
2893 * If a menu already exists, destroy it before creating a new one,
2894 * thus freeing-up the memory it occupied.
2896 if (menu != NULL)
2897 gtk_widget_destroy(menu);
2899 menu = gtk_menu_new();
2901 if (gtkconv->u.im->anim &&
2902 !(gdk_pixbuf_animation_is_static_image(gtkconv->u.im->anim)))
2904 pidgin_new_check_item(menu, _("Animate"),
2905 G_CALLBACK(toggle_icon_animate_cb), gtkconv,
2906 gtkconv->u.im->icon_timer);
2909 pidgin_new_item_from_stock(menu, _("Hide Icon"), NULL, G_CALLBACK(remove_icon),
2910 gtkconv, 0, 0, NULL);
2912 pidgin_new_item_from_stock(menu, _("Save Icon As..."), GTK_STOCK_SAVE_AS,
2913 G_CALLBACK(icon_menu_save_cb), gtkconv,
2914 0, 0, NULL);
2916 pidgin_new_item_from_stock(menu, _("Set Custom Icon..."), NULL,
2917 G_CALLBACK(set_custom_icon_cb), gtkconv,
2918 0, 0, NULL);
2920 pidgin_new_item_from_stock(menu, _("Change Size"), NULL,
2921 G_CALLBACK(change_size_cb), gtkconv,
2922 0, 0, NULL);
2924 /* Is there a custom icon for this person? */
2925 conv = gtkconv->active_conv;
2926 buddy = purple_find_buddy(purple_conversation_get_account(conv),
2927 purple_conversation_get_name(conv));
2928 if (buddy)
2930 PurpleContact *contact = purple_buddy_get_contact(buddy);
2931 if (contact && purple_buddy_icons_node_has_custom_icon((PurpleBlistNode*)contact))
2933 pidgin_new_item_from_stock(menu, _("Remove Custom Icon"), NULL,
2934 G_CALLBACK(remove_custom_icon_cb), gtkconv,
2935 0, 0, NULL);
2939 gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, e->button, e->time);
2941 return TRUE;
2944 /**************************************************************************
2945 * End of the bunch of buddy icon functions
2946 **************************************************************************/
2947 void
2948 pidgin_conv_present_conversation(PurpleConversation *conv)
2950 PidginConversation *gtkconv;
2951 GdkModifierType state;
2953 pidgin_conv_attach_to_conversation(conv);
2954 gtkconv = PIDGIN_CONVERSATION(conv);
2956 pidgin_conv_switch_active_conversation(conv);
2957 /* Switch the tab only if the user initiated the event by pressing
2958 * a button or hitting a key. */
2959 if (gtk_get_current_event_state(&state))
2960 pidgin_conv_window_switch_gtkconv(gtkconv->win, gtkconv);
2961 gtk_window_present(GTK_WINDOW(gtkconv->win->window));
2964 GList *
2965 pidgin_conversations_find_unseen_list(PurpleConversationType type,
2966 PidginUnseenState min_state,
2967 gboolean hidden_only,
2968 guint max_count)
2970 GList *l;
2971 GList *r = NULL;
2972 guint c = 0;
2974 if (type == PURPLE_CONV_TYPE_IM) {
2975 l = purple_get_ims();
2976 } else if (type == PURPLE_CONV_TYPE_CHAT) {
2977 l = purple_get_chats();
2978 } else {
2979 l = purple_get_conversations();
2982 for (; l != NULL && (max_count == 0 || c < max_count); l = l->next) {
2983 PurpleConversation *conv = (PurpleConversation*)l->data;
2984 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
2986 if(gtkconv == NULL || gtkconv->active_conv != conv)
2987 continue;
2989 if (gtkconv->unseen_state >= min_state
2990 && (!hidden_only ||
2991 (hidden_only && gtkconv->win == hidden_convwin))) {
2993 r = g_list_prepend(r, conv);
2994 c++;
2998 return r;
3001 static void
3002 unseen_conv_menu_cb(GtkMenuItem *item, PurpleConversation *conv)
3004 g_return_if_fail(conv != NULL);
3005 pidgin_conv_present_conversation(conv);
3008 static void
3009 unseen_all_conv_menu_cb(GtkMenuItem *item, GList *list)
3011 g_return_if_fail(list != NULL);
3012 /* Do not free the list from here. It will be freed from the
3013 * 'destroy' callback on the menuitem. */
3014 while (list) {
3015 pidgin_conv_present_conversation(list->data);
3016 list = list->next;
3020 guint
3021 pidgin_conversations_fill_menu(GtkWidget *menu, GList *convs)
3023 GList *l;
3024 guint ret=0;
3026 g_return_val_if_fail(menu != NULL, 0);
3027 g_return_val_if_fail(convs != NULL, 0);
3029 for (l = convs; l != NULL ; l = l->next) {
3030 PurpleConversation *conv = (PurpleConversation*)l->data;
3031 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
3033 GtkWidget *icon = gtk_image_new_from_stock(pidgin_conv_get_icon_stock(conv),
3034 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_MICROSCOPIC));
3035 GtkWidget *item;
3036 gchar *text = g_strdup_printf("%s (%d)",
3037 gtk_label_get_text(GTK_LABEL(gtkconv->tab_label)),
3038 gtkconv->unseen_count);
3040 item = gtk_image_menu_item_new_with_label(text);
3041 gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(item), icon);
3042 g_signal_connect(G_OBJECT(item), "activate", G_CALLBACK(unseen_conv_menu_cb), conv);
3043 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
3044 g_free(text);
3045 ret++;
3048 if (convs->next) {
3049 /* There are more than one conversation. Add an option to show all conversations. */
3050 GtkWidget *item;
3051 GList *list = g_list_copy(convs);
3053 pidgin_separator(menu);
3055 item = gtk_menu_item_new_with_label(_("Show All"));
3056 g_signal_connect(G_OBJECT(item), "activate", G_CALLBACK(unseen_all_conv_menu_cb), list);
3057 g_signal_connect_swapped(G_OBJECT(item), "destroy", G_CALLBACK(g_list_free), list);
3058 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
3061 return ret;
3064 PidginWindow *
3065 pidgin_conv_get_window(PidginConversation *gtkconv)
3067 g_return_val_if_fail(gtkconv != NULL, NULL);
3068 return gtkconv->win;
3071 static GtkItemFactoryEntry menu_items[] =
3073 /* Conversation menu */
3074 { N_("/_Conversation"), NULL, NULL, 0, "<Branch>", NULL },
3076 { N_("/Conversation/New Instant _Message..."), "<CTL>M", menu_new_conv_cb,
3077 0, "<StockItem>", PIDGIN_STOCK_TOOLBAR_MESSAGE_NEW },
3078 { N_("/Conversation/Join a _Chat..."), NULL, menu_join_chat_cb,
3079 0, "<StockItem>", PIDGIN_STOCK_CHAT },
3081 { "/Conversation/sep0", NULL, NULL, 0, "<Separator>", NULL },
3083 { N_("/Conversation/_Find..."), NULL, menu_find_cb, 0,
3084 "<StockItem>", GTK_STOCK_FIND },
3085 { N_("/Conversation/View _Log"), NULL, menu_view_log_cb, 0, "<Item>", NULL },
3086 { N_("/Conversation/_Save As..."), NULL, menu_save_as_cb, 0,
3087 "<StockItem>", GTK_STOCK_SAVE_AS },
3088 { N_("/Conversation/Clea_r Scrollback"), "<CTL>L", menu_clear_cb, 0, "<StockItem>", GTK_STOCK_CLEAR },
3090 { "/Conversation/sep1", NULL, NULL, 0, "<Separator>", NULL },
3092 #ifdef USE_VV
3093 { N_("/Conversation/M_edia"), NULL, NULL, 0, "<Branch>", NULL },
3095 { N_("/Conversation/Media/_Audio Call"), NULL, menu_initiate_media_call_cb, 0,
3096 "<StockItem>", PIDGIN_STOCK_TOOLBAR_AUDIO_CALL },
3097 { N_("/Conversation/Media/_Video Call"), NULL, menu_initiate_media_call_cb, 1,
3098 "<StockItem>", PIDGIN_STOCK_TOOLBAR_VIDEO_CALL },
3099 { N_("/Conversation/Media/Audio\\/Video _Call"), NULL, menu_initiate_media_call_cb, 2,
3100 "<StockItem>", PIDGIN_STOCK_TOOLBAR_VIDEO_CALL },
3101 #endif
3103 { N_("/Conversation/Se_nd File..."), NULL, menu_send_file_cb, 0, "<StockItem>", PIDGIN_STOCK_TOOLBAR_SEND_FILE },
3104 { N_("/Conversation/Get _Attention"), NULL, menu_get_attention_cb, 0, "<StockItem>", PIDGIN_STOCK_TOOLBAR_SEND_ATTENTION },
3105 { N_("/Conversation/Add Buddy _Pounce..."), NULL, menu_add_pounce_cb,
3106 0, "<Item>", NULL },
3107 { N_("/Conversation/_Get Info"), "<CTL>O", menu_get_info_cb, 0,
3108 "<StockItem>", PIDGIN_STOCK_TOOLBAR_USER_INFO },
3109 { N_("/Conversation/In_vite..."), NULL, menu_invite_cb, 0,
3110 "<Item>", NULL },
3111 { N_("/Conversation/M_ore"), NULL, NULL, 0, "<Branch>", NULL },
3113 { "/Conversation/sep2", NULL, NULL, 0, "<Separator>", NULL },
3115 { N_("/Conversation/Al_ias..."), NULL, menu_alias_cb, 0,
3116 "<Item>", NULL },
3117 { N_("/Conversation/_Block..."), NULL, menu_block_cb, 0,
3118 "<StockItem>", PIDGIN_STOCK_TOOLBAR_BLOCK },
3119 { N_("/Conversation/_Unblock..."), NULL, menu_unblock_cb, 0,
3120 "<StockItem>", PIDGIN_STOCK_TOOLBAR_UNBLOCK },
3121 { N_("/Conversation/_Add..."), NULL, menu_add_remove_cb, 0,
3122 "<StockItem>", GTK_STOCK_ADD },
3123 { N_("/Conversation/_Remove..."), NULL, menu_add_remove_cb, 0,
3124 "<StockItem>", GTK_STOCK_REMOVE },
3126 { "/Conversation/sep3", NULL, NULL, 0, "<Separator>", NULL },
3128 { N_("/Conversation/Insert Lin_k..."), NULL, menu_insert_link_cb, 0,
3129 "<StockItem>", PIDGIN_STOCK_TOOLBAR_INSERT_LINK },
3130 { N_("/Conversation/Insert Imag_e..."), NULL, menu_insert_image_cb, 0,
3131 "<StockItem>", PIDGIN_STOCK_TOOLBAR_INSERT_IMAGE },
3133 { "/Conversation/sep4", NULL, NULL, 0, "<Separator>", NULL },
3136 { N_("/Conversation/_Close"), NULL, menu_close_conv_cb, 0,
3137 "<StockItem>", GTK_STOCK_CLOSE },
3139 /* Options */
3140 { N_("/_Options"), NULL, NULL, 0, "<Branch>", NULL },
3141 { N_("/Options/Enable _Logging"), NULL, menu_logging_cb, 0, "<CheckItem>", NULL },
3142 { N_("/Options/Enable _Sounds"), NULL, menu_sounds_cb, 0, "<CheckItem>", NULL },
3143 { "/Options/sep0", NULL, NULL, 0, "<Separator>", NULL },
3144 { N_("/Options/Show Formatting _Toolbars"), NULL, menu_toolbar_cb, 0, "<CheckItem>", NULL },
3145 { N_("/Options/Show Ti_mestamps"), NULL, menu_timestamps_cb, 0, "<CheckItem>", NULL },
3148 static const int menu_item_count =
3149 sizeof(menu_items) / sizeof(*menu_items);
3151 static const char *
3152 item_factory_translate_func (const char *path, gpointer func_data)
3154 return _(path);
3157 static void
3158 sound_method_pref_changed_cb(const char *name, PurplePrefType type,
3159 gconstpointer value, gpointer data)
3161 PidginWindow *win = data;
3162 const char *method = value;
3164 if (!strcmp(method, "none"))
3166 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(win->menu.sounds),
3167 FALSE);
3168 gtk_widget_set_sensitive(win->menu.sounds, FALSE);
3170 else
3172 PidginConversation *gtkconv = pidgin_conv_window_get_active_gtkconv(win);
3174 if (gtkconv != NULL)
3175 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(win->menu.sounds),
3176 gtkconv->make_sound);
3177 gtk_widget_set_sensitive(win->menu.sounds, TRUE);
3182 /* Returns TRUE if some items were added to the menu, FALSE otherwise */
3183 static gboolean
3184 populate_menu_with_options(GtkWidget *menu, PidginConversation *gtkconv, gboolean all)
3186 GList *list;
3187 PurpleConversation *conv;
3188 PurpleBlistNode *node = NULL;
3189 PurpleChat *chat = NULL;
3190 PurpleBuddy *buddy = NULL;
3191 gboolean ret;
3193 conv = gtkconv->active_conv;
3195 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) {
3196 chat = purple_blist_find_chat(conv->account, conv->name);
3198 if ((chat == NULL) && (gtkconv->imhtml != NULL)) {
3199 chat = g_object_get_data(G_OBJECT(gtkconv->imhtml), "transient_chat");
3202 if ((chat == NULL) && (gtkconv->imhtml != NULL)) {
3203 GHashTable *components;
3204 PurpleAccount *account = purple_conversation_get_account(conv);
3205 PurplePlugin *prpl = purple_find_prpl(purple_account_get_protocol_id(account));
3206 PurplePluginProtocolInfo *prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(prpl);
3207 if (purple_account_get_connection(account) != NULL &&
3208 PURPLE_PROTOCOL_PLUGIN_HAS_FUNC(prpl_info, chat_info_defaults)) {
3209 components = prpl_info->chat_info_defaults(purple_account_get_connection(account),
3210 purple_conversation_get_name(conv));
3211 } else {
3212 components = g_hash_table_new_full(g_str_hash, g_str_equal,
3213 g_free, g_free);
3214 g_hash_table_replace(components, g_strdup("channel"),
3215 g_strdup(purple_conversation_get_name(conv)));
3217 chat = purple_chat_new(conv->account, NULL, components);
3218 purple_blist_node_set_flags((PurpleBlistNode *)chat,
3219 PURPLE_BLIST_NODE_FLAG_NO_SAVE);
3220 g_object_set_data_full(G_OBJECT(gtkconv->imhtml), "transient_chat",
3221 chat, (GDestroyNotify)purple_blist_remove_chat);
3223 } else {
3224 if (!purple_account_is_connected(conv->account))
3225 return FALSE;
3227 buddy = purple_find_buddy(conv->account, conv->name);
3229 /* gotta remain bug-compatible :( libpurple < 2.0.2 didn't handle
3230 * removing "isolated" buddy nodes well */
3231 if (purple_version_check(2, 0, 2) == NULL) {
3232 if ((buddy == NULL) && (gtkconv->imhtml != NULL)) {
3233 buddy = g_object_get_data(G_OBJECT(gtkconv->imhtml), "transient_buddy");
3236 if ((buddy == NULL) && (gtkconv->imhtml != NULL)) {
3237 buddy = purple_buddy_new(conv->account, conv->name, NULL);
3238 purple_blist_node_set_flags((PurpleBlistNode *)buddy,
3239 PURPLE_BLIST_NODE_FLAG_NO_SAVE);
3240 g_object_set_data_full(G_OBJECT(gtkconv->imhtml), "transient_buddy",
3241 buddy, (GDestroyNotify)purple_buddy_destroy);
3246 if (chat)
3247 node = (PurpleBlistNode *)chat;
3248 else if (buddy)
3249 node = (PurpleBlistNode *)buddy;
3251 /* Now add the stuff */
3252 if (all) {
3253 if (buddy)
3254 pidgin_blist_make_buddy_menu(menu, buddy, TRUE);
3255 else if (chat) {
3256 /* XXX: */
3258 } else if (node) {
3259 if (purple_account_is_connected(conv->account))
3260 pidgin_append_blist_node_proto_menu(menu, conv->account->gc, node);
3261 pidgin_append_blist_node_extended_menu(menu, node);
3264 if ((list = gtk_container_get_children(GTK_CONTAINER(menu))) == NULL) {
3265 ret = FALSE;
3266 } else {
3267 g_list_free(list);
3268 ret = TRUE;
3270 return ret;
3273 static void
3274 regenerate_media_items(PidginWindow *win)
3276 #ifdef USE_VV
3277 PurpleAccount *account;
3278 PurpleConversation *conv;
3280 conv = pidgin_conv_window_get_active_conversation(win);
3282 if (conv == NULL) {
3283 purple_debug_error("gtkconv", "couldn't get active conversation"
3284 " when regenerating media items\n");
3285 return;
3288 account = purple_conversation_get_account(conv);
3290 if (account == NULL) {
3291 purple_debug_error("gtkconv", "couldn't get account when"
3292 " regenerating media items\n");
3293 return;
3297 * Check if account support voice and/or calls, and
3298 * if the current buddy supports it.
3300 if (account != NULL && purple_conversation_get_type(conv)
3301 == PURPLE_CONV_TYPE_IM) {
3302 PurpleMediaCaps caps =
3303 purple_prpl_get_media_caps(account,
3304 purple_conversation_get_name(conv));
3306 gtk_widget_set_sensitive(win->audio_call,
3307 caps & PURPLE_MEDIA_CAPS_AUDIO
3308 ? TRUE : FALSE);
3309 gtk_widget_set_sensitive(win->video_call,
3310 caps & PURPLE_MEDIA_CAPS_VIDEO
3311 ? TRUE : FALSE);
3312 gtk_widget_set_sensitive(win->audio_video_call,
3313 caps & PURPLE_MEDIA_CAPS_AUDIO_VIDEO
3314 ? TRUE : FALSE);
3315 } else if (purple_conversation_get_type(conv)
3316 == PURPLE_CONV_TYPE_CHAT) {
3317 /* for now, don't care about chats... */
3318 gtk_widget_set_sensitive(win->audio_call, FALSE);
3319 gtk_widget_set_sensitive(win->video_call, FALSE);
3320 gtk_widget_set_sensitive(win->audio_video_call, FALSE);
3321 } else {
3322 gtk_widget_set_sensitive(win->audio_call, FALSE);
3323 gtk_widget_set_sensitive(win->video_call, FALSE);
3324 gtk_widget_set_sensitive(win->audio_video_call, FALSE);
3326 #endif
3329 static void
3330 regenerate_options_items(PidginWindow *win)
3332 GtkWidget *menu;
3333 PidginConversation *gtkconv;
3334 GList *list;
3336 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
3337 menu = gtk_item_factory_get_widget(win->menu.item_factory, N_("/Conversation/More"));
3339 /* Remove the previous entries */
3340 for (list = gtk_container_get_children(GTK_CONTAINER(menu)); list; )
3342 GtkWidget *w = list->data;
3343 list = g_list_delete_link(list, list);
3344 gtk_widget_destroy(w);
3347 if (!populate_menu_with_options(menu, gtkconv, FALSE))
3349 GtkWidget *item = gtk_menu_item_new_with_label(_("No actions available"));
3350 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
3351 gtk_widget_set_sensitive(item, FALSE);
3354 gtk_widget_show_all(menu);
3357 static void
3358 remove_from_list(GtkWidget *widget, PidginWindow *win)
3360 GList *list = g_object_get_data(G_OBJECT(win->window), "plugin-actions");
3361 list = g_list_remove(list, widget);
3362 g_object_set_data(G_OBJECT(win->window), "plugin-actions", list);
3365 static void
3366 regenerate_plugins_items(PidginWindow *win)
3368 GList *action_items;
3369 GtkWidget *menu;
3370 GList *list;
3371 PidginConversation *gtkconv;
3372 PurpleConversation *conv;
3373 GtkWidget *item;
3375 if (win->window == NULL || win == hidden_convwin)
3376 return;
3378 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
3379 if (gtkconv == NULL)
3380 return;
3382 conv = gtkconv->active_conv;
3383 action_items = g_object_get_data(G_OBJECT(win->window), "plugin-actions");
3385 /* Remove the old menuitems */
3386 while (action_items) {
3387 g_signal_handlers_disconnect_by_func(G_OBJECT(action_items->data),
3388 G_CALLBACK(remove_from_list), win);
3389 gtk_widget_destroy(action_items->data);
3390 action_items = g_list_delete_link(action_items, action_items);
3393 menu = gtk_item_factory_get_widget(win->menu.item_factory, N_("/Options"));
3395 list = purple_conversation_get_extended_menu(conv);
3396 if (list) {
3397 action_items = g_list_prepend(NULL, (item = pidgin_separator(menu)));
3398 g_signal_connect(G_OBJECT(item), "destroy", G_CALLBACK(remove_from_list), win);
3401 for(; list; list = g_list_delete_link(list, list)) {
3402 PurpleMenuAction *act = (PurpleMenuAction *) list->data;
3403 item = pidgin_append_menu_action(menu, act, conv);
3404 action_items = g_list_prepend(action_items, item);
3405 gtk_widget_show_all(item);
3406 g_signal_connect(G_OBJECT(item), "destroy", G_CALLBACK(remove_from_list), win);
3408 g_object_set_data(G_OBJECT(win->window), "plugin-actions", action_items);
3411 static void menubar_activated(GtkWidget *item, gpointer data)
3413 PidginWindow *win = data;
3414 regenerate_media_items(win);
3415 regenerate_options_items(win);
3416 regenerate_plugins_items(win);
3418 /* The following are to make sure the 'More' submenu is not regenerated every time
3419 * the focus shifts from 'Conversations' to some other menu and back. */
3420 g_signal_handlers_block_by_func(G_OBJECT(item), G_CALLBACK(menubar_activated), data);
3421 g_signal_connect(G_OBJECT(win->menu.menubar), "deactivate", G_CALLBACK(focus_out_from_menubar), data);
3424 static void
3425 focus_out_from_menubar(GtkWidget *wid, PidginWindow *win)
3427 /* The menubar has been deactivated. Make sure the 'More' submenu is regenerated next time
3428 * the 'Conversation' menu pops up. */
3429 GtkWidget *menuitem = gtk_item_factory_get_item(win->menu.item_factory, N_("/Conversation"));
3430 g_signal_handlers_unblock_by_func(G_OBJECT(menuitem), G_CALLBACK(menubar_activated), win);
3431 g_signal_handlers_disconnect_by_func(G_OBJECT(win->menu.menubar),
3432 G_CALLBACK(focus_out_from_menubar), win);
3435 static GtkWidget *
3436 setup_menubar(PidginWindow *win)
3438 GtkAccelGroup *accel_group;
3439 const char *method;
3440 GtkWidget *menuitem;
3442 accel_group = gtk_accel_group_new ();
3443 gtk_window_add_accel_group(GTK_WINDOW(win->window), accel_group);
3444 g_object_unref(accel_group);
3446 win->menu.item_factory =
3447 gtk_item_factory_new(GTK_TYPE_MENU_BAR, "<main>", accel_group);
3449 gtk_item_factory_set_translate_func(win->menu.item_factory,
3450 (GtkTranslateFunc)item_factory_translate_func,
3451 NULL, NULL);
3453 gtk_item_factory_create_items(win->menu.item_factory, menu_item_count,
3454 menu_items, win);
3455 g_signal_connect(G_OBJECT(accel_group), "accel-changed",
3456 G_CALLBACK(pidgin_save_accels_cb), NULL);
3458 /* Make sure the 'Conversation -> More' menuitems are regenerated whenever
3459 * the 'Conversation' menu pops up because the entries can change after the
3460 * conversation is created. */
3461 menuitem = gtk_item_factory_get_item(win->menu.item_factory, N_("/Conversation"));
3462 g_signal_connect(G_OBJECT(menuitem), "activate", G_CALLBACK(menubar_activated), win);
3464 win->menu.menubar =
3465 gtk_item_factory_get_widget(win->menu.item_factory, "<main>");
3467 win->menu.view_log =
3468 gtk_item_factory_get_widget(win->menu.item_factory,
3469 N_("/Conversation/View Log"));
3471 #ifdef USE_VV
3472 win->audio_call =
3473 gtk_item_factory_get_widget(win->menu.item_factory,
3474 N_("/Conversation/Media/Audio Call"));
3475 win->video_call =
3476 gtk_item_factory_get_widget(win->menu.item_factory,
3477 N_("/Conversation/Media/Video Call"));
3478 win->audio_video_call =
3479 gtk_item_factory_get_widget(win->menu.item_factory,
3480 N_("/Conversation/Media/Audio\\/Video Call"));
3481 #endif
3483 /* --- */
3485 win->menu.send_file =
3486 gtk_item_factory_get_widget(win->menu.item_factory,
3487 N_("/Conversation/Send File..."));
3489 g_object_set_data(G_OBJECT(win->window), "get_attention",
3490 gtk_item_factory_get_widget(win->menu.item_factory,
3491 N_("/Conversation/Get Attention")));
3492 win->menu.add_pounce =
3493 gtk_item_factory_get_widget(win->menu.item_factory,
3494 N_("/Conversation/Add Buddy Pounce..."));
3496 /* --- */
3498 win->menu.get_info =
3499 gtk_item_factory_get_widget(win->menu.item_factory,
3500 N_("/Conversation/Get Info"));
3502 win->menu.invite =
3503 gtk_item_factory_get_widget(win->menu.item_factory,
3504 N_("/Conversation/Invite..."));
3506 /* --- */
3508 win->menu.alias =
3509 gtk_item_factory_get_widget(win->menu.item_factory,
3510 N_("/Conversation/Alias..."));
3512 win->menu.block =
3513 gtk_item_factory_get_widget(win->menu.item_factory,
3514 N_("/Conversation/Block..."));
3516 win->menu.unblock =
3517 gtk_item_factory_get_widget(win->menu.item_factory,
3518 N_("/Conversation/Unblock..."));
3520 win->menu.add =
3521 gtk_item_factory_get_widget(win->menu.item_factory,
3522 N_("/Conversation/Add..."));
3524 win->menu.remove =
3525 gtk_item_factory_get_widget(win->menu.item_factory,
3526 N_("/Conversation/Remove..."));
3528 /* --- */
3530 win->menu.insert_link =
3531 gtk_item_factory_get_widget(win->menu.item_factory,
3532 N_("/Conversation/Insert Link..."));
3534 win->menu.insert_image =
3535 gtk_item_factory_get_widget(win->menu.item_factory,
3536 N_("/Conversation/Insert Image..."));
3538 /* --- */
3540 win->menu.logging =
3541 gtk_item_factory_get_widget(win->menu.item_factory,
3542 N_("/Options/Enable Logging"));
3543 win->menu.sounds =
3544 gtk_item_factory_get_widget(win->menu.item_factory,
3545 N_("/Options/Enable Sounds"));
3546 method = purple_prefs_get_string(PIDGIN_PREFS_ROOT "/sound/method");
3547 if (method != NULL && !strcmp(method, "none"))
3549 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(win->menu.sounds),
3550 FALSE);
3551 gtk_widget_set_sensitive(win->menu.sounds, FALSE);
3553 purple_prefs_connect_callback(win, PIDGIN_PREFS_ROOT "/sound/method",
3554 sound_method_pref_changed_cb, win);
3556 win->menu.show_formatting_toolbar =
3557 gtk_item_factory_get_widget(win->menu.item_factory,
3558 N_("/Options/Show Formatting Toolbars"));
3559 win->menu.show_timestamps =
3560 gtk_item_factory_get_widget(win->menu.item_factory,
3561 N_("/Options/Show Timestamps"));
3562 win->menu.show_icon = NULL;
3564 win->menu.tray = pidgin_menu_tray_new();
3565 gtk_menu_shell_append(GTK_MENU_SHELL(win->menu.menubar),
3566 win->menu.tray);
3567 gtk_widget_show(win->menu.tray);
3569 gtk_widget_show(win->menu.menubar);
3571 return win->menu.menubar;
3575 /**************************************************************************
3576 * Utility functions
3577 **************************************************************************/
3579 static void
3580 got_typing_keypress(PidginConversation *gtkconv, gboolean first)
3582 PurpleConversation *conv = gtkconv->active_conv;
3583 PurpleConvIm *im;
3586 * We know we got something, so we at least have to make sure we don't
3587 * send PURPLE_TYPED any time soon.
3590 im = PURPLE_CONV_IM(conv);
3592 purple_conv_im_stop_send_typed_timeout(im);
3593 purple_conv_im_start_send_typed_timeout(im);
3595 /* Check if we need to send another PURPLE_TYPING message */
3596 if (first || (purple_conv_im_get_type_again(im) != 0 &&
3597 time(NULL) > purple_conv_im_get_type_again(im)))
3599 unsigned int timeout;
3600 timeout = serv_send_typing(purple_conversation_get_gc(conv),
3601 purple_conversation_get_name(conv),
3602 PURPLE_TYPING);
3603 purple_conv_im_set_type_again(im, timeout);
3607 #if 0
3608 static gboolean
3609 typing_animation(gpointer data) {
3610 PidginConversation *gtkconv = data;
3611 PidginWindow *gtkwin = gtkconv->win;
3612 const char *stock_id = NULL;
3614 if(gtkconv != pidgin_conv_window_get_active_gtkconv(gtkwin)) {
3615 return FALSE;
3618 switch (rand() % 5) {
3619 case 0:
3620 stock_id = PIDGIN_STOCK_ANIMATION_TYPING0;
3621 break;
3622 case 1:
3623 stock_id = PIDGIN_STOCK_ANIMATION_TYPING1;
3624 break;
3625 case 2:
3626 stock_id = PIDGIN_STOCK_ANIMATION_TYPING2;
3627 break;
3628 case 3:
3629 stock_id = PIDGIN_STOCK_ANIMATION_TYPING3;
3630 break;
3631 case 4:
3632 stock_id = PIDGIN_STOCK_ANIMATION_TYPING4;
3633 break;
3635 if (gtkwin->menu.typing_icon == NULL) {
3636 gtkwin->menu.typing_icon = gtk_image_new_from_stock(stock_id, GTK_ICON_SIZE_MENU);
3637 pidgin_menu_tray_append(PIDGIN_MENU_TRAY(gtkwin->menu.tray),
3638 gtkwin->menu.typing_icon,
3639 _("User is typing..."));
3640 } else {
3641 gtk_image_set_from_stock(GTK_IMAGE(gtkwin->menu.typing_icon), stock_id, GTK_ICON_SIZE_MENU);
3643 gtk_widget_show(gtkwin->menu.typing_icon);
3644 return TRUE;
3646 #endif
3648 static void
3649 update_typing_message(PidginConversation *gtkconv, const char *message)
3651 GtkTextBuffer *buffer;
3652 GtkTextMark *stmark, *enmark;
3654 if (g_object_get_data(G_OBJECT(gtkconv->imhtml), "disable-typing-notification"))
3655 return;
3657 buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(gtkconv->imhtml));
3658 stmark = gtk_text_buffer_get_mark(buffer, "typing-notification-start");
3659 enmark = gtk_text_buffer_get_mark(buffer, "typing-notification-end");
3660 if (stmark && enmark) {
3661 GtkTextIter start, end;
3662 gtk_text_buffer_get_iter_at_mark(buffer, &start, stmark);
3663 gtk_text_buffer_get_iter_at_mark(buffer, &end, enmark);
3664 gtk_text_buffer_delete_mark(buffer, stmark);
3665 gtk_text_buffer_delete_mark(buffer, enmark);
3666 gtk_text_buffer_delete(buffer, &start, &end);
3667 } else if (message && *message == '\n' && message[1] == ' ' && message[2] == '\0')
3668 message = NULL;
3670 #ifdef RESERVE_LINE
3671 if (!message)
3672 message = "\n "; /* The blank space is required to avoid a GTK+/Pango bug */
3673 #endif
3675 if (message) {
3676 GtkTextIter iter;
3677 gtk_text_buffer_get_end_iter(buffer, &iter);
3678 gtk_text_buffer_create_mark(buffer, "typing-notification-start", &iter, TRUE);
3679 gtk_text_buffer_insert_with_tags_by_name(buffer, &iter, message, -1, "TYPING-NOTIFICATION", NULL);
3680 gtk_text_buffer_get_end_iter(buffer, &iter);
3681 gtk_text_buffer_create_mark(buffer, "typing-notification-end", &iter, TRUE);
3685 static void
3686 update_typing_icon(PidginConversation *gtkconv)
3688 PurpleConvIm *im = NULL;
3689 PurpleConversation *conv = gtkconv->active_conv;
3690 char *message = NULL;
3692 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM)
3693 im = PURPLE_CONV_IM(conv);
3695 if (im == NULL)
3696 return;
3698 if (purple_conv_im_get_typing_state(im) == PURPLE_NOT_TYPING) {
3699 #ifdef RESERVE_LINE
3700 update_typing_message(gtkconv, NULL);
3701 #else
3702 update_typing_message(gtkconv, "\n ");
3703 #endif
3704 return;
3707 if (purple_conv_im_get_typing_state(im) == PURPLE_TYPING) {
3708 message = g_strdup_printf(_("\n%s is typing..."), purple_conversation_get_title(conv));
3709 } else {
3710 message = g_strdup_printf(_("\n%s has stopped typing"), purple_conversation_get_title(conv));
3713 update_typing_message(gtkconv, message);
3714 g_free(message);
3717 static gboolean
3718 update_send_to_selection(PidginWindow *win)
3720 PurpleAccount *account;
3721 PurpleConversation *conv;
3722 GtkWidget *menu;
3723 GList *child;
3724 PurpleBuddy *b;
3726 conv = pidgin_conv_window_get_active_conversation(win);
3728 if (conv == NULL)
3729 return FALSE;
3731 account = purple_conversation_get_account(conv);
3733 if (account == NULL)
3734 return FALSE;
3736 if (win->menu.send_to == NULL)
3737 return FALSE;
3739 if (!(b = purple_find_buddy(account, conv->name)))
3740 return FALSE;
3743 gtk_widget_show(win->menu.send_to);
3745 menu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(win->menu.send_to));
3747 for (child = gtk_container_get_children(GTK_CONTAINER(menu));
3748 child != NULL;
3749 child = g_list_delete_link(child, child)) {
3751 GtkWidget *item = child->data;
3752 PurpleBuddy *item_buddy;
3753 PurpleAccount *item_account = g_object_get_data(G_OBJECT(item), "purple_account");
3754 gchar *buddy_name = g_object_get_data(G_OBJECT(item),
3755 "purple_buddy_name");
3756 item_buddy = purple_find_buddy(item_account, buddy_name);
3758 if (b == item_buddy) {
3759 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item), TRUE);
3760 g_list_free(child);
3761 break;
3765 return FALSE;
3768 static gboolean
3769 send_to_item_enter_notify_cb(GtkWidget *menuitem, GdkEventCrossing *event, GtkWidget *label)
3771 gtk_widget_set_sensitive(GTK_WIDGET(label), TRUE);
3772 return FALSE;
3775 static gboolean
3776 send_to_item_leave_notify_cb(GtkWidget *menuitem, GdkEventCrossing *event, GtkWidget *label)
3778 gtk_widget_set_sensitive(GTK_WIDGET(label), FALSE);
3779 return FALSE;
3782 static void
3783 create_sendto_item(GtkWidget *menu, GtkSizeGroup *sg, GSList **group, PurpleBuddy *buddy, PurpleAccount *account, const char *name)
3785 GtkWidget *box;
3786 GtkWidget *label;
3787 GtkWidget *image;
3788 GtkWidget *menuitem;
3789 GdkPixbuf *pixbuf;
3790 gchar *text;
3792 /* Create a pixmap for the protocol icon. */
3793 pixbuf = pidgin_create_prpl_icon(account, PIDGIN_PRPL_ICON_SMALL);
3795 /* Now convert it to GtkImage */
3796 if (pixbuf == NULL)
3797 image = gtk_image_new();
3798 else
3800 image = gtk_image_new_from_pixbuf(pixbuf);
3801 g_object_unref(G_OBJECT(pixbuf));
3804 gtk_size_group_add_widget(sg, image);
3806 /* Make our menu item */
3807 text = g_strdup_printf("%s (%s)", name, purple_account_get_name_for_display(account));
3808 menuitem = gtk_radio_menu_item_new_with_label(*group, text);
3809 g_free(text);
3810 *group = gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(menuitem));
3812 /* Do some evil, see some evil, speak some evil. */
3813 box = gtk_hbox_new(FALSE, 0);
3815 label = gtk_bin_get_child(GTK_BIN(menuitem));
3816 g_object_ref(label);
3817 gtk_container_remove(GTK_CONTAINER(menuitem), label);
3819 gtk_box_pack_start(GTK_BOX(box), image, FALSE, FALSE, 0);
3820 gtk_box_pack_start(GTK_BOX(box), label, TRUE, TRUE, 4);
3822 if (buddy != NULL &&
3823 !purple_presence_is_online(purple_buddy_get_presence(buddy)))
3825 gtk_widget_set_sensitive(label, FALSE);
3827 /* Set the label sensitive when the menuitem is highlighted and
3828 * insensitive again when the mouse leaves it. This way, it
3829 * doesn't appear weird from the highlighting of the embossed
3830 * (insensitive style) text.*/
3831 g_signal_connect(menuitem, "enter-notify-event",
3832 G_CALLBACK(send_to_item_enter_notify_cb), label);
3833 g_signal_connect(menuitem, "leave-notify-event",
3834 G_CALLBACK(send_to_item_leave_notify_cb), label);
3837 g_object_unref(label);
3839 gtk_container_add(GTK_CONTAINER(menuitem), box);
3841 gtk_widget_show(label);
3842 gtk_widget_show(image);
3843 gtk_widget_show(box);
3845 /* Set our data and callbacks. */
3846 g_object_set_data(G_OBJECT(menuitem), "purple_account", account);
3847 g_object_set_data_full(G_OBJECT(menuitem), "purple_buddy_name", g_strdup(name), g_free);
3849 g_signal_connect(G_OBJECT(menuitem), "activate",
3850 G_CALLBACK(menu_conv_sel_send_cb), NULL);
3852 gtk_widget_show(menuitem);
3853 gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
3856 static gboolean
3857 compare_buddy_presence(PurplePresence *p1, PurplePresence *p2)
3859 /* This is necessary because multiple PurpleBuddy's don't share the same
3860 * PurplePresence anymore.
3862 PurpleBuddy *b1 = purple_presence_get_buddy(p1);
3863 PurpleBuddy *b2 = purple_presence_get_buddy(p2);
3864 if (purple_buddy_get_account(b1) == purple_buddy_get_account(b2) &&
3865 strcmp(purple_buddy_get_name(b1), purple_buddy_get_name(b2)) == 0)
3866 return FALSE;
3867 return TRUE;
3870 static void
3871 generate_send_to_items(PidginWindow *win)
3873 GtkWidget *menu;
3874 GSList *group = NULL;
3875 GtkSizeGroup *sg = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL);
3876 PidginConversation *gtkconv;
3877 GSList *l, *buds;
3879 g_return_if_fail(win != NULL);
3881 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
3883 g_return_if_fail(gtkconv != NULL);
3885 if (win->menu.send_to != NULL)
3886 gtk_widget_destroy(win->menu.send_to);
3888 /* Build the Send To menu */
3889 win->menu.send_to = gtk_menu_item_new_with_mnemonic(_("S_end To"));
3890 gtk_widget_show(win->menu.send_to);
3892 menu = gtk_menu_new();
3893 gtk_menu_shell_insert(GTK_MENU_SHELL(win->menu.menubar),
3894 win->menu.send_to, 2);
3895 gtk_menu_item_set_submenu(GTK_MENU_ITEM(win->menu.send_to), menu);
3897 gtk_widget_show(menu);
3899 if (gtkconv->active_conv->type == PURPLE_CONV_TYPE_IM) {
3900 buds = purple_find_buddies(gtkconv->active_conv->account, gtkconv->active_conv->name);
3902 if (buds == NULL)
3904 /* The user isn't on the buddy list. So we don't create any sendto menu. */
3906 else
3908 GList *list = NULL, *iter;
3909 for (l = buds; l != NULL; l = l->next)
3911 PurpleBlistNode *node;
3913 node = PURPLE_BLIST_NODE(purple_buddy_get_contact(PURPLE_BUDDY(l->data)));
3915 for (node = node->child; node != NULL; node = node->next)
3917 PurpleBuddy *buddy = (PurpleBuddy *)node;
3918 PurpleAccount *account;
3920 if (!PURPLE_BLIST_NODE_IS_BUDDY(node))
3921 continue;
3923 account = purple_buddy_get_account(buddy);
3924 if (purple_account_is_connected(account) || account == gtkconv->active_conv->account)
3926 /* Use the PurplePresence to get unique buddies. */
3927 PurplePresence *presence = purple_buddy_get_presence(buddy);
3928 if (!g_list_find_custom(list, presence, (GCompareFunc)compare_buddy_presence))
3929 list = g_list_prepend(list, presence);
3934 /* Create the sendto menu only if it has more than one item to show */
3935 if (list && list->next) {
3936 /* Loop over the list backwards so we get the items in the right order,
3937 * since we did a g_list_prepend() earlier. */
3938 for (iter = g_list_last(list); iter != NULL; iter = iter->prev) {
3939 PurplePresence *pre = iter->data;
3940 PurpleBuddy *buddy = purple_presence_get_buddy(pre);
3941 create_sendto_item(menu, sg, &group, buddy,
3942 purple_buddy_get_account(buddy), purple_buddy_get_name(buddy));
3945 g_list_free(list);
3946 g_slist_free(buds);
3950 g_object_unref(sg);
3952 gtk_widget_show(win->menu.send_to);
3953 /* TODO: This should never be insensitive. Possibly hidden or not. */
3954 if (!group)
3955 gtk_widget_set_sensitive(win->menu.send_to, FALSE);
3956 update_send_to_selection(win);
3959 static const char *
3960 get_chat_buddy_status_icon(PurpleConvChat *chat, const char *name, PurpleConvChatBuddyFlags flags)
3962 const char *image = NULL;
3964 if (flags & PURPLE_CBFLAGS_FOUNDER) {
3965 image = PIDGIN_STOCK_STATUS_FOUNDER;
3966 } else if (flags & PURPLE_CBFLAGS_OP) {
3967 image = PIDGIN_STOCK_STATUS_OPERATOR;
3968 } else if (flags & PURPLE_CBFLAGS_HALFOP) {
3969 image = PIDGIN_STOCK_STATUS_HALFOP;
3970 } else if (flags & PURPLE_CBFLAGS_VOICE) {
3971 image = PIDGIN_STOCK_STATUS_VOICE;
3972 } else if ((!flags) && purple_conv_chat_is_user_ignored(chat, name)) {
3973 image = PIDGIN_STOCK_STATUS_IGNORED;
3974 } else {
3975 return NULL;
3977 return image;
3980 static void
3981 deleting_chat_buddy_cb(PurpleConvChatBuddy *cb)
3983 if (cb->ui_data) {
3984 GtkTreeRowReference *ref = cb->ui_data;
3985 gtk_tree_row_reference_free(ref);
3986 cb->ui_data = NULL;
3990 static void
3991 add_chat_buddy_common(PurpleConversation *conv, PurpleConvChatBuddy *cb, const char *old_name)
3993 PidginConversation *gtkconv;
3994 PidginChatPane *gtkchat;
3995 PurpleConvChat *chat;
3996 PurpleConnection *gc;
3997 PurplePluginProtocolInfo *prpl_info;
3998 GtkTreeModel *tm;
3999 GtkListStore *ls;
4000 GtkTreePath *newpath;
4001 const char *stock;
4002 GtkTreeIter iter;
4003 gboolean is_me = FALSE;
4004 gboolean is_buddy;
4005 gchar *tmp, *alias_key, *name, *alias;
4006 PurpleConvChatBuddyFlags flags;
4007 GdkColor *color = NULL;
4009 alias = cb->alias;
4010 name = cb->name;
4011 flags = cb->flags;
4013 chat = PURPLE_CONV_CHAT(conv);
4014 gtkconv = PIDGIN_CONVERSATION(conv);
4015 gtkchat = gtkconv->u.chat;
4016 gc = purple_conversation_get_gc(conv);
4018 if (!gc || !(prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(gc->prpl)))
4019 return;
4021 tm = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
4022 ls = GTK_LIST_STORE(tm);
4024 stock = get_chat_buddy_status_icon(chat, name, flags);
4026 if (!strcmp(chat->nick, purple_normalize(conv->account, old_name != NULL ? old_name : name)))
4027 is_me = TRUE;
4029 is_buddy = cb->buddy;
4031 tmp = g_utf8_casefold(alias, -1);
4032 alias_key = g_utf8_collate_key(tmp, -1);
4033 g_free(tmp);
4035 if (is_me) {
4036 GtkTextTag *tag = gtk_text_tag_table_lookup(
4037 gtk_text_buffer_get_tag_table(GTK_IMHTML(gtkconv->imhtml)->text_buffer),
4038 "send-name");
4039 g_object_get(tag, "foreground-gdk", &color, NULL);
4040 } else {
4041 GtkTextTag *tag;
4042 if ((tag = get_buddy_tag(conv, name, 0, FALSE)))
4043 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_NORMAL, NULL);
4044 if ((tag = get_buddy_tag(conv, name, PURPLE_MESSAGE_NICK, FALSE)))
4045 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_NORMAL, NULL);
4046 color = (GdkColor*)get_nick_color(gtkconv, name);
4049 gtk_list_store_insert_with_values(ls, &iter,
4051 * The GTK docs are mute about the effects of the "row" value for performance.
4052 * X-Chat hardcodes their value to 0 (prepend) and -1 (append), so we will too.
4053 * It *might* be faster to search the gtk_list_store and set row accurately,
4054 * but no one in #gtk+ seems to know anything about it either.
4055 * Inserting in the "wrong" location has no visible ill effects. - F.P.
4057 -1, /* "row" */
4058 CHAT_USERS_ICON_STOCK_COLUMN, stock,
4059 CHAT_USERS_ALIAS_COLUMN, alias,
4060 CHAT_USERS_ALIAS_KEY_COLUMN, alias_key,
4061 CHAT_USERS_NAME_COLUMN, name,
4062 CHAT_USERS_FLAGS_COLUMN, flags,
4063 CHAT_USERS_COLOR_COLUMN, color,
4064 CHAT_USERS_WEIGHT_COLUMN, is_buddy ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL,
4065 -1);
4067 if (cb->ui_data) {
4068 GtkTreeRowReference *ref = cb->ui_data;
4069 gtk_tree_row_reference_free(ref);
4072 newpath = gtk_tree_model_get_path(tm, &iter);
4073 cb->ui_data = gtk_tree_row_reference_new(tm, newpath);
4074 gtk_tree_path_free(newpath);
4076 if (is_me && color)
4077 gdk_color_free(color);
4078 g_free(alias_key);
4082 * @param most_matched Used internally by this function.
4083 * @param entered The partial string that the user types before hitting the
4084 * tab key.
4085 * @param entered_bytes The length of entered.
4086 * @param partial This is a return variable. This will be set to a string
4087 * containing the largest common string between all matches. This will
4088 * be inserted into the input box at the start of the word that the
4089 * user is tab completing. For example, if a chat room contains
4090 * "AlfFan" and "AlfHater" and the user types "a<TAB>" then this will
4091 * contain "Alf"
4092 * @param nick_partial Used internally by this function. Shoudl be a
4093 * temporary buffer that is entered_bytes+1 bytes long.
4094 * @param matches This is a return variable. If the given name is a potential
4095 * match for the entered string, then add a copy of the name to this
4096 * list. The caller is responsible for g_free'ing the data in this
4097 * list.
4098 * @param name The buddy name or alias or slash command name that we're
4099 * checking for a match.
4101 static void
4102 tab_complete_process_item(int *most_matched, const char *entered, gsize entered_bytes, char **partial, char *nick_partial,
4103 GList **matches, char *name)
4105 memcpy(nick_partial, name, entered_bytes);
4106 if (purple_utf8_strcasecmp(nick_partial, entered))
4107 return;
4109 /* if we're here, it's a possible completion */
4111 if (*most_matched == -1) {
4113 * this will only get called once, since from now
4114 * on *most_matched is >= 0
4116 *most_matched = strlen(name);
4117 *partial = g_strdup(name);
4119 else if (*most_matched) {
4120 char *tmp = g_strdup(name);
4122 while (purple_utf8_strcasecmp(tmp, *partial)) {
4123 (*partial)[*most_matched] = '\0';
4124 if (*most_matched < strlen(tmp))
4125 tmp[*most_matched] = '\0';
4126 (*most_matched)--;
4128 (*most_matched)++;
4130 g_free(tmp);
4133 *matches = g_list_insert_sorted(*matches, g_strdup(name),
4134 (GCompareFunc)purple_utf8_strcasecmp);
4137 static gboolean
4138 tab_complete(PurpleConversation *conv)
4140 PidginConversation *gtkconv;
4141 GtkTextIter cursor, word_start, start_buffer;
4142 int start;
4143 int most_matched = -1;
4144 char *entered, *partial = NULL;
4145 char *text;
4146 char *nick_partial;
4147 const char *prefix;
4148 GList *matches = NULL;
4149 gboolean command = FALSE;
4150 gsize entered_bytes = 0;
4152 gtkconv = PIDGIN_CONVERSATION(conv);
4154 gtk_text_buffer_get_start_iter(gtkconv->entry_buffer, &start_buffer);
4155 gtk_text_buffer_get_iter_at_mark(gtkconv->entry_buffer, &cursor,
4156 gtk_text_buffer_get_insert(gtkconv->entry_buffer));
4158 word_start = cursor;
4160 /* if there's nothing there just return */
4161 if (!gtk_text_iter_compare(&cursor, &start_buffer))
4162 return (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) ? TRUE : FALSE;
4164 text = gtk_text_buffer_get_text(gtkconv->entry_buffer, &start_buffer,
4165 &cursor, FALSE);
4167 /* if we're at the end of ": " we need to move back 2 spaces */
4168 start = strlen(text) - 1;
4170 if (start >= 1 && !strncmp(&text[start-1], ": ", 2)) {
4171 gtk_text_iter_backward_chars(&word_start, 2);
4174 /* find the start of the word that we're tabbing.
4175 * Using gtk_text_iter_backward_word_start won't work, because a nick can contain
4176 * characters (e.g. '.', '/' etc.) that Pango may think are word separators. */
4177 while (gtk_text_iter_backward_char(&word_start)) {
4178 if (gtk_text_iter_get_char(&word_start) == ' ') {
4179 /* Reached the whitespace before the start of the word. Move forward once */
4180 gtk_text_iter_forward_char(&word_start);
4181 break;
4185 prefix = pidgin_get_cmd_prefix();
4186 if (gtk_text_iter_get_offset(&word_start) == 0 &&
4187 (strlen(text) >= strlen(prefix)) && !strncmp(text, prefix, strlen(prefix))) {
4188 command = TRUE;
4189 gtk_text_iter_forward_chars(&word_start, strlen(prefix));
4192 g_free(text);
4194 entered = gtk_text_buffer_get_text(gtkconv->entry_buffer, &word_start,
4195 &cursor, FALSE);
4196 entered_bytes = strlen(entered);
4198 if (!g_utf8_strlen(entered, -1)) {
4199 g_free(entered);
4200 return (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) ? TRUE : FALSE;
4203 nick_partial = g_malloc0(entered_bytes + 1);
4205 if (command) {
4206 GList *list = purple_cmd_list(conv);
4207 GList *l;
4209 /* Commands */
4210 for (l = list; l != NULL; l = l->next) {
4211 tab_complete_process_item(&most_matched, entered, entered_bytes, &partial, nick_partial,
4212 &matches, l->data);
4214 g_list_free(list);
4215 } else if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) {
4216 PurpleConvChat *chat = PURPLE_CONV_CHAT(conv);
4217 GList *l = purple_conv_chat_get_users(chat);
4218 GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(PIDGIN_CONVERSATION(conv)->u.chat->list));
4219 GtkTreeIter iter;
4220 int f;
4222 /* Users */
4223 for (; l != NULL; l = l->next) {
4224 tab_complete_process_item(&most_matched, entered, entered_bytes, &partial, nick_partial,
4225 &matches, ((PurpleConvChatBuddy *)l->data)->name);
4229 /* Aliases */
4230 if (gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter))
4232 do {
4233 char *name;
4234 char *alias;
4236 gtk_tree_model_get(model, &iter,
4237 CHAT_USERS_NAME_COLUMN, &name,
4238 CHAT_USERS_ALIAS_COLUMN, &alias,
4239 -1);
4241 if (name && alias && strcmp(name, alias))
4242 tab_complete_process_item(&most_matched, entered, entered_bytes, &partial, nick_partial,
4243 &matches, alias);
4244 g_free(name);
4245 g_free(alias);
4247 f = gtk_tree_model_iter_next(model, &iter);
4248 } while (f != 0);
4250 } else {
4251 g_free(nick_partial);
4252 g_free(entered);
4253 return FALSE;
4256 g_free(nick_partial);
4258 /* we're only here if we're doing new style */
4260 /* if there weren't any matches, return */
4261 if (!matches) {
4262 /* if matches isn't set partials won't be either */
4263 g_free(entered);
4264 return (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) ? TRUE : FALSE;
4267 gtk_text_buffer_delete(gtkconv->entry_buffer, &word_start, &cursor);
4269 if (!matches->next) {
4270 /* there was only one match. fill it in. */
4271 gtk_text_buffer_get_start_iter(gtkconv->entry_buffer, &start_buffer);
4272 gtk_text_buffer_get_iter_at_mark(gtkconv->entry_buffer, &cursor,
4273 gtk_text_buffer_get_insert(gtkconv->entry_buffer));
4275 if (!gtk_text_iter_compare(&cursor, &start_buffer)) {
4276 char *tmp = g_strdup_printf("%s: ", (char *)matches->data);
4277 gtk_text_buffer_insert_at_cursor(gtkconv->entry_buffer, tmp, -1);
4278 g_free(tmp);
4280 else
4281 gtk_text_buffer_insert_at_cursor(gtkconv->entry_buffer,
4282 matches->data, -1);
4284 g_free(matches->data);
4285 matches = g_list_remove(matches, matches->data);
4287 else {
4289 * there were lots of matches, fill in as much as possible
4290 * and display all of them
4292 char *addthis = g_malloc0(1);
4294 while (matches) {
4295 char *tmp = addthis;
4296 addthis = g_strconcat(tmp, matches->data, " ", NULL);
4297 g_free(tmp);
4298 g_free(matches->data);
4299 matches = g_list_remove(matches, matches->data);
4302 purple_conversation_write(conv, NULL, addthis, PURPLE_MESSAGE_NO_LOG,
4303 time(NULL));
4304 gtk_text_buffer_insert_at_cursor(gtkconv->entry_buffer, partial, -1);
4305 g_free(addthis);
4308 g_free(entered);
4309 g_free(partial);
4311 return TRUE;
4314 static void topic_callback(GtkWidget *w, PidginConversation *gtkconv)
4316 PurplePluginProtocolInfo *prpl_info = NULL;
4317 PurpleConnection *gc;
4318 PurpleConversation *conv = gtkconv->active_conv;
4319 PidginChatPane *gtkchat;
4320 char *new_topic;
4321 const char *current_topic;
4323 gc = purple_conversation_get_gc(conv);
4325 if(!gc || !(prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(gc->prpl)))
4326 return;
4328 if(prpl_info->set_chat_topic == NULL)
4329 return;
4331 gtkconv = PIDGIN_CONVERSATION(conv);
4332 gtkchat = gtkconv->u.chat;
4333 new_topic = g_strdup(gtk_entry_get_text(GTK_ENTRY(gtkchat->topic_text)));
4334 current_topic = purple_conv_chat_get_topic(PURPLE_CONV_CHAT(conv));
4336 if(current_topic && !g_utf8_collate(new_topic, current_topic)){
4337 g_free(new_topic);
4338 return;
4341 if (current_topic)
4342 gtk_entry_set_text(GTK_ENTRY(gtkchat->topic_text), current_topic);
4343 else
4344 gtk_entry_set_text(GTK_ENTRY(gtkchat->topic_text), "");
4346 prpl_info->set_chat_topic(gc, purple_conv_chat_get_id(PURPLE_CONV_CHAT(conv)),
4347 new_topic);
4349 g_free(new_topic);
4352 static gint
4353 sort_chat_users(GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, gpointer userdata)
4355 PurpleConvChatBuddyFlags f1 = 0, f2 = 0;
4356 char *user1 = NULL, *user2 = NULL;
4357 gboolean buddy1 = FALSE, buddy2 = FALSE;
4358 gint ret = 0;
4360 gtk_tree_model_get(model, a,
4361 CHAT_USERS_ALIAS_KEY_COLUMN, &user1,
4362 CHAT_USERS_FLAGS_COLUMN, &f1,
4363 CHAT_USERS_WEIGHT_COLUMN, &buddy1,
4364 -1);
4365 gtk_tree_model_get(model, b,
4366 CHAT_USERS_ALIAS_KEY_COLUMN, &user2,
4367 CHAT_USERS_FLAGS_COLUMN, &f2,
4368 CHAT_USERS_WEIGHT_COLUMN, &buddy2,
4369 -1);
4371 /* Only sort by membership levels */
4372 f1 &= PURPLE_CBFLAGS_VOICE | PURPLE_CBFLAGS_HALFOP | PURPLE_CBFLAGS_OP |
4373 PURPLE_CBFLAGS_FOUNDER;
4374 f2 &= PURPLE_CBFLAGS_VOICE | PURPLE_CBFLAGS_HALFOP | PURPLE_CBFLAGS_OP |
4375 PURPLE_CBFLAGS_FOUNDER;
4377 if (user1 == NULL || user2 == NULL) {
4378 if (!(user1 == NULL && user2 == NULL))
4379 ret = (user1 == NULL) ? -1: 1;
4380 } else if (f1 != f2) {
4381 /* sort more important users first */
4382 ret = (f1 > f2) ? -1 : 1;
4383 } else if (buddy1 != buddy2) {
4384 ret = (buddy1 > buddy2) ? -1 : 1;
4385 } else {
4386 ret = strcmp(user1, user2);
4389 g_free(user1);
4390 g_free(user2);
4392 return ret;
4395 static void
4396 update_chat_alias(PurpleBuddy *buddy, PurpleConversation *conv, PurpleConnection *gc, PurplePluginProtocolInfo *prpl_info)
4398 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
4399 PurpleConvChat *chat = PURPLE_CONV_CHAT(conv);
4400 GtkTreeModel *model;
4401 char *normalized_name;
4402 GtkTreeIter iter;
4403 int f;
4405 g_return_if_fail(buddy != NULL);
4406 g_return_if_fail(conv != NULL);
4408 /* This is safe because this callback is only used in chats, not IMs. */
4409 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkconv->u.chat->list));
4411 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter))
4412 return;
4414 normalized_name = g_strdup(purple_normalize(conv->account, buddy->name));
4416 do {
4417 char *name;
4419 gtk_tree_model_get(model, &iter, CHAT_USERS_NAME_COLUMN, &name, -1);
4421 if (!strcmp(normalized_name, purple_normalize(conv->account, name))) {
4422 const char *alias = name;
4423 char *tmp;
4424 char *alias_key = NULL;
4425 PurpleBuddy *buddy2;
4427 if (strcmp(chat->nick, purple_normalize(conv->account, name))) {
4428 /* This user is not me, so look into updating the alias. */
4430 if ((buddy2 = purple_find_buddy(conv->account, name)) != NULL) {
4431 alias = purple_buddy_get_contact_alias(buddy2);
4434 tmp = g_utf8_casefold(alias, -1);
4435 alias_key = g_utf8_collate_key(tmp, -1);
4436 g_free(tmp);
4438 gtk_list_store_set(GTK_LIST_STORE(model), &iter,
4439 CHAT_USERS_ALIAS_COLUMN, alias,
4440 CHAT_USERS_ALIAS_KEY_COLUMN, alias_key,
4441 -1);
4442 g_free(alias_key);
4444 g_free(name);
4445 break;
4448 f = gtk_tree_model_iter_next(model, &iter);
4450 g_free(name);
4451 } while (f != 0);
4453 g_free(normalized_name);
4456 static void
4457 blist_node_aliased_cb(PurpleBlistNode *node, const char *old_alias, PurpleConversation *conv)
4459 PurpleConnection *gc;
4460 PurplePluginProtocolInfo *prpl_info;
4462 g_return_if_fail(node != NULL);
4463 g_return_if_fail(conv != NULL);
4465 gc = purple_conversation_get_gc(conv);
4466 g_return_if_fail(gc != NULL);
4467 g_return_if_fail(gc->prpl != NULL);
4468 prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(gc->prpl);
4470 if (prpl_info->options & OPT_PROTO_UNIQUE_CHATNAME)
4471 return;
4473 if (PURPLE_BLIST_NODE_IS_CONTACT(node))
4475 PurpleBlistNode *bnode;
4477 for(bnode = node->child; bnode; bnode = bnode->next) {
4479 if(!PURPLE_BLIST_NODE_IS_BUDDY(bnode))
4480 continue;
4482 update_chat_alias((PurpleBuddy *)bnode, conv, gc, prpl_info);
4485 else if (PURPLE_BLIST_NODE_IS_BUDDY(node))
4486 update_chat_alias((PurpleBuddy *)node, conv, gc, prpl_info);
4487 else if (PURPLE_BLIST_NODE_IS_CHAT(node) &&
4488 purple_conversation_get_account(conv) == ((PurpleChat*)node)->account)
4490 if (old_alias == NULL || g_utf8_collate(old_alias, purple_conversation_get_title(conv)) == 0)
4491 pidgin_conv_update_fields(conv, PIDGIN_CONV_SET_TITLE);
4495 static void
4496 buddy_cb_common(PurpleBuddy *buddy, PurpleConversation *conv, gboolean is_buddy)
4498 GtkTreeModel *model;
4499 char *normalized_name;
4500 GtkTreeIter iter;
4501 GtkTextTag *texttag;
4502 int f;
4504 g_return_if_fail(buddy != NULL);
4505 g_return_if_fail(conv != NULL);
4507 /* Do nothing if the buddy does not belong to the conv's account */
4508 if (purple_buddy_get_account(buddy) != purple_conversation_get_account(conv))
4509 return;
4511 /* This is safe because this callback is only used in chats, not IMs. */
4512 model = gtk_tree_view_get_model(GTK_TREE_VIEW(PIDGIN_CONVERSATION(conv)->u.chat->list));
4514 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter))
4515 return;
4517 normalized_name = g_strdup(purple_normalize(conv->account, buddy->name));
4519 do {
4520 char *name;
4522 gtk_tree_model_get(model, &iter, CHAT_USERS_NAME_COLUMN, &name, -1);
4524 if (!strcmp(normalized_name, purple_normalize(conv->account, name))) {
4525 gtk_list_store_set(GTK_LIST_STORE(model), &iter,
4526 CHAT_USERS_WEIGHT_COLUMN, is_buddy ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL, -1);
4527 g_free(name);
4528 break;
4531 f = gtk_tree_model_iter_next(model, &iter);
4533 g_free(name);
4534 } while (f != 0);
4536 g_free(normalized_name);
4538 blist_node_aliased_cb((PurpleBlistNode *)buddy, NULL, conv);
4540 texttag = get_buddy_tag(conv, purple_buddy_get_name(buddy), 0, FALSE); /* XXX: do we want the normalized name? */
4541 if (texttag) {
4542 g_object_set(texttag, "weight", is_buddy ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL, NULL);
4546 static void
4547 buddy_added_cb(PurpleBlistNode *node, PurpleConversation *conv)
4549 if (!PURPLE_BLIST_NODE_IS_BUDDY(node))
4550 return;
4552 buddy_cb_common(PURPLE_BUDDY(node), conv, TRUE);
4555 static void
4556 buddy_removed_cb(PurpleBlistNode *node, PurpleConversation *conv)
4558 if (!PURPLE_BLIST_NODE_IS_BUDDY(node))
4559 return;
4561 /* If there's another buddy for the same "dude" on the list, do nothing. */
4562 if (purple_find_buddy(purple_buddy_get_account(PURPLE_BUDDY(node)),
4563 purple_buddy_get_name(PURPLE_BUDDY(node))) != NULL)
4564 return;
4566 buddy_cb_common(PURPLE_BUDDY(node), conv, FALSE);
4569 static void send_menu_cb(GtkWidget *widget, PidginConversation *gtkconv)
4571 g_signal_emit_by_name(gtkconv->entry, "message_send");
4574 static void
4575 entry_popup_menu_cb(GtkIMHtml *imhtml, GtkMenu *menu, gpointer data)
4577 GtkWidget *menuitem;
4578 PidginConversation *gtkconv = data;
4580 g_return_if_fail(menu != NULL);
4581 g_return_if_fail(gtkconv != NULL);
4583 menuitem = pidgin_new_item_from_stock(NULL, _("_Send"), NULL,
4584 G_CALLBACK(send_menu_cb), gtkconv,
4585 0, 0, NULL);
4586 if (gtk_text_buffer_get_char_count(imhtml->text_buffer) == 0)
4587 gtk_widget_set_sensitive(menuitem, FALSE);
4588 gtk_menu_shell_insert(GTK_MENU_SHELL(menu), menuitem, 0);
4590 menuitem = gtk_separator_menu_item_new();
4591 gtk_widget_show(menuitem);
4592 gtk_menu_shell_insert(GTK_MENU_SHELL(menu), menuitem, 1);
4595 static gboolean resize_imhtml_cb(PidginConversation *gtkconv)
4597 GtkTextBuffer *buffer;
4598 GtkTextIter iter;
4599 int lines;
4600 GdkRectangle oneline;
4601 int height, diff;
4602 int pad_top, pad_inside, pad_bottom;
4603 int total_height = (gtkconv->imhtml->allocation.height + gtkconv->entry->allocation.height);
4604 int max_height = total_height / 2;
4605 int min_lines = purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/minimum_entry_lines");
4606 int min_height;
4607 gboolean interior_focus;
4608 int focus_width;
4610 pad_top = gtk_text_view_get_pixels_above_lines(GTK_TEXT_VIEW(gtkconv->entry));
4611 pad_bottom = gtk_text_view_get_pixels_below_lines(GTK_TEXT_VIEW(gtkconv->entry));
4612 pad_inside = gtk_text_view_get_pixels_inside_wrap(GTK_TEXT_VIEW(gtkconv->entry));
4614 buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(gtkconv->entry));
4615 gtk_text_buffer_get_start_iter(buffer, &iter);
4616 gtk_text_view_get_iter_location(GTK_TEXT_VIEW(gtkconv->entry), &iter, &oneline);
4618 lines = gtk_text_buffer_get_line_count(buffer);
4620 height = 0;
4621 do {
4622 int lineheight = 0;
4623 gtk_text_view_get_line_yrange(GTK_TEXT_VIEW(gtkconv->entry), &iter, NULL, &lineheight);
4624 height += lineheight;
4625 lines--;
4626 } while (gtk_text_iter_forward_line(&iter));
4627 height += lines * (oneline.height + pad_top + pad_bottom);
4629 /* Make sure there's enough room for at least min_lines. Allocate enough space to
4630 * prevent scrolling when the second line is a continuation of the first line, or
4631 * is the beginning of a new paragraph. */
4632 min_height = min_lines * (oneline.height + MAX(pad_inside, pad_top + pad_bottom));
4633 height = CLAMP(height, MIN(min_height, max_height), max_height);
4635 gtk_widget_style_get(gtkconv->entry,
4636 "interior-focus", &interior_focus,
4637 "focus-line-width", &focus_width,
4638 NULL);
4639 if (!interior_focus)
4640 height += 2 * focus_width;
4642 diff = height - gtkconv->entry->allocation.height;
4643 if (ABS(diff) < oneline.height / 2)
4644 return FALSE;
4646 gtk_widget_set_size_request(gtkconv->lower_hbox, -1,
4647 diff + gtkconv->lower_hbox->allocation.height);
4649 return FALSE;
4652 static void
4653 minimum_entry_lines_pref_cb(const char *name,
4654 PurplePrefType type,
4655 gconstpointer value,
4656 gpointer data)
4658 GList *l = purple_get_conversations();
4659 PurpleConversation *conv;
4660 while (l != NULL)
4662 conv = (PurpleConversation *)l->data;
4664 if (PIDGIN_IS_PIDGIN_CONVERSATION(conv))
4665 resize_imhtml_cb(PIDGIN_CONVERSATION(conv));
4667 l = l->next;
4671 static void
4672 setup_chat_topic(PidginConversation *gtkconv, GtkWidget *vbox)
4674 PurpleConversation *conv = gtkconv->active_conv;
4675 PurpleConnection *gc = purple_conversation_get_gc(conv);
4676 PurplePluginProtocolInfo *prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(gc->prpl);
4677 if (prpl_info->options & OPT_PROTO_CHAT_TOPIC)
4679 GtkWidget *hbox, *label;
4680 PidginChatPane *gtkchat = gtkconv->u.chat;
4682 hbox = gtk_hbox_new(FALSE, PIDGIN_HIG_BOX_SPACE);
4683 gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);
4685 label = gtk_label_new(_("Topic:"));
4686 gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0);
4688 gtkchat->topic_text = gtk_entry_new();
4689 gtk_widget_set_size_request(gtkchat->topic_text, -1, BUDDYICON_SIZE_MIN);
4691 if(prpl_info->set_chat_topic == NULL) {
4692 gtk_editable_set_editable(GTK_EDITABLE(gtkchat->topic_text), FALSE);
4693 } else {
4694 g_signal_connect(GTK_OBJECT(gtkchat->topic_text), "activate",
4695 G_CALLBACK(topic_callback), gtkconv);
4698 gtk_box_pack_start(GTK_BOX(hbox), gtkchat->topic_text, TRUE, TRUE, 0);
4699 g_signal_connect(G_OBJECT(gtkchat->topic_text), "key_press_event",
4700 G_CALLBACK(entry_key_press_cb), gtkconv);
4704 static gboolean
4705 pidgin_conv_userlist_create_tooltip(GtkWidget *tipwindow, GtkTreePath *path,
4706 gpointer userdata, int *w, int *h)
4708 PidginConversation *gtkconv = userdata;
4709 GtkTreeIter iter;
4710 GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkconv->u.chat->list));
4711 PurpleConversation *conv = gtkconv->active_conv;
4712 PurpleBlistNode *node;
4713 PurplePluginProtocolInfo *prpl_info;
4714 PurpleAccount *account = purple_conversation_get_account(conv);
4715 char *who = NULL;
4717 if (account->gc == NULL)
4718 return FALSE;
4720 if (!gtk_tree_model_get_iter(GTK_TREE_MODEL(model), &iter, path))
4721 return FALSE;
4723 gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, CHAT_USERS_NAME_COLUMN, &who, -1);
4725 prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(account->gc->prpl);
4726 node = (PurpleBlistNode*)(purple_find_buddy(conv->account, who));
4727 if (node && prpl_info && (prpl_info->options & OPT_PROTO_UNIQUE_CHATNAME))
4728 pidgin_blist_draw_tooltip(node, gtkconv->infopane);
4730 g_free(who);
4731 return FALSE;
4734 static void
4735 setup_chat_userlist(PidginConversation *gtkconv, GtkWidget *hpaned)
4737 PidginChatPane *gtkchat = gtkconv->u.chat;
4738 GtkWidget *lbox, *list;
4739 GtkListStore *ls;
4740 GtkCellRenderer *rend;
4741 GtkTreeViewColumn *col;
4742 int ul_width;
4743 void *blist_handle = purple_blist_get_handle();
4744 PurpleConversation *conv = gtkconv->active_conv;
4746 /* Build the right pane. */
4747 lbox = gtk_vbox_new(FALSE, PIDGIN_HIG_BOX_SPACE);
4748 gtk_paned_pack2(GTK_PANED(hpaned), lbox, FALSE, TRUE);
4749 gtk_widget_show(lbox);
4751 /* Setup the label telling how many people are in the room. */
4752 gtkchat->count = gtk_label_new(_("0 people in room"));
4753 gtk_label_set_ellipsize(GTK_LABEL(gtkchat->count), PANGO_ELLIPSIZE_END);
4754 gtk_box_pack_start(GTK_BOX(lbox), gtkchat->count, FALSE, FALSE, 0);
4755 gtk_widget_show(gtkchat->count);
4757 /* Setup the list of users. */
4759 ls = gtk_list_store_new(CHAT_USERS_COLUMNS, GDK_TYPE_PIXBUF, G_TYPE_STRING,
4760 G_TYPE_STRING, G_TYPE_STRING, G_TYPE_INT,
4761 GDK_TYPE_COLOR, G_TYPE_INT, G_TYPE_STRING);
4762 gtk_tree_sortable_set_sort_func(GTK_TREE_SORTABLE(ls), CHAT_USERS_ALIAS_KEY_COLUMN,
4763 sort_chat_users, NULL, NULL);
4765 list = gtk_tree_view_new_with_model(GTK_TREE_MODEL(ls));
4767 /* Allow a user to specify gtkrc settings for the chat userlist only */
4768 gtk_widget_set_name(list, "pidgin_conv_userlist");
4770 rend = gtk_cell_renderer_pixbuf_new();
4771 g_object_set(G_OBJECT(rend),
4772 "stock-size", gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL),
4773 NULL);
4774 col = gtk_tree_view_column_new_with_attributes(NULL, rend,
4775 "stock-id", CHAT_USERS_ICON_STOCK_COLUMN, NULL);
4776 gtk_tree_view_column_set_sizing(col, GTK_TREE_VIEW_COLUMN_AUTOSIZE);
4777 gtk_tree_view_append_column(GTK_TREE_VIEW(list), col);
4778 ul_width = purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/userlist_width");
4779 gtk_widget_set_size_request(lbox, ul_width, -1);
4781 /* Hack to prevent completely collapsed userlist coming back with a 1 pixel width.
4782 * I would have liked to use the GtkPaned "max-position", but for some reason that didn't work */
4783 if (ul_width == 0)
4784 gtk_paned_set_position(GTK_PANED(hpaned), 999999);
4786 g_signal_connect(G_OBJECT(list), "button_press_event",
4787 G_CALLBACK(right_click_chat_cb), gtkconv);
4788 g_signal_connect(G_OBJECT(list), "row-activated",
4789 G_CALLBACK(activate_list_cb), gtkconv);
4790 g_signal_connect(G_OBJECT(list), "popup-menu",
4791 G_CALLBACK(gtkconv_chat_popup_menu_cb), gtkconv);
4792 g_signal_connect(G_OBJECT(lbox), "size-allocate", G_CALLBACK(lbox_size_allocate_cb), gtkconv);
4794 pidgin_tooltip_setup_for_treeview(list, gtkconv,
4795 pidgin_conv_userlist_create_tooltip, NULL);
4797 rend = gtk_cell_renderer_text_new();
4798 g_object_set(rend,
4799 "foreground-set", TRUE,
4800 "weight-set", TRUE,
4801 NULL);
4802 g_object_set(G_OBJECT(rend), "editable", TRUE, NULL);
4804 col = gtk_tree_view_column_new_with_attributes(NULL, rend,
4805 "text", CHAT_USERS_ALIAS_COLUMN,
4806 "foreground-gdk", CHAT_USERS_COLOR_COLUMN,
4807 "weight", CHAT_USERS_WEIGHT_COLUMN,
4808 NULL);
4810 purple_signal_connect(blist_handle, "blist-node-added",
4811 gtkchat, PURPLE_CALLBACK(buddy_added_cb), conv);
4812 purple_signal_connect(blist_handle, "blist-node-removed",
4813 gtkchat, PURPLE_CALLBACK(buddy_removed_cb), conv);
4814 purple_signal_connect(blist_handle, "blist-node-aliased",
4815 gtkchat, PURPLE_CALLBACK(blist_node_aliased_cb), conv);
4817 gtk_tree_view_column_set_expand(col, TRUE);
4818 g_object_set(rend, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
4820 gtk_tree_view_append_column(GTK_TREE_VIEW(list), col);
4822 gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(list), FALSE);
4823 gtk_widget_show(list);
4825 gtkchat->list = list;
4827 gtk_box_pack_start(GTK_BOX(lbox),
4828 pidgin_make_scrollable(list, GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC, GTK_SHADOW_IN, -1, -1),
4829 TRUE, TRUE, 0);
4832 static gboolean
4833 pidgin_conv_create_tooltip(GtkWidget *tipwindow, gpointer userdata, int *w, int *h)
4835 PurpleBlistNode *node = NULL;
4836 PurpleConversation *conv;
4837 PidginConversation *gtkconv = userdata;
4839 conv = gtkconv->active_conv;
4840 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) {
4841 node = (PurpleBlistNode*)(purple_blist_find_chat(conv->account, conv->name));
4842 if (!node)
4843 node = g_object_get_data(G_OBJECT(gtkconv->imhtml), "transient_chat");
4844 } else {
4845 node = (PurpleBlistNode*)(purple_find_buddy(conv->account, conv->name));
4846 #if 0
4847 /* Using the transient blist nodes to show the tooltip doesn't quite work yet. */
4848 if (!node)
4849 node = g_object_get_data(G_OBJECT(gtkconv->imhtml), "transient_buddy");
4850 #endif
4853 if (node)
4854 pidgin_blist_draw_tooltip(node, gtkconv->infopane);
4855 return FALSE;
4858 /* Quick Find {{{ */
4859 static gboolean
4860 pidgin_conv_end_quickfind(PidginConversation *gtkconv)
4862 gtk_widget_modify_base(gtkconv->quickfind.entry, GTK_STATE_NORMAL, NULL);
4864 gtk_imhtml_search_clear(GTK_IMHTML(gtkconv->imhtml));
4865 gtk_widget_hide_all(gtkconv->quickfind.container);
4867 gtk_widget_grab_focus(gtkconv->entry);
4868 return TRUE;
4871 static gboolean
4872 quickfind_process_input(GtkWidget *entry, GdkEventKey *event, PidginConversation *gtkconv)
4874 switch (event->keyval) {
4875 case GDK_Return:
4876 case GDK_KP_Enter:
4877 if (gtk_imhtml_search_find(GTK_IMHTML(gtkconv->imhtml), gtk_entry_get_text(GTK_ENTRY(entry)))) {
4878 gtk_widget_modify_base(gtkconv->quickfind.entry, GTK_STATE_NORMAL, NULL);
4879 } else {
4880 GdkColor col;
4881 col.red = 0xffff;
4882 col.green = 0xafff;
4883 col.blue = 0xafff;
4884 gtk_widget_modify_base(gtkconv->quickfind.entry, GTK_STATE_NORMAL, &col);
4886 break;
4887 case GDK_Escape:
4888 pidgin_conv_end_quickfind(gtkconv);
4889 break;
4890 default:
4891 return FALSE;
4893 return TRUE;
4896 static void
4897 pidgin_conv_setup_quickfind(PidginConversation *gtkconv, GtkWidget *container)
4899 GtkWidget *widget = gtk_hbox_new(FALSE, 0);
4900 GtkWidget *label, *entry, *close;
4902 gtk_box_pack_start(GTK_BOX(container), widget, FALSE, FALSE, 0);
4904 close = pidgin_create_small_button(gtk_label_new("×"));
4905 gtk_box_pack_start(GTK_BOX(widget), close, FALSE, FALSE, 0);
4906 gtk_tooltips_set_tip(gtkconv->tooltips, close,
4907 _("Close Find bar"), NULL);
4909 label = gtk_label_new(_("Find:"));
4910 gtk_box_pack_start(GTK_BOX(widget), label, FALSE, FALSE, 10);
4912 entry = gtk_entry_new();
4913 gtk_box_pack_start(GTK_BOX(widget), entry, TRUE, TRUE, 0);
4915 gtkconv->quickfind.entry = entry;
4916 gtkconv->quickfind.container = widget;
4918 /* Hook to signals and stuff */
4919 g_signal_connect(G_OBJECT(entry), "key_press_event",
4920 G_CALLBACK(quickfind_process_input), gtkconv);
4921 g_signal_connect_swapped(G_OBJECT(close), "button-press-event",
4922 G_CALLBACK(pidgin_conv_end_quickfind), gtkconv);
4925 /* }}} */
4927 static GtkWidget *
4928 setup_common_pane(PidginConversation *gtkconv)
4930 GtkWidget *vbox, *frame, *imhtml_sw, *event_box;
4931 GtkCellRenderer *rend;
4932 GtkTreePath *path;
4933 PurpleConversation *conv = gtkconv->active_conv;
4934 PurpleBuddy *buddy;
4935 gboolean chat = (conv->type == PURPLE_CONV_TYPE_CHAT);
4936 int buddyicon_size = 0;
4938 /* Setup the top part of the pane */
4939 vbox = gtk_vbox_new(FALSE, PIDGIN_HIG_BOX_SPACE);
4940 gtk_widget_show(vbox);
4942 /* Setup the info pane */
4943 event_box = gtk_event_box_new();
4944 gtk_event_box_set_visible_window(GTK_EVENT_BOX(event_box), FALSE);
4945 gtk_widget_show(event_box);
4946 gtkconv->infopane_hbox = gtk_hbox_new(FALSE, 0);
4947 gtk_box_pack_start(GTK_BOX(vbox), event_box, FALSE, FALSE, 0);
4948 gtk_container_add(GTK_CONTAINER(event_box), gtkconv->infopane_hbox);
4949 gtk_widget_show(gtkconv->infopane_hbox);
4950 gtk_widget_add_events(event_box,
4951 GDK_POINTER_MOTION_MASK | GDK_LEAVE_NOTIFY_MASK);
4952 g_signal_connect(G_OBJECT(event_box), "button-press-event",
4953 G_CALLBACK(infopane_press_cb), gtkconv);
4955 pidgin_tooltip_setup_for_widget(event_box, gtkconv,
4956 pidgin_conv_create_tooltip, NULL);
4958 gtkconv->infopane = gtk_cell_view_new();
4959 gtkconv->infopane_model = gtk_list_store_new(CONV_NUM_COLUMNS, G_TYPE_STRING, G_TYPE_STRING, GDK_TYPE_PIXBUF, GDK_TYPE_PIXBUF);
4960 gtk_cell_view_set_model(GTK_CELL_VIEW(gtkconv->infopane),
4961 GTK_TREE_MODEL(gtkconv->infopane_model));
4962 g_object_unref(gtkconv->infopane_model);
4963 gtk_list_store_append(gtkconv->infopane_model, &(gtkconv->infopane_iter));
4964 gtk_box_pack_start(GTK_BOX(gtkconv->infopane_hbox), gtkconv->infopane, TRUE, TRUE, 0);
4965 path = gtk_tree_path_new_from_string("0");
4966 gtk_cell_view_set_displayed_row(GTK_CELL_VIEW(gtkconv->infopane), path);
4967 gtk_tree_path_free(path);
4969 if (chat) {
4970 /* This empty widget is used to ensure that the infopane is consistently
4971 sized for chat windows. The correct fix is to put an icon in the chat
4972 window as well, because that would make "Set Custom Icon" consistent
4973 for both the buddy list and the chat window, but PidginConversation
4974 is pretty much stuck until 3.0. */
4975 GtkWidget *sizing_vbox;
4976 sizing_vbox = gtk_vbox_new(FALSE, 0);
4977 gtk_widget_set_size_request(sizing_vbox, -1, BUDDYICON_SIZE_MIN);
4978 gtk_box_pack_start(GTK_BOX(gtkconv->infopane_hbox), sizing_vbox, FALSE, FALSE, 0);
4979 gtk_widget_show(sizing_vbox);
4981 else {
4982 gtkconv->u.im->icon_container = gtk_vbox_new(FALSE, 0);
4984 if ((buddy = purple_find_buddy(purple_conversation_get_account(conv),
4985 purple_conversation_get_name(conv))) != NULL) {
4986 PurpleContact *contact = purple_buddy_get_contact(buddy);
4987 if (contact) {
4988 buddyicon_size = purple_blist_node_get_int((PurpleBlistNode*)contact, "pidgin-infopane-iconsize");
4991 buddyicon_size = CLAMP(buddyicon_size, BUDDYICON_SIZE_MIN, BUDDYICON_SIZE_MAX);
4992 gtk_widget_set_size_request(gtkconv->u.im->icon_container, -1, buddyicon_size);
4994 gtk_box_pack_start(GTK_BOX(gtkconv->infopane_hbox),
4995 gtkconv->u.im->icon_container, FALSE, FALSE, 0);
4997 gtk_widget_show(gtkconv->u.im->icon_container);
5000 gtk_widget_show(gtkconv->infopane);
5002 rend = gtk_cell_renderer_pixbuf_new();
5003 gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(gtkconv->infopane), rend, FALSE);
5004 gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(gtkconv->infopane), rend, "stock-id", CONV_ICON_COLUMN, NULL);
5005 g_object_set(rend, "xalign", 0.0, "xpad", 6, "ypad", 0,
5006 "stock-size", gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL),
5007 NULL);
5009 rend = gtk_cell_renderer_text_new();
5010 gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(gtkconv->infopane), rend, TRUE);
5011 gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(gtkconv->infopane), rend, "markup", CONV_TEXT_COLUMN, NULL);
5012 g_object_set(rend, "ypad", 0, "yalign", 0.5, NULL);
5014 g_object_set(rend, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
5016 rend = gtk_cell_renderer_pixbuf_new();
5017 gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(gtkconv->infopane), rend, FALSE);
5018 gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(gtkconv->infopane), rend, "pixbuf", CONV_PROTOCOL_ICON_COLUMN, NULL);
5019 g_object_set(rend, "xalign", 0.0, "xpad", 3, "ypad", 0, NULL);
5021 rend = gtk_cell_renderer_pixbuf_new();
5022 gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(gtkconv->infopane), rend, FALSE);
5023 gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(gtkconv->infopane), rend, "pixbuf", CONV_EMBLEM_COLUMN, NULL);
5024 g_object_set(rend, "xalign", 0.0, "xpad", 6, "ypad", 0, NULL);
5026 /* Setup the gtkimhtml widget */
5027 frame = pidgin_create_imhtml(FALSE, &gtkconv->imhtml, NULL, &imhtml_sw);
5028 gtk_widget_set_size_request(gtkconv->imhtml, -1, 0);
5029 if (chat) {
5030 GtkWidget *hpaned;
5032 /* Add the topic */
5033 setup_chat_topic(gtkconv, vbox);
5035 /* Add the gtkimhtml frame */
5036 hpaned = gtk_hpaned_new();
5037 gtk_box_pack_start(GTK_BOX(vbox), hpaned, TRUE, TRUE, 0);
5038 gtk_widget_show(hpaned);
5039 gtk_paned_pack1(GTK_PANED(hpaned), frame, TRUE, TRUE);
5041 /* Now add the userlist */
5042 setup_chat_userlist(gtkconv, hpaned);
5043 } else {
5044 gtk_box_pack_start(GTK_BOX(vbox), frame, TRUE, TRUE, 0);
5046 gtk_widget_show(frame);
5048 gtk_widget_set_name(gtkconv->imhtml, "pidgin_conv_imhtml");
5049 gtk_imhtml_show_comments(GTK_IMHTML(gtkconv->imhtml),TRUE);
5050 g_object_set_data(G_OBJECT(gtkconv->imhtml), "gtkconv", gtkconv);
5052 g_object_set(G_OBJECT(imhtml_sw), "vscrollbar-policy", GTK_POLICY_ALWAYS, NULL);
5054 g_signal_connect_after(G_OBJECT(gtkconv->imhtml), "button_press_event",
5055 G_CALLBACK(entry_stop_rclick_cb), NULL);
5056 g_signal_connect(G_OBJECT(gtkconv->imhtml), "key_press_event",
5057 G_CALLBACK(refocus_entry_cb), gtkconv);
5058 g_signal_connect(G_OBJECT(gtkconv->imhtml), "key_release_event",
5059 G_CALLBACK(refocus_entry_cb), gtkconv);
5061 pidgin_conv_setup_quickfind(gtkconv, vbox);
5063 gtkconv->lower_hbox = gtk_hbox_new(FALSE, PIDGIN_HIG_BOX_SPACE);
5064 gtk_box_pack_start(GTK_BOX(vbox), gtkconv->lower_hbox, FALSE, FALSE, 0);
5065 gtk_widget_show(gtkconv->lower_hbox);
5067 /* Setup the toolbar, entry widget and all signals */
5068 frame = pidgin_create_imhtml(TRUE, &gtkconv->entry, &gtkconv->toolbar, NULL);
5069 gtk_box_pack_start(GTK_BOX(gtkconv->lower_hbox), frame, TRUE, TRUE, 0);
5070 gtk_widget_show(frame);
5072 gtk_widget_set_name(gtkconv->entry, "pidgin_conv_entry");
5073 gtk_imhtml_set_protocol_name(GTK_IMHTML(gtkconv->entry),
5074 purple_account_get_protocol_name(conv->account));
5076 g_signal_connect(G_OBJECT(gtkconv->entry), "populate-popup",
5077 G_CALLBACK(entry_popup_menu_cb), gtkconv);
5078 g_signal_connect(G_OBJECT(gtkconv->entry), "key_press_event",
5079 G_CALLBACK(entry_key_press_cb), gtkconv);
5080 g_signal_connect_after(G_OBJECT(gtkconv->entry), "message_send",
5081 G_CALLBACK(send_cb), gtkconv);
5082 g_signal_connect_after(G_OBJECT(gtkconv->entry), "button_press_event",
5083 G_CALLBACK(entry_stop_rclick_cb), NULL);
5085 gtkconv->entry_buffer =
5086 gtk_text_view_get_buffer(GTK_TEXT_VIEW(gtkconv->entry));
5087 g_object_set_data(G_OBJECT(gtkconv->entry_buffer), "user_data", gtkconv);
5089 if (!chat) {
5090 /* For sending typing notifications for IMs */
5091 g_signal_connect(G_OBJECT(gtkconv->entry_buffer), "insert_text",
5092 G_CALLBACK(insert_text_cb), gtkconv);
5093 g_signal_connect(G_OBJECT(gtkconv->entry_buffer), "delete_range",
5094 G_CALLBACK(delete_text_cb), gtkconv);
5095 gtkconv->u.im->typing_timer = 0;
5096 gtkconv->u.im->animate = purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/animate_buddy_icons");
5097 gtkconv->u.im->show_icon = TRUE;
5100 g_signal_connect_swapped(G_OBJECT(gtkconv->entry_buffer), "changed",
5101 G_CALLBACK(resize_imhtml_cb), gtkconv);
5102 g_signal_connect_swapped(G_OBJECT(gtkconv->entry), "size-allocate",
5103 G_CALLBACK(resize_imhtml_cb), gtkconv);
5105 default_formatize(gtkconv);
5106 g_signal_connect_after(G_OBJECT(gtkconv->entry), "format_function_clear",
5107 G_CALLBACK(clear_formatting_cb), gtkconv);
5108 return vbox;
5111 static void
5112 conv_dnd_recv(GtkWidget *widget, GdkDragContext *dc, guint x, guint y,
5113 GtkSelectionData *sd, guint info, guint t,
5114 PidginConversation *gtkconv)
5116 PurpleConversation *conv = gtkconv->active_conv;
5117 PidginWindow *win = gtkconv->win;
5118 PurpleConversation *c;
5119 PurpleAccount *convaccount = purple_conversation_get_account(conv);
5120 PurpleConnection *gc = purple_account_get_connection(convaccount);
5121 PurplePluginProtocolInfo *prpl_info = gc ? PURPLE_PLUGIN_PROTOCOL_INFO(gc->prpl) : NULL;
5123 if (sd->target == gdk_atom_intern("PURPLE_BLIST_NODE", FALSE))
5125 PurpleBlistNode *n = NULL;
5126 PurpleBuddy *b;
5127 PidginConversation *gtkconv = NULL;
5128 PurpleAccount *buddyaccount;
5129 const char *buddyname;
5131 n = *(PurpleBlistNode **)sd->data;
5133 if (PURPLE_BLIST_NODE_IS_CONTACT(n))
5134 b = purple_contact_get_priority_buddy((PurpleContact*)n);
5135 else if (PURPLE_BLIST_NODE_IS_BUDDY(n))
5136 b = (PurpleBuddy*)n;
5137 else
5138 return;
5140 buddyaccount = purple_buddy_get_account(b);
5141 buddyname = purple_buddy_get_name(b);
5143 * If a buddy is dragged to a chat window of the same protocol,
5144 * invite him to the chat.
5146 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT &&
5147 prpl_info && PURPLE_PROTOCOL_PLUGIN_HAS_FUNC(prpl_info, chat_invite) &&
5148 strcmp(purple_account_get_protocol_id(convaccount),
5149 purple_account_get_protocol_id(buddyaccount)) == 0) {
5150 purple_conv_chat_invite_user(PURPLE_CONV_CHAT(conv), buddyname, NULL, TRUE);
5151 } else {
5153 * If we already have an open conversation with this buddy, then
5154 * just move the conv to this window. Otherwise, create a new
5155 * conv and add it to this window.
5157 c = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, buddyname, buddyaccount);
5158 if (c != NULL) {
5159 PidginWindow *oldwin;
5160 gtkconv = PIDGIN_CONVERSATION(c);
5161 oldwin = gtkconv->win;
5162 if (oldwin != win) {
5163 pidgin_conv_window_remove_gtkconv(oldwin, gtkconv);
5164 pidgin_conv_window_add_gtkconv(win, gtkconv);
5166 } else {
5167 c = purple_conversation_new(PURPLE_CONV_TYPE_IM, buddyaccount, buddyname);
5168 gtkconv = PIDGIN_CONVERSATION(c);
5169 if (gtkconv->win != win) {
5170 pidgin_conv_window_remove_gtkconv(gtkconv->win, gtkconv);
5171 pidgin_conv_window_add_gtkconv(win, gtkconv);
5175 /* Make this conversation the active conversation */
5176 pidgin_conv_window_switch_gtkconv(win, gtkconv);
5179 gtk_drag_finish(dc, TRUE, (dc->action == GDK_ACTION_MOVE), t);
5181 else if (sd->target == gdk_atom_intern("application/x-im-contact", FALSE))
5183 char *protocol = NULL;
5184 char *username = NULL;
5185 PurpleAccount *account;
5186 PidginConversation *gtkconv;
5188 if (pidgin_parse_x_im_contact((const char *)sd->data, FALSE, &account,
5189 &protocol, &username, NULL))
5191 if (account == NULL)
5193 purple_notify_error(win, NULL,
5194 _("You are not currently signed on with an account that "
5195 "can add that buddy."), NULL);
5196 } else {
5198 * If a buddy is dragged to a chat window of the same protocol,
5199 * invite him to the chat.
5201 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT &&
5202 prpl_info && PURPLE_PROTOCOL_PLUGIN_HAS_FUNC(prpl_info, chat_invite) &&
5203 strcmp(purple_account_get_protocol_id(convaccount), protocol) == 0) {
5204 purple_conv_chat_invite_user(PURPLE_CONV_CHAT(conv), username, NULL, TRUE);
5205 } else {
5206 c = purple_conversation_new(PURPLE_CONV_TYPE_IM, account, username);
5207 gtkconv = PIDGIN_CONVERSATION(c);
5208 if (gtkconv->win != win) {
5209 pidgin_conv_window_remove_gtkconv(gtkconv->win, gtkconv);
5210 pidgin_conv_window_add_gtkconv(win, gtkconv);
5216 g_free(username);
5217 g_free(protocol);
5219 gtk_drag_finish(dc, TRUE, (dc->action == GDK_ACTION_MOVE), t);
5221 else if (sd->target == gdk_atom_intern("text/uri-list", FALSE)) {
5222 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM)
5223 pidgin_dnd_file_manage(sd, convaccount, purple_conversation_get_name(conv));
5224 gtk_drag_finish(dc, TRUE, (dc->action == GDK_ACTION_MOVE), t);
5226 else
5227 gtk_drag_finish(dc, FALSE, FALSE, t);
5231 static const GtkTargetEntry te[] =
5233 GTK_IMHTML_DND_TARGETS,
5234 {"PURPLE_BLIST_NODE", GTK_TARGET_SAME_APP, GTK_IMHTML_DRAG_NUM},
5235 {"application/x-im-contact", 0, GTK_IMHTML_DRAG_NUM + 1}
5238 static PidginConversation *
5239 pidgin_conv_find_gtkconv(PurpleConversation * conv)
5241 PurpleBuddy *bud = purple_find_buddy(conv->account, conv->name);
5242 PurpleContact *c;
5243 PurpleBlistNode *cn, *bn;
5245 if (!bud)
5246 return NULL;
5248 if (!(c = purple_buddy_get_contact(bud)))
5249 return NULL;
5251 cn = PURPLE_BLIST_NODE(c);
5252 for (bn = purple_blist_node_get_first_child(cn); bn; bn = purple_blist_node_get_sibling_next(bn)) {
5253 PurpleBuddy *b = PURPLE_BUDDY(bn);
5254 PurpleConversation *conv;
5255 if ((conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, b->name, b->account))) {
5256 if (conv->ui_data)
5257 return conv->ui_data;
5261 return NULL;
5264 static void
5265 buddy_update_cb(PurpleBlistNode *bnode, gpointer null)
5267 GList *list;
5269 g_return_if_fail(bnode);
5270 if (!PURPLE_BLIST_NODE_IS_BUDDY(bnode))
5271 return;
5273 for (list = pidgin_conv_windows_get_list(); list; list = list->next)
5275 PidginWindow *win = list->data;
5276 PurpleConversation *conv = pidgin_conv_window_get_active_conversation(win);
5278 if (purple_conversation_get_type(conv) != PURPLE_CONV_TYPE_IM)
5279 continue;
5281 pidgin_conv_update_fields(conv, PIDGIN_CONV_MENU);
5285 static gboolean
5286 ignore_middle_click(GtkWidget *widget, GdkEventButton *e, gpointer null)
5288 /* A click on the pane is propagated to the notebook containing the pane.
5289 * So if Stu accidentally aims high and middle clicks on the pane-handle,
5290 * it causes a conversation tab to close. Let's stop that from happening.
5292 if (e->button == 2 && e->type == GDK_BUTTON_PRESS)
5293 return TRUE;
5294 return FALSE;
5297 static void set_typing_font(GtkWidget *widget, GtkStyle *style, PidginConversation *gtkconv)
5299 static PangoFontDescription *font_desc = NULL;
5300 static GdkColor *color = NULL;
5301 static gboolean enable = TRUE;
5303 if (font_desc == NULL) {
5304 char *string = NULL;
5305 gtk_widget_style_get(widget,
5306 "typing-notification-font", &string,
5307 "typing-notification-color", &color,
5308 "typing-notification-enable", &enable,
5309 NULL);
5310 font_desc = pango_font_description_from_string(string);
5311 g_free(string);
5312 if (color == NULL) {
5313 GdkColor def = {0, 0x8888, 0x8888, 0x8888};
5314 color = gdk_color_copy(&def);
5318 gtk_text_buffer_create_tag(GTK_IMHTML(widget)->text_buffer, "TYPING-NOTIFICATION",
5319 "foreground-gdk", color,
5320 "font-desc", font_desc,
5321 NULL);
5323 if (!enable) {
5324 g_object_set_data(G_OBJECT(widget), "disable-typing-notification", GINT_TO_POINTER(TRUE));
5325 /* or may be 'gtkconv->disable_typing = TRUE;' instead? */
5328 g_signal_handlers_disconnect_by_func(G_OBJECT(widget), set_typing_font, gtkconv);
5331 /**************************************************************************
5332 * Conversation UI operations
5333 **************************************************************************/
5334 static void
5335 private_gtkconv_new(PurpleConversation *conv, gboolean hidden)
5337 PidginConversation *gtkconv;
5338 PurpleConversationType conv_type = purple_conversation_get_type(conv);
5339 GtkWidget *pane = NULL;
5340 GtkWidget *tab_cont;
5341 PurpleBlistNode *convnode;
5342 PurpleValue *value;
5344 if (conv_type == PURPLE_CONV_TYPE_IM && (gtkconv = pidgin_conv_find_gtkconv(conv))) {
5345 conv->ui_data = gtkconv;
5346 if (!g_list_find(gtkconv->convs, conv))
5347 gtkconv->convs = g_list_prepend(gtkconv->convs, conv);
5348 pidgin_conv_switch_active_conversation(conv);
5349 return;
5352 gtkconv = g_new0(PidginConversation, 1);
5353 conv->ui_data = gtkconv;
5354 gtkconv->active_conv = conv;
5355 gtkconv->convs = g_list_prepend(gtkconv->convs, conv);
5356 gtkconv->send_history = g_list_append(NULL, NULL);
5358 /* Setup some initial variables. */
5359 gtkconv->tooltips = gtk_tooltips_new();
5360 gtkconv->unseen_state = PIDGIN_UNSEEN_NONE;
5361 gtkconv->unseen_count = 0;
5363 if (conv_type == PURPLE_CONV_TYPE_IM) {
5364 gtkconv->u.im = g_malloc0(sizeof(PidginImPane));
5365 } else if (conv_type == PURPLE_CONV_TYPE_CHAT) {
5366 gtkconv->u.chat = g_malloc0(sizeof(PidginChatPane));
5368 pane = setup_common_pane(gtkconv);
5370 gtk_imhtml_set_format_functions(GTK_IMHTML(gtkconv->imhtml),
5371 gtk_imhtml_get_format_functions(GTK_IMHTML(gtkconv->imhtml)) | GTK_IMHTML_IMAGE);
5373 if (pane == NULL) {
5374 if (conv_type == PURPLE_CONV_TYPE_CHAT)
5375 g_free(gtkconv->u.chat);
5376 else if (conv_type == PURPLE_CONV_TYPE_IM)
5377 g_free(gtkconv->u.im);
5379 g_free(gtkconv);
5380 conv->ui_data = NULL;
5381 return;
5384 /* Setup drag-and-drop */
5385 gtk_drag_dest_set(pane,
5386 GTK_DEST_DEFAULT_MOTION |
5387 GTK_DEST_DEFAULT_DROP,
5388 te, sizeof(te) / sizeof(GtkTargetEntry),
5389 GDK_ACTION_COPY);
5390 gtk_drag_dest_set(pane,
5391 GTK_DEST_DEFAULT_MOTION |
5392 GTK_DEST_DEFAULT_DROP,
5393 te, sizeof(te) / sizeof(GtkTargetEntry),
5394 GDK_ACTION_COPY);
5395 gtk_drag_dest_set(gtkconv->imhtml, 0,
5396 te, sizeof(te) / sizeof(GtkTargetEntry),
5397 GDK_ACTION_COPY);
5399 gtk_drag_dest_set(gtkconv->entry, 0,
5400 te, sizeof(te) / sizeof(GtkTargetEntry),
5401 GDK_ACTION_COPY);
5403 g_signal_connect(G_OBJECT(pane), "button_press_event",
5404 G_CALLBACK(ignore_middle_click), NULL);
5405 g_signal_connect(G_OBJECT(pane), "drag_data_received",
5406 G_CALLBACK(conv_dnd_recv), gtkconv);
5407 g_signal_connect(G_OBJECT(gtkconv->imhtml), "drag_data_received",
5408 G_CALLBACK(conv_dnd_recv), gtkconv);
5409 g_signal_connect(G_OBJECT(gtkconv->entry), "drag_data_received",
5410 G_CALLBACK(conv_dnd_recv), gtkconv);
5412 g_signal_connect(gtkconv->imhtml, "style-set", G_CALLBACK(set_typing_font), gtkconv);
5414 /* Setup the container for the tab. */
5415 gtkconv->tab_cont = tab_cont = gtk_vbox_new(FALSE, PIDGIN_HIG_BOX_SPACE);
5416 g_object_set_data(G_OBJECT(tab_cont), "PidginConversation", gtkconv);
5417 gtk_container_set_border_width(GTK_CONTAINER(tab_cont), PIDGIN_HIG_BOX_SPACE);
5418 gtk_container_add(GTK_CONTAINER(tab_cont), pane);
5419 gtk_widget_show(pane);
5421 convnode = get_conversation_blist_node(conv);
5422 if (convnode == NULL || !purple_blist_node_get_bool(convnode, "gtk-mute-sound"))
5423 gtkconv->make_sound = TRUE;
5425 if (convnode != NULL &&
5426 (value = g_hash_table_lookup(convnode->settings, "enable-logging")) &&
5427 purple_value_get_type(value) == PURPLE_TYPE_BOOLEAN)
5429 purple_conversation_set_logging(conv, purple_value_get_boolean(value));
5432 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/show_formatting_toolbar"))
5433 gtk_widget_show(gtkconv->toolbar);
5434 else
5435 gtk_widget_hide(gtkconv->toolbar);
5437 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons"))
5438 gtk_widget_show(gtkconv->infopane_hbox);
5439 else
5440 gtk_widget_hide(gtkconv->infopane_hbox);
5442 gtk_imhtml_show_comments(GTK_IMHTML(gtkconv->imhtml),
5443 purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/show_timestamps"));
5444 gtk_imhtml_set_protocol_name(GTK_IMHTML(gtkconv->imhtml),
5445 purple_account_get_protocol_name(conv->account));
5447 g_signal_connect_swapped(G_OBJECT(pane), "focus",
5448 G_CALLBACK(gtk_widget_grab_focus),
5449 gtkconv->entry);
5451 if (hidden)
5452 pidgin_conv_window_add_gtkconv(hidden_convwin, gtkconv);
5453 else
5454 pidgin_conv_placement_place(gtkconv);
5456 if (nick_colors == NULL) {
5457 nbr_nick_colors = NUM_NICK_COLORS;
5458 nick_colors = generate_nick_colors(&nbr_nick_colors, gtk_widget_get_style(gtkconv->imhtml)->base[GTK_STATE_NORMAL]);
5461 if (conv->features & PURPLE_CONNECTION_ALLOW_CUSTOM_SMILEY)
5462 pidgin_themes_smiley_themeize_custom(gtkconv->entry);
5465 static void
5466 pidgin_conv_new_hidden(PurpleConversation *conv)
5468 private_gtkconv_new(conv, TRUE);
5471 void
5472 pidgin_conv_new(PurpleConversation *conv)
5474 private_gtkconv_new(conv, FALSE);
5475 if (PIDGIN_IS_PIDGIN_CONVERSATION(conv))
5476 purple_signal_emit(pidgin_conversations_get_handle(),
5477 "conversation-displayed", PIDGIN_CONVERSATION(conv));
5480 static void
5481 received_im_msg_cb(PurpleAccount *account, char *sender, char *message,
5482 PurpleConversation *conv, PurpleMessageFlags flags)
5484 PurpleConversationUiOps *ui_ops = pidgin_conversations_get_conv_ui_ops();
5485 gboolean hide = FALSE;
5486 guint timer;
5488 /* create hidden conv if hide_new pref is always */
5489 if (strcmp(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new"), "always") == 0)
5490 hide = TRUE;
5492 /* create hidden conv if hide_new pref is away and account is away */
5493 if (strcmp(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new"), "away") == 0 &&
5494 !purple_status_is_available(purple_account_get_active_status(account)))
5495 hide = TRUE;
5497 if (conv && PIDGIN_IS_PIDGIN_CONVERSATION(conv) && !hide) {
5498 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
5499 if (gtkconv->win == hidden_convwin) {
5500 pidgin_conv_attach_to_conversation(gtkconv->active_conv);
5502 return;
5505 if (hide) {
5506 ui_ops->create_conversation = pidgin_conv_new_hidden;
5507 purple_conversation_new(PURPLE_CONV_TYPE_IM, account, sender);
5508 ui_ops->create_conversation = pidgin_conv_new;
5511 /* Somebody wants to keep this conversation around, so don't time it out */
5512 if (conv) {
5513 timer = GPOINTER_TO_INT(purple_conversation_get_data(conv, "close-timer"));
5514 if (timer) {
5515 purple_timeout_remove(timer);
5516 purple_conversation_set_data(conv, "close-timer", GINT_TO_POINTER(0));
5521 static void
5522 pidgin_conv_destroy(PurpleConversation *conv)
5524 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
5526 gtkconv->convs = g_list_remove(gtkconv->convs, conv);
5527 /* Don't destroy ourselves until all our convos are gone */
5528 if (gtkconv->convs) {
5529 /* Make sure the destroyed conversation is not the active one */
5530 if (gtkconv->active_conv == conv) {
5531 gtkconv->active_conv = gtkconv->convs->data;
5532 purple_conversation_update(gtkconv->active_conv, PURPLE_CONV_UPDATE_FEATURES);
5534 return;
5537 pidgin_conv_window_remove_gtkconv(gtkconv->win, gtkconv);
5539 /* If the "Save Conversation" or "Save Icon" dialogs are open then close them */
5540 purple_request_close_with_handle(gtkconv);
5541 purple_notify_close_with_handle(gtkconv);
5543 gtk_widget_destroy(gtkconv->tab_cont);
5544 g_object_unref(gtkconv->tab_cont);
5546 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) {
5547 if (gtkconv->u.im->icon_timer != 0)
5548 g_source_remove(gtkconv->u.im->icon_timer);
5550 if (gtkconv->u.im->anim != NULL)
5551 g_object_unref(G_OBJECT(gtkconv->u.im->anim));
5553 if (gtkconv->u.im->typing_timer != 0)
5554 g_source_remove(gtkconv->u.im->typing_timer);
5556 g_free(gtkconv->u.im);
5557 } else if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) {
5558 purple_signals_disconnect_by_handle(gtkconv->u.chat);
5559 g_free(gtkconv->u.chat);
5562 gtk_object_sink(GTK_OBJECT(gtkconv->tooltips));
5564 gtkconv->send_history = g_list_first(gtkconv->send_history);
5565 g_list_foreach(gtkconv->send_history, (GFunc)g_free, NULL);
5566 g_list_free(gtkconv->send_history);
5568 if (gtkconv->attach.timer) {
5569 g_source_remove(gtkconv->attach.timer);
5572 g_free(gtkconv);
5576 static void
5577 pidgin_conv_write_im(PurpleConversation *conv, const char *who,
5578 const char *message, PurpleMessageFlags flags,
5579 time_t mtime)
5581 PidginConversation *gtkconv;
5583 gtkconv = PIDGIN_CONVERSATION(conv);
5585 if (conv != gtkconv->active_conv &&
5586 flags & PURPLE_MESSAGE_ACTIVE_ONLY)
5588 /* Plugins that want these messages suppressed should be
5589 * calling purple_conv_im_write(), so they get suppressed here,
5590 * before being written to the log. */
5591 purple_debug_info("gtkconv",
5592 "Suppressing message for an inactive conversation in pidgin_conv_write_im()\n");
5593 return;
5596 purple_conversation_write(conv, who, message, flags, mtime);
5599 static const char *
5600 get_text_tag_color(GtkTextTag *tag)
5602 GdkColor *color = NULL;
5603 gboolean set = FALSE;
5604 static char colcode[] = "#XXXXXX";
5605 if (tag)
5606 g_object_get(G_OBJECT(tag), "foreground-set", &set, "foreground-gdk", &color, NULL);
5607 if (set && color)
5608 g_snprintf(colcode, sizeof(colcode), "#%02x%02x%02x",
5609 color->red >> 8, color->green >> 8, color->blue >> 8);
5610 else
5611 colcode[0] = '\0';
5612 if (color)
5613 gdk_color_free(color);
5614 return colcode;
5617 /* The callback for an event on a link tag. */
5618 static gboolean buddytag_event(GtkTextTag *tag, GObject *imhtml,
5619 GdkEvent *event, GtkTextIter *arg2, gpointer data)
5621 if (event->type == GDK_BUTTON_PRESS
5622 || event->type == GDK_2BUTTON_PRESS) {
5623 GdkEventButton *btn_event = (GdkEventButton*) event;
5624 PurpleConversation *conv = data;
5625 char *buddyname;
5627 /* strlen("BUDDY " or "HILIT ") == 6 */
5628 g_return_val_if_fail((tag->name != NULL)
5629 && (strlen(tag->name) > 6), FALSE);
5631 buddyname = (tag->name) + 6;
5633 /* emit chat-nick-clicked signal */
5634 if (event->type == GDK_BUTTON_PRESS) {
5635 gint plugin_return = GPOINTER_TO_INT(purple_signal_emit_return_1(
5636 pidgin_conversations_get_handle(), "chat-nick-clicked",
5637 data, buddyname, btn_event->button));
5638 if (plugin_return)
5639 return TRUE;
5642 if (btn_event->button == 1 &&
5643 event->type == GDK_2BUTTON_PRESS) {
5644 chat_do_im(PIDGIN_CONVERSATION(conv), buddyname);
5645 return TRUE;
5646 } else if (btn_event->button == 2
5647 && event->type == GDK_2BUTTON_PRESS) {
5648 chat_do_info(PIDGIN_CONVERSATION(conv), buddyname);
5650 return TRUE;
5651 } else if (btn_event->button == 3
5652 && event->type == GDK_BUTTON_PRESS) {
5653 GtkTextIter start, end;
5655 /* we shouldn't display the popup
5656 * if the user has selected something: */
5657 if (!gtk_text_buffer_get_selection_bounds(
5658 gtk_text_iter_get_buffer(arg2),
5659 &start, &end)) {
5660 GtkWidget *menu = NULL;
5661 PurpleConnection *gc =
5662 purple_conversation_get_gc(conv);
5665 menu = create_chat_menu(conv, buddyname, gc);
5666 gtk_menu_popup(GTK_MENU(menu), NULL, NULL,
5667 NULL, GTK_WIDGET(imhtml),
5668 btn_event->button,
5669 btn_event->time);
5671 /* Don't propagate the event any further */
5672 return TRUE;
5677 return FALSE;
5680 static GtkTextTag *get_buddy_tag(PurpleConversation *conv, const char *who, PurpleMessageFlags flag,
5681 gboolean create)
5683 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
5684 GtkTextTag *buddytag;
5685 gchar *str;
5686 gboolean highlight = (flag & PURPLE_MESSAGE_NICK);
5687 GtkTextBuffer *buffer = GTK_IMHTML(gtkconv->imhtml)->text_buffer;
5689 str = g_strdup_printf(highlight ? "HILIT %s" : "BUDDY %s", who);
5691 buddytag = gtk_text_tag_table_lookup(
5692 gtk_text_buffer_get_tag_table(buffer), str);
5694 if (buddytag == NULL && create) {
5695 if (highlight)
5696 buddytag = gtk_text_buffer_create_tag(buffer, str,
5697 "foreground", get_text_tag_color(gtk_text_tag_table_lookup(
5698 gtk_text_buffer_get_tag_table(buffer), "highlight-name")),
5699 "weight", PANGO_WEIGHT_BOLD,
5700 NULL);
5701 else
5702 buddytag = gtk_text_buffer_create_tag(
5703 buffer, str,
5704 "foreground-gdk", get_nick_color(gtkconv, who),
5705 "weight", purple_find_buddy(purple_conversation_get_account(conv), who) ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL,
5706 NULL);
5708 g_object_set_data(G_OBJECT(buddytag), "cursor", "");
5709 g_signal_connect(G_OBJECT(buddytag), "event",
5710 G_CALLBACK(buddytag_event), conv);
5713 g_free(str);
5715 return buddytag;
5718 static void pidgin_conv_calculate_newday(PidginConversation *gtkconv, time_t mtime)
5720 struct tm *tm = localtime(&mtime);
5722 tm->tm_hour = tm->tm_min = tm->tm_sec = 0;
5723 tm->tm_mday++;
5725 gtkconv->newday = mktime(tm);
5728 /* Detect string direction and encapsulate the string in RLE/LRE/PDF unicode characters
5729 str - pointer to string (string is re-allocated and the pointer updated) */
5730 static void
5731 str_embed_direction_chars(char **str)
5733 #ifdef HAVE_PANGO14
5734 char pre_str[4];
5735 char post_str[10];
5736 char *ret;
5738 if (PANGO_DIRECTION_RTL == pango_find_base_dir(*str, -1))
5740 sprintf(pre_str, "%c%c%c",
5741 0xE2, 0x80, 0xAB); /* RLE */
5742 sprintf(post_str, "%c%c%c%c%c%c%c%c%c",
5743 0xE2, 0x80, 0xAC, /* PDF */
5744 0xE2, 0x80, 0x8E, /* LRM */
5745 0xE2, 0x80, 0xAC); /* PDF */
5747 else
5749 sprintf(pre_str, "%c%c%c",
5750 0xE2, 0x80, 0xAA); /* LRE */
5751 sprintf(post_str, "%c%c%c%c%c%c%c%c%c",
5752 0xE2, 0x80, 0xAC, /* PDF */
5753 0xE2, 0x80, 0x8F, /* RLM */
5754 0xE2, 0x80, 0xAC); /* PDF */
5757 ret = g_strconcat(pre_str, *str, post_str, NULL);
5759 g_free(*str);
5760 *str = ret;
5761 #endif
5764 static void
5765 pidgin_conv_write_conv(PurpleConversation *conv, const char *name, const char *alias,
5766 const char *message, PurpleMessageFlags flags,
5767 time_t mtime)
5769 PidginConversation *gtkconv;
5770 PurpleConnection *gc;
5771 PurpleAccount *account;
5772 int gtk_font_options = 0;
5773 int gtk_font_options_all = 0;
5774 int max_scrollback_lines;
5775 int line_count;
5776 char buf2[BUF_LONG];
5777 gboolean show_date;
5778 char *mdate;
5779 char *str;
5780 char *with_font_tag;
5781 char *sml_attrib = NULL;
5782 size_t length;
5783 PurpleConversationType type;
5784 char *displaying;
5785 gboolean plugin_return;
5786 char *bracket;
5787 int tag_count = 0;
5788 gboolean is_rtl_message = FALSE;
5790 g_return_if_fail(conv != NULL);
5791 gtkconv = PIDGIN_CONVERSATION(conv);
5792 g_return_if_fail(gtkconv != NULL);
5794 if (gtkconv->attach.timer) {
5795 /* We are currently in the process of filling up the buffer with the message
5796 * history of the conversation. So we do not need to add the message here.
5797 * Instead, this message will be added to the message-list, which in turn will
5798 * be processed and displayed by the attach-callback.
5800 return;
5803 if (conv != gtkconv->active_conv)
5805 if (flags & PURPLE_MESSAGE_ACTIVE_ONLY)
5807 /* Unless this had PURPLE_MESSAGE_NO_LOG, this message
5808 * was logged. Plugin writers: if this isn't what
5809 * you wanted, call purple_conv_im_write() instead of
5810 * purple_conversation_write(). */
5811 purple_debug_info("gtkconv",
5812 "Suppressing message for an inactive conversation in pidgin_conv_write_conv()\n");
5813 return;
5816 /* Set the active conversation to the one that just messaged us. */
5817 /* TODO: consider not doing this if the account is offline or something */
5818 if (flags & (PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_RECV))
5819 pidgin_conv_switch_active_conversation(conv);
5822 type = purple_conversation_get_type(conv);
5823 account = purple_conversation_get_account(conv);
5824 g_return_if_fail(account != NULL);
5825 gc = purple_account_get_connection(account);
5826 g_return_if_fail(gc != NULL || !(flags & (PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_RECV)));
5828 /* Make sure URLs are clickable */
5829 if(flags & PURPLE_MESSAGE_NO_LINKIFY)
5830 displaying = g_strdup(message);
5831 else
5832 displaying = purple_markup_linkify(message);
5834 plugin_return = GPOINTER_TO_INT(purple_signal_emit_return_1(
5835 pidgin_conversations_get_handle(), (type == PURPLE_CONV_TYPE_IM ?
5836 "displaying-im-msg" : "displaying-chat-msg"),
5837 account, name, &displaying, conv, flags));
5838 if (plugin_return)
5840 g_free(displaying);
5841 return;
5843 length = strlen(displaying) + 1;
5845 /* Awful hack to work around GtkIMHtml's inefficient rendering of messages with lots of formatting changes.
5846 * If a message has over 100 '<' characters, strip formatting before appending it. Hopefully nobody actually
5847 * needs that much formatting, anyway.
5849 for (bracket = strchr(displaying, '<'); bracket && *(bracket + 1); bracket = strchr(bracket + 1, '<'))
5850 tag_count++;
5852 if (tag_count > 100) {
5853 char *tmp = displaying;
5854 displaying = purple_markup_strip_html(tmp);
5855 g_free(tmp);
5858 line_count = gtk_text_buffer_get_line_count(
5859 gtk_text_view_get_buffer(GTK_TEXT_VIEW(
5860 gtkconv->imhtml)));
5862 max_scrollback_lines = purple_prefs_get_int(
5863 PIDGIN_PREFS_ROOT "/conversations/scrollback_lines");
5864 /* If we're sitting at more than 100 lines more than the
5865 max scrollback, trim down to max scrollback */
5866 if (max_scrollback_lines > 0
5867 && line_count > (max_scrollback_lines + 100)) {
5868 GtkTextBuffer *text_buffer = gtk_text_view_get_buffer(
5869 GTK_TEXT_VIEW(gtkconv->imhtml));
5870 GtkTextIter start, end;
5872 gtk_text_buffer_get_start_iter(text_buffer, &start);
5873 gtk_text_buffer_get_iter_at_line(text_buffer, &end,
5874 (line_count - max_scrollback_lines));
5875 gtk_imhtml_delete(GTK_IMHTML(gtkconv->imhtml), &start, &end);
5878 if (type == PURPLE_CONV_TYPE_CHAT)
5880 /* Create anchor for user */
5881 GtkTextIter iter;
5882 char *tmp = g_strconcat("user:", name, NULL);
5884 gtk_text_buffer_get_end_iter(gtk_text_view_get_buffer(GTK_TEXT_VIEW(gtkconv->imhtml)), &iter);
5885 gtk_text_buffer_create_mark(gtk_text_view_get_buffer(GTK_TEXT_VIEW(gtkconv->imhtml)),
5886 tmp, &iter, TRUE);
5887 g_free(tmp);
5890 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/use_smooth_scrolling"))
5891 gtk_font_options_all |= GTK_IMHTML_USE_SMOOTHSCROLLING;
5893 if (gtk_text_buffer_get_char_count(gtk_text_view_get_buffer(GTK_TEXT_VIEW(gtkconv->imhtml))))
5894 gtk_imhtml_append_text(GTK_IMHTML(gtkconv->imhtml), "<BR>", gtk_font_options_all | GTK_IMHTML_NO_SCROLL);
5896 /* First message in a conversation. */
5897 if (gtkconv->newday == 0)
5898 pidgin_conv_calculate_newday(gtkconv, mtime);
5900 /* Show the date on the first message in a new day, or if the message is
5901 * older than 20 minutes. */
5902 show_date = (mtime >= gtkconv->newday) || (time(NULL) > mtime + 20*60);
5904 mdate = purple_signal_emit_return_1(pidgin_conversations_get_handle(),
5905 "conversation-timestamp",
5906 conv, mtime, show_date);
5908 if (mdate == NULL)
5910 struct tm *tm = localtime(&mtime);
5911 const char *tmp;
5912 if (show_date)
5913 tmp = purple_date_format_long(tm);
5914 else
5915 tmp = purple_time_format(tm);
5916 mdate = g_strdup_printf("(%s)", tmp);
5919 /* Bi-Directional support - set timestamp direction using unicode characters */
5920 is_rtl_message = purple_markup_is_rtl(message);
5921 /* Enforce direction only if message is RTL - doesn't effect LTR users */
5922 if (is_rtl_message)
5923 str_embed_direction_chars(&mdate);
5925 if (mtime >= gtkconv->newday)
5926 pidgin_conv_calculate_newday(gtkconv, mtime);
5928 sml_attrib = g_strdup_printf("sml=\"%s\"", purple_account_get_protocol_name(account));
5930 gtk_font_options |= GTK_IMHTML_NO_COMMENTS;
5932 if ((flags & PURPLE_MESSAGE_RECV) &&
5933 !purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/show_incoming_formatting"))
5934 gtk_font_options |= GTK_IMHTML_NO_COLOURS | GTK_IMHTML_NO_FONTS | GTK_IMHTML_NO_SIZES | GTK_IMHTML_NO_FORMATTING;
5936 /* this is gonna crash one day, I can feel it. */
5937 if (PURPLE_PLUGIN_PROTOCOL_INFO(purple_find_prpl(purple_account_get_protocol_id(conv->account)))->options &
5938 OPT_PROTO_USE_POINTSIZE) {
5939 gtk_font_options |= GTK_IMHTML_USE_POINTSIZE;
5942 if (!(flags & PURPLE_MESSAGE_RECV) && (conv->features & PURPLE_CONNECTION_ALLOW_CUSTOM_SMILEY))
5944 /* We want to see our own smileys. Need to revert it after send*/
5945 pidgin_themes_smiley_themeize_custom(gtkconv->imhtml);
5948 /* TODO: These colors should not be hardcoded so log.c can use them */
5949 if (flags & PURPLE_MESSAGE_RAW) {
5950 gtk_imhtml_append_text(GTK_IMHTML(gtkconv->imhtml), message, gtk_font_options_all);
5951 } else if (flags & PURPLE_MESSAGE_SYSTEM) {
5952 g_snprintf(buf2, sizeof(buf2),
5953 "<FONT %s><FONT SIZE=\"2\"><!--%s --></FONT><B>%s</B></FONT>",
5954 sml_attrib ? sml_attrib : "", mdate, displaying);
5956 gtk_imhtml_append_text(GTK_IMHTML(gtkconv->imhtml), buf2, gtk_font_options_all);
5958 } else if (flags & PURPLE_MESSAGE_ERROR) {
5959 g_snprintf(buf2, sizeof(buf2),
5960 "<FONT COLOR=\"#ff0000\"><FONT %s><FONT SIZE=\"2\"><!--%s --></FONT><B>%s</B></FONT></FONT>",
5961 sml_attrib ? sml_attrib : "", mdate, displaying);
5963 gtk_imhtml_append_text(GTK_IMHTML(gtkconv->imhtml), buf2, gtk_font_options_all);
5965 } else if (flags & PURPLE_MESSAGE_NO_LOG) {
5966 g_snprintf(buf2, BUF_LONG,
5967 "<B><FONT %s COLOR=\"#777777\">%s</FONT></B>",
5968 sml_attrib ? sml_attrib : "", displaying);
5970 gtk_imhtml_append_text(GTK_IMHTML(gtkconv->imhtml), buf2, gtk_font_options_all);
5971 } else {
5972 char *new_message = g_memdup(displaying, length);
5973 char *alias_escaped = (alias ? g_markup_escape_text(alias, strlen(alias)) : g_strdup(""));
5974 /* The initial offset is to deal with
5975 * escaped entities making the string longer */
5976 int tag_start_offset = 0;
5977 int tag_end_offset = 0;
5978 const char *tagname = NULL;
5980 GtkTextIter start, end;
5981 GtkTextMark *mark;
5982 GtkTextTag *tag;
5983 GtkTextBuffer *buffer = GTK_IMHTML(gtkconv->imhtml)->text_buffer;
5985 /* Enforce direction on alias */
5986 if (is_rtl_message)
5987 str_embed_direction_chars(&alias_escaped);
5989 str = g_malloc(1024);
5990 if (flags & PURPLE_MESSAGE_WHISPER) {
5991 /* If we're whispering, it's not an autoresponse. */
5992 if (purple_message_meify(new_message, -1 )) {
5993 g_snprintf(str, 1024, "***%s", alias_escaped);
5994 tag_start_offset += 3;
5995 tagname = "whisper-action-name";
5997 else {
5998 g_snprintf(str, 1024, "*%s*:", alias_escaped);
5999 tag_start_offset += 1;
6000 tag_end_offset = 2;
6001 tagname = "whisper-name";
6003 } else {
6004 if (purple_message_meify(new_message, -1)) {
6005 if (flags & PURPLE_MESSAGE_AUTO_RESP) {
6006 g_snprintf(str, 1024, "%s ***%s", AUTO_RESPONSE, alias_escaped);
6007 tag_start_offset += strlen(AUTO_RESPONSE) - 6 + 4;
6008 } else {
6009 g_snprintf(str, 1024, "***%s", alias_escaped);
6010 tag_start_offset += 3;
6013 if (flags & PURPLE_MESSAGE_NICK)
6014 tagname = "highlight-name";
6015 else
6016 tagname = "action-name";
6017 } else {
6018 if (flags & PURPLE_MESSAGE_AUTO_RESP) {
6019 g_snprintf(str, 1024, "%s %s", alias_escaped, AUTO_RESPONSE);
6020 tag_start_offset += strlen(AUTO_RESPONSE) - 6 + 1;
6021 } else {
6022 g_snprintf(str, 1024, "%s:", alias_escaped);
6023 tag_end_offset = 1;
6026 if (flags & PURPLE_MESSAGE_NICK) {
6027 if (type == PURPLE_CONV_TYPE_IM) {
6028 tagname = "highlight-name";
6030 } else if (flags & PURPLE_MESSAGE_RECV) {
6031 /* The tagname for chats is handled by get_buddy_tag */
6032 if (type == PURPLE_CONV_TYPE_IM) {
6033 tagname = "receive-name";
6035 } else if (flags & PURPLE_MESSAGE_SEND) {
6036 tagname = "send-name";
6037 } else {
6038 purple_debug_error("gtkconv", "message missing flags\n");
6043 g_free(alias_escaped);
6045 if (tagname)
6046 tag = gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(buffer), tagname);
6047 else
6048 tag = get_buddy_tag(conv, name, flags, TRUE);
6050 if (GTK_IMHTML(gtkconv->imhtml)->show_comments) {
6051 /* The color for the timestamp has to be set in the font-tags, unfortunately.
6052 * Applying the nick-tag to timestamps would work, but that can make it
6053 * bold. I thought applying the "comment" tag again, which has "weight" set
6054 * to PANGO_WEIGHT_NORMAL, would remove the boldness. But it doesn't. So
6055 * this will have to do. I don't terribly like it. -- sadrul */
6056 const char *color = get_text_tag_color(tag);
6057 g_snprintf(buf2, BUF_LONG, "<FONT %s%s%s SIZE=\"2\"><!--%s --></FONT>",
6058 color ? "COLOR=\"" : "", color ? color : "", color ? "\"" : "", mdate);
6059 gtk_imhtml_append_text(GTK_IMHTML(gtkconv->imhtml), buf2, gtk_font_options_all | GTK_IMHTML_NO_SCROLL);
6062 gtk_text_buffer_get_end_iter(buffer, &end);
6063 mark = gtk_text_buffer_create_mark(buffer, NULL, &end, TRUE);
6065 g_snprintf(buf2, BUF_LONG, "<FONT %s>%s</FONT> ", sml_attrib ? sml_attrib : "", str);
6066 gtk_imhtml_append_text(GTK_IMHTML(gtkconv->imhtml), buf2, gtk_font_options_all | GTK_IMHTML_NO_SCROLL);
6068 gtk_text_buffer_get_end_iter(buffer, &end);
6069 gtk_text_buffer_get_iter_at_mark(buffer, &start, mark);
6070 gtk_text_buffer_apply_tag(buffer, tag, &start, &end);
6071 gtk_text_buffer_delete_mark(buffer, mark);
6073 g_free(str);
6075 if(gc){
6076 char *pre = g_strdup_printf("<font %s>", sml_attrib ? sml_attrib : "");
6077 char *post = "</font>";
6078 int pre_len = strlen(pre);
6079 int post_len = strlen(post);
6081 with_font_tag = g_malloc(length + pre_len + post_len + 1);
6083 strcpy(with_font_tag, pre);
6084 memcpy(with_font_tag + pre_len, new_message, length);
6085 strcpy(with_font_tag + pre_len + length, post);
6087 length += pre_len + post_len;
6088 g_free(pre);
6089 } else
6090 with_font_tag = g_memdup(new_message, length);
6092 gtk_imhtml_append_text(GTK_IMHTML(gtkconv->imhtml),
6093 with_font_tag, gtk_font_options | gtk_font_options_all);
6095 g_free(with_font_tag);
6096 g_free(new_message);
6099 g_free(mdate);
6100 g_free(sml_attrib);
6102 /* Tab highlighting stuff */
6103 if (!(flags & PURPLE_MESSAGE_SEND) && !pidgin_conv_has_focus(conv))
6105 PidginUnseenState unseen = PIDGIN_UNSEEN_NONE;
6107 if ((flags & PURPLE_MESSAGE_NICK) == PURPLE_MESSAGE_NICK)
6108 unseen = PIDGIN_UNSEEN_NICK;
6109 else if (((flags & PURPLE_MESSAGE_SYSTEM) == PURPLE_MESSAGE_SYSTEM) ||
6110 ((flags & PURPLE_MESSAGE_ERROR) == PURPLE_MESSAGE_ERROR))
6111 unseen = PIDGIN_UNSEEN_EVENT;
6112 else if ((flags & PURPLE_MESSAGE_NO_LOG) == PURPLE_MESSAGE_NO_LOG)
6113 unseen = PIDGIN_UNSEEN_NO_LOG;
6114 else
6115 unseen = PIDGIN_UNSEEN_TEXT;
6117 gtkconv_set_unseen(gtkconv, unseen);
6120 if (!(flags & PURPLE_MESSAGE_RECV) && (conv->features & PURPLE_CONNECTION_ALLOW_CUSTOM_SMILEY))
6122 /* Restore the smiley-data */
6123 pidgin_themes_smiley_themeize(gtkconv->imhtml);
6126 purple_signal_emit(pidgin_conversations_get_handle(),
6127 (type == PURPLE_CONV_TYPE_IM ? "displayed-im-msg" : "displayed-chat-msg"),
6128 account, name, displaying, conv, flags);
6129 g_free(displaying);
6130 update_typing_message(gtkconv, NULL);
6133 static gboolean get_iter_from_chatbuddy(PurpleConvChatBuddy *cb, GtkTreeIter *iter)
6135 GtkTreeRowReference *ref;
6136 GtkTreePath *path;
6137 GtkTreeModel *model;
6139 g_return_val_if_fail(cb != NULL, FALSE);
6141 ref = cb->ui_data;
6142 if (!ref)
6143 return FALSE;
6145 if ((path = gtk_tree_row_reference_get_path(ref)) == NULL)
6146 return FALSE;
6148 model = gtk_tree_row_reference_get_model(ref);
6149 if (!gtk_tree_model_get_iter(GTK_TREE_MODEL(model), iter, path)) {
6150 gtk_tree_path_free(path);
6151 return FALSE;
6154 gtk_tree_path_free(path);
6155 return TRUE;
6158 static void
6159 pidgin_conv_chat_add_users(PurpleConversation *conv, GList *cbuddies, gboolean new_arrivals)
6161 PurpleConvChat *chat;
6162 PidginConversation *gtkconv;
6163 PidginChatPane *gtkchat;
6164 GtkListStore *ls;
6165 GList *l;
6167 char tmp[BUF_LONG];
6168 int num_users;
6170 chat = PURPLE_CONV_CHAT(conv);
6171 gtkconv = PIDGIN_CONVERSATION(conv);
6172 gtkchat = gtkconv->u.chat;
6174 num_users = g_list_length(purple_conv_chat_get_users(chat));
6176 g_snprintf(tmp, sizeof(tmp),
6177 ngettext("%d person in room", "%d people in room",
6178 num_users),
6179 num_users);
6181 gtk_label_set_text(GTK_LABEL(gtkchat->count), tmp);
6183 ls = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list)));
6185 gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(ls), GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID,
6186 GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID);
6188 l = cbuddies;
6189 while (l != NULL) {
6190 add_chat_buddy_common(conv, (PurpleConvChatBuddy *)l->data, NULL);
6191 l = l->next;
6194 /* Currently GTK+ maintains our sorted list after it's in the tree.
6195 * This may change if it turns out we can manage it faster ourselves.
6197 gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(ls), CHAT_USERS_ALIAS_KEY_COLUMN,
6198 GTK_SORT_ASCENDING);
6201 static void
6202 pidgin_conv_chat_rename_user(PurpleConversation *conv, const char *old_name,
6203 const char *new_name, const char *new_alias)
6205 PurpleConvChat *chat;
6206 PidginConversation *gtkconv;
6207 PidginChatPane *gtkchat;
6208 PurpleConvChatBuddy *old_cbuddy, *new_cbuddy;
6209 GtkTreeIter iter;
6210 GtkTreeModel *model;
6211 GtkTextTag *tag;
6213 chat = PURPLE_CONV_CHAT(conv);
6214 gtkconv = PIDGIN_CONVERSATION(conv);
6215 gtkchat = gtkconv->u.chat;
6217 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
6219 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter))
6220 return;
6222 if ((tag = get_buddy_tag(conv, old_name, 0, FALSE)))
6223 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_ITALIC, NULL);
6224 if ((tag = get_buddy_tag(conv, old_name, PURPLE_MESSAGE_NICK, FALSE)))
6225 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_ITALIC, NULL);
6227 old_cbuddy = purple_conv_chat_cb_find(chat, old_name);
6228 if (!old_cbuddy)
6229 return;
6231 if (get_iter_from_chatbuddy(old_cbuddy, &iter)) {
6232 GtkTreeRowReference *ref = old_cbuddy->ui_data;
6234 gtk_list_store_remove(GTK_LIST_STORE(model), &iter);
6235 gtk_tree_row_reference_free(ref);
6236 old_cbuddy->ui_data = NULL;
6239 g_return_if_fail(new_alias != NULL);
6241 new_cbuddy = purple_conv_chat_cb_find(chat, new_name);
6243 add_chat_buddy_common(conv, new_cbuddy, old_name);
6246 static void
6247 pidgin_conv_chat_remove_users(PurpleConversation *conv, GList *users)
6249 PurpleConvChat *chat;
6250 PidginConversation *gtkconv;
6251 PidginChatPane *gtkchat;
6252 GtkTreeIter iter;
6253 GtkTreeModel *model;
6254 GList *l;
6255 char tmp[BUF_LONG];
6256 int num_users;
6257 gboolean f;
6258 GtkTextTag *tag;
6260 chat = PURPLE_CONV_CHAT(conv);
6261 gtkconv = PIDGIN_CONVERSATION(conv);
6262 gtkchat = gtkconv->u.chat;
6264 num_users = g_list_length(purple_conv_chat_get_users(chat));
6266 for (l = users; l != NULL; l = l->next) {
6267 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
6269 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter))
6270 /* XXX: Break? */
6271 continue;
6273 do {
6274 char *val;
6276 gtk_tree_model_get(GTK_TREE_MODEL(model), &iter,
6277 CHAT_USERS_NAME_COLUMN, &val, -1);
6279 if (!purple_utf8_strcasecmp((char *)l->data, val)) {
6280 f = gtk_list_store_remove(GTK_LIST_STORE(model), &iter);
6282 else
6283 f = gtk_tree_model_iter_next(GTK_TREE_MODEL(model), &iter);
6285 g_free(val);
6286 } while (f);
6288 if ((tag = get_buddy_tag(conv, l->data, 0, FALSE)))
6289 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_ITALIC, NULL);
6290 if ((tag = get_buddy_tag(conv, l->data, PURPLE_MESSAGE_NICK, FALSE)))
6291 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_ITALIC, NULL);
6294 g_snprintf(tmp, sizeof(tmp),
6295 ngettext("%d person in room", "%d people in room",
6296 num_users), num_users);
6298 gtk_label_set_text(GTK_LABEL(gtkchat->count), tmp);
6301 static void
6302 pidgin_conv_chat_update_user(PurpleConversation *conv, const char *user)
6304 PurpleConvChat *chat;
6305 PurpleConvChatBuddy *cbuddy;
6306 PidginConversation *gtkconv;
6307 PidginChatPane *gtkchat;
6308 GtkTreeIter iter;
6309 GtkTreeModel *model;
6311 chat = PURPLE_CONV_CHAT(conv);
6312 gtkconv = PIDGIN_CONVERSATION(conv);
6313 gtkchat = gtkconv->u.chat;
6315 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
6317 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter))
6318 return;
6320 cbuddy = purple_conv_chat_cb_find(chat, user);
6321 if (!cbuddy)
6322 return;
6324 if (get_iter_from_chatbuddy(cbuddy, &iter)) {
6325 GtkTreeRowReference *ref = cbuddy->ui_data;
6326 gtk_list_store_remove(GTK_LIST_STORE(model), &iter);
6327 gtk_tree_row_reference_free(ref);
6328 cbuddy->ui_data = NULL;
6331 if (cbuddy)
6332 add_chat_buddy_common(conv, cbuddy, NULL);
6335 gboolean
6336 pidgin_conv_has_focus(PurpleConversation *conv)
6338 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
6339 PidginWindow *win;
6340 gboolean has_focus;
6342 win = gtkconv->win;
6344 g_object_get(G_OBJECT(win->window), "has-toplevel-focus", &has_focus, NULL);
6346 if (has_focus && pidgin_conv_window_is_active_conversation(conv))
6347 return TRUE;
6349 return FALSE;
6352 static gboolean
6353 add_custom_smiley_for_imhtml(GtkIMHtml *imhtml, const char *sml, const char *smile)
6355 GtkIMHtmlSmiley *smiley;
6357 smiley = gtk_imhtml_smiley_get(imhtml, sml, smile);
6359 if (smiley) {
6360 if (!(smiley->flags & GTK_IMHTML_SMILEY_CUSTOM)) {
6361 return FALSE;
6363 gtk_imhtml_smiley_reload(smiley);
6364 return TRUE;
6367 smiley = gtk_imhtml_smiley_create(NULL, smile, FALSE, GTK_IMHTML_SMILEY_CUSTOM);
6368 gtk_imhtml_associate_smiley(imhtml, sml, smiley);
6369 g_signal_connect_swapped(imhtml, "destroy", G_CALLBACK(gtk_imhtml_smiley_destroy), smiley);
6371 return TRUE;
6374 static gboolean
6375 pidgin_conv_custom_smiley_add(PurpleConversation *conv, const char *smile, gboolean remote)
6377 PidginConversation *gtkconv;
6378 struct smiley_list *list;
6379 const char *sml = NULL, *conv_sml;
6381 if (!conv || !smile || !*smile) {
6382 return FALSE;
6385 /* If smileys are off, return false */
6386 if (pidgin_themes_smileys_disabled())
6387 return FALSE;
6389 /* If possible add this smiley to the current theme.
6390 * The addition is only temporary: custom smilies aren't saved to disk. */
6391 conv_sml = purple_account_get_protocol_name(conv->account);
6392 gtkconv = PIDGIN_CONVERSATION(conv);
6394 for (list = (struct smiley_list *)current_smiley_theme->list; list; list = list->next) {
6395 if (!strcmp(list->sml, conv_sml)) {
6396 sml = list->sml;
6397 break;
6401 if (!add_custom_smiley_for_imhtml(GTK_IMHTML(gtkconv->imhtml), sml, smile))
6402 return FALSE;
6404 if (!remote) /* If it's a local custom smiley, then add it for the entry */
6405 if (!add_custom_smiley_for_imhtml(GTK_IMHTML(gtkconv->entry), sml, smile))
6406 return FALSE;
6408 return TRUE;
6411 static void
6412 pidgin_conv_custom_smiley_write(PurpleConversation *conv, const char *smile,
6413 const guchar *data, gsize size)
6415 PidginConversation *gtkconv;
6416 GtkIMHtmlSmiley *smiley;
6417 const char *sml;
6418 GError *error = NULL;
6420 sml = purple_account_get_protocol_name(conv->account);
6421 gtkconv = PIDGIN_CONVERSATION(conv);
6422 smiley = gtk_imhtml_smiley_get(GTK_IMHTML(gtkconv->imhtml), sml, smile);
6424 if (!smiley)
6425 return;
6427 smiley->data = g_realloc(smiley->data, smiley->datasize + size);
6428 g_memmove((guchar *)smiley->data + smiley->datasize, data, size);
6429 smiley->datasize += size;
6431 if (!smiley->loader)
6432 return;
6434 if (!gdk_pixbuf_loader_write(smiley->loader, data, size, &error) || error) {
6435 purple_debug_warning("gtkconv", "gdk_pixbuf_loader_write() "
6436 "failed with size=%zu: %s\n", size,
6437 error ? error->message : "(no error message)");
6438 if (error)
6439 g_error_free(error);
6440 /* We must stop using the GdkPixbufLoader because trying to load
6441 certain invalid GIFs with at least gdk-pixbuf 2.23.3 can return
6442 a GdkPixbuf that will cause some operations (like
6443 gdk_pixbuf_scale_simple()) to consume memory in an infinite loop.
6444 But we also don't want to set smiley->loader to NULL because our
6445 code might expect it to be set. So create a new loader. */
6446 g_object_unref(G_OBJECT(smiley->loader));
6447 smiley->loader = gdk_pixbuf_loader_new();
6451 static void
6452 pidgin_conv_custom_smiley_close(PurpleConversation *conv, const char *smile)
6454 PidginConversation *gtkconv;
6455 GtkIMHtmlSmiley *smiley;
6456 const char *sml;
6457 GError *error = NULL;
6459 g_return_if_fail(conv != NULL);
6460 g_return_if_fail(smile != NULL);
6462 sml = purple_account_get_protocol_name(conv->account);
6463 gtkconv = PIDGIN_CONVERSATION(conv);
6464 smiley = gtk_imhtml_smiley_get(GTK_IMHTML(gtkconv->imhtml), sml, smile);
6466 if (!smiley)
6467 return;
6469 if (!smiley->loader)
6470 return;
6472 purple_debug_info("gtkconv", "About to close the smiley pixbuf\n");
6474 if (!gdk_pixbuf_loader_close(smiley->loader, &error) || error) {
6475 purple_debug_warning("gtkconv", "gdk_pixbuf_loader_close() "
6476 "failed: %s\n",
6477 error ? error->message : "(no error message)");
6478 if (error)
6479 g_error_free(error);
6480 /* We must stop using the GdkPixbufLoader because if we tried to
6481 load certain invalid GIFs with all current versions of GDK (as
6482 of 2011-06-15) then it's possible the loader will contain data
6483 that could cause some operations (like gdk_pixbuf_scale_simple())
6484 to consume memory in an infinite loop. But we also don't want
6485 to set smiley->loader to NULL because our code might expect it
6486 to be set. So create a new loader. */
6487 g_object_unref(G_OBJECT(smiley->loader));
6488 smiley->loader = gdk_pixbuf_loader_new();
6492 static void
6493 pidgin_conv_send_confirm(PurpleConversation *conv, const char *message)
6495 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
6497 gtk_imhtml_append_text(GTK_IMHTML(gtkconv->entry), message, 0);
6501 * Makes sure all the menu items and all the buttons are hidden/shown and
6502 * sensitive/insensitive. This is called after changing tabs and when an
6503 * account signs on or off.
6505 static void
6506 gray_stuff_out(PidginConversation *gtkconv)
6508 PidginWindow *win;
6509 PurpleConversation *conv = gtkconv->active_conv;
6510 PurpleConnection *gc;
6511 PurplePluginProtocolInfo *prpl_info = NULL;
6512 GdkPixbuf *window_icon = NULL;
6513 GtkIMHtmlButtons buttons;
6514 PurpleAccount *account;
6516 win = pidgin_conv_get_window(gtkconv);
6517 gc = purple_conversation_get_gc(conv);
6518 account = purple_conversation_get_account(conv);
6520 if (gc != NULL)
6521 prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(gc->prpl);
6523 if (win->menu.send_to != NULL)
6524 update_send_to_selection(win);
6527 * Handle hiding and showing stuff based on what type of conv this is.
6528 * Stuff that Purple IMs support in general should be shown for IM
6529 * conversations. Stuff that Purple chats support in general should be
6530 * shown for chat conversations. It doesn't matter whether the PRPL
6531 * supports it or not--that only affects if the button or menu item
6532 * is sensitive or not.
6534 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) {
6535 /* Show stuff that applies to IMs, hide stuff that applies to chats */
6537 /* Deal with menu items */
6538 gtk_widget_show(win->menu.view_log);
6539 gtk_widget_show(win->menu.send_file);
6540 gtk_widget_show(g_object_get_data(G_OBJECT(win->window), "get_attention"));
6541 gtk_widget_show(win->menu.add_pounce);
6542 gtk_widget_show(win->menu.get_info);
6543 gtk_widget_hide(win->menu.invite);
6544 gtk_widget_show(win->menu.alias);
6545 if (purple_privacy_check(account, purple_conversation_get_name(conv))) {
6546 gtk_widget_hide(win->menu.unblock);
6547 gtk_widget_show(win->menu.block);
6548 } else {
6549 gtk_widget_hide(win->menu.block);
6550 gtk_widget_show(win->menu.unblock);
6553 if ((account == NULL) || purple_find_buddy(account, purple_conversation_get_name(conv)) == NULL) {
6554 gtk_widget_show(win->menu.add);
6555 gtk_widget_hide(win->menu.remove);
6556 } else {
6557 gtk_widget_show(win->menu.remove);
6558 gtk_widget_hide(win->menu.add);
6561 gtk_widget_show(win->menu.insert_link);
6562 gtk_widget_show(win->menu.insert_image);
6563 } else if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) {
6564 /* Show stuff that applies to Chats, hide stuff that applies to IMs */
6566 /* Deal with menu items */
6567 gtk_widget_show(win->menu.view_log);
6568 gtk_widget_hide(win->menu.send_file);
6569 gtk_widget_hide(g_object_get_data(G_OBJECT(win->window), "get_attention"));
6570 gtk_widget_hide(win->menu.add_pounce);
6571 gtk_widget_hide(win->menu.get_info);
6572 gtk_widget_show(win->menu.invite);
6573 gtk_widget_show(win->menu.alias);
6574 gtk_widget_hide(win->menu.block);
6575 gtk_widget_hide(win->menu.unblock);
6577 if ((account == NULL) || purple_blist_find_chat(account, purple_conversation_get_name(conv)) == NULL) {
6578 /* If the chat is NOT in the buddy list */
6579 gtk_widget_show(win->menu.add);
6580 gtk_widget_hide(win->menu.remove);
6581 } else {
6582 /* If the chat IS in the buddy list */
6583 gtk_widget_hide(win->menu.add);
6584 gtk_widget_show(win->menu.remove);
6587 gtk_widget_show(win->menu.insert_link);
6588 gtk_widget_show(win->menu.insert_image);
6592 * Handle graying stuff out based on whether an account is connected
6593 * and what features that account supports.
6595 if ((gc != NULL) &&
6596 ((purple_conversation_get_type(conv) != PURPLE_CONV_TYPE_CHAT) ||
6597 !purple_conv_chat_has_left(PURPLE_CONV_CHAT(conv)) ))
6599 /* Account is online */
6600 /* Deal with the toolbar */
6601 if (conv->features & PURPLE_CONNECTION_HTML)
6603 buttons = GTK_IMHTML_ALL; /* Everything on */
6604 if (conv->features & PURPLE_CONNECTION_NO_BGCOLOR)
6605 buttons &= ~GTK_IMHTML_BACKCOLOR;
6606 if (conv->features & PURPLE_CONNECTION_NO_FONTSIZE)
6608 buttons &= ~GTK_IMHTML_GROW;
6609 buttons &= ~GTK_IMHTML_SHRINK;
6611 if (conv->features & PURPLE_CONNECTION_NO_URLDESC)
6612 buttons &= ~GTK_IMHTML_LINKDESC;
6613 } else {
6614 buttons = GTK_IMHTML_SMILEY | GTK_IMHTML_IMAGE;
6617 if (!(prpl_info->options & OPT_PROTO_IM_IMAGE))
6618 conv->features |= PURPLE_CONNECTION_NO_IMAGES;
6620 if(conv->features & PURPLE_CONNECTION_NO_IMAGES)
6621 buttons &= ~GTK_IMHTML_IMAGE;
6623 if (conv->features & PURPLE_CONNECTION_ALLOW_CUSTOM_SMILEY)
6624 buttons |= GTK_IMHTML_CUSTOM_SMILEY;
6625 else
6626 buttons &= ~GTK_IMHTML_CUSTOM_SMILEY;
6628 gtk_imhtml_set_format_functions(GTK_IMHTML(gtkconv->entry), buttons);
6629 if (account != NULL)
6630 gtk_imhtmltoolbar_associate_smileys(GTK_IMHTMLTOOLBAR(gtkconv->toolbar), purple_account_get_protocol_id(account));
6632 /* Deal with menu items */
6633 gtk_widget_set_sensitive(win->menu.view_log, TRUE);
6634 gtk_widget_set_sensitive(win->menu.add_pounce, TRUE);
6635 gtk_widget_set_sensitive(win->menu.get_info, (prpl_info->get_info != NULL));
6636 gtk_widget_set_sensitive(win->menu.invite, (prpl_info->chat_invite != NULL));
6637 gtk_widget_set_sensitive(win->menu.insert_link, (conv->features & PURPLE_CONNECTION_HTML));
6638 gtk_widget_set_sensitive(win->menu.insert_image, !(conv->features & PURPLE_CONNECTION_NO_IMAGES));
6640 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM)
6642 gtk_widget_set_sensitive(win->menu.add, (prpl_info->add_buddy != NULL) || (prpl_info->add_buddy_with_invite != NULL));
6643 gtk_widget_set_sensitive(win->menu.remove, (prpl_info->remove_buddy != NULL));
6644 gtk_widget_set_sensitive(win->menu.send_file,
6645 (prpl_info->send_file != NULL && (!prpl_info->can_receive_file ||
6646 prpl_info->can_receive_file(gc, purple_conversation_get_name(conv)))));
6647 gtk_widget_set_sensitive(g_object_get_data(G_OBJECT(win->window), "get_attention"), (prpl_info->send_attention != NULL));
6648 gtk_widget_set_sensitive(win->menu.alias,
6649 (account != NULL) &&
6650 (purple_find_buddy(account, purple_conversation_get_name(conv)) != NULL));
6652 else if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT)
6654 gtk_widget_set_sensitive(win->menu.add, (prpl_info->join_chat != NULL));
6655 gtk_widget_set_sensitive(win->menu.remove, (prpl_info->join_chat != NULL));
6656 gtk_widget_set_sensitive(win->menu.alias,
6657 (account != NULL) &&
6658 (purple_blist_find_chat(account, purple_conversation_get_name(conv)) != NULL));
6661 } else {
6662 /* Account is offline */
6663 /* Or it's a chat that we've left. */
6665 /* Then deal with menu items */
6666 gtk_widget_set_sensitive(win->menu.view_log, TRUE);
6667 gtk_widget_set_sensitive(win->menu.send_file, FALSE);
6668 gtk_widget_set_sensitive(g_object_get_data(G_OBJECT(win->window),
6669 "get_attention"), FALSE);
6670 gtk_widget_set_sensitive(win->menu.add_pounce, TRUE);
6671 gtk_widget_set_sensitive(win->menu.get_info, FALSE);
6672 gtk_widget_set_sensitive(win->menu.invite, FALSE);
6673 gtk_widget_set_sensitive(win->menu.alias, FALSE);
6674 gtk_widget_set_sensitive(win->menu.add, FALSE);
6675 gtk_widget_set_sensitive(win->menu.remove, FALSE);
6676 gtk_widget_set_sensitive(win->menu.insert_link, TRUE);
6677 gtk_widget_set_sensitive(win->menu.insert_image, FALSE);
6681 * Update the window's icon
6683 if (pidgin_conv_window_is_active_conversation(conv))
6685 GList *l = NULL;
6686 if ((purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) &&
6687 (gtkconv->u.im->anim))
6689 PurpleBuddy *buddy = purple_find_buddy(conv->account, conv->name);
6690 window_icon =
6691 gdk_pixbuf_animation_get_static_image(gtkconv->u.im->anim);
6693 if (buddy && !PURPLE_BUDDY_IS_ONLINE(buddy))
6694 gdk_pixbuf_saturate_and_pixelate(window_icon, window_icon, 0.0, FALSE);
6696 g_object_ref(window_icon);
6697 l = g_list_append(l, window_icon);
6698 } else {
6699 l = pidgin_conv_get_tab_icons(conv);
6701 gtk_window_set_icon_list(GTK_WINDOW(win->window), l);
6702 if (window_icon != NULL) {
6703 g_object_unref(G_OBJECT(window_icon));
6704 g_list_free(l);
6709 static void
6710 pidgin_conv_update_fields(PurpleConversation *conv, PidginConvFields fields)
6712 PidginConversation *gtkconv;
6713 PidginWindow *win;
6715 gtkconv = PIDGIN_CONVERSATION(conv);
6716 if (!gtkconv)
6717 return;
6718 win = pidgin_conv_get_window(gtkconv);
6719 if (!win)
6720 return;
6722 if (fields & PIDGIN_CONV_SET_TITLE)
6724 purple_conversation_autoset_title(conv);
6727 if (fields & PIDGIN_CONV_BUDDY_ICON)
6729 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM)
6730 pidgin_conv_update_buddy_icon(conv);
6733 if (fields & PIDGIN_CONV_MENU)
6735 gray_stuff_out(PIDGIN_CONVERSATION(conv));
6736 generate_send_to_items(win);
6739 if (fields & PIDGIN_CONV_TAB_ICON)
6741 update_tab_icon(conv);
6742 generate_send_to_items(win); /* To update the icons in SendTo menu */
6745 if ((fields & PIDGIN_CONV_TOPIC) &&
6746 purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT)
6748 const char *topic;
6749 PurpleConvChat *chat = PURPLE_CONV_CHAT(conv);
6750 PidginChatPane *gtkchat = gtkconv->u.chat;
6752 if (gtkchat->topic_text != NULL)
6754 topic = purple_conv_chat_get_topic(chat);
6756 gtk_entry_set_text(GTK_ENTRY(gtkchat->topic_text), topic ? topic : "");
6757 gtk_tooltips_set_tip(gtkconv->tooltips, gtkchat->topic_text,
6758 topic ? topic : "", NULL);
6762 if (fields & PIDGIN_CONV_SMILEY_THEME)
6763 pidgin_themes_smiley_themeize(PIDGIN_CONVERSATION(conv)->imhtml);
6765 if ((fields & PIDGIN_CONV_COLORIZE_TITLE) ||
6766 (fields & PIDGIN_CONV_SET_TITLE) ||
6767 (fields & PIDGIN_CONV_TOPIC))
6769 char *title;
6770 PurpleConvIm *im = NULL;
6771 PurpleAccount *account = purple_conversation_get_account(conv);
6772 PurpleBuddy *buddy = NULL;
6773 char *markup = NULL;
6774 AtkObject *accessibility_obj;
6775 /* I think this is a little longer than it needs to be but I'm lazy. */
6776 char *style;
6778 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM)
6779 im = PURPLE_CONV_IM(conv);
6781 if ((account == NULL) ||
6782 !purple_account_is_connected(account) ||
6783 ((purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT)
6784 && purple_conv_chat_has_left(PURPLE_CONV_CHAT(conv))))
6785 title = g_strdup_printf("(%s)", purple_conversation_get_title(conv));
6786 else
6787 title = g_strdup(purple_conversation_get_title(conv));
6789 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) {
6790 buddy = purple_find_buddy(account, conv->name);
6791 if (buddy) {
6792 markup = pidgin_blist_get_name_markup(buddy, FALSE, FALSE);
6793 } else {
6794 markup = title;
6796 } else if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) {
6797 const char *topic = gtkconv->u.chat->topic_text
6798 ? gtk_entry_get_text(GTK_ENTRY(gtkconv->u.chat->topic_text))
6799 : NULL;
6800 char *esc = NULL, *tmp;
6801 esc = topic ? g_markup_escape_text(topic, -1) : NULL;
6802 tmp = g_markup_escape_text(purple_conversation_get_title(conv), -1);
6803 markup = g_strdup_printf("%s%s<span color='%s' size='smaller'>%s</span>",
6804 tmp, esc && *esc ? "\n" : "",
6805 pidgin_get_dim_grey_string(gtkconv->infopane),
6806 esc ? esc : "");
6807 g_free(tmp);
6808 g_free(esc);
6810 gtk_list_store_set(gtkconv->infopane_model, &(gtkconv->infopane_iter),
6811 CONV_TEXT_COLUMN, markup, -1);
6812 /* XXX seanegan Why do I have to do this? */
6813 gtk_widget_queue_draw(gtkconv->infopane);
6815 if (title != markup)
6816 g_free(markup);
6818 if (!GTK_WIDGET_REALIZED(gtkconv->tab_label))
6819 gtk_widget_realize(gtkconv->tab_label);
6821 accessibility_obj = gtk_widget_get_accessible(gtkconv->tab_cont);
6822 if (im != NULL &&
6823 purple_conv_im_get_typing_state(im) == PURPLE_TYPING) {
6824 atk_object_set_description(accessibility_obj, _("Typing"));
6825 style = "tab-label-typing";
6826 } else if (im != NULL &&
6827 purple_conv_im_get_typing_state(im) == PURPLE_TYPED) {
6828 atk_object_set_description(accessibility_obj, _("Stopped Typing"));
6829 style = "tab-label-typed";
6830 } else if (gtkconv->unseen_state == PIDGIN_UNSEEN_NICK) {
6831 atk_object_set_description(accessibility_obj, _("Nick Said"));
6832 style = "tab-label-attention";
6833 } else if (gtkconv->unseen_state == PIDGIN_UNSEEN_TEXT) {
6834 atk_object_set_description(accessibility_obj, _("Unread Messages"));
6835 if (gtkconv->active_conv->type == PURPLE_CONV_TYPE_CHAT)
6836 style = "tab-label-unreadchat";
6837 else
6838 style = "tab-label-attention";
6839 } else if (gtkconv->unseen_state == PIDGIN_UNSEEN_EVENT) {
6840 atk_object_set_description(accessibility_obj, _("New Event"));
6841 style = "tab-label-event";
6842 } else {
6843 style = "tab-label";
6846 gtk_widget_set_name(gtkconv->tab_label, style);
6847 gtk_label_set_text(GTK_LABEL(gtkconv->tab_label), title);
6848 gtk_widget_set_state(gtkconv->tab_label, GTK_STATE_ACTIVE);
6850 if (gtkconv->unseen_state == PIDGIN_UNSEEN_TEXT ||
6851 gtkconv->unseen_state == PIDGIN_UNSEEN_NICK ||
6852 gtkconv->unseen_state == PIDGIN_UNSEEN_EVENT) {
6853 PangoAttrList *list = pango_attr_list_new();
6854 PangoAttribute *attr = pango_attr_weight_new(PANGO_WEIGHT_BOLD);
6855 attr->start_index = 0;
6856 attr->end_index = -1;
6857 pango_attr_list_insert(list, attr);
6858 gtk_label_set_attributes(GTK_LABEL(gtkconv->tab_label), list);
6859 pango_attr_list_unref(list);
6860 } else
6861 gtk_label_set_attributes(GTK_LABEL(gtkconv->tab_label), NULL);
6863 if (pidgin_conv_window_is_active_conversation(conv))
6864 update_typing_icon(gtkconv);
6866 gtk_label_set_text(GTK_LABEL(gtkconv->menu_label), title);
6867 if (pidgin_conv_window_is_active_conversation(conv)) {
6868 const char* current_title = gtk_window_get_title(GTK_WINDOW(win->window));
6869 if (current_title == NULL || strcmp(current_title, title) != 0)
6870 gtk_window_set_title(GTK_WINDOW(win->window), title);
6873 g_free(title);
6877 static void
6878 pidgin_conv_updated(PurpleConversation *conv, PurpleConvUpdateType type)
6880 PidginConvFields flags = 0;
6882 g_return_if_fail(conv != NULL);
6884 if (type == PURPLE_CONV_UPDATE_ACCOUNT)
6886 flags = PIDGIN_CONV_ALL;
6888 else if (type == PURPLE_CONV_UPDATE_TYPING ||
6889 type == PURPLE_CONV_UPDATE_UNSEEN ||
6890 type == PURPLE_CONV_UPDATE_TITLE)
6892 flags = PIDGIN_CONV_COLORIZE_TITLE;
6894 else if (type == PURPLE_CONV_UPDATE_TOPIC)
6896 flags = PIDGIN_CONV_TOPIC;
6898 else if (type == PURPLE_CONV_ACCOUNT_ONLINE ||
6899 type == PURPLE_CONV_ACCOUNT_OFFLINE)
6901 flags = PIDGIN_CONV_MENU | PIDGIN_CONV_TAB_ICON | PIDGIN_CONV_SET_TITLE;
6903 else if (type == PURPLE_CONV_UPDATE_AWAY)
6905 flags = PIDGIN_CONV_TAB_ICON;
6907 else if (type == PURPLE_CONV_UPDATE_ADD ||
6908 type == PURPLE_CONV_UPDATE_REMOVE ||
6909 type == PURPLE_CONV_UPDATE_CHATLEFT)
6911 flags = PIDGIN_CONV_SET_TITLE | PIDGIN_CONV_MENU;
6913 else if (type == PURPLE_CONV_UPDATE_ICON)
6915 flags = PIDGIN_CONV_BUDDY_ICON;
6917 else if (type == PURPLE_CONV_UPDATE_FEATURES)
6919 flags = PIDGIN_CONV_MENU;
6922 pidgin_conv_update_fields(conv, flags);
6925 static void
6926 wrote_msg_update_unseen_cb(PurpleAccount *account, const char *who, const char *message,
6927 PurpleConversation *conv, PurpleMessageFlags flags, gpointer null)
6929 PidginConversation *gtkconv = conv ? PIDGIN_CONVERSATION(conv) : NULL;
6930 if (conv == NULL || (gtkconv && gtkconv->win != hidden_convwin))
6931 return;
6932 if (flags & (PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_RECV)) {
6933 PidginUnseenState unseen = PIDGIN_UNSEEN_NONE;
6935 if ((flags & PURPLE_MESSAGE_NICK) == PURPLE_MESSAGE_NICK)
6936 unseen = PIDGIN_UNSEEN_NICK;
6937 else if (((flags & PURPLE_MESSAGE_SYSTEM) == PURPLE_MESSAGE_SYSTEM) ||
6938 ((flags & PURPLE_MESSAGE_ERROR) == PURPLE_MESSAGE_ERROR))
6939 unseen = PIDGIN_UNSEEN_EVENT;
6940 else if ((flags & PURPLE_MESSAGE_NO_LOG) == PURPLE_MESSAGE_NO_LOG)
6941 unseen = PIDGIN_UNSEEN_NO_LOG;
6942 else
6943 unseen = PIDGIN_UNSEEN_TEXT;
6945 conv_set_unseen(conv, unseen);
6949 static PurpleConversationUiOps conversation_ui_ops =
6951 pidgin_conv_new,
6952 pidgin_conv_destroy, /* destroy_conversation */
6953 NULL, /* write_chat */
6954 pidgin_conv_write_im, /* write_im */
6955 pidgin_conv_write_conv, /* write_conv */
6956 pidgin_conv_chat_add_users, /* chat_add_users */
6957 pidgin_conv_chat_rename_user, /* chat_rename_user */
6958 pidgin_conv_chat_remove_users, /* chat_remove_users */
6959 pidgin_conv_chat_update_user, /* chat_update_user */
6960 pidgin_conv_present_conversation, /* present */
6961 pidgin_conv_has_focus, /* has_focus */
6962 pidgin_conv_custom_smiley_add, /* custom_smiley_add */
6963 pidgin_conv_custom_smiley_write, /* custom_smiley_write */
6964 pidgin_conv_custom_smiley_close, /* custom_smiley_close */
6965 pidgin_conv_send_confirm, /* send_confirm */
6966 NULL,
6967 NULL,
6968 NULL,
6969 NULL
6972 PurpleConversationUiOps *
6973 pidgin_conversations_get_conv_ui_ops(void)
6975 return &conversation_ui_ops;
6978 /**************************************************************************
6979 * Public conversation utility functions
6980 **************************************************************************/
6981 void
6982 pidgin_conv_update_buddy_icon(PurpleConversation *conv)
6984 PidginConversation *gtkconv;
6985 PidginWindow *win;
6987 PurpleBuddy *buddy;
6989 PurpleStoredImage *custom_img = NULL;
6990 gconstpointer data = NULL;
6991 size_t len;
6993 GdkPixbuf *buf;
6995 GList *children;
6996 GtkWidget *event;
6997 GdkPixbuf *scale;
6998 int scale_width, scale_height;
6999 int size = 0;
7001 PurpleAccount *account;
7003 PurpleBuddyIcon *icon;
7005 g_return_if_fail(conv != NULL);
7006 g_return_if_fail(PIDGIN_IS_PIDGIN_CONVERSATION(conv));
7007 g_return_if_fail(purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM);
7009 gtkconv = PIDGIN_CONVERSATION(conv);
7010 win = gtkconv->win;
7011 if (conv != gtkconv->active_conv)
7012 return;
7014 if (!gtkconv->u.im->show_icon)
7015 return;
7017 account = purple_conversation_get_account(conv);
7019 /* Remove the current icon stuff */
7020 children = gtk_container_get_children(GTK_CONTAINER(gtkconv->u.im->icon_container));
7021 if (children) {
7022 /* We know there's only one child here. It'd be nice to shortcut to the
7023 event box, but we can't change the PidginConversation until 3.0 */
7024 event = (GtkWidget *)children->data;
7025 gtk_container_remove(GTK_CONTAINER(gtkconv->u.im->icon_container), event);
7026 g_list_free(children);
7029 if (gtkconv->u.im->anim != NULL)
7030 g_object_unref(G_OBJECT(gtkconv->u.im->anim));
7032 gtkconv->u.im->anim = NULL;
7034 if (gtkconv->u.im->icon_timer != 0)
7035 g_source_remove(gtkconv->u.im->icon_timer);
7037 gtkconv->u.im->icon_timer = 0;
7039 if (gtkconv->u.im->iter != NULL)
7040 g_object_unref(G_OBJECT(gtkconv->u.im->iter));
7042 gtkconv->u.im->iter = NULL;
7044 if (!purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons"))
7045 return;
7047 if (purple_conversation_get_gc(conv) == NULL)
7048 return;
7050 buddy = purple_find_buddy(account, purple_conversation_get_name(conv));
7051 if (buddy)
7053 PurpleContact *contact = purple_buddy_get_contact(buddy);
7054 if (contact) {
7055 custom_img = purple_buddy_icons_node_find_custom_icon((PurpleBlistNode*)contact);
7056 if (custom_img) {
7057 /* There is a custom icon for this user */
7058 data = purple_imgstore_get_data(custom_img);
7059 len = purple_imgstore_get_size(custom_img);
7064 if (data == NULL) {
7065 icon = purple_conv_im_get_icon(PURPLE_CONV_IM(conv));
7066 if (icon == NULL)
7068 gtk_widget_set_size_request(gtkconv->u.im->icon_container,
7069 -1, BUDDYICON_SIZE_MIN);
7070 return;
7073 data = purple_buddy_icon_get_data(icon, &len);
7074 if (data == NULL)
7076 gtk_widget_set_size_request(gtkconv->u.im->icon_container,
7077 -1, BUDDYICON_SIZE_MIN);
7078 return;
7082 gtkconv->u.im->anim = pidgin_pixbuf_anim_from_data(data, len);
7083 purple_imgstore_unref(custom_img);
7085 if (!gtkconv->u.im->anim) {
7086 purple_debug_error("gtkconv", "Couldn't load icon for conv %s\n",
7087 purple_conversation_get_name(conv));
7088 return;
7091 if (gdk_pixbuf_animation_is_static_image(gtkconv->u.im->anim)) {
7092 GdkPixbuf *stat;
7093 gtkconv->u.im->iter = NULL;
7094 stat = gdk_pixbuf_animation_get_static_image(gtkconv->u.im->anim);
7095 buf = gdk_pixbuf_add_alpha(stat, FALSE, 0, 0, 0);
7096 } else {
7097 GdkPixbuf *stat;
7098 gtkconv->u.im->iter =
7099 gdk_pixbuf_animation_get_iter(gtkconv->u.im->anim, NULL); /* LEAK */
7100 stat = gdk_pixbuf_animation_iter_get_pixbuf(gtkconv->u.im->iter);
7101 buf = gdk_pixbuf_add_alpha(stat, FALSE, 0, 0, 0);
7102 if (gtkconv->u.im->animate)
7103 start_anim(NULL, gtkconv);
7106 scale_width = gdk_pixbuf_get_width(buf);
7107 scale_height = gdk_pixbuf_get_height(buf);
7109 gtk_widget_get_size_request(gtkconv->u.im->icon_container, NULL, &size);
7110 size = MIN(size, MIN(scale_width, scale_height));
7112 /* Some sanity checks */
7113 size = CLAMP(size, BUDDYICON_SIZE_MIN, BUDDYICON_SIZE_MAX);
7114 if (scale_width == scale_height) {
7115 scale_width = scale_height = size;
7116 } else if (scale_height > scale_width) {
7117 scale_width = size * scale_width / scale_height;
7118 scale_height = size;
7119 } else {
7120 scale_height = size * scale_height / scale_width;
7121 scale_width = size;
7123 scale = gdk_pixbuf_scale_simple(buf, scale_width, scale_height,
7124 GDK_INTERP_BILINEAR);
7125 g_object_unref(buf);
7126 if (pidgin_gdk_pixbuf_is_opaque(scale))
7127 pidgin_gdk_pixbuf_make_round(scale);
7129 event = gtk_event_box_new();
7130 gtk_container_add(GTK_CONTAINER(gtkconv->u.im->icon_container), event);
7131 gtk_event_box_set_visible_window(GTK_EVENT_BOX(event), FALSE);
7132 gtk_widget_add_events(event,
7133 GDK_POINTER_MOTION_MASK | GDK_LEAVE_NOTIFY_MASK);
7134 g_signal_connect(G_OBJECT(event), "button-press-event",
7135 G_CALLBACK(icon_menu), gtkconv);
7137 pidgin_tooltip_setup_for_widget(event, gtkconv, pidgin_conv_create_tooltip, NULL);
7138 gtk_widget_show(event);
7140 gtkconv->u.im->icon = gtk_image_new_from_pixbuf(scale);
7141 gtk_container_add(GTK_CONTAINER(event), gtkconv->u.im->icon);
7142 gtk_widget_show(gtkconv->u.im->icon);
7144 g_object_unref(G_OBJECT(scale));
7146 /* The buddy icon code needs badly to be fixed. */
7147 if(pidgin_conv_window_is_active_conversation(conv))
7149 buf = gdk_pixbuf_animation_get_static_image(gtkconv->u.im->anim);
7150 if (buddy && !PURPLE_BUDDY_IS_ONLINE(buddy))
7151 gdk_pixbuf_saturate_and_pixelate(buf, buf, 0.0, FALSE);
7152 gtk_window_set_icon(GTK_WINDOW(win->window), buf);
7156 void
7157 pidgin_conv_update_buttons_by_protocol(PurpleConversation *conv)
7159 PidginWindow *win;
7161 if (!PIDGIN_IS_PIDGIN_CONVERSATION(conv))
7162 return;
7164 win = PIDGIN_CONVERSATION(conv)->win;
7166 if (win != NULL && pidgin_conv_window_is_active_conversation(conv))
7167 gray_stuff_out(PIDGIN_CONVERSATION(conv));
7170 static gboolean
7171 pidgin_conv_xy_to_right_infopane(PidginWindow *win, int x, int y)
7173 gint pane_x, pane_y, x_rel;
7174 PidginConversation *gtkconv;
7176 gdk_window_get_origin(win->notebook->window, &pane_x, &pane_y);
7177 x_rel = x - pane_x;
7178 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
7179 return (x_rel > gtkconv->infopane->allocation.x + gtkconv->infopane->allocation.width / 2);
7183 pidgin_conv_get_tab_at_xy(PidginWindow *win, int x, int y, gboolean *to_right)
7185 gint nb_x, nb_y, x_rel, y_rel;
7186 GtkNotebook *notebook;
7187 GtkWidget *page, *tab;
7188 gint i, page_num = -1;
7189 gint count;
7190 gboolean horiz;
7192 if (to_right)
7193 *to_right = FALSE;
7195 notebook = GTK_NOTEBOOK(win->notebook);
7197 gdk_window_get_origin(win->notebook->window, &nb_x, &nb_y);
7198 x_rel = x - nb_x;
7199 y_rel = y - nb_y;
7201 horiz = (gtk_notebook_get_tab_pos(notebook) == GTK_POS_TOP ||
7202 gtk_notebook_get_tab_pos(notebook) == GTK_POS_BOTTOM);
7204 count = gtk_notebook_get_n_pages(GTK_NOTEBOOK(notebook));
7206 for (i = 0; i < count; i++) {
7208 page = gtk_notebook_get_nth_page(GTK_NOTEBOOK(notebook), i);
7209 tab = gtk_notebook_get_tab_label(GTK_NOTEBOOK(notebook), page);
7211 /* Make sure the tab is not hidden beyond an arrow */
7212 if (!GTK_WIDGET_DRAWABLE(tab) && gtk_notebook_get_show_tabs(notebook))
7213 continue;
7215 if (horiz) {
7216 if (x_rel >= tab->allocation.x - PIDGIN_HIG_BOX_SPACE &&
7217 x_rel <= tab->allocation.x + tab->allocation.width + PIDGIN_HIG_BOX_SPACE) {
7218 page_num = i;
7220 if (to_right && x_rel >= tab->allocation.x + tab->allocation.width/2)
7221 *to_right = TRUE;
7223 break;
7225 } else {
7226 if (y_rel >= tab->allocation.y - PIDGIN_HIG_BOX_SPACE &&
7227 y_rel <= tab->allocation.y + tab->allocation.height + PIDGIN_HIG_BOX_SPACE) {
7228 page_num = i;
7230 if (to_right && y_rel >= tab->allocation.y + tab->allocation.height/2)
7231 *to_right = TRUE;
7233 break;
7238 if (page_num == -1) {
7239 /* Add after the last tab */
7240 page_num = count - 1;
7243 return page_num;
7246 static void
7247 close_on_tabs_pref_cb(const char *name, PurplePrefType type,
7248 gconstpointer value, gpointer data)
7250 GList *l;
7251 PurpleConversation *conv;
7252 PidginConversation *gtkconv;
7254 for (l = purple_get_conversations(); l != NULL; l = l->next) {
7255 conv = (PurpleConversation *)l->data;
7257 if (!PIDGIN_IS_PIDGIN_CONVERSATION(conv))
7258 continue;
7260 gtkconv = PIDGIN_CONVERSATION(conv);
7262 if (value)
7263 gtk_widget_show(gtkconv->close);
7264 else
7265 gtk_widget_hide(gtkconv->close);
7269 static void
7270 spellcheck_pref_cb(const char *name, PurplePrefType type,
7271 gconstpointer value, gpointer data)
7273 #ifdef USE_GTKSPELL
7274 GList *cl;
7275 PurpleConversation *conv;
7276 PidginConversation *gtkconv;
7277 GtkSpell *spell;
7279 for (cl = purple_get_conversations(); cl != NULL; cl = cl->next) {
7281 conv = (PurpleConversation *)cl->data;
7283 if (!PIDGIN_IS_PIDGIN_CONVERSATION(conv))
7284 continue;
7286 gtkconv = PIDGIN_CONVERSATION(conv);
7288 if (value)
7289 pidgin_setup_gtkspell(GTK_TEXT_VIEW(gtkconv->entry));
7290 else {
7291 spell = gtkspell_get_from_text_view(GTK_TEXT_VIEW(gtkconv->entry));
7292 if (spell)
7293 gtkspell_detach(spell);
7296 #endif
7299 static void
7300 tab_side_pref_cb(const char *name, PurplePrefType type,
7301 gconstpointer value, gpointer data)
7303 GList *gtkwins, *gtkconvs;
7304 GtkPositionType pos;
7305 PidginWindow *gtkwin;
7307 pos = GPOINTER_TO_INT(value);
7309 for (gtkwins = pidgin_conv_windows_get_list(); gtkwins != NULL; gtkwins = gtkwins->next) {
7310 gtkwin = gtkwins->data;
7311 gtk_notebook_set_tab_pos(GTK_NOTEBOOK(gtkwin->notebook), pos&~8);
7312 for (gtkconvs = gtkwin->gtkconvs; gtkconvs != NULL; gtkconvs = gtkconvs->next) {
7313 pidgin_conv_tab_pack(gtkwin, gtkconvs->data);
7318 static void
7319 show_timestamps_pref_cb(const char *name, PurplePrefType type,
7320 gconstpointer value, gpointer data)
7322 GList *l;
7323 PurpleConversation *conv;
7324 PidginConversation *gtkconv;
7325 PidginWindow *win;
7327 for (l = purple_get_conversations(); l != NULL; l = l->next)
7329 conv = (PurpleConversation *)l->data;
7331 if (!PIDGIN_IS_PIDGIN_CONVERSATION(conv))
7332 continue;
7334 gtkconv = PIDGIN_CONVERSATION(conv);
7335 win = gtkconv->win;
7337 gtk_check_menu_item_set_active(
7338 GTK_CHECK_MENU_ITEM(win->menu.show_timestamps),
7339 (gboolean)GPOINTER_TO_INT(value));
7341 gtk_imhtml_show_comments(GTK_IMHTML(gtkconv->imhtml),
7342 (gboolean)GPOINTER_TO_INT(value));
7346 static void
7347 show_formatting_toolbar_pref_cb(const char *name, PurplePrefType type,
7348 gconstpointer value, gpointer data)
7350 GList *l;
7351 PurpleConversation *conv;
7352 PidginConversation *gtkconv;
7353 PidginWindow *win;
7355 for (l = purple_get_conversations(); l != NULL; l = l->next)
7357 conv = (PurpleConversation *)l->data;
7359 if (!PIDGIN_IS_PIDGIN_CONVERSATION(conv))
7360 continue;
7362 gtkconv = PIDGIN_CONVERSATION(conv);
7363 win = gtkconv->win;
7365 gtk_check_menu_item_set_active(
7366 GTK_CHECK_MENU_ITEM(win->menu.show_formatting_toolbar),
7367 (gboolean)GPOINTER_TO_INT(value));
7369 if ((gboolean)GPOINTER_TO_INT(value))
7370 gtk_widget_show(gtkconv->toolbar);
7371 else
7372 gtk_widget_hide(gtkconv->toolbar);
7374 g_idle_add((GSourceFunc)resize_imhtml_cb,gtkconv);
7378 static void
7379 animate_buddy_icons_pref_cb(const char *name, PurplePrefType type,
7380 gconstpointer value, gpointer data)
7382 GList *l;
7383 PurpleConversation *conv;
7384 PidginConversation *gtkconv;
7385 PidginWindow *win;
7387 if (!purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons"))
7388 return;
7390 /* Set the "animate" flag for each icon based on the new preference */
7391 for (l = purple_get_ims(); l != NULL; l = l->next) {
7392 conv = (PurpleConversation *)l->data;
7393 gtkconv = PIDGIN_CONVERSATION(conv);
7394 if (gtkconv)
7395 gtkconv->u.im->animate = GPOINTER_TO_INT(value);
7398 /* Now either stop or start animation for the active conversation in each window */
7399 for (l = pidgin_conv_windows_get_list(); l != NULL; l = l->next) {
7400 win = l->data;
7401 conv = pidgin_conv_window_get_active_conversation(win);
7402 pidgin_conv_update_buddy_icon(conv);
7406 static void
7407 show_buddy_icons_pref_cb(const char *name, PurplePrefType type,
7408 gconstpointer value, gpointer data)
7410 GList *l;
7412 for (l = purple_get_conversations(); l != NULL; l = l->next) {
7413 PurpleConversation *conv = l->data;
7414 if (!PIDGIN_CONVERSATION(conv))
7415 continue;
7416 if (GPOINTER_TO_INT(value))
7417 gtk_widget_show(PIDGIN_CONVERSATION(conv)->infopane_hbox);
7418 else
7419 gtk_widget_hide(PIDGIN_CONVERSATION(conv)->infopane_hbox);
7421 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) {
7422 pidgin_conv_update_buddy_icon(conv);
7426 /* Make the tabs show/hide correctly */
7427 for (l = pidgin_conv_windows_get_list(); l != NULL; l = l->next) {
7428 PidginWindow *win = l->data;
7429 if (pidgin_conv_window_get_gtkconv_count(win) == 1)
7430 gtk_notebook_set_show_tabs(GTK_NOTEBOOK(win->notebook),
7431 GPOINTER_TO_INT(value) == 0);
7435 static void
7436 show_protocol_icons_pref_cb(const char *name, PurplePrefType type,
7437 gconstpointer value, gpointer data)
7439 GList *l;
7440 for (l = purple_get_conversations(); l != NULL; l = l->next) {
7441 PurpleConversation *conv = l->data;
7442 if (PIDGIN_CONVERSATION(conv))
7443 update_tab_icon(conv);
7447 static void
7448 conv_placement_usetabs_cb(const char *name, PurplePrefType type,
7449 gconstpointer value, gpointer data)
7451 purple_prefs_trigger_callback(PIDGIN_PREFS_ROOT "/conversations/placement");
7454 static void
7455 account_status_changed_cb(PurpleAccount *account, PurpleStatus *oldstatus,
7456 PurpleStatus *newstatus)
7458 GList *l;
7459 PurpleConversation *conv = NULL;
7460 PidginConversation *gtkconv;
7462 if(strcmp(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new"), "away")!=0)
7463 return;
7465 if(purple_status_is_available(oldstatus) || !purple_status_is_available(newstatus))
7466 return;
7468 for (l = hidden_convwin->gtkconvs; l; ) {
7469 gtkconv = l->data;
7470 l = l->next;
7472 conv = gtkconv->active_conv;
7473 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT ||
7474 account != purple_conversation_get_account(conv))
7475 continue;
7477 pidgin_conv_attach_to_conversation(conv);
7479 /* TODO: do we need to do anything for any other conversations that are in the same gtkconv here?
7480 * I'm a little concerned that not doing so will cause the "pending" indicator in the gtkblist not to be cleared. -DAA*/
7481 purple_conversation_update(conv, PURPLE_CONV_UPDATE_UNSEEN);
7485 static void
7486 hide_new_pref_cb(const char *name, PurplePrefType type,
7487 gconstpointer value, gpointer data)
7489 GList *l;
7490 PurpleConversation *conv = NULL;
7491 PidginConversation *gtkconv;
7492 gboolean when_away = FALSE;
7494 if(!hidden_convwin)
7495 return;
7497 if(strcmp(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new"), "always")==0)
7498 return;
7500 if(strcmp(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new"), "away")==0)
7501 when_away = TRUE;
7503 for (l = hidden_convwin->gtkconvs; l; )
7505 gtkconv = l->data;
7506 l = l->next;
7508 conv = gtkconv->active_conv;
7510 if (conv->type == PURPLE_CONV_TYPE_CHAT ||
7511 gtkconv->unseen_count == 0 ||
7512 (when_away && !purple_status_is_available(
7513 purple_account_get_active_status(
7514 purple_conversation_get_account(conv)))))
7515 continue;
7517 pidgin_conv_attach_to_conversation(conv);
7522 static void
7523 conv_placement_pref_cb(const char *name, PurplePrefType type,
7524 gconstpointer value, gpointer data)
7526 PidginConvPlacementFunc func;
7528 if (strcmp(name, PIDGIN_PREFS_ROOT "/conversations/placement"))
7529 return;
7531 func = pidgin_conv_placement_get_fnc(value);
7533 if (func == NULL)
7534 return;
7536 pidgin_conv_placement_set_current_func(func);
7539 static PidginConversation *
7540 get_gtkconv_with_contact(PurpleContact *contact)
7542 PurpleBlistNode *node;
7544 node = ((PurpleBlistNode*)contact)->child;
7546 for (; node; node = node->next)
7548 PurpleBuddy *buddy = (PurpleBuddy*)node;
7549 PurpleConversation *conv;
7550 conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, buddy->name, buddy->account);
7551 if (conv)
7552 return PIDGIN_CONVERSATION(conv);
7554 return NULL;
7557 static void
7558 account_signed_off_cb(PurpleConnection *gc, gpointer event)
7560 GList *iter;
7562 for (iter = purple_get_conversations(); iter; iter = iter->next)
7564 PurpleConversation *conv = iter->data;
7566 /* This seems fine in theory, but we also need to cover the
7567 * case of this account matching one of the other buddies in
7568 * one of the contacts containing the buddy corresponding to
7569 * a conversation. It's easier to just update them all. */
7570 /* if (purple_conversation_get_account(conv) == account) */
7571 pidgin_conv_update_fields(conv, PIDGIN_CONV_TAB_ICON |
7572 PIDGIN_CONV_MENU | PIDGIN_CONV_COLORIZE_TITLE);
7574 if (PURPLE_CONNECTION_IS_CONNECTED(gc) &&
7575 conv->type == PURPLE_CONV_TYPE_CHAT &&
7576 conv->account == gc->account &&
7577 purple_conversation_get_data(conv, "want-to-rejoin")) {
7578 GHashTable *comps = NULL;
7579 PurpleChat *chat = purple_blist_find_chat(conv->account, conv->name);
7580 if (chat == NULL) {
7581 if (PURPLE_PLUGIN_PROTOCOL_INFO(gc->prpl)->chat_info_defaults != NULL)
7582 comps = PURPLE_PLUGIN_PROTOCOL_INFO(gc->prpl)->chat_info_defaults(gc, conv->name);
7583 } else {
7584 comps = chat->components;
7586 serv_join_chat(gc, comps);
7587 if (chat == NULL && comps != NULL)
7588 g_hash_table_destroy(comps);
7593 static void
7594 account_signing_off(PurpleConnection *gc)
7596 GList *list = purple_get_chats();
7597 PurpleAccount *account = purple_connection_get_account(gc);
7599 /* We are about to sign off. See which chats we are currently in, and mark
7600 * them for rejoin on reconnect. */
7601 while (list) {
7602 PurpleConversation *conv = list->data;
7603 if (!purple_conv_chat_has_left(PURPLE_CONV_CHAT(conv)) &&
7604 purple_conversation_get_account(conv) == account) {
7605 purple_conversation_set_data(conv, "want-to-rejoin", GINT_TO_POINTER(TRUE));
7606 purple_conversation_write(conv, NULL, _("The account has disconnected and you are no "
7607 "longer in this chat. You will automatically rejoin the chat when "
7608 "the account reconnects."),
7609 PURPLE_MESSAGE_SYSTEM, time(NULL));
7611 list = list->next;
7615 static void
7616 update_buddy_status_changed(PurpleBuddy *buddy, PurpleStatus *old, PurpleStatus *newstatus)
7618 PidginConversation *gtkconv;
7619 PurpleConversation *conv;
7621 gtkconv = get_gtkconv_with_contact(purple_buddy_get_contact(buddy));
7622 if (gtkconv)
7624 conv = gtkconv->active_conv;
7625 pidgin_conv_update_fields(conv, PIDGIN_CONV_TAB_ICON
7626 | PIDGIN_CONV_COLORIZE_TITLE
7627 | PIDGIN_CONV_BUDDY_ICON);
7628 if ((purple_status_is_online(old) ^ purple_status_is_online(newstatus)) != 0)
7629 pidgin_conv_update_fields(conv, PIDGIN_CONV_MENU);
7633 static void
7634 update_buddy_privacy_changed(PurpleBuddy *buddy)
7636 PidginConversation *gtkconv;
7637 PurpleConversation *conv;
7639 gtkconv = get_gtkconv_with_contact(purple_buddy_get_contact(buddy));
7640 if (gtkconv) {
7641 conv = gtkconv->active_conv;
7642 pidgin_conv_update_fields(conv, PIDGIN_CONV_TAB_ICON | PIDGIN_CONV_MENU);
7646 static void
7647 update_buddy_idle_changed(PurpleBuddy *buddy, gboolean old, gboolean newidle)
7649 PurpleConversation *conv;
7651 conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, buddy->name, buddy->account);
7652 if (conv)
7653 pidgin_conv_update_fields(conv, PIDGIN_CONV_TAB_ICON);
7656 static void
7657 update_buddy_icon(PurpleBuddy *buddy)
7659 PurpleConversation *conv;
7661 conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, buddy->name, buddy->account);
7662 if (conv)
7663 pidgin_conv_update_fields(conv, PIDGIN_CONV_BUDDY_ICON);
7666 static void
7667 update_buddy_sign(PurpleBuddy *buddy, const char *which)
7669 PurplePresence *presence;
7670 PurpleStatus *on, *off;
7672 presence = purple_buddy_get_presence(buddy);
7673 if (!presence)
7674 return;
7675 off = purple_presence_get_status(presence, "offline");
7676 on = purple_presence_get_status(presence, "available");
7678 if (*(which+1) == 'f')
7679 update_buddy_status_changed(buddy, on, off);
7680 else
7681 update_buddy_status_changed(buddy, off, on);
7684 static void
7685 update_conversation_switched(PurpleConversation *conv)
7687 pidgin_conv_update_fields(conv, PIDGIN_CONV_TAB_ICON | PIDGIN_CONV_SET_TITLE |
7688 PIDGIN_CONV_MENU | PIDGIN_CONV_BUDDY_ICON);
7691 static void
7692 update_buddy_typing(PurpleAccount *account, const char *who)
7694 PurpleConversation *conv;
7695 PidginConversation *gtkconv;
7697 conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, who, account);
7698 if (!conv)
7699 return;
7701 gtkconv = PIDGIN_CONVERSATION(conv);
7702 if (gtkconv && gtkconv->active_conv == conv)
7703 pidgin_conv_update_fields(conv, PIDGIN_CONV_COLORIZE_TITLE);
7706 static void
7707 update_chat(PurpleConversation *conv)
7709 pidgin_conv_update_fields(conv, PIDGIN_CONV_TOPIC |
7710 PIDGIN_CONV_MENU | PIDGIN_CONV_SET_TITLE);
7713 static void
7714 update_chat_topic(PurpleConversation *conv, const char *old, const char *new)
7716 pidgin_conv_update_fields(conv, PIDGIN_CONV_TOPIC);
7719 /* Message history stuff */
7721 /* Compare two PurpleConvMessage's, according to time in ascending order. */
7722 static int
7723 message_compare(gconstpointer p1, gconstpointer p2)
7725 const PurpleConvMessage *m1 = p1, *m2 = p2;
7726 return (m1->when > m2->when);
7729 /* Adds some message history to the gtkconv. This happens in a idle-callback. */
7730 static gboolean
7731 add_message_history_to_gtkconv(gpointer data)
7733 PidginConversation *gtkconv = data;
7734 int count = 0;
7735 int timer = gtkconv->attach.timer;
7736 time_t when = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(gtkconv->entry), "attach-start-time"));
7737 gboolean im = (gtkconv->active_conv->type == PURPLE_CONV_TYPE_IM);
7739 gtkconv->attach.timer = 0;
7740 while (gtkconv->attach.current && count < 100) { /* XXX: 100 is a random value here */
7741 PurpleConvMessage *msg = gtkconv->attach.current->data;
7742 if (!im && when && when < msg->when) {
7743 gtk_imhtml_append_text(GTK_IMHTML(gtkconv->imhtml), "<BR><HR>", 0);
7744 g_object_set_data(G_OBJECT(gtkconv->entry), "attach-start-time", NULL);
7746 pidgin_conv_write_conv(msg->conv, msg->who, msg->alias, msg->what, msg->flags, msg->when);
7747 if (im) {
7748 gtkconv->attach.current = g_list_delete_link(gtkconv->attach.current, gtkconv->attach.current);
7749 } else {
7750 gtkconv->attach.current = gtkconv->attach.current->prev;
7752 count++;
7754 gtkconv->attach.timer = timer;
7755 if (gtkconv->attach.current)
7756 return TRUE;
7758 g_source_remove(gtkconv->attach.timer);
7759 gtkconv->attach.timer = 0;
7760 if (im) {
7761 /* Print any message that was sent while the old history was being added back. */
7762 GList *msgs = NULL;
7763 GList *iter = gtkconv->convs;
7764 for (; iter; iter = iter->next) {
7765 PurpleConversation *conv = iter->data;
7766 GList *history = purple_conversation_get_message_history(conv);
7767 for (; history; history = history->next) {
7768 PurpleConvMessage *msg = history->data;
7769 if (msg->when > when)
7770 msgs = g_list_prepend(msgs, msg);
7773 msgs = g_list_sort(msgs, message_compare);
7774 for (; msgs; msgs = g_list_delete_link(msgs, msgs)) {
7775 PurpleConvMessage *msg = msgs->data;
7776 pidgin_conv_write_conv(msg->conv, msg->who, msg->alias, msg->what, msg->flags, msg->when);
7778 gtk_imhtml_append_text(GTK_IMHTML(gtkconv->imhtml), "<BR><HR>", 0);
7779 g_object_set_data(G_OBJECT(gtkconv->entry), "attach-start-time", NULL);
7782 g_object_set_data(G_OBJECT(gtkconv->entry), "attach-start-time", NULL);
7783 purple_signal_emit(pidgin_conversations_get_handle(),
7784 "conversation-displayed", gtkconv);
7785 return FALSE;
7788 static void
7789 pidgin_conv_attach(PurpleConversation *conv)
7791 int timer;
7792 purple_conversation_set_data(conv, "unseen-count", NULL);
7793 purple_conversation_set_data(conv, "unseen-state", NULL);
7794 purple_conversation_set_ui_ops(conv, pidgin_conversations_get_conv_ui_ops());
7795 if (!PIDGIN_CONVERSATION(conv))
7796 private_gtkconv_new(conv, FALSE);
7797 timer = GPOINTER_TO_INT(purple_conversation_get_data(conv, "close-timer"));
7798 if (timer) {
7799 purple_timeout_remove(timer);
7800 purple_conversation_set_data(conv, "close-timer", NULL);
7804 gboolean pidgin_conv_attach_to_conversation(PurpleConversation *conv)
7806 GList *list;
7807 PidginConversation *gtkconv;
7809 if (PIDGIN_IS_PIDGIN_CONVERSATION(conv)) {
7810 /* This is pretty much always the case now. */
7811 gtkconv = PIDGIN_CONVERSATION(conv);
7812 if (gtkconv->win != hidden_convwin)
7813 return FALSE;
7814 pidgin_conv_window_remove_gtkconv(hidden_convwin, gtkconv);
7815 pidgin_conv_placement_place(gtkconv);
7816 purple_signal_emit(pidgin_conversations_get_handle(),
7817 "conversation-displayed", gtkconv);
7818 list = gtkconv->convs;
7819 while (list) {
7820 pidgin_conv_attach(list->data);
7821 list = list->next;
7823 return TRUE;
7826 pidgin_conv_attach(conv);
7827 gtkconv = PIDGIN_CONVERSATION(conv);
7829 list = purple_conversation_get_message_history(conv);
7830 if (list) {
7831 switch (purple_conversation_get_type(conv)) {
7832 case PURPLE_CONV_TYPE_IM:
7834 GList *convs;
7835 list = g_list_copy(list);
7836 for (convs = purple_get_ims(); convs; convs = convs->next)
7837 if (convs->data != conv &&
7838 pidgin_conv_find_gtkconv(convs->data) == gtkconv) {
7839 pidgin_conv_attach(convs->data);
7840 list = g_list_concat(list, g_list_copy(purple_conversation_get_message_history(convs->data)));
7842 list = g_list_sort(list, message_compare);
7843 gtkconv->attach.current = list;
7844 list = g_list_last(list);
7845 break;
7847 case PURPLE_CONV_TYPE_CHAT:
7848 gtkconv->attach.current = g_list_last(list);
7849 break;
7850 default:
7851 g_return_val_if_reached(TRUE);
7853 g_object_set_data(G_OBJECT(gtkconv->entry), "attach-start-time",
7854 GINT_TO_POINTER(((PurpleConvMessage*)(list->data))->when));
7855 gtkconv->attach.timer = g_idle_add(add_message_history_to_gtkconv, gtkconv);
7856 } else {
7857 purple_signal_emit(pidgin_conversations_get_handle(),
7858 "conversation-displayed", gtkconv);
7861 if (conv->type == PURPLE_CONV_TYPE_CHAT) {
7862 pidgin_conv_update_fields(conv, PIDGIN_CONV_TOPIC);
7863 pidgin_conv_chat_add_users(conv, PURPLE_CONV_CHAT(conv)->in_room, TRUE);
7866 return TRUE;
7869 void *
7870 pidgin_conversations_get_handle(void)
7872 static int handle;
7874 return &handle;
7877 void
7878 pidgin_conversations_init(void)
7880 void *handle = pidgin_conversations_get_handle();
7881 void *blist_handle = purple_blist_get_handle();
7883 /* Conversations */
7884 purple_prefs_add_none(PIDGIN_PREFS_ROOT "/conversations");
7885 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/use_smooth_scrolling", TRUE);
7886 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/close_on_tabs", TRUE);
7887 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/send_bold", FALSE);
7888 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/send_italic", FALSE);
7889 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/send_underline", FALSE);
7890 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/spellcheck", TRUE);
7891 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/show_incoming_formatting", TRUE);
7892 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/resize_custom_smileys", TRUE);
7893 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/custom_smileys_size", 96);
7894 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/minimum_entry_lines", 2);
7896 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/show_timestamps", TRUE);
7897 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/show_formatting_toolbar", TRUE);
7899 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/placement", "last");
7900 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/placement_number", 1);
7901 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/bgcolor", "");
7902 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/fgcolor", "");
7903 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/font_face", "");
7904 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/font_size", 3);
7905 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/tabs", TRUE);
7906 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/tab_side", GTK_POS_TOP);
7907 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/scrollback_lines", 4000);
7909 #ifdef _WIN32
7910 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/use_theme_font", TRUE);
7911 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/custom_font", "");
7912 #endif
7914 /* Conversations -> Chat */
7915 purple_prefs_add_none(PIDGIN_PREFS_ROOT "/conversations/chat");
7916 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/entry_height", 54);
7917 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/userlist_width", 80);
7918 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/x", 0);
7919 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/y", 0);
7920 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/width", 340);
7921 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/height", 390);
7923 /* Conversations -> IM */
7924 purple_prefs_add_none(PIDGIN_PREFS_ROOT "/conversations/im");
7925 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/im/x", 0);
7926 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/im/y", 0);
7927 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/im/width", 340);
7928 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/im/height", 390);
7930 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/im/animate_buddy_icons", TRUE);
7932 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/im/entry_height", 54);
7933 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons", TRUE);
7935 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new", "never");
7936 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/im/close_immediately", TRUE);
7938 #ifdef _WIN32
7939 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/win32/minimize_new_convs", FALSE);
7940 #endif
7942 /* Connect callbacks. */
7943 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/close_on_tabs",
7944 close_on_tabs_pref_cb, NULL);
7945 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/show_timestamps",
7946 show_timestamps_pref_cb, NULL);
7947 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/show_formatting_toolbar",
7948 show_formatting_toolbar_pref_cb, NULL);
7949 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/spellcheck",
7950 spellcheck_pref_cb, NULL);
7951 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/tab_side",
7952 tab_side_pref_cb, NULL);
7954 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/tabs",
7955 conv_placement_usetabs_cb, NULL);
7957 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/placement",
7958 conv_placement_pref_cb, NULL);
7959 purple_prefs_trigger_callback(PIDGIN_PREFS_ROOT "/conversations/placement");
7961 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/minimum_entry_lines",
7962 minimum_entry_lines_pref_cb, NULL);
7964 /* IM callbacks */
7965 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/im/animate_buddy_icons",
7966 animate_buddy_icons_pref_cb, NULL);
7967 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons",
7968 show_buddy_icons_pref_cb, NULL);
7969 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/blist/show_protocol_icons",
7970 show_protocol_icons_pref_cb, NULL);
7971 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/im/hide_new",
7972 hide_new_pref_cb, NULL);
7976 /**********************************************************************
7977 * Register signals
7978 **********************************************************************/
7979 purple_signal_register(handle, "conversation-dragging",
7980 purple_marshal_VOID__POINTER_POINTER, NULL, 2,
7981 purple_value_new(PURPLE_TYPE_BOXED,
7982 "PidginWindow *"),
7983 purple_value_new(PURPLE_TYPE_BOXED,
7984 "PidginWindow *"));
7986 purple_signal_register(handle, "conversation-timestamp",
7987 #if SIZEOF_TIME_T == 4
7988 purple_marshal_POINTER__POINTER_INT_BOOLEAN,
7989 #elif SIZEOF_TIME_T == 8
7990 purple_marshal_POINTER__POINTER_INT64_BOOLEAN,
7991 #else
7992 #error Unkown size of time_t
7993 #endif
7994 purple_value_new(PURPLE_TYPE_STRING), 3,
7995 purple_value_new(PURPLE_TYPE_SUBTYPE,
7996 PURPLE_SUBTYPE_CONVERSATION),
7997 #if SIZEOF_TIME_T == 4
7998 purple_value_new(PURPLE_TYPE_INT),
7999 #elif SIZEOF_TIME_T == 8
8000 purple_value_new(PURPLE_TYPE_INT64),
8001 #else
8002 # error Unknown size of time_t
8003 #endif
8004 purple_value_new(PURPLE_TYPE_BOOLEAN));
8006 purple_signal_register(handle, "displaying-im-msg",
8007 purple_marshal_BOOLEAN__POINTER_POINTER_POINTER_POINTER_POINTER,
8008 purple_value_new(PURPLE_TYPE_BOOLEAN), 5,
8009 purple_value_new(PURPLE_TYPE_SUBTYPE,
8010 PURPLE_SUBTYPE_ACCOUNT),
8011 purple_value_new(PURPLE_TYPE_STRING),
8012 purple_value_new_outgoing(PURPLE_TYPE_STRING),
8013 purple_value_new(PURPLE_TYPE_SUBTYPE,
8014 PURPLE_SUBTYPE_CONVERSATION),
8015 purple_value_new(PURPLE_TYPE_INT));
8017 purple_signal_register(handle, "displayed-im-msg",
8018 purple_marshal_VOID__POINTER_POINTER_POINTER_POINTER_UINT,
8019 NULL, 5,
8020 purple_value_new(PURPLE_TYPE_SUBTYPE,
8021 PURPLE_SUBTYPE_ACCOUNT),
8022 purple_value_new(PURPLE_TYPE_STRING),
8023 purple_value_new(PURPLE_TYPE_STRING),
8024 purple_value_new(PURPLE_TYPE_SUBTYPE,
8025 PURPLE_SUBTYPE_CONVERSATION),
8026 purple_value_new(PURPLE_TYPE_INT));
8028 purple_signal_register(handle, "displaying-chat-msg",
8029 purple_marshal_BOOLEAN__POINTER_POINTER_POINTER_POINTER_POINTER,
8030 purple_value_new(PURPLE_TYPE_BOOLEAN), 5,
8031 purple_value_new(PURPLE_TYPE_SUBTYPE,
8032 PURPLE_SUBTYPE_ACCOUNT),
8033 purple_value_new(PURPLE_TYPE_STRING),
8034 purple_value_new_outgoing(PURPLE_TYPE_STRING),
8035 purple_value_new(PURPLE_TYPE_SUBTYPE,
8036 PURPLE_SUBTYPE_CONVERSATION),
8037 purple_value_new(PURPLE_TYPE_INT));
8039 purple_signal_register(handle, "displayed-chat-msg",
8040 purple_marshal_VOID__POINTER_POINTER_POINTER_POINTER_UINT,
8041 NULL, 5,
8042 purple_value_new(PURPLE_TYPE_SUBTYPE,
8043 PURPLE_SUBTYPE_ACCOUNT),
8044 purple_value_new(PURPLE_TYPE_STRING),
8045 purple_value_new(PURPLE_TYPE_STRING),
8046 purple_value_new(PURPLE_TYPE_SUBTYPE,
8047 PURPLE_SUBTYPE_CONVERSATION),
8048 purple_value_new(PURPLE_TYPE_INT));
8050 purple_signal_register(handle, "conversation-switched",
8051 purple_marshal_VOID__POINTER, NULL, 1,
8052 purple_value_new(PURPLE_TYPE_SUBTYPE,
8053 PURPLE_SUBTYPE_CONVERSATION));
8055 purple_signal_register(handle, "conversation-hiding",
8056 purple_marshal_VOID__POINTER, NULL, 1,
8057 purple_value_new(PURPLE_TYPE_BOXED,
8058 "PidginConversation *"));
8060 purple_signal_register(handle, "conversation-displayed",
8061 purple_marshal_VOID__POINTER, NULL, 1,
8062 purple_value_new(PURPLE_TYPE_BOXED,
8063 "PidginConversation *"));
8065 purple_signal_register(handle, "chat-nick-autocomplete",
8066 purple_marshal_BOOLEAN__POINTER_BOOLEAN,
8067 purple_value_new(PURPLE_TYPE_BOOLEAN), 1,
8068 purple_value_new(PURPLE_TYPE_SUBTYPE,
8069 PURPLE_SUBTYPE_CONVERSATION));
8071 purple_signal_register(handle, "chat-nick-clicked",
8072 purple_marshal_BOOLEAN__POINTER_POINTER_UINT,
8073 purple_value_new(PURPLE_TYPE_BOOLEAN), 3,
8074 purple_value_new(PURPLE_TYPE_SUBTYPE,
8075 PURPLE_SUBTYPE_CONVERSATION),
8076 purple_value_new(PURPLE_TYPE_STRING),
8077 purple_value_new(PURPLE_TYPE_UINT));
8080 /**********************************************************************
8081 * Register commands
8082 **********************************************************************/
8083 purple_cmd_register("say", "S", PURPLE_CMD_P_DEFAULT,
8084 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM, NULL,
8085 say_command_cb, _("say &lt;message&gt;: Send a message normally as if you weren't using a command."), NULL);
8086 purple_cmd_register("me", "S", PURPLE_CMD_P_DEFAULT,
8087 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM, NULL,
8088 me_command_cb, _("me &lt;action&gt;: Send an IRC style action to a buddy or chat."), NULL);
8089 purple_cmd_register("debug", "w", PURPLE_CMD_P_DEFAULT,
8090 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM, NULL,
8091 debug_command_cb, _("debug &lt;option&gt;: Send various debug information to the current conversation."), NULL);
8092 purple_cmd_register("clear", "", PURPLE_CMD_P_DEFAULT,
8093 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM, NULL,
8094 clear_command_cb, _("clear: Clears the conversation scrollback."), NULL);
8095 purple_cmd_register("clearall", "", PURPLE_CMD_P_DEFAULT,
8096 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM, NULL,
8097 clearall_command_cb, _("clear: Clears all conversation scrollbacks."), NULL);
8098 purple_cmd_register("help", "w", PURPLE_CMD_P_DEFAULT,
8099 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM | PURPLE_CMD_FLAG_ALLOW_WRONG_ARGS, NULL,
8100 help_command_cb, _("help &lt;command&gt;: Help on a specific command."), NULL);
8102 /**********************************************************************
8103 * UI operations
8104 **********************************************************************/
8106 purple_signal_connect(purple_connections_get_handle(), "signed-on", handle,
8107 G_CALLBACK(account_signed_off_cb),
8108 GINT_TO_POINTER(PURPLE_CONV_ACCOUNT_ONLINE));
8109 purple_signal_connect(purple_connections_get_handle(), "signed-off", handle,
8110 G_CALLBACK(account_signed_off_cb),
8111 GINT_TO_POINTER(PURPLE_CONV_ACCOUNT_OFFLINE));
8112 purple_signal_connect(purple_connections_get_handle(), "signing-off", handle,
8113 G_CALLBACK(account_signing_off), NULL);
8115 purple_signal_connect(purple_conversations_get_handle(), "received-im-msg",
8116 handle, G_CALLBACK(received_im_msg_cb), NULL);
8117 purple_signal_connect(purple_conversations_get_handle(), "cleared-message-history",
8118 handle, G_CALLBACK(clear_conversation_scrollback_cb), NULL);
8120 purple_signal_connect(purple_conversations_get_handle(), "deleting-chat-buddy",
8121 handle, G_CALLBACK(deleting_chat_buddy_cb), NULL);
8123 purple_conversations_set_ui_ops(&conversation_ui_ops);
8125 hidden_convwin = pidgin_conv_window_new();
8126 window_list = g_list_remove(window_list, hidden_convwin);
8128 purple_signal_connect(purple_accounts_get_handle(), "account-status-changed",
8129 handle, PURPLE_CALLBACK(account_status_changed_cb), NULL);
8131 /* Callbacks to update a conversation */
8132 purple_signal_connect(blist_handle, "blist-node-added", handle,
8133 G_CALLBACK(buddy_update_cb), NULL);
8134 purple_signal_connect(blist_handle, "blist-node-removed", handle,
8135 G_CALLBACK(buddy_update_cb), NULL);
8136 purple_signal_connect(blist_handle, "buddy-signed-on",
8137 handle, PURPLE_CALLBACK(update_buddy_sign), "on");
8138 purple_signal_connect(blist_handle, "buddy-signed-off",
8139 handle, PURPLE_CALLBACK(update_buddy_sign), "off");
8140 purple_signal_connect(blist_handle, "buddy-status-changed",
8141 handle, PURPLE_CALLBACK(update_buddy_status_changed), NULL);
8142 purple_signal_connect(blist_handle, "buddy-privacy-changed",
8143 handle, PURPLE_CALLBACK(update_buddy_privacy_changed), NULL);
8144 purple_signal_connect(blist_handle, "buddy-idle-changed",
8145 handle, PURPLE_CALLBACK(update_buddy_idle_changed), NULL);
8146 purple_signal_connect(blist_handle, "buddy-icon-changed",
8147 handle, PURPLE_CALLBACK(update_buddy_icon), NULL);
8148 purple_signal_connect(purple_conversations_get_handle(), "buddy-typing",
8149 handle, PURPLE_CALLBACK(update_buddy_typing), NULL);
8150 purple_signal_connect(purple_conversations_get_handle(), "buddy-typing-stopped",
8151 handle, PURPLE_CALLBACK(update_buddy_typing), NULL);
8152 purple_signal_connect(pidgin_conversations_get_handle(), "conversation-switched",
8153 handle, PURPLE_CALLBACK(update_conversation_switched), NULL);
8154 purple_signal_connect(purple_conversations_get_handle(), "chat-left", handle,
8155 PURPLE_CALLBACK(update_chat), NULL);
8156 purple_signal_connect(purple_conversations_get_handle(), "chat-joined", handle,
8157 PURPLE_CALLBACK(update_chat), NULL);
8158 purple_signal_connect(purple_conversations_get_handle(), "chat-topic-changed", handle,
8159 PURPLE_CALLBACK(update_chat_topic), NULL);
8160 purple_signal_connect_priority(purple_conversations_get_handle(), "conversation-updated", handle,
8161 PURPLE_CALLBACK(pidgin_conv_updated), NULL,
8162 PURPLE_SIGNAL_PRIORITY_LOWEST);
8163 purple_signal_connect(purple_conversations_get_handle(), "wrote-im-msg", handle,
8164 PURPLE_CALLBACK(wrote_msg_update_unseen_cb), NULL);
8165 purple_signal_connect(purple_conversations_get_handle(), "wrote-chat-msg", handle,
8166 PURPLE_CALLBACK(wrote_msg_update_unseen_cb), NULL);
8169 /* Set default tab colors */
8170 GString *str = g_string_new(NULL);
8171 GtkSettings *settings = gtk_settings_get_default();
8172 GtkStyle *parent = gtk_rc_get_style_by_paths(settings, "tab-container.tab-label*", NULL, G_TYPE_NONE), *now;
8173 struct {
8174 const char *stylename;
8175 const char *labelname;
8176 const char *color;
8177 } styles[] = {
8178 {"pidgin_tab_label_typing_default", "tab-label-typing", "#4e9a06"},
8179 {"pidgin_tab_label_typed_default", "tab-label-typed", "#c4a000"},
8180 {"pidgin_tab_label_attention_default", "tab-label-attention", "#006aff"},
8181 {"pidgin_tab_label_unreadchat_default", "tab-label-unreadchat", "#cc0000"},
8182 {"pidgin_tab_label_event_default", "tab-label-event", "#888a85"},
8183 {NULL, NULL, NULL}
8185 int iter;
8186 for (iter = 0; styles[iter].stylename; iter++) {
8187 now = gtk_rc_get_style_by_paths(settings, styles[iter].labelname, NULL, G_TYPE_NONE);
8188 if (parent == now ||
8189 (parent && now && parent->rc_style == now->rc_style)) {
8190 g_string_append_printf(str, "style \"%s\" {\n"
8191 "fg[ACTIVE] = \"%s\"\n"
8192 "}\n"
8193 "widget \"*%s\" style \"%s\"\n",
8194 styles[iter].stylename,
8195 styles[iter].color,
8196 styles[iter].labelname, styles[iter].stylename);
8199 gtk_rc_parse_string(str->str);
8200 g_string_free(str, TRUE);
8201 gtk_rc_reset_styles(settings);
8205 void
8206 pidgin_conversations_uninit(void)
8208 purple_prefs_disconnect_by_handle(pidgin_conversations_get_handle());
8209 purple_signals_disconnect_by_handle(pidgin_conversations_get_handle());
8210 purple_signals_unregister_by_instance(pidgin_conversations_get_handle());
8228 /* down here is where gtkconvwin.c ought to start. except they share like every freaking function,
8229 * and touch each others' private members all day long */
8232 * @file gtkconvwin.c GTK+ Conversation Window API
8233 * @ingroup pidgin
8235 * pidgin
8237 * Pidgin is the legal property of its developers, whose names are too numerous
8238 * to list here. Please refer to the COPYRIGHT file distributed with this
8239 * source distribution.
8241 * This program is free software; you can redistribute it and/or modify
8242 * it under the terms of the GNU General Public License as published by
8243 * the Free Software Foundation; either version 2 of the License, or
8244 * (at your option) any later version.
8246 * This program is distributed in the hope that it will be useful,
8247 * but WITHOUT ANY WARRANTY; without even the implied warranty of
8248 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
8249 * GNU General Public License for more details.
8251 * You should have received a copy of the GNU General Public License
8252 * along with this program; if not, write to the Free Software
8253 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
8256 #include "internal.h"
8257 #include "pidgin.h"
8260 #include <gdk/gdkkeysyms.h>
8262 #include "account.h"
8263 #include "cmds.h"
8264 #include "debug.h"
8265 #include "imgstore.h"
8266 #include "log.h"
8267 #include "notify.h"
8268 #include "prpl.h"
8269 #include "request.h"
8270 #include "util.h"
8272 #include "gtkdnd-hints.h"
8273 #include "gtkblist.h"
8274 #include "gtkconv.h"
8275 #include "gtkdialogs.h"
8276 #include "gtkmenutray.h"
8277 #include "gtkpounce.h"
8278 #include "gtkprefs.h"
8279 #include "gtkprivacy.h"
8280 #include "gtkutils.h"
8281 #include "pidginstock.h"
8282 #include "gtkimhtml.h"
8283 #include "gtkimhtmltoolbar.h"
8285 static void
8286 do_close(GtkWidget *w, int resp, PidginWindow *win)
8288 gtk_widget_destroy(warn_close_dialog);
8289 warn_close_dialog = NULL;
8291 if (resp == GTK_RESPONSE_OK)
8292 pidgin_conv_window_destroy(win);
8295 static void
8296 build_warn_close_dialog(PidginWindow *gtkwin)
8298 GtkWidget *label, *vbox, *hbox, *img;
8300 g_return_if_fail(warn_close_dialog == NULL);
8302 warn_close_dialog = gtk_dialog_new_with_buttons(_("Confirm close"),
8303 GTK_WINDOW(gtkwin->window), GTK_DIALOG_MODAL,
8304 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
8305 GTK_STOCK_CLOSE, GTK_RESPONSE_OK, NULL);
8307 gtk_dialog_set_default_response(GTK_DIALOG(warn_close_dialog),
8308 GTK_RESPONSE_OK);
8310 gtk_container_set_border_width(GTK_CONTAINER(warn_close_dialog),
8312 gtk_window_set_resizable(GTK_WINDOW(warn_close_dialog), FALSE);
8313 gtk_dialog_set_has_separator(GTK_DIALOG(warn_close_dialog),
8314 FALSE);
8316 /* Setup the outside spacing. */
8317 vbox = GTK_DIALOG(warn_close_dialog)->vbox;
8319 gtk_box_set_spacing(GTK_BOX(vbox), 12);
8320 gtk_container_set_border_width(GTK_CONTAINER(vbox), 6);
8322 img = gtk_image_new_from_stock(PIDGIN_STOCK_DIALOG_WARNING,
8323 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_HUGE));
8324 /* Setup the inner hbox and put the dialog's icon in it. */
8325 hbox = gtk_hbox_new(FALSE, 12);
8326 gtk_container_add(GTK_CONTAINER(vbox), hbox);
8327 gtk_box_pack_start(GTK_BOX(hbox), img, FALSE, FALSE, 0);
8328 gtk_misc_set_alignment(GTK_MISC(img), 0, 0);
8330 /* Setup the right vbox. */
8331 vbox = gtk_vbox_new(FALSE, 12);
8332 gtk_container_add(GTK_CONTAINER(hbox), vbox);
8334 label = gtk_label_new(_("You have unread messages. Are you sure you want to close the window?"));
8335 gtk_widget_set_size_request(label, 350, -1);
8336 gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
8337 gtk_misc_set_alignment(GTK_MISC(label), 0, 0);
8338 gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 0);
8340 /* Connect the signals. */
8341 g_signal_connect(G_OBJECT(warn_close_dialog), "response",
8342 G_CALLBACK(do_close), gtkwin);
8346 /**************************************************************************
8347 * Callbacks
8348 **************************************************************************/
8350 static gboolean
8351 close_win_cb(GtkWidget *w, GdkEventAny *e, gpointer d)
8353 PidginWindow *win = d;
8354 GList *l;
8356 /* If there are unread messages then show a warning dialog */
8357 for (l = pidgin_conv_window_get_gtkconvs(win);
8358 l != NULL; l = l->next)
8360 PidginConversation *gtkconv = l->data;
8361 if (purple_conversation_get_type(gtkconv->active_conv) == PURPLE_CONV_TYPE_IM &&
8362 gtkconv->unseen_state >= PIDGIN_UNSEEN_TEXT)
8364 build_warn_close_dialog(win);
8365 gtk_widget_show_all(warn_close_dialog);
8367 return TRUE;
8371 pidgin_conv_window_destroy(win);
8373 return TRUE;
8376 static void
8377 conv_set_unseen(PurpleConversation *conv, PidginUnseenState state)
8379 int unseen_count = 0;
8380 PidginUnseenState unseen_state = PIDGIN_UNSEEN_NONE;
8382 if(purple_conversation_get_data(conv, "unseen-count"))
8383 unseen_count = GPOINTER_TO_INT(purple_conversation_get_data(conv, "unseen-count"));
8385 if(purple_conversation_get_data(conv, "unseen-state"))
8386 unseen_state = GPOINTER_TO_INT(purple_conversation_get_data(conv, "unseen-state"));
8388 if (state == PIDGIN_UNSEEN_NONE)
8390 unseen_count = 0;
8391 unseen_state = PIDGIN_UNSEEN_NONE;
8393 else
8395 if (state >= PIDGIN_UNSEEN_TEXT)
8396 unseen_count++;
8398 if (state > unseen_state)
8399 unseen_state = state;
8402 purple_conversation_set_data(conv, "unseen-count", GINT_TO_POINTER(unseen_count));
8403 purple_conversation_set_data(conv, "unseen-state", GINT_TO_POINTER(unseen_state));
8405 purple_conversation_update(conv, PURPLE_CONV_UPDATE_UNSEEN);
8408 static void
8409 gtkconv_set_unseen(PidginConversation *gtkconv, PidginUnseenState state)
8411 if (state == PIDGIN_UNSEEN_NONE)
8413 gtkconv->unseen_count = 0;
8414 gtkconv->unseen_state = PIDGIN_UNSEEN_NONE;
8416 else
8418 if (state >= PIDGIN_UNSEEN_TEXT)
8419 gtkconv->unseen_count++;
8421 if (state > gtkconv->unseen_state)
8422 gtkconv->unseen_state = state;
8425 purple_conversation_set_data(gtkconv->active_conv, "unseen-count", GINT_TO_POINTER(gtkconv->unseen_count));
8426 purple_conversation_set_data(gtkconv->active_conv, "unseen-state", GINT_TO_POINTER(gtkconv->unseen_state));
8428 purple_conversation_update(gtkconv->active_conv, PURPLE_CONV_UPDATE_UNSEEN);
8432 * When a conversation window is focused, we know the user
8433 * has looked at it so we know there are no longer unseen
8434 * messages.
8436 static gboolean
8437 focus_win_cb(GtkWidget *w, GdkEventFocus *e, gpointer d)
8439 PidginWindow *win = d;
8440 PidginConversation *gtkconv = pidgin_conv_window_get_active_gtkconv(win);
8442 if (gtkconv)
8443 gtkconv_set_unseen(gtkconv, PIDGIN_UNSEEN_NONE);
8445 return FALSE;
8448 static void
8449 notebook_init_grab(PidginWindow *gtkwin, GtkWidget *widget)
8451 static GdkCursor *cursor = NULL;
8453 gtkwin->in_drag = TRUE;
8455 if (gtkwin->drag_leave_signal) {
8456 g_signal_handler_disconnect(G_OBJECT(widget),
8457 gtkwin->drag_leave_signal);
8458 gtkwin->drag_leave_signal = 0;
8461 if (cursor == NULL)
8462 cursor = gdk_cursor_new(GDK_FLEUR);
8464 /* Grab the pointer */
8465 gtk_grab_add(gtkwin->notebook);
8466 #ifndef _WIN32
8467 /* Currently for win32 GTK+ (as of 2.2.1), gdk_pointer_is_grabbed will
8468 always be true after a button press. */
8469 if (!gdk_pointer_is_grabbed())
8470 #endif
8471 gdk_pointer_grab(gtkwin->notebook->window, FALSE,
8472 GDK_BUTTON1_MOTION_MASK | GDK_BUTTON_RELEASE_MASK,
8473 NULL, cursor, GDK_CURRENT_TIME);
8476 static gboolean
8477 notebook_motion_cb(GtkWidget *widget, GdkEventButton *e, PidginWindow *win)
8481 * Make sure the user moved the mouse far enough for the
8482 * drag to be initiated.
8484 if (win->in_predrag) {
8485 if (e->x_root < win->drag_min_x ||
8486 e->x_root >= win->drag_max_x ||
8487 e->y_root < win->drag_min_y ||
8488 e->y_root >= win->drag_max_y) {
8490 win->in_predrag = FALSE;
8491 notebook_init_grab(win, widget);
8494 else { /* Otherwise, draw the arrows. */
8495 PidginWindow *dest_win;
8496 GtkNotebook *dest_notebook;
8497 GtkWidget *tab;
8498 gint page_num;
8499 gboolean horiz_tabs = FALSE;
8500 gboolean to_right = FALSE;
8502 /* Get the window that the cursor is over. */
8503 dest_win = pidgin_conv_window_get_at_xy(e->x_root, e->y_root);
8505 if (dest_win == NULL) {
8506 dnd_hints_hide_all();
8508 return TRUE;
8511 dest_notebook = GTK_NOTEBOOK(dest_win->notebook);
8513 if (gtk_notebook_get_show_tabs(dest_notebook)) {
8514 page_num = pidgin_conv_get_tab_at_xy(dest_win,
8515 e->x_root, e->y_root, &to_right);
8516 to_right = to_right && (win != dest_win);
8517 tab = pidgin_conv_window_get_gtkconv_at_index(dest_win, page_num)->tabby;
8518 } else {
8519 page_num = 0;
8520 to_right = pidgin_conv_xy_to_right_infopane(dest_win, e->x_root, e->y_root);
8521 tab = pidgin_conv_window_get_gtkconv_at_index(dest_win, page_num)->infopane_hbox;
8524 if (gtk_notebook_get_tab_pos(dest_notebook) == GTK_POS_TOP ||
8525 gtk_notebook_get_tab_pos(dest_notebook) == GTK_POS_BOTTOM) {
8526 horiz_tabs = TRUE;
8529 if (gtk_notebook_get_show_tabs(dest_notebook) == FALSE && win == dest_win)
8531 /* dragging a tab from a single-tabbed window over its own window */
8532 dnd_hints_hide_all();
8533 return TRUE;
8534 } else if (horiz_tabs) {
8535 if (((gpointer)win == (gpointer)dest_win && win->drag_tab < page_num) || to_right) {
8536 dnd_hints_show_relative(HINT_ARROW_DOWN, tab, HINT_POSITION_RIGHT, HINT_POSITION_TOP);
8537 dnd_hints_show_relative(HINT_ARROW_UP, tab, HINT_POSITION_RIGHT, HINT_POSITION_BOTTOM);
8538 } else {
8539 dnd_hints_show_relative(HINT_ARROW_DOWN, tab, HINT_POSITION_LEFT, HINT_POSITION_TOP);
8540 dnd_hints_show_relative(HINT_ARROW_UP, tab, HINT_POSITION_LEFT, HINT_POSITION_BOTTOM);
8542 } else {
8543 if (((gpointer)win == (gpointer)dest_win && win->drag_tab < page_num) || to_right) {
8544 dnd_hints_show_relative(HINT_ARROW_RIGHT, tab, HINT_POSITION_LEFT, HINT_POSITION_BOTTOM);
8545 dnd_hints_show_relative(HINT_ARROW_LEFT, tab, HINT_POSITION_RIGHT, HINT_POSITION_BOTTOM);
8546 } else {
8547 dnd_hints_show_relative(HINT_ARROW_RIGHT, tab, HINT_POSITION_LEFT, HINT_POSITION_TOP);
8548 dnd_hints_show_relative(HINT_ARROW_LEFT, tab, HINT_POSITION_RIGHT, HINT_POSITION_TOP);
8553 return TRUE;
8556 static gboolean
8557 notebook_leave_cb(GtkWidget *widget, GdkEventCrossing *e, PidginWindow *win)
8559 if (win->in_drag)
8560 return FALSE;
8562 if (e->x_root < win->drag_min_x ||
8563 e->x_root >= win->drag_max_x ||
8564 e->y_root < win->drag_min_y ||
8565 e->y_root >= win->drag_max_y) {
8567 win->in_predrag = FALSE;
8568 notebook_init_grab(win, widget);
8571 return TRUE;
8575 * THANK YOU GALEON!
8578 static gboolean
8579 infopane_press_cb(GtkWidget *widget, GdkEventButton *e, PidginConversation *gtkconv)
8581 if (e->type == GDK_2BUTTON_PRESS && e->button == 1) {
8582 if (infopane_entry_activate(gtkconv))
8583 return TRUE;
8586 if (e->type != GDK_BUTTON_PRESS)
8587 return FALSE;
8589 if (e->button == 1) {
8590 int nb_x, nb_y;
8592 if (gtkconv->win->in_drag)
8593 return TRUE;
8595 gtkconv->win->in_predrag = TRUE;
8596 gtkconv->win->drag_tab = gtk_notebook_page_num(GTK_NOTEBOOK(gtkconv->win->notebook), gtkconv->tab_cont);
8598 gdk_window_get_origin(gtkconv->infopane_hbox->window, &nb_x, &nb_y);
8600 gtkconv->win->drag_min_x = gtkconv->infopane_hbox->allocation.x + nb_x;
8601 gtkconv->win->drag_min_y = gtkconv->infopane_hbox->allocation.y + nb_y;
8602 gtkconv->win->drag_max_x = gtkconv->infopane_hbox->allocation.width + gtkconv->win->drag_min_x;
8603 gtkconv->win->drag_max_y = gtkconv->infopane_hbox->allocation.height + gtkconv->win->drag_min_y;
8605 gtkconv->win->drag_motion_signal = g_signal_connect(G_OBJECT(gtkconv->win->notebook), "motion_notify_event",
8606 G_CALLBACK(notebook_motion_cb), gtkconv->win);
8607 gtkconv->win->drag_leave_signal = g_signal_connect(G_OBJECT(gtkconv->win->notebook), "leave_notify_event",
8608 G_CALLBACK(notebook_leave_cb), gtkconv->win);
8609 return FALSE;
8612 if (e->button == 3) {
8613 /* Right click was pressed. Popup the context menu. */
8614 GtkWidget *menu = gtk_menu_new(), *sub;
8615 gboolean populated = populate_menu_with_options(menu, gtkconv, TRUE);
8616 sub = gtk_menu_item_get_submenu(GTK_MENU_ITEM(gtkconv->win->menu.send_to));
8618 if (sub && GTK_WIDGET_IS_SENSITIVE(gtkconv->win->menu.send_to)) {
8619 GtkWidget *item = gtk_menu_item_new_with_mnemonic(_("S_end To"));
8620 if (populated)
8621 pidgin_separator(menu);
8622 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
8623 gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), sub);
8624 gtk_widget_show(item);
8625 gtk_widget_show_all(sub);
8626 } else if (!populated) {
8627 gtk_widget_destroy(menu);
8628 return FALSE;
8631 gtk_widget_show_all(menu);
8632 gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, e->button, e->time);
8633 return TRUE;
8635 return FALSE;
8638 static gboolean
8639 notebook_press_cb(GtkWidget *widget, GdkEventButton *e, PidginWindow *win)
8641 gint nb_x, nb_y;
8642 int tab_clicked;
8643 GtkWidget *page;
8644 GtkWidget *tab;
8646 if (e->button == 2 && e->type == GDK_BUTTON_PRESS) {
8647 PidginConversation *gtkconv;
8648 tab_clicked = pidgin_conv_get_tab_at_xy(win, e->x_root, e->y_root, NULL);
8650 if (tab_clicked == -1)
8651 return FALSE;
8653 gtkconv = pidgin_conv_window_get_gtkconv_at_index(win, tab_clicked);
8654 close_conv_cb(NULL, gtkconv);
8655 return TRUE;
8659 if (e->button != 1 || e->type != GDK_BUTTON_PRESS)
8660 return FALSE;
8663 if (win->in_drag) {
8664 purple_debug(PURPLE_DEBUG_WARNING, "gtkconv",
8665 "Already in the middle of a window drag at tab_press_cb\n");
8666 return TRUE;
8670 * Make sure a tab was actually clicked. The arrow buttons
8671 * mess things up.
8673 tab_clicked = pidgin_conv_get_tab_at_xy(win, e->x_root, e->y_root, NULL);
8675 if (tab_clicked == -1)
8676 return FALSE;
8679 * Get the relative position of the press event, with regards to
8680 * the position of the notebook.
8682 gdk_window_get_origin(win->notebook->window, &nb_x, &nb_y);
8684 /* Reset the min/max x/y */
8685 win->drag_min_x = 0;
8686 win->drag_min_y = 0;
8687 win->drag_max_x = 0;
8688 win->drag_max_y = 0;
8690 /* Find out which tab was dragged. */
8691 page = gtk_notebook_get_nth_page(GTK_NOTEBOOK(win->notebook), tab_clicked);
8692 tab = gtk_notebook_get_tab_label(GTK_NOTEBOOK(win->notebook), page);
8694 win->drag_min_x = tab->allocation.x + nb_x;
8695 win->drag_min_y = tab->allocation.y + nb_y;
8696 win->drag_max_x = tab->allocation.width + win->drag_min_x;
8697 win->drag_max_y = tab->allocation.height + win->drag_min_y;
8699 /* Make sure the click occurred in the tab. */
8700 if (e->x_root < win->drag_min_x ||
8701 e->x_root >= win->drag_max_x ||
8702 e->y_root < win->drag_min_y ||
8703 e->y_root >= win->drag_max_y) {
8705 return FALSE;
8708 win->in_predrag = TRUE;
8709 win->drag_tab = tab_clicked;
8711 /* Connect the new motion signals. */
8712 win->drag_motion_signal =
8713 g_signal_connect(G_OBJECT(widget), "motion_notify_event",
8714 G_CALLBACK(notebook_motion_cb), win);
8716 win->drag_leave_signal =
8717 g_signal_connect(G_OBJECT(widget), "leave_notify_event",
8718 G_CALLBACK(notebook_leave_cb), win);
8720 return FALSE;
8723 static gboolean
8724 notebook_release_cb(GtkWidget *widget, GdkEventButton *e, PidginWindow *win)
8726 PidginWindow *dest_win;
8727 GtkNotebook *dest_notebook;
8728 PurpleConversation *conv;
8729 PidginConversation *gtkconv;
8730 gint dest_page_num = 0;
8731 gboolean new_window = FALSE;
8732 gboolean to_right = FALSE;
8735 * Don't check to make sure that the event's window matches the
8736 * widget's, because we may be getting an event passed on from the
8737 * close button.
8739 if (e->button != 1 && e->type != GDK_BUTTON_RELEASE)
8740 return FALSE;
8742 if (gdk_pointer_is_grabbed()) {
8743 gdk_pointer_ungrab(GDK_CURRENT_TIME);
8744 gtk_grab_remove(widget);
8747 if (!win->in_predrag && !win->in_drag)
8748 return FALSE;
8750 /* Disconnect the motion signal. */
8751 if (win->drag_motion_signal) {
8752 g_signal_handler_disconnect(G_OBJECT(widget),
8753 win->drag_motion_signal);
8755 win->drag_motion_signal = 0;
8759 * If we're in a pre-drag, we'll also need to disconnect the leave
8760 * signal.
8762 if (win->in_predrag) {
8763 win->in_predrag = FALSE;
8765 if (win->drag_leave_signal) {
8766 g_signal_handler_disconnect(G_OBJECT(widget),
8767 win->drag_leave_signal);
8769 win->drag_leave_signal = 0;
8773 /* If we're not in drag... */
8774 /* We're perfectly normal people! */
8775 if (!win->in_drag)
8776 return FALSE;
8778 win->in_drag = FALSE;
8780 dnd_hints_hide_all();
8782 dest_win = pidgin_conv_window_get_at_xy(e->x_root, e->y_root);
8784 conv = pidgin_conv_window_get_active_conversation(win);
8786 if (dest_win == NULL) {
8787 /* If the current window doesn't have any other conversations,
8788 * there isn't much point transferring the conv to a new window. */
8789 if (pidgin_conv_window_get_gtkconv_count(win) > 1) {
8790 /* Make a new window to stick this to. */
8791 dest_win = pidgin_conv_window_new();
8792 new_window = TRUE;
8796 if (dest_win == NULL)
8797 return FALSE;
8799 purple_signal_emit(pidgin_conversations_get_handle(),
8800 "conversation-dragging", win, dest_win);
8802 /* Get the destination page number. */
8803 if (!new_window) {
8804 dest_notebook = GTK_NOTEBOOK(dest_win->notebook);
8805 if (gtk_notebook_get_show_tabs(dest_notebook)) {
8806 dest_page_num = pidgin_conv_get_tab_at_xy(dest_win,
8807 e->x_root, e->y_root, &to_right);
8808 } else {
8809 dest_page_num = 0;
8810 to_right = pidgin_conv_xy_to_right_infopane(dest_win, e->x_root, e->y_root);
8814 gtkconv = pidgin_conv_window_get_gtkconv_at_index(win, win->drag_tab);
8816 if (win == dest_win) {
8817 gtk_notebook_reorder_child(GTK_NOTEBOOK(win->notebook), gtkconv->tab_cont, dest_page_num);
8818 } else {
8819 pidgin_conv_window_remove_gtkconv(win, gtkconv);
8820 pidgin_conv_window_add_gtkconv(dest_win, gtkconv);
8821 gtk_notebook_reorder_child(GTK_NOTEBOOK(dest_win->notebook), gtkconv->tab_cont, dest_page_num + to_right);
8822 pidgin_conv_window_switch_gtkconv(dest_win, gtkconv);
8823 if (new_window) {
8824 gint win_width, win_height;
8826 gtk_window_get_size(GTK_WINDOW(dest_win->window),
8827 &win_width, &win_height);
8828 #ifdef _WIN32 /* only override window manager placement on Windows */
8829 gtk_window_move(GTK_WINDOW(dest_win->window),
8830 e->x_root - (win_width / 2),
8831 e->y_root - (win_height / 2));
8832 #endif
8834 pidgin_conv_window_show(dest_win);
8838 gtk_widget_grab_focus(PIDGIN_CONVERSATION(conv)->entry);
8840 return TRUE;
8844 static void
8845 before_switch_conv_cb(GtkNotebook *notebook, GtkWidget *page, gint page_num,
8846 gpointer user_data)
8848 PidginWindow *win;
8849 PurpleConversation *conv;
8850 PidginConversation *gtkconv;
8852 win = user_data;
8853 conv = pidgin_conv_window_get_active_conversation(win);
8855 g_return_if_fail(conv != NULL);
8857 if (purple_conversation_get_type(conv) != PURPLE_CONV_TYPE_IM)
8858 return;
8860 gtkconv = PIDGIN_CONVERSATION(conv);
8862 if (gtkconv->u.im->typing_timer != 0) {
8863 g_source_remove(gtkconv->u.im->typing_timer);
8864 gtkconv->u.im->typing_timer = 0;
8867 stop_anim(NULL, gtkconv);
8869 static void
8870 close_window(GtkWidget *w, PidginWindow *win)
8872 close_win_cb(w, NULL, win);
8875 static void
8876 detach_tab_cb(GtkWidget *w, GObject *menu)
8878 PidginWindow *win, *new_window;
8879 PidginConversation *gtkconv;
8881 gtkconv = g_object_get_data(menu, "clicked_tab");
8883 if (!gtkconv)
8884 return;
8886 win = pidgin_conv_get_window(gtkconv);
8887 /* Nothing to do if there's only one tab in the window */
8888 if (pidgin_conv_window_get_gtkconv_count(win) == 1)
8889 return;
8891 pidgin_conv_window_remove_gtkconv(win, gtkconv);
8893 new_window = pidgin_conv_window_new();
8894 pidgin_conv_window_add_gtkconv(new_window, gtkconv);
8895 pidgin_conv_window_show(new_window);
8898 static void
8899 close_others_cb(GtkWidget *w, GObject *menu)
8901 GList *iter;
8902 PidginConversation *gtkconv;
8903 PidginWindow *win;
8905 gtkconv = g_object_get_data(menu, "clicked_tab");
8907 if (!gtkconv)
8908 return;
8910 win = pidgin_conv_get_window(gtkconv);
8912 for (iter = pidgin_conv_window_get_gtkconvs(win); iter; )
8914 PidginConversation *gconv = iter->data;
8915 iter = iter->next;
8917 if (gconv != gtkconv)
8919 close_conv_cb(NULL, gconv);
8924 static void close_tab_cb(GtkWidget *w, GObject *menu)
8926 PidginConversation *gtkconv;
8928 gtkconv = g_object_get_data(menu, "clicked_tab");
8930 if (gtkconv)
8931 close_conv_cb(NULL, gtkconv);
8934 static gboolean
8935 right_click_menu_cb(GtkNotebook *notebook, GdkEventButton *event, PidginWindow *win)
8937 GtkWidget *item, *menu;
8938 PidginConversation *gtkconv;
8940 if (event->type != GDK_BUTTON_PRESS || event->button != 3)
8941 return FALSE;
8943 gtkconv = pidgin_conv_window_get_gtkconv_at_index(win,
8944 pidgin_conv_get_tab_at_xy(win, event->x_root, event->y_root, NULL));
8946 if (g_object_get_data(G_OBJECT(notebook->menu), "clicked_tab"))
8948 g_object_set_data(G_OBJECT(notebook->menu), "clicked_tab", gtkconv);
8949 return FALSE;
8952 g_object_set_data(G_OBJECT(notebook->menu), "clicked_tab", gtkconv);
8954 menu = notebook->menu;
8955 pidgin_separator(GTK_WIDGET(menu));
8957 item = gtk_menu_item_new_with_label(_("Close other tabs"));
8958 gtk_widget_show(item);
8959 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
8960 g_signal_connect(G_OBJECT(item), "activate",
8961 G_CALLBACK(close_others_cb), menu);
8963 item = gtk_menu_item_new_with_label(_("Close all tabs"));
8964 gtk_widget_show(item);
8965 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
8966 g_signal_connect(G_OBJECT(item), "activate",
8967 G_CALLBACK(close_window), win);
8969 pidgin_separator(menu);
8971 item = gtk_menu_item_new_with_label(_("Detach this tab"));
8972 gtk_widget_show(item);
8973 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
8974 g_signal_connect(G_OBJECT(item), "activate",
8975 G_CALLBACK(detach_tab_cb), menu);
8977 item = gtk_menu_item_new_with_label(_("Close this tab"));
8978 gtk_widget_show(item);
8979 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
8980 g_signal_connect(G_OBJECT(item), "activate",
8981 G_CALLBACK(close_tab_cb), menu);
8983 return FALSE;
8986 static void
8987 remove_edit_entry(PidginConversation *gtkconv, GtkWidget *entry)
8989 g_signal_handlers_disconnect_matched(G_OBJECT(entry), G_SIGNAL_MATCH_DATA,
8990 0, 0, NULL, NULL, gtkconv);
8991 gtk_widget_show(gtkconv->infopane);
8992 gtk_widget_grab_focus(gtkconv->entry);
8993 gtk_widget_destroy(entry);
8996 static gboolean
8997 alias_focus_cb(GtkWidget *widget, GdkEventFocus *event, gpointer user_data)
8999 remove_edit_entry(user_data, widget);
9000 return FALSE;
9003 static gboolean
9004 alias_key_press_cb(GtkWidget *widget, GdkEventKey *event, gpointer user_data)
9006 if (event->keyval == GDK_Escape) {
9007 remove_edit_entry(user_data, widget);
9008 return TRUE;
9010 return FALSE;
9013 static void
9014 alias_cb(GtkEntry *entry, gpointer user_data)
9016 PidginConversation *gtkconv;
9017 PurpleConversation *conv;
9018 PurpleAccount *account;
9019 const char *name;
9021 gtkconv = (PidginConversation *)user_data;
9022 if (gtkconv == NULL) {
9023 return;
9025 conv = gtkconv->active_conv;
9026 account = purple_conversation_get_account(conv);
9027 name = purple_conversation_get_name(conv);
9029 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) {
9030 PurpleBuddy *buddy;
9031 buddy = purple_find_buddy(account, name);
9032 if (buddy != NULL) {
9033 purple_blist_alias_buddy(buddy,
9034 gtk_entry_get_text(entry));
9036 serv_alias_buddy(buddy);
9037 } else if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) {
9038 gtk_entry_set_text(GTK_ENTRY(gtkconv->u.chat->topic_text), gtk_entry_get_text(entry));
9039 topic_callback(NULL, gtkconv);
9041 remove_edit_entry(user_data, GTK_WIDGET(entry));
9044 static gboolean
9045 infopane_entry_activate(PidginConversation *gtkconv)
9047 GtkWidget *entry = NULL;
9048 PurpleConversation *conv = gtkconv->active_conv;
9049 const char *text = NULL;
9051 if (!GTK_WIDGET_VISIBLE(gtkconv->infopane)) {
9052 /* There's already an entry for alias. Let's not create another one. */
9053 return FALSE;
9056 if (!purple_account_is_connected(gtkconv->active_conv->account)) {
9057 /* Do not allow aliasing someone on a disconnected account. */
9058 return FALSE;
9061 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) {
9062 PurpleBuddy *buddy = purple_find_buddy(gtkconv->active_conv->account, gtkconv->active_conv->name);
9063 if (!buddy)
9064 /* This buddy isn't in your buddy list, so we can't alias him */
9065 return FALSE;
9067 text = purple_buddy_get_contact_alias(buddy);
9068 } else if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) {
9069 PurpleConnection *gc;
9070 PurplePluginProtocolInfo *prpl_info = NULL;
9072 gc = purple_conversation_get_gc(conv);
9073 if (gc != NULL)
9074 prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(gc->prpl);
9075 if (prpl_info && prpl_info->set_chat_topic == NULL)
9076 /* This protocol doesn't support setting the chat room topic */
9077 return FALSE;
9079 text = purple_conv_chat_get_topic(PURPLE_CONV_CHAT(conv));
9082 /* alias label */
9083 entry = gtk_entry_new();
9084 gtk_entry_set_has_frame(GTK_ENTRY(entry), FALSE);
9085 gtk_entry_set_width_chars(GTK_ENTRY(entry), 10);
9086 gtk_entry_set_alignment(GTK_ENTRY(entry), 0.5);
9088 gtk_box_pack_start(GTK_BOX(gtkconv->infopane_hbox), entry, TRUE, TRUE, 0);
9089 /* after the tab label */
9090 gtk_box_reorder_child(GTK_BOX(gtkconv->infopane_hbox), entry, 0);
9092 g_signal_connect(G_OBJECT(entry), "activate", G_CALLBACK(alias_cb), gtkconv);
9093 g_signal_connect(G_OBJECT(entry), "focus-out-event", G_CALLBACK(alias_focus_cb), gtkconv);
9094 g_signal_connect(G_OBJECT(entry), "key-press-event", G_CALLBACK(alias_key_press_cb), gtkconv);
9096 if (text != NULL)
9097 gtk_entry_set_text(GTK_ENTRY(entry), text);
9098 gtk_widget_show(entry);
9099 gtk_widget_hide(gtkconv->infopane);
9100 gtk_widget_grab_focus(entry);
9102 return TRUE;
9105 static gboolean
9106 window_keypress_cb(GtkWidget *widget, GdkEventKey *event, PidginWindow *win)
9108 PidginConversation *gtkconv = pidgin_conv_window_get_active_gtkconv(win);
9110 return conv_keypress_common(gtkconv, event);
9113 static void
9114 switch_conv_cb(GtkNotebook *notebook, GtkWidget *page, gint page_num,
9115 gpointer user_data)
9117 PidginWindow *win;
9118 PurpleConversation *conv;
9119 PidginConversation *gtkconv;
9120 const char *sound_method;
9122 win = user_data;
9123 gtkconv = pidgin_conv_window_get_gtkconv_at_index(win, page_num);
9124 conv = gtkconv->active_conv;
9126 g_return_if_fail(conv != NULL);
9128 /* clear unseen flag if conversation is not hidden */
9129 if(!pidgin_conv_is_hidden(gtkconv)) {
9130 gtkconv_set_unseen(gtkconv, PIDGIN_UNSEEN_NONE);
9133 /* Update the menubar */
9135 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(gtkconv->win->menu.logging),
9136 purple_conversation_is_logging(conv));
9138 generate_send_to_items(win);
9139 regenerate_options_items(win);
9140 regenerate_plugins_items(win);
9142 pidgin_conv_switch_active_conversation(conv);
9144 sound_method = purple_prefs_get_string(PIDGIN_PREFS_ROOT "/sound/method");
9145 if (strcmp(sound_method, "none") != 0)
9146 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(win->menu.sounds),
9147 gtkconv->make_sound);
9149 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(win->menu.show_formatting_toolbar),
9150 purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/show_formatting_toolbar"));
9152 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(win->menu.show_timestamps),
9153 purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/show_timestamps"));
9156 * We pause icons when they are not visible. If this icon should
9157 * be animated then start it back up again.
9159 if ((purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) &&
9160 (gtkconv->u.im->animate))
9161 start_anim(NULL, gtkconv);
9163 purple_signal_emit(pidgin_conversations_get_handle(), "conversation-switched", conv);
9166 /**************************************************************************
9167 * GTK+ window ops
9168 **************************************************************************/
9170 GList *
9171 pidgin_conv_windows_get_list()
9173 return window_list;
9176 static GList*
9177 make_status_icon_list(const char *stock, GtkWidget *w)
9179 GList *l = NULL;
9180 l = g_list_append(l, gtk_widget_render_icon (w, stock,
9181 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL), "GtkWindow"));
9182 l = g_list_append(l, gtk_widget_render_icon (w, stock,
9183 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_SMALL), "GtkWindow"));
9184 l = g_list_append(l, gtk_widget_render_icon (w, stock,
9185 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_MEDIUM), "GtkWindow"));
9186 l = g_list_append(l, gtk_widget_render_icon (w, stock,
9187 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_LARGE), "GtkWindow"));
9188 return l;
9191 static void
9192 create_icon_lists(GtkWidget *w)
9194 available_list = make_status_icon_list(PIDGIN_STOCK_STATUS_AVAILABLE, w);
9195 busy_list = make_status_icon_list(PIDGIN_STOCK_STATUS_BUSY, w);
9196 xa_list = make_status_icon_list(PIDGIN_STOCK_STATUS_XA, w);
9197 offline_list = make_status_icon_list(PIDGIN_STOCK_STATUS_OFFLINE, w);
9198 away_list = make_status_icon_list(PIDGIN_STOCK_STATUS_AWAY, w);
9199 prpl_lists = g_hash_table_new(g_str_hash, g_str_equal);
9202 static void
9203 plugin_changed_cb(PurplePlugin *p, gpointer data)
9205 regenerate_plugins_items(data);
9208 static gboolean gtk_conv_configure_cb(GtkWidget *w, GdkEventConfigure *event, gpointer data) {
9209 int x, y;
9211 if (GTK_WIDGET_VISIBLE(w))
9212 gtk_window_get_position(GTK_WINDOW(w), &x, &y);
9213 else
9214 return FALSE; /* carry on normally */
9216 /* Workaround for GTK+ bug # 169811 - "configure_event" is fired
9217 * when the window is being maximized */
9218 if (gdk_window_get_state(w->window) & GDK_WINDOW_STATE_MAXIMIZED)
9219 return FALSE;
9221 /* don't save off-screen positioning */
9222 if (x + event->width < 0 ||
9223 y + event->height < 0 ||
9224 x > gdk_screen_width() ||
9225 y > gdk_screen_height())
9226 return FALSE; /* carry on normally */
9228 /* store the position */
9229 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/x", x);
9230 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/y", y);
9231 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/width", event->width);
9232 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/height", event->height);
9234 /* continue to handle event normally */
9235 return FALSE;
9239 static void
9240 pidgin_conv_set_position_size(PidginWindow *win, int conv_x, int conv_y,
9241 int conv_width, int conv_height)
9243 /* if the window exists, is hidden, we're saving positions, and the
9244 * position is sane... */
9245 if (win && win->window &&
9246 !GTK_WIDGET_VISIBLE(win->window) && conv_width != 0) {
9248 #ifdef _WIN32 /* only override window manager placement on Windows */
9249 /* ...check position is on screen... */
9250 if (conv_x >= gdk_screen_width())
9251 conv_x = gdk_screen_width() - 100;
9252 else if (conv_x + conv_width < 0)
9253 conv_x = 100;
9255 if (conv_y >= gdk_screen_height())
9256 conv_y = gdk_screen_height() - 100;
9257 else if (conv_y + conv_height < 0)
9258 conv_y = 100;
9260 /* ...and move it back. */
9261 gtk_window_move(GTK_WINDOW(win->window), conv_x, conv_y);
9262 #endif
9263 gtk_window_resize(GTK_WINDOW(win->window), conv_width, conv_height);
9267 static void
9268 pidgin_conv_restore_position(PidginWindow *win) {
9269 pidgin_conv_set_position_size(win,
9270 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/x"),
9271 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/y"),
9272 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/width"),
9273 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/height"));
9276 PidginWindow *
9277 pidgin_conv_window_new()
9279 PidginWindow *win;
9280 GtkPositionType pos;
9281 GtkWidget *testidea;
9282 GtkWidget *menubar;
9283 GdkModifierType state;
9285 win = g_malloc0(sizeof(PidginWindow));
9287 window_list = g_list_append(window_list, win);
9289 /* Create the window. */
9290 win->window = pidgin_create_window(NULL, 0, "conversation", TRUE);
9291 if (!gtk_get_current_event_state(&state))
9292 gtk_window_set_focus_on_map(GTK_WINDOW(win->window), FALSE);
9294 /* Etan: I really think this entire function call should happen only
9295 * when we are on Windows but I was informed that back before we used
9296 * to save the window position we stored the window size, so I'm
9297 * leaving it for now. */
9298 #if TRUE || defined(_WIN32)
9299 pidgin_conv_restore_position(win);
9300 #endif
9302 if (available_list == NULL) {
9303 create_icon_lists(win->window);
9306 g_signal_connect(G_OBJECT(win->window), "delete_event",
9307 G_CALLBACK(close_win_cb), win);
9308 g_signal_connect(G_OBJECT(win->window), "focus_in_event",
9309 G_CALLBACK(focus_win_cb), win);
9311 /* Intercept keystrokes from the menu items */
9312 g_signal_connect(G_OBJECT(win->window), "key_press_event",
9313 G_CALLBACK(window_keypress_cb), win);
9316 /* Create the notebook. */
9317 win->notebook = gtk_notebook_new();
9319 pos = purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side");
9321 #if 0
9322 gtk_notebook_set_tab_hborder(GTK_NOTEBOOK(win->notebook), 0);
9323 gtk_notebook_set_tab_vborder(GTK_NOTEBOOK(win->notebook), 0);
9324 #endif
9325 gtk_notebook_set_tab_pos(GTK_NOTEBOOK(win->notebook), pos);
9326 gtk_notebook_set_scrollable(GTK_NOTEBOOK(win->notebook), TRUE);
9327 gtk_notebook_popup_enable(GTK_NOTEBOOK(win->notebook));
9328 gtk_notebook_set_show_tabs(GTK_NOTEBOOK(win->notebook), FALSE);
9329 gtk_notebook_set_show_border(GTK_NOTEBOOK(win->notebook), TRUE);
9331 g_signal_connect(G_OBJECT(win->notebook), "button-press-event",
9332 G_CALLBACK(right_click_menu_cb), win);
9334 gtk_widget_show(win->notebook);
9336 g_signal_connect(G_OBJECT(win->notebook), "switch_page",
9337 G_CALLBACK(before_switch_conv_cb), win);
9338 g_signal_connect_after(G_OBJECT(win->notebook), "switch_page",
9339 G_CALLBACK(switch_conv_cb), win);
9341 /* Setup the tab drag and drop signals. */
9342 gtk_widget_add_events(win->notebook,
9343 GDK_BUTTON1_MOTION_MASK | GDK_LEAVE_NOTIFY_MASK);
9344 g_signal_connect(G_OBJECT(win->notebook), "button_press_event",
9345 G_CALLBACK(notebook_press_cb), win);
9346 g_signal_connect(G_OBJECT(win->notebook), "button_release_event",
9347 G_CALLBACK(notebook_release_cb), win);
9349 testidea = gtk_vbox_new(FALSE, 0);
9351 /* Setup the menubar. */
9352 menubar = setup_menubar(win);
9353 gtk_box_pack_start(GTK_BOX(testidea), menubar, FALSE, TRUE, 0);
9355 gtk_box_pack_start(GTK_BOX(testidea), win->notebook, TRUE, TRUE, 0);
9357 gtk_container_add(GTK_CONTAINER(win->window), testidea);
9359 gtk_widget_show(testidea);
9361 /* Update the plugin actions when plugins are (un)loaded */
9362 purple_signal_connect(purple_plugins_get_handle(), "plugin-load",
9363 win, PURPLE_CALLBACK(plugin_changed_cb), win);
9364 purple_signal_connect(purple_plugins_get_handle(), "plugin-unload",
9365 win, PURPLE_CALLBACK(plugin_changed_cb), win);
9368 #ifdef _WIN32
9369 g_signal_connect(G_OBJECT(win->window), "show",
9370 G_CALLBACK(winpidgin_ensure_onscreen), win->window);
9372 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/win32/minimize_new_convs")
9373 && !gtk_get_current_event_state(&state))
9374 gtk_window_iconify(GTK_WINDOW(win->window));
9375 #endif
9377 return win;
9380 void
9381 pidgin_conv_window_destroy(PidginWindow *win)
9383 purple_prefs_disconnect_by_handle(win);
9384 window_list = g_list_remove(window_list, win);
9386 /* Close the "Find" dialog if it's open */
9387 if (win->dialogs.search)
9388 gtk_widget_destroy(win->dialogs.search);
9390 if (win->gtkconvs) {
9391 while (win->gtkconvs) {
9392 gboolean last = (win->gtkconvs->next == NULL);
9393 close_conv_cb(NULL, win->gtkconvs->data);
9394 if (last)
9395 break;
9397 return;
9399 gtk_widget_destroy(win->window);
9401 g_object_unref(G_OBJECT(win->menu.item_factory));
9403 purple_notify_close_with_handle(win);
9404 purple_signals_disconnect_by_handle(win);
9406 g_free(win);
9409 void
9410 pidgin_conv_window_show(PidginWindow *win)
9412 gtk_widget_show(win->window);
9415 void
9416 pidgin_conv_window_hide(PidginWindow *win)
9418 gtk_widget_hide(win->window);
9421 void
9422 pidgin_conv_window_raise(PidginWindow *win)
9424 gdk_window_raise(GDK_WINDOW(win->window->window));
9427 void
9428 pidgin_conv_window_switch_gtkconv(PidginWindow *win, PidginConversation *gtkconv)
9430 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook),
9431 gtk_notebook_page_num(GTK_NOTEBOOK(win->notebook),
9432 gtkconv->tab_cont));
9435 static gboolean
9436 gtkconv_tab_set_tip(GtkWidget *widget, GdkEventCrossing *event, PidginConversation *gtkconv)
9438 #if GTK_CHECK_VERSION(2, 12, 0)
9439 #define gtk_tooltips_set_tip(tips, w, l, p) gtk_widget_set_tooltip_text(w, l)
9440 #endif
9441 /* PANGO_VERSION_CHECK macro was introduced in 1.15. So we need this double check. */
9442 #ifndef PANGO_VERSION_CHECK
9443 #define pango_layout_is_ellipsized(l) TRUE
9444 #elif !PANGO_VERSION_CHECK(1,16,0)
9445 #define pango_layout_is_ellipsized(l) TRUE
9446 #endif
9447 PangoLayout *layout;
9449 layout = gtk_label_get_layout(GTK_LABEL(gtkconv->tab_label));
9450 gtk_tooltips_set_tip(gtkconv->tooltips, widget,
9451 pango_layout_is_ellipsized(layout) ? gtk_label_get_text(GTK_LABEL(gtkconv->tab_label)) : NULL,
9452 NULL);
9453 return FALSE;
9454 #if GTK_CHECK_VERSION(2, 12, 0)
9455 #undef gtk_tooltips_set_tip
9456 #endif
9459 void
9460 pidgin_conv_window_add_gtkconv(PidginWindow *win, PidginConversation *gtkconv)
9462 PurpleConversation *conv = gtkconv->active_conv;
9463 PidginConversation *focus_gtkconv;
9464 GtkWidget *tab_cont = gtkconv->tab_cont;
9465 PurpleConversationType conv_type;
9466 const gchar *tmp_lab;
9468 conv_type = purple_conversation_get_type(conv);
9470 win->gtkconvs = g_list_append(win->gtkconvs, gtkconv);
9471 gtkconv->win = win;
9473 if (win->gtkconvs && win->gtkconvs->next && win->gtkconvs->next->next == NULL)
9474 pidgin_conv_tab_pack(win, ((PidginConversation*)win->gtkconvs->data));
9477 /* Close button. */
9478 gtkconv->close = pidgin_create_small_button(gtk_label_new("×"));
9479 gtk_tooltips_set_tip(gtkconv->tooltips, gtkconv->close,
9480 _("Close conversation"), NULL);
9482 g_signal_connect(gtkconv->close, "clicked", G_CALLBACK (close_conv_cb), gtkconv);
9484 /* Status icon. */
9485 gtkconv->icon = gtk_image_new();
9486 gtkconv->menu_icon = gtk_image_new();
9487 g_object_set(G_OBJECT(gtkconv->icon),
9488 "icon-size", gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_MICROSCOPIC),
9489 NULL);
9490 g_object_set(G_OBJECT(gtkconv->menu_icon),
9491 "icon-size", gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_MICROSCOPIC),
9492 NULL);
9493 gtk_widget_show(gtkconv->icon);
9494 update_tab_icon(conv);
9496 /* Tab label. */
9497 gtkconv->tab_label = gtk_label_new(tmp_lab = purple_conversation_get_title(conv));
9498 gtk_widget_set_name(gtkconv->tab_label, "tab-label");
9500 gtkconv->menu_tabby = gtk_hbox_new(FALSE, PIDGIN_HIG_BOX_SPACE);
9501 gtkconv->menu_label = gtk_label_new(tmp_lab);
9502 gtk_box_pack_start(GTK_BOX(gtkconv->menu_tabby), gtkconv->menu_icon, FALSE, FALSE, 0);
9504 gtk_widget_show_all(gtkconv->menu_icon);
9506 gtk_box_pack_start(GTK_BOX(gtkconv->menu_tabby), gtkconv->menu_label, TRUE, TRUE, 0);
9507 gtk_widget_show(gtkconv->menu_label);
9508 gtk_misc_set_alignment(GTK_MISC(gtkconv->menu_label), 0, 0);
9510 gtk_widget_show(gtkconv->menu_tabby);
9512 if (conv_type == PURPLE_CONV_TYPE_IM)
9513 pidgin_conv_update_buddy_icon(conv);
9515 /* Build and set conversations tab */
9516 pidgin_conv_tab_pack(win, gtkconv);
9518 gtk_notebook_set_menu_label(GTK_NOTEBOOK(win->notebook), tab_cont, gtkconv->menu_tabby);
9520 gtk_widget_show(tab_cont);
9522 if (pidgin_conv_window_get_gtkconv_count(win) == 1) {
9523 /* Er, bug in notebooks? Switch to the page manually. */
9524 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), 0);
9525 } else {
9526 gtk_notebook_set_show_tabs(GTK_NOTEBOOK(win->notebook), TRUE);
9529 focus_gtkconv = g_list_nth_data(pidgin_conv_window_get_gtkconvs(win),
9530 gtk_notebook_get_current_page(GTK_NOTEBOOK(win->notebook)));
9531 gtk_widget_grab_focus(focus_gtkconv->entry);
9533 if (pidgin_conv_window_get_gtkconv_count(win) == 1)
9534 update_send_to_selection(win);
9537 static void
9538 pidgin_conv_tab_pack(PidginWindow *win, PidginConversation *gtkconv)
9540 gboolean tabs_side = FALSE;
9541 gint angle = 0;
9542 GtkWidget *first, *third, *ebox;
9544 if (purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") == GTK_POS_LEFT ||
9545 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") == GTK_POS_RIGHT)
9546 tabs_side = TRUE;
9547 else if (purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") == (GTK_POS_LEFT|8))
9548 angle = 90;
9549 else if (purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") == (GTK_POS_RIGHT|8))
9550 angle = 270;
9552 if (!angle) {
9553 g_object_set(G_OBJECT(gtkconv->tab_label), "ellipsize", PANGO_ELLIPSIZE_END, NULL);
9554 gtk_label_set_width_chars(GTK_LABEL(gtkconv->tab_label), 4);
9555 } else {
9556 g_object_set(G_OBJECT(gtkconv->tab_label), "ellipsize", PANGO_ELLIPSIZE_NONE, NULL);
9557 gtk_label_set_width_chars(GTK_LABEL(gtkconv->tab_label), -1);
9560 if (tabs_side) {
9561 gtk_label_set_width_chars(
9562 GTK_LABEL(gtkconv->tab_label),
9563 MIN(g_utf8_strlen(gtk_label_get_text(GTK_LABEL(gtkconv->tab_label)), -1), 12)
9567 gtk_label_set_angle(GTK_LABEL(gtkconv->tab_label), angle);
9569 #if 0
9570 gtk_misc_set_alignment(GTK_MISC(gtkconv->tab_label), 0.00, 0.5);
9571 gtk_misc_set_padding(GTK_MISC(gtkconv->tab_label), 4, 0);
9572 #endif
9574 if (angle)
9575 gtkconv->tabby = gtk_vbox_new(FALSE, PIDGIN_HIG_BOX_SPACE);
9576 else
9577 gtkconv->tabby = gtk_hbox_new(FALSE, PIDGIN_HIG_BOX_SPACE);
9578 gtk_widget_set_name(gtkconv->tabby, "tab-container");
9580 /* select the correct ordering for verticle tabs */
9581 if (angle == 90) {
9582 first = gtkconv->close;
9583 third = gtkconv->icon;
9584 } else {
9585 first = gtkconv->icon;
9586 third = gtkconv->close;
9589 ebox = gtk_event_box_new();
9590 gtk_event_box_set_visible_window(GTK_EVENT_BOX(ebox), FALSE);
9591 gtk_container_add(GTK_CONTAINER(ebox), gtkconv->tabby);
9592 g_signal_connect(G_OBJECT(ebox), "enter-notify-event",
9593 G_CALLBACK(gtkconv_tab_set_tip), gtkconv);
9595 if (gtkconv->tab_label->parent == NULL) {
9596 /* Pack if it's a new widget */
9597 gtk_box_pack_start(GTK_BOX(gtkconv->tabby), first, FALSE, FALSE, 0);
9598 gtk_box_pack_start(GTK_BOX(gtkconv->tabby), gtkconv->tab_label, TRUE, TRUE, 0);
9599 gtk_box_pack_start(GTK_BOX(gtkconv->tabby), third, FALSE, FALSE, 0);
9601 /* Add this pane to the conversation's notebook. */
9602 gtk_notebook_append_page(GTK_NOTEBOOK(win->notebook), gtkconv->tab_cont, ebox);
9603 } else {
9604 /* reparent old widgets on preference changes */
9605 gtk_widget_reparent(first, gtkconv->tabby);
9606 gtk_widget_reparent(gtkconv->tab_label, gtkconv->tabby);
9607 gtk_widget_reparent(third, gtkconv->tabby);
9608 gtk_box_set_child_packing(GTK_BOX(gtkconv->tabby), first, FALSE, FALSE, 0, GTK_PACK_START);
9609 gtk_box_set_child_packing(GTK_BOX(gtkconv->tabby), gtkconv->tab_label, TRUE, TRUE, 0, GTK_PACK_START);
9610 gtk_box_set_child_packing(GTK_BOX(gtkconv->tabby), third, FALSE, FALSE, 0, GTK_PACK_START);
9612 /* Reset the tabs label to the new version */
9613 gtk_notebook_set_tab_label(GTK_NOTEBOOK(win->notebook), gtkconv->tab_cont, ebox);
9616 gtk_notebook_set_tab_label_packing(GTK_NOTEBOOK(win->notebook), gtkconv->tab_cont,
9617 !tabs_side && !angle,
9618 TRUE, GTK_PACK_START);
9620 if (pidgin_conv_window_get_gtkconv_count(win) == 1)
9621 gtk_notebook_set_show_tabs(GTK_NOTEBOOK(win->notebook),
9622 purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/tabs") &&
9623 (!purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons") ||
9624 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") != GTK_POS_TOP));
9626 /* show the widgets */
9627 /* gtk_widget_show(gtkconv->icon); */
9628 gtk_widget_show(gtkconv->tab_label);
9629 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/close_on_tabs"))
9630 gtk_widget_show(gtkconv->close);
9631 gtk_widget_show(gtkconv->tabby);
9632 gtk_widget_show(ebox);
9635 void
9636 pidgin_conv_window_remove_gtkconv(PidginWindow *win, PidginConversation *gtkconv)
9638 unsigned int index;
9640 index = gtk_notebook_page_num(GTK_NOTEBOOK(win->notebook), gtkconv->tab_cont);
9642 g_object_ref(gtkconv->tab_cont);
9643 gtk_object_sink(GTK_OBJECT(gtkconv->tab_cont));
9645 gtk_notebook_remove_page(GTK_NOTEBOOK(win->notebook), index);
9647 win->gtkconvs = g_list_remove(win->gtkconvs, gtkconv);
9649 g_signal_handlers_disconnect_matched(win->window, G_SIGNAL_MATCH_DATA,
9650 0, 0, NULL, NULL, gtkconv);
9652 if (win->gtkconvs && win->gtkconvs->next == NULL)
9653 pidgin_conv_tab_pack(win, win->gtkconvs->data);
9655 if (!win->gtkconvs && win != hidden_convwin)
9656 pidgin_conv_window_destroy(win);
9659 PidginConversation *
9660 pidgin_conv_window_get_gtkconv_at_index(const PidginWindow *win, int index)
9662 GtkWidget *tab_cont;
9664 if (index == -1)
9665 index = 0;
9666 tab_cont = gtk_notebook_get_nth_page(GTK_NOTEBOOK(win->notebook), index);
9667 return tab_cont ? g_object_get_data(G_OBJECT(tab_cont), "PidginConversation") : NULL;
9670 PidginConversation *
9671 pidgin_conv_window_get_active_gtkconv(const PidginWindow *win)
9673 int index;
9674 GtkWidget *tab_cont;
9676 index = gtk_notebook_get_current_page(GTK_NOTEBOOK(win->notebook));
9677 if (index == -1)
9678 index = 0;
9679 tab_cont = gtk_notebook_get_nth_page(GTK_NOTEBOOK(win->notebook), index);
9680 if (!tab_cont)
9681 return NULL;
9682 return g_object_get_data(G_OBJECT(tab_cont), "PidginConversation");
9686 PurpleConversation *
9687 pidgin_conv_window_get_active_conversation(const PidginWindow *win)
9689 PidginConversation *gtkconv;
9691 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
9692 return gtkconv ? gtkconv->active_conv : NULL;
9695 gboolean
9696 pidgin_conv_window_is_active_conversation(const PurpleConversation *conv)
9698 return conv == pidgin_conv_window_get_active_conversation(PIDGIN_CONVERSATION(conv)->win);
9701 gboolean
9702 pidgin_conv_window_has_focus(PidginWindow *win)
9704 gboolean has_focus = FALSE;
9706 g_object_get(G_OBJECT(win->window), "has-toplevel-focus", &has_focus, NULL);
9708 return has_focus;
9711 PidginWindow *
9712 pidgin_conv_window_get_at_xy(int x, int y)
9714 PidginWindow *win;
9715 GdkWindow *gdkwin;
9716 GList *l;
9718 gdkwin = gdk_window_at_pointer(&x, &y);
9720 if (gdkwin)
9721 gdkwin = gdk_window_get_toplevel(gdkwin);
9723 for (l = pidgin_conv_windows_get_list(); l != NULL; l = l->next) {
9724 win = l->data;
9726 if (gdkwin == win->window->window)
9727 return win;
9730 return NULL;
9733 GList *
9734 pidgin_conv_window_get_gtkconvs(PidginWindow *win)
9736 return win->gtkconvs;
9739 guint
9740 pidgin_conv_window_get_gtkconv_count(PidginWindow *win)
9742 return g_list_length(win->gtkconvs);
9745 PidginWindow *
9746 pidgin_conv_window_first_with_type(PurpleConversationType type)
9748 GList *wins, *convs;
9749 PidginWindow *win;
9750 PidginConversation *conv;
9752 if (type == PURPLE_CONV_TYPE_UNKNOWN)
9753 return NULL;
9755 for (wins = pidgin_conv_windows_get_list(); wins != NULL; wins = wins->next) {
9756 win = wins->data;
9758 for (convs = win->gtkconvs;
9759 convs != NULL;
9760 convs = convs->next) {
9762 conv = convs->data;
9764 if (purple_conversation_get_type(conv->active_conv) == type)
9765 return win;
9769 return NULL;
9772 PidginWindow *
9773 pidgin_conv_window_last_with_type(PurpleConversationType type)
9775 GList *wins, *convs;
9776 PidginWindow *win;
9777 PidginConversation *conv;
9779 if (type == PURPLE_CONV_TYPE_UNKNOWN)
9780 return NULL;
9782 for (wins = g_list_last(pidgin_conv_windows_get_list());
9783 wins != NULL;
9784 wins = wins->prev) {
9786 win = wins->data;
9788 for (convs = win->gtkconvs;
9789 convs != NULL;
9790 convs = convs->next) {
9792 conv = convs->data;
9794 if (purple_conversation_get_type(conv->active_conv) == type)
9795 return win;
9799 return NULL;
9803 /**************************************************************************
9804 * Conversation placement functions
9805 **************************************************************************/
9806 typedef struct
9808 char *id;
9809 char *name;
9810 PidginConvPlacementFunc fnc;
9812 } ConvPlacementData;
9814 static GList *conv_placement_fncs = NULL;
9815 static PidginConvPlacementFunc place_conv = NULL;
9817 /* This one places conversations in the last made window. */
9818 static void
9819 conv_placement_last_created_win(PidginConversation *conv)
9821 PidginWindow *win;
9823 GList *l = g_list_last(pidgin_conv_windows_get_list());
9824 win = l ? l->data : NULL;;
9826 if (win == NULL) {
9827 win = pidgin_conv_window_new();
9829 g_signal_connect(G_OBJECT(win->window), "configure_event",
9830 G_CALLBACK(gtk_conv_configure_cb), NULL);
9832 pidgin_conv_window_add_gtkconv(win, conv);
9833 pidgin_conv_window_show(win);
9834 } else {
9835 pidgin_conv_window_add_gtkconv(win, conv);
9839 /* This one places conversations in the last made window of the same type. */
9840 static gboolean
9841 conv_placement_last_created_win_type_configured_cb(GtkWidget *w,
9842 GdkEventConfigure *event, PidginConversation *conv)
9844 int x, y;
9845 PurpleConversationType type = purple_conversation_get_type(conv->active_conv);
9846 GList *all;
9848 if (GTK_WIDGET_VISIBLE(w))
9849 gtk_window_get_position(GTK_WINDOW(w), &x, &y);
9850 else
9851 return FALSE; /* carry on normally */
9853 /* Workaround for GTK+ bug # 169811 - "configure_event" is fired
9854 * when the window is being maximized */
9855 if (gdk_window_get_state(w->window) & GDK_WINDOW_STATE_MAXIMIZED)
9856 return FALSE;
9858 /* don't save off-screen positioning */
9859 if (x + event->width < 0 ||
9860 y + event->height < 0 ||
9861 x > gdk_screen_width() ||
9862 y > gdk_screen_height())
9863 return FALSE; /* carry on normally */
9865 for (all = conv->convs; all != NULL; all = all->next) {
9866 if (type != purple_conversation_get_type(all->data)) {
9867 /* this window has different types of conversation, don't save */
9868 return FALSE;
9872 if (type == PURPLE_CONV_TYPE_IM) {
9873 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/x", x);
9874 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/y", y);
9875 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/width", event->width);
9876 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/height", event->height);
9877 } else if (type == PURPLE_CONV_TYPE_CHAT) {
9878 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/chat/x", x);
9879 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/chat/y", y);
9880 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/chat/width", event->width);
9881 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/chat/height", event->height);
9884 return FALSE;
9887 static void
9888 conv_placement_last_created_win_type(PidginConversation *conv)
9890 PidginWindow *win;
9892 win = pidgin_conv_window_last_with_type(purple_conversation_get_type(conv->active_conv));
9894 if (win == NULL) {
9895 win = pidgin_conv_window_new();
9897 if (PURPLE_CONV_TYPE_IM == purple_conversation_get_type(conv->active_conv) ||
9898 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/width") == 0) {
9899 pidgin_conv_set_position_size(win,
9900 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/x"),
9901 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/y"),
9902 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/width"),
9903 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/height"));
9904 } else if (PURPLE_CONV_TYPE_CHAT == purple_conversation_get_type(conv->active_conv)) {
9905 pidgin_conv_set_position_size(win,
9906 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/x"),
9907 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/y"),
9908 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/width"),
9909 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/height"));
9912 pidgin_conv_window_add_gtkconv(win, conv);
9913 pidgin_conv_window_show(win);
9915 g_signal_connect(G_OBJECT(win->window), "configure_event",
9916 G_CALLBACK(conv_placement_last_created_win_type_configured_cb), conv);
9917 } else
9918 pidgin_conv_window_add_gtkconv(win, conv);
9921 /* This one places each conversation in its own window. */
9922 static void
9923 conv_placement_new_window(PidginConversation *conv)
9925 PidginWindow *win;
9927 win = pidgin_conv_window_new();
9929 g_signal_connect(G_OBJECT(win->window), "configure_event",
9930 G_CALLBACK(gtk_conv_configure_cb), NULL);
9932 pidgin_conv_window_add_gtkconv(win, conv);
9934 pidgin_conv_window_show(win);
9937 static PurpleGroup *
9938 conv_get_group(PidginConversation *conv)
9940 PurpleGroup *group = NULL;
9942 if (purple_conversation_get_type(conv->active_conv) == PURPLE_CONV_TYPE_IM) {
9943 PurpleBuddy *buddy;
9945 buddy = purple_find_buddy(purple_conversation_get_account(conv->active_conv),
9946 purple_conversation_get_name(conv->active_conv));
9948 if (buddy != NULL)
9949 group = purple_buddy_get_group(buddy);
9951 } else if (purple_conversation_get_type(conv->active_conv) == PURPLE_CONV_TYPE_CHAT) {
9952 PurpleChat *chat;
9954 chat = purple_blist_find_chat(purple_conversation_get_account(conv->active_conv),
9955 purple_conversation_get_name(conv->active_conv));
9957 if (chat != NULL)
9958 group = purple_chat_get_group(chat);
9961 return group;
9965 * This groups things by, well, group. Buddies from groups will always be
9966 * grouped together, and a buddy from a group not belonging to any currently
9967 * open windows will get a new window.
9969 static void
9970 conv_placement_by_group(PidginConversation *conv)
9972 PurpleGroup *group = NULL;
9973 GList *wl, *cl;
9975 group = conv_get_group(conv);
9977 /* Go through the list of IMs and find one with this group. */
9978 for (wl = pidgin_conv_windows_get_list(); wl != NULL; wl = wl->next) {
9979 PidginWindow *win2;
9980 PidginConversation *conv2;
9981 PurpleGroup *group2 = NULL;
9983 win2 = wl->data;
9985 for (cl = win2->gtkconvs;
9986 cl != NULL;
9987 cl = cl->next) {
9988 conv2 = cl->data;
9990 group2 = conv_get_group(conv2);
9992 if (group == group2) {
9993 pidgin_conv_window_add_gtkconv(win2, conv);
9995 return;
10000 /* Make a new window. */
10001 conv_placement_new_window(conv);
10004 /* This groups things by account. Otherwise, the same semantics as above */
10005 static void
10006 conv_placement_by_account(PidginConversation *conv)
10008 GList *wins, *convs;
10009 PurpleAccount *account;
10011 account = purple_conversation_get_account(conv->active_conv);
10013 /* Go through the list of IMs and find one with this group. */
10014 for (wins = pidgin_conv_windows_get_list(); wins != NULL; wins = wins->next) {
10015 PidginWindow *win2;
10016 PidginConversation *conv2;
10018 win2 = wins->data;
10020 for (convs = win2->gtkconvs;
10021 convs != NULL;
10022 convs = convs->next) {
10023 conv2 = convs->data;
10025 if (account == purple_conversation_get_account(conv2->active_conv)) {
10026 pidgin_conv_window_add_gtkconv(win2, conv);
10027 return;
10032 /* Make a new window. */
10033 conv_placement_new_window(conv);
10036 static ConvPlacementData *
10037 get_conv_placement_data(const char *id)
10039 ConvPlacementData *data = NULL;
10040 GList *n;
10042 for (n = conv_placement_fncs; n; n = n->next) {
10043 data = n->data;
10044 if (!strcmp(data->id, id))
10045 return data;
10048 return NULL;
10051 static void
10052 add_conv_placement_fnc(const char *id, const char *name,
10053 PidginConvPlacementFunc fnc)
10055 ConvPlacementData *data;
10057 data = g_new(ConvPlacementData, 1);
10059 data->id = g_strdup(id);
10060 data->name = g_strdup(name);
10061 data->fnc = fnc;
10063 conv_placement_fncs = g_list_append(conv_placement_fncs, data);
10066 static void
10067 ensure_default_funcs(void)
10069 if (conv_placement_fncs == NULL) {
10070 add_conv_placement_fnc("last", _("Last created window"),
10071 conv_placement_last_created_win);
10072 add_conv_placement_fnc("im_chat", _("Separate IM and Chat windows"),
10073 conv_placement_last_created_win_type);
10074 add_conv_placement_fnc("new", _("New window"),
10075 conv_placement_new_window);
10076 add_conv_placement_fnc("group", _("By group"),
10077 conv_placement_by_group);
10078 add_conv_placement_fnc("account", _("By account"),
10079 conv_placement_by_account);
10083 GList *
10084 pidgin_conv_placement_get_options(void)
10086 GList *n, *list = NULL;
10087 ConvPlacementData *data;
10089 ensure_default_funcs();
10091 for (n = conv_placement_fncs; n; n = n->next) {
10092 data = n->data;
10093 list = g_list_append(list, data->name);
10094 list = g_list_append(list, data->id);
10097 return list;
10101 void
10102 pidgin_conv_placement_add_fnc(const char *id, const char *name,
10103 PidginConvPlacementFunc fnc)
10105 g_return_if_fail(id != NULL);
10106 g_return_if_fail(name != NULL);
10107 g_return_if_fail(fnc != NULL);
10109 ensure_default_funcs();
10111 add_conv_placement_fnc(id, name, fnc);
10114 void
10115 pidgin_conv_placement_remove_fnc(const char *id)
10117 ConvPlacementData *data = get_conv_placement_data(id);
10119 if (data == NULL)
10120 return;
10122 conv_placement_fncs = g_list_remove(conv_placement_fncs, data);
10124 g_free(data->id);
10125 g_free(data->name);
10126 g_free(data);
10129 const char *
10130 pidgin_conv_placement_get_name(const char *id)
10132 ConvPlacementData *data;
10134 ensure_default_funcs();
10136 data = get_conv_placement_data(id);
10138 if (data == NULL)
10139 return NULL;
10141 return data->name;
10144 PidginConvPlacementFunc
10145 pidgin_conv_placement_get_fnc(const char *id)
10147 ConvPlacementData *data;
10149 ensure_default_funcs();
10151 data = get_conv_placement_data(id);
10153 if (data == NULL)
10154 return NULL;
10156 return data->fnc;
10159 void
10160 pidgin_conv_placement_set_current_func(PidginConvPlacementFunc func)
10162 g_return_if_fail(func != NULL);
10164 /* If tabs are enabled, set the function, otherwise, NULL it out. */
10165 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/tabs"))
10166 place_conv = func;
10167 else
10168 place_conv = NULL;
10171 PidginConvPlacementFunc
10172 pidgin_conv_placement_get_current_func(void)
10174 return place_conv;
10177 void
10178 pidgin_conv_placement_place(PidginConversation *gtkconv)
10180 if (place_conv)
10181 place_conv(gtkconv);
10182 else
10183 conv_placement_new_window(gtkconv);
10186 gboolean
10187 pidgin_conv_is_hidden(PidginConversation *gtkconv)
10189 g_return_val_if_fail(gtkconv != NULL, FALSE);
10191 return (gtkconv->win == hidden_convwin);
10195 /* Algorithm from http://www.w3.org/TR/AERT#color-contrast */
10196 static gboolean
10197 color_is_visible(GdkColor foreground, GdkColor background, int color_contrast, int brightness_contrast)
10199 gulong fg_brightness;
10200 gulong bg_brightness;
10201 gulong br_diff;
10202 gulong col_diff;
10203 int fred, fgreen, fblue, bred, bgreen, bblue;
10205 /* this algorithm expects colors between 0 and 255 for each of red green and blue.
10206 * GTK on the other hand has values between 0 and 65535
10207 * Err suggested I >> 8, which grabbed the high bits.
10210 fred = foreground.red >> 8 ;
10211 fgreen = foreground.green >> 8 ;
10212 fblue = foreground.blue >> 8 ;
10215 bred = background.red >> 8 ;
10216 bgreen = background.green >> 8 ;
10217 bblue = background.blue >> 8 ;
10219 fg_brightness = (fred * 299 + fgreen * 587 + fblue * 114) / 1000;
10220 bg_brightness = (bred * 299 + bgreen * 587 + bblue * 114) / 1000;
10221 br_diff = abs(fg_brightness - bg_brightness);
10223 col_diff = abs(fred - bred) + abs(fgreen - bgreen) + abs(fblue - bblue);
10225 return ((col_diff > color_contrast) && (br_diff > brightness_contrast));
10229 static GdkColor*
10230 generate_nick_colors(guint *color_count, GdkColor background)
10232 guint numcolors = *color_count;
10233 guint i = 0, j = 0;
10234 GdkColor *colors = g_new(GdkColor, numcolors);
10235 GdkColor nick_highlight;
10236 GdkColor send_color;
10237 time_t breakout_time;
10239 gdk_color_parse(DEFAULT_HIGHLIGHT_COLOR, &nick_highlight);
10240 gdk_color_parse(DEFAULT_SEND_COLOR, &send_color);
10242 srand(background.red + background.green + background.blue + 1);
10244 breakout_time = time(NULL) + 3;
10246 /* first we look through the list of "good" colors: colors that differ from every other color in the
10247 * list. only some of them will differ from the background color though. lets see if we can find
10248 * numcolors of them that do
10250 while (i < numcolors && j < NUM_NICK_SEED_COLORS && time(NULL) < breakout_time)
10252 GdkColor color = nick_seed_colors[j];
10254 if (color_is_visible(color, background, MIN_COLOR_CONTRAST, MIN_BRIGHTNESS_CONTRAST) &&
10255 color_is_visible(color, nick_highlight, MIN_COLOR_CONTRAST / 2, 0) &&
10256 color_is_visible(color, send_color, MIN_COLOR_CONTRAST / 4, 0))
10258 colors[i] = color;
10259 i++;
10261 j++;
10264 /* we might not have found numcolors in the last loop. if we did, we'll never enter this one.
10265 * if we did not, lets just find some colors that don't conflict with the background. its
10266 * expensive to find colors that not only don't conflict with the background, but also do not
10267 * conflict with each other.
10269 while(i < numcolors && time(NULL) < breakout_time)
10271 GdkColor color = { 0, rand() % 65536, rand() % 65536, rand() % 65536 };
10273 if (color_is_visible(color, background, MIN_COLOR_CONTRAST, MIN_BRIGHTNESS_CONTRAST) &&
10274 color_is_visible(color, nick_highlight, MIN_COLOR_CONTRAST / 2, 0) &&
10275 color_is_visible(color, send_color, MIN_COLOR_CONTRAST / 4, 0))
10277 colors[i] = color;
10278 i++;
10282 if (i < numcolors) {
10283 GdkColor *c = colors;
10284 purple_debug_warning("gtkconv", "Unable to generate enough random colors before timeout. %u colors found.\n", i);
10285 colors = g_memdup(c, i * sizeof(GdkColor));
10286 g_free(c);
10287 *color_count = i;
10290 return colors;