Move migration to core, check for errors
[pidgin-git.git] / pidgin / gtkconv.c
bloba667109fef5af8a9babe5f6e34a25b9c0ade36ea
1 /* pidgin
3 * Pidgin is the legal property of its developers, whose names are too numerous
4 * to list here. Please refer to the COPYRIGHT file distributed with this
5 * source distribution.
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
22 #define _PIDGIN_GTKCONV_C_
24 #include "internal.h"
25 #include "pidgin.h"
27 #ifndef _WIN32
28 # include <X11/Xlib.h>
29 #endif
31 #include <gdk/gdkkeysyms.h>
33 #include "account.h"
34 #include "cmds.h"
35 #include "core.h"
36 #include "debug.h"
37 #include "glibcompat.h"
38 #include "idle.h"
39 #include "image-store.h"
40 #include "log.h"
41 #include "notify.h"
42 #include "plugins.h"
43 #include "protocol.h"
44 #include "request.h"
45 #include "smiley-parser.h"
46 #include "theme-loader.h"
47 #include "theme-manager.h"
48 #include "util.h"
49 #include "version.h"
51 #include "gtkinternal.h"
52 #include "gtkdnd-hints.h"
53 #include "gtkblist.h"
54 #include "gtkconv.h"
55 #include "gtkconvwin.h"
56 #include "gtkconv-theme.h"
57 #include "gtkconv-theme-loader.h"
58 #include "gtkdialogs.h"
59 #include "gtklog.h"
60 #include "gtkmenutray.h"
61 #include "gtkpounce.h"
62 #include "gtkprefs.h"
63 #include "gtkprivacy.h"
64 #include "gtkutils.h"
65 #include "gtkwebview.h"
66 #include "pidginstock.h"
67 #include "pidgintooltip.h"
69 #include "gtknickcolors.h"
71 #define GTK_TOOLTIPS_VAR gtkconv->tooltips
72 #include "gtk3compat.h"
74 #define ADD_MESSAGE_HISTORY_AT_ONCE 100
77 * A GTK+ Instant Message pane.
79 struct _PidginImPane
81 GtkWidget *block;
82 GtkWidget *send_file;
83 GtkWidget *sep1;
84 GtkWidget *sep2;
85 GtkWidget *check;
86 GtkWidget *progress;
87 guint32 typing_timer;
89 /* Buddy icon stuff */
90 GtkWidget *icon_container;
91 GtkWidget *icon;
92 gboolean show_icon;
93 gboolean animate;
94 GdkPixbufAnimation *anim;
95 GdkPixbufAnimationIter *iter;
96 guint32 icon_timer;
100 * GTK+ Chat panes.
102 struct _PidginChatPane
104 GtkWidget *count;
105 GtkWidget *list;
106 GtkWidget *topic_text;
109 #define CLOSE_CONV_TIMEOUT_SECS (10 * 60)
111 #define AUTO_RESPONSE "&lt;AUTO-REPLY&gt; : "
113 typedef enum
115 PIDGIN_CONV_SET_TITLE = 1 << 0,
116 PIDGIN_CONV_BUDDY_ICON = 1 << 1,
117 PIDGIN_CONV_MENU = 1 << 2,
118 PIDGIN_CONV_TAB_ICON = 1 << 3,
119 PIDGIN_CONV_TOPIC = 1 << 4,
120 PIDGIN_CONV_SMILEY_THEME = 1 << 5,
121 PIDGIN_CONV_COLORIZE_TITLE = 1 << 6,
122 PIDGIN_CONV_E2EE = 1 << 7
123 }PidginConvFields;
125 enum {
126 CONV_ICON_COLUMN,
127 CONV_TEXT_COLUMN,
128 CONV_EMBLEM_COLUMN,
129 CONV_PROTOCOL_ICON_COLUMN,
130 CONV_NUM_COLUMNS
131 } PidginInfopaneColumns;
133 #define PIDGIN_CONV_ALL ((1 << 7) - 1)
135 /* XXX: These color defines shouldn't really be here. But the nick-color
136 * generation algorithm uses them, so keeping these around until we fix that. */
137 #define DEFAULT_SEND_COLOR "#204a87"
138 #define DEFAULT_HIGHLIGHT_COLOR "#AF7F00"
140 #define BUDDYICON_SIZE_MIN 32
141 #define BUDDYICON_SIZE_MAX 96
143 #define MIN_LUMINANCE_CONTRAST_RATIO 4.5
145 #define NICK_COLOR_GENERATE_COUNT 220
146 static GArray *generated_nick_colors = NULL;
148 /* These probably won't conflict with any WebKit values. */
149 #define PIDGIN_DRAG_BLIST_NODE (1337)
150 #define PIDGIN_DRAG_IM_CONTACT (31337)
152 static const GtkTargetEntry dnd_targets[] =
154 {"PURPLE_BLIST_NODE", GTK_TARGET_SAME_APP, PIDGIN_DRAG_BLIST_NODE},
155 {"application/x-im-contact", 0, PIDGIN_DRAG_IM_CONTACT}
158 static GtkTargetList *webkit_dnd_targets = NULL;
160 typedef struct {
161 GtkWidget *window;
163 GtkWidget *entry;
164 GtkWidget *message;
166 PurpleChatConversation *chat;
168 } InviteBuddyInfo;
170 static GtkWidget *invite_dialog = NULL;
171 static GtkWidget *warn_close_dialog = NULL;
173 static PidginConvWindow *hidden_convwin = NULL;
174 static GList *window_list = NULL;
176 /* Lists of status icons at all available sizes for use as window icons */
177 static GList *available_list = NULL;
178 static GList *away_list = NULL;
179 static GList *busy_list = NULL;
180 static GList *xa_list = NULL;
181 static GList *offline_list = NULL;
182 static GHashTable *protocol_lists = NULL;
183 static GHashTable *e2ee_stock = NULL;
185 static PurpleTheme *default_conv_theme = NULL;
187 static GRegex *image_store_tag_re = NULL;
189 static gboolean update_send_to_selection(PidginConvWindow *win);
190 static void generate_send_to_items(PidginConvWindow *win);
192 /* Prototypes. <-- because Paco-Paco hates this comment. */
193 static void load_conv_theme(PidginConversation *gtkconv);
194 static gboolean infopane_entry_activate(PidginConversation *gtkconv);
195 static void got_typing_keypress(PidginConversation *gtkconv, gboolean first);
196 static void gray_stuff_out(PidginConversation *gtkconv);
197 static void add_chat_user_common(PurpleChatConversation *chat, PurpleChatUser *cb, const char *old_name);
198 static gboolean tab_complete(PurpleConversation *conv);
199 static void pidgin_conv_updated(PurpleConversation *conv, PurpleConversationUpdateType type);
200 static void conv_set_unseen(PurpleConversation *gtkconv, PidginUnseenState state);
201 static void gtkconv_set_unseen(PidginConversation *gtkconv, PidginUnseenState state);
202 static void update_typing_icon(PidginConversation *gtkconv);
203 static void update_typing_message(PidginConversation *gtkconv, const char *message);
204 gboolean pidgin_conv_has_focus(PurpleConversation *conv);
205 static GArray* generate_nick_colors(guint numcolors, GdkRGBA background);
206 gdouble luminance(GdkRGBA color);
207 static gboolean color_is_visible(GdkRGBA foreground, GdkRGBA background, gdouble min_contrast_ratio);
208 static GtkTextTag *get_buddy_tag(PurpleChatConversation *chat, const char *who, PurpleMessageFlags flag, gboolean create);
209 static void pidgin_conv_update_fields(PurpleConversation *conv, PidginConvFields fields);
210 static void focus_out_from_menubar(GtkWidget *wid, PidginConvWindow *win);
211 static void pidgin_conv_tab_pack(PidginConvWindow *win, PidginConversation *gtkconv);
212 static gboolean infopane_press_cb(GtkWidget *widget, GdkEventButton *e, PidginConversation *conv);
213 static void hide_conv(PidginConversation *gtkconv, gboolean closetimer);
215 static void pidgin_conv_set_position_size(PidginConvWindow *win, int x, int y,
216 int width, int height);
217 static gboolean pidgin_conv_xy_to_right_infopane(PidginConvWindow *win, int x, int y);
219 static const GdkRGBA *
220 get_nick_color(PidginConversation *gtkconv, const gchar *name)
222 static GdkRGBA col;
224 if (name == NULL) {
225 col.red = col.green = col.blue = 0;
226 col.alpha = 1;
227 return &col;
230 col = g_array_index(gtkconv->nick_colors, GdkRGBA,
231 g_str_hash(name) % gtkconv->nick_colors->len);
233 return &col;
236 static PurpleBlistNode *
237 get_conversation_blist_node(PurpleConversation *conv)
239 PurpleAccount *account = purple_conversation_get_account(conv);
240 PurpleBlistNode *node = NULL;
242 if (PURPLE_IS_IM_CONVERSATION(conv)) {
243 node = PURPLE_BLIST_NODE(purple_blist_find_buddy(account, purple_conversation_get_name(conv)));
244 node = node ? node->parent : NULL;
245 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
246 node = PURPLE_BLIST_NODE(purple_blist_find_chat(account, purple_conversation_get_name(conv)));
249 return node;
252 /**************************************************************************
253 * Callbacks
254 **************************************************************************/
256 static gboolean
257 close_this_sucker(gpointer data)
259 PidginConversation *gtkconv = data;
260 GList *list = g_list_copy(gtkconv->convs);
261 g_list_foreach(list, (GFunc)g_object_unref, NULL);
262 g_list_free(list);
263 return FALSE;
266 static gboolean
267 close_conv_cb(GtkButton *button, PidginConversation *gtkconv)
269 /* We are going to destroy the conversations immediately only if the 'close immediately'
270 * preference is selected. Otherwise, close the conversation after a reasonable timeout
271 * (I am going to consider 10 minutes as a 'reasonable timeout' here.
272 * For chats, close immediately if the chat is not in the buddylist, or if the chat is
273 * not marked 'Persistent' */
274 PurpleConversation *conv = gtkconv->active_conv;
275 PurpleAccount *account = purple_conversation_get_account(conv);
276 const char *name = purple_conversation_get_name(conv);
278 if (PURPLE_IS_IM_CONVERSATION(conv)) {
279 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/close_immediately"))
280 close_this_sucker(gtkconv);
281 else
282 hide_conv(gtkconv, TRUE);
283 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
284 PurpleChat *chat = purple_blist_find_chat(account, name);
285 if (!chat ||
286 !purple_blist_node_get_bool(&chat->node, "gtk-persistent"))
287 close_this_sucker(gtkconv);
288 else
289 hide_conv(gtkconv, FALSE);
292 return TRUE;
295 static gboolean
296 lbox_size_allocate_cb(GtkWidget *w, GtkAllocation *allocation, gpointer data)
298 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/chat/userlist_width", allocation->width == 1 ? 0 : allocation->width);
300 return FALSE;
303 static void
304 default_formatize(PidginConversation *c)
306 PurpleConversation *conv = c->active_conv;
307 pidgin_webview_setup_entry(PIDGIN_WEBVIEW(c->entry), purple_conversation_get_features(conv));
310 static void
311 conversation_entry_clear(PidginConversation *gtkconv)
313 PidginWebView *webview = PIDGIN_WEBVIEW(gtkconv->entry);
315 //XXX: hotfix for not focused entry after sending a message
316 //pidgin_webview_load_html_string(webview, "");
317 pidgin_webview_load_html_string_with_selection(webview, "<div id='caret'></div>");
319 #if 0
320 /* TODO WebKit */
321 gtk_source_undo_manager_begin_not_undoable_action(webview->undo_manager);
322 pidgin_webview_clear(webview);
323 gtk_source_undo_manager_end_not_undoable_action(webview->undo_manager);
324 #endif
327 static void
328 clear_formatting_cb(PidginWebView *webview, PidginConversation *gtkconv)
330 default_formatize(gtkconv);
333 static const char *
334 pidgin_get_cmd_prefix(void)
336 return "/";
339 static PurpleCmdRet
340 say_command_cb(PurpleConversation *conv,
341 const char *cmd, char **args, char **error, void *data)
343 purple_conversation_send(conv, args[0]);
345 return PURPLE_CMD_RET_OK;
348 static PurpleCmdRet
349 me_command_cb(PurpleConversation *conv,
350 const char *cmd, char **args, char **error, void *data)
352 char *tmp;
354 tmp = g_strdup_printf("/me %s", args[0]);
355 purple_conversation_send(conv, tmp);
357 g_free(tmp);
358 return PURPLE_CMD_RET_OK;
361 static PurpleCmdRet
362 debug_command_cb(PurpleConversation *conv,
363 const char *cmd, char **args, char **error, void *data)
365 char *tmp, *markup;
367 if (!g_ascii_strcasecmp(args[0], "version")) {
368 tmp = g_strdup_printf("Using Pidgin v%s with libpurple v%s.",
369 DISPLAY_VERSION, purple_core_get_version());
370 } else if (!g_ascii_strcasecmp(args[0], "plugins")) {
371 /* Show all the loaded plugins, including plugins marked internal.
372 * This is intentional, since third party protocols are often sources of bugs, and some
373 * plugin loaders can also be buggy.
375 GString *str = g_string_new("Loaded Plugins: ");
376 const GList *plugins = purple_plugins_get_loaded();
377 if (plugins) {
378 for (; plugins; plugins = plugins->next) {
379 PurplePluginInfo *info = purple_plugin_get_info(PURPLE_PLUGIN(plugins->data));
380 str = g_string_append(str, purple_plugin_info_get_name(info));
382 if (plugins->next)
383 str = g_string_append(str, ", ");
385 } else {
386 str = g_string_append(str, "(none)");
389 tmp = g_string_free(str, FALSE);
390 } else if (!g_ascii_strcasecmp(args[0], "unsafe")) {
391 if (purple_debug_is_unsafe()) {
392 purple_debug_set_unsafe(FALSE);
393 purple_conversation_write_system_message(conv,
394 _("Unsafe debugging is now disabled."),
395 PURPLE_MESSAGE_NO_LOG);
396 } else {
397 purple_debug_set_unsafe(TRUE);
398 purple_conversation_write_system_message(conv,
399 _("Unsafe debugging is now enabled."),
400 PURPLE_MESSAGE_NO_LOG);
403 return PURPLE_CMD_RET_OK;
404 } else if (!g_ascii_strcasecmp(args[0], "verbose")) {
405 if (purple_debug_is_verbose()) {
406 purple_debug_set_verbose(FALSE);
407 purple_conversation_write_system_message(conv,
408 _("Verbose debugging is now disabled."),
409 PURPLE_MESSAGE_NO_LOG);
410 } else {
411 purple_debug_set_verbose(TRUE);
412 purple_conversation_write_system_message(conv,
413 _("Verbose debugging is now enabled."),
414 PURPLE_MESSAGE_NO_LOG);
417 return PURPLE_CMD_RET_OK;
418 } else {
419 purple_conversation_write_system_message(conv,
420 _("Supported debug options are: plugins, version, unsafe, verbose"),
421 PURPLE_MESSAGE_NO_LOG);
422 return PURPLE_CMD_RET_OK;
425 markup = g_markup_escape_text(tmp, -1);
426 purple_conversation_send(conv, markup);
428 g_free(tmp);
429 g_free(markup);
430 return PURPLE_CMD_RET_OK;
433 static void clear_conversation_scrollback_cb(PurpleConversation *conv,
434 void *data)
436 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
438 if (PIDGIN_CONVERSATION(conv)) {
439 load_conv_theme(gtkconv);
440 gtkconv->last_flags = 0;
444 static PurpleCmdRet
445 clear_command_cb(PurpleConversation *conv,
446 const char *cmd, char **args, char **error, void *data)
448 purple_conversation_clear_message_history(conv);
449 return PURPLE_CMD_RET_OK;
452 static PurpleCmdRet
453 clearall_command_cb(PurpleConversation *conv,
454 const char *cmd, char **args, char **error, void *data)
456 GList *l;
457 for (l = purple_conversations_get_all(); l != NULL; l = l->next)
458 purple_conversation_clear_message_history(PURPLE_CONVERSATION(l->data));
460 return PURPLE_CMD_RET_OK;
463 static PurpleCmdRet
464 help_command_cb(PurpleConversation *conv,
465 const char *cmd, char **args, char **error, void *data)
467 GList *l, *text;
468 GString *s;
470 if (args[0] != NULL) {
471 s = g_string_new("");
472 text = purple_cmd_help(conv, args[0]);
474 if (text) {
475 for (l = text; l; l = l->next)
476 if (l->next)
477 g_string_append_printf(s, "%s\n", (char *)l->data);
478 else
479 g_string_append_printf(s, "%s", (char *)l->data);
480 } else {
481 g_string_append(s, _("No such command (in this context)."));
483 } else {
484 s = g_string_new(_("Use \"/help &lt;command&gt;\" for help with a "
485 "specific command.<br/>The following commands are available "
486 "in this context:<br/>"));
488 text = purple_cmd_list(conv);
489 for (l = text; l; l = l->next)
490 if (l->next)
491 g_string_append_printf(s, "%s, ", (char *)l->data);
492 else
493 g_string_append_printf(s, "%s.", (char *)l->data);
494 g_list_free(text);
497 purple_conversation_write_system_message(conv, s->str, PURPLE_MESSAGE_NO_LOG);
498 g_string_free(s, TRUE);
500 return PURPLE_CMD_RET_OK;
503 static void
504 send_history_add(PidginConversation *gtkconv, const char *message)
506 GList *first;
508 first = g_list_first(gtkconv->send_history);
509 g_free(first->data);
510 first->data = g_strdup(message);
511 gtkconv->send_history = g_list_prepend(first, NULL);
514 static gboolean
515 check_for_and_do_command(PurpleConversation *conv)
517 PidginConversation *gtkconv;
518 char *cmd;
519 const char *prefix;
520 gboolean retval = FALSE;
522 gtkconv = PIDGIN_CONVERSATION(conv);
523 prefix = pidgin_get_cmd_prefix();
525 cmd = pidgin_webview_get_body_text(PIDGIN_WEBVIEW(gtkconv->entry));
527 if (cmd && purple_str_has_prefix(cmd, prefix)) {
528 PurpleCmdStatus status;
529 char *error, *cmdline, *markup, *send_history;
531 send_history = pidgin_webview_get_body_html(PIDGIN_WEBVIEW(gtkconv->entry));
532 send_history_add(gtkconv, send_history);
533 g_free(send_history);
535 cmdline = cmd + strlen(prefix);
537 if (strcmp(cmdline, "xyzzy") == 0) {
538 purple_conversation_write_system_message(conv,
539 "Nothing happens", PURPLE_MESSAGE_NO_LOG);
540 g_free(cmd);
541 return TRUE;
544 /* TODO WebKit: Cut out prefix for markup... */
545 markup = pidgin_webview_get_body_html(PIDGIN_WEBVIEW(gtkconv->entry));
546 status = purple_cmd_do_command(conv, cmdline, markup, &error);
547 g_free(markup);
549 switch (status) {
550 case PURPLE_CMD_STATUS_OK:
551 retval = TRUE;
552 break;
553 case PURPLE_CMD_STATUS_NOT_FOUND:
555 PurpleProtocol *protocol = NULL;
556 PurpleConnection *gc;
558 if ((gc = purple_conversation_get_connection(conv)))
559 protocol = purple_connection_get_protocol(gc);
561 if ((protocol != NULL) && (purple_protocol_get_options(protocol) & OPT_PROTO_SLASH_COMMANDS_NATIVE)) {
562 char *spaceslash;
564 /* If the first word in the entered text has a '/' in it, then the user
565 * probably didn't mean it as a command. So send the text as message. */
566 spaceslash = cmdline;
567 while (*spaceslash && *spaceslash != ' ' && *spaceslash != '/')
568 spaceslash++;
570 if (*spaceslash != '/') {
571 purple_conversation_write_system_message(conv,
572 _("Unknown command."), PURPLE_MESSAGE_NO_LOG);
573 retval = TRUE;
576 break;
578 case PURPLE_CMD_STATUS_WRONG_ARGS:
579 purple_conversation_write_system_message(conv,
580 _("Syntax Error: You typed the wrong "
581 "number of arguments to that command."),
582 PURPLE_MESSAGE_NO_LOG);
583 retval = TRUE;
584 break;
585 case PURPLE_CMD_STATUS_FAILED:
586 purple_conversation_write_system_message(conv,
587 error ? error : _("Your command failed for an unknown reason."),
588 PURPLE_MESSAGE_NO_LOG);
589 g_free(error);
590 retval = TRUE;
591 break;
592 case PURPLE_CMD_STATUS_WRONG_TYPE:
593 if(PURPLE_IS_IM_CONVERSATION(conv))
594 purple_conversation_write_system_message(conv,
595 _("That command only works in chats, not IMs."),
596 PURPLE_MESSAGE_NO_LOG);
597 else
598 purple_conversation_write_system_message(conv,
599 _("That command only works in IMs, not chats."),
600 PURPLE_MESSAGE_NO_LOG);
601 retval = TRUE;
602 break;
603 case PURPLE_CMD_STATUS_WRONG_PROTOCOL:
604 purple_conversation_write_system_message(conv,
605 _("That command doesn't work on this protocol."),
606 PURPLE_MESSAGE_NO_LOG);
607 retval = TRUE;
608 break;
612 g_free(cmd);
614 return retval;
617 static void
618 send_cb(GtkWidget *widget, PidginConversation *gtkconv)
620 PurpleConversation *conv = gtkconv->active_conv;
621 PurpleAccount *account;
622 #if 0
623 PurpleConnection *gc;
624 #endif
625 PurpleMessageFlags flags = 0;
626 char *buf;
628 account = purple_conversation_get_account(conv);
630 if (check_for_and_do_command(conv)) {
631 conversation_entry_clear(gtkconv);
632 return;
635 if (PURPLE_IS_CHAT_CONVERSATION(conv) &&
636 purple_chat_conversation_has_left(PURPLE_CHAT_CONVERSATION(conv)))
637 return;
639 if (!purple_account_is_connected(account))
640 return;
642 if (pidgin_webview_is_empty(PIDGIN_WEBVIEW(gtkconv->entry)))
643 return;
645 buf = pidgin_webview_get_body_html(PIDGIN_WEBVIEW(gtkconv->entry));
646 g_return_if_fail(buf != NULL);
648 gtk_widget_grab_focus(gtkconv->entry);
650 purple_idle_touch();
652 /* XXX: is there a better way to tell if the message has images? */
653 if (strstr(buf, "<img ") != NULL)
654 flags |= PURPLE_MESSAGE_IMAGES;
656 #if 0
657 gc = purple_account_get_connection(account);
658 if (gc && (purple_conversation_get_features(conv) & PURPLE_CONNECTION_FLAG_NO_NEWLINES)) {
659 /* TODO WebKit */
660 char **bufs;
661 int i;
663 bufs = pidgin_webview_get_markup_lines(PIDGIN_WEBVIEW(gtkconv->entry));
664 for (i = 0; bufs[i]; i++) {
665 send_history_add(gtkconv, bufs[i]);
666 purple_conversation_send_with_flags(conv, bufs[i], flags);
669 g_strfreev(bufs);
670 } else
671 #endif
673 send_history_add(gtkconv, buf);
674 purple_conversation_send_with_flags(conv, buf, flags);
677 g_free(buf);
679 conversation_entry_clear(gtkconv);
680 gtkconv_set_unseen(gtkconv, PIDGIN_UNSEEN_NONE);
681 gtk_widget_grab_focus(gtkconv->entry); // XXX: doesn't work
684 static void
685 add_remove_cb(GtkWidget *widget, PidginConversation *gtkconv)
687 PurpleAccount *account;
688 const char *name;
689 PurpleConversation *conv = gtkconv->active_conv;
691 account = purple_conversation_get_account(conv);
692 name = purple_conversation_get_name(conv);
694 if (PURPLE_IS_IM_CONVERSATION(conv)) {
695 PurpleBuddy *b;
697 b = purple_blist_find_buddy(account, name);
698 if (b != NULL)
699 pidgin_dialogs_remove_buddy(b);
700 else if (account != NULL && purple_account_is_connected(account))
701 purple_blist_request_add_buddy(account, (char *)name, NULL, NULL);
702 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
703 PurpleChat *c;
705 c = purple_blist_find_chat(account, name);
706 if (c != NULL)
707 pidgin_dialogs_remove_chat(c);
708 else if (account != NULL && purple_account_is_connected(account))
709 purple_blist_request_add_chat(account, NULL, NULL, name);
712 gtk_widget_grab_focus(PIDGIN_CONVERSATION(conv)->entry);
715 static void chat_do_info(PidginConversation *gtkconv, const char *who)
717 PurpleChatConversation *chat = PURPLE_CHAT_CONVERSATION(gtkconv->active_conv);
718 PurpleConnection *gc;
720 if ((gc = purple_conversation_get_connection(gtkconv->active_conv))) {
721 pidgin_retrieve_user_info_in_chat(gc, who, purple_chat_conversation_get_id(chat));
726 static void
727 info_cb(GtkWidget *widget, PidginConversation *gtkconv)
729 PurpleConversation *conv = gtkconv->active_conv;
731 if (PURPLE_IS_IM_CONVERSATION(conv)) {
732 pidgin_retrieve_user_info(purple_conversation_get_connection(conv),
733 purple_conversation_get_name(conv));
734 gtk_widget_grab_focus(gtkconv->entry);
735 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
736 /* Get info of the person currently selected in the GtkTreeView */
737 PidginChatPane *gtkchat;
738 GtkTreeIter iter;
739 GtkTreeModel *model;
740 GtkTreeSelection *sel;
741 char *name;
743 gtkchat = gtkconv->u.chat;
745 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
746 sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(gtkchat->list));
748 if (gtk_tree_selection_get_selected(sel, NULL, &iter))
749 gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, CHAT_USERS_NAME_COLUMN, &name, -1);
750 else
751 return;
753 chat_do_info(gtkconv, name);
754 g_free(name);
758 static void
759 block_cb(GtkWidget *widget, PidginConversation *gtkconv)
761 PurpleConversation *conv = gtkconv->active_conv;
762 PurpleAccount *account;
764 account = purple_conversation_get_account(conv);
766 if (account != NULL && purple_account_is_connected(account))
767 pidgin_request_add_block(account, purple_conversation_get_name(conv));
769 gtk_widget_grab_focus(PIDGIN_CONVERSATION(conv)->entry);
772 static void
773 unblock_cb(GtkWidget *widget, PidginConversation *gtkconv)
775 PurpleConversation *conv = gtkconv->active_conv;
776 PurpleAccount *account;
778 account = purple_conversation_get_account(conv);
780 if (account != NULL && purple_account_is_connected(account))
781 pidgin_request_add_permit(account, purple_conversation_get_name(conv));
783 gtk_widget_grab_focus(PIDGIN_CONVERSATION(conv)->entry);
786 static gboolean
787 chat_invite_filter(const PidginBuddyCompletionEntry *entry, gpointer data)
789 PurpleAccount *filter_account = data;
790 PurpleAccount *account = NULL;
792 if (entry->is_buddy) {
793 if (PURPLE_BUDDY_IS_ONLINE(entry->entry.buddy))
794 account = purple_buddy_get_account(entry->entry.buddy);
795 else
796 return FALSE;
797 } else {
798 account = entry->entry.logged_buddy->account;
800 if (account == filter_account)
801 return TRUE;
802 return FALSE;
805 static void
806 do_invite(GtkWidget *w, int resp, InviteBuddyInfo *info)
808 const char *buddy, *message;
809 PurpleChatConversation *chat;
811 chat = info->chat;
813 if (resp == GTK_RESPONSE_OK) {
814 buddy = gtk_entry_get_text(GTK_ENTRY(info->entry));
815 message = gtk_entry_get_text(GTK_ENTRY(info->message));
817 if (!g_ascii_strcasecmp(buddy, ""))
818 return;
820 purple_serv_chat_invite(purple_conversation_get_connection(PURPLE_CONVERSATION(chat)),
821 purple_chat_conversation_get_id(chat),
822 message, buddy);
825 gtk_widget_destroy(invite_dialog);
826 invite_dialog = NULL;
828 g_free(info);
831 static void
832 invite_dnd_recv(GtkWidget *widget, GdkDragContext *dc, gint x, gint y,
833 GtkSelectionData *sd, guint dnd_info, guint t, gpointer data)
835 InviteBuddyInfo *info = (InviteBuddyInfo *)data;
836 const char *convprotocol;
837 gboolean success = TRUE;
839 convprotocol = purple_account_get_protocol_id(
840 purple_conversation_get_account(PURPLE_CONVERSATION(info->chat)));
842 if (dnd_info == PIDGIN_DRAG_BLIST_NODE)
844 PurpleBlistNode *node = NULL;
845 PurpleBuddy *buddy;
846 const guchar *data = gtk_selection_data_get_data(sd);
848 memcpy(&node, data, sizeof(node));
850 if (PURPLE_IS_CONTACT(node))
851 buddy = purple_contact_get_priority_buddy((PurpleContact *)node);
852 else if (PURPLE_IS_BUDDY(node))
853 buddy = (PurpleBuddy *)node;
854 else
855 return;
857 if (strcmp(convprotocol, purple_account_get_protocol_id(purple_buddy_get_account(buddy))))
859 purple_notify_error(PIDGIN_CONVERSATION(PURPLE_CONVERSATION(info->chat)),
860 NULL, _("That buddy is not on the same protocol"
861 " as this chat."), NULL,
862 purple_request_cpar_from_conversation(PURPLE_CONVERSATION(info->chat)));
863 success = FALSE;
865 else
866 gtk_entry_set_text(GTK_ENTRY(info->entry), purple_buddy_get_name(buddy));
868 gtk_drag_finish(dc, success,
869 gdk_drag_context_get_actions(dc) == GDK_ACTION_MOVE, t);
871 else if (dnd_info == PIDGIN_DRAG_IM_CONTACT)
873 char *protocol = NULL;
874 char *username = NULL;
875 PurpleAccount *account;
877 if (pidgin_parse_x_im_contact((const char *) data, FALSE, &account,
878 &protocol, &username, NULL))
880 if (account == NULL)
882 purple_notify_error(PIDGIN_CONVERSATION(PURPLE_CONVERSATION(info->chat)), NULL,
883 _("You are not currently signed on with an account that "
884 "can invite that buddy."), NULL, NULL);
886 else if (strcmp(convprotocol, purple_account_get_protocol_id(account)))
888 purple_notify_error(
889 PIDGIN_CONVERSATION(PURPLE_CONVERSATION(info->chat)), NULL,
890 _("That buddy is not on the same "
891 "protocol as this chat."), NULL,
892 purple_request_cpar_from_account(
893 account));
894 success = FALSE;
896 else
898 gtk_entry_set_text(GTK_ENTRY(info->entry), username);
902 g_free(username);
903 g_free(protocol);
905 gtk_drag_finish(dc, success,
906 gdk_drag_context_get_actions(dc) == GDK_ACTION_MOVE, t);
910 static void
911 invite_cb(GtkWidget *widget, PidginConversation *gtkconv)
913 PurpleChatConversation *chat = PURPLE_CHAT_CONVERSATION(gtkconv->active_conv);
914 InviteBuddyInfo *info = NULL;
916 if (invite_dialog == NULL) {
917 PidginConvWindow *gtkwin;
918 GtkWidget *label;
919 GtkWidget *vbox, *hbox;
920 GtkWidget *grid;
921 GtkWidget *img;
923 img = gtk_image_new_from_stock(PIDGIN_STOCK_DIALOG_QUESTION,
924 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_HUGE));
926 info = g_new0(InviteBuddyInfo, 1);
927 info->chat = chat;
929 gtkwin = pidgin_conv_get_window(gtkconv);
931 /* Create the new dialog. */
932 invite_dialog = gtk_dialog_new_with_buttons(
933 _("Invite Buddy Into Chat Room"),
934 GTK_WINDOW(gtkwin->window), 0,
935 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
936 PIDGIN_STOCK_INVITE, GTK_RESPONSE_OK, NULL);
938 gtk_dialog_set_default_response(GTK_DIALOG(invite_dialog),
939 GTK_RESPONSE_OK);
940 gtk_container_set_border_width(GTK_CONTAINER(invite_dialog), PIDGIN_HIG_BOX_SPACE);
941 gtk_window_set_resizable(GTK_WINDOW(invite_dialog), FALSE);
943 info->window = GTK_WIDGET(invite_dialog);
945 /* Setup the outside spacing. */
946 vbox = gtk_dialog_get_content_area(GTK_DIALOG(invite_dialog));
948 gtk_box_set_spacing(GTK_BOX(vbox), PIDGIN_HIG_BORDER);
949 gtk_container_set_border_width(GTK_CONTAINER(vbox), PIDGIN_HIG_BOX_SPACE);
951 /* Setup the inner hbox and put the dialog's icon in it. */
952 hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PIDGIN_HIG_BORDER);
953 gtk_container_add(GTK_CONTAINER(vbox), hbox);
954 gtk_box_pack_start(GTK_BOX(hbox), img, FALSE, FALSE, 0);
955 gtk_widget_set_halign(img, GTK_ALIGN_START);
956 gtk_widget_set_valign(img, GTK_ALIGN_START);
958 /* Setup the right vbox. */
959 vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
960 gtk_container_add(GTK_CONTAINER(hbox), vbox);
962 /* Put our happy label in it. */
963 label = gtk_label_new(_("Please enter the name of the user you wish "
964 "to invite, along with an optional invite "
965 "message."));
966 gtk_widget_set_size_request(label, 350, -1);
967 gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
968 gtk_label_set_xalign(GTK_LABEL(label), 0);
969 gtk_label_set_yalign(GTK_LABEL(label), 0);
970 gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 0);
972 /* hbox for the grid, and to give it some spacing on the left. */
973 hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PIDGIN_HIG_BOX_SPACE);
974 gtk_container_add(GTK_CONTAINER(vbox), hbox);
976 /* Setup the grid we're going to use to lay stuff out. */
977 grid = gtk_grid_new();
978 gtk_grid_set_row_spacing(GTK_GRID(grid), PIDGIN_HIG_BOX_SPACE);
979 gtk_grid_set_column_spacing(GTK_GRID(grid), PIDGIN_HIG_BOX_SPACE);
980 gtk_container_set_border_width(GTK_CONTAINER(grid), PIDGIN_HIG_BORDER);
981 gtk_box_pack_start(GTK_BOX(vbox), grid, FALSE, FALSE, 0);
983 /* Now the Buddy label */
984 label = gtk_label_new(NULL);
985 gtk_label_set_markup_with_mnemonic(GTK_LABEL(label), _("_Buddy:"));
986 gtk_widget_set_hexpand(label, TRUE);
987 gtk_widget_set_vexpand(label, TRUE);
988 gtk_label_set_xalign(GTK_LABEL(label), 0);
989 gtk_label_set_yalign(GTK_LABEL(label), 0);
990 gtk_grid_attach(GTK_GRID(grid), label, 0, 0, 1, 1);
992 /* Now the Buddy drop-down entry field. */
993 info->entry = gtk_entry_new();
994 pidgin_setup_screenname_autocomplete(info->entry, NULL, chat_invite_filter,
995 purple_conversation_get_account(PURPLE_CONVERSATION(chat)));
996 gtk_widget_set_hexpand(info->entry, TRUE);
997 gtk_widget_set_vexpand(info->entry, TRUE);
998 gtk_grid_attach(GTK_GRID(grid), info->entry, 1, 0, 1, 1);
999 gtk_label_set_mnemonic_widget(GTK_LABEL(label), info->entry);
1001 /* Now the label for "Message" */
1002 label = gtk_label_new(NULL);
1003 gtk_label_set_markup_with_mnemonic(GTK_LABEL(label), _("_Message:"));
1004 gtk_widget_set_hexpand(label, TRUE);
1005 gtk_widget_set_vexpand(label, TRUE);
1006 gtk_label_set_xalign(GTK_LABEL(label), 0);
1007 gtk_label_set_yalign(GTK_LABEL(label), 0);
1008 gtk_grid_attach(GTK_GRID(grid), label, 0, 1, 1, 1);
1010 /* And finally, the Message entry field. */
1011 info->message = gtk_entry_new();
1012 gtk_entry_set_activates_default(GTK_ENTRY(info->message), TRUE);
1013 gtk_widget_set_hexpand(info->message, TRUE);
1014 gtk_widget_set_vexpand(info->message, TRUE);
1015 gtk_grid_attach(GTK_GRID(grid), info->message, 1, 1, 1, 1);
1016 gtk_label_set_mnemonic_widget(GTK_LABEL(label), info->message);
1018 /* Connect the signals. */
1019 g_signal_connect(G_OBJECT(invite_dialog), "response",
1020 G_CALLBACK(do_invite), info);
1021 /* Setup drag-and-drop */
1022 gtk_drag_dest_set(info->window,
1023 GTK_DEST_DEFAULT_MOTION |
1024 GTK_DEST_DEFAULT_DROP,
1025 dnd_targets,
1026 sizeof(dnd_targets) / sizeof(GtkTargetEntry),
1027 GDK_ACTION_COPY);
1028 gtk_drag_dest_set(info->entry,
1029 GTK_DEST_DEFAULT_MOTION |
1030 GTK_DEST_DEFAULT_DROP,
1031 dnd_targets,
1032 sizeof(dnd_targets) / sizeof(GtkTargetEntry),
1033 GDK_ACTION_COPY);
1035 g_signal_connect(G_OBJECT(info->window), "drag_data_received",
1036 G_CALLBACK(invite_dnd_recv), info);
1037 g_signal_connect(G_OBJECT(info->entry), "drag_data_received",
1038 G_CALLBACK(invite_dnd_recv), info);
1041 gtk_widget_show_all(invite_dialog);
1043 if (info != NULL)
1044 gtk_widget_grab_focus(info->entry);
1047 static void
1048 menu_new_conv_cb(GtkAction *action, gpointer data)
1050 pidgin_dialogs_im();
1053 static void
1054 menu_join_chat_cb(GtkAction *action, gpointer data)
1056 pidgin_blist_joinchat_show();
1059 static void
1060 savelog_writefile_cb(void *user_data, const char *filename)
1062 PurpleConversation *conv = (PurpleConversation *)user_data;
1063 PidginWebView *webview;
1064 FILE *fp;
1065 const char *name;
1066 gchar *text;
1068 if ((fp = g_fopen(filename, "w+")) == NULL) {
1069 purple_notify_error(PIDGIN_CONVERSATION(conv), NULL,
1070 _("Unable to open file."), NULL,
1071 purple_request_cpar_from_conversation(conv));
1072 return;
1075 webview = PIDGIN_WEBVIEW(PIDGIN_CONVERSATION(conv)->webview);
1076 name = purple_conversation_get_name(conv);
1077 fprintf(fp, "<html>\n");
1079 fprintf(fp, "<head>\n");
1080 fprintf(fp, "<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">\n");
1081 fprintf(fp, "<title>%s</title>\n", name);
1082 text = pidgin_webview_get_head_html(webview);
1083 fprintf(fp, "%s", text);
1084 g_free(text);
1085 fprintf(fp, "</head>\n");
1087 fprintf(fp, "<body>\n");
1088 fprintf(fp, _("<h1>Conversation with %s</h1>\n"), name);
1089 text = pidgin_webview_get_body_html(webview);
1090 fprintf(fp, "%s", text);
1091 g_free(text);
1092 fprintf(fp, "\n</body>\n");
1094 fprintf(fp, "</html>\n");
1095 fclose(fp);
1099 * It would be kinda cool if this gave the option of saving a
1100 * plaintext v. HTML file.
1102 static void
1103 menu_save_as_cb(GtkAction *action, gpointer data)
1105 PidginConvWindow *win = data;
1106 PurpleConversation *conv = pidgin_conv_window_get_active_conversation(win);
1107 PurpleAccount *account = purple_conversation_get_account(conv);
1108 PurpleBuddy *buddy = purple_blist_find_buddy(account, purple_conversation_get_name(conv));
1109 const char *name;
1110 gchar *buf;
1111 gchar *c;
1113 if (buddy != NULL)
1114 name = purple_buddy_get_contact_alias(buddy);
1115 else
1116 name = purple_normalize(account, purple_conversation_get_name(conv));
1118 buf = g_strdup_printf("%s.html", name);
1119 for (c = buf ; *c ; c++)
1121 if (*c == '/' || *c == '\\')
1122 *c = ' ';
1124 purple_request_file(PIDGIN_CONVERSATION(conv), _("Save Conversation"),
1125 buf, TRUE, G_CALLBACK(savelog_writefile_cb), NULL,
1126 purple_request_cpar_from_conversation(conv), conv);
1128 g_free(buf);
1131 static void
1132 menu_view_log_cb(GtkAction *action, gpointer data)
1134 PidginConvWindow *win = data;
1135 PurpleConversation *conv;
1136 PurpleLogType type;
1137 PidginBuddyList *gtkblist;
1138 const char *name;
1139 PurpleAccount *account;
1140 GSList *buddies;
1141 GSList *cur;
1143 conv = pidgin_conv_window_get_active_conversation(win);
1145 if (PURPLE_IS_IM_CONVERSATION(conv))
1146 type = PURPLE_LOG_IM;
1147 else if (PURPLE_IS_CHAT_CONVERSATION(conv))
1148 type = PURPLE_LOG_CHAT;
1149 else
1150 return;
1152 gtkblist = pidgin_blist_get_default_gtk_blist();
1154 pidgin_set_cursor(gtkblist->window, GDK_WATCH);
1155 pidgin_set_cursor(win->window, GDK_WATCH);
1157 name = purple_conversation_get_name(conv);
1158 account = purple_conversation_get_account(conv);
1160 buddies = purple_blist_find_buddies(account, name);
1161 for (cur = buddies; cur != NULL; cur = cur->next)
1163 PurpleBlistNode *node = cur->data;
1164 if ((node != NULL) && ((node->prev != NULL) || (node->next != NULL)))
1166 pidgin_log_show_contact((PurpleContact *)node->parent);
1167 g_slist_free(buddies);
1168 pidgin_clear_cursor(gtkblist->window);
1169 pidgin_clear_cursor(win->window);
1170 return;
1173 g_slist_free(buddies);
1175 pidgin_log_show(type, name, account);
1177 pidgin_clear_cursor(gtkblist->window);
1178 pidgin_clear_cursor(win->window);
1181 static void
1182 menu_clear_cb(GtkAction *action, gpointer data)
1184 PidginConvWindow *win = data;
1185 PurpleConversation *conv;
1187 conv = pidgin_conv_window_get_active_conversation(win);
1188 purple_conversation_clear_message_history(conv);
1191 static void
1192 menu_find_cb(GtkAction *action, gpointer data)
1194 PidginConvWindow *gtkwin = data;
1195 PidginConversation *gtkconv = pidgin_conv_window_get_active_gtkconv(gtkwin);
1196 gtk_widget_show_all(gtkconv->quickfind_container);
1197 gtk_widget_grab_focus(gtkconv->quickfind_entry);
1200 #ifdef USE_VV
1201 static void
1202 menu_initiate_media_call_cb(GtkAction *action, gpointer data)
1204 PidginConvWindow *win = (PidginConvWindow *)data;
1205 PurpleConversation *conv = pidgin_conv_window_get_active_conversation(win);
1206 PurpleAccount *account = purple_conversation_get_account(conv);
1208 purple_protocol_initiate_media(account,
1209 purple_conversation_get_name(conv),
1210 action == win->menu->audio_call ? PURPLE_MEDIA_AUDIO :
1211 action == win->menu->video_call ? PURPLE_MEDIA_VIDEO :
1212 action == win->menu->audio_video_call ? PURPLE_MEDIA_AUDIO |
1213 PURPLE_MEDIA_VIDEO : PURPLE_MEDIA_NONE);
1215 #endif
1217 static void
1218 menu_send_file_cb(GtkAction *action, gpointer data)
1220 PidginConvWindow *win = data;
1221 PurpleConversation *conv = pidgin_conv_window_get_active_conversation(win);
1223 if (PURPLE_IS_IM_CONVERSATION(conv)) {
1224 purple_serv_send_file(purple_conversation_get_connection(conv), purple_conversation_get_name(conv), NULL);
1229 static void
1230 menu_get_attention_cb(GObject *obj, gpointer data)
1232 PidginConvWindow *win = data;
1233 PurpleConversation *conv = pidgin_conv_window_get_active_conversation(win);
1235 if (PURPLE_IS_IM_CONVERSATION(conv)) {
1236 int index;
1237 if ((GtkAction *)obj == win->menu->get_attention)
1238 index = 0;
1239 else
1240 index = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(obj), "index"));
1241 purple_protocol_send_attention(purple_conversation_get_connection(conv),
1242 purple_conversation_get_name(conv), index);
1246 static void
1247 menu_add_pounce_cb(GtkAction *action, gpointer data)
1249 PidginConvWindow *win = data;
1250 PurpleConversation *conv;
1252 conv = pidgin_conv_window_get_active_gtkconv(win)->active_conv;
1254 pidgin_pounce_editor_show(purple_conversation_get_account(conv),
1255 purple_conversation_get_name(conv), NULL);
1258 static void
1259 menu_insert_link_cb(GtkAction *action, gpointer data)
1261 PidginConvWindow *win = data;
1262 PidginConversation *gtkconv;
1263 PidginWebView *entry;
1265 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
1266 entry = PIDGIN_WEBVIEW(gtkconv->entry);
1268 pidgin_webview_activate_toolbar(entry, PIDGIN_WEBVIEW_ACTION_LINK);
1271 static void
1272 menu_insert_image_cb(GtkAction *action, gpointer data)
1274 PidginConvWindow *win = data;
1275 PidginConversation *gtkconv;
1276 PidginWebView *entry;
1278 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
1279 entry = PIDGIN_WEBVIEW(gtkconv->entry);
1281 pidgin_webview_activate_toolbar(entry, PIDGIN_WEBVIEW_ACTION_IMAGE);
1284 static void
1285 menu_alias_cb(GtkAction *action, gpointer data)
1287 PidginConvWindow *win = data;
1288 PurpleConversation *conv;
1289 PurpleAccount *account;
1290 const char *name;
1292 conv = pidgin_conv_window_get_active_conversation(win);
1293 account = purple_conversation_get_account(conv);
1294 name = purple_conversation_get_name(conv);
1296 if (PURPLE_IS_IM_CONVERSATION(conv)) {
1297 PurpleBuddy *b;
1299 b = purple_blist_find_buddy(account, name);
1300 if (b != NULL)
1301 pidgin_dialogs_alias_buddy(b);
1302 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
1303 PurpleChat *c;
1305 c = purple_blist_find_chat(account, name);
1306 if (c != NULL)
1307 pidgin_dialogs_alias_chat(c);
1311 static void
1312 menu_get_info_cb(GtkAction *action, gpointer data)
1314 PidginConvWindow *win = data;
1315 PurpleConversation *conv;
1317 conv = pidgin_conv_window_get_active_conversation(win);
1319 info_cb(NULL, PIDGIN_CONVERSATION(conv));
1322 static void
1323 menu_invite_cb(GtkAction *action, gpointer data)
1325 PidginConvWindow *win = data;
1326 PurpleConversation *conv;
1328 conv = pidgin_conv_window_get_active_conversation(win);
1330 invite_cb(NULL, PIDGIN_CONVERSATION(conv));
1333 static void
1334 menu_block_cb(GtkAction *action, gpointer data)
1336 PidginConvWindow *win = data;
1337 PurpleConversation *conv;
1339 conv = pidgin_conv_window_get_active_conversation(win);
1341 block_cb(NULL, PIDGIN_CONVERSATION(conv));
1344 static void
1345 menu_unblock_cb(GtkAction *action, gpointer data)
1347 PidginConvWindow *win = data;
1348 PurpleConversation *conv;
1350 conv = pidgin_conv_window_get_active_conversation(win);
1352 unblock_cb(NULL, PIDGIN_CONVERSATION(conv));
1355 static void
1356 menu_add_remove_cb(GtkAction *action, gpointer data)
1358 PidginConvWindow *win = data;
1359 PurpleConversation *conv;
1361 conv = pidgin_conv_window_get_active_conversation(win);
1363 add_remove_cb(NULL, PIDGIN_CONVERSATION(conv));
1366 static gboolean
1367 close_already(gpointer data)
1369 g_object_unref(data);
1370 return FALSE;
1373 static void
1374 hide_conv(PidginConversation *gtkconv, gboolean closetimer)
1376 GList *list;
1378 purple_signal_emit(pidgin_conversations_get_handle(),
1379 "conversation-hiding", gtkconv);
1381 for (list = g_list_copy(gtkconv->convs); list; list = g_list_delete_link(list, list)) {
1382 PurpleConversation *conv = list->data;
1383 if (closetimer) {
1384 guint timer = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(conv), "close-timer"));
1385 if (timer)
1386 purple_timeout_remove(timer);
1387 timer = purple_timeout_add_seconds(CLOSE_CONV_TIMEOUT_SECS, close_already, conv);
1388 g_object_set_data(G_OBJECT(conv), "close-timer", GINT_TO_POINTER(timer));
1390 #if 0
1391 /* I will miss you */
1392 purple_conversation_set_ui_ops(conv, NULL);
1393 #else
1394 pidgin_conv_window_remove_gtkconv(gtkconv->win, gtkconv);
1395 pidgin_conv_window_add_gtkconv(hidden_convwin, gtkconv);
1396 #endif
1400 static void
1401 menu_close_conv_cb(GtkAction *action, gpointer data)
1403 PidginConvWindow *win = data;
1405 close_conv_cb(NULL, PIDGIN_CONVERSATION(pidgin_conv_window_get_active_conversation(win)));
1408 static void
1409 menu_logging_cb(GtkAction *action, gpointer data)
1411 PidginConvWindow *win = data;
1412 PurpleConversation *conv;
1413 gboolean logging;
1414 PurpleBlistNode *node;
1416 conv = pidgin_conv_window_get_active_conversation(win);
1418 if (conv == NULL)
1419 return;
1421 logging = gtk_toggle_action_get_active(GTK_TOGGLE_ACTION(action));
1423 if (logging == purple_conversation_is_logging(conv))
1424 return;
1426 node = get_conversation_blist_node(conv);
1428 if (logging)
1430 /* Enable logging first so the message below can be logged. */
1431 purple_conversation_set_logging(conv, TRUE);
1433 purple_conversation_write_system_message(conv,
1434 _("Logging started. Future messages in this conversation will be logged."), 0);
1436 else
1438 purple_conversation_write_system_message(conv,
1439 _("Logging stopped. Future messages in this conversation will not be logged."), 0);
1441 /* Disable the logging second, so that the above message can be logged. */
1442 purple_conversation_set_logging(conv, FALSE);
1445 /* Save the setting IFF it's different than the pref. */
1446 if (PURPLE_IS_IM_CONVERSATION(conv)) {
1447 if (logging == purple_prefs_get_bool("/purple/logging/log_ims"))
1448 purple_blist_node_remove_setting(node, "enable-logging");
1449 else
1450 purple_blist_node_set_bool(node, "enable-logging", logging);
1451 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
1452 if (logging == purple_prefs_get_bool("/purple/logging/log_chats"))
1453 purple_blist_node_remove_setting(node, "enable-logging");
1454 else
1455 purple_blist_node_set_bool(node, "enable-logging", logging);
1459 static void
1460 menu_toolbar_cb(GtkAction *action, gpointer data)
1462 purple_prefs_set_bool(PIDGIN_PREFS_ROOT "/conversations/show_formatting_toolbar",
1463 gtk_toggle_action_get_active(GTK_TOGGLE_ACTION(action)));
1466 static void
1467 menu_sounds_cb(GtkAction *action, gpointer data)
1469 PidginConvWindow *win = data;
1470 PurpleConversation *conv;
1471 PidginConversation *gtkconv;
1472 PurpleBlistNode *node;
1474 conv = pidgin_conv_window_get_active_conversation(win);
1476 if (!conv)
1477 return;
1479 gtkconv = PIDGIN_CONVERSATION(conv);
1481 gtkconv->make_sound =
1482 gtk_toggle_action_get_active(GTK_TOGGLE_ACTION(action));
1483 node = get_conversation_blist_node(conv);
1484 if (node)
1485 purple_blist_node_set_bool(node, "gtk-mute-sound", !gtkconv->make_sound);
1488 static void
1489 chat_do_im(PidginConversation *gtkconv, const char *who)
1491 PurpleConversation *conv = gtkconv->active_conv;
1492 PurpleAccount *account;
1493 PurpleConnection *gc;
1494 PurpleProtocol *protocol = NULL;
1495 gchar *real_who = NULL;
1497 account = purple_conversation_get_account(conv);
1498 g_return_if_fail(account != NULL);
1500 gc = purple_account_get_connection(account);
1501 g_return_if_fail(gc != NULL);
1503 protocol = purple_connection_get_protocol(gc);
1505 if (protocol)
1506 real_who = purple_protocol_chat_iface_get_user_real_name(protocol, gc,
1507 purple_chat_conversation_get_id(PURPLE_CHAT_CONVERSATION(conv)), who);
1509 if(!who && !real_who)
1510 return;
1512 pidgin_dialogs_im_with_user(account, real_who ? real_who : who);
1514 g_free(real_who);
1517 static void pidgin_conv_chat_update_user(PurpleChatUser *chatuser);
1519 static void
1520 ignore_cb(GtkWidget *w, PidginConversation *gtkconv)
1522 PurpleChatConversation *chat = PURPLE_CHAT_CONVERSATION(gtkconv->active_conv);
1523 const char *name;
1525 name = g_object_get_data(G_OBJECT(w), "user_data");
1527 if (name == NULL)
1528 return;
1530 if (purple_chat_conversation_is_ignored_user(chat, name))
1531 purple_chat_conversation_unignore(chat, name);
1532 else
1533 purple_chat_conversation_ignore(chat, name);
1535 pidgin_conv_chat_update_user(purple_chat_conversation_find_user(chat, name));
1538 static void
1539 menu_chat_im_cb(GtkWidget *w, PidginConversation *gtkconv)
1541 const char *who = g_object_get_data(G_OBJECT(w), "user_data");
1543 chat_do_im(gtkconv, who);
1546 static void
1547 menu_chat_send_file_cb(GtkWidget *w, PidginConversation *gtkconv)
1549 PurpleProtocol *protocol;
1550 PurpleConversation *conv = gtkconv->active_conv;
1551 const char *who = g_object_get_data(G_OBJECT(w), "user_data");
1552 PurpleConnection *gc = purple_conversation_get_connection(conv);
1553 gchar *real_who = NULL;
1555 g_return_if_fail(gc != NULL);
1557 protocol = purple_connection_get_protocol(gc);
1559 if (protocol)
1560 real_who = purple_protocol_chat_iface_get_user_real_name(protocol, gc,
1561 purple_chat_conversation_get_id(PURPLE_CHAT_CONVERSATION(conv)), who);
1563 purple_serv_send_file(gc, real_who ? real_who : who, NULL);
1564 g_free(real_who);
1567 static void
1568 menu_chat_info_cb(GtkWidget *w, PidginConversation *gtkconv)
1570 char *who;
1572 who = g_object_get_data(G_OBJECT(w), "user_data");
1574 chat_do_info(gtkconv, who);
1577 static void
1578 menu_chat_add_remove_cb(GtkWidget *w, PidginConversation *gtkconv)
1580 PurpleConversation *conv = gtkconv->active_conv;
1581 PurpleAccount *account;
1582 PurpleBuddy *b;
1583 char *name;
1585 account = purple_conversation_get_account(conv);
1586 name = g_object_get_data(G_OBJECT(w), "user_data");
1587 b = purple_blist_find_buddy(account, name);
1589 if (b != NULL)
1590 pidgin_dialogs_remove_buddy(b);
1591 else if (account != NULL && purple_account_is_connected(account))
1592 purple_blist_request_add_buddy(account, name, NULL, NULL);
1594 gtk_widget_grab_focus(PIDGIN_CONVERSATION(conv)->entry);
1597 static char *
1598 get_class_for_user(const char *who)
1600 return g_strconcat("-pidgin-user:", who, NULL);
1603 static WebKitDOMNode *
1604 get_mark_for_user(PidginConversation *gtkconv, const char *who)
1606 WebKitDOMDocument *doc;
1607 WebKitDOMNodeList *nodes;
1608 WebKitDOMNode *node = NULL;
1609 gulong len;
1610 char *tmp;
1612 doc = webkit_web_view_get_dom_document(WEBKIT_WEB_VIEW(gtkconv->webview));
1614 tmp = get_class_for_user(who);
1615 nodes = webkit_dom_document_get_elements_by_class_name(doc, tmp);
1616 g_free(tmp);
1618 if (nodes != NULL) {
1619 len = webkit_dom_node_list_get_length(nodes);
1620 if (len > 0)
1621 node = webkit_dom_node_list_item(nodes, len - 1);
1624 g_object_unref(nodes);
1626 return node;
1629 static void
1630 menu_last_said_cb(GtkWidget *w, PidginConversation *gtkconv)
1632 WebKitDOMNode *node;
1633 const char *who;
1635 who = g_object_get_data(G_OBJECT(w), "user_data");
1636 node = get_mark_for_user(gtkconv, who);
1638 if (node != NULL)
1639 webkit_dom_element_scroll_into_view(WEBKIT_DOM_ELEMENT(node), TRUE);
1640 else
1641 g_return_if_reached();
1644 static GtkWidget *
1645 create_chat_menu(PurpleChatConversation *chat, const char *who, PurpleConnection *gc)
1647 static GtkWidget *menu = NULL;
1648 PurpleProtocol *protocol = NULL;
1649 PurpleConversation *conv = PURPLE_CONVERSATION(chat);
1650 PurpleAccount *account = purple_conversation_get_account(conv);
1651 gboolean is_me = FALSE;
1652 GtkWidget *button;
1653 PurpleBuddy *buddy = NULL;
1655 if (gc != NULL)
1656 protocol = purple_connection_get_protocol(gc);
1659 * If a menu already exists, destroy it before creating a new one,
1660 * thus freeing-up the memory it occupied.
1662 if (menu)
1663 gtk_widget_destroy(menu);
1665 if (!strcmp(purple_chat_conversation_get_nick(chat), purple_normalize(account, who)))
1666 is_me = TRUE;
1668 menu = gtk_menu_new();
1670 if (!is_me) {
1671 button = pidgin_new_menu_item(menu, _("IM"),
1672 PIDGIN_STOCK_TOOLBAR_MESSAGE_NEW,
1673 G_CALLBACK(menu_chat_im_cb),
1674 PIDGIN_CONVERSATION(conv));
1676 if (gc == NULL)
1677 gtk_widget_set_sensitive(button, FALSE);
1678 else
1679 g_object_set_data_full(G_OBJECT(button), "user_data", g_strdup(who), g_free);
1682 if (protocol && PURPLE_PROTOCOL_IMPLEMENTS(protocol, XFER_IFACE, send))
1684 gboolean can_receive_file = TRUE;
1686 button = pidgin_new_menu_item(menu, _("Send File"),
1687 PIDGIN_STOCK_TOOLBAR_SEND_FILE, G_CALLBACK(menu_chat_send_file_cb),
1688 PIDGIN_CONVERSATION(conv));
1690 if (gc == NULL || protocol == NULL)
1691 can_receive_file = FALSE;
1692 else {
1693 gchar *real_who = NULL;
1694 real_who = purple_protocol_chat_iface_get_user_real_name(protocol, gc,
1695 purple_chat_conversation_get_id(chat), who);
1696 if (!(!PURPLE_PROTOCOL_IMPLEMENTS(protocol, XFER_IFACE, can_receive) ||
1697 purple_protocol_xfer_iface_can_receive(protocol, gc, real_who ? real_who : who)))
1698 can_receive_file = FALSE;
1699 g_free(real_who);
1702 if (!can_receive_file)
1703 gtk_widget_set_sensitive(button, FALSE);
1704 else
1705 g_object_set_data_full(G_OBJECT(button), "user_data", g_strdup(who), g_free);
1709 if (purple_chat_conversation_is_ignored_user(chat, who))
1710 button = pidgin_new_menu_item(menu, _("Un-Ignore"),
1711 PIDGIN_STOCK_IGNORE, G_CALLBACK(ignore_cb),
1712 PIDGIN_CONVERSATION(conv));
1713 else
1714 button = pidgin_new_menu_item(menu, _("Ignore"),
1715 PIDGIN_STOCK_IGNORE, G_CALLBACK(ignore_cb),
1716 PIDGIN_CONVERSATION(conv));
1718 if (gc == NULL)
1719 gtk_widget_set_sensitive(button, FALSE);
1720 else
1721 g_object_set_data_full(G_OBJECT(button), "user_data", g_strdup(who), g_free);
1724 if (protocol && PURPLE_PROTOCOL_IMPLEMENTS(protocol, SERVER_IFACE, get_info)) {
1725 button = pidgin_new_menu_item(menu, _("Info"),
1726 PIDGIN_STOCK_TOOLBAR_USER_INFO,
1727 G_CALLBACK(menu_chat_info_cb),
1728 PIDGIN_CONVERSATION(conv));
1730 if (gc == NULL)
1731 gtk_widget_set_sensitive(button, FALSE);
1732 else
1733 g_object_set_data_full(G_OBJECT(button), "user_data", g_strdup(who), g_free);
1736 if (!is_me && protocol && !(purple_protocol_get_options(protocol) & OPT_PROTO_UNIQUE_CHATNAME)) {
1737 if ((buddy = purple_blist_find_buddy(account, who)) != NULL)
1738 button = pidgin_new_menu_item(menu, _("Remove"),
1739 GTK_STOCK_REMOVE,
1740 G_CALLBACK(menu_chat_add_remove_cb),
1741 PIDGIN_CONVERSATION(conv));
1742 else
1743 button = pidgin_new_menu_item(menu, _("Add"),
1744 GTK_STOCK_ADD,
1745 G_CALLBACK(menu_chat_add_remove_cb),
1746 PIDGIN_CONVERSATION(conv));
1748 if (gc == NULL)
1749 gtk_widget_set_sensitive(button, FALSE);
1750 else
1751 g_object_set_data_full(G_OBJECT(button), "user_data", g_strdup(who), g_free);
1754 button = pidgin_new_menu_item(menu, _("Last Said"), GTK_STOCK_INDEX,
1755 G_CALLBACK(menu_last_said_cb), PIDGIN_CONVERSATION(conv));
1756 g_object_set_data_full(G_OBJECT(button), "user_data", g_strdup(who), g_free);
1757 if (!get_mark_for_user(PIDGIN_CONVERSATION(conv), who))
1758 gtk_widget_set_sensitive(button, FALSE);
1760 if (buddy != NULL)
1762 if (purple_account_is_connected(account))
1763 pidgin_append_blist_node_proto_menu(menu, purple_account_get_connection(account),
1764 (PurpleBlistNode *)buddy);
1765 pidgin_append_blist_node_extended_menu(menu, (PurpleBlistNode *)buddy);
1766 gtk_widget_show_all(menu);
1769 return menu;
1773 static gint
1774 gtkconv_chat_popup_menu_cb(GtkWidget *widget, PidginConversation *gtkconv)
1776 PurpleConversation *conv = gtkconv->active_conv;
1777 PidginChatPane *gtkchat;
1778 PurpleConnection *gc;
1779 PurpleAccount *account;
1780 GtkTreeSelection *sel;
1781 GtkTreeIter iter;
1782 GtkTreeModel *model;
1783 GtkWidget *menu;
1784 gchar *who;
1786 gtkconv = PIDGIN_CONVERSATION(conv);
1787 gtkchat = gtkconv->u.chat;
1788 account = purple_conversation_get_account(conv);
1789 gc = purple_account_get_connection(account);
1791 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
1793 sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(gtkchat->list));
1794 if(!gtk_tree_selection_get_selected(sel, NULL, &iter))
1795 return FALSE;
1797 gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, CHAT_USERS_NAME_COLUMN, &who, -1);
1798 menu = create_chat_menu (PURPLE_CHAT_CONVERSATION(conv), who, gc);
1799 gtk_menu_popup(GTK_MENU(menu), NULL, NULL,
1800 pidgin_treeview_popup_menu_position_func, widget,
1801 0, GDK_CURRENT_TIME);
1802 g_free(who);
1804 return TRUE;
1808 static gint
1809 right_click_chat_cb(GtkWidget *widget, GdkEventButton *event,
1810 PidginConversation *gtkconv)
1812 PurpleConversation *conv = gtkconv->active_conv;
1813 PidginChatPane *gtkchat;
1814 PurpleConnection *gc;
1815 PurpleAccount *account;
1816 GtkTreePath *path;
1817 GtkTreeIter iter;
1818 GtkTreeModel *model;
1819 GtkTreeViewColumn *column;
1820 gchar *who;
1821 int x, y;
1823 gtkchat = gtkconv->u.chat;
1824 account = purple_conversation_get_account(conv);
1825 gc = purple_account_get_connection(account);
1827 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
1829 gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(gtkchat->list),
1830 event->x, event->y, &path, &column, &x, &y);
1832 if (path == NULL)
1833 return FALSE;
1835 gtk_tree_selection_select_path(GTK_TREE_SELECTION(
1836 gtk_tree_view_get_selection(GTK_TREE_VIEW(gtkchat->list))), path);
1837 gtk_tree_view_set_cursor(GTK_TREE_VIEW(gtkchat->list),
1838 path, NULL, FALSE);
1839 gtk_widget_grab_focus(GTK_WIDGET(gtkchat->list));
1841 gtk_tree_model_get_iter(GTK_TREE_MODEL(model), &iter, path);
1842 gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, CHAT_USERS_NAME_COLUMN, &who, -1);
1844 /* emit chat-nick-clicked signal */
1845 if (event->type == GDK_BUTTON_PRESS) {
1846 gint plugin_return = GPOINTER_TO_INT(purple_signal_emit_return_1(
1847 pidgin_conversations_get_handle(), "chat-nick-clicked",
1848 conv, who, event->button));
1849 if (plugin_return)
1850 goto handled;
1853 if (event->button == 1 && event->type == GDK_2BUTTON_PRESS) {
1854 chat_do_im(gtkconv, who);
1855 } else if (event->button == 2 && event->type == GDK_BUTTON_PRESS) {
1856 /* Move to user's anchor */
1857 WebKitDOMNode *node = get_mark_for_user(gtkconv, who);
1859 if (node != NULL)
1860 webkit_dom_element_scroll_into_view(WEBKIT_DOM_ELEMENT(node), TRUE);
1862 } else if (event->button == 3 && event->type == GDK_BUTTON_PRESS) {
1863 GtkWidget *menu = create_chat_menu (PURPLE_CHAT_CONVERSATION(conv), who, gc);
1864 gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL,
1865 event->button, event->time);
1868 handled:
1869 g_free(who);
1870 gtk_tree_path_free(path);
1872 return TRUE;
1875 static void
1876 activate_list_cb(GtkTreeView *list, GtkTreePath *path, GtkTreeViewColumn *column, PidginConversation *gtkconv)
1878 GtkTreeIter iter;
1879 GtkTreeModel *model;
1880 gchar *who;
1882 model = gtk_tree_view_get_model(GTK_TREE_VIEW(list));
1884 gtk_tree_model_get_iter(GTK_TREE_MODEL(model), &iter, path);
1885 gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, CHAT_USERS_NAME_COLUMN, &who, -1);
1886 chat_do_im(gtkconv, who);
1888 g_free(who);
1891 static void
1892 move_to_next_unread_tab(PidginConversation *gtkconv, gboolean forward)
1894 PidginConversation *next_gtkconv = NULL, *most_active = NULL;
1895 PidginUnseenState unseen_state = PIDGIN_UNSEEN_NONE;
1896 PidginConvWindow *win;
1897 int initial, i, total, diff;
1899 win = gtkconv->win;
1900 initial = gtk_notebook_page_num(GTK_NOTEBOOK(win->notebook),
1901 gtkconv->tab_cont);
1902 total = pidgin_conv_window_get_gtkconv_count(win);
1903 /* By adding total here, the moduli calculated later will always have two
1904 * positive arguments. x % y where x < 0 is not guaranteed to return a
1905 * positive number.
1907 diff = (forward ? 1 : -1) + total;
1909 for (i = (initial + diff) % total; i != initial; i = (i + diff) % total) {
1910 next_gtkconv = pidgin_conv_window_get_gtkconv_at_index(win, i);
1911 if (next_gtkconv->unseen_state > unseen_state) {
1912 most_active = next_gtkconv;
1913 unseen_state = most_active->unseen_state;
1914 if(PIDGIN_UNSEEN_NICK == unseen_state) /* highest possible state */
1915 break;
1919 if (most_active == NULL) { /* no new messages */
1920 i = (i + diff) % total;
1921 most_active = pidgin_conv_window_get_gtkconv_at_index(win, i);
1924 if (most_active != NULL && most_active != gtkconv)
1925 pidgin_conv_window_switch_gtkconv(win, most_active);
1928 static gboolean
1929 gtkconv_cycle_focus(PidginConversation *gtkconv, GtkDirectionType dir)
1931 PurpleConversation *conv = gtkconv->active_conv;
1932 gboolean chat = PURPLE_IS_CHAT_CONVERSATION(conv);
1933 GtkWidget *next = NULL;
1934 struct {
1935 GtkWidget *from;
1936 GtkWidget *to;
1937 } transitions[] = {
1938 {gtkconv->entry, gtkconv->webview},
1939 {gtkconv->webview, chat ? gtkconv->u.chat->list : gtkconv->entry},
1940 {chat ? gtkconv->u.chat->list : NULL, gtkconv->entry},
1941 {NULL, NULL}
1942 }, *ptr;
1944 for (ptr = transitions; !next && ptr->from; ptr++) {
1945 GtkWidget *from, *to;
1946 if (dir == GTK_DIR_TAB_FORWARD) {
1947 from = ptr->from;
1948 to = ptr->to;
1949 } else {
1950 from = ptr->to;
1951 to = ptr->from;
1953 if (gtk_widget_is_focus(from))
1954 next = to;
1957 if (next)
1958 gtk_widget_grab_focus(next);
1959 return !!next;
1962 static void
1963 update_typing_inserting(PidginConversation *gtkconv)
1965 gboolean is_empty;
1967 g_return_if_fail(gtkconv != NULL);
1969 is_empty = pidgin_webview_is_empty(PIDGIN_WEBVIEW(gtkconv->entry));
1971 got_typing_keypress(gtkconv, is_empty);
1974 static gboolean
1975 update_typing_deleting_cb(PidginConversation *gtkconv)
1977 PurpleIMConversation *im = PURPLE_IM_CONVERSATION(gtkconv->active_conv);
1978 gboolean is_empty = pidgin_webview_is_empty(PIDGIN_WEBVIEW(gtkconv->entry));
1980 if (!is_empty) {
1981 /* We deleted all the text, so turn off typing. */
1982 purple_im_conversation_stop_send_typed_timeout(im);
1984 purple_serv_send_typing(purple_conversation_get_connection(gtkconv->active_conv),
1985 purple_conversation_get_name(gtkconv->active_conv),
1986 PURPLE_IM_NOT_TYPING);
1988 else {
1989 /* We're deleting, but not all of it, so it counts as typing. */
1990 got_typing_keypress(gtkconv, FALSE);
1993 return FALSE;
1996 static void
1997 update_typing_deleting(PidginConversation *gtkconv)
1999 gboolean is_empty;
2001 g_return_if_fail(gtkconv != NULL);
2003 is_empty = pidgin_webview_is_empty(PIDGIN_WEBVIEW(gtkconv->entry));
2005 if (!is_empty)
2006 purple_timeout_add(0, (GSourceFunc)update_typing_deleting_cb, gtkconv);
2009 static gboolean
2010 conv_keypress_common(PidginConversation *gtkconv, GdkEventKey *event)
2012 PidginConvWindow *win;
2013 int curconv;
2015 win = gtkconv->win;
2016 curconv = gtk_notebook_get_current_page(GTK_NOTEBOOK(win->notebook));
2018 /* clear any tooltips */
2019 pidgin_tooltip_destroy();
2021 /* If CTRL was held down... */
2022 if (event->state & GDK_CONTROL_MASK) {
2023 switch (event->keyval) {
2024 case GDK_KEY_Page_Down:
2025 case GDK_KEY_KP_Page_Down:
2026 case ']':
2027 if (!pidgin_conv_window_get_gtkconv_at_index(win, curconv + 1))
2028 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), 0);
2029 else
2030 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), curconv + 1);
2031 return TRUE;
2032 break;
2034 case GDK_KEY_Page_Up:
2035 case GDK_KEY_KP_Page_Up:
2036 case '[':
2037 if (!pidgin_conv_window_get_gtkconv_at_index(win, curconv - 1))
2038 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), -1);
2039 else
2040 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), curconv - 1);
2041 return TRUE;
2042 break;
2044 case GDK_KEY_Tab:
2045 case GDK_KEY_KP_Tab:
2046 case GDK_KEY_ISO_Left_Tab:
2047 if (event->state & GDK_SHIFT_MASK) {
2048 move_to_next_unread_tab(gtkconv, FALSE);
2049 } else {
2050 move_to_next_unread_tab(gtkconv, TRUE);
2053 return TRUE;
2054 break;
2056 case GDK_KEY_comma:
2057 gtk_notebook_reorder_child(GTK_NOTEBOOK(win->notebook),
2058 gtk_notebook_get_nth_page(GTK_NOTEBOOK(win->notebook), curconv),
2059 curconv - 1);
2060 return TRUE;
2061 break;
2063 case GDK_KEY_period:
2064 gtk_notebook_reorder_child(GTK_NOTEBOOK(win->notebook),
2065 gtk_notebook_get_nth_page(GTK_NOTEBOOK(win->notebook), curconv),
2066 (curconv + 1) % gtk_notebook_get_n_pages(GTK_NOTEBOOK(win->notebook)));
2067 return TRUE;
2068 break;
2069 case GDK_KEY_F6:
2070 if (gtkconv_cycle_focus(gtkconv, event->state & GDK_SHIFT_MASK ? GTK_DIR_TAB_BACKWARD : GTK_DIR_TAB_FORWARD))
2071 return TRUE;
2072 break;
2073 } /* End of switch */
2076 /* If ALT (or whatever) was held down... */
2077 else if (event->state & GDK_MOD1_MASK)
2079 if (event->keyval > '0' && event->keyval <= '9')
2081 guint switchto = event->keyval - '1';
2082 if (switchto < pidgin_conv_window_get_gtkconv_count(win))
2083 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), switchto);
2085 return TRUE;
2089 /* If neither CTRL nor ALT were held down... */
2090 else
2092 switch (event->keyval) {
2093 case GDK_KEY_F2:
2094 if (gtk_widget_is_focus(GTK_WIDGET(win->notebook))) {
2095 infopane_entry_activate(gtkconv);
2096 return TRUE;
2098 break;
2099 case GDK_KEY_F6:
2100 if (gtkconv_cycle_focus(gtkconv, event->state & GDK_SHIFT_MASK ? GTK_DIR_TAB_BACKWARD : GTK_DIR_TAB_FORWARD))
2101 return TRUE;
2102 break;
2105 return FALSE;
2108 static gboolean
2109 entry_key_press_cb(GtkWidget *entry, GdkEventKey *event, gpointer data)
2111 PurpleConversation *conv;
2112 PidginConversation *gtkconv;
2114 gtkconv = (PidginConversation *)data;
2115 conv = gtkconv->active_conv;
2117 if (conv_keypress_common(gtkconv, event))
2118 return TRUE;
2120 /* If CTRL was held down... */
2121 if (event->state & GDK_CONTROL_MASK) {
2122 switch (event->keyval) {
2123 case GDK_KEY_Up:
2124 if (!gtkconv->send_history)
2125 break;
2127 if (gtkconv->entry != entry)
2128 break;
2130 if (!gtkconv->send_history->prev) {
2131 g_free(gtkconv->send_history->data);
2133 gtkconv->send_history->data =
2134 pidgin_webview_get_body_html(PIDGIN_WEBVIEW(gtkconv->entry));
2137 if (gtkconv->send_history->next && gtkconv->send_history->next->data) {
2138 GObject *object;
2139 #if 0
2140 /* TODO WebKit: maybe not necessary? */
2141 GtkTextIter iter;
2142 GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(gtkconv->entry));
2143 #endif
2145 gtkconv->send_history = gtkconv->send_history->next;
2147 /* Block the signal to prevent application of default formatting. */
2148 object = g_object_ref(G_OBJECT(gtkconv->entry));
2149 g_signal_handlers_block_matched(object, G_SIGNAL_MATCH_DATA,
2150 0, 0, NULL, NULL, gtkconv);
2151 /* Clear the formatting. */
2152 pidgin_webview_clear_formatting(PIDGIN_WEBVIEW(gtkconv->entry));
2153 /* Unblock the signal. */
2154 g_signal_handlers_unblock_matched(object, G_SIGNAL_MATCH_DATA,
2155 0, 0, NULL, NULL, gtkconv);
2156 g_object_unref(object);
2158 pidgin_webview_load_html_string(PIDGIN_WEBVIEW(gtkconv->entry),
2159 gtkconv->send_history->data);
2160 /* this is mainly just a hack so the formatting at the
2161 * cursor gets picked up. */
2162 #if 0
2163 /* TODO WebKit: maybe not necessary? */
2164 gtk_text_buffer_get_end_iter(buffer, &iter);
2165 gtk_text_buffer_move_mark_by_name(buffer, "insert", &iter);
2166 #endif
2169 return TRUE;
2170 break;
2172 case GDK_KEY_Down:
2173 if (!gtkconv->send_history)
2174 break;
2176 if (gtkconv->entry != entry)
2177 break;
2179 if (gtkconv->send_history->prev && gtkconv->send_history->prev->data) {
2180 GObject *object;
2181 #if 0
2182 /* TODO WebKit: maybe not necessary? */
2183 GtkTextIter iter;
2184 GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(gtkconv->entry));
2185 #endif
2187 gtkconv->send_history = gtkconv->send_history->prev;
2189 /* Block the signal to prevent application of default formatting. */
2190 object = g_object_ref(G_OBJECT(gtkconv->entry));
2191 g_signal_handlers_block_matched(object, G_SIGNAL_MATCH_DATA,
2192 0, 0, NULL, NULL, gtkconv);
2193 /* Clear the formatting. */
2194 pidgin_webview_clear_formatting(PIDGIN_WEBVIEW(gtkconv->entry));
2195 /* Unblock the signal. */
2196 g_signal_handlers_unblock_matched(object, G_SIGNAL_MATCH_DATA,
2197 0, 0, NULL, NULL, gtkconv);
2198 g_object_unref(object);
2200 pidgin_webview_load_html_string(PIDGIN_WEBVIEW(gtkconv->entry),
2201 gtkconv->send_history->data);
2202 /* this is mainly just a hack so the formatting at the
2203 * cursor gets picked up. */
2204 if (*(char *)gtkconv->send_history->data) {
2205 #if 0
2206 /* TODO WebKit: maybe not necessary? */
2207 gtk_text_buffer_get_end_iter(buffer, &iter);
2208 gtk_text_buffer_move_mark_by_name(buffer, "insert", &iter);
2209 #endif
2210 } else {
2211 /* Restore the default formatting */
2212 default_formatize(gtkconv);
2216 return TRUE;
2217 break;
2218 } /* End of switch */
2221 /* If ALT (or whatever) was held down... */
2222 else if (event->state & GDK_MOD1_MASK) {
2226 /* If neither CTRL nor ALT were held down... */
2227 else {
2228 switch (event->keyval) {
2229 case GDK_KEY_Tab:
2230 case GDK_KEY_KP_Tab:
2231 case GDK_KEY_ISO_Left_Tab:
2232 if (gtkconv->entry != entry)
2233 break;
2235 gint plugin_return;
2236 plugin_return = GPOINTER_TO_INT(purple_signal_emit_return_1(
2237 pidgin_conversations_get_handle(), "chat-nick-autocomplete",
2238 conv, event->state & GDK_SHIFT_MASK));
2239 return plugin_return ? TRUE : tab_complete(conv);
2241 break;
2243 case GDK_KEY_Page_Up:
2244 case GDK_KEY_KP_Page_Up:
2245 pidgin_webview_page_up(PIDGIN_WEBVIEW(gtkconv->webview));
2246 return TRUE;
2247 break;
2249 case GDK_KEY_Page_Down:
2250 case GDK_KEY_KP_Page_Down:
2251 pidgin_webview_page_down(PIDGIN_WEBVIEW(gtkconv->webview));
2252 return TRUE;
2253 break;
2255 case GDK_KEY_KP_Enter:
2256 case GDK_KEY_Return:
2257 send_cb(entry, gtkconv);
2258 return TRUE;
2259 break;
2264 if (PURPLE_IS_IM_CONVERSATION(conv) &&
2265 purple_prefs_get_bool("/purple/conversations/im/send_typing")) {
2267 switch (event->keyval) {
2268 case GDK_KEY_BackSpace:
2269 case GDK_KEY_Delete:
2270 case GDK_KEY_KP_Delete:
2271 update_typing_deleting(gtkconv);
2272 break;
2273 default:
2274 update_typing_inserting(gtkconv);
2278 return FALSE;
2282 * NOTE:
2283 * This guy just kills a single right click from being propagated any
2284 * further. I have no idea *why* we need this, but we do ... It
2285 * prevents right clicks on the GtkTextView in a convo dialog from
2286 * going all the way down to the notebook. I suspect a bug in
2287 * GtkTextView, but I'm not ready to point any fingers yet.
2289 static gboolean
2290 entry_stop_rclick_cb(GtkWidget *widget, GdkEventButton *event, gpointer data)
2292 if (event->button == 3 && event->type == GDK_BUTTON_PRESS) {
2293 /* Right single click */
2294 g_signal_stop_emission_by_name(G_OBJECT(widget), "button_press_event");
2296 return TRUE;
2299 return FALSE;
2303 * If someone tries to type into the conversation backlog of a
2304 * conversation window then we yank focus from the conversation backlog
2305 * and give it to the text entry box so that people can type
2306 * all the live long day and it will get entered into the entry box.
2308 static gboolean
2309 refocus_entry_cb(GtkWidget *widget, GdkEventKey *event, gpointer data)
2311 PidginConversation *gtkconv = data;
2313 /* If we have a valid key for the conversation display, then exit */
2314 if ((event->state & GDK_CONTROL_MASK) ||
2315 (event->keyval == GDK_KEY_F6) ||
2316 (event->keyval == GDK_KEY_F10) ||
2317 (event->keyval == GDK_KEY_Menu) ||
2318 (event->keyval == GDK_KEY_Shift_L) ||
2319 (event->keyval == GDK_KEY_Shift_R) ||
2320 (event->keyval == GDK_KEY_Control_L) ||
2321 (event->keyval == GDK_KEY_Control_R) ||
2322 (event->keyval == GDK_KEY_Escape) ||
2323 (event->keyval == GDK_KEY_Up) ||
2324 (event->keyval == GDK_KEY_Down) ||
2325 (event->keyval == GDK_KEY_Left) ||
2326 (event->keyval == GDK_KEY_Right) ||
2327 (event->keyval == GDK_KEY_Page_Up) ||
2328 (event->keyval == GDK_KEY_KP_Page_Up) ||
2329 (event->keyval == GDK_KEY_Page_Down) ||
2330 (event->keyval == GDK_KEY_KP_Page_Down) ||
2331 (event->keyval == GDK_KEY_Home) ||
2332 (event->keyval == GDK_KEY_End) ||
2333 (event->keyval == GDK_KEY_Tab) ||
2334 (event->keyval == GDK_KEY_KP_Tab) ||
2335 (event->keyval == GDK_KEY_ISO_Left_Tab))
2337 if (event->type == GDK_KEY_PRESS)
2338 return conv_keypress_common(gtkconv, event);
2339 return FALSE;
2342 gtk_widget_grab_focus(gtkconv->entry);
2343 gtk_widget_event(gtkconv->entry, (GdkEvent *)event);
2345 return TRUE;
2348 static void
2349 regenerate_options_items(PidginConvWindow *win);
2351 void
2352 pidgin_conv_switch_active_conversation(PurpleConversation *conv)
2354 PidginConversation *gtkconv;
2355 PurpleConversation *old_conv;
2356 PidginWebView *entry;
2357 PurpleConnectionFlags features;
2359 g_return_if_fail(conv != NULL);
2361 gtkconv = PIDGIN_CONVERSATION(conv);
2362 old_conv = gtkconv->active_conv;
2364 purple_debug_info("gtkconv", "setting active conversation on toolbar %p\n",
2365 conv);
2367 if (old_conv == conv)
2368 return;
2370 purple_conversation_close_logs(old_conv);
2371 gtkconv->active_conv = conv;
2373 pidgin_webview_switch_active_conversation(
2374 PIDGIN_WEBVIEW(gtkconv->entry), conv);
2375 pidgin_webview_switch_active_conversation(
2376 PIDGIN_WEBVIEW(gtkconv->webview), conv);
2377 purple_conversation_set_logging(conv,
2378 gtk_toggle_action_get_active(GTK_TOGGLE_ACTION(gtkconv->win->menu->logging)));
2380 entry = PIDGIN_WEBVIEW(gtkconv->entry);
2382 features = purple_conversation_get_features(conv);
2383 if (!(features & PURPLE_CONNECTION_FLAG_HTML))
2384 pidgin_webview_clear_formatting(PIDGIN_WEBVIEW(gtkconv->entry));
2385 else if (features & PURPLE_CONNECTION_FLAG_FORMATTING_WBFO &&
2386 !(purple_conversation_get_features(old_conv) & PURPLE_CONNECTION_FLAG_FORMATTING_WBFO))
2388 /* The old conversation allowed formatting on parts of the
2389 * buffer, but the new one only allows it on the whole
2390 * buffer. This code saves the formatting from the current
2391 * position of the cursor, clears the formatting, then
2392 * applies the saved formatting to the entire buffer. */
2394 gboolean bold;
2395 gboolean italic;
2396 gboolean underline;
2397 gboolean strike;
2398 char *fontface = pidgin_webview_get_current_fontface(entry);
2399 char *forecolor = pidgin_webview_get_current_forecolor(entry);
2400 char *backcolor = pidgin_webview_get_current_backcolor(entry);
2401 #if 0
2402 /* TODO WebKit: Do we need this again? */
2403 char *background = pidgin_webview_get_current_background(entry);
2404 #endif
2405 gint fontsize = pidgin_webview_get_current_fontsize(entry);
2406 gboolean bold2;
2407 gboolean italic2;
2408 gboolean underline2;
2409 gboolean strike2;
2411 pidgin_webview_get_current_format(entry, &bold, &italic, &underline, &strike);
2413 /* Clear existing formatting */
2414 pidgin_webview_clear_formatting(entry);
2416 /* Apply saved formatting to the whole buffer. */
2418 pidgin_webview_get_current_format(entry, &bold2, &italic2, &underline2, &strike2);
2420 if (bold != bold2)
2421 pidgin_webview_toggle_bold(entry);
2423 if (italic != italic2)
2424 pidgin_webview_toggle_italic(entry);
2426 if (underline != underline2)
2427 pidgin_webview_toggle_underline(entry);
2429 if (strike != strike2)
2430 pidgin_webview_toggle_strike(entry);
2432 pidgin_webview_toggle_fontface(entry, fontface);
2434 if (!(features & PURPLE_CONNECTION_FLAG_NO_FONTSIZE))
2435 pidgin_webview_font_set_size(entry, fontsize);
2437 pidgin_webview_toggle_forecolor(entry, forecolor);
2439 if (!(features & PURPLE_CONNECTION_FLAG_NO_BGCOLOR))
2441 pidgin_webview_toggle_backcolor(entry, backcolor);
2442 #if 0
2443 pidgin_webview_toggle_background(entry, background);
2444 #endif
2447 g_free(fontface);
2448 g_free(forecolor);
2449 g_free(backcolor);
2450 #if 0
2451 g_free(background);
2452 #endif
2454 else
2456 /* This is done in default_formatize, which is called from clear_formatting_cb,
2457 * which is (obviously) a clear_formatting signal handler. However, if we're
2458 * here, we didn't call pidgin_webview_clear_formatting() (because we want to
2459 * preserve the formatting exactly as it is), so we have to do this now. */
2460 pidgin_webview_set_whole_buffer_formatting_only(entry,
2461 (features & PURPLE_CONNECTION_FLAG_FORMATTING_WBFO));
2464 purple_signal_emit(pidgin_conversations_get_handle(), "conversation-switched", conv);
2466 gray_stuff_out(gtkconv);
2467 update_typing_icon(gtkconv);
2468 g_object_set_data(G_OBJECT(entry), "transient_buddy", NULL);
2469 regenerate_options_items(gtkconv->win);
2471 gtk_window_set_title(GTK_WINDOW(gtkconv->win->window),
2472 gtk_label_get_text(GTK_LABEL(gtkconv->tab_label)));
2475 static void
2476 menu_conv_sel_send_cb(GObject *m, gpointer data)
2478 PurpleAccount *account = g_object_get_data(m, "purple_account");
2479 gchar *name = g_object_get_data(m, "purple_buddy_name");
2480 PurpleIMConversation *im;
2482 if (gtk_check_menu_item_get_active((GtkCheckMenuItem*) m) == FALSE)
2483 return;
2485 im = purple_im_conversation_new(account, name);
2486 pidgin_conv_switch_active_conversation(PURPLE_CONVERSATION(im));
2489 /**************************************************************************
2490 * A bunch of buddy icon functions
2491 **************************************************************************/
2493 static GList *get_protocol_icon_list(PurpleAccount *account)
2495 GList *l = NULL;
2496 PurpleProtocol *protocol =
2497 purple_protocols_find(purple_account_get_protocol_id(account));
2498 const char *protoname = purple_protocol_class_list_icon(protocol, account, NULL);
2499 l = g_hash_table_lookup(protocol_lists, protoname);
2500 if (l)
2501 return l;
2503 l = g_list_prepend(l, pidgin_create_protocol_icon(account, PIDGIN_PROTOCOL_ICON_LARGE));
2504 l = g_list_prepend(l, pidgin_create_protocol_icon(account, PIDGIN_PROTOCOL_ICON_MEDIUM));
2505 l = g_list_prepend(l, pidgin_create_protocol_icon(account, PIDGIN_PROTOCOL_ICON_SMALL));
2507 g_hash_table_insert(protocol_lists, g_strdup(protoname), l);
2508 return l;
2511 static GList *
2512 pidgin_conv_get_tab_icons(PurpleConversation *conv)
2514 PurpleAccount *account = NULL;
2515 const char *name = NULL;
2517 g_return_val_if_fail(conv != NULL, NULL);
2519 account = purple_conversation_get_account(conv);
2520 name = purple_conversation_get_name(conv);
2522 g_return_val_if_fail(account != NULL, NULL);
2523 g_return_val_if_fail(name != NULL, NULL);
2525 /* Use the buddy icon, if possible */
2526 if (PURPLE_IS_IM_CONVERSATION(conv)) {
2527 PurpleBuddy *b = purple_blist_find_buddy(account, name);
2528 if (b != NULL) {
2529 PurplePresence *p;
2530 p = purple_buddy_get_presence(b);
2531 if (purple_presence_is_status_primitive_active(p, PURPLE_STATUS_AWAY))
2532 return away_list;
2533 if (purple_presence_is_status_primitive_active(p, PURPLE_STATUS_UNAVAILABLE))
2534 return busy_list;
2535 if (purple_presence_is_status_primitive_active(p, PURPLE_STATUS_EXTENDED_AWAY))
2536 return xa_list;
2537 if (purple_presence_is_status_primitive_active(p, PURPLE_STATUS_OFFLINE))
2538 return offline_list;
2539 else
2540 return available_list;
2544 return get_protocol_icon_list(account);
2547 static const char *
2548 pidgin_conv_get_icon_stock(PurpleConversation *conv)
2550 PurpleAccount *account = NULL;
2551 const char *stock = NULL;
2553 g_return_val_if_fail(conv != NULL, NULL);
2555 account = purple_conversation_get_account(conv);
2556 g_return_val_if_fail(account != NULL, NULL);
2558 /* Use the buddy icon, if possible */
2559 if (PURPLE_IS_IM_CONVERSATION(conv)) {
2560 const char *name = NULL;
2561 PurpleBuddy *b;
2562 name = purple_conversation_get_name(conv);
2563 b = purple_blist_find_buddy(account, name);
2564 if (b != NULL) {
2565 PurplePresence *p = purple_buddy_get_presence(b);
2566 PurpleStatus *active = purple_presence_get_active_status(p);
2567 PurpleStatusType *type = purple_status_get_status_type(active);
2568 PurpleStatusPrimitive prim = purple_status_type_get_primitive(type);
2569 stock = pidgin_stock_id_from_status_primitive(prim);
2570 } else {
2571 stock = PIDGIN_STOCK_STATUS_PERSON;
2573 } else {
2574 stock = PIDGIN_STOCK_STATUS_CHAT;
2577 return stock;
2580 static GdkPixbuf *
2581 pidgin_conv_get_icon(PurpleConversation *conv, GtkWidget *parent, const char *icon_size)
2583 PurpleAccount *account = NULL;
2584 const char *name = NULL;
2585 const char *stock = NULL;
2586 GdkPixbuf *status = NULL;
2587 PurpleBlistUiOps *ops = purple_blist_get_ui_ops();
2588 GtkIconSize size;
2590 g_return_val_if_fail(conv != NULL, NULL);
2592 account = purple_conversation_get_account(conv);
2593 name = purple_conversation_get_name(conv);
2595 g_return_val_if_fail(account != NULL, NULL);
2596 g_return_val_if_fail(name != NULL, NULL);
2598 /* Use the buddy icon, if possible */
2599 if (PURPLE_IS_IM_CONVERSATION(conv)) {
2600 PurpleBuddy *b = purple_blist_find_buddy(account, name);
2601 if (b != NULL) {
2602 /* I hate this hack. It fixes a bug where the pending message icon
2603 * displays in the conv tab even though it shouldn't.
2604 * A better solution would be great. */
2605 if (ops && ops->update)
2606 ops->update(NULL, (PurpleBlistNode*)b);
2610 stock = pidgin_conv_get_icon_stock(conv);
2611 size = gtk_icon_size_from_name(icon_size);
2612 status = gtk_widget_render_icon (parent, stock, size, "GtkWidget");
2613 return status;
2616 GdkPixbuf *
2617 pidgin_conv_get_tab_icon(PurpleConversation *conv, gboolean small_icon)
2619 const char *icon_size = small_icon ? PIDGIN_ICON_SIZE_TANGO_MICROSCOPIC : PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL;
2620 return pidgin_conv_get_icon(conv, PIDGIN_CONVERSATION(conv)->icon, icon_size);
2624 static void
2625 update_tab_icon(PurpleConversation *conv)
2627 PidginConversation *gtkconv;
2628 PidginConvWindow *win;
2629 GList *l;
2630 GdkPixbuf *emblem = NULL;
2631 const char *status = NULL;
2632 const char *infopane_status = NULL;
2634 g_return_if_fail(conv != NULL);
2636 gtkconv = PIDGIN_CONVERSATION(conv);
2637 win = gtkconv->win;
2638 if (conv != gtkconv->active_conv)
2639 return;
2641 status = infopane_status = pidgin_conv_get_icon_stock(conv);
2643 if (PURPLE_IS_IM_CONVERSATION(conv)) {
2644 PurpleBuddy *b = purple_blist_find_buddy(purple_conversation_get_account(conv), purple_conversation_get_name(conv));
2645 if (b)
2646 emblem = pidgin_blist_get_emblem((PurpleBlistNode*)b);
2649 g_return_if_fail(status != NULL);
2651 g_object_set(G_OBJECT(gtkconv->icon), "stock", status, NULL);
2652 g_object_set(G_OBJECT(gtkconv->menu_icon), "stock", status, NULL);
2654 gtk_list_store_set(GTK_LIST_STORE(gtkconv->infopane_model),
2655 &(gtkconv->infopane_iter),
2656 CONV_ICON_COLUMN, infopane_status, -1);
2658 gtk_list_store_set(GTK_LIST_STORE(gtkconv->infopane_model),
2659 &(gtkconv->infopane_iter),
2660 CONV_EMBLEM_COLUMN, emblem, -1);
2661 if (emblem)
2662 g_object_unref(emblem);
2664 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/blist/show_protocol_icons")) {
2665 emblem = pidgin_create_protocol_icon(purple_conversation_get_account(gtkconv->active_conv), PIDGIN_PROTOCOL_ICON_SMALL);
2666 } else {
2667 emblem = NULL;
2670 gtk_list_store_set(GTK_LIST_STORE(gtkconv->infopane_model),
2671 &(gtkconv->infopane_iter),
2672 CONV_PROTOCOL_ICON_COLUMN, emblem, -1);
2673 if (emblem)
2674 g_object_unref(emblem);
2676 /* XXX seanegan Why do I have to do this? */
2677 gtk_widget_queue_resize(gtkconv->infopane);
2678 gtk_widget_queue_draw(gtkconv->infopane);
2680 if (pidgin_conv_window_is_active_conversation(conv) &&
2681 (!PURPLE_IS_IM_CONVERSATION(conv) || gtkconv->u.im->anim == NULL))
2683 l = pidgin_conv_get_tab_icons(conv);
2685 gtk_window_set_icon_list(GTK_WINDOW(win->window), l);
2689 #if 0
2690 /* This gets added as an idle handler when doing something that
2691 * redraws the icon. It sets the auto_resize gboolean to TRUE.
2692 * This way, when the size_allocate callback gets triggered, it notices
2693 * that this is an autoresize, and after the main loop iterates, it
2694 * gets set back to FALSE
2696 static gboolean reset_auto_resize_cb(gpointer data)
2698 PidginConversation *gtkconv = (PidginConversation *)data;
2699 gtkconv->auto_resize = FALSE;
2700 return FALSE;
2702 #endif
2704 static gboolean
2705 redraw_icon(gpointer data)
2707 PidginConversation *gtkconv = (PidginConversation *)data;
2708 PurpleConversation *conv = gtkconv->active_conv;
2709 PurpleAccount *account;
2711 GdkPixbuf *buf;
2712 GdkPixbuf *scale;
2713 gint delay;
2714 int scale_width, scale_height;
2715 int size;
2717 gtkconv = PIDGIN_CONVERSATION(conv);
2718 account = purple_conversation_get_account(conv);
2720 if (!(account && purple_account_get_connection(account))) {
2721 gtkconv->u.im->icon_timer = 0;
2722 return FALSE;
2725 gdk_pixbuf_animation_iter_advance(gtkconv->u.im->iter, NULL);
2726 buf = gdk_pixbuf_animation_iter_get_pixbuf(gtkconv->u.im->iter);
2728 scale_width = gdk_pixbuf_get_width(buf);
2729 scale_height = gdk_pixbuf_get_height(buf);
2731 gtk_widget_get_size_request(gtkconv->u.im->icon_container, NULL, &size);
2732 size = MIN(size, MIN(scale_width, scale_height));
2733 size = CLAMP(size, BUDDYICON_SIZE_MIN, BUDDYICON_SIZE_MAX);
2735 if (scale_width == scale_height) {
2736 scale_width = scale_height = size;
2737 } else if (scale_height > scale_width) {
2738 scale_width = size * scale_width / scale_height;
2739 scale_height = size;
2740 } else {
2741 scale_height = size * scale_height / scale_width;
2742 scale_width = size;
2745 scale = gdk_pixbuf_scale_simple(buf, scale_width, scale_height,
2746 GDK_INTERP_BILINEAR);
2747 if (pidgin_gdk_pixbuf_is_opaque(scale))
2748 pidgin_gdk_pixbuf_make_round(scale);
2750 gtk_image_set_from_pixbuf(GTK_IMAGE(gtkconv->u.im->icon), scale);
2751 g_object_unref(G_OBJECT(scale));
2752 gtk_widget_queue_draw(gtkconv->u.im->icon);
2754 delay = gdk_pixbuf_animation_iter_get_delay_time(gtkconv->u.im->iter);
2756 if (delay < 100)
2757 delay = 100;
2759 gtkconv->u.im->icon_timer = g_timeout_add(delay, redraw_icon, gtkconv);
2761 return FALSE;
2764 static void
2765 start_anim(GtkWidget *widget, PidginConversation *gtkconv)
2767 int delay;
2769 if (gtkconv->u.im->anim == NULL)
2770 return;
2772 if (gtkconv->u.im->icon_timer != 0)
2773 return;
2775 if (gdk_pixbuf_animation_is_static_image(gtkconv->u.im->anim))
2776 return;
2778 delay = gdk_pixbuf_animation_iter_get_delay_time(gtkconv->u.im->iter);
2780 if (delay < 100)
2781 delay = 100;
2783 gtkconv->u.im->icon_timer = g_timeout_add(delay, redraw_icon, gtkconv);
2786 static void
2787 remove_icon(GtkWidget *widget, PidginConversation *gtkconv)
2789 GList *children;
2790 GtkWidget *event;
2791 PurpleConversation *conv = gtkconv->active_conv;
2793 g_return_if_fail(conv != NULL);
2795 gtk_widget_set_size_request(gtkconv->u.im->icon_container, -1, BUDDYICON_SIZE_MIN);
2796 children = gtk_container_get_children(GTK_CONTAINER(gtkconv->u.im->icon_container));
2797 if (children) {
2798 /* We know there's only one child here. It'd be nice to shortcut to the
2799 event box, but we can't change the PidginConversation until 3.0 */
2800 event = (GtkWidget *)children->data;
2801 gtk_container_remove(GTK_CONTAINER(gtkconv->u.im->icon_container), event);
2802 g_list_free(children);
2805 if (gtkconv->u.im->anim != NULL)
2806 g_object_unref(G_OBJECT(gtkconv->u.im->anim));
2808 if (gtkconv->u.im->icon_timer != 0)
2809 g_source_remove(gtkconv->u.im->icon_timer);
2811 if (gtkconv->u.im->iter != NULL)
2812 g_object_unref(G_OBJECT(gtkconv->u.im->iter));
2814 gtkconv->u.im->icon_timer = 0;
2815 gtkconv->u.im->icon = NULL;
2816 gtkconv->u.im->anim = NULL;
2817 gtkconv->u.im->iter = NULL;
2818 gtkconv->u.im->show_icon = FALSE;
2821 static void
2822 saveicon_writefile_cb(void *user_data, const char *filename)
2824 PidginConversation *gtkconv = (PidginConversation *)user_data;
2825 PurpleIMConversation *im = PURPLE_IM_CONVERSATION(gtkconv->active_conv);
2826 PurpleBuddyIcon *icon;
2827 const void *data;
2828 size_t len;
2830 icon = purple_im_conversation_get_icon(im);
2831 data = purple_buddy_icon_get_data(icon, &len);
2833 if ((len <= 0) || (data == NULL) || !purple_util_write_data_to_file_absolute(filename, data, len)) {
2834 purple_notify_error(gtkconv, NULL, _("Unable to save icon file to disk."), NULL, NULL);
2838 static void
2839 custom_icon_sel_cb(const char *filename, gpointer data)
2841 if (filename) {
2842 const gchar *name;
2843 PurpleBuddy *buddy;
2844 PurpleContact *contact;
2845 PidginConversation *gtkconv = data;
2846 PurpleConversation *conv = gtkconv->active_conv;
2847 PurpleAccount *account = purple_conversation_get_account(conv);
2849 name = purple_conversation_get_name(conv);
2850 buddy = purple_blist_find_buddy(account, name);
2851 if (!buddy) {
2852 purple_debug_info("custom-icon", "You can only set custom icons for people on your buddylist.\n");
2853 return;
2855 contact = purple_buddy_get_contact(buddy);
2857 purple_buddy_icons_node_set_custom_icon_from_file((PurpleBlistNode*)contact, filename);
2861 static void
2862 set_custom_icon_cb(GtkWidget *widget, PidginConversation *gtkconv)
2864 GtkWidget *win = pidgin_buddy_icon_chooser_new(GTK_WINDOW(gtkconv->win->window),
2865 custom_icon_sel_cb, gtkconv);
2866 gtk_widget_show_all(win);
2869 static void
2870 change_size_cb(GtkWidget *widget, PidginConversation *gtkconv)
2872 int size = 0;
2873 PurpleConversation *conv = gtkconv->active_conv;
2874 GSList *buddies;
2876 gtk_widget_get_size_request(gtkconv->u.im->icon_container, NULL, &size);
2878 if (size == BUDDYICON_SIZE_MAX) {
2879 size = BUDDYICON_SIZE_MIN;
2880 } else {
2881 size = BUDDYICON_SIZE_MAX;
2884 gtk_widget_set_size_request(gtkconv->u.im->icon_container, -1, size);
2885 pidgin_conv_update_buddy_icon(PURPLE_IM_CONVERSATION(conv));
2887 buddies = purple_blist_find_buddies(purple_conversation_get_account(conv),
2888 purple_conversation_get_name(conv));
2889 for (; buddies; buddies = g_slist_delete_link(buddies, buddies)) {
2890 PurpleBuddy *buddy = buddies->data;
2891 PurpleContact *contact = purple_buddy_get_contact(buddy);
2892 purple_blist_node_set_int((PurpleBlistNode*)contact, "pidgin-infopane-iconsize", size);
2896 static void
2897 remove_custom_icon_cb(GtkWidget *widget, PidginConversation *gtkconv)
2899 const gchar *name;
2900 PurpleBuddy *buddy;
2901 PurpleAccount *account;
2902 PurpleContact *contact;
2903 PurpleConversation *conv = gtkconv->active_conv;
2905 account = purple_conversation_get_account(conv);
2906 name = purple_conversation_get_name(conv);
2907 buddy = purple_blist_find_buddy(account, name);
2908 if (!buddy) {
2909 return;
2911 contact = purple_buddy_get_contact(buddy);
2913 purple_buddy_icons_node_set_custom_icon_from_file((PurpleBlistNode*)contact, NULL);
2916 static void
2917 icon_menu_save_cb(GtkWidget *widget, PidginConversation *gtkconv)
2919 PurpleConversation *conv = gtkconv->active_conv;
2920 const gchar *ext;
2921 gchar *buf;
2923 g_return_if_fail(conv != NULL);
2925 ext = purple_buddy_icon_get_extension(purple_im_conversation_get_icon(PURPLE_IM_CONVERSATION(conv)));
2927 buf = g_strdup_printf("%s.%s", purple_normalize(purple_conversation_get_account(conv), purple_conversation_get_name(conv)), ext);
2929 purple_request_file(gtkconv, _("Save Icon"), buf, TRUE,
2930 G_CALLBACK(saveicon_writefile_cb), NULL,
2931 purple_request_cpar_from_conversation(conv), gtkconv);
2933 g_free(buf);
2936 static void
2937 stop_anim(GtkWidget *widget, PidginConversation *gtkconv)
2939 if (gtkconv->u.im->icon_timer != 0)
2940 g_source_remove(gtkconv->u.im->icon_timer);
2942 gtkconv->u.im->icon_timer = 0;
2946 static void
2947 toggle_icon_animate_cb(GtkWidget *w, PidginConversation *gtkconv)
2949 gtkconv->u.im->animate =
2950 gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(w));
2952 if (gtkconv->u.im->animate)
2953 start_anim(NULL, gtkconv);
2954 else
2955 stop_anim(NULL, gtkconv);
2958 static gboolean
2959 icon_menu(GtkWidget *widget, GdkEventButton *e, PidginConversation *gtkconv)
2961 static GtkWidget *menu = NULL;
2962 PurpleConversation *conv;
2963 PurpleBuddy *buddy;
2965 if (e->button == 1 && e->type == GDK_BUTTON_PRESS) {
2966 change_size_cb(NULL, gtkconv);
2967 return TRUE;
2970 if (e->button != 3 || e->type != GDK_BUTTON_PRESS) {
2971 return FALSE;
2975 * If a menu already exists, destroy it before creating a new one,
2976 * thus freeing-up the memory it occupied.
2978 if (menu != NULL)
2979 gtk_widget_destroy(menu);
2981 menu = gtk_menu_new();
2983 if (gtkconv->u.im->anim &&
2984 !(gdk_pixbuf_animation_is_static_image(gtkconv->u.im->anim)))
2986 pidgin_new_check_item(menu, _("Animate"),
2987 G_CALLBACK(toggle_icon_animate_cb), gtkconv,
2988 gtkconv->u.im->icon_timer);
2991 pidgin_new_menu_item(menu, _("Hide Icon"), NULL,
2992 G_CALLBACK(remove_icon), gtkconv);
2994 pidgin_new_menu_item(menu, _("Save Icon As..."), GTK_STOCK_SAVE_AS,
2995 G_CALLBACK(icon_menu_save_cb), gtkconv);
2997 pidgin_new_menu_item(menu, _("Set Custom Icon..."), NULL,
2998 G_CALLBACK(set_custom_icon_cb), gtkconv);
3000 pidgin_new_menu_item(menu, _("Change Size"), NULL,
3001 G_CALLBACK(change_size_cb), gtkconv);
3003 /* Is there a custom icon for this person? */
3004 conv = gtkconv->active_conv;
3005 buddy = purple_blist_find_buddy(purple_conversation_get_account(conv),
3006 purple_conversation_get_name(conv));
3007 if (buddy)
3009 PurpleContact *contact = purple_buddy_get_contact(buddy);
3010 if (contact && purple_buddy_icons_node_has_custom_icon((PurpleBlistNode*)contact))
3012 pidgin_new_menu_item(menu, _("Remove Custom Icon"),
3013 NULL, G_CALLBACK(remove_custom_icon_cb),
3014 gtkconv);
3018 gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, e->button, e->time);
3020 return TRUE;
3023 /**************************************************************************
3024 * End of the bunch of buddy icon functions
3025 **************************************************************************/
3026 void
3027 pidgin_conv_present_conversation(PurpleConversation *conv)
3029 PidginConversation *gtkconv;
3030 GdkModifierType state;
3032 pidgin_conv_attach_to_conversation(conv);
3033 gtkconv = PIDGIN_CONVERSATION(conv);
3035 pidgin_conv_switch_active_conversation(conv);
3036 /* Switch the tab only if the user initiated the event by pressing
3037 * a button or hitting a key. */
3038 if (gtk_get_current_event_state(&state))
3039 pidgin_conv_window_switch_gtkconv(gtkconv->win, gtkconv);
3040 gtk_window_present(GTK_WINDOW(gtkconv->win->window));
3043 static GList *
3044 pidgin_conversations_get_unseen(GList *l,
3045 PidginUnseenState min_state,
3046 gboolean hidden_only,
3047 guint max_count)
3049 GList *r = NULL;
3050 guint c = 0;
3052 for (; l != NULL && (max_count == 0 || c < max_count); l = l->next) {
3053 PurpleConversation *conv = (PurpleConversation*)l->data;
3054 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
3056 if(gtkconv == NULL || gtkconv->active_conv != conv)
3057 continue;
3059 if (gtkconv->unseen_state >= min_state
3060 && (!hidden_only ||
3061 (hidden_only && gtkconv->win == hidden_convwin))) {
3063 r = g_list_prepend(r, conv);
3064 c++;
3068 return r;
3071 GList *
3072 pidgin_conversations_get_unseen_all(PidginUnseenState min_state,
3073 gboolean hidden_only,
3074 guint max_count)
3076 return pidgin_conversations_get_unseen(purple_conversations_get_all(),
3077 min_state, hidden_only, max_count);
3080 GList *
3081 pidgin_conversations_get_unseen_ims(PidginUnseenState min_state,
3082 gboolean hidden_only,
3083 guint max_count)
3085 return pidgin_conversations_get_unseen(purple_conversations_get_ims(),
3086 min_state, hidden_only, max_count);
3089 GList *
3090 pidgin_conversations_get_unseen_chats(PidginUnseenState min_state,
3091 gboolean hidden_only,
3092 guint max_count)
3094 return pidgin_conversations_get_unseen(purple_conversations_get_chats(),
3095 min_state, hidden_only, max_count);
3098 static void
3099 unseen_conv_menu_cb(GtkMenuItem *item, PurpleConversation *conv)
3101 g_return_if_fail(conv != NULL);
3102 pidgin_conv_present_conversation(conv);
3105 static void
3106 unseen_all_conv_menu_cb(GtkMenuItem *item, GList *list)
3108 g_return_if_fail(list != NULL);
3109 /* Do not free the list from here. It will be freed from the
3110 * 'destroy' callback on the menuitem. */
3111 while (list) {
3112 pidgin_conv_present_conversation(list->data);
3113 list = list->next;
3117 guint
3118 pidgin_conversations_fill_menu(GtkWidget *menu, GList *convs)
3120 GList *l;
3121 guint ret=0;
3123 g_return_val_if_fail(menu != NULL, 0);
3124 g_return_val_if_fail(convs != NULL, 0);
3126 for (l = convs; l != NULL ; l = l->next) {
3127 PurpleConversation *conv = (PurpleConversation*)l->data;
3128 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
3130 GtkWidget *icon = gtk_image_new_from_stock(pidgin_conv_get_icon_stock(conv),
3131 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_MICROSCOPIC));
3132 GtkWidget *item;
3133 gchar *text = g_strdup_printf("%s (%d)",
3134 gtk_label_get_text(GTK_LABEL(gtkconv->tab_label)),
3135 gtkconv->unseen_count);
3137 item = gtk_image_menu_item_new_with_label(text);
3138 gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(item), icon);
3139 g_signal_connect(G_OBJECT(item), "activate", G_CALLBACK(unseen_conv_menu_cb), conv);
3140 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
3141 g_free(text);
3142 ret++;
3145 if (convs->next) {
3146 /* There are more than one conversation. Add an option to show all conversations. */
3147 GtkWidget *item;
3148 GList *list = g_list_copy(convs);
3150 pidgin_separator(menu);
3152 item = gtk_menu_item_new_with_label(_("Show All"));
3153 g_signal_connect(G_OBJECT(item), "activate", G_CALLBACK(unseen_all_conv_menu_cb), list);
3154 g_signal_connect_swapped(G_OBJECT(item), "destroy", G_CALLBACK(g_list_free), list);
3155 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
3158 return ret;
3161 PidginConvWindow *
3162 pidgin_conv_get_window(PidginConversation *gtkconv)
3164 g_return_val_if_fail(gtkconv != NULL, NULL);
3165 return gtkconv->win;
3168 static GtkActionEntry menu_entries[] =
3169 /* TODO: fill out tooltips... */
3171 /* Conversation menu */
3172 { "ConversationMenu", NULL, N_("_Conversation"), NULL, NULL, NULL },
3173 { "NewInstantMessage", PIDGIN_STOCK_TOOLBAR_MESSAGE_NEW, N_("New Instant _Message..."), "<control>M", NULL, G_CALLBACK(menu_new_conv_cb) },
3174 { "JoinAChat", PIDGIN_STOCK_CHAT, N_("Join a _Chat..."), NULL, NULL, G_CALLBACK(menu_join_chat_cb) },
3175 { "Find", GTK_STOCK_FIND, N_("_Find..."), NULL, NULL, G_CALLBACK(menu_find_cb) },
3176 { "ViewLog", NULL, N_("View _Log"), NULL, NULL, G_CALLBACK(menu_view_log_cb) },
3177 { "SaveAs", GTK_STOCK_SAVE_AS, N_("_Save As..."), NULL, NULL, G_CALLBACK(menu_save_as_cb) },
3178 { "ClearScrollback", GTK_STOCK_CLEAR, N_("Clea_r Scrollback"), "<control>L", NULL, G_CALLBACK(menu_clear_cb) },
3180 #ifdef USE_VV
3181 { "MediaMenu", NULL, N_("M_edia"), NULL, NULL, NULL },
3182 { "AudioCall", PIDGIN_STOCK_TOOLBAR_AUDIO_CALL, N_("_Audio Call"), NULL, NULL, G_CALLBACK(menu_initiate_media_call_cb) },
3183 { "VideoCall", PIDGIN_STOCK_TOOLBAR_VIDEO_CALL, N_("_Video Call"), NULL, NULL, G_CALLBACK(menu_initiate_media_call_cb) },
3184 { "AudioVideoCall", PIDGIN_STOCK_TOOLBAR_VIDEO_CALL, N_("Audio/Video _Call"), NULL, NULL, G_CALLBACK(menu_initiate_media_call_cb) },
3185 #endif
3187 { "SendFile", PIDGIN_STOCK_TOOLBAR_SEND_FILE, N_("Se_nd File..."), NULL, NULL, G_CALLBACK(menu_send_file_cb) },
3188 { "GetAttention", PIDGIN_STOCK_TOOLBAR_SEND_ATTENTION, N_("Get _Attention"), NULL, NULL, G_CALLBACK(menu_get_attention_cb) },
3189 { "AddBuddyPounce", NULL, N_("Add Buddy _Pounce..."), NULL, NULL, G_CALLBACK(menu_add_pounce_cb) },
3190 { "GetInfo", PIDGIN_STOCK_TOOLBAR_USER_INFO, N_("_Get Info"), "<control>O", NULL, G_CALLBACK(menu_get_info_cb) },
3191 { "Invite", NULL, N_("In_vite..."), NULL, NULL, G_CALLBACK(menu_invite_cb) },
3192 { "MoreMenu", NULL, N_("M_ore"), NULL, NULL, NULL },
3193 { "Alias", NULL, N_("Al_ias..."), NULL, NULL, G_CALLBACK(menu_alias_cb) },
3194 { "Block", PIDGIN_STOCK_TOOLBAR_BLOCK, N_("_Block..."), NULL, NULL, G_CALLBACK(menu_block_cb) },
3195 { "Unblock", PIDGIN_STOCK_TOOLBAR_UNBLOCK, N_("_Unblock..."), NULL, NULL, G_CALLBACK(menu_unblock_cb) },
3196 { "Add", GTK_STOCK_ADD, N_("_Add..."), NULL, NULL, G_CALLBACK(menu_add_remove_cb) },
3197 { "Remove", GTK_STOCK_REMOVE, N_("_Remove..."), NULL, NULL, G_CALLBACK(menu_add_remove_cb) },
3198 { "InsertLink", PIDGIN_STOCK_TOOLBAR_INSERT_LINK, N_("Insert Lin_k..."), NULL, NULL, G_CALLBACK(menu_insert_link_cb) },
3199 { "InsertImage", PIDGIN_STOCK_TOOLBAR_INSERT_IMAGE, N_("Insert Imag_e..."), NULL, NULL, G_CALLBACK(menu_insert_image_cb) },
3200 { "Close", GTK_STOCK_CLOSE, N_("_Close"), NULL, NULL, G_CALLBACK(menu_close_conv_cb) },
3202 /* Options */
3203 { "OptionsMenu", NULL, N_("_Options"), NULL, NULL, NULL },
3206 /* Toggle items */
3207 static const GtkToggleActionEntry menu_toggle_entries[] = {
3208 { "EnableLogging", NULL, N_("Enable _Logging"), NULL, NULL, G_CALLBACK(menu_logging_cb), FALSE },
3209 { "EnableSounds", NULL, N_("Enable _Sounds"), NULL, NULL, G_CALLBACK(menu_sounds_cb), FALSE },
3210 { "ShowFormattingToolbars", NULL, N_("Show Formatting _Toolbars"), NULL, NULL, G_CALLBACK(menu_toolbar_cb), FALSE },
3213 static const char *conversation_menu =
3214 "<ui>"
3215 "<menubar name='Conversation'>"
3216 "<menu action='ConversationMenu'>"
3217 "<menuitem action='NewInstantMessage'/>"
3218 "<menuitem action='JoinAChat'/>"
3219 "<separator/>"
3220 "<menuitem action='Find'/>"
3221 "<menuitem action='ViewLog'/>"
3222 "<menuitem action='SaveAs'/>"
3223 "<menuitem action='ClearScrollback'/>"
3224 "<separator/>"
3225 #ifdef USE_VV
3226 "<menu action='MediaMenu'>"
3227 "<menuitem action='AudioCall'/>"
3228 "<menuitem action='VideoCall'/>"
3229 "<menuitem action='AudioVideoCall'/>"
3230 "</menu>"
3231 #endif
3232 "<menuitem action='SendFile'/>"
3233 "<menuitem action='GetAttention'/>"
3234 "<menuitem action='AddBuddyPounce'/>"
3235 "<menuitem action='GetInfo'/>"
3236 "<menuitem action='Invite'/>"
3237 "<menu action='MoreMenu'/>"
3238 "<separator/>"
3239 "<menuitem action='Alias'/>"
3240 "<menuitem action='Block'/>"
3241 "<menuitem action='Unblock'/>"
3242 "<menuitem action='Add'/>"
3243 "<menuitem action='Remove'/>"
3244 "<separator/>"
3245 "<menuitem action='InsertLink'/>"
3246 "<menuitem action='InsertImage'/>"
3247 "<separator/>"
3248 "<menuitem action='Close'/>"
3249 "</menu>"
3250 "<menu action='OptionsMenu'>"
3251 "<menuitem action='EnableLogging'/>"
3252 "<menuitem action='EnableSounds'/>"
3253 "<separator/>"
3254 "<menuitem action='ShowFormattingToolbars'/>"
3255 "</menu>"
3256 "</menubar>"
3257 "</ui>";
3259 static void
3260 sound_method_pref_changed_cb(const char *name, PurplePrefType type,
3261 gconstpointer value, gpointer data)
3263 PidginConvWindow *win = data;
3264 const char *method = value;
3266 if (!strcmp(method, "none"))
3268 gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(win->menu->sounds),
3269 FALSE);
3270 gtk_action_set_sensitive(win->menu->sounds, FALSE);
3272 else
3274 PidginConversation *gtkconv = pidgin_conv_window_get_active_gtkconv(win);
3276 if (gtkconv != NULL)
3277 gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(win->menu->sounds),
3278 gtkconv->make_sound);
3279 gtk_action_set_sensitive(win->menu->sounds, TRUE);
3283 /* Returns TRUE if some items were added to the menu, FALSE otherwise */
3284 static gboolean
3285 populate_menu_with_options(GtkWidget *menu, PidginConversation *gtkconv, gboolean all)
3287 GList *list;
3288 PurpleConversation *conv;
3289 PurpleAccount *account;
3290 PurpleBlistNode *node = NULL;
3291 PurpleChat *chat = NULL;
3292 PurpleBuddy *buddy = NULL;
3293 gboolean ret;
3295 conv = gtkconv->active_conv;
3296 account = purple_conversation_get_account(conv);
3298 if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
3299 chat = purple_blist_find_chat(account, purple_conversation_get_name(conv));
3301 if ((chat == NULL) && (gtkconv->webview != NULL)) {
3302 chat = g_object_get_data(G_OBJECT(gtkconv->webview), "transient_chat");
3305 if ((chat == NULL) && (gtkconv->webview != NULL)) {
3306 GHashTable *components;
3307 PurpleAccount *account = purple_conversation_get_account(conv);
3308 PurpleProtocol *protocol =
3309 purple_protocols_find(purple_account_get_protocol_id(account));
3310 if (purple_account_get_connection(account) != NULL &&
3311 PURPLE_PROTOCOL_IMPLEMENTS(protocol, CHAT_IFACE, info_defaults)) {
3312 components = purple_protocol_chat_iface_info_defaults(protocol, purple_account_get_connection(account),
3313 purple_conversation_get_name(conv));
3314 } else {
3315 components = g_hash_table_new_full(g_str_hash, g_str_equal,
3316 g_free, g_free);
3317 g_hash_table_replace(components, g_strdup("channel"),
3318 g_strdup(purple_conversation_get_name(conv)));
3320 chat = purple_chat_new(account, NULL, components);
3321 purple_blist_node_set_transient((PurpleBlistNode *)chat, TRUE);
3322 g_object_set_data_full(G_OBJECT(gtkconv->webview), "transient_chat",
3323 chat, (GDestroyNotify)purple_blist_remove_chat);
3325 } else {
3326 if (!purple_account_is_connected(account))
3327 return FALSE;
3329 buddy = purple_blist_find_buddy(account, purple_conversation_get_name(conv));
3330 if (!buddy && gtkconv->webview) {
3331 buddy = g_object_get_data(G_OBJECT(gtkconv->webview), "transient_buddy");
3333 if (!buddy) {
3334 buddy = purple_buddy_new(account, purple_conversation_get_name(conv), NULL);
3335 purple_blist_node_set_transient((PurpleBlistNode *)buddy, TRUE);
3336 g_object_set_data_full(G_OBJECT(gtkconv->webview), "transient_buddy",
3337 buddy, (GDestroyNotify)g_object_unref);
3342 if (chat)
3343 node = (PurpleBlistNode *)chat;
3344 else if (buddy)
3345 node = (PurpleBlistNode *)buddy;
3347 /* Now add the stuff */
3348 if (all) {
3349 if (buddy)
3350 pidgin_blist_make_buddy_menu(menu, buddy, TRUE);
3351 else if (chat) {
3352 /* XXX: */
3354 } else if (node) {
3355 if (purple_account_is_connected(account))
3356 pidgin_append_blist_node_proto_menu(menu, purple_account_get_connection(account), node);
3357 pidgin_append_blist_node_extended_menu(menu, node);
3360 if ((list = gtk_container_get_children(GTK_CONTAINER(menu))) == NULL) {
3361 ret = FALSE;
3362 } else {
3363 g_list_free(list);
3364 ret = TRUE;
3366 return ret;
3369 static void
3370 regenerate_media_items(PidginConvWindow *win)
3372 #ifdef USE_VV
3373 PurpleAccount *account;
3374 PurpleConversation *conv;
3376 conv = pidgin_conv_window_get_active_conversation(win);
3378 if (conv == NULL) {
3379 purple_debug_error("gtkconv", "couldn't get active conversation"
3380 " when regenerating media items\n");
3381 return;
3384 account = purple_conversation_get_account(conv);
3386 if (account == NULL) {
3387 purple_debug_error("gtkconv", "couldn't get account when"
3388 " regenerating media items\n");
3389 return;
3393 * Check if account support voice and/or calls, and
3394 * if the current buddy supports it.
3396 if (account != NULL && PURPLE_IS_IM_CONVERSATION(conv)) {
3397 PurpleMediaCaps caps =
3398 purple_protocol_get_media_caps(account,
3399 purple_conversation_get_name(conv));
3401 gtk_action_set_sensitive(win->menu->audio_call,
3402 caps & PURPLE_MEDIA_CAPS_AUDIO
3403 ? TRUE : FALSE);
3404 gtk_action_set_sensitive(win->menu->video_call,
3405 caps & PURPLE_MEDIA_CAPS_VIDEO
3406 ? TRUE : FALSE);
3407 gtk_action_set_sensitive(win->menu->audio_video_call,
3408 caps & PURPLE_MEDIA_CAPS_AUDIO_VIDEO
3409 ? TRUE : FALSE);
3410 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
3411 /* for now, don't care about chats... */
3412 gtk_action_set_sensitive(win->menu->audio_call, FALSE);
3413 gtk_action_set_sensitive(win->menu->video_call, FALSE);
3414 gtk_action_set_sensitive(win->menu->audio_video_call, FALSE);
3415 } else {
3416 gtk_action_set_sensitive(win->menu->audio_call, FALSE);
3417 gtk_action_set_sensitive(win->menu->video_call, FALSE);
3418 gtk_action_set_sensitive(win->menu->audio_video_call, FALSE);
3420 #endif
3423 static void
3424 regenerate_attention_items(PidginConvWindow *win)
3426 GtkWidget *attention;
3427 GtkWidget *menu;
3428 PurpleConversation *conv;
3429 PurpleConnection *pc;
3430 PurpleProtocol *protocol = NULL;
3431 GList *list;
3433 conv = pidgin_conv_window_get_active_conversation(win);
3434 if (!conv)
3435 return;
3437 attention = gtk_ui_manager_get_widget(win->menu->ui,
3438 "/Conversation/ConversationMenu/GetAttention");
3440 /* Remove the previous entries */
3441 gtk_menu_item_set_submenu(GTK_MENU_ITEM(attention), NULL);
3443 pc = purple_conversation_get_connection(conv);
3444 if (pc != NULL)
3445 protocol = purple_connection_get_protocol(pc);
3447 if (protocol && PURPLE_PROTOCOL_IMPLEMENTS(protocol, ATTENTION_IFACE, get_types)) {
3448 list = purple_protocol_attention_iface_get_types(protocol, purple_connection_get_account(pc));
3450 /* Multiple attention types */
3451 if (list && list->next) {
3452 int index = 0;
3454 menu = gtk_menu_new();
3455 while (list) {
3456 PurpleAttentionType *type;
3457 GtkWidget *menuitem;
3459 type = list->data;
3461 menuitem = gtk_menu_item_new_with_label(purple_attention_type_get_name(type));
3462 g_object_set_data(G_OBJECT(menuitem), "index", GINT_TO_POINTER(index));
3463 g_signal_connect(G_OBJECT(menuitem), "activate",
3464 G_CALLBACK(menu_get_attention_cb),
3465 win);
3466 gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
3468 index++;
3469 list = g_list_delete_link(list, list);
3472 gtk_menu_item_set_submenu(GTK_MENU_ITEM(attention), menu);
3473 gtk_widget_show_all(menu);
3478 static void
3479 regenerate_options_items(PidginConvWindow *win)
3481 GtkWidget *menu;
3482 PidginConversation *gtkconv;
3483 GList *list;
3484 GtkWidget *more_menu;
3486 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
3487 more_menu = gtk_ui_manager_get_widget(win->menu->ui,
3488 "/Conversation/ConversationMenu/MoreMenu");
3489 gtk_widget_show(more_menu);
3490 menu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(more_menu));
3492 /* Remove the previous entries */
3493 for (list = gtk_container_get_children(GTK_CONTAINER(menu)); list; )
3495 GtkWidget *w = list->data;
3496 list = g_list_delete_link(list, list);
3497 gtk_widget_destroy(w);
3500 if (!populate_menu_with_options(menu, gtkconv, FALSE))
3502 GtkWidget *item = gtk_menu_item_new_with_label(_("No actions available"));
3503 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
3504 gtk_widget_set_sensitive(item, FALSE);
3507 gtk_widget_show_all(menu);
3510 static void
3511 remove_from_list(GtkWidget *widget, PidginConvWindow *win)
3513 GList *list = g_object_get_data(G_OBJECT(win->window), "plugin-actions");
3514 list = g_list_remove(list, widget);
3515 g_object_set_data(G_OBJECT(win->window), "plugin-actions", list);
3518 static void
3519 regenerate_plugins_items(PidginConvWindow *win)
3521 GList *action_items;
3522 GtkWidget *menu;
3523 GList *list;
3524 PidginConversation *gtkconv;
3525 PurpleConversation *conv;
3526 GtkWidget *item;
3528 if (win->window == NULL || win == hidden_convwin)
3529 return;
3531 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
3532 if (gtkconv == NULL)
3533 return;
3535 conv = gtkconv->active_conv;
3536 action_items = g_object_get_data(G_OBJECT(win->window), "plugin-actions");
3538 /* Remove the old menuitems */
3539 while (action_items) {
3540 g_signal_handlers_disconnect_by_func(G_OBJECT(action_items->data),
3541 G_CALLBACK(remove_from_list), win);
3542 gtk_widget_destroy(action_items->data);
3543 action_items = g_list_delete_link(action_items, action_items);
3546 item = gtk_ui_manager_get_widget(win->menu->ui, "/Conversation/OptionsMenu");
3547 menu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(item));
3549 list = purple_conversation_get_extended_menu(conv);
3550 if (list) {
3551 action_items = g_list_prepend(NULL, (item = pidgin_separator(menu)));
3552 g_signal_connect(G_OBJECT(item), "destroy", G_CALLBACK(remove_from_list), win);
3555 for(; list; list = g_list_delete_link(list, list)) {
3556 PurpleMenuAction *act = (PurpleMenuAction *) list->data;
3557 item = pidgin_append_menu_action(menu, act, conv);
3558 action_items = g_list_prepend(action_items, item);
3559 gtk_widget_show_all(item);
3560 g_signal_connect(G_OBJECT(item), "destroy", G_CALLBACK(remove_from_list), win);
3562 g_object_set_data(G_OBJECT(win->window), "plugin-actions", action_items);
3565 static void menubar_activated(GtkWidget *item, gpointer data)
3567 PidginConvWindow *win = data;
3568 regenerate_media_items(win);
3569 regenerate_options_items(win);
3570 regenerate_plugins_items(win);
3571 regenerate_attention_items(win);
3573 /* The following are to make sure the 'More' submenu is not regenerated every time
3574 * the focus shifts from 'Conversations' to some other menu and back. */
3575 g_signal_handlers_block_by_func(G_OBJECT(item), G_CALLBACK(menubar_activated), data);
3576 g_signal_connect(G_OBJECT(win->menu->menubar), "deactivate", G_CALLBACK(focus_out_from_menubar), data);
3579 static void
3580 focus_out_from_menubar(GtkWidget *wid, PidginConvWindow *win)
3582 /* The menubar has been deactivated. Make sure the 'More' submenu is regenerated next time
3583 * the 'Conversation' menu pops up. */
3584 GtkWidget *menuitem = gtk_ui_manager_get_widget(win->menu->ui, "/Conversation/ConversationMenu");
3585 g_signal_handlers_unblock_by_func(G_OBJECT(menuitem), G_CALLBACK(menubar_activated), win);
3586 g_signal_handlers_disconnect_by_func(G_OBJECT(win->menu->menubar),
3587 G_CALLBACK(focus_out_from_menubar), win);
3590 static GtkWidget *
3591 setup_menubar(PidginConvWindow *win)
3593 GtkAccelGroup *accel_group;
3594 const char *method;
3595 GtkActionGroup *action_group;
3596 GError *error;
3597 GtkWidget *menuitem;
3599 action_group = gtk_action_group_new("ConversationActions");
3600 #ifdef ENABLE_NLS
3601 gtk_action_group_set_translation_domain(action_group, PACKAGE);
3602 #endif
3603 gtk_action_group_add_actions(action_group,
3604 menu_entries,
3605 G_N_ELEMENTS(menu_entries),
3606 win);
3607 gtk_action_group_add_toggle_actions(action_group,
3608 menu_toggle_entries,
3609 G_N_ELEMENTS(menu_toggle_entries),
3610 win);
3612 win->menu->ui = gtk_ui_manager_new();
3613 gtk_ui_manager_insert_action_group(win->menu->ui, action_group, 0);
3615 accel_group = gtk_ui_manager_get_accel_group(win->menu->ui);
3616 gtk_window_add_accel_group(GTK_WINDOW(win->window), accel_group);
3617 g_signal_connect(G_OBJECT(accel_group), "accel-changed",
3618 G_CALLBACK(pidgin_save_accels_cb), NULL);
3620 error = NULL;
3621 if (!gtk_ui_manager_add_ui_from_string(win->menu->ui, conversation_menu, -1, &error))
3623 g_message("building menus failed: %s", error->message);
3624 g_error_free(error);
3625 exit(EXIT_FAILURE);
3628 win->menu->menubar =
3629 gtk_ui_manager_get_widget(win->menu->ui, "/Conversation");
3631 /* Make sure the 'Conversation -> More' menuitems are regenerated whenever
3632 * the 'Conversation' menu pops up because the entries can change after the
3633 * conversation is created. */
3634 menuitem = gtk_ui_manager_get_widget(win->menu->ui, "/Conversation/ConversationMenu");
3635 g_signal_connect(G_OBJECT(menuitem), "activate", G_CALLBACK(menubar_activated), win);
3637 win->menu->view_log =
3638 gtk_ui_manager_get_action(win->menu->ui,
3639 "/Conversation/ConversationMenu/ViewLog");
3641 #ifdef USE_VV
3642 win->menu->audio_call =
3643 gtk_ui_manager_get_action(win->menu->ui,
3644 "/Conversation/ConversationMenu/MediaMenu/AudioCall");
3645 win->menu->video_call =
3646 gtk_ui_manager_get_action(win->menu->ui,
3647 "/Conversation/ConversationMenu/MediaMenu/VideoCall");
3648 win->menu->audio_video_call =
3649 gtk_ui_manager_get_action(win->menu->ui,
3650 "/Conversation/ConversationMenu/MediaMenu/AudioVideoCall");
3651 #endif
3653 /* --- */
3655 win->menu->send_file =
3656 gtk_ui_manager_get_action(win->menu->ui,
3657 "/Conversation/ConversationMenu/SendFile");
3659 win->menu->get_attention =
3660 gtk_ui_manager_get_action(win->menu->ui,
3661 "/Conversation/ConversationMenu/GetAttention");
3663 win->menu->add_pounce =
3664 gtk_ui_manager_get_action(win->menu->ui,
3665 "/Conversation/ConversationMenu/AddBuddyPounce");
3667 /* --- */
3669 win->menu->get_info =
3670 gtk_ui_manager_get_action(win->menu->ui,
3671 "/Conversation/ConversationMenu/GetInfo");
3673 win->menu->invite =
3674 gtk_ui_manager_get_action(win->menu->ui,
3675 "/Conversation/ConversationMenu/Invite");
3677 /* --- */
3679 win->menu->alias =
3680 gtk_ui_manager_get_action(win->menu->ui,
3681 "/Conversation/ConversationMenu/Alias");
3683 win->menu->block =
3684 gtk_ui_manager_get_action(win->menu->ui,
3685 "/Conversation/ConversationMenu/Block");
3687 win->menu->unblock =
3688 gtk_ui_manager_get_action(win->menu->ui,
3689 "/Conversation/ConversationMenu/Unblock");
3691 win->menu->add =
3692 gtk_ui_manager_get_action(win->menu->ui,
3693 "/Conversation/ConversationMenu/Add");
3695 win->menu->remove =
3696 gtk_ui_manager_get_action(win->menu->ui,
3697 "/Conversation/ConversationMenu/Remove");
3699 /* --- */
3701 win->menu->insert_link =
3702 gtk_ui_manager_get_action(win->menu->ui,
3703 "/Conversation/ConversationMenu/InsertLink");
3705 win->menu->insert_image =
3706 gtk_ui_manager_get_action(win->menu->ui,
3707 "/Conversation/ConversationMenu/InsertImage");
3709 /* --- */
3711 win->menu->logging =
3712 gtk_ui_manager_get_action(win->menu->ui,
3713 "/Conversation/OptionsMenu/EnableLogging");
3714 win->menu->sounds =
3715 gtk_ui_manager_get_action(win->menu->ui,
3716 "/Conversation/OptionsMenu/EnableSounds");
3717 method = purple_prefs_get_string(PIDGIN_PREFS_ROOT "/sound/method");
3718 if (method != NULL && !strcmp(method, "none"))
3720 gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(win->menu->sounds),
3721 FALSE);
3722 gtk_action_set_sensitive(win->menu->sounds, FALSE);
3724 purple_prefs_connect_callback(win, PIDGIN_PREFS_ROOT "/sound/method",
3725 sound_method_pref_changed_cb, win);
3727 win->menu->show_formatting_toolbar =
3728 gtk_ui_manager_get_action(win->menu->ui,
3729 "/Conversation/OptionsMenu/ShowFormattingToolbars");
3731 win->menu->tray = pidgin_menu_tray_new();
3732 gtk_menu_shell_append(GTK_MENU_SHELL(win->menu->menubar),
3733 win->menu->tray);
3734 gtk_widget_show(win->menu->tray);
3736 gtk_widget_show(win->menu->menubar);
3738 return win->menu->menubar;
3742 /**************************************************************************
3743 * Utility functions
3744 **************************************************************************/
3746 static void
3747 got_typing_keypress(PidginConversation *gtkconv, gboolean first)
3749 PurpleConversation *conv = gtkconv->active_conv;
3750 PurpleIMConversation *im;
3753 * We know we got something, so we at least have to make sure we don't
3754 * send PURPLE_IM_TYPED any time soon.
3757 im = PURPLE_IM_CONVERSATION(conv);
3759 purple_im_conversation_stop_send_typed_timeout(im);
3760 purple_im_conversation_start_send_typed_timeout(im);
3762 /* Check if we need to send another PURPLE_IM_TYPING message */
3763 if (first || (purple_im_conversation_get_type_again(im) != 0 &&
3764 time(NULL) > purple_im_conversation_get_type_again(im)))
3766 unsigned int timeout;
3767 timeout = purple_serv_send_typing(purple_conversation_get_connection(conv),
3768 purple_conversation_get_name(conv),
3769 PURPLE_IM_TYPING);
3770 purple_im_conversation_set_type_again(im, timeout);
3774 #if 0
3775 static gboolean
3776 typing_animation(gpointer data) {
3777 PidginConversation *gtkconv = data;
3778 PidginConvWindow *gtkwin = gtkconv->win;
3779 const char *stock_id = NULL;
3781 if(gtkconv != pidgin_conv_window_get_active_gtkconv(gtkwin)) {
3782 return FALSE;
3785 switch (rand() % 5) {
3786 case 0:
3787 stock_id = PIDGIN_STOCK_ANIMATION_TYPING0;
3788 break;
3789 case 1:
3790 stock_id = PIDGIN_STOCK_ANIMATION_TYPING1;
3791 break;
3792 case 2:
3793 stock_id = PIDGIN_STOCK_ANIMATION_TYPING2;
3794 break;
3795 case 3:
3796 stock_id = PIDGIN_STOCK_ANIMATION_TYPING3;
3797 break;
3798 case 4:
3799 stock_id = PIDGIN_STOCK_ANIMATION_TYPING4;
3800 break;
3802 if (gtkwin->menu->typing_icon == NULL) {
3803 gtkwin->menu->typing_icon = gtk_image_new_from_stock(stock_id, GTK_ICON_SIZE_MENU);
3804 pidgin_menu_tray_append(PIDGIN_MENU_TRAY(gtkwin->menu->tray),
3805 gtkwin->menu->typing_icon,
3806 _("User is typing..."));
3807 } else {
3808 gtk_image_set_from_stock(GTK_IMAGE(gtkwin->menu->typing_icon), stock_id, GTK_ICON_SIZE_MENU);
3810 gtk_widget_show(gtkwin->menu->typing_icon);
3811 return TRUE;
3813 #endif
3815 static void
3816 update_typing_message(PidginConversation *gtkconv, const char *message)
3818 /* TODO WEBKIT: this is not handled at all */
3819 #if 0
3820 GtkTextBuffer *buffer;
3821 GtkTextMark *stmark, *enmark;
3823 if (g_object_get_data(G_OBJECT(gtkconv->imhtml), "disable-typing-notification"))
3824 return;
3826 buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(gtkconv->imhtml));
3827 stmark = gtk_text_buffer_get_mark(buffer, "typing-notification-start");
3828 enmark = gtk_text_buffer_get_mark(buffer, "typing-notification-end");
3829 if (stmark && enmark) {
3830 GtkTextIter start, end;
3831 gtk_text_buffer_get_iter_at_mark(buffer, &start, stmark);
3832 gtk_text_buffer_get_iter_at_mark(buffer, &end, enmark);
3833 gtk_text_buffer_delete_mark(buffer, stmark);
3834 gtk_text_buffer_delete_mark(buffer, enmark);
3835 gtk_text_buffer_delete(buffer, &start, &end);
3836 } else if (message && *message == '\n' && message[1] == ' ' && message[2] == '\0')
3837 message = NULL;
3839 #ifdef RESERVE_LINE
3840 if (!message)
3841 message = "\n "; /* The blank space is required to avoid a GTK+/Pango bug */
3842 #endif
3844 if (message) {
3845 GtkTextIter iter;
3846 gtk_text_buffer_get_end_iter(buffer, &iter);
3847 gtk_text_buffer_create_mark(buffer, "typing-notification-start", &iter, TRUE);
3848 gtk_text_buffer_insert_with_tags_by_name(buffer, &iter, message, -1, "TYPING-NOTIFICATION", NULL);
3849 gtk_text_buffer_get_end_iter(buffer, &iter);
3850 gtk_text_buffer_create_mark(buffer, "typing-notification-end", &iter, TRUE);
3852 #endif /* if 0 */
3855 static void
3856 update_typing_icon(PidginConversation *gtkconv)
3858 PurpleIMConversation *im;
3859 char *message = NULL;
3861 if (!PURPLE_IS_IM_CONVERSATION(gtkconv->active_conv))
3862 return;
3864 im = PURPLE_IM_CONVERSATION(gtkconv->active_conv);
3866 if (purple_im_conversation_get_typing_state(im) == PURPLE_IM_NOT_TYPING) {
3867 #ifdef RESERVE_LINE
3868 update_typing_message(gtkconv, NULL);
3869 #else
3870 update_typing_message(gtkconv, "\n ");
3871 #endif
3872 return;
3875 if (purple_im_conversation_get_typing_state(im) == PURPLE_IM_TYPING) {
3876 message = g_strdup_printf(_("\n%s is typing..."), purple_conversation_get_title(PURPLE_CONVERSATION(im)));
3877 } else {
3878 message = g_strdup_printf(_("\n%s has stopped typing"), purple_conversation_get_title(PURPLE_CONVERSATION(im)));
3881 update_typing_message(gtkconv, message);
3882 g_free(message);
3885 static gboolean
3886 update_send_to_selection(PidginConvWindow *win)
3888 PurpleAccount *account;
3889 PurpleConversation *conv;
3890 GtkWidget *menu;
3891 GList *child;
3892 PurpleBuddy *b;
3894 conv = pidgin_conv_window_get_active_conversation(win);
3896 if (conv == NULL)
3897 return FALSE;
3899 account = purple_conversation_get_account(conv);
3901 if (account == NULL)
3902 return FALSE;
3904 if (win->menu->send_to == NULL)
3905 return FALSE;
3907 if (!(b = purple_blist_find_buddy(account, purple_conversation_get_name(conv))))
3908 return FALSE;
3910 gtk_widget_show(win->menu->send_to);
3912 menu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(win->menu->send_to));
3914 for (child = gtk_container_get_children(GTK_CONTAINER(menu));
3915 child != NULL;
3916 child = g_list_delete_link(child, child)) {
3918 GtkWidget *item = child->data;
3919 PurpleBuddy *item_buddy;
3920 PurpleAccount *item_account = g_object_get_data(G_OBJECT(item), "purple_account");
3921 gchar *buddy_name = g_object_get_data(G_OBJECT(item),
3922 "purple_buddy_name");
3923 item_buddy = purple_blist_find_buddy(item_account, buddy_name);
3925 if (b == item_buddy) {
3926 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item), TRUE);
3927 g_list_free(child);
3928 break;
3932 return FALSE;
3935 static gboolean
3936 send_to_item_enter_notify_cb(GtkWidget *menuitem, GdkEventCrossing *event, GtkWidget *label)
3938 gtk_widget_set_sensitive(GTK_WIDGET(label), TRUE);
3939 return FALSE;
3942 static gboolean
3943 send_to_item_leave_notify_cb(GtkWidget *menuitem, GdkEventCrossing *event, GtkWidget *label)
3945 gtk_widget_set_sensitive(GTK_WIDGET(label), FALSE);
3946 return FALSE;
3949 static GtkWidget *
3950 e2ee_state_to_gtkimage(PurpleE2eeState *state)
3952 PurpleImage *img;
3954 img = _pidgin_e2ee_stock_icon_get(
3955 purple_e2ee_state_get_stock_icon(state));
3956 if (!img)
3957 return NULL;
3959 return gtk_image_new_from_pixbuf(pidgin_pixbuf_from_image(img));
3962 static void
3963 create_sendto_item(GtkWidget *menu, GtkSizeGroup *sg, GSList **group,
3964 PurpleBuddy *buddy, PurpleAccount *account, const char *name,
3965 gboolean e2ee_enabled)
3967 GtkWidget *box;
3968 GtkWidget *label;
3969 GtkWidget *image;
3970 GtkWidget *e2ee_image = NULL;
3971 GtkWidget *menuitem;
3972 GdkPixbuf *pixbuf;
3973 gchar *text;
3975 /* Create a pixmap for the protocol icon. */
3976 pixbuf = pidgin_create_protocol_icon(account, PIDGIN_PROTOCOL_ICON_SMALL);
3978 /* Now convert it to GtkImage */
3979 if (pixbuf == NULL)
3980 image = gtk_image_new();
3981 else
3983 image = gtk_image_new_from_pixbuf(pixbuf);
3984 g_object_unref(G_OBJECT(pixbuf));
3987 if (e2ee_enabled) {
3988 PurpleIMConversation *im;
3989 PurpleE2eeState *state = NULL;
3991 im = purple_conversations_find_im_with_account(
3992 purple_buddy_get_name(buddy), purple_buddy_get_account(buddy));
3993 if (im)
3994 state = purple_conversation_get_e2ee_state(PURPLE_CONVERSATION(im));
3995 if (state)
3996 e2ee_image = e2ee_state_to_gtkimage(state);
3997 else
3998 e2ee_image = gtk_image_new();
4001 gtk_size_group_add_widget(sg, image);
4003 /* Make our menu item */
4004 text = g_strdup_printf("%s (%s)", name, purple_account_get_name_for_display(account));
4005 menuitem = gtk_radio_menu_item_new_with_label(*group, text);
4006 g_free(text);
4007 *group = gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(menuitem));
4009 /* Do some evil, see some evil, speak some evil. */
4010 box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
4012 label = gtk_bin_get_child(GTK_BIN(menuitem));
4013 g_object_ref(label);
4014 gtk_container_remove(GTK_CONTAINER(menuitem), label);
4016 gtk_box_pack_start(GTK_BOX(box), image, FALSE, FALSE, 0);
4018 gtk_box_pack_start(GTK_BOX(box), label, TRUE, TRUE, 4);
4019 if (e2ee_image)
4020 gtk_box_pack_start(GTK_BOX(box), e2ee_image, FALSE, FALSE, 0);
4022 if (buddy != NULL &&
4023 !purple_presence_is_online(purple_buddy_get_presence(buddy)))
4025 gtk_widget_set_sensitive(label, FALSE);
4027 /* Set the label sensitive when the menuitem is highlighted and
4028 * insensitive again when the mouse leaves it. This way, it
4029 * doesn't appear weird from the highlighting of the embossed
4030 * (insensitive style) text.*/
4031 g_signal_connect(menuitem, "enter-notify-event",
4032 G_CALLBACK(send_to_item_enter_notify_cb), label);
4033 g_signal_connect(menuitem, "leave-notify-event",
4034 G_CALLBACK(send_to_item_leave_notify_cb), label);
4037 g_object_unref(label);
4039 gtk_container_add(GTK_CONTAINER(menuitem), box);
4041 gtk_widget_show(label);
4042 gtk_widget_show(image);
4043 if (e2ee_image)
4044 gtk_widget_show(e2ee_image);
4045 gtk_widget_show(box);
4047 /* Set our data and callbacks. */
4048 g_object_set_data(G_OBJECT(menuitem), "purple_account", account);
4049 g_object_set_data_full(G_OBJECT(menuitem), "purple_buddy_name", g_strdup(name), g_free);
4051 g_signal_connect(G_OBJECT(menuitem), "activate",
4052 G_CALLBACK(menu_conv_sel_send_cb), NULL);
4054 gtk_widget_show(menuitem);
4055 gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
4058 static gboolean
4059 compare_buddy_presence(PurplePresence *p1, PurplePresence *p2)
4061 /* This is necessary because multiple PurpleBuddy's don't share the same
4062 * PurplePresence anymore.
4064 PurpleBuddy *b1 = purple_buddy_presence_get_buddy(PURPLE_BUDDY_PRESENCE(p1));
4065 PurpleBuddy *b2 = purple_buddy_presence_get_buddy(PURPLE_BUDDY_PRESENCE(p2));
4066 if (purple_buddy_get_account(b1) == purple_buddy_get_account(b2) &&
4067 strcmp(purple_buddy_get_name(b1), purple_buddy_get_name(b2)) == 0)
4068 return FALSE;
4069 return TRUE;
4072 static void
4073 generate_send_to_items(PidginConvWindow *win)
4075 GtkWidget *menu;
4076 GSList *group = NULL;
4077 GtkSizeGroup *sg = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL);
4078 PidginConversation *gtkconv;
4079 GSList *l, *buds;
4081 g_return_if_fail(win != NULL);
4083 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
4085 g_return_if_fail(gtkconv != NULL);
4087 if (win->menu->send_to != NULL)
4088 gtk_widget_destroy(win->menu->send_to);
4090 /* Build the Send To menu */
4091 win->menu->send_to = gtk_menu_item_new_with_mnemonic(_("S_end To"));
4092 gtk_widget_show(win->menu->send_to);
4094 menu = gtk_menu_new();
4095 gtk_menu_shell_insert(GTK_MENU_SHELL(win->menu->menubar),
4096 win->menu->send_to, 2);
4097 gtk_menu_item_set_submenu(GTK_MENU_ITEM(win->menu->send_to), menu);
4099 gtk_widget_show(menu);
4101 if (PURPLE_IS_IM_CONVERSATION(gtkconv->active_conv)) {
4102 buds = purple_blist_find_buddies(purple_conversation_get_account(gtkconv->active_conv), purple_conversation_get_name(gtkconv->active_conv));
4104 if (buds == NULL)
4106 /* The user isn't on the buddy list. So we don't create any sendto menu. */
4108 else
4110 gboolean e2ee_enabled = FALSE;
4111 GList *list = NULL, *iter;
4112 for (l = buds; l != NULL; l = l->next)
4114 PurpleBlistNode *node;
4116 node = PURPLE_BLIST_NODE(purple_buddy_get_contact(PURPLE_BUDDY(l->data)));
4118 for (node = node->child; node != NULL; node = node->next)
4120 PurpleBuddy *buddy = (PurpleBuddy *)node;
4121 PurpleAccount *account;
4122 PurpleIMConversation *im;
4124 if (!PURPLE_IS_BUDDY(node))
4125 continue;
4127 im = purple_conversations_find_im_with_account(purple_buddy_get_name(buddy), purple_buddy_get_account(buddy));
4128 if (im && purple_conversation_get_e2ee_state(PURPLE_CONVERSATION(im)) != NULL)
4129 e2ee_enabled = TRUE;
4131 account = purple_buddy_get_account(buddy);
4132 /* TODO WEBKIT: (I'm not actually sure if this is webkit-related --Mark Doliner) */
4133 if (purple_account_is_connected(account) /*|| account == purple_conversation_get_account(gtkconv->active_conv)*/)
4135 /* Use the PurplePresence to get unique buddies. */
4136 PurplePresence *presence = purple_buddy_get_presence(buddy);
4137 if (!g_list_find_custom(list, presence, (GCompareFunc)compare_buddy_presence))
4138 list = g_list_prepend(list, presence);
4143 /* Create the sendto menu only if it has more than one item to show */
4144 if (list && list->next) {
4145 /* Loop over the list backwards so we get the items in the right order,
4146 * since we did a g_list_prepend() earlier. */
4147 for (iter = g_list_last(list); iter != NULL; iter = iter->prev) {
4148 PurplePresence *pre = iter->data;
4149 PurpleBuddy *buddy = purple_buddy_presence_get_buddy(PURPLE_BUDDY_PRESENCE(pre));
4150 create_sendto_item(menu, sg, &group, buddy,
4151 purple_buddy_get_account(buddy), purple_buddy_get_name(buddy), e2ee_enabled);
4154 g_list_free(list);
4155 g_slist_free(buds);
4159 g_object_unref(sg);
4161 gtk_widget_show(win->menu->send_to);
4162 /* TODO: This should never be insensitive. Possibly hidden or not. */
4163 if (!group)
4164 gtk_widget_set_sensitive(win->menu->send_to, FALSE);
4165 update_send_to_selection(win);
4168 PurpleImage *
4169 _pidgin_e2ee_stock_icon_get(const gchar *stock_name)
4171 gchar filename[100], *path;
4172 PurpleImage *image;
4174 /* core is quitting */
4175 if (e2ee_stock == NULL)
4176 return NULL;
4178 if (g_hash_table_lookup_extended(e2ee_stock, stock_name, NULL, (gpointer*)&image))
4179 return image;
4181 g_snprintf(filename, sizeof(filename), "%s.png", stock_name);
4182 path = g_build_filename(PURPLE_DATADIR, "pixmaps", "pidgin",
4183 "e2ee", "16", filename, NULL);
4184 image = purple_image_new_from_file(path, FALSE);
4185 g_free(path);
4187 g_hash_table_insert(e2ee_stock, g_strdup(stock_name), image);
4188 return image;
4191 static void
4192 generate_e2ee_controls(PidginConvWindow *win)
4194 PidginConversation *gtkconv;
4195 PurpleConversation *conv;
4196 PurpleE2eeState *state;
4197 PurpleE2eeProvider *provider;
4198 GtkWidget *menu;
4199 GList *menu_actions, *it;
4200 GtkWidget *e2ee_image;
4202 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
4203 g_return_if_fail(gtkconv != NULL);
4205 conv = gtkconv->active_conv;
4206 g_return_if_fail(conv != NULL);
4208 if (win->menu->e2ee != NULL) {
4209 gtk_widget_destroy(win->menu->e2ee);
4210 win->menu->e2ee = NULL;
4213 provider = purple_e2ee_provider_get_main();
4214 state = purple_conversation_get_e2ee_state(conv);
4215 if (state == NULL || provider == NULL)
4216 return;
4217 if (purple_e2ee_state_get_provider(state) != provider)
4218 return;
4220 win->menu->e2ee = gtk_image_menu_item_new_with_label(
4221 purple_e2ee_provider_get_name(provider));
4223 menu = gtk_menu_new();
4224 gtk_menu_shell_insert(GTK_MENU_SHELL(win->menu->menubar),
4225 win->menu->e2ee, 3);
4226 gtk_menu_item_set_submenu(GTK_MENU_ITEM(win->menu->e2ee), menu);
4228 e2ee_image = e2ee_state_to_gtkimage(state);
4229 if (e2ee_image) {
4230 gtk_image_menu_item_set_image(
4231 GTK_IMAGE_MENU_ITEM(win->menu->e2ee), e2ee_image);
4234 gtk_widget_set_tooltip_text(win->menu->e2ee,
4235 purple_e2ee_state_get_name(state));
4237 menu_actions = purple_e2ee_provider_get_conv_menu_actions(provider, conv);
4238 for (it = menu_actions; it; it = g_list_next(it)) {
4239 PurpleMenuAction *action = it->data;
4241 gtk_widget_show_all(
4242 pidgin_append_menu_action(menu, action, conv));
4244 g_list_free(menu_actions);
4246 gtk_widget_show(win->menu->e2ee);
4247 gtk_widget_show(menu);
4250 static const char *
4251 get_chat_user_status_icon(PurpleChatConversation *chat, const char *name, PurpleChatUserFlags flags)
4253 const char *image = NULL;
4255 if (flags & PURPLE_CHAT_USER_FOUNDER) {
4256 image = PIDGIN_STOCK_STATUS_FOUNDER;
4257 } else if (flags & PURPLE_CHAT_USER_OP) {
4258 image = PIDGIN_STOCK_STATUS_OPERATOR;
4259 } else if (flags & PURPLE_CHAT_USER_HALFOP) {
4260 image = PIDGIN_STOCK_STATUS_HALFOP;
4261 } else if (flags & PURPLE_CHAT_USER_VOICE) {
4262 image = PIDGIN_STOCK_STATUS_VOICE;
4263 } else if ((!flags) && purple_chat_conversation_is_ignored_user(chat, name)) {
4264 image = PIDGIN_STOCK_STATUS_IGNORED;
4265 } else {
4266 return NULL;
4268 return image;
4271 static void
4272 deleting_chat_user_cb(PurpleChatUser *cb)
4274 GtkTreeRowReference *ref = purple_chat_user_get_ui_data(cb);
4276 if (ref) {
4277 gtk_tree_row_reference_free(ref);
4278 purple_chat_user_set_ui_data(cb, NULL);
4282 static void
4283 add_chat_user_common(PurpleChatConversation *chat, PurpleChatUser *cb, const char *old_name)
4285 PidginConversation *gtkconv;
4286 PurpleConversation *conv;
4287 PidginChatPane *gtkchat;
4288 PurpleConnection *gc;
4289 PurpleProtocol *protocol;
4290 GtkTreeModel *tm;
4291 GtkListStore *ls;
4292 GtkTreePath *newpath;
4293 const char *stock;
4294 GtkTreeIter iter;
4295 gboolean is_me = FALSE;
4296 gboolean is_buddy;
4297 const gchar *name, *alias;
4298 gchar *tmp, *alias_key;
4299 PurpleChatUserFlags flags;
4300 GdkRGBA *color = NULL;
4302 alias = purple_chat_user_get_alias(cb);
4303 name = purple_chat_user_get_name(cb);
4304 flags = purple_chat_user_get_flags(cb);
4306 conv = PURPLE_CONVERSATION(chat);
4307 gtkconv = PIDGIN_CONVERSATION(conv);
4308 gtkchat = gtkconv->u.chat;
4309 gc = purple_conversation_get_connection(conv);
4311 if (!gc || !(protocol = purple_connection_get_protocol(gc)))
4312 return;
4314 tm = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
4315 ls = GTK_LIST_STORE(tm);
4317 stock = get_chat_user_status_icon(chat, name, flags);
4319 if (!strcmp(purple_chat_conversation_get_nick(chat), purple_normalize(purple_conversation_get_account(conv), old_name != NULL ? old_name : name)))
4320 is_me = TRUE;
4322 is_buddy = purple_chat_user_is_buddy(cb);
4324 tmp = g_utf8_casefold(alias, -1);
4325 alias_key = g_utf8_collate_key(tmp, -1);
4326 g_free(tmp);
4328 if (is_me) {
4329 #if 0
4330 /* TODO WEBKIT: No tags in webkit stuff, yet. */
4331 GtkTextTag *tag = gtk_text_tag_table_lookup(
4332 gtk_text_buffer_get_tag_table(GTK_IMHTML(gtkconv->webview)->text_buffer),
4333 "send-name");
4334 g_object_get(tag, "foreground-rgba", &color, NULL);
4335 #endif /* if 0 */
4336 } else {
4337 GtkTextTag *tag;
4338 if ((tag = get_buddy_tag(chat, name, 0, FALSE)))
4339 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_NORMAL, NULL);
4340 if ((tag = get_buddy_tag(chat, name, PURPLE_MESSAGE_NICK, FALSE)))
4341 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_NORMAL, NULL);
4342 color = (GdkRGBA*)get_nick_color(gtkconv, name);
4345 gtk_list_store_insert_with_values(ls, &iter,
4347 * The GTK docs are mute about the effects of the "row" value for performance.
4348 * X-Chat hardcodes their value to 0 (prepend) and -1 (append), so we will too.
4349 * It *might* be faster to search the gtk_list_store and set row accurately,
4350 * but no one in #gtk+ seems to know anything about it either.
4351 * Inserting in the "wrong" location has no visible ill effects. - F.P.
4353 -1, /* "row" */
4354 CHAT_USERS_ICON_STOCK_COLUMN, stock,
4355 CHAT_USERS_ALIAS_COLUMN, alias,
4356 CHAT_USERS_ALIAS_KEY_COLUMN, alias_key,
4357 CHAT_USERS_NAME_COLUMN, name,
4358 CHAT_USERS_FLAGS_COLUMN, flags,
4359 CHAT_USERS_COLOR_COLUMN, color,
4360 CHAT_USERS_WEIGHT_COLUMN, is_buddy ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL,
4361 -1);
4363 if (purple_chat_user_get_ui_data(cb)) {
4364 GtkTreeRowReference *ref = purple_chat_user_get_ui_data(cb);
4365 gtk_tree_row_reference_free(ref);
4368 newpath = gtk_tree_model_get_path(tm, &iter);
4369 purple_chat_user_set_ui_data(cb, gtk_tree_row_reference_new(tm, newpath));
4370 gtk_tree_path_free(newpath);
4372 #if 0
4373 if (is_me && color)
4374 gdk_rgba_free(color);
4375 #endif
4376 g_free(alias_key);
4380 * tab_complete_process_item:
4381 * @most_matched: Used internally by this function.
4382 * @entered: The partial string that the user types before hitting the
4383 * tab key.
4384 * @entered_chars: The length of entered.
4385 * @partial: This is a return variable. This will be set to a string
4386 * containing the largest common string between all matches. This will
4387 * be inserted into the input box at the start of the word that the
4388 * user is tab completing. For example, if a chat room contains
4389 * "AlfFan" and "AlfHater" and the user types "a<TAB>" then this will
4390 * contain "Alf"
4391 * @matches: This is a return variable. If the given name is a potential
4392 * match for the entered string, then add a copy of the name to this
4393 * list. The caller is responsible for g_free'ing the data in this
4394 * list.
4395 * @name: The buddy name or alias or slash command name that we're
4396 * checking for a match.
4398 static void
4399 tab_complete_process_item(int *most_matched, const char *entered, gsize entered_chars, char **partial,
4400 GList **matches, const char *name)
4402 char *nick_partial;
4403 gsize name_len = g_utf8_strlen(name, -1);
4405 if (entered_chars > name_len)
4406 return;
4408 nick_partial = g_utf8_substring(name, 0, entered_chars);
4409 if (purple_utf8_strcasecmp(nick_partial, entered)) {
4410 g_free(nick_partial);
4411 return;
4413 g_free(nick_partial);
4415 /* if we're here, it's a possible completion */
4417 if (*most_matched == -1) {
4419 * this will only get called once, since from now
4420 * on *most_matched is >= 0
4422 *most_matched = name_len;
4423 *partial = g_strdup(name);
4425 else if (*most_matched) {
4426 char *tmp = g_strdup(name);
4428 while (purple_utf8_strcasecmp(tmp, *partial)) {
4429 *(g_utf8_offset_to_pointer(*partial, *most_matched)) = '\0';
4430 if (*most_matched < (goffset)g_utf8_strlen(tmp, -1))
4431 *(g_utf8_offset_to_pointer(tmp, *most_matched)) = '\0';
4432 (*most_matched)--;
4434 (*most_matched)++;
4436 g_free(tmp);
4439 *matches = g_list_insert_sorted(*matches, g_strdup(name),
4440 (GCompareFunc)purple_utf8_strcasecmp);
4443 static gboolean
4444 is_first_container(WebKitDOMNode *container)
4446 gchar *name;
4447 WebKitDOMNode *parent;
4449 while (container) {
4450 parent = webkit_dom_node_get_parent_node(container);
4451 if (parent) {
4452 name = webkit_dom_node_get_node_name(parent);
4454 if (!strcmp(name, "BODY")) {
4455 g_free(name);
4457 if (webkit_dom_node_get_previous_sibling(container) == NULL)
4458 return TRUE;
4459 else
4460 return FALSE;
4462 g_free(name);
4464 else
4465 break;
4467 container = parent;
4470 return FALSE;
4473 static gboolean
4474 tab_complete(PurpleConversation *conv)
4476 PidginConversation *gtkconv;
4477 WebKitDOMNode *container;
4478 glong caret, word_start, content_len;
4479 int most_matched = -1, colon = 0;
4480 char *ch, *ch2 = NULL;
4481 char *entered, *partial = NULL;
4482 char *content, *sub1, *sub2, *modified;
4483 const char *prefix;
4484 GList *matches = NULL;
4485 gboolean command = FALSE;
4486 gsize entered_chars = 0;
4488 gtkconv = PIDGIN_CONVERSATION(conv);
4489 pidgin_webview_get_caret(PIDGIN_WEBVIEW(gtkconv->entry), &container, &caret);
4491 /* if there's nothing there just return */
4492 if (caret <= 0)
4493 return PURPLE_IS_CHAT_CONVERSATION(conv);
4495 content = webkit_dom_node_get_node_value(container);
4496 content_len = g_utf8_strlen(content, -1);
4498 /* if we're at the end of ":" or ": " we need to move back 1 or 2 spaces */
4499 if (caret >= 2) {
4500 ch = g_utf8_offset_to_pointer(content, caret - 2);
4501 ch2 = g_utf8_find_next_char(ch, NULL);
4504 if (caret >= 2 && *ch == ':' && g_unichar_isspace(g_utf8_get_char(ch2)))
4505 colon = 2;
4506 else if (caret >= 1 && content[caret - 1] == ':')
4507 colon = 1;
4509 caret -= colon;
4510 word_start = caret;
4512 /* find the start of the word that we're tabbing. */
4513 ch = g_utf8_offset_to_pointer(content, caret);
4514 while ((ch = g_utf8_find_prev_char(content, ch))) {
4515 if (!g_unichar_isspace(g_utf8_get_char(ch)))
4516 --word_start;
4517 else
4518 break;
4521 prefix = pidgin_get_cmd_prefix();
4522 if (word_start == 0 &&
4523 ((gsize)caret >= strlen(prefix)) && !strncmp(content, prefix, strlen(prefix))) {
4524 command = TRUE;
4525 word_start += strlen(prefix);
4528 entered = g_utf8_substring(content, word_start, caret);
4529 entered_chars = g_utf8_strlen(entered, -1);
4531 if (!entered_chars) {
4532 g_free(content);
4533 g_free(entered);
4534 return PURPLE_IS_CHAT_CONVERSATION(conv);
4537 if (command) {
4538 GList *list = purple_cmd_list(conv);
4539 GList *l;
4541 /* Commands */
4542 for (l = list; l != NULL; l = l->next) {
4543 tab_complete_process_item(&most_matched, entered, entered_chars, &partial,
4544 &matches, l->data);
4546 g_list_free(list);
4547 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
4548 GList *l, *users;
4549 GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(PIDGIN_CONVERSATION(conv)->u.chat->list));
4550 GtkTreeIter iter;
4551 int f;
4553 /* Users */
4554 users = purple_chat_conversation_get_users(PURPLE_CHAT_CONVERSATION(conv));
4555 for (l = users; l != NULL; l = l->next) {
4556 tab_complete_process_item(&most_matched, entered, entered_chars, &partial,
4557 &matches, purple_chat_user_get_name((PurpleChatUser *)l->data));
4559 g_list_free(users);
4561 /* Aliases */
4562 if (gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter))
4564 do {
4565 char *name;
4566 char *alias;
4568 gtk_tree_model_get(model, &iter,
4569 CHAT_USERS_NAME_COLUMN, &name,
4570 CHAT_USERS_ALIAS_COLUMN, &alias,
4571 -1);
4573 if (name && alias && strcmp(name, alias))
4574 tab_complete_process_item(&most_matched, entered, entered_chars, &partial,
4575 &matches, alias);
4576 g_free(name);
4577 g_free(alias);
4579 f = gtk_tree_model_iter_next(model, &iter);
4580 } while (f != 0);
4582 } else {
4583 g_free(content);
4584 g_free(entered);
4585 return FALSE;
4588 /* if there weren't any matches, return */
4589 if (!matches) {
4590 /* if matches isn't set partials won't be either */
4591 g_free(content);
4592 g_free(entered);
4593 return PURPLE_IS_CHAT_CONVERSATION(conv);
4596 sub1 = g_utf8_substring(content, 0, word_start);
4597 sub2 = g_utf8_substring(content, caret, content_len);
4599 if (!matches->next) {
4600 /* there was only one match. fill it in. */
4602 if (!colon && !word_start && is_first_container(container)) {
4603 char *tmp = NULL;
4604 if (caret < content_len) {
4605 tmp = g_strdup_printf("%s: ", (char *)matches->data);
4606 } else {
4607 char nbsp[6] = {0};
4608 g_unichar_to_utf8(0xA0, nbsp);
4609 tmp = g_strdup_printf("%s:%s", (char *)matches->data, nbsp);
4612 modified = g_strdup_printf("%s%s", tmp, sub2);
4613 webkit_dom_node_set_node_value(container, modified, NULL);
4614 pidgin_webview_set_caret(PIDGIN_WEBVIEW(gtkconv->entry), container,
4615 g_utf8_strlen(tmp, -1));
4616 g_free(tmp);
4617 g_free(modified);
4619 else {
4620 modified = g_strdup_printf("%s%s%s", sub1, (char *)matches->data, sub2);
4621 webkit_dom_node_set_node_value(container, modified, NULL);
4622 pidgin_webview_set_caret(PIDGIN_WEBVIEW(gtkconv->entry), container,
4623 word_start + g_utf8_strlen(matches->data, -1) + colon);
4624 g_free(modified);
4627 g_free(matches->data);
4628 g_list_free(matches);
4630 else {
4632 * there were lots of matches, fill in as much as possible
4633 * and display all of them
4635 char *addthis = g_malloc0(1);
4637 while (matches) {
4638 char *tmp = addthis;
4639 addthis = g_strconcat(tmp, matches->data, " ", NULL);
4640 g_free(tmp);
4641 g_free(matches->data);
4642 matches = g_list_remove(matches, matches->data);
4645 purple_conversation_write_system_message(conv, addthis, PURPLE_MESSAGE_NO_LOG);
4647 modified = g_strdup_printf("%s%s%s", sub1, partial, sub2);
4648 webkit_dom_node_set_node_value(container, modified, NULL);
4649 pidgin_webview_set_caret(PIDGIN_WEBVIEW(gtkconv->entry), container,
4650 word_start + g_utf8_strlen(partial, -1) + colon);
4651 g_free(addthis);
4652 g_free(modified);
4655 g_free(content);
4656 g_free(entered);
4657 g_free(partial);
4658 g_free(sub1);
4659 g_free(sub2);
4661 return TRUE;
4664 static void topic_callback(GtkWidget *w, PidginConversation *gtkconv)
4666 PurpleProtocol *protocol = NULL;
4667 PurpleConnection *gc;
4668 PurpleConversation *conv = gtkconv->active_conv;
4669 PidginChatPane *gtkchat;
4670 char *new_topic;
4671 const char *current_topic;
4673 gc = purple_conversation_get_connection(conv);
4675 if(!gc || !(protocol = purple_connection_get_protocol(gc)))
4676 return;
4678 if(!PURPLE_PROTOCOL_IMPLEMENTS(protocol, CHAT_IFACE, set_topic))
4679 return;
4681 gtkconv = PIDGIN_CONVERSATION(conv);
4682 gtkchat = gtkconv->u.chat;
4683 new_topic = g_strdup(gtk_entry_get_text(GTK_ENTRY(gtkchat->topic_text)));
4684 current_topic = purple_chat_conversation_get_topic(PURPLE_CHAT_CONVERSATION(conv));
4686 if(current_topic && !g_utf8_collate(new_topic, current_topic)){
4687 g_free(new_topic);
4688 return;
4691 if (current_topic)
4692 gtk_entry_set_text(GTK_ENTRY(gtkchat->topic_text), current_topic);
4693 else
4694 gtk_entry_set_text(GTK_ENTRY(gtkchat->topic_text), "");
4696 purple_protocol_chat_iface_set_topic(protocol, gc, purple_chat_conversation_get_id(PURPLE_CHAT_CONVERSATION(conv)),
4697 new_topic);
4699 g_free(new_topic);
4702 static gint
4703 sort_chat_users(GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, gpointer userdata)
4705 PurpleChatUserFlags f1 = 0, f2 = 0;
4706 char *user1 = NULL, *user2 = NULL;
4707 gboolean buddy1 = FALSE, buddy2 = FALSE;
4708 gint ret = 0;
4710 gtk_tree_model_get(model, a,
4711 CHAT_USERS_ALIAS_KEY_COLUMN, &user1,
4712 CHAT_USERS_FLAGS_COLUMN, &f1,
4713 CHAT_USERS_WEIGHT_COLUMN, &buddy1,
4714 -1);
4715 gtk_tree_model_get(model, b,
4716 CHAT_USERS_ALIAS_KEY_COLUMN, &user2,
4717 CHAT_USERS_FLAGS_COLUMN, &f2,
4718 CHAT_USERS_WEIGHT_COLUMN, &buddy2,
4719 -1);
4721 /* Only sort by membership levels */
4722 f1 &= PURPLE_CHAT_USER_VOICE | PURPLE_CHAT_USER_HALFOP | PURPLE_CHAT_USER_OP |
4723 PURPLE_CHAT_USER_FOUNDER;
4724 f2 &= PURPLE_CHAT_USER_VOICE | PURPLE_CHAT_USER_HALFOP | PURPLE_CHAT_USER_OP |
4725 PURPLE_CHAT_USER_FOUNDER;
4727 if (user1 == NULL || user2 == NULL) {
4728 if (!(user1 == NULL && user2 == NULL))
4729 ret = (user1 == NULL) ? -1: 1;
4730 } else if (f1 != f2) {
4731 /* sort more important users first */
4732 ret = (f1 > f2) ? -1 : 1;
4733 } else if (buddy1 != buddy2) {
4734 ret = (buddy1 > buddy2) ? -1 : 1;
4735 } else {
4736 ret = strcmp(user1, user2);
4739 g_free(user1);
4740 g_free(user2);
4742 return ret;
4745 static void
4746 update_chat_alias(PurpleBuddy *buddy, PurpleChatConversation *chat, PurpleConnection *gc, PurpleProtocol *protocol)
4748 PidginConversation *gtkconv = PIDGIN_CONVERSATION(PURPLE_CONVERSATION(chat));
4749 PurpleAccount *account = purple_conversation_get_account(PURPLE_CONVERSATION(chat));
4750 GtkTreeModel *model;
4751 char *normalized_name;
4752 GtkTreeIter iter;
4753 int f;
4755 g_return_if_fail(buddy != NULL);
4756 g_return_if_fail(chat != NULL);
4758 /* This is safe because this callback is only used in chats, not IMs. */
4759 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkconv->u.chat->list));
4761 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter))
4762 return;
4764 normalized_name = g_strdup(purple_normalize(account, purple_buddy_get_name(buddy)));
4766 do {
4767 char *name;
4769 gtk_tree_model_get(model, &iter, CHAT_USERS_NAME_COLUMN, &name, -1);
4771 if (!strcmp(normalized_name, purple_normalize(account, name))) {
4772 const char *alias = name;
4773 char *tmp;
4774 char *alias_key = NULL;
4775 PurpleBuddy *buddy2;
4777 if (strcmp(purple_chat_conversation_get_nick(chat), purple_normalize(account, name))) {
4778 /* This user is not me, so look into updating the alias. */
4780 if ((buddy2 = purple_blist_find_buddy(account, name)) != NULL) {
4781 alias = purple_buddy_get_contact_alias(buddy2);
4784 tmp = g_utf8_casefold(alias, -1);
4785 alias_key = g_utf8_collate_key(tmp, -1);
4786 g_free(tmp);
4788 gtk_list_store_set(GTK_LIST_STORE(model), &iter,
4789 CHAT_USERS_ALIAS_COLUMN, alias,
4790 CHAT_USERS_ALIAS_KEY_COLUMN, alias_key,
4791 -1);
4792 g_free(alias_key);
4794 g_free(name);
4795 break;
4798 f = gtk_tree_model_iter_next(model, &iter);
4800 g_free(name);
4801 } while (f != 0);
4803 g_free(normalized_name);
4806 static void
4807 blist_node_aliased_cb(PurpleBlistNode *node, const char *old_alias, PurpleChatConversation *chat)
4809 PurpleConnection *gc;
4810 PurpleProtocol *protocol;
4811 PurpleConversation *conv = PURPLE_CONVERSATION(chat);
4813 g_return_if_fail(node != NULL);
4814 g_return_if_fail(conv != NULL);
4816 gc = purple_conversation_get_connection(conv);
4817 g_return_if_fail(gc != NULL);
4818 g_return_if_fail(purple_connection_get_protocol(gc) != NULL);
4819 protocol = purple_connection_get_protocol(gc);
4821 if (purple_protocol_get_options(protocol) & OPT_PROTO_UNIQUE_CHATNAME)
4822 return;
4824 if (PURPLE_IS_CONTACT(node))
4826 PurpleBlistNode *bnode;
4828 for(bnode = node->child; bnode; bnode = bnode->next) {
4830 if(!PURPLE_IS_BUDDY(bnode))
4831 continue;
4833 update_chat_alias((PurpleBuddy *)bnode, chat, gc, protocol);
4836 else if (PURPLE_IS_BUDDY(node))
4837 update_chat_alias((PurpleBuddy *)node, chat, gc, protocol);
4838 else if (PURPLE_IS_CHAT(node) &&
4839 purple_conversation_get_account(conv) == purple_chat_get_account((PurpleChat*)node))
4841 if (old_alias == NULL || g_utf8_collate(old_alias, purple_conversation_get_title(conv)) == 0)
4842 pidgin_conv_update_fields(conv, PIDGIN_CONV_SET_TITLE);
4846 static void
4847 buddy_cb_common(PurpleBuddy *buddy, PurpleChatConversation *chat, gboolean is_buddy)
4849 GtkTreeModel *model;
4850 char *normalized_name;
4851 GtkTreeIter iter;
4852 GtkTextTag *texttag;
4853 PurpleConversation *conv = PURPLE_CONVERSATION(chat);
4854 int f;
4856 g_return_if_fail(buddy != NULL);
4857 g_return_if_fail(conv != NULL);
4859 /* Do nothing if the buddy does not belong to the conv's account */
4860 if (purple_buddy_get_account(buddy) != purple_conversation_get_account(conv))
4861 return;
4863 /* This is safe because this callback is only used in chats, not IMs. */
4864 model = gtk_tree_view_get_model(GTK_TREE_VIEW(PIDGIN_CONVERSATION(conv)->u.chat->list));
4866 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter))
4867 return;
4869 normalized_name = g_strdup(purple_normalize(purple_conversation_get_account(conv), purple_buddy_get_name(buddy)));
4871 do {
4872 char *name;
4874 gtk_tree_model_get(model, &iter, CHAT_USERS_NAME_COLUMN, &name, -1);
4876 if (!strcmp(normalized_name, purple_normalize(purple_conversation_get_account(conv), name))) {
4877 gtk_list_store_set(GTK_LIST_STORE(model), &iter,
4878 CHAT_USERS_WEIGHT_COLUMN, is_buddy ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL, -1);
4879 g_free(name);
4880 break;
4883 f = gtk_tree_model_iter_next(model, &iter);
4885 g_free(name);
4886 } while (f != 0);
4888 g_free(normalized_name);
4890 blist_node_aliased_cb((PurpleBlistNode *)buddy, NULL, chat);
4892 texttag = get_buddy_tag(chat, purple_buddy_get_name(buddy), 0, FALSE); /* XXX: do we want the normalized name? */
4893 if (texttag) {
4894 g_object_set(texttag, "weight", is_buddy ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL, NULL);
4898 static void
4899 buddy_added_cb(PurpleBlistNode *node, PurpleChatConversation *chat)
4901 if (!PURPLE_IS_BUDDY(node))
4902 return;
4904 buddy_cb_common(PURPLE_BUDDY(node), chat, TRUE);
4907 static void
4908 buddy_removed_cb(PurpleBlistNode *node, PurpleChatConversation *chat)
4910 if (!PURPLE_IS_BUDDY(node))
4911 return;
4913 /* If there's another buddy for the same "dude" on the list, do nothing. */
4914 if (purple_blist_find_buddy(purple_buddy_get_account(PURPLE_BUDDY(node)),
4915 purple_buddy_get_name(PURPLE_BUDDY(node))) != NULL)
4916 return;
4918 buddy_cb_common(PURPLE_BUDDY(node), chat, FALSE);
4921 static void
4922 entry_popup_menu_cb(PidginWebView *webview, GtkMenu *menu, gpointer data)
4924 GtkWidget *menuitem;
4925 PidginConversation *gtkconv = data;
4926 gboolean is_empty;
4928 g_return_if_fail(menu != NULL);
4929 g_return_if_fail(gtkconv != NULL);
4931 menuitem = pidgin_new_menu_item(NULL, _("_Send"), NULL,
4932 G_CALLBACK(send_cb), gtkconv);
4933 is_empty = pidgin_webview_is_empty(webview);
4934 if (is_empty)
4935 gtk_widget_set_sensitive(menuitem, FALSE);
4936 gtk_menu_shell_insert(GTK_MENU_SHELL(menu), menuitem, 0);
4938 menuitem = gtk_separator_menu_item_new();
4939 gtk_widget_show(menuitem);
4940 gtk_menu_shell_insert(GTK_MENU_SHELL(menu), menuitem, 1);
4943 static gboolean
4944 resize_webview_cb(PidginConversation *gtkconv)
4946 PidginWebView *webview;
4947 gint min_lines;
4948 gint max_height;
4949 gint min_height;
4950 gint font_size;
4951 gint total_height;
4952 gint height;
4953 gint toolbar_size;
4954 gint old_w;
4955 gint old_h;
4956 GtkAllocation webview_allocation;
4957 GtkAllocation entry_allocation;
4959 webview = PIDGIN_WEBVIEW(gtkconv->entry);
4961 /* Get text height from the DOM */
4962 height = pidgin_webview_get_DOM_height(webview);
4964 /* Find the height of the conversation window to calculate the maximum possible entry
4965 * size (1/2 of the window)
4967 gtk_widget_get_allocation(gtkconv->webview, &webview_allocation);
4968 gtk_widget_get_allocation(gtkconv->entry, &entry_allocation);
4969 total_height = webview_allocation.height + entry_allocation.height;
4970 max_height = total_height / 2;
4972 /* Get size of the characters to calculate initial minimum space for the entry */
4973 font_size = pidgin_webview_get_font_size(webview);
4975 /* Allow to have a minimum of "min_lines" height as defined in the preference */
4976 min_lines = purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/minimum_entry_lines");
4977 min_height = (font_size + WEBVIEW_DOM_FONT_PADDING) * min_lines + WEBVIEW_DOM_TEXT_PADDING;
4980 /* Take into account the size of the formatting toolbar */
4981 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/show_formatting_toolbar")) {
4982 toolbar_size = gtk_widget_get_allocated_height(pidgin_webview_get_toolbar(webview));
4983 } else {
4984 toolbar_size = 0;
4987 /* Calculate conv entry height */
4988 height = CLAMP(height, MIN(min_height, max_height), max_height);
4989 /* Add the size used by the toolbar so we always take it into consideration. */
4990 height += toolbar_size;
4992 /* Actually set the size of the gtkconv entry widget. */
4993 gtk_widget_get_size_request(gtkconv->lower_hbox, &old_w, &old_h);
4994 if (old_w != -1 || old_h != height) {
4995 gtk_widget_set_size_request(gtkconv->lower_hbox, -1, height);
4996 purple_debug_info("pidgin", "resizing to %d, %d lines\n", height, min_lines);
4999 return FALSE;
5002 static void
5003 minimum_entry_lines_pref_cb(const char *name,
5004 PurplePrefType type,
5005 gconstpointer value,
5006 gpointer data)
5008 GList *l = purple_conversations_get_all();
5009 PurpleConversation *conv;
5010 while (l != NULL)
5012 conv = (PurpleConversation *)l->data;
5014 if (PIDGIN_IS_PIDGIN_CONVERSATION(conv))
5015 resize_webview_cb(PIDGIN_CONVERSATION(conv));
5017 l = l->next;
5021 static void
5022 setup_chat_topic(PidginConversation *gtkconv, GtkWidget *vbox)
5024 PurpleConversation *conv = gtkconv->active_conv;
5025 PurpleConnection *gc = purple_conversation_get_connection(conv);
5026 PurpleProtocol *protocol = purple_connection_get_protocol(gc);
5027 if (purple_protocol_get_options(protocol) & OPT_PROTO_CHAT_TOPIC)
5029 GtkWidget *hbox, *label;
5030 PidginChatPane *gtkchat = gtkconv->u.chat;
5032 hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PIDGIN_HIG_BOX_SPACE);
5033 gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);
5035 label = gtk_label_new(_("Topic:"));
5036 gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0);
5038 gtkchat->topic_text = gtk_entry_new();
5039 gtk_widget_set_size_request(gtkchat->topic_text, -1, BUDDYICON_SIZE_MIN);
5041 if(!PURPLE_PROTOCOL_IMPLEMENTS(protocol, CHAT_IFACE, set_topic)) {
5042 gtk_editable_set_editable(GTK_EDITABLE(gtkchat->topic_text), FALSE);
5043 } else {
5044 g_signal_connect(G_OBJECT(gtkchat->topic_text), "activate",
5045 G_CALLBACK(topic_callback), gtkconv);
5048 gtk_box_pack_start(GTK_BOX(hbox), gtkchat->topic_text, TRUE, TRUE, 0);
5049 g_signal_connect(G_OBJECT(gtkchat->topic_text), "key_press_event",
5050 G_CALLBACK(entry_key_press_cb), gtkconv);
5054 static gboolean
5055 pidgin_conv_userlist_create_tooltip(GtkWidget *tipwindow, GtkTreePath *path,
5056 gpointer userdata, int *w, int *h)
5058 PidginConversation *gtkconv = userdata;
5059 GtkTreeIter iter;
5060 GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkconv->u.chat->list));
5061 PurpleConversation *conv = gtkconv->active_conv;
5062 PurpleBlistNode *node;
5063 PurpleProtocol *protocol;
5064 PurpleAccount *account = purple_conversation_get_account(conv);
5065 char *who = NULL;
5067 if (purple_account_get_connection(account) == NULL)
5068 return FALSE;
5070 if (!gtk_tree_model_get_iter(GTK_TREE_MODEL(model), &iter, path))
5071 return FALSE;
5073 gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, CHAT_USERS_NAME_COLUMN, &who, -1);
5075 protocol = purple_connection_get_protocol(purple_account_get_connection(account));
5076 node = (PurpleBlistNode*)(purple_blist_find_buddy(purple_conversation_get_account(conv), who));
5077 if (node && protocol && (purple_protocol_get_options(protocol) & OPT_PROTO_UNIQUE_CHATNAME))
5078 pidgin_blist_draw_tooltip(node, gtkconv->infopane);
5080 g_free(who);
5081 return FALSE;
5084 static void
5085 setup_chat_userlist(PidginConversation *gtkconv, GtkWidget *hpaned)
5087 PidginChatPane *gtkchat = gtkconv->u.chat;
5088 GtkWidget *lbox, *list;
5089 GtkListStore *ls;
5090 GtkCellRenderer *rend;
5091 GtkTreeViewColumn *col;
5092 int ul_width;
5093 void *blist_handle = purple_blist_get_handle();
5094 PurpleConversation *conv = gtkconv->active_conv;
5096 /* Build the right pane. */
5097 lbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, PIDGIN_HIG_BOX_SPACE);
5098 gtk_paned_pack2(GTK_PANED(hpaned), lbox, FALSE, TRUE);
5099 gtk_widget_show(lbox);
5101 /* Setup the label telling how many people are in the room. */
5102 gtkchat->count = gtk_label_new(_("0 people in room"));
5103 gtk_label_set_ellipsize(GTK_LABEL(gtkchat->count), PANGO_ELLIPSIZE_END);
5104 gtk_box_pack_start(GTK_BOX(lbox), gtkchat->count, FALSE, FALSE, 0);
5105 gtk_widget_show(gtkchat->count);
5107 /* Setup the list of users. */
5109 ls = gtk_list_store_new(CHAT_USERS_COLUMNS, GDK_TYPE_PIXBUF, G_TYPE_STRING,
5110 G_TYPE_STRING, G_TYPE_STRING, G_TYPE_INT,
5111 GDK_TYPE_RGBA, G_TYPE_INT, G_TYPE_STRING);
5112 gtk_tree_sortable_set_sort_func(GTK_TREE_SORTABLE(ls), CHAT_USERS_ALIAS_KEY_COLUMN,
5113 sort_chat_users, NULL, NULL);
5115 list = gtk_tree_view_new_with_model(GTK_TREE_MODEL(ls));
5117 /* Allow a user to specify gtkrc settings for the chat userlist only */
5118 gtk_widget_set_name(list, "pidgin_conv_userlist");
5120 rend = gtk_cell_renderer_pixbuf_new();
5121 g_object_set(G_OBJECT(rend),
5122 "stock-size", gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL),
5123 NULL);
5124 col = gtk_tree_view_column_new_with_attributes(NULL, rend,
5125 "stock-id", CHAT_USERS_ICON_STOCK_COLUMN, NULL);
5126 gtk_tree_view_column_set_sizing(col, GTK_TREE_VIEW_COLUMN_AUTOSIZE);
5127 gtk_tree_view_append_column(GTK_TREE_VIEW(list), col);
5128 ul_width = purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/userlist_width");
5129 gtk_widget_set_size_request(lbox, ul_width, -1);
5131 /* Hack to prevent completely collapsed userlist coming back with a 1 pixel width.
5132 * I would have liked to use the GtkPaned "max-position", but for some reason that didn't work */
5133 if (ul_width == 0)
5134 gtk_paned_set_position(GTK_PANED(hpaned), 999999);
5136 g_signal_connect(G_OBJECT(list), "button_press_event",
5137 G_CALLBACK(right_click_chat_cb), gtkconv);
5138 g_signal_connect(G_OBJECT(list), "row-activated",
5139 G_CALLBACK(activate_list_cb), gtkconv);
5140 g_signal_connect(G_OBJECT(list), "popup-menu",
5141 G_CALLBACK(gtkconv_chat_popup_menu_cb), gtkconv);
5142 g_signal_connect(G_OBJECT(lbox), "size-allocate", G_CALLBACK(lbox_size_allocate_cb), gtkconv);
5144 pidgin_tooltip_setup_for_treeview(list, gtkconv,
5145 pidgin_conv_userlist_create_tooltip, NULL);
5147 rend = gtk_cell_renderer_text_new();
5148 g_object_set(rend,
5149 "foreground-set", TRUE,
5150 "weight-set", TRUE,
5151 NULL);
5152 g_object_set(G_OBJECT(rend), "editable", TRUE, NULL);
5154 col = gtk_tree_view_column_new_with_attributes(NULL, rend,
5155 "text", CHAT_USERS_ALIAS_COLUMN,
5156 "foreground-rgba", CHAT_USERS_COLOR_COLUMN,
5157 "weight", CHAT_USERS_WEIGHT_COLUMN,
5158 NULL);
5160 purple_signal_connect(blist_handle, "blist-node-added",
5161 gtkchat, PURPLE_CALLBACK(buddy_added_cb), conv);
5162 purple_signal_connect(blist_handle, "blist-node-removed",
5163 gtkchat, PURPLE_CALLBACK(buddy_removed_cb), conv);
5164 purple_signal_connect(blist_handle, "blist-node-aliased",
5165 gtkchat, PURPLE_CALLBACK(blist_node_aliased_cb), conv);
5167 gtk_tree_view_column_set_expand(col, TRUE);
5168 g_object_set(rend, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
5170 gtk_tree_view_append_column(GTK_TREE_VIEW(list), col);
5172 gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(list), FALSE);
5173 gtk_widget_show(list);
5175 gtkchat->list = list;
5177 gtk_box_pack_start(GTK_BOX(lbox),
5178 pidgin_make_scrollable(list, GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC, GTK_SHADOW_IN, -1, -1),
5179 TRUE, TRUE, 0);
5182 static gboolean
5183 pidgin_conv_create_tooltip(GtkWidget *tipwindow, gpointer userdata, int *w, int *h)
5185 PurpleBlistNode *node = NULL;
5186 PurpleConversation *conv;
5187 PidginConversation *gtkconv = userdata;
5189 conv = gtkconv->active_conv;
5190 if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
5191 node = (PurpleBlistNode*)(purple_blist_find_chat(purple_conversation_get_account(conv), purple_conversation_get_name(conv)));
5192 if (!node)
5193 node = g_object_get_data(G_OBJECT(gtkconv->webview), "transient_chat");
5194 } else {
5195 node = (PurpleBlistNode*)(purple_blist_find_buddy(purple_conversation_get_account(conv), purple_conversation_get_name(conv)));
5196 #if 0
5197 /* Using the transient blist nodes to show the tooltip doesn't quite work yet. */
5198 if (!node)
5199 node = g_object_get_data(G_OBJECT(gtkconv->webview), "transient_buddy");
5200 #endif
5203 if (node)
5204 pidgin_blist_draw_tooltip(node, gtkconv->infopane);
5205 return FALSE;
5208 /* Quick Find {{{ */
5209 static gboolean
5210 pidgin_conv_end_quickfind(PidginConversation *gtkconv)
5212 GtkStyleContext *context = gtk_widget_get_style_context(gtkconv->quickfind_entry);
5213 gtk_style_context_remove_class(context, "not-found");
5215 webkit_web_view_unmark_text_matches(WEBKIT_WEB_VIEW(gtkconv->webview));
5216 gtk_widget_hide(gtkconv->quickfind_container);
5218 gtk_widget_grab_focus(gtkconv->entry);
5219 return TRUE;
5222 static gboolean
5223 quickfind_process_input(GtkWidget *entry, GdkEventKey *event, PidginConversation *gtkconv)
5225 switch (event->keyval) {
5226 case GDK_KEY_Return:
5227 case GDK_KEY_KP_Enter:
5228 if (webkit_web_view_search_text(WEBKIT_WEB_VIEW(gtkconv->webview), gtk_entry_get_text(GTK_ENTRY(entry)), FALSE, TRUE, TRUE)) {
5229 GtkStyleContext *context = gtk_widget_get_style_context(gtkconv->quickfind_entry);
5230 gtk_style_context_remove_class(context, "not-found");
5231 } else {
5232 GtkStyleContext *context = gtk_widget_get_style_context(gtkconv->quickfind_entry);
5233 gtk_style_context_add_class(context, "not-found");
5235 break;
5236 case GDK_KEY_Escape:
5237 pidgin_conv_end_quickfind(gtkconv);
5238 break;
5239 default:
5240 return FALSE;
5242 return TRUE;
5245 static void
5246 pidgin_conv_setup_quickfind(PidginConversation *gtkconv, GtkWidget *container)
5248 GtkWidget *widget = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
5249 GtkWidget *label, *entry, *close;
5250 GtkStyleContext *context;
5251 GtkCssProvider *filter_css;
5252 const gchar filter_style[] =
5253 ".not-found {"
5254 "color: @error_fg_color;"
5255 "text-shadow: 0 1px @error_text_shadow;"
5256 "background-image: none;"
5257 "background-color: @error_bg_color;"
5258 "}";
5260 gtk_box_pack_start(GTK_BOX(container), widget, FALSE, FALSE, 0);
5262 close = pidgin_create_small_button(gtk_label_new("×"));
5263 gtk_box_pack_start(GTK_BOX(widget), close, FALSE, FALSE, 0);
5264 gtk_widget_set_tooltip_text(close, _("Close Find bar"));
5266 label = gtk_label_new(_("Find:"));
5267 gtk_box_pack_start(GTK_BOX(widget), label, FALSE, FALSE, 10);
5269 entry = gtk_entry_new();
5270 gtk_box_pack_start(GTK_BOX(widget), entry, TRUE, TRUE, 0);
5271 filter_css = gtk_css_provider_new();
5272 gtk_css_provider_load_from_data(filter_css, filter_style, -1, NULL);
5273 context = gtk_widget_get_style_context(entry);
5274 gtk_style_context_add_provider(context,
5275 GTK_STYLE_PROVIDER(filter_css),
5276 GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
5278 gtkconv->quickfind_entry = entry;
5279 gtkconv->quickfind_container = widget;
5281 /* Hook to signals and stuff */
5282 g_signal_connect(G_OBJECT(entry), "key-press-event",
5283 G_CALLBACK(quickfind_process_input), gtkconv);
5284 g_signal_connect_swapped(G_OBJECT(close), "button-press-event",
5285 G_CALLBACK(pidgin_conv_end_quickfind), gtkconv);
5288 /* }}} */
5290 static char *
5291 replace_header_tokens(PurpleConversation *conv, const char *text)
5293 PurpleAccount *account = purple_conversation_get_account(conv);
5294 GString *str;
5295 const char *cur = text;
5296 const char *prev = cur;
5297 time_t mtime;
5298 struct tm *tm = NULL;
5300 if (text == NULL || *text == '\0')
5301 return NULL;
5303 str = g_string_new(NULL);
5304 while ((cur = strchr(cur, '%'))) {
5305 char *freeval = NULL;
5306 const char *replace = NULL;
5307 const char *fin = NULL;
5309 if (g_str_has_prefix(cur, "%chatName%")) {
5310 replace = purple_conversation_get_name(conv);
5312 } else if (g_str_has_prefix(cur, "%sourceName%")) {
5313 replace = purple_account_get_private_alias(account);
5314 if (replace == NULL)
5315 replace = purple_account_get_username(account);
5317 } else if (g_str_has_prefix(cur, "%destinationName%")) {
5318 PurpleBuddy *buddy = purple_blist_find_buddy(account, purple_conversation_get_name(conv));
5319 if (buddy) {
5320 replace = purple_buddy_get_alias(buddy);
5321 } else {
5322 replace = purple_conversation_get_name(conv);
5325 } else if (g_str_has_prefix(cur, "%incomingIconPath%")) {
5326 PurpleBuddyIcon *icon = purple_im_conversation_get_icon(PURPLE_IM_CONVERSATION(conv));
5327 if (icon)
5328 replace = purple_buddy_icon_get_full_path(icon);
5330 } else if (g_str_has_prefix(cur, "%outgoingIconPath%")) {
5331 replace = purple_account_get_buddy_icon_path(account);
5333 } else if (g_str_has_prefix(cur, "%timeOpened")) {
5334 const char *tmp = cur + strlen("%timeOpened");
5336 if (*tmp == '{') {
5337 const char *end;
5338 tmp++;
5339 end = strstr(tmp, "}%");
5340 if (!end) /* Invalid string */
5341 continue;
5342 if (!tm) {
5343 mtime = time(NULL);
5344 tm = localtime(&mtime);
5346 replace = freeval = purple_uts35_to_str(tmp, end - tmp, tm);
5347 fin = end + 1;
5348 } else {
5349 if (!tm) {
5350 mtime = time(NULL);
5351 tm = localtime(&mtime);
5354 replace = purple_utf8_strftime("%X", tm);
5357 } else if (g_str_has_prefix(cur, "%dateOpened%")) {
5358 if (!tm) {
5359 mtime = time(NULL);
5360 tm = localtime(&mtime);
5363 replace = purple_date_format_short(tm);
5365 } else {
5366 cur++;
5367 continue;
5370 /* Here we have a replacement to make */
5371 g_string_append_len(str, prev, cur - prev);
5372 if (replace)
5373 g_string_append(str, replace);
5375 /* And update the pointers */
5376 if (fin) {
5377 prev = cur = fin + 1;
5378 } else {
5379 prev = cur = strchr(cur + 1, '%') + 1;
5381 g_free(freeval);
5382 freeval = NULL;
5385 /* And wrap it up */
5386 g_string_append(str, prev);
5387 return g_string_free(str, FALSE);
5390 static char *
5391 replace_template_tokens(PidginConvTheme *theme, const char *header, const char *footer)
5393 GString *str;
5394 const char *text;
5395 char **ms;
5396 char *path;
5398 text = pidgin_conversation_theme_get_template(theme, PIDGIN_CONVERSATION_THEME_TEMPLATE_MAIN);
5399 if (text == NULL)
5400 return NULL;
5402 ms = g_strsplit(text, "%@", 6);
5403 if (ms[0] == NULL || ms[1] == NULL || ms[2] == NULL || ms[3] == NULL || ms[4] == NULL || ms[5] == NULL) {
5404 g_strfreev(ms);
5405 return NULL;
5408 str = g_string_new(NULL);
5410 g_string_append(str, ms[0]);
5411 g_string_append(str, "file://");
5412 path = pidgin_conversation_theme_get_template_path(theme);
5413 g_string_append(str, path);
5414 g_free(path);
5416 g_string_append(str, ms[1]);
5418 text = pidgin_conversation_theme_get_template(theme, PIDGIN_CONVERSATION_THEME_TEMPLATE_BASESTYLE_CSS);
5419 g_string_append(str, text);
5421 g_string_append(str, ms[2]);
5423 g_string_append(str, "file://");
5424 path = pidgin_conversation_theme_get_css_path(theme);
5425 g_string_append(str, path);
5426 g_free(path);
5428 g_string_append(str, ms[3]);
5429 if (header)
5430 g_string_append(str, header);
5431 g_string_append(str, ms[4]);
5432 if (footer)
5433 g_string_append(str, footer);
5434 g_string_append(str, ms[5]);
5436 g_strfreev(ms);
5438 return g_string_free(str, FALSE);
5441 static void
5442 set_theme_webkit_settings(WebKitWebView *webview, PidginConvTheme *theme)
5444 WebKitWebSettings *settings;
5445 const GValue *val;
5447 g_object_get(G_OBJECT(webview), "settings", &settings, NULL);
5449 val = pidgin_conversation_theme_lookup(theme, "DefaultFontFamily", TRUE);
5450 if (val && G_VALUE_HOLDS_STRING(val))
5452 const gchar *font_family = g_value_get_string(val);
5453 #ifdef _WIN32
5454 /* XXX: a hack for not converting backslash to yen sign.
5455 * See gtkwebview.c: pidgin_webview_new.
5457 if (g_ascii_strcasecmp(font_family, "sans-serif") == 0)
5458 font_family = NULL;
5459 #endif
5460 if (font_family)
5461 g_object_set(G_OBJECT(settings), "default-font-family", font_family, NULL);
5464 val = pidgin_conversation_theme_lookup(theme, "DefaultFontSize", TRUE);
5465 if (val && G_VALUE_HOLDS_INT(val))
5466 g_object_set(G_OBJECT(settings), "default-font-size", GINT_TO_POINTER(g_value_get_int(val)), NULL);
5468 val = pidgin_conversation_theme_lookup(theme, "DefaultBackgroundIsTransparent", TRUE);
5469 if (val && G_VALUE_HOLDS_BOOLEAN(val))
5470 /* this does not work :( */
5471 webkit_web_view_set_transparent(webview, g_value_get_boolean(val));
5474 static void
5475 conv_variant_changed_cb(GObject *gobject, GParamSpec *pspec, gpointer user_data)
5477 PidginConversation *gtkconv = user_data;
5478 char *path, *js;
5480 path = pidgin_conversation_theme_get_css_path(PIDGIN_CONV_THEME(gobject));
5481 js = g_strdup_printf("setStylesheet(\"mainStyle\", \"file://%s\");", path);
5482 g_free(path);
5483 pidgin_webview_safe_execute_script(PIDGIN_WEBVIEW(gtkconv->webview), js);
5484 g_free(js);
5487 static void
5488 load_conv_theme(PidginConversation *gtkconv)
5490 char *header, *footer;
5491 char *template;
5492 char *basedir, *baseuri;
5494 header = replace_header_tokens(gtkconv->active_conv,
5495 pidgin_conversation_theme_get_template(gtkconv->theme, PIDGIN_CONVERSATION_THEME_TEMPLATE_HEADER));
5496 footer = replace_header_tokens(gtkconv->active_conv,
5497 pidgin_conversation_theme_get_template(gtkconv->theme, PIDGIN_CONVERSATION_THEME_TEMPLATE_FOOTER));
5498 template = replace_template_tokens(gtkconv->theme, header, footer);
5499 g_free(header);
5500 g_free(footer);
5502 if (template == NULL)
5503 return;
5505 set_theme_webkit_settings(WEBKIT_WEB_VIEW(gtkconv->webview), gtkconv->theme);
5507 basedir = pidgin_conversation_theme_get_template_path(gtkconv->theme);
5508 baseuri = g_strdup_printf("file://%s", basedir);
5509 webkit_web_view_load_string(WEBKIT_WEB_VIEW(gtkconv->webview), template,
5510 "text/html", "UTF-8", baseuri);
5512 if (PURPLE_IS_CHAT_CONVERSATION(gtkconv->active_conv))
5513 pidgin_webview_safe_execute_script(PIDGIN_WEBVIEW(gtkconv->webview),
5514 "document.getElementById('Chat').className = 'groupchat'");
5516 g_signal_connect(G_OBJECT(gtkconv->theme), "notify::variant",
5517 G_CALLBACK(conv_variant_changed_cb), gtkconv);
5519 g_free(basedir);
5520 g_free(baseuri);
5521 g_free(template);
5524 static GtkWidget *
5525 setup_common_pane(PidginConversation *gtkconv)
5527 GtkWidget *vbox, *frame, *webview_sw, *event_box;
5528 GtkCellRenderer *rend;
5529 GtkTreePath *path;
5530 PurpleConversation *conv = gtkconv->active_conv;
5531 PurpleBuddy *buddy;
5532 gboolean chat = PURPLE_IS_CHAT_CONVERSATION(conv);
5533 int buddyicon_size = 0;
5535 /* Setup the top part of the pane */
5536 vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, PIDGIN_HIG_BOX_SPACE);
5537 gtk_widget_show(vbox);
5539 /* Setup the info pane */
5540 event_box = gtk_event_box_new();
5541 gtk_event_box_set_visible_window(GTK_EVENT_BOX(event_box), FALSE);
5542 gtk_widget_show(event_box);
5543 gtkconv->infopane_hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
5544 gtk_box_pack_start(GTK_BOX(vbox), event_box, FALSE, FALSE, 0);
5545 gtk_container_add(GTK_CONTAINER(event_box), gtkconv->infopane_hbox);
5546 gtk_widget_show(gtkconv->infopane_hbox);
5547 gtk_widget_add_events(event_box,
5548 GDK_POINTER_MOTION_MASK | GDK_LEAVE_NOTIFY_MASK);
5549 g_signal_connect(G_OBJECT(event_box), "button-press-event",
5550 G_CALLBACK(infopane_press_cb), gtkconv);
5552 pidgin_tooltip_setup_for_widget(event_box, gtkconv,
5553 pidgin_conv_create_tooltip, NULL);
5555 gtkconv->infopane = gtk_cell_view_new();
5556 gtkconv->infopane_model = gtk_list_store_new(CONV_NUM_COLUMNS, G_TYPE_STRING, G_TYPE_STRING, GDK_TYPE_PIXBUF, GDK_TYPE_PIXBUF);
5557 gtk_cell_view_set_model(GTK_CELL_VIEW(gtkconv->infopane),
5558 GTK_TREE_MODEL(gtkconv->infopane_model));
5559 g_object_unref(gtkconv->infopane_model);
5560 gtk_list_store_append(gtkconv->infopane_model, &(gtkconv->infopane_iter));
5561 gtk_box_pack_start(GTK_BOX(gtkconv->infopane_hbox), gtkconv->infopane, TRUE, TRUE, 0);
5562 path = gtk_tree_path_new_from_string("0");
5563 gtk_cell_view_set_displayed_row(GTK_CELL_VIEW(gtkconv->infopane), path);
5564 gtk_tree_path_free(path);
5566 if (chat) {
5567 /* This empty widget is used to ensure that the infopane is consistently
5568 sized for chat windows. The correct fix is to put an icon in the chat
5569 window as well, because that would make "Set Custom Icon" consistent
5570 for both the buddy list and the chat window, but PidginConversation
5571 is pretty much stuck until 3.0. */
5572 GtkWidget *sizing_vbox;
5573 sizing_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
5574 gtk_widget_set_size_request(sizing_vbox, -1, BUDDYICON_SIZE_MIN);
5575 gtk_box_pack_start(GTK_BOX(gtkconv->infopane_hbox), sizing_vbox, FALSE, FALSE, 0);
5576 gtk_widget_show(sizing_vbox);
5578 else {
5579 gtkconv->u.im->icon_container = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
5581 if ((buddy = purple_blist_find_buddy(purple_conversation_get_account(conv),
5582 purple_conversation_get_name(conv))) != NULL) {
5583 PurpleContact *contact = purple_buddy_get_contact(buddy);
5584 if (contact) {
5585 buddyicon_size = purple_blist_node_get_int((PurpleBlistNode*)contact, "pidgin-infopane-iconsize");
5588 buddyicon_size = CLAMP(buddyicon_size, BUDDYICON_SIZE_MIN, BUDDYICON_SIZE_MAX);
5589 gtk_widget_set_size_request(gtkconv->u.im->icon_container, -1, buddyicon_size);
5591 gtk_box_pack_start(GTK_BOX(gtkconv->infopane_hbox),
5592 gtkconv->u.im->icon_container, FALSE, FALSE, 0);
5594 gtk_widget_show(gtkconv->u.im->icon_container);
5597 gtk_widget_show(gtkconv->infopane);
5599 rend = gtk_cell_renderer_pixbuf_new();
5600 gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(gtkconv->infopane), rend, FALSE);
5601 gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(gtkconv->infopane), rend, "stock-id", CONV_ICON_COLUMN, NULL);
5602 g_object_set(rend, "xalign", 0.0, "xpad", 6, "ypad", 0,
5603 "stock-size", gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL),
5604 NULL);
5606 rend = gtk_cell_renderer_text_new();
5607 gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(gtkconv->infopane), rend, TRUE);
5608 gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(gtkconv->infopane), rend, "markup", CONV_TEXT_COLUMN, NULL);
5609 g_object_set(rend, "ypad", 0, "yalign", 0.5, NULL);
5611 g_object_set(rend, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
5613 rend = gtk_cell_renderer_pixbuf_new();
5614 gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(gtkconv->infopane), rend, FALSE);
5615 gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(gtkconv->infopane), rend, "pixbuf", CONV_PROTOCOL_ICON_COLUMN, NULL);
5616 g_object_set(rend, "xalign", 0.0, "xpad", 3, "ypad", 0, NULL);
5618 rend = gtk_cell_renderer_pixbuf_new();
5619 gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(gtkconv->infopane), rend, FALSE);
5620 gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(gtkconv->infopane), rend, "pixbuf", CONV_EMBLEM_COLUMN, NULL);
5621 g_object_set(rend, "xalign", 0.0, "xpad", 6, "ypad", 0, NULL);
5623 /* Setup the webkit widget */
5624 frame = pidgin_create_webview(FALSE, &gtkconv->webview, &webview_sw);
5625 g_object_set(G_OBJECT(gtkconv->webview), "expand", TRUE, NULL);
5626 _pidgin_widget_set_accessible_name(frame, "Conversation Pane");
5628 load_conv_theme(gtkconv);
5630 if (chat) {
5631 GtkWidget *hpaned;
5633 /* Add the topic */
5634 setup_chat_topic(gtkconv, vbox);
5636 /* Add the gtkwebview frame */
5637 hpaned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
5638 gtk_box_pack_start(GTK_BOX(vbox), hpaned, TRUE, TRUE, 0);
5639 gtk_widget_show(hpaned);
5640 gtk_paned_pack1(GTK_PANED(hpaned), frame, TRUE, TRUE);
5642 /* Now add the userlist */
5643 setup_chat_userlist(gtkconv, hpaned);
5644 } else {
5645 gtk_box_pack_start(GTK_BOX(vbox), frame, TRUE, TRUE, 0);
5647 gtk_widget_show_all(frame);
5649 gtk_widget_set_name(gtkconv->webview, "pidgin_conv_webview");
5650 g_object_set_data(G_OBJECT(gtkconv->webview), "gtkconv", gtkconv);
5652 g_signal_connect_after(G_OBJECT(gtkconv->webview), "button_press_event",
5653 G_CALLBACK(entry_stop_rclick_cb), NULL);
5654 g_signal_connect(G_OBJECT(gtkconv->webview), "key_press_event",
5655 G_CALLBACK(refocus_entry_cb), gtkconv);
5656 g_signal_connect(G_OBJECT(gtkconv->webview), "key_release_event",
5657 G_CALLBACK(refocus_entry_cb), gtkconv);
5659 pidgin_conv_setup_quickfind(gtkconv, vbox);
5661 gtkconv->lower_hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PIDGIN_HIG_BOX_SPACE);
5662 gtk_box_pack_start(GTK_BOX(vbox), gtkconv->lower_hbox, FALSE, FALSE, 0);
5663 gtk_widget_show(gtkconv->lower_hbox);
5665 /* Setup the entry widget and all signals */
5666 frame = pidgin_create_webview(TRUE, &gtkconv->entry, NULL);
5667 gtk_box_pack_start(GTK_BOX(gtkconv->lower_hbox), frame, TRUE, TRUE, 0);
5668 gtk_widget_show(frame);
5670 _pidgin_widget_set_accessible_name(frame, "Message Input");
5671 gtk_widget_set_name(gtkconv->entry, "pidgin_conv_entry");
5673 g_signal_connect(G_OBJECT(gtkconv->entry), "populate-popup",
5674 G_CALLBACK(entry_popup_menu_cb), gtkconv);
5675 g_signal_connect(G_OBJECT(gtkconv->entry), "key-press-event",
5676 G_CALLBACK(entry_key_press_cb), gtkconv);
5677 #if 0
5678 /* TODO WebKit: Why? */
5679 g_signal_connect_after(G_OBJECT(gtkconv->entry), "button-press-event",
5680 G_CALLBACK(entry_stop_rclick_cb), NULL);
5681 #endif
5683 if (!chat) {
5684 /* For sending typing notifications for IMs */
5685 gtkconv->u.im->typing_timer = 0;
5686 gtkconv->u.im->animate = purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/animate_buddy_icons");
5687 gtkconv->u.im->show_icon = TRUE;
5690 #if 0
5691 /* TODO WebKit: sizing stuff? */
5692 g_signal_connect_swapped(G_OBJECT(gtkconv->entry_buffer), "changed",
5693 G_CALLBACK(resize_webview_cb), gtkconv);
5694 g_signal_connect_swapped(G_OBJECT(gtkconv->entry), "size-allocate",
5695 G_CALLBACK(resize_webview_cb), gtkconv);
5696 #endif
5697 g_signal_connect_swapped(G_OBJECT(gtkconv->entry), "changed",
5698 G_CALLBACK(resize_webview_cb), gtkconv);
5699 g_signal_connect_swapped(G_OBJECT(gtkconv->entry), "size-allocate",
5700 G_CALLBACK(resize_webview_cb), gtkconv);
5702 default_formatize(gtkconv);
5703 g_signal_connect_after(G_OBJECT(gtkconv->entry), "format-cleared",
5704 G_CALLBACK(clear_formatting_cb), gtkconv);
5705 return vbox;
5708 static void
5709 conv_dnd_recv(GtkWidget *widget, GdkDragContext *dc, guint x, guint y,
5710 GtkSelectionData *sd, guint info, guint t,
5711 PidginConversation *gtkconv)
5713 PurpleConversation *conv = gtkconv->active_conv;
5714 PidginConvWindow *win = gtkconv->win;
5715 PurpleIMConversation *im;
5716 PurpleAccount *convaccount = purple_conversation_get_account(conv);
5717 PurpleConnection *gc = purple_account_get_connection(convaccount);
5718 PurpleProtocol *protocol = gc ? purple_connection_get_protocol(gc) : NULL;
5719 const guchar *data = gtk_selection_data_get_data(sd);
5721 if (info == PIDGIN_DRAG_BLIST_NODE)
5723 PurpleBlistNode *n = NULL;
5724 PurpleBuddy *b;
5725 PidginConversation *gtkconv = NULL;
5726 PurpleAccount *buddyaccount;
5727 const char *buddyname;
5728 PurpleBlistNode **data_val = (gpointer)data;
5730 n = *data_val;
5732 if (PURPLE_IS_CONTACT(n))
5733 b = purple_contact_get_priority_buddy((PurpleContact*)n);
5734 else if (PURPLE_IS_BUDDY(n))
5735 b = (PurpleBuddy*)n;
5736 else
5737 return;
5739 buddyaccount = purple_buddy_get_account(b);
5740 buddyname = purple_buddy_get_name(b);
5742 * If a buddy is dragged to a chat window of the same protocol,
5743 * invite him to the chat.
5745 if (PURPLE_IS_CHAT_CONVERSATION(conv) &&
5746 protocol && PURPLE_PROTOCOL_IMPLEMENTS(protocol, CHAT_IFACE, invite) &&
5747 strcmp(purple_account_get_protocol_id(convaccount),
5748 purple_account_get_protocol_id(buddyaccount)) == 0) {
5749 purple_chat_conversation_invite_user(PURPLE_CHAT_CONVERSATION(conv), buddyname, NULL, TRUE);
5750 } else {
5752 * If we already have an open conversation with this buddy, then
5753 * just move the conv to this window. Otherwise, create a new
5754 * conv and add it to this window.
5756 im = purple_conversations_find_im_with_account(buddyname, buddyaccount);
5757 if (im != NULL) {
5758 PidginConvWindow *oldwin;
5759 gtkconv = PIDGIN_CONVERSATION(PURPLE_CONVERSATION(im));
5760 oldwin = gtkconv->win;
5761 if (oldwin != win) {
5762 pidgin_conv_window_remove_gtkconv(oldwin, gtkconv);
5763 pidgin_conv_window_add_gtkconv(win, gtkconv);
5765 } else {
5766 im = purple_im_conversation_new(buddyaccount, buddyname);
5767 gtkconv = PIDGIN_CONVERSATION(PURPLE_CONVERSATION(im));
5768 if (gtkconv->win != win) {
5769 pidgin_conv_window_remove_gtkconv(gtkconv->win, gtkconv);
5770 pidgin_conv_window_add_gtkconv(win, gtkconv);
5774 /* Make this conversation the active conversation */
5775 pidgin_conv_window_switch_gtkconv(win, gtkconv);
5778 gtk_drag_finish(dc, TRUE,
5779 gdk_drag_context_get_actions(dc) == GDK_ACTION_MOVE, t);
5781 else if (info == PIDGIN_DRAG_IM_CONTACT)
5783 char *protocol_id = NULL;
5784 char *username = NULL;
5785 PurpleAccount *account;
5786 PidginConversation *gtkconv;
5788 if (pidgin_parse_x_im_contact((const char *) data, FALSE, &account,
5789 &protocol_id, &username, NULL))
5791 if (account == NULL)
5793 purple_notify_error(win, NULL,
5794 _("You are not currently signed on with an account that "
5795 "can add that buddy."), NULL, NULL);
5796 } else {
5798 * If a buddy is dragged to a chat window of the same protocol,
5799 * invite him to the chat.
5801 if (PURPLE_IS_CHAT_CONVERSATION(conv) &&
5802 protocol && PURPLE_PROTOCOL_IMPLEMENTS(protocol, CHAT_IFACE, invite) &&
5803 strcmp(purple_account_get_protocol_id(convaccount), protocol_id) == 0) {
5804 purple_chat_conversation_invite_user(PURPLE_CHAT_CONVERSATION(conv), username, NULL, TRUE);
5805 } else {
5806 im = purple_im_conversation_new(account, username);
5807 gtkconv = PIDGIN_CONVERSATION(PURPLE_CONVERSATION(im));
5808 if (gtkconv->win != win) {
5809 pidgin_conv_window_remove_gtkconv(gtkconv->win, gtkconv);
5810 pidgin_conv_window_add_gtkconv(win, gtkconv);
5816 g_free(username);
5817 g_free(protocol_id);
5819 gtk_drag_finish(dc, TRUE,
5820 gdk_drag_context_get_actions(dc) == GDK_ACTION_MOVE, t);
5822 else if (info == WEBKIT_WEB_VIEW_TARGET_INFO_URI_LIST) {
5823 if (PURPLE_IS_IM_CONVERSATION(conv))
5824 pidgin_dnd_file_manage(sd, convaccount, purple_conversation_get_name(conv));
5825 gtk_drag_finish(dc, TRUE,
5826 gdk_drag_context_get_actions(dc) == GDK_ACTION_MOVE, t);
5828 else
5829 gtk_drag_finish(dc, FALSE, FALSE, t);
5833 static PidginConversation *
5834 pidgin_conv_find_gtkconv(PurpleConversation * conv)
5836 PurpleBuddy *bud = purple_blist_find_buddy(purple_conversation_get_account(conv), purple_conversation_get_name(conv));
5837 PurpleContact *c;
5838 PurpleBlistNode *cn, *bn;
5840 if (!bud)
5841 return NULL;
5843 if (!(c = purple_buddy_get_contact(bud)))
5844 return NULL;
5846 cn = PURPLE_BLIST_NODE(c);
5847 for (bn = purple_blist_node_get_first_child(cn); bn; bn = purple_blist_node_get_sibling_next(bn)) {
5848 PurpleBuddy *b = PURPLE_BUDDY(bn);
5849 PurpleIMConversation *im;
5850 if ((im = purple_conversations_find_im_with_account(purple_buddy_get_name(b), purple_buddy_get_account(b)))) {
5851 if (PIDGIN_CONVERSATION(PURPLE_CONVERSATION(im)))
5852 return PIDGIN_CONVERSATION(PURPLE_CONVERSATION(im));
5856 return NULL;
5859 static void
5860 buddy_update_cb(PurpleBlistNode *bnode, gpointer null)
5862 GList *list;
5864 g_return_if_fail(bnode);
5865 if (!PURPLE_IS_BUDDY(bnode))
5866 return;
5868 for (list = pidgin_conv_windows_get_list(); list; list = list->next)
5870 PidginConvWindow *win = list->data;
5871 PurpleConversation *conv = pidgin_conv_window_get_active_conversation(win);
5873 if (!PURPLE_IS_IM_CONVERSATION(conv))
5874 continue;
5876 pidgin_conv_update_fields(conv, PIDGIN_CONV_MENU);
5880 static gboolean
5881 ignore_middle_click(GtkWidget *widget, GdkEventButton *e, gpointer null)
5883 /* A click on the pane is propagated to the notebook containing the pane.
5884 * So if Stu accidentally aims high and middle clicks on the pane-handle,
5885 * it causes a conversation tab to close. Let's stop that from happening.
5887 if (e->button == 2 && e->type == GDK_BUTTON_PRESS)
5888 return TRUE;
5889 return FALSE;
5892 static void set_typing_font(GtkWidget *widget, PidginConversation *gtkconv)
5894 /* TODO WEBKIT */
5895 #if 0
5896 static PangoFontDescription *font_desc = NULL;
5897 static GdkRGBA *color = NULL;
5898 static gboolean enable = TRUE;
5900 if (font_desc == NULL) {
5901 char *string = NULL;
5902 gtk_widget_style_get(widget,
5903 "typing-notification-font", &string,
5904 "typing-notification-color", &color,
5905 "typing-notification-enable", &enable,
5906 NULL);
5907 font_desc = pango_font_description_from_string(string);
5908 g_free(string);
5909 if (color == NULL) {
5910 GdkRGBA def = {0x8888/65535.0, 0x8888/65535.0, 0x8888/65535.0, 1.0};
5911 color = gdk_rgba_copy(&def);
5915 gtk_text_buffer_create_tag(GTK_IMHTML(widget)->text_buffer, "TYPING-NOTIFICATION",
5916 "foreground-rgba", color,
5917 "font-desc", font_desc,
5918 NULL);
5920 if (!enable) {
5921 g_object_set_data(G_OBJECT(widget), "disable-typing-notification", GINT_TO_POINTER(TRUE));
5922 /* or may be 'gtkconv->disable_typing = TRUE;' instead? */
5925 g_signal_handlers_disconnect_by_func(G_OBJECT(widget), set_typing_font, gtkconv);
5926 #endif /* if 0 */
5929 /**************************************************************************
5930 * Conversation UI operations
5931 **************************************************************************/
5932 static void
5933 private_gtkconv_new(PurpleConversation *conv, gboolean hidden)
5935 PidginConversation *gtkconv;
5936 const char *theme_name;
5937 PurpleTheme *theme = NULL;
5938 GtkWidget *pane = NULL;
5939 GtkWidget *tab_cont;
5940 PurpleBlistNode *convnode;
5941 GtkTargetList *targets;
5943 if (PURPLE_IS_IM_CONVERSATION(conv) && (gtkconv = pidgin_conv_find_gtkconv(conv))) {
5944 purple_conversation_set_ui_data(conv, gtkconv);
5945 if (!g_list_find(gtkconv->convs, conv))
5946 gtkconv->convs = g_list_prepend(gtkconv->convs, conv);
5947 pidgin_conv_switch_active_conversation(conv);
5948 return;
5951 gtkconv = g_new0(PidginConversation, 1);
5952 purple_conversation_set_ui_data(conv, gtkconv);
5953 gtkconv->active_conv = conv;
5954 gtkconv->convs = g_list_prepend(gtkconv->convs, conv);
5955 gtkconv->send_history = g_list_append(NULL, NULL);
5957 /* Setup some initial variables. */
5958 gtkconv->unseen_state = PIDGIN_UNSEEN_NONE;
5959 gtkconv->unseen_count = 0;
5960 theme_name = purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/theme");
5961 if (theme_name && *theme_name)
5962 theme = purple_theme_manager_find_theme(theme_name, "conversation");
5963 if (!theme)
5964 theme = default_conv_theme;
5965 gtkconv->theme = PIDGIN_CONV_THEME(g_object_ref(theme));
5966 gtkconv->last_flags = 0;
5968 if (PURPLE_IS_IM_CONVERSATION(conv)) {
5969 gtkconv->u.im = g_malloc0(sizeof(PidginImPane));
5970 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
5971 gtkconv->u.chat = g_malloc0(sizeof(PidginChatPane));
5973 pane = setup_common_pane(gtkconv);
5975 if (pane == NULL) {
5976 if (PURPLE_IS_CHAT_CONVERSATION(conv))
5977 g_free(gtkconv->u.chat);
5978 else if (PURPLE_IS_IM_CONVERSATION(conv))
5979 g_free(gtkconv->u.im);
5981 g_free(gtkconv);
5982 purple_conversation_set_ui_data(conv, NULL);
5983 return;
5986 /* Setup drag-and-drop */
5987 gtk_drag_dest_set(pane, GTK_DEST_DEFAULT_MOTION | GTK_DEST_DEFAULT_DROP,
5988 NULL, 0, GDK_ACTION_COPY);
5989 targets = gtk_target_list_new(dnd_targets, G_N_ELEMENTS(dnd_targets));
5990 gtk_target_list_add(targets, gdk_atom_intern("text/uri-list", FALSE), 0,
5991 WEBKIT_WEB_VIEW_TARGET_INFO_URI_LIST);
5992 gtk_drag_dest_set_target_list(pane, targets);
5994 if (webkit_dnd_targets) {
5995 targets = webkit_dnd_targets;
5996 } else {
5997 GtkTargetEntry *entries;
5998 gint count;
6000 targets = webkit_web_view_get_paste_target_list(WEBKIT_WEB_VIEW(gtkconv->webview));
6001 entries = gtk_target_table_new_from_list(targets, &count);
6002 targets = gtk_target_list_new(entries, count);
6003 gtk_target_table_free(entries, count);
6005 gtk_target_list_add_table(targets, dnd_targets, G_N_ELEMENTS(dnd_targets));
6006 webkit_dnd_targets = targets;
6009 gtk_drag_dest_set(gtkconv->webview, 0, NULL, 0, GDK_ACTION_COPY);
6010 gtk_drag_dest_set_target_list(gtkconv->webview, targets);
6012 gtk_drag_dest_set(gtkconv->entry, 0, NULL, 0, GDK_ACTION_COPY);
6013 gtk_drag_dest_set_target_list(gtkconv->entry, targets);
6015 g_signal_connect(G_OBJECT(pane), "button_press_event",
6016 G_CALLBACK(ignore_middle_click), NULL);
6017 g_signal_connect(G_OBJECT(pane), "drag-data-received",
6018 G_CALLBACK(conv_dnd_recv), gtkconv);
6019 #if 0
6020 /* FIXME: WebKit confuses the dnd source when this is enabled */
6021 g_signal_connect(G_OBJECT(gtkconv->webview), "drag-data-received",
6022 G_CALLBACK(conv_dnd_recv), gtkconv);
6023 g_signal_connect(G_OBJECT(gtkconv->entry), "drag-data-received",
6024 G_CALLBACK(conv_dnd_recv), gtkconv);
6025 #endif
6027 g_signal_connect(gtkconv->webview, "style-updated", G_CALLBACK(set_typing_font), gtkconv);
6029 /* Setup the container for the tab. */
6030 gtkconv->tab_cont = tab_cont = gtk_box_new(GTK_ORIENTATION_VERTICAL, PIDGIN_HIG_BOX_SPACE);
6031 g_object_set_data(G_OBJECT(tab_cont), "PidginConversation", gtkconv);
6032 gtk_container_set_border_width(GTK_CONTAINER(tab_cont), PIDGIN_HIG_BOX_SPACE);
6033 gtk_container_add(GTK_CONTAINER(tab_cont), pane);
6034 gtk_widget_show(pane);
6036 convnode = get_conversation_blist_node(conv);
6037 if (convnode == NULL || !purple_blist_node_get_bool(convnode, "gtk-mute-sound"))
6038 gtkconv->make_sound = TRUE;
6040 if (convnode != NULL && purple_blist_node_has_setting(convnode, "enable-logging")) {
6041 gboolean logging = purple_blist_node_get_bool(convnode, "enable-logging");
6042 purple_conversation_set_logging(conv, logging);
6045 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/show_formatting_toolbar"))
6046 pidgin_webview_show_toolbar(PIDGIN_WEBVIEW(gtkconv->entry));
6047 else
6048 pidgin_webview_hide_toolbar(PIDGIN_WEBVIEW(gtkconv->entry));
6049 pidgin_webview_switch_active_conversation(
6050 PIDGIN_WEBVIEW(gtkconv->entry), conv);
6051 pidgin_webview_switch_active_conversation(
6052 PIDGIN_WEBVIEW(gtkconv->webview), conv);
6054 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons"))
6055 gtk_widget_show(gtkconv->infopane_hbox);
6056 else
6057 gtk_widget_hide(gtkconv->infopane_hbox);
6060 g_signal_connect_swapped(G_OBJECT(pane), "focus",
6061 G_CALLBACK(gtk_widget_grab_focus),
6062 gtkconv->entry);
6064 if (hidden)
6065 pidgin_conv_window_add_gtkconv(hidden_convwin, gtkconv);
6066 else
6067 pidgin_conv_placement_place(gtkconv);
6069 if (generated_nick_colors == NULL) {
6070 GdkColor color;
6071 GdkRGBA rgba;
6072 /* FIXME: No matter how I ask the GtkStyleContext, it always gives me
6073 * back black instead of the _actual_ background colour. */
6074 color = gtk_widget_get_style(gtkconv->webview)->base[GTK_STATE_NORMAL];
6075 rgba.red = color.red / 65535.0;
6076 rgba.green = color.green / 65535.0;
6077 rgba.blue = color.blue / 65535.0;
6078 generated_nick_colors = generate_nick_colors(NICK_COLOR_GENERATE_COUNT, rgba);
6081 if(NULL == (gtkconv->nick_colors = pidgin_conversation_theme_get_nick_colors(gtkconv->theme)))
6083 gtkconv->nick_colors = g_array_ref(generated_nick_colors);
6087 static void
6088 pidgin_conv_new_hidden(PurpleConversation *conv)
6090 private_gtkconv_new(conv, TRUE);
6093 void
6094 pidgin_conv_new(PurpleConversation *conv)
6096 private_gtkconv_new(conv, FALSE);
6097 if (PIDGIN_IS_PIDGIN_CONVERSATION(conv))
6098 purple_signal_emit(pidgin_conversations_get_handle(),
6099 "conversation-displayed", PIDGIN_CONVERSATION(conv));
6102 static void
6103 received_im_msg_cb(PurpleAccount *account, char *sender, char *message,
6104 PurpleConversation *conv, PurpleMessageFlags flags)
6106 PurpleConversationUiOps *ui_ops = pidgin_conversations_get_conv_ui_ops();
6107 gboolean hide = FALSE;
6108 guint timer;
6110 /* create hidden conv if hide_new pref is always */
6111 if (strcmp(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new"), "always") == 0)
6112 hide = TRUE;
6114 /* create hidden conv if hide_new pref is away and account is away */
6115 if (strcmp(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new"), "away") == 0 &&
6116 !purple_status_is_available(purple_account_get_active_status(account)))
6117 hide = TRUE;
6119 if (conv && PIDGIN_IS_PIDGIN_CONVERSATION(conv) && !hide) {
6120 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
6121 if (gtkconv->win == hidden_convwin) {
6122 pidgin_conv_attach_to_conversation(gtkconv->active_conv);
6124 return;
6127 if (hide) {
6128 ui_ops->create_conversation = pidgin_conv_new_hidden;
6129 purple_im_conversation_new(account, sender);
6130 ui_ops->create_conversation = pidgin_conv_new;
6133 /* Somebody wants to keep this conversation around, so don't time it out */
6134 if (conv) {
6135 timer = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(conv), "close-timer"));
6136 if (timer) {
6137 purple_timeout_remove(timer);
6138 g_object_set_data(G_OBJECT(conv), "close-timer", GINT_TO_POINTER(0));
6143 static void
6144 pidgin_conv_destroy(PurpleConversation *conv)
6146 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
6148 gtkconv->convs = g_list_remove(gtkconv->convs, conv);
6149 /* Don't destroy ourselves until all our convos are gone */
6150 if (gtkconv->convs) {
6151 /* Make sure the destroyed conversation is not the active one */
6152 if (gtkconv->active_conv == conv) {
6153 gtkconv->active_conv = gtkconv->convs->data;
6154 purple_conversation_update(gtkconv->active_conv, PURPLE_CONVERSATION_UPDATE_FEATURES);
6156 return;
6159 pidgin_conv_window_remove_gtkconv(gtkconv->win, gtkconv);
6161 /* If the "Save Conversation" or "Save Icon" dialogs are open then close them */
6162 purple_request_close_with_handle(gtkconv);
6163 purple_notify_close_with_handle(gtkconv);
6165 gtk_widget_destroy(gtkconv->tab_cont);
6166 g_object_unref(gtkconv->tab_cont);
6168 if (PURPLE_IS_IM_CONVERSATION(conv)) {
6169 if (gtkconv->u.im->icon_timer != 0)
6170 g_source_remove(gtkconv->u.im->icon_timer);
6172 if (gtkconv->u.im->anim != NULL)
6173 g_object_unref(G_OBJECT(gtkconv->u.im->anim));
6175 if (gtkconv->u.im->typing_timer != 0)
6176 g_source_remove(gtkconv->u.im->typing_timer);
6178 g_free(gtkconv->u.im);
6179 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
6180 purple_signals_disconnect_by_handle(gtkconv->u.chat);
6181 g_free(gtkconv->u.chat);
6184 gtkconv->send_history = g_list_first(gtkconv->send_history);
6185 g_list_foreach(gtkconv->send_history, (GFunc)g_free, NULL);
6186 g_list_free(gtkconv->send_history);
6188 if (gtkconv->attach_timer) {
6189 g_source_remove(gtkconv->attach_timer);
6192 g_array_unref(gtkconv->nick_colors);
6194 g_object_disconnect(G_OBJECT(gtkconv->theme), "any_signal::notify",
6195 conv_variant_changed_cb, gtkconv, NULL);
6196 g_object_unref(gtkconv->theme);
6198 g_free(gtkconv);
6201 #if 0
6202 static const char *
6203 get_text_tag_color(GtkTextTag *tag)
6205 GdkRGBA *color = NULL;
6206 gboolean set = FALSE;
6207 static char colcode[] = "#XXXXXX";
6208 if (tag)
6209 g_object_get(G_OBJECT(tag), "foreground-set", &set, "foreground-rgba", &color, NULL);
6210 if (set && color)
6211 g_snprintf(colcode, sizeof(colcode), "#%02x%02x%02x",
6212 (unsigned int)(color->red * 255),
6213 (unsigned int)(color->green * 255),
6214 (unsigned int)(color->blue * 255));
6215 else
6216 colcode[0] = '\0';
6217 if (color)
6218 gdk_rgba_free(color);
6219 return colcode;
6222 /* The callback for an event on a link tag. */
6223 static gboolean buddytag_event(GtkTextTag *tag, GObject *imhtml,
6224 GdkEvent *event, GtkTextIter *arg2, gpointer data)
6226 if (event->type == GDK_BUTTON_PRESS
6227 || event->type == GDK_2BUTTON_PRESS) {
6228 GdkEventButton *btn_event = (GdkEventButton*) event;
6229 PurpleConversation *conv = data;
6230 char *buddyname;
6231 gchar *name;
6233 g_object_get(G_OBJECT(tag), "name", &name, NULL);
6235 /* strlen("BUDDY " or "HILIT ") == 6 */
6236 g_return_val_if_fail((name != NULL) && (strlen(name) > 6), FALSE);
6238 buddyname = name + 6;
6240 /* emit chat-nick-clicked signal */
6241 if (event->type == GDK_BUTTON_PRESS) {
6242 gint plugin_return = GPOINTER_TO_INT(purple_signal_emit_return_1(
6243 pidgin_conversations_get_handle(), "chat-nick-clicked",
6244 data, buddyname, btn_event->button));
6245 if (plugin_return) {
6246 g_free(name);
6247 return TRUE;
6251 if (btn_event->button == 1 && event->type == GDK_2BUTTON_PRESS) {
6252 chat_do_im(PIDGIN_CONVERSATION(conv), buddyname);
6253 g_free(name);
6255 return TRUE;
6256 } else if (btn_event->button == 2 && event->type == GDK_2BUTTON_PRESS) {
6257 chat_do_info(PIDGIN_CONVERSATION(conv), buddyname);
6258 g_free(name);
6260 return TRUE;
6261 } else if (btn_event->button == 3 && event->type == GDK_BUTTON_PRESS) {
6262 GtkTextIter start, end;
6264 /* we shouldn't display the popup
6265 * if the user has selected something: */
6266 if (!gtk_text_buffer_get_selection_bounds(
6267 gtk_text_iter_get_buffer(arg2),
6268 &start, &end)) {
6269 GtkWidget *menu = NULL;
6270 PurpleConnection *gc =
6271 purple_conversation_get_connection(conv);
6274 menu = create_chat_menu(conv, buddyname, gc);
6275 gtk_menu_popup(GTK_MENU(menu), NULL, NULL,
6276 NULL, GTK_WIDGET(imhtml),
6277 btn_event->button,
6278 btn_event->time);
6280 g_free(name);
6282 /* Don't propagate the event any further */
6283 return TRUE;
6287 g_free(name);
6290 return FALSE;
6292 #endif
6294 static GtkTextTag *get_buddy_tag(PurpleChatConversation *chat, const char *who, PurpleMessageFlags flag,
6295 gboolean create)
6297 /* TODO WEBKIT */
6298 #if 0
6299 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
6300 GtkTextTag *buddytag;
6301 gchar *str;
6302 gboolean highlight = (flag & PURPLE_MESSAGE_NICK);
6303 GtkTextBuffer *buffer = GTK_IMHTML(gtkconv->imhtml)->text_buffer;
6305 str = g_strdup_printf(highlight ? "HILIT %s" : "BUDDY %s", who);
6307 buddytag = gtk_text_tag_table_lookup(
6308 gtk_text_buffer_get_tag_table(buffer), str);
6310 if (buddytag == NULL && create) {
6311 if (highlight)
6312 buddytag = gtk_text_buffer_create_tag(buffer, str,
6313 "foreground", get_text_tag_color(gtk_text_tag_table_lookup(
6314 gtk_text_buffer_get_tag_table(buffer), "highlight-name")),
6315 "weight", PANGO_WEIGHT_BOLD,
6316 NULL);
6317 else
6318 buddytag = gtk_text_buffer_create_tag(
6319 buffer, str,
6320 "foreground-rgba", get_nick_color(gtkconv, who),
6321 "weight", purple_blist_find_buddy(purple_conversation_get_account(conv), who) ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL,
6322 NULL);
6324 g_object_set_data(G_OBJECT(buddytag), "cursor", "");
6325 g_signal_connect(G_OBJECT(buddytag), "event",
6326 G_CALLBACK(buddytag_event), conv);
6329 g_free(str);
6331 return buddytag;
6332 #endif /* if 0 */
6333 return NULL;
6336 #if 0
6337 static void pidgin_conv_calculate_newday(PidginConversation *gtkconv, time_t mtime)
6339 struct tm *tm = localtime(&mtime);
6341 tm->tm_hour = tm->tm_min = tm->tm_sec = 0;
6342 tm->tm_mday++;
6344 gtkconv->newday = mktime(tm);
6347 /* Detect string direction and encapsulate the string in RLE/LRE/PDF unicode characters
6348 str - pointer to string (string is re-allocated and the pointer updated) */
6349 static void
6350 str_embed_direction_chars(char **str)
6352 #ifdef HAVE_PANGO14
6353 char pre_str[4];
6354 char post_str[10];
6355 char *ret;
6357 if (PANGO_DIRECTION_RTL == pango_find_base_dir(*str, -1))
6359 sprintf(pre_str, "%c%c%c",
6360 0xE2, 0x80, 0xAB); /* RLE */
6361 sprintf(post_str, "%c%c%c%c%c%c%c%c%c",
6362 0xE2, 0x80, 0xAC, /* PDF */
6363 0xE2, 0x80, 0x8E, /* LRM */
6364 0xE2, 0x80, 0xAC); /* PDF */
6366 else
6368 sprintf(pre_str, "%c%c%c",
6369 0xE2, 0x80, 0xAA); /* LRE */
6370 sprintf(post_str, "%c%c%c%c%c%c%c%c%c",
6371 0xE2, 0x80, 0xAC, /* PDF */
6372 0xE2, 0x80, 0x8F, /* RLM */
6373 0xE2, 0x80, 0xAC); /* PDF */
6376 ret = g_strconcat(pre_str, *str, post_str, NULL);
6378 g_free(*str);
6379 *str = ret;
6380 #endif
6382 #endif
6384 static char *
6385 replace_message_tokens(
6386 const char *text,
6387 PurpleConversation *conv,
6388 const char *name, /* author */
6389 const char *alias, /* author's alias */
6390 const char *message,
6391 PurpleMessageFlags flags,
6392 time_t mtime)
6394 GString *str;
6395 const char *cur = text;
6396 const char *prev = cur;
6397 struct tm *tm = NULL;
6399 if (text == NULL || *text == '\0')
6400 return NULL;
6402 str = g_string_new(NULL);
6403 while ((cur = strchr(cur, '%'))) {
6404 const char *replace = NULL;
6405 const char *fin = NULL;
6406 gpointer freeval = NULL;
6408 if (g_str_has_prefix(cur, "%message%")) {
6409 replace = message;
6411 } else if (g_str_has_prefix(cur, "%messageClasses%")) {
6412 char *user;
6413 GString *classes = g_string_new(NULL);
6414 #define ADD_CLASS(f, class) \
6415 if (flags & f) \
6416 g_string_append(classes, class);
6417 ADD_CLASS(PURPLE_MESSAGE_SEND, "outgoing ");
6418 ADD_CLASS(PURPLE_MESSAGE_RECV, "incoming ");
6419 ADD_CLASS(PURPLE_MESSAGE_SYSTEM, "event ");
6420 ADD_CLASS(PURPLE_MESSAGE_AUTO_RESP, "autoreply ");
6421 ADD_CLASS(PURPLE_MESSAGE_DELAYED, "history ");
6422 ADD_CLASS(PURPLE_MESSAGE_NICK, "mention ");
6423 #undef ADD_CLASS
6424 user = get_class_for_user(name);
6425 g_string_append(classes, user);
6426 g_free(user);
6428 replace = freeval = g_string_free(classes, FALSE);
6430 } else if (g_str_has_prefix(cur, "%time")) {
6431 const char *tmp = cur + strlen("%time");
6433 if (*tmp == '{') {
6434 char *end;
6435 tmp++;
6436 end = strstr(tmp, "}%");
6437 if (!end) /* Invalid string */
6438 continue;
6439 if (!tm)
6440 tm = localtime(&mtime);
6441 replace = freeval = purple_uts35_to_str(tmp, end - tmp, tm);
6442 fin = end + 1;
6443 } else {
6444 if (!tm)
6445 tm = localtime(&mtime);
6447 replace = purple_utf8_strftime("%X", tm);
6450 } else if (g_str_has_prefix(cur, "%shortTime%")) {
6451 if (!tm)
6452 tm = localtime(&mtime);
6454 replace = purple_utf8_strftime("%H:%M", tm);
6456 } else if (g_str_has_prefix(cur, "%userIconPath%")) {
6457 if (flags & PURPLE_MESSAGE_SEND) {
6458 if (purple_account_get_bool(purple_conversation_get_account(conv), "use-global-buddyicon", TRUE)) {
6459 replace = purple_prefs_get_path(PIDGIN_PREFS_ROOT "/accounts/buddyicon");
6460 } else {
6461 PurpleImage *img = purple_buddy_icons_find_account_icon(purple_conversation_get_account(conv));
6462 /* XXX: this may be NULL */
6463 replace = purple_image_get_path(img);
6465 if (replace == NULL || !g_file_test(replace, G_FILE_TEST_EXISTS)) {
6466 replace = freeval = g_build_filename("Outgoing", "buddy_icon.png", NULL);
6468 } else if (flags & PURPLE_MESSAGE_RECV) {
6469 PurpleBuddyIcon *icon = purple_im_conversation_get_icon(PURPLE_IM_CONVERSATION(conv));
6470 if (icon)
6471 replace = purple_buddy_icon_get_full_path(icon);
6472 if (replace == NULL || !g_file_test(replace, G_FILE_TEST_EXISTS)) {
6473 replace = freeval = g_build_filename("Incoming", "buddy_icon.png", NULL);
6477 } else if (g_str_has_prefix(cur, "%senderScreenName%")) {
6478 replace = name;
6480 } else if (g_str_has_prefix(cur, "%sender%")) {
6481 replace = alias;
6483 } else if (g_str_has_prefix(cur, "%senderColor%")) {
6484 const GdkRGBA *color = get_nick_color(PIDGIN_CONVERSATION(conv), name);
6485 replace = freeval = g_strdup_printf("#%02x%02x%02x",
6486 (unsigned int)(color->red * 255),
6487 (unsigned int)(color->green * 255),
6488 (unsigned int)(color->blue * 255));
6490 } else if (g_str_has_prefix(cur, "%service%")) {
6491 replace = purple_account_get_protocol_name(purple_conversation_get_account(conv));
6493 } else if (g_str_has_prefix(cur, "%messageDirection%")) {
6494 replace = purple_markup_is_rtl(message) ? "rtl" : "ltr";
6496 } else if (g_str_has_prefix(cur, "%status%")) {
6497 GString *classes = g_string_new(NULL);
6499 if (flags & PURPLE_MESSAGE_ERROR)
6500 g_string_append(classes, "error ");
6502 replace = freeval = g_string_free(classes, FALSE);
6504 } else if (g_str_has_prefix(cur, "%variant%")) {
6505 replace = pidgin_conversation_theme_get_variant(PIDGIN_CONVERSATION(conv)->theme);
6506 replace = freeval = g_strdup(replace);
6507 purple_util_chrreplace(freeval, ' ', '_');
6509 } else {
6510 cur++;
6511 continue;
6514 /* Here we have a replacement to make */
6515 g_string_append_len(str, prev, cur - prev);
6516 if (replace)
6517 g_string_append(str, replace);
6518 g_free(freeval);
6519 replace = freeval = NULL;
6521 /* And update the pointers */
6522 if (fin) {
6523 prev = cur = fin + 1;
6524 } else {
6525 prev = cur = strchr(cur + 1, '%') + 1;
6530 /* And wrap it up */
6531 g_string_append(str, prev);
6533 return g_string_free(str, FALSE);
6536 static gboolean
6537 pidgin_conv_write_smiley(GString *out, PurpleSmiley *smiley,
6538 PurpleConversation *conv, gpointer _proto_name)
6540 PurpleImage *image;
6541 gchar *escaped_shortcut;
6542 gchar *uri;
6544 escaped_shortcut = g_markup_escape_text(
6545 purple_smiley_get_shortcut(smiley), -1);
6546 image = purple_smiley_get_image(smiley);
6547 uri = purple_image_store_get_uri(image);
6549 g_string_append_printf(out,
6550 "<img class=\"emoticon\" alt=\"%s\" title=\"%s\" "
6551 "src=\"%s\" />", escaped_shortcut,
6552 escaped_shortcut, uri);
6554 g_free(uri);
6555 g_free(escaped_shortcut);
6557 return TRUE;
6560 static void
6561 remote_image_got(PurpleImage *image, gpointer _conv)
6563 PurpleConversation *conv = _conv;
6564 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
6565 guint image_id;
6566 gchar *js;
6568 if (!gtkconv)
6569 return;
6571 image_id = purple_image_store_add_temporary(image);
6573 purple_debug_info("gtkconv", "Remote image %u is ready for display",
6574 image_id);
6576 js = g_strdup_printf("remoteImageIsReady(%u)", image_id);
6577 pidgin_webview_safe_execute_script(
6578 PIDGIN_WEBVIEW(gtkconv->webview), js);
6579 g_free(js);
6582 static gboolean
6583 box_remote_image_cb(const GMatchInfo *info, GString *result, gpointer _conv)
6585 PurpleConversation *conv = _conv;
6586 gchar *uri, *before, *after, *full, *alt;
6587 PurpleImage *image;
6588 guint img_id;
6590 uri = g_match_info_fetch(info, 2);
6591 image = purple_image_store_get_from_uri(uri);
6592 g_free(uri);
6594 full = g_match_info_fetch(info, 0);
6596 if (purple_image_is_ready(image)) {
6597 g_string_append(result, full);
6598 g_free(full);
6599 return FALSE;
6602 /* search for alt */
6603 alt = strstr(full, "alt=\"");
6604 if (alt) {
6605 gchar *end;
6606 alt += strlen("alt=\"");
6607 end = strstr(alt, "\"");
6608 if (end)
6609 end[0] = '\0';
6610 else
6611 alt = NULL;
6612 if (alt && alt[0] == '\0')
6613 alt = NULL;
6616 /* add for ever - we don't know, when transfer finishes */
6617 img_id = purple_image_store_add(image);
6619 before = g_match_info_fetch(info, 1);
6620 after = g_match_info_fetch(info, 3);
6622 g_string_append_printf(result, "<span class=\"pending-image "
6623 "pending-image-id-%u\">", img_id);
6625 if (alt)
6626 g_string_append(result, alt);
6627 else
6628 g_string_append(result, "&lt;img&gt;");
6630 g_string_append(result, before);
6631 g_string_append(result, "about:blank");
6632 g_string_append(result, after);
6634 g_string_append(result, "</span>");
6636 g_free(before);
6637 g_free(after);
6638 g_free(full);
6640 g_signal_connect_object(image, "ready",
6641 G_CALLBACK(remote_image_got), conv, 0);
6643 return FALSE;
6646 static gchar *
6647 box_remote_images(PurpleConversation *conv, const gchar *msg)
6649 return g_regex_replace_eval(image_store_tag_re, msg, -1, 0, 0,
6650 box_remote_image_cb, conv, NULL);
6653 static gboolean
6654 writing_msg(PurpleConversation *conv, PurpleMessage *msg, gpointer _unused)
6656 PidginConversation *gtkconv;
6658 g_return_val_if_fail(msg != NULL, FALSE);
6660 if (!(purple_message_get_flags(msg) & PURPLE_MESSAGE_ACTIVE_ONLY))
6661 return FALSE;
6663 g_return_val_if_fail(conv != NULL, FALSE);
6664 gtkconv = PIDGIN_CONVERSATION(conv);
6665 g_return_val_if_fail(gtkconv != NULL, FALSE);
6667 if (conv == gtkconv->active_conv)
6668 return FALSE;
6670 purple_debug_info("gtkconv",
6671 "Suppressing message for an inactive conversation");
6673 return TRUE;
6676 static void
6677 pidgin_conv_write_conv(PurpleConversation *conv, PurpleMessage *pmsg)
6679 PidginConversation *gtkconv;
6680 PurpleConnection *gc;
6681 PurpleAccount *account;
6682 #if 0
6683 int gtk_font_options = 0;
6684 int gtk_font_options_all = 0;
6685 char buf2[BUF_LONG];
6686 gboolean show_date;
6687 char *mdate;
6688 char *str;
6689 char *with_font_tag;
6690 char *sml_attrib = NULL;
6691 size_t length;
6692 #endif
6693 char *displaying;
6694 gboolean plugin_return;
6695 #if 0
6696 gboolean is_rtl_message = FALSE;
6697 #endif
6699 const char *message_html;
6700 char *msg_tokenized;
6701 char *escape;
6702 char *script;
6703 char *smileyed;
6704 gchar *imgized;
6705 PurpleMessageFlags flags, old_flags;
6706 const char *func = "appendMessage";
6708 g_return_if_fail(conv != NULL);
6709 gtkconv = PIDGIN_CONVERSATION(conv);
6710 g_return_if_fail(gtkconv != NULL);
6711 flags = purple_message_get_flags(pmsg);
6713 if (gtkconv->attach_timer) {
6714 /* We are currently in the process of filling up the buffer with the message
6715 * history of the conversation. So we do not need to add the message here.
6716 * Instead, this message will be added to the message-list, which in turn will
6717 * be processed and displayed by the attach-callback.
6719 return;
6722 if (conv != gtkconv->active_conv)
6724 /* Set the active conversation to the one that just messaged us. */
6725 /* TODO: consider not doing this if the account is offline or something */
6726 if (flags & (PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_RECV))
6727 pidgin_conv_switch_active_conversation(conv);
6730 account = purple_conversation_get_account(conv);
6731 g_return_if_fail(account != NULL);
6732 gc = purple_account_get_connection(account);
6733 g_return_if_fail(gc != NULL || !(flags & (PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_RECV)));
6735 /* Make sure URLs are clickable */
6736 if(flags & PURPLE_MESSAGE_NO_LINKIFY)
6737 displaying = g_strdup(purple_message_get_contents(pmsg));
6738 else
6739 displaying = purple_markup_linkify(purple_message_get_contents(pmsg));
6741 plugin_return = GPOINTER_TO_INT(purple_signal_emit_return_1(
6742 pidgin_conversations_get_handle(),
6743 (PURPLE_IS_IM_CONVERSATION(conv) ? "displaying-im-msg" : "displaying-chat-msg"),
6744 conv, pmsg));
6745 if (plugin_return)
6747 g_free(displaying);
6748 return;
6750 #if 0
6751 length = strlen(displaying) + 1;
6752 #endif
6754 old_flags = gtkconv->last_flags;
6755 if ((flags & PURPLE_MESSAGE_SEND) && (old_flags & PURPLE_MESSAGE_SEND)) {
6756 message_html = pidgin_conversation_theme_get_template(gtkconv->theme,
6757 PIDGIN_CONVERSATION_THEME_TEMPLATE_OUTGOING_NEXT_CONTENT);
6758 func = "appendNextMessage";
6760 } else if (flags & PURPLE_MESSAGE_SEND) {
6761 message_html = pidgin_conversation_theme_get_template(gtkconv->theme,
6762 PIDGIN_CONVERSATION_THEME_TEMPLATE_OUTGOING_CONTENT);
6764 } else if ((flags & PURPLE_MESSAGE_RECV) && (old_flags & PURPLE_MESSAGE_RECV)) {
6765 GList *history = purple_conversation_get_message_history(gtkconv->last_conversed);
6766 PurpleMessage *last_msg = history ? history->data : NULL;
6768 g_assert(history != NULL);
6769 g_assert(last_msg != NULL);
6771 /* If the senders are the same, use appendNextMessage */
6772 if (purple_strequal(purple_message_get_author(last_msg), purple_message_get_author(pmsg))) {
6773 message_html = pidgin_conversation_theme_get_template(gtkconv->theme,
6774 PIDGIN_CONVERSATION_THEME_TEMPLATE_INCOMING_NEXT_CONTENT);
6775 func = "appendNextMessage";
6776 } else {
6777 message_html = pidgin_conversation_theme_get_template(gtkconv->theme,
6778 PIDGIN_CONVERSATION_THEME_TEMPLATE_INCOMING_CONTENT);
6780 } else if (flags & PURPLE_MESSAGE_RECV) {
6781 message_html = pidgin_conversation_theme_get_template(gtkconv->theme,
6782 PIDGIN_CONVERSATION_THEME_TEMPLATE_INCOMING_CONTENT);
6784 } else {
6785 message_html = pidgin_conversation_theme_get_template(gtkconv->theme,
6786 PIDGIN_CONVERSATION_THEME_TEMPLATE_STATUS);
6788 gtkconv->last_flags = flags;
6789 gtkconv->last_conversed = conv;
6791 if(purple_message_get_flags(pmsg) & PURPLE_MESSAGE_SYSTEM) {
6792 smileyed = g_strdup(displaying);
6793 } else {
6794 smileyed = purple_smiley_parser_smileify(conv, displaying,
6795 (flags & PURPLE_MESSAGE_RECV), pidgin_conv_write_smiley,
6796 (gpointer)purple_account_get_protocol_name(account));
6798 imgized = box_remote_images(conv, smileyed);
6799 msg_tokenized = replace_message_tokens(message_html, conv,
6800 purple_message_get_author(pmsg),
6801 purple_message_get_author_alias(pmsg),
6802 imgized,
6803 purple_message_get_flags(pmsg),
6804 purple_message_get_time(pmsg));
6805 escape = pidgin_webview_quote_js_string(msg_tokenized ? msg_tokenized : "");
6806 script = g_strdup_printf("%s(%s)", func, escape);
6808 purple_debug_info("webkit", "JS: %s\n", script);
6809 pidgin_webview_safe_execute_script(PIDGIN_WEBVIEW(gtkconv->webview), script);
6811 g_free(script);
6812 g_free(smileyed);
6813 g_free(imgized);
6814 g_free(msg_tokenized);
6815 g_free(escape);
6817 #if 0
6818 /* if the buffer is not empty add a <br> */
6819 if (!pidgin_webview_is_empty(PIDGIN_WEBVIEW(gtkconv->webview)))
6820 pidgin_webview_append_html(PIDGIN_WEBVIEW(gtkconv->webview), "<br />");
6822 /* First message in a conversation. */
6823 if (gtkconv->newday == 0)
6824 pidgin_conv_calculate_newday(gtkconv, mtime);
6826 /* Show the date on the first message in a new day, or if the message is
6827 * older than 20 minutes. */
6828 show_date = (mtime >= gtkconv->newday) || (time(NULL) > mtime + 20*60);
6830 mdate = purple_signal_emit_return_1(pidgin_conversations_get_handle(),
6831 "conversation-timestamp",
6832 conv, mtime, show_date);
6834 if (mdate == NULL)
6836 struct tm *tm = localtime(&mtime);
6837 const char *tmp;
6838 if (show_date)
6839 tmp = purple_date_format_long(tm);
6840 else
6841 tmp = purple_time_format(tm);
6842 mdate = g_strdup_printf("(%s)", tmp);
6845 /* Bi-Directional support - set timestamp direction using unicode characters */
6846 is_rtl_message = purple_markup_is_rtl(message);
6848 /* Handle plaintext messages with RTL text but no direction in the markup */
6849 if (!is_rtl_message && pango_find_base_dir(message, -1) == PANGO_DIRECTION_RTL)
6851 char *wrapped = g_strdup_printf("<SPAN style=\"direction:rtl;text-align:right;\">%s</SPAN>", displaying);
6853 g_free(displaying);
6854 displaying = wrapped;
6856 length = strlen(displaying) + 1;
6857 is_rtl_message = TRUE;
6860 /* Enforce direction only if message is RTL - doesn't effect LTR users */
6861 if (is_rtl_message)
6862 str_embed_direction_chars(&mdate);
6864 if (mtime >= gtkconv->newday)
6865 pidgin_conv_calculate_newday(gtkconv, mtime);
6867 sml_attrib = g_strdup_printf("sml=\"%s\"", purple_account_get_protocol_name(account));
6869 gtk_font_options |= GTK_IMHTML_NO_COMMENTS;
6871 if ((flags & PURPLE_MESSAGE_RECV) &&
6872 !purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/show_incoming_formatting"))
6873 gtk_font_options |= GTK_IMHTML_NO_COLOURS | GTK_IMHTML_NO_FONTS | GTK_IMHTML_NO_SIZES | GTK_IMHTML_NO_FORMATTING;
6875 /* this is gonna crash one day, I can feel it. */
6876 if (purple_protocols_find(purple_account_get_protocol_id(purple_conversation_get_account(conv)))->options &
6877 OPT_PROTO_USE_POINTSIZE) {
6878 gtk_font_options |= GTK_IMHTML_USE_POINTSIZE;
6881 /* TODO: These colors should not be hardcoded so log.c can use them */
6882 if (flags & PURPLE_MESSAGE_RAW) {
6883 pidgin_webview_append_html(PIDGIN_WEBVIEW(gtkconv->webview), message);
6884 } else if (flags & PURPLE_MESSAGE_SYSTEM) {
6885 g_snprintf(buf2, sizeof(buf2),
6886 "<font %s><font size=\"2\"><span class='timestamp'>%s</span></font><b>%s</b></font>",
6887 sml_attrib ? sml_attrib : "", mdate, displaying);
6889 pidgin_webview_append_html(PIDGIN_WEBVIEW(gtkconv->webview), buf2);
6891 } else if (flags & PURPLE_MESSAGE_ERROR) {
6892 g_snprintf(buf2, sizeof(buf2),
6893 "<font color=\"#ff0000\"><font %s><font size=\"2\"><span class='timestamp'>%s</span> </font><b>%s</b></font></font>",
6894 sml_attrib ? sml_attrib : "", mdate, displaying);
6896 pidgin_webview_append_html(PIDGIN_WEBVIEW(gtkconv->webview), buf2);
6898 } else if (flags & PURPLE_MESSAGE_NO_LOG) {
6899 g_snprintf(buf2, BUF_LONG,
6900 "<b><font %s color=\"#777777\">%s</font></b>",
6901 sml_attrib ? sml_attrib : "", displaying);
6903 pidgin_webview_append_html(PIDGIN_WEBVIEW(gtkconv->webview), buf2);
6904 } else {
6905 char *new_message = g_memdup(displaying, length);
6906 char *alias_escaped = (alias ? g_markup_escape_text(alias, strlen(alias)) : g_strdup(""));
6907 /* The initial offset is to deal with
6908 * escaped entities making the string longer */
6909 int tag_start_offset = 0;
6910 const char *tagname = NULL;
6912 /* Enforce direction on alias */
6913 if (is_rtl_message)
6914 str_embed_direction_chars(&alias_escaped);
6916 str = g_malloc(1024);
6917 if (TRUE) { /* XXX: reduce numer of indentations */
6918 if (purple_message_meify(new_message, -1)) {
6919 if (flags & PURPLE_MESSAGE_AUTO_RESP) {
6920 g_snprintf(str, 1024, "%s ***%s", AUTO_RESPONSE, alias_escaped);
6921 tag_start_offset += strlen(AUTO_RESPONSE) - 6 + 4;
6922 } else {
6923 g_snprintf(str, 1024, "***%s", alias_escaped);
6924 tag_start_offset += 3;
6927 if (flags & PURPLE_MESSAGE_NICK)
6928 tagname = "highlight-name";
6929 else
6930 tagname = "action-name";
6931 } else {
6932 if (flags & PURPLE_MESSAGE_AUTO_RESP) {
6933 g_snprintf(str, 1024, "%s %s", alias_escaped, AUTO_RESPONSE);
6934 tag_start_offset += strlen(AUTO_RESPONSE) - 6 + 1;
6935 } else {
6936 g_snprintf(str, 1024, "%s:", alias_escaped);
6937 #if 0
6938 tag_end_offset = 1;
6939 #endif
6942 if (flags & PURPLE_MESSAGE_NICK) {
6943 if (type == PURPLE_CONV_TYPE_IM) {
6944 tagname = "highlight-name";
6946 } else if (flags & PURPLE_MESSAGE_RECV) {
6947 /* The tagname for chats is handled by get_buddy_tag */
6948 if (type == PURPLE_CONV_TYPE_IM) {
6949 tagname = "receive-name";
6951 } else if (flags & PURPLE_MESSAGE_SEND) {
6952 tagname = "send-name";
6953 } else {
6954 purple_debug_error("gtkconv", "message missing flags\n");
6959 g_free(alias_escaped);
6961 /* TODO WEBKIT */
6962 #if 0
6963 if (tagname)
6964 tag = gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table(buffer), tagname);
6965 else
6966 tag = get_buddy_tag(conv, name, flags, TRUE);
6968 if (GTK_IMHTML(gtkconv->imhtml)->show_comments) {
6970 /* The color for the timestamp has to be set in the font-tags, unfortunately.
6971 * Applying the nick-tag to timestamps would work, but that can make it
6972 * bold. I thought applying the "comment" tag again, which has "weight" set
6973 * to PANGO_WEIGHT_NORMAL, would remove the boldness. But it doesn't. So
6974 * this will have to do. I don't terribly like it. -- sadrul */
6975 /* const char *color = get_text_tag_color(tag); */
6976 g_snprintf(buf2, BUF_LONG, "<FONT %s%s%s SIZE=\"2\"><!--%s --></FONT>",
6977 color ? "COLOR=\"" : "", color ? color : "", color ? "\"" : "", mdate);
6978 pidgin_webview_append_html (PIDGIN_WEBVIEW(gtkconv->webview), buf2);
6980 #endif /* if 0 */
6981 g_snprintf(buf2, BUF_LONG, "<font %s>%s</font> ", sml_attrib ? sml_attrib : "", str);
6982 pidgin_webview_append_html(PIDGIN_WEBVIEW(gtkconv->webview), buf2);
6984 g_free(str);
6986 if (gc) {
6987 char *pre = g_strdup_printf("<font %s>", sml_attrib ? sml_attrib : "");
6988 char *post = "</font>";
6989 with_font_tag = g_strdup_printf("%s%s%s", pre, new_message, post);
6990 g_free(pre);
6991 } else
6992 with_font_tag = g_memdup(new_message, length);
6994 pidgin_webview_append_html(PIDGIN_WEBVIEW(gtkconv->webview),
6995 with_font_tag);
6997 g_free(with_font_tag);
6998 g_free(new_message);
7001 g_free(mdate);
7002 g_free(sml_attrib);
7004 #endif
7006 /* Tab highlighting stuff */
7007 if (!(flags & PURPLE_MESSAGE_SEND) && !pidgin_conv_has_focus(conv))
7009 PidginUnseenState unseen = PIDGIN_UNSEEN_NONE;
7011 if ((flags & PURPLE_MESSAGE_NICK) == PURPLE_MESSAGE_NICK)
7012 unseen = PIDGIN_UNSEEN_NICK;
7013 else if (((flags & PURPLE_MESSAGE_SYSTEM) == PURPLE_MESSAGE_SYSTEM) ||
7014 ((flags & PURPLE_MESSAGE_ERROR) == PURPLE_MESSAGE_ERROR))
7015 unseen = PIDGIN_UNSEEN_EVENT;
7016 else if ((flags & PURPLE_MESSAGE_NO_LOG) == PURPLE_MESSAGE_NO_LOG)
7017 unseen = PIDGIN_UNSEEN_NO_LOG;
7018 else
7019 unseen = PIDGIN_UNSEEN_TEXT;
7021 gtkconv_set_unseen(gtkconv, unseen);
7024 /* on rejoin only request message history from after this message */
7025 if (flags & (PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_RECV) &&
7026 PURPLE_IS_CHAT_CONVERSATION(conv)) {
7027 PurpleChat *chat = purple_blist_find_chat(
7028 purple_conversation_get_account(conv),
7029 purple_conversation_get_name(conv));
7030 if (chat) {
7031 GHashTable *comps = purple_chat_get_components(chat);
7032 time_t now, history_since, prev_history_since = 0;
7033 struct tm *history_since_tm;
7034 const char *history_since_s, *prev_history_since_s;
7036 history_since = purple_message_get_time(pmsg) + 1;
7038 prev_history_since_s = g_hash_table_lookup(comps,
7039 "history_since");
7040 if (prev_history_since_s != NULL)
7041 prev_history_since = purple_str_to_time(
7042 prev_history_since_s, TRUE, NULL, NULL,
7043 NULL);
7045 now = time(NULL);
7046 /* in case of incorrectly stored timestamps */
7047 if (prev_history_since > now)
7048 prev_history_since = now;
7049 /* in case of delayed messages */
7050 if (history_since < prev_history_since)
7051 history_since = prev_history_since;
7053 history_since_tm = gmtime(&history_since);
7054 history_since_s = purple_utf8_strftime(
7055 "%Y-%m-%dT%H:%M:%SZ", history_since_tm);
7056 if (g_strcmp0(prev_history_since_s,
7057 history_since_s) != 0)
7058 g_hash_table_replace(comps,
7059 g_strdup("history_since"),
7060 g_strdup(history_since_s));
7064 purple_signal_emit(pidgin_conversations_get_handle(),
7065 (PURPLE_IS_IM_CONVERSATION(conv) ? "displayed-im-msg" : "displayed-chat-msg"),
7066 conv, pmsg);
7067 g_free(displaying);
7068 update_typing_message(gtkconv, NULL);
7071 static gboolean get_iter_from_chatuser(PurpleChatUser *cb, GtkTreeIter *iter)
7073 GtkTreeRowReference *ref;
7074 GtkTreePath *path;
7075 GtkTreeModel *model;
7077 g_return_val_if_fail(cb != NULL, FALSE);
7079 ref = purple_chat_user_get_ui_data(cb);
7080 if (!ref)
7081 return FALSE;
7083 if ((path = gtk_tree_row_reference_get_path(ref)) == NULL)
7084 return FALSE;
7086 model = gtk_tree_row_reference_get_model(ref);
7087 if (!gtk_tree_model_get_iter(GTK_TREE_MODEL(model), iter, path)) {
7088 gtk_tree_path_free(path);
7089 return FALSE;
7092 gtk_tree_path_free(path);
7093 return TRUE;
7096 static void
7097 pidgin_conv_chat_add_users(PurpleChatConversation *chat, GList *cbuddies, gboolean new_arrivals)
7099 PidginConversation *gtkconv;
7100 PidginChatPane *gtkchat;
7101 GtkListStore *ls;
7102 GList *l;
7104 char tmp[BUF_LONG];
7105 int num_users;
7107 gtkconv = PIDGIN_CONVERSATION(PURPLE_CONVERSATION(chat));
7108 gtkchat = gtkconv->u.chat;
7110 num_users = purple_chat_conversation_get_users_count(chat);
7112 g_snprintf(tmp, sizeof(tmp),
7113 ngettext("%d person in room", "%d people in room",
7114 num_users),
7115 num_users);
7117 gtk_label_set_text(GTK_LABEL(gtkchat->count), tmp);
7119 ls = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list)));
7121 gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(ls), GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID,
7122 GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID);
7124 l = cbuddies;
7125 while (l != NULL) {
7126 add_chat_user_common(chat, (PurpleChatUser *)l->data, NULL);
7127 l = l->next;
7130 /* Currently GTK+ maintains our sorted list after it's in the tree.
7131 * This may change if it turns out we can manage it faster ourselves.
7133 gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(ls), CHAT_USERS_ALIAS_KEY_COLUMN,
7134 GTK_SORT_ASCENDING);
7137 static void
7138 pidgin_conv_chat_rename_user(PurpleChatConversation *chat, const char *old_name,
7139 const char *new_name, const char *new_alias)
7141 PidginConversation *gtkconv;
7142 PidginChatPane *gtkchat;
7143 PurpleChatUser *old_chatuser, *new_chatuser;
7144 GtkTreeIter iter;
7145 GtkTreeModel *model;
7146 GtkTextTag *tag;
7148 gtkconv = PIDGIN_CONVERSATION(PURPLE_CONVERSATION(chat));
7149 gtkchat = gtkconv->u.chat;
7151 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
7153 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter))
7154 return;
7156 if ((tag = get_buddy_tag(chat, old_name, 0, FALSE)))
7157 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_ITALIC, NULL);
7158 if ((tag = get_buddy_tag(chat, old_name, PURPLE_MESSAGE_NICK, FALSE)))
7159 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_ITALIC, NULL);
7161 old_chatuser = purple_chat_conversation_find_user(chat, old_name);
7162 if (!old_chatuser)
7163 return;
7165 if (get_iter_from_chatuser(old_chatuser, &iter)) {
7166 GtkTreeRowReference *ref = purple_chat_user_get_ui_data(old_chatuser);
7168 gtk_list_store_remove(GTK_LIST_STORE(model), &iter);
7169 gtk_tree_row_reference_free(ref);
7170 purple_chat_user_set_ui_data(old_chatuser, NULL);
7173 g_return_if_fail(new_alias != NULL);
7175 new_chatuser = purple_chat_conversation_find_user(chat, new_name);
7177 add_chat_user_common(chat, new_chatuser, old_name);
7180 static void
7181 pidgin_conv_chat_remove_users(PurpleChatConversation *chat, GList *users)
7183 PidginConversation *gtkconv;
7184 PidginChatPane *gtkchat;
7185 GtkTreeIter iter;
7186 GtkTreeModel *model;
7187 GList *l;
7188 char tmp[BUF_LONG];
7189 int num_users;
7190 gboolean f;
7191 GtkTextTag *tag;
7193 gtkconv = PIDGIN_CONVERSATION(PURPLE_CONVERSATION(chat));
7194 gtkchat = gtkconv->u.chat;
7196 num_users = purple_chat_conversation_get_users_count(chat);
7198 for (l = users; l != NULL; l = l->next) {
7199 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
7201 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter))
7202 /* XXX: Break? */
7203 continue;
7205 do {
7206 char *val;
7208 gtk_tree_model_get(GTK_TREE_MODEL(model), &iter,
7209 CHAT_USERS_NAME_COLUMN, &val, -1);
7211 if (!purple_utf8_strcasecmp((char *)l->data, val)) {
7212 f = gtk_list_store_remove(GTK_LIST_STORE(model), &iter);
7214 else
7215 f = gtk_tree_model_iter_next(GTK_TREE_MODEL(model), &iter);
7217 g_free(val);
7218 } while (f);
7220 if ((tag = get_buddy_tag(chat, l->data, 0, FALSE)))
7221 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_ITALIC, NULL);
7222 if ((tag = get_buddy_tag(chat, l->data, PURPLE_MESSAGE_NICK, FALSE)))
7223 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_ITALIC, NULL);
7226 g_snprintf(tmp, sizeof(tmp),
7227 ngettext("%d person in room", "%d people in room",
7228 num_users), num_users);
7230 gtk_label_set_text(GTK_LABEL(gtkchat->count), tmp);
7233 static void
7234 pidgin_conv_chat_update_user(PurpleChatUser *chatuser)
7236 PurpleChatConversation *chat;
7237 PidginConversation *gtkconv;
7238 PidginChatPane *gtkchat;
7239 GtkTreeIter iter;
7240 GtkTreeModel *model;
7242 if (!chatuser)
7243 return;
7245 chat = purple_chat_user_get_chat(chatuser);
7246 gtkconv = PIDGIN_CONVERSATION(PURPLE_CONVERSATION(chat));
7247 gtkchat = gtkconv->u.chat;
7249 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
7251 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter))
7252 return;
7254 if (get_iter_from_chatuser(chatuser, &iter)) {
7255 GtkTreeRowReference *ref = purple_chat_user_get_ui_data(chatuser);
7256 gtk_list_store_remove(GTK_LIST_STORE(model), &iter);
7257 gtk_tree_row_reference_free(ref);
7258 purple_chat_user_set_ui_data(chatuser, NULL);
7261 if (chatuser)
7262 add_chat_user_common(chat, chatuser, NULL);
7265 gboolean
7266 pidgin_conv_has_focus(PurpleConversation *conv)
7268 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
7269 PidginConvWindow *win;
7270 gboolean has_focus;
7272 win = gtkconv->win;
7274 g_object_get(G_OBJECT(win->window), "has-toplevel-focus", &has_focus, NULL);
7276 if (has_focus && pidgin_conv_window_is_active_conversation(conv))
7277 return TRUE;
7279 return FALSE;
7282 static void
7283 pidgin_conv_send_confirm(PurpleConversation *conv, const char *message)
7285 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
7287 pidgin_webview_append_html(PIDGIN_WEBVIEW(gtkconv->webview), message);
7291 * Makes sure all the menu items and all the buttons are hidden/shown and
7292 * sensitive/insensitive. This is called after changing tabs and when an
7293 * account signs on or off.
7295 static void
7296 gray_stuff_out(PidginConversation *gtkconv)
7298 PidginConvWindow *win;
7299 PurpleConversation *conv = gtkconv->active_conv;
7300 PurpleConnection *gc;
7301 PurpleProtocol *protocol = NULL;
7302 GdkPixbuf *window_icon = NULL;
7303 PidginWebViewButtons buttons;
7304 PurpleAccount *account;
7306 win = pidgin_conv_get_window(gtkconv);
7307 gc = purple_conversation_get_connection(conv);
7308 account = purple_conversation_get_account(conv);
7310 if (gc != NULL)
7311 protocol = purple_connection_get_protocol(gc);
7313 if (win->menu->send_to != NULL)
7314 update_send_to_selection(win);
7317 * Handle hiding and showing stuff based on what type of conv this is.
7318 * Stuff that Purple IMs support in general should be shown for IM
7319 * conversations. Stuff that Purple chats support in general should be
7320 * shown for chat conversations. It doesn't matter whether the protocol
7321 * supports it or not--that only affects if the button or menu item
7322 * is sensitive or not.
7324 if (PURPLE_IS_IM_CONVERSATION(conv)) {
7325 /* Show stuff that applies to IMs, hide stuff that applies to chats */
7327 /* Deal with menu items */
7328 gtk_action_set_visible(win->menu->view_log, TRUE);
7329 gtk_action_set_visible(win->menu->send_file, TRUE);
7330 gtk_action_set_visible(win->menu->get_attention, TRUE);
7331 gtk_action_set_visible(win->menu->add_pounce, TRUE);
7332 gtk_action_set_visible(win->menu->get_info, TRUE);
7333 gtk_action_set_visible(win->menu->invite, FALSE);
7334 gtk_action_set_visible(win->menu->alias, TRUE);
7335 if (purple_account_privacy_check(account, purple_conversation_get_name(conv))) {
7336 gtk_action_set_visible(win->menu->unblock, FALSE);
7337 gtk_action_set_visible(win->menu->block, TRUE);
7338 } else {
7339 gtk_action_set_visible(win->menu->block, FALSE);
7340 gtk_action_set_visible(win->menu->unblock, TRUE);
7343 if (purple_blist_find_buddy(account, purple_conversation_get_name(conv)) == NULL) {
7344 gtk_action_set_visible(win->menu->add, TRUE);
7345 gtk_action_set_visible(win->menu->remove, FALSE);
7346 } else {
7347 gtk_action_set_visible(win->menu->remove, TRUE);
7348 gtk_action_set_visible(win->menu->add, FALSE);
7351 gtk_action_set_visible(win->menu->insert_link, TRUE);
7352 gtk_action_set_visible(win->menu->insert_image, TRUE);
7353 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
7354 /* Show stuff that applies to Chats, hide stuff that applies to IMs */
7356 /* Deal with menu items */
7357 gtk_action_set_visible(win->menu->view_log, TRUE);
7358 gtk_action_set_visible(win->menu->send_file, FALSE);
7359 gtk_action_set_visible(win->menu->get_attention, FALSE);
7360 gtk_action_set_visible(win->menu->add_pounce, FALSE);
7361 gtk_action_set_visible(win->menu->get_info, FALSE);
7362 gtk_action_set_visible(win->menu->invite, TRUE);
7363 gtk_action_set_visible(win->menu->alias, TRUE);
7364 gtk_action_set_visible(win->menu->block, FALSE);
7365 gtk_action_set_visible(win->menu->unblock, FALSE);
7367 if ((account == NULL) || purple_blist_find_chat(account, purple_conversation_get_name(conv)) == NULL) {
7368 /* If the chat is NOT in the buddy list */
7369 gtk_action_set_visible(win->menu->add, TRUE);
7370 gtk_action_set_visible(win->menu->remove, FALSE);
7371 } else {
7372 /* If the chat IS in the buddy list */
7373 gtk_action_set_visible(win->menu->add, FALSE);
7374 gtk_action_set_visible(win->menu->remove, TRUE);
7377 gtk_action_set_visible(win->menu->insert_link, TRUE);
7378 gtk_action_set_visible(win->menu->insert_image, TRUE);
7382 * Handle graying stuff out based on whether an account is connected
7383 * and what features that account supports.
7385 if ((gc != NULL) &&
7386 (!PURPLE_IS_CHAT_CONVERSATION(conv) ||
7387 !purple_chat_conversation_has_left(PURPLE_CHAT_CONVERSATION(conv)) ))
7389 PurpleConnectionFlags features = purple_conversation_get_features(conv);
7390 /* Account is online */
7391 /* Deal with the toolbar */
7392 if (features & PURPLE_CONNECTION_FLAG_HTML)
7394 buttons = PIDGIN_WEBVIEW_ALL; /* Everything on */
7395 if (features & PURPLE_CONNECTION_FLAG_NO_BGCOLOR)
7396 buttons &= ~PIDGIN_WEBVIEW_BACKCOLOR;
7397 if (features & PURPLE_CONNECTION_FLAG_NO_FONTSIZE)
7399 buttons &= ~PIDGIN_WEBVIEW_GROW;
7400 buttons &= ~PIDGIN_WEBVIEW_SHRINK;
7402 if (features & PURPLE_CONNECTION_FLAG_NO_URLDESC)
7403 buttons &= ~PIDGIN_WEBVIEW_LINKDESC;
7404 } else {
7405 buttons = PIDGIN_WEBVIEW_SMILEY | PIDGIN_WEBVIEW_IMAGE;
7408 if (features & PURPLE_CONNECTION_FLAG_NO_IMAGES)
7409 buttons &= ~PIDGIN_WEBVIEW_IMAGE;
7411 if (features & PURPLE_CONNECTION_FLAG_ALLOW_CUSTOM_SMILEY)
7412 buttons |= PIDGIN_WEBVIEW_CUSTOM_SMILEY;
7413 else
7414 buttons &= ~PIDGIN_WEBVIEW_CUSTOM_SMILEY;
7416 pidgin_webview_set_format_functions(PIDGIN_WEBVIEW(gtkconv->entry), buttons);
7418 /* Deal with menu items */
7419 gtk_action_set_sensitive(win->menu->view_log, TRUE);
7420 gtk_action_set_sensitive(win->menu->add_pounce, TRUE);
7421 gtk_action_set_sensitive(win->menu->get_info, (PURPLE_PROTOCOL_IMPLEMENTS(protocol, SERVER_IFACE, get_info)));
7422 gtk_action_set_sensitive(win->menu->invite, (PURPLE_PROTOCOL_IMPLEMENTS(protocol, CHAT_IFACE, invite)));
7423 gtk_action_set_sensitive(win->menu->insert_link, (features & PURPLE_CONNECTION_FLAG_HTML));
7424 gtk_action_set_sensitive(win->menu->insert_image, !(features & PURPLE_CONNECTION_FLAG_NO_IMAGES));
7426 if (PURPLE_IS_IM_CONVERSATION(conv))
7428 gtk_action_set_sensitive(win->menu->add, (PURPLE_PROTOCOL_IMPLEMENTS(protocol, SERVER_IFACE, add_buddy)));
7429 gtk_action_set_sensitive(win->menu->remove, (PURPLE_PROTOCOL_IMPLEMENTS(protocol, SERVER_IFACE, remove_buddy)));
7430 gtk_action_set_sensitive(win->menu->send_file,
7431 (PURPLE_PROTOCOL_IMPLEMENTS(protocol, XFER_IFACE, send) &&
7432 (!PURPLE_PROTOCOL_IMPLEMENTS(protocol, XFER_IFACE, can_receive) ||
7433 purple_protocol_xfer_iface_can_receive(protocol, gc, purple_conversation_get_name(conv)))));
7434 gtk_action_set_sensitive(win->menu->get_attention, (PURPLE_PROTOCOL_IMPLEMENTS(protocol, ATTENTION_IFACE, send)));
7435 gtk_action_set_sensitive(win->menu->alias,
7436 (account != NULL) &&
7437 (purple_blist_find_buddy(account, purple_conversation_get_name(conv)) != NULL));
7439 else if (PURPLE_IS_CHAT_CONVERSATION(conv))
7441 gtk_action_set_sensitive(win->menu->add, (PURPLE_PROTOCOL_IMPLEMENTS(protocol, CHAT_IFACE, join)));
7442 gtk_action_set_sensitive(win->menu->remove, (PURPLE_PROTOCOL_IMPLEMENTS(protocol, CHAT_IFACE, join)));
7443 gtk_action_set_sensitive(win->menu->alias,
7444 (account != NULL) &&
7445 (purple_blist_find_chat(account, purple_conversation_get_name(conv)) != NULL));
7448 } else {
7449 /* Account is offline */
7450 /* Or it's a chat that we've left. */
7452 /* Then deal with menu items */
7453 gtk_action_set_sensitive(win->menu->view_log, TRUE);
7454 gtk_action_set_sensitive(win->menu->send_file, FALSE);
7455 gtk_action_set_sensitive(win->menu->get_attention, FALSE);
7456 gtk_action_set_sensitive(win->menu->add_pounce, TRUE);
7457 gtk_action_set_sensitive(win->menu->get_info, FALSE);
7458 gtk_action_set_sensitive(win->menu->invite, FALSE);
7459 gtk_action_set_sensitive(win->menu->alias, FALSE);
7460 gtk_action_set_sensitive(win->menu->add, FALSE);
7461 gtk_action_set_sensitive(win->menu->remove, FALSE);
7462 gtk_action_set_sensitive(win->menu->insert_link, TRUE);
7463 gtk_action_set_sensitive(win->menu->insert_image, FALSE);
7467 * Update the window's icon
7469 if (pidgin_conv_window_is_active_conversation(conv))
7471 GList *l = NULL;
7472 if (PURPLE_IS_IM_CONVERSATION(conv) &&
7473 (gtkconv->u.im->anim))
7475 PurpleBuddy *buddy = purple_blist_find_buddy(purple_conversation_get_account(conv), purple_conversation_get_name(conv));
7476 window_icon =
7477 gdk_pixbuf_animation_get_static_image(gtkconv->u.im->anim);
7479 if (buddy && !PURPLE_BUDDY_IS_ONLINE(buddy))
7480 gdk_pixbuf_saturate_and_pixelate(window_icon, window_icon, 0.0, FALSE);
7482 g_object_ref(window_icon);
7483 l = g_list_append(l, window_icon);
7484 } else {
7485 l = pidgin_conv_get_tab_icons(conv);
7487 gtk_window_set_icon_list(GTK_WINDOW(win->window), l);
7488 if (window_icon != NULL) {
7489 g_object_unref(G_OBJECT(window_icon));
7490 g_list_free(l);
7495 static void
7496 pidgin_conv_update_fields(PurpleConversation *conv, PidginConvFields fields)
7498 PidginConversation *gtkconv;
7499 PidginConvWindow *win;
7501 gtkconv = PIDGIN_CONVERSATION(conv);
7502 if (!gtkconv)
7503 return;
7504 win = pidgin_conv_get_window(gtkconv);
7505 if (!win)
7506 return;
7508 if (fields & PIDGIN_CONV_SET_TITLE)
7510 purple_conversation_autoset_title(conv);
7513 if (fields & PIDGIN_CONV_BUDDY_ICON)
7515 if (PURPLE_IS_IM_CONVERSATION(conv))
7516 pidgin_conv_update_buddy_icon(PURPLE_IM_CONVERSATION(conv));
7519 if (fields & PIDGIN_CONV_MENU)
7521 gray_stuff_out(PIDGIN_CONVERSATION(conv));
7522 generate_send_to_items(win);
7523 regenerate_plugins_items(win);
7526 if (fields & PIDGIN_CONV_E2EE)
7527 generate_e2ee_controls(win);
7529 if (fields & PIDGIN_CONV_TAB_ICON)
7531 update_tab_icon(conv);
7532 generate_send_to_items(win); /* To update the icons in SendTo menu */
7535 if ((fields & PIDGIN_CONV_TOPIC) &&
7536 PURPLE_IS_CHAT_CONVERSATION(conv))
7538 const char *topic;
7539 PidginChatPane *gtkchat = gtkconv->u.chat;
7541 if (gtkchat->topic_text != NULL)
7543 topic = purple_chat_conversation_get_topic(PURPLE_CHAT_CONVERSATION(conv));
7545 gtk_entry_set_text(GTK_ENTRY(gtkchat->topic_text), topic ? topic : "");
7546 gtk_widget_set_tooltip_text(gtkchat->topic_text,
7547 topic ? topic : "");
7551 if ((fields & PIDGIN_CONV_COLORIZE_TITLE) ||
7552 (fields & PIDGIN_CONV_SET_TITLE) ||
7553 (fields & PIDGIN_CONV_TOPIC))
7555 char *title;
7556 PurpleIMConversation *im = NULL;
7557 PurpleAccount *account = purple_conversation_get_account(conv);
7558 PurpleBuddy *buddy = NULL;
7559 char *markup = NULL;
7560 AtkObject *accessibility_obj;
7561 /* I think this is a little longer than it needs to be but I'm lazy. */
7562 char *style;
7564 if (PURPLE_IS_IM_CONVERSATION(conv))
7565 im = PURPLE_IM_CONVERSATION(conv);
7567 if ((account == NULL) ||
7568 !purple_account_is_connected(account) ||
7569 (PURPLE_IS_CHAT_CONVERSATION(conv)
7570 && purple_chat_conversation_has_left(PURPLE_CHAT_CONVERSATION(conv))))
7571 title = g_strdup_printf("(%s)", purple_conversation_get_title(conv));
7572 else
7573 title = g_strdup(purple_conversation_get_title(conv));
7575 if (PURPLE_IS_IM_CONVERSATION(conv)) {
7576 buddy = purple_blist_find_buddy(account, purple_conversation_get_name(conv));
7577 if (buddy) {
7578 markup = pidgin_blist_get_name_markup(buddy, FALSE, FALSE);
7579 } else {
7580 markup = title;
7582 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
7583 const char *topic = gtkconv->u.chat->topic_text
7584 ? gtk_entry_get_text(GTK_ENTRY(gtkconv->u.chat->topic_text))
7585 : NULL;
7586 const char *title = purple_conversation_get_title(conv);
7587 const char *name = purple_conversation_get_name(conv);
7589 char *topic_esc, *unaliased, *unaliased_esc, *title_esc;
7591 topic_esc = topic ? g_markup_escape_text(topic, -1) : NULL;
7592 unaliased = g_utf8_collate(title, name) ? g_strdup_printf("(%s)", name) : NULL;
7593 unaliased_esc = unaliased ? g_markup_escape_text(unaliased, -1) : NULL;
7594 title_esc = g_markup_escape_text(title, -1);
7596 markup = g_strdup_printf("%s%s<span size='smaller'>%s</span>%s<span color='%s' size='smaller'>%s</span>",
7597 title_esc,
7598 unaliased_esc ? " " : "",
7599 unaliased_esc ? unaliased_esc : "",
7600 topic_esc && *topic_esc ? "\n" : "",
7601 pidgin_get_dim_grey_string(gtkconv->infopane),
7602 topic_esc ? topic_esc : "");
7604 g_free(title_esc);
7605 g_free(topic_esc);
7606 g_free(unaliased);
7607 g_free(unaliased_esc);
7609 gtk_list_store_set(gtkconv->infopane_model, &(gtkconv->infopane_iter),
7610 CONV_TEXT_COLUMN, markup, -1);
7611 /* XXX seanegan Why do I have to do this? */
7612 gtk_widget_queue_draw(gtkconv->infopane);
7614 if (title != markup)
7615 g_free(markup);
7617 if (!gtk_widget_get_realized(gtkconv->tab_label))
7618 gtk_widget_realize(gtkconv->tab_label);
7620 accessibility_obj = gtk_widget_get_accessible(gtkconv->tab_cont);
7621 if (im != NULL &&
7622 purple_im_conversation_get_typing_state(im) == PURPLE_IM_TYPING) {
7623 atk_object_set_description(accessibility_obj, _("Typing"));
7624 style = "tab-label-typing";
7625 } else if (im != NULL &&
7626 purple_im_conversation_get_typing_state(im) == PURPLE_IM_TYPED) {
7627 atk_object_set_description(accessibility_obj, _("Stopped Typing"));
7628 style = "tab-label-typed";
7629 } else if (gtkconv->unseen_state == PIDGIN_UNSEEN_NICK) {
7630 atk_object_set_description(accessibility_obj, _("Nick Said"));
7631 style = "tab-label-attention";
7632 } else if (gtkconv->unseen_state == PIDGIN_UNSEEN_TEXT) {
7633 atk_object_set_description(accessibility_obj, _("Unread Messages"));
7634 if (PURPLE_IS_CHAT_CONVERSATION(gtkconv->active_conv))
7635 style = "tab-label-unreadchat";
7636 else
7637 style = "tab-label-attention";
7638 } else if (gtkconv->unseen_state == PIDGIN_UNSEEN_EVENT) {
7639 atk_object_set_description(accessibility_obj, _("New Event"));
7640 style = "tab-label-event";
7641 } else {
7642 style = "tab-label";
7645 gtk_widget_set_name(gtkconv->tab_label, style);
7646 gtk_label_set_text(GTK_LABEL(gtkconv->tab_label), title);
7647 gtk_widget_set_state_flags(gtkconv->tab_label, GTK_STATE_FLAG_ACTIVE, TRUE);
7649 if (gtkconv->unseen_state == PIDGIN_UNSEEN_TEXT ||
7650 gtkconv->unseen_state == PIDGIN_UNSEEN_NICK ||
7651 gtkconv->unseen_state == PIDGIN_UNSEEN_EVENT) {
7652 PangoAttrList *list = pango_attr_list_new();
7653 PangoAttribute *attr = pango_attr_weight_new(PANGO_WEIGHT_BOLD);
7654 attr->start_index = 0;
7655 attr->end_index = -1;
7656 pango_attr_list_insert(list, attr);
7657 gtk_label_set_attributes(GTK_LABEL(gtkconv->tab_label), list);
7658 pango_attr_list_unref(list);
7659 } else
7660 gtk_label_set_attributes(GTK_LABEL(gtkconv->tab_label), NULL);
7662 if (pidgin_conv_window_is_active_conversation(conv))
7663 update_typing_icon(gtkconv);
7665 gtk_label_set_text(GTK_LABEL(gtkconv->menu_label), title);
7666 if (pidgin_conv_window_is_active_conversation(conv)) {
7667 const char* current_title = gtk_window_get_title(GTK_WINDOW(win->window));
7668 if (current_title == NULL || g_strcmp0(current_title, title) != 0)
7669 gtk_window_set_title(GTK_WINDOW(win->window), title);
7672 g_free(title);
7676 static void
7677 pidgin_conv_updated(PurpleConversation *conv, PurpleConversationUpdateType type)
7679 PidginConvFields flags = 0;
7681 g_return_if_fail(conv != NULL);
7683 if (type == PURPLE_CONVERSATION_UPDATE_ACCOUNT)
7685 flags = PIDGIN_CONV_ALL;
7687 else if (type == PURPLE_CONVERSATION_UPDATE_TYPING ||
7688 type == PURPLE_CONVERSATION_UPDATE_UNSEEN ||
7689 type == PURPLE_CONVERSATION_UPDATE_TITLE)
7691 flags = PIDGIN_CONV_COLORIZE_TITLE;
7693 else if (type == PURPLE_CONVERSATION_UPDATE_TOPIC)
7695 flags = PIDGIN_CONV_TOPIC;
7697 else if (type == PURPLE_CONVERSATION_ACCOUNT_ONLINE ||
7698 type == PURPLE_CONVERSATION_ACCOUNT_OFFLINE)
7700 flags = PIDGIN_CONV_MENU | PIDGIN_CONV_TAB_ICON | PIDGIN_CONV_SET_TITLE;
7702 else if (type == PURPLE_CONVERSATION_UPDATE_AWAY)
7704 flags = PIDGIN_CONV_TAB_ICON;
7706 else if (type == PURPLE_CONVERSATION_UPDATE_ADD ||
7707 type == PURPLE_CONVERSATION_UPDATE_REMOVE ||
7708 type == PURPLE_CONVERSATION_UPDATE_CHATLEFT)
7710 flags = PIDGIN_CONV_SET_TITLE | PIDGIN_CONV_MENU;
7712 else if (type == PURPLE_CONVERSATION_UPDATE_ICON)
7714 flags = PIDGIN_CONV_BUDDY_ICON;
7716 else if (type == PURPLE_CONVERSATION_UPDATE_FEATURES)
7718 flags = PIDGIN_CONV_MENU;
7720 else if (type == PURPLE_CONVERSATION_UPDATE_E2EE)
7722 flags = PIDGIN_CONV_E2EE | PIDGIN_CONV_MENU;
7725 pidgin_conv_update_fields(conv, flags);
7728 static void
7729 wrote_msg_update_unseen_cb(PurpleConversation *conv, PurpleMessage *msg,
7730 gpointer _unused)
7732 PidginConversation *gtkconv = conv ? PIDGIN_CONVERSATION(conv) : NULL;
7733 PurpleMessageFlags flags;
7734 if (conv == NULL || (gtkconv && gtkconv->win != hidden_convwin))
7735 return;
7736 flags = purple_message_get_flags(msg);
7737 if (flags & (PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_RECV)) {
7738 PidginUnseenState unseen = PIDGIN_UNSEEN_NONE;
7740 if ((flags & PURPLE_MESSAGE_NICK) == PURPLE_MESSAGE_NICK)
7741 unseen = PIDGIN_UNSEEN_NICK;
7742 else if (((flags & PURPLE_MESSAGE_SYSTEM) == PURPLE_MESSAGE_SYSTEM) ||
7743 ((flags & PURPLE_MESSAGE_ERROR) == PURPLE_MESSAGE_ERROR))
7744 unseen = PIDGIN_UNSEEN_EVENT;
7745 else if ((flags & PURPLE_MESSAGE_NO_LOG) == PURPLE_MESSAGE_NO_LOG)
7746 unseen = PIDGIN_UNSEEN_NO_LOG;
7747 else
7748 unseen = PIDGIN_UNSEEN_TEXT;
7750 conv_set_unseen(conv, unseen);
7754 static PurpleConversationUiOps conversation_ui_ops =
7756 pidgin_conv_new,
7757 pidgin_conv_destroy, /* destroy_conversation */
7758 NULL, /* write_chat */
7759 NULL, /* write_im */
7760 pidgin_conv_write_conv, /* write_conv */
7761 pidgin_conv_chat_add_users, /* chat_add_users */
7762 pidgin_conv_chat_rename_user, /* chat_rename_user */
7763 pidgin_conv_chat_remove_users, /* chat_remove_users */
7764 pidgin_conv_chat_update_user, /* chat_update_user */
7765 pidgin_conv_present_conversation, /* present */
7766 pidgin_conv_has_focus, /* has_focus */
7767 pidgin_conv_send_confirm, /* send_confirm */
7768 NULL,
7769 NULL,
7770 NULL,
7771 NULL
7774 PurpleConversationUiOps *
7775 pidgin_conversations_get_conv_ui_ops(void)
7777 return &conversation_ui_ops;
7780 /**************************************************************************
7781 * Public conversation utility functions
7782 **************************************************************************/
7783 void
7784 pidgin_conv_update_buddy_icon(PurpleIMConversation *im)
7786 PidginConversation *gtkconv;
7787 PurpleConversation *conv;
7788 PidginConvWindow *win;
7790 PurpleBuddy *buddy;
7792 PurpleImage *custom_img = NULL;
7793 gconstpointer data = NULL;
7794 size_t len;
7796 GdkPixbuf *buf;
7798 GList *children;
7799 GtkWidget *event;
7800 GdkPixbuf *scale;
7801 int scale_width, scale_height;
7802 int size = 0;
7804 PurpleAccount *account;
7806 PurpleBuddyIcon *icon;
7808 conv = PURPLE_CONVERSATION(im);
7810 g_return_if_fail(conv != NULL);
7811 g_return_if_fail(PIDGIN_IS_PIDGIN_CONVERSATION(conv));
7813 gtkconv = PIDGIN_CONVERSATION(conv);
7814 win = gtkconv->win;
7815 if (conv != gtkconv->active_conv)
7816 return;
7818 if (!gtkconv->u.im->show_icon)
7819 return;
7821 account = purple_conversation_get_account(conv);
7823 /* Remove the current icon stuff */
7824 children = gtk_container_get_children(GTK_CONTAINER(gtkconv->u.im->icon_container));
7825 if (children) {
7826 /* We know there's only one child here. It'd be nice to shortcut to the
7827 event box, but we can't change the PidginConversation until 3.0 */
7828 event = (GtkWidget *)children->data;
7829 gtk_container_remove(GTK_CONTAINER(gtkconv->u.im->icon_container), event);
7830 g_list_free(children);
7833 if (gtkconv->u.im->anim != NULL)
7834 g_object_unref(G_OBJECT(gtkconv->u.im->anim));
7836 gtkconv->u.im->anim = NULL;
7838 if (gtkconv->u.im->icon_timer != 0)
7839 g_source_remove(gtkconv->u.im->icon_timer);
7841 gtkconv->u.im->icon_timer = 0;
7843 if (gtkconv->u.im->iter != NULL)
7844 g_object_unref(G_OBJECT(gtkconv->u.im->iter));
7846 gtkconv->u.im->iter = NULL;
7848 if (!purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons"))
7849 return;
7851 if (purple_conversation_get_connection(conv) == NULL)
7852 return;
7854 buddy = purple_blist_find_buddy(account, purple_conversation_get_name(conv));
7855 if (buddy)
7857 PurpleContact *contact = purple_buddy_get_contact(buddy);
7858 if (contact) {
7859 custom_img = purple_buddy_icons_node_find_custom_icon((PurpleBlistNode*)contact);
7860 if (custom_img) {
7861 /* There is a custom icon for this user */
7862 data = purple_image_get_data(custom_img);
7863 len = purple_image_get_size(custom_img);
7868 if (data == NULL) {
7869 icon = purple_im_conversation_get_icon(im);
7870 if (icon == NULL)
7872 gtk_widget_set_size_request(gtkconv->u.im->icon_container,
7873 -1, BUDDYICON_SIZE_MIN);
7874 return;
7877 data = purple_buddy_icon_get_data(icon, &len);
7878 if (data == NULL)
7880 gtk_widget_set_size_request(gtkconv->u.im->icon_container,
7881 -1, BUDDYICON_SIZE_MIN);
7882 return;
7886 gtkconv->u.im->anim = pidgin_pixbuf_anim_from_data(data, len);
7887 if (custom_img)
7888 g_object_unref(custom_img);
7890 if (!gtkconv->u.im->anim) {
7891 purple_debug_error("gtkconv", "Couldn't load icon for conv %s\n",
7892 purple_conversation_get_name(conv));
7893 return;
7896 if (gdk_pixbuf_animation_is_static_image(gtkconv->u.im->anim)) {
7897 GdkPixbuf *stat;
7898 gtkconv->u.im->iter = NULL;
7899 stat = gdk_pixbuf_animation_get_static_image(gtkconv->u.im->anim);
7900 buf = gdk_pixbuf_add_alpha(stat, FALSE, 0, 0, 0);
7901 } else {
7902 GdkPixbuf *stat;
7903 gtkconv->u.im->iter =
7904 gdk_pixbuf_animation_get_iter(gtkconv->u.im->anim, NULL); /* LEAK */
7905 stat = gdk_pixbuf_animation_iter_get_pixbuf(gtkconv->u.im->iter);
7906 buf = gdk_pixbuf_add_alpha(stat, FALSE, 0, 0, 0);
7907 if (gtkconv->u.im->animate)
7908 start_anim(NULL, gtkconv);
7911 scale_width = gdk_pixbuf_get_width(buf);
7912 scale_height = gdk_pixbuf_get_height(buf);
7914 gtk_widget_get_size_request(gtkconv->u.im->icon_container, NULL, &size);
7915 size = MIN(size, MIN(scale_width, scale_height));
7917 /* Some sanity checks */
7918 size = CLAMP(size, BUDDYICON_SIZE_MIN, BUDDYICON_SIZE_MAX);
7919 if (scale_width == scale_height) {
7920 scale_width = scale_height = size;
7921 } else if (scale_height > scale_width) {
7922 scale_width = size * scale_width / scale_height;
7923 scale_height = size;
7924 } else {
7925 scale_height = size * scale_height / scale_width;
7926 scale_width = size;
7928 scale = gdk_pixbuf_scale_simple(buf, scale_width, scale_height,
7929 GDK_INTERP_BILINEAR);
7930 g_object_unref(buf);
7931 if (pidgin_gdk_pixbuf_is_opaque(scale))
7932 pidgin_gdk_pixbuf_make_round(scale);
7934 event = gtk_event_box_new();
7935 gtk_container_add(GTK_CONTAINER(gtkconv->u.im->icon_container), event);
7936 gtk_event_box_set_visible_window(GTK_EVENT_BOX(event), FALSE);
7937 gtk_widget_add_events(event,
7938 GDK_POINTER_MOTION_MASK | GDK_LEAVE_NOTIFY_MASK);
7939 g_signal_connect(G_OBJECT(event), "button-press-event",
7940 G_CALLBACK(icon_menu), gtkconv);
7942 pidgin_tooltip_setup_for_widget(event, gtkconv, pidgin_conv_create_tooltip, NULL);
7943 gtk_widget_show(event);
7945 gtkconv->u.im->icon = gtk_image_new_from_pixbuf(scale);
7946 gtk_container_add(GTK_CONTAINER(event), gtkconv->u.im->icon);
7947 gtk_widget_show(gtkconv->u.im->icon);
7949 g_object_unref(G_OBJECT(scale));
7951 /* The buddy icon code needs badly to be fixed. */
7952 if(pidgin_conv_window_is_active_conversation(conv))
7954 buf = gdk_pixbuf_animation_get_static_image(gtkconv->u.im->anim);
7955 if (buddy && !PURPLE_BUDDY_IS_ONLINE(buddy))
7956 gdk_pixbuf_saturate_and_pixelate(buf, buf, 0.0, FALSE);
7957 gtk_window_set_icon(GTK_WINDOW(win->window), buf);
7961 void
7962 pidgin_conv_update_buttons_by_protocol(PurpleConversation *conv)
7964 PidginConvWindow *win;
7966 if (!PIDGIN_IS_PIDGIN_CONVERSATION(conv))
7967 return;
7969 win = PIDGIN_CONVERSATION(conv)->win;
7971 if (win != NULL && pidgin_conv_window_is_active_conversation(conv))
7972 gray_stuff_out(PIDGIN_CONVERSATION(conv));
7975 static gboolean
7976 pidgin_conv_xy_to_right_infopane(PidginConvWindow *win, int x, int y)
7978 gint pane_x, pane_y, x_rel;
7979 PidginConversation *gtkconv;
7980 GtkAllocation allocation;
7982 gdk_window_get_origin(gtk_widget_get_window(win->notebook),
7983 &pane_x, &pane_y);
7984 x_rel = x - pane_x;
7985 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
7986 gtk_widget_get_allocation(gtkconv->infopane, &allocation);
7987 return (x_rel > allocation.x + allocation.width / 2);
7991 pidgin_conv_get_tab_at_xy(PidginConvWindow *win, int x, int y, gboolean *to_right)
7993 gint nb_x, nb_y, x_rel, y_rel;
7994 GtkNotebook *notebook;
7995 GtkWidget *page, *tab;
7996 gint i, page_num = -1;
7997 gint count;
7998 gboolean horiz;
8000 if (to_right)
8001 *to_right = FALSE;
8003 notebook = GTK_NOTEBOOK(win->notebook);
8005 gdk_window_get_origin(gtk_widget_get_window(win->notebook), &nb_x, &nb_y);
8006 x_rel = x - nb_x;
8007 y_rel = y - nb_y;
8009 horiz = (gtk_notebook_get_tab_pos(notebook) == GTK_POS_TOP ||
8010 gtk_notebook_get_tab_pos(notebook) == GTK_POS_BOTTOM);
8012 count = gtk_notebook_get_n_pages(GTK_NOTEBOOK(notebook));
8014 for (i = 0; i < count; i++) {
8015 GtkAllocation allocation;
8017 page = gtk_notebook_get_nth_page(GTK_NOTEBOOK(notebook), i);
8018 tab = gtk_notebook_get_tab_label(GTK_NOTEBOOK(notebook), page);
8019 gtk_widget_get_allocation(tab, &allocation);
8021 /* Make sure the tab is not hidden beyond an arrow */
8022 if (!gtk_widget_is_drawable(tab) && gtk_notebook_get_show_tabs(notebook))
8023 continue;
8025 if (horiz) {
8026 if (x_rel >= allocation.x - PIDGIN_HIG_BOX_SPACE &&
8027 x_rel <= allocation.x + allocation.width + PIDGIN_HIG_BOX_SPACE) {
8028 page_num = i;
8030 if (to_right && x_rel >= allocation.x + allocation.width/2)
8031 *to_right = TRUE;
8033 break;
8035 } else {
8036 if (y_rel >= allocation.y - PIDGIN_HIG_BOX_SPACE &&
8037 y_rel <= allocation.y + allocation.height + PIDGIN_HIG_BOX_SPACE) {
8038 page_num = i;
8040 if (to_right && y_rel >= allocation.y + allocation.height/2)
8041 *to_right = TRUE;
8043 break;
8048 if (page_num == -1) {
8049 /* Add after the last tab */
8050 page_num = count - 1;
8053 return page_num;
8056 static void
8057 close_on_tabs_pref_cb(const char *name, PurplePrefType type,
8058 gconstpointer value, gpointer data)
8060 GList *l;
8061 PurpleConversation *conv;
8062 PidginConversation *gtkconv;
8064 for (l = purple_conversations_get_all(); l != NULL; l = l->next) {
8065 conv = (PurpleConversation *)l->data;
8067 if (!PIDGIN_IS_PIDGIN_CONVERSATION(conv))
8068 continue;
8070 gtkconv = PIDGIN_CONVERSATION(conv);
8072 if (value)
8073 gtk_widget_show(gtkconv->close);
8074 else
8075 gtk_widget_hide(gtkconv->close);
8079 static void
8080 spellcheck_pref_cb(const char *name, PurplePrefType type,
8081 gconstpointer value, gpointer data)
8083 GList *cl;
8084 PurpleConversation *conv;
8085 PidginConversation *gtkconv;
8087 for (cl = purple_conversations_get_all(); cl != NULL; cl = cl->next) {
8089 conv = (PurpleConversation *)cl->data;
8091 if (!PIDGIN_IS_PIDGIN_CONVERSATION(conv))
8092 continue;
8094 gtkconv = PIDGIN_CONVERSATION(conv);
8096 pidgin_webview_set_spellcheck(PIDGIN_WEBVIEW(gtkconv->entry),
8097 (gboolean)GPOINTER_TO_INT(value));
8101 static void
8102 tab_side_pref_cb(const char *name, PurplePrefType type,
8103 gconstpointer value, gpointer data)
8105 GList *gtkwins, *gtkconvs;
8106 GtkPositionType pos;
8107 PidginConvWindow *gtkwin;
8109 pos = GPOINTER_TO_INT(value);
8111 for (gtkwins = pidgin_conv_windows_get_list(); gtkwins != NULL; gtkwins = gtkwins->next) {
8112 gtkwin = gtkwins->data;
8113 gtk_notebook_set_tab_pos(GTK_NOTEBOOK(gtkwin->notebook), pos&~8);
8114 for (gtkconvs = gtkwin->gtkconvs; gtkconvs != NULL; gtkconvs = gtkconvs->next) {
8115 pidgin_conv_tab_pack(gtkwin, gtkconvs->data);
8120 static void
8121 show_formatting_toolbar_pref_cb(const char *name, PurplePrefType type,
8122 gconstpointer value, gpointer data)
8124 GList *l;
8125 PurpleConversation *conv;
8126 PidginConversation *gtkconv;
8127 PidginConvWindow *win;
8129 for (l = purple_conversations_get_all(); l != NULL; l = l->next)
8131 conv = (PurpleConversation *)l->data;
8133 if (!PIDGIN_IS_PIDGIN_CONVERSATION(conv))
8134 continue;
8136 gtkconv = PIDGIN_CONVERSATION(conv);
8137 win = gtkconv->win;
8139 gtk_toggle_action_set_active(
8140 GTK_TOGGLE_ACTION(win->menu->show_formatting_toolbar),
8141 (gboolean)GPOINTER_TO_INT(value));
8143 if ((gboolean)GPOINTER_TO_INT(value))
8144 pidgin_webview_show_toolbar(PIDGIN_WEBVIEW(gtkconv->entry));
8145 else
8146 pidgin_webview_hide_toolbar(PIDGIN_WEBVIEW(gtkconv->entry));
8148 resize_webview_cb(gtkconv);
8152 static void
8153 animate_buddy_icons_pref_cb(const char *name, PurplePrefType type,
8154 gconstpointer value, gpointer data)
8156 GList *l;
8157 PurpleConversation *conv;
8158 PidginConversation *gtkconv;
8159 PidginConvWindow *win;
8161 if (!purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons"))
8162 return;
8164 /* Set the "animate" flag for each icon based on the new preference */
8165 for (l = purple_conversations_get_ims(); l != NULL; l = l->next) {
8166 conv = (PurpleConversation *)l->data;
8167 gtkconv = PIDGIN_CONVERSATION(conv);
8168 if (gtkconv)
8169 gtkconv->u.im->animate = GPOINTER_TO_INT(value);
8172 /* Now either stop or start animation for the active conversation in each window */
8173 for (l = pidgin_conv_windows_get_list(); l != NULL; l = l->next) {
8174 win = l->data;
8175 conv = pidgin_conv_window_get_active_conversation(win);
8176 pidgin_conv_update_buddy_icon(PURPLE_IM_CONVERSATION(conv));
8180 static void
8181 show_buddy_icons_pref_cb(const char *name, PurplePrefType type,
8182 gconstpointer value, gpointer data)
8184 GList *l;
8186 for (l = purple_conversations_get_all(); l != NULL; l = l->next) {
8187 PurpleConversation *conv = l->data;
8188 if (!PIDGIN_CONVERSATION(conv))
8189 continue;
8190 if (GPOINTER_TO_INT(value))
8191 gtk_widget_show(PIDGIN_CONVERSATION(conv)->infopane_hbox);
8192 else
8193 gtk_widget_hide(PIDGIN_CONVERSATION(conv)->infopane_hbox);
8195 if (PURPLE_IS_IM_CONVERSATION(conv)) {
8196 pidgin_conv_update_buddy_icon(PURPLE_IM_CONVERSATION(conv));
8200 /* Make the tabs show/hide correctly */
8201 for (l = pidgin_conv_windows_get_list(); l != NULL; l = l->next) {
8202 PidginConvWindow *win = l->data;
8203 if (pidgin_conv_window_get_gtkconv_count(win) == 1)
8204 gtk_notebook_set_show_tabs(GTK_NOTEBOOK(win->notebook),
8205 GPOINTER_TO_INT(value) == 0);
8209 static void
8210 show_protocol_icons_pref_cb(const char *name, PurplePrefType type,
8211 gconstpointer value, gpointer data)
8213 GList *l;
8214 for (l = purple_conversations_get_all(); l != NULL; l = l->next) {
8215 PurpleConversation *conv = l->data;
8216 if (PIDGIN_CONVERSATION(conv))
8217 update_tab_icon(conv);
8221 static void
8222 conv_placement_usetabs_cb(const char *name, PurplePrefType type,
8223 gconstpointer value, gpointer data)
8225 purple_prefs_trigger_callback(PIDGIN_PREFS_ROOT "/conversations/placement");
8228 static void
8229 account_status_changed_cb(PurpleAccount *account, PurpleStatus *oldstatus,
8230 PurpleStatus *newstatus)
8232 GList *l;
8233 PurpleConversation *conv = NULL;
8234 PidginConversation *gtkconv;
8236 if(strcmp(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new"), "away")!=0)
8237 return;
8239 if(purple_status_is_available(oldstatus) || !purple_status_is_available(newstatus))
8240 return;
8242 for (l = hidden_convwin->gtkconvs; l; ) {
8243 gtkconv = l->data;
8244 l = l->next;
8246 conv = gtkconv->active_conv;
8247 if (PURPLE_IS_CHAT_CONVERSATION(conv) ||
8248 account != purple_conversation_get_account(conv))
8249 continue;
8251 pidgin_conv_attach_to_conversation(conv);
8253 /* TODO: do we need to do anything for any other conversations that are in the same gtkconv here?
8254 * I'm a little concerned that not doing so will cause the "pending" indicator in the gtkblist not to be cleared. -DAA*/
8255 purple_conversation_update(conv, PURPLE_CONVERSATION_UPDATE_UNSEEN);
8259 static void
8260 hide_new_pref_cb(const char *name, PurplePrefType type,
8261 gconstpointer value, gpointer data)
8263 GList *l;
8264 PurpleConversation *conv = NULL;
8265 PidginConversation *gtkconv;
8266 gboolean when_away = FALSE;
8268 if(!hidden_convwin)
8269 return;
8271 if(strcmp(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new"), "always")==0)
8272 return;
8274 if(strcmp(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new"), "away")==0)
8275 when_away = TRUE;
8277 for (l = hidden_convwin->gtkconvs; l; )
8279 gtkconv = l->data;
8280 l = l->next;
8282 conv = gtkconv->active_conv;
8284 if (PURPLE_IS_CHAT_CONVERSATION(conv) ||
8285 gtkconv->unseen_count == 0 ||
8286 (when_away && !purple_status_is_available(
8287 purple_account_get_active_status(
8288 purple_conversation_get_account(conv)))))
8289 continue;
8291 pidgin_conv_attach_to_conversation(conv);
8296 static void
8297 conv_placement_pref_cb(const char *name, PurplePrefType type,
8298 gconstpointer value, gpointer data)
8300 PidginConvPlacementFunc func;
8302 if (strcmp(name, PIDGIN_PREFS_ROOT "/conversations/placement"))
8303 return;
8305 func = pidgin_conv_placement_get_fnc(value);
8307 if (func == NULL)
8308 return;
8310 pidgin_conv_placement_set_current_func(func);
8313 static PidginConversation *
8314 get_gtkconv_with_contact(PurpleContact *contact)
8316 PurpleBlistNode *node;
8318 node = ((PurpleBlistNode*)contact)->child;
8320 for (; node; node = node->next)
8322 PurpleBuddy *buddy = (PurpleBuddy*)node;
8323 PurpleIMConversation *im;
8324 im = purple_conversations_find_im_with_account(purple_buddy_get_name(buddy), purple_buddy_get_account(buddy));
8325 if (im)
8326 return PIDGIN_CONVERSATION(PURPLE_CONVERSATION(im));
8328 return NULL;
8331 static void
8332 account_signed_off_cb(PurpleConnection *gc, gpointer event)
8334 GList *iter;
8336 for (iter = purple_conversations_get_all(); iter; iter = iter->next)
8338 PurpleConversation *conv = iter->data;
8340 /* This seems fine in theory, but we also need to cover the
8341 * case of this account matching one of the other buddies in
8342 * one of the contacts containing the buddy corresponding to
8343 * a conversation. It's easier to just update them all. */
8344 /* if (purple_conversation_get_account(conv) == account) */
8345 pidgin_conv_update_fields(conv, PIDGIN_CONV_TAB_ICON |
8346 PIDGIN_CONV_MENU | PIDGIN_CONV_COLORIZE_TITLE);
8348 if (PURPLE_CONNECTION_IS_CONNECTED(gc) &&
8349 PURPLE_IS_CHAT_CONVERSATION(conv) &&
8350 purple_conversation_get_account(conv) == purple_connection_get_account(gc) &&
8351 g_object_get_data(G_OBJECT(conv), "want-to-rejoin")) {
8352 GHashTable *comps = NULL;
8353 PurpleChat *chat = purple_blist_find_chat(purple_conversation_get_account(conv), purple_conversation_get_name(conv));
8354 if (chat == NULL) {
8355 PurpleProtocol *protocol = purple_connection_get_protocol(gc);
8356 comps = purple_protocol_chat_iface_info_defaults(protocol, gc, purple_conversation_get_name(conv));
8357 } else {
8358 comps = purple_chat_get_components(chat);
8360 purple_serv_join_chat(gc, comps);
8361 if (chat == NULL && comps != NULL)
8362 g_hash_table_destroy(comps);
8367 static void
8368 account_signing_off(PurpleConnection *gc)
8370 GList *list = purple_conversations_get_chats();
8371 PurpleAccount *account = purple_connection_get_account(gc);
8373 /* We are about to sign off. See which chats we are currently in, and mark
8374 * them for rejoin on reconnect. */
8375 while (list) {
8376 PurpleConversation *conv = list->data;
8377 if (!purple_chat_conversation_has_left(PURPLE_CHAT_CONVERSATION(conv)) &&
8378 purple_conversation_get_account(conv) == account) {
8379 g_object_set_data(G_OBJECT(conv), "want-to-rejoin", GINT_TO_POINTER(TRUE));
8380 purple_conversation_write_system_message(conv,
8381 _("The account has disconnected and you are no "
8382 "longer in this chat. You will automatically "
8383 "rejoin the chat when the account reconnects."),
8384 PURPLE_MESSAGE_NO_LOG);
8386 list = list->next;
8390 static void
8391 update_buddy_status_changed(PurpleBuddy *buddy, PurpleStatus *old, PurpleStatus *newstatus)
8393 PidginConversation *gtkconv;
8394 PurpleConversation *conv;
8396 gtkconv = get_gtkconv_with_contact(purple_buddy_get_contact(buddy));
8397 if (gtkconv)
8399 conv = gtkconv->active_conv;
8400 pidgin_conv_update_fields(conv, PIDGIN_CONV_TAB_ICON
8401 | PIDGIN_CONV_COLORIZE_TITLE
8402 | PIDGIN_CONV_BUDDY_ICON);
8403 if ((purple_status_is_online(old) ^ purple_status_is_online(newstatus)) != 0)
8404 pidgin_conv_update_fields(conv, PIDGIN_CONV_MENU);
8408 static void
8409 update_buddy_privacy_changed(PurpleBuddy *buddy)
8411 PidginConversation *gtkconv;
8412 PurpleConversation *conv;
8414 gtkconv = get_gtkconv_with_contact(purple_buddy_get_contact(buddy));
8415 if (gtkconv) {
8416 conv = gtkconv->active_conv;
8417 pidgin_conv_update_fields(conv, PIDGIN_CONV_TAB_ICON | PIDGIN_CONV_MENU);
8421 static void
8422 update_buddy_idle_changed(PurpleBuddy *buddy, gboolean old, gboolean newidle)
8424 PurpleIMConversation *im;
8426 im = purple_conversations_find_im_with_account(purple_buddy_get_name(buddy), purple_buddy_get_account(buddy));
8427 if (im)
8428 pidgin_conv_update_fields(PURPLE_CONVERSATION(im), PIDGIN_CONV_TAB_ICON);
8431 static void
8432 update_buddy_icon(PurpleBuddy *buddy)
8434 PurpleIMConversation *im;
8436 im = purple_conversations_find_im_with_account(purple_buddy_get_name(buddy), purple_buddy_get_account(buddy));
8437 if (im)
8438 pidgin_conv_update_fields(PURPLE_CONVERSATION(im), PIDGIN_CONV_BUDDY_ICON);
8441 static void
8442 update_buddy_sign(PurpleBuddy *buddy, const char *which)
8444 PurplePresence *presence;
8445 PurpleStatus *on, *off;
8447 presence = purple_buddy_get_presence(buddy);
8448 if (!presence)
8449 return;
8450 off = purple_presence_get_status(presence, "offline");
8451 on = purple_presence_get_status(presence, "available");
8453 if (*(which+1) == 'f')
8454 update_buddy_status_changed(buddy, on, off);
8455 else
8456 update_buddy_status_changed(buddy, off, on);
8459 static void
8460 update_conversation_switched(PurpleConversation *conv)
8462 pidgin_conv_update_fields(conv, PIDGIN_CONV_TAB_ICON |
8463 PIDGIN_CONV_SET_TITLE | PIDGIN_CONV_MENU |
8464 PIDGIN_CONV_BUDDY_ICON | PIDGIN_CONV_E2EE );
8467 static void
8468 update_buddy_typing(PurpleAccount *account, const char *who)
8470 PurpleConversation *conv;
8471 PidginConversation *gtkconv;
8473 conv = PURPLE_CONVERSATION(purple_conversations_find_im_with_account(who, account));
8474 if (!conv)
8475 return;
8477 gtkconv = PIDGIN_CONVERSATION(conv);
8478 if (gtkconv && gtkconv->active_conv == conv)
8479 pidgin_conv_update_fields(conv, PIDGIN_CONV_COLORIZE_TITLE);
8482 static void
8483 update_chat(PurpleChatConversation *chat)
8485 pidgin_conv_update_fields(PURPLE_CONVERSATION(chat), PIDGIN_CONV_TOPIC |
8486 PIDGIN_CONV_MENU | PIDGIN_CONV_SET_TITLE);
8489 static void
8490 update_chat_topic(PurpleChatConversation *chat, const char *old, const char *new)
8492 pidgin_conv_update_fields(PURPLE_CONVERSATION(chat), PIDGIN_CONV_TOPIC);
8495 /* Message history stuff */
8497 /* Compare two PurpleMessages, according to time in ascending order. */
8498 static int
8499 message_compare(gconstpointer p1, gconstpointer p2)
8501 const PurpleMessage *m1 = p1, *m2 = p2;
8502 return (purple_message_get_time(m1) > purple_message_get_time(m2));
8505 /* Adds some message history to the gtkconv. This happens in a idle-callback. */
8506 static gboolean
8507 add_message_history_to_gtkconv(gpointer data)
8509 PidginConversation *gtkconv = data;
8510 PidginWebView *webview = PIDGIN_WEBVIEW(gtkconv->webview);
8511 int count = 0;
8512 int timer = gtkconv->attach_timer;
8513 time_t when = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(gtkconv->entry), "attach-start-time"));
8514 gboolean im = (PURPLE_IS_IM_CONVERSATION(gtkconv->active_conv));
8516 gtkconv->attach_timer = 0;
8517 while (gtkconv->attach_current && count < ADD_MESSAGE_HISTORY_AT_ONCE) {
8518 PurpleMessage *msg = gtkconv->attach_current->data;
8519 if (!im && when && (guint64)when < purple_message_get_time(msg)) {
8520 pidgin_webview_append_html(webview, "<BR><HR>");
8521 pidgin_webview_scroll_to_end(webview, TRUE);
8522 g_object_set_data(G_OBJECT(gtkconv->entry), "attach-start-time", NULL);
8524 /* XXX: should it be gtkconv->active_conv? */
8525 pidgin_conv_write_conv(gtkconv->active_conv, msg);
8526 if (im) {
8527 gtkconv->attach_current = g_list_delete_link(gtkconv->attach_current, gtkconv->attach_current);
8528 } else {
8529 gtkconv->attach_current = gtkconv->attach_current->prev;
8531 count++;
8533 gtkconv->attach_timer = timer;
8534 if (gtkconv->attach_current)
8535 return TRUE;
8537 g_source_remove(gtkconv->attach_timer);
8538 gtkconv->attach_timer = 0;
8539 if (im) {
8540 /* Print any message that was sent while the old history was being added back. */
8541 GList *msgs = NULL;
8542 GList *iter = gtkconv->convs;
8543 for (; iter; iter = iter->next) {
8544 PurpleConversation *conv = iter->data;
8545 GList *history = purple_conversation_get_message_history(conv);
8546 for (; history; history = history->next) {
8547 PurpleMessage *msg = history->data;
8548 if (purple_message_get_time(msg) > (guint64)when)
8549 msgs = g_list_prepend(msgs, msg);
8552 msgs = g_list_sort(msgs, message_compare);
8553 for (; msgs; msgs = g_list_delete_link(msgs, msgs)) {
8554 PurpleMessage *msg = msgs->data;
8555 /* XXX: see above - should it be active_conv? */
8556 pidgin_conv_write_conv(gtkconv->active_conv, msg);
8558 pidgin_webview_append_html(webview, "<BR><HR>");
8559 pidgin_webview_scroll_to_end(webview, TRUE);
8560 g_object_set_data(G_OBJECT(gtkconv->entry), "attach-start-time", NULL);
8563 g_object_set_data(G_OBJECT(gtkconv->entry), "attach-start-time", NULL);
8564 purple_signal_emit(pidgin_conversations_get_handle(),
8565 "conversation-displayed", gtkconv);
8566 return FALSE;
8569 static void
8570 pidgin_conv_attach(PurpleConversation *conv)
8572 int timer;
8573 g_object_set_data(G_OBJECT(conv), "unseen-count", NULL);
8574 g_object_set_data(G_OBJECT(conv), "unseen-state", NULL);
8575 purple_conversation_set_ui_ops(conv, pidgin_conversations_get_conv_ui_ops());
8576 if (!PIDGIN_CONVERSATION(conv))
8577 private_gtkconv_new(conv, FALSE);
8578 timer = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(conv), "close-timer"));
8579 if (timer) {
8580 purple_timeout_remove(timer);
8581 g_object_set_data(G_OBJECT(conv), "close-timer", NULL);
8585 gboolean pidgin_conv_attach_to_conversation(PurpleConversation *conv)
8587 GList *list;
8588 PidginConversation *gtkconv;
8590 if (PIDGIN_IS_PIDGIN_CONVERSATION(conv)) {
8591 /* This is pretty much always the case now. */
8592 gtkconv = PIDGIN_CONVERSATION(conv);
8593 if (gtkconv->win != hidden_convwin)
8594 return FALSE;
8595 pidgin_conv_window_remove_gtkconv(hidden_convwin, gtkconv);
8596 pidgin_conv_placement_place(gtkconv);
8597 purple_signal_emit(pidgin_conversations_get_handle(),
8598 "conversation-displayed", gtkconv);
8599 list = gtkconv->convs;
8600 while (list) {
8601 pidgin_conv_attach(list->data);
8602 list = list->next;
8604 return TRUE;
8607 pidgin_conv_attach(conv);
8608 gtkconv = PIDGIN_CONVERSATION(conv);
8610 list = purple_conversation_get_message_history(conv);
8611 if (list) {
8612 if (PURPLE_IS_IM_CONVERSATION(conv)) {
8613 GList *convs;
8614 list = g_list_copy(list);
8615 for (convs = purple_conversations_get_ims(); convs; convs = convs->next)
8616 if (convs->data != conv &&
8617 pidgin_conv_find_gtkconv(convs->data) == gtkconv) {
8618 pidgin_conv_attach(convs->data);
8619 list = g_list_concat(list, g_list_copy(purple_conversation_get_message_history(convs->data)));
8621 list = g_list_sort(list, message_compare);
8622 gtkconv->attach_current = list;
8623 list = g_list_last(list);
8624 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
8625 gtkconv->attach_current = g_list_last(list);
8628 g_object_set_data(G_OBJECT(gtkconv->entry), "attach-start-time",
8629 GINT_TO_POINTER(purple_message_get_time(list->data)));
8630 gtkconv->attach_timer = g_idle_add(add_message_history_to_gtkconv, gtkconv);
8631 } else {
8632 purple_signal_emit(pidgin_conversations_get_handle(),
8633 "conversation-displayed", gtkconv);
8636 if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
8637 GList *users;
8638 PurpleChatConversation *chat = PURPLE_CHAT_CONVERSATION(conv);
8639 pidgin_conv_update_fields(conv, PIDGIN_CONV_TOPIC);
8640 users = purple_chat_conversation_get_users(chat);
8641 pidgin_conv_chat_add_users(chat, users, TRUE);
8642 g_list_free(users);
8645 return TRUE;
8648 PurpleTheme *
8649 pidgin_conversations_get_default_theme(void)
8651 return default_conv_theme;
8654 void *
8655 pidgin_conversations_get_handle(void)
8657 static int handle;
8659 return &handle;
8662 static void
8663 pidgin_conversations_pre_uninit(void);
8665 void
8666 pidgin_conversations_init(void)
8668 void *handle = pidgin_conversations_get_handle();
8669 void *blist_handle = purple_blist_get_handle();
8670 char *theme_dir;
8672 e2ee_stock = g_hash_table_new_full(g_str_hash, g_str_equal,
8673 g_free, g_object_unref);
8675 image_store_tag_re = g_regex_new("(<img [^>]*src=\")("
8676 PURPLE_IMAGE_STORE_PROTOCOL "[0-9]+)(\"[^>]*>)",
8677 G_REGEX_OPTIMIZE | G_REGEX_DOTALL, 0, NULL);
8679 /* Conversations */
8680 purple_prefs_add_none(PIDGIN_PREFS_ROOT "/conversations");
8681 purple_prefs_add_none(PIDGIN_PREFS_ROOT "/conversations/themes");
8682 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/use_smooth_scrolling", TRUE);
8683 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/close_on_tabs", TRUE);
8684 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/send_bold", FALSE);
8685 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/send_italic", FALSE);
8686 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/send_underline", FALSE);
8687 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/send_strike", FALSE);
8688 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/spellcheck", TRUE);
8689 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/show_incoming_formatting", TRUE);
8690 /* TODO: it's about *remote* smileys, not local ones */
8691 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/resize_custom_smileys", TRUE);
8692 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/custom_smileys_size", 96);
8693 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/minimum_entry_lines", 2);
8695 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/show_formatting_toolbar", TRUE);
8697 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/placement", "last");
8698 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/placement_number", 1);
8699 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/bgcolor", "");
8700 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/fgcolor", "");
8701 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/font_face", "");
8702 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/font_size", 3);
8703 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/tabs", TRUE);
8704 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/tab_side", GTK_POS_TOP);
8705 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/scrollback_lines", 4000);
8707 #ifdef _WIN32
8708 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/use_theme_font", TRUE);
8709 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/custom_font", "");
8710 #endif
8712 /* Conversations -> Chat */
8713 purple_prefs_add_none(PIDGIN_PREFS_ROOT "/conversations/chat");
8714 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/entry_height", 54);
8715 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/userlist_width", 80);
8716 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/x", 0);
8717 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/y", 0);
8718 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/width", 340);
8719 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/height", 390);
8721 /* Conversations -> IM */
8722 purple_prefs_add_none(PIDGIN_PREFS_ROOT "/conversations/im");
8723 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/im/x", 0);
8724 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/im/y", 0);
8725 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/im/width", 340);
8726 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/im/height", 390);
8728 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/im/animate_buddy_icons", TRUE);
8730 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/im/entry_height", 54);
8731 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons", TRUE);
8733 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new", "never");
8734 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/im/close_immediately", TRUE);
8736 #ifdef _WIN32
8737 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/win32/minimize_new_convs", FALSE);
8738 #endif
8740 /* Connect callbacks. */
8741 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/close_on_tabs",
8742 close_on_tabs_pref_cb, NULL);
8743 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/show_formatting_toolbar",
8744 show_formatting_toolbar_pref_cb, NULL);
8745 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/spellcheck",
8746 spellcheck_pref_cb, NULL);
8747 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/tab_side",
8748 tab_side_pref_cb, NULL);
8750 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/tabs",
8751 conv_placement_usetabs_cb, NULL);
8753 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/placement",
8754 conv_placement_pref_cb, NULL);
8755 purple_prefs_trigger_callback(PIDGIN_PREFS_ROOT "/conversations/placement");
8757 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/minimum_entry_lines",
8758 minimum_entry_lines_pref_cb, NULL);
8760 /* IM callbacks */
8761 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/im/animate_buddy_icons",
8762 animate_buddy_icons_pref_cb, NULL);
8763 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons",
8764 show_buddy_icons_pref_cb, NULL);
8765 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/blist/show_protocol_icons",
8766 show_protocol_icons_pref_cb, NULL);
8767 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/im/hide_new",
8768 hide_new_pref_cb, NULL);
8770 /**********************************************************************
8771 * Register signals
8772 **********************************************************************/
8773 purple_signal_register(handle, "conversation-dragging",
8774 purple_marshal_VOID__POINTER_POINTER, G_TYPE_NONE, 2,
8775 G_TYPE_POINTER, /* pointer to a (PidginConvWindow *) */
8776 G_TYPE_POINTER); /* pointer to a (PidginConvWindow *) */
8778 purple_signal_register(handle, "conversation-timestamp",
8779 #if SIZEOF_TIME_T == 4
8780 purple_marshal_POINTER__POINTER_INT_BOOLEAN,
8781 #elif SIZEOF_TIME_T == 8
8782 purple_marshal_POINTER__POINTER_INT64_BOOLEAN,
8783 #else
8784 #error Unkown size of time_t
8785 #endif
8786 G_TYPE_STRING, 3, PURPLE_TYPE_CONVERSATION,
8787 #if SIZEOF_TIME_T == 4
8788 G_TYPE_INT,
8789 #elif SIZEOF_TIME_T == 8
8790 G_TYPE_INT64,
8791 #else
8792 # error Unknown size of time_t
8793 #endif
8794 G_TYPE_BOOLEAN);
8796 purple_signal_register(handle, "displaying-im-msg",
8797 purple_marshal_BOOLEAN__POINTER_POINTER,
8798 G_TYPE_BOOLEAN, 2, PURPLE_TYPE_CONVERSATION, PURPLE_TYPE_MESSAGE);
8800 purple_signal_register(handle, "displayed-im-msg",
8801 purple_marshal_VOID__POINTER_POINTER, G_TYPE_NONE, 2,
8802 PURPLE_TYPE_CONVERSATION, PURPLE_TYPE_MESSAGE);
8804 purple_signal_register(handle, "displaying-chat-msg",
8805 purple_marshal_BOOLEAN__POINTER_POINTER,
8806 G_TYPE_BOOLEAN, 2, PURPLE_TYPE_CONVERSATION, PURPLE_TYPE_MESSAGE);
8808 purple_signal_register(handle, "displayed-chat-msg",
8809 purple_marshal_VOID__POINTER_POINTER, G_TYPE_NONE, 2,
8810 PURPLE_TYPE_CONVERSATION, PURPLE_TYPE_MESSAGE);
8812 purple_signal_register(handle, "conversation-switched",
8813 purple_marshal_VOID__POINTER, G_TYPE_NONE, 1,
8814 PURPLE_TYPE_CONVERSATION);
8816 purple_signal_register(handle, "conversation-hiding",
8817 purple_marshal_VOID__POINTER, G_TYPE_NONE, 1,
8818 G_TYPE_POINTER); /* (PidginConversation *) */
8820 purple_signal_register(handle, "conversation-displayed",
8821 purple_marshal_VOID__POINTER, G_TYPE_NONE, 1,
8822 G_TYPE_POINTER); /* (PidginConversation *) */
8824 purple_signal_register(handle, "chat-nick-autocomplete",
8825 purple_marshal_BOOLEAN__POINTER_BOOLEAN,
8826 G_TYPE_BOOLEAN, 1, PURPLE_TYPE_CONVERSATION);
8828 purple_signal_register(handle, "chat-nick-clicked",
8829 purple_marshal_BOOLEAN__POINTER_POINTER_UINT,
8830 G_TYPE_BOOLEAN, 3, PURPLE_TYPE_CONVERSATION,
8831 G_TYPE_STRING, G_TYPE_UINT);
8833 purple_signal_register(handle, "conversation-window-created",
8834 purple_marshal_VOID__POINTER, G_TYPE_NONE, 1,
8835 G_TYPE_POINTER); /* (PidginConvWindow *) */
8838 /**********************************************************************
8839 * Register commands
8840 **********************************************************************/
8841 purple_cmd_register("say", "S", PURPLE_CMD_P_DEFAULT,
8842 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM, NULL,
8843 say_command_cb, _("say &lt;message&gt;: Send a message normally as if you weren't using a command."), NULL);
8844 purple_cmd_register("me", "S", PURPLE_CMD_P_DEFAULT,
8845 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM, NULL,
8846 me_command_cb, _("me &lt;action&gt;: Send an IRC style action to a buddy or chat."), NULL);
8847 purple_cmd_register("debug", "w", PURPLE_CMD_P_DEFAULT,
8848 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM, NULL,
8849 debug_command_cb, _("debug &lt;option&gt;: Send various debug information to the current conversation."), NULL);
8850 purple_cmd_register("clear", "", PURPLE_CMD_P_DEFAULT,
8851 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM, NULL,
8852 clear_command_cb, _("clear: Clears the conversation scrollback."), NULL);
8853 purple_cmd_register("clearall", "", PURPLE_CMD_P_DEFAULT,
8854 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM, NULL,
8855 clearall_command_cb, _("clear: Clears all conversation scrollbacks."), NULL);
8856 purple_cmd_register("help", "w", PURPLE_CMD_P_DEFAULT,
8857 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM | PURPLE_CMD_FLAG_ALLOW_WRONG_ARGS, NULL,
8858 help_command_cb, _("help &lt;command&gt;: Help on a specific command."), NULL);
8860 /**********************************************************************
8861 * UI operations
8862 **********************************************************************/
8864 purple_signal_connect(purple_connections_get_handle(), "signed-on", handle,
8865 G_CALLBACK(account_signed_off_cb),
8866 GINT_TO_POINTER(PURPLE_CONVERSATION_ACCOUNT_ONLINE));
8867 purple_signal_connect(purple_connections_get_handle(), "signed-off", handle,
8868 G_CALLBACK(account_signed_off_cb),
8869 GINT_TO_POINTER(PURPLE_CONVERSATION_ACCOUNT_OFFLINE));
8870 purple_signal_connect(purple_connections_get_handle(), "signing-off", handle,
8871 G_CALLBACK(account_signing_off), NULL);
8873 purple_signal_connect(purple_conversations_get_handle(), "writing-im-msg",
8874 handle, G_CALLBACK(writing_msg), NULL);
8875 purple_signal_connect(purple_conversations_get_handle(), "writing-chat-msg",
8876 handle, G_CALLBACK(writing_msg), NULL);
8877 purple_signal_connect(purple_conversations_get_handle(), "received-im-msg",
8878 handle, G_CALLBACK(received_im_msg_cb), NULL);
8879 purple_signal_connect(purple_conversations_get_handle(), "cleared-message-history",
8880 handle, G_CALLBACK(clear_conversation_scrollback_cb), NULL);
8882 purple_signal_connect(purple_conversations_get_handle(), "deleting-chat-user",
8883 handle, G_CALLBACK(deleting_chat_user_cb), NULL);
8885 purple_conversations_set_ui_ops(&conversation_ui_ops);
8887 hidden_convwin = pidgin_conv_window_new();
8888 window_list = g_list_remove(window_list, hidden_convwin);
8890 purple_signal_connect(purple_accounts_get_handle(), "account-status-changed",
8891 handle, PURPLE_CALLBACK(account_status_changed_cb), NULL);
8893 purple_signal_connect_priority(purple_get_core(), "quitting", handle,
8894 PURPLE_CALLBACK(pidgin_conversations_pre_uninit), NULL, PURPLE_SIGNAL_PRIORITY_HIGHEST);
8896 /* Callbacks to update a conversation */
8897 purple_signal_connect(blist_handle, "blist-node-added", handle,
8898 G_CALLBACK(buddy_update_cb), NULL);
8899 purple_signal_connect(blist_handle, "blist-node-removed", handle,
8900 G_CALLBACK(buddy_update_cb), NULL);
8901 purple_signal_connect(blist_handle, "buddy-signed-on",
8902 handle, PURPLE_CALLBACK(update_buddy_sign), "on");
8903 purple_signal_connect(blist_handle, "buddy-signed-off",
8904 handle, PURPLE_CALLBACK(update_buddy_sign), "off");
8905 purple_signal_connect(blist_handle, "buddy-status-changed",
8906 handle, PURPLE_CALLBACK(update_buddy_status_changed), NULL);
8907 purple_signal_connect(blist_handle, "buddy-privacy-changed",
8908 handle, PURPLE_CALLBACK(update_buddy_privacy_changed), NULL);
8909 purple_signal_connect(blist_handle, "buddy-idle-changed",
8910 handle, PURPLE_CALLBACK(update_buddy_idle_changed), NULL);
8911 purple_signal_connect(blist_handle, "buddy-icon-changed",
8912 handle, PURPLE_CALLBACK(update_buddy_icon), NULL);
8913 purple_signal_connect(purple_conversations_get_handle(), "buddy-typing",
8914 handle, PURPLE_CALLBACK(update_buddy_typing), NULL);
8915 purple_signal_connect(purple_conversations_get_handle(), "buddy-typing-stopped",
8916 handle, PURPLE_CALLBACK(update_buddy_typing), NULL);
8917 purple_signal_connect(pidgin_conversations_get_handle(), "conversation-switched",
8918 handle, PURPLE_CALLBACK(update_conversation_switched), NULL);
8919 purple_signal_connect(purple_conversations_get_handle(), "chat-left", handle,
8920 PURPLE_CALLBACK(update_chat), NULL);
8921 purple_signal_connect(purple_conversations_get_handle(), "chat-joined", handle,
8922 PURPLE_CALLBACK(update_chat), NULL);
8923 purple_signal_connect(purple_conversations_get_handle(), "chat-topic-changed", handle,
8924 PURPLE_CALLBACK(update_chat_topic), NULL);
8925 purple_signal_connect_priority(purple_conversations_get_handle(), "conversation-updated", handle,
8926 PURPLE_CALLBACK(pidgin_conv_updated), NULL,
8927 PURPLE_SIGNAL_PRIORITY_LOWEST);
8928 purple_signal_connect(purple_conversations_get_handle(), "wrote-im-msg", handle,
8929 PURPLE_CALLBACK(wrote_msg_update_unseen_cb), NULL);
8930 purple_signal_connect(purple_conversations_get_handle(), "wrote-chat-msg", handle,
8931 PURPLE_CALLBACK(wrote_msg_update_unseen_cb), NULL);
8933 purple_theme_manager_register_type(g_object_new(PIDGIN_TYPE_CONV_THEME_LOADER, "type", "conversation", NULL));
8934 #if defined(_WIN32) && !defined(USE_WIN32_FHS)
8935 theme_dir = g_build_filename(PURPLE_DATADIR, "theme", NULL);
8936 #else
8937 theme_dir = g_build_filename(PURPLE_DATADIR, "pidgin", "theme", NULL);
8938 #endif
8939 default_conv_theme = purple_theme_manager_load_theme(theme_dir, "conversation");
8940 g_free(theme_dir);
8944 static void
8945 pidgin_conversations_pre_uninit(void)
8947 g_hash_table_destroy(e2ee_stock);
8948 e2ee_stock = NULL;
8951 void
8952 pidgin_conversations_uninit(void)
8954 purple_prefs_disconnect_by_handle(pidgin_conversations_get_handle());
8955 purple_signals_disconnect_by_handle(pidgin_conversations_get_handle());
8956 purple_signals_unregister_by_instance(pidgin_conversations_get_handle());
8958 g_regex_unref(image_store_tag_re);
8959 image_store_tag_re = NULL;
8962 /**************************************************************************
8963 * PidginConversation GBoxed code
8964 **************************************************************************/
8965 static PidginConversation *
8966 pidgin_conversation_ref(PidginConversation *gtkconv)
8968 g_return_val_if_fail(gtkconv != NULL, NULL);
8970 gtkconv->box_count++;
8972 return gtkconv;
8975 static void
8976 pidgin_conversation_unref(PidginConversation *gtkconv)
8978 g_return_if_fail(gtkconv != NULL);
8979 g_return_if_fail(gtkconv->box_count >= 0);
8981 if (!gtkconv->box_count--)
8982 pidgin_conv_destroy(gtkconv->active_conv);
8985 GType
8986 pidgin_conversation_get_type(void)
8988 static GType type = 0;
8990 if (type == 0) {
8991 type = g_boxed_type_register_static("PidginConversation",
8992 (GBoxedCopyFunc)pidgin_conversation_ref,
8993 (GBoxedFreeFunc)pidgin_conversation_unref);
8996 return type;
9014 /* down here is where gtkconvwin.c ought to start. except they share like every freaking function,
9015 * and touch each others' private members all day long */
9017 /* pidgin
9019 * Pidgin is the legal property of its developers, whose names are too numerous
9020 * to list here. Please refer to the COPYRIGHT file distributed with this
9021 * source distribution.
9023 * This program is free software; you can redistribute it and/or modify
9024 * it under the terms of the GNU General Public License as published by
9025 * the Free Software Foundation; either version 2 of the License, or
9026 * (at your option) any later version.
9028 * This program is distributed in the hope that it will be useful,
9029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
9030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9031 * GNU General Public License for more details.
9033 * You should have received a copy of the GNU General Public License
9034 * along with this program; if not, write to the Free Software
9035 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
9038 #include "internal.h"
9039 #include "pidgin.h"
9042 #include <gdk/gdkkeysyms.h>
9044 #include "account.h"
9045 #include "cmds.h"
9046 #include "debug.h"
9047 #include "log.h"
9048 #include "notify.h"
9049 #include "protocol.h"
9050 #include "request.h"
9051 #include "util.h"
9053 #include "gtkdnd-hints.h"
9054 #include "gtkblist.h"
9055 #include "gtkconv.h"
9056 #include "gtkdialogs.h"
9057 #include "gtkmenutray.h"
9058 #include "gtkpounce.h"
9059 #include "gtkprefs.h"
9060 #include "gtkprivacy.h"
9061 #include "gtkutils.h"
9062 #include "pidginstock.h"
9064 static void
9065 do_close(GtkWidget *w, int resp, PidginConvWindow *win)
9067 gtk_widget_destroy(warn_close_dialog);
9068 warn_close_dialog = NULL;
9070 if (resp == GTK_RESPONSE_OK)
9071 pidgin_conv_window_destroy(win);
9074 static void
9075 build_warn_close_dialog(PidginConvWindow *gtkwin)
9077 GtkWidget *label, *vbox, *hbox, *img;
9079 g_return_if_fail(warn_close_dialog == NULL);
9081 warn_close_dialog = gtk_dialog_new_with_buttons(_("Confirm close"),
9082 GTK_WINDOW(gtkwin->window), GTK_DIALOG_MODAL,
9083 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
9084 GTK_STOCK_CLOSE, GTK_RESPONSE_OK, NULL);
9086 gtk_dialog_set_default_response(GTK_DIALOG(warn_close_dialog),
9087 GTK_RESPONSE_OK);
9089 gtk_container_set_border_width(GTK_CONTAINER(warn_close_dialog),
9091 gtk_window_set_resizable(GTK_WINDOW(warn_close_dialog), FALSE);
9093 /* Setup the outside spacing. */
9094 vbox = gtk_dialog_get_content_area(GTK_DIALOG(warn_close_dialog));
9096 gtk_box_set_spacing(GTK_BOX(vbox), 12);
9097 gtk_container_set_border_width(GTK_CONTAINER(vbox), 6);
9099 img = gtk_image_new_from_stock(PIDGIN_STOCK_DIALOG_WARNING,
9100 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_HUGE));
9101 /* Setup the inner hbox and put the dialog's icon in it. */
9102 hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12);
9103 gtk_container_add(GTK_CONTAINER(vbox), hbox);
9104 gtk_box_pack_start(GTK_BOX(hbox), img, FALSE, FALSE, 0);
9105 gtk_widget_set_halign(img, GTK_ALIGN_START);
9106 gtk_widget_set_valign(img, GTK_ALIGN_START);
9108 /* Setup the right vbox. */
9109 vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
9110 gtk_container_add(GTK_CONTAINER(hbox), vbox);
9112 label = gtk_label_new(_("You have unread messages. Are you sure you want to close the window?"));
9113 gtk_widget_set_size_request(label, 350, -1);
9114 gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
9115 gtk_label_set_xalign(GTK_LABEL(label), 0);
9116 gtk_label_set_yalign(GTK_LABEL(label), 0);
9117 gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 0);
9119 /* Connect the signals. */
9120 g_signal_connect(G_OBJECT(warn_close_dialog), "response",
9121 G_CALLBACK(do_close), gtkwin);
9125 /**************************************************************************
9126 * Callbacks
9127 **************************************************************************/
9129 static gboolean
9130 close_win_cb(GtkWidget *w, GdkEventAny *e, gpointer d)
9132 PidginConvWindow *win = d;
9133 GList *l;
9135 /* If there are unread messages then show a warning dialog */
9136 for (l = pidgin_conv_window_get_gtkconvs(win);
9137 l != NULL; l = l->next)
9139 PidginConversation *gtkconv = l->data;
9140 if (PURPLE_IS_IM_CONVERSATION(gtkconv->active_conv) &&
9141 gtkconv->unseen_state >= PIDGIN_UNSEEN_TEXT)
9143 build_warn_close_dialog(win);
9144 gtk_widget_show_all(warn_close_dialog);
9146 return TRUE;
9150 pidgin_conv_window_destroy(win);
9152 return TRUE;
9155 static void
9156 conv_set_unseen(PurpleConversation *conv, PidginUnseenState state)
9158 int unseen_count = 0;
9159 PidginUnseenState unseen_state = PIDGIN_UNSEEN_NONE;
9161 if(g_object_get_data(G_OBJECT(conv), "unseen-count"))
9162 unseen_count = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(conv), "unseen-count"));
9164 if(g_object_get_data(G_OBJECT(conv), "unseen-state"))
9165 unseen_state = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(conv), "unseen-state"));
9167 if (state == PIDGIN_UNSEEN_NONE)
9169 unseen_count = 0;
9170 unseen_state = PIDGIN_UNSEEN_NONE;
9172 else
9174 if (state >= PIDGIN_UNSEEN_TEXT)
9175 unseen_count++;
9177 if (state > unseen_state)
9178 unseen_state = state;
9181 g_object_set_data(G_OBJECT(conv), "unseen-count", GINT_TO_POINTER(unseen_count));
9182 g_object_set_data(G_OBJECT(conv), "unseen-state", GINT_TO_POINTER(unseen_state));
9184 purple_conversation_update(conv, PURPLE_CONVERSATION_UPDATE_UNSEEN);
9187 static void
9188 gtkconv_set_unseen(PidginConversation *gtkconv, PidginUnseenState state)
9190 if (state == PIDGIN_UNSEEN_NONE)
9192 gtkconv->unseen_count = 0;
9193 gtkconv->unseen_state = PIDGIN_UNSEEN_NONE;
9195 else
9197 if (state >= PIDGIN_UNSEEN_TEXT)
9198 gtkconv->unseen_count++;
9200 if (state > gtkconv->unseen_state)
9201 gtkconv->unseen_state = state;
9204 g_object_set_data(G_OBJECT(gtkconv->active_conv), "unseen-count", GINT_TO_POINTER(gtkconv->unseen_count));
9205 g_object_set_data(G_OBJECT(gtkconv->active_conv), "unseen-state", GINT_TO_POINTER(gtkconv->unseen_state));
9207 purple_conversation_update(gtkconv->active_conv, PURPLE_CONVERSATION_UPDATE_UNSEEN);
9211 * When a conversation window is focused, we know the user
9212 * has looked at it so we know there are no longer unseen
9213 * messages.
9215 static gboolean
9216 focus_win_cb(GtkWidget *w, GdkEventFocus *e, gpointer d)
9218 PidginConvWindow *win = d;
9219 PidginConversation *gtkconv = pidgin_conv_window_get_active_gtkconv(win);
9221 if (gtkconv)
9222 gtkconv_set_unseen(gtkconv, PIDGIN_UNSEEN_NONE);
9224 return FALSE;
9227 static void
9228 notebook_init_grab(PidginConvWindow *gtkwin, GtkWidget *widget, GdkEvent *event)
9230 static GdkCursor *cursor = NULL;
9231 GdkDevice *device;
9233 gtkwin->in_drag = TRUE;
9235 if (gtkwin->drag_leave_signal) {
9236 g_signal_handler_disconnect(G_OBJECT(widget),
9237 gtkwin->drag_leave_signal);
9238 gtkwin->drag_leave_signal = 0;
9241 if (cursor == NULL) {
9242 GdkDisplay *display = gtk_widget_get_display(gtkwin->notebook);
9243 cursor = gdk_cursor_new_for_display(display, GDK_FLEUR);
9246 /* Grab the pointer */
9247 gtk_grab_add(gtkwin->notebook);
9248 device = gdk_event_get_device(event);
9249 if (!gdk_display_device_is_grabbed(gdk_device_get_display(device), device))
9250 gdk_device_grab(device, gtk_widget_get_window(gtkwin->notebook),
9251 GDK_OWNERSHIP_WINDOW, FALSE,
9252 GDK_BUTTON1_MOTION_MASK | GDK_BUTTON_RELEASE_MASK,
9253 cursor, gdk_event_get_time(event));
9256 static gboolean
9257 notebook_motion_cb(GtkWidget *widget, GdkEventButton *e, PidginConvWindow *win)
9261 * Make sure the user moved the mouse far enough for the
9262 * drag to be initiated.
9264 if (win->in_predrag) {
9265 if (e->x_root < win->drag_min_x ||
9266 e->x_root >= win->drag_max_x ||
9267 e->y_root < win->drag_min_y ||
9268 e->y_root >= win->drag_max_y) {
9270 win->in_predrag = FALSE;
9271 notebook_init_grab(win, widget, (GdkEvent *)e);
9274 else { /* Otherwise, draw the arrows. */
9275 PidginConvWindow *dest_win;
9276 GtkNotebook *dest_notebook;
9277 GtkWidget *tab;
9278 gint page_num;
9279 gboolean horiz_tabs = FALSE;
9280 gboolean to_right = FALSE;
9282 /* Get the window that the cursor is over. */
9283 dest_win = pidgin_conv_window_get_at_event((GdkEvent *)e);
9285 if (dest_win == NULL) {
9286 pidgin_dnd_hints_hide_all();
9288 return TRUE;
9291 dest_notebook = GTK_NOTEBOOK(dest_win->notebook);
9293 if (gtk_notebook_get_show_tabs(dest_notebook)) {
9294 page_num = pidgin_conv_get_tab_at_xy(dest_win,
9295 e->x_root, e->y_root, &to_right);
9296 to_right = to_right && (win != dest_win);
9297 tab = pidgin_conv_window_get_gtkconv_at_index(dest_win, page_num)->tabby;
9298 } else {
9299 page_num = 0;
9300 to_right = pidgin_conv_xy_to_right_infopane(dest_win, e->x_root, e->y_root);
9301 tab = pidgin_conv_window_get_gtkconv_at_index(dest_win, page_num)->infopane_hbox;
9304 if (gtk_notebook_get_tab_pos(dest_notebook) == GTK_POS_TOP ||
9305 gtk_notebook_get_tab_pos(dest_notebook) == GTK_POS_BOTTOM) {
9306 horiz_tabs = TRUE;
9309 if (gtk_notebook_get_show_tabs(dest_notebook) == FALSE && win == dest_win)
9311 /* dragging a tab from a single-tabbed window over its own window */
9312 pidgin_dnd_hints_hide_all();
9313 return TRUE;
9314 } else if (horiz_tabs) {
9315 if (((gpointer)win == (gpointer)dest_win && win->drag_tab < page_num) || to_right) {
9316 pidgin_dnd_hints_show_relative(HINT_ARROW_DOWN, tab, HINT_POSITION_RIGHT, HINT_POSITION_TOP);
9317 pidgin_dnd_hints_show_relative(HINT_ARROW_UP, tab, HINT_POSITION_RIGHT, HINT_POSITION_BOTTOM);
9318 } else {
9319 pidgin_dnd_hints_show_relative(HINT_ARROW_DOWN, tab, HINT_POSITION_LEFT, HINT_POSITION_TOP);
9320 pidgin_dnd_hints_show_relative(HINT_ARROW_UP, tab, HINT_POSITION_LEFT, HINT_POSITION_BOTTOM);
9322 } else {
9323 if (((gpointer)win == (gpointer)dest_win && win->drag_tab < page_num) || to_right) {
9324 pidgin_dnd_hints_show_relative(HINT_ARROW_RIGHT, tab, HINT_POSITION_LEFT, HINT_POSITION_BOTTOM);
9325 pidgin_dnd_hints_show_relative(HINT_ARROW_LEFT, tab, HINT_POSITION_RIGHT, HINT_POSITION_BOTTOM);
9326 } else {
9327 pidgin_dnd_hints_show_relative(HINT_ARROW_RIGHT, tab, HINT_POSITION_LEFT, HINT_POSITION_TOP);
9328 pidgin_dnd_hints_show_relative(HINT_ARROW_LEFT, tab, HINT_POSITION_RIGHT, HINT_POSITION_TOP);
9333 return TRUE;
9336 static gboolean
9337 notebook_leave_cb(GtkWidget *widget, GdkEventCrossing *e, PidginConvWindow *win)
9339 if (win->in_drag)
9340 return FALSE;
9342 if (e->x_root < win->drag_min_x ||
9343 e->x_root >= win->drag_max_x ||
9344 e->y_root < win->drag_min_y ||
9345 e->y_root >= win->drag_max_y) {
9347 win->in_predrag = FALSE;
9348 notebook_init_grab(win, widget, (GdkEvent *)e);
9351 return TRUE;
9355 * THANK YOU GALEON!
9358 static gboolean
9359 infopane_press_cb(GtkWidget *widget, GdkEventButton *e, PidginConversation *gtkconv)
9361 if (e->type == GDK_2BUTTON_PRESS && e->button == 1) {
9362 if (infopane_entry_activate(gtkconv))
9363 return TRUE;
9366 if (e->type != GDK_BUTTON_PRESS)
9367 return FALSE;
9369 if (e->button == 1) {
9370 int nb_x, nb_y;
9371 GtkAllocation allocation;
9373 gtk_widget_get_allocation(gtkconv->infopane_hbox, &allocation);
9375 if (gtkconv->win->in_drag)
9376 return TRUE;
9378 gtkconv->win->in_predrag = TRUE;
9379 gtkconv->win->drag_tab = gtk_notebook_page_num(GTK_NOTEBOOK(gtkconv->win->notebook), gtkconv->tab_cont);
9381 gdk_window_get_origin(gtk_widget_get_window(gtkconv->infopane_hbox), &nb_x, &nb_y);
9383 gtkconv->win->drag_min_x = allocation.x + nb_x;
9384 gtkconv->win->drag_min_y = allocation.y + nb_y;
9385 gtkconv->win->drag_max_x = allocation.width + gtkconv->win->drag_min_x;
9386 gtkconv->win->drag_max_y = allocation.height + gtkconv->win->drag_min_y;
9388 gtkconv->win->drag_motion_signal = g_signal_connect(G_OBJECT(gtkconv->win->notebook), "motion_notify_event",
9389 G_CALLBACK(notebook_motion_cb), gtkconv->win);
9390 gtkconv->win->drag_leave_signal = g_signal_connect(G_OBJECT(gtkconv->win->notebook), "leave_notify_event",
9391 G_CALLBACK(notebook_leave_cb), gtkconv->win);
9392 return FALSE;
9395 if (e->button == 3) {
9396 /* Right click was pressed. Popup the context menu. */
9397 GtkWidget *menu = gtk_menu_new(), *sub;
9398 gboolean populated = populate_menu_with_options(menu, gtkconv, TRUE);
9400 sub = gtk_menu_item_get_submenu(GTK_MENU_ITEM(gtkconv->win->menu->send_to));
9401 if (sub && gtk_widget_is_sensitive(gtkconv->win->menu->send_to)) {
9402 GtkWidget *item = gtk_menu_item_new_with_mnemonic(_("S_end To"));
9403 if (populated)
9404 pidgin_separator(menu);
9405 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
9406 gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), sub);
9407 gtk_widget_show(item);
9408 gtk_widget_show_all(sub);
9409 } else if (!populated) {
9410 gtk_widget_destroy(menu);
9411 return FALSE;
9414 gtk_widget_show_all(menu);
9415 gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, e->button, e->time);
9416 return TRUE;
9418 return FALSE;
9421 static gboolean
9422 notebook_press_cb(GtkWidget *widget, GdkEventButton *e, PidginConvWindow *win)
9424 gint nb_x, nb_y;
9425 int tab_clicked;
9426 GtkWidget *page;
9427 GtkWidget *tab;
9428 GtkAllocation allocation;
9430 if (e->button == 2 && e->type == GDK_BUTTON_PRESS) {
9431 PidginConversation *gtkconv;
9432 tab_clicked = pidgin_conv_get_tab_at_xy(win, e->x_root, e->y_root, NULL);
9434 if (tab_clicked == -1)
9435 return FALSE;
9437 gtkconv = pidgin_conv_window_get_gtkconv_at_index(win, tab_clicked);
9438 close_conv_cb(NULL, gtkconv);
9439 return TRUE;
9443 if (e->button != 1 || e->type != GDK_BUTTON_PRESS)
9444 return FALSE;
9447 if (win->in_drag) {
9448 purple_debug(PURPLE_DEBUG_WARNING, "gtkconv",
9449 "Already in the middle of a window drag at tab_press_cb\n");
9450 return TRUE;
9454 * Make sure a tab was actually clicked. The arrow buttons
9455 * mess things up.
9457 tab_clicked = pidgin_conv_get_tab_at_xy(win, e->x_root, e->y_root, NULL);
9459 if (tab_clicked == -1)
9460 return FALSE;
9463 * Get the relative position of the press event, with regards to
9464 * the position of the notebook.
9466 gdk_window_get_origin(gtk_widget_get_window(win->notebook), &nb_x, &nb_y);
9468 /* Reset the min/max x/y */
9469 win->drag_min_x = 0;
9470 win->drag_min_y = 0;
9471 win->drag_max_x = 0;
9472 win->drag_max_y = 0;
9474 /* Find out which tab was dragged. */
9475 page = gtk_notebook_get_nth_page(GTK_NOTEBOOK(win->notebook), tab_clicked);
9476 tab = gtk_notebook_get_tab_label(GTK_NOTEBOOK(win->notebook), page);
9478 gtk_widget_get_allocation(tab, &allocation);
9480 win->drag_min_x = allocation.x + nb_x;
9481 win->drag_min_y = allocation.y + nb_y;
9482 win->drag_max_x = allocation.width + win->drag_min_x;
9483 win->drag_max_y = allocation.height + win->drag_min_y;
9485 /* Make sure the click occurred in the tab. */
9486 if (e->x_root < win->drag_min_x ||
9487 e->x_root >= win->drag_max_x ||
9488 e->y_root < win->drag_min_y ||
9489 e->y_root >= win->drag_max_y) {
9491 return FALSE;
9494 win->in_predrag = TRUE;
9495 win->drag_tab = tab_clicked;
9497 /* Connect the new motion signals. */
9498 win->drag_motion_signal =
9499 g_signal_connect(G_OBJECT(widget), "motion_notify_event",
9500 G_CALLBACK(notebook_motion_cb), win);
9502 win->drag_leave_signal =
9503 g_signal_connect(G_OBJECT(widget), "leave_notify_event",
9504 G_CALLBACK(notebook_leave_cb), win);
9506 return FALSE;
9509 static gboolean
9510 notebook_release_cb(GtkWidget *widget, GdkEventButton *e, PidginConvWindow *win)
9512 PidginConvWindow *dest_win;
9513 GtkNotebook *dest_notebook;
9514 PidginConversation *active_gtkconv;
9515 PidginConversation *gtkconv;
9516 gint dest_page_num = 0;
9517 gboolean new_window = FALSE;
9518 gboolean to_right = FALSE;
9519 GdkDevice *device;
9522 * Don't check to make sure that the event's window matches the
9523 * widget's, because we may be getting an event passed on from the
9524 * close button.
9526 if (e->button != 1 && e->type != GDK_BUTTON_RELEASE)
9527 return FALSE;
9529 device = gdk_event_get_device((GdkEvent *)e);
9530 if (gdk_display_device_is_grabbed(gdk_device_get_display(device), device)) {
9531 gdk_device_ungrab(device, gdk_event_get_time((GdkEvent *)e));
9532 gtk_grab_remove(widget);
9535 if (!win->in_predrag && !win->in_drag)
9536 return FALSE;
9538 /* Disconnect the motion signal. */
9539 if (win->drag_motion_signal) {
9540 g_signal_handler_disconnect(G_OBJECT(widget),
9541 win->drag_motion_signal);
9543 win->drag_motion_signal = 0;
9547 * If we're in a pre-drag, we'll also need to disconnect the leave
9548 * signal.
9550 if (win->in_predrag) {
9551 win->in_predrag = FALSE;
9553 if (win->drag_leave_signal) {
9554 g_signal_handler_disconnect(G_OBJECT(widget),
9555 win->drag_leave_signal);
9557 win->drag_leave_signal = 0;
9561 /* If we're not in drag... */
9562 /* We're perfectly normal people! */
9563 if (!win->in_drag)
9564 return FALSE;
9566 win->in_drag = FALSE;
9568 pidgin_dnd_hints_hide_all();
9570 dest_win = pidgin_conv_window_get_at_event((GdkEvent *)e);
9572 active_gtkconv = pidgin_conv_window_get_active_gtkconv(win);
9574 if (dest_win == NULL) {
9575 /* If the current window doesn't have any other conversations,
9576 * there isn't much point transferring the conv to a new window. */
9577 if (pidgin_conv_window_get_gtkconv_count(win) > 1) {
9578 /* Make a new window to stick this to. */
9579 dest_win = pidgin_conv_window_new();
9580 new_window = TRUE;
9584 if (dest_win == NULL)
9585 return FALSE;
9587 purple_signal_emit(pidgin_conversations_get_handle(),
9588 "conversation-dragging", win, dest_win);
9590 /* Get the destination page number. */
9591 if (!new_window) {
9592 dest_notebook = GTK_NOTEBOOK(dest_win->notebook);
9593 if (gtk_notebook_get_show_tabs(dest_notebook)) {
9594 dest_page_num = pidgin_conv_get_tab_at_xy(dest_win,
9595 e->x_root, e->y_root, &to_right);
9596 } else {
9597 dest_page_num = 0;
9598 to_right = pidgin_conv_xy_to_right_infopane(dest_win, e->x_root, e->y_root);
9602 gtkconv = pidgin_conv_window_get_gtkconv_at_index(win, win->drag_tab);
9604 if (win == dest_win) {
9605 gtk_notebook_reorder_child(GTK_NOTEBOOK(win->notebook), gtkconv->tab_cont, dest_page_num);
9606 } else {
9607 pidgin_conv_window_remove_gtkconv(win, gtkconv);
9608 pidgin_conv_window_add_gtkconv(dest_win, gtkconv);
9609 gtk_notebook_reorder_child(GTK_NOTEBOOK(dest_win->notebook), gtkconv->tab_cont, dest_page_num + to_right);
9610 pidgin_conv_window_switch_gtkconv(dest_win, gtkconv);
9611 if (new_window) {
9612 gint win_width, win_height;
9614 gtk_window_get_size(GTK_WINDOW(dest_win->window),
9615 &win_width, &win_height);
9616 #ifdef _WIN32 /* only override window manager placement on Windows */
9617 gtk_window_move(GTK_WINDOW(dest_win->window),
9618 e->x_root - (win_width / 2),
9619 e->y_root - (win_height / 2));
9620 #endif
9622 pidgin_conv_window_show(dest_win);
9626 gtk_widget_grab_focus(active_gtkconv->entry);
9628 return TRUE;
9632 static void
9633 before_switch_conv_cb(GtkNotebook *notebook, GtkWidget *page, gint page_num,
9634 gpointer user_data)
9636 PidginConvWindow *win;
9637 PurpleConversation *conv;
9638 PidginConversation *gtkconv;
9640 win = user_data;
9641 conv = pidgin_conv_window_get_active_conversation(win);
9643 g_return_if_fail(conv != NULL);
9645 if (!PURPLE_IS_IM_CONVERSATION(conv))
9646 return;
9648 gtkconv = PIDGIN_CONVERSATION(conv);
9650 if (gtkconv->u.im->typing_timer != 0) {
9651 g_source_remove(gtkconv->u.im->typing_timer);
9652 gtkconv->u.im->typing_timer = 0;
9655 stop_anim(NULL, gtkconv);
9658 static void
9659 close_window(GtkWidget *w, PidginConvWindow *win)
9661 close_win_cb(w, NULL, win);
9664 static void
9665 detach_tab_cb(GtkWidget *w, PidginConvWindow *win)
9667 PidginConvWindow *new_window;
9668 PidginConversation *gtkconv;
9670 gtkconv = win->clicked_tab;
9672 if (!gtkconv)
9673 return;
9675 /* Nothing to do if there's only one tab in the window */
9676 if (pidgin_conv_window_get_gtkconv_count(win) == 1)
9677 return;
9679 pidgin_conv_window_remove_gtkconv(win, gtkconv);
9681 new_window = pidgin_conv_window_new();
9682 pidgin_conv_window_add_gtkconv(new_window, gtkconv);
9683 pidgin_conv_window_show(new_window);
9686 static void
9687 close_others_cb(GtkWidget *w, PidginConvWindow *win)
9689 GList *iter;
9690 PidginConversation *gtkconv;
9692 gtkconv = win->clicked_tab;
9694 if (!gtkconv)
9695 return;
9697 for (iter = pidgin_conv_window_get_gtkconvs(win); iter; )
9699 PidginConversation *gconv = iter->data;
9700 iter = iter->next;
9702 if (gconv != gtkconv)
9704 close_conv_cb(NULL, gconv);
9709 static void
9710 close_tab_cb(GtkWidget *w, PidginConvWindow *win)
9712 PidginConversation *gtkconv;
9714 gtkconv = win->clicked_tab;
9716 if (gtkconv)
9717 close_conv_cb(NULL, gtkconv);
9720 static void
9721 notebook_menu_switch_cb(GtkWidget *item, GtkWidget *child)
9723 GtkNotebook *notebook;
9724 int index;
9726 notebook = GTK_NOTEBOOK(gtk_widget_get_parent(child));
9727 index = gtk_notebook_page_num(notebook, child);
9728 gtk_notebook_set_current_page(notebook, index);
9731 static void
9732 notebook_menu_update_label_cb(GtkWidget *child, GParamSpec *pspec,
9733 GtkNotebook *notebook)
9735 GtkWidget *item;
9736 GtkWidget *label;
9738 item = g_object_get_data(G_OBJECT(child), "popup-menu-item");
9739 label = gtk_bin_get_child(GTK_BIN(item));
9740 if (label)
9741 gtk_container_remove(GTK_CONTAINER(item), label);
9743 label = gtk_notebook_get_menu_label(notebook, child);
9744 if (label) {
9745 gtk_widget_show(label);
9746 gtk_container_add(GTK_CONTAINER(item), label);
9747 gtk_widget_show(item);
9748 } else {
9749 gtk_widget_hide(item);
9753 static void
9754 notebook_add_tab_to_menu_cb(GtkNotebook *notebook, GtkWidget *child,
9755 guint page_num, PidginConvWindow *win)
9757 GtkWidget *item;
9758 GtkWidget *label;
9760 item = gtk_menu_item_new();
9761 label = gtk_notebook_get_menu_label(notebook, child);
9762 if (label) {
9763 gtk_widget_show(label);
9764 gtk_container_add(GTK_CONTAINER(item), label);
9765 gtk_widget_show(item);
9768 g_signal_connect(child, "child-notify::menu-label",
9769 G_CALLBACK(notebook_menu_update_label_cb), notebook);
9770 g_signal_connect(item, "activate",
9771 G_CALLBACK(notebook_menu_switch_cb), child);
9772 g_object_set_data(G_OBJECT(child), "popup-menu-item", item);
9774 gtk_menu_shell_insert(GTK_MENU_SHELL(win->notebook_menu), item, page_num);
9777 static void
9778 notebook_remove_tab_from_menu_cb(GtkNotebook *notebook, GtkWidget *child,
9779 guint page_num, PidginConvWindow *win)
9781 GtkWidget *item;
9783 /* Disconnecting the "child-notify::menu-label" signal. */
9784 g_signal_handlers_disconnect_by_data(child, notebook);
9786 item = g_object_get_data(G_OBJECT(child), "popup-menu-item");
9787 gtk_container_remove(GTK_CONTAINER(win->notebook_menu), item);
9791 static void
9792 notebook_reorder_tab_in_menu_cb(GtkNotebook *notebook, GtkWidget *child,
9793 guint page_num, PidginConvWindow *win)
9795 GtkWidget *item;
9797 item = g_object_get_data(G_OBJECT(child), "popup-menu-item");
9798 gtk_menu_reorder_child(GTK_MENU(win->notebook_menu), item, page_num);
9801 static gboolean
9802 notebook_right_click_menu_cb(GtkNotebook *notebook, GdkEventButton *event,
9803 PidginConvWindow *win)
9805 GtkWidget *menu;
9806 PidginConversation *gtkconv;
9808 if (event->type != GDK_BUTTON_PRESS || event->button != 3)
9809 return FALSE;
9811 gtkconv = pidgin_conv_window_get_gtkconv_at_index(win,
9812 pidgin_conv_get_tab_at_xy(win, event->x_root, event->y_root, NULL));
9814 win->clicked_tab = gtkconv;
9816 menu = win->notebook_menu;
9818 gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, 3, event->time);
9820 return TRUE;
9823 static void
9824 remove_edit_entry(PidginConversation *gtkconv, GtkWidget *entry)
9826 g_signal_handlers_disconnect_matched(G_OBJECT(entry), G_SIGNAL_MATCH_DATA,
9827 0, 0, NULL, NULL, gtkconv);
9828 gtk_widget_show(gtkconv->infopane);
9829 gtk_widget_grab_focus(gtkconv->entry);
9830 gtk_widget_destroy(entry);
9833 static gboolean
9834 alias_focus_cb(GtkWidget *widget, GdkEventFocus *event, gpointer user_data)
9836 remove_edit_entry(user_data, widget);
9837 return FALSE;
9840 static gboolean
9841 alias_key_press_cb(GtkWidget *widget, GdkEventKey *event, gpointer user_data)
9843 if (event->keyval == GDK_KEY_Escape) {
9844 remove_edit_entry(user_data, widget);
9845 return TRUE;
9847 return FALSE;
9850 static void
9851 alias_cb(GtkEntry *entry, gpointer user_data)
9853 PidginConversation *gtkconv;
9854 PurpleConversation *conv;
9855 PurpleAccount *account;
9856 const char *name;
9858 gtkconv = (PidginConversation *)user_data;
9859 if (gtkconv == NULL) {
9860 return;
9862 conv = gtkconv->active_conv;
9863 account = purple_conversation_get_account(conv);
9864 name = purple_conversation_get_name(conv);
9866 if (PURPLE_IS_IM_CONVERSATION(conv)) {
9867 PurpleBuddy *buddy;
9868 buddy = purple_blist_find_buddy(account, name);
9869 if (buddy != NULL) {
9870 purple_buddy_set_local_alias(buddy, gtk_entry_get_text(entry));
9872 purple_serv_alias_buddy(buddy);
9873 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
9874 gtk_entry_set_text(GTK_ENTRY(gtkconv->u.chat->topic_text), gtk_entry_get_text(entry));
9875 topic_callback(NULL, gtkconv);
9877 remove_edit_entry(user_data, GTK_WIDGET(entry));
9880 static gboolean
9881 infopane_entry_activate(PidginConversation *gtkconv)
9883 GtkWidget *entry = NULL;
9884 PurpleConversation *conv = gtkconv->active_conv;
9885 const char *text = NULL;
9887 if (!gtk_widget_get_visible(gtkconv->infopane)) {
9888 /* There's already an entry for alias. Let's not create another one. */
9889 return FALSE;
9892 if (!purple_account_is_connected(purple_conversation_get_account(gtkconv->active_conv))) {
9893 /* Do not allow aliasing someone on a disconnected account. */
9894 return FALSE;
9897 if (PURPLE_IS_IM_CONVERSATION(conv)) {
9898 PurpleBuddy *buddy = purple_blist_find_buddy(purple_conversation_get_account(gtkconv->active_conv), purple_conversation_get_name(gtkconv->active_conv));
9899 if (!buddy)
9900 /* This buddy isn't in your buddy list, so we can't alias him */
9901 return FALSE;
9903 text = purple_buddy_get_contact_alias(buddy);
9904 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
9905 PurpleConnection *gc;
9906 PurpleProtocol *protocol = NULL;
9908 gc = purple_conversation_get_connection(conv);
9909 if (gc != NULL)
9910 protocol = purple_connection_get_protocol(gc);
9911 if (protocol && !PURPLE_PROTOCOL_IMPLEMENTS(protocol, CHAT_IFACE, set_topic))
9912 /* This protocol doesn't support setting the chat room topic */
9913 return FALSE;
9915 text = purple_chat_conversation_get_topic(PURPLE_CHAT_CONVERSATION(conv));
9918 /* alias label */
9919 entry = gtk_entry_new();
9920 gtk_entry_set_has_frame(GTK_ENTRY(entry), FALSE);
9921 gtk_entry_set_width_chars(GTK_ENTRY(entry), 10);
9922 gtk_entry_set_alignment(GTK_ENTRY(entry), 0.5);
9924 gtk_box_pack_start(GTK_BOX(gtkconv->infopane_hbox), entry, TRUE, TRUE, 0);
9925 /* after the tab label */
9926 gtk_box_reorder_child(GTK_BOX(gtkconv->infopane_hbox), entry, 0);
9928 g_signal_connect(G_OBJECT(entry), "activate", G_CALLBACK(alias_cb), gtkconv);
9929 g_signal_connect(G_OBJECT(entry), "focus-out-event", G_CALLBACK(alias_focus_cb), gtkconv);
9930 g_signal_connect(G_OBJECT(entry), "key-press-event", G_CALLBACK(alias_key_press_cb), gtkconv);
9932 if (text != NULL)
9933 gtk_entry_set_text(GTK_ENTRY(entry), text);
9934 gtk_widget_show(entry);
9935 gtk_widget_hide(gtkconv->infopane);
9936 gtk_widget_grab_focus(entry);
9938 return TRUE;
9941 static gboolean
9942 window_keypress_cb(GtkWidget *widget, GdkEventKey *event, PidginConvWindow *win)
9944 PidginConversation *gtkconv = pidgin_conv_window_get_active_gtkconv(win);
9946 return conv_keypress_common(gtkconv, event);
9949 static void
9950 switch_conv_cb(GtkNotebook *notebook, GtkWidget *page, gint page_num,
9951 gpointer user_data)
9953 PidginConvWindow *win;
9954 PurpleConversation *conv;
9955 PidginConversation *gtkconv;
9956 const char *sound_method;
9958 win = user_data;
9959 gtkconv = pidgin_conv_window_get_gtkconv_at_index(win, page_num);
9960 conv = gtkconv->active_conv;
9962 g_return_if_fail(conv != NULL);
9964 /* clear unseen flag if conversation is not hidden */
9965 if(!pidgin_conv_is_hidden(gtkconv)) {
9966 gtkconv_set_unseen(gtkconv, PIDGIN_UNSEEN_NONE);
9969 /* Update the menubar */
9971 gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(gtkconv->win->menu->logging),
9972 purple_conversation_is_logging(conv));
9974 generate_send_to_items(win);
9975 generate_e2ee_controls(win);
9976 regenerate_options_items(win);
9977 regenerate_plugins_items(win);
9979 pidgin_conv_switch_active_conversation(conv);
9981 sound_method = purple_prefs_get_string(PIDGIN_PREFS_ROOT "/sound/method");
9982 if (strcmp(sound_method, "none") != 0)
9983 gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(win->menu->sounds),
9984 gtkconv->make_sound);
9986 gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(win->menu->show_formatting_toolbar),
9987 purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/show_formatting_toolbar"));
9990 * We pause icons when they are not visible. If this icon should
9991 * be animated then start it back up again.
9993 if (PURPLE_IS_IM_CONVERSATION(conv) &&
9994 (gtkconv->u.im->animate))
9995 start_anim(NULL, gtkconv);
9997 purple_signal_emit(pidgin_conversations_get_handle(), "conversation-switched", conv);
10000 /**************************************************************************
10001 * GTK+ window ops
10002 **************************************************************************/
10004 GList *
10005 pidgin_conv_windows_get_list()
10007 return window_list;
10010 static GList*
10011 make_status_icon_list(const char *stock, GtkWidget *w)
10013 GList *l = NULL;
10014 l = g_list_append(l,
10015 gtk_widget_render_icon(w, stock,
10016 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL), "GtkWindow"));
10017 l = g_list_append(l,
10018 gtk_widget_render_icon(w, stock,
10019 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_SMALL), "GtkWindow"));
10020 l = g_list_append(l,
10021 gtk_widget_render_icon(w, stock,
10022 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_MEDIUM), "GtkWindow"));
10023 l = g_list_append(l,
10024 gtk_widget_render_icon(w, stock,
10025 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_LARGE), "GtkWindow"));
10026 return l;
10029 static void
10030 create_icon_lists(GtkWidget *w)
10032 available_list = make_status_icon_list(PIDGIN_STOCK_STATUS_AVAILABLE, w);
10033 busy_list = make_status_icon_list(PIDGIN_STOCK_STATUS_BUSY, w);
10034 xa_list = make_status_icon_list(PIDGIN_STOCK_STATUS_XA, w);
10035 offline_list = make_status_icon_list(PIDGIN_STOCK_STATUS_OFFLINE, w);
10036 away_list = make_status_icon_list(PIDGIN_STOCK_STATUS_AWAY, w);
10037 protocol_lists = g_hash_table_new(g_str_hash, g_str_equal);
10040 static void
10041 plugin_changed_cb(PurplePlugin *p, gpointer data)
10043 regenerate_plugins_items(data);
10046 static gboolean gtk_conv_configure_cb(GtkWidget *w, GdkEventConfigure *event, gpointer data) {
10047 int x, y;
10049 if (gtk_widget_get_visible(w))
10050 gtk_window_get_position(GTK_WINDOW(w), &x, &y);
10051 else
10052 return FALSE; /* carry on normally */
10054 /* Workaround for GTK+ bug # 169811 - "configure_event" is fired
10055 * when the window is being maximized */
10056 if (gdk_window_get_state(gtk_widget_get_window(w)) & GDK_WINDOW_STATE_MAXIMIZED)
10057 return FALSE;
10059 /* don't save off-screen positioning */
10060 if (x + event->width < 0 ||
10061 y + event->height < 0 ||
10062 x > gdk_screen_width() ||
10063 y > gdk_screen_height())
10064 return FALSE; /* carry on normally */
10066 /* store the position */
10067 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/x", x);
10068 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/y", y);
10069 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/width", event->width);
10070 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/height", event->height);
10072 /* continue to handle event normally */
10073 return FALSE;
10077 static void
10078 pidgin_conv_set_position_size(PidginConvWindow *win, int conv_x, int conv_y,
10079 int conv_width, int conv_height)
10081 /* if the window exists, is hidden, we're saving positions, and the
10082 * position is sane... */
10083 if (win && win->window &&
10084 !gtk_widget_get_visible(win->window) && conv_width != 0) {
10086 #ifdef _WIN32 /* only override window manager placement on Windows */
10087 /* ...check position is on screen... */
10088 if (conv_x >= gdk_screen_width())
10089 conv_x = gdk_screen_width() - 100;
10090 else if (conv_x + conv_width < 0)
10091 conv_x = 100;
10093 if (conv_y >= gdk_screen_height())
10094 conv_y = gdk_screen_height() - 100;
10095 else if (conv_y + conv_height < 0)
10096 conv_y = 100;
10098 /* ...and move it back. */
10099 gtk_window_move(GTK_WINDOW(win->window), conv_x, conv_y);
10100 #endif
10101 gtk_window_resize(GTK_WINDOW(win->window), conv_width, conv_height);
10105 static void
10106 pidgin_conv_restore_position(PidginConvWindow *win) {
10107 pidgin_conv_set_position_size(win,
10108 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/x"),
10109 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/y"),
10110 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/width"),
10111 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/height"));
10114 PidginConvWindow *
10115 pidgin_conv_window_new()
10117 PidginConvWindow *win;
10118 GtkPositionType pos;
10119 GtkWidget *testidea;
10120 GtkWidget *menubar;
10121 GtkWidget *menu;
10122 GtkWidget *item;
10123 GdkModifierType state;
10125 win = g_malloc0(sizeof(PidginConvWindow));
10126 win->menu = g_malloc0(sizeof(PidginConvWindowMenu));
10128 window_list = g_list_append(window_list, win);
10130 /* Create the window. */
10131 win->window = pidgin_create_window(NULL, 0, "conversation", TRUE);
10132 /*_pidgin_widget_set_accessible_name(win->window, "Conversations");*/
10133 if (!gtk_get_current_event_state(&state))
10134 gtk_window_set_focus_on_map(GTK_WINDOW(win->window), FALSE);
10136 /* Etan: I really think this entire function call should happen only
10137 * when we are on Windows but I was informed that back before we used
10138 * to save the window position we stored the window size, so I'm
10139 * leaving it for now. */
10140 #if TRUE || defined(_WIN32)
10141 pidgin_conv_restore_position(win);
10142 #endif
10144 if (available_list == NULL) {
10145 create_icon_lists(win->window);
10148 g_signal_connect(G_OBJECT(win->window), "delete_event",
10149 G_CALLBACK(close_win_cb), win);
10150 g_signal_connect(G_OBJECT(win->window), "focus_in_event",
10151 G_CALLBACK(focus_win_cb), win);
10153 /* Intercept keystrokes from the menu items */
10154 g_signal_connect(G_OBJECT(win->window), "key_press_event",
10155 G_CALLBACK(window_keypress_cb), win);
10158 /* Create the notebook. */
10159 win->notebook = gtk_notebook_new();
10161 pos = purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side");
10163 #if 0
10164 gtk_notebook_set_tab_hborder(GTK_NOTEBOOK(win->notebook), 0);
10165 gtk_notebook_set_tab_vborder(GTK_NOTEBOOK(win->notebook), 0);
10166 #endif
10167 gtk_notebook_set_tab_pos(GTK_NOTEBOOK(win->notebook), pos);
10168 gtk_notebook_set_scrollable(GTK_NOTEBOOK(win->notebook), TRUE);
10169 gtk_notebook_set_show_tabs(GTK_NOTEBOOK(win->notebook), FALSE);
10170 gtk_notebook_set_show_border(GTK_NOTEBOOK(win->notebook), TRUE);
10172 menu = win->notebook_menu = gtk_menu_new();
10174 pidgin_separator(GTK_WIDGET(menu));
10176 item = gtk_menu_item_new_with_label(_("Close other tabs"));
10177 gtk_widget_show(item);
10178 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
10179 g_signal_connect(G_OBJECT(item), "activate",
10180 G_CALLBACK(close_others_cb), win);
10182 item = gtk_menu_item_new_with_label(_("Close all tabs"));
10183 gtk_widget_show(item);
10184 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
10185 g_signal_connect(G_OBJECT(item), "activate",
10186 G_CALLBACK(close_window), win);
10188 pidgin_separator(menu);
10190 item = gtk_menu_item_new_with_label(_("Detach this tab"));
10191 gtk_widget_show(item);
10192 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
10193 g_signal_connect(G_OBJECT(item), "activate",
10194 G_CALLBACK(detach_tab_cb), win);
10196 item = gtk_menu_item_new_with_label(_("Close this tab"));
10197 gtk_widget_show(item);
10198 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
10199 g_signal_connect(G_OBJECT(item), "activate",
10200 G_CALLBACK(close_tab_cb), win);
10202 g_signal_connect(G_OBJECT(win->notebook), "page-added",
10203 G_CALLBACK(notebook_add_tab_to_menu_cb), win);
10204 g_signal_connect(G_OBJECT(win->notebook), "page-removed",
10205 G_CALLBACK(notebook_remove_tab_from_menu_cb), win);
10206 g_signal_connect(G_OBJECT(win->notebook), "page-reordered",
10207 G_CALLBACK(notebook_reorder_tab_in_menu_cb), win);
10209 g_signal_connect(G_OBJECT(win->notebook), "button-press-event",
10210 G_CALLBACK(notebook_right_click_menu_cb), win);
10212 gtk_widget_show(win->notebook);
10214 g_signal_connect(G_OBJECT(win->notebook), "switch_page",
10215 G_CALLBACK(before_switch_conv_cb), win);
10216 g_signal_connect_after(G_OBJECT(win->notebook), "switch_page",
10217 G_CALLBACK(switch_conv_cb), win);
10219 /* Setup the tab drag and drop signals. */
10220 gtk_widget_add_events(win->notebook,
10221 GDK_BUTTON1_MOTION_MASK | GDK_LEAVE_NOTIFY_MASK);
10222 g_signal_connect(G_OBJECT(win->notebook), "button_press_event",
10223 G_CALLBACK(notebook_press_cb), win);
10224 g_signal_connect(G_OBJECT(win->notebook), "button_release_event",
10225 G_CALLBACK(notebook_release_cb), win);
10227 testidea = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
10229 /* Setup the menubar. */
10230 menubar = setup_menubar(win);
10231 gtk_box_pack_start(GTK_BOX(testidea), menubar, FALSE, TRUE, 0);
10233 gtk_box_pack_start(GTK_BOX(testidea), win->notebook, TRUE, TRUE, 0);
10235 gtk_container_add(GTK_CONTAINER(win->window), testidea);
10237 gtk_widget_show(testidea);
10239 /* Update the plugin actions when plugins are (un)loaded */
10240 purple_signal_connect(purple_plugins_get_handle(), "plugin-load",
10241 win, PURPLE_CALLBACK(plugin_changed_cb), win);
10242 purple_signal_connect(purple_plugins_get_handle(), "plugin-unload",
10243 win, PURPLE_CALLBACK(plugin_changed_cb), win);
10246 #ifdef _WIN32
10247 g_signal_connect(G_OBJECT(win->window), "show",
10248 G_CALLBACK(winpidgin_ensure_onscreen), win->window);
10250 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/win32/minimize_new_convs")
10251 && !gtk_get_current_event_state(&state))
10252 gtk_window_iconify(GTK_WINDOW(win->window));
10253 #endif
10255 purple_signal_emit(pidgin_conversations_get_handle(),
10256 "conversation-window-created", win);
10258 return win;
10261 void
10262 pidgin_conv_window_destroy(PidginConvWindow *win)
10264 if (win->gtkconvs) {
10265 GList *iter = win->gtkconvs;
10266 while (iter)
10268 PidginConversation *gtkconv = iter->data;
10269 iter = iter->next;
10270 close_conv_cb(NULL, gtkconv);
10272 return;
10275 purple_prefs_disconnect_by_handle(win);
10276 window_list = g_list_remove(window_list, win);
10278 gtk_widget_destroy(win->notebook_menu);
10279 gtk_widget_destroy(win->window);
10281 g_object_unref(G_OBJECT(win->menu->ui));
10283 purple_notify_close_with_handle(win);
10284 purple_signals_disconnect_by_handle(win);
10286 g_free(win->menu);
10287 g_free(win);
10290 void
10291 pidgin_conv_window_show(PidginConvWindow *win)
10293 gtk_widget_show(win->window);
10296 void
10297 pidgin_conv_window_hide(PidginConvWindow *win)
10299 gtk_widget_hide(win->window);
10302 void
10303 pidgin_conv_window_raise(PidginConvWindow *win)
10305 gdk_window_raise(GDK_WINDOW(gtk_widget_get_window(win->window)));
10308 void
10309 pidgin_conv_window_switch_gtkconv(PidginConvWindow *win, PidginConversation *gtkconv)
10311 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook),
10312 gtk_notebook_page_num(GTK_NOTEBOOK(win->notebook),
10313 gtkconv->tab_cont));
10316 static gboolean
10317 gtkconv_tab_set_tip(GtkWidget *widget, GdkEventCrossing *event, PidginConversation *gtkconv)
10319 /* PANGO_VERSION_CHECK macro was introduced in 1.15. So we need this double check. */
10320 #ifndef PANGO_VERSION_CHECK
10321 #define pango_layout_is_ellipsized(l) TRUE
10322 #elif !PANGO_VERSION_CHECK(1,16,0)
10323 #define pango_layout_is_ellipsized(l) TRUE
10324 #endif
10325 PangoLayout *layout;
10327 layout = gtk_label_get_layout(GTK_LABEL(gtkconv->tab_label));
10328 if (pango_layout_is_ellipsized(layout))
10329 gtk_widget_set_tooltip_text(widget, gtk_label_get_text(GTK_LABEL(gtkconv->tab_label)));
10330 else
10331 gtk_widget_set_tooltip_text(widget, NULL);
10333 return FALSE;
10336 static void
10337 set_default_tab_colors(GtkWidget *widget)
10339 GString *str;
10340 GtkCssProvider *provider;
10341 GError *error = NULL;
10342 int iter;
10344 struct {
10345 const char *labelname;
10346 const char *color;
10347 } styles[] = {
10348 {"tab-label-typing", "#4e9a06"},
10349 {"tab-label-typed", "#c4a000"},
10350 {"tab-label-attention", "#006aff"},
10351 {"tab-label-unreadchat", "#cc0000"},
10352 {"tab-label-event", "#888a85"},
10353 {NULL, NULL}
10356 str = g_string_new(NULL);
10358 for (iter = 0; styles[iter].labelname; iter++) {
10359 g_string_append_printf(str,
10360 "#%s {\n"
10361 " color: %s;\n"
10362 "}\n",
10363 styles[iter].labelname,
10364 styles[iter].color);
10367 provider = gtk_css_provider_new();
10369 gtk_css_provider_load_from_data(provider, str->str, str->len, &error);
10371 gtk_style_context_add_provider(gtk_widget_get_style_context(widget),
10372 GTK_STYLE_PROVIDER(provider),
10373 GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
10375 if (error)
10376 g_error_free(error);
10377 g_string_free(str, TRUE);
10380 void
10381 pidgin_conv_window_add_gtkconv(PidginConvWindow *win, PidginConversation *gtkconv)
10383 PurpleConversation *conv = gtkconv->active_conv;
10384 PidginConversation *focus_gtkconv;
10385 GtkWidget *tab_cont = gtkconv->tab_cont;
10386 const gchar *tmp_lab;
10388 win->gtkconvs = g_list_append(win->gtkconvs, gtkconv);
10389 gtkconv->win = win;
10391 if (win->gtkconvs && win->gtkconvs->next && win->gtkconvs->next->next == NULL)
10392 pidgin_conv_tab_pack(win, ((PidginConversation*)win->gtkconvs->data));
10395 /* Close button. */
10396 gtkconv->close = pidgin_create_small_button(gtk_label_new("×"));
10397 gtk_widget_set_tooltip_text(gtkconv->close, _("Close conversation"));
10399 g_signal_connect(gtkconv->close, "clicked", G_CALLBACK (close_conv_cb), gtkconv);
10401 /* Status icon. */
10402 gtkconv->icon = gtk_image_new();
10403 gtkconv->menu_icon = gtk_image_new();
10404 g_object_set(G_OBJECT(gtkconv->icon),
10405 "icon-size", gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_MICROSCOPIC),
10406 NULL);
10407 g_object_set(G_OBJECT(gtkconv->menu_icon),
10408 "icon-size", gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_MICROSCOPIC),
10409 NULL);
10410 gtk_widget_show(gtkconv->icon);
10411 update_tab_icon(conv);
10413 /* Tab label. */
10414 gtkconv->tab_label = gtk_label_new(tmp_lab = purple_conversation_get_title(conv));
10415 set_default_tab_colors(gtkconv->tab_label);
10416 gtk_widget_set_name(gtkconv->tab_label, "tab-label");
10418 gtkconv->menu_tabby = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PIDGIN_HIG_BOX_SPACE);
10419 gtkconv->menu_label = gtk_label_new(tmp_lab);
10420 gtk_box_pack_start(GTK_BOX(gtkconv->menu_tabby), gtkconv->menu_icon, FALSE, FALSE, 0);
10422 gtk_widget_show_all(gtkconv->menu_icon);
10424 gtk_box_pack_start(GTK_BOX(gtkconv->menu_tabby), gtkconv->menu_label, TRUE, TRUE, 0);
10425 gtk_widget_show(gtkconv->menu_label);
10426 gtk_label_set_xalign(GTK_LABEL(gtkconv->menu_label), 0);
10427 gtk_label_set_yalign(GTK_LABEL(gtkconv->menu_label), 0);
10429 gtk_widget_show(gtkconv->menu_tabby);
10431 if (PURPLE_IS_IM_CONVERSATION(conv))
10432 pidgin_conv_update_buddy_icon(PURPLE_IM_CONVERSATION(conv));
10434 /* Build and set conversations tab */
10435 pidgin_conv_tab_pack(win, gtkconv);
10437 gtk_notebook_set_menu_label(GTK_NOTEBOOK(win->notebook), tab_cont, gtkconv->menu_tabby);
10439 gtk_widget_show(tab_cont);
10441 if (pidgin_conv_window_get_gtkconv_count(win) == 1) {
10442 /* Er, bug in notebooks? Switch to the page manually. */
10443 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), 0);
10444 } else {
10445 gtk_notebook_set_show_tabs(GTK_NOTEBOOK(win->notebook), TRUE);
10448 focus_gtkconv = g_list_nth_data(pidgin_conv_window_get_gtkconvs(win),
10449 gtk_notebook_get_current_page(GTK_NOTEBOOK(win->notebook)));
10450 gtk_widget_grab_focus(focus_gtkconv->entry);
10452 if (pidgin_conv_window_get_gtkconv_count(win) == 1)
10453 update_send_to_selection(win);
10456 static void
10457 pidgin_conv_tab_pack(PidginConvWindow *win, PidginConversation *gtkconv)
10459 gboolean tabs_side = FALSE;
10460 gint angle = 0;
10461 GtkWidget *first, *third, *ebox, *parent;
10463 if (purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") == GTK_POS_LEFT ||
10464 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") == GTK_POS_RIGHT)
10465 tabs_side = TRUE;
10466 else if (purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") == (GTK_POS_LEFT|8))
10467 angle = 90;
10468 else if (purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") == (GTK_POS_RIGHT|8))
10469 angle = 270;
10471 if (!angle) {
10472 g_object_set(G_OBJECT(gtkconv->tab_label), "ellipsize", PANGO_ELLIPSIZE_END, NULL);
10473 gtk_label_set_width_chars(GTK_LABEL(gtkconv->tab_label), 4);
10474 } else {
10475 g_object_set(G_OBJECT(gtkconv->tab_label), "ellipsize", PANGO_ELLIPSIZE_NONE, NULL);
10476 gtk_label_set_width_chars(GTK_LABEL(gtkconv->tab_label), -1);
10479 if (tabs_side) {
10480 gtk_label_set_width_chars(
10481 GTK_LABEL(gtkconv->tab_label),
10482 MIN(g_utf8_strlen(gtk_label_get_text(GTK_LABEL(gtkconv->tab_label)), -1), 12)
10486 gtk_label_set_angle(GTK_LABEL(gtkconv->tab_label), angle);
10488 if (angle)
10489 gtkconv->tabby = gtk_box_new(GTK_ORIENTATION_VERTICAL, PIDGIN_HIG_BOX_SPACE);
10490 else
10491 gtkconv->tabby = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PIDGIN_HIG_BOX_SPACE);
10492 gtk_widget_set_name(gtkconv->tabby, "tab-container");
10494 /* select the correct ordering for verticle tabs */
10495 if (angle == 90) {
10496 first = gtkconv->close;
10497 third = gtkconv->icon;
10498 } else {
10499 first = gtkconv->icon;
10500 third = gtkconv->close;
10503 ebox = gtk_event_box_new();
10504 gtk_event_box_set_visible_window(GTK_EVENT_BOX(ebox), FALSE);
10505 gtk_container_add(GTK_CONTAINER(ebox), gtkconv->tabby);
10506 g_signal_connect(G_OBJECT(ebox), "enter-notify-event",
10507 G_CALLBACK(gtkconv_tab_set_tip), gtkconv);
10509 parent = gtk_widget_get_parent(gtkconv->tab_label);
10510 if (parent != NULL) {
10511 /* reparent old widgets on preference changes */
10512 g_object_ref(first);
10513 g_object_ref(gtkconv->tab_label);
10514 g_object_ref(third);
10515 gtk_container_remove(GTK_CONTAINER(parent), first);
10516 gtk_container_remove(GTK_CONTAINER(parent), gtkconv->tab_label);
10517 gtk_container_remove(GTK_CONTAINER(parent), third);
10520 gtk_box_pack_start(GTK_BOX(gtkconv->tabby), first, FALSE, FALSE, 0);
10521 gtk_box_pack_start(GTK_BOX(gtkconv->tabby), gtkconv->tab_label, TRUE, TRUE, 0);
10522 gtk_box_pack_start(GTK_BOX(gtkconv->tabby), third, FALSE, FALSE, 0);
10524 if (parent == NULL) {
10525 /* Add this pane to the conversation's notebook. */
10526 gtk_notebook_append_page(GTK_NOTEBOOK(win->notebook), gtkconv->tab_cont, ebox);
10527 } else {
10528 /* reparent old widgets on preference changes */
10529 g_object_unref(first);
10530 g_object_unref(gtkconv->tab_label);
10531 g_object_unref(third);
10533 /* Reset the tabs label to the new version */
10534 gtk_notebook_set_tab_label(GTK_NOTEBOOK(win->notebook), gtkconv->tab_cont, ebox);
10537 gtk_container_child_set(GTK_CONTAINER(win->notebook), gtkconv->tab_cont,
10538 "tab-expand", !tabs_side && !angle,
10539 "tab-fill", TRUE, NULL);
10541 if (pidgin_conv_window_get_gtkconv_count(win) == 1)
10542 gtk_notebook_set_show_tabs(GTK_NOTEBOOK(win->notebook),
10543 purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/tabs") &&
10544 (!purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons") ||
10545 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") != GTK_POS_TOP));
10547 /* show the widgets */
10548 /* gtk_widget_show(gtkconv->icon); */
10549 gtk_widget_show(gtkconv->tab_label);
10550 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/close_on_tabs"))
10551 gtk_widget_show(gtkconv->close);
10552 gtk_widget_show(gtkconv->tabby);
10553 gtk_widget_show(ebox);
10556 void
10557 pidgin_conv_window_remove_gtkconv(PidginConvWindow *win, PidginConversation *gtkconv)
10559 unsigned int index;
10561 index = gtk_notebook_page_num(GTK_NOTEBOOK(win->notebook), gtkconv->tab_cont);
10563 g_object_ref_sink(G_OBJECT(gtkconv->tab_cont));
10565 gtk_notebook_remove_page(GTK_NOTEBOOK(win->notebook), index);
10567 win->gtkconvs = g_list_remove(win->gtkconvs, gtkconv);
10569 g_signal_handlers_disconnect_matched(win->window, G_SIGNAL_MATCH_DATA,
10570 0, 0, NULL, NULL, gtkconv);
10572 if (win->gtkconvs && win->gtkconvs->next == NULL)
10573 pidgin_conv_tab_pack(win, win->gtkconvs->data);
10575 if (!win->gtkconvs && win != hidden_convwin)
10576 pidgin_conv_window_destroy(win);
10579 PidginConversation *
10580 pidgin_conv_window_get_gtkconv_at_index(const PidginConvWindow *win, int index)
10582 GtkWidget *tab_cont;
10584 if (index == -1)
10585 index = 0;
10586 tab_cont = gtk_notebook_get_nth_page(GTK_NOTEBOOK(win->notebook), index);
10587 return tab_cont ? g_object_get_data(G_OBJECT(tab_cont), "PidginConversation") : NULL;
10590 PidginConversation *
10591 pidgin_conv_window_get_active_gtkconv(const PidginConvWindow *win)
10593 int index;
10594 GtkWidget *tab_cont;
10596 index = gtk_notebook_get_current_page(GTK_NOTEBOOK(win->notebook));
10597 if (index == -1)
10598 index = 0;
10599 tab_cont = gtk_notebook_get_nth_page(GTK_NOTEBOOK(win->notebook), index);
10600 if (!tab_cont)
10601 return NULL;
10602 return g_object_get_data(G_OBJECT(tab_cont), "PidginConversation");
10606 PurpleConversation *
10607 pidgin_conv_window_get_active_conversation(const PidginConvWindow *win)
10609 PidginConversation *gtkconv;
10611 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
10612 return gtkconv ? gtkconv->active_conv : NULL;
10615 gboolean
10616 pidgin_conv_window_is_active_conversation(const PurpleConversation *conv)
10618 return conv == pidgin_conv_window_get_active_conversation(PIDGIN_CONVERSATION(conv)->win);
10621 gboolean
10622 pidgin_conv_window_has_focus(PidginConvWindow *win)
10624 gboolean has_focus = FALSE;
10626 g_object_get(G_OBJECT(win->window), "has-toplevel-focus", &has_focus, NULL);
10628 return has_focus;
10631 PidginConvWindow *
10632 pidgin_conv_window_get_at_event(GdkEvent *event)
10634 PidginConvWindow *win;
10635 GdkWindow *gdkwin;
10636 GList *l;
10637 int x, y;
10639 gdkwin = gdk_device_get_window_at_position(gdk_event_get_device(event),
10640 &x, &y);
10642 if (gdkwin)
10643 gdkwin = gdk_window_get_toplevel(gdkwin);
10645 for (l = pidgin_conv_windows_get_list(); l != NULL; l = l->next) {
10646 win = l->data;
10648 if (gdkwin == gtk_widget_get_window(win->window))
10649 return win;
10652 return NULL;
10655 GList *
10656 pidgin_conv_window_get_gtkconvs(PidginConvWindow *win)
10658 return win->gtkconvs;
10661 guint
10662 pidgin_conv_window_get_gtkconv_count(PidginConvWindow *win)
10664 return g_list_length(win->gtkconvs);
10667 PidginConvWindow *
10668 pidgin_conv_window_first_im(void)
10670 GList *wins, *convs;
10671 PidginConvWindow *win;
10672 PidginConversation *conv;
10674 for (wins = pidgin_conv_windows_get_list(); wins != NULL; wins = wins->next) {
10675 win = wins->data;
10677 for (convs = win->gtkconvs;
10678 convs != NULL;
10679 convs = convs->next) {
10681 conv = convs->data;
10683 if (PURPLE_IS_IM_CONVERSATION(conv->active_conv))
10684 return win;
10688 return NULL;
10691 PidginConvWindow *
10692 pidgin_conv_window_last_im(void)
10694 GList *wins, *convs;
10695 PidginConvWindow *win;
10696 PidginConversation *conv;
10698 for (wins = g_list_last(pidgin_conv_windows_get_list());
10699 wins != NULL;
10700 wins = wins->prev) {
10702 win = wins->data;
10704 for (convs = win->gtkconvs;
10705 convs != NULL;
10706 convs = convs->next) {
10708 conv = convs->data;
10710 if (PURPLE_IS_IM_CONVERSATION(conv->active_conv))
10711 return win;
10715 return NULL;
10718 PidginConvWindow *
10719 pidgin_conv_window_first_chat(void)
10721 GList *wins, *convs;
10722 PidginConvWindow *win;
10723 PidginConversation *conv;
10725 for (wins = pidgin_conv_windows_get_list(); wins != NULL; wins = wins->next) {
10726 win = wins->data;
10728 for (convs = win->gtkconvs;
10729 convs != NULL;
10730 convs = convs->next) {
10732 conv = convs->data;
10734 if (PURPLE_IS_CHAT_CONVERSATION(conv->active_conv))
10735 return win;
10739 return NULL;
10742 PidginConvWindow *
10743 pidgin_conv_window_last_chat(void)
10745 GList *wins, *convs;
10746 PidginConvWindow *win;
10747 PidginConversation *conv;
10749 for (wins = g_list_last(pidgin_conv_windows_get_list());
10750 wins != NULL;
10751 wins = wins->prev) {
10753 win = wins->data;
10755 for (convs = win->gtkconvs;
10756 convs != NULL;
10757 convs = convs->next) {
10759 conv = convs->data;
10761 if (PURPLE_IS_CHAT_CONVERSATION(conv->active_conv))
10762 return win;
10766 return NULL;
10770 /**************************************************************************
10771 * Conversation placement functions
10772 **************************************************************************/
10773 typedef struct
10775 char *id;
10776 char *name;
10777 PidginConvPlacementFunc fnc;
10779 } ConvPlacementData;
10781 static GList *conv_placement_fncs = NULL;
10782 static PidginConvPlacementFunc place_conv = NULL;
10784 /* This one places conversations in the last made window. */
10785 static void
10786 conv_placement_last_created_win(PidginConversation *conv)
10788 PidginConvWindow *win;
10790 GList *l = g_list_last(pidgin_conv_windows_get_list());
10791 win = l ? l->data : NULL;;
10793 if (win == NULL) {
10794 win = pidgin_conv_window_new();
10796 g_signal_connect(G_OBJECT(win->window), "configure_event",
10797 G_CALLBACK(gtk_conv_configure_cb), NULL);
10799 pidgin_conv_window_add_gtkconv(win, conv);
10800 pidgin_conv_window_show(win);
10801 } else {
10802 pidgin_conv_window_add_gtkconv(win, conv);
10806 /* This one places conversations in the last made window of the same type. */
10807 static gboolean
10808 conv_placement_last_created_win_type_configured_cb(GtkWidget *w,
10809 GdkEventConfigure *event, PidginConversation *conv)
10811 int x, y;
10812 GList *all;
10814 if (gtk_widget_get_visible(w))
10815 gtk_window_get_position(GTK_WINDOW(w), &x, &y);
10816 else
10817 return FALSE; /* carry on normally */
10819 /* Workaround for GTK+ bug # 169811 - "configure_event" is fired
10820 * when the window is being maximized */
10821 if (gdk_window_get_state(gtk_widget_get_window(w)) & GDK_WINDOW_STATE_MAXIMIZED)
10822 return FALSE;
10824 /* don't save off-screen positioning */
10825 if (x + event->width < 0 ||
10826 y + event->height < 0 ||
10827 x > gdk_screen_width() ||
10828 y > gdk_screen_height())
10829 return FALSE; /* carry on normally */
10831 for (all = conv->convs; all != NULL; all = all->next) {
10832 if (PURPLE_IS_IM_CONVERSATION(conv->active_conv) != PURPLE_IS_IM_CONVERSATION(all->data)) {
10833 /* this window has different types of conversation, don't save */
10834 return FALSE;
10838 if (PURPLE_IS_IM_CONVERSATION(conv->active_conv)) {
10839 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/x", x);
10840 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/y", y);
10841 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/width", event->width);
10842 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/height", event->height);
10843 } else if (PURPLE_IS_CHAT_CONVERSATION(conv->active_conv)) {
10844 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/chat/x", x);
10845 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/chat/y", y);
10846 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/chat/width", event->width);
10847 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/chat/height", event->height);
10850 return FALSE;
10853 static void
10854 conv_placement_last_created_win_type(PidginConversation *conv)
10856 PidginConvWindow *win;
10858 if (PURPLE_IS_IM_CONVERSATION(conv->active_conv))
10859 win = pidgin_conv_window_last_im();
10860 else
10861 win = pidgin_conv_window_last_chat();
10863 if (win == NULL) {
10864 win = pidgin_conv_window_new();
10866 if (PURPLE_IS_IM_CONVERSATION(conv->active_conv) ||
10867 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/width") == 0) {
10868 pidgin_conv_set_position_size(win,
10869 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/x"),
10870 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/y"),
10871 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/width"),
10872 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/height"));
10873 } else if (PURPLE_IS_CHAT_CONVERSATION(conv->active_conv)) {
10874 pidgin_conv_set_position_size(win,
10875 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/x"),
10876 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/y"),
10877 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/width"),
10878 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/height"));
10881 pidgin_conv_window_add_gtkconv(win, conv);
10882 pidgin_conv_window_show(win);
10884 g_signal_connect(G_OBJECT(win->window), "configure_event",
10885 G_CALLBACK(conv_placement_last_created_win_type_configured_cb), conv);
10886 } else
10887 pidgin_conv_window_add_gtkconv(win, conv);
10890 /* This one places each conversation in its own window. */
10891 static void
10892 conv_placement_new_window(PidginConversation *conv)
10894 PidginConvWindow *win;
10896 win = pidgin_conv_window_new();
10898 g_signal_connect(G_OBJECT(win->window), "configure_event",
10899 G_CALLBACK(gtk_conv_configure_cb), NULL);
10901 pidgin_conv_window_add_gtkconv(win, conv);
10903 pidgin_conv_window_show(win);
10906 static PurpleGroup *
10907 conv_get_group(PidginConversation *conv)
10909 PurpleGroup *group = NULL;
10911 if (PURPLE_IS_IM_CONVERSATION(conv->active_conv)) {
10912 PurpleBuddy *buddy;
10914 buddy = purple_blist_find_buddy(purple_conversation_get_account(conv->active_conv),
10915 purple_conversation_get_name(conv->active_conv));
10917 if (buddy != NULL)
10918 group = purple_buddy_get_group(buddy);
10920 } else if (PURPLE_IS_CHAT_CONVERSATION(conv->active_conv)) {
10921 PurpleChat *chat;
10923 chat = purple_blist_find_chat(purple_conversation_get_account(conv->active_conv),
10924 purple_conversation_get_name(conv->active_conv));
10926 if (chat != NULL)
10927 group = purple_chat_get_group(chat);
10930 return group;
10934 * This groups things by, well, group. Buddies from groups will always be
10935 * grouped together, and a buddy from a group not belonging to any currently
10936 * open windows will get a new window.
10938 static void
10939 conv_placement_by_group(PidginConversation *conv)
10941 PurpleGroup *group = NULL;
10942 GList *wl, *cl;
10944 group = conv_get_group(conv);
10946 /* Go through the list of IMs and find one with this group. */
10947 for (wl = pidgin_conv_windows_get_list(); wl != NULL; wl = wl->next) {
10948 PidginConvWindow *win2;
10949 PidginConversation *conv2;
10950 PurpleGroup *group2 = NULL;
10952 win2 = wl->data;
10954 for (cl = win2->gtkconvs;
10955 cl != NULL;
10956 cl = cl->next) {
10957 conv2 = cl->data;
10959 group2 = conv_get_group(conv2);
10961 if (group == group2) {
10962 pidgin_conv_window_add_gtkconv(win2, conv);
10964 return;
10969 /* Make a new window. */
10970 conv_placement_new_window(conv);
10973 /* This groups things by account. Otherwise, the same semantics as above */
10974 static void
10975 conv_placement_by_account(PidginConversation *conv)
10977 GList *wins, *convs;
10978 PurpleAccount *account;
10980 account = purple_conversation_get_account(conv->active_conv);
10982 /* Go through the list of IMs and find one with this group. */
10983 for (wins = pidgin_conv_windows_get_list(); wins != NULL; wins = wins->next) {
10984 PidginConvWindow *win2;
10985 PidginConversation *conv2;
10987 win2 = wins->data;
10989 for (convs = win2->gtkconvs;
10990 convs != NULL;
10991 convs = convs->next) {
10992 conv2 = convs->data;
10994 if (account == purple_conversation_get_account(conv2->active_conv)) {
10995 pidgin_conv_window_add_gtkconv(win2, conv);
10996 return;
11001 /* Make a new window. */
11002 conv_placement_new_window(conv);
11005 static ConvPlacementData *
11006 get_conv_placement_data(const char *id)
11008 ConvPlacementData *data = NULL;
11009 GList *n;
11011 for (n = conv_placement_fncs; n; n = n->next) {
11012 data = n->data;
11013 if (!strcmp(data->id, id))
11014 return data;
11017 return NULL;
11020 static void
11021 add_conv_placement_fnc(const char *id, const char *name,
11022 PidginConvPlacementFunc fnc)
11024 ConvPlacementData *data;
11026 data = g_new(ConvPlacementData, 1);
11028 data->id = g_strdup(id);
11029 data->name = g_strdup(name);
11030 data->fnc = fnc;
11032 conv_placement_fncs = g_list_append(conv_placement_fncs, data);
11035 static void
11036 ensure_default_funcs(void)
11038 if (conv_placement_fncs == NULL) {
11039 add_conv_placement_fnc("last", _("Last created window"),
11040 conv_placement_last_created_win);
11041 add_conv_placement_fnc("im_chat", _("Separate IM and Chat windows"),
11042 conv_placement_last_created_win_type);
11043 add_conv_placement_fnc("new", _("New window"),
11044 conv_placement_new_window);
11045 add_conv_placement_fnc("group", _("By group"),
11046 conv_placement_by_group);
11047 add_conv_placement_fnc("account", _("By account"),
11048 conv_placement_by_account);
11052 GList *
11053 pidgin_conv_placement_get_options(void)
11055 GList *n, *list = NULL;
11056 ConvPlacementData *data;
11058 ensure_default_funcs();
11060 for (n = conv_placement_fncs; n; n = n->next) {
11061 data = n->data;
11062 list = g_list_append(list, data->name);
11063 list = g_list_append(list, data->id);
11066 return list;
11070 void
11071 pidgin_conv_placement_add_fnc(const char *id, const char *name,
11072 PidginConvPlacementFunc fnc)
11074 g_return_if_fail(id != NULL);
11075 g_return_if_fail(name != NULL);
11076 g_return_if_fail(fnc != NULL);
11078 ensure_default_funcs();
11080 add_conv_placement_fnc(id, name, fnc);
11083 void
11084 pidgin_conv_placement_remove_fnc(const char *id)
11086 ConvPlacementData *data = get_conv_placement_data(id);
11088 if (data == NULL)
11089 return;
11091 conv_placement_fncs = g_list_remove(conv_placement_fncs, data);
11093 g_free(data->id);
11094 g_free(data->name);
11095 g_free(data);
11098 const char *
11099 pidgin_conv_placement_get_name(const char *id)
11101 ConvPlacementData *data;
11103 ensure_default_funcs();
11105 data = get_conv_placement_data(id);
11107 if (data == NULL)
11108 return NULL;
11110 return data->name;
11113 PidginConvPlacementFunc
11114 pidgin_conv_placement_get_fnc(const char *id)
11116 ConvPlacementData *data;
11118 ensure_default_funcs();
11120 data = get_conv_placement_data(id);
11122 if (data == NULL)
11123 return NULL;
11125 return data->fnc;
11128 void
11129 pidgin_conv_placement_set_current_func(PidginConvPlacementFunc func)
11131 g_return_if_fail(func != NULL);
11133 /* If tabs are enabled, set the function, otherwise, NULL it out. */
11134 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/tabs"))
11135 place_conv = func;
11136 else
11137 place_conv = NULL;
11140 PidginConvPlacementFunc
11141 pidgin_conv_placement_get_current_func(void)
11143 return place_conv;
11146 void
11147 pidgin_conv_placement_place(PidginConversation *gtkconv)
11149 if (place_conv)
11150 place_conv(gtkconv);
11151 else
11152 conv_placement_new_window(gtkconv);
11155 gboolean
11156 pidgin_conv_is_hidden(PidginConversation *gtkconv)
11158 g_return_val_if_fail(gtkconv != NULL, FALSE);
11160 return (gtkconv->win == hidden_convwin);
11164 gdouble luminance(GdkRGBA color)
11166 gdouble r, g, b;
11167 gdouble rr, gg, bb;
11168 gdouble cutoff = 0.03928, scale = 12.92;
11169 gdouble a = 0.055, d = 1.055, p = 2.2;
11171 rr = color.red;
11172 gg = color.green;
11173 bb = color.blue;
11175 r = (rr > cutoff) ? pow((rr+a)/d, p) : rr/scale;
11176 g = (gg > cutoff) ? pow((gg+a)/d, p) : gg/scale;
11177 b = (bb > cutoff) ? pow((bb+a)/d, p) : bb/scale;
11179 return (r*0.2126 + g*0.7152 + b*0.0722);
11182 /* Algorithm from https://www.w3.org/TR/2008/REC-WCAG20-20081211/relative-luminance.xml */
11183 static gboolean
11184 color_is_visible(GdkRGBA foreground, GdkRGBA background, gdouble min_contrast_ratio)
11186 gdouble lfg, lbg, lmin, lmax;
11187 gdouble luminosity_ratio;
11188 gdouble nr, dr;
11190 lfg = luminance(foreground);
11191 lbg = luminance(background);
11193 if (lfg > lbg)
11194 lmax = lfg, lmin = lbg;
11195 else
11196 lmax = lbg, lmin = lfg;
11198 nr = lmax + 0.05, dr = lmin - 0.05;
11199 if ( dr == 0 )
11200 dr += 0.01;
11202 luminosity_ratio = nr/dr;
11203 if ( luminosity_ratio < 0)
11204 luminosity_ratio *= -1.0;
11205 return (luminosity_ratio > min_contrast_ratio);
11209 static GArray*
11210 generate_nick_colors(guint numcolors, GdkRGBA background)
11212 guint i = 0, j = 0;
11213 GArray *colors = g_array_new(FALSE, FALSE, sizeof(GdkRGBA));
11214 GdkRGBA nick_highlight;
11215 GdkRGBA send_color;
11216 time_t breakout_time;
11218 gdk_rgba_parse(&nick_highlight, DEFAULT_HIGHLIGHT_COLOR);
11219 gdk_rgba_parse(&send_color, DEFAULT_SEND_COLOR);
11221 srand(background.red * 65535 + background.green * 65535 + background.blue * 65535 + 1);
11223 breakout_time = time(NULL) + 3;
11225 /* first we look through the list of "good" colors: colors that differ from every other color in the
11226 * list. only some of them will differ from the background color though. lets see if we can find
11227 * numcolors of them that do
11229 while (i < numcolors && j < PIDGIN_NUM_NICK_SEED_COLORS && time(NULL) < breakout_time)
11231 GdkRGBA color = nick_seed_colors[j];
11233 if (color_is_visible(color, background, MIN_LUMINANCE_CONTRAST_RATIO) &&
11234 color_is_visible(color, nick_highlight, MIN_LUMINANCE_CONTRAST_RATIO) &&
11235 color_is_visible(color, send_color, MIN_LUMINANCE_CONTRAST_RATIO))
11237 g_array_append_val(colors, color);
11238 i++;
11240 j++;
11243 /* we might not have found numcolors in the last loop. if we did, we'll never enter this one.
11244 * if we did not, lets just find some colors that don't conflict with the background. its
11245 * expensive to find colors that not only don't conflict with the background, but also do not
11246 * conflict with each other.
11248 while(i < numcolors && time(NULL) < breakout_time)
11250 GdkRGBA color = {rand() % 65536 / 65535.0, rand() % 65536 / 65535.0, rand() % 65536 / 65535.0, 1};
11252 if (color_is_visible(color, background, MIN_LUMINANCE_CONTRAST_RATIO) &&
11253 color_is_visible(color, nick_highlight, MIN_LUMINANCE_CONTRAST_RATIO) &&
11254 color_is_visible(color, send_color, MIN_LUMINANCE_CONTRAST_RATIO))
11256 g_array_append_val(colors, color);
11257 i++;
11261 if (i < numcolors) {
11262 purple_debug_warning("gtkconv", "Unable to generate enough random colors before timeout. %u colors found.\n", i);
11265 if( i == 0 ) {
11266 /* To remove errors caused by an empty array. */
11267 GdkRGBA color = {0.5, 0.5, 0.5, 1.0};
11268 g_array_append_val(colors, color);
11271 return colors;
11274 /**************************************************************************
11275 * PidginConvWindow GBoxed code
11276 **************************************************************************/
11277 static PidginConvWindow *
11278 pidgin_conv_window_ref(PidginConvWindow *win)
11280 g_return_val_if_fail(win != NULL, NULL);
11282 win->box_count++;
11284 return win;
11287 static void
11288 pidgin_conv_window_unref(PidginConvWindow *win)
11290 g_return_if_fail(win != NULL);
11291 g_return_if_fail(win->box_count >= 0);
11293 if (!win->box_count--)
11294 pidgin_conv_window_destroy(win);
11297 GType
11298 pidgin_conv_window_get_type(void)
11300 static GType type = 0;
11302 if (type == 0) {
11303 type = g_boxed_type_register_static("PidginConvWindow",
11304 (GBoxedCopyFunc)pidgin_conv_window_ref,
11305 (GBoxedFreeFunc)pidgin_conv_window_unref);
11308 return type;