Use new GdkSeat API instead of deprecated GdkDevice.
[pidgin-git.git] / pidgin / gtkconv.c
blob0a9216461ccf86f0c01121de0426d84b96bb2996
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
23 #include "internal.h"
24 #include "pidgin.h"
26 #ifndef _WIN32
27 # include <X11/Xlib.h>
28 #endif
30 #include <gdk/gdkkeysyms.h>
32 #include <talkatu.h>
34 #include "account.h"
35 #include "attention.h"
36 #include "action.h"
37 #include "cmds.h"
38 #include "core.h"
39 #include "debug.h"
40 #include "glibcompat.h"
41 #include "idle.h"
42 #include "image-store.h"
43 #include "log.h"
44 #include "notify.h"
45 #include "plugins.h"
46 #include "protocol.h"
47 #include "request.h"
48 #include "smiley-parser.h"
49 #include "util.h"
50 #include "version.h"
52 #include "gtkinternal.h"
53 #include "gtkdnd-hints.h"
54 #include "gtkblist.h"
55 #include "gtkconv.h"
56 #include "gtkconvwin.h"
57 #include "gtkdialogs.h"
58 #include "gtkmenutray.h"
59 #include "gtkpounce.h"
60 #include "gtkprefs.h"
61 #include "gtkprivacy.h"
62 #include "gtkstyle.h"
63 #include "gtkutils.h"
64 #include "pidgingdkpixbuf.h"
65 #include "pidgininvitedialog.h"
66 #include "pidginlog.h"
67 #include "pidginmessage.h"
68 #include "pidginstock.h"
69 #include "pidgintooltip.h"
71 #include "gtknickcolors.h"
73 #define GTK_TOOLTIPS_VAR gtkconv->tooltips
74 #include "gtk3compat.h"
76 #define ADD_MESSAGE_HISTORY_AT_ONCE 100
79 * A GTK+ Instant Message pane.
81 struct _PidginImPane
83 GtkWidget *block;
84 GtkWidget *send_file;
85 GtkWidget *sep1;
86 GtkWidget *sep2;
87 GtkWidget *check;
88 GtkWidget *progress;
89 guint32 typing_timer;
91 /* Buddy icon stuff */
92 GtkWidget *icon_container;
93 GtkWidget *icon;
94 gboolean show_icon;
95 gboolean animate;
96 GdkPixbufAnimation *anim;
97 GdkPixbufAnimationIter *iter;
98 guint32 icon_timer;
102 * GTK+ Chat panes.
104 struct _PidginChatPane
106 GtkWidget *count;
107 GtkWidget *list;
108 GtkWidget *topic_text;
111 #define CLOSE_CONV_TIMEOUT_SECS (10 * 60)
113 #define AUTO_RESPONSE "&lt;AUTO-REPLY&gt; : "
115 typedef enum
117 PIDGIN_CONV_SET_TITLE = 1 << 0,
118 PIDGIN_CONV_BUDDY_ICON = 1 << 1,
119 PIDGIN_CONV_MENU = 1 << 2,
120 PIDGIN_CONV_TAB_ICON = 1 << 3,
121 PIDGIN_CONV_TOPIC = 1 << 4,
122 PIDGIN_CONV_SMILEY_THEME = 1 << 5,
123 PIDGIN_CONV_COLORIZE_TITLE = 1 << 6,
124 PIDGIN_CONV_E2EE = 1 << 7
125 }PidginConvFields;
127 enum {
128 CONV_ICON_COLUMN,
129 CONV_TEXT_COLUMN,
130 CONV_EMBLEM_COLUMN,
131 CONV_PROTOCOL_ICON_COLUMN,
132 CONV_NUM_COLUMNS
133 } PidginInfopaneColumns;
135 #define PIDGIN_CONV_ALL ((1 << 7) - 1)
137 /* XXX: These color defines shouldn't really be here. But the nick-color
138 * generation algorithm uses them, so keeping these around until we fix that. */
139 #define DEFAULT_SEND_COLOR "#204a87"
140 #define DEFAULT_HIGHLIGHT_COLOR "#AF7F00"
142 #define BUDDYICON_SIZE_MIN 32
143 #define BUDDYICON_SIZE_MAX 96
145 #define MIN_LUMINANCE_CONTRAST_RATIO 4.5
147 #define NICK_COLOR_GENERATE_COUNT 220
148 static GArray *generated_nick_colors = NULL;
150 /* These probably won't conflict with any WebKit values. */
151 #define PIDGIN_DRAG_BLIST_NODE (1337)
152 #define PIDGIN_DRAG_IM_CONTACT (31337)
154 static GtkWidget *invite_dialog = NULL;
155 static GtkWidget *warn_close_dialog = NULL;
157 static PidginConvWindow *hidden_convwin = NULL;
158 static GList *window_list = NULL;
160 /* Lists of status icons at all available sizes for use as window icons */
161 static GList *available_list = NULL;
162 static GList *away_list = NULL;
163 static GList *busy_list = NULL;
164 static GList *xa_list = NULL;
165 static GList *offline_list = NULL;
166 static GHashTable *protocol_lists = NULL;
167 static GHashTable *e2ee_stock = NULL;
169 static gboolean update_send_to_selection(PidginConvWindow *win);
170 static void generate_send_to_items(PidginConvWindow *win);
172 /* Prototypes. <-- because Paco-Paco hates this comment. */
173 static gboolean infopane_entry_activate(PidginConversation *gtkconv);
174 static void got_typing_keypress(PidginConversation *gtkconv, gboolean first);
175 static void gray_stuff_out(PidginConversation *gtkconv);
176 static void add_chat_user_common(PurpleChatConversation *chat, PurpleChatUser *cb, const char *old_name);
177 static void pidgin_conv_updated(PurpleConversation *conv, PurpleConversationUpdateType type);
178 static void conv_set_unseen(PurpleConversation *gtkconv, PidginUnseenState state);
179 static void gtkconv_set_unseen(PidginConversation *gtkconv, PidginUnseenState state);
180 static void update_typing_icon(PidginConversation *gtkconv);
181 static void update_typing_message(PidginConversation *gtkconv, const char *message);
182 gboolean pidgin_conv_has_focus(PurpleConversation *conv);
183 static GArray* generate_nick_colors(guint numcolors, GdkRGBA background);
184 gdouble luminance(GdkRGBA color);
185 static gboolean color_is_visible(GdkRGBA foreground, GdkRGBA background, gdouble min_contrast_ratio);
186 static GtkTextTag *get_buddy_tag(PurpleChatConversation *chat, const char *who, PurpleMessageFlags flag, gboolean create);
187 static void pidgin_conv_update_fields(PurpleConversation *conv, PidginConvFields fields);
188 static void focus_out_from_menubar(GtkWidget *wid, PidginConvWindow *win);
189 static void pidgin_conv_tab_pack(PidginConvWindow *win, PidginConversation *gtkconv);
190 static gboolean infopane_press_cb(GtkWidget *widget, GdkEventButton *e, PidginConversation *conv);
191 static void hide_conv(PidginConversation *gtkconv, gboolean closetimer);
193 static void pidgin_conv_set_position_size(PidginConvWindow *win, int x, int y,
194 int width, int height);
195 static gboolean pidgin_conv_xy_to_right_infopane(PidginConvWindow *win, int x, int y);
197 static const GdkRGBA *
198 get_nick_color(PidginConversation *gtkconv, const gchar *name)
200 static GdkRGBA col;
202 if (name == NULL) {
203 col.red = col.green = col.blue = 0;
204 col.alpha = 1;
205 return &col;
208 col = g_array_index(gtkconv->nick_colors, GdkRGBA,
209 g_str_hash(name) % gtkconv->nick_colors->len);
211 return &col;
214 static PurpleBlistNode *
215 get_conversation_blist_node(PurpleConversation *conv)
217 PurpleAccount *account = purple_conversation_get_account(conv);
218 PurpleBlistNode *node = NULL;
220 if (PURPLE_IS_IM_CONVERSATION(conv)) {
221 node = PURPLE_BLIST_NODE(purple_blist_find_buddy(account, purple_conversation_get_name(conv)));
222 node = node ? node->parent : NULL;
223 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
224 node = PURPLE_BLIST_NODE(purple_blist_find_chat(account, purple_conversation_get_name(conv)));
227 return node;
230 /**************************************************************************
231 * Callbacks
232 **************************************************************************/
234 static gboolean
235 close_this_sucker(gpointer data)
237 PidginConversation *gtkconv = data;
238 GList *list = g_list_copy(gtkconv->convs);
239 g_list_free_full(list, g_object_unref);
240 return FALSE;
243 static gboolean
244 close_conv_cb(GtkButton *button, PidginConversation *gtkconv)
246 /* We are going to destroy the conversations immediately only if the 'close immediately'
247 * preference is selected. Otherwise, close the conversation after a reasonable timeout
248 * (I am going to consider 10 minutes as a 'reasonable timeout' here.
249 * For chats, close immediately if the chat is not in the buddylist, or if the chat is
250 * not marked 'Persistent' */
251 PurpleConversation *conv = gtkconv->active_conv;
252 PurpleAccount *account = purple_conversation_get_account(conv);
253 const char *name = purple_conversation_get_name(conv);
255 if (PURPLE_IS_IM_CONVERSATION(conv)) {
256 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/close_immediately"))
257 close_this_sucker(gtkconv);
258 else
259 hide_conv(gtkconv, TRUE);
260 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
261 PurpleChat *chat = purple_blist_find_chat(account, name);
262 if (!chat ||
263 !purple_blist_node_get_bool(&chat->node, "gtk-persistent"))
264 close_this_sucker(gtkconv);
265 else
266 hide_conv(gtkconv, FALSE);
269 return TRUE;
272 static gboolean
273 lbox_size_allocate_cb(GtkWidget *w, GtkAllocation *allocation, gpointer data)
275 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/chat/userlist_width", allocation->width == 1 ? 0 : allocation->width);
277 return FALSE;
280 static const char *
281 pidgin_get_cmd_prefix(void)
283 return "/";
286 static PurpleCmdRet
287 say_command_cb(PurpleConversation *conv,
288 const char *cmd, char **args, char **error, void *data)
290 purple_conversation_send(conv, args[0]);
292 return PURPLE_CMD_RET_OK;
295 static PurpleCmdRet
296 me_command_cb(PurpleConversation *conv,
297 const char *cmd, char **args, char **error, void *data)
299 char *tmp;
301 tmp = g_strdup_printf("/me %s", args[0]);
302 purple_conversation_send(conv, tmp);
304 g_free(tmp);
305 return PURPLE_CMD_RET_OK;
308 static PurpleCmdRet
309 debug_command_cb(PurpleConversation *conv,
310 const char *cmd, char **args, char **error, void *data)
312 char *tmp, *markup;
314 if (!g_ascii_strcasecmp(args[0], "version")) {
315 tmp = g_strdup_printf("Using Pidgin v%s with libpurple v%s.",
316 DISPLAY_VERSION, purple_core_get_version());
317 } else if (!g_ascii_strcasecmp(args[0], "plugins")) {
318 /* Show all the loaded plugins, including plugins marked internal.
319 * This is intentional, since third party protocols are often sources of bugs, and some
320 * plugin loaders can also be buggy.
322 GString *str = g_string_new("Loaded Plugins: ");
323 const GList *plugins = purple_plugins_get_loaded();
324 if (plugins) {
325 for (; plugins; plugins = plugins->next) {
326 GPluginPluginInfo *info = GPLUGIN_PLUGIN_INFO(
327 purple_plugin_get_info(
328 PURPLE_PLUGIN(plugins->data)));
329 str = g_string_append(
330 str,
331 gplugin_plugin_info_get_name(info));
333 if (plugins->next)
334 str = g_string_append(str, ", ");
336 } else {
337 str = g_string_append(str, "(none)");
340 tmp = g_string_free(str, FALSE);
341 } else if (!g_ascii_strcasecmp(args[0], "unsafe")) {
342 if (purple_debug_is_unsafe()) {
343 purple_debug_set_unsafe(FALSE);
344 purple_conversation_write_system_message(conv,
345 _("Unsafe debugging is now disabled."),
346 PURPLE_MESSAGE_NO_LOG);
347 } else {
348 purple_debug_set_unsafe(TRUE);
349 purple_conversation_write_system_message(conv,
350 _("Unsafe debugging is now enabled."),
351 PURPLE_MESSAGE_NO_LOG);
354 return PURPLE_CMD_RET_OK;
355 } else if (!g_ascii_strcasecmp(args[0], "verbose")) {
356 if (purple_debug_is_verbose()) {
357 purple_debug_set_verbose(FALSE);
358 purple_conversation_write_system_message(conv,
359 _("Verbose debugging is now disabled."),
360 PURPLE_MESSAGE_NO_LOG);
361 } else {
362 purple_debug_set_verbose(TRUE);
363 purple_conversation_write_system_message(conv,
364 _("Verbose debugging is now enabled."),
365 PURPLE_MESSAGE_NO_LOG);
368 return PURPLE_CMD_RET_OK;
369 } else {
370 purple_conversation_write_system_message(conv,
371 _("Supported debug options are: plugins, version, unsafe, verbose"),
372 PURPLE_MESSAGE_NO_LOG);
373 return PURPLE_CMD_RET_OK;
376 markup = g_markup_escape_text(tmp, -1);
377 purple_conversation_send(conv, markup);
379 g_free(tmp);
380 g_free(markup);
381 return PURPLE_CMD_RET_OK;
384 static void clear_conversation_scrollback_cb(PurpleConversation *conv,
385 void *data)
387 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
389 if (PIDGIN_CONVERSATION(conv)) {
390 gtkconv->last_flags = 0;
394 static PurpleCmdRet
395 clear_command_cb(PurpleConversation *conv,
396 const char *cmd, char **args, char **error, void *data)
398 purple_conversation_clear_message_history(conv);
399 return PURPLE_CMD_RET_OK;
402 static PurpleCmdRet
403 clearall_command_cb(PurpleConversation *conv,
404 const char *cmd, char **args, char **error, void *data)
406 GList *l;
407 for (l = purple_conversations_get_all(); l != NULL; l = l->next)
408 purple_conversation_clear_message_history(PURPLE_CONVERSATION(l->data));
410 return PURPLE_CMD_RET_OK;
413 static PurpleCmdRet
414 help_command_cb(PurpleConversation *conv,
415 const char *cmd, char **args, char **error, void *data)
417 GList *l, *text;
418 GString *s;
420 if (args[0] != NULL) {
421 s = g_string_new("");
422 text = purple_cmd_help(conv, args[0]);
424 if (text) {
425 for (l = text; l; l = l->next)
426 if (l->next)
427 g_string_append_printf(s, "%s\n", (char *)l->data);
428 else
429 g_string_append_printf(s, "%s", (char *)l->data);
430 } else {
431 g_string_append(s, _("No such command (in this context)."));
433 } else {
434 s = g_string_new(_("Use \"/help &lt;command&gt;\" for help with a "
435 "specific command.<br/>The following commands are available "
436 "in this context:<br/>"));
438 text = purple_cmd_list(conv);
439 for (l = text; l; l = l->next)
440 if (l->next)
441 g_string_append_printf(s, "%s, ", (char *)l->data);
442 else
443 g_string_append_printf(s, "%s.", (char *)l->data);
444 g_list_free(text);
447 purple_conversation_write_system_message(conv, s->str, PURPLE_MESSAGE_NO_LOG);
448 g_string_free(s, TRUE);
450 return PURPLE_CMD_RET_OK;
453 static void
454 send_history_add(PidginConversation *gtkconv, const char *message)
456 GList *first;
458 first = g_list_first(gtkconv->send_history);
459 g_free(first->data);
460 first->data = g_strdup(message);
461 gtkconv->send_history = g_list_prepend(first, NULL);
464 static gboolean
465 check_for_and_do_command(PurpleConversation *conv)
467 PidginConversation *gtkconv;
468 GtkWidget *view = NULL;
469 GtkTextBuffer *buffer = NULL;
470 gchar *cmd;
471 const gchar *prefix;
472 gboolean retval = FALSE;
474 gtkconv = PIDGIN_CONVERSATION(conv);
475 prefix = pidgin_get_cmd_prefix();
477 view = talkatu_editor_get_view(TALKATU_EDITOR(gtkconv->editor));
478 buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(view));
480 cmd = talkatu_buffer_get_plain_text(TALKATU_BUFFER(buffer));
482 if (cmd && purple_str_has_prefix(cmd, prefix)) {
483 PurpleCmdStatus status;
484 char *error, *cmdline, *markup, *send_history;
486 send_history = talkatu_markup_get_html(buffer, NULL);
487 send_history_add(gtkconv, send_history);
488 g_free(send_history);
490 cmdline = cmd + strlen(prefix);
492 if (purple_strequal(cmdline, "xyzzy")) {
493 purple_conversation_write_system_message(conv,
494 "Nothing happens", PURPLE_MESSAGE_NO_LOG);
495 g_free(cmd);
496 return TRUE;
499 /* Docs are unclear on whether or not prefix should be removed from
500 * the markup so, ignoring for now. Notably if the markup is
501 * `<b>/foo arg1</b>` we now have to move the bold tag around?
502 * - gk 20190709 */
503 markup = talkatu_markup_get_html(buffer, NULL);
504 status = purple_cmd_do_command(conv, cmdline, markup, &error);
505 g_free(markup);
507 switch (status) {
508 case PURPLE_CMD_STATUS_OK:
509 retval = TRUE;
510 break;
511 case PURPLE_CMD_STATUS_NOT_FOUND:
513 PurpleProtocol *protocol = NULL;
514 PurpleConnection *gc;
516 if ((gc = purple_conversation_get_connection(conv)))
517 protocol = purple_connection_get_protocol(gc);
519 if ((protocol != NULL) && (purple_protocol_get_options(protocol) & OPT_PROTO_SLASH_COMMANDS_NATIVE)) {
520 char *spaceslash;
522 /* If the first word in the entered text has a '/' in it, then the user
523 * probably didn't mean it as a command. So send the text as message. */
524 spaceslash = cmdline;
525 while (*spaceslash && *spaceslash != ' ' && *spaceslash != '/')
526 spaceslash++;
528 if (*spaceslash != '/') {
529 purple_conversation_write_system_message(conv,
530 _("Unknown command."), PURPLE_MESSAGE_NO_LOG);
531 retval = TRUE;
534 break;
536 case PURPLE_CMD_STATUS_WRONG_ARGS:
537 purple_conversation_write_system_message(conv,
538 _("Syntax Error: You typed the wrong "
539 "number of arguments to that command."),
540 PURPLE_MESSAGE_NO_LOG);
541 retval = TRUE;
542 break;
543 case PURPLE_CMD_STATUS_FAILED:
544 purple_conversation_write_system_message(conv,
545 error ? error : _("Your command failed for an unknown reason."),
546 PURPLE_MESSAGE_NO_LOG);
547 g_free(error);
548 retval = TRUE;
549 break;
550 case PURPLE_CMD_STATUS_WRONG_TYPE:
551 if(PURPLE_IS_IM_CONVERSATION(conv))
552 purple_conversation_write_system_message(conv,
553 _("That command only works in chats, not IMs."),
554 PURPLE_MESSAGE_NO_LOG);
555 else
556 purple_conversation_write_system_message(conv,
557 _("That command only works in IMs, not chats."),
558 PURPLE_MESSAGE_NO_LOG);
559 retval = TRUE;
560 break;
561 case PURPLE_CMD_STATUS_WRONG_PROTOCOL:
562 purple_conversation_write_system_message(conv,
563 _("That command doesn't work on this protocol."),
564 PURPLE_MESSAGE_NO_LOG);
565 retval = TRUE;
566 break;
570 g_free(cmd);
572 return retval;
575 static void
576 send_cb(GtkWidget *widget, PidginConversation *gtkconv)
578 PurpleConversation *conv = gtkconv->active_conv;
579 PurpleAccount *account;
580 PurpleMessageFlags flags = 0;
581 GtkTextBuffer *buffer = NULL;
582 gchar *content;
584 account = purple_conversation_get_account(conv);
586 buffer = talkatu_editor_get_buffer(TALKATU_EDITOR(gtkconv->editor));
588 if (check_for_and_do_command(conv)) {
589 talkatu_buffer_clear(TALKATU_BUFFER(buffer));
590 return;
593 if (PURPLE_IS_CHAT_CONVERSATION(conv) &&
594 purple_chat_conversation_has_left(PURPLE_CHAT_CONVERSATION(conv))) {
595 return;
598 if (!purple_account_is_connected(account)) {
599 return;
602 content = talkatu_markup_get_html(buffer, NULL);
603 if (purple_strequal(content, "")) {
604 g_free(content);
605 return;
608 purple_idle_touch();
610 /* XXX: is there a better way to tell if the message has images? */
611 // if (strstr(buf, "<img ") != NULL)
612 // flags |= PURPLE_MESSAGE_IMAGES;
614 purple_conversation_send_with_flags(conv, content, flags);
616 g_free(content);
618 talkatu_buffer_clear(TALKATU_BUFFER(buffer));
619 gtkconv_set_unseen(gtkconv, PIDGIN_UNSEEN_NONE);
622 static void
623 add_remove_cb(GtkWidget *widget, PidginConversation *gtkconv)
625 PurpleAccount *account;
626 const char *name;
627 PurpleConversation *conv = gtkconv->active_conv;
629 account = purple_conversation_get_account(conv);
630 name = purple_conversation_get_name(conv);
632 if (PURPLE_IS_IM_CONVERSATION(conv)) {
633 PurpleBuddy *b;
635 b = purple_blist_find_buddy(account, name);
636 if (b != NULL)
637 pidgin_dialogs_remove_buddy(b);
638 else if (account != NULL && purple_account_is_connected(account))
639 purple_blist_request_add_buddy(account, (char *)name, NULL, NULL);
640 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
641 PurpleChat *c;
643 c = purple_blist_find_chat(account, name);
644 if (c != NULL)
645 pidgin_dialogs_remove_chat(c);
646 else if (account != NULL && purple_account_is_connected(account))
647 purple_blist_request_add_chat(account, NULL, NULL, name);
651 static void chat_do_info(PidginConversation *gtkconv, const char *who)
653 PurpleChatConversation *chat = PURPLE_CHAT_CONVERSATION(gtkconv->active_conv);
654 PurpleConnection *gc;
656 if ((gc = purple_conversation_get_connection(gtkconv->active_conv))) {
657 pidgin_retrieve_user_info_in_chat(gc, who, purple_chat_conversation_get_id(chat));
662 static void
663 info_cb(GtkWidget *widget, PidginConversation *gtkconv)
665 PurpleConversation *conv = gtkconv->active_conv;
667 if (PURPLE_IS_IM_CONVERSATION(conv)) {
668 pidgin_retrieve_user_info(purple_conversation_get_connection(conv),
669 purple_conversation_get_name(conv));
670 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
671 /* Get info of the person currently selected in the GtkTreeView */
672 PidginChatPane *gtkchat;
673 GtkTreeIter iter;
674 GtkTreeModel *model;
675 GtkTreeSelection *sel;
676 char *name;
678 gtkchat = gtkconv->u.chat;
680 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
681 sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(gtkchat->list));
683 if (gtk_tree_selection_get_selected(sel, NULL, &iter))
684 gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, CHAT_USERS_NAME_COLUMN, &name, -1);
685 else
686 return;
688 chat_do_info(gtkconv, name);
689 g_free(name);
693 static void
694 block_cb(GtkWidget *widget, PidginConversation *gtkconv)
696 PurpleConversation *conv = gtkconv->active_conv;
697 PurpleAccount *account;
699 account = purple_conversation_get_account(conv);
701 if (account != NULL && purple_account_is_connected(account))
702 pidgin_request_add_block(account, purple_conversation_get_name(conv));
705 static void
706 unblock_cb(GtkWidget *widget, PidginConversation *gtkconv)
708 PurpleConversation *conv = gtkconv->active_conv;
709 PurpleAccount *account;
711 account = purple_conversation_get_account(conv);
713 if (account != NULL && purple_account_is_connected(account))
714 pidgin_request_add_permit(account, purple_conversation_get_name(conv));
717 static void
718 do_invite(GtkWidget *w, int resp, gpointer data)
720 PidginInviteDialog *dialog = PIDGIN_INVITE_DIALOG(w);
721 PurpleChatConversation *chat = pidgin_invite_dialog_get_conversation(dialog);
722 const gchar *contact, *message;
724 if (resp == GTK_RESPONSE_ACCEPT) {
725 contact = pidgin_invite_dialog_get_contact(dialog);
726 if (!g_ascii_strcasecmp(contact, ""))
727 return;
729 message = pidgin_invite_dialog_get_message(dialog);
731 purple_serv_chat_invite(purple_conversation_get_connection(PURPLE_CONVERSATION(chat)),
732 purple_chat_conversation_get_id(chat),
733 message, contact);
736 g_clear_pointer(&invite_dialog, gtk_widget_destroy);
739 static void
740 invite_cb(GtkWidget *widget, PidginConversation *gtkconv)
742 PurpleChatConversation *chat = PURPLE_CHAT_CONVERSATION(gtkconv->active_conv);
744 if (invite_dialog == NULL) {
745 invite_dialog = pidgin_invite_dialog_new(chat);
747 /* Connect the signals. */
748 g_signal_connect(G_OBJECT(invite_dialog), "response",
749 G_CALLBACK(do_invite), NULL);
752 gtk_widget_show_all(invite_dialog);
755 static void
756 menu_new_conv_cb(GtkAction *action, gpointer data)
758 pidgin_dialogs_im();
761 static void
762 menu_join_chat_cb(GtkAction *action, gpointer data)
764 pidgin_blist_joinchat_show();
767 static void
768 savelog_writefile_cb(void *user_data, const char *filename)
770 PurpleConversation *conv = (PurpleConversation *)user_data;
771 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
772 GtkTextBuffer *buffer = NULL;
773 FILE *fp;
774 const char *name;
775 gchar *text;
777 if ((fp = g_fopen(filename, "w+")) == NULL) {
778 purple_notify_error(PIDGIN_CONVERSATION(conv), NULL,
779 _("Unable to open file."), NULL,
780 purple_request_cpar_from_conversation(conv));
781 return;
784 name = purple_conversation_get_name(conv);
786 fprintf(fp, "<html>\n");
787 fprintf(fp, "<head>\n");
788 fprintf(fp, "<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">\n");
789 fprintf(fp, "<title>%s</title>\n", name);
790 fprintf(fp, "</head>\n");
792 fprintf(fp, "<body>\n");
793 fprintf(fp, _("<h1>Conversation with %s</h1>\n"), name);
794 buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(gtkconv->history));
795 text = talkatu_markup_get_html(buffer, NULL);
796 fprintf(fp, "%s", text);
797 g_free(text);
798 fprintf(fp, "\n</body>\n");
800 fprintf(fp, "</html>\n");
801 fclose(fp);
805 * It would be kinda cool if this gave the option of saving a
806 * plaintext v. HTML file.
808 static void
809 menu_save_as_cb(GtkAction *action, gpointer data)
811 PidginConvWindow *win = data;
812 PurpleConversation *conv = pidgin_conv_window_get_active_conversation(win);
813 PurpleAccount *account = purple_conversation_get_account(conv);
814 PurpleBuddy *buddy = purple_blist_find_buddy(account, purple_conversation_get_name(conv));
815 const char *name;
816 gchar *buf;
817 gchar *c;
819 if (buddy != NULL)
820 name = purple_buddy_get_contact_alias(buddy);
821 else
822 name = purple_normalize(account, purple_conversation_get_name(conv));
824 buf = g_strdup_printf("%s.html", name);
825 for (c = buf ; *c ; c++)
827 if (*c == '/' || *c == '\\')
828 *c = ' ';
830 purple_request_file(PIDGIN_CONVERSATION(conv), _("Save Conversation"),
831 buf, TRUE, G_CALLBACK(savelog_writefile_cb), NULL,
832 purple_request_cpar_from_conversation(conv), conv);
834 g_free(buf);
837 static void
838 menu_view_log_cb(GtkAction *action, gpointer data)
840 PidginConvWindow *win = data;
841 PurpleConversation *conv;
842 PurpleLogType type;
843 PidginBuddyList *gtkblist;
844 const char *name;
845 PurpleAccount *account;
846 GSList *buddies;
847 GSList *cur;
849 conv = pidgin_conv_window_get_active_conversation(win);
851 if (PURPLE_IS_IM_CONVERSATION(conv))
852 type = PURPLE_LOG_IM;
853 else if (PURPLE_IS_CHAT_CONVERSATION(conv))
854 type = PURPLE_LOG_CHAT;
855 else
856 return;
858 gtkblist = pidgin_blist_get_default_gtk_blist();
860 pidgin_set_cursor(gtkblist->window, GDK_WATCH);
861 pidgin_set_cursor(win->window, GDK_WATCH);
863 name = purple_conversation_get_name(conv);
864 account = purple_conversation_get_account(conv);
866 buddies = purple_blist_find_buddies(account, name);
867 for (cur = buddies; cur != NULL; cur = cur->next)
869 PurpleBlistNode *node = cur->data;
870 if ((node != NULL) && ((node->prev != NULL) || (node->next != NULL)))
872 pidgin_log_show_contact((PurpleContact *)node->parent);
873 g_slist_free(buddies);
874 pidgin_clear_cursor(gtkblist->window);
875 pidgin_clear_cursor(win->window);
876 return;
879 g_slist_free(buddies);
881 pidgin_log_show(type, name, account);
883 pidgin_clear_cursor(gtkblist->window);
884 pidgin_clear_cursor(win->window);
887 static void
888 menu_clear_cb(GtkAction *action, gpointer data)
890 PidginConvWindow *win = data;
891 PurpleConversation *conv;
893 conv = pidgin_conv_window_get_active_conversation(win);
894 purple_conversation_clear_message_history(conv);
897 static void
898 menu_find_cb(GtkAction *action, gpointer data)
900 PidginConvWindow *gtkwin = data;
901 PidginConversation *gtkconv = pidgin_conv_window_get_active_gtkconv(gtkwin);
902 gtk_widget_show_all(gtkconv->quickfind_container);
903 gtk_widget_grab_focus(gtkconv->quickfind_entry);
906 #ifdef USE_VV
907 static void
908 menu_initiate_media_call_cb(GtkAction *action, gpointer data)
910 PidginConvWindow *win = (PidginConvWindow *)data;
911 PurpleConversation *conv = pidgin_conv_window_get_active_conversation(win);
912 PurpleAccount *account = purple_conversation_get_account(conv);
914 purple_protocol_initiate_media(account,
915 purple_conversation_get_name(conv),
916 action == win->menu->audio_call ? PURPLE_MEDIA_AUDIO :
917 action == win->menu->video_call ? PURPLE_MEDIA_VIDEO :
918 action == win->menu->audio_video_call ? PURPLE_MEDIA_AUDIO |
919 PURPLE_MEDIA_VIDEO : PURPLE_MEDIA_NONE);
921 #endif
923 static void
924 menu_send_file_cb(GtkAction *action, gpointer data)
926 PidginConvWindow *win = data;
927 PurpleConversation *conv = pidgin_conv_window_get_active_conversation(win);
929 if (PURPLE_IS_IM_CONVERSATION(conv)) {
930 purple_serv_send_file(purple_conversation_get_connection(conv), purple_conversation_get_name(conv), NULL);
935 static void
936 menu_get_attention_cb(GObject *obj, gpointer data)
938 PidginConvWindow *win = data;
939 PurpleConversation *conv = pidgin_conv_window_get_active_conversation(win);
941 if (PURPLE_IS_IM_CONVERSATION(conv)) {
942 int index;
943 if ((GtkAction *)obj == win->menu->get_attention)
944 index = 0;
945 else
946 index = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(obj), "index"));
947 purple_protocol_send_attention(purple_conversation_get_connection(conv),
948 purple_conversation_get_name(conv), index);
952 static void
953 menu_add_pounce_cb(GtkAction *action, gpointer data)
955 PidginConvWindow *win = data;
956 PurpleConversation *conv;
958 conv = pidgin_conv_window_get_active_gtkconv(win)->active_conv;
960 pidgin_pounce_editor_show(purple_conversation_get_account(conv),
961 purple_conversation_get_name(conv), NULL);
964 static void
965 menu_alias_cb(GtkAction *action, gpointer data)
967 PidginConvWindow *win = data;
968 PurpleConversation *conv;
969 PurpleAccount *account;
970 const char *name;
972 conv = pidgin_conv_window_get_active_conversation(win);
973 account = purple_conversation_get_account(conv);
974 name = purple_conversation_get_name(conv);
976 if (PURPLE_IS_IM_CONVERSATION(conv)) {
977 PurpleBuddy *b;
979 b = purple_blist_find_buddy(account, name);
980 if (b != NULL)
981 pidgin_dialogs_alias_buddy(b);
982 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
983 PurpleChat *c;
985 c = purple_blist_find_chat(account, name);
986 if (c != NULL)
987 pidgin_dialogs_alias_chat(c);
991 static void
992 menu_get_info_cb(GtkAction *action, gpointer data)
994 PidginConvWindow *win = data;
995 PurpleConversation *conv;
997 conv = pidgin_conv_window_get_active_conversation(win);
999 info_cb(NULL, PIDGIN_CONVERSATION(conv));
1002 static void
1003 menu_invite_cb(GtkAction *action, gpointer data)
1005 PidginConvWindow *win = data;
1006 PurpleConversation *conv;
1008 conv = pidgin_conv_window_get_active_conversation(win);
1010 invite_cb(NULL, PIDGIN_CONVERSATION(conv));
1013 static void
1014 menu_block_cb(GtkAction *action, gpointer data)
1016 PidginConvWindow *win = data;
1017 PurpleConversation *conv;
1019 conv = pidgin_conv_window_get_active_conversation(win);
1021 block_cb(NULL, PIDGIN_CONVERSATION(conv));
1024 static void
1025 menu_unblock_cb(GtkAction *action, gpointer data)
1027 PidginConvWindow *win = data;
1028 PurpleConversation *conv;
1030 conv = pidgin_conv_window_get_active_conversation(win);
1032 unblock_cb(NULL, PIDGIN_CONVERSATION(conv));
1035 static void
1036 menu_add_remove_cb(GtkAction *action, gpointer data)
1038 PidginConvWindow *win = data;
1039 PurpleConversation *conv;
1041 conv = pidgin_conv_window_get_active_conversation(win);
1043 add_remove_cb(NULL, PIDGIN_CONVERSATION(conv));
1046 static gboolean
1047 close_already(gpointer data)
1049 g_object_unref(data);
1050 return FALSE;
1053 static void
1054 hide_conv(PidginConversation *gtkconv, gboolean closetimer)
1056 GList *list;
1058 purple_signal_emit(pidgin_conversations_get_handle(),
1059 "conversation-hiding", gtkconv);
1061 for (list = g_list_copy(gtkconv->convs); list; list = g_list_delete_link(list, list)) {
1062 PurpleConversation *conv = list->data;
1063 if (closetimer) {
1064 guint timer = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(conv), "close-timer"));
1065 if (timer)
1066 g_source_remove(timer);
1067 timer = g_timeout_add_seconds(CLOSE_CONV_TIMEOUT_SECS, close_already, conv);
1068 g_object_set_data(G_OBJECT(conv), "close-timer", GINT_TO_POINTER(timer));
1070 pidgin_conv_window_remove_gtkconv(gtkconv->win, gtkconv);
1071 pidgin_conv_window_add_gtkconv(hidden_convwin, gtkconv);
1075 static void
1076 menu_close_conv_cb(GtkAction *action, gpointer data)
1078 PidginConvWindow *win = data;
1080 close_conv_cb(NULL, PIDGIN_CONVERSATION(pidgin_conv_window_get_active_conversation(win)));
1083 static void
1084 menu_logging_cb(GtkAction *action, gpointer data)
1086 PidginConvWindow *win = data;
1087 PurpleConversation *conv;
1088 gboolean logging;
1089 PurpleBlistNode *node;
1091 conv = pidgin_conv_window_get_active_conversation(win);
1093 if (conv == NULL)
1094 return;
1096 logging = gtk_toggle_action_get_active(GTK_TOGGLE_ACTION(action));
1098 if (logging == purple_conversation_is_logging(conv))
1099 return;
1101 node = get_conversation_blist_node(conv);
1103 if (logging)
1105 /* Enable logging first so the message below can be logged. */
1106 purple_conversation_set_logging(conv, TRUE);
1108 purple_conversation_write_system_message(conv,
1109 _("Logging started. Future messages in this conversation will be logged."), 0);
1111 else
1113 purple_conversation_write_system_message(conv,
1114 _("Logging stopped. Future messages in this conversation will not be logged."), 0);
1116 /* Disable the logging second, so that the above message can be logged. */
1117 purple_conversation_set_logging(conv, FALSE);
1120 /* Save the setting IFF it's different than the pref. */
1121 if (PURPLE_IS_IM_CONVERSATION(conv)) {
1122 if (logging == purple_prefs_get_bool("/purple/logging/log_ims"))
1123 purple_blist_node_remove_setting(node, "enable-logging");
1124 else
1125 purple_blist_node_set_bool(node, "enable-logging", logging);
1126 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
1127 if (logging == purple_prefs_get_bool("/purple/logging/log_chats"))
1128 purple_blist_node_remove_setting(node, "enable-logging");
1129 else
1130 purple_blist_node_set_bool(node, "enable-logging", logging);
1134 static void
1135 menu_toolbar_cb(GtkAction *action, gpointer data)
1137 purple_prefs_set_bool(PIDGIN_PREFS_ROOT "/conversations/show_formatting_toolbar",
1138 gtk_toggle_action_get_active(GTK_TOGGLE_ACTION(action)));
1141 static void
1142 menu_sounds_cb(GtkAction *action, gpointer data)
1144 PidginConvWindow *win = data;
1145 PurpleConversation *conv;
1146 PidginConversation *gtkconv;
1147 PurpleBlistNode *node;
1149 conv = pidgin_conv_window_get_active_conversation(win);
1151 if (!conv)
1152 return;
1154 gtkconv = PIDGIN_CONVERSATION(conv);
1156 gtkconv->make_sound =
1157 gtk_toggle_action_get_active(GTK_TOGGLE_ACTION(action));
1158 node = get_conversation_blist_node(conv);
1159 if (node)
1160 purple_blist_node_set_bool(node, "gtk-mute-sound", !gtkconv->make_sound);
1163 static void
1164 chat_do_im(PidginConversation *gtkconv, const char *who)
1166 PurpleConversation *conv = gtkconv->active_conv;
1167 PurpleAccount *account;
1168 PurpleConnection *gc;
1169 PurpleProtocol *protocol = NULL;
1170 gchar *real_who = NULL;
1172 account = purple_conversation_get_account(conv);
1173 g_return_if_fail(account != NULL);
1175 gc = purple_account_get_connection(account);
1176 g_return_if_fail(gc != NULL);
1178 protocol = purple_connection_get_protocol(gc);
1180 if (protocol)
1181 real_who = purple_protocol_chat_iface_get_user_real_name(protocol, gc,
1182 purple_chat_conversation_get_id(PURPLE_CHAT_CONVERSATION(conv)), who);
1184 if(!who && !real_who)
1185 return;
1187 pidgin_dialogs_im_with_user(account, real_who ? real_who : who);
1189 g_free(real_who);
1192 static void pidgin_conv_chat_update_user(PurpleChatUser *chatuser);
1194 static void
1195 ignore_cb(GtkWidget *w, PidginConversation *gtkconv)
1197 PurpleChatConversation *chat = PURPLE_CHAT_CONVERSATION(gtkconv->active_conv);
1198 const char *name;
1200 name = g_object_get_data(G_OBJECT(w), "user_data");
1202 if (name == NULL)
1203 return;
1205 if (purple_chat_conversation_is_ignored_user(chat, name))
1206 purple_chat_conversation_unignore(chat, name);
1207 else
1208 purple_chat_conversation_ignore(chat, name);
1210 pidgin_conv_chat_update_user(purple_chat_conversation_find_user(chat, name));
1213 static void
1214 menu_chat_im_cb(GtkWidget *w, PidginConversation *gtkconv)
1216 const char *who = g_object_get_data(G_OBJECT(w), "user_data");
1218 chat_do_im(gtkconv, who);
1221 static void
1222 menu_chat_send_file_cb(GtkWidget *w, PidginConversation *gtkconv)
1224 PurpleProtocol *protocol;
1225 PurpleConversation *conv = gtkconv->active_conv;
1226 const char *who = g_object_get_data(G_OBJECT(w), "user_data");
1227 PurpleConnection *gc = purple_conversation_get_connection(conv);
1228 gchar *real_who = NULL;
1230 g_return_if_fail(gc != NULL);
1232 protocol = purple_connection_get_protocol(gc);
1234 if (protocol)
1235 real_who = purple_protocol_chat_iface_get_user_real_name(protocol, gc,
1236 purple_chat_conversation_get_id(PURPLE_CHAT_CONVERSATION(conv)), who);
1238 purple_serv_send_file(gc, real_who ? real_who : who, NULL);
1239 g_free(real_who);
1242 static void
1243 menu_chat_info_cb(GtkWidget *w, PidginConversation *gtkconv)
1245 char *who;
1247 who = g_object_get_data(G_OBJECT(w), "user_data");
1249 chat_do_info(gtkconv, who);
1252 static void
1253 menu_chat_add_remove_cb(GtkWidget *w, PidginConversation *gtkconv)
1255 PurpleConversation *conv = gtkconv->active_conv;
1256 PurpleAccount *account;
1257 PurpleBuddy *b;
1258 char *name;
1260 account = purple_conversation_get_account(conv);
1261 name = g_object_get_data(G_OBJECT(w), "user_data");
1262 b = purple_blist_find_buddy(account, name);
1264 if (b != NULL)
1265 pidgin_dialogs_remove_buddy(b);
1266 else if (account != NULL && purple_account_is_connected(account))
1267 purple_blist_request_add_buddy(account, name, NULL, NULL);
1269 gtk_widget_grab_focus(PIDGIN_CONVERSATION(conv)->entry);
1272 static GtkWidget *
1273 create_chat_menu(PurpleChatConversation *chat, const char *who, PurpleConnection *gc)
1275 static GtkWidget *menu = NULL;
1276 PurpleProtocol *protocol = NULL;
1277 PurpleConversation *conv = PURPLE_CONVERSATION(chat);
1278 PurpleAccount *account = purple_conversation_get_account(conv);
1279 gboolean is_me = FALSE;
1280 GtkWidget *button;
1281 PurpleBuddy *buddy = NULL;
1283 if (gc != NULL)
1284 protocol = purple_connection_get_protocol(gc);
1287 * If a menu already exists, destroy it before creating a new one,
1288 * thus freeing-up the memory it occupied.
1290 if (menu)
1291 gtk_widget_destroy(menu);
1293 if (purple_strequal(purple_chat_conversation_get_nick(chat), purple_normalize(account, who)))
1294 is_me = TRUE;
1296 menu = gtk_menu_new();
1298 if (!is_me) {
1299 button = pidgin_new_menu_item(menu, _("IM"),
1300 PIDGIN_STOCK_TOOLBAR_MESSAGE_NEW,
1301 G_CALLBACK(menu_chat_im_cb),
1302 PIDGIN_CONVERSATION(conv));
1304 if (gc == NULL)
1305 gtk_widget_set_sensitive(button, FALSE);
1306 else
1307 g_object_set_data_full(G_OBJECT(button), "user_data", g_strdup(who), g_free);
1310 if (protocol && PURPLE_IS_PROTOCOL_XFER(protocol))
1312 gboolean can_receive_file = TRUE;
1314 button = pidgin_new_menu_item(menu, _("Send File"),
1315 PIDGIN_STOCK_TOOLBAR_SEND_FILE, G_CALLBACK(menu_chat_send_file_cb),
1316 PIDGIN_CONVERSATION(conv));
1318 if (gc == NULL) {
1319 can_receive_file = FALSE;
1320 } else {
1321 gchar *real_who = NULL;
1322 real_who = purple_protocol_chat_iface_get_user_real_name(protocol, gc,
1323 purple_chat_conversation_get_id(chat), who);
1325 if (!purple_protocol_xfer_can_receive(
1326 PURPLE_PROTOCOL_XFER(protocol),
1327 gc, real_who ? real_who : who)) {
1328 can_receive_file = FALSE;
1331 g_free(real_who);
1334 if (!can_receive_file)
1335 gtk_widget_set_sensitive(button, FALSE);
1336 else
1337 g_object_set_data_full(G_OBJECT(button), "user_data", g_strdup(who), g_free);
1341 if (purple_chat_conversation_is_ignored_user(chat, who))
1342 button = pidgin_new_menu_item(menu, _("Un-Ignore"),
1343 PIDGIN_STOCK_IGNORE, G_CALLBACK(ignore_cb),
1344 PIDGIN_CONVERSATION(conv));
1345 else
1346 button = pidgin_new_menu_item(menu, _("Ignore"),
1347 PIDGIN_STOCK_IGNORE, G_CALLBACK(ignore_cb),
1348 PIDGIN_CONVERSATION(conv));
1350 if (gc == NULL)
1351 gtk_widget_set_sensitive(button, FALSE);
1352 else
1353 g_object_set_data_full(G_OBJECT(button), "user_data", g_strdup(who), g_free);
1356 if (protocol && PURPLE_PROTOCOL_IMPLEMENTS(protocol, SERVER, get_info)) {
1357 button = pidgin_new_menu_item(menu, _("Info"),
1358 PIDGIN_STOCK_TOOLBAR_USER_INFO,
1359 G_CALLBACK(menu_chat_info_cb),
1360 PIDGIN_CONVERSATION(conv));
1362 if (gc == NULL)
1363 gtk_widget_set_sensitive(button, FALSE);
1364 else
1365 g_object_set_data_full(G_OBJECT(button), "user_data", g_strdup(who), g_free);
1368 if (!is_me && protocol && !(purple_protocol_get_options(protocol) & OPT_PROTO_UNIQUE_CHATNAME) && PURPLE_PROTOCOL_IMPLEMENTS(protocol, SERVER, add_buddy)) {
1369 if ((buddy = purple_blist_find_buddy(account, who)) != NULL)
1370 button = pidgin_new_menu_item(menu, _("Remove"),
1371 GTK_STOCK_REMOVE,
1372 G_CALLBACK(menu_chat_add_remove_cb),
1373 PIDGIN_CONVERSATION(conv));
1374 else
1375 button = pidgin_new_menu_item(menu, _("Add"),
1376 GTK_STOCK_ADD,
1377 G_CALLBACK(menu_chat_add_remove_cb),
1378 PIDGIN_CONVERSATION(conv));
1380 if (gc == NULL)
1381 gtk_widget_set_sensitive(button, FALSE);
1382 else
1383 g_object_set_data_full(G_OBJECT(button), "user_data", g_strdup(who), g_free);
1386 if (buddy != NULL)
1388 if (purple_account_is_connected(account))
1389 pidgin_append_blist_node_proto_menu(menu, purple_account_get_connection(account),
1390 (PurpleBlistNode *)buddy);
1391 pidgin_append_blist_node_extended_menu(menu, (PurpleBlistNode *)buddy);
1392 gtk_widget_show_all(menu);
1395 return menu;
1399 static gint
1400 gtkconv_chat_popup_menu_cb(GtkWidget *widget, PidginConversation *gtkconv)
1402 PurpleConversation *conv = gtkconv->active_conv;
1403 PidginChatPane *gtkchat;
1404 PurpleConnection *gc;
1405 PurpleAccount *account;
1406 GtkTreeSelection *sel;
1407 GtkTreeIter iter;
1408 GtkTreeModel *model;
1409 GtkWidget *menu;
1410 gchar *who;
1412 gtkconv = PIDGIN_CONVERSATION(conv);
1413 gtkchat = gtkconv->u.chat;
1414 account = purple_conversation_get_account(conv);
1415 gc = purple_account_get_connection(account);
1417 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
1419 sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(gtkchat->list));
1420 if(!gtk_tree_selection_get_selected(sel, NULL, &iter))
1421 return FALSE;
1423 gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, CHAT_USERS_NAME_COLUMN, &who, -1);
1424 menu = create_chat_menu (PURPLE_CHAT_CONVERSATION(conv), who, gc);
1425 pidgin_menu_popup_at_treeview_selection(menu, widget);
1426 g_free(who);
1428 return TRUE;
1432 static gint
1433 right_click_chat_cb(GtkWidget *widget, GdkEventButton *event,
1434 PidginConversation *gtkconv)
1436 PurpleConversation *conv = gtkconv->active_conv;
1437 PidginChatPane *gtkchat;
1438 PurpleConnection *gc;
1439 PurpleAccount *account;
1440 GtkTreePath *path;
1441 GtkTreeIter iter;
1442 GtkTreeModel *model;
1443 GtkTreeViewColumn *column;
1444 gchar *who;
1445 int x, y;
1447 gtkchat = gtkconv->u.chat;
1448 account = purple_conversation_get_account(conv);
1449 gc = purple_account_get_connection(account);
1451 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
1453 gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(gtkchat->list),
1454 event->x, event->y, &path, &column, &x, &y);
1456 if (path == NULL)
1457 return FALSE;
1459 gtk_tree_selection_select_path(GTK_TREE_SELECTION(
1460 gtk_tree_view_get_selection(GTK_TREE_VIEW(gtkchat->list))), path);
1461 gtk_tree_view_set_cursor(GTK_TREE_VIEW(gtkchat->list),
1462 path, NULL, FALSE);
1463 gtk_widget_grab_focus(GTK_WIDGET(gtkchat->list));
1465 gtk_tree_model_get_iter(GTK_TREE_MODEL(model), &iter, path);
1466 gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, CHAT_USERS_NAME_COLUMN, &who, -1);
1468 /* emit chat-nick-clicked signal */
1469 if (event->type == GDK_BUTTON_PRESS) {
1470 gint plugin_return = GPOINTER_TO_INT(purple_signal_emit_return_1(
1471 pidgin_conversations_get_handle(), "chat-nick-clicked",
1472 conv, who, event->button));
1473 if (plugin_return)
1474 goto handled;
1477 if (event->button == GDK_BUTTON_PRIMARY && event->type == GDK_2BUTTON_PRESS) {
1478 chat_do_im(gtkconv, who);
1479 } else if (gdk_event_triggers_context_menu((GdkEvent *)event)) {
1480 GtkWidget *menu = create_chat_menu (PURPLE_CHAT_CONVERSATION(conv), who, gc);
1481 gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent *)event);
1484 handled:
1485 g_free(who);
1486 gtk_tree_path_free(path);
1488 return TRUE;
1491 static void
1492 activate_list_cb(GtkTreeView *list, GtkTreePath *path, GtkTreeViewColumn *column, PidginConversation *gtkconv)
1494 GtkTreeIter iter;
1495 GtkTreeModel *model;
1496 gchar *who;
1498 model = gtk_tree_view_get_model(GTK_TREE_VIEW(list));
1500 gtk_tree_model_get_iter(GTK_TREE_MODEL(model), &iter, path);
1501 gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, CHAT_USERS_NAME_COLUMN, &who, -1);
1502 chat_do_im(gtkconv, who);
1504 g_free(who);
1507 static void
1508 move_to_next_unread_tab(PidginConversation *gtkconv, gboolean forward)
1510 PidginConversation *next_gtkconv = NULL, *most_active = NULL;
1511 PidginUnseenState unseen_state = PIDGIN_UNSEEN_NONE;
1512 PidginConvWindow *win;
1513 int initial, i, total, diff;
1515 win = gtkconv->win;
1516 initial = gtk_notebook_page_num(GTK_NOTEBOOK(win->notebook),
1517 gtkconv->tab_cont);
1518 total = pidgin_conv_window_get_gtkconv_count(win);
1519 /* By adding total here, the moduli calculated later will always have two
1520 * positive arguments. x % y where x < 0 is not guaranteed to return a
1521 * positive number.
1523 diff = (forward ? 1 : -1) + total;
1525 for (i = (initial + diff) % total; i != initial; i = (i + diff) % total) {
1526 next_gtkconv = pidgin_conv_window_get_gtkconv_at_index(win, i);
1527 if (next_gtkconv->unseen_state > unseen_state) {
1528 most_active = next_gtkconv;
1529 unseen_state = most_active->unseen_state;
1530 if(PIDGIN_UNSEEN_NICK == unseen_state) /* highest possible state */
1531 break;
1535 if (most_active == NULL) { /* no new messages */
1536 i = (i + diff) % total;
1537 most_active = pidgin_conv_window_get_gtkconv_at_index(win, i);
1540 if (most_active != NULL && most_active != gtkconv)
1541 pidgin_conv_window_switch_gtkconv(win, most_active);
1544 static gboolean
1545 gtkconv_cycle_focus(PidginConversation *gtkconv, GtkDirectionType dir)
1547 PurpleConversation *conv = gtkconv->active_conv;
1548 gboolean chat = PURPLE_IS_CHAT_CONVERSATION(conv);
1549 GtkWidget *next = NULL;
1550 struct {
1551 GtkWidget *from;
1552 GtkWidget *to;
1553 } transitions[] = {
1554 {gtkconv->entry, gtkconv->history},
1555 {gtkconv->history, chat ? gtkconv->u.chat->list : gtkconv->entry},
1556 {chat ? gtkconv->u.chat->list : NULL, gtkconv->entry},
1557 {NULL, NULL}
1558 }, *ptr;
1560 for (ptr = transitions; !next && ptr->from; ptr++) {
1561 GtkWidget *from, *to;
1562 if (dir == GTK_DIR_TAB_FORWARD) {
1563 from = ptr->from;
1564 to = ptr->to;
1565 } else {
1566 from = ptr->to;
1567 to = ptr->from;
1569 if (gtk_widget_is_focus(from))
1570 next = to;
1573 if (next)
1574 gtk_widget_grab_focus(next);
1575 return !!next;
1578 static void
1579 update_typing_inserting(PidginConversation *gtkconv)
1581 GtkTextBuffer *buffer = NULL;
1582 gboolean is_empty = FALSE;
1584 g_return_if_fail(gtkconv != NULL);
1586 buffer = talkatu_editor_get_buffer(TALKATU_EDITOR(gtkconv->editor));
1587 is_empty = talkatu_buffer_get_is_empty(TALKATU_BUFFER(buffer));
1589 got_typing_keypress(gtkconv, is_empty);
1592 static gboolean
1593 update_typing_deleting_cb(PidginConversation *gtkconv)
1595 PurpleIMConversation *im = PURPLE_IM_CONVERSATION(gtkconv->active_conv);
1596 GtkTextBuffer *buffer = NULL;
1598 buffer = talkatu_editor_get_buffer(TALKATU_EDITOR(gtkconv->editor));
1600 if (!talkatu_buffer_get_is_empty(TALKATU_BUFFER(buffer))) {
1601 /* We deleted all the text, so turn off typing. */
1602 purple_im_conversation_stop_send_typed_timeout(im);
1604 purple_serv_send_typing(purple_conversation_get_connection(gtkconv->active_conv),
1605 purple_conversation_get_name(gtkconv->active_conv),
1606 PURPLE_IM_NOT_TYPING);
1607 } else {
1608 /* We're deleting, but not all of it, so it counts as typing. */
1609 got_typing_keypress(gtkconv, FALSE);
1612 return FALSE;
1615 static void
1616 update_typing_deleting(PidginConversation *gtkconv)
1618 GtkTextBuffer *buffer = NULL;
1620 g_return_if_fail(gtkconv != NULL);
1622 buffer = talkatu_editor_get_buffer(TALKATU_EDITOR(gtkconv->editor));
1624 if (!talkatu_buffer_get_is_empty(TALKATU_BUFFER(buffer))) {
1625 g_timeout_add(0, (GSourceFunc)update_typing_deleting_cb, gtkconv);
1629 static gboolean
1630 conv_keypress_common(PidginConversation *gtkconv, GdkEventKey *event)
1632 PidginConvWindow *win;
1633 int curconv;
1635 win = gtkconv->win;
1636 curconv = gtk_notebook_get_current_page(GTK_NOTEBOOK(win->notebook));
1638 /* clear any tooltips */
1639 pidgin_tooltip_destroy();
1641 /* If CTRL was held down... */
1642 if (event->state & GDK_CONTROL_MASK) {
1643 switch (event->keyval) {
1644 case GDK_KEY_Page_Down:
1645 case GDK_KEY_KP_Page_Down:
1646 case ']':
1647 if (!pidgin_conv_window_get_gtkconv_at_index(win, curconv + 1))
1648 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), 0);
1649 else
1650 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), curconv + 1);
1651 return TRUE;
1652 break;
1654 case GDK_KEY_Page_Up:
1655 case GDK_KEY_KP_Page_Up:
1656 case '[':
1657 if (!pidgin_conv_window_get_gtkconv_at_index(win, curconv - 1))
1658 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), -1);
1659 else
1660 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), curconv - 1);
1661 return TRUE;
1662 break;
1664 case GDK_KEY_Tab:
1665 case GDK_KEY_KP_Tab:
1666 case GDK_KEY_ISO_Left_Tab:
1667 if (event->state & GDK_SHIFT_MASK) {
1668 move_to_next_unread_tab(gtkconv, FALSE);
1669 } else {
1670 move_to_next_unread_tab(gtkconv, TRUE);
1673 return TRUE;
1674 break;
1676 case GDK_KEY_comma:
1677 gtk_notebook_reorder_child(GTK_NOTEBOOK(win->notebook),
1678 gtk_notebook_get_nth_page(GTK_NOTEBOOK(win->notebook), curconv),
1679 curconv - 1);
1680 return TRUE;
1681 break;
1683 case GDK_KEY_period:
1684 gtk_notebook_reorder_child(GTK_NOTEBOOK(win->notebook),
1685 gtk_notebook_get_nth_page(GTK_NOTEBOOK(win->notebook), curconv),
1686 (curconv + 1) % gtk_notebook_get_n_pages(GTK_NOTEBOOK(win->notebook)));
1687 return TRUE;
1688 break;
1689 case GDK_KEY_F6:
1690 if (gtkconv_cycle_focus(gtkconv, event->state & GDK_SHIFT_MASK ? GTK_DIR_TAB_BACKWARD : GTK_DIR_TAB_FORWARD))
1691 return TRUE;
1692 break;
1693 } /* End of switch */
1696 /* If ALT (or whatever) was held down... */
1697 else if (event->state & GDK_MOD1_MASK)
1699 if (event->keyval > '0' && event->keyval <= '9')
1701 guint switchto = event->keyval - '1';
1702 if (switchto < pidgin_conv_window_get_gtkconv_count(win))
1703 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), switchto);
1705 return TRUE;
1709 /* If neither CTRL nor ALT were held down... */
1710 else
1712 switch (event->keyval) {
1713 case GDK_KEY_F2:
1714 if (gtk_widget_is_focus(GTK_WIDGET(win->notebook))) {
1715 infopane_entry_activate(gtkconv);
1716 return TRUE;
1718 break;
1719 case GDK_KEY_F6:
1720 if (gtkconv_cycle_focus(gtkconv, event->state & GDK_SHIFT_MASK ? GTK_DIR_TAB_BACKWARD : GTK_DIR_TAB_FORWARD))
1721 return TRUE;
1722 break;
1725 return FALSE;
1728 static gboolean
1729 entry_key_press_cb(GtkWidget *entry, GdkEventKey *event, gpointer data)
1731 PurpleConversation *conv;
1732 PidginConversation *gtkconv;
1734 gtkconv = (PidginConversation *)data;
1735 conv = gtkconv->active_conv;
1737 if (conv_keypress_common(gtkconv, event))
1738 return TRUE;
1740 /* If CTRL was held down... */
1741 if (event->state & GDK_CONTROL_MASK) {
1743 /* If ALT (or whatever) was held down... */
1744 else if (event->state & GDK_MOD1_MASK) {
1747 /* If neither CTRL nor ALT were held down... */
1748 else {
1749 switch (event->keyval) {
1750 case GDK_KEY_Tab:
1751 case GDK_KEY_KP_Tab:
1752 case GDK_KEY_ISO_Left_Tab:
1753 if (gtkconv->entry != entry)
1754 break;
1756 gint plugin_return;
1757 plugin_return = GPOINTER_TO_INT(purple_signal_emit_return_1(
1758 pidgin_conversations_get_handle(), "chat-nick-autocomplete",
1759 conv, event->state & GDK_SHIFT_MASK));
1760 return plugin_return;
1762 break;
1764 case GDK_KEY_Page_Up:
1765 case GDK_KEY_KP_Page_Up:
1766 talkatu_history_page_up(TALKATU_HISTORY(gtkconv->history));
1767 return TRUE;
1768 break;
1770 case GDK_KEY_Page_Down:
1771 case GDK_KEY_KP_Page_Down:
1772 talkatu_history_page_down(TALKATU_HISTORY(gtkconv->history));
1773 return TRUE;
1774 break;
1776 case GDK_KEY_KP_Enter:
1777 case GDK_KEY_Return:
1778 send_cb(entry, gtkconv);
1779 return TRUE;
1780 break;
1785 if (PURPLE_IS_IM_CONVERSATION(conv) &&
1786 purple_prefs_get_bool("/purple/conversations/im/send_typing")) {
1788 switch (event->keyval) {
1789 case GDK_KEY_BackSpace:
1790 case GDK_KEY_Delete:
1791 case GDK_KEY_KP_Delete:
1792 update_typing_deleting(gtkconv);
1793 break;
1794 default:
1795 update_typing_inserting(gtkconv);
1799 return FALSE;
1803 * If someone tries to type into the conversation backlog of a
1804 * conversation window then we yank focus from the conversation backlog
1805 * and give it to the text entry box so that people can type
1806 * all the live long day and it will get entered into the entry box.
1808 static gboolean
1809 refocus_entry_cb(GtkWidget *widget, GdkEventKey *event, gpointer data)
1811 GtkWidget *view = NULL;
1812 PidginConversation *gtkconv = data;
1814 /* If we have a valid key for the conversation display, then exit */
1815 if ((event->state & GDK_CONTROL_MASK) ||
1816 (event->keyval == GDK_KEY_F6) ||
1817 (event->keyval == GDK_KEY_F10) ||
1818 (event->keyval == GDK_KEY_Menu) ||
1819 (event->keyval == GDK_KEY_Shift_L) ||
1820 (event->keyval == GDK_KEY_Shift_R) ||
1821 (event->keyval == GDK_KEY_Control_L) ||
1822 (event->keyval == GDK_KEY_Control_R) ||
1823 (event->keyval == GDK_KEY_Escape) ||
1824 (event->keyval == GDK_KEY_Up) ||
1825 (event->keyval == GDK_KEY_Down) ||
1826 (event->keyval == GDK_KEY_Left) ||
1827 (event->keyval == GDK_KEY_Right) ||
1828 (event->keyval == GDK_KEY_Page_Up) ||
1829 (event->keyval == GDK_KEY_KP_Page_Up) ||
1830 (event->keyval == GDK_KEY_Page_Down) ||
1831 (event->keyval == GDK_KEY_KP_Page_Down) ||
1832 (event->keyval == GDK_KEY_Home) ||
1833 (event->keyval == GDK_KEY_End) ||
1834 (event->keyval == GDK_KEY_Tab) ||
1835 (event->keyval == GDK_KEY_KP_Tab) ||
1836 (event->keyval == GDK_KEY_ISO_Left_Tab))
1838 if (event->type == GDK_KEY_PRESS)
1839 return conv_keypress_common(gtkconv, event);
1840 return FALSE;
1843 view = talkatu_editor_get_view(TALKATU_EDITOR(gtkconv->editor));
1844 gtk_widget_grab_focus(view);
1845 gtk_widget_event(view, (GdkEvent *)event);
1847 return TRUE;
1850 static void
1851 regenerate_options_items(PidginConvWindow *win);
1853 void
1854 pidgin_conv_switch_active_conversation(PurpleConversation *conv)
1856 PidginConversation *gtkconv;
1857 PurpleConversation *old_conv;
1858 PurpleConnectionFlags features;
1860 g_return_if_fail(conv != NULL);
1862 gtkconv = PIDGIN_CONVERSATION(conv);
1863 old_conv = gtkconv->active_conv;
1865 purple_debug_info("gtkconv", "setting active conversation on toolbar %p\n",
1866 conv);
1868 if (old_conv == conv)
1869 return;
1871 purple_conversation_close_logs(old_conv);
1872 gtkconv->active_conv = conv;
1874 purple_conversation_set_logging(conv,
1875 gtk_toggle_action_get_active(GTK_TOGGLE_ACTION(gtkconv->win->menu->logging)));
1877 purple_signal_emit(pidgin_conversations_get_handle(), "conversation-switched", conv);
1879 gray_stuff_out(gtkconv);
1880 update_typing_icon(gtkconv);
1881 g_object_set_data(G_OBJECT(gtkconv->entry), "transient_buddy", NULL);
1882 regenerate_options_items(gtkconv->win);
1884 gtk_window_set_title(GTK_WINDOW(gtkconv->win->window),
1885 gtk_label_get_text(GTK_LABEL(gtkconv->tab_label)));
1888 static void
1889 menu_conv_sel_send_cb(GObject *m, gpointer data)
1891 PurpleAccount *account = g_object_get_data(m, "purple_account");
1892 gchar *name = g_object_get_data(m, "purple_buddy_name");
1893 PurpleIMConversation *im;
1895 if (gtk_check_menu_item_get_active((GtkCheckMenuItem*) m) == FALSE)
1896 return;
1898 im = purple_im_conversation_new(account, name);
1899 pidgin_conv_switch_active_conversation(PURPLE_CONVERSATION(im));
1902 /**************************************************************************
1903 * A bunch of buddy icon functions
1904 **************************************************************************/
1906 static GList *get_protocol_icon_list(PurpleAccount *account)
1908 GList *l = NULL;
1909 PurpleProtocol *protocol =
1910 purple_protocols_find(purple_account_get_protocol_id(account));
1911 const char *protoname = purple_protocol_class_list_icon(protocol, account, NULL);
1912 l = g_hash_table_lookup(protocol_lists, protoname);
1913 if (l)
1914 return l;
1916 l = g_list_prepend(l, pidgin_create_protocol_icon(account, PIDGIN_PROTOCOL_ICON_LARGE));
1917 l = g_list_prepend(l, pidgin_create_protocol_icon(account, PIDGIN_PROTOCOL_ICON_MEDIUM));
1918 l = g_list_prepend(l, pidgin_create_protocol_icon(account, PIDGIN_PROTOCOL_ICON_SMALL));
1920 g_hash_table_insert(protocol_lists, g_strdup(protoname), l);
1921 return l;
1924 static GList *
1925 pidgin_conv_get_tab_icons(PurpleConversation *conv)
1927 PurpleAccount *account = NULL;
1928 const char *name = NULL;
1930 g_return_val_if_fail(conv != NULL, NULL);
1932 account = purple_conversation_get_account(conv);
1933 name = purple_conversation_get_name(conv);
1935 g_return_val_if_fail(account != NULL, NULL);
1936 g_return_val_if_fail(name != NULL, NULL);
1938 /* Use the buddy icon, if possible */
1939 if (PURPLE_IS_IM_CONVERSATION(conv)) {
1940 PurpleBuddy *b = purple_blist_find_buddy(account, name);
1941 if (b != NULL) {
1942 PurplePresence *p;
1943 p = purple_buddy_get_presence(b);
1944 if (purple_presence_is_status_primitive_active(p, PURPLE_STATUS_AWAY))
1945 return away_list;
1946 if (purple_presence_is_status_primitive_active(p, PURPLE_STATUS_UNAVAILABLE))
1947 return busy_list;
1948 if (purple_presence_is_status_primitive_active(p, PURPLE_STATUS_EXTENDED_AWAY))
1949 return xa_list;
1950 if (purple_presence_is_status_primitive_active(p, PURPLE_STATUS_OFFLINE))
1951 return offline_list;
1952 else
1953 return available_list;
1957 return get_protocol_icon_list(account);
1960 static const char *
1961 pidgin_conv_get_icon_stock(PurpleConversation *conv)
1963 PurpleAccount *account = NULL;
1964 const char *stock = NULL;
1966 g_return_val_if_fail(conv != NULL, NULL);
1968 account = purple_conversation_get_account(conv);
1969 g_return_val_if_fail(account != NULL, NULL);
1971 /* Use the buddy icon, if possible */
1972 if (PURPLE_IS_IM_CONVERSATION(conv)) {
1973 const char *name = NULL;
1974 PurpleBuddy *b;
1975 name = purple_conversation_get_name(conv);
1976 b = purple_blist_find_buddy(account, name);
1977 if (b != NULL) {
1978 PurplePresence *p = purple_buddy_get_presence(b);
1979 PurpleStatus *active = purple_presence_get_active_status(p);
1980 PurpleStatusType *type = purple_status_get_status_type(active);
1981 PurpleStatusPrimitive prim = purple_status_type_get_primitive(type);
1982 stock = pidgin_stock_id_from_status_primitive(prim);
1983 } else {
1984 stock = PIDGIN_STOCK_STATUS_PERSON;
1986 } else {
1987 stock = PIDGIN_STOCK_STATUS_CHAT;
1990 return stock;
1993 static GdkPixbuf *
1994 pidgin_conv_get_icon(PurpleConversation *conv, GtkWidget *parent, const char *icon_size)
1996 PurpleAccount *account = NULL;
1997 const char *name = NULL;
1998 const char *stock = NULL;
1999 GdkPixbuf *status = NULL;
2000 GtkIconSize size;
2002 g_return_val_if_fail(conv != NULL, NULL);
2004 account = purple_conversation_get_account(conv);
2005 name = purple_conversation_get_name(conv);
2007 g_return_val_if_fail(account != NULL, NULL);
2008 g_return_val_if_fail(name != NULL, NULL);
2010 /* Use the buddy icon, if possible */
2011 if (PURPLE_IS_IM_CONVERSATION(conv)) {
2012 PurpleBuddy *b = purple_blist_find_buddy(account, name);
2013 if (b != NULL) {
2014 /* I hate this hack. It fixes a bug where the pending message icon
2015 * displays in the conv tab even though it shouldn't.
2016 * A better solution would be great. */
2017 purple_blist_update_node(NULL, PURPLE_BLIST_NODE(b));
2021 stock = pidgin_conv_get_icon_stock(conv);
2022 size = gtk_icon_size_from_name(icon_size);
2023 status = gtk_widget_render_icon (parent, stock, size, "GtkWidget");
2024 return status;
2027 GdkPixbuf *
2028 pidgin_conv_get_tab_icon(PurpleConversation *conv, gboolean small_icon)
2030 const char *icon_size = small_icon ? PIDGIN_ICON_SIZE_TANGO_MICROSCOPIC : PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL;
2031 return pidgin_conv_get_icon(conv, PIDGIN_CONVERSATION(conv)->icon, icon_size);
2035 static void
2036 update_tab_icon(PurpleConversation *conv)
2038 PidginConversation *gtkconv;
2039 PidginConvWindow *win;
2040 GList *l;
2041 GdkPixbuf *emblem = NULL;
2042 const char *status = NULL;
2043 const char *infopane_status = NULL;
2045 g_return_if_fail(conv != NULL);
2047 gtkconv = PIDGIN_CONVERSATION(conv);
2048 win = gtkconv->win;
2049 if (conv != gtkconv->active_conv)
2050 return;
2052 status = infopane_status = pidgin_conv_get_icon_stock(conv);
2054 if (PURPLE_IS_IM_CONVERSATION(conv)) {
2055 PurpleBuddy *b = purple_blist_find_buddy(purple_conversation_get_account(conv), purple_conversation_get_name(conv));
2056 if (b)
2057 emblem = pidgin_blist_get_emblem((PurpleBlistNode*)b);
2060 g_return_if_fail(status != NULL);
2062 g_object_set(G_OBJECT(gtkconv->icon), "stock", status, NULL);
2063 g_object_set(G_OBJECT(gtkconv->menu_icon), "stock", status, NULL);
2065 gtk_list_store_set(GTK_LIST_STORE(gtkconv->infopane_model),
2066 &(gtkconv->infopane_iter),
2067 CONV_ICON_COLUMN, infopane_status, -1);
2069 gtk_list_store_set(GTK_LIST_STORE(gtkconv->infopane_model),
2070 &(gtkconv->infopane_iter),
2071 CONV_EMBLEM_COLUMN, emblem, -1);
2072 if (emblem)
2073 g_object_unref(emblem);
2075 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/blist/show_protocol_icons")) {
2076 emblem = pidgin_create_protocol_icon(purple_conversation_get_account(gtkconv->active_conv), PIDGIN_PROTOCOL_ICON_SMALL);
2077 } else {
2078 emblem = NULL;
2081 gtk_list_store_set(GTK_LIST_STORE(gtkconv->infopane_model),
2082 &(gtkconv->infopane_iter),
2083 CONV_PROTOCOL_ICON_COLUMN, emblem, -1);
2084 if (emblem)
2085 g_object_unref(emblem);
2087 /* XXX seanegan Why do I have to do this? */
2088 gtk_widget_queue_resize(gtkconv->infopane);
2089 gtk_widget_queue_draw(gtkconv->infopane);
2091 if (pidgin_conv_window_is_active_conversation(conv) &&
2092 (!PURPLE_IS_IM_CONVERSATION(conv) || gtkconv->u.im->anim == NULL))
2094 l = pidgin_conv_get_tab_icons(conv);
2096 gtk_window_set_icon_list(GTK_WINDOW(win->window), l);
2100 static gboolean
2101 redraw_icon(gpointer data)
2103 PidginConversation *gtkconv = (PidginConversation *)data;
2104 PurpleConversation *conv = gtkconv->active_conv;
2105 PurpleAccount *account;
2107 GdkPixbuf *buf;
2108 GdkPixbuf *scale;
2109 gint delay;
2110 int scale_width, scale_height;
2111 int size;
2113 gtkconv = PIDGIN_CONVERSATION(conv);
2114 account = purple_conversation_get_account(conv);
2116 if (!(account && purple_account_get_connection(account))) {
2117 gtkconv->u.im->icon_timer = 0;
2118 return FALSE;
2121 gdk_pixbuf_animation_iter_advance(gtkconv->u.im->iter, NULL);
2122 buf = gdk_pixbuf_animation_iter_get_pixbuf(gtkconv->u.im->iter);
2124 scale_width = gdk_pixbuf_get_width(buf);
2125 scale_height = gdk_pixbuf_get_height(buf);
2127 gtk_widget_get_size_request(gtkconv->u.im->icon_container, NULL, &size);
2128 size = MIN(size, MIN(scale_width, scale_height));
2129 size = CLAMP(size, BUDDYICON_SIZE_MIN, BUDDYICON_SIZE_MAX);
2131 if (scale_width == scale_height) {
2132 scale_width = scale_height = size;
2133 } else if (scale_height > scale_width) {
2134 scale_width = size * scale_width / scale_height;
2135 scale_height = size;
2136 } else {
2137 scale_height = size * scale_height / scale_width;
2138 scale_width = size;
2141 scale = gdk_pixbuf_scale_simple(buf, scale_width, scale_height,
2142 GDK_INTERP_BILINEAR);
2143 if (pidgin_gdk_pixbuf_is_opaque(scale))
2144 pidgin_gdk_pixbuf_make_round(scale);
2146 gtk_image_set_from_pixbuf(GTK_IMAGE(gtkconv->u.im->icon), scale);
2147 g_object_unref(G_OBJECT(scale));
2148 gtk_widget_queue_draw(gtkconv->u.im->icon);
2150 delay = gdk_pixbuf_animation_iter_get_delay_time(gtkconv->u.im->iter);
2152 if (delay < 100)
2153 delay = 100;
2155 gtkconv->u.im->icon_timer = g_timeout_add(delay, redraw_icon, gtkconv);
2157 return FALSE;
2160 static void
2161 start_anim(GtkWidget *widget, PidginConversation *gtkconv)
2163 int delay;
2165 if (gtkconv->u.im->anim == NULL)
2166 return;
2168 if (gtkconv->u.im->icon_timer != 0)
2169 return;
2171 if (gdk_pixbuf_animation_is_static_image(gtkconv->u.im->anim))
2172 return;
2174 delay = gdk_pixbuf_animation_iter_get_delay_time(gtkconv->u.im->iter);
2176 if (delay < 100)
2177 delay = 100;
2179 gtkconv->u.im->icon_timer = g_timeout_add(delay, redraw_icon, gtkconv);
2182 static void
2183 remove_icon(GtkWidget *widget, PidginConversation *gtkconv)
2185 GList *children;
2186 GtkWidget *event;
2187 PurpleConversation *conv = gtkconv->active_conv;
2189 g_return_if_fail(conv != NULL);
2191 gtk_widget_set_size_request(gtkconv->u.im->icon_container, -1, BUDDYICON_SIZE_MIN);
2192 children = gtk_container_get_children(GTK_CONTAINER(gtkconv->u.im->icon_container));
2193 if (children) {
2194 /* We know there's only one child here. It'd be nice to shortcut to the
2195 event box, but we can't change the PidginConversation until 3.0 */
2196 event = (GtkWidget *)children->data;
2197 gtk_container_remove(GTK_CONTAINER(gtkconv->u.im->icon_container), event);
2198 g_list_free(children);
2201 if (gtkconv->u.im->anim != NULL)
2202 g_object_unref(G_OBJECT(gtkconv->u.im->anim));
2204 if (gtkconv->u.im->icon_timer != 0)
2205 g_source_remove(gtkconv->u.im->icon_timer);
2207 if (gtkconv->u.im->iter != NULL)
2208 g_object_unref(G_OBJECT(gtkconv->u.im->iter));
2210 gtkconv->u.im->icon_timer = 0;
2211 gtkconv->u.im->icon = NULL;
2212 gtkconv->u.im->anim = NULL;
2213 gtkconv->u.im->iter = NULL;
2214 gtkconv->u.im->show_icon = FALSE;
2217 static void
2218 saveicon_writefile_cb(void *user_data, const char *filename)
2220 PidginConversation *gtkconv = (PidginConversation *)user_data;
2221 PurpleIMConversation *im = PURPLE_IM_CONVERSATION(gtkconv->active_conv);
2222 PurpleBuddyIcon *icon;
2223 const void *data;
2224 size_t len;
2226 icon = purple_im_conversation_get_icon(im);
2227 data = purple_buddy_icon_get_data(icon, &len);
2229 if ((len <= 0) || (data == NULL) || !purple_util_write_data_to_file_absolute(filename, data, len)) {
2230 purple_notify_error(gtkconv, NULL, _("Unable to save icon file to disk."), NULL, NULL);
2234 static void
2235 custom_icon_sel_cb(const char *filename, gpointer data)
2237 if (filename) {
2238 PurpleContact *contact = data;
2240 purple_buddy_icons_node_set_custom_icon_from_file((PurpleBlistNode*)contact, filename);
2242 g_object_set_data(G_OBJECT(data), "buddy-icon-chooser", NULL);
2245 static void
2246 set_custom_icon_cb(GtkWidget *widget, PurpleContact *contact)
2248 GtkFileChooserNative *win = NULL;
2250 /* Should not happen as menu item should be disabled. */
2251 g_return_if_fail(contact != NULL);
2253 win = g_object_get_data(G_OBJECT(contact), "buddy-icon-chooser");
2254 if (win == NULL) {
2255 GtkMenu *menu = GTK_MENU(gtk_widget_get_parent(widget));
2256 GtkWidget *toplevel =
2257 gtk_widget_get_toplevel(gtk_menu_get_attach_widget(menu));
2258 win = pidgin_buddy_icon_chooser_new(GTK_WINDOW(toplevel),
2259 custom_icon_sel_cb, contact);
2260 g_object_set_data_full(G_OBJECT(contact), "buddy-icon-chooser", win,
2261 (GDestroyNotify)g_object_unref);
2263 gtk_native_dialog_show(GTK_NATIVE_DIALOG(win));
2266 static void
2267 change_size_cb(GtkWidget *widget, PidginConversation *gtkconv)
2269 int size = 0;
2270 PurpleConversation *conv = gtkconv->active_conv;
2271 GSList *buddies;
2273 gtk_widget_get_size_request(gtkconv->u.im->icon_container, NULL, &size);
2275 if (size == BUDDYICON_SIZE_MAX) {
2276 size = BUDDYICON_SIZE_MIN;
2277 } else {
2278 size = BUDDYICON_SIZE_MAX;
2281 gtk_widget_set_size_request(gtkconv->u.im->icon_container, -1, size);
2282 pidgin_conv_update_buddy_icon(PURPLE_IM_CONVERSATION(conv));
2284 buddies = purple_blist_find_buddies(purple_conversation_get_account(conv),
2285 purple_conversation_get_name(conv));
2286 for (; buddies; buddies = g_slist_delete_link(buddies, buddies)) {
2287 PurpleBuddy *buddy = buddies->data;
2288 PurpleContact *contact = purple_buddy_get_contact(buddy);
2289 purple_blist_node_set_int((PurpleBlistNode*)contact, "pidgin-infopane-iconsize", size);
2293 static void
2294 remove_custom_icon_cb(GtkWidget *widget, PidginConversation *gtkconv)
2296 const gchar *name;
2297 PurpleBuddy *buddy;
2298 PurpleAccount *account;
2299 PurpleContact *contact;
2300 PurpleConversation *conv = gtkconv->active_conv;
2302 account = purple_conversation_get_account(conv);
2303 name = purple_conversation_get_name(conv);
2304 buddy = purple_blist_find_buddy(account, name);
2305 if (!buddy) {
2306 return;
2308 contact = purple_buddy_get_contact(buddy);
2310 purple_buddy_icons_node_set_custom_icon_from_file((PurpleBlistNode*)contact, NULL);
2313 static void
2314 icon_menu_save_cb(GtkWidget *widget, PidginConversation *gtkconv)
2316 PurpleConversation *conv = gtkconv->active_conv;
2317 const gchar *ext;
2318 gchar *buf;
2320 g_return_if_fail(conv != NULL);
2322 ext = purple_buddy_icon_get_extension(purple_im_conversation_get_icon(PURPLE_IM_CONVERSATION(conv)));
2324 buf = g_strdup_printf("%s.%s", purple_normalize(purple_conversation_get_account(conv), purple_conversation_get_name(conv)), ext);
2326 purple_request_file(gtkconv, _("Save Icon"), buf, TRUE,
2327 G_CALLBACK(saveicon_writefile_cb), NULL,
2328 purple_request_cpar_from_conversation(conv), gtkconv);
2330 g_free(buf);
2333 static void
2334 stop_anim(GtkWidget *widget, PidginConversation *gtkconv)
2336 if (gtkconv->u.im->icon_timer != 0)
2337 g_source_remove(gtkconv->u.im->icon_timer);
2339 gtkconv->u.im->icon_timer = 0;
2343 static void
2344 toggle_icon_animate_cb(GtkWidget *w, PidginConversation *gtkconv)
2346 gtkconv->u.im->animate =
2347 gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(w));
2349 if (gtkconv->u.im->animate)
2350 start_anim(NULL, gtkconv);
2351 else
2352 stop_anim(NULL, gtkconv);
2355 static gboolean
2356 icon_menu(GtkWidget *widget, GdkEventButton *e, PidginConversation *gtkconv)
2358 GtkWidget *menu = NULL;
2359 GList *old_menus = NULL;
2360 PurpleConversation *conv;
2361 PurpleBuddy *buddy;
2363 if (e->button == GDK_BUTTON_PRIMARY && e->type == GDK_BUTTON_PRESS) {
2364 change_size_cb(NULL, gtkconv);
2365 return TRUE;
2368 if (!gdk_event_triggers_context_menu((GdkEvent *)e)) {
2369 return FALSE;
2373 * If a menu already exists, destroy it before creating a new one,
2374 * thus freeing-up the memory it occupied.
2376 while ((old_menus = gtk_menu_get_for_attach_widget(widget)) != NULL) {
2377 menu = old_menus->data;
2378 gtk_menu_detach(GTK_MENU(menu));
2379 gtk_widget_destroy(menu);
2382 menu = gtk_menu_new();
2383 gtk_menu_attach_to_widget(GTK_MENU(menu), widget, NULL);
2385 if (gtkconv->u.im->anim &&
2386 !(gdk_pixbuf_animation_is_static_image(gtkconv->u.im->anim)))
2388 pidgin_new_check_item(menu, _("Animate"),
2389 G_CALLBACK(toggle_icon_animate_cb), gtkconv,
2390 gtkconv->u.im->icon_timer);
2393 conv = gtkconv->active_conv;
2394 buddy = purple_blist_find_buddy(purple_conversation_get_account(conv),
2395 purple_conversation_get_name(conv));
2397 pidgin_new_menu_item(menu, _("Hide Icon"), NULL,
2398 G_CALLBACK(remove_icon), gtkconv);
2400 pidgin_new_menu_item(menu, _("Save Icon As..."), GTK_STOCK_SAVE_AS,
2401 G_CALLBACK(icon_menu_save_cb), gtkconv);
2403 if (buddy) {
2404 PurpleContact *contact = purple_buddy_get_contact(buddy);
2405 pidgin_new_menu_item(menu, _("Set Custom Icon..."), NULL,
2406 G_CALLBACK(set_custom_icon_cb), contact);
2407 } else {
2408 GtkWidget *item =
2409 pidgin_new_menu_item(menu, _("Set Custom Icon..."), NULL,
2410 G_CALLBACK(set_custom_icon_cb), NULL);
2411 gtk_widget_set_sensitive(item, FALSE);
2414 pidgin_new_menu_item(menu, _("Change Size"), NULL,
2415 G_CALLBACK(change_size_cb), gtkconv);
2417 /* Is there a custom icon for this person? */
2418 if (buddy)
2420 PurpleContact *contact = purple_buddy_get_contact(buddy);
2421 if (contact && purple_buddy_icons_node_has_custom_icon((PurpleBlistNode*)contact))
2423 pidgin_new_menu_item(menu, _("Remove Custom Icon"),
2424 NULL, G_CALLBACK(remove_custom_icon_cb),
2425 gtkconv);
2429 gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent *)e);
2431 return TRUE;
2434 /**************************************************************************
2435 * End of the bunch of buddy icon functions
2436 **************************************************************************/
2437 void
2438 pidgin_conv_present_conversation(PurpleConversation *conv)
2440 PidginConversation *gtkconv;
2441 GdkModifierType state;
2443 pidgin_conv_attach_to_conversation(conv);
2444 gtkconv = PIDGIN_CONVERSATION(conv);
2446 pidgin_conv_switch_active_conversation(conv);
2447 /* Switch the tab only if the user initiated the event by pressing
2448 * a button or hitting a key. */
2449 if (gtk_get_current_event_state(&state))
2450 pidgin_conv_window_switch_gtkconv(gtkconv->win, gtkconv);
2451 gtk_window_present(GTK_WINDOW(gtkconv->win->window));
2454 static GList *
2455 pidgin_conversations_get_unseen(GList *l,
2456 PidginUnseenState min_state,
2457 gboolean hidden_only,
2458 guint max_count)
2460 GList *r = NULL;
2461 guint c = 0;
2463 for (; l != NULL && (max_count == 0 || c < max_count); l = l->next) {
2464 PurpleConversation *conv = (PurpleConversation*)l->data;
2465 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
2467 if(gtkconv == NULL || gtkconv->active_conv != conv)
2468 continue;
2470 if (gtkconv->unseen_state >= min_state &&
2471 (!hidden_only || gtkconv->win == hidden_convwin)) {
2473 r = g_list_prepend(r, conv);
2474 c++;
2478 return r;
2481 GList *
2482 pidgin_conversations_get_unseen_all(PidginUnseenState min_state,
2483 gboolean hidden_only,
2484 guint max_count)
2486 return pidgin_conversations_get_unseen(purple_conversations_get_all(),
2487 min_state, hidden_only, max_count);
2490 GList *
2491 pidgin_conversations_get_unseen_ims(PidginUnseenState min_state,
2492 gboolean hidden_only,
2493 guint max_count)
2495 return pidgin_conversations_get_unseen(purple_conversations_get_ims(),
2496 min_state, hidden_only, max_count);
2499 GList *
2500 pidgin_conversations_get_unseen_chats(PidginUnseenState min_state,
2501 gboolean hidden_only,
2502 guint max_count)
2504 return pidgin_conversations_get_unseen(purple_conversations_get_chats(),
2505 min_state, hidden_only, max_count);
2508 static void
2509 unseen_conv_menu_cb(GtkMenuItem *item, PurpleConversation *conv)
2511 g_return_if_fail(conv != NULL);
2512 pidgin_conv_present_conversation(conv);
2515 static void
2516 unseen_all_conv_menu_cb(GtkMenuItem *item, GList *list)
2518 g_return_if_fail(list != NULL);
2519 /* Do not free the list from here. It will be freed from the
2520 * 'destroy' callback on the menuitem. */
2521 while (list) {
2522 pidgin_conv_present_conversation(list->data);
2523 list = list->next;
2527 guint
2528 pidgin_conversations_fill_menu(GtkWidget *menu, GList *convs)
2530 GList *l;
2531 guint ret=0;
2533 g_return_val_if_fail(menu != NULL, 0);
2534 g_return_val_if_fail(convs != NULL, 0);
2536 for (l = convs; l != NULL ; l = l->next) {
2537 PurpleConversation *conv = (PurpleConversation*)l->data;
2538 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
2540 GtkWidget *icon = gtk_image_new_from_stock(pidgin_conv_get_icon_stock(conv),
2541 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_MICROSCOPIC));
2542 GtkWidget *item;
2543 gchar *text = g_strdup_printf("%s (%d)",
2544 gtk_label_get_text(GTK_LABEL(gtkconv->tab_label)),
2545 gtkconv->unseen_count);
2547 item = gtk_image_menu_item_new_with_label(text);
2548 gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(item), icon);
2549 g_signal_connect(G_OBJECT(item), "activate", G_CALLBACK(unseen_conv_menu_cb), conv);
2550 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
2551 g_free(text);
2552 ret++;
2555 if (convs->next) {
2556 /* There are more than one conversation. Add an option to show all conversations. */
2557 GtkWidget *item;
2558 GList *list = g_list_copy(convs);
2560 pidgin_separator(menu);
2562 item = gtk_menu_item_new_with_label(_("Show All"));
2563 g_signal_connect(G_OBJECT(item), "activate", G_CALLBACK(unseen_all_conv_menu_cb), list);
2564 g_signal_connect_swapped(G_OBJECT(item), "destroy", G_CALLBACK(g_list_free), list);
2565 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
2568 return ret;
2571 PidginConvWindow *
2572 pidgin_conv_get_window(PidginConversation *gtkconv)
2574 g_return_val_if_fail(gtkconv != NULL, NULL);
2575 return gtkconv->win;
2578 static GtkActionEntry menu_entries[] =
2579 /* TODO: fill out tooltips... */
2581 /* Conversation menu */
2582 { "ConversationMenu", NULL, N_("_Conversation"), NULL, NULL, NULL },
2583 { "NewInstantMessage", PIDGIN_STOCK_TOOLBAR_MESSAGE_NEW, N_("New Instant _Message..."), "<control>M", NULL, G_CALLBACK(menu_new_conv_cb) },
2584 { "JoinAChat", PIDGIN_STOCK_CHAT, N_("Join a _Chat..."), NULL, NULL, G_CALLBACK(menu_join_chat_cb) },
2585 { "Find", GTK_STOCK_FIND, N_("_Find..."), NULL, NULL, G_CALLBACK(menu_find_cb) },
2586 { "ViewLog", NULL, N_("View _Log"), NULL, NULL, G_CALLBACK(menu_view_log_cb) },
2587 { "SaveAs", GTK_STOCK_SAVE_AS, N_("_Save As..."), NULL, NULL, G_CALLBACK(menu_save_as_cb) },
2588 { "ClearScrollback", GTK_STOCK_CLEAR, N_("Clea_r Scrollback"), "<control>L", NULL, G_CALLBACK(menu_clear_cb) },
2590 #ifdef USE_VV
2591 { "MediaMenu", NULL, N_("M_edia"), NULL, NULL, NULL },
2592 { "AudioCall", PIDGIN_STOCK_TOOLBAR_AUDIO_CALL, N_("_Audio Call"), NULL, NULL, G_CALLBACK(menu_initiate_media_call_cb) },
2593 { "VideoCall", PIDGIN_STOCK_TOOLBAR_VIDEO_CALL, N_("_Video Call"), NULL, NULL, G_CALLBACK(menu_initiate_media_call_cb) },
2594 { "AudioVideoCall", PIDGIN_STOCK_TOOLBAR_VIDEO_CALL, N_("Audio/Video _Call"), NULL, NULL, G_CALLBACK(menu_initiate_media_call_cb) },
2595 #endif
2597 { "SendFile", PIDGIN_STOCK_TOOLBAR_SEND_FILE, N_("Se_nd File..."), NULL, NULL, G_CALLBACK(menu_send_file_cb) },
2598 { "GetAttention", PIDGIN_STOCK_TOOLBAR_SEND_ATTENTION, N_("Get _Attention"), NULL, NULL, G_CALLBACK(menu_get_attention_cb) },
2599 { "AddBuddyPounce", NULL, N_("Add Buddy _Pounce..."), NULL, NULL, G_CALLBACK(menu_add_pounce_cb) },
2600 { "GetInfo", PIDGIN_STOCK_TOOLBAR_USER_INFO, N_("_Get Info"), "<control>O", NULL, G_CALLBACK(menu_get_info_cb) },
2601 { "Invite", NULL, N_("In_vite..."), NULL, NULL, G_CALLBACK(menu_invite_cb) },
2602 { "MoreMenu", NULL, N_("M_ore"), NULL, NULL, NULL },
2603 { "Alias", NULL, N_("Al_ias..."), NULL, NULL, G_CALLBACK(menu_alias_cb) },
2604 { "Block", PIDGIN_STOCK_TOOLBAR_BLOCK, N_("_Block..."), NULL, NULL, G_CALLBACK(menu_block_cb) },
2605 { "Unblock", PIDGIN_STOCK_TOOLBAR_UNBLOCK, N_("_Unblock..."), NULL, NULL, G_CALLBACK(menu_unblock_cb) },
2606 { "Add", GTK_STOCK_ADD, N_("_Add..."), NULL, NULL, G_CALLBACK(menu_add_remove_cb) },
2607 { "Remove", GTK_STOCK_REMOVE, N_("_Remove..."), NULL, NULL, G_CALLBACK(menu_add_remove_cb) },
2608 { "InsertLink", PIDGIN_STOCK_TOOLBAR_INSERT_LINK, N_("Insert Lin_k..."), NULL, NULL, NULL },
2609 { "InsertImage", PIDGIN_STOCK_TOOLBAR_INSERT_IMAGE, N_("Insert Imag_e..."), NULL, NULL, NULL },
2610 { "Close", GTK_STOCK_CLOSE, N_("_Close"), "<control>W", NULL, G_CALLBACK(menu_close_conv_cb) },
2612 /* Options */
2613 { "OptionsMenu", NULL, N_("_Options"), NULL, NULL, NULL },
2616 /* Toggle items */
2617 static const GtkToggleActionEntry menu_toggle_entries[] = {
2618 { "EnableLogging", NULL, N_("Enable _Logging"), NULL, NULL, G_CALLBACK(menu_logging_cb), FALSE },
2619 { "EnableSounds", NULL, N_("Enable _Sounds"), NULL, NULL, G_CALLBACK(menu_sounds_cb), FALSE },
2620 { "ShowFormattingToolbars", NULL, N_("Show Formatting _Toolbars"), NULL, NULL, G_CALLBACK(menu_toolbar_cb), FALSE },
2623 static const char *conversation_menu =
2624 "<ui>"
2625 "<menubar name='Conversation'>"
2626 "<menu action='ConversationMenu'>"
2627 "<menuitem action='NewInstantMessage'/>"
2628 "<menuitem action='JoinAChat'/>"
2629 "<separator/>"
2630 "<menuitem action='Find'/>"
2631 "<menuitem action='ViewLog'/>"
2632 "<menuitem action='SaveAs'/>"
2633 "<menuitem action='ClearScrollback'/>"
2634 "<separator/>"
2635 #ifdef USE_VV
2636 "<menu action='MediaMenu'>"
2637 "<menuitem action='AudioCall'/>"
2638 "<menuitem action='VideoCall'/>"
2639 "<menuitem action='AudioVideoCall'/>"
2640 "</menu>"
2641 #endif
2642 "<menuitem action='SendFile'/>"
2643 "<menuitem action='GetAttention'/>"
2644 "<menuitem action='AddBuddyPounce'/>"
2645 "<menuitem action='GetInfo'/>"
2646 "<menuitem action='Invite'/>"
2647 "<menu action='MoreMenu'/>"
2648 "<separator/>"
2649 "<menuitem action='Alias'/>"
2650 "<menuitem action='Block'/>"
2651 "<menuitem action='Unblock'/>"
2652 "<menuitem action='Add'/>"
2653 "<menuitem action='Remove'/>"
2654 "<separator/>"
2655 "<menuitem action='InsertLink'/>"
2656 "<menuitem action='InsertImage'/>"
2657 "<separator/>"
2658 "<menuitem action='Close'/>"
2659 "</menu>"
2660 "<menu action='OptionsMenu'>"
2661 "<menuitem action='EnableLogging'/>"
2662 "<menuitem action='EnableSounds'/>"
2663 "<separator/>"
2664 "<menuitem action='ShowFormattingToolbars'/>"
2665 "</menu>"
2666 "</menubar>"
2667 "</ui>";
2669 static void
2670 sound_method_pref_changed_cb(const char *name, PurplePrefType type,
2671 gconstpointer value, gpointer data)
2673 PidginConvWindow *win = data;
2674 const char *method = value;
2676 if (purple_strequal(method, "none"))
2678 gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(win->menu->sounds),
2679 FALSE);
2680 gtk_action_set_sensitive(win->menu->sounds, FALSE);
2682 else
2684 PidginConversation *gtkconv = pidgin_conv_window_get_active_gtkconv(win);
2686 if (gtkconv != NULL)
2687 gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(win->menu->sounds),
2688 gtkconv->make_sound);
2689 gtk_action_set_sensitive(win->menu->sounds, TRUE);
2693 /* Returns TRUE if some items were added to the menu, FALSE otherwise */
2694 static gboolean
2695 populate_menu_with_options(GtkWidget *menu, PidginConversation *gtkconv, gboolean all)
2697 GList *list;
2698 PurpleConversation *conv;
2699 PurpleAccount *account;
2700 PurpleBlistNode *node = NULL;
2701 PurpleChat *chat = NULL;
2702 PurpleBuddy *buddy = NULL;
2703 gboolean ret;
2705 conv = gtkconv->active_conv;
2706 account = purple_conversation_get_account(conv);
2708 if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
2709 chat = purple_blist_find_chat(account, purple_conversation_get_name(conv));
2711 if ((chat == NULL) && (gtkconv->history != NULL)) {
2712 chat = g_object_get_data(G_OBJECT(gtkconv->history), "transient_chat");
2715 if ((chat == NULL) && (gtkconv->history != NULL)) {
2716 GHashTable *components;
2717 PurpleAccount *account = purple_conversation_get_account(conv);
2718 PurpleProtocol *protocol =
2719 purple_protocols_find(purple_account_get_protocol_id(account));
2720 if (purple_account_get_connection(account) != NULL &&
2721 PURPLE_PROTOCOL_IMPLEMENTS(protocol, CHAT, info_defaults)) {
2722 components = purple_protocol_chat_iface_info_defaults(protocol, purple_account_get_connection(account),
2723 purple_conversation_get_name(conv));
2724 } else {
2725 components = g_hash_table_new_full(g_str_hash, g_str_equal,
2726 g_free, g_free);
2727 g_hash_table_replace(components, g_strdup("channel"),
2728 g_strdup(purple_conversation_get_name(conv)));
2730 chat = purple_chat_new(account, NULL, components);
2731 purple_blist_node_set_transient((PurpleBlistNode *)chat, TRUE);
2732 g_object_set_data_full(G_OBJECT(gtkconv->history), "transient_chat",
2733 chat, (GDestroyNotify)purple_blist_remove_chat);
2735 } else {
2736 if (!purple_account_is_connected(account))
2737 return FALSE;
2739 buddy = purple_blist_find_buddy(account, purple_conversation_get_name(conv));
2740 if (!buddy && gtkconv->history) {
2741 buddy = g_object_get_data(G_OBJECT(gtkconv->history), "transient_buddy");
2743 if (!buddy) {
2744 buddy = purple_buddy_new(account, purple_conversation_get_name(conv), NULL);
2745 purple_blist_node_set_transient((PurpleBlistNode *)buddy, TRUE);
2746 g_object_set_data_full(G_OBJECT(gtkconv->history), "transient_buddy",
2747 buddy, (GDestroyNotify)g_object_unref);
2752 if (chat)
2753 node = (PurpleBlistNode *)chat;
2754 else if (buddy)
2755 node = (PurpleBlistNode *)buddy;
2757 /* Now add the stuff */
2758 if (all) {
2759 if (buddy)
2760 pidgin_blist_make_buddy_menu(menu, buddy, TRUE);
2761 else if (chat) {
2762 /* XXX: */
2764 } else if (node) {
2765 if (purple_account_is_connected(account))
2766 pidgin_append_blist_node_proto_menu(menu, purple_account_get_connection(account), node);
2767 pidgin_append_blist_node_extended_menu(menu, node);
2770 if ((list = gtk_container_get_children(GTK_CONTAINER(menu))) == NULL) {
2771 ret = FALSE;
2772 } else {
2773 g_list_free(list);
2774 ret = TRUE;
2776 return ret;
2779 static void
2780 regenerate_media_items(PidginConvWindow *win)
2782 #ifdef USE_VV
2783 PurpleAccount *account;
2784 PurpleConversation *conv;
2786 conv = pidgin_conv_window_get_active_conversation(win);
2788 if (conv == NULL) {
2789 purple_debug_error("gtkconv", "couldn't get active conversation"
2790 " when regenerating media items\n");
2791 return;
2794 account = purple_conversation_get_account(conv);
2796 if (account == NULL) {
2797 purple_debug_error("gtkconv", "couldn't get account when"
2798 " regenerating media items\n");
2799 return;
2803 * Check if account support voice and/or calls, and
2804 * if the current buddy supports it.
2806 if (PURPLE_IS_IM_CONVERSATION(conv)) {
2807 PurpleMediaCaps caps =
2808 purple_protocol_get_media_caps(account,
2809 purple_conversation_get_name(conv));
2811 gtk_action_set_sensitive(win->menu->audio_call,
2812 caps & PURPLE_MEDIA_CAPS_AUDIO
2813 ? TRUE : FALSE);
2814 gtk_action_set_sensitive(win->menu->video_call,
2815 caps & PURPLE_MEDIA_CAPS_VIDEO
2816 ? TRUE : FALSE);
2817 gtk_action_set_sensitive(win->menu->audio_video_call,
2818 caps & PURPLE_MEDIA_CAPS_AUDIO_VIDEO
2819 ? TRUE : FALSE);
2820 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
2821 /* for now, don't care about chats... */
2822 gtk_action_set_sensitive(win->menu->audio_call, FALSE);
2823 gtk_action_set_sensitive(win->menu->video_call, FALSE);
2824 gtk_action_set_sensitive(win->menu->audio_video_call, FALSE);
2825 } else {
2826 gtk_action_set_sensitive(win->menu->audio_call, FALSE);
2827 gtk_action_set_sensitive(win->menu->video_call, FALSE);
2828 gtk_action_set_sensitive(win->menu->audio_video_call, FALSE);
2830 #endif
2833 static void
2834 regenerate_attention_items(PidginConvWindow *win)
2836 GtkWidget *attention;
2837 GtkWidget *menu;
2838 PurpleConversation *conv;
2839 PurpleConnection *pc;
2840 PurpleProtocol *protocol = NULL;
2841 GList *list;
2843 conv = pidgin_conv_window_get_active_conversation(win);
2844 if (!conv)
2845 return;
2847 attention = gtk_ui_manager_get_widget(win->menu->ui,
2848 "/Conversation/ConversationMenu/GetAttention");
2850 /* Remove the previous entries */
2851 gtk_menu_item_set_submenu(GTK_MENU_ITEM(attention), NULL);
2853 pc = purple_conversation_get_connection(conv);
2854 if (pc != NULL)
2855 protocol = purple_connection_get_protocol(pc);
2857 if (protocol && PURPLE_IS_PROTOCOL_ATTENTION(protocol)) {
2858 list = purple_protocol_attention_get_types(PURPLE_PROTOCOL_ATTENTION(protocol), purple_connection_get_account(pc));
2860 /* Multiple attention types */
2861 if (list && list->next) {
2862 int index = 0;
2864 menu = gtk_menu_new();
2865 while (list) {
2866 PurpleAttentionType *type;
2867 GtkWidget *menuitem;
2869 type = list->data;
2871 menuitem = gtk_menu_item_new_with_label(purple_attention_type_get_name(type));
2872 g_object_set_data(G_OBJECT(menuitem), "index", GINT_TO_POINTER(index));
2873 g_signal_connect(G_OBJECT(menuitem), "activate",
2874 G_CALLBACK(menu_get_attention_cb),
2875 win);
2876 gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
2878 index++;
2879 list = g_list_delete_link(list, list);
2882 gtk_menu_item_set_submenu(GTK_MENU_ITEM(attention), menu);
2883 gtk_widget_show_all(menu);
2888 static void
2889 regenerate_options_items(PidginConvWindow *win)
2891 GtkWidget *menu;
2892 PidginConversation *gtkconv;
2893 GList *list;
2894 GtkWidget *more_menu;
2896 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
2897 more_menu = gtk_ui_manager_get_widget(win->menu->ui,
2898 "/Conversation/ConversationMenu/MoreMenu");
2899 gtk_widget_show(more_menu);
2900 menu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(more_menu));
2902 /* Remove the previous entries */
2903 for (list = gtk_container_get_children(GTK_CONTAINER(menu)); list; )
2905 GtkWidget *w = list->data;
2906 list = g_list_delete_link(list, list);
2907 gtk_widget_destroy(w);
2910 if (!populate_menu_with_options(menu, gtkconv, FALSE))
2912 GtkWidget *item = gtk_menu_item_new_with_label(_("No actions available"));
2913 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
2914 gtk_widget_set_sensitive(item, FALSE);
2917 gtk_widget_show_all(menu);
2920 static void
2921 remove_from_list(GtkWidget *widget, PidginConvWindow *win)
2923 GList *list = g_object_get_data(G_OBJECT(win->window), "plugin-actions");
2924 list = g_list_remove(list, widget);
2925 g_object_set_data(G_OBJECT(win->window), "plugin-actions", list);
2928 static void
2929 regenerate_plugins_items(PidginConvWindow *win)
2931 GList *action_items;
2932 GtkWidget *menu;
2933 GList *list;
2934 PidginConversation *gtkconv;
2935 PurpleConversation *conv;
2936 GtkWidget *item;
2938 if (win->window == NULL || win == hidden_convwin)
2939 return;
2941 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
2942 if (gtkconv == NULL)
2943 return;
2945 conv = gtkconv->active_conv;
2946 action_items = g_object_get_data(G_OBJECT(win->window), "plugin-actions");
2948 /* Remove the old menuitems */
2949 while (action_items) {
2950 g_signal_handlers_disconnect_by_func(G_OBJECT(action_items->data),
2951 G_CALLBACK(remove_from_list), win);
2952 gtk_widget_destroy(action_items->data);
2953 action_items = g_list_delete_link(action_items, action_items);
2956 item = gtk_ui_manager_get_widget(win->menu->ui, "/Conversation/OptionsMenu");
2957 menu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(item));
2959 list = purple_conversation_get_extended_menu(conv);
2960 if (list) {
2961 action_items = g_list_prepend(NULL, (item = pidgin_separator(menu)));
2962 g_signal_connect(G_OBJECT(item), "destroy", G_CALLBACK(remove_from_list), win);
2965 for(; list; list = g_list_delete_link(list, list)) {
2966 PurpleActionMenu *act = (PurpleActionMenu *) list->data;
2967 item = pidgin_append_menu_action(menu, act, conv);
2968 action_items = g_list_prepend(action_items, item);
2969 gtk_widget_show_all(item);
2970 g_signal_connect(G_OBJECT(item), "destroy", G_CALLBACK(remove_from_list), win);
2972 g_object_set_data(G_OBJECT(win->window), "plugin-actions", action_items);
2975 static void menubar_activated(GtkWidget *item, gpointer data)
2977 PidginConvWindow *win = data;
2978 regenerate_media_items(win);
2979 regenerate_options_items(win);
2980 regenerate_plugins_items(win);
2981 regenerate_attention_items(win);
2983 /* The following are to make sure the 'More' submenu is not regenerated every time
2984 * the focus shifts from 'Conversations' to some other menu and back. */
2985 g_signal_handlers_block_by_func(G_OBJECT(item), G_CALLBACK(menubar_activated), data);
2986 g_signal_connect(G_OBJECT(win->menu->menubar), "deactivate", G_CALLBACK(focus_out_from_menubar), data);
2989 static void
2990 focus_out_from_menubar(GtkWidget *wid, PidginConvWindow *win)
2992 /* The menubar has been deactivated. Make sure the 'More' submenu is regenerated next time
2993 * the 'Conversation' menu pops up. */
2994 GtkWidget *menuitem = gtk_ui_manager_get_widget(win->menu->ui, "/Conversation/ConversationMenu");
2995 g_signal_handlers_unblock_by_func(G_OBJECT(menuitem), G_CALLBACK(menubar_activated), win);
2996 g_signal_handlers_disconnect_by_func(G_OBJECT(win->menu->menubar),
2997 G_CALLBACK(focus_out_from_menubar), win);
3000 static GtkWidget *
3001 setup_menubar(PidginConvWindow *win)
3003 GtkAccelGroup *accel_group;
3004 const char *method;
3005 GtkActionGroup *action_group;
3006 GError *error;
3007 GtkWidget *menuitem;
3009 action_group = gtk_action_group_new("ConversationActions");
3010 gtk_action_group_set_translation_domain(action_group, PACKAGE);
3011 gtk_action_group_add_actions(action_group,
3012 menu_entries,
3013 G_N_ELEMENTS(menu_entries),
3014 win);
3015 gtk_action_group_add_toggle_actions(action_group,
3016 menu_toggle_entries,
3017 G_N_ELEMENTS(menu_toggle_entries),
3018 win);
3020 win->menu->ui = gtk_ui_manager_new();
3021 gtk_ui_manager_insert_action_group(win->menu->ui, action_group, 0);
3023 accel_group = gtk_ui_manager_get_accel_group(win->menu->ui);
3024 gtk_window_add_accel_group(GTK_WINDOW(win->window), accel_group);
3025 g_signal_connect(G_OBJECT(accel_group), "accel-changed",
3026 G_CALLBACK(pidgin_save_accels_cb), NULL);
3028 error = NULL;
3029 if (!gtk_ui_manager_add_ui_from_string(win->menu->ui, conversation_menu, -1, &error))
3031 g_message("building menus failed: %s", error->message);
3032 g_error_free(error);
3033 exit(EXIT_FAILURE);
3036 win->menu->menubar =
3037 gtk_ui_manager_get_widget(win->menu->ui, "/Conversation");
3039 /* Make sure the 'Conversation ⇨ More' menuitems are regenerated whenever
3040 * the 'Conversation' menu pops up because the entries can change after the
3041 * conversation is created. */
3042 menuitem = gtk_ui_manager_get_widget(win->menu->ui, "/Conversation/ConversationMenu");
3043 g_signal_connect(G_OBJECT(menuitem), "activate", G_CALLBACK(menubar_activated), win);
3045 win->menu->view_log =
3046 gtk_ui_manager_get_action(win->menu->ui,
3047 "/Conversation/ConversationMenu/ViewLog");
3049 #ifdef USE_VV
3050 win->menu->audio_call =
3051 gtk_ui_manager_get_action(win->menu->ui,
3052 "/Conversation/ConversationMenu/MediaMenu/AudioCall");
3053 win->menu->video_call =
3054 gtk_ui_manager_get_action(win->menu->ui,
3055 "/Conversation/ConversationMenu/MediaMenu/VideoCall");
3056 win->menu->audio_video_call =
3057 gtk_ui_manager_get_action(win->menu->ui,
3058 "/Conversation/ConversationMenu/MediaMenu/AudioVideoCall");
3059 #endif
3061 /* --- */
3063 win->menu->send_file =
3064 gtk_ui_manager_get_action(win->menu->ui,
3065 "/Conversation/ConversationMenu/SendFile");
3067 win->menu->get_attention =
3068 gtk_ui_manager_get_action(win->menu->ui,
3069 "/Conversation/ConversationMenu/GetAttention");
3071 win->menu->add_pounce =
3072 gtk_ui_manager_get_action(win->menu->ui,
3073 "/Conversation/ConversationMenu/AddBuddyPounce");
3075 /* --- */
3077 win->menu->get_info =
3078 gtk_ui_manager_get_action(win->menu->ui,
3079 "/Conversation/ConversationMenu/GetInfo");
3081 win->menu->invite =
3082 gtk_ui_manager_get_action(win->menu->ui,
3083 "/Conversation/ConversationMenu/Invite");
3085 /* --- */
3087 win->menu->alias =
3088 gtk_ui_manager_get_action(win->menu->ui,
3089 "/Conversation/ConversationMenu/Alias");
3091 win->menu->block =
3092 gtk_ui_manager_get_action(win->menu->ui,
3093 "/Conversation/ConversationMenu/Block");
3095 win->menu->unblock =
3096 gtk_ui_manager_get_action(win->menu->ui,
3097 "/Conversation/ConversationMenu/Unblock");
3099 win->menu->add =
3100 gtk_ui_manager_get_action(win->menu->ui,
3101 "/Conversation/ConversationMenu/Add");
3103 win->menu->remove =
3104 gtk_ui_manager_get_action(win->menu->ui,
3105 "/Conversation/ConversationMenu/Remove");
3107 /* --- */
3109 win->menu->insert_link =
3110 gtk_ui_manager_get_action(win->menu->ui,
3111 "/Conversation/ConversationMenu/InsertLink");
3113 win->menu->insert_image =
3114 gtk_ui_manager_get_action(win->menu->ui,
3115 "/Conversation/ConversationMenu/InsertImage");
3117 /* --- */
3119 win->menu->logging =
3120 gtk_ui_manager_get_action(win->menu->ui,
3121 "/Conversation/OptionsMenu/EnableLogging");
3122 win->menu->sounds =
3123 gtk_ui_manager_get_action(win->menu->ui,
3124 "/Conversation/OptionsMenu/EnableSounds");
3125 method = purple_prefs_get_string(PIDGIN_PREFS_ROOT "/sound/method");
3126 if (purple_strequal(method, "none"))
3128 gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(win->menu->sounds),
3129 FALSE);
3130 gtk_action_set_sensitive(win->menu->sounds, FALSE);
3132 purple_prefs_connect_callback(win, PIDGIN_PREFS_ROOT "/sound/method",
3133 sound_method_pref_changed_cb, win);
3135 win->menu->show_formatting_toolbar =
3136 gtk_ui_manager_get_action(win->menu->ui,
3137 "/Conversation/OptionsMenu/ShowFormattingToolbars");
3139 win->menu->tray = pidgin_menu_tray_new();
3140 gtk_menu_shell_append(GTK_MENU_SHELL(win->menu->menubar),
3141 win->menu->tray);
3142 gtk_widget_show(win->menu->tray);
3144 gtk_widget_show(win->menu->menubar);
3146 return win->menu->menubar;
3150 /**************************************************************************
3151 * Utility functions
3152 **************************************************************************/
3154 static void
3155 got_typing_keypress(PidginConversation *gtkconv, gboolean first)
3157 PurpleConversation *conv = gtkconv->active_conv;
3158 PurpleIMConversation *im;
3161 * We know we got something, so we at least have to make sure we don't
3162 * send PURPLE_IM_TYPED any time soon.
3165 im = PURPLE_IM_CONVERSATION(conv);
3167 purple_im_conversation_stop_send_typed_timeout(im);
3168 purple_im_conversation_start_send_typed_timeout(im);
3170 /* Check if we need to send another PURPLE_IM_TYPING message */
3171 if (first || (purple_im_conversation_get_type_again(im) != 0 &&
3172 time(NULL) > purple_im_conversation_get_type_again(im)))
3174 unsigned int timeout;
3175 timeout = purple_serv_send_typing(purple_conversation_get_connection(conv),
3176 purple_conversation_get_name(conv),
3177 PURPLE_IM_TYPING);
3178 purple_im_conversation_set_type_again(im, timeout);
3182 #if 0
3183 static gboolean
3184 typing_animation(gpointer data) {
3185 PidginConversation *gtkconv = data;
3186 PidginConvWindow *gtkwin = gtkconv->win;
3187 const char *stock_id = NULL;
3189 if(gtkconv != pidgin_conv_window_get_active_gtkconv(gtkwin)) {
3190 return FALSE;
3193 switch (rand() % 5) {
3194 case 0:
3195 stock_id = PIDGIN_STOCK_ANIMATION_TYPING0;
3196 break;
3197 case 1:
3198 stock_id = PIDGIN_STOCK_ANIMATION_TYPING1;
3199 break;
3200 case 2:
3201 stock_id = PIDGIN_STOCK_ANIMATION_TYPING2;
3202 break;
3203 case 3:
3204 stock_id = PIDGIN_STOCK_ANIMATION_TYPING3;
3205 break;
3206 case 4:
3207 stock_id = PIDGIN_STOCK_ANIMATION_TYPING4;
3208 break;
3210 if (gtkwin->menu->typing_icon == NULL) {
3211 gtkwin->menu->typing_icon = gtk_image_new_from_stock(stock_id, GTK_ICON_SIZE_MENU);
3212 pidgin_menu_tray_append(PIDGIN_MENU_TRAY(gtkwin->menu->tray),
3213 gtkwin->menu->typing_icon,
3214 _("User is typing..."));
3215 } else {
3216 gtk_image_set_from_stock(GTK_IMAGE(gtkwin->menu->typing_icon), stock_id, GTK_ICON_SIZE_MENU);
3218 gtk_widget_show(gtkwin->menu->typing_icon);
3219 return TRUE;
3221 #endif
3223 static void
3224 update_typing_message(PidginConversation *gtkconv, const char *message)
3226 /* TODO WEBKIT: this is not handled at all */
3227 #if 0
3228 GtkTextBuffer *buffer;
3229 GtkTextMark *stmark, *enmark;
3231 if (g_object_get_data(G_OBJECT(gtkconv->imhtml), "disable-typing-notification"))
3232 return;
3234 buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(gtkconv->imhtml));
3235 stmark = gtk_text_buffer_get_mark(buffer, "typing-notification-start");
3236 enmark = gtk_text_buffer_get_mark(buffer, "typing-notification-end");
3237 if (stmark && enmark) {
3238 GtkTextIter start, end;
3239 gtk_text_buffer_get_iter_at_mark(buffer, &start, stmark);
3240 gtk_text_buffer_get_iter_at_mark(buffer, &end, enmark);
3241 gtk_text_buffer_delete_mark(buffer, stmark);
3242 gtk_text_buffer_delete_mark(buffer, enmark);
3243 gtk_text_buffer_delete(buffer, &start, &end);
3244 } else if (message && *message == '\n' && message[1] == ' ' && message[2] == '\0')
3245 message = NULL;
3247 #ifdef RESERVE_LINE
3248 if (!message)
3249 message = "\n "; /* The blank space is required to avoid a GTK+/Pango bug */
3250 #endif
3252 if (message) {
3253 GtkTextIter iter;
3254 gtk_text_buffer_get_end_iter(buffer, &iter);
3255 gtk_text_buffer_create_mark(buffer, "typing-notification-start", &iter, TRUE);
3256 gtk_text_buffer_insert_with_tags_by_name(buffer, &iter, message, -1, "TYPING-NOTIFICATION", NULL);
3257 gtk_text_buffer_get_end_iter(buffer, &iter);
3258 gtk_text_buffer_create_mark(buffer, "typing-notification-end", &iter, TRUE);
3260 #endif /* if 0 */
3263 static void
3264 update_typing_icon(PidginConversation *gtkconv)
3266 PurpleIMConversation *im;
3267 char *message = NULL;
3269 if (!PURPLE_IS_IM_CONVERSATION(gtkconv->active_conv))
3270 return;
3272 im = PURPLE_IM_CONVERSATION(gtkconv->active_conv);
3274 if (purple_im_conversation_get_typing_state(im) == PURPLE_IM_NOT_TYPING) {
3275 #ifdef RESERVE_LINE
3276 update_typing_message(gtkconv, NULL);
3277 #else
3278 update_typing_message(gtkconv, "\n ");
3279 #endif
3280 return;
3283 if (purple_im_conversation_get_typing_state(im) == PURPLE_IM_TYPING) {
3284 message = g_strdup_printf(_("\n%s is typing..."), purple_conversation_get_title(PURPLE_CONVERSATION(im)));
3285 } else {
3286 message = g_strdup_printf(_("\n%s has stopped typing"), purple_conversation_get_title(PURPLE_CONVERSATION(im)));
3289 update_typing_message(gtkconv, message);
3290 g_free(message);
3293 static gboolean
3294 update_send_to_selection(PidginConvWindow *win)
3296 PurpleAccount *account;
3297 PurpleConversation *conv;
3298 GtkWidget *menu;
3299 GList *child;
3300 PurpleBuddy *b;
3302 conv = pidgin_conv_window_get_active_conversation(win);
3304 if (conv == NULL)
3305 return FALSE;
3307 account = purple_conversation_get_account(conv);
3309 if (account == NULL)
3310 return FALSE;
3312 if (win->menu->send_to == NULL)
3313 return FALSE;
3315 if (!(b = purple_blist_find_buddy(account, purple_conversation_get_name(conv))))
3316 return FALSE;
3318 gtk_widget_show(win->menu->send_to);
3320 menu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(win->menu->send_to));
3322 for (child = gtk_container_get_children(GTK_CONTAINER(menu));
3323 child != NULL;
3324 child = g_list_delete_link(child, child)) {
3326 GtkWidget *item = child->data;
3327 PurpleBuddy *item_buddy;
3328 PurpleAccount *item_account = g_object_get_data(G_OBJECT(item), "purple_account");
3329 gchar *buddy_name = g_object_get_data(G_OBJECT(item),
3330 "purple_buddy_name");
3331 item_buddy = purple_blist_find_buddy(item_account, buddy_name);
3333 if (b == item_buddy) {
3334 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item), TRUE);
3335 g_list_free(child);
3336 break;
3340 return FALSE;
3343 static gboolean
3344 send_to_item_enter_notify_cb(GtkWidget *menuitem, GdkEventCrossing *event, GtkWidget *label)
3346 gtk_widget_set_sensitive(GTK_WIDGET(label), TRUE);
3347 return FALSE;
3350 static gboolean
3351 send_to_item_leave_notify_cb(GtkWidget *menuitem, GdkEventCrossing *event, GtkWidget *label)
3353 gtk_widget_set_sensitive(GTK_WIDGET(label), FALSE);
3354 return FALSE;
3357 static GtkWidget *
3358 e2ee_state_to_gtkimage(PurpleE2eeState *state)
3360 PurpleImage *img;
3362 img = _pidgin_e2ee_stock_icon_get(
3363 purple_e2ee_state_get_stock_icon(state));
3364 if (!img)
3365 return NULL;
3367 return gtk_image_new_from_pixbuf(pidgin_pixbuf_from_image(img));
3370 static void
3371 create_sendto_item(GtkWidget *menu, GtkSizeGroup *sg, GSList **group,
3372 PurpleBuddy *buddy, PurpleAccount *account, const char *name,
3373 gboolean e2ee_enabled)
3375 GtkWidget *box;
3376 GtkWidget *label;
3377 GtkWidget *image;
3378 GtkWidget *e2ee_image = NULL;
3379 GtkWidget *menuitem;
3380 GdkPixbuf *pixbuf;
3381 gchar *text;
3383 /* Create a pixmap for the protocol icon. */
3384 pixbuf = pidgin_create_protocol_icon(account, PIDGIN_PROTOCOL_ICON_SMALL);
3386 /* Now convert it to GtkImage */
3387 if (pixbuf == NULL)
3388 image = gtk_image_new();
3389 else
3391 image = gtk_image_new_from_pixbuf(pixbuf);
3392 g_object_unref(G_OBJECT(pixbuf));
3395 if (e2ee_enabled) {
3396 PurpleIMConversation *im;
3397 PurpleE2eeState *state = NULL;
3399 im = purple_conversations_find_im_with_account(
3400 purple_buddy_get_name(buddy), purple_buddy_get_account(buddy));
3401 if (im)
3402 state = purple_conversation_get_e2ee_state(PURPLE_CONVERSATION(im));
3403 if (state)
3404 e2ee_image = e2ee_state_to_gtkimage(state);
3405 else
3406 e2ee_image = gtk_image_new();
3409 gtk_size_group_add_widget(sg, image);
3411 /* Make our menu item */
3412 text = g_strdup_printf("%s (%s)", name, purple_account_get_name_for_display(account));
3413 menuitem = gtk_radio_menu_item_new_with_label(*group, text);
3414 g_free(text);
3415 *group = gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(menuitem));
3417 /* Do some evil, see some evil, speak some evil. */
3418 box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
3420 label = gtk_bin_get_child(GTK_BIN(menuitem));
3421 g_object_ref(label);
3422 gtk_container_remove(GTK_CONTAINER(menuitem), label);
3424 gtk_box_pack_start(GTK_BOX(box), image, FALSE, FALSE, 0);
3426 gtk_box_pack_start(GTK_BOX(box), label, TRUE, TRUE, 4);
3427 if (e2ee_image)
3428 gtk_box_pack_start(GTK_BOX(box), e2ee_image, FALSE, FALSE, 0);
3430 if (buddy != NULL &&
3431 !purple_presence_is_online(purple_buddy_get_presence(buddy)))
3433 gtk_widget_set_sensitive(label, FALSE);
3435 /* Set the label sensitive when the menuitem is highlighted and
3436 * insensitive again when the mouse leaves it. This way, it
3437 * doesn't appear weird from the highlighting of the embossed
3438 * (insensitive style) text.*/
3439 g_signal_connect(menuitem, "enter-notify-event",
3440 G_CALLBACK(send_to_item_enter_notify_cb), label);
3441 g_signal_connect(menuitem, "leave-notify-event",
3442 G_CALLBACK(send_to_item_leave_notify_cb), label);
3445 g_object_unref(label);
3447 gtk_container_add(GTK_CONTAINER(menuitem), box);
3449 gtk_widget_show(label);
3450 gtk_widget_show(image);
3451 if (e2ee_image)
3452 gtk_widget_show(e2ee_image);
3453 gtk_widget_show(box);
3455 /* Set our data and callbacks. */
3456 g_object_set_data(G_OBJECT(menuitem), "purple_account", account);
3457 g_object_set_data_full(G_OBJECT(menuitem), "purple_buddy_name", g_strdup(name), g_free);
3459 g_signal_connect(G_OBJECT(menuitem), "activate",
3460 G_CALLBACK(menu_conv_sel_send_cb), NULL);
3462 gtk_widget_show(menuitem);
3463 gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
3466 static gboolean
3467 compare_buddy_presence(PurplePresence *p1, PurplePresence *p2)
3469 /* This is necessary because multiple PurpleBuddy's don't share the same
3470 * PurplePresence anymore.
3472 PurpleBuddy *b1 = purple_buddy_presence_get_buddy(PURPLE_BUDDY_PRESENCE(p1));
3473 PurpleBuddy *b2 = purple_buddy_presence_get_buddy(PURPLE_BUDDY_PRESENCE(p2));
3474 if (purple_buddy_get_account(b1) == purple_buddy_get_account(b2) &&
3475 purple_strequal(purple_buddy_get_name(b1), purple_buddy_get_name(b2)))
3476 return FALSE;
3477 return TRUE;
3480 static void
3481 generate_send_to_items(PidginConvWindow *win)
3483 GtkWidget *menu;
3484 GSList *group = NULL;
3485 GtkSizeGroup *sg = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL);
3486 PidginConversation *gtkconv;
3487 GSList *l, *buds;
3489 g_return_if_fail(win != NULL);
3491 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
3493 g_return_if_fail(gtkconv != NULL);
3495 if (win->menu->send_to != NULL)
3496 gtk_widget_destroy(win->menu->send_to);
3498 /* Build the Send To menu */
3499 win->menu->send_to = gtk_menu_item_new_with_mnemonic(_("S_end To"));
3500 gtk_widget_show(win->menu->send_to);
3502 menu = gtk_menu_new();
3503 gtk_menu_shell_insert(GTK_MENU_SHELL(win->menu->menubar),
3504 win->menu->send_to, 2);
3505 gtk_menu_item_set_submenu(GTK_MENU_ITEM(win->menu->send_to), menu);
3507 gtk_widget_show(menu);
3509 if (PURPLE_IS_IM_CONVERSATION(gtkconv->active_conv)) {
3510 buds = purple_blist_find_buddies(purple_conversation_get_account(gtkconv->active_conv), purple_conversation_get_name(gtkconv->active_conv));
3512 if (buds == NULL)
3514 /* The user isn't on the buddy list. So we don't create any sendto menu. */
3516 else
3518 gboolean e2ee_enabled = FALSE;
3519 GList *list = NULL, *iter;
3520 for (l = buds; l != NULL; l = l->next)
3522 PurpleBlistNode *node;
3524 node = PURPLE_BLIST_NODE(purple_buddy_get_contact(PURPLE_BUDDY(l->data)));
3526 for (node = node->child; node != NULL; node = node->next)
3528 PurpleBuddy *buddy = (PurpleBuddy *)node;
3529 PurpleAccount *account;
3530 PurpleIMConversation *im;
3532 if (!PURPLE_IS_BUDDY(node))
3533 continue;
3535 im = purple_conversations_find_im_with_account(purple_buddy_get_name(buddy), purple_buddy_get_account(buddy));
3536 if (im && purple_conversation_get_e2ee_state(PURPLE_CONVERSATION(im)) != NULL)
3537 e2ee_enabled = TRUE;
3539 account = purple_buddy_get_account(buddy);
3540 /* TODO WEBKIT: (I'm not actually sure if this is webkit-related --Mark Doliner) */
3541 if (purple_account_is_connected(account) /*|| account == purple_conversation_get_account(gtkconv->active_conv)*/)
3543 /* Use the PurplePresence to get unique buddies. */
3544 PurplePresence *presence = purple_buddy_get_presence(buddy);
3545 if (!g_list_find_custom(list, presence, (GCompareFunc)compare_buddy_presence))
3546 list = g_list_prepend(list, presence);
3551 /* Create the sendto menu only if it has more than one item to show */
3552 if (list && list->next) {
3553 /* Loop over the list backwards so we get the items in the right order,
3554 * since we did a g_list_prepend() earlier. */
3555 for (iter = g_list_last(list); iter != NULL; iter = iter->prev) {
3556 PurplePresence *pre = iter->data;
3557 PurpleBuddy *buddy = purple_buddy_presence_get_buddy(PURPLE_BUDDY_PRESENCE(pre));
3558 create_sendto_item(menu, sg, &group, buddy,
3559 purple_buddy_get_account(buddy), purple_buddy_get_name(buddy), e2ee_enabled);
3562 g_list_free(list);
3563 g_slist_free(buds);
3567 g_object_unref(sg);
3569 gtk_widget_show(win->menu->send_to);
3570 /* TODO: This should never be insensitive. Possibly hidden or not. */
3571 if (!group)
3572 gtk_widget_set_sensitive(win->menu->send_to, FALSE);
3573 update_send_to_selection(win);
3576 PurpleImage *
3577 _pidgin_e2ee_stock_icon_get(const gchar *stock_name)
3579 gchar filename[100], *path;
3580 PurpleImage *image;
3582 /* core is quitting */
3583 if (e2ee_stock == NULL)
3584 return NULL;
3586 if (g_hash_table_lookup_extended(e2ee_stock, stock_name, NULL, (gpointer*)&image))
3587 return image;
3589 g_snprintf(filename, sizeof(filename), "e2ee-%s.png", stock_name);
3590 path = g_build_filename(PURPLE_DATADIR, "pidgin", "icons",
3591 "hicolor", "16x16", "status", filename, NULL);
3592 image = purple_image_new_from_file(path, NULL);
3593 g_free(path);
3595 g_hash_table_insert(e2ee_stock, g_strdup(stock_name), image);
3596 return image;
3599 static void
3600 generate_e2ee_controls(PidginConvWindow *win)
3602 PidginConversation *gtkconv;
3603 PurpleConversation *conv;
3604 PurpleE2eeState *state;
3605 PurpleE2eeProvider *provider;
3606 GtkWidget *menu;
3607 GList *menu_actions, *it;
3608 GtkWidget *e2ee_image;
3610 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
3611 g_return_if_fail(gtkconv != NULL);
3613 conv = gtkconv->active_conv;
3614 g_return_if_fail(conv != NULL);
3616 if (win->menu->e2ee != NULL) {
3617 gtk_widget_destroy(win->menu->e2ee);
3618 win->menu->e2ee = NULL;
3621 provider = purple_e2ee_provider_get_main();
3622 state = purple_conversation_get_e2ee_state(conv);
3623 if (state == NULL || provider == NULL)
3624 return;
3625 if (purple_e2ee_state_get_provider(state) != provider)
3626 return;
3628 win->menu->e2ee = gtk_image_menu_item_new_with_label(
3629 purple_e2ee_provider_get_name(provider));
3631 menu = gtk_menu_new();
3632 gtk_menu_shell_insert(GTK_MENU_SHELL(win->menu->menubar),
3633 win->menu->e2ee, 3);
3634 gtk_menu_item_set_submenu(GTK_MENU_ITEM(win->menu->e2ee), menu);
3636 e2ee_image = e2ee_state_to_gtkimage(state);
3637 if (e2ee_image) {
3638 gtk_image_menu_item_set_image(
3639 GTK_IMAGE_MENU_ITEM(win->menu->e2ee), e2ee_image);
3642 gtk_widget_set_tooltip_text(win->menu->e2ee,
3643 purple_e2ee_state_get_name(state));
3645 menu_actions = purple_e2ee_provider_get_conv_menu_actions(provider, conv);
3646 for (it = menu_actions; it; it = g_list_next(it)) {
3647 PurpleActionMenu *action = it->data;
3649 gtk_widget_show_all(
3650 pidgin_append_menu_action(menu, action, conv));
3652 g_list_free(menu_actions);
3654 gtk_widget_show(win->menu->e2ee);
3655 gtk_widget_show(menu);
3658 static const char *
3659 get_chat_user_status_icon(PurpleChatConversation *chat, const char *name, PurpleChatUserFlags flags)
3661 const char *image = NULL;
3663 if (flags & PURPLE_CHAT_USER_FOUNDER) {
3664 image = PIDGIN_STOCK_STATUS_FOUNDER;
3665 } else if (flags & PURPLE_CHAT_USER_OP) {
3666 image = PIDGIN_STOCK_STATUS_OPERATOR;
3667 } else if (flags & PURPLE_CHAT_USER_HALFOP) {
3668 image = PIDGIN_STOCK_STATUS_HALFOP;
3669 } else if (flags & PURPLE_CHAT_USER_VOICE) {
3670 image = PIDGIN_STOCK_STATUS_VOICE;
3671 } else if ((!flags) && purple_chat_conversation_is_ignored_user(chat, name)) {
3672 image = PIDGIN_STOCK_STATUS_IGNORED;
3673 } else {
3674 return NULL;
3676 return image;
3679 static void
3680 deleting_chat_user_cb(PurpleChatUser *cb)
3682 GtkTreeRowReference *ref = purple_chat_user_get_ui_data(cb);
3684 if (ref) {
3685 gtk_tree_row_reference_free(ref);
3686 purple_chat_user_set_ui_data(cb, NULL);
3690 static void
3691 add_chat_user_common(PurpleChatConversation *chat, PurpleChatUser *cb, const char *old_name)
3693 PidginConversation *gtkconv;
3694 PurpleConversation *conv;
3695 PidginChatPane *gtkchat;
3696 PurpleConnection *gc;
3697 PurpleProtocol *protocol;
3698 GtkTreeModel *tm;
3699 GtkListStore *ls;
3700 GtkTreePath *newpath;
3701 const char *stock;
3702 GtkTreeIter iter;
3703 gboolean is_me = FALSE;
3704 gboolean is_buddy;
3705 const gchar *name, *alias;
3706 gchar *tmp, *alias_key;
3707 PurpleChatUserFlags flags;
3708 GdkRGBA *color = NULL;
3710 alias = purple_chat_user_get_alias(cb);
3711 name = purple_chat_user_get_name(cb);
3712 flags = purple_chat_user_get_flags(cb);
3714 conv = PURPLE_CONVERSATION(chat);
3715 gtkconv = PIDGIN_CONVERSATION(conv);
3716 gtkchat = gtkconv->u.chat;
3717 gc = purple_conversation_get_connection(conv);
3719 if (!gc || !(protocol = purple_connection_get_protocol(gc)))
3720 return;
3722 tm = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
3723 ls = GTK_LIST_STORE(tm);
3725 stock = get_chat_user_status_icon(chat, name, flags);
3727 if (purple_strequal(purple_chat_conversation_get_nick(chat), purple_normalize(purple_conversation_get_account(conv), old_name != NULL ? old_name : name)))
3728 is_me = TRUE;
3730 is_buddy = purple_chat_user_is_buddy(cb);
3732 tmp = g_utf8_casefold(alias, -1);
3733 alias_key = g_utf8_collate_key(tmp, -1);
3734 g_free(tmp);
3736 if (is_me) {
3737 #if 0
3738 /* TODO WEBKIT: No tags in webkit stuff, yet. */
3739 GtkTextTag *tag = gtk_text_tag_table_lookup(
3740 gtk_text_buffer_get_tag_table(GTK_IMHTML(gtkconv->webview)->text_buffer),
3741 "send-name");
3742 g_object_get(tag, "foreground-rgba", &color, NULL);
3743 #endif /* if 0 */
3744 } else {
3745 GtkTextTag *tag;
3746 if ((tag = get_buddy_tag(chat, name, 0, FALSE)))
3747 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_NORMAL, NULL);
3748 if ((tag = get_buddy_tag(chat, name, PURPLE_MESSAGE_NICK, FALSE)))
3749 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_NORMAL, NULL);
3750 color = (GdkRGBA*)get_nick_color(gtkconv, name);
3753 gtk_list_store_insert_with_values(ls, &iter,
3755 * The GTK docs are mute about the effects of the "row" value for performance.
3756 * X-Chat hardcodes their value to 0 (prepend) and -1 (append), so we will too.
3757 * It *might* be faster to search the gtk_list_store and set row accurately,
3758 * but no one in #gtk+ seems to know anything about it either.
3759 * Inserting in the "wrong" location has no visible ill effects. - F.P.
3761 -1, /* "row" */
3762 CHAT_USERS_ICON_STOCK_COLUMN, stock,
3763 CHAT_USERS_ALIAS_COLUMN, alias,
3764 CHAT_USERS_ALIAS_KEY_COLUMN, alias_key,
3765 CHAT_USERS_NAME_COLUMN, name,
3766 CHAT_USERS_FLAGS_COLUMN, flags,
3767 CHAT_USERS_COLOR_COLUMN, color,
3768 CHAT_USERS_WEIGHT_COLUMN, is_buddy ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL,
3769 -1);
3771 if (purple_chat_user_get_ui_data(cb)) {
3772 GtkTreeRowReference *ref = purple_chat_user_get_ui_data(cb);
3773 gtk_tree_row_reference_free(ref);
3776 newpath = gtk_tree_model_get_path(tm, &iter);
3777 purple_chat_user_set_ui_data(cb, gtk_tree_row_reference_new(tm, newpath));
3778 gtk_tree_path_free(newpath);
3780 #if 0
3781 if (is_me && color)
3782 gdk_rgba_free(color);
3783 #endif
3784 g_free(alias_key);
3787 static void topic_callback(GtkWidget *w, PidginConversation *gtkconv)
3789 PurpleProtocol *protocol = NULL;
3790 PurpleConnection *gc;
3791 PurpleConversation *conv = gtkconv->active_conv;
3792 PidginChatPane *gtkchat;
3793 char *new_topic;
3794 const char *current_topic;
3796 gc = purple_conversation_get_connection(conv);
3798 if(!gc || !(protocol = purple_connection_get_protocol(gc)))
3799 return;
3801 if(!PURPLE_PROTOCOL_IMPLEMENTS(protocol, CHAT, set_topic))
3802 return;
3804 gtkconv = PIDGIN_CONVERSATION(conv);
3805 gtkchat = gtkconv->u.chat;
3806 new_topic = g_strdup(gtk_entry_get_text(GTK_ENTRY(gtkchat->topic_text)));
3807 current_topic = purple_chat_conversation_get_topic(PURPLE_CHAT_CONVERSATION(conv));
3809 if(current_topic && !g_utf8_collate(new_topic, current_topic)){
3810 g_free(new_topic);
3811 return;
3814 if (current_topic)
3815 gtk_entry_set_text(GTK_ENTRY(gtkchat->topic_text), current_topic);
3816 else
3817 gtk_entry_set_text(GTK_ENTRY(gtkchat->topic_text), "");
3819 purple_protocol_chat_iface_set_topic(protocol, gc, purple_chat_conversation_get_id(PURPLE_CHAT_CONVERSATION(conv)),
3820 new_topic);
3822 g_free(new_topic);
3825 static gint
3826 sort_chat_users(GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, gpointer userdata)
3828 PurpleChatUserFlags f1 = 0, f2 = 0;
3829 char *user1 = NULL, *user2 = NULL;
3830 gboolean buddy1 = FALSE, buddy2 = FALSE;
3831 gint ret = 0;
3833 gtk_tree_model_get(model, a,
3834 CHAT_USERS_ALIAS_KEY_COLUMN, &user1,
3835 CHAT_USERS_FLAGS_COLUMN, &f1,
3836 CHAT_USERS_WEIGHT_COLUMN, &buddy1,
3837 -1);
3838 gtk_tree_model_get(model, b,
3839 CHAT_USERS_ALIAS_KEY_COLUMN, &user2,
3840 CHAT_USERS_FLAGS_COLUMN, &f2,
3841 CHAT_USERS_WEIGHT_COLUMN, &buddy2,
3842 -1);
3844 /* Only sort by membership levels */
3845 f1 &= PURPLE_CHAT_USER_VOICE | PURPLE_CHAT_USER_HALFOP | PURPLE_CHAT_USER_OP |
3846 PURPLE_CHAT_USER_FOUNDER;
3847 f2 &= PURPLE_CHAT_USER_VOICE | PURPLE_CHAT_USER_HALFOP | PURPLE_CHAT_USER_OP |
3848 PURPLE_CHAT_USER_FOUNDER;
3850 ret = g_strcmp0(user1, user2);
3852 if (user1 != NULL && user2 != NULL) {
3853 if (f1 != f2) {
3854 /* sort more important users first */
3855 ret = (f1 > f2) ? -1 : 1;
3856 } else if (buddy1 != buddy2) {
3857 ret = (buddy1 > buddy2) ? -1 : 1;
3861 g_free(user1);
3862 g_free(user2);
3864 return ret;
3867 static void
3868 update_chat_alias(PurpleBuddy *buddy, PurpleChatConversation *chat, PurpleConnection *gc, PurpleProtocol *protocol)
3870 PidginConversation *gtkconv = PIDGIN_CONVERSATION(PURPLE_CONVERSATION(chat));
3871 PurpleAccount *account = purple_conversation_get_account(PURPLE_CONVERSATION(chat));
3872 GtkTreeModel *model;
3873 char *normalized_name;
3874 GtkTreeIter iter;
3875 int f;
3877 g_return_if_fail(buddy != NULL);
3878 g_return_if_fail(chat != NULL);
3880 /* This is safe because this callback is only used in chats, not IMs. */
3881 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkconv->u.chat->list));
3883 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter))
3884 return;
3886 normalized_name = g_strdup(purple_normalize(account, purple_buddy_get_name(buddy)));
3888 do {
3889 char *name;
3891 gtk_tree_model_get(model, &iter, CHAT_USERS_NAME_COLUMN, &name, -1);
3893 if (purple_strequal(normalized_name, purple_normalize(account, name))) {
3894 const char *alias = name;
3895 char *tmp;
3896 char *alias_key = NULL;
3897 PurpleBuddy *buddy2;
3899 if (!purple_strequal(purple_chat_conversation_get_nick(chat), purple_normalize(account, name))) {
3900 /* This user is not me, so look into updating the alias. */
3902 if ((buddy2 = purple_blist_find_buddy(account, name)) != NULL) {
3903 alias = purple_buddy_get_contact_alias(buddy2);
3906 tmp = g_utf8_casefold(alias, -1);
3907 alias_key = g_utf8_collate_key(tmp, -1);
3908 g_free(tmp);
3910 gtk_list_store_set(GTK_LIST_STORE(model), &iter,
3911 CHAT_USERS_ALIAS_COLUMN, alias,
3912 CHAT_USERS_ALIAS_KEY_COLUMN, alias_key,
3913 -1);
3914 g_free(alias_key);
3916 g_free(name);
3917 break;
3920 f = gtk_tree_model_iter_next(model, &iter);
3922 g_free(name);
3923 } while (f != 0);
3925 g_free(normalized_name);
3928 static void
3929 blist_node_aliased_cb(PurpleBlistNode *node, const char *old_alias, PurpleChatConversation *chat)
3931 PurpleConnection *gc;
3932 PurpleProtocol *protocol;
3933 PurpleConversation *conv = PURPLE_CONVERSATION(chat);
3935 g_return_if_fail(node != NULL);
3936 g_return_if_fail(conv != NULL);
3938 gc = purple_conversation_get_connection(conv);
3939 g_return_if_fail(gc != NULL);
3940 g_return_if_fail(purple_connection_get_protocol(gc) != NULL);
3941 protocol = purple_connection_get_protocol(gc);
3943 if (purple_protocol_get_options(protocol) & OPT_PROTO_UNIQUE_CHATNAME)
3944 return;
3946 if (PURPLE_IS_CONTACT(node))
3948 PurpleBlistNode *bnode;
3950 for(bnode = node->child; bnode; bnode = bnode->next) {
3952 if(!PURPLE_IS_BUDDY(bnode))
3953 continue;
3955 update_chat_alias((PurpleBuddy *)bnode, chat, gc, protocol);
3958 else if (PURPLE_IS_BUDDY(node))
3959 update_chat_alias((PurpleBuddy *)node, chat, gc, protocol);
3960 else if (PURPLE_IS_CHAT(node) &&
3961 purple_conversation_get_account(conv) == purple_chat_get_account((PurpleChat*)node))
3963 if (old_alias == NULL || g_utf8_collate(old_alias, purple_conversation_get_title(conv)) == 0)
3964 pidgin_conv_update_fields(conv, PIDGIN_CONV_SET_TITLE);
3968 static void
3969 buddy_cb_common(PurpleBuddy *buddy, PurpleChatConversation *chat, gboolean is_buddy)
3971 GtkTreeModel *model;
3972 char *normalized_name;
3973 GtkTreeIter iter;
3974 GtkTextTag *texttag;
3975 PurpleConversation *conv = PURPLE_CONVERSATION(chat);
3976 int f;
3978 g_return_if_fail(buddy != NULL);
3979 g_return_if_fail(conv != NULL);
3981 /* Do nothing if the buddy does not belong to the conv's account */
3982 if (purple_buddy_get_account(buddy) != purple_conversation_get_account(conv))
3983 return;
3985 /* This is safe because this callback is only used in chats, not IMs. */
3986 model = gtk_tree_view_get_model(GTK_TREE_VIEW(PIDGIN_CONVERSATION(conv)->u.chat->list));
3988 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter))
3989 return;
3991 normalized_name = g_strdup(purple_normalize(purple_conversation_get_account(conv), purple_buddy_get_name(buddy)));
3993 do {
3994 char *name;
3996 gtk_tree_model_get(model, &iter, CHAT_USERS_NAME_COLUMN, &name, -1);
3998 if (purple_strequal(normalized_name, purple_normalize(purple_conversation_get_account(conv), name))) {
3999 gtk_list_store_set(GTK_LIST_STORE(model), &iter,
4000 CHAT_USERS_WEIGHT_COLUMN, is_buddy ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL, -1);
4001 g_free(name);
4002 break;
4005 f = gtk_tree_model_iter_next(model, &iter);
4007 g_free(name);
4008 } while (f != 0);
4010 g_free(normalized_name);
4012 blist_node_aliased_cb((PurpleBlistNode *)buddy, NULL, chat);
4014 texttag = get_buddy_tag(chat, purple_buddy_get_name(buddy), 0, FALSE); /* XXX: do we want the normalized name? */
4015 if (texttag) {
4016 g_object_set(texttag, "weight", is_buddy ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL, NULL);
4020 static void
4021 buddy_added_cb(PurpleBlistNode *node, PurpleChatConversation *chat)
4023 if (!PURPLE_IS_BUDDY(node))
4024 return;
4026 buddy_cb_common(PURPLE_BUDDY(node), chat, TRUE);
4029 static void
4030 buddy_removed_cb(PurpleBlistNode *node, PurpleChatConversation *chat)
4032 if (!PURPLE_IS_BUDDY(node))
4033 return;
4035 /* If there's another buddy for the same "dude" on the list, do nothing. */
4036 if (purple_blist_find_buddy(purple_buddy_get_account(PURPLE_BUDDY(node)),
4037 purple_buddy_get_name(PURPLE_BUDDY(node))) != NULL)
4038 return;
4040 buddy_cb_common(PURPLE_BUDDY(node), chat, FALSE);
4043 static void
4044 minimum_entry_lines_pref_cb(const char *name,
4045 PurplePrefType type,
4046 gconstpointer value,
4047 gpointer data)
4049 #if 0
4050 GList *l = purple_conversations_get_all();
4051 PurpleConversation *conv;
4052 while (l != NULL)
4054 conv = (PurpleConversation *)l->data;
4056 if (PIDGIN_IS_PIDGIN_CONVERSATION(conv))
4057 resize_webview_cb(PIDGIN_CONVERSATION(conv));
4058 l = l->next;
4060 #endif
4063 static void
4064 setup_chat_topic(PidginConversation *gtkconv, GtkWidget *vbox)
4066 PurpleConversation *conv = gtkconv->active_conv;
4067 PurpleConnection *gc = purple_conversation_get_connection(conv);
4068 PurpleProtocol *protocol = purple_connection_get_protocol(gc);
4069 if (purple_protocol_get_options(protocol) & OPT_PROTO_CHAT_TOPIC)
4071 GtkWidget *hbox, *label;
4072 PidginChatPane *gtkchat = gtkconv->u.chat;
4074 hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PIDGIN_HIG_BOX_SPACE);
4075 gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);
4077 label = gtk_label_new(_("Topic:"));
4078 gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0);
4080 gtkchat->topic_text = gtk_entry_new();
4081 gtk_widget_set_size_request(gtkchat->topic_text, -1, BUDDYICON_SIZE_MIN);
4083 if(!PURPLE_PROTOCOL_IMPLEMENTS(protocol, CHAT, set_topic)) {
4084 gtk_editable_set_editable(GTK_EDITABLE(gtkchat->topic_text), FALSE);
4085 } else {
4086 g_signal_connect(G_OBJECT(gtkchat->topic_text), "activate",
4087 G_CALLBACK(topic_callback), gtkconv);
4090 gtk_box_pack_start(GTK_BOX(hbox), gtkchat->topic_text, TRUE, TRUE, 0);
4091 g_signal_connect(G_OBJECT(gtkchat->topic_text), "key_press_event",
4092 G_CALLBACK(entry_key_press_cb), gtkconv);
4096 static gboolean
4097 pidgin_conv_userlist_create_tooltip(GtkWidget *tipwindow, GtkTreePath *path,
4098 gpointer userdata, int *w, int *h)
4100 PidginConversation *gtkconv = userdata;
4101 GtkTreeIter iter;
4102 GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkconv->u.chat->list));
4103 PurpleConversation *conv = gtkconv->active_conv;
4104 PurpleBlistNode *node;
4105 PurpleProtocol *protocol;
4106 PurpleAccount *account = purple_conversation_get_account(conv);
4107 char *who = NULL;
4109 if (purple_account_get_connection(account) == NULL)
4110 return FALSE;
4112 if (!gtk_tree_model_get_iter(GTK_TREE_MODEL(model), &iter, path))
4113 return FALSE;
4115 gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, CHAT_USERS_NAME_COLUMN, &who, -1);
4117 protocol = purple_connection_get_protocol(purple_account_get_connection(account));
4118 node = (PurpleBlistNode*)(purple_blist_find_buddy(purple_conversation_get_account(conv), who));
4119 if (node && protocol && (purple_protocol_get_options(protocol) & OPT_PROTO_UNIQUE_CHATNAME))
4120 pidgin_blist_draw_tooltip(node, gtkconv->infopane);
4122 g_free(who);
4123 return FALSE;
4126 static void
4127 setup_chat_userlist(PidginConversation *gtkconv, GtkWidget *hpaned)
4129 PidginChatPane *gtkchat = gtkconv->u.chat;
4130 GtkWidget *lbox, *list;
4131 GtkListStore *ls;
4132 GtkCellRenderer *rend;
4133 GtkTreeViewColumn *col;
4134 int ul_width;
4135 void *blist_handle = purple_blist_get_handle();
4136 PurpleConversation *conv = gtkconv->active_conv;
4138 /* Build the right pane. */
4139 lbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, PIDGIN_HIG_BOX_SPACE);
4140 gtk_paned_pack2(GTK_PANED(hpaned), lbox, FALSE, TRUE);
4141 gtk_widget_show(lbox);
4143 /* Setup the label telling how many people are in the room. */
4144 gtkchat->count = gtk_label_new(_("0 people in room"));
4145 gtk_label_set_ellipsize(GTK_LABEL(gtkchat->count), PANGO_ELLIPSIZE_END);
4146 gtk_box_pack_start(GTK_BOX(lbox), gtkchat->count, FALSE, FALSE, 0);
4147 gtk_widget_show(gtkchat->count);
4149 /* Setup the list of users. */
4151 ls = gtk_list_store_new(CHAT_USERS_COLUMNS, GDK_TYPE_PIXBUF, G_TYPE_STRING,
4152 G_TYPE_STRING, G_TYPE_STRING, G_TYPE_INT,
4153 GDK_TYPE_RGBA, G_TYPE_INT, G_TYPE_STRING);
4154 gtk_tree_sortable_set_sort_func(GTK_TREE_SORTABLE(ls), CHAT_USERS_ALIAS_KEY_COLUMN,
4155 sort_chat_users, NULL, NULL);
4157 list = gtk_tree_view_new_with_model(GTK_TREE_MODEL(ls));
4159 /* Allow a user to specify gtkrc settings for the chat userlist only */
4160 gtk_widget_set_name(list, "pidgin_conv_userlist");
4162 rend = gtk_cell_renderer_pixbuf_new();
4163 g_object_set(G_OBJECT(rend),
4164 "stock-size", gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL),
4165 NULL);
4166 col = gtk_tree_view_column_new_with_attributes(NULL, rend,
4167 "stock-id", CHAT_USERS_ICON_STOCK_COLUMN, NULL);
4168 gtk_tree_view_column_set_sizing(col, GTK_TREE_VIEW_COLUMN_AUTOSIZE);
4169 gtk_tree_view_append_column(GTK_TREE_VIEW(list), col);
4170 ul_width = purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/userlist_width");
4171 gtk_widget_set_size_request(lbox, ul_width, -1);
4173 /* Hack to prevent completely collapsed userlist coming back with a 1 pixel width.
4174 * I would have liked to use the GtkPaned "max-position", but for some reason that didn't work */
4175 if (ul_width == 0)
4176 gtk_paned_set_position(GTK_PANED(hpaned), 999999);
4178 g_signal_connect(G_OBJECT(list), "button_press_event",
4179 G_CALLBACK(right_click_chat_cb), gtkconv);
4180 g_signal_connect(G_OBJECT(list), "row-activated",
4181 G_CALLBACK(activate_list_cb), gtkconv);
4182 g_signal_connect(G_OBJECT(list), "popup-menu",
4183 G_CALLBACK(gtkconv_chat_popup_menu_cb), gtkconv);
4184 g_signal_connect(G_OBJECT(lbox), "size-allocate", G_CALLBACK(lbox_size_allocate_cb), gtkconv);
4186 pidgin_tooltip_setup_for_treeview(list, gtkconv,
4187 pidgin_conv_userlist_create_tooltip, NULL);
4189 rend = gtk_cell_renderer_text_new();
4190 g_object_set(rend,
4191 "foreground-set", TRUE,
4192 "weight-set", TRUE,
4193 NULL);
4194 g_object_set(G_OBJECT(rend), "editable", TRUE, NULL);
4196 col = gtk_tree_view_column_new_with_attributes(NULL, rend,
4197 "text", CHAT_USERS_ALIAS_COLUMN,
4198 "foreground-rgba", CHAT_USERS_COLOR_COLUMN,
4199 "weight", CHAT_USERS_WEIGHT_COLUMN,
4200 NULL);
4202 purple_signal_connect(blist_handle, "blist-node-added",
4203 gtkchat, PURPLE_CALLBACK(buddy_added_cb), conv);
4204 purple_signal_connect(blist_handle, "blist-node-removed",
4205 gtkchat, PURPLE_CALLBACK(buddy_removed_cb), conv);
4206 purple_signal_connect(blist_handle, "blist-node-aliased",
4207 gtkchat, PURPLE_CALLBACK(blist_node_aliased_cb), conv);
4209 gtk_tree_view_column_set_expand(col, TRUE);
4210 g_object_set(rend, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
4212 gtk_tree_view_append_column(GTK_TREE_VIEW(list), col);
4214 gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(list), FALSE);
4215 gtk_widget_show(list);
4217 gtkchat->list = list;
4219 gtk_box_pack_start(GTK_BOX(lbox),
4220 pidgin_make_scrollable(list, GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC, GTK_SHADOW_IN, -1, -1),
4221 TRUE, TRUE, 0);
4224 static gboolean
4225 pidgin_conv_create_tooltip(GtkWidget *tipwindow, gpointer userdata, int *w, int *h)
4227 PurpleBlistNode *node = NULL;
4228 PurpleConversation *conv;
4229 PidginConversation *gtkconv = userdata;
4231 conv = gtkconv->active_conv;
4232 if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
4233 node = (PurpleBlistNode*)(purple_blist_find_chat(purple_conversation_get_account(conv), purple_conversation_get_name(conv)));
4234 if (!node)
4235 node = g_object_get_data(G_OBJECT(gtkconv->history), "transient_chat");
4236 } else {
4237 node = (PurpleBlistNode*)(purple_blist_find_buddy(purple_conversation_get_account(conv), purple_conversation_get_name(conv)));
4238 #if 0
4239 /* Using the transient blist nodes to show the tooltip doesn't quite work yet. */
4240 if (!node)
4241 node = g_object_get_data(G_OBJECT(gtkconv->webview), "transient_buddy");
4242 #endif
4245 if (node)
4246 pidgin_blist_draw_tooltip(node, gtkconv->infopane);
4247 return FALSE;
4250 static GtkWidget *
4251 setup_common_pane(PidginConversation *gtkconv)
4253 GtkWidget *vbox, *sw, *event_box, *view;
4254 GtkCellRenderer *rend;
4255 GtkTreePath *path;
4256 PurpleConversation *conv = gtkconv->active_conv;
4257 PurpleBuddy *buddy;
4258 gboolean chat = PURPLE_IS_CHAT_CONVERSATION(conv);
4259 int buddyicon_size = 0;
4261 /* Setup the top part of the pane */
4262 vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, PIDGIN_HIG_BOX_SPACE);
4263 gtk_widget_show(vbox);
4265 /* Setup the info pane */
4266 event_box = gtk_event_box_new();
4267 gtk_event_box_set_visible_window(GTK_EVENT_BOX(event_box), FALSE);
4268 gtk_widget_show(event_box);
4269 gtkconv->infopane_hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
4270 gtk_box_pack_start(GTK_BOX(vbox), event_box, FALSE, FALSE, 0);
4271 gtk_container_add(GTK_CONTAINER(event_box), gtkconv->infopane_hbox);
4272 gtk_widget_show(gtkconv->infopane_hbox);
4273 gtk_widget_add_events(event_box,
4274 GDK_POINTER_MOTION_MASK | GDK_LEAVE_NOTIFY_MASK);
4275 g_signal_connect(G_OBJECT(event_box), "button-press-event",
4276 G_CALLBACK(infopane_press_cb), gtkconv);
4278 pidgin_tooltip_setup_for_widget(event_box, gtkconv,
4279 pidgin_conv_create_tooltip, NULL);
4281 gtkconv->infopane = gtk_cell_view_new();
4282 gtkconv->infopane_model = gtk_list_store_new(CONV_NUM_COLUMNS, G_TYPE_STRING, G_TYPE_STRING, GDK_TYPE_PIXBUF, GDK_TYPE_PIXBUF);
4283 gtk_cell_view_set_model(GTK_CELL_VIEW(gtkconv->infopane),
4284 GTK_TREE_MODEL(gtkconv->infopane_model));
4285 g_object_unref(gtkconv->infopane_model);
4286 gtk_list_store_append(gtkconv->infopane_model, &(gtkconv->infopane_iter));
4287 gtk_box_pack_start(GTK_BOX(gtkconv->infopane_hbox), gtkconv->infopane, TRUE, TRUE, 0);
4288 path = gtk_tree_path_new_from_string("0");
4289 gtk_cell_view_set_displayed_row(GTK_CELL_VIEW(gtkconv->infopane), path);
4290 gtk_tree_path_free(path);
4292 if (chat) {
4293 /* This empty widget is used to ensure that the infopane is consistently
4294 sized for chat windows. The correct fix is to put an icon in the chat
4295 window as well, because that would make "Set Custom Icon" consistent
4296 for both the buddy list and the chat window, but PidginConversation
4297 is pretty much stuck until 3.0. */
4298 GtkWidget *sizing_vbox;
4299 sizing_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
4300 gtk_widget_set_size_request(sizing_vbox, -1, BUDDYICON_SIZE_MIN);
4301 gtk_box_pack_start(GTK_BOX(gtkconv->infopane_hbox), sizing_vbox, FALSE, FALSE, 0);
4302 gtk_widget_show(sizing_vbox);
4304 else {
4305 gtkconv->u.im->icon_container = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
4307 if ((buddy = purple_blist_find_buddy(purple_conversation_get_account(conv),
4308 purple_conversation_get_name(conv))) != NULL) {
4309 PurpleContact *contact = purple_buddy_get_contact(buddy);
4310 if (contact) {
4311 buddyicon_size = purple_blist_node_get_int((PurpleBlistNode*)contact, "pidgin-infopane-iconsize");
4314 buddyicon_size = CLAMP(buddyicon_size, BUDDYICON_SIZE_MIN, BUDDYICON_SIZE_MAX);
4315 gtk_widget_set_size_request(gtkconv->u.im->icon_container, -1, buddyicon_size);
4317 gtk_box_pack_start(GTK_BOX(gtkconv->infopane_hbox),
4318 gtkconv->u.im->icon_container, FALSE, FALSE, 0);
4320 gtk_widget_show(gtkconv->u.im->icon_container);
4323 gtk_widget_show(gtkconv->infopane);
4325 rend = gtk_cell_renderer_pixbuf_new();
4326 gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(gtkconv->infopane), rend, FALSE);
4327 gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(gtkconv->infopane), rend, "stock-id", CONV_ICON_COLUMN, NULL);
4328 g_object_set(rend, "xalign", 0.0, "xpad", 6, "ypad", 0,
4329 "stock-size", gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL),
4330 NULL);
4332 rend = gtk_cell_renderer_text_new();
4333 gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(gtkconv->infopane), rend, TRUE);
4334 gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(gtkconv->infopane), rend, "markup", CONV_TEXT_COLUMN, NULL);
4335 g_object_set(rend, "ypad", 0, "yalign", 0.5, NULL);
4337 g_object_set(rend, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
4339 rend = gtk_cell_renderer_pixbuf_new();
4340 gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(gtkconv->infopane), rend, FALSE);
4341 gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(gtkconv->infopane), rend, "pixbuf", CONV_PROTOCOL_ICON_COLUMN, NULL);
4342 g_object_set(rend, "xalign", 0.0, "xpad", 3, "ypad", 0, NULL);
4344 rend = gtk_cell_renderer_pixbuf_new();
4345 gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(gtkconv->infopane), rend, FALSE);
4346 gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(gtkconv->infopane), rend, "pixbuf", CONV_EMBLEM_COLUMN, NULL);
4347 g_object_set(rend, "xalign", 0.0, "xpad", 6, "ypad", 0, NULL);
4349 /* Setup the history widget */
4350 sw = gtk_scrolled_window_new(NULL, NULL);
4351 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(sw), GTK_SHADOW_IN);
4352 gtk_scrolled_window_set_policy(
4353 GTK_SCROLLED_WINDOW(sw),
4354 GTK_POLICY_NEVER,
4355 GTK_POLICY_ALWAYS
4358 gtkconv->history_buffer = talkatu_history_buffer_new();
4359 gtkconv->history = talkatu_history_new();
4360 gtk_text_view_set_buffer(GTK_TEXT_VIEW(gtkconv->history), gtkconv->history_buffer);
4361 gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(gtkconv->history), GTK_WRAP_WORD);
4362 gtk_container_add(GTK_CONTAINER(sw), gtkconv->history);
4364 if (chat) {
4365 GtkWidget *hpaned;
4367 /* Add the topic */
4368 setup_chat_topic(gtkconv, vbox);
4370 /* Add the talkatu history */
4371 hpaned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
4372 gtk_box_pack_start(GTK_BOX(vbox), hpaned, TRUE, TRUE, 0);
4373 gtk_widget_show(hpaned);
4374 gtk_paned_pack1(GTK_PANED(hpaned), sw, TRUE, TRUE);
4376 /* Now add the userlist */
4377 setup_chat_userlist(gtkconv, hpaned);
4378 } else {
4379 gtk_box_pack_start(GTK_BOX(vbox), sw, TRUE, TRUE, 0);
4381 gtk_widget_show_all(sw);
4383 g_object_set_data(G_OBJECT(gtkconv->history), "gtkconv", gtkconv);
4385 g_signal_connect(G_OBJECT(gtkconv->history), "key_press_event",
4386 G_CALLBACK(refocus_entry_cb), gtkconv);
4387 g_signal_connect(G_OBJECT(gtkconv->history), "key_release_event",
4388 G_CALLBACK(refocus_entry_cb), gtkconv);
4390 gtkconv->lower_hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PIDGIN_HIG_BOX_SPACE);
4391 gtk_box_pack_start(GTK_BOX(vbox), gtkconv->lower_hbox, FALSE, FALSE, 0);
4392 gtk_widget_show(gtkconv->lower_hbox);
4394 /* Setup the entry widget and all signals */
4395 gtkconv->editor = talkatu_editor_new();
4396 talkatu_editor_set_buffer(TALKATU_EDITOR(gtkconv->editor), talkatu_html_buffer_new());
4397 gtk_box_pack_start(GTK_BOX(gtkconv->lower_hbox), gtkconv->editor, TRUE, TRUE, 0);
4399 view = talkatu_editor_get_view(TALKATU_EDITOR(gtkconv->editor));
4400 gtk_widget_set_name(view, "pidgin_conv_entry");
4401 talkatu_view_set_send_binding(TALKATU_VIEW(view), TALKATU_VIEW_SEND_BINDING_RETURN | TALKATU_VIEW_SEND_BINDING_KP_ENTER);
4402 g_signal_connect(
4403 G_OBJECT(view),
4404 "send-message",
4405 G_CALLBACK(send_cb),
4406 gtkconv
4409 if (!chat) {
4410 /* For sending typing notifications for IMs */
4411 gtkconv->u.im->typing_timer = 0;
4412 gtkconv->u.im->animate = purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/animate_buddy_icons");
4413 gtkconv->u.im->show_icon = TRUE;
4416 return vbox;
4419 static PidginConversation *
4420 pidgin_conv_find_gtkconv(PurpleConversation * conv)
4422 PurpleBuddy *bud = purple_blist_find_buddy(purple_conversation_get_account(conv), purple_conversation_get_name(conv));
4423 PurpleContact *c;
4424 PurpleBlistNode *cn, *bn;
4426 if (!bud)
4427 return NULL;
4429 if (!(c = purple_buddy_get_contact(bud)))
4430 return NULL;
4432 cn = PURPLE_BLIST_NODE(c);
4433 for (bn = purple_blist_node_get_first_child(cn); bn; bn = purple_blist_node_get_sibling_next(bn)) {
4434 PurpleBuddy *b = PURPLE_BUDDY(bn);
4435 PurpleIMConversation *im;
4436 if ((im = purple_conversations_find_im_with_account(purple_buddy_get_name(b), purple_buddy_get_account(b)))) {
4437 if (PIDGIN_CONVERSATION(PURPLE_CONVERSATION(im)))
4438 return PIDGIN_CONVERSATION(PURPLE_CONVERSATION(im));
4442 return NULL;
4445 static void
4446 buddy_update_cb(PurpleBlistNode *bnode, gpointer null)
4448 GList *list;
4450 g_return_if_fail(bnode);
4451 if (!PURPLE_IS_BUDDY(bnode))
4452 return;
4454 for (list = pidgin_conv_windows_get_list(); list; list = list->next)
4456 PidginConvWindow *win = list->data;
4457 PurpleConversation *conv = pidgin_conv_window_get_active_conversation(win);
4459 if (!PURPLE_IS_IM_CONVERSATION(conv))
4460 continue;
4462 pidgin_conv_update_fields(conv, PIDGIN_CONV_MENU);
4466 static gboolean
4467 ignore_middle_click(GtkWidget *widget, GdkEventButton *e, gpointer null)
4469 /* A click on the pane is propagated to the notebook containing the pane.
4470 * So if Stu accidentally aims high and middle clicks on the pane-handle,
4471 * it causes a conversation tab to close. Let's stop that from happening.
4473 if (e->button == GDK_BUTTON_MIDDLE && e->type == GDK_BUTTON_PRESS)
4474 return TRUE;
4475 return FALSE;
4478 /**************************************************************************
4479 * Conversation UI operations
4480 **************************************************************************/
4481 static void
4482 private_gtkconv_new(PurpleConversation *conv, gboolean hidden)
4484 PidginConversation *gtkconv;
4485 GtkWidget *pane = NULL;
4486 GtkWidget *tab_cont;
4487 PurpleBlistNode *convnode;
4489 if (PURPLE_IS_IM_CONVERSATION(conv) && (gtkconv = pidgin_conv_find_gtkconv(conv))) {
4490 purple_conversation_set_ui_data(conv, gtkconv);
4491 if (!g_list_find(gtkconv->convs, conv))
4492 gtkconv->convs = g_list_prepend(gtkconv->convs, conv);
4493 pidgin_conv_switch_active_conversation(conv);
4494 return;
4497 gtkconv = g_new0(PidginConversation, 1);
4498 purple_conversation_set_ui_data(conv, gtkconv);
4499 gtkconv->active_conv = conv;
4500 gtkconv->convs = g_list_prepend(gtkconv->convs, conv);
4501 gtkconv->send_history = g_list_append(NULL, NULL);
4503 /* Setup some initial variables. */
4504 gtkconv->unseen_state = PIDGIN_UNSEEN_NONE;
4505 gtkconv->unseen_count = 0;
4506 gtkconv->last_flags = 0;
4508 if (PURPLE_IS_IM_CONVERSATION(conv)) {
4509 gtkconv->u.im = g_malloc0(sizeof(PidginImPane));
4510 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
4511 gtkconv->u.chat = g_malloc0(sizeof(PidginChatPane));
4513 pane = setup_common_pane(gtkconv);
4515 if (pane == NULL) {
4516 if (PURPLE_IS_CHAT_CONVERSATION(conv))
4517 g_free(gtkconv->u.chat);
4518 else if (PURPLE_IS_IM_CONVERSATION(conv))
4519 g_free(gtkconv->u.im);
4521 g_free(gtkconv);
4522 purple_conversation_set_ui_data(conv, NULL);
4523 return;
4526 g_signal_connect(G_OBJECT(pane), "button_press_event",
4527 G_CALLBACK(ignore_middle_click), NULL);
4529 /* Setup the container for the tab. */
4530 gtkconv->tab_cont = tab_cont = gtk_box_new(GTK_ORIENTATION_VERTICAL, PIDGIN_HIG_BOX_SPACE);
4531 g_object_set_data(G_OBJECT(tab_cont), "PidginConversation", gtkconv);
4532 gtk_container_set_border_width(GTK_CONTAINER(tab_cont), PIDGIN_HIG_BOX_SPACE);
4533 gtk_box_pack_start(GTK_BOX(tab_cont), pane, TRUE, TRUE, 0);
4534 gtk_widget_show(pane);
4536 convnode = get_conversation_blist_node(conv);
4537 if (convnode == NULL || !purple_blist_node_get_bool(convnode, "gtk-mute-sound"))
4538 gtkconv->make_sound = TRUE;
4540 if (convnode != NULL && purple_blist_node_has_setting(convnode, "enable-logging")) {
4541 gboolean logging = purple_blist_node_get_bool(convnode, "enable-logging");
4542 purple_conversation_set_logging(conv, logging);
4545 talkatu_editor_set_toolbar_visible(
4546 TALKATU_EDITOR(gtkconv->editor),
4547 purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/show_formatting_toolbar")
4550 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons"))
4551 gtk_widget_show(gtkconv->infopane_hbox);
4552 else
4553 gtk_widget_hide(gtkconv->infopane_hbox);
4556 g_signal_connect_swapped(G_OBJECT(pane), "focus",
4557 G_CALLBACK(gtk_widget_grab_focus),
4558 gtkconv->editor);
4560 if (hidden)
4561 pidgin_conv_window_add_gtkconv(hidden_convwin, gtkconv);
4562 else
4563 pidgin_conv_placement_place(gtkconv);
4565 if (generated_nick_colors == NULL) {
4566 GdkColor color;
4567 GdkRGBA rgba;
4568 color = gtk_widget_get_style(gtkconv->history)->base[GTK_STATE_NORMAL];
4569 rgba.red = color.red / 65535.0;
4570 rgba.green = color.green / 65535.0;
4571 rgba.blue = color.blue / 65535.0;
4572 rgba.alpha = 1.0;
4573 generated_nick_colors = generate_nick_colors(NICK_COLOR_GENERATE_COUNT, rgba);
4576 gtkconv->nick_colors = g_array_ref(generated_nick_colors);
4579 static void
4580 pidgin_conv_new_hidden(PurpleConversation *conv)
4582 private_gtkconv_new(conv, TRUE);
4585 void
4586 pidgin_conv_new(PurpleConversation *conv)
4588 private_gtkconv_new(conv, FALSE);
4589 if (PIDGIN_IS_PIDGIN_CONVERSATION(conv))
4590 purple_signal_emit(pidgin_conversations_get_handle(),
4591 "conversation-displayed", PIDGIN_CONVERSATION(conv));
4594 static void
4595 received_im_msg_cb(PurpleAccount *account, char *sender, char *message,
4596 PurpleConversation *conv, PurpleMessageFlags flags)
4598 PurpleConversationUiOps *ui_ops = pidgin_conversations_get_conv_ui_ops();
4599 gboolean hide = FALSE;
4600 guint timer;
4602 /* create hidden conv if hide_new pref is always */
4603 if (purple_strequal(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new"), "always"))
4604 hide = TRUE;
4606 /* create hidden conv if hide_new pref is away and account is away */
4607 if (purple_strequal(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new"), "away") &&
4608 !purple_status_is_available(purple_account_get_active_status(account)))
4609 hide = TRUE;
4611 if (conv && PIDGIN_IS_PIDGIN_CONVERSATION(conv) && !hide) {
4612 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
4613 if (gtkconv->win == hidden_convwin) {
4614 pidgin_conv_attach_to_conversation(gtkconv->active_conv);
4616 return;
4619 if (hide) {
4620 ui_ops->create_conversation = pidgin_conv_new_hidden;
4621 purple_im_conversation_new(account, sender);
4622 ui_ops->create_conversation = pidgin_conv_new;
4625 /* Somebody wants to keep this conversation around, so don't time it out */
4626 if (conv) {
4627 timer = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(conv), "close-timer"));
4628 if (timer) {
4629 g_source_remove(timer);
4630 g_object_set_data(G_OBJECT(conv), "close-timer", GINT_TO_POINTER(0));
4635 static void
4636 pidgin_conv_destroy(PurpleConversation *conv)
4638 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
4640 gtkconv->convs = g_list_remove(gtkconv->convs, conv);
4641 /* Don't destroy ourselves until all our convos are gone */
4642 if (gtkconv->convs) {
4643 /* Make sure the destroyed conversation is not the active one */
4644 if (gtkconv->active_conv == conv) {
4645 gtkconv->active_conv = gtkconv->convs->data;
4646 purple_conversation_update(gtkconv->active_conv, PURPLE_CONVERSATION_UPDATE_FEATURES);
4648 return;
4651 pidgin_conv_window_remove_gtkconv(gtkconv->win, gtkconv);
4653 /* If the "Save Conversation" or "Save Icon" dialogs are open then close them */
4654 purple_request_close_with_handle(gtkconv);
4655 purple_notify_close_with_handle(gtkconv);
4657 gtk_widget_destroy(gtkconv->tab_cont);
4658 g_object_unref(gtkconv->tab_cont);
4660 if (PURPLE_IS_IM_CONVERSATION(conv)) {
4661 if (gtkconv->u.im->icon_timer != 0)
4662 g_source_remove(gtkconv->u.im->icon_timer);
4664 if (gtkconv->u.im->anim != NULL)
4665 g_object_unref(G_OBJECT(gtkconv->u.im->anim));
4667 if (gtkconv->u.im->typing_timer != 0)
4668 g_source_remove(gtkconv->u.im->typing_timer);
4670 g_free(gtkconv->u.im);
4671 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
4672 purple_signals_disconnect_by_handle(gtkconv->u.chat);
4673 g_free(gtkconv->u.chat);
4676 gtkconv->send_history = g_list_first(gtkconv->send_history);
4677 g_list_free_full(gtkconv->send_history, g_free);
4679 if (gtkconv->attach_timer) {
4680 g_source_remove(gtkconv->attach_timer);
4683 g_array_unref(gtkconv->nick_colors);
4685 g_free(gtkconv);
4688 #if 0
4689 static const char *
4690 get_text_tag_color(GtkTextTag *tag)
4692 GdkRGBA *color = NULL;
4693 gboolean set = FALSE;
4694 static char colcode[] = "#XXXXXX";
4695 if (tag)
4696 g_object_get(G_OBJECT(tag), "foreground-set", &set, "foreground-rgba", &color, NULL);
4697 if (set && color)
4698 g_snprintf(colcode, sizeof(colcode), "#%02x%02x%02x",
4699 (unsigned int)(color->red * 255),
4700 (unsigned int)(color->green * 255),
4701 (unsigned int)(color->blue * 255));
4702 else
4703 colcode[0] = '\0';
4704 if (color)
4705 gdk_rgba_free(color);
4706 return colcode;
4709 /* The callback for an event on a link tag. */
4710 static gboolean buddytag_event(GtkTextTag *tag, GObject *imhtml,
4711 GdkEvent *event, GtkTextIter *arg2, gpointer data)
4713 if (event->type == GDK_BUTTON_PRESS
4714 || event->type == GDK_2BUTTON_PRESS) {
4715 GdkEventButton *btn_event = (GdkEventButton*) event;
4716 PurpleConversation *conv = data;
4717 char *buddyname;
4718 gchar *name;
4720 g_object_get(G_OBJECT(tag), "name", &name, NULL);
4722 /* strlen("BUDDY " or "HILIT ") == 6 */
4723 g_return_val_if_fail((name != NULL) && (strlen(name) > 6), FALSE);
4725 buddyname = name + 6;
4727 /* emit chat-nick-clicked signal */
4728 if (event->type == GDK_BUTTON_PRESS) {
4729 gint plugin_return = GPOINTER_TO_INT(purple_signal_emit_return_1(
4730 pidgin_conversations_get_handle(), "chat-nick-clicked",
4731 data, buddyname, btn_event->button));
4732 if (plugin_return) {
4733 g_free(name);
4734 return TRUE;
4738 if (btn_event->button == GDK_BUTTON_PRIMARY && event->type == GDK_2BUTTON_PRESS) {
4739 chat_do_im(PIDGIN_CONVERSATION(conv), buddyname);
4740 g_free(name);
4742 return TRUE;
4743 } else if (btn_event->button == GDK_BUTTON_MIDDLE && event->type == GDK_2BUTTON_PRESS) {
4744 chat_do_info(PIDGIN_CONVERSATION(conv), buddyname);
4745 g_free(name);
4747 return TRUE;
4748 } else if (gdk_event_triggers_context_menu(event)) {
4749 GtkTextIter start, end;
4751 /* we shouldn't display the popup
4752 * if the user has selected something: */
4753 if (!gtk_text_buffer_get_selection_bounds(
4754 gtk_text_iter_get_buffer(arg2),
4755 &start, &end)) {
4756 GtkWidget *menu = NULL;
4757 PurpleConnection *gc =
4758 purple_conversation_get_connection(conv);
4760 menu = create_chat_menu(conv, buddyname, gc);
4761 gtk_menu_popup_at_pointer(GTK_MENU(menu), event);
4763 g_free(name);
4765 /* Don't propagate the event any further */
4766 return TRUE;
4770 g_free(name);
4773 return FALSE;
4775 #endif
4777 static GtkTextTag *get_buddy_tag(PurpleChatConversation *chat, const char *who, PurpleMessageFlags flag,
4778 gboolean create)
4780 /* TODO WEBKIT */
4781 #if 0
4782 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
4783 GtkTextTag *buddytag;
4784 gchar *str;
4785 gboolean highlight = (flag & PURPLE_MESSAGE_NICK);
4786 GtkTextBuffer *buffer = GTK_IMHTML(gtkconv->imhtml)->text_buffer;
4788 str = g_strdup_printf(highlight ? "HILIT %s" : "BUDDY %s", who);
4790 buddytag = gtk_text_tag_table_lookup(
4791 gtk_text_buffer_get_tag_table(buffer), str);
4793 if (buddytag == NULL && create) {
4794 if (highlight)
4795 buddytag = gtk_text_buffer_create_tag(buffer, str,
4796 "foreground", get_text_tag_color(gtk_text_tag_table_lookup(
4797 gtk_text_buffer_get_tag_table(buffer), "highlight-name")),
4798 "weight", PANGO_WEIGHT_BOLD,
4799 NULL);
4800 else
4801 buddytag = gtk_text_buffer_create_tag(
4802 buffer, str,
4803 "foreground-rgba", get_nick_color(gtkconv, who),
4804 "weight", purple_blist_find_buddy(purple_conversation_get_account(conv), who) ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL,
4805 NULL);
4807 g_object_set_data(G_OBJECT(buddytag), "cursor", "");
4808 g_signal_connect(G_OBJECT(buddytag), "event",
4809 G_CALLBACK(buddytag_event), conv);
4812 g_free(str);
4814 return buddytag;
4815 #endif /* if 0 */
4816 return NULL;
4819 static gboolean
4820 writing_msg(PurpleConversation *conv, PurpleMessage *msg, gpointer _unused)
4822 PidginConversation *gtkconv;
4824 g_return_val_if_fail(msg != NULL, FALSE);
4826 if (!(purple_message_get_flags(msg) & PURPLE_MESSAGE_ACTIVE_ONLY))
4827 return FALSE;
4829 g_return_val_if_fail(conv != NULL, FALSE);
4830 gtkconv = PIDGIN_CONVERSATION(conv);
4831 g_return_val_if_fail(gtkconv != NULL, FALSE);
4833 if (conv == gtkconv->active_conv)
4834 return FALSE;
4836 purple_debug_info("gtkconv",
4837 "Suppressing message for an inactive conversation");
4839 return TRUE;
4842 static void
4843 pidgin_conv_write_conv(PurpleConversation *conv, PurpleMessage *pmsg)
4845 PidginMessage *pidgin_msg = NULL;
4846 PurpleMessageFlags flags;
4847 PidginConversation *gtkconv;
4848 PurpleConnection *gc;
4849 PurpleAccount *account;
4850 gboolean plugin_return;
4852 g_return_if_fail(conv != NULL);
4853 gtkconv = PIDGIN_CONVERSATION(conv);
4854 g_return_if_fail(gtkconv != NULL);
4855 flags = purple_message_get_flags(pmsg);
4857 #if 0
4858 if (gtkconv->attach_timer) {
4859 /* We are currently in the process of filling up the buffer with the message
4860 * history of the conversation. So we do not need to add the message here.
4861 * Instead, this message will be added to the message-list, which in turn will
4862 * be processed and displayed by the attach-callback.
4864 return;
4867 if (conv != gtkconv->active_conv)
4869 /* Set the active conversation to the one that just messaged us. */
4870 /* TODO: consider not doing this if the account is offline or something */
4871 if (flags & (PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_RECV))
4872 pidgin_conv_switch_active_conversation(conv);
4874 #endif
4876 account = purple_conversation_get_account(conv);
4877 g_return_if_fail(account != NULL);
4878 gc = purple_account_get_connection(account);
4879 g_return_if_fail(gc != NULL || !(flags & (PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_RECV)));
4881 plugin_return = GPOINTER_TO_INT(purple_signal_emit_return_1(
4882 pidgin_conversations_get_handle(),
4883 (PURPLE_IS_IM_CONVERSATION(conv) ? "displaying-im-msg" : "displaying-chat-msg"),
4884 conv, pmsg));
4885 if (plugin_return)
4887 return;
4890 pidgin_msg = pidgin_message_new(pmsg);
4891 talkatu_history_buffer_write_message(
4892 TALKATU_HISTORY_BUFFER(gtkconv->history_buffer),
4893 TALKATU_MESSAGE(pidgin_msg)
4896 /* Tab highlighting stuff */
4897 if (!(flags & PURPLE_MESSAGE_SEND) && !pidgin_conv_has_focus(conv))
4899 PidginUnseenState unseen = PIDGIN_UNSEEN_NONE;
4901 if ((flags & PURPLE_MESSAGE_NICK) == PURPLE_MESSAGE_NICK)
4902 unseen = PIDGIN_UNSEEN_NICK;
4903 else if (((flags & PURPLE_MESSAGE_SYSTEM) == PURPLE_MESSAGE_SYSTEM) ||
4904 ((flags & PURPLE_MESSAGE_ERROR) == PURPLE_MESSAGE_ERROR))
4905 unseen = PIDGIN_UNSEEN_EVENT;
4906 else if ((flags & PURPLE_MESSAGE_NO_LOG) == PURPLE_MESSAGE_NO_LOG)
4907 unseen = PIDGIN_UNSEEN_NO_LOG;
4908 else
4909 unseen = PIDGIN_UNSEEN_TEXT;
4911 gtkconv_set_unseen(gtkconv, unseen);
4914 /* on rejoin only request message history from after this message */
4915 if (flags & (PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_RECV) &&
4916 PURPLE_IS_CHAT_CONVERSATION(conv)) {
4917 PurpleChat *chat = purple_blist_find_chat(
4918 purple_conversation_get_account(conv),
4919 purple_conversation_get_name(conv));
4920 if (chat) {
4921 GHashTable *comps = purple_chat_get_components(chat);
4922 time_t now, history_since, prev_history_since = 0;
4923 struct tm *history_since_tm;
4924 const char *history_since_s, *prev_history_since_s;
4926 history_since = purple_message_get_time(pmsg) + 1;
4928 prev_history_since_s = g_hash_table_lookup(comps,
4929 "history_since");
4930 if (prev_history_since_s != NULL)
4931 prev_history_since = purple_str_to_time(
4932 prev_history_since_s, TRUE, NULL, NULL,
4933 NULL);
4935 now = time(NULL);
4936 /* in case of incorrectly stored timestamps */
4937 if (prev_history_since > now)
4938 prev_history_since = now;
4939 /* in case of delayed messages */
4940 if (history_since < prev_history_since)
4941 history_since = prev_history_since;
4943 history_since_tm = gmtime(&history_since);
4944 history_since_s = purple_utf8_strftime(
4945 "%Y-%m-%dT%H:%M:%SZ", history_since_tm);
4946 if (!purple_strequal(prev_history_since_s,
4947 history_since_s))
4948 g_hash_table_replace(comps,
4949 g_strdup("history_since"),
4950 g_strdup(history_since_s));
4954 purple_signal_emit(pidgin_conversations_get_handle(),
4955 (PURPLE_IS_IM_CONVERSATION(conv) ? "displayed-im-msg" : "displayed-chat-msg"),
4956 conv, pmsg);
4957 update_typing_message(gtkconv, NULL);
4960 static gboolean get_iter_from_chatuser(PurpleChatUser *cb, GtkTreeIter *iter)
4962 GtkTreeRowReference *ref;
4963 GtkTreePath *path;
4964 GtkTreeModel *model;
4966 g_return_val_if_fail(cb != NULL, FALSE);
4968 ref = purple_chat_user_get_ui_data(cb);
4969 if (!ref)
4970 return FALSE;
4972 if ((path = gtk_tree_row_reference_get_path(ref)) == NULL)
4973 return FALSE;
4975 model = gtk_tree_row_reference_get_model(ref);
4976 if (!gtk_tree_model_get_iter(GTK_TREE_MODEL(model), iter, path)) {
4977 gtk_tree_path_free(path);
4978 return FALSE;
4981 gtk_tree_path_free(path);
4982 return TRUE;
4985 static void
4986 pidgin_conv_chat_add_users(PurpleChatConversation *chat, GList *cbuddies, gboolean new_arrivals)
4988 PidginConversation *gtkconv;
4989 PidginChatPane *gtkchat;
4990 GtkListStore *ls;
4991 GList *l;
4993 char tmp[BUF_LONG];
4994 int num_users;
4996 gtkconv = PIDGIN_CONVERSATION(PURPLE_CONVERSATION(chat));
4997 gtkchat = gtkconv->u.chat;
4999 num_users = purple_chat_conversation_get_users_count(chat);
5001 g_snprintf(tmp, sizeof(tmp),
5002 ngettext("%d person in room", "%d people in room",
5003 num_users),
5004 num_users);
5006 gtk_label_set_text(GTK_LABEL(gtkchat->count), tmp);
5008 ls = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list)));
5010 gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(ls), GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID,
5011 GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID);
5013 l = cbuddies;
5014 while (l != NULL) {
5015 add_chat_user_common(chat, (PurpleChatUser *)l->data, NULL);
5016 l = l->next;
5019 /* Currently GTK+ maintains our sorted list after it's in the tree.
5020 * This may change if it turns out we can manage it faster ourselves.
5022 gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(ls), CHAT_USERS_ALIAS_KEY_COLUMN,
5023 GTK_SORT_ASCENDING);
5026 static void
5027 pidgin_conv_chat_rename_user(PurpleChatConversation *chat, const char *old_name,
5028 const char *new_name, const char *new_alias)
5030 PidginConversation *gtkconv;
5031 PidginChatPane *gtkchat;
5032 PurpleChatUser *old_chatuser, *new_chatuser;
5033 GtkTreeIter iter;
5034 GtkTreeModel *model;
5035 GtkTextTag *tag;
5037 gtkconv = PIDGIN_CONVERSATION(PURPLE_CONVERSATION(chat));
5038 gtkchat = gtkconv->u.chat;
5040 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
5042 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter))
5043 return;
5045 if ((tag = get_buddy_tag(chat, old_name, 0, FALSE)))
5046 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_ITALIC, NULL);
5047 if ((tag = get_buddy_tag(chat, old_name, PURPLE_MESSAGE_NICK, FALSE)))
5048 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_ITALIC, NULL);
5050 old_chatuser = purple_chat_conversation_find_user(chat, old_name);
5051 if (!old_chatuser)
5052 return;
5054 if (get_iter_from_chatuser(old_chatuser, &iter)) {
5055 GtkTreeRowReference *ref = purple_chat_user_get_ui_data(old_chatuser);
5057 gtk_list_store_remove(GTK_LIST_STORE(model), &iter);
5058 gtk_tree_row_reference_free(ref);
5059 purple_chat_user_set_ui_data(old_chatuser, NULL);
5062 g_return_if_fail(new_alias != NULL);
5064 new_chatuser = purple_chat_conversation_find_user(chat, new_name);
5066 add_chat_user_common(chat, new_chatuser, old_name);
5069 static void
5070 pidgin_conv_chat_remove_users(PurpleChatConversation *chat, GList *users)
5072 PidginConversation *gtkconv;
5073 PidginChatPane *gtkchat;
5074 GtkTreeIter iter;
5075 GtkTreeModel *model;
5076 GList *l;
5077 char tmp[BUF_LONG];
5078 int num_users;
5079 gboolean f;
5080 GtkTextTag *tag;
5082 gtkconv = PIDGIN_CONVERSATION(PURPLE_CONVERSATION(chat));
5083 gtkchat = gtkconv->u.chat;
5085 num_users = purple_chat_conversation_get_users_count(chat);
5087 for (l = users; l != NULL; l = l->next) {
5088 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
5090 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter))
5091 /* XXX: Break? */
5092 continue;
5094 do {
5095 char *val;
5097 gtk_tree_model_get(GTK_TREE_MODEL(model), &iter,
5098 CHAT_USERS_NAME_COLUMN, &val, -1);
5100 if (!purple_utf8_strcasecmp((char *)l->data, val)) {
5101 f = gtk_list_store_remove(GTK_LIST_STORE(model), &iter);
5103 else
5104 f = gtk_tree_model_iter_next(GTK_TREE_MODEL(model), &iter);
5106 g_free(val);
5107 } while (f);
5109 if ((tag = get_buddy_tag(chat, l->data, 0, FALSE)))
5110 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_ITALIC, NULL);
5111 if ((tag = get_buddy_tag(chat, l->data, PURPLE_MESSAGE_NICK, FALSE)))
5112 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_ITALIC, NULL);
5115 g_snprintf(tmp, sizeof(tmp),
5116 ngettext("%d person in room", "%d people in room",
5117 num_users), num_users);
5119 gtk_label_set_text(GTK_LABEL(gtkchat->count), tmp);
5122 static void
5123 pidgin_conv_chat_update_user(PurpleChatUser *chatuser)
5125 PurpleChatConversation *chat;
5126 PidginConversation *gtkconv;
5127 PidginChatPane *gtkchat;
5128 GtkTreeIter iter;
5129 GtkTreeModel *model;
5131 if (!chatuser)
5132 return;
5134 chat = purple_chat_user_get_chat(chatuser);
5135 gtkconv = PIDGIN_CONVERSATION(PURPLE_CONVERSATION(chat));
5136 gtkchat = gtkconv->u.chat;
5138 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
5140 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter))
5141 return;
5143 if (get_iter_from_chatuser(chatuser, &iter)) {
5144 GtkTreeRowReference *ref = purple_chat_user_get_ui_data(chatuser);
5145 gtk_list_store_remove(GTK_LIST_STORE(model), &iter);
5146 gtk_tree_row_reference_free(ref);
5147 purple_chat_user_set_ui_data(chatuser, NULL);
5150 add_chat_user_common(chat, chatuser, NULL);
5153 gboolean
5154 pidgin_conv_has_focus(PurpleConversation *conv)
5156 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
5157 PidginConvWindow *win;
5158 gboolean has_focus;
5160 win = gtkconv->win;
5162 g_object_get(G_OBJECT(win->window), "has-toplevel-focus", &has_focus, NULL);
5164 if (has_focus && pidgin_conv_window_is_active_conversation(conv))
5165 return TRUE;
5167 return FALSE;
5171 * Makes sure all the menu items and all the buttons are hidden/shown and
5172 * sensitive/insensitive. This is called after changing tabs and when an
5173 * account signs on or off.
5175 static void
5176 gray_stuff_out(PidginConversation *gtkconv)
5178 PidginConvWindow *win;
5179 PurpleConversation *conv = gtkconv->active_conv;
5180 PurpleConnection *gc;
5181 PurpleProtocol *protocol = NULL;
5182 GdkPixbuf *window_icon = NULL;
5183 // PidginWebViewButtons buttons;
5184 PurpleAccount *account;
5186 win = pidgin_conv_get_window(gtkconv);
5187 gc = purple_conversation_get_connection(conv);
5188 account = purple_conversation_get_account(conv);
5190 if (gc != NULL)
5191 protocol = purple_connection_get_protocol(gc);
5193 if (win->menu->send_to != NULL)
5194 update_send_to_selection(win);
5197 * Handle hiding and showing stuff based on what type of conv this is.
5198 * Stuff that Purple IMs support in general should be shown for IM
5199 * conversations. Stuff that Purple chats support in general should be
5200 * shown for chat conversations. It doesn't matter whether the protocol
5201 * supports it or not--that only affects if the button or menu item
5202 * is sensitive or not.
5204 if (PURPLE_IS_IM_CONVERSATION(conv)) {
5205 /* Show stuff that applies to IMs, hide stuff that applies to chats */
5207 /* Deal with menu items */
5208 gtk_action_set_visible(win->menu->view_log, TRUE);
5209 gtk_action_set_visible(win->menu->send_file, TRUE);
5210 gtk_action_set_visible(win->menu->get_attention, TRUE);
5211 gtk_action_set_visible(win->menu->add_pounce, TRUE);
5212 gtk_action_set_visible(win->menu->get_info, TRUE);
5213 gtk_action_set_visible(win->menu->invite, FALSE);
5214 gtk_action_set_visible(win->menu->alias, TRUE);
5215 if (purple_account_privacy_check(account, purple_conversation_get_name(conv))) {
5216 gtk_action_set_visible(win->menu->unblock, FALSE);
5217 gtk_action_set_visible(win->menu->block, TRUE);
5218 } else {
5219 gtk_action_set_visible(win->menu->block, FALSE);
5220 gtk_action_set_visible(win->menu->unblock, TRUE);
5223 if (purple_blist_find_buddy(account, purple_conversation_get_name(conv)) == NULL) {
5224 gtk_action_set_visible(win->menu->add, TRUE);
5225 gtk_action_set_visible(win->menu->remove, FALSE);
5226 } else {
5227 gtk_action_set_visible(win->menu->remove, TRUE);
5228 gtk_action_set_visible(win->menu->add, FALSE);
5231 gtk_action_set_visible(win->menu->insert_link, TRUE);
5232 gtk_action_set_visible(win->menu->insert_image, TRUE);
5233 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
5234 /* Show stuff that applies to Chats, hide stuff that applies to IMs */
5236 /* Deal with menu items */
5237 gtk_action_set_visible(win->menu->view_log, TRUE);
5238 gtk_action_set_visible(win->menu->send_file, FALSE);
5239 gtk_action_set_visible(win->menu->get_attention, FALSE);
5240 gtk_action_set_visible(win->menu->add_pounce, FALSE);
5241 gtk_action_set_visible(win->menu->get_info, FALSE);
5242 gtk_action_set_visible(win->menu->invite, TRUE);
5243 gtk_action_set_visible(win->menu->alias, TRUE);
5244 gtk_action_set_visible(win->menu->block, FALSE);
5245 gtk_action_set_visible(win->menu->unblock, FALSE);
5247 if ((account == NULL) || purple_blist_find_chat(account, purple_conversation_get_name(conv)) == NULL) {
5248 /* If the chat is NOT in the buddy list */
5249 gtk_action_set_visible(win->menu->add, TRUE);
5250 gtk_action_set_visible(win->menu->remove, FALSE);
5251 } else {
5252 /* If the chat IS in the buddy list */
5253 gtk_action_set_visible(win->menu->add, FALSE);
5254 gtk_action_set_visible(win->menu->remove, TRUE);
5257 gtk_action_set_visible(win->menu->insert_link, TRUE);
5258 gtk_action_set_visible(win->menu->insert_image, TRUE);
5262 * Handle graying stuff out based on whether an account is connected
5263 * and what features that account supports.
5265 if ((gc != NULL) &&
5266 (!PURPLE_IS_CHAT_CONVERSATION(conv) ||
5267 !purple_chat_conversation_has_left(PURPLE_CHAT_CONVERSATION(conv)) ))
5269 PurpleConnectionFlags features = purple_conversation_get_features(conv);
5270 /* Account is online */
5271 /* Deal with the toolbar */
5272 #if 0
5273 if (features & PURPLE_CONNECTION_FLAG_HTML)
5275 buttons = PIDGIN_WEBVIEW_ALL; /* Everything on */
5276 if (features & PURPLE_CONNECTION_FLAG_NO_BGCOLOR)
5277 buttons &= ~PIDGIN_WEBVIEW_BACKCOLOR;
5278 if (features & PURPLE_CONNECTION_FLAG_NO_FONTSIZE)
5280 buttons &= ~PIDGIN_WEBVIEW_GROW;
5281 buttons &= ~PIDGIN_WEBVIEW_SHRINK;
5283 if (features & PURPLE_CONNECTION_FLAG_NO_URLDESC)
5284 buttons &= ~PIDGIN_WEBVIEW_LINKDESC
5285 } else {
5286 buttons = PIDGIN_WEBVIEW_SMILEY | PIDGIN_WEBVIEW_IMAGE;
5289 if (features & PURPLE_CONNECTION_FLAG_NO_IMAGES)
5290 buttons &= ~PIDGIN_WEBVIEW_IMAGE;
5292 if (features & PURPLE_CONNECTION_FLAG_ALLOW_CUSTOM_SMILEY)
5293 buttons |= PIDGIN_WEBVIEW_CUSTOM_SMILEY;
5294 else
5295 buttons &= ~PIDGIN_WEBVIEW_CUSTOM_SMILEY;
5297 pidgin_webview_set_format_functions(PIDGIN_WEBVIEW(gtkconv->entry), buttons);
5298 #endif
5300 /* Deal with menu items */
5301 gtk_action_set_sensitive(win->menu->view_log, TRUE);
5302 gtk_action_set_sensitive(win->menu->add_pounce, TRUE);
5303 gtk_action_set_sensitive(win->menu->get_info, (PURPLE_PROTOCOL_IMPLEMENTS(protocol, SERVER, get_info)));
5304 gtk_action_set_sensitive(win->menu->invite, (PURPLE_PROTOCOL_IMPLEMENTS(protocol, CHAT, invite)));
5305 gtk_action_set_sensitive(win->menu->insert_link, (features & PURPLE_CONNECTION_FLAG_HTML));
5306 gtk_action_set_sensitive(win->menu->insert_image, !(features & PURPLE_CONNECTION_FLAG_NO_IMAGES));
5308 if (PURPLE_IS_IM_CONVERSATION(conv))
5310 gboolean can_send_file = FALSE;
5311 const gchar *name = purple_conversation_get_name(conv);
5313 if (PURPLE_IS_PROTOCOL_XFER(protocol) &&
5314 purple_protocol_xfer_can_receive(PURPLE_PROTOCOL_XFER(protocol), gc, name)
5316 can_send_file = TRUE;
5319 gtk_action_set_sensitive(win->menu->add, (PURPLE_PROTOCOL_IMPLEMENTS(protocol, SERVER, add_buddy)));
5320 gtk_action_set_sensitive(win->menu->remove, (PURPLE_PROTOCOL_IMPLEMENTS(protocol, SERVER, remove_buddy)));
5321 gtk_action_set_sensitive(win->menu->send_file, can_send_file);
5322 gtk_action_set_sensitive(win->menu->get_attention, (PURPLE_IS_PROTOCOL_ATTENTION(protocol)));
5323 gtk_action_set_sensitive(win->menu->alias,
5324 (account != NULL) &&
5325 (purple_blist_find_buddy(account, purple_conversation_get_name(conv)) != NULL));
5327 else if (PURPLE_IS_CHAT_CONVERSATION(conv))
5329 gtk_action_set_sensitive(win->menu->add, (PURPLE_PROTOCOL_IMPLEMENTS(protocol, CHAT, join)));
5330 gtk_action_set_sensitive(win->menu->remove, (PURPLE_PROTOCOL_IMPLEMENTS(protocol, CHAT, join)));
5331 gtk_action_set_sensitive(win->menu->alias,
5332 (account != NULL) &&
5333 (purple_blist_find_chat(account, purple_conversation_get_name(conv)) != NULL));
5336 } else {
5337 /* Account is offline */
5338 /* Or it's a chat that we've left. */
5340 /* Then deal with menu items */
5341 gtk_action_set_sensitive(win->menu->view_log, TRUE);
5342 gtk_action_set_sensitive(win->menu->send_file, FALSE);
5343 gtk_action_set_sensitive(win->menu->get_attention, FALSE);
5344 gtk_action_set_sensitive(win->menu->add_pounce, TRUE);
5345 gtk_action_set_sensitive(win->menu->get_info, FALSE);
5346 gtk_action_set_sensitive(win->menu->invite, FALSE);
5347 gtk_action_set_sensitive(win->menu->alias, FALSE);
5348 gtk_action_set_sensitive(win->menu->add, FALSE);
5349 gtk_action_set_sensitive(win->menu->remove, FALSE);
5350 gtk_action_set_sensitive(win->menu->insert_link, TRUE);
5351 gtk_action_set_sensitive(win->menu->insert_image, FALSE);
5355 * Update the window's icon
5357 if (pidgin_conv_window_is_active_conversation(conv))
5359 GList *l = NULL;
5360 if (PURPLE_IS_IM_CONVERSATION(conv) &&
5361 (gtkconv->u.im->anim))
5363 PurpleBuddy *buddy = purple_blist_find_buddy(purple_conversation_get_account(conv), purple_conversation_get_name(conv));
5364 window_icon =
5365 gdk_pixbuf_animation_get_static_image(gtkconv->u.im->anim);
5367 if (buddy && !PURPLE_BUDDY_IS_ONLINE(buddy))
5368 gdk_pixbuf_saturate_and_pixelate(window_icon, window_icon, 0.0, FALSE);
5370 g_object_ref(window_icon);
5371 l = g_list_append(l, window_icon);
5372 } else {
5373 l = pidgin_conv_get_tab_icons(conv);
5375 gtk_window_set_icon_list(GTK_WINDOW(win->window), l);
5376 if (window_icon != NULL) {
5377 g_object_unref(G_OBJECT(window_icon));
5378 g_list_free(l);
5383 static void
5384 pidgin_conv_update_fields(PurpleConversation *conv, PidginConvFields fields)
5386 PidginConversation *gtkconv;
5387 PidginConvWindow *win;
5389 gtkconv = PIDGIN_CONVERSATION(conv);
5390 if (!gtkconv)
5391 return;
5392 win = pidgin_conv_get_window(gtkconv);
5393 if (!win)
5394 return;
5396 if (fields & PIDGIN_CONV_SET_TITLE)
5398 purple_conversation_autoset_title(conv);
5401 if (fields & PIDGIN_CONV_BUDDY_ICON)
5403 if (PURPLE_IS_IM_CONVERSATION(conv))
5404 pidgin_conv_update_buddy_icon(PURPLE_IM_CONVERSATION(conv));
5407 if (fields & PIDGIN_CONV_MENU)
5409 gray_stuff_out(PIDGIN_CONVERSATION(conv));
5410 generate_send_to_items(win);
5411 regenerate_plugins_items(win);
5414 if (fields & PIDGIN_CONV_E2EE)
5415 generate_e2ee_controls(win);
5417 if (fields & PIDGIN_CONV_TAB_ICON)
5419 update_tab_icon(conv);
5420 generate_send_to_items(win); /* To update the icons in SendTo menu */
5423 if ((fields & PIDGIN_CONV_TOPIC) &&
5424 PURPLE_IS_CHAT_CONVERSATION(conv))
5426 const char *topic;
5427 PidginChatPane *gtkchat = gtkconv->u.chat;
5429 if (gtkchat->topic_text != NULL)
5431 topic = purple_chat_conversation_get_topic(PURPLE_CHAT_CONVERSATION(conv));
5433 gtk_entry_set_text(GTK_ENTRY(gtkchat->topic_text), topic ? topic : "");
5434 gtk_widget_set_tooltip_text(gtkchat->topic_text,
5435 topic ? topic : "");
5439 if ((fields & PIDGIN_CONV_COLORIZE_TITLE) ||
5440 (fields & PIDGIN_CONV_SET_TITLE) ||
5441 (fields & PIDGIN_CONV_TOPIC))
5443 char *title;
5444 PurpleIMConversation *im = NULL;
5445 PurpleAccount *account = purple_conversation_get_account(conv);
5446 PurpleBuddy *buddy = NULL;
5447 char *markup = NULL;
5448 AtkObject *accessibility_obj;
5449 /* I think this is a little longer than it needs to be but I'm lazy. */
5450 char *style;
5452 if (PURPLE_IS_IM_CONVERSATION(conv))
5453 im = PURPLE_IM_CONVERSATION(conv);
5455 if ((account == NULL) ||
5456 !purple_account_is_connected(account) ||
5457 (PURPLE_IS_CHAT_CONVERSATION(conv)
5458 && purple_chat_conversation_has_left(PURPLE_CHAT_CONVERSATION(conv))))
5459 title = g_strdup_printf("(%s)", purple_conversation_get_title(conv));
5460 else
5461 title = g_strdup(purple_conversation_get_title(conv));
5463 if (PURPLE_IS_IM_CONVERSATION(conv)) {
5464 buddy = purple_blist_find_buddy(account, purple_conversation_get_name(conv));
5465 if (buddy) {
5466 markup = pidgin_blist_get_name_markup(buddy, FALSE, FALSE);
5467 } else {
5468 markup = title;
5470 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
5471 const char *topic = gtkconv->u.chat->topic_text
5472 ? gtk_entry_get_text(GTK_ENTRY(gtkconv->u.chat->topic_text))
5473 : NULL;
5474 const char *title = purple_conversation_get_title(conv);
5475 const char *name = purple_conversation_get_name(conv);
5477 char *topic_esc, *unaliased, *unaliased_esc, *title_esc;
5479 topic_esc = topic ? g_markup_escape_text(topic, -1) : NULL;
5480 unaliased = g_utf8_collate(title, name) ? g_strdup_printf("(%s)", name) : NULL;
5481 unaliased_esc = unaliased ? g_markup_escape_text(unaliased, -1) : NULL;
5482 title_esc = g_markup_escape_text(title, -1);
5484 markup = g_strdup_printf("%s%s<span size='smaller'>%s</span>%s<span color='%s' size='smaller'>%s</span>",
5485 title_esc,
5486 unaliased_esc ? " " : "",
5487 unaliased_esc ? unaliased_esc : "",
5488 topic_esc && *topic_esc ? "\n" : "",
5489 pidgin_get_dim_grey_string(gtkconv->infopane),
5490 topic_esc ? topic_esc : "");
5492 g_free(title_esc);
5493 g_free(topic_esc);
5494 g_free(unaliased);
5495 g_free(unaliased_esc);
5497 gtk_list_store_set(gtkconv->infopane_model, &(gtkconv->infopane_iter),
5498 CONV_TEXT_COLUMN, markup, -1);
5499 /* XXX seanegan Why do I have to do this? */
5500 gtk_widget_queue_draw(gtkconv->infopane);
5502 if (title != markup)
5503 g_free(markup);
5505 if (!gtk_widget_get_realized(gtkconv->tab_label))
5506 gtk_widget_realize(gtkconv->tab_label);
5508 accessibility_obj = gtk_widget_get_accessible(gtkconv->tab_cont);
5509 if (im != NULL &&
5510 purple_im_conversation_get_typing_state(im) == PURPLE_IM_TYPING) {
5511 atk_object_set_description(accessibility_obj, _("Typing"));
5512 style = "tab-label-typing";
5513 } else if (im != NULL &&
5514 purple_im_conversation_get_typing_state(im) == PURPLE_IM_TYPED) {
5515 atk_object_set_description(accessibility_obj, _("Stopped Typing"));
5516 style = "tab-label-typed";
5517 } else if (gtkconv->unseen_state == PIDGIN_UNSEEN_NICK) {
5518 atk_object_set_description(accessibility_obj, _("Nick Said"));
5519 style = "tab-label-attention";
5520 } else if (gtkconv->unseen_state == PIDGIN_UNSEEN_TEXT) {
5521 atk_object_set_description(accessibility_obj, _("Unread Messages"));
5522 if (PURPLE_IS_CHAT_CONVERSATION(gtkconv->active_conv))
5523 style = "tab-label-unreadchat";
5524 else
5525 style = "tab-label-attention";
5526 } else if (gtkconv->unseen_state == PIDGIN_UNSEEN_EVENT) {
5527 atk_object_set_description(accessibility_obj, _("New Event"));
5528 style = "tab-label-event";
5529 } else {
5530 style = "tab-label";
5533 gtk_widget_set_name(gtkconv->tab_label, style);
5534 gtk_label_set_text(GTK_LABEL(gtkconv->tab_label), title);
5535 gtk_widget_set_state_flags(gtkconv->tab_label, GTK_STATE_FLAG_ACTIVE, TRUE);
5537 if (gtkconv->unseen_state == PIDGIN_UNSEEN_TEXT ||
5538 gtkconv->unseen_state == PIDGIN_UNSEEN_NICK ||
5539 gtkconv->unseen_state == PIDGIN_UNSEEN_EVENT) {
5540 PangoAttrList *list = pango_attr_list_new();
5541 PangoAttribute *attr = pango_attr_weight_new(PANGO_WEIGHT_BOLD);
5542 attr->start_index = 0;
5543 attr->end_index = -1;
5544 pango_attr_list_insert(list, attr);
5545 gtk_label_set_attributes(GTK_LABEL(gtkconv->tab_label), list);
5546 pango_attr_list_unref(list);
5547 } else
5548 gtk_label_set_attributes(GTK_LABEL(gtkconv->tab_label), NULL);
5550 if (pidgin_conv_window_is_active_conversation(conv))
5551 update_typing_icon(gtkconv);
5553 gtk_label_set_text(GTK_LABEL(gtkconv->menu_label), title);
5554 if (pidgin_conv_window_is_active_conversation(conv)) {
5555 const char* current_title = gtk_window_get_title(GTK_WINDOW(win->window));
5556 if (current_title == NULL || !purple_strequal(current_title, title))
5557 gtk_window_set_title(GTK_WINDOW(win->window), title);
5560 g_free(title);
5564 static void
5565 pidgin_conv_updated(PurpleConversation *conv, PurpleConversationUpdateType type)
5567 PidginConvFields flags = 0;
5569 g_return_if_fail(conv != NULL);
5571 if (type == PURPLE_CONVERSATION_UPDATE_ACCOUNT)
5573 flags = PIDGIN_CONV_ALL;
5575 else if (type == PURPLE_CONVERSATION_UPDATE_TYPING ||
5576 type == PURPLE_CONVERSATION_UPDATE_UNSEEN ||
5577 type == PURPLE_CONVERSATION_UPDATE_TITLE)
5579 flags = PIDGIN_CONV_COLORIZE_TITLE;
5581 else if (type == PURPLE_CONVERSATION_UPDATE_TOPIC)
5583 flags = PIDGIN_CONV_TOPIC;
5585 else if (type == PURPLE_CONVERSATION_ACCOUNT_ONLINE ||
5586 type == PURPLE_CONVERSATION_ACCOUNT_OFFLINE)
5588 flags = PIDGIN_CONV_MENU | PIDGIN_CONV_TAB_ICON | PIDGIN_CONV_SET_TITLE;
5590 else if (type == PURPLE_CONVERSATION_UPDATE_AWAY)
5592 flags = PIDGIN_CONV_TAB_ICON;
5594 else if (type == PURPLE_CONVERSATION_UPDATE_ADD ||
5595 type == PURPLE_CONVERSATION_UPDATE_REMOVE ||
5596 type == PURPLE_CONVERSATION_UPDATE_CHATLEFT)
5598 flags = PIDGIN_CONV_SET_TITLE | PIDGIN_CONV_MENU;
5600 else if (type == PURPLE_CONVERSATION_UPDATE_ICON)
5602 flags = PIDGIN_CONV_BUDDY_ICON;
5604 else if (type == PURPLE_CONVERSATION_UPDATE_FEATURES)
5606 flags = PIDGIN_CONV_MENU;
5608 else if (type == PURPLE_CONVERSATION_UPDATE_E2EE)
5610 flags = PIDGIN_CONV_E2EE | PIDGIN_CONV_MENU;
5613 pidgin_conv_update_fields(conv, flags);
5616 static void
5617 wrote_msg_update_unseen_cb(PurpleConversation *conv, PurpleMessage *msg,
5618 gpointer _unused)
5620 PidginConversation *gtkconv = conv ? PIDGIN_CONVERSATION(conv) : NULL;
5621 PurpleMessageFlags flags;
5622 if (conv == NULL || (gtkconv && gtkconv->win != hidden_convwin))
5623 return;
5624 flags = purple_message_get_flags(msg);
5625 if (flags & (PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_RECV)) {
5626 PidginUnseenState unseen = PIDGIN_UNSEEN_NONE;
5628 if ((flags & PURPLE_MESSAGE_NICK) == PURPLE_MESSAGE_NICK)
5629 unseen = PIDGIN_UNSEEN_NICK;
5630 else if (((flags & PURPLE_MESSAGE_SYSTEM) == PURPLE_MESSAGE_SYSTEM) ||
5631 ((flags & PURPLE_MESSAGE_ERROR) == PURPLE_MESSAGE_ERROR))
5632 unseen = PIDGIN_UNSEEN_EVENT;
5633 else if ((flags & PURPLE_MESSAGE_NO_LOG) == PURPLE_MESSAGE_NO_LOG)
5634 unseen = PIDGIN_UNSEEN_NO_LOG;
5635 else
5636 unseen = PIDGIN_UNSEEN_TEXT;
5638 conv_set_unseen(conv, unseen);
5642 static PurpleConversationUiOps conversation_ui_ops =
5644 pidgin_conv_new,
5645 pidgin_conv_destroy, /* destroy_conversation */
5646 NULL, /* write_chat */
5647 NULL, /* write_im */
5648 pidgin_conv_write_conv, /* write_conv */
5649 pidgin_conv_chat_add_users, /* chat_add_users */
5650 pidgin_conv_chat_rename_user, /* chat_rename_user */
5651 pidgin_conv_chat_remove_users, /* chat_remove_users */
5652 pidgin_conv_chat_update_user, /* chat_update_user */
5653 pidgin_conv_present_conversation, /* present */
5654 pidgin_conv_has_focus, /* has_focus */
5655 NULL, /* send_confirm */
5656 NULL,
5657 NULL,
5658 NULL,
5659 NULL
5662 PurpleConversationUiOps *
5663 pidgin_conversations_get_conv_ui_ops(void)
5665 return &conversation_ui_ops;
5668 /**************************************************************************
5669 * Public conversation utility functions
5670 **************************************************************************/
5671 void
5672 pidgin_conv_update_buddy_icon(PurpleIMConversation *im)
5674 PidginConversation *gtkconv;
5675 PurpleConversation *conv;
5676 PidginConvWindow *win;
5678 PurpleBuddy *buddy;
5680 PurpleImage *custom_img = NULL;
5681 gconstpointer data = NULL;
5682 size_t len;
5684 GdkPixbuf *buf;
5686 GList *children;
5687 GtkWidget *event;
5688 GdkPixbuf *scale;
5689 int scale_width, scale_height;
5690 int size = 0;
5692 PurpleAccount *account;
5694 PurpleBuddyIcon *icon;
5696 conv = PURPLE_CONVERSATION(im);
5698 g_return_if_fail(conv != NULL);
5699 g_return_if_fail(PIDGIN_IS_PIDGIN_CONVERSATION(conv));
5701 gtkconv = PIDGIN_CONVERSATION(conv);
5702 win = gtkconv->win;
5703 if (conv != gtkconv->active_conv)
5704 return;
5706 if (!gtkconv->u.im->show_icon)
5707 return;
5709 account = purple_conversation_get_account(conv);
5711 /* Remove the current icon stuff */
5712 children = gtk_container_get_children(GTK_CONTAINER(gtkconv->u.im->icon_container));
5713 if (children) {
5714 /* We know there's only one child here. It'd be nice to shortcut to the
5715 event box, but we can't change the PidginConversation until 3.0 */
5716 event = (GtkWidget *)children->data;
5717 gtk_container_remove(GTK_CONTAINER(gtkconv->u.im->icon_container), event);
5718 g_list_free(children);
5721 if (gtkconv->u.im->anim != NULL)
5722 g_object_unref(G_OBJECT(gtkconv->u.im->anim));
5724 gtkconv->u.im->anim = NULL;
5726 if (gtkconv->u.im->icon_timer != 0)
5727 g_source_remove(gtkconv->u.im->icon_timer);
5729 gtkconv->u.im->icon_timer = 0;
5731 if (gtkconv->u.im->iter != NULL)
5732 g_object_unref(G_OBJECT(gtkconv->u.im->iter));
5734 gtkconv->u.im->iter = NULL;
5736 if (!purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons"))
5737 return;
5739 if (purple_conversation_get_connection(conv) == NULL)
5740 return;
5742 buddy = purple_blist_find_buddy(account, purple_conversation_get_name(conv));
5743 if (buddy)
5745 PurpleContact *contact = purple_buddy_get_contact(buddy);
5746 if (contact) {
5747 custom_img = purple_buddy_icons_node_find_custom_icon((PurpleBlistNode*)contact);
5748 if (custom_img) {
5749 /* There is a custom icon for this user */
5750 data = purple_image_get_data(custom_img);
5751 len = purple_image_get_data_size(custom_img);
5756 if (data == NULL) {
5757 icon = purple_im_conversation_get_icon(im);
5758 if (icon == NULL)
5760 gtk_widget_set_size_request(gtkconv->u.im->icon_container,
5761 -1, BUDDYICON_SIZE_MIN);
5762 return;
5765 data = purple_buddy_icon_get_data(icon, &len);
5766 if (data == NULL)
5768 gtk_widget_set_size_request(gtkconv->u.im->icon_container,
5769 -1, BUDDYICON_SIZE_MIN);
5770 return;
5774 gtkconv->u.im->anim = pidgin_pixbuf_anim_from_data(data, len);
5775 if (custom_img)
5776 g_object_unref(custom_img);
5778 if (!gtkconv->u.im->anim) {
5779 purple_debug_error("gtkconv", "Couldn't load icon for conv %s\n",
5780 purple_conversation_get_name(conv));
5781 return;
5784 if (gdk_pixbuf_animation_is_static_image(gtkconv->u.im->anim)) {
5785 GdkPixbuf *stat;
5786 gtkconv->u.im->iter = NULL;
5787 stat = gdk_pixbuf_animation_get_static_image(gtkconv->u.im->anim);
5788 buf = gdk_pixbuf_add_alpha(stat, FALSE, 0, 0, 0);
5789 } else {
5790 GdkPixbuf *stat;
5791 gtkconv->u.im->iter =
5792 gdk_pixbuf_animation_get_iter(gtkconv->u.im->anim, NULL); /* LEAK */
5793 stat = gdk_pixbuf_animation_iter_get_pixbuf(gtkconv->u.im->iter);
5794 buf = gdk_pixbuf_add_alpha(stat, FALSE, 0, 0, 0);
5795 if (gtkconv->u.im->animate)
5796 start_anim(NULL, gtkconv);
5799 scale_width = gdk_pixbuf_get_width(buf);
5800 scale_height = gdk_pixbuf_get_height(buf);
5802 gtk_widget_get_size_request(gtkconv->u.im->icon_container, NULL, &size);
5803 size = MIN(size, MIN(scale_width, scale_height));
5805 /* Some sanity checks */
5806 size = CLAMP(size, BUDDYICON_SIZE_MIN, BUDDYICON_SIZE_MAX);
5807 if (scale_width == scale_height) {
5808 scale_width = scale_height = size;
5809 } else if (scale_height > scale_width) {
5810 scale_width = size * scale_width / scale_height;
5811 scale_height = size;
5812 } else {
5813 scale_height = size * scale_height / scale_width;
5814 scale_width = size;
5816 scale = gdk_pixbuf_scale_simple(buf, scale_width, scale_height,
5817 GDK_INTERP_BILINEAR);
5818 g_object_unref(buf);
5819 if (pidgin_gdk_pixbuf_is_opaque(scale))
5820 pidgin_gdk_pixbuf_make_round(scale);
5822 event = gtk_event_box_new();
5823 gtk_container_add(GTK_CONTAINER(gtkconv->u.im->icon_container), event);
5824 gtk_event_box_set_visible_window(GTK_EVENT_BOX(event), FALSE);
5825 gtk_widget_add_events(event,
5826 GDK_POINTER_MOTION_MASK | GDK_LEAVE_NOTIFY_MASK);
5827 g_signal_connect(G_OBJECT(event), "button-press-event",
5828 G_CALLBACK(icon_menu), gtkconv);
5830 pidgin_tooltip_setup_for_widget(event, gtkconv, pidgin_conv_create_tooltip, NULL);
5831 gtk_widget_show(event);
5833 gtkconv->u.im->icon = gtk_image_new_from_pixbuf(scale);
5834 gtk_container_add(GTK_CONTAINER(event), gtkconv->u.im->icon);
5835 gtk_widget_show(gtkconv->u.im->icon);
5837 g_object_unref(G_OBJECT(scale));
5839 /* The buddy icon code needs badly to be fixed. */
5840 if(pidgin_conv_window_is_active_conversation(conv))
5842 buf = gdk_pixbuf_animation_get_static_image(gtkconv->u.im->anim);
5843 if (buddy && !PURPLE_BUDDY_IS_ONLINE(buddy))
5844 gdk_pixbuf_saturate_and_pixelate(buf, buf, 0.0, FALSE);
5845 gtk_window_set_icon(GTK_WINDOW(win->window), buf);
5849 void
5850 pidgin_conv_update_buttons_by_protocol(PurpleConversation *conv)
5852 PidginConvWindow *win;
5854 if (!PIDGIN_IS_PIDGIN_CONVERSATION(conv))
5855 return;
5857 win = PIDGIN_CONVERSATION(conv)->win;
5859 if (win != NULL && pidgin_conv_window_is_active_conversation(conv))
5860 gray_stuff_out(PIDGIN_CONVERSATION(conv));
5863 static gboolean
5864 pidgin_conv_xy_to_right_infopane(PidginConvWindow *win, int x, int y)
5866 gint pane_x, pane_y, x_rel;
5867 PidginConversation *gtkconv;
5868 GtkAllocation allocation;
5870 gdk_window_get_origin(gtk_widget_get_window(win->notebook),
5871 &pane_x, &pane_y);
5872 x_rel = x - pane_x;
5873 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
5874 gtk_widget_get_allocation(gtkconv->infopane, &allocation);
5875 return (x_rel > allocation.x + allocation.width / 2);
5879 pidgin_conv_get_tab_at_xy(PidginConvWindow *win, int x, int y, gboolean *to_right)
5881 gint nb_x, nb_y, x_rel, y_rel;
5882 GtkNotebook *notebook;
5883 GtkWidget *page, *tab;
5884 gint i, page_num = -1;
5885 gint count;
5886 gboolean horiz;
5888 if (to_right)
5889 *to_right = FALSE;
5891 notebook = GTK_NOTEBOOK(win->notebook);
5893 gdk_window_get_origin(gtk_widget_get_window(win->notebook), &nb_x, &nb_y);
5894 x_rel = x - nb_x;
5895 y_rel = y - nb_y;
5897 horiz = (gtk_notebook_get_tab_pos(notebook) == GTK_POS_TOP ||
5898 gtk_notebook_get_tab_pos(notebook) == GTK_POS_BOTTOM);
5900 count = gtk_notebook_get_n_pages(GTK_NOTEBOOK(notebook));
5902 for (i = 0; i < count; i++) {
5903 GtkAllocation allocation;
5905 page = gtk_notebook_get_nth_page(GTK_NOTEBOOK(notebook), i);
5906 tab = gtk_notebook_get_tab_label(GTK_NOTEBOOK(notebook), page);
5907 gtk_widget_get_allocation(tab, &allocation);
5909 /* Make sure the tab is not hidden beyond an arrow */
5910 if (!gtk_widget_is_drawable(tab) && gtk_notebook_get_show_tabs(notebook))
5911 continue;
5913 if (horiz) {
5914 if (x_rel >= allocation.x - PIDGIN_HIG_BOX_SPACE &&
5915 x_rel <= allocation.x + allocation.width + PIDGIN_HIG_BOX_SPACE) {
5916 page_num = i;
5918 if (to_right && x_rel >= allocation.x + allocation.width/2)
5919 *to_right = TRUE;
5921 break;
5923 } else {
5924 if (y_rel >= allocation.y - PIDGIN_HIG_BOX_SPACE &&
5925 y_rel <= allocation.y + allocation.height + PIDGIN_HIG_BOX_SPACE) {
5926 page_num = i;
5928 if (to_right && y_rel >= allocation.y + allocation.height/2)
5929 *to_right = TRUE;
5931 break;
5936 if (page_num == -1) {
5937 /* Add after the last tab */
5938 page_num = count - 1;
5941 return page_num;
5944 static void
5945 close_on_tabs_pref_cb(const char *name, PurplePrefType type,
5946 gconstpointer value, gpointer data)
5948 GList *l;
5949 PurpleConversation *conv;
5950 PidginConversation *gtkconv;
5952 for (l = purple_conversations_get_all(); l != NULL; l = l->next) {
5953 conv = (PurpleConversation *)l->data;
5955 if (!PIDGIN_IS_PIDGIN_CONVERSATION(conv))
5956 continue;
5958 gtkconv = PIDGIN_CONVERSATION(conv);
5960 if (value)
5961 gtk_widget_show(gtkconv->close);
5962 else
5963 gtk_widget_hide(gtkconv->close);
5967 static void
5968 spellcheck_pref_cb(const char *name, PurplePrefType type,
5969 gconstpointer value, gpointer data)
5971 GList *cl;
5972 PurpleConversation *conv;
5973 PidginConversation *gtkconv;
5975 for (cl = purple_conversations_get_all(); cl != NULL; cl = cl->next) {
5977 conv = (PurpleConversation *)cl->data;
5979 if (!PIDGIN_IS_PIDGIN_CONVERSATION(conv))
5980 continue;
5982 gtkconv = PIDGIN_CONVERSATION(conv);
5984 # warning toggle spell checking when talkatu #60 is done.
5988 static void
5989 tab_side_pref_cb(const char *name, PurplePrefType type,
5990 gconstpointer value, gpointer data)
5992 GList *gtkwins, *gtkconvs;
5993 GtkPositionType pos;
5994 PidginConvWindow *gtkwin;
5996 pos = GPOINTER_TO_INT(value);
5998 for (gtkwins = pidgin_conv_windows_get_list(); gtkwins != NULL; gtkwins = gtkwins->next) {
5999 gtkwin = gtkwins->data;
6000 gtk_notebook_set_tab_pos(GTK_NOTEBOOK(gtkwin->notebook), pos&~8);
6001 for (gtkconvs = gtkwin->gtkconvs; gtkconvs != NULL; gtkconvs = gtkconvs->next) {
6002 pidgin_conv_tab_pack(gtkwin, gtkconvs->data);
6007 static void
6008 show_formatting_toolbar_pref_cb(const char *name, PurplePrefType type,
6009 gconstpointer value, gpointer data)
6011 GList *l;
6012 PurpleConversation *conv;
6013 PidginConversation *gtkconv;
6014 PidginConvWindow *win;
6015 gboolean visible = (gboolean)GPOINTER_TO_INT(value);
6017 for (l = purple_conversations_get_all(); l != NULL; l = l->next)
6019 conv = (PurpleConversation *)l->data;
6021 if (!PIDGIN_IS_PIDGIN_CONVERSATION(conv))
6022 continue;
6024 gtkconv = PIDGIN_CONVERSATION(conv);
6025 win = gtkconv->win;
6027 gtk_toggle_action_set_active(
6028 GTK_TOGGLE_ACTION(win->menu->show_formatting_toolbar),
6029 visible
6032 talkatu_editor_set_toolbar_visible(TALKATU_EDITOR(gtkconv->editor), visible);
6036 static void
6037 animate_buddy_icons_pref_cb(const char *name, PurplePrefType type,
6038 gconstpointer value, gpointer data)
6040 GList *l;
6041 PurpleConversation *conv;
6042 PidginConversation *gtkconv;
6043 PidginConvWindow *win;
6045 if (!purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons"))
6046 return;
6048 /* Set the "animate" flag for each icon based on the new preference */
6049 for (l = purple_conversations_get_ims(); l != NULL; l = l->next) {
6050 conv = (PurpleConversation *)l->data;
6051 gtkconv = PIDGIN_CONVERSATION(conv);
6052 if (gtkconv)
6053 gtkconv->u.im->animate = GPOINTER_TO_INT(value);
6056 /* Now either stop or start animation for the active conversation in each window */
6057 for (l = pidgin_conv_windows_get_list(); l != NULL; l = l->next) {
6058 win = l->data;
6059 conv = pidgin_conv_window_get_active_conversation(win);
6060 pidgin_conv_update_buddy_icon(PURPLE_IM_CONVERSATION(conv));
6064 static void
6065 show_buddy_icons_pref_cb(const char *name, PurplePrefType type,
6066 gconstpointer value, gpointer data)
6068 GList *l;
6070 for (l = purple_conversations_get_all(); l != NULL; l = l->next) {
6071 PurpleConversation *conv = l->data;
6072 if (!PIDGIN_CONVERSATION(conv))
6073 continue;
6074 if (GPOINTER_TO_INT(value))
6075 gtk_widget_show(PIDGIN_CONVERSATION(conv)->infopane_hbox);
6076 else
6077 gtk_widget_hide(PIDGIN_CONVERSATION(conv)->infopane_hbox);
6079 if (PURPLE_IS_IM_CONVERSATION(conv)) {
6080 pidgin_conv_update_buddy_icon(PURPLE_IM_CONVERSATION(conv));
6084 /* Make the tabs show/hide correctly */
6085 for (l = pidgin_conv_windows_get_list(); l != NULL; l = l->next) {
6086 PidginConvWindow *win = l->data;
6087 if (pidgin_conv_window_get_gtkconv_count(win) == 1)
6088 gtk_notebook_set_show_tabs(GTK_NOTEBOOK(win->notebook),
6089 GPOINTER_TO_INT(value) == 0);
6093 static void
6094 show_protocol_icons_pref_cb(const char *name, PurplePrefType type,
6095 gconstpointer value, gpointer data)
6097 GList *l;
6098 for (l = purple_conversations_get_all(); l != NULL; l = l->next) {
6099 PurpleConversation *conv = l->data;
6100 if (PIDGIN_CONVERSATION(conv))
6101 update_tab_icon(conv);
6105 static void
6106 conv_placement_usetabs_cb(const char *name, PurplePrefType type,
6107 gconstpointer value, gpointer data)
6109 purple_prefs_trigger_callback(PIDGIN_PREFS_ROOT "/conversations/placement");
6112 static void
6113 account_status_changed_cb(PurpleAccount *account, PurpleStatus *oldstatus,
6114 PurpleStatus *newstatus)
6116 GList *l;
6117 PurpleConversation *conv = NULL;
6118 PidginConversation *gtkconv;
6120 if(!purple_strequal(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new"), "away"))
6121 return;
6123 if(purple_status_is_available(oldstatus) || !purple_status_is_available(newstatus))
6124 return;
6126 for (l = hidden_convwin->gtkconvs; l; ) {
6127 gtkconv = l->data;
6128 l = l->next;
6130 conv = gtkconv->active_conv;
6131 if (PURPLE_IS_CHAT_CONVERSATION(conv) ||
6132 account != purple_conversation_get_account(conv))
6133 continue;
6135 pidgin_conv_attach_to_conversation(conv);
6137 /* TODO: do we need to do anything for any other conversations that are in the same gtkconv here?
6138 * I'm a little concerned that not doing so will cause the "pending" indicator in the gtkblist not to be cleared. -DAA*/
6139 purple_conversation_update(conv, PURPLE_CONVERSATION_UPDATE_UNSEEN);
6143 static void
6144 hide_new_pref_cb(const char *name, PurplePrefType type,
6145 gconstpointer value, gpointer data)
6147 GList *l;
6148 PurpleConversation *conv = NULL;
6149 PidginConversation *gtkconv;
6150 gboolean when_away = FALSE;
6152 if(!hidden_convwin)
6153 return;
6155 if(purple_strequal(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new"), "always"))
6156 return;
6158 if(purple_strequal(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new"), "away"))
6159 when_away = TRUE;
6161 for (l = hidden_convwin->gtkconvs; l; )
6163 gtkconv = l->data;
6164 l = l->next;
6166 conv = gtkconv->active_conv;
6168 if (PURPLE_IS_CHAT_CONVERSATION(conv) ||
6169 gtkconv->unseen_count == 0 ||
6170 (when_away && !purple_status_is_available(
6171 purple_account_get_active_status(
6172 purple_conversation_get_account(conv)))))
6173 continue;
6175 pidgin_conv_attach_to_conversation(conv);
6180 static void
6181 conv_placement_pref_cb(const char *name, PurplePrefType type,
6182 gconstpointer value, gpointer data)
6184 PidginConvPlacementFunc func;
6186 if (!purple_strequal(name, PIDGIN_PREFS_ROOT "/conversations/placement"))
6187 return;
6189 func = pidgin_conv_placement_get_fnc(value);
6191 if (func == NULL)
6192 return;
6194 pidgin_conv_placement_set_current_func(func);
6197 static PidginConversation *
6198 get_gtkconv_with_contact(PurpleContact *contact)
6200 PurpleBlistNode *node;
6202 node = ((PurpleBlistNode*)contact)->child;
6204 for (; node; node = node->next)
6206 PurpleBuddy *buddy = (PurpleBuddy*)node;
6207 PurpleIMConversation *im;
6208 im = purple_conversations_find_im_with_account(purple_buddy_get_name(buddy), purple_buddy_get_account(buddy));
6209 if (im)
6210 return PIDGIN_CONVERSATION(PURPLE_CONVERSATION(im));
6212 return NULL;
6215 static void
6216 account_signed_off_cb(PurpleConnection *gc, gpointer event)
6218 GList *iter;
6220 for (iter = purple_conversations_get_all(); iter; iter = iter->next)
6222 PurpleConversation *conv = iter->data;
6224 /* This seems fine in theory, but we also need to cover the
6225 * case of this account matching one of the other buddies in
6226 * one of the contacts containing the buddy corresponding to
6227 * a conversation. It's easier to just update them all. */
6228 /* if (purple_conversation_get_account(conv) == account) */
6229 pidgin_conv_update_fields(conv, PIDGIN_CONV_TAB_ICON |
6230 PIDGIN_CONV_MENU | PIDGIN_CONV_COLORIZE_TITLE);
6232 if (PURPLE_CONNECTION_IS_CONNECTED(gc) &&
6233 PURPLE_IS_CHAT_CONVERSATION(conv) &&
6234 purple_conversation_get_account(conv) == purple_connection_get_account(gc) &&
6235 g_object_get_data(G_OBJECT(conv), "want-to-rejoin")) {
6236 GHashTable *comps = NULL;
6237 PurpleChat *chat = purple_blist_find_chat(purple_conversation_get_account(conv), purple_conversation_get_name(conv));
6238 if (chat == NULL) {
6239 PurpleProtocol *protocol = purple_connection_get_protocol(gc);
6240 comps = purple_protocol_chat_iface_info_defaults(protocol, gc, purple_conversation_get_name(conv));
6241 } else {
6242 comps = purple_chat_get_components(chat);
6244 purple_serv_join_chat(gc, comps);
6245 if (chat == NULL && comps != NULL)
6246 g_hash_table_destroy(comps);
6251 static void
6252 account_signing_off(PurpleConnection *gc)
6254 GList *list = purple_conversations_get_chats();
6255 PurpleAccount *account = purple_connection_get_account(gc);
6257 /* We are about to sign off. See which chats we are currently in, and mark
6258 * them for rejoin on reconnect. */
6259 while (list) {
6260 PurpleConversation *conv = list->data;
6261 if (!purple_chat_conversation_has_left(PURPLE_CHAT_CONVERSATION(conv)) &&
6262 purple_conversation_get_account(conv) == account) {
6263 g_object_set_data(G_OBJECT(conv), "want-to-rejoin", GINT_TO_POINTER(TRUE));
6264 purple_conversation_write_system_message(conv,
6265 _("The account has disconnected and you are no "
6266 "longer in this chat. You will automatically "
6267 "rejoin the chat when the account reconnects."),
6268 PURPLE_MESSAGE_NO_LOG);
6270 list = list->next;
6274 static void
6275 update_buddy_status_changed(PurpleBuddy *buddy, PurpleStatus *old, PurpleStatus *newstatus)
6277 PidginConversation *gtkconv;
6278 PurpleConversation *conv;
6280 gtkconv = get_gtkconv_with_contact(purple_buddy_get_contact(buddy));
6281 if (gtkconv)
6283 conv = gtkconv->active_conv;
6284 pidgin_conv_update_fields(conv, PIDGIN_CONV_TAB_ICON
6285 | PIDGIN_CONV_COLORIZE_TITLE
6286 | PIDGIN_CONV_BUDDY_ICON);
6287 if ((purple_status_is_online(old) ^ purple_status_is_online(newstatus)) != 0)
6288 pidgin_conv_update_fields(conv, PIDGIN_CONV_MENU);
6292 static void
6293 update_buddy_privacy_changed(PurpleBuddy *buddy)
6295 PidginConversation *gtkconv;
6296 PurpleConversation *conv;
6298 gtkconv = get_gtkconv_with_contact(purple_buddy_get_contact(buddy));
6299 if (gtkconv) {
6300 conv = gtkconv->active_conv;
6301 pidgin_conv_update_fields(conv, PIDGIN_CONV_TAB_ICON | PIDGIN_CONV_MENU);
6305 static void
6306 update_buddy_idle_changed(PurpleBuddy *buddy, gboolean old, gboolean newidle)
6308 PurpleIMConversation *im;
6310 im = purple_conversations_find_im_with_account(purple_buddy_get_name(buddy), purple_buddy_get_account(buddy));
6311 if (im)
6312 pidgin_conv_update_fields(PURPLE_CONVERSATION(im), PIDGIN_CONV_TAB_ICON);
6315 static void
6316 update_buddy_icon(PurpleBuddy *buddy)
6318 PurpleIMConversation *im;
6320 im = purple_conversations_find_im_with_account(purple_buddy_get_name(buddy), purple_buddy_get_account(buddy));
6321 if (im)
6322 pidgin_conv_update_fields(PURPLE_CONVERSATION(im), PIDGIN_CONV_BUDDY_ICON);
6325 static void
6326 update_buddy_sign(PurpleBuddy *buddy, const char *which)
6328 PurplePresence *presence;
6329 PurpleStatus *on, *off;
6331 presence = purple_buddy_get_presence(buddy);
6332 if (!presence)
6333 return;
6334 off = purple_presence_get_status(presence, "offline");
6335 on = purple_presence_get_status(presence, "available");
6337 if (*(which+1) == 'f')
6338 update_buddy_status_changed(buddy, on, off);
6339 else
6340 update_buddy_status_changed(buddy, off, on);
6343 static void
6344 update_conversation_switched(PurpleConversation *conv)
6346 pidgin_conv_update_fields(conv, PIDGIN_CONV_TAB_ICON |
6347 PIDGIN_CONV_SET_TITLE | PIDGIN_CONV_MENU |
6348 PIDGIN_CONV_BUDDY_ICON | PIDGIN_CONV_E2EE );
6351 static void
6352 update_buddy_typing(PurpleAccount *account, const char *who)
6354 PurpleConversation *conv;
6355 PidginConversation *gtkconv;
6357 conv = PURPLE_CONVERSATION(purple_conversations_find_im_with_account(who, account));
6358 if (!conv)
6359 return;
6361 gtkconv = PIDGIN_CONVERSATION(conv);
6362 if (gtkconv && gtkconv->active_conv == conv)
6363 pidgin_conv_update_fields(conv, PIDGIN_CONV_COLORIZE_TITLE);
6366 static void
6367 update_chat(PurpleChatConversation *chat)
6369 pidgin_conv_update_fields(PURPLE_CONVERSATION(chat), PIDGIN_CONV_TOPIC |
6370 PIDGIN_CONV_MENU | PIDGIN_CONV_SET_TITLE);
6373 static void
6374 update_chat_topic(PurpleChatConversation *chat, const char *old, const char *new)
6376 pidgin_conv_update_fields(PURPLE_CONVERSATION(chat), PIDGIN_CONV_TOPIC);
6379 /* Message history stuff */
6381 /* Compare two PurpleMessages, according to time in ascending order. */
6382 static int
6383 message_compare(PurpleMessage *m1, PurpleMessage *m2)
6385 guint64 t1 = purple_message_get_time(m1), t2 = purple_message_get_time(m2);
6386 return (t1 > t2) - (t1 < t2);
6389 /* Adds some message history to the gtkconv. This happens in a idle-callback. */
6390 static gboolean
6391 add_message_history_to_gtkconv(gpointer data)
6393 PidginConversation *gtkconv = data;
6394 int count = 0;
6395 int timer = gtkconv->attach_timer;
6396 time_t when = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(gtkconv->editor), "attach-start-time"));
6397 gboolean im = (PURPLE_IS_IM_CONVERSATION(gtkconv->active_conv));
6399 gtkconv->attach_timer = 0;
6400 while (gtkconv->attach_current && count < ADD_MESSAGE_HISTORY_AT_ONCE) {
6401 PurpleMessage *msg = gtkconv->attach_current->data;
6402 if (!im && when && (guint64)when < purple_message_get_time(msg)) {
6403 g_object_set_data(G_OBJECT(gtkconv->editor), "attach-start-time", NULL);
6405 /* XXX: should it be gtkconv->active_conv? */
6406 pidgin_conv_write_conv(gtkconv->active_conv, msg);
6407 if (im) {
6408 gtkconv->attach_current = g_list_delete_link(gtkconv->attach_current, gtkconv->attach_current);
6409 } else {
6410 gtkconv->attach_current = gtkconv->attach_current->prev;
6412 count++;
6414 gtkconv->attach_timer = timer;
6415 if (gtkconv->attach_current)
6416 return TRUE;
6418 g_source_remove(gtkconv->attach_timer);
6419 gtkconv->attach_timer = 0;
6420 if (im) {
6421 /* Print any message that was sent while the old history was being added back. */
6422 GList *msgs = NULL;
6423 GList *iter = gtkconv->convs;
6424 for (; iter; iter = iter->next) {
6425 PurpleConversation *conv = iter->data;
6426 GList *history = purple_conversation_get_message_history(conv);
6427 for (; history; history = history->next) {
6428 PurpleMessage *msg = history->data;
6429 if (purple_message_get_time(msg) > (guint64)when)
6430 msgs = g_list_prepend(msgs, msg);
6433 msgs = g_list_sort(msgs, (GCompareFunc)message_compare);
6434 for (; msgs; msgs = g_list_delete_link(msgs, msgs)) {
6435 PurpleMessage *msg = msgs->data;
6436 /* XXX: see above - should it be active_conv? */
6437 pidgin_conv_write_conv(gtkconv->active_conv, msg);
6439 g_object_set_data(G_OBJECT(gtkconv->editor), "attach-start-time", NULL);
6442 g_object_set_data(G_OBJECT(gtkconv->editor), "attach-start-time", NULL);
6443 purple_signal_emit(pidgin_conversations_get_handle(),
6444 "conversation-displayed", gtkconv);
6445 return FALSE;
6448 static void
6449 pidgin_conv_attach(PurpleConversation *conv)
6451 int timer;
6452 g_object_set_data(G_OBJECT(conv), "unseen-count", NULL);
6453 g_object_set_data(G_OBJECT(conv), "unseen-state", NULL);
6454 purple_conversation_set_ui_ops(conv, pidgin_conversations_get_conv_ui_ops());
6455 if (!PIDGIN_CONVERSATION(conv))
6456 private_gtkconv_new(conv, FALSE);
6457 timer = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(conv), "close-timer"));
6458 if (timer) {
6459 g_source_remove(timer);
6460 g_object_set_data(G_OBJECT(conv), "close-timer", NULL);
6464 gboolean pidgin_conv_attach_to_conversation(PurpleConversation *conv)
6466 GList *list;
6467 PidginConversation *gtkconv;
6469 if (PIDGIN_IS_PIDGIN_CONVERSATION(conv)) {
6470 /* This is pretty much always the case now. */
6471 gtkconv = PIDGIN_CONVERSATION(conv);
6472 if (gtkconv->win != hidden_convwin)
6473 return FALSE;
6474 pidgin_conv_window_remove_gtkconv(hidden_convwin, gtkconv);
6475 pidgin_conv_placement_place(gtkconv);
6476 purple_signal_emit(pidgin_conversations_get_handle(),
6477 "conversation-displayed", gtkconv);
6478 list = gtkconv->convs;
6479 while (list) {
6480 pidgin_conv_attach(list->data);
6481 list = list->next;
6483 return TRUE;
6486 pidgin_conv_attach(conv);
6487 gtkconv = PIDGIN_CONVERSATION(conv);
6489 list = purple_conversation_get_message_history(conv);
6490 if (list) {
6491 if (PURPLE_IS_IM_CONVERSATION(conv)) {
6492 GList *convs;
6493 list = g_list_copy(list);
6494 for (convs = purple_conversations_get_ims(); convs; convs = convs->next)
6495 if (convs->data != conv &&
6496 pidgin_conv_find_gtkconv(convs->data) == gtkconv) {
6497 pidgin_conv_attach(convs->data);
6498 list = g_list_concat(list, g_list_copy(purple_conversation_get_message_history(convs->data)));
6500 list = g_list_sort(list, (GCompareFunc)message_compare);
6501 gtkconv->attach_current = list;
6502 list = g_list_last(list);
6503 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
6504 gtkconv->attach_current = g_list_last(list);
6507 g_object_set_data(G_OBJECT(gtkconv->editor), "attach-start-time",
6508 GINT_TO_POINTER(purple_message_get_time(list->data)));
6509 gtkconv->attach_timer = g_idle_add(add_message_history_to_gtkconv, gtkconv);
6510 } else {
6511 purple_signal_emit(pidgin_conversations_get_handle(),
6512 "conversation-displayed", gtkconv);
6515 if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
6516 GList *users;
6517 PurpleChatConversation *chat = PURPLE_CHAT_CONVERSATION(conv);
6518 pidgin_conv_update_fields(conv, PIDGIN_CONV_TOPIC);
6519 users = purple_chat_conversation_get_users(chat);
6520 pidgin_conv_chat_add_users(chat, users, TRUE);
6521 g_list_free(users);
6524 return TRUE;
6527 void *
6528 pidgin_conversations_get_handle(void)
6530 static int handle;
6532 return &handle;
6535 static void
6536 pidgin_conversations_pre_uninit(void);
6538 void
6539 pidgin_conversations_init(void)
6541 void *handle = pidgin_conversations_get_handle();
6542 void *blist_handle = purple_blist_get_handle();
6544 e2ee_stock = g_hash_table_new_full(g_str_hash, g_str_equal,
6545 g_free, g_object_unref);
6547 /* Conversations */
6548 purple_prefs_add_none(PIDGIN_PREFS_ROOT "/conversations");
6549 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/use_smooth_scrolling", TRUE);
6550 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/close_on_tabs", TRUE);
6551 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/send_bold", FALSE);
6552 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/send_italic", FALSE);
6553 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/send_underline", FALSE);
6554 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/send_strike", FALSE);
6555 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/spellcheck", TRUE);
6556 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/show_incoming_formatting", TRUE);
6557 /* TODO: it's about *remote* smileys, not local ones */
6558 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/resize_custom_smileys", TRUE);
6559 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/custom_smileys_size", 96);
6560 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/minimum_entry_lines", 2);
6562 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/show_formatting_toolbar", TRUE);
6564 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/placement", "last");
6565 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/placement_number", 1);
6566 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/bgcolor", "");
6567 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/fgcolor", "");
6568 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/font_face", "");
6569 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/font_size", 3);
6570 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/tabs", TRUE);
6571 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/tab_side", GTK_POS_TOP);
6572 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/scrollback_lines", 4000);
6574 #ifdef _WIN32
6575 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/use_theme_font", TRUE);
6576 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/custom_font", "");
6577 #endif
6579 /* Conversations -> Chat */
6580 purple_prefs_add_none(PIDGIN_PREFS_ROOT "/conversations/chat");
6581 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/entry_height", 54);
6582 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/userlist_width", 80);
6583 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/x", 0);
6584 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/y", 0);
6585 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/width", 340);
6586 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/height", 390);
6588 /* Conversations -> IM */
6589 purple_prefs_add_none(PIDGIN_PREFS_ROOT "/conversations/im");
6590 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/im/x", 0);
6591 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/im/y", 0);
6592 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/im/width", 340);
6593 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/im/height", 390);
6595 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/im/animate_buddy_icons", TRUE);
6597 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/im/entry_height", 54);
6598 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons", TRUE);
6600 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new", "never");
6601 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/im/close_immediately", TRUE);
6603 #ifdef _WIN32
6604 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/win32/minimize_new_convs", FALSE);
6605 #endif
6607 /* Connect callbacks. */
6608 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/close_on_tabs",
6609 close_on_tabs_pref_cb, NULL);
6610 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/show_formatting_toolbar",
6611 show_formatting_toolbar_pref_cb, NULL);
6612 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/spellcheck",
6613 spellcheck_pref_cb, NULL);
6614 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/tab_side",
6615 tab_side_pref_cb, NULL);
6617 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/tabs",
6618 conv_placement_usetabs_cb, NULL);
6620 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/placement",
6621 conv_placement_pref_cb, NULL);
6622 purple_prefs_trigger_callback(PIDGIN_PREFS_ROOT "/conversations/placement");
6624 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/minimum_entry_lines",
6625 minimum_entry_lines_pref_cb, NULL);
6627 /* IM callbacks */
6628 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/im/animate_buddy_icons",
6629 animate_buddy_icons_pref_cb, NULL);
6630 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons",
6631 show_buddy_icons_pref_cb, NULL);
6632 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/blist/show_protocol_icons",
6633 show_protocol_icons_pref_cb, NULL);
6634 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/im/hide_new",
6635 hide_new_pref_cb, NULL);
6637 /**********************************************************************
6638 * Register signals
6639 **********************************************************************/
6640 purple_signal_register(handle, "conversation-dragging",
6641 purple_marshal_VOID__POINTER_POINTER, G_TYPE_NONE, 2,
6642 G_TYPE_POINTER, /* pointer to a (PidginConvWindow *) */
6643 G_TYPE_POINTER); /* pointer to a (PidginConvWindow *) */
6645 purple_signal_register(handle, "conversation-timestamp",
6646 #if SIZEOF_TIME_T == 4
6647 purple_marshal_POINTER__POINTER_INT_BOOLEAN,
6648 #elif SIZEOF_TIME_T == 8
6649 purple_marshal_POINTER__POINTER_INT64_BOOLEAN,
6650 #else
6651 #error Unkown size of time_t
6652 #endif
6653 G_TYPE_STRING, 3, PURPLE_TYPE_CONVERSATION,
6654 #if SIZEOF_TIME_T == 4
6655 G_TYPE_INT,
6656 #elif SIZEOF_TIME_T == 8
6657 G_TYPE_INT64,
6658 #else
6659 # error Unknown size of time_t
6660 #endif
6661 G_TYPE_BOOLEAN);
6663 purple_signal_register(handle, "displaying-im-msg",
6664 purple_marshal_BOOLEAN__POINTER_POINTER,
6665 G_TYPE_BOOLEAN, 2, PURPLE_TYPE_CONVERSATION, PURPLE_TYPE_MESSAGE);
6667 purple_signal_register(handle, "displayed-im-msg",
6668 purple_marshal_VOID__POINTER_POINTER, G_TYPE_NONE, 2,
6669 PURPLE_TYPE_CONVERSATION, PURPLE_TYPE_MESSAGE);
6671 purple_signal_register(handle, "displaying-chat-msg",
6672 purple_marshal_BOOLEAN__POINTER_POINTER,
6673 G_TYPE_BOOLEAN, 2, PURPLE_TYPE_CONVERSATION, PURPLE_TYPE_MESSAGE);
6675 purple_signal_register(handle, "displayed-chat-msg",
6676 purple_marshal_VOID__POINTER_POINTER, G_TYPE_NONE, 2,
6677 PURPLE_TYPE_CONVERSATION, PURPLE_TYPE_MESSAGE);
6679 purple_signal_register(handle, "conversation-switched",
6680 purple_marshal_VOID__POINTER, G_TYPE_NONE, 1,
6681 PURPLE_TYPE_CONVERSATION);
6683 purple_signal_register(handle, "conversation-hiding",
6684 purple_marshal_VOID__POINTER, G_TYPE_NONE, 1,
6685 G_TYPE_POINTER); /* (PidginConversation *) */
6687 purple_signal_register(handle, "conversation-displayed",
6688 purple_marshal_VOID__POINTER, G_TYPE_NONE, 1,
6689 G_TYPE_POINTER); /* (PidginConversation *) */
6691 purple_signal_register(handle, "chat-nick-autocomplete",
6692 purple_marshal_BOOLEAN__POINTER_BOOLEAN,
6693 G_TYPE_BOOLEAN, 1, PURPLE_TYPE_CONVERSATION);
6695 purple_signal_register(handle, "chat-nick-clicked",
6696 purple_marshal_BOOLEAN__POINTER_POINTER_UINT,
6697 G_TYPE_BOOLEAN, 3, PURPLE_TYPE_CONVERSATION,
6698 G_TYPE_STRING, G_TYPE_UINT);
6700 purple_signal_register(handle, "conversation-window-created",
6701 purple_marshal_VOID__POINTER, G_TYPE_NONE, 1,
6702 G_TYPE_POINTER); /* (PidginConvWindow *) */
6705 /**********************************************************************
6706 * Register commands
6707 **********************************************************************/
6708 purple_cmd_register("say", "S", PURPLE_CMD_P_DEFAULT,
6709 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM, NULL,
6710 say_command_cb, _("say &lt;message&gt;: Send a message normally as if you weren't using a command."), NULL);
6711 purple_cmd_register("me", "S", PURPLE_CMD_P_DEFAULT,
6712 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM, NULL,
6713 me_command_cb, _("me &lt;action&gt;: Send an IRC style action to a buddy or chat."), NULL);
6714 purple_cmd_register("debug", "w", PURPLE_CMD_P_DEFAULT,
6715 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM, NULL,
6716 debug_command_cb, _("debug &lt;option&gt;: Send various debug information to the current conversation."), NULL);
6717 purple_cmd_register("clear", "", PURPLE_CMD_P_DEFAULT,
6718 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM, NULL,
6719 clear_command_cb, _("clear: Clears the conversation scrollback."), NULL);
6720 purple_cmd_register("clearall", "", PURPLE_CMD_P_DEFAULT,
6721 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM, NULL,
6722 clearall_command_cb, _("clear: Clears all conversation scrollbacks."), NULL);
6723 purple_cmd_register("help", "w", PURPLE_CMD_P_DEFAULT,
6724 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM | PURPLE_CMD_FLAG_ALLOW_WRONG_ARGS, NULL,
6725 help_command_cb, _("help &lt;command&gt;: Help on a specific command."), NULL);
6727 /**********************************************************************
6728 * UI operations
6729 **********************************************************************/
6731 purple_signal_connect(purple_connections_get_handle(), "signed-on", handle,
6732 G_CALLBACK(account_signed_off_cb),
6733 GINT_TO_POINTER(PURPLE_CONVERSATION_ACCOUNT_ONLINE));
6734 purple_signal_connect(purple_connections_get_handle(), "signed-off", handle,
6735 G_CALLBACK(account_signed_off_cb),
6736 GINT_TO_POINTER(PURPLE_CONVERSATION_ACCOUNT_OFFLINE));
6737 purple_signal_connect(purple_connections_get_handle(), "signing-off", handle,
6738 G_CALLBACK(account_signing_off), NULL);
6740 purple_signal_connect(purple_conversations_get_handle(), "writing-im-msg",
6741 handle, G_CALLBACK(writing_msg), NULL);
6742 purple_signal_connect(purple_conversations_get_handle(), "writing-chat-msg",
6743 handle, G_CALLBACK(writing_msg), NULL);
6744 purple_signal_connect(purple_conversations_get_handle(), "received-im-msg",
6745 handle, G_CALLBACK(received_im_msg_cb), NULL);
6746 purple_signal_connect(purple_conversations_get_handle(), "cleared-message-history",
6747 handle, G_CALLBACK(clear_conversation_scrollback_cb), NULL);
6749 purple_signal_connect(purple_conversations_get_handle(), "deleting-chat-user",
6750 handle, G_CALLBACK(deleting_chat_user_cb), NULL);
6752 purple_conversations_set_ui_ops(&conversation_ui_ops);
6754 hidden_convwin = pidgin_conv_window_new();
6755 window_list = g_list_remove(window_list, hidden_convwin);
6757 purple_signal_connect(purple_accounts_get_handle(), "account-status-changed",
6758 handle, PURPLE_CALLBACK(account_status_changed_cb), NULL);
6760 purple_signal_connect_priority(purple_get_core(), "quitting", handle,
6761 PURPLE_CALLBACK(pidgin_conversations_pre_uninit), NULL, PURPLE_SIGNAL_PRIORITY_HIGHEST);
6763 /* Callbacks to update a conversation */
6764 purple_signal_connect(blist_handle, "blist-node-added", handle,
6765 G_CALLBACK(buddy_update_cb), NULL);
6766 purple_signal_connect(blist_handle, "blist-node-removed", handle,
6767 G_CALLBACK(buddy_update_cb), NULL);
6768 purple_signal_connect(blist_handle, "buddy-signed-on",
6769 handle, PURPLE_CALLBACK(update_buddy_sign), "on");
6770 purple_signal_connect(blist_handle, "buddy-signed-off",
6771 handle, PURPLE_CALLBACK(update_buddy_sign), "off");
6772 purple_signal_connect(blist_handle, "buddy-status-changed",
6773 handle, PURPLE_CALLBACK(update_buddy_status_changed), NULL);
6774 purple_signal_connect(blist_handle, "buddy-privacy-changed",
6775 handle, PURPLE_CALLBACK(update_buddy_privacy_changed), NULL);
6776 purple_signal_connect(blist_handle, "buddy-idle-changed",
6777 handle, PURPLE_CALLBACK(update_buddy_idle_changed), NULL);
6778 purple_signal_connect(blist_handle, "buddy-icon-changed",
6779 handle, PURPLE_CALLBACK(update_buddy_icon), NULL);
6780 purple_signal_connect(purple_conversations_get_handle(), "buddy-typing",
6781 handle, PURPLE_CALLBACK(update_buddy_typing), NULL);
6782 purple_signal_connect(purple_conversations_get_handle(), "buddy-typing-stopped",
6783 handle, PURPLE_CALLBACK(update_buddy_typing), NULL);
6784 purple_signal_connect(pidgin_conversations_get_handle(), "conversation-switched",
6785 handle, PURPLE_CALLBACK(update_conversation_switched), NULL);
6786 purple_signal_connect(purple_conversations_get_handle(), "chat-left", handle,
6787 PURPLE_CALLBACK(update_chat), NULL);
6788 purple_signal_connect(purple_conversations_get_handle(), "chat-joined", handle,
6789 PURPLE_CALLBACK(update_chat), NULL);
6790 purple_signal_connect(purple_conversations_get_handle(), "chat-topic-changed", handle,
6791 PURPLE_CALLBACK(update_chat_topic), NULL);
6792 purple_signal_connect_priority(purple_conversations_get_handle(), "conversation-updated", handle,
6793 PURPLE_CALLBACK(pidgin_conv_updated), NULL,
6794 PURPLE_SIGNAL_PRIORITY_LOWEST);
6795 purple_signal_connect(purple_conversations_get_handle(), "wrote-im-msg", handle,
6796 PURPLE_CALLBACK(wrote_msg_update_unseen_cb), NULL);
6797 purple_signal_connect(purple_conversations_get_handle(), "wrote-chat-msg", handle,
6798 PURPLE_CALLBACK(wrote_msg_update_unseen_cb), NULL);
6801 static void
6802 pidgin_conversations_pre_uninit(void)
6804 g_hash_table_destroy(e2ee_stock);
6805 e2ee_stock = NULL;
6808 /* Invalidate the first tab color set */
6809 static gboolean tab_color_fuse = TRUE;
6811 static void
6812 pidgin_conversations_set_tab_colors(void)
6814 /* Set default tab colors */
6815 GString *str;
6816 GtkSettings *settings;
6817 GtkStyle *parent, *now;
6818 struct {
6819 const char *stylename;
6820 const char *labelname;
6821 const char *color;
6822 } styles[] = {
6823 {"pidgin_tab_label_typing_default", "tab-label-typing", "#4e9a06"},
6824 {"pidgin_tab_label_typed_default", "tab-label-typed", "#c4a000"},
6825 {"pidgin_tab_label_attention_default", "tab-label-attention", "#006aff"},
6826 {"pidgin_tab_label_unreadchat_default", "tab-label-unreadchat", "#cc0000"},
6827 {"pidgin_tab_label_event_default", "tab-label-event", "#888a85"},
6828 {NULL, NULL, NULL}
6830 int iter;
6832 if(tab_color_fuse) {
6833 tab_color_fuse = FALSE;
6834 return;
6837 str = g_string_new(NULL);
6838 settings = gtk_settings_get_default();
6839 parent = gtk_rc_get_style_by_paths(settings, "tab-container.tab-label*",
6840 NULL, G_TYPE_NONE);
6842 for (iter = 0; styles[iter].stylename; iter++) {
6843 now = gtk_rc_get_style_by_paths(settings, styles[iter].labelname, NULL, G_TYPE_NONE);
6844 if (parent == now ||
6845 (parent && now && parent->rc_style == now->rc_style)) {
6846 GdkRGBA color;
6847 gchar *color_str;
6849 gdk_rgba_parse(&color, styles[iter].color);
6850 pidgin_style_adjust_contrast(gtk_widget_get_default_style(), &color);
6852 color_str = gdk_rgba_to_string(&color);
6853 g_string_append_printf(str, "style \"%s\" {\n"
6854 "fg[ACTIVE] = \"%s\"\n"
6855 "}\n"
6856 "widget \"*%s\" style \"%s\"\n",
6857 styles[iter].stylename,
6858 color_str,
6859 styles[iter].labelname, styles[iter].stylename);
6860 g_free(color_str);
6863 gtk_rc_parse_string(str->str);
6864 g_string_free(str, TRUE);
6865 gtk_rc_reset_styles(settings);
6868 void
6869 pidgin_conversations_uninit(void)
6871 purple_prefs_disconnect_by_handle(pidgin_conversations_get_handle());
6872 purple_signals_disconnect_by_handle(pidgin_conversations_get_handle());
6873 purple_signals_unregister_by_instance(pidgin_conversations_get_handle());
6876 /**************************************************************************
6877 * PidginConversation GBoxed code
6878 **************************************************************************/
6879 static PidginConversation *
6880 pidgin_conversation_ref(PidginConversation *gtkconv)
6882 g_return_val_if_fail(gtkconv != NULL, NULL);
6884 gtkconv->box_count++;
6886 return gtkconv;
6889 static void
6890 pidgin_conversation_unref(PidginConversation *gtkconv)
6892 g_return_if_fail(gtkconv != NULL);
6893 g_return_if_fail(gtkconv->box_count >= 0);
6895 if (!gtkconv->box_count--)
6896 pidgin_conv_destroy(gtkconv->active_conv);
6899 GType
6900 pidgin_conversation_get_type(void)
6902 static GType type = 0;
6904 if (type == 0) {
6905 type = g_boxed_type_register_static("PidginConversation",
6906 (GBoxedCopyFunc)pidgin_conversation_ref,
6907 (GBoxedFreeFunc)pidgin_conversation_unref);
6910 return type;
6928 /* down here is where gtkconvwin.c ought to start. except they share like every freaking function,
6929 * and touch each others' private members all day long */
6931 /* pidgin
6933 * Pidgin is the legal property of its developers, whose names are too numerous
6934 * to list here. Please refer to the COPYRIGHT file distributed with this
6935 * source distribution.
6937 * This program is free software; you can redistribute it and/or modify
6938 * it under the terms of the GNU General Public License as published by
6939 * the Free Software Foundation; either version 2 of the License, or
6940 * (at your option) any later version.
6942 * This program is distributed in the hope that it will be useful,
6943 * but WITHOUT ANY WARRANTY; without even the implied warranty of
6944 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
6945 * GNU General Public License for more details.
6947 * You should have received a copy of the GNU General Public License
6948 * along with this program; if not, write to the Free Software
6949 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
6952 #include "internal.h"
6953 #include "pidgin.h"
6956 #include <gdk/gdkkeysyms.h>
6958 #include "account.h"
6959 #include "cmds.h"
6960 #include "debug.h"
6961 #include "log.h"
6962 #include "notify.h"
6963 #include "protocol.h"
6964 #include "request.h"
6965 #include "util.h"
6967 #include "gtkdnd-hints.h"
6968 #include "gtkblist.h"
6969 #include "gtkconv.h"
6970 #include "gtkdialogs.h"
6971 #include "gtkmenutray.h"
6972 #include "gtkpounce.h"
6973 #include "gtkprefs.h"
6974 #include "gtkprivacy.h"
6975 #include "gtkutils.h"
6976 #include "pidginstock.h"
6978 static void
6979 do_close(GtkWidget *w, int resp, PidginConvWindow *win)
6981 gtk_widget_destroy(warn_close_dialog);
6982 warn_close_dialog = NULL;
6984 if (resp == GTK_RESPONSE_OK)
6985 pidgin_conv_window_destroy(win);
6988 static void
6989 build_warn_close_dialog(PidginConvWindow *gtkwin)
6991 GtkWidget *label, *vbox, *hbox, *img;
6993 g_return_if_fail(warn_close_dialog == NULL);
6995 warn_close_dialog = gtk_dialog_new_with_buttons(_("Confirm close"),
6996 GTK_WINDOW(gtkwin->window), GTK_DIALOG_MODAL,
6997 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
6998 GTK_STOCK_CLOSE, GTK_RESPONSE_OK, NULL);
7000 gtk_dialog_set_default_response(GTK_DIALOG(warn_close_dialog),
7001 GTK_RESPONSE_OK);
7003 gtk_container_set_border_width(GTK_CONTAINER(warn_close_dialog),
7005 gtk_window_set_resizable(GTK_WINDOW(warn_close_dialog), FALSE);
7007 /* Setup the outside spacing. */
7008 vbox = gtk_dialog_get_content_area(GTK_DIALOG(warn_close_dialog));
7010 gtk_box_set_spacing(GTK_BOX(vbox), 12);
7011 gtk_container_set_border_width(GTK_CONTAINER(vbox), 6);
7013 img = gtk_image_new_from_icon_name("dialog-warning",
7014 GTK_ICON_SIZE_DIALOG);
7016 /* Setup the inner hbox and put the dialog's icon in it. */
7017 hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12);
7018 gtk_container_add(GTK_CONTAINER(vbox), hbox);
7019 gtk_box_pack_start(GTK_BOX(hbox), img, FALSE, FALSE, 0);
7020 gtk_widget_set_halign(img, GTK_ALIGN_START);
7021 gtk_widget_set_valign(img, GTK_ALIGN_START);
7023 /* Setup the right vbox. */
7024 vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
7025 gtk_container_add(GTK_CONTAINER(hbox), vbox);
7027 label = gtk_label_new(_("You have unread messages. Are you sure you want to close the window?"));
7028 gtk_widget_set_size_request(label, 350, -1);
7029 gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
7030 gtk_label_set_xalign(GTK_LABEL(label), 0);
7031 gtk_label_set_yalign(GTK_LABEL(label), 0);
7032 gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 0);
7034 /* Connect the signals. */
7035 g_signal_connect(G_OBJECT(warn_close_dialog), "response",
7036 G_CALLBACK(do_close), gtkwin);
7040 /**************************************************************************
7041 * Callbacks
7042 **************************************************************************/
7044 static gboolean
7045 close_win_cb(GtkWidget *w, GdkEventAny *e, gpointer d)
7047 PidginConvWindow *win = d;
7048 GList *l;
7050 /* If there are unread messages then show a warning dialog */
7051 for (l = pidgin_conv_window_get_gtkconvs(win);
7052 l != NULL; l = l->next)
7054 PidginConversation *gtkconv = l->data;
7055 if (PURPLE_IS_IM_CONVERSATION(gtkconv->active_conv) &&
7056 gtkconv->unseen_state >= PIDGIN_UNSEEN_TEXT)
7058 build_warn_close_dialog(win);
7059 gtk_widget_show_all(warn_close_dialog);
7061 return TRUE;
7065 pidgin_conv_window_destroy(win);
7067 return TRUE;
7070 static void
7071 conv_set_unseen(PurpleConversation *conv, PidginUnseenState state)
7073 int unseen_count = 0;
7074 PidginUnseenState unseen_state = PIDGIN_UNSEEN_NONE;
7076 if(g_object_get_data(G_OBJECT(conv), "unseen-count"))
7077 unseen_count = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(conv), "unseen-count"));
7079 if(g_object_get_data(G_OBJECT(conv), "unseen-state"))
7080 unseen_state = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(conv), "unseen-state"));
7082 if (state == PIDGIN_UNSEEN_NONE)
7084 unseen_count = 0;
7085 unseen_state = PIDGIN_UNSEEN_NONE;
7087 else
7089 if (state >= PIDGIN_UNSEEN_TEXT)
7090 unseen_count++;
7092 if (state > unseen_state)
7093 unseen_state = state;
7096 g_object_set_data(G_OBJECT(conv), "unseen-count", GINT_TO_POINTER(unseen_count));
7097 g_object_set_data(G_OBJECT(conv), "unseen-state", GINT_TO_POINTER(unseen_state));
7099 purple_conversation_update(conv, PURPLE_CONVERSATION_UPDATE_UNSEEN);
7102 static void
7103 gtkconv_set_unseen(PidginConversation *gtkconv, PidginUnseenState state)
7105 if (state == PIDGIN_UNSEEN_NONE)
7107 gtkconv->unseen_count = 0;
7108 gtkconv->unseen_state = PIDGIN_UNSEEN_NONE;
7110 else
7112 if (state >= PIDGIN_UNSEEN_TEXT)
7113 gtkconv->unseen_count++;
7115 if (state > gtkconv->unseen_state)
7116 gtkconv->unseen_state = state;
7119 g_object_set_data(G_OBJECT(gtkconv->active_conv), "unseen-count", GINT_TO_POINTER(gtkconv->unseen_count));
7120 g_object_set_data(G_OBJECT(gtkconv->active_conv), "unseen-state", GINT_TO_POINTER(gtkconv->unseen_state));
7122 purple_conversation_update(gtkconv->active_conv, PURPLE_CONVERSATION_UPDATE_UNSEEN);
7126 * When a conversation window is focused, we know the user
7127 * has looked at it so we know there are no longer unseen
7128 * messages.
7130 static gboolean
7131 focus_win_cb(GtkWidget *w, GdkEventFocus *e, gpointer d)
7133 PidginConvWindow *win = d;
7134 PidginConversation *gtkconv = pidgin_conv_window_get_active_gtkconv(win);
7136 if (gtkconv)
7137 gtkconv_set_unseen(gtkconv, PIDGIN_UNSEEN_NONE);
7139 return FALSE;
7142 static void
7143 notebook_init_grab(PidginConvWindow *gtkwin, GtkWidget *widget, GdkEvent *event)
7145 static GdkCursor *cursor = NULL;
7146 GdkDevice *device;
7148 gtkwin->in_drag = TRUE;
7150 if (gtkwin->drag_leave_signal) {
7151 g_signal_handler_disconnect(G_OBJECT(widget),
7152 gtkwin->drag_leave_signal);
7153 gtkwin->drag_leave_signal = 0;
7156 if (cursor == NULL) {
7157 GdkDisplay *display = gtk_widget_get_display(gtkwin->notebook);
7158 cursor = gdk_cursor_new_for_display(display, GDK_FLEUR);
7161 /* Grab the pointer */
7162 gtk_grab_add(gtkwin->notebook);
7163 device = gdk_event_get_device(event);
7164 if (!gdk_display_device_is_grabbed(gdk_device_get_display(device),
7165 device)) {
7166 gdk_seat_grab(gdk_event_get_seat(event),
7167 gtk_widget_get_window(gtkwin->notebook),
7168 GDK_SEAT_CAPABILITY_ALL_POINTING, FALSE, cursor, event,
7169 NULL, NULL);
7173 static gboolean
7174 notebook_motion_cb(GtkWidget *widget, GdkEventButton *e, PidginConvWindow *win)
7178 * Make sure the user moved the mouse far enough for the
7179 * drag to be initiated.
7181 if (win->in_predrag) {
7182 if (e->x_root < win->drag_min_x ||
7183 e->x_root >= win->drag_max_x ||
7184 e->y_root < win->drag_min_y ||
7185 e->y_root >= win->drag_max_y) {
7187 win->in_predrag = FALSE;
7188 notebook_init_grab(win, widget, (GdkEvent *)e);
7191 else { /* Otherwise, draw the arrows. */
7192 PidginConvWindow *dest_win;
7193 GtkNotebook *dest_notebook;
7194 GtkWidget *tab;
7195 gint page_num;
7196 gboolean horiz_tabs = FALSE;
7197 gboolean to_right = FALSE;
7199 /* Get the window that the cursor is over. */
7200 dest_win = pidgin_conv_window_get_at_event((GdkEvent *)e);
7202 if (dest_win == NULL) {
7203 pidgin_dnd_hints_hide_all();
7205 return TRUE;
7208 dest_notebook = GTK_NOTEBOOK(dest_win->notebook);
7210 if (gtk_notebook_get_show_tabs(dest_notebook)) {
7211 page_num = pidgin_conv_get_tab_at_xy(dest_win,
7212 e->x_root, e->y_root, &to_right);
7213 to_right = to_right && (win != dest_win);
7214 tab = pidgin_conv_window_get_gtkconv_at_index(dest_win, page_num)->tabby;
7215 } else {
7216 page_num = 0;
7217 to_right = pidgin_conv_xy_to_right_infopane(dest_win, e->x_root, e->y_root);
7218 tab = pidgin_conv_window_get_gtkconv_at_index(dest_win, page_num)->infopane_hbox;
7221 if (gtk_notebook_get_tab_pos(dest_notebook) == GTK_POS_TOP ||
7222 gtk_notebook_get_tab_pos(dest_notebook) == GTK_POS_BOTTOM) {
7223 horiz_tabs = TRUE;
7226 if (gtk_notebook_get_show_tabs(dest_notebook) == FALSE && win == dest_win)
7228 /* dragging a tab from a single-tabbed window over its own window */
7229 pidgin_dnd_hints_hide_all();
7230 return TRUE;
7231 } else if (horiz_tabs) {
7232 if (((gpointer)win == (gpointer)dest_win && win->drag_tab < page_num) || to_right) {
7233 pidgin_dnd_hints_show_relative(HINT_ARROW_DOWN, tab, HINT_POSITION_RIGHT, HINT_POSITION_TOP);
7234 pidgin_dnd_hints_show_relative(HINT_ARROW_UP, tab, HINT_POSITION_RIGHT, HINT_POSITION_BOTTOM);
7235 } else {
7236 pidgin_dnd_hints_show_relative(HINT_ARROW_DOWN, tab, HINT_POSITION_LEFT, HINT_POSITION_TOP);
7237 pidgin_dnd_hints_show_relative(HINT_ARROW_UP, tab, HINT_POSITION_LEFT, HINT_POSITION_BOTTOM);
7239 } else {
7240 if (((gpointer)win == (gpointer)dest_win && win->drag_tab < page_num) || to_right) {
7241 pidgin_dnd_hints_show_relative(HINT_ARROW_RIGHT, tab, HINT_POSITION_LEFT, HINT_POSITION_BOTTOM);
7242 pidgin_dnd_hints_show_relative(HINT_ARROW_LEFT, tab, HINT_POSITION_RIGHT, HINT_POSITION_BOTTOM);
7243 } else {
7244 pidgin_dnd_hints_show_relative(HINT_ARROW_RIGHT, tab, HINT_POSITION_LEFT, HINT_POSITION_TOP);
7245 pidgin_dnd_hints_show_relative(HINT_ARROW_LEFT, tab, HINT_POSITION_RIGHT, HINT_POSITION_TOP);
7250 return TRUE;
7253 static gboolean
7254 notebook_leave_cb(GtkWidget *widget, GdkEventCrossing *e, PidginConvWindow *win)
7256 if (win->in_drag)
7257 return FALSE;
7259 if (e->x_root < win->drag_min_x ||
7260 e->x_root >= win->drag_max_x ||
7261 e->y_root < win->drag_min_y ||
7262 e->y_root >= win->drag_max_y) {
7264 win->in_predrag = FALSE;
7265 notebook_init_grab(win, widget, (GdkEvent *)e);
7268 return TRUE;
7272 * THANK YOU GALEON!
7275 static gboolean
7276 infopane_press_cb(GtkWidget *widget, GdkEventButton *e, PidginConversation *gtkconv)
7278 if (e->type == GDK_2BUTTON_PRESS && e->button == GDK_BUTTON_PRIMARY) {
7279 if (infopane_entry_activate(gtkconv))
7280 return TRUE;
7283 if (e->type != GDK_BUTTON_PRESS)
7284 return FALSE;
7286 if (e->button == GDK_BUTTON_PRIMARY) {
7287 int nb_x, nb_y;
7288 GtkAllocation allocation;
7290 gtk_widget_get_allocation(gtkconv->infopane_hbox, &allocation);
7292 if (gtkconv->win->in_drag)
7293 return TRUE;
7295 gtkconv->win->in_predrag = TRUE;
7296 gtkconv->win->drag_tab = gtk_notebook_page_num(GTK_NOTEBOOK(gtkconv->win->notebook), gtkconv->tab_cont);
7298 gdk_window_get_origin(gtk_widget_get_window(gtkconv->infopane_hbox), &nb_x, &nb_y);
7300 gtkconv->win->drag_min_x = allocation.x + nb_x;
7301 gtkconv->win->drag_min_y = allocation.y + nb_y;
7302 gtkconv->win->drag_max_x = allocation.width + gtkconv->win->drag_min_x;
7303 gtkconv->win->drag_max_y = allocation.height + gtkconv->win->drag_min_y;
7305 gtkconv->win->drag_motion_signal = g_signal_connect(G_OBJECT(gtkconv->win->notebook), "motion_notify_event",
7306 G_CALLBACK(notebook_motion_cb), gtkconv->win);
7307 gtkconv->win->drag_leave_signal = g_signal_connect(G_OBJECT(gtkconv->win->notebook), "leave_notify_event",
7308 G_CALLBACK(notebook_leave_cb), gtkconv->win);
7309 return FALSE;
7312 if (gdk_event_triggers_context_menu((GdkEvent *)e)) {
7313 /* Right click was pressed. Popup the context menu. */
7314 GtkWidget *menu = gtk_menu_new(), *sub;
7315 gboolean populated = populate_menu_with_options(menu, gtkconv, TRUE);
7317 sub = gtk_menu_item_get_submenu(GTK_MENU_ITEM(gtkconv->win->menu->send_to));
7318 if (sub && gtk_widget_is_sensitive(gtkconv->win->menu->send_to)) {
7319 GtkWidget *item = gtk_menu_item_new_with_mnemonic(_("S_end To"));
7320 if (populated)
7321 pidgin_separator(menu);
7322 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
7323 gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), sub);
7324 gtk_widget_show(item);
7325 gtk_widget_show_all(sub);
7326 } else if (!populated) {
7327 gtk_widget_destroy(menu);
7328 return FALSE;
7331 gtk_widget_show_all(menu);
7332 gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent *)e);
7333 return TRUE;
7335 return FALSE;
7338 static gboolean
7339 notebook_press_cb(GtkWidget *widget, GdkEventButton *e, PidginConvWindow *win)
7341 gint nb_x, nb_y;
7342 int tab_clicked;
7343 GtkWidget *page;
7344 GtkWidget *tab;
7345 GtkAllocation allocation;
7347 if (e->button == GDK_BUTTON_MIDDLE && e->type == GDK_BUTTON_PRESS) {
7348 PidginConversation *gtkconv;
7349 tab_clicked = pidgin_conv_get_tab_at_xy(win, e->x_root, e->y_root, NULL);
7351 if (tab_clicked == -1)
7352 return FALSE;
7354 gtkconv = pidgin_conv_window_get_gtkconv_at_index(win, tab_clicked);
7355 close_conv_cb(NULL, gtkconv);
7356 return TRUE;
7360 if (e->button != GDK_BUTTON_PRIMARY || e->type != GDK_BUTTON_PRESS)
7361 return FALSE;
7364 if (win->in_drag) {
7365 purple_debug(PURPLE_DEBUG_WARNING, "gtkconv",
7366 "Already in the middle of a window drag at tab_press_cb\n");
7367 return TRUE;
7371 * Make sure a tab was actually clicked. The arrow buttons
7372 * mess things up.
7374 tab_clicked = pidgin_conv_get_tab_at_xy(win, e->x_root, e->y_root, NULL);
7376 if (tab_clicked == -1)
7377 return FALSE;
7380 * Get the relative position of the press event, with regards to
7381 * the position of the notebook.
7383 gdk_window_get_origin(gtk_widget_get_window(win->notebook), &nb_x, &nb_y);
7385 /* Reset the min/max x/y */
7386 win->drag_min_x = 0;
7387 win->drag_min_y = 0;
7388 win->drag_max_x = 0;
7389 win->drag_max_y = 0;
7391 /* Find out which tab was dragged. */
7392 page = gtk_notebook_get_nth_page(GTK_NOTEBOOK(win->notebook), tab_clicked);
7393 tab = gtk_notebook_get_tab_label(GTK_NOTEBOOK(win->notebook), page);
7395 gtk_widget_get_allocation(tab, &allocation);
7397 win->drag_min_x = allocation.x + nb_x;
7398 win->drag_min_y = allocation.y + nb_y;
7399 win->drag_max_x = allocation.width + win->drag_min_x;
7400 win->drag_max_y = allocation.height + win->drag_min_y;
7402 /* Make sure the click occurred in the tab. */
7403 if (e->x_root < win->drag_min_x ||
7404 e->x_root >= win->drag_max_x ||
7405 e->y_root < win->drag_min_y ||
7406 e->y_root >= win->drag_max_y) {
7408 return FALSE;
7411 win->in_predrag = TRUE;
7412 win->drag_tab = tab_clicked;
7414 /* Connect the new motion signals. */
7415 win->drag_motion_signal =
7416 g_signal_connect(G_OBJECT(widget), "motion_notify_event",
7417 G_CALLBACK(notebook_motion_cb), win);
7419 win->drag_leave_signal =
7420 g_signal_connect(G_OBJECT(widget), "leave_notify_event",
7421 G_CALLBACK(notebook_leave_cb), win);
7423 return FALSE;
7426 static gboolean
7427 notebook_release_cb(GtkWidget *widget, GdkEventButton *e, PidginConvWindow *win)
7429 PidginConvWindow *dest_win;
7430 GtkNotebook *dest_notebook;
7431 PidginConversation *active_gtkconv;
7432 PidginConversation *gtkconv;
7433 gint dest_page_num = 0;
7434 gboolean new_window = FALSE;
7435 gboolean to_right = FALSE;
7436 GdkDevice *device;
7439 * Don't check to make sure that the event's window matches the
7440 * widget's, because we may be getting an event passed on from the
7441 * close button.
7443 if (e->button != GDK_BUTTON_PRIMARY && e->type != GDK_BUTTON_RELEASE)
7444 return FALSE;
7446 device = gdk_event_get_device((GdkEvent *)e);
7447 if (gdk_display_device_is_grabbed(gdk_device_get_display(device), device)) {
7448 gdk_seat_ungrab(gdk_event_get_seat((GdkEvent *)e));
7449 gtk_grab_remove(widget);
7452 if (!win->in_predrag && !win->in_drag)
7453 return FALSE;
7455 /* Disconnect the motion signal. */
7456 if (win->drag_motion_signal) {
7457 g_signal_handler_disconnect(G_OBJECT(widget),
7458 win->drag_motion_signal);
7460 win->drag_motion_signal = 0;
7464 * If we're in a pre-drag, we'll also need to disconnect the leave
7465 * signal.
7467 if (win->in_predrag) {
7468 win->in_predrag = FALSE;
7470 if (win->drag_leave_signal) {
7471 g_signal_handler_disconnect(G_OBJECT(widget),
7472 win->drag_leave_signal);
7474 win->drag_leave_signal = 0;
7478 /* If we're not in drag... */
7479 /* We're perfectly normal people! */
7480 if (!win->in_drag)
7481 return FALSE;
7483 win->in_drag = FALSE;
7485 pidgin_dnd_hints_hide_all();
7487 dest_win = pidgin_conv_window_get_at_event((GdkEvent *)e);
7489 active_gtkconv = pidgin_conv_window_get_active_gtkconv(win);
7491 if (dest_win == NULL) {
7492 /* If the current window doesn't have any other conversations,
7493 * there isn't much point transferring the conv to a new window. */
7494 if (pidgin_conv_window_get_gtkconv_count(win) > 1) {
7495 /* Make a new window to stick this to. */
7496 dest_win = pidgin_conv_window_new();
7497 new_window = TRUE;
7501 if (dest_win == NULL)
7502 return FALSE;
7504 purple_signal_emit(pidgin_conversations_get_handle(),
7505 "conversation-dragging", win, dest_win);
7507 /* Get the destination page number. */
7508 if (!new_window) {
7509 dest_notebook = GTK_NOTEBOOK(dest_win->notebook);
7510 if (gtk_notebook_get_show_tabs(dest_notebook)) {
7511 dest_page_num = pidgin_conv_get_tab_at_xy(dest_win,
7512 e->x_root, e->y_root, &to_right);
7513 } else {
7514 dest_page_num = 0;
7515 to_right = pidgin_conv_xy_to_right_infopane(dest_win, e->x_root, e->y_root);
7519 gtkconv = pidgin_conv_window_get_gtkconv_at_index(win, win->drag_tab);
7521 if (win == dest_win) {
7522 gtk_notebook_reorder_child(GTK_NOTEBOOK(win->notebook), gtkconv->tab_cont, dest_page_num);
7523 } else {
7524 pidgin_conv_window_remove_gtkconv(win, gtkconv);
7525 pidgin_conv_window_add_gtkconv(dest_win, gtkconv);
7526 gtk_notebook_reorder_child(GTK_NOTEBOOK(dest_win->notebook), gtkconv->tab_cont, dest_page_num + to_right);
7527 pidgin_conv_window_switch_gtkconv(dest_win, gtkconv);
7528 if (new_window) {
7529 gint win_width, win_height;
7531 gtk_window_get_size(GTK_WINDOW(dest_win->window),
7532 &win_width, &win_height);
7533 #ifdef _WIN32 /* only override window manager placement on Windows */
7534 gtk_window_move(GTK_WINDOW(dest_win->window),
7535 e->x_root - (win_width / 2),
7536 e->y_root - (win_height / 2));
7537 #endif
7539 pidgin_conv_window_show(dest_win);
7543 gtk_widget_grab_focus(active_gtkconv->editor);
7545 return TRUE;
7549 static void
7550 before_switch_conv_cb(GtkNotebook *notebook, GtkWidget *page, gint page_num,
7551 gpointer user_data)
7553 PidginConvWindow *win;
7554 PurpleConversation *conv;
7555 PidginConversation *gtkconv;
7557 win = user_data;
7558 conv = pidgin_conv_window_get_active_conversation(win);
7560 g_return_if_fail(conv != NULL);
7562 if (!PURPLE_IS_IM_CONVERSATION(conv))
7563 return;
7565 gtkconv = PIDGIN_CONVERSATION(conv);
7567 if (gtkconv->u.im->typing_timer != 0) {
7568 g_source_remove(gtkconv->u.im->typing_timer);
7569 gtkconv->u.im->typing_timer = 0;
7572 stop_anim(NULL, gtkconv);
7575 static void
7576 close_window(GtkWidget *w, PidginConvWindow *win)
7578 close_win_cb(w, NULL, win);
7581 static void
7582 detach_tab_cb(GtkWidget *w, PidginConvWindow *win)
7584 PidginConvWindow *new_window;
7585 PidginConversation *gtkconv;
7587 gtkconv = win->clicked_tab;
7589 if (!gtkconv)
7590 return;
7592 /* Nothing to do if there's only one tab in the window */
7593 if (pidgin_conv_window_get_gtkconv_count(win) == 1)
7594 return;
7596 pidgin_conv_window_remove_gtkconv(win, gtkconv);
7598 new_window = pidgin_conv_window_new();
7599 pidgin_conv_window_add_gtkconv(new_window, gtkconv);
7600 pidgin_conv_window_show(new_window);
7603 static void
7604 close_others_cb(GtkWidget *w, PidginConvWindow *win)
7606 GList *iter;
7607 PidginConversation *gtkconv;
7609 gtkconv = win->clicked_tab;
7611 if (!gtkconv)
7612 return;
7614 for (iter = pidgin_conv_window_get_gtkconvs(win); iter; )
7616 PidginConversation *gconv = iter->data;
7617 iter = iter->next;
7619 if (gconv != gtkconv)
7621 close_conv_cb(NULL, gconv);
7626 static void
7627 close_tab_cb(GtkWidget *w, PidginConvWindow *win)
7629 PidginConversation *gtkconv;
7631 gtkconv = win->clicked_tab;
7633 if (gtkconv)
7634 close_conv_cb(NULL, gtkconv);
7637 static void
7638 notebook_menu_switch_cb(GtkWidget *item, GtkWidget *child)
7640 GtkNotebook *notebook;
7641 int index;
7643 notebook = GTK_NOTEBOOK(gtk_widget_get_parent(child));
7644 index = gtk_notebook_page_num(notebook, child);
7645 gtk_notebook_set_current_page(notebook, index);
7648 static void
7649 notebook_menu_update_label_cb(GtkWidget *child, GParamSpec *pspec,
7650 GtkNotebook *notebook)
7652 GtkWidget *item;
7653 GtkWidget *label;
7655 item = g_object_get_data(G_OBJECT(child), "popup-menu-item");
7656 label = gtk_bin_get_child(GTK_BIN(item));
7657 if (label)
7658 gtk_container_remove(GTK_CONTAINER(item), label);
7660 label = gtk_notebook_get_menu_label(notebook, child);
7661 if (label) {
7662 gtk_widget_show(label);
7663 gtk_container_add(GTK_CONTAINER(item), label);
7664 gtk_widget_show(item);
7665 } else {
7666 gtk_widget_hide(item);
7670 static void
7671 notebook_add_tab_to_menu_cb(GtkNotebook *notebook, GtkWidget *child,
7672 guint page_num, PidginConvWindow *win)
7674 GtkWidget *item;
7675 GtkWidget *label;
7677 item = gtk_menu_item_new();
7678 label = gtk_notebook_get_menu_label(notebook, child);
7679 if (label) {
7680 gtk_widget_show(label);
7681 gtk_container_add(GTK_CONTAINER(item), label);
7682 gtk_widget_show(item);
7685 g_signal_connect(child, "child-notify::menu-label",
7686 G_CALLBACK(notebook_menu_update_label_cb), notebook);
7687 g_signal_connect(item, "activate",
7688 G_CALLBACK(notebook_menu_switch_cb), child);
7689 g_object_set_data(G_OBJECT(child), "popup-menu-item", item);
7691 gtk_menu_shell_insert(GTK_MENU_SHELL(win->notebook_menu), item, page_num);
7694 static void
7695 notebook_remove_tab_from_menu_cb(GtkNotebook *notebook, GtkWidget *child,
7696 guint page_num, PidginConvWindow *win)
7698 GtkWidget *item;
7700 /* Disconnecting the "child-notify::menu-label" signal. */
7701 g_signal_handlers_disconnect_by_data(child, notebook);
7703 item = g_object_get_data(G_OBJECT(child), "popup-menu-item");
7704 gtk_container_remove(GTK_CONTAINER(win->notebook_menu), item);
7708 static void
7709 notebook_reorder_tab_in_menu_cb(GtkNotebook *notebook, GtkWidget *child,
7710 guint page_num, PidginConvWindow *win)
7712 GtkWidget *item;
7714 item = g_object_get_data(G_OBJECT(child), "popup-menu-item");
7715 gtk_menu_reorder_child(GTK_MENU(win->notebook_menu), item, page_num);
7718 static gboolean
7719 notebook_right_click_menu_cb(GtkNotebook *notebook, GdkEventButton *event,
7720 PidginConvWindow *win)
7722 GtkWidget *menu;
7723 PidginConversation *gtkconv;
7725 if (!gdk_event_triggers_context_menu((GdkEvent *)event))
7726 return FALSE;
7728 gtkconv = pidgin_conv_window_get_gtkconv_at_index(win,
7729 pidgin_conv_get_tab_at_xy(win, event->x_root, event->y_root, NULL));
7731 win->clicked_tab = gtkconv;
7733 menu = win->notebook_menu;
7735 gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent *)event);
7737 return TRUE;
7740 static void
7741 remove_edit_entry(PidginConversation *gtkconv, GtkWidget *entry)
7743 g_signal_handlers_disconnect_matched(G_OBJECT(entry), G_SIGNAL_MATCH_DATA,
7744 0, 0, NULL, NULL, gtkconv);
7745 gtk_widget_show(gtkconv->infopane);
7746 gtk_widget_grab_focus(gtkconv->editor);
7747 gtk_widget_destroy(entry);
7750 static gboolean
7751 alias_focus_cb(GtkWidget *widget, GdkEventFocus *event, gpointer user_data)
7753 remove_edit_entry(user_data, widget);
7754 return FALSE;
7757 static gboolean
7758 alias_key_press_cb(GtkWidget *widget, GdkEventKey *event, gpointer user_data)
7760 if (event->keyval == GDK_KEY_Escape) {
7761 remove_edit_entry(user_data, widget);
7762 return TRUE;
7764 return FALSE;
7767 static void
7768 alias_cb(GtkEntry *entry, gpointer user_data)
7770 PidginConversation *gtkconv;
7771 PurpleConversation *conv;
7772 PurpleAccount *account;
7773 const char *name;
7775 gtkconv = (PidginConversation *)user_data;
7776 if (gtkconv == NULL) {
7777 return;
7779 conv = gtkconv->active_conv;
7780 account = purple_conversation_get_account(conv);
7781 name = purple_conversation_get_name(conv);
7783 if (PURPLE_IS_IM_CONVERSATION(conv)) {
7784 PurpleBuddy *buddy;
7785 buddy = purple_blist_find_buddy(account, name);
7786 if (buddy != NULL) {
7787 purple_buddy_set_local_alias(buddy, gtk_entry_get_text(entry));
7789 purple_serv_alias_buddy(buddy);
7790 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
7791 gtk_entry_set_text(GTK_ENTRY(gtkconv->u.chat->topic_text), gtk_entry_get_text(entry));
7792 topic_callback(NULL, gtkconv);
7794 remove_edit_entry(user_data, GTK_WIDGET(entry));
7797 static gboolean
7798 infopane_entry_activate(PidginConversation *gtkconv)
7800 GtkWidget *entry = NULL;
7801 PurpleConversation *conv = gtkconv->active_conv;
7802 const char *text = NULL;
7804 if (!gtk_widget_get_visible(gtkconv->infopane)) {
7805 /* There's already an entry for alias. Let's not create another one. */
7806 return FALSE;
7809 if (!purple_account_is_connected(purple_conversation_get_account(gtkconv->active_conv))) {
7810 /* Do not allow aliasing someone on a disconnected account. */
7811 return FALSE;
7814 if (PURPLE_IS_IM_CONVERSATION(conv)) {
7815 PurpleBuddy *buddy = purple_blist_find_buddy(purple_conversation_get_account(gtkconv->active_conv), purple_conversation_get_name(gtkconv->active_conv));
7816 if (!buddy)
7817 /* This buddy isn't in your buddy list, so we can't alias him */
7818 return FALSE;
7820 text = purple_buddy_get_contact_alias(buddy);
7821 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
7822 PurpleConnection *gc;
7823 PurpleProtocol *protocol = NULL;
7825 gc = purple_conversation_get_connection(conv);
7826 if (gc != NULL)
7827 protocol = purple_connection_get_protocol(gc);
7828 if (protocol && !PURPLE_PROTOCOL_IMPLEMENTS(protocol, CHAT, set_topic))
7829 /* This protocol doesn't support setting the chat room topic */
7830 return FALSE;
7832 text = purple_chat_conversation_get_topic(PURPLE_CHAT_CONVERSATION(conv));
7835 /* alias label */
7836 entry = gtk_entry_new();
7837 gtk_entry_set_has_frame(GTK_ENTRY(entry), FALSE);
7838 gtk_entry_set_width_chars(GTK_ENTRY(entry), 10);
7839 gtk_entry_set_alignment(GTK_ENTRY(entry), 0.5);
7841 gtk_box_pack_start(GTK_BOX(gtkconv->infopane_hbox), entry, TRUE, TRUE, 0);
7842 /* after the tab label */
7843 gtk_box_reorder_child(GTK_BOX(gtkconv->infopane_hbox), entry, 0);
7845 g_signal_connect(G_OBJECT(entry), "activate", G_CALLBACK(alias_cb), gtkconv);
7846 g_signal_connect(G_OBJECT(entry), "focus-out-event", G_CALLBACK(alias_focus_cb), gtkconv);
7847 g_signal_connect(G_OBJECT(entry), "key-press-event", G_CALLBACK(alias_key_press_cb), gtkconv);
7849 if (text != NULL)
7850 gtk_entry_set_text(GTK_ENTRY(entry), text);
7851 gtk_widget_show(entry);
7852 gtk_widget_hide(gtkconv->infopane);
7853 gtk_widget_grab_focus(entry);
7855 return TRUE;
7858 static gboolean
7859 window_keypress_cb(GtkWidget *widget, GdkEventKey *event, PidginConvWindow *win)
7861 PidginConversation *gtkconv = pidgin_conv_window_get_active_gtkconv(win);
7863 return conv_keypress_common(gtkconv, event);
7866 static void
7867 switch_conv_cb(GtkNotebook *notebook, GtkWidget *page, gint page_num,
7868 gpointer user_data)
7870 PidginConvWindow *win;
7871 PurpleConversation *conv;
7872 PidginConversation *gtkconv;
7873 const char *sound_method;
7875 win = user_data;
7876 gtkconv = pidgin_conv_window_get_gtkconv_at_index(win, page_num);
7877 conv = gtkconv->active_conv;
7879 g_return_if_fail(conv != NULL);
7881 /* clear unseen flag if conversation is not hidden */
7882 if(!pidgin_conv_is_hidden(gtkconv)) {
7883 gtkconv_set_unseen(gtkconv, PIDGIN_UNSEEN_NONE);
7886 /* Update the menubar */
7888 gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(gtkconv->win->menu->logging),
7889 purple_conversation_is_logging(conv));
7891 generate_send_to_items(win);
7892 generate_e2ee_controls(win);
7893 regenerate_options_items(win);
7894 regenerate_plugins_items(win);
7896 pidgin_conv_switch_active_conversation(conv);
7898 sound_method = purple_prefs_get_string(PIDGIN_PREFS_ROOT "/sound/method");
7899 if (!purple_strequal(sound_method, "none"))
7900 gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(win->menu->sounds),
7901 gtkconv->make_sound);
7903 gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(win->menu->show_formatting_toolbar),
7904 purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/show_formatting_toolbar"));
7907 * We pause icons when they are not visible. If this icon should
7908 * be animated then start it back up again.
7910 if (PURPLE_IS_IM_CONVERSATION(conv) &&
7911 (gtkconv->u.im->animate))
7912 start_anim(NULL, gtkconv);
7914 purple_signal_emit(pidgin_conversations_get_handle(), "conversation-switched", conv);
7917 /**************************************************************************
7918 * GTK+ window ops
7919 **************************************************************************/
7921 GList *
7922 pidgin_conv_windows_get_list()
7924 return window_list;
7927 static GList*
7928 make_status_icon_list(const char *stock, GtkWidget *w)
7930 GList *l = NULL;
7931 l = g_list_append(l,
7932 gtk_widget_render_icon(w, stock,
7933 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL), "GtkWindow"));
7934 l = g_list_append(l,
7935 gtk_widget_render_icon(w, stock,
7936 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_SMALL), "GtkWindow"));
7937 l = g_list_append(l,
7938 gtk_widget_render_icon(w, stock,
7939 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_MEDIUM), "GtkWindow"));
7940 l = g_list_append(l,
7941 gtk_widget_render_icon(w, stock,
7942 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_LARGE), "GtkWindow"));
7943 return l;
7946 static void
7947 create_icon_lists(GtkWidget *w)
7949 available_list = make_status_icon_list(PIDGIN_STOCK_STATUS_AVAILABLE, w);
7950 busy_list = make_status_icon_list(PIDGIN_STOCK_STATUS_BUSY, w);
7951 xa_list = make_status_icon_list(PIDGIN_STOCK_STATUS_XA, w);
7952 offline_list = make_status_icon_list(PIDGIN_STOCK_STATUS_OFFLINE, w);
7953 away_list = make_status_icon_list(PIDGIN_STOCK_STATUS_AWAY, w);
7954 protocol_lists = g_hash_table_new(g_str_hash, g_str_equal);
7957 static void
7958 plugin_changed_cb(PurplePlugin *p, gpointer data)
7960 regenerate_plugins_items(data);
7963 static gboolean gtk_conv_configure_cb(GtkWidget *w, GdkEventConfigure *event, gpointer data) {
7964 int x, y;
7966 if (gtk_widget_get_visible(w))
7967 gtk_window_get_position(GTK_WINDOW(w), &x, &y);
7968 else
7969 return FALSE; /* carry on normally */
7971 /* Workaround for GTK+ bug # 169811 - "configure_event" is fired
7972 * when the window is being maximized */
7973 if (gdk_window_get_state(gtk_widget_get_window(w)) & GDK_WINDOW_STATE_MAXIMIZED)
7974 return FALSE;
7976 /* don't save off-screen positioning */
7977 if (x + event->width < 0 ||
7978 y + event->height < 0 ||
7979 x > gdk_screen_width() ||
7980 y > gdk_screen_height())
7981 return FALSE; /* carry on normally */
7983 /* store the position */
7984 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/x", x);
7985 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/y", y);
7986 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/width", event->width);
7987 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/height", event->height);
7989 /* continue to handle event normally */
7990 return FALSE;
7994 static void
7995 pidgin_conv_set_position_size(PidginConvWindow *win, int conv_x, int conv_y,
7996 int conv_width, int conv_height)
7998 /* if the window exists, is hidden, we're saving positions, and the
7999 * position is sane... */
8000 if (win && win->window &&
8001 !gtk_widget_get_visible(win->window) && conv_width != 0) {
8003 #ifdef _WIN32 /* only override window manager placement on Windows */
8004 /* ...check position is on screen... */
8005 if (conv_x >= gdk_screen_width())
8006 conv_x = gdk_screen_width() - 100;
8007 else if (conv_x + conv_width < 0)
8008 conv_x = 100;
8010 if (conv_y >= gdk_screen_height())
8011 conv_y = gdk_screen_height() - 100;
8012 else if (conv_y + conv_height < 0)
8013 conv_y = 100;
8015 /* ...and move it back. */
8016 gtk_window_move(GTK_WINDOW(win->window), conv_x, conv_y);
8017 #endif
8018 gtk_window_resize(GTK_WINDOW(win->window), conv_width, conv_height);
8022 static void
8023 pidgin_conv_restore_position(PidginConvWindow *win) {
8024 pidgin_conv_set_position_size(win,
8025 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/x"),
8026 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/y"),
8027 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/width"),
8028 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/height"));
8031 PidginConvWindow *
8032 pidgin_conv_window_new()
8034 PidginConvWindow *win;
8035 GtkPositionType pos;
8036 GtkWidget *testidea;
8037 GtkWidget *menubar;
8038 GtkWidget *menu;
8039 GtkWidget *item;
8040 GdkModifierType state;
8042 win = g_malloc0(sizeof(PidginConvWindow));
8043 win->menu = g_malloc0(sizeof(PidginConvWindowMenu));
8045 window_list = g_list_append(window_list, win);
8047 /* Create the window. */
8048 win->window = pidgin_create_window(NULL, 0, "conversation", TRUE);
8049 /*_pidgin_widget_set_accessible_name(win->window, "Conversations");*/
8050 if (!gtk_get_current_event_state(&state))
8051 gtk_window_set_focus_on_map(GTK_WINDOW(win->window), FALSE);
8053 /* Etan: I really think this entire function call should happen only
8054 * when we are on Windows but I was informed that back before we used
8055 * to save the window position we stored the window size, so I'm
8056 * leaving it for now. */
8057 #if TRUE || defined(_WIN32)
8058 pidgin_conv_restore_position(win);
8059 #endif
8061 if (available_list == NULL) {
8062 create_icon_lists(win->window);
8065 g_signal_connect(G_OBJECT(win->window), "delete_event",
8066 G_CALLBACK(close_win_cb), win);
8067 g_signal_connect(G_OBJECT(win->window), "focus_in_event",
8068 G_CALLBACK(focus_win_cb), win);
8070 /* Intercept keystrokes from the menu items */
8071 g_signal_connect(G_OBJECT(win->window), "key_press_event",
8072 G_CALLBACK(window_keypress_cb), win);
8075 /* Create the notebook. */
8076 win->notebook = gtk_notebook_new();
8078 pos = purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side");
8080 gtk_notebook_set_tab_pos(GTK_NOTEBOOK(win->notebook), pos);
8081 gtk_notebook_set_scrollable(GTK_NOTEBOOK(win->notebook), TRUE);
8082 gtk_notebook_set_show_tabs(GTK_NOTEBOOK(win->notebook), FALSE);
8083 gtk_notebook_set_show_border(GTK_NOTEBOOK(win->notebook), TRUE);
8085 menu = win->notebook_menu = gtk_menu_new();
8087 pidgin_separator(GTK_WIDGET(menu));
8089 item = gtk_menu_item_new_with_label(_("Close other tabs"));
8090 gtk_widget_show(item);
8091 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
8092 g_signal_connect(G_OBJECT(item), "activate",
8093 G_CALLBACK(close_others_cb), win);
8095 item = gtk_menu_item_new_with_label(_("Close all tabs"));
8096 gtk_widget_show(item);
8097 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
8098 g_signal_connect(G_OBJECT(item), "activate",
8099 G_CALLBACK(close_window), win);
8101 pidgin_separator(menu);
8103 item = gtk_menu_item_new_with_label(_("Detach this tab"));
8104 gtk_widget_show(item);
8105 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
8106 g_signal_connect(G_OBJECT(item), "activate",
8107 G_CALLBACK(detach_tab_cb), win);
8109 item = gtk_menu_item_new_with_label(_("Close this tab"));
8110 gtk_widget_show(item);
8111 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
8112 g_signal_connect(G_OBJECT(item), "activate",
8113 G_CALLBACK(close_tab_cb), win);
8115 g_signal_connect(G_OBJECT(win->notebook), "page-added",
8116 G_CALLBACK(notebook_add_tab_to_menu_cb), win);
8117 g_signal_connect(G_OBJECT(win->notebook), "page-removed",
8118 G_CALLBACK(notebook_remove_tab_from_menu_cb), win);
8119 g_signal_connect(G_OBJECT(win->notebook), "page-reordered",
8120 G_CALLBACK(notebook_reorder_tab_in_menu_cb), win);
8122 g_signal_connect(G_OBJECT(win->notebook), "button-press-event",
8123 G_CALLBACK(notebook_right_click_menu_cb), win);
8125 gtk_widget_show(win->notebook);
8127 g_signal_connect(G_OBJECT(win->notebook), "switch_page",
8128 G_CALLBACK(before_switch_conv_cb), win);
8129 g_signal_connect_after(G_OBJECT(win->notebook), "switch_page",
8130 G_CALLBACK(switch_conv_cb), win);
8132 /* Setup the tab drag and drop signals. */
8133 gtk_widget_add_events(win->notebook,
8134 GDK_BUTTON1_MOTION_MASK | GDK_LEAVE_NOTIFY_MASK);
8135 g_signal_connect(G_OBJECT(win->notebook), "button_press_event",
8136 G_CALLBACK(notebook_press_cb), win);
8137 g_signal_connect(G_OBJECT(win->notebook), "button_release_event",
8138 G_CALLBACK(notebook_release_cb), win);
8140 testidea = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
8142 /* Setup the menubar. */
8143 menubar = setup_menubar(win);
8144 gtk_box_pack_start(GTK_BOX(testidea), menubar, FALSE, TRUE, 0);
8146 gtk_box_pack_start(GTK_BOX(testidea), win->notebook, TRUE, TRUE, 0);
8148 gtk_container_add(GTK_CONTAINER(win->window), testidea);
8150 gtk_widget_show(testidea);
8152 /* Update the plugin actions when plugins are (un)loaded */
8153 purple_signal_connect(purple_plugins_get_handle(), "plugin-load",
8154 win, PURPLE_CALLBACK(plugin_changed_cb), win);
8155 purple_signal_connect(purple_plugins_get_handle(), "plugin-unload",
8156 win, PURPLE_CALLBACK(plugin_changed_cb), win);
8159 #ifdef _WIN32
8160 g_signal_connect(G_OBJECT(win->window), "show",
8161 G_CALLBACK(winpidgin_ensure_onscreen), win->window);
8163 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/win32/minimize_new_convs")
8164 && !gtk_get_current_event_state(&state))
8165 gtk_window_iconify(GTK_WINDOW(win->window));
8166 #endif
8168 purple_signal_emit(pidgin_conversations_get_handle(),
8169 "conversation-window-created", win);
8171 /* Fix colours */
8172 pidgin_conversations_set_tab_colors();
8174 return win;
8177 void
8178 pidgin_conv_window_destroy(PidginConvWindow *win)
8180 if (win->gtkconvs) {
8181 GList *iter = win->gtkconvs;
8182 while (iter)
8184 PidginConversation *gtkconv = iter->data;
8185 iter = iter->next;
8186 close_conv_cb(NULL, gtkconv);
8188 return;
8191 purple_prefs_disconnect_by_handle(win);
8192 window_list = g_list_remove(window_list, win);
8194 gtk_widget_destroy(win->notebook_menu);
8195 gtk_widget_destroy(win->window);
8197 g_object_unref(G_OBJECT(win->menu->ui));
8199 purple_notify_close_with_handle(win);
8200 purple_signals_disconnect_by_handle(win);
8202 g_free(win->menu);
8203 g_free(win);
8206 void
8207 pidgin_conv_window_show(PidginConvWindow *win)
8209 gtk_widget_show(win->window);
8212 void
8213 pidgin_conv_window_hide(PidginConvWindow *win)
8215 gtk_widget_hide(win->window);
8218 void
8219 pidgin_conv_window_raise(PidginConvWindow *win)
8221 gdk_window_raise(GDK_WINDOW(gtk_widget_get_window(win->window)));
8224 void
8225 pidgin_conv_window_switch_gtkconv(PidginConvWindow *win, PidginConversation *gtkconv)
8227 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook),
8228 gtk_notebook_page_num(GTK_NOTEBOOK(win->notebook),
8229 gtkconv->tab_cont));
8232 static gboolean
8233 gtkconv_tab_set_tip(GtkWidget *widget, GdkEventCrossing *event, PidginConversation *gtkconv)
8235 /* PANGO_VERSION_CHECK macro was introduced in 1.15. So we need this double check. */
8236 #ifndef PANGO_VERSION_CHECK
8237 #define pango_layout_is_ellipsized(l) TRUE
8238 #elif !PANGO_VERSION_CHECK(1,16,0)
8239 #define pango_layout_is_ellipsized(l) TRUE
8240 #endif
8241 PangoLayout *layout;
8243 layout = gtk_label_get_layout(GTK_LABEL(gtkconv->tab_label));
8244 if (pango_layout_is_ellipsized(layout))
8245 gtk_widget_set_tooltip_text(widget, gtk_label_get_text(GTK_LABEL(gtkconv->tab_label)));
8246 else
8247 gtk_widget_set_tooltip_text(widget, NULL);
8249 return FALSE;
8252 static void
8253 set_default_tab_colors(GtkWidget *widget)
8255 GString *str;
8256 GtkCssProvider *provider;
8257 GError *error = NULL;
8258 int iter;
8260 struct {
8261 const char *labelname;
8262 const char *color;
8263 } styles[] = {
8264 {"tab-label-typing", "#4e9a06"},
8265 {"tab-label-typed", "#c4a000"},
8266 {"tab-label-attention", "#006aff"},
8267 {"tab-label-unreadchat", "#cc0000"},
8268 {"tab-label-event", "#888a85"},
8269 {NULL, NULL}
8272 str = g_string_new(NULL);
8274 for (iter = 0; styles[iter].labelname; iter++) {
8275 g_string_append_printf(str,
8276 "#%s {\n"
8277 " color: %s;\n"
8278 "}\n",
8279 styles[iter].labelname,
8280 styles[iter].color);
8283 provider = gtk_css_provider_new();
8285 gtk_css_provider_load_from_data(provider, str->str, str->len, &error);
8287 gtk_style_context_add_provider(gtk_widget_get_style_context(widget),
8288 GTK_STYLE_PROVIDER(provider),
8289 GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
8291 if (error)
8292 g_error_free(error);
8293 g_string_free(str, TRUE);
8296 void
8297 pidgin_conv_window_add_gtkconv(PidginConvWindow *win, PidginConversation *gtkconv)
8299 PurpleConversation *conv = gtkconv->active_conv;
8300 PidginConversation *focus_gtkconv;
8301 GtkWidget *tab_cont = gtkconv->tab_cont;
8302 const gchar *tmp_lab;
8304 win->gtkconvs = g_list_append(win->gtkconvs, gtkconv);
8305 gtkconv->win = win;
8307 if (win->gtkconvs && win->gtkconvs->next && win->gtkconvs->next->next == NULL)
8308 pidgin_conv_tab_pack(win, ((PidginConversation*)win->gtkconvs->data));
8311 /* Close button. */
8312 gtkconv->close = pidgin_create_small_button(gtk_label_new("×"));
8313 gtk_widget_set_tooltip_text(gtkconv->close, _("Close conversation"));
8315 g_signal_connect(gtkconv->close, "clicked", G_CALLBACK (close_conv_cb), gtkconv);
8317 /* Status icon. */
8318 gtkconv->icon = gtk_image_new();
8319 gtkconv->menu_icon = gtk_image_new();
8320 g_object_set(G_OBJECT(gtkconv->icon),
8321 "icon-size", gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_MICROSCOPIC),
8322 NULL);
8323 g_object_set(G_OBJECT(gtkconv->menu_icon),
8324 "icon-size", gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_MICROSCOPIC),
8325 NULL);
8326 gtk_widget_show(gtkconv->icon);
8327 update_tab_icon(conv);
8329 /* Tab label. */
8330 gtkconv->tab_label = gtk_label_new(tmp_lab = purple_conversation_get_title(conv));
8331 set_default_tab_colors(gtkconv->tab_label);
8332 gtk_widget_set_name(gtkconv->tab_label, "tab-label");
8334 gtkconv->menu_tabby = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PIDGIN_HIG_BOX_SPACE);
8335 gtkconv->menu_label = gtk_label_new(tmp_lab);
8336 gtk_box_pack_start(GTK_BOX(gtkconv->menu_tabby), gtkconv->menu_icon, FALSE, FALSE, 0);
8338 gtk_widget_show_all(gtkconv->menu_icon);
8340 gtk_box_pack_start(GTK_BOX(gtkconv->menu_tabby), gtkconv->menu_label, TRUE, TRUE, 0);
8341 gtk_widget_show(gtkconv->menu_label);
8342 gtk_label_set_xalign(GTK_LABEL(gtkconv->menu_label), 0);
8343 gtk_label_set_yalign(GTK_LABEL(gtkconv->menu_label), 0);
8345 gtk_widget_show(gtkconv->menu_tabby);
8347 if (PURPLE_IS_IM_CONVERSATION(conv))
8348 pidgin_conv_update_buddy_icon(PURPLE_IM_CONVERSATION(conv));
8350 /* Build and set conversations tab */
8351 pidgin_conv_tab_pack(win, gtkconv);
8353 gtk_notebook_set_menu_label(GTK_NOTEBOOK(win->notebook), tab_cont, gtkconv->menu_tabby);
8355 gtk_widget_show(tab_cont);
8357 if (pidgin_conv_window_get_gtkconv_count(win) == 1) {
8358 /* Er, bug in notebooks? Switch to the page manually. */
8359 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), 0);
8360 } else {
8361 gtk_notebook_set_show_tabs(GTK_NOTEBOOK(win->notebook), TRUE);
8364 focus_gtkconv = g_list_nth_data(pidgin_conv_window_get_gtkconvs(win),
8365 gtk_notebook_get_current_page(GTK_NOTEBOOK(win->notebook)));
8366 gtk_widget_grab_focus(focus_gtkconv->editor);
8368 if (pidgin_conv_window_get_gtkconv_count(win) == 1)
8369 update_send_to_selection(win);
8372 static void
8373 pidgin_conv_tab_pack(PidginConvWindow *win, PidginConversation *gtkconv)
8375 gboolean tabs_side = FALSE;
8376 gint angle = 0;
8377 GtkWidget *first, *third, *ebox, *parent;
8379 if (purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") == GTK_POS_LEFT ||
8380 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") == GTK_POS_RIGHT)
8381 tabs_side = TRUE;
8382 else if (purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") == (GTK_POS_LEFT|8))
8383 angle = 90;
8384 else if (purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") == (GTK_POS_RIGHT|8))
8385 angle = 270;
8387 if (!angle) {
8388 g_object_set(G_OBJECT(gtkconv->tab_label), "ellipsize", PANGO_ELLIPSIZE_END, NULL);
8389 gtk_label_set_width_chars(GTK_LABEL(gtkconv->tab_label), 4);
8390 } else {
8391 g_object_set(G_OBJECT(gtkconv->tab_label), "ellipsize", PANGO_ELLIPSIZE_NONE, NULL);
8392 gtk_label_set_width_chars(GTK_LABEL(gtkconv->tab_label), -1);
8395 if (tabs_side) {
8396 gtk_label_set_width_chars(
8397 GTK_LABEL(gtkconv->tab_label),
8398 MIN(g_utf8_strlen(gtk_label_get_text(GTK_LABEL(gtkconv->tab_label)), -1), 12)
8402 gtk_label_set_angle(GTK_LABEL(gtkconv->tab_label), angle);
8404 if (angle)
8405 gtkconv->tabby = gtk_box_new(GTK_ORIENTATION_VERTICAL, PIDGIN_HIG_BOX_SPACE);
8406 else
8407 gtkconv->tabby = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PIDGIN_HIG_BOX_SPACE);
8408 gtk_widget_set_name(gtkconv->tabby, "tab-container");
8410 /* select the correct ordering for verticle tabs */
8411 if (angle == 90) {
8412 first = gtkconv->close;
8413 third = gtkconv->icon;
8414 } else {
8415 first = gtkconv->icon;
8416 third = gtkconv->close;
8419 ebox = gtk_event_box_new();
8420 gtk_event_box_set_visible_window(GTK_EVENT_BOX(ebox), FALSE);
8421 gtk_container_add(GTK_CONTAINER(ebox), gtkconv->tabby);
8422 g_signal_connect(G_OBJECT(ebox), "enter-notify-event",
8423 G_CALLBACK(gtkconv_tab_set_tip), gtkconv);
8425 parent = gtk_widget_get_parent(gtkconv->tab_label);
8426 if (parent != NULL) {
8427 /* reparent old widgets on preference changes */
8428 g_object_ref(first);
8429 g_object_ref(gtkconv->tab_label);
8430 g_object_ref(third);
8431 gtk_container_remove(GTK_CONTAINER(parent), first);
8432 gtk_container_remove(GTK_CONTAINER(parent), gtkconv->tab_label);
8433 gtk_container_remove(GTK_CONTAINER(parent), third);
8436 gtk_box_pack_start(GTK_BOX(gtkconv->tabby), first, FALSE, FALSE, 0);
8437 gtk_box_pack_start(GTK_BOX(gtkconv->tabby), gtkconv->tab_label, TRUE, TRUE, 0);
8438 gtk_box_pack_start(GTK_BOX(gtkconv->tabby), third, FALSE, FALSE, 0);
8440 if (parent == NULL) {
8441 /* Add this pane to the conversation's notebook. */
8442 gtk_notebook_append_page(GTK_NOTEBOOK(win->notebook), gtkconv->tab_cont, ebox);
8443 } else {
8444 /* reparent old widgets on preference changes */
8445 g_object_unref(first);
8446 g_object_unref(gtkconv->tab_label);
8447 g_object_unref(third);
8449 /* Reset the tabs label to the new version */
8450 gtk_notebook_set_tab_label(GTK_NOTEBOOK(win->notebook), gtkconv->tab_cont, ebox);
8453 gtk_container_child_set(GTK_CONTAINER(win->notebook), gtkconv->tab_cont,
8454 "tab-expand", !tabs_side && !angle,
8455 "tab-fill", TRUE, NULL);
8457 if (pidgin_conv_window_get_gtkconv_count(win) == 1)
8458 gtk_notebook_set_show_tabs(GTK_NOTEBOOK(win->notebook),
8459 purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/tabs") &&
8460 (!purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons") ||
8461 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") != GTK_POS_TOP));
8463 /* show the widgets */
8464 /* gtk_widget_show(gtkconv->icon); */
8465 gtk_widget_show(gtkconv->tab_label);
8466 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/close_on_tabs"))
8467 gtk_widget_show(gtkconv->close);
8468 gtk_widget_show(gtkconv->tabby);
8469 gtk_widget_show(ebox);
8472 void
8473 pidgin_conv_window_remove_gtkconv(PidginConvWindow *win, PidginConversation *gtkconv)
8475 unsigned int index;
8477 index = gtk_notebook_page_num(GTK_NOTEBOOK(win->notebook), gtkconv->tab_cont);
8479 g_object_ref_sink(G_OBJECT(gtkconv->tab_cont));
8481 gtk_notebook_remove_page(GTK_NOTEBOOK(win->notebook), index);
8483 win->gtkconvs = g_list_remove(win->gtkconvs, gtkconv);
8485 g_signal_handlers_disconnect_matched(win->window, G_SIGNAL_MATCH_DATA,
8486 0, 0, NULL, NULL, gtkconv);
8488 if (win->gtkconvs && win->gtkconvs->next == NULL)
8489 pidgin_conv_tab_pack(win, win->gtkconvs->data);
8491 if (!win->gtkconvs && win != hidden_convwin)
8492 pidgin_conv_window_destroy(win);
8495 PidginConversation *
8496 pidgin_conv_window_get_gtkconv_at_index(const PidginConvWindow *win, int index)
8498 GtkWidget *tab_cont;
8500 if (index == -1)
8501 index = 0;
8502 tab_cont = gtk_notebook_get_nth_page(GTK_NOTEBOOK(win->notebook), index);
8503 return tab_cont ? g_object_get_data(G_OBJECT(tab_cont), "PidginConversation") : NULL;
8506 PidginConversation *
8507 pidgin_conv_window_get_active_gtkconv(const PidginConvWindow *win)
8509 int index;
8510 GtkWidget *tab_cont;
8512 index = gtk_notebook_get_current_page(GTK_NOTEBOOK(win->notebook));
8513 if (index == -1)
8514 index = 0;
8515 tab_cont = gtk_notebook_get_nth_page(GTK_NOTEBOOK(win->notebook), index);
8516 if (!tab_cont)
8517 return NULL;
8518 return g_object_get_data(G_OBJECT(tab_cont), "PidginConversation");
8522 PurpleConversation *
8523 pidgin_conv_window_get_active_conversation(const PidginConvWindow *win)
8525 PidginConversation *gtkconv;
8527 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
8528 return gtkconv ? gtkconv->active_conv : NULL;
8531 gboolean
8532 pidgin_conv_window_is_active_conversation(const PurpleConversation *conv)
8534 return conv == pidgin_conv_window_get_active_conversation(PIDGIN_CONVERSATION(conv)->win);
8537 gboolean
8538 pidgin_conv_window_has_focus(PidginConvWindow *win)
8540 gboolean has_focus = FALSE;
8542 g_object_get(G_OBJECT(win->window), "has-toplevel-focus", &has_focus, NULL);
8544 return has_focus;
8547 PidginConvWindow *
8548 pidgin_conv_window_get_at_event(GdkEvent *event)
8550 PidginConvWindow *win;
8551 GdkWindow *gdkwin;
8552 GList *l;
8553 int x, y;
8555 gdkwin = gdk_device_get_window_at_position(gdk_event_get_device(event),
8556 &x, &y);
8558 if (gdkwin)
8559 gdkwin = gdk_window_get_toplevel(gdkwin);
8561 for (l = pidgin_conv_windows_get_list(); l != NULL; l = l->next) {
8562 win = l->data;
8564 if (gdkwin == gtk_widget_get_window(win->window))
8565 return win;
8568 return NULL;
8571 GList *
8572 pidgin_conv_window_get_gtkconvs(PidginConvWindow *win)
8574 return win->gtkconvs;
8577 guint
8578 pidgin_conv_window_get_gtkconv_count(PidginConvWindow *win)
8580 return g_list_length(win->gtkconvs);
8583 PidginConvWindow *
8584 pidgin_conv_window_first_im(void)
8586 GList *wins, *convs;
8587 PidginConvWindow *win;
8588 PidginConversation *conv;
8590 for (wins = pidgin_conv_windows_get_list(); wins != NULL; wins = wins->next) {
8591 win = wins->data;
8593 for (convs = win->gtkconvs;
8594 convs != NULL;
8595 convs = convs->next) {
8597 conv = convs->data;
8599 if (PURPLE_IS_IM_CONVERSATION(conv->active_conv))
8600 return win;
8604 return NULL;
8607 PidginConvWindow *
8608 pidgin_conv_window_last_im(void)
8610 GList *wins, *convs;
8611 PidginConvWindow *win;
8612 PidginConversation *conv;
8614 for (wins = g_list_last(pidgin_conv_windows_get_list());
8615 wins != NULL;
8616 wins = wins->prev) {
8618 win = wins->data;
8620 for (convs = win->gtkconvs;
8621 convs != NULL;
8622 convs = convs->next) {
8624 conv = convs->data;
8626 if (PURPLE_IS_IM_CONVERSATION(conv->active_conv))
8627 return win;
8631 return NULL;
8634 PidginConvWindow *
8635 pidgin_conv_window_first_chat(void)
8637 GList *wins, *convs;
8638 PidginConvWindow *win;
8639 PidginConversation *conv;
8641 for (wins = pidgin_conv_windows_get_list(); wins != NULL; wins = wins->next) {
8642 win = wins->data;
8644 for (convs = win->gtkconvs;
8645 convs != NULL;
8646 convs = convs->next) {
8648 conv = convs->data;
8650 if (PURPLE_IS_CHAT_CONVERSATION(conv->active_conv))
8651 return win;
8655 return NULL;
8658 PidginConvWindow *
8659 pidgin_conv_window_last_chat(void)
8661 GList *wins, *convs;
8662 PidginConvWindow *win;
8663 PidginConversation *conv;
8665 for (wins = g_list_last(pidgin_conv_windows_get_list());
8666 wins != NULL;
8667 wins = wins->prev) {
8669 win = wins->data;
8671 for (convs = win->gtkconvs;
8672 convs != NULL;
8673 convs = convs->next) {
8675 conv = convs->data;
8677 if (PURPLE_IS_CHAT_CONVERSATION(conv->active_conv))
8678 return win;
8682 return NULL;
8686 /**************************************************************************
8687 * Conversation placement functions
8688 **************************************************************************/
8689 typedef struct
8691 char *id;
8692 char *name;
8693 PidginConvPlacementFunc fnc;
8695 } ConvPlacementData;
8697 static GList *conv_placement_fncs = NULL;
8698 static PidginConvPlacementFunc place_conv = NULL;
8700 /* This one places conversations in the last made window. */
8701 static void
8702 conv_placement_last_created_win(PidginConversation *conv)
8704 PidginConvWindow *win;
8706 GList *l = g_list_last(pidgin_conv_windows_get_list());
8707 win = l ? l->data : NULL;;
8709 if (win == NULL) {
8710 win = pidgin_conv_window_new();
8712 g_signal_connect(G_OBJECT(win->window), "configure_event",
8713 G_CALLBACK(gtk_conv_configure_cb), NULL);
8715 pidgin_conv_window_add_gtkconv(win, conv);
8716 pidgin_conv_window_show(win);
8717 } else {
8718 pidgin_conv_window_add_gtkconv(win, conv);
8722 /* This one places conversations in the last made window of the same type. */
8723 static gboolean
8724 conv_placement_last_created_win_type_configured_cb(GtkWidget *w,
8725 GdkEventConfigure *event, PidginConversation *conv)
8727 int x, y;
8728 GList *all;
8730 if (gtk_widget_get_visible(w))
8731 gtk_window_get_position(GTK_WINDOW(w), &x, &y);
8732 else
8733 return FALSE; /* carry on normally */
8735 /* Workaround for GTK+ bug # 169811 - "configure_event" is fired
8736 * when the window is being maximized */
8737 if (gdk_window_get_state(gtk_widget_get_window(w)) & GDK_WINDOW_STATE_MAXIMIZED)
8738 return FALSE;
8740 /* don't save off-screen positioning */
8741 if (x + event->width < 0 ||
8742 y + event->height < 0 ||
8743 x > gdk_screen_width() ||
8744 y > gdk_screen_height())
8745 return FALSE; /* carry on normally */
8747 for (all = conv->convs; all != NULL; all = all->next) {
8748 if (PURPLE_IS_IM_CONVERSATION(conv->active_conv) != PURPLE_IS_IM_CONVERSATION(all->data)) {
8749 /* this window has different types of conversation, don't save */
8750 return FALSE;
8754 if (PURPLE_IS_IM_CONVERSATION(conv->active_conv)) {
8755 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/x", x);
8756 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/y", y);
8757 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/width", event->width);
8758 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/height", event->height);
8759 } else if (PURPLE_IS_CHAT_CONVERSATION(conv->active_conv)) {
8760 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/chat/x", x);
8761 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/chat/y", y);
8762 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/chat/width", event->width);
8763 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/chat/height", event->height);
8766 return FALSE;
8769 static void
8770 conv_placement_last_created_win_type(PidginConversation *conv)
8772 PidginConvWindow *win;
8774 if (PURPLE_IS_IM_CONVERSATION(conv->active_conv))
8775 win = pidgin_conv_window_last_im();
8776 else
8777 win = pidgin_conv_window_last_chat();
8779 if (win == NULL) {
8780 win = pidgin_conv_window_new();
8782 if (PURPLE_IS_IM_CONVERSATION(conv->active_conv) ||
8783 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/width") == 0) {
8784 pidgin_conv_set_position_size(win,
8785 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/x"),
8786 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/y"),
8787 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/width"),
8788 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/height"));
8789 } else if (PURPLE_IS_CHAT_CONVERSATION(conv->active_conv)) {
8790 pidgin_conv_set_position_size(win,
8791 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/x"),
8792 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/y"),
8793 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/width"),
8794 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/height"));
8797 pidgin_conv_window_add_gtkconv(win, conv);
8798 pidgin_conv_window_show(win);
8800 g_signal_connect(G_OBJECT(win->window), "configure_event",
8801 G_CALLBACK(conv_placement_last_created_win_type_configured_cb), conv);
8802 } else
8803 pidgin_conv_window_add_gtkconv(win, conv);
8806 /* This one places each conversation in its own window. */
8807 static void
8808 conv_placement_new_window(PidginConversation *conv)
8810 PidginConvWindow *win;
8812 win = pidgin_conv_window_new();
8814 g_signal_connect(G_OBJECT(win->window), "configure_event",
8815 G_CALLBACK(gtk_conv_configure_cb), NULL);
8817 pidgin_conv_window_add_gtkconv(win, conv);
8819 pidgin_conv_window_show(win);
8822 static PurpleGroup *
8823 conv_get_group(PidginConversation *conv)
8825 PurpleGroup *group = NULL;
8827 if (PURPLE_IS_IM_CONVERSATION(conv->active_conv)) {
8828 PurpleBuddy *buddy;
8830 buddy = purple_blist_find_buddy(purple_conversation_get_account(conv->active_conv),
8831 purple_conversation_get_name(conv->active_conv));
8833 if (buddy != NULL)
8834 group = purple_buddy_get_group(buddy);
8836 } else if (PURPLE_IS_CHAT_CONVERSATION(conv->active_conv)) {
8837 PurpleChat *chat;
8839 chat = purple_blist_find_chat(purple_conversation_get_account(conv->active_conv),
8840 purple_conversation_get_name(conv->active_conv));
8842 if (chat != NULL)
8843 group = purple_chat_get_group(chat);
8846 return group;
8850 * This groups things by, well, group. Buddies from groups will always be
8851 * grouped together, and a buddy from a group not belonging to any currently
8852 * open windows will get a new window.
8854 static void
8855 conv_placement_by_group(PidginConversation *conv)
8857 PurpleGroup *group = NULL;
8858 GList *wl, *cl;
8860 group = conv_get_group(conv);
8862 /* Go through the list of IMs and find one with this group. */
8863 for (wl = pidgin_conv_windows_get_list(); wl != NULL; wl = wl->next) {
8864 PidginConvWindow *win2;
8865 PidginConversation *conv2;
8866 PurpleGroup *group2 = NULL;
8868 win2 = wl->data;
8870 for (cl = win2->gtkconvs;
8871 cl != NULL;
8872 cl = cl->next) {
8873 conv2 = cl->data;
8875 group2 = conv_get_group(conv2);
8877 if (group == group2) {
8878 pidgin_conv_window_add_gtkconv(win2, conv);
8880 return;
8885 /* Make a new window. */
8886 conv_placement_new_window(conv);
8889 /* This groups things by account. Otherwise, the same semantics as above */
8890 static void
8891 conv_placement_by_account(PidginConversation *conv)
8893 GList *wins, *convs;
8894 PurpleAccount *account;
8896 account = purple_conversation_get_account(conv->active_conv);
8898 /* Go through the list of IMs and find one with this group. */
8899 for (wins = pidgin_conv_windows_get_list(); wins != NULL; wins = wins->next) {
8900 PidginConvWindow *win2;
8901 PidginConversation *conv2;
8903 win2 = wins->data;
8905 for (convs = win2->gtkconvs;
8906 convs != NULL;
8907 convs = convs->next) {
8908 conv2 = convs->data;
8910 if (account == purple_conversation_get_account(conv2->active_conv)) {
8911 pidgin_conv_window_add_gtkconv(win2, conv);
8912 return;
8917 /* Make a new window. */
8918 conv_placement_new_window(conv);
8921 static ConvPlacementData *
8922 get_conv_placement_data(const char *id)
8924 ConvPlacementData *data = NULL;
8925 GList *n;
8927 for (n = conv_placement_fncs; n; n = n->next) {
8928 data = n->data;
8929 if (purple_strequal(data->id, id))
8930 return data;
8933 return NULL;
8936 static void
8937 add_conv_placement_fnc(const char *id, const char *name,
8938 PidginConvPlacementFunc fnc)
8940 ConvPlacementData *data;
8942 data = g_new(ConvPlacementData, 1);
8944 data->id = g_strdup(id);
8945 data->name = g_strdup(name);
8946 data->fnc = fnc;
8948 conv_placement_fncs = g_list_append(conv_placement_fncs, data);
8951 static void
8952 ensure_default_funcs(void)
8954 if (conv_placement_fncs == NULL) {
8955 add_conv_placement_fnc("last", _("Last created window"),
8956 conv_placement_last_created_win);
8957 add_conv_placement_fnc("im_chat", _("Separate IM and Chat windows"),
8958 conv_placement_last_created_win_type);
8959 add_conv_placement_fnc("new", _("New window"),
8960 conv_placement_new_window);
8961 add_conv_placement_fnc("group", _("By group"),
8962 conv_placement_by_group);
8963 add_conv_placement_fnc("account", _("By account"),
8964 conv_placement_by_account);
8968 GList *
8969 pidgin_conv_placement_get_options(void)
8971 GList *n, *list = NULL;
8972 ConvPlacementData *data;
8974 ensure_default_funcs();
8976 for (n = conv_placement_fncs; n; n = n->next) {
8977 data = n->data;
8978 list = g_list_append(list, data->name);
8979 list = g_list_append(list, data->id);
8982 return list;
8986 void
8987 pidgin_conv_placement_add_fnc(const char *id, const char *name,
8988 PidginConvPlacementFunc fnc)
8990 g_return_if_fail(id != NULL);
8991 g_return_if_fail(name != NULL);
8992 g_return_if_fail(fnc != NULL);
8994 ensure_default_funcs();
8996 add_conv_placement_fnc(id, name, fnc);
8999 void
9000 pidgin_conv_placement_remove_fnc(const char *id)
9002 ConvPlacementData *data = get_conv_placement_data(id);
9004 if (data == NULL)
9005 return;
9007 conv_placement_fncs = g_list_remove(conv_placement_fncs, data);
9009 g_free(data->id);
9010 g_free(data->name);
9011 g_free(data);
9014 const char *
9015 pidgin_conv_placement_get_name(const char *id)
9017 ConvPlacementData *data;
9019 ensure_default_funcs();
9021 data = get_conv_placement_data(id);
9023 if (data == NULL)
9024 return NULL;
9026 return data->name;
9029 PidginConvPlacementFunc
9030 pidgin_conv_placement_get_fnc(const char *id)
9032 ConvPlacementData *data;
9034 ensure_default_funcs();
9036 data = get_conv_placement_data(id);
9038 if (data == NULL)
9039 return NULL;
9041 return data->fnc;
9044 void
9045 pidgin_conv_placement_set_current_func(PidginConvPlacementFunc func)
9047 g_return_if_fail(func != NULL);
9049 /* If tabs are enabled, set the function, otherwise, NULL it out. */
9050 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/tabs"))
9051 place_conv = func;
9052 else
9053 place_conv = NULL;
9056 PidginConvPlacementFunc
9057 pidgin_conv_placement_get_current_func(void)
9059 return place_conv;
9062 void
9063 pidgin_conv_placement_place(PidginConversation *gtkconv)
9065 if (place_conv)
9066 place_conv(gtkconv);
9067 else
9068 conv_placement_new_window(gtkconv);
9071 gboolean
9072 pidgin_conv_is_hidden(PidginConversation *gtkconv)
9074 g_return_val_if_fail(gtkconv != NULL, FALSE);
9076 return (gtkconv->win == hidden_convwin);
9080 gdouble luminance(GdkRGBA color)
9082 gdouble r, g, b;
9083 gdouble rr, gg, bb;
9084 gdouble cutoff = 0.03928, scale = 12.92;
9085 gdouble a = 0.055, d = 1.055, p = 2.2;
9087 rr = color.red;
9088 gg = color.green;
9089 bb = color.blue;
9091 r = (rr > cutoff) ? pow((rr+a)/d, p) : rr/scale;
9092 g = (gg > cutoff) ? pow((gg+a)/d, p) : gg/scale;
9093 b = (bb > cutoff) ? pow((bb+a)/d, p) : bb/scale;
9095 return (r*0.2126 + g*0.7152 + b*0.0722);
9098 /* Algorithm from https://www.w3.org/TR/2008/REC-WCAG20-20081211/relative-luminance.xml */
9099 static gboolean
9100 color_is_visible(GdkRGBA foreground, GdkRGBA background, gdouble min_contrast_ratio)
9102 gdouble lfg, lbg, lmin, lmax;
9103 gdouble luminosity_ratio;
9104 gdouble nr, dr;
9106 lfg = luminance(foreground);
9107 lbg = luminance(background);
9109 if (lfg > lbg)
9110 lmax = lfg, lmin = lbg;
9111 else
9112 lmax = lbg, lmin = lfg;
9114 nr = lmax + 0.05, dr = lmin - 0.05;
9115 if (dr < 0.005 && dr > -0.005)
9116 dr += 0.01;
9118 luminosity_ratio = nr/dr;
9119 if ( luminosity_ratio < 0)
9120 luminosity_ratio *= -1.0;
9121 return (luminosity_ratio > min_contrast_ratio);
9125 static GArray*
9126 generate_nick_colors(guint numcolors, GdkRGBA background)
9128 guint i = 0, j = 0;
9129 GArray *colors = g_array_new(FALSE, FALSE, sizeof(GdkRGBA));
9130 GdkRGBA nick_highlight;
9131 GdkRGBA send_color;
9132 time_t breakout_time;
9134 gdk_rgba_parse(&nick_highlight, DEFAULT_HIGHLIGHT_COLOR);
9135 gdk_rgba_parse(&send_color, DEFAULT_SEND_COLOR);
9137 pidgin_style_adjust_contrast(NULL, &nick_highlight);
9138 pidgin_style_adjust_contrast(NULL, &send_color);
9140 srand(background.red * 65535 + background.green * 65535 + background.blue * 65535 + 1);
9142 breakout_time = time(NULL) + 3;
9144 /* first we look through the list of "good" colors: colors that differ from every other color in the
9145 * list. only some of them will differ from the background color though. lets see if we can find
9146 * numcolors of them that do
9148 while (i < numcolors && j < PIDGIN_NUM_NICK_SEED_COLORS && time(NULL) < breakout_time)
9150 GdkRGBA color = nick_seed_colors[j];
9152 if (color_is_visible(color, background, MIN_LUMINANCE_CONTRAST_RATIO) &&
9153 color_is_visible(color, nick_highlight, MIN_LUMINANCE_CONTRAST_RATIO) &&
9154 color_is_visible(color, send_color, MIN_LUMINANCE_CONTRAST_RATIO))
9156 g_array_append_val(colors, color);
9157 i++;
9159 j++;
9162 /* we might not have found numcolors in the last loop. if we did, we'll never enter this one.
9163 * if we did not, lets just find some colors that don't conflict with the background. its
9164 * expensive to find colors that not only don't conflict with the background, but also do not
9165 * conflict with each other.
9167 while(i < numcolors && time(NULL) < breakout_time)
9169 GdkRGBA color = {g_random_double_range(0, 1), g_random_double_range(0, 1), g_random_double_range(0, 1), 1};
9171 if (color_is_visible(color, background, MIN_LUMINANCE_CONTRAST_RATIO) &&
9172 color_is_visible(color, nick_highlight, MIN_LUMINANCE_CONTRAST_RATIO) &&
9173 color_is_visible(color, send_color, MIN_LUMINANCE_CONTRAST_RATIO))
9175 g_array_append_val(colors, color);
9176 i++;
9180 if (i < numcolors) {
9181 purple_debug_warning("gtkconv", "Unable to generate enough random colors before timeout. %u colors found.\n", i);
9184 if( i == 0 ) {
9185 /* To remove errors caused by an empty array. */
9186 GdkRGBA color = {0.5, 0.5, 0.5, 1.0};
9187 g_array_append_val(colors, color);
9190 return colors;
9193 /**************************************************************************
9194 * PidginConvWindow GBoxed code
9195 **************************************************************************/
9196 static PidginConvWindow *
9197 pidgin_conv_window_ref(PidginConvWindow *win)
9199 g_return_val_if_fail(win != NULL, NULL);
9201 win->box_count++;
9203 return win;
9206 static void
9207 pidgin_conv_window_unref(PidginConvWindow *win)
9209 g_return_if_fail(win != NULL);
9210 g_return_if_fail(win->box_count >= 0);
9212 if (!win->box_count--)
9213 pidgin_conv_window_destroy(win);
9216 GType
9217 pidgin_conv_window_get_type(void)
9219 static GType type = 0;
9221 if (type == 0) {
9222 type = g_boxed_type_register_static("PidginConvWindow",
9223 (GBoxedCopyFunc)pidgin_conv_window_ref,
9224 (GBoxedFreeFunc)pidgin_conv_window_unref);
9227 return type;