Use g_strcmp0() for code simplification
[pidgin-git.git] / pidgin / gtkconv.c
blobb98c6375ef5977c5424be239840f48602bdffcc5
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 #ifdef USE_GTKSPELL
33 # include <gtkspell/gtkspell.h>
34 # ifdef _WIN32
35 # include "wspell.h"
36 # endif
37 #endif
39 #include <gdk/gdkkeysyms.h>
41 #include "account.h"
42 #include "cmds.h"
43 #include "core.h"
44 #include "debug.h"
45 #include "idle.h"
46 #include "imgstore.h"
47 #include "log.h"
48 #include "notify.h"
49 #include "prpl.h"
50 #include "request.h"
51 #include "util.h"
52 #include "version.h"
54 #include "gtkdnd-hints.h"
55 #include "gtkblist.h"
56 #include "gtkconv.h"
57 #include "gtkconvwin.h"
58 #include "gtkdialogs.h"
59 #include "gtkimhtml.h"
60 #include "gtkimhtmltoolbar.h"
61 #include "gtklog.h"
62 #include "gtkmenutray.h"
63 #include "gtkpounce.h"
64 #include "gtkprefs.h"
65 #include "gtkprivacy.h"
66 #include "gtkthemes.h"
67 #include "gtkutils.h"
68 #include "pidginstock.h"
69 #include "pidgintooltip.h"
71 #include "gtknickcolors.h"
73 #define CLOSE_CONV_TIMEOUT_SECS (10 * 60)
75 #define AUTO_RESPONSE "&lt;AUTO-REPLY&gt; : "
77 typedef enum
79 PIDGIN_CONV_SET_TITLE = 1 << 0,
80 PIDGIN_CONV_BUDDY_ICON = 1 << 1,
81 PIDGIN_CONV_MENU = 1 << 2,
82 PIDGIN_CONV_TAB_ICON = 1 << 3,
83 PIDGIN_CONV_TOPIC = 1 << 4,
84 PIDGIN_CONV_SMILEY_THEME = 1 << 5,
85 PIDGIN_CONV_COLORIZE_TITLE = 1 << 6
86 }PidginConvFields;
88 enum {
89 CONV_ICON_COLUMN,
90 CONV_TEXT_COLUMN,
91 CONV_EMBLEM_COLUMN,
92 CONV_PROTOCOL_ICON_COLUMN,
93 CONV_NUM_COLUMNS
94 } PidginInfopaneColumns;
96 #define PIDGIN_CONV_ALL ((1 << 7) - 1)
98 /* XXX: These color defines shouldn't really be here. But the nick-color
99 * generation algorithm uses them, so keeping these around until we fix that. */
100 #define DEFAULT_SEND_COLOR "#204a87"
101 #define DEFAULT_HIGHLIGHT_COLOR "#AF7F00"
103 #define BUDDYICON_SIZE_MIN 32
104 #define BUDDYICON_SIZE_MAX 96
106 /* Undef this to turn off "custom-smiley" debug messages */
107 #define DEBUG_CUSTOM_SMILEY
109 #define LUMINANCE(c) (float)((0.3*(c.red))+(0.59*(c.green))+(0.11*(c.blue)))
111 /* From http://www.w3.org/TR/AERT#color-contrast */
112 #define MIN_BRIGHTNESS_CONTRAST 75
113 #define MIN_COLOR_CONTRAST 200
115 #define NUM_NICK_COLORS 220
116 static GdkColor *nick_colors = NULL;
117 static guint nbr_nick_colors;
119 typedef struct {
120 GtkWidget *window;
122 GtkWidget *entry;
123 GtkWidget *message;
125 PurpleConversation *conv;
127 } InviteBuddyInfo;
129 static GtkWidget *invite_dialog = NULL;
130 static GtkWidget *warn_close_dialog = NULL;
132 static PidginWindow *hidden_convwin = NULL;
133 static GList *window_list = NULL;
135 /* Lists of status icons at all available sizes for use as window icons */
136 static GList *available_list = NULL;
137 static GList *away_list = NULL;
138 static GList *busy_list = NULL;
139 static GList *xa_list = NULL;
140 static GList *offline_list = NULL;
141 static GHashTable *prpl_lists = NULL;
143 static gboolean update_send_to_selection(PidginWindow *win);
144 static void generate_send_to_items(PidginWindow *win);
146 /* Prototypes. <-- because Paco-Paco hates this comment. */
147 static gboolean infopane_entry_activate(PidginConversation *gtkconv);
148 static void got_typing_keypress(PidginConversation *gtkconv, gboolean first);
149 static void gray_stuff_out(PidginConversation *gtkconv);
150 static void add_chat_buddy_common(PurpleConversation *conv, PurpleConvChatBuddy *cb, const char *old_name);
151 static gboolean tab_complete(PurpleConversation *conv);
152 static void pidgin_conv_updated(PurpleConversation *conv, PurpleConvUpdateType type);
153 static void conv_set_unseen(PurpleConversation *gtkconv, PidginUnseenState state);
154 static void gtkconv_set_unseen(PidginConversation *gtkconv, PidginUnseenState state);
155 static void update_typing_icon(PidginConversation *gtkconv);
156 static void update_typing_message(PidginConversation *gtkconv, const char *message);
157 static const char *item_factory_translate_func (const char *path, gpointer func_data);
158 gboolean pidgin_conv_has_focus(PurpleConversation *conv);
159 static GdkColor* generate_nick_colors(guint *numcolors, GdkColor background);
160 static gboolean color_is_visible(GdkColor foreground, GdkColor background, guint color_contrast, guint brightness_contrast);
161 static GtkTextTag *get_buddy_tag(PurpleConversation *conv, const char *who, PurpleMessageFlags flag, gboolean create);
162 static void pidgin_conv_update_fields(PurpleConversation *conv, PidginConvFields fields);
163 static void focus_out_from_menubar(GtkWidget *wid, PidginWindow *win);
164 static void pidgin_conv_tab_pack(PidginWindow *win, PidginConversation *gtkconv);
165 static gboolean infopane_press_cb(GtkWidget *widget, GdkEventButton *e, PidginConversation *conv);
166 static void hide_conv(PidginConversation *gtkconv, gboolean closetimer);
168 static void pidgin_conv_set_position_size(PidginWindow *win, int x, int y,
169 int width, int height);
170 static gboolean pidgin_conv_xy_to_right_infopane(PidginWindow *win, int x, int y);
172 static const GdkColor *get_nick_color(PidginConversation *gtkconv, const char *name)
174 static GdkColor col;
175 GtkStyle *style = gtk_widget_get_style(gtkconv->imhtml);
176 float scale;
178 col = nick_colors[g_str_hash(name) % nbr_nick_colors];
179 scale = ((1-(LUMINANCE(style->base[GTK_STATE_NORMAL]) / LUMINANCE(style->white))) *
180 (LUMINANCE(style->white)/MAX(MAX(col.red, col.blue), col.green)));
182 /* The colors are chosen to look fine on white; we should never have to darken */
183 if (scale > 1) {
184 col.red *= scale;
185 col.green *= scale;
186 col.blue *= scale;
189 return &col;
192 static PurpleBlistNode *
193 get_conversation_blist_node(PurpleConversation *conv)
195 PurpleBlistNode *node = NULL;
197 switch (purple_conversation_get_type(conv)) {
198 case PURPLE_CONV_TYPE_IM:
199 node = PURPLE_BLIST_NODE(purple_find_buddy(conv->account, conv->name));
200 node = node ? node->parent : NULL;
201 break;
202 case PURPLE_CONV_TYPE_CHAT:
203 node = PURPLE_BLIST_NODE(purple_blist_find_chat(conv->account, conv->name));
204 break;
205 default:
206 break;
208 return node;
211 /**************************************************************************
212 * Callbacks
213 **************************************************************************/
215 static gboolean
216 close_this_sucker(gpointer data)
218 PidginConversation *gtkconv = data;
219 GList *list = g_list_copy(gtkconv->convs);
220 g_list_foreach(list, (GFunc)purple_conversation_destroy, NULL);
221 g_list_free(list);
222 return FALSE;
225 static gboolean
226 close_conv_cb(GtkButton *button, PidginConversation *gtkconv)
228 /* We are going to destroy the conversations immediately only if the 'close immediately'
229 * preference is selected. Otherwise, close the conversation after a reasonable timeout
230 * (I am going to consider 10 minutes as a 'reasonable timeout' here.
231 * For chats, close immediately if the chat is not in the buddylist, or if the chat is
232 * not marked 'Persistent' */
233 PurpleConversation *conv = gtkconv->active_conv;
234 PurpleAccount *account = purple_conversation_get_account(conv);
235 const char *name = purple_conversation_get_name(conv);
237 switch (purple_conversation_get_type(conv)) {
238 case PURPLE_CONV_TYPE_IM:
240 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/close_immediately"))
241 close_this_sucker(gtkconv);
242 else
243 hide_conv(gtkconv, TRUE);
244 break;
246 case PURPLE_CONV_TYPE_CHAT:
248 PurpleChat *chat = purple_blist_find_chat(account, name);
249 if (!chat ||
250 !purple_blist_node_get_bool(&chat->node, "gtk-persistent"))
251 close_this_sucker(gtkconv);
252 else
253 hide_conv(gtkconv, FALSE);
254 break;
256 default:
260 return TRUE;
263 static gboolean
264 lbox_size_allocate_cb(GtkWidget *w, GtkAllocation *allocation, gpointer data)
266 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/chat/userlist_width", allocation->width == 1 ? 0 : allocation->width);
268 return FALSE;
271 static void
272 default_formatize(PidginConversation *c)
274 PurpleConversation *conv = c->active_conv;
275 gtk_imhtml_setup_entry(GTK_IMHTML(c->entry), conv->features);
278 static void
279 conversation_entry_clear(PidginConversation *gtkconv)
281 GtkIMHtml *imhtml = GTK_IMHTML(gtkconv->entry);
282 gtk_source_undo_manager_begin_not_undoable_action(imhtml->undo_manager);
283 gtk_imhtml_clear(imhtml);
284 gtk_source_undo_manager_end_not_undoable_action(imhtml->undo_manager);
287 static void
288 clear_formatting_cb(GtkIMHtml *imhtml, PidginConversation *gtkconv)
290 default_formatize(gtkconv);
293 static const char *
294 pidgin_get_cmd_prefix(void)
296 return "/";
299 static PurpleCmdRet
300 say_command_cb(PurpleConversation *conv,
301 const char *cmd, char **args, char **error, void *data)
303 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM)
304 purple_conv_im_send(PURPLE_CONV_IM(conv), args[0]);
305 else if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT)
306 purple_conv_chat_send(PURPLE_CONV_CHAT(conv), args[0]);
308 return PURPLE_CMD_RET_OK;
311 static PurpleCmdRet
312 me_command_cb(PurpleConversation *conv,
313 const char *cmd, char **args, char **error, void *data)
315 char *tmp;
317 tmp = g_strdup_printf("/me %s", args[0]);
319 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM)
320 purple_conv_im_send(PURPLE_CONV_IM(conv), tmp);
321 else if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT)
322 purple_conv_chat_send(PURPLE_CONV_CHAT(conv), tmp);
324 g_free(tmp);
325 return PURPLE_CMD_RET_OK;
328 static PurpleCmdRet
329 debug_command_cb(PurpleConversation *conv,
330 const char *cmd, char **args, char **error, void *data)
332 char *tmp, *markup;
334 if (!g_ascii_strcasecmp(args[0], "version")) {
335 tmp = g_strdup_printf("Using Pidgin v%s with libpurple v%s.",
336 DISPLAY_VERSION, purple_core_get_version());
337 } else if (!g_ascii_strcasecmp(args[0], "plugins")) {
338 /* Show all the loaded plugins, including the protocol plugins and plugin loaders.
339 * This is intentional, since third party prpls are often sources of bugs, and some
340 * plugin loaders (e.g. mono) can also be buggy.
342 GString *str = g_string_new("Loaded Plugins: ");
343 const GList *plugins = purple_plugins_get_loaded();
344 if (plugins) {
345 for (; plugins; plugins = plugins->next) {
346 str = g_string_append(str, purple_plugin_get_name(plugins->data));
347 if (plugins->next)
348 str = g_string_append(str, ", ");
350 } else {
351 str = g_string_append(str, "(none)");
354 tmp = g_string_free(str, FALSE);
355 } else {
356 purple_conversation_write(conv, NULL, _("Supported debug options are: plugins, version"),
357 PURPLE_MESSAGE_NO_LOG|PURPLE_MESSAGE_ERROR, time(NULL));
358 return PURPLE_CMD_RET_OK;
361 markup = g_markup_escape_text(tmp, -1);
362 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM)
363 purple_conv_im_send(PURPLE_CONV_IM(conv), markup);
364 else if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT)
365 purple_conv_chat_send(PURPLE_CONV_CHAT(conv), markup);
367 g_free(tmp);
368 g_free(markup);
369 return PURPLE_CMD_RET_OK;
372 static void clear_conversation_scrollback_cb(PurpleConversation *conv,
373 void *data)
375 PidginConversation *gtkconv = NULL;
377 gtkconv = PIDGIN_CONVERSATION(conv);
378 if (gtkconv)
379 gtk_imhtml_clear(GTK_IMHTML(gtkconv->imhtml));
382 static PurpleCmdRet
383 clear_command_cb(PurpleConversation *conv,
384 const char *cmd, char **args, char **error, void *data)
386 purple_conversation_clear_message_history(conv);
387 return PURPLE_CMD_RET_OK;
390 static PurpleCmdRet
391 clearall_command_cb(PurpleConversation *conv,
392 const char *cmd, char **args, char **error, void *data)
394 purple_conversation_foreach(purple_conversation_clear_message_history);
395 return PURPLE_CMD_RET_OK;
398 static PurpleCmdRet
399 help_command_cb(PurpleConversation *conv,
400 const char *cmd, char **args, char **error, void *data)
402 GList *l, *text;
403 GString *s;
405 if (args[0] != NULL) {
406 s = g_string_new("");
407 text = purple_cmd_help(conv, args[0]);
409 if (text) {
410 for (l = text; l; l = l->next)
411 if (l->next)
412 g_string_append_printf(s, "%s\n", (char *)l->data);
413 else
414 g_string_append_printf(s, "%s", (char *)l->data);
415 } else {
416 g_string_append(s, _("No such command (in this context)."));
418 } else {
419 s = g_string_new(_("Use \"/help &lt;command&gt;\" for help on a specific command.\n"
420 "The following commands are available in this context:\n"));
422 text = purple_cmd_list(conv);
423 for (l = text; l; l = l->next)
424 if (l->next)
425 g_string_append_printf(s, "%s, ", (char *)l->data);
426 else
427 g_string_append_printf(s, "%s.", (char *)l->data);
428 g_list_free(text);
431 purple_conversation_write(conv, NULL, s->str, PURPLE_MESSAGE_NO_LOG, time(NULL));
432 g_string_free(s, TRUE);
434 return PURPLE_CMD_RET_OK;
437 static void
438 send_history_add(PidginConversation *gtkconv, const char *message)
440 GList *first;
442 first = g_list_first(gtkconv->send_history);
443 g_free(first->data);
444 first->data = g_strdup(message);
445 gtkconv->send_history = g_list_prepend(first, NULL);
448 static gboolean
449 check_for_and_do_command(PurpleConversation *conv)
451 PidginConversation *gtkconv;
452 char *cmd;
453 const char *prefix;
454 GtkTextIter start;
455 gboolean retval = FALSE;
457 gtkconv = PIDGIN_CONVERSATION(conv);
458 prefix = pidgin_get_cmd_prefix();
460 cmd = gtk_imhtml_get_text(GTK_IMHTML(gtkconv->entry), NULL, NULL);
461 gtk_text_buffer_get_start_iter(GTK_IMHTML(gtkconv->entry)->text_buffer, &start);
463 if (cmd && (strncmp(cmd, prefix, strlen(prefix)) == 0)
464 && !gtk_text_iter_get_child_anchor(&start)) {
465 PurpleCmdStatus status;
466 char *error, *cmdline, *markup, *send_history;
467 GtkTextIter end;
469 send_history = gtk_imhtml_get_markup(GTK_IMHTML(gtkconv->entry));
470 send_history_add(gtkconv, send_history);
471 g_free(send_history);
473 cmdline = cmd + strlen(prefix);
475 if (purple_strequal(cmdline, "xyzzy")) {
476 purple_conversation_write(conv, "", "Nothing happens",
477 PURPLE_MESSAGE_NO_LOG, time(NULL));
478 g_free(cmd);
479 return TRUE;
482 gtk_text_iter_forward_chars(&start, g_utf8_strlen(prefix, -1));
483 gtk_text_buffer_get_end_iter(GTK_IMHTML(gtkconv->entry)->text_buffer, &end);
484 markup = gtk_imhtml_get_markup_range(GTK_IMHTML(gtkconv->entry), &start, &end);
485 status = purple_cmd_do_command(conv, cmdline, markup, &error);
486 g_free(markup);
488 switch (status) {
489 case PURPLE_CMD_STATUS_OK:
490 retval = TRUE;
491 break;
492 case PURPLE_CMD_STATUS_NOT_FOUND:
494 PurplePluginProtocolInfo *prpl_info = NULL;
495 PurpleConnection *gc;
497 if ((gc = purple_conversation_get_gc(conv)))
498 prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(gc->prpl);
500 if ((prpl_info != NULL) && (prpl_info->options & OPT_PROTO_SLASH_COMMANDS_NATIVE)) {
501 char *spaceslash;
503 /* If the first word in the entered text has a '/' in it, then the user
504 * probably didn't mean it as a command. So send the text as message. */
505 spaceslash = cmdline;
506 while (*spaceslash && *spaceslash != ' ' && *spaceslash != '/')
507 spaceslash++;
509 if (*spaceslash != '/') {
510 purple_conversation_write(conv, "", _("Unknown command."), PURPLE_MESSAGE_NO_LOG, time(NULL));
511 retval = TRUE;
514 break;
516 case PURPLE_CMD_STATUS_WRONG_ARGS:
517 purple_conversation_write(conv, "", _("Syntax Error: You typed the wrong number of arguments "
518 "to that command."),
519 PURPLE_MESSAGE_NO_LOG, time(NULL));
520 retval = TRUE;
521 break;
522 case PURPLE_CMD_STATUS_FAILED:
523 purple_conversation_write(conv, "", error ? error : _("Your command failed for an unknown reason."),
524 PURPLE_MESSAGE_NO_LOG, time(NULL));
525 g_free(error);
526 retval = TRUE;
527 break;
528 case PURPLE_CMD_STATUS_WRONG_TYPE:
529 if(purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM)
530 purple_conversation_write(conv, "", _("That command only works in chats, not IMs."),
531 PURPLE_MESSAGE_NO_LOG, time(NULL));
532 else
533 purple_conversation_write(conv, "", _("That command only works in IMs, not chats."),
534 PURPLE_MESSAGE_NO_LOG, time(NULL));
535 retval = TRUE;
536 break;
537 case PURPLE_CMD_STATUS_WRONG_PRPL:
538 purple_conversation_write(conv, "", _("That command doesn't work on this protocol."),
539 PURPLE_MESSAGE_NO_LOG, time(NULL));
540 retval = TRUE;
541 break;
545 g_free(cmd);
546 return retval;
549 static void
550 send_cb(GtkWidget *widget, PidginConversation *gtkconv)
552 PurpleConversation *conv = gtkconv->active_conv;
553 PurpleAccount *account;
554 PurpleConnection *gc;
555 PurpleMessageFlags flags = 0;
556 char *buf, *clean;
558 account = purple_conversation_get_account(conv);
560 if (check_for_and_do_command(conv)) {
561 conversation_entry_clear(gtkconv);
562 return;
565 if ((purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) &&
566 purple_conv_chat_has_left(PURPLE_CONV_CHAT(conv)))
567 return;
569 if (!purple_account_is_connected(account))
570 return;
572 buf = gtk_imhtml_get_markup(GTK_IMHTML(gtkconv->entry));
573 clean = gtk_imhtml_get_text(GTK_IMHTML(gtkconv->entry), NULL, NULL);
575 gtk_widget_grab_focus(gtkconv->entry);
577 if (strlen(clean) == 0) {
578 g_free(buf);
579 g_free(clean);
580 return;
583 purple_idle_touch();
585 /* XXX: is there a better way to tell if the message has images? */
586 if (GTK_IMHTML(gtkconv->entry)->im_images != NULL)
587 flags |= PURPLE_MESSAGE_IMAGES;
589 gc = purple_account_get_connection(account);
590 if (gc && (conv->features & PURPLE_CONNECTION_NO_NEWLINES)) {
591 char **bufs;
592 int i;
594 bufs = gtk_imhtml_get_markup_lines(GTK_IMHTML(gtkconv->entry));
595 for (i = 0; bufs[i]; i++) {
596 send_history_add(gtkconv, bufs[i]);
597 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM)
598 purple_conv_im_send_with_flags(PURPLE_CONV_IM(conv), bufs[i], flags);
599 else if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT)
600 purple_conv_chat_send_with_flags(PURPLE_CONV_CHAT(conv), bufs[i], flags);
603 g_strfreev(bufs);
605 } else {
606 send_history_add(gtkconv, buf);
607 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM)
608 purple_conv_im_send_with_flags(PURPLE_CONV_IM(conv), buf, flags);
609 else if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT)
610 purple_conv_chat_send_with_flags(PURPLE_CONV_CHAT(conv), buf, flags);
613 g_free(clean);
614 g_free(buf);
616 conversation_entry_clear(gtkconv);
617 gtkconv_set_unseen(gtkconv, PIDGIN_UNSEEN_NONE);
620 static void
621 add_remove_cb(GtkWidget *widget, PidginConversation *gtkconv)
623 PurpleAccount *account;
624 const char *name;
625 PurpleConversation *conv = gtkconv->active_conv;
627 account = purple_conversation_get_account(conv);
628 name = purple_conversation_get_name(conv);
630 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) {
631 PurpleBuddy *b;
633 b = purple_find_buddy(account, name);
634 if (b != NULL)
635 pidgin_dialogs_remove_buddy(b);
636 else if (account != NULL && purple_account_is_connected(account))
637 purple_blist_request_add_buddy(account, (char *)name, NULL, NULL);
638 } else if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) {
639 PurpleChat *c;
641 c = purple_blist_find_chat(account, name);
642 if (c != NULL)
643 pidgin_dialogs_remove_chat(c);
644 else if (account != NULL && purple_account_is_connected(account))
645 purple_blist_request_add_chat(account, NULL, NULL, name);
648 gtk_widget_grab_focus(PIDGIN_CONVERSATION(conv)->entry);
651 static void chat_do_info(PidginConversation *gtkconv, const char *who)
653 PurpleConversation *conv = gtkconv->active_conv;
654 PurpleConnection *gc;
656 if ((gc = purple_conversation_get_gc(conv))) {
657 pidgin_retrieve_user_info_in_chat(gc, who, purple_conv_chat_get_id(PURPLE_CONV_CHAT(conv)));
662 static void
663 info_cb(GtkWidget *widget, PidginConversation *gtkconv)
665 PurpleConversation *conv = gtkconv->active_conv;
667 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) {
668 pidgin_retrieve_user_info(purple_conversation_get_gc(conv),
669 purple_conversation_get_name(conv));
670 gtk_widget_grab_focus(gtkconv->entry);
671 } else if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) {
672 /* Get info of the person currently selected in the GtkTreeView */
673 PidginChatPane *gtkchat;
674 GtkTreeIter iter;
675 GtkTreeModel *model;
676 GtkTreeSelection *sel;
677 char *name;
679 gtkchat = gtkconv->u.chat;
681 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
682 sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(gtkchat->list));
684 if (gtk_tree_selection_get_selected(sel, NULL, &iter))
685 gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, CHAT_USERS_NAME_COLUMN, &name, -1);
686 else
687 return;
689 chat_do_info(gtkconv, name);
690 g_free(name);
694 static void
695 block_cb(GtkWidget *widget, PidginConversation *gtkconv)
697 PurpleConversation *conv = gtkconv->active_conv;
698 PurpleAccount *account;
700 account = purple_conversation_get_account(conv);
702 if (account != NULL && purple_account_is_connected(account))
703 pidgin_request_add_block(account, purple_conversation_get_name(conv));
705 gtk_widget_grab_focus(PIDGIN_CONVERSATION(conv)->entry);
708 static void
709 unblock_cb(GtkWidget *widget, PidginConversation *gtkconv)
711 PurpleConversation *conv = gtkconv->active_conv;
712 PurpleAccount *account;
714 account = purple_conversation_get_account(conv);
716 if (account != NULL && purple_account_is_connected(account))
717 pidgin_request_add_permit(account, purple_conversation_get_name(conv));
719 gtk_widget_grab_focus(PIDGIN_CONVERSATION(conv)->entry);
722 static gboolean
723 chat_invite_filter(const PidginBuddyCompletionEntry *entry, gpointer data)
725 PurpleAccount *filter_account = data;
726 PurpleAccount *account = NULL;
728 if (entry->is_buddy) {
729 if (PURPLE_BUDDY_IS_ONLINE(entry->entry.buddy))
730 account = purple_buddy_get_account(entry->entry.buddy);
731 else
732 return FALSE;
733 } else {
734 account = entry->entry.logged_buddy->account;
736 if (account == filter_account)
737 return TRUE;
738 return FALSE;
741 static void
742 do_invite(GtkWidget *w, int resp, InviteBuddyInfo *info)
744 const char *buddy, *message;
745 PurpleConversation *conv;
747 conv = info->conv;
749 if (resp == GTK_RESPONSE_OK) {
750 buddy = gtk_entry_get_text(GTK_ENTRY(info->entry));
751 message = gtk_entry_get_text(GTK_ENTRY(info->message));
753 if (!g_ascii_strcasecmp(buddy, ""))
754 return;
756 serv_chat_invite(purple_conversation_get_gc(conv),
757 purple_conv_chat_get_id(PURPLE_CONV_CHAT(conv)),
758 message, buddy);
761 gtk_widget_destroy(invite_dialog);
762 invite_dialog = NULL;
764 g_free(info);
767 static void
768 invite_dnd_recv(GtkWidget *widget, GdkDragContext *dc, gint x, gint y,
769 GtkSelectionData *sd, guint inf, guint t, gpointer data)
771 InviteBuddyInfo *info = (InviteBuddyInfo *)data;
772 const char *convprotocol;
773 gboolean success = TRUE;
775 convprotocol = purple_account_get_protocol_id(purple_conversation_get_account(info->conv));
777 if (sd->target == gdk_atom_intern("PURPLE_BLIST_NODE", FALSE))
779 PurpleBlistNode *node = NULL;
780 PurpleBuddy *buddy;
782 memcpy(&node, sd->data, sizeof(node));
784 if (PURPLE_BLIST_NODE_IS_CONTACT(node))
785 buddy = purple_contact_get_priority_buddy((PurpleContact *)node);
786 else if (PURPLE_BLIST_NODE_IS_BUDDY(node))
787 buddy = (PurpleBuddy *)node;
788 else
789 return;
791 if (!purple_strequal(convprotocol, purple_account_get_protocol_id(buddy->account)))
793 purple_notify_error(PIDGIN_CONVERSATION(info->conv), NULL,
794 _("That buddy is not on the same protocol as this "
795 "chat."), NULL);
796 success = FALSE;
798 else
799 gtk_entry_set_text(GTK_ENTRY(info->entry), purple_buddy_get_name(buddy));
801 gtk_drag_finish(dc, success, (dc->action == GDK_ACTION_MOVE), t);
803 else if (sd->target == gdk_atom_intern("application/x-im-contact", FALSE))
805 char *protocol = NULL;
806 char *username = NULL;
807 PurpleAccount *account;
809 if (pidgin_parse_x_im_contact((const char *)sd->data, FALSE, &account,
810 &protocol, &username, NULL))
812 if (account == NULL)
814 purple_notify_error(PIDGIN_CONVERSATION(info->conv), NULL,
815 _("You are not currently signed on with an account that "
816 "can invite that buddy."), NULL);
818 else if (!purple_strequal(convprotocol, purple_account_get_protocol_id(account)))
820 purple_notify_error(PIDGIN_CONVERSATION(info->conv), NULL,
821 _("That buddy is not on the same protocol as this "
822 "chat."), NULL);
823 success = FALSE;
825 else
827 gtk_entry_set_text(GTK_ENTRY(info->entry), username);
831 g_free(username);
832 g_free(protocol);
834 gtk_drag_finish(dc, success, (dc->action == GDK_ACTION_MOVE), t);
838 static const GtkTargetEntry dnd_targets[] =
840 {"PURPLE_BLIST_NODE", GTK_TARGET_SAME_APP, 0},
841 {"application/x-im-contact", 0, 1}
844 static void
845 invite_cb(GtkWidget *widget, PidginConversation *gtkconv)
847 PurpleConversation *conv = gtkconv->active_conv;
848 InviteBuddyInfo *info = NULL;
850 if (invite_dialog == NULL) {
851 PidginWindow *gtkwin;
852 GtkWidget *label;
853 GtkWidget *vbox, *hbox;
854 GtkWidget *table;
855 GtkWidget *img;
857 img = gtk_image_new_from_stock(PIDGIN_STOCK_DIALOG_QUESTION,
858 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_HUGE));
860 info = g_new0(InviteBuddyInfo, 1);
861 info->conv = conv;
863 gtkwin = pidgin_conv_get_window(gtkconv);
865 /* Create the new dialog. */
866 invite_dialog = gtk_dialog_new_with_buttons(
867 _("Invite Buddy Into Chat Room"),
868 GTK_WINDOW(gtkwin->window), 0,
869 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
870 PIDGIN_STOCK_INVITE, GTK_RESPONSE_OK, NULL);
872 gtk_dialog_set_default_response(GTK_DIALOG(invite_dialog),
873 GTK_RESPONSE_OK);
874 gtk_container_set_border_width(GTK_CONTAINER(invite_dialog), PIDGIN_HIG_BOX_SPACE);
875 gtk_window_set_resizable(GTK_WINDOW(invite_dialog), FALSE);
876 gtk_dialog_set_has_separator(GTK_DIALOG(invite_dialog), FALSE);
878 info->window = GTK_WIDGET(invite_dialog);
880 /* Setup the outside spacing. */
881 vbox = GTK_DIALOG(invite_dialog)->vbox;
883 gtk_box_set_spacing(GTK_BOX(vbox), PIDGIN_HIG_BORDER);
884 gtk_container_set_border_width(GTK_CONTAINER(vbox), PIDGIN_HIG_BOX_SPACE);
886 /* Setup the inner hbox and put the dialog's icon in it. */
887 hbox = gtk_hbox_new(FALSE, PIDGIN_HIG_BORDER);
888 gtk_container_add(GTK_CONTAINER(vbox), hbox);
889 gtk_box_pack_start(GTK_BOX(hbox), img, FALSE, FALSE, 0);
890 gtk_misc_set_alignment(GTK_MISC(img), 0, 0);
892 /* Setup the right vbox. */
893 vbox = gtk_vbox_new(FALSE, 0);
894 gtk_container_add(GTK_CONTAINER(hbox), vbox);
896 /* Put our happy label in it. */
897 label = gtk_label_new(_("Please enter the name of the user you wish "
898 "to invite, along with an optional invite "
899 "message."));
900 gtk_widget_set_size_request(label, 350, -1);
901 gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
902 gtk_misc_set_alignment(GTK_MISC(label), 0, 0);
903 gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 0);
905 /* hbox for the table, and to give it some spacing on the left. */
906 hbox = gtk_hbox_new(FALSE, PIDGIN_HIG_BOX_SPACE);
907 gtk_container_add(GTK_CONTAINER(vbox), hbox);
909 /* Setup the table we're going to use to lay stuff out. */
910 table = gtk_table_new(2, 2, FALSE);
911 gtk_table_set_row_spacings(GTK_TABLE(table), PIDGIN_HIG_BOX_SPACE);
912 gtk_table_set_col_spacings(GTK_TABLE(table), PIDGIN_HIG_BOX_SPACE);
913 gtk_container_set_border_width(GTK_CONTAINER(table), PIDGIN_HIG_BORDER);
914 gtk_box_pack_start(GTK_BOX(vbox), table, FALSE, FALSE, 0);
916 /* Now the Buddy label */
917 label = gtk_label_new(NULL);
918 gtk_label_set_markup_with_mnemonic(GTK_LABEL(label), _("_Buddy:"));
919 gtk_misc_set_alignment(GTK_MISC(label), 0, 0);
920 gtk_table_attach_defaults(GTK_TABLE(table), label, 0, 1, 0, 1);
922 /* Now the Buddy drop-down entry field. */
923 info->entry = gtk_entry_new();
924 pidgin_setup_screenname_autocomplete_with_filter(info->entry, NULL, chat_invite_filter,
925 purple_conversation_get_account(conv));
926 gtk_table_attach_defaults(GTK_TABLE(table), info->entry, 1, 2, 0, 1);
927 gtk_label_set_mnemonic_widget(GTK_LABEL(label), info->entry);
929 /* Now the label for "Message" */
930 label = gtk_label_new(NULL);
931 gtk_label_set_markup_with_mnemonic(GTK_LABEL(label), _("_Message:"));
932 gtk_misc_set_alignment(GTK_MISC(label), 0, 0);
933 gtk_table_attach_defaults(GTK_TABLE(table), label, 0, 1, 1, 2);
936 /* And finally, the Message entry field. */
937 info->message = gtk_entry_new();
938 gtk_entry_set_activates_default(GTK_ENTRY(info->message), TRUE);
940 gtk_table_attach_defaults(GTK_TABLE(table), info->message, 1, 2, 1, 2);
941 gtk_label_set_mnemonic_widget(GTK_LABEL(label), info->message);
943 /* Connect the signals. */
944 g_signal_connect(G_OBJECT(invite_dialog), "response",
945 G_CALLBACK(do_invite), info);
946 /* Setup drag-and-drop */
947 gtk_drag_dest_set(info->window,
948 GTK_DEST_DEFAULT_MOTION |
949 GTK_DEST_DEFAULT_DROP,
950 dnd_targets,
951 sizeof(dnd_targets) / sizeof(GtkTargetEntry),
952 GDK_ACTION_COPY);
953 gtk_drag_dest_set(info->entry,
954 GTK_DEST_DEFAULT_MOTION |
955 GTK_DEST_DEFAULT_DROP,
956 dnd_targets,
957 sizeof(dnd_targets) / sizeof(GtkTargetEntry),
958 GDK_ACTION_COPY);
960 g_signal_connect(G_OBJECT(info->window), "drag_data_received",
961 G_CALLBACK(invite_dnd_recv), info);
962 g_signal_connect(G_OBJECT(info->entry), "drag_data_received",
963 G_CALLBACK(invite_dnd_recv), info);
966 gtk_widget_show_all(invite_dialog);
968 if (info != NULL)
969 gtk_widget_grab_focus(info->entry);
972 static void
973 menu_new_conv_cb(gpointer data, guint action, GtkWidget *widget)
975 pidgin_dialogs_im();
978 static void
979 menu_join_chat_cb(gpointer data, guint action, GtkWidget *widget)
981 pidgin_blist_joinchat_show();
984 static void
985 savelog_writefile_cb(void *user_data, const char *filename)
987 PurpleConversation *conv = (PurpleConversation *)user_data;
988 FILE *fp;
989 const char *name;
990 char **lines;
991 gchar *text;
993 if ((fp = g_fopen(filename, "w+")) == NULL) {
994 purple_notify_error(PIDGIN_CONVERSATION(conv), NULL, _("Unable to open file."), NULL);
995 return;
998 name = purple_conversation_get_name(conv);
999 fprintf(fp, "<html>\n<head>\n");
1000 fprintf(fp, "<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">\n");
1001 fprintf(fp, "<title>%s</title>\n</head>\n<body>\n", name);
1002 fprintf(fp, _("<h1>Conversation with %s</h1>\n"), name);
1004 lines = gtk_imhtml_get_markup_lines(
1005 GTK_IMHTML(PIDGIN_CONVERSATION(conv)->imhtml));
1006 text = g_strjoinv("<br>\n", lines);
1007 fprintf(fp, "%s", text);
1008 g_free(text);
1009 g_strfreev(lines);
1011 fprintf(fp, "\n</body>\n</html>\n");
1012 fclose(fp);
1016 * It would be kinda cool if this gave the option of saving a
1017 * plaintext v. HTML file.
1019 static void
1020 menu_save_as_cb(gpointer data, guint action, GtkWidget *widget)
1022 PidginWindow *win = data;
1023 PurpleConversation *conv = pidgin_conv_window_get_active_conversation(win);
1024 PurpleBuddy *buddy = purple_find_buddy(conv->account, conv->name);
1025 const char *name;
1026 gchar *buf;
1027 gchar *c;
1029 if (buddy != NULL)
1030 name = purple_buddy_get_contact_alias(buddy);
1031 else
1032 name = purple_normalize(conv->account, conv->name);
1034 buf = g_strdup_printf("%s.html", name);
1035 for (c = buf ; *c ; c++)
1037 if (*c == '/' || *c == '\\')
1038 *c = ' ';
1040 purple_request_file(PIDGIN_CONVERSATION(conv), _("Save Conversation"),
1041 buf,
1042 TRUE, G_CALLBACK(savelog_writefile_cb), NULL,
1043 NULL, NULL, conv,
1044 conv);
1046 g_free(buf);
1049 static void
1050 menu_view_log_cb(gpointer data, guint action, GtkWidget *widget)
1052 PidginWindow *win = data;
1053 PurpleConversation *conv;
1054 PurpleLogType type;
1055 PidginBuddyList *gtkblist;
1056 GdkCursor *cursor;
1057 const char *name;
1058 PurpleAccount *account;
1059 GSList *buddies;
1060 GSList *cur;
1062 conv = pidgin_conv_window_get_active_conversation(win);
1064 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM)
1065 type = PURPLE_LOG_IM;
1066 else if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT)
1067 type = PURPLE_LOG_CHAT;
1068 else
1069 return;
1071 gtkblist = pidgin_blist_get_default_gtk_blist();
1073 cursor = gdk_cursor_new(GDK_WATCH);
1074 gdk_window_set_cursor(gtkblist->window->window, cursor);
1075 gdk_window_set_cursor(win->window->window, cursor);
1076 gdk_cursor_unref(cursor);
1077 gdk_display_flush(gdk_drawable_get_display(GDK_DRAWABLE(widget->window)));
1079 name = purple_conversation_get_name(conv);
1080 account = purple_conversation_get_account(conv);
1082 buddies = purple_find_buddies(account, name);
1083 for (cur = buddies; cur != NULL; cur = cur->next)
1085 PurpleBlistNode *node = cur->data;
1086 if ((node != NULL) && ((node->prev != NULL) || (node->next != NULL)))
1088 pidgin_log_show_contact((PurpleContact *)node->parent);
1089 g_slist_free(buddies);
1090 gdk_window_set_cursor(gtkblist->window->window, NULL);
1091 gdk_window_set_cursor(win->window->window, NULL);
1092 return;
1095 g_slist_free(buddies);
1097 pidgin_log_show(type, name, account);
1099 gdk_window_set_cursor(gtkblist->window->window, NULL);
1100 gdk_window_set_cursor(win->window->window, NULL);
1103 static void
1104 menu_clear_cb(gpointer data, guint action, GtkWidget *widget)
1106 PidginWindow *win = data;
1107 PurpleConversation *conv;
1109 conv = pidgin_conv_window_get_active_conversation(win);
1110 purple_conversation_clear_message_history(conv);
1113 static void
1114 menu_find_cb(gpointer data, guint action, GtkWidget *widget)
1116 PidginWindow *gtkwin = data;
1117 PidginConversation *gtkconv = pidgin_conv_window_get_active_gtkconv(gtkwin);
1118 gtk_widget_show_all(gtkconv->quickfind.container);
1119 gtk_widget_grab_focus(gtkconv->quickfind.entry);
1122 #ifdef USE_VV
1123 static void
1124 menu_initiate_media_call_cb(gpointer data, guint action, GtkWidget *widget)
1126 PidginWindow *win = (PidginWindow *)data;
1127 PurpleConversation *conv = pidgin_conv_window_get_active_conversation(win);
1128 PurpleAccount *account = purple_conversation_get_account(conv);
1130 purple_prpl_initiate_media(account,
1131 purple_conversation_get_name(conv),
1132 action == 0 ? PURPLE_MEDIA_AUDIO :
1133 action == 1 ? PURPLE_MEDIA_VIDEO :
1134 action == 2 ? PURPLE_MEDIA_AUDIO |
1135 PURPLE_MEDIA_VIDEO : PURPLE_MEDIA_NONE);
1137 #endif
1139 static void
1140 menu_send_file_cb(gpointer data, guint action, GtkWidget *widget)
1142 PidginWindow *win = data;
1143 PurpleConversation *conv = pidgin_conv_window_get_active_conversation(win);
1145 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) {
1146 serv_send_file(purple_conversation_get_gc(conv), purple_conversation_get_name(conv), NULL);
1151 static void
1152 menu_get_attention_cb(gpointer data, guint action, GtkWidget *widget)
1154 PidginWindow *win = data;
1155 PurpleConversation *conv = pidgin_conv_window_get_active_conversation(win);
1157 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) {
1158 purple_prpl_send_attention(purple_conversation_get_gc(conv),
1159 purple_conversation_get_name(conv), 0);
1163 static void
1164 menu_add_pounce_cb(gpointer data, guint action, GtkWidget *widget)
1166 PidginWindow *win = data;
1167 PurpleConversation *conv;
1169 conv = pidgin_conv_window_get_active_gtkconv(win)->active_conv;
1171 pidgin_pounce_editor_show(purple_conversation_get_account(conv),
1172 purple_conversation_get_name(conv), NULL);
1175 static void
1176 menu_insert_link_cb(gpointer data, guint action, GtkWidget *widget)
1178 PidginWindow *win = data;
1179 PidginConversation *gtkconv;
1180 GtkIMHtmlToolbar *toolbar;
1182 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
1183 toolbar = GTK_IMHTMLTOOLBAR(gtkconv->toolbar);
1185 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(toolbar->link),
1186 !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toolbar->link)));
1189 static void
1190 menu_insert_image_cb(gpointer data, guint action, GtkWidget *widget)
1192 PidginWindow *win = data;
1193 PidginConversation *gtkconv;
1194 GtkIMHtmlToolbar *toolbar;
1196 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
1197 toolbar = GTK_IMHTMLTOOLBAR(gtkconv->toolbar);
1199 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(toolbar->image),
1200 !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toolbar->image)));
1204 static void
1205 menu_alias_cb(gpointer data, guint action, GtkWidget *widget)
1207 PidginWindow *win = data;
1208 PurpleConversation *conv;
1209 PurpleAccount *account;
1210 const char *name;
1212 conv = pidgin_conv_window_get_active_conversation(win);
1213 account = purple_conversation_get_account(conv);
1214 name = purple_conversation_get_name(conv);
1216 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) {
1217 PurpleBuddy *b;
1219 b = purple_find_buddy(account, name);
1220 if (b != NULL)
1221 pidgin_dialogs_alias_buddy(b);
1222 } else if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) {
1223 PurpleChat *c;
1225 c = purple_blist_find_chat(account, name);
1226 if (c != NULL)
1227 pidgin_dialogs_alias_chat(c);
1231 static void
1232 menu_get_info_cb(gpointer data, guint action, GtkWidget *widget)
1234 PidginWindow *win = data;
1235 PurpleConversation *conv;
1237 conv = pidgin_conv_window_get_active_conversation(win);
1239 info_cb(NULL, PIDGIN_CONVERSATION(conv));
1242 static void
1243 menu_invite_cb(gpointer data, guint action, GtkWidget *widget)
1245 PidginWindow *win = data;
1246 PurpleConversation *conv;
1248 conv = pidgin_conv_window_get_active_conversation(win);
1250 invite_cb(NULL, PIDGIN_CONVERSATION(conv));
1253 static void
1254 menu_block_cb(gpointer data, guint action, GtkWidget *widget)
1256 PidginWindow *win = data;
1257 PurpleConversation *conv;
1259 conv = pidgin_conv_window_get_active_conversation(win);
1261 block_cb(NULL, PIDGIN_CONVERSATION(conv));
1264 static void
1265 menu_unblock_cb(gpointer data, guint action, GtkWidget *widget)
1267 PidginWindow *win = data;
1268 PurpleConversation *conv;
1270 conv = pidgin_conv_window_get_active_conversation(win);
1272 unblock_cb(NULL, PIDGIN_CONVERSATION(conv));
1275 static void
1276 menu_add_remove_cb(gpointer data, guint action, GtkWidget *widget)
1278 PidginWindow *win = data;
1279 PurpleConversation *conv;
1281 conv = pidgin_conv_window_get_active_conversation(win);
1283 add_remove_cb(NULL, PIDGIN_CONVERSATION(conv));
1286 static gboolean
1287 close_already(gpointer data)
1289 purple_conversation_destroy(data);
1290 return FALSE;
1293 static void
1294 hide_conv(PidginConversation *gtkconv, gboolean closetimer)
1296 GList *list;
1298 purple_signal_emit(pidgin_conversations_get_handle(),
1299 "conversation-hiding", gtkconv);
1301 for (list = g_list_copy(gtkconv->convs); list; list = g_list_delete_link(list, list)) {
1302 PurpleConversation *conv = list->data;
1303 if (closetimer) {
1304 guint timer = GPOINTER_TO_INT(purple_conversation_get_data(conv, "close-timer"));
1305 if (timer)
1306 purple_timeout_remove(timer);
1307 timer = purple_timeout_add_seconds(CLOSE_CONV_TIMEOUT_SECS, close_already, conv);
1308 purple_conversation_set_data(conv, "close-timer", GINT_TO_POINTER(timer));
1310 #if 0
1311 /* I will miss you */
1312 purple_conversation_set_ui_ops(conv, NULL);
1313 #else
1314 pidgin_conv_window_remove_gtkconv(gtkconv->win, gtkconv);
1315 pidgin_conv_window_add_gtkconv(hidden_convwin, gtkconv);
1316 #endif
1320 static void
1321 menu_close_conv_cb(gpointer data, guint action, GtkWidget *widget)
1323 PidginWindow *win = data;
1325 close_conv_cb(NULL, PIDGIN_CONVERSATION(pidgin_conv_window_get_active_conversation(win)));
1328 static void
1329 menu_logging_cb(gpointer data, guint action, GtkWidget *widget)
1331 PidginWindow *win = data;
1332 PurpleConversation *conv;
1333 gboolean logging;
1334 PurpleBlistNode *node;
1336 conv = pidgin_conv_window_get_active_conversation(win);
1338 if (conv == NULL)
1339 return;
1341 logging = gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(widget));
1343 if (logging == purple_conversation_is_logging(conv))
1344 return;
1346 node = get_conversation_blist_node(conv);
1348 if (logging)
1350 /* Enable logging first so the message below can be logged. */
1351 purple_conversation_set_logging(conv, TRUE);
1353 purple_conversation_write(conv, NULL,
1354 _("Logging started. Future messages in this conversation will be logged."),
1355 conv->logs ? (PURPLE_MESSAGE_SYSTEM) :
1356 (PURPLE_MESSAGE_SYSTEM | PURPLE_MESSAGE_NO_LOG),
1357 time(NULL));
1359 else
1361 purple_conversation_write(conv, NULL,
1362 _("Logging stopped. Future messages in this conversation will not be logged."),
1363 conv->logs ? (PURPLE_MESSAGE_SYSTEM) :
1364 (PURPLE_MESSAGE_SYSTEM | PURPLE_MESSAGE_NO_LOG),
1365 time(NULL));
1367 /* Disable the logging second, so that the above message can be logged. */
1368 purple_conversation_set_logging(conv, FALSE);
1371 /* Save the setting IFF it's different than the pref. */
1372 switch (conv->type)
1374 case PURPLE_CONV_TYPE_IM:
1375 if (logging == purple_prefs_get_bool("/purple/logging/log_ims"))
1376 purple_blist_node_remove_setting(node, "enable-logging");
1377 else
1378 purple_blist_node_set_bool(node, "enable-logging", logging);
1379 break;
1381 case PURPLE_CONV_TYPE_CHAT:
1382 if (logging == purple_prefs_get_bool("/purple/logging/log_chats"))
1383 purple_blist_node_remove_setting(node, "enable-logging");
1384 else
1385 purple_blist_node_set_bool(node, "enable-logging", logging);
1386 break;
1388 default:
1389 break;
1393 static void
1394 menu_toolbar_cb(gpointer data, guint action, GtkWidget *widget)
1396 purple_prefs_set_bool(PIDGIN_PREFS_ROOT "/conversations/show_formatting_toolbar",
1397 gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(widget)));
1400 static void
1401 menu_sounds_cb(gpointer data, guint action, GtkWidget *widget)
1403 PidginWindow *win = data;
1404 PurpleConversation *conv;
1405 PidginConversation *gtkconv;
1406 PurpleBlistNode *node;
1408 conv = pidgin_conv_window_get_active_conversation(win);
1410 if (!conv)
1411 return;
1413 gtkconv = PIDGIN_CONVERSATION(conv);
1415 gtkconv->make_sound =
1416 gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(widget));
1417 node = get_conversation_blist_node(conv);
1418 if (node)
1419 purple_blist_node_set_bool(node, "gtk-mute-sound", !gtkconv->make_sound);
1422 static void
1423 menu_timestamps_cb(gpointer data, guint action, GtkWidget *widget)
1425 purple_prefs_set_bool(PIDGIN_PREFS_ROOT "/conversations/show_timestamps",
1426 gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(widget)));
1429 static void
1430 chat_do_im(PidginConversation *gtkconv, const char *who)
1432 PurpleConversation *conv = gtkconv->active_conv;
1433 PurpleAccount *account;
1434 PurpleConnection *gc;
1435 PurplePluginProtocolInfo *prpl_info = NULL;
1436 gchar *real_who = NULL;
1438 account = purple_conversation_get_account(conv);
1439 g_return_if_fail(account != NULL);
1441 gc = purple_account_get_connection(account);
1442 g_return_if_fail(gc != NULL);
1444 prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(gc->prpl);
1446 if (prpl_info && prpl_info->get_cb_real_name)
1447 real_who = prpl_info->get_cb_real_name(gc,
1448 purple_conv_chat_get_id(PURPLE_CONV_CHAT(conv)), who);
1450 if(!who && !real_who)
1451 return;
1453 pidgin_dialogs_im_with_user(account, real_who ? real_who : who);
1455 g_free(real_who);
1458 static void pidgin_conv_chat_update_user(PurpleConversation *conv, const char *user);
1460 static void
1461 ignore_cb(GtkWidget *w, PidginConversation *gtkconv)
1463 PurpleConversation *conv = gtkconv->active_conv;
1464 PurpleConvChat *chat;
1465 const char *name;
1467 chat = PURPLE_CONV_CHAT(conv);
1468 name = g_object_get_data(G_OBJECT(w), "user_data");
1470 if (name == NULL)
1471 return;
1473 if (purple_conv_chat_is_user_ignored(chat, name))
1474 purple_conv_chat_unignore(chat, name);
1475 else
1476 purple_conv_chat_ignore(chat, name);
1478 pidgin_conv_chat_update_user(conv, name);
1481 static void
1482 menu_chat_im_cb(GtkWidget *w, PidginConversation *gtkconv)
1484 const char *who = g_object_get_data(G_OBJECT(w), "user_data");
1486 chat_do_im(gtkconv, who);
1489 static void
1490 menu_chat_send_file_cb(GtkWidget *w, PidginConversation *gtkconv)
1492 PurplePluginProtocolInfo *prpl_info;
1493 PurpleConversation *conv = gtkconv->active_conv;
1494 const char *who = g_object_get_data(G_OBJECT(w), "user_data");
1495 PurpleConnection *gc = purple_conversation_get_gc(conv);
1496 gchar *real_who = NULL;
1498 g_return_if_fail(gc != NULL);
1500 prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(gc->prpl);
1502 if (prpl_info && prpl_info->get_cb_real_name)
1503 real_who = prpl_info->get_cb_real_name(gc,
1504 purple_conv_chat_get_id(PURPLE_CONV_CHAT(conv)), who);
1506 serv_send_file(gc, real_who ? real_who : who, NULL);
1507 g_free(real_who);
1510 static void
1511 menu_chat_info_cb(GtkWidget *w, PidginConversation *gtkconv)
1513 char *who;
1515 who = g_object_get_data(G_OBJECT(w), "user_data");
1517 chat_do_info(gtkconv, who);
1520 static void
1521 menu_chat_get_away_cb(GtkWidget *w, PidginConversation *gtkconv)
1523 PurpleConversation *conv = gtkconv->active_conv;
1524 PurplePluginProtocolInfo *prpl_info = NULL;
1525 PurpleConnection *gc;
1526 char *who;
1528 gc = purple_conversation_get_gc(conv);
1529 who = g_object_get_data(G_OBJECT(w), "user_data");
1531 if (gc != NULL) {
1532 prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(gc->prpl);
1535 * May want to expand this to work similarly to menu_info_cb?
1538 if (prpl_info->get_cb_away != NULL)
1540 prpl_info->get_cb_away(gc,
1541 purple_conv_chat_get_id(PURPLE_CONV_CHAT(conv)), who);
1546 static void
1547 menu_chat_add_remove_cb(GtkWidget *w, PidginConversation *gtkconv)
1549 PurpleConversation *conv = gtkconv->active_conv;
1550 PurpleAccount *account;
1551 PurpleBuddy *b;
1552 char *name;
1554 account = purple_conversation_get_account(conv);
1555 name = g_object_get_data(G_OBJECT(w), "user_data");
1556 b = purple_find_buddy(account, name);
1558 if (b != NULL)
1559 pidgin_dialogs_remove_buddy(b);
1560 else if (account != NULL && purple_account_is_connected(account))
1561 purple_blist_request_add_buddy(account, name, NULL, NULL);
1563 gtk_widget_grab_focus(PIDGIN_CONVERSATION(conv)->entry);
1566 static GtkTextMark *
1567 get_mark_for_user(PidginConversation *gtkconv, const char *who)
1569 GtkTextBuffer *buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(gtkconv->imhtml));
1570 char *tmp = g_strconcat("user:", who, NULL);
1571 GtkTextMark *mark = gtk_text_buffer_get_mark(buf, tmp);
1573 g_free(tmp);
1574 return mark;
1577 static void
1578 menu_last_said_cb(GtkWidget *w, PidginConversation *gtkconv)
1580 GtkTextMark *mark;
1581 const char *who;
1583 who = g_object_get_data(G_OBJECT(w), "user_data");
1584 mark = get_mark_for_user(gtkconv, who);
1586 if (mark != NULL)
1587 gtk_text_view_scroll_to_mark(GTK_TEXT_VIEW(gtkconv->imhtml), mark, 0.1, FALSE, 0, 0);
1588 else
1589 g_return_if_reached();
1592 static GtkWidget *
1593 create_chat_menu(PurpleConversation *conv, const char *who, PurpleConnection *gc)
1595 static GtkWidget *menu = NULL;
1596 PurplePluginProtocolInfo *prpl_info = NULL;
1597 PurpleConvChat *chat = PURPLE_CONV_CHAT(conv);
1598 gboolean is_me = FALSE;
1599 GtkWidget *button;
1600 PurpleBuddy *buddy = NULL;
1602 if (gc != NULL)
1603 prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(gc->prpl);
1606 * If a menu already exists, destroy it before creating a new one,
1607 * thus freeing-up the memory it occupied.
1609 if (menu)
1610 gtk_widget_destroy(menu);
1612 if (purple_strequal(chat->nick, purple_normalize(conv->account, who)))
1613 is_me = TRUE;
1615 menu = gtk_menu_new();
1617 if (!is_me) {
1618 button = pidgin_new_item_from_stock(menu, _("IM"), PIDGIN_STOCK_TOOLBAR_MESSAGE_NEW,
1619 G_CALLBACK(menu_chat_im_cb), PIDGIN_CONVERSATION(conv), 0, 0, NULL);
1621 if (gc == NULL)
1622 gtk_widget_set_sensitive(button, FALSE);
1623 else
1624 g_object_set_data_full(G_OBJECT(button), "user_data", g_strdup(who), g_free);
1627 if (prpl_info && prpl_info->send_file)
1629 gboolean can_receive_file = TRUE;
1631 button = pidgin_new_item_from_stock(menu, _("Send File"),
1632 PIDGIN_STOCK_TOOLBAR_SEND_FILE, G_CALLBACK(menu_chat_send_file_cb),
1633 PIDGIN_CONVERSATION(conv), 0, 0, NULL);
1635 if (gc == NULL || prpl_info == NULL)
1636 can_receive_file = FALSE;
1637 else {
1638 gchar *real_who = NULL;
1639 if (prpl_info->get_cb_real_name)
1640 real_who = prpl_info->get_cb_real_name(gc,
1641 purple_conv_chat_get_id(PURPLE_CONV_CHAT(conv)), who);
1642 if (!(!prpl_info->can_receive_file || prpl_info->can_receive_file(gc, real_who ? real_who : who)))
1643 can_receive_file = FALSE;
1644 g_free(real_who);
1647 if (!can_receive_file)
1648 gtk_widget_set_sensitive(button, FALSE);
1649 else
1650 g_object_set_data_full(G_OBJECT(button), "user_data", g_strdup(who), g_free);
1654 if (purple_conv_chat_is_user_ignored(PURPLE_CONV_CHAT(conv), who))
1655 button = pidgin_new_item_from_stock(menu, _("Un-Ignore"), PIDGIN_STOCK_IGNORE,
1656 G_CALLBACK(ignore_cb), PIDGIN_CONVERSATION(conv), 0, 0, NULL);
1657 else
1658 button = pidgin_new_item_from_stock(menu, _("Ignore"), PIDGIN_STOCK_IGNORE,
1659 G_CALLBACK(ignore_cb), PIDGIN_CONVERSATION(conv), 0, 0, NULL);
1661 if (gc == NULL)
1662 gtk_widget_set_sensitive(button, FALSE);
1663 else
1664 g_object_set_data_full(G_OBJECT(button), "user_data", g_strdup(who), g_free);
1667 if (prpl_info && (prpl_info->get_info || prpl_info->get_cb_info)) {
1668 button = pidgin_new_item_from_stock(menu, _("Info"), PIDGIN_STOCK_TOOLBAR_USER_INFO,
1669 G_CALLBACK(menu_chat_info_cb), PIDGIN_CONVERSATION(conv), 0, 0, NULL);
1671 if (gc == NULL)
1672 gtk_widget_set_sensitive(button, FALSE);
1673 else
1674 g_object_set_data_full(G_OBJECT(button), "user_data", g_strdup(who), g_free);
1677 if (prpl_info && prpl_info->get_cb_away) {
1678 button = pidgin_new_item_from_stock(menu, _("Get Away Message"), PIDGIN_STOCK_AWAY,
1679 G_CALLBACK(menu_chat_get_away_cb), PIDGIN_CONVERSATION(conv), 0, 0, NULL);
1681 if (gc == NULL)
1682 gtk_widget_set_sensitive(button, FALSE);
1683 else
1684 g_object_set_data_full(G_OBJECT(button), "user_data", g_strdup(who), g_free);
1687 if (!is_me && prpl_info && !(prpl_info->options & OPT_PROTO_UNIQUE_CHATNAME)) {
1688 if ((buddy = purple_find_buddy(conv->account, who)) != NULL)
1689 button = pidgin_new_item_from_stock(menu, _("Remove"), GTK_STOCK_REMOVE,
1690 G_CALLBACK(menu_chat_add_remove_cb), PIDGIN_CONVERSATION(conv), 0, 0, NULL);
1691 else
1692 button = pidgin_new_item_from_stock(menu, _("Add"), GTK_STOCK_ADD,
1693 G_CALLBACK(menu_chat_add_remove_cb), PIDGIN_CONVERSATION(conv), 0, 0, NULL);
1695 if (gc == NULL)
1696 gtk_widget_set_sensitive(button, FALSE);
1697 else
1698 g_object_set_data_full(G_OBJECT(button), "user_data", g_strdup(who), g_free);
1701 button = pidgin_new_item_from_stock(menu, _("Last Said"), GTK_STOCK_INDEX,
1702 G_CALLBACK(menu_last_said_cb), PIDGIN_CONVERSATION(conv), 0, 0, NULL);
1703 g_object_set_data_full(G_OBJECT(button), "user_data", g_strdup(who), g_free);
1704 if (!get_mark_for_user(PIDGIN_CONVERSATION(conv), who))
1705 gtk_widget_set_sensitive(button, FALSE);
1707 if (buddy != NULL)
1709 if (purple_account_is_connected(conv->account))
1710 pidgin_append_blist_node_proto_menu(menu, conv->account->gc,
1711 (PurpleBlistNode *)buddy);
1712 pidgin_append_blist_node_extended_menu(menu, (PurpleBlistNode *)buddy);
1713 gtk_widget_show_all(menu);
1716 return menu;
1720 static gint
1721 gtkconv_chat_popup_menu_cb(GtkWidget *widget, PidginConversation *gtkconv)
1723 PurpleConversation *conv = gtkconv->active_conv;
1724 PidginChatPane *gtkchat;
1725 PurpleConnection *gc;
1726 PurpleAccount *account;
1727 GtkTreeSelection *sel;
1728 GtkTreeIter iter;
1729 GtkTreeModel *model;
1730 GtkWidget *menu;
1731 gchar *who;
1733 gtkconv = PIDGIN_CONVERSATION(conv);
1734 gtkchat = gtkconv->u.chat;
1735 account = purple_conversation_get_account(conv);
1736 gc = account->gc;
1738 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
1740 sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(gtkchat->list));
1741 if(!gtk_tree_selection_get_selected(sel, NULL, &iter))
1742 return FALSE;
1744 gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, CHAT_USERS_NAME_COLUMN, &who, -1);
1745 menu = create_chat_menu (conv, who, gc);
1746 gtk_menu_popup(GTK_MENU(menu), NULL, NULL,
1747 pidgin_treeview_popup_menu_position_func, widget,
1748 0, GDK_CURRENT_TIME);
1749 g_free(who);
1751 return TRUE;
1755 static gint
1756 right_click_chat_cb(GtkWidget *widget, GdkEventButton *event,
1757 PidginConversation *gtkconv)
1759 PurpleConversation *conv = gtkconv->active_conv;
1760 PidginChatPane *gtkchat;
1761 PurpleConnection *gc;
1762 PurpleAccount *account;
1763 GtkTreePath *path;
1764 GtkTreeIter iter;
1765 GtkTreeModel *model;
1766 GtkTreeViewColumn *column;
1767 gchar *who;
1768 int x, y;
1770 gtkchat = gtkconv->u.chat;
1771 account = purple_conversation_get_account(conv);
1772 gc = account->gc;
1774 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
1776 gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(gtkchat->list),
1777 event->x, event->y, &path, &column, &x, &y);
1779 if (path == NULL)
1780 return FALSE;
1782 gtk_tree_selection_select_path(GTK_TREE_SELECTION(
1783 gtk_tree_view_get_selection(GTK_TREE_VIEW(gtkchat->list))), path);
1784 gtk_tree_view_set_cursor(GTK_TREE_VIEW(gtkchat->list),
1785 path, NULL, FALSE);
1786 gtk_widget_grab_focus(GTK_WIDGET(gtkchat->list));
1788 gtk_tree_model_get_iter(GTK_TREE_MODEL(model), &iter, path);
1789 gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, CHAT_USERS_NAME_COLUMN, &who, -1);
1791 /* emit chat-nick-clicked signal */
1792 if (event->type == GDK_BUTTON_PRESS) {
1793 gint plugin_return = GPOINTER_TO_INT(purple_signal_emit_return_1(
1794 pidgin_conversations_get_handle(), "chat-nick-clicked",
1795 conv, who, event->button));
1796 if (plugin_return)
1797 goto handled;
1800 if (event->button == 1 && event->type == GDK_2BUTTON_PRESS) {
1801 chat_do_im(gtkconv, who);
1802 } else if (event->button == 2 && event->type == GDK_BUTTON_PRESS) {
1803 /* Move to user's anchor */
1804 GtkTextMark *mark = get_mark_for_user(gtkconv, who);
1806 if(mark != NULL)
1807 gtk_text_view_scroll_to_mark(GTK_TEXT_VIEW(gtkconv->imhtml), mark, 0.1, FALSE, 0, 0);
1808 } else if (event->button == 3 && event->type == GDK_BUTTON_PRESS) {
1809 GtkWidget *menu = create_chat_menu (conv, who, gc);
1810 gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL,
1811 event->button, event->time);
1814 handled:
1815 g_free(who);
1816 gtk_tree_path_free(path);
1818 return TRUE;
1821 static void
1822 activate_list_cb(GtkTreeView *list, GtkTreePath *path, GtkTreeViewColumn *column, PidginConversation *gtkconv)
1824 GtkTreeIter iter;
1825 GtkTreeModel *model;
1826 gchar *who;
1828 model = gtk_tree_view_get_model(GTK_TREE_VIEW(list));
1830 gtk_tree_model_get_iter(GTK_TREE_MODEL(model), &iter, path);
1831 gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, CHAT_USERS_NAME_COLUMN, &who, -1);
1832 chat_do_im(gtkconv, who);
1834 g_free(who);
1837 static void
1838 move_to_next_unread_tab(PidginConversation *gtkconv, gboolean forward)
1840 PidginConversation *next_gtkconv = NULL, *most_active = NULL;
1841 PidginUnseenState unseen_state = PIDGIN_UNSEEN_NONE;
1842 PidginWindow *win;
1843 int initial, i, total, diff;
1845 win = gtkconv->win;
1846 initial = gtk_notebook_page_num(GTK_NOTEBOOK(win->notebook),
1847 gtkconv->tab_cont);
1848 total = pidgin_conv_window_get_gtkconv_count(win);
1849 /* By adding total here, the moduli calculated later will always have two
1850 * positive arguments. x % y where x < 0 is not guaranteed to return a
1851 * positive number.
1853 diff = (forward ? 1 : -1) + total;
1855 for (i = (initial + diff) % total; i != initial; i = (i + diff) % total) {
1856 next_gtkconv = pidgin_conv_window_get_gtkconv_at_index(win, i);
1857 if (next_gtkconv->unseen_state > unseen_state) {
1858 most_active = next_gtkconv;
1859 unseen_state = most_active->unseen_state;
1860 if(PIDGIN_UNSEEN_NICK == unseen_state) /* highest possible state */
1861 break;
1865 if (most_active == NULL) { /* no new messages */
1866 i = (i + diff) % total;
1867 most_active = pidgin_conv_window_get_gtkconv_at_index(win, i);
1870 if (most_active != NULL && most_active != gtkconv)
1871 pidgin_conv_window_switch_gtkconv(win, most_active);
1874 static gboolean
1875 gtkconv_cycle_focus(PidginConversation *gtkconv, GtkDirectionType dir)
1877 PurpleConversation *conv = gtkconv->active_conv;
1878 gboolean chat = purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT;
1879 GtkWidget *next = NULL;
1880 struct {
1881 GtkWidget *from;
1882 GtkWidget *to;
1883 } transitions[] = {
1884 {gtkconv->entry, gtkconv->imhtml},
1885 {gtkconv->imhtml, chat ? gtkconv->u.chat->list : gtkconv->entry},
1886 {chat ? gtkconv->u.chat->list : NULL, gtkconv->entry},
1887 {NULL, NULL}
1888 }, *ptr;
1890 for (ptr = transitions; !next && ptr->from; ptr++) {
1891 GtkWidget *from, *to;
1892 if (dir == GTK_DIR_TAB_FORWARD) {
1893 from = ptr->from;
1894 to = ptr->to;
1895 } else {
1896 from = ptr->to;
1897 to = ptr->from;
1899 if (gtk_widget_is_focus(from))
1900 next = to;
1903 if (next)
1904 gtk_widget_grab_focus(next);
1905 return !!next;
1908 static gboolean
1909 conv_keypress_common(PidginConversation *gtkconv, GdkEventKey *event)
1911 PidginWindow *win;
1912 int curconv;
1914 win = gtkconv->win;
1915 curconv = gtk_notebook_get_current_page(GTK_NOTEBOOK(win->notebook));
1917 /* clear any tooltips */
1918 pidgin_tooltip_destroy();
1920 /* If CTRL was held down... */
1921 if (event->state & GDK_CONTROL_MASK) {
1922 switch (event->keyval) {
1923 case GDK_Page_Down:
1924 case GDK_KP_Page_Down:
1925 case ']':
1926 if (!pidgin_conv_window_get_gtkconv_at_index(win, curconv + 1))
1927 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), 0);
1928 else
1929 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), curconv + 1);
1930 return TRUE;
1931 break;
1933 case GDK_Page_Up:
1934 case GDK_KP_Page_Up:
1935 case '[':
1936 if (!pidgin_conv_window_get_gtkconv_at_index(win, curconv - 1))
1937 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), -1);
1938 else
1939 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), curconv - 1);
1940 return TRUE;
1941 break;
1943 case GDK_Tab:
1944 case GDK_KP_Tab:
1945 case GDK_ISO_Left_Tab:
1946 if (event->state & GDK_SHIFT_MASK) {
1947 move_to_next_unread_tab(gtkconv, FALSE);
1948 } else {
1949 move_to_next_unread_tab(gtkconv, TRUE);
1952 return TRUE;
1953 break;
1955 case GDK_comma:
1956 gtk_notebook_reorder_child(GTK_NOTEBOOK(win->notebook),
1957 gtk_notebook_get_nth_page(GTK_NOTEBOOK(win->notebook), curconv),
1958 curconv - 1);
1959 return TRUE;
1960 break;
1962 case GDK_period:
1963 gtk_notebook_reorder_child(GTK_NOTEBOOK(win->notebook),
1964 gtk_notebook_get_nth_page(GTK_NOTEBOOK(win->notebook), curconv),
1965 (curconv + 1) % gtk_notebook_get_n_pages(GTK_NOTEBOOK(win->notebook)));
1966 return TRUE;
1967 break;
1968 case GDK_F6:
1969 if (gtkconv_cycle_focus(gtkconv, event->state & GDK_SHIFT_MASK ? GTK_DIR_TAB_BACKWARD : GTK_DIR_TAB_FORWARD))
1970 return TRUE;
1971 break;
1972 } /* End of switch */
1975 /* If ALT (or whatever) was held down... */
1976 else if (event->state & GDK_MOD1_MASK)
1978 if (event->keyval > '0' && event->keyval <= '9')
1980 guint switchto = event->keyval - '1';
1981 if (switchto < pidgin_conv_window_get_gtkconv_count(win))
1982 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), switchto);
1984 return TRUE;
1988 /* If neither CTRL nor ALT were held down... */
1989 else
1991 switch (event->keyval) {
1992 case GDK_F2:
1993 if (gtk_widget_is_focus(GTK_WIDGET(win->notebook))) {
1994 infopane_entry_activate(gtkconv);
1995 return TRUE;
1997 break;
1998 case GDK_F6:
1999 if (gtkconv_cycle_focus(gtkconv, event->state & GDK_SHIFT_MASK ? GTK_DIR_TAB_BACKWARD : GTK_DIR_TAB_FORWARD))
2000 return TRUE;
2001 break;
2004 return FALSE;
2007 static gboolean
2008 entry_key_press_cb(GtkWidget *entry, GdkEventKey *event, gpointer data)
2010 PurpleConversation *conv;
2011 PidginConversation *gtkconv;
2013 gtkconv = (PidginConversation *)data;
2014 conv = gtkconv->active_conv;
2016 if (conv_keypress_common(gtkconv, event))
2017 return TRUE;
2019 /* If CTRL was held down... */
2020 if (event->state & GDK_CONTROL_MASK) {
2021 switch (event->keyval) {
2022 case GDK_Up:
2023 if (!gtkconv->send_history)
2024 break;
2026 if (gtkconv->entry != entry)
2027 break;
2029 if (!gtkconv->send_history->prev) {
2030 GtkTextIter start, end;
2032 g_free(gtkconv->send_history->data);
2034 gtk_text_buffer_get_start_iter(gtkconv->entry_buffer,
2035 &start);
2036 gtk_text_buffer_get_end_iter(gtkconv->entry_buffer, &end);
2038 gtkconv->send_history->data =
2039 gtk_imhtml_get_markup(GTK_IMHTML(gtkconv->entry));
2042 if (gtkconv->send_history->next && gtkconv->send_history->next->data) {
2043 GObject *object;
2044 GtkTextIter iter;
2045 GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(gtkconv->entry));
2047 gtkconv->send_history = gtkconv->send_history->next;
2049 /* Block the signal to prevent application of default formatting. */
2050 object = g_object_ref(G_OBJECT(gtkconv->entry));
2051 g_signal_handlers_block_matched(object, G_SIGNAL_MATCH_DATA, 0, 0, NULL,
2052 NULL, gtkconv);
2053 /* Clear the formatting. */
2054 gtk_imhtml_clear_formatting(GTK_IMHTML(gtkconv->entry));
2055 /* Unblock the signal. */
2056 g_signal_handlers_unblock_matched(object, G_SIGNAL_MATCH_DATA, 0, 0, NULL,
2057 NULL, gtkconv);
2058 g_object_unref(object);
2060 gtk_imhtml_clear(GTK_IMHTML(gtkconv->entry));
2061 gtk_imhtml_append_text_with_images(
2062 GTK_IMHTML(gtkconv->entry), gtkconv->send_history->data,
2063 0, NULL);
2064 /* this is mainly just a hack so the formatting at the
2065 * cursor gets picked up. */
2066 gtk_text_buffer_get_end_iter(buffer, &iter);
2067 gtk_text_buffer_move_mark_by_name(buffer, "insert", &iter);
2070 return TRUE;
2071 break;
2073 case GDK_Down:
2074 if (!gtkconv->send_history)
2075 break;
2077 if (gtkconv->entry != entry)
2078 break;
2080 if (gtkconv->send_history->prev && gtkconv->send_history->prev->data) {
2081 GObject *object;
2082 GtkTextIter iter;
2083 GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(gtkconv->entry));
2085 gtkconv->send_history = gtkconv->send_history->prev;
2087 /* Block the signal to prevent application of default formatting. */
2088 object = g_object_ref(G_OBJECT(gtkconv->entry));
2089 g_signal_handlers_block_matched(object, G_SIGNAL_MATCH_DATA, 0, 0, NULL,
2090 NULL, gtkconv);
2091 /* Clear the formatting. */
2092 gtk_imhtml_clear_formatting(GTK_IMHTML(gtkconv->entry));
2093 /* Unblock the signal. */
2094 g_signal_handlers_unblock_matched(object, G_SIGNAL_MATCH_DATA, 0, 0, NULL,
2095 NULL, gtkconv);
2096 g_object_unref(object);
2098 gtk_imhtml_clear(GTK_IMHTML(gtkconv->entry));
2099 gtk_imhtml_append_text_with_images(
2100 GTK_IMHTML(gtkconv->entry), gtkconv->send_history->data,
2101 0, NULL);
2102 /* this is mainly just a hack so the formatting at the
2103 * cursor gets picked up. */
2104 if (*(char *)gtkconv->send_history->data) {
2105 gtk_text_buffer_get_end_iter(buffer, &iter);
2106 gtk_text_buffer_move_mark_by_name(buffer, "insert", &iter);
2107 } else {
2108 /* Restore the default formatting */
2109 default_formatize(gtkconv);
2113 return TRUE;
2114 break;
2115 } /* End of switch */
2118 /* If ALT (or whatever) was held down... */
2119 else if (event->state & GDK_MOD1_MASK) {
2123 /* If neither CTRL nor ALT were held down... */
2124 else {
2125 switch (event->keyval) {
2126 case GDK_Tab:
2127 case GDK_KP_Tab:
2128 case GDK_ISO_Left_Tab:
2129 if (gtkconv->entry != entry)
2130 break;
2132 gint plugin_return;
2133 plugin_return = GPOINTER_TO_INT(purple_signal_emit_return_1(
2134 pidgin_conversations_get_handle(), "chat-nick-autocomplete",
2135 conv, event->state & GDK_SHIFT_MASK));
2136 return plugin_return ? TRUE : tab_complete(conv);
2138 break;
2140 case GDK_Page_Up:
2141 case GDK_KP_Page_Up:
2142 gtk_imhtml_page_up(GTK_IMHTML(gtkconv->imhtml));
2143 return TRUE;
2144 break;
2146 case GDK_Page_Down:
2147 case GDK_KP_Page_Down:
2148 gtk_imhtml_page_down(GTK_IMHTML(gtkconv->imhtml));
2149 return TRUE;
2150 break;
2154 return FALSE;
2158 * NOTE:
2159 * This guy just kills a single right click from being propagated any
2160 * further. I have no idea *why* we need this, but we do ... It
2161 * prevents right clicks on the GtkTextView in a convo dialog from
2162 * going all the way down to the notebook. I suspect a bug in
2163 * GtkTextView, but I'm not ready to point any fingers yet.
2165 static gboolean
2166 entry_stop_rclick_cb(GtkWidget *widget, GdkEventButton *event, gpointer data)
2168 if (event->button == 3 && event->type == GDK_BUTTON_PRESS) {
2169 /* Right single click */
2170 g_signal_stop_emission_by_name(G_OBJECT(widget), "button_press_event");
2172 return TRUE;
2175 return FALSE;
2179 * If someone tries to type into the conversation backlog of a
2180 * conversation window then we yank focus from the conversation backlog
2181 * and give it to the text entry box so that people can type
2182 * all the live long day and it will get entered into the entry box.
2184 static gboolean
2185 refocus_entry_cb(GtkWidget *widget, GdkEventKey *event, gpointer data)
2187 PidginConversation *gtkconv = data;
2189 /* If we have a valid key for the conversation display, then exit */
2190 if ((event->state & GDK_CONTROL_MASK) ||
2191 (event->keyval == GDK_F6) ||
2192 (event->keyval == GDK_F10) ||
2193 (event->keyval == GDK_Shift_L) ||
2194 (event->keyval == GDK_Shift_R) ||
2195 (event->keyval == GDK_Control_L) ||
2196 (event->keyval == GDK_Control_R) ||
2197 (event->keyval == GDK_Escape) ||
2198 (event->keyval == GDK_Up) ||
2199 (event->keyval == GDK_Down) ||
2200 (event->keyval == GDK_Left) ||
2201 (event->keyval == GDK_Right) ||
2202 (event->keyval == GDK_Page_Up) ||
2203 (event->keyval == GDK_KP_Page_Up) ||
2204 (event->keyval == GDK_Page_Down) ||
2205 (event->keyval == GDK_KP_Page_Down) ||
2206 (event->keyval == GDK_Home) ||
2207 (event->keyval == GDK_End) ||
2208 (event->keyval == GDK_Tab) ||
2209 (event->keyval == GDK_KP_Tab) ||
2210 (event->keyval == GDK_ISO_Left_Tab))
2212 if (event->type == GDK_KEY_PRESS)
2213 return conv_keypress_common(gtkconv, event);
2214 return FALSE;
2217 if (event->type == GDK_KEY_RELEASE)
2218 gtk_widget_grab_focus(gtkconv->entry);
2220 gtk_widget_event(gtkconv->entry, (GdkEvent *)event);
2222 return TRUE;
2225 static void
2226 regenerate_options_items(PidginWindow *win);
2228 void
2229 pidgin_conv_switch_active_conversation(PurpleConversation *conv)
2231 PidginConversation *gtkconv;
2232 PurpleConversation *old_conv;
2233 GtkIMHtml *entry;
2234 const char *protocol_name;
2236 g_return_if_fail(conv != NULL);
2238 gtkconv = PIDGIN_CONVERSATION(conv);
2239 old_conv = gtkconv->active_conv;
2241 purple_debug_info("gtkconv", "setting active conversation on toolbar %p\n",
2242 conv);
2243 gtk_imhtmltoolbar_switch_active_conversation(GTK_IMHTMLTOOLBAR(gtkconv->toolbar),
2244 conv);
2246 if (old_conv == conv)
2247 return;
2249 purple_conversation_close_logs(old_conv);
2250 gtkconv->active_conv = conv;
2252 purple_conversation_set_logging(conv,
2253 gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(gtkconv->win->menu.logging)));
2255 entry = GTK_IMHTML(gtkconv->entry);
2256 protocol_name = purple_account_get_protocol_name(conv->account);
2257 gtk_imhtml_set_protocol_name(entry, protocol_name);
2258 gtk_imhtml_set_protocol_name(GTK_IMHTML(gtkconv->imhtml), protocol_name);
2260 if (!(conv->features & PURPLE_CONNECTION_HTML))
2261 gtk_imhtml_clear_formatting(GTK_IMHTML(gtkconv->entry));
2262 else if (conv->features & PURPLE_CONNECTION_FORMATTING_WBFO &&
2263 !(old_conv->features & PURPLE_CONNECTION_FORMATTING_WBFO))
2265 /* The old conversation allowed formatting on parts of the
2266 * buffer, but the new one only allows it on the whole
2267 * buffer. This code saves the formatting from the current
2268 * position of the cursor, clears the formatting, then
2269 * applies the saved formatting to the entire buffer. */
2271 gboolean bold;
2272 gboolean italic;
2273 gboolean underline;
2274 char *fontface = gtk_imhtml_get_current_fontface(entry);
2275 char *forecolor = gtk_imhtml_get_current_forecolor(entry);
2276 char *backcolor = gtk_imhtml_get_current_backcolor(entry);
2277 char *background = gtk_imhtml_get_current_background(entry);
2278 gint fontsize = gtk_imhtml_get_current_fontsize(entry);
2279 gboolean bold2;
2280 gboolean italic2;
2281 gboolean underline2;
2283 gtk_imhtml_get_current_format(entry, &bold, &italic, &underline);
2285 /* Clear existing formatting */
2286 gtk_imhtml_clear_formatting(entry);
2288 /* Apply saved formatting to the whole buffer. */
2290 gtk_imhtml_get_current_format(entry, &bold2, &italic2, &underline2);
2292 if (bold != bold2)
2293 gtk_imhtml_toggle_bold(entry);
2295 if (italic != italic2)
2296 gtk_imhtml_toggle_italic(entry);
2298 if (underline != underline2)
2299 gtk_imhtml_toggle_underline(entry);
2301 gtk_imhtml_toggle_fontface(entry, fontface);
2303 if (!(conv->features & PURPLE_CONNECTION_NO_FONTSIZE))
2304 gtk_imhtml_font_set_size(entry, fontsize);
2306 gtk_imhtml_toggle_forecolor(entry, forecolor);
2308 if (!(conv->features & PURPLE_CONNECTION_NO_BGCOLOR))
2310 gtk_imhtml_toggle_backcolor(entry, backcolor);
2311 gtk_imhtml_toggle_background(entry, background);
2314 g_free(fontface);
2315 g_free(forecolor);
2316 g_free(backcolor);
2317 g_free(background);
2319 else
2321 /* This is done in default_formatize, which is called from clear_formatting_cb,
2322 * which is (obviously) a clear_formatting signal handler. However, if we're
2323 * here, we didn't call gtk_imhtml_clear_formatting() (because we want to
2324 * preserve the formatting exactly as it is), so we have to do this now. */
2325 gtk_imhtml_set_whole_buffer_formatting_only(entry,
2326 (conv->features & PURPLE_CONNECTION_FORMATTING_WBFO));
2329 purple_signal_emit(pidgin_conversations_get_handle(), "conversation-switched", conv);
2331 gray_stuff_out(gtkconv);
2332 update_typing_icon(gtkconv);
2333 g_object_set_data(G_OBJECT(entry), "transient_buddy", NULL);
2334 regenerate_options_items(gtkconv->win);
2336 gtk_window_set_title(GTK_WINDOW(gtkconv->win->window),
2337 gtk_label_get_text(GTK_LABEL(gtkconv->tab_label)));
2340 static void
2341 menu_conv_sel_send_cb(GObject *m, gpointer data)
2343 PurpleAccount *account = g_object_get_data(m, "purple_account");
2344 gchar *name = g_object_get_data(m, "purple_buddy_name");
2345 PurpleConversation *conv;
2347 if (gtk_check_menu_item_get_active((GtkCheckMenuItem*) m) == FALSE)
2348 return;
2350 conv = purple_conversation_new(PURPLE_CONV_TYPE_IM, account, name);
2351 pidgin_conv_switch_active_conversation(conv);
2354 static void
2355 insert_text_cb(GtkTextBuffer *textbuffer, GtkTextIter *position,
2356 gchar *new_text, gint new_text_length, gpointer user_data)
2358 PidginConversation *gtkconv = (PidginConversation *)user_data;
2360 g_return_if_fail(gtkconv != NULL);
2362 if (!purple_prefs_get_bool("/purple/conversations/im/send_typing"))
2363 return;
2365 got_typing_keypress(gtkconv, (gtk_text_iter_is_start(position) &&
2366 gtk_text_iter_is_end(position)));
2369 static void
2370 delete_text_cb(GtkTextBuffer *textbuffer, GtkTextIter *start_pos,
2371 GtkTextIter *end_pos, gpointer user_data)
2373 PidginConversation *gtkconv = (PidginConversation *)user_data;
2374 PurpleConversation *conv;
2375 PurpleConvIm *im;
2377 g_return_if_fail(gtkconv != NULL);
2379 conv = gtkconv->active_conv;
2381 if (!purple_prefs_get_bool("/purple/conversations/im/send_typing"))
2382 return;
2384 im = PURPLE_CONV_IM(conv);
2386 if (gtk_text_iter_is_start(start_pos) && gtk_text_iter_is_end(end_pos)) {
2388 /* We deleted all the text, so turn off typing. */
2389 purple_conv_im_stop_send_typed_timeout(im);
2391 serv_send_typing(purple_conversation_get_gc(conv),
2392 purple_conversation_get_name(conv),
2393 PURPLE_NOT_TYPING);
2395 else {
2396 /* We're deleting, but not all of it, so it counts as typing. */
2397 got_typing_keypress(gtkconv, FALSE);
2401 /**************************************************************************
2402 * A bunch of buddy icon functions
2403 **************************************************************************/
2405 static GList *get_prpl_icon_list(PurpleAccount *account)
2407 GList *l = NULL;
2408 PurplePlugin *prpl = purple_find_prpl(purple_account_get_protocol_id(account));
2409 PurplePluginProtocolInfo *prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(prpl);
2410 const char *prplname = prpl_info->list_icon(account, NULL);
2411 l = g_hash_table_lookup(prpl_lists, prplname);
2412 if (l)
2413 return l;
2415 l = g_list_prepend(l, pidgin_create_prpl_icon(account, PIDGIN_PRPL_ICON_LARGE));
2416 l = g_list_prepend(l, pidgin_create_prpl_icon(account, PIDGIN_PRPL_ICON_MEDIUM));
2417 l = g_list_prepend(l, pidgin_create_prpl_icon(account, PIDGIN_PRPL_ICON_SMALL));
2419 g_hash_table_insert(prpl_lists, g_strdup(prplname), l);
2420 return l;
2423 static GList *
2424 pidgin_conv_get_tab_icons(PurpleConversation *conv)
2426 PurpleAccount *account = NULL;
2427 const char *name = NULL;
2429 g_return_val_if_fail(conv != NULL, NULL);
2431 account = purple_conversation_get_account(conv);
2432 name = purple_conversation_get_name(conv);
2434 g_return_val_if_fail(account != NULL, NULL);
2435 g_return_val_if_fail(name != NULL, NULL);
2437 /* Use the buddy icon, if possible */
2438 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) {
2439 PurpleBuddy *b = purple_find_buddy(account, name);
2440 if (b != NULL) {
2441 PurplePresence *p;
2442 p = purple_buddy_get_presence(b);
2443 if (purple_presence_is_status_primitive_active(p, PURPLE_STATUS_AWAY))
2444 return away_list;
2445 if (purple_presence_is_status_primitive_active(p, PURPLE_STATUS_UNAVAILABLE))
2446 return busy_list;
2447 if (purple_presence_is_status_primitive_active(p, PURPLE_STATUS_EXTENDED_AWAY))
2448 return xa_list;
2449 if (purple_presence_is_status_primitive_active(p, PURPLE_STATUS_OFFLINE))
2450 return offline_list;
2451 else
2452 return available_list;
2456 return get_prpl_icon_list(account);
2459 static const char *
2460 pidgin_conv_get_icon_stock(PurpleConversation *conv)
2462 PurpleAccount *account = NULL;
2463 const char *stock = NULL;
2465 g_return_val_if_fail(conv != NULL, NULL);
2467 account = purple_conversation_get_account(conv);
2468 g_return_val_if_fail(account != NULL, NULL);
2470 /* Use the buddy icon, if possible */
2471 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) {
2472 const char *name = NULL;
2473 PurpleBuddy *b;
2474 name = purple_conversation_get_name(conv);
2475 b = purple_find_buddy(account, name);
2476 if (b != NULL) {
2477 PurplePresence *p = purple_buddy_get_presence(b);
2478 PurpleStatus *active = purple_presence_get_active_status(p);
2479 PurpleStatusType *type = purple_status_get_type(active);
2480 PurpleStatusPrimitive prim = purple_status_type_get_primitive(type);
2481 stock = pidgin_stock_id_from_status_primitive(prim);
2482 } else {
2483 stock = PIDGIN_STOCK_STATUS_PERSON;
2485 } else {
2486 stock = PIDGIN_STOCK_STATUS_CHAT;
2489 return stock;
2492 static GdkPixbuf *
2493 pidgin_conv_get_icon(PurpleConversation *conv, GtkWidget *parent, const char *icon_size)
2495 PurpleAccount *account = NULL;
2496 const char *name = NULL;
2497 const char *stock = NULL;
2498 GdkPixbuf *status = NULL;
2499 PurpleBlistUiOps *ops = purple_blist_get_ui_ops();
2500 GtkIconSize size;
2502 g_return_val_if_fail(conv != NULL, NULL);
2504 account = purple_conversation_get_account(conv);
2505 name = purple_conversation_get_name(conv);
2507 g_return_val_if_fail(account != NULL, NULL);
2508 g_return_val_if_fail(name != NULL, NULL);
2510 /* Use the buddy icon, if possible */
2511 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) {
2512 PurpleBuddy *b = purple_find_buddy(account, name);
2513 if (b != NULL) {
2514 /* I hate this hack. It fixes a bug where the pending message icon
2515 * displays in the conv tab even though it shouldn't.
2516 * A better solution would be great. */
2517 if (ops && ops->update)
2518 ops->update(NULL, (PurpleBlistNode*)b);
2522 stock = pidgin_conv_get_icon_stock(conv);
2523 size = gtk_icon_size_from_name(icon_size);
2524 status = gtk_widget_render_icon (parent, stock, size, "GtkWidget");
2525 return status;
2528 GdkPixbuf *
2529 pidgin_conv_get_tab_icon(PurpleConversation *conv, gboolean small_icon)
2531 const char *icon_size = small_icon ? PIDGIN_ICON_SIZE_TANGO_MICROSCOPIC : PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL;
2532 return pidgin_conv_get_icon(conv, PIDGIN_CONVERSATION(conv)->icon, icon_size);
2536 static void
2537 update_tab_icon(PurpleConversation *conv)
2539 PidginConversation *gtkconv;
2540 PidginWindow *win;
2541 GList *l;
2542 GdkPixbuf *emblem = NULL;
2543 const char *status = NULL;
2544 const char *infopane_status = NULL;
2546 g_return_if_fail(conv != NULL);
2548 gtkconv = PIDGIN_CONVERSATION(conv);
2549 win = gtkconv->win;
2550 if (conv != gtkconv->active_conv)
2551 return;
2553 status = infopane_status = pidgin_conv_get_icon_stock(conv);
2555 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) {
2556 PurpleBuddy *b = purple_find_buddy(conv->account, conv->name);
2557 if (b)
2558 emblem = pidgin_blist_get_emblem((PurpleBlistNode*)b);
2561 g_return_if_fail(status != NULL);
2563 g_object_set(G_OBJECT(gtkconv->icon), "stock", status, NULL);
2564 g_object_set(G_OBJECT(gtkconv->menu_icon), "stock", status, NULL);
2566 gtk_list_store_set(GTK_LIST_STORE(gtkconv->infopane_model),
2567 &(gtkconv->infopane_iter),
2568 CONV_ICON_COLUMN, infopane_status, -1);
2570 gtk_list_store_set(GTK_LIST_STORE(gtkconv->infopane_model),
2571 &(gtkconv->infopane_iter),
2572 CONV_EMBLEM_COLUMN, emblem, -1);
2573 if (emblem)
2574 g_object_unref(emblem);
2576 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/blist/show_protocol_icons")) {
2577 emblem = pidgin_create_prpl_icon(gtkconv->active_conv->account, PIDGIN_PRPL_ICON_SMALL);
2578 } else {
2579 emblem = NULL;
2582 gtk_list_store_set(GTK_LIST_STORE(gtkconv->infopane_model),
2583 &(gtkconv->infopane_iter),
2584 CONV_PROTOCOL_ICON_COLUMN, emblem, -1);
2585 if (emblem)
2586 g_object_unref(emblem);
2588 /* XXX seanegan Why do I have to do this? */
2589 gtk_widget_queue_resize(gtkconv->infopane);
2590 gtk_widget_queue_draw(gtkconv->infopane);
2592 if (pidgin_conv_window_is_active_conversation(conv) &&
2593 (purple_conversation_get_type(conv) != PURPLE_CONV_TYPE_IM ||
2594 gtkconv->u.im->anim == NULL))
2596 l = pidgin_conv_get_tab_icons(conv);
2598 gtk_window_set_icon_list(GTK_WINDOW(win->window), l);
2602 #if 0
2603 /* This gets added as an idle handler when doing something that
2604 * redraws the icon. It sets the auto_resize gboolean to TRUE.
2605 * This way, when the size_allocate callback gets triggered, it notices
2606 * that this is an autoresize, and after the main loop iterates, it
2607 * gets set back to FALSE
2609 static gboolean reset_auto_resize_cb(gpointer data)
2611 PidginConversation *gtkconv = (PidginConversation *)data;
2612 gtkconv->auto_resize = FALSE;
2613 return FALSE;
2615 #endif
2617 static gboolean
2618 redraw_icon(gpointer data)
2620 PidginConversation *gtkconv = (PidginConversation *)data;
2621 PurpleConversation *conv = gtkconv->active_conv;
2622 PurpleAccount *account;
2624 GdkPixbuf *buf;
2625 GdkPixbuf *scale;
2626 gint delay;
2627 int scale_width, scale_height;
2628 int size;
2630 gtkconv = PIDGIN_CONVERSATION(conv);
2631 account = purple_conversation_get_account(conv);
2633 if (!(account && account->gc)) {
2634 gtkconv->u.im->icon_timer = 0;
2635 return FALSE;
2638 gdk_pixbuf_animation_iter_advance(gtkconv->u.im->iter, NULL);
2639 buf = gdk_pixbuf_animation_iter_get_pixbuf(gtkconv->u.im->iter);
2641 scale_width = gdk_pixbuf_get_width(buf);
2642 scale_height = gdk_pixbuf_get_height(buf);
2644 gtk_widget_get_size_request(gtkconv->u.im->icon_container, NULL, &size);
2645 size = MIN(size, MIN(scale_width, scale_height));
2646 size = CLAMP(size, BUDDYICON_SIZE_MIN, BUDDYICON_SIZE_MAX);
2648 if (scale_width == scale_height) {
2649 scale_width = scale_height = size;
2650 } else if (scale_height > scale_width) {
2651 scale_width = size * scale_width / scale_height;
2652 scale_height = size;
2653 } else {
2654 scale_height = size * scale_height / scale_width;
2655 scale_width = size;
2658 scale = gdk_pixbuf_scale_simple(buf, scale_width, scale_height,
2659 GDK_INTERP_BILINEAR);
2660 if (pidgin_gdk_pixbuf_is_opaque(scale))
2661 pidgin_gdk_pixbuf_make_round(scale);
2663 gtk_image_set_from_pixbuf(GTK_IMAGE(gtkconv->u.im->icon), scale);
2664 g_object_unref(G_OBJECT(scale));
2665 gtk_widget_queue_draw(gtkconv->u.im->icon);
2667 delay = gdk_pixbuf_animation_iter_get_delay_time(gtkconv->u.im->iter);
2669 if (delay < 100)
2670 delay = 100;
2672 gtkconv->u.im->icon_timer = g_timeout_add(delay, redraw_icon, gtkconv);
2674 return FALSE;
2677 static void
2678 start_anim(GtkObject *obj, PidginConversation *gtkconv)
2680 int delay;
2682 if (gtkconv->u.im->anim == NULL)
2683 return;
2685 if (gtkconv->u.im->icon_timer != 0)
2686 return;
2688 if (gdk_pixbuf_animation_is_static_image(gtkconv->u.im->anim))
2689 return;
2691 delay = gdk_pixbuf_animation_iter_get_delay_time(gtkconv->u.im->iter);
2693 if (delay < 100)
2694 delay = 100;
2696 gtkconv->u.im->icon_timer = g_timeout_add(delay, redraw_icon, gtkconv);
2699 static void
2700 remove_icon(GtkWidget *widget, PidginConversation *gtkconv)
2702 GList *children;
2703 GtkWidget *event;
2704 PurpleConversation *conv = gtkconv->active_conv;
2706 g_return_if_fail(conv != NULL);
2708 gtk_widget_set_size_request(gtkconv->u.im->icon_container, -1, BUDDYICON_SIZE_MIN);
2709 children = gtk_container_get_children(GTK_CONTAINER(gtkconv->u.im->icon_container));
2710 if (children) {
2711 /* We know there's only one child here. It'd be nice to shortcut to the
2712 event box, but we can't change the PidginConversation until 3.0 */
2713 event = (GtkWidget *)children->data;
2714 gtk_container_remove(GTK_CONTAINER(gtkconv->u.im->icon_container), event);
2715 g_list_free(children);
2718 if (gtkconv->u.im->anim != NULL)
2719 g_object_unref(G_OBJECT(gtkconv->u.im->anim));
2721 if (gtkconv->u.im->icon_timer != 0)
2722 g_source_remove(gtkconv->u.im->icon_timer);
2724 if (gtkconv->u.im->iter != NULL)
2725 g_object_unref(G_OBJECT(gtkconv->u.im->iter));
2727 gtkconv->u.im->icon_timer = 0;
2728 gtkconv->u.im->icon = NULL;
2729 gtkconv->u.im->anim = NULL;
2730 gtkconv->u.im->iter = NULL;
2731 gtkconv->u.im->show_icon = FALSE;
2734 static void
2735 saveicon_writefile_cb(void *user_data, const char *filename)
2737 PidginConversation *gtkconv = (PidginConversation *)user_data;
2738 PurpleConversation *conv = gtkconv->active_conv;
2739 PurpleBuddyIcon *icon;
2740 const void *data;
2741 size_t len;
2743 icon = purple_conv_im_get_icon(PURPLE_CONV_IM(conv));
2744 data = purple_buddy_icon_get_data(icon, &len);
2746 if ((len <= 0) || (data == NULL) || !purple_util_write_data_to_file_absolute(filename, data, len)) {
2747 purple_notify_error(gtkconv, NULL, _("Unable to save icon file to disk."), NULL);
2751 static void
2752 custom_icon_sel_cb(const char *filename, gpointer data)
2754 if (filename) {
2755 const gchar *name;
2756 PurpleBuddy *buddy;
2757 PurpleContact *contact;
2758 PidginConversation *gtkconv = data;
2759 PurpleConversation *conv = gtkconv->active_conv;
2760 PurpleAccount *account = purple_conversation_get_account(conv);
2762 name = purple_conversation_get_name(conv);
2763 buddy = purple_find_buddy(account, name);
2764 if (!buddy) {
2765 purple_debug_info("custom-icon", "You can only set custom icons for people on your buddylist.\n");
2766 return;
2768 contact = purple_buddy_get_contact(buddy);
2770 purple_buddy_icons_node_set_custom_icon_from_file((PurpleBlistNode*)contact, filename);
2774 static void
2775 set_custom_icon_cb(GtkWidget *widget, PidginConversation *gtkconv)
2777 GtkWidget *win = pidgin_buddy_icon_chooser_new(GTK_WINDOW(gtkconv->win->window),
2778 custom_icon_sel_cb, gtkconv);
2779 gtk_widget_show_all(win);
2782 static void
2783 change_size_cb(GtkWidget *widget, PidginConversation *gtkconv)
2785 int size = 0;
2786 PurpleConversation *conv = gtkconv->active_conv;
2787 GSList *buddies;
2789 gtk_widget_get_size_request(gtkconv->u.im->icon_container, NULL, &size);
2791 if (size == BUDDYICON_SIZE_MAX) {
2792 size = BUDDYICON_SIZE_MIN;
2793 } else {
2794 size = BUDDYICON_SIZE_MAX;
2797 gtk_widget_set_size_request(gtkconv->u.im->icon_container, -1, size);
2798 pidgin_conv_update_buddy_icon(conv);
2800 buddies = purple_find_buddies(purple_conversation_get_account(conv),
2801 purple_conversation_get_name(conv));
2802 for (; buddies; buddies = g_slist_delete_link(buddies, buddies)) {
2803 PurpleBuddy *buddy = buddies->data;
2804 PurpleContact *contact = purple_buddy_get_contact(buddy);
2805 purple_blist_node_set_int((PurpleBlistNode*)contact, "pidgin-infopane-iconsize", size);
2809 static void
2810 remove_custom_icon_cb(GtkWidget *widget, PidginConversation *gtkconv)
2812 const gchar *name;
2813 PurpleBuddy *buddy;
2814 PurpleAccount *account;
2815 PurpleContact *contact;
2816 PurpleConversation *conv = gtkconv->active_conv;
2818 account = purple_conversation_get_account(conv);
2819 name = purple_conversation_get_name(conv);
2820 buddy = purple_find_buddy(account, name);
2821 if (!buddy) {
2822 return;
2824 contact = purple_buddy_get_contact(buddy);
2826 purple_buddy_icons_node_set_custom_icon_from_file((PurpleBlistNode*)contact, NULL);
2829 static void
2830 icon_menu_save_cb(GtkWidget *widget, PidginConversation *gtkconv)
2832 PurpleConversation *conv = gtkconv->active_conv;
2833 const gchar *ext;
2834 gchar *buf;
2836 g_return_if_fail(conv != NULL);
2838 ext = purple_buddy_icon_get_extension(purple_conv_im_get_icon(PURPLE_CONV_IM(conv)));
2840 buf = g_strdup_printf("%s.%s", purple_normalize(conv->account, conv->name), ext);
2842 purple_request_file(gtkconv, _("Save Icon"), buf, TRUE,
2843 G_CALLBACK(saveicon_writefile_cb), NULL,
2844 conv->account, NULL, conv,
2845 gtkconv);
2847 g_free(buf);
2850 static void
2851 stop_anim(GtkObject *obj, PidginConversation *gtkconv)
2853 if (gtkconv->u.im->icon_timer != 0)
2854 g_source_remove(gtkconv->u.im->icon_timer);
2856 gtkconv->u.im->icon_timer = 0;
2860 static void
2861 toggle_icon_animate_cb(GtkWidget *w, PidginConversation *gtkconv)
2863 gtkconv->u.im->animate =
2864 gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(w));
2866 if (gtkconv->u.im->animate)
2867 start_anim(NULL, gtkconv);
2868 else
2869 stop_anim(NULL, gtkconv);
2872 static gboolean
2873 icon_menu(GtkObject *obj, GdkEventButton *e, PidginConversation *gtkconv)
2875 static GtkWidget *menu = NULL;
2876 PurpleConversation *conv;
2877 PurpleBuddy *buddy;
2879 if (e->button == 1 && e->type == GDK_BUTTON_PRESS) {
2880 change_size_cb(NULL, gtkconv);
2881 return TRUE;
2884 if (e->button != 3 || e->type != GDK_BUTTON_PRESS) {
2885 return FALSE;
2889 * If a menu already exists, destroy it before creating a new one,
2890 * thus freeing-up the memory it occupied.
2892 if (menu != NULL)
2893 gtk_widget_destroy(menu);
2895 menu = gtk_menu_new();
2897 if (gtkconv->u.im->anim &&
2898 !(gdk_pixbuf_animation_is_static_image(gtkconv->u.im->anim)))
2900 pidgin_new_check_item(menu, _("Animate"),
2901 G_CALLBACK(toggle_icon_animate_cb), gtkconv,
2902 gtkconv->u.im->icon_timer);
2905 pidgin_new_item_from_stock(menu, _("Hide Icon"), NULL, G_CALLBACK(remove_icon),
2906 gtkconv, 0, 0, NULL);
2908 pidgin_new_item_from_stock(menu, _("Save Icon As..."), GTK_STOCK_SAVE_AS,
2909 G_CALLBACK(icon_menu_save_cb), gtkconv,
2910 0, 0, NULL);
2912 pidgin_new_item_from_stock(menu, _("Set Custom Icon..."), NULL,
2913 G_CALLBACK(set_custom_icon_cb), gtkconv,
2914 0, 0, NULL);
2916 pidgin_new_item_from_stock(menu, _("Change Size"), NULL,
2917 G_CALLBACK(change_size_cb), gtkconv,
2918 0, 0, NULL);
2920 /* Is there a custom icon for this person? */
2921 conv = gtkconv->active_conv;
2922 buddy = purple_find_buddy(purple_conversation_get_account(conv),
2923 purple_conversation_get_name(conv));
2924 if (buddy)
2926 PurpleContact *contact = purple_buddy_get_contact(buddy);
2927 if (contact && purple_buddy_icons_node_has_custom_icon((PurpleBlistNode*)contact))
2929 pidgin_new_item_from_stock(menu, _("Remove Custom Icon"), NULL,
2930 G_CALLBACK(remove_custom_icon_cb), gtkconv,
2931 0, 0, NULL);
2935 gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, e->button, e->time);
2937 return TRUE;
2940 /**************************************************************************
2941 * End of the bunch of buddy icon functions
2942 **************************************************************************/
2943 void
2944 pidgin_conv_present_conversation(PurpleConversation *conv)
2946 PidginConversation *gtkconv;
2947 GdkModifierType state;
2949 pidgin_conv_attach_to_conversation(conv);
2950 gtkconv = PIDGIN_CONVERSATION(conv);
2952 pidgin_conv_switch_active_conversation(conv);
2953 /* Switch the tab only if the user initiated the event by pressing
2954 * a button or hitting a key. */
2955 if (gtk_get_current_event_state(&state))
2956 pidgin_conv_window_switch_gtkconv(gtkconv->win, gtkconv);
2957 gtk_window_present(GTK_WINDOW(gtkconv->win->window));
2960 GList *
2961 pidgin_conversations_find_unseen_list(PurpleConversationType type,
2962 PidginUnseenState min_state,
2963 gboolean hidden_only,
2964 guint max_count)
2966 GList *l;
2967 GList *r = NULL;
2968 guint c = 0;
2970 if (type == PURPLE_CONV_TYPE_IM) {
2971 l = purple_get_ims();
2972 } else if (type == PURPLE_CONV_TYPE_CHAT) {
2973 l = purple_get_chats();
2974 } else {
2975 l = purple_get_conversations();
2978 for (; l != NULL && (max_count == 0 || c < max_count); l = l->next) {
2979 PurpleConversation *conv = (PurpleConversation*)l->data;
2980 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
2982 if(gtkconv == NULL || gtkconv->active_conv != conv)
2983 continue;
2985 if (gtkconv->unseen_state >= min_state
2986 && (!hidden_only ||
2987 (hidden_only && gtkconv->win == hidden_convwin))) {
2989 r = g_list_prepend(r, conv);
2990 c++;
2994 return r;
2997 static void
2998 unseen_conv_menu_cb(GtkMenuItem *item, PurpleConversation *conv)
3000 g_return_if_fail(conv != NULL);
3001 pidgin_conv_present_conversation(conv);
3004 static void
3005 unseen_all_conv_menu_cb(GtkMenuItem *item, GList *list)
3007 g_return_if_fail(list != NULL);
3008 /* Do not free the list from here. It will be freed from the
3009 * 'destroy' callback on the menuitem. */
3010 while (list) {
3011 pidgin_conv_present_conversation(list->data);
3012 list = list->next;
3016 guint
3017 pidgin_conversations_fill_menu(GtkWidget *menu, GList *convs)
3019 GList *l;
3020 guint ret=0;
3022 g_return_val_if_fail(menu != NULL, 0);
3023 g_return_val_if_fail(convs != NULL, 0);
3025 for (l = convs; l != NULL ; l = l->next) {
3026 PurpleConversation *conv = (PurpleConversation*)l->data;
3027 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
3029 GtkWidget *icon = gtk_image_new_from_stock(pidgin_conv_get_icon_stock(conv),
3030 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_MICROSCOPIC));
3031 GtkWidget *item;
3032 gchar *text = g_strdup_printf("%s (%d)",
3033 gtk_label_get_text(GTK_LABEL(gtkconv->tab_label)),
3034 gtkconv->unseen_count);
3036 item = gtk_image_menu_item_new_with_label(text);
3037 gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(item), icon);
3038 g_signal_connect(G_OBJECT(item), "activate", G_CALLBACK(unseen_conv_menu_cb), conv);
3039 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
3040 g_free(text);
3041 ret++;
3044 if (convs->next) {
3045 /* There are more than one conversation. Add an option to show all conversations. */
3046 GtkWidget *item;
3047 GList *list = g_list_copy(convs);
3049 pidgin_separator(menu);
3051 item = gtk_menu_item_new_with_label(_("Show All"));
3052 g_signal_connect(G_OBJECT(item), "activate", G_CALLBACK(unseen_all_conv_menu_cb), list);
3053 g_signal_connect_swapped(G_OBJECT(item), "destroy", G_CALLBACK(g_list_free), list);
3054 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
3057 return ret;
3060 PidginWindow *
3061 pidgin_conv_get_window(PidginConversation *gtkconv)
3063 g_return_val_if_fail(gtkconv != NULL, NULL);
3064 return gtkconv->win;
3067 static GtkItemFactoryEntry menu_items[] =
3069 /* Conversation menu */
3070 { N_("/_Conversation"), NULL, NULL, 0, "<Branch>", NULL },
3072 { N_("/Conversation/New Instant _Message..."), "<CTL>M", menu_new_conv_cb,
3073 0, "<StockItem>", PIDGIN_STOCK_TOOLBAR_MESSAGE_NEW },
3074 { N_("/Conversation/Join a _Chat..."), NULL, menu_join_chat_cb,
3075 0, "<StockItem>", PIDGIN_STOCK_CHAT },
3077 { "/Conversation/sep0", NULL, NULL, 0, "<Separator>", NULL },
3079 { N_("/Conversation/_Find..."), NULL, menu_find_cb, 0,
3080 "<StockItem>", GTK_STOCK_FIND },
3081 { N_("/Conversation/View _Log"), NULL, menu_view_log_cb, 0, "<Item>", NULL },
3082 { N_("/Conversation/_Save As..."), NULL, menu_save_as_cb, 0,
3083 "<StockItem>", GTK_STOCK_SAVE_AS },
3084 { N_("/Conversation/Clea_r Scrollback"), "<CTL>L", menu_clear_cb, 0, "<StockItem>", GTK_STOCK_CLEAR },
3086 { "/Conversation/sep1", NULL, NULL, 0, "<Separator>", NULL },
3088 #ifdef USE_VV
3089 { N_("/Conversation/M_edia"), NULL, NULL, 0, "<Branch>", NULL },
3091 { N_("/Conversation/Media/_Audio Call"), NULL, menu_initiate_media_call_cb, 0,
3092 "<StockItem>", PIDGIN_STOCK_TOOLBAR_AUDIO_CALL },
3093 { N_("/Conversation/Media/_Video Call"), NULL, menu_initiate_media_call_cb, 1,
3094 "<StockItem>", PIDGIN_STOCK_TOOLBAR_VIDEO_CALL },
3095 { N_("/Conversation/Media/Audio\\/Video _Call"), NULL, menu_initiate_media_call_cb, 2,
3096 "<StockItem>", PIDGIN_STOCK_TOOLBAR_VIDEO_CALL },
3097 #endif
3099 { N_("/Conversation/Se_nd File..."), NULL, menu_send_file_cb, 0, "<StockItem>", PIDGIN_STOCK_TOOLBAR_SEND_FILE },
3100 { N_("/Conversation/Get _Attention"), NULL, menu_get_attention_cb, 0, "<StockItem>", PIDGIN_STOCK_TOOLBAR_SEND_ATTENTION },
3101 { N_("/Conversation/Add Buddy _Pounce..."), NULL, menu_add_pounce_cb,
3102 0, "<Item>", NULL },
3103 { N_("/Conversation/_Get Info"), "<CTL>O", menu_get_info_cb, 0,
3104 "<StockItem>", PIDGIN_STOCK_TOOLBAR_USER_INFO },
3105 { N_("/Conversation/In_vite..."), NULL, menu_invite_cb, 0,
3106 "<Item>", NULL },
3107 { N_("/Conversation/M_ore"), NULL, NULL, 0, "<Branch>", NULL },
3109 { "/Conversation/sep2", NULL, NULL, 0, "<Separator>", NULL },
3111 { N_("/Conversation/Al_ias..."), NULL, menu_alias_cb, 0,
3112 "<Item>", NULL },
3113 { N_("/Conversation/_Block..."), NULL, menu_block_cb, 0,
3114 "<StockItem>", PIDGIN_STOCK_TOOLBAR_BLOCK },
3115 { N_("/Conversation/_Unblock..."), NULL, menu_unblock_cb, 0,
3116 "<StockItem>", PIDGIN_STOCK_TOOLBAR_UNBLOCK },
3117 { N_("/Conversation/_Add..."), NULL, menu_add_remove_cb, 0,
3118 "<StockItem>", GTK_STOCK_ADD },
3119 { N_("/Conversation/_Remove..."), NULL, menu_add_remove_cb, 0,
3120 "<StockItem>", GTK_STOCK_REMOVE },
3122 { "/Conversation/sep3", NULL, NULL, 0, "<Separator>", NULL },
3124 { N_("/Conversation/Insert Lin_k..."), NULL, menu_insert_link_cb, 0,
3125 "<StockItem>", PIDGIN_STOCK_TOOLBAR_INSERT_LINK },
3126 { N_("/Conversation/Insert Imag_e..."), NULL, menu_insert_image_cb, 0,
3127 "<StockItem>", PIDGIN_STOCK_TOOLBAR_INSERT_IMAGE },
3129 { "/Conversation/sep4", NULL, NULL, 0, "<Separator>", NULL },
3132 { N_("/Conversation/_Close"), NULL, menu_close_conv_cb, 0,
3133 "<StockItem>", GTK_STOCK_CLOSE },
3135 /* Options */
3136 { N_("/_Options"), NULL, NULL, 0, "<Branch>", NULL },
3137 { N_("/Options/Enable _Logging"), NULL, menu_logging_cb, 0, "<CheckItem>", NULL },
3138 { N_("/Options/Enable _Sounds"), NULL, menu_sounds_cb, 0, "<CheckItem>", NULL },
3139 { "/Options/sep0", NULL, NULL, 0, "<Separator>", NULL },
3140 { N_("/Options/Show Formatting _Toolbars"), NULL, menu_toolbar_cb, 0, "<CheckItem>", NULL },
3141 { N_("/Options/Show Ti_mestamps"), NULL, menu_timestamps_cb, 0, "<CheckItem>", NULL },
3144 static const int menu_item_count =
3145 sizeof(menu_items) / sizeof(*menu_items);
3147 static const char *
3148 item_factory_translate_func (const char *path, gpointer func_data)
3150 return _(path);
3153 static void
3154 sound_method_pref_changed_cb(const char *name, PurplePrefType type,
3155 gconstpointer value, gpointer data)
3157 PidginWindow *win = data;
3158 const char *method = value;
3160 if (purple_strequal(method, "none"))
3162 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(win->menu.sounds),
3163 FALSE);
3164 gtk_widget_set_sensitive(win->menu.sounds, FALSE);
3166 else
3168 PidginConversation *gtkconv = pidgin_conv_window_get_active_gtkconv(win);
3170 if (gtkconv != NULL)
3171 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(win->menu.sounds),
3172 gtkconv->make_sound);
3173 gtk_widget_set_sensitive(win->menu.sounds, TRUE);
3178 /* Returns TRUE if some items were added to the menu, FALSE otherwise */
3179 static gboolean
3180 populate_menu_with_options(GtkWidget *menu, PidginConversation *gtkconv, gboolean all)
3182 GList *list;
3183 PurpleConversation *conv;
3184 PurpleBlistNode *node = NULL;
3185 PurpleChat *chat = NULL;
3186 PurpleBuddy *buddy = NULL;
3187 gboolean ret;
3189 conv = gtkconv->active_conv;
3191 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) {
3192 chat = purple_blist_find_chat(conv->account, conv->name);
3194 if ((chat == NULL) && (gtkconv->imhtml != NULL)) {
3195 chat = g_object_get_data(G_OBJECT(gtkconv->imhtml), "transient_chat");
3198 if ((chat == NULL) && (gtkconv->imhtml != NULL)) {
3199 GHashTable *components;
3200 PurpleAccount *account = purple_conversation_get_account(conv);
3201 PurplePlugin *prpl = purple_find_prpl(purple_account_get_protocol_id(account));
3202 PurplePluginProtocolInfo *prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(prpl);
3203 if (purple_account_get_connection(account) != NULL &&
3204 PURPLE_PROTOCOL_PLUGIN_HAS_FUNC(prpl_info, chat_info_defaults)) {
3205 components = prpl_info->chat_info_defaults(purple_account_get_connection(account),
3206 purple_conversation_get_name(conv));
3207 } else {
3208 components = g_hash_table_new_full(g_str_hash, g_str_equal,
3209 g_free, g_free);
3210 g_hash_table_replace(components, g_strdup("channel"),
3211 g_strdup(purple_conversation_get_name(conv)));
3213 chat = purple_chat_new(conv->account, NULL, components);
3214 purple_blist_node_set_flags((PurpleBlistNode *)chat,
3215 PURPLE_BLIST_NODE_FLAG_NO_SAVE);
3216 g_object_set_data_full(G_OBJECT(gtkconv->imhtml), "transient_chat",
3217 chat, (GDestroyNotify)purple_blist_remove_chat);
3219 } else {
3220 if (!purple_account_is_connected(conv->account))
3221 return FALSE;
3223 buddy = purple_find_buddy(conv->account, conv->name);
3225 /* gotta remain bug-compatible :( libpurple < 2.0.2 didn't handle
3226 * removing "isolated" buddy nodes well */
3227 if (purple_version_check(2, 0, 2) == NULL) {
3228 if ((buddy == NULL) && (gtkconv->imhtml != NULL)) {
3229 buddy = g_object_get_data(G_OBJECT(gtkconv->imhtml), "transient_buddy");
3232 if ((buddy == NULL) && (gtkconv->imhtml != NULL)) {
3233 buddy = purple_buddy_new(conv->account, conv->name, NULL);
3234 purple_blist_node_set_flags((PurpleBlistNode *)buddy,
3235 PURPLE_BLIST_NODE_FLAG_NO_SAVE);
3236 g_object_set_data_full(G_OBJECT(gtkconv->imhtml), "transient_buddy",
3237 buddy, (GDestroyNotify)purple_buddy_destroy);
3242 if (chat)
3243 node = (PurpleBlistNode *)chat;
3244 else if (buddy)
3245 node = (PurpleBlistNode *)buddy;
3247 /* Now add the stuff */
3248 if (all) {
3249 if (buddy)
3250 pidgin_blist_make_buddy_menu(menu, buddy, TRUE);
3251 else if (chat) {
3252 /* XXX: */
3254 } else if (node) {
3255 if (purple_account_is_connected(conv->account))
3256 pidgin_append_blist_node_proto_menu(menu, conv->account->gc, node);
3257 pidgin_append_blist_node_extended_menu(menu, node);
3260 if ((list = gtk_container_get_children(GTK_CONTAINER(menu))) == NULL) {
3261 ret = FALSE;
3262 } else {
3263 g_list_free(list);
3264 ret = TRUE;
3266 return ret;
3269 static void
3270 regenerate_media_items(PidginWindow *win)
3272 #ifdef USE_VV
3273 PurpleAccount *account;
3274 PurpleConversation *conv;
3276 conv = pidgin_conv_window_get_active_conversation(win);
3278 if (conv == NULL) {
3279 purple_debug_error("gtkconv", "couldn't get active conversation"
3280 " when regenerating media items\n");
3281 return;
3284 account = purple_conversation_get_account(conv);
3286 if (account == NULL) {
3287 purple_debug_error("gtkconv", "couldn't get account when"
3288 " regenerating media items\n");
3289 return;
3293 * Check if account support voice and/or calls, and
3294 * if the current buddy supports it.
3296 if (account != NULL && purple_conversation_get_type(conv)
3297 == PURPLE_CONV_TYPE_IM) {
3298 PurpleMediaCaps caps =
3299 purple_prpl_get_media_caps(account,
3300 purple_conversation_get_name(conv));
3302 gtk_widget_set_sensitive(win->audio_call,
3303 caps & PURPLE_MEDIA_CAPS_AUDIO
3304 ? TRUE : FALSE);
3305 gtk_widget_set_sensitive(win->video_call,
3306 caps & PURPLE_MEDIA_CAPS_VIDEO
3307 ? TRUE : FALSE);
3308 gtk_widget_set_sensitive(win->audio_video_call,
3309 caps & PURPLE_MEDIA_CAPS_AUDIO_VIDEO
3310 ? TRUE : FALSE);
3311 } else if (purple_conversation_get_type(conv)
3312 == PURPLE_CONV_TYPE_CHAT) {
3313 /* for now, don't care about chats... */
3314 gtk_widget_set_sensitive(win->audio_call, FALSE);
3315 gtk_widget_set_sensitive(win->video_call, FALSE);
3316 gtk_widget_set_sensitive(win->audio_video_call, FALSE);
3317 } else {
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);
3322 #endif
3325 static void
3326 regenerate_options_items(PidginWindow *win)
3328 GtkWidget *menu;
3329 PidginConversation *gtkconv;
3330 GList *list;
3332 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
3333 menu = gtk_item_factory_get_widget(win->menu.item_factory, N_("/Conversation/More"));
3335 /* Remove the previous entries */
3336 for (list = gtk_container_get_children(GTK_CONTAINER(menu)); list; )
3338 GtkWidget *w = list->data;
3339 list = g_list_delete_link(list, list);
3340 gtk_widget_destroy(w);
3343 if (!populate_menu_with_options(menu, gtkconv, FALSE))
3345 GtkWidget *item = gtk_menu_item_new_with_label(_("No actions available"));
3346 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
3347 gtk_widget_set_sensitive(item, FALSE);
3350 gtk_widget_show_all(menu);
3353 static void
3354 remove_from_list(GtkWidget *widget, PidginWindow *win)
3356 GList *list = g_object_get_data(G_OBJECT(win->window), "plugin-actions");
3357 list = g_list_remove(list, widget);
3358 g_object_set_data(G_OBJECT(win->window), "plugin-actions", list);
3361 static void
3362 regenerate_plugins_items(PidginWindow *win)
3364 GList *action_items;
3365 GtkWidget *menu;
3366 GList *list;
3367 PidginConversation *gtkconv;
3368 PurpleConversation *conv;
3369 GtkWidget *item;
3371 if (win->window == NULL || win == hidden_convwin)
3372 return;
3374 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
3375 if (gtkconv == NULL)
3376 return;
3378 conv = gtkconv->active_conv;
3379 action_items = g_object_get_data(G_OBJECT(win->window), "plugin-actions");
3381 /* Remove the old menuitems */
3382 while (action_items) {
3383 g_signal_handlers_disconnect_by_func(G_OBJECT(action_items->data),
3384 G_CALLBACK(remove_from_list), win);
3385 gtk_widget_destroy(action_items->data);
3386 action_items = g_list_delete_link(action_items, action_items);
3389 menu = gtk_item_factory_get_widget(win->menu.item_factory, N_("/Options"));
3391 list = purple_conversation_get_extended_menu(conv);
3392 if (list) {
3393 action_items = g_list_prepend(NULL, (item = pidgin_separator(menu)));
3394 g_signal_connect(G_OBJECT(item), "destroy", G_CALLBACK(remove_from_list), win);
3397 for(; list; list = g_list_delete_link(list, list)) {
3398 PurpleMenuAction *act = (PurpleMenuAction *) list->data;
3399 item = pidgin_append_menu_action(menu, act, conv);
3400 action_items = g_list_prepend(action_items, item);
3401 gtk_widget_show_all(item);
3402 g_signal_connect(G_OBJECT(item), "destroy", G_CALLBACK(remove_from_list), win);
3404 g_object_set_data(G_OBJECT(win->window), "plugin-actions", action_items);
3407 static void menubar_activated(GtkWidget *item, gpointer data)
3409 PidginWindow *win = data;
3410 regenerate_media_items(win);
3411 regenerate_options_items(win);
3412 regenerate_plugins_items(win);
3414 /* The following are to make sure the 'More' submenu is not regenerated every time
3415 * the focus shifts from 'Conversations' to some other menu and back. */
3416 g_signal_handlers_block_by_func(G_OBJECT(item), G_CALLBACK(menubar_activated), data);
3417 g_signal_connect(G_OBJECT(win->menu.menubar), "deactivate", G_CALLBACK(focus_out_from_menubar), data);
3420 static void
3421 focus_out_from_menubar(GtkWidget *wid, PidginWindow *win)
3423 /* The menubar has been deactivated. Make sure the 'More' submenu is regenerated next time
3424 * the 'Conversation' menu pops up. */
3425 GtkWidget *menuitem = gtk_item_factory_get_item(win->menu.item_factory, N_("/Conversation"));
3426 g_signal_handlers_unblock_by_func(G_OBJECT(menuitem), G_CALLBACK(menubar_activated), win);
3427 g_signal_handlers_disconnect_by_func(G_OBJECT(win->menu.menubar),
3428 G_CALLBACK(focus_out_from_menubar), win);
3431 static GtkWidget *
3432 setup_menubar(PidginWindow *win)
3434 GtkAccelGroup *accel_group;
3435 const char *method;
3436 GtkWidget *menuitem;
3438 accel_group = gtk_accel_group_new ();
3439 gtk_window_add_accel_group(GTK_WINDOW(win->window), accel_group);
3440 g_object_unref(accel_group);
3442 win->menu.item_factory =
3443 gtk_item_factory_new(GTK_TYPE_MENU_BAR, "<main>", accel_group);
3445 gtk_item_factory_set_translate_func(win->menu.item_factory,
3446 (GtkTranslateFunc)item_factory_translate_func,
3447 NULL, NULL);
3449 gtk_item_factory_create_items(win->menu.item_factory, menu_item_count,
3450 menu_items, win);
3451 g_signal_connect(G_OBJECT(accel_group), "accel-changed",
3452 G_CALLBACK(pidgin_save_accels_cb), NULL);
3454 /* Make sure the 'Conversation -> More' menuitems are regenerated whenever
3455 * the 'Conversation' menu pops up because the entries can change after the
3456 * conversation is created. */
3457 menuitem = gtk_item_factory_get_item(win->menu.item_factory, N_("/Conversation"));
3458 g_signal_connect(G_OBJECT(menuitem), "activate", G_CALLBACK(menubar_activated), win);
3460 win->menu.menubar =
3461 gtk_item_factory_get_widget(win->menu.item_factory, "<main>");
3463 win->menu.view_log =
3464 gtk_item_factory_get_widget(win->menu.item_factory,
3465 N_("/Conversation/View Log"));
3467 #ifdef USE_VV
3468 win->audio_call =
3469 gtk_item_factory_get_widget(win->menu.item_factory,
3470 N_("/Conversation/Media/Audio Call"));
3471 win->video_call =
3472 gtk_item_factory_get_widget(win->menu.item_factory,
3473 N_("/Conversation/Media/Video Call"));
3474 win->audio_video_call =
3475 gtk_item_factory_get_widget(win->menu.item_factory,
3476 N_("/Conversation/Media/Audio\\/Video Call"));
3477 #endif
3479 /* --- */
3481 win->menu.send_file =
3482 gtk_item_factory_get_widget(win->menu.item_factory,
3483 N_("/Conversation/Send File..."));
3485 g_object_set_data(G_OBJECT(win->window), "get_attention",
3486 gtk_item_factory_get_widget(win->menu.item_factory,
3487 N_("/Conversation/Get Attention")));
3488 win->menu.add_pounce =
3489 gtk_item_factory_get_widget(win->menu.item_factory,
3490 N_("/Conversation/Add Buddy Pounce..."));
3492 /* --- */
3494 win->menu.get_info =
3495 gtk_item_factory_get_widget(win->menu.item_factory,
3496 N_("/Conversation/Get Info"));
3498 win->menu.invite =
3499 gtk_item_factory_get_widget(win->menu.item_factory,
3500 N_("/Conversation/Invite..."));
3502 /* --- */
3504 win->menu.alias =
3505 gtk_item_factory_get_widget(win->menu.item_factory,
3506 N_("/Conversation/Alias..."));
3508 win->menu.block =
3509 gtk_item_factory_get_widget(win->menu.item_factory,
3510 N_("/Conversation/Block..."));
3512 win->menu.unblock =
3513 gtk_item_factory_get_widget(win->menu.item_factory,
3514 N_("/Conversation/Unblock..."));
3516 win->menu.add =
3517 gtk_item_factory_get_widget(win->menu.item_factory,
3518 N_("/Conversation/Add..."));
3520 win->menu.remove =
3521 gtk_item_factory_get_widget(win->menu.item_factory,
3522 N_("/Conversation/Remove..."));
3524 /* --- */
3526 win->menu.insert_link =
3527 gtk_item_factory_get_widget(win->menu.item_factory,
3528 N_("/Conversation/Insert Link..."));
3530 win->menu.insert_image =
3531 gtk_item_factory_get_widget(win->menu.item_factory,
3532 N_("/Conversation/Insert Image..."));
3534 /* --- */
3536 win->menu.logging =
3537 gtk_item_factory_get_widget(win->menu.item_factory,
3538 N_("/Options/Enable Logging"));
3539 win->menu.sounds =
3540 gtk_item_factory_get_widget(win->menu.item_factory,
3541 N_("/Options/Enable Sounds"));
3542 method = purple_prefs_get_string(PIDGIN_PREFS_ROOT "/sound/method");
3543 if (purple_strequal(method, "none"))
3545 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(win->menu.sounds),
3546 FALSE);
3547 gtk_widget_set_sensitive(win->menu.sounds, FALSE);
3549 purple_prefs_connect_callback(win, PIDGIN_PREFS_ROOT "/sound/method",
3550 sound_method_pref_changed_cb, win);
3552 win->menu.show_formatting_toolbar =
3553 gtk_item_factory_get_widget(win->menu.item_factory,
3554 N_("/Options/Show Formatting Toolbars"));
3555 win->menu.show_timestamps =
3556 gtk_item_factory_get_widget(win->menu.item_factory,
3557 N_("/Options/Show Timestamps"));
3558 win->menu.show_icon = NULL;
3560 win->menu.tray = pidgin_menu_tray_new();
3561 gtk_menu_shell_append(GTK_MENU_SHELL(win->menu.menubar),
3562 win->menu.tray);
3563 gtk_widget_show(win->menu.tray);
3565 gtk_widget_show(win->menu.menubar);
3567 return win->menu.menubar;
3571 /**************************************************************************
3572 * Utility functions
3573 **************************************************************************/
3575 static void
3576 got_typing_keypress(PidginConversation *gtkconv, gboolean first)
3578 PurpleConversation *conv = gtkconv->active_conv;
3579 PurpleConvIm *im;
3582 * We know we got something, so we at least have to make sure we don't
3583 * send PURPLE_TYPED any time soon.
3586 im = PURPLE_CONV_IM(conv);
3588 purple_conv_im_stop_send_typed_timeout(im);
3589 purple_conv_im_start_send_typed_timeout(im);
3591 /* Check if we need to send another PURPLE_TYPING message */
3592 if (first || (purple_conv_im_get_type_again(im) != 0 &&
3593 time(NULL) > purple_conv_im_get_type_again(im)))
3595 unsigned int timeout;
3596 timeout = serv_send_typing(purple_conversation_get_gc(conv),
3597 purple_conversation_get_name(conv),
3598 PURPLE_TYPING);
3599 purple_conv_im_set_type_again(im, timeout);
3603 #if 0
3604 static gboolean
3605 typing_animation(gpointer data) {
3606 PidginConversation *gtkconv = data;
3607 PidginWindow *gtkwin = gtkconv->win;
3608 const char *stock_id = NULL;
3610 if(gtkconv != pidgin_conv_window_get_active_gtkconv(gtkwin)) {
3611 return FALSE;
3614 switch (rand() % 5) {
3615 case 0:
3616 stock_id = PIDGIN_STOCK_ANIMATION_TYPING0;
3617 break;
3618 case 1:
3619 stock_id = PIDGIN_STOCK_ANIMATION_TYPING1;
3620 break;
3621 case 2:
3622 stock_id = PIDGIN_STOCK_ANIMATION_TYPING2;
3623 break;
3624 case 3:
3625 stock_id = PIDGIN_STOCK_ANIMATION_TYPING3;
3626 break;
3627 case 4:
3628 stock_id = PIDGIN_STOCK_ANIMATION_TYPING4;
3629 break;
3631 if (gtkwin->menu.typing_icon == NULL) {
3632 gtkwin->menu.typing_icon = gtk_image_new_from_stock(stock_id, GTK_ICON_SIZE_MENU);
3633 pidgin_menu_tray_append(PIDGIN_MENU_TRAY(gtkwin->menu.tray),
3634 gtkwin->menu.typing_icon,
3635 _("User is typing..."));
3636 } else {
3637 gtk_image_set_from_stock(GTK_IMAGE(gtkwin->menu.typing_icon), stock_id, GTK_ICON_SIZE_MENU);
3639 gtk_widget_show(gtkwin->menu.typing_icon);
3640 return TRUE;
3642 #endif
3644 static void
3645 update_typing_message(PidginConversation *gtkconv, const char *message)
3647 GtkTextBuffer *buffer;
3648 GtkTextMark *stmark, *enmark;
3650 if (g_object_get_data(G_OBJECT(gtkconv->imhtml), "disable-typing-notification"))
3651 return;
3653 buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(gtkconv->imhtml));
3654 stmark = gtk_text_buffer_get_mark(buffer, "typing-notification-start");
3655 enmark = gtk_text_buffer_get_mark(buffer, "typing-notification-end");
3656 if (stmark && enmark) {
3657 GtkTextIter start, end;
3658 gtk_text_buffer_get_iter_at_mark(buffer, &start, stmark);
3659 gtk_text_buffer_get_iter_at_mark(buffer, &end, enmark);
3660 gtk_text_buffer_delete_mark(buffer, stmark);
3661 gtk_text_buffer_delete_mark(buffer, enmark);
3662 gtk_text_buffer_delete(buffer, &start, &end);
3663 } else if (message && *message == '\n' && message[1] == ' ' && message[2] == '\0')
3664 message = NULL;
3666 #ifdef RESERVE_LINE
3667 if (!message)
3668 message = "\n "; /* The blank space is required to avoid a GTK+/Pango bug */
3669 #endif
3671 if (message) {
3672 GtkTextIter iter;
3673 gtk_text_buffer_get_end_iter(buffer, &iter);
3674 gtk_text_buffer_create_mark(buffer, "typing-notification-start", &iter, TRUE);
3675 gtk_text_buffer_insert_with_tags_by_name(buffer, &iter, message, -1, "TYPING-NOTIFICATION", NULL);
3676 gtk_text_buffer_get_end_iter(buffer, &iter);
3677 gtk_text_buffer_create_mark(buffer, "typing-notification-end", &iter, TRUE);
3681 static void
3682 update_typing_icon(PidginConversation *gtkconv)
3684 PurpleConvIm *im = NULL;
3685 PurpleConversation *conv = gtkconv->active_conv;
3686 char *message = NULL;
3688 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM)
3689 im = PURPLE_CONV_IM(conv);
3691 if (im == NULL)
3692 return;
3694 if (purple_conv_im_get_typing_state(im) == PURPLE_NOT_TYPING) {
3695 #ifdef RESERVE_LINE
3696 update_typing_message(gtkconv, NULL);
3697 #else
3698 update_typing_message(gtkconv, "\n ");
3699 #endif
3700 return;
3703 if (purple_conv_im_get_typing_state(im) == PURPLE_TYPING) {
3704 message = g_strdup_printf(_("\n%s is typing..."), purple_conversation_get_title(conv));
3705 } else {
3706 message = g_strdup_printf(_("\n%s has stopped typing"), purple_conversation_get_title(conv));
3709 update_typing_message(gtkconv, message);
3710 g_free(message);
3713 static gboolean
3714 update_send_to_selection(PidginWindow *win)
3716 PurpleAccount *account;
3717 PurpleConversation *conv;
3718 GtkWidget *menu;
3719 GList *child;
3720 PurpleBuddy *b;
3722 conv = pidgin_conv_window_get_active_conversation(win);
3724 if (conv == NULL)
3725 return FALSE;
3727 account = purple_conversation_get_account(conv);
3729 if (account == NULL)
3730 return FALSE;
3732 if (win->menu.send_to == NULL)
3733 return FALSE;
3735 if (!(b = purple_find_buddy(account, conv->name)))
3736 return FALSE;
3739 gtk_widget_show(win->menu.send_to);
3741 menu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(win->menu.send_to));
3743 for (child = gtk_container_get_children(GTK_CONTAINER(menu));
3744 child != NULL;
3745 child = g_list_delete_link(child, child)) {
3747 GtkWidget *item = child->data;
3748 PurpleBuddy *item_buddy;
3749 PurpleAccount *item_account = g_object_get_data(G_OBJECT(item), "purple_account");
3750 gchar *buddy_name = g_object_get_data(G_OBJECT(item),
3751 "purple_buddy_name");
3752 item_buddy = purple_find_buddy(item_account, buddy_name);
3754 if (b == item_buddy) {
3755 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item), TRUE);
3756 g_list_free(child);
3757 break;
3761 return FALSE;
3764 static gboolean
3765 send_to_item_enter_notify_cb(GtkWidget *menuitem, GdkEventCrossing *event, GtkWidget *label)
3767 gtk_widget_set_sensitive(GTK_WIDGET(label), TRUE);
3768 return FALSE;
3771 static gboolean
3772 send_to_item_leave_notify_cb(GtkWidget *menuitem, GdkEventCrossing *event, GtkWidget *label)
3774 gtk_widget_set_sensitive(GTK_WIDGET(label), FALSE);
3775 return FALSE;
3778 static void
3779 create_sendto_item(GtkWidget *menu, GtkSizeGroup *sg, GSList **group, PurpleBuddy *buddy, PurpleAccount *account, const char *name)
3781 GtkWidget *box;
3782 GtkWidget *label;
3783 GtkWidget *image;
3784 GtkWidget *menuitem;
3785 GdkPixbuf *pixbuf;
3786 gchar *text;
3788 /* Create a pixmap for the protocol icon. */
3789 pixbuf = pidgin_create_prpl_icon(account, PIDGIN_PRPL_ICON_SMALL);
3791 /* Now convert it to GtkImage */
3792 if (pixbuf == NULL)
3793 image = gtk_image_new();
3794 else
3796 image = gtk_image_new_from_pixbuf(pixbuf);
3797 g_object_unref(G_OBJECT(pixbuf));
3800 gtk_size_group_add_widget(sg, image);
3802 /* Make our menu item */
3803 text = g_strdup_printf("%s (%s)", name, purple_account_get_name_for_display(account));
3804 menuitem = gtk_radio_menu_item_new_with_label(*group, text);
3805 g_free(text);
3806 *group = gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(menuitem));
3808 /* Do some evil, see some evil, speak some evil. */
3809 box = gtk_hbox_new(FALSE, 0);
3811 label = gtk_bin_get_child(GTK_BIN(menuitem));
3812 g_object_ref(label);
3813 gtk_container_remove(GTK_CONTAINER(menuitem), label);
3815 gtk_box_pack_start(GTK_BOX(box), image, FALSE, FALSE, 0);
3816 gtk_box_pack_start(GTK_BOX(box), label, TRUE, TRUE, 4);
3818 if (buddy != NULL &&
3819 !purple_presence_is_online(purple_buddy_get_presence(buddy)))
3821 gtk_widget_set_sensitive(label, FALSE);
3823 /* Set the label sensitive when the menuitem is highlighted and
3824 * insensitive again when the mouse leaves it. This way, it
3825 * doesn't appear weird from the highlighting of the embossed
3826 * (insensitive style) text.*/
3827 g_signal_connect(menuitem, "enter-notify-event",
3828 G_CALLBACK(send_to_item_enter_notify_cb), label);
3829 g_signal_connect(menuitem, "leave-notify-event",
3830 G_CALLBACK(send_to_item_leave_notify_cb), label);
3833 g_object_unref(label);
3835 gtk_container_add(GTK_CONTAINER(menuitem), box);
3837 gtk_widget_show(label);
3838 gtk_widget_show(image);
3839 gtk_widget_show(box);
3841 /* Set our data and callbacks. */
3842 g_object_set_data(G_OBJECT(menuitem), "purple_account", account);
3843 g_object_set_data_full(G_OBJECT(menuitem), "purple_buddy_name", g_strdup(name), g_free);
3845 g_signal_connect(G_OBJECT(menuitem), "activate",
3846 G_CALLBACK(menu_conv_sel_send_cb), NULL);
3848 gtk_widget_show(menuitem);
3849 gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
3852 static gboolean
3853 compare_buddy_presence(PurplePresence *p1, PurplePresence *p2)
3855 /* This is necessary because multiple PurpleBuddy's don't share the same
3856 * PurplePresence anymore.
3858 PurpleBuddy *b1 = purple_presence_get_buddy(p1);
3859 PurpleBuddy *b2 = purple_presence_get_buddy(p2);
3860 if (purple_buddy_get_account(b1) == purple_buddy_get_account(b2) &&
3861 purple_strequal(purple_buddy_get_name(b1), purple_buddy_get_name(b2)))
3862 return FALSE;
3863 return TRUE;
3866 static void
3867 generate_send_to_items(PidginWindow *win)
3869 GtkWidget *menu;
3870 GSList *group = NULL;
3871 GtkSizeGroup *sg = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL);
3872 PidginConversation *gtkconv;
3873 GSList *l, *buds;
3875 g_return_if_fail(win != NULL);
3877 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
3879 g_return_if_fail(gtkconv != NULL);
3881 if (win->menu.send_to != NULL)
3882 gtk_widget_destroy(win->menu.send_to);
3884 /* Build the Send To menu */
3885 win->menu.send_to = gtk_menu_item_new_with_mnemonic(_("S_end To"));
3886 gtk_widget_show(win->menu.send_to);
3888 menu = gtk_menu_new();
3889 gtk_menu_shell_insert(GTK_MENU_SHELL(win->menu.menubar),
3890 win->menu.send_to, 2);
3891 gtk_menu_item_set_submenu(GTK_MENU_ITEM(win->menu.send_to), menu);
3893 gtk_widget_show(menu);
3895 if (gtkconv->active_conv->type == PURPLE_CONV_TYPE_IM) {
3896 buds = purple_find_buddies(gtkconv->active_conv->account, gtkconv->active_conv->name);
3898 if (buds == NULL)
3900 /* The user isn't on the buddy list. So we don't create any sendto menu. */
3902 else
3904 GList *list = NULL, *iter;
3905 for (l = buds; l != NULL; l = l->next)
3907 PurpleBlistNode *node;
3909 node = PURPLE_BLIST_NODE(purple_buddy_get_contact(PURPLE_BUDDY(l->data)));
3911 for (node = node->child; node != NULL; node = node->next)
3913 PurpleBuddy *buddy = (PurpleBuddy *)node;
3914 PurpleAccount *account;
3916 if (!PURPLE_BLIST_NODE_IS_BUDDY(node))
3917 continue;
3919 account = purple_buddy_get_account(buddy);
3920 if (purple_account_is_connected(account) || account == gtkconv->active_conv->account)
3922 /* Use the PurplePresence to get unique buddies. */
3923 PurplePresence *presence = purple_buddy_get_presence(buddy);
3924 if (!g_list_find_custom(list, presence, (GCompareFunc)compare_buddy_presence))
3925 list = g_list_prepend(list, presence);
3930 /* Create the sendto menu only if it has more than one item to show */
3931 if (list && list->next) {
3932 /* Loop over the list backwards so we get the items in the right order,
3933 * since we did a g_list_prepend() earlier. */
3934 for (iter = g_list_last(list); iter != NULL; iter = iter->prev) {
3935 PurplePresence *pre = iter->data;
3936 PurpleBuddy *buddy = purple_presence_get_buddy(pre);
3937 create_sendto_item(menu, sg, &group, buddy,
3938 purple_buddy_get_account(buddy), purple_buddy_get_name(buddy));
3941 g_list_free(list);
3942 g_slist_free(buds);
3946 g_object_unref(sg);
3948 gtk_widget_show(win->menu.send_to);
3949 /* TODO: This should never be insensitive. Possibly hidden or not. */
3950 if (!group)
3951 gtk_widget_set_sensitive(win->menu.send_to, FALSE);
3952 update_send_to_selection(win);
3955 static const char *
3956 get_chat_buddy_status_icon(PurpleConvChat *chat, const char *name, PurpleConvChatBuddyFlags flags)
3958 const char *image = NULL;
3960 if (flags & PURPLE_CBFLAGS_FOUNDER) {
3961 image = PIDGIN_STOCK_STATUS_FOUNDER;
3962 } else if (flags & PURPLE_CBFLAGS_OP) {
3963 image = PIDGIN_STOCK_STATUS_OPERATOR;
3964 } else if (flags & PURPLE_CBFLAGS_HALFOP) {
3965 image = PIDGIN_STOCK_STATUS_HALFOP;
3966 } else if (flags & PURPLE_CBFLAGS_VOICE) {
3967 image = PIDGIN_STOCK_STATUS_VOICE;
3968 } else if ((!flags) && purple_conv_chat_is_user_ignored(chat, name)) {
3969 image = PIDGIN_STOCK_STATUS_IGNORED;
3970 } else {
3971 return NULL;
3973 return image;
3976 static void
3977 deleting_chat_buddy_cb(PurpleConvChatBuddy *cb)
3979 if (cb->ui_data) {
3980 GtkTreeRowReference *ref = cb->ui_data;
3981 gtk_tree_row_reference_free(ref);
3982 cb->ui_data = NULL;
3986 static void
3987 add_chat_buddy_common(PurpleConversation *conv, PurpleConvChatBuddy *cb, const char *old_name)
3989 PidginConversation *gtkconv;
3990 PidginChatPane *gtkchat;
3991 PurpleConvChat *chat;
3992 PurpleConnection *gc;
3993 PurplePluginProtocolInfo *prpl_info;
3994 GtkTreeModel *tm;
3995 GtkListStore *ls;
3996 GtkTreePath *newpath;
3997 const char *stock;
3998 GtkTreeIter iter;
3999 gboolean is_me = FALSE;
4000 gboolean is_buddy;
4001 gchar *tmp, *alias_key, *name, *alias;
4002 PurpleConvChatBuddyFlags flags;
4003 GdkColor *color = NULL;
4005 alias = cb->alias;
4006 name = cb->name;
4007 flags = cb->flags;
4009 chat = PURPLE_CONV_CHAT(conv);
4010 gtkconv = PIDGIN_CONVERSATION(conv);
4011 gtkchat = gtkconv->u.chat;
4012 gc = purple_conversation_get_gc(conv);
4014 if (!gc || !(prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(gc->prpl)))
4015 return;
4017 tm = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
4018 ls = GTK_LIST_STORE(tm);
4020 stock = get_chat_buddy_status_icon(chat, name, flags);
4022 if (purple_strequal(chat->nick, purple_normalize(conv->account, old_name != NULL ? old_name : name)))
4023 is_me = TRUE;
4025 is_buddy = cb->buddy;
4027 tmp = g_utf8_casefold(alias, -1);
4028 alias_key = g_utf8_collate_key(tmp, -1);
4029 g_free(tmp);
4031 if (is_me) {
4032 GtkTextTag *tag = gtk_text_tag_table_lookup(
4033 gtk_text_buffer_get_tag_table(GTK_IMHTML(gtkconv->imhtml)->text_buffer),
4034 "send-name");
4035 g_object_get(tag, "foreground-gdk", &color, NULL);
4036 } else {
4037 GtkTextTag *tag;
4038 if ((tag = get_buddy_tag(conv, name, 0, FALSE)))
4039 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_NORMAL, NULL);
4040 if ((tag = get_buddy_tag(conv, name, PURPLE_MESSAGE_NICK, FALSE)))
4041 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_NORMAL, NULL);
4042 color = (GdkColor*)get_nick_color(gtkconv, name);
4045 gtk_list_store_insert_with_values(ls, &iter,
4047 * The GTK docs are mute about the effects of the "row" value for performance.
4048 * X-Chat hardcodes their value to 0 (prepend) and -1 (append), so we will too.
4049 * It *might* be faster to search the gtk_list_store and set row accurately,
4050 * but no one in #gtk+ seems to know anything about it either.
4051 * Inserting in the "wrong" location has no visible ill effects. - F.P.
4053 -1, /* "row" */
4054 CHAT_USERS_ICON_STOCK_COLUMN, stock,
4055 CHAT_USERS_ALIAS_COLUMN, alias,
4056 CHAT_USERS_ALIAS_KEY_COLUMN, alias_key,
4057 CHAT_USERS_NAME_COLUMN, name,
4058 CHAT_USERS_FLAGS_COLUMN, flags,
4059 CHAT_USERS_COLOR_COLUMN, color,
4060 CHAT_USERS_WEIGHT_COLUMN, is_buddy ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL,
4061 -1);
4063 if (cb->ui_data) {
4064 GtkTreeRowReference *ref = cb->ui_data;
4065 gtk_tree_row_reference_free(ref);
4068 newpath = gtk_tree_model_get_path(tm, &iter);
4069 cb->ui_data = gtk_tree_row_reference_new(tm, newpath);
4070 gtk_tree_path_free(newpath);
4072 if (is_me && color)
4073 gdk_color_free(color);
4074 g_free(alias_key);
4078 * @param most_matched Used internally by this function.
4079 * @param entered The partial string that the user types before hitting the
4080 * tab key.
4081 * @param entered_bytes The length of entered.
4082 * @param partial This is a return variable. This will be set to a string
4083 * containing the largest common string between all matches. This will
4084 * be inserted into the input box at the start of the word that the
4085 * user is tab completing. For example, if a chat room contains
4086 * "AlfFan" and "AlfHater" and the user types "a<TAB>" then this will
4087 * contain "Alf"
4088 * @param nick_partial Used internally by this function. Shoudl be a
4089 * temporary buffer that is entered_bytes+1 bytes long.
4090 * @param matches This is a return variable. If the given name is a potential
4091 * match for the entered string, then add a copy of the name to this
4092 * list. The caller is responsible for g_free'ing the data in this
4093 * list.
4094 * @param name The buddy name or alias or slash command name that we're
4095 * checking for a match.
4097 static void
4098 tab_complete_process_item(int *most_matched, const char *entered, gsize entered_bytes, char **partial, char *nick_partial,
4099 GList **matches, char *name)
4101 memcpy(nick_partial, name, entered_bytes);
4102 if (purple_utf8_strcasecmp(nick_partial, entered))
4103 return;
4105 /* if we're here, it's a possible completion */
4107 if (*most_matched == -1) {
4109 * this will only get called once, since from now
4110 * on *most_matched is >= 0
4112 *most_matched = strlen(name);
4113 *partial = g_strdup(name);
4115 else if (*most_matched) {
4116 char *tmp = g_strdup(name);
4118 while (purple_utf8_strcasecmp(tmp, *partial)) {
4119 (*partial)[*most_matched] = '\0';
4120 if (*most_matched < (goffset)strlen(tmp))
4121 tmp[*most_matched] = '\0';
4122 (*most_matched)--;
4124 (*most_matched)++;
4126 g_free(tmp);
4129 *matches = g_list_insert_sorted(*matches, g_strdup(name),
4130 (GCompareFunc)purple_utf8_strcasecmp);
4133 static gboolean
4134 tab_complete(PurpleConversation *conv)
4136 PidginConversation *gtkconv;
4137 GtkTextIter cursor, word_start, start_buffer;
4138 int start;
4139 int most_matched = -1;
4140 char *entered, *partial = NULL;
4141 char *text;
4142 char *nick_partial;
4143 const char *prefix;
4144 GList *matches = NULL;
4145 gboolean command = FALSE;
4146 gsize entered_bytes = 0;
4148 gtkconv = PIDGIN_CONVERSATION(conv);
4150 gtk_text_buffer_get_start_iter(gtkconv->entry_buffer, &start_buffer);
4151 gtk_text_buffer_get_iter_at_mark(gtkconv->entry_buffer, &cursor,
4152 gtk_text_buffer_get_insert(gtkconv->entry_buffer));
4154 word_start = cursor;
4156 /* if there's nothing there just return */
4157 if (!gtk_text_iter_compare(&cursor, &start_buffer))
4158 return (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) ? TRUE : FALSE;
4160 text = gtk_text_buffer_get_text(gtkconv->entry_buffer, &start_buffer,
4161 &cursor, FALSE);
4163 /* if we're at the end of ": " we need to move back 2 spaces */
4164 start = strlen(text) - 1;
4166 if (start >= 1 && !strncmp(&text[start-1], ": ", 2)) {
4167 gtk_text_iter_backward_chars(&word_start, 2);
4170 /* find the start of the word that we're tabbing.
4171 * Using gtk_text_iter_backward_word_start won't work, because a nick can contain
4172 * characters (e.g. '.', '/' etc.) that Pango may think are word separators. */
4173 while (gtk_text_iter_backward_char(&word_start)) {
4174 if (gtk_text_iter_get_char(&word_start) == ' ') {
4175 /* Reached the whitespace before the start of the word. Move forward once */
4176 gtk_text_iter_forward_char(&word_start);
4177 break;
4181 prefix = pidgin_get_cmd_prefix();
4182 if (gtk_text_iter_get_offset(&word_start) == 0 &&
4183 (strlen(text) >= strlen(prefix)) && !strncmp(text, prefix, strlen(prefix))) {
4184 command = TRUE;
4185 gtk_text_iter_forward_chars(&word_start, strlen(prefix));
4188 g_free(text);
4190 entered = gtk_text_buffer_get_text(gtkconv->entry_buffer, &word_start,
4191 &cursor, FALSE);
4192 entered_bytes = strlen(entered);
4194 if (!g_utf8_strlen(entered, -1)) {
4195 g_free(entered);
4196 return (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) ? TRUE : FALSE;
4199 nick_partial = g_malloc0(entered_bytes + 1);
4201 if (command) {
4202 GList *list = purple_cmd_list(conv);
4203 GList *l;
4205 /* Commands */
4206 for (l = list; l != NULL; l = l->next) {
4207 tab_complete_process_item(&most_matched, entered, entered_bytes, &partial, nick_partial,
4208 &matches, l->data);
4210 g_list_free(list);
4211 } else if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) {
4212 PurpleConvChat *chat = PURPLE_CONV_CHAT(conv);
4213 GList *l = purple_conv_chat_get_users(chat);
4214 GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(PIDGIN_CONVERSATION(conv)->u.chat->list));
4215 GtkTreeIter iter;
4216 int f;
4218 /* Users */
4219 for (; l != NULL; l = l->next) {
4220 tab_complete_process_item(&most_matched, entered, entered_bytes, &partial, nick_partial,
4221 &matches, ((PurpleConvChatBuddy *)l->data)->name);
4225 /* Aliases */
4226 if (gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter))
4228 do {
4229 char *name;
4230 char *alias;
4232 gtk_tree_model_get(model, &iter,
4233 CHAT_USERS_NAME_COLUMN, &name,
4234 CHAT_USERS_ALIAS_COLUMN, &alias,
4235 -1);
4237 if (name && alias && !purple_strequal(name, alias))
4238 tab_complete_process_item(&most_matched, entered, entered_bytes, &partial, nick_partial,
4239 &matches, alias);
4240 g_free(name);
4241 g_free(alias);
4243 f = gtk_tree_model_iter_next(model, &iter);
4244 } while (f != 0);
4246 } else {
4247 g_free(nick_partial);
4248 g_free(entered);
4249 return FALSE;
4252 g_free(nick_partial);
4254 /* we're only here if we're doing new style */
4256 /* if there weren't any matches, return */
4257 if (!matches) {
4258 /* if matches isn't set partials won't be either */
4259 g_free(entered);
4260 return (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) ? TRUE : FALSE;
4263 gtk_text_buffer_delete(gtkconv->entry_buffer, &word_start, &cursor);
4265 if (!matches->next) {
4266 /* there was only one match. fill it in. */
4267 gtk_text_buffer_get_start_iter(gtkconv->entry_buffer, &start_buffer);
4268 gtk_text_buffer_get_iter_at_mark(gtkconv->entry_buffer, &cursor,
4269 gtk_text_buffer_get_insert(gtkconv->entry_buffer));
4271 if (!gtk_text_iter_compare(&cursor, &start_buffer)) {
4272 char *tmp = g_strdup_printf("%s: ", (char *)matches->data);
4273 gtk_text_buffer_insert_at_cursor(gtkconv->entry_buffer, tmp, -1);
4274 g_free(tmp);
4276 else
4277 gtk_text_buffer_insert_at_cursor(gtkconv->entry_buffer,
4278 matches->data, -1);
4280 g_free(matches->data);
4281 g_list_free(matches);
4283 else {
4285 * there were lots of matches, fill in as much as possible
4286 * and display all of them
4288 char *addthis = g_malloc0(1);
4290 while (matches) {
4291 char *tmp = addthis;
4292 addthis = g_strconcat(tmp, matches->data, " ", NULL);
4293 g_free(tmp);
4294 g_free(matches->data);
4295 matches = g_list_remove(matches, matches->data);
4298 purple_conversation_write(conv, NULL, addthis, PURPLE_MESSAGE_NO_LOG,
4299 time(NULL));
4300 gtk_text_buffer_insert_at_cursor(gtkconv->entry_buffer, partial, -1);
4301 g_free(addthis);
4304 g_free(entered);
4305 g_free(partial);
4307 return TRUE;
4310 static void topic_callback(GtkWidget *w, PidginConversation *gtkconv)
4312 PurplePluginProtocolInfo *prpl_info = NULL;
4313 PurpleConnection *gc;
4314 PurpleConversation *conv = gtkconv->active_conv;
4315 PidginChatPane *gtkchat;
4316 char *new_topic;
4317 const char *current_topic;
4319 gc = purple_conversation_get_gc(conv);
4321 if(!gc || !(prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(gc->prpl)))
4322 return;
4324 if(prpl_info->set_chat_topic == NULL)
4325 return;
4327 gtkconv = PIDGIN_CONVERSATION(conv);
4328 gtkchat = gtkconv->u.chat;
4329 new_topic = g_strdup(gtk_entry_get_text(GTK_ENTRY(gtkchat->topic_text)));
4330 current_topic = purple_conv_chat_get_topic(PURPLE_CONV_CHAT(conv));
4332 if(current_topic && !g_utf8_collate(new_topic, current_topic)){
4333 g_free(new_topic);
4334 return;
4337 if (current_topic)
4338 gtk_entry_set_text(GTK_ENTRY(gtkchat->topic_text), current_topic);
4339 else
4340 gtk_entry_set_text(GTK_ENTRY(gtkchat->topic_text), "");
4342 prpl_info->set_chat_topic(gc, purple_conv_chat_get_id(PURPLE_CONV_CHAT(conv)),
4343 new_topic);
4345 g_free(new_topic);
4348 static gint
4349 sort_chat_users(GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, gpointer userdata)
4351 PurpleConvChatBuddyFlags f1 = 0, f2 = 0;
4352 char *user1 = NULL, *user2 = NULL;
4353 gboolean buddy1 = FALSE, buddy2 = FALSE;
4354 gint ret = 0;
4356 gtk_tree_model_get(model, a,
4357 CHAT_USERS_ALIAS_KEY_COLUMN, &user1,
4358 CHAT_USERS_FLAGS_COLUMN, &f1,
4359 CHAT_USERS_WEIGHT_COLUMN, &buddy1,
4360 -1);
4361 gtk_tree_model_get(model, b,
4362 CHAT_USERS_ALIAS_KEY_COLUMN, &user2,
4363 CHAT_USERS_FLAGS_COLUMN, &f2,
4364 CHAT_USERS_WEIGHT_COLUMN, &buddy2,
4365 -1);
4367 /* Only sort by membership levels */
4368 f1 &= PURPLE_CBFLAGS_VOICE | PURPLE_CBFLAGS_HALFOP | PURPLE_CBFLAGS_OP |
4369 PURPLE_CBFLAGS_FOUNDER;
4370 f2 &= PURPLE_CBFLAGS_VOICE | PURPLE_CBFLAGS_HALFOP | PURPLE_CBFLAGS_OP |
4371 PURPLE_CBFLAGS_FOUNDER;
4373 ret = g_strcmp0(user1, user2);
4375 if (user1 != NULL && user2 != NULL) {
4376 if (f1 != f2) {
4377 /* sort more important users first */
4378 ret = (f1 > f2) ? -1 : 1;
4379 } else if (buddy1 != buddy2) {
4380 ret = (buddy1 > buddy2) ? -1 : 1;
4384 g_free(user1);
4385 g_free(user2);
4387 return ret;
4390 static void
4391 update_chat_alias(PurpleBuddy *buddy, PurpleConversation *conv, PurpleConnection *gc, PurplePluginProtocolInfo *prpl_info)
4393 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
4394 PurpleConvChat *chat = PURPLE_CONV_CHAT(conv);
4395 GtkTreeModel *model;
4396 char *normalized_name;
4397 GtkTreeIter iter;
4398 int f;
4400 g_return_if_fail(buddy != NULL);
4401 g_return_if_fail(conv != NULL);
4403 /* This is safe because this callback is only used in chats, not IMs. */
4404 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkconv->u.chat->list));
4406 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter))
4407 return;
4409 normalized_name = g_strdup(purple_normalize(conv->account, buddy->name));
4411 do {
4412 char *name;
4414 gtk_tree_model_get(model, &iter, CHAT_USERS_NAME_COLUMN, &name, -1);
4416 if (purple_strequal(normalized_name, purple_normalize(conv->account, name))) {
4417 const char *alias = name;
4418 char *tmp;
4419 char *alias_key = NULL;
4420 PurpleBuddy *buddy2;
4422 if (!purple_strequal(chat->nick, purple_normalize(conv->account, name))) {
4423 /* This user is not me, so look into updating the alias. */
4425 if ((buddy2 = purple_find_buddy(conv->account, name)) != NULL) {
4426 alias = purple_buddy_get_contact_alias(buddy2);
4429 tmp = g_utf8_casefold(alias, -1);
4430 alias_key = g_utf8_collate_key(tmp, -1);
4431 g_free(tmp);
4433 gtk_list_store_set(GTK_LIST_STORE(model), &iter,
4434 CHAT_USERS_ALIAS_COLUMN, alias,
4435 CHAT_USERS_ALIAS_KEY_COLUMN, alias_key,
4436 -1);
4437 g_free(alias_key);
4439 g_free(name);
4440 break;
4443 f = gtk_tree_model_iter_next(model, &iter);
4445 g_free(name);
4446 } while (f != 0);
4448 g_free(normalized_name);
4451 static void
4452 blist_node_aliased_cb(PurpleBlistNode *node, const char *old_alias, PurpleConversation *conv)
4454 PurpleConnection *gc;
4455 PurplePluginProtocolInfo *prpl_info;
4457 g_return_if_fail(node != NULL);
4458 g_return_if_fail(conv != NULL);
4460 gc = purple_conversation_get_gc(conv);
4461 g_return_if_fail(gc != NULL);
4462 g_return_if_fail(gc->prpl != NULL);
4463 prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(gc->prpl);
4465 if (prpl_info->options & OPT_PROTO_UNIQUE_CHATNAME)
4466 return;
4468 if (PURPLE_BLIST_NODE_IS_CONTACT(node))
4470 PurpleBlistNode *bnode;
4472 for(bnode = node->child; bnode; bnode = bnode->next) {
4474 if(!PURPLE_BLIST_NODE_IS_BUDDY(bnode))
4475 continue;
4477 update_chat_alias((PurpleBuddy *)bnode, conv, gc, prpl_info);
4480 else if (PURPLE_BLIST_NODE_IS_BUDDY(node))
4481 update_chat_alias((PurpleBuddy *)node, conv, gc, prpl_info);
4482 else if (PURPLE_BLIST_NODE_IS_CHAT(node) &&
4483 purple_conversation_get_account(conv) == ((PurpleChat*)node)->account)
4485 if (old_alias == NULL || g_utf8_collate(old_alias, purple_conversation_get_title(conv)) == 0)
4486 pidgin_conv_update_fields(conv, PIDGIN_CONV_SET_TITLE);
4490 static void
4491 buddy_cb_common(PurpleBuddy *buddy, PurpleConversation *conv, gboolean is_buddy)
4493 GtkTreeModel *model;
4494 char *normalized_name;
4495 GtkTreeIter iter;
4496 GtkTextTag *texttag;
4497 int f;
4499 g_return_if_fail(buddy != NULL);
4500 g_return_if_fail(conv != NULL);
4502 /* Do nothing if the buddy does not belong to the conv's account */
4503 if (purple_buddy_get_account(buddy) != purple_conversation_get_account(conv))
4504 return;
4506 /* This is safe because this callback is only used in chats, not IMs. */
4507 model = gtk_tree_view_get_model(GTK_TREE_VIEW(PIDGIN_CONVERSATION(conv)->u.chat->list));
4509 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter))
4510 return;
4512 normalized_name = g_strdup(purple_normalize(conv->account, buddy->name));
4514 do {
4515 char *name;
4517 gtk_tree_model_get(model, &iter, CHAT_USERS_NAME_COLUMN, &name, -1);
4519 if (purple_strequal(normalized_name, purple_normalize(conv->account, name))) {
4520 gtk_list_store_set(GTK_LIST_STORE(model), &iter,
4521 CHAT_USERS_WEIGHT_COLUMN, is_buddy ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL, -1);
4522 g_free(name);
4523 break;
4526 f = gtk_tree_model_iter_next(model, &iter);
4528 g_free(name);
4529 } while (f != 0);
4531 g_free(normalized_name);
4533 blist_node_aliased_cb((PurpleBlistNode *)buddy, NULL, conv);
4535 texttag = get_buddy_tag(conv, purple_buddy_get_name(buddy), 0, FALSE); /* XXX: do we want the normalized name? */
4536 if (texttag) {
4537 g_object_set(texttag, "weight", is_buddy ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL, NULL);
4541 static void
4542 buddy_added_cb(PurpleBlistNode *node, PurpleConversation *conv)
4544 if (!PURPLE_BLIST_NODE_IS_BUDDY(node))
4545 return;
4547 buddy_cb_common(PURPLE_BUDDY(node), conv, TRUE);
4550 static void
4551 buddy_removed_cb(PurpleBlistNode *node, PurpleConversation *conv)
4553 if (!PURPLE_BLIST_NODE_IS_BUDDY(node))
4554 return;
4556 /* If there's another buddy for the same "dude" on the list, do nothing. */
4557 if (purple_find_buddy(purple_buddy_get_account(PURPLE_BUDDY(node)),
4558 purple_buddy_get_name(PURPLE_BUDDY(node))) != NULL)
4559 return;
4561 buddy_cb_common(PURPLE_BUDDY(node), conv, FALSE);
4564 static void send_menu_cb(GtkWidget *widget, PidginConversation *gtkconv)
4566 g_signal_emit_by_name(gtkconv->entry, "message_send");
4569 static void
4570 entry_popup_menu_cb(GtkIMHtml *imhtml, GtkMenu *menu, gpointer data)
4572 GtkWidget *menuitem;
4573 PidginConversation *gtkconv = data;
4575 g_return_if_fail(menu != NULL);
4576 g_return_if_fail(gtkconv != NULL);
4578 menuitem = pidgin_new_item_from_stock(NULL, _("_Send"), NULL,
4579 G_CALLBACK(send_menu_cb), gtkconv,
4580 0, 0, NULL);
4581 if (gtk_text_buffer_get_char_count(imhtml->text_buffer) == 0)
4582 gtk_widget_set_sensitive(menuitem, FALSE);
4583 gtk_menu_shell_insert(GTK_MENU_SHELL(menu), menuitem, 0);
4585 menuitem = gtk_separator_menu_item_new();
4586 gtk_widget_show(menuitem);
4587 gtk_menu_shell_insert(GTK_MENU_SHELL(menu), menuitem, 1);
4590 static gboolean resize_imhtml_cb(PidginConversation *gtkconv)
4592 GtkTextBuffer *buffer;
4593 GtkTextIter iter;
4594 int lines;
4595 GdkRectangle oneline;
4596 int height, diff;
4597 int pad_top, pad_inside, pad_bottom;
4598 int total_height = (gtkconv->imhtml->allocation.height + gtkconv->entry->allocation.height);
4599 int max_height = total_height / 2;
4600 int min_lines = purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/minimum_entry_lines");
4601 int min_height;
4602 gboolean interior_focus;
4603 int focus_width;
4605 pad_top = gtk_text_view_get_pixels_above_lines(GTK_TEXT_VIEW(gtkconv->entry));
4606 pad_bottom = gtk_text_view_get_pixels_below_lines(GTK_TEXT_VIEW(gtkconv->entry));
4607 pad_inside = gtk_text_view_get_pixels_inside_wrap(GTK_TEXT_VIEW(gtkconv->entry));
4609 buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(gtkconv->entry));
4610 gtk_text_buffer_get_start_iter(buffer, &iter);
4611 gtk_text_view_get_iter_location(GTK_TEXT_VIEW(gtkconv->entry), &iter, &oneline);
4613 lines = gtk_text_buffer_get_line_count(buffer);
4615 height = 0;
4616 do {
4617 int lineheight = 0;
4618 gtk_text_view_get_line_yrange(GTK_TEXT_VIEW(gtkconv->entry), &iter, NULL, &lineheight);
4619 height += lineheight;
4620 lines--;
4621 } while (gtk_text_iter_forward_line(&iter));
4622 height += lines * (oneline.height + pad_top + pad_bottom);
4624 /* Make sure there's enough room for at least min_lines. Allocate enough space to
4625 * prevent scrolling when the second line is a continuation of the first line, or
4626 * is the beginning of a new paragraph. */
4627 min_height = min_lines * (oneline.height + MAX(pad_inside, pad_top + pad_bottom));
4628 height = CLAMP(height, MIN(min_height, max_height), max_height);
4630 gtk_widget_style_get(gtkconv->entry,
4631 "interior-focus", &interior_focus,
4632 "focus-line-width", &focus_width,
4633 NULL);
4634 if (!interior_focus)
4635 height += 2 * focus_width;
4637 diff = height - gtkconv->entry->allocation.height;
4638 if (ABS(diff) < oneline.height / 2)
4639 return FALSE;
4641 gtk_widget_set_size_request(gtkconv->lower_hbox, -1,
4642 diff + gtkconv->lower_hbox->allocation.height);
4644 return FALSE;
4647 static void
4648 minimum_entry_lines_pref_cb(const char *name,
4649 PurplePrefType type,
4650 gconstpointer value,
4651 gpointer data)
4653 GList *l = purple_get_conversations();
4654 PurpleConversation *conv;
4655 while (l != NULL)
4657 conv = (PurpleConversation *)l->data;
4659 if (PIDGIN_IS_PIDGIN_CONVERSATION(conv))
4660 resize_imhtml_cb(PIDGIN_CONVERSATION(conv));
4662 l = l->next;
4666 static void
4667 setup_chat_topic(PidginConversation *gtkconv, GtkWidget *vbox)
4669 PurpleConversation *conv = gtkconv->active_conv;
4670 PurpleConnection *gc = purple_conversation_get_gc(conv);
4671 PurplePluginProtocolInfo *prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(gc->prpl);
4672 if (prpl_info->options & OPT_PROTO_CHAT_TOPIC)
4674 GtkWidget *hbox, *label;
4675 PidginChatPane *gtkchat = gtkconv->u.chat;
4677 hbox = gtk_hbox_new(FALSE, PIDGIN_HIG_BOX_SPACE);
4678 gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);
4680 label = gtk_label_new(_("Topic:"));
4681 gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0);
4683 gtkchat->topic_text = gtk_entry_new();
4684 gtk_widget_set_size_request(gtkchat->topic_text, -1, BUDDYICON_SIZE_MIN);
4686 if(prpl_info->set_chat_topic == NULL) {
4687 gtk_editable_set_editable(GTK_EDITABLE(gtkchat->topic_text), FALSE);
4688 } else {
4689 g_signal_connect(GTK_OBJECT(gtkchat->topic_text), "activate",
4690 G_CALLBACK(topic_callback), gtkconv);
4693 gtk_box_pack_start(GTK_BOX(hbox), gtkchat->topic_text, TRUE, TRUE, 0);
4694 g_signal_connect(G_OBJECT(gtkchat->topic_text), "key_press_event",
4695 G_CALLBACK(entry_key_press_cb), gtkconv);
4699 static gboolean
4700 pidgin_conv_userlist_create_tooltip(GtkWidget *tipwindow, GtkTreePath *path,
4701 gpointer userdata, int *w, int *h)
4703 PidginConversation *gtkconv = userdata;
4704 GtkTreeIter iter;
4705 GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkconv->u.chat->list));
4706 PurpleConversation *conv = gtkconv->active_conv;
4707 PurpleBlistNode *node;
4708 PurplePluginProtocolInfo *prpl_info;
4709 PurpleAccount *account = purple_conversation_get_account(conv);
4710 char *who = NULL;
4712 if (account->gc == NULL)
4713 return FALSE;
4715 if (!gtk_tree_model_get_iter(GTK_TREE_MODEL(model), &iter, path))
4716 return FALSE;
4718 gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, CHAT_USERS_NAME_COLUMN, &who, -1);
4720 prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(account->gc->prpl);
4721 node = (PurpleBlistNode*)(purple_find_buddy(conv->account, who));
4722 if (node && prpl_info && (prpl_info->options & OPT_PROTO_UNIQUE_CHATNAME))
4723 pidgin_blist_draw_tooltip(node, gtkconv->infopane);
4725 g_free(who);
4726 return FALSE;
4729 static void
4730 setup_chat_userlist(PidginConversation *gtkconv, GtkWidget *hpaned)
4732 PidginChatPane *gtkchat = gtkconv->u.chat;
4733 GtkWidget *lbox, *list;
4734 GtkListStore *ls;
4735 GtkCellRenderer *rend;
4736 GtkTreeViewColumn *col;
4737 int ul_width;
4738 void *blist_handle = purple_blist_get_handle();
4739 PurpleConversation *conv = gtkconv->active_conv;
4741 /* Build the right pane. */
4742 lbox = gtk_vbox_new(FALSE, PIDGIN_HIG_BOX_SPACE);
4743 gtk_paned_pack2(GTK_PANED(hpaned), lbox, FALSE, TRUE);
4744 gtk_widget_show(lbox);
4746 /* Setup the label telling how many people are in the room. */
4747 gtkchat->count = gtk_label_new(_("0 people in room"));
4748 gtk_label_set_ellipsize(GTK_LABEL(gtkchat->count), PANGO_ELLIPSIZE_END);
4749 gtk_box_pack_start(GTK_BOX(lbox), gtkchat->count, FALSE, FALSE, 0);
4750 gtk_widget_show(gtkchat->count);
4752 /* Setup the list of users. */
4754 ls = gtk_list_store_new(CHAT_USERS_COLUMNS, GDK_TYPE_PIXBUF, G_TYPE_STRING,
4755 G_TYPE_STRING, G_TYPE_STRING, G_TYPE_INT,
4756 GDK_TYPE_COLOR, G_TYPE_INT, G_TYPE_STRING);
4757 gtk_tree_sortable_set_sort_func(GTK_TREE_SORTABLE(ls), CHAT_USERS_ALIAS_KEY_COLUMN,
4758 sort_chat_users, NULL, NULL);
4760 list = gtk_tree_view_new_with_model(GTK_TREE_MODEL(ls));
4762 /* Allow a user to specify gtkrc settings for the chat userlist only */
4763 gtk_widget_set_name(list, "pidgin_conv_userlist");
4765 rend = gtk_cell_renderer_pixbuf_new();
4766 g_object_set(G_OBJECT(rend),
4767 "stock-size", gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL),
4768 NULL);
4769 col = gtk_tree_view_column_new_with_attributes(NULL, rend,
4770 "stock-id", CHAT_USERS_ICON_STOCK_COLUMN, NULL);
4771 gtk_tree_view_column_set_sizing(col, GTK_TREE_VIEW_COLUMN_AUTOSIZE);
4772 gtk_tree_view_append_column(GTK_TREE_VIEW(list), col);
4773 ul_width = purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/userlist_width");
4774 gtk_widget_set_size_request(lbox, ul_width, -1);
4776 /* Hack to prevent completely collapsed userlist coming back with a 1 pixel width.
4777 * I would have liked to use the GtkPaned "max-position", but for some reason that didn't work */
4778 if (ul_width == 0)
4779 gtk_paned_set_position(GTK_PANED(hpaned), 999999);
4781 g_signal_connect(G_OBJECT(list), "button_press_event",
4782 G_CALLBACK(right_click_chat_cb), gtkconv);
4783 g_signal_connect(G_OBJECT(list), "row-activated",
4784 G_CALLBACK(activate_list_cb), gtkconv);
4785 g_signal_connect(G_OBJECT(list), "popup-menu",
4786 G_CALLBACK(gtkconv_chat_popup_menu_cb), gtkconv);
4787 g_signal_connect(G_OBJECT(lbox), "size-allocate", G_CALLBACK(lbox_size_allocate_cb), gtkconv);
4789 pidgin_tooltip_setup_for_treeview(list, gtkconv,
4790 pidgin_conv_userlist_create_tooltip, NULL);
4792 rend = gtk_cell_renderer_text_new();
4793 g_object_set(rend,
4794 "foreground-set", TRUE,
4795 "weight-set", TRUE,
4796 NULL);
4797 g_object_set(G_OBJECT(rend), "editable", TRUE, NULL);
4799 col = gtk_tree_view_column_new_with_attributes(NULL, rend,
4800 "text", CHAT_USERS_ALIAS_COLUMN,
4801 "foreground-gdk", CHAT_USERS_COLOR_COLUMN,
4802 "weight", CHAT_USERS_WEIGHT_COLUMN,
4803 NULL);
4805 purple_signal_connect(blist_handle, "blist-node-added",
4806 gtkchat, PURPLE_CALLBACK(buddy_added_cb), conv);
4807 purple_signal_connect(blist_handle, "blist-node-removed",
4808 gtkchat, PURPLE_CALLBACK(buddy_removed_cb), conv);
4809 purple_signal_connect(blist_handle, "blist-node-aliased",
4810 gtkchat, PURPLE_CALLBACK(blist_node_aliased_cb), conv);
4812 gtk_tree_view_column_set_expand(col, TRUE);
4813 g_object_set(rend, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
4815 gtk_tree_view_append_column(GTK_TREE_VIEW(list), col);
4817 gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(list), FALSE);
4818 gtk_widget_show(list);
4820 gtkchat->list = list;
4822 gtk_box_pack_start(GTK_BOX(lbox),
4823 pidgin_make_scrollable(list, GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC, GTK_SHADOW_IN, -1, -1),
4824 TRUE, TRUE, 0);
4827 static gboolean
4828 pidgin_conv_create_tooltip(GtkWidget *tipwindow, gpointer userdata, int *w, int *h)
4830 PurpleBlistNode *node = NULL;
4831 PurpleConversation *conv;
4832 PidginConversation *gtkconv = userdata;
4834 conv = gtkconv->active_conv;
4835 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) {
4836 node = (PurpleBlistNode*)(purple_blist_find_chat(conv->account, conv->name));
4837 if (!node)
4838 node = g_object_get_data(G_OBJECT(gtkconv->imhtml), "transient_chat");
4839 } else {
4840 node = (PurpleBlistNode*)(purple_find_buddy(conv->account, conv->name));
4841 #if 0
4842 /* Using the transient blist nodes to show the tooltip doesn't quite work yet. */
4843 if (!node)
4844 node = g_object_get_data(G_OBJECT(gtkconv->imhtml), "transient_buddy");
4845 #endif
4848 if (node)
4849 pidgin_blist_draw_tooltip(node, gtkconv->infopane);
4850 return FALSE;
4853 /* Quick Find {{{ */
4854 static gboolean
4855 pidgin_conv_end_quickfind(PidginConversation *gtkconv)
4857 gtk_widget_modify_base(gtkconv->quickfind.entry, GTK_STATE_NORMAL, NULL);
4859 gtk_imhtml_search_clear(GTK_IMHTML(gtkconv->imhtml));
4860 gtk_widget_hide_all(gtkconv->quickfind.container);
4862 gtk_widget_grab_focus(gtkconv->entry);
4863 return TRUE;
4866 static gboolean
4867 quickfind_process_input(GtkWidget *entry, GdkEventKey *event, PidginConversation *gtkconv)
4869 switch (event->keyval) {
4870 case GDK_Return:
4871 case GDK_KP_Enter:
4872 if (gtk_imhtml_search_find(GTK_IMHTML(gtkconv->imhtml), gtk_entry_get_text(GTK_ENTRY(entry)))) {
4873 gtk_widget_modify_base(gtkconv->quickfind.entry, GTK_STATE_NORMAL, NULL);
4874 } else {
4875 GdkColor col;
4876 col.red = 0xffff;
4877 col.green = 0xafff;
4878 col.blue = 0xafff;
4879 gtk_widget_modify_base(gtkconv->quickfind.entry, GTK_STATE_NORMAL, &col);
4881 break;
4882 case GDK_Escape:
4883 pidgin_conv_end_quickfind(gtkconv);
4884 break;
4885 default:
4886 return FALSE;
4888 return TRUE;
4891 static void
4892 pidgin_conv_setup_quickfind(PidginConversation *gtkconv, GtkWidget *container)
4894 GtkWidget *widget = gtk_hbox_new(FALSE, 0);
4895 GtkWidget *label, *entry, *close;
4897 gtk_box_pack_start(GTK_BOX(container), widget, FALSE, FALSE, 0);
4899 close = pidgin_create_small_button(gtk_label_new("×"));
4900 gtk_box_pack_start(GTK_BOX(widget), close, FALSE, FALSE, 0);
4901 gtk_tooltips_set_tip(gtkconv->tooltips, close,
4902 _("Close Find bar"), NULL);
4904 label = gtk_label_new(_("Find:"));
4905 gtk_box_pack_start(GTK_BOX(widget), label, FALSE, FALSE, 10);
4907 entry = gtk_entry_new();
4908 gtk_box_pack_start(GTK_BOX(widget), entry, TRUE, TRUE, 0);
4910 gtkconv->quickfind.entry = entry;
4911 gtkconv->quickfind.container = widget;
4913 /* Hook to signals and stuff */
4914 g_signal_connect(G_OBJECT(entry), "key_press_event",
4915 G_CALLBACK(quickfind_process_input), gtkconv);
4916 g_signal_connect_swapped(G_OBJECT(close), "button-press-event",
4917 G_CALLBACK(pidgin_conv_end_quickfind), gtkconv);
4920 /* }}} */
4922 static GtkWidget *
4923 setup_common_pane(PidginConversation *gtkconv)
4925 GtkWidget *vbox, *frame, *imhtml_sw, *event_box;
4926 GtkCellRenderer *rend;
4927 GtkTreePath *path;
4928 PurpleConversation *conv = gtkconv->active_conv;
4929 PurpleBuddy *buddy;
4930 gboolean chat = (conv->type == PURPLE_CONV_TYPE_CHAT);
4931 int buddyicon_size = 0;
4933 /* Setup the top part of the pane */
4934 vbox = gtk_vbox_new(FALSE, PIDGIN_HIG_BOX_SPACE);
4935 gtk_widget_show(vbox);
4937 /* Setup the info pane */
4938 event_box = gtk_event_box_new();
4939 gtk_event_box_set_visible_window(GTK_EVENT_BOX(event_box), FALSE);
4940 gtk_widget_show(event_box);
4941 gtkconv->infopane_hbox = gtk_hbox_new(FALSE, 0);
4942 gtk_box_pack_start(GTK_BOX(vbox), event_box, FALSE, FALSE, 0);
4943 gtk_container_add(GTK_CONTAINER(event_box), gtkconv->infopane_hbox);
4944 gtk_widget_show(gtkconv->infopane_hbox);
4945 gtk_widget_add_events(event_box,
4946 GDK_POINTER_MOTION_MASK | GDK_LEAVE_NOTIFY_MASK);
4947 g_signal_connect(G_OBJECT(event_box), "button-press-event",
4948 G_CALLBACK(infopane_press_cb), gtkconv);
4950 pidgin_tooltip_setup_for_widget(event_box, gtkconv,
4951 pidgin_conv_create_tooltip, NULL);
4953 gtkconv->infopane = gtk_cell_view_new();
4954 gtkconv->infopane_model = gtk_list_store_new(CONV_NUM_COLUMNS, G_TYPE_STRING, G_TYPE_STRING, GDK_TYPE_PIXBUF, GDK_TYPE_PIXBUF);
4955 gtk_cell_view_set_model(GTK_CELL_VIEW(gtkconv->infopane),
4956 GTK_TREE_MODEL(gtkconv->infopane_model));
4957 g_object_unref(gtkconv->infopane_model);
4958 gtk_list_store_append(gtkconv->infopane_model, &(gtkconv->infopane_iter));
4959 gtk_box_pack_start(GTK_BOX(gtkconv->infopane_hbox), gtkconv->infopane, TRUE, TRUE, 0);
4960 path = gtk_tree_path_new_from_string("0");
4961 gtk_cell_view_set_displayed_row(GTK_CELL_VIEW(gtkconv->infopane), path);
4962 gtk_tree_path_free(path);
4964 if (chat) {
4965 /* This empty widget is used to ensure that the infopane is consistently
4966 sized for chat windows. The correct fix is to put an icon in the chat
4967 window as well, because that would make "Set Custom Icon" consistent
4968 for both the buddy list and the chat window, but PidginConversation
4969 is pretty much stuck until 3.0. */
4970 GtkWidget *sizing_vbox;
4971 sizing_vbox = gtk_vbox_new(FALSE, 0);
4972 gtk_widget_set_size_request(sizing_vbox, -1, BUDDYICON_SIZE_MIN);
4973 gtk_box_pack_start(GTK_BOX(gtkconv->infopane_hbox), sizing_vbox, FALSE, FALSE, 0);
4974 gtk_widget_show(sizing_vbox);
4976 else {
4977 gtkconv->u.im->icon_container = gtk_vbox_new(FALSE, 0);
4979 if ((buddy = purple_find_buddy(purple_conversation_get_account(conv),
4980 purple_conversation_get_name(conv))) != NULL) {
4981 PurpleContact *contact = purple_buddy_get_contact(buddy);
4982 if (contact) {
4983 buddyicon_size = purple_blist_node_get_int((PurpleBlistNode*)contact, "pidgin-infopane-iconsize");
4986 buddyicon_size = CLAMP(buddyicon_size, BUDDYICON_SIZE_MIN, BUDDYICON_SIZE_MAX);
4987 gtk_widget_set_size_request(gtkconv->u.im->icon_container, -1, buddyicon_size);
4989 gtk_box_pack_start(GTK_BOX(gtkconv->infopane_hbox),
4990 gtkconv->u.im->icon_container, FALSE, FALSE, 0);
4992 gtk_widget_show(gtkconv->u.im->icon_container);
4995 gtk_widget_show(gtkconv->infopane);
4997 rend = gtk_cell_renderer_pixbuf_new();
4998 gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(gtkconv->infopane), rend, FALSE);
4999 gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(gtkconv->infopane), rend, "stock-id", CONV_ICON_COLUMN, NULL);
5000 g_object_set(rend, "xalign", 0.0, "xpad", 6, "ypad", 0,
5001 "stock-size", gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL),
5002 NULL);
5004 rend = gtk_cell_renderer_text_new();
5005 gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(gtkconv->infopane), rend, TRUE);
5006 gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(gtkconv->infopane), rend, "markup", CONV_TEXT_COLUMN, NULL);
5007 g_object_set(rend, "ypad", 0, "yalign", 0.5, NULL);
5009 g_object_set(rend, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
5011 rend = gtk_cell_renderer_pixbuf_new();
5012 gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(gtkconv->infopane), rend, FALSE);
5013 gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(gtkconv->infopane), rend, "pixbuf", CONV_PROTOCOL_ICON_COLUMN, NULL);
5014 g_object_set(rend, "xalign", 0.0, "xpad", 3, "ypad", 0, 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_EMBLEM_COLUMN, NULL);
5019 g_object_set(rend, "xalign", 0.0, "xpad", 6, "ypad", 0, NULL);
5021 /* Setup the gtkimhtml widget */
5022 frame = pidgin_create_imhtml(FALSE, &gtkconv->imhtml, NULL, &imhtml_sw);
5023 gtk_widget_set_size_request(gtkconv->imhtml, -1, 0);
5024 if (chat) {
5025 GtkWidget *hpaned;
5027 /* Add the topic */
5028 setup_chat_topic(gtkconv, vbox);
5030 /* Add the gtkimhtml frame */
5031 hpaned = gtk_hpaned_new();
5032 gtk_box_pack_start(GTK_BOX(vbox), hpaned, TRUE, TRUE, 0);
5033 gtk_widget_show(hpaned);
5034 gtk_paned_pack1(GTK_PANED(hpaned), frame, TRUE, TRUE);
5036 /* Now add the userlist */
5037 setup_chat_userlist(gtkconv, hpaned);
5038 } else {
5039 gtk_box_pack_start(GTK_BOX(vbox), frame, TRUE, TRUE, 0);
5041 gtk_widget_show(frame);
5043 gtk_widget_set_name(gtkconv->imhtml, "pidgin_conv_imhtml");
5044 gtk_imhtml_show_comments(GTK_IMHTML(gtkconv->imhtml),TRUE);
5045 g_object_set_data(G_OBJECT(gtkconv->imhtml), "gtkconv", gtkconv);
5047 g_object_set(G_OBJECT(imhtml_sw), "vscrollbar-policy", GTK_POLICY_ALWAYS, NULL);
5049 g_signal_connect_after(G_OBJECT(gtkconv->imhtml), "button_press_event",
5050 G_CALLBACK(entry_stop_rclick_cb), NULL);
5051 g_signal_connect(G_OBJECT(gtkconv->imhtml), "key_press_event",
5052 G_CALLBACK(refocus_entry_cb), gtkconv);
5053 g_signal_connect(G_OBJECT(gtkconv->imhtml), "key_release_event",
5054 G_CALLBACK(refocus_entry_cb), gtkconv);
5056 pidgin_conv_setup_quickfind(gtkconv, vbox);
5058 gtkconv->lower_hbox = gtk_hbox_new(FALSE, PIDGIN_HIG_BOX_SPACE);
5059 gtk_box_pack_start(GTK_BOX(vbox), gtkconv->lower_hbox, FALSE, FALSE, 0);
5060 gtk_widget_show(gtkconv->lower_hbox);
5062 /* Setup the toolbar, entry widget and all signals */
5063 frame = pidgin_create_imhtml(TRUE, &gtkconv->entry, &gtkconv->toolbar, NULL);
5064 gtk_box_pack_start(GTK_BOX(gtkconv->lower_hbox), frame, TRUE, TRUE, 0);
5065 gtk_widget_show(frame);
5067 gtk_widget_set_name(gtkconv->entry, "pidgin_conv_entry");
5068 gtk_imhtml_set_protocol_name(GTK_IMHTML(gtkconv->entry),
5069 purple_account_get_protocol_name(conv->account));
5071 g_signal_connect(G_OBJECT(gtkconv->entry), "populate-popup",
5072 G_CALLBACK(entry_popup_menu_cb), gtkconv);
5073 g_signal_connect(G_OBJECT(gtkconv->entry), "key_press_event",
5074 G_CALLBACK(entry_key_press_cb), gtkconv);
5075 g_signal_connect_after(G_OBJECT(gtkconv->entry), "message_send",
5076 G_CALLBACK(send_cb), gtkconv);
5077 g_signal_connect_after(G_OBJECT(gtkconv->entry), "button_press_event",
5078 G_CALLBACK(entry_stop_rclick_cb), NULL);
5080 gtkconv->entry_buffer =
5081 gtk_text_view_get_buffer(GTK_TEXT_VIEW(gtkconv->entry));
5082 g_object_set_data(G_OBJECT(gtkconv->entry_buffer), "user_data", gtkconv);
5084 if (!chat) {
5085 /* For sending typing notifications for IMs */
5086 g_signal_connect(G_OBJECT(gtkconv->entry_buffer), "insert_text",
5087 G_CALLBACK(insert_text_cb), gtkconv);
5088 g_signal_connect(G_OBJECT(gtkconv->entry_buffer), "delete_range",
5089 G_CALLBACK(delete_text_cb), gtkconv);
5090 gtkconv->u.im->typing_timer = 0;
5091 gtkconv->u.im->animate = purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/animate_buddy_icons");
5092 gtkconv->u.im->show_icon = TRUE;
5095 g_signal_connect_swapped(G_OBJECT(gtkconv->entry_buffer), "changed",
5096 G_CALLBACK(resize_imhtml_cb), gtkconv);
5097 g_signal_connect_swapped(G_OBJECT(gtkconv->entry), "size-allocate",
5098 G_CALLBACK(resize_imhtml_cb), gtkconv);
5100 default_formatize(gtkconv);
5101 g_signal_connect_after(G_OBJECT(gtkconv->entry), "format_function_clear",
5102 G_CALLBACK(clear_formatting_cb), gtkconv);
5103 return vbox;
5106 static void
5107 conv_dnd_recv(GtkWidget *widget, GdkDragContext *dc, guint x, guint y,
5108 GtkSelectionData *sd, guint info, guint t,
5109 PidginConversation *gtkconv)
5111 PurpleConversation *conv = gtkconv->active_conv;
5112 PidginWindow *win = gtkconv->win;
5113 PurpleConversation *c;
5114 PurpleAccount *convaccount = purple_conversation_get_account(conv);
5115 PurpleConnection *gc = purple_account_get_connection(convaccount);
5116 PurplePluginProtocolInfo *prpl_info = gc ? PURPLE_PLUGIN_PROTOCOL_INFO(gc->prpl) : NULL;
5118 if (sd->target == gdk_atom_intern("PURPLE_BLIST_NODE", FALSE))
5120 PurpleBlistNode *n = NULL;
5121 PurpleBuddy *b;
5122 PidginConversation *gtkconv = NULL;
5123 PurpleAccount *buddyaccount;
5124 const char *buddyname;
5126 n = *(PurpleBlistNode **)sd->data;
5128 if (PURPLE_BLIST_NODE_IS_CONTACT(n))
5129 b = purple_contact_get_priority_buddy((PurpleContact*)n);
5130 else if (PURPLE_BLIST_NODE_IS_BUDDY(n))
5131 b = (PurpleBuddy*)n;
5132 else
5133 return;
5135 buddyaccount = purple_buddy_get_account(b);
5136 buddyname = purple_buddy_get_name(b);
5138 * If a buddy is dragged to a chat window of the same protocol,
5139 * invite him to the chat.
5141 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT &&
5142 prpl_info && PURPLE_PROTOCOL_PLUGIN_HAS_FUNC(prpl_info, chat_invite) &&
5143 purple_strequal(purple_account_get_protocol_id(convaccount),
5144 purple_account_get_protocol_id(buddyaccount))) {
5145 purple_conv_chat_invite_user(PURPLE_CONV_CHAT(conv), buddyname, NULL, TRUE);
5146 } else {
5148 * If we already have an open conversation with this buddy, then
5149 * just move the conv to this window. Otherwise, create a new
5150 * conv and add it to this window.
5152 c = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, buddyname, buddyaccount);
5153 if (c != NULL) {
5154 PidginWindow *oldwin;
5155 gtkconv = PIDGIN_CONVERSATION(c);
5156 oldwin = gtkconv->win;
5157 if (oldwin != win) {
5158 pidgin_conv_window_remove_gtkconv(oldwin, gtkconv);
5159 pidgin_conv_window_add_gtkconv(win, gtkconv);
5161 } else {
5162 c = purple_conversation_new(PURPLE_CONV_TYPE_IM, buddyaccount, buddyname);
5163 gtkconv = PIDGIN_CONVERSATION(c);
5164 if (gtkconv->win != win) {
5165 pidgin_conv_window_remove_gtkconv(gtkconv->win, gtkconv);
5166 pidgin_conv_window_add_gtkconv(win, gtkconv);
5170 /* Make this conversation the active conversation */
5171 pidgin_conv_window_switch_gtkconv(win, gtkconv);
5174 gtk_drag_finish(dc, TRUE, (dc->action == GDK_ACTION_MOVE), t);
5176 else if (sd->target == gdk_atom_intern("application/x-im-contact", FALSE))
5178 char *protocol = NULL;
5179 char *username = NULL;
5180 PurpleAccount *account;
5181 PidginConversation *gtkconv;
5183 if (pidgin_parse_x_im_contact((const char *)sd->data, FALSE, &account,
5184 &protocol, &username, NULL))
5186 if (account == NULL)
5188 purple_notify_error(win, NULL,
5189 _("You are not currently signed on with an account that "
5190 "can add that buddy."), NULL);
5191 } else {
5193 * If a buddy is dragged to a chat window of the same protocol,
5194 * invite him to the chat.
5196 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT &&
5197 prpl_info && PURPLE_PROTOCOL_PLUGIN_HAS_FUNC(prpl_info, chat_invite) &&
5198 purple_strequal(purple_account_get_protocol_id(convaccount), protocol)) {
5199 purple_conv_chat_invite_user(PURPLE_CONV_CHAT(conv), username, NULL, TRUE);
5200 } else {
5201 c = purple_conversation_new(PURPLE_CONV_TYPE_IM, account, username);
5202 gtkconv = PIDGIN_CONVERSATION(c);
5203 if (gtkconv->win != win) {
5204 pidgin_conv_window_remove_gtkconv(gtkconv->win, gtkconv);
5205 pidgin_conv_window_add_gtkconv(win, gtkconv);
5211 g_free(username);
5212 g_free(protocol);
5214 gtk_drag_finish(dc, TRUE, (dc->action == GDK_ACTION_MOVE), t);
5216 else if (sd->target == gdk_atom_intern("text/uri-list", FALSE)) {
5217 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM)
5218 pidgin_dnd_file_manage(sd, convaccount, purple_conversation_get_name(conv));
5219 gtk_drag_finish(dc, TRUE, (dc->action == GDK_ACTION_MOVE), t);
5221 else
5222 gtk_drag_finish(dc, FALSE, FALSE, t);
5226 static const GtkTargetEntry te[] =
5228 GTK_IMHTML_DND_TARGETS,
5229 {"PURPLE_BLIST_NODE", GTK_TARGET_SAME_APP, GTK_IMHTML_DRAG_NUM},
5230 {"application/x-im-contact", 0, GTK_IMHTML_DRAG_NUM + 1}
5233 static PidginConversation *
5234 pidgin_conv_find_gtkconv(PurpleConversation * conv)
5236 PurpleBuddy *bud = purple_find_buddy(conv->account, conv->name);
5237 PurpleContact *c;
5238 PurpleBlistNode *cn, *bn;
5240 if (!bud)
5241 return NULL;
5243 if (!(c = purple_buddy_get_contact(bud)))
5244 return NULL;
5246 cn = PURPLE_BLIST_NODE(c);
5247 for (bn = purple_blist_node_get_first_child(cn); bn; bn = purple_blist_node_get_sibling_next(bn)) {
5248 PurpleBuddy *b = PURPLE_BUDDY(bn);
5249 PurpleConversation *conv;
5250 if ((conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, b->name, b->account))) {
5251 if (conv->ui_data)
5252 return conv->ui_data;
5256 return NULL;
5259 static void
5260 buddy_update_cb(PurpleBlistNode *bnode, gpointer null)
5262 GList *list;
5264 g_return_if_fail(bnode);
5265 if (!PURPLE_BLIST_NODE_IS_BUDDY(bnode))
5266 return;
5268 for (list = pidgin_conv_windows_get_list(); list; list = list->next)
5270 PidginWindow *win = list->data;
5271 PurpleConversation *conv = pidgin_conv_window_get_active_conversation(win);
5273 if (purple_conversation_get_type(conv) != PURPLE_CONV_TYPE_IM)
5274 continue;
5276 pidgin_conv_update_fields(conv, PIDGIN_CONV_MENU);
5280 static gboolean
5281 ignore_middle_click(GtkWidget *widget, GdkEventButton *e, gpointer null)
5283 /* A click on the pane is propagated to the notebook containing the pane.
5284 * So if Stu accidentally aims high and middle clicks on the pane-handle,
5285 * it causes a conversation tab to close. Let's stop that from happening.
5287 if (e->button == 2 && e->type == GDK_BUTTON_PRESS)
5288 return TRUE;
5289 return FALSE;
5292 static void set_typing_font(GtkWidget *widget, GtkStyle *style, PidginConversation *gtkconv)
5294 static PangoFontDescription *font_desc = NULL;
5295 static GdkColor *color = NULL;
5296 static gboolean enable = TRUE;
5298 if (font_desc == NULL) {
5299 char *string = NULL;
5300 gtk_widget_style_get(widget,
5301 "typing-notification-font", &string,
5302 "typing-notification-color", &color,
5303 "typing-notification-enable", &enable,
5304 NULL);
5305 font_desc = pango_font_description_from_string(string);
5306 g_free(string);
5307 if (color == NULL) {
5308 GdkColor def = {0, 0x8888, 0x8888, 0x8888};
5309 color = gdk_color_copy(&def);
5313 gtk_text_buffer_create_tag(GTK_IMHTML(widget)->text_buffer, "TYPING-NOTIFICATION",
5314 "foreground-gdk", color,
5315 "font-desc", font_desc,
5316 NULL);
5318 if (!enable) {
5319 g_object_set_data(G_OBJECT(widget), "disable-typing-notification", GINT_TO_POINTER(TRUE));
5320 /* or may be 'gtkconv->disable_typing = TRUE;' instead? */
5323 g_signal_handlers_disconnect_by_func(G_OBJECT(widget), set_typing_font, gtkconv);
5326 /**************************************************************************
5327 * Conversation UI operations
5328 **************************************************************************/
5329 static void
5330 private_gtkconv_new(PurpleConversation *conv, gboolean hidden)
5332 PidginConversation *gtkconv;
5333 PurpleConversationType conv_type = purple_conversation_get_type(conv);
5334 GtkWidget *pane = NULL;
5335 GtkWidget *tab_cont;
5336 PurpleBlistNode *convnode;
5337 PurpleValue *value;
5339 if (conv_type == PURPLE_CONV_TYPE_IM && (gtkconv = pidgin_conv_find_gtkconv(conv))) {
5340 conv->ui_data = gtkconv;
5341 if (!g_list_find(gtkconv->convs, conv))
5342 gtkconv->convs = g_list_prepend(gtkconv->convs, conv);
5343 pidgin_conv_switch_active_conversation(conv);
5344 return;
5347 gtkconv = g_new0(PidginConversation, 1);
5348 conv->ui_data = gtkconv;
5349 gtkconv->active_conv = conv;
5350 gtkconv->convs = g_list_prepend(gtkconv->convs, conv);
5351 gtkconv->send_history = g_list_append(NULL, NULL);
5353 /* Setup some initial variables. */
5354 gtkconv->tooltips = gtk_tooltips_new();
5355 gtkconv->unseen_state = PIDGIN_UNSEEN_NONE;
5356 gtkconv->unseen_count = 0;
5358 if (conv_type == PURPLE_CONV_TYPE_IM) {
5359 gtkconv->u.im = g_malloc0(sizeof(PidginImPane));
5360 } else if (conv_type == PURPLE_CONV_TYPE_CHAT) {
5361 gtkconv->u.chat = g_malloc0(sizeof(PidginChatPane));
5363 pane = setup_common_pane(gtkconv);
5365 gtk_imhtml_set_format_functions(GTK_IMHTML(gtkconv->imhtml),
5366 gtk_imhtml_get_format_functions(GTK_IMHTML(gtkconv->imhtml)) | GTK_IMHTML_IMAGE);
5368 if (pane == NULL) {
5369 if (conv_type == PURPLE_CONV_TYPE_CHAT)
5370 g_free(gtkconv->u.chat);
5371 else if (conv_type == PURPLE_CONV_TYPE_IM)
5372 g_free(gtkconv->u.im);
5374 g_free(gtkconv);
5375 conv->ui_data = NULL;
5376 return;
5379 /* Setup drag-and-drop */
5380 gtk_drag_dest_set(pane,
5381 GTK_DEST_DEFAULT_MOTION |
5382 GTK_DEST_DEFAULT_DROP,
5383 te, sizeof(te) / sizeof(GtkTargetEntry),
5384 GDK_ACTION_COPY);
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(gtkconv->imhtml, 0,
5391 te, sizeof(te) / sizeof(GtkTargetEntry),
5392 GDK_ACTION_COPY);
5394 gtk_drag_dest_set(gtkconv->entry, 0,
5395 te, sizeof(te) / sizeof(GtkTargetEntry),
5396 GDK_ACTION_COPY);
5398 g_signal_connect(G_OBJECT(pane), "button_press_event",
5399 G_CALLBACK(ignore_middle_click), NULL);
5400 g_signal_connect(G_OBJECT(pane), "drag_data_received",
5401 G_CALLBACK(conv_dnd_recv), gtkconv);
5402 g_signal_connect(G_OBJECT(gtkconv->imhtml), "drag_data_received",
5403 G_CALLBACK(conv_dnd_recv), gtkconv);
5404 g_signal_connect(G_OBJECT(gtkconv->entry), "drag_data_received",
5405 G_CALLBACK(conv_dnd_recv), gtkconv);
5407 g_signal_connect(gtkconv->imhtml, "style-set", G_CALLBACK(set_typing_font), gtkconv);
5409 /* Setup the container for the tab. */
5410 gtkconv->tab_cont = tab_cont = gtk_vbox_new(FALSE, PIDGIN_HIG_BOX_SPACE);
5411 g_object_set_data(G_OBJECT(tab_cont), "PidginConversation", gtkconv);
5412 gtk_container_set_border_width(GTK_CONTAINER(tab_cont), PIDGIN_HIG_BOX_SPACE);
5413 gtk_container_add(GTK_CONTAINER(tab_cont), pane);
5414 gtk_widget_show(pane);
5416 convnode = get_conversation_blist_node(conv);
5417 if (convnode == NULL || !purple_blist_node_get_bool(convnode, "gtk-mute-sound"))
5418 gtkconv->make_sound = TRUE;
5420 if (convnode != NULL &&
5421 (value = g_hash_table_lookup(convnode->settings, "enable-logging")) &&
5422 purple_value_get_type(value) == PURPLE_TYPE_BOOLEAN)
5424 purple_conversation_set_logging(conv, purple_value_get_boolean(value));
5427 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/show_formatting_toolbar"))
5428 gtk_widget_show(gtkconv->toolbar);
5429 else
5430 gtk_widget_hide(gtkconv->toolbar);
5432 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons"))
5433 gtk_widget_show(gtkconv->infopane_hbox);
5434 else
5435 gtk_widget_hide(gtkconv->infopane_hbox);
5437 gtk_imhtml_show_comments(GTK_IMHTML(gtkconv->imhtml),
5438 purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/show_timestamps"));
5439 gtk_imhtml_set_protocol_name(GTK_IMHTML(gtkconv->imhtml),
5440 purple_account_get_protocol_name(conv->account));
5442 g_signal_connect_swapped(G_OBJECT(pane), "focus",
5443 G_CALLBACK(gtk_widget_grab_focus),
5444 gtkconv->entry);
5446 if (hidden)
5447 pidgin_conv_window_add_gtkconv(hidden_convwin, gtkconv);
5448 else
5449 pidgin_conv_placement_place(gtkconv);
5451 if (nick_colors == NULL) {
5452 nbr_nick_colors = NUM_NICK_COLORS;
5453 nick_colors = generate_nick_colors(&nbr_nick_colors, gtk_widget_get_style(gtkconv->imhtml)->base[GTK_STATE_NORMAL]);
5456 if (conv->features & PURPLE_CONNECTION_ALLOW_CUSTOM_SMILEY)
5457 pidgin_themes_smiley_themeize_custom(gtkconv->entry);
5460 static void
5461 pidgin_conv_new_hidden(PurpleConversation *conv)
5463 private_gtkconv_new(conv, TRUE);
5466 void
5467 pidgin_conv_new(PurpleConversation *conv)
5469 private_gtkconv_new(conv, FALSE);
5470 if (PIDGIN_IS_PIDGIN_CONVERSATION(conv))
5471 purple_signal_emit(pidgin_conversations_get_handle(),
5472 "conversation-displayed", PIDGIN_CONVERSATION(conv));
5475 static void
5476 received_im_msg_cb(PurpleAccount *account, char *sender, char *message,
5477 PurpleConversation *conv, PurpleMessageFlags flags)
5479 PurpleConversationUiOps *ui_ops = pidgin_conversations_get_conv_ui_ops();
5480 gboolean hide = FALSE;
5481 guint timer;
5483 /* create hidden conv if hide_new pref is always */
5484 if (purple_strequal(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new"), "always"))
5485 hide = TRUE;
5487 /* create hidden conv if hide_new pref is away and account is away */
5488 if (purple_strequal(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new"), "away") &&
5489 !purple_status_is_available(purple_account_get_active_status(account)))
5490 hide = TRUE;
5492 if (conv && PIDGIN_IS_PIDGIN_CONVERSATION(conv) && !hide) {
5493 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
5494 if (gtkconv->win == hidden_convwin) {
5495 pidgin_conv_attach_to_conversation(gtkconv->active_conv);
5497 return;
5500 if (hide) {
5501 ui_ops->create_conversation = pidgin_conv_new_hidden;
5502 purple_conversation_new(PURPLE_CONV_TYPE_IM, account, sender);
5503 ui_ops->create_conversation = pidgin_conv_new;
5506 /* Somebody wants to keep this conversation around, so don't time it out */
5507 if (conv) {
5508 timer = GPOINTER_TO_INT(purple_conversation_get_data(conv, "close-timer"));
5509 if (timer) {
5510 purple_timeout_remove(timer);
5511 purple_conversation_set_data(conv, "close-timer", GINT_TO_POINTER(0));
5516 static void
5517 pidgin_conv_destroy(PurpleConversation *conv)
5519 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
5521 gtkconv->convs = g_list_remove(gtkconv->convs, conv);
5522 /* Don't destroy ourselves until all our convos are gone */
5523 if (gtkconv->convs) {
5524 /* Make sure the destroyed conversation is not the active one */
5525 if (gtkconv->active_conv == conv) {
5526 gtkconv->active_conv = gtkconv->convs->data;
5527 purple_conversation_update(gtkconv->active_conv, PURPLE_CONV_UPDATE_FEATURES);
5529 return;
5532 pidgin_conv_window_remove_gtkconv(gtkconv->win, gtkconv);
5534 /* If the "Save Conversation" or "Save Icon" dialogs are open then close them */
5535 purple_request_close_with_handle(gtkconv);
5536 purple_notify_close_with_handle(gtkconv);
5538 gtk_widget_destroy(gtkconv->tab_cont);
5539 g_object_unref(gtkconv->tab_cont);
5541 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) {
5542 if (gtkconv->u.im->icon_timer != 0)
5543 g_source_remove(gtkconv->u.im->icon_timer);
5545 if (gtkconv->u.im->anim != NULL)
5546 g_object_unref(G_OBJECT(gtkconv->u.im->anim));
5548 if (gtkconv->u.im->typing_timer != 0)
5549 g_source_remove(gtkconv->u.im->typing_timer);
5551 g_free(gtkconv->u.im);
5552 } else if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) {
5553 purple_signals_disconnect_by_handle(gtkconv->u.chat);
5554 g_free(gtkconv->u.chat);
5557 gtk_object_sink(GTK_OBJECT(gtkconv->tooltips));
5559 gtkconv->send_history = g_list_first(gtkconv->send_history);
5560 g_list_foreach(gtkconv->send_history, (GFunc)g_free, NULL);
5561 g_list_free(gtkconv->send_history);
5563 if (gtkconv->attach.timer) {
5564 g_source_remove(gtkconv->attach.timer);
5567 g_free(gtkconv);
5571 static void
5572 pidgin_conv_write_im(PurpleConversation *conv, const char *who,
5573 const char *message, PurpleMessageFlags flags,
5574 time_t mtime)
5576 PidginConversation *gtkconv;
5578 gtkconv = PIDGIN_CONVERSATION(conv);
5580 if (conv != gtkconv->active_conv &&
5581 flags & PURPLE_MESSAGE_ACTIVE_ONLY)
5583 /* Plugins that want these messages suppressed should be
5584 * calling purple_conv_im_write(), so they get suppressed here,
5585 * before being written to the log. */
5586 purple_debug_info("gtkconv",
5587 "Suppressing message for an inactive conversation in pidgin_conv_write_im()\n");
5588 return;
5591 purple_conversation_write(conv, who, message, flags, mtime);
5594 static const char *
5595 get_text_tag_color(GtkTextTag *tag)
5597 GdkColor *color = NULL;
5598 gboolean set = FALSE;
5599 static char colcode[] = "#XXXXXX";
5600 if (tag)
5601 g_object_get(G_OBJECT(tag), "foreground-set", &set, "foreground-gdk", &color, NULL);
5602 if (set && color)
5603 g_snprintf(colcode, sizeof(colcode), "#%02x%02x%02x",
5604 color->red >> 8, color->green >> 8, color->blue >> 8);
5605 else
5606 colcode[0] = '\0';
5607 if (color)
5608 gdk_color_free(color);
5609 return colcode;
5612 /* The callback for an event on a link tag. */
5613 static gboolean buddytag_event(GtkTextTag *tag, GObject *imhtml,
5614 GdkEvent *event, GtkTextIter *arg2, gpointer data)
5616 if (event->type == GDK_BUTTON_PRESS
5617 || event->type == GDK_2BUTTON_PRESS) {
5618 GdkEventButton *btn_event = (GdkEventButton*) event;
5619 PurpleConversation *conv = data;
5620 char *buddyname;
5622 /* strlen("BUDDY " or "HILIT ") == 6 */
5623 g_return_val_if_fail((tag->name != NULL)
5624 && (strlen(tag->name) > 6), FALSE);
5626 buddyname = (tag->name) + 6;
5628 /* emit chat-nick-clicked signal */
5629 if (event->type == GDK_BUTTON_PRESS) {
5630 gint plugin_return = GPOINTER_TO_INT(purple_signal_emit_return_1(
5631 pidgin_conversations_get_handle(), "chat-nick-clicked",
5632 data, buddyname, btn_event->button));
5633 if (plugin_return)
5634 return TRUE;
5637 if (btn_event->button == 1 &&
5638 event->type == GDK_2BUTTON_PRESS) {
5639 chat_do_im(PIDGIN_CONVERSATION(conv), buddyname);
5640 return TRUE;
5641 } else if (btn_event->button == 2
5642 && event->type == GDK_2BUTTON_PRESS) {
5643 chat_do_info(PIDGIN_CONVERSATION(conv), buddyname);
5645 return TRUE;
5646 } else if (btn_event->button == 3
5647 && event->type == GDK_BUTTON_PRESS) {
5648 GtkTextIter start, end;
5650 /* we shouldn't display the popup
5651 * if the user has selected something: */
5652 if (!gtk_text_buffer_get_selection_bounds(
5653 gtk_text_iter_get_buffer(arg2),
5654 &start, &end)) {
5655 GtkWidget *menu = NULL;
5656 PurpleConnection *gc =
5657 purple_conversation_get_gc(conv);
5660 menu = create_chat_menu(conv, buddyname, gc);
5661 gtk_menu_popup(GTK_MENU(menu), NULL, NULL,
5662 NULL, GTK_WIDGET(imhtml),
5663 btn_event->button,
5664 btn_event->time);
5666 /* Don't propagate the event any further */
5667 return TRUE;
5672 return FALSE;
5675 static GtkTextTag *get_buddy_tag(PurpleConversation *conv, const char *who, PurpleMessageFlags flag,
5676 gboolean create)
5678 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
5679 GtkTextTag *buddytag;
5680 gchar *str;
5681 gboolean highlight = (flag & PURPLE_MESSAGE_NICK);
5682 GtkTextBuffer *buffer = GTK_IMHTML(gtkconv->imhtml)->text_buffer;
5684 str = g_strdup_printf(highlight ? "HILIT %s" : "BUDDY %s", who);
5686 buddytag = gtk_text_tag_table_lookup(
5687 gtk_text_buffer_get_tag_table(buffer), str);
5689 if (buddytag == NULL && create) {
5690 if (highlight)
5691 buddytag = gtk_text_buffer_create_tag(buffer, str,
5692 "foreground", get_text_tag_color(gtk_text_tag_table_lookup(
5693 gtk_text_buffer_get_tag_table(buffer), "highlight-name")),
5694 "weight", PANGO_WEIGHT_BOLD,
5695 NULL);
5696 else
5697 buddytag = gtk_text_buffer_create_tag(
5698 buffer, str,
5699 "foreground-gdk", get_nick_color(gtkconv, who),
5700 "weight", purple_find_buddy(purple_conversation_get_account(conv), who) ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL,
5701 NULL);
5703 g_object_set_data(G_OBJECT(buddytag), "cursor", "");
5704 g_signal_connect(G_OBJECT(buddytag), "event",
5705 G_CALLBACK(buddytag_event), conv);
5708 g_free(str);
5710 return buddytag;
5713 static void pidgin_conv_calculate_newday(PidginConversation *gtkconv, time_t mtime)
5715 struct tm *tm = localtime(&mtime);
5717 tm->tm_hour = tm->tm_min = tm->tm_sec = 0;
5718 tm->tm_mday++;
5720 gtkconv->newday = mktime(tm);
5723 /* Detect string direction and encapsulate the string in RLE/LRE/PDF unicode characters
5724 str - pointer to string (string is re-allocated and the pointer updated) */
5725 static void
5726 str_embed_direction_chars(char **str)
5728 #ifdef HAVE_PANGO14
5729 char pre_str[4];
5730 char post_str[10];
5731 char *ret;
5733 if (PANGO_DIRECTION_RTL == pango_find_base_dir(*str, -1))
5735 sprintf(pre_str, "%c%c%c",
5736 0xE2, 0x80, 0xAB); /* RLE */
5737 sprintf(post_str, "%c%c%c%c%c%c%c%c%c",
5738 0xE2, 0x80, 0xAC, /* PDF */
5739 0xE2, 0x80, 0x8E, /* LRM */
5740 0xE2, 0x80, 0xAC); /* PDF */
5742 else
5744 sprintf(pre_str, "%c%c%c",
5745 0xE2, 0x80, 0xAA); /* LRE */
5746 sprintf(post_str, "%c%c%c%c%c%c%c%c%c",
5747 0xE2, 0x80, 0xAC, /* PDF */
5748 0xE2, 0x80, 0x8F, /* RLM */
5749 0xE2, 0x80, 0xAC); /* PDF */
5752 ret = g_strconcat(pre_str, *str, post_str, NULL);
5754 g_free(*str);
5755 *str = ret;
5756 #endif
5759 static void
5760 pidgin_conv_write_conv(PurpleConversation *conv, const char *name, const char *alias,
5761 const char *message, PurpleMessageFlags flags,
5762 time_t mtime)
5764 PidginConversation *gtkconv;
5765 PurpleConnection *gc;
5766 PurpleAccount *account;
5767 int gtk_font_options = 0;
5768 int gtk_font_options_all = 0;
5769 int max_scrollback_lines;
5770 int line_count;
5771 char buf2[BUF_LONG];
5772 gboolean show_date;
5773 char *mdate;
5774 char *str;
5775 char *with_font_tag;
5776 char *sml_attrib = NULL;
5777 size_t length;
5778 PurpleConversationType type;
5779 char *displaying;
5780 gboolean plugin_return;
5781 char *bracket;
5782 int tag_count = 0;
5783 gboolean is_rtl_message = FALSE;
5785 g_return_if_fail(conv != NULL);
5786 gtkconv = PIDGIN_CONVERSATION(conv);
5787 g_return_if_fail(gtkconv != NULL);
5789 if (gtkconv->attach.timer) {
5790 /* We are currently in the process of filling up the buffer with the message
5791 * history of the conversation. So we do not need to add the message here.
5792 * Instead, this message will be added to the message-list, which in turn will
5793 * be processed and displayed by the attach-callback.
5795 return;
5798 if (conv != gtkconv->active_conv)
5800 if (flags & PURPLE_MESSAGE_ACTIVE_ONLY)
5802 /* Unless this had PURPLE_MESSAGE_NO_LOG, this message
5803 * was logged. Plugin writers: if this isn't what
5804 * you wanted, call purple_conv_im_write() instead of
5805 * purple_conversation_write(). */
5806 purple_debug_info("gtkconv",
5807 "Suppressing message for an inactive conversation in pidgin_conv_write_conv()\n");
5808 return;
5811 /* Set the active conversation to the one that just messaged us. */
5812 /* TODO: consider not doing this if the account is offline or something */
5813 if (flags & (PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_RECV))
5814 pidgin_conv_switch_active_conversation(conv);
5817 type = purple_conversation_get_type(conv);
5818 account = purple_conversation_get_account(conv);
5819 g_return_if_fail(account != NULL);
5820 gc = purple_account_get_connection(account);
5821 g_return_if_fail(gc != NULL || !(flags & (PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_RECV)));
5823 /* Make sure URLs are clickable */
5824 if(flags & PURPLE_MESSAGE_NO_LINKIFY)
5825 displaying = g_strdup(message);
5826 else
5827 displaying = purple_markup_linkify(message);
5829 plugin_return = GPOINTER_TO_INT(purple_signal_emit_return_1(
5830 pidgin_conversations_get_handle(), (type == PURPLE_CONV_TYPE_IM ?
5831 "displaying-im-msg" : "displaying-chat-msg"),
5832 account, name, &displaying, conv, flags));
5833 if (plugin_return)
5835 g_free(displaying);
5836 return;
5838 length = strlen(displaying) + 1;
5840 /* Awful hack to work around GtkIMHtml's inefficient rendering of messages with lots of formatting changes.
5841 * If a message has over 100 '<' characters, strip formatting before appending it. Hopefully nobody actually
5842 * needs that much formatting, anyway.
5844 for (bracket = strchr(displaying, '<'); bracket && *(bracket + 1); bracket = strchr(bracket + 1, '<'))
5845 tag_count++;
5847 if (tag_count > 100) {
5848 char *tmp = displaying;
5849 displaying = purple_markup_strip_html(tmp);
5850 g_free(tmp);
5853 line_count = gtk_text_buffer_get_line_count(
5854 gtk_text_view_get_buffer(GTK_TEXT_VIEW(
5855 gtkconv->imhtml)));
5857 max_scrollback_lines = purple_prefs_get_int(
5858 PIDGIN_PREFS_ROOT "/conversations/scrollback_lines");
5859 /* If we're sitting at more than 100 lines more than the
5860 max scrollback, trim down to max scrollback */
5861 if (max_scrollback_lines > 0
5862 && line_count > (max_scrollback_lines + 100)) {
5863 GtkTextBuffer *text_buffer = gtk_text_view_get_buffer(
5864 GTK_TEXT_VIEW(gtkconv->imhtml));
5865 GtkTextIter start, end;
5867 gtk_text_buffer_get_start_iter(text_buffer, &start);
5868 gtk_text_buffer_get_iter_at_line(text_buffer, &end,
5869 (line_count - max_scrollback_lines));
5870 gtk_imhtml_delete(GTK_IMHTML(gtkconv->imhtml), &start, &end);
5873 if (type == PURPLE_CONV_TYPE_CHAT)
5875 /* Create anchor for user */
5876 GtkTextIter iter;
5877 char *tmp = g_strconcat("user:", name, NULL);
5879 gtk_text_buffer_get_end_iter(gtk_text_view_get_buffer(GTK_TEXT_VIEW(gtkconv->imhtml)), &iter);
5880 gtk_text_buffer_create_mark(gtk_text_view_get_buffer(GTK_TEXT_VIEW(gtkconv->imhtml)),
5881 tmp, &iter, TRUE);
5882 g_free(tmp);
5885 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/use_smooth_scrolling"))
5886 gtk_font_options_all |= GTK_IMHTML_USE_SMOOTHSCROLLING;
5888 if (gtk_text_buffer_get_char_count(gtk_text_view_get_buffer(GTK_TEXT_VIEW(gtkconv->imhtml))))
5889 gtk_imhtml_append_text(GTK_IMHTML(gtkconv->imhtml), "<BR>", gtk_font_options_all | GTK_IMHTML_NO_SCROLL);
5891 /* First message in a conversation. */
5892 if (gtkconv->newday == 0)
5893 pidgin_conv_calculate_newday(gtkconv, mtime);
5895 /* Show the date on the first message in a new day, or if the message is
5896 * older than 20 minutes. */
5897 show_date = (mtime >= gtkconv->newday) || (time(NULL) > mtime + 20*60);
5899 mdate = purple_signal_emit_return_1(pidgin_conversations_get_handle(),
5900 "conversation-timestamp",
5901 conv, mtime, show_date);
5903 if (mdate == NULL)
5905 struct tm *tm = localtime(&mtime);
5906 const char *tmp;
5907 if (show_date)
5908 tmp = purple_date_format_long(tm);
5909 else
5910 tmp = purple_time_format(tm);
5911 mdate = g_strdup_printf("(%s)", tmp);
5914 /* Bi-Directional support - set timestamp direction using unicode characters */
5915 is_rtl_message = purple_markup_is_rtl(message);
5917 /* Handle plaintext messages with RTL text but no direction in the markup */
5918 if (!is_rtl_message && pango_find_base_dir(message, -1) == PANGO_DIRECTION_RTL)
5920 char *wrapped = g_strdup_printf("<SPAN style=\"direction:rtl;text-align:right;\">%s</SPAN>", displaying);
5922 g_free(displaying);
5923 displaying = wrapped;
5925 length = strlen(displaying) + 1;
5926 is_rtl_message = TRUE;
5929 /* Enforce direction only if message is RTL - doesn't effect LTR users */
5930 if (is_rtl_message)
5931 str_embed_direction_chars(&mdate);
5933 if (mtime >= gtkconv->newday)
5934 pidgin_conv_calculate_newday(gtkconv, mtime);
5936 sml_attrib = g_strdup_printf("sml=\"%s\"", purple_account_get_protocol_name(account));
5938 gtk_font_options |= GTK_IMHTML_NO_COMMENTS;
5940 if ((flags & PURPLE_MESSAGE_RECV) &&
5941 !purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/show_incoming_formatting"))
5942 gtk_font_options |= GTK_IMHTML_NO_COLOURS | GTK_IMHTML_NO_FONTS | GTK_IMHTML_NO_SIZES | GTK_IMHTML_NO_FORMATTING;
5944 /* this is gonna crash one day, I can feel it. */
5945 if (PURPLE_PLUGIN_PROTOCOL_INFO(purple_find_prpl(purple_account_get_protocol_id(conv->account)))->options &
5946 OPT_PROTO_USE_POINTSIZE) {
5947 gtk_font_options |= GTK_IMHTML_USE_POINTSIZE;
5950 if (!(flags & PURPLE_MESSAGE_RECV) && (conv->features & PURPLE_CONNECTION_ALLOW_CUSTOM_SMILEY))
5952 /* We want to see our own smileys. Need to revert it after send*/
5953 pidgin_themes_smiley_themeize_custom(gtkconv->imhtml);
5956 /* TODO: These colors should not be hardcoded so log.c can use them */
5957 if (flags & PURPLE_MESSAGE_RAW) {
5958 gtk_imhtml_append_text(GTK_IMHTML(gtkconv->imhtml), message, gtk_font_options_all);
5959 } else if (flags & PURPLE_MESSAGE_SYSTEM) {
5960 g_snprintf(buf2, sizeof(buf2),
5961 "<FONT %s><FONT SIZE=\"2\"><!--%s --></FONT><B>%s</B></FONT>",
5962 sml_attrib ? sml_attrib : "", mdate, displaying);
5964 gtk_imhtml_append_text(GTK_IMHTML(gtkconv->imhtml), buf2, gtk_font_options_all);
5966 } else if (flags & PURPLE_MESSAGE_ERROR) {
5967 g_snprintf(buf2, sizeof(buf2),
5968 "<FONT COLOR=\"#ff0000\"><FONT %s><FONT SIZE=\"2\"><!--%s --></FONT><B>%s</B></FONT></FONT>",
5969 sml_attrib ? sml_attrib : "", mdate, displaying);
5971 gtk_imhtml_append_text(GTK_IMHTML(gtkconv->imhtml), buf2, gtk_font_options_all);
5973 } else if (flags & PURPLE_MESSAGE_NO_LOG) {
5974 g_snprintf(buf2, BUF_LONG,
5975 "<B><FONT %s COLOR=\"#777777\">%s</FONT></B>",
5976 sml_attrib ? sml_attrib : "", displaying);
5978 gtk_imhtml_append_text(GTK_IMHTML(gtkconv->imhtml), buf2, gtk_font_options_all);
5979 } else {
5980 char *new_message = g_memdup(displaying, length);
5981 char *alias_escaped = (alias ? g_markup_escape_text(alias, strlen(alias)) : g_strdup(""));
5982 /* The initial offset is to deal with
5983 * escaped entities making the string longer */
5984 int tag_start_offset = 0;
5985 const char *tagname = NULL;
5987 GtkTextIter start, end;
5988 GtkTextMark *mark;
5989 GtkTextTag *tag;
5990 GtkTextBuffer *buffer = GTK_IMHTML(gtkconv->imhtml)->text_buffer;
5992 /* Enforce direction on alias */
5993 if (is_rtl_message)
5994 str_embed_direction_chars(&alias_escaped);
5996 str = g_malloc(1024);
5997 if (flags & PURPLE_MESSAGE_WHISPER) {
5998 /* If we're whispering, it's not an autoresponse. */
5999 if (purple_message_meify(new_message, -1 )) {
6000 g_snprintf(str, 1024, "***%s", alias_escaped);
6001 tag_start_offset += 3;
6002 tagname = "whisper-action-name";
6004 else {
6005 g_snprintf(str, 1024, "*%s*:", alias_escaped);
6006 tag_start_offset += 1;
6007 #if 0
6008 tag_end_offset = 2;
6009 #endif
6010 tagname = "whisper-name";
6012 } else {
6013 if (purple_message_meify(new_message, -1)) {
6014 if (flags & PURPLE_MESSAGE_AUTO_RESP) {
6015 g_snprintf(str, 1024, "%s ***%s", AUTO_RESPONSE, alias_escaped);
6016 tag_start_offset += strlen(AUTO_RESPONSE) - 6 + 4;
6017 } else {
6018 g_snprintf(str, 1024, "***%s", alias_escaped);
6019 tag_start_offset += 3;
6022 if (flags & PURPLE_MESSAGE_NICK)
6023 tagname = "highlight-name";
6024 else
6025 tagname = "action-name";
6026 } else {
6027 if (flags & PURPLE_MESSAGE_AUTO_RESP) {
6028 g_snprintf(str, 1024, "%s %s", alias_escaped, AUTO_RESPONSE);
6029 tag_start_offset += strlen(AUTO_RESPONSE) - 6 + 1;
6030 } else {
6031 g_snprintf(str, 1024, "%s:", alias_escaped);
6032 #if 0
6033 tag_end_offset = 1;
6034 #endif
6037 if (flags & PURPLE_MESSAGE_NICK) {
6038 if (type == PURPLE_CONV_TYPE_IM) {
6039 tagname = "highlight-name";
6041 } else if (flags & PURPLE_MESSAGE_RECV) {
6042 /* The tagname for chats is handled by get_buddy_tag */
6043 if (type == PURPLE_CONV_TYPE_IM) {
6044 tagname = "receive-name";
6046 } else if (flags & PURPLE_MESSAGE_SEND) {
6047 tagname = "send-name";
6048 } else {
6049 purple_debug_error("gtkconv", "message missing flags\n");
6054 g_free(alias_escaped);
6056 if (tagname)
6057 tag = gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(buffer), tagname);
6058 else
6059 tag = get_buddy_tag(conv, name, flags, TRUE);
6061 if (GTK_IMHTML(gtkconv->imhtml)->show_comments) {
6062 /* The color for the timestamp has to be set in the font-tags, unfortunately.
6063 * Applying the nick-tag to timestamps would work, but that can make it
6064 * bold. I thought applying the "comment" tag again, which has "weight" set
6065 * to PANGO_WEIGHT_NORMAL, would remove the boldness. But it doesn't. So
6066 * this will have to do. I don't terribly like it. -- sadrul */
6067 const char *color = get_text_tag_color(tag);
6068 g_snprintf(buf2, BUF_LONG, "<FONT %s%s%s SIZE=\"2\"><!--%s --></FONT>",
6069 color ? "COLOR=\"" : "", color ? color : "", color ? "\"" : "", mdate);
6070 gtk_imhtml_append_text(GTK_IMHTML(gtkconv->imhtml), buf2, gtk_font_options_all | GTK_IMHTML_NO_SCROLL);
6073 gtk_text_buffer_get_end_iter(buffer, &end);
6074 mark = gtk_text_buffer_create_mark(buffer, NULL, &end, TRUE);
6076 g_snprintf(buf2, BUF_LONG, "<FONT %s>%s</FONT> ", sml_attrib ? sml_attrib : "", str);
6077 gtk_imhtml_append_text(GTK_IMHTML(gtkconv->imhtml), buf2, gtk_font_options_all | GTK_IMHTML_NO_SCROLL);
6079 gtk_text_buffer_get_end_iter(buffer, &end);
6080 gtk_text_buffer_get_iter_at_mark(buffer, &start, mark);
6081 gtk_text_buffer_apply_tag(buffer, tag, &start, &end);
6082 gtk_text_buffer_delete_mark(buffer, mark);
6084 g_free(str);
6086 if(gc){
6087 char *pre = g_strdup_printf("<font %s>", sml_attrib ? sml_attrib : "");
6088 char *post = "</font>";
6089 int pre_len = strlen(pre);
6090 int post_len = strlen(post);
6092 with_font_tag = g_malloc(length + pre_len + post_len + 1);
6094 strcpy(with_font_tag, pre);
6095 memcpy(with_font_tag + pre_len, new_message, length);
6096 strcpy(with_font_tag + pre_len + length, post);
6098 length += pre_len + post_len;
6099 g_free(pre);
6100 } else
6101 with_font_tag = g_memdup(new_message, length);
6103 gtk_imhtml_append_text(GTK_IMHTML(gtkconv->imhtml),
6104 with_font_tag, gtk_font_options | gtk_font_options_all);
6106 g_free(with_font_tag);
6107 g_free(new_message);
6110 g_free(mdate);
6111 g_free(sml_attrib);
6113 /* Tab highlighting stuff */
6114 if (!(flags & PURPLE_MESSAGE_SEND) && !pidgin_conv_has_focus(conv))
6116 PidginUnseenState unseen = PIDGIN_UNSEEN_NONE;
6118 if ((flags & PURPLE_MESSAGE_NICK) == PURPLE_MESSAGE_NICK)
6119 unseen = PIDGIN_UNSEEN_NICK;
6120 else if (((flags & PURPLE_MESSAGE_SYSTEM) == PURPLE_MESSAGE_SYSTEM) ||
6121 ((flags & PURPLE_MESSAGE_ERROR) == PURPLE_MESSAGE_ERROR))
6122 unseen = PIDGIN_UNSEEN_EVENT;
6123 else if ((flags & PURPLE_MESSAGE_NO_LOG) == PURPLE_MESSAGE_NO_LOG)
6124 unseen = PIDGIN_UNSEEN_NO_LOG;
6125 else
6126 unseen = PIDGIN_UNSEEN_TEXT;
6128 gtkconv_set_unseen(gtkconv, unseen);
6131 if (!(flags & PURPLE_MESSAGE_RECV) && (conv->features & PURPLE_CONNECTION_ALLOW_CUSTOM_SMILEY))
6133 /* Restore the smiley-data */
6134 pidgin_themes_smiley_themeize(gtkconv->imhtml);
6137 purple_signal_emit(pidgin_conversations_get_handle(),
6138 (type == PURPLE_CONV_TYPE_IM ? "displayed-im-msg" : "displayed-chat-msg"),
6139 account, name, displaying, conv, flags);
6140 g_free(displaying);
6141 update_typing_message(gtkconv, NULL);
6144 static gboolean get_iter_from_chatbuddy(PurpleConvChatBuddy *cb, GtkTreeIter *iter)
6146 GtkTreeRowReference *ref;
6147 GtkTreePath *path;
6148 GtkTreeModel *model;
6150 g_return_val_if_fail(cb != NULL, FALSE);
6152 ref = cb->ui_data;
6153 if (!ref)
6154 return FALSE;
6156 if ((path = gtk_tree_row_reference_get_path(ref)) == NULL)
6157 return FALSE;
6159 model = gtk_tree_row_reference_get_model(ref);
6160 if (!gtk_tree_model_get_iter(GTK_TREE_MODEL(model), iter, path)) {
6161 gtk_tree_path_free(path);
6162 return FALSE;
6165 gtk_tree_path_free(path);
6166 return TRUE;
6169 static void
6170 pidgin_conv_chat_add_users(PurpleConversation *conv, GList *cbuddies, gboolean new_arrivals)
6172 PurpleConvChat *chat;
6173 PidginConversation *gtkconv;
6174 PidginChatPane *gtkchat;
6175 GtkListStore *ls;
6176 GList *l;
6178 char tmp[BUF_LONG];
6179 int num_users;
6181 chat = PURPLE_CONV_CHAT(conv);
6182 gtkconv = PIDGIN_CONVERSATION(conv);
6183 gtkchat = gtkconv->u.chat;
6185 num_users = g_list_length(purple_conv_chat_get_users(chat));
6187 g_snprintf(tmp, sizeof(tmp),
6188 ngettext("%d person in room", "%d people in room",
6189 num_users),
6190 num_users);
6192 gtk_label_set_text(GTK_LABEL(gtkchat->count), tmp);
6194 ls = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list)));
6196 gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(ls), GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID,
6197 GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID);
6199 l = cbuddies;
6200 while (l != NULL) {
6201 add_chat_buddy_common(conv, (PurpleConvChatBuddy *)l->data, NULL);
6202 l = l->next;
6205 /* Currently GTK+ maintains our sorted list after it's in the tree.
6206 * This may change if it turns out we can manage it faster ourselves.
6208 gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(ls), CHAT_USERS_ALIAS_KEY_COLUMN,
6209 GTK_SORT_ASCENDING);
6212 static void
6213 pidgin_conv_chat_rename_user(PurpleConversation *conv, const char *old_name,
6214 const char *new_name, const char *new_alias)
6216 PurpleConvChat *chat;
6217 PidginConversation *gtkconv;
6218 PidginChatPane *gtkchat;
6219 PurpleConvChatBuddy *old_cbuddy, *new_cbuddy;
6220 GtkTreeIter iter;
6221 GtkTreeModel *model;
6222 GtkTextTag *tag;
6224 chat = PURPLE_CONV_CHAT(conv);
6225 gtkconv = PIDGIN_CONVERSATION(conv);
6226 gtkchat = gtkconv->u.chat;
6228 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
6230 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter))
6231 return;
6233 if ((tag = get_buddy_tag(conv, old_name, 0, FALSE)))
6234 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_ITALIC, NULL);
6235 if ((tag = get_buddy_tag(conv, old_name, PURPLE_MESSAGE_NICK, FALSE)))
6236 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_ITALIC, NULL);
6238 old_cbuddy = purple_conv_chat_cb_find(chat, old_name);
6239 if (!old_cbuddy)
6240 return;
6242 if (get_iter_from_chatbuddy(old_cbuddy, &iter)) {
6243 GtkTreeRowReference *ref = old_cbuddy->ui_data;
6245 gtk_list_store_remove(GTK_LIST_STORE(model), &iter);
6246 gtk_tree_row_reference_free(ref);
6247 old_cbuddy->ui_data = NULL;
6250 g_return_if_fail(new_alias != NULL);
6252 new_cbuddy = purple_conv_chat_cb_find(chat, new_name);
6254 add_chat_buddy_common(conv, new_cbuddy, old_name);
6257 static void
6258 pidgin_conv_chat_remove_users(PurpleConversation *conv, GList *users)
6260 PurpleConvChat *chat;
6261 PidginConversation *gtkconv;
6262 PidginChatPane *gtkchat;
6263 GtkTreeIter iter;
6264 GtkTreeModel *model;
6265 GList *l;
6266 char tmp[BUF_LONG];
6267 int num_users;
6268 gboolean f;
6269 GtkTextTag *tag;
6271 chat = PURPLE_CONV_CHAT(conv);
6272 gtkconv = PIDGIN_CONVERSATION(conv);
6273 gtkchat = gtkconv->u.chat;
6275 num_users = g_list_length(purple_conv_chat_get_users(chat));
6277 for (l = users; l != NULL; l = l->next) {
6278 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
6280 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter))
6281 /* XXX: Break? */
6282 continue;
6284 do {
6285 char *val;
6287 gtk_tree_model_get(GTK_TREE_MODEL(model), &iter,
6288 CHAT_USERS_NAME_COLUMN, &val, -1);
6290 if (!purple_utf8_strcasecmp((char *)l->data, val)) {
6291 f = gtk_list_store_remove(GTK_LIST_STORE(model), &iter);
6293 else
6294 f = gtk_tree_model_iter_next(GTK_TREE_MODEL(model), &iter);
6296 g_free(val);
6297 } while (f);
6299 if ((tag = get_buddy_tag(conv, l->data, 0, FALSE)))
6300 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_ITALIC, NULL);
6301 if ((tag = get_buddy_tag(conv, l->data, PURPLE_MESSAGE_NICK, FALSE)))
6302 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_ITALIC, NULL);
6305 g_snprintf(tmp, sizeof(tmp),
6306 ngettext("%d person in room", "%d people in room",
6307 num_users), num_users);
6309 gtk_label_set_text(GTK_LABEL(gtkchat->count), tmp);
6312 static void
6313 pidgin_conv_chat_update_user(PurpleConversation *conv, const char *user)
6315 PurpleConvChat *chat;
6316 PurpleConvChatBuddy *cbuddy;
6317 PidginConversation *gtkconv;
6318 PidginChatPane *gtkchat;
6319 GtkTreeIter iter;
6320 GtkTreeModel *model;
6322 chat = PURPLE_CONV_CHAT(conv);
6323 gtkconv = PIDGIN_CONVERSATION(conv);
6324 gtkchat = gtkconv->u.chat;
6326 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
6328 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter))
6329 return;
6331 cbuddy = purple_conv_chat_cb_find(chat, user);
6332 if (!cbuddy)
6333 return;
6335 if (get_iter_from_chatbuddy(cbuddy, &iter)) {
6336 GtkTreeRowReference *ref = cbuddy->ui_data;
6337 gtk_list_store_remove(GTK_LIST_STORE(model), &iter);
6338 gtk_tree_row_reference_free(ref);
6339 cbuddy->ui_data = NULL;
6342 if (cbuddy)
6343 add_chat_buddy_common(conv, cbuddy, NULL);
6346 gboolean
6347 pidgin_conv_has_focus(PurpleConversation *conv)
6349 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
6350 PidginWindow *win;
6351 gboolean has_focus;
6353 win = gtkconv->win;
6355 g_object_get(G_OBJECT(win->window), "has-toplevel-focus", &has_focus, NULL);
6357 if (has_focus && pidgin_conv_window_is_active_conversation(conv))
6358 return TRUE;
6360 return FALSE;
6363 static gboolean
6364 add_custom_smiley_for_imhtml(GtkIMHtml *imhtml, const char *sml, const char *smile)
6366 GtkIMHtmlSmiley *smiley;
6368 smiley = gtk_imhtml_smiley_get(imhtml, sml, smile);
6370 if (smiley) {
6371 if (!(smiley->flags & GTK_IMHTML_SMILEY_CUSTOM)) {
6372 return FALSE;
6374 gtk_imhtml_smiley_reload(smiley);
6375 return TRUE;
6378 smiley = gtk_imhtml_smiley_create(NULL, smile, FALSE, GTK_IMHTML_SMILEY_CUSTOM);
6379 gtk_imhtml_associate_smiley(imhtml, sml, smiley);
6380 g_signal_connect_swapped(imhtml, "destroy", G_CALLBACK(gtk_imhtml_smiley_destroy), smiley);
6382 return TRUE;
6385 static gboolean
6386 pidgin_conv_custom_smiley_add(PurpleConversation *conv, const char *smile, gboolean remote)
6388 PidginConversation *gtkconv;
6389 struct smiley_list *list;
6390 const char *sml = NULL, *conv_sml;
6392 if (!conv || !smile || !*smile) {
6393 return FALSE;
6396 /* If smileys are off, return false */
6397 if (pidgin_themes_smileys_disabled())
6398 return FALSE;
6400 /* If possible add this smiley to the current theme.
6401 * The addition is only temporary: custom smilies aren't saved to disk. */
6402 conv_sml = purple_account_get_protocol_name(conv->account);
6403 gtkconv = PIDGIN_CONVERSATION(conv);
6405 for (list = (struct smiley_list *)current_smiley_theme->list; list; list = list->next) {
6406 if (purple_strequal(list->sml, conv_sml)) {
6407 sml = list->sml;
6408 break;
6412 if (!add_custom_smiley_for_imhtml(GTK_IMHTML(gtkconv->imhtml), sml, smile))
6413 return FALSE;
6415 if (!remote) /* If it's a local custom smiley, then add it for the entry */
6416 if (!add_custom_smiley_for_imhtml(GTK_IMHTML(gtkconv->entry), sml, smile))
6417 return FALSE;
6419 return TRUE;
6422 static void
6423 pidgin_conv_custom_smiley_write(PurpleConversation *conv, const char *smile,
6424 const guchar *data, gsize size)
6426 PidginConversation *gtkconv;
6427 GtkIMHtmlSmiley *smiley;
6428 const char *sml;
6429 GError *error = NULL;
6431 sml = purple_account_get_protocol_name(conv->account);
6432 gtkconv = PIDGIN_CONVERSATION(conv);
6433 smiley = gtk_imhtml_smiley_get(GTK_IMHTML(gtkconv->imhtml), sml, smile);
6435 if (!smiley)
6436 return;
6438 smiley->data = g_realloc(smiley->data, smiley->datasize + size);
6439 g_memmove((guchar *)smiley->data + smiley->datasize, data, size);
6440 smiley->datasize += size;
6442 if (!smiley->loader)
6443 return;
6445 if (!gdk_pixbuf_loader_write(smiley->loader, data, size, &error) || error) {
6446 purple_debug_warning("gtkconv", "gdk_pixbuf_loader_write() "
6447 "failed with size=%zu: %s\n", size,
6448 error ? error->message : "(no error message)");
6449 if (error)
6450 g_error_free(error);
6451 /* We must stop using the GdkPixbufLoader because trying to load
6452 certain invalid GIFs with at least gdk-pixbuf 2.23.3 can return
6453 a GdkPixbuf that will cause some operations (like
6454 gdk_pixbuf_scale_simple()) to consume memory in an infinite loop.
6455 But we also don't want to set smiley->loader to NULL because our
6456 code might expect it to be set. So create a new loader. */
6457 g_object_unref(G_OBJECT(smiley->loader));
6458 smiley->loader = gdk_pixbuf_loader_new();
6462 static void
6463 pidgin_conv_custom_smiley_close(PurpleConversation *conv, const char *smile)
6465 PidginConversation *gtkconv;
6466 GtkIMHtmlSmiley *smiley;
6467 const char *sml;
6468 GError *error = NULL;
6470 g_return_if_fail(conv != NULL);
6471 g_return_if_fail(smile != NULL);
6473 sml = purple_account_get_protocol_name(conv->account);
6474 gtkconv = PIDGIN_CONVERSATION(conv);
6475 smiley = gtk_imhtml_smiley_get(GTK_IMHTML(gtkconv->imhtml), sml, smile);
6477 if (!smiley)
6478 return;
6480 if (!smiley->loader)
6481 return;
6483 purple_debug_info("gtkconv", "About to close the smiley pixbuf\n");
6485 if (!gdk_pixbuf_loader_close(smiley->loader, &error) || error) {
6486 purple_debug_warning("gtkconv", "gdk_pixbuf_loader_close() "
6487 "failed: %s\n",
6488 error ? error->message : "(no error message)");
6489 if (error)
6490 g_error_free(error);
6491 /* We must stop using the GdkPixbufLoader because if we tried to
6492 load certain invalid GIFs with all current versions of GDK (as
6493 of 2011-06-15) then it's possible the loader will contain data
6494 that could cause some operations (like gdk_pixbuf_scale_simple())
6495 to consume memory in an infinite loop. But we also don't want
6496 to set smiley->loader to NULL because our code might expect it
6497 to be set. So create a new loader. */
6498 g_object_unref(G_OBJECT(smiley->loader));
6499 smiley->loader = gdk_pixbuf_loader_new();
6503 static void
6504 pidgin_conv_send_confirm(PurpleConversation *conv, const char *message)
6506 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
6508 gtk_imhtml_append_text(GTK_IMHTML(gtkconv->entry), message, 0);
6512 * Makes sure all the menu items and all the buttons are hidden/shown and
6513 * sensitive/insensitive. This is called after changing tabs and when an
6514 * account signs on or off.
6516 static void
6517 gray_stuff_out(PidginConversation *gtkconv)
6519 PidginWindow *win;
6520 PurpleConversation *conv = gtkconv->active_conv;
6521 PurpleConnection *gc;
6522 PurplePluginProtocolInfo *prpl_info = NULL;
6523 GdkPixbuf *window_icon = NULL;
6524 GtkIMHtmlButtons buttons;
6525 PurpleAccount *account;
6527 win = pidgin_conv_get_window(gtkconv);
6528 gc = purple_conversation_get_gc(conv);
6529 account = purple_conversation_get_account(conv);
6531 if (gc != NULL)
6532 prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(gc->prpl);
6534 if (win->menu.send_to != NULL)
6535 update_send_to_selection(win);
6538 * Handle hiding and showing stuff based on what type of conv this is.
6539 * Stuff that Purple IMs support in general should be shown for IM
6540 * conversations. Stuff that Purple chats support in general should be
6541 * shown for chat conversations. It doesn't matter whether the PRPL
6542 * supports it or not--that only affects if the button or menu item
6543 * is sensitive or not.
6545 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) {
6546 /* Show stuff that applies to IMs, hide stuff that applies to chats */
6548 /* Deal with menu items */
6549 gtk_widget_show(win->menu.view_log);
6550 gtk_widget_show(win->menu.send_file);
6551 gtk_widget_show(g_object_get_data(G_OBJECT(win->window), "get_attention"));
6552 gtk_widget_show(win->menu.add_pounce);
6553 gtk_widget_show(win->menu.get_info);
6554 gtk_widget_hide(win->menu.invite);
6555 gtk_widget_show(win->menu.alias);
6556 if (purple_privacy_check(account, purple_conversation_get_name(conv))) {
6557 gtk_widget_hide(win->menu.unblock);
6558 gtk_widget_show(win->menu.block);
6559 } else {
6560 gtk_widget_hide(win->menu.block);
6561 gtk_widget_show(win->menu.unblock);
6564 if (purple_find_buddy(account, purple_conversation_get_name(conv)) == NULL) {
6565 gtk_widget_show(win->menu.add);
6566 gtk_widget_hide(win->menu.remove);
6567 } else {
6568 gtk_widget_show(win->menu.remove);
6569 gtk_widget_hide(win->menu.add);
6572 gtk_widget_show(win->menu.insert_link);
6573 gtk_widget_show(win->menu.insert_image);
6574 } else if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) {
6575 /* Show stuff that applies to Chats, hide stuff that applies to IMs */
6577 /* Deal with menu items */
6578 gtk_widget_show(win->menu.view_log);
6579 gtk_widget_hide(win->menu.send_file);
6580 gtk_widget_hide(g_object_get_data(G_OBJECT(win->window), "get_attention"));
6581 gtk_widget_hide(win->menu.add_pounce);
6582 gtk_widget_hide(win->menu.get_info);
6583 gtk_widget_show(win->menu.invite);
6584 gtk_widget_show(win->menu.alias);
6585 gtk_widget_hide(win->menu.block);
6586 gtk_widget_hide(win->menu.unblock);
6588 if ((account == NULL) || purple_blist_find_chat(account, purple_conversation_get_name(conv)) == NULL) {
6589 /* If the chat is NOT in the buddy list */
6590 gtk_widget_show(win->menu.add);
6591 gtk_widget_hide(win->menu.remove);
6592 } else {
6593 /* If the chat IS in the buddy list */
6594 gtk_widget_hide(win->menu.add);
6595 gtk_widget_show(win->menu.remove);
6598 gtk_widget_show(win->menu.insert_link);
6599 gtk_widget_show(win->menu.insert_image);
6603 * Handle graying stuff out based on whether an account is connected
6604 * and what features that account supports.
6606 if ((gc != NULL) &&
6607 ((purple_conversation_get_type(conv) != PURPLE_CONV_TYPE_CHAT) ||
6608 !purple_conv_chat_has_left(PURPLE_CONV_CHAT(conv)) ))
6610 /* Account is online */
6611 /* Deal with the toolbar */
6612 if (conv->features & PURPLE_CONNECTION_HTML)
6614 buttons = GTK_IMHTML_ALL; /* Everything on */
6615 if (conv->features & PURPLE_CONNECTION_NO_BGCOLOR)
6616 buttons &= ~GTK_IMHTML_BACKCOLOR;
6617 if (conv->features & PURPLE_CONNECTION_NO_FONTSIZE)
6619 buttons &= ~GTK_IMHTML_GROW;
6620 buttons &= ~GTK_IMHTML_SHRINK;
6622 if (conv->features & PURPLE_CONNECTION_NO_URLDESC)
6623 buttons &= ~GTK_IMHTML_LINKDESC;
6624 } else {
6625 buttons = GTK_IMHTML_SMILEY | GTK_IMHTML_IMAGE;
6628 if (!(prpl_info->options & OPT_PROTO_IM_IMAGE))
6629 conv->features |= PURPLE_CONNECTION_NO_IMAGES;
6631 if(conv->features & PURPLE_CONNECTION_NO_IMAGES)
6632 buttons &= ~GTK_IMHTML_IMAGE;
6634 if (conv->features & PURPLE_CONNECTION_ALLOW_CUSTOM_SMILEY)
6635 buttons |= GTK_IMHTML_CUSTOM_SMILEY;
6636 else
6637 buttons &= ~GTK_IMHTML_CUSTOM_SMILEY;
6639 gtk_imhtml_set_format_functions(GTK_IMHTML(gtkconv->entry), buttons);
6640 if (account != NULL)
6641 gtk_imhtmltoolbar_associate_smileys(GTK_IMHTMLTOOLBAR(gtkconv->toolbar), purple_account_get_protocol_id(account));
6643 /* Deal with menu items */
6644 gtk_widget_set_sensitive(win->menu.view_log, TRUE);
6645 gtk_widget_set_sensitive(win->menu.add_pounce, TRUE);
6646 gtk_widget_set_sensitive(win->menu.get_info, (prpl_info->get_info != NULL));
6647 gtk_widget_set_sensitive(win->menu.invite, (prpl_info->chat_invite != NULL));
6648 gtk_widget_set_sensitive(win->menu.insert_link, (conv->features & PURPLE_CONNECTION_HTML));
6649 gtk_widget_set_sensitive(win->menu.insert_image, !(conv->features & PURPLE_CONNECTION_NO_IMAGES));
6651 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM)
6653 gtk_widget_set_sensitive(win->menu.add, (prpl_info->add_buddy != NULL) || (prpl_info->add_buddy_with_invite != NULL));
6654 gtk_widget_set_sensitive(win->menu.remove, (prpl_info->remove_buddy != NULL));
6655 gtk_widget_set_sensitive(win->menu.send_file,
6656 (prpl_info->send_file != NULL && (!prpl_info->can_receive_file ||
6657 prpl_info->can_receive_file(gc, purple_conversation_get_name(conv)))));
6658 gtk_widget_set_sensitive(g_object_get_data(G_OBJECT(win->window), "get_attention"), (prpl_info->send_attention != NULL));
6659 gtk_widget_set_sensitive(win->menu.alias,
6660 (account != NULL) &&
6661 (purple_find_buddy(account, purple_conversation_get_name(conv)) != NULL));
6663 else if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT)
6665 gtk_widget_set_sensitive(win->menu.add, (prpl_info->join_chat != NULL));
6666 gtk_widget_set_sensitive(win->menu.remove, (prpl_info->join_chat != NULL));
6667 gtk_widget_set_sensitive(win->menu.alias,
6668 (account != NULL) &&
6669 (purple_blist_find_chat(account, purple_conversation_get_name(conv)) != NULL));
6672 } else {
6673 /* Account is offline */
6674 /* Or it's a chat that we've left. */
6676 /* Then deal with menu items */
6677 gtk_widget_set_sensitive(win->menu.view_log, TRUE);
6678 gtk_widget_set_sensitive(win->menu.send_file, FALSE);
6679 gtk_widget_set_sensitive(g_object_get_data(G_OBJECT(win->window),
6680 "get_attention"), FALSE);
6681 gtk_widget_set_sensitive(win->menu.add_pounce, TRUE);
6682 gtk_widget_set_sensitive(win->menu.get_info, FALSE);
6683 gtk_widget_set_sensitive(win->menu.invite, FALSE);
6684 gtk_widget_set_sensitive(win->menu.alias, FALSE);
6685 gtk_widget_set_sensitive(win->menu.add, FALSE);
6686 gtk_widget_set_sensitive(win->menu.remove, FALSE);
6687 gtk_widget_set_sensitive(win->menu.insert_link, TRUE);
6688 gtk_widget_set_sensitive(win->menu.insert_image, FALSE);
6692 * Update the window's icon
6694 if (pidgin_conv_window_is_active_conversation(conv))
6696 GList *l = NULL;
6697 if ((purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) &&
6698 (gtkconv->u.im->anim))
6700 PurpleBuddy *buddy = purple_find_buddy(conv->account, conv->name);
6701 window_icon =
6702 gdk_pixbuf_animation_get_static_image(gtkconv->u.im->anim);
6704 if (buddy && !PURPLE_BUDDY_IS_ONLINE(buddy))
6705 gdk_pixbuf_saturate_and_pixelate(window_icon, window_icon, 0.0, FALSE);
6707 g_object_ref(window_icon);
6708 l = g_list_append(l, window_icon);
6709 } else {
6710 l = pidgin_conv_get_tab_icons(conv);
6712 gtk_window_set_icon_list(GTK_WINDOW(win->window), l);
6713 if (window_icon != NULL) {
6714 g_object_unref(G_OBJECT(window_icon));
6715 g_list_free(l);
6720 static void
6721 pidgin_conv_update_fields(PurpleConversation *conv, PidginConvFields fields)
6723 PidginConversation *gtkconv;
6724 PidginWindow *win;
6726 gtkconv = PIDGIN_CONVERSATION(conv);
6727 if (!gtkconv)
6728 return;
6729 win = pidgin_conv_get_window(gtkconv);
6730 if (!win)
6731 return;
6733 if (fields & PIDGIN_CONV_SET_TITLE)
6735 purple_conversation_autoset_title(conv);
6738 if (fields & PIDGIN_CONV_BUDDY_ICON)
6740 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM)
6741 pidgin_conv_update_buddy_icon(conv);
6744 if (fields & PIDGIN_CONV_MENU)
6746 gray_stuff_out(PIDGIN_CONVERSATION(conv));
6747 generate_send_to_items(win);
6750 if (fields & PIDGIN_CONV_TAB_ICON)
6752 update_tab_icon(conv);
6753 generate_send_to_items(win); /* To update the icons in SendTo menu */
6756 if ((fields & PIDGIN_CONV_TOPIC) &&
6757 purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT)
6759 const char *topic;
6760 PurpleConvChat *chat = PURPLE_CONV_CHAT(conv);
6761 PidginChatPane *gtkchat = gtkconv->u.chat;
6763 if (gtkchat->topic_text != NULL)
6765 topic = purple_conv_chat_get_topic(chat);
6767 gtk_entry_set_text(GTK_ENTRY(gtkchat->topic_text), topic ? topic : "");
6768 gtk_tooltips_set_tip(gtkconv->tooltips, gtkchat->topic_text,
6769 topic ? topic : "", NULL);
6773 if (fields & PIDGIN_CONV_SMILEY_THEME)
6774 pidgin_themes_smiley_themeize(PIDGIN_CONVERSATION(conv)->imhtml);
6776 if ((fields & PIDGIN_CONV_COLORIZE_TITLE) ||
6777 (fields & PIDGIN_CONV_SET_TITLE) ||
6778 (fields & PIDGIN_CONV_TOPIC))
6780 char *title;
6781 PurpleConvIm *im = NULL;
6782 PurpleAccount *account = purple_conversation_get_account(conv);
6783 PurpleBuddy *buddy = NULL;
6784 char *markup = NULL;
6785 AtkObject *accessibility_obj;
6786 /* I think this is a little longer than it needs to be but I'm lazy. */
6787 char *style;
6789 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM)
6790 im = PURPLE_CONV_IM(conv);
6792 if ((account == NULL) ||
6793 !purple_account_is_connected(account) ||
6794 ((purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT)
6795 && purple_conv_chat_has_left(PURPLE_CONV_CHAT(conv))))
6796 title = g_strdup_printf("(%s)", purple_conversation_get_title(conv));
6797 else
6798 title = g_strdup(purple_conversation_get_title(conv));
6800 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) {
6801 buddy = purple_find_buddy(account, conv->name);
6802 if (buddy) {
6803 markup = pidgin_blist_get_name_markup(buddy, FALSE, FALSE);
6804 } else {
6805 markup = title;
6807 } else if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) {
6808 const char *topic = gtkconv->u.chat->topic_text
6809 ? gtk_entry_get_text(GTK_ENTRY(gtkconv->u.chat->topic_text))
6810 : NULL;
6811 char *esc = NULL, *tmp;
6812 esc = topic ? g_markup_escape_text(topic, -1) : NULL;
6813 tmp = g_markup_escape_text(purple_conversation_get_title(conv), -1);
6814 markup = g_strdup_printf("%s%s<span color='%s' size='smaller'>%s</span>",
6815 tmp, esc && *esc ? "\n" : "",
6816 pidgin_get_dim_grey_string(gtkconv->infopane),
6817 esc ? esc : "");
6818 g_free(tmp);
6819 g_free(esc);
6821 gtk_list_store_set(gtkconv->infopane_model, &(gtkconv->infopane_iter),
6822 CONV_TEXT_COLUMN, markup, -1);
6823 /* XXX seanegan Why do I have to do this? */
6824 gtk_widget_queue_draw(gtkconv->infopane);
6826 if (title != markup)
6827 g_free(markup);
6829 if (!GTK_WIDGET_REALIZED(gtkconv->tab_label))
6830 gtk_widget_realize(gtkconv->tab_label);
6832 accessibility_obj = gtk_widget_get_accessible(gtkconv->tab_cont);
6833 if (im != NULL &&
6834 purple_conv_im_get_typing_state(im) == PURPLE_TYPING) {
6835 atk_object_set_description(accessibility_obj, _("Typing"));
6836 style = "tab-label-typing";
6837 } else if (im != NULL &&
6838 purple_conv_im_get_typing_state(im) == PURPLE_TYPED) {
6839 atk_object_set_description(accessibility_obj, _("Stopped Typing"));
6840 style = "tab-label-typed";
6841 } else if (gtkconv->unseen_state == PIDGIN_UNSEEN_NICK) {
6842 atk_object_set_description(accessibility_obj, _("Nick Said"));
6843 style = "tab-label-attention";
6844 } else if (gtkconv->unseen_state == PIDGIN_UNSEEN_TEXT) {
6845 atk_object_set_description(accessibility_obj, _("Unread Messages"));
6846 if (gtkconv->active_conv->type == PURPLE_CONV_TYPE_CHAT)
6847 style = "tab-label-unreadchat";
6848 else
6849 style = "tab-label-attention";
6850 } else if (gtkconv->unseen_state == PIDGIN_UNSEEN_EVENT) {
6851 atk_object_set_description(accessibility_obj, _("New Event"));
6852 style = "tab-label-event";
6853 } else {
6854 style = "tab-label";
6857 gtk_widget_set_name(gtkconv->tab_label, style);
6858 gtk_label_set_text(GTK_LABEL(gtkconv->tab_label), title);
6859 gtk_widget_set_state(gtkconv->tab_label, GTK_STATE_ACTIVE);
6861 if (gtkconv->unseen_state == PIDGIN_UNSEEN_TEXT ||
6862 gtkconv->unseen_state == PIDGIN_UNSEEN_NICK ||
6863 gtkconv->unseen_state == PIDGIN_UNSEEN_EVENT) {
6864 PangoAttrList *list = pango_attr_list_new();
6865 PangoAttribute *attr = pango_attr_weight_new(PANGO_WEIGHT_BOLD);
6866 attr->start_index = 0;
6867 attr->end_index = -1;
6868 pango_attr_list_insert(list, attr);
6869 gtk_label_set_attributes(GTK_LABEL(gtkconv->tab_label), list);
6870 pango_attr_list_unref(list);
6871 } else
6872 gtk_label_set_attributes(GTK_LABEL(gtkconv->tab_label), NULL);
6874 if (pidgin_conv_window_is_active_conversation(conv))
6875 update_typing_icon(gtkconv);
6877 gtk_label_set_text(GTK_LABEL(gtkconv->menu_label), title);
6878 if (pidgin_conv_window_is_active_conversation(conv)) {
6879 const char* current_title = gtk_window_get_title(GTK_WINDOW(win->window));
6880 if (current_title == NULL || !purple_strequal(current_title, title))
6881 gtk_window_set_title(GTK_WINDOW(win->window), title);
6884 g_free(title);
6888 static void
6889 pidgin_conv_updated(PurpleConversation *conv, PurpleConvUpdateType type)
6891 PidginConvFields flags = 0;
6893 g_return_if_fail(conv != NULL);
6895 if (type == PURPLE_CONV_UPDATE_ACCOUNT)
6897 flags = PIDGIN_CONV_ALL;
6899 else if (type == PURPLE_CONV_UPDATE_TYPING ||
6900 type == PURPLE_CONV_UPDATE_UNSEEN ||
6901 type == PURPLE_CONV_UPDATE_TITLE)
6903 flags = PIDGIN_CONV_COLORIZE_TITLE;
6905 else if (type == PURPLE_CONV_UPDATE_TOPIC)
6907 flags = PIDGIN_CONV_TOPIC;
6909 else if (type == PURPLE_CONV_ACCOUNT_ONLINE ||
6910 type == PURPLE_CONV_ACCOUNT_OFFLINE)
6912 flags = PIDGIN_CONV_MENU | PIDGIN_CONV_TAB_ICON | PIDGIN_CONV_SET_TITLE;
6914 else if (type == PURPLE_CONV_UPDATE_AWAY)
6916 flags = PIDGIN_CONV_TAB_ICON;
6918 else if (type == PURPLE_CONV_UPDATE_ADD ||
6919 type == PURPLE_CONV_UPDATE_REMOVE ||
6920 type == PURPLE_CONV_UPDATE_CHATLEFT)
6922 flags = PIDGIN_CONV_SET_TITLE | PIDGIN_CONV_MENU;
6924 else if (type == PURPLE_CONV_UPDATE_ICON)
6926 flags = PIDGIN_CONV_BUDDY_ICON;
6928 else if (type == PURPLE_CONV_UPDATE_FEATURES)
6930 flags = PIDGIN_CONV_MENU;
6933 pidgin_conv_update_fields(conv, flags);
6936 static void
6937 wrote_msg_update_unseen_cb(PurpleAccount *account, const char *who, const char *message,
6938 PurpleConversation *conv, PurpleMessageFlags flags, gpointer null)
6940 PidginConversation *gtkconv = conv ? PIDGIN_CONVERSATION(conv) : NULL;
6941 if (conv == NULL || (gtkconv && gtkconv->win != hidden_convwin))
6942 return;
6943 if (flags & (PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_RECV)) {
6944 PidginUnseenState unseen = PIDGIN_UNSEEN_NONE;
6946 if ((flags & PURPLE_MESSAGE_NICK) == PURPLE_MESSAGE_NICK)
6947 unseen = PIDGIN_UNSEEN_NICK;
6948 else if (((flags & PURPLE_MESSAGE_SYSTEM) == PURPLE_MESSAGE_SYSTEM) ||
6949 ((flags & PURPLE_MESSAGE_ERROR) == PURPLE_MESSAGE_ERROR))
6950 unseen = PIDGIN_UNSEEN_EVENT;
6951 else if ((flags & PURPLE_MESSAGE_NO_LOG) == PURPLE_MESSAGE_NO_LOG)
6952 unseen = PIDGIN_UNSEEN_NO_LOG;
6953 else
6954 unseen = PIDGIN_UNSEEN_TEXT;
6956 conv_set_unseen(conv, unseen);
6960 static PurpleConversationUiOps conversation_ui_ops =
6962 pidgin_conv_new,
6963 pidgin_conv_destroy, /* destroy_conversation */
6964 NULL, /* write_chat */
6965 pidgin_conv_write_im, /* write_im */
6966 pidgin_conv_write_conv, /* write_conv */
6967 pidgin_conv_chat_add_users, /* chat_add_users */
6968 pidgin_conv_chat_rename_user, /* chat_rename_user */
6969 pidgin_conv_chat_remove_users, /* chat_remove_users */
6970 pidgin_conv_chat_update_user, /* chat_update_user */
6971 pidgin_conv_present_conversation, /* present */
6972 pidgin_conv_has_focus, /* has_focus */
6973 pidgin_conv_custom_smiley_add, /* custom_smiley_add */
6974 pidgin_conv_custom_smiley_write, /* custom_smiley_write */
6975 pidgin_conv_custom_smiley_close, /* custom_smiley_close */
6976 pidgin_conv_send_confirm, /* send_confirm */
6977 NULL,
6978 NULL,
6979 NULL,
6980 NULL
6983 PurpleConversationUiOps *
6984 pidgin_conversations_get_conv_ui_ops(void)
6986 return &conversation_ui_ops;
6989 /**************************************************************************
6990 * Public conversation utility functions
6991 **************************************************************************/
6992 void
6993 pidgin_conv_update_buddy_icon(PurpleConversation *conv)
6995 PidginConversation *gtkconv;
6996 PidginWindow *win;
6998 PurpleBuddy *buddy;
7000 PurpleStoredImage *custom_img = NULL;
7001 gconstpointer data = NULL;
7002 size_t len;
7004 GdkPixbuf *buf;
7006 GList *children;
7007 GtkWidget *event;
7008 GdkPixbuf *scale;
7009 int scale_width, scale_height;
7010 int size = 0;
7012 PurpleAccount *account;
7014 PurpleBuddyIcon *icon;
7016 g_return_if_fail(conv != NULL);
7017 g_return_if_fail(PIDGIN_IS_PIDGIN_CONVERSATION(conv));
7018 g_return_if_fail(purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM);
7020 gtkconv = PIDGIN_CONVERSATION(conv);
7021 win = gtkconv->win;
7022 if (conv != gtkconv->active_conv)
7023 return;
7025 if (!gtkconv->u.im->show_icon)
7026 return;
7028 account = purple_conversation_get_account(conv);
7030 /* Remove the current icon stuff */
7031 children = gtk_container_get_children(GTK_CONTAINER(gtkconv->u.im->icon_container));
7032 if (children) {
7033 /* We know there's only one child here. It'd be nice to shortcut to the
7034 event box, but we can't change the PidginConversation until 3.0 */
7035 event = (GtkWidget *)children->data;
7036 gtk_container_remove(GTK_CONTAINER(gtkconv->u.im->icon_container), event);
7037 g_list_free(children);
7040 if (gtkconv->u.im->anim != NULL)
7041 g_object_unref(G_OBJECT(gtkconv->u.im->anim));
7043 gtkconv->u.im->anim = NULL;
7045 if (gtkconv->u.im->icon_timer != 0)
7046 g_source_remove(gtkconv->u.im->icon_timer);
7048 gtkconv->u.im->icon_timer = 0;
7050 if (gtkconv->u.im->iter != NULL)
7051 g_object_unref(G_OBJECT(gtkconv->u.im->iter));
7053 gtkconv->u.im->iter = NULL;
7055 if (!purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons"))
7056 return;
7058 if (purple_conversation_get_gc(conv) == NULL)
7059 return;
7061 buddy = purple_find_buddy(account, purple_conversation_get_name(conv));
7062 if (buddy)
7064 PurpleContact *contact = purple_buddy_get_contact(buddy);
7065 if (contact) {
7066 custom_img = purple_buddy_icons_node_find_custom_icon((PurpleBlistNode*)contact);
7067 if (custom_img) {
7068 /* There is a custom icon for this user */
7069 data = purple_imgstore_get_data(custom_img);
7070 len = purple_imgstore_get_size(custom_img);
7075 if (data == NULL) {
7076 icon = purple_conv_im_get_icon(PURPLE_CONV_IM(conv));
7077 if (icon == NULL)
7079 gtk_widget_set_size_request(gtkconv->u.im->icon_container,
7080 -1, BUDDYICON_SIZE_MIN);
7081 return;
7084 data = purple_buddy_icon_get_data(icon, &len);
7085 if (data == NULL)
7087 gtk_widget_set_size_request(gtkconv->u.im->icon_container,
7088 -1, BUDDYICON_SIZE_MIN);
7089 return;
7093 gtkconv->u.im->anim = pidgin_pixbuf_anim_from_data(data, len);
7094 purple_imgstore_unref(custom_img);
7096 if (!gtkconv->u.im->anim) {
7097 purple_debug_error("gtkconv", "Couldn't load icon for conv %s\n",
7098 purple_conversation_get_name(conv));
7099 return;
7102 if (gdk_pixbuf_animation_is_static_image(gtkconv->u.im->anim)) {
7103 GdkPixbuf *stat;
7104 gtkconv->u.im->iter = NULL;
7105 stat = gdk_pixbuf_animation_get_static_image(gtkconv->u.im->anim);
7106 buf = gdk_pixbuf_add_alpha(stat, FALSE, 0, 0, 0);
7107 } else {
7108 GdkPixbuf *stat;
7109 gtkconv->u.im->iter =
7110 gdk_pixbuf_animation_get_iter(gtkconv->u.im->anim, NULL); /* LEAK */
7111 stat = gdk_pixbuf_animation_iter_get_pixbuf(gtkconv->u.im->iter);
7112 buf = gdk_pixbuf_add_alpha(stat, FALSE, 0, 0, 0);
7113 if (gtkconv->u.im->animate)
7114 start_anim(NULL, gtkconv);
7117 scale_width = gdk_pixbuf_get_width(buf);
7118 scale_height = gdk_pixbuf_get_height(buf);
7120 gtk_widget_get_size_request(gtkconv->u.im->icon_container, NULL, &size);
7121 size = MIN(size, MIN(scale_width, scale_height));
7123 /* Some sanity checks */
7124 size = CLAMP(size, BUDDYICON_SIZE_MIN, BUDDYICON_SIZE_MAX);
7125 if (scale_width == scale_height) {
7126 scale_width = scale_height = size;
7127 } else if (scale_height > scale_width) {
7128 scale_width = size * scale_width / scale_height;
7129 scale_height = size;
7130 } else {
7131 scale_height = size * scale_height / scale_width;
7132 scale_width = size;
7134 scale = gdk_pixbuf_scale_simple(buf, scale_width, scale_height,
7135 GDK_INTERP_BILINEAR);
7136 g_object_unref(buf);
7137 if (pidgin_gdk_pixbuf_is_opaque(scale))
7138 pidgin_gdk_pixbuf_make_round(scale);
7140 event = gtk_event_box_new();
7141 gtk_container_add(GTK_CONTAINER(gtkconv->u.im->icon_container), event);
7142 gtk_event_box_set_visible_window(GTK_EVENT_BOX(event), FALSE);
7143 gtk_widget_add_events(event,
7144 GDK_POINTER_MOTION_MASK | GDK_LEAVE_NOTIFY_MASK);
7145 g_signal_connect(G_OBJECT(event), "button-press-event",
7146 G_CALLBACK(icon_menu), gtkconv);
7148 pidgin_tooltip_setup_for_widget(event, gtkconv, pidgin_conv_create_tooltip, NULL);
7149 gtk_widget_show(event);
7151 gtkconv->u.im->icon = gtk_image_new_from_pixbuf(scale);
7152 gtk_container_add(GTK_CONTAINER(event), gtkconv->u.im->icon);
7153 gtk_widget_show(gtkconv->u.im->icon);
7155 g_object_unref(G_OBJECT(scale));
7157 /* The buddy icon code needs badly to be fixed. */
7158 if(pidgin_conv_window_is_active_conversation(conv))
7160 buf = gdk_pixbuf_animation_get_static_image(gtkconv->u.im->anim);
7161 if (buddy && !PURPLE_BUDDY_IS_ONLINE(buddy))
7162 gdk_pixbuf_saturate_and_pixelate(buf, buf, 0.0, FALSE);
7163 gtk_window_set_icon(GTK_WINDOW(win->window), buf);
7167 void
7168 pidgin_conv_update_buttons_by_protocol(PurpleConversation *conv)
7170 PidginWindow *win;
7172 if (!PIDGIN_IS_PIDGIN_CONVERSATION(conv))
7173 return;
7175 win = PIDGIN_CONVERSATION(conv)->win;
7177 if (win != NULL && pidgin_conv_window_is_active_conversation(conv))
7178 gray_stuff_out(PIDGIN_CONVERSATION(conv));
7181 static gboolean
7182 pidgin_conv_xy_to_right_infopane(PidginWindow *win, int x, int y)
7184 gint pane_x, pane_y, x_rel;
7185 PidginConversation *gtkconv;
7187 gdk_window_get_origin(win->notebook->window, &pane_x, &pane_y);
7188 x_rel = x - pane_x;
7189 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
7190 return (x_rel > gtkconv->infopane->allocation.x + gtkconv->infopane->allocation.width / 2);
7194 pidgin_conv_get_tab_at_xy(PidginWindow *win, int x, int y, gboolean *to_right)
7196 gint nb_x, nb_y, x_rel, y_rel;
7197 GtkNotebook *notebook;
7198 GtkWidget *page, *tab;
7199 gint i, page_num = -1;
7200 gint count;
7201 gboolean horiz;
7203 if (to_right)
7204 *to_right = FALSE;
7206 notebook = GTK_NOTEBOOK(win->notebook);
7208 gdk_window_get_origin(win->notebook->window, &nb_x, &nb_y);
7209 x_rel = x - nb_x;
7210 y_rel = y - nb_y;
7212 horiz = (gtk_notebook_get_tab_pos(notebook) == GTK_POS_TOP ||
7213 gtk_notebook_get_tab_pos(notebook) == GTK_POS_BOTTOM);
7215 count = gtk_notebook_get_n_pages(GTK_NOTEBOOK(notebook));
7217 for (i = 0; i < count; i++) {
7219 page = gtk_notebook_get_nth_page(GTK_NOTEBOOK(notebook), i);
7220 tab = gtk_notebook_get_tab_label(GTK_NOTEBOOK(notebook), page);
7222 /* Make sure the tab is not hidden beyond an arrow */
7223 if (!GTK_WIDGET_DRAWABLE(tab) && gtk_notebook_get_show_tabs(notebook))
7224 continue;
7226 if (horiz) {
7227 if (x_rel >= tab->allocation.x - PIDGIN_HIG_BOX_SPACE &&
7228 x_rel <= tab->allocation.x + tab->allocation.width + PIDGIN_HIG_BOX_SPACE) {
7229 page_num = i;
7231 if (to_right && x_rel >= tab->allocation.x + tab->allocation.width/2)
7232 *to_right = TRUE;
7234 break;
7236 } else {
7237 if (y_rel >= tab->allocation.y - PIDGIN_HIG_BOX_SPACE &&
7238 y_rel <= tab->allocation.y + tab->allocation.height + PIDGIN_HIG_BOX_SPACE) {
7239 page_num = i;
7241 if (to_right && y_rel >= tab->allocation.y + tab->allocation.height/2)
7242 *to_right = TRUE;
7244 break;
7249 if (page_num == -1) {
7250 /* Add after the last tab */
7251 page_num = count - 1;
7254 return page_num;
7257 static void
7258 close_on_tabs_pref_cb(const char *name, PurplePrefType type,
7259 gconstpointer value, gpointer data)
7261 GList *l;
7262 PurpleConversation *conv;
7263 PidginConversation *gtkconv;
7265 for (l = purple_get_conversations(); l != NULL; l = l->next) {
7266 conv = (PurpleConversation *)l->data;
7268 if (!PIDGIN_IS_PIDGIN_CONVERSATION(conv))
7269 continue;
7271 gtkconv = PIDGIN_CONVERSATION(conv);
7273 if (value)
7274 gtk_widget_show(gtkconv->close);
7275 else
7276 gtk_widget_hide(gtkconv->close);
7280 static void
7281 spellcheck_pref_cb(const char *name, PurplePrefType type,
7282 gconstpointer value, gpointer data)
7284 #ifdef USE_GTKSPELL
7285 GList *cl;
7286 PurpleConversation *conv;
7287 PidginConversation *gtkconv;
7288 GtkSpell *spell;
7290 for (cl = purple_get_conversations(); cl != NULL; cl = cl->next) {
7292 conv = (PurpleConversation *)cl->data;
7294 if (!PIDGIN_IS_PIDGIN_CONVERSATION(conv))
7295 continue;
7297 gtkconv = PIDGIN_CONVERSATION(conv);
7299 if (value)
7300 pidgin_setup_gtkspell(GTK_TEXT_VIEW(gtkconv->entry));
7301 else {
7302 spell = gtkspell_get_from_text_view(GTK_TEXT_VIEW(gtkconv->entry));
7303 if (spell)
7304 gtkspell_detach(spell);
7307 #endif
7310 static void
7311 tab_side_pref_cb(const char *name, PurplePrefType type,
7312 gconstpointer value, gpointer data)
7314 GList *gtkwins, *gtkconvs;
7315 GtkPositionType pos;
7316 PidginWindow *gtkwin;
7318 pos = GPOINTER_TO_INT(value);
7320 for (gtkwins = pidgin_conv_windows_get_list(); gtkwins != NULL; gtkwins = gtkwins->next) {
7321 gtkwin = gtkwins->data;
7322 gtk_notebook_set_tab_pos(GTK_NOTEBOOK(gtkwin->notebook), pos&~8);
7323 for (gtkconvs = gtkwin->gtkconvs; gtkconvs != NULL; gtkconvs = gtkconvs->next) {
7324 pidgin_conv_tab_pack(gtkwin, gtkconvs->data);
7329 static void
7330 show_timestamps_pref_cb(const char *name, PurplePrefType type,
7331 gconstpointer value, gpointer data)
7333 GList *l;
7334 PurpleConversation *conv;
7335 PidginConversation *gtkconv;
7336 PidginWindow *win;
7338 for (l = purple_get_conversations(); l != NULL; l = l->next)
7340 conv = (PurpleConversation *)l->data;
7342 if (!PIDGIN_IS_PIDGIN_CONVERSATION(conv))
7343 continue;
7345 gtkconv = PIDGIN_CONVERSATION(conv);
7346 win = gtkconv->win;
7348 gtk_check_menu_item_set_active(
7349 GTK_CHECK_MENU_ITEM(win->menu.show_timestamps),
7350 (gboolean)GPOINTER_TO_INT(value));
7352 gtk_imhtml_show_comments(GTK_IMHTML(gtkconv->imhtml),
7353 (gboolean)GPOINTER_TO_INT(value));
7357 static void
7358 show_formatting_toolbar_pref_cb(const char *name, PurplePrefType type,
7359 gconstpointer value, gpointer data)
7361 GList *l;
7362 PurpleConversation *conv;
7363 PidginConversation *gtkconv;
7364 PidginWindow *win;
7366 for (l = purple_get_conversations(); l != NULL; l = l->next)
7368 conv = (PurpleConversation *)l->data;
7370 if (!PIDGIN_IS_PIDGIN_CONVERSATION(conv))
7371 continue;
7373 gtkconv = PIDGIN_CONVERSATION(conv);
7374 win = gtkconv->win;
7376 gtk_check_menu_item_set_active(
7377 GTK_CHECK_MENU_ITEM(win->menu.show_formatting_toolbar),
7378 (gboolean)GPOINTER_TO_INT(value));
7380 if ((gboolean)GPOINTER_TO_INT(value))
7381 gtk_widget_show(gtkconv->toolbar);
7382 else
7383 gtk_widget_hide(gtkconv->toolbar);
7385 g_idle_add((GSourceFunc)resize_imhtml_cb,gtkconv);
7389 static void
7390 animate_buddy_icons_pref_cb(const char *name, PurplePrefType type,
7391 gconstpointer value, gpointer data)
7393 GList *l;
7394 PurpleConversation *conv;
7395 PidginConversation *gtkconv;
7396 PidginWindow *win;
7398 if (!purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons"))
7399 return;
7401 /* Set the "animate" flag for each icon based on the new preference */
7402 for (l = purple_get_ims(); l != NULL; l = l->next) {
7403 conv = (PurpleConversation *)l->data;
7404 gtkconv = PIDGIN_CONVERSATION(conv);
7405 if (gtkconv)
7406 gtkconv->u.im->animate = GPOINTER_TO_INT(value);
7409 /* Now either stop or start animation for the active conversation in each window */
7410 for (l = pidgin_conv_windows_get_list(); l != NULL; l = l->next) {
7411 win = l->data;
7412 conv = pidgin_conv_window_get_active_conversation(win);
7413 pidgin_conv_update_buddy_icon(conv);
7417 static void
7418 show_buddy_icons_pref_cb(const char *name, PurplePrefType type,
7419 gconstpointer value, gpointer data)
7421 GList *l;
7423 for (l = purple_get_conversations(); l != NULL; l = l->next) {
7424 PurpleConversation *conv = l->data;
7425 if (!PIDGIN_CONVERSATION(conv))
7426 continue;
7427 if (GPOINTER_TO_INT(value))
7428 gtk_widget_show(PIDGIN_CONVERSATION(conv)->infopane_hbox);
7429 else
7430 gtk_widget_hide(PIDGIN_CONVERSATION(conv)->infopane_hbox);
7432 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) {
7433 pidgin_conv_update_buddy_icon(conv);
7437 /* Make the tabs show/hide correctly */
7438 for (l = pidgin_conv_windows_get_list(); l != NULL; l = l->next) {
7439 PidginWindow *win = l->data;
7440 if (pidgin_conv_window_get_gtkconv_count(win) == 1)
7441 gtk_notebook_set_show_tabs(GTK_NOTEBOOK(win->notebook),
7442 GPOINTER_TO_INT(value) == 0);
7446 static void
7447 show_protocol_icons_pref_cb(const char *name, PurplePrefType type,
7448 gconstpointer value, gpointer data)
7450 GList *l;
7451 for (l = purple_get_conversations(); l != NULL; l = l->next) {
7452 PurpleConversation *conv = l->data;
7453 if (PIDGIN_CONVERSATION(conv))
7454 update_tab_icon(conv);
7458 static void
7459 conv_placement_usetabs_cb(const char *name, PurplePrefType type,
7460 gconstpointer value, gpointer data)
7462 purple_prefs_trigger_callback(PIDGIN_PREFS_ROOT "/conversations/placement");
7465 static void
7466 account_status_changed_cb(PurpleAccount *account, PurpleStatus *oldstatus,
7467 PurpleStatus *newstatus)
7469 GList *l;
7470 PurpleConversation *conv = NULL;
7471 PidginConversation *gtkconv;
7473 if(!purple_strequal(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new"), "away"))
7474 return;
7476 if(purple_status_is_available(oldstatus) || !purple_status_is_available(newstatus))
7477 return;
7479 for (l = hidden_convwin->gtkconvs; l; ) {
7480 gtkconv = l->data;
7481 l = l->next;
7483 conv = gtkconv->active_conv;
7484 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT ||
7485 account != purple_conversation_get_account(conv))
7486 continue;
7488 pidgin_conv_attach_to_conversation(conv);
7490 /* TODO: do we need to do anything for any other conversations that are in the same gtkconv here?
7491 * I'm a little concerned that not doing so will cause the "pending" indicator in the gtkblist not to be cleared. -DAA*/
7492 purple_conversation_update(conv, PURPLE_CONV_UPDATE_UNSEEN);
7496 static void
7497 hide_new_pref_cb(const char *name, PurplePrefType type,
7498 gconstpointer value, gpointer data)
7500 GList *l;
7501 PurpleConversation *conv = NULL;
7502 PidginConversation *gtkconv;
7503 gboolean when_away = FALSE;
7505 if(!hidden_convwin)
7506 return;
7508 if(purple_strequal(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new"), "always"))
7509 return;
7511 if(purple_strequal(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new"), "away"))
7512 when_away = TRUE;
7514 for (l = hidden_convwin->gtkconvs; l; )
7516 gtkconv = l->data;
7517 l = l->next;
7519 conv = gtkconv->active_conv;
7521 if (conv->type == PURPLE_CONV_TYPE_CHAT ||
7522 gtkconv->unseen_count == 0 ||
7523 (when_away && !purple_status_is_available(
7524 purple_account_get_active_status(
7525 purple_conversation_get_account(conv)))))
7526 continue;
7528 pidgin_conv_attach_to_conversation(conv);
7533 static void
7534 conv_placement_pref_cb(const char *name, PurplePrefType type,
7535 gconstpointer value, gpointer data)
7537 PidginConvPlacementFunc func;
7539 if (!purple_strequal(name, PIDGIN_PREFS_ROOT "/conversations/placement"))
7540 return;
7542 func = pidgin_conv_placement_get_fnc(value);
7544 if (func == NULL)
7545 return;
7547 pidgin_conv_placement_set_current_func(func);
7550 static PidginConversation *
7551 get_gtkconv_with_contact(PurpleContact *contact)
7553 PurpleBlistNode *node;
7555 node = ((PurpleBlistNode*)contact)->child;
7557 for (; node; node = node->next)
7559 PurpleBuddy *buddy = (PurpleBuddy*)node;
7560 PurpleConversation *conv;
7561 conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, buddy->name, buddy->account);
7562 if (conv)
7563 return PIDGIN_CONVERSATION(conv);
7565 return NULL;
7568 static void
7569 account_signed_off_cb(PurpleConnection *gc, gpointer event)
7571 GList *iter;
7573 for (iter = purple_get_conversations(); iter; iter = iter->next)
7575 PurpleConversation *conv = iter->data;
7577 /* This seems fine in theory, but we also need to cover the
7578 * case of this account matching one of the other buddies in
7579 * one of the contacts containing the buddy corresponding to
7580 * a conversation. It's easier to just update them all. */
7581 /* if (purple_conversation_get_account(conv) == account) */
7582 pidgin_conv_update_fields(conv, PIDGIN_CONV_TAB_ICON |
7583 PIDGIN_CONV_MENU | PIDGIN_CONV_COLORIZE_TITLE);
7585 if (PURPLE_CONNECTION_IS_CONNECTED(gc) &&
7586 conv->type == PURPLE_CONV_TYPE_CHAT &&
7587 conv->account == gc->account &&
7588 purple_conversation_get_data(conv, "want-to-rejoin")) {
7589 GHashTable *comps = NULL;
7590 PurpleChat *chat = purple_blist_find_chat(conv->account, conv->name);
7591 if (chat == NULL) {
7592 if (PURPLE_PLUGIN_PROTOCOL_INFO(gc->prpl)->chat_info_defaults != NULL)
7593 comps = PURPLE_PLUGIN_PROTOCOL_INFO(gc->prpl)->chat_info_defaults(gc, conv->name);
7594 } else {
7595 comps = chat->components;
7597 serv_join_chat(gc, comps);
7598 if (chat == NULL && comps != NULL)
7599 g_hash_table_destroy(comps);
7604 static void
7605 account_signing_off(PurpleConnection *gc)
7607 GList *list = purple_get_chats();
7608 PurpleAccount *account = purple_connection_get_account(gc);
7610 /* We are about to sign off. See which chats we are currently in, and mark
7611 * them for rejoin on reconnect. */
7612 while (list) {
7613 PurpleConversation *conv = list->data;
7614 if (!purple_conv_chat_has_left(PURPLE_CONV_CHAT(conv)) &&
7615 purple_conversation_get_account(conv) == account) {
7616 purple_conversation_set_data(conv, "want-to-rejoin", GINT_TO_POINTER(TRUE));
7617 purple_conversation_write(conv, NULL, _("The account has disconnected and you are no "
7618 "longer in this chat. You will automatically rejoin the chat when "
7619 "the account reconnects."),
7620 PURPLE_MESSAGE_SYSTEM, time(NULL));
7622 list = list->next;
7626 static void
7627 update_buddy_status_changed(PurpleBuddy *buddy, PurpleStatus *old, PurpleStatus *newstatus)
7629 PidginConversation *gtkconv;
7630 PurpleConversation *conv;
7632 gtkconv = get_gtkconv_with_contact(purple_buddy_get_contact(buddy));
7633 if (gtkconv)
7635 conv = gtkconv->active_conv;
7636 pidgin_conv_update_fields(conv, PIDGIN_CONV_TAB_ICON
7637 | PIDGIN_CONV_COLORIZE_TITLE
7638 | PIDGIN_CONV_BUDDY_ICON);
7639 if ((purple_status_is_online(old) ^ purple_status_is_online(newstatus)) != 0)
7640 pidgin_conv_update_fields(conv, PIDGIN_CONV_MENU);
7644 static void
7645 update_buddy_privacy_changed(PurpleBuddy *buddy)
7647 PidginConversation *gtkconv;
7648 PurpleConversation *conv;
7650 gtkconv = get_gtkconv_with_contact(purple_buddy_get_contact(buddy));
7651 if (gtkconv) {
7652 conv = gtkconv->active_conv;
7653 pidgin_conv_update_fields(conv, PIDGIN_CONV_TAB_ICON | PIDGIN_CONV_MENU);
7657 static void
7658 update_buddy_idle_changed(PurpleBuddy *buddy, gboolean old, gboolean newidle)
7660 PurpleConversation *conv;
7662 conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, buddy->name, buddy->account);
7663 if (conv)
7664 pidgin_conv_update_fields(conv, PIDGIN_CONV_TAB_ICON);
7667 static void
7668 update_buddy_icon(PurpleBuddy *buddy)
7670 PurpleConversation *conv;
7672 conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, buddy->name, buddy->account);
7673 if (conv)
7674 pidgin_conv_update_fields(conv, PIDGIN_CONV_BUDDY_ICON);
7677 static void
7678 update_buddy_sign(PurpleBuddy *buddy, const char *which)
7680 PurplePresence *presence;
7681 PurpleStatus *on, *off;
7683 presence = purple_buddy_get_presence(buddy);
7684 if (!presence)
7685 return;
7686 off = purple_presence_get_status(presence, "offline");
7687 on = purple_presence_get_status(presence, "available");
7689 if (*(which+1) == 'f')
7690 update_buddy_status_changed(buddy, on, off);
7691 else
7692 update_buddy_status_changed(buddy, off, on);
7695 static void
7696 update_conversation_switched(PurpleConversation *conv)
7698 pidgin_conv_update_fields(conv, PIDGIN_CONV_TAB_ICON | PIDGIN_CONV_SET_TITLE |
7699 PIDGIN_CONV_MENU | PIDGIN_CONV_BUDDY_ICON);
7702 static void
7703 update_buddy_typing(PurpleAccount *account, const char *who)
7705 PurpleConversation *conv;
7706 PidginConversation *gtkconv;
7708 conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, who, account);
7709 if (!conv)
7710 return;
7712 gtkconv = PIDGIN_CONVERSATION(conv);
7713 if (gtkconv && gtkconv->active_conv == conv)
7714 pidgin_conv_update_fields(conv, PIDGIN_CONV_COLORIZE_TITLE);
7717 static void
7718 update_chat(PurpleConversation *conv)
7720 pidgin_conv_update_fields(conv, PIDGIN_CONV_TOPIC |
7721 PIDGIN_CONV_MENU | PIDGIN_CONV_SET_TITLE);
7724 static void
7725 update_chat_topic(PurpleConversation *conv, const char *old, const char *new)
7727 pidgin_conv_update_fields(conv, PIDGIN_CONV_TOPIC);
7730 /* Message history stuff */
7732 /* Compare two PurpleConvMessage's, according to time in ascending order. */
7733 static int
7734 message_compare(gconstpointer p1, gconstpointer p2)
7736 const PurpleConvMessage *m1 = p1, *m2 = p2;
7737 return (m1->when > m2->when);
7740 /* Adds some message history to the gtkconv. This happens in a idle-callback. */
7741 static gboolean
7742 add_message_history_to_gtkconv(gpointer data)
7744 PidginConversation *gtkconv = data;
7745 int count = 0;
7746 int timer = gtkconv->attach.timer;
7747 time_t when = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(gtkconv->entry), "attach-start-time"));
7748 gboolean im = (gtkconv->active_conv->type == PURPLE_CONV_TYPE_IM);
7750 gtkconv->attach.timer = 0;
7751 while (gtkconv->attach.current && count < 100) { /* XXX: 100 is a random value here */
7752 PurpleConvMessage *msg = gtkconv->attach.current->data;
7753 if (!im && when && when < msg->when) {
7754 gtk_imhtml_append_text(GTK_IMHTML(gtkconv->imhtml), "<BR><HR>", 0);
7755 g_object_set_data(G_OBJECT(gtkconv->entry), "attach-start-time", NULL);
7757 pidgin_conv_write_conv(msg->conv, msg->who, msg->alias, msg->what, msg->flags, msg->when);
7758 if (im) {
7759 gtkconv->attach.current = g_list_delete_link(gtkconv->attach.current, gtkconv->attach.current);
7760 } else {
7761 gtkconv->attach.current = gtkconv->attach.current->prev;
7763 count++;
7765 gtkconv->attach.timer = timer;
7766 if (gtkconv->attach.current)
7767 return TRUE;
7769 g_source_remove(gtkconv->attach.timer);
7770 gtkconv->attach.timer = 0;
7771 if (im) {
7772 /* Print any message that was sent while the old history was being added back. */
7773 GList *msgs = NULL;
7774 GList *iter = gtkconv->convs;
7775 for (; iter; iter = iter->next) {
7776 PurpleConversation *conv = iter->data;
7777 GList *history = purple_conversation_get_message_history(conv);
7778 for (; history; history = history->next) {
7779 PurpleConvMessage *msg = history->data;
7780 if (msg->when > when)
7781 msgs = g_list_prepend(msgs, msg);
7784 msgs = g_list_sort(msgs, message_compare);
7785 for (; msgs; msgs = g_list_delete_link(msgs, msgs)) {
7786 PurpleConvMessage *msg = msgs->data;
7787 pidgin_conv_write_conv(msg->conv, msg->who, msg->alias, msg->what, msg->flags, msg->when);
7789 gtk_imhtml_append_text(GTK_IMHTML(gtkconv->imhtml), "<BR><HR>", 0);
7790 g_object_set_data(G_OBJECT(gtkconv->entry), "attach-start-time", NULL);
7793 g_object_set_data(G_OBJECT(gtkconv->entry), "attach-start-time", NULL);
7794 purple_signal_emit(pidgin_conversations_get_handle(),
7795 "conversation-displayed", gtkconv);
7796 return FALSE;
7799 static void
7800 pidgin_conv_attach(PurpleConversation *conv)
7802 int timer;
7803 purple_conversation_set_data(conv, "unseen-count", NULL);
7804 purple_conversation_set_data(conv, "unseen-state", NULL);
7805 purple_conversation_set_ui_ops(conv, pidgin_conversations_get_conv_ui_ops());
7806 if (!PIDGIN_CONVERSATION(conv))
7807 private_gtkconv_new(conv, FALSE);
7808 timer = GPOINTER_TO_INT(purple_conversation_get_data(conv, "close-timer"));
7809 if (timer) {
7810 purple_timeout_remove(timer);
7811 purple_conversation_set_data(conv, "close-timer", NULL);
7815 gboolean pidgin_conv_attach_to_conversation(PurpleConversation *conv)
7817 GList *list;
7818 PidginConversation *gtkconv;
7820 if (PIDGIN_IS_PIDGIN_CONVERSATION(conv)) {
7821 /* This is pretty much always the case now. */
7822 gtkconv = PIDGIN_CONVERSATION(conv);
7823 if (gtkconv->win != hidden_convwin)
7824 return FALSE;
7825 pidgin_conv_window_remove_gtkconv(hidden_convwin, gtkconv);
7826 pidgin_conv_placement_place(gtkconv);
7827 purple_signal_emit(pidgin_conversations_get_handle(),
7828 "conversation-displayed", gtkconv);
7829 list = gtkconv->convs;
7830 while (list) {
7831 pidgin_conv_attach(list->data);
7832 list = list->next;
7834 return TRUE;
7837 pidgin_conv_attach(conv);
7838 gtkconv = PIDGIN_CONVERSATION(conv);
7840 list = purple_conversation_get_message_history(conv);
7841 if (list) {
7842 switch (purple_conversation_get_type(conv)) {
7843 case PURPLE_CONV_TYPE_IM:
7845 GList *convs;
7846 list = g_list_copy(list);
7847 for (convs = purple_get_ims(); convs; convs = convs->next)
7848 if (convs->data != conv &&
7849 pidgin_conv_find_gtkconv(convs->data) == gtkconv) {
7850 pidgin_conv_attach(convs->data);
7851 list = g_list_concat(list, g_list_copy(purple_conversation_get_message_history(convs->data)));
7853 list = g_list_sort(list, message_compare);
7854 gtkconv->attach.current = list;
7855 list = g_list_last(list);
7856 break;
7858 case PURPLE_CONV_TYPE_CHAT:
7859 gtkconv->attach.current = g_list_last(list);
7860 break;
7861 default:
7862 g_return_val_if_reached(TRUE);
7864 g_object_set_data(G_OBJECT(gtkconv->entry), "attach-start-time",
7865 GINT_TO_POINTER(((PurpleConvMessage*)(list->data))->when));
7866 gtkconv->attach.timer = g_idle_add(add_message_history_to_gtkconv, gtkconv);
7867 } else {
7868 purple_signal_emit(pidgin_conversations_get_handle(),
7869 "conversation-displayed", gtkconv);
7872 if (conv->type == PURPLE_CONV_TYPE_CHAT) {
7873 pidgin_conv_update_fields(conv, PIDGIN_CONV_TOPIC);
7874 pidgin_conv_chat_add_users(conv, PURPLE_CONV_CHAT(conv)->in_room, TRUE);
7877 return TRUE;
7880 void *
7881 pidgin_conversations_get_handle(void)
7883 static int handle;
7885 return &handle;
7888 void
7889 pidgin_conversations_init(void)
7891 void *handle = pidgin_conversations_get_handle();
7892 void *blist_handle = purple_blist_get_handle();
7894 /* Conversations */
7895 purple_prefs_add_none(PIDGIN_PREFS_ROOT "/conversations");
7896 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/use_smooth_scrolling", TRUE);
7897 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/close_on_tabs", TRUE);
7898 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/send_bold", FALSE);
7899 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/send_italic", FALSE);
7900 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/send_underline", FALSE);
7901 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/spellcheck", TRUE);
7902 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/show_incoming_formatting", TRUE);
7903 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/resize_custom_smileys", TRUE);
7904 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/custom_smileys_size", 96);
7905 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/minimum_entry_lines", 2);
7907 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/show_timestamps", TRUE);
7908 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/show_formatting_toolbar", TRUE);
7910 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/placement", "last");
7911 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/placement_number", 1);
7912 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/bgcolor", "");
7913 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/fgcolor", "");
7914 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/font_face", "");
7915 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/font_size", 3);
7916 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/tabs", TRUE);
7917 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/tab_side", GTK_POS_TOP);
7918 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/scrollback_lines", 4000);
7920 #ifdef _WIN32
7921 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/use_theme_font", TRUE);
7922 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/custom_font", "");
7923 #endif
7925 /* Conversations -> Chat */
7926 purple_prefs_add_none(PIDGIN_PREFS_ROOT "/conversations/chat");
7927 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/entry_height", 54);
7928 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/userlist_width", 80);
7929 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/x", 0);
7930 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/y", 0);
7931 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/width", 340);
7932 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/height", 390);
7934 /* Conversations -> IM */
7935 purple_prefs_add_none(PIDGIN_PREFS_ROOT "/conversations/im");
7936 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/im/x", 0);
7937 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/im/y", 0);
7938 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/im/width", 340);
7939 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/im/height", 390);
7941 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/im/animate_buddy_icons", TRUE);
7943 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/im/entry_height", 54);
7944 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons", TRUE);
7946 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new", "never");
7947 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/im/close_immediately", TRUE);
7949 #ifdef _WIN32
7950 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/win32/minimize_new_convs", FALSE);
7951 #endif
7953 /* Connect callbacks. */
7954 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/close_on_tabs",
7955 close_on_tabs_pref_cb, NULL);
7956 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/show_timestamps",
7957 show_timestamps_pref_cb, NULL);
7958 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/show_formatting_toolbar",
7959 show_formatting_toolbar_pref_cb, NULL);
7960 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/spellcheck",
7961 spellcheck_pref_cb, NULL);
7962 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/tab_side",
7963 tab_side_pref_cb, NULL);
7965 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/tabs",
7966 conv_placement_usetabs_cb, NULL);
7968 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/placement",
7969 conv_placement_pref_cb, NULL);
7970 purple_prefs_trigger_callback(PIDGIN_PREFS_ROOT "/conversations/placement");
7972 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/minimum_entry_lines",
7973 minimum_entry_lines_pref_cb, NULL);
7975 /* IM callbacks */
7976 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/im/animate_buddy_icons",
7977 animate_buddy_icons_pref_cb, NULL);
7978 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons",
7979 show_buddy_icons_pref_cb, NULL);
7980 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/blist/show_protocol_icons",
7981 show_protocol_icons_pref_cb, NULL);
7982 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/im/hide_new",
7983 hide_new_pref_cb, NULL);
7987 /**********************************************************************
7988 * Register signals
7989 **********************************************************************/
7990 purple_signal_register(handle, "conversation-dragging",
7991 purple_marshal_VOID__POINTER_POINTER, NULL, 2,
7992 purple_value_new(PURPLE_TYPE_BOXED,
7993 "PidginWindow *"),
7994 purple_value_new(PURPLE_TYPE_BOXED,
7995 "PidginWindow *"));
7997 purple_signal_register(handle, "conversation-timestamp",
7998 #if SIZEOF_TIME_T == 4
7999 purple_marshal_POINTER__POINTER_INT_BOOLEAN,
8000 #elif SIZEOF_TIME_T == 8
8001 purple_marshal_POINTER__POINTER_INT64_BOOLEAN,
8002 #else
8003 #error Unkown size of time_t
8004 #endif
8005 purple_value_new(PURPLE_TYPE_STRING), 3,
8006 purple_value_new(PURPLE_TYPE_SUBTYPE,
8007 PURPLE_SUBTYPE_CONVERSATION),
8008 #if SIZEOF_TIME_T == 4
8009 purple_value_new(PURPLE_TYPE_INT),
8010 #elif SIZEOF_TIME_T == 8
8011 purple_value_new(PURPLE_TYPE_INT64),
8012 #else
8013 # error Unknown size of time_t
8014 #endif
8015 purple_value_new(PURPLE_TYPE_BOOLEAN));
8017 purple_signal_register(handle, "displaying-im-msg",
8018 purple_marshal_BOOLEAN__POINTER_POINTER_POINTER_POINTER_POINTER,
8019 purple_value_new(PURPLE_TYPE_BOOLEAN), 5,
8020 purple_value_new(PURPLE_TYPE_SUBTYPE,
8021 PURPLE_SUBTYPE_ACCOUNT),
8022 purple_value_new(PURPLE_TYPE_STRING),
8023 purple_value_new_outgoing(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, "displayed-im-msg",
8029 purple_marshal_VOID__POINTER_POINTER_POINTER_POINTER_UINT,
8030 NULL, 5,
8031 purple_value_new(PURPLE_TYPE_SUBTYPE,
8032 PURPLE_SUBTYPE_ACCOUNT),
8033 purple_value_new(PURPLE_TYPE_STRING),
8034 purple_value_new(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, "displaying-chat-msg",
8040 purple_marshal_BOOLEAN__POINTER_POINTER_POINTER_POINTER_POINTER,
8041 purple_value_new(PURPLE_TYPE_BOOLEAN), 5,
8042 purple_value_new(PURPLE_TYPE_SUBTYPE,
8043 PURPLE_SUBTYPE_ACCOUNT),
8044 purple_value_new(PURPLE_TYPE_STRING),
8045 purple_value_new_outgoing(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, "displayed-chat-msg",
8051 purple_marshal_VOID__POINTER_POINTER_POINTER_POINTER_UINT,
8052 NULL, 5,
8053 purple_value_new(PURPLE_TYPE_SUBTYPE,
8054 PURPLE_SUBTYPE_ACCOUNT),
8055 purple_value_new(PURPLE_TYPE_STRING),
8056 purple_value_new(PURPLE_TYPE_STRING),
8057 purple_value_new(PURPLE_TYPE_SUBTYPE,
8058 PURPLE_SUBTYPE_CONVERSATION),
8059 purple_value_new(PURPLE_TYPE_INT));
8061 purple_signal_register(handle, "conversation-switched",
8062 purple_marshal_VOID__POINTER, NULL, 1,
8063 purple_value_new(PURPLE_TYPE_SUBTYPE,
8064 PURPLE_SUBTYPE_CONVERSATION));
8066 purple_signal_register(handle, "conversation-hiding",
8067 purple_marshal_VOID__POINTER, NULL, 1,
8068 purple_value_new(PURPLE_TYPE_BOXED,
8069 "PidginConversation *"));
8071 purple_signal_register(handle, "conversation-displayed",
8072 purple_marshal_VOID__POINTER, NULL, 1,
8073 purple_value_new(PURPLE_TYPE_BOXED,
8074 "PidginConversation *"));
8076 purple_signal_register(handle, "chat-nick-autocomplete",
8077 purple_marshal_BOOLEAN__POINTER_BOOLEAN,
8078 purple_value_new(PURPLE_TYPE_BOOLEAN), 1,
8079 purple_value_new(PURPLE_TYPE_SUBTYPE,
8080 PURPLE_SUBTYPE_CONVERSATION));
8082 purple_signal_register(handle, "chat-nick-clicked",
8083 purple_marshal_BOOLEAN__POINTER_POINTER_UINT,
8084 purple_value_new(PURPLE_TYPE_BOOLEAN), 3,
8085 purple_value_new(PURPLE_TYPE_SUBTYPE,
8086 PURPLE_SUBTYPE_CONVERSATION),
8087 purple_value_new(PURPLE_TYPE_STRING),
8088 purple_value_new(PURPLE_TYPE_UINT));
8091 /**********************************************************************
8092 * Register commands
8093 **********************************************************************/
8094 purple_cmd_register("say", "S", PURPLE_CMD_P_DEFAULT,
8095 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM, NULL,
8096 say_command_cb, _("say &lt;message&gt;: Send a message normally as if you weren't using a command."), NULL);
8097 purple_cmd_register("me", "S", PURPLE_CMD_P_DEFAULT,
8098 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM, NULL,
8099 me_command_cb, _("me &lt;action&gt;: Send an IRC style action to a buddy or chat."), NULL);
8100 purple_cmd_register("debug", "w", PURPLE_CMD_P_DEFAULT,
8101 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM, NULL,
8102 debug_command_cb, _("debug &lt;option&gt;: Send various debug information to the current conversation."), NULL);
8103 purple_cmd_register("clear", "", PURPLE_CMD_P_DEFAULT,
8104 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM, NULL,
8105 clear_command_cb, _("clear: Clears the conversation scrollback."), NULL);
8106 purple_cmd_register("clearall", "", PURPLE_CMD_P_DEFAULT,
8107 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM, NULL,
8108 clearall_command_cb, _("clear: Clears all conversation scrollbacks."), NULL);
8109 purple_cmd_register("help", "w", PURPLE_CMD_P_DEFAULT,
8110 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM | PURPLE_CMD_FLAG_ALLOW_WRONG_ARGS, NULL,
8111 help_command_cb, _("help &lt;command&gt;: Help on a specific command."), NULL);
8113 /**********************************************************************
8114 * UI operations
8115 **********************************************************************/
8117 purple_signal_connect(purple_connections_get_handle(), "signed-on", handle,
8118 G_CALLBACK(account_signed_off_cb),
8119 GINT_TO_POINTER(PURPLE_CONV_ACCOUNT_ONLINE));
8120 purple_signal_connect(purple_connections_get_handle(), "signed-off", handle,
8121 G_CALLBACK(account_signed_off_cb),
8122 GINT_TO_POINTER(PURPLE_CONV_ACCOUNT_OFFLINE));
8123 purple_signal_connect(purple_connections_get_handle(), "signing-off", handle,
8124 G_CALLBACK(account_signing_off), NULL);
8126 purple_signal_connect(purple_conversations_get_handle(), "received-im-msg",
8127 handle, G_CALLBACK(received_im_msg_cb), NULL);
8128 purple_signal_connect(purple_conversations_get_handle(), "cleared-message-history",
8129 handle, G_CALLBACK(clear_conversation_scrollback_cb), NULL);
8131 purple_signal_connect(purple_conversations_get_handle(), "deleting-chat-buddy",
8132 handle, G_CALLBACK(deleting_chat_buddy_cb), NULL);
8134 purple_conversations_set_ui_ops(&conversation_ui_ops);
8136 hidden_convwin = pidgin_conv_window_new();
8137 window_list = g_list_remove(window_list, hidden_convwin);
8139 purple_signal_connect(purple_accounts_get_handle(), "account-status-changed",
8140 handle, PURPLE_CALLBACK(account_status_changed_cb), NULL);
8142 /* Callbacks to update a conversation */
8143 purple_signal_connect(blist_handle, "blist-node-added", handle,
8144 G_CALLBACK(buddy_update_cb), NULL);
8145 purple_signal_connect(blist_handle, "blist-node-removed", handle,
8146 G_CALLBACK(buddy_update_cb), NULL);
8147 purple_signal_connect(blist_handle, "buddy-signed-on",
8148 handle, PURPLE_CALLBACK(update_buddy_sign), "on");
8149 purple_signal_connect(blist_handle, "buddy-signed-off",
8150 handle, PURPLE_CALLBACK(update_buddy_sign), "off");
8151 purple_signal_connect(blist_handle, "buddy-status-changed",
8152 handle, PURPLE_CALLBACK(update_buddy_status_changed), NULL);
8153 purple_signal_connect(blist_handle, "buddy-privacy-changed",
8154 handle, PURPLE_CALLBACK(update_buddy_privacy_changed), NULL);
8155 purple_signal_connect(blist_handle, "buddy-idle-changed",
8156 handle, PURPLE_CALLBACK(update_buddy_idle_changed), NULL);
8157 purple_signal_connect(blist_handle, "buddy-icon-changed",
8158 handle, PURPLE_CALLBACK(update_buddy_icon), NULL);
8159 purple_signal_connect(purple_conversations_get_handle(), "buddy-typing",
8160 handle, PURPLE_CALLBACK(update_buddy_typing), NULL);
8161 purple_signal_connect(purple_conversations_get_handle(), "buddy-typing-stopped",
8162 handle, PURPLE_CALLBACK(update_buddy_typing), NULL);
8163 purple_signal_connect(pidgin_conversations_get_handle(), "conversation-switched",
8164 handle, PURPLE_CALLBACK(update_conversation_switched), NULL);
8165 purple_signal_connect(purple_conversations_get_handle(), "chat-left", handle,
8166 PURPLE_CALLBACK(update_chat), NULL);
8167 purple_signal_connect(purple_conversations_get_handle(), "chat-joined", handle,
8168 PURPLE_CALLBACK(update_chat), NULL);
8169 purple_signal_connect(purple_conversations_get_handle(), "chat-topic-changed", handle,
8170 PURPLE_CALLBACK(update_chat_topic), NULL);
8171 purple_signal_connect_priority(purple_conversations_get_handle(), "conversation-updated", handle,
8172 PURPLE_CALLBACK(pidgin_conv_updated), NULL,
8173 PURPLE_SIGNAL_PRIORITY_LOWEST);
8174 purple_signal_connect(purple_conversations_get_handle(), "wrote-im-msg", handle,
8175 PURPLE_CALLBACK(wrote_msg_update_unseen_cb), NULL);
8176 purple_signal_connect(purple_conversations_get_handle(), "wrote-chat-msg", handle,
8177 PURPLE_CALLBACK(wrote_msg_update_unseen_cb), NULL);
8180 /* Set default tab colors */
8181 GString *str = g_string_new(NULL);
8182 GtkSettings *settings = gtk_settings_get_default();
8183 GtkStyle *parent = gtk_rc_get_style_by_paths(settings, "tab-container.tab-label*", NULL, G_TYPE_NONE), *now;
8184 struct {
8185 const char *stylename;
8186 const char *labelname;
8187 const char *color;
8188 } styles[] = {
8189 {"pidgin_tab_label_typing_default", "tab-label-typing", "#4e9a06"},
8190 {"pidgin_tab_label_typed_default", "tab-label-typed", "#c4a000"},
8191 {"pidgin_tab_label_attention_default", "tab-label-attention", "#006aff"},
8192 {"pidgin_tab_label_unreadchat_default", "tab-label-unreadchat", "#cc0000"},
8193 {"pidgin_tab_label_event_default", "tab-label-event", "#888a85"},
8194 {NULL, NULL, NULL}
8196 int iter;
8197 for (iter = 0; styles[iter].stylename; iter++) {
8198 now = gtk_rc_get_style_by_paths(settings, styles[iter].labelname, NULL, G_TYPE_NONE);
8199 if (parent == now ||
8200 (parent && now && parent->rc_style == now->rc_style)) {
8201 g_string_append_printf(str, "style \"%s\" {\n"
8202 "fg[ACTIVE] = \"%s\"\n"
8203 "}\n"
8204 "widget \"*%s\" style \"%s\"\n",
8205 styles[iter].stylename,
8206 styles[iter].color,
8207 styles[iter].labelname, styles[iter].stylename);
8210 gtk_rc_parse_string(str->str);
8211 g_string_free(str, TRUE);
8212 gtk_rc_reset_styles(settings);
8216 void
8217 pidgin_conversations_uninit(void)
8219 purple_prefs_disconnect_by_handle(pidgin_conversations_get_handle());
8220 purple_signals_disconnect_by_handle(pidgin_conversations_get_handle());
8221 purple_signals_unregister_by_instance(pidgin_conversations_get_handle());
8239 /* down here is where gtkconvwin.c ought to start. except they share like every freaking function,
8240 * and touch each others' private members all day long */
8243 * @file gtkconvwin.c GTK+ Conversation Window API
8244 * @ingroup pidgin
8246 * pidgin
8248 * Pidgin is the legal property of its developers, whose names are too numerous
8249 * to list here. Please refer to the COPYRIGHT file distributed with this
8250 * source distribution.
8252 * This program is free software; you can redistribute it and/or modify
8253 * it under the terms of the GNU General Public License as published by
8254 * the Free Software Foundation; either version 2 of the License, or
8255 * (at your option) any later version.
8257 * This program is distributed in the hope that it will be useful,
8258 * but WITHOUT ANY WARRANTY; without even the implied warranty of
8259 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
8260 * GNU General Public License for more details.
8262 * You should have received a copy of the GNU General Public License
8263 * along with this program; if not, write to the Free Software
8264 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
8267 #include "internal.h"
8268 #include "pidgin.h"
8271 #include <gdk/gdkkeysyms.h>
8273 #include "account.h"
8274 #include "cmds.h"
8275 #include "debug.h"
8276 #include "imgstore.h"
8277 #include "log.h"
8278 #include "notify.h"
8279 #include "prpl.h"
8280 #include "request.h"
8281 #include "util.h"
8283 #include "gtkdnd-hints.h"
8284 #include "gtkblist.h"
8285 #include "gtkconv.h"
8286 #include "gtkdialogs.h"
8287 #include "gtkmenutray.h"
8288 #include "gtkpounce.h"
8289 #include "gtkprefs.h"
8290 #include "gtkprivacy.h"
8291 #include "gtkutils.h"
8292 #include "pidginstock.h"
8293 #include "gtkimhtml.h"
8294 #include "gtkimhtmltoolbar.h"
8296 static void
8297 do_close(GtkWidget *w, int resp, PidginWindow *win)
8299 gtk_widget_destroy(warn_close_dialog);
8300 warn_close_dialog = NULL;
8302 if (resp == GTK_RESPONSE_OK)
8303 pidgin_conv_window_destroy(win);
8306 static void
8307 build_warn_close_dialog(PidginWindow *gtkwin)
8309 GtkWidget *label, *vbox, *hbox, *img;
8311 g_return_if_fail(warn_close_dialog == NULL);
8313 warn_close_dialog = gtk_dialog_new_with_buttons(_("Confirm close"),
8314 GTK_WINDOW(gtkwin->window), GTK_DIALOG_MODAL,
8315 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
8316 GTK_STOCK_CLOSE, GTK_RESPONSE_OK, NULL);
8318 gtk_dialog_set_default_response(GTK_DIALOG(warn_close_dialog),
8319 GTK_RESPONSE_OK);
8321 gtk_container_set_border_width(GTK_CONTAINER(warn_close_dialog),
8323 gtk_window_set_resizable(GTK_WINDOW(warn_close_dialog), FALSE);
8324 gtk_dialog_set_has_separator(GTK_DIALOG(warn_close_dialog),
8325 FALSE);
8327 /* Setup the outside spacing. */
8328 vbox = GTK_DIALOG(warn_close_dialog)->vbox;
8330 gtk_box_set_spacing(GTK_BOX(vbox), 12);
8331 gtk_container_set_border_width(GTK_CONTAINER(vbox), 6);
8333 img = gtk_image_new_from_stock(PIDGIN_STOCK_DIALOG_WARNING,
8334 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_HUGE));
8335 /* Setup the inner hbox and put the dialog's icon in it. */
8336 hbox = gtk_hbox_new(FALSE, 12);
8337 gtk_container_add(GTK_CONTAINER(vbox), hbox);
8338 gtk_box_pack_start(GTK_BOX(hbox), img, FALSE, FALSE, 0);
8339 gtk_misc_set_alignment(GTK_MISC(img), 0, 0);
8341 /* Setup the right vbox. */
8342 vbox = gtk_vbox_new(FALSE, 12);
8343 gtk_container_add(GTK_CONTAINER(hbox), vbox);
8345 label = gtk_label_new(_("You have unread messages. Are you sure you want to close the window?"));
8346 gtk_widget_set_size_request(label, 350, -1);
8347 gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
8348 gtk_misc_set_alignment(GTK_MISC(label), 0, 0);
8349 gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 0);
8351 /* Connect the signals. */
8352 g_signal_connect(G_OBJECT(warn_close_dialog), "response",
8353 G_CALLBACK(do_close), gtkwin);
8357 /**************************************************************************
8358 * Callbacks
8359 **************************************************************************/
8361 static gboolean
8362 close_win_cb(GtkWidget *w, GdkEventAny *e, gpointer d)
8364 PidginWindow *win = d;
8365 GList *l;
8367 /* If there are unread messages then show a warning dialog */
8368 for (l = pidgin_conv_window_get_gtkconvs(win);
8369 l != NULL; l = l->next)
8371 PidginConversation *gtkconv = l->data;
8372 if (purple_conversation_get_type(gtkconv->active_conv) == PURPLE_CONV_TYPE_IM &&
8373 gtkconv->unseen_state >= PIDGIN_UNSEEN_TEXT)
8375 build_warn_close_dialog(win);
8376 gtk_widget_show_all(warn_close_dialog);
8378 return TRUE;
8382 pidgin_conv_window_destroy(win);
8384 return TRUE;
8387 static void
8388 conv_set_unseen(PurpleConversation *conv, PidginUnseenState state)
8390 int unseen_count = 0;
8391 PidginUnseenState unseen_state = PIDGIN_UNSEEN_NONE;
8393 if(purple_conversation_get_data(conv, "unseen-count"))
8394 unseen_count = GPOINTER_TO_INT(purple_conversation_get_data(conv, "unseen-count"));
8396 if(purple_conversation_get_data(conv, "unseen-state"))
8397 unseen_state = GPOINTER_TO_INT(purple_conversation_get_data(conv, "unseen-state"));
8399 if (state == PIDGIN_UNSEEN_NONE)
8401 unseen_count = 0;
8402 unseen_state = PIDGIN_UNSEEN_NONE;
8404 else
8406 if (state >= PIDGIN_UNSEEN_TEXT)
8407 unseen_count++;
8409 if (state > unseen_state)
8410 unseen_state = state;
8413 purple_conversation_set_data(conv, "unseen-count", GINT_TO_POINTER(unseen_count));
8414 purple_conversation_set_data(conv, "unseen-state", GINT_TO_POINTER(unseen_state));
8416 purple_conversation_update(conv, PURPLE_CONV_UPDATE_UNSEEN);
8419 static void
8420 gtkconv_set_unseen(PidginConversation *gtkconv, PidginUnseenState state)
8422 if (state == PIDGIN_UNSEEN_NONE)
8424 gtkconv->unseen_count = 0;
8425 gtkconv->unseen_state = PIDGIN_UNSEEN_NONE;
8427 else
8429 if (state >= PIDGIN_UNSEEN_TEXT)
8430 gtkconv->unseen_count++;
8432 if (state > gtkconv->unseen_state)
8433 gtkconv->unseen_state = state;
8436 purple_conversation_set_data(gtkconv->active_conv, "unseen-count", GINT_TO_POINTER(gtkconv->unseen_count));
8437 purple_conversation_set_data(gtkconv->active_conv, "unseen-state", GINT_TO_POINTER(gtkconv->unseen_state));
8439 purple_conversation_update(gtkconv->active_conv, PURPLE_CONV_UPDATE_UNSEEN);
8443 * When a conversation window is focused, we know the user
8444 * has looked at it so we know there are no longer unseen
8445 * messages.
8447 static gboolean
8448 focus_win_cb(GtkWidget *w, GdkEventFocus *e, gpointer d)
8450 PidginWindow *win = d;
8451 PidginConversation *gtkconv = pidgin_conv_window_get_active_gtkconv(win);
8453 if (gtkconv)
8454 gtkconv_set_unseen(gtkconv, PIDGIN_UNSEEN_NONE);
8456 return FALSE;
8459 static void
8460 notebook_init_grab(PidginWindow *gtkwin, GtkWidget *widget)
8462 static GdkCursor *cursor = NULL;
8464 gtkwin->in_drag = TRUE;
8466 if (gtkwin->drag_leave_signal) {
8467 g_signal_handler_disconnect(G_OBJECT(widget),
8468 gtkwin->drag_leave_signal);
8469 gtkwin->drag_leave_signal = 0;
8472 if (cursor == NULL)
8473 cursor = gdk_cursor_new(GDK_FLEUR);
8475 /* Grab the pointer */
8476 gtk_grab_add(gtkwin->notebook);
8477 #ifndef _WIN32
8478 /* Currently for win32 GTK+ (as of 2.2.1), gdk_pointer_is_grabbed will
8479 always be true after a button press. */
8480 if (!gdk_pointer_is_grabbed())
8481 #endif
8482 gdk_pointer_grab(gtkwin->notebook->window, FALSE,
8483 GDK_BUTTON1_MOTION_MASK | GDK_BUTTON_RELEASE_MASK,
8484 NULL, cursor, GDK_CURRENT_TIME);
8487 static gboolean
8488 notebook_motion_cb(GtkWidget *widget, GdkEventButton *e, PidginWindow *win)
8492 * Make sure the user moved the mouse far enough for the
8493 * drag to be initiated.
8495 if (win->in_predrag) {
8496 if (e->x_root < win->drag_min_x ||
8497 e->x_root >= win->drag_max_x ||
8498 e->y_root < win->drag_min_y ||
8499 e->y_root >= win->drag_max_y) {
8501 win->in_predrag = FALSE;
8502 notebook_init_grab(win, widget);
8505 else { /* Otherwise, draw the arrows. */
8506 PidginWindow *dest_win;
8507 GtkNotebook *dest_notebook;
8508 GtkWidget *tab;
8509 gint page_num;
8510 gboolean horiz_tabs = FALSE;
8511 gboolean to_right = FALSE;
8513 /* Get the window that the cursor is over. */
8514 dest_win = pidgin_conv_window_get_at_xy(e->x_root, e->y_root);
8516 if (dest_win == NULL) {
8517 dnd_hints_hide_all();
8519 return TRUE;
8522 dest_notebook = GTK_NOTEBOOK(dest_win->notebook);
8524 if (gtk_notebook_get_show_tabs(dest_notebook)) {
8525 page_num = pidgin_conv_get_tab_at_xy(dest_win,
8526 e->x_root, e->y_root, &to_right);
8527 to_right = to_right && (win != dest_win);
8528 tab = pidgin_conv_window_get_gtkconv_at_index(dest_win, page_num)->tabby;
8529 } else {
8530 page_num = 0;
8531 to_right = pidgin_conv_xy_to_right_infopane(dest_win, e->x_root, e->y_root);
8532 tab = pidgin_conv_window_get_gtkconv_at_index(dest_win, page_num)->infopane_hbox;
8535 if (gtk_notebook_get_tab_pos(dest_notebook) == GTK_POS_TOP ||
8536 gtk_notebook_get_tab_pos(dest_notebook) == GTK_POS_BOTTOM) {
8537 horiz_tabs = TRUE;
8540 if (gtk_notebook_get_show_tabs(dest_notebook) == FALSE && win == dest_win)
8542 /* dragging a tab from a single-tabbed window over its own window */
8543 dnd_hints_hide_all();
8544 return TRUE;
8545 } else if (horiz_tabs) {
8546 if (((gpointer)win == (gpointer)dest_win && win->drag_tab < page_num) || to_right) {
8547 dnd_hints_show_relative(HINT_ARROW_DOWN, tab, HINT_POSITION_RIGHT, HINT_POSITION_TOP);
8548 dnd_hints_show_relative(HINT_ARROW_UP, tab, HINT_POSITION_RIGHT, HINT_POSITION_BOTTOM);
8549 } else {
8550 dnd_hints_show_relative(HINT_ARROW_DOWN, tab, HINT_POSITION_LEFT, HINT_POSITION_TOP);
8551 dnd_hints_show_relative(HINT_ARROW_UP, tab, HINT_POSITION_LEFT, HINT_POSITION_BOTTOM);
8553 } else {
8554 if (((gpointer)win == (gpointer)dest_win && win->drag_tab < page_num) || to_right) {
8555 dnd_hints_show_relative(HINT_ARROW_RIGHT, tab, HINT_POSITION_LEFT, HINT_POSITION_BOTTOM);
8556 dnd_hints_show_relative(HINT_ARROW_LEFT, tab, HINT_POSITION_RIGHT, HINT_POSITION_BOTTOM);
8557 } else {
8558 dnd_hints_show_relative(HINT_ARROW_RIGHT, tab, HINT_POSITION_LEFT, HINT_POSITION_TOP);
8559 dnd_hints_show_relative(HINT_ARROW_LEFT, tab, HINT_POSITION_RIGHT, HINT_POSITION_TOP);
8564 return TRUE;
8567 static gboolean
8568 notebook_leave_cb(GtkWidget *widget, GdkEventCrossing *e, PidginWindow *win)
8570 if (win->in_drag)
8571 return FALSE;
8573 if (e->x_root < win->drag_min_x ||
8574 e->x_root >= win->drag_max_x ||
8575 e->y_root < win->drag_min_y ||
8576 e->y_root >= win->drag_max_y) {
8578 win->in_predrag = FALSE;
8579 notebook_init_grab(win, widget);
8582 return TRUE;
8586 * THANK YOU GALEON!
8589 static gboolean
8590 infopane_press_cb(GtkWidget *widget, GdkEventButton *e, PidginConversation *gtkconv)
8592 if (e->type == GDK_2BUTTON_PRESS && e->button == 1) {
8593 if (infopane_entry_activate(gtkconv))
8594 return TRUE;
8597 if (e->type != GDK_BUTTON_PRESS)
8598 return FALSE;
8600 if (e->button == 1) {
8601 int nb_x, nb_y;
8603 if (gtkconv->win->in_drag)
8604 return TRUE;
8606 gtkconv->win->in_predrag = TRUE;
8607 gtkconv->win->drag_tab = gtk_notebook_page_num(GTK_NOTEBOOK(gtkconv->win->notebook), gtkconv->tab_cont);
8609 gdk_window_get_origin(gtkconv->infopane_hbox->window, &nb_x, &nb_y);
8611 gtkconv->win->drag_min_x = gtkconv->infopane_hbox->allocation.x + nb_x;
8612 gtkconv->win->drag_min_y = gtkconv->infopane_hbox->allocation.y + nb_y;
8613 gtkconv->win->drag_max_x = gtkconv->infopane_hbox->allocation.width + gtkconv->win->drag_min_x;
8614 gtkconv->win->drag_max_y = gtkconv->infopane_hbox->allocation.height + gtkconv->win->drag_min_y;
8616 gtkconv->win->drag_motion_signal = g_signal_connect(G_OBJECT(gtkconv->win->notebook), "motion_notify_event",
8617 G_CALLBACK(notebook_motion_cb), gtkconv->win);
8618 gtkconv->win->drag_leave_signal = g_signal_connect(G_OBJECT(gtkconv->win->notebook), "leave_notify_event",
8619 G_CALLBACK(notebook_leave_cb), gtkconv->win);
8620 return FALSE;
8623 if (e->button == 3) {
8624 /* Right click was pressed. Popup the context menu. */
8625 GtkWidget *menu = gtk_menu_new(), *sub;
8626 gboolean populated = populate_menu_with_options(menu, gtkconv, TRUE);
8627 sub = gtk_menu_item_get_submenu(GTK_MENU_ITEM(gtkconv->win->menu.send_to));
8629 if (sub && GTK_WIDGET_IS_SENSITIVE(gtkconv->win->menu.send_to)) {
8630 GtkWidget *item = gtk_menu_item_new_with_mnemonic(_("S_end To"));
8631 if (populated)
8632 pidgin_separator(menu);
8633 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
8634 gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), sub);
8635 gtk_widget_show(item);
8636 gtk_widget_show_all(sub);
8637 } else if (!populated) {
8638 gtk_widget_destroy(menu);
8639 return FALSE;
8642 gtk_widget_show_all(menu);
8643 gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, e->button, e->time);
8644 return TRUE;
8646 return FALSE;
8649 static gboolean
8650 notebook_press_cb(GtkWidget *widget, GdkEventButton *e, PidginWindow *win)
8652 gint nb_x, nb_y;
8653 int tab_clicked;
8654 GtkWidget *page;
8655 GtkWidget *tab;
8657 if (e->button == 2 && e->type == GDK_BUTTON_PRESS) {
8658 PidginConversation *gtkconv;
8659 tab_clicked = pidgin_conv_get_tab_at_xy(win, e->x_root, e->y_root, NULL);
8661 if (tab_clicked == -1)
8662 return FALSE;
8664 gtkconv = pidgin_conv_window_get_gtkconv_at_index(win, tab_clicked);
8665 close_conv_cb(NULL, gtkconv);
8666 return TRUE;
8670 if (e->button != 1 || e->type != GDK_BUTTON_PRESS)
8671 return FALSE;
8674 if (win->in_drag) {
8675 purple_debug(PURPLE_DEBUG_WARNING, "gtkconv",
8676 "Already in the middle of a window drag at tab_press_cb\n");
8677 return TRUE;
8681 * Make sure a tab was actually clicked. The arrow buttons
8682 * mess things up.
8684 tab_clicked = pidgin_conv_get_tab_at_xy(win, e->x_root, e->y_root, NULL);
8686 if (tab_clicked == -1)
8687 return FALSE;
8690 * Get the relative position of the press event, with regards to
8691 * the position of the notebook.
8693 gdk_window_get_origin(win->notebook->window, &nb_x, &nb_y);
8695 /* Reset the min/max x/y */
8696 win->drag_min_x = 0;
8697 win->drag_min_y = 0;
8698 win->drag_max_x = 0;
8699 win->drag_max_y = 0;
8701 /* Find out which tab was dragged. */
8702 page = gtk_notebook_get_nth_page(GTK_NOTEBOOK(win->notebook), tab_clicked);
8703 tab = gtk_notebook_get_tab_label(GTK_NOTEBOOK(win->notebook), page);
8705 win->drag_min_x = tab->allocation.x + nb_x;
8706 win->drag_min_y = tab->allocation.y + nb_y;
8707 win->drag_max_x = tab->allocation.width + win->drag_min_x;
8708 win->drag_max_y = tab->allocation.height + win->drag_min_y;
8710 /* Make sure the click occurred in the tab. */
8711 if (e->x_root < win->drag_min_x ||
8712 e->x_root >= win->drag_max_x ||
8713 e->y_root < win->drag_min_y ||
8714 e->y_root >= win->drag_max_y) {
8716 return FALSE;
8719 win->in_predrag = TRUE;
8720 win->drag_tab = tab_clicked;
8722 /* Connect the new motion signals. */
8723 win->drag_motion_signal =
8724 g_signal_connect(G_OBJECT(widget), "motion_notify_event",
8725 G_CALLBACK(notebook_motion_cb), win);
8727 win->drag_leave_signal =
8728 g_signal_connect(G_OBJECT(widget), "leave_notify_event",
8729 G_CALLBACK(notebook_leave_cb), win);
8731 return FALSE;
8734 static gboolean
8735 notebook_release_cb(GtkWidget *widget, GdkEventButton *e, PidginWindow *win)
8737 PidginWindow *dest_win;
8738 GtkNotebook *dest_notebook;
8739 PidginConversation *active_gtkconv;
8740 PidginConversation *gtkconv;
8741 gint dest_page_num = 0;
8742 gboolean new_window = FALSE;
8743 gboolean to_right = FALSE;
8746 * Don't check to make sure that the event's window matches the
8747 * widget's, because we may be getting an event passed on from the
8748 * close button.
8750 if (e->button != 1 && e->type != GDK_BUTTON_RELEASE)
8751 return FALSE;
8753 if (gdk_pointer_is_grabbed()) {
8754 gdk_pointer_ungrab(GDK_CURRENT_TIME);
8755 gtk_grab_remove(widget);
8758 if (!win->in_predrag && !win->in_drag)
8759 return FALSE;
8761 /* Disconnect the motion signal. */
8762 if (win->drag_motion_signal) {
8763 g_signal_handler_disconnect(G_OBJECT(widget),
8764 win->drag_motion_signal);
8766 win->drag_motion_signal = 0;
8770 * If we're in a pre-drag, we'll also need to disconnect the leave
8771 * signal.
8773 if (win->in_predrag) {
8774 win->in_predrag = FALSE;
8776 if (win->drag_leave_signal) {
8777 g_signal_handler_disconnect(G_OBJECT(widget),
8778 win->drag_leave_signal);
8780 win->drag_leave_signal = 0;
8784 /* If we're not in drag... */
8785 /* We're perfectly normal people! */
8786 if (!win->in_drag)
8787 return FALSE;
8789 win->in_drag = FALSE;
8791 dnd_hints_hide_all();
8793 dest_win = pidgin_conv_window_get_at_xy(e->x_root, e->y_root);
8795 active_gtkconv = pidgin_conv_window_get_active_gtkconv(win);
8797 if (dest_win == NULL) {
8798 /* If the current window doesn't have any other conversations,
8799 * there isn't much point transferring the conv to a new window. */
8800 if (pidgin_conv_window_get_gtkconv_count(win) > 1) {
8801 /* Make a new window to stick this to. */
8802 dest_win = pidgin_conv_window_new();
8803 new_window = TRUE;
8807 if (dest_win == NULL)
8808 return FALSE;
8810 purple_signal_emit(pidgin_conversations_get_handle(),
8811 "conversation-dragging", win, dest_win);
8813 /* Get the destination page number. */
8814 if (!new_window) {
8815 dest_notebook = GTK_NOTEBOOK(dest_win->notebook);
8816 if (gtk_notebook_get_show_tabs(dest_notebook)) {
8817 dest_page_num = pidgin_conv_get_tab_at_xy(dest_win,
8818 e->x_root, e->y_root, &to_right);
8819 } else {
8820 dest_page_num = 0;
8821 to_right = pidgin_conv_xy_to_right_infopane(dest_win, e->x_root, e->y_root);
8825 gtkconv = pidgin_conv_window_get_gtkconv_at_index(win, win->drag_tab);
8827 if (win == dest_win) {
8828 gtk_notebook_reorder_child(GTK_NOTEBOOK(win->notebook), gtkconv->tab_cont, dest_page_num);
8829 } else {
8830 pidgin_conv_window_remove_gtkconv(win, gtkconv);
8831 pidgin_conv_window_add_gtkconv(dest_win, gtkconv);
8832 gtk_notebook_reorder_child(GTK_NOTEBOOK(dest_win->notebook), gtkconv->tab_cont, dest_page_num + to_right);
8833 pidgin_conv_window_switch_gtkconv(dest_win, gtkconv);
8834 if (new_window) {
8835 gint win_width, win_height;
8837 gtk_window_get_size(GTK_WINDOW(dest_win->window),
8838 &win_width, &win_height);
8839 #ifdef _WIN32 /* only override window manager placement on Windows */
8840 gtk_window_move(GTK_WINDOW(dest_win->window),
8841 e->x_root - (win_width / 2),
8842 e->y_root - (win_height / 2));
8843 #endif
8845 pidgin_conv_window_show(dest_win);
8849 gtk_widget_grab_focus(active_gtkconv->entry);
8851 return TRUE;
8855 static void
8856 before_switch_conv_cb(GtkNotebook *notebook, GtkWidget *page, gint page_num,
8857 gpointer user_data)
8859 PidginWindow *win;
8860 PurpleConversation *conv;
8861 PidginConversation *gtkconv;
8863 win = user_data;
8864 conv = pidgin_conv_window_get_active_conversation(win);
8866 g_return_if_fail(conv != NULL);
8868 if (purple_conversation_get_type(conv) != PURPLE_CONV_TYPE_IM)
8869 return;
8871 gtkconv = PIDGIN_CONVERSATION(conv);
8873 if (gtkconv->u.im->typing_timer != 0) {
8874 g_source_remove(gtkconv->u.im->typing_timer);
8875 gtkconv->u.im->typing_timer = 0;
8878 stop_anim(NULL, gtkconv);
8880 static void
8881 close_window(GtkWidget *w, PidginWindow *win)
8883 close_win_cb(w, NULL, win);
8886 static void
8887 detach_tab_cb(GtkWidget *w, GObject *menu)
8889 PidginWindow *win, *new_window;
8890 PidginConversation *gtkconv;
8892 gtkconv = g_object_get_data(menu, "clicked_tab");
8894 if (!gtkconv)
8895 return;
8897 win = pidgin_conv_get_window(gtkconv);
8898 /* Nothing to do if there's only one tab in the window */
8899 if (pidgin_conv_window_get_gtkconv_count(win) == 1)
8900 return;
8902 pidgin_conv_window_remove_gtkconv(win, gtkconv);
8904 new_window = pidgin_conv_window_new();
8905 pidgin_conv_window_add_gtkconv(new_window, gtkconv);
8906 pidgin_conv_window_show(new_window);
8909 static void
8910 close_others_cb(GtkWidget *w, GObject *menu)
8912 GList *iter;
8913 PidginConversation *gtkconv;
8914 PidginWindow *win;
8916 gtkconv = g_object_get_data(menu, "clicked_tab");
8918 if (!gtkconv)
8919 return;
8921 win = pidgin_conv_get_window(gtkconv);
8923 for (iter = pidgin_conv_window_get_gtkconvs(win); iter; )
8925 PidginConversation *gconv = iter->data;
8926 iter = iter->next;
8928 if (gconv != gtkconv)
8930 close_conv_cb(NULL, gconv);
8935 static void close_tab_cb(GtkWidget *w, GObject *menu)
8937 PidginConversation *gtkconv;
8939 gtkconv = g_object_get_data(menu, "clicked_tab");
8941 if (gtkconv)
8942 close_conv_cb(NULL, gtkconv);
8945 static gboolean
8946 right_click_menu_cb(GtkNotebook *notebook, GdkEventButton *event, PidginWindow *win)
8948 GtkWidget *item, *menu;
8949 PidginConversation *gtkconv;
8951 if (event->type != GDK_BUTTON_PRESS || event->button != 3)
8952 return FALSE;
8954 gtkconv = pidgin_conv_window_get_gtkconv_at_index(win,
8955 pidgin_conv_get_tab_at_xy(win, event->x_root, event->y_root, NULL));
8957 if (g_object_get_data(G_OBJECT(notebook->menu), "clicked_tab"))
8959 g_object_set_data(G_OBJECT(notebook->menu), "clicked_tab", gtkconv);
8960 return FALSE;
8963 g_object_set_data(G_OBJECT(notebook->menu), "clicked_tab", gtkconv);
8965 menu = notebook->menu;
8966 pidgin_separator(GTK_WIDGET(menu));
8968 item = gtk_menu_item_new_with_label(_("Close other tabs"));
8969 gtk_widget_show(item);
8970 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
8971 g_signal_connect(G_OBJECT(item), "activate",
8972 G_CALLBACK(close_others_cb), menu);
8974 item = gtk_menu_item_new_with_label(_("Close all tabs"));
8975 gtk_widget_show(item);
8976 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
8977 g_signal_connect(G_OBJECT(item), "activate",
8978 G_CALLBACK(close_window), win);
8980 pidgin_separator(menu);
8982 item = gtk_menu_item_new_with_label(_("Detach this tab"));
8983 gtk_widget_show(item);
8984 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
8985 g_signal_connect(G_OBJECT(item), "activate",
8986 G_CALLBACK(detach_tab_cb), menu);
8988 item = gtk_menu_item_new_with_label(_("Close this tab"));
8989 gtk_widget_show(item);
8990 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
8991 g_signal_connect(G_OBJECT(item), "activate",
8992 G_CALLBACK(close_tab_cb), menu);
8994 return FALSE;
8997 static void
8998 remove_edit_entry(PidginConversation *gtkconv, GtkWidget *entry)
9000 g_signal_handlers_disconnect_matched(G_OBJECT(entry), G_SIGNAL_MATCH_DATA,
9001 0, 0, NULL, NULL, gtkconv);
9002 gtk_widget_show(gtkconv->infopane);
9003 gtk_widget_grab_focus(gtkconv->entry);
9004 gtk_widget_destroy(entry);
9007 static gboolean
9008 alias_focus_cb(GtkWidget *widget, GdkEventFocus *event, gpointer user_data)
9010 remove_edit_entry(user_data, widget);
9011 return FALSE;
9014 static gboolean
9015 alias_key_press_cb(GtkWidget *widget, GdkEventKey *event, gpointer user_data)
9017 if (event->keyval == GDK_Escape) {
9018 remove_edit_entry(user_data, widget);
9019 return TRUE;
9021 return FALSE;
9024 static void
9025 alias_cb(GtkEntry *entry, gpointer user_data)
9027 PidginConversation *gtkconv;
9028 PurpleConversation *conv;
9029 PurpleAccount *account;
9030 const char *name;
9032 gtkconv = (PidginConversation *)user_data;
9033 if (gtkconv == NULL) {
9034 return;
9036 conv = gtkconv->active_conv;
9037 account = purple_conversation_get_account(conv);
9038 name = purple_conversation_get_name(conv);
9040 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) {
9041 PurpleBuddy *buddy;
9042 buddy = purple_find_buddy(account, name);
9043 if (buddy != NULL) {
9044 purple_blist_alias_buddy(buddy,
9045 gtk_entry_get_text(entry));
9047 serv_alias_buddy(buddy);
9048 } else if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) {
9049 gtk_entry_set_text(GTK_ENTRY(gtkconv->u.chat->topic_text), gtk_entry_get_text(entry));
9050 topic_callback(NULL, gtkconv);
9052 remove_edit_entry(user_data, GTK_WIDGET(entry));
9055 static gboolean
9056 infopane_entry_activate(PidginConversation *gtkconv)
9058 GtkWidget *entry = NULL;
9059 PurpleConversation *conv = gtkconv->active_conv;
9060 const char *text = NULL;
9062 if (!GTK_WIDGET_VISIBLE(gtkconv->infopane)) {
9063 /* There's already an entry for alias. Let's not create another one. */
9064 return FALSE;
9067 if (!purple_account_is_connected(gtkconv->active_conv->account)) {
9068 /* Do not allow aliasing someone on a disconnected account. */
9069 return FALSE;
9072 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) {
9073 PurpleBuddy *buddy = purple_find_buddy(gtkconv->active_conv->account, gtkconv->active_conv->name);
9074 if (!buddy)
9075 /* This buddy isn't in your buddy list, so we can't alias him */
9076 return FALSE;
9078 text = purple_buddy_get_contact_alias(buddy);
9079 } else if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) {
9080 PurpleConnection *gc;
9081 PurplePluginProtocolInfo *prpl_info = NULL;
9083 gc = purple_conversation_get_gc(conv);
9084 if (gc != NULL)
9085 prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(gc->prpl);
9086 if (prpl_info && prpl_info->set_chat_topic == NULL)
9087 /* This protocol doesn't support setting the chat room topic */
9088 return FALSE;
9090 text = purple_conv_chat_get_topic(PURPLE_CONV_CHAT(conv));
9093 /* alias label */
9094 entry = gtk_entry_new();
9095 gtk_entry_set_has_frame(GTK_ENTRY(entry), FALSE);
9096 gtk_entry_set_width_chars(GTK_ENTRY(entry), 10);
9097 gtk_entry_set_alignment(GTK_ENTRY(entry), 0.5);
9099 gtk_box_pack_start(GTK_BOX(gtkconv->infopane_hbox), entry, TRUE, TRUE, 0);
9100 /* after the tab label */
9101 gtk_box_reorder_child(GTK_BOX(gtkconv->infopane_hbox), entry, 0);
9103 g_signal_connect(G_OBJECT(entry), "activate", G_CALLBACK(alias_cb), gtkconv);
9104 g_signal_connect(G_OBJECT(entry), "focus-out-event", G_CALLBACK(alias_focus_cb), gtkconv);
9105 g_signal_connect(G_OBJECT(entry), "key-press-event", G_CALLBACK(alias_key_press_cb), gtkconv);
9107 if (text != NULL)
9108 gtk_entry_set_text(GTK_ENTRY(entry), text);
9109 gtk_widget_show(entry);
9110 gtk_widget_hide(gtkconv->infopane);
9111 gtk_widget_grab_focus(entry);
9113 return TRUE;
9116 static gboolean
9117 window_keypress_cb(GtkWidget *widget, GdkEventKey *event, PidginWindow *win)
9119 PidginConversation *gtkconv = pidgin_conv_window_get_active_gtkconv(win);
9121 return conv_keypress_common(gtkconv, event);
9124 static void
9125 switch_conv_cb(GtkNotebook *notebook, GtkWidget *page, gint page_num,
9126 gpointer user_data)
9128 PidginWindow *win;
9129 PurpleConversation *conv;
9130 PidginConversation *gtkconv;
9131 const char *sound_method;
9133 win = user_data;
9134 gtkconv = pidgin_conv_window_get_gtkconv_at_index(win, page_num);
9135 conv = gtkconv->active_conv;
9137 g_return_if_fail(conv != NULL);
9139 /* clear unseen flag if conversation is not hidden */
9140 if(!pidgin_conv_is_hidden(gtkconv)) {
9141 gtkconv_set_unseen(gtkconv, PIDGIN_UNSEEN_NONE);
9144 /* Update the menubar */
9146 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(gtkconv->win->menu.logging),
9147 purple_conversation_is_logging(conv));
9149 generate_send_to_items(win);
9150 regenerate_options_items(win);
9151 regenerate_plugins_items(win);
9153 pidgin_conv_switch_active_conversation(conv);
9155 sound_method = purple_prefs_get_string(PIDGIN_PREFS_ROOT "/sound/method");
9156 if (!purple_strequal(sound_method, "none"))
9157 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(win->menu.sounds),
9158 gtkconv->make_sound);
9160 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(win->menu.show_formatting_toolbar),
9161 purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/show_formatting_toolbar"));
9163 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(win->menu.show_timestamps),
9164 purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/show_timestamps"));
9167 * We pause icons when they are not visible. If this icon should
9168 * be animated then start it back up again.
9170 if ((purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM) &&
9171 (gtkconv->u.im->animate))
9172 start_anim(NULL, gtkconv);
9174 purple_signal_emit(pidgin_conversations_get_handle(), "conversation-switched", conv);
9177 /**************************************************************************
9178 * GTK+ window ops
9179 **************************************************************************/
9181 GList *
9182 pidgin_conv_windows_get_list()
9184 return window_list;
9187 static GList*
9188 make_status_icon_list(const char *stock, GtkWidget *w)
9190 GList *l = NULL;
9191 l = g_list_append(l, gtk_widget_render_icon (w, stock,
9192 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL), "GtkWindow"));
9193 l = g_list_append(l, gtk_widget_render_icon (w, stock,
9194 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_SMALL), "GtkWindow"));
9195 l = g_list_append(l, gtk_widget_render_icon (w, stock,
9196 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_MEDIUM), "GtkWindow"));
9197 l = g_list_append(l, gtk_widget_render_icon (w, stock,
9198 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_LARGE), "GtkWindow"));
9199 return l;
9202 static void
9203 create_icon_lists(GtkWidget *w)
9205 available_list = make_status_icon_list(PIDGIN_STOCK_STATUS_AVAILABLE, w);
9206 busy_list = make_status_icon_list(PIDGIN_STOCK_STATUS_BUSY, w);
9207 xa_list = make_status_icon_list(PIDGIN_STOCK_STATUS_XA, w);
9208 offline_list = make_status_icon_list(PIDGIN_STOCK_STATUS_OFFLINE, w);
9209 away_list = make_status_icon_list(PIDGIN_STOCK_STATUS_AWAY, w);
9210 prpl_lists = g_hash_table_new(g_str_hash, g_str_equal);
9213 static void
9214 plugin_changed_cb(PurplePlugin *p, gpointer data)
9216 regenerate_plugins_items(data);
9219 static gboolean gtk_conv_configure_cb(GtkWidget *w, GdkEventConfigure *event, gpointer data) {
9220 int x, y;
9222 if (GTK_WIDGET_VISIBLE(w))
9223 gtk_window_get_position(GTK_WINDOW(w), &x, &y);
9224 else
9225 return FALSE; /* carry on normally */
9227 /* Workaround for GTK+ bug # 169811 - "configure_event" is fired
9228 * when the window is being maximized */
9229 if (gdk_window_get_state(w->window) & GDK_WINDOW_STATE_MAXIMIZED)
9230 return FALSE;
9232 /* don't save off-screen positioning */
9233 if (x + event->width < 0 ||
9234 y + event->height < 0 ||
9235 x > gdk_screen_width() ||
9236 y > gdk_screen_height())
9237 return FALSE; /* carry on normally */
9239 /* store the position */
9240 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/x", x);
9241 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/y", y);
9242 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/width", event->width);
9243 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/height", event->height);
9245 /* continue to handle event normally */
9246 return FALSE;
9250 static void
9251 pidgin_conv_set_position_size(PidginWindow *win, int conv_x, int conv_y,
9252 int conv_width, int conv_height)
9254 /* if the window exists, is hidden, we're saving positions, and the
9255 * position is sane... */
9256 if (win && win->window &&
9257 !GTK_WIDGET_VISIBLE(win->window) && conv_width != 0) {
9259 #ifdef _WIN32 /* only override window manager placement on Windows */
9260 /* ...check position is on screen... */
9261 if (conv_x >= gdk_screen_width())
9262 conv_x = gdk_screen_width() - 100;
9263 else if (conv_x + conv_width < 0)
9264 conv_x = 100;
9266 if (conv_y >= gdk_screen_height())
9267 conv_y = gdk_screen_height() - 100;
9268 else if (conv_y + conv_height < 0)
9269 conv_y = 100;
9271 /* ...and move it back. */
9272 gtk_window_move(GTK_WINDOW(win->window), conv_x, conv_y);
9273 #endif
9274 gtk_window_resize(GTK_WINDOW(win->window), conv_width, conv_height);
9278 static void
9279 pidgin_conv_restore_position(PidginWindow *win) {
9280 pidgin_conv_set_position_size(win,
9281 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/x"),
9282 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/y"),
9283 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/width"),
9284 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/height"));
9287 PidginWindow *
9288 pidgin_conv_window_new()
9290 PidginWindow *win;
9291 GtkPositionType pos;
9292 GtkWidget *testidea;
9293 GtkWidget *menubar;
9294 GdkModifierType state;
9296 win = g_malloc0(sizeof(PidginWindow));
9298 window_list = g_list_append(window_list, win);
9300 /* Create the window. */
9301 win->window = pidgin_create_window(NULL, 0, "conversation", TRUE);
9302 if (!gtk_get_current_event_state(&state))
9303 gtk_window_set_focus_on_map(GTK_WINDOW(win->window), FALSE);
9305 /* Etan: I really think this entire function call should happen only
9306 * when we are on Windows but I was informed that back before we used
9307 * to save the window position we stored the window size, so I'm
9308 * leaving it for now. */
9309 #if TRUE || defined(_WIN32)
9310 pidgin_conv_restore_position(win);
9311 #endif
9313 if (available_list == NULL) {
9314 create_icon_lists(win->window);
9317 g_signal_connect(G_OBJECT(win->window), "delete_event",
9318 G_CALLBACK(close_win_cb), win);
9319 g_signal_connect(G_OBJECT(win->window), "focus_in_event",
9320 G_CALLBACK(focus_win_cb), win);
9322 /* Intercept keystrokes from the menu items */
9323 g_signal_connect(G_OBJECT(win->window), "key_press_event",
9324 G_CALLBACK(window_keypress_cb), win);
9327 /* Create the notebook. */
9328 win->notebook = gtk_notebook_new();
9330 pos = purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side");
9332 #if 0
9333 gtk_notebook_set_tab_hborder(GTK_NOTEBOOK(win->notebook), 0);
9334 gtk_notebook_set_tab_vborder(GTK_NOTEBOOK(win->notebook), 0);
9335 #endif
9336 gtk_notebook_set_tab_pos(GTK_NOTEBOOK(win->notebook), pos);
9337 gtk_notebook_set_scrollable(GTK_NOTEBOOK(win->notebook), TRUE);
9338 gtk_notebook_popup_enable(GTK_NOTEBOOK(win->notebook));
9339 gtk_notebook_set_show_tabs(GTK_NOTEBOOK(win->notebook), FALSE);
9340 gtk_notebook_set_show_border(GTK_NOTEBOOK(win->notebook), TRUE);
9342 g_signal_connect(G_OBJECT(win->notebook), "button-press-event",
9343 G_CALLBACK(right_click_menu_cb), win);
9345 gtk_widget_show(win->notebook);
9347 g_signal_connect(G_OBJECT(win->notebook), "switch_page",
9348 G_CALLBACK(before_switch_conv_cb), win);
9349 g_signal_connect_after(G_OBJECT(win->notebook), "switch_page",
9350 G_CALLBACK(switch_conv_cb), win);
9352 /* Setup the tab drag and drop signals. */
9353 gtk_widget_add_events(win->notebook,
9354 GDK_BUTTON1_MOTION_MASK | GDK_LEAVE_NOTIFY_MASK);
9355 g_signal_connect(G_OBJECT(win->notebook), "button_press_event",
9356 G_CALLBACK(notebook_press_cb), win);
9357 g_signal_connect(G_OBJECT(win->notebook), "button_release_event",
9358 G_CALLBACK(notebook_release_cb), win);
9360 testidea = gtk_vbox_new(FALSE, 0);
9362 /* Setup the menubar. */
9363 menubar = setup_menubar(win);
9364 gtk_box_pack_start(GTK_BOX(testidea), menubar, FALSE, TRUE, 0);
9366 gtk_box_pack_start(GTK_BOX(testidea), win->notebook, TRUE, TRUE, 0);
9368 gtk_container_add(GTK_CONTAINER(win->window), testidea);
9370 gtk_widget_show(testidea);
9372 /* Update the plugin actions when plugins are (un)loaded */
9373 purple_signal_connect(purple_plugins_get_handle(), "plugin-load",
9374 win, PURPLE_CALLBACK(plugin_changed_cb), win);
9375 purple_signal_connect(purple_plugins_get_handle(), "plugin-unload",
9376 win, PURPLE_CALLBACK(plugin_changed_cb), win);
9379 #ifdef _WIN32
9380 g_signal_connect(G_OBJECT(win->window), "show",
9381 G_CALLBACK(winpidgin_ensure_onscreen), win->window);
9383 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/win32/minimize_new_convs")
9384 && !gtk_get_current_event_state(&state))
9385 gtk_window_iconify(GTK_WINDOW(win->window));
9386 #endif
9388 return win;
9391 void
9392 pidgin_conv_window_destroy(PidginWindow *win)
9394 if (win->gtkconvs) {
9395 GList *iter = win->gtkconvs;
9396 while (iter)
9398 PidginConversation *gtkconv = iter->data;
9399 iter = iter->next;
9400 close_conv_cb(NULL, gtkconv);
9402 return;
9405 purple_prefs_disconnect_by_handle(win);
9406 window_list = g_list_remove(window_list, win);
9408 /* Close the "Find" dialog if it's open */
9409 if (win->dialogs.search)
9410 gtk_widget_destroy(win->dialogs.search);
9412 gtk_widget_destroy(win->window);
9414 g_object_unref(G_OBJECT(win->menu.item_factory));
9416 purple_notify_close_with_handle(win);
9417 purple_signals_disconnect_by_handle(win);
9419 g_free(win);
9422 void
9423 pidgin_conv_window_show(PidginWindow *win)
9425 gtk_widget_show(win->window);
9428 void
9429 pidgin_conv_window_hide(PidginWindow *win)
9431 gtk_widget_hide(win->window);
9434 void
9435 pidgin_conv_window_raise(PidginWindow *win)
9437 gdk_window_raise(GDK_WINDOW(win->window->window));
9440 void
9441 pidgin_conv_window_switch_gtkconv(PidginWindow *win, PidginConversation *gtkconv)
9443 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook),
9444 gtk_notebook_page_num(GTK_NOTEBOOK(win->notebook),
9445 gtkconv->tab_cont));
9448 static gboolean
9449 gtkconv_tab_set_tip(GtkWidget *widget, GdkEventCrossing *event, PidginConversation *gtkconv)
9451 #if GTK_CHECK_VERSION(2, 12, 0)
9452 #define gtk_tooltips_set_tip(tips, w, l, p) gtk_widget_set_tooltip_text(w, l)
9453 #endif
9454 /* PANGO_VERSION_CHECK macro was introduced in 1.15. So we need this double check. */
9455 #ifndef PANGO_VERSION_CHECK
9456 #define pango_layout_is_ellipsized(l) TRUE
9457 #elif !PANGO_VERSION_CHECK(1,16,0)
9458 #define pango_layout_is_ellipsized(l) TRUE
9459 #endif
9460 PangoLayout *layout;
9462 layout = gtk_label_get_layout(GTK_LABEL(gtkconv->tab_label));
9463 gtk_tooltips_set_tip(gtkconv->tooltips, widget,
9464 pango_layout_is_ellipsized(layout) ? gtk_label_get_text(GTK_LABEL(gtkconv->tab_label)) : NULL,
9465 NULL);
9466 return FALSE;
9467 #if GTK_CHECK_VERSION(2, 12, 0)
9468 #undef gtk_tooltips_set_tip
9469 #endif
9472 void
9473 pidgin_conv_window_add_gtkconv(PidginWindow *win, PidginConversation *gtkconv)
9475 PurpleConversation *conv = gtkconv->active_conv;
9476 PidginConversation *focus_gtkconv;
9477 GtkWidget *tab_cont = gtkconv->tab_cont;
9478 PurpleConversationType conv_type;
9479 const gchar *tmp_lab;
9481 conv_type = purple_conversation_get_type(conv);
9483 win->gtkconvs = g_list_append(win->gtkconvs, gtkconv);
9484 gtkconv->win = win;
9486 if (win->gtkconvs && win->gtkconvs->next && win->gtkconvs->next->next == NULL)
9487 pidgin_conv_tab_pack(win, ((PidginConversation*)win->gtkconvs->data));
9490 /* Close button. */
9491 gtkconv->close = pidgin_create_small_button(gtk_label_new("×"));
9492 gtk_tooltips_set_tip(gtkconv->tooltips, gtkconv->close,
9493 _("Close conversation"), NULL);
9495 g_signal_connect(gtkconv->close, "clicked", G_CALLBACK (close_conv_cb), gtkconv);
9497 /* Status icon. */
9498 gtkconv->icon = gtk_image_new();
9499 gtkconv->menu_icon = gtk_image_new();
9500 g_object_set(G_OBJECT(gtkconv->icon),
9501 "icon-size", gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_MICROSCOPIC),
9502 NULL);
9503 g_object_set(G_OBJECT(gtkconv->menu_icon),
9504 "icon-size", gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_MICROSCOPIC),
9505 NULL);
9506 gtk_widget_show(gtkconv->icon);
9507 update_tab_icon(conv);
9509 /* Tab label. */
9510 gtkconv->tab_label = gtk_label_new(tmp_lab = purple_conversation_get_title(conv));
9511 gtk_widget_set_name(gtkconv->tab_label, "tab-label");
9513 gtkconv->menu_tabby = gtk_hbox_new(FALSE, PIDGIN_HIG_BOX_SPACE);
9514 gtkconv->menu_label = gtk_label_new(tmp_lab);
9515 gtk_box_pack_start(GTK_BOX(gtkconv->menu_tabby), gtkconv->menu_icon, FALSE, FALSE, 0);
9517 gtk_widget_show_all(gtkconv->menu_icon);
9519 gtk_box_pack_start(GTK_BOX(gtkconv->menu_tabby), gtkconv->menu_label, TRUE, TRUE, 0);
9520 gtk_widget_show(gtkconv->menu_label);
9521 gtk_misc_set_alignment(GTK_MISC(gtkconv->menu_label), 0, 0);
9523 gtk_widget_show(gtkconv->menu_tabby);
9525 if (conv_type == PURPLE_CONV_TYPE_IM)
9526 pidgin_conv_update_buddy_icon(conv);
9528 /* Build and set conversations tab */
9529 pidgin_conv_tab_pack(win, gtkconv);
9531 gtk_notebook_set_menu_label(GTK_NOTEBOOK(win->notebook), tab_cont, gtkconv->menu_tabby);
9533 gtk_widget_show(tab_cont);
9535 if (pidgin_conv_window_get_gtkconv_count(win) == 1) {
9536 /* Er, bug in notebooks? Switch to the page manually. */
9537 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), 0);
9538 } else {
9539 gtk_notebook_set_show_tabs(GTK_NOTEBOOK(win->notebook), TRUE);
9542 focus_gtkconv = g_list_nth_data(pidgin_conv_window_get_gtkconvs(win),
9543 gtk_notebook_get_current_page(GTK_NOTEBOOK(win->notebook)));
9544 gtk_widget_grab_focus(focus_gtkconv->entry);
9546 if (pidgin_conv_window_get_gtkconv_count(win) == 1)
9547 update_send_to_selection(win);
9550 static void
9551 pidgin_conv_tab_pack(PidginWindow *win, PidginConversation *gtkconv)
9553 gboolean tabs_side = FALSE;
9554 gint angle = 0;
9555 GtkWidget *first, *third, *ebox;
9557 if (purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") == GTK_POS_LEFT ||
9558 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") == GTK_POS_RIGHT)
9559 tabs_side = TRUE;
9560 else if (purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") == (GTK_POS_LEFT|8))
9561 angle = 90;
9562 else if (purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") == (GTK_POS_RIGHT|8))
9563 angle = 270;
9565 if (!angle) {
9566 g_object_set(G_OBJECT(gtkconv->tab_label), "ellipsize", PANGO_ELLIPSIZE_END, NULL);
9567 gtk_label_set_width_chars(GTK_LABEL(gtkconv->tab_label), 4);
9568 } else {
9569 g_object_set(G_OBJECT(gtkconv->tab_label), "ellipsize", PANGO_ELLIPSIZE_NONE, NULL);
9570 gtk_label_set_width_chars(GTK_LABEL(gtkconv->tab_label), -1);
9573 if (tabs_side) {
9574 gtk_label_set_width_chars(
9575 GTK_LABEL(gtkconv->tab_label),
9576 MIN(g_utf8_strlen(gtk_label_get_text(GTK_LABEL(gtkconv->tab_label)), -1), 12)
9580 gtk_label_set_angle(GTK_LABEL(gtkconv->tab_label), angle);
9582 #if 0
9583 gtk_misc_set_alignment(GTK_MISC(gtkconv->tab_label), 0.00, 0.5);
9584 gtk_misc_set_padding(GTK_MISC(gtkconv->tab_label), 4, 0);
9585 #endif
9587 if (angle)
9588 gtkconv->tabby = gtk_vbox_new(FALSE, PIDGIN_HIG_BOX_SPACE);
9589 else
9590 gtkconv->tabby = gtk_hbox_new(FALSE, PIDGIN_HIG_BOX_SPACE);
9591 gtk_widget_set_name(gtkconv->tabby, "tab-container");
9593 /* select the correct ordering for verticle tabs */
9594 if (angle == 90) {
9595 first = gtkconv->close;
9596 third = gtkconv->icon;
9597 } else {
9598 first = gtkconv->icon;
9599 third = gtkconv->close;
9602 ebox = gtk_event_box_new();
9603 gtk_event_box_set_visible_window(GTK_EVENT_BOX(ebox), FALSE);
9604 gtk_container_add(GTK_CONTAINER(ebox), gtkconv->tabby);
9605 g_signal_connect(G_OBJECT(ebox), "enter-notify-event",
9606 G_CALLBACK(gtkconv_tab_set_tip), gtkconv);
9608 if (gtkconv->tab_label->parent == NULL) {
9609 /* Pack if it's a new widget */
9610 gtk_box_pack_start(GTK_BOX(gtkconv->tabby), first, FALSE, FALSE, 0);
9611 gtk_box_pack_start(GTK_BOX(gtkconv->tabby), gtkconv->tab_label, TRUE, TRUE, 0);
9612 gtk_box_pack_start(GTK_BOX(gtkconv->tabby), third, FALSE, FALSE, 0);
9614 /* Add this pane to the conversation's notebook. */
9615 gtk_notebook_append_page(GTK_NOTEBOOK(win->notebook), gtkconv->tab_cont, ebox);
9616 } else {
9617 /* reparent old widgets on preference changes */
9618 gtk_widget_reparent(first, gtkconv->tabby);
9619 gtk_widget_reparent(gtkconv->tab_label, gtkconv->tabby);
9620 gtk_widget_reparent(third, gtkconv->tabby);
9621 gtk_box_set_child_packing(GTK_BOX(gtkconv->tabby), first, FALSE, FALSE, 0, GTK_PACK_START);
9622 gtk_box_set_child_packing(GTK_BOX(gtkconv->tabby), gtkconv->tab_label, TRUE, TRUE, 0, GTK_PACK_START);
9623 gtk_box_set_child_packing(GTK_BOX(gtkconv->tabby), third, FALSE, FALSE, 0, GTK_PACK_START);
9625 /* Reset the tabs label to the new version */
9626 gtk_notebook_set_tab_label(GTK_NOTEBOOK(win->notebook), gtkconv->tab_cont, ebox);
9629 gtk_notebook_set_tab_label_packing(GTK_NOTEBOOK(win->notebook), gtkconv->tab_cont,
9630 !tabs_side && !angle,
9631 TRUE, GTK_PACK_START);
9633 if (pidgin_conv_window_get_gtkconv_count(win) == 1)
9634 gtk_notebook_set_show_tabs(GTK_NOTEBOOK(win->notebook),
9635 purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/tabs") &&
9636 (!purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons") ||
9637 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") != GTK_POS_TOP));
9639 /* show the widgets */
9640 /* gtk_widget_show(gtkconv->icon); */
9641 gtk_widget_show(gtkconv->tab_label);
9642 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/close_on_tabs"))
9643 gtk_widget_show(gtkconv->close);
9644 gtk_widget_show(gtkconv->tabby);
9645 gtk_widget_show(ebox);
9648 void
9649 pidgin_conv_window_remove_gtkconv(PidginWindow *win, PidginConversation *gtkconv)
9651 unsigned int index;
9653 index = gtk_notebook_page_num(GTK_NOTEBOOK(win->notebook), gtkconv->tab_cont);
9655 g_object_ref(gtkconv->tab_cont);
9656 gtk_object_sink(GTK_OBJECT(gtkconv->tab_cont));
9658 gtk_notebook_remove_page(GTK_NOTEBOOK(win->notebook), index);
9660 win->gtkconvs = g_list_remove(win->gtkconvs, gtkconv);
9662 g_signal_handlers_disconnect_matched(win->window, G_SIGNAL_MATCH_DATA,
9663 0, 0, NULL, NULL, gtkconv);
9665 if (win->gtkconvs && win->gtkconvs->next == NULL)
9666 pidgin_conv_tab_pack(win, win->gtkconvs->data);
9668 if (!win->gtkconvs && win != hidden_convwin)
9669 pidgin_conv_window_destroy(win);
9672 PidginConversation *
9673 pidgin_conv_window_get_gtkconv_at_index(const PidginWindow *win, int index)
9675 GtkWidget *tab_cont;
9677 if (index == -1)
9678 index = 0;
9679 tab_cont = gtk_notebook_get_nth_page(GTK_NOTEBOOK(win->notebook), index);
9680 return tab_cont ? g_object_get_data(G_OBJECT(tab_cont), "PidginConversation") : NULL;
9683 PidginConversation *
9684 pidgin_conv_window_get_active_gtkconv(const PidginWindow *win)
9686 int index;
9687 GtkWidget *tab_cont;
9689 index = gtk_notebook_get_current_page(GTK_NOTEBOOK(win->notebook));
9690 if (index == -1)
9691 index = 0;
9692 tab_cont = gtk_notebook_get_nth_page(GTK_NOTEBOOK(win->notebook), index);
9693 if (!tab_cont)
9694 return NULL;
9695 return g_object_get_data(G_OBJECT(tab_cont), "PidginConversation");
9699 PurpleConversation *
9700 pidgin_conv_window_get_active_conversation(const PidginWindow *win)
9702 PidginConversation *gtkconv;
9704 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
9705 return gtkconv ? gtkconv->active_conv : NULL;
9708 gboolean
9709 pidgin_conv_window_is_active_conversation(const PurpleConversation *conv)
9711 return conv == pidgin_conv_window_get_active_conversation(PIDGIN_CONVERSATION(conv)->win);
9714 gboolean
9715 pidgin_conv_window_has_focus(PidginWindow *win)
9717 gboolean has_focus = FALSE;
9719 g_object_get(G_OBJECT(win->window), "has-toplevel-focus", &has_focus, NULL);
9721 return has_focus;
9724 PidginWindow *
9725 pidgin_conv_window_get_at_xy(int x, int y)
9727 PidginWindow *win;
9728 GdkWindow *gdkwin;
9729 GList *l;
9731 gdkwin = gdk_window_at_pointer(&x, &y);
9733 if (gdkwin)
9734 gdkwin = gdk_window_get_toplevel(gdkwin);
9736 for (l = pidgin_conv_windows_get_list(); l != NULL; l = l->next) {
9737 win = l->data;
9739 if (gdkwin == win->window->window)
9740 return win;
9743 return NULL;
9746 GList *
9747 pidgin_conv_window_get_gtkconvs(PidginWindow *win)
9749 return win->gtkconvs;
9752 guint
9753 pidgin_conv_window_get_gtkconv_count(PidginWindow *win)
9755 return g_list_length(win->gtkconvs);
9758 PidginWindow *
9759 pidgin_conv_window_first_with_type(PurpleConversationType type)
9761 GList *wins, *convs;
9762 PidginWindow *win;
9763 PidginConversation *conv;
9765 if (type == PURPLE_CONV_TYPE_UNKNOWN)
9766 return NULL;
9768 for (wins = pidgin_conv_windows_get_list(); wins != NULL; wins = wins->next) {
9769 win = wins->data;
9771 for (convs = win->gtkconvs;
9772 convs != NULL;
9773 convs = convs->next) {
9775 conv = convs->data;
9777 if (purple_conversation_get_type(conv->active_conv) == type)
9778 return win;
9782 return NULL;
9785 PidginWindow *
9786 pidgin_conv_window_last_with_type(PurpleConversationType type)
9788 GList *wins, *convs;
9789 PidginWindow *win;
9790 PidginConversation *conv;
9792 if (type == PURPLE_CONV_TYPE_UNKNOWN)
9793 return NULL;
9795 for (wins = g_list_last(pidgin_conv_windows_get_list());
9796 wins != NULL;
9797 wins = wins->prev) {
9799 win = wins->data;
9801 for (convs = win->gtkconvs;
9802 convs != NULL;
9803 convs = convs->next) {
9805 conv = convs->data;
9807 if (purple_conversation_get_type(conv->active_conv) == type)
9808 return win;
9812 return NULL;
9816 /**************************************************************************
9817 * Conversation placement functions
9818 **************************************************************************/
9819 typedef struct
9821 char *id;
9822 char *name;
9823 PidginConvPlacementFunc fnc;
9825 } ConvPlacementData;
9827 static GList *conv_placement_fncs = NULL;
9828 static PidginConvPlacementFunc place_conv = NULL;
9830 /* This one places conversations in the last made window. */
9831 static void
9832 conv_placement_last_created_win(PidginConversation *conv)
9834 PidginWindow *win;
9836 GList *l = g_list_last(pidgin_conv_windows_get_list());
9837 win = l ? l->data : NULL;;
9839 if (win == NULL) {
9840 win = pidgin_conv_window_new();
9842 g_signal_connect(G_OBJECT(win->window), "configure_event",
9843 G_CALLBACK(gtk_conv_configure_cb), NULL);
9845 pidgin_conv_window_add_gtkconv(win, conv);
9846 pidgin_conv_window_show(win);
9847 } else {
9848 pidgin_conv_window_add_gtkconv(win, conv);
9852 /* This one places conversations in the last made window of the same type. */
9853 static gboolean
9854 conv_placement_last_created_win_type_configured_cb(GtkWidget *w,
9855 GdkEventConfigure *event, PidginConversation *conv)
9857 int x, y;
9858 PurpleConversationType type = purple_conversation_get_type(conv->active_conv);
9859 GList *all;
9861 if (GTK_WIDGET_VISIBLE(w))
9862 gtk_window_get_position(GTK_WINDOW(w), &x, &y);
9863 else
9864 return FALSE; /* carry on normally */
9866 /* Workaround for GTK+ bug # 169811 - "configure_event" is fired
9867 * when the window is being maximized */
9868 if (gdk_window_get_state(w->window) & GDK_WINDOW_STATE_MAXIMIZED)
9869 return FALSE;
9871 /* don't save off-screen positioning */
9872 if (x + event->width < 0 ||
9873 y + event->height < 0 ||
9874 x > gdk_screen_width() ||
9875 y > gdk_screen_height())
9876 return FALSE; /* carry on normally */
9878 for (all = conv->convs; all != NULL; all = all->next) {
9879 if (type != purple_conversation_get_type(all->data)) {
9880 /* this window has different types of conversation, don't save */
9881 return FALSE;
9885 if (type == PURPLE_CONV_TYPE_IM) {
9886 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/x", x);
9887 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/y", y);
9888 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/width", event->width);
9889 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/height", event->height);
9890 } else if (type == PURPLE_CONV_TYPE_CHAT) {
9891 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/chat/x", x);
9892 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/chat/y", y);
9893 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/chat/width", event->width);
9894 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/chat/height", event->height);
9897 return FALSE;
9900 static void
9901 conv_placement_last_created_win_type(PidginConversation *conv)
9903 PidginWindow *win;
9905 win = pidgin_conv_window_last_with_type(purple_conversation_get_type(conv->active_conv));
9907 if (win == NULL) {
9908 win = pidgin_conv_window_new();
9910 if (PURPLE_CONV_TYPE_IM == purple_conversation_get_type(conv->active_conv) ||
9911 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/width") == 0) {
9912 pidgin_conv_set_position_size(win,
9913 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/x"),
9914 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/y"),
9915 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/width"),
9916 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/height"));
9917 } else if (PURPLE_CONV_TYPE_CHAT == purple_conversation_get_type(conv->active_conv)) {
9918 pidgin_conv_set_position_size(win,
9919 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/x"),
9920 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/y"),
9921 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/width"),
9922 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/height"));
9925 pidgin_conv_window_add_gtkconv(win, conv);
9926 pidgin_conv_window_show(win);
9928 g_signal_connect(G_OBJECT(win->window), "configure_event",
9929 G_CALLBACK(conv_placement_last_created_win_type_configured_cb), conv);
9930 } else
9931 pidgin_conv_window_add_gtkconv(win, conv);
9934 /* This one places each conversation in its own window. */
9935 static void
9936 conv_placement_new_window(PidginConversation *conv)
9938 PidginWindow *win;
9940 win = pidgin_conv_window_new();
9942 g_signal_connect(G_OBJECT(win->window), "configure_event",
9943 G_CALLBACK(gtk_conv_configure_cb), NULL);
9945 pidgin_conv_window_add_gtkconv(win, conv);
9947 pidgin_conv_window_show(win);
9950 static PurpleGroup *
9951 conv_get_group(PidginConversation *conv)
9953 PurpleGroup *group = NULL;
9955 if (purple_conversation_get_type(conv->active_conv) == PURPLE_CONV_TYPE_IM) {
9956 PurpleBuddy *buddy;
9958 buddy = purple_find_buddy(purple_conversation_get_account(conv->active_conv),
9959 purple_conversation_get_name(conv->active_conv));
9961 if (buddy != NULL)
9962 group = purple_buddy_get_group(buddy);
9964 } else if (purple_conversation_get_type(conv->active_conv) == PURPLE_CONV_TYPE_CHAT) {
9965 PurpleChat *chat;
9967 chat = purple_blist_find_chat(purple_conversation_get_account(conv->active_conv),
9968 purple_conversation_get_name(conv->active_conv));
9970 if (chat != NULL)
9971 group = purple_chat_get_group(chat);
9974 return group;
9978 * This groups things by, well, group. Buddies from groups will always be
9979 * grouped together, and a buddy from a group not belonging to any currently
9980 * open windows will get a new window.
9982 static void
9983 conv_placement_by_group(PidginConversation *conv)
9985 PurpleGroup *group = NULL;
9986 GList *wl, *cl;
9988 group = conv_get_group(conv);
9990 /* Go through the list of IMs and find one with this group. */
9991 for (wl = pidgin_conv_windows_get_list(); wl != NULL; wl = wl->next) {
9992 PidginWindow *win2;
9993 PidginConversation *conv2;
9994 PurpleGroup *group2 = NULL;
9996 win2 = wl->data;
9998 for (cl = win2->gtkconvs;
9999 cl != NULL;
10000 cl = cl->next) {
10001 conv2 = cl->data;
10003 group2 = conv_get_group(conv2);
10005 if (group == group2) {
10006 pidgin_conv_window_add_gtkconv(win2, conv);
10008 return;
10013 /* Make a new window. */
10014 conv_placement_new_window(conv);
10017 /* This groups things by account. Otherwise, the same semantics as above */
10018 static void
10019 conv_placement_by_account(PidginConversation *conv)
10021 GList *wins, *convs;
10022 PurpleAccount *account;
10024 account = purple_conversation_get_account(conv->active_conv);
10026 /* Go through the list of IMs and find one with this group. */
10027 for (wins = pidgin_conv_windows_get_list(); wins != NULL; wins = wins->next) {
10028 PidginWindow *win2;
10029 PidginConversation *conv2;
10031 win2 = wins->data;
10033 for (convs = win2->gtkconvs;
10034 convs != NULL;
10035 convs = convs->next) {
10036 conv2 = convs->data;
10038 if (account == purple_conversation_get_account(conv2->active_conv)) {
10039 pidgin_conv_window_add_gtkconv(win2, conv);
10040 return;
10045 /* Make a new window. */
10046 conv_placement_new_window(conv);
10049 static ConvPlacementData *
10050 get_conv_placement_data(const char *id)
10052 ConvPlacementData *data = NULL;
10053 GList *n;
10055 for (n = conv_placement_fncs; n; n = n->next) {
10056 data = n->data;
10057 if (purple_strequal(data->id, id))
10058 return data;
10061 return NULL;
10064 static void
10065 add_conv_placement_fnc(const char *id, const char *name,
10066 PidginConvPlacementFunc fnc)
10068 ConvPlacementData *data;
10070 data = g_new(ConvPlacementData, 1);
10072 data->id = g_strdup(id);
10073 data->name = g_strdup(name);
10074 data->fnc = fnc;
10076 conv_placement_fncs = g_list_append(conv_placement_fncs, data);
10079 static void
10080 ensure_default_funcs(void)
10082 if (conv_placement_fncs == NULL) {
10083 add_conv_placement_fnc("last", _("Last created window"),
10084 conv_placement_last_created_win);
10085 add_conv_placement_fnc("im_chat", _("Separate IM and Chat windows"),
10086 conv_placement_last_created_win_type);
10087 add_conv_placement_fnc("new", _("New window"),
10088 conv_placement_new_window);
10089 add_conv_placement_fnc("group", _("By group"),
10090 conv_placement_by_group);
10091 add_conv_placement_fnc("account", _("By account"),
10092 conv_placement_by_account);
10096 GList *
10097 pidgin_conv_placement_get_options(void)
10099 GList *n, *list = NULL;
10100 ConvPlacementData *data;
10102 ensure_default_funcs();
10104 for (n = conv_placement_fncs; n; n = n->next) {
10105 data = n->data;
10106 list = g_list_append(list, data->name);
10107 list = g_list_append(list, data->id);
10110 return list;
10114 void
10115 pidgin_conv_placement_add_fnc(const char *id, const char *name,
10116 PidginConvPlacementFunc fnc)
10118 g_return_if_fail(id != NULL);
10119 g_return_if_fail(name != NULL);
10120 g_return_if_fail(fnc != NULL);
10122 ensure_default_funcs();
10124 add_conv_placement_fnc(id, name, fnc);
10127 void
10128 pidgin_conv_placement_remove_fnc(const char *id)
10130 ConvPlacementData *data = get_conv_placement_data(id);
10132 if (data == NULL)
10133 return;
10135 conv_placement_fncs = g_list_remove(conv_placement_fncs, data);
10137 g_free(data->id);
10138 g_free(data->name);
10139 g_free(data);
10142 const char *
10143 pidgin_conv_placement_get_name(const char *id)
10145 ConvPlacementData *data;
10147 ensure_default_funcs();
10149 data = get_conv_placement_data(id);
10151 if (data == NULL)
10152 return NULL;
10154 return data->name;
10157 PidginConvPlacementFunc
10158 pidgin_conv_placement_get_fnc(const char *id)
10160 ConvPlacementData *data;
10162 ensure_default_funcs();
10164 data = get_conv_placement_data(id);
10166 if (data == NULL)
10167 return NULL;
10169 return data->fnc;
10172 void
10173 pidgin_conv_placement_set_current_func(PidginConvPlacementFunc func)
10175 g_return_if_fail(func != NULL);
10177 /* If tabs are enabled, set the function, otherwise, NULL it out. */
10178 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/tabs"))
10179 place_conv = func;
10180 else
10181 place_conv = NULL;
10184 PidginConvPlacementFunc
10185 pidgin_conv_placement_get_current_func(void)
10187 return place_conv;
10190 void
10191 pidgin_conv_placement_place(PidginConversation *gtkconv)
10193 if (place_conv)
10194 place_conv(gtkconv);
10195 else
10196 conv_placement_new_window(gtkconv);
10199 gboolean
10200 pidgin_conv_is_hidden(PidginConversation *gtkconv)
10202 g_return_val_if_fail(gtkconv != NULL, FALSE);
10204 return (gtkconv->win == hidden_convwin);
10208 /* Algorithm from http://www.w3.org/TR/AERT#color-contrast */
10209 static gboolean
10210 color_is_visible(GdkColor foreground, GdkColor background, guint color_contrast, guint brightness_contrast)
10212 gulong fg_brightness;
10213 gulong bg_brightness;
10214 gulong br_diff;
10215 gulong col_diff;
10216 int fred, fgreen, fblue, bred, bgreen, bblue;
10218 /* this algorithm expects colors between 0 and 255 for each of red green and blue.
10219 * GTK on the other hand has values between 0 and 65535
10220 * Err suggested I >> 8, which grabbed the high bits.
10223 fred = foreground.red >> 8 ;
10224 fgreen = foreground.green >> 8 ;
10225 fblue = foreground.blue >> 8 ;
10228 bred = background.red >> 8 ;
10229 bgreen = background.green >> 8 ;
10230 bblue = background.blue >> 8 ;
10232 fg_brightness = (fred * 299 + fgreen * 587 + fblue * 114) / 1000;
10233 bg_brightness = (bred * 299 + bgreen * 587 + bblue * 114) / 1000;
10234 br_diff = abs(fg_brightness - bg_brightness);
10236 col_diff = abs(fred - bred) + abs(fgreen - bgreen) + abs(fblue - bblue);
10238 return ((col_diff > color_contrast) && (br_diff > brightness_contrast));
10242 static GdkColor*
10243 generate_nick_colors(guint *color_count, GdkColor background)
10245 guint numcolors = *color_count;
10246 guint i = 0, j = 0;
10247 GdkColor *colors = g_new(GdkColor, numcolors);
10248 GdkColor nick_highlight;
10249 GdkColor send_color;
10250 time_t breakout_time;
10252 gdk_color_parse(DEFAULT_HIGHLIGHT_COLOR, &nick_highlight);
10253 gdk_color_parse(DEFAULT_SEND_COLOR, &send_color);
10255 srand(background.red + background.green + background.blue + 1);
10257 breakout_time = time(NULL) + 3;
10259 /* first we look through the list of "good" colors: colors that differ from every other color in the
10260 * list. only some of them will differ from the background color though. lets see if we can find
10261 * numcolors of them that do
10263 while (i < numcolors && j < NUM_NICK_SEED_COLORS && time(NULL) < breakout_time)
10265 GdkColor color = nick_seed_colors[j];
10267 if (color_is_visible(color, background, MIN_COLOR_CONTRAST, MIN_BRIGHTNESS_CONTRAST) &&
10268 color_is_visible(color, nick_highlight, MIN_COLOR_CONTRAST / 2, 0) &&
10269 color_is_visible(color, send_color, MIN_COLOR_CONTRAST / 4, 0))
10271 colors[i] = color;
10272 i++;
10274 j++;
10277 /* we might not have found numcolors in the last loop. if we did, we'll never enter this one.
10278 * if we did not, lets just find some colors that don't conflict with the background. its
10279 * expensive to find colors that not only don't conflict with the background, but also do not
10280 * conflict with each other.
10282 while(i < numcolors && time(NULL) < breakout_time)
10284 GdkColor color = { 0, rand() % 65536, rand() % 65536, rand() % 65536 };
10286 if (color_is_visible(color, background, MIN_COLOR_CONTRAST, MIN_BRIGHTNESS_CONTRAST) &&
10287 color_is_visible(color, nick_highlight, MIN_COLOR_CONTRAST / 2, 0) &&
10288 color_is_visible(color, send_color, MIN_COLOR_CONTRAST / 4, 0))
10290 colors[i] = color;
10291 i++;
10295 if (i < numcolors) {
10296 GdkColor *c = colors;
10297 purple_debug_warning("gtkconv", "Unable to generate enough random colors before timeout. %u colors found.\n", i);
10298 colors = g_memdup(c, i * sizeof(GdkColor));
10299 g_free(c);
10300 *color_count = i;
10303 return colors;