Replace functions which called once with their bodies
[pidgin-git.git] / pidgin / gtkconv.c
blobc15098ee39b8963ab19ce37f4bae6106cd5c9063
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 #ifdef HAVE_X11
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 list = gtk_container_get_children(GTK_CONTAINER(menu));
2904 g_list_free_full(list, (GDestroyNotify)gtk_widget_destroy);
2906 if (!populate_menu_with_options(menu, gtkconv, FALSE))
2908 GtkWidget *item = gtk_menu_item_new_with_label(_("No actions available"));
2909 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
2910 gtk_widget_set_sensitive(item, FALSE);
2913 gtk_widget_show_all(menu);
2916 static void
2917 remove_from_list(GtkWidget *widget, PidginConvWindow *win)
2919 GList *list = g_object_get_data(G_OBJECT(win->window), "plugin-actions");
2920 list = g_list_remove(list, widget);
2921 g_object_set_data(G_OBJECT(win->window), "plugin-actions", list);
2924 static void
2925 regenerate_plugins_items(PidginConvWindow *win)
2927 GList *action_items;
2928 GtkWidget *menu;
2929 GList *list;
2930 PidginConversation *gtkconv;
2931 PurpleConversation *conv;
2932 GtkWidget *item;
2934 if (win->window == NULL || win == hidden_convwin)
2935 return;
2937 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
2938 if (gtkconv == NULL)
2939 return;
2941 conv = gtkconv->active_conv;
2942 action_items = g_object_get_data(G_OBJECT(win->window), "plugin-actions");
2944 /* Remove the old menuitems */
2945 while (action_items) {
2946 g_signal_handlers_disconnect_by_func(G_OBJECT(action_items->data),
2947 G_CALLBACK(remove_from_list), win);
2948 gtk_widget_destroy(action_items->data);
2949 action_items = g_list_delete_link(action_items, action_items);
2952 item = gtk_ui_manager_get_widget(win->menu->ui, "/Conversation/OptionsMenu");
2953 menu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(item));
2955 list = purple_conversation_get_extended_menu(conv);
2956 if (list) {
2957 action_items = g_list_prepend(NULL, (item = pidgin_separator(menu)));
2958 g_signal_connect(G_OBJECT(item), "destroy", G_CALLBACK(remove_from_list), win);
2961 for(; list; list = g_list_delete_link(list, list)) {
2962 PurpleActionMenu *act = (PurpleActionMenu *) list->data;
2963 item = pidgin_append_menu_action(menu, act, conv);
2964 action_items = g_list_prepend(action_items, item);
2965 gtk_widget_show_all(item);
2966 g_signal_connect(G_OBJECT(item), "destroy", G_CALLBACK(remove_from_list), win);
2968 g_object_set_data(G_OBJECT(win->window), "plugin-actions", action_items);
2971 static void menubar_activated(GtkWidget *item, gpointer data)
2973 PidginConvWindow *win = data;
2974 regenerate_media_items(win);
2975 regenerate_options_items(win);
2976 regenerate_plugins_items(win);
2977 regenerate_attention_items(win);
2979 /* The following are to make sure the 'More' submenu is not regenerated every time
2980 * the focus shifts from 'Conversations' to some other menu and back. */
2981 g_signal_handlers_block_by_func(G_OBJECT(item), G_CALLBACK(menubar_activated), data);
2982 g_signal_connect(G_OBJECT(win->menu->menubar), "deactivate", G_CALLBACK(focus_out_from_menubar), data);
2985 static void
2986 focus_out_from_menubar(GtkWidget *wid, PidginConvWindow *win)
2988 /* The menubar has been deactivated. Make sure the 'More' submenu is regenerated next time
2989 * the 'Conversation' menu pops up. */
2990 GtkWidget *menuitem = gtk_ui_manager_get_widget(win->menu->ui, "/Conversation/ConversationMenu");
2991 g_signal_handlers_unblock_by_func(G_OBJECT(menuitem), G_CALLBACK(menubar_activated), win);
2992 g_signal_handlers_disconnect_by_func(G_OBJECT(win->menu->menubar),
2993 G_CALLBACK(focus_out_from_menubar), win);
2996 static GtkWidget *
2997 setup_menubar(PidginConvWindow *win)
2999 GtkAccelGroup *accel_group;
3000 const char *method;
3001 GtkActionGroup *action_group;
3002 GError *error;
3003 GtkWidget *menuitem;
3005 action_group = gtk_action_group_new("ConversationActions");
3006 gtk_action_group_set_translation_domain(action_group, PACKAGE);
3007 gtk_action_group_add_actions(action_group,
3008 menu_entries,
3009 G_N_ELEMENTS(menu_entries),
3010 win);
3011 gtk_action_group_add_toggle_actions(action_group,
3012 menu_toggle_entries,
3013 G_N_ELEMENTS(menu_toggle_entries),
3014 win);
3016 win->menu->ui = gtk_ui_manager_new();
3017 gtk_ui_manager_insert_action_group(win->menu->ui, action_group, 0);
3019 accel_group = gtk_ui_manager_get_accel_group(win->menu->ui);
3020 gtk_window_add_accel_group(GTK_WINDOW(win->window), accel_group);
3021 g_signal_connect(G_OBJECT(accel_group), "accel-changed",
3022 G_CALLBACK(pidgin_save_accels_cb), NULL);
3024 error = NULL;
3025 if (!gtk_ui_manager_add_ui_from_string(win->menu->ui, conversation_menu, -1, &error))
3027 g_message("building menus failed: %s", error->message);
3028 g_error_free(error);
3029 exit(EXIT_FAILURE);
3032 win->menu->menubar =
3033 gtk_ui_manager_get_widget(win->menu->ui, "/Conversation");
3035 /* Make sure the 'Conversation ⇨ More' menuitems are regenerated whenever
3036 * the 'Conversation' menu pops up because the entries can change after the
3037 * conversation is created. */
3038 menuitem = gtk_ui_manager_get_widget(win->menu->ui, "/Conversation/ConversationMenu");
3039 g_signal_connect(G_OBJECT(menuitem), "activate", G_CALLBACK(menubar_activated), win);
3041 win->menu->view_log =
3042 gtk_ui_manager_get_action(win->menu->ui,
3043 "/Conversation/ConversationMenu/ViewLog");
3045 #ifdef USE_VV
3046 win->menu->audio_call =
3047 gtk_ui_manager_get_action(win->menu->ui,
3048 "/Conversation/ConversationMenu/MediaMenu/AudioCall");
3049 win->menu->video_call =
3050 gtk_ui_manager_get_action(win->menu->ui,
3051 "/Conversation/ConversationMenu/MediaMenu/VideoCall");
3052 win->menu->audio_video_call =
3053 gtk_ui_manager_get_action(win->menu->ui,
3054 "/Conversation/ConversationMenu/MediaMenu/AudioVideoCall");
3055 #endif
3057 /* --- */
3059 win->menu->send_file =
3060 gtk_ui_manager_get_action(win->menu->ui,
3061 "/Conversation/ConversationMenu/SendFile");
3063 win->menu->get_attention =
3064 gtk_ui_manager_get_action(win->menu->ui,
3065 "/Conversation/ConversationMenu/GetAttention");
3067 win->menu->add_pounce =
3068 gtk_ui_manager_get_action(win->menu->ui,
3069 "/Conversation/ConversationMenu/AddBuddyPounce");
3071 /* --- */
3073 win->menu->get_info =
3074 gtk_ui_manager_get_action(win->menu->ui,
3075 "/Conversation/ConversationMenu/GetInfo");
3077 win->menu->invite =
3078 gtk_ui_manager_get_action(win->menu->ui,
3079 "/Conversation/ConversationMenu/Invite");
3081 /* --- */
3083 win->menu->alias =
3084 gtk_ui_manager_get_action(win->menu->ui,
3085 "/Conversation/ConversationMenu/Alias");
3087 win->menu->block =
3088 gtk_ui_manager_get_action(win->menu->ui,
3089 "/Conversation/ConversationMenu/Block");
3091 win->menu->unblock =
3092 gtk_ui_manager_get_action(win->menu->ui,
3093 "/Conversation/ConversationMenu/Unblock");
3095 win->menu->add =
3096 gtk_ui_manager_get_action(win->menu->ui,
3097 "/Conversation/ConversationMenu/Add");
3099 win->menu->remove =
3100 gtk_ui_manager_get_action(win->menu->ui,
3101 "/Conversation/ConversationMenu/Remove");
3103 /* --- */
3105 win->menu->insert_link =
3106 gtk_ui_manager_get_action(win->menu->ui,
3107 "/Conversation/ConversationMenu/InsertLink");
3109 win->menu->insert_image =
3110 gtk_ui_manager_get_action(win->menu->ui,
3111 "/Conversation/ConversationMenu/InsertImage");
3113 /* --- */
3115 win->menu->logging =
3116 gtk_ui_manager_get_action(win->menu->ui,
3117 "/Conversation/OptionsMenu/EnableLogging");
3118 win->menu->sounds =
3119 gtk_ui_manager_get_action(win->menu->ui,
3120 "/Conversation/OptionsMenu/EnableSounds");
3121 method = purple_prefs_get_string(PIDGIN_PREFS_ROOT "/sound/method");
3122 if (purple_strequal(method, "none"))
3124 gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(win->menu->sounds),
3125 FALSE);
3126 gtk_action_set_sensitive(win->menu->sounds, FALSE);
3128 purple_prefs_connect_callback(win, PIDGIN_PREFS_ROOT "/sound/method",
3129 sound_method_pref_changed_cb, win);
3131 win->menu->show_formatting_toolbar =
3132 gtk_ui_manager_get_action(win->menu->ui,
3133 "/Conversation/OptionsMenu/ShowFormattingToolbars");
3135 win->menu->tray = pidgin_menu_tray_new();
3136 gtk_menu_shell_append(GTK_MENU_SHELL(win->menu->menubar),
3137 win->menu->tray);
3138 gtk_widget_show(win->menu->tray);
3140 gtk_widget_show(win->menu->menubar);
3142 return win->menu->menubar;
3146 /**************************************************************************
3147 * Utility functions
3148 **************************************************************************/
3150 static void
3151 got_typing_keypress(PidginConversation *gtkconv, gboolean first)
3153 PurpleConversation *conv = gtkconv->active_conv;
3154 PurpleIMConversation *im;
3157 * We know we got something, so we at least have to make sure we don't
3158 * send PURPLE_IM_TYPED any time soon.
3161 im = PURPLE_IM_CONVERSATION(conv);
3163 purple_im_conversation_stop_send_typed_timeout(im);
3164 purple_im_conversation_start_send_typed_timeout(im);
3166 /* Check if we need to send another PURPLE_IM_TYPING message */
3167 if (first || (purple_im_conversation_get_type_again(im) != 0 &&
3168 time(NULL) > purple_im_conversation_get_type_again(im)))
3170 unsigned int timeout;
3171 timeout = purple_serv_send_typing(purple_conversation_get_connection(conv),
3172 purple_conversation_get_name(conv),
3173 PURPLE_IM_TYPING);
3174 purple_im_conversation_set_type_again(im, timeout);
3178 #if 0
3179 static gboolean
3180 typing_animation(gpointer data) {
3181 PidginConversation *gtkconv = data;
3182 PidginConvWindow *gtkwin = gtkconv->win;
3183 const char *stock_id = NULL;
3185 if(gtkconv != pidgin_conv_window_get_active_gtkconv(gtkwin)) {
3186 return FALSE;
3189 switch (rand() % 5) {
3190 case 0:
3191 stock_id = PIDGIN_STOCK_ANIMATION_TYPING0;
3192 break;
3193 case 1:
3194 stock_id = PIDGIN_STOCK_ANIMATION_TYPING1;
3195 break;
3196 case 2:
3197 stock_id = PIDGIN_STOCK_ANIMATION_TYPING2;
3198 break;
3199 case 3:
3200 stock_id = PIDGIN_STOCK_ANIMATION_TYPING3;
3201 break;
3202 case 4:
3203 stock_id = PIDGIN_STOCK_ANIMATION_TYPING4;
3204 break;
3206 if (gtkwin->menu->typing_icon == NULL) {
3207 gtkwin->menu->typing_icon = gtk_image_new_from_stock(stock_id, GTK_ICON_SIZE_MENU);
3208 pidgin_menu_tray_append(PIDGIN_MENU_TRAY(gtkwin->menu->tray),
3209 gtkwin->menu->typing_icon,
3210 _("User is typing..."));
3211 } else {
3212 gtk_image_set_from_stock(GTK_IMAGE(gtkwin->menu->typing_icon), stock_id, GTK_ICON_SIZE_MENU);
3214 gtk_widget_show(gtkwin->menu->typing_icon);
3215 return TRUE;
3217 #endif
3219 static void
3220 update_typing_message(PidginConversation *gtkconv, const char *message)
3222 /* TODO WEBKIT: this is not handled at all */
3223 #if 0
3224 GtkTextBuffer *buffer;
3225 GtkTextMark *stmark, *enmark;
3227 if (g_object_get_data(G_OBJECT(gtkconv->imhtml), "disable-typing-notification"))
3228 return;
3230 buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(gtkconv->imhtml));
3231 stmark = gtk_text_buffer_get_mark(buffer, "typing-notification-start");
3232 enmark = gtk_text_buffer_get_mark(buffer, "typing-notification-end");
3233 if (stmark && enmark) {
3234 GtkTextIter start, end;
3235 gtk_text_buffer_get_iter_at_mark(buffer, &start, stmark);
3236 gtk_text_buffer_get_iter_at_mark(buffer, &end, enmark);
3237 gtk_text_buffer_delete_mark(buffer, stmark);
3238 gtk_text_buffer_delete_mark(buffer, enmark);
3239 gtk_text_buffer_delete(buffer, &start, &end);
3240 } else if (message && *message == '\n' && message[1] == ' ' && message[2] == '\0')
3241 message = NULL;
3243 #ifdef RESERVE_LINE
3244 if (!message)
3245 message = "\n "; /* The blank space is required to avoid a GTK+/Pango bug */
3246 #endif
3248 if (message) {
3249 GtkTextIter iter;
3250 gtk_text_buffer_get_end_iter(buffer, &iter);
3251 gtk_text_buffer_create_mark(buffer, "typing-notification-start", &iter, TRUE);
3252 gtk_text_buffer_insert_with_tags_by_name(buffer, &iter, message, -1, "TYPING-NOTIFICATION", NULL);
3253 gtk_text_buffer_get_end_iter(buffer, &iter);
3254 gtk_text_buffer_create_mark(buffer, "typing-notification-end", &iter, TRUE);
3256 #endif /* if 0 */
3259 static void
3260 update_typing_icon(PidginConversation *gtkconv)
3262 PurpleIMConversation *im;
3263 char *message = NULL;
3265 if (!PURPLE_IS_IM_CONVERSATION(gtkconv->active_conv))
3266 return;
3268 im = PURPLE_IM_CONVERSATION(gtkconv->active_conv);
3270 if (purple_im_conversation_get_typing_state(im) == PURPLE_IM_NOT_TYPING) {
3271 #ifdef RESERVE_LINE
3272 update_typing_message(gtkconv, NULL);
3273 #else
3274 update_typing_message(gtkconv, "\n ");
3275 #endif
3276 return;
3279 if (purple_im_conversation_get_typing_state(im) == PURPLE_IM_TYPING) {
3280 message = g_strdup_printf(_("\n%s is typing..."), purple_conversation_get_title(PURPLE_CONVERSATION(im)));
3281 } else {
3282 message = g_strdup_printf(_("\n%s has stopped typing"), purple_conversation_get_title(PURPLE_CONVERSATION(im)));
3285 update_typing_message(gtkconv, message);
3286 g_free(message);
3289 static gboolean
3290 update_send_to_selection(PidginConvWindow *win)
3292 PurpleAccount *account;
3293 PurpleConversation *conv;
3294 GtkWidget *menu;
3295 GList *child;
3296 PurpleBuddy *b;
3298 conv = pidgin_conv_window_get_active_conversation(win);
3300 if (conv == NULL)
3301 return FALSE;
3303 account = purple_conversation_get_account(conv);
3305 if (account == NULL)
3306 return FALSE;
3308 if (win->menu->send_to == NULL)
3309 return FALSE;
3311 if (!(b = purple_blist_find_buddy(account, purple_conversation_get_name(conv))))
3312 return FALSE;
3314 gtk_widget_show(win->menu->send_to);
3316 menu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(win->menu->send_to));
3318 for (child = gtk_container_get_children(GTK_CONTAINER(menu));
3319 child != NULL;
3320 child = g_list_delete_link(child, child)) {
3322 GtkWidget *item = child->data;
3323 PurpleBuddy *item_buddy;
3324 PurpleAccount *item_account = g_object_get_data(G_OBJECT(item), "purple_account");
3325 gchar *buddy_name = g_object_get_data(G_OBJECT(item),
3326 "purple_buddy_name");
3327 item_buddy = purple_blist_find_buddy(item_account, buddy_name);
3329 if (b == item_buddy) {
3330 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item), TRUE);
3331 g_list_free(child);
3332 break;
3336 return FALSE;
3339 static gboolean
3340 send_to_item_enter_notify_cb(GtkWidget *menuitem, GdkEventCrossing *event, GtkWidget *label)
3342 gtk_widget_set_sensitive(GTK_WIDGET(label), TRUE);
3343 return FALSE;
3346 static gboolean
3347 send_to_item_leave_notify_cb(GtkWidget *menuitem, GdkEventCrossing *event, GtkWidget *label)
3349 gtk_widget_set_sensitive(GTK_WIDGET(label), FALSE);
3350 return FALSE;
3353 static GtkWidget *
3354 e2ee_state_to_gtkimage(PurpleE2eeState *state)
3356 PurpleImage *img;
3358 img = _pidgin_e2ee_stock_icon_get(
3359 purple_e2ee_state_get_stock_icon(state));
3360 if (!img)
3361 return NULL;
3363 return gtk_image_new_from_pixbuf(pidgin_pixbuf_from_image(img));
3366 static void
3367 create_sendto_item(GtkWidget *menu, GtkSizeGroup *sg, GSList **group,
3368 PurpleBuddy *buddy, PurpleAccount *account, const char *name,
3369 gboolean e2ee_enabled)
3371 GtkWidget *box;
3372 GtkWidget *label;
3373 GtkWidget *image;
3374 GtkWidget *e2ee_image = NULL;
3375 GtkWidget *menuitem;
3376 GdkPixbuf *pixbuf;
3377 gchar *text;
3379 /* Create a pixmap for the protocol icon. */
3380 pixbuf = pidgin_create_protocol_icon(account, PIDGIN_PROTOCOL_ICON_SMALL);
3382 /* Now convert it to GtkImage */
3383 if (pixbuf == NULL)
3384 image = gtk_image_new();
3385 else
3387 image = gtk_image_new_from_pixbuf(pixbuf);
3388 g_object_unref(G_OBJECT(pixbuf));
3391 if (e2ee_enabled) {
3392 PurpleIMConversation *im;
3393 PurpleE2eeState *state = NULL;
3395 im = purple_conversations_find_im_with_account(
3396 purple_buddy_get_name(buddy), purple_buddy_get_account(buddy));
3397 if (im)
3398 state = purple_conversation_get_e2ee_state(PURPLE_CONVERSATION(im));
3399 if (state)
3400 e2ee_image = e2ee_state_to_gtkimage(state);
3401 else
3402 e2ee_image = gtk_image_new();
3405 gtk_size_group_add_widget(sg, image);
3407 /* Make our menu item */
3408 text = g_strdup_printf("%s (%s)", name, purple_account_get_name_for_display(account));
3409 menuitem = gtk_radio_menu_item_new_with_label(*group, text);
3410 g_free(text);
3411 *group = gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(menuitem));
3413 /* Do some evil, see some evil, speak some evil. */
3414 box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
3416 label = gtk_bin_get_child(GTK_BIN(menuitem));
3417 g_object_ref(label);
3418 gtk_container_remove(GTK_CONTAINER(menuitem), label);
3420 gtk_box_pack_start(GTK_BOX(box), image, FALSE, FALSE, 0);
3422 gtk_box_pack_start(GTK_BOX(box), label, TRUE, TRUE, 4);
3423 if (e2ee_image)
3424 gtk_box_pack_start(GTK_BOX(box), e2ee_image, FALSE, FALSE, 0);
3426 if (buddy != NULL &&
3427 !purple_presence_is_online(purple_buddy_get_presence(buddy)))
3429 gtk_widget_set_sensitive(label, FALSE);
3431 /* Set the label sensitive when the menuitem is highlighted and
3432 * insensitive again when the mouse leaves it. This way, it
3433 * doesn't appear weird from the highlighting of the embossed
3434 * (insensitive style) text.*/
3435 g_signal_connect(menuitem, "enter-notify-event",
3436 G_CALLBACK(send_to_item_enter_notify_cb), label);
3437 g_signal_connect(menuitem, "leave-notify-event",
3438 G_CALLBACK(send_to_item_leave_notify_cb), label);
3441 g_object_unref(label);
3443 gtk_container_add(GTK_CONTAINER(menuitem), box);
3445 gtk_widget_show(label);
3446 gtk_widget_show(image);
3447 if (e2ee_image)
3448 gtk_widget_show(e2ee_image);
3449 gtk_widget_show(box);
3451 /* Set our data and callbacks. */
3452 g_object_set_data(G_OBJECT(menuitem), "purple_account", account);
3453 g_object_set_data_full(G_OBJECT(menuitem), "purple_buddy_name", g_strdup(name), g_free);
3455 g_signal_connect(G_OBJECT(menuitem), "activate",
3456 G_CALLBACK(menu_conv_sel_send_cb), NULL);
3458 gtk_widget_show(menuitem);
3459 gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
3462 static gboolean
3463 compare_buddy_presence(PurplePresence *p1, PurplePresence *p2)
3465 /* This is necessary because multiple PurpleBuddy's don't share the same
3466 * PurplePresence anymore.
3468 PurpleBuddy *b1 = purple_buddy_presence_get_buddy(PURPLE_BUDDY_PRESENCE(p1));
3469 PurpleBuddy *b2 = purple_buddy_presence_get_buddy(PURPLE_BUDDY_PRESENCE(p2));
3470 if (purple_buddy_get_account(b1) == purple_buddy_get_account(b2) &&
3471 purple_strequal(purple_buddy_get_name(b1), purple_buddy_get_name(b2)))
3472 return FALSE;
3473 return TRUE;
3476 static void
3477 generate_send_to_items(PidginConvWindow *win)
3479 GtkWidget *menu;
3480 GSList *group = NULL;
3481 GtkSizeGroup *sg = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL);
3482 PidginConversation *gtkconv;
3483 GSList *l, *buds;
3485 g_return_if_fail(win != NULL);
3487 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
3489 g_return_if_fail(gtkconv != NULL);
3491 if (win->menu->send_to != NULL)
3492 gtk_widget_destroy(win->menu->send_to);
3494 /* Build the Send To menu */
3495 win->menu->send_to = gtk_menu_item_new_with_mnemonic(_("S_end To"));
3496 gtk_widget_show(win->menu->send_to);
3498 menu = gtk_menu_new();
3499 gtk_menu_shell_insert(GTK_MENU_SHELL(win->menu->menubar),
3500 win->menu->send_to, 2);
3501 gtk_menu_item_set_submenu(GTK_MENU_ITEM(win->menu->send_to), menu);
3503 gtk_widget_show(menu);
3505 if (PURPLE_IS_IM_CONVERSATION(gtkconv->active_conv)) {
3506 buds = purple_blist_find_buddies(purple_conversation_get_account(gtkconv->active_conv), purple_conversation_get_name(gtkconv->active_conv));
3508 if (buds == NULL)
3510 /* The user isn't on the buddy list. So we don't create any sendto menu. */
3512 else
3514 gboolean e2ee_enabled = FALSE;
3515 GList *list = NULL, *iter;
3516 for (l = buds; l != NULL; l = l->next)
3518 PurpleBlistNode *node;
3520 node = PURPLE_BLIST_NODE(purple_buddy_get_contact(PURPLE_BUDDY(l->data)));
3522 for (node = node->child; node != NULL; node = node->next)
3524 PurpleBuddy *buddy = (PurpleBuddy *)node;
3525 PurpleAccount *account;
3526 PurpleIMConversation *im;
3528 if (!PURPLE_IS_BUDDY(node))
3529 continue;
3531 im = purple_conversations_find_im_with_account(purple_buddy_get_name(buddy), purple_buddy_get_account(buddy));
3532 if (im && purple_conversation_get_e2ee_state(PURPLE_CONVERSATION(im)) != NULL)
3533 e2ee_enabled = TRUE;
3535 account = purple_buddy_get_account(buddy);
3536 /* TODO WEBKIT: (I'm not actually sure if this is webkit-related --Mark Doliner) */
3537 if (purple_account_is_connected(account) /*|| account == purple_conversation_get_account(gtkconv->active_conv)*/)
3539 /* Use the PurplePresence to get unique buddies. */
3540 PurplePresence *presence = purple_buddy_get_presence(buddy);
3541 if (!g_list_find_custom(list, presence, (GCompareFunc)compare_buddy_presence))
3542 list = g_list_prepend(list, presence);
3547 /* Create the sendto menu only if it has more than one item to show */
3548 if (list && list->next) {
3549 /* Loop over the list backwards so we get the items in the right order,
3550 * since we did a g_list_prepend() earlier. */
3551 for (iter = g_list_last(list); iter != NULL; iter = iter->prev) {
3552 PurplePresence *pre = iter->data;
3553 PurpleBuddy *buddy = purple_buddy_presence_get_buddy(PURPLE_BUDDY_PRESENCE(pre));
3554 create_sendto_item(menu, sg, &group, buddy,
3555 purple_buddy_get_account(buddy), purple_buddy_get_name(buddy), e2ee_enabled);
3558 g_list_free(list);
3559 g_slist_free(buds);
3563 g_object_unref(sg);
3565 gtk_widget_show(win->menu->send_to);
3566 /* TODO: This should never be insensitive. Possibly hidden or not. */
3567 if (!group)
3568 gtk_widget_set_sensitive(win->menu->send_to, FALSE);
3569 update_send_to_selection(win);
3572 PurpleImage *
3573 _pidgin_e2ee_stock_icon_get(const gchar *stock_name)
3575 gchar filename[100], *path;
3576 PurpleImage *image;
3578 /* core is quitting */
3579 if (e2ee_stock == NULL)
3580 return NULL;
3582 if (g_hash_table_lookup_extended(e2ee_stock, stock_name, NULL, (gpointer*)&image))
3583 return image;
3585 g_snprintf(filename, sizeof(filename), "e2ee-%s.png", stock_name);
3586 path = g_build_filename(PURPLE_DATADIR, "pidgin", "icons",
3587 "hicolor", "16x16", "status", filename, NULL);
3588 image = purple_image_new_from_file(path, NULL);
3589 g_free(path);
3591 g_hash_table_insert(e2ee_stock, g_strdup(stock_name), image);
3592 return image;
3595 static void
3596 generate_e2ee_controls(PidginConvWindow *win)
3598 PidginConversation *gtkconv;
3599 PurpleConversation *conv;
3600 PurpleE2eeState *state;
3601 PurpleE2eeProvider *provider;
3602 GtkWidget *menu;
3603 GList *menu_actions, *it;
3604 GtkWidget *e2ee_image;
3606 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
3607 g_return_if_fail(gtkconv != NULL);
3609 conv = gtkconv->active_conv;
3610 g_return_if_fail(conv != NULL);
3612 if (win->menu->e2ee != NULL) {
3613 gtk_widget_destroy(win->menu->e2ee);
3614 win->menu->e2ee = NULL;
3617 provider = purple_e2ee_provider_get_main();
3618 state = purple_conversation_get_e2ee_state(conv);
3619 if (state == NULL || provider == NULL)
3620 return;
3621 if (purple_e2ee_state_get_provider(state) != provider)
3622 return;
3624 win->menu->e2ee = gtk_image_menu_item_new_with_label(
3625 purple_e2ee_provider_get_name(provider));
3627 menu = gtk_menu_new();
3628 gtk_menu_shell_insert(GTK_MENU_SHELL(win->menu->menubar),
3629 win->menu->e2ee, 3);
3630 gtk_menu_item_set_submenu(GTK_MENU_ITEM(win->menu->e2ee), menu);
3632 e2ee_image = e2ee_state_to_gtkimage(state);
3633 if (e2ee_image) {
3634 gtk_image_menu_item_set_image(
3635 GTK_IMAGE_MENU_ITEM(win->menu->e2ee), e2ee_image);
3638 gtk_widget_set_tooltip_text(win->menu->e2ee,
3639 purple_e2ee_state_get_name(state));
3641 menu_actions = purple_e2ee_provider_get_conv_menu_actions(provider, conv);
3642 for (it = menu_actions; it; it = g_list_next(it)) {
3643 PurpleActionMenu *action = it->data;
3645 gtk_widget_show_all(
3646 pidgin_append_menu_action(menu, action, conv));
3648 g_list_free(menu_actions);
3650 gtk_widget_show(win->menu->e2ee);
3651 gtk_widget_show(menu);
3654 static const char *
3655 get_chat_user_status_icon(PurpleChatConversation *chat, const char *name, PurpleChatUserFlags flags)
3657 const char *image = NULL;
3659 if (flags & PURPLE_CHAT_USER_FOUNDER) {
3660 image = PIDGIN_STOCK_STATUS_FOUNDER;
3661 } else if (flags & PURPLE_CHAT_USER_OP) {
3662 image = PIDGIN_STOCK_STATUS_OPERATOR;
3663 } else if (flags & PURPLE_CHAT_USER_HALFOP) {
3664 image = PIDGIN_STOCK_STATUS_HALFOP;
3665 } else if (flags & PURPLE_CHAT_USER_VOICE) {
3666 image = PIDGIN_STOCK_STATUS_VOICE;
3667 } else if ((!flags) && purple_chat_conversation_is_ignored_user(chat, name)) {
3668 image = PIDGIN_STOCK_STATUS_IGNORED;
3669 } else {
3670 return NULL;
3672 return image;
3675 static void
3676 deleting_chat_user_cb(PurpleChatUser *cb)
3678 GtkTreeRowReference *ref = purple_chat_user_get_ui_data(cb);
3680 if (ref) {
3681 gtk_tree_row_reference_free(ref);
3682 purple_chat_user_set_ui_data(cb, NULL);
3686 static void
3687 add_chat_user_common(PurpleChatConversation *chat, PurpleChatUser *cb, const char *old_name)
3689 PidginConversation *gtkconv;
3690 PurpleConversation *conv;
3691 PidginChatPane *gtkchat;
3692 PurpleConnection *gc;
3693 PurpleProtocol *protocol;
3694 GtkTreeModel *tm;
3695 GtkListStore *ls;
3696 GtkTreePath *newpath;
3697 const char *stock;
3698 GtkTreeIter iter;
3699 gboolean is_me = FALSE;
3700 gboolean is_buddy;
3701 const gchar *name, *alias;
3702 gchar *tmp, *alias_key;
3703 PurpleChatUserFlags flags;
3704 GdkRGBA *color = NULL;
3706 alias = purple_chat_user_get_alias(cb);
3707 name = purple_chat_user_get_name(cb);
3708 flags = purple_chat_user_get_flags(cb);
3710 conv = PURPLE_CONVERSATION(chat);
3711 gtkconv = PIDGIN_CONVERSATION(conv);
3712 gtkchat = gtkconv->u.chat;
3713 gc = purple_conversation_get_connection(conv);
3715 if (!gc || !(protocol = purple_connection_get_protocol(gc)))
3716 return;
3718 tm = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
3719 ls = GTK_LIST_STORE(tm);
3721 stock = get_chat_user_status_icon(chat, name, flags);
3723 if (purple_strequal(purple_chat_conversation_get_nick(chat), purple_normalize(purple_conversation_get_account(conv), old_name != NULL ? old_name : name)))
3724 is_me = TRUE;
3726 is_buddy = purple_chat_user_is_buddy(cb);
3728 tmp = g_utf8_casefold(alias, -1);
3729 alias_key = g_utf8_collate_key(tmp, -1);
3730 g_free(tmp);
3732 if (is_me) {
3733 #if 0
3734 /* TODO WEBKIT: No tags in webkit stuff, yet. */
3735 GtkTextTag *tag = gtk_text_tag_table_lookup(
3736 gtk_text_buffer_get_tag_table(GTK_IMHTML(gtkconv->webview)->text_buffer),
3737 "send-name");
3738 g_object_get(tag, "foreground-rgba", &color, NULL);
3739 #endif /* if 0 */
3740 } else {
3741 GtkTextTag *tag;
3742 if ((tag = get_buddy_tag(chat, name, 0, FALSE)))
3743 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_NORMAL, NULL);
3744 if ((tag = get_buddy_tag(chat, name, PURPLE_MESSAGE_NICK, FALSE)))
3745 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_NORMAL, NULL);
3746 color = (GdkRGBA*)get_nick_color(gtkconv, name);
3749 gtk_list_store_insert_with_values(ls, &iter,
3751 * The GTK docs are mute about the effects of the "row" value for performance.
3752 * X-Chat hardcodes their value to 0 (prepend) and -1 (append), so we will too.
3753 * It *might* be faster to search the gtk_list_store and set row accurately,
3754 * but no one in #gtk+ seems to know anything about it either.
3755 * Inserting in the "wrong" location has no visible ill effects. - F.P.
3757 -1, /* "row" */
3758 CHAT_USERS_ICON_STOCK_COLUMN, stock,
3759 CHAT_USERS_ALIAS_COLUMN, alias,
3760 CHAT_USERS_ALIAS_KEY_COLUMN, alias_key,
3761 CHAT_USERS_NAME_COLUMN, name,
3762 CHAT_USERS_FLAGS_COLUMN, flags,
3763 CHAT_USERS_COLOR_COLUMN, color,
3764 CHAT_USERS_WEIGHT_COLUMN, is_buddy ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL,
3765 -1);
3767 if (purple_chat_user_get_ui_data(cb)) {
3768 GtkTreeRowReference *ref = purple_chat_user_get_ui_data(cb);
3769 gtk_tree_row_reference_free(ref);
3772 newpath = gtk_tree_model_get_path(tm, &iter);
3773 purple_chat_user_set_ui_data(cb, gtk_tree_row_reference_new(tm, newpath));
3774 gtk_tree_path_free(newpath);
3776 #if 0
3777 if (is_me && color)
3778 gdk_rgba_free(color);
3779 #endif
3780 g_free(alias_key);
3783 static void topic_callback(GtkWidget *w, PidginConversation *gtkconv)
3785 PurpleProtocol *protocol = NULL;
3786 PurpleConnection *gc;
3787 PurpleConversation *conv = gtkconv->active_conv;
3788 PidginChatPane *gtkchat;
3789 char *new_topic;
3790 const char *current_topic;
3792 gc = purple_conversation_get_connection(conv);
3794 if(!gc || !(protocol = purple_connection_get_protocol(gc)))
3795 return;
3797 if(!PURPLE_PROTOCOL_IMPLEMENTS(protocol, CHAT, set_topic))
3798 return;
3800 gtkconv = PIDGIN_CONVERSATION(conv);
3801 gtkchat = gtkconv->u.chat;
3802 new_topic = g_strdup(gtk_entry_get_text(GTK_ENTRY(gtkchat->topic_text)));
3803 current_topic = purple_chat_conversation_get_topic(PURPLE_CHAT_CONVERSATION(conv));
3805 if(current_topic && !g_utf8_collate(new_topic, current_topic)){
3806 g_free(new_topic);
3807 return;
3810 if (current_topic)
3811 gtk_entry_set_text(GTK_ENTRY(gtkchat->topic_text), current_topic);
3812 else
3813 gtk_entry_set_text(GTK_ENTRY(gtkchat->topic_text), "");
3815 purple_protocol_chat_iface_set_topic(protocol, gc, purple_chat_conversation_get_id(PURPLE_CHAT_CONVERSATION(conv)),
3816 new_topic);
3818 g_free(new_topic);
3821 static gint
3822 sort_chat_users(GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, gpointer userdata)
3824 PurpleChatUserFlags f1 = 0, f2 = 0;
3825 char *user1 = NULL, *user2 = NULL;
3826 gboolean buddy1 = FALSE, buddy2 = FALSE;
3827 gint ret = 0;
3829 gtk_tree_model_get(model, a,
3830 CHAT_USERS_ALIAS_KEY_COLUMN, &user1,
3831 CHAT_USERS_FLAGS_COLUMN, &f1,
3832 CHAT_USERS_WEIGHT_COLUMN, &buddy1,
3833 -1);
3834 gtk_tree_model_get(model, b,
3835 CHAT_USERS_ALIAS_KEY_COLUMN, &user2,
3836 CHAT_USERS_FLAGS_COLUMN, &f2,
3837 CHAT_USERS_WEIGHT_COLUMN, &buddy2,
3838 -1);
3840 /* Only sort by membership levels */
3841 f1 &= PURPLE_CHAT_USER_VOICE | PURPLE_CHAT_USER_HALFOP | PURPLE_CHAT_USER_OP |
3842 PURPLE_CHAT_USER_FOUNDER;
3843 f2 &= PURPLE_CHAT_USER_VOICE | PURPLE_CHAT_USER_HALFOP | PURPLE_CHAT_USER_OP |
3844 PURPLE_CHAT_USER_FOUNDER;
3846 ret = g_strcmp0(user1, user2);
3848 if (user1 != NULL && user2 != NULL) {
3849 if (f1 != f2) {
3850 /* sort more important users first */
3851 ret = (f1 > f2) ? -1 : 1;
3852 } else if (buddy1 != buddy2) {
3853 ret = (buddy1 > buddy2) ? -1 : 1;
3857 g_free(user1);
3858 g_free(user2);
3860 return ret;
3863 static void
3864 update_chat_alias(PurpleBuddy *buddy, PurpleChatConversation *chat, PurpleConnection *gc, PurpleProtocol *protocol)
3866 PidginConversation *gtkconv = PIDGIN_CONVERSATION(PURPLE_CONVERSATION(chat));
3867 PurpleAccount *account = purple_conversation_get_account(PURPLE_CONVERSATION(chat));
3868 GtkTreeModel *model;
3869 char *normalized_name;
3870 GtkTreeIter iter;
3871 int f;
3873 g_return_if_fail(buddy != NULL);
3874 g_return_if_fail(chat != NULL);
3876 /* This is safe because this callback is only used in chats, not IMs. */
3877 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkconv->u.chat->list));
3879 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter))
3880 return;
3882 normalized_name = g_strdup(purple_normalize(account, purple_buddy_get_name(buddy)));
3884 do {
3885 char *name;
3887 gtk_tree_model_get(model, &iter, CHAT_USERS_NAME_COLUMN, &name, -1);
3889 if (purple_strequal(normalized_name, purple_normalize(account, name))) {
3890 const char *alias = name;
3891 char *tmp;
3892 char *alias_key = NULL;
3893 PurpleBuddy *buddy2;
3895 if (!purple_strequal(purple_chat_conversation_get_nick(chat), purple_normalize(account, name))) {
3896 /* This user is not me, so look into updating the alias. */
3898 if ((buddy2 = purple_blist_find_buddy(account, name)) != NULL) {
3899 alias = purple_buddy_get_contact_alias(buddy2);
3902 tmp = g_utf8_casefold(alias, -1);
3903 alias_key = g_utf8_collate_key(tmp, -1);
3904 g_free(tmp);
3906 gtk_list_store_set(GTK_LIST_STORE(model), &iter,
3907 CHAT_USERS_ALIAS_COLUMN, alias,
3908 CHAT_USERS_ALIAS_KEY_COLUMN, alias_key,
3909 -1);
3910 g_free(alias_key);
3912 g_free(name);
3913 break;
3916 f = gtk_tree_model_iter_next(model, &iter);
3918 g_free(name);
3919 } while (f != 0);
3921 g_free(normalized_name);
3924 static void
3925 blist_node_aliased_cb(PurpleBlistNode *node, const char *old_alias, PurpleChatConversation *chat)
3927 PurpleConnection *gc;
3928 PurpleProtocol *protocol;
3929 PurpleConversation *conv = PURPLE_CONVERSATION(chat);
3931 g_return_if_fail(node != NULL);
3932 g_return_if_fail(conv != NULL);
3934 gc = purple_conversation_get_connection(conv);
3935 g_return_if_fail(gc != NULL);
3936 g_return_if_fail(purple_connection_get_protocol(gc) != NULL);
3937 protocol = purple_connection_get_protocol(gc);
3939 if (purple_protocol_get_options(protocol) & OPT_PROTO_UNIQUE_CHATNAME)
3940 return;
3942 if (PURPLE_IS_CONTACT(node))
3944 PurpleBlistNode *bnode;
3946 for(bnode = node->child; bnode; bnode = bnode->next) {
3948 if(!PURPLE_IS_BUDDY(bnode))
3949 continue;
3951 update_chat_alias((PurpleBuddy *)bnode, chat, gc, protocol);
3954 else if (PURPLE_IS_BUDDY(node))
3955 update_chat_alias((PurpleBuddy *)node, chat, gc, protocol);
3956 else if (PURPLE_IS_CHAT(node) &&
3957 purple_conversation_get_account(conv) == purple_chat_get_account((PurpleChat*)node))
3959 if (old_alias == NULL || g_utf8_collate(old_alias, purple_conversation_get_title(conv)) == 0)
3960 pidgin_conv_update_fields(conv, PIDGIN_CONV_SET_TITLE);
3964 static void
3965 buddy_cb_common(PurpleBuddy *buddy, PurpleChatConversation *chat, gboolean is_buddy)
3967 GtkTreeModel *model;
3968 char *normalized_name;
3969 GtkTreeIter iter;
3970 GtkTextTag *texttag;
3971 PurpleConversation *conv = PURPLE_CONVERSATION(chat);
3972 int f;
3974 g_return_if_fail(buddy != NULL);
3975 g_return_if_fail(conv != NULL);
3977 /* Do nothing if the buddy does not belong to the conv's account */
3978 if (purple_buddy_get_account(buddy) != purple_conversation_get_account(conv))
3979 return;
3981 /* This is safe because this callback is only used in chats, not IMs. */
3982 model = gtk_tree_view_get_model(GTK_TREE_VIEW(PIDGIN_CONVERSATION(conv)->u.chat->list));
3984 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter))
3985 return;
3987 normalized_name = g_strdup(purple_normalize(purple_conversation_get_account(conv), purple_buddy_get_name(buddy)));
3989 do {
3990 char *name;
3992 gtk_tree_model_get(model, &iter, CHAT_USERS_NAME_COLUMN, &name, -1);
3994 if (purple_strequal(normalized_name, purple_normalize(purple_conversation_get_account(conv), name))) {
3995 gtk_list_store_set(GTK_LIST_STORE(model), &iter,
3996 CHAT_USERS_WEIGHT_COLUMN, is_buddy ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL, -1);
3997 g_free(name);
3998 break;
4001 f = gtk_tree_model_iter_next(model, &iter);
4003 g_free(name);
4004 } while (f != 0);
4006 g_free(normalized_name);
4008 blist_node_aliased_cb((PurpleBlistNode *)buddy, NULL, chat);
4010 texttag = get_buddy_tag(chat, purple_buddy_get_name(buddy), 0, FALSE); /* XXX: do we want the normalized name? */
4011 if (texttag) {
4012 g_object_set(texttag, "weight", is_buddy ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL, NULL);
4016 static void
4017 buddy_added_cb(PurpleBlistNode *node, PurpleChatConversation *chat)
4019 if (!PURPLE_IS_BUDDY(node))
4020 return;
4022 buddy_cb_common(PURPLE_BUDDY(node), chat, TRUE);
4025 static void
4026 buddy_removed_cb(PurpleBlistNode *node, PurpleChatConversation *chat)
4028 if (!PURPLE_IS_BUDDY(node))
4029 return;
4031 /* If there's another buddy for the same "dude" on the list, do nothing. */
4032 if (purple_blist_find_buddy(purple_buddy_get_account(PURPLE_BUDDY(node)),
4033 purple_buddy_get_name(PURPLE_BUDDY(node))) != NULL)
4034 return;
4036 buddy_cb_common(PURPLE_BUDDY(node), chat, FALSE);
4039 static void
4040 minimum_entry_lines_pref_cb(const char *name,
4041 PurplePrefType type,
4042 gconstpointer value,
4043 gpointer data)
4045 #if 0
4046 GList *l = purple_conversations_get_all();
4047 PurpleConversation *conv;
4048 while (l != NULL)
4050 conv = (PurpleConversation *)l->data;
4052 if (PIDGIN_IS_PIDGIN_CONVERSATION(conv))
4053 resize_webview_cb(PIDGIN_CONVERSATION(conv));
4054 l = l->next;
4056 #endif
4059 static void
4060 setup_chat_topic(PidginConversation *gtkconv, GtkWidget *vbox)
4062 PurpleConversation *conv = gtkconv->active_conv;
4063 PurpleConnection *gc = purple_conversation_get_connection(conv);
4064 PurpleProtocol *protocol = purple_connection_get_protocol(gc);
4065 if (purple_protocol_get_options(protocol) & OPT_PROTO_CHAT_TOPIC)
4067 GtkWidget *hbox, *label;
4068 PidginChatPane *gtkchat = gtkconv->u.chat;
4070 hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PIDGIN_HIG_BOX_SPACE);
4071 gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);
4073 label = gtk_label_new(_("Topic:"));
4074 gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0);
4076 gtkchat->topic_text = gtk_entry_new();
4077 gtk_widget_set_size_request(gtkchat->topic_text, -1, BUDDYICON_SIZE_MIN);
4079 if(!PURPLE_PROTOCOL_IMPLEMENTS(protocol, CHAT, set_topic)) {
4080 gtk_editable_set_editable(GTK_EDITABLE(gtkchat->topic_text), FALSE);
4081 } else {
4082 g_signal_connect(G_OBJECT(gtkchat->topic_text), "activate",
4083 G_CALLBACK(topic_callback), gtkconv);
4086 gtk_box_pack_start(GTK_BOX(hbox), gtkchat->topic_text, TRUE, TRUE, 0);
4087 g_signal_connect(G_OBJECT(gtkchat->topic_text), "key_press_event",
4088 G_CALLBACK(entry_key_press_cb), gtkconv);
4092 static gboolean
4093 pidgin_conv_userlist_create_tooltip(GtkWidget *tipwindow, GtkTreePath *path,
4094 gpointer userdata, int *w, int *h)
4096 PidginConversation *gtkconv = userdata;
4097 GtkTreeIter iter;
4098 GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkconv->u.chat->list));
4099 PurpleConversation *conv = gtkconv->active_conv;
4100 PurpleBlistNode *node;
4101 PurpleProtocol *protocol;
4102 PurpleAccount *account = purple_conversation_get_account(conv);
4103 char *who = NULL;
4105 if (purple_account_get_connection(account) == NULL)
4106 return FALSE;
4108 if (!gtk_tree_model_get_iter(GTK_TREE_MODEL(model), &iter, path))
4109 return FALSE;
4111 gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, CHAT_USERS_NAME_COLUMN, &who, -1);
4113 protocol = purple_connection_get_protocol(purple_account_get_connection(account));
4114 node = (PurpleBlistNode*)(purple_blist_find_buddy(purple_conversation_get_account(conv), who));
4115 if (node && protocol && (purple_protocol_get_options(protocol) & OPT_PROTO_UNIQUE_CHATNAME))
4116 pidgin_blist_draw_tooltip(node, gtkconv->infopane);
4118 g_free(who);
4119 return FALSE;
4122 static void
4123 setup_chat_userlist(PidginConversation *gtkconv, GtkWidget *hpaned)
4125 PidginChatPane *gtkchat = gtkconv->u.chat;
4126 GtkWidget *lbox, *list;
4127 GtkListStore *ls;
4128 GtkCellRenderer *rend;
4129 GtkTreeViewColumn *col;
4130 int ul_width;
4131 void *blist_handle = purple_blist_get_handle();
4132 PurpleConversation *conv = gtkconv->active_conv;
4134 /* Build the right pane. */
4135 lbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, PIDGIN_HIG_BOX_SPACE);
4136 gtk_paned_pack2(GTK_PANED(hpaned), lbox, FALSE, TRUE);
4137 gtk_widget_show(lbox);
4139 /* Setup the label telling how many people are in the room. */
4140 gtkchat->count = gtk_label_new(_("0 people in room"));
4141 gtk_label_set_ellipsize(GTK_LABEL(gtkchat->count), PANGO_ELLIPSIZE_END);
4142 gtk_box_pack_start(GTK_BOX(lbox), gtkchat->count, FALSE, FALSE, 0);
4143 gtk_widget_show(gtkchat->count);
4145 /* Setup the list of users. */
4147 ls = gtk_list_store_new(CHAT_USERS_COLUMNS, GDK_TYPE_PIXBUF, G_TYPE_STRING,
4148 G_TYPE_STRING, G_TYPE_STRING, G_TYPE_INT,
4149 GDK_TYPE_RGBA, G_TYPE_INT, G_TYPE_STRING);
4150 gtk_tree_sortable_set_sort_func(GTK_TREE_SORTABLE(ls), CHAT_USERS_ALIAS_KEY_COLUMN,
4151 sort_chat_users, NULL, NULL);
4153 list = gtk_tree_view_new_with_model(GTK_TREE_MODEL(ls));
4155 /* Allow a user to specify gtkrc settings for the chat userlist only */
4156 gtk_widget_set_name(list, "pidgin_conv_userlist");
4158 rend = gtk_cell_renderer_pixbuf_new();
4159 g_object_set(G_OBJECT(rend),
4160 "stock-size", gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL),
4161 NULL);
4162 col = gtk_tree_view_column_new_with_attributes(NULL, rend,
4163 "stock-id", CHAT_USERS_ICON_STOCK_COLUMN, NULL);
4164 gtk_tree_view_column_set_sizing(col, GTK_TREE_VIEW_COLUMN_AUTOSIZE);
4165 gtk_tree_view_append_column(GTK_TREE_VIEW(list), col);
4166 ul_width = purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/userlist_width");
4167 gtk_widget_set_size_request(lbox, ul_width, -1);
4169 /* Hack to prevent completely collapsed userlist coming back with a 1 pixel width.
4170 * I would have liked to use the GtkPaned "max-position", but for some reason that didn't work */
4171 if (ul_width == 0)
4172 gtk_paned_set_position(GTK_PANED(hpaned), 999999);
4174 g_signal_connect(G_OBJECT(list), "button_press_event",
4175 G_CALLBACK(right_click_chat_cb), gtkconv);
4176 g_signal_connect(G_OBJECT(list), "row-activated",
4177 G_CALLBACK(activate_list_cb), gtkconv);
4178 g_signal_connect(G_OBJECT(list), "popup-menu",
4179 G_CALLBACK(gtkconv_chat_popup_menu_cb), gtkconv);
4180 g_signal_connect(G_OBJECT(lbox), "size-allocate", G_CALLBACK(lbox_size_allocate_cb), gtkconv);
4182 pidgin_tooltip_setup_for_treeview(list, gtkconv,
4183 pidgin_conv_userlist_create_tooltip, NULL);
4185 rend = gtk_cell_renderer_text_new();
4186 g_object_set(rend,
4187 "foreground-set", TRUE,
4188 "weight-set", TRUE,
4189 NULL);
4190 g_object_set(G_OBJECT(rend), "editable", TRUE, NULL);
4192 col = gtk_tree_view_column_new_with_attributes(NULL, rend,
4193 "text", CHAT_USERS_ALIAS_COLUMN,
4194 "foreground-rgba", CHAT_USERS_COLOR_COLUMN,
4195 "weight", CHAT_USERS_WEIGHT_COLUMN,
4196 NULL);
4198 purple_signal_connect(blist_handle, "blist-node-added",
4199 gtkchat, PURPLE_CALLBACK(buddy_added_cb), conv);
4200 purple_signal_connect(blist_handle, "blist-node-removed",
4201 gtkchat, PURPLE_CALLBACK(buddy_removed_cb), conv);
4202 purple_signal_connect(blist_handle, "blist-node-aliased",
4203 gtkchat, PURPLE_CALLBACK(blist_node_aliased_cb), conv);
4205 gtk_tree_view_column_set_expand(col, TRUE);
4206 g_object_set(rend, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
4208 gtk_tree_view_append_column(GTK_TREE_VIEW(list), col);
4210 gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(list), FALSE);
4211 gtk_widget_show(list);
4213 gtkchat->list = list;
4215 gtk_box_pack_start(GTK_BOX(lbox),
4216 pidgin_make_scrollable(list, GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC, GTK_SHADOW_IN, -1, -1),
4217 TRUE, TRUE, 0);
4220 static gboolean
4221 pidgin_conv_create_tooltip(GtkWidget *tipwindow, gpointer userdata, int *w, int *h)
4223 PurpleBlistNode *node = NULL;
4224 PurpleConversation *conv;
4225 PidginConversation *gtkconv = userdata;
4227 conv = gtkconv->active_conv;
4228 if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
4229 node = (PurpleBlistNode*)(purple_blist_find_chat(purple_conversation_get_account(conv), purple_conversation_get_name(conv)));
4230 if (!node)
4231 node = g_object_get_data(G_OBJECT(gtkconv->history), "transient_chat");
4232 } else {
4233 node = (PurpleBlistNode*)(purple_blist_find_buddy(purple_conversation_get_account(conv), purple_conversation_get_name(conv)));
4234 #if 0
4235 /* Using the transient blist nodes to show the tooltip doesn't quite work yet. */
4236 if (!node)
4237 node = g_object_get_data(G_OBJECT(gtkconv->webview), "transient_buddy");
4238 #endif
4241 if (node)
4242 pidgin_blist_draw_tooltip(node, gtkconv->infopane);
4243 return FALSE;
4246 static GtkWidget *
4247 setup_common_pane(PidginConversation *gtkconv)
4249 GtkWidget *vbox, *sw, *event_box, *view;
4250 GtkCellRenderer *rend;
4251 GtkTreePath *path;
4252 PurpleConversation *conv = gtkconv->active_conv;
4253 PurpleBuddy *buddy;
4254 gboolean chat = PURPLE_IS_CHAT_CONVERSATION(conv);
4255 int buddyicon_size = 0;
4257 /* Setup the top part of the pane */
4258 vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, PIDGIN_HIG_BOX_SPACE);
4259 gtk_widget_show(vbox);
4261 /* Setup the info pane */
4262 event_box = gtk_event_box_new();
4263 gtk_event_box_set_visible_window(GTK_EVENT_BOX(event_box), FALSE);
4264 gtk_widget_show(event_box);
4265 gtkconv->infopane_hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
4266 gtk_box_pack_start(GTK_BOX(vbox), event_box, FALSE, FALSE, 0);
4267 gtk_container_add(GTK_CONTAINER(event_box), gtkconv->infopane_hbox);
4268 gtk_widget_show(gtkconv->infopane_hbox);
4269 gtk_widget_add_events(event_box,
4270 GDK_POINTER_MOTION_MASK | GDK_LEAVE_NOTIFY_MASK);
4271 g_signal_connect(G_OBJECT(event_box), "button-press-event",
4272 G_CALLBACK(infopane_press_cb), gtkconv);
4274 pidgin_tooltip_setup_for_widget(event_box, gtkconv,
4275 pidgin_conv_create_tooltip, NULL);
4277 gtkconv->infopane = gtk_cell_view_new();
4278 gtkconv->infopane_model = gtk_list_store_new(CONV_NUM_COLUMNS, G_TYPE_STRING, G_TYPE_STRING, GDK_TYPE_PIXBUF, GDK_TYPE_PIXBUF);
4279 gtk_cell_view_set_model(GTK_CELL_VIEW(gtkconv->infopane),
4280 GTK_TREE_MODEL(gtkconv->infopane_model));
4281 g_object_unref(gtkconv->infopane_model);
4282 gtk_list_store_append(gtkconv->infopane_model, &(gtkconv->infopane_iter));
4283 gtk_box_pack_start(GTK_BOX(gtkconv->infopane_hbox), gtkconv->infopane, TRUE, TRUE, 0);
4284 path = gtk_tree_path_new_from_string("0");
4285 gtk_cell_view_set_displayed_row(GTK_CELL_VIEW(gtkconv->infopane), path);
4286 gtk_tree_path_free(path);
4288 if (chat) {
4289 /* This empty widget is used to ensure that the infopane is consistently
4290 sized for chat windows. The correct fix is to put an icon in the chat
4291 window as well, because that would make "Set Custom Icon" consistent
4292 for both the buddy list and the chat window, but PidginConversation
4293 is pretty much stuck until 3.0. */
4294 GtkWidget *sizing_vbox;
4295 sizing_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
4296 gtk_widget_set_size_request(sizing_vbox, -1, BUDDYICON_SIZE_MIN);
4297 gtk_box_pack_start(GTK_BOX(gtkconv->infopane_hbox), sizing_vbox, FALSE, FALSE, 0);
4298 gtk_widget_show(sizing_vbox);
4300 else {
4301 gtkconv->u.im->icon_container = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
4303 if ((buddy = purple_blist_find_buddy(purple_conversation_get_account(conv),
4304 purple_conversation_get_name(conv))) != NULL) {
4305 PurpleContact *contact = purple_buddy_get_contact(buddy);
4306 if (contact) {
4307 buddyicon_size = purple_blist_node_get_int((PurpleBlistNode*)contact, "pidgin-infopane-iconsize");
4310 buddyicon_size = CLAMP(buddyicon_size, BUDDYICON_SIZE_MIN, BUDDYICON_SIZE_MAX);
4311 gtk_widget_set_size_request(gtkconv->u.im->icon_container, -1, buddyicon_size);
4313 gtk_box_pack_start(GTK_BOX(gtkconv->infopane_hbox),
4314 gtkconv->u.im->icon_container, FALSE, FALSE, 0);
4316 gtk_widget_show(gtkconv->u.im->icon_container);
4319 gtk_widget_show(gtkconv->infopane);
4321 rend = gtk_cell_renderer_pixbuf_new();
4322 gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(gtkconv->infopane), rend, FALSE);
4323 gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(gtkconv->infopane), rend, "stock-id", CONV_ICON_COLUMN, NULL);
4324 g_object_set(rend, "xalign", 0.0, "xpad", 6, "ypad", 0,
4325 "stock-size", gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL),
4326 NULL);
4328 rend = gtk_cell_renderer_text_new();
4329 gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(gtkconv->infopane), rend, TRUE);
4330 gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(gtkconv->infopane), rend, "markup", CONV_TEXT_COLUMN, NULL);
4331 g_object_set(rend, "ypad", 0, "yalign", 0.5, NULL);
4333 g_object_set(rend, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
4335 rend = gtk_cell_renderer_pixbuf_new();
4336 gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(gtkconv->infopane), rend, FALSE);
4337 gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(gtkconv->infopane), rend, "pixbuf", CONV_PROTOCOL_ICON_COLUMN, NULL);
4338 g_object_set(rend, "xalign", 0.0, "xpad", 3, "ypad", 0, NULL);
4340 rend = gtk_cell_renderer_pixbuf_new();
4341 gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(gtkconv->infopane), rend, FALSE);
4342 gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(gtkconv->infopane), rend, "pixbuf", CONV_EMBLEM_COLUMN, NULL);
4343 g_object_set(rend, "xalign", 0.0, "xpad", 6, "ypad", 0, NULL);
4345 /* Setup the history widget */
4346 sw = gtk_scrolled_window_new(NULL, NULL);
4347 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(sw), GTK_SHADOW_IN);
4348 gtk_scrolled_window_set_policy(
4349 GTK_SCROLLED_WINDOW(sw),
4350 GTK_POLICY_NEVER,
4351 GTK_POLICY_ALWAYS
4354 gtkconv->history_buffer = talkatu_history_buffer_new();
4355 gtkconv->history = talkatu_history_new();
4356 gtk_text_view_set_buffer(GTK_TEXT_VIEW(gtkconv->history), gtkconv->history_buffer);
4357 gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(gtkconv->history), GTK_WRAP_WORD);
4358 gtk_container_add(GTK_CONTAINER(sw), gtkconv->history);
4360 if (chat) {
4361 GtkWidget *hpaned;
4363 /* Add the topic */
4364 setup_chat_topic(gtkconv, vbox);
4366 /* Add the talkatu history */
4367 hpaned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
4368 gtk_box_pack_start(GTK_BOX(vbox), hpaned, TRUE, TRUE, 0);
4369 gtk_widget_show(hpaned);
4370 gtk_paned_pack1(GTK_PANED(hpaned), sw, TRUE, TRUE);
4372 /* Now add the userlist */
4373 setup_chat_userlist(gtkconv, hpaned);
4374 } else {
4375 gtk_box_pack_start(GTK_BOX(vbox), sw, TRUE, TRUE, 0);
4377 gtk_widget_show_all(sw);
4379 g_object_set_data(G_OBJECT(gtkconv->history), "gtkconv", gtkconv);
4381 g_signal_connect(G_OBJECT(gtkconv->history), "key_press_event",
4382 G_CALLBACK(refocus_entry_cb), gtkconv);
4383 g_signal_connect(G_OBJECT(gtkconv->history), "key_release_event",
4384 G_CALLBACK(refocus_entry_cb), gtkconv);
4386 gtkconv->lower_hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PIDGIN_HIG_BOX_SPACE);
4387 gtk_box_pack_start(GTK_BOX(vbox), gtkconv->lower_hbox, FALSE, FALSE, 0);
4388 gtk_widget_show(gtkconv->lower_hbox);
4390 /* Setup the entry widget and all signals */
4391 gtkconv->editor = talkatu_editor_new();
4392 talkatu_editor_set_buffer(TALKATU_EDITOR(gtkconv->editor), talkatu_html_buffer_new());
4393 gtk_box_pack_start(GTK_BOX(gtkconv->lower_hbox), gtkconv->editor, TRUE, TRUE, 0);
4395 view = talkatu_editor_get_view(TALKATU_EDITOR(gtkconv->editor));
4396 gtk_widget_set_name(view, "pidgin_conv_entry");
4397 talkatu_view_set_send_binding(TALKATU_VIEW(view), TALKATU_VIEW_SEND_BINDING_RETURN | TALKATU_VIEW_SEND_BINDING_KP_ENTER);
4398 g_signal_connect(
4399 G_OBJECT(view),
4400 "send-message",
4401 G_CALLBACK(send_cb),
4402 gtkconv
4405 if (!chat) {
4406 /* For sending typing notifications for IMs */
4407 gtkconv->u.im->typing_timer = 0;
4408 gtkconv->u.im->animate = purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/animate_buddy_icons");
4409 gtkconv->u.im->show_icon = TRUE;
4412 return vbox;
4415 static PidginConversation *
4416 pidgin_conv_find_gtkconv(PurpleConversation * conv)
4418 PurpleBuddy *bud = purple_blist_find_buddy(purple_conversation_get_account(conv), purple_conversation_get_name(conv));
4419 PurpleContact *c;
4420 PurpleBlistNode *cn, *bn;
4422 if (!bud)
4423 return NULL;
4425 if (!(c = purple_buddy_get_contact(bud)))
4426 return NULL;
4428 cn = PURPLE_BLIST_NODE(c);
4429 for (bn = purple_blist_node_get_first_child(cn); bn; bn = purple_blist_node_get_sibling_next(bn)) {
4430 PurpleBuddy *b = PURPLE_BUDDY(bn);
4431 PurpleIMConversation *im;
4432 if ((im = purple_conversations_find_im_with_account(purple_buddy_get_name(b), purple_buddy_get_account(b)))) {
4433 if (PIDGIN_CONVERSATION(PURPLE_CONVERSATION(im)))
4434 return PIDGIN_CONVERSATION(PURPLE_CONVERSATION(im));
4438 return NULL;
4441 static void
4442 buddy_update_cb(PurpleBlistNode *bnode, gpointer null)
4444 GList *list;
4446 g_return_if_fail(bnode);
4447 if (!PURPLE_IS_BUDDY(bnode))
4448 return;
4450 for (list = pidgin_conv_windows_get_list(); list; list = list->next)
4452 PidginConvWindow *win = list->data;
4453 PurpleConversation *conv = pidgin_conv_window_get_active_conversation(win);
4455 if (!PURPLE_IS_IM_CONVERSATION(conv))
4456 continue;
4458 pidgin_conv_update_fields(conv, PIDGIN_CONV_MENU);
4462 static gboolean
4463 ignore_middle_click(GtkWidget *widget, GdkEventButton *e, gpointer null)
4465 /* A click on the pane is propagated to the notebook containing the pane.
4466 * So if Stu accidentally aims high and middle clicks on the pane-handle,
4467 * it causes a conversation tab to close. Let's stop that from happening.
4469 if (e->button == GDK_BUTTON_MIDDLE && e->type == GDK_BUTTON_PRESS)
4470 return TRUE;
4471 return FALSE;
4474 /**************************************************************************
4475 * Conversation UI operations
4476 **************************************************************************/
4477 static void
4478 private_gtkconv_new(PurpleConversation *conv, gboolean hidden)
4480 PidginConversation *gtkconv;
4481 GtkWidget *pane = NULL;
4482 GtkWidget *tab_cont;
4483 PurpleBlistNode *convnode;
4485 if (PURPLE_IS_IM_CONVERSATION(conv) && (gtkconv = pidgin_conv_find_gtkconv(conv))) {
4486 purple_conversation_set_ui_data(conv, gtkconv);
4487 if (!g_list_find(gtkconv->convs, conv))
4488 gtkconv->convs = g_list_prepend(gtkconv->convs, conv);
4489 pidgin_conv_switch_active_conversation(conv);
4490 return;
4493 gtkconv = g_new0(PidginConversation, 1);
4494 purple_conversation_set_ui_data(conv, gtkconv);
4495 gtkconv->active_conv = conv;
4496 gtkconv->convs = g_list_prepend(gtkconv->convs, conv);
4497 gtkconv->send_history = g_list_append(NULL, NULL);
4499 /* Setup some initial variables. */
4500 gtkconv->unseen_state = PIDGIN_UNSEEN_NONE;
4501 gtkconv->unseen_count = 0;
4502 gtkconv->last_flags = 0;
4504 if (PURPLE_IS_IM_CONVERSATION(conv)) {
4505 gtkconv->u.im = g_malloc0(sizeof(PidginImPane));
4506 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
4507 gtkconv->u.chat = g_malloc0(sizeof(PidginChatPane));
4509 pane = setup_common_pane(gtkconv);
4511 if (pane == NULL) {
4512 if (PURPLE_IS_CHAT_CONVERSATION(conv))
4513 g_free(gtkconv->u.chat);
4514 else if (PURPLE_IS_IM_CONVERSATION(conv))
4515 g_free(gtkconv->u.im);
4517 g_free(gtkconv);
4518 purple_conversation_set_ui_data(conv, NULL);
4519 return;
4522 g_signal_connect(G_OBJECT(pane), "button_press_event",
4523 G_CALLBACK(ignore_middle_click), NULL);
4525 /* Setup the container for the tab. */
4526 gtkconv->tab_cont = tab_cont = gtk_box_new(GTK_ORIENTATION_VERTICAL, PIDGIN_HIG_BOX_SPACE);
4527 g_object_set_data(G_OBJECT(tab_cont), "PidginConversation", gtkconv);
4528 gtk_container_set_border_width(GTK_CONTAINER(tab_cont), PIDGIN_HIG_BOX_SPACE);
4529 gtk_box_pack_start(GTK_BOX(tab_cont), pane, TRUE, TRUE, 0);
4530 gtk_widget_show(pane);
4532 convnode = get_conversation_blist_node(conv);
4533 if (convnode == NULL || !purple_blist_node_get_bool(convnode, "gtk-mute-sound"))
4534 gtkconv->make_sound = TRUE;
4536 if (convnode != NULL && purple_blist_node_has_setting(convnode, "enable-logging")) {
4537 gboolean logging = purple_blist_node_get_bool(convnode, "enable-logging");
4538 purple_conversation_set_logging(conv, logging);
4541 talkatu_editor_set_toolbar_visible(
4542 TALKATU_EDITOR(gtkconv->editor),
4543 purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/show_formatting_toolbar")
4546 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons"))
4547 gtk_widget_show(gtkconv->infopane_hbox);
4548 else
4549 gtk_widget_hide(gtkconv->infopane_hbox);
4552 g_signal_connect_swapped(G_OBJECT(pane), "focus",
4553 G_CALLBACK(gtk_widget_grab_focus),
4554 gtkconv->editor);
4556 if (hidden)
4557 pidgin_conv_window_add_gtkconv(hidden_convwin, gtkconv);
4558 else
4559 pidgin_conv_placement_place(gtkconv);
4561 if (generated_nick_colors == NULL) {
4562 GdkColor color;
4563 GdkRGBA rgba;
4564 color = gtk_widget_get_style(gtkconv->history)->base[GTK_STATE_NORMAL];
4565 rgba.red = color.red / 65535.0;
4566 rgba.green = color.green / 65535.0;
4567 rgba.blue = color.blue / 65535.0;
4568 rgba.alpha = 1.0;
4569 generated_nick_colors = generate_nick_colors(NICK_COLOR_GENERATE_COUNT, rgba);
4572 gtkconv->nick_colors = g_array_ref(generated_nick_colors);
4575 static void
4576 pidgin_conv_new_hidden(PurpleConversation *conv)
4578 private_gtkconv_new(conv, TRUE);
4581 void
4582 pidgin_conv_new(PurpleConversation *conv)
4584 private_gtkconv_new(conv, FALSE);
4585 if (PIDGIN_IS_PIDGIN_CONVERSATION(conv))
4586 purple_signal_emit(pidgin_conversations_get_handle(),
4587 "conversation-displayed", PIDGIN_CONVERSATION(conv));
4590 static void
4591 received_im_msg_cb(PurpleAccount *account, char *sender, char *message,
4592 PurpleConversation *conv, PurpleMessageFlags flags)
4594 PurpleConversationUiOps *ui_ops = pidgin_conversations_get_conv_ui_ops();
4595 gboolean hide = FALSE;
4596 guint timer;
4598 /* create hidden conv if hide_new pref is always */
4599 if (purple_strequal(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new"), "always"))
4600 hide = TRUE;
4602 /* create hidden conv if hide_new pref is away and account is away */
4603 if (purple_strequal(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new"), "away") &&
4604 !purple_status_is_available(purple_account_get_active_status(account)))
4605 hide = TRUE;
4607 if (conv && PIDGIN_IS_PIDGIN_CONVERSATION(conv) && !hide) {
4608 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
4609 if (gtkconv->win == hidden_convwin) {
4610 pidgin_conv_attach_to_conversation(gtkconv->active_conv);
4612 return;
4615 if (hide) {
4616 ui_ops->create_conversation = pidgin_conv_new_hidden;
4617 purple_im_conversation_new(account, sender);
4618 ui_ops->create_conversation = pidgin_conv_new;
4621 /* Somebody wants to keep this conversation around, so don't time it out */
4622 if (conv) {
4623 timer = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(conv), "close-timer"));
4624 if (timer) {
4625 g_source_remove(timer);
4626 g_object_set_data(G_OBJECT(conv), "close-timer", GINT_TO_POINTER(0));
4631 static void
4632 pidgin_conv_destroy(PurpleConversation *conv)
4634 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
4636 gtkconv->convs = g_list_remove(gtkconv->convs, conv);
4637 /* Don't destroy ourselves until all our convos are gone */
4638 if (gtkconv->convs) {
4639 /* Make sure the destroyed conversation is not the active one */
4640 if (gtkconv->active_conv == conv) {
4641 gtkconv->active_conv = gtkconv->convs->data;
4642 purple_conversation_update(gtkconv->active_conv, PURPLE_CONVERSATION_UPDATE_FEATURES);
4644 return;
4647 pidgin_conv_window_remove_gtkconv(gtkconv->win, gtkconv);
4649 /* If the "Save Conversation" or "Save Icon" dialogs are open then close them */
4650 purple_request_close_with_handle(gtkconv);
4651 purple_notify_close_with_handle(gtkconv);
4653 gtk_widget_destroy(gtkconv->tab_cont);
4654 g_object_unref(gtkconv->tab_cont);
4656 if (PURPLE_IS_IM_CONVERSATION(conv)) {
4657 if (gtkconv->u.im->icon_timer != 0)
4658 g_source_remove(gtkconv->u.im->icon_timer);
4660 if (gtkconv->u.im->anim != NULL)
4661 g_object_unref(G_OBJECT(gtkconv->u.im->anim));
4663 if (gtkconv->u.im->typing_timer != 0)
4664 g_source_remove(gtkconv->u.im->typing_timer);
4666 g_free(gtkconv->u.im);
4667 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
4668 purple_signals_disconnect_by_handle(gtkconv->u.chat);
4669 g_free(gtkconv->u.chat);
4672 gtkconv->send_history = g_list_first(gtkconv->send_history);
4673 g_list_free_full(gtkconv->send_history, g_free);
4675 if (gtkconv->attach_timer) {
4676 g_source_remove(gtkconv->attach_timer);
4679 g_array_unref(gtkconv->nick_colors);
4681 g_free(gtkconv);
4684 #if 0
4685 static const char *
4686 get_text_tag_color(GtkTextTag *tag)
4688 GdkRGBA *color = NULL;
4689 gboolean set = FALSE;
4690 static char colcode[] = "#XXXXXX";
4691 if (tag)
4692 g_object_get(G_OBJECT(tag), "foreground-set", &set, "foreground-rgba", &color, NULL);
4693 if (set && color)
4694 g_snprintf(colcode, sizeof(colcode), "#%02x%02x%02x",
4695 (unsigned int)(color->red * 255),
4696 (unsigned int)(color->green * 255),
4697 (unsigned int)(color->blue * 255));
4698 else
4699 colcode[0] = '\0';
4700 if (color)
4701 gdk_rgba_free(color);
4702 return colcode;
4705 /* The callback for an event on a link tag. */
4706 static gboolean buddytag_event(GtkTextTag *tag, GObject *imhtml,
4707 GdkEvent *event, GtkTextIter *arg2, gpointer data)
4709 if (event->type == GDK_BUTTON_PRESS
4710 || event->type == GDK_2BUTTON_PRESS) {
4711 GdkEventButton *btn_event = (GdkEventButton*) event;
4712 PurpleConversation *conv = data;
4713 char *buddyname;
4714 gchar *name;
4716 g_object_get(G_OBJECT(tag), "name", &name, NULL);
4718 /* strlen("BUDDY " or "HILIT ") == 6 */
4719 g_return_val_if_fail((name != NULL) && (strlen(name) > 6), FALSE);
4721 buddyname = name + 6;
4723 /* emit chat-nick-clicked signal */
4724 if (event->type == GDK_BUTTON_PRESS) {
4725 gint plugin_return = GPOINTER_TO_INT(purple_signal_emit_return_1(
4726 pidgin_conversations_get_handle(), "chat-nick-clicked",
4727 data, buddyname, btn_event->button));
4728 if (plugin_return) {
4729 g_free(name);
4730 return TRUE;
4734 if (btn_event->button == GDK_BUTTON_PRIMARY && event->type == GDK_2BUTTON_PRESS) {
4735 chat_do_im(PIDGIN_CONVERSATION(conv), buddyname);
4736 g_free(name);
4738 return TRUE;
4739 } else if (btn_event->button == GDK_BUTTON_MIDDLE && event->type == GDK_2BUTTON_PRESS) {
4740 chat_do_info(PIDGIN_CONVERSATION(conv), buddyname);
4741 g_free(name);
4743 return TRUE;
4744 } else if (gdk_event_triggers_context_menu(event)) {
4745 GtkTextIter start, end;
4747 /* we shouldn't display the popup
4748 * if the user has selected something: */
4749 if (!gtk_text_buffer_get_selection_bounds(
4750 gtk_text_iter_get_buffer(arg2),
4751 &start, &end)) {
4752 GtkWidget *menu = NULL;
4753 PurpleConnection *gc =
4754 purple_conversation_get_connection(conv);
4756 menu = create_chat_menu(conv, buddyname, gc);
4757 gtk_menu_popup_at_pointer(GTK_MENU(menu), event);
4759 g_free(name);
4761 /* Don't propagate the event any further */
4762 return TRUE;
4766 g_free(name);
4769 return FALSE;
4771 #endif
4773 static GtkTextTag *get_buddy_tag(PurpleChatConversation *chat, const char *who, PurpleMessageFlags flag,
4774 gboolean create)
4776 /* TODO WEBKIT */
4777 #if 0
4778 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
4779 GtkTextTag *buddytag;
4780 gchar *str;
4781 gboolean highlight = (flag & PURPLE_MESSAGE_NICK);
4782 GtkTextBuffer *buffer = GTK_IMHTML(gtkconv->imhtml)->text_buffer;
4784 str = g_strdup_printf(highlight ? "HILIT %s" : "BUDDY %s", who);
4786 buddytag = gtk_text_tag_table_lookup(
4787 gtk_text_buffer_get_tag_table(buffer), str);
4789 if (buddytag == NULL && create) {
4790 if (highlight)
4791 buddytag = gtk_text_buffer_create_tag(buffer, str,
4792 "foreground", get_text_tag_color(gtk_text_tag_table_lookup(
4793 gtk_text_buffer_get_tag_table(buffer), "highlight-name")),
4794 "weight", PANGO_WEIGHT_BOLD,
4795 NULL);
4796 else
4797 buddytag = gtk_text_buffer_create_tag(
4798 buffer, str,
4799 "foreground-rgba", get_nick_color(gtkconv, who),
4800 "weight", purple_blist_find_buddy(purple_conversation_get_account(conv), who) ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL,
4801 NULL);
4803 g_object_set_data(G_OBJECT(buddytag), "cursor", "");
4804 g_signal_connect(G_OBJECT(buddytag), "event",
4805 G_CALLBACK(buddytag_event), conv);
4808 g_free(str);
4810 return buddytag;
4811 #endif /* if 0 */
4812 return NULL;
4815 static gboolean
4816 writing_msg(PurpleConversation *conv, PurpleMessage *msg, gpointer _unused)
4818 PidginConversation *gtkconv;
4820 g_return_val_if_fail(msg != NULL, FALSE);
4822 if (!(purple_message_get_flags(msg) & PURPLE_MESSAGE_ACTIVE_ONLY))
4823 return FALSE;
4825 g_return_val_if_fail(conv != NULL, FALSE);
4826 gtkconv = PIDGIN_CONVERSATION(conv);
4827 g_return_val_if_fail(gtkconv != NULL, FALSE);
4829 if (conv == gtkconv->active_conv)
4830 return FALSE;
4832 purple_debug_info("gtkconv",
4833 "Suppressing message for an inactive conversation");
4835 return TRUE;
4838 static void
4839 pidgin_conv_write_conv(PurpleConversation *conv, PurpleMessage *pmsg)
4841 PidginMessage *pidgin_msg = NULL;
4842 PurpleMessageFlags flags;
4843 PidginConversation *gtkconv;
4844 PurpleConnection *gc;
4845 PurpleAccount *account;
4846 gboolean plugin_return;
4848 g_return_if_fail(conv != NULL);
4849 gtkconv = PIDGIN_CONVERSATION(conv);
4850 g_return_if_fail(gtkconv != NULL);
4851 flags = purple_message_get_flags(pmsg);
4853 #if 0
4854 if (gtkconv->attach_timer) {
4855 /* We are currently in the process of filling up the buffer with the message
4856 * history of the conversation. So we do not need to add the message here.
4857 * Instead, this message will be added to the message-list, which in turn will
4858 * be processed and displayed by the attach-callback.
4860 return;
4863 if (conv != gtkconv->active_conv)
4865 /* Set the active conversation to the one that just messaged us. */
4866 /* TODO: consider not doing this if the account is offline or something */
4867 if (flags & (PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_RECV))
4868 pidgin_conv_switch_active_conversation(conv);
4870 #endif
4872 account = purple_conversation_get_account(conv);
4873 g_return_if_fail(account != NULL);
4874 gc = purple_account_get_connection(account);
4875 g_return_if_fail(gc != NULL || !(flags & (PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_RECV)));
4877 plugin_return = GPOINTER_TO_INT(purple_signal_emit_return_1(
4878 pidgin_conversations_get_handle(),
4879 (PURPLE_IS_IM_CONVERSATION(conv) ? "displaying-im-msg" : "displaying-chat-msg"),
4880 conv, pmsg));
4881 if (plugin_return)
4883 return;
4886 pidgin_msg = pidgin_message_new(pmsg);
4887 talkatu_history_buffer_write_message(
4888 TALKATU_HISTORY_BUFFER(gtkconv->history_buffer),
4889 TALKATU_MESSAGE(pidgin_msg)
4892 /* Tab highlighting stuff */
4893 if (!(flags & PURPLE_MESSAGE_SEND) && !pidgin_conv_has_focus(conv))
4895 PidginUnseenState unseen = PIDGIN_UNSEEN_NONE;
4897 if ((flags & PURPLE_MESSAGE_NICK) == PURPLE_MESSAGE_NICK)
4898 unseen = PIDGIN_UNSEEN_NICK;
4899 else if (((flags & PURPLE_MESSAGE_SYSTEM) == PURPLE_MESSAGE_SYSTEM) ||
4900 ((flags & PURPLE_MESSAGE_ERROR) == PURPLE_MESSAGE_ERROR))
4901 unseen = PIDGIN_UNSEEN_EVENT;
4902 else if ((flags & PURPLE_MESSAGE_NO_LOG) == PURPLE_MESSAGE_NO_LOG)
4903 unseen = PIDGIN_UNSEEN_NO_LOG;
4904 else
4905 unseen = PIDGIN_UNSEEN_TEXT;
4907 gtkconv_set_unseen(gtkconv, unseen);
4910 /* on rejoin only request message history from after this message */
4911 if (flags & (PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_RECV) &&
4912 PURPLE_IS_CHAT_CONVERSATION(conv)) {
4913 PurpleChat *chat = purple_blist_find_chat(
4914 purple_conversation_get_account(conv),
4915 purple_conversation_get_name(conv));
4916 if (chat) {
4917 GHashTable *comps = purple_chat_get_components(chat);
4918 time_t now, history_since, prev_history_since = 0;
4919 struct tm *history_since_tm;
4920 const char *history_since_s, *prev_history_since_s;
4922 history_since = purple_message_get_time(pmsg) + 1;
4924 prev_history_since_s = g_hash_table_lookup(comps,
4925 "history_since");
4926 if (prev_history_since_s != NULL)
4927 prev_history_since = purple_str_to_time(
4928 prev_history_since_s, TRUE, NULL, NULL,
4929 NULL);
4931 now = time(NULL);
4932 /* in case of incorrectly stored timestamps */
4933 if (prev_history_since > now)
4934 prev_history_since = now;
4935 /* in case of delayed messages */
4936 if (history_since < prev_history_since)
4937 history_since = prev_history_since;
4939 history_since_tm = gmtime(&history_since);
4940 history_since_s = purple_utf8_strftime(
4941 "%Y-%m-%dT%H:%M:%SZ", history_since_tm);
4942 if (!purple_strequal(prev_history_since_s,
4943 history_since_s))
4944 g_hash_table_replace(comps,
4945 g_strdup("history_since"),
4946 g_strdup(history_since_s));
4950 purple_signal_emit(pidgin_conversations_get_handle(),
4951 (PURPLE_IS_IM_CONVERSATION(conv) ? "displayed-im-msg" : "displayed-chat-msg"),
4952 conv, pmsg);
4953 update_typing_message(gtkconv, NULL);
4956 static gboolean get_iter_from_chatuser(PurpleChatUser *cb, GtkTreeIter *iter)
4958 GtkTreeRowReference *ref;
4959 GtkTreePath *path;
4960 GtkTreeModel *model;
4962 g_return_val_if_fail(cb != NULL, FALSE);
4964 ref = purple_chat_user_get_ui_data(cb);
4965 if (!ref)
4966 return FALSE;
4968 if ((path = gtk_tree_row_reference_get_path(ref)) == NULL)
4969 return FALSE;
4971 model = gtk_tree_row_reference_get_model(ref);
4972 if (!gtk_tree_model_get_iter(GTK_TREE_MODEL(model), iter, path)) {
4973 gtk_tree_path_free(path);
4974 return FALSE;
4977 gtk_tree_path_free(path);
4978 return TRUE;
4981 static void
4982 pidgin_conv_chat_add_users(PurpleChatConversation *chat, GList *cbuddies, gboolean new_arrivals)
4984 PidginConversation *gtkconv;
4985 PidginChatPane *gtkchat;
4986 GtkListStore *ls;
4987 GList *l;
4989 char tmp[BUF_LONG];
4990 int num_users;
4992 gtkconv = PIDGIN_CONVERSATION(PURPLE_CONVERSATION(chat));
4993 gtkchat = gtkconv->u.chat;
4995 num_users = purple_chat_conversation_get_users_count(chat);
4997 g_snprintf(tmp, sizeof(tmp),
4998 ngettext("%d person in room", "%d people in room",
4999 num_users),
5000 num_users);
5002 gtk_label_set_text(GTK_LABEL(gtkchat->count), tmp);
5004 ls = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list)));
5006 gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(ls), GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID,
5007 GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID);
5009 l = cbuddies;
5010 while (l != NULL) {
5011 add_chat_user_common(chat, (PurpleChatUser *)l->data, NULL);
5012 l = l->next;
5015 /* Currently GTK+ maintains our sorted list after it's in the tree.
5016 * This may change if it turns out we can manage it faster ourselves.
5018 gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(ls), CHAT_USERS_ALIAS_KEY_COLUMN,
5019 GTK_SORT_ASCENDING);
5022 static void
5023 pidgin_conv_chat_rename_user(PurpleChatConversation *chat, const char *old_name,
5024 const char *new_name, const char *new_alias)
5026 PidginConversation *gtkconv;
5027 PidginChatPane *gtkchat;
5028 PurpleChatUser *old_chatuser, *new_chatuser;
5029 GtkTreeIter iter;
5030 GtkTreeModel *model;
5031 GtkTextTag *tag;
5033 gtkconv = PIDGIN_CONVERSATION(PURPLE_CONVERSATION(chat));
5034 gtkchat = gtkconv->u.chat;
5036 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
5038 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter))
5039 return;
5041 if ((tag = get_buddy_tag(chat, old_name, 0, FALSE)))
5042 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_ITALIC, NULL);
5043 if ((tag = get_buddy_tag(chat, old_name, PURPLE_MESSAGE_NICK, FALSE)))
5044 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_ITALIC, NULL);
5046 old_chatuser = purple_chat_conversation_find_user(chat, old_name);
5047 if (!old_chatuser)
5048 return;
5050 if (get_iter_from_chatuser(old_chatuser, &iter)) {
5051 GtkTreeRowReference *ref = purple_chat_user_get_ui_data(old_chatuser);
5053 gtk_list_store_remove(GTK_LIST_STORE(model), &iter);
5054 gtk_tree_row_reference_free(ref);
5055 purple_chat_user_set_ui_data(old_chatuser, NULL);
5058 g_return_if_fail(new_alias != NULL);
5060 new_chatuser = purple_chat_conversation_find_user(chat, new_name);
5062 add_chat_user_common(chat, new_chatuser, old_name);
5065 static void
5066 pidgin_conv_chat_remove_users(PurpleChatConversation *chat, GList *users)
5068 PidginConversation *gtkconv;
5069 PidginChatPane *gtkchat;
5070 GtkTreeIter iter;
5071 GtkTreeModel *model;
5072 GList *l;
5073 char tmp[BUF_LONG];
5074 int num_users;
5075 gboolean f;
5076 GtkTextTag *tag;
5078 gtkconv = PIDGIN_CONVERSATION(PURPLE_CONVERSATION(chat));
5079 gtkchat = gtkconv->u.chat;
5081 num_users = purple_chat_conversation_get_users_count(chat);
5083 for (l = users; l != NULL; l = l->next) {
5084 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
5086 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter))
5087 /* XXX: Break? */
5088 continue;
5090 do {
5091 char *val;
5093 gtk_tree_model_get(GTK_TREE_MODEL(model), &iter,
5094 CHAT_USERS_NAME_COLUMN, &val, -1);
5096 if (!purple_utf8_strcasecmp((char *)l->data, val)) {
5097 f = gtk_list_store_remove(GTK_LIST_STORE(model), &iter);
5099 else
5100 f = gtk_tree_model_iter_next(GTK_TREE_MODEL(model), &iter);
5102 g_free(val);
5103 } while (f);
5105 if ((tag = get_buddy_tag(chat, l->data, 0, FALSE)))
5106 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_ITALIC, NULL);
5107 if ((tag = get_buddy_tag(chat, l->data, PURPLE_MESSAGE_NICK, FALSE)))
5108 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_ITALIC, NULL);
5111 g_snprintf(tmp, sizeof(tmp),
5112 ngettext("%d person in room", "%d people in room",
5113 num_users), num_users);
5115 gtk_label_set_text(GTK_LABEL(gtkchat->count), tmp);
5118 static void
5119 pidgin_conv_chat_update_user(PurpleChatUser *chatuser)
5121 PurpleChatConversation *chat;
5122 PidginConversation *gtkconv;
5123 PidginChatPane *gtkchat;
5124 GtkTreeIter iter;
5125 GtkTreeModel *model;
5127 if (!chatuser)
5128 return;
5130 chat = purple_chat_user_get_chat(chatuser);
5131 gtkconv = PIDGIN_CONVERSATION(PURPLE_CONVERSATION(chat));
5132 gtkchat = gtkconv->u.chat;
5134 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
5136 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter))
5137 return;
5139 if (get_iter_from_chatuser(chatuser, &iter)) {
5140 GtkTreeRowReference *ref = purple_chat_user_get_ui_data(chatuser);
5141 gtk_list_store_remove(GTK_LIST_STORE(model), &iter);
5142 gtk_tree_row_reference_free(ref);
5143 purple_chat_user_set_ui_data(chatuser, NULL);
5146 add_chat_user_common(chat, chatuser, NULL);
5149 gboolean
5150 pidgin_conv_has_focus(PurpleConversation *conv)
5152 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
5153 PidginConvWindow *win;
5154 gboolean has_focus;
5156 win = gtkconv->win;
5158 g_object_get(G_OBJECT(win->window), "has-toplevel-focus", &has_focus, NULL);
5160 if (has_focus && pidgin_conv_window_is_active_conversation(conv))
5161 return TRUE;
5163 return FALSE;
5167 * Makes sure all the menu items and all the buttons are hidden/shown and
5168 * sensitive/insensitive. This is called after changing tabs and when an
5169 * account signs on or off.
5171 static void
5172 gray_stuff_out(PidginConversation *gtkconv)
5174 PidginConvWindow *win;
5175 PurpleConversation *conv = gtkconv->active_conv;
5176 PurpleConnection *gc;
5177 PurpleProtocol *protocol = NULL;
5178 GdkPixbuf *window_icon = NULL;
5179 // PidginWebViewButtons buttons;
5180 PurpleAccount *account;
5182 win = pidgin_conv_get_window(gtkconv);
5183 gc = purple_conversation_get_connection(conv);
5184 account = purple_conversation_get_account(conv);
5186 if (gc != NULL)
5187 protocol = purple_connection_get_protocol(gc);
5189 if (win->menu->send_to != NULL)
5190 update_send_to_selection(win);
5193 * Handle hiding and showing stuff based on what type of conv this is.
5194 * Stuff that Purple IMs support in general should be shown for IM
5195 * conversations. Stuff that Purple chats support in general should be
5196 * shown for chat conversations. It doesn't matter whether the protocol
5197 * supports it or not--that only affects if the button or menu item
5198 * is sensitive or not.
5200 if (PURPLE_IS_IM_CONVERSATION(conv)) {
5201 /* Show stuff that applies to IMs, hide stuff that applies to chats */
5203 /* Deal with menu items */
5204 gtk_action_set_visible(win->menu->view_log, TRUE);
5205 gtk_action_set_visible(win->menu->send_file, TRUE);
5206 gtk_action_set_visible(win->menu->get_attention, TRUE);
5207 gtk_action_set_visible(win->menu->add_pounce, TRUE);
5208 gtk_action_set_visible(win->menu->get_info, TRUE);
5209 gtk_action_set_visible(win->menu->invite, FALSE);
5210 gtk_action_set_visible(win->menu->alias, TRUE);
5211 if (purple_account_privacy_check(account, purple_conversation_get_name(conv))) {
5212 gtk_action_set_visible(win->menu->unblock, FALSE);
5213 gtk_action_set_visible(win->menu->block, TRUE);
5214 } else {
5215 gtk_action_set_visible(win->menu->block, FALSE);
5216 gtk_action_set_visible(win->menu->unblock, TRUE);
5219 if (purple_blist_find_buddy(account, purple_conversation_get_name(conv)) == NULL) {
5220 gtk_action_set_visible(win->menu->add, TRUE);
5221 gtk_action_set_visible(win->menu->remove, FALSE);
5222 } else {
5223 gtk_action_set_visible(win->menu->remove, TRUE);
5224 gtk_action_set_visible(win->menu->add, FALSE);
5227 gtk_action_set_visible(win->menu->insert_link, TRUE);
5228 gtk_action_set_visible(win->menu->insert_image, TRUE);
5229 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
5230 /* Show stuff that applies to Chats, hide stuff that applies to IMs */
5232 /* Deal with menu items */
5233 gtk_action_set_visible(win->menu->view_log, TRUE);
5234 gtk_action_set_visible(win->menu->send_file, FALSE);
5235 gtk_action_set_visible(win->menu->get_attention, FALSE);
5236 gtk_action_set_visible(win->menu->add_pounce, FALSE);
5237 gtk_action_set_visible(win->menu->get_info, FALSE);
5238 gtk_action_set_visible(win->menu->invite, TRUE);
5239 gtk_action_set_visible(win->menu->alias, TRUE);
5240 gtk_action_set_visible(win->menu->block, FALSE);
5241 gtk_action_set_visible(win->menu->unblock, FALSE);
5243 if ((account == NULL) || purple_blist_find_chat(account, purple_conversation_get_name(conv)) == NULL) {
5244 /* If the chat is NOT in the buddy list */
5245 gtk_action_set_visible(win->menu->add, TRUE);
5246 gtk_action_set_visible(win->menu->remove, FALSE);
5247 } else {
5248 /* If the chat IS in the buddy list */
5249 gtk_action_set_visible(win->menu->add, FALSE);
5250 gtk_action_set_visible(win->menu->remove, TRUE);
5253 gtk_action_set_visible(win->menu->insert_link, TRUE);
5254 gtk_action_set_visible(win->menu->insert_image, TRUE);
5258 * Handle graying stuff out based on whether an account is connected
5259 * and what features that account supports.
5261 if ((gc != NULL) &&
5262 (!PURPLE_IS_CHAT_CONVERSATION(conv) ||
5263 !purple_chat_conversation_has_left(PURPLE_CHAT_CONVERSATION(conv)) ))
5265 PurpleConnectionFlags features = purple_conversation_get_features(conv);
5266 /* Account is online */
5267 /* Deal with the toolbar */
5268 #if 0
5269 if (features & PURPLE_CONNECTION_FLAG_HTML)
5271 buttons = PIDGIN_WEBVIEW_ALL; /* Everything on */
5272 if (features & PURPLE_CONNECTION_FLAG_NO_BGCOLOR)
5273 buttons &= ~PIDGIN_WEBVIEW_BACKCOLOR;
5274 if (features & PURPLE_CONNECTION_FLAG_NO_FONTSIZE)
5276 buttons &= ~PIDGIN_WEBVIEW_GROW;
5277 buttons &= ~PIDGIN_WEBVIEW_SHRINK;
5279 if (features & PURPLE_CONNECTION_FLAG_NO_URLDESC)
5280 buttons &= ~PIDGIN_WEBVIEW_LINKDESC
5281 } else {
5282 buttons = PIDGIN_WEBVIEW_SMILEY | PIDGIN_WEBVIEW_IMAGE;
5285 if (features & PURPLE_CONNECTION_FLAG_NO_IMAGES)
5286 buttons &= ~PIDGIN_WEBVIEW_IMAGE;
5288 if (features & PURPLE_CONNECTION_FLAG_ALLOW_CUSTOM_SMILEY)
5289 buttons |= PIDGIN_WEBVIEW_CUSTOM_SMILEY;
5290 else
5291 buttons &= ~PIDGIN_WEBVIEW_CUSTOM_SMILEY;
5293 pidgin_webview_set_format_functions(PIDGIN_WEBVIEW(gtkconv->entry), buttons);
5294 #endif
5296 /* Deal with menu items */
5297 gtk_action_set_sensitive(win->menu->view_log, TRUE);
5298 gtk_action_set_sensitive(win->menu->add_pounce, TRUE);
5299 gtk_action_set_sensitive(win->menu->get_info, (PURPLE_PROTOCOL_IMPLEMENTS(protocol, SERVER, get_info)));
5300 gtk_action_set_sensitive(win->menu->invite, (PURPLE_PROTOCOL_IMPLEMENTS(protocol, CHAT, invite)));
5301 gtk_action_set_sensitive(win->menu->insert_link, (features & PURPLE_CONNECTION_FLAG_HTML));
5302 gtk_action_set_sensitive(win->menu->insert_image, !(features & PURPLE_CONNECTION_FLAG_NO_IMAGES));
5304 if (PURPLE_IS_IM_CONVERSATION(conv))
5306 gboolean can_send_file = FALSE;
5307 const gchar *name = purple_conversation_get_name(conv);
5309 if (PURPLE_IS_PROTOCOL_XFER(protocol) &&
5310 purple_protocol_xfer_can_receive(PURPLE_PROTOCOL_XFER(protocol), gc, name)
5312 can_send_file = TRUE;
5315 gtk_action_set_sensitive(win->menu->add, (PURPLE_PROTOCOL_IMPLEMENTS(protocol, SERVER, add_buddy)));
5316 gtk_action_set_sensitive(win->menu->remove, (PURPLE_PROTOCOL_IMPLEMENTS(protocol, SERVER, remove_buddy)));
5317 gtk_action_set_sensitive(win->menu->send_file, can_send_file);
5318 gtk_action_set_sensitive(win->menu->get_attention, (PURPLE_IS_PROTOCOL_ATTENTION(protocol)));
5319 gtk_action_set_sensitive(win->menu->alias,
5320 (account != NULL) &&
5321 (purple_blist_find_buddy(account, purple_conversation_get_name(conv)) != NULL));
5323 else if (PURPLE_IS_CHAT_CONVERSATION(conv))
5325 gtk_action_set_sensitive(win->menu->add, (PURPLE_PROTOCOL_IMPLEMENTS(protocol, CHAT, join)));
5326 gtk_action_set_sensitive(win->menu->remove, (PURPLE_PROTOCOL_IMPLEMENTS(protocol, CHAT, join)));
5327 gtk_action_set_sensitive(win->menu->alias,
5328 (account != NULL) &&
5329 (purple_blist_find_chat(account, purple_conversation_get_name(conv)) != NULL));
5332 } else {
5333 /* Account is offline */
5334 /* Or it's a chat that we've left. */
5336 /* Then deal with menu items */
5337 gtk_action_set_sensitive(win->menu->view_log, TRUE);
5338 gtk_action_set_sensitive(win->menu->send_file, FALSE);
5339 gtk_action_set_sensitive(win->menu->get_attention, FALSE);
5340 gtk_action_set_sensitive(win->menu->add_pounce, TRUE);
5341 gtk_action_set_sensitive(win->menu->get_info, FALSE);
5342 gtk_action_set_sensitive(win->menu->invite, FALSE);
5343 gtk_action_set_sensitive(win->menu->alias, FALSE);
5344 gtk_action_set_sensitive(win->menu->add, FALSE);
5345 gtk_action_set_sensitive(win->menu->remove, FALSE);
5346 gtk_action_set_sensitive(win->menu->insert_link, TRUE);
5347 gtk_action_set_sensitive(win->menu->insert_image, FALSE);
5351 * Update the window's icon
5353 if (pidgin_conv_window_is_active_conversation(conv))
5355 GList *l = NULL;
5356 if (PURPLE_IS_IM_CONVERSATION(conv) &&
5357 (gtkconv->u.im->anim))
5359 PurpleBuddy *buddy = purple_blist_find_buddy(purple_conversation_get_account(conv), purple_conversation_get_name(conv));
5360 window_icon =
5361 gdk_pixbuf_animation_get_static_image(gtkconv->u.im->anim);
5363 if (buddy && !PURPLE_BUDDY_IS_ONLINE(buddy))
5364 gdk_pixbuf_saturate_and_pixelate(window_icon, window_icon, 0.0, FALSE);
5366 g_object_ref(window_icon);
5367 l = g_list_append(l, window_icon);
5368 } else {
5369 l = pidgin_conv_get_tab_icons(conv);
5371 gtk_window_set_icon_list(GTK_WINDOW(win->window), l);
5372 if (window_icon != NULL) {
5373 g_object_unref(G_OBJECT(window_icon));
5374 g_list_free(l);
5379 static void
5380 pidgin_conv_update_fields(PurpleConversation *conv, PidginConvFields fields)
5382 PidginConversation *gtkconv;
5383 PidginConvWindow *win;
5385 gtkconv = PIDGIN_CONVERSATION(conv);
5386 if (!gtkconv)
5387 return;
5388 win = pidgin_conv_get_window(gtkconv);
5389 if (!win)
5390 return;
5392 if (fields & PIDGIN_CONV_SET_TITLE)
5394 purple_conversation_autoset_title(conv);
5397 if (fields & PIDGIN_CONV_BUDDY_ICON)
5399 if (PURPLE_IS_IM_CONVERSATION(conv))
5400 pidgin_conv_update_buddy_icon(PURPLE_IM_CONVERSATION(conv));
5403 if (fields & PIDGIN_CONV_MENU)
5405 gray_stuff_out(PIDGIN_CONVERSATION(conv));
5406 generate_send_to_items(win);
5407 regenerate_plugins_items(win);
5410 if (fields & PIDGIN_CONV_E2EE)
5411 generate_e2ee_controls(win);
5413 if (fields & PIDGIN_CONV_TAB_ICON)
5415 update_tab_icon(conv);
5416 generate_send_to_items(win); /* To update the icons in SendTo menu */
5419 if ((fields & PIDGIN_CONV_TOPIC) &&
5420 PURPLE_IS_CHAT_CONVERSATION(conv))
5422 const char *topic;
5423 PidginChatPane *gtkchat = gtkconv->u.chat;
5425 if (gtkchat->topic_text != NULL)
5427 topic = purple_chat_conversation_get_topic(PURPLE_CHAT_CONVERSATION(conv));
5429 gtk_entry_set_text(GTK_ENTRY(gtkchat->topic_text), topic ? topic : "");
5430 gtk_widget_set_tooltip_text(gtkchat->topic_text,
5431 topic ? topic : "");
5435 if ((fields & PIDGIN_CONV_COLORIZE_TITLE) ||
5436 (fields & PIDGIN_CONV_SET_TITLE) ||
5437 (fields & PIDGIN_CONV_TOPIC))
5439 char *title;
5440 PurpleIMConversation *im = NULL;
5441 PurpleAccount *account = purple_conversation_get_account(conv);
5442 PurpleBuddy *buddy = NULL;
5443 char *markup = NULL;
5444 AtkObject *accessibility_obj;
5445 /* I think this is a little longer than it needs to be but I'm lazy. */
5446 char *style;
5448 if (PURPLE_IS_IM_CONVERSATION(conv))
5449 im = PURPLE_IM_CONVERSATION(conv);
5451 if ((account == NULL) ||
5452 !purple_account_is_connected(account) ||
5453 (PURPLE_IS_CHAT_CONVERSATION(conv)
5454 && purple_chat_conversation_has_left(PURPLE_CHAT_CONVERSATION(conv))))
5455 title = g_strdup_printf("(%s)", purple_conversation_get_title(conv));
5456 else
5457 title = g_strdup(purple_conversation_get_title(conv));
5459 if (PURPLE_IS_IM_CONVERSATION(conv)) {
5460 buddy = purple_blist_find_buddy(account, purple_conversation_get_name(conv));
5461 if (buddy) {
5462 markup = pidgin_blist_get_name_markup(buddy, FALSE, FALSE);
5463 } else {
5464 markup = title;
5466 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
5467 const char *topic = gtkconv->u.chat->topic_text
5468 ? gtk_entry_get_text(GTK_ENTRY(gtkconv->u.chat->topic_text))
5469 : NULL;
5470 const char *title = purple_conversation_get_title(conv);
5471 const char *name = purple_conversation_get_name(conv);
5473 char *topic_esc, *unaliased, *unaliased_esc, *title_esc;
5475 topic_esc = topic ? g_markup_escape_text(topic, -1) : NULL;
5476 unaliased = g_utf8_collate(title, name) ? g_strdup_printf("(%s)", name) : NULL;
5477 unaliased_esc = unaliased ? g_markup_escape_text(unaliased, -1) : NULL;
5478 title_esc = g_markup_escape_text(title, -1);
5480 markup = g_strdup_printf("%s%s<span size='smaller'>%s</span>%s<span color='%s' size='smaller'>%s</span>",
5481 title_esc,
5482 unaliased_esc ? " " : "",
5483 unaliased_esc ? unaliased_esc : "",
5484 topic_esc && *topic_esc ? "\n" : "",
5485 pidgin_get_dim_grey_string(gtkconv->infopane),
5486 topic_esc ? topic_esc : "");
5488 g_free(title_esc);
5489 g_free(topic_esc);
5490 g_free(unaliased);
5491 g_free(unaliased_esc);
5493 gtk_list_store_set(gtkconv->infopane_model, &(gtkconv->infopane_iter),
5494 CONV_TEXT_COLUMN, markup, -1);
5495 /* XXX seanegan Why do I have to do this? */
5496 gtk_widget_queue_draw(gtkconv->infopane);
5498 if (title != markup)
5499 g_free(markup);
5501 if (!gtk_widget_get_realized(gtkconv->tab_label))
5502 gtk_widget_realize(gtkconv->tab_label);
5504 accessibility_obj = gtk_widget_get_accessible(gtkconv->tab_cont);
5505 if (im != NULL &&
5506 purple_im_conversation_get_typing_state(im) == PURPLE_IM_TYPING) {
5507 atk_object_set_description(accessibility_obj, _("Typing"));
5508 style = "tab-label-typing";
5509 } else if (im != NULL &&
5510 purple_im_conversation_get_typing_state(im) == PURPLE_IM_TYPED) {
5511 atk_object_set_description(accessibility_obj, _("Stopped Typing"));
5512 style = "tab-label-typed";
5513 } else if (gtkconv->unseen_state == PIDGIN_UNSEEN_NICK) {
5514 atk_object_set_description(accessibility_obj, _("Nick Said"));
5515 style = "tab-label-attention";
5516 } else if (gtkconv->unseen_state == PIDGIN_UNSEEN_TEXT) {
5517 atk_object_set_description(accessibility_obj, _("Unread Messages"));
5518 if (PURPLE_IS_CHAT_CONVERSATION(gtkconv->active_conv))
5519 style = "tab-label-unreadchat";
5520 else
5521 style = "tab-label-attention";
5522 } else if (gtkconv->unseen_state == PIDGIN_UNSEEN_EVENT) {
5523 atk_object_set_description(accessibility_obj, _("New Event"));
5524 style = "tab-label-event";
5525 } else {
5526 style = "tab-label";
5529 gtk_widget_set_name(gtkconv->tab_label, style);
5530 gtk_label_set_text(GTK_LABEL(gtkconv->tab_label), title);
5531 gtk_widget_set_state_flags(gtkconv->tab_label, GTK_STATE_FLAG_ACTIVE, TRUE);
5533 if (gtkconv->unseen_state == PIDGIN_UNSEEN_TEXT ||
5534 gtkconv->unseen_state == PIDGIN_UNSEEN_NICK ||
5535 gtkconv->unseen_state == PIDGIN_UNSEEN_EVENT) {
5536 PangoAttrList *list = pango_attr_list_new();
5537 PangoAttribute *attr = pango_attr_weight_new(PANGO_WEIGHT_BOLD);
5538 attr->start_index = 0;
5539 attr->end_index = -1;
5540 pango_attr_list_insert(list, attr);
5541 gtk_label_set_attributes(GTK_LABEL(gtkconv->tab_label), list);
5542 pango_attr_list_unref(list);
5543 } else
5544 gtk_label_set_attributes(GTK_LABEL(gtkconv->tab_label), NULL);
5546 if (pidgin_conv_window_is_active_conversation(conv))
5547 update_typing_icon(gtkconv);
5549 gtk_label_set_text(GTK_LABEL(gtkconv->menu_label), title);
5550 if (pidgin_conv_window_is_active_conversation(conv)) {
5551 const char* current_title = gtk_window_get_title(GTK_WINDOW(win->window));
5552 if (current_title == NULL || !purple_strequal(current_title, title))
5553 gtk_window_set_title(GTK_WINDOW(win->window), title);
5556 g_free(title);
5560 static void
5561 pidgin_conv_updated(PurpleConversation *conv, PurpleConversationUpdateType type)
5563 PidginConvFields flags = 0;
5565 g_return_if_fail(conv != NULL);
5567 if (type == PURPLE_CONVERSATION_UPDATE_ACCOUNT)
5569 flags = PIDGIN_CONV_ALL;
5571 else if (type == PURPLE_CONVERSATION_UPDATE_TYPING ||
5572 type == PURPLE_CONVERSATION_UPDATE_UNSEEN ||
5573 type == PURPLE_CONVERSATION_UPDATE_TITLE)
5575 flags = PIDGIN_CONV_COLORIZE_TITLE;
5577 else if (type == PURPLE_CONVERSATION_UPDATE_TOPIC)
5579 flags = PIDGIN_CONV_TOPIC;
5581 else if (type == PURPLE_CONVERSATION_ACCOUNT_ONLINE ||
5582 type == PURPLE_CONVERSATION_ACCOUNT_OFFLINE)
5584 flags = PIDGIN_CONV_MENU | PIDGIN_CONV_TAB_ICON | PIDGIN_CONV_SET_TITLE;
5586 else if (type == PURPLE_CONVERSATION_UPDATE_AWAY)
5588 flags = PIDGIN_CONV_TAB_ICON;
5590 else if (type == PURPLE_CONVERSATION_UPDATE_ADD ||
5591 type == PURPLE_CONVERSATION_UPDATE_REMOVE ||
5592 type == PURPLE_CONVERSATION_UPDATE_CHATLEFT)
5594 flags = PIDGIN_CONV_SET_TITLE | PIDGIN_CONV_MENU;
5596 else if (type == PURPLE_CONVERSATION_UPDATE_ICON)
5598 flags = PIDGIN_CONV_BUDDY_ICON;
5600 else if (type == PURPLE_CONVERSATION_UPDATE_FEATURES)
5602 flags = PIDGIN_CONV_MENU;
5604 else if (type == PURPLE_CONVERSATION_UPDATE_E2EE)
5606 flags = PIDGIN_CONV_E2EE | PIDGIN_CONV_MENU;
5609 pidgin_conv_update_fields(conv, flags);
5612 static void
5613 wrote_msg_update_unseen_cb(PurpleConversation *conv, PurpleMessage *msg,
5614 gpointer _unused)
5616 PidginConversation *gtkconv = conv ? PIDGIN_CONVERSATION(conv) : NULL;
5617 PurpleMessageFlags flags;
5618 if (conv == NULL || (gtkconv && gtkconv->win != hidden_convwin))
5619 return;
5620 flags = purple_message_get_flags(msg);
5621 if (flags & (PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_RECV)) {
5622 PidginUnseenState unseen = PIDGIN_UNSEEN_NONE;
5624 if ((flags & PURPLE_MESSAGE_NICK) == PURPLE_MESSAGE_NICK)
5625 unseen = PIDGIN_UNSEEN_NICK;
5626 else if (((flags & PURPLE_MESSAGE_SYSTEM) == PURPLE_MESSAGE_SYSTEM) ||
5627 ((flags & PURPLE_MESSAGE_ERROR) == PURPLE_MESSAGE_ERROR))
5628 unseen = PIDGIN_UNSEEN_EVENT;
5629 else if ((flags & PURPLE_MESSAGE_NO_LOG) == PURPLE_MESSAGE_NO_LOG)
5630 unseen = PIDGIN_UNSEEN_NO_LOG;
5631 else
5632 unseen = PIDGIN_UNSEEN_TEXT;
5634 conv_set_unseen(conv, unseen);
5638 static PurpleConversationUiOps conversation_ui_ops =
5640 pidgin_conv_new,
5641 pidgin_conv_destroy, /* destroy_conversation */
5642 NULL, /* write_chat */
5643 NULL, /* write_im */
5644 pidgin_conv_write_conv, /* write_conv */
5645 pidgin_conv_chat_add_users, /* chat_add_users */
5646 pidgin_conv_chat_rename_user, /* chat_rename_user */
5647 pidgin_conv_chat_remove_users, /* chat_remove_users */
5648 pidgin_conv_chat_update_user, /* chat_update_user */
5649 pidgin_conv_present_conversation, /* present */
5650 pidgin_conv_has_focus, /* has_focus */
5651 NULL, /* send_confirm */
5652 NULL,
5653 NULL,
5654 NULL,
5655 NULL
5658 PurpleConversationUiOps *
5659 pidgin_conversations_get_conv_ui_ops(void)
5661 return &conversation_ui_ops;
5664 /**************************************************************************
5665 * Public conversation utility functions
5666 **************************************************************************/
5667 void
5668 pidgin_conv_update_buddy_icon(PurpleIMConversation *im)
5670 PidginConversation *gtkconv;
5671 PurpleConversation *conv;
5672 PidginConvWindow *win;
5674 PurpleBuddy *buddy;
5676 PurpleImage *custom_img = NULL;
5677 gconstpointer data = NULL;
5678 size_t len;
5680 GdkPixbuf *buf;
5682 GList *children;
5683 GtkWidget *event;
5684 GdkPixbuf *scale;
5685 int scale_width, scale_height;
5686 int size = 0;
5688 PurpleAccount *account;
5690 PurpleBuddyIcon *icon;
5692 conv = PURPLE_CONVERSATION(im);
5694 g_return_if_fail(conv != NULL);
5695 g_return_if_fail(PIDGIN_IS_PIDGIN_CONVERSATION(conv));
5697 gtkconv = PIDGIN_CONVERSATION(conv);
5698 win = gtkconv->win;
5699 if (conv != gtkconv->active_conv)
5700 return;
5702 if (!gtkconv->u.im->show_icon)
5703 return;
5705 account = purple_conversation_get_account(conv);
5707 /* Remove the current icon stuff */
5708 children = gtk_container_get_children(GTK_CONTAINER(gtkconv->u.im->icon_container));
5709 if (children) {
5710 /* We know there's only one child here. It'd be nice to shortcut to the
5711 event box, but we can't change the PidginConversation until 3.0 */
5712 event = (GtkWidget *)children->data;
5713 gtk_container_remove(GTK_CONTAINER(gtkconv->u.im->icon_container), event);
5714 g_list_free(children);
5717 if (gtkconv->u.im->anim != NULL)
5718 g_object_unref(G_OBJECT(gtkconv->u.im->anim));
5720 gtkconv->u.im->anim = NULL;
5722 if (gtkconv->u.im->icon_timer != 0)
5723 g_source_remove(gtkconv->u.im->icon_timer);
5725 gtkconv->u.im->icon_timer = 0;
5727 if (gtkconv->u.im->iter != NULL)
5728 g_object_unref(G_OBJECT(gtkconv->u.im->iter));
5730 gtkconv->u.im->iter = NULL;
5732 if (!purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons"))
5733 return;
5735 if (purple_conversation_get_connection(conv) == NULL)
5736 return;
5738 buddy = purple_blist_find_buddy(account, purple_conversation_get_name(conv));
5739 if (buddy)
5741 PurpleContact *contact = purple_buddy_get_contact(buddy);
5742 if (contact) {
5743 custom_img = purple_buddy_icons_node_find_custom_icon((PurpleBlistNode*)contact);
5744 if (custom_img) {
5745 /* There is a custom icon for this user */
5746 data = purple_image_get_data(custom_img);
5747 len = purple_image_get_data_size(custom_img);
5752 if (data == NULL) {
5753 icon = purple_im_conversation_get_icon(im);
5754 if (icon == NULL)
5756 gtk_widget_set_size_request(gtkconv->u.im->icon_container,
5757 -1, BUDDYICON_SIZE_MIN);
5758 return;
5761 data = purple_buddy_icon_get_data(icon, &len);
5762 if (data == NULL)
5764 gtk_widget_set_size_request(gtkconv->u.im->icon_container,
5765 -1, BUDDYICON_SIZE_MIN);
5766 return;
5770 gtkconv->u.im->anim = pidgin_pixbuf_anim_from_data(data, len);
5771 if (custom_img)
5772 g_object_unref(custom_img);
5774 if (!gtkconv->u.im->anim) {
5775 purple_debug_error("gtkconv", "Couldn't load icon for conv %s\n",
5776 purple_conversation_get_name(conv));
5777 return;
5780 if (gdk_pixbuf_animation_is_static_image(gtkconv->u.im->anim)) {
5781 GdkPixbuf *stat;
5782 gtkconv->u.im->iter = NULL;
5783 stat = gdk_pixbuf_animation_get_static_image(gtkconv->u.im->anim);
5784 buf = gdk_pixbuf_add_alpha(stat, FALSE, 0, 0, 0);
5785 } else {
5786 GdkPixbuf *stat;
5787 gtkconv->u.im->iter =
5788 gdk_pixbuf_animation_get_iter(gtkconv->u.im->anim, NULL); /* LEAK */
5789 stat = gdk_pixbuf_animation_iter_get_pixbuf(gtkconv->u.im->iter);
5790 buf = gdk_pixbuf_add_alpha(stat, FALSE, 0, 0, 0);
5791 if (gtkconv->u.im->animate)
5792 start_anim(NULL, gtkconv);
5795 scale_width = gdk_pixbuf_get_width(buf);
5796 scale_height = gdk_pixbuf_get_height(buf);
5798 gtk_widget_get_size_request(gtkconv->u.im->icon_container, NULL, &size);
5799 size = MIN(size, MIN(scale_width, scale_height));
5801 /* Some sanity checks */
5802 size = CLAMP(size, BUDDYICON_SIZE_MIN, BUDDYICON_SIZE_MAX);
5803 if (scale_width == scale_height) {
5804 scale_width = scale_height = size;
5805 } else if (scale_height > scale_width) {
5806 scale_width = size * scale_width / scale_height;
5807 scale_height = size;
5808 } else {
5809 scale_height = size * scale_height / scale_width;
5810 scale_width = size;
5812 scale = gdk_pixbuf_scale_simple(buf, scale_width, scale_height,
5813 GDK_INTERP_BILINEAR);
5814 g_object_unref(buf);
5815 if (pidgin_gdk_pixbuf_is_opaque(scale))
5816 pidgin_gdk_pixbuf_make_round(scale);
5818 event = gtk_event_box_new();
5819 gtk_container_add(GTK_CONTAINER(gtkconv->u.im->icon_container), event);
5820 gtk_event_box_set_visible_window(GTK_EVENT_BOX(event), FALSE);
5821 gtk_widget_add_events(event,
5822 GDK_POINTER_MOTION_MASK | GDK_LEAVE_NOTIFY_MASK);
5823 g_signal_connect(G_OBJECT(event), "button-press-event",
5824 G_CALLBACK(icon_menu), gtkconv);
5826 pidgin_tooltip_setup_for_widget(event, gtkconv, pidgin_conv_create_tooltip, NULL);
5827 gtk_widget_show(event);
5829 gtkconv->u.im->icon = gtk_image_new_from_pixbuf(scale);
5830 gtk_container_add(GTK_CONTAINER(event), gtkconv->u.im->icon);
5831 gtk_widget_show(gtkconv->u.im->icon);
5833 g_object_unref(G_OBJECT(scale));
5835 /* The buddy icon code needs badly to be fixed. */
5836 if(pidgin_conv_window_is_active_conversation(conv))
5838 buf = gdk_pixbuf_animation_get_static_image(gtkconv->u.im->anim);
5839 if (buddy && !PURPLE_BUDDY_IS_ONLINE(buddy))
5840 gdk_pixbuf_saturate_and_pixelate(buf, buf, 0.0, FALSE);
5841 gtk_window_set_icon(GTK_WINDOW(win->window), buf);
5845 void
5846 pidgin_conv_update_buttons_by_protocol(PurpleConversation *conv)
5848 PidginConvWindow *win;
5850 if (!PIDGIN_IS_PIDGIN_CONVERSATION(conv))
5851 return;
5853 win = PIDGIN_CONVERSATION(conv)->win;
5855 if (win != NULL && pidgin_conv_window_is_active_conversation(conv))
5856 gray_stuff_out(PIDGIN_CONVERSATION(conv));
5859 static gboolean
5860 pidgin_conv_xy_to_right_infopane(PidginConvWindow *win, int x, int y)
5862 gint pane_x, pane_y, x_rel;
5863 PidginConversation *gtkconv;
5864 GtkAllocation allocation;
5866 gdk_window_get_origin(gtk_widget_get_window(win->notebook),
5867 &pane_x, &pane_y);
5868 x_rel = x - pane_x;
5869 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
5870 gtk_widget_get_allocation(gtkconv->infopane, &allocation);
5871 return (x_rel > allocation.x + allocation.width / 2);
5875 pidgin_conv_get_tab_at_xy(PidginConvWindow *win, int x, int y, gboolean *to_right)
5877 gint nb_x, nb_y, x_rel, y_rel;
5878 GtkNotebook *notebook;
5879 GtkWidget *page, *tab;
5880 gint i, page_num = -1;
5881 gint count;
5882 gboolean horiz;
5884 if (to_right)
5885 *to_right = FALSE;
5887 notebook = GTK_NOTEBOOK(win->notebook);
5889 gdk_window_get_origin(gtk_widget_get_window(win->notebook), &nb_x, &nb_y);
5890 x_rel = x - nb_x;
5891 y_rel = y - nb_y;
5893 horiz = (gtk_notebook_get_tab_pos(notebook) == GTK_POS_TOP ||
5894 gtk_notebook_get_tab_pos(notebook) == GTK_POS_BOTTOM);
5896 count = gtk_notebook_get_n_pages(GTK_NOTEBOOK(notebook));
5898 for (i = 0; i < count; i++) {
5899 GtkAllocation allocation;
5901 page = gtk_notebook_get_nth_page(GTK_NOTEBOOK(notebook), i);
5902 tab = gtk_notebook_get_tab_label(GTK_NOTEBOOK(notebook), page);
5903 gtk_widget_get_allocation(tab, &allocation);
5905 /* Make sure the tab is not hidden beyond an arrow */
5906 if (!gtk_widget_is_drawable(tab) && gtk_notebook_get_show_tabs(notebook))
5907 continue;
5909 if (horiz) {
5910 if (x_rel >= allocation.x - PIDGIN_HIG_BOX_SPACE &&
5911 x_rel <= allocation.x + allocation.width + PIDGIN_HIG_BOX_SPACE) {
5912 page_num = i;
5914 if (to_right && x_rel >= allocation.x + allocation.width/2)
5915 *to_right = TRUE;
5917 break;
5919 } else {
5920 if (y_rel >= allocation.y - PIDGIN_HIG_BOX_SPACE &&
5921 y_rel <= allocation.y + allocation.height + PIDGIN_HIG_BOX_SPACE) {
5922 page_num = i;
5924 if (to_right && y_rel >= allocation.y + allocation.height/2)
5925 *to_right = TRUE;
5927 break;
5932 if (page_num == -1) {
5933 /* Add after the last tab */
5934 page_num = count - 1;
5937 return page_num;
5940 static void
5941 close_on_tabs_pref_cb(const char *name, PurplePrefType type,
5942 gconstpointer value, gpointer data)
5944 GList *l;
5945 PurpleConversation *conv;
5946 PidginConversation *gtkconv;
5948 for (l = purple_conversations_get_all(); l != NULL; l = l->next) {
5949 conv = (PurpleConversation *)l->data;
5951 if (!PIDGIN_IS_PIDGIN_CONVERSATION(conv))
5952 continue;
5954 gtkconv = PIDGIN_CONVERSATION(conv);
5956 if (value)
5957 gtk_widget_show(gtkconv->close);
5958 else
5959 gtk_widget_hide(gtkconv->close);
5963 static void
5964 spellcheck_pref_cb(const char *name, PurplePrefType type,
5965 gconstpointer value, gpointer data)
5967 GList *cl;
5968 PurpleConversation *conv;
5969 PidginConversation *gtkconv;
5971 for (cl = purple_conversations_get_all(); cl != NULL; cl = cl->next) {
5973 conv = (PurpleConversation *)cl->data;
5975 if (!PIDGIN_IS_PIDGIN_CONVERSATION(conv))
5976 continue;
5978 gtkconv = PIDGIN_CONVERSATION(conv);
5980 # warning toggle spell checking when talkatu #60 is done.
5984 static void
5985 tab_side_pref_cb(const char *name, PurplePrefType type,
5986 gconstpointer value, gpointer data)
5988 GList *gtkwins, *gtkconvs;
5989 GtkPositionType pos;
5990 PidginConvWindow *gtkwin;
5992 pos = GPOINTER_TO_INT(value);
5994 for (gtkwins = pidgin_conv_windows_get_list(); gtkwins != NULL; gtkwins = gtkwins->next) {
5995 gtkwin = gtkwins->data;
5996 gtk_notebook_set_tab_pos(GTK_NOTEBOOK(gtkwin->notebook), pos&~8);
5997 for (gtkconvs = gtkwin->gtkconvs; gtkconvs != NULL; gtkconvs = gtkconvs->next) {
5998 pidgin_conv_tab_pack(gtkwin, gtkconvs->data);
6003 static void
6004 show_formatting_toolbar_pref_cb(const char *name, PurplePrefType type,
6005 gconstpointer value, gpointer data)
6007 GList *l;
6008 PurpleConversation *conv;
6009 PidginConversation *gtkconv;
6010 PidginConvWindow *win;
6011 gboolean visible = (gboolean)GPOINTER_TO_INT(value);
6013 for (l = purple_conversations_get_all(); l != NULL; l = l->next)
6015 conv = (PurpleConversation *)l->data;
6017 if (!PIDGIN_IS_PIDGIN_CONVERSATION(conv))
6018 continue;
6020 gtkconv = PIDGIN_CONVERSATION(conv);
6021 win = gtkconv->win;
6023 gtk_toggle_action_set_active(
6024 GTK_TOGGLE_ACTION(win->menu->show_formatting_toolbar),
6025 visible
6028 talkatu_editor_set_toolbar_visible(TALKATU_EDITOR(gtkconv->editor), visible);
6032 static void
6033 animate_buddy_icons_pref_cb(const char *name, PurplePrefType type,
6034 gconstpointer value, gpointer data)
6036 GList *l;
6037 PurpleConversation *conv;
6038 PidginConversation *gtkconv;
6039 PidginConvWindow *win;
6041 if (!purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons"))
6042 return;
6044 /* Set the "animate" flag for each icon based on the new preference */
6045 for (l = purple_conversations_get_ims(); l != NULL; l = l->next) {
6046 conv = (PurpleConversation *)l->data;
6047 gtkconv = PIDGIN_CONVERSATION(conv);
6048 if (gtkconv)
6049 gtkconv->u.im->animate = GPOINTER_TO_INT(value);
6052 /* Now either stop or start animation for the active conversation in each window */
6053 for (l = pidgin_conv_windows_get_list(); l != NULL; l = l->next) {
6054 win = l->data;
6055 conv = pidgin_conv_window_get_active_conversation(win);
6056 pidgin_conv_update_buddy_icon(PURPLE_IM_CONVERSATION(conv));
6060 static void
6061 show_buddy_icons_pref_cb(const char *name, PurplePrefType type,
6062 gconstpointer value, gpointer data)
6064 GList *l;
6066 for (l = purple_conversations_get_all(); l != NULL; l = l->next) {
6067 PurpleConversation *conv = l->data;
6068 if (!PIDGIN_CONVERSATION(conv))
6069 continue;
6070 if (GPOINTER_TO_INT(value))
6071 gtk_widget_show(PIDGIN_CONVERSATION(conv)->infopane_hbox);
6072 else
6073 gtk_widget_hide(PIDGIN_CONVERSATION(conv)->infopane_hbox);
6075 if (PURPLE_IS_IM_CONVERSATION(conv)) {
6076 pidgin_conv_update_buddy_icon(PURPLE_IM_CONVERSATION(conv));
6080 /* Make the tabs show/hide correctly */
6081 for (l = pidgin_conv_windows_get_list(); l != NULL; l = l->next) {
6082 PidginConvWindow *win = l->data;
6083 if (pidgin_conv_window_get_gtkconv_count(win) == 1)
6084 gtk_notebook_set_show_tabs(GTK_NOTEBOOK(win->notebook),
6085 GPOINTER_TO_INT(value) == 0);
6089 static void
6090 show_protocol_icons_pref_cb(const char *name, PurplePrefType type,
6091 gconstpointer value, gpointer data)
6093 GList *l;
6094 for (l = purple_conversations_get_all(); l != NULL; l = l->next) {
6095 PurpleConversation *conv = l->data;
6096 if (PIDGIN_CONVERSATION(conv))
6097 update_tab_icon(conv);
6101 static void
6102 conv_placement_usetabs_cb(const char *name, PurplePrefType type,
6103 gconstpointer value, gpointer data)
6105 purple_prefs_trigger_callback(PIDGIN_PREFS_ROOT "/conversations/placement");
6108 static void
6109 account_status_changed_cb(PurpleAccount *account, PurpleStatus *oldstatus,
6110 PurpleStatus *newstatus)
6112 GList *l;
6113 PurpleConversation *conv = NULL;
6114 PidginConversation *gtkconv;
6116 if(!purple_strequal(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new"), "away"))
6117 return;
6119 if(purple_status_is_available(oldstatus) || !purple_status_is_available(newstatus))
6120 return;
6122 for (l = hidden_convwin->gtkconvs; l; ) {
6123 gtkconv = l->data;
6124 l = l->next;
6126 conv = gtkconv->active_conv;
6127 if (PURPLE_IS_CHAT_CONVERSATION(conv) ||
6128 account != purple_conversation_get_account(conv))
6129 continue;
6131 pidgin_conv_attach_to_conversation(conv);
6133 /* TODO: do we need to do anything for any other conversations that are in the same gtkconv here?
6134 * I'm a little concerned that not doing so will cause the "pending" indicator in the gtkblist not to be cleared. -DAA*/
6135 purple_conversation_update(conv, PURPLE_CONVERSATION_UPDATE_UNSEEN);
6139 static void
6140 hide_new_pref_cb(const char *name, PurplePrefType type,
6141 gconstpointer value, gpointer data)
6143 GList *l;
6144 PurpleConversation *conv = NULL;
6145 PidginConversation *gtkconv;
6146 gboolean when_away = FALSE;
6148 if(!hidden_convwin)
6149 return;
6151 if(purple_strequal(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new"), "always"))
6152 return;
6154 if(purple_strequal(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new"), "away"))
6155 when_away = TRUE;
6157 for (l = hidden_convwin->gtkconvs; l; )
6159 gtkconv = l->data;
6160 l = l->next;
6162 conv = gtkconv->active_conv;
6164 if (PURPLE_IS_CHAT_CONVERSATION(conv) ||
6165 gtkconv->unseen_count == 0 ||
6166 (when_away && !purple_status_is_available(
6167 purple_account_get_active_status(
6168 purple_conversation_get_account(conv)))))
6169 continue;
6171 pidgin_conv_attach_to_conversation(conv);
6176 static void
6177 conv_placement_pref_cb(const char *name, PurplePrefType type,
6178 gconstpointer value, gpointer data)
6180 PidginConvPlacementFunc func;
6182 if (!purple_strequal(name, PIDGIN_PREFS_ROOT "/conversations/placement"))
6183 return;
6185 func = pidgin_conv_placement_get_fnc(value);
6187 if (func == NULL)
6188 return;
6190 pidgin_conv_placement_set_current_func(func);
6193 static PidginConversation *
6194 get_gtkconv_with_contact(PurpleContact *contact)
6196 PurpleBlistNode *node;
6198 node = ((PurpleBlistNode*)contact)->child;
6200 for (; node; node = node->next)
6202 PurpleBuddy *buddy = (PurpleBuddy*)node;
6203 PurpleIMConversation *im;
6204 im = purple_conversations_find_im_with_account(purple_buddy_get_name(buddy), purple_buddy_get_account(buddy));
6205 if (im)
6206 return PIDGIN_CONVERSATION(PURPLE_CONVERSATION(im));
6208 return NULL;
6211 static void
6212 account_signed_off_cb(PurpleConnection *gc, gpointer event)
6214 GList *iter;
6216 for (iter = purple_conversations_get_all(); iter; iter = iter->next)
6218 PurpleConversation *conv = iter->data;
6220 /* This seems fine in theory, but we also need to cover the
6221 * case of this account matching one of the other buddies in
6222 * one of the contacts containing the buddy corresponding to
6223 * a conversation. It's easier to just update them all. */
6224 /* if (purple_conversation_get_account(conv) == account) */
6225 pidgin_conv_update_fields(conv, PIDGIN_CONV_TAB_ICON |
6226 PIDGIN_CONV_MENU | PIDGIN_CONV_COLORIZE_TITLE);
6228 if (PURPLE_CONNECTION_IS_CONNECTED(gc) &&
6229 PURPLE_IS_CHAT_CONVERSATION(conv) &&
6230 purple_conversation_get_account(conv) == purple_connection_get_account(gc) &&
6231 g_object_get_data(G_OBJECT(conv), "want-to-rejoin")) {
6232 GHashTable *comps = NULL;
6233 PurpleChat *chat = purple_blist_find_chat(purple_conversation_get_account(conv), purple_conversation_get_name(conv));
6234 if (chat == NULL) {
6235 PurpleProtocol *protocol = purple_connection_get_protocol(gc);
6236 comps = purple_protocol_chat_iface_info_defaults(protocol, gc, purple_conversation_get_name(conv));
6237 } else {
6238 comps = purple_chat_get_components(chat);
6240 purple_serv_join_chat(gc, comps);
6241 if (chat == NULL && comps != NULL)
6242 g_hash_table_destroy(comps);
6247 static void
6248 account_signing_off(PurpleConnection *gc)
6250 GList *list = purple_conversations_get_chats();
6251 PurpleAccount *account = purple_connection_get_account(gc);
6253 /* We are about to sign off. See which chats we are currently in, and mark
6254 * them for rejoin on reconnect. */
6255 while (list) {
6256 PurpleConversation *conv = list->data;
6257 if (!purple_chat_conversation_has_left(PURPLE_CHAT_CONVERSATION(conv)) &&
6258 purple_conversation_get_account(conv) == account) {
6259 g_object_set_data(G_OBJECT(conv), "want-to-rejoin", GINT_TO_POINTER(TRUE));
6260 purple_conversation_write_system_message(conv,
6261 _("The account has disconnected and you are no "
6262 "longer in this chat. You will automatically "
6263 "rejoin the chat when the account reconnects."),
6264 PURPLE_MESSAGE_NO_LOG);
6266 list = list->next;
6270 static void
6271 update_buddy_status_changed(PurpleBuddy *buddy, PurpleStatus *old, PurpleStatus *newstatus)
6273 PidginConversation *gtkconv;
6274 PurpleConversation *conv;
6276 gtkconv = get_gtkconv_with_contact(purple_buddy_get_contact(buddy));
6277 if (gtkconv)
6279 conv = gtkconv->active_conv;
6280 pidgin_conv_update_fields(conv, PIDGIN_CONV_TAB_ICON
6281 | PIDGIN_CONV_COLORIZE_TITLE
6282 | PIDGIN_CONV_BUDDY_ICON);
6283 if ((purple_status_is_online(old) ^ purple_status_is_online(newstatus)) != 0)
6284 pidgin_conv_update_fields(conv, PIDGIN_CONV_MENU);
6288 static void
6289 update_buddy_privacy_changed(PurpleBuddy *buddy)
6291 PidginConversation *gtkconv;
6292 PurpleConversation *conv;
6294 gtkconv = get_gtkconv_with_contact(purple_buddy_get_contact(buddy));
6295 if (gtkconv) {
6296 conv = gtkconv->active_conv;
6297 pidgin_conv_update_fields(conv, PIDGIN_CONV_TAB_ICON | PIDGIN_CONV_MENU);
6301 static void
6302 update_buddy_idle_changed(PurpleBuddy *buddy, gboolean old, gboolean newidle)
6304 PurpleIMConversation *im;
6306 im = purple_conversations_find_im_with_account(purple_buddy_get_name(buddy), purple_buddy_get_account(buddy));
6307 if (im)
6308 pidgin_conv_update_fields(PURPLE_CONVERSATION(im), PIDGIN_CONV_TAB_ICON);
6311 static void
6312 update_buddy_icon(PurpleBuddy *buddy)
6314 PurpleIMConversation *im;
6316 im = purple_conversations_find_im_with_account(purple_buddy_get_name(buddy), purple_buddy_get_account(buddy));
6317 if (im)
6318 pidgin_conv_update_fields(PURPLE_CONVERSATION(im), PIDGIN_CONV_BUDDY_ICON);
6321 static void
6322 update_buddy_sign(PurpleBuddy *buddy, const char *which)
6324 PurplePresence *presence;
6325 PurpleStatus *on, *off;
6327 presence = purple_buddy_get_presence(buddy);
6328 if (!presence)
6329 return;
6330 off = purple_presence_get_status(presence, "offline");
6331 on = purple_presence_get_status(presence, "available");
6333 if (*(which+1) == 'f')
6334 update_buddy_status_changed(buddy, on, off);
6335 else
6336 update_buddy_status_changed(buddy, off, on);
6339 static void
6340 update_conversation_switched(PurpleConversation *conv)
6342 pidgin_conv_update_fields(conv, PIDGIN_CONV_TAB_ICON |
6343 PIDGIN_CONV_SET_TITLE | PIDGIN_CONV_MENU |
6344 PIDGIN_CONV_BUDDY_ICON | PIDGIN_CONV_E2EE );
6347 static void
6348 update_buddy_typing(PurpleAccount *account, const char *who)
6350 PurpleConversation *conv;
6351 PidginConversation *gtkconv;
6353 conv = PURPLE_CONVERSATION(purple_conversations_find_im_with_account(who, account));
6354 if (!conv)
6355 return;
6357 gtkconv = PIDGIN_CONVERSATION(conv);
6358 if (gtkconv && gtkconv->active_conv == conv)
6359 pidgin_conv_update_fields(conv, PIDGIN_CONV_COLORIZE_TITLE);
6362 static void
6363 update_chat(PurpleChatConversation *chat)
6365 pidgin_conv_update_fields(PURPLE_CONVERSATION(chat), PIDGIN_CONV_TOPIC |
6366 PIDGIN_CONV_MENU | PIDGIN_CONV_SET_TITLE);
6369 static void
6370 update_chat_topic(PurpleChatConversation *chat, const char *old, const char *new)
6372 pidgin_conv_update_fields(PURPLE_CONVERSATION(chat), PIDGIN_CONV_TOPIC);
6375 /* Message history stuff */
6377 /* Compare two PurpleMessages, according to time in ascending order. */
6378 static int
6379 message_compare(PurpleMessage *m1, PurpleMessage *m2)
6381 guint64 t1 = purple_message_get_time(m1), t2 = purple_message_get_time(m2);
6382 return (t1 > t2) - (t1 < t2);
6385 /* Adds some message history to the gtkconv. This happens in a idle-callback. */
6386 static gboolean
6387 add_message_history_to_gtkconv(gpointer data)
6389 PidginConversation *gtkconv = data;
6390 int count = 0;
6391 int timer = gtkconv->attach_timer;
6392 time_t when = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(gtkconv->editor), "attach-start-time"));
6393 gboolean im = (PURPLE_IS_IM_CONVERSATION(gtkconv->active_conv));
6395 gtkconv->attach_timer = 0;
6396 while (gtkconv->attach_current && count < ADD_MESSAGE_HISTORY_AT_ONCE) {
6397 PurpleMessage *msg = gtkconv->attach_current->data;
6398 if (!im && when && (guint64)when < purple_message_get_time(msg)) {
6399 g_object_set_data(G_OBJECT(gtkconv->editor), "attach-start-time", NULL);
6401 /* XXX: should it be gtkconv->active_conv? */
6402 pidgin_conv_write_conv(gtkconv->active_conv, msg);
6403 if (im) {
6404 gtkconv->attach_current = g_list_delete_link(gtkconv->attach_current, gtkconv->attach_current);
6405 } else {
6406 gtkconv->attach_current = gtkconv->attach_current->prev;
6408 count++;
6410 gtkconv->attach_timer = timer;
6411 if (gtkconv->attach_current)
6412 return TRUE;
6414 g_source_remove(gtkconv->attach_timer);
6415 gtkconv->attach_timer = 0;
6416 if (im) {
6417 /* Print any message that was sent while the old history was being added back. */
6418 GList *msgs = NULL;
6419 GList *iter = gtkconv->convs;
6420 for (; iter; iter = iter->next) {
6421 PurpleConversation *conv = iter->data;
6422 GList *history = purple_conversation_get_message_history(conv);
6423 for (; history; history = history->next) {
6424 PurpleMessage *msg = history->data;
6425 if (purple_message_get_time(msg) > (guint64)when)
6426 msgs = g_list_prepend(msgs, msg);
6429 msgs = g_list_sort(msgs, (GCompareFunc)message_compare);
6430 for (; msgs; msgs = g_list_delete_link(msgs, msgs)) {
6431 PurpleMessage *msg = msgs->data;
6432 /* XXX: see above - should it be active_conv? */
6433 pidgin_conv_write_conv(gtkconv->active_conv, msg);
6435 g_object_set_data(G_OBJECT(gtkconv->editor), "attach-start-time", NULL);
6438 g_object_set_data(G_OBJECT(gtkconv->editor), "attach-start-time", NULL);
6439 purple_signal_emit(pidgin_conversations_get_handle(),
6440 "conversation-displayed", gtkconv);
6441 return FALSE;
6444 static void
6445 pidgin_conv_attach(PurpleConversation *conv)
6447 int timer;
6448 g_object_set_data(G_OBJECT(conv), "unseen-count", NULL);
6449 g_object_set_data(G_OBJECT(conv), "unseen-state", NULL);
6450 purple_conversation_set_ui_ops(conv, pidgin_conversations_get_conv_ui_ops());
6451 if (!PIDGIN_CONVERSATION(conv))
6452 private_gtkconv_new(conv, FALSE);
6453 timer = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(conv), "close-timer"));
6454 if (timer) {
6455 g_source_remove(timer);
6456 g_object_set_data(G_OBJECT(conv), "close-timer", NULL);
6460 gboolean pidgin_conv_attach_to_conversation(PurpleConversation *conv)
6462 GList *list;
6463 PidginConversation *gtkconv;
6465 if (PIDGIN_IS_PIDGIN_CONVERSATION(conv)) {
6466 /* This is pretty much always the case now. */
6467 gtkconv = PIDGIN_CONVERSATION(conv);
6468 if (gtkconv->win != hidden_convwin)
6469 return FALSE;
6470 pidgin_conv_window_remove_gtkconv(hidden_convwin, gtkconv);
6471 pidgin_conv_placement_place(gtkconv);
6472 purple_signal_emit(pidgin_conversations_get_handle(),
6473 "conversation-displayed", gtkconv);
6474 list = gtkconv->convs;
6475 while (list) {
6476 pidgin_conv_attach(list->data);
6477 list = list->next;
6479 return TRUE;
6482 pidgin_conv_attach(conv);
6483 gtkconv = PIDGIN_CONVERSATION(conv);
6485 list = purple_conversation_get_message_history(conv);
6486 if (list) {
6487 if (PURPLE_IS_IM_CONVERSATION(conv)) {
6488 GList *convs;
6489 list = g_list_copy(list);
6490 for (convs = purple_conversations_get_ims(); convs; convs = convs->next)
6491 if (convs->data != conv &&
6492 pidgin_conv_find_gtkconv(convs->data) == gtkconv) {
6493 pidgin_conv_attach(convs->data);
6494 list = g_list_concat(list, g_list_copy(purple_conversation_get_message_history(convs->data)));
6496 list = g_list_sort(list, (GCompareFunc)message_compare);
6497 gtkconv->attach_current = list;
6498 list = g_list_last(list);
6499 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
6500 gtkconv->attach_current = g_list_last(list);
6503 g_object_set_data(G_OBJECT(gtkconv->editor), "attach-start-time",
6504 GINT_TO_POINTER(purple_message_get_time(list->data)));
6505 gtkconv->attach_timer = g_idle_add(add_message_history_to_gtkconv, gtkconv);
6506 } else {
6507 purple_signal_emit(pidgin_conversations_get_handle(),
6508 "conversation-displayed", gtkconv);
6511 if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
6512 GList *users;
6513 PurpleChatConversation *chat = PURPLE_CHAT_CONVERSATION(conv);
6514 pidgin_conv_update_fields(conv, PIDGIN_CONV_TOPIC);
6515 users = purple_chat_conversation_get_users(chat);
6516 pidgin_conv_chat_add_users(chat, users, TRUE);
6517 g_list_free(users);
6520 return TRUE;
6523 void *
6524 pidgin_conversations_get_handle(void)
6526 static int handle;
6528 return &handle;
6531 static void
6532 pidgin_conversations_pre_uninit(void);
6534 void
6535 pidgin_conversations_init(void)
6537 void *handle = pidgin_conversations_get_handle();
6538 void *blist_handle = purple_blist_get_handle();
6540 e2ee_stock = g_hash_table_new_full(g_str_hash, g_str_equal,
6541 g_free, g_object_unref);
6543 /* Conversations */
6544 purple_prefs_add_none(PIDGIN_PREFS_ROOT "/conversations");
6545 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/use_smooth_scrolling", TRUE);
6546 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/close_on_tabs", TRUE);
6547 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/send_bold", FALSE);
6548 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/send_italic", FALSE);
6549 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/send_underline", FALSE);
6550 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/send_strike", FALSE);
6551 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/spellcheck", TRUE);
6552 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/show_incoming_formatting", TRUE);
6553 /* TODO: it's about *remote* smileys, not local ones */
6554 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/resize_custom_smileys", TRUE);
6555 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/custom_smileys_size", 96);
6556 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/minimum_entry_lines", 2);
6558 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/show_formatting_toolbar", TRUE);
6560 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/placement", "last");
6561 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/placement_number", 1);
6562 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/bgcolor", "");
6563 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/fgcolor", "");
6564 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/font_face", "");
6565 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/font_size", 3);
6566 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/tabs", TRUE);
6567 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/tab_side", GTK_POS_TOP);
6568 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/scrollback_lines", 4000);
6570 #ifdef _WIN32
6571 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/use_theme_font", TRUE);
6572 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/custom_font", "");
6573 #endif
6575 /* Conversations -> Chat */
6576 purple_prefs_add_none(PIDGIN_PREFS_ROOT "/conversations/chat");
6577 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/entry_height", 54);
6578 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/userlist_width", 80);
6579 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/x", 0);
6580 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/y", 0);
6581 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/width", 340);
6582 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/height", 390);
6584 /* Conversations -> IM */
6585 purple_prefs_add_none(PIDGIN_PREFS_ROOT "/conversations/im");
6586 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/im/x", 0);
6587 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/im/y", 0);
6588 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/im/width", 340);
6589 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/im/height", 390);
6591 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/im/animate_buddy_icons", TRUE);
6593 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/im/entry_height", 54);
6594 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons", TRUE);
6596 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new", "never");
6597 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/im/close_immediately", TRUE);
6599 #ifdef _WIN32
6600 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/win32/minimize_new_convs", FALSE);
6601 #endif
6603 /* Connect callbacks. */
6604 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/close_on_tabs",
6605 close_on_tabs_pref_cb, NULL);
6606 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/show_formatting_toolbar",
6607 show_formatting_toolbar_pref_cb, NULL);
6608 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/spellcheck",
6609 spellcheck_pref_cb, NULL);
6610 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/tab_side",
6611 tab_side_pref_cb, NULL);
6613 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/tabs",
6614 conv_placement_usetabs_cb, NULL);
6616 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/placement",
6617 conv_placement_pref_cb, NULL);
6618 purple_prefs_trigger_callback(PIDGIN_PREFS_ROOT "/conversations/placement");
6620 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/minimum_entry_lines",
6621 minimum_entry_lines_pref_cb, NULL);
6623 /* IM callbacks */
6624 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/im/animate_buddy_icons",
6625 animate_buddy_icons_pref_cb, NULL);
6626 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons",
6627 show_buddy_icons_pref_cb, NULL);
6628 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/blist/show_protocol_icons",
6629 show_protocol_icons_pref_cb, NULL);
6630 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/im/hide_new",
6631 hide_new_pref_cb, NULL);
6633 /**********************************************************************
6634 * Register signals
6635 **********************************************************************/
6636 purple_signal_register(handle, "conversation-dragging",
6637 purple_marshal_VOID__POINTER_POINTER, G_TYPE_NONE, 2,
6638 G_TYPE_POINTER, /* pointer to a (PidginConvWindow *) */
6639 G_TYPE_POINTER); /* pointer to a (PidginConvWindow *) */
6641 purple_signal_register(handle, "conversation-timestamp",
6642 #if SIZEOF_TIME_T == 4
6643 purple_marshal_POINTER__POINTER_INT_BOOLEAN,
6644 #elif SIZEOF_TIME_T == 8
6645 purple_marshal_POINTER__POINTER_INT64_BOOLEAN,
6646 #else
6647 #error Unkown size of time_t
6648 #endif
6649 G_TYPE_STRING, 3, PURPLE_TYPE_CONVERSATION,
6650 #if SIZEOF_TIME_T == 4
6651 G_TYPE_INT,
6652 #elif SIZEOF_TIME_T == 8
6653 G_TYPE_INT64,
6654 #else
6655 # error Unknown size of time_t
6656 #endif
6657 G_TYPE_BOOLEAN);
6659 purple_signal_register(handle, "displaying-im-msg",
6660 purple_marshal_BOOLEAN__POINTER_POINTER,
6661 G_TYPE_BOOLEAN, 2, PURPLE_TYPE_CONVERSATION, PURPLE_TYPE_MESSAGE);
6663 purple_signal_register(handle, "displayed-im-msg",
6664 purple_marshal_VOID__POINTER_POINTER, G_TYPE_NONE, 2,
6665 PURPLE_TYPE_CONVERSATION, PURPLE_TYPE_MESSAGE);
6667 purple_signal_register(handle, "displaying-chat-msg",
6668 purple_marshal_BOOLEAN__POINTER_POINTER,
6669 G_TYPE_BOOLEAN, 2, PURPLE_TYPE_CONVERSATION, PURPLE_TYPE_MESSAGE);
6671 purple_signal_register(handle, "displayed-chat-msg",
6672 purple_marshal_VOID__POINTER_POINTER, G_TYPE_NONE, 2,
6673 PURPLE_TYPE_CONVERSATION, PURPLE_TYPE_MESSAGE);
6675 purple_signal_register(handle, "conversation-switched",
6676 purple_marshal_VOID__POINTER, G_TYPE_NONE, 1,
6677 PURPLE_TYPE_CONVERSATION);
6679 purple_signal_register(handle, "conversation-hiding",
6680 purple_marshal_VOID__POINTER, G_TYPE_NONE, 1,
6681 G_TYPE_POINTER); /* (PidginConversation *) */
6683 purple_signal_register(handle, "conversation-displayed",
6684 purple_marshal_VOID__POINTER, G_TYPE_NONE, 1,
6685 G_TYPE_POINTER); /* (PidginConversation *) */
6687 purple_signal_register(handle, "chat-nick-autocomplete",
6688 purple_marshal_BOOLEAN__POINTER_BOOLEAN,
6689 G_TYPE_BOOLEAN, 1, PURPLE_TYPE_CONVERSATION);
6691 purple_signal_register(handle, "chat-nick-clicked",
6692 purple_marshal_BOOLEAN__POINTER_POINTER_UINT,
6693 G_TYPE_BOOLEAN, 3, PURPLE_TYPE_CONVERSATION,
6694 G_TYPE_STRING, G_TYPE_UINT);
6696 purple_signal_register(handle, "conversation-window-created",
6697 purple_marshal_VOID__POINTER, G_TYPE_NONE, 1,
6698 G_TYPE_POINTER); /* (PidginConvWindow *) */
6701 /**********************************************************************
6702 * Register commands
6703 **********************************************************************/
6704 purple_cmd_register("say", "S", PURPLE_CMD_P_DEFAULT,
6705 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM, NULL,
6706 say_command_cb, _("say &lt;message&gt;: Send a message normally as if you weren't using a command."), NULL);
6707 purple_cmd_register("me", "S", PURPLE_CMD_P_DEFAULT,
6708 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM, NULL,
6709 me_command_cb, _("me &lt;action&gt;: Send an IRC style action to a buddy or chat."), NULL);
6710 purple_cmd_register("debug", "w", PURPLE_CMD_P_DEFAULT,
6711 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM, NULL,
6712 debug_command_cb, _("debug &lt;option&gt;: Send various debug information to the current conversation."), NULL);
6713 purple_cmd_register("clear", "", PURPLE_CMD_P_DEFAULT,
6714 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM, NULL,
6715 clear_command_cb, _("clear: Clears the conversation scrollback."), NULL);
6716 purple_cmd_register("clearall", "", PURPLE_CMD_P_DEFAULT,
6717 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM, NULL,
6718 clearall_command_cb, _("clear: Clears all conversation scrollbacks."), NULL);
6719 purple_cmd_register("help", "w", PURPLE_CMD_P_DEFAULT,
6720 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM | PURPLE_CMD_FLAG_ALLOW_WRONG_ARGS, NULL,
6721 help_command_cb, _("help &lt;command&gt;: Help on a specific command."), NULL);
6723 /**********************************************************************
6724 * UI operations
6725 **********************************************************************/
6727 purple_signal_connect(purple_connections_get_handle(), "signed-on", handle,
6728 G_CALLBACK(account_signed_off_cb),
6729 GINT_TO_POINTER(PURPLE_CONVERSATION_ACCOUNT_ONLINE));
6730 purple_signal_connect(purple_connections_get_handle(), "signed-off", handle,
6731 G_CALLBACK(account_signed_off_cb),
6732 GINT_TO_POINTER(PURPLE_CONVERSATION_ACCOUNT_OFFLINE));
6733 purple_signal_connect(purple_connections_get_handle(), "signing-off", handle,
6734 G_CALLBACK(account_signing_off), NULL);
6736 purple_signal_connect(purple_conversations_get_handle(), "writing-im-msg",
6737 handle, G_CALLBACK(writing_msg), NULL);
6738 purple_signal_connect(purple_conversations_get_handle(), "writing-chat-msg",
6739 handle, G_CALLBACK(writing_msg), NULL);
6740 purple_signal_connect(purple_conversations_get_handle(), "received-im-msg",
6741 handle, G_CALLBACK(received_im_msg_cb), NULL);
6742 purple_signal_connect(purple_conversations_get_handle(), "cleared-message-history",
6743 handle, G_CALLBACK(clear_conversation_scrollback_cb), NULL);
6745 purple_signal_connect(purple_conversations_get_handle(), "deleting-chat-user",
6746 handle, G_CALLBACK(deleting_chat_user_cb), NULL);
6748 purple_conversations_set_ui_ops(&conversation_ui_ops);
6750 hidden_convwin = pidgin_conv_window_new();
6751 window_list = g_list_remove(window_list, hidden_convwin);
6753 purple_signal_connect(purple_accounts_get_handle(), "account-status-changed",
6754 handle, PURPLE_CALLBACK(account_status_changed_cb), NULL);
6756 purple_signal_connect_priority(purple_get_core(), "quitting", handle,
6757 PURPLE_CALLBACK(pidgin_conversations_pre_uninit), NULL, PURPLE_SIGNAL_PRIORITY_HIGHEST);
6759 /* Callbacks to update a conversation */
6760 purple_signal_connect(blist_handle, "blist-node-added", handle,
6761 G_CALLBACK(buddy_update_cb), NULL);
6762 purple_signal_connect(blist_handle, "blist-node-removed", handle,
6763 G_CALLBACK(buddy_update_cb), NULL);
6764 purple_signal_connect(blist_handle, "buddy-signed-on",
6765 handle, PURPLE_CALLBACK(update_buddy_sign), "on");
6766 purple_signal_connect(blist_handle, "buddy-signed-off",
6767 handle, PURPLE_CALLBACK(update_buddy_sign), "off");
6768 purple_signal_connect(blist_handle, "buddy-status-changed",
6769 handle, PURPLE_CALLBACK(update_buddy_status_changed), NULL);
6770 purple_signal_connect(blist_handle, "buddy-privacy-changed",
6771 handle, PURPLE_CALLBACK(update_buddy_privacy_changed), NULL);
6772 purple_signal_connect(blist_handle, "buddy-idle-changed",
6773 handle, PURPLE_CALLBACK(update_buddy_idle_changed), NULL);
6774 purple_signal_connect(blist_handle, "buddy-icon-changed",
6775 handle, PURPLE_CALLBACK(update_buddy_icon), NULL);
6776 purple_signal_connect(purple_conversations_get_handle(), "buddy-typing",
6777 handle, PURPLE_CALLBACK(update_buddy_typing), NULL);
6778 purple_signal_connect(purple_conversations_get_handle(), "buddy-typing-stopped",
6779 handle, PURPLE_CALLBACK(update_buddy_typing), NULL);
6780 purple_signal_connect(pidgin_conversations_get_handle(), "conversation-switched",
6781 handle, PURPLE_CALLBACK(update_conversation_switched), NULL);
6782 purple_signal_connect(purple_conversations_get_handle(), "chat-left", handle,
6783 PURPLE_CALLBACK(update_chat), NULL);
6784 purple_signal_connect(purple_conversations_get_handle(), "chat-joined", handle,
6785 PURPLE_CALLBACK(update_chat), NULL);
6786 purple_signal_connect(purple_conversations_get_handle(), "chat-topic-changed", handle,
6787 PURPLE_CALLBACK(update_chat_topic), NULL);
6788 purple_signal_connect_priority(purple_conversations_get_handle(), "conversation-updated", handle,
6789 PURPLE_CALLBACK(pidgin_conv_updated), NULL,
6790 PURPLE_SIGNAL_PRIORITY_LOWEST);
6791 purple_signal_connect(purple_conversations_get_handle(), "wrote-im-msg", handle,
6792 PURPLE_CALLBACK(wrote_msg_update_unseen_cb), NULL);
6793 purple_signal_connect(purple_conversations_get_handle(), "wrote-chat-msg", handle,
6794 PURPLE_CALLBACK(wrote_msg_update_unseen_cb), NULL);
6797 static void
6798 pidgin_conversations_pre_uninit(void)
6800 g_hash_table_destroy(e2ee_stock);
6801 e2ee_stock = NULL;
6804 /* Invalidate the first tab color set */
6805 static gboolean tab_color_fuse = TRUE;
6807 static void
6808 pidgin_conversations_set_tab_colors(void)
6810 /* Set default tab colors */
6811 GString *str;
6812 GtkSettings *settings;
6813 GtkStyle *parent, *now;
6814 struct {
6815 const char *stylename;
6816 const char *labelname;
6817 const char *color;
6818 } styles[] = {
6819 {"pidgin_tab_label_typing_default", "tab-label-typing", "#4e9a06"},
6820 {"pidgin_tab_label_typed_default", "tab-label-typed", "#c4a000"},
6821 {"pidgin_tab_label_attention_default", "tab-label-attention", "#006aff"},
6822 {"pidgin_tab_label_unreadchat_default", "tab-label-unreadchat", "#cc0000"},
6823 {"pidgin_tab_label_event_default", "tab-label-event", "#888a85"},
6824 {NULL, NULL, NULL}
6826 int iter;
6828 if(tab_color_fuse) {
6829 tab_color_fuse = FALSE;
6830 return;
6833 str = g_string_new(NULL);
6834 settings = gtk_settings_get_default();
6835 parent = gtk_rc_get_style_by_paths(settings, "tab-container.tab-label*",
6836 NULL, G_TYPE_NONE);
6838 for (iter = 0; styles[iter].stylename; iter++) {
6839 now = gtk_rc_get_style_by_paths(settings, styles[iter].labelname, NULL, G_TYPE_NONE);
6840 if (parent == now ||
6841 (parent && now && parent->rc_style == now->rc_style)) {
6842 GdkRGBA color;
6843 gchar *color_str;
6845 gdk_rgba_parse(&color, styles[iter].color);
6846 pidgin_style_adjust_contrast(gtk_widget_get_default_style(), &color);
6848 color_str = gdk_rgba_to_string(&color);
6849 g_string_append_printf(str, "style \"%s\" {\n"
6850 "fg[ACTIVE] = \"%s\"\n"
6851 "}\n"
6852 "widget \"*%s\" style \"%s\"\n",
6853 styles[iter].stylename,
6854 color_str,
6855 styles[iter].labelname, styles[iter].stylename);
6856 g_free(color_str);
6859 gtk_rc_parse_string(str->str);
6860 g_string_free(str, TRUE);
6861 gtk_rc_reset_styles(settings);
6864 void
6865 pidgin_conversations_uninit(void)
6867 purple_prefs_disconnect_by_handle(pidgin_conversations_get_handle());
6868 purple_signals_disconnect_by_handle(pidgin_conversations_get_handle());
6869 purple_signals_unregister_by_instance(pidgin_conversations_get_handle());
6872 /**************************************************************************
6873 * PidginConversation GBoxed code
6874 **************************************************************************/
6875 static PidginConversation *
6876 pidgin_conversation_ref(PidginConversation *gtkconv)
6878 g_return_val_if_fail(gtkconv != NULL, NULL);
6880 gtkconv->box_count++;
6882 return gtkconv;
6885 static void
6886 pidgin_conversation_unref(PidginConversation *gtkconv)
6888 g_return_if_fail(gtkconv != NULL);
6889 g_return_if_fail(gtkconv->box_count >= 0);
6891 if (!gtkconv->box_count--)
6892 pidgin_conv_destroy(gtkconv->active_conv);
6895 GType
6896 pidgin_conversation_get_type(void)
6898 static GType type = 0;
6900 if (type == 0) {
6901 type = g_boxed_type_register_static("PidginConversation",
6902 (GBoxedCopyFunc)pidgin_conversation_ref,
6903 (GBoxedFreeFunc)pidgin_conversation_unref);
6906 return type;
6924 /* down here is where gtkconvwin.c ought to start. except they share like every freaking function,
6925 * and touch each others' private members all day long */
6927 /* pidgin
6929 * Pidgin is the legal property of its developers, whose names are too numerous
6930 * to list here. Please refer to the COPYRIGHT file distributed with this
6931 * source distribution.
6933 * This program is free software; you can redistribute it and/or modify
6934 * it under the terms of the GNU General Public License as published by
6935 * the Free Software Foundation; either version 2 of the License, or
6936 * (at your option) any later version.
6938 * This program is distributed in the hope that it will be useful,
6939 * but WITHOUT ANY WARRANTY; without even the implied warranty of
6940 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
6941 * GNU General Public License for more details.
6943 * You should have received a copy of the GNU General Public License
6944 * along with this program; if not, write to the Free Software
6945 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
6948 #include "internal.h"
6949 #include "pidgin.h"
6952 #include <gdk/gdkkeysyms.h>
6954 #include "account.h"
6955 #include "cmds.h"
6956 #include "debug.h"
6957 #include "log.h"
6958 #include "notify.h"
6959 #include "protocol.h"
6960 #include "request.h"
6961 #include "util.h"
6963 #include "gtkdnd-hints.h"
6964 #include "gtkblist.h"
6965 #include "gtkconv.h"
6966 #include "gtkdialogs.h"
6967 #include "gtkmenutray.h"
6968 #include "gtkpounce.h"
6969 #include "gtkprefs.h"
6970 #include "gtkprivacy.h"
6971 #include "gtkutils.h"
6972 #include "pidginstock.h"
6974 static void
6975 do_close(GtkWidget *w, int resp, PidginConvWindow *win)
6977 gtk_widget_destroy(warn_close_dialog);
6978 warn_close_dialog = NULL;
6980 if (resp == GTK_RESPONSE_OK)
6981 pidgin_conv_window_destroy(win);
6984 static void
6985 build_warn_close_dialog(PidginConvWindow *gtkwin)
6987 GtkWidget *label, *vbox, *hbox, *img;
6989 g_return_if_fail(warn_close_dialog == NULL);
6991 warn_close_dialog = gtk_dialog_new_with_buttons(_("Confirm close"),
6992 GTK_WINDOW(gtkwin->window), GTK_DIALOG_MODAL,
6993 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
6994 GTK_STOCK_CLOSE, GTK_RESPONSE_OK, NULL);
6996 gtk_dialog_set_default_response(GTK_DIALOG(warn_close_dialog),
6997 GTK_RESPONSE_OK);
6999 gtk_container_set_border_width(GTK_CONTAINER(warn_close_dialog),
7001 gtk_window_set_resizable(GTK_WINDOW(warn_close_dialog), FALSE);
7003 /* Setup the outside spacing. */
7004 vbox = gtk_dialog_get_content_area(GTK_DIALOG(warn_close_dialog));
7006 gtk_box_set_spacing(GTK_BOX(vbox), 12);
7007 gtk_container_set_border_width(GTK_CONTAINER(vbox), 6);
7009 img = gtk_image_new_from_icon_name("dialog-warning",
7010 GTK_ICON_SIZE_DIALOG);
7012 /* Setup the inner hbox and put the dialog's icon in it. */
7013 hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12);
7014 gtk_container_add(GTK_CONTAINER(vbox), hbox);
7015 gtk_box_pack_start(GTK_BOX(hbox), img, FALSE, FALSE, 0);
7016 gtk_widget_set_halign(img, GTK_ALIGN_START);
7017 gtk_widget_set_valign(img, GTK_ALIGN_START);
7019 /* Setup the right vbox. */
7020 vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
7021 gtk_container_add(GTK_CONTAINER(hbox), vbox);
7023 label = gtk_label_new(_("You have unread messages. Are you sure you want to close the window?"));
7024 gtk_widget_set_size_request(label, 350, -1);
7025 gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
7026 gtk_label_set_xalign(GTK_LABEL(label), 0);
7027 gtk_label_set_yalign(GTK_LABEL(label), 0);
7028 gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 0);
7030 /* Connect the signals. */
7031 g_signal_connect(G_OBJECT(warn_close_dialog), "response",
7032 G_CALLBACK(do_close), gtkwin);
7036 /**************************************************************************
7037 * Callbacks
7038 **************************************************************************/
7040 static gboolean
7041 close_win_cb(GtkWidget *w, GdkEventAny *e, gpointer d)
7043 PidginConvWindow *win = d;
7044 GList *l;
7046 /* If there are unread messages then show a warning dialog */
7047 for (l = pidgin_conv_window_get_gtkconvs(win);
7048 l != NULL; l = l->next)
7050 PidginConversation *gtkconv = l->data;
7051 if (PURPLE_IS_IM_CONVERSATION(gtkconv->active_conv) &&
7052 gtkconv->unseen_state >= PIDGIN_UNSEEN_TEXT)
7054 build_warn_close_dialog(win);
7055 gtk_widget_show_all(warn_close_dialog);
7057 return TRUE;
7061 pidgin_conv_window_destroy(win);
7063 return TRUE;
7066 static void
7067 conv_set_unseen(PurpleConversation *conv, PidginUnseenState state)
7069 int unseen_count = 0;
7070 PidginUnseenState unseen_state = PIDGIN_UNSEEN_NONE;
7072 if(g_object_get_data(G_OBJECT(conv), "unseen-count"))
7073 unseen_count = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(conv), "unseen-count"));
7075 if(g_object_get_data(G_OBJECT(conv), "unseen-state"))
7076 unseen_state = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(conv), "unseen-state"));
7078 if (state == PIDGIN_UNSEEN_NONE)
7080 unseen_count = 0;
7081 unseen_state = PIDGIN_UNSEEN_NONE;
7083 else
7085 if (state >= PIDGIN_UNSEEN_TEXT)
7086 unseen_count++;
7088 if (state > unseen_state)
7089 unseen_state = state;
7092 g_object_set_data(G_OBJECT(conv), "unseen-count", GINT_TO_POINTER(unseen_count));
7093 g_object_set_data(G_OBJECT(conv), "unseen-state", GINT_TO_POINTER(unseen_state));
7095 purple_conversation_update(conv, PURPLE_CONVERSATION_UPDATE_UNSEEN);
7098 static void
7099 gtkconv_set_unseen(PidginConversation *gtkconv, PidginUnseenState state)
7101 if (state == PIDGIN_UNSEEN_NONE)
7103 gtkconv->unseen_count = 0;
7104 gtkconv->unseen_state = PIDGIN_UNSEEN_NONE;
7106 else
7108 if (state >= PIDGIN_UNSEEN_TEXT)
7109 gtkconv->unseen_count++;
7111 if (state > gtkconv->unseen_state)
7112 gtkconv->unseen_state = state;
7115 g_object_set_data(G_OBJECT(gtkconv->active_conv), "unseen-count", GINT_TO_POINTER(gtkconv->unseen_count));
7116 g_object_set_data(G_OBJECT(gtkconv->active_conv), "unseen-state", GINT_TO_POINTER(gtkconv->unseen_state));
7118 purple_conversation_update(gtkconv->active_conv, PURPLE_CONVERSATION_UPDATE_UNSEEN);
7122 * When a conversation window is focused, we know the user
7123 * has looked at it so we know there are no longer unseen
7124 * messages.
7126 static gboolean
7127 focus_win_cb(GtkWidget *w, GdkEventFocus *e, gpointer d)
7129 PidginConvWindow *win = d;
7130 PidginConversation *gtkconv = pidgin_conv_window_get_active_gtkconv(win);
7132 if (gtkconv)
7133 gtkconv_set_unseen(gtkconv, PIDGIN_UNSEEN_NONE);
7135 return FALSE;
7138 static void
7139 notebook_init_grab(PidginConvWindow *gtkwin, GtkWidget *widget, GdkEvent *event)
7141 static GdkCursor *cursor = NULL;
7142 GdkDevice *device;
7144 gtkwin->in_drag = TRUE;
7146 if (gtkwin->drag_leave_signal) {
7147 g_signal_handler_disconnect(G_OBJECT(widget),
7148 gtkwin->drag_leave_signal);
7149 gtkwin->drag_leave_signal = 0;
7152 if (cursor == NULL) {
7153 GdkDisplay *display = gtk_widget_get_display(gtkwin->notebook);
7154 cursor = gdk_cursor_new_for_display(display, GDK_FLEUR);
7157 /* Grab the pointer */
7158 gtk_grab_add(gtkwin->notebook);
7159 device = gdk_event_get_device(event);
7160 if (!gdk_display_device_is_grabbed(gdk_device_get_display(device),
7161 device)) {
7162 gdk_seat_grab(gdk_event_get_seat(event),
7163 gtk_widget_get_window(gtkwin->notebook),
7164 GDK_SEAT_CAPABILITY_ALL_POINTING, FALSE, cursor, event,
7165 NULL, NULL);
7169 static gboolean
7170 notebook_motion_cb(GtkWidget *widget, GdkEventButton *e, PidginConvWindow *win)
7174 * Make sure the user moved the mouse far enough for the
7175 * drag to be initiated.
7177 if (win->in_predrag) {
7178 if (e->x_root < win->drag_min_x ||
7179 e->x_root >= win->drag_max_x ||
7180 e->y_root < win->drag_min_y ||
7181 e->y_root >= win->drag_max_y) {
7183 win->in_predrag = FALSE;
7184 notebook_init_grab(win, widget, (GdkEvent *)e);
7187 else { /* Otherwise, draw the arrows. */
7188 PidginConvWindow *dest_win;
7189 GtkNotebook *dest_notebook;
7190 GtkWidget *tab;
7191 gint page_num;
7192 gboolean horiz_tabs = FALSE;
7193 gboolean to_right = FALSE;
7195 /* Get the window that the cursor is over. */
7196 dest_win = pidgin_conv_window_get_at_event((GdkEvent *)e);
7198 if (dest_win == NULL) {
7199 pidgin_dnd_hints_hide_all();
7201 return TRUE;
7204 dest_notebook = GTK_NOTEBOOK(dest_win->notebook);
7206 if (gtk_notebook_get_show_tabs(dest_notebook)) {
7207 page_num = pidgin_conv_get_tab_at_xy(dest_win,
7208 e->x_root, e->y_root, &to_right);
7209 to_right = to_right && (win != dest_win);
7210 tab = pidgin_conv_window_get_gtkconv_at_index(dest_win, page_num)->tabby;
7211 } else {
7212 page_num = 0;
7213 to_right = pidgin_conv_xy_to_right_infopane(dest_win, e->x_root, e->y_root);
7214 tab = pidgin_conv_window_get_gtkconv_at_index(dest_win, page_num)->infopane_hbox;
7217 if (gtk_notebook_get_tab_pos(dest_notebook) == GTK_POS_TOP ||
7218 gtk_notebook_get_tab_pos(dest_notebook) == GTK_POS_BOTTOM) {
7219 horiz_tabs = TRUE;
7222 if (gtk_notebook_get_show_tabs(dest_notebook) == FALSE && win == dest_win)
7224 /* dragging a tab from a single-tabbed window over its own window */
7225 pidgin_dnd_hints_hide_all();
7226 return TRUE;
7227 } else if (horiz_tabs) {
7228 if (((gpointer)win == (gpointer)dest_win && win->drag_tab < page_num) || to_right) {
7229 pidgin_dnd_hints_show_relative(HINT_ARROW_DOWN, tab, HINT_POSITION_RIGHT, HINT_POSITION_TOP);
7230 pidgin_dnd_hints_show_relative(HINT_ARROW_UP, tab, HINT_POSITION_RIGHT, HINT_POSITION_BOTTOM);
7231 } else {
7232 pidgin_dnd_hints_show_relative(HINT_ARROW_DOWN, tab, HINT_POSITION_LEFT, HINT_POSITION_TOP);
7233 pidgin_dnd_hints_show_relative(HINT_ARROW_UP, tab, HINT_POSITION_LEFT, HINT_POSITION_BOTTOM);
7235 } else {
7236 if (((gpointer)win == (gpointer)dest_win && win->drag_tab < page_num) || to_right) {
7237 pidgin_dnd_hints_show_relative(HINT_ARROW_RIGHT, tab, HINT_POSITION_LEFT, HINT_POSITION_BOTTOM);
7238 pidgin_dnd_hints_show_relative(HINT_ARROW_LEFT, tab, HINT_POSITION_RIGHT, HINT_POSITION_BOTTOM);
7239 } else {
7240 pidgin_dnd_hints_show_relative(HINT_ARROW_RIGHT, tab, HINT_POSITION_LEFT, HINT_POSITION_TOP);
7241 pidgin_dnd_hints_show_relative(HINT_ARROW_LEFT, tab, HINT_POSITION_RIGHT, HINT_POSITION_TOP);
7246 return TRUE;
7249 static gboolean
7250 notebook_leave_cb(GtkWidget *widget, GdkEventCrossing *e, PidginConvWindow *win)
7252 if (win->in_drag)
7253 return FALSE;
7255 if (e->x_root < win->drag_min_x ||
7256 e->x_root >= win->drag_max_x ||
7257 e->y_root < win->drag_min_y ||
7258 e->y_root >= win->drag_max_y) {
7260 win->in_predrag = FALSE;
7261 notebook_init_grab(win, widget, (GdkEvent *)e);
7264 return TRUE;
7268 * THANK YOU GALEON!
7271 static gboolean
7272 infopane_press_cb(GtkWidget *widget, GdkEventButton *e, PidginConversation *gtkconv)
7274 if (e->type == GDK_2BUTTON_PRESS && e->button == GDK_BUTTON_PRIMARY) {
7275 if (infopane_entry_activate(gtkconv))
7276 return TRUE;
7279 if (e->type != GDK_BUTTON_PRESS)
7280 return FALSE;
7282 if (e->button == GDK_BUTTON_PRIMARY) {
7283 int nb_x, nb_y;
7284 GtkAllocation allocation;
7286 gtk_widget_get_allocation(gtkconv->infopane_hbox, &allocation);
7288 if (gtkconv->win->in_drag)
7289 return TRUE;
7291 gtkconv->win->in_predrag = TRUE;
7292 gtkconv->win->drag_tab = gtk_notebook_page_num(GTK_NOTEBOOK(gtkconv->win->notebook), gtkconv->tab_cont);
7294 gdk_window_get_origin(gtk_widget_get_window(gtkconv->infopane_hbox), &nb_x, &nb_y);
7296 gtkconv->win->drag_min_x = allocation.x + nb_x;
7297 gtkconv->win->drag_min_y = allocation.y + nb_y;
7298 gtkconv->win->drag_max_x = allocation.width + gtkconv->win->drag_min_x;
7299 gtkconv->win->drag_max_y = allocation.height + gtkconv->win->drag_min_y;
7301 gtkconv->win->drag_motion_signal = g_signal_connect(G_OBJECT(gtkconv->win->notebook), "motion_notify_event",
7302 G_CALLBACK(notebook_motion_cb), gtkconv->win);
7303 gtkconv->win->drag_leave_signal = g_signal_connect(G_OBJECT(gtkconv->win->notebook), "leave_notify_event",
7304 G_CALLBACK(notebook_leave_cb), gtkconv->win);
7305 return FALSE;
7308 if (gdk_event_triggers_context_menu((GdkEvent *)e)) {
7309 /* Right click was pressed. Popup the context menu. */
7310 GtkWidget *menu = gtk_menu_new(), *sub;
7311 gboolean populated = populate_menu_with_options(menu, gtkconv, TRUE);
7313 sub = gtk_menu_item_get_submenu(GTK_MENU_ITEM(gtkconv->win->menu->send_to));
7314 if (sub && gtk_widget_is_sensitive(gtkconv->win->menu->send_to)) {
7315 GtkWidget *item = gtk_menu_item_new_with_mnemonic(_("S_end To"));
7316 if (populated)
7317 pidgin_separator(menu);
7318 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
7319 gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), sub);
7320 gtk_widget_show(item);
7321 gtk_widget_show_all(sub);
7322 } else if (!populated) {
7323 gtk_widget_destroy(menu);
7324 return FALSE;
7327 gtk_widget_show_all(menu);
7328 gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent *)e);
7329 return TRUE;
7331 return FALSE;
7334 static gboolean
7335 notebook_press_cb(GtkWidget *widget, GdkEventButton *e, PidginConvWindow *win)
7337 gint nb_x, nb_y;
7338 int tab_clicked;
7339 GtkWidget *page;
7340 GtkWidget *tab;
7341 GtkAllocation allocation;
7343 if (e->button == GDK_BUTTON_MIDDLE && e->type == GDK_BUTTON_PRESS) {
7344 PidginConversation *gtkconv;
7345 tab_clicked = pidgin_conv_get_tab_at_xy(win, e->x_root, e->y_root, NULL);
7347 if (tab_clicked == -1)
7348 return FALSE;
7350 gtkconv = pidgin_conv_window_get_gtkconv_at_index(win, tab_clicked);
7351 close_conv_cb(NULL, gtkconv);
7352 return TRUE;
7356 if (e->button != GDK_BUTTON_PRIMARY || e->type != GDK_BUTTON_PRESS)
7357 return FALSE;
7360 if (win->in_drag) {
7361 purple_debug(PURPLE_DEBUG_WARNING, "gtkconv",
7362 "Already in the middle of a window drag at tab_press_cb\n");
7363 return TRUE;
7367 * Make sure a tab was actually clicked. The arrow buttons
7368 * mess things up.
7370 tab_clicked = pidgin_conv_get_tab_at_xy(win, e->x_root, e->y_root, NULL);
7372 if (tab_clicked == -1)
7373 return FALSE;
7376 * Get the relative position of the press event, with regards to
7377 * the position of the notebook.
7379 gdk_window_get_origin(gtk_widget_get_window(win->notebook), &nb_x, &nb_y);
7381 /* Reset the min/max x/y */
7382 win->drag_min_x = 0;
7383 win->drag_min_y = 0;
7384 win->drag_max_x = 0;
7385 win->drag_max_y = 0;
7387 /* Find out which tab was dragged. */
7388 page = gtk_notebook_get_nth_page(GTK_NOTEBOOK(win->notebook), tab_clicked);
7389 tab = gtk_notebook_get_tab_label(GTK_NOTEBOOK(win->notebook), page);
7391 gtk_widget_get_allocation(tab, &allocation);
7393 win->drag_min_x = allocation.x + nb_x;
7394 win->drag_min_y = allocation.y + nb_y;
7395 win->drag_max_x = allocation.width + win->drag_min_x;
7396 win->drag_max_y = allocation.height + win->drag_min_y;
7398 /* Make sure the click occurred in the tab. */
7399 if (e->x_root < win->drag_min_x ||
7400 e->x_root >= win->drag_max_x ||
7401 e->y_root < win->drag_min_y ||
7402 e->y_root >= win->drag_max_y) {
7404 return FALSE;
7407 win->in_predrag = TRUE;
7408 win->drag_tab = tab_clicked;
7410 /* Connect the new motion signals. */
7411 win->drag_motion_signal =
7412 g_signal_connect(G_OBJECT(widget), "motion_notify_event",
7413 G_CALLBACK(notebook_motion_cb), win);
7415 win->drag_leave_signal =
7416 g_signal_connect(G_OBJECT(widget), "leave_notify_event",
7417 G_CALLBACK(notebook_leave_cb), win);
7419 return FALSE;
7422 static gboolean
7423 notebook_release_cb(GtkWidget *widget, GdkEventButton *e, PidginConvWindow *win)
7425 PidginConvWindow *dest_win;
7426 GtkNotebook *dest_notebook;
7427 PidginConversation *active_gtkconv;
7428 PidginConversation *gtkconv;
7429 gint dest_page_num = 0;
7430 gboolean new_window = FALSE;
7431 gboolean to_right = FALSE;
7432 GdkDevice *device;
7435 * Don't check to make sure that the event's window matches the
7436 * widget's, because we may be getting an event passed on from the
7437 * close button.
7439 if (e->button != GDK_BUTTON_PRIMARY && e->type != GDK_BUTTON_RELEASE)
7440 return FALSE;
7442 device = gdk_event_get_device((GdkEvent *)e);
7443 if (gdk_display_device_is_grabbed(gdk_device_get_display(device), device)) {
7444 gdk_seat_ungrab(gdk_event_get_seat((GdkEvent *)e));
7445 gtk_grab_remove(widget);
7448 if (!win->in_predrag && !win->in_drag)
7449 return FALSE;
7451 /* Disconnect the motion signal. */
7452 if (win->drag_motion_signal) {
7453 g_signal_handler_disconnect(G_OBJECT(widget),
7454 win->drag_motion_signal);
7456 win->drag_motion_signal = 0;
7460 * If we're in a pre-drag, we'll also need to disconnect the leave
7461 * signal.
7463 if (win->in_predrag) {
7464 win->in_predrag = FALSE;
7466 if (win->drag_leave_signal) {
7467 g_signal_handler_disconnect(G_OBJECT(widget),
7468 win->drag_leave_signal);
7470 win->drag_leave_signal = 0;
7474 /* If we're not in drag... */
7475 /* We're perfectly normal people! */
7476 if (!win->in_drag)
7477 return FALSE;
7479 win->in_drag = FALSE;
7481 pidgin_dnd_hints_hide_all();
7483 dest_win = pidgin_conv_window_get_at_event((GdkEvent *)e);
7485 active_gtkconv = pidgin_conv_window_get_active_gtkconv(win);
7487 if (dest_win == NULL) {
7488 /* If the current window doesn't have any other conversations,
7489 * there isn't much point transferring the conv to a new window. */
7490 if (pidgin_conv_window_get_gtkconv_count(win) > 1) {
7491 /* Make a new window to stick this to. */
7492 dest_win = pidgin_conv_window_new();
7493 new_window = TRUE;
7497 if (dest_win == NULL)
7498 return FALSE;
7500 purple_signal_emit(pidgin_conversations_get_handle(),
7501 "conversation-dragging", win, dest_win);
7503 /* Get the destination page number. */
7504 if (!new_window) {
7505 dest_notebook = GTK_NOTEBOOK(dest_win->notebook);
7506 if (gtk_notebook_get_show_tabs(dest_notebook)) {
7507 dest_page_num = pidgin_conv_get_tab_at_xy(dest_win,
7508 e->x_root, e->y_root, &to_right);
7509 } else {
7510 dest_page_num = 0;
7511 to_right = pidgin_conv_xy_to_right_infopane(dest_win, e->x_root, e->y_root);
7515 gtkconv = pidgin_conv_window_get_gtkconv_at_index(win, win->drag_tab);
7517 if (win == dest_win) {
7518 gtk_notebook_reorder_child(GTK_NOTEBOOK(win->notebook), gtkconv->tab_cont, dest_page_num);
7519 } else {
7520 pidgin_conv_window_remove_gtkconv(win, gtkconv);
7521 pidgin_conv_window_add_gtkconv(dest_win, gtkconv);
7522 gtk_notebook_reorder_child(GTK_NOTEBOOK(dest_win->notebook), gtkconv->tab_cont, dest_page_num + to_right);
7523 pidgin_conv_window_switch_gtkconv(dest_win, gtkconv);
7524 if (new_window) {
7525 gint win_width, win_height;
7527 gtk_window_get_size(GTK_WINDOW(dest_win->window),
7528 &win_width, &win_height);
7529 #ifdef _WIN32 /* only override window manager placement on Windows */
7530 gtk_window_move(GTK_WINDOW(dest_win->window),
7531 e->x_root - (win_width / 2),
7532 e->y_root - (win_height / 2));
7533 #endif
7535 pidgin_conv_window_show(dest_win);
7539 gtk_widget_grab_focus(active_gtkconv->editor);
7541 return TRUE;
7545 static void
7546 before_switch_conv_cb(GtkNotebook *notebook, GtkWidget *page, gint page_num,
7547 gpointer user_data)
7549 PidginConvWindow *win;
7550 PurpleConversation *conv;
7551 PidginConversation *gtkconv;
7553 win = user_data;
7554 conv = pidgin_conv_window_get_active_conversation(win);
7556 g_return_if_fail(conv != NULL);
7558 if (!PURPLE_IS_IM_CONVERSATION(conv))
7559 return;
7561 gtkconv = PIDGIN_CONVERSATION(conv);
7563 if (gtkconv->u.im->typing_timer != 0) {
7564 g_source_remove(gtkconv->u.im->typing_timer);
7565 gtkconv->u.im->typing_timer = 0;
7568 stop_anim(NULL, gtkconv);
7571 static void
7572 close_window(GtkWidget *w, PidginConvWindow *win)
7574 close_win_cb(w, NULL, win);
7577 static void
7578 detach_tab_cb(GtkWidget *w, PidginConvWindow *win)
7580 PidginConvWindow *new_window;
7581 PidginConversation *gtkconv;
7583 gtkconv = win->clicked_tab;
7585 if (!gtkconv)
7586 return;
7588 /* Nothing to do if there's only one tab in the window */
7589 if (pidgin_conv_window_get_gtkconv_count(win) == 1)
7590 return;
7592 pidgin_conv_window_remove_gtkconv(win, gtkconv);
7594 new_window = pidgin_conv_window_new();
7595 pidgin_conv_window_add_gtkconv(new_window, gtkconv);
7596 pidgin_conv_window_show(new_window);
7599 static void
7600 close_others_cb(GtkWidget *w, PidginConvWindow *win)
7602 GList *iter;
7603 PidginConversation *gtkconv;
7605 gtkconv = win->clicked_tab;
7607 if (!gtkconv)
7608 return;
7610 for (iter = pidgin_conv_window_get_gtkconvs(win); iter; )
7612 PidginConversation *gconv = iter->data;
7613 iter = iter->next;
7615 if (gconv != gtkconv)
7617 close_conv_cb(NULL, gconv);
7622 static void
7623 close_tab_cb(GtkWidget *w, PidginConvWindow *win)
7625 PidginConversation *gtkconv;
7627 gtkconv = win->clicked_tab;
7629 if (gtkconv)
7630 close_conv_cb(NULL, gtkconv);
7633 static void
7634 notebook_menu_switch_cb(GtkWidget *item, GtkWidget *child)
7636 GtkNotebook *notebook;
7637 int index;
7639 notebook = GTK_NOTEBOOK(gtk_widget_get_parent(child));
7640 index = gtk_notebook_page_num(notebook, child);
7641 gtk_notebook_set_current_page(notebook, index);
7644 static void
7645 notebook_menu_update_label_cb(GtkWidget *child, GParamSpec *pspec,
7646 GtkNotebook *notebook)
7648 GtkWidget *item;
7649 GtkWidget *label;
7651 item = g_object_get_data(G_OBJECT(child), "popup-menu-item");
7652 label = gtk_bin_get_child(GTK_BIN(item));
7653 if (label)
7654 gtk_container_remove(GTK_CONTAINER(item), label);
7656 label = gtk_notebook_get_menu_label(notebook, child);
7657 if (label) {
7658 gtk_widget_show(label);
7659 gtk_container_add(GTK_CONTAINER(item), label);
7660 gtk_widget_show(item);
7661 } else {
7662 gtk_widget_hide(item);
7666 static void
7667 notebook_add_tab_to_menu_cb(GtkNotebook *notebook, GtkWidget *child,
7668 guint page_num, PidginConvWindow *win)
7670 GtkWidget *item;
7671 GtkWidget *label;
7673 item = gtk_menu_item_new();
7674 label = gtk_notebook_get_menu_label(notebook, child);
7675 if (label) {
7676 gtk_widget_show(label);
7677 gtk_container_add(GTK_CONTAINER(item), label);
7678 gtk_widget_show(item);
7681 g_signal_connect(child, "child-notify::menu-label",
7682 G_CALLBACK(notebook_menu_update_label_cb), notebook);
7683 g_signal_connect(item, "activate",
7684 G_CALLBACK(notebook_menu_switch_cb), child);
7685 g_object_set_data(G_OBJECT(child), "popup-menu-item", item);
7687 gtk_menu_shell_insert(GTK_MENU_SHELL(win->notebook_menu), item, page_num);
7690 static void
7691 notebook_remove_tab_from_menu_cb(GtkNotebook *notebook, GtkWidget *child,
7692 guint page_num, PidginConvWindow *win)
7694 GtkWidget *item;
7696 /* Disconnecting the "child-notify::menu-label" signal. */
7697 g_signal_handlers_disconnect_by_data(child, notebook);
7699 item = g_object_get_data(G_OBJECT(child), "popup-menu-item");
7700 gtk_container_remove(GTK_CONTAINER(win->notebook_menu), item);
7704 static void
7705 notebook_reorder_tab_in_menu_cb(GtkNotebook *notebook, GtkWidget *child,
7706 guint page_num, PidginConvWindow *win)
7708 GtkWidget *item;
7710 item = g_object_get_data(G_OBJECT(child), "popup-menu-item");
7711 gtk_menu_reorder_child(GTK_MENU(win->notebook_menu), item, page_num);
7714 static gboolean
7715 notebook_right_click_menu_cb(GtkNotebook *notebook, GdkEventButton *event,
7716 PidginConvWindow *win)
7718 GtkWidget *menu;
7719 PidginConversation *gtkconv;
7721 if (!gdk_event_triggers_context_menu((GdkEvent *)event))
7722 return FALSE;
7724 gtkconv = pidgin_conv_window_get_gtkconv_at_index(win,
7725 pidgin_conv_get_tab_at_xy(win, event->x_root, event->y_root, NULL));
7727 win->clicked_tab = gtkconv;
7729 menu = win->notebook_menu;
7731 gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent *)event);
7733 return TRUE;
7736 static void
7737 remove_edit_entry(PidginConversation *gtkconv, GtkWidget *entry)
7739 g_signal_handlers_disconnect_matched(G_OBJECT(entry), G_SIGNAL_MATCH_DATA,
7740 0, 0, NULL, NULL, gtkconv);
7741 gtk_widget_show(gtkconv->infopane);
7742 gtk_widget_grab_focus(gtkconv->editor);
7743 gtk_widget_destroy(entry);
7746 static gboolean
7747 alias_focus_cb(GtkWidget *widget, GdkEventFocus *event, gpointer user_data)
7749 remove_edit_entry(user_data, widget);
7750 return FALSE;
7753 static gboolean
7754 alias_key_press_cb(GtkWidget *widget, GdkEventKey *event, gpointer user_data)
7756 if (event->keyval == GDK_KEY_Escape) {
7757 remove_edit_entry(user_data, widget);
7758 return TRUE;
7760 return FALSE;
7763 static void
7764 alias_cb(GtkEntry *entry, gpointer user_data)
7766 PidginConversation *gtkconv;
7767 PurpleConversation *conv;
7768 PurpleAccount *account;
7769 const char *name;
7771 gtkconv = (PidginConversation *)user_data;
7772 if (gtkconv == NULL) {
7773 return;
7775 conv = gtkconv->active_conv;
7776 account = purple_conversation_get_account(conv);
7777 name = purple_conversation_get_name(conv);
7779 if (PURPLE_IS_IM_CONVERSATION(conv)) {
7780 PurpleBuddy *buddy;
7781 buddy = purple_blist_find_buddy(account, name);
7782 if (buddy != NULL) {
7783 purple_buddy_set_local_alias(buddy, gtk_entry_get_text(entry));
7785 purple_serv_alias_buddy(buddy);
7786 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
7787 gtk_entry_set_text(GTK_ENTRY(gtkconv->u.chat->topic_text), gtk_entry_get_text(entry));
7788 topic_callback(NULL, gtkconv);
7790 remove_edit_entry(user_data, GTK_WIDGET(entry));
7793 static gboolean
7794 infopane_entry_activate(PidginConversation *gtkconv)
7796 GtkWidget *entry = NULL;
7797 PurpleConversation *conv = gtkconv->active_conv;
7798 const char *text = NULL;
7800 if (!gtk_widget_get_visible(gtkconv->infopane)) {
7801 /* There's already an entry for alias. Let's not create another one. */
7802 return FALSE;
7805 if (!purple_account_is_connected(purple_conversation_get_account(gtkconv->active_conv))) {
7806 /* Do not allow aliasing someone on a disconnected account. */
7807 return FALSE;
7810 if (PURPLE_IS_IM_CONVERSATION(conv)) {
7811 PurpleBuddy *buddy = purple_blist_find_buddy(purple_conversation_get_account(gtkconv->active_conv), purple_conversation_get_name(gtkconv->active_conv));
7812 if (!buddy)
7813 /* This buddy isn't in your buddy list, so we can't alias him */
7814 return FALSE;
7816 text = purple_buddy_get_contact_alias(buddy);
7817 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
7818 PurpleConnection *gc;
7819 PurpleProtocol *protocol = NULL;
7821 gc = purple_conversation_get_connection(conv);
7822 if (gc != NULL)
7823 protocol = purple_connection_get_protocol(gc);
7824 if (protocol && !PURPLE_PROTOCOL_IMPLEMENTS(protocol, CHAT, set_topic))
7825 /* This protocol doesn't support setting the chat room topic */
7826 return FALSE;
7828 text = purple_chat_conversation_get_topic(PURPLE_CHAT_CONVERSATION(conv));
7831 /* alias label */
7832 entry = gtk_entry_new();
7833 gtk_entry_set_has_frame(GTK_ENTRY(entry), FALSE);
7834 gtk_entry_set_width_chars(GTK_ENTRY(entry), 10);
7835 gtk_entry_set_alignment(GTK_ENTRY(entry), 0.5);
7837 gtk_box_pack_start(GTK_BOX(gtkconv->infopane_hbox), entry, TRUE, TRUE, 0);
7838 /* after the tab label */
7839 gtk_box_reorder_child(GTK_BOX(gtkconv->infopane_hbox), entry, 0);
7841 g_signal_connect(G_OBJECT(entry), "activate", G_CALLBACK(alias_cb), gtkconv);
7842 g_signal_connect(G_OBJECT(entry), "focus-out-event", G_CALLBACK(alias_focus_cb), gtkconv);
7843 g_signal_connect(G_OBJECT(entry), "key-press-event", G_CALLBACK(alias_key_press_cb), gtkconv);
7845 if (text != NULL)
7846 gtk_entry_set_text(GTK_ENTRY(entry), text);
7847 gtk_widget_show(entry);
7848 gtk_widget_hide(gtkconv->infopane);
7849 gtk_widget_grab_focus(entry);
7851 return TRUE;
7854 static gboolean
7855 window_keypress_cb(GtkWidget *widget, GdkEventKey *event, PidginConvWindow *win)
7857 PidginConversation *gtkconv = pidgin_conv_window_get_active_gtkconv(win);
7859 return conv_keypress_common(gtkconv, event);
7862 static void
7863 switch_conv_cb(GtkNotebook *notebook, GtkWidget *page, gint page_num,
7864 gpointer user_data)
7866 PidginConvWindow *win;
7867 PurpleConversation *conv;
7868 PidginConversation *gtkconv;
7869 const char *sound_method;
7871 win = user_data;
7872 gtkconv = pidgin_conv_window_get_gtkconv_at_index(win, page_num);
7873 conv = gtkconv->active_conv;
7875 g_return_if_fail(conv != NULL);
7877 /* clear unseen flag if conversation is not hidden */
7878 if(!pidgin_conv_is_hidden(gtkconv)) {
7879 gtkconv_set_unseen(gtkconv, PIDGIN_UNSEEN_NONE);
7882 /* Update the menubar */
7884 gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(gtkconv->win->menu->logging),
7885 purple_conversation_is_logging(conv));
7887 generate_send_to_items(win);
7888 generate_e2ee_controls(win);
7889 regenerate_options_items(win);
7890 regenerate_plugins_items(win);
7892 pidgin_conv_switch_active_conversation(conv);
7894 sound_method = purple_prefs_get_string(PIDGIN_PREFS_ROOT "/sound/method");
7895 if (!purple_strequal(sound_method, "none"))
7896 gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(win->menu->sounds),
7897 gtkconv->make_sound);
7899 gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(win->menu->show_formatting_toolbar),
7900 purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/show_formatting_toolbar"));
7903 * We pause icons when they are not visible. If this icon should
7904 * be animated then start it back up again.
7906 if (PURPLE_IS_IM_CONVERSATION(conv) &&
7907 (gtkconv->u.im->animate))
7908 start_anim(NULL, gtkconv);
7910 purple_signal_emit(pidgin_conversations_get_handle(), "conversation-switched", conv);
7913 /**************************************************************************
7914 * GTK+ window ops
7915 **************************************************************************/
7917 GList *
7918 pidgin_conv_windows_get_list()
7920 return window_list;
7923 static GList*
7924 make_status_icon_list(const char *stock, GtkWidget *w)
7926 GList *l = NULL;
7927 l = g_list_append(l,
7928 gtk_widget_render_icon(w, stock,
7929 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL), "GtkWindow"));
7930 l = g_list_append(l,
7931 gtk_widget_render_icon(w, stock,
7932 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_SMALL), "GtkWindow"));
7933 l = g_list_append(l,
7934 gtk_widget_render_icon(w, stock,
7935 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_MEDIUM), "GtkWindow"));
7936 l = g_list_append(l,
7937 gtk_widget_render_icon(w, stock,
7938 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_LARGE), "GtkWindow"));
7939 return l;
7942 static void
7943 create_icon_lists(GtkWidget *w)
7945 available_list = make_status_icon_list(PIDGIN_STOCK_STATUS_AVAILABLE, w);
7946 busy_list = make_status_icon_list(PIDGIN_STOCK_STATUS_BUSY, w);
7947 xa_list = make_status_icon_list(PIDGIN_STOCK_STATUS_XA, w);
7948 offline_list = make_status_icon_list(PIDGIN_STOCK_STATUS_OFFLINE, w);
7949 away_list = make_status_icon_list(PIDGIN_STOCK_STATUS_AWAY, w);
7950 protocol_lists = g_hash_table_new(g_str_hash, g_str_equal);
7953 static void
7954 plugin_changed_cb(PurplePlugin *p, gpointer data)
7956 regenerate_plugins_items(data);
7959 static gboolean gtk_conv_configure_cb(GtkWidget *w, GdkEventConfigure *event, gpointer data) {
7960 int x, y;
7962 if (gtk_widget_get_visible(w))
7963 gtk_window_get_position(GTK_WINDOW(w), &x, &y);
7964 else
7965 return FALSE; /* carry on normally */
7967 /* Workaround for GTK+ bug # 169811 - "configure_event" is fired
7968 * when the window is being maximized */
7969 if (gdk_window_get_state(gtk_widget_get_window(w)) & GDK_WINDOW_STATE_MAXIMIZED)
7970 return FALSE;
7972 /* don't save off-screen positioning */
7973 if (x + event->width < 0 ||
7974 y + event->height < 0 ||
7975 x > gdk_screen_width() ||
7976 y > gdk_screen_height())
7977 return FALSE; /* carry on normally */
7979 /* store the position */
7980 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/x", x);
7981 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/y", y);
7982 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/width", event->width);
7983 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/height", event->height);
7985 /* continue to handle event normally */
7986 return FALSE;
7990 static void
7991 pidgin_conv_set_position_size(PidginConvWindow *win, int conv_x, int conv_y,
7992 int conv_width, int conv_height)
7994 /* if the window exists, is hidden, we're saving positions, and the
7995 * position is sane... */
7996 if (win && win->window &&
7997 !gtk_widget_get_visible(win->window) && conv_width != 0) {
7999 #ifdef _WIN32 /* only override window manager placement on Windows */
8000 /* ...check position is on screen... */
8001 if (conv_x >= gdk_screen_width())
8002 conv_x = gdk_screen_width() - 100;
8003 else if (conv_x + conv_width < 0)
8004 conv_x = 100;
8006 if (conv_y >= gdk_screen_height())
8007 conv_y = gdk_screen_height() - 100;
8008 else if (conv_y + conv_height < 0)
8009 conv_y = 100;
8011 /* ...and move it back. */
8012 gtk_window_move(GTK_WINDOW(win->window), conv_x, conv_y);
8013 #endif
8014 gtk_window_resize(GTK_WINDOW(win->window), conv_width, conv_height);
8018 static void
8019 pidgin_conv_restore_position(PidginConvWindow *win) {
8020 pidgin_conv_set_position_size(win,
8021 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/x"),
8022 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/y"),
8023 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/width"),
8024 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/height"));
8027 PidginConvWindow *
8028 pidgin_conv_window_new()
8030 PidginConvWindow *win;
8031 GtkPositionType pos;
8032 GtkWidget *testidea;
8033 GtkWidget *menubar;
8034 GtkWidget *menu;
8035 GtkWidget *item;
8036 GdkModifierType state;
8038 win = g_malloc0(sizeof(PidginConvWindow));
8039 win->menu = g_malloc0(sizeof(PidginConvWindowMenu));
8041 window_list = g_list_append(window_list, win);
8043 /* Create the window. */
8044 win->window = pidgin_create_window(NULL, 0, "conversation", TRUE);
8045 /*_pidgin_widget_set_accessible_name(win->window, "Conversations");*/
8046 if (!gtk_get_current_event_state(&state))
8047 gtk_window_set_focus_on_map(GTK_WINDOW(win->window), FALSE);
8049 /* Etan: I really think this entire function call should happen only
8050 * when we are on Windows but I was informed that back before we used
8051 * to save the window position we stored the window size, so I'm
8052 * leaving it for now. */
8053 #if TRUE || defined(_WIN32)
8054 pidgin_conv_restore_position(win);
8055 #endif
8057 if (available_list == NULL) {
8058 create_icon_lists(win->window);
8061 g_signal_connect(G_OBJECT(win->window), "delete_event",
8062 G_CALLBACK(close_win_cb), win);
8063 g_signal_connect(G_OBJECT(win->window), "focus_in_event",
8064 G_CALLBACK(focus_win_cb), win);
8066 /* Intercept keystrokes from the menu items */
8067 g_signal_connect(G_OBJECT(win->window), "key_press_event",
8068 G_CALLBACK(window_keypress_cb), win);
8071 /* Create the notebook. */
8072 win->notebook = gtk_notebook_new();
8074 pos = purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side");
8076 gtk_notebook_set_tab_pos(GTK_NOTEBOOK(win->notebook), pos);
8077 gtk_notebook_set_scrollable(GTK_NOTEBOOK(win->notebook), TRUE);
8078 gtk_notebook_set_show_tabs(GTK_NOTEBOOK(win->notebook), FALSE);
8079 gtk_notebook_set_show_border(GTK_NOTEBOOK(win->notebook), TRUE);
8081 menu = win->notebook_menu = gtk_menu_new();
8083 pidgin_separator(GTK_WIDGET(menu));
8085 item = gtk_menu_item_new_with_label(_("Close other tabs"));
8086 gtk_widget_show(item);
8087 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
8088 g_signal_connect(G_OBJECT(item), "activate",
8089 G_CALLBACK(close_others_cb), win);
8091 item = gtk_menu_item_new_with_label(_("Close all tabs"));
8092 gtk_widget_show(item);
8093 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
8094 g_signal_connect(G_OBJECT(item), "activate",
8095 G_CALLBACK(close_window), win);
8097 pidgin_separator(menu);
8099 item = gtk_menu_item_new_with_label(_("Detach this tab"));
8100 gtk_widget_show(item);
8101 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
8102 g_signal_connect(G_OBJECT(item), "activate",
8103 G_CALLBACK(detach_tab_cb), win);
8105 item = gtk_menu_item_new_with_label(_("Close this tab"));
8106 gtk_widget_show(item);
8107 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
8108 g_signal_connect(G_OBJECT(item), "activate",
8109 G_CALLBACK(close_tab_cb), win);
8111 g_signal_connect(G_OBJECT(win->notebook), "page-added",
8112 G_CALLBACK(notebook_add_tab_to_menu_cb), win);
8113 g_signal_connect(G_OBJECT(win->notebook), "page-removed",
8114 G_CALLBACK(notebook_remove_tab_from_menu_cb), win);
8115 g_signal_connect(G_OBJECT(win->notebook), "page-reordered",
8116 G_CALLBACK(notebook_reorder_tab_in_menu_cb), win);
8118 g_signal_connect(G_OBJECT(win->notebook), "button-press-event",
8119 G_CALLBACK(notebook_right_click_menu_cb), win);
8121 gtk_widget_show(win->notebook);
8123 g_signal_connect(G_OBJECT(win->notebook), "switch_page",
8124 G_CALLBACK(before_switch_conv_cb), win);
8125 g_signal_connect_after(G_OBJECT(win->notebook), "switch_page",
8126 G_CALLBACK(switch_conv_cb), win);
8128 /* Setup the tab drag and drop signals. */
8129 gtk_widget_add_events(win->notebook,
8130 GDK_BUTTON1_MOTION_MASK | GDK_LEAVE_NOTIFY_MASK);
8131 g_signal_connect(G_OBJECT(win->notebook), "button_press_event",
8132 G_CALLBACK(notebook_press_cb), win);
8133 g_signal_connect(G_OBJECT(win->notebook), "button_release_event",
8134 G_CALLBACK(notebook_release_cb), win);
8136 testidea = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
8138 /* Setup the menubar. */
8139 menubar = setup_menubar(win);
8140 gtk_box_pack_start(GTK_BOX(testidea), menubar, FALSE, TRUE, 0);
8142 gtk_box_pack_start(GTK_BOX(testidea), win->notebook, TRUE, TRUE, 0);
8144 gtk_container_add(GTK_CONTAINER(win->window), testidea);
8146 gtk_widget_show(testidea);
8148 /* Update the plugin actions when plugins are (un)loaded */
8149 purple_signal_connect(purple_plugins_get_handle(), "plugin-load",
8150 win, PURPLE_CALLBACK(plugin_changed_cb), win);
8151 purple_signal_connect(purple_plugins_get_handle(), "plugin-unload",
8152 win, PURPLE_CALLBACK(plugin_changed_cb), win);
8155 #ifdef _WIN32
8156 g_signal_connect(G_OBJECT(win->window), "show",
8157 G_CALLBACK(winpidgin_ensure_onscreen), win->window);
8159 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/win32/minimize_new_convs")
8160 && !gtk_get_current_event_state(&state))
8161 gtk_window_iconify(GTK_WINDOW(win->window));
8162 #endif
8164 purple_signal_emit(pidgin_conversations_get_handle(),
8165 "conversation-window-created", win);
8167 /* Fix colours */
8168 pidgin_conversations_set_tab_colors();
8170 return win;
8173 void
8174 pidgin_conv_window_destroy(PidginConvWindow *win)
8176 if (win->gtkconvs) {
8177 GList *iter = win->gtkconvs;
8178 while (iter)
8180 PidginConversation *gtkconv = iter->data;
8181 iter = iter->next;
8182 close_conv_cb(NULL, gtkconv);
8184 return;
8187 purple_prefs_disconnect_by_handle(win);
8188 window_list = g_list_remove(window_list, win);
8190 gtk_widget_destroy(win->notebook_menu);
8191 gtk_widget_destroy(win->window);
8193 g_object_unref(G_OBJECT(win->menu->ui));
8195 purple_notify_close_with_handle(win);
8196 purple_signals_disconnect_by_handle(win);
8198 g_free(win->menu);
8199 g_free(win);
8202 void
8203 pidgin_conv_window_show(PidginConvWindow *win)
8205 gtk_widget_show(win->window);
8208 void
8209 pidgin_conv_window_hide(PidginConvWindow *win)
8211 gtk_widget_hide(win->window);
8214 void
8215 pidgin_conv_window_raise(PidginConvWindow *win)
8217 gdk_window_raise(GDK_WINDOW(gtk_widget_get_window(win->window)));
8220 void
8221 pidgin_conv_window_switch_gtkconv(PidginConvWindow *win, PidginConversation *gtkconv)
8223 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook),
8224 gtk_notebook_page_num(GTK_NOTEBOOK(win->notebook),
8225 gtkconv->tab_cont));
8228 static gboolean
8229 gtkconv_tab_set_tip(GtkWidget *widget, GdkEventCrossing *event, PidginConversation *gtkconv)
8231 /* PANGO_VERSION_CHECK macro was introduced in 1.15. So we need this double check. */
8232 #ifndef PANGO_VERSION_CHECK
8233 #define pango_layout_is_ellipsized(l) TRUE
8234 #elif !PANGO_VERSION_CHECK(1,16,0)
8235 #define pango_layout_is_ellipsized(l) TRUE
8236 #endif
8237 PangoLayout *layout;
8239 layout = gtk_label_get_layout(GTK_LABEL(gtkconv->tab_label));
8240 if (pango_layout_is_ellipsized(layout))
8241 gtk_widget_set_tooltip_text(widget, gtk_label_get_text(GTK_LABEL(gtkconv->tab_label)));
8242 else
8243 gtk_widget_set_tooltip_text(widget, NULL);
8245 return FALSE;
8248 static void
8249 set_default_tab_colors(GtkWidget *widget)
8251 GString *str;
8252 GtkCssProvider *provider;
8253 GError *error = NULL;
8254 int iter;
8256 struct {
8257 const char *labelname;
8258 const char *color;
8259 } styles[] = {
8260 {"tab-label-typing", "#4e9a06"},
8261 {"tab-label-typed", "#c4a000"},
8262 {"tab-label-attention", "#006aff"},
8263 {"tab-label-unreadchat", "#cc0000"},
8264 {"tab-label-event", "#888a85"},
8265 {NULL, NULL}
8268 str = g_string_new(NULL);
8270 for (iter = 0; styles[iter].labelname; iter++) {
8271 g_string_append_printf(str,
8272 "#%s {\n"
8273 " color: %s;\n"
8274 "}\n",
8275 styles[iter].labelname,
8276 styles[iter].color);
8279 provider = gtk_css_provider_new();
8281 gtk_css_provider_load_from_data(provider, str->str, str->len, &error);
8283 gtk_style_context_add_provider(gtk_widget_get_style_context(widget),
8284 GTK_STYLE_PROVIDER(provider),
8285 GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
8287 if (error)
8288 g_error_free(error);
8289 g_string_free(str, TRUE);
8292 void
8293 pidgin_conv_window_add_gtkconv(PidginConvWindow *win, PidginConversation *gtkconv)
8295 PurpleConversation *conv = gtkconv->active_conv;
8296 PidginConversation *focus_gtkconv;
8297 GtkWidget *tab_cont = gtkconv->tab_cont;
8298 const gchar *tmp_lab;
8300 win->gtkconvs = g_list_append(win->gtkconvs, gtkconv);
8301 gtkconv->win = win;
8303 if (win->gtkconvs && win->gtkconvs->next && win->gtkconvs->next->next == NULL)
8304 pidgin_conv_tab_pack(win, ((PidginConversation*)win->gtkconvs->data));
8307 /* Close button. */
8308 gtkconv->close = pidgin_create_small_button(gtk_label_new("×"));
8309 gtk_widget_set_tooltip_text(gtkconv->close, _("Close conversation"));
8311 g_signal_connect(gtkconv->close, "clicked", G_CALLBACK (close_conv_cb), gtkconv);
8313 /* Status icon. */
8314 gtkconv->icon = gtk_image_new();
8315 gtkconv->menu_icon = gtk_image_new();
8316 g_object_set(G_OBJECT(gtkconv->icon),
8317 "icon-size", gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_MICROSCOPIC),
8318 NULL);
8319 g_object_set(G_OBJECT(gtkconv->menu_icon),
8320 "icon-size", gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_MICROSCOPIC),
8321 NULL);
8322 gtk_widget_show(gtkconv->icon);
8323 update_tab_icon(conv);
8325 /* Tab label. */
8326 gtkconv->tab_label = gtk_label_new(tmp_lab = purple_conversation_get_title(conv));
8327 set_default_tab_colors(gtkconv->tab_label);
8328 gtk_widget_set_name(gtkconv->tab_label, "tab-label");
8330 gtkconv->menu_tabby = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PIDGIN_HIG_BOX_SPACE);
8331 gtkconv->menu_label = gtk_label_new(tmp_lab);
8332 gtk_box_pack_start(GTK_BOX(gtkconv->menu_tabby), gtkconv->menu_icon, FALSE, FALSE, 0);
8334 gtk_widget_show_all(gtkconv->menu_icon);
8336 gtk_box_pack_start(GTK_BOX(gtkconv->menu_tabby), gtkconv->menu_label, TRUE, TRUE, 0);
8337 gtk_widget_show(gtkconv->menu_label);
8338 gtk_label_set_xalign(GTK_LABEL(gtkconv->menu_label), 0);
8339 gtk_label_set_yalign(GTK_LABEL(gtkconv->menu_label), 0);
8341 gtk_widget_show(gtkconv->menu_tabby);
8343 if (PURPLE_IS_IM_CONVERSATION(conv))
8344 pidgin_conv_update_buddy_icon(PURPLE_IM_CONVERSATION(conv));
8346 /* Build and set conversations tab */
8347 pidgin_conv_tab_pack(win, gtkconv);
8349 gtk_notebook_set_menu_label(GTK_NOTEBOOK(win->notebook), tab_cont, gtkconv->menu_tabby);
8351 gtk_widget_show(tab_cont);
8353 if (pidgin_conv_window_get_gtkconv_count(win) == 1) {
8354 /* Er, bug in notebooks? Switch to the page manually. */
8355 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), 0);
8356 } else {
8357 gtk_notebook_set_show_tabs(GTK_NOTEBOOK(win->notebook), TRUE);
8360 focus_gtkconv = g_list_nth_data(pidgin_conv_window_get_gtkconvs(win),
8361 gtk_notebook_get_current_page(GTK_NOTEBOOK(win->notebook)));
8362 gtk_widget_grab_focus(focus_gtkconv->editor);
8364 if (pidgin_conv_window_get_gtkconv_count(win) == 1)
8365 update_send_to_selection(win);
8368 static void
8369 pidgin_conv_tab_pack(PidginConvWindow *win, PidginConversation *gtkconv)
8371 gboolean tabs_side = FALSE;
8372 gint angle = 0;
8373 GtkWidget *first, *third, *ebox, *parent;
8375 if (purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") == GTK_POS_LEFT ||
8376 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") == GTK_POS_RIGHT)
8377 tabs_side = TRUE;
8378 else if (purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") == (GTK_POS_LEFT|8))
8379 angle = 90;
8380 else if (purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") == (GTK_POS_RIGHT|8))
8381 angle = 270;
8383 if (!angle) {
8384 g_object_set(G_OBJECT(gtkconv->tab_label), "ellipsize", PANGO_ELLIPSIZE_END, NULL);
8385 gtk_label_set_width_chars(GTK_LABEL(gtkconv->tab_label), 4);
8386 } else {
8387 g_object_set(G_OBJECT(gtkconv->tab_label), "ellipsize", PANGO_ELLIPSIZE_NONE, NULL);
8388 gtk_label_set_width_chars(GTK_LABEL(gtkconv->tab_label), -1);
8391 if (tabs_side) {
8392 gtk_label_set_width_chars(
8393 GTK_LABEL(gtkconv->tab_label),
8394 MIN(g_utf8_strlen(gtk_label_get_text(GTK_LABEL(gtkconv->tab_label)), -1), 12)
8398 gtk_label_set_angle(GTK_LABEL(gtkconv->tab_label), angle);
8400 if (angle)
8401 gtkconv->tabby = gtk_box_new(GTK_ORIENTATION_VERTICAL, PIDGIN_HIG_BOX_SPACE);
8402 else
8403 gtkconv->tabby = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PIDGIN_HIG_BOX_SPACE);
8404 gtk_widget_set_name(gtkconv->tabby, "tab-container");
8406 /* select the correct ordering for verticle tabs */
8407 if (angle == 90) {
8408 first = gtkconv->close;
8409 third = gtkconv->icon;
8410 } else {
8411 first = gtkconv->icon;
8412 third = gtkconv->close;
8415 ebox = gtk_event_box_new();
8416 gtk_event_box_set_visible_window(GTK_EVENT_BOX(ebox), FALSE);
8417 gtk_container_add(GTK_CONTAINER(ebox), gtkconv->tabby);
8418 g_signal_connect(G_OBJECT(ebox), "enter-notify-event",
8419 G_CALLBACK(gtkconv_tab_set_tip), gtkconv);
8421 parent = gtk_widget_get_parent(gtkconv->tab_label);
8422 if (parent != NULL) {
8423 /* reparent old widgets on preference changes */
8424 g_object_ref(first);
8425 g_object_ref(gtkconv->tab_label);
8426 g_object_ref(third);
8427 gtk_container_remove(GTK_CONTAINER(parent), first);
8428 gtk_container_remove(GTK_CONTAINER(parent), gtkconv->tab_label);
8429 gtk_container_remove(GTK_CONTAINER(parent), third);
8432 gtk_box_pack_start(GTK_BOX(gtkconv->tabby), first, FALSE, FALSE, 0);
8433 gtk_box_pack_start(GTK_BOX(gtkconv->tabby), gtkconv->tab_label, TRUE, TRUE, 0);
8434 gtk_box_pack_start(GTK_BOX(gtkconv->tabby), third, FALSE, FALSE, 0);
8436 if (parent == NULL) {
8437 /* Add this pane to the conversation's notebook. */
8438 gtk_notebook_append_page(GTK_NOTEBOOK(win->notebook), gtkconv->tab_cont, ebox);
8439 } else {
8440 /* reparent old widgets on preference changes */
8441 g_object_unref(first);
8442 g_object_unref(gtkconv->tab_label);
8443 g_object_unref(third);
8445 /* Reset the tabs label to the new version */
8446 gtk_notebook_set_tab_label(GTK_NOTEBOOK(win->notebook), gtkconv->tab_cont, ebox);
8449 gtk_container_child_set(GTK_CONTAINER(win->notebook), gtkconv->tab_cont,
8450 "tab-expand", !tabs_side && !angle,
8451 "tab-fill", TRUE, NULL);
8453 if (pidgin_conv_window_get_gtkconv_count(win) == 1)
8454 gtk_notebook_set_show_tabs(GTK_NOTEBOOK(win->notebook),
8455 purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/tabs") &&
8456 (!purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons") ||
8457 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") != GTK_POS_TOP));
8459 /* show the widgets */
8460 /* gtk_widget_show(gtkconv->icon); */
8461 gtk_widget_show(gtkconv->tab_label);
8462 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/close_on_tabs"))
8463 gtk_widget_show(gtkconv->close);
8464 gtk_widget_show(gtkconv->tabby);
8465 gtk_widget_show(ebox);
8468 void
8469 pidgin_conv_window_remove_gtkconv(PidginConvWindow *win, PidginConversation *gtkconv)
8471 unsigned int index;
8473 index = gtk_notebook_page_num(GTK_NOTEBOOK(win->notebook), gtkconv->tab_cont);
8475 g_object_ref_sink(G_OBJECT(gtkconv->tab_cont));
8477 gtk_notebook_remove_page(GTK_NOTEBOOK(win->notebook), index);
8479 win->gtkconvs = g_list_remove(win->gtkconvs, gtkconv);
8481 g_signal_handlers_disconnect_matched(win->window, G_SIGNAL_MATCH_DATA,
8482 0, 0, NULL, NULL, gtkconv);
8484 if (win->gtkconvs && win->gtkconvs->next == NULL)
8485 pidgin_conv_tab_pack(win, win->gtkconvs->data);
8487 if (!win->gtkconvs && win != hidden_convwin)
8488 pidgin_conv_window_destroy(win);
8491 PidginConversation *
8492 pidgin_conv_window_get_gtkconv_at_index(const PidginConvWindow *win, int index)
8494 GtkWidget *tab_cont;
8496 if (index == -1)
8497 index = 0;
8498 tab_cont = gtk_notebook_get_nth_page(GTK_NOTEBOOK(win->notebook), index);
8499 return tab_cont ? g_object_get_data(G_OBJECT(tab_cont), "PidginConversation") : NULL;
8502 PidginConversation *
8503 pidgin_conv_window_get_active_gtkconv(const PidginConvWindow *win)
8505 int index;
8506 GtkWidget *tab_cont;
8508 index = gtk_notebook_get_current_page(GTK_NOTEBOOK(win->notebook));
8509 if (index == -1)
8510 index = 0;
8511 tab_cont = gtk_notebook_get_nth_page(GTK_NOTEBOOK(win->notebook), index);
8512 if (!tab_cont)
8513 return NULL;
8514 return g_object_get_data(G_OBJECT(tab_cont), "PidginConversation");
8518 PurpleConversation *
8519 pidgin_conv_window_get_active_conversation(const PidginConvWindow *win)
8521 PidginConversation *gtkconv;
8523 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
8524 return gtkconv ? gtkconv->active_conv : NULL;
8527 gboolean
8528 pidgin_conv_window_is_active_conversation(const PurpleConversation *conv)
8530 return conv == pidgin_conv_window_get_active_conversation(PIDGIN_CONVERSATION(conv)->win);
8533 gboolean
8534 pidgin_conv_window_has_focus(PidginConvWindow *win)
8536 gboolean has_focus = FALSE;
8538 g_object_get(G_OBJECT(win->window), "has-toplevel-focus", &has_focus, NULL);
8540 return has_focus;
8543 PidginConvWindow *
8544 pidgin_conv_window_get_at_event(GdkEvent *event)
8546 PidginConvWindow *win;
8547 GdkWindow *gdkwin;
8548 GList *l;
8549 int x, y;
8551 gdkwin = gdk_device_get_window_at_position(gdk_event_get_device(event),
8552 &x, &y);
8554 if (gdkwin)
8555 gdkwin = gdk_window_get_toplevel(gdkwin);
8557 for (l = pidgin_conv_windows_get_list(); l != NULL; l = l->next) {
8558 win = l->data;
8560 if (gdkwin == gtk_widget_get_window(win->window))
8561 return win;
8564 return NULL;
8567 GList *
8568 pidgin_conv_window_get_gtkconvs(PidginConvWindow *win)
8570 return win->gtkconvs;
8573 guint
8574 pidgin_conv_window_get_gtkconv_count(PidginConvWindow *win)
8576 return g_list_length(win->gtkconvs);
8579 PidginConvWindow *
8580 pidgin_conv_window_first_im(void)
8582 GList *wins, *convs;
8583 PidginConvWindow *win;
8584 PidginConversation *conv;
8586 for (wins = pidgin_conv_windows_get_list(); wins != NULL; wins = wins->next) {
8587 win = wins->data;
8589 for (convs = win->gtkconvs;
8590 convs != NULL;
8591 convs = convs->next) {
8593 conv = convs->data;
8595 if (PURPLE_IS_IM_CONVERSATION(conv->active_conv))
8596 return win;
8600 return NULL;
8603 PidginConvWindow *
8604 pidgin_conv_window_last_im(void)
8606 GList *wins, *convs;
8607 PidginConvWindow *win;
8608 PidginConversation *conv;
8610 for (wins = g_list_last(pidgin_conv_windows_get_list());
8611 wins != NULL;
8612 wins = wins->prev) {
8614 win = wins->data;
8616 for (convs = win->gtkconvs;
8617 convs != NULL;
8618 convs = convs->next) {
8620 conv = convs->data;
8622 if (PURPLE_IS_IM_CONVERSATION(conv->active_conv))
8623 return win;
8627 return NULL;
8630 PidginConvWindow *
8631 pidgin_conv_window_first_chat(void)
8633 GList *wins, *convs;
8634 PidginConvWindow *win;
8635 PidginConversation *conv;
8637 for (wins = pidgin_conv_windows_get_list(); wins != NULL; wins = wins->next) {
8638 win = wins->data;
8640 for (convs = win->gtkconvs;
8641 convs != NULL;
8642 convs = convs->next) {
8644 conv = convs->data;
8646 if (PURPLE_IS_CHAT_CONVERSATION(conv->active_conv))
8647 return win;
8651 return NULL;
8654 PidginConvWindow *
8655 pidgin_conv_window_last_chat(void)
8657 GList *wins, *convs;
8658 PidginConvWindow *win;
8659 PidginConversation *conv;
8661 for (wins = g_list_last(pidgin_conv_windows_get_list());
8662 wins != NULL;
8663 wins = wins->prev) {
8665 win = wins->data;
8667 for (convs = win->gtkconvs;
8668 convs != NULL;
8669 convs = convs->next) {
8671 conv = convs->data;
8673 if (PURPLE_IS_CHAT_CONVERSATION(conv->active_conv))
8674 return win;
8678 return NULL;
8682 /**************************************************************************
8683 * Conversation placement functions
8684 **************************************************************************/
8685 typedef struct
8687 char *id;
8688 char *name;
8689 PidginConvPlacementFunc fnc;
8691 } ConvPlacementData;
8693 static GList *conv_placement_fncs = NULL;
8694 static PidginConvPlacementFunc place_conv = NULL;
8696 /* This one places conversations in the last made window. */
8697 static void
8698 conv_placement_last_created_win(PidginConversation *conv)
8700 PidginConvWindow *win;
8702 GList *l = g_list_last(pidgin_conv_windows_get_list());
8703 win = l ? l->data : NULL;;
8705 if (win == NULL) {
8706 win = pidgin_conv_window_new();
8708 g_signal_connect(G_OBJECT(win->window), "configure_event",
8709 G_CALLBACK(gtk_conv_configure_cb), NULL);
8711 pidgin_conv_window_add_gtkconv(win, conv);
8712 pidgin_conv_window_show(win);
8713 } else {
8714 pidgin_conv_window_add_gtkconv(win, conv);
8718 /* This one places conversations in the last made window of the same type. */
8719 static gboolean
8720 conv_placement_last_created_win_type_configured_cb(GtkWidget *w,
8721 GdkEventConfigure *event, PidginConversation *conv)
8723 int x, y;
8724 GList *all;
8726 if (gtk_widget_get_visible(w))
8727 gtk_window_get_position(GTK_WINDOW(w), &x, &y);
8728 else
8729 return FALSE; /* carry on normally */
8731 /* Workaround for GTK+ bug # 169811 - "configure_event" is fired
8732 * when the window is being maximized */
8733 if (gdk_window_get_state(gtk_widget_get_window(w)) & GDK_WINDOW_STATE_MAXIMIZED)
8734 return FALSE;
8736 /* don't save off-screen positioning */
8737 if (x + event->width < 0 ||
8738 y + event->height < 0 ||
8739 x > gdk_screen_width() ||
8740 y > gdk_screen_height())
8741 return FALSE; /* carry on normally */
8743 for (all = conv->convs; all != NULL; all = all->next) {
8744 if (PURPLE_IS_IM_CONVERSATION(conv->active_conv) != PURPLE_IS_IM_CONVERSATION(all->data)) {
8745 /* this window has different types of conversation, don't save */
8746 return FALSE;
8750 if (PURPLE_IS_IM_CONVERSATION(conv->active_conv)) {
8751 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/x", x);
8752 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/y", y);
8753 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/width", event->width);
8754 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/height", event->height);
8755 } else if (PURPLE_IS_CHAT_CONVERSATION(conv->active_conv)) {
8756 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/chat/x", x);
8757 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/chat/y", y);
8758 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/chat/width", event->width);
8759 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/chat/height", event->height);
8762 return FALSE;
8765 static void
8766 conv_placement_last_created_win_type(PidginConversation *conv)
8768 PidginConvWindow *win;
8770 if (PURPLE_IS_IM_CONVERSATION(conv->active_conv))
8771 win = pidgin_conv_window_last_im();
8772 else
8773 win = pidgin_conv_window_last_chat();
8775 if (win == NULL) {
8776 win = pidgin_conv_window_new();
8778 if (PURPLE_IS_IM_CONVERSATION(conv->active_conv) ||
8779 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/width") == 0) {
8780 pidgin_conv_set_position_size(win,
8781 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/x"),
8782 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/y"),
8783 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/width"),
8784 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/height"));
8785 } else if (PURPLE_IS_CHAT_CONVERSATION(conv->active_conv)) {
8786 pidgin_conv_set_position_size(win,
8787 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/x"),
8788 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/y"),
8789 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/width"),
8790 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/height"));
8793 pidgin_conv_window_add_gtkconv(win, conv);
8794 pidgin_conv_window_show(win);
8796 g_signal_connect(G_OBJECT(win->window), "configure_event",
8797 G_CALLBACK(conv_placement_last_created_win_type_configured_cb), conv);
8798 } else
8799 pidgin_conv_window_add_gtkconv(win, conv);
8802 /* This one places each conversation in its own window. */
8803 static void
8804 conv_placement_new_window(PidginConversation *conv)
8806 PidginConvWindow *win;
8808 win = pidgin_conv_window_new();
8810 g_signal_connect(G_OBJECT(win->window), "configure_event",
8811 G_CALLBACK(gtk_conv_configure_cb), NULL);
8813 pidgin_conv_window_add_gtkconv(win, conv);
8815 pidgin_conv_window_show(win);
8818 static PurpleGroup *
8819 conv_get_group(PidginConversation *conv)
8821 PurpleGroup *group = NULL;
8823 if (PURPLE_IS_IM_CONVERSATION(conv->active_conv)) {
8824 PurpleBuddy *buddy;
8826 buddy = purple_blist_find_buddy(purple_conversation_get_account(conv->active_conv),
8827 purple_conversation_get_name(conv->active_conv));
8829 if (buddy != NULL)
8830 group = purple_buddy_get_group(buddy);
8832 } else if (PURPLE_IS_CHAT_CONVERSATION(conv->active_conv)) {
8833 PurpleChat *chat;
8835 chat = purple_blist_find_chat(purple_conversation_get_account(conv->active_conv),
8836 purple_conversation_get_name(conv->active_conv));
8838 if (chat != NULL)
8839 group = purple_chat_get_group(chat);
8842 return group;
8846 * This groups things by, well, group. Buddies from groups will always be
8847 * grouped together, and a buddy from a group not belonging to any currently
8848 * open windows will get a new window.
8850 static void
8851 conv_placement_by_group(PidginConversation *conv)
8853 PurpleGroup *group = NULL;
8854 GList *wl, *cl;
8856 group = conv_get_group(conv);
8858 /* Go through the list of IMs and find one with this group. */
8859 for (wl = pidgin_conv_windows_get_list(); wl != NULL; wl = wl->next) {
8860 PidginConvWindow *win2;
8861 PidginConversation *conv2;
8862 PurpleGroup *group2 = NULL;
8864 win2 = wl->data;
8866 for (cl = win2->gtkconvs;
8867 cl != NULL;
8868 cl = cl->next) {
8869 conv2 = cl->data;
8871 group2 = conv_get_group(conv2);
8873 if (group == group2) {
8874 pidgin_conv_window_add_gtkconv(win2, conv);
8876 return;
8881 /* Make a new window. */
8882 conv_placement_new_window(conv);
8885 /* This groups things by account. Otherwise, the same semantics as above */
8886 static void
8887 conv_placement_by_account(PidginConversation *conv)
8889 GList *wins, *convs;
8890 PurpleAccount *account;
8892 account = purple_conversation_get_account(conv->active_conv);
8894 /* Go through the list of IMs and find one with this group. */
8895 for (wins = pidgin_conv_windows_get_list(); wins != NULL; wins = wins->next) {
8896 PidginConvWindow *win2;
8897 PidginConversation *conv2;
8899 win2 = wins->data;
8901 for (convs = win2->gtkconvs;
8902 convs != NULL;
8903 convs = convs->next) {
8904 conv2 = convs->data;
8906 if (account == purple_conversation_get_account(conv2->active_conv)) {
8907 pidgin_conv_window_add_gtkconv(win2, conv);
8908 return;
8913 /* Make a new window. */
8914 conv_placement_new_window(conv);
8917 static ConvPlacementData *
8918 get_conv_placement_data(const char *id)
8920 ConvPlacementData *data = NULL;
8921 GList *n;
8923 for (n = conv_placement_fncs; n; n = n->next) {
8924 data = n->data;
8925 if (purple_strequal(data->id, id))
8926 return data;
8929 return NULL;
8932 static void
8933 add_conv_placement_fnc(const char *id, const char *name,
8934 PidginConvPlacementFunc fnc)
8936 ConvPlacementData *data;
8938 data = g_new(ConvPlacementData, 1);
8940 data->id = g_strdup(id);
8941 data->name = g_strdup(name);
8942 data->fnc = fnc;
8944 conv_placement_fncs = g_list_append(conv_placement_fncs, data);
8947 static void
8948 ensure_default_funcs(void)
8950 if (conv_placement_fncs == NULL) {
8951 add_conv_placement_fnc("last", _("Last created window"),
8952 conv_placement_last_created_win);
8953 add_conv_placement_fnc("im_chat", _("Separate IM and Chat windows"),
8954 conv_placement_last_created_win_type);
8955 add_conv_placement_fnc("new", _("New window"),
8956 conv_placement_new_window);
8957 add_conv_placement_fnc("group", _("By group"),
8958 conv_placement_by_group);
8959 add_conv_placement_fnc("account", _("By account"),
8960 conv_placement_by_account);
8964 GList *
8965 pidgin_conv_placement_get_options(void)
8967 GList *n, *list = NULL;
8968 ConvPlacementData *data;
8970 ensure_default_funcs();
8972 for (n = conv_placement_fncs; n; n = n->next) {
8973 data = n->data;
8974 list = g_list_append(list, data->name);
8975 list = g_list_append(list, data->id);
8978 return list;
8982 void
8983 pidgin_conv_placement_add_fnc(const char *id, const char *name,
8984 PidginConvPlacementFunc fnc)
8986 g_return_if_fail(id != NULL);
8987 g_return_if_fail(name != NULL);
8988 g_return_if_fail(fnc != NULL);
8990 ensure_default_funcs();
8992 add_conv_placement_fnc(id, name, fnc);
8995 void
8996 pidgin_conv_placement_remove_fnc(const char *id)
8998 ConvPlacementData *data = get_conv_placement_data(id);
9000 if (data == NULL)
9001 return;
9003 conv_placement_fncs = g_list_remove(conv_placement_fncs, data);
9005 g_free(data->id);
9006 g_free(data->name);
9007 g_free(data);
9010 const char *
9011 pidgin_conv_placement_get_name(const char *id)
9013 ConvPlacementData *data;
9015 ensure_default_funcs();
9017 data = get_conv_placement_data(id);
9019 if (data == NULL)
9020 return NULL;
9022 return data->name;
9025 PidginConvPlacementFunc
9026 pidgin_conv_placement_get_fnc(const char *id)
9028 ConvPlacementData *data;
9030 ensure_default_funcs();
9032 data = get_conv_placement_data(id);
9034 if (data == NULL)
9035 return NULL;
9037 return data->fnc;
9040 void
9041 pidgin_conv_placement_set_current_func(PidginConvPlacementFunc func)
9043 g_return_if_fail(func != NULL);
9045 /* If tabs are enabled, set the function, otherwise, NULL it out. */
9046 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/tabs"))
9047 place_conv = func;
9048 else
9049 place_conv = NULL;
9052 PidginConvPlacementFunc
9053 pidgin_conv_placement_get_current_func(void)
9055 return place_conv;
9058 void
9059 pidgin_conv_placement_place(PidginConversation *gtkconv)
9061 if (place_conv)
9062 place_conv(gtkconv);
9063 else
9064 conv_placement_new_window(gtkconv);
9067 gboolean
9068 pidgin_conv_is_hidden(PidginConversation *gtkconv)
9070 g_return_val_if_fail(gtkconv != NULL, FALSE);
9072 return (gtkconv->win == hidden_convwin);
9076 gdouble luminance(GdkRGBA color)
9078 gdouble r, g, b;
9079 gdouble rr, gg, bb;
9080 gdouble cutoff = 0.03928, scale = 12.92;
9081 gdouble a = 0.055, d = 1.055, p = 2.2;
9083 rr = color.red;
9084 gg = color.green;
9085 bb = color.blue;
9087 r = (rr > cutoff) ? pow((rr+a)/d, p) : rr/scale;
9088 g = (gg > cutoff) ? pow((gg+a)/d, p) : gg/scale;
9089 b = (bb > cutoff) ? pow((bb+a)/d, p) : bb/scale;
9091 return (r*0.2126 + g*0.7152 + b*0.0722);
9094 /* Algorithm from https://www.w3.org/TR/2008/REC-WCAG20-20081211/relative-luminance.xml */
9095 static gboolean
9096 color_is_visible(GdkRGBA foreground, GdkRGBA background, gdouble min_contrast_ratio)
9098 gdouble lfg, lbg, lmin, lmax;
9099 gdouble luminosity_ratio;
9100 gdouble nr, dr;
9102 lfg = luminance(foreground);
9103 lbg = luminance(background);
9105 if (lfg > lbg)
9106 lmax = lfg, lmin = lbg;
9107 else
9108 lmax = lbg, lmin = lfg;
9110 nr = lmax + 0.05, dr = lmin - 0.05;
9111 if (dr < 0.005 && dr > -0.005)
9112 dr += 0.01;
9114 luminosity_ratio = nr/dr;
9115 if ( luminosity_ratio < 0)
9116 luminosity_ratio *= -1.0;
9117 return (luminosity_ratio > min_contrast_ratio);
9121 static GArray*
9122 generate_nick_colors(guint numcolors, GdkRGBA background)
9124 guint i = 0, j = 0;
9125 GArray *colors = g_array_new(FALSE, FALSE, sizeof(GdkRGBA));
9126 GdkRGBA nick_highlight;
9127 GdkRGBA send_color;
9128 time_t breakout_time;
9130 gdk_rgba_parse(&nick_highlight, DEFAULT_HIGHLIGHT_COLOR);
9131 gdk_rgba_parse(&send_color, DEFAULT_SEND_COLOR);
9133 pidgin_style_adjust_contrast(NULL, &nick_highlight);
9134 pidgin_style_adjust_contrast(NULL, &send_color);
9136 srand(background.red * 65535 + background.green * 65535 + background.blue * 65535 + 1);
9138 breakout_time = time(NULL) + 3;
9140 /* first we look through the list of "good" colors: colors that differ from every other color in the
9141 * list. only some of them will differ from the background color though. lets see if we can find
9142 * numcolors of them that do
9144 while (i < numcolors && j < PIDGIN_NUM_NICK_SEED_COLORS && time(NULL) < breakout_time)
9146 GdkRGBA color = nick_seed_colors[j];
9148 if (color_is_visible(color, background, MIN_LUMINANCE_CONTRAST_RATIO) &&
9149 color_is_visible(color, nick_highlight, MIN_LUMINANCE_CONTRAST_RATIO) &&
9150 color_is_visible(color, send_color, MIN_LUMINANCE_CONTRAST_RATIO))
9152 g_array_append_val(colors, color);
9153 i++;
9155 j++;
9158 /* we might not have found numcolors in the last loop. if we did, we'll never enter this one.
9159 * if we did not, lets just find some colors that don't conflict with the background. its
9160 * expensive to find colors that not only don't conflict with the background, but also do not
9161 * conflict with each other.
9163 while(i < numcolors && time(NULL) < breakout_time)
9165 GdkRGBA color = {g_random_double_range(0, 1), g_random_double_range(0, 1), g_random_double_range(0, 1), 1};
9167 if (color_is_visible(color, background, MIN_LUMINANCE_CONTRAST_RATIO) &&
9168 color_is_visible(color, nick_highlight, MIN_LUMINANCE_CONTRAST_RATIO) &&
9169 color_is_visible(color, send_color, MIN_LUMINANCE_CONTRAST_RATIO))
9171 g_array_append_val(colors, color);
9172 i++;
9176 if (i < numcolors) {
9177 purple_debug_warning("gtkconv", "Unable to generate enough random colors before timeout. %u colors found.\n", i);
9180 if( i == 0 ) {
9181 /* To remove errors caused by an empty array. */
9182 GdkRGBA color = {0.5, 0.5, 0.5, 1.0};
9183 g_array_append_val(colors, color);
9186 return colors;
9189 /**************************************************************************
9190 * PidginConvWindow GBoxed code
9191 **************************************************************************/
9192 static PidginConvWindow *
9193 pidgin_conv_window_ref(PidginConvWindow *win)
9195 g_return_val_if_fail(win != NULL, NULL);
9197 win->box_count++;
9199 return win;
9202 static void
9203 pidgin_conv_window_unref(PidginConvWindow *win)
9205 g_return_if_fail(win != NULL);
9206 g_return_if_fail(win->box_count >= 0);
9208 if (!win->box_count--)
9209 pidgin_conv_window_destroy(win);
9212 GType
9213 pidgin_conv_window_get_type(void)
9215 static GType type = 0;
9217 if (type == 0) {
9218 type = g_boxed_type_register_static("PidginConvWindow",
9219 (GBoxedCopyFunc)pidgin_conv_window_ref,
9220 (GBoxedFreeFunc)pidgin_conv_window_unref);
9223 return type;